Files
BMS/bms-front/src/views/price/set/create.vue
T
2026-09-28 16:26:55 +08:00

322 lines
9.7 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="app-container">
<el-steps :active="active" finish-status="success" align-center class="wizard-steps">
<el-step :title="step1Title" />
<el-step :title="step2Title" />
</el-steps>
<div v-show="active === 0">
<step-basic-info ref="basicRef" v-model="form.basic" />
</div>
<step-details
v-show="active === 1"
ref="detailsRef"
:details="form.details"
:price-set-id="form.priceSetId"
:set-type="form.basic.setType"
:version-no="form.version.versionNo"
:ref-data="refData"
/>
<div class="wizard-actions">
<el-button @click="goBack">取消</el-button>
<el-button v-if="active > 0 && !directStep2" @click="active--">上一步</el-button>
<el-button v-if="active === 0" type="primary" :loading="creating" @click="goStep2">创建套餐价格</el-button>
<el-button v-if="active === 0" type="info" :loading="saving" @click="saveDraft">保存</el-button>
<el-button v-if="active === 1" type="primary" @click="finishEdit">完成</el-button>
</div>
<el-dialog
v-model="progressVisible"
:title="'正在保存套餐'"
width="420px"
:close-on-click-modal="false"
:close-on-press-escape="false"
:show-close="false"
append-to-body
>
<div class="save-progress">
<el-progress :percentage="progressPercent" :status="progressStatus" />
<p class="save-progress__msg">{{ progressMessage }}</p>
<p class="save-progress__hint">请勿关闭页面,大量明细保存可能需要一些时间</p>
</div>
</el-dialog>
</div>
</template>
<script setup name="PriceSetCreate">
import StepBasicInfo from './components/create/StepBasicInfo.vue'
import StepDetails from './components/create/StepDetails.vue'
import { usePriceSetRefData, useDetailCascade } from './composables/usePriceSetRefData'
import {
createEmptyBasicInfo,
nextVersionNo,
toCustomerPayload,
toDetailPayload
} from './utils/priceSet'
import { getPriceSet, savePriceSetAsync } from '@/api/price/set'
const { proxy } = getCurrentInstance()
const route = useRoute()
const refData = usePriceSetRefData()
const cascade = useDetailCascade(refData)
const active = ref(0)
const saving = ref(false)
const creating = ref(false)
const progressVisible = ref(false)
const progressPercent = ref(0)
const progressMessage = ref('')
const progressStatus = ref(undefined)
const basicRef = ref()
const detailsRef = ref()
const copyMode = computed(() => String(route.query.copyVersion || '') === '1')
/** Entered from detail: continue-config / copy-version jump to step2 — hide 上一步 */
const directStep2 = computed(() => copyMode.value || String(route.query.step || '') === '2')
const step1Title = '基本信息'
const step2Title = '编辑产品价格'
const form = reactive({
priceSetId: undefined,
basic: createEmptyBasicInfo(),
version: { psvId: undefined, versionNo: 'V1', effectiveTime: undefined, expireTime: undefined },
details: [],
customers: []
})
function mapSaveProgressMessage(message) {
const m = message || ''
if (!m) return ''
if (m === 'queued') return '排队中'
if (m === 'pulling library') return '拉取产品库'
if (m === 'details exist, skip pull') return '明细已存在,跳过拉取'
if (m === 'saving basic') return '保存基本信息'
if (m === 'saving version') return '保存版本'
if (m.startsWith('saving details ')) return '保存明细 ' + m.slice('saving details '.length)
if (m === 'saving surcharges') return '保存附加费'
if (m === 'details unchanged') return '明细未变更'
if (m === 'saving customers') return '保存客户'
if (m === 'finishing') return '收尾中'
if (m === 'done') return '完成'
if (m === 'failed') return '失败'
return m
}
async function runSaveWithProgress(payload) {
progressVisible.value = true
progressPercent.value = 0
progressMessage.value = ''
progressStatus.value = undefined
try {
const result = await savePriceSetAsync(payload, (task) => {
progressPercent.value = Math.min(100, Number(task.percent) || 0)
progressMessage.value = mapSaveProgressMessage(task.message)
if (task.status === 'FAIL') {
progressStatus.value = 'exception'
} else if (task.status === 'SUCCESS') {
progressStatus.value = 'success'
} else {
progressStatus.value = undefined
}
})
progressPercent.value = 100
progressStatus.value = 'success'
return result.priceSetId
} catch (e) {
progressStatus.value = 'exception'
const msg = (e && e.task && e.task.error) || (e && e.message) || '保存失败'
progressMessage.value = msg
if (proxy && proxy.$modal) {
proxy.$modal.msgError(msg)
}
await new Promise(resolve => setTimeout(resolve, 800))
throw e
} finally {
progressVisible.value = false
}
}
/** Step1 -> Step2: create package prices from library when needed */
async function goStep2() {
const validBasic = await basicRef.value.validate()
if (!validBasic) return
const hasPersistedDetails = (form.details || []).some(d => d && d.psdId)
if (form.priceSetId && form.version.psvId && hasPersistedDetails) {
active.value = 1
return
}
creating.value = true
try {
form.details.splice(0, form.details.length)
const priceSetId = await runSaveWithProgress({
...buildPayload(),
details: [],
pullFromLibrary: 'Y'
})
form.priceSetId = priceSetId
const fresh = await getPriceSet(priceSetId)
applySetData(fresh.data || {}, false)
active.value = 1
proxy.$modal.msgSuccess('已创建套餐价格,可在第 2 步编辑')
} catch (e) {
// surfaced
} finally {
creating.value = false
}
}
/** Step2: prices already live-saved via PUT /detail; go to detail page. */
function finishEdit() {
if (!form.priceSetId) {
goBack()
return
}
proxy.$modal.msgSuccess('修改已实时落库')
const obj = {
path: '/customer/setDetail',
query: { priceSetId: form.priceSetId }
}
if (proxy.$tab) {
proxy.$tab.closeOpenPage(obj)
} else {
proxy.$router.push(obj)
}
}
/** Step0 only: persist basic draft (no detail wipe). */
async function saveDraft() {
const validBasic = await basicRef.value.validate()
if (!validBasic) {
active.value = 0
return
}
saving.value = true
try {
const priceSetId = await runSaveWithProgress(buildPayload())
form.priceSetId = priceSetId
proxy.$modal.msgSuccess('已保存(待启用),套餐ID=' + priceSetId)
goBack()
} catch (e) {
// surfaced
} finally {
saving.value = false
}
}
function buildPayload() {
const { basic, version } = form
const detailRows = form.details.filter(d => d && d.productCode)
// Price edits already persist via PUT /detail; product remove uses DELETE.
// Omit details when all rows already have psdId so save only updates header/version.
const hasUnsavedDetail = detailRows.some(d => d.psdId == null)
const payload = {
priceSetId: form.priceSetId,
priceSetName: basic.priceSetName,
setType: basic.setType,
defaultDiscount: basic.setType === 'DISCOUNT' ? basic.defaultDiscount : undefined,
priceSetNote: basic.priceSetNote,
version: {
psvId: copyMode.value ? undefined : version.psvId,
versionNo: version.versionNo
}
}
if (hasUnsavedDetail) {
payload.details = detailRows.map(toDetailPayload)
}
// Customer bind is managed on detail page APIs; only send when present to avoid wipe.
const customers = form.customers.filter(c => c && c.customerCode).map(toCustomerPayload)
if (customers.length) {
payload.customers = customers
}
return payload
}
function goBack() {
const obj = { path: '/customer/set' }
if (proxy.$tab) {
proxy.$tab.closeOpenPage(obj)
} else {
proxy.$router.push(obj)
}
}
function applySetData(data, asCopy) {
form.priceSetId = data.priceSetId
Object.assign(form.basic, {
priceSetName: data.priceSetName,
setType: data.setType || 'FIXED',
defaultDiscount: data.defaultDiscount != null ? Number(data.defaultDiscount) : 88,
priceSetNote: data.priceSetNote
})
const versions = data.versions || []
const qPsv = route.query.psvId
const byQuery = qPsv != null ? versions.find(v => String(v.psvId) === String(qPsv)) : null
const current = byQuery || data.version || versions[0] || {}
if (asCopy) {
form.version.psvId = undefined
form.version.versionNo = nextVersionNo(versions)
form.version.effectiveTime = undefined
form.version.expireTime = undefined
} else {
form.version.psvId = current.psvId
form.version.versionNo = current.versionNo || nextVersionNo(versions)
form.version.effectiveTime = current.effectiveTime
form.version.expireTime = current.expireTime
}
const srcDetails = (current.details && current.details.length) ? current.details : (data.details || [])
form.details = srcDetails.map(d => {
const row = cascade.hydrateRow(d, form.basic.setType)
if (asCopy) row.psdId = undefined
return row
})
form.customers = (data.customers || []).map(c => ({ ...c }))
}
function loadExisting(id, asCopy) {
getPriceSet(id).then(res => {
applySetData(res.data || {}, asCopy)
if (asCopy || String(route.query.step || '') === '2') {
active.value = 1
}
})
}
onMounted(() => {
refData.loadProducts().finally(() => {
const id = route.query.priceSetId
if (id) {
loadExisting(id, copyMode.value)
}
})
})
</script>
<style scoped>
.wizard-steps {
margin-bottom: 20px;
background: #fff;
padding: 20px 12px 12px;
border-radius: 6px;
}
.wizard-actions {
text-align: center;
padding: 8px 0 24px;
}
.save-progress__msg {
margin: 12px 0 4px;
color: #606266;
font-size: 13px;
word-break: break-all;
}
.save-progress__hint {
margin: 0;
color: #909399;
font-size: 12px;
}
</style>