增加前端的代码,改编文件结构
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 profiles and drivers
|
||||||
# 注意:Chrome profile 包含大量缓存文件,不应加入Git
|
# 注意:Chrome profile 包含大量缓存文件,不应加入Git
|
||||||
scripts/config/chrome_profile/
|
backend/scripts/config/chrome_profile/
|
||||||
drivers/*
|
backend/drivers/*
|
||||||
!drivers/chromedriver.exe
|
!backend/drivers/chromedriver.exe
|
||||||
|
|
||||||
# Rankings config directory
|
# Rankings config directory
|
||||||
handlers/Rankings/config/
|
backend/handlers/Rankings/config/
|
||||||
|
|
||||||
# Environment variables
|
# Environment variables
|
||||||
.env
|
.env
|
||||||
@ -48,6 +48,16 @@ ENV/
|
|||||||
env.bak/
|
env.bak/
|
||||||
venv.bak/
|
venv.bak/
|
||||||
|
|
||||||
|
# Node.js
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.npm
|
||||||
|
.yarn-integrity
|
||||||
|
.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
# IDE
|
# IDE
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.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.rank_api_routes import rank_bp
|
||||||
|
from routers.article_routes import article_bp
|
||||||
app.register_blueprint(rank_bp)
|
app.register_blueprint(rank_bp)
|
||||||
|
app.register_blueprint(article_bp)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
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)}"}
|
return {"success": False, "message": f"获取数据失败: {str(e)}"}
|
||||||
|
|
||||||
def get_growth_mixes(page=1, limit=20, start_date=None, end_date=None):
|
def get_growth_mixes(page=1, limit=20, start_date=None, end_date=None):
|
||||||
"""获取按播放量增长排序的合集列表 - 优先从定时器生成的数据中读取"""
|
"""获取按播放量增长排序的合集列表 - 仅从Ranking_storage读取预计算数据"""
|
||||||
try:
|
try:
|
||||||
# 计算跳过的数量
|
# 计算跳过的数量
|
||||||
skip = (page - 1) * limit
|
skip = (page - 1) * limit
|
||||||
@ -228,102 +228,37 @@ def get_growth_mixes(page=1, limit=20, start_date=None, end_date=None):
|
|||||||
end_date_str = end_date.strftime("%Y-%m-%d")
|
end_date_str = end_date.strftime("%Y-%m-%d")
|
||||||
start_date_str = start_date.strftime("%Y-%m-%d")
|
start_date_str = start_date.strftime("%Y-%m-%d")
|
||||||
|
|
||||||
# 优先尝试从定时器生成的增长榜数据中读取
|
# 从Ranking_storage读取预计算的增长榜数据
|
||||||
try:
|
|
||||||
growth_ranking = daily_rankings_collection.find_one({
|
growth_ranking = daily_rankings_collection.find_one({
|
||||||
"date": end_date_str,
|
"date": end_date_str,
|
||||||
"type": "growth",
|
"type": "comprehensive" # 使用comprehensive类型,包含增长数据
|
||||||
"start_date": start_date_str,
|
|
||||||
"end_date": end_date_str
|
|
||||||
}, sort=[("calculation_sequence", -1)]) # 获取最新的计算结果
|
}, 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"
|
||||||
|
}, sort=[("calculation_sequence", -1)])
|
||||||
|
|
||||||
if growth_ranking and "data" in growth_ranking:
|
if growth_ranking and "data" in growth_ranking:
|
||||||
logging.info(f"📈 从定时器生成的增长榜数据中读取 {end_date_str} 的增长榜")
|
logging.info(f"📈 从Ranking_storage读取 {end_date_str} 的增长榜数据")
|
||||||
|
|
||||||
# 获取预先计算好的增长榜数据
|
# 获取预先计算好的增长榜数据
|
||||||
growth_data = growth_ranking["data"]
|
growth_data = growth_ranking["data"]
|
||||||
|
|
||||||
# 分页处理
|
# 如果是comprehensive类型,需要按增长值排序
|
||||||
total = len(growth_data)
|
if growth_ranking.get("type") == "comprehensive":
|
||||||
paginated_data = growth_data[skip:skip + limit]
|
# 按timeline_data中的play_vv_change排序
|
||||||
|
growth_data = sorted(growth_data,
|
||||||
return {
|
key=lambda x: x.get("timeline_data", {}).get("play_vv_change", 0),
|
||||||
"success": True,
|
reverse=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}")
|
|
||||||
|
|
||||||
# 如果定时器数据不存在或读取失败,回退到动态计算
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
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)
|
total = len(growth_data)
|
||||||
paginated_data = growth_data[skip:skip + limit]
|
paginated_data = growth_data[skip:skip + limit]
|
||||||
|
|
||||||
# 添加排名
|
# 为分页数据添加排名
|
||||||
for i, item in enumerate(paginated_data):
|
for i, item in enumerate(paginated_data):
|
||||||
item["rank"] = skip + i + 1
|
item["rank"] = skip + i + 1
|
||||||
|
|
||||||
@ -343,14 +278,51 @@ def get_growth_mixes(page=1, limit=20, start_date=None, end_date=None):
|
|||||||
"start_date": start_date_str,
|
"start_date": start_date_str,
|
||||||
"end_date": end_date_str
|
"end_date": end_date_str
|
||||||
},
|
},
|
||||||
"data_source": "dynamic_calculation", # 标识数据来源
|
"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", ""))
|
||||||
|
}
|
||||||
|
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")
|
"update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"获取增长合集列表失败: {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):
|
def get_top_mixes(limit=10):
|
||||||
"""获取热门合集(TOP榜单)"""
|
"""获取热门合集(TOP榜单)"""
|
||||||
307
docs/API接口文档.md
307
docs/API接口文档.md
@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
## 概述
|
## 概述
|
||||||
|
|
||||||
本API服务提供抖音播放量数据的查询、搜索、统计等功能,专为小程序优化设计。
|
本API服务提供抖音播放量数据和文章内容的查询、搜索、统计等功能,专为小程序优化设计。
|
||||||
|
|
||||||
**基础信息**
|
**基础信息**
|
||||||
- 服务地址:`http://localhost:5000`
|
- 服务地址:`http://localhost:5001`
|
||||||
- 数据源:MongoDB数据库
|
- 数据源:MongoDB数据库
|
||||||
- 数据更新:每晚24:00自动更新
|
- 数据更新:每晚24:00自动更新
|
||||||
- 响应格式:JSON
|
- 响应格式: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
|
```json
|
||||||
{
|
{
|
||||||
@ -95,7 +126,12 @@ GET /
|
|||||||
"/api/rank/rankings/dates": "获取可用榜单日期",
|
"/api/rank/rankings/dates": "获取可用榜单日期",
|
||||||
"/api/rank/rankings/types": "获取榜单类型",
|
"/api/rank/rankings/types": "获取榜单类型",
|
||||||
"/api/rank/rankings/latest": "获取最新榜单",
|
"/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": [
|
"features": [
|
||||||
"分页支持",
|
"分页支持",
|
||||||
@ -105,7 +141,9 @@ GET /
|
|||||||
"统计分析",
|
"统计分析",
|
||||||
"榜单查询",
|
"榜单查询",
|
||||||
"动态排序",
|
"动态排序",
|
||||||
"小程序优化"
|
"小程序优化",
|
||||||
|
"文章管理",
|
||||||
|
"内容搜索"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -127,8 +165,12 @@ GET /api/rank/videos
|
|||||||
| page | int | 否 | 1 | 页码 |
|
| page | int | 否 | 1 | 页码 |
|
||||||
| limit | int | 否 | 20 | 每页数量 |
|
| limit | int | 否 | 20 | 每页数量 |
|
||||||
| sort | string | 否 | playcount | 排序方式:playcount(播放量) / growth(增长量) |
|
| sort | string | 否 | playcount | 排序方式:playcount(播放量) / growth(增长量) |
|
||||||
| start_date | string | 否 | 昨天 | 增长计算开始日期(格式: YYYY-MM-DD) |
|
| start_date | string | 否 | 昨天 | 增长计算开始日期(格式: YYYY-MM-DD),仅在sort=growth时有效 |
|
||||||
| end_date | string | 否 | 今天 | 增长计算结束日期(格式: YYYY-MM-DD) |
|
| 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",
|
"request_id": "request_xxx",
|
||||||
"rank": 1,
|
"rank": 1,
|
||||||
"cover_image_url": "https://p3.douyinpic.com/xxx",
|
"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": {
|
"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_next": true,
|
||||||
"has_prev": false
|
"has_prev": false
|
||||||
},
|
},
|
||||||
"sort_by": "playcount",
|
"sort_by": "growth",
|
||||||
|
"data_source": "ranking_storage",
|
||||||
"update_time": "2025-10-17 15:30:00"
|
"update_time": "2025-10-17 15:30:00"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -619,9 +664,199 @@ GET /api/rank/rankings/stats
|
|||||||
### 常见错误
|
### 常见错误
|
||||||
- `数据库连接失败`:MongoDB连接异常
|
- `数据库连接失败`: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. 分页加载
|
### 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
|
```bash
|
||||||
cd C:\Users\EDY\Desktop\rank_backend
|
cd /Users/binghuixiong/剧变AI/rank_backend
|
||||||
python app.py
|
python3 app.py
|
||||||
```
|
```
|
||||||
|
|
||||||
### 服务信息
|
### 服务信息
|
||||||
- 端口:5000
|
- 端口:5001
|
||||||
- 数据库:MongoDB (localhost:27017)
|
- 数据库:MongoDB (localhost:27017)
|
||||||
|
- 数据库名:kemeng_media
|
||||||
- 数据更新:每晚24:00自动执行
|
- 数据更新:每晚24:00自动执行
|
||||||
|
|
||||||
### 注意事项
|
### 注意事项
|
||||||
@ -681,7 +960,7 @@ python app.py
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**文档版本**:v3.0
|
**文档版本**:v4.1
|
||||||
**最后更新**:2025-01-17
|
**最后更新**: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