+
-
-
-
- | 序号 |
- 分组名称 |
- 组长 |
- 组员数量 |
- 组员 |
- 创建时间 |
- 修改时间 |
- 操作 |
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | 序号 |
+ 分组名称 |
+ 组长 |
+ 组员数量 |
+ 组员 |
+ 创建时间 |
+ 修改时间 |
+ 操作 |
+
+
+
+
+
+
@@ -1865,6 +2142,111 @@
});
}
var shopManageAllUsers = [];
+ function escapeHtml(value) {
+ return String(value == null ? '' : value)
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''');
+ }
+ 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 '
';
+ }).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 '
';
+ return '
';
});
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
+ ? '
' +
+ memberNames.map(function (name) {
+ return '' + escapeHtml(name || '') + '';
+ }).join('') + '
'
+ : '
-';
var canEdit = currentUserRole === 'super_admin' || String(item.leader_user_id || '') === String(currentUserId || '');
var actionHtml = canEdit
- ? ('
' +
- '
')
+ ? ('
' +
+ '
')
: '
-';
- return '
| ' + (index + 1) + ' | ' + (item.group_name || '') + ' | ' + (item.leader_username || '') + ' | ' + (item.member_count || 0) + ' | ' + (memberNames || '-') + ' | ' + (item.created_at || '') + ' | ' + (item.updated_at || '') + ' | ' +
- actionHtml + ' |
';
+ return '
' +
+ '| ' + (index + 1) + ' | ' +
+ '' + escapeHtml(item.group_name || '') + ' | ' +
+ '' + escapeHtml(item.leader_username || '') + ' | ' +
+ '' + escapeHtml(item.member_count || 0) + ' | ' +
+ '' + memberHtml + ' | ' +
+ '' + escapeHtml(item.created_at || '') + ' | ' +
+ '' + escapeHtml(item.updated_at || '') + ' | ' +
+ '' + actionHtml + ' | ' +
+ '
';
}).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);
};
diff --git a/frontend-vue/src/pages/brand/components/BrandAppearancePatentTab.vue b/frontend-vue/src/pages/brand/components/BrandAppearancePatentTab.vue
index 91a6535..73a0805 100644
--- a/frontend-vue/src/pages/brand/components/BrandAppearancePatentTab.vue
+++ b/frontend-vue/src/pages/brand/components/BrandAppearancePatentTab.vue
@@ -153,6 +153,7 @@ import {
import { expandBrandFolderRecursive, type BrandExpandFolderItem } from '@/shared/api/brand'
import { getPywebviewApi } from '@/shared/bridges/pywebview'
import { getTaskPollIntervalMs } from '@/shared/task-progress-config'
+import { createCategorizedTimers } from '@/shared/utils/categorized-timers'
const selectedFileNames = ref
([])
const uploadedFiles = ref([])
@@ -171,6 +172,8 @@ const queuePayloadText = ref('')
const pollingTaskIds = ref([])
const pollTimer = ref(null)
const pollingInFlight = ref(false)
+let disposed = false
+const timers = createCategorizedTimers('appearance-patent')
const dashboard = ref({
pendingTaskCount: 0,
@@ -389,33 +392,36 @@ function removePollingTask(taskId: number) {
}
function ensurePolling() {
+ if (disposed) return
if (pollTimer.value != null) return
scheduleNextPoll(true)
}
function stopPolling() {
if (pollTimer.value != null) {
- window.clearTimeout(pollTimer.value)
+ timers.clearTimer('task-poll', pollTimer.value)
pollTimer.value = null
}
}
function scheduleNextPoll(immediate = false) {
+ if (disposed) return
if (pollTimer.value != null) {
if (!immediate) return
- window.clearTimeout(pollTimer.value)
+ timers.clearTimer('task-poll', pollTimer.value)
pollTimer.value = null
}
const run = async () => {
pollTimer.value = null
+ if (disposed) return
if (!pollingTaskIds.value.length) return
await refreshTaskProgress()
- if (pollingTaskIds.value.length) {
- pollTimer.value = window.setTimeout(run, getTaskPollIntervalMs())
+ if (!disposed && pollingTaskIds.value.length) {
+ pollTimer.value = timers.setTimeout('task-poll', run, getTaskPollIntervalMs())
}
}
if (immediate) void run()
- else pollTimer.value = window.setTimeout(run, getTaskPollIntervalMs())
+ else pollTimer.value = timers.setTimeout('task-poll', run, getTaskPollIntervalMs())
}
async function refreshTaskProgress() {
@@ -547,7 +553,11 @@ onMounted(async () => {
])
})
-onUnmounted(() => stopPolling())
+onUnmounted(() => {
+ disposed = true
+ stopPolling()
+ timers.clearScope()
+})