mirror of
https://github.com/zhinianboke/xianyu-auto-reply.git
synced 2025-08-31 01:38:48 +08:00
优化商品详情逻辑
优化商品详情逻辑
This commit is contained in:
commit
a9302527f8
@ -119,6 +119,10 @@ class XianyuLive:
|
|||||||
# 记录订单详情锁的使用时间
|
# 记录订单详情锁的使用时间
|
||||||
_order_detail_lock_times = {}
|
_order_detail_lock_times = {}
|
||||||
|
|
||||||
|
# 商品详情缓存(24小时有效)
|
||||||
|
_item_detail_cache = {} # {item_id: {'detail': str, 'timestamp': float}}
|
||||||
|
_item_detail_cache_lock = asyncio.Lock()
|
||||||
|
|
||||||
def _safe_str(self, e):
|
def _safe_str(self, e):
|
||||||
"""安全地将异常转换为字符串"""
|
"""安全地将异常转换为字符串"""
|
||||||
try:
|
try:
|
||||||
@ -864,7 +868,7 @@ class XianyuLive:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
async def fetch_item_detail_from_api(self, item_id: str) -> str:
|
async def fetch_item_detail_from_api(self, item_id: str) -> str:
|
||||||
"""从外部API获取商品详情
|
"""获取商品详情(优先使用浏览器,备用外部API,支持24小时缓存)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
item_id: 商品ID
|
item_id: 商品ID
|
||||||
@ -881,6 +885,181 @@ class XianyuLive:
|
|||||||
logger.debug(f"自动获取商品详情功能已禁用: {item_id}")
|
logger.debug(f"自动获取商品详情功能已禁用: {item_id}")
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
# 1. 首先检查缓存(24小时有效)
|
||||||
|
async with self._item_detail_cache_lock:
|
||||||
|
if item_id in self._item_detail_cache:
|
||||||
|
cache_data = self._item_detail_cache[item_id]
|
||||||
|
cache_time = cache_data['timestamp']
|
||||||
|
current_time = time.time()
|
||||||
|
|
||||||
|
# 检查缓存是否在24小时内
|
||||||
|
if current_time - cache_time < 24 * 60 * 60: # 24小时
|
||||||
|
logger.info(f"从缓存获取商品详情: {item_id}")
|
||||||
|
return cache_data['detail']
|
||||||
|
else:
|
||||||
|
# 缓存过期,删除
|
||||||
|
del self._item_detail_cache[item_id]
|
||||||
|
logger.debug(f"缓存已过期,删除: {item_id}")
|
||||||
|
|
||||||
|
# 2. 尝试使用浏览器获取商品详情
|
||||||
|
detail_from_browser = await self._fetch_item_detail_from_browser(item_id)
|
||||||
|
if detail_from_browser:
|
||||||
|
# 保存到缓存
|
||||||
|
async with self._item_detail_cache_lock:
|
||||||
|
self._item_detail_cache[item_id] = {
|
||||||
|
'detail': detail_from_browser,
|
||||||
|
'timestamp': time.time()
|
||||||
|
}
|
||||||
|
logger.info(f"成功通过浏览器获取商品详情: {item_id}, 长度: {len(detail_from_browser)}")
|
||||||
|
return detail_from_browser
|
||||||
|
|
||||||
|
# 3. 浏览器获取失败,使用外部API作为备用
|
||||||
|
logger.warning(f"浏览器获取商品详情失败,尝试外部API: {item_id}")
|
||||||
|
detail_from_api = await self._fetch_item_detail_from_external_api(item_id)
|
||||||
|
if detail_from_api:
|
||||||
|
# 保存到缓存
|
||||||
|
async with self._item_detail_cache_lock:
|
||||||
|
self._item_detail_cache[item_id] = {
|
||||||
|
'detail': detail_from_api,
|
||||||
|
'timestamp': time.time()
|
||||||
|
}
|
||||||
|
logger.info(f"成功通过外部API获取商品详情: {item_id}, 长度: {len(detail_from_api)}")
|
||||||
|
return detail_from_api
|
||||||
|
|
||||||
|
logger.warning(f"所有方式都无法获取商品详情: {item_id}")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取商品详情异常: {item_id}, 错误: {self._safe_str(e)}")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
async def _fetch_item_detail_from_browser(self, item_id: str) -> str:
|
||||||
|
"""使用浏览器获取商品详情"""
|
||||||
|
try:
|
||||||
|
from playwright.async_api import async_playwright
|
||||||
|
|
||||||
|
logger.info(f"开始使用浏览器获取商品详情: {item_id}")
|
||||||
|
|
||||||
|
playwright = await async_playwright().start()
|
||||||
|
|
||||||
|
# 启动浏览器(参照order_detail_fetcher的配置)
|
||||||
|
browser_args = [
|
||||||
|
'--no-sandbox',
|
||||||
|
'--disable-setuid-sandbox',
|
||||||
|
'--disable-dev-shm-usage',
|
||||||
|
'--disable-accelerated-2d-canvas',
|
||||||
|
'--no-first-run',
|
||||||
|
'--no-zygote',
|
||||||
|
'--disable-gpu',
|
||||||
|
'--disable-background-timer-throttling',
|
||||||
|
'--disable-backgrounding-occluded-windows',
|
||||||
|
'--disable-renderer-backgrounding',
|
||||||
|
'--disable-features=TranslateUI',
|
||||||
|
'--disable-ipc-flooding-protection',
|
||||||
|
'--disable-extensions',
|
||||||
|
'--disable-default-apps',
|
||||||
|
'--disable-sync',
|
||||||
|
'--disable-translate',
|
||||||
|
'--hide-scrollbars',
|
||||||
|
'--mute-audio',
|
||||||
|
'--no-default-browser-check',
|
||||||
|
'--no-pings'
|
||||||
|
]
|
||||||
|
|
||||||
|
# 在Docker环境中添加额外参数
|
||||||
|
if os.getenv('DOCKER_ENV'):
|
||||||
|
browser_args.extend([
|
||||||
|
'--single-process',
|
||||||
|
'--disable-background-networking',
|
||||||
|
'--disable-client-side-phishing-detection',
|
||||||
|
'--disable-hang-monitor',
|
||||||
|
'--disable-popup-blocking',
|
||||||
|
'--disable-prompt-on-repost',
|
||||||
|
'--disable-web-resources',
|
||||||
|
'--metrics-recording-only',
|
||||||
|
'--safebrowsing-disable-auto-update',
|
||||||
|
'--enable-automation',
|
||||||
|
'--password-store=basic',
|
||||||
|
'--use-mock-keychain'
|
||||||
|
])
|
||||||
|
|
||||||
|
browser = await playwright.chromium.launch(
|
||||||
|
headless=True,
|
||||||
|
args=browser_args
|
||||||
|
)
|
||||||
|
|
||||||
|
# 创建浏览器上下文
|
||||||
|
context = await browser.new_context(
|
||||||
|
viewport={'width': 1920, 'height': 1080},
|
||||||
|
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36'
|
||||||
|
)
|
||||||
|
|
||||||
|
# 设置Cookie
|
||||||
|
cookies = []
|
||||||
|
for cookie_pair in self.cookies_str.split('; '):
|
||||||
|
if '=' in cookie_pair:
|
||||||
|
name, value = cookie_pair.split('=', 1)
|
||||||
|
cookies.append({
|
||||||
|
'name': name.strip(),
|
||||||
|
'value': value.strip(),
|
||||||
|
'domain': '.goofish.com',
|
||||||
|
'path': '/'
|
||||||
|
})
|
||||||
|
|
||||||
|
await context.add_cookies(cookies)
|
||||||
|
logger.debug(f"已设置 {len(cookies)} 个Cookie")
|
||||||
|
|
||||||
|
# 创建页面
|
||||||
|
page = await context.new_page()
|
||||||
|
|
||||||
|
# 构造商品详情页面URL
|
||||||
|
item_url = f"https://www.goofish.com/item?id={item_id}"
|
||||||
|
logger.info(f"访问商品页面: {item_url}")
|
||||||
|
|
||||||
|
# 访问页面
|
||||||
|
await page.goto(item_url, wait_until='networkidle', timeout=30000)
|
||||||
|
|
||||||
|
# 等待页面完全加载
|
||||||
|
await asyncio.sleep(3)
|
||||||
|
|
||||||
|
# 获取商品详情内容
|
||||||
|
try:
|
||||||
|
# 等待目标元素出现
|
||||||
|
await page.wait_for_selector('.desc--GaIUKUQY', timeout=10000)
|
||||||
|
|
||||||
|
# 获取商品详情文本
|
||||||
|
detail_element = await page.query_selector('.desc--GaIUKUQY')
|
||||||
|
if detail_element:
|
||||||
|
detail_text = await detail_element.inner_text()
|
||||||
|
logger.info(f"成功获取商品详情: {item_id}, 长度: {len(detail_text)}")
|
||||||
|
|
||||||
|
# 清理资源
|
||||||
|
await browser.close()
|
||||||
|
await playwright.stop()
|
||||||
|
|
||||||
|
return detail_text.strip()
|
||||||
|
else:
|
||||||
|
logger.warning(f"未找到商品详情元素: {item_id}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"获取商品详情元素失败: {item_id}, 错误: {self._safe_str(e)}")
|
||||||
|
|
||||||
|
# 清理资源
|
||||||
|
await browser.close()
|
||||||
|
await playwright.stop()
|
||||||
|
|
||||||
|
return ""
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"浏览器获取商品详情异常: {item_id}, 错误: {self._safe_str(e)}")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
async def _fetch_item_detail_from_external_api(self, item_id: str) -> str:
|
||||||
|
"""从外部API获取商品详情(备用方案)"""
|
||||||
|
try:
|
||||||
|
from config import config
|
||||||
|
auto_fetch_config = config.get('ITEM_DETAIL', {}).get('auto_fetch', {})
|
||||||
|
|
||||||
# 从配置获取API地址和超时时间
|
# 从配置获取API地址和超时时间
|
||||||
api_base_url = auto_fetch_config.get('api_url', 'https://selfapi.zhinianboke.com/api/getItemDetail')
|
api_base_url = auto_fetch_config.get('api_url', 'https://selfapi.zhinianboke.com/api/getItemDetail')
|
||||||
timeout_seconds = auto_fetch_config.get('timeout', 10)
|
timeout_seconds = auto_fetch_config.get('timeout', 10)
|
||||||
@ -891,7 +1070,6 @@ class XianyuLive:
|
|||||||
|
|
||||||
# 使用aiohttp发送异步请求
|
# 使用aiohttp发送异步请求
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import asyncio
|
|
||||||
|
|
||||||
timeout = aiohttp.ClientTimeout(total=timeout_seconds)
|
timeout = aiohttp.ClientTimeout(total=timeout_seconds)
|
||||||
|
|
||||||
@ -903,21 +1081,20 @@ class XianyuLive:
|
|||||||
# 检查返回状态
|
# 检查返回状态
|
||||||
if result.get('status') == '200' and result.get('data'):
|
if result.get('status') == '200' and result.get('data'):
|
||||||
item_detail = result['data']
|
item_detail = result['data']
|
||||||
logger.info(f"成功获取商品详情: {item_id}, 长度: {len(item_detail)}")
|
logger.info(f"外部API成功获取商品详情: {item_id}, 长度: {len(item_detail)}")
|
||||||
logger.debug(f"商品详情内容: {item_detail[:200]}...")
|
|
||||||
return item_detail
|
return item_detail
|
||||||
else:
|
else:
|
||||||
logger.warning(f"API返回状态异常: {result.get('status')}, message: {result.get('message')}")
|
logger.warning(f"外部API返回状态异常: {result.get('status')}, message: {result.get('message')}")
|
||||||
return ""
|
return ""
|
||||||
else:
|
else:
|
||||||
logger.warning(f"API请求失败: HTTP {response.status}")
|
logger.warning(f"外部API请求失败: HTTP {response.status}")
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
logger.warning(f"获取商品详情超时: {item_id}")
|
logger.warning(f"外部API获取商品详情超时: {item_id}")
|
||||||
return ""
|
return ""
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"获取商品详情异常: {item_id}, 错误: {self._safe_str(e)}")
|
logger.error(f"外部API获取商品详情异常: {item_id}, 错误: {self._safe_str(e)}")
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
async def save_items_list_to_db(self, items_list):
|
async def save_items_list_to_db(self, items_list):
|
||||||
|
@ -444,51 +444,18 @@ async def admin_page():
|
|||||||
return HTMLResponse(f.read())
|
return HTMLResponse(f.read())
|
||||||
|
|
||||||
|
|
||||||
# 用户管理页面路由
|
|
||||||
@app.get('/user_management.html', response_class=HTMLResponse)
|
|
||||||
async def user_management_page():
|
|
||||||
page_path = os.path.join(static_dir, 'user_management.html')
|
|
||||||
if os.path.exists(page_path):
|
|
||||||
with open(page_path, 'r', encoding='utf-8') as f:
|
|
||||||
return HTMLResponse(f.read())
|
|
||||||
else:
|
|
||||||
return HTMLResponse('<h3>User management page not found</h3>')
|
|
||||||
|
|
||||||
|
|
||||||
# 日志管理页面路由
|
|
||||||
@app.get('/log_management.html', response_class=HTMLResponse)
|
|
||||||
async def log_management_page():
|
|
||||||
page_path = os.path.join(static_dir, 'log_management.html')
|
|
||||||
if os.path.exists(page_path):
|
|
||||||
with open(page_path, 'r', encoding='utf-8') as f:
|
|
||||||
return HTMLResponse(f.read())
|
|
||||||
else:
|
|
||||||
return HTMLResponse('<h3>Log management page not found</h3>')
|
|
||||||
|
|
||||||
|
|
||||||
# 数据管理页面路由
|
|
||||||
@app.get('/data_management.html', response_class=HTMLResponse)
|
|
||||||
async def data_management_page():
|
|
||||||
page_path = os.path.join(static_dir, 'data_management.html')
|
|
||||||
if os.path.exists(page_path):
|
|
||||||
with open(page_path, 'r', encoding='utf-8') as f:
|
|
||||||
return HTMLResponse(f.read())
|
|
||||||
else:
|
|
||||||
return HTMLResponse('<h3>Data management page not found</h3>')
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# 商品搜索页面路由
|
|
||||||
@app.get('/item_search.html', response_class=HTMLResponse)
|
|
||||||
async def item_search_page():
|
|
||||||
page_path = os.path.join(static_dir, 'item_search.html')
|
|
||||||
if os.path.exists(page_path):
|
|
||||||
with open(page_path, 'r', encoding='utf-8') as f:
|
|
||||||
return HTMLResponse(f.read())
|
|
||||||
else:
|
|
||||||
return HTMLResponse('<h3>Item search page not found</h3>')
|
|
||||||
|
|
||||||
|
|
||||||
# 登录接口
|
# 登录接口
|
||||||
@ -2884,32 +2851,6 @@ async def search_multiple_pages(
|
|||||||
raise HTTPException(status_code=500, detail=f"多页商品搜索失败: {error_msg}")
|
raise HTTPException(status_code=500, detail=f"多页商品搜索失败: {error_msg}")
|
||||||
|
|
||||||
|
|
||||||
@app.get("/items/detail/{item_id}")
|
|
||||||
async def get_public_item_detail(
|
|
||||||
item_id: str,
|
|
||||||
current_user: Optional[Dict[str, Any]] = Depends(get_current_user_optional)
|
|
||||||
):
|
|
||||||
"""获取公开商品详情(通过外部API)"""
|
|
||||||
try:
|
|
||||||
from utils.item_search import get_item_detail_from_api
|
|
||||||
|
|
||||||
# 从外部API获取商品详情
|
|
||||||
detail = await get_item_detail_from_api(item_id)
|
|
||||||
|
|
||||||
if detail:
|
|
||||||
return {
|
|
||||||
"success": True,
|
|
||||||
"data": detail
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
raise HTTPException(status_code=404, detail="商品详情获取失败")
|
|
||||||
|
|
||||||
except HTTPException:
|
|
||||||
raise
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"获取商品详情失败: {str(e)}")
|
|
||||||
raise HTTPException(status_code=500, detail=f"获取商品详情失败: {str(e)}")
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/items/cookie/{cookie_id}")
|
@app.get("/items/cookie/{cookie_id}")
|
||||||
def get_items_by_cookie(cookie_id: str, current_user: Dict[str, Any] = Depends(get_current_user)):
|
def get_items_by_cookie(cookie_id: str, current_user: Dict[str, Any] = Depends(get_current_user)):
|
||||||
|
@ -1,556 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>数据管理 - 闲鱼自动回复系统</title>
|
|
||||||
<link href="/static/lib/bootstrap/bootstrap.min.css" rel="stylesheet">
|
|
||||||
<link href="/static/lib/bootstrap-icons/bootstrap-icons.css" rel="stylesheet">
|
|
||||||
<style>
|
|
||||||
.admin-header {
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
color: white;
|
|
||||||
padding: 1rem 0;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
.table-container {
|
|
||||||
max-height: 600px;
|
|
||||||
overflow-y: auto;
|
|
||||||
border: 1px solid #dee2e6;
|
|
||||||
border-radius: 0.375rem;
|
|
||||||
}
|
|
||||||
.table th {
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
background: #f8f9fa;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
.btn-delete {
|
|
||||||
padding: 0.25rem 0.5rem;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
}
|
|
||||||
.table-info {
|
|
||||||
background: #e3f2fd;
|
|
||||||
border-left: 4px solid #2196f3;
|
|
||||||
padding: 1rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
.data-stats {
|
|
||||||
background: linear-gradient(135deg, #4caf50 0%, #45a049 100%);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<!-- 导航栏 -->
|
|
||||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
|
||||||
<div class="container">
|
|
||||||
<a class="navbar-brand" href="/">
|
|
||||||
<i class="bi bi-robot"></i> 闲鱼自动回复系统
|
|
||||||
</a>
|
|
||||||
<div class="navbar-nav ms-auto">
|
|
||||||
<a class="nav-link" href="/">
|
|
||||||
<i class="bi bi-house"></i> 首页
|
|
||||||
</a>
|
|
||||||
<a class="nav-link" href="/user_management.html">
|
|
||||||
<i class="bi bi-people"></i> 用户管理
|
|
||||||
</a>
|
|
||||||
<a class="nav-link active" href="/data_management.html">
|
|
||||||
<i class="bi bi-database"></i> 数据管理
|
|
||||||
</a>
|
|
||||||
<a class="nav-link" href="/log_management.html">
|
|
||||||
<i class="bi bi-file-text"></i> 日志管理
|
|
||||||
</a>
|
|
||||||
<a class="nav-link" href="#" onclick="logout()">
|
|
||||||
<i class="bi bi-box-arrow-right"></i> 退出
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<!-- 管理员标题 -->
|
|
||||||
<div class="admin-header">
|
|
||||||
<div class="container">
|
|
||||||
<h1 class="mb-0">
|
|
||||||
<i class="bi bi-database"></i> 数据管理
|
|
||||||
</h1>
|
|
||||||
<p class="mb-0 mt-2">查看和管理数据库中的所有表数据</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
<!-- 表选择器 -->
|
|
||||||
<div class="card mb-4">
|
|
||||||
<div class="card-header">
|
|
||||||
<h5 class="mb-0">
|
|
||||||
<i class="bi bi-table"></i> 数据表选择
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="row align-items-center">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label">选择数据表</label>
|
|
||||||
<select class="form-select" id="tableSelect" onchange="loadTableData()">
|
|
||||||
<option value="">请选择数据表...</option>
|
|
||||||
<option value="users">users - 用户表</option>
|
|
||||||
<option value="cookies">cookies - Cookie账号表</option>
|
|
||||||
<option value="cookie_status">cookie_status - Cookie状态表</option>
|
|
||||||
<option value="keywords">keywords - 关键字表</option>
|
|
||||||
<option value="default_replies">default_replies - 默认回复表</option>
|
|
||||||
<option value="item_replay">item_replay - 指定商品回复表</option>
|
|
||||||
<option value="default_reply_records">default_reply_records - 默认回复记录表</option>
|
|
||||||
<option value="ai_reply_settings">ai_reply_settings - AI回复设置表</option>
|
|
||||||
<option value="ai_conversations">ai_conversations - AI对话历史表</option>
|
|
||||||
<option value="ai_item_cache">ai_item_cache - AI商品信息缓存表</option>
|
|
||||||
<option value="item_info">item_info - 商品信息表</option>
|
|
||||||
<option value="message_notifications">message_notifications - 消息通知表</option>
|
|
||||||
<option value="cards">cards - 卡券表</option>
|
|
||||||
<option value="delivery_rules">delivery_rules - 发货规则表</option>
|
|
||||||
<option value="notification_channels">notification_channels - 通知渠道表</option>
|
|
||||||
<option value="user_settings">user_settings - 用户设置表</option>
|
|
||||||
<option value="system_settings">system_settings - 系统设置表</option>
|
|
||||||
<option value="email_verifications">email_verifications - 邮箱验证表</option>
|
|
||||||
<option value="captcha_codes">captcha_codes - 验证码表</option>
|
|
||||||
<option value="orders">orders - 订单表</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label class="form-label">数据统计</label>
|
|
||||||
<div class="card data-stats">
|
|
||||||
<div class="card-body text-center py-2">
|
|
||||||
<h5 id="recordCount" class="mb-0">-</h5>
|
|
||||||
<small>条记录</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label class="form-label"> </label>
|
|
||||||
<div class="d-grid">
|
|
||||||
<button class="btn btn-primary" onclick="refreshTableData()">
|
|
||||||
<i class="bi bi-arrow-clockwise"></i> 刷新数据
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 表信息 -->
|
|
||||||
<div id="tableInfo" class="table-info" style="display: none;">
|
|
||||||
<h6 class="mb-2">
|
|
||||||
<i class="bi bi-info-circle"></i> 表信息
|
|
||||||
</h6>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-4">
|
|
||||||
<strong>表名:</strong> <span id="tableName">-</span>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<strong>中文名:</strong> <span id="tableDescription">-</span>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<strong>记录数:</strong> <span id="tableRecordCount">-</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 数据表格 -->
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
|
||||||
<h5 class="mb-0">
|
|
||||||
<i class="bi bi-table"></i> 数据内容
|
|
||||||
</h5>
|
|
||||||
<div class="d-flex gap-2">
|
|
||||||
<button class="btn btn-sm btn-outline-warning" onclick="confirmDeleteAll()" style="display: none;" id="deleteAllBtn">
|
|
||||||
<i class="bi bi-trash"></i> 清空表
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-body p-0">
|
|
||||||
<div id="loadingData" class="text-center py-4" style="display: none;">
|
|
||||||
<div class="spinner-border" role="status">
|
|
||||||
<span class="visually-hidden">加载中...</span>
|
|
||||||
</div>
|
|
||||||
<p class="mt-2">正在加载数据...</p>
|
|
||||||
</div>
|
|
||||||
<div id="noTableSelected" class="text-center py-4">
|
|
||||||
<i class="bi bi-table" style="font-size: 3rem; color: #ccc;"></i>
|
|
||||||
<p class="mt-2 text-muted">请选择要查看的数据表</p>
|
|
||||||
</div>
|
|
||||||
<div id="noData" class="text-center py-4" style="display: none;">
|
|
||||||
<i class="bi bi-inbox" style="font-size: 3rem; color: #ccc;"></i>
|
|
||||||
<p class="mt-2 text-muted">该表暂无数据</p>
|
|
||||||
</div>
|
|
||||||
<div id="tableContainer" class="table-container" style="display: none;">
|
|
||||||
<table class="table table-striped table-hover mb-0">
|
|
||||||
<thead id="tableHead"></thead>
|
|
||||||
<tbody id="tableBody"></tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 删除确认模态框 -->
|
|
||||||
<div class="modal fade" id="deleteModal" tabindex="-1">
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title">
|
|
||||||
<i class="bi bi-exclamation-triangle text-warning"></i> 确认删除
|
|
||||||
</h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<p>您确定要删除这条记录吗?</p>
|
|
||||||
<div class="alert alert-warning">
|
|
||||||
<i class="bi bi-exclamation-triangle"></i>
|
|
||||||
<strong>警告:</strong>此操作不可恢复!
|
|
||||||
</div>
|
|
||||||
<div id="deleteRecordInfo" class="bg-light p-2 rounded">
|
|
||||||
<!-- 记录信息将在这里显示 -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
|
||||||
<button type="button" class="btn btn-danger" onclick="confirmDelete()">
|
|
||||||
<i class="bi bi-trash"></i> 确认删除
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 清空表确认模态框 -->
|
|
||||||
<div class="modal fade" id="deleteAllModal" tabindex="-1">
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title">
|
|
||||||
<i class="bi bi-exclamation-triangle text-danger"></i> 确认清空表
|
|
||||||
</h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<p>您确定要清空整个表的所有数据吗?</p>
|
|
||||||
<div class="alert alert-danger">
|
|
||||||
<i class="bi bi-exclamation-triangle"></i>
|
|
||||||
<strong>危险操作:</strong>这将删除表中的所有记录,此操作不可恢复!
|
|
||||||
</div>
|
|
||||||
<p>表名: <strong id="deleteAllTableName">-</strong></p>
|
|
||||||
<p>记录数: <strong id="deleteAllRecordCount">-</strong> 条</p>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
|
||||||
<button type="button" class="btn btn-danger" onclick="confirmDeleteAll()">
|
|
||||||
<i class="bi bi-trash"></i> 确认清空
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="/static/lib/bootstrap/bootstrap.bundle.min.js"></script>
|
|
||||||
<script>
|
|
||||||
let currentTable = '';
|
|
||||||
let currentData = [];
|
|
||||||
let deleteModal = null;
|
|
||||||
let deleteAllModal = null;
|
|
||||||
let currentDeleteId = null;
|
|
||||||
|
|
||||||
// 表的中文描述
|
|
||||||
const tableDescriptions = {
|
|
||||||
'users': '用户表',
|
|
||||||
'cookies': 'Cookie账号表',
|
|
||||||
'cookie_status': 'Cookie状态表',
|
|
||||||
'keywords': '关键字表',
|
|
||||||
'item_replay': '指定商品回复表',
|
|
||||||
'default_replies': '默认回复表',
|
|
||||||
'default_reply_records': '默认回复记录表',
|
|
||||||
'ai_reply_settings': 'AI回复设置表',
|
|
||||||
'ai_conversations': 'AI对话历史表',
|
|
||||||
'ai_item_cache': 'AI商品信息缓存表',
|
|
||||||
'item_info': '商品信息表',
|
|
||||||
'message_notifications': '消息通知表',
|
|
||||||
'cards': '卡券表',
|
|
||||||
'delivery_rules': '发货规则表',
|
|
||||||
'notification_channels': '通知渠道表',
|
|
||||||
'user_settings': '用户设置表',
|
|
||||||
'system_settings': '系统设置表',
|
|
||||||
'email_verifications': '邮箱验证表',
|
|
||||||
'captcha_codes': '验证码表',
|
|
||||||
'orders': '订单表'
|
|
||||||
};
|
|
||||||
|
|
||||||
// 页面加载完成后初始化
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
// 检查管理员权限
|
|
||||||
checkAdminPermission();
|
|
||||||
|
|
||||||
// 初始化模态框
|
|
||||||
deleteModal = new bootstrap.Modal(document.getElementById('deleteModal'));
|
|
||||||
deleteAllModal = new bootstrap.Modal(document.getElementById('deleteAllModal'));
|
|
||||||
});
|
|
||||||
|
|
||||||
// 检查管理员权限
|
|
||||||
function checkAdminPermission() {
|
|
||||||
const token = localStorage.getItem('auth_token');
|
|
||||||
if (!token) {
|
|
||||||
alert('请先登录');
|
|
||||||
window.location.href = '/login.html';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证token并检查是否为管理员
|
|
||||||
fetch('/verify', {
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${token}`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
if (!data.authenticated) {
|
|
||||||
alert('登录已过期,请重新登录');
|
|
||||||
window.location.href = '/login.html';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否为管理员
|
|
||||||
if (data.username !== 'admin') {
|
|
||||||
alert('此功能仅限管理员使用');
|
|
||||||
window.location.href = '/';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('权限验证失败:', error);
|
|
||||||
alert('权限验证失败');
|
|
||||||
window.location.href = '/login.html';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载表数据
|
|
||||||
function loadTableData() {
|
|
||||||
const tableSelect = document.getElementById('tableSelect');
|
|
||||||
const selectedTable = tableSelect.value;
|
|
||||||
|
|
||||||
if (!selectedTable) {
|
|
||||||
showNoTableSelected();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentTable = selectedTable;
|
|
||||||
showLoading();
|
|
||||||
|
|
||||||
const token = localStorage.getItem('auth_token');
|
|
||||||
|
|
||||||
fetch(`/admin/data/${selectedTable}`, {
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${token}`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.success) {
|
|
||||||
currentData = data.data;
|
|
||||||
displayTableData(data.data, data.columns);
|
|
||||||
updateTableInfo(selectedTable, data.data.length);
|
|
||||||
} else {
|
|
||||||
alert('加载数据失败: ' + data.message);
|
|
||||||
showNoData();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('加载数据失败:', error);
|
|
||||||
alert('加载数据失败');
|
|
||||||
showNoData();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 刷新表数据
|
|
||||||
function refreshTableData() {
|
|
||||||
if (currentTable) {
|
|
||||||
loadTableData();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示表数据
|
|
||||||
function displayTableData(data, columns) {
|
|
||||||
const tableHead = document.getElementById('tableHead');
|
|
||||||
const tableBody = document.getElementById('tableBody');
|
|
||||||
|
|
||||||
// 清空表格
|
|
||||||
tableHead.innerHTML = '';
|
|
||||||
tableBody.innerHTML = '';
|
|
||||||
|
|
||||||
if (!data || data.length === 0) {
|
|
||||||
showNoData();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建表头
|
|
||||||
const headerRow = document.createElement('tr');
|
|
||||||
columns.forEach(column => {
|
|
||||||
const th = document.createElement('th');
|
|
||||||
th.textContent = column;
|
|
||||||
headerRow.appendChild(th);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 添加操作列
|
|
||||||
const actionTh = document.createElement('th');
|
|
||||||
actionTh.textContent = '操作';
|
|
||||||
actionTh.style.width = '100px';
|
|
||||||
headerRow.appendChild(actionTh);
|
|
||||||
|
|
||||||
tableHead.appendChild(headerRow);
|
|
||||||
|
|
||||||
// 创建表格数据
|
|
||||||
data.forEach((row, index) => {
|
|
||||||
const tr = document.createElement('tr');
|
|
||||||
|
|
||||||
columns.forEach(column => {
|
|
||||||
const td = document.createElement('td');
|
|
||||||
let value = row[column];
|
|
||||||
|
|
||||||
// 处理特殊值
|
|
||||||
if (value === null || value === undefined) {
|
|
||||||
value = '-';
|
|
||||||
} else if (typeof value === 'string' && value.length > 50) {
|
|
||||||
value = value.substring(0, 50) + '...';
|
|
||||||
}
|
|
||||||
|
|
||||||
td.textContent = value;
|
|
||||||
tr.appendChild(td);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 添加操作按钮
|
|
||||||
const actionTd = document.createElement('td');
|
|
||||||
const deleteBtn = document.createElement('button');
|
|
||||||
deleteBtn.className = 'btn btn-danger btn-delete';
|
|
||||||
deleteBtn.innerHTML = '<i class="bi bi-trash"></i>';
|
|
||||||
deleteBtn.onclick = () => deleteRecord(row, index);
|
|
||||||
actionTd.appendChild(deleteBtn);
|
|
||||||
tr.appendChild(actionTd);
|
|
||||||
|
|
||||||
tableBody.appendChild(tr);
|
|
||||||
});
|
|
||||||
|
|
||||||
showTableContainer();
|
|
||||||
updateRecordCount(data.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新表信息
|
|
||||||
function updateTableInfo(tableName, recordCount) {
|
|
||||||
document.getElementById('tableName').textContent = tableName;
|
|
||||||
document.getElementById('tableDescription').textContent = tableDescriptions[tableName] || '未知表';
|
|
||||||
document.getElementById('tableRecordCount').textContent = recordCount;
|
|
||||||
document.getElementById('tableInfo').style.display = 'block';
|
|
||||||
|
|
||||||
// 显示/隐藏清空表按钮
|
|
||||||
const deleteAllBtn = document.getElementById('deleteAllBtn');
|
|
||||||
if (recordCount > 0) {
|
|
||||||
deleteAllBtn.style.display = 'inline-block';
|
|
||||||
} else {
|
|
||||||
deleteAllBtn.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新记录数
|
|
||||||
function updateRecordCount(count) {
|
|
||||||
document.getElementById('recordCount').textContent = count;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示状态函数
|
|
||||||
function showLoading() {
|
|
||||||
document.getElementById('loadingData').style.display = 'block';
|
|
||||||
document.getElementById('noTableSelected').style.display = 'none';
|
|
||||||
document.getElementById('noData').style.display = 'none';
|
|
||||||
document.getElementById('tableContainer').style.display = 'none';
|
|
||||||
document.getElementById('tableInfo').style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
function showNoTableSelected() {
|
|
||||||
document.getElementById('loadingData').style.display = 'none';
|
|
||||||
document.getElementById('noTableSelected').style.display = 'block';
|
|
||||||
document.getElementById('noData').style.display = 'none';
|
|
||||||
document.getElementById('tableContainer').style.display = 'none';
|
|
||||||
document.getElementById('tableInfo').style.display = 'none';
|
|
||||||
updateRecordCount('-');
|
|
||||||
}
|
|
||||||
|
|
||||||
function showNoData() {
|
|
||||||
document.getElementById('loadingData').style.display = 'none';
|
|
||||||
document.getElementById('noTableSelected').style.display = 'none';
|
|
||||||
document.getElementById('noData').style.display = 'block';
|
|
||||||
document.getElementById('tableContainer').style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
function showTableContainer() {
|
|
||||||
document.getElementById('loadingData').style.display = 'none';
|
|
||||||
document.getElementById('noTableSelected').style.display = 'none';
|
|
||||||
document.getElementById('noData').style.display = 'none';
|
|
||||||
document.getElementById('tableContainer').style.display = 'block';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 删除记录
|
|
||||||
function deleteRecord(record, index) {
|
|
||||||
currentDeleteId = record.id || record.user_id || index;
|
|
||||||
|
|
||||||
// 显示记录信息
|
|
||||||
const deleteRecordInfo = document.getElementById('deleteRecordInfo');
|
|
||||||
deleteRecordInfo.innerHTML = '';
|
|
||||||
|
|
||||||
Object.keys(record).forEach(key => {
|
|
||||||
const div = document.createElement('div');
|
|
||||||
div.innerHTML = `<strong>${key}:</strong> ${record[key] || '-'}`;
|
|
||||||
deleteRecordInfo.appendChild(div);
|
|
||||||
});
|
|
||||||
|
|
||||||
deleteModal.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确认删除
|
|
||||||
function confirmDelete() {
|
|
||||||
if (!currentDeleteId || !currentTable) return;
|
|
||||||
|
|
||||||
const token = localStorage.getItem('auth_token');
|
|
||||||
|
|
||||||
fetch(`/admin/data/${currentTable}/${currentDeleteId}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${token}`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
deleteModal.hide();
|
|
||||||
if (data.success) {
|
|
||||||
alert('删除成功');
|
|
||||||
loadTableData(); // 重新加载数据
|
|
||||||
} else {
|
|
||||||
alert('删除失败: ' + data.message);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('删除失败:', error);
|
|
||||||
alert('删除失败');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确认清空表
|
|
||||||
function confirmDeleteAll() {
|
|
||||||
if (!currentTable) return;
|
|
||||||
|
|
||||||
document.getElementById('deleteAllTableName').textContent = currentTable;
|
|
||||||
document.getElementById('deleteAllRecordCount').textContent = currentData.length;
|
|
||||||
deleteAllModal.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 退出登录
|
|
||||||
function logout() {
|
|
||||||
localStorage.removeItem('auth_token');
|
|
||||||
window.location.href = '/login.html';
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,954 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>商品搜索 - 闲鱼管理系统</title>
|
|
||||||
<link href="/static/lib/bootstrap/bootstrap.min.css" rel="stylesheet">
|
|
||||||
<link href="/static/lib/bootstrap-icons/bootstrap-icons.css" rel="stylesheet">
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
}
|
|
||||||
.navbar {
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
}
|
|
||||||
.card {
|
|
||||||
border: none;
|
|
||||||
border-radius: 15px;
|
|
||||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
.search-form {
|
|
||||||
background: white;
|
|
||||||
padding: 30px;
|
|
||||||
border-radius: 15px;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
}
|
|
||||||
.item-card {
|
|
||||||
transition: transform 0.2s;
|
|
||||||
}
|
|
||||||
.item-card:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
.item-image {
|
|
||||||
width: 100%;
|
|
||||||
height: 200px;
|
|
||||||
object-fit: cover;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
.price {
|
|
||||||
color: #e74c3c;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 1.2em;
|
|
||||||
}
|
|
||||||
.seller-name {
|
|
||||||
color: #6c757d;
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
.tags {
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
.tag {
|
|
||||||
background-color: #e9ecef;
|
|
||||||
color: #495057;
|
|
||||||
padding: 2px 8px;
|
|
||||||
border-radius: 12px;
|
|
||||||
font-size: 0.8em;
|
|
||||||
margin-right: 5px;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
.loading {
|
|
||||||
text-align: center;
|
|
||||||
padding: 50px;
|
|
||||||
}
|
|
||||||
.no-results {
|
|
||||||
text-align: center;
|
|
||||||
padding: 50px;
|
|
||||||
color: #6c757d;
|
|
||||||
}
|
|
||||||
.pagination-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
margin-top: 30px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<!-- 导航栏 -->
|
|
||||||
<nav class="navbar navbar-expand-lg navbar-dark">
|
|
||||||
<div class="container">
|
|
||||||
<a class="navbar-brand" href="/index.html">
|
|
||||||
<i class="bi bi-search me-2"></i>
|
|
||||||
商品搜索
|
|
||||||
</a>
|
|
||||||
<div class="navbar-nav ms-auto">
|
|
||||||
<a class="nav-link" href="/index.html">
|
|
||||||
<i class="bi bi-house"></i> 返回主页
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div class="container mt-4">
|
|
||||||
<!-- 加载提示 -->
|
|
||||||
<div id="loadingAuth" class="text-center py-5">
|
|
||||||
<div class="spinner-border text-primary" role="status">
|
|
||||||
<span class="visually-hidden">验证登录状态...</span>
|
|
||||||
</div>
|
|
||||||
<p class="mt-3">正在验证登录状态...</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 主要内容(初始隐藏) -->
|
|
||||||
<div id="mainContent" style="display: none;">
|
|
||||||
<!-- 搜索表单 -->
|
|
||||||
<div class="search-form">
|
|
||||||
<h4 class="mb-4">
|
|
||||||
<i class="bi bi-search me-2"></i>
|
|
||||||
闲鱼商品搜索
|
|
||||||
</h4>
|
|
||||||
<div class="alert alert-info mb-4">
|
|
||||||
<i class="bi bi-info-circle me-2"></i>
|
|
||||||
<strong>功能说明:</strong>
|
|
||||||
<ul class="mb-0 mt-2">
|
|
||||||
<li><strong>查询总页数:</strong>输入要获取的页数(1-20页),系统会一次性获取所有页面的数据</li>
|
|
||||||
<li><strong>每页显示:</strong>前端分页显示的每页商品数量</li>
|
|
||||||
<li><strong>数据来源:</strong>使用真实的闲鱼数据,通过浏览器自动化技术获取</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<form id="searchForm">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-5">
|
|
||||||
<label for="keyword" class="form-label">搜索关键词</label>
|
|
||||||
<input type="text" class="form-control" id="keyword" placeholder="请输入商品关键词" required>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label for="totalPages" class="form-label">查询总页数</label>
|
|
||||||
<input type="number" class="form-control" id="totalPages" value="1" min="1" max="20" placeholder="输入页数">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-2">
|
|
||||||
<label for="pageSize" class="form-label">每页显示</label>
|
|
||||||
<select class="form-select" id="pageSize">
|
|
||||||
<option value="10">10条</option>
|
|
||||||
<option value="20" selected>20条</option>
|
|
||||||
<option value="30">30条</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-2">
|
|
||||||
<label class="form-label"> </label>
|
|
||||||
<div class="d-grid">
|
|
||||||
<button type="submit" class="btn btn-primary">
|
|
||||||
<i class="bi bi-search me-2"></i>
|
|
||||||
搜索
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 搜索结果统计 -->
|
|
||||||
<div id="searchStats" class="alert alert-info" style="display: none;">
|
|
||||||
<i class="bi bi-info-circle me-2"></i>
|
|
||||||
<span id="statsText"></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 加载状态 -->
|
|
||||||
<div id="loading" class="loading" style="display: none;">
|
|
||||||
<div class="spinner-border text-primary" role="status">
|
|
||||||
<span class="visually-hidden">搜索中...</span>
|
|
||||||
</div>
|
|
||||||
<p class="mt-3">正在搜索商品,请稍候...</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 搜索结果 -->
|
|
||||||
<div id="searchResults" class="row">
|
|
||||||
<!-- 商品卡片将在这里动态生成 -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 无结果提示 -->
|
|
||||||
<div id="noResults" class="no-results" style="display: none;">
|
|
||||||
<i class="bi bi-search" style="font-size: 3em; color: #dee2e6;"></i>
|
|
||||||
<h5 class="mt-3">没有找到相关商品</h5>
|
|
||||||
<p class="text-muted">请尝试使用其他关键词搜索</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 分页 -->
|
|
||||||
<div id="paginationContainer" class="pagination-container" style="display: none;">
|
|
||||||
<nav>
|
|
||||||
<ul class="pagination" id="pagination">
|
|
||||||
<!-- 分页按钮将在这里动态生成 -->
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 商品详情模态框 -->
|
|
||||||
<div class="modal fade" id="itemDetailModal" tabindex="-1">
|
|
||||||
<div class="modal-dialog modal-lg">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title">
|
|
||||||
<i class="bi bi-box-seam me-2"></i>
|
|
||||||
商品详情
|
|
||||||
</h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body" id="itemDetailContent">
|
|
||||||
<!-- 详情内容将在这里动态加载 -->
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div> <!-- 结束 mainContent -->
|
|
||||||
|
|
||||||
<script src="/static/lib/bootstrap/bootstrap.bundle.min.js"></script>
|
|
||||||
<script>
|
|
||||||
// 全局变量
|
|
||||||
let currentPage = 1;
|
|
||||||
let currentKeyword = '';
|
|
||||||
let allItems = []; // 存储所有获取的商品数据
|
|
||||||
let currentPageSize = 20; // 前端分页每页显示数量
|
|
||||||
let totalResults = 0;
|
|
||||||
let authToken = localStorage.getItem('auth_token');
|
|
||||||
const apiBase = location.origin;
|
|
||||||
|
|
||||||
// HTML转义函数(全局函数)
|
|
||||||
function escapeHtml(text) {
|
|
||||||
const div = document.createElement('div');
|
|
||||||
div.textContent = text;
|
|
||||||
return div.innerHTML;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示商品详情(全局函数)
|
|
||||||
async function showItemDetail(itemId) {
|
|
||||||
try {
|
|
||||||
const modal = new bootstrap.Modal(document.getElementById('itemDetailModal'));
|
|
||||||
const content = document.getElementById('itemDetailContent');
|
|
||||||
|
|
||||||
// 显示加载状态
|
|
||||||
content.innerHTML = `
|
|
||||||
<div class="text-center">
|
|
||||||
<div class="spinner-border" role="status">
|
|
||||||
<span class="visually-hidden">加载中...</span>
|
|
||||||
</div>
|
|
||||||
<p class="mt-2">正在加载商品详情...</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
modal.show();
|
|
||||||
|
|
||||||
// 调用API获取商品详情
|
|
||||||
const headers = {};
|
|
||||||
if (authToken) {
|
|
||||||
headers['Authorization'] = `Bearer ${authToken}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(`${apiBase}/items/detail/${itemId}`, {
|
|
||||||
headers: headers
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success && result.data) {
|
|
||||||
content.innerHTML = `
|
|
||||||
<div class="item-detail">
|
|
||||||
<h6>商品详情</h6>
|
|
||||||
<pre style="white-space: pre-wrap; font-family: inherit;">${escapeHtml(result.data)}</pre>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
} else {
|
|
||||||
content.innerHTML = `
|
|
||||||
<div class="alert alert-warning">
|
|
||||||
<i class="bi bi-exclamation-triangle me-2"></i>
|
|
||||||
${escapeHtml(result.message || '获取商品详情失败')}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
content.innerHTML = `
|
|
||||||
<div class="alert alert-danger">
|
|
||||||
<i class="bi bi-x-circle me-2"></i>
|
|
||||||
获取商品详情失败,请稍后重试
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取商品详情失败:', error);
|
|
||||||
const content = document.getElementById('itemDetailContent');
|
|
||||||
content.innerHTML = `
|
|
||||||
<div class="alert alert-danger">
|
|
||||||
<i class="bi bi-x-circle me-2"></i>
|
|
||||||
网络错误,请检查网络连接
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查用户登录状态
|
|
||||||
async function checkLoginStatus() {
|
|
||||||
if (!authToken) {
|
|
||||||
// 未登录,直接重定向到登录页面
|
|
||||||
redirectToLogin();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证token是否有效
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${apiBase}/verify`, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${authToken}`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.authenticated) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// token无效,清除并重定向
|
|
||||||
localStorage.removeItem('auth_token');
|
|
||||||
redirectToLogin();
|
|
||||||
return false;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Token验证失败:', error);
|
|
||||||
// 网络错误时也清除token并重定向
|
|
||||||
localStorage.removeItem('auth_token');
|
|
||||||
redirectToLogin();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 重定向到登录页面
|
|
||||||
function redirectToLogin() {
|
|
||||||
// 保存当前页面URL,登录后可以返回
|
|
||||||
const currentUrl = window.location.href;
|
|
||||||
localStorage.setItem('redirectAfterLogin', currentUrl);
|
|
||||||
|
|
||||||
// 立即重定向到登录页面
|
|
||||||
window.location.href = 'login.html';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 页面加载时立即检查登录状态
|
|
||||||
(function() {
|
|
||||||
// 在页面内容加载前就检查登录状态
|
|
||||||
if (!authToken) {
|
|
||||||
redirectToLogin();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
// 页面加载完成后初始化
|
|
||||||
document.addEventListener('DOMContentLoaded', async function() {
|
|
||||||
// 异步检查登录状态(验证token有效性)
|
|
||||||
const isLoggedIn = await checkLoginStatus();
|
|
||||||
if (!isLoggedIn) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 隐藏加载提示,显示主要内容
|
|
||||||
document.getElementById('loadingAuth').style.display = 'none';
|
|
||||||
document.getElementById('mainContent').style.display = 'block';
|
|
||||||
|
|
||||||
// 如果已登录,继续原有的初始化逻辑
|
|
||||||
initializePage();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 初始化页面
|
|
||||||
function initializePage() {
|
|
||||||
|
|
||||||
// 搜索表单提交
|
|
||||||
const searchForm = document.getElementById('searchForm');
|
|
||||||
|
|
||||||
if (searchForm) {
|
|
||||||
searchForm.addEventListener('submit', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const keyword = document.getElementById('keyword').value.trim();
|
|
||||||
const totalPages = parseInt(document.getElementById('totalPages').value);
|
|
||||||
const pageSize = parseInt(document.getElementById('pageSize').value);
|
|
||||||
|
|
||||||
if (!keyword) {
|
|
||||||
alert('请输入搜索关键词');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isNaN(totalPages) || totalPages < 1 || totalPages > 20) {
|
|
||||||
alert('请输入有效的页数(1-20页)');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentKeyword = keyword;
|
|
||||||
currentPageSize = pageSize;
|
|
||||||
currentPage = 1; // 重置到第一页
|
|
||||||
|
|
||||||
searchAllPages(keyword, totalPages);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.error('找不到搜索表单元素!');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听每页显示数量的变化
|
|
||||||
const pageSizeSelect = document.getElementById('pageSize');
|
|
||||||
if (pageSizeSelect) {
|
|
||||||
pageSizeSelect.addEventListener('change', function() {
|
|
||||||
const newPageSize = parseInt(this.value);
|
|
||||||
|
|
||||||
// 如果已经有搜索结果,立即更新分页显示
|
|
||||||
if (allItems.length > 0) {
|
|
||||||
currentPageSize = newPageSize;
|
|
||||||
currentPage = 1; // 重置到第一页
|
|
||||||
displayCurrentPage();
|
|
||||||
updateFrontendPagination();
|
|
||||||
|
|
||||||
// 更新统计信息
|
|
||||||
updateStatsDisplay();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否有有效的cookies账户
|
|
||||||
async function checkValidCookies() {
|
|
||||||
try {
|
|
||||||
const headers = {};
|
|
||||||
if (authToken) {
|
|
||||||
headers['Authorization'] = `Bearer ${authToken}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(`${apiBase}/cookies/check`, {
|
|
||||||
headers: headers
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
return data.hasValidCookies;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('检查cookies失败:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 搜索多页商品数据
|
|
||||||
async function searchAllPages(keyword, totalPages) {
|
|
||||||
try {
|
|
||||||
// 检查是否有有效的cookies账户
|
|
||||||
const hasValidCookies = await checkValidCookies();
|
|
||||||
if (!hasValidCookies) {
|
|
||||||
showErrorMessage('搜索失败:系统中不存在有效的账户信息。请先在Cookie管理中添加有效的闲鱼账户。');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示加载状态
|
|
||||||
showLoading(true);
|
|
||||||
hideResults();
|
|
||||||
|
|
||||||
// 构建请求头
|
|
||||||
const headers = {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
};
|
|
||||||
|
|
||||||
// 如果有token则添加认证头
|
|
||||||
if (authToken) {
|
|
||||||
headers['Authorization'] = `Bearer ${authToken}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const requestData = {
|
|
||||||
keyword: keyword,
|
|
||||||
total_pages: totalPages // 传递总页数参数
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('发送多页搜索API请求:', {
|
|
||||||
url: `${apiBase}/items/search_multiple`,
|
|
||||||
method: 'POST',
|
|
||||||
headers: headers,
|
|
||||||
body: requestData
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await fetch(`${apiBase}/items/search_multiple`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: headers,
|
|
||||||
body: JSON.stringify(requestData)
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('API响应状态:', response.status);
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
console.log('API返回的完整数据:', data);
|
|
||||||
|
|
||||||
if (data.success) {
|
|
||||||
allItems = data.data || [];
|
|
||||||
totalResults = allItems.length;
|
|
||||||
|
|
||||||
console.log('设置allItems:', allItems);
|
|
||||||
console.log('allItems长度:', allItems.length);
|
|
||||||
console.log('totalResults:', totalResults);
|
|
||||||
|
|
||||||
// 检查是否有错误信息
|
|
||||||
if (data.error) {
|
|
||||||
showErrorMessage(`搜索完成,但遇到问题: ${data.error}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('调用displayPaginatedResults...');
|
|
||||||
displayPaginatedResults(data);
|
|
||||||
console.log('调用updateFrontendPagination...');
|
|
||||||
updateFrontendPagination();
|
|
||||||
} else {
|
|
||||||
console.error('API返回success=false:', data);
|
|
||||||
throw new Error(data.message || data.error || '搜索失败');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const errorText = await response.text();
|
|
||||||
throw new Error(`服务器错误 (${response.status}): ${errorText}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('搜索失败:', error);
|
|
||||||
showErrorMessage('搜索失败: ' + error.message);
|
|
||||||
} finally {
|
|
||||||
showLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 搜索单页商品(保留原有功能)
|
|
||||||
async function searchItems(page = 1) {
|
|
||||||
console.log('searchItems函数被调用,参数:', { page, currentKeyword, currentPageSize });
|
|
||||||
try {
|
|
||||||
// 显示加载状态
|
|
||||||
showLoading(true);
|
|
||||||
hideResults();
|
|
||||||
|
|
||||||
// 构建请求头
|
|
||||||
const headers = {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
};
|
|
||||||
|
|
||||||
// 如果有token则添加认证头
|
|
||||||
if (authToken) {
|
|
||||||
headers['Authorization'] = `Bearer ${authToken}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const requestData = {
|
|
||||||
keyword: currentKeyword,
|
|
||||||
page: page,
|
|
||||||
page_size: 20 // 固定每页20条数据
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('发送API请求:', {
|
|
||||||
url: `${apiBase}/items/search`,
|
|
||||||
method: 'POST',
|
|
||||||
headers: headers,
|
|
||||||
body: requestData
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await fetch(`${apiBase}/items/search`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: headers,
|
|
||||||
body: JSON.stringify(requestData)
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('API响应状态:', response.status);
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
if (data.success) {
|
|
||||||
displayResults(data);
|
|
||||||
currentPage = page;
|
|
||||||
totalResults = data.total;
|
|
||||||
updatePagination();
|
|
||||||
} else {
|
|
||||||
throw new Error(data.message || '搜索失败');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new Error(`HTTP ${response.status}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('搜索失败:', error);
|
|
||||||
showErrorMessage('搜索失败: ' + error.message);
|
|
||||||
} finally {
|
|
||||||
showLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示/隐藏加载状态
|
|
||||||
function showLoading(show) {
|
|
||||||
document.getElementById('loading').style.display = show ? 'block' : 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示错误消息
|
|
||||||
function showErrorMessage(message) {
|
|
||||||
// 创建或更新错误提示
|
|
||||||
let errorAlert = document.getElementById('errorAlert');
|
|
||||||
if (!errorAlert) {
|
|
||||||
errorAlert = document.createElement('div');
|
|
||||||
errorAlert.id = 'errorAlert';
|
|
||||||
errorAlert.className = 'alert alert-danger alert-dismissible fade show';
|
|
||||||
errorAlert.innerHTML = `
|
|
||||||
<i class="bi bi-exclamation-triangle me-2"></i>
|
|
||||||
<span id="errorMessage"></span>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// 插入到搜索表单后面
|
|
||||||
const searchForm = document.querySelector('.search-form');
|
|
||||||
searchForm.parentNode.insertBefore(errorAlert, searchForm.nextSibling);
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('errorMessage').textContent = message;
|
|
||||||
errorAlert.style.display = 'block';
|
|
||||||
|
|
||||||
// 5秒后自动隐藏
|
|
||||||
setTimeout(() => {
|
|
||||||
if (errorAlert) {
|
|
||||||
errorAlert.style.display = 'none';
|
|
||||||
}
|
|
||||||
}, 5000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 隐藏结果
|
|
||||||
function hideResults() {
|
|
||||||
document.getElementById('searchResults').innerHTML = '';
|
|
||||||
document.getElementById('searchStats').style.display = 'none';
|
|
||||||
document.getElementById('noResults').style.display = 'none';
|
|
||||||
document.getElementById('paginationContainer').style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示分页搜索结果
|
|
||||||
function displayPaginatedResults(data) {
|
|
||||||
console.log('displayPaginatedResults被调用,data:', data);
|
|
||||||
|
|
||||||
// 显示统计信息
|
|
||||||
const statsElement = document.getElementById('searchStats');
|
|
||||||
const statsText = document.getElementById('statsText');
|
|
||||||
|
|
||||||
console.log('statsElement:', statsElement);
|
|
||||||
console.log('statsText:', statsText);
|
|
||||||
|
|
||||||
// 检查数据来源
|
|
||||||
let dataSource = '';
|
|
||||||
if (data.is_real_data) {
|
|
||||||
dataSource = ' [真实数据]';
|
|
||||||
statsElement.className = 'alert alert-success';
|
|
||||||
} else {
|
|
||||||
dataSource = ' [数据获取异常]';
|
|
||||||
statsElement.className = 'alert alert-danger';
|
|
||||||
}
|
|
||||||
|
|
||||||
statsElement.style.display = 'block';
|
|
||||||
console.log('统计信息已显示');
|
|
||||||
|
|
||||||
// 使用统一的统计信息更新函数
|
|
||||||
console.log('调用updateStatsDisplay...');
|
|
||||||
updateStatsDisplay();
|
|
||||||
|
|
||||||
// 显示当前页的数据
|
|
||||||
console.log('调用displayCurrentPage...');
|
|
||||||
displayCurrentPage();
|
|
||||||
}
|
|
||||||
|
|
||||||
} // 结束 initializePage 函数
|
|
||||||
|
|
||||||
// ========================= 全局函数 =========================
|
|
||||||
|
|
||||||
// 显示当前页数据(全局函数)
|
|
||||||
function displayCurrentPage() {
|
|
||||||
console.log('displayCurrentPage被调用');
|
|
||||||
console.log('allItems:', allItems);
|
|
||||||
console.log('allItems.length:', allItems.length);
|
|
||||||
console.log('currentPage:', currentPage);
|
|
||||||
console.log('currentPageSize:', currentPageSize);
|
|
||||||
|
|
||||||
const resultsContainer = document.getElementById('searchResults');
|
|
||||||
console.log('resultsContainer:', resultsContainer);
|
|
||||||
|
|
||||||
// 计算当前页的数据范围
|
|
||||||
const startIndex = (currentPage - 1) * currentPageSize;
|
|
||||||
const endIndex = startIndex + currentPageSize;
|
|
||||||
const currentItems = allItems.slice(startIndex, endIndex);
|
|
||||||
|
|
||||||
console.log('startIndex:', startIndex);
|
|
||||||
console.log('endIndex:', endIndex);
|
|
||||||
console.log('currentItems:', currentItems);
|
|
||||||
console.log('currentItems.length:', currentItems.length);
|
|
||||||
|
|
||||||
if (currentItems.length === 0) {
|
|
||||||
console.log('没有数据,显示无结果提示');
|
|
||||||
document.getElementById('noResults').style.display = 'block';
|
|
||||||
resultsContainer.innerHTML = '';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('noResults').style.display = 'none';
|
|
||||||
|
|
||||||
// 生成商品卡片
|
|
||||||
console.log('生成商品卡片...');
|
|
||||||
const cardsHtml = currentItems.map(item => createItemCard(item)).join('');
|
|
||||||
console.log('生成的HTML长度:', cardsHtml.length);
|
|
||||||
resultsContainer.innerHTML = cardsHtml;
|
|
||||||
console.log('商品卡片已设置到容器中');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示搜索结果(保留原有功能)
|
|
||||||
function displayResults(data) {
|
|
||||||
const resultsContainer = document.getElementById('searchResults');
|
|
||||||
const items = data.data || [];
|
|
||||||
|
|
||||||
// 显示统计信息
|
|
||||||
const statsElement = document.getElementById('searchStats');
|
|
||||||
const statsText = document.getElementById('statsText');
|
|
||||||
|
|
||||||
// 检查数据来源
|
|
||||||
let dataSource = '';
|
|
||||||
if (data.is_real_data) {
|
|
||||||
dataSource = ' [真实数据]';
|
|
||||||
statsElement.className = 'alert alert-success';
|
|
||||||
} else {
|
|
||||||
dataSource = ' [数据获取异常]';
|
|
||||||
statsElement.className = 'alert alert-danger';
|
|
||||||
}
|
|
||||||
|
|
||||||
statsText.textContent = `搜索"${data.keyword}",第 ${data.page} 页结果,共显示 ${data.data.length} 个商品${dataSource}`;
|
|
||||||
statsElement.style.display = 'block';
|
|
||||||
|
|
||||||
if (items.length === 0) {
|
|
||||||
document.getElementById('noResults').style.display = 'block';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成商品卡片
|
|
||||||
resultsContainer.innerHTML = items.map(item => createItemCard(item)).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 更新分页提示(保留原有功能)
|
|
||||||
function updatePagination() {
|
|
||||||
const paginationContainer = document.getElementById('paginationContainer');
|
|
||||||
const pagination = document.getElementById('pagination');
|
|
||||||
|
|
||||||
// 显示页码选择提示
|
|
||||||
paginationContainer.style.display = 'block';
|
|
||||||
|
|
||||||
let paginationHtml = `
|
|
||||||
<li class="page-item disabled">
|
|
||||||
<span class="page-link">
|
|
||||||
<i class="bi bi-info-circle me-2"></i>
|
|
||||||
要查看其他页面,请在上方选择页码后重新搜索
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
`;
|
|
||||||
|
|
||||||
pagination.innerHTML = paginationHtml;
|
|
||||||
} // 结束 initializePage 函数
|
|
||||||
|
|
||||||
// ========================= 全局函数 =========================
|
|
||||||
|
|
||||||
// 跳转到指定页(全局函数,供分页按钮调用)
|
|
||||||
function goToPage(page) {
|
|
||||||
currentPage = page;
|
|
||||||
displayCurrentPage();
|
|
||||||
updateFrontendPagination();
|
|
||||||
|
|
||||||
// 更新统计信息
|
|
||||||
updateStatsDisplay();
|
|
||||||
|
|
||||||
// 滚动到页面顶部
|
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新统计信息显示(全局函数)
|
|
||||||
function updateStatsDisplay() {
|
|
||||||
const statsText = document.getElementById('statsText');
|
|
||||||
let dataSource = '';
|
|
||||||
|
|
||||||
if (allItems.length > 0) {
|
|
||||||
dataSource = ' [真实数据]';
|
|
||||||
}
|
|
||||||
|
|
||||||
const totalPages = Math.ceil(allItems.length / currentPageSize);
|
|
||||||
statsText.textContent = `搜索"${currentKeyword}",共获取 ${allItems.length} 个商品${dataSource},当前显示第 ${currentPage}/${totalPages} 页(每页${currentPageSize}条)`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新前端分页(全局函数)
|
|
||||||
function updateFrontendPagination() {
|
|
||||||
const totalPages = Math.ceil(allItems.length / currentPageSize);
|
|
||||||
const paginationContainer = document.getElementById('paginationContainer');
|
|
||||||
const pagination = document.getElementById('pagination');
|
|
||||||
|
|
||||||
if (totalPages <= 1) {
|
|
||||||
paginationContainer.style.display = 'none';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
paginationContainer.style.display = 'block';
|
|
||||||
|
|
||||||
let paginationHtml = '';
|
|
||||||
|
|
||||||
// 上一页
|
|
||||||
if (currentPage > 1) {
|
|
||||||
paginationHtml += `<li class="page-item"><a class="page-link" href="#" onclick="goToPage(${currentPage - 1}); return false;">上一页</a></li>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 页码显示逻辑
|
|
||||||
if (totalPages <= 7) {
|
|
||||||
// 如果总页数不超过7页,显示所有页码
|
|
||||||
for (let i = 1; i <= totalPages; i++) {
|
|
||||||
const active = i === currentPage ? 'active' : '';
|
|
||||||
paginationHtml += `<li class="page-item ${active}"><a class="page-link" href="#" onclick="goToPage(${i}); return false;">${i}</a></li>`;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 如果总页数超过7页,使用省略号显示
|
|
||||||
if (currentPage <= 4) {
|
|
||||||
// 当前页在前面,显示 1 2 3 4 5 ... 最后页
|
|
||||||
for (let i = 1; i <= 5; i++) {
|
|
||||||
const active = i === currentPage ? 'active' : '';
|
|
||||||
paginationHtml += `<li class="page-item ${active}"><a class="page-link" href="#" onclick="goToPage(${i}); return false;">${i}</a></li>`;
|
|
||||||
}
|
|
||||||
paginationHtml += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
|
|
||||||
paginationHtml += `<li class="page-item"><a class="page-link" href="#" onclick="goToPage(${totalPages}); return false;">${totalPages}</a></li>`;
|
|
||||||
} else if (currentPage >= totalPages - 3) {
|
|
||||||
// 当前页在后面,显示 1 ... 倒数5页
|
|
||||||
paginationHtml += `<li class="page-item"><a class="page-link" href="#" onclick="goToPage(1); return false;">1</a></li>`;
|
|
||||||
paginationHtml += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
|
|
||||||
for (let i = totalPages - 4; i <= totalPages; i++) {
|
|
||||||
const active = i === currentPage ? 'active' : '';
|
|
||||||
paginationHtml += `<li class="page-item ${active}"><a class="page-link" href="#" onclick="goToPage(${i}); return false;">${i}</a></li>`;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 当前页在中间,显示 1 ... 当前页前后2页 ... 最后页
|
|
||||||
paginationHtml += `<li class="page-item"><a class="page-link" href="#" onclick="goToPage(1); return false;">1</a></li>`;
|
|
||||||
paginationHtml += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
|
|
||||||
for (let i = currentPage - 2; i <= currentPage + 2; i++) {
|
|
||||||
const active = i === currentPage ? 'active' : '';
|
|
||||||
paginationHtml += `<li class="page-item ${active}"><a class="page-link" href="#" onclick="goToPage(${i}); return false;">${i}</a></li>`;
|
|
||||||
}
|
|
||||||
paginationHtml += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
|
|
||||||
paginationHtml += `<li class="page-item"><a class="page-link" href="#" onclick="goToPage(${totalPages}); return false;">${totalPages}</a></li>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 下一页
|
|
||||||
if (currentPage < totalPages) {
|
|
||||||
paginationHtml += `<li class="page-item"><a class="page-link" href="#" onclick="goToPage(${currentPage + 1}); return false;">下一页</a></li>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
pagination.innerHTML = paginationHtml;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建商品卡片HTML(全局函数)
|
|
||||||
function createItemCard(item) {
|
|
||||||
console.log('createItemCard被调用,item数据:', item);
|
|
||||||
console.log('item的所有字段:', Object.keys(item));
|
|
||||||
|
|
||||||
const tags = item.tags ? item.tags.map(tag => `<span class="tag">${escapeHtml(tag)}</span>`).join('') : '';
|
|
||||||
const imageUrl = item.main_image || 'https://via.placeholder.com/200x200?text=暂无图片';
|
|
||||||
const wantCount = item.want_count || 0;
|
|
||||||
|
|
||||||
console.log('处理后的数据:', {
|
|
||||||
title: item.title,
|
|
||||||
price: item.price,
|
|
||||||
seller_name: item.seller_name,
|
|
||||||
imageUrl: imageUrl,
|
|
||||||
wantCount: wantCount
|
|
||||||
});
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="col-md-6 col-lg-4 col-xl-3 mb-4">
|
|
||||||
<div class="card item-card h-100">
|
|
||||||
<img src="${escapeHtml(imageUrl)}" class="item-image" alt="${escapeHtml(item.title)}" onerror="this.src='https://via.placeholder.com/200x200?text=图片加载失败'">
|
|
||||||
<div class="card-body d-flex flex-column">
|
|
||||||
<h6 class="card-title" title="${escapeHtml(item.title)}">${escapeHtml(item.title.length > 50 ? item.title.substring(0, 50) + '...' : item.title)}</h6>
|
|
||||||
<div class="price mb-2">${escapeHtml(item.price)}</div>
|
|
||||||
<div class="seller-name mb-2">
|
|
||||||
<i class="bi bi-person me-1"></i>
|
|
||||||
${escapeHtml(item.seller_name)}
|
|
||||||
</div>
|
|
||||||
${wantCount > 0 ? `<div class="want-count mb-2">
|
|
||||||
<i class="bi bi-heart-fill me-1" style="color: #ff6b6b;"></i>
|
|
||||||
<span class="badge bg-danger">${wantCount}人想要</span>
|
|
||||||
</div>` : ''}
|
|
||||||
${item.publish_time ? `<div class="text-muted small mb-2">
|
|
||||||
<i class="bi bi-clock me-1"></i>
|
|
||||||
${escapeHtml(item.publish_time)}
|
|
||||||
</div>` : ''}
|
|
||||||
<div class="tags mb-3">${tags}</div>
|
|
||||||
<div class="mt-auto">
|
|
||||||
<div class="btn-group w-100" role="group">
|
|
||||||
<a href="${escapeHtml(item.item_url)}" target="_blank" class="btn btn-outline-primary btn-sm">
|
|
||||||
<i class="bi bi-link-45deg me-1"></i>
|
|
||||||
查看商品
|
|
||||||
</a>
|
|
||||||
<button class="btn btn-outline-info btn-sm" onclick="showItemDetail('${escapeHtml(item.item_id)}')">
|
|
||||||
<i class="bi bi-info-circle me-1"></i>
|
|
||||||
详情
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示商品详情(全局函数)
|
|
||||||
function showItemDetail(itemId) {
|
|
||||||
// 查找对应的商品数据
|
|
||||||
const item = allItems.find(i => i.item_id === itemId);
|
|
||||||
if (!item) {
|
|
||||||
alert('商品信息不存在');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 填充模态框内容
|
|
||||||
const modalBody = document.querySelector('#itemDetailModal .modal-body');
|
|
||||||
modalBody.innerHTML = `
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<img src="${escapeHtml(item.main_image || 'https://via.placeholder.com/400x400?text=暂无图片')}"
|
|
||||||
class="img-fluid rounded" alt="${escapeHtml(item.title)}"
|
|
||||||
onerror="this.src='https://via.placeholder.com/400x400?text=图片加载失败'">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<h5>${escapeHtml(item.title)}</h5>
|
|
||||||
<p class="text-primary fs-4 fw-bold">${escapeHtml(item.price)}</p>
|
|
||||||
<p><strong>卖家:</strong>${escapeHtml(item.seller_name)}</p>
|
|
||||||
${item.want_count ? `<p><strong>想要人数:</strong>${item.want_count}人</p>` : ''}
|
|
||||||
${item.publish_time ? `<p><strong>发布时间:</strong>${escapeHtml(item.publish_time)}</p>` : ''}
|
|
||||||
${item.tags && item.tags.length > 0 ? `
|
|
||||||
<p><strong>标签:</strong></p>
|
|
||||||
<div class="mb-3">
|
|
||||||
${item.tags.map(tag => `<span class="badge bg-secondary me-1">${escapeHtml(tag)}</span>`).join('')}
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
<p><strong>商品链接:</strong></p>
|
|
||||||
<a href="${escapeHtml(item.item_url)}" target="_blank" class="btn btn-primary">
|
|
||||||
<i class="bi bi-link-45deg me-1"></i>
|
|
||||||
在闲鱼中查看
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// 显示模态框
|
|
||||||
const modal = new bootstrap.Modal(document.getElementById('itemDetailModal'));
|
|
||||||
modal.show();
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,457 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>日志管理 - 闲鱼自动回复系统</title>
|
|
||||||
<link href="/static/lib/bootstrap/bootstrap.min.css" rel="stylesheet">
|
|
||||||
<link href="/static/lib/bootstrap-icons/bootstrap-icons.css" rel="stylesheet">
|
|
||||||
<style>
|
|
||||||
.admin-header {
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
color: white;
|
|
||||||
padding: 1rem 0;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
.log-container {
|
|
||||||
background: #1e1e1e;
|
|
||||||
color: #f8f9fa;
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
max-height: 600px;
|
|
||||||
overflow-y: auto;
|
|
||||||
border-radius: 0.375rem;
|
|
||||||
}
|
|
||||||
.log-line {
|
|
||||||
padding: 0.25rem 0.5rem;
|
|
||||||
border-bottom: 1px solid #333;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
.log-line:hover {
|
|
||||||
background: #2d2d2d;
|
|
||||||
}
|
|
||||||
.log-info { color: #17a2b8; }
|
|
||||||
.log-warning { color: #ffc107; }
|
|
||||||
.log-error { color: #dc3545; }
|
|
||||||
.log-debug { color: #6c757d; }
|
|
||||||
.filter-badge {
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
.filter-badge:hover {
|
|
||||||
transform: scale(1.05);
|
|
||||||
}
|
|
||||||
.filter-badge.active {
|
|
||||||
box-shadow: 0 0 0 2px rgba(255,255,255,0.5);
|
|
||||||
}
|
|
||||||
.auto-refresh-indicator {
|
|
||||||
animation: pulse 2s infinite;
|
|
||||||
}
|
|
||||||
@keyframes pulse {
|
|
||||||
0% { opacity: 1; }
|
|
||||||
50% { opacity: 0.5; }
|
|
||||||
100% { opacity: 1; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<!-- 导航栏 -->
|
|
||||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
|
||||||
<div class="container">
|
|
||||||
<a class="navbar-brand" href="/">
|
|
||||||
<i class="bi bi-robot"></i> 闲鱼自动回复系统
|
|
||||||
</a>
|
|
||||||
<div class="navbar-nav ms-auto">
|
|
||||||
<a class="nav-link" href="/">
|
|
||||||
<i class="bi bi-house"></i> 首页
|
|
||||||
</a>
|
|
||||||
<a class="nav-link" href="/user_management.html">
|
|
||||||
<i class="bi bi-people"></i> 用户管理
|
|
||||||
</a>
|
|
||||||
<a class="nav-link" href="/data_management.html">
|
|
||||||
<i class="bi bi-database"></i> 数据管理
|
|
||||||
</a>
|
|
||||||
<a class="nav-link active" href="/log_management.html">
|
|
||||||
<i class="bi bi-file-text"></i> 日志管理
|
|
||||||
</a>
|
|
||||||
<a class="nav-link" href="#" onclick="logout()">
|
|
||||||
<i class="bi bi-box-arrow-right"></i> 退出
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<!-- 管理员标题 -->
|
|
||||||
<div class="admin-header">
|
|
||||||
<div class="container">
|
|
||||||
<h1 class="mb-0">
|
|
||||||
<i class="bi bi-file-text"></i> 日志管理
|
|
||||||
</h1>
|
|
||||||
<p class="mb-0 mt-2">查看和监控系统运行日志</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
<!-- 控制面板 -->
|
|
||||||
<div class="card mb-4">
|
|
||||||
<div class="card-header">
|
|
||||||
<h5 class="mb-0">
|
|
||||||
<i class="bi bi-sliders"></i> 日志控制
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="row align-items-center">
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label class="form-label">显示行数</label>
|
|
||||||
<select class="form-select" id="logLines" onchange="loadLogs()">
|
|
||||||
<option value="50">50行</option>
|
|
||||||
<option value="100" selected>100行</option>
|
|
||||||
<option value="200">200行</option>
|
|
||||||
<option value="500">500行</option>
|
|
||||||
<option value="1000">1000行</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<label class="form-label">日志级别过滤</label>
|
|
||||||
<div class="d-flex gap-2 flex-wrap">
|
|
||||||
<span class="badge bg-secondary filter-badge active" data-level="" onclick="filterByLevel('')">
|
|
||||||
全部
|
|
||||||
</span>
|
|
||||||
<span class="badge bg-info filter-badge log-info" data-level="info" onclick="filterByLevel('info')">
|
|
||||||
INFO
|
|
||||||
</span>
|
|
||||||
<span class="badge bg-warning filter-badge log-warning" data-level="warning" onclick="filterByLevel('warning')">
|
|
||||||
WARNING
|
|
||||||
</span>
|
|
||||||
<span class="badge bg-danger filter-badge log-error" data-level="error" onclick="filterByLevel('error')">
|
|
||||||
ERROR
|
|
||||||
</span>
|
|
||||||
<span class="badge bg-secondary filter-badge log-debug" data-level="debug" onclick="filterByLevel('debug')">
|
|
||||||
DEBUG
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label class="form-label">自动刷新</label>
|
|
||||||
<div class="form-check form-switch">
|
|
||||||
<input class="form-check-input" type="checkbox" id="autoRefresh" onchange="toggleAutoRefresh()">
|
|
||||||
<label class="form-check-label" for="autoRefresh">
|
|
||||||
<span id="autoRefreshLabel">关闭</span>
|
|
||||||
<i id="autoRefreshIcon" class="bi bi-circle" style="display: none;"></i>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-2">
|
|
||||||
<label class="form-label"> </label>
|
|
||||||
<div class="d-grid">
|
|
||||||
<button class="btn btn-primary" onclick="loadLogs()">
|
|
||||||
<i class="bi bi-arrow-clockwise"></i> 刷新
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 日志信息 -->
|
|
||||||
<div class="card mb-4">
|
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
|
||||||
<h5 class="mb-0">
|
|
||||||
<i class="bi bi-info-circle"></i> 日志信息
|
|
||||||
</h5>
|
|
||||||
<small class="text-muted" id="lastUpdate">最后更新: -</small>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-4">
|
|
||||||
<strong>日志文件:</strong>
|
|
||||||
<span id="logFile" class="text-muted">-</span>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<strong>显示行数:</strong>
|
|
||||||
<span id="displayLines" class="text-muted">-</span>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<strong>当前过滤:</strong>
|
|
||||||
<span id="currentFilter" class="text-muted">全部</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 日志内容 -->
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
|
||||||
<h5 class="mb-0">
|
|
||||||
<i class="bi bi-terminal"></i> 日志内容
|
|
||||||
</h5>
|
|
||||||
<div class="d-flex gap-2">
|
|
||||||
<button class="btn btn-sm btn-outline-secondary" onclick="scrollToTop()">
|
|
||||||
<i class="bi bi-arrow-up"></i> 顶部
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-sm btn-outline-secondary" onclick="scrollToBottom()">
|
|
||||||
<i class="bi bi-arrow-down"></i> 底部
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-body p-0">
|
|
||||||
<div id="loadingLogs" class="text-center py-4">
|
|
||||||
<div class="spinner-border" role="status">
|
|
||||||
<span class="visually-hidden">加载中...</span>
|
|
||||||
</div>
|
|
||||||
<p class="mt-2">正在加载日志...</p>
|
|
||||||
</div>
|
|
||||||
<div id="logContainer" class="log-container" style="display: none;"></div>
|
|
||||||
<div id="noLogs" class="text-center py-4" style="display: none;">
|
|
||||||
<i class="bi bi-file-text" style="font-size: 3rem; color: #ccc;"></i>
|
|
||||||
<p class="mt-2 text-muted">暂无日志数据</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="/static/lib/bootstrap/bootstrap.bundle.min.js"></script>
|
|
||||||
<script>
|
|
||||||
let autoRefreshInterval = null;
|
|
||||||
let currentLevel = '';
|
|
||||||
|
|
||||||
// 页面加载完成后初始化
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
// 检查管理员权限
|
|
||||||
checkAdminPermission();
|
|
||||||
|
|
||||||
// 加载日志
|
|
||||||
loadLogs();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 检查管理员权限
|
|
||||||
function checkAdminPermission() {
|
|
||||||
const token = localStorage.getItem('auth_token');
|
|
||||||
if (!token) {
|
|
||||||
alert('请先登录');
|
|
||||||
window.location.href = '/login.html';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证token并检查是否为管理员
|
|
||||||
fetch('/verify', {
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${token}`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
if (!data.authenticated) {
|
|
||||||
alert('登录已过期,请重新登录');
|
|
||||||
window.location.href = '/login.html';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否为管理员
|
|
||||||
if (data.username !== 'admin') {
|
|
||||||
alert('此功能仅限管理员使用');
|
|
||||||
window.location.href = '/';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('权限验证失败:', error);
|
|
||||||
alert('权限验证失败');
|
|
||||||
window.location.href = '/login.html';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载日志
|
|
||||||
function loadLogs() {
|
|
||||||
const token = localStorage.getItem('auth_token');
|
|
||||||
const lines = document.getElementById('logLines').value;
|
|
||||||
const level = currentLevel;
|
|
||||||
|
|
||||||
const loadingDiv = document.getElementById('loadingLogs');
|
|
||||||
const logContainer = document.getElementById('logContainer');
|
|
||||||
const noLogsDiv = document.getElementById('noLogs');
|
|
||||||
|
|
||||||
loadingDiv.style.display = 'block';
|
|
||||||
logContainer.style.display = 'none';
|
|
||||||
noLogsDiv.style.display = 'none';
|
|
||||||
|
|
||||||
let url = `/admin/logs?lines=${lines}`;
|
|
||||||
if (level) {
|
|
||||||
url += `&level=${level}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
fetch(url, {
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${token}`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(response => {
|
|
||||||
console.log('API响应状态:', response.status);
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
return response.json();
|
|
||||||
})
|
|
||||||
.then(data => {
|
|
||||||
console.log('API响应数据:', data);
|
|
||||||
loadingDiv.style.display = 'none';
|
|
||||||
|
|
||||||
if (data.success === false) {
|
|
||||||
console.error('API返回错误:', data.message);
|
|
||||||
noLogsDiv.innerHTML = `
|
|
||||||
<div class="text-center p-4 text-danger">
|
|
||||||
<i class="bi bi-exclamation-triangle fs-1"></i>
|
|
||||||
<p class="mt-2">加载日志失败</p>
|
|
||||||
<p class="small">${data.message || '未知错误'}</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
noLogsDiv.style.display = 'block';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.logs && data.logs.length > 0) {
|
|
||||||
console.log(`显示 ${data.logs.length} 条日志`);
|
|
||||||
displayLogs(data.logs);
|
|
||||||
updateLogInfo(data);
|
|
||||||
logContainer.style.display = 'block';
|
|
||||||
} else {
|
|
||||||
console.log('没有日志数据');
|
|
||||||
noLogsDiv.innerHTML = `
|
|
||||||
<div class="text-center p-4 text-muted">
|
|
||||||
<i class="bi bi-file-text fs-1"></i>
|
|
||||||
<p class="mt-2">暂无日志数据</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
noLogsDiv.style.display = 'block';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新最后更新时间
|
|
||||||
document.getElementById('lastUpdate').textContent =
|
|
||||||
'最后更新: ' + new Date().toLocaleTimeString('zh-CN');
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('加载日志失败:', error);
|
|
||||||
loadingDiv.style.display = 'none';
|
|
||||||
noLogsDiv.innerHTML = `
|
|
||||||
<div class="text-center p-4 text-danger">
|
|
||||||
<i class="bi bi-exclamation-triangle fs-1"></i>
|
|
||||||
<p class="mt-2">加载日志失败</p>
|
|
||||||
<p class="small">${error.message}</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
noLogsDiv.style.display = 'block';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示日志
|
|
||||||
function displayLogs(logs) {
|
|
||||||
const logContainer = document.getElementById('logContainer');
|
|
||||||
logContainer.innerHTML = '';
|
|
||||||
|
|
||||||
// 反转日志数组,让最新的日志显示在最上面
|
|
||||||
const reversedLogs = [...logs].reverse();
|
|
||||||
|
|
||||||
reversedLogs.forEach(log => {
|
|
||||||
const logLine = document.createElement('div');
|
|
||||||
logLine.className = 'log-line';
|
|
||||||
|
|
||||||
// 根据日志级别添加颜色类
|
|
||||||
if (log.includes('| INFO |')) {
|
|
||||||
logLine.classList.add('log-info');
|
|
||||||
} else if (log.includes('| WARNING |')) {
|
|
||||||
logLine.classList.add('log-warning');
|
|
||||||
} else if (log.includes('| ERROR |')) {
|
|
||||||
logLine.classList.add('log-error');
|
|
||||||
} else if (log.includes('| DEBUG |')) {
|
|
||||||
logLine.classList.add('log-debug');
|
|
||||||
}
|
|
||||||
|
|
||||||
logLine.textContent = log;
|
|
||||||
logContainer.appendChild(logLine);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 自动滚动到顶部(显示最新日志)
|
|
||||||
scrollToTop();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新日志信息
|
|
||||||
function updateLogInfo(data) {
|
|
||||||
document.getElementById('logFile').textContent = data.log_file || '-';
|
|
||||||
document.getElementById('displayLines').textContent = data.total_lines || '-';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 按级别过滤
|
|
||||||
function filterByLevel(level) {
|
|
||||||
currentLevel = level;
|
|
||||||
|
|
||||||
// 更新过滤按钮状态
|
|
||||||
document.querySelectorAll('.filter-badge').forEach(badge => {
|
|
||||||
badge.classList.remove('active');
|
|
||||||
});
|
|
||||||
document.querySelector(`[data-level="${level}"]`).classList.add('active');
|
|
||||||
|
|
||||||
// 更新当前过滤显示
|
|
||||||
const filterText = level ? level.toUpperCase() : '全部';
|
|
||||||
document.getElementById('currentFilter').textContent = filterText;
|
|
||||||
|
|
||||||
// 重新加载日志
|
|
||||||
loadLogs();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 切换自动刷新
|
|
||||||
function toggleAutoRefresh() {
|
|
||||||
const autoRefresh = document.getElementById('autoRefresh');
|
|
||||||
const label = document.getElementById('autoRefreshLabel');
|
|
||||||
const icon = document.getElementById('autoRefreshIcon');
|
|
||||||
|
|
||||||
if (autoRefresh.checked) {
|
|
||||||
// 开启自动刷新
|
|
||||||
autoRefreshInterval = setInterval(loadLogs, 5000); // 每5秒刷新
|
|
||||||
label.textContent = '开启 (5s)';
|
|
||||||
icon.style.display = 'inline';
|
|
||||||
icon.classList.add('auto-refresh-indicator');
|
|
||||||
} else {
|
|
||||||
// 关闭自动刷新
|
|
||||||
if (autoRefreshInterval) {
|
|
||||||
clearInterval(autoRefreshInterval);
|
|
||||||
autoRefreshInterval = null;
|
|
||||||
}
|
|
||||||
label.textContent = '关闭';
|
|
||||||
icon.style.display = 'none';
|
|
||||||
icon.classList.remove('auto-refresh-indicator');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 滚动到顶部
|
|
||||||
function scrollToTop() {
|
|
||||||
const logContainer = document.getElementById('logContainer');
|
|
||||||
logContainer.scrollTop = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 滚动到底部
|
|
||||||
function scrollToBottom() {
|
|
||||||
const logContainer = document.getElementById('logContainer');
|
|
||||||
logContainer.scrollTop = logContainer.scrollHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 退出登录
|
|
||||||
function logout() {
|
|
||||||
// 清理自动刷新
|
|
||||||
if (autoRefreshInterval) {
|
|
||||||
clearInterval(autoRefreshInterval);
|
|
||||||
}
|
|
||||||
|
|
||||||
localStorage.removeItem('auth_token');
|
|
||||||
window.location.href = '/login.html';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 页面卸载时清理定时器
|
|
||||||
window.addEventListener('beforeunload', function() {
|
|
||||||
if (autoRefreshInterval) {
|
|
||||||
clearInterval(autoRefreshInterval);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,400 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>用户管理 - 闲鱼自动回复系统</title>
|
|
||||||
<link href="/static/lib/bootstrap/bootstrap.min.css" rel="stylesheet">
|
|
||||||
<link href="/static/lib/bootstrap-icons/bootstrap-icons.css" rel="stylesheet">
|
|
||||||
<style>
|
|
||||||
.admin-header {
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
color: white;
|
|
||||||
padding: 1rem 0;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
.user-card {
|
|
||||||
transition: transform 0.2s;
|
|
||||||
border: 1px solid #e0e0e0;
|
|
||||||
}
|
|
||||||
.user-card:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
.stats-card {
|
|
||||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
.btn-danger-custom {
|
|
||||||
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%);
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
.btn-danger-custom:hover {
|
|
||||||
background: linear-gradient(135deg, #ee5a52 0%, #ff6b6b 100%);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<!-- 导航栏 -->
|
|
||||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
|
||||||
<div class="container">
|
|
||||||
<a class="navbar-brand" href="/">
|
|
||||||
<i class="bi bi-robot"></i> 闲鱼自动回复系统
|
|
||||||
</a>
|
|
||||||
<div class="navbar-nav ms-auto">
|
|
||||||
<a class="nav-link" href="/">
|
|
||||||
<i class="bi bi-house"></i> 首页
|
|
||||||
</a>
|
|
||||||
<a class="nav-link active" href="/user_management.html">
|
|
||||||
<i class="bi bi-people"></i> 用户管理
|
|
||||||
</a>
|
|
||||||
<a class="nav-link" href="/data_management.html">
|
|
||||||
<i class="bi bi-database"></i> 数据管理
|
|
||||||
</a>
|
|
||||||
<a class="nav-link" href="/log_management.html">
|
|
||||||
<i class="bi bi-file-text"></i> 日志管理
|
|
||||||
</a>
|
|
||||||
<a class="nav-link" href="#" onclick="logout()">
|
|
||||||
<i class="bi bi-box-arrow-right"></i> 退出
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<!-- 管理员标题 -->
|
|
||||||
<div class="admin-header">
|
|
||||||
<div class="container">
|
|
||||||
<h1 class="mb-0">
|
|
||||||
<i class="bi bi-people"></i> 用户管理
|
|
||||||
</h1>
|
|
||||||
<p class="mb-0 mt-2">管理系统中的所有用户账号</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
<!-- 统计信息 -->
|
|
||||||
<div class="row mb-4">
|
|
||||||
<div class="col-md-3">
|
|
||||||
<div class="card stats-card">
|
|
||||||
<div class="card-body text-center">
|
|
||||||
<h3 id="totalUsers">-</h3>
|
|
||||||
<p class="mb-0">总用户数</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<div class="card bg-info text-white">
|
|
||||||
<div class="card-body text-center">
|
|
||||||
<h3 id="totalCookies">-</h3>
|
|
||||||
<p class="mb-0">总Cookie数</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<div class="card bg-success text-white">
|
|
||||||
<div class="card-body text-center">
|
|
||||||
<h3 id="totalCards">-</h3>
|
|
||||||
<p class="mb-0">总卡券数</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<div class="card bg-warning text-white">
|
|
||||||
<div class="card-body text-center">
|
|
||||||
<h3 id="systemUptime">运行中</h3>
|
|
||||||
<p class="mb-0">系统状态</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 用户列表 -->
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
|
||||||
<h5 class="mb-0">
|
|
||||||
<i class="bi bi-people-fill"></i> 用户列表
|
|
||||||
</h5>
|
|
||||||
<button class="btn btn-primary" onclick="refreshUsers()">
|
|
||||||
<i class="bi bi-arrow-clockwise"></i> 刷新
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div id="loadingUsers" class="text-center py-4">
|
|
||||||
<div class="spinner-border" role="status">
|
|
||||||
<span class="visually-hidden">加载中...</span>
|
|
||||||
</div>
|
|
||||||
<p class="mt-2">正在加载用户信息...</p>
|
|
||||||
</div>
|
|
||||||
<div id="usersList" class="row" style="display: none;"></div>
|
|
||||||
<div id="noUsers" class="text-center py-4" style="display: none;">
|
|
||||||
<i class="bi bi-people" style="font-size: 3rem; color: #ccc;"></i>
|
|
||||||
<p class="mt-2 text-muted">暂无用户</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 删除确认模态框 -->
|
|
||||||
<div class="modal fade" id="deleteUserModal" tabindex="-1">
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title">
|
|
||||||
<i class="bi bi-exclamation-triangle text-warning"></i> 确认删除
|
|
||||||
</h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<p>您确定要删除用户 <strong id="deleteUserName"></strong> 吗?</p>
|
|
||||||
<div class="alert alert-warning">
|
|
||||||
<i class="bi bi-exclamation-triangle"></i>
|
|
||||||
<strong>警告:</strong>此操作将删除该用户的所有数据,包括:
|
|
||||||
<ul class="mt-2 mb-0">
|
|
||||||
<li>所有Cookie账号</li>
|
|
||||||
<li>所有卡券</li>
|
|
||||||
<li>所有关键字和回复设置</li>
|
|
||||||
<li>所有个人设置</li>
|
|
||||||
</ul>
|
|
||||||
<p class="mt-2 mb-0"><strong>此操作不可恢复!</strong></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
|
||||||
<button type="button" class="btn btn-danger-custom" onclick="confirmDeleteUser()">
|
|
||||||
<i class="bi bi-trash"></i> 确认删除
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="/static/lib/bootstrap/bootstrap.bundle.min.js"></script>
|
|
||||||
<script>
|
|
||||||
let currentDeleteUserId = null;
|
|
||||||
let deleteUserModal = null;
|
|
||||||
|
|
||||||
// 页面加载完成后初始化
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
// 检查管理员权限
|
|
||||||
checkAdminPermission();
|
|
||||||
|
|
||||||
// 初始化模态框
|
|
||||||
deleteUserModal = new bootstrap.Modal(document.getElementById('deleteUserModal'));
|
|
||||||
|
|
||||||
// 加载数据
|
|
||||||
loadSystemStats();
|
|
||||||
loadUsers();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 检查管理员权限
|
|
||||||
function checkAdminPermission() {
|
|
||||||
const token = localStorage.getItem('auth_token');
|
|
||||||
if (!token) {
|
|
||||||
alert('请先登录');
|
|
||||||
window.location.href = '/login.html';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证token并检查是否为管理员
|
|
||||||
fetch('/verify', {
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${token}`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
if (!data.authenticated) {
|
|
||||||
alert('登录已过期,请重新登录');
|
|
||||||
window.location.href = '/login.html';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否为管理员
|
|
||||||
if (data.username !== 'admin') {
|
|
||||||
alert('此功能仅限管理员使用');
|
|
||||||
window.location.href = '/';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('权限验证失败:', error);
|
|
||||||
alert('权限验证失败');
|
|
||||||
window.location.href = '/login.html';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载系统统计信息
|
|
||||||
function loadSystemStats() {
|
|
||||||
const token = localStorage.getItem('auth_token');
|
|
||||||
|
|
||||||
fetch('/admin/stats', {
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${token}`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
document.getElementById('totalUsers').textContent = data.users.total;
|
|
||||||
document.getElementById('totalCookies').textContent = data.cookies.total;
|
|
||||||
document.getElementById('totalCards').textContent = data.cards.total;
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('加载统计信息失败:', error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载用户列表
|
|
||||||
function loadUsers() {
|
|
||||||
const token = localStorage.getItem('auth_token');
|
|
||||||
const loadingDiv = document.getElementById('loadingUsers');
|
|
||||||
const usersListDiv = document.getElementById('usersList');
|
|
||||||
const noUsersDiv = document.getElementById('noUsers');
|
|
||||||
|
|
||||||
loadingDiv.style.display = 'block';
|
|
||||||
usersListDiv.style.display = 'none';
|
|
||||||
noUsersDiv.style.display = 'none';
|
|
||||||
|
|
||||||
fetch('/admin/users', {
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${token}`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
loadingDiv.style.display = 'none';
|
|
||||||
|
|
||||||
if (data.users && data.users.length > 0) {
|
|
||||||
displayUsers(data.users);
|
|
||||||
usersListDiv.style.display = 'block';
|
|
||||||
} else {
|
|
||||||
noUsersDiv.style.display = 'block';
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('加载用户列表失败:', error);
|
|
||||||
loadingDiv.style.display = 'none';
|
|
||||||
noUsersDiv.style.display = 'block';
|
|
||||||
alert('加载用户列表失败');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示用户列表
|
|
||||||
function displayUsers(users) {
|
|
||||||
const usersListDiv = document.getElementById('usersList');
|
|
||||||
usersListDiv.innerHTML = '';
|
|
||||||
|
|
||||||
users.forEach(user => {
|
|
||||||
const userCard = createUserCard(user);
|
|
||||||
usersListDiv.appendChild(userCard);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建用户卡片
|
|
||||||
function createUserCard(user) {
|
|
||||||
const col = document.createElement('div');
|
|
||||||
col.className = 'col-md-6 col-lg-4 mb-3';
|
|
||||||
|
|
||||||
const isAdmin = user.username === 'admin';
|
|
||||||
const badgeClass = isAdmin ? 'bg-danger' : 'bg-primary';
|
|
||||||
const badgeText = isAdmin ? '管理员' : '普通用户';
|
|
||||||
|
|
||||||
col.innerHTML = `
|
|
||||||
<div class="card user-card h-100">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
|
||||||
<h6 class="card-title mb-0">
|
|
||||||
<i class="bi bi-person-circle"></i> ${user.username}
|
|
||||||
</h6>
|
|
||||||
<span class="badge ${badgeClass}">${badgeText}</span>
|
|
||||||
</div>
|
|
||||||
<p class="card-text text-muted small mb-2">
|
|
||||||
<i class="bi bi-envelope"></i> ${user.email}
|
|
||||||
</p>
|
|
||||||
<div class="row text-center mb-3">
|
|
||||||
<div class="col">
|
|
||||||
<small class="text-muted">Cookie数</small>
|
|
||||||
<div class="fw-bold">${user.cookie_count || 0}</div>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<small class="text-muted">卡券数</small>
|
|
||||||
<div class="fw-bold">${user.card_count || 0}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="text-muted small mb-2">
|
|
||||||
<i class="bi bi-calendar"></i> 注册时间:${formatDate(user.created_at)}
|
|
||||||
</div>
|
|
||||||
${!isAdmin ? `
|
|
||||||
<div class="d-grid">
|
|
||||||
<button class="btn btn-danger-custom btn-sm" onclick="deleteUser(${user.id}, '${user.username}')">
|
|
||||||
<i class="bi bi-trash"></i> 删除用户
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
` : `
|
|
||||||
<div class="text-center">
|
|
||||||
<small class="text-muted">
|
|
||||||
<i class="bi bi-shield-check"></i> 管理员账号不可删除
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
`}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
return col;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 格式化日期
|
|
||||||
function formatDate(dateString) {
|
|
||||||
const date = new Date(dateString);
|
|
||||||
return date.toLocaleDateString('zh-CN') + ' ' + date.toLocaleTimeString('zh-CN');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 删除用户
|
|
||||||
function deleteUser(userId, username) {
|
|
||||||
currentDeleteUserId = userId;
|
|
||||||
document.getElementById('deleteUserName').textContent = username;
|
|
||||||
deleteUserModal.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确认删除用户
|
|
||||||
function confirmDeleteUser() {
|
|
||||||
if (!currentDeleteUserId) return;
|
|
||||||
|
|
||||||
const token = localStorage.getItem('auth_token');
|
|
||||||
|
|
||||||
fetch(`/admin/users/${currentDeleteUserId}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${token}`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
deleteUserModal.hide();
|
|
||||||
alert(data.message || '用户删除成功');
|
|
||||||
|
|
||||||
// 刷新页面数据
|
|
||||||
loadSystemStats();
|
|
||||||
loadUsers();
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('删除用户失败:', error);
|
|
||||||
alert('删除用户失败');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 刷新用户列表
|
|
||||||
function refreshUsers() {
|
|
||||||
loadSystemStats();
|
|
||||||
loadUsers();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 退出登录
|
|
||||||
function logout() {
|
|
||||||
localStorage.removeItem('auth_token');
|
|
||||||
window.location.href = '/login.html';
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -57,23 +57,6 @@ class XianyuSearcher:
|
|||||||
return default
|
return default
|
||||||
return data
|
return data
|
||||||
|
|
||||||
async def test_browser_launch(self):
|
|
||||||
"""测试浏览器是否能正常启动"""
|
|
||||||
try:
|
|
||||||
if not PLAYWRIGHT_AVAILABLE:
|
|
||||||
return False, "Playwright 未安装"
|
|
||||||
|
|
||||||
playwright = await async_playwright().start()
|
|
||||||
browser = await playwright.chromium.launch(headless=True)
|
|
||||||
context = await browser.new_context()
|
|
||||||
page = await context.new_page()
|
|
||||||
await page.goto("https://www.baidu.com")
|
|
||||||
await asyncio.sleep(2)
|
|
||||||
await browser.close()
|
|
||||||
return True, "浏览器测试成功"
|
|
||||||
except Exception as e:
|
|
||||||
return False, f"浏览器测试失败: {str(e)}"
|
|
||||||
|
|
||||||
async def get_first_valid_cookie(self):
|
async def get_first_valid_cookie(self):
|
||||||
"""获取第一个有效的cookie"""
|
"""获取第一个有效的cookie"""
|
||||||
try:
|
try:
|
||||||
@ -889,123 +872,6 @@ class XianyuSearcher:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async def _parse_item_old(self, item_data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
解析单个商品数据
|
|
||||||
|
|
||||||
Args:
|
|
||||||
item_data: 商品原始数据
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
解析后的商品信息
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# 基本信息 - 适配多种可能的字段名
|
|
||||||
item_id = (item_data.get('id') or
|
|
||||||
item_data.get('itemId') or
|
|
||||||
item_data.get('item_id') or
|
|
||||||
item_data.get('spuId') or '')
|
|
||||||
|
|
||||||
title = (item_data.get('title') or
|
|
||||||
item_data.get('name') or
|
|
||||||
item_data.get('itemTitle') or
|
|
||||||
item_data.get('subject') or '')
|
|
||||||
|
|
||||||
# 价格处理
|
|
||||||
price_raw = (item_data.get('price') or
|
|
||||||
item_data.get('priceText') or
|
|
||||||
item_data.get('currentPrice') or
|
|
||||||
item_data.get('realPrice') or '')
|
|
||||||
|
|
||||||
# 清理价格格式
|
|
||||||
if isinstance(price_raw, (int, float)):
|
|
||||||
price = str(price_raw)
|
|
||||||
elif isinstance(price_raw, str):
|
|
||||||
price = price_raw.replace('¥', '').replace('元', '').strip()
|
|
||||||
else:
|
|
||||||
price = '价格面议'
|
|
||||||
|
|
||||||
# 卖家信息
|
|
||||||
seller_info = (item_data.get('seller') or
|
|
||||||
item_data.get('user') or
|
|
||||||
item_data.get('owner') or {})
|
|
||||||
|
|
||||||
seller_name = ''
|
|
||||||
if seller_info:
|
|
||||||
seller_name = (seller_info.get('nick') or
|
|
||||||
seller_info.get('nickname') or
|
|
||||||
seller_info.get('name') or
|
|
||||||
seller_info.get('userName') or '匿名用户')
|
|
||||||
|
|
||||||
# 商品链接
|
|
||||||
if item_id:
|
|
||||||
item_url = f"https://www.goofish.com/item?id={item_id}"
|
|
||||||
else:
|
|
||||||
item_url = item_data.get('url', item_data.get('link', ''))
|
|
||||||
|
|
||||||
# 发布时间
|
|
||||||
publish_time = (item_data.get('publishTime') or
|
|
||||||
item_data.get('createTime') or
|
|
||||||
item_data.get('gmtCreate') or
|
|
||||||
item_data.get('time') or '')
|
|
||||||
|
|
||||||
# 格式化时间
|
|
||||||
if publish_time and isinstance(publish_time, (int, float)):
|
|
||||||
import datetime
|
|
||||||
publish_time = datetime.datetime.fromtimestamp(publish_time / 1000).strftime('%Y-%m-%d')
|
|
||||||
|
|
||||||
# 商品标签
|
|
||||||
tags = item_data.get('tags', item_data.get('labels', []))
|
|
||||||
tag_names = []
|
|
||||||
if isinstance(tags, list):
|
|
||||||
for tag in tags:
|
|
||||||
if isinstance(tag, dict):
|
|
||||||
tag_name = (tag.get('name') or
|
|
||||||
tag.get('text') or
|
|
||||||
tag.get('label') or '')
|
|
||||||
if tag_name:
|
|
||||||
tag_names.append(tag_name)
|
|
||||||
elif isinstance(tag, str):
|
|
||||||
tag_names.append(tag)
|
|
||||||
|
|
||||||
# 图片
|
|
||||||
images = (item_data.get('images') or
|
|
||||||
item_data.get('pics') or
|
|
||||||
item_data.get('pictures') or
|
|
||||||
item_data.get('imgList') or [])
|
|
||||||
|
|
||||||
main_image = ''
|
|
||||||
if images and len(images) > 0:
|
|
||||||
if isinstance(images[0], str):
|
|
||||||
main_image = images[0]
|
|
||||||
elif isinstance(images[0], dict):
|
|
||||||
main_image = (images[0].get('url') or
|
|
||||||
images[0].get('src') or
|
|
||||||
images[0].get('image') or '')
|
|
||||||
|
|
||||||
# 如果没有图片,使用默认占位图
|
|
||||||
if not main_image:
|
|
||||||
main_image = f'https://via.placeholder.com/200x200?text={title[:10] if title else "商品"}...'
|
|
||||||
|
|
||||||
return {
|
|
||||||
'item_id': item_id,
|
|
||||||
'title': title,
|
|
||||||
'price': price,
|
|
||||||
'seller_name': seller_name,
|
|
||||||
'item_url': item_url,
|
|
||||||
'publish_time': publish_time,
|
|
||||||
'tags': tag_names,
|
|
||||||
'main_image': main_image,
|
|
||||||
'raw_data': item_data # 保留原始数据以备后用
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"解析商品数据失败: {str(e)}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
# 搜索器工具函数
|
# 搜索器工具函数
|
||||||
|
|
||||||
async def search_xianyu_items(keyword: str, page: int = 1, page_size: int = 20) -> Dict[str, Any]:
|
async def search_xianyu_items(keyword: str, page: int = 1, page_size: int = 20) -> Dict[str, Any]:
|
||||||
@ -1130,50 +996,3 @@ async def search_multiple_pages_xianyu(keyword: str, total_pages: int = 1) -> Di
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async def get_item_detail_from_api(item_id: str) -> Optional[str]:
|
|
||||||
"""
|
|
||||||
从外部API获取商品详情
|
|
||||||
|
|
||||||
Args:
|
|
||||||
item_id: 商品ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
商品详情文本,获取失败返回None
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# 使用默认的API配置
|
|
||||||
api_base_url = 'https://selfapi.zhinianboke.com/api/getItemDetail'
|
|
||||||
timeout_seconds = 10
|
|
||||||
|
|
||||||
api_url = f"{api_base_url}/{item_id}"
|
|
||||||
|
|
||||||
logger.info(f"正在从外部API获取商品详情: {item_id}")
|
|
||||||
|
|
||||||
# 使用简单的HTTP请求
|
|
||||||
import aiohttp
|
|
||||||
timeout = aiohttp.ClientTimeout(total=timeout_seconds)
|
|
||||||
|
|
||||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
|
||||||
async with session.get(api_url) as response:
|
|
||||||
if response.status == 200:
|
|
||||||
result = await response.json()
|
|
||||||
|
|
||||||
# 检查返回状态
|
|
||||||
if result.get('status') == '200' and result.get('data'):
|
|
||||||
item_detail = result['data']
|
|
||||||
logger.info(f"成功获取商品详情: {item_id}, 长度: {len(item_detail)}")
|
|
||||||
return item_detail
|
|
||||||
else:
|
|
||||||
logger.warning(f"API返回状态异常: {result.get('status')}, message: {result.get('message')}")
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
logger.warning(f"API请求失败: HTTP {response.status}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
logger.warning(f"获取商品详情超时: {item_id}")
|
|
||||||
return None
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"获取商品详情异常: {item_id}, 错误: {str(e)}")
|
|
||||||
return None
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user