322 lines
9.7 KiB
Vue
322 lines
9.7 KiB
Vue
<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>
|