优化后台业务

This commit is contained in:
super
2026-04-30 11:36:58 +08:00
parent 9c16c9c589
commit c6ecfb5b77
48 changed files with 1211 additions and 2391 deletions

View File

@@ -336,6 +336,178 @@
font-size: 16px;
}
#shopManageGroupModal .shop-group-modal {
width: min(1180px, calc(100vw - 48px));
max-width: none;
max-height: calc(100vh - 48px);
padding: 0;
overflow: hidden;
display: flex;
flex-direction: column;
}
.shop-group-modal-header {
padding: 20px 24px 14px;
border-bottom: 1px solid #edf0f5;
flex: 0 0 auto;
}
.shop-group-modal-header h3 {
margin: 0;
}
.shop-group-editor {
padding: 18px 24px 14px;
display: grid;
grid-template-columns: minmax(180px, 240px) minmax(240px, 320px) minmax(360px, 1fr);
gap: 16px;
align-items: start;
flex: 0 0 auto;
}
.shop-group-editor .form-group {
margin-bottom: 0;
}
.shop-group-member-select {
min-height: 128px;
max-height: 180px;
}
.shop-group-help {
color: #777;
font-size: 12px;
line-height: 1.5;
margin-top: 6px;
}
.shop-group-action-bar {
padding: 0 24px 12px;
display: flex;
gap: 8px;
justify-content: flex-end;
align-items: center;
flex: 0 0 auto;
}
#msgShopManageGroup {
margin: 0;
padding: 0 24px 10px;
min-height: 18px;
flex: 0 0 auto;
}
.shop-group-table-wrap {
margin: 0 24px;
border: 1px solid #edf0f5;
border-radius: 8px;
overflow: auto;
min-height: 220px;
flex: 1 1 auto;
}
.shop-group-table {
min-width: 1040px;
table-layout: fixed;
}
.shop-group-table th {
position: sticky;
top: 0;
z-index: 1;
}
.shop-group-table th,
.shop-group-table td {
vertical-align: top;
}
.shop-group-table .col-index {
width: 58px;
}
.shop-group-table .col-name {
width: 130px;
}
.shop-group-table .col-leader {
width: 110px;
}
.shop-group-table .col-count {
width: 86px;
}
.shop-group-table .col-members {
width: auto;
}
.shop-group-table .col-time {
width: 120px;
}
.shop-group-table .col-action {
width: 128px;
}
.shop-group-time-cell,
.shop-group-action-cell {
white-space: nowrap;
}
.shop-group-name-cell,
.shop-group-leader-cell {
word-break: break-word;
}
.shop-group-members {
display: flex;
flex-wrap: wrap;
gap: 6px;
max-height: 96px;
overflow-y: auto;
padding-right: 4px;
}
.shop-group-member-chip {
display: inline-flex;
align-items: center;
max-width: 150px;
padding: 3px 8px;
border-radius: 999px;
background: #eef1ff;
color: #4f63d8;
font-size: 12px;
line-height: 1.4;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.shop-group-modal-footer {
padding: 14px 24px 18px;
display: flex;
gap: 8px;
justify-content: flex-end;
border-top: 1px solid #edf0f5;
flex: 0 0 auto;
}
@media (max-width: 900px) {
#shopManageGroupModal .shop-group-modal {
width: calc(100vw - 24px);
max-height: calc(100vh - 24px);
}
.shop-group-editor {
grid-template-columns: 1fr;
}
.shop-group-table-wrap {
margin: 0 16px;
}
}
.column-permission-cards {
display: flex;
flex-wrap: wrap;
@@ -347,6 +519,97 @@
overflow: hidden;
}
.multi-select-native {
position: absolute;
opacity: 0;
pointer-events: none;
width: 1px !important;
height: 1px !important;
min-height: 0 !important;
padding: 0 !important;
border: 0 !important;
}
.multi-select-dropdown {
position: relative;
width: 100%;
}
.multi-select-trigger {
width: 100%;
min-height: 42px;
padding: 10px 34px 10px 12px;
border: 1px solid #ddd;
border-radius: 6px;
background: #fff;
color: #333;
font-size: 14px;
text-align: left;
cursor: pointer;
position: relative;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.multi-select-trigger::after {
content: "";
position: absolute;
right: 12px;
top: 50%;
width: 7px;
height: 7px;
border-right: 1.5px solid #666;
border-bottom: 1.5px solid #666;
transform: translateY(-65%) rotate(45deg);
}
.multi-select-dropdown.open .multi-select-trigger {
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.12);
}
.multi-select-panel {
display: none;
position: absolute;
left: 0;
right: 0;
top: calc(100% + 6px);
z-index: 30;
background: #fff;
border: 1px solid #ddd;
border-radius: 8px;
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.14);
padding: 6px;
max-height: 220px;
overflow-y: auto;
}
.multi-select-dropdown.open .multi-select-panel {
display: block;
}
.multi-select-option {
display: flex;
align-items: center;
gap: 8px;
min-height: 34px;
padding: 7px 8px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
color: #333;
}
.multi-select-option:hover {
background: #f4f6ff;
}
.multi-select-option input {
width: auto;
margin: 0;
}
.column-permission-card {
display: inline-flex;
align-items: center;
@@ -762,7 +1025,7 @@
<label>&nbsp;</label>
<button class="btn btn-secondary" id="btnChooseSkipPriceAsinShop" type="button">选择店铺</button>
</div>
<select id="skipPriceAsinCountries" multiple size="5">
<select id="skipPriceAsinCountries" class="js-dropdown-multi" multiple>
<option value="DE">德国</option>
<option value="UK">英国</option>
<option value="FR">法国</option>
@@ -840,7 +1103,7 @@
</div>
<div class="form-group" style="min-width:220px;">
<label>国家</label>
<select id="queryAsinCountries" multiple size="5" style="min-height:98px;">
<select id="queryAsinCountries" class="js-dropdown-multi" multiple>
<option value="DE">德国</option>
<option value="UK">英国</option>
<option value="FR">法国</option>
@@ -1063,48 +1326,62 @@
<!-- 分组管理弹窗 -->
<div class="modal-mask" id="shopManageGroupModal">
<div class="modal" style="max-width:760px;">
<h3>分组管理</h3>
<div class="form-row">
<div class="modal shop-group-modal">
<div class="shop-group-modal-header">
<h3>分组管理</h3>
</div>
<div class="shop-group-editor">
<input type="hidden" id="shopManageGroupEditId">
<input type="hidden" id="shopManageGroupLeaderUserId">
<div class="form-group" style="min-width:220px;">
<div class="form-group">
<label>组长</label>
<input type="text" id="shopManageGroupLeaderName" placeholder="当前登录用户" readonly
style="background:#f5f5f5;">
</div>
<div class="form-group" style="min-width:260px;">
<div class="form-group">
<label>分组名称</label>
<input type="text" id="shopManageGroupInput" placeholder="请输入分组名称">
</div>
<div class="form-group" style="min-width:280px;flex:1;">
<div class="form-group">
<label>组员</label>
<select id="shopManageGroupMemberSelect" multiple size="6" style="min-height:140px;">
<select id="shopManageGroupMemberSelect" class="shop-group-member-select" multiple size="6">
</select>
<div id="shopManageGroupMemberHelp" style="color:#888;font-size:12px;margin-top:6px;">可添加当前组长创建的普通员工账号,按住 Ctrl 或 Command 可多选。</div>
<div id="shopManageGroupMemberHelp" class="shop-group-help">可添加当前组长创建的普通员工账号,按住 Ctrl 或 Command 可多选。</div>
</div>
</div>
<div style="display:flex;gap:8px;justify-content:flex-end;margin-bottom:12px;">
<div class="shop-group-action-bar">
<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;">
<div class="shop-group-table-wrap">
<table class="shop-group-table">
<colgroup>
<col class="col-index">
<col class="col-name">
<col class="col-leader">
<col class="col-count">
<col class="col-members">
<col class="col-time">
<col class="col-time">
<col class="col-action">
</colgroup>
<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>
<div class="shop-group-modal-footer">
<button class="btn btn-secondary" id="btnCloseShopManageGroupModal" type="button">关闭</button>
</div>
</div>
@@ -1865,6 +2142,111 @@
});
}
var shopManageAllUsers = [];
function escapeHtml(value) {
return String(value == null ? '' : value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
var dropdownMultiSelects = {};
function initDropdownMultiSelect(selectId, placeholder) {
var select = document.getElementById(selectId);
if (!select || dropdownMultiSelects[selectId]) return;
select.classList.add('multi-select-native');
var wrapper = document.createElement('div');
wrapper.className = 'multi-select-dropdown';
wrapper.setAttribute('data-multi-select-id', selectId);
var trigger = document.createElement('button');
trigger.type = 'button';
trigger.className = 'multi-select-trigger';
trigger.textContent = placeholder || '请选择';
var panel = document.createElement('div');
panel.className = 'multi-select-panel';
select.parentNode.insertBefore(wrapper, select);
wrapper.appendChild(trigger);
wrapper.appendChild(panel);
wrapper.appendChild(select);
function selectedOptions() {
return Array.from(select.options).filter(function (option) { return option.selected; });
}
function updateSummary() {
var selected = selectedOptions();
if (!selected.length) {
trigger.textContent = placeholder || '请选择';
trigger.title = '';
return;
}
var labels = selected.map(function (option) { return option.textContent || option.value; });
trigger.textContent = labels.join('、');
trigger.title = labels.join('、');
}
function renderOptions() {
panel.innerHTML = Array.from(select.options).map(function (option, index) {
var optionId = selectId + '_multi_' + index;
return '<label class="multi-select-option" for="' + escapeHtml(optionId) + '">' +
'<input type="checkbox" id="' + escapeHtml(optionId) + '" data-multi-option-index="' + index + '"' + (option.selected ? ' checked' : '') + '>' +
'<span>' + escapeHtml(option.textContent || option.value) + '</span>' +
'</label>';
}).join('');
panel.querySelectorAll('[data-multi-option-index]').forEach(function (checkbox) {
checkbox.onchange = function () {
var option = select.options[Number(checkbox.getAttribute('data-multi-option-index'))];
if (!option) return;
option.selected = checkbox.checked;
updateSummary();
select.dispatchEvent(new Event('change', { bubbles: true }));
};
});
}
trigger.onclick = function (event) {
event.stopPropagation();
document.querySelectorAll('.multi-select-dropdown.open').forEach(function (item) {
if (item !== wrapper) item.classList.remove('open');
});
wrapper.classList.toggle('open');
};
panel.onclick = function (event) {
event.stopPropagation();
};
select.addEventListener('change', function () {
panel.querySelectorAll('[data-multi-option-index]').forEach(function (checkbox) {
var option = select.options[Number(checkbox.getAttribute('data-multi-option-index'))];
checkbox.checked = !!(option && option.selected);
});
updateSummary();
});
renderOptions();
updateSummary();
dropdownMultiSelects[selectId] = {
refresh: function () {
renderOptions();
updateSummary();
}
};
}
function refreshDropdownMultiSelect(selectId) {
if (dropdownMultiSelects[selectId]) {
dropdownMultiSelects[selectId].refresh();
}
}
document.addEventListener('click', function () {
document.querySelectorAll('.multi-select-dropdown.open').forEach(function (item) {
item.classList.remove('open');
});
});
function getEligibleShopManageGroupUsers(leaderUserId) {
var canViewAllMembers = currentUserRole === 'super_admin';
return shopManageAllUsers.filter(function (u) {
@@ -1884,7 +2266,7 @@
});
var eligibleUsers = getEligibleShopManageGroupUsers(leaderUserId);
var options = eligibleUsers.map(function (u) {
return '<option value="' + u.id + '"' + (selectedMap[String(u.id)] ? ' selected' : '') + '>' + (u.username || '') + '</option>';
return '<option value="' + escapeHtml(u.id) + '"' + (selectedMap[String(u.id)] ? ' selected' : '') + '>' + escapeHtml(u.username || '') + '</option>';
});
if (options.length) {
sel.innerHTML = options.join('');
@@ -2405,6 +2787,7 @@
// ========== 店铺管理 ==========
var shopManagePage = 1, shopManagePageSize = 15;
var shopManageGroups = [];
var shopManageGroupsLoadedAt = 0;
var currentShopManageGroupGrantRoutes = [];
function buildShopManageQuery(page) {
@@ -2521,17 +2904,23 @@
if (chooseQueryShopSel && selectedChooseQueryShopId) chooseQueryShopSel.value = selectedChooseQueryShopId;
}
function loadShopManageGroups(selectedCreateId, selectedEditId) {
function loadShopManageGroups(selectedCreateId, selectedEditId, force) {
if (!force && shopManageGroups.length && Date.now() - shopManageGroupsLoadedAt < 30000) {
refreshShopGroupSelects(selectedCreateId, selectedEditId);
return Promise.resolve(shopManageGroups);
}
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 || [];
shopManageGroupsLoadedAt = Date.now();
refreshShopGroupSelects(selectedCreateId, selectedEditId);
return shopManageGroups;
})
.catch(function () {
shopManageGroups = [];
shopManageGroupsLoadedAt = 0;
refreshShopGroupSelects();
return [];
});
@@ -2576,14 +2965,28 @@
return;
}
tbody.innerHTML = shopManageGroups.map(function (item, index) {
var memberNames = Array.isArray(item.member_usernames) ? item.member_usernames.join('、') : '';
var memberNames = Array.isArray(item.member_usernames) ? item.member_usernames : [];
var memberHtml = memberNames.length
? '<div class="shop-group-members" title="' + escapeHtml(memberNames.join('、')) + '">' +
memberNames.map(function (name) {
return '<span class="shop-group-member-chip">' + escapeHtml(name || '') + '</span>';
}).join('') + '</div>'
: '<span style="color:#999;">-</span>';
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>')
? ('<button class="btn btn-sm" data-shop-group-edit="' + escapeHtml(item.id) + '">编辑</button> ' +
'<button class="btn btn-sm btn-danger" data-shop-group-delete="' + escapeHtml(item.id) + '" data-shop-group-name="' + escapeHtml(item.group_name || '') + '">删除</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>';
return '<tr>' +
'<td>' + (index + 1) + '</td>' +
'<td class="shop-group-name-cell">' + escapeHtml(item.group_name || '') + '</td>' +
'<td class="shop-group-leader-cell">' + escapeHtml(item.leader_username || '') + '</td>' +
'<td>' + escapeHtml(item.member_count || 0) + '</td>' +
'<td>' + memberHtml + '</td>' +
'<td class="shop-group-time-cell">' + escapeHtml(item.created_at || '') + '</td>' +
'<td class="shop-group-time-cell">' + escapeHtml(item.updated_at || '') + '</td>' +
'<td class="shop-group-action-cell">' + actionHtml + '</td>' +
'</tr>';
}).join('');
document.querySelectorAll('[data-shop-group-edit]').forEach(function (btn) {
@@ -2617,7 +3020,7 @@
alert(res.error || '删除失败');
return;
}
loadShopManageGroups().then(function () {
loadShopManageGroups(null, null, true).then(function () {
renderShopManageGroupRows();
loadShopManage(shopManagePage);
loadSkipPriceAsin(skipPriceAsinPage);
@@ -2689,7 +3092,7 @@
resetShopManageGroupForm();
msgEl.textContent = res.msg || '保存成功';
msgEl.className = 'msg ok';
loadShopManageGroups().then(function () {
loadShopManageGroups(null, null, true).then(function () {
renderShopManageGroupRows();
loadShopManage(shopManagePage);
loadSkipPriceAsin(skipPriceAsinPage);
@@ -3174,6 +3577,7 @@
Array.from(document.getElementById('skipPriceAsinCountries').options).forEach(function (option) {
option.selected = false;
});
refreshDropdownMultiSelect('skipPriceAsinCountries');
renderSkipPriceAsinInputs();
msgEl.textContent = res.msg || '保存成功';
msgEl.className = 'msg ok';
@@ -3185,6 +3589,7 @@
});
};
setupSkipPriceAsinShopPicker();
initDropdownMultiSelect('skipPriceAsinCountries', '请选择国家');
renderSkipPriceAsinInputs();
// ========== 查询 ASIN ==========
@@ -3677,6 +4082,7 @@
Array.from(document.getElementById('queryAsinCountries').options).forEach(function (option) {
option.selected = false;
});
refreshDropdownMultiSelect('queryAsinCountries');
renderQueryAsinInputs();
msgEl.textContent = res.msg || '保存成功';
msgEl.className = 'msg ok';
@@ -3687,6 +4093,7 @@
msgEl.className = 'msg err';
});
};
initDropdownMultiSelect('queryAsinCountries', '请选择国家');
document.getElementById('btnImportQueryAsin').onclick = function () {
uploadQueryAsinImport(false);
};