feat: 添加产品复制功能,新增复制产品模态框及相关逻辑;优化产品管理页面交互

This commit is contained in:
2026-04-01 19:03:55 +08:00
parent f793cb3cdd
commit 94811b52e2

View File

@@ -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 { 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; } .thumb-wrap { display: flex; gap: 12px; align-items: flex-start; flex-wrap: wrap; }
.muted { color: #64748b; font-size: 12px; } .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 { 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-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; } .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) {
</section> </section>
</div> </div>
<div class="modal-mask" id="copyModal">
<div class="modal-card" role="dialog" aria-modal="true" aria-labelledby="copyModalTitle">
<h3 class="modal-title" id="copyModalTitle">复制产品</h3>
<div class="modal-grid">
<div>
<label for="copyNameInput">新产品名称</label>
<input id="copyNameInput" placeholder="请输入新产品名称" />
</div>
<div>
<label for="copyModelInput">新产品型号</label>
<input id="copyModelInput" placeholder="请输入新产品型号" />
</div>
</div>
<div class="modal-actions">
<button class="btn btn-light" id="copyCancelBtn" type="button">取消</button>
<button class="btn btn-primary" id="copyConfirmBtn" type="button">确定复制</button>
</div>
</div>
</div>
<div class="loading-mask" id="loadingMask"> <div class="loading-mask" id="loadingMask">
<div class="loading-card"> <div class="loading-card">
<div class="loading-spinner"></div> <div class="loading-spinner"></div>
@@ -289,6 +315,9 @@ routerAdd('GET', '/manage/product-manage', function (e) {
const prodStatusOptionsEl = document.getElementById('prodStatusOptions') const prodStatusOptionsEl = document.getElementById('prodStatusOptions')
const prodStatusSelectedEl = document.getElementById('prodStatusSelected') const prodStatusSelectedEl = document.getElementById('prodStatusSelected')
const paramsBodyEl = document.getElementById('paramsBody') 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 loadingMaskEl = document.getElementById('loadingMask')
const loadingTextEl = document.getElementById('loadingText') const loadingTextEl = document.getElementById('loadingText')
const iconPreviewEl = document.getElementById('iconPreview') const iconPreviewEl = document.getElementById('iconPreview')
@@ -339,7 +368,7 @@ routerAdd('GET', '/manage/product-manage', function (e) {
parameterRows: [], parameterRows: [],
currentIconAttachment: null, currentIconAttachment: null,
pendingIconFile: null, pendingIconFile: null,
removedIconId: '', copySourceProductId: '',
} }
const multiFieldConfig = { const multiFieldConfig = {
@@ -1013,7 +1042,6 @@ routerAdd('GET', '/manage/product-manage', function (e) {
state.selections.tags = [] state.selections.tags = []
state.parameterRows = [] state.parameterRows = []
state.currentIconAttachment = null state.currentIconAttachment = null
state.removedIconId = ''
clearPendingIconPreview() clearPendingIconPreview()
setIconPreview('') setIconPreview('')
renderProductStatusSelector() renderProductStatusSelector()
@@ -1060,7 +1088,6 @@ routerAdd('GET', '/manage/product-manage', function (e) {
state.parameterRows = normalizeParameterRows(item.prod_list_parameters) state.parameterRows = normalizeParameterRows(item.prod_list_parameters)
state.currentIconAttachment = item.prod_list_icon_attachment || null state.currentIconAttachment = item.prod_list_icon_attachment || null
clearPendingIconPreview() clearPendingIconPreview()
state.removedIconId = ''
renderProductStatusSelector() renderProductStatusSelector()
renderAllMultiOptionLists() renderAllMultiOptionLists()
@@ -1101,12 +1128,115 @@ routerAdd('GET', '/manage/product-manage', function (e) {
+ '<td data-label="更新时间"><span class="muted">' + escapeHtml(item.updated || '') + '</span></td>' + '<td data-label="更新时间"><span class="muted">' + escapeHtml(item.updated || '') + '</span></td>'
+ '<td data-label="操作"><div class="table-actions">' + '<td data-label="操作"><div class="table-actions">'
+ '<button class="btn btn-light" type="button" onclick="window.__editProduct(\\'' + encodeURIComponent(item.prod_list_id) + '\\')">编辑</button>' + '<button class="btn btn-light" type="button" onclick="window.__editProduct(\\'' + encodeURIComponent(item.prod_list_id) + '\\')">编辑</button>'
+ '<button class="btn btn-secondary" type="button" onclick="window.__copyProduct(\\'' + encodeURIComponent(item.prod_list_id) + '\\')">复制</button>'
+ '<button class="btn btn-danger" type="button" onclick="window.__deleteProduct(\\'' + encodeURIComponent(item.prod_list_id) + '\\')">删除</button>' + '<button class="btn btn-danger" type="button" onclick="window.__deleteProduct(\\'' + encodeURIComponent(item.prod_list_id) + '\\')">删除</button>'
+ '</div></td>' + '</div></td>'
+ '</tr>' + '</tr>'
}).join('') }).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() { async function loadProducts() {
setStatus('正在加载产品列表...', '') setStatus('正在加载产品列表...', '')
showLoading('正在加载产品列表...') showLoading('正在加载产品列表...')
@@ -1216,13 +1346,6 @@ routerAdd('GET', '/manage/product-manage', function (e) {
saved = await requestJson('/product/create', payload) saved = await requestJson('/product/create', payload)
} }
if (state.removedIconId) {
try {
await requestJson('/attachment/delete', { attachments_id: state.removedIconId })
} catch (_error) {}
state.removedIconId = ''
}
await loadProducts() await loadProducts()
if (saved && saved.prod_list_id) { if (saved && saved.prod_list_id) {
await enterEditMode(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) { window.__deleteProduct = function (encodedProductId) {
deleteProduct(decodeURIComponent(encodedProductId)) deleteProduct(decodeURIComponent(encodedProductId))
} }
window.__copyProduct = function (encodedProductId) {
copyProduct(decodeURIComponent(encodedProductId))
}
document.addEventListener('change', function (event) { document.addEventListener('change', function (event) {
const target = event.target const target = event.target
@@ -1418,15 +1544,32 @@ routerAdd('GET', '/manage/product-manage', function (e) {
} }
document.getElementById('clearIconBtn').addEventListener('click', function () { document.getElementById('clearIconBtn').addEventListener('click', function () {
if (state.currentIconAttachment && state.currentIconAttachment.attachments_id) {
state.removedIconId = state.currentIconAttachment.attachments_id
}
state.currentIconAttachment = null state.currentIconAttachment = null
clearPendingIconPreview() clearPendingIconPreview()
fields.iconFile.value = '' fields.iconFile.value = ''
setIconPreview('') 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 () { document.getElementById('submitBtn').addEventListener('click', function () {
submitProduct() submitProduct()
}) })