重构产品价和红线价功能
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
<!-- start catalog version: pick effective hour -->
|
||||
<template>
|
||||
<el-dialog v-model="visible" :title="dialogTitle" width="460px" append-to-body @closed="onClosed">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
||||
<el-form-item :label="labNo">
|
||||
<span>{{ form.versionNo || '-' }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item :label="labEff" prop="effectiveTime">
|
||||
<el-date-picker
|
||||
v-model="form.effectiveTime"
|
||||
type="datetime"
|
||||
format="YYYY-MM-DD HH:00"
|
||||
value-format="YYYY-MM-DD HH:00:00"
|
||||
popper-class="activate-hour-picker"
|
||||
:placeholder="ph"
|
||||
:disabled-date="disabledDate"
|
||||
:disabled-hours="disabledHours"
|
||||
style="width: 100%"
|
||||
@change="onTimeChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<div class="activate-hint">{{ hint }}</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">{{ btnCancel }}</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="submit">{{ btnOk }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
loadVersions: { type: Function, required: true },
|
||||
activateFn: { type: Function, required: true }
|
||||
})
|
||||
const emit = defineEmits(['success'])
|
||||
const { proxy } = getCurrentInstance()
|
||||
const visible = ref(false)
|
||||
const submitting = ref(false)
|
||||
const formRef = ref()
|
||||
const versions = ref([])
|
||||
const prevEff = ref()
|
||||
const form = reactive({ psvId: undefined, versionNo: '', versionSeq: 0, effectiveTime: undefined })
|
||||
const dialogTitle = '启动版本'
|
||||
const labNo = '版本号'
|
||||
const labEff = '生效时间'
|
||||
const ph = '请选择生效时间'
|
||||
const btnCancel = '取消'
|
||||
const btnOk = '确认启动'
|
||||
const beforePrev = '生效时间不能早于上一版本的生效时间'
|
||||
const hint = computed(() => {
|
||||
const base = '若生效时间晚于当前,版本将进入待生效;到时自动生效并使上一版本失效。'
|
||||
return prevEff.value ? (base + '不得早于上一版:' + prevEff.value + '。') : base
|
||||
})
|
||||
const rules = {
|
||||
effectiveTime: [
|
||||
{ required: true, message: ph, trigger: 'change' },
|
||||
{
|
||||
validator: (_r, v, cb) => {
|
||||
if (!v || !prevEff.value) return cb()
|
||||
if (toMs(v) < toMs(prevEff.value)) return cb(new Error(beforePrev))
|
||||
cb()
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
]
|
||||
}
|
||||
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 toMs(val) {
|
||||
const n = normalizeToHour(val)
|
||||
if (!n) return NaN
|
||||
return new Date(String(n).replace(/-/g, '/')).getTime()
|
||||
}
|
||||
function findPrev(row) {
|
||||
const cur = Number(row && row.versionSeq) || 0
|
||||
let prev = null
|
||||
for (const v of versions.value || []) {
|
||||
if (!v || v.psvId == null || String(v.psvId) === String(row.psvId)) continue
|
||||
const vs = Number(v.versionSeq) || 0
|
||||
if (vs >= cur || !v.effectiveTime) continue
|
||||
if (!prev || vs > (Number(prev.versionSeq) || 0)) prev = v
|
||||
}
|
||||
return prev ? normalizeToHour(prev.effectiveTime) : undefined
|
||||
}
|
||||
function pickDefault(prev) {
|
||||
const now = formatHour(new Date())
|
||||
if (!prev) return now
|
||||
return toMs(now) >= toMs(prev) ? now : prev
|
||||
}
|
||||
function disabledDate(date) {
|
||||
if (!prevEff.value || !date) return false
|
||||
const prevDay = new Date(String(prevEff.value).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 disabledHours() {
|
||||
if (!prevEff.value) return []
|
||||
const prevD = new Date(String(prevEff.value).replace(/-/g, '/'))
|
||||
const curD = form.effectiveTime
|
||||
? new Date(String(normalizeToHour(form.effectiveTime)).replace(/-/g, '/'))
|
||||
: new Date()
|
||||
if (prevD.getFullYear() !== curD.getFullYear() || prevD.getMonth() !== curD.getMonth() || prevD.getDate() !== curD.getDate()) return []
|
||||
const hours = []
|
||||
for (let h = 0; h < prevD.getHours(); h++) hours.push(h)
|
||||
return hours
|
||||
}
|
||||
function onTimeChange(val) {
|
||||
form.effectiveTime = normalizeToHour(val)
|
||||
formRef.value && formRef.value.validateField('effectiveTime')
|
||||
}
|
||||
async function open(row) {
|
||||
if (!row || row.psvId == null) return
|
||||
const list = await props.loadVersions()
|
||||
versions.value = list || []
|
||||
const found = versions.value.find(v => String(v.psvId) === String(row.psvId)) || row
|
||||
form.psvId = found.psvId
|
||||
form.versionNo = found.versionNo || ''
|
||||
form.versionSeq = found.versionSeq
|
||||
prevEff.value = findPrev(found)
|
||||
form.effectiveTime = pickDefault(prevEff.value)
|
||||
visible.value = true
|
||||
}
|
||||
function onClosed() {
|
||||
form.psvId = undefined
|
||||
form.versionNo = ''
|
||||
form.effectiveTime = undefined
|
||||
prevEff.value = undefined
|
||||
formRef.value && formRef.value.clearValidate()
|
||||
}
|
||||
function submit() {
|
||||
formRef.value.validate(valid => {
|
||||
if (!valid) return
|
||||
form.effectiveTime = normalizeToHour(form.effectiveTime)
|
||||
submitting.value = true
|
||||
props.activateFn(form.psvId, { effectiveTime: form.effectiveTime }).then(() => {
|
||||
const pending = form.effectiveTime && new Date(String(form.effectiveTime).replace(/-/g, '/')).getTime() > Date.now()
|
||||
proxy.$modal.msgSuccess(pending ? '已设为待生效' : '版本已启动')
|
||||
visible.value = false
|
||||
emit('success')
|
||||
}).finally(() => { submitting.value = false })
|
||||
})
|
||||
}
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
<style scoped>
|
||||
.activate-hint { color: var(--el-text-color-secondary); font-size: 12px; line-height: 1.5; }
|
||||
</style>
|
||||
@@ -0,0 +1,689 @@
|
||||
<template>
|
||||
<div class="app-container" v-loading="pageLoading">
|
||||
<el-card shadow="never" class="step-card">
|
||||
<template #header>
|
||||
<div class="card-head">
|
||||
<span>
|
||||
{{ headerTitle }}
|
||||
<span v-if="versionNo" class="sub">· {{ labVer }} {{ versionNo }}</span>
|
||||
</span>
|
||||
<div class="card-actions">
|
||||
<el-dropdown trigger="click" @command="handleImportCommand">
|
||||
<el-button type="primary" size="small" :disabled="!buildMode || importing || finishing">
|
||||
{{ labImport }}
|
||||
<el-icon class="el-icon--right"><arrow-down /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="all">{{ labImportAll }}</el-dropdown-item>
|
||||
<el-dropdown-item command="country">{{ labImportCtry }}</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="build-choice">
|
||||
<p class="build-choice__tip">{{ labTip }}</p>
|
||||
<div class="build-choice__cards">
|
||||
<button
|
||||
type="button"
|
||||
class="build-card build-card--pull"
|
||||
:class="{ selected: buildMode === 'COPY' }"
|
||||
:disabled="!!psvId || choosing || finishing"
|
||||
@click="chooseMode(true)"
|
||||
>
|
||||
<div class="build-card__title">{{ labModeCopy }}</div>
|
||||
<div class="build-card__desc">{{ labModeCopyDesc }}</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="build-card build-card--import"
|
||||
:class="{ selected: buildMode === 'IMPORT' }"
|
||||
:disabled="!!psvId || choosing || finishing"
|
||||
@click="chooseMode(false)"
|
||||
>
|
||||
<div class="build-card__title">{{ labModeFresh }}</div>
|
||||
<div class="build-card__desc">{{ labModeFreshDesc }}</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-alert
|
||||
v-if="pendingImports.length"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="mb12"
|
||||
:title="pendingImportTip"
|
||||
/>
|
||||
<div class="filters">
|
||||
<div class="filters__left">
|
||||
<el-input v-model="keyword" clearable :placeholder="labKw" style="width: 220px" />
|
||||
<el-button size="small" @click="noop">{{ labQuery }}</el-button>
|
||||
</div>
|
||||
<div class="filters__count">{{ productCountText }}</div>
|
||||
</div>
|
||||
|
||||
<el-empty v-if="!filteredRows.length" :description="labEmpty" :image-size="80" />
|
||||
<el-table v-else :data="filteredRows" border>
|
||||
<el-table-column :label="labCode" min-width="100">
|
||||
<template #default="{ row }"><b>{{ row.productCode }}</b></template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="labName" prop="productName" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column :label="labLevel" prop="serviceLevel" width="90" align="center" />
|
||||
<el-table-column :label="labCargo" min-width="140" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.cargoAttr" type="primary" size="small" effect="plain">{{ row.cargoAttr }}</el-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="labRegion" prop="destRegion" width="90" align="center" />
|
||||
<el-table-column :label="labAction" width="220" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.productId || row.productCode" link type="primary" @click="openCountry(row)">{{ labDetail }}</el-button>
|
||||
<el-button v-if="row.productId || row.productCode" link type="primary" @click="openSurcharge(row)">{{ labSurcharge }}</el-button>
|
||||
<el-button link type="danger" @click="removeRow(row)">{{ labRemove }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<div class="wizard-actions">
|
||||
<el-button @click="goBack">{{ labCancel }}</el-button>
|
||||
<el-button type="primary" :loading="finishing" @click="finishSave">{{ labFinish }}</el-button>
|
||||
</div>
|
||||
|
||||
<component :is="countryComp" ref="countryRef" />
|
||||
<component :is="surchargeComp" ref="surchargeRef" @success="loadProducts" />
|
||||
|
||||
<excel-import-dialog
|
||||
ref="importRef"
|
||||
:title="titleAll"
|
||||
width="560px"
|
||||
:show-update-support="false"
|
||||
template-url="/templates/product/product-import-all-template.xlsx"
|
||||
:template-file-name="tplAll"
|
||||
:import-desc="allImportDesc"
|
||||
:file-limit="5"
|
||||
:submit-handler="runImportAll"
|
||||
@success="onImportOk"
|
||||
/>
|
||||
<excel-import-dialog
|
||||
ref="countryImportRef"
|
||||
:title="titleCtry"
|
||||
width="560px"
|
||||
:show-update-support="false"
|
||||
:before-submit="validateCountryImport"
|
||||
:import-desc="countryImportDesc"
|
||||
template-url="/templates/product/product-import-country-template.xlsx"
|
||||
:template-file-name="tplCtry"
|
||||
:file-limit="5"
|
||||
:submit-handler="runImportCountry"
|
||||
@success="onImportOk"
|
||||
>
|
||||
<template #extra>
|
||||
<el-form label-width="90px">
|
||||
<el-form-item :label="labCountry" required>
|
||||
<el-select v-model="importCountryCode" filterable clearable :placeholder="labPickCountry" style="width: 100%">
|
||||
<el-option
|
||||
v-for="item in regionOptions"
|
||||
:key="item.countryCode2"
|
||||
:label="regionLabel(item)"
|
||||
:value="item.countryCode2"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
</excel-import-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="importProgressVisible"
|
||||
:title="progressTitle"
|
||||
width="420px"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
:show-close="false"
|
||||
append-to-body
|
||||
>
|
||||
<div class="import-progress">
|
||||
<el-progress :percentage="importProgressPercent" :status="importProgressStatus" />
|
||||
<div class="import-progress-msg">{{ importProgressMessage }}</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ExcelImportDialog from '@/components/ExcelImportDialog'
|
||||
import { listRegionOptions } from '@/api/data/region'
|
||||
|
||||
const props = defineProps({
|
||||
kind: { type: String, required: true },
|
||||
listPath: { type: String, required: true },
|
||||
listFn: { type: Function, required: true },
|
||||
delFn: { type: Function, required: true },
|
||||
prepareFn: { type: Function, required: true },
|
||||
applyPreviewFn: { type: Function, required: true },
|
||||
attachExcelFn: { type: Function, required: true },
|
||||
importAllFn: { type: Function, required: true },
|
||||
importCountryFn: { type: Function, required: true },
|
||||
previewAllFn: { type: Function, required: true },
|
||||
previewCountryFn: { type: Function, required: true },
|
||||
countryComp: { type: [Object, String], required: true },
|
||||
surchargeComp: { type: [Object, String], required: true }
|
||||
})
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const labTip = '创建价格方式'
|
||||
const labModeCopy = '按现有生效版本建价'
|
||||
const labModeCopyDesc = '先本地预览已生效版本产品,点击「完成」时再复制到待生效草稿'
|
||||
const labModeFresh = '重新建产品价'
|
||||
const labModeFreshDesc = "不沿用已生效版本;导入解析后先展示,点「完成」时再写入草稿"
|
||||
const labImport = '导入 Excel 建价'
|
||||
const labImportAll = '全部导入(多国家价表)'
|
||||
const labImportCtry = '按国家导入(单国专线成本价)'
|
||||
const labKw = '产品代码/名称'
|
||||
const labQuery = '查询'
|
||||
const labEmpty = '请先选择建价方式,或导入 Excel'
|
||||
const labCode = '产品代码'
|
||||
const labName = '产品名称'
|
||||
const labLevel = '时效等级'
|
||||
const labCargo = '走货属性'
|
||||
const labRegion = '目的区域'
|
||||
const labAction = '操作'
|
||||
const labDetail = "明细"
|
||||
const labSurcharge = "附加费"
|
||||
const labRemove = '移除'
|
||||
const labCancel = '取消'
|
||||
const labFinish = '完成'
|
||||
const labVer = '版本'
|
||||
const labCountry = '目的国家'
|
||||
const labPickCountry = '请选择国家'
|
||||
const labImporting = '正在导入'
|
||||
const labSaving = '正在保存'
|
||||
const tplAll = '产品多国家价表导入模板'
|
||||
const tplCtry = '产品按国家导入模板'
|
||||
const titleAll = props.kind === 'product' ? '产品 Excel 导入' : '红线价 Excel 导入'
|
||||
const titleCtry = '按国家导入'
|
||||
const headerTitle = props.kind === 'product' ? '编辑产品价格' : '编辑红线价'
|
||||
|
||||
const allImportDesc = ['适用多国家价表,按目录 sheet 匹配国家导入。', '导入不选生效时间,数据落到待生效草稿版本。', '请在弹窗底部下载模板。']
|
||||
const countryImportDesc = ['选择目的国家后上传专线成本价表。', '导入不选生效时间,数据落到待生效草稿版本。', '邮编、赔偿标准等 sheet 不导入。']
|
||||
|
||||
const pageLoading = ref(false)
|
||||
const progressTitle = ref(labImporting)
|
||||
const choosing = ref(false)
|
||||
const importing = ref(false)
|
||||
const psvId = ref()
|
||||
const versionNo = ref('')
|
||||
const buildMode = ref('')
|
||||
const rows = ref([])
|
||||
const keyword = ref('')
|
||||
const countryRef = ref()
|
||||
const surchargeRef = ref()
|
||||
const importRef = ref()
|
||||
const countryImportRef = ref()
|
||||
const importCountryCode = ref('')
|
||||
const regionOptions = ref([])
|
||||
const importProgressVisible = ref(false)
|
||||
const importProgressPercent = ref(0)
|
||||
const importProgressMessage = ref('')
|
||||
const importProgressStatus = ref(undefined)
|
||||
|
||||
const finishing = ref(false)
|
||||
const localRemoves = ref([])
|
||||
const pendingImports = ref([])
|
||||
const previewDetailsByCode = ref({})
|
||||
const pendingImportTip = computed(() => {
|
||||
const n = pendingImports.value.length
|
||||
if (!n) return ''
|
||||
return "已解析展示,暂存 " + n + " 个导入文件;点「完成」后一并写入草稿"
|
||||
})
|
||||
const filteredRows = computed(() => {
|
||||
const kw = (keyword.value || '').trim().toLowerCase()
|
||||
if (!kw) return rows.value || []
|
||||
return (rows.value || []).filter(r => {
|
||||
const blob = [r.productCode, r.productName].filter(Boolean).join(' ').toLowerCase()
|
||||
return blob.includes(kw)
|
||||
})
|
||||
})
|
||||
const productCountText = computed(() => {
|
||||
const n = filteredRows.value.length
|
||||
const total = (rows.value || []).length
|
||||
if (n === total) {
|
||||
return "共 " + n + " 个产品"
|
||||
}
|
||||
return "共 " + n + " 个产品(全部 " + total + " 个)"
|
||||
})
|
||||
|
||||
|
||||
function noop() {}
|
||||
|
||||
function isRoutable(path) {
|
||||
if (!path) return false
|
||||
const resolved = router.resolve(path)
|
||||
const matched = resolved.matched || []
|
||||
if (!matched.length) return false
|
||||
return !matched.some(m => String(m.path || '').includes('pathMatch'))
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
const path = props.listPath || (router.currentRoute.value.meta && router.currentRoute.value.meta.activeMenu)
|
||||
const obj = isRoutable(path) ? { path } : undefined
|
||||
if (proxy.$tab) {
|
||||
if (obj) proxy.$tab.closeOpenPage(obj)
|
||||
else proxy.$tab.closePage()
|
||||
return
|
||||
}
|
||||
if (obj) router.push(obj)
|
||||
else router.back()
|
||||
}
|
||||
|
||||
function applyVersion(ver) {
|
||||
if (!ver) {
|
||||
psvId.value = undefined
|
||||
versionNo.value = ''
|
||||
return
|
||||
}
|
||||
psvId.value = ver.psvId
|
||||
versionNo.value = ver.versionNo || ''
|
||||
const mode = String(ver.priceBuildMode || '').toUpperCase()
|
||||
if (mode === 'COPY') buildMode.value = 'COPY'
|
||||
else if (mode === 'IMPORT' || mode === 'PULL') buildMode.value = 'IMPORT'
|
||||
}
|
||||
|
||||
async function loadProducts() {
|
||||
if (!psvId.value) {
|
||||
rows.value = []
|
||||
return
|
||||
}
|
||||
const res = await props.listFn({ psvId: psvId.value, pageNum: 1, pageSize: 500 })
|
||||
rows.value = res.rows || res.data || []
|
||||
}
|
||||
|
||||
async function loadEffectPreview() {
|
||||
const res = await props.listFn({ statusGroup: 'effect', pageNum: 1, pageSize: 500 })
|
||||
rows.value = (res.rows || res.data || []).map(r => ({ ...r, _preview: true }))
|
||||
}
|
||||
|
||||
async function bootstrap() {
|
||||
pageLoading.value = true
|
||||
try {
|
||||
// Only resume a draft when opened from pending list "Edit" (?psvId=).
|
||||
// Plain "New" must stay empty so build-mode cards stay selectable.
|
||||
const qPsv = route.query && route.query.psvId
|
||||
if (qPsv != null && qPsv !== '') {
|
||||
psvId.value = Number(qPsv)
|
||||
versionNo.value = (route.query && route.query.versionNo) ? String(route.query.versionNo) : ''
|
||||
if (!buildMode.value) buildMode.value = 'IMPORT'
|
||||
await loadProducts()
|
||||
}
|
||||
} finally {
|
||||
pageLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** Mode select: local only, no DB write. */
|
||||
async function chooseMode(copyFromLatest) {
|
||||
if (psvId.value || choosing.value || finishing.value) return
|
||||
const next = copyFromLatest ? 'COPY' : 'IMPORT'
|
||||
if (buildMode.value === next && (copyFromLatest ? rows.value.length : true)) {
|
||||
if (copyFromLatest && rows.value.length) return
|
||||
if (!copyFromLatest && !rows.value.length && !pendingImports.value.length) return
|
||||
}
|
||||
choosing.value = true
|
||||
pageLoading.value = true
|
||||
try {
|
||||
buildMode.value = next
|
||||
localRemoves.value = []
|
||||
pendingImports.value = []
|
||||
previewDetailsByCode.value = {}
|
||||
if (copyFromLatest) {
|
||||
await loadEffectPreview()
|
||||
} else {
|
||||
rows.value = []
|
||||
}
|
||||
} finally {
|
||||
choosing.value = false
|
||||
pageLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openCountry(row) {
|
||||
if (!row || !(row.productId || row.productCode)) return
|
||||
const code = String(row.productCode || '').toUpperCase()
|
||||
const preview = previewDetailsByCode.value && previewDetailsByCode.value[code]
|
||||
if ((!row.productId || row._preview) && Array.isArray(preview)) {
|
||||
countryRef.value && countryRef.value.open(row, preview)
|
||||
return
|
||||
}
|
||||
countryRef.value && countryRef.value.open(row)
|
||||
}
|
||||
|
||||
function openSurcharge(row) {
|
||||
if (!row || !(row.productId || row.productCode)) return
|
||||
surchargeRef.value && surchargeRef.value.open(row)
|
||||
}
|
||||
|
||||
function removeRow(row) {
|
||||
if (!row) return
|
||||
const code = row.productCode
|
||||
if (psvId.value && row.productId != null) {
|
||||
proxy.$modal.confirm('确认从草稿中移除该产品?').then(() => props.delFn(row.productId)).then(() => {
|
||||
proxy.$modal.msgSuccess('移除成功')
|
||||
loadProducts()
|
||||
}).catch(() => {})
|
||||
return
|
||||
}
|
||||
rows.value = (rows.value || []).filter(r => r !== row && r.productCode !== code)
|
||||
if (code) {
|
||||
const up = String(code).toUpperCase()
|
||||
if (previewDetailsByCode.value && previewDetailsByCode.value[up]) {
|
||||
const next = { ...previewDetailsByCode.value }
|
||||
delete next[up]
|
||||
previewDetailsByCode.value = next
|
||||
}
|
||||
if (!localRemoves.value.includes(code)) {
|
||||
localRemoves.value = localRemoves.value.concat([code])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function regionLabel(item) {
|
||||
const name = item.countryNameCn || item.countryNameEn || ''
|
||||
const code = item.countryCode2 || ''
|
||||
return name && code ? name + ' (' + code + ')' : (name || code)
|
||||
}
|
||||
|
||||
function resolveImportCountryName(code) {
|
||||
const hit = (regionOptions.value || []).find(r => String(r.countryCode2 || '').toUpperCase() === String(code || '').toUpperCase())
|
||||
if (!hit) return code || ''
|
||||
return hit.countryNameCn || hit.countryNameEn || hit.countryCode2 || code || ''
|
||||
}
|
||||
|
||||
function loadRegionOptions() {
|
||||
if (regionOptions.value.length) return
|
||||
listRegionOptions().then(res => { regionOptions.value = res.data || [] })
|
||||
}
|
||||
|
||||
async function persistDraft() {
|
||||
if (psvId.value) return true
|
||||
if (!buildMode.value) {
|
||||
proxy.$modal.msgWarning('请先选择建价方式')
|
||||
return false
|
||||
}
|
||||
const res = await props.prepareFn(buildMode.value === 'COPY')
|
||||
applyVersion(res.data)
|
||||
return !!psvId.value
|
||||
}
|
||||
|
||||
async function applyLocalRemoves() {
|
||||
const codes = localRemoves.value || []
|
||||
if (!codes.length || !psvId.value) return
|
||||
const res = await props.listFn({ psvId: psvId.value, pageNum: 1, pageSize: 500 })
|
||||
const list = res.rows || res.data || []
|
||||
for (const code of codes) {
|
||||
const hit = list.find(r => r && String(r.productCode) === String(code) && r.productId != null)
|
||||
if (hit) {
|
||||
await props.delFn(hit.productId).catch(() => {})
|
||||
}
|
||||
}
|
||||
localRemoves.value = []
|
||||
}
|
||||
|
||||
/** Write in-memory preview details into draft (avoids re-import requiring effectiveDate). */
|
||||
async function applyPendingPreview(onProgress) {
|
||||
const src = previewDetailsByCode.value || {}
|
||||
const detailsByProductCode = {}
|
||||
for (const [k, v] of Object.entries(src)) {
|
||||
const code = String(k || '').toUpperCase()
|
||||
if (!code || !Array.isArray(v) || !v.length) continue
|
||||
detailsByProductCode[code] = v
|
||||
}
|
||||
const codes = Object.keys(detailsByProductCode)
|
||||
if (!codes.length) {
|
||||
return
|
||||
}
|
||||
if (!psvId.value) {
|
||||
throw new Error('missing draft psvId')
|
||||
}
|
||||
const metaMap = new Map()
|
||||
for (const r of rows.value || []) {
|
||||
const code = String((r && r.productCode) || '').toUpperCase()
|
||||
if (code) metaMap.set(code, r)
|
||||
}
|
||||
const products = codes.map(code => {
|
||||
const r = metaMap.get(code) || {}
|
||||
return {
|
||||
productCode: r.productCode || code,
|
||||
productName: r.productName,
|
||||
serviceLevel: r.serviceLevel,
|
||||
cargoAttr: r.cargoAttr,
|
||||
destRegion: r.destRegion,
|
||||
taxMode: r.taxMode
|
||||
}
|
||||
})
|
||||
await props.applyPreviewFn({
|
||||
psvId: psvId.value,
|
||||
products,
|
||||
detailsByProductCode
|
||||
}, onProgress)
|
||||
}
|
||||
|
||||
/** Persist staged Excel files onto draft version (prc_price_excel_file). */
|
||||
async function savePendingExcelFiles() {
|
||||
const jobs = pendingImports.value.slice()
|
||||
pendingImports.value = []
|
||||
if (!jobs.length || !psvId.value || typeof props.attachExcelFn !== 'function') return
|
||||
for (const job of jobs) {
|
||||
if (!job || !job.file) continue
|
||||
await props.attachExcelFn(psvId.value, job.file)
|
||||
}
|
||||
}
|
||||
|
||||
async function finishSave() {
|
||||
if (finishing.value) return
|
||||
if (!buildMode.value) {
|
||||
proxy.$modal.msgWarning('请先选择建价方式')
|
||||
return
|
||||
}
|
||||
finishing.value = true
|
||||
progressTitle.value = labSaving
|
||||
importProgressVisible.value = true
|
||||
importProgressPercent.value = 0
|
||||
importProgressMessage.value = ''
|
||||
importProgressStatus.value = undefined
|
||||
try {
|
||||
const ok = await persistDraft()
|
||||
if (!ok) return
|
||||
await applyPendingPreview(applyImportProgress)
|
||||
await savePendingExcelFiles()
|
||||
await applyLocalRemoves()
|
||||
proxy.$modal.msgSuccess('已保存到待生效草稿')
|
||||
goBack()
|
||||
} catch (e) {
|
||||
const msg = (e && e.message) || (e && e.msg) || 'save failed'
|
||||
if (proxy.$modal && proxy.$modal.msgError) proxy.$modal.msgError(msg)
|
||||
} finally {
|
||||
importProgressVisible.value = false
|
||||
finishing.value = false
|
||||
pageLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleImportCommand(command) {
|
||||
if (!buildMode.value) {
|
||||
proxy.$modal.msgWarning('请先选择建价方式')
|
||||
return
|
||||
}
|
||||
if (command === 'country') {
|
||||
importCountryCode.value = ''
|
||||
loadRegionOptions()
|
||||
countryImportRef.value.open()
|
||||
return
|
||||
}
|
||||
importRef.value.open()
|
||||
}
|
||||
|
||||
function validateCountryImport() {
|
||||
if (!importCountryCode.value) return '请先选择目的国家'
|
||||
return ''
|
||||
}
|
||||
|
||||
function applyImportProgress(t) {
|
||||
importProgressPercent.value = Math.min(100, Number(t.percent) || 0)
|
||||
importProgressMessage.value = t.message || t.error || ''
|
||||
if (t.status === 'SUCCESS') importProgressStatus.value = 'success'
|
||||
else if (t.status === 'FAIL') importProgressStatus.value = 'exception'
|
||||
}
|
||||
|
||||
async function runImportWithProgress(runner) {
|
||||
importing.value = true
|
||||
importProgressVisible.value = true
|
||||
importProgressPercent.value = 0
|
||||
importProgressMessage.value = ''
|
||||
importProgressStatus.value = undefined
|
||||
try {
|
||||
const { task } = await runner(applyImportProgress)
|
||||
importProgressPercent.value = 100
|
||||
importProgressStatus.value = 'success'
|
||||
return (task && task.message) ? task.message : 'ok'
|
||||
} catch (e) {
|
||||
importProgressStatus.value = 'exception'
|
||||
importProgressMessage.value = (e && e.task && e.task.error) || (e && e.message) || 'fail'
|
||||
await new Promise(r => setTimeout(r, 600))
|
||||
throw e
|
||||
} finally {
|
||||
importProgressVisible.value = false
|
||||
importing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function mergePreviewDetails(detailsByCode) {
|
||||
if (!detailsByCode || typeof detailsByCode !== 'object') return
|
||||
const next = { ...(previewDetailsByCode.value || {}) }
|
||||
for (const [k, v] of Object.entries(detailsByCode)) {
|
||||
const code = String(k || '').toUpperCase()
|
||||
if (!code || !Array.isArray(v)) continue
|
||||
next[code] = v
|
||||
}
|
||||
previewDetailsByCode.value = next
|
||||
}
|
||||
|
||||
function mergePreviewProducts(list, detailsByCode) {
|
||||
mergePreviewDetails(detailsByCode)
|
||||
const map = new Map()
|
||||
for (const r of rows.value || []) {
|
||||
const code = String((r && r.productCode) || '').toUpperCase()
|
||||
if (code) map.set(code, r)
|
||||
}
|
||||
for (const p of list || []) {
|
||||
if (!p || !p.productCode) continue
|
||||
const code = String(p.productCode).toUpperCase()
|
||||
localRemoves.value = (localRemoves.value || []).filter(c => String(c).toUpperCase() !== code)
|
||||
map.set(code, {
|
||||
productCode: p.productCode,
|
||||
productName: p.productName,
|
||||
serviceLevel: p.serviceLevel,
|
||||
cargoAttr: p.cargoAttr,
|
||||
destRegion: p.destRegion,
|
||||
_preview: true
|
||||
})
|
||||
}
|
||||
rows.value = Array.from(map.values())
|
||||
}
|
||||
|
||||
function unwrapUploadFiles(uploadFile) {
|
||||
const list = Array.isArray(uploadFile) ? uploadFile : [uploadFile]
|
||||
const out = []
|
||||
for (const item of list) {
|
||||
const raw = item && (item.raw || item)
|
||||
if (raw) out.push(raw)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
async function runImportAll(uploadFile) {
|
||||
const files = unwrapUploadFiles(uploadFile)
|
||||
if (!files.length) throw new Error("请先选择文件")
|
||||
const res = await props.previewAllFn(files, (t) => {
|
||||
importRef.value && importRef.value.setProgress && importRef.value.setProgress(t && t.percent)
|
||||
})
|
||||
const data = (res && res.data) ? res.data : {}
|
||||
mergePreviewProducts(data.products || [], data.detailsByProductCode)
|
||||
pendingImports.value = pendingImports.value.concat(files.map(raw => ({ kind: 'all', file: raw })))
|
||||
const shown = (rows.value || []).length
|
||||
return "解析成功,共 " + files.length + " 个文件,已展示 " + shown + " 个产品,点「完成」后写入草稿"
|
||||
}
|
||||
|
||||
async function runImportCountry(uploadFile) {
|
||||
const files = unwrapUploadFiles(uploadFile)
|
||||
if (!files.length) throw new Error("请先选择文件")
|
||||
const err = validateCountryImport()
|
||||
if (err) throw new Error(err)
|
||||
const code = importCountryCode.value
|
||||
const countryName = resolveImportCountryName(code)
|
||||
const res = await props.previewCountryFn(files, code, (t) => {
|
||||
countryImportRef.value && countryImportRef.value.setProgress && countryImportRef.value.setProgress(t && t.percent)
|
||||
})
|
||||
const data = (res && res.data) ? res.data : {}
|
||||
const products = (data.products || []).map(p => ({
|
||||
...p,
|
||||
destRegion: p.destRegion || countryName
|
||||
}))
|
||||
mergePreviewProducts(products, data.detailsByProductCode)
|
||||
pendingImports.value = pendingImports.value.concat(files.map(raw => ({ kind: 'country', file: raw, countryCode: code })))
|
||||
const shown = (rows.value || []).length
|
||||
return "解析成功,共 " + files.length + " 个文件,已展示 " + shown + " 个产品,点「完成」后写入草稿"
|
||||
}
|
||||
|
||||
function onImportOk() {
|
||||
// preview already merged into rows; finish will persist
|
||||
}
|
||||
|
||||
onMounted(() => { bootstrap() })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.step-card { margin-bottom: 16px; }
|
||||
.mb12 { margin-bottom: 12px; }
|
||||
.card-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
.sub { margin-left: 8px; color: #909399; font-size: 13px; font-weight: 400; }
|
||||
.card-actions { display: flex; align-items: center; gap: 8px; }
|
||||
.build-choice { padding: 8px 0 16px; text-align: left; }
|
||||
.build-choice__tip { margin: 0 0 12px; color: #606266; font-size: 13px; font-weight: 600; }
|
||||
.build-choice__cards { display: flex; justify-content: flex-start; gap: 12px; flex-wrap: wrap; margin-bottom: 8px; }
|
||||
.build-card {
|
||||
width: 280px; min-height: 88px; padding: 14px 16px;
|
||||
border: 1px solid #e4e7ed; border-radius: 8px; background: #fff;
|
||||
cursor: pointer; text-align: left;
|
||||
transition: border-color 0.2s, box-shadow 0.2s, background 0.2s;
|
||||
}
|
||||
.build-card.selected { border-color: #1890ff; background: #e6f7ff; }
|
||||
.build-card:hover:not(:disabled) { box-shadow: 0 4px 14px rgba(0, 0, 0, 0.06); }
|
||||
.build-card:disabled { opacity: 0.55; cursor: not-allowed; }
|
||||
.build-card__title { font-size: 16px; font-weight: 600; color: #303133; margin-bottom: 10px; }
|
||||
.build-card--pull .build-card__title { color: #67c23a; }
|
||||
.build-card--import .build-card__title { color: #409eff; }
|
||||
.build-card__desc { font-size: 13px; line-height: 1.6; color: #909399; }
|
||||
.filters { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 12px; }
|
||||
.filters__left { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
|
||||
.filters__count { color: #909399; font-size: 13px; white-space: nowrap; }
|
||||
.wizard-actions { display: flex; justify-content: center; gap: 12px; padding: 8px 0 24px; }
|
||||
.import-progress { padding: 8px 4px 4px; }
|
||||
.import-progress-msg {
|
||||
margin-top: 12px; color: var(--el-text-color-secondary); font-size: 13px;
|
||||
line-height: 1.5; word-break: break-word; max-height: 160px; overflow: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<el-drawer v-model="visible" size="860px" :title="title" append-to-body>
|
||||
<el-table v-loading="loading" :data="list" border max-height="calc(100vh - 160px)" :empty-text="emptyText">
|
||||
<el-table-column :label="labFile" prop="fileName" min-width="220" show-overflow-tooltip />
|
||||
<el-table-column :label="labEff" prop="effectiveTime" width="170" align="center" />
|
||||
<el-table-column :label="labSize" width="100" align="right">
|
||||
<template #default="{ row }">{{ formatSize(row.fileSize) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="labUser" prop="createBy" width="110" align="center" show-overflow-tooltip />
|
||||
<el-table-column :label="labTime" prop="createTime" width="170" align="center" />
|
||||
<el-table-column :label="labAction" width="90" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" :disabled="!row.filePath" @click="handleDownload(row)">{{ labDown }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
listFn: { type: Function, required: true }
|
||||
})
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
|
||||
const emptyText = '\u8be5\u7248\u672c\u6682\u65e0\u5bfc\u5165\u9644\u4ef6'
|
||||
const labFile = '\u6587\u4ef6\u540d\u79f0'
|
||||
const labEff = '\u751f\u6548\u65f6\u95f4'
|
||||
const labSize = '\u5927\u5c0f'
|
||||
const labUser = '\u4e0a\u4f20\u4eba'
|
||||
const labTime = '\u4e0a\u4f20\u65f6\u95f4'
|
||||
const labAction = '\u64cd\u4f5c'
|
||||
const labDown = '\u4e0b\u8f7d'
|
||||
const labTitle = '\u5bfc\u5165\u9644\u4ef6'
|
||||
const labNoPath = '\u6587\u4ef6\u8def\u5f84\u4e0d\u5b58\u5728'
|
||||
|
||||
const visible = ref(false)
|
||||
const title = ref('')
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const version = ref(null)
|
||||
|
||||
function open(row) {
|
||||
version.value = row
|
||||
const no = (row && (row.versionNo || row.productCode)) || ''
|
||||
title.value = no ? (no + ' \u00b7 ' + labTitle) : labTitle
|
||||
visible.value = true
|
||||
loadList()
|
||||
}
|
||||
|
||||
function loadList() {
|
||||
if (!version.value || version.value.psvId == null) {
|
||||
list.value = []
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
props.listFn(version.value.psvId).then(res => {
|
||||
list.value = res.data || []
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function formatSize(bytes) {
|
||||
const n = Number(bytes)
|
||||
if (!n || n < 0) return '-'
|
||||
if (n < 1024) return n + ' B'
|
||||
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KB'
|
||||
return (n / (1024 * 1024)).toFixed(2) + ' MB'
|
||||
}
|
||||
|
||||
function handleDownload(row) {
|
||||
if (!row || !row.filePath) {
|
||||
proxy.$modal.msgWarning(labNoPath)
|
||||
return
|
||||
}
|
||||
proxy.$download.resource(row.filePath)
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
@@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<el-drawer v-model="visible" :title="title" size="72%" destroy-on-close append-to-body>
|
||||
<div class="toolbar">
|
||||
<el-input v-model="keyword" clearable :placeholder="phKw" style="width: 260px" />
|
||||
<el-button type="primary" @click="noop">{{ labQuery }}</el-button>
|
||||
<el-button @click="resetKw">{{ labReset }}</el-button>
|
||||
</div>
|
||||
<el-alert type="info" :closable="false" show-icon class="mb12" :title="hint" />
|
||||
<el-table v-loading="loading" :data="filteredRows" border height="calc(100vh - 280px)">
|
||||
<el-table-column :label="colCode" prop="productCode" min-width="110" />
|
||||
<el-table-column :label="colName" prop="productName" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column :label="colLevel" prop="serviceLevel" width="90" align="center" />
|
||||
<el-table-column :label="colCargo" prop="cargoAttr" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column :label="colRegion" prop="destRegion" width="90" align="center" />
|
||||
<el-table-column :label="colAction" width="110" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click.stop="view(row)">{{ viewText }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="product-count">{{ countText }}</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
<script setup>
|
||||
const L = {
|
||||
title: "版本产品",
|
||||
hint: "点击「查看明细」查看国家/重量段明细",
|
||||
phKw: "产品代码/名称",
|
||||
colCode: "产品代码",
|
||||
colName: "产品名称",
|
||||
colLevel: "时效等级",
|
||||
colCargo: "走货属性",
|
||||
colRegion: "目的区域",
|
||||
colAction: "操作",
|
||||
view: "查看明细",
|
||||
countAll: "共 ",
|
||||
countUnit: " 个产品",
|
||||
countFilter: " 个产品(全部 ",
|
||||
countFilterEnd: " 个)"
|
||||
}
|
||||
const props = defineProps({
|
||||
listFn: { type: Function, required: true }
|
||||
})
|
||||
const emit = defineEmits(['view'])
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const rows = ref([])
|
||||
const version = ref(null)
|
||||
const keyword = ref('')
|
||||
const title = computed(() => {
|
||||
const no = version.value && version.value.versionNo ? version.value.versionNo : ''
|
||||
return no ? (L.title + ' ' + no) : L.title
|
||||
})
|
||||
const hint = L.hint
|
||||
const phKw = L.phKw
|
||||
const colCode = L.colCode
|
||||
const colName = L.colName
|
||||
const colLevel = L.colLevel
|
||||
const colCargo = L.colCargo
|
||||
const colRegion = L.colRegion
|
||||
const colAction = L.colAction
|
||||
const viewText = L.view
|
||||
const labQuery = "查询"
|
||||
const labReset = "重置"
|
||||
const filteredRows = computed(() => {
|
||||
const kw = (keyword.value || '').trim().toLowerCase()
|
||||
if (!kw) return rows.value || []
|
||||
return (rows.value || []).filter(r => {
|
||||
const blob = [r.productCode, r.productName].filter(Boolean).join(' ').toLowerCase()
|
||||
return blob.includes(kw)
|
||||
})
|
||||
})
|
||||
const countText = computed(() => {
|
||||
const n = filteredRows.value.length
|
||||
const total = (rows.value || []).length
|
||||
if (n === total) return L.countAll + n + L.countUnit
|
||||
return L.countAll + n + L.countFilter + total + L.countFilterEnd
|
||||
})
|
||||
function noop() {}
|
||||
function resetKw() {
|
||||
keyword.value = ''
|
||||
}
|
||||
function view(row) {
|
||||
emit('view', row)
|
||||
}
|
||||
function open(ver) {
|
||||
version.value = ver
|
||||
visible.value = true
|
||||
rows.value = []
|
||||
keyword.value = ''
|
||||
if (!ver || ver.psvId == null || !props.listFn) return
|
||||
loading.value = true
|
||||
props.listFn({ psvId: ver.psvId, pageNum: 1, pageSize: 500 }).then(res => {
|
||||
rows.value = res.rows || res.data || []
|
||||
}).finally(() => { loading.value = false })
|
||||
}
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
<style scoped>
|
||||
.mb12 { margin-bottom: 12px; }
|
||||
.toolbar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 12px; }
|
||||
.product-count { margin-top: 10px; color: #909399; font-size: 13px; text-align: right; }
|
||||
</style>
|
||||
@@ -160,13 +160,21 @@ function load() {
|
||||
})
|
||||
}
|
||||
|
||||
function open(row) {
|
||||
function open(row, previewList) {
|
||||
title.value = row.productCode + ' · ' + row.productName + ' · 国家服务明细'
|
||||
currentProduct.value = row
|
||||
visible.value = true
|
||||
list.value = []
|
||||
keyword.value = ''
|
||||
status.value = ''
|
||||
if (Array.isArray(previewList)) {
|
||||
list.value = previewList.slice().sort((a, b) => {
|
||||
const c = String(a.countryCode || '').localeCompare(String(b.countryCode || ''))
|
||||
if (c !== 0) return c
|
||||
return String(a.zoneCode || '').localeCompare(String(b.zoneCode || ''))
|
||||
})
|
||||
return
|
||||
}
|
||||
load()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
<template>
|
||||
<el-drawer v-model="visible" size="860px" :title="title" append-to-body>
|
||||
<el-table v-loading="loading" :data="list" border max-height="calc(100vh - 160px)" empty-text="该产品暂无导入附件">
|
||||
<el-table-column label="文件名称" prop="fileName" min-width="220" show-overflow-tooltip />
|
||||
<el-table-column label="生效时间" prop="effectiveTime" width="170" align="center" />
|
||||
<el-table-column label="大小" width="100" align="right">
|
||||
<template #default="{ row }">{{ formatSize(row.fileSize) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="上传人" prop="createBy" width="110" align="center" show-overflow-tooltip />
|
||||
<el-table-column label="上传时间" prop="createTime" width="170" align="center" />
|
||||
<el-table-column label="操作" width="90" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" :disabled="!row.filePath" @click="handleDownload(row)">下载</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { listProductExcelFiles } from '@/api/price/product'
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
|
||||
const visible = ref(false)
|
||||
const title = ref('')
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const product = ref(null)
|
||||
|
||||
function open(row) {
|
||||
product.value = row
|
||||
title.value = (row.productCode || '') + ' · ' + (row.productName || '') + ' · 导入附件'
|
||||
visible.value = true
|
||||
loadList()
|
||||
}
|
||||
|
||||
function loadList() {
|
||||
if (!product.value || !product.value.productCode) return
|
||||
loading.value = true
|
||||
listProductExcelFiles(product.value.productCode).then(res => {
|
||||
list.value = res.data || []
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function formatSize(bytes) {
|
||||
const n = Number(bytes)
|
||||
if (!n || n < 0) return '-'
|
||||
if (n < 1024) return n + ' B'
|
||||
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KB'
|
||||
return (n / (1024 * 1024)).toFixed(2) + ' MB'
|
||||
}
|
||||
|
||||
function handleDownload(row) {
|
||||
if (!row || !row.filePath) {
|
||||
proxy.$modal.msgWarning('文件路径不存在')
|
||||
return
|
||||
}
|
||||
proxy.$download.resource(row.filePath)
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
@@ -1,148 +0,0 @@
|
||||
<template>
|
||||
<el-drawer v-model="visible" size="960px" :title="title" append-to-body>
|
||||
<el-form :inline="true" @submit.prevent>
|
||||
<el-form-item label="产品代码">
|
||||
<el-input
|
||||
v-model="queryParams.productCode"
|
||||
placeholder="产品代码"
|
||||
clearable
|
||||
style="width: 160px"
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="产品名称">
|
||||
<el-input
|
||||
v-model="queryParams.productName"
|
||||
placeholder="产品名称"
|
||||
clearable
|
||||
style="width: 160px"
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="失效时间段">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
value-format="YYYY-MM-DD"
|
||||
range-separator="-"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
style="width: 260px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-table v-loading="loading" :data="list" border max-height="calc(100vh - 260px)" empty-text="暂无失效产品">
|
||||
<el-table-column label="产品代码" prop="productCode" min-width="110" show-overflow-tooltip />
|
||||
<el-table-column label="产品名称" prop="productName" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="时效等级" prop="serviceLevel" width="90" align="center" />
|
||||
<el-table-column label="走货属性" prop="cargoAttr" min-width="110" show-overflow-tooltip />
|
||||
<el-table-column label="目的区域" prop="destRegion" width="90" align="center" />
|
||||
<el-table-column label="生效日期" prop="effectiveDate" width="120" align="center" />
|
||||
<el-table-column label="失效日期" prop="expireDate" width="120" align="center" />
|
||||
<el-table-column label="国家数" prop="countryCount" width="80" align="center" />
|
||||
<el-table-column label="附加费数" prop="surchargeCount" width="90" align="center" />
|
||||
<el-table-column label="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<dict-tag :options="bms_product_effect_status" :value="row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="更新内容" prop="updateNote" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="120" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
title="价格"
|
||||
icon="Document"
|
||||
@click="excelFileRef.open(row)"
|
||||
v-hasPermi="['price:product:query']"
|
||||
></el-button>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
title="产品明细"
|
||||
icon="View"
|
||||
@click="emit('view-country', row)"
|
||||
v-hasPermi="['price:product:query']"
|
||||
></el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination
|
||||
v-show="total > 0"
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNum"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="loadList"
|
||||
/>
|
||||
<product-excel-file-drawer ref="excelFileRef" />
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { listProduct } from '@/api/price/product'
|
||||
import ProductExcelFileDrawer from './ProductExcelFileDrawer.vue'
|
||||
|
||||
const emit = defineEmits(['view-country'])
|
||||
const { proxy } = getCurrentInstance()
|
||||
const { bms_product_effect_status } = useDict('bms_product_effect_status')
|
||||
|
||||
const excelFileRef = ref()
|
||||
const title = '失效价格'
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const total = ref(0)
|
||||
const dateRange = ref([])
|
||||
const queryParams = ref({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
productCode: undefined,
|
||||
productName: undefined,
|
||||
statusGroup: 'expired'
|
||||
})
|
||||
|
||||
function open() {
|
||||
visible.value = true
|
||||
resetQuery()
|
||||
}
|
||||
|
||||
function loadList() {
|
||||
loading.value = true
|
||||
const p = { ...queryParams.value }
|
||||
if (dateRange.value && dateRange.value.length === 2) {
|
||||
proxy.addDateRange(p, dateRange.value)
|
||||
}
|
||||
listProduct(p).then(res => {
|
||||
list.value = res.rows || []
|
||||
total.value = res.total || 0
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function handleQuery() {
|
||||
queryParams.value.pageNum = 1
|
||||
loadList()
|
||||
}
|
||||
|
||||
function resetQuery() {
|
||||
dateRange.value = []
|
||||
queryParams.value = {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
productCode: undefined,
|
||||
productName: undefined,
|
||||
statusGroup: 'expired'
|
||||
}
|
||||
loadList()
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
@@ -1,167 +0,0 @@
|
||||
<template>
|
||||
<el-dialog :title="title" v-model="visible" width="640px" append-to-body>
|
||||
<basic-form ref="formRef" v-model="form" :schemas="schemas" label-width="100px" />
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="visible = false">取 消</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import BasicForm from '@/components/BasicForm'
|
||||
import { getProduct, addProduct, updateProduct } from '@/api/price/product'
|
||||
|
||||
const emit = defineEmits(['success'])
|
||||
const { proxy } = getCurrentInstance()
|
||||
|
||||
const formRef = ref()
|
||||
const visible = ref(false)
|
||||
const title = ref('')
|
||||
const form = ref(emptyForm())
|
||||
|
||||
const schemas = computed(() => [
|
||||
{
|
||||
label: '产品代码',
|
||||
prop: 'productCode',
|
||||
component: 'input',
|
||||
placeholder: '如 WSP037',
|
||||
span: 12,
|
||||
width: '100%',
|
||||
disabled: !!form.value.productId,
|
||||
props: { maxlength: 20 },
|
||||
rules: [{ required: true, message: '产品代码不能为空', trigger: 'blur' }]
|
||||
},
|
||||
{
|
||||
label: '产品名称',
|
||||
prop: 'productName',
|
||||
component: 'input',
|
||||
placeholder: '产品名称',
|
||||
span: 12,
|
||||
width: '100%',
|
||||
props: { maxlength: 100 },
|
||||
rules: [{ required: true, message: '产品名称不能为空', trigger: 'blur' }]
|
||||
},
|
||||
{
|
||||
label: '时效等级',
|
||||
prop: 'serviceLevel',
|
||||
component: 'input',
|
||||
placeholder: '如 标准',
|
||||
span: 12,
|
||||
width: '100%',
|
||||
props: { maxlength: 20 }
|
||||
},
|
||||
{
|
||||
label: '走货属性',
|
||||
prop: 'cargoAttr',
|
||||
component: 'input',
|
||||
placeholder: '普货/带电等',
|
||||
span: 12,
|
||||
width: '100%',
|
||||
props: { maxlength: 100 }
|
||||
},
|
||||
{
|
||||
label: '目的区域',
|
||||
prop: 'destRegion',
|
||||
component: 'input',
|
||||
placeholder: '如 欧盟',
|
||||
span: 12,
|
||||
width: '100%',
|
||||
props: { maxlength: 50 }
|
||||
},
|
||||
// {
|
||||
// label: '税模式',
|
||||
// prop: 'taxMode',
|
||||
// component: 'select',
|
||||
// placeholder: '请选择',
|
||||
// dict: 'bms_tax_mode',
|
||||
// span: 12,
|
||||
// width: '100%',
|
||||
// rules: [{ required: true, message: '请选择税模式', trigger: 'change' }]
|
||||
// },
|
||||
{
|
||||
label: '生效时间',
|
||||
prop: 'effectiveDate',
|
||||
component: 'datetime',
|
||||
placeholder: '选择时间',
|
||||
span: 12,
|
||||
width: '100%',
|
||||
rules: [{ required: true, message: '生效时间不能为空', trigger: 'change' }]
|
||||
},
|
||||
{
|
||||
label: '失效时间',
|
||||
prop: 'expireDate',
|
||||
component: 'datetime',
|
||||
placeholder: '选择时间',
|
||||
span: 12,
|
||||
width: '100%'
|
||||
},
|
||||
{
|
||||
label: '更新内容',
|
||||
prop: 'updateNote',
|
||||
component: 'textarea',
|
||||
span: 24,
|
||||
width: '100%',
|
||||
props: { rows: 2, maxlength: 500 }
|
||||
},
|
||||
{
|
||||
label: '备注',
|
||||
prop: 'remark',
|
||||
component: 'textarea',
|
||||
span: 24,
|
||||
width: '100%',
|
||||
props: { rows: 2, maxlength: 500 }
|
||||
}
|
||||
])
|
||||
|
||||
function emptyForm() {
|
||||
return {
|
||||
productId: undefined,
|
||||
productCode: undefined,
|
||||
productName: undefined,
|
||||
serviceLevel: undefined,
|
||||
cargoAttr: undefined,
|
||||
destRegion: undefined,
|
||||
updateNote: undefined,
|
||||
taxMode: 'TAX_INCLUDED',
|
||||
effectiveDate: undefined,
|
||||
expireDate: undefined,
|
||||
status: '0',
|
||||
remark: undefined
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
form.value = emptyForm()
|
||||
nextTick(() => formRef.value?.clearValidate())
|
||||
}
|
||||
|
||||
function open(row) {
|
||||
reset()
|
||||
if (!row) {
|
||||
title.value = '新建产品'
|
||||
visible.value = true
|
||||
return
|
||||
}
|
||||
getProduct(row.productId).then(res => {
|
||||
form.value = { ...emptyForm(), ...(res.data || {}) }
|
||||
title.value = '修改产品'
|
||||
visible.value = true
|
||||
nextTick(() => formRef.value?.clearValidate())
|
||||
})
|
||||
}
|
||||
|
||||
function submitForm() {
|
||||
formRef.value.validate(valid => {
|
||||
if (!valid) return
|
||||
const req = form.value.productId ? updateProduct(form.value) : addProduct(form.value)
|
||||
req.then(() => {
|
||||
proxy.$modal.msgSuccess('操作成功')
|
||||
visible.value = false
|
||||
emit('success')
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<catalog-price-create
|
||||
kind="product"
|
||||
list-path="/price/product"
|
||||
:list-fn="listProduct"
|
||||
:del-fn="delProduct"
|
||||
:prepare-fn="prepareProductVersion"
|
||||
:apply-preview-fn="applyProductCatalogPreview"
|
||||
:attach-excel-fn="attachProductVersionExcel"
|
||||
:import-all-fn="importProductExcelAsync"
|
||||
:import-country-fn="importProductCountryExcelAsync"
|
||||
:preview-all-fn="previewProductImport"
|
||||
:preview-country-fn="previewProductCountryImport"
|
||||
:country-comp="ProductCountryDrawer"
|
||||
:surcharge-comp="ProductSurchargeDrawer"
|
||||
/>
|
||||
</template>
|
||||
<script setup name="ProductPriceCreate">
|
||||
import CatalogPriceCreate from '../components/CatalogPriceCreate.vue'
|
||||
import ProductCountryDrawer from './components/ProductCountryDrawer.vue'
|
||||
import ProductSurchargeDrawer from './components/ProductSurchargeDrawer.vue'
|
||||
import {
|
||||
listProduct, delProduct, prepareProductVersion, applyProductCatalogPreview, attachProductVersionExcel,
|
||||
importProductExcelAsync, importProductCountryExcelAsync,
|
||||
previewProductImport, previewProductCountryImport
|
||||
} from '@/api/price/product'
|
||||
</script>
|
||||
@@ -5,9 +5,16 @@
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="mb16"
|
||||
title="导入前须先选择生效时间;产品与国家明细均按此时间落库,上一版产品/明细失效时间设为该时间。多国家价表用「全部导入」,单国专线成本价用「按国家导入」。相同产品代码+生效日期不覆盖主数据。"
|
||||
title="已生效展示当前生效版本产品。待生效、已失效按版本展示;待生效可启动或删除整版。建价请点「新建产品价格」。"
|
||||
/>
|
||||
|
||||
<el-tabs v-model="statusTab" class="catalog-tabs" @tab-change="onTabChange">
|
||||
<el-tab-pane label="已生效" name="effect" />
|
||||
<el-tab-pane label="待生效" name="pending" />
|
||||
<el-tab-pane label="已失效" name="expired" />
|
||||
</el-tabs>
|
||||
|
||||
<div v-show="statusTab === 'effect'">
|
||||
<basic-table
|
||||
ref="tableRef"
|
||||
:extra-params="extraParams"
|
||||
@@ -15,37 +22,11 @@
|
||||
:search-columns="searchColumns"
|
||||
:query="queryList"
|
||||
row-key="productId"
|
||||
:selection="true"
|
||||
:selection="false"
|
||||
>
|
||||
<template #toolbar="{ selection }">
|
||||
<template #toolbar>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['price:product:add']">新建产品</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-dropdown @command="handleImportCommand">
|
||||
<el-button type="info" plain icon="Upload" v-hasPermi="['price:product:import']">
|
||||
导入 Excel<el-icon class="el-icon--right"><arrow-down /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="all">全部导入(多国家价表)</el-dropdown-item>
|
||||
<el-dropdown-item command="country">按国家导入(单国专线成本价)</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="danger"
|
||||
plain
|
||||
icon="Delete"
|
||||
:disabled="!selection.length"
|
||||
@click="handleBatchDelete(selection)"
|
||||
v-hasPermi="['price:product:remove']"
|
||||
>批量删除</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button plain icon="Clock" @click="expiredRef.open()" v-hasPermi="['price:product:list']">失效价格</el-button>
|
||||
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['price:product:add']">新建产品价格</el-button>
|
||||
</el-col>
|
||||
</template>
|
||||
|
||||
@@ -58,167 +39,104 @@
|
||||
</template>
|
||||
|
||||
<template #col-action="{ row }">
|
||||
<el-button link type="primary" title="价格" icon="Document" @click="excelFileRef.open(row)" v-hasPermi="['price:product:query']"></el-button>
|
||||
<el-button link type="primary" title="产品明细" icon="View" @click="countryRef.open(row)" v-hasPermi="['price:product:query']"></el-button>
|
||||
<el-button link type="primary" title="附加费" icon="Money" @click="surchargeRef.open(row)" v-hasPermi="['price:surcharge:query']"></el-button>
|
||||
<el-button title="编辑" link type="primary" icon="Edit" @click="handleUpdate(row)" v-hasPermi="['price:product:edit']"></el-button>
|
||||
</template>
|
||||
</basic-table>
|
||||
</div>
|
||||
|
||||
<product-form-dialog ref="formRef" @success="() => tableRef.refresh()" />
|
||||
<product-excel-file-drawer ref="excelFileRef" />
|
||||
<div v-show="statusTab === 'pending'">
|
||||
<el-row class="mb12">
|
||||
<el-col :span="1.5">
|
||||
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['price:product:add']">新建产品价格</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-table v-loading="versionLoading" :data="versionRows" border empty-text="暂无待生效版本" @row-click="openVersionProducts">
|
||||
<el-table-column label="版本号" prop="versionNo" width="120" />
|
||||
<el-table-column label="生效时间" prop="effectiveTime" min-width="170" />
|
||||
<el-table-column label="失效时间" prop="expireTime" min-width="170" />
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{ row }">{{ versionStatusText(row.status) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="产品数" prop="productCount" width="90" align="center" />
|
||||
<el-table-column label="明细行数" prop="detailCount" width="100" align="center" />
|
||||
<el-table-column label="操作" width="360" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click.stop="onEditVersion(row)" v-hasPermi="['price:product:edit']">编辑</el-button>
|
||||
<el-button link type="primary" @click.stop="openVersionProducts(row)">查看产品</el-button>
|
||||
<el-button link type="primary" @click.stop="openExcel(row)" v-hasPermi="['price:product:query']">查看附件</el-button>
|
||||
<el-button link type="success" @click.stop="onStartVersion(row)" v-hasPermi="['price:product:edit']">启动</el-button>
|
||||
<el-button link type="danger" @click.stop="onDeleteVersion(row)" v-hasPermi="['price:product:remove']">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination
|
||||
v-show="versionTotal > 0"
|
||||
:total="versionTotal"
|
||||
v-model:page="versionQuery.pageNum"
|
||||
v-model:limit="versionQuery.pageSize"
|
||||
@pagination="loadVersions"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-show="statusTab === 'expired'">
|
||||
<el-table v-loading="versionLoading" :data="versionRows" border empty-text="暂无失效版本" @row-click="openVersionProducts">
|
||||
<el-table-column label="版本号" prop="versionNo" width="120" />
|
||||
<el-table-column label="生效时间" prop="effectiveTime" min-width="170" />
|
||||
<el-table-column label="失效时间" prop="expireTime" min-width="170" />
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{ row }">{{ versionStatusText(row.status) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="产品数" prop="productCount" width="90" align="center" />
|
||||
<el-table-column label="明细行数" prop="detailCount" width="100" align="center" />
|
||||
<el-table-column label="操作" width="220" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click.stop="openVersionProducts(row)">查看产品</el-button>
|
||||
<el-button link type="primary" @click.stop="openExcel(row)" v-hasPermi="['price:product:query']">查看附件</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination
|
||||
v-show="versionTotal > 0"
|
||||
:total="versionTotal"
|
||||
v-model:page="versionQuery.pageNum"
|
||||
v-model:limit="versionQuery.pageSize"
|
||||
@pagination="loadVersions"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<catalog-version-excel-drawer ref="excelRef" :list-fn="listProductVersionExcelFiles" />
|
||||
<product-country-drawer ref="countryRef" />
|
||||
<product-surcharge-drawer ref="surchargeRef" @success="() => tableRef.refresh()" />
|
||||
<product-expired-drawer ref="expiredRef" @view-country="(row) => countryRef.open(row)" />
|
||||
<excel-import-dialog
|
||||
ref="importRef"
|
||||
title="产品 Excel 导入"
|
||||
width="560px"
|
||||
:show-update-support="false"
|
||||
template-url="/templates/product/product-import-all-template.xlsx"
|
||||
template-file-name="产品多国家价表导入模板"
|
||||
:import-desc="allImportDesc"
|
||||
:before-submit="validateImportEffectiveDate"
|
||||
:submit-handler="runImportAllAsync"
|
||||
@success="() => tableRef.refresh()"
|
||||
>
|
||||
<template #extra>
|
||||
<el-form label-width="90px">
|
||||
<el-form-item label="生效时间" required>
|
||||
<el-date-picker
|
||||
v-model="importEffectiveDate"
|
||||
type="datetime"
|
||||
format="YYYY-MM-DD HH:00"
|
||||
value-format="YYYY-MM-DD HH:00:00"
|
||||
popper-class="product-import-hour-picker"
|
||||
placeholder="请选择生效时间"
|
||||
style="width: 100%"
|
||||
@change="onImportEffectiveTimeChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
</excel-import-dialog>
|
||||
<excel-import-dialog
|
||||
ref="countryImportRef"
|
||||
title="按国家导入"
|
||||
width="560px"
|
||||
:show-update-support="false"
|
||||
:before-submit="validateCountryImport"
|
||||
:import-desc="countryImportDesc"
|
||||
template-url="/templates/product/product-import-country-template.xlsx"
|
||||
template-file-name="产品按国家导入模板"
|
||||
:submit-handler="runImportCountryAsync"
|
||||
@success="() => tableRef.refresh()"
|
||||
>
|
||||
<template #extra>
|
||||
<el-form label-width="90px">
|
||||
<el-form-item label="生效时间" required>
|
||||
<el-date-picker
|
||||
v-model="importEffectiveDate"
|
||||
type="datetime"
|
||||
format="YYYY-MM-DD HH:00"
|
||||
value-format="YYYY-MM-DD HH:00:00"
|
||||
popper-class="product-import-hour-picker"
|
||||
placeholder="请选择生效时间"
|
||||
style="width: 100%"
|
||||
@change="onImportEffectiveTimeChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="目的国家" required>
|
||||
<el-select v-model="importCountryCode" filterable clearable placeholder="请选择国家" style="width: 100%">
|
||||
<el-option
|
||||
v-for="item in regionOptions"
|
||||
:key="item.countryCode2"
|
||||
:label="regionLabel(item)"
|
||||
:value="item.countryCode2"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
</excel-import-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="importProgressVisible"
|
||||
title="正在导入产品"
|
||||
width="420px"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
:show-close="false"
|
||||
append-to-body
|
||||
>
|
||||
<div class="import-progress">
|
||||
<el-progress :percentage="importProgressPercent" :status="importProgressStatus" />
|
||||
<div class="import-progress-msg">{{ importProgressMessage }}</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
<catalog-version-products-drawer ref="versionProductsRef" :list-fn="listProduct" @view="(row) => countryRef.open(row)" />
|
||||
<catalog-activate-dialog ref="activateRef" :load-versions="loadAllVersions" :activate-fn="activateProductVersion" @success="onActivated" />
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="PriceProduct">
|
||||
import ExcelImportDialog from '@/components/ExcelImportDialog'
|
||||
import ProductFormDialog from './components/ProductFormDialog.vue'
|
||||
import ProductExcelFileDrawer from './components/ProductExcelFileDrawer.vue'
|
||||
import ProductCountryDrawer from './components/ProductCountryDrawer.vue'
|
||||
import ProductSurchargeDrawer from './components/ProductSurchargeDrawer.vue'
|
||||
import ProductExpiredDrawer from './components/ProductExpiredDrawer.vue'
|
||||
import { listProduct, delProduct, importProductExcelAsync, importProductCountryExcelAsync } from '@/api/price/product'
|
||||
import { listRegionOptions } from '@/api/data/region'
|
||||
import CatalogVersionProductsDrawer from '../components/CatalogVersionProductsDrawer.vue'
|
||||
import CatalogVersionExcelDrawer from '../components/CatalogVersionExcelDrawer.vue'
|
||||
import CatalogActivateDialog from '../components/CatalogActivateDialog.vue'
|
||||
import { listProduct, listProductVersions, activateProductVersion, deleteProductVersion, listProductVersionExcelFiles } from '@/api/price/product'
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const { bms_product_effect_status } = useDict('bms_product_effect_status')
|
||||
|
||||
const extraParams = { statusGroup: 'active' }
|
||||
const statusTab = ref('effect')
|
||||
const extraParams = reactive({ statusGroup: 'effect' })
|
||||
const versionProductsRef = ref()
|
||||
const activateRef = ref()
|
||||
const versionLoading = ref(false)
|
||||
const versionRows = ref([])
|
||||
const versionTotal = ref(0)
|
||||
const versionQuery = reactive({ pageNum: 1, pageSize: 10 })
|
||||
const tableRef = ref()
|
||||
const formRef = ref()
|
||||
const excelFileRef = ref()
|
||||
const countryRef = ref()
|
||||
const surchargeRef = ref()
|
||||
const expiredRef = ref()
|
||||
const importRef = ref()
|
||||
const countryImportRef = ref()
|
||||
const importCountryCode = ref('')
|
||||
const importEffectiveDate = ref('')
|
||||
|
||||
function pad2(n) {
|
||||
return String(n).padStart(2, '0')
|
||||
}
|
||||
|
||||
function formatImportHour(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 normalizeImportToHour(val) {
|
||||
if (val == null || val === '') return val
|
||||
if (val instanceof Date) return formatImportHour(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'
|
||||
if (/^\d{4}-\d{2}-\d{2}$/.test(s)) return s + ' 00:00:00'
|
||||
return formatImportHour(new Date(s.replace(/-/g, '/'))) || val
|
||||
}
|
||||
|
||||
function onImportEffectiveTimeChange(val) {
|
||||
importEffectiveDate.value = normalizeImportToHour(val) || ''
|
||||
}
|
||||
const regionOptions = ref([])
|
||||
const importProgressVisible = ref(false)
|
||||
const importProgressPercent = ref(0)
|
||||
const importProgressMessage = ref('')
|
||||
const importProgressStatus = ref(undefined)
|
||||
const allImportDesc = [
|
||||
'适用多国家价表(B2C 小包价表等),按目录 sheet 匹配国家导入。',
|
||||
'必选生效时间:产品与 product_country 明细均按此时间计费;上一版产品/明细失效时间设为该时间。',
|
||||
'请在弹窗底部下载模板,按模板格式整理后上传。相同产品代码+生效日期不覆盖主数据。'
|
||||
]
|
||||
const countryImportDesc = [
|
||||
'选择目的国家后上传专线成本价表,目录中的产品都会记到该国家。',
|
||||
'必选生效时间:产品与 product_country 明细均按此时间计费;上一版产品/明细失效时间设为该时间。',
|
||||
'邮编、赔偿标准等 sheet 不导入。可在弹窗底部下载模板。'
|
||||
]
|
||||
const excelRef = ref()
|
||||
|
||||
const columns = [
|
||||
{ prop: 'productCode', label: '产品代码', minWidth: 110 },
|
||||
@@ -232,20 +150,12 @@ const columns = [
|
||||
{ prop: 'surchargeCount', label: '附加费数', width: 90, align: 'center' },
|
||||
{ prop: 'status', label: '状态', width: 90, align: 'center', slot: 'status' },
|
||||
{ prop: 'updateNote', label: '更新内容', minWidth: 160, showOverflowTooltip: true },
|
||||
{ prop: 'action', label: '操作', width: 240, fixed: 'right', slot: 'action' }
|
||||
{ prop: 'action', label: '操作', width: 120, fixed: 'right', slot: 'action' }
|
||||
]
|
||||
|
||||
const searchColumns = [
|
||||
{ prop: 'productCode', label: '产品代码', component: 'input', placeholder: '产品代码' },
|
||||
{ prop: 'productName', label: '产品名称', component: 'input', placeholder: '产品名称' },
|
||||
{
|
||||
prop: 'status',
|
||||
label: '状态',
|
||||
component: 'select',
|
||||
dict: 'bms_product_effect_status',
|
||||
placeholder: '请选择状态',
|
||||
width: '160px'
|
||||
}
|
||||
{ prop: 'productName', label: '产品名称', component: 'input', placeholder: '产品名称' }
|
||||
]
|
||||
|
||||
function queryList(params) {
|
||||
@@ -258,177 +168,102 @@ function queryList(params) {
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
formRef.value.open()
|
||||
proxy.$router.push({ path: '/price/product-create/index' })
|
||||
}
|
||||
|
||||
function handleUpdate(row) {
|
||||
formRef.value.open(row)
|
||||
}
|
||||
|
||||
function isPendingProduct(row) {
|
||||
if (!row) return false
|
||||
if (String(row.status) === '0') return true
|
||||
// fallback: effective in future and not expired
|
||||
if (!row.effectiveDate) return false
|
||||
const eff = new Date(String(row.effectiveDate).replace(/-/g, '/')).getTime()
|
||||
if (Number.isNaN(eff) || eff <= Date.now()) return false
|
||||
if (row.expireDate) {
|
||||
const exp = new Date(String(row.expireDate).replace(/-/g, '/')).getTime()
|
||||
if (!Number.isNaN(exp) && exp <= Date.now()) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function handleBatchDelete(selection) {
|
||||
const rows = (selection || []).filter(r => r && r.productId != null)
|
||||
if (!rows.length) {
|
||||
proxy.$modal.msgWarning('请先勾选产品')
|
||||
function openExcel(row) {
|
||||
if (!row || row.psvId == null) {
|
||||
proxy.$modal.msgWarning('该行没有版本,无法查看附件')
|
||||
return
|
||||
}
|
||||
const pending = rows.filter(isPendingProduct)
|
||||
const blocked = rows.filter(r => !isPendingProduct(r))
|
||||
if (!pending.length) {
|
||||
proxy.$modal.msgWarning('仅待生效产品可删除,当前勾选无待生效记录')
|
||||
excelRef.value && excelRef.value.open(row)
|
||||
}
|
||||
|
||||
function onTabChange(name) {
|
||||
if (name === 'pending' || name === 'expired') {
|
||||
versionQuery.pageNum = 1
|
||||
loadVersions()
|
||||
return
|
||||
}
|
||||
if (blocked.length) {
|
||||
const tip = blocked.map(r => r.productCode || r.productId).join('、')
|
||||
proxy.$modal.msgWarning('已跳过非待生效产品:' + tip)
|
||||
extraParams.statusGroup = 'effect'
|
||||
nextTick(() => tableRef.value && tableRef.value.refresh())
|
||||
}
|
||||
|
||||
function versionStatusText(status) {
|
||||
const map = { '0': '草稿', '1': '已生效', '2': '已失效', '3': '待生效' }
|
||||
return map[String(status)] || status || '-'
|
||||
}
|
||||
|
||||
function loadVersions() {
|
||||
versionLoading.value = true
|
||||
const params = {
|
||||
pageNum: versionQuery.pageNum,
|
||||
pageSize: versionQuery.pageSize
|
||||
}
|
||||
const codes = pending.map(r => r.productCode || r.productId).join('、')
|
||||
const ids = pending.map(r => r.productId)
|
||||
proxy.$modal.confirm(
|
||||
'确认删除已选待生效产品(' + pending.length + '个):'
|
||||
+ codes
|
||||
+ '?删除后产品及国家/重量段明细将一并清除,且不可恢复。'
|
||||
).then(() => delProduct(ids)).then(() => {
|
||||
tableRef.value.refresh()
|
||||
if (statusTab.value === 'pending') params.statusGroup = 'pending'
|
||||
else params.status = '2'
|
||||
listProductVersions(params).then(res => {
|
||||
versionRows.value = res.rows || []
|
||||
versionTotal.value = res.total || 0
|
||||
}).finally(() => { versionLoading.value = false })
|
||||
}
|
||||
|
||||
function loadAllVersions() {
|
||||
return listProductVersions({ pageNum: 1, pageSize: 500 }).then(res => res.rows || [])
|
||||
}
|
||||
|
||||
function openVersionProducts(row) {
|
||||
versionProductsRef.value && versionProductsRef.value.open(row)
|
||||
}
|
||||
|
||||
function onEditVersion(row) {
|
||||
if (!row || row.psvId == null) return
|
||||
proxy.$router.push({
|
||||
path: '/price/product-create/index',
|
||||
query: {
|
||||
psvId: row.psvId,
|
||||
versionNo: row.versionNo || undefined
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function onStartVersion(row) {
|
||||
if (row && row.psvId != null) {
|
||||
activateRef.value && activateRef.value.open(row)
|
||||
return
|
||||
}
|
||||
loadAllVersions().then(list => {
|
||||
const pending = (list || []).filter(v => v && (String(v.status) === '0' || String(v.status) === '3'))
|
||||
if (!pending.length) {
|
||||
proxy.$modal.msgWarning('当前没有可启动的待生效版本')
|
||||
return
|
||||
}
|
||||
pending.sort((a, b) => (Number(b.versionSeq) || 0) - (Number(a.versionSeq) || 0))
|
||||
const draft = pending.find(v => String(v.status) === '0')
|
||||
activateRef.value && activateRef.value.open(draft || pending[0])
|
||||
})
|
||||
}
|
||||
|
||||
function onDeleteVersion(row) {
|
||||
if (!row || row.psvId == null) return
|
||||
proxy.$modal.confirm('确认硬删除该待生效版本?版本、产品、明细将从数据库真删除,且不可恢复。').then(() => deleteProductVersion(row.psvId)).then(() => {
|
||||
loadVersions()
|
||||
proxy.$modal.msgSuccess('删除成功')
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
function handleImportCommand(command) {
|
||||
if (command === 'country') {
|
||||
handleCountryImport()
|
||||
return
|
||||
}
|
||||
handleImport()
|
||||
function onActivated() {
|
||||
if (statusTab.value === 'pending' || statusTab.value === 'expired') loadVersions()
|
||||
else if (tableRef.value) tableRef.value.refresh()
|
||||
}
|
||||
|
||||
function handleImport() {
|
||||
importEffectiveDate.value = ''
|
||||
importRef.value.open()
|
||||
}
|
||||
|
||||
function regionLabel(item) {
|
||||
const name = item.countryNameCn || item.countryNameEn || ''
|
||||
const code = item.countryCode2 || ''
|
||||
return name && code ? name + ' (' + code + ')' : (name || code)
|
||||
}
|
||||
|
||||
function loadRegionOptions() {
|
||||
if (regionOptions.value.length) return
|
||||
listRegionOptions().then(res => {
|
||||
regionOptions.value = res.data || []
|
||||
})
|
||||
}
|
||||
|
||||
function handleCountryImport() {
|
||||
importCountryCode.value = ''
|
||||
importEffectiveDate.value = ''
|
||||
loadRegionOptions()
|
||||
countryImportRef.value.open()
|
||||
}
|
||||
|
||||
function validateImportEffectiveDate() {
|
||||
if (!importEffectiveDate.value) {
|
||||
return '请先选择生效时间'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function validateCountryImport() {
|
||||
const effErr = validateImportEffectiveDate()
|
||||
if (effErr) return effErr
|
||||
if (!importCountryCode.value) {
|
||||
return '请先选择目的国家'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function applyImportProgress(t) {
|
||||
importProgressPercent.value = Math.min(100, Number(t.percent) || 0)
|
||||
if (t.status === 'PENDING') importProgressMessage.value = '排队中…'
|
||||
else if (t.status === 'RUNNING') {
|
||||
const m = t.message || ''
|
||||
if (m === 'parsing excel') importProgressMessage.value = '正在解析价表…'
|
||||
else if (m === 'finishing') importProgressMessage.value = '收尾中…'
|
||||
else if (m === 'queued') importProgressMessage.value = '排队中…'
|
||||
else if (m.indexOf('importing ') === 0) {
|
||||
const rest = m.slice('importing '.length)
|
||||
importProgressMessage.value = '正在导入产品 ' + rest
|
||||
} else importProgressMessage.value = m || '正在导入…'
|
||||
} else if (t.status === 'SUCCESS') {
|
||||
importProgressMessage.value = t.message || '导入完成'
|
||||
importProgressStatus.value = 'success'
|
||||
} else if (t.status === 'FAIL') {
|
||||
importProgressMessage.value = t.error || t.message || '导入失败'
|
||||
importProgressStatus.value = 'exception'
|
||||
}
|
||||
}
|
||||
|
||||
async function runImportWithProgress(runner) {
|
||||
importProgressVisible.value = true
|
||||
importProgressPercent.value = 0
|
||||
importProgressMessage.value = '上传并解析中…'
|
||||
importProgressStatus.value = undefined
|
||||
try {
|
||||
const { task } = await runner(applyImportProgress)
|
||||
importProgressPercent.value = 100
|
||||
importProgressStatus.value = 'success'
|
||||
return (task && task.message) ? task.message : '导入完成'
|
||||
} catch (e) {
|
||||
importProgressStatus.value = 'exception'
|
||||
const tip = (e && e.task && e.task.error) || (e && e.message) || '导入失败'
|
||||
importProgressMessage.value = tip
|
||||
await new Promise(resolve => setTimeout(resolve, 600))
|
||||
throw e
|
||||
} finally {
|
||||
importProgressVisible.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function runImportAllAsync(uploadFile) {
|
||||
const raw = uploadFile && (uploadFile.raw || uploadFile)
|
||||
if (!raw) {
|
||||
throw new Error('请先选择文件')
|
||||
}
|
||||
if (!importEffectiveDate.value) {
|
||||
throw new Error('请先选择生效时间')
|
||||
}
|
||||
const eff = importEffectiveDate.value
|
||||
return runImportWithProgress((onProgress) => importProductExcelAsync(raw, eff, onProgress))
|
||||
}
|
||||
|
||||
async function runImportCountryAsync(uploadFile) {
|
||||
const raw = uploadFile && (uploadFile.raw || uploadFile)
|
||||
if (!raw) {
|
||||
throw new Error('请先选择文件')
|
||||
}
|
||||
if (!importEffectiveDate.value) {
|
||||
throw new Error('请先选择生效时间')
|
||||
}
|
||||
if (!importCountryCode.value) {
|
||||
throw new Error('请先选择目的国家')
|
||||
}
|
||||
const code = importCountryCode.value
|
||||
const eff = importEffectiveDate.value
|
||||
return runImportWithProgress((onProgress) => importProductCountryExcelAsync(raw, code, eff, onProgress))
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mb16 { margin-bottom: 16px; }
|
||||
.mb12 { margin-bottom: 12px; }
|
||||
.catalog-tabs { margin-bottom: 8px; }
|
||||
.import-progress { padding: 8px 4px 4px; }
|
||||
.import-progress-msg {
|
||||
margin-top: 12px;
|
||||
|
||||
@@ -160,13 +160,21 @@ function load() {
|
||||
})
|
||||
}
|
||||
|
||||
function open(row) {
|
||||
function open(row, previewList) {
|
||||
title.value = row.productCode + ' · ' + row.productName + ' · 国家服务明细'
|
||||
currentProduct.value = row
|
||||
visible.value = true
|
||||
list.value = []
|
||||
keyword.value = ''
|
||||
status.value = ''
|
||||
if (Array.isArray(previewList)) {
|
||||
list.value = previewList.slice().sort((a, b) => {
|
||||
const c = String(a.countryCode || '').localeCompare(String(b.countryCode || ''))
|
||||
if (c !== 0) return c
|
||||
return String(a.zoneCode || '').localeCompare(String(b.zoneCode || ''))
|
||||
})
|
||||
return
|
||||
}
|
||||
load()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
<template>
|
||||
<el-drawer v-model="visible" size="860px" :title="title" append-to-body>
|
||||
<el-table v-loading="loading" :data="list" border max-height="calc(100vh - 160px)" empty-text="该产品暂无导入附件">
|
||||
<el-table-column label="文件名称" prop="fileName" min-width="220" show-overflow-tooltip />
|
||||
<el-table-column label="生效时间" prop="effectiveTime" width="170" align="center" />
|
||||
<el-table-column label="大小" width="100" align="right">
|
||||
<template #default="{ row }">{{ formatSize(row.fileSize) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="上传人" prop="createBy" width="110" align="center" show-overflow-tooltip />
|
||||
<el-table-column label="上传时间" prop="createTime" width="170" align="center" />
|
||||
<el-table-column label="操作" width="90" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" :disabled="!row.filePath" @click="handleDownload(row)">下载</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { listProductExcelFiles } from '@/api/price/redline'
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
|
||||
const visible = ref(false)
|
||||
const title = ref('')
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const product = ref(null)
|
||||
|
||||
function open(row) {
|
||||
product.value = row
|
||||
title.value = (row.productCode || '') + ' · ' + (row.productName || '') + ' · 导入附件'
|
||||
visible.value = true
|
||||
loadList()
|
||||
}
|
||||
|
||||
function loadList() {
|
||||
if (!product.value || !product.value.productCode) return
|
||||
loading.value = true
|
||||
listProductExcelFiles(product.value.productCode).then(res => {
|
||||
list.value = res.data || []
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function formatSize(bytes) {
|
||||
const n = Number(bytes)
|
||||
if (!n || n < 0) return '-'
|
||||
if (n < 1024) return n + ' B'
|
||||
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KB'
|
||||
return (n / (1024 * 1024)).toFixed(2) + ' MB'
|
||||
}
|
||||
|
||||
function handleDownload(row) {
|
||||
if (!row || !row.filePath) {
|
||||
proxy.$modal.msgWarning('文件路径不存在')
|
||||
return
|
||||
}
|
||||
proxy.$download.resource(row.filePath)
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
@@ -1,148 +0,0 @@
|
||||
<template>
|
||||
<el-drawer v-model="visible" size="960px" :title="title" append-to-body>
|
||||
<el-form :inline="true" @submit.prevent>
|
||||
<el-form-item label="产品代码">
|
||||
<el-input
|
||||
v-model="queryParams.productCode"
|
||||
placeholder="产品代码"
|
||||
clearable
|
||||
style="width: 160px"
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="产品名称">
|
||||
<el-input
|
||||
v-model="queryParams.productName"
|
||||
placeholder="产品名称"
|
||||
clearable
|
||||
style="width: 160px"
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="失效时间段">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
value-format="YYYY-MM-DD"
|
||||
range-separator="-"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
style="width: 260px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-table v-loading="loading" :data="list" border max-height="calc(100vh - 260px)" empty-text="暂无失效产品">
|
||||
<el-table-column label="产品代码" prop="productCode" min-width="110" show-overflow-tooltip />
|
||||
<el-table-column label="产品名称" prop="productName" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="时效等级" prop="serviceLevel" width="90" align="center" />
|
||||
<el-table-column label="走货属性" prop="cargoAttr" min-width="110" show-overflow-tooltip />
|
||||
<el-table-column label="目的区域" prop="destRegion" width="90" align="center" />
|
||||
<el-table-column label="生效日期" prop="effectiveDate" width="120" align="center" />
|
||||
<el-table-column label="失效日期" prop="expireDate" width="120" align="center" />
|
||||
<el-table-column label="国家数" prop="countryCount" width="80" align="center" />
|
||||
<el-table-column label="附加费数" prop="surchargeCount" width="90" align="center" />
|
||||
<el-table-column label="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<dict-tag :options="bms_product_effect_status" :value="row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="更新内容" prop="updateNote" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="120" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
title="价格"
|
||||
icon="Document"
|
||||
@click="excelFileRef.open(row)"
|
||||
v-hasPermi="['price:redline:query']"
|
||||
></el-button>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
title="红线价明细"
|
||||
icon="View"
|
||||
@click="emit('view-country', row)"
|
||||
v-hasPermi="['price:redline:query']"
|
||||
></el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination
|
||||
v-show="total > 0"
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNum"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="loadList"
|
||||
/>
|
||||
<redline-excel-file-drawer ref="excelFileRef" />
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { listRedline } from '@/api/price/redline'
|
||||
import RedlineExcelFileDrawer from './RedlineExcelFileDrawer.vue'
|
||||
|
||||
const emit = defineEmits(['view-country'])
|
||||
const { proxy } = getCurrentInstance()
|
||||
const { bms_product_effect_status } = useDict('bms_product_effect_status')
|
||||
|
||||
const excelFileRef = ref()
|
||||
const title = '失效价格'
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const total = ref(0)
|
||||
const dateRange = ref([])
|
||||
const queryParams = ref({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
productCode: undefined,
|
||||
productName: undefined,
|
||||
statusGroup: 'expired'
|
||||
})
|
||||
|
||||
function open() {
|
||||
visible.value = true
|
||||
resetQuery()
|
||||
}
|
||||
|
||||
function loadList() {
|
||||
loading.value = true
|
||||
const p = { ...queryParams.value }
|
||||
if (dateRange.value && dateRange.value.length === 2) {
|
||||
proxy.addDateRange(p, dateRange.value)
|
||||
}
|
||||
listRedline(p).then(res => {
|
||||
list.value = res.rows || []
|
||||
total.value = res.total || 0
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function handleQuery() {
|
||||
queryParams.value.pageNum = 1
|
||||
loadList()
|
||||
}
|
||||
|
||||
function resetQuery() {
|
||||
dateRange.value = []
|
||||
queryParams.value = {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
productCode: undefined,
|
||||
productName: undefined,
|
||||
statusGroup: 'expired'
|
||||
}
|
||||
loadList()
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
@@ -1,167 +0,0 @@
|
||||
<template>
|
||||
<el-dialog :title="title" v-model="visible" width="640px" append-to-body>
|
||||
<basic-form ref="formRef" v-model="form" :schemas="schemas" label-width="100px" />
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="visible = false">取 消</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import BasicForm from '@/components/BasicForm'
|
||||
import { getRedline, addRedline, updateRedline } from '@/api/price/redline'
|
||||
|
||||
const emit = defineEmits(['success'])
|
||||
const { proxy } = getCurrentInstance()
|
||||
|
||||
const formRef = ref()
|
||||
const visible = ref(false)
|
||||
const title = ref('')
|
||||
const form = ref(emptyForm())
|
||||
|
||||
const schemas = computed(() => [
|
||||
{
|
||||
label: '产品代码',
|
||||
prop: 'productCode',
|
||||
component: 'input',
|
||||
placeholder: '如 WSP037',
|
||||
span: 12,
|
||||
width: '100%',
|
||||
disabled: !!form.value.productId,
|
||||
props: { maxlength: 20 },
|
||||
rules: [{ required: true, message: '产品代码不能为空', trigger: 'blur' }]
|
||||
},
|
||||
{
|
||||
label: '产品名称',
|
||||
prop: 'productName',
|
||||
component: 'input',
|
||||
placeholder: '产品名称',
|
||||
span: 12,
|
||||
width: '100%',
|
||||
props: { maxlength: 100 },
|
||||
rules: [{ required: true, message: '产品名称不能为空', trigger: 'blur' }]
|
||||
},
|
||||
{
|
||||
label: '时效等级',
|
||||
prop: 'serviceLevel',
|
||||
component: 'input',
|
||||
placeholder: '如 标准',
|
||||
span: 12,
|
||||
width: '100%',
|
||||
props: { maxlength: 20 }
|
||||
},
|
||||
{
|
||||
label: '走货属性',
|
||||
prop: 'cargoAttr',
|
||||
component: 'input',
|
||||
placeholder: '普货/带电等',
|
||||
span: 12,
|
||||
width: '100%',
|
||||
props: { maxlength: 100 }
|
||||
},
|
||||
{
|
||||
label: '目的区域',
|
||||
prop: 'destRegion',
|
||||
component: 'input',
|
||||
placeholder: '如 欧盟',
|
||||
span: 12,
|
||||
width: '100%',
|
||||
props: { maxlength: 50 }
|
||||
},
|
||||
// {
|
||||
// label: '税模式',
|
||||
// prop: 'taxMode',
|
||||
// component: 'select',
|
||||
// placeholder: '请选择',
|
||||
// dict: 'bms_tax_mode',
|
||||
// span: 12,
|
||||
// width: '100%',
|
||||
// rules: [{ required: true, message: '请选择税模式', trigger: 'change' }]
|
||||
// },
|
||||
{
|
||||
label: '生效时间',
|
||||
prop: 'effectiveDate',
|
||||
component: 'datetime',
|
||||
placeholder: '选择时间',
|
||||
span: 12,
|
||||
width: '100%',
|
||||
rules: [{ required: true, message: '生效时间不能为空', trigger: 'change' }]
|
||||
},
|
||||
{
|
||||
label: '失效时间',
|
||||
prop: 'expireDate',
|
||||
component: 'datetime',
|
||||
placeholder: '选择时间',
|
||||
span: 12,
|
||||
width: '100%'
|
||||
},
|
||||
{
|
||||
label: '更新内容',
|
||||
prop: 'updateNote',
|
||||
component: 'textarea',
|
||||
span: 24,
|
||||
width: '100%',
|
||||
props: { rows: 2, maxlength: 500 }
|
||||
},
|
||||
{
|
||||
label: '备注',
|
||||
prop: 'remark',
|
||||
component: 'textarea',
|
||||
span: 24,
|
||||
width: '100%',
|
||||
props: { rows: 2, maxlength: 500 }
|
||||
}
|
||||
])
|
||||
|
||||
function emptyForm() {
|
||||
return {
|
||||
productId: undefined,
|
||||
productCode: undefined,
|
||||
productName: undefined,
|
||||
serviceLevel: undefined,
|
||||
cargoAttr: undefined,
|
||||
destRegion: undefined,
|
||||
updateNote: undefined,
|
||||
taxMode: 'TAX_INCLUDED',
|
||||
effectiveDate: undefined,
|
||||
expireDate: undefined,
|
||||
status: '0',
|
||||
remark: undefined
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
form.value = emptyForm()
|
||||
nextTick(() => formRef.value?.clearValidate())
|
||||
}
|
||||
|
||||
function open(row) {
|
||||
reset()
|
||||
if (!row) {
|
||||
title.value = '新建红线价'
|
||||
visible.value = true
|
||||
return
|
||||
}
|
||||
getRedline(row.productId).then(res => {
|
||||
form.value = { ...emptyForm(), ...(res.data || {}) }
|
||||
title.value = '修改产品'
|
||||
visible.value = true
|
||||
nextTick(() => formRef.value?.clearValidate())
|
||||
})
|
||||
}
|
||||
|
||||
function submitForm() {
|
||||
formRef.value.validate(valid => {
|
||||
if (!valid) return
|
||||
const req = form.value.productId ? updateRedline(form.value) : addRedline(form.value)
|
||||
req.then(() => {
|
||||
proxy.$modal.msgSuccess('操作成功')
|
||||
visible.value = false
|
||||
emit('success')
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<catalog-price-create
|
||||
kind="redline"
|
||||
list-path="/price/redline"
|
||||
:list-fn="listRedline"
|
||||
:del-fn="delRedline"
|
||||
:prepare-fn="prepareRedlineVersion"
|
||||
:apply-preview-fn="applyRedlineCatalogPreview"
|
||||
:attach-excel-fn="attachRedlineVersionExcel"
|
||||
:import-all-fn="importRedlineExcelAsync"
|
||||
:import-country-fn="importRedlineCountryExcelAsync"
|
||||
:preview-all-fn="previewRedlineImport"
|
||||
:preview-country-fn="previewRedlineCountryImport"
|
||||
:country-comp="RedlineCountryDrawer"
|
||||
:surcharge-comp="RedlineSurchargeDrawer"
|
||||
/>
|
||||
</template>
|
||||
<script setup name="RedlinePriceCreate">
|
||||
import CatalogPriceCreate from '../components/CatalogPriceCreate.vue'
|
||||
import RedlineCountryDrawer from './components/RedlineCountryDrawer.vue'
|
||||
import RedlineSurchargeDrawer from './components/RedlineSurchargeDrawer.vue'
|
||||
import {
|
||||
listRedline, delRedline, prepareRedlineVersion, applyRedlineCatalogPreview, attachRedlineVersionExcel,
|
||||
importRedlineExcelAsync, importRedlineCountryExcelAsync,
|
||||
previewRedlineImport, previewRedlineCountryImport
|
||||
} from '@/api/price/redline'
|
||||
</script>
|
||||
@@ -5,9 +5,16 @@
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="mb16"
|
||||
title="导入前须先选择生效时间;红线价与国家明细均按此时间落库,上一版产品/明细失效时间设为该时间。多国家价表用「全部导入」,单国专线成本价用「按国家导入」。相同产品代码+生效日期不覆盖主数据。"
|
||||
title="红线价已生效展示当前生效版本产品。待生效、已失效按版本展示;待生效可启动或删除整版。"
|
||||
/>
|
||||
|
||||
<el-tabs v-model="statusTab" class="catalog-tabs" @tab-change="onTabChange">
|
||||
<el-tab-pane label="已生效" name="effect" />
|
||||
<el-tab-pane label="待生效" name="pending" />
|
||||
<el-tab-pane label="已失效" name="expired" />
|
||||
</el-tabs>
|
||||
|
||||
<div v-show="statusTab === 'effect'">
|
||||
<basic-table
|
||||
ref="tableRef"
|
||||
:extra-params="extraParams"
|
||||
@@ -15,38 +22,12 @@
|
||||
:search-columns="searchColumns"
|
||||
:query="queryList"
|
||||
row-key="productId"
|
||||
:selection="true"
|
||||
:selection="false"
|
||||
>
|
||||
<template #toolbar="{ selection }">
|
||||
<template #toolbar>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['price:redline:add']">新建红线价</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-dropdown @command="handleImportCommand">
|
||||
<el-button type="info" plain icon="Upload" v-hasPermi="['price:redline:import']">
|
||||
导入 Excel<el-icon class="el-icon--right"><arrow-down /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="all">全部导入(多国家价表)</el-dropdown-item>
|
||||
<el-dropdown-item command="country">按国家导入(单国专线成本价)</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="danger"
|
||||
plain
|
||||
icon="Delete"
|
||||
:disabled="!selection.length"
|
||||
@click="handleBatchDelete(selection)"
|
||||
v-hasPermi="['price:redline:remove']"
|
||||
>批量删除</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button plain icon="Clock" @click="expiredRef.open()" v-hasPermi="['price:redline:list']">失效价格</el-button>
|
||||
</el-col>
|
||||
</template>
|
||||
|
||||
<template #col-cargoAttr="{ row }">
|
||||
@@ -58,167 +39,104 @@
|
||||
</template>
|
||||
|
||||
<template #col-action="{ row }">
|
||||
<el-button link type="primary" title="价格" icon="Document" @click="excelFileRef.open(row)" v-hasPermi="['price:redline:query']"></el-button>
|
||||
<el-button link type="primary" title="红线价明细" icon="View" @click="countryRef.open(row)" v-hasPermi="['price:redline:query']"></el-button>
|
||||
<el-button link type="primary" title="附加费" icon="Money" @click="surchargeRef.open(row)" v-hasPermi="['price:surcharge:query']"></el-button>
|
||||
<el-button title="编辑" link type="primary" icon="Edit" @click="handleUpdate(row)" v-hasPermi="['price:redline:edit']"></el-button>
|
||||
</template>
|
||||
</basic-table>
|
||||
</div>
|
||||
|
||||
<redline-form-dialog ref="formRef" @success="() => tableRef.refresh()" />
|
||||
<redline-excel-file-drawer ref="excelFileRef" />
|
||||
<div v-show="statusTab === 'pending'">
|
||||
<el-row class="mb12">
|
||||
<el-col :span="1.5">
|
||||
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['price:redline:add']">新建红线价</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-table v-loading="versionLoading" :data="versionRows" border empty-text="暂无待生效版本" @row-click="openVersionProducts">
|
||||
<el-table-column label="版本号" prop="versionNo" width="120" />
|
||||
<el-table-column label="生效时间" prop="effectiveTime" min-width="170" />
|
||||
<el-table-column label="失效时间" prop="expireTime" min-width="170" />
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{ row }">{{ versionStatusText(row.status) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="产品数" prop="productCount" width="90" align="center" />
|
||||
<el-table-column label="明细行数" prop="detailCount" width="100" align="center" />
|
||||
<el-table-column label="操作" width="360" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click.stop="onEditVersion(row)" v-hasPermi="['price:redline:edit']">编辑</el-button>
|
||||
<el-button link type="primary" @click.stop="openVersionProducts(row)">查看产品</el-button>
|
||||
<el-button link type="primary" @click.stop="openExcel(row)" v-hasPermi="['price:redline:query']">查看附件</el-button>
|
||||
<el-button link type="success" @click.stop="onStartVersion(row)" v-hasPermi="['price:redline:edit']">启动</el-button>
|
||||
<el-button link type="danger" @click.stop="onDeleteVersion(row)" v-hasPermi="['price:redline:remove']">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination
|
||||
v-show="versionTotal > 0"
|
||||
:total="versionTotal"
|
||||
v-model:page="versionQuery.pageNum"
|
||||
v-model:limit="versionQuery.pageSize"
|
||||
@pagination="loadVersions"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-show="statusTab === 'expired'">
|
||||
<el-table v-loading="versionLoading" :data="versionRows" border empty-text="暂无失效版本" @row-click="openVersionProducts">
|
||||
<el-table-column label="版本号" prop="versionNo" width="120" />
|
||||
<el-table-column label="生效时间" prop="effectiveTime" min-width="170" />
|
||||
<el-table-column label="失效时间" prop="expireTime" min-width="170" />
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{ row }">{{ versionStatusText(row.status) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="产品数" prop="productCount" width="90" align="center" />
|
||||
<el-table-column label="明细行数" prop="detailCount" width="100" align="center" />
|
||||
<el-table-column label="操作" width="220" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click.stop="openVersionProducts(row)">查看产品</el-button>
|
||||
<el-button link type="primary" @click.stop="openExcel(row)" v-hasPermi="['price:redline:query']">查看附件</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination
|
||||
v-show="versionTotal > 0"
|
||||
:total="versionTotal"
|
||||
v-model:page="versionQuery.pageNum"
|
||||
v-model:limit="versionQuery.pageSize"
|
||||
@pagination="loadVersions"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<catalog-version-excel-drawer ref="excelRef" :list-fn="listRedlineVersionExcelFiles" />
|
||||
<redline-country-drawer ref="countryRef" />
|
||||
<redline-surcharge-drawer ref="surchargeRef" @success="() => tableRef.refresh()" />
|
||||
<redline-expired-drawer ref="expiredRef" @view-country="(row) => countryRef.open(row)" />
|
||||
<excel-import-dialog
|
||||
ref="importRef"
|
||||
title="红线价 Excel 导入"
|
||||
width="560px"
|
||||
:show-update-support="false"
|
||||
template-url="/templates/product/product-import-all-template.xlsx"
|
||||
template-file-name="产品多国家价表导入模板"
|
||||
:import-desc="allImportDesc"
|
||||
:before-submit="validateImportEffectiveDate"
|
||||
:submit-handler="runImportAllAsync"
|
||||
@success="() => tableRef.refresh()"
|
||||
>
|
||||
<template #extra>
|
||||
<el-form label-width="90px">
|
||||
<el-form-item label="生效时间" required>
|
||||
<el-date-picker
|
||||
v-model="importEffectiveDate"
|
||||
type="datetime"
|
||||
format="YYYY-MM-DD HH:00"
|
||||
value-format="YYYY-MM-DD HH:00:00"
|
||||
popper-class="redline-import-hour-picker"
|
||||
placeholder="请选择生效时间"
|
||||
style="width: 100%"
|
||||
@change="onImportEffectiveTimeChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
</excel-import-dialog>
|
||||
<excel-import-dialog
|
||||
ref="countryImportRef"
|
||||
title="按国家导入"
|
||||
width="560px"
|
||||
:show-update-support="false"
|
||||
:before-submit="validateCountryImport"
|
||||
:import-desc="countryImportDesc"
|
||||
template-url="/templates/product/product-import-country-template.xlsx"
|
||||
template-file-name="产品按国家导入模板"
|
||||
:submit-handler="runImportCountryAsync"
|
||||
@success="() => tableRef.refresh()"
|
||||
>
|
||||
<template #extra>
|
||||
<el-form label-width="90px">
|
||||
<el-form-item label="生效时间" required>
|
||||
<el-date-picker
|
||||
v-model="importEffectiveDate"
|
||||
type="datetime"
|
||||
format="YYYY-MM-DD HH:00"
|
||||
value-format="YYYY-MM-DD HH:00:00"
|
||||
popper-class="redline-import-hour-picker"
|
||||
placeholder="请选择生效时间"
|
||||
style="width: 100%"
|
||||
@change="onImportEffectiveTimeChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="目的国家" required>
|
||||
<el-select v-model="importCountryCode" filterable clearable placeholder="请选择国家" style="width: 100%">
|
||||
<el-option
|
||||
v-for="item in regionOptions"
|
||||
:key="item.countryCode2"
|
||||
:label="regionLabel(item)"
|
||||
:value="item.countryCode2"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
</excel-import-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="importProgressVisible"
|
||||
title="正在导入产品"
|
||||
width="420px"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
:show-close="false"
|
||||
append-to-body
|
||||
>
|
||||
<div class="import-progress">
|
||||
<el-progress :percentage="importProgressPercent" :status="importProgressStatus" />
|
||||
<div class="import-progress-msg">{{ importProgressMessage }}</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
<catalog-version-products-drawer ref="versionProductsRef" :list-fn="listRedline" @view="(row) => countryRef.open(row)" />
|
||||
<catalog-activate-dialog ref="activateRef" :load-versions="loadAllVersions" :activate-fn="activateRedlineVersion" @success="onActivated" />
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="PriceProduct">
|
||||
import ExcelImportDialog from '@/components/ExcelImportDialog'
|
||||
import RedlineFormDialog from './components/RedlineFormDialog.vue'
|
||||
import RedlineExcelFileDrawer from './components/RedlineExcelFileDrawer.vue'
|
||||
import RedlineCountryDrawer from './components/RedlineCountryDrawer.vue'
|
||||
import RedlineSurchargeDrawer from './components/RedlineSurchargeDrawer.vue'
|
||||
import RedlineExpiredDrawer from './components/RedlineExpiredDrawer.vue'
|
||||
import { listRedline, delRedline, importRedlineExcelAsync, importRedlineCountryExcelAsync } from '@/api/price/redline'
|
||||
import { listRegionOptions } from '@/api/data/region'
|
||||
import CatalogVersionProductsDrawer from '../components/CatalogVersionProductsDrawer.vue'
|
||||
import CatalogVersionExcelDrawer from '../components/CatalogVersionExcelDrawer.vue'
|
||||
import CatalogActivateDialog from '../components/CatalogActivateDialog.vue'
|
||||
import { listRedline, listRedlineVersions, activateRedlineVersion, deleteRedlineVersion, listRedlineVersionExcelFiles } from '@/api/price/redline'
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const { bms_product_effect_status } = useDict('bms_product_effect_status')
|
||||
|
||||
const extraParams = { statusGroup: 'active' }
|
||||
const statusTab = ref('effect')
|
||||
const extraParams = reactive({ statusGroup: 'effect' })
|
||||
const versionProductsRef = ref()
|
||||
const activateRef = ref()
|
||||
const versionLoading = ref(false)
|
||||
const versionRows = ref([])
|
||||
const versionTotal = ref(0)
|
||||
const versionQuery = reactive({ pageNum: 1, pageSize: 10 })
|
||||
const tableRef = ref()
|
||||
const formRef = ref()
|
||||
const excelFileRef = ref()
|
||||
const countryRef = ref()
|
||||
const surchargeRef = ref()
|
||||
const expiredRef = ref()
|
||||
const importRef = ref()
|
||||
const countryImportRef = ref()
|
||||
const importCountryCode = ref('')
|
||||
const importEffectiveDate = ref('')
|
||||
|
||||
function pad2(n) {
|
||||
return String(n).padStart(2, '0')
|
||||
}
|
||||
|
||||
function formatImportHour(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 normalizeImportToHour(val) {
|
||||
if (val == null || val === '') return val
|
||||
if (val instanceof Date) return formatImportHour(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'
|
||||
if (/^\d{4}-\d{2}-\d{2}$/.test(s)) return s + ' 00:00:00'
|
||||
return formatImportHour(new Date(s.replace(/-/g, '/'))) || val
|
||||
}
|
||||
|
||||
function onImportEffectiveTimeChange(val) {
|
||||
importEffectiveDate.value = normalizeImportToHour(val) || ''
|
||||
}
|
||||
const regionOptions = ref([])
|
||||
const importProgressVisible = ref(false)
|
||||
const importProgressPercent = ref(0)
|
||||
const importProgressMessage = ref('')
|
||||
const importProgressStatus = ref(undefined)
|
||||
const allImportDesc = [
|
||||
'适用多国家价表(B2C 小包价表等),按目录 sheet 匹配国家导入。',
|
||||
'必选生效时间:产品与 product_country 明细均按此时间计费;上一版产品/明细失效时间设为该时间。',
|
||||
'请在弹窗底部下载模板,按模板格式整理后上传。相同产品代码+生效日期不覆盖主数据。'
|
||||
]
|
||||
const countryImportDesc = [
|
||||
'选择目的国家后上传专线成本价表,目录中的产品都会记到该国家。',
|
||||
'必选生效时间:产品与 product_country 明细均按此时间计费;上一版产品/明细失效时间设为该时间。',
|
||||
'邮编、赔偿标准等 sheet 不导入。可在弹窗底部下载模板。'
|
||||
]
|
||||
const excelRef = ref()
|
||||
|
||||
const columns = [
|
||||
{ prop: 'productCode', label: '产品代码', minWidth: 110 },
|
||||
@@ -232,7 +150,7 @@ const columns = [
|
||||
{ prop: 'surchargeCount', label: '附加费数', width: 90, align: 'center' },
|
||||
{ prop: 'status', label: '状态', width: 90, align: 'center', slot: 'status' },
|
||||
{ prop: 'updateNote', label: '更新内容', minWidth: 160, showOverflowTooltip: true },
|
||||
{ prop: 'action', label: '操作', width: 240, fixed: 'right', slot: 'action' }
|
||||
{ prop: 'action', label: '操作', width: 120, fixed: 'right', slot: 'action' }
|
||||
]
|
||||
|
||||
const searchColumns = [
|
||||
@@ -245,176 +163,102 @@ function queryList(params) {
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
formRef.value.open()
|
||||
proxy.$router.push({ path: '/price/redline-create/index' })
|
||||
}
|
||||
|
||||
function handleUpdate(row) {
|
||||
formRef.value.open(row)
|
||||
}
|
||||
|
||||
function isPendingProduct(row) {
|
||||
if (!row) return false
|
||||
if (String(row.status) === '0') return true
|
||||
if (!row.effectiveDate) return false
|
||||
const eff = new Date(String(row.effectiveDate).replace(/-/g, '/')).getTime()
|
||||
if (Number.isNaN(eff) || eff <= Date.now()) return false
|
||||
if (row.expireDate) {
|
||||
const exp = new Date(String(row.expireDate).replace(/-/g, '/')).getTime()
|
||||
if (!Number.isNaN(exp) && exp <= Date.now()) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function handleBatchDelete(selection) {
|
||||
const rows = (selection || []).filter(r => r && r.productId != null)
|
||||
if (!rows.length) {
|
||||
proxy.$modal.msgWarning('请先勾选红线价')
|
||||
function openExcel(row) {
|
||||
if (!row || row.psvId == null) {
|
||||
proxy.$modal.msgWarning('该行没有版本,无法查看附件')
|
||||
return
|
||||
}
|
||||
const pending = rows.filter(isPendingProduct)
|
||||
const blocked = rows.filter(r => !isPendingProduct(r))
|
||||
if (!pending.length) {
|
||||
proxy.$modal.msgWarning('仅待生效红线价可删除,当前勾选无待生效记录')
|
||||
excelRef.value && excelRef.value.open(row)
|
||||
}
|
||||
|
||||
function onTabChange(name) {
|
||||
if (name === 'pending' || name === 'expired') {
|
||||
versionQuery.pageNum = 1
|
||||
loadVersions()
|
||||
return
|
||||
}
|
||||
if (blocked.length) {
|
||||
const tip = blocked.map(r => r.productCode || r.productId).join('、')
|
||||
proxy.$modal.msgWarning('已跳过非待生效红线价:' + tip)
|
||||
extraParams.statusGroup = 'effect'
|
||||
nextTick(() => tableRef.value && tableRef.value.refresh())
|
||||
}
|
||||
|
||||
function versionStatusText(status) {
|
||||
const map = { '0': '草稿', '1': '已生效', '2': '已失效', '3': '待生效' }
|
||||
return map[String(status)] || status || '-'
|
||||
}
|
||||
|
||||
function loadVersions() {
|
||||
versionLoading.value = true
|
||||
const params = {
|
||||
pageNum: versionQuery.pageNum,
|
||||
pageSize: versionQuery.pageSize
|
||||
}
|
||||
const codes = pending.map(r => r.productCode || r.productId).join('、')
|
||||
const ids = pending.map(r => r.productId)
|
||||
proxy.$modal.confirm(
|
||||
'确认删除已选待生效红线价(' + pending.length + '个):'
|
||||
+ codes
|
||||
+ '?删除后产品及国家/重量段明细将一并清除,且不可恢复。'
|
||||
).then(() => delRedline(ids)).then(() => {
|
||||
tableRef.value.refresh()
|
||||
if (statusTab.value === 'pending') params.statusGroup = 'pending'
|
||||
else params.status = '2'
|
||||
listRedlineVersions(params).then(res => {
|
||||
versionRows.value = res.rows || []
|
||||
versionTotal.value = res.total || 0
|
||||
}).finally(() => { versionLoading.value = false })
|
||||
}
|
||||
|
||||
function loadAllVersions() {
|
||||
return listRedlineVersions({ pageNum: 1, pageSize: 500 }).then(res => res.rows || [])
|
||||
}
|
||||
|
||||
function openVersionProducts(row) {
|
||||
versionProductsRef.value && versionProductsRef.value.open(row)
|
||||
}
|
||||
|
||||
function onEditVersion(row) {
|
||||
if (!row || row.psvId == null) return
|
||||
proxy.$router.push({
|
||||
path: '/price/redline-create/index',
|
||||
query: {
|
||||
psvId: row.psvId,
|
||||
versionNo: row.versionNo || undefined
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function onStartVersion(row) {
|
||||
if (row && row.psvId != null) {
|
||||
activateRef.value && activateRef.value.open(row)
|
||||
return
|
||||
}
|
||||
loadAllVersions().then(list => {
|
||||
const pending = (list || []).filter(v => v && (String(v.status) === '0' || String(v.status) === '3'))
|
||||
if (!pending.length) {
|
||||
proxy.$modal.msgWarning('当前没有可启动的待生效版本')
|
||||
return
|
||||
}
|
||||
pending.sort((a, b) => (Number(b.versionSeq) || 0) - (Number(a.versionSeq) || 0))
|
||||
const draft = pending.find(v => String(v.status) === '0')
|
||||
activateRef.value && activateRef.value.open(draft || pending[0])
|
||||
})
|
||||
}
|
||||
|
||||
function onDeleteVersion(row) {
|
||||
if (!row || row.psvId == null) return
|
||||
proxy.$modal.confirm('确认硬删除该待生效版本?版本、产品、明细将从数据库真删除,且不可恢复。').then(() => deleteRedlineVersion(row.psvId)).then(() => {
|
||||
loadVersions()
|
||||
proxy.$modal.msgSuccess('删除成功')
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
function handleImportCommand(command) {
|
||||
if (command === 'country') {
|
||||
handleCountryImport()
|
||||
return
|
||||
}
|
||||
handleImport()
|
||||
function onActivated() {
|
||||
if (statusTab.value === 'pending' || statusTab.value === 'expired') loadVersions()
|
||||
else if (tableRef.value) tableRef.value.refresh()
|
||||
}
|
||||
|
||||
function handleImport() {
|
||||
importEffectiveDate.value = ''
|
||||
importRef.value.open()
|
||||
}
|
||||
|
||||
function regionLabel(item) {
|
||||
const name = item.countryNameCn || item.countryNameEn || ''
|
||||
const code = item.countryCode2 || ''
|
||||
return name && code ? name + ' (' + code + ')' : (name || code)
|
||||
}
|
||||
|
||||
function loadRegionOptions() {
|
||||
if (regionOptions.value.length) return
|
||||
listRegionOptions().then(res => {
|
||||
regionOptions.value = res.data || []
|
||||
})
|
||||
}
|
||||
|
||||
function handleCountryImport() {
|
||||
importCountryCode.value = ''
|
||||
importEffectiveDate.value = ''
|
||||
loadRegionOptions()
|
||||
countryImportRef.value.open()
|
||||
}
|
||||
|
||||
function validateImportEffectiveDate() {
|
||||
if (!importEffectiveDate.value) {
|
||||
return '请先选择生效时间'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function validateCountryImport() {
|
||||
const effErr = validateImportEffectiveDate()
|
||||
if (effErr) return effErr
|
||||
if (!importCountryCode.value) {
|
||||
return '请先选择目的国家'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function applyImportProgress(t) {
|
||||
importProgressPercent.value = Math.min(100, Number(t.percent) || 0)
|
||||
if (t.status === 'PENDING') importProgressMessage.value = '排队中…'
|
||||
else if (t.status === 'RUNNING') {
|
||||
const m = t.message || ''
|
||||
if (m === 'parsing excel') importProgressMessage.value = '正在解析价表…'
|
||||
else if (m === 'finishing') importProgressMessage.value = '收尾中…'
|
||||
else if (m === 'queued') importProgressMessage.value = '排队中…'
|
||||
else if (m.indexOf('importing ') === 0) {
|
||||
const rest = m.slice('importing '.length)
|
||||
importProgressMessage.value = '正在导入产品 ' + rest
|
||||
} else importProgressMessage.value = m || '正在导入…'
|
||||
} else if (t.status === 'SUCCESS') {
|
||||
importProgressMessage.value = t.message || '导入完成'
|
||||
importProgressStatus.value = 'success'
|
||||
} else if (t.status === 'FAIL') {
|
||||
importProgressMessage.value = t.error || t.message || '导入失败'
|
||||
importProgressStatus.value = 'exception'
|
||||
}
|
||||
}
|
||||
|
||||
async function runImportWithProgress(runner) {
|
||||
importProgressVisible.value = true
|
||||
importProgressPercent.value = 0
|
||||
importProgressMessage.value = '上传并解析中…'
|
||||
importProgressStatus.value = undefined
|
||||
try {
|
||||
const { task } = await runner(applyImportProgress)
|
||||
importProgressPercent.value = 100
|
||||
importProgressStatus.value = 'success'
|
||||
return (task && task.message) ? task.message : '导入完成'
|
||||
} catch (e) {
|
||||
importProgressStatus.value = 'exception'
|
||||
const tip = (e && e.task && e.task.error) || (e && e.message) || '导入失败'
|
||||
importProgressMessage.value = tip
|
||||
await new Promise(resolve => setTimeout(resolve, 600))
|
||||
throw e
|
||||
} finally {
|
||||
importProgressVisible.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function runImportAllAsync(uploadFile) {
|
||||
const raw = uploadFile && (uploadFile.raw || uploadFile)
|
||||
if (!raw) {
|
||||
throw new Error('请先选择文件')
|
||||
}
|
||||
if (!importEffectiveDate.value) {
|
||||
throw new Error('请先选择生效时间')
|
||||
}
|
||||
const eff = importEffectiveDate.value
|
||||
return runImportWithProgress((onProgress) => importRedlineExcelAsync(raw, eff, onProgress))
|
||||
}
|
||||
|
||||
async function runImportCountryAsync(uploadFile) {
|
||||
const raw = uploadFile && (uploadFile.raw || uploadFile)
|
||||
if (!raw) {
|
||||
throw new Error('请先选择文件')
|
||||
}
|
||||
if (!importEffectiveDate.value) {
|
||||
throw new Error('请先选择生效时间')
|
||||
}
|
||||
if (!importCountryCode.value) {
|
||||
throw new Error('请先选择目的国家')
|
||||
}
|
||||
const code = importCountryCode.value
|
||||
const eff = importEffectiveDate.value
|
||||
return runImportWithProgress((onProgress) => importRedlineCountryExcelAsync(raw, code, eff, onProgress))
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mb16 { margin-bottom: 16px; }
|
||||
.mb12 { margin-bottom: 12px; }
|
||||
.catalog-tabs { margin-bottom: 8px; }
|
||||
.import-progress { padding: 8px 4px 4px; }
|
||||
.import-progress-msg {
|
||||
margin-top: 12px;
|
||||
|
||||
@@ -143,6 +143,7 @@
|
||||
width="560px"
|
||||
:show-update-support="false"
|
||||
:import-desc="importDesc"
|
||||
:file-limit="5"
|
||||
:submit-handler="runImportAsync"
|
||||
@success="onImportSuccess"
|
||||
/>
|
||||
@@ -153,6 +154,7 @@
|
||||
width="560px"
|
||||
:show-update-support="false"
|
||||
:import-desc="countryImportDesc"
|
||||
:file-limit="5"
|
||||
:before-submit="validateCountryImport"
|
||||
:submit-handler="runCountryImportAsync"
|
||||
@success="onImportSuccess"
|
||||
@@ -482,7 +484,6 @@ function mergeLocalProducts(existing, incoming) {
|
||||
const key = String(p.productCode).toUpperCase()
|
||||
const prev = map.get(key) || {}
|
||||
const next = { ...prev, ...p }
|
||||
if (p.pefId == null && prev.pefId != null) next.pefId = prev.pefId
|
||||
map.set(key, next)
|
||||
}
|
||||
return Array.from(map.values())
|
||||
@@ -573,26 +574,32 @@ function validateCountryImport() {
|
||||
return ''
|
||||
}
|
||||
|
||||
function unwrapUploadFiles(uploadFile) {
|
||||
const list = Array.isArray(uploadFile) ? uploadFile : [uploadFile]
|
||||
const out = []
|
||||
for (const item of list) {
|
||||
const raw = item && (item.raw || item)
|
||||
if (raw) out.push(raw)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
async function runImportAsync(uploadFile, countryCode) {
|
||||
const raw = uploadFile && (uploadFile.raw || uploadFile)
|
||||
if (!raw) {
|
||||
const files = unwrapUploadFiles(uploadFile)
|
||||
if (!files.length) {
|
||||
throw new Error('请先选择文件')
|
||||
}
|
||||
// Do not open a second el-dialog over ExcelImportDialog: closing the overlay
|
||||
// click-throughs into el-upload and EP crashes (inputRef.value.value = '').
|
||||
// Excel build-price (IMPORT mode): merge by product+country. Modify-price / other modes unchanged.
|
||||
const replaceByProductCountry = !isModifyImport.value || !!countryCode
|
||||
if (!props.livePersist) {
|
||||
const res = await previewImportDetailPrices(raw, {
|
||||
const res = await previewImportDetailPrices(files, {
|
||||
updateOnly: isModifyImport.value && !countryCode,
|
||||
replaceByProductCountry,
|
||||
countryCode: countryCode || undefined,
|
||||
details: props.details || []
|
||||
})
|
||||
const data = res.data || {}
|
||||
if (data.pefId != null) {
|
||||
emit('update:pefId', data.pefId)
|
||||
}
|
||||
const ids = Array.isArray(data.pefIds) ? data.pefIds : (data.pefId != null ? [data.pefId] : [])
|
||||
ids.forEach(id => emit('update:pefId', id))
|
||||
const products = replaceByProductCountry
|
||||
? mergeLocalProducts(props.products, data.products || [])
|
||||
: (isModifyImport.value ? (props.products || []) : (data.products || []))
|
||||
@@ -600,7 +607,7 @@ async function runImportAsync(uploadFile, countryCode) {
|
||||
return data.message || '导入完成'
|
||||
}
|
||||
const { task } = await importSetDetailPricesAsync(
|
||||
raw,
|
||||
files,
|
||||
{
|
||||
priceSetId: props.priceSetId,
|
||||
psvId: await resolvePsvId(false),
|
||||
|
||||
@@ -113,6 +113,7 @@ const form = reactive({
|
||||
})
|
||||
/** 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(() =>
|
||||
@@ -314,7 +315,9 @@ function buildPayload(opts = {}) {
|
||||
priceBuildMode: version.priceBuildMode || undefined
|
||||
}
|
||||
}
|
||||
if (draftPefId.value != null) {
|
||||
if (draftPefIds.value.length) {
|
||||
payload.pefIds = draftPefIds.value.slice()
|
||||
} else if (draftPefId.value != null) {
|
||||
payload.pefId = draftPefId.value
|
||||
}
|
||||
if (opts.forceDetails || hasUnsavedDetail) {
|
||||
@@ -376,6 +379,11 @@ function onPsvIdUpdate(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: edit live-saves mode; create only local + optional preview pull. */
|
||||
@@ -391,6 +399,7 @@ function onPriceBuildModeUpdate(mode) {
|
||||
form.version.priceBuildMode = mode
|
||||
if (mode !== 'IMPORT') {
|
||||
draftPefId.value = undefined
|
||||
draftPefIds.value = []
|
||||
}
|
||||
form.details.splice(0, form.details.length)
|
||||
if (detailsRef.value && detailsRef.value.clearLocalDraft) {
|
||||
@@ -485,6 +494,7 @@ function applySetData(data, asCopy, asNewVersion) {
|
||||
form.version.status = undefined
|
||||
form.version.priceBuildMode = undefined
|
||||
draftPefId.value = undefined
|
||||
draftPefIds.value = []
|
||||
savedPriceBuildMode.value = undefined
|
||||
form.details = []
|
||||
form.products = []
|
||||
@@ -498,6 +508,7 @@ function applySetData(data, asCopy, asNewVersion) {
|
||||
form.version.status = (resume && resume.status) || '0'
|
||||
form.version.priceBuildMode = undefined
|
||||
draftPefId.value = undefined
|
||||
draftPefIds.value = []
|
||||
savedPriceBuildMode.value = undefined
|
||||
form.details = []
|
||||
form.products = []
|
||||
@@ -509,6 +520,7 @@ function applySetData(data, asCopy, asNewVersion) {
|
||||
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 }))
|
||||
|
||||
@@ -421,7 +421,6 @@ export function toDetailPayload(d) {
|
||||
return {
|
||||
psdId: d.psdId || undefined,
|
||||
psvId: d.psvId || undefined,
|
||||
priceSetId: d.priceSetId || undefined,
|
||||
productCode: d.productCode,
|
||||
productName: d.productName,
|
||||
countryCode: d.countryCode || undefined,
|
||||
|
||||
Reference in New Issue
Block a user