feat: 添加产品复制功能,新增复制产品模态框及相关逻辑;优化产品管理页面交互
This commit is contained in:
@@ -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()
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user