Merge branch 'test' into feauture-20261104
# Conflicts: # bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcProfitReportServiceImpl.java
This commit is contained in:
@@ -102,31 +102,81 @@ export function getLocalRebillTask(taskId) {
|
||||
})
|
||||
}
|
||||
|
||||
export function cancelLocalRebill(taskId) {
|
||||
return request({
|
||||
url: '/price/billing/orderBill/rebill/task/' + taskId + '/cancel',
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
/** One in-flight poll per taskId — avoids stacked loops that flood XHR and freeze the page. */
|
||||
const localRebillPollControllers = new Map()
|
||||
|
||||
/**
|
||||
* Poll local rebill task until terminal status.
|
||||
* @param {string} taskId
|
||||
* @param {(task: object) => void} [onProgress]
|
||||
*/
|
||||
export async function pollLocalRebillTask(taskId, onProgress) {
|
||||
const id = String(taskId || '')
|
||||
if (!id) throw new Error('missing local rebill taskId')
|
||||
|
||||
const prev = localRebillPollControllers.get(id)
|
||||
if (prev) prev.aborted = true
|
||||
const ctrl = { aborted: false }
|
||||
localRebillPollControllers.set(id, ctrl)
|
||||
|
||||
const started = Date.now()
|
||||
const maxMs = 30 * 60 * 1000
|
||||
let lastSig = ''
|
||||
try {
|
||||
while (Date.now() - started < maxMs) {
|
||||
if (ctrl.aborted) {
|
||||
const err = new Error('local rebill poll superseded')
|
||||
err.superseded = true
|
||||
throw err
|
||||
}
|
||||
// Hidden tab: slow down; no need to hammer Redis while user is away.
|
||||
const hidden = typeof document !== 'undefined' && document.visibilityState === 'hidden'
|
||||
const res = await getLocalRebillTask(id)
|
||||
const task = res.data || {}
|
||||
const st = String(task.status || '').toUpperCase()
|
||||
const sig = [st, task.percent, task.success, task.fail, task.total].join('|')
|
||||
if (typeof onProgress === 'function' && sig !== lastSig) {
|
||||
lastSig = sig
|
||||
onProgress(task)
|
||||
}
|
||||
if (st === 'SUCCESS') return { task }
|
||||
if (st === 'CANCELLED') return { task, cancelled: true }
|
||||
if (st === 'FAIL') {
|
||||
const err = new Error(task.error || task.message || 'local rebill failed')
|
||||
err.task = task
|
||||
throw err
|
||||
}
|
||||
// 1s visible / 2.5s hidden — dense 400ms polls were freezing the list page.
|
||||
await new Promise(resolve => setTimeout(resolve, hidden ? 2500 : 1000))
|
||||
}
|
||||
throw new Error('local rebill timeout')
|
||||
} finally {
|
||||
if (localRebillPollControllers.get(id) === ctrl) {
|
||||
localRebillPollControllers.delete(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Async local rebill; poll until done.
|
||||
* @param {object} data LocalRebillRequest
|
||||
* @param {(task: object) => void} [onProgress]
|
||||
* @param {{ getTaskId?: (taskId: string) => void }} [options]
|
||||
*/
|
||||
export async function localRebillAsync(data, onProgress) {
|
||||
export async function localRebillAsync(data, onProgress, options) {
|
||||
const submitRes = await submitLocalRebill(data)
|
||||
const d = submitRes && submitRes.data
|
||||
const taskId = d && (d.taskId || d)
|
||||
if (!taskId) throw new Error('missing local rebill taskId')
|
||||
const started = Date.now()
|
||||
const maxMs = 30 * 60 * 1000
|
||||
while (Date.now() - started < maxMs) {
|
||||
const res = await getLocalRebillTask(String(taskId))
|
||||
const task = res.data || {}
|
||||
if (typeof onProgress === 'function') onProgress(task)
|
||||
if (task.status === 'SUCCESS') return { task }
|
||||
if (task.status === 'FAIL') {
|
||||
const err = new Error(task.error || task.message || 'local rebill failed')
|
||||
err.task = task
|
||||
throw err
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 800))
|
||||
}
|
||||
throw new Error('local rebill timeout')
|
||||
if (options && typeof options.getTaskId === 'function') options.getTaskId(String(taskId))
|
||||
return pollLocalRebillTask(String(taskId), onProgress)
|
||||
}
|
||||
|
||||
/** Receivable bill detail */
|
||||
|
||||
@@ -0,0 +1,484 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/** Build price-set style API against a controller base path (e.g. /price/set, /price/exclusive). */
|
||||
export function createPriceSetApi(basePath) {
|
||||
const base = String(basePath || '/price/set').replace(/\/$/, '')
|
||||
function listPriceSet(query) {
|
||||
return request({
|
||||
url: base + '/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
function getPriceSet(priceSetId) {
|
||||
return request({
|
||||
url: base + '/' + priceSetId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
function savePriceSet(data) {
|
||||
return request({
|
||||
url: base + '',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
function getPriceSetSaveTask(taskId) {
|
||||
return request({
|
||||
url: base + '/save/task/' + taskId,
|
||||
method: 'get',
|
||||
timeout: 10 * 60 * 1000
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit save async and poll Redis progress until SUCCESS/FAIL.
|
||||
* @returns {Promise<{ priceSetId: number|string, task: object }>}
|
||||
*/
|
||||
async function savePriceSetAsync(data, onProgress) {
|
||||
const submitRes = await savePriceSet(data)
|
||||
const taskId = pickSaveTaskId(submitRes)
|
||||
if (!taskId) {
|
||||
throw new Error('missing save taskId')
|
||||
}
|
||||
const started = Date.now()
|
||||
const maxMs = 30 * 60 * 1000
|
||||
while (Date.now() - started < maxMs) {
|
||||
const res = await getPriceSetSaveTask(taskId)
|
||||
const task = res.data || {}
|
||||
if (typeof onProgress === 'function') {
|
||||
onProgress(task)
|
||||
}
|
||||
if (task.status === 'SUCCESS') {
|
||||
return { priceSetId: task.priceSetId, task }
|
||||
}
|
||||
if (task.status === 'FAIL') {
|
||||
const err = new Error(task.error || task.message || 'save failed')
|
||||
err.task = task
|
||||
throw err
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 400))
|
||||
}
|
||||
throw new Error('save timeout')
|
||||
}
|
||||
|
||||
/** Accept data.taskId / data string / legacy msg UUID (Bms String overload). */
|
||||
function pickSaveTaskId(res) {
|
||||
if (!res) return null
|
||||
const d = res.data
|
||||
if (d && typeof d === 'object' && d.taskId) return String(d.taskId)
|
||||
if (typeof d === 'string' && d.trim()) return d.trim()
|
||||
if (typeof res.msg === 'string') {
|
||||
const m = res.msg.trim()
|
||||
if (/^[a-f0-9]{32}$/i.test(m)) return m
|
||||
if (/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(m)) {
|
||||
return m.replace(/-/g, '')
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function updatePriceSet(data) {
|
||||
return request({
|
||||
url: base + '',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
/** Update package name and note only. */
|
||||
function updatePriceSetBasic(data) {
|
||||
return request({
|
||||
url: base + '/basic',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
function delPriceSet(priceSetIds) {
|
||||
return request({
|
||||
url: base + '/' + priceSetIds,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
function submitPriceSet(priceSetId) {
|
||||
return request({
|
||||
url: base + '/submit/' + priceSetId,
|
||||
method: 'put'
|
||||
})
|
||||
}
|
||||
|
||||
function approvePriceSet(priceSetId) {
|
||||
return request({
|
||||
url: base + '/approve/' + priceSetId,
|
||||
method: 'put'
|
||||
})
|
||||
}
|
||||
|
||||
function rejectPriceSet(priceSetId) {
|
||||
return request({
|
||||
url: base + '/reject/' + priceSetId,
|
||||
method: 'put'
|
||||
})
|
||||
}
|
||||
|
||||
function activatePriceSetVersion(psvId, data) {
|
||||
return request({
|
||||
url: base + '/version/activate/' + psvId,
|
||||
method: 'put',
|
||||
data: data || {}
|
||||
})
|
||||
}
|
||||
|
||||
function copyPriceSetVersion(psvId) {
|
||||
return request({
|
||||
url: base + '/version/copy/' + psvId,
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
/** Create empty draft version under package */
|
||||
function createPriceSetVersion(priceSetId) {
|
||||
return request({
|
||||
url: base + '/version/create/' + priceSetId,
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
/** Import excel attachments for a price-set version (via prc_price_set_product.psv_id). */
|
||||
function listPriceSetVersionExcelFiles(psvId) {
|
||||
return request({
|
||||
url: base + '/version/' + psvId + '/excelFiles',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
function deletePriceSetVersion(psvId) {
|
||||
return request({
|
||||
url: base + '/version/' + psvId,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
function disablePriceSet(priceSetId) {
|
||||
return request({
|
||||
url: base + '/disable/' + priceSetId,
|
||||
method: 'put'
|
||||
})
|
||||
}
|
||||
|
||||
function enablePriceSet(priceSetId) {
|
||||
return request({
|
||||
url: base + '/enable/' + priceSetId,
|
||||
method: 'put'
|
||||
})
|
||||
}
|
||||
|
||||
function copyPriceSet(priceSetId) {
|
||||
return request({
|
||||
url: base + '/copy/' + priceSetId,
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
function validatePriceSet(data) {
|
||||
return request({
|
||||
url: base + '/validate',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
function listSetDetail(query) {
|
||||
return request({
|
||||
url: base + '/detail/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
/** All detail rows for one product under a version (no pagination). */
|
||||
function listSetDetailByProduct(psvId, productCode) {
|
||||
return request({
|
||||
url: base + '/detail/byProduct',
|
||||
method: 'get',
|
||||
params: { psvId, productCode }
|
||||
})
|
||||
}
|
||||
|
||||
/** All detail rows for a version (no pagination); one call instead of N byProduct. */
|
||||
function listSetDetailByVersion(psvId) {
|
||||
return request({
|
||||
url: base + '/detail/byVersion',
|
||||
method: 'get',
|
||||
params: { psvId }
|
||||
})
|
||||
}
|
||||
|
||||
/** 版本明细搜索下拉选项:按版本去重后的 产品/国家/分区 */
|
||||
function listSetDetailOptions(psvId) {
|
||||
return request({
|
||||
url: base + '/detail/options',
|
||||
method: 'get',
|
||||
params: { psvId }
|
||||
})
|
||||
}
|
||||
|
||||
function addSetDetail(data) {
|
||||
return request({
|
||||
url: base + '/detail',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Async import quote Excel into FIXED package detail prices; poll until done.
|
||||
* @param {File|Blob} file
|
||||
* @param {{ priceSetId: string|number, psvId?: string|number, updateOnly?: boolean, replaceByProductCountry?: boolean, countryCode?: string }} query
|
||||
* @param {(task: object) => void} [onProgress]
|
||||
* @returns {Promise<{ task: object }>}
|
||||
*/
|
||||
function appendUploadFiles(formData, file) {
|
||||
const list = Array.isArray(file) ? file : [file]
|
||||
for (const item of list) {
|
||||
const raw = item && (item.raw || item)
|
||||
if (raw) formData.append('files', raw)
|
||||
}
|
||||
}
|
||||
|
||||
async function importSetDetailPricesAsync(file, query, onProgress) {
|
||||
const formData = new FormData()
|
||||
appendUploadFiles(formData, file)
|
||||
const params = new URLSearchParams()
|
||||
if (query && query.priceSetId != null) params.set('priceSetId', query.priceSetId)
|
||||
if (query && query.psvId != null && query.psvId !== '') params.set('psvId', query.psvId)
|
||||
if (query && query.updateOnly) params.set('updateOnly', 'true')
|
||||
// Package Excel build-price only (product+country replace scope)
|
||||
if (query && query.replaceByProductCountry) params.set('replaceByProductCountry', 'true')
|
||||
if (query && query.countryCode) params.set('countryCode', query.countryCode)
|
||||
const submitRes = await request({
|
||||
url: base + '/detail/importPrices?' + params.toString(),
|
||||
method: 'post',
|
||||
data: formData,
|
||||
headers: { 'Content-Type': 'multipart/form-data', repeatSubmit: false }
|
||||
})
|
||||
const taskId = pickSaveTaskId(submitRes)
|
||||
if (!taskId) {
|
||||
throw new Error('missing import taskId')
|
||||
}
|
||||
return pollPriceSetSaveTask(taskId, onProgress)
|
||||
}
|
||||
|
||||
/**
|
||||
* Preview pull product library (no DB write). Returns { products, details, message }.
|
||||
*/
|
||||
function previewPullFromLibrary() {
|
||||
return request({
|
||||
url: base + '/detail/previewPullFromLibrary',
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Preview Excel import (no DB write).
|
||||
* @param {File|Blob} file
|
||||
* @param {{ updateOnly?: boolean, replaceByProductCountry?: boolean, countryCode?: string, details?: Array }} [opts]
|
||||
*/
|
||||
function previewImportDetailPrices(file, opts = {}) {
|
||||
const formData = new FormData()
|
||||
appendUploadFiles(formData, file)
|
||||
if (opts.updateOnly) formData.append('updateOnly', 'true')
|
||||
if (opts.replaceByProductCountry) formData.append('replaceByProductCountry', 'true')
|
||||
if (opts.countryCode) formData.append('countryCode', opts.countryCode)
|
||||
// updateOnly / Excel build-price merge need current draft details
|
||||
if ((opts.updateOnly || opts.replaceByProductCountry || opts.countryCode) && Array.isArray(opts.details)) {
|
||||
formData.append('detailsJson', JSON.stringify(opts.details))
|
||||
}
|
||||
return request({
|
||||
url: base + '/detail/previewImportPrices',
|
||||
method: 'post',
|
||||
data: formData,
|
||||
// FormData must not go through repeat-submit JSON.stringify (collapses to "{}")
|
||||
headers: { 'Content-Type': 'multipart/form-data', repeatSubmit: false },
|
||||
timeout: 10 * 60 * 1000
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Async pull product library into package version details; poll until done.
|
||||
* @param {{ priceSetId: string|number, psvId?: string|number, force?: boolean }} query
|
||||
* @param {(task: object) => void} [onProgress]
|
||||
*/
|
||||
async function pullSetDetailsFromLibraryAsync(query, onProgress) {
|
||||
const params = new URLSearchParams()
|
||||
if (query && query.priceSetId != null) params.set('priceSetId', query.priceSetId)
|
||||
if (query && query.psvId != null && query.psvId !== '') params.set('psvId', query.psvId)
|
||||
params.set('force', query && query.force ? 'true' : 'false')
|
||||
const submitRes = await request({
|
||||
url: base + '/detail/pullFromLibrary?' + params.toString(),
|
||||
method: 'post'
|
||||
})
|
||||
const taskId = pickSaveTaskId(submitRes)
|
||||
if (!taskId) {
|
||||
throw new Error('missing pull taskId')
|
||||
}
|
||||
return pollPriceSetSaveTask(taskId, onProgress)
|
||||
}
|
||||
|
||||
async function pollPriceSetSaveTask(taskId, onProgress) {
|
||||
const started = Date.now()
|
||||
const maxMs = 30 * 60 * 1000
|
||||
while (Date.now() - started < maxMs) {
|
||||
const res = await getPriceSetSaveTask(taskId)
|
||||
const task = res.data || {}
|
||||
if (typeof onProgress === 'function') {
|
||||
onProgress(task)
|
||||
}
|
||||
if (task.status === 'SUCCESS') {
|
||||
return { task }
|
||||
}
|
||||
if (task.status === 'FAIL') {
|
||||
const err = new Error(task.error || task.message || 'task failed')
|
||||
err.task = task
|
||||
throw err
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 400))
|
||||
}
|
||||
throw new Error('task timeout')
|
||||
}
|
||||
|
||||
function updateSetDetail(data) {
|
||||
return request({
|
||||
url: base + '/detail',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit async batch write of detail surcharge snapshots and poll until done.
|
||||
* @returns {Promise<{ task: object }>}
|
||||
*/
|
||||
async function updateSetDetailSurchargeAsync(details, onProgress) {
|
||||
const submitRes = await request({
|
||||
url: base + '/detail/surcharge/async',
|
||||
method: 'post',
|
||||
data: details || []
|
||||
})
|
||||
const taskId = pickSaveTaskId(submitRes)
|
||||
if (!taskId) {
|
||||
throw new Error('missing surcharge taskId')
|
||||
}
|
||||
const started = Date.now()
|
||||
const maxMs = 30 * 60 * 1000
|
||||
while (Date.now() - started < maxMs) {
|
||||
const res = await getPriceSetSaveTask(taskId)
|
||||
const task = res.data || {}
|
||||
if (typeof onProgress === 'function') {
|
||||
onProgress(task)
|
||||
}
|
||||
if (task.status === 'SUCCESS') {
|
||||
return { task }
|
||||
}
|
||||
if (task.status === 'FAIL') {
|
||||
const err = new Error(task.error || task.message || 'surcharge save failed')
|
||||
err.task = task
|
||||
throw err
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 400))
|
||||
}
|
||||
throw new Error('surcharge save timeout')
|
||||
}
|
||||
|
||||
function delSetDetail(psdIds) {
|
||||
return request({
|
||||
url: base + '/detail/' + psdIds,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
function listSetCustomer(query) {
|
||||
return request({
|
||||
url: base + '/customer/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
function addSetCustomer(data) {
|
||||
return request({
|
||||
url: base + '/customer',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
function batchAddSetCustomer(data) {
|
||||
return request({
|
||||
url: base + '/customer/batch',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
function updateSetCustomer(data) {
|
||||
return request({
|
||||
url: base + '/customer',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
function delSetCustomer(ids) {
|
||||
return request({
|
||||
url: base + '/customer/' + ids,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
listPriceSet,
|
||||
getPriceSet,
|
||||
savePriceSet,
|
||||
getPriceSetSaveTask,
|
||||
savePriceSetAsync,
|
||||
updatePriceSet,
|
||||
updatePriceSetBasic,
|
||||
delPriceSet,
|
||||
submitPriceSet,
|
||||
approvePriceSet,
|
||||
rejectPriceSet,
|
||||
activatePriceSetVersion,
|
||||
copyPriceSetVersion,
|
||||
createPriceSetVersion,
|
||||
listPriceSetVersionExcelFiles,
|
||||
deletePriceSetVersion,
|
||||
disablePriceSet,
|
||||
enablePriceSet,
|
||||
copyPriceSet,
|
||||
validatePriceSet,
|
||||
listSetDetail,
|
||||
listSetDetailByProduct,
|
||||
listSetDetailByVersion,
|
||||
listSetDetailOptions,
|
||||
addSetDetail,
|
||||
importSetDetailPricesAsync,
|
||||
previewPullFromLibrary,
|
||||
previewImportDetailPrices,
|
||||
pullSetDetailsFromLibraryAsync,
|
||||
updateSetDetail,
|
||||
updateSetDetailSurchargeAsync,
|
||||
delSetDetail,
|
||||
listSetCustomer,
|
||||
addSetCustomer,
|
||||
batchAddSetCustomer,
|
||||
updateSetCustomer,
|
||||
delSetCustomer
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { createPriceSetApi } from './createPriceSetApi'
|
||||
|
||||
export const exclusiveQuoteApi = createPriceSetApi('/price/exclusive')
|
||||
|
||||
export const {
|
||||
listPriceSet,
|
||||
getPriceSet,
|
||||
savePriceSet,
|
||||
getPriceSetSaveTask,
|
||||
savePriceSetAsync,
|
||||
updatePriceSet,
|
||||
updatePriceSetBasic,
|
||||
delPriceSet,
|
||||
submitPriceSet,
|
||||
approvePriceSet,
|
||||
rejectPriceSet,
|
||||
activatePriceSetVersion,
|
||||
copyPriceSetVersion,
|
||||
createPriceSetVersion,
|
||||
listPriceSetVersionExcelFiles,
|
||||
deletePriceSetVersion,
|
||||
disablePriceSet,
|
||||
enablePriceSet,
|
||||
copyPriceSet,
|
||||
validatePriceSet,
|
||||
listSetDetail,
|
||||
listSetDetailByProduct,
|
||||
listSetDetailByVersion,
|
||||
listSetDetailOptions,
|
||||
addSetDetail,
|
||||
importSetDetailPricesAsync,
|
||||
previewPullFromLibrary,
|
||||
previewImportDetailPrices,
|
||||
pullSetDetailsFromLibraryAsync,
|
||||
updateSetDetail,
|
||||
updateSetDetailSurchargeAsync,
|
||||
delSetDetail,
|
||||
listSetCustomer,
|
||||
addSetCustomer,
|
||||
batchAddSetCustomer,
|
||||
updateSetCustomer,
|
||||
delSetCustomer
|
||||
} = exclusiveQuoteApi
|
||||
+41
-438
@@ -1,440 +1,43 @@
|
||||
import request from '@/utils/request'
|
||||
import { createPriceSetApi } from './createPriceSetApi'
|
||||
|
||||
export function listPriceSet(query) {
|
||||
return request({
|
||||
url: '/price/set/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
export const priceSetApi = createPriceSetApi('/price/set')
|
||||
|
||||
export function getPriceSet(priceSetId) {
|
||||
return request({
|
||||
url: '/price/set/' + priceSetId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function savePriceSet(data) {
|
||||
return request({
|
||||
url: '/price/set',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function getPriceSetSaveTask(taskId) {
|
||||
return request({
|
||||
url: '/price/set/save/task/' + taskId,
|
||||
method: 'get',
|
||||
timeout: 10 * 60 * 1000
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit save async and poll Redis progress until SUCCESS/FAIL.
|
||||
* @returns {Promise<{ priceSetId: number|string, task: object }>}
|
||||
*/
|
||||
export async function savePriceSetAsync(data, onProgress) {
|
||||
const submitRes = await savePriceSet(data)
|
||||
const taskId = pickSaveTaskId(submitRes)
|
||||
if (!taskId) {
|
||||
throw new Error('missing save taskId')
|
||||
}
|
||||
const started = Date.now()
|
||||
const maxMs = 30 * 60 * 1000
|
||||
while (Date.now() - started < maxMs) {
|
||||
const res = await getPriceSetSaveTask(taskId)
|
||||
const task = res.data || {}
|
||||
if (typeof onProgress === 'function') {
|
||||
onProgress(task)
|
||||
}
|
||||
if (task.status === 'SUCCESS') {
|
||||
return { priceSetId: task.priceSetId, task }
|
||||
}
|
||||
if (task.status === 'FAIL') {
|
||||
const err = new Error(task.error || task.message || 'save failed')
|
||||
err.task = task
|
||||
throw err
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 400))
|
||||
}
|
||||
throw new Error('save timeout')
|
||||
}
|
||||
|
||||
/** Accept data.taskId / data string / legacy msg UUID (Bms String overload). */
|
||||
function pickSaveTaskId(res) {
|
||||
if (!res) return null
|
||||
const d = res.data
|
||||
if (d && typeof d === 'object' && d.taskId) return String(d.taskId)
|
||||
if (typeof d === 'string' && d.trim()) return d.trim()
|
||||
if (typeof res.msg === 'string') {
|
||||
const m = res.msg.trim()
|
||||
if (/^[a-f0-9]{32}$/i.test(m)) return m
|
||||
if (/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(m)) {
|
||||
return m.replace(/-/g, '')
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function updatePriceSet(data) {
|
||||
return request({
|
||||
url: '/price/set',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
/** Update package name and note only. */
|
||||
export function updatePriceSetBasic(data) {
|
||||
return request({
|
||||
url: '/price/set/basic',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function delPriceSet(priceSetIds) {
|
||||
return request({
|
||||
url: '/price/set/' + priceSetIds,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
export function submitPriceSet(priceSetId) {
|
||||
return request({
|
||||
url: '/price/set/submit/' + priceSetId,
|
||||
method: 'put'
|
||||
})
|
||||
}
|
||||
|
||||
export function approvePriceSet(priceSetId) {
|
||||
return request({
|
||||
url: '/price/set/approve/' + priceSetId,
|
||||
method: 'put'
|
||||
})
|
||||
}
|
||||
|
||||
export function rejectPriceSet(priceSetId) {
|
||||
return request({
|
||||
url: '/price/set/reject/' + priceSetId,
|
||||
method: 'put'
|
||||
})
|
||||
}
|
||||
|
||||
export function activatePriceSetVersion(psvId, data) {
|
||||
return request({
|
||||
url: '/price/set/version/activate/' + psvId,
|
||||
method: 'put',
|
||||
data: data || {}
|
||||
})
|
||||
}
|
||||
|
||||
export function copyPriceSetVersion(psvId) {
|
||||
return request({
|
||||
url: '/price/set/version/copy/' + psvId,
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
/** Create empty draft version under package */
|
||||
export function createPriceSetVersion(priceSetId) {
|
||||
return request({
|
||||
url: '/price/set/version/create/' + priceSetId,
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
/** Import excel attachments for a price-set version (via prc_price_set_product.psv_id). */
|
||||
export function listPriceSetVersionExcelFiles(psvId) {
|
||||
return request({
|
||||
url: '/price/set/version/' + psvId + '/excelFiles',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function deletePriceSetVersion(psvId) {
|
||||
return request({
|
||||
url: '/price/set/version/' + psvId,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
export function disablePriceSet(priceSetId) {
|
||||
return request({
|
||||
url: '/price/set/disable/' + priceSetId,
|
||||
method: 'put'
|
||||
})
|
||||
}
|
||||
|
||||
export function enablePriceSet(priceSetId) {
|
||||
return request({
|
||||
url: '/price/set/enable/' + priceSetId,
|
||||
method: 'put'
|
||||
})
|
||||
}
|
||||
|
||||
export function copyPriceSet(priceSetId) {
|
||||
return request({
|
||||
url: '/price/set/copy/' + priceSetId,
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
export function validatePriceSet(data) {
|
||||
return request({
|
||||
url: '/price/set/validate',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function listSetDetail(query) {
|
||||
return request({
|
||||
url: '/price/set/detail/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
/** All detail rows for one product under a version (no pagination). */
|
||||
export function listSetDetailByProduct(psvId, productCode) {
|
||||
return request({
|
||||
url: '/price/set/detail/byProduct',
|
||||
method: 'get',
|
||||
params: { psvId, productCode }
|
||||
})
|
||||
}
|
||||
|
||||
/** All detail rows for a version (no pagination); one call instead of N byProduct. */
|
||||
export function listSetDetailByVersion(psvId) {
|
||||
return request({
|
||||
url: '/price/set/detail/byVersion',
|
||||
method: 'get',
|
||||
params: { psvId }
|
||||
})
|
||||
}
|
||||
|
||||
/** 版本明细搜索下拉选项:按版本去重后的 产品/国家/分区 */
|
||||
export function listSetDetailOptions(psvId) {
|
||||
return request({
|
||||
url: '/price/set/detail/options',
|
||||
method: 'get',
|
||||
params: { psvId }
|
||||
})
|
||||
}
|
||||
|
||||
export function addSetDetail(data) {
|
||||
return request({
|
||||
url: '/price/set/detail',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Async import quote Excel into FIXED package detail prices; poll until done.
|
||||
* @param {File|Blob} file
|
||||
* @param {{ priceSetId: string|number, psvId?: string|number, updateOnly?: boolean, replaceByProductCountry?: boolean, countryCode?: string }} query
|
||||
* @param {(task: object) => void} [onProgress]
|
||||
* @returns {Promise<{ task: object }>}
|
||||
*/
|
||||
function appendUploadFiles(formData, file) {
|
||||
const list = Array.isArray(file) ? file : [file]
|
||||
for (const item of list) {
|
||||
const raw = item && (item.raw || item)
|
||||
if (raw) formData.append('files', raw)
|
||||
}
|
||||
}
|
||||
|
||||
export async function importSetDetailPricesAsync(file, query, onProgress) {
|
||||
const formData = new FormData()
|
||||
appendUploadFiles(formData, file)
|
||||
const params = new URLSearchParams()
|
||||
if (query && query.priceSetId != null) params.set('priceSetId', query.priceSetId)
|
||||
if (query && query.psvId != null && query.psvId !== '') params.set('psvId', query.psvId)
|
||||
if (query && query.updateOnly) params.set('updateOnly', 'true')
|
||||
// Package Excel build-price only (product+country replace scope)
|
||||
if (query && query.replaceByProductCountry) params.set('replaceByProductCountry', 'true')
|
||||
if (query && query.countryCode) params.set('countryCode', query.countryCode)
|
||||
const submitRes = await request({
|
||||
url: '/price/set/detail/importPrices?' + params.toString(),
|
||||
method: 'post',
|
||||
data: formData,
|
||||
headers: { 'Content-Type': 'multipart/form-data', repeatSubmit: false }
|
||||
})
|
||||
const taskId = pickSaveTaskId(submitRes)
|
||||
if (!taskId) {
|
||||
throw new Error('missing import taskId')
|
||||
}
|
||||
return pollPriceSetSaveTask(taskId, onProgress)
|
||||
}
|
||||
|
||||
/**
|
||||
* Preview pull product library (no DB write). Returns { products, details, message }.
|
||||
*/
|
||||
export function previewPullFromLibrary() {
|
||||
return request({
|
||||
url: '/price/set/detail/previewPullFromLibrary',
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Preview Excel import (no DB write).
|
||||
* @param {File|Blob} file
|
||||
* @param {{ updateOnly?: boolean, replaceByProductCountry?: boolean, countryCode?: string, details?: Array }} [opts]
|
||||
*/
|
||||
export function previewImportDetailPrices(file, opts = {}) {
|
||||
const formData = new FormData()
|
||||
appendUploadFiles(formData, file)
|
||||
if (opts.updateOnly) formData.append('updateOnly', 'true')
|
||||
if (opts.replaceByProductCountry) formData.append('replaceByProductCountry', 'true')
|
||||
if (opts.countryCode) formData.append('countryCode', opts.countryCode)
|
||||
// updateOnly / Excel build-price merge need current draft details
|
||||
if ((opts.updateOnly || opts.replaceByProductCountry || opts.countryCode) && Array.isArray(opts.details)) {
|
||||
formData.append('detailsJson', JSON.stringify(opts.details))
|
||||
}
|
||||
return request({
|
||||
url: '/price/set/detail/previewImportPrices',
|
||||
method: 'post',
|
||||
data: formData,
|
||||
// FormData must not go through repeat-submit JSON.stringify (collapses to "{}")
|
||||
headers: { 'Content-Type': 'multipart/form-data', repeatSubmit: false },
|
||||
timeout: 10 * 60 * 1000
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Async pull product library into package version details; poll until done.
|
||||
* @param {{ priceSetId: string|number, psvId?: string|number, force?: boolean }} query
|
||||
* @param {(task: object) => void} [onProgress]
|
||||
*/
|
||||
export async function pullSetDetailsFromLibraryAsync(query, onProgress) {
|
||||
const params = new URLSearchParams()
|
||||
if (query && query.priceSetId != null) params.set('priceSetId', query.priceSetId)
|
||||
if (query && query.psvId != null && query.psvId !== '') params.set('psvId', query.psvId)
|
||||
params.set('force', query && query.force ? 'true' : 'false')
|
||||
const submitRes = await request({
|
||||
url: '/price/set/detail/pullFromLibrary?' + params.toString(),
|
||||
method: 'post'
|
||||
})
|
||||
const taskId = pickSaveTaskId(submitRes)
|
||||
if (!taskId) {
|
||||
throw new Error('missing pull taskId')
|
||||
}
|
||||
return pollPriceSetSaveTask(taskId, onProgress)
|
||||
}
|
||||
|
||||
async function pollPriceSetSaveTask(taskId, onProgress) {
|
||||
const started = Date.now()
|
||||
const maxMs = 30 * 60 * 1000
|
||||
while (Date.now() - started < maxMs) {
|
||||
const res = await getPriceSetSaveTask(taskId)
|
||||
const task = res.data || {}
|
||||
if (typeof onProgress === 'function') {
|
||||
onProgress(task)
|
||||
}
|
||||
if (task.status === 'SUCCESS') {
|
||||
return { task }
|
||||
}
|
||||
if (task.status === 'FAIL') {
|
||||
const err = new Error(task.error || task.message || 'task failed')
|
||||
err.task = task
|
||||
throw err
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 400))
|
||||
}
|
||||
throw new Error('task timeout')
|
||||
}
|
||||
|
||||
export function updateSetDetail(data) {
|
||||
return request({
|
||||
url: '/price/set/detail',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit async batch write of detail surcharge snapshots and poll until done.
|
||||
* @returns {Promise<{ task: object }>}
|
||||
*/
|
||||
export async function updateSetDetailSurchargeAsync(details, onProgress) {
|
||||
const submitRes = await request({
|
||||
url: '/price/set/detail/surcharge/async',
|
||||
method: 'post',
|
||||
data: details || []
|
||||
})
|
||||
const taskId = pickSaveTaskId(submitRes)
|
||||
if (!taskId) {
|
||||
throw new Error('missing surcharge taskId')
|
||||
}
|
||||
const started = Date.now()
|
||||
const maxMs = 30 * 60 * 1000
|
||||
while (Date.now() - started < maxMs) {
|
||||
const res = await getPriceSetSaveTask(taskId)
|
||||
const task = res.data || {}
|
||||
if (typeof onProgress === 'function') {
|
||||
onProgress(task)
|
||||
}
|
||||
if (task.status === 'SUCCESS') {
|
||||
return { task }
|
||||
}
|
||||
if (task.status === 'FAIL') {
|
||||
const err = new Error(task.error || task.message || 'surcharge save failed')
|
||||
err.task = task
|
||||
throw err
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 400))
|
||||
}
|
||||
throw new Error('surcharge save timeout')
|
||||
}
|
||||
|
||||
export function delSetDetail(psdIds) {
|
||||
return request({
|
||||
url: '/price/set/detail/' + psdIds,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
export function listSetCustomer(query) {
|
||||
return request({
|
||||
url: '/price/set/customer/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
export function addSetCustomer(data) {
|
||||
return request({
|
||||
url: '/price/set/customer',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function batchAddSetCustomer(data) {
|
||||
return request({
|
||||
url: '/price/set/customer/batch',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function updateSetCustomer(data) {
|
||||
return request({
|
||||
url: '/price/set/customer',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function delSetCustomer(ids) {
|
||||
return request({
|
||||
url: '/price/set/customer/' + ids,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
export const {
|
||||
listPriceSet,
|
||||
getPriceSet,
|
||||
savePriceSet,
|
||||
getPriceSetSaveTask,
|
||||
savePriceSetAsync,
|
||||
updatePriceSet,
|
||||
updatePriceSetBasic,
|
||||
delPriceSet,
|
||||
submitPriceSet,
|
||||
approvePriceSet,
|
||||
rejectPriceSet,
|
||||
activatePriceSetVersion,
|
||||
copyPriceSetVersion,
|
||||
createPriceSetVersion,
|
||||
listPriceSetVersionExcelFiles,
|
||||
deletePriceSetVersion,
|
||||
disablePriceSet,
|
||||
enablePriceSet,
|
||||
copyPriceSet,
|
||||
validatePriceSet,
|
||||
listSetDetail,
|
||||
listSetDetailByProduct,
|
||||
listSetDetailByVersion,
|
||||
listSetDetailOptions,
|
||||
addSetDetail,
|
||||
importSetDetailPricesAsync,
|
||||
previewPullFromLibrary,
|
||||
previewImportDetailPrices,
|
||||
pullSetDetailsFromLibraryAsync,
|
||||
updateSetDetail,
|
||||
updateSetDetailSurchargeAsync,
|
||||
delSetDetail,
|
||||
listSetCustomer,
|
||||
addSetCustomer,
|
||||
batchAddSetCustomer,
|
||||
updateSetCustomer,
|
||||
delSetCustomer
|
||||
} = priceSetApi
|
||||
|
||||
@@ -19,6 +19,10 @@ export function listProfitCustomerOptions(params) {
|
||||
})
|
||||
}
|
||||
|
||||
export function listProfitAreaRules() {
|
||||
return request({ url: '/report/profit/area-rules', method: 'get' })
|
||||
}
|
||||
|
||||
export function listProfitSummary(params) {
|
||||
return request({
|
||||
url: '/report/profit/summary',
|
||||
|
||||
@@ -63,10 +63,10 @@
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="区域类型" prop="areaType">
|
||||
<el-select v-model="form.areaType" placeholder="请选择区域类型" clearable style="width: 100%;">
|
||||
<el-form-item label="客户区域" prop="customerArea">
|
||||
<el-select v-model="form.customerArea" placeholder="请选择客户区域" clearable style="width: 100%;">
|
||||
<el-option
|
||||
v-for="dict in data_area_type"
|
||||
v-for="dict in customer_area"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
@@ -739,7 +739,7 @@ const router = useRouter()
|
||||
|
||||
const { settlement_type } = useDict('settlement_type')
|
||||
const { customer_type } = useDict('customer_type')
|
||||
const { data_area_type } = useDict('data_area_type')
|
||||
const { customer_area } = useDict('customer_area')
|
||||
|
||||
const { ensureProducts, findProduct } = useProducts()
|
||||
|
||||
@@ -765,7 +765,7 @@ function buildEmptyForm() {
|
||||
customerCodeDw: null,
|
||||
creditCode: null,
|
||||
customerType: null,
|
||||
areaType: null,
|
||||
customerArea: null,
|
||||
isEnable: null,
|
||||
salesUserId: null,
|
||||
salesUser: null,
|
||||
|
||||
@@ -30,16 +30,16 @@
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="区域类型" prop="areaType">
|
||||
<el-form-item label="客户区域" prop="customerArea">
|
||||
<el-select
|
||||
v-model="queryParams.areaType"
|
||||
placeholder="请选择区域类型"
|
||||
v-model="queryParams.customerArea"
|
||||
placeholder="请选择客户区域"
|
||||
clearable
|
||||
style="width: 160px"
|
||||
@change="handleQuery"
|
||||
>
|
||||
<el-option
|
||||
v-for="dict in data_area_type"
|
||||
v-for="dict in customer_area"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
@@ -167,9 +167,9 @@
|
||||
<dict-tag :options="customer_type" :value="scope.row.customerType" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="isColVisible('areaType')" label="区域类型" align="center" prop="areaType" width="110">
|
||||
<el-table-column v-if="isColVisible('customerArea')" label="客户区域" align="center" prop="customerArea" width="110">
|
||||
<template #default="scope">
|
||||
<dict-tag :options="data_area_type" :value="scope.row.areaType" />
|
||||
<dict-tag :options="customer_area" :value="scope.row.customerArea" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="isColVisible('isEnable')" label="启用状态" align="center" prop="isEnable">
|
||||
@@ -349,7 +349,7 @@ const columnDefs = [
|
||||
{ key: 'customerCodeDw', label: '对外代码' },
|
||||
{ key: 'creditCode', label: '统一社会信用代码' },
|
||||
{ key: 'customerType', label: '业务类型' },
|
||||
{ key: 'areaType', label: '区域类型' },
|
||||
{ key: 'customerArea', label: '客户区域' },
|
||||
{ key: 'isEnable', label: '启用状态' },
|
||||
{ key: 'salesUserId', label: '业务员' },
|
||||
// { key: 'ed', label: '资金额度' },
|
||||
@@ -378,7 +378,7 @@ const single = ref(true)
|
||||
const multiple = ref(true)
|
||||
const total = ref(0)
|
||||
|
||||
const { sys_normal_disable, settlement_type, customer_type, data_area_type } = useDict("sys_normal_disable", "settlement_type", "customer_type", "data_area_type")
|
||||
const { sys_normal_disable, settlement_type, customer_type, customer_area } = useDict("sys_normal_disable", "settlement_type", "customer_type", "customer_area")
|
||||
|
||||
/** 绑定产品代码串 -> 产品名称展示(产品代码+名称;查不到的产品回显原代码) */
|
||||
const { ensureProducts, getProductLabel } = useProducts()
|
||||
@@ -412,7 +412,7 @@ const copyColumns = computed(() => [
|
||||
{ key: 'customerCodeDw', label: '对外代码', get: r => r.customerCodeDw || '' },
|
||||
{ key: 'creditCode', label: '统一社会信用代码', get: r => r.creditCode || '' },
|
||||
{ key: 'customerType', label: '业务类型', get: r => proxy.selectDictLabel(customer_type.value, r.customerType) },
|
||||
{ key: 'areaType', label: '区域类型', get: r => proxy.selectDictLabel(data_area_type.value, r.areaType) },
|
||||
{ key: 'customerArea', label: '客户区域', get: r => proxy.selectDictLabel(customer_area.value, r.customerArea) },
|
||||
{ key: 'isEnable', label: '启用状态', get: r => proxy.selectDictLabel(sys_normal_disable.value, r.isEnable) },
|
||||
{ key: 'salesUserId', label: '业务员', get: r => r.salesUserNickName || '' },
|
||||
{ key: 'zq', label: '回款账期', get: r => (r.zq === null || r.zq === undefined ? '' : r.zq) },
|
||||
@@ -445,7 +445,7 @@ const data = reactive({
|
||||
keywords: undefined,
|
||||
creditCode: undefined,
|
||||
customerType: undefined,
|
||||
areaType: undefined,
|
||||
customerArea: undefined,
|
||||
isEnable: undefined,
|
||||
salesUserId: undefined,
|
||||
salesUser: undefined,
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="dialogTitle"
|
||||
width="520px"
|
||||
width="480px"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
:close-on-click-modal="!loading"
|
||||
:close-on-press-escape="!loading"
|
||||
:show-close="!loading"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="true"
|
||||
:show-close="true"
|
||||
@closed="onClosed"
|
||||
>
|
||||
<el-form v-show="!loading && !finished" label-width="96px">
|
||||
@@ -28,7 +27,7 @@
|
||||
/>
|
||||
</el-form>
|
||||
|
||||
<div v-if="loading || finished" class="filter-retry-progress">
|
||||
<div v-if="loading || finished" class="sync-progress">
|
||||
<el-progress
|
||||
:percentage="progressPercent"
|
||||
:status="progressStatus"
|
||||
@@ -36,15 +35,14 @@
|
||||
striped
|
||||
:striped-flow="loading"
|
||||
/>
|
||||
<div class="filter-retry-progress__msg">{{ progressMessage }}</div>
|
||||
<div class="filter-retry-progress__text">
|
||||
<div class="sync-progress__msg">{{ progressMessage }}</div>
|
||||
<div class="sync-progress__text">
|
||||
总数 {{ progress.total || 0 }}
|
||||
/ 已处理 {{ progress.processed || 0 }}
|
||||
/ 成功 {{ progress.ok || 0 }}
|
||||
/ 失败 {{ progress.fail || 0 }}
|
||||
</div>
|
||||
<div v-if="progress.error" class="filter-retry-progress__error">{{ progress.error }}</div>
|
||||
<p v-if="loading" class="filter-retry-progress__hint">请勿关闭页面,大量订单同步落库可能需要一些时间</p>
|
||||
<div v-if="progress.error" class="sync-progress__error">{{ progress.error }}</div>
|
||||
<p v-if="loading" class="sync-progress__hint">同步在后台进行,可收起弹窗继续操作;请勿关闭或刷新本页</p>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
@@ -52,11 +50,11 @@
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submit">开始同步落库</el-button>
|
||||
</template>
|
||||
<template v-else-if="finished">
|
||||
<el-button type="primary" @click="visible = false">关闭</el-button>
|
||||
<template v-else-if="loading">
|
||||
<el-button @click="visible = false">收起</el-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-button disabled>执行中...</el-button>
|
||||
<el-button type="primary" @click="visible = false">关闭</el-button>
|
||||
</template>
|
||||
</template>
|
||||
</el-dialog>
|
||||
@@ -72,6 +70,9 @@ const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const finished = ref(false)
|
||||
const query = ref({})
|
||||
const asyncTaskIdRef = ref('')
|
||||
const pollGeneration = ref(0)
|
||||
const autoCollapseTimer = ref(null)
|
||||
const progress = ref({
|
||||
percent: 0,
|
||||
message: '',
|
||||
@@ -102,7 +103,11 @@ const filterText = computed(() => {
|
||||
|
||||
const dialogTitle = computed(() => {
|
||||
if (loading.value) return '异步同步落库进行中'
|
||||
if (finished.value) return '异步同步落库完成'
|
||||
if (finished.value) {
|
||||
const st = String(progress.value?.status || '').toUpperCase()
|
||||
if (st === 'FAIL') return '异步同步落库失败'
|
||||
return '异步同步落库完成'
|
||||
}
|
||||
return '条件批量同步落库'
|
||||
})
|
||||
|
||||
@@ -112,6 +117,7 @@ const progressStatus = computed(() => {
|
||||
const st = (progress.value?.status || '').toUpperCase()
|
||||
if (st === 'SUCCESS') return 'success'
|
||||
if (st === 'FAIL') return 'exception'
|
||||
if (finished.value && (progress.value?.fail || 0) > 0) return 'warning'
|
||||
return undefined
|
||||
})
|
||||
|
||||
@@ -119,12 +125,15 @@ const progressMessage = computed(() => mapProgressMessage(progress.value?.messag
|
||||
|
||||
function mapProgressMessage(message, data) {
|
||||
const m = message || ''
|
||||
if (!m || m === 'queued') return '排队中...'
|
||||
if (m === 'starting') return '正在统计订单...'
|
||||
if (!m || m === 'queued') return '排队中…'
|
||||
if (m === 'starting') return '正在统计订单…'
|
||||
if (m === 'no tasks') return '无匹配订单'
|
||||
if (m === 'failed') return '执行失败'
|
||||
if (m.startsWith('processing ')) {
|
||||
return '正在同步落库 ' + m.slice('processing '.length)
|
||||
const ok = data?.ok || 0
|
||||
const total = data?.total || 0
|
||||
const fail = data?.fail || 0
|
||||
return '正在同步落库 ' + ok + '/' + total + (fail ? ('(失败 ' + fail + ')') : '')
|
||||
}
|
||||
if (m.startsWith('done ')) {
|
||||
return '完成:成功 ' + (data?.ok || 0) + ' / 失败 ' + (data?.fail || 0)
|
||||
@@ -147,12 +156,28 @@ function emitProgress() {
|
||||
})
|
||||
}
|
||||
|
||||
function clearAutoCollapse() {
|
||||
if (autoCollapseTimer.value) {
|
||||
clearTimeout(autoCollapseTimer.value)
|
||||
autoCollapseTimer.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleAutoCollapse() {
|
||||
clearAutoCollapse()
|
||||
autoCollapseTimer.value = window.setTimeout(() => {
|
||||
if (loading.value) visible.value = false
|
||||
}, 600)
|
||||
}
|
||||
|
||||
function open(filterQuery, autoStart) {
|
||||
// Already running / finished: just reopen progress view (same as order-list rebill).
|
||||
if (loading.value || finished.value) {
|
||||
visible.value = true
|
||||
return
|
||||
}
|
||||
query.value = { ...(filterQuery || {}) }
|
||||
asyncTaskIdRef.value = ''
|
||||
progress.value = {
|
||||
percent: 0,
|
||||
message: '',
|
||||
@@ -173,7 +198,8 @@ function open(filterQuery, autoStart) {
|
||||
}
|
||||
|
||||
function onClosed() {
|
||||
if (!loading.value) {
|
||||
// Keep finished state until next open so float can still reopen detail.
|
||||
if (!loading.value && !finished.value) {
|
||||
finished.value = false
|
||||
}
|
||||
}
|
||||
@@ -185,7 +211,6 @@ function submit(skipConfirm) {
|
||||
const identifiers = Array.isArray(q.orderIdentifiers)
|
||||
? q.orderIdentifiers.filter(v => v != null && String(v).trim() !== '')
|
||||
: []
|
||||
// 与查询同口径:有单号即可同步;无单号时必须客户 + 提货时间
|
||||
if (!identifiers.length && ((!codes.length && !keywords.length) || !q.beginTime || !q.endTime)) {
|
||||
proxy.$modal.msgError('请输入单号,或先输入客户并选择提货时间')
|
||||
return
|
||||
@@ -204,6 +229,7 @@ function submit(skipConfirm) {
|
||||
error: ''
|
||||
}
|
||||
emitProgress()
|
||||
scheduleAutoCollapse()
|
||||
return syncOrderSourceByFilter({
|
||||
customerCodes: codes.length ? codes : undefined,
|
||||
customerKeywords: keywords.length ? keywords : undefined,
|
||||
@@ -214,15 +240,28 @@ function submit(skipConfirm) {
|
||||
orgOrderId: q.orgOrderId || undefined,
|
||||
transferOrderId: q.transferOrderId || undefined,
|
||||
orderStatuses: Array.isArray(q.orderStatuses) && q.orderStatuses.length ? q.orderStatuses : undefined,
|
||||
blPickup: q.blPickup === 0 || q.blPickup === 1 ? q.blPickup : 1,
|
||||
batchSize: 100
|
||||
}).then(res => {
|
||||
const asyncTaskId = res.data?.asyncTaskId || res.data
|
||||
if (!asyncTaskId) {
|
||||
throw new Error('asyncTaskId missing')
|
||||
}
|
||||
return poll(asyncTaskId)
|
||||
asyncTaskIdRef.value = String(asyncTaskId)
|
||||
return poll(String(asyncTaskId))
|
||||
}).then((data) => {
|
||||
finished.value = true
|
||||
progress.value = {
|
||||
...progress.value,
|
||||
percent: 100,
|
||||
status: (data && data.status) || 'SUCCESS',
|
||||
total: data?.total ?? progress.value.total,
|
||||
processed: data?.processed ?? progress.value.processed,
|
||||
ok: data?.ok ?? progress.value.ok,
|
||||
fail: data?.fail ?? progress.value.fail,
|
||||
message: data?.message || progress.value.message,
|
||||
error: data?.error || ''
|
||||
}
|
||||
emit('success', data)
|
||||
proxy.$modal.msgSuccess(
|
||||
'完成:成功 ' + (data.ok || 0) + ' / 失败 ' + (data.fail || 0)
|
||||
@@ -241,6 +280,7 @@ function submit(skipConfirm) {
|
||||
}
|
||||
}
|
||||
}).finally(() => {
|
||||
clearAutoCollapse()
|
||||
loading.value = false
|
||||
emitProgress()
|
||||
})
|
||||
@@ -255,12 +295,28 @@ function submit(skipConfirm) {
|
||||
).then(() => start()).catch(() => {})
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll like order-list rebill: 1s visible / 2.5s hidden, skip unchanged payload emits,
|
||||
* single generation so reopen won't stack loops.
|
||||
*/
|
||||
function poll(asyncTaskId) {
|
||||
const gen = ++pollGeneration.value
|
||||
let lastSig = ''
|
||||
return new Promise((resolve, reject) => {
|
||||
const tick = () => {
|
||||
if (gen !== pollGeneration.value) {
|
||||
reject(Object.assign(new Error('sync poll superseded'), { superseded: true }))
|
||||
return
|
||||
}
|
||||
const hidden = typeof document !== 'undefined' && document.visibilityState === 'hidden'
|
||||
getOrderSourceSyncProgress(asyncTaskId).then(res => {
|
||||
if (gen !== pollGeneration.value) {
|
||||
reject(Object.assign(new Error('sync poll superseded'), { superseded: true }))
|
||||
return
|
||||
}
|
||||
const data = res.data || {}
|
||||
progress.value = {
|
||||
const st = String(data.status || '').toUpperCase()
|
||||
const next = {
|
||||
percent: Number(data.percent) || 0,
|
||||
message: data.message || '',
|
||||
total: data.total || 0,
|
||||
@@ -270,8 +326,12 @@ function poll(asyncTaskId) {
|
||||
status: data.status || '',
|
||||
error: data.error || ''
|
||||
}
|
||||
emitProgress()
|
||||
const st = (data.status || '').toUpperCase()
|
||||
const sig = [st, next.percent, next.ok, next.fail, next.total, next.message].join('|')
|
||||
progress.value = next
|
||||
if (sig !== lastSig) {
|
||||
lastSig = sig
|
||||
emitProgress()
|
||||
}
|
||||
if (st === 'SUCCESS') {
|
||||
progress.value.percent = 100
|
||||
resolve(data)
|
||||
@@ -282,43 +342,46 @@ function poll(asyncTaskId) {
|
||||
reject(new Error(data.error || 'fail'))
|
||||
return
|
||||
}
|
||||
setTimeout(tick, 1500)
|
||||
}).catch(err => reject(err))
|
||||
setTimeout(tick, hidden ? 2500 : 1000)
|
||||
}).catch(err => {
|
||||
if (gen !== pollGeneration.value) return
|
||||
reject(err)
|
||||
})
|
||||
}
|
||||
tick()
|
||||
})
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearAutoCollapse()
|
||||
pollGeneration.value += 1
|
||||
})
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.filter-retry-progress {
|
||||
.sync-progress {
|
||||
padding: 8px 4px 0;
|
||||
}
|
||||
.filter-retry-progress__msg {
|
||||
margin-top: 14px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
line-height: 1.5;
|
||||
.sync-progress__msg {
|
||||
margin-top: 12px;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
.filter-retry-progress__text {
|
||||
.sync-progress__text {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
line-height: 1.6;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
.filter-retry-progress__error {
|
||||
.sync-progress__error {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #f56c6c;
|
||||
word-break: break-all;
|
||||
}
|
||||
.filter-retry-progress__hint {
|
||||
margin: 12px 0 0;
|
||||
.sync-progress__hint {
|
||||
margin-top: 12px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
line-height: 1.5;
|
||||
color: var(--el-color-warning);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -191,8 +191,14 @@ const orderStatusMap = {
|
||||
|
||||
const orderStatusOptions = Object.keys(orderStatusMap).map(k => ({ label: orderStatusMap[k], value: Number(k) }))
|
||||
|
||||
const blPickupOptions = [
|
||||
{ label: '已收货', value: 1 },
|
||||
{ label: '未收货', value: 0 }
|
||||
]
|
||||
|
||||
const defaultParams = {
|
||||
orderStatuses: [3, 4, 5, 6, 7, 8, 11]
|
||||
orderStatuses: [3, 4, 5, 6, 7, 8, 11],
|
||||
blPickup: 1
|
||||
}
|
||||
|
||||
const searchColumns = [
|
||||
@@ -243,6 +249,13 @@ const searchColumns = [
|
||||
collapseTagsTooltip: true
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '收货状态',
|
||||
prop: 'blPickup',
|
||||
component: 'select',
|
||||
options: blPickupOptions,
|
||||
placeholder: '请选择'
|
||||
},
|
||||
]
|
||||
|
||||
const columns = [
|
||||
@@ -389,6 +402,7 @@ function queryList(params) {
|
||||
const customers = splitLines(src.customerKeywords)
|
||||
const fillQuery = { orderIdentifiers: identifiers }
|
||||
if (customers.length) fillQuery.customerKeywords = customers
|
||||
fillQuery.blPickup = src.blPickup === 0 || src.blPickup === 1 ? src.blPickup : 1
|
||||
fillQuery.pageNum = params.pageNum
|
||||
fillQuery.pageSize = params.pageSize
|
||||
return listOrderSourceByTransferNos(fillQuery)
|
||||
@@ -421,7 +435,7 @@ function openOrderCompare() {
|
||||
return
|
||||
}
|
||||
proxy.$modal.confirm(
|
||||
'将按当前客户、提货时间与订单状态筛选,异步分页比对上游转单号是否已落库,确认继续?',
|
||||
'将按当前客户、提货时间、订单状态和收货状态筛选,异步分页比对上游转单号是否已落库,确认继续?',
|
||||
'二次确认'
|
||||
).then(() => {
|
||||
// 比对只按客户+时间;不带单号框内容(回填后可能有数千转单号,带上会把比对拖死)
|
||||
@@ -459,12 +473,17 @@ function onCompareFillSearch(payload) {
|
||||
}
|
||||
|
||||
function openFilterSync() {
|
||||
// Running / just finished: reopen progress dialog (same as order-list float click).
|
||||
if (syncFloat.value && (syncFloat.value.loading || syncFloat.value.finished)) {
|
||||
syncDialogRef.value?.open(null, false)
|
||||
return
|
||||
}
|
||||
const q = buildFilterQuery()
|
||||
const identifierCount = (q.orderIdentifiers || []).length
|
||||
const hasIdentifier = identifierCount > 0
|
||||
const hasCustomer = !!(q.customerKeywords && q.customerKeywords.length)
|
||||
const hasTime = !!q.beginTime && !!q.endTime
|
||||
// 与查询同口径:有单号即可同步;无单号时必须客户+提货时间
|
||||
// Same as list query: identifiers alone OK; else customer + pickup time required.
|
||||
if (!hasIdentifier && !(hasCustomer && hasTime)) {
|
||||
ElMessage.warning('请输入单号,或选择客户和提货时间后再同步落库')
|
||||
return
|
||||
@@ -481,13 +500,19 @@ function openFilterSync() {
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
let syncFloatHideTimer = null
|
||||
function onSyncProgress(payload) {
|
||||
syncFloat.value = payload
|
||||
if (syncFloatHideTimer) {
|
||||
clearTimeout(syncFloatHideTimer)
|
||||
syncFloatHideTimer = null
|
||||
}
|
||||
if (payload && payload.finished && !payload.loading) {
|
||||
window.setTimeout(() => {
|
||||
syncFloatHideTimer = window.setTimeout(() => {
|
||||
if (syncFloat.value && syncFloat.value.finished && !syncFloat.value.loading) {
|
||||
syncFloat.value = null
|
||||
}
|
||||
syncFloatHideTimer = null
|
||||
}, 8000)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -370,7 +370,7 @@ const billingFailStatusOptions = [
|
||||
{ label: '产品价未匹配国家', value: 24 },
|
||||
{ label: '产品价未匹配重量段', value: 25 },
|
||||
{ label: '未绑定套餐', value: 4 },
|
||||
{ label: '无生效套餐版本', value: 5 },
|
||||
{ label: '套餐未启用/无生效版本', value: 5 },
|
||||
{ label: '已生成应收账单', value: 6 },
|
||||
{ label: '产品档案缺失', value: 7 },
|
||||
{ label: '未命中价格', value: 8 },
|
||||
|
||||
@@ -0,0 +1,565 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<div v-if="showBasic" class="create-section">
|
||||
<step-basic-info ref="basicRef" v-model="form.basic" />
|
||||
</div>
|
||||
|
||||
<div class="create-section">
|
||||
<step-details
|
||||
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"
|
||||
@update:pef-id="onPefIdUpdate"
|
||||
:version-no="form.version.versionNo"
|
||||
:version-status="form.version.status"
|
||||
:ref-data="refData"
|
||||
:live-persist="livePersist"
|
||||
:build-mode-dirty="buildModeDirty"
|
||||
:build-mode-ready="pageReady"
|
||||
:show-import-inherit="false"
|
||||
:header-title="detailsHeaderTitle"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="wizard-actions">
|
||||
<el-button @click="goBack">取消</el-button>
|
||||
<el-button type="primary" :loading="creating" @click="finishEdit">完成</el-button>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
v-model="progressVisible"
|
||||
:title="mod.labels.saveProgressTitle"
|
||||
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="ExclusiveQuoteCreate">
|
||||
import StepBasicInfo from '../set/components/create/StepBasicInfo.vue'
|
||||
import StepDetails from '../set/components/create/StepDetails.vue'
|
||||
import { usePriceSetRefData } from '../set/composables/usePriceSetRefData'
|
||||
import {
|
||||
createEmptyBasicInfo,
|
||||
isBuildModeLockedVersion,
|
||||
nextVersionNo,
|
||||
toCustomerPayload,
|
||||
toDetailPayload
|
||||
} from '../set/utils/priceSet'
|
||||
import { exclusiveQuoteApi } from '@/api/price/exclusive'
|
||||
import { providePriceSetModule, createExclusiveModule } from '../set/composables/usePriceSetModule'
|
||||
|
||||
// Same-component inject cannot see provide(); use return value for this page, provide for children.
|
||||
const mod = providePriceSetModule(createExclusiveModule(exclusiveQuoteApi))
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const route = useRoute()
|
||||
|
||||
const refData = usePriceSetRefData()
|
||||
|
||||
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 */
|
||||
const deferPersist = computed(() => copyMode.value || newVersionMode.value)
|
||||
/** Continue-config / copy / new-version: hide basic (package already exists) */
|
||||
const showBasic = 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)
|
||||
/** Brand-new package has no prior version — hide IMPORT inherit card. */
|
||||
const showImportInherit = computed(() => newVersionMode.value || copyMode.value)
|
||||
|
||||
const detailsHeaderTitle = '编辑产品价格'
|
||||
|
||||
const form = reactive({
|
||||
priceSetId: undefined,
|
||||
basic: createEmptyBasicInfo(),
|
||||
version: { psvId: undefined, versionNo: 'V1', effectiveTime: undefined, expireTime: undefined, status: undefined, priceBuildMode: 'IMPORT_FRESH' },
|
||||
details: [],
|
||||
products: [],
|
||||
customers: []
|
||||
})
|
||||
/** Draft IMPORT Excel attachment id; stamped onto prc_price_set_product on finish */
|
||||
const draftPefId = ref(undefined)
|
||||
const draftPefIds = ref([])
|
||||
const savedPriceBuildMode = ref(undefined)
|
||||
const savedProductsSnapshot = ref([])
|
||||
const buildModeDirty = computed(() =>
|
||||
String(form.version.priceBuildMode || '') !== String(savedPriceBuildMode.value || '')
|
||||
)
|
||||
/** Gate build-mode cards until package payload applied (avoids first click being wiped). */
|
||||
const pageReady = ref(!route.query.priceSetId)
|
||||
|
||||
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 mod.api.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
|
||||
}
|
||||
}
|
||||
|
||||
/** Finish: create/copy async save all; edit already live-saved. */
|
||||
async function finishEdit() {
|
||||
if (livePersist.value) {
|
||||
if (!form.priceSetId) {
|
||||
goBack()
|
||||
return
|
||||
}
|
||||
if (buildModeDirty.value) {
|
||||
if (!form.version.priceBuildMode) {
|
||||
proxy.$modal.msgWarning("请先选择创建价格方式")
|
||||
return
|
||||
}
|
||||
creating.value = true
|
||||
try {
|
||||
const payload = buildPayload()
|
||||
delete payload.pullFromLibrary
|
||||
delete payload.forcePullLibrary
|
||||
delete payload.details
|
||||
await runSaveWithProgress(payload)
|
||||
savedPriceBuildMode.value = form.version.priceBuildMode
|
||||
await nextTick()
|
||||
if (form.version.priceBuildMode === 'PULL' && detailsRef.value && detailsRef.value.pullAllFromLibrary) {
|
||||
await detailsRef.value.pullAllFromLibrary({ silent: true })
|
||||
savedProductsSnapshot.value = (form.products || []).map(p => ({ ...p }))
|
||||
proxy.$modal.msgSuccess("建价方式已保存")
|
||||
} else if (form.version.priceBuildMode === 'IMPORT') {
|
||||
// Backend copied prior version into this draft; refresh product list from current psv
|
||||
await refreshProductsFromCurrentVersion()
|
||||
proxy.$modal.msgSuccess("建价方式已保存,已带出上一版产品")
|
||||
} else if (form.version.priceBuildMode === 'IMPORT_FRESH') {
|
||||
savedProductsSnapshot.value = (form.products || []).map(p => ({ ...p }))
|
||||
proxy.$modal.msgSuccess("建价方式已保存,可直接导入 Excel 建价(不延用以前的产品价格)")
|
||||
} else {
|
||||
savedProductsSnapshot.value = (form.products || []).map(p => ({ ...p }))
|
||||
proxy.$modal.msgSuccess("建价方式已保存")
|
||||
}
|
||||
} catch (e) {
|
||||
return
|
||||
} finally {
|
||||
creating.value = false
|
||||
}
|
||||
} else {
|
||||
proxy.$modal.msgSuccess("修改已实时落库")
|
||||
}
|
||||
const obj = {
|
||||
path: mod.routes.detail,
|
||||
query: { priceSetId: form.priceSetId }
|
||||
}
|
||||
if (proxy.$tab) {
|
||||
proxy.$tab.closeOpenPage(obj)
|
||||
} else {
|
||||
proxy.$router.push(obj)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (showBasic.value) {
|
||||
const validBasic = await basicRef.value.validate()
|
||||
if (!validBasic) 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 {
|
||||
if (newVersionMode.value && form.version.psvId == null && form.priceSetId) {
|
||||
const verRes = await mod.api.createPriceSetVersion(form.priceSetId)
|
||||
if (verRes.data != null) {
|
||||
form.version.psvId = verRes.data
|
||||
}
|
||||
}
|
||||
const payload = buildPayload({ forceDetails: true })
|
||||
const priceSetId = await runSaveWithProgress(payload)
|
||||
form.priceSetId = priceSetId
|
||||
proxy.$modal.msgSuccess(mod.labels.savedSuccess)
|
||||
const obj = {
|
||||
path: mod.routes.detail,
|
||||
query: { priceSetId }
|
||||
}
|
||||
if (proxy.$tab) {
|
||||
proxy.$tab.closeOpenPage(obj)
|
||||
} else {
|
||||
proxy.$router.push(obj)
|
||||
}
|
||||
} catch (e) {
|
||||
// surfaced
|
||||
} finally {
|
||||
creating.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 || (newVersionMode.value && version.psvId == null)) ? undefined : version.psvId,
|
||||
versionNo: version.versionNo,
|
||||
priceBuildMode: version.priceBuildMode || undefined
|
||||
}
|
||||
}
|
||||
if (draftPefIds.value.length) {
|
||||
payload.pefIds = draftPefIds.value.slice()
|
||||
} else if (draftPefId.value != null) {
|
||||
payload.pefId = draftPefId.value
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* New-version flow creates an empty draft as soon as user clicks "新建价格版本".
|
||||
* If user cancels without finishing (no build mode / no details on server), remove that orphan draft.
|
||||
*/
|
||||
async function tryDeleteEmptyNewVersionDraft() {
|
||||
if (!newVersionMode.value || !form.priceSetId || form.version.psvId == null) return
|
||||
const psvId = form.version.psvId
|
||||
try {
|
||||
const res = await mod.api.getPriceSet(form.priceSetId)
|
||||
const versions = (res.data || {}).versions || []
|
||||
const v = versions.find(x => x && String(x.psvId) === String(psvId))
|
||||
if (!v || String(v.status) !== '0') return
|
||||
const noMode = !v.priceBuildMode
|
||||
const noDetails = !(Number(v.detailCount) > 0) && !(v.details || []).length
|
||||
if (noMode && noDetails) {
|
||||
await mod.api.deletePriceSetVersion(psvId)
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore cleanup errors; still navigate back
|
||||
}
|
||||
}
|
||||
|
||||
async function goBack() {
|
||||
await tryDeleteEmptyNewVersionDraft()
|
||||
const obj = (newVersionMode.value || copyMode.value) && form.priceSetId
|
||||
? { path: mod.routes.detail, query: { priceSetId: form.priceSetId } }
|
||||
: { path: mod.routes.list }
|
||||
if (proxy.$tab) {
|
||||
proxy.$tab.closeOpenPage(obj)
|
||||
} else {
|
||||
proxy.$router.push(obj)
|
||||
}
|
||||
}
|
||||
|
||||
function onPsvIdUpdate(psvId) {
|
||||
if (psvId != null && psvId !== '') form.version.psvId = psvId
|
||||
}
|
||||
|
||||
function onPefIdUpdate(pefId) {
|
||||
draftPefId.value = pefId
|
||||
if (pefId == null || pefId === '') return
|
||||
const id = String(pefId)
|
||||
if (!draftPefIds.value.some(x => String(x) === id)) {
|
||||
draftPefIds.value.push(pefId)
|
||||
}
|
||||
}
|
||||
|
||||
/** Step2 chose PULL / IMPORT / IMPORT_FRESH: edit live-saves mode; create only local + optional preview pull. */
|
||||
function onPriceBuildModeUpdate(mode) {
|
||||
if (!mode) return
|
||||
if (isBuildModeLockedVersion(form.version.status)) return
|
||||
if (mode === form.version.priceBuildMode) return
|
||||
const leavingSaved = livePersist.value
|
||||
&& String(form.version.priceBuildMode || '') === String(savedPriceBuildMode.value || '')
|
||||
if (leavingSaved) {
|
||||
savedProductsSnapshot.value = (form.products || []).map(p => ({ ...p }))
|
||||
}
|
||||
form.version.priceBuildMode = mode
|
||||
if (mode !== 'IMPORT' && mode !== 'IMPORT_FRESH') {
|
||||
draftPefId.value = undefined
|
||||
draftPefIds.value = []
|
||||
}
|
||||
form.details.splice(0, form.details.length)
|
||||
if (detailsRef.value && detailsRef.value.clearLocalDraft) {
|
||||
detailsRef.value.clearLocalDraft()
|
||||
}
|
||||
if (livePersist.value) {
|
||||
const backToSaved = String(mode || '') === String(savedPriceBuildMode.value || '')
|
||||
if (backToSaved) {
|
||||
form.products.splice(0, form.products.length, ...(savedProductsSnapshot.value || []).map(p => ({ ...p })))
|
||||
if (!form.products.length && form.priceSetId) {
|
||||
reloadSavedProductsForCreate()
|
||||
} else {
|
||||
proxy.$modal.msgSuccess("已恢复原建价方式及产品列表(未保存,库数据未变)")
|
||||
}
|
||||
return
|
||||
}
|
||||
form.products.splice(0, form.products.length)
|
||||
proxy.$modal.msgSuccess("已切换建价方式(本地),请点「完成」保存后再拉取或导入;切回原方式可恢复列表")
|
||||
return
|
||||
}
|
||||
form.products.splice(0, form.products.length)
|
||||
if (mode === 'PULL' && detailsRef.value && detailsRef.value.pullAllFromLibrary) {
|
||||
nextTick(() => {
|
||||
detailsRef.value.pullAllFromLibrary({ silent: true }).then(() => {
|
||||
proxy.$modal.msgSuccess("已拉取产品列表,可继续编辑价格")
|
||||
}).catch(() => {})
|
||||
})
|
||||
} else if (mode === 'IMPORT' && showImportInherit.value) {
|
||||
nextTick(async () => {
|
||||
// newVersion/copy: wait until loadExisting fills priceSetId
|
||||
const qid = route.query.priceSetId
|
||||
if (!form.priceSetId && qid) {
|
||||
for (let i = 0; i < 50 && !form.priceSetId; i++) {
|
||||
await new Promise(r => setTimeout(r, 100))
|
||||
}
|
||||
}
|
||||
const loader = detailsRef.value && detailsRef.value.loadLatestPriorVersionDraft
|
||||
if (!loader) {
|
||||
proxy.$modal.msgSuccess("请导入 Excel 建价")
|
||||
return
|
||||
}
|
||||
if (!form.priceSetId) {
|
||||
proxy.$modal.msgSuccess("请直接导入 Excel 建价")
|
||||
return
|
||||
}
|
||||
try {
|
||||
const ok = await loader()
|
||||
proxy.$modal.msgSuccess(ok
|
||||
? "已带出上一版全部产品,导入 Excel 按产品+国家覆盖或新增"
|
||||
: "未找到含产品的上一版,请导入 Excel 建价")
|
||||
} catch (e) {
|
||||
proxy.$modal.msgSuccess("请导入 Excel 建价")
|
||||
}
|
||||
})
|
||||
} else if (mode === 'IMPORT_FRESH') {
|
||||
proxy.$modal.msgSuccess("请直接导入 Excel 建价(不延用以前的产品价格)")
|
||||
}
|
||||
}
|
||||
|
||||
function reloadSavedProductsForCreate() {
|
||||
mod.api.getPriceSet(form.priceSetId).then(res => {
|
||||
const data = res.data || {}
|
||||
const versions = data.versions || []
|
||||
const qPsv = form.version.psvId
|
||||
const byQuery = qPsv != null ? versions.find(v => String(v.psvId) === String(qPsv)) : null
|
||||
const current = byQuery || data.version || versions[0] || {}
|
||||
const list = (current.products || []).map(p => ({ ...p }))
|
||||
form.products.splice(0, form.products.length, ...list)
|
||||
savedProductsSnapshot.value = list.map(p => ({ ...p }))
|
||||
proxy.$modal.msgSuccess("已恢复原建价方式及产品列表(未保存,库数据未变)")
|
||||
})
|
||||
}
|
||||
|
||||
async function refreshProductsFromCurrentVersion() {
|
||||
if (!form.priceSetId) return
|
||||
const res = await mod.api.getPriceSet(form.priceSetId)
|
||||
const data = res.data || {}
|
||||
const versions = data.versions || []
|
||||
const qPsv = form.version.psvId
|
||||
const byQuery = qPsv != null ? versions.find(v => String(v.psvId) === String(qPsv)) : null
|
||||
const current = byQuery || data.version || versions[0] || {}
|
||||
const list = (current.products || []).map(p => ({ ...p }))
|
||||
form.products.splice(0, form.products.length, ...list)
|
||||
form.details.splice(0, form.details.length)
|
||||
savedProductsSnapshot.value = list.map(p => ({ ...p }))
|
||||
}
|
||||
|
||||
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) {
|
||||
const keptMode = form.version.priceBuildMode
|
||||
const keptProducts = keptMode ? form.products.slice() : []
|
||||
const keptDetails = keptMode ? form.details.slice() : []
|
||||
form.version.psvId = undefined
|
||||
form.version.versionNo = nextVersionNo(versions)
|
||||
form.version.effectiveTime = undefined
|
||||
form.version.expireTime = undefined
|
||||
form.version.status = undefined
|
||||
form.version.priceBuildMode = keptMode || undefined
|
||||
draftPefId.value = undefined
|
||||
draftPefIds.value = []
|
||||
savedPriceBuildMode.value = undefined
|
||||
if (keptMode) {
|
||||
form.products.splice(0, form.products.length, ...keptProducts)
|
||||
form.details.splice(0, form.details.length, ...keptDetails)
|
||||
} else {
|
||||
form.details = []
|
||||
form.products = []
|
||||
}
|
||||
} else if (asNewVersion) {
|
||||
// Draft is created on finish only. Resume only when query has psvId (legacy empty draft).
|
||||
// Keep mode/products if user already chose a build mode before this response landed.
|
||||
const keptMode = form.version.priceBuildMode
|
||||
const keptProducts = keptMode ? form.products.slice() : []
|
||||
const keptDetails = keptMode ? form.details.slice() : []
|
||||
const resume = qPsv != null ? byQuery : null
|
||||
form.version.psvId = resume ? resume.psvId : undefined
|
||||
form.version.versionNo = (resume && resume.versionNo) || nextVersionNo(versions)
|
||||
form.version.effectiveTime = undefined
|
||||
form.version.expireTime = undefined
|
||||
form.version.status = (resume && resume.status) || '0'
|
||||
form.version.priceBuildMode = keptMode || undefined
|
||||
draftPefId.value = undefined
|
||||
draftPefIds.value = []
|
||||
savedPriceBuildMode.value = undefined
|
||||
if (keptMode) {
|
||||
form.products.splice(0, form.products.length, ...keptProducts)
|
||||
form.details.splice(0, form.details.length, ...keptDetails)
|
||||
} else {
|
||||
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.status = current.status
|
||||
form.version.priceBuildMode = current.priceBuildMode || undefined
|
||||
draftPefId.value = undefined
|
||||
draftPefIds.value = []
|
||||
savedPriceBuildMode.value = current.priceBuildMode || undefined
|
||||
form.details = []
|
||||
form.products = (current.products || []).map(p => ({ ...p }))
|
||||
savedProductsSnapshot.value = form.products.map(p => ({ ...p }))
|
||||
}
|
||||
form.customers = (data.customers || []).map(c => ({ ...c }))
|
||||
}
|
||||
|
||||
function loadExisting(id, asCopy, asNewVersion) {
|
||||
pageReady.value = false
|
||||
return mod.api.getPriceSet(id).then(res => {
|
||||
applySetData(res.data || {}, asCopy, asNewVersion)
|
||||
}).finally(() => {
|
||||
pageReady.value = true
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
refData.loadProducts().finally(() => {
|
||||
const id = route.query.priceSetId
|
||||
if (id) {
|
||||
loadExisting(id, copyMode.value, newVersionMode.value)
|
||||
} else {
|
||||
pageReady.value = true
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.create-section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.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>
|
||||
@@ -0,0 +1,538 @@
|
||||
<template>
|
||||
<div class="app-container" v-loading="loading">
|
||||
<!-- 编辑模式:整屏切换,等同独立页面 -->
|
||||
<price-edit-panel
|
||||
v-if="editVisible"
|
||||
:price-set-id="set.priceSetId"
|
||||
:psv-id="editPsvId"
|
||||
@close="closeEdit"
|
||||
/>
|
||||
|
||||
<template v-else>
|
||||
<set-detail-header
|
||||
:set="set"
|
||||
@back="goList"
|
||||
@disable="onDisable"
|
||||
@enable="onEnable"
|
||||
/>
|
||||
|
||||
<set-basic-info-card :set="set" />
|
||||
|
||||
<set-customer-table :set="set" @refresh="load" />
|
||||
|
||||
<set-version-table
|
||||
v-model="currentPsvId"
|
||||
:versions="set.versions || []"
|
||||
:removing-psv-id="removingPsvId"
|
||||
@create-version="onCreateVersion"
|
||||
@open-details="onOpenVersionDetails"
|
||||
@view-import-attach="onViewImportAttach"
|
||||
@activate="onActivateVersion"
|
||||
@reschedule="onRescheduleVersion"
|
||||
@edit="onEditVersion"
|
||||
@remove="onRemoveVersion"
|
||||
/>
|
||||
|
||||
<set-version-detail-drawer
|
||||
v-model="detailDrawerVisible"
|
||||
:version="detailVersion"
|
||||
:set-type="set.setType"
|
||||
/>
|
||||
<set-version-excel-file-drawer ref="versionExcelDrawerRef" />
|
||||
|
||||
<el-dialog
|
||||
v-model="activateVisible"
|
||||
:title="activateDialogTitle"
|
||||
width="460px"
|
||||
append-to-body
|
||||
@closed="onActivateClosed"
|
||||
>
|
||||
<el-form ref="activateFormRef" :model="activateForm" :rules="activateRules" label-width="100px">
|
||||
<el-form-item label="生效时间" prop="effectiveTime">
|
||||
<el-date-picker
|
||||
v-model="activateForm.effectiveTime"
|
||||
type="datetime"
|
||||
format="YYYY-MM-DD HH:00"
|
||||
value-format="YYYY-MM-DD HH:00:00"
|
||||
popper-class="activate-hour-picker"
|
||||
:placeholder="activatePh"
|
||||
:disabled-date="disabledActivateDate"
|
||||
:disabled-hours="disabledActivateHours"
|
||||
style="width: 100%"
|
||||
@change="onActivateTimeChange"
|
||||
@visible-change="onActivatePickerVisible"
|
||||
/>
|
||||
</el-form-item>
|
||||
<div class="activate-hint">{{ activateDialogHint }}</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="activateVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="activateSubmitting" @click="submitActivate">{{ activateConfirmText }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="ExclusiveQuoteDetail">
|
||||
import SetDetailHeader from '../set/components/detail/SetDetailHeader.vue'
|
||||
import SetBasicInfoCard from '../set/components/detail/SetBasicInfoCard.vue'
|
||||
import SetCustomerTable from '../set/components/detail/SetCustomerTable.vue'
|
||||
import SetVersionTable from '../set/components/detail/SetVersionTable.vue'
|
||||
import SetVersionDetailDrawer from '../set/components/detail/SetVersionDetailDrawer.vue'
|
||||
import SetVersionExcelFileDrawer from '../set/components/detail/SetVersionExcelFileDrawer.vue'
|
||||
import PriceEditPanel from '../set/components/create/PriceEditPanel.vue'
|
||||
import { usePriceSetRefData } from '../set/composables/usePriceSetRefData'
|
||||
import { exclusiveQuoteApi } from '@/api/price/exclusive'
|
||||
import { providePriceSetModule, createExclusiveModule } from '../set/composables/usePriceSetModule'
|
||||
import { isDraftVersion, isPendingVersion, isEffectVersion, isImportFreshBuildMode } from '../set/utils/priceSet'
|
||||
|
||||
// Same-component inject cannot see provide(); use return value for this page, provide for children.
|
||||
const mod = providePriceSetModule(createExclusiveModule(exclusiveQuoteApi))
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const refData = usePriceSetRefData()
|
||||
|
||||
const loading = ref(false)
|
||||
const removingPsvId = ref(null)
|
||||
const set = ref({})
|
||||
const currentPsvId = ref()
|
||||
const detailDrawerVisible = ref(false)
|
||||
const detailVersion = ref(null)
|
||||
const versionExcelDrawerRef = ref()
|
||||
const editVisible = ref(false)
|
||||
const editPsvId = ref(undefined)
|
||||
/** 已加载的套餐ID;页面被 keep-alive 缓存复用时据此判断是否需要重新加载 */
|
||||
const loadedId = ref(null)
|
||||
/** 从详情页跳到新建/编辑套餐版本页后,返回时套餐版本数据可能已变化,需强制刷新 */
|
||||
const pendingRefresh = ref(false)
|
||||
|
||||
/** 切换到另一个套餐时,重置所有临时面板状态,避免残留上一套餐的数据 */
|
||||
function resetTransientState() {
|
||||
set.value = {}
|
||||
currentPsvId.value = undefined
|
||||
detailDrawerVisible.value = false
|
||||
detailVersion.value = null
|
||||
editVisible.value = false
|
||||
editPsvId.value = undefined
|
||||
activateVisible.value = false
|
||||
activateSubmitting.value = false
|
||||
activateMode.value = 'activate'
|
||||
activateForm.psvId = undefined
|
||||
activateForm.versionNo = ''
|
||||
activateForm.effectiveTime = undefined
|
||||
activatePrevEffectiveTime.value = undefined
|
||||
}
|
||||
|
||||
function onOpenVersionDetails(row) {
|
||||
detailVersion.value = row || null
|
||||
detailDrawerVisible.value = true
|
||||
}
|
||||
|
||||
function onViewImportAttach(row) {
|
||||
if (!row || row.psvId == null) return
|
||||
versionExcelDrawerRef.value?.open?.(row)
|
||||
}
|
||||
|
||||
function load() {
|
||||
const id = route.query.priceSetId
|
||||
if (!id) return
|
||||
// 立即标记,首次挂载后紧随的 activated 不会重复请求
|
||||
loadedId.value = String(id)
|
||||
loading.value = true
|
||||
mod.api.getPriceSet(id).then(res => {
|
||||
set.value = res.data || {}
|
||||
const versions = set.value.versions || []
|
||||
const cur = set.value.version || versions[0]
|
||||
currentPsvId.value = cur ? cur.psvId : undefined
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* keep-alive 复用同一组件实例时同步数据:
|
||||
* - priceSetId 变化:加载新套餐
|
||||
* - 从新建/编辑版本页返回(force):即使套餐相同也强制重载,保证版本列表最新
|
||||
*/
|
||||
function syncFromRoute(force) {
|
||||
const id = route.query.priceSetId
|
||||
if (!id) return
|
||||
pendingRefresh.value = false
|
||||
if (!force && String(id) === String(loadedId.value)) return
|
||||
resetTransientState()
|
||||
load()
|
||||
}
|
||||
|
||||
function confirmAction(text, request, successText, done) {
|
||||
proxy.$modal.confirm(text).then(() => request()).then(() => {
|
||||
proxy.$modal.msgSuccess(successText)
|
||||
done ? done() : load()
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
function onDisable() {
|
||||
confirmAction(mod.labels.disableConfirm.replace('{name}', set.value.priceSetName || ''), () => mod.api.disablePriceSet(set.value.priceSetId), '已停用')
|
||||
}
|
||||
function onEnable() {
|
||||
confirmAction(mod.labels.enableConfirm.replace('{name}', set.value.priceSetName || ''), () => mod.api.enablePriceSet(set.value.priceSetId), '已启用')
|
||||
}
|
||||
/** Effective time precision: hour only (mm:ss always 00) */
|
||||
function pad2(n) {
|
||||
return String(n).padStart(2, '0')
|
||||
}
|
||||
|
||||
function formatHour(d) {
|
||||
if (!d || Number.isNaN(d.getTime())) return undefined
|
||||
return d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate())
|
||||
+ ' ' + pad2(d.getHours()) + ':00:00'
|
||||
}
|
||||
|
||||
function normalizeToHour(val) {
|
||||
if (val == null || val === '') return val
|
||||
if (val instanceof Date) return formatHour(val)
|
||||
const s = String(val).trim().replace('T', ' ')
|
||||
const m = s.match(/^(\d{4}-\d{2}-\d{2})[ ](\d{2})/)
|
||||
if (m) return m[1] + ' ' + m[2] + ':00:00'
|
||||
return formatHour(new Date(s.replace(/-/g, '/'))) || val
|
||||
}
|
||||
|
||||
function formatNow() {
|
||||
const d = new Date()
|
||||
d.setHours(9, 0, 0, 0)
|
||||
return formatHour(d)
|
||||
}
|
||||
|
||||
function onActivateTimeChange(val) {
|
||||
activateForm.effectiveTime = normalizeToHour(val)
|
||||
if (activateFormRef.value) {
|
||||
activateFormRef.value.validateField('effectiveTime')
|
||||
}
|
||||
}
|
||||
|
||||
const activateVisible = ref(false)
|
||||
const activateSubmitting = ref(false)
|
||||
const activateFormRef = ref()
|
||||
const activateMode = ref('activate')
|
||||
const activatePh = '请选择生效时间'
|
||||
const activateForm = reactive({
|
||||
psvId: undefined,
|
||||
versionNo: '',
|
||||
effectiveTime: undefined
|
||||
})
|
||||
const activateDialogTitle = computed(() =>
|
||||
activateMode.value === 'reschedule' ? '修改生效时间' : '启动版本'
|
||||
)
|
||||
const activateConfirmText = computed(() =>
|
||||
activateMode.value === 'reschedule' ? '确认修改' : '确认启动'
|
||||
)
|
||||
const activatePrevEffectiveTime = ref(undefined)
|
||||
const activateIsImportFresh = ref(false)
|
||||
/** Other versions' effective/expire times (hour) — forbidden for IMPORT_FRESH */
|
||||
const activateBoundaryTimes = ref([])
|
||||
const activateEffBeforePrevMsg = '生效时间不能早于上一版本的生效时间'
|
||||
const activateEffHitBoundaryMsg = '生效时间不能与已有版本的生效时间或失效时间相同'
|
||||
const activateDialogHint = computed(() => {
|
||||
if (activateIsImportFresh.value) {
|
||||
const base = activateMode.value === 'reschedule'
|
||||
? 'Excel 新建价:生效时间可落在历史版本区间内,将自动截断该版本失效时间并同步产品/明细。'
|
||||
: 'Excel 新建价:生效时间可落在历史版本区间内;落在版本 B 区间时,B 失效时间改为本次启动时间,本版本失效时间取 B 原失效时间,并同步双方产品/明细。'
|
||||
return base + '不可与已有版本的生效时间或失效时间相同。'
|
||||
}
|
||||
const base = activateMode.value === 'reschedule'
|
||||
? '仅待生效版本可修改;若时间晚于当前仍为待生效,到时自动生效并使上一版本失效。'
|
||||
: '若生效时间晚于当前,版本将进入「待生效」;到时自动生效并使上一版本失效。'
|
||||
const prev = activatePrevEffectiveTime.value
|
||||
if (!prev) return base
|
||||
return base + '不得早于上一版:' + prev + '。'
|
||||
})
|
||||
const activateRules = computed(() => ({
|
||||
effectiveTime: [
|
||||
{ required: true, message: '请选择生效时间', trigger: 'change' },
|
||||
{
|
||||
validator: (_r, v, cb) => {
|
||||
if (!v) return cb()
|
||||
if (activateIsImportFresh.value) {
|
||||
const ms = toTimeMs(v)
|
||||
if (activateBoundaryTimes.value.some(b => toTimeMs(b) === ms)) {
|
||||
return cb(new Error(activateEffHitBoundaryMsg))
|
||||
}
|
||||
return cb()
|
||||
}
|
||||
const prev = activatePrevEffectiveTime.value
|
||||
if (prev && toTimeMs(v) < toTimeMs(prev)) return cb(new Error(activateEffBeforePrevMsg))
|
||||
cb()
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
]
|
||||
}))
|
||||
|
||||
function toTimeMs(val) {
|
||||
if (val == null || val === '') return NaN
|
||||
const n = normalizeToHour(val)
|
||||
if (!n) return NaN
|
||||
return new Date(String(n).replace(/-/g, '/')).getTime()
|
||||
}
|
||||
|
||||
function findPrevVersionEffectiveTime(row) {
|
||||
if (!row) return undefined
|
||||
const versions = set.value.versions || []
|
||||
const curSeq = Number(row.versionSeq) || 0
|
||||
let prev = null
|
||||
for (const v of versions) {
|
||||
if (!v || v.psvId == null || String(v.psvId) === String(row.psvId)) continue
|
||||
const vs = Number(v.versionSeq) || 0
|
||||
if (vs >= curSeq || !v.effectiveTime) continue
|
||||
if (!prev || vs > (Number(prev.versionSeq) || 0)) prev = v
|
||||
}
|
||||
return prev ? normalizeToHour(prev.effectiveTime) : undefined
|
||||
}
|
||||
|
||||
/** Collect other versions' effective/expire hour times (forbidden for IMPORT_FRESH). */
|
||||
function collectActivateBoundaryTimes(excludePsvId) {
|
||||
const out = []
|
||||
const seen = new Set()
|
||||
for (const v of set.value.versions || []) {
|
||||
if (!v || v.psvId == null || String(v.psvId) === String(excludePsvId)) continue
|
||||
;[v.effectiveTime, v.expireTime].forEach(t => {
|
||||
const n = t ? normalizeToHour(t) : ''
|
||||
if (!n || seen.has(n)) return
|
||||
seen.add(n)
|
||||
out.push(n)
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function pickActivateDefaultTime(prev, importFresh) {
|
||||
const now = formatNow()
|
||||
if (importFresh || !prev) return now
|
||||
return toTimeMs(now) >= toTimeMs(prev) ? now : prev
|
||||
}
|
||||
|
||||
function disabledActivateDate(date) {
|
||||
if (activateIsImportFresh.value) return false
|
||||
const prev = activatePrevEffectiveTime.value
|
||||
if (!prev || !date) return false
|
||||
const prevDay = new Date(String(prev).replace(/-/g, '/'))
|
||||
prevDay.setHours(0, 0, 0, 0)
|
||||
const d = new Date(date)
|
||||
d.setHours(0, 0, 0, 0)
|
||||
return d.getTime() < prevDay.getTime()
|
||||
}
|
||||
|
||||
function disabledActivateHours() {
|
||||
const cur = activateForm.effectiveTime
|
||||
const curD = cur
|
||||
? new Date(String(normalizeToHour(cur)).replace(/-/g, '/'))
|
||||
: new Date()
|
||||
if (activateIsImportFresh.value) {
|
||||
const hours = new Set()
|
||||
for (const b of activateBoundaryTimes.value) {
|
||||
const bd = new Date(String(b).replace(/-/g, '/'))
|
||||
if (
|
||||
bd.getFullYear() === curD.getFullYear() &&
|
||||
bd.getMonth() === curD.getMonth() &&
|
||||
bd.getDate() === curD.getDate()
|
||||
) {
|
||||
hours.add(bd.getHours())
|
||||
}
|
||||
}
|
||||
return [...hours]
|
||||
}
|
||||
const prev = activatePrevEffectiveTime.value
|
||||
if (!prev) return []
|
||||
const prevD = new Date(String(prev).replace(/-/g, '/'))
|
||||
if (
|
||||
prevD.getFullYear() !== curD.getFullYear() ||
|
||||
prevD.getMonth() !== curD.getMonth() ||
|
||||
prevD.getDate() !== curD.getDate()
|
||||
) {
|
||||
return []
|
||||
}
|
||||
const minH = prevD.getHours()
|
||||
const hours = []
|
||||
for (let h = 0; h < minH; h++) hours.push(h)
|
||||
return hours
|
||||
}
|
||||
|
||||
function onActivatePickerVisible(visible) {
|
||||
if (visible && activateFormRef.value) {
|
||||
activateFormRef.value.validateField('effectiveTime')
|
||||
}
|
||||
}
|
||||
|
||||
function openActivateDialog(row, mode) {
|
||||
if (!row || row.psvId == null) return
|
||||
activateMode.value = mode
|
||||
activateForm.psvId = row.psvId
|
||||
activateForm.versionNo = row.versionNo || ''
|
||||
activateIsImportFresh.value = isImportFreshBuildMode(row.priceBuildMode)
|
||||
activatePrevEffectiveTime.value = findPrevVersionEffectiveTime(row)
|
||||
activateBoundaryTimes.value = activateIsImportFresh.value
|
||||
? collectActivateBoundaryTimes(row.psvId)
|
||||
: []
|
||||
const prev = activatePrevEffectiveTime.value
|
||||
if (mode === 'reschedule') {
|
||||
const cur = normalizeToHour(row.effectiveTime)
|
||||
if (activateIsImportFresh.value) {
|
||||
activateForm.effectiveTime = cur || pickActivateDefaultTime(prev, true)
|
||||
} else if (cur && (!prev || toTimeMs(cur) >= toTimeMs(prev))) {
|
||||
activateForm.effectiveTime = cur
|
||||
} else {
|
||||
activateForm.effectiveTime = pickActivateDefaultTime(prev, false)
|
||||
}
|
||||
} else {
|
||||
activateForm.effectiveTime = pickActivateDefaultTime(prev, activateIsImportFresh.value)
|
||||
}
|
||||
activateVisible.value = true
|
||||
}
|
||||
|
||||
function onActivateVersion(row) {
|
||||
openActivateDialog(row, 'activate')
|
||||
}
|
||||
|
||||
function onRescheduleVersion(row) {
|
||||
openActivateDialog(row, 'reschedule')
|
||||
}
|
||||
|
||||
function onActivateClosed() {
|
||||
activateMode.value = 'activate'
|
||||
activateForm.psvId = undefined
|
||||
activateForm.versionNo = ''
|
||||
activateForm.effectiveTime = undefined
|
||||
activatePrevEffectiveTime.value = undefined
|
||||
activateIsImportFresh.value = false
|
||||
activateBoundaryTimes.value = []
|
||||
activateFormRef.value && activateFormRef.value.clearValidate()
|
||||
}
|
||||
|
||||
function submitActivate() {
|
||||
activateFormRef.value.validate(valid => {
|
||||
if (!valid) return
|
||||
activateForm.effectiveTime = normalizeToHour(activateForm.effectiveTime)
|
||||
activateSubmitting.value = true
|
||||
mod.api.activatePriceSetVersion(activateForm.psvId, { effectiveTime: activateForm.effectiveTime })
|
||||
.then(() => {
|
||||
const eff = activateForm.effectiveTime
|
||||
const pending = eff && new Date(eff.replace(/-/g, '/')).getTime() > Date.now()
|
||||
const okMsg = activateMode.value === 'reschedule'
|
||||
? (pending ? '已更新为待生效' : '生效时间已更新,版本已生效')
|
||||
: (pending ? '已设为待生效' : '版本已启动')
|
||||
proxy.$modal.msgSuccess(okMsg)
|
||||
activateVisible.value = false
|
||||
load()
|
||||
})
|
||||
.finally(() => {
|
||||
activateSubmitting.value = false
|
||||
})
|
||||
})
|
||||
}
|
||||
function onCreateVersion() {
|
||||
const priceSetId = set.value.priceSetId
|
||||
if (priceSetId == null) return
|
||||
const hasDraft = (set.value.versions || []).some(v => v && String(v.status) === '0')
|
||||
if (hasDraft) {
|
||||
proxy.$modal.msgWarning('已有草稿版本,请先启动或删除后再新建')
|
||||
return
|
||||
}
|
||||
// Do not insert draft until user clicks finish on the build-price page.
|
||||
router.push({
|
||||
path: mod.routes.create,
|
||||
query: { priceSetId, step: '2', newVersion: '1' }
|
||||
})
|
||||
}
|
||||
function onRemoveVersion(row) {
|
||||
if (!row || row.psvId == null) return
|
||||
const msg = isEffectVersion(row)
|
||||
? "确认软删除已生效版本(生效时间:" + (row.effectiveTime || '未设置') + ")?版本及产品、明细将标记为已删除,上一版将自动恢复生效并清空失效时间。"
|
||||
: "确认删除" + (isPendingVersion(row) ? "待生效" : "草稿") + "版本(生效时间:" + (row.effectiveTime || '未设置') + ")?其明细与附加费将一并删除。"
|
||||
proxy.$modal.confirm(msg).then(() => {
|
||||
removingPsvId.value = row.psvId
|
||||
proxy.$modal.loading("正在删除,请稍候...")
|
||||
return mod.api.deletePriceSetVersion(row.psvId).then(() => {
|
||||
proxy.$modal.msgSuccess("版本已删除")
|
||||
load()
|
||||
}).finally(() => {
|
||||
removingPsvId.value = null
|
||||
proxy.$modal.closeLoading()
|
||||
})
|
||||
}).catch(() => {})
|
||||
}
|
||||
function onEditVersion(row) {
|
||||
if (!row || row.psvId == null) return
|
||||
const priceSetId = set.value.priceSetId
|
||||
const noMode = !row.priceBuildMode
|
||||
const noProducts = !(row.products || []).some(p => p && p.productCode)
|
||||
// Empty unfinished draft (new version, not finished): resume create step2 with build-mode choice
|
||||
if (isDraftVersion(row) && noMode && noProducts && priceSetId != null) {
|
||||
router.push({
|
||||
path: mod.routes.create,
|
||||
query: { priceSetId, psvId: row.psvId, step: '2', newVersion: '1' }
|
||||
})
|
||||
return
|
||||
}
|
||||
editPsvId.value = row.psvId
|
||||
editVisible.value = true
|
||||
}
|
||||
|
||||
/** 关闭内嵌编辑面板,刷新版本列表(明细数、状态可能变化) */
|
||||
function closeEdit() {
|
||||
editVisible.value = false
|
||||
load()
|
||||
}
|
||||
function goList() {
|
||||
const obj = { path: mod.routes.list }
|
||||
if (proxy.$tab) {
|
||||
proxy.$tab.closeOpenPage(obj)
|
||||
} else {
|
||||
proxy.$router.push(obj)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
refData.loadProducts()
|
||||
load()
|
||||
})
|
||||
|
||||
// 从其他页签切回(新建完成跳转、列表点另一条详情)时同步数据;
|
||||
// 从新建/编辑版本页返回时即使套餐ID相同也强制刷新版本列表
|
||||
onActivated(() => {
|
||||
syncFromRoute(pendingRefresh.value)
|
||||
})
|
||||
|
||||
// 组件已处于激活状态时路由 query 变化的兜底
|
||||
watch(() => route.query.priceSetId, () => {
|
||||
syncFromRoute(false)
|
||||
})
|
||||
|
||||
// 离开详情页去新建/编辑套餐版本,返回后需要重新加载
|
||||
onBeforeRouteLeave((to) => {
|
||||
if (to && to.path === mod.routes.create) {
|
||||
pendingRefresh.value = true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.activate-hint {
|
||||
margin: -4px 0 0 100px;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* Hour-only: hide minute/second columns in activate datetime popper */
|
||||
.activate-hour-picker .el-time-spinner__wrapper:nth-child(2),
|
||||
.activate-hour-picker .el-time-spinner__wrapper:nth-child(3) {
|
||||
display: none;
|
||||
}
|
||||
.activate-hour-picker .el-time-spinner__wrapper:nth-child(1) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,297 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<basic-table
|
||||
ref="tableRef"
|
||||
:columns="columns"
|
||||
:search-columns="searchColumns"
|
||||
:query="queryList"
|
||||
row-key="priceSetId"
|
||||
>
|
||||
<template #toolbar>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
icon="Plus"
|
||||
@click="handleCreate"
|
||||
v-hasPermi="addPerm"
|
||||
>{{ mod.labels.createButton }}</el-button>
|
||||
</el-col>
|
||||
</template>
|
||||
|
||||
<template #search-customerId="{ model }">
|
||||
<customer-select
|
||||
v-model="model.customerId"
|
||||
option-select
|
||||
remote-search
|
||||
:page-size="20"
|
||||
placeholder="客户名称 / 代码"
|
||||
style="width: 240px"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #col-setType="{ row }">
|
||||
<el-tag :type="row.setType === 'FIXED' ? 'warning' : 'primary'" size="small" effect="plain">
|
||||
{{ row.setType === 'FIXED' ? 'FIXED 固定价' : 'DISCOUNT 折扣' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
|
||||
<template #col-status="{ row }">
|
||||
<el-tag :type="statusTag(row)" size="small">{{ statusLabel(row) }}</el-tag>
|
||||
</template>
|
||||
|
||||
<template #col-boundCustomers="{ row }">
|
||||
<span v-if="row.boundCustomers">{{ row.boundCustomers }}</span>
|
||||
<span v-else class="text-muted">-</span>
|
||||
<el-tag v-if="row.customerCount > 0" size="small" type="info" effect="plain" class="cust-count">{{ row.customerCount }}</el-tag>
|
||||
</template>
|
||||
|
||||
<template #col-activeDetail="{ row }">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
:loading="activeLoadingId === row.priceSetId"
|
||||
@click="handleActiveDetail(row)"
|
||||
v-hasPermi="queryPerm"
|
||||
>查看明细</el-button>
|
||||
</template>
|
||||
|
||||
<template #col-action="{ row }">
|
||||
<el-button link type="primary" @click="handleDetail(row)" v-hasPermi="queryPerm">详情</el-button>
|
||||
<el-button link type="primary" @click="openBasicEdit(row)" v-hasPermi="editPerm">修改</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 'E'"
|
||||
link
|
||||
type="warning"
|
||||
@click="handleDisable(row)"
|
||||
v-hasPermi="auditPerm"
|
||||
>停用</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 'D' || row.status === 'S'"
|
||||
link
|
||||
type="success"
|
||||
@click="handleEnable(row)"
|
||||
v-hasPermi="auditPerm"
|
||||
>启用</el-button>
|
||||
</template>
|
||||
</basic-table>
|
||||
|
||||
<el-dialog v-model="basicDialog.visible" :title="basicDialogTitle" width="520px" append-to-body destroy-on-close>
|
||||
<el-form ref="basicFormRef" :model="basicForm" :rules="basicRules" label-width="96px">
|
||||
<el-form-item :label="mod.labels.entityCode">
|
||||
<el-input :model-value="basicForm.priceSetCode" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item :label="mod.labels.entityName" prop="priceSetName">
|
||||
<el-input v-model="basicForm.priceSetName" maxlength="50" show-word-limit :placeholder="'请输入' + mod.labels.entityName" />
|
||||
</el-form-item>
|
||||
<el-form-item label="说明备注" prop="priceSetNote">
|
||||
<el-input v-model="basicForm.priceSetNote" type="textarea" :rows="3" maxlength="200" show-word-limit placeholder="可选" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="basicDialog.visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="basicDialog.saving" @click="submitBasicEdit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<set-version-detail-drawer
|
||||
v-model="drawerVisible"
|
||||
:version="activeVersion"
|
||||
:set-type="drawerSetType"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="ExclusiveQuote">
|
||||
import SetVersionDetailDrawer from '../set/components/detail/SetVersionDetailDrawer.vue'
|
||||
import CustomerSelect from '@/components/CustomerSelect/index.vue'
|
||||
import { setStatusLabel, setStatusTagType } from '../set/utils/priceSet'
|
||||
import { exclusiveQuoteApi } from '@/api/price/exclusive'
|
||||
import { providePriceSetModule, createExclusiveModule } from '../set/composables/usePriceSetModule'
|
||||
|
||||
// Same-component inject cannot see provide(); use return value for this page, provide for children.
|
||||
const mod = providePriceSetModule(createExclusiveModule(exclusiveQuoteApi))
|
||||
const addPerm = [`${mod.permPrefix}:add`]
|
||||
const queryPerm = [`${mod.permPrefix}:query`]
|
||||
const editPerm = [`${mod.permPrefix}:edit`]
|
||||
const auditPerm = [`${mod.permPrefix}:audit`]
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const tableRef = ref()
|
||||
const isFirstActivate = ref(true)
|
||||
|
||||
const searchColumns = computed(() => [
|
||||
{
|
||||
label: '状态',
|
||||
prop: 'status',
|
||||
component: 'select',
|
||||
options: [
|
||||
{ label: '待启用', value: 'D' },
|
||||
{ label: '生效', value: 'E' },
|
||||
{ label: '已停用', value: 'S' }
|
||||
],
|
||||
placeholder: '全部状态',
|
||||
width: '140px'
|
||||
},
|
||||
{ label: '关键字', prop: 'keyword', component: 'input', placeholder: mod.labels.keywordPlaceholder, width: '200px' },
|
||||
{ label: '客户', prop: 'customerId', placeholder: '客户名称 / 代码', width: '240px' }
|
||||
])
|
||||
|
||||
const columns = computed(() => [
|
||||
{ label: mod.labels.entityCode, prop: 'priceSetCode', width: 110 },
|
||||
{ label: mod.labels.entityName, prop: 'priceSetName', minWidth: 160 },
|
||||
{ label: '类型', prop: 'setType', width: 140 },
|
||||
{ label: '绑定客户', prop: 'boundCustomers', minWidth: 220 },
|
||||
{ label: '说明', prop: 'priceSetNote', minWidth: 140 },
|
||||
{ label: '状态', prop: 'status', width: 90 },
|
||||
{ label: '生效明细', prop: 'activeDetail', width: 100 },
|
||||
{ label: '操作', prop: 'action', width: 200, showInToolbar: false }
|
||||
])
|
||||
|
||||
function queryList(params) {
|
||||
return mod.api.listPriceSet(params)
|
||||
}
|
||||
|
||||
function statusLabel(row) {
|
||||
return setStatusLabel(row)
|
||||
}
|
||||
|
||||
function statusTag(row) {
|
||||
return setStatusTagType(row)
|
||||
}
|
||||
|
||||
function handleCreate() {
|
||||
proxy.$router.push({ path: mod.routes.create })
|
||||
}
|
||||
|
||||
function handleDetail(row) {
|
||||
proxy.$router.push({ path: mod.routes.detail, query: { priceSetId: row.priceSetId } })
|
||||
}
|
||||
|
||||
|
||||
const basicDialog = reactive({ visible: false, saving: false })
|
||||
const basicFormRef = ref()
|
||||
const basicForm = reactive({
|
||||
priceSetId: null,
|
||||
priceSetCode: '',
|
||||
priceSetName: '',
|
||||
priceSetNote: ''
|
||||
})
|
||||
const basicRules = {
|
||||
priceSetName: [{ required: true, message: '名称不能为空', trigger: 'blur' }]
|
||||
}
|
||||
const basicDialogTitle = computed(() => mod.labels.basicEditTitle)
|
||||
|
||||
function openBasicEdit(row) {
|
||||
if (!row || row.priceSetId == null) return
|
||||
basicForm.priceSetId = row.priceSetId
|
||||
basicForm.priceSetCode = row.priceSetCode || ''
|
||||
basicForm.priceSetName = row.priceSetName || ''
|
||||
basicForm.priceSetNote = row.priceSetNote || ''
|
||||
basicDialog.visible = true
|
||||
nextTick(() => basicFormRef.value && basicFormRef.value.clearValidate())
|
||||
}
|
||||
|
||||
function submitBasicEdit() {
|
||||
basicFormRef.value.validate(valid => {
|
||||
if (!valid) return
|
||||
basicDialog.saving = true
|
||||
mod.api.updatePriceSetBasic({
|
||||
priceSetId: basicForm.priceSetId,
|
||||
priceSetName: basicForm.priceSetName,
|
||||
priceSetNote: basicForm.priceSetNote
|
||||
}).then(() => {
|
||||
proxy.$modal.msgSuccess('保存成功')
|
||||
basicDialog.visible = false
|
||||
refresh()
|
||||
}).finally(() => {
|
||||
basicDialog.saving = false
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/** 生效明细:拉详情找生效版本(status=1)弹抽屉 */
|
||||
const drawerVisible = ref(false)
|
||||
const activeVersion = ref(null)
|
||||
const drawerSetType = ref('FIXED')
|
||||
const activeLoadingId = ref(null)
|
||||
|
||||
function handleActiveDetail(row) {
|
||||
activeLoadingId.value = row.priceSetId
|
||||
mod.api.getPriceSet(row.priceSetId).then(res => {
|
||||
const data = res.data || {}
|
||||
const versions = data.versions || []
|
||||
const active = versions.find(v => String(v.status) === '1')
|
||||
if (!active || active.psvId == null) {
|
||||
proxy.$modal.msgWarning(mod.labels.noActiveVersion)
|
||||
return
|
||||
}
|
||||
drawerSetType.value = row.setType
|
||||
activeVersion.value = { psvId: active.psvId, versionNo: active.versionNo }
|
||||
drawerVisible.value = true
|
||||
}).finally(() => {
|
||||
activeLoadingId.value = null
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
function refresh() {
|
||||
tableRef.value?.refresh()
|
||||
}
|
||||
|
||||
onActivated(() => {
|
||||
if (isFirstActivate.value) {
|
||||
isFirstActivate.value = false
|
||||
return
|
||||
}
|
||||
tableRef.value?.reload()
|
||||
})
|
||||
|
||||
|
||||
function handleDisable(row) {
|
||||
proxy.$modal.confirm(mod.labels.disableConfirm.replace('{name}', row.priceSetName)).then(() => {
|
||||
return mod.api.disablePriceSet(row.priceSetId)
|
||||
}).then(() => {
|
||||
proxy.$modal.msgSuccess('已停用')
|
||||
refresh()
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
function handleEnable(row) {
|
||||
proxy.$modal.confirm(mod.labels.enableConfirm.replace('{name}', row.priceSetName)).then(() => {
|
||||
return mod.api.enablePriceSet(row.priceSetId)
|
||||
}).then(() => {
|
||||
proxy.$modal.msgSuccess('已启用')
|
||||
tableRef.value.refresh()
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
function handleCopy(row) {
|
||||
mod.api.copyPriceSet(row.priceSetId).then(res => {
|
||||
proxy.$modal.msgSuccess('已复制为草稿')
|
||||
proxy.$router.push({ path: mod.routes.create, query: { priceSetId: res.data } })
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.text-muted { color: #909399; }
|
||||
.cust-count { margin-left: 6px; vertical-align: middle; }
|
||||
.hint-card {
|
||||
margin-top: 16px;
|
||||
}
|
||||
.hint-body {
|
||||
font-size: 13px;
|
||||
color: #595959;
|
||||
line-height: 1.7;
|
||||
padding: 8px 12px;
|
||||
background: #f6ffed;
|
||||
border: 1px solid #b7eb8f;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.hint-body b {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,19 @@
|
||||
<template>
|
||||
<div class="app-container order-bill-query">
|
||||
<div v-if="rebillFloatVisible" class="retry-status-float">
|
||||
<el-tooltip :content="rebillFloatTooltip" placement="bottom">
|
||||
<el-button class="retry-status-button" type="primary" plain size="small" @click="openRebillProgress()">
|
||||
<span class="retry-progress-ring" :style="{ '--retry-progress': rebillProgressDegree + 'deg' }" aria-hidden="true">
|
||||
<span class="retry-progress-value">{{ rebillProgressText }}</span>
|
||||
</span>
|
||||
<span class="retry-status-text">
|
||||
<strong>{{ rebillFloatTitle }}</strong>
|
||||
<small>{{ rebillFloatSummary }}</small>
|
||||
</span>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
<basic-table
|
||||
ref="tableRef"
|
||||
:columns="columns"
|
||||
@@ -161,13 +175,12 @@
|
||||
</basic-table>
|
||||
<el-dialog
|
||||
v-model="rebillProgressVisible"
|
||||
title="本地重新计费"
|
||||
:title="rebillDialogTitle"
|
||||
width="480px"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
:close-on-click-modal="!batchLoading"
|
||||
:close-on-press-escape="!batchLoading"
|
||||
:show-close="!batchLoading"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="true"
|
||||
:show-close="true"
|
||||
>
|
||||
<el-progress
|
||||
:percentage="rebillProgressPercent"
|
||||
@@ -182,10 +195,17 @@
|
||||
/ 成功 {{ rebillProgressSuccess }}
|
||||
/ 失败 {{ rebillProgressFail }}
|
||||
</div>
|
||||
<p v-if="batchLoading" class="rebill-progress__hint">请勿关闭页面,大量订单重新计费可能需要一些时间</p>
|
||||
<p v-if="batchLoading" class="rebill-progress__hint">计费在后台进行,可收起弹窗继续操作;请勿关闭或刷新本页</p>
|
||||
<template #footer>
|
||||
<el-button v-if="!batchLoading" type="primary" @click="rebillProgressVisible = false">关闭</el-button>
|
||||
<el-button v-else disabled>执行中...</el-button>
|
||||
<el-button v-if="batchLoading" @click="rebillProgressVisible = false">收起</el-button>
|
||||
<el-button
|
||||
v-if="batchLoading"
|
||||
type="danger"
|
||||
plain
|
||||
:loading="rebillCancelLoading"
|
||||
@click="handleCancelRebill"
|
||||
>取消计费</el-button>
|
||||
<el-button v-else type="primary" @click="rebillProgressVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<order-source-sync-log-drawer ref="logDrawerRef" />
|
||||
@@ -201,7 +221,7 @@
|
||||
|
||||
<script setup name="OrderBillQuery">
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { listOrderBill, localRebillAsync } from '@/api/price/billing'
|
||||
import { listOrderBill, localRebillAsync, pollLocalRebillTask, cancelLocalRebill, getLocalRebillTask } from '@/api/price/billing'
|
||||
import { exportAsyncFromTable } from '@/utils/excelExport'
|
||||
import ExcelExportFieldDialog from '@/components/ExcelExportFieldDialog/index.vue'
|
||||
import { ORDER_BILL_EXPORT_FIELDS } from '@/utils/exportFieldDefs'
|
||||
@@ -214,6 +234,7 @@ const logDrawerRef = ref()
|
||||
const productCheckDrawerRef = ref()
|
||||
const checkLoading = ref(false)
|
||||
const batchLoading = ref(false)
|
||||
const rebillFinished = ref(false)
|
||||
const rebillProgressVisible = ref(false)
|
||||
const rebillProgressPercent = ref(0)
|
||||
const rebillProgressMessage = ref('')
|
||||
@@ -221,6 +242,69 @@ const rebillProgressStatus = ref(undefined)
|
||||
const rebillProgressTotal = ref(0)
|
||||
const rebillProgressSuccess = ref(0)
|
||||
const rebillProgressFail = ref(0)
|
||||
const rebillTaskId = ref('')
|
||||
const rebillCancelLoading = ref(false)
|
||||
let rebillFloatHideTimer = null
|
||||
const REBILL_TASK_STORAGE_KEY = 'price:orderBill:localRebillTaskId'
|
||||
|
||||
function saveRebillTaskId(taskId) {
|
||||
rebillTaskId.value = taskId ? String(taskId) : ''
|
||||
try {
|
||||
if (rebillTaskId.value) sessionStorage.setItem(REBILL_TASK_STORAGE_KEY, rebillTaskId.value)
|
||||
else sessionStorage.removeItem(REBILL_TASK_STORAGE_KEY)
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
function clearRebillTaskId() {
|
||||
saveRebillTaskId('')
|
||||
}
|
||||
|
||||
function readStoredRebillTaskId() {
|
||||
try {
|
||||
return String(sessionStorage.getItem(REBILL_TASK_STORAGE_KEY) || '').trim()
|
||||
} catch (e) {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
const rebillFloatVisible = computed(() => batchLoading.value || rebillFinished.value)
|
||||
const rebillProgressDegree = computed(() => Math.round(((rebillProgressPercent.value || 0) / 100) * 360))
|
||||
const rebillProgressText = computed(() => (rebillProgressPercent.value || 0) + '%')
|
||||
const rebillDialogTitle = computed(() => {
|
||||
if (batchLoading.value) return '异步重新计费进行中'
|
||||
if (rebillFinished.value) return '异步重新计费完成'
|
||||
return '本地重新计费'
|
||||
})
|
||||
const rebillFloatTitle = computed(() => {
|
||||
if (batchLoading.value) return '异步重新计费中'
|
||||
if (rebillProgressStatus.value === 'exception') return '重新计费失败'
|
||||
if (rebillProgressStatus.value === 'warning' && String(rebillProgressMessage.value || '').includes('已取消')) return '重新计费已取消'
|
||||
if (rebillFinished.value) return '重新计费完成'
|
||||
return '异步重新计费'
|
||||
})
|
||||
const rebillFloatSummary = computed(() =>
|
||||
'成功 ' + (rebillProgressSuccess.value || 0) +
|
||||
' / 失败 ' + (rebillProgressFail.value || 0) +
|
||||
' / 总数 ' + (rebillProgressTotal.value || 0)
|
||||
)
|
||||
const rebillFloatTooltip = computed(() => rebillProgressMessage.value || '点击查看详情')
|
||||
|
||||
function openRebillProgress() {
|
||||
rebillProgressVisible.value = true
|
||||
}
|
||||
|
||||
function scheduleHideRebillFloat() {
|
||||
if (rebillFloatHideTimer) {
|
||||
clearTimeout(rebillFloatHideTimer)
|
||||
rebillFloatHideTimer = null
|
||||
}
|
||||
rebillFloatHideTimer = window.setTimeout(() => {
|
||||
if (!batchLoading.value && rebillFinished.value) {
|
||||
rebillFinished.value = false
|
||||
}
|
||||
rebillFloatHideTimer = null
|
||||
}, 8000)
|
||||
}
|
||||
const tableHeight = ref(480)
|
||||
let tableResizeObserver = null
|
||||
const MAX_ORDER_IDENTIFIERS = 1000
|
||||
@@ -279,7 +363,7 @@ const billingFailStatusOptions = [
|
||||
{ label: '产品价未匹配国家', value: 24 },
|
||||
{ label: '产品价未匹配重量段', value: 25 },
|
||||
{ label: '未绑定套餐', value: 4 },
|
||||
{ label: '无生效套餐版本', value: 5 },
|
||||
{ label: '套餐未启用/无生效版本', value: 5 },
|
||||
{ label: '已生成应收账单', value: 6 },
|
||||
{ label: '产品档案缺失', value: 7 },
|
||||
{ label: '未命中价格', value: 8 },
|
||||
@@ -504,49 +588,69 @@ function handleCheckProducts() {
|
||||
}
|
||||
}
|
||||
|
||||
async function runLocalRebill(payload, confirmText) {
|
||||
try {
|
||||
await ElMessageBox.confirm(confirmText, '二次确认', { type: 'warning' })
|
||||
} catch (e) {
|
||||
return
|
||||
function applyRebillProgress(t) {
|
||||
const total = Number(t.total) || 0
|
||||
const success = Number(t.success) || 0
|
||||
const fail = Number(t.fail) || 0
|
||||
rebillProgressPercent.value = Math.min(100, Number(t.percent) || 0)
|
||||
rebillProgressTotal.value = total
|
||||
rebillProgressSuccess.value = success
|
||||
rebillProgressFail.value = fail
|
||||
if (t.status === 'PENDING') {
|
||||
rebillProgressMessage.value = '排队中…'
|
||||
} else if (t.status === 'RUNNING' && String(t.message || '').startsWith('cancelling')) {
|
||||
rebillProgressMessage.value = '正在取消,等待当前批次结束… ' + success + '/' + total
|
||||
} else if (t.status === 'RUNNING') {
|
||||
rebillProgressMessage.value = '正在重新计费 ' + success + '/' + total +
|
||||
(fail ? ('(失败 ' + fail + ')') : '')
|
||||
}
|
||||
}
|
||||
|
||||
function finishRebillFromTask(task, cancelled) {
|
||||
const success = Number(task && task.success) || 0
|
||||
const fail = Number(task && task.fail) || 0
|
||||
rebillProgressPercent.value = Math.min(100, Number(task && task.percent) || 100)
|
||||
rebillProgressStatus.value = cancelled ? 'warning' : (fail > 0 ? 'warning' : 'success')
|
||||
rebillProgressTotal.value = Number(task && task.total) || rebillProgressTotal.value
|
||||
rebillProgressSuccess.value = success
|
||||
rebillProgressFail.value = fail
|
||||
rebillProgressMessage.value = cancelled
|
||||
? ('已取消:成功 ' + success + ' / 失败 ' + fail)
|
||||
: ('完成:成功 ' + success + ' / 失败 ' + fail)
|
||||
rebillFinished.value = true
|
||||
clearRebillTaskId()
|
||||
tableRef.value?.refresh?.()
|
||||
if (cancelled) ElMessage.warning(rebillProgressMessage.value)
|
||||
else ElMessage.success(rebillProgressMessage.value)
|
||||
scheduleHideRebillFloat()
|
||||
}
|
||||
|
||||
async function resumeRebillTask(taskId) {
|
||||
if (!taskId || batchLoading.value) return
|
||||
batchLoading.value = true
|
||||
rebillProgressVisible.value = true
|
||||
rebillFinished.value = false
|
||||
rebillCancelLoading.value = false
|
||||
saveRebillTaskId(taskId)
|
||||
rebillProgressVisible.value = false
|
||||
rebillProgressPercent.value = 0
|
||||
rebillProgressStatus.value = undefined
|
||||
rebillProgressMessage.value = '任务提交中…'
|
||||
rebillProgressMessage.value = '恢复进度中…'
|
||||
rebillProgressTotal.value = 0
|
||||
rebillProgressSuccess.value = 0
|
||||
rebillProgressFail.value = 0
|
||||
let superseded = false
|
||||
try {
|
||||
const { task } = await localRebillAsync(payload, (t) => {
|
||||
const total = Number(t.total) || 0
|
||||
const success = Number(t.success) || 0
|
||||
const fail = Number(t.fail) || 0
|
||||
rebillProgressPercent.value = Math.min(100, Number(t.percent) || 0)
|
||||
rebillProgressTotal.value = total
|
||||
rebillProgressSuccess.value = success
|
||||
rebillProgressFail.value = fail
|
||||
if (t.status === 'PENDING') {
|
||||
rebillProgressMessage.value = '排队中…'
|
||||
} else if (t.status === 'RUNNING') {
|
||||
rebillProgressMessage.value = '正在重新计费 ' + success + '/' + total +
|
||||
(fail ? ('(失败 ' + fail + ')') : '')
|
||||
}
|
||||
})
|
||||
const success = Number(task && task.success) || 0
|
||||
const fail = Number(task && task.fail) || 0
|
||||
rebillProgressPercent.value = 100
|
||||
rebillProgressStatus.value = fail > 0 ? 'warning' : 'success'
|
||||
rebillProgressTotal.value = Number(task && task.total) || rebillProgressTotal.value
|
||||
rebillProgressSuccess.value = success
|
||||
rebillProgressFail.value = fail
|
||||
rebillProgressMessage.value = '完成:成功 ' + success + ' / 失败 ' + fail
|
||||
tableRef.value?.refresh?.()
|
||||
const result = await pollLocalRebillTask(taskId, applyRebillProgress)
|
||||
finishRebillFromTask(result && result.task, !!(result && result.cancelled))
|
||||
} catch (e) {
|
||||
if (e && e.superseded) {
|
||||
superseded = true
|
||||
return
|
||||
}
|
||||
const task = e && e.task
|
||||
rebillProgressStatus.value = 'exception'
|
||||
rebillProgressMessage.value = (e && e.message) || '重新计费失败'
|
||||
rebillFinished.value = true
|
||||
if (task) {
|
||||
rebillProgressPercent.value = Math.min(100, Number(task.percent) || 100)
|
||||
rebillProgressTotal.value = Number(task.total) || 0
|
||||
@@ -554,8 +658,122 @@ async function runLocalRebill(payload, confirmText) {
|
||||
rebillProgressFail.value = Number(task.fail) || 0
|
||||
tableRef.value?.refresh?.()
|
||||
}
|
||||
clearRebillTaskId()
|
||||
rebillProgressVisible.value = true
|
||||
scheduleHideRebillFloat()
|
||||
} finally {
|
||||
batchLoading.value = false
|
||||
// Keep loading if a newer poll replaced this one.
|
||||
if (!superseded) batchLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function tryRestoreRebillProgress() {
|
||||
const taskId = readStoredRebillTaskId()
|
||||
if (!taskId) return
|
||||
try {
|
||||
const res = await getLocalRebillTask(taskId)
|
||||
const task = (res && res.data) || {}
|
||||
const st = String(task.status || '').toUpperCase()
|
||||
if (st === 'PENDING' || st === 'RUNNING') {
|
||||
applyRebillProgress(task)
|
||||
resumeRebillTask(taskId)
|
||||
return
|
||||
}
|
||||
if (st === 'SUCCESS' || st === 'CANCELLED' || st === 'FAIL') {
|
||||
clearRebillTaskId()
|
||||
return
|
||||
}
|
||||
clearRebillTaskId()
|
||||
} catch (e) {
|
||||
clearRebillTaskId()
|
||||
}
|
||||
}
|
||||
|
||||
async function runLocalRebill(payload, confirmText) {
|
||||
if (batchLoading.value) {
|
||||
openRebillProgress()
|
||||
return
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(confirmText, '二次确认', { type: 'warning' })
|
||||
} catch (e) {
|
||||
return
|
||||
}
|
||||
if (rebillFloatHideTimer) {
|
||||
clearTimeout(rebillFloatHideTimer)
|
||||
rebillFloatHideTimer = null
|
||||
}
|
||||
batchLoading.value = true
|
||||
rebillFinished.value = false
|
||||
rebillCancelLoading.value = false
|
||||
clearRebillTaskId()
|
||||
rebillProgressVisible.value = true
|
||||
rebillProgressPercent.value = 0
|
||||
rebillProgressStatus.value = undefined
|
||||
rebillProgressMessage.value = '任务提交中…'
|
||||
rebillProgressTotal.value = 0
|
||||
rebillProgressSuccess.value = 0
|
||||
rebillProgressFail.value = 0
|
||||
window.setTimeout(() => {
|
||||
if (batchLoading.value) rebillProgressVisible.value = false
|
||||
}, 600)
|
||||
let superseded = false
|
||||
try {
|
||||
const result = await localRebillAsync(
|
||||
payload,
|
||||
applyRebillProgress,
|
||||
{ getTaskId: (id) => { saveRebillTaskId(id) } }
|
||||
)
|
||||
finishRebillFromTask(result && result.task, !!(result && result.cancelled))
|
||||
} catch (e) {
|
||||
if (e && e.superseded) {
|
||||
superseded = true
|
||||
return
|
||||
}
|
||||
const task = e && e.task
|
||||
rebillProgressStatus.value = 'exception'
|
||||
rebillProgressMessage.value = (e && e.message) || '重新计费失败'
|
||||
rebillFinished.value = true
|
||||
if (task) {
|
||||
rebillProgressPercent.value = Math.min(100, Number(task.percent) || 100)
|
||||
rebillProgressTotal.value = Number(task.total) || 0
|
||||
rebillProgressSuccess.value = Number(task.success) || 0
|
||||
rebillProgressFail.value = Number(task.fail) || 0
|
||||
tableRef.value?.refresh?.()
|
||||
}
|
||||
clearRebillTaskId()
|
||||
rebillProgressVisible.value = true
|
||||
scheduleHideRebillFloat()
|
||||
} finally {
|
||||
if (!superseded) batchLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCancelRebill() {
|
||||
if (!batchLoading.value || !rebillTaskId.value || rebillCancelLoading.value) return
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'确认取消当前重新计费?已计费完成的订单会保留,未开始的将停止。',
|
||||
'取消计费',
|
||||
{ type: 'warning' }
|
||||
)
|
||||
} catch (e) {
|
||||
return
|
||||
}
|
||||
rebillCancelLoading.value = true
|
||||
try {
|
||||
const res = await cancelLocalRebill(rebillTaskId.value)
|
||||
const task = (res && res.data) || {}
|
||||
rebillProgressPercent.value = Math.min(100, Number(task.percent) || rebillProgressPercent.value || 0)
|
||||
rebillProgressTotal.value = Number(task.total) || rebillProgressTotal.value
|
||||
rebillProgressSuccess.value = Number(task.success) || rebillProgressSuccess.value
|
||||
rebillProgressFail.value = Number(task.fail) || rebillProgressFail.value
|
||||
rebillProgressMessage.value = '取消请求已提交,等待当前批次结束…'
|
||||
ElMessage.success('已提交取消')
|
||||
} catch (e) {
|
||||
// global interceptor shows error
|
||||
} finally {
|
||||
rebillCancelLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -645,6 +863,7 @@ onMounted(() => {
|
||||
}
|
||||
})
|
||||
window.addEventListener('resize', updateTableHeight)
|
||||
tryRestoreRebillProgress()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
@@ -653,6 +872,10 @@ onBeforeUnmount(() => {
|
||||
tableResizeObserver.disconnect()
|
||||
tableResizeObserver = null
|
||||
}
|
||||
if (rebillFloatHideTimer) {
|
||||
clearTimeout(rebillFloatHideTimer)
|
||||
rebillFloatHideTimer = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -666,4 +889,64 @@ onBeforeUnmount(() => {
|
||||
.rebill-progress__msg { margin-top: 12px; color: var(--el-text-color-regular); }
|
||||
.rebill-progress__text { margin-top: 8px; font-size: 13px; color: var(--el-text-color-secondary); }
|
||||
.rebill-progress__hint { margin-top: 12px; font-size: 12px; color: var(--el-color-warning); }
|
||||
.order-bill-query .retry-status-float {
|
||||
position: fixed;
|
||||
top: 92px;
|
||||
right: 24px;
|
||||
z-index: 3001;
|
||||
}
|
||||
.order-bill-query .retry-status-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 200px;
|
||||
min-height: 68px;
|
||||
padding: 6px 16px 6px 7px;
|
||||
border-radius: 36px;
|
||||
}
|
||||
.order-bill-query .retry-progress-ring {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 54px;
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
border-radius: 50%;
|
||||
background: conic-gradient(
|
||||
var(--el-color-primary) 0deg var(--retry-progress, 45deg),
|
||||
var(--el-color-primary-light-8) var(--retry-progress, 45deg) 360deg
|
||||
);
|
||||
}
|
||||
.order-bill-query .retry-progress-ring::after {
|
||||
position: absolute;
|
||||
inset: 5px;
|
||||
content: '';
|
||||
border-radius: 50%;
|
||||
background: var(--el-bg-color);
|
||||
}
|
||||
.order-bill-query .retry-progress-value {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
color: var(--el-color-primary);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
}
|
||||
.order-bill-query .retry-status-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 3px;
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.order-bill-query .retry-status-text strong {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.order-bill-query .retry-status-text small {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 11px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -44,7 +44,9 @@ import { ArrowLeft } from '@element-plus/icons-vue'
|
||||
import StepDetails from './StepDetails.vue'
|
||||
import { usePriceSetRefData } from '../../composables/usePriceSetRefData'
|
||||
import { isBuildModeLockedVersion } from '../../utils/priceSet'
|
||||
import { getPriceSet, savePriceSetAsync } from '@/api/price/set'
|
||||
import { usePriceSetModule } from '../../composables/usePriceSetModule'
|
||||
|
||||
const { api } = usePriceSetModule()
|
||||
|
||||
const props = defineProps({
|
||||
priceSetId: { type: [Number, String], default: undefined },
|
||||
@@ -82,7 +84,7 @@ function load() {
|
||||
}
|
||||
loading.value = true
|
||||
loaded.value = false
|
||||
getPriceSet(id).then(res => {
|
||||
api.getPriceSet(id).then(res => {
|
||||
const data = res.data || {}
|
||||
setType.value = data.setType || 'FIXED'
|
||||
priceSetName.value = data.priceSetName || ''
|
||||
@@ -137,7 +139,7 @@ function reloadSavedProducts() {
|
||||
const id = props.priceSetId
|
||||
if (!id) return
|
||||
loading.value = true
|
||||
getPriceSet(id).then(res => {
|
||||
api.getPriceSet(id).then(res => {
|
||||
const data = res.data || {}
|
||||
const versions = data.versions || []
|
||||
const qPsv = props.psvId || version.psvId
|
||||
@@ -154,7 +156,7 @@ function reloadSavedProducts() {
|
||||
async function reloadProductsAfterImportModeSave() {
|
||||
const id = props.priceSetId
|
||||
if (!id) return
|
||||
const res = await getPriceSet(id)
|
||||
const res = await api.getPriceSet(id)
|
||||
const data = res.data || {}
|
||||
const versions = data.versions || []
|
||||
const qPsv = props.psvId || version.psvId
|
||||
@@ -190,7 +192,7 @@ async function finish() {
|
||||
priceBuildMode: version.priceBuildMode
|
||||
}
|
||||
}
|
||||
await savePriceSetAsync(payload)
|
||||
await api.savePriceSetAsync(payload)
|
||||
savedPriceBuildMode.value = version.priceBuildMode
|
||||
await nextTick()
|
||||
if (version.priceBuildMode === 'PULL' && detailsRef.value && detailsRef.value.pullAllFromLibrary) {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
@click="handlePullLibrary"
|
||||
>拉取产品列表</el-button>
|
||||
<el-dropdown
|
||||
v-if="showImportAction && !isModifyImport"
|
||||
v-if="showImportAction && !isModifyImport && showCountryImport"
|
||||
trigger="click"
|
||||
@command="handleImportCommand"
|
||||
>
|
||||
@@ -44,7 +44,8 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="build-choice">
|
||||
<!-- Exclusive quote: only Excel fresh build; hide the mode picker and default IMPORT_FRESH -->
|
||||
<div v-if="!onlyImportFresh" class="build-choice">
|
||||
<p class="build-choice__tip">创建价格方式</p>
|
||||
<div class="build-choice__cards">
|
||||
<button
|
||||
@@ -76,7 +77,7 @@
|
||||
@click="chooseBuildMode('IMPORT_FRESH')"
|
||||
>
|
||||
<div class="build-card__title">Excel 新建价</div>
|
||||
<div class="build-card__desc">不延用以前的产品价格,不带出上一版,上传 Excel 直接生成套餐销售价</div>
|
||||
<div class="build-card__desc">{{ importFreshCardDesc }}</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -227,9 +228,14 @@ import {
|
||||
surchargeAppliesToRow,
|
||||
toDetailPayload
|
||||
} from '../../utils/priceSet'
|
||||
import { getPriceSet, listSetDetailByProduct, listSetDetailByVersion, importSetDetailPricesAsync, pullSetDetailsFromLibraryAsync, previewPullFromLibrary, previewImportDetailPrices, updateSetDetail, updateSetDetailSurchargeAsync, delSetDetail } from '@/api/price/set'
|
||||
import { usePriceSetModule } from '../../composables/usePriceSetModule'
|
||||
import { listRegionOptions } from '@/api/data/region'
|
||||
|
||||
const mod = usePriceSetModule()
|
||||
const { api } = mod
|
||||
const onlyImportFresh = computed(() => !!mod.onlyImportFresh)
|
||||
const showCountryImport = computed(() => !mod.exclusiveQuote)
|
||||
|
||||
const props = defineProps({
|
||||
details: { type: Array, required: true },
|
||||
products: { type: Array, default: () => [] },
|
||||
@@ -326,7 +332,18 @@ const importActionLabel = computed(() =>
|
||||
isModifyImport.value ? '导入修改价格' : '导入 Excel 建价'
|
||||
)
|
||||
const importDialogTitle = computed(() => importActionLabel.value)
|
||||
const importFreshCardDesc = computed(() => onlyImportFresh.value
|
||||
? '仅支持大客折扣报价表(目的地/运费/操作费),导入后直接生成专属报价明细'
|
||||
: '不延用以前的产品价格,不带出上一版,上传 Excel 直接生成套餐销售价'
|
||||
)
|
||||
const importDesc = computed(() => {
|
||||
if (onlyImportFresh.value) {
|
||||
return [
|
||||
'\u4e13\u5c5e\u62a5\u4ef7\u4ec5\u652f\u6301\u5927\u5ba2\u6298\u6263\u62a5\u4ef7\u8868\uff08\u542b\u300c\u4ea7\u54c1\u76ee\u5f55\u300d\u4e0e\u5404\u56fd\u4ef7\u8868\uff09\u3002',
|
||||
'\u8868\u5934\u4e3a\u76ee\u7684\u5730 / \u91cd\u91cf\u6bb5 / \u8fd0\u8d39(CNY/KG) / \u64cd\u4f5c\u8d39(CNY/PCS)\uff0c\u4e0e\u666e\u901a\u5957\u9910 Excel \u6a21\u677f\u4e0d\u540c\u3002',
|
||||
'\u4ec5\u300cExcel \u65b0\u5efa\u4ef7\u300d\uff1a\u4e0d\u62c9\u53d6\u4ea7\u54c1\u5e93\u3001\u4e0d\u5ef6\u7528\u4e0a\u4e00\u7248\uff0c\u5bfc\u5165\u540e\u76f4\u63a5\u751f\u6210\u9500\u552e\u4ef7\u660e\u7ec6\u3002'
|
||||
]
|
||||
}
|
||||
if (isModifyImport.value) {
|
||||
return [
|
||||
'适用威狮B2C小包价表(含「产品目录」)。',
|
||||
@@ -368,18 +385,35 @@ const emptyTip = computed(() => {
|
||||
})
|
||||
|
||||
|
||||
/** Resolve version id: prop first, else reload package current version. */
|
||||
/** Resolve version id: editing prop/local first; never replace with package EFFECT version. */
|
||||
const resolvedPsvId = ref(props.psvId)
|
||||
watch(() => props.psvId, v => { if (v != null && v !== '') resolvedPsvId.value = v })
|
||||
|
||||
function knownPsvId() {
|
||||
if (props.psvId != null && props.psvId !== '') return props.psvId
|
||||
if (resolvedPsvId.value != null && resolvedPsvId.value !== '') return resolvedPsvId.value
|
||||
return null
|
||||
}
|
||||
|
||||
async function resolvePsvId(force) {
|
||||
if (!force && resolvedPsvId.value != null && resolvedPsvId.value !== '') {
|
||||
return resolvedPsvId.value
|
||||
const known = knownPsvId()
|
||||
// Keep the version being edited. force must NOT switch to data.version (EFFECT).
|
||||
if (known != null && !force) {
|
||||
resolvedPsvId.value = known
|
||||
return known
|
||||
}
|
||||
if (!props.priceSetId) return null
|
||||
const res = await getPriceSet(props.priceSetId)
|
||||
if (!props.priceSetId) return known
|
||||
const res = await api.getPriceSet(props.priceSetId)
|
||||
const data = res.data || {}
|
||||
const versions = data.versions || []
|
||||
if (known != null) {
|
||||
const still = versions.find(v => v && String(v.psvId) === String(known))
|
||||
if (still && still.psvId != null) {
|
||||
resolvedPsvId.value = still.psvId
|
||||
return still.psvId
|
||||
}
|
||||
}
|
||||
// Only when no editing psvId (e.g. brand-new draft): fall back to package current.
|
||||
const fromData = (data.version && data.version.psvId) ? data.version : null
|
||||
const current = fromData || versions[0] || {}
|
||||
if (current.psvId != null) {
|
||||
@@ -420,7 +454,7 @@ async function loadLatestPriorVersionDraft() {
|
||||
if (!props.priceSetId) return false
|
||||
pulling.value = true
|
||||
try {
|
||||
const res = await getPriceSet(props.priceSetId)
|
||||
const res = await api.getPriceSet(props.priceSetId)
|
||||
const data = res.data || {}
|
||||
const versions = data.versions || []
|
||||
// Only exclude the draft being edited. Do NOT use resolvedPsvId — it may
|
||||
@@ -437,7 +471,7 @@ async function loadLatestPriorVersionDraft() {
|
||||
return row
|
||||
})
|
||||
if (!products.length) return false
|
||||
const detailRes = await listSetDetailByVersion(prior.psvId)
|
||||
const detailRes = await api.listSetDetailByVersion(prior.psvId)
|
||||
applyLocalDraft(products, detailRes.data || [])
|
||||
return true
|
||||
} finally {
|
||||
@@ -455,7 +489,7 @@ async function loadProductDetails(productCode, force) {
|
||||
}
|
||||
const psvId = await resolvePsvId(false)
|
||||
if (!psvId) return []
|
||||
const res = await listSetDetailByProduct(psvId, productCode)
|
||||
const res = await api.listSetDetailByProduct(psvId, productCode)
|
||||
const rows = (res.data || []).map(d => hydrateRow(d, props.setType))
|
||||
detailByProduct[productCode] = rows
|
||||
// keep parent details array in sync for any legacy callers
|
||||
@@ -577,7 +611,8 @@ async function canOpenImport() {
|
||||
return false
|
||||
}
|
||||
if (props.livePersist) {
|
||||
const psvId = await resolvePsvId(true)
|
||||
// Use the version being edited (props.psvId). Do not force-refresh to EFFECT.
|
||||
const psvId = await resolvePsvId(false)
|
||||
if (!psvId) {
|
||||
proxy.$modal.msgWarning('缺少套餐版本,请先保存基本信息')
|
||||
return false
|
||||
@@ -586,6 +621,18 @@ async function canOpenImport() {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
watch(
|
||||
() => [onlyImportFresh.value, props.priceBuildMode, props.buildModeReady],
|
||||
() => {
|
||||
if (!onlyImportFresh.value || !props.buildModeReady) return
|
||||
if (isImportFreshBuildMode(props.priceBuildMode)) return
|
||||
if (buildModeLocked.value) return
|
||||
emit('update:priceBuildMode', 'IMPORT_FRESH')
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
async function openImport() {
|
||||
if (!(await canOpenImport())) return
|
||||
importRef.value && importRef.value.open()
|
||||
@@ -625,7 +672,7 @@ async function runImportAsync(uploadFile, countryCode) {
|
||||
}
|
||||
const replaceByProductCountry = !isModifyImport.value || !!countryCode
|
||||
if (!props.livePersist) {
|
||||
const res = await previewImportDetailPrices(files, {
|
||||
const res = await api.previewImportDetailPrices(files, {
|
||||
updateOnly: isModifyImport.value && !countryCode,
|
||||
replaceByProductCountry,
|
||||
countryCode: countryCode || undefined,
|
||||
@@ -640,7 +687,7 @@ async function runImportAsync(uploadFile, countryCode) {
|
||||
applyLocalDraft(products, data.details || [])
|
||||
return data.message || '导入完成'
|
||||
}
|
||||
const { task } = await importSetDetailPricesAsync(
|
||||
const { task } = await api.importSetDetailPricesAsync(
|
||||
files,
|
||||
{
|
||||
priceSetId: props.priceSetId,
|
||||
@@ -693,7 +740,7 @@ async function reloadDetailsFromServer() {
|
||||
if (!props.priceSetId) return
|
||||
try {
|
||||
clearDetailCache()
|
||||
const res = await getPriceSet(props.priceSetId)
|
||||
const res = await api.getPriceSet(props.priceSetId)
|
||||
const data = res.data || {}
|
||||
const versions = data.versions || []
|
||||
const qPsv = resolvedPsvId.value || props.psvId
|
||||
@@ -754,7 +801,7 @@ async function pullAllFromLibrary(opts = {}) {
|
||||
if (!props.livePersist) {
|
||||
importProgressMessage.value = '正在拉取产品库…'
|
||||
importProgressPercent.value = 40
|
||||
const res = await previewPullFromLibrary()
|
||||
const res = await api.previewPullFromLibrary()
|
||||
const data = res.data || {}
|
||||
applyLocalDraft(data.products || [], data.details || [])
|
||||
importProgressPercent.value = 100
|
||||
@@ -764,7 +811,7 @@ async function pullAllFromLibrary(opts = {}) {
|
||||
}
|
||||
return { added: (props.products || []).length }
|
||||
}
|
||||
const { task } = await pullSetDetailsFromLibraryAsync(
|
||||
const { task } = await api.pullSetDetailsFromLibraryAsync(
|
||||
{ priceSetId: props.priceSetId, psvId: await resolvePsvId(false), force },
|
||||
applyBuildProgress
|
||||
)
|
||||
@@ -864,7 +911,7 @@ async function persistDetail(row, withSurcharge) {
|
||||
const payload = toDetailPayload(row)
|
||||
payload.priceSetId = props.priceSetId
|
||||
if (!withSurcharge) delete payload.surcharges
|
||||
await updateSetDetail(payload)
|
||||
await api.updateSetDetail(payload)
|
||||
}
|
||||
|
||||
/** drawer hover remove surcharge -> write snapshot for this row */
|
||||
@@ -1035,7 +1082,7 @@ function onSurchargeConfirm(picked) {
|
||||
return
|
||||
}
|
||||
proxy.$modal.msgSuccess(okText + ',正在后台写入…')
|
||||
updateSetDetailSurchargeAsync(payload).then(() => {
|
||||
api.updateSetDetailSurchargeAsync(payload).then(() => {
|
||||
proxy.$modal.msgSuccess(okText + ',已写入数据库')
|
||||
}).catch(e => {
|
||||
proxy.$modal.msgError((e && e.message) ? e.message : '附加费后台写入失败')
|
||||
@@ -1048,7 +1095,7 @@ async function removeProduct(code) {
|
||||
const rows = await loadProductDetails(code)
|
||||
const ids = rows.filter(d => d && d.psdId).map(d => d.psdId)
|
||||
if (ids.length) {
|
||||
await delSetDetail(ids.join(','))
|
||||
await api.delSetDetail(ids.join(','))
|
||||
}
|
||||
} catch (e) {
|
||||
return
|
||||
|
||||
@@ -83,7 +83,9 @@
|
||||
* 套餐绑定客户 · 新增(支持批量) / 编辑弹窗
|
||||
*/
|
||||
import { createEmptyCustomerRow } from '../../utils/priceSet'
|
||||
import { batchAddSetCustomer, updateSetCustomer } from '@/api/price/set'
|
||||
import { usePriceSetModule } from '../../composables/usePriceSetModule'
|
||||
|
||||
const { api } = usePriceSetModule()
|
||||
|
||||
const props = defineProps({
|
||||
priceSetId: {
|
||||
@@ -174,7 +176,7 @@ async function submit() {
|
||||
try {
|
||||
if (isEdit.value) {
|
||||
const payload = { ...form.value, priceSetId: props.priceSetId }
|
||||
await updateSetCustomer(payload)
|
||||
await api.updateSetCustomer(payload)
|
||||
proxy.$modal.msgSuccess('保存成功')
|
||||
} else {
|
||||
const customers = selectedCustomers.value.map(c => ({
|
||||
@@ -182,7 +184,7 @@ async function submit() {
|
||||
customerCode: c.customerCode,
|
||||
customerName: c.customerName
|
||||
}))
|
||||
const res = await batchAddSetCustomer({
|
||||
const res = await api.batchAddSetCustomer({
|
||||
priceSetId: props.priceSetId,
|
||||
bindPriority: form.value.bindPriority,
|
||||
prBegindate: form.value.prBegindate,
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
size="small"
|
||||
icon="Plus"
|
||||
@click="dialogRef.open()"
|
||||
v-hasPermi="['price:set:edit']"
|
||||
v-hasPermi="editPerm"
|
||||
>添加客户</el-button>
|
||||
</template>
|
||||
|
||||
@@ -31,8 +31,8 @@
|
||||
</el-table-column>
|
||||
<el-table-column v-if="editable" label="操作" width="120" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="dialogRef.open(row)" v-hasPermi="['price:set:edit']">编辑</el-button>
|
||||
<el-button link type="danger" @click="remove(row)" v-hasPermi="['price:set:edit']">解绑</el-button>
|
||||
<el-button link type="primary" @click="dialogRef.open(row)" v-hasPermi="editPerm">编辑</el-button>
|
||||
<el-button link type="danger" @click="remove(row)" v-hasPermi="editPerm">解绑</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -55,7 +55,10 @@
|
||||
import SetSectionCard from './SetSectionCard.vue'
|
||||
import SetCustomerDialog from './SetCustomerDialog.vue'
|
||||
import { isChildEditable } from '../../utils/priceSet'
|
||||
import { delSetCustomer } from '@/api/price/set'
|
||||
import { usePriceSetModule, usePriceSetPerm } from '../../composables/usePriceSetModule'
|
||||
|
||||
const { api } = usePriceSetModule()
|
||||
const editPerm = usePriceSetPerm('edit')
|
||||
|
||||
const props = defineProps({
|
||||
/** 套餐详情对象 */
|
||||
@@ -75,7 +78,7 @@ const customers = computed(() => props.set.customers || [])
|
||||
const editable = computed(() => isChildEditable(props.set.status))
|
||||
|
||||
function remove(row) {
|
||||
proxy.$modal.confirm('确认解绑该客户?').then(() => delSetCustomer(row.customerPricesetId)).then(() => {
|
||||
proxy.$modal.confirm('确认解绑该客户?').then(() => api.delSetCustomer(row.customerPricesetId)).then(() => {
|
||||
proxy.$modal.msgSuccess('已解绑')
|
||||
emit('refresh')
|
||||
}).catch(() => {})
|
||||
|
||||
@@ -164,7 +164,9 @@
|
||||
import SurchargeTags from '../SurchargeTags.vue'
|
||||
import { useDetailCascade } from '../../composables/usePriceSetRefData'
|
||||
import { createEmptyDetailRow, isFixedType, toEditableDetailRow } from '../../utils/priceSet'
|
||||
import { addSetDetail, updateSetDetail } from '@/api/price/set'
|
||||
import { usePriceSetModule } from '../../composables/usePriceSetModule'
|
||||
|
||||
const { api } = usePriceSetModule()
|
||||
|
||||
const props = defineProps({
|
||||
/** 所属套餐ID */
|
||||
@@ -228,7 +230,7 @@ async function submit() {
|
||||
const payload = { ...form.value, priceSetId: props.priceSetId }
|
||||
delete payload._countryKey
|
||||
submitting.value = true
|
||||
const req = payload.psdId ? updateSetDetail(payload) : addSetDetail(payload)
|
||||
const req = payload.psdId ? api.updateSetDetail(payload) : api.addSetDetail(payload)
|
||||
req.then(() => {
|
||||
proxy.$modal.msgSuccess('保存成功')
|
||||
visible.value = false
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="set-header">
|
||||
<div class="hd-main">
|
||||
<div class="hd-line">
|
||||
<h2 class="hd-name">{{ set.priceSetName || '套餐详情' }}</h2>
|
||||
<h2 class="hd-name">{{ set.priceSetName || mod.labels.detailTitleFallback }}</h2>
|
||||
<span v-if="set.priceSetCode" class="hd-code">{{ set.priceSetCode }}</span>
|
||||
<el-tag :type="setStatusTagType(set)" size="small">{{ setStatusLabel(set) }}</el-tag>
|
||||
</div>
|
||||
@@ -20,14 +20,14 @@
|
||||
type="danger"
|
||||
plain
|
||||
@click="emit('disable')"
|
||||
v-hasPermi="['price:set:audit']"
|
||||
v-hasPermi="auditPerm"
|
||||
>停用</el-button>
|
||||
<el-button
|
||||
v-if="set.status !== 'E'"
|
||||
type="success"
|
||||
plain
|
||||
@click="emit('enable')"
|
||||
v-hasPermi="['price:set:audit']"
|
||||
v-hasPermi="auditPerm"
|
||||
>启用</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -35,6 +35,10 @@
|
||||
|
||||
<script setup name="SetDetailHeader">
|
||||
import { isFixedType, setTypeLabel, setStatusLabel, setStatusTagType } from '../../utils/priceSet'
|
||||
import { usePriceSetModule, usePriceSetPerm } from '../../composables/usePriceSetModule'
|
||||
|
||||
const mod = usePriceSetModule()
|
||||
const auditPerm = usePriceSetPerm('audit')
|
||||
|
||||
defineProps({
|
||||
set: { type: Object, required: true }
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
size="small"
|
||||
icon="Plus"
|
||||
@click="dialogRef.open()"
|
||||
v-hasPermi="['price:set:edit']"
|
||||
v-hasPermi="editPerm"
|
||||
>添加明细</el-button>
|
||||
</template>
|
||||
|
||||
@@ -86,8 +86,8 @@
|
||||
|
||||
<el-table-column v-if="editable" label="操作" width="120" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="dialogRef.open(row)" v-hasPermi="['price:set:edit']">编辑</el-button>
|
||||
<el-button link type="danger" @click="remove(row)" v-hasPermi="['price:set:edit']">删除</el-button>
|
||||
<el-button link type="primary" @click="dialogRef.open(row)" v-hasPermi="editPerm">编辑</el-button>
|
||||
<el-button link type="danger" @click="remove(row)" v-hasPermi="editPerm">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -113,7 +113,10 @@ import SetSectionCard from './SetSectionCard.vue'
|
||||
import SetDetailDialog from './SetDetailDialog.vue'
|
||||
import SurchargeTags from '../SurchargeTags.vue'
|
||||
import { isChildEditable, isFixedType } from '../../utils/priceSet'
|
||||
import { delSetDetail } from '@/api/price/set'
|
||||
import { usePriceSetModule, usePriceSetPerm } from '../../composables/usePriceSetModule'
|
||||
|
||||
const { api } = usePriceSetModule()
|
||||
const editPerm = usePriceSetPerm('edit')
|
||||
|
||||
const props = defineProps({
|
||||
/** 套餐详情对象 */
|
||||
@@ -172,7 +175,7 @@ function roundModeLabel(mode) {
|
||||
}
|
||||
|
||||
function remove(row) {
|
||||
proxy.$modal.confirm('确认删除该明细?').then(() => delSetDetail(row.psdId)).then(() => {
|
||||
proxy.$modal.confirm('确认删除该明细?').then(() => api.delSetDetail(row.psdId)).then(() => {
|
||||
proxy.$modal.msgSuccess('已删除')
|
||||
emit('refresh')
|
||||
}).catch(() => {})
|
||||
|
||||
@@ -129,7 +129,9 @@
|
||||
import { ArrowLeft } from '@element-plus/icons-vue'
|
||||
import SurchargeTags from '../SurchargeTags.vue'
|
||||
import { isFixedType } from '../../utils/priceSet'
|
||||
import { listSetDetailByProduct, listSetDetailOptions } from '@/api/price/set'
|
||||
import { usePriceSetModule } from '../../composables/usePriceSetModule'
|
||||
|
||||
const { api } = usePriceSetModule()
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
@@ -318,7 +320,7 @@ async function loadProducts() {
|
||||
products.value = fromVersion
|
||||
return
|
||||
}
|
||||
const res = await listSetDetailOptions(psvId)
|
||||
const res = await api.listSetDetailOptions(psvId)
|
||||
const data = res.data || {}
|
||||
products.value = normalizeProducts(data.products || [])
|
||||
} finally {
|
||||
@@ -335,7 +337,7 @@ async function openProductDetail(row) {
|
||||
detailKeyword.value = ''
|
||||
detailLoading.value = true
|
||||
try {
|
||||
const res = await listSetDetailByProduct(psvId, row.productCode)
|
||||
const res = await api.listSetDetailByProduct(psvId, row.productCode)
|
||||
detailRows.value = res.data || []
|
||||
} catch (e) {
|
||||
detailRows.value = []
|
||||
|
||||
@@ -18,7 +18,9 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { listPriceSetVersionExcelFiles } from '@/api/price/set'
|
||||
import { usePriceSetModule } from '../../composables/usePriceSetModule'
|
||||
|
||||
const { api } = usePriceSetModule()
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
|
||||
@@ -39,7 +41,7 @@ function open(row) {
|
||||
function loadList() {
|
||||
if (!version.value || version.value.psvId == null) return
|
||||
loading.value = true
|
||||
listPriceSetVersionExcelFiles(version.value.psvId).then(res => {
|
||||
api.listPriceSetVersionExcelFiles(version.value.psvId).then(res => {
|
||||
list.value = res.data || []
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
plain
|
||||
size="small"
|
||||
:disabled="hasDraft"
|
||||
v-hasPermi="['price:set:add']"
|
||||
v-hasPermi="addPerm"
|
||||
@click="emit('create-version')"
|
||||
>新建价格版本</el-button>
|
||||
</template>
|
||||
@@ -50,14 +50,14 @@
|
||||
link
|
||||
type="success"
|
||||
@click="emit('activate', row)"
|
||||
v-hasPermi="['price:set:edit']"
|
||||
v-hasPermi="editPerm"
|
||||
>启动</el-button>
|
||||
<el-button
|
||||
v-if="isPendingVersion(row)"
|
||||
link
|
||||
type="primary"
|
||||
@click="emit('reschedule', row)"
|
||||
v-hasPermi="['price:set:edit']"
|
||||
v-hasPermi="editPerm"
|
||||
>修改生效时间</el-button>
|
||||
<el-button
|
||||
v-if="isDraftVersion(row) || isPendingVersion(row) || isEffectVersion(row)"
|
||||
@@ -66,7 +66,7 @@
|
||||
:loading="isRemoving(row)"
|
||||
:disabled="removingPsvId != null"
|
||||
@click="emit('remove', row)"
|
||||
v-hasPermi="['price:set:edit']"
|
||||
v-hasPermi="editPerm"
|
||||
>删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -78,6 +78,11 @@
|
||||
import SetSectionCard from './SetSectionCard.vue'
|
||||
import { versionStatusLabel, versionStatusTag, isDraftVersion, isPendingVersion, isEffectVersion, isPriceEditableVersion } from '../../utils/priceSet'
|
||||
import { checkPermi } from '@/utils/permission'
|
||||
import { usePriceSetModule, usePriceSetPerm } from '../../composables/usePriceSetModule'
|
||||
|
||||
const mod = usePriceSetModule()
|
||||
const addPerm = usePriceSetPerm('add')
|
||||
const editPerm = usePriceSetPerm('edit')
|
||||
|
||||
const props = defineProps({
|
||||
versions: { type: Array, default: () => [] },
|
||||
@@ -164,8 +169,8 @@ function detailCount(row) {
|
||||
}
|
||||
|
||||
function showEditButton(row) {
|
||||
if (checkPermi(['price:set:forceEdit'])) return true
|
||||
return isPriceEditableVersion(row) && checkPermi(['price:set:edit'])
|
||||
if (checkPermi([mod.permPrefix + ':forceEdit'])) return true
|
||||
return isPriceEditableVersion(row) && checkPermi([mod.permPrefix + ':edit'])
|
||||
}
|
||||
|
||||
function isRemoving(row) {
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { inject, provide } from 'vue'
|
||||
import { priceSetApi } from '@/api/price/set'
|
||||
|
||||
export const PRICE_SET_MODULE_KEY = 'priceSetModule'
|
||||
|
||||
const defaultModule = Object.freeze({
|
||||
api: priceSetApi,
|
||||
permPrefix: 'price:set',
|
||||
exclusiveQuote: false,
|
||||
onlyImportFresh: false,
|
||||
routes: Object.freeze({
|
||||
list: '/customer/set',
|
||||
create: '/customer/setCreate',
|
||||
detail: '/customer/setDetail'
|
||||
}),
|
||||
labels: Object.freeze({
|
||||
entity: '套餐',
|
||||
entityCode: '套餐编码',
|
||||
entityName: '套餐名称',
|
||||
createButton: '新建套餐',
|
||||
saveProgressTitle: '正在保存套餐',
|
||||
savedSuccess: '套餐已保存',
|
||||
noActiveVersion: '该套餐暂无生效版本',
|
||||
disableConfirm: '停用后该套餐不再参与计费命中,确认停用【{name}】?',
|
||||
enableConfirm: '启用后该套餐将重新参与计费命中,确认启用【{name}】?',
|
||||
basicEditTitle: '修改套餐基本信息',
|
||||
keywordPlaceholder: '套餐编码 / 名称',
|
||||
detailTitleFallback: '套餐详情'
|
||||
})
|
||||
})
|
||||
|
||||
export function createExclusiveModule(api) {
|
||||
return Object.freeze({
|
||||
api,
|
||||
permPrefix: 'price:exclusive',
|
||||
exclusiveQuote: true,
|
||||
onlyImportFresh: true,
|
||||
routes: Object.freeze({
|
||||
list: '/exclusive/quote',
|
||||
create: '/exclusive/quoteCreate',
|
||||
detail: '/exclusive/quoteDetail'
|
||||
}),
|
||||
labels: Object.freeze({
|
||||
entity: '专属报价',
|
||||
entityCode: '报价编码',
|
||||
entityName: '报价名称',
|
||||
createButton: '新建报价',
|
||||
saveProgressTitle: '正在保存专属报价',
|
||||
savedSuccess: '专属报价已保存',
|
||||
noActiveVersion: '该专属报价暂无生效版本',
|
||||
disableConfirm: '停用后该专属报价不再参与计费命中,确认停用【{name}】?',
|
||||
enableConfirm: '启用后该专属报价将重新参与计费命中,确认启用【{name}】?',
|
||||
basicEditTitle: '修改专属报价基本信息',
|
||||
keywordPlaceholder: '报价编码 / 名称',
|
||||
detailTitleFallback: '专属报价详情'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide module for child inject, and return cfg for the providing page itself
|
||||
* (same-component inject cannot see its own provide).
|
||||
*/
|
||||
export function providePriceSetModule(moduleConfig) {
|
||||
const cfg = moduleConfig || defaultModule
|
||||
provide(PRICE_SET_MODULE_KEY, cfg)
|
||||
return cfg
|
||||
}
|
||||
|
||||
export function usePriceSetModule() {
|
||||
return inject(PRICE_SET_MODULE_KEY, defaultModule)
|
||||
}
|
||||
|
||||
export function usePriceSetPerm(action) {
|
||||
const mod = usePriceSetModule()
|
||||
return [`${mod.permPrefix}:${action}`]
|
||||
}
|
||||
@@ -34,7 +34,7 @@
|
||||
|
||||
<el-dialog
|
||||
v-model="progressVisible"
|
||||
:title="'正在保存套餐'"
|
||||
:title="mod.labels.saveProgressTitle"
|
||||
width="420px"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
@@ -61,7 +61,9 @@ import {
|
||||
toCustomerPayload,
|
||||
toDetailPayload
|
||||
} from './utils/priceSet'
|
||||
import { getPriceSet, savePriceSetAsync, createPriceSetVersion, deletePriceSetVersion } from '@/api/price/set'
|
||||
import { usePriceSetModule } from './composables/usePriceSetModule'
|
||||
|
||||
const mod = usePriceSetModule()
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const route = useRoute()
|
||||
@@ -131,7 +133,7 @@ async function runSaveWithProgress(payload) {
|
||||
progressMessage.value = ''
|
||||
progressStatus.value = undefined
|
||||
try {
|
||||
const result = await savePriceSetAsync(payload, (task) => {
|
||||
const result = await mod.api.savePriceSetAsync(payload, (task) => {
|
||||
progressPercent.value = Math.min(100, Number(task.percent) || 0)
|
||||
progressMessage.value = mapSaveProgressMessage(task.message)
|
||||
if (task.status === 'FAIL') {
|
||||
@@ -204,7 +206,7 @@ async function finishEdit() {
|
||||
proxy.$modal.msgSuccess("修改已实时落库")
|
||||
}
|
||||
const obj = {
|
||||
path: '/customer/setDetail',
|
||||
path: mod.routes.detail,
|
||||
query: { priceSetId: form.priceSetId }
|
||||
}
|
||||
if (proxy.$tab) {
|
||||
@@ -230,7 +232,7 @@ async function finishEdit() {
|
||||
creating.value = true
|
||||
try {
|
||||
if (newVersionMode.value && form.version.psvId == null && form.priceSetId) {
|
||||
const verRes = await createPriceSetVersion(form.priceSetId)
|
||||
const verRes = await mod.api.createPriceSetVersion(form.priceSetId)
|
||||
if (verRes.data != null) {
|
||||
form.version.psvId = verRes.data
|
||||
}
|
||||
@@ -238,9 +240,9 @@ async function finishEdit() {
|
||||
const payload = buildPayload({ forceDetails: true })
|
||||
const priceSetId = await runSaveWithProgress(payload)
|
||||
form.priceSetId = priceSetId
|
||||
proxy.$modal.msgSuccess('套餐已保存')
|
||||
proxy.$modal.msgSuccess(mod.labels.savedSuccess)
|
||||
const obj = {
|
||||
path: '/customer/setDetail',
|
||||
path: mod.routes.detail,
|
||||
query: { priceSetId }
|
||||
}
|
||||
if (proxy.$tab) {
|
||||
@@ -305,14 +307,14 @@ async function tryDeleteEmptyNewVersionDraft() {
|
||||
if (!newVersionMode.value || !form.priceSetId || form.version.psvId == null) return
|
||||
const psvId = form.version.psvId
|
||||
try {
|
||||
const res = await getPriceSet(form.priceSetId)
|
||||
const res = await mod.api.getPriceSet(form.priceSetId)
|
||||
const versions = (res.data || {}).versions || []
|
||||
const v = versions.find(x => x && String(x.psvId) === String(psvId))
|
||||
if (!v || String(v.status) !== '0') return
|
||||
const noMode = !v.priceBuildMode
|
||||
const noDetails = !(Number(v.detailCount) > 0) && !(v.details || []).length
|
||||
if (noMode && noDetails) {
|
||||
await deletePriceSetVersion(psvId)
|
||||
await mod.api.deletePriceSetVersion(psvId)
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore cleanup errors; still navigate back
|
||||
@@ -322,8 +324,8 @@ async function tryDeleteEmptyNewVersionDraft() {
|
||||
async function goBack() {
|
||||
await tryDeleteEmptyNewVersionDraft()
|
||||
const obj = (newVersionMode.value || copyMode.value) && form.priceSetId
|
||||
? { path: '/customer/setDetail', query: { priceSetId: form.priceSetId } }
|
||||
: { path: '/customer/set' }
|
||||
? { path: mod.routes.detail, query: { priceSetId: form.priceSetId } }
|
||||
: { path: mod.routes.list }
|
||||
if (proxy.$tab) {
|
||||
proxy.$tab.closeOpenPage(obj)
|
||||
} else {
|
||||
@@ -418,7 +420,7 @@ function onPriceBuildModeUpdate(mode) {
|
||||
}
|
||||
|
||||
function reloadSavedProductsForCreate() {
|
||||
getPriceSet(form.priceSetId).then(res => {
|
||||
mod.api.getPriceSet(form.priceSetId).then(res => {
|
||||
const data = res.data || {}
|
||||
const versions = data.versions || []
|
||||
const qPsv = form.version.psvId
|
||||
@@ -433,7 +435,7 @@ function reloadSavedProductsForCreate() {
|
||||
|
||||
async function refreshProductsFromCurrentVersion() {
|
||||
if (!form.priceSetId) return
|
||||
const res = await getPriceSet(form.priceSetId)
|
||||
const res = await mod.api.getPriceSet(form.priceSetId)
|
||||
const data = res.data || {}
|
||||
const versions = data.versions || []
|
||||
const qPsv = form.version.psvId
|
||||
@@ -520,7 +522,7 @@ function applySetData(data, asCopy, asNewVersion) {
|
||||
|
||||
function loadExisting(id, asCopy, asNewVersion) {
|
||||
pageReady.value = false
|
||||
return getPriceSet(id).then(res => {
|
||||
return mod.api.getPriceSet(id).then(res => {
|
||||
applySetData(res.data || {}, asCopy, asNewVersion)
|
||||
}).finally(() => {
|
||||
pageReady.value = true
|
||||
|
||||
@@ -83,11 +83,11 @@ import SetVersionDetailDrawer from './components/detail/SetVersionDetailDrawer.v
|
||||
import SetVersionExcelFileDrawer from './components/detail/SetVersionExcelFileDrawer.vue'
|
||||
import PriceEditPanel from './components/create/PriceEditPanel.vue'
|
||||
import { usePriceSetRefData } from './composables/usePriceSetRefData'
|
||||
import {
|
||||
getPriceSet, disablePriceSet, enablePriceSet, activatePriceSetVersion, deletePriceSetVersion
|
||||
} from '@/api/price/set'
|
||||
import { usePriceSetModule } from './composables/usePriceSetModule'
|
||||
import { isDraftVersion, isPendingVersion, isEffectVersion, isImportFreshBuildMode } from './utils/priceSet'
|
||||
|
||||
const mod = usePriceSetModule()
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -140,7 +140,7 @@ function load() {
|
||||
// 立即标记,首次挂载后紧随的 activated 不会重复请求
|
||||
loadedId.value = String(id)
|
||||
loading.value = true
|
||||
getPriceSet(id).then(res => {
|
||||
mod.api.getPriceSet(id).then(res => {
|
||||
set.value = res.data || {}
|
||||
const versions = set.value.versions || []
|
||||
const cur = set.value.version || versions[0]
|
||||
@@ -172,10 +172,10 @@ function confirmAction(text, request, successText, done) {
|
||||
}
|
||||
|
||||
function onDisable() {
|
||||
confirmAction('停用后不再参与计费命中,确认停用?', () => disablePriceSet(set.value.priceSetId), '已停用')
|
||||
confirmAction(mod.labels.disableConfirm.replace('{name}', set.value.priceSetName || ''), () => mod.api.disablePriceSet(set.value.priceSetId), '已停用')
|
||||
}
|
||||
function onEnable() {
|
||||
confirmAction('启用后套餐将重新参与计费命中,确认启用?', () => enablePriceSet(set.value.priceSetId), '已启用')
|
||||
confirmAction(mod.labels.enableConfirm.replace('{name}', set.value.priceSetName || ''), () => mod.api.enablePriceSet(set.value.priceSetId), '已启用')
|
||||
}
|
||||
/** Effective time precision: hour only (mm:ss always 00) */
|
||||
function pad2(n) {
|
||||
@@ -413,7 +413,7 @@ function submitActivate() {
|
||||
if (!valid) return
|
||||
activateForm.effectiveTime = normalizeToHour(activateForm.effectiveTime)
|
||||
activateSubmitting.value = true
|
||||
activatePriceSetVersion(activateForm.psvId, { effectiveTime: activateForm.effectiveTime })
|
||||
mod.api.activatePriceSetVersion(activateForm.psvId, { effectiveTime: activateForm.effectiveTime })
|
||||
.then(() => {
|
||||
const eff = activateForm.effectiveTime
|
||||
const pending = eff && new Date(eff.replace(/-/g, '/')).getTime() > Date.now()
|
||||
@@ -439,7 +439,7 @@ function onCreateVersion() {
|
||||
}
|
||||
// Do not insert draft until user clicks finish on the build-price page.
|
||||
router.push({
|
||||
path: '/customer/setCreate',
|
||||
path: mod.routes.create,
|
||||
query: { priceSetId, step: '2', newVersion: '1' }
|
||||
})
|
||||
}
|
||||
@@ -451,7 +451,7 @@ function onRemoveVersion(row) {
|
||||
proxy.$modal.confirm(msg).then(() => {
|
||||
removingPsvId.value = row.psvId
|
||||
proxy.$modal.loading("正在删除,请稍候...")
|
||||
return deletePriceSetVersion(row.psvId).then(() => {
|
||||
return mod.api.deletePriceSetVersion(row.psvId).then(() => {
|
||||
proxy.$modal.msgSuccess("版本已删除")
|
||||
load()
|
||||
}).finally(() => {
|
||||
@@ -468,7 +468,7 @@ function onEditVersion(row) {
|
||||
// Empty unfinished draft (new version, not finished): resume create step2 with build-mode choice
|
||||
if (isDraftVersion(row) && noMode && noProducts && priceSetId != null) {
|
||||
router.push({
|
||||
path: '/customer/setCreate',
|
||||
path: mod.routes.create,
|
||||
query: { priceSetId, psvId: row.psvId, step: '2', newVersion: '1' }
|
||||
})
|
||||
return
|
||||
@@ -483,7 +483,7 @@ function closeEdit() {
|
||||
load()
|
||||
}
|
||||
function goList() {
|
||||
const obj = { path: '/customer/set' }
|
||||
const obj = { path: mod.routes.list }
|
||||
if (proxy.$tab) {
|
||||
proxy.$tab.closeOpenPage(obj)
|
||||
} else {
|
||||
@@ -509,7 +509,7 @@ watch(() => route.query.priceSetId, () => {
|
||||
|
||||
// 离开详情页去新建/编辑套餐版本,返回后需要重新加载
|
||||
onBeforeRouteLeave((to) => {
|
||||
if (to && to.path === '/customer/setCreate') {
|
||||
if (to && to.path === mod.routes.create) {
|
||||
pendingRefresh.value = true
|
||||
}
|
||||
})
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
plain
|
||||
icon="Plus"
|
||||
@click="handleCreate"
|
||||
v-hasPermi="['price:set:add']"
|
||||
>新建套餐</el-button>
|
||||
v-hasPermi="addPerm"
|
||||
>{{ mod.labels.createButton }}</el-button>
|
||||
</el-col>
|
||||
</template>
|
||||
|
||||
@@ -52,37 +52,37 @@
|
||||
type="primary"
|
||||
:loading="activeLoadingId === row.priceSetId"
|
||||
@click="handleActiveDetail(row)"
|
||||
v-hasPermi="['price:set:query']"
|
||||
v-hasPermi="queryPerm"
|
||||
>查看明细</el-button>
|
||||
</template>
|
||||
|
||||
<template #col-action="{ row }">
|
||||
<el-button link type="primary" @click="handleDetail(row)" v-hasPermi="['price:set:query']">详情</el-button>
|
||||
<el-button link type="primary" @click="openBasicEdit(row)" v-hasPermi="['price:set:edit']">修改</el-button>
|
||||
<el-button link type="primary" @click="handleDetail(row)" v-hasPermi="queryPerm">详情</el-button>
|
||||
<el-button link type="primary" @click="openBasicEdit(row)" v-hasPermi="editPerm">修改</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 'E'"
|
||||
link
|
||||
type="warning"
|
||||
@click="handleDisable(row)"
|
||||
v-hasPermi="['price:set:audit']"
|
||||
v-hasPermi="auditPerm"
|
||||
>停用</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 'D' || row.status === 'S'"
|
||||
link
|
||||
type="success"
|
||||
@click="handleEnable(row)"
|
||||
v-hasPermi="['price:set:audit']"
|
||||
v-hasPermi="auditPerm"
|
||||
>启用</el-button>
|
||||
</template>
|
||||
</basic-table>
|
||||
|
||||
<el-dialog v-model="basicDialog.visible" :title="basicDialogTitle" width="520px" append-to-body destroy-on-close>
|
||||
<el-form ref="basicFormRef" :model="basicForm" :rules="basicRules" label-width="96px">
|
||||
<el-form-item label="套餐编码">
|
||||
<el-form-item :label="mod.labels.entityCode">
|
||||
<el-input :model-value="basicForm.priceSetCode" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="套餐名称" prop="priceSetName">
|
||||
<el-input v-model="basicForm.priceSetName" maxlength="50" show-word-limit placeholder="请输入套餐名称" />
|
||||
<el-form-item :label="mod.labels.entityName" prop="priceSetName">
|
||||
<el-input v-model="basicForm.priceSetName" maxlength="50" show-word-limit :placeholder="'请输入' + mod.labels.entityName" />
|
||||
</el-form-item>
|
||||
<el-form-item label="说明备注" prop="priceSetNote">
|
||||
<el-input v-model="basicForm.priceSetNote" type="textarea" :rows="3" maxlength="200" show-word-limit placeholder="可选" />
|
||||
@@ -104,18 +104,21 @@
|
||||
</template>
|
||||
|
||||
<script setup name="PriceSet">
|
||||
import {
|
||||
listPriceSet, disablePriceSet, enablePriceSet, copyPriceSet, getPriceSet, updatePriceSetBasic
|
||||
} from '@/api/price/set'
|
||||
import SetVersionDetailDrawer from './components/detail/SetVersionDetailDrawer.vue'
|
||||
import CustomerSelect from '@/components/CustomerSelect/index.vue'
|
||||
import { setStatusLabel, setStatusTagType } from './utils/priceSet'
|
||||
import { usePriceSetModule, usePriceSetPerm } from './composables/usePriceSetModule'
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const mod = usePriceSetModule()
|
||||
const addPerm = usePriceSetPerm('add')
|
||||
const queryPerm = usePriceSetPerm('query')
|
||||
const editPerm = usePriceSetPerm('edit')
|
||||
const auditPerm = usePriceSetPerm('audit')
|
||||
const tableRef = ref()
|
||||
const isFirstActivate = ref(true)
|
||||
|
||||
const searchColumns = [
|
||||
const searchColumns = computed(() => [
|
||||
{
|
||||
label: '状态',
|
||||
prop: 'status',
|
||||
@@ -128,23 +131,23 @@ const searchColumns = [
|
||||
placeholder: '全部状态',
|
||||
width: '140px'
|
||||
},
|
||||
{ label: '关键字', prop: 'keyword', component: 'input', placeholder: '套餐编码 / 名称', width: '200px' },
|
||||
{ label: '关键字', prop: 'keyword', component: 'input', placeholder: mod.labels.keywordPlaceholder, width: '200px' },
|
||||
{ label: '客户', prop: 'customerId', placeholder: '客户名称 / 代码', width: '240px' }
|
||||
]
|
||||
])
|
||||
|
||||
const columns = [
|
||||
{ label: '套餐编码', prop: 'priceSetCode', width: 110 },
|
||||
{ label: '套餐名称', prop: 'priceSetName', minWidth: 160 },
|
||||
const columns = computed(() => [
|
||||
{ label: mod.labels.entityCode, prop: 'priceSetCode', width: 110 },
|
||||
{ label: mod.labels.entityName, prop: 'priceSetName', minWidth: 160 },
|
||||
{ label: '类型', prop: 'setType', width: 140 },
|
||||
{ label: '绑定客户', prop: 'boundCustomers', minWidth: 220 },
|
||||
{ label: '说明', prop: 'priceSetNote', minWidth: 140 },
|
||||
{ label: '状态', prop: 'status', width: 90 },
|
||||
{ label: '生效明细', prop: 'activeDetail', width: 100 },
|
||||
{ label: '操作', prop: 'action', width: 200, showInToolbar: false }
|
||||
]
|
||||
])
|
||||
|
||||
function queryList(params) {
|
||||
return listPriceSet(params)
|
||||
return mod.api.listPriceSet(params)
|
||||
}
|
||||
|
||||
function statusLabel(row) {
|
||||
@@ -156,11 +159,11 @@ function statusTag(row) {
|
||||
}
|
||||
|
||||
function handleCreate() {
|
||||
proxy.$router.push({ path: '/customer/setCreate' })
|
||||
proxy.$router.push({ path: mod.routes.create })
|
||||
}
|
||||
|
||||
function handleDetail(row) {
|
||||
proxy.$router.push({ path: '/customer/setDetail', query: { priceSetId: row.priceSetId } })
|
||||
proxy.$router.push({ path: mod.routes.detail, query: { priceSetId: row.priceSetId } })
|
||||
}
|
||||
|
||||
|
||||
@@ -173,9 +176,9 @@ const basicForm = reactive({
|
||||
priceSetNote: ''
|
||||
})
|
||||
const basicRules = {
|
||||
priceSetName: [{ required: true, message: '套餐名称不能为空', trigger: 'blur' }]
|
||||
priceSetName: [{ required: true, message: '名称不能为空', trigger: 'blur' }]
|
||||
}
|
||||
const basicDialogTitle = computed(() => '修改套餐基本信息')
|
||||
const basicDialogTitle = computed(() => mod.labels.basicEditTitle)
|
||||
|
||||
function openBasicEdit(row) {
|
||||
if (!row || row.priceSetId == null) return
|
||||
@@ -191,7 +194,7 @@ function submitBasicEdit() {
|
||||
basicFormRef.value.validate(valid => {
|
||||
if (!valid) return
|
||||
basicDialog.saving = true
|
||||
updatePriceSetBasic({
|
||||
mod.api.updatePriceSetBasic({
|
||||
priceSetId: basicForm.priceSetId,
|
||||
priceSetName: basicForm.priceSetName,
|
||||
priceSetNote: basicForm.priceSetNote
|
||||
@@ -214,12 +217,12 @@ const activeLoadingId = ref(null)
|
||||
|
||||
function handleActiveDetail(row) {
|
||||
activeLoadingId.value = row.priceSetId
|
||||
getPriceSet(row.priceSetId).then(res => {
|
||||
mod.api.getPriceSet(row.priceSetId).then(res => {
|
||||
const data = res.data || {}
|
||||
const versions = data.versions || []
|
||||
const active = versions.find(v => String(v.status) === '1')
|
||||
if (!active || active.psvId == null) {
|
||||
proxy.$modal.msgWarning('该套餐暂无生效版本')
|
||||
proxy.$modal.msgWarning(mod.labels.noActiveVersion)
|
||||
return
|
||||
}
|
||||
drawerSetType.value = row.setType
|
||||
@@ -245,8 +248,8 @@ onActivated(() => {
|
||||
|
||||
|
||||
function handleDisable(row) {
|
||||
proxy.$modal.confirm('停用后该套餐不再参与计费命中,确认停用【' + row.priceSetName + '】?').then(() => {
|
||||
return disablePriceSet(row.priceSetId)
|
||||
proxy.$modal.confirm(mod.labels.disableConfirm.replace('{name}', row.priceSetName)).then(() => {
|
||||
return mod.api.disablePriceSet(row.priceSetId)
|
||||
}).then(() => {
|
||||
proxy.$modal.msgSuccess('已停用')
|
||||
refresh()
|
||||
@@ -254,8 +257,8 @@ function handleDisable(row) {
|
||||
}
|
||||
|
||||
function handleEnable(row) {
|
||||
proxy.$modal.confirm('启用后该套餐将重新参与计费命中,确认启用【' + row.priceSetName + '】?').then(() => {
|
||||
return enablePriceSet(row.priceSetId)
|
||||
proxy.$modal.confirm(mod.labels.enableConfirm.replace('{name}', row.priceSetName)).then(() => {
|
||||
return mod.api.enablePriceSet(row.priceSetId)
|
||||
}).then(() => {
|
||||
proxy.$modal.msgSuccess('已启用')
|
||||
tableRef.value.refresh()
|
||||
@@ -263,9 +266,9 @@ function handleEnable(row) {
|
||||
}
|
||||
|
||||
function handleCopy(row) {
|
||||
copyPriceSet(row.priceSetId).then(res => {
|
||||
mod.api.copyPriceSet(row.priceSetId).then(res => {
|
||||
proxy.$modal.msgSuccess('已复制为草稿')
|
||||
proxy.$router.push({ path: '/customer/setCreate', query: { priceSetId: res.data } })
|
||||
proxy.$router.push({ path: mod.routes.create, query: { priceSetId: res.data } })
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -3,6 +3,13 @@
|
||||
<el-card shadow="never" class="search-card">
|
||||
<el-form :model="query" label-width="88px" @submit.prevent>
|
||||
<el-row :gutter="16">
|
||||
<el-col :xs="24" :sm="12" :lg="8">
|
||||
<el-form-item label="客户区域" class="filter-item">
|
||||
<el-select v-model="query.customerArea" placeholder="请选择客户区域" class="filter-control" @change="handleSearch">
|
||||
<el-option v-for="area in customer_area" :key="area.value" :label="area.label" :value="area.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :lg="8">
|
||||
<el-form-item label="计费日期" class="filter-item">
|
||||
<el-date-picker
|
||||
@@ -63,7 +70,7 @@
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :lg="8" class="filter-actions-col">
|
||||
<div class="filter-item filter-actions">
|
||||
<el-button type="primary" icon="Search" @click="handleSearch">搜索</el-button>
|
||||
<el-button type="primary" icon="Search" :disabled="!hasProfitRule()" @click="handleSearch">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
</el-col>
|
||||
@@ -71,23 +78,26 @@
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<div v-if="summaryList.length" class="summary-list">
|
||||
<el-card v-for="summary in summaryList" :key="summary.currency" shadow="never" class="summary-card">
|
||||
<el-alert v-if="areaRulesFailed" title="毛利规则加载失败,请刷新页面重试" type="error" :closable="false" show-icon class="rule-alert" />
|
||||
<el-alert v-else-if="areaRules && hasCustomerArea() && !currentRule" title="该客户区域未配置毛利规则" type="warning" :closable="false" show-icon class="rule-alert" />
|
||||
|
||||
<div v-if="summaryLoaded && hasProfitRule()" class="summary-list">
|
||||
<el-card v-for="summary in summaryCards" :key="summary.currency || 'empty'" shadow="never" class="summary-card">
|
||||
<template #header>
|
||||
<span>{{ summary.currency || '未标币种' }} 汇总</span>
|
||||
<span class="summary-subtitle">订单 {{ summary.orderCount || 0 }} 条</span>
|
||||
<span>{{ summary.empty ? '汇总' : `${summary.currency || '未标币种'} 汇总` }}</span>
|
||||
<span class="summary-subtitle">订单 {{ summary.orderCount ?? '-' }} 条</span>
|
||||
</template>
|
||||
<el-row :gutter="12" class="summary-grid">
|
||||
<el-col :xs="12" :sm="8" :md="6">
|
||||
<div class="metric-item"><div class="metric-label">红线价</div><div class="metric-value">{{ money(summary.redlineAmount) }}</div></div>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="8" :md="6">
|
||||
<el-col v-if="!isGuangzhou" :xs="12" :sm="8" :md="6">
|
||||
<div class="metric-item"><div class="metric-label">产品价</div><div class="metric-value">{{ money(summary.costAmount) }}</div></div>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="8" :md="6">
|
||||
<div class="metric-item"><div class="metric-label">销售价</div><div class="metric-value">{{ money(summary.salesAmount) }}</div></div>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="8" :md="6">
|
||||
<el-col v-if="!isGuangzhou" :xs="12" :sm="8" :md="6">
|
||||
<div class="metric-item"><div class="metric-label">红线毛利</div><div class="metric-value">{{ money(summary.redlineGrossProfit) }} <small>{{ rate(summary.redlineGrossMarginRate) }}</small></div></div>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="8" :md="6">
|
||||
@@ -132,13 +142,14 @@
|
||||
icon="Download"
|
||||
v-hasPermi="['report:profit:export']"
|
||||
:loading="exporting"
|
||||
:disabled="!hasProfitRule()"
|
||||
@click="handleExport"
|
||||
>导出当前维度</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-text-drag-copy="true" class="report-table-copy-area">
|
||||
<el-table :key="dimension" v-loading="loading" :data="rows" border stripe :row-key="rowKey">
|
||||
<el-table :key="dimension + '-' + currentRule" v-loading="loading" :data="rows" border stripe :row-key="rowKey">
|
||||
<template v-if="dimension === 'product'">
|
||||
<el-table-column prop="productCode" label="产品代码" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="productName" label="产品名称" min-width="180" show-overflow-tooltip />
|
||||
@@ -148,10 +159,10 @@
|
||||
<el-table-column prop="orderCount" label="订单数" width="100" align="right">
|
||||
<template #default="{ row }"><span class="success-value order-count-link" :class="{ 'is-disabled': !row.orderCount }" @click.stop="openOrderList(row)">{{ row.orderCount }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="costAmount" label="产品价" min-width="130" align="right"><template #default="{ row }">{{ money(row.costAmount) }}</template></el-table-column>
|
||||
<el-table-column v-if="!isGuangzhou" prop="costAmount" label="产品价" min-width="130" align="right"><template #default="{ row }">{{ money(row.costAmount) }}</template></el-table-column>
|
||||
<el-table-column prop="redlineAmount" label="红线价" min-width="130" align="right"><template #default="{ row }">{{ money(row.redlineAmount) }}</template></el-table-column>
|
||||
<el-table-column prop="redlineGrossProfit" label="红线毛利" min-width="130" align="right"><template #default="{ row }">{{ money(row.redlineGrossProfit) }}</template></el-table-column>
|
||||
<el-table-column prop="redlineGrossMarginRate" label="红线毛利率" min-width="120" align="right"><template #default="{ row }">{{ rate(row.redlineGrossMarginRate) }}</template></el-table-column>
|
||||
<el-table-column v-if="!isGuangzhou" prop="redlineGrossProfit" label="红线毛利" min-width="130" align="right"><template #default="{ row }">{{ money(row.redlineGrossProfit) }}</template></el-table-column>
|
||||
<el-table-column v-if="!isGuangzhou" prop="redlineGrossMarginRate" label="红线毛利率" min-width="120" align="right"><template #default="{ row }">{{ rate(row.redlineGrossMarginRate) }}</template></el-table-column>
|
||||
<el-table-column prop="salesAmount" label="销售价" min-width="130" align="right"><template #default="{ row }">{{ money(row.salesAmount) }}</template></el-table-column>
|
||||
<el-table-column prop="salesGrossProfit" label="销售毛利" min-width="130" align="right"><template #default="{ row }">{{ money(row.salesGrossProfit) }}</template></el-table-column>
|
||||
<el-table-column prop="salesGrossMarginRate" label="销售毛利率" min-width="120" align="right"><template #default="{ row }">{{ rate(row.salesGrossMarginRate) }}</template></el-table-column>
|
||||
@@ -173,10 +184,10 @@
|
||||
<el-table-column prop="productName" label="产品名称" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="countryName" label="国家/地区" min-width="110" show-overflow-tooltip />
|
||||
<el-table-column prop="currency" label="币种" width="80" align="center" />
|
||||
<el-table-column prop="costAmount" label="产品价" min-width="130" align="right"><template #default="{ row }">{{ money(row.costAmount) }}</template></el-table-column>
|
||||
<el-table-column v-if="!isGuangzhou" prop="costAmount" label="产品价" min-width="130" align="right"><template #default="{ row }">{{ money(row.costAmount) }}</template></el-table-column>
|
||||
<el-table-column prop="redlineAmount" label="红线价" min-width="130" align="right"><template #default="{ row }">{{ money(row.redlineAmount) }}</template></el-table-column>
|
||||
<el-table-column prop="redlineGrossProfit" label="红线毛利" min-width="130" align="right"><template #default="{ row }">{{ money(row.redlineGrossProfit) }}</template></el-table-column>
|
||||
<el-table-column prop="redlineGrossMarginRate" label="红线毛利率" min-width="120" align="right"><template #default="{ row }">{{ rate(row.redlineGrossMarginRate) }}</template></el-table-column>
|
||||
<el-table-column v-if="!isGuangzhou" prop="redlineGrossProfit" label="红线毛利" min-width="130" align="right"><template #default="{ row }">{{ money(row.redlineGrossProfit) }}</template></el-table-column>
|
||||
<el-table-column v-if="!isGuangzhou" prop="redlineGrossMarginRate" label="红线毛利率" min-width="120" align="right"><template #default="{ row }">{{ rate(row.redlineGrossMarginRate) }}</template></el-table-column>
|
||||
<el-table-column prop="salesAmount" label="销售价" min-width="130" align="right"><template #default="{ row }">{{ money(row.salesAmount) }}</template></el-table-column>
|
||||
<el-table-column prop="salesGrossProfit" label="销售毛利" min-width="130" align="right"><template #default="{ row }">{{ money(row.salesGrossProfit) }}</template></el-table-column>
|
||||
<el-table-column prop="salesGrossMarginRate" label="销售毛利率" min-width="120" align="right"><template #default="{ row }">{{ rate(row.salesGrossMarginRate) }}</template></el-table-column>
|
||||
@@ -212,9 +223,11 @@
|
||||
<el-table-column prop="productName" label="产品名称" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="countryName" label="国家/地区" width="110" />
|
||||
<el-table-column prop="currency" label="币种" width="80" align="center" />
|
||||
<el-table-column prop="costAmount" label="产品价" width="110" align="right"><template #default="{ row }">{{ money(row.costAmount) }}</template></el-table-column>
|
||||
<el-table-column v-if="!isGuangzhou" prop="costAmount" label="产品价" width="110" align="right"><template #default="{ row }">{{ money(row.costAmount) }}</template></el-table-column>
|
||||
<el-table-column prop="redlineAmount" label="红线价" width="110" align="right"><template #default="{ row }">{{ money(row.redlineAmount) }}</template></el-table-column>
|
||||
<el-table-column prop="salesAmount" label="销售价" width="110" align="right"><template #default="{ row }">{{ money(row.salesAmount) }}</template></el-table-column>
|
||||
<el-table-column v-if="isGuangzhou" prop="salesGrossProfit" label="销售毛利" width="110" align="right"><template #default="{ row }">{{ money(row.salesGrossProfit) }}</template></el-table-column>
|
||||
<el-table-column v-if="isGuangzhou" prop="salesGrossMarginRate" label="销售毛利率" width="110" align="right"><template #default="{ row }">{{ rate(row.salesGrossMarginRate) }}</template></el-table-column>
|
||||
<el-table-column prop="arBillStatus" label="应收状态" width="100" align="center"><template #default="{ row }">{{ arBillStatusLabel(row.arBillStatus) }}</template></el-table-column>
|
||||
<template #empty><el-empty description="暂无符合条件的订单" /></template>
|
||||
</el-table>
|
||||
@@ -296,7 +309,7 @@
|
||||
</template>
|
||||
|
||||
<script setup name="ProfitReport">
|
||||
import { downloadProfitExport, exportProfitOrders, exportProfitProducts, listProfitCustomerOptions, listProfitExports, listProfitOrders, listProfitProducts, listProfitSummary } from '@/api/report/profit'
|
||||
import { downloadProfitExport, exportProfitOrders, exportProfitProducts, listProfitAreaRules, listProfitCustomerOptions, listProfitExports, listProfitOrders, listProfitProducts, listProfitSummary } from '@/api/report/profit'
|
||||
// 提成功能暂停使用:import { deleteProfitCommissionRate, listProfitCommissionRates, saveProfitCommissionRate } from '@/api/report/profit'
|
||||
import { parseTime } from '@/utils/bms'
|
||||
import { ElMessage } from 'element-plus'
|
||||
@@ -304,16 +317,25 @@ import { saveAs } from 'file-saver'
|
||||
// 提成功能暂停使用:import { ElMessageBox } from 'element-plus'
|
||||
|
||||
const dimension = ref('product')
|
||||
const { customer_area } = useDict('customer_area')
|
||||
const multiValueFilterLimit = 2000
|
||||
const dateRange = ref([formatLocalDate(-30), formatLocalDate(0)])
|
||||
const arBillStatus = ref('')
|
||||
const query = reactive({
|
||||
customerArea: '',
|
||||
orderNo: '',
|
||||
customerCode: '',
|
||||
belongSalerCode: '',
|
||||
productCode: ''
|
||||
})
|
||||
const areaRules = ref(null)
|
||||
const areaRulesFailed = ref(false)
|
||||
const currentRule = computed(() => areaRules.value?.[String(query.customerArea)] || '')
|
||||
const isGuangzhou = computed(() => currentRule.value === 'GUANGZHOU')
|
||||
const summaryList = ref([])
|
||||
const summaryLoaded = ref(false)
|
||||
const summaryCards = computed(() => summaryList.value.length ? summaryList.value : [{ empty: true }])
|
||||
let summaryRequestVersion = 0
|
||||
const rows = ref([])
|
||||
let rowsRequestVersion = 0
|
||||
const loading = ref(false)
|
||||
@@ -397,15 +419,27 @@ function validateMultiValueFilters(showMessage = true) {
|
||||
return true
|
||||
}
|
||||
|
||||
function hasCustomerArea() {
|
||||
return query.customerArea !== '' && query.customerArea !== null && query.customerArea !== undefined
|
||||
}
|
||||
|
||||
function hasProfitRule() {
|
||||
return hasCustomerArea() && !!currentRule.value
|
||||
}
|
||||
|
||||
function loadSummary() {
|
||||
if (!validateMultiValueFilters(false)) return Promise.resolve()
|
||||
if (!hasProfitRule() || !validateMultiValueFilters(false)) return Promise.resolve()
|
||||
const requestVersion = ++summaryRequestVersion
|
||||
summaryLoaded.value = false
|
||||
return listProfitSummary(buildQuery(false)).then(res => {
|
||||
if (requestVersion !== summaryRequestVersion) return
|
||||
summaryList.value = res.data || []
|
||||
summaryLoaded.value = true
|
||||
})
|
||||
}
|
||||
|
||||
function loadRows() {
|
||||
if (!validateMultiValueFilters()) return Promise.resolve()
|
||||
if (!hasProfitRule() || !validateMultiValueFilters()) return Promise.resolve()
|
||||
const requestVersion = ++rowsRequestVersion
|
||||
const requestDimension = dimension.value
|
||||
loading.value = true
|
||||
@@ -419,14 +453,32 @@ function loadRows() {
|
||||
})
|
||||
}
|
||||
|
||||
function clearReportResults() {
|
||||
summaryRequestVersion++
|
||||
rowsRequestVersion++
|
||||
orderListRequestVersion++
|
||||
summaryList.value = []
|
||||
summaryLoaded.value = false
|
||||
rows.value = []
|
||||
total.value = 0
|
||||
loading.value = false
|
||||
orderListDialogVisible.value = false
|
||||
orderListLoading.value = false
|
||||
orderListRows.value = []
|
||||
orderListTotal.value = 0
|
||||
orderListContext.value = null
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
if (!validateMultiValueFilters()) return
|
||||
clearReportResults()
|
||||
if (!hasProfitRule() || !validateMultiValueFilters()) return
|
||||
pageNum.value = 1
|
||||
Promise.all([loadSummary(), loadRows()])
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
Object.keys(query).forEach(key => { query[key] = '' })
|
||||
query.customerArea = customer_area.value[0]?.value ?? ''
|
||||
arBillStatus.value = ''
|
||||
dateRange.value = [formatLocalDate(-30), formatLocalDate(0)]
|
||||
handleSearch()
|
||||
@@ -459,7 +511,7 @@ function openOrderList(row) {
|
||||
|
||||
function loadOrderList() {
|
||||
if (!orderListContext.value) return
|
||||
if (!validateMultiValueFilters()) return
|
||||
if (!hasProfitRule() || !validateMultiValueFilters()) return
|
||||
const requestVersion = ++orderListRequestVersion
|
||||
orderListLoading.value = true
|
||||
const params = {
|
||||
@@ -513,7 +565,7 @@ function arBillStatusLabel(value) {
|
||||
}
|
||||
|
||||
async function handleExport() {
|
||||
if (!validateMultiValueFilters()) return
|
||||
if (!hasProfitRule() || !validateMultiValueFilters()) return
|
||||
exporting.value = true
|
||||
const exporter = dimension.value === 'product' ? exportProfitProducts : exportProfitOrders
|
||||
try {
|
||||
@@ -625,7 +677,26 @@ function handleDeleteRate(row) {
|
||||
}
|
||||
*/
|
||||
|
||||
handleSearch()
|
||||
listProfitAreaRules().then(res => {
|
||||
areaRules.value = res.data || {}
|
||||
handleSearch()
|
||||
}).catch(() => {
|
||||
areaRules.value = {}
|
||||
areaRulesFailed.value = true
|
||||
clearReportResults()
|
||||
})
|
||||
|
||||
watch(customer_area, options => {
|
||||
if (!options?.length) {
|
||||
query.customerArea = ''
|
||||
clearReportResults()
|
||||
return
|
||||
}
|
||||
if (!options.some(area => area.value === query.customerArea)) {
|
||||
query.customerArea = options[0].value
|
||||
handleSearch()
|
||||
}
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -663,6 +734,10 @@ handleSearch()
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.rule-alert {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.summary-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(560px, 1fr));
|
||||
|
||||
Reference in New Issue
Block a user