提交产品列表
This commit is contained in:
@@ -46,6 +46,7 @@
|
||||
:height="height"
|
||||
:max-height="maxHeight"
|
||||
:default-sort="defaultSort"
|
||||
:span-method="spanMethod"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-click="(row, col, e) => $emit('row-click', row, col, e)"
|
||||
@sort-change="(c) => $emit('sort-change', c)"
|
||||
@@ -241,6 +242,11 @@ const props = defineProps({
|
||||
type: Object,
|
||||
default: undefined
|
||||
},
|
||||
// 单元格合并方法(透传 el-table span-method),签名同 el-table:({ row, column, rowIndex, columnIndex }) => { rowspan, colspan }
|
||||
spanMethod: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
// 每页条数可选项
|
||||
pageSizes: {
|
||||
type: Array,
|
||||
@@ -425,7 +431,10 @@ defineExpose({
|
||||
getTableRef,
|
||||
getSearchFormRef,
|
||||
getQueryParams,
|
||||
setQueryParams
|
||||
setQueryParams,
|
||||
getData: () => tableData.value,
|
||||
/** 当前页数据(响应式,父组件 span-method 可直接订阅) */
|
||||
data: tableData
|
||||
})
|
||||
|
||||
if (props.autoLoad) {
|
||||
|
||||
@@ -1,22 +1,69 @@
|
||||
<template>
|
||||
<el-dialog :title="title" v-model="visible" :width="width" append-to-body @close="handleClose">
|
||||
<el-upload ref="uploadRef" :limit="1" accept=".xlsx, .xls" :headers="headers" :action="uploadUrl" :disabled="isUploading" :on-progress="handleProgress" :on-change="handleFileChange" :on-remove="handleFileRemove" :on-success="handleSuccess" :auto-upload="false" drag>
|
||||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip text-center">
|
||||
<div class="el-upload__tip">
|
||||
<el-checkbox v-model="updateSupport"> {{ updateSupportLabel }} </el-checkbox>
|
||||
</div>
|
||||
<span>仅允许导入xls、xlsx格式文件。</span>
|
||||
<el-link v-if="templateUrl" type="primary" underline="never" style="font-size: 12px; vertical-align: baseline" @click="handleDownloadTemplate">下载模板</el-link>
|
||||
<el-dialog :title="title" v-model="visible" :width="width" append-to-body @close="handleClose" class="excel-import-dialog">
|
||||
<div class="import-body">
|
||||
<!-- 业务说明(可选) -->
|
||||
<div v-if="importDesc.length" class="import-desc">
|
||||
<el-icon class="desc-icon"><Info-Filled /></el-icon>
|
||||
<ul class="desc-list">
|
||||
<li v-for="(line, i) in importDesc" :key="i">{{ line }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 上传拖拽区 -->
|
||||
<el-upload
|
||||
ref="uploadRef"
|
||||
:limit="1"
|
||||
:accept="accept"
|
||||
:headers="headers"
|
||||
:action="uploadUrl"
|
||||
:disabled="isUploading"
|
||||
:on-progress="handleProgress"
|
||||
:on-change="handleFileChange"
|
||||
:on-remove="handleFileRemove"
|
||||
:on-success="handleSuccess"
|
||||
:auto-upload="false"
|
||||
drag
|
||||
class="upload-area"
|
||||
>
|
||||
<div class="upload-placeholder">
|
||||
<el-icon class="upload-icon"><upload-filled /></el-icon>
|
||||
<div class="upload-title">点击或拖拽上传文件</div>
|
||||
<div class="upload-hint">支持 {{ accept }} 格式,单次最多导入 1 个文件,文件大小不超过 {{ fileSizeMb }} MB</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
</el-upload>
|
||||
|
||||
<!-- 已选文件 + 进度 -->
|
||||
<div v-if="selectedFile || isUploading" class="file-progress">
|
||||
<div v-if="selectedFile" class="selected-file">
|
||||
<el-icon class="file-icon"><Document /></el-icon>
|
||||
<span class="file-name" :title="selectedFile.name">{{ selectedFile.name }}</span>
|
||||
<el-icon v-if="!isUploading" class="file-remove" @click="handleClearFile"><CircleClose /></el-icon>
|
||||
</div>
|
||||
<el-progress v-if="isUploading" :percentage="progressPercent" :stroke-width="6" :show-text="true" status="success" class="upload-progress" />
|
||||
</div>
|
||||
|
||||
<!-- 校验错误 -->
|
||||
<div v-if="validateError" class="validate-error">
|
||||
<el-icon><Warning-Filled /></el-icon>
|
||||
<span>{{ validateError }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 选项与提示 -->
|
||||
<div class="import-options">
|
||||
<el-checkbox v-if="showUpdateSupport" v-model="updateSupport" class="update-checkbox">{{ updateSupportLabel }}</el-checkbox>
|
||||
<div class="import-tips">
|
||||
<el-icon class="tip-icon"><Info-Filled /></el-icon>
|
||||
<div class="tip-text">
|
||||
仅允许上传 {{ accept }} 格式文件,文件大小不超过 {{ fileSizeMb }} MB。
|
||||
<el-link v-if="templateUrl" type="primary" underline="never" class="template-link" @click="handleDownloadTemplate">下载模板</el-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button type="primary" @click="handleSubmit">确 定</el-button>
|
||||
<el-button @click="visible = false">取 消</el-button>
|
||||
<el-button type="primary" :disabled="!selectedFile || isUploading" :loading="isUploading" @click="handleSubmit">{{ submitText }}</el-button>
|
||||
<el-button @click="visible = false" :disabled="isUploading">取 消</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
@@ -36,7 +83,7 @@ const props = defineProps({
|
||||
// 对话框宽度
|
||||
width: {
|
||||
type: String,
|
||||
default: '400px'
|
||||
default: '420px'
|
||||
},
|
||||
// 上传接口地址(必传)
|
||||
action: {
|
||||
@@ -62,6 +109,31 @@ const props = defineProps({
|
||||
defaultUpdateSupport: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 是否显示「覆盖更新」勾选(设为 false 可完全隐藏该选项)
|
||||
showUpdateSupport: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 接收的文件类型(扩展名或 MIME),默认 xlsx/xls
|
||||
accept: {
|
||||
type: String,
|
||||
default: '.xlsx, .xls'
|
||||
},
|
||||
// 单文件大小上限(MB),默认 50
|
||||
fileSizeMb: {
|
||||
type: Number,
|
||||
default: 50
|
||||
},
|
||||
// 业务说明(字符串数组),显示在弹窗顶部 importDesc 段
|
||||
importDesc: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
// 确定按钮文字
|
||||
submitText: {
|
||||
type: String,
|
||||
default: '确 定'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -72,6 +144,8 @@ const visible = ref(false)
|
||||
const selectedFile = ref(null)
|
||||
const isUploading = ref(false)
|
||||
const updateSupport = ref(false)
|
||||
const progressPercent = ref(0)
|
||||
const validateError = ref('')
|
||||
const headers = { Authorization: 'Bearer ' + getToken() }
|
||||
|
||||
const uploadUrl = computed(() => {
|
||||
@@ -84,6 +158,8 @@ const templateUrl = computed(() => !!props.templateAction)
|
||||
function open() {
|
||||
updateSupport.value = props.defaultUpdateSupport
|
||||
isUploading.value = false
|
||||
progressPercent.value = 0
|
||||
validateError.value = ''
|
||||
visible.value = true
|
||||
nextTick(() => {
|
||||
selectedFile.value = null
|
||||
@@ -94,6 +170,8 @@ function open() {
|
||||
// 关闭时清理
|
||||
function handleClose() {
|
||||
isUploading.value = false
|
||||
progressPercent.value = 0
|
||||
validateError.value = ''
|
||||
selectedFile.value = null
|
||||
uploadRef.value?.clearFiles()
|
||||
}
|
||||
@@ -104,24 +182,64 @@ function handleDownloadTemplate() {
|
||||
}
|
||||
|
||||
// 上传进度
|
||||
function handleProgress() {
|
||||
function handleProgress(event, file) {
|
||||
isUploading.value = true
|
||||
if (file && typeof file.percent === 'number') {
|
||||
progressPercent.value = Math.round(file.percent)
|
||||
}
|
||||
}
|
||||
|
||||
// 文件类型/大小校验
|
||||
function checkFile(file) {
|
||||
if (!file) return '请先选择文件'
|
||||
const name = (file.name || '').toLowerCase()
|
||||
const types = props.accept.split(',').map(s => s.trim().toLowerCase()).filter(Boolean)
|
||||
if (types.length) {
|
||||
const ok = types.some(t => {
|
||||
const ext = t.startsWith('.') ? t : '.' + t
|
||||
return name.endsWith(ext)
|
||||
})
|
||||
if (!ok) return `仅允许上传 ${props.accept} 格式文件`
|
||||
}
|
||||
const sizeMb = file.size / 1024 / 1024
|
||||
if (sizeMb > props.fileSizeMb) return `文件大小不能超过 ${props.fileSizeMb} MB`
|
||||
return ''
|
||||
}
|
||||
|
||||
/** 文件选择处理 */
|
||||
const handleFileChange = (file, fileList) => {
|
||||
progressPercent.value = 0
|
||||
const err = checkFile(file)
|
||||
if (err) {
|
||||
validateError.value = err
|
||||
selectedFile.value = null
|
||||
nextTick(() => uploadRef.value?.clearFiles())
|
||||
return
|
||||
}
|
||||
validateError.value = ''
|
||||
selectedFile.value = file
|
||||
}
|
||||
|
||||
/** 文件删除处理 */
|
||||
const handleFileRemove = (file, fileList) => {
|
||||
selectedFile.value = null
|
||||
progressPercent.value = 0
|
||||
validateError.value = ''
|
||||
}
|
||||
|
||||
/** 手动清除已选文件 */
|
||||
function handleClearFile() {
|
||||
selectedFile.value = null
|
||||
validateError.value = ''
|
||||
uploadRef.value?.clearFiles()
|
||||
}
|
||||
|
||||
// 上传成功
|
||||
function handleSuccess(response) {
|
||||
visible.value = false
|
||||
isUploading.value = false
|
||||
progressPercent.value = 0
|
||||
validateError.value = ''
|
||||
selectedFile.value = null
|
||||
uploadRef.value?.clearFiles()
|
||||
proxy.$alert("<div style='overflow:auto;overflow-x:hidden;max-height:70vh;padding:10px 20px 0;'>" + response.msg + '</div>', '导入结果', { dangerouslyUseHTMLString: true })
|
||||
@@ -131,8 +249,9 @@ function handleSuccess(response) {
|
||||
// 提交上传
|
||||
function handleSubmit() {
|
||||
const file = selectedFile.value
|
||||
if (!file || file.length === 0 || !file.name.toLowerCase().endsWith('.xls') && !file.name.toLowerCase().endsWith('.xlsx')) {
|
||||
proxy.$modal.msgError("请选择后缀为 “xls”或“xlsx”的文件。")
|
||||
const err = checkFile(file)
|
||||
if (err) {
|
||||
proxy.$modal.msgError(err)
|
||||
return
|
||||
}
|
||||
uploadRef.value.submit()
|
||||
@@ -140,3 +259,167 @@ function handleSubmit() {
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.excel-import-dialog :deep(.el-dialog__body) {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.import-body {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.upload-area :deep(.el-upload-dragger) {
|
||||
padding: 44px 20px;
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 8px;
|
||||
transition: border-color 0.2s, background-color 0.2s;
|
||||
}
|
||||
|
||||
.upload-area :deep(.el-upload-dragger:hover),
|
||||
.upload-area :deep(.el-upload-dragger.is-dragover) {
|
||||
border-color: var(--el-color-primary);
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
}
|
||||
|
||||
.upload-placeholder {
|
||||
text-align: center;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
|
||||
.upload-icon {
|
||||
font-size: 46px;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.upload-title {
|
||||
font-size: 16px;
|
||||
margin-top: 14px;
|
||||
color: var(--el-text-color-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.upload-hint {
|
||||
font-size: 13px;
|
||||
margin-top: 8px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.selected-file {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 14px;
|
||||
background: var(--el-color-primary-light-9);
|
||||
border: 1px solid var(--el-color-primary-light-7);
|
||||
border-radius: 6px;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
color: var(--el-color-primary);
|
||||
font-size: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.file-remove {
|
||||
color: var(--el-text-color-secondary);
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.file-remove:hover {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
|
||||
.import-options {
|
||||
margin-top: 16px;
|
||||
padding: 14px 16px;
|
||||
background: var(--el-fill-color-light);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.update-checkbox {
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.import-tips {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.tip-icon {
|
||||
margin-top: 1px;
|
||||
font-size: 14px;
|
||||
color: var(--el-color-info);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.template-link {
|
||||
font-size: 12px;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
.import-desc {
|
||||
margin-bottom: 14px;
|
||||
padding: 12px 14px;
|
||||
background: var(--el-color-info-light-9);
|
||||
border: 1px solid var(--el-color-info-light-7);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
color: var(--el-text-color-regular);
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.desc-icon {
|
||||
margin-top: 2px;
|
||||
color: var(--el-color-info);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.desc-list {
|
||||
margin: 0;
|
||||
padding-left: 18px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.file-progress {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.upload-progress {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.validate-error {
|
||||
margin-top: 12px;
|
||||
padding: 8px 12px;
|
||||
background: var(--el-color-danger-light-9);
|
||||
border: 1px solid var(--el-color-danger-light-7);
|
||||
border-radius: 6px;
|
||||
color: var(--el-color-danger);
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
:columns="columns"
|
||||
:search-columns="searchColumns"
|
||||
:query="queryList"
|
||||
:span-method="objectSpanMethod"
|
||||
row-key="regionId"
|
||||
>
|
||||
<template #toolbar="{ selection, ids }">
|
||||
@@ -285,6 +286,53 @@ function queryList(params) {
|
||||
return listRegion(params)
|
||||
}
|
||||
|
||||
// 单元格合并:按「洲/大区」continent 合并相同大区的行(仅作用于 continent 列)
|
||||
// 关键:el-table 在 :data 引用变化时会重新调用 span-method,
|
||||
// 但这里 BasicTable 内部的 tableData 是异步请求回来的,父组件拿不到其引用,
|
||||
// 因此用 watch + nextTick + el-table.doLayout() 强制刷新。
|
||||
const spanMap = ref([])
|
||||
function rebuildSpanMap() {
|
||||
const rows = tableRef.value?.getData?.() || []
|
||||
const map = new Array(rows.length)
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const cur = rows[i]?.continent
|
||||
if (i > 0 && rows[i - 1]?.continent === cur) {
|
||||
map[i] = { rowspan: 0, colspan: 0 }
|
||||
} else {
|
||||
let count = 1
|
||||
while (i + count < rows.length && rows[i + count]?.continent === cur) count++
|
||||
map[i] = { rowspan: count, colspan: 1 }
|
||||
}
|
||||
}
|
||||
spanMap.value = map
|
||||
// 强制 el-table 重新调用 span-method
|
||||
tableRef.value?.getTableRef?.()?.doLayout?.()
|
||||
}
|
||||
function objectSpanMethod({ column, rowIndex }) {
|
||||
if (column.property !== 'continent') return { rowspan: 1, colspan: 1 }
|
||||
return spanMap.value[rowIndex] || { rowspan: 1, colspan: 1 }
|
||||
}
|
||||
// 监听 BasicTable 数据变化
|
||||
// 关键:expose.data 是 ref 对象;getData() 是 ()=>tableData.value 函数
|
||||
// watch 源用 getData() 函数(Vue 会调用 source getter 并比较返回值),
|
||||
// 每次 tableData.value 被重新赋值(分页/搜索/刷新),返回值(新数组)变化,watch 触发
|
||||
onMounted(() => {
|
||||
let lastLen = -1
|
||||
watch(
|
||||
() => {
|
||||
const ref = tableRef.value?.data
|
||||
return ref?.value || []
|
||||
},
|
||||
(rows) => {
|
||||
if (rows.length !== lastLen) {
|
||||
lastLen = rows.length
|
||||
nextTick(rebuildSpanMap)
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
})
|
||||
|
||||
const data = reactive({
|
||||
form: {},
|
||||
rules: {
|
||||
|
||||
@@ -20,10 +20,10 @@
|
||||
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['price:product:add']">新建产品</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="success" plain icon="Edit" :disabled="selection.length !== 1" @click="handleUpdate(selection[0])" v-hasPermi="['price:product:edit']">修改</el-button>
|
||||
<el-button type="success" plain icon="Edit" :disabled="selection.length !== 1 || (selection[0] && isProductActive(selection[0]))" @click="handleUpdate(selection[0])" v-hasPermi="['price:product:edit']">修改</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="danger" plain icon="Delete" :disabled="!selection.length" @click="handleDelete(ids)" v-hasPermi="['price:product:remove']">删除</el-button>
|
||||
<el-button type="danger" plain icon="Delete" :disabled="!selection.length || selection.some(isProductActive)" @click="handleDelete(ids)" v-hasPermi="['price:product:remove']">删除</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="info" plain icon="Upload" @click="handleImport" v-hasPermi="['price:product:import']">导入 Excel</el-button>
|
||||
@@ -48,8 +48,8 @@
|
||||
|
||||
<template #col-action="{ row }">
|
||||
<el-button link type="primary" icon="View" @click="openCountries(row)" v-hasPermi="['price:product:query']">产品明细</el-button>
|
||||
<el-button link type="primary" icon="Edit" @click="handleUpdate(row)" v-hasPermi="['price:product:edit']">编辑</el-button>
|
||||
<el-button link type="primary" icon="Delete" @click="handleDelete(row.productId)" v-hasPermi="['price:product:remove']">删除</el-button>
|
||||
<el-button :disabled="isProductActive(row)" :title="isProductActive(row) ? '生效中不可编辑' : ''" link type="primary" icon="Edit" @click="handleUpdate(row)" v-hasPermi="['price:product:edit']">编辑</el-button>
|
||||
<el-button :disabled="isProductActive(row)" :title="isProductActive(row) ? '生效中不可删除' : ''" link type="primary" icon="Delete" @click="handleDelete(row.productId)" v-hasPermi="['price:product:remove']">删除</el-button>
|
||||
</template>
|
||||
</basic-table>
|
||||
|
||||
@@ -123,7 +123,8 @@
|
||||
</el-dialog>
|
||||
|
||||
<el-drawer v-model="countryOpen" size="920px" :title="countryTitle" append-to-body>
|
||||
<el-table v-loading="countryLoading" :data="countryList" border empty-text="暂无国家明细">
|
||||
<el-input v-model="countryKeyword" placeholder="搜索国家(中文名 / ISO2)" clearable class="mb8" style="width: 260px" />
|
||||
<el-table v-loading="countryLoading" :data="filteredCountryList" border :span-method="countrySpanMethod" max-height="calc(100vh - 120px)" empty-text="暂无国家明细">
|
||||
<el-table-column label="国家" prop="countryName" width="90" show-overflow-tooltip />
|
||||
<el-table-column label="ISO2" prop="countryCode" width="60" align="center" />
|
||||
<el-table-column label="重量段(KG)" width="120" align="center">
|
||||
@@ -173,6 +174,15 @@ const countryOpen = ref(false)
|
||||
const countryTitle = ref('')
|
||||
const countryLoading = ref(false)
|
||||
const countryList = ref([])
|
||||
const countryKeyword = ref('')
|
||||
const filteredCountryList = computed(() => {
|
||||
const kw = countryKeyword.value.trim().toLowerCase()
|
||||
if (!kw) return countryList.value
|
||||
return countryList.value.filter(r =>
|
||||
String(r.countryName || '').toLowerCase().includes(kw) ||
|
||||
String(r.countryCode || '').toLowerCase().includes(kw)
|
||||
)
|
||||
})
|
||||
|
||||
const columns = [
|
||||
{ prop: 'productCode', label: '产品代码', minWidth: 110 },
|
||||
@@ -225,6 +235,15 @@ function queryList(params) {
|
||||
return listProduct(params)
|
||||
}
|
||||
|
||||
// 判断产品是否"生效中":status='0'(未停用)且 effectiveDate <= 今天
|
||||
function isProductActive(row) {
|
||||
if (!row) return false
|
||||
if (row.status !== '0') return false
|
||||
if (!row.effectiveDate) return false
|
||||
const today = new Date().toISOString().slice(0, 10)
|
||||
return String(row.effectiveDate).slice(0, 10) <= today
|
||||
}
|
||||
|
||||
function reset() {
|
||||
form.value = {
|
||||
productId: undefined,
|
||||
@@ -249,6 +268,10 @@ function handleAdd() {
|
||||
}
|
||||
|
||||
function handleUpdate(row) {
|
||||
if (isProductActive(row)) {
|
||||
proxy.$modal.msgError('生效中的产品不可编辑')
|
||||
return
|
||||
}
|
||||
reset()
|
||||
getProduct(row.productId).then(res => {
|
||||
form.value = res.data
|
||||
@@ -288,12 +311,31 @@ function openCountries(row) {
|
||||
countryOpen.value = true
|
||||
countryLoading.value = true
|
||||
countryList.value = []
|
||||
countryKeyword.value = ''
|
||||
listProductCountries(row.productCode).then(res => {
|
||||
countryList.value = res.data || []
|
||||
// 按 ISO2 排序,保证相同国家相邻,便于合并单元格
|
||||
const list = (res.data || []).slice().sort((a, b) => {
|
||||
return String(a.countryCode || '').localeCompare(String(b.countryCode || ''))
|
||||
})
|
||||
countryList.value = list
|
||||
}).finally(() => {
|
||||
countryLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
// 弹窗表格合并:相同 countryCode 的连续行合并「国家」「ISO2」列
|
||||
function countrySpanMethod({ column, rowIndex }) {
|
||||
if (column.property !== 'countryName' && column.property !== 'countryCode') return
|
||||
const list = filteredCountryList.value
|
||||
if (!list.length) return
|
||||
const cur = list[rowIndex]?.countryCode
|
||||
if (rowIndex > 0 && list[rowIndex - 1]?.countryCode === cur) {
|
||||
return { rowspan: 0, colspan: 0 }
|
||||
}
|
||||
let count = 1
|
||||
while (rowIndex + count < list.length && list[rowIndex + count]?.countryCode === cur) count++
|
||||
return { rowspan: count, colspan: 1 }
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
<el-descriptions-item label="运费单价">{{ formatMoney(result.freightRate) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="折扣前单价">{{ formatMoney(result.baseFreightRate) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="折扣%">{{ result.discountPercent != null ? result.discountPercent : '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="取值方式">{{ roundModeLabel(result.roundMode) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="运费金额">{{ formatMoney(result.freightAmount) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="挂号费">{{ formatMoney(result.registrationFee) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="应付总额">
|
||||
@@ -238,6 +239,15 @@ function sourceLabel(source) {
|
||||
return map[source] || source || '-'
|
||||
}
|
||||
|
||||
function roundModeLabel(mode) {
|
||||
const map = {
|
||||
UP: '向上取值',
|
||||
DOWN: '向下取整',
|
||||
HALF_UP: '四舍五入'
|
||||
}
|
||||
return map[mode] || mode || '四舍五入'
|
||||
}
|
||||
|
||||
function sourceTagType(source) {
|
||||
if (source === 'PACKAGE_FIXED' || source === 'FIXED') return 'success'
|
||||
if (source === 'PACKAGE_DISCOUNT' || source === 'DISCOUNT') return 'warning'
|
||||
|
||||
@@ -157,6 +157,13 @@
|
||||
<el-input-number v-model="row.discountValue" :min="1" :max="99" :precision="2" controls-position="right" style="width: 100%" placeholder="空=默认折扣" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="取值方式" min-width="130">
|
||||
<template #default="{ row }">
|
||||
<el-select v-model="row.roundMode" placeholder="取值方式" style="width: 100%">
|
||||
<el-option v-for="d in bms_round_mode" :key="d.value" :label="d.label" :value="d.value" />
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="80" align="center">
|
||||
<template #default="{ $index }">
|
||||
<el-button link type="danger" @click="form.details.splice($index, 1)">删除</el-button>
|
||||
@@ -248,6 +255,7 @@ import { getPriceSet, savePriceSet, validatePriceSet } from '@/api/price/set'
|
||||
import { listProduct, listProductCountries } from '@/api/price/product'
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const { bms_round_mode } = useDict('bms_round_mode')
|
||||
const route = useRoute()
|
||||
const active = ref(0)
|
||||
const saving = ref(false)
|
||||
@@ -366,7 +374,8 @@ function emptyDetail() {
|
||||
productCountryId: undefined,
|
||||
fvValue: undefined,
|
||||
fvRegistrationFee: undefined,
|
||||
discountValue: undefined
|
||||
discountValue: undefined,
|
||||
roundMode: 'HALF_UP'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -430,6 +439,7 @@ function onWeightChange(row) {
|
||||
function hydrateDetailRow(d) {
|
||||
const row = {
|
||||
...d,
|
||||
roundMode: d.roundMode || 'HALF_UP',
|
||||
_countryKey: countryKeyOf(d)
|
||||
}
|
||||
// normalize historical weight labels to min≤W≤max when possible
|
||||
@@ -463,7 +473,8 @@ function buildPayload(submitFlag) {
|
||||
productCountryId: d.productCountryId || undefined,
|
||||
fvValue: d.fvValue,
|
||||
fvRegistrationFee: d.fvRegistrationFee,
|
||||
discountValue: d.discountValue
|
||||
discountValue: d.discountValue,
|
||||
roundMode: d.roundMode || 'HALF_UP'
|
||||
}))
|
||||
const customers = (form.customers || [])
|
||||
.filter(c => c.customerId)
|
||||
|
||||
@@ -76,6 +76,9 @@
|
||||
<el-table-column v-if="set.setType === 'DISCOUNT'" label="产品折扣" min-width="110">
|
||||
<template #default="{ row }">{{ row.discountValue != null ? row.discountValue + '%' : '默认折扣' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="取值方式" min-width="110" align="center">
|
||||
<template #default="{ row }">{{ roundModeLabel(row.roundMode) }}</template>
|
||||
</el-table-column>
|
||||
<!-- <el-table-column label="关联 pr_id" prop="prId" width="110">
|
||||
<template #default="{ row }">{{ row.prId || '-' }}</template>
|
||||
</el-table-column> -->
|
||||
@@ -175,6 +178,11 @@
|
||||
<el-form-item v-else label="产品折扣%" prop="discountValue">
|
||||
<el-input-number v-model="detailForm.discountValue" :min="1" :max="99" controls-position="right" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="取值方式" prop="roundMode">
|
||||
<el-select v-model="detailForm.roundMode" placeholder="请选择" style="width: 100%">
|
||||
<el-option v-for="d in bms_round_mode" :key="d.value" :label="d.label" :value="d.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<!-- <el-form-item label="关联 pr_id" prop="prId">
|
||||
<el-input v-model="detailForm.prId" placeholder="可选" />
|
||||
</el-form-item> -->
|
||||
@@ -221,6 +229,7 @@ import { listProduct, listProductCountries } from '@/api/price/product'
|
||||
import { parseTime } from '@/utils/ruoyi'
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const { bms_round_mode } = useDict('bms_round_mode')
|
||||
const route = useRoute()
|
||||
const loading = ref(false)
|
||||
const set = ref({})
|
||||
@@ -232,7 +241,8 @@ const detailTitle = ref('')
|
||||
const detailFormRef = ref()
|
||||
const detailForm = ref({})
|
||||
const detailRules = {
|
||||
productCode: [{ required: true, message: '请选择产品', trigger: 'change' }]
|
||||
productCode: [{ required: true, message: '请选择产品', trigger: 'change' }],
|
||||
roundMode: [{ required: true, message: '请选择取值方式', trigger: 'change' }]
|
||||
}
|
||||
|
||||
const customerOpen = ref(false)
|
||||
@@ -360,6 +370,11 @@ function onDetailWeightChange() {
|
||||
|
||||
const childEditable = computed(() => ['D', 'R', 'E'].includes(set.value.status))
|
||||
|
||||
function roundModeLabel(mode) {
|
||||
const hit = (bms_round_mode.value || []).find(d => d.value === mode)
|
||||
return hit ? hit.label : (mode || '四舍五入')
|
||||
}
|
||||
|
||||
const statusLabel = computed(() => {
|
||||
if (set.value.expired) return '已过期'
|
||||
const map = { D: '草稿', A: '审核中', E: '生效中', R: '驳回', S: '已停用' }
|
||||
@@ -438,6 +453,7 @@ function onCopy() {
|
||||
function openDetailDialog(row) {
|
||||
detailForm.value = row ? {
|
||||
...row,
|
||||
roundMode: row.roundMode || 'HALF_UP',
|
||||
_countryKey: countryKeyOf(row)
|
||||
} : {
|
||||
priceSetId: set.value.priceSetId,
|
||||
@@ -450,7 +466,8 @@ function openDetailDialog(row) {
|
||||
productCountryId: undefined,
|
||||
fvValue: undefined,
|
||||
fvRegistrationFee: undefined,
|
||||
discountValue: undefined
|
||||
discountValue: undefined,
|
||||
roundMode: 'HALF_UP'
|
||||
}
|
||||
detailTitle.value = row ? '编辑明细' : '添加明细'
|
||||
detailOpen.value = true
|
||||
|
||||
Reference in New Issue
Block a user