Files
BMS/bms-front/src/views/price/set/create.vue
T

424 lines
13 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
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>