提交一些更改
This commit is contained in:
9
app/.env
9
app/.env
@@ -3,7 +3,7 @@ workflow_id=7608812635877900322
|
||||
mysql_host=47.110.241.161
|
||||
mysql_user=aiimage
|
||||
|
||||
proxy_url=https://api.jikip.com/ip-get?num=1&minute=1&format=json&area=all&protocol=1&mode=2&key=t24g6gi44ubufd8
|
||||
proxy_url=https://api.jikip.com/ip-get?num=1&minute=3&format=json&area=all&protocol=1&mode=2&key=t24g6gi44ubufd8
|
||||
proxy_mode=2
|
||||
|
||||
zn_company=rongchuang123
|
||||
@@ -11,9 +11,10 @@ zn_username=%E8%87%AA%E5%8A%A8%E5%8C%96_Robot
|
||||
|
||||
client_name=ShuFuAI
|
||||
|
||||
|
||||
# java_api_base=http://47.111.163.154:18080
|
||||
# java_api_base=http://127.0.0.1:18080
|
||||
java_api_base=http://121.196.149.225:18080
|
||||
java_api_base=http://127.0.0.1:18080
|
||||
# java_api_base=http://121.196.149.225:18080
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -484,7 +484,11 @@ class SimilarAsinTask(TaskBase):
|
||||
|
||||
return_data = {
|
||||
'image_url': "",
|
||||
'title': ""
|
||||
'title': "",
|
||||
'category' : '',
|
||||
'sku' : '',
|
||||
'success' : False,
|
||||
'similar_data' : []
|
||||
}
|
||||
asin = value.get("asin")
|
||||
country = value.get("country")
|
||||
@@ -494,8 +498,9 @@ class SimilarAsinTask(TaskBase):
|
||||
self.log(f"抓取结果->{return_data}")
|
||||
break
|
||||
except Exception as e:
|
||||
if "与页面的连接已断开" in str(e):
|
||||
chrome = ChromeAmzone()
|
||||
# if "与页面的连接已断开" in str(e):
|
||||
chrome = ChromeAmzone()
|
||||
self.log(f"{asin}抓取数据报错,{e}")
|
||||
if not isinstance(return_data, dict):
|
||||
return_data = {}
|
||||
if return_data.get("image_url"):
|
||||
@@ -564,7 +569,8 @@ class SimilarAsinTask(TaskBase):
|
||||
self.log(f"任务 {task_id} 已被暂停!")
|
||||
else:
|
||||
runing_task[task_id]["status"] = "completed"
|
||||
self.log(f"任务 {task_id} 处理完成!")
|
||||
|
||||
self.log(f"任务 {task_id} 处理完成!")
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"任务处理失败: {traceback.format_exc()}", "ERROR")
|
||||
|
||||
File diff suppressed because one or more lines are too long
1
app/assets/appearance-patent-eAIM0l0f.css
Normal file
1
app/assets/appearance-patent-eAIM0l0f.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
import{bb as r}from"./pywebview-84Lhn3TI.js";const n="";function s(e){return r(`${n}/api/brand/expand-folder-recursive`,{folder:e})}export{s as e};
|
||||
import{b8 as r}from"./pywebview-3bFwh9Cg.js";const n="";function s(e){return r(`${n}/api/brand/expand-folder-recursive`,{folder:e})}export{s as e};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
app/assets/query-asin-CACA04ev.css
Normal file
1
app/assets/query-asin-CACA04ev.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
app/assets/similar-asin-B5CvMmwV.css
Normal file
1
app/assets/similar-asin-B5CvMmwV.css
Normal file
@@ -0,0 +1 @@
|
||||
.module-page[data-v-49f8fcb3]{min-height:100vh;background:#1a1a1a}.main-content[data-v-49f8fcb3]{display:flex;height:calc(100vh - 56px);min-height:calc(100vh - 56px)}.left-panel[data-v-49f8fcb3]{width:400px;background:#1e1e1e;padding:20px;overflow-y:auto;border-right:1px solid #2a2a2a}.right-panel[data-v-49f8fcb3]{flex:1;min-width:0;background:#1a1a1a;display:flex;flex-direction:column}.section-title[data-v-49f8fcb3],.subsection-title[data-v-49f8fcb3]{font-size:13px;color:#bbb;margin-bottom:10px}.upload-zone[data-v-49f8fcb3]{border:1px dashed #3a3a3a;border-radius:10px;padding:18px;background:#252525;margin-bottom:18px}.hint[data-v-49f8fcb3],.loading-msg[data-v-49f8fcb3],.files[data-v-49f8fcb3],.muted[data-v-49f8fcb3]{color:#888;font-size:12px;line-height:1.5}.link[data-v-49f8fcb3]{color:#6ea8fe;text-decoration:none}.link[data-v-49f8fcb3]:hover{color:#9fc5ff}.btns[data-v-49f8fcb3],.run-row[data-v-49f8fcb3]{display:flex;gap:10px;flex-wrap:wrap}.opt-btn[data-v-49f8fcb3],.btn-run[data-v-49f8fcb3],.btn-delete[data-v-49f8fcb3],.download[data-v-49f8fcb3]{border:none;cursor:pointer;border-radius:7px}.opt-btn[data-v-49f8fcb3]{padding:8px 14px;color:#ccc;background:#2a2a2a;border:1px solid #3a3a3a}.btn-run[data-v-49f8fcb3]{padding:10px 18px;color:#fff;background:#3498db;font-weight:600}.btn-queue[data-v-49f8fcb3]{background:#27ae60}.btn-run[data-v-49f8fcb3]:disabled{opacity:.55;cursor:not-allowed}.selected-files[data-v-49f8fcb3]{margin-top:14px;color:#999;font-size:12px;word-break:break-all}.selected-files span[data-v-49f8fcb3]{display:block;margin:4px 0}.parse-card[data-v-49f8fcb3],.queue-payload[data-v-49f8fcb3]{margin-top:14px;padding:12px;border:1px solid #2a2a2a;border-radius:8px;background:#202020;color:#b8c1cc;font-size:12px}.queue-payload[data-v-49f8fcb3]{max-height:220px;overflow:auto;color:#8fd3ff;white-space:pre-wrap}.panel-header[data-v-49f8fcb3]{padding:16px 20px;border-bottom:1px solid #2a2a2a;font-size:15px;font-weight:600;color:#ddd}.task-list-wrap[data-v-49f8fcb3]{flex:1;padding:16px 20px;overflow:auto}.clean-result-summary[data-v-49f8fcb3]{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin-bottom:16px}.summary-card[data-v-49f8fcb3]{padding:14px 16px;border:1px solid #2a2a2a;border-radius:8px;background:#1e1e1e}.summary-card strong[data-v-49f8fcb3]{display:block;margin-top:8px;color:#eaf4ff;font-size:22px}.summary-label[data-v-49f8fcb3]{color:#8d8d8d;font-size:12px}.result-list-wrap[data-v-49f8fcb3]{border:1px solid #2a2a2a;border-radius:8px;background:#1e1e1e;min-height:180px;margin:0 0 16px}.result-list-header[data-v-49f8fcb3]{display:flex;justify-content:space-between;padding:12px 16px;border-bottom:1px solid #2a2a2a;color:#ddd;font-size:14px}.empty-tasks[data-v-49f8fcb3]{color:#666;font-size:13px;padding:18px;text-align:center}.task-list[data-v-49f8fcb3]{list-style:none;margin:0;padding:12px}.task-item[data-v-49f8fcb3]{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;padding:12px 14px;border:1px solid #2a2a2a;border-radius:8px;margin-bottom:8px;background:#222}.left[data-v-49f8fcb3]{flex:1;min-width:0}.id[data-v-49f8fcb3]{color:#e0e0e0;font-size:13px;font-weight:600}.task-right[data-v-49f8fcb3]{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.status[data-v-49f8fcb3]{padding:4px 10px;border-radius:6px;font-size:12px}.status.success[data-v-49f8fcb3]{background:#2ecc712e;color:#2ecc71}.status.failed[data-v-49f8fcb3]{background:#e74c3c2e;color:#ff6b6b}.status.running[data-v-49f8fcb3]{background:#3498db2e;color:#3498db}.status.pending[data-v-49f8fcb3]{background:#95a5a62e;color:#bdc3c7}.result-hint[data-v-49f8fcb3]{margin-top:6px;color:#e0b96d}.file-progress[data-v-49f8fcb3]{margin-top:8px;max-width:520px}.file-progress-meta[data-v-49f8fcb3]{display:flex;justify-content:space-between;gap:12px;color:#d8c278;font-size:12px}.file-progress-track[data-v-49f8fcb3]{margin-top:5px;height:8px;border-radius:999px;overflow:hidden;background:#303030;border:1px solid #3b3b3b}.file-progress-bar[data-v-49f8fcb3]{height:100%;border-radius:inherit;background:linear-gradient(90deg,#4aa3ff,#f0c75e);transition:width .25s ease}.file-progress-count[data-v-49f8fcb3]{margin-top:4px;color:#858585;font-size:11px}.download[data-v-49f8fcb3]{padding:6px 10px;color:#d6ecff;background:#3498db2e}.btn-delete[data-v-49f8fcb3]{padding:6px 10px;color:#ff8f8f;background:#e74c3c1f}@media(max-width:1100px){.main-content[data-v-49f8fcb3]{flex-direction:column;height:auto}.left-panel[data-v-49f8fcb3]{width:100%;border-right:none;border-bottom:1px solid #2a2a2a}.clean-result-summary[data-v-49f8fcb3]{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -5,8 +5,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>格式转换 - 数富AI</title>
|
||||
<script type="module" crossorigin src="/assets/convert.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-84Lhn3TI.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/brand-DIMFUl0j.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-3bFwh9Cg.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/brand-D__LhhPM.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-B7UKcPVJ.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/convert-BKSNvX8i.css">
|
||||
</head>
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>数据去重 - 数富AI</title>
|
||||
<script type="module" crossorigin src="/assets/dedupe.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-84Lhn3TI.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/brand-DIMFUl0j.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-3bFwh9Cg.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/brand-D__LhhPM.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-B7UKcPVJ.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/dedupe-DNlVfFj-.css">
|
||||
</head>
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>删除品牌 - 数富AI</title>
|
||||
<script type="module" crossorigin src="/assets/delete-brand.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-84Lhn3TI.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/brand-DIMFUl0j.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-3bFwh9Cg.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/brand-D__LhhPM.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/categorized-timers-JPA-olTr.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-B7UKcPVJ.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/delete-brand-BJod9K65.css">
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>数据拆分 - 数富AI</title>
|
||||
<script type="module" crossorigin src="/assets/split.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-84Lhn3TI.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/brand-DIMFUl0j.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-3bFwh9Cg.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/brand-D__LhhPM.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-B7UKcPVJ.css">
|
||||
<link rel="stylesheet" crossorigin href="/assets/split-BoBVrLdC.css">
|
||||
</head>
|
||||
|
||||
@@ -1,539 +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>管理后台 - 南日AI</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: "Microsoft YaHei", "PingFang SC", sans-serif;
|
||||
background: #f5f5f5;
|
||||
padding: 24px;
|
||||
}
|
||||
.nav { margin-bottom: 24px; }
|
||||
.nav a { color: #667eea; text-decoration: none; }
|
||||
.nav a:hover { text-decoration: underline; }
|
||||
h1 { font-size: 20px; margin-bottom: 20px; }
|
||||
|
||||
/* Tabs */
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
.tab {
|
||||
padding: 12px 24px;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
font-size: 15px;
|
||||
border-bottom: 2px solid transparent;
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
.tab:hover { color: #667eea; }
|
||||
.tab.active { color: #667eea; font-weight: 600; border-bottom-color: #667eea; }
|
||||
.tab-panel { display: none; }
|
||||
.tab-panel.active { display: block; }
|
||||
|
||||
/* Form & Table */
|
||||
.form-box, .panel-box {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.form-group { margin-bottom: 16px; }
|
||||
.form-group label { display: block; font-size: 14px; margin-bottom: 6px; }
|
||||
.form-group input, .form-group select {
|
||||
width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 6px;
|
||||
}
|
||||
.form-group input[type="checkbox"] { width: auto; margin-right: 8px; }
|
||||
.form-row { display: flex; gap: 16px; flex-wrap: wrap; align-items: flex-end; margin-bottom: 16px; }
|
||||
.form-row .form-group { margin-bottom: 0; flex: 1; min-width: 120px; }
|
||||
|
||||
.btn { padding: 10px 20px; background: #667eea; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; }
|
||||
.btn:hover { opacity: 0.9; }
|
||||
.btn-sm { padding: 6px 12px; font-size: 13px; }
|
||||
.btn-danger { background: #e74c3c; }
|
||||
.btn-secondary { background: #95a5a6; }
|
||||
|
||||
.msg { margin-top: 12px; font-size: 14px; }
|
||||
.msg.ok { color: #27ae60; }
|
||||
.msg.err { color: #e74c3c; }
|
||||
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th, td { padding: 12px; text-align: left; border-bottom: 1px solid #eee; font-size: 14px; }
|
||||
th { background: #f8f9fa; font-weight: 600; }
|
||||
.pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.pagination span { font-size: 14px; color: #666; }
|
||||
.pagination button { padding: 6px 12px; border: 1px solid #ddd; background: #fff; cursor: pointer; border-radius: 4px; }
|
||||
.pagination button:hover:not(:disabled) { background: #f0f0f0; }
|
||||
.pagination button:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
|
||||
.thumb { width: 60px; height: 60px; object-fit: cover; border-radius: 4px; margin-right: 8px; vertical-align: middle; }
|
||||
.thumb-wrap { display: flex; flex-wrap: wrap; gap: 4px; }
|
||||
.empty-tip { color: #999; font-size: 14px; padding: 24px; text-align: center; }
|
||||
.modal-mask {
|
||||
display: none;
|
||||
position: fixed; left: 0; top: 0; right: 0; bottom: 0;
|
||||
background: rgba(0,0,0,0.4); z-index: 1000;
|
||||
align-items: center; justify-content: center;
|
||||
}
|
||||
.modal-mask.show { display: flex; }
|
||||
.modal { background: #fff; border-radius: 8px; padding: 24px; min-width: 320px; max-width: 90%; }
|
||||
.modal h3 { margin-bottom: 16px; font-size: 16px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="nav"><a href="/home">← 返回首页</a></div>
|
||||
<h1>管理后台</h1>
|
||||
|
||||
<div class="tabs">
|
||||
<div class="tab active" data-tab="users">用户管理</div>
|
||||
<div class="tab" data-tab="history">查看生成记录</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户管理 -->
|
||||
<div id="panel-users" class="tab-panel active">
|
||||
<div class="form-box">
|
||||
<h3 style="margin-bottom:16px;font-size:15px;">创建用户</h3>
|
||||
<p style="margin-bottom:16px;font-size:13px;color:#666;">无注册入口,仅管理员可在此创建用户。层级:超级管理员 → 管理员 → 普通号。</p>
|
||||
<div class="form-group">
|
||||
<label>用户名</label>
|
||||
<input type="text" id="username" placeholder="用户名(至少2个字符)">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>密码</label>
|
||||
<input type="password" id="password" placeholder="密码(至少6个字符)">
|
||||
</div>
|
||||
<div class="form-group" id="formGroupRole">
|
||||
<label>角色</label>
|
||||
<select id="createRole">
|
||||
<option value="normal">普通号</option>
|
||||
<option value="admin" id="optAdmin">管理员</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" id="formGroupCreatedBy" style="display:none;">
|
||||
<label>所属管理员</label>
|
||||
<select id="createCreatedBy">
|
||||
<option value="">请选择管理员</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn" id="btnCreate">创建用户</button>
|
||||
<p class="msg" id="msgCreate"></p>
|
||||
</div>
|
||||
|
||||
<div class="panel-box">
|
||||
<h3 style="margin-bottom:16px;font-size:15px;">用户列表</h3>
|
||||
<div class="form-row" style="margin-bottom:16px;">
|
||||
<div class="form-group" style="min-width:180px;">
|
||||
<label>用户名(模糊搜索)</label>
|
||||
<input type="text" id="searchUsername" placeholder="输入用户名关键字">
|
||||
</div>
|
||||
<div class="form-group" id="filterCreatedByGroup" style="min-width:160px;display:none;">
|
||||
<label>所属管理员</label>
|
||||
<select id="filterCreatedBy">
|
||||
<option value="">全部</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn" id="btnSearchUsers">查询</button>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>用户名</th>
|
||||
<th>角色</th>
|
||||
<th>所属管理员</th>
|
||||
<th>创建时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="userListBody"></tbody>
|
||||
</table>
|
||||
<div class="pagination" id="userPagination"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 查看生成记录 -->
|
||||
<div id="panel-history" class="tab-panel">
|
||||
<div class="form-box">
|
||||
<h3 style="margin-bottom:16px;font-size:15px;">筛选条件</h3>
|
||||
<div class="form-row">
|
||||
<div class="form-group" style="min-width:140px;">
|
||||
<label>指定用户</label>
|
||||
<select id="filterUser">
|
||||
<option value="">全部用户</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" style="min-width:140px;">
|
||||
<label>开始时间</label>
|
||||
<input type="datetime-local" id="filterTimeStart">
|
||||
</div>
|
||||
<div class="form-group" style="min-width:140px;">
|
||||
<label>结束时间</label>
|
||||
<input type="datetime-local" id="filterTimeEnd">
|
||||
</div>
|
||||
<button class="btn" id="btnFilterHistory">查询</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-box">
|
||||
<h3 style="margin-bottom:16px;font-size:15px;">生成记录</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>用户</th>
|
||||
<th>类型</th>
|
||||
<th>创建时间</th>
|
||||
<th>结果预览</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="historyListBody"></tbody>
|
||||
</table>
|
||||
<div class="pagination" id="historyPagination"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑用户弹窗 -->
|
||||
<div class="modal-mask" id="editUserModal">
|
||||
<div class="modal">
|
||||
<h3>编辑用户</h3>
|
||||
<input type="hidden" id="editUserId">
|
||||
<div class="form-group">
|
||||
<label>用户名</label>
|
||||
<input type="text" id="editUsername" readonly style="background:#f5f5f5;">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>新密码(不修改留空)</label>
|
||||
<input type="password" id="editPassword" placeholder="留空则不修改密码">
|
||||
</div>
|
||||
<div class="form-group" id="editFormGroupRole" style="display:none;">
|
||||
<label>角色</label>
|
||||
<select id="editRole">
|
||||
<option value="normal">普通号</option>
|
||||
<option value="admin">管理员</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" id="editFormGroupCreator" style="display:none;">
|
||||
<label>所属管理员</label>
|
||||
<input type="text" id="editCreatorName" readonly style="background:#f5f5f5;">
|
||||
</div>
|
||||
<p class="msg" id="msgEdit"></p>
|
||||
<div style="margin-top:16px;display:flex;gap:8px;">
|
||||
<button class="btn" id="btnSaveUser">保存</button>
|
||||
<button class="btn btn-secondary" id="btnCloseEdit">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
// Tab 切换
|
||||
document.querySelectorAll('.tab').forEach(function(t) {
|
||||
t.onclick = function() {
|
||||
document.querySelectorAll('.tab').forEach(function(x) { x.classList.remove('active'); });
|
||||
document.querySelectorAll('.tab-panel').forEach(function(x) { x.classList.remove('active'); });
|
||||
t.classList.add('active');
|
||||
var id = 'panel-' + t.dataset.tab;
|
||||
document.getElementById(id).classList.add('active');
|
||||
if (t.dataset.tab === 'users') loadUsers(1);
|
||||
else if (t.dataset.tab === 'history') loadHistory(1);
|
||||
};
|
||||
});
|
||||
|
||||
// ========== 用户管理 ==========
|
||||
var userPage = 1, userPageSize = 15;
|
||||
var currentUserRole = 'admin';
|
||||
var adminsList = [];
|
||||
function roleLabel(role) {
|
||||
if (role === 'super_admin') return '超级管理员';
|
||||
if (role === 'admin') return '管理员';
|
||||
return '普通号';
|
||||
}
|
||||
function buildUserListQuery(page) {
|
||||
var q = 'page=' + (page || 1) + '&page_size=' + userPageSize;
|
||||
var kw = (document.getElementById('searchUsername').value || '').trim();
|
||||
if (kw) q += '&username=' + encodeURIComponent(kw);
|
||||
var cby = document.getElementById('filterCreatedBy').value;
|
||||
if (cby) q += '&created_by_id=' + encodeURIComponent(cby);
|
||||
return q;
|
||||
}
|
||||
function loadUsers(page) {
|
||||
userPage = page || 1;
|
||||
fetch('/api/admin/users?' + buildUserListQuery(userPage))
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(res) {
|
||||
var tbody = document.getElementById('userListBody');
|
||||
if (!res.success) {
|
||||
tbody.innerHTML = '<tr><td colspan="6" class="empty-tip">加载失败: ' + (res.error || '') + '</td></tr>';
|
||||
return;
|
||||
}
|
||||
currentUserRole = res.current_user_role || 'admin';
|
||||
adminsList = res.admins || [];
|
||||
var items = res.items || [];
|
||||
if (items.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="6" class="empty-tip">暂无用户</td></tr>';
|
||||
} else {
|
||||
tbody.innerHTML = items.map(function(u) {
|
||||
return '<tr><td>' + u.id + '</td><td>' + (u.username || '') + '</td><td>' +
|
||||
roleLabel(u.role || 'normal') + '</td><td>' + (u.creator_username || '-') + '</td><td>' + (u.created_at || '') + '</td><td>' +
|
||||
'<button class="btn btn-sm" data-edit="' + u.id + '" data-user="' + (JSON.stringify(u).replace(/"/g, '"')) + '">编辑</button> ' +
|
||||
'<button class="btn btn-sm btn-danger" data-delete="' + u.id + '" data-name="' + (u.username || '').replace(/"/g, '"') + '">删除</button>' +
|
||||
'</td></tr>';
|
||||
}).join('');
|
||||
}
|
||||
renderPagination('userPagination', res.total, res.page, res.page_size, loadUsers);
|
||||
bindUserActions();
|
||||
updateCreateFormByRole();
|
||||
updateUserFilterByRole();
|
||||
})
|
||||
.catch(function() {
|
||||
document.getElementById('userListBody').innerHTML = '<tr><td colspan="6" class="empty-tip">请求失败</td></tr>';
|
||||
});
|
||||
}
|
||||
function updateUserFilterByRole() {
|
||||
var grp = document.getElementById('filterCreatedByGroup');
|
||||
var sel = document.getElementById('filterCreatedBy');
|
||||
if (currentUserRole === 'super_admin') {
|
||||
grp.style.display = 'block';
|
||||
var cur = sel.value;
|
||||
sel.innerHTML = '<option value="">全部</option>';
|
||||
adminsList.forEach(function(a) {
|
||||
var opt = document.createElement('option');
|
||||
opt.value = a.id;
|
||||
opt.textContent = a.username;
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
sel.value = cur || '';
|
||||
} else {
|
||||
grp.style.display = 'none';
|
||||
}
|
||||
}
|
||||
function updateCreateFormByRole() {
|
||||
var roleSel = document.getElementById('createRole');
|
||||
var optAdmin = document.getElementById('optAdmin');
|
||||
var formCreatedBy = document.getElementById('formGroupCreatedBy');
|
||||
var selCreatedBy = document.getElementById('createCreatedBy');
|
||||
if (currentUserRole === 'super_admin') {
|
||||
if (optAdmin) optAdmin.style.display = '';
|
||||
formCreatedBy.style.display = (roleSel.value === 'normal') ? 'block' : 'none';
|
||||
selCreatedBy.innerHTML = '<option value="">请选择管理员</option>';
|
||||
adminsList.forEach(function(a) {
|
||||
var opt = document.createElement('option');
|
||||
opt.value = a.id;
|
||||
opt.textContent = a.username;
|
||||
selCreatedBy.appendChild(opt);
|
||||
});
|
||||
} else {
|
||||
if (optAdmin) optAdmin.style.display = 'none';
|
||||
roleSel.value = 'normal';
|
||||
formCreatedBy.style.display = 'none';
|
||||
}
|
||||
}
|
||||
function bindUserActions() {
|
||||
document.querySelectorAll('[data-edit]').forEach(function(btn) {
|
||||
btn.onclick = function() {
|
||||
var raw = (btn.getAttribute('data-user') || '{}').replace(/"/g, '"');
|
||||
var u;
|
||||
try { u = JSON.parse(raw); } catch (e) { u = {}; }
|
||||
document.getElementById('editUserId').value = u.id || '';
|
||||
document.getElementById('editUsername').value = u.username || '';
|
||||
document.getElementById('editPassword').value = '';
|
||||
var editRole = document.getElementById('editRole');
|
||||
var editFormGroupRole = document.getElementById('editFormGroupRole');
|
||||
var editFormGroupCreator = document.getElementById('editFormGroupCreator');
|
||||
var editCreatorName = document.getElementById('editCreatorName');
|
||||
editFormGroupRole.style.display = (currentUserRole === 'super_admin' && u.role !== 'super_admin') ? 'block' : 'none';
|
||||
editFormGroupCreator.style.display = (u.role === 'normal' && u.creator_username) ? 'block' : 'none';
|
||||
editCreatorName.value = u.creator_username || '';
|
||||
if (u.role !== 'super_admin') { editRole.value = u.role || 'normal'; }
|
||||
document.getElementById('msgEdit').textContent = '';
|
||||
document.getElementById('editUserModal').classList.add('show');
|
||||
};
|
||||
});
|
||||
document.querySelectorAll('[data-delete]').forEach(function(btn) {
|
||||
btn.onclick = function() {
|
||||
if (!confirm('确定删除用户 "' + (btn.dataset.name || '') + '" 吗?')) return;
|
||||
fetch('/api/admin/user/' + btn.dataset.delete, { method: 'DELETE' })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(res) {
|
||||
if (res.success) { loadUsers(userPage); }
|
||||
else { alert(res.error || '删除失败'); }
|
||||
});
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('btnSearchUsers').onclick = function() { loadUsers(1); };
|
||||
document.getElementById('createRole').onchange = function() { updateCreateFormByRole(); };
|
||||
document.getElementById('btnCreate').onclick = function() {
|
||||
var username = (document.getElementById('username').value || '').trim();
|
||||
var password = document.getElementById('password').value || '';
|
||||
var role = document.getElementById('createRole').value || 'normal';
|
||||
var createdById = document.getElementById('createCreatedBy').value ? parseInt(document.getElementById('createCreatedBy').value, 10) : null;
|
||||
var msgEl = document.getElementById('msgCreate');
|
||||
msgEl.textContent = '';
|
||||
msgEl.className = 'msg';
|
||||
if (!username || username.length < 2) {
|
||||
msgEl.textContent = '用户名至少2个字符';
|
||||
msgEl.classList.add('err');
|
||||
return;
|
||||
}
|
||||
if (!password || password.length < 6) {
|
||||
msgEl.textContent = '密码至少6个字符';
|
||||
msgEl.classList.add('err');
|
||||
return;
|
||||
}
|
||||
var body = { username: username, password: password, role: role };
|
||||
if (role === 'normal' && currentUserRole === 'super_admin' && createdById) body.created_by_id = createdById;
|
||||
fetch('/api/admin/user', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(res) {
|
||||
if (res.success) {
|
||||
msgEl.textContent = res.msg || '创建成功';
|
||||
msgEl.classList.add('ok');
|
||||
document.getElementById('username').value = '';
|
||||
document.getElementById('password').value = '';
|
||||
loadUsers(1);
|
||||
} else {
|
||||
msgEl.textContent = res.error || '创建失败';
|
||||
msgEl.classList.add('err');
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
msgEl.textContent = '请求失败';
|
||||
msgEl.classList.add('err');
|
||||
});
|
||||
};
|
||||
|
||||
document.getElementById('btnSaveUser').onclick = function() {
|
||||
var uid = document.getElementById('editUserId').value;
|
||||
var password = document.getElementById('editPassword').value;
|
||||
var editRoleEl = document.getElementById('editRole');
|
||||
var msgEl = document.getElementById('msgEdit');
|
||||
msgEl.textContent = '';
|
||||
msgEl.className = 'msg';
|
||||
var body = {};
|
||||
if (password) body.password = password;
|
||||
if (currentUserRole === 'super_admin' && editRoleEl && editRoleEl.offsetParent !== null)
|
||||
body.role = editRoleEl.value || 'normal';
|
||||
fetch('/api/admin/user/' + uid, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(res) {
|
||||
if (res.success) {
|
||||
msgEl.textContent = res.msg || '保存成功';
|
||||
msgEl.classList.add('ok');
|
||||
document.getElementById('editUserModal').classList.remove('show');
|
||||
loadUsers(userPage);
|
||||
} else {
|
||||
msgEl.textContent = res.error || '保存失败';
|
||||
msgEl.classList.add('err');
|
||||
}
|
||||
});
|
||||
};
|
||||
document.getElementById('btnCloseEdit').onclick = function() {
|
||||
document.getElementById('editUserModal').classList.remove('show');
|
||||
};
|
||||
|
||||
// ========== 生成记录 ==========
|
||||
var historyPage = 1, historyPageSize = 15;
|
||||
function toSqlDatetime(val) {
|
||||
if (!val) return '';
|
||||
return val.replace('T', ' ');
|
||||
}
|
||||
function buildHistoryQuery(page) {
|
||||
var q = 'page=' + (page || 1) + '&page_size=' + historyPageSize;
|
||||
var uid = document.getElementById('filterUser').value;
|
||||
var start = toSqlDatetime(document.getElementById('filterTimeStart').value);
|
||||
var end = toSqlDatetime(document.getElementById('filterTimeEnd').value);
|
||||
if (uid) q += '&user_id=' + uid;
|
||||
if (start) q += '&time_start=' + encodeURIComponent(start);
|
||||
if (end) q += '&time_end=' + encodeURIComponent(end);
|
||||
return q;
|
||||
}
|
||||
function loadHistory(page) {
|
||||
historyPage = page || 1;
|
||||
fetch('/api/admin/history?' + buildHistoryQuery(historyPage))
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(res) {
|
||||
var tbody = document.getElementById('historyListBody');
|
||||
if (!res.success) {
|
||||
tbody.innerHTML = '<tr><td colspan="5" class="empty-tip">加载失败: ' + (res.error || '') + '</td></tr>';
|
||||
return;
|
||||
}
|
||||
var items = res.items || [];
|
||||
if (items.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="5" class="empty-tip">暂无记录</td></tr>';
|
||||
} else {
|
||||
tbody.innerHTML = items.map(function(h) {
|
||||
var urls = (h.result_urls || []);
|
||||
var thumbUrls = (h.long_image_url ? [h.long_image_url] : []).concat(urls);
|
||||
var thumbs = thumbUrls.slice(0, 3).map(function(url) {
|
||||
return '<img src="' + (url || '').replace(/"/g, '"') + '" class="thumb" alt="">';
|
||||
}).join('');
|
||||
return '<tr><td>' + h.id + '</td><td>' + (h.username || '-') + '</td><td>' +
|
||||
(h.panel_type || '-') + '</td><td>' + (h.created_at || '') + '</td><td>' +
|
||||
'<div class="thumb-wrap">' + (thumbs || '-') + '</div></td></tr>';
|
||||
}).join('');
|
||||
}
|
||||
renderPagination('historyPagination', res.total, res.page, res.page_size, loadHistory);
|
||||
})
|
||||
.catch(function() {
|
||||
document.getElementById('historyListBody').innerHTML = '<tr><td colspan="5" class="empty-tip">请求失败</td></tr>';
|
||||
});
|
||||
}
|
||||
function loadUserOptions() {
|
||||
fetch('/api/admin/users?page=1&page_size=999')
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(res) {
|
||||
var sel = document.getElementById('filterUser');
|
||||
var cur = sel.value;
|
||||
sel.innerHTML = '<option value="">全部用户</option>';
|
||||
(res.items || []).forEach(function(u) {
|
||||
var opt = document.createElement('option');
|
||||
opt.value = u.id;
|
||||
opt.textContent = u.username + ' (' + roleLabel(u.role || 'normal') + ')';
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
sel.value = cur || '';
|
||||
});
|
||||
}
|
||||
document.getElementById('btnFilterHistory').onclick = function() { loadHistory(1); };
|
||||
|
||||
// ========== 分页 ==========
|
||||
function renderPagination(elId, total, page, pageSize, onPage) {
|
||||
var el = document.getElementById(elId);
|
||||
if (!el) return;
|
||||
var totalPages = Math.max(1, Math.ceil(total / pageSize));
|
||||
el.innerHTML = '<span>共 ' + total + ' 条</span>' +
|
||||
'<button ' + (page <= 1 ? 'disabled' : '') + ' data-p="' + (page - 1) + '">上一页</button>' +
|
||||
'<span>第 ' + page + ' / ' + totalPages + ' 页</span>' +
|
||||
'<button ' + (page >= totalPages ? 'disabled' : '') + ' data-p="' + (page + 1) + '">下一页</button>';
|
||||
el.querySelectorAll('[data-p]').forEach(function(b) {
|
||||
if (!b.disabled) b.onclick = function() { onPage(parseInt(b.dataset.p, 10)); };
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化
|
||||
loadUsers(1);
|
||||
loadUserOptions();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,918 +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>亚马逊 - 南日AI</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: "Microsoft YaHei", "Noto Serif SC", "SimSun", "Songti SC", "Times New Roman", serif;
|
||||
background: rgba(13, 13, 13, 1);
|
||||
color: #e0e0e0;
|
||||
overflow: hidden;
|
||||
height: 100vh;
|
||||
font-weight: bold;
|
||||
}
|
||||
.top-bar {
|
||||
height: 56px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
border-bottom: 1px solid #2a2a2a;
|
||||
}
|
||||
.logo-area {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.top-bar .app-name { font-size: 18px; font-weight: 600; color: #fff; }
|
||||
.btn-home {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
text-decoration: none;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.btn-home:hover { color: #fff; background: #2a2a2a; }
|
||||
|
||||
/* 顶部栏目导航(容器化,便于后续增加栏目) */
|
||||
.nav-tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
}
|
||||
.nav-tab-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: rgba(77, 72, 72, 0.99);
|
||||
border-radius: 10px;
|
||||
}
|
||||
.nav-tab-sep {
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
background: linear-gradient(to bottom, transparent, #444 15%, #444 85%, transparent);
|
||||
margin: 0 10px;
|
||||
flex-shrink: 0;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.nav-tab {
|
||||
padding: 8px 14px;
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.nav-tab:hover {
|
||||
color: #fff;
|
||||
background: #2a2a2a;
|
||||
}
|
||||
.nav-tab.active {
|
||||
color: #3498db;
|
||||
background: rgba(52, 152, 219, 0.2);
|
||||
}
|
||||
.top-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
display: flex;
|
||||
height: calc(100vh - 56px);
|
||||
}
|
||||
/* 栏目内容容器:每个栏目一个 .tab-panel,通过 data-panel 与导航对应 */
|
||||
.tab-panel {
|
||||
display: none;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
.tab-panel.active {
|
||||
display: flex;
|
||||
}
|
||||
.left-panel {
|
||||
width: 380px;
|
||||
background: #1e1e1e;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
border-right: 1px solid #2a2a2a;
|
||||
}
|
||||
.right-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
background: #1a1a1a;
|
||||
}
|
||||
.section-title {
|
||||
font-size: 13px;
|
||||
color: #bbb;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.upload-zone {
|
||||
border: 1px dashed #3a3a3a;
|
||||
border-radius: 10px;
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
background: #252525;
|
||||
margin-bottom: 20px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.upload-zone:hover { border-color: #3498db; background: #2a2a2a; }
|
||||
.upload-zone .hint { color: #888; font-size: 13px; margin-bottom: 12px; }
|
||||
.upload-zone .btns { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
|
||||
.opt-btn {
|
||||
padding: 8px 16px;
|
||||
font-size: 13px;
|
||||
color: #ccc;
|
||||
background: #2a2a2a;
|
||||
border: 1px solid #3a3a3a;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.opt-btn:hover { color: #fff; background: #333; border-color: #3498db; color: #3498db; }
|
||||
.selected-files {
|
||||
margin-top: 12px;
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
max-height: 72px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.selected-files span { display: block; margin: 4px 0; word-break: break-all; }
|
||||
.option-group { margin-bottom: 20px; }
|
||||
.radio-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
background: #252525;
|
||||
border: 1px solid #2a2a2a;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.radio-item:hover { background: #2a2a2a; }
|
||||
.radio-item input { margin-top: 3px; }
|
||||
.radio-item .label { font-weight: 500; color: #e0e0e0; font-size: 13px; }
|
||||
.radio-item .desc { font-size: 12px; color: #888; margin-top: 2px; font-weight: normal; }
|
||||
.run-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
.btn-run {
|
||||
padding: 10px 22px;
|
||||
background: #3498db;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.btn-run:hover { background: #2980b9; }
|
||||
.btn-run:disabled { background: #555; cursor: not-allowed; color: #999; }
|
||||
.loading-msg { color: #3498db; font-size: 13px; }
|
||||
|
||||
.template-download-row {
|
||||
margin-top: 16px;
|
||||
padding: 14px 16px;
|
||||
background: linear-gradient(135deg, #252525 0%, #1e2a1e 100%);
|
||||
border: 1px solid #2a3a2a;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
transition: all 0.25s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
.template-download-row:hover {
|
||||
border-color: #2e7d32;
|
||||
background: linear-gradient(135deg, #2a2a2a 0%, #243324 100%);
|
||||
box-shadow: 0 4px 12px rgba(46, 125, 50, 0.15);
|
||||
}
|
||||
.template-download-row:hover .template-download-text .title { color: #2ecc71; }
|
||||
.template-download-row:hover .template-download-icon {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 6px 16px rgba(46, 125, 50, 0.45), inset 0 1px 0 rgba(255,255,255,0.15);
|
||||
}
|
||||
.template-download-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(145deg, #1b5e20 0%, #2e7d32 50%, #388e3c 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4px 12px rgba(46, 125, 50, 0.35), inset 0 1px 0 rgba(255,255,255,0.1);
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
.template-download-icon svg {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
.template-download-text .title { font-size: 14px; color: #e0e0e0; font-weight: 600; display: block; }
|
||||
.template-download-text .hint { font-size: 12px; color: #888; margin-top: 2px; font-weight: normal; }
|
||||
|
||||
.panel-header {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #2a2a2a;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #ddd;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
.panel-header-title {
|
||||
flex: 1;
|
||||
}
|
||||
.btn-refresh-tasks {
|
||||
padding: 6px 14px;
|
||||
font-size: 12px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #3a3a3a;
|
||||
background: #2a2a2a;
|
||||
color: #ccc;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.btn-refresh-tasks:hover {
|
||||
background: #333;
|
||||
border-color: #3498db;
|
||||
color: #3498db;
|
||||
}
|
||||
.task-list-wrap {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
}
|
||||
.task-list { list-style: none; }
|
||||
.task-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 16px;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 10px;
|
||||
background: #1e1e1e;
|
||||
}
|
||||
.task-item .left { flex: 1; min-width: 0; }
|
||||
.task-item .id { font-weight: 600; color: #e0e0e0; font-size: 13px; }
|
||||
.task-item .files { font-size: 12px; color: #888; margin-top: 4px; }
|
||||
.task-item .time { font-size: 12px; color: #666; margin-top: 2px; }
|
||||
.task-item .status {
|
||||
padding: 4px 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
.task-item .status.pending { background: rgba(255,193,7,0.2); color: #d4a500; }
|
||||
.task-item .status.running { background: rgba(52,152,219,0.2); color: #3498db; }
|
||||
.task-item .status.success { background: rgba(46,204,113,0.2); color: #2ecc71; }
|
||||
.task-item .status.failed { background: rgba(231,76,60,0.2); color: #e74c3c; }
|
||||
.task-item .status.cancelled { background: rgba(149,165,166,0.2); color: #95a5a6; }
|
||||
.task-item .progress-wrap { margin-top: 8px; margin-right: 8px; flex-shrink: 0; width: 120px; }
|
||||
.task-item .progress-bar-bg { height: 6px; background: #2a2a2a; border-radius: 3px; overflow: hidden; }
|
||||
.task-item .progress-bar-fill { height: 100%; background: #3498db; border-radius: 3px; transition: width 0.2s; }
|
||||
.task-item .btn-cancel, .task-item .btn-delete {
|
||||
padding: 6px 12px; font-size: 12px; border-radius: 6px; cursor: pointer; border: none; margin-left: 6px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.task-item .btn-cancel { background: #e67e22; color: #fff; }
|
||||
.task-item .btn-cancel:hover { background: #d35400; }
|
||||
.task-item .btn-delete { background: #444; color: #ccc; }
|
||||
.task-item .btn-delete:hover { background: #e74c3c; color: #fff; }
|
||||
.task-item .task-right { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||||
.run-row .progress-wrap { margin-top: 10px; width: 100%; max-width: 280px; }
|
||||
.run-row .progress-bar-bg { height: 8px; background: #2a2a2a; border-radius: 4px; overflow: hidden; }
|
||||
.run-row .progress-bar-fill { height: 100%; background: #3498db; border-radius: 4px; transition: width 0.2s; }
|
||||
.run-row .btn-cancel-run { margin-left: 12px; padding: 8px 16px; background: #e67e22; color: #fff; border: none; border-radius: 6px; font-size: 13px; cursor: pointer; }
|
||||
.run-row .btn-cancel-run:hover { background: #d35400; }
|
||||
.task-item .download {
|
||||
padding: 6px 14px;
|
||||
background: #3498db;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.task-item .download:hover { background: #2980b9; color: #fff; }
|
||||
.task-item .download.hide { display: none; }
|
||||
.empty-tasks { text-align: center; color: #666; padding: 32px; font-size: 13px; }
|
||||
.toast {
|
||||
position: fixed;
|
||||
bottom: 32px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(0,0,0,0.85);
|
||||
color: #fff;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
z-index: 1000;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
.toast.show { opacity: 1; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header class="top-bar">
|
||||
<div class="logo-area">
|
||||
<span class="app-name">南日AI-亚马逊</span>
|
||||
<a href="/home" class="btn-home">返回首页</a>
|
||||
</div>
|
||||
<nav class="nav-tabs">
|
||||
<span class="nav-tab-group">
|
||||
<button type="button" class="nav-tab active" data-panel="brandCheck">品牌检测</button>
|
||||
<!-- 后续增加栏目时在此添加,例如:<button type="button" class="nav-tab" data-panel="other">其他栏目</button> -->
|
||||
</span>
|
||||
<!-- 多组栏目时可用分隔符:<span class="nav-tab-sep" aria-hidden="true"></span> -->
|
||||
</nav>
|
||||
<div class="top-right"></div>
|
||||
</header>
|
||||
|
||||
<div class="main-content">
|
||||
<!-- 品牌检测栏目容器;后续增加栏目时复制此结构,改 data-panel 与 id,并去掉 .active -->
|
||||
<div class="tab-panel active" data-panel="brandCheck" id="panel-brandCheck">
|
||||
<aside class="left-panel">
|
||||
<div class="section-title">选择文件</div>
|
||||
<div class="upload-zone" id="uploadZone">
|
||||
<div class="hint">选择 .xlsx 文件或文件夹(将读取该文件夹下所有 xlsx)</div>
|
||||
<div class="btns">
|
||||
<button type="button" class="opt-btn" id="btnSelectFiles">选择 Excel 文件</button>
|
||||
<button type="button" class="opt-btn" id="btnSelectFolder">选择文件夹</button>
|
||||
</div>
|
||||
<div class="selected-files" id="selectedFiles"></div>
|
||||
</div>
|
||||
|
||||
<div class="section-title">运行方式</div>
|
||||
<div class="option-group">
|
||||
<label class="radio-item">
|
||||
<input type="radio" name="runMode" value="immediate" checked>
|
||||
<div>
|
||||
<span class="label">立即运行</span>
|
||||
<div class="desc">立刻执行,执行完成后可下载结果</div>
|
||||
</div>
|
||||
</label>
|
||||
<label class="radio-item">
|
||||
<input type="radio" name="runMode" value="task">
|
||||
<div>
|
||||
<span class="label">添加任务</span>
|
||||
<div class="desc">数量较多时建议添加任务,后台执行,可在右侧任务队列查看状态与下载结果</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="section-title">匹配方式</div>
|
||||
<div class="option-group">
|
||||
<label class="radio-item">
|
||||
<input type="radio" name="matchStrategy" value="Terms" checked>
|
||||
<div>
|
||||
<span class="label">精确匹配表达式</span>
|
||||
<div class="desc">使用精确匹配,适合严格匹配品牌名称</div>
|
||||
</div>
|
||||
</label>
|
||||
<label class="radio-item">
|
||||
<input type="radio" name="matchStrategy" value="Simple">
|
||||
<div>
|
||||
<span class="label">嵌入(结果包含输入的词语)</span>
|
||||
<div class="desc">使用嵌入策略,结果中包含输入的品牌词语</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="run-row" id="runRow">
|
||||
<div style="display:flex;align-items:center;flex-wrap:wrap;gap:10px;">
|
||||
<button type="button" class="btn-run" id="btnRun">立即运行</button>
|
||||
<span class="loading-msg" id="loadingMsg" style="display:none;">生成中,请稍候…</span>
|
||||
<div class="progress-wrap" id="runProgressWrap" style="display:none;">
|
||||
<div class="progress-bar-bg"><div class="progress-bar-fill" id="runProgressFill" style="width:0%;"></div></div>
|
||||
<span class="hint" style="font-size:12px;color:#888;" id="runProgressText">0 / 0</span>
|
||||
</div>
|
||||
<!-- 当前文件行级进度(不经数据库中转,只读内存接口) -->
|
||||
<div class="progress-wrap" id="runLineProgressWrap" style="display:none;">
|
||||
<div class="progress-bar-bg"><div class="progress-bar-fill" id="runLineProgressFill" style="width:0%;"></div></div>
|
||||
<span class="hint" style="font-size:12px;color:#888;" id="runLineProgressText">0 / 0</span>
|
||||
</div>
|
||||
<button type="button" class="btn-cancel-run" id="btnCancelRun" style="display:none;">取消生成</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="template-download-row" id="templateDownloadRow" role="button" tabindex="0" title="下载品牌文档格式模板(选择保存位置)">
|
||||
<span class="template-download-icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6z" stroke="rgba(255,255,255,0.9)" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M14 2v6h6" stroke="rgba(255,255,255,0.9)" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8 13h8M8 17h5" stroke="rgba(255,255,255,0.85)" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="template-download-text">
|
||||
<span class="title">模板下载</span>
|
||||
<span class="hint">品牌文档格式_模板.xlsx · 点击选择保存位置</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="template-download-row" id="templateDownloadRow2" role="button" tabindex="0" title="下载模板2(以文件夹方式上传)">
|
||||
<span class="template-download-icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6z" stroke="rgba(255,255,255,0.9)" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M14 2v6h6" stroke="rgba(255,255,255,0.9)" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M3 7v10h4v-4h2v4h8v-4h2v4h2V7H3z" stroke="rgba(255,255,255,0.85)" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="template-download-text">
|
||||
<span class="title">模板2下载</span>
|
||||
<span class="hint">以文件夹方式上传.zip · 点击下载</span>
|
||||
</span>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="right-panel">
|
||||
<div class="panel-header">
|
||||
<span class="panel-header-title">任务队列</span>
|
||||
<button type="button" class="btn-refresh-tasks" id="btnRefreshTasks">刷新任务状态</button>
|
||||
</div>
|
||||
<div class="task-list-wrap">
|
||||
<ul class="task-list" id="taskList"></ul>
|
||||
<div class="empty-tasks" id="emptyTasks">暂无任务</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
var selectedPaths = [];
|
||||
var cachedTasks = [];
|
||||
var api = window.pywebview && window.pywebview.api;
|
||||
|
||||
// 顶部栏目切换:点击 nav-tab 时显示对应 data-panel 的 tab-panel
|
||||
document.querySelectorAll('.nav-tab').forEach(function(tab) {
|
||||
tab.addEventListener('click', function() {
|
||||
var panelId = this.getAttribute('data-panel');
|
||||
if (!panelId) return;
|
||||
document.querySelectorAll('.nav-tab').forEach(function(t) { t.classList.remove('active'); });
|
||||
document.querySelectorAll('.tab-panel').forEach(function(p) {
|
||||
p.classList.toggle('active', p.getAttribute('data-panel') === panelId);
|
||||
});
|
||||
this.classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
function showToast(msg) {
|
||||
var el = document.getElementById('toast');
|
||||
el.textContent = msg;
|
||||
el.classList.add('show');
|
||||
setTimeout(function() { el.classList.remove('show'); }, 2500);
|
||||
}
|
||||
|
||||
function renderSelected() {
|
||||
var el = document.getElementById('selectedFiles');
|
||||
if (selectedPaths.length === 0) {
|
||||
el.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
el.innerHTML = '已选 <b>' + selectedPaths.length + '</b> 个文件:<br>' +
|
||||
selectedPaths.slice(0, 20).map(function(p) {
|
||||
var name = p.split(/[/\\]/).pop();
|
||||
return '<span title="' + p + '">' + name + '</span>';
|
||||
}).join('') +
|
||||
(selectedPaths.length > 20 ? '<span>… 等 ' + selectedPaths.length + ' 个</span>' : '');
|
||||
}
|
||||
|
||||
document.getElementById('btnSelectFiles').onclick = async function(e) {
|
||||
e.stopPropagation();
|
||||
if (!api || !api.select_brand_xlsx_files) {
|
||||
showToast('当前环境不支持文件选择,请在本机客户端中打开');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var paths = await api.select_brand_xlsx_files();
|
||||
if (paths && paths.length) {
|
||||
selectedPaths = paths;
|
||||
renderSelected();
|
||||
showToast('已选择 ' + paths.length + ' 个文件');
|
||||
}
|
||||
} catch (err) {
|
||||
showToast('选择失败:' + (err.message || err));
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementById('templateDownloadRow').onclick = async function(e) {
|
||||
e.preventDefault();
|
||||
if (api && api.save_template_xlsx) {
|
||||
try {
|
||||
var res = await api.save_template_xlsx();
|
||||
if (res && res.success) {
|
||||
showToast('模板已保存至:' + (res.path || ''));
|
||||
} else {
|
||||
showToast(res && res.error ? res.error : '保存失败');
|
||||
}
|
||||
} catch (err) {
|
||||
showToast('下载失败:' + (err.message || err));
|
||||
}
|
||||
return;
|
||||
}
|
||||
window.location.href = '/static/品牌文档格式_模板.xlsx';
|
||||
};
|
||||
document.getElementById('templateDownloadRow').onkeydown = function(e) {
|
||||
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.click(); }
|
||||
};
|
||||
|
||||
document.getElementById('templateDownloadRow2').onclick = async function(e) {
|
||||
e.preventDefault();
|
||||
if (api && api.save_template_zip) {
|
||||
try {
|
||||
var res = await api.save_template_zip();
|
||||
if (res && res.success) {
|
||||
showToast('模板已保存至:' + (res.path || ''));
|
||||
} else {
|
||||
showToast(res && res.error ? res.error : '保存失败');
|
||||
}
|
||||
} catch (err) {
|
||||
showToast('下载失败:' + (err.message || err));
|
||||
}
|
||||
return;
|
||||
}
|
||||
window.location.href = '/static/模板2-以文件夹方式上传.zip';
|
||||
};
|
||||
document.getElementById('templateDownloadRow2').onkeydown = function(e) {
|
||||
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.click(); }
|
||||
};
|
||||
|
||||
document.getElementById('btnSelectFolder').onclick =async function(e) {
|
||||
e.stopPropagation();
|
||||
if (!api || !api.select_brand_folder) {
|
||||
showToast('当前环境不支持文件夹选择,请在本机客户端中打开');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var folder = await api.select_brand_folder();
|
||||
if (!folder) return;
|
||||
fetch('/api/brand/expand-folder', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' },
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({ folder: folder })
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(res) {
|
||||
if (res.success && res.paths && res.paths.length) {
|
||||
selectedPaths = res.paths;
|
||||
renderSelected();
|
||||
showToast('已选择文件夹内 ' + res.paths.length + ' 个 xlsx 文件');
|
||||
} else {
|
||||
showToast(res.error || '该文件夹下没有 xlsx 文件');
|
||||
}
|
||||
})
|
||||
.catch(function() { showToast('请求失败'); });
|
||||
} catch (err) {
|
||||
showToast('选择失败:' + (err.message || err));
|
||||
}
|
||||
};
|
||||
|
||||
function getRunMode() {
|
||||
var r = document.querySelector('input[name="runMode"]:checked');
|
||||
return r ? r.value : 'immediate';
|
||||
}
|
||||
|
||||
function getMatchStrategy() {
|
||||
var r = document.querySelector('input[name="matchStrategy"]:checked');
|
||||
return r ? r.value : 'Terms';
|
||||
}
|
||||
|
||||
var pollTaskId = null;
|
||||
var pollTimer = null;
|
||||
var runTaskEventSource = null;
|
||||
|
||||
function stopPollAndResetRunUI() {
|
||||
pollTaskId = null;
|
||||
if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
|
||||
if (runTaskEventSource) {
|
||||
runTaskEventSource.close();
|
||||
runTaskEventSource = null;
|
||||
}
|
||||
var btn = document.getElementById('btnRun');
|
||||
var loadingMsg = document.getElementById('loadingMsg');
|
||||
var runProgressWrap = document.getElementById('runProgressWrap');
|
||||
var runLineProgressWrap = document.getElementById('runLineProgressWrap');
|
||||
var btnCancelRun = document.getElementById('btnCancelRun');
|
||||
btn.disabled = false;
|
||||
loadingMsg.style.display = 'none';
|
||||
if (runProgressWrap) runProgressWrap.style.display = 'none';
|
||||
if (runLineProgressWrap) runLineProgressWrap.style.display = 'none';
|
||||
if (btnCancelRun) btnCancelRun.style.display = 'none';
|
||||
}
|
||||
|
||||
function onRunTaskFinished(status) {
|
||||
stopPollAndResetRunUI();
|
||||
loadTasks();
|
||||
if (status === 'success') showToast('生成完成,可下载结果');
|
||||
else if (status === 'failed') showToast('生成失败');
|
||||
else if (status === 'cancelled') showToast('已取消');
|
||||
}
|
||||
|
||||
function pollRunTask(taskId) {
|
||||
pollTaskId = taskId;
|
||||
var runProgressFill = document.getElementById('runProgressFill');
|
||||
var runProgressText = document.getElementById('runProgressText');
|
||||
var runLineProgressWrap = document.getElementById('runLineProgressWrap');
|
||||
var runLineProgressFill = document.getElementById('runLineProgressFill');
|
||||
var runLineProgressText = document.getElementById('runLineProgressText');
|
||||
var btnCancelRun = document.getElementById('btnCancelRun');
|
||||
var runProgressWrap = document.getElementById('runProgressWrap');
|
||||
runProgressWrap.style.display = 'block';
|
||||
btnCancelRun.style.display = 'inline-block';
|
||||
btnCancelRun.onclick = function() {
|
||||
fetch('/api/brand/tasks/' + taskId + '/cancel', { method: 'POST', credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest' } })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(res) {
|
||||
if (res.success) onRunTaskFinished('cancelled');
|
||||
else showToast(res.error || '取消失败');
|
||||
});
|
||||
};
|
||||
|
||||
// 通过 SSE 订阅任务完成事件,完成后由后端主动推送,前端收到后立即隐藏进度条和取消按钮
|
||||
var eventsUrl = '/api/brand/tasks/' + taskId + '/events';
|
||||
runTaskEventSource = new EventSource(eventsUrl);
|
||||
runTaskEventSource.onmessage = function(e) {
|
||||
try {
|
||||
var data = JSON.parse(e.data);
|
||||
if (data && (data.status === 'success' || data.status === 'failed' || data.status === 'cancelled')) {
|
||||
runTaskEventSource.close();
|
||||
runTaskEventSource = null;
|
||||
// tick();
|
||||
onRunTaskFinished(data.status);
|
||||
}
|
||||
} catch (err) {}
|
||||
};
|
||||
runTaskEventSource.onerror = function() {
|
||||
runTaskEventSource.close();
|
||||
runTaskEventSource = null;
|
||||
};
|
||||
|
||||
function tick() {
|
||||
if (!pollTaskId) return;
|
||||
fetch('/api/brand/tasks/' + taskId, { credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest' } })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(res) {
|
||||
if (!res.success || !res.task) return;
|
||||
var t = res.task;
|
||||
var total = t.progress_total || 0;
|
||||
var cur = t.progress_current || 0;
|
||||
if (runProgressFill) runProgressFill.style.width = total ? (100 * cur / total) + '%' : '30%';
|
||||
if (runProgressText) {
|
||||
var st = (t.status || '').toLowerCase();
|
||||
if (st === 'running') {
|
||||
runProgressText.textContent = total ? (cur + ' / ' + total) : '生成中…';
|
||||
} else if (st === 'success') {
|
||||
runProgressText.textContent = '已完成';
|
||||
} else if (st === 'failed') {
|
||||
runProgressText.textContent = '失败';
|
||||
} else if (st === 'cancelled') {
|
||||
runProgressText.textContent = '已取消';
|
||||
} else {
|
||||
runProgressText.textContent = '执行中…';
|
||||
}
|
||||
}
|
||||
// 行级进度:从内存接口 /api/brand/tasks/<id>/line-progress 拉取
|
||||
if (runLineProgressWrap && runLineProgressFill && runLineProgressText) {
|
||||
var st2 = (t.status || '').toLowerCase();
|
||||
if (st2 === 'running') {
|
||||
fetch('/api/brand/tasks/' + taskId + '/line-progress', {
|
||||
credentials: 'same-origin',
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(res2) {
|
||||
if (!res2.success || !res2.has_progress || !res2.info) {
|
||||
runLineProgressWrap.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
var info = res2.info;
|
||||
var totalLines = info.total_lines || 0;
|
||||
var curLine = info.current_line || 0;
|
||||
runLineProgressWrap.style.display = 'block';
|
||||
runLineProgressFill.style.width = totalLines ? (100 * curLine / totalLines) + '%' : '0%';
|
||||
runLineProgressText.textContent = totalLines ? (curLine + ' / ' + totalLines) : '当前文件处理中…';
|
||||
})
|
||||
.catch(function() {});
|
||||
} else {
|
||||
runLineProgressWrap.style.display = 'none';
|
||||
}
|
||||
}
|
||||
// 终态也通过轮询兜底,确保 UI 一定被重置(SSE 可能因网络未收到)
|
||||
var st = (t.status || '').toLowerCase();
|
||||
if (st === 'success' || st === 'failed' || st === 'cancelled') {
|
||||
onRunTaskFinished(st);
|
||||
}
|
||||
});
|
||||
}
|
||||
tick();
|
||||
pollTimer = setInterval(tick, 5000);
|
||||
}
|
||||
|
||||
document.getElementById('btnRun').onclick = function() {
|
||||
if (selectedPaths.length === 0) {
|
||||
showToast('请先选择 Excel 文件或文件夹');
|
||||
return;
|
||||
}
|
||||
var mode = getRunMode();
|
||||
var strategy = getMatchStrategy();
|
||||
var btn = document.getElementById('btnRun');
|
||||
var loadingMsg = document.getElementById('loadingMsg');
|
||||
|
||||
if (mode === 'immediate') {
|
||||
btn.disabled = true;
|
||||
loadingMsg.style.display = 'inline';
|
||||
fetch('/api/brand/run', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' },
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({ paths: selectedPaths, strategy: strategy, task_type: 1 })
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(res) {
|
||||
if (res.success && res.task_id) {
|
||||
loadingMsg.style.display = 'none';
|
||||
pollRunTask(res.task_id);
|
||||
} else {
|
||||
stopPollAndResetRunUI();
|
||||
showToast(res.error || '运行失败');
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
stopPollAndResetRunUI();
|
||||
showToast('请求失败或超时');
|
||||
});
|
||||
} else {
|
||||
fetch('/api/brand/tasks', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' },
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({ paths: selectedPaths, strategy: strategy, task_type: 2 })
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(res) {
|
||||
if (res.success) {
|
||||
showToast('任务已添加,请在任务队列查看');
|
||||
loadTasks();
|
||||
} else {
|
||||
showToast(res.error || '添加失败');
|
||||
}
|
||||
})
|
||||
.catch(function() { showToast('请求失败'); });
|
||||
}
|
||||
};
|
||||
|
||||
function loadTasks() {
|
||||
fetch('/api/brand/tasks', { credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest' } })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(res) {
|
||||
var list = document.getElementById('taskList');
|
||||
var empty = document.getElementById('emptyTasks');
|
||||
if (!res.success || !res.items || res.items.length === 0) {
|
||||
list.innerHTML = '';
|
||||
empty.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
empty.style.display = 'none';
|
||||
cachedTasks = res.items;
|
||||
list.innerHTML = res.items.map(function(t) {
|
||||
var statusClass = (t.status || 'pending').toLowerCase();
|
||||
var statusText = { pending: '等待中', running: '执行中', success: '已完成', failed: '失败', cancelled: '已取消' }[t.status] || t.status;
|
||||
var filesDesc = (t.file_paths && t.file_paths.length) ? t.file_paths.length + ' 个文件' : '';
|
||||
var total = t.progress_total || 0;
|
||||
var cur = t.progress_current || 0;
|
||||
var pct = total ? Math.min(100, (100 * cur / total)) : 0;
|
||||
var progressHtml = (statusClass === 'running' && total > 0)
|
||||
? '<div class="progress-wrap"><div class="progress-bar-bg"><div class="progress-bar-fill" style="width:' + pct + '%"></div></div><span class="hint" style="font-size:11px;color:#888;">' + cur + ' / ' + total + '</span></div>'
|
||||
: '';
|
||||
var hasZipUrl = t.result_paths && t.result_paths.zip_url;
|
||||
var downloadHtml = hasZipUrl
|
||||
? '<a class="download js-save-zip" href="#" data-task-id="' + t.id + '">下载结果</a>'
|
||||
: '<span class="download hide"></span>';
|
||||
var cancelBtn = statusClass === 'running'
|
||||
? '<button type="button" class="btn-cancel js-cancel-task" data-task-id="' + t.id + '">取消</button>'
|
||||
: '';
|
||||
var deleteBtn = '<button type="button" class="btn-delete js-delete-task" data-task-id="' + t.id + '">删除</button>';
|
||||
var titleDesc = (t.desc && String(t.desc).trim()) ? String(t.desc).trim() : ('任务 #' + t.id);
|
||||
var displayDesc = titleDesc.length > 20 ? titleDesc.substring(0, 20) + '....' : titleDesc;
|
||||
return '<li class="task-item">' +
|
||||
'<div class="left">' +
|
||||
'<span class="id" title="' + (titleDesc.replace(/"/g, '"')) + '">' + displayDesc + '</span>' +
|
||||
'<div class="files">' + filesDesc + '</div>' +
|
||||
'<div class="time">' + (t.created_at || '') + '</div>' +
|
||||
'</div>' +
|
||||
'<div class="task-right">' +
|
||||
progressHtml +
|
||||
'<span class="status ' + statusClass + '">' + statusText + '</span>' +
|
||||
cancelBtn +
|
||||
downloadHtml +
|
||||
deleteBtn +
|
||||
'</div></li>';
|
||||
}).join('');
|
||||
})
|
||||
.catch(function() {});
|
||||
}
|
||||
|
||||
// 手动刷新任务状态按钮
|
||||
var btnRefreshTasks = document.getElementById('btnRefreshTasks');
|
||||
if (btnRefreshTasks) {
|
||||
btnRefreshTasks.onclick = function () {
|
||||
loadTasks();
|
||||
showToast('任务状态已刷新');
|
||||
};
|
||||
}
|
||||
|
||||
document.getElementById('taskList').addEventListener('click', function(e) {
|
||||
var target = e.target && e.target.closest && (e.target.closest('a.js-save-zip') || e.target.closest('button.js-cancel-task') || e.target.closest('button.js-delete-task'));
|
||||
if (!target) return;
|
||||
e.preventDefault();
|
||||
var taskId = target.getAttribute('data-task-id');
|
||||
if (target.classList && (target.classList.contains('js-cancel-task') || target.closest('.js-cancel-task'))) {
|
||||
var btn = target.classList.contains('js-cancel-task') ? target : target.closest('.js-cancel-task');
|
||||
if (!btn) return;
|
||||
fetch('/api/brand/tasks/' + taskId + '/cancel', { method: 'POST', credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest' } })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(res) {
|
||||
if (res.success) { showToast('已取消'); loadTasks(); } else { showToast(res.error || '取消失败'); }
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (target.classList && (target.classList.contains('js-delete-task') || target.closest('.js-delete-task'))) {
|
||||
var btn = target.classList.contains('js-delete-task') ? target : target.closest('.js-delete-task');
|
||||
if (!btn) return;
|
||||
fetch('/api/brand/tasks/' + taskId, { method: 'DELETE', credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest' } })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(res) {
|
||||
if (res.success) { showToast('已删除'); loadTasks(); } else { showToast(res.error || '删除失败'); }
|
||||
});
|
||||
return;
|
||||
}
|
||||
var a = target.closest('a.js-save-zip') || target;
|
||||
if (!a || !a.classList || !a.classList.contains('js-save-zip')) return;
|
||||
var task = cachedTasks.filter(function(t) { return String(t.id) === String(taskId); })[0];
|
||||
if (!task || !task.result_paths || !task.result_paths.zip_url) {
|
||||
showToast('无法获取下载地址');
|
||||
return;
|
||||
}
|
||||
var zipUrl = task.result_paths.zip_url;
|
||||
var filename = 'brand_task_' + taskId + '.zip';
|
||||
if (api && api.save_file_from_url) {
|
||||
a.style.pointerEvents = 'none';
|
||||
var done = function(ret) {
|
||||
if (ret && ret.success) {
|
||||
showToast('已保存:' + (ret.path || filename));
|
||||
} else if (ret && ret.error && ret.error !== '用户取消') {
|
||||
showToast('保存失败:' + ret.error);
|
||||
}
|
||||
a.style.pointerEvents = '';
|
||||
};
|
||||
try {
|
||||
var ret = api.save_file_from_url(zipUrl, filename);
|
||||
if (ret && typeof ret.then === 'function') {
|
||||
ret.then(done).catch(function(err) { showToast('保存失败:' + (err && err.message || err)); a.style.pointerEvents = ''; });
|
||||
} else {
|
||||
done(ret);
|
||||
}
|
||||
} catch (err) {
|
||||
showToast('保存失败:' + (err.message || err));
|
||||
a.style.pointerEvents = '';
|
||||
}
|
||||
} else {
|
||||
window.open('/api/brand/download/' + taskId, '_blank');
|
||||
}
|
||||
});
|
||||
|
||||
loadTasks();
|
||||
// setInterval(loadTasks, 10000);
|
||||
|
||||
if (window.addEventListener) {
|
||||
window.addEventListener('pywebviewready', function() {
|
||||
api = window.pywebview && window.pywebview.api;
|
||||
});
|
||||
}
|
||||
if (window.pywebview && window.pywebview.api) api = window.pywebview.api;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,352 +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>首页 - 南日AI</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: "Microsoft YaHei", "PingFang SC", "Hiragino Sans GB", sans-serif;
|
||||
background: linear-gradient(rgba(255,255,255,0.5), rgba(255,255,255,0.5)),
|
||||
url("/static/bg.jpg") center/cover no-repeat;;
|
||||
/*background-image: url("/static/bg.jpg");*/
|
||||
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
}
|
||||
.header {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 56px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 24px;
|
||||
background: rgba(255,255,255,0.5);
|
||||
}
|
||||
.header-title { font-size: 18px; font-weight: 600; color: #333; }
|
||||
.header-right { display: flex; align-items: center; gap: 16px; position: relative; }
|
||||
.header-right a, .header-right span {
|
||||
font-size: 14px; color: #555; text-decoration: none;
|
||||
}
|
||||
.header-right a:hover { color: #667eea; }
|
||||
.entrances {
|
||||
display: flex;
|
||||
gap: 32px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
.entrance-btn {
|
||||
width: 160px;
|
||||
height: 100px;
|
||||
background: #c5c1c1;
|
||||
border: 4px solid #b8d4e3;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
text-decoration: none;
|
||||
}
|
||||
.entrance-btn:hover {
|
||||
background: #f8fbfd;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
|
||||
}
|
||||
.entrance-btn.wb { cursor: pointer; }
|
||||
.entrance-btn.disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.toast {
|
||||
position: fixed;
|
||||
bottom: 40px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(0,0,0,0.75);
|
||||
color: #fff;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
pointer-events: none;
|
||||
}
|
||||
.toast.show { opacity: 1; }
|
||||
.admin-link {
|
||||
font-size: 13px;
|
||||
color: #888;
|
||||
}
|
||||
.update-section {
|
||||
margin-top: 48px;
|
||||
padding: 20px 24px;
|
||||
background: rgba(255,255,255,0.85);
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(0,0,0,0.06);
|
||||
max-width: 420px;
|
||||
}
|
||||
.update-section-title {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.update-block {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
.update-version-text {
|
||||
font-size: 13px;
|
||||
color: #555;
|
||||
}
|
||||
.btn-check-update {
|
||||
padding: 8px 14px;
|
||||
font-size: 13px;
|
||||
background: #667eea;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn-check-update:hover {
|
||||
background: #5a6fd6;
|
||||
}
|
||||
.update-hint {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-top: 8px;
|
||||
min-height: 18px;
|
||||
}
|
||||
.btn-download-update {
|
||||
margin-top: 8px;
|
||||
padding: 8px 14px;
|
||||
font-size: 13px;
|
||||
background: #28a745;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn-download-update:hover {
|
||||
background: #218838;
|
||||
}
|
||||
.header-update-btn {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: rgba(102,126,234,0.12);
|
||||
color: #4c5bd4;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 15px;
|
||||
padding: 0;
|
||||
}
|
||||
.header-update-btn:hover {
|
||||
background: rgba(102,126,234,0.2);
|
||||
}
|
||||
.header-update-panel {
|
||||
position: absolute;
|
||||
top: 44px;
|
||||
right: 0;
|
||||
width: 280px;
|
||||
padding: 14px 16px;
|
||||
background: rgba(255,255,255,0.98);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.12);
|
||||
border: 1px solid rgba(0,0,0,0.04);
|
||||
z-index: 10;
|
||||
display: none;
|
||||
}
|
||||
.header-update-title {
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body data-user-id="{{ user_id or '' }}">
|
||||
<header class="header">
|
||||
<span class="header-title">南日AI</span>
|
||||
<div class="header-right">
|
||||
{% if is_admin %}
|
||||
<!-- <a href="/admin" class="admin-link">用户管理</a>-->
|
||||
{% endif %}
|
||||
<span>{{ username }}</span>
|
||||
<button type="button" class="header-update-btn" id="homeUpdateToggle" title="检测更新">↻</button>
|
||||
<a href="/logout" onclick="handleLogout()">退出</a>
|
||||
<div class="header-update-panel" id="homeUpdatePanel">
|
||||
<div class="header-update-title"><span>↻</span> 软件更新</div>
|
||||
<div class="update-block">
|
||||
<span class="update-version-text" id="homeVersionText">当前版本: {{ version }}</span>
|
||||
<button type="button" class="btn-check-update" id="homeBtnCheckUpdate"><span>↻</span> 检测</button>
|
||||
</div>
|
||||
<div class="update-hint" id="homeUpdateHint"></div>
|
||||
<div style="margin-top: 4px;">
|
||||
<button type="button" class="btn-download-update" id="homeBtnDownloadUpdate" style="display: none;">立即更新</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="entrances" id="entrances">
|
||||
<a class="entrance-btn" href="/brand" data-column-key="brand">亚马逊</a>
|
||||
<a class="entrance-btn disabled" href="javascript:;" data-msg="暂未开通,敬请期待" data-column-key="wb">wildberries</a>
|
||||
<a class="entrance-btn image" href="/image" data-column-key="image">图片</a>
|
||||
</div>
|
||||
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
<script>
|
||||
// 权限校验:根据 column-permissions 接口按 column_key 显示入口
|
||||
(function() {
|
||||
var uid = document.body.getAttribute('data-user-id');
|
||||
if (!uid) return;
|
||||
var baseUrl = window.location.origin;
|
||||
var apiUrl = baseUrl + '/api/admin/user/' + uid + '/column-permissions';
|
||||
fetch(apiUrl, { credentials: 'same-origin' })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(res) {
|
||||
if (!res || !res.success || !Array.isArray(res.items)) return;
|
||||
var allowedKeys = res.items.map(function(item) { return (item.column_key || '').toLowerCase(); });
|
||||
document.querySelectorAll('.entrances .entrance-btn[data-column-key]').forEach(function(btn) {
|
||||
var key = (btn.getAttribute('data-column-key') || '').toLowerCase();
|
||||
btn.style.display = allowedKeys.indexOf(key) >= 0 ? '' : 'none';
|
||||
});
|
||||
})
|
||||
.catch(function() {});
|
||||
})();
|
||||
|
||||
document.querySelectorAll('.entrance-btn.disabled').forEach(function(btn) {
|
||||
btn.onclick = function(e) {
|
||||
e.preventDefault();
|
||||
var msg = btn.getAttribute('data-msg') || '暂未开通,敬请期待';
|
||||
var t = document.getElementById('toast');
|
||||
t.textContent = msg;
|
||||
t.classList.add('show');
|
||||
setTimeout(function() { t.classList.remove('show'); }, 2000);
|
||||
};
|
||||
});
|
||||
|
||||
function handleLogout() {
|
||||
try{
|
||||
localStorage.removeItem('maixiang_api_key');
|
||||
}catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
// 软件更新(与 index 设置中逻辑一致)
|
||||
(function() {
|
||||
const versionText = document.getElementById('homeVersionText');
|
||||
const updateHint = document.getElementById('homeUpdateHint');
|
||||
const btnDownload = document.getElementById('homeBtnDownloadUpdate');
|
||||
const updatePanel = document.getElementById('homeUpdatePanel');
|
||||
const toggleBtn = document.getElementById('homeUpdateToggle');
|
||||
if (!versionText || !updateHint || !btnDownload || !updatePanel || !toggleBtn) return;
|
||||
|
||||
let currentVersion = '{{version}}';
|
||||
versionText.textContent = '当前版本: v' + currentVersion;
|
||||
|
||||
// 打开/关闭浮层
|
||||
toggleBtn.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
const isVisible = updatePanel.style.display === 'block';
|
||||
updatePanel.style.display = isVisible ? 'none' : 'block';
|
||||
});
|
||||
|
||||
// 点击外部关闭浮层
|
||||
document.addEventListener('click', function() {
|
||||
updatePanel.style.display = 'none';
|
||||
});
|
||||
updatePanel.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
document.getElementById('homeBtnCheckUpdate').onclick = async function() {
|
||||
updateHint.textContent = '正在检测更新...';
|
||||
btnDownload.style.display = 'none';
|
||||
try {
|
||||
const resp = await fetch('/api/version', { credentials: 'same-origin' }).catch(function() { return null; });
|
||||
if (resp && resp.ok) {
|
||||
const data = await resp.json();
|
||||
currentVersion = (data.version || currentVersion).replace(/^v/i, '');
|
||||
versionText.textContent = '当前版本: v' + currentVersion;
|
||||
if (data.has_update && data.latest_version) {
|
||||
updateHint.textContent = '发现新版本 v' + data.latest_version + (data.desc ? ':' + data.desc : '');
|
||||
btnDownload.style.display = 'inline-block';
|
||||
btnDownload.textContent = '立即更新';
|
||||
btnDownload.onclick = async function() {
|
||||
if (!confirm('有更新,是否现在更新?\n更新将下载安装包并重启程序。')) return;
|
||||
if (!data.file_url) {
|
||||
updateHint.textContent = '暂无下载地址,请关注官方渠道。';
|
||||
return;
|
||||
}
|
||||
updateHint.textContent = '正在下载并准备更新,程序将自动退出...';
|
||||
btnDownload.disabled = true;
|
||||
try {
|
||||
const updateResp = await fetch('/api/update/do', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ file_url: data.file_url })
|
||||
});
|
||||
const result = await updateResp.json().catch(function() { return {}; });
|
||||
if (result.success) {
|
||||
updateHint.textContent = '更新已启动,程序即将退出...';
|
||||
} else {
|
||||
updateHint.textContent = result.error || '更新启动失败';
|
||||
btnDownload.disabled = false;
|
||||
}
|
||||
} catch (e) {
|
||||
updateHint.textContent = '请求更新失败,请重试';
|
||||
btnDownload.disabled = false;
|
||||
}
|
||||
};
|
||||
} else {
|
||||
updateHint.textContent = '已是最新版本';
|
||||
}
|
||||
} else {
|
||||
updateHint.textContent = '无法连接更新服务,请稍后重试。';
|
||||
}
|
||||
} catch (e) {
|
||||
updateHint.textContent = '检测更新失败';
|
||||
}
|
||||
};
|
||||
btnDownload.style.display = 'none';
|
||||
})();
|
||||
// (function() {
|
||||
// fetch('/api/auth/check', { credentials: 'same-origin' })
|
||||
// .then(function(r) { return r.json(); })
|
||||
// .then(function(res) {
|
||||
// if (res.logged_in && res.redirect) {
|
||||
// window.location.href = res.redirect;
|
||||
// }else{
|
||||
// window.location.href = "/login"
|
||||
// // handleLogout()
|
||||
// }
|
||||
// })
|
||||
// .catch(function() {});
|
||||
// })();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,164 +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>登录 - 南日AI</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: "Microsoft YaHei", "PingFang SC", "Hiragino Sans GB", sans-serif;
|
||||
background: #d8e4ec;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
}
|
||||
.header {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 56px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 24px;
|
||||
background: rgba(255,255,255,0.5);
|
||||
}
|
||||
.header-title { font-size: 18px; font-weight: 600; color: #333; }
|
||||
.login-box {
|
||||
background: #fff;
|
||||
border: 1px solid #b8d4e3;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||
padding: 40px;
|
||||
width: 100%;
|
||||
max-width: 380px;
|
||||
}
|
||||
.login-title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #555;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 12px 14px;
|
||||
font-size: 14px;
|
||||
border: 1px solid #b8d4e3;
|
||||
border-radius: 8px;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
background: #fff;
|
||||
}
|
||||
.form-group input:focus {
|
||||
border-color: #3498db;
|
||||
}
|
||||
.error-msg {
|
||||
color: #e74c3c;
|
||||
font-size: 13px;
|
||||
margin-bottom: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
.btn-login {
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #3498db 0%, #2980b9 100%);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.btn-login:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
.btn-login:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<span class="header-title">南日AI</span>
|
||||
</header>
|
||||
|
||||
<div class="login-box">
|
||||
<h1 class="login-title">登录</h1>
|
||||
{% if error %}
|
||||
<p class="error-msg">{{ error }}</p>
|
||||
{% endif %}
|
||||
<form id="loginForm" method="POST" action="/login">
|
||||
<div class="form-group">
|
||||
<label>用户名</label>
|
||||
<input type="text" name="username" placeholder="请输入用户名" required autofocus>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>密码</label>
|
||||
<input type="password" name="password" placeholder="请输入密码" required>
|
||||
</div>
|
||||
<button type="submit" class="btn-login" id="btnLogin">登录</button>
|
||||
</form>
|
||||
</div>
|
||||
<script>
|
||||
(function() {
|
||||
fetch('/api/auth/check', { credentials: 'same-origin' })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(res) {
|
||||
if (res.logged_in && res.redirect) {
|
||||
window.location.href = res.redirect;
|
||||
}
|
||||
})
|
||||
.catch(function() {});
|
||||
})();
|
||||
document.getElementById('loginForm').onsubmit = function(e) {
|
||||
e.preventDefault();
|
||||
var btn = document.getElementById('btnLogin');
|
||||
btn.disabled = true;
|
||||
btn.textContent = '登录中...';
|
||||
var form = e.target;
|
||||
var fd = new FormData(form);
|
||||
fetch(form.action, {
|
||||
method: 'POST',
|
||||
body: fd,
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(res) {
|
||||
if (res.success) {
|
||||
window.location.href = res.redirect || '/home';
|
||||
} else {
|
||||
var errEl = document.querySelector('.error-msg');
|
||||
if (!errEl) {
|
||||
errEl = document.createElement('p');
|
||||
errEl.className = 'error-msg';
|
||||
form.insertBefore(errEl, form.firstChild);
|
||||
}
|
||||
errEl.textContent = res.error || '登录失败';
|
||||
btn.disabled = false;
|
||||
btn.textContent = '登录';
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
form.submit();
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user