Files
crawler-plugin/backend/web_source/admin.html
2026-04-11 22:25:14 +08:00

2023 lines
109 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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; }
.progress-wrap { margin-top: 12px; }
.progress-bar { width: 100%; height: 10px; background: #edf0f5; border-radius: 999px; overflow: hidden; }
.progress-fill { width: 0; height: 100%; background: #667eea; transition: width 0.2s ease; }
.progress-text { margin-top: 8px; font-size: 13px; color: #666; }
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; }
.column-permission-cards { display: flex; flex-wrap: wrap; gap: 8px; min-height: 32px; margin-bottom: 8px; }
.column-permission-card {
display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; background: #e8ebf7; color: #667eea;
border-radius: 6px; font-size: 13px;
}
.column-permission-card .col-card-remove { margin-left: 2px; padding: 0 4px; line-height: 1; cursor: pointer; color: #999; border: none; background: none; font-size: 16px; }
.column-permission-card .col-card-remove:hover { color: #e74c3c; }
.column-permission-select { min-height: 80px; max-height: 140px; overflow-y: auto; padding: 8px; }
.column-permission-select option { padding: 4px 0; }
</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="columns">栏目权限配置</div>
<div class="tab" data-tab="dedupe-total-data">数据去重总数据</div>
<div class="tab" data-tab="shop-keys">店铺密钥管理</div>
<div class="tab" data-tab="shop-manage">店铺管理</div>
<div class="tab" data-tab="history">查看生成记录</div>
<div class="tab" data-tab="version">版本管理</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>
<div class="form-group" id="formGroupCreateColumns">
<label>栏目权限可多选Ctrl/Shift 点选)</label>
<div id="createColumnCards" class="column-permission-cards"></div>
<select id="createColumnPermissionWrap" class="column-permission-select" multiple></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-columns" 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:200px;">
<label>栏目名</label>
<input type="text" id="columnName" placeholder="例如:首页推荐">
</div>
<div class="form-group" style="min-width:200px;">
<label>栏目标识</label>
<input type="text" id="columnKey" placeholder="例如home_recommend">
</div>
<button class="btn" id="btnAddColumn">新增栏目</button>
</div>
<p class="msg" id="msgColumn"></p>
</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="columnListBody"></tbody>
</table>
</div>
</div>
<!-- 版本管理 -->
<div id="panel-version" class="tab-panel">
<div class="form-box">
<h3 style="margin-bottom:16px;font-size:15px;">发布新版本</h3>
<div class="form-group">
<label>版本号</label>
<input type="text" id="versionNumber" placeholder="例如1.0.0">
</div>
<div class="form-group">
<label>ZIP 压缩包</label>
<input type="file" id="versionZip" accept=".zip">
</div>
<button class="btn" id="btnUploadVersion">上传并发布</button>
<p class="msg" id="msgVersion"></p>
</div>
<div class="panel-box">
<h3 style="margin-bottom:16px;font-size:15px;">版本列表</h3>
<table>
<thead>
<tr>
<th>版本号</th>
<th>下载链接</th>
<th>发布时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id="versionListBody"></tbody>
</table>
</div>
</div>
<!-- 数据去重总数据 -->
<div id="panel-dedupe-total-data" class="tab-panel">
<div class="form-box">
<div class="form-row" style="align-items:flex-start;gap:16px;">
<div style="flex:1;min-width:320px;">
<h3 style="margin-bottom:16px;font-size:15px;">新增总数据</h3>
<div class="form-row">
<div class="form-group" style="min-width:320px;">
<label>上传 Excel 文件(读取 ASIN 列)</label>
<input type="file" id="dedupeTotalDataFile" accept=".xlsx,.xls">
</div>
<button class="btn" id="btnAddDedupeTotalData">上传并导入</button>
</div>
<p class="msg" id="msgDedupeTotalData"></p>
<div class="progress-wrap" id="dedupeTotalDataProgressWrap" style="display:none;">
<div class="progress-bar"><div class="progress-fill" id="dedupeTotalDataProgressFill"></div></div>
<div class="progress-text" id="dedupeTotalDataProgressText"></div>
</div>
</div>
<div style="flex:1;min-width:320px;">
<h3 style="margin-bottom:16px;font-size:15px;">删除匹配总数据</h3>
<div class="form-row">
<div class="form-group" style="min-width:320px;">
<label>上传 Excel 文件(读取 ASIN 列)</label>
<input type="file" id="dedupeTotalDataDeleteFile" accept=".xlsx,.xls">
</div>
<button class="btn btn-danger" id="btnDeleteImportDedupeTotalData">上传并删除</button>
</div>
<p class="msg" id="msgDeleteDedupeTotalData"></p>
<div class="progress-wrap" id="dedupeTotalDataDeleteProgressWrap" style="display:none;">
<div class="progress-bar"><div class="progress-fill" id="dedupeTotalDataDeleteProgressFill"></div></div>
<div class="progress-text" id="dedupeTotalDataDeleteProgressText"></div>
</div>
</div>
</div>
</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:220px;">
<label>数据值(模糊搜索)</label>
<input type="text" id="searchDedupeTotalData" placeholder="输入关键字">
</div>
<button class="btn" id="btnSearchDedupeTotalData">查询</button>
</div>
<table>
<thead>
<tr>
<th>ID</th>
<th>总数据值</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id="dedupeTotalDataListBody"></tbody>
</table>
<div class="pagination" id="dedupeTotalDataPagination"></div>
</div>
</div>
<!-- 店铺密钥管理 -->
<div id="panel-shop-keys" 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:220px;">
<label>紫鸟账号名称</label>
<input type="text" id="shopKeyZiniaoAccountName" placeholder="请输入紫鸟账号名称">
</div>
<div class="form-group" style="min-width:260px;">
<label>紫鸟令牌</label>
<input type="text" id="shopKeyZiniaoToken" placeholder="请输入紫鸟令牌">
</div>
<button class="btn" id="btnCreateShopKey">新增店铺密钥</button>
</div>
<p class="msg" id="msgShopKey"></p>
</div>
<div class="panel-box">
<h3 style="margin-bottom:16px;font-size:15px;">店铺密钥列表</h3>
<table>
<thead>
<tr>
<th>序号</th>
<th>紫鸟账号名称</th>
<th>紫鸟令牌</th>
<th>创建时间</th>
<th>修改时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id="shopKeyListBody"></tbody>
</table>
<div class="pagination" id="shopKeyPagination"></div>
</div>
</div>
<!-- 店铺管理 -->
<div id="panel-shop-manage" 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:220px;">
<label>分组</label>
<div style="display:flex;gap:8px;align-items:center;">
<select id="shopManageGroupSelect" style="min-width:150px;">
<option value="">请选择分组</option>
</select>
<button class="btn btn-secondary" id="btnManageShopGroups" type="button">管理分组</button>
</div>
</div>
<div class="form-group" style="min-width:160px;">
<label>店铺名</label>
<input type="text" id="shopManageShopName" placeholder="请输入店铺名称">
</div>
<div class="form-group" style="min-width:180px;">
<label>店铺商城名</label>
<input type="text" id="shopManageMallName" placeholder="请输入店铺商城名">
</div>
<div class="form-group" style="min-width:180px;">
<label>账号</label>
<input type="text" id="shopManageAccount" placeholder="请输入账号">
</div>
<div class="form-group" style="min-width:180px;">
<label>密码</label>
<input type="text" id="shopManagePassword" placeholder="请输入密码">
</div>
<button class="btn" id="btnCreateShopManage">新增店铺</button>
</div>
<p class="msg" id="msgShopManage"></p>
</div>
<div class="panel-box">
<h3 style="margin-bottom:16px;font-size:15px;">店铺列表</h3>
<div class="form-row" style="margin-bottom:12px;">
<div class="form-group" style="min-width:180px;">
<label>分组</label>
<select id="shopManageFilterGroupId">
<option value="">全部分组</option>
</select>
</div>
<div class="form-group" style="min-width:220px;">
<label>店铺名</label>
<input type="text" id="shopManageFilterShopName" placeholder="请输入店铺名">
</div>
<button class="btn" id="btnSearchShopManage">查询</button>
</div>
<table>
<thead>
<tr>
<th>序号</th>
<th>分组</th>
<th>店铺名</th>
<th>店铺商城名</th>
<th>账号</th>
<th>密码</th>
<th>创建时间</th>
<th>修改时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id="shopManageListBody"></tbody>
</table>
<div class="pagination" id="shopManagePagination"></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>
<div class="form-group">
<label>栏目权限可多选Ctrl/Shift 点选)</label>
<div id="editColumnCards" class="column-permission-cards"></div>
<select id="editColumnPermissionWrap" class="column-permission-select" multiple></select>
</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>
<!-- 编辑总数据弹窗 -->
<div class="modal-mask" id="editDedupeTotalDataModal">
<div class="modal">
<h3>编辑总数据</h3>
<input type="hidden" id="editDedupeTotalDataId">
<div class="form-group">
<label>总数据值</label>
<input type="text" id="editDedupeTotalDataValue" placeholder="请输入总数据值">
</div>
<p class="msg" id="msgEditDedupeTotalData"></p>
<div style="margin-top:16px;display:flex;gap:8px;">
<button class="btn" id="btnSaveDedupeTotalData">保存</button>
<button class="btn btn-secondary" id="btnCloseEditDedupeTotalData">取消</button>
</div>
</div>
</div>
<!-- 编辑栏目弹窗 -->
<div class="modal-mask" id="editColumnModal">
<div class="modal">
<h3>编辑栏目</h3>
<input type="hidden" id="editColumnId">
<div class="form-group">
<label>栏目名</label>
<input type="text" id="editColumnName" placeholder="栏目名">
</div>
<div class="form-group">
<label>栏目标识</label>
<input type="text" id="editColumnKey" placeholder="栏目标识">
</div>
<p class="msg" id="msgEditColumn"></p>
<div style="margin-top:16px;display:flex;gap:8px;">
<button class="btn" id="btnSaveColumn">保存</button>
<button class="btn btn-secondary" id="btnCloseEditColumn">取消</button>
</div>
</div>
</div>
<!-- 分组管理弹窗 -->
<div class="modal-mask" id="shopManageGroupModal">
<div class="modal" style="max-width:760px;">
<h3>分组管理</h3>
<div class="form-row">
<input type="hidden" id="shopManageGroupEditId">
<div class="form-group" style="min-width:260px;">
<label>分组名称</label>
<input type="text" id="shopManageGroupInput" placeholder="请输入分组名称">
</div>
<button class="btn" id="btnSaveShopManageGroup" type="button">保存分组</button>
<button class="btn btn-secondary" id="btnCancelShopManageGroupEdit" type="button">取消编辑</button>
</div>
<p class="msg" id="msgShopManageGroup"></p>
<table>
<thead>
<tr><th>序号</th><th>分组名称</th><th>创建时间</th><th>修改时间</th><th>操作</th></tr>
</thead>
<tbody id="shopManageGroupListBody"></tbody>
</table>
<div style="margin-top:16px;display:flex;gap:8px;justify-content:flex-end;">
<button class="btn btn-secondary" id="btnCloseShopManageGroupModal" type="button">关闭</button>
</div>
</div>
</div>
<!-- 编辑店铺管理弹窗 -->
<div class="modal-mask" id="editShopManageModal">
<div class="modal">
<h3>编辑店铺</h3>
<input type="hidden" id="editShopManageId">
<div class="form-group">
<label>分组</label>
<div style="display:flex;gap:8px;align-items:center;">
<select id="editShopManageGroupSelect" style="min-width:200px;">
<option value="">请选择分组</option>
</select>
<button class="btn btn-secondary" id="btnManageShopGroupsFromEdit" type="button">管理分组</button>
</div>
</div>
<div class="form-group">
<label>店铺名</label>
<input type="text" id="editShopManageShopName" placeholder="店铺名">
</div>
<div class="form-group">
<label>店铺商城名</label>
<input type="text" id="editShopManageMallName" placeholder="店铺商城名">
</div>
<div class="form-group">
<label>账号</label>
<input type="text" id="editShopManageAccount" placeholder="账号">
</div>
<div class="form-group">
<label>密码</label>
<input type="text" id="editShopManagePassword" placeholder="密码">
</div>
<p class="msg" id="msgEditShopManage"></p>
<div style="margin-top:16px;display:flex;gap:8px;">
<button class="btn" id="btnSaveShopManage">保存</button>
<button class="btn btn-secondary" id="btnCloseEditShopManage">取消</button>
</div>
</div>
</div>
<!-- 编辑店铺密钥弹窗 -->
<div class="modal-mask" id="editShopKeyModal">
<div class="modal">
<h3>编辑店铺密钥</h3>
<input type="hidden" id="editShopKeyId">
<div class="form-group">
<label>紫鸟账号名称</label>
<input type="text" id="editShopKeyZiniaoAccountName" placeholder="紫鸟账号名称">
</div>
<div class="form-group">
<label>紫鸟令牌</label>
<input type="text" id="editShopKeyZiniaoToken" placeholder="紫鸟令牌">
</div>
<p class="msg" id="msgEditShopKey"></p>
<div style="margin-top:16px;display:flex;gap:8px;">
<button class="btn" id="btnSaveShopKey">保存</button>
<button class="btn btn-secondary" id="btnCloseEditShopKey">取消</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); loadColumnsForPermission(); }
else if (t.dataset.tab === 'columns') loadColumns();
else if (t.dataset.tab === 'dedupe-total-data') loadDedupeTotalData(1);
else if (t.dataset.tab === 'shop-keys') loadShopKeys(1);
else if (t.dataset.tab === 'shop-manage') loadShopManage(1);
else if (t.dataset.tab === 'history') loadHistory(1);
else if (t.dataset.tab === 'version') loadVersions();
};
});
// ========== 用户管理 ==========
var userPage = 1, userPageSize = 15;
var currentUserId = null;
var currentUserRole = 'admin';
var currentUserUsername = '';
var currentUserAdminPermissionKeys = {};
var currentUserAdminPermissionRoutes = {};
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;
}
currentUserId = res.current_user_id || null;
currentUserRole = res.current_user_role || 'admin';
currentUserUsername = (res.current_user_username || '').trim();
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, '&quot;')) + '">编辑</button> ' +
'<button class="btn btn-sm btn-danger" data-delete="' + u.id + '" data-name="' + (u.username || '').replace(/"/g, '&quot;') + '">删除</button>' +
'</td></tr>';
}).join('');
}
renderPagination('userPagination', res.total, res.page, res.page_size, loadUsers);
bindUserActions();
updateCreateFormByRole();
updateUserFilterByRole();
updateDedupeTotalDataAccess();
loadCurrentUserAdminPermissions();
})
.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 updateDedupeTotalDataAccess() {
var tab = document.querySelector('.tab[data-tab="dedupe-total-data"]');
var panel = document.getElementById('panel-dedupe-total-data');
var canUse = currentUserRole === 'super_admin' || (currentUserRole === 'admin' && currentUserUsername === '刘丽泓');
if (tab) tab.style.display = canUse ? '' : 'none';
if (panel) panel.style.display = canUse ? '' : 'none';
if (!canUse && tab && tab.classList.contains('active')) {
tab.classList.remove('active');
if (panel) panel.classList.remove('active');
var usersTab = document.querySelector('.tab[data-tab="users"]');
var usersPanel = document.getElementById('panel-users');
if (usersTab) usersTab.classList.add('active');
if (usersPanel) usersPanel.classList.add('active');
}
}
function updateShopManageAccess() {
var tab = document.querySelector('.tab[data-tab="shop-manage"]');
var panel = document.getElementById('panel-shop-manage');
if (!tab || !panel) return;
var canUse = currentUserRole === 'super_admin' ||
!!currentUserAdminPermissionKeys['admin_shop_manage'] ||
!!currentUserAdminPermissionRoutes['shop-manage'];
tab.style.display = canUse ? '' : 'none';
panel.style.display = canUse ? '' : 'none';
if (!canUse && tab.classList.contains('active')) {
tab.classList.remove('active');
panel.classList.remove('active');
var usersTab = document.querySelector('.tab[data-tab="users"]');
var usersPanel = document.getElementById('panel-users');
if (usersTab) usersTab.classList.add('active');
if (usersPanel) usersPanel.classList.add('active');
}
}
function loadCurrentUserAdminPermissions() {
currentUserAdminPermissionKeys = {};
currentUserAdminPermissionRoutes = {};
if (!currentUserId || currentUserRole === 'super_admin') {
updateShopManageAccess();
return;
}
fetch('/api/admin/user/' + currentUserId + '/column-permissions?menu_type=admin')
.then(function(r) { return r.json(); })
.then(function(res) {
if (!res.success) {
updateShopManageAccess();
return;
}
(res.items || []).forEach(function(item) {
var columnKey = (item.column_key || '').trim();
var routePath = (item.route_path || '').trim();
if (columnKey) currentUserAdminPermissionKeys[columnKey] = true;
if (routePath) currentUserAdminPermissionRoutes[routePath] = true;
});
updateShopManageAccess();
})
.catch(function() {
updateShopManageAccess();
});
}
var allColumnsList = [];
function loadColumnsForPermission() {
fetch('/api/admin/columns')
.then(function(r) { return r.json(); })
.then(function(res) {
if (!res.success) return;
allColumnsList = res.items || [];
renderColumnPermissionWrap('createColumnPermissionWrap');
renderColumnPermissionWrap('editColumnPermissionWrap');
var selCreate = document.getElementById('createColumnPermissionWrap');
var selEdit = document.getElementById('editColumnPermissionWrap');
if (selCreate && !selCreate._colCardsBound) {
selCreate._colCardsBound = true;
selCreate.onchange = function() { renderColumnCards('createColumnPermissionWrap'); };
}
if (selEdit && !selEdit._colCardsBound) {
selEdit._colCardsBound = true;
selEdit.onchange = function() { renderColumnCards('editColumnPermissionWrap'); };
}
})
.catch(function() {});
}
var columnCardsContainerMap = { createColumnPermissionWrap: 'createColumnCards', editColumnPermissionWrap: 'editColumnCards' };
function renderColumnCards(selectId) {
var sel = document.getElementById(selectId);
var cardsId = columnCardsContainerMap[selectId];
var cardsEl = cardsId ? document.getElementById(cardsId) : null;
if (!sel || sel.tagName !== 'SELECT' || !cardsEl) return;
cardsEl.innerHTML = '';
for (var i = 0; i < sel.options.length; i++) {
var opt = sel.options[i];
if (opt.disabled || !opt.selected) continue;
var card = document.createElement('span');
card.className = 'column-permission-card';
card.textContent = opt.textContent;
var btn = document.createElement('button');
btn.type = 'button';
btn.className = 'col-card-remove';
btn.setAttribute('aria-label', '移除');
btn.textContent = '×';
(function(option, seldId) {
btn.onclick = function() {
option.selected = false;
renderColumnCards(seldId);
};
})(opt, selectId);
card.appendChild(btn);
cardsEl.appendChild(card);
}
}
function renderColumnPermissionWrap(wrapId) {
var sel = document.getElementById(wrapId);
if (!sel || sel.tagName !== 'SELECT') return;
sel.innerHTML = '';
allColumnsList.forEach(function(c) {
var opt = document.createElement('option');
opt.value = c.id;
opt.textContent = c.name + ' (' + c.column_key + ')';
sel.appendChild(opt);
});
if (allColumnsList.length === 0) {
var opt = document.createElement('option');
opt.disabled = true;
opt.textContent = '暂无栏目,请先在「栏目权限配置」中新增栏目';
sel.appendChild(opt);
}
renderColumnCards(wrapId);
}
function getSelectedColumnIds(wrapId) {
var sel = document.getElementById(wrapId);
if (!sel || sel.tagName !== 'SELECT') return [];
var ids = [];
for (var i = 0; i < sel.options.length; i++) {
if (sel.options[i].selected) {
var v = parseInt(sel.options[i].value, 10);
if (!isNaN(v)) ids.push(v);
}
}
return ids;
}
function setColumnPermissionCheckboxes(wrapId, columnIds) {
var sel = document.getElementById(wrapId);
if (!sel || sel.tagName !== 'SELECT') return;
var set = {};
(columnIds || []).forEach(function(id) { set[id] = true; });
for (var i = 0; i < sel.options.length; i++) {
var opt = sel.options[i];
if (opt.disabled) continue;
opt.selected = set[parseInt(opt.value, 10)] || false;
}
renderColumnCards(wrapId);
}
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(/&quot;/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 = '';
if (allColumnsList.length) {
renderColumnPermissionWrap('editColumnPermissionWrap');
fetch('/api/admin/user/' + (u.id) + '/columns')
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.success && res.column_ids) setColumnPermissionCheckboxes('editColumnPermissionWrap', res.column_ids);
});
}
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;
body.column_ids = getSelectedColumnIds('createColumnPermissionWrap');
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';
body.column_ids = getSelectedColumnIds('editColumnPermissionWrap');
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, '&quot;') + '" 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); };
// ========== 数据去重总数据 ==========
var dedupeTotalDataPage = 1, dedupeTotalDataPageSize = 15;
function buildDedupeTotalDataQuery(page) {
var q = 'page=' + (page || 1) + '&page_size=' + dedupeTotalDataPageSize;
var keyword = (document.getElementById('searchDedupeTotalData').value || '').trim();
if (keyword) q += '&keyword=' + encodeURIComponent(keyword);
return q;
}
function loadDedupeTotalData(page) {
dedupeTotalDataPage = page || 1;
fetch('/api/admin/dedupe-total-data?' + buildDedupeTotalDataQuery(dedupeTotalDataPage))
.then(function(r) { return r.json(); })
.then(function(res) {
var tbody = document.getElementById('dedupeTotalDataListBody');
if (!res.success) {
tbody.innerHTML = '<tr><td colspan="4" class="empty-tip">加载失败: ' + (res.error || '') + '</td></tr>';
return;
}
var items = res.items || [];
if (items.length === 0) {
tbody.innerHTML = '<tr><td colspan="4" class="empty-tip">暂无总数据</td></tr>';
} else {
tbody.innerHTML = items.map(function(item) {
return '<tr><td>' + item.id + '</td><td>' + (item.data_value || '') + '</td><td>' + (item.created_at || '') + '</td><td>' +
'<button class="btn btn-sm" data-dedupe-total-edit="' + item.id + '" data-value="' + (item.data_value || '').replace(/"/g, '&quot;') + '">编辑</button> ' +
'<button class="btn btn-sm btn-danger" data-dedupe-total-delete="' + item.id + '" data-value="' + (item.data_value || '').replace(/"/g, '&quot;') + '">删除</button>' +
'</td></tr>';
}).join('');
}
renderPagination('dedupeTotalDataPagination', res.total, res.page, res.page_size, loadDedupeTotalData);
bindDedupeTotalDataActions();
})
.catch(function() {
document.getElementById('dedupeTotalDataListBody').innerHTML = '<tr><td colspan="4" class="empty-tip">请求失败</td></tr>';
});
}
function bindDedupeTotalDataActions() {
document.querySelectorAll('[data-dedupe-total-edit]').forEach(function(btn) {
btn.onclick = function() {
document.getElementById('editDedupeTotalDataId').value = btn.dataset.dedupeTotalEdit || '';
document.getElementById('editDedupeTotalDataValue').value = (btn.dataset.value || '').replace(/&quot;/g, '"');
document.getElementById('msgEditDedupeTotalData').textContent = '';
document.getElementById('msgEditDedupeTotalData').className = 'msg';
document.getElementById('editDedupeTotalDataModal').classList.add('show');
};
});
document.querySelectorAll('[data-dedupe-total-delete]').forEach(function(btn) {
btn.onclick = function() {
var value = (btn.dataset.value || '').replace(/&quot;/g, '"');
if (!confirm('确定删除总数据「' + value + '」吗?')) return;
fetch('/api/admin/dedupe-total-data/' + btn.dataset.dedupeTotalDelete, { method: 'DELETE' })
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.success) { loadDedupeTotalData(dedupeTotalDataPage); }
else { alert(res.error || '删除失败'); }
});
};
});
}
document.getElementById('btnSearchDedupeTotalData').onclick = function() { loadDedupeTotalData(1); };
var dedupeImportPollTimer = null;
var dedupeDeleteImportPollTimer = null;
function stopDedupeImportProgress() {
if (dedupeImportPollTimer) {
clearInterval(dedupeImportPollTimer);
dedupeImportPollTimer = null;
}
}
function stopDedupeDeleteImportProgress() {
if (dedupeDeleteImportPollTimer) {
clearInterval(dedupeDeleteImportPollTimer);
dedupeDeleteImportPollTimer = null;
}
}
function setDedupeImportProgress(percent, text) {
var wrap = document.getElementById('dedupeTotalDataProgressWrap');
var fill = document.getElementById('dedupeTotalDataProgressFill');
var textEl = document.getElementById('dedupeTotalDataProgressText');
wrap.style.display = 'block';
fill.style.width = Math.max(0, Math.min(100, percent || 0)) + '%';
textEl.textContent = text || '';
}
function setDedupeDeleteImportProgress(percent, text) {
var wrap = document.getElementById('dedupeTotalDataDeleteProgressWrap');
var fill = document.getElementById('dedupeTotalDataDeleteProgressFill');
var textEl = document.getElementById('dedupeTotalDataDeleteProgressText');
wrap.style.display = 'block';
fill.style.width = Math.max(0, Math.min(100, percent || 0)) + '%';
textEl.textContent = text || '';
}
function pollDedupeImport(importId) {
stopDedupeImportProgress();
function tick() {
fetch('/api/admin/dedupe-total-data/import/' + encodeURIComponent(importId))
.then(function(r) { return r.json(); })
.then(function(res) {
if (!res.success) {
stopDedupeImportProgress();
document.getElementById('msgDedupeTotalData').textContent = res.error || '查询导入进度失败';
document.getElementById('msgDedupeTotalData').className = 'msg err';
return;
}
var progress = res.progress || {};
var totalRows = progress.total_rows || 0;
var processedRows = progress.processed_rows || 0;
var percent = totalRows > 0 ? Math.round(processedRows * 100 / totalRows) : 0;
setDedupeImportProgress(percent, '处理中:已处理 ' + processedRows + ' / ' + totalRows + ',新增 ' + (progress.inserted_count || 0) + ',跳过 ' + (progress.skipped_count || 0));
if (progress.status === 'success') {
stopDedupeImportProgress();
document.getElementById('msgDedupeTotalData').textContent = '导入成功:总行数 ' + (progress.total_rows || 0) + 'ASIN 数量 ' + (progress.asin_count || 0) + ',新增 ' + (progress.inserted_count || 0) + ',跳过 ' + (progress.skipped_count || 0);
document.getElementById('msgDedupeTotalData').className = 'msg ok';
loadDedupeTotalData(1);
} else if (progress.status === 'failed') {
stopDedupeImportProgress();
document.getElementById('msgDedupeTotalData').textContent = progress.error_message || '导入失败';
document.getElementById('msgDedupeTotalData').className = 'msg err';
}
})
.catch(function() {
stopDedupeImportProgress();
document.getElementById('msgDedupeTotalData').textContent = '查询导入进度失败';
document.getElementById('msgDedupeTotalData').className = 'msg err';
});
}
tick();
dedupeImportPollTimer = setInterval(tick, 1000);
}
function pollDedupeDeleteImport(importId) {
stopDedupeDeleteImportProgress();
function tick() {
fetch('/api/admin/dedupe-total-data/delete-import/' + encodeURIComponent(importId))
.then(function(r) { return r.json(); })
.then(function(res) {
if (!res.success) {
stopDedupeDeleteImportProgress();
document.getElementById('msgDeleteDedupeTotalData').textContent = res.error || '查询删除进度失败';
document.getElementById('msgDeleteDedupeTotalData').className = 'msg err';
return;
}
var progress = res.progress || {};
var totalRows = progress.total_rows || 0;
var processedRows = progress.processed_rows || 0;
var percent = totalRows > 0 ? Math.round(processedRows * 100 / totalRows) : 0;
setDedupeDeleteImportProgress(percent, '处理中:已处理 ' + processedRows + ' / ' + totalRows + ',删除 ' + (progress.deleted_count || 0) + ',跳过 ' + (progress.skipped_count || 0));
if (progress.status === 'success') {
stopDedupeDeleteImportProgress();
document.getElementById('msgDeleteDedupeTotalData').textContent = '删除成功:总行数 ' + (progress.total_rows || 0) + 'ASIN 数量 ' + (progress.asin_count || 0) + ',删除 ' + (progress.deleted_count || 0) + ',跳过 ' + (progress.skipped_count || 0);
document.getElementById('msgDeleteDedupeTotalData').className = 'msg ok';
loadDedupeTotalData(1);
} else if (progress.status === 'failed') {
stopDedupeDeleteImportProgress();
document.getElementById('msgDeleteDedupeTotalData').textContent = progress.error_message || '删除失败';
document.getElementById('msgDeleteDedupeTotalData').className = 'msg err';
}
})
.catch(function() {
stopDedupeDeleteImportProgress();
document.getElementById('msgDeleteDedupeTotalData').textContent = '查询删除进度失败';
document.getElementById('msgDeleteDedupeTotalData').className = 'msg err';
});
}
tick();
dedupeDeleteImportPollTimer = setInterval(tick, 1000);
}
document.getElementById('btnAddDedupeTotalData').onclick = function() {
var fileInput = document.getElementById('dedupeTotalDataFile');
var msgEl = document.getElementById('msgDedupeTotalData');
msgEl.textContent = '';
msgEl.className = 'msg';
stopDedupeImportProgress();
document.getElementById('dedupeTotalDataProgressWrap').style.display = 'none';
if (!fileInput.files || fileInput.files.length === 0) {
msgEl.textContent = '请选择 Excel 文件';
msgEl.classList.add('err');
return;
}
var file = fileInput.files[0];
var lowerName = (file.name || '').toLowerCase();
if (!(lowerName.endsWith('.xlsx') || lowerName.endsWith('.xls'))) {
msgEl.textContent = '仅支持 .xlsx 或 .xls 文件';
msgEl.classList.add('err');
return;
}
var formData = new FormData();
formData.append('file', file);
var xhr = new XMLHttpRequest();
xhr.open('POST', '/api/admin/dedupe-total-data/import', true);
xhr.upload.onprogress = function(event) {
if (event.lengthComputable) {
var percent = Math.round(event.loaded * 100 / event.total);
setDedupeImportProgress(percent, '上传中:' + percent + '%');
}
};
xhr.onreadystatechange = function() {
if (xhr.readyState !== 4) return;
if (xhr.status < 200 || xhr.status >= 300) {
msgEl.textContent = '请求失败';
msgEl.className = 'msg err';
return;
}
var res;
try { res = JSON.parse(xhr.responseText || '{}'); } catch (e) { res = { success: false, error: '返回格式错误' }; }
if (!res.success) {
msgEl.textContent = res.error || '导入失败';
msgEl.className = 'msg err';
return;
}
setDedupeImportProgress(100, '上传完成,后端处理中...');
pollDedupeImport(res.import_id);
fileInput.value = '';
};
xhr.onerror = function() {
msgEl.textContent = '请求失败';
msgEl.className = 'msg err';
};
xhr.send(formData);
};
document.getElementById('btnDeleteImportDedupeTotalData').onclick = function() {
var fileInput = document.getElementById('dedupeTotalDataDeleteFile');
var msgEl = document.getElementById('msgDeleteDedupeTotalData');
msgEl.textContent = '';
msgEl.className = 'msg';
stopDedupeDeleteImportProgress();
document.getElementById('dedupeTotalDataDeleteProgressWrap').style.display = 'none';
if (!fileInput.files || fileInput.files.length === 0) {
msgEl.textContent = '请选择 Excel 文件';
msgEl.classList.add('err');
return;
}
var file = fileInput.files[0];
var lowerName = (file.name || '').toLowerCase();
if (!(lowerName.endsWith('.xlsx') || lowerName.endsWith('.xls'))) {
msgEl.textContent = '仅支持 .xlsx 或 .xls 文件';
msgEl.classList.add('err');
return;
}
if (!confirm('确定按 Excel 中的 ASIN 批量删除匹配的总数据吗?')) {
return;
}
var formData = new FormData();
formData.append('file', file);
var xhr = new XMLHttpRequest();
xhr.open('POST', '/api/admin/dedupe-total-data/delete-import', true);
xhr.upload.onprogress = function(event) {
if (event.lengthComputable) {
var percent = Math.round(event.loaded * 100 / event.total);
setDedupeDeleteImportProgress(percent, '上传中:' + percent + '%');
}
};
xhr.onreadystatechange = function() {
if (xhr.readyState !== 4) return;
if (xhr.status < 200 || xhr.status >= 300) {
msgEl.textContent = '请求失败';
msgEl.className = 'msg err';
return;
}
var res;
try { res = JSON.parse(xhr.responseText || '{}'); } catch (e) { res = { success: false, error: '返回格式错误' }; }
if (!res.success) {
msgEl.textContent = res.error || '删除失败';
msgEl.className = 'msg err';
return;
}
setDedupeDeleteImportProgress(100, '上传完成,后端处理中...');
pollDedupeDeleteImport(res.import_id);
fileInput.value = '';
};
xhr.onerror = function() {
msgEl.textContent = '请求失败';
msgEl.className = 'msg err';
};
xhr.send(formData);
};
document.getElementById('btnSaveDedupeTotalData').onclick = function() {
var itemId = document.getElementById('editDedupeTotalDataId').value;
var value = (document.getElementById('editDedupeTotalDataValue').value || '').trim();
var msgEl = document.getElementById('msgEditDedupeTotalData');
msgEl.textContent = '';
msgEl.className = 'msg';
if (!value) {
msgEl.textContent = '请填写总数据值';
msgEl.classList.add('err');
return;
}
fetch('/api/admin/dedupe-total-data/' + itemId, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ data_value: value })
})
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.success) {
document.getElementById('editDedupeTotalDataModal').classList.remove('show');
loadDedupeTotalData(dedupeTotalDataPage);
} else {
msgEl.textContent = res.error || '保存失败';
msgEl.classList.add('err');
}
});
};
document.getElementById('btnCloseEditDedupeTotalData').onclick = function() {
document.getElementById('editDedupeTotalDataModal').classList.remove('show');
};
// ========== 店铺密钥管理 ==========
var shopKeyPage = 1, shopKeyPageSize = 15;
function buildShopKeyQuery(page) {
return 'page=' + (page || 1) + '&page_size=' + shopKeyPageSize;
}
function loadShopKeys(page) {
shopKeyPage = page || 1;
fetch('/api/admin/shop-keys?' + buildShopKeyQuery(shopKeyPage))
.then(function(r) { return r.json(); })
.then(function(res) {
var tbody = document.getElementById('shopKeyListBody');
if (!res.success) {
tbody.innerHTML = '<tr><td colspan="6" class="empty-tip">加载失败: ' + (res.error || '') + '</td></tr>';
return;
}
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(item, index) {
var rowNo = (shopKeyPage - 1) * shopKeyPageSize + index + 1;
return '<tr><td>' + rowNo + '</td><td>' + (item.ziniao_account_name || '') + '</td><td>' + (item.ziniao_token || '') + '</td><td>' + (item.created_at || '') + '</td><td>' + (item.updated_at || '') + '</td><td>' +
'<button class="btn btn-sm" data-shop-key-edit="' + item.id + '" data-shop-key="' + (JSON.stringify(item).replace(/"/g, '&quot;')) + '">编辑</button> ' +
'<button class="btn btn-sm btn-danger" data-shop-key-delete="' + item.id + '" data-ziniao-account-name="' + (item.ziniao_account_name || '').replace(/"/g, '&quot;') + '">删除</button>' +
'</td></tr>';
}).join('');
}
renderPagination('shopKeyPagination', res.total, res.page, res.page_size, loadShopKeys);
bindShopKeyActions();
})
.catch(function() {
document.getElementById('shopKeyListBody').innerHTML = '<tr><td colspan="6" class="empty-tip">请求失败</td></tr>';
});
}
function bindShopKeyActions() {
document.querySelectorAll('[data-shop-key-edit]').forEach(function(btn) {
btn.onclick = function() {
var item = {};
try { item = JSON.parse((btn.dataset.shopKey || '').replace(/&quot;/g, '"')); } catch (e) { item = {}; }
document.getElementById('editShopKeyId').value = item.id || '';
document.getElementById('editShopKeyZiniaoAccountName').value = item.ziniao_account_name || '';
document.getElementById('editShopKeyZiniaoToken').value = item.ziniao_token || '';
document.getElementById('msgEditShopKey').textContent = '';
document.getElementById('msgEditShopKey').className = 'msg';
document.getElementById('editShopKeyModal').classList.add('show');
};
});
document.querySelectorAll('[data-shop-key-delete]').forEach(function(btn) {
btn.onclick = function() {
var name = (btn.dataset.ziniaoAccountName || '').replace(/&quot;/g, '"');
if (!confirm('确定删除店铺密钥「' + name + '」吗?')) return;
fetch('/api/admin/shop-key/' + btn.dataset.shopKeyDelete, { method: 'DELETE' })
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.success) { loadShopKeys(shopKeyPage); }
else { alert(res.error || '删除失败'); }
});
};
});
}
document.getElementById('btnCreateShopKey').onclick = function() {
var ziniaoAccountName = (document.getElementById('shopKeyZiniaoAccountName').value || '').trim();
var ziniaoToken = (document.getElementById('shopKeyZiniaoToken').value || '').trim();
var msgEl = document.getElementById('msgShopKey');
msgEl.textContent = '';
msgEl.className = 'msg';
if (!ziniaoAccountName || !ziniaoToken) {
msgEl.textContent = '请完整填写紫鸟账号名称、紫鸟令牌';
msgEl.classList.add('err');
return;
}
fetch('/api/admin/shop-key', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
ziniao_account_name: ziniaoAccountName,
ziniao_token: ziniaoToken
})
})
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.success) {
document.getElementById('shopKeyZiniaoAccountName').value = '';
document.getElementById('shopKeyZiniaoToken').value = '';
msgEl.textContent = res.msg || '创建成功';
msgEl.className = 'msg ok';
loadShopKeys(1);
} else {
msgEl.textContent = res.error || '创建失败';
msgEl.className = 'msg err';
}
})
.catch(function() {
msgEl.textContent = '请求失败';
msgEl.className = 'msg err';
});
};
document.getElementById('btnSaveShopKey').onclick = function() {
var itemId = document.getElementById('editShopKeyId').value;
var ziniaoAccountName = (document.getElementById('editShopKeyZiniaoAccountName').value || '').trim();
var ziniaoToken = (document.getElementById('editShopKeyZiniaoToken').value || '').trim();
var msgEl = document.getElementById('msgEditShopKey');
msgEl.textContent = '';
msgEl.className = 'msg';
if (!ziniaoAccountName || !ziniaoToken) {
msgEl.textContent = '请完整填写紫鸟账号名称、紫鸟令牌';
msgEl.classList.add('err');
return;
}
fetch('/api/admin/shop-key/' + itemId, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
ziniao_account_name: ziniaoAccountName,
ziniao_token: ziniaoToken
})
})
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.success) {
document.getElementById('editShopKeyModal').classList.remove('show');
loadShopKeys(shopKeyPage);
} else {
msgEl.textContent = res.error || '保存失败';
msgEl.classList.add('err');
}
})
.catch(function() {
msgEl.textContent = '请求失败';
msgEl.classList.add('err');
});
};
document.getElementById('btnCloseEditShopKey').onclick = function() {
document.getElementById('editShopKeyModal').classList.remove('show');
};
// ========== 店铺管理 ==========
var shopManagePage = 1, shopManagePageSize = 15;
var shopManageGroups = [];
function buildShopManageQuery(page) {
var query = 'page=' + (page || 1) + '&page_size=' + shopManagePageSize;
var groupId = (document.getElementById('shopManageFilterGroupId').value || '').trim();
var shopName = (document.getElementById('shopManageFilterShopName').value || '').trim();
if (groupId) query += '&group_id=' + encodeURIComponent(groupId);
if (shopName) query += '&shop_name=' + encodeURIComponent(shopName);
return query;
}
function refreshShopGroupSelects(selectedCreateId, selectedEditId) {
var createSel = document.getElementById('shopManageGroupSelect');
var editSel = document.getElementById('editShopManageGroupSelect');
var filterSel = document.getElementById('shopManageFilterGroupId');
var selectedFilterId = filterSel ? filterSel.value : '';
var createOpts = ['<option value="">请选择分组</option>'];
var filterOpts = ['<option value="">全部分组</option>'];
shopManageGroups.forEach(function(g) {
var option = '<option value="' + g.id + '">' + (g.group_name || '') + '</option>';
createOpts.push(option);
filterOpts.push(option);
});
createSel.innerHTML = createOpts.join('');
editSel.innerHTML = createOpts.join('');
if (filterSel) filterSel.innerHTML = filterOpts.join('');
if (selectedCreateId != null) createSel.value = String(selectedCreateId);
if (selectedEditId != null) editSel.value = String(selectedEditId);
if (filterSel && selectedFilterId) filterSel.value = selectedFilterId;
}
function loadShopManageGroups(selectedCreateId, selectedEditId) {
return fetch('/api/admin/shop-manage-groups')
.then(function(r) { return r.json(); })
.then(function(res) {
if (!res.success) throw new Error(res.error || '加载分组失败');
shopManageGroups = res.items || [];
refreshShopGroupSelects(selectedCreateId, selectedEditId);
return shopManageGroups;
})
.catch(function() {
shopManageGroups = [];
refreshShopGroupSelects();
return [];
});
}
function openShopManageGroupModal() {
document.getElementById('shopManageGroupEditId').value = '';
document.getElementById('shopManageGroupInput').value = '';
document.getElementById('msgShopManageGroup').textContent = '';
document.getElementById('msgShopManageGroup').className = 'msg';
loadShopManageGroups().then(function() {
renderShopManageGroupRows();
document.getElementById('shopManageGroupModal').classList.add('show');
});
}
function renderShopManageGroupRows() {
var tbody = document.getElementById('shopManageGroupListBody');
if (!shopManageGroups.length) {
tbody.innerHTML = '<tr><td colspan="5" class="empty-tip">暂无分组</td></tr>';
return;
}
tbody.innerHTML = shopManageGroups.map(function(item, index) {
return '<tr><td>' + (index + 1) + '</td><td>' + (item.group_name || '') + '</td><td>' + (item.created_at || '') + '</td><td>' + (item.updated_at || '') + '</td><td>' +
'<button class="btn btn-sm" data-shop-group-edit="' + item.id + '" data-shop-group-name="' + (item.group_name || '').replace(/"/g, '&quot;') + '">编辑</button> ' +
'<button class="btn btn-sm btn-danger" data-shop-group-delete="' + item.id + '" data-shop-group-name="' + (item.group_name || '').replace(/"/g, '&quot;') + '">删除</button>' +
'</td></tr>';
}).join('');
document.querySelectorAll('[data-shop-group-edit]').forEach(function(btn) {
btn.onclick = function() {
document.getElementById('shopManageGroupEditId').value = btn.dataset.shopGroupEdit;
document.getElementById('shopManageGroupInput').value = (btn.dataset.shopGroupName || '').replace(/&quot;/g, '"');
};
});
document.querySelectorAll('[data-shop-group-delete]').forEach(function(btn) {
btn.onclick = function() {
var gid = btn.dataset.shopGroupDelete;
var gname = (btn.dataset.shopGroupName || '').replace(/&quot;/g, '"');
if (!confirm('确定删除分组「' + gname + '」吗?')) return;
fetch('/api/admin/shop-manage-group/' + gid, { method: 'DELETE' })
.then(function(r) { return r.json(); })
.then(function(res) {
if (!res.success) {
alert(res.error || '删除失败');
return;
}
loadShopManageGroups().then(function() {
renderShopManageGroupRows();
loadShopManage(shopManagePage);
});
});
};
});
}
function loadShopManage(page) {
shopManagePage = page || 1;
fetch('/api/admin/shop-manages?' + buildShopManageQuery(shopManagePage))
.then(function(r) { return r.json(); })
.then(function(res) {
var tbody = document.getElementById('shopManageListBody');
if (!res.success) {
tbody.innerHTML = '<tr><td colspan="9" class="empty-tip">加载失败: ' + (res.error || '') + '</td></tr>';
return;
}
var items = res.items || [];
if (items.length === 0) {
tbody.innerHTML = '<tr><td colspan="9" class="empty-tip">暂无店铺</td></tr>';
} else {
tbody.innerHTML = items.map(function(item, index) {
var rowNo = (shopManagePage - 1) * shopManagePageSize + index + 1;
return '<tr><td>' + rowNo + '</td><td>' + (item.group_name || '') + '</td><td>' + (item.shop_name || '') + '</td><td>' + (item.mall_name || '') + '</td><td>' + (item.account || '') + '</td><td>' + (item.password || '') + '</td><td>' + (item.created_at || '') + '</td><td>' + (item.updated_at || '') + '</td><td>' +
'<button class="btn btn-sm" data-shop-manage-edit="' + item.id + '" data-shop-manage="' + (JSON.stringify(item).replace(/"/g, '&quot;')) + '">编辑</button> ' +
'<button class="btn btn-sm btn-danger" data-shop-manage-delete="' + item.id + '" data-shop-manage-name="' + (item.shop_name || '').replace(/"/g, '&quot;') + '">删除</button>' +
'</td></tr>';
}).join('');
}
renderPagination('shopManagePagination', res.total, res.page, res.page_size, loadShopManage);
bindShopManageActions();
})
.catch(function() {
document.getElementById('shopManageListBody').innerHTML = '<tr><td colspan="9" class="empty-tip">请求失败</td></tr>';
});
}
function bindShopManageActions() {
document.querySelectorAll('[data-shop-manage-edit]').forEach(function(btn) {
btn.onclick = function() {
var item = {};
try { item = JSON.parse((btn.dataset.shopManage || '').replace(/&quot;/g, '"')); } catch (e) { item = {}; }
document.getElementById('editShopManageId').value = item.id || '';
document.getElementById('editShopManageShopName').value = item.shop_name || '';
document.getElementById('editShopManageMallName').value = item.mall_name || '';
document.getElementById('editShopManageAccount').value = item.account || '';
document.getElementById('editShopManagePassword').value = item.password || '';
document.getElementById('msgEditShopManage').textContent = '';
document.getElementById('msgEditShopManage').className = 'msg';
loadShopManageGroups(null, item.group_id).then(function() {
document.getElementById('editShopManageModal').classList.add('show');
});
};
});
document.querySelectorAll('[data-shop-manage-delete]').forEach(function(btn) {
btn.onclick = function() {
var name = (btn.dataset.shopManageName || '').replace(/&quot;/g, '"');
if (!confirm('确定删除店铺「' + name + '」吗?')) return;
fetch('/api/admin/shop-manage/' + btn.dataset.shopManageDelete, { method: 'DELETE' })
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.success) { loadShopManage(shopManagePage); }
else { alert(res.error || '删除失败'); }
});
};
});
}
document.getElementById('btnManageShopGroups').onclick = openShopManageGroupModal;
document.getElementById('btnManageShopGroupsFromEdit').onclick = openShopManageGroupModal;
document.getElementById('btnSearchShopManage').onclick = function() {
loadShopManage(1);
};
document.getElementById('shopManageFilterShopName').addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
loadShopManage(1);
}
});
document.getElementById('btnCloseShopManageGroupModal').onclick = function() {
document.getElementById('shopManageGroupModal').classList.remove('show');
};
document.getElementById('btnCancelShopManageGroupEdit').onclick = function() {
document.getElementById('shopManageGroupEditId').value = '';
document.getElementById('shopManageGroupInput').value = '';
};
document.getElementById('btnSaveShopManageGroup').onclick = function() {
var editId = (document.getElementById('shopManageGroupEditId').value || '').trim();
var groupName = (document.getElementById('shopManageGroupInput').value || '').trim();
var msgEl = document.getElementById('msgShopManageGroup');
msgEl.textContent = '';
msgEl.className = 'msg';
if (!groupName) {
msgEl.textContent = '请输入分组名称';
msgEl.className = 'msg err';
return;
}
var method = editId ? 'PUT' : 'POST';
var url = editId ? ('/api/admin/shop-manage-group/' + editId) : '/api/admin/shop-manage-group';
fetch(url, {
method: method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ group_name: groupName })
})
.then(function(r) { return r.json(); })
.then(function(res) {
if (!res.success) {
msgEl.textContent = res.error || '保存失败';
msgEl.className = 'msg err';
return;
}
document.getElementById('shopManageGroupEditId').value = '';
document.getElementById('shopManageGroupInput').value = '';
msgEl.textContent = res.msg || '保存成功';
msgEl.className = 'msg ok';
loadShopManageGroups().then(function() {
renderShopManageGroupRows();
loadShopManage(shopManagePage);
});
})
.catch(function() {
msgEl.textContent = '请求失败';
msgEl.className = 'msg err';
});
};
document.getElementById('btnCreateShopManage').onclick = function() {
var groupId = (document.getElementById('shopManageGroupSelect').value || '').trim();
var shopName = (document.getElementById('shopManageShopName').value || '').trim();
var mallName = (document.getElementById('shopManageMallName').value || '').trim();
var account = (document.getElementById('shopManageAccount').value || '').trim();
var password = (document.getElementById('shopManagePassword').value || '').trim();
var msgEl = document.getElementById('msgShopManage');
msgEl.textContent = '';
msgEl.className = 'msg';
if (!groupId || !shopName || !mallName || !account || !password) {
msgEl.textContent = '请完整填写分组、店铺名、店铺商城名、账号、密码';
msgEl.classList.add('err');
return;
}
fetch('/api/admin/shop-manage', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ group_id: Number(groupId), shop_name: shopName, mall_name: mallName, account: account, password: password })
})
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.success) {
document.getElementById('shopManageGroupSelect').value = '';
document.getElementById('shopManageShopName').value = '';
document.getElementById('shopManageMallName').value = '';
document.getElementById('shopManageAccount').value = '';
document.getElementById('shopManagePassword').value = '';
msgEl.textContent = res.msg || '创建成功';
msgEl.className = 'msg ok';
loadShopManage(1);
} else {
msgEl.textContent = res.error || '创建失败';
msgEl.className = 'msg err';
}
})
.catch(function() {
msgEl.textContent = '请求失败';
msgEl.className = 'msg err';
});
};
document.getElementById('btnSaveShopManage').onclick = function() {
var itemId = document.getElementById('editShopManageId').value;
var groupId = (document.getElementById('editShopManageGroupSelect').value || '').trim();
var shopName = (document.getElementById('editShopManageShopName').value || '').trim();
var mallName = (document.getElementById('editShopManageMallName').value || '').trim();
var account = (document.getElementById('editShopManageAccount').value || '').trim();
var password = (document.getElementById('editShopManagePassword').value || '').trim();
var msgEl = document.getElementById('msgEditShopManage');
msgEl.textContent = '';
msgEl.className = 'msg';
if (!groupId || !shopName || !mallName || !account || !password) {
msgEl.textContent = '请完整填写分组、店铺名、店铺商城名、账号、密码';
msgEl.classList.add('err');
return;
}
fetch('/api/admin/shop-manage/' + itemId, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ group_id: Number(groupId), shop_name: shopName, mall_name: mallName, account: account, password: password })
})
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.success) {
document.getElementById('editShopManageModal').classList.remove('show');
loadShopManage(shopManagePage);
} else {
msgEl.textContent = res.error || '保存失败';
msgEl.classList.add('err');
}
})
.catch(function() {
msgEl.textContent = '请求失败';
msgEl.classList.add('err');
});
};
document.getElementById('btnCloseEditShopManage').onclick = function() {
document.getElementById('editShopManageModal').classList.remove('show');
};
// ========== 版本管理 ==========
function loadVersions() {
fetch('/api/admin/versions')
.then(function(r) { return r.json(); })
.then(function(res) {
var tbody = document.getElementById('versionListBody');
if (!res.success) {
tbody.innerHTML = '<tr><td colspan="4" class="empty-tip">加载失败: ' + (res.error || '') + '</td></tr>';
return;
}
var items = res.items || [];
if (items.length === 0) {
tbody.innerHTML = '<tr><td colspan="4" class="empty-tip">暂无版本记录</td></tr>';
} else {
tbody.innerHTML = items.map(function(v) {
var url = (v.file_url || '').replace(/"/g, '&quot;');
return '<tr><td>' + (v.version || '') + '</td><td><a href="' + url + '" target="_blank" rel="noopener">' + url + '</a></td><td>' + (v.created_at || '') + '</td><td><a href="' + url + '" download class="btn btn-sm">下载</a></td></tr>';
}).join('');
}
})
.catch(function() {
document.getElementById('versionListBody').innerHTML = '<tr><td colspan="4" class="empty-tip">请求失败</td></tr>';
});
}
document.getElementById('btnUploadVersion').onclick = function() {
var version = (document.getElementById('versionNumber').value || '').trim();
var fileInput = document.getElementById('versionZip');
var msgEl = document.getElementById('msgVersion');
msgEl.textContent = '';
msgEl.className = 'msg';
if (!version) {
msgEl.textContent = '请填写版本号';
msgEl.classList.add('err');
return;
}
if (!fileInput.files || fileInput.files.length === 0) {
msgEl.textContent = '请选择 zip 压缩包';
msgEl.classList.add('err');
return;
}
var file = fileInput.files[0];
if (!(file.name || '').toLowerCase().endsWith('.zip')) {
msgEl.textContent = '仅支持 .zip 格式';
msgEl.classList.add('err');
return;
}
var formData = new FormData();
formData.append('version', version);
formData.append('file', file);
msgEl.textContent = '上传中…';
msgEl.classList.remove('err', 'ok');
fetch('/api/admin/version', {
method: 'POST',
body: formData
})
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.success) {
msgEl.textContent = '发布成功。版本:' + res.version + ',链接:' + (res.file_url || '');
msgEl.classList.add('ok');
document.getElementById('versionNumber').value = '';
fileInput.value = '';
loadVersions();
} else {
msgEl.textContent = res.error || '上传失败';
msgEl.classList.add('err');
}
})
.catch(function() {
msgEl.textContent = '请求失败';
msgEl.classList.add('err');
});
};
// ========== 栏目权限配置 ==========
function loadColumns() {
fetch('/api/admin/columns')
.then(function(r) { return r.json(); })
.then(function(res) {
var tbody = document.getElementById('columnListBody');
if (!res.success) {
tbody.innerHTML = '<tr><td colspan="5" class="empty-tip">加载失败: ' + (res.error || '') + '</td></tr>';
return;
}
allColumnsList = res.items || [];
if (allColumnsList.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" class="empty-tip">暂无栏目,请在上方新增</td></tr>';
} else {
tbody.innerHTML = allColumnsList.map(function(c) {
return '<tr><td>' + c.id + '</td><td>' + (c.name || '') + '</td><td>' + (c.column_key || '') + '</td><td>' + (c.created_at || '') + '</td><td>' +
'<button class="btn btn-sm" data-column-edit="' + c.id + '" data-name="' + (c.name || '').replace(/"/g, '&quot;') + '" data-key="' + (c.column_key || '').replace(/"/g, '&quot;') + '">编辑</button> ' +
'<button class="btn btn-sm btn-danger" data-column-delete="' + c.id + '" data-name="' + (c.name || '').replace(/"/g, '&quot;') + '">删除</button></td></tr>';
}).join('');
}
bindColumnActions();
})
.catch(function() {
document.getElementById('columnListBody').innerHTML = '<tr><td colspan="5" class="empty-tip">请求失败</td></tr>';
});
}
function bindColumnActions() {
document.querySelectorAll('[data-column-edit]').forEach(function(btn) {
btn.onclick = function() {
document.getElementById('editColumnId').value = btn.dataset.columnEdit || '';
document.getElementById('editColumnName').value = (btn.dataset.name || '').replace(/&quot;/g, '"');
document.getElementById('editColumnKey').value = (btn.dataset.key || '').replace(/&quot;/g, '"');
document.getElementById('msgEditColumn').textContent = '';
document.getElementById('editColumnModal').classList.add('show');
};
});
document.querySelectorAll('[data-column-delete]').forEach(function(btn) {
btn.onclick = function() {
if (!confirm('确定删除栏目「' + (btn.dataset.name || '').replace(/&quot;/g, '"') + '」吗?')) return;
fetch('/api/admin/column/' + btn.dataset.columnDelete, { method: 'DELETE' })
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.success) { loadColumns(); loadColumnsForPermission(); }
else { alert(res.error || '删除失败'); }
});
};
});
}
document.getElementById('btnAddColumn').onclick = function() {
var name = (document.getElementById('columnName').value || '').trim();
var key = (document.getElementById('columnKey').value || '').trim();
var msgEl = document.getElementById('msgColumn');
msgEl.textContent = '';
msgEl.className = 'msg';
if (!name) { msgEl.textContent = '请填写栏目名'; msgEl.classList.add('err'); return; }
if (!key) { msgEl.textContent = '请填写栏目标识'; msgEl.classList.add('err'); return; }
fetch('/api/admin/column', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: name, column_key: key })
})
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.success) {
msgEl.textContent = res.msg || '新增成功';
msgEl.classList.add('ok');
document.getElementById('columnName').value = '';
document.getElementById('columnKey').value = '';
loadColumns();
loadColumnsForPermission();
} else {
msgEl.textContent = res.error || '新增失败';
msgEl.classList.add('err');
}
})
.catch(function() { msgEl.textContent = '请求失败'; msgEl.classList.add('err'); });
};
document.getElementById('btnSaveColumn').onclick = function() {
var cid = document.getElementById('editColumnId').value;
var name = (document.getElementById('editColumnName').value || '').trim();
var key = (document.getElementById('editColumnKey').value || '').trim();
var msgEl = document.getElementById('msgEditColumn');
msgEl.textContent = '';
msgEl.className = 'msg';
if (!name) { msgEl.textContent = '请填写栏目名'; msgEl.classList.add('err'); return; }
if (!key) { msgEl.textContent = '请填写栏目标识'; msgEl.classList.add('err'); return; }
fetch('/api/admin/column/' + cid, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: name, column_key: key })
})
.then(function(r) { return r.json(); })
.then(function(res) {
if (res.success) {
document.getElementById('editColumnModal').classList.remove('show');
loadColumns();
loadColumnsForPermission();
} else {
msgEl.textContent = res.error || '保存失败';
msgEl.classList.add('err');
}
});
};
document.getElementById('btnCloseEditColumn').onclick = function() {
document.getElementById('editColumnModal').classList.remove('show');
};
// ========== 分页 ==========
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();
loadColumnsForPermission();
loadDedupeTotalData(1);
loadShopKeys(1);
loadShopManageGroups();
loadShopManage(1);
})();
</script>
</body>
</html>