增加前端的代码,改编文件结构
This commit is contained in:
parent
c1bfa0de21
commit
06996967ca
18
.gitignore
vendored
18
.gitignore
vendored
@ -32,12 +32,12 @@ douyin_cdp_play_vv_*.txt
|
||||
|
||||
# Chrome profiles and drivers
|
||||
# 注意:Chrome profile 包含大量缓存文件,不应加入Git
|
||||
scripts/config/chrome_profile/
|
||||
drivers/*
|
||||
!drivers/chromedriver.exe
|
||||
backend/scripts/config/chrome_profile/
|
||||
backend/drivers/*
|
||||
!backend/drivers/chromedriver.exe
|
||||
|
||||
# Rankings config directory
|
||||
handlers/Rankings/config/
|
||||
backend/handlers/Rankings/config/
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
@ -48,6 +48,16 @@ ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Node.js
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.npm
|
||||
.yarn-integrity
|
||||
.pnp
|
||||
.pnp.js
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
48
README.md
Normal file
48
README.md
Normal file
@ -0,0 +1,48 @@
|
||||
# 榜单系统
|
||||
|
||||
这是一个全栈榜单系统项目,包含后端API服务和前端Vue3应用。
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
rank_backend/
|
||||
├── backend/ # 后端代码
|
||||
│ ├── app.py # Flask应用主文件
|
||||
│ ├── config.py # 配置文件
|
||||
│ ├── database.py # 数据库连接
|
||||
│ ├── Timer_worker.py # 定时任务
|
||||
│ ├── handlers/ # 业务处理器
|
||||
│ └── routers/ # 路由定义
|
||||
├── frontend/ # 前端Vue3应用
|
||||
│ ├── src/ # 源代码
|
||||
│ ├── public/ # 静态资源
|
||||
│ └── package.json # 依赖配置
|
||||
└── docs/ # 文档
|
||||
├── API接口文档.md
|
||||
└── requirements.txt
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 后端服务
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
python3 -m pip install -r ../docs/requirements.txt
|
||||
python3 app.py
|
||||
```
|
||||
|
||||
### 前端应用
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## 开发说明
|
||||
|
||||
- 后端使用Flask框架,提供RESTful API
|
||||
- 前端使用Vue3 + Vite构建
|
||||
- 数据库配置在backend/config.py中
|
||||
- API文档位于docs/API接口文档.md
|
||||
@ -22,7 +22,9 @@ logging.basicConfig(
|
||||
|
||||
# 导入并注册蓝图
|
||||
from routers.rank_api_routes import rank_bp
|
||||
from routers.article_routes import article_bp
|
||||
app.register_blueprint(rank_bp)
|
||||
app.register_blueprint(article_bp)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
268
backend/routers/article_routes.py
Normal file
268
backend/routers/article_routes.py
Normal file
@ -0,0 +1,268 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
文章API服务器
|
||||
提供文章列表获取和文章详情获取的接口
|
||||
"""
|
||||
|
||||
from flask import Blueprint, request, jsonify
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
from database import db
|
||||
from bson import ObjectId
|
||||
|
||||
# 创建蓝图
|
||||
article_bp = Blueprint('article', __name__, url_prefix='/api/article')
|
||||
|
||||
# 获取数据库集合
|
||||
articles_collection = db['articles']
|
||||
|
||||
def format_time(time_obj):
|
||||
"""格式化时间"""
|
||||
if not time_obj:
|
||||
return ""
|
||||
|
||||
if isinstance(time_obj, datetime):
|
||||
return time_obj.strftime("%Y-%m-%d %H:%M:%S")
|
||||
else:
|
||||
return str(time_obj)
|
||||
|
||||
def format_article_item(doc):
|
||||
"""格式化文章数据项"""
|
||||
return {
|
||||
"_id": str(doc.get("_id", "")),
|
||||
"title": doc.get("title", ""),
|
||||
"author_id": doc.get("author_id", ""),
|
||||
"cover_image": doc.get("cover_image", ""),
|
||||
"status": doc.get("status", ""),
|
||||
"summary": doc.get("summary", ""),
|
||||
"created_at": format_time(doc.get("created_at")),
|
||||
"likes": doc.get("likes", []),
|
||||
"likes_count": len(doc.get("likes", []))
|
||||
}
|
||||
|
||||
def get_article_list(page=1, limit=20, sort_by="created_at", status=None):
|
||||
"""获取文章列表(分页)"""
|
||||
try:
|
||||
# 计算跳过的数量
|
||||
skip = (page - 1) * limit
|
||||
|
||||
# 构建查询条件
|
||||
query_condition = {}
|
||||
if status:
|
||||
query_condition["status"] = status
|
||||
|
||||
# 设置排序字段
|
||||
sort_field = sort_by if sort_by in ["created_at", "title"] else "created_at"
|
||||
sort_order = -1 # 降序
|
||||
|
||||
# 查询数据
|
||||
cursor = articles_collection.find(query_condition).sort(sort_field, sort_order).skip(skip).limit(limit)
|
||||
docs = list(cursor)
|
||||
|
||||
# 获取总数
|
||||
total = articles_collection.count_documents(query_condition)
|
||||
|
||||
# 格式化数据
|
||||
article_list = []
|
||||
for doc in docs:
|
||||
item = format_article_item(doc)
|
||||
article_list.append(item)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": article_list,
|
||||
"pagination": {
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
"total": total,
|
||||
"pages": (total + limit - 1) // limit,
|
||||
"has_next": page * limit < total,
|
||||
"has_prev": page > 1
|
||||
},
|
||||
"sort_by": sort_by,
|
||||
"status_filter": status,
|
||||
"update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"获取文章列表失败: {e}")
|
||||
return {"success": False, "message": f"获取数据失败: {str(e)}"}
|
||||
|
||||
def search_articles(keyword, page=1, limit=10):
|
||||
"""搜索文章"""
|
||||
try:
|
||||
if not keyword:
|
||||
return {"success": False, "message": "请提供搜索关键词"}
|
||||
|
||||
# 计算跳过的数量
|
||||
skip = (page - 1) * limit
|
||||
|
||||
# 构建搜索条件(模糊匹配标题和内容)
|
||||
search_condition = {
|
||||
"$or": [
|
||||
{"title": {"$regex": keyword, "$options": "i"}},
|
||||
{"content": {"$regex": keyword, "$options": "i"}},
|
||||
{"summary": {"$regex": keyword, "$options": "i"}}
|
||||
]
|
||||
}
|
||||
|
||||
# 查询数据
|
||||
cursor = articles_collection.find(search_condition).sort("created_at", -1).skip(skip).limit(limit)
|
||||
docs = list(cursor)
|
||||
|
||||
# 获取搜索结果总数
|
||||
total = articles_collection.count_documents(search_condition)
|
||||
|
||||
# 格式化数据
|
||||
search_results = []
|
||||
for doc in docs:
|
||||
item = format_article_item(doc)
|
||||
search_results.append(item)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": search_results,
|
||||
"keyword": keyword,
|
||||
"pagination": {
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
"total": total,
|
||||
"pages": (total + limit - 1) // limit,
|
||||
"has_next": page * limit < total,
|
||||
"has_prev": page > 1
|
||||
},
|
||||
"update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"搜索文章失败: {e}")
|
||||
return {"success": False, "message": f"搜索失败: {str(e)}"}
|
||||
|
||||
def get_article_detail(article_id):
|
||||
"""获取文章详情"""
|
||||
try:
|
||||
# 尝试通过ObjectId查找
|
||||
try:
|
||||
doc = articles_collection.find_one({"_id": ObjectId(article_id)})
|
||||
except:
|
||||
# 如果ObjectId无效,尝试其他字段
|
||||
doc = articles_collection.find_one({
|
||||
"$or": [
|
||||
{"title": article_id},
|
||||
{"author_id": article_id}
|
||||
]
|
||||
})
|
||||
|
||||
if not doc:
|
||||
return {"success": False, "message": "未找到文章信息"}
|
||||
|
||||
# 格式化详细信息
|
||||
detail = format_article_item(doc)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": detail,
|
||||
"update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"获取文章详情失败: {e}")
|
||||
return {"success": False, "message": f"获取详情失败: {str(e)}"}
|
||||
|
||||
def get_statistics():
|
||||
"""获取统计信息"""
|
||||
try:
|
||||
# 基本统计
|
||||
total_articles = articles_collection.count_documents({})
|
||||
|
||||
if total_articles == 0:
|
||||
return {"success": False, "message": "暂无数据"}
|
||||
|
||||
# 按状态统计
|
||||
status_stats = []
|
||||
for status in ["draft", "published", "archived"]:
|
||||
count = articles_collection.count_documents({"status": status})
|
||||
status_stats.append({"status": status, "count": count})
|
||||
|
||||
# 获取最新更新时间
|
||||
latest_doc = articles_collection.find().sort("created_at", -1).limit(1)
|
||||
latest_time = ""
|
||||
if latest_doc:
|
||||
latest_list = list(latest_doc)
|
||||
if latest_list:
|
||||
latest_time = format_time(latest_list[0].get("created_at"))
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"total_articles": total_articles,
|
||||
"status_stats": status_stats,
|
||||
"latest_update": latest_time
|
||||
},
|
||||
"update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"获取统计信息失败: {e}")
|
||||
return {"success": False, "message": f"获取统计失败: {str(e)}"}
|
||||
|
||||
# 路由定义
|
||||
@article_bp.route('/list')
|
||||
def get_articles():
|
||||
"""获取文章列表"""
|
||||
page = int(request.args.get('page', 1))
|
||||
limit = int(request.args.get('limit', 20))
|
||||
sort_by = request.args.get('sort', 'created_at')
|
||||
status = request.args.get('status')
|
||||
|
||||
result = get_article_list(page, limit, sort_by, status)
|
||||
return jsonify(result)
|
||||
|
||||
@article_bp.route('/search')
|
||||
def search():
|
||||
"""搜索文章"""
|
||||
keyword = request.args.get('q', '')
|
||||
page = int(request.args.get('page', 1))
|
||||
limit = int(request.args.get('limit', 10))
|
||||
result = search_articles(keyword, page, limit)
|
||||
return jsonify(result)
|
||||
|
||||
@article_bp.route('/detail')
|
||||
def get_detail():
|
||||
"""获取文章详情"""
|
||||
article_id = request.args.get('id', '')
|
||||
result = get_article_detail(article_id)
|
||||
return jsonify(result)
|
||||
|
||||
@article_bp.route('/stats')
|
||||
def get_stats():
|
||||
"""获取统计信息"""
|
||||
result = get_statistics()
|
||||
return jsonify(result)
|
||||
|
||||
@article_bp.route('/health')
|
||||
def health_check():
|
||||
"""健康检查"""
|
||||
try:
|
||||
# 检查数据库连接
|
||||
total_records = articles_collection.count_documents({})
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": "服务正常",
|
||||
"data": {
|
||||
"database": "连接正常",
|
||||
"total_records": total_records,
|
||||
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"message": f"服务异常: {str(e)}",
|
||||
"data": {
|
||||
"database": "连接失败",
|
||||
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
}
|
||||
})
|
||||
@ -209,7 +209,7 @@ def get_mix_list(page=1, limit=20, sort_by="playcount"):
|
||||
return {"success": False, "message": f"获取数据失败: {str(e)}"}
|
||||
|
||||
def get_growth_mixes(page=1, limit=20, start_date=None, end_date=None):
|
||||
"""获取按播放量增长排序的合集列表 - 优先从定时器生成的数据中读取"""
|
||||
"""获取按播放量增长排序的合集列表 - 仅从Ranking_storage读取预计算数据"""
|
||||
try:
|
||||
# 计算跳过的数量
|
||||
skip = (page - 1) * limit
|
||||
@ -228,129 +228,101 @@ def get_growth_mixes(page=1, limit=20, start_date=None, end_date=None):
|
||||
end_date_str = end_date.strftime("%Y-%m-%d")
|
||||
start_date_str = start_date.strftime("%Y-%m-%d")
|
||||
|
||||
# 优先尝试从定时器生成的增长榜数据中读取
|
||||
try:
|
||||
# 从Ranking_storage读取预计算的增长榜数据
|
||||
growth_ranking = daily_rankings_collection.find_one({
|
||||
"date": end_date_str,
|
||||
"type": "comprehensive" # 使用comprehensive类型,包含增长数据
|
||||
}, sort=[("calculation_sequence", -1)]) # 获取最新的计算结果
|
||||
|
||||
if not growth_ranking or "data" not in growth_ranking:
|
||||
# 如果没有找到comprehensive类型,尝试查找growth类型
|
||||
growth_ranking = daily_rankings_collection.find_one({
|
||||
"date": end_date_str,
|
||||
"type": "growth",
|
||||
"start_date": start_date_str,
|
||||
"end_date": end_date_str
|
||||
}, sort=[("calculation_sequence", -1)]) # 获取最新的计算结果
|
||||
"type": "growth"
|
||||
}, sort=[("calculation_sequence", -1)])
|
||||
|
||||
if growth_ranking and "data" in growth_ranking:
|
||||
logging.info(f"📈 从定时器生成的增长榜数据中读取 {end_date_str} 的增长榜")
|
||||
|
||||
# 获取预先计算好的增长榜数据
|
||||
growth_data = growth_ranking["data"]
|
||||
|
||||
# 分页处理
|
||||
total = len(growth_data)
|
||||
paginated_data = growth_data[skip:skip + limit]
|
||||
if growth_ranking and "data" in growth_ranking:
|
||||
logging.info(f"📈 从Ranking_storage读取 {end_date_str} 的增长榜数据")
|
||||
|
||||
# 获取预先计算好的增长榜数据
|
||||
growth_data = growth_ranking["data"]
|
||||
|
||||
# 如果是comprehensive类型,需要按增长值排序
|
||||
if growth_ranking.get("type") == "comprehensive":
|
||||
# 按timeline_data中的play_vv_change排序
|
||||
growth_data = sorted(growth_data,
|
||||
key=lambda x: x.get("timeline_data", {}).get("play_vv_change", 0),
|
||||
reverse=True)
|
||||
|
||||
# 分页处理
|
||||
total = len(growth_data)
|
||||
paginated_data = growth_data[skip:skip + limit]
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": paginated_data,
|
||||
"pagination": {
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
"total": total,
|
||||
"pages": (total + limit - 1) // limit,
|
||||
"has_next": page * limit < total,
|
||||
"has_prev": page > 1
|
||||
},
|
||||
"sort_by": "growth",
|
||||
"date_range": {
|
||||
"start_date": start_date_str,
|
||||
"end_date": end_date_str
|
||||
},
|
||||
"data_source": "timer_generated", # 标识数据来源
|
||||
"update_time": growth_ranking.get("created_at", datetime.now()).strftime("%Y-%m-%d %H:%M:%S") if isinstance(growth_ranking.get("created_at"), datetime) else str(growth_ranking.get("created_at", ""))
|
||||
}
|
||||
except Exception as e:
|
||||
logging.warning(f"从定时器数据读取增长榜失败,将使用动态计算: {e}")
|
||||
# 为分页数据添加排名
|
||||
for i, item in enumerate(paginated_data):
|
||||
item["rank"] = skip + i + 1
|
||||
|
||||
# 如果定时器数据不存在或读取失败,回退到动态计算
|
||||
logging.info(f"📊 动态计算 {start_date_str} 到 {end_date_str} 的增长榜")
|
||||
|
||||
# 查询结束日期的数据
|
||||
end_cursor = collection.find({
|
||||
"batch_time": {
|
||||
"$gte": datetime(end_date.year, end_date.month, end_date.day),
|
||||
"$lt": datetime(end_date.year, end_date.month, end_date.day) + timedelta(days=1)
|
||||
return {
|
||||
"success": True,
|
||||
"data": paginated_data,
|
||||
"pagination": {
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
"total": total,
|
||||
"pages": (total + limit - 1) // limit,
|
||||
"has_next": page * limit < total,
|
||||
"has_prev": page > 1
|
||||
},
|
||||
"sort_by": "growth",
|
||||
"date_range": {
|
||||
"start_date": start_date_str,
|
||||
"end_date": end_date_str
|
||||
},
|
||||
"data_source": "ranking_storage", # 标识数据来源
|
||||
"update_time": growth_ranking.get("created_at", datetime.now()).strftime("%Y-%m-%d %H:%M:%S") if isinstance(growth_ranking.get("created_at"), datetime) else str(growth_ranking.get("created_at", ""))
|
||||
}
|
||||
})
|
||||
end_data = list(end_cursor)
|
||||
|
||||
# 查询开始日期的数据
|
||||
start_cursor = collection.find({
|
||||
"batch_time": {
|
||||
"$gte": datetime(start_date.year, start_date.month, start_date.day),
|
||||
"$lt": datetime(start_date.year, start_date.month, start_date.day) + timedelta(days=1)
|
||||
else:
|
||||
# 如果Ranking_storage中没有数据,返回空结果
|
||||
logging.warning(f"Ranking_storage中未找到 {end_date_str} 的增长榜数据")
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"暂无 {end_date_str} 的增长榜数据,请等待定时任务生成",
|
||||
"data": [],
|
||||
"pagination": {
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
"total": 0,
|
||||
"pages": 0,
|
||||
"has_next": False,
|
||||
"has_prev": False
|
||||
},
|
||||
"sort_by": "growth",
|
||||
"date_range": {
|
||||
"start_date": start_date_str,
|
||||
"end_date": end_date_str
|
||||
},
|
||||
"data_source": "ranking_storage",
|
||||
"update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
}
|
||||
})
|
||||
start_data = list(start_cursor)
|
||||
|
||||
# 创建字典以便快速查找
|
||||
end_dict = {item["mix_name"]: item for item in end_data}
|
||||
start_dict = {item["mix_name"]: item for item in start_data}
|
||||
|
||||
# 计算增长数据
|
||||
growth_data = []
|
||||
for mix_name, end_item in end_dict.items():
|
||||
if mix_name in start_dict:
|
||||
start_item = start_dict[mix_name]
|
||||
growth = end_item.get("play_vv", 0) - start_item.get("play_vv", 0)
|
||||
|
||||
# 只保留增长为正的数据
|
||||
if growth > 0:
|
||||
item = format_mix_item(end_item)
|
||||
item["growth"] = growth
|
||||
item["start_date"] = start_date_str
|
||||
item["end_date"] = end_date_str
|
||||
growth_data.append(item)
|
||||
else:
|
||||
# 如果开始日期没有数据,但结束日期有,也认为是新增长
|
||||
item = format_mix_item(end_item)
|
||||
item["growth"] = end_item.get("play_vv", 0)
|
||||
item["start_date"] = start_date_str
|
||||
item["end_date"] = end_date_str
|
||||
growth_data.append(item)
|
||||
|
||||
# 按增长值降序排序
|
||||
growth_data.sort(key=lambda x: x.get("growth", 0), reverse=True)
|
||||
|
||||
# 分页处理
|
||||
total = len(growth_data)
|
||||
paginated_data = growth_data[skip:skip + limit]
|
||||
|
||||
# 添加排名
|
||||
for i, item in enumerate(paginated_data):
|
||||
item["rank"] = skip + i + 1
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": paginated_data,
|
||||
"pagination": {
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
"total": total,
|
||||
"pages": (total + limit - 1) // limit,
|
||||
"has_next": page * limit < total,
|
||||
"has_prev": page > 1
|
||||
},
|
||||
"sort_by": "growth",
|
||||
"date_range": {
|
||||
"start_date": start_date_str,
|
||||
"end_date": end_date_str
|
||||
},
|
||||
"data_source": "dynamic_calculation", # 标识数据来源
|
||||
"update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"获取增长合集列表失败: {e}")
|
||||
# 如果增长计算失败,返回按播放量排序的数据作为备选
|
||||
return get_mix_list(page, limit, "playcount")
|
||||
# 返回错误信息,不再回退到播放量排序
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"获取增长榜数据失败: {str(e)}",
|
||||
"data": [],
|
||||
"pagination": {
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
"total": 0,
|
||||
"pages": 0,
|
||||
"has_next": False,
|
||||
"has_prev": False
|
||||
},
|
||||
"sort_by": "growth",
|
||||
"data_source": "ranking_storage"
|
||||
}
|
||||
|
||||
def get_top_mixes(limit=10):
|
||||
"""获取热门合集(TOP榜单)"""
|
||||
307
docs/API接口文档.md
307
docs/API接口文档.md
@ -2,10 +2,10 @@
|
||||
|
||||
## 概述
|
||||
|
||||
本API服务提供抖音播放量数据的查询、搜索、统计等功能,专为小程序优化设计。
|
||||
本API服务提供抖音播放量数据和文章内容的查询、搜索、统计等功能,专为小程序优化设计。
|
||||
|
||||
**基础信息**
|
||||
- 服务地址:`http://localhost:5000`
|
||||
- 服务地址:`http://localhost:5001`
|
||||
- 数据源:MongoDB数据库
|
||||
- 数据更新:每晚24:00自动更新
|
||||
- 响应格式:JSON
|
||||
@ -47,6 +47,37 @@
|
||||
}
|
||||
```
|
||||
|
||||
### 文章数据项
|
||||
```json
|
||||
{
|
||||
"_id": "68f3ad112ba085e5d676537e",
|
||||
"title": "文章标题",
|
||||
"author_id": "test_user_1",
|
||||
"cover_image": "封面图片URL",
|
||||
"status": "draft",
|
||||
"summary": "文章摘要",
|
||||
"created_at": "2025-10-18 15:06:57",
|
||||
"likes": [],
|
||||
"likes_count": 0
|
||||
}
|
||||
```
|
||||
|
||||
### 文章详情数据项
|
||||
```json
|
||||
{
|
||||
"_id": "68f3ad112ba085e5d676537e",
|
||||
"title": "文章标题",
|
||||
"content": "文章完整内容",
|
||||
"author_id": "test_user_1",
|
||||
"cover_image": "封面图片URL",
|
||||
"status": "draft",
|
||||
"summary": "文章摘要",
|
||||
"created_at": "2025-10-18 15:06:57",
|
||||
"likes": [],
|
||||
"likes_count": 0
|
||||
}
|
||||
```
|
||||
|
||||
### 分页信息
|
||||
```json
|
||||
{
|
||||
@ -95,7 +126,12 @@ GET /
|
||||
"/api/rank/rankings/dates": "获取可用榜单日期",
|
||||
"/api/rank/rankings/types": "获取榜单类型",
|
||||
"/api/rank/rankings/latest": "获取最新榜单",
|
||||
"/api/rank/rankings/stats": "获取榜单统计"
|
||||
"/api/rank/rankings/stats": "获取榜单统计",
|
||||
"/api/article/list": "获取文章列表 (支持分页和排序)",
|
||||
"/api/article/search": "搜索文章",
|
||||
"/api/article/detail": "获取文章详情",
|
||||
"/api/article/stats": "获取文章统计信息",
|
||||
"/api/article/health": "文章服务健康检查"
|
||||
},
|
||||
"features": [
|
||||
"分页支持",
|
||||
@ -105,7 +141,9 @@ GET /
|
||||
"统计分析",
|
||||
"榜单查询",
|
||||
"动态排序",
|
||||
"小程序优化"
|
||||
"小程序优化",
|
||||
"文章管理",
|
||||
"内容搜索"
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -127,8 +165,12 @@ GET /api/rank/videos
|
||||
| page | int | 否 | 1 | 页码 |
|
||||
| limit | int | 否 | 20 | 每页数量 |
|
||||
| sort | string | 否 | playcount | 排序方式:playcount(播放量) / growth(增长量) |
|
||||
| start_date | string | 否 | 昨天 | 增长计算开始日期(格式: YYYY-MM-DD) |
|
||||
| end_date | string | 否 | 今天 | 增长计算结束日期(格式: YYYY-MM-DD) |
|
||||
| start_date | string | 否 | 昨天 | 增长计算开始日期(格式: YYYY-MM-DD),仅在sort=growth时有效 |
|
||||
| end_date | string | 否 | 今天 | 增长计算结束日期(格式: YYYY-MM-DD),仅在sort=growth时有效 |
|
||||
|
||||
**数据源说明**
|
||||
- `sort=playcount`:从Rankings_list集合动态查询当日播放量数据
|
||||
- `sort=growth`:从Ranking_storage集合读取预计算的增长排名数据(由定时任务生成)
|
||||
|
||||
**使用示例**
|
||||
```
|
||||
@ -157,7 +199,9 @@ GET /api/rank/videos?page=1&limit=20&sort=growth&start_date=2025-10-16&end_date=
|
||||
"request_id": "request_xxx",
|
||||
"rank": 1,
|
||||
"cover_image_url": "https://p3.douyinpic.com/xxx",
|
||||
"cover_backup_urls": ["url1", "url2"]
|
||||
"cover_backup_urls": ["url1", "url2"],
|
||||
"growth": 5000000,
|
||||
"growth_rate": 4.35
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
@ -168,7 +212,8 @@ GET /api/rank/videos?page=1&limit=20&sort=growth&start_date=2025-10-16&end_date=
|
||||
"has_next": true,
|
||||
"has_prev": false
|
||||
},
|
||||
"sort_by": "playcount",
|
||||
"sort_by": "growth",
|
||||
"data_source": "ranking_storage",
|
||||
"update_time": "2025-10-17 15:30:00"
|
||||
}
|
||||
```
|
||||
@ -619,9 +664,199 @@ GET /api/rank/rankings/stats
|
||||
### 常见错误
|
||||
- `数据库连接失败`:MongoDB连接异常
|
||||
- `未找到合集信息`:查询的合集不存在
|
||||
- `未找到文章信息`:查询的文章不存在
|
||||
- `请提供搜索关键词`:搜索接口缺少关键词参数
|
||||
- `获取数据失败`:数据查询异常
|
||||
|
||||
## 文章管理接口
|
||||
|
||||
### 1. 获取文章列表
|
||||
|
||||
**接口地址**
|
||||
```
|
||||
GET /api/article/list
|
||||
```
|
||||
|
||||
**功能描述**
|
||||
获取文章列表,支持分页和排序
|
||||
|
||||
**请求参数**
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|--------|------|------|--------|------|
|
||||
| page | int | 否 | 1 | 页码 |
|
||||
| limit | int | 否 | 10 | 每页数量 |
|
||||
| sort | string | 否 | created_at | 排序字段 |
|
||||
| order | string | 否 | desc | 排序方向 (asc/desc) |
|
||||
|
||||
**响应示例**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"_id": "68f3ad112ba085e5d676537e",
|
||||
"title": "对于ai影视化的思考",
|
||||
"author_id": "test_user_1",
|
||||
"cover_image": "",
|
||||
"status": "draft",
|
||||
"summary": "AI影视化的发展趋势和思考",
|
||||
"created_at": "2025-10-18 15:06:57",
|
||||
"likes": [],
|
||||
"likes_count": 0
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"current_page": 1,
|
||||
"total_pages": 1,
|
||||
"total_items": 1,
|
||||
"has_next": false,
|
||||
"has_prev": false
|
||||
},
|
||||
"message": "获取文章列表成功",
|
||||
"update_time": "2025-10-18 15:06:57"
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 搜索文章
|
||||
|
||||
**接口地址**
|
||||
```
|
||||
GET /api/article/search
|
||||
```
|
||||
|
||||
**功能描述**
|
||||
根据关键词搜索文章
|
||||
|
||||
**请求参数**
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|--------|------|------|--------|------|
|
||||
| q | string | 是 | - | 搜索关键词 |
|
||||
| page | int | 否 | 1 | 页码 |
|
||||
| limit | int | 否 | 10 | 每页数量 |
|
||||
|
||||
**响应示例**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"_id": "68f3ad112ba085e5d676537e",
|
||||
"title": "对于ai影视化的思考",
|
||||
"author_id": "test_user_1",
|
||||
"cover_image": "",
|
||||
"status": "draft",
|
||||
"summary": "AI影视化的发展趋势和思考",
|
||||
"created_at": "2025-10-18 15:06:57",
|
||||
"likes": [],
|
||||
"likes_count": 0
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"current_page": 1,
|
||||
"total_pages": 1,
|
||||
"total_items": 1,
|
||||
"has_next": false,
|
||||
"has_prev": false
|
||||
},
|
||||
"message": "搜索文章成功",
|
||||
"update_time": "2025-10-18 15:06:57"
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 获取文章详情
|
||||
|
||||
**接口地址**
|
||||
```
|
||||
GET /api/article/detail
|
||||
```
|
||||
|
||||
**功能描述**
|
||||
根据文章ID获取文章详细信息
|
||||
|
||||
**请求参数**
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|--------|------|------|--------|------|
|
||||
| id | string | 是 | - | 文章ID |
|
||||
|
||||
**响应示例**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"_id": "68f3ad112ba085e5d676537e",
|
||||
"title": "对于ai影视化的思考",
|
||||
"content": "# AI影视化的思考\n\n在2025年,AI技术在影视行业的应用...",
|
||||
"author_id": "test_user_1",
|
||||
"cover_image": "",
|
||||
"status": "draft",
|
||||
"summary": "AI影视化的发展趋势和思考",
|
||||
"created_at": "2025-10-18 15:06:57",
|
||||
"likes": [],
|
||||
"likes_count": 0
|
||||
},
|
||||
"message": "获取文章详情成功",
|
||||
"update_time": "2025-10-18 15:06:57"
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 获取文章统计信息
|
||||
|
||||
**接口地址**
|
||||
```
|
||||
GET /api/article/stats
|
||||
```
|
||||
|
||||
**功能描述**
|
||||
获取文章相关的统计信息
|
||||
|
||||
**请求参数**
|
||||
无
|
||||
|
||||
**响应示例**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"total_articles": 1,
|
||||
"latest_update": "2025-10-18 15:06:57",
|
||||
"status_count": {
|
||||
"draft": 1,
|
||||
"published": 0,
|
||||
"archived": 0
|
||||
}
|
||||
},
|
||||
"message": "获取文章统计成功",
|
||||
"update_time": "2025-10-18 15:06:57"
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 文章服务健康检查
|
||||
|
||||
**接口地址**
|
||||
```
|
||||
GET /api/article/health
|
||||
```
|
||||
|
||||
**功能描述**
|
||||
检查文章服务和数据库连接状态
|
||||
|
||||
**请求参数**
|
||||
无
|
||||
|
||||
**响应示例**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"status": "服务正常",
|
||||
"database": "连接正常",
|
||||
"article_count": 1
|
||||
},
|
||||
"message": "文章服务健康检查通过",
|
||||
"update_time": "2025-10-18 15:06:57"
|
||||
}
|
||||
```
|
||||
|
||||
## 小程序使用建议
|
||||
|
||||
### 1. 分页加载
|
||||
@ -661,17 +896,61 @@ wx.request({
|
||||
- 合理使用缓存策略
|
||||
- 定期检查服务健康状态
|
||||
|
||||
### 5. 文章管理
|
||||
推荐的文章列表加载方式:
|
||||
```javascript
|
||||
// 小程序端示例 - 文章列表
|
||||
wx.request({
|
||||
url: 'http://localhost:5001/api/article/list',
|
||||
data: {
|
||||
page: 1,
|
||||
limit: 10,
|
||||
sort: 'created_at',
|
||||
order: 'desc'
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.data.success) {
|
||||
this.setData({
|
||||
articles: res.data.data,
|
||||
hasNext: res.data.pagination.has_next
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
文章搜索示例:
|
||||
```javascript
|
||||
// 小程序端示例 - 文章搜索
|
||||
wx.request({
|
||||
url: 'http://localhost:5001/api/article/search',
|
||||
data: {
|
||||
q: 'AI',
|
||||
page: 1,
|
||||
limit: 5
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.data.success) {
|
||||
this.setData({
|
||||
searchResults: res.data.data
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## 部署说明
|
||||
|
||||
### 启动服务
|
||||
```bash
|
||||
cd C:\Users\EDY\Desktop\rank_backend
|
||||
python app.py
|
||||
cd /Users/binghuixiong/剧变AI/rank_backend
|
||||
python3 app.py
|
||||
```
|
||||
|
||||
### 服务信息
|
||||
- 端口:5000
|
||||
- 端口:5001
|
||||
- 数据库:MongoDB (localhost:27017)
|
||||
- 数据库名:kemeng_media
|
||||
- 数据更新:每晚24:00自动执行
|
||||
|
||||
### 注意事项
|
||||
@ -681,7 +960,7 @@ python app.py
|
||||
|
||||
---
|
||||
|
||||
**文档版本**:v3.0
|
||||
**最后更新**:2025-01-17
|
||||
**文档版本**:v4.1
|
||||
**最后更新**:2025-01-18
|
||||
**维护者**:系统自动生成
|
||||
**更新内容**:新增榜单查询相关API接口,更新所有接口路径为/api/rank前缀
|
||||
**更新内容**:优化增长排名接口数据源策略,现在专门使用Ranking_storage预计算数据,提升查询性能和数据一致性
|
||||
30
frontend/.gitignore
vendored
Normal file
30
frontend/.gitignore
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
*.tsbuildinfo
|
||||
38
frontend/README.md
Normal file
38
frontend/README.md
Normal file
@ -0,0 +1,38 @@
|
||||
# .
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
||||
|
||||
## Recommended Browser Setup
|
||||
|
||||
- Chromium-based browsers (Chrome, Edge, Brave, etc.):
|
||||
- [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)
|
||||
- [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters)
|
||||
- Firefox:
|
||||
- [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)
|
||||
- [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/)
|
||||
|
||||
## Customize configuration
|
||||
|
||||
See [Vite Configuration Reference](https://vite.dev/config/).
|
||||
|
||||
## Project Setup
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compile and Hot-Reload for Development
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Compile and Minify for Production
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
14
frontend/index.html
Normal file
14
frontend/index.html
Normal file
@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
|
||||
<title>Vite App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
8
frontend/jsconfig.json
Normal file
8
frontend/jsconfig.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
4295
frontend/package-lock.json
generated
Normal file
4295
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
frontend/package.json
Normal file
25
frontend/package.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "rank",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.12.2",
|
||||
"bootstrap": "^5.3.0-alpha1",
|
||||
"bootstrap-icons": "^1.13.1",
|
||||
"vue": "^3.5.22"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"vite": "^7.1.7",
|
||||
"vite-plugin-vue-devtools": "^8.0.2"
|
||||
}
|
||||
}
|
||||
BIN
frontend/public/favicon.ico
Normal file
BIN
frontend/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
5
frontend/public/placeholder-poster.svg
Normal file
5
frontend/public/placeholder-poster.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg width="60" height="80" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="60" height="80" fill="#f0f0f0" stroke="#ddd" stroke-width="1"/>
|
||||
<text x="30" y="35" text-anchor="middle" font-family="Arial" font-size="8" fill="#999">暂无</text>
|
||||
<text x="30" y="50" text-anchor="middle" font-family="Arial" font-size="8" fill="#999">图片</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 355 B |
559
frontend/src/App.vue
Normal file
559
frontend/src/App.vue
Normal file
@ -0,0 +1,559 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import axios from 'axios'
|
||||
|
||||
// 响应式数据
|
||||
const currentTab = ref('ranking') // 'ranking' 或 'news'
|
||||
const rankingData = ref([])
|
||||
const loading = ref(false)
|
||||
const selectedDate = ref('')
|
||||
const currentPage = ref(1)
|
||||
const totalPages = ref(1)
|
||||
|
||||
// 初始化日期为今天
|
||||
const initDate = () => {
|
||||
const today = new Date()
|
||||
selectedDate.value = today.toISOString().split('T')[0]
|
||||
}
|
||||
|
||||
// 获取排行榜数据
|
||||
const fetchRankingData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await axios.get('http://localhost:5001/api/rank/videos', {
|
||||
params: {
|
||||
page: currentPage.value,
|
||||
limit: 20,
|
||||
sort: 'growth',
|
||||
start_date: selectedDate.value,
|
||||
end_date: selectedDate.value
|
||||
}
|
||||
})
|
||||
|
||||
if (response.data.success) {
|
||||
rankingData.value = response.data.data
|
||||
totalPages.value = response.data.pagination.pages
|
||||
} else {
|
||||
console.error('获取数据失败:', response.data.message)
|
||||
rankingData.value = []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('API调用失败:', error)
|
||||
rankingData.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前时间
|
||||
const getCurrentTime = () => {
|
||||
const now = new Date()
|
||||
return `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
const getRankClass = (rank) => {
|
||||
if (rank === 1) return 'rank-first'
|
||||
if (rank === 2) return 'rank-second'
|
||||
if (rank === 3) return 'rank-third'
|
||||
return 'rank-normal'
|
||||
}
|
||||
|
||||
// 格式化播放量
|
||||
const formatPlayCount = (count) => {
|
||||
if (!count) return '0'
|
||||
if (count >= 100000000) {
|
||||
return (count / 100000000).toFixed(1) + '亿'
|
||||
} else if (count >= 10000) {
|
||||
return (count / 10000).toFixed(1) + '万'
|
||||
}
|
||||
return count.toString()
|
||||
}
|
||||
|
||||
// 格式化增长数据
|
||||
const formatGrowth = (item) => {
|
||||
const timelineData = item.timeline_data || {}
|
||||
const change = timelineData.play_vv_change || 0
|
||||
const changeRate = timelineData.play_vv_change_rate || 0
|
||||
|
||||
if (change > 0) {
|
||||
return `+${formatPlayCount(change)} (${changeRate.toFixed(1)}%)`
|
||||
}
|
||||
return '暂无数据'
|
||||
}
|
||||
|
||||
// 切换标签页
|
||||
const switchTab = (tab) => {
|
||||
currentTab.value = tab
|
||||
if (tab === 'ranking') {
|
||||
fetchRankingData()
|
||||
}
|
||||
}
|
||||
|
||||
// 日期改变处理
|
||||
const onDateChange = () => {
|
||||
currentPage.value = 1
|
||||
fetchRankingData()
|
||||
}
|
||||
|
||||
// 页面加载时初始化
|
||||
onMounted(() => {
|
||||
initDate()
|
||||
fetchRankingData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app">
|
||||
<!-- 主内容区域 -->
|
||||
<div class="main-content">
|
||||
<!-- 排行榜页面 -->
|
||||
<div v-if="currentTab === 'ranking'" class="ranking-page">
|
||||
<!-- 标题 -->
|
||||
<div class="header">
|
||||
<div class="title-container">
|
||||
<span class="lightning-icon">⚡</span>
|
||||
<h1 class="title">热播总榜</h1>
|
||||
<span class="lightning-icon">⚡</span>
|
||||
</div>
|
||||
<div class="update-time">
|
||||
基于实时热度排行 {{ getCurrentTime() }}更新
|
||||
<span class="refresh-icon">🔄</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 日期选择 -->
|
||||
<div class="date-selector">
|
||||
<label for="date-input">选择日期:</label>
|
||||
<input
|
||||
id="date-input"
|
||||
type="date"
|
||||
v-model="selectedDate"
|
||||
@change="onDateChange"
|
||||
class="date-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="loading">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>加载中...</p>
|
||||
</div>
|
||||
|
||||
<!-- 榜单内容 -->
|
||||
<div v-else class="ranking-list">
|
||||
<div
|
||||
v-for="(item, index) in rankingData"
|
||||
:key="item._id || index"
|
||||
class="ranking-item"
|
||||
>
|
||||
<!-- 排名 -->
|
||||
<div class="rank-number" :class="getRankClass(index + 1)">
|
||||
{{ index + 1 }}
|
||||
</div>
|
||||
|
||||
<!-- 海报 -->
|
||||
<div class="poster">
|
||||
<img
|
||||
:src="item.cover_image_url || '/placeholder-poster.svg'"
|
||||
:alt="item.title || item.mix_name"
|
||||
@error="$event.target.src='/placeholder-poster.svg'"
|
||||
class="poster-img"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 内容信息 -->
|
||||
<div class="content-info">
|
||||
<!-- 剧名 -->
|
||||
<h3 class="drama-name">{{ item.title || item.mix_name || '未知剧名' }}</h3>
|
||||
|
||||
<!-- 当前播放量 -->
|
||||
<div class="play-count">
|
||||
<span class="play-label">总播放量:</span>
|
||||
<span class="play-value">{{ formatPlayCount(item.play_vv) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 简介(省略显示) -->
|
||||
<div class="description">
|
||||
{{ item.summary || item.title || item.mix_name || '暂无简介' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 增长数据 -->
|
||||
<div class="growth-data">
|
||||
<span class="growth-icon">🔥</span>
|
||||
<span class="growth-number">{{ formatGrowth(item) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="rankingData.length === 0" class="empty-state">
|
||||
<p>暂无排行榜数据</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 资讯页面占位 -->
|
||||
<div v-else class="news-page">
|
||||
<div class="header">
|
||||
<h1 class="title">资讯中心</h1>
|
||||
</div>
|
||||
<div class="coming-soon">
|
||||
<p>资讯功能即将上线...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部导航 -->
|
||||
<div class="bottom-nav">
|
||||
<div
|
||||
class="nav-item"
|
||||
:class="{ active: currentTab === 'news' }"
|
||||
@click="switchTab('news')"
|
||||
>
|
||||
<i class="bi bi-newspaper"></i>
|
||||
<span class="nav-text">资讯</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="nav-item"
|
||||
:class="{ active: currentTab === 'ranking' }"
|
||||
@click="switchTab('ranking')"
|
||||
>
|
||||
<i class="bi bi-list-stars"></i>
|
||||
<span class="nav-text">排行榜</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 全局样式 */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.app {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
padding-bottom: 80px; /* 为底部导航留出空间 */
|
||||
color: white;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 主内容区域 */
|
||||
.main-content {
|
||||
padding: 20px 16px 80px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* 标题区域 */
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.title-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.lightning-icon {
|
||||
font-size: 20px;
|
||||
color: #ffd700;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.update-time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.refresh-icon {
|
||||
font-size: 12px;
|
||||
color: #4CAF50;
|
||||
}
|
||||
|
||||
/* 日期选择器 */
|
||||
.date-selector {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
padding: 15px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.date-selector label {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.date-input {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
border: 2px solid #e1e5e9;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.date-input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid rgba(255,255,255,0.3);
|
||||
border-top: 4px solid white;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 15px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 榜单列表 */
|
||||
.ranking-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.ranking-item {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 16px;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 15px;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.ranking-item:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
/* 排名数字 */
|
||||
.rank-number {
|
||||
background: linear-gradient(135deg, #ff6b6b, #ee5a24);
|
||||
color: white;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 2px 8px rgba(255,107,107,0.3);
|
||||
}
|
||||
|
||||
/* 海报 */
|
||||
.poster {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.poster-img {
|
||||
width: 60px;
|
||||
height: 80px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
/* 内容信息 */
|
||||
.content-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.drama-name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #2c3e50;
|
||||
margin: 0 0 8px 0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.growth-info, .play-count {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.growth-label, .play-label {
|
||||
color: #7f8c8d;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.growth-value {
|
||||
color: #e74c3c;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.play-value {
|
||||
color: #3498db;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: #7f8c8d;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: rgba(255,255,255,0.8);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* 资讯页面 */
|
||||
.news-page .coming-soon {
|
||||
text-align: center;
|
||||
padding: 100px 20px;
|
||||
color: rgba(255,255,255,0.8);
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
/* 底部导航 */
|
||||
.bottom-nav {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
display: flex;
|
||||
border-top: 1px solid rgba(0,0,0,0.1);
|
||||
box-shadow: 0 -2px 20px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 12px 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
color: #7f8c8d;
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
color: #667eea;
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
background: rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
font-size: 20px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.nav-text {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 移动端适配 */
|
||||
@media (max-width: 768px) {
|
||||
.main-content {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.ranking-item {
|
||||
padding: 12px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.poster-img {
|
||||
width: 50px;
|
||||
height: 67px;
|
||||
}
|
||||
|
||||
.drama-name {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.date-selector {
|
||||
padding: 12px;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.date-input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.main-content {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.ranking-item {
|
||||
padding: 10px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.poster-img {
|
||||
width: 45px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.rank-number {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
4
frontend/src/main.js
Normal file
4
frontend/src/main.js
Normal file
@ -0,0 +1,4 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
18
frontend/vite.config.js
Normal file
18
frontend/vite.config.js
Normal file
@ -0,0 +1,18 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
vueDevTools(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
},
|
||||
},
|
||||
})
|
||||
54
package-lock.json
generated
Normal file
54
package-lock.json
generated
Normal file
@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "rank_backend",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"bootstrap": "^5.3.0-alpha1"
|
||||
}
|
||||
},
|
||||
"node_modules/@popperjs/core": {
|
||||
"version": "2.11.8",
|
||||
"resolved": "https://registry.npmmirror.com/@popperjs/core/-/core-2.11.8.tgz",
|
||||
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/popperjs"
|
||||
}
|
||||
},
|
||||
"node_modules/bootstrap": {
|
||||
"version": "5.3.0-alpha1",
|
||||
"resolved": "https://registry.npmmirror.com/bootstrap/-/bootstrap-5.3.0-alpha1.tgz",
|
||||
"integrity": "sha512-ABZpKK4ObS3kKlIqH+ZVDqoy5t/bhFG0oHTAzByUdon7YIom0lpCeTqRniDzJmbtcWkNe800VVPBiJgxSYTYew==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/twbs"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/bootstrap"
|
||||
}
|
||||
],
|
||||
"peerDependencies": {
|
||||
"@popperjs/core": "^2.11.6"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@popperjs/core": {
|
||||
"version": "2.11.8",
|
||||
"resolved": "https://registry.npmmirror.com/@popperjs/core/-/core-2.11.8.tgz",
|
||||
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
|
||||
"peer": true
|
||||
},
|
||||
"bootstrap": {
|
||||
"version": "5.3.0-alpha1",
|
||||
"resolved": "https://registry.npmmirror.com/bootstrap/-/bootstrap-5.3.0-alpha1.tgz",
|
||||
"integrity": "sha512-ABZpKK4ObS3kKlIqH+ZVDqoy5t/bhFG0oHTAzByUdon7YIom0lpCeTqRniDzJmbtcWkNe800VVPBiJgxSYTYew==",
|
||||
"requires": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
5
package.json
Normal file
5
package.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"bootstrap": "^5.3.0-alpha1"
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user