diff --git a/pocket-base/bai_web_pb_hooks/pages/product-manage.js b/pocket-base/bai_web_pb_hooks/pages/product-manage.js index 2f84922..4ba8ae8 100644 --- a/pocket-base/bai_web_pb_hooks/pages/product-manage.js +++ b/pocket-base/bai_web_pb_hooks/pages/product-manage.js @@ -57,6 +57,12 @@ routerAdd('GET', '/manage/product-manage', function (e) { .thumb { width: 84px; height: 84px; border-radius: 10px; border: 1px solid #dbe3f0; object-fit: cover; background: #fff; } .thumb-wrap { display: flex; gap: 12px; align-items: flex-start; flex-wrap: wrap; } .muted { color: #64748b; font-size: 12px; } + .modal-mask { position: fixed; inset: 0; display: none; align-items: center; justify-content: center; padding: 16px; background: rgba(15, 23, 42, 0.42); z-index: 9999; } + .modal-mask.show { display: flex; } + .modal-card { width: min(560px, 100%); background: #fff; border: 1px solid #e5e7eb; border-radius: 16px; box-shadow: 0 24px 70px rgba(15, 23, 42, 0.2); padding: 18px; } + .modal-title { margin: 0 0 14px; font-size: 22px; color: #0f172a; } + .modal-grid { display: grid; grid-template-columns: 1fr; gap: 10px; } + .modal-actions { margin-top: 14px; display: flex; justify-content: flex-end; gap: 10px; } .loading-mask { position: fixed; inset: 0; display: none; align-items: center; justify-content: center; padding: 24px; background: rgba(15, 23, 42, 0.42); backdrop-filter: blur(4px); z-index: 9998; } .loading-mask.show { display: flex; } .loading-card { min-width: min(92vw, 360px); padding: 24px 22px; border-radius: 20px; background: rgba(255,255,255,0.98); box-shadow: 0 28px 70px rgba(15, 23, 42, 0.22); border: 1px solid #dbe3f0; text-align: center; } @@ -258,6 +264,26 @@ routerAdd('GET', '/manage/product-manage', function (e) { + +
@@ -289,6 +315,9 @@ routerAdd('GET', '/manage/product-manage', function (e) { const prodStatusOptionsEl = document.getElementById('prodStatusOptions') const prodStatusSelectedEl = document.getElementById('prodStatusSelected') const paramsBodyEl = document.getElementById('paramsBody') + const copyModalEl = document.getElementById('copyModal') + const copyNameInputEl = document.getElementById('copyNameInput') + const copyModelInputEl = document.getElementById('copyModelInput') const loadingMaskEl = document.getElementById('loadingMask') const loadingTextEl = document.getElementById('loadingText') const iconPreviewEl = document.getElementById('iconPreview') @@ -339,7 +368,7 @@ routerAdd('GET', '/manage/product-manage', function (e) { parameterRows: [], currentIconAttachment: null, pendingIconFile: null, - removedIconId: '', + copySourceProductId: '', } const multiFieldConfig = { @@ -1013,7 +1042,6 @@ routerAdd('GET', '/manage/product-manage', function (e) { state.selections.tags = [] state.parameterRows = [] state.currentIconAttachment = null - state.removedIconId = '' clearPendingIconPreview() setIconPreview('') renderProductStatusSelector() @@ -1060,7 +1088,6 @@ routerAdd('GET', '/manage/product-manage', function (e) { state.parameterRows = normalizeParameterRows(item.prod_list_parameters) state.currentIconAttachment = item.prod_list_icon_attachment || null clearPendingIconPreview() - state.removedIconId = '' renderProductStatusSelector() renderAllMultiOptionLists() @@ -1101,12 +1128,115 @@ routerAdd('GET', '/manage/product-manage', function (e) { + '' + escapeHtml(item.updated || '') + '' + '
' + '' + + '' + '' + '
' + '' }).join('') } + function buildCopyPayload(source, nextName, nextModel) { + return { + prod_list_id: '', + prod_list_name: normalizeText(nextName), + prod_list_modelnumber: normalizeText(nextModel), + prod_list_icon: source && source.prod_list_icon ? normalizeText(source.prod_list_icon) : '', + prod_list_description: source && source.prod_list_description ? normalizeText(source.prod_list_description) : '', + prod_list_feature: source && source.prod_list_feature ? normalizeText(source.prod_list_feature) : '', + prod_list_parameters: normalizeParameterRows(source ? source.prod_list_parameters : []), + prod_list_plantype: source && source.prod_list_plantype ? normalizeText(source.prod_list_plantype) : '', + prod_list_category: source && source.prod_list_category ? normalizeText(source.prod_list_category) : '', + prod_list_sort: source && (source.prod_list_sort || source.prod_list_sort === 0) ? Number(source.prod_list_sort) : 0, + prod_list_comm_type: source && source.prod_list_comm_type ? normalizeText(source.prod_list_comm_type) : '', + prod_list_series: source && source.prod_list_series ? normalizeText(source.prod_list_series) : '', + prod_list_power_supply: source && source.prod_list_power_supply ? normalizeText(source.prod_list_power_supply) : '', + prod_list_tags: source && source.prod_list_tags ? normalizeText(source.prod_list_tags) : '', + prod_list_status: source && source.prod_list_status ? normalizeText(source.prod_list_status) : '有效', + prod_list_basic_price: source && !(source.prod_list_basic_price === null || typeof source.prod_list_basic_price === 'undefined') + ? String(source.prod_list_basic_price) + : '', + prod_list_remark: source && source.prod_list_remark ? normalizeText(source.prod_list_remark) : '', + } + } + + function openCopyModal(source) { + state.copySourceProductId = source ? normalizeText(source.prod_list_id) : '' + const defaultName = normalizeText(source && source.prod_list_name) + const defaultModel = normalizeText(source && source.prod_list_modelnumber) + + copyNameInputEl.value = defaultName ? (defaultName + '-副本') : '' + copyModelInputEl.value = defaultModel + copyModalEl.classList.add('show') + + setTimeout(function () { + copyNameInputEl.focus() + copyNameInputEl.select() + }, 0) + } + + function closeCopyModal() { + state.copySourceProductId = '' + copyModalEl.classList.remove('show') + copyNameInputEl.value = '' + copyModelInputEl.value = '' + } + + async function copyProduct(productId) { + const source = state.list.find(function (item) { + return normalizeText(item.prod_list_id) === normalizeText(productId) + }) + + if (!source) { + setStatus('未找到要复制的产品。', 'error') + return + } + + openCopyModal(source) + } + + async function confirmCopyProduct() { + const sourceId = normalizeText(state.copySourceProductId) + if (!sourceId) { + setStatus('复制失败:未找到源产品。', 'error') + closeCopyModal() + return + } + + const source = state.list.find(function (item) { + return normalizeText(item.prod_list_id) === sourceId + }) + if (!source) { + setStatus('复制失败:源产品不存在。', 'error') + closeCopyModal() + return + } + + const normalizedName = normalizeText(copyNameInputEl.value) + if (!normalizedName) { + setStatus('复制失败:新产品名称不能为空。', 'error') + copyNameInputEl.focus() + return + } + + const payload = buildCopyPayload(source, normalizedName, copyModelInputEl.value) + + setStatus('正在复制产品...', '') + showLoading('正在复制产品,请稍候...') + try { + const created = await requestJson('/product/create', payload) + closeCopyModal() + await loadProducts() + if (created && created.prod_list_id) { + await enterEditMode(created.prod_list_id) + } + setStatus('复制产品成功。', 'success') + } catch (err) { + setStatus(err.message || '复制产品失败', 'error') + } finally { + hideLoading() + } + } + async function loadProducts() { setStatus('正在加载产品列表...', '') showLoading('正在加载产品列表...') @@ -1216,13 +1346,6 @@ routerAdd('GET', '/manage/product-manage', function (e) { saved = await requestJson('/product/create', payload) } - if (state.removedIconId) { - try { - await requestJson('/attachment/delete', { attachments_id: state.removedIconId }) - } catch (_error) {} - state.removedIconId = '' - } - await loadProducts() if (saved && saved.prod_list_id) { await enterEditMode(saved.prod_list_id) @@ -1266,6 +1389,9 @@ routerAdd('GET', '/manage/product-manage', function (e) { window.__deleteProduct = function (encodedProductId) { deleteProduct(decodeURIComponent(encodedProductId)) } + window.__copyProduct = function (encodedProductId) { + copyProduct(decodeURIComponent(encodedProductId)) + } document.addEventListener('change', function (event) { const target = event.target @@ -1418,15 +1544,32 @@ routerAdd('GET', '/manage/product-manage', function (e) { } document.getElementById('clearIconBtn').addEventListener('click', function () { - if (state.currentIconAttachment && state.currentIconAttachment.attachments_id) { - state.removedIconId = state.currentIconAttachment.attachments_id - } state.currentIconAttachment = null clearPendingIconPreview() fields.iconFile.value = '' setIconPreview('') }) + document.getElementById('copyCancelBtn').addEventListener('click', function () { + closeCopyModal() + }) + + document.getElementById('copyConfirmBtn').addEventListener('click', function () { + confirmCopyProduct() + }) + + copyModalEl.addEventListener('click', function (event) { + if (event.target === copyModalEl) { + closeCopyModal() + } + }) + + copyModelInputEl.addEventListener('keydown', function (event) { + if (event.key === 'Enter') { + confirmCopyProduct() + } + }) + document.getElementById('submitBtn').addEventListener('click', function () { submitProduct() })