Files
crawler-plugin/backend/web_source/admin.html
2026-04-24 15:14:14 +08:00

4182 lines
214 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;
}
.page-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 20px;
}
.page-head h1 {
margin-bottom: 0;
}
.admin-user-box {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 12px;
background: #fff;
border: 1px solid #e6eaf2;
border-radius: 999px;
box-shadow: 0 4px 14px rgba(102, 126, 234, 0.08);
}
.admin-user-name {
font-size: 14px;
color: #333;
max-width: 220px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.admin-user-role {
font-size: 12px;
color: #667eea;
background: #eef1ff;
padding: 4px 8px;
border-radius: 999px;
}
.admin-user-logout {
padding: 6px 12px;
font-size: 13px;
}
.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;
}
#shopManageGroupModal {
z-index: 1010;
}
.modal {
background: #fff;
border-radius: 8px;
padding: 24px;
min-width: 320px;
width: min(920px, calc(100vw - 48px));
max-width: 90%;
max-height: calc(100vh - 48px);
overflow-y: auto;
overflow-x: hidden;
box-sizing: border-box;
}
.modal h3 {
margin-bottom: 16px;
font-size: 16px;
}
.column-permission-cards {
display: flex;
flex-wrap: wrap;
gap: 8px;
min-height: 32px;
margin-bottom: 8px;
width: 100%;
max-width: 100%;
overflow: hidden;
}
.column-permission-card {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
background: #e8ebf7;
color: #667eea;
border-radius: 6px;
font-size: 13px;
max-width: 100%;
min-width: 0;
flex: 0 1 auto;
white-space: normal;
overflow-wrap: anywhere;
word-break: break-word;
}
.column-permission-card .col-card-remove {
flex: 0 0 auto;
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 {
width: 100%;
max-width: 100%;
min-height: 80px;
max-height: 140px;
overflow-y: auto;
padding: 8px;
box-sizing: border-box;
}
.column-permission-select option {
padding: 4px 0;
}
</style>
</head>
<body>
<!-- <div class="nav"><a href="/home">返回首页</a></div> -->
<h1>管理后台</h1>
<div class="admin-user-box" style="position:absolute;top:24px;right:24px;z-index:10;">
<span class="admin-user-name" id="adminCurrentUsername">加载中...</span>
<span class="admin-user-role" id="adminCurrentUserRole" style="display:none;"></span>
<button class="btn btn-secondary admin-user-logout" id="btnAdminLogout" type="button">退出登录</button>
</div>
<div class="tabs" id="adminTabs">
<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="skip-price-asin">跳过跟价 ASIN</div>
<div class="tab" data-tab="query-asin">查询 ASIN</div>
<div class="tab" data-tab="history">查看生成记录</div>
<div class="tab" data-tab="version">版本管理</div>
</div>
<!-- 用户管理 -->
<div id="panel-users" class="tab-panel">
<div class="form-box">
<h3 style="margin-bottom:16px;font-size:15px;">创建用户</h3>
<p style="margin-bottom:16px;font-size:13px;color:#666;">
当前没有开放注册入口,仅管理员可在此创建用户。层级关系:超级管理员 -&gt; 管理员 -&gt; 普通账号。</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="例如shop_manage">
</div>
<div class="form-group" style="min-width:220px;">
<label>菜单路由</label>
<input type="text" id="columnRoutePath" placeholder="例如dedupe-total-data">
</div>
<div class="form-group" style="min-width:140px;">
<label>排序</label>
<input type="number" id="columnSortOrder" placeholder="例如10">
</div>
<div class="form-group" style="min-width:160px;">
<label>菜单类型</label>
<select id="columnMenuType">
<option value="admin">后台(admin)</option>
<option value="app">软件(app)</option>
</select>
</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>
<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-skip-price-asin" class="tab-panel">
<div class="form-box">
<h3 style="margin-bottom:16px;font-size:15px;">新增 ASIN</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="skipPriceAsinGroupSelect" style="min-width:150px;">
<option value="">请选择分组</option>
</select>
<button class="btn btn-secondary" id="btnManageSkipPriceAsinGroups" type="button">管理分组</button>
</div>
</div>
<div class="form-group" style="min-width:180px;">
<label>店铺名</label>
<input type="text" id="skipPriceAsinShopName" placeholder="请输入店铺名称">
</div>
<div class=" form-group" style="min-width:220px;">
<label>国家</label>
<div style="margin-bottom:8px;">
<label>&nbsp;</label>
<button class="btn btn-secondary" id="btnChooseSkipPriceAsinShop" type="button">选择店铺</button>
</div>
<select id="skipPriceAsinCountries" multiple size="5">
<option value="DE">德国</option>
<option value="UK">英国</option>
<option value="FR">法国</option>
<option value="IT">意大利</option>
<option value="ES">西班牙</option>
</select>
</div>
<div class="form-group" style="min-width:320px;flex:1;">
<label>ASIN / 最低价</label>
<div id="skipPriceAsinInputs" style="display:flex;flex-direction:column;gap:8px;">
<div style="color:#999;font-size:13px;">请选择国家后输入对应 ASIN 和最低价</div>
</div>
</div>
<button class="btn" id="btnCreateSkipPriceAsin">新增 ASIN</button>
</div>
<p class="msg" id="msgSkipPriceAsin"></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="skipPriceAsinFilterGroupId">
<option value="">全部分组</option>
</select>
</div>
<div class="form-group" style="min-width:220px;">
<label>店铺名</label>
<input type="text" id="skipPriceAsinFilterShopName" placeholder="请输入店铺名">
</div>
<div class="form-group" style="min-width:220px;">
<label>ASIN</label>
<input type="text" id="skipPriceAsinFilterAsin" placeholder="请输入 ASIN">
</div>
<button class="btn" id="btnSearchSkipPriceAsin">查询</button>
</div>
<table>
<thead>
<tr>
<th>序号</th>
<th>分组</th>
<th>店铺名</th>
<th>德国</th>
<th>英国</th>
<th>法国</th>
<th>意大利</th>
<th>西班牙</th>
</tr>
</thead>
<tbody id="skipPriceAsinListBody"></tbody>
</table>
<div class="pagination" id="skipPriceAsinPagination"></div>
</div>
</div>
<div id="panel-query-asin" class="tab-panel">
<div class="form-box">
<h3 style="margin-bottom:16px;font-size:15px;">新增查询 ASIN</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="queryAsinGroupSelect" style="min-width:150px;">
<option value="">请选择分组</option>
</select>
<button class="btn btn-secondary" id="btnManageQueryAsinGroups" type="button">管理分组</button>
</div>
</div>
<div class="form-group" style="min-width:340px;">
<label>店铺名</label>
<div style="display:flex;gap:8px;align-items:center;">
<input type="text" id="queryAsinShopName" placeholder="请输入店铺名称" style="flex:1;min-width:180px;">
<button class="btn btn-secondary" id="btnChooseQueryAsinShop" type="button">选择店铺</button>
</div>
</div>
<div class="form-group" style="min-width:220px;">
<label>国家</label>
<select id="queryAsinCountries" multiple size="5" style="min-height:98px;">
<option value="DE">德国</option>
<option value="UK">英国</option>
<option value="FR">法国</option>
<option value="IT">意大利</option>
<option value="ES">西班牙</option>
</select>
</div>
<div class="form-group" style="min-width:320px;flex:1;">
<label>ASIN</label>
<div id="queryAsinInputs" style="display:flex;flex-direction:column;gap:8px;">
<div style="color:#999;font-size:13px;">请选择国家后输入对应 ASIN</div>
</div>
</div>
<button class="btn" id="btnCreateQueryAsin" style="align-self:flex-end;margin-bottom:40px;">新增 ASIN</button>
</div>
<p class="msg" id="msgQueryAsin"></p>
<div class="form-row" style="align-items:flex-start;gap:16px;margin-top:18px;border-top:1px solid #f0f0f0;padding-top:16px;">
<div style="flex:1;min-width:320px;">
<h3 style="margin-bottom:12px;font-size:14px;">导入添加</h3>
<div class="form-row">
<div class="form-group" style="min-width:320px;">
<label>上传 Excel分组、店铺名、德国/英国/法国/意大利/西班牙)</label>
<input type="file" id="queryAsinImportFile" accept=".xlsx,.xls">
</div>
<button class="btn" id="btnImportQueryAsin" type="button">上传并添加</button>
</div>
<p class="msg" id="msgQueryAsinImport"></p>
<div class="progress-wrap" id="queryAsinImportProgressWrap" style="display:none;">
<div class="progress-bar">
<div class="progress-fill" id="queryAsinImportProgressFill"></div>
</div>
<div class="progress-text" id="queryAsinImportProgressText"></div>
</div>
</div>
<div style="flex:1;min-width:320px;">
<h3 style="margin-bottom:12px;font-size:14px;">导入删除</h3>
<div class="form-row">
<div class="form-group" style="min-width:320px;">
<label>上传 Excel匹配分组 + 店铺名 + 国家 ASIN 后删除)</label>
<input type="file" id="queryAsinDeleteImportFile" accept=".xlsx,.xls">
</div>
<button class="btn btn-danger" id="btnDeleteImportQueryAsin" type="button">上传并删除</button>
</div>
<p class="msg" id="msgQueryAsinDeleteImport"></p>
<div class="progress-wrap" id="queryAsinDeleteImportProgressWrap" style="display:none;">
<div class="progress-bar">
<div class="progress-fill" id="queryAsinDeleteImportProgressFill"></div>
</div>
<div class="progress-text" id="queryAsinDeleteImportProgressText"></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:12px;">
<div class="form-group" style="min-width:180px;">
<label>分组</label>
<select id="queryAsinFilterGroupId">
<option value="">全部分组</option>
</select>
</div>
<div class="form-group" style="min-width:220px;">
<label>店铺名</label>
<input type="text" id="queryAsinFilterShopName" placeholder="请输入店铺名">
</div>
<div class="form-group" style="min-width:220px;">
<label>ASIN</label>
<input type="text" id="queryAsinFilterAsin" placeholder="请输入 ASIN">
</div>
<button class="btn" id="btnSearchQueryAsin">查询</button>
</div>
<table>
<thead>
<tr>
<th>序号</th>
<th>分组</th>
<th>店铺名</th>
<th>德国</th>
<th>英国</th>
<th>法国</th>
<th>意大利</th>
<th>西班牙</th>
</tr>
</thead>
<tbody id="queryAsinListBody"></tbody>
</table>
<div class="pagination" id="queryAsinPagination"></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>
<div class="form-group">
<label>菜单路由</label>
<input type="text" id="editColumnRoutePath" placeholder="菜单路由">
</div>
<div class="form-group">
<label>排序</label>
<input type="number" id="editColumnSortOrder" placeholder="排序">
</div>
<div class="form-group">
<label>菜单类型</label>
<select id="editColumnMenuType">
<option value="admin">后台(admin)</option>
<option value="app">软件(app)</option>
</select>
</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">
<input type="hidden" id="shopManageGroupLeaderUserId">
<div class="form-group" style="min-width:220px;">
<label>组长</label>
<input type="text" id="shopManageGroupLeaderName" placeholder="当前登录用户" readonly
style="background:#f5f5f5;">
</div>
<div class="form-group" style="min-width:260px;">
<label>分组名称</label>
<input type="text" id="shopManageGroupInput" placeholder="请输入分组名称">
</div>
<div class="form-group" style="min-width:280px;flex:1;">
<label>组员</label>
<select id="shopManageGroupMemberSelect" multiple size="6" style="min-height:140px;">
</select>
<div id="shopManageGroupMemberHelp" style="color:#888;font-size:12px;margin-top:6px;">可添加当前组长创建的普通员工账号,按住 Ctrl 或 Command 可多选。</div>
</div>
</div>
<div style="display:flex;gap:8px;justify-content:flex-end;margin-bottom:12px;">
<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>
<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="chooseSkipPriceAsinShopModal">
<div class="modal" style="max-width:900px;width:90%;max-height:80vh;display:flex;flex-direction:column;">
<h3>选择店铺</h3>
<div class="form-row">
<div class="form-group" style="min-width:180px;">
<label>分组</label>
<select id="chooseSkipPriceAsinShopGroupId">
<option value="">全部分组</option>
</select>
</div>
<div class="form-group" style="min-width:220px;">
<label>店铺名</label>
<input type="text" id="chooseSkipPriceAsinShopKeyword" placeholder="请输入店铺名">
</div>
<button class="btn" id="btnSearchChooseSkipPriceAsinShop" type="button">查询</button>
</div>
<div style="flex:1;min-height:0;overflow:auto;">
<table>
<thead>
<tr>
<th>序号</th>
<th>分组</th>
<th>店铺名</th>
<th>店铺商城名</th>
<th>账号</th>
<th>操作</th>
</tr>
</thead>
<tbody id="chooseSkipPriceAsinShopListBody"></tbody>
</table>
</div>
<div class="pagination" id="chooseSkipPriceAsinShopPagination"></div>
<div style="margin-top:16px;display:flex;gap:8px;justify-content:flex-end;">
<button class="btn btn-secondary" id="btnCloseChooseSkipPriceAsinShopModal" type="button">关闭</button>
</div>
</div>
</div>
<div class="modal-mask" id="editSkipPriceAsinModal">
<div class="modal">
<h3>编辑跳过跟价 ASIN</h3>
<input type="hidden" id="editSkipPriceAsinId">
<input type="hidden" id="editSkipPriceAsinCountry">
<div class="form-group">
<label>店铺名</label>
<input type="text" id="editSkipPriceAsinShopName" readonly style="background:#f5f5f5;">
</div>
<div class="form-group">
<label>国家</label>
<input type="text" id="editSkipPriceAsinCountryLabel" readonly style="background:#f5f5f5;">
</div>
<div class="form-group">
<label>ASIN</label>
<input type="text" id="editSkipPriceAsinValue" placeholder="请输入 ASIN">
</div>
<div class="form-group">
<label>最低价</label>
<input type="number" id="editSkipPriceMinimumPrice" min="0" step="0.01" placeholder="请输入最低价,可留空">
</div>
<p class="msg" id="msgEditSkipPriceAsin"></p>
<div style="margin-top:16px;display:flex;gap:8px;">
<button class="btn" id="btnSaveSkipPriceAsin">保存</button>
<button class="btn btn-secondary" id="btnCloseEditSkipPriceAsin">取消</button>
</div>
</div>
</div>
<div class="modal-mask" id="chooseQueryAsinShopModal">
<div class="modal" style="max-width:900px;width:90%;max-height:80vh;display:flex;flex-direction:column;">
<h3>选择店铺</h3>
<div class="form-row">
<div class="form-group" style="min-width:180px;">
<label>分组</label>
<select id="chooseQueryAsinShopGroupId">
<option value="">全部分组</option>
</select>
</div>
<div class="form-group" style="min-width:220px;">
<label>店铺名</label>
<input type="text" id="chooseQueryAsinShopKeyword" placeholder="请输入店铺名">
</div>
<button class="btn" id="btnSearchChooseQueryAsinShop" type="button">查询</button>
</div>
<div style="flex:1;min-height:0;overflow:auto;">
<table>
<thead>
<tr>
<th>序号</th>
<th>分组</th>
<th>店铺名</th>
<th>店铺商城名</th>
<th>账号</th>
<th>操作</th>
</tr>
</thead>
<tbody id="chooseQueryAsinShopListBody"></tbody>
</table>
</div>
<div class="pagination" id="chooseQueryAsinShopPagination"></div>
<div style="margin-top:16px;display:flex;gap:8px;justify-content:flex-end;">
<button class="btn btn-secondary" id="btnCloseChooseQueryAsinShopModal" type="button">关闭</button>
</div>
</div>
</div>
<div class="modal-mask" id="editQueryAsinModal">
<div class="modal">
<h3>编辑查询 ASIN</h3>
<input type="hidden" id="editQueryAsinId">
<input type="hidden" id="editQueryAsinCountry">
<div class="form-group">
<label>店铺名</label>
<input type="text" id="editQueryAsinShopName" readonly style="background:#f5f5f5;">
</div>
<div class="form-group">
<label>国家</label>
<input type="text" id="editQueryAsinCountryLabel" readonly style="background:#f5f5f5;">
</div>
<div class="form-group">
<label>ASIN</label>
<input type="text" id="editQueryAsinValue" placeholder="请输入 ASIN">
</div>
<p class="msg" id="msgEditQueryAsin"></p>
<div style="margin-top:16px;display:flex;gap:8px;">
<button class="btn" id="btnSaveQueryAsin">保存</button>
<button class="btn btn-secondary" id="btnCloseEditQueryAsin">取消</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 切换
var adminTabsEl = document.getElementById('adminTabs');
if (adminTabsEl) adminTabsEl.innerHTML = '';
var activeAdminTabName = '';
var ADMIN_PANEL_MAP = {
'users': 'panel-users',
'columns': 'panel-columns',
'dedupe-total-data': 'panel-dedupe-total-data',
'shop-keys': 'panel-shop-keys',
'shop-manage': 'panel-shop-manage',
'skip-price-asin': 'panel-skip-price-asin',
'query-asin': 'panel-query-asin',
'history': 'panel-history',
'version': 'panel-version'
};
function runTabLoader(tabName) {
if (tabName === 'users') { loadUsers(1); loadColumnsForPermission(); }
else if (tabName === 'columns') loadColumns();
else if (tabName === 'dedupe-total-data') loadDedupeTotalData(1);
else if (tabName === 'shop-keys') loadShopKeys(1);
else if (tabName === 'shop-manage') loadShopManage(1);
else if (tabName === 'skip-price-asin') loadSkipPriceAsin(1);
else if (tabName === 'query-asin') loadQueryAsin(1);
else if (tabName === 'history') loadHistory(1);
else if (tabName === 'version') loadVersions();
}
function hideAllAdminPanels() {
document.querySelectorAll('.tab-panel').forEach(function (panel) {
panel.classList.remove('active');
panel.style.display = 'none';
});
}
function getActiveAdminTabName() {
return activeAdminTabName;
}
function activateAdminTab(tabName) {
var panelId = ADMIN_PANEL_MAP[tabName];
var panel = panelId ? document.getElementById(panelId) : null;
if (!panel) return;
activeAdminTabName = tabName;
document.querySelectorAll('#adminTabs .tab').forEach(function (tab) {
tab.classList.toggle('active', tab.dataset.tab === tabName);
});
hideAllAdminPanels();
panel.style.display = '';
panel.classList.add('active');
runTabLoader(tabName);
}
function renderAdminTabs(items) {
var knownItems = (items || []).filter(function (item) {
return !!ADMIN_PANEL_MAP[item.route_path];
});
if (!knownItems.length) {
activeAdminTabName = '';
if (adminTabsEl) adminTabsEl.innerHTML = '<div class="tab active" style="cursor:default;">暂无可用菜单</div>';
hideAllAdminPanels();
return;
}
if (adminTabsEl) {
adminTabsEl.innerHTML = knownItems.map(function (item) {
return '<div class="tab" data-tab="' + item.route_path + '">' + (item.name || item.route_path) + '</div>';
}).join('');
}
document.querySelectorAll('#adminTabs .tab').forEach(function (tab) {
tab.onclick = function () {
activateAdminTab(tab.dataset.tab);
};
});
var fallbackTab = activeAdminTabName && knownItems.some(function (item) { return item.route_path === activeAdminTabName; })
? activeAdminTabName
: knownItems[0].route_path;
activateAdminTab(fallbackTab);
}
function loadAdminMenus(preferredTab) {
if (preferredTab) activeAdminTabName = preferredTab;
fetch('/api/admin/current-user/menus')
.then(function (r) { return r.json(); })
.then(function (res) {
if (!res.success) {
if (adminTabsEl) adminTabsEl.innerHTML = '<div class="tab active" style="cursor:default;">菜单加载失败</div>';
hideAllAdminPanels();
return;
}
renderAdminTabs(res.items || []);
})
.catch(function () {
if (adminTabsEl) adminTabsEl.innerHTML = '<div class="tab active" style="cursor:default;">菜单加载失败</div>';
hideAllAdminPanels();
});
}
hideAllAdminPanels();
// ========== 用户管理 ==========
var userPage = 1, userPageSize = 15;
var currentUserId = null;
var currentUserRole = 'admin';
var currentUserUsername = '';
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="7" 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();
})
.catch(function () {
document.getElementById('userListBody').innerHTML = '<tr><td colspan="7" 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')
.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();
});
}
function applyTabAccess(tabName, canUse) {
var tab = document.querySelector('.tab[data-tab="' + tabName + '"]');
var panel = document.getElementById('panel-' + tabName);
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 hasAdminTabAccess(tabName) {
var config = ADMIN_TAB_ACCESS_CONFIG[tabName];
if (!config) return true;
if (currentUserRole === 'super_admin') return true;
if (config.superAdminOnly) return false;
return !!currentUserAdminPermissionKeys[config.columnKey] ||
!!currentUserAdminPermissionRoutes[config.routePath];
}
function refreshAdminTabAccess() {
Object.keys(ADMIN_TAB_ACCESS_CONFIG).forEach(function (tabName) {
applyTabAccess(tabName, hasAdminTabAccess(tabName));
});
}
function loadCurrentUserAdminPermissions() {
currentUserAdminPermissionKeys = {};
currentUserAdminPermissionRoutes = {};
if (!currentUserId || currentUserRole === 'super_admin') {
refreshAdminTabAccess();
return;
}
fetch('/api/admin/user/' + currentUserId + '/column-permissions')
.then(function (r) { return r.json(); })
.then(function (res) {
if (!res.success) {
refreshAdminTabAccess();
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;
});
refreshAdminTabAccess();
})
.catch(function () {
refreshAdminTabAccess();
});
}
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);
if (String(uid || '') === String(currentUserId || '')) {
loadAdminMenus(getActiveAdminTabName());
}
} 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>';
});
}
var shopManageAllUsers = [];
function getEligibleShopManageGroupUsers(leaderUserId) {
var canViewAllMembers = currentUserRole === 'super_admin';
return shopManageAllUsers.filter(function (u) {
if (String(u.id || '') === String(leaderUserId || '')) return false;
if (canViewAllMembers) return true;
return String(u.role || '') === 'normal' &&
String(u.created_by_id || '') === String(leaderUserId || '');
});
}
function refreshShopManageGroupMemberSelect(leaderUserId, selectedUserIds) {
var sel = document.getElementById('shopManageGroupMemberSelect');
var helpEl = document.getElementById('shopManageGroupMemberHelp');
if (!sel) return;
var selectedMap = {};
(selectedUserIds || []).forEach(function (id) {
selectedMap[String(id)] = true;
});
var eligibleUsers = getEligibleShopManageGroupUsers(leaderUserId);
var options = eligibleUsers.map(function (u) {
return '<option value="' + u.id + '"' + (selectedMap[String(u.id)] ? ' selected' : '') + '>' + (u.username || '') + '</option>';
});
if (options.length) {
sel.innerHTML = options.join('');
if (helpEl) {
helpEl.textContent = '可添加当前组长创建的普通员工账号,按住 Ctrl 或 Command 可多选。';
}
return;
}
sel.innerHTML = '<option value="" disabled>当前组长暂无可添加组员</option>';
if (helpEl) {
helpEl.textContent = currentUserRole === 'normal'
? '普通账号没有下属普通员工时,这里会为空;当前账号只能作为组长使用。'
: '当前组长名下暂无可添加的普通员工账号。';
}
}
function setShopManageGroupLeader(leaderUserId, leaderUsername, selectedUserIds) {
var leaderIdEl = document.getElementById('shopManageGroupLeaderUserId');
var leaderNameEl = document.getElementById('shopManageGroupLeaderName');
if (leaderIdEl) leaderIdEl.value = leaderUserId ? String(leaderUserId) : '';
if (leaderNameEl) leaderNameEl.value = leaderUsername || '';
refreshShopManageGroupMemberSelect(leaderUserId, selectedUserIds || []);
}
function getSelectedShopManageGroupMemberIds() {
var sel = document.getElementById('shopManageGroupMemberSelect');
if (!sel) return [];
return Array.prototype.slice.call(sel.options || [])
.filter(function (opt) { return !!opt.selected; })
.map(function (opt) { return Number(opt.value); })
.filter(function (id) { return !!id; });
}
function loadUserOptions() {
return fetch('/api/admin/users?page=1&page_size=999')
.then(function (r) { return r.json(); })
.then(function (res) {
if (!res.success) {
throw new Error(res.error || '加载用户失败');
}
var items = res.items || [];
shopManageAllUsers = items.map(function (u) {
return {
id: u.id,
username: u.username || '',
role: u.role || 'normal',
created_by_id: u.created_by_id || null
};
});
setShopManageGroupLeader(
document.getElementById('shopManageGroupLeaderUserId') ? document.getElementById('shopManageGroupLeaderUserId').value : '',
document.getElementById('shopManageGroupLeaderName') ? document.getElementById('shopManageGroupLeaderName').value : '',
getSelectedShopManageGroupMemberIds()
);
var sel = document.getElementById('filterUser');
var cur = sel.value;
sel.innerHTML = '<option value="">全部用户</option>';
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 || '';
})
.catch(function () {
shopManageAllUsers = [];
var sel = document.getElementById('filterUser');
if (sel) {
sel.innerHTML = '<option value="">全部用户</option>';
}
refreshShopManageGroupMemberSelect(
document.getElementById('shopManageGroupLeaderUserId') ? document.getElementById('shopManageGroupLeaderUserId').value : '',
[]
);
});
}
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="8" 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="7" 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 = [];
var currentShopManageGroupGrantRoutes = [];
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 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 || '删除失败'); }
});
};
});
}
function refreshShopGroupSelects(selectedCreateId, selectedEditId) {
var createSel = document.getElementById('shopManageGroupSelect');
var editSel = document.getElementById('editShopManageGroupSelect');
var filterSel = document.getElementById('shopManageFilterGroupId');
var skipCreateSel = document.getElementById('skipPriceAsinGroupSelect');
var skipFilterSel = document.getElementById('skipPriceAsinFilterGroupId');
var chooseSkipShopSel = document.getElementById('chooseSkipPriceAsinShopGroupId');
var queryCreateSel = document.getElementById('queryAsinGroupSelect');
var queryFilterSel = document.getElementById('queryAsinFilterGroupId');
var chooseQueryShopSel = document.getElementById('chooseQueryAsinShopGroupId');
var selectedFilterId = filterSel ? filterSel.value : '';
var selectedSkipCreateId = skipCreateSel ? skipCreateSel.value : '';
var selectedSkipFilterId = skipFilterSel ? skipFilterSel.value : '';
var selectedChooseSkipShopId = chooseSkipShopSel ? chooseSkipShopSel.value : '';
var selectedQueryCreateId = queryCreateSel ? queryCreateSel.value : '';
var selectedQueryFilterId = queryFilterSel ? queryFilterSel.value : '';
var selectedChooseQueryShopId = chooseQueryShopSel ? chooseQueryShopSel.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 (skipCreateSel) skipCreateSel.innerHTML = createOpts.join('');
if (queryCreateSel) queryCreateSel.innerHTML = createOpts.join('');
if (filterSel) filterSel.innerHTML = filterOpts.join('');
if (skipFilterSel) skipFilterSel.innerHTML = filterOpts.join('');
if (chooseSkipShopSel) chooseSkipShopSel.innerHTML = filterOpts.join('');
if (queryFilterSel) queryFilterSel.innerHTML = filterOpts.join('');
if (chooseQueryShopSel) chooseQueryShopSel.innerHTML = filterOpts.join('');
if (selectedCreateId != null) createSel.value = String(selectedCreateId);
if (selectedEditId != null) editSel.value = String(selectedEditId);
if (filterSel && selectedFilterId) filterSel.value = selectedFilterId;
if (skipCreateSel && selectedSkipCreateId) skipCreateSel.value = selectedSkipCreateId;
if (skipFilterSel && selectedSkipFilterId) skipFilterSel.value = selectedSkipFilterId;
if (chooseSkipShopSel && selectedChooseSkipShopId) chooseSkipShopSel.value = selectedChooseSkipShopId;
if (queryCreateSel && selectedQueryCreateId) queryCreateSel.value = selectedQueryCreateId;
if (queryFilterSel && selectedQueryFilterId) queryFilterSel.value = selectedQueryFilterId;
if (chooseQueryShopSel && selectedChooseQueryShopId) chooseQueryShopSel.value = selectedChooseQueryShopId;
}
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 resetShopManageGroupForm() {
document.getElementById('shopManageGroupEditId').value = '';
document.getElementById('shopManageGroupLeaderUserId').value = currentUserId || '';
document.getElementById('shopManageGroupLeaderName').value = currentUserUsername || '';
document.getElementById('shopManageGroupInput').value = '';
document.getElementById('msgShopManageGroup').textContent = '';
document.getElementById('msgShopManageGroup').className = 'msg';
refreshShopManageGroupMemberSelect(currentUserId, []);
}
function setShopManageGroupGrantRoutes(routes) {
currentShopManageGroupGrantRoutes = Array.isArray(routes) ? routes.slice() : [];
}
function updateShopManageGroupButtonsAccess() {
var canManageGroups = !!currentUserId;
['btnManageShopGroups', 'btnManageShopGroupsFromEdit', 'btnManageSkipPriceAsinGroups', 'btnManageQueryAsinGroups'].forEach(function (id) {
var btn = document.getElementById(id);
if (btn) btn.style.display = canManageGroups ? '' : 'none';
});
}
function openShopManageGroupModal() {
resetShopManageGroupForm();
loadUserOptions()
.then(function () { return 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="8" class="empty-tip">暂无分组</td></tr>';
return;
}
tbody.innerHTML = shopManageGroups.map(function (item, index) {
var memberNames = Array.isArray(item.member_usernames) ? item.member_usernames.join('、') : '';
var canEdit = currentUserRole === 'super_admin' || String(item.leader_user_id || '') === String(currentUserId || '');
var actionHtml = canEdit
? ('<button class="btn btn-sm" data-shop-group-edit="' + item.id + '">编辑</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>')
: '<span style="color:#999;">-</span>';
return '<tr><td>' + (index + 1) + '</td><td>' + (item.group_name || '') + '</td><td>' + (item.leader_username || '') + '</td><td>' + (item.member_count || 0) + '</td><td>' + (memberNames || '-') + '</td><td>' + (item.created_at || '') + '</td><td>' + (item.updated_at || '') + '</td><td>' +
actionHtml + '</td></tr>';
}).join('');
document.querySelectorAll('[data-shop-group-edit]').forEach(function (btn) {
btn.onclick = function () {
var item = null;
shopManageGroups.some(function (group) {
if (String(group.id || '') === String(btn.dataset.shopGroupEdit || '')) {
item = group;
return true;
}
return false;
});
if (!item) return;
document.getElementById('shopManageGroupEditId').value = item.id || '';
document.getElementById('shopManageGroupInput').value = item.group_name || '';
setShopManageGroupLeader(item.leader_user_id || '', item.leader_username || '', item.member_user_ids || []);
document.getElementById('msgShopManageGroup').textContent = '';
document.getElementById('msgShopManageGroup').className = 'msg';
};
});
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);
loadSkipPriceAsin(skipPriceAsinPage);
});
});
};
});
}
document.getElementById('btnManageShopGroups').onclick = function () {
setShopManageGroupGrantRoutes(['shop-manage']);
openShopManageGroupModal();
};
document.getElementById('btnManageShopGroupsFromEdit').onclick = function () {
setShopManageGroupGrantRoutes(['shop-manage']);
openShopManageGroupModal();
};
document.getElementById('btnManageSkipPriceAsinGroups').onclick = function () {
setShopManageGroupGrantRoutes(['skip-price-asin']);
openShopManageGroupModal();
};
document.getElementById('btnManageQueryAsinGroups').onclick = function () {
setShopManageGroupGrantRoutes(['query-asin']);
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 = resetShopManageGroupForm;
document.getElementById('btnSaveShopManageGroup').onclick = function () {
var editId = (document.getElementById('shopManageGroupEditId').value || '').trim();
var groupName = (document.getElementById('shopManageGroupInput').value || '').trim();
var memberUserIds = getSelectedShopManageGroupMemberIds();
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,
member_user_ids: memberUserIds,
grant_menu_routes: currentShopManageGroupGrantRoutes
})
})
.then(function (r) { return r.json(); })
.then(function (res) {
if (!res.success) {
msgEl.textContent = res.error || '保存失败';
msgEl.className = 'msg err';
return;
}
resetShopManageGroupForm();
msgEl.textContent = res.msg || '保存成功';
msgEl.className = 'msg ok';
loadShopManageGroups().then(function () {
renderShopManageGroupRows();
loadShopManage(shopManagePage);
loadSkipPriceAsin(skipPriceAsinPage);
});
})
.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');
};
// ========== 版本管理 ==========
// ========== 跳过跟价 ASIN ==========
var skipPriceAsinPage = 1, skipPriceAsinPageSize = 15;
var chooseSkipPriceAsinShopPage = 1, chooseSkipPriceAsinShopPageSize = 10;
var skipPriceCountryColumns = [
{ code: 'DE', field: 'asin_de', minimumPriceField: 'minimum_price_de', label: '德国' },
{ code: 'UK', field: 'asin_uk', minimumPriceField: 'minimum_price_uk', label: '英国' },
{ code: 'FR', field: 'asin_fr', minimumPriceField: 'minimum_price_fr', label: '法国' },
{ code: 'IT', field: 'asin_it', minimumPriceField: 'minimum_price_it', label: '意大利' },
{ code: 'ES', field: 'asin_es', minimumPriceField: 'minimum_price_es', label: '西班牙' }
];
function buildChooseSkipPriceAsinShopQuery(page) {
var query = 'page=' + (page || 1) + '&page_size=' + chooseSkipPriceAsinShopPageSize;
var groupId = (document.getElementById('chooseSkipPriceAsinShopGroupId').value || '').trim();
var shopName = (document.getElementById('chooseSkipPriceAsinShopKeyword').value || '').trim();
if (groupId) query += '&group_id=' + encodeURIComponent(groupId);
if (shopName) query += '&shop_name=' + encodeURIComponent(shopName);
return query;
}
function loadChooseSkipPriceAsinShops(page) {
chooseSkipPriceAsinShopPage = page || 1;
fetch('/api/admin/shop-manages?' + buildChooseSkipPriceAsinShopQuery(chooseSkipPriceAsinShopPage))
.then(function (r) { return r.json(); })
.then(function (res) {
var tbody = document.getElementById('chooseSkipPriceAsinShopListBody');
if (!res.success) {
tbody.innerHTML = '<tr><td colspan="7" class="empty-tip">加载失败: ' + (res.error || '') + '</td></tr>';
return;
}
var items = res.items || [];
if (!items.length) {
tbody.innerHTML = '<tr><td colspan="6" class="empty-tip">暂无店铺</td></tr>';
} else {
tbody.innerHTML = items.map(function (item, index) {
var rowNo = (chooseSkipPriceAsinShopPage - 1) * chooseSkipPriceAsinShopPageSize + 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>' +
'<button class="btn btn-sm" type="button" data-choose-skip-price-asin-shop="' + item.id + '" data-shop-name="' + (item.shop_name || '').replace(/"/g, '&quot;') + '" data-group-id="' + (item.group_id || '') + '">选择</button>' +
'</td></tr>';
}).join('');
}
renderPagination('chooseSkipPriceAsinShopPagination', res.total, res.page, res.page_size, loadChooseSkipPriceAsinShops);
bindChooseSkipPriceAsinShopActions();
})
.catch(function () {
document.getElementById('chooseSkipPriceAsinShopListBody').innerHTML = '<tr><td colspan="7" class="empty-tip">请求失败</td></tr>';
});
}
function bindChooseSkipPriceAsinShopActions() {
document.querySelectorAll('[data-choose-skip-price-asin-shop]').forEach(function (btn) {
btn.onclick = function () {
document.getElementById('skipPriceAsinShopName').value = (btn.dataset.shopName || '').replace(/&quot;/g, '"');
if (!document.getElementById('skipPriceAsinGroupSelect').value && btn.dataset.groupId) {
document.getElementById('skipPriceAsinGroupSelect').value = btn.dataset.groupId;
}
document.getElementById('chooseSkipPriceAsinShopModal').classList.remove('show');
};
});
}
function openChooseSkipPriceAsinShopModal() {
var currentGroupId = (document.getElementById('skipPriceAsinGroupSelect').value || '').trim();
if (currentGroupId) {
document.getElementById('chooseSkipPriceAsinShopGroupId').value = currentGroupId;
}
document.getElementById('chooseSkipPriceAsinShopKeyword').value = (document.getElementById('skipPriceAsinShopName').value || '').trim();
document.getElementById('chooseSkipPriceAsinShopModal').classList.add('show');
loadChooseSkipPriceAsinShops(1);
}
function setupSkipPriceAsinShopPicker() {
var input = document.getElementById('skipPriceAsinShopName');
var button = document.getElementById('btnChooseSkipPriceAsinShop');
if (!input || !button) return;
var inputParent = input.parentNode;
if (inputParent && inputParent.style && inputParent.style.display === 'flex') return;
var legacyWrap = button.parentNode;
var wrapper = document.createElement('div');
wrapper.style.display = 'flex';
wrapper.style.gap = '8px';
wrapper.style.alignItems = 'center';
input.parentNode.insertBefore(wrapper, input);
wrapper.appendChild(input);
wrapper.appendChild(button);
button.style.whiteSpace = 'nowrap';
button.style.marginTop = '0';
if (legacyWrap && legacyWrap !== wrapper) {
legacyWrap.style.display = 'none';
}
}
function getSelectedSkipPriceCountries() {
return Array.from(document.getElementById('skipPriceAsinCountries').selectedOptions).map(function (option) {
return option.value;
});
}
function formatSkipPriceMinimumPrice(value) {
if (value === null || value === undefined || value === '') return '';
var num = Number(value);
if (!isFinite(num)) return String(value);
return num.toFixed(2);
}
function getSkipPriceCountryLabel(countryCode) {
var country = skipPriceCountryColumns.find(function (item) { return item.code === countryCode; });
return country ? country.label : countryCode;
}
function renderSkipPriceAsinInputs() {
var container = document.getElementById('skipPriceAsinInputs');
var selectedCountries = getSelectedSkipPriceCountries();
var existingValues = {};
var existingMinimumPrices = {};
container.querySelectorAll('[data-skip-price-country-input]').forEach(function (input) {
existingValues[input.getAttribute('data-skip-price-country-input')] = input.value;
});
container.querySelectorAll('[data-skip-price-country-minimum-price-input]').forEach(function (input) {
existingMinimumPrices[input.getAttribute('data-skip-price-country-minimum-price-input')] = input.value;
});
if (!selectedCountries.length) {
container.innerHTML = '<div style="color:#999;font-size:13px;">请选择国家后输入 ASIN 和最低价</div>';
return;
}
container.innerHTML = selectedCountries.map(function (countryCode) {
var label = getSkipPriceCountryLabel(countryCode);
var value = existingValues[countryCode] || '';
var minimumPrice = existingMinimumPrices[countryCode] || '';
return '<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">' +
'<span style="min-width:56px;color:#555;">' + label + '</span>' +
'<input type="text" data-skip-price-country-input="' + countryCode + '" value="' + value.replace(/"/g, '&quot;') + '" placeholder="请输入' + label + ' ASIN" style="flex:1;min-width:180px;">' +
'<input type="number" data-skip-price-country-minimum-price-input="' + countryCode + '" value="' + minimumPrice.replace(/"/g, '&quot;') + '" min="0" step="0.01" placeholder="最低价" style="width:140px;">' +
'</div>';
}).join('');
}
function collectSkipPriceAsinMappings(countries) {
var asinMappings = {};
var minimumPriceMappings = {};
for (var i = 0; i < countries.length; i++) {
var countryCode = countries[i];
var input = document.querySelector('[data-skip-price-country-input="' + countryCode + '"]');
var minimumPriceInput = document.querySelector('[data-skip-price-country-minimum-price-input="' + countryCode + '"]');
var asin = input ? (input.value || '').trim().toUpperCase() : '';
if (!asin) {
var label = getSkipPriceCountryLabel(countryCode);
throw new Error(label + ' ASIN 不能为空');
}
var minimumPrice = minimumPriceInput ? (minimumPriceInput.value || '').trim() : '';
if (minimumPrice) {
var minimumPriceNumber = Number(minimumPrice);
if (!isFinite(minimumPriceNumber) || minimumPriceNumber < 0) {
throw new Error(getSkipPriceCountryLabel(countryCode) + ' 最低价格式不正确');
}
minimumPriceMappings[countryCode] = minimumPrice;
}
asinMappings[countryCode] = asin;
}
return {
asinMappings: asinMappings,
minimumPriceMappings: minimumPriceMappings
};
}
function buildSkipPriceAsinQuery(page) {
var query = 'page=' + (page || 1) + '&page_size=' + skipPriceAsinPageSize;
var groupId = (document.getElementById('skipPriceAsinFilterGroupId').value || '').trim();
var shopName = (document.getElementById('skipPriceAsinFilterShopName').value || '').trim();
var asin = (document.getElementById('skipPriceAsinFilterAsin').value || '').trim();
if (groupId) query += '&group_id=' + encodeURIComponent(groupId);
if (shopName) query += '&shop_name=' + encodeURIComponent(shopName);
if (asin) query += '&asin=' + encodeURIComponent(asin);
return query;
}
function renderSkipPriceAsinCell(item, country) {
var asinValue = item[country.field] || '';
var minimumPriceValue = formatSkipPriceMinimumPrice(item[country.minimumPriceField]);
var hasValue = !!asinValue || !!minimumPriceValue;
var infoHtml = hasValue
? ('<div style="display:flex;flex-direction:column;gap:4px;min-width:0;">' +
'<span>' + (asinValue || '<span style="color:#999;">-</span>') + '</span>' +
'<span style="color:#666;font-size:12px;">最低价:' + (minimumPriceValue || '-') + '</span>' +
'</div>')
: '<span style="color:#999;">-</span>';
var deleteHtml = hasValue
? ('<button class="btn btn-sm btn-danger" data-skip-price-asin-delete="' + item.id + '" data-country="' + country.code + '" data-shop-name="' + (item.shop_name || '').replace(/"/g, '&quot;') + '">删除</button>')
: '';
return '<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">' +
infoHtml +
'<button class="btn btn-sm" data-skip-price-asin-edit="' + item.id + '" data-country="' + country.code + '" data-asin="' + asinValue.replace(/"/g, '&quot;') + '" data-minimum-price="' + minimumPriceValue.replace(/"/g, '&quot;') + '" data-shop-name="' + (item.shop_name || '').replace(/"/g, '&quot;') + '">编辑</button>' +
deleteHtml +
'</div>';
}
function openEditSkipPriceAsinModal(itemId, countryCode, shopName, asinValue, minimumPriceValue) {
document.getElementById('editSkipPriceAsinId').value = itemId || '';
document.getElementById('editSkipPriceAsinCountry').value = countryCode || '';
document.getElementById('editSkipPriceAsinShopName').value = shopName || '';
document.getElementById('editSkipPriceAsinCountryLabel').value = getSkipPriceCountryLabel(countryCode || '');
document.getElementById('editSkipPriceAsinValue').value = asinValue || '';
document.getElementById('editSkipPriceMinimumPrice').value = minimumPriceValue || '';
document.getElementById('msgEditSkipPriceAsin').textContent = '';
document.getElementById('msgEditSkipPriceAsin').className = 'msg';
document.getElementById('editSkipPriceAsinModal').classList.add('show');
}
function bindSkipPriceAsinActions() {
document.querySelectorAll('[data-skip-price-asin-edit]').forEach(function (btn) {
btn.onclick = function () {
var shopName = (btn.dataset.shopName || '').replace(/&quot;/g, '"');
var countryCode = btn.dataset.country || '';
var currentAsin = (btn.dataset.asin || '').replace(/&quot;/g, '"');
var currentMinimumPrice = (btn.dataset.minimumPrice || '').replace(/&quot;/g, '"');
openEditSkipPriceAsinModal(
btn.dataset.skipPriceAsinEdit,
countryCode,
shopName,
currentAsin,
currentMinimumPrice
);
};
});
document.querySelectorAll('[data-skip-price-asin-delete]').forEach(function (btn) {
btn.onclick = function () {
var shopName = (btn.dataset.shopName || '').replace(/&quot;/g, '"');
var countryCode = btn.dataset.country || '';
if (!confirm('确定删除店铺“' + shopName + '”在 ' + countryCode + ' 的 ASIN 吗?')) return;
fetch('/api/admin/skip-price-asin/' + btn.dataset.skipPriceAsinDelete + '/country/' + countryCode, {
method: 'DELETE'
})
.then(function (r) { return r.json(); })
.then(function (res) {
if (res.success) loadSkipPriceAsin(skipPriceAsinPage);
else alert(res.error || '删除失败');
});
};
});
}
function loadSkipPriceAsin(page) {
skipPriceAsinPage = page || 1;
fetch('/api/admin/skip-price-asins?' + buildSkipPriceAsinQuery(skipPriceAsinPage))
.then(function (r) { return r.json(); })
.then(function (res) {
var tbody = document.getElementById('skipPriceAsinListBody');
if (!res.success) {
tbody.innerHTML = '<tr><td colspan="8" class="empty-tip">加载失败: ' + (res.error || '') + '</td></tr>';
return;
}
var items = res.items || [];
if (items.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" class="empty-tip">暂无数据</td></tr>';
} else {
tbody.innerHTML = items.map(function (item, index) {
var rowNo = (skipPriceAsinPage - 1) * skipPriceAsinPageSize + index + 1;
return '<tr><td>' + rowNo + '</td><td>' + (item.group_name || '') + '</td><td>' + (item.shop_name || '') + '</td>' +
skipPriceCountryColumns.map(function (country) {
return '<td>' + renderSkipPriceAsinCell(item, country) + '</td>';
}).join('') +
'</tr>';
}).join('');
}
renderPagination('skipPriceAsinPagination', res.total, res.page, res.page_size, loadSkipPriceAsin);
bindSkipPriceAsinActions();
})
.catch(function () {
document.getElementById('skipPriceAsinListBody').innerHTML = '<tr><td colspan="8" class="empty-tip">请求失败</td></tr>';
});
}
document.getElementById('btnChooseSkipPriceAsinShop').onclick = openChooseSkipPriceAsinShopModal;
document.getElementById('btnSearchChooseSkipPriceAsinShop').onclick = function () {
loadChooseSkipPriceAsinShops(1);
};
document.getElementById('chooseSkipPriceAsinShopGroupId').onchange = function () {
loadChooseSkipPriceAsinShops(1);
};
document.getElementById('chooseSkipPriceAsinShopKeyword').addEventListener('keydown', function (e) {
if (e.key === 'Enter') {
e.preventDefault();
loadChooseSkipPriceAsinShops(1);
}
});
document.getElementById('btnCloseChooseSkipPriceAsinShopModal').onclick = function () {
document.getElementById('chooseSkipPriceAsinShopModal').classList.remove('show');
};
document.getElementById('btnCloseEditSkipPriceAsin').onclick = function () {
document.getElementById('editSkipPriceAsinModal').classList.remove('show');
};
document.getElementById('btnSaveSkipPriceAsin').onclick = function () {
var itemId = (document.getElementById('editSkipPriceAsinId').value || '').trim();
var countryCode = (document.getElementById('editSkipPriceAsinCountry').value || '').trim();
var asin = (document.getElementById('editSkipPriceAsinValue').value || '').trim().toUpperCase();
var minimumPrice = (document.getElementById('editSkipPriceMinimumPrice').value || '').trim();
var msgEl = document.getElementById('msgEditSkipPriceAsin');
msgEl.textContent = '';
msgEl.className = 'msg';
if (!itemId || !countryCode) {
msgEl.textContent = '缺少编辑记录';
msgEl.className = 'msg err';
return;
}
if (!asin) {
msgEl.textContent = 'ASIN 不能为空';
msgEl.className = 'msg err';
return;
}
if (minimumPrice) {
var minimumPriceNumber = Number(minimumPrice);
if (!isFinite(minimumPriceNumber) || minimumPriceNumber < 0) {
msgEl.textContent = '最低价格式不正确';
msgEl.className = 'msg err';
return;
}
}
fetch('/api/admin/skip-price-asin/' + itemId + '/country/' + countryCode, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
asin: asin,
minimum_price: minimumPrice || null
})
})
.then(function (r) { return r.json(); })
.then(function (res) {
if (!res.success) {
msgEl.textContent = res.error || '保存失败';
msgEl.className = 'msg err';
return;
}
document.getElementById('editSkipPriceAsinModal').classList.remove('show');
loadSkipPriceAsin(skipPriceAsinPage);
})
.catch(function () {
msgEl.textContent = '请求失败';
msgEl.className = 'msg err';
});
};
document.getElementById('skipPriceAsinCountries').addEventListener('change', renderSkipPriceAsinInputs);
document.getElementById('btnSearchSkipPriceAsin').onclick = function () {
loadSkipPriceAsin(1);
};
document.getElementById('skipPriceAsinFilterShopName').addEventListener('keydown', function (e) {
if (e.key === 'Enter') {
e.preventDefault();
loadSkipPriceAsin(1);
}
});
document.getElementById('skipPriceAsinFilterAsin').addEventListener('keydown', function (e) {
if (e.key === 'Enter') {
e.preventDefault();
loadSkipPriceAsin(1);
}
});
document.getElementById('btnCreateSkipPriceAsin').onclick = function () {
var groupId = (document.getElementById('skipPriceAsinGroupSelect').value || '').trim();
var shopName = (document.getElementById('skipPriceAsinShopName').value || '').trim();
var countries = getSelectedSkipPriceCountries();
var msgEl = document.getElementById('msgSkipPriceAsin');
msgEl.textContent = '';
msgEl.className = 'msg';
if (!groupId || !shopName || !countries.length) {
msgEl.textContent = '请完整填写分组、店铺名、国家和 ASIN';
msgEl.className = 'msg err';
return;
}
var asinMappings = {};
var minimumPriceMappings = {};
try {
var mappings = collectSkipPriceAsinMappings(countries);
asinMappings = mappings.asinMappings || {};
minimumPriceMappings = mappings.minimumPriceMappings || {};
} catch (err) {
msgEl.textContent = err.message || '请输入 ASIN';
msgEl.className = 'msg err';
return;
}
var fallbackAsin = '';
Object.keys(asinMappings).some(function (countryCode) {
fallbackAsin = asinMappings[countryCode] || '';
return !!fallbackAsin;
});
fetch('/api/admin/skip-price-asin', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
group_id: Number(groupId),
shop_name: shopName,
countries: countries,
asin: fallbackAsin,
asin_mappings: asinMappings,
minimum_price_mappings: minimumPriceMappings
})
})
.then(function (r) { return r.json(); })
.then(function (res) {
if (!res.success) {
msgEl.textContent = res.error || '保存失败';
msgEl.className = 'msg err';
return;
}
document.getElementById('skipPriceAsinGroupSelect').value = '';
document.getElementById('skipPriceAsinShopName').value = '';
Array.from(document.getElementById('skipPriceAsinCountries').options).forEach(function (option) {
option.selected = false;
});
renderSkipPriceAsinInputs();
msgEl.textContent = res.msg || '保存成功';
msgEl.className = 'msg ok';
loadSkipPriceAsin(1);
})
.catch(function () {
msgEl.textContent = '请求失败';
msgEl.className = 'msg err';
});
};
setupSkipPriceAsinShopPicker();
renderSkipPriceAsinInputs();
// ========== 查询 ASIN ==========
var queryAsinPage = 1, queryAsinPageSize = 15;
var chooseQueryAsinShopPage = 1, chooseQueryAsinShopPageSize = 10;
var queryAsinCountryColumns = [
{ code: 'DE', field: 'asin_de', label: '德国' },
{ code: 'UK', field: 'asin_uk', label: '英国' },
{ code: 'FR', field: 'asin_fr', label: '法国' },
{ code: 'IT', field: 'asin_it', label: '意大利' },
{ code: 'ES', field: 'asin_es', label: '西班牙' }
];
function getQueryAsinCountryLabel(countryCode) {
var country = queryAsinCountryColumns.find(function (item) { return item.code === countryCode; });
return country ? country.label : countryCode;
}
function getSelectedQueryAsinCountries() {
return Array.from(document.getElementById('queryAsinCountries').selectedOptions).map(function (option) {
return option.value;
});
}
function renderQueryAsinInputs() {
var container = document.getElementById('queryAsinInputs');
var selectedCountries = getSelectedQueryAsinCountries();
var existingValues = {};
container.querySelectorAll('[data-query-asin-country-input]').forEach(function (input) {
existingValues[input.getAttribute('data-query-asin-country-input')] = input.value;
});
if (!selectedCountries.length) {
container.innerHTML = '<div style="color:#999;font-size:13px;">请选择国家后输入 ASIN</div>';
return;
}
container.innerHTML = selectedCountries.map(function (countryCode) {
var label = getQueryAsinCountryLabel(countryCode);
var value = existingValues[countryCode] || '';
return '<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">' +
'<span style="min-width:56px;color:#555;">' + label + '</span>' +
'<input type="text" data-query-asin-country-input="' + countryCode + '" value="' + value.replace(/"/g, '&quot;') + '" placeholder="请输入' + label + ' ASIN" style="flex:1;min-width:180px;">' +
'</div>';
}).join('');
}
function collectQueryAsinMappings(countries) {
var asinMappings = {};
for (var i = 0; i < countries.length; i++) {
var countryCode = countries[i];
var input = document.querySelector('[data-query-asin-country-input="' + countryCode + '"]');
var asin = input ? (input.value || '').trim().toUpperCase() : '';
if (!asin) {
throw new Error(getQueryAsinCountryLabel(countryCode) + ' ASIN 不能为空');
}
asinMappings[countryCode] = asin;
}
return asinMappings;
}
function buildChooseQueryAsinShopQuery(page) {
var query = 'page=' + (page || 1) + '&page_size=' + chooseQueryAsinShopPageSize;
var groupId = (document.getElementById('chooseQueryAsinShopGroupId').value || '').trim();
var shopName = (document.getElementById('chooseQueryAsinShopKeyword').value || '').trim();
if (groupId) query += '&group_id=' + encodeURIComponent(groupId);
if (shopName) query += '&shop_name=' + encodeURIComponent(shopName);
return query;
}
function loadChooseQueryAsinShops(page) {
chooseQueryAsinShopPage = page || 1;
fetch('/api/admin/shop-manages?' + buildChooseQueryAsinShopQuery(chooseQueryAsinShopPage))
.then(function (r) { return r.json(); })
.then(function (res) {
var tbody = document.getElementById('chooseQueryAsinShopListBody');
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 = (chooseQueryAsinShopPage - 1) * chooseQueryAsinShopPageSize + index + 1;
return '<tr><td>' + rowNo + '</td><td>' + (item.group_name || '') + '</td><td>' + (item.shop_name || '') + '</td><td>' + (item.mall_name || '') + '</td><td>' + (item.username || '') + '</td><td>' +
'<button class="btn btn-sm" type="button" data-choose-query-asin-shop="' + item.id + '" data-shop-name="' + (item.shop_name || '').replace(/"/g, '&quot;') + '" data-group-id="' + (item.group_id || '') + '">选择</button>' +
'</td></tr>';
}).join('');
}
renderPagination('chooseQueryAsinShopPagination', res.total, res.page, res.page_size, loadChooseQueryAsinShops);
bindChooseQueryAsinShopActions();
})
.catch(function () {
document.getElementById('chooseQueryAsinShopListBody').innerHTML = '<tr><td colspan="6" class="empty-tip">请求失败</td></tr>';
});
}
function bindChooseQueryAsinShopActions() {
document.querySelectorAll('[data-choose-query-asin-shop]').forEach(function (btn) {
btn.onclick = function () {
document.getElementById('queryAsinShopName').value = (btn.dataset.shopName || '').replace(/&quot;/g, '"');
if (btn.dataset.groupId) {
document.getElementById('queryAsinGroupSelect').value = btn.dataset.groupId;
}
document.getElementById('chooseQueryAsinShopModal').classList.remove('show');
};
});
}
function openChooseQueryAsinShopModal() {
var currentGroupId = (document.getElementById('queryAsinGroupSelect').value || '').trim();
if (currentGroupId) {
document.getElementById('chooseQueryAsinShopGroupId').value = currentGroupId;
}
document.getElementById('chooseQueryAsinShopKeyword').value = (document.getElementById('queryAsinShopName').value || '').trim();
document.getElementById('chooseQueryAsinShopModal').classList.add('show');
loadChooseQueryAsinShops(1);
}
function buildQueryAsinQuery(page) {
var query = 'page=' + (page || 1) + '&page_size=' + queryAsinPageSize;
var groupId = (document.getElementById('queryAsinFilterGroupId').value || '').trim();
var shopName = (document.getElementById('queryAsinFilterShopName').value || '').trim();
var asin = (document.getElementById('queryAsinFilterAsin').value || '').trim();
if (groupId) query += '&group_id=' + encodeURIComponent(groupId);
if (shopName) query += '&shop_name=' + encodeURIComponent(shopName);
if (asin) query += '&asin=' + encodeURIComponent(asin);
return query;
}
function renderQueryAsinCell(item, country) {
var asinValue = item[country.field] || '';
var infoHtml = asinValue ? '<span>' + asinValue + '</span>' : '<span style="color:#999;">-</span>';
var deleteHtml = asinValue
? ('<button class="btn btn-sm btn-danger" data-query-asin-delete="' + item.id + '" data-country="' + country.code + '" data-shop-name="' + (item.shop_name || '').replace(/"/g, '&quot;') + '">删除</button>')
: '';
return '<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">' +
infoHtml +
'<button class="btn btn-sm" data-query-asin-edit="' + item.id + '" data-country="' + country.code + '" data-asin="' + asinValue.replace(/"/g, '&quot;') + '" data-shop-name="' + (item.shop_name || '').replace(/"/g, '&quot;') + '">编辑</button>' +
deleteHtml +
'</div>';
}
function openEditQueryAsinModal(itemId, countryCode, shopName, asinValue) {
document.getElementById('editQueryAsinId').value = itemId || '';
document.getElementById('editQueryAsinCountry').value = countryCode || '';
document.getElementById('editQueryAsinShopName').value = shopName || '';
document.getElementById('editQueryAsinCountryLabel').value = getQueryAsinCountryLabel(countryCode || '');
document.getElementById('editQueryAsinValue').value = asinValue || '';
document.getElementById('msgEditQueryAsin').textContent = '';
document.getElementById('msgEditQueryAsin').className = 'msg';
document.getElementById('editQueryAsinModal').classList.add('show');
}
function bindQueryAsinActions() {
document.querySelectorAll('[data-query-asin-edit]').forEach(function (btn) {
btn.onclick = function () {
openEditQueryAsinModal(
btn.dataset.queryAsinEdit,
btn.dataset.country || '',
(btn.dataset.shopName || '').replace(/&quot;/g, '"'),
(btn.dataset.asin || '').replace(/&quot;/g, '"')
);
};
});
document.querySelectorAll('[data-query-asin-delete]').forEach(function (btn) {
btn.onclick = function () {
var shopName = (btn.dataset.shopName || '').replace(/&quot;/g, '"');
var countryCode = btn.dataset.country || '';
if (!confirm('确定删除店铺“' + shopName + '”在 ' + countryCode + ' 的 ASIN 吗?')) return;
fetch('/api/admin/query-asin/' + btn.dataset.queryAsinDelete + '/country/' + countryCode, {
method: 'DELETE'
})
.then(function (r) { return r.json(); })
.then(function (res) {
if (res.success) loadQueryAsin(queryAsinPage);
else alert(res.error || '删除失败');
});
};
});
}
function loadQueryAsin(page) {
queryAsinPage = page || 1;
fetch('/api/admin/query-asins?' + buildQueryAsinQuery(queryAsinPage))
.then(function (r) { return r.json(); })
.then(function (res) {
var tbody = document.getElementById('queryAsinListBody');
if (!res.success) {
tbody.innerHTML = '<tr><td colspan="8" class="empty-tip">加载失败: ' + (res.error || '') + '</td></tr>';
return;
}
var items = res.items || [];
if (items.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" class="empty-tip">暂无数据</td></tr>';
} else {
tbody.innerHTML = items.map(function (item, index) {
var rowNo = (queryAsinPage - 1) * queryAsinPageSize + index + 1;
return '<tr><td>' + rowNo + '</td><td>' + (item.group_name || '') + '</td><td>' + (item.shop_name || '') + '</td>' +
queryAsinCountryColumns.map(function (country) {
return '<td>' + renderQueryAsinCell(item, country) + '</td>';
}).join('') +
'</tr>';
}).join('');
}
renderPagination('queryAsinPagination', res.total, res.page, res.page_size, loadQueryAsin);
bindQueryAsinActions();
})
.catch(function () {
document.getElementById('queryAsinListBody').innerHTML = '<tr><td colspan="8" class="empty-tip">请求失败</td></tr>';
});
}
var queryAsinImportPollTimer = null;
var queryAsinDeleteImportPollTimer = null;
function stopQueryAsinImportProgress() {
if (queryAsinImportPollTimer) {
clearInterval(queryAsinImportPollTimer);
queryAsinImportPollTimer = null;
}
}
function stopQueryAsinDeleteImportProgress() {
if (queryAsinDeleteImportPollTimer) {
clearInterval(queryAsinDeleteImportPollTimer);
queryAsinDeleteImportPollTimer = null;
}
}
function setQueryAsinImportProgress(percent, text) {
var wrap = document.getElementById('queryAsinImportProgressWrap');
var fill = document.getElementById('queryAsinImportProgressFill');
var textEl = document.getElementById('queryAsinImportProgressText');
wrap.style.display = 'block';
fill.style.width = Math.max(0, Math.min(100, percent || 0)) + '%';
textEl.textContent = text || '';
}
function setQueryAsinDeleteImportProgress(percent, text) {
var wrap = document.getElementById('queryAsinDeleteImportProgressWrap');
var fill = document.getElementById('queryAsinDeleteImportProgressFill');
var textEl = document.getElementById('queryAsinDeleteImportProgressText');
wrap.style.display = 'block';
fill.style.width = Math.max(0, Math.min(100, percent || 0)) + '%';
textEl.textContent = text || '';
}
function pollQueryAsinImport(importId) {
stopQueryAsinImportProgress();
function tick() {
fetch('/api/admin/query-asins/import/' + encodeURIComponent(importId))
.then(function (r) { return r.json(); })
.then(function (res) {
if (!res.success) {
stopQueryAsinImportProgress();
document.getElementById('msgQueryAsinImport').textContent = res.error || '查询导入进度失败';
document.getElementById('msgQueryAsinImport').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;
setQueryAsinImportProgress(percent, '处理中:已处理 ' + processedRows + ' / ' + totalRows + ',添加 ' + (progress.inserted_count || 0) + ',跳过 ' + (progress.skipped_count || 0));
if (progress.status === 'success') {
stopQueryAsinImportProgress();
document.getElementById('msgQueryAsinImport').textContent = '导入成功:总行数 ' + totalRows + 'ASIN 数量 ' + (progress.asin_count || 0) + ',添加 ' + (progress.inserted_count || 0) + ',跳过 ' + (progress.skipped_count || 0);
document.getElementById('msgQueryAsinImport').className = 'msg ok';
loadQueryAsin(1);
} else if (progress.status === 'failed') {
stopQueryAsinImportProgress();
document.getElementById('msgQueryAsinImport').textContent = progress.error_message || '导入失败';
document.getElementById('msgQueryAsinImport').className = 'msg err';
}
})
.catch(function () {
stopQueryAsinImportProgress();
document.getElementById('msgQueryAsinImport').textContent = '查询导入进度失败';
document.getElementById('msgQueryAsinImport').className = 'msg err';
});
}
tick();
queryAsinImportPollTimer = setInterval(tick, 1000);
}
function pollQueryAsinDeleteImport(importId) {
stopQueryAsinDeleteImportProgress();
function tick() {
fetch('/api/admin/query-asins/delete-import/' + encodeURIComponent(importId))
.then(function (r) { return r.json(); })
.then(function (res) {
if (!res.success) {
stopQueryAsinDeleteImportProgress();
document.getElementById('msgQueryAsinDeleteImport').textContent = res.error || '查询删除进度失败';
document.getElementById('msgQueryAsinDeleteImport').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;
setQueryAsinDeleteImportProgress(percent, '处理中:已处理 ' + processedRows + ' / ' + totalRows + ',删除 ' + (progress.deleted_count || 0) + ',跳过 ' + (progress.skipped_count || 0));
if (progress.status === 'success') {
stopQueryAsinDeleteImportProgress();
document.getElementById('msgQueryAsinDeleteImport').textContent = '删除成功:总行数 ' + totalRows + 'ASIN 数量 ' + (progress.asin_count || 0) + ',删除 ' + (progress.deleted_count || 0) + ',跳过 ' + (progress.skipped_count || 0);
document.getElementById('msgQueryAsinDeleteImport').className = 'msg ok';
loadQueryAsin(1);
} else if (progress.status === 'failed') {
stopQueryAsinDeleteImportProgress();
document.getElementById('msgQueryAsinDeleteImport').textContent = progress.error_message || '删除失败';
document.getElementById('msgQueryAsinDeleteImport').className = 'msg err';
}
})
.catch(function () {
stopQueryAsinDeleteImportProgress();
document.getElementById('msgQueryAsinDeleteImport').textContent = '查询删除进度失败';
document.getElementById('msgQueryAsinDeleteImport').className = 'msg err';
});
}
tick();
queryAsinDeleteImportPollTimer = setInterval(tick, 1000);
}
function uploadQueryAsinImport(deleteMode) {
var fileInput = document.getElementById(deleteMode ? 'queryAsinDeleteImportFile' : 'queryAsinImportFile');
var msgEl = document.getElementById(deleteMode ? 'msgQueryAsinDeleteImport' : 'msgQueryAsinImport');
msgEl.textContent = '';
msgEl.className = 'msg';
if (deleteMode) {
stopQueryAsinDeleteImportProgress();
document.getElementById('queryAsinDeleteImportProgressWrap').style.display = 'none';
} else {
stopQueryAsinImportProgress();
document.getElementById('queryAsinImportProgressWrap').style.display = 'none';
}
if (!fileInput.files || fileInput.files.length === 0) {
msgEl.textContent = '请选择 Excel 文件';
msgEl.className = 'msg err';
return;
}
var file = fileInput.files[0];
var lowerName = (file.name || '').toLowerCase();
if (!(lowerName.endsWith('.xlsx') || lowerName.endsWith('.xls'))) {
msgEl.textContent = '仅支持 .xlsx 或 .xls 文件';
msgEl.className = 'msg err';
return;
}
if (deleteMode && !confirm('确定按 Excel 中的分组、店铺名和国家 ASIN 批量删除吗?')) {
return;
}
var formData = new FormData();
formData.append('file', file);
var groupId = (document.getElementById('queryAsinGroupSelect').value || '').trim();
if (groupId) formData.append('group_id', groupId);
var xhr = new XMLHttpRequest();
xhr.open('POST', deleteMode ? '/api/admin/query-asins/delete-import' : '/api/admin/query-asins/import', true);
xhr.upload.onprogress = function (event) {
if (event.lengthComputable) {
var percent = Math.round(event.loaded * 100 / event.total);
if (deleteMode) setQueryAsinDeleteImportProgress(percent, '上传中:' + percent + '%');
else setQueryAsinImportProgress(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 || (deleteMode ? '删除失败' : '导入失败');
msgEl.className = 'msg err';
return;
}
if (deleteMode) {
setQueryAsinDeleteImportProgress(100, '上传完成,后端处理中...');
pollQueryAsinDeleteImport(res.import_id);
} else {
setQueryAsinImportProgress(100, '上传完成,后端处理中...');
pollQueryAsinImport(res.import_id);
}
fileInput.value = '';
};
xhr.onerror = function () {
msgEl.textContent = '请求失败';
msgEl.className = 'msg err';
};
xhr.send(formData);
}
document.getElementById('btnChooseQueryAsinShop').onclick = openChooseQueryAsinShopModal;
document.getElementById('btnSearchChooseQueryAsinShop').onclick = function () {
loadChooseQueryAsinShops(1);
};
document.getElementById('chooseQueryAsinShopGroupId').onchange = function () {
loadChooseQueryAsinShops(1);
};
document.getElementById('chooseQueryAsinShopKeyword').addEventListener('keydown', function (e) {
if (e.key === 'Enter') {
e.preventDefault();
loadChooseQueryAsinShops(1);
}
});
document.getElementById('btnCloseChooseQueryAsinShopModal').onclick = function () {
document.getElementById('chooseQueryAsinShopModal').classList.remove('show');
};
document.getElementById('btnCloseEditQueryAsin').onclick = function () {
document.getElementById('editQueryAsinModal').classList.remove('show');
};
document.getElementById('btnSaveQueryAsin').onclick = function () {
var itemId = (document.getElementById('editQueryAsinId').value || '').trim();
var countryCode = (document.getElementById('editQueryAsinCountry').value || '').trim();
var asin = (document.getElementById('editQueryAsinValue').value || '').trim().toUpperCase();
var msgEl = document.getElementById('msgEditQueryAsin');
msgEl.textContent = '';
msgEl.className = 'msg';
if (!itemId || !countryCode) {
msgEl.textContent = '缺少编辑记录';
msgEl.className = 'msg err';
return;
}
if (!asin) {
msgEl.textContent = 'ASIN 不能为空';
msgEl.className = 'msg err';
return;
}
fetch('/api/admin/query-asin/' + itemId + '/country/' + countryCode, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ asin: asin })
})
.then(function (r) { return r.json(); })
.then(function (res) {
if (!res.success) {
msgEl.textContent = res.error || '保存失败';
msgEl.className = 'msg err';
return;
}
document.getElementById('editQueryAsinModal').classList.remove('show');
loadQueryAsin(queryAsinPage);
})
.catch(function () {
msgEl.textContent = '请求失败';
msgEl.className = 'msg err';
});
};
document.getElementById('queryAsinCountries').addEventListener('change', renderQueryAsinInputs);
document.getElementById('btnSearchQueryAsin').onclick = function () {
loadQueryAsin(1);
};
document.getElementById('queryAsinFilterShopName').addEventListener('keydown', function (e) {
if (e.key === 'Enter') {
e.preventDefault();
loadQueryAsin(1);
}
});
document.getElementById('queryAsinFilterAsin').addEventListener('keydown', function (e) {
if (e.key === 'Enter') {
e.preventDefault();
loadQueryAsin(1);
}
});
document.getElementById('btnCreateQueryAsin').onclick = function () {
var groupId = (document.getElementById('queryAsinGroupSelect').value || '').trim();
var shopName = (document.getElementById('queryAsinShopName').value || '').trim();
var countries = getSelectedQueryAsinCountries();
var msgEl = document.getElementById('msgQueryAsin');
msgEl.textContent = '';
msgEl.className = 'msg';
if (!groupId || !shopName || !countries.length) {
msgEl.textContent = '请完整填写分组、店铺名、国家和 ASIN';
msgEl.className = 'msg err';
return;
}
var asinMappings = {};
try {
asinMappings = collectQueryAsinMappings(countries);
} catch (err) {
msgEl.textContent = err.message || '请输入 ASIN';
msgEl.className = 'msg err';
return;
}
var fallbackAsin = '';
Object.keys(asinMappings).some(function (countryCode) {
fallbackAsin = asinMappings[countryCode] || '';
return !!fallbackAsin;
});
fetch('/api/admin/query-asin', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
group_id: Number(groupId),
shop_name: shopName,
countries: countries,
asin: fallbackAsin,
asin_mappings: asinMappings
})
})
.then(function (r) { return r.json(); })
.then(function (res) {
if (!res.success) {
msgEl.textContent = res.error || '保存失败';
msgEl.className = 'msg err';
return;
}
document.getElementById('queryAsinGroupSelect').value = '';
document.getElementById('queryAsinShopName').value = '';
Array.from(document.getElementById('queryAsinCountries').options).forEach(function (option) {
option.selected = false;
});
renderQueryAsinInputs();
msgEl.textContent = res.msg || '保存成功';
msgEl.className = 'msg ok';
loadQueryAsin(1);
})
.catch(function () {
msgEl.textContent = '请求失败';
msgEl.className = 'msg err';
});
};
document.getElementById('btnImportQueryAsin').onclick = function () {
uploadQueryAsinImport(false);
};
document.getElementById('btnDeleteImportQueryAsin').onclick = function () {
uploadQueryAsinImport(true);
};
renderQueryAsinInputs();
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(); loadAdminMenus(getActiveAdminTabName()); }
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();
loadAdminMenus(getActiveAdminTabName());
} 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();
loadAdminMenus(getActiveAdminTabName());
} else {
msgEl.textContent = res.error || '保存失败';
msgEl.classList.add('err');
}
});
};
document.getElementById('btnCloseEditColumn').onclick = function () {
document.getElementById('editColumnModal').classList.remove('show');
};
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="7" class="empty-tip">加载失败: ' + (res.error || '') + '</td></tr>';
return;
}
allColumnsList = res.items || [];
if (allColumnsList.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" class="empty-tip">暂无菜单,请先在上方新增</td></tr>';
} else {
tbody.innerHTML = allColumnsList.map(function (c, index) {
var moveButtons =
'<button class="btn btn-sm btn-secondary" data-column-move-up="' + c.id + '"' + (index === 0 ? ' disabled' : '') + '>上移</button> ' +
'<button class="btn btn-sm btn-secondary" data-column-move-down="' + c.id + '"' + (index === allColumnsList.length - 1 ? ' disabled' : '') + '>下移</button> ';
return '<tr><td>' + c.id + '</td><td>' + (c.name || '') + '</td><td>' + (c.column_key || '') + '</td><td>' + ((c.menu_type || '') === 'admin' ? '后台(admin)' : '软件(app)') + '</td><td>' + (c.sort_order != null ? c.sort_order : 0) + '</td><td>' + (c.route_path || '') + '</td><td>' + (c.created_at || '') + '</td><td>' +
moveButtons +
'<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;') + '" data-route="' + (c.route_path || '').replace(/"/g, '&quot;') + '" data-menu-type="' + (c.menu_type || 'app').replace(/"/g, '&quot;') + '" data-sort-order="' + (c.sort_order != null ? String(c.sort_order) : '0').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="8" class="empty-tip">请求失败</td></tr>';
});
}
function bindColumnActions() {
document.querySelectorAll('[data-column-move-up]').forEach(function (btn) {
btn.onclick = function () {
if (btn.disabled) return;
moveColumnItem(btn.dataset.columnMoveUp, -1, btn);
};
});
document.querySelectorAll('[data-column-move-down]').forEach(function (btn) {
btn.onclick = function () {
if (btn.disabled) return;
moveColumnItem(btn.dataset.columnMoveDown, 1, btn);
};
});
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('editColumnRoutePath').value = (btn.dataset.route || '').replace(/&quot;/g, '"');
document.getElementById('editColumnMenuType').value = ((btn.dataset.menuType || 'app').replace(/&quot;/g, '"') || 'app');
document.getElementById('editColumnSortOrder').value = ((btn.dataset.sortOrder || '0').replace(/&quot;/g, '"') || '0');
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(); loadAdminMenus(getActiveAdminTabName()); }
else { alert(res.error || '删除失败'); }
});
};
});
}
function moveColumnItem(columnId, delta, triggerBtn) {
var currentIndex = -1;
for (var i = 0; i < allColumnsList.length; i++) {
if (String(allColumnsList[i].id) === String(columnId)) {
currentIndex = i;
break;
}
}
if (currentIndex < 0) return;
var targetIndex = currentIndex + delta;
if (targetIndex < 0 || targetIndex >= allColumnsList.length) return;
var currentItem = allColumnsList[currentIndex];
var targetItem = allColumnsList[targetIndex];
if (!currentItem || !targetItem) return;
if (triggerBtn) triggerBtn.disabled = true;
fetch('/api/admin/column/reorder', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
column_id: currentItem.id,
target_id: targetItem.id
})
}).then(function (r) { return r.json(); })
.then(function (res) {
if (!res || !res.success) {
alert((res && res.error) || '排序保存失败');
return;
}
loadColumns();
loadColumnsForPermission();
loadAdminMenus(getActiveAdminTabName());
}).catch(function () {
alert('排序保存失败');
}).finally(function () {
if (triggerBtn) triggerBtn.disabled = false;
});
}
document.getElementById('btnAddColumn').onclick = function () {
var name = (document.getElementById('columnName').value || '').trim();
var key = (document.getElementById('columnKey').value || '').trim();
var routePath = (document.getElementById('columnRoutePath').value || '').trim();
var sortOrderValue = (document.getElementById('columnSortOrder').value || '').trim();
var menuType = (document.getElementById('columnMenuType').value || 'admin').trim() || 'admin';
var sortOrder = sortOrderValue === '' ? null : parseInt(sortOrderValue, 10);
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;
}
if (!routePath) {
msgEl.textContent = '请填写菜单路由'; msgEl.classList.add('err'); return;
}
if (sortOrderValue !== '' && isNaN(sortOrder)) {
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, route_path: routePath, menu_type: menuType, sort_order: sortOrder })
})
.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 = '';
document.getElementById('columnRoutePath').value = '';
document.getElementById('columnSortOrder').value = '';
document.getElementById('columnMenuType').value = 'admin';
loadColumns();
loadColumnsForPermission();
loadAdminMenus(getActiveAdminTabName());
} 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 routePath = (document.getElementById('editColumnRoutePath').value || '').trim();
var sortOrderValue = (document.getElementById('editColumnSortOrder').value || '').trim();
var menuType = (document.getElementById('editColumnMenuType').value || 'admin').trim() || 'admin';
var sortOrder = sortOrderValue === '' ? null : parseInt(sortOrderValue, 10);
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;
}
if (!routePath) {
msgEl.textContent = '请填写菜单路由'; msgEl.classList.add('err'); return;
}
if (sortOrderValue !== '' && isNaN(sortOrder)) {
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, route_path: routePath, menu_type: menuType, sort_order: sortOrder })
})
.then(function (r) { return r.json(); })
.then(function (res) {
if (res.success) {
document.getElementById('editColumnModal').classList.remove('show');
loadColumns();
loadColumnsForPermission();
loadAdminMenus(getActiveAdminTabName());
} else {
msgEl.textContent = res.error || '保存失败';
msgEl.classList.add('err');
}
});
};
// ========== 分页 ==========
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)); };
});
}
// 初始化
function loadAdminCurrentUser() {
fetch('/api/admin/current-user')
.then(function (r) {
if (r.status === 404) {
return fetch('/api/auth/check')
.then(function (fallbackResp) { return fallbackResp.json(); })
.then(function (fallbackRes) {
if (fallbackRes && fallbackRes.logged_in) {
return {
success: true,
item: {
username: '当前用户',
role: ''
}
};
}
return { success: false };
});
}
return r.json();
})
.then(function (res) {
var nameEl = document.getElementById('adminCurrentUsername');
var roleEl = document.getElementById('adminCurrentUserRole');
if (!res.success) {
nameEl.textContent = '未登录';
roleEl.style.display = 'none';
return;
}
var item = res.item || {};
currentUserId = item.id || currentUserId;
currentUserRole = item.role || currentUserRole;
currentUserUsername = item.username || currentUserUsername;
nameEl.textContent = item.username || '管理员';
if (item.role) {
roleEl.textContent = item.role === 'super_admin'
? '超级管理员'
: (item.role === 'admin' ? '管理员' : '普通账号');
roleEl.style.display = 'inline-block';
} else {
roleEl.style.display = 'none';
}
updateShopManageGroupButtonsAccess();
})
.catch(function () {
fetch('/api/auth/check')
.then(function (r) { return r.json(); })
.then(function (res) {
document.getElementById('adminCurrentUsername').textContent = res && res.logged_in ? '当前用户' : '未登录';
document.getElementById('adminCurrentUserRole').style.display = 'none';
updateShopManageGroupButtonsAccess();
})
.catch(function () {
document.getElementById('adminCurrentUsername').textContent = '当前用户';
document.getElementById('adminCurrentUserRole').style.display = 'none';
updateShopManageGroupButtonsAccess();
});
});
}
document.getElementById('btnAdminLogout').onclick = function () {
fetch('/api/admin/logout', {
method: 'POST',
headers: { 'X-Requested-With': 'XMLHttpRequest' }
})
.then(function (r) {
if (r.status === 404) {
window.location.href = '/logout';
return null;
}
return r.json();
})
.then(function (res) {
if (!res) return;
if (!res.success) {
alert(res.error || '退出失败');
return;
}
window.location.replace(res.redirect || '/login?logout=1');
})
.catch(function () {
window.location.replace('/logout');
});
};
loadAdminCurrentUser();
loadUserOptions();
loadColumnsForPermission();
loadShopManageGroups();
loadAdminMenus();
}) ();
</script>
</body>
</html>