424 lines
13 KiB
Vue
424 lines
13 KiB
Vue
<template>
|
||
<div class="app-container">
|
||
<el-steps
|
||
v-if="!newVersionMode"
|
||
: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"
|
||
:products="form.products"
|
||
:price-set-id="form.priceSetId"
|
||
:psv-id="form.version.psvId"
|
||
@update:psv-id="onPsvIdUpdate"
|
||
:set-type="form.basic.setType"
|
||
:price-build-mode="form.version.priceBuildMode"
|
||
@update:price-build-mode="onPriceBuildModeUpdate"
|
||
:version-no="form.version.versionNo"
|
||
:ref-data="refData"
|
||
:live-persist="livePersist"
|
||
:header-title="detailsHeaderTitle"
|
||
/>
|
||
|
||
<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" :loading="creating" @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 } 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 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')
|
||
const newVersionMode = computed(() => String(route.query.newVersion || '') === '1')
|
||
/** copy / newVersion: defer DB write until finish (same as create step2) */
|
||
const deferPersist = computed(() => copyMode.value || newVersionMode.value)
|
||
/** Entered from detail: continue-config / copy-version / new-version jump to step2 */
|
||
const directStep2 = computed(() => deferPersist.value || String(route.query.step || '') === '2')
|
||
/** Edit existing version: live PUT/pull/import. New create / copy / newVersion: defer until finish. */
|
||
const livePersist = computed(() => !!form.priceSetId && !deferPersist.value)
|
||
|
||
const step1Title = '基本信息'
|
||
const step2Title = '编辑产品价格'
|
||
const detailsHeaderTitle = computed(() =>
|
||
newVersionMode.value ? '编辑产品价格' : '第 2 步 · 编辑产品价格'
|
||
)
|
||
|
||
const form = reactive({
|
||
priceSetId: undefined,
|
||
basic: createEmptyBasicInfo(),
|
||
version: { psvId: undefined, versionNo: 'V1', effectiveTime: undefined, expireTime: undefined, priceBuildMode: undefined },
|
||
details: [],
|
||
products: [],
|
||
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: validate only; no DB write on create (defer until finish). */
|
||
async function goStep2() {
|
||
const validBasic = await basicRef.value.validate()
|
||
if (!validBasic) return
|
||
if (!(form.products || []).some(p => p && p.productCode) && !form.version.priceBuildMode) {
|
||
form.details = []
|
||
form.products = []
|
||
}
|
||
active.value = 1
|
||
}
|
||
|
||
/** Step2 finish: create/copy async save all; edit already live-saved. */
|
||
async function finishEdit() {
|
||
if (livePersist.value) {
|
||
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)
|
||
}
|
||
return
|
||
}
|
||
const validBasic = await basicRef.value.validate()
|
||
if (!validBasic) {
|
||
active.value = 0
|
||
return
|
||
}
|
||
if (!form.version.priceBuildMode) {
|
||
proxy.$modal.msgWarning('请先选择创建价格方式')
|
||
return
|
||
}
|
||
const detailRows = (form.details || []).filter(d => d && d.productCode)
|
||
if (!detailRows.length) {
|
||
proxy.$modal.msgWarning('请先拉取产品或导入 Excel 建价')
|
||
return
|
||
}
|
||
creating.value = true
|
||
try {
|
||
const payload = buildPayload({ forceDetails: true })
|
||
const priceSetId = await runSaveWithProgress(payload)
|
||
form.priceSetId = priceSetId
|
||
proxy.$modal.msgSuccess('套餐已保存')
|
||
const obj = {
|
||
path: '/customer/setDetail',
|
||
query: { priceSetId }
|
||
}
|
||
if (proxy.$tab) {
|
||
proxy.$tab.closeOpenPage(obj)
|
||
} else {
|
||
proxy.$router.push(obj)
|
||
}
|
||
} catch (e) {
|
||
// surfaced
|
||
} finally {
|
||
creating.value = false
|
||
}
|
||
}
|
||
|
||
/** 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(opts = {}) {
|
||
const { basic, version } = form
|
||
const detailRows = form.details.filter(d => d && d.productCode)
|
||
// Edit live-save: omit details when all rows already have psdId.
|
||
// Create finish: forceDetails sends all rows (psdId cleared) for insert.
|
||
const hasUnsavedDetail = detailRows.some(d => d.psdId == null)
|
||
const payload = {
|
||
priceSetId: form.priceSetId,
|
||
priceSetName: basic.priceSetName,
|
||
setType: 'FIXED',
|
||
defaultDiscount: undefined,
|
||
priceSetNote: basic.priceSetNote,
|
||
version: {
|
||
psvId: copyMode.value ? undefined : version.psvId,
|
||
versionNo: version.versionNo,
|
||
priceBuildMode: version.priceBuildMode || undefined
|
||
}
|
||
}
|
||
if (opts.forceDetails || hasUnsavedDetail) {
|
||
payload.details = detailRows.map(d => {
|
||
const p = toDetailPayload(d)
|
||
if (opts.forceDetails) {
|
||
delete p.psdId
|
||
delete p.pspId
|
||
delete p.priceSetId
|
||
delete p.psvId
|
||
}
|
||
return p
|
||
})
|
||
}
|
||
const customers = form.customers.filter(c => c && c.customerCode).map(toCustomerPayload)
|
||
if (customers.length) {
|
||
payload.customers = customers
|
||
}
|
||
return payload
|
||
}
|
||
|
||
function goBack() {
|
||
const obj = (newVersionMode.value || copyMode.value) && form.priceSetId
|
||
? { path: '/customer/setDetail', query: { priceSetId: form.priceSetId } }
|
||
: { path: '/customer/set' }
|
||
if (proxy.$tab) {
|
||
proxy.$tab.closeOpenPage(obj)
|
||
} else {
|
||
proxy.$router.push(obj)
|
||
}
|
||
}
|
||
|
||
function onPsvIdUpdate(psvId) {
|
||
if (psvId != null && psvId !== '') form.version.psvId = psvId
|
||
}
|
||
|
||
/** Step2 chose PULL / IMPORT: edit live-saves mode; create only local + optional preview pull. */
|
||
async function onPriceBuildModeUpdate(mode) {
|
||
if (!mode) return
|
||
const prev = form.version.priceBuildMode
|
||
if (prev === mode) return
|
||
form.version.priceBuildMode = mode
|
||
try {
|
||
// Create/copy: switching build mode clears the product table before reload/import.
|
||
if (!livePersist.value && prev) {
|
||
form.details.splice(0, form.details.length)
|
||
form.products.splice(0, form.products.length)
|
||
if (detailsRef.value && detailsRef.value.clearLocalDraft) {
|
||
detailsRef.value.clearLocalDraft()
|
||
}
|
||
}
|
||
if (livePersist.value) {
|
||
const payload = buildPayload()
|
||
delete payload.pullFromLibrary
|
||
delete payload.forcePullLibrary
|
||
delete payload.details
|
||
await runSaveWithProgress(payload)
|
||
}
|
||
await nextTick()
|
||
if (mode === 'PULL' && detailsRef.value && detailsRef.value.pullAllFromLibrary) {
|
||
const hasProducts = (form.products || []).some(p => p && p.productCode)
|
||
if (!hasProducts) {
|
||
await detailsRef.value.pullAllFromLibrary({ silent: true })
|
||
proxy.$modal.msgSuccess('已拉取产品列表,可继续编辑价格')
|
||
} else if (prev !== mode) {
|
||
proxy.$modal.msgSuccess('已切换为按产品价建价,可导入 Excel 修改价格')
|
||
}
|
||
} else if (mode === 'IMPORT' && prev !== mode) {
|
||
proxy.$modal.msgSuccess('请导入 Excel 建价')
|
||
}
|
||
} catch (e) {
|
||
form.version.priceBuildMode = prev
|
||
}
|
||
}
|
||
|
||
function applySetData(data, asCopy, asNewVersion) {
|
||
form.priceSetId = data.priceSetId
|
||
Object.assign(form.basic, {
|
||
priceSetName: data.priceSetName,
|
||
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 fromData = (data.version && data.version.psvId) ? data.version : null
|
||
const current = byQuery || fromData || versions[0] || {}
|
||
if (asCopy) {
|
||
form.version.psvId = undefined
|
||
form.version.versionNo = nextVersionNo(versions)
|
||
form.version.effectiveTime = undefined
|
||
form.version.expireTime = undefined
|
||
form.version.priceBuildMode = undefined
|
||
form.details = []
|
||
form.products = []
|
||
} else if (asNewVersion) {
|
||
// Pre-created empty draft from detail page; same step2 UX as create package
|
||
form.version.psvId = current.psvId
|
||
form.version.versionNo = current.versionNo || nextVersionNo(versions)
|
||
form.version.effectiveTime = undefined
|
||
form.version.expireTime = undefined
|
||
form.version.priceBuildMode = undefined
|
||
form.details = []
|
||
form.products = []
|
||
} else {
|
||
form.version.psvId = current.psvId
|
||
form.version.versionNo = current.versionNo || nextVersionNo(versions)
|
||
form.version.effectiveTime = current.effectiveTime
|
||
form.version.expireTime = current.expireTime
|
||
form.version.priceBuildMode = current.priceBuildMode || undefined
|
||
form.details = []
|
||
form.products = (current.products || []).map(p => ({ ...p }))
|
||
}
|
||
form.customers = (data.customers || []).map(c => ({ ...c }))
|
||
}
|
||
|
||
function loadExisting(id, asCopy, asNewVersion) {
|
||
getPriceSet(id).then(res => {
|
||
applySetData(res.data || {}, asCopy, asNewVersion)
|
||
if (asCopy || asNewVersion || String(route.query.step || '') === '2') {
|
||
active.value = 1
|
||
}
|
||
})
|
||
}
|
||
|
||
onMounted(() => {
|
||
refData.loadProducts().finally(() => {
|
||
const id = route.query.priceSetId
|
||
if (id) {
|
||
loadExisting(id, copyMode.value, newVersionMode.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>
|