1、新增结算模块
2、调整业务模块 3、调整客商模块
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" :title="readonly ? '计费方案详情' : index < 0 ? '添加计费方案' : '编辑计费方案'" width="92%" append-to-body destroy-on-close>
|
||||
<el-form ref="formRef" :model="draft" :rules="formRules" label-position="right" label-width="auto">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8"><el-form-item label="方案名称" prop="planName"><el-input v-model="draft.planName" maxlength="100" :disabled="readonly" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="默认方案"><el-checkbox v-model="draft.defaultPlan" :disabled="readonly">默认方案</el-checkbox></el-form-item></el-col>
|
||||
<el-col :span="24"><el-form-item label="备注"><el-input v-model="draft.remark" maxlength="100" show-word-limit :disabled="readonly" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<div class="rule-head"><el-link v-if="!readonly" type="primary" @click="addRule">+添加规则</el-link></div>
|
||||
<el-table :data="draft.rules" border>
|
||||
<el-table-column type="index" label="序号" width="65" />
|
||||
<el-table-column label="费用类型" width="180"><template #default="{ row }"><el-select v-model="row.feeType" clearable filterable :disabled="readonly" :loading="feeCategoryLoading" @change="value => handleFeeTypeChange(row, value)"><el-option v-for="item in feeCategories" :key="item.dictKey" :label="item.dictValue" :value="item.dictKey" /></el-select></template></el-table-column>
|
||||
<el-table-column label="费用项" width="210"><template #default="{ row }"><el-select v-model="row.feeItem" clearable filterable :disabled="readonly || !row.feeType" :loading="feeItemLoadingMap[feeTypeKey(row)]"><el-option v-for="item in feeItemOptions(row)" :key="item.id || item.name || item.englishName" :label="item.name || item.englishName" :value="item.name || item.englishName" /></el-select></template></el-table-column>
|
||||
<el-table-column label="计费要素" width="170"><template #default="{ row }"><el-select v-model="row.billingElement" :disabled="readonly" placeholder="请选择" @change="handleElementChange(row)"><el-option v-for="item in billingElements" :key="item" :label="item" :value="item" /></el-select></template></el-table-column>
|
||||
<el-table-column label="计费类型" width="180"><template #default="{ row }"><el-select v-model="row.billingType" :disabled="readonly || !row.billingElement" placeholder="请选择" @change="handleTypeChange(row)"><el-option v-for="item in billingTypes(row)" :key="item" :label="item" :value="item" /></el-select></template></el-table-column>
|
||||
<el-table-column label="计费单位" width="150"><template #default="{ row }"><el-select v-model="row.billingUnit" clearable filterable :disabled="readonly" :loading="unitLoading"><el-option v-for="item in unitOptions" :key="item.id || item.dictKey || item.dictValue" :label="item.dictValue" :value="item.dictValue" /></el-select></template></el-table-column>
|
||||
<el-table-column label="单价(元)" width="180"><template #default="{ row }"><div v-if="usesRangeUnitPrice(row)" class="limit-list"><el-input v-for="(item, rangeIndex) in getRanges(row)" :key="rangeIndex" v-model="item.unitPrice" :disabled="readonly" placeholder="请输入单价" @input="value => rangeUnitPriceInput(row, rangeIndex, value)" /></div><el-input v-else v-model="row.unitPrice" :disabled="readonly" @input="value => decimalInput(row, 'unitPrice', value)" /></template></el-table-column>
|
||||
<el-table-column label="计费要素下限" width="250"><template #default="{ row }"><div class="limit-list"><el-input v-for="(item, rangeIndex) in getRanges(row)" :key="rangeIndex" v-model="item.lowerLimit" :disabled="!canEditLimit(row)" :placeholder="canEditLimit(row) ? '请输入下限' : '无需配置'" @input="value => limitInput(row, rangeIndex, 'lowerLimit', value)" /></div></template></el-table-column>
|
||||
<el-table-column label="计费要素上限" width="270"><template #default="{ row }"><div class="limit-list"><div v-for="(item, rangeIndex) in getRanges(row)" :key="rangeIndex" class="limit-row"><el-input v-model="item.upperLimit" :disabled="!canEditLimit(row)" :placeholder="canEditLimit(row) ? '请输入上限' : '无需配置'" @input="value => limitInput(row, rangeIndex, 'upperLimit', value)" /><el-link v-if="canEditLimit(row)" type="primary" @click="addRange(row, rangeIndex)">添加</el-link><el-link v-if="canEditLimit(row) && getRanges(row).length > 1" type="danger" @click="removeRange(row, rangeIndex)">删除</el-link></div></div></template></el-table-column>
|
||||
<el-table-column label="保底计费重量" width="240"><template #default="{ row }"><el-input v-model="row.minimumBillingWeight" :disabled="!canEditMinimum(row)" :placeholder="canEditMinimum(row) ? '实际计量值小于保底数值时按保底计费' : '仅按重量、按吨·公里可填写'" @input="value => decimalInput(row, 'minimumBillingWeight', value)" /></template></el-table-column>
|
||||
<el-table-column label="备注" width="180"><template #default="{ row }"><el-input v-model="row.remark" maxlength="100" show-word-limit :disabled="readonly" /></template></el-table-column>
|
||||
<el-table-column label="操作" width="250" fixed="right"><template #default="{ row, $index }"><el-link type="primary" @click="openMatch(row, $index)">{{ readonly ? '查看匹配条件' : '设置匹配条件' }}</el-link><el-link v-if="!readonly" type="primary" @click="copyRule(row)">复制</el-link><el-link v-if="!readonly" type="danger" @click="removeRule($index)">删除</el-link></template></el-table-column>
|
||||
</el-table>
|
||||
<template #footer><el-button v-if="readonly" type="primary" @click="visible = false">关闭</el-button><template v-else><el-button @click="visible = false">取消</el-button><el-button type="primary" @click="save">提交</el-button></template></template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="matchVisible" title="设置匹配条件" append-to-body width="720px">
|
||||
<el-form :model="matchForm" label-position="right" label-width="90px">
|
||||
<el-row :gutter="48">
|
||||
<el-col :span="12"><el-form-item label="起运地"><el-cascader v-model="matchForm.originPath" :options="regionOptions" :props="regionProps" :placeholder="matchForm.origin || '请选择起运地'" :loading="regionLoading" :disabled="readonly" clearable filterable @visible-change="v => v && ensureRegions()" @change="v => matchRegionChange('origin', v)" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="目的地"><el-cascader v-model="matchForm.destinationPath" :options="regionOptions" :props="regionProps" :placeholder="matchForm.destination || '请选择目的地'" :loading="regionLoading" :disabled="readonly" clearable filterable @visible-change="v => v && ensureRegions()" @change="v => matchRegionChange('destination', v)" /></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="运输方式"><el-select v-model="matchForm.transportMode" :disabled="readonly" clearable><el-option label="公路" value="公路" /><el-option label="铁路" value="铁路" /><el-option label="水路" value="水路" /></el-select></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="货物类型"><el-cascader v-model="matchForm.cargoTypePath" :options="cargoOptions" :props="cargoProps" :placeholder="matchForm.cargoType || '请选择货物类型'" :loading="cargoLoading" :disabled="readonly" clearable filterable :filter-method="filterCargo" @visible-change="v => v && ensureCargo()" @change="matchCargoChange" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<template #footer><el-button v-if="readonly" type="primary" @click="matchVisible = false">关闭</el-button><template v-else><el-button @click="matchVisible = false">取消</el-button><el-button type="primary" @click="saveMatch">保存</el-button></template></template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getList as getCargoTypeList } from '@/api/base/cargo-type';
|
||||
import { getList as getFeeItemList } from '@/api/base/fee-item';
|
||||
import { getLazyTree as getRegionLazyTree } from '@/api/base/region';
|
||||
import { getDictionary } from '@/api/system/dictbiz';
|
||||
|
||||
const clone = value => JSON.parse(JSON.stringify(value));
|
||||
const defaultRule = () => ({ feeType: '', feeItem: '', billingElement: '', billingType: '', billingUnit: '', unitPrice: '', lowerLimit: '', upperLimit: '', limitRanges: [{ lowerLimit: '', upperLimit: '', unitPrice: '' }], minimumBillingWeight: '', remark: '', matchCondition: { origin: '', originCode: '', originPath: [], destination: '', destinationCode: '', destinationPath: [], transportMode: '', cargoType: '', cargoTypeCode: '', cargoTypePath: [] } });
|
||||
|
||||
export default {
|
||||
props: { modelValue: Boolean, value: { type: Object, default: () => ({}) }, index: { type: Number, default: -1 }, readonly: Boolean },
|
||||
emits: ['update:modelValue', 'save'],
|
||||
data() { return { draft: this.normalizePlan(this.value), feeCategories: [], feeCategoryLoading: false, feeItems: {}, feeItemLoadingMap: {}, unitOptions: [], unitLoading: false, billingElements: ['按重量', '按体积', '按车辆', '按里程', '按吨·公里', '固定金额(整单一口价)', '按数量'], typeMap: { 按重量: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'], 按体积: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'], 按车辆: ['固定单价'], 按里程: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'], '按吨·公里': ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'], '固定金额(整单一口价)': ['固定一口价'], 按数量: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'] }, matchVisible: false, matchIndex: -1, matchForm: defaultRule().matchCondition, regionOptions: [], regionLoading: false, regionRequest: null, cargoOptions: [], cargoFlatOptions: [], cargoLoading: false, cargoRequest: null, formRules: { planName: [{ required: true, message: '请输入方案名称', trigger: 'blur' }] } }; },
|
||||
computed: { visible: { get() { return this.modelValue; }, set(value) { this.$emit('update:modelValue', value); } }, regionProps() { return { label: 'title', value: 'id', children: 'children', leaf: 'leaf', emitPath: true }; }, cargoProps() { return { label: 'cargoName', value: 'id', children: 'children', disabled: (data, node) => node.level === 1, leaf: 'leaf', checkStrictly: true, emitPath: true }; } },
|
||||
mounted() { this.loadDictionaries(); },
|
||||
watch: { value: { deep: true, handler(value) { if (this.modelValue) { this.draft = this.normalizePlan(value); this.loadRuleItems(); } } } },
|
||||
methods: {
|
||||
normalizePlan(value = {}) { const plan = { planName: '', defaultPlan: false, remark: '', ...clone(value || {}) }; plan.rules = (value?.rules?.length ? value.rules : [defaultRule()]).map(rule => this.normalizeRule(rule)); return plan; },
|
||||
normalizeRule(rule = {}) { const next = { ...defaultRule(), ...clone(rule) }; next.limitRanges = this.normalizeRanges(next); this.syncLegacyLimit(next); next.matchCondition = { ...defaultRule().matchCondition, ...(rule.matchCondition || {}) }; return next; },
|
||||
normalizeRanges(row) { const ranges = Array.isArray(row.limitRanges) ? row.limitRanges.map(item => ({ lowerLimit: item?.lowerLimit ?? '', upperLimit: item?.upperLimit ?? '', unitPrice: item?.unitPrice === undefined || item?.unitPrice === '' ? row.unitPrice ?? '' : item.unitPrice })).filter(item => item.lowerLimit !== '' || item.upperLimit !== '' || item.unitPrice !== '') : []; if (!ranges.length && (row.lowerLimit !== '' || row.upperLimit !== '' || row.unitPrice !== '')) ranges.push({ lowerLimit: row.lowerLimit ?? '', upperLimit: row.upperLimit ?? '', unitPrice: row.unitPrice ?? '' }); return ranges.length ? ranges : [{ lowerLimit: '', upperLimit: '', unitPrice: '' }]; },
|
||||
syncLegacyLimit(row) { const first = row.limitRanges?.[0] || {}; row.lowerLimit = first.lowerLimit || ''; row.upperLimit = first.upperLimit || ''; if (this.usesRangeUnitPrice(row)) row.unitPrice = first.unitPrice || ''; },
|
||||
loadDictionaries() { this.feeCategoryLoading = true; getDictionary({ code: 'fee_category' }).then(res => { this.feeCategories = res.data?.data || []; this.loadRuleItems(); }).finally(() => { this.feeCategoryLoading = false; }); this.unitLoading = true; getDictionary({ code: 'unit_fee' }).then(res => { this.unitOptions = res.data?.data || []; }).finally(() => { this.unitLoading = false; }); },
|
||||
feeTypeKey(row) { const option = this.feeCategories.find(item => String(item.dictKey) === String(row.feeType) || String(item.dictValue) === String(row.feeType)); return option?.dictKey || row.feeType || ''; },
|
||||
feeItemOptions(row) { return this.feeItems[this.feeTypeKey(row)] || []; },
|
||||
loadRuleItems() { (this.draft.rules || []).forEach(row => this.loadFeeItems(row.feeType)); },
|
||||
loadFeeItems(value) { const key = this.feeTypeKey({ feeType: value }); if (!key || this.feeItems[key]) return; this.feeItemLoadingMap[key] = true; getFeeItemList(1, 9999, { feeCategory: key }).then(res => { const data = res?.data?.data || res?.data || {}; this.feeItems[key] = Array.isArray(data) ? data : (data.records || []); }).finally(() => { this.feeItemLoadingMap[key] = false; }); },
|
||||
handleFeeTypeChange(row, value) { row.feeType = this.feeTypeKey({ feeType: value }); row.feeItem = ''; this.loadFeeItems(row.feeType); },
|
||||
billingTypes(row) { return this.typeMap[row.billingElement] || []; },
|
||||
handleElementChange(row) { if (!this.billingTypes(row).includes(row.billingType)) row.billingType = ''; if (!this.canEditMinimum(row)) row.minimumBillingWeight = ''; this.handleTypeChange(row); },
|
||||
handleTypeChange(row) { if (!this.canEditLimit(row)) { row.limitRanges = [{ lowerLimit: '', upperLimit: '', unitPrice: '' }]; row.lowerLimit = ''; row.upperLimit = ''; } else { row.limitRanges = this.normalizeRanges(row); this.syncLegacyLimit(row); } },
|
||||
canEditLimit(row) { return !this.readonly && Boolean(row.billingElement) && Boolean(row.billingType) && (row.billingType.includes('区间') || row.billingType.includes('阶梯')); },
|
||||
usesRangeUnitPrice(row) { return ['区间单价', '阶梯单价'].includes(row.billingType); },
|
||||
canEditMinimum(row) { return !this.readonly && ['按重量', '按吨·公里'].includes(row.billingElement); },
|
||||
getRanges(row) { return Array.isArray(row.limitRanges) && row.limitRanges.length ? row.limitRanges : [{ lowerLimit: '', upperLimit: '', unitPrice: '' }]; },
|
||||
addRange(row, index) { row.limitRanges = this.getRanges(row); row.limitRanges.splice(index + 1, 0, { lowerLimit: '', upperLimit: '', unitPrice: '' }); },
|
||||
removeRange(row, index) { row.limitRanges = this.getRanges(row); row.limitRanges.splice(index, 1); if (!row.limitRanges.length) row.limitRanges.push({ lowerLimit: '', upperLimit: '', unitPrice: '' }); this.syncLegacyLimit(row); },
|
||||
limitInput(row, index, prop, value) { row.limitRanges = this.getRanges(row); const text = String(value || '').replace(/[^\d.]/g, ''); const parts = text.split('.'); row.limitRanges[index][prop] = parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 2)}` : parts[0]; this.syncLegacyLimit(row); },
|
||||
decimalInput(row, prop, value) { const text = String(value || '').replace(/[^\d.]/g, ''); const parts = text.split('.'); row[prop] = parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 2)}` : parts[0]; },
|
||||
rangeUnitPriceInput(row, index, value) { row.limitRanges = this.getRanges(row); const target = { value }; this.decimalInput(target, 'value', value); row.limitRanges[index].unitPrice = target.value; if (index === 0) row.unitPrice = target.value; },
|
||||
addRule() { this.draft.rules.push(defaultRule()); },
|
||||
copyRule(row) { const next = clone(row); this.draft.rules.push(this.normalizeRule(next)); this.loadFeeItems(next.feeType); },
|
||||
removeRule(index) { this.draft.rules.splice(index, 1); if (!this.draft.rules.length) this.addRule(); },
|
||||
validateRules() { const groups = {}; const feeItemSet = new Set(); const required = [['feeType', '费用类型'], ['feeItem', '费用项'], ['billingElement', '计费要素'], ['billingType', '计费类型'], ['billingUnit', '计费单位']]; for (const [i, row] of this.draft.rules.entries()) { const empty = required.find(([key]) => row[key] === undefined || row[key] === null || String(row[key]).trim() === ''); if (empty) { this.$message.warning(`第${i + 1}行${empty[1]}不能为空`); return false; } const feeItem = String(row.feeItem).trim(); if (feeItemSet.has(feeItem)) { this.$message.warning(`费用项“${feeItem}”不能重复`); return false; } feeItemSet.add(feeItem); if (!this.usesRangeUnitPrice(row) && (row.unitPrice === undefined || row.unitPrice === null || String(row.unitPrice).trim() === '')) { this.$message.warning(`第${i + 1}行单价不能为空`); return false; } if (!this.billingTypes(row).includes(row.billingType)) { this.$message.warning('请选择计费要素对应的计费类型'); return false; } if (!this.canEditLimit(row)) continue; const key = row.billingElement; groups[key] = groups[key] || []; for (const range of this.getRanges(row)) { const lower = Number(range.lowerLimit); const upper = Number(range.upperLimit); if (range.lowerLimit === '' || range.upperLimit === '' || Number.isNaN(lower) || Number.isNaN(upper)) { this.$message.warning('请完整填写计费要素上下限'); return false; } if (this.usesRangeUnitPrice(row) && (range.unitPrice === undefined || range.unitPrice === null || String(range.unitPrice).trim() === '')) { this.$message.warning('请为每组计费要素区间填写单价'); return false; } if (lower > upper) { this.$message.warning('计费要素下限不能大于上限'); return false; } groups[key].push({ lower, upper }); } } return this.validateRangeGroups(groups); },
|
||||
validateRangeGroups(groups) { const precision = 0.000001; for (const [element, ranges] of Object.entries(groups)) { const sorted = [...ranges].sort((a, b) => a.lower - b.lower || a.upper - b.upper); for (let i = 1; i < sorted.length; i += 1) { if (sorted[i].lower < sorted[i - 1].upper - precision) { this.$message.warning(`${element}的计费要素区间不能重叠`); return false; } if (sorted[i].lower > sorted[i - 1].upper + precision) { this.$message.warning(`${element}的计费要素区间必须连续,不能存在间隙`); return false; } } } return true; },
|
||||
save() { this.$refs.formRef.validate(valid => { if (!valid || !this.validateRules()) return; const plan = clone(this.draft); plan.rules = plan.rules.map(rule => { const ranges = this.canEditLimit(rule) ? this.getRanges(rule) : []; return { ...rule, unitPrice: this.usesRangeUnitPrice(rule) ? ranges[0]?.unitPrice || '' : rule.unitPrice, limitRanges: ranges, lowerLimit: ranges[0]?.lowerLimit || '', upperLimit: ranges[0]?.upperLimit || '', minimumBillingWeight: this.canEditMinimum(rule) ? rule.minimumBillingWeight : '' }; }); this.$emit('save', plan, this.index); this.visible = false; }); },
|
||||
openMatch(row, index) { this.matchIndex = index; this.matchForm = { ...defaultRule().matchCondition, ...(row.matchCondition || {}) }; this.matchVisible = true; this.ensureRegions(); this.ensureCargo().then(() => { const path = this.resolveCargoPath(this.matchForm); if (path.length) this.matchCargoChange(path); }); },
|
||||
ensureRegions() { if (this.regionOptions.length) return Promise.resolve(this.regionOptions); if (this.regionRequest) return this.regionRequest; this.regionLoading = true; this.regionRequest = getRegionLazyTree().then(res => { const raw = res.data?.data || []; this.regionOptions = this.normalizeRegionTree(raw); return this.regionOptions; }).finally(() => { this.regionLoading = false; this.regionRequest = null; }); return this.regionRequest; },
|
||||
normalizeRegionTree(list) { const map = new Map(); const flat = []; const walk = items => (items || []).forEach(item => { flat.push({ ...item, children: undefined }); if (item.children?.length) walk(item.children); }); walk(list); flat.forEach(item => map.set(String(item.id ?? item.code ?? item.value), { ...item, id: String(item.id ?? item.code ?? item.value), parentId: String(item.parentId ?? item.parentCode ?? ''), children: [] })); const roots = []; map.forEach(node => { const parent = map.get(node.parentId); if (parent && parent !== node) parent.children.push(node); else roots.push(node); }); const china = roots.find(item => ['中国', '中华人民共和国'].includes(item.title || item.name)); const source = china?.children?.length ? china.children : roots; const normalize = (node, level) => ({ ...node, title: node.title || node.name || '', leaf: level === 2, children: level === 1 ? (node.children || []).map(child => normalize(child, 2)) : undefined }); return source.map(node => normalize(node, 1)); },
|
||||
regionLabels(path) { let options = this.regionOptions; const labels = []; (path || []).forEach(value => { const option = options.find(item => String(item.id) === String(value)); if (option) { labels.push(option.title || option.name || ''); options = option.children || []; } }); return labels; },
|
||||
matchRegionChange(prop, value) { const path = Array.isArray(value) && value.length >= 2 ? value.slice(0, 2) : []; this.matchForm[`${prop}Path`] = path; const labels = this.regionLabels(path); this.matchForm[prop] = labels.join(''); this.matchForm[`${prop}Code`] = path.length ? String(path[path.length - 1]) : ''; },
|
||||
ensureCargo() { if (this.cargoOptions.length) return Promise.resolve(this.cargoOptions); if (this.cargoRequest) return this.cargoRequest; this.cargoLoading = true; this.cargoRequest = getCargoTypeList(1, 9999).then(res => { const data = res?.data?.data || res?.data || {}; const records = Array.isArray(data) ? data : (data.records || []); this.cargoOptions = this.buildCargoTree(records); this.cargoFlatOptions = this.flatten(this.cargoOptions); return this.cargoOptions; }).finally(() => { this.cargoLoading = false; this.cargoRequest = null; }); return this.cargoRequest; },
|
||||
buildCargoTree(records) { const flat = []; const walk = items => (items || []).forEach(item => { flat.push(item); if (item.children?.length) walk(item.children); }); walk(records); const map = new Map(); flat.forEach(item => { const id = String(item.id ?? item.cargoCode ?? item.code); map.set(id, { ...item, id, cargoName: item.cargoName || item.name || item.typeName || '', parentId: String(item.parentId ?? item.parentCode ?? ''), children: [] }); }); const roots = []; map.forEach(node => { const parent = map.get(node.parentId); if (parent && parent !== node && Number(node.typeLevel) !== 1) parent.children.push(node); else roots.push(node); }); const normalize = (node, level, parent = []) => { const path = [...parent, node.id]; const children = level === 1 ? node.children.map(item => normalize(item, 2, path)) : []; return { ...node, path, leaf: level === 2, children: children.length ? children : undefined }; }; return roots.map(node => normalize(node, 1)); },
|
||||
flatten(list) { const result = []; const walk = items => (items || []).forEach(item => { result.push(item); if (item.children?.length) walk(item.children); }); walk(list); return result; },
|
||||
filterCargo(node, keyword) { const text = String(keyword || '').trim(); return !text || String(node.label || node.text || node.cargoName || '').includes(text); },
|
||||
cargoLabels(path) { let options = this.cargoOptions; const labels = []; (path || []).forEach(value => { const option = options.find(item => String(item.id) === String(value)); if (option) { labels.push(option.cargoName || ''); options = option.children || []; } }); return labels; },
|
||||
matchCargoChange(value) { const path = Array.isArray(value) && value.length >= 2 ? value.slice(0, 2).map(item => String(item)) : []; const item = this.cargoFlatOptions.find(option => String(option.id) === String(path[1]) && option.path?.length >= 2); this.matchForm.cargoTypePath = path; this.matchForm.cargoType = item ? this.cargoLabels(path).join('/') : ''; this.matchForm.cargoTypeCode = item ? item.cargoCode || item.code || item.id : ''; },
|
||||
resolveCargoPath(condition) { if (Array.isArray(condition.cargoTypePath) && condition.cargoTypePath.length >= 2) return condition.cargoTypePath; const item = this.cargoFlatOptions.find(option => option.path?.length >= 2 && (String(option.cargoCode || option.code || option.id) === String(condition.cargoTypeCode) || option.cargoName === condition.cargoType)); return item?.path || []; },
|
||||
saveMatch() { const row = this.draft.rules[this.matchIndex]; if (row) { this.matchRegionChange('origin', this.matchForm.originPath); this.matchRegionChange('destination', this.matchForm.destinationPath); this.matchCargoChange(this.matchForm.cargoTypePath); row.matchCondition = clone(this.matchForm); } this.matchVisible = false; },
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped>.rule-head { margin: 12px 0; }.limit-list { display: flex; flex-direction: column; gap: 8px; }.limit-row { display: flex; align-items: center; gap: 8px; }.limit-row .el-input { flex: 1; }</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -144,7 +144,7 @@ export default {
|
||||
.segment-detail__header-right { display: flex; align-items: center; gap: 12px; }
|
||||
.segment-index { display: inline-flex; width: 36px; height: 36px; align-items: center; justify-content: center; border-radius: 50%; background: #2088ee; color: #fff; font-size: 20px; font-weight: 500; }
|
||||
.segment-execution { padding-top: 12px; }
|
||||
.segment-stats { display: flex; gap: 72px; padding: 4px 0 24px; div { display: flex; flex-direction: column; gap: 8px; } span { color: #8a9bb8; font-size: 14px; } strong { color: #1f2d3d; font-size: 32px; line-height: 1; } small { margin-left: 6px; color: #8a9bb8; font-size: 16px; font-weight: 400; } .dispatched { color: #16a34a; } .remaining { color: #f59e0b; } }
|
||||
.segment-stats { display: flex; gap: 72px; padding: 4px 0 24px; div { display: flex; flex-direction: column; gap: 8px; } span { color: #8a9bb8; font-size: 14px; } strong { color: #1f2d3d; font-size: 32px; line-height: 1; } small { margin-left: 6px; color: #8a9bb8; font-size: 16px; font-weight: 400; } .dispatched { color: #409eff; } .remaining { color: #f56c6c; } }
|
||||
.waybill-table { :deep(th.el-table__cell) { background: #f5f7fa; color: #60738f; } }
|
||||
.transport-plan-detail { margin-top: 20px; h4 { margin: 0 0 12px; padding-left: 12px; border-left: 4px solid #409eff; font-size: 15px; } }
|
||||
.master-goods-card { margin-bottom: 8px; border: 1px solid #eff1f7; background: #fff; }
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
<div><dt>客户</dt><dd>{{ master.customerName || '-' }}</dd></div>
|
||||
<div><dt>合同编号</dt><dd>{{ master.contractNo || '-' }}</dd></div>
|
||||
<div><dt>项目</dt><dd>{{ master.projectName || '-' }}</dd></div>
|
||||
<div><dt>货物名称</dt><dd>{{ goodsNames }}</dd></div>
|
||||
<div><dt>货物类型</dt><dd>{{ goodsTypes }}</dd></div>
|
||||
<div><dt>货物名称</dt><dd class="overview-meta__plain-value">{{ goodsNames }}</dd></div>
|
||||
<div><dt>货物类型</dt><dd class="overview-meta__plain-value">{{ goodsTypes }}</dd></div>
|
||||
<div><dt>运输周期</dt><dd>{{ dateRange }}</dd></div>
|
||||
</dl>
|
||||
<div class="route-overview">
|
||||
@@ -39,7 +39,19 @@
|
||||
<section v-for="(route, index) in routes" :key="route.segmentNo" class="dispatch-section" :data-segment="route.segmentNo">
|
||||
<header class="segment-header">
|
||||
<div>
|
||||
<el-checkbox v-model="route.selected" :label="`${route.segmentNo}:${route.departureName} → ${route.arrivalName}`" />
|
||||
<el-checkbox v-model="route.selected">
|
||||
<span class="segment-checkbox-label">
|
||||
<span>{{ route.segmentNo }}:</span>
|
||||
<el-tooltip
|
||||
:content="`${route.departureName || '-'} → ${route.arrivalName || '-'}`"
|
||||
placement="top"
|
||||
>
|
||||
<span class="segment-checkbox-path"
|
||||
>{{ route.departureName || '-' }} → {{ route.arrivalName || '-' }}</span
|
||||
>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</el-checkbox>
|
||||
</div>
|
||||
<div>总量 {{ formatQuantity(totalQuantity) }},已调度 <em>{{ formatQuantity(dispatchedQuantity(route)) }}</em>,剩余 <em>{{ formatQuantity(remaining(route)) }}</em></div>
|
||||
</header>
|
||||
@@ -49,13 +61,6 @@
|
||||
<el-col :span="8">
|
||||
<el-form-item label="运输方式" required class="transport-type-field"><el-input :model-value="route.transportType" readonly /></el-form-item>
|
||||
</el-col>
|
||||
<el-col v-if="isRoad(route)" :span="8">
|
||||
<el-form-item label="单据类型" required>
|
||||
<el-radio-group v-model="route.documentType">
|
||||
<el-radio value="计划单">计划单</el-radio><el-radio value="运单">运单</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
@@ -78,8 +83,8 @@
|
||||
<div class="goods-heading"><h3>货物信息</h3><div><el-link type="primary" @click="addGoods(route)">新增货物</el-link></div></div>
|
||||
<el-table :data="route.goods" border class="goods-table">
|
||||
<el-table-column type="index" label="序号" width="64" />
|
||||
<el-table-column label="货物类型" min-width="180"><template #default="{ row }"><el-cascader v-model="row.cargoTypePath" :options="cargoTypeOptions" :props="cargoTypeCascaderProps" placeholder="请选择货物类型" clearable filterable @change="value => handleCargoTypeChange(route, row, value)" /></template></el-table-column>
|
||||
<el-table-column label="货物名称" min-width="180"><template #default="{ row }"><el-select v-model="row.sourceIndex" placeholder="请选择总单货物" @change="selectGoods(route, row)"><el-option v-for="item in masterGoodsByCargoType(row)" :key="item.sourceIndex" :label="item.cargoName" :value="item.sourceIndex" /></el-select></template></el-table-column>
|
||||
<el-table-column label="货物类型" min-width="180"><template #default="{ row }"><el-cascader v-model="row.cargoTypePath" class="goods-table__cargo-type" :options="cargoTypeOptions" :props="cargoTypeCascaderProps" placeholder="请选择货物类型" clearable filterable @change="value => handleCargoTypeChange(route, row, value)" /></template></el-table-column>
|
||||
<el-table-column label="货物名称" min-width="180"><template #default="{ row }"><el-select v-model="row.sourceIndex" class="goods-table__cargo-name" placeholder="请选择总单货物" @change="selectGoods(route, row)"><el-option v-for="item in masterGoodsByCargoType(row)" :key="item.sourceIndex" :label="item.cargoName" :value="item.sourceIndex" /></el-select></template></el-table-column>
|
||||
<el-table-column prop="remainingQuantity" column-key="remainingQuantity" label="剩余数量" width="150" class-name="goods-table__remaining"><template #default="{ row }">{{ formatQuantity(goodsRemainingQuantity(route, row)) }}</template></el-table-column>
|
||||
<el-table-column prop="dispatchQuantity" column-key="dispatchQuantity" label="本次数量" width="200" class-name="goods-table__dispatch"><template #default="{ row }"><el-input :model-value="row.dispatchQuantity" inputmode="decimal" placeholder="请输入" @input="value => handleDispatchQuantityInput(route, row, value)" /></template></el-table-column>
|
||||
<el-table-column prop="quantityUnit" label="数量单位" width="110" />
|
||||
@@ -95,14 +100,14 @@
|
||||
<el-form :model="route" label-position="right" label-width="auto" class="dispatch-form freight-form">
|
||||
<el-row v-for="(item, index) in route.freightItems" :key="item.sourceIndex" :gutter="16">
|
||||
<el-col :span="6">
|
||||
<el-form-item :label="`单价${index + 1}`" required>
|
||||
<el-form-item :label="`单价${index + 1}`">
|
||||
<el-input :model-value="item.unitPrice" inputmode="decimal" placeholder="请输入" @input="value => handleFreightUnitPriceInput(item, value)">
|
||||
<template #append><el-select v-model="item.priceUnit" class="freight-unit-select"><el-option label="元/吨" value="元/吨" /><el-option label="元/件" value="元/件" /><el-option label="元/方" value="元/方" /></el-select></template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item :label="`数量${index + 1}`" required>
|
||||
<el-form-item :label="`数量${index + 1}`">
|
||||
<el-input :model-value="formatQuantity(item.quantity)" readonly>
|
||||
<template #append><el-select v-model="item.quantityUnit" class="freight-unit-select" @change="value => handleFreightQuantityUnitChange(route, item, value)"><el-option v-for="unit in quantityUnitOptions" :key="unit" :label="unit" :value="unit" /></el-select></template>
|
||||
</el-input>
|
||||
@@ -142,7 +147,7 @@
|
||||
<el-col :span="6"><el-form-item label="箱号" required><el-input v-model="route.containerNo" placeholder="请输入" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="舱位" required><el-input v-model="route.cabinNo" placeholder="请输入" /></el-form-item></el-col>
|
||||
</template>
|
||||
<el-col :span="6"><el-form-item label="里程(km)" required><el-input :model-value="route.mileage" inputmode="numeric" maxlength="10" placeholder="请输入" @input="value => handleMileageInput(route, value)" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="里程(km)"><el-input :model-value="route.mileage" inputmode="numeric" maxlength="10" placeholder="请输入" @input="value => handleMileageInput(route, value)" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="备注"><el-input v-model="route.remark" maxlength="200" show-word-limit placeholder="请输入" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
@@ -433,9 +438,7 @@ export default {
|
||||
validateFreightItems(route) {
|
||||
for (const [index, item] of (route.freightItems || []).entries()) {
|
||||
const fields = [
|
||||
['unitPrice', '单价'],
|
||||
['priceUnit', '单价单位'],
|
||||
['quantity', '数量'],
|
||||
['quantityUnit', '数量单位'],
|
||||
];
|
||||
const emptyField = fields.find(
|
||||
@@ -590,10 +593,10 @@ export default {
|
||||
if (!this.validateRoutePhones(route)) return;
|
||||
if (route.documentType === '运单') {
|
||||
if (this.isRoad(route)) {
|
||||
if (route.carrierType === '承运商' && (!route.carrierName || !route.vehicleNo || !route.mileage)) return this.$message.warning('请填写承运商、车牌号和里程');
|
||||
if (route.carrierType !== '承运商' && (!route.driverName || !route.driverPhone || !route.vehicleNo || !route.trailerVehicleNo || !route.escortName || !route.escortPhone || route.mileage === undefined || route.mileage === null || route.mileage === '')) return this.$message.warning('请补全自运或网货平台的车辆与人员信息');
|
||||
} else if (!route.vehicleNo || !route.captainName || !route.driverPhone || !route.containerNo || !route.cabinNo || route.mileage === undefined || route.mileage === null || route.mileage === '' || (route.carrierType === '承运商' && !route.carrierName)) {
|
||||
return this.$message.warning('请补全非公路运输的承运信息和里程');
|
||||
if (route.carrierType === '承运商' && (!route.carrierName || !route.vehicleNo)) return this.$message.warning('请填写承运商和车牌号');
|
||||
if (route.carrierType !== '承运商' && (!route.driverName || !route.driverPhone || !route.vehicleNo || !route.trailerVehicleNo || !route.escortName || !route.escortPhone)) return this.$message.warning('请补全自运或网货平台的车辆与人员信息');
|
||||
} else if (!route.vehicleNo || !route.captainName || !route.driverPhone || !route.containerNo || !route.cabinNo || (route.carrierType === '承运商' && !route.carrierName)) {
|
||||
return this.$message.warning('请补全非公路运输的承运信息');
|
||||
}
|
||||
}
|
||||
const batchNo = `${route.segmentNo}-${Date.now()}`;
|
||||
@@ -666,9 +669,13 @@ export default {
|
||||
.pending-list-spacer { height: min(436px, calc(100vh - 130px)); }
|
||||
.master-overview, .dispatch-section { margin-bottom: 8px; border: 1px solid #eff1f7; background: #fff; }
|
||||
.overview-heading, .segment-header, .pending-bar { display: flex; align-items: center; justify-content: space-between; }
|
||||
.segment-header :deep(.el-checkbox.is-checked .el-checkbox__label) { font-size: 16px; font-weight: 700; }
|
||||
.segment-checkbox-label { display: inline-flex; align-items: center; min-width: 0; }
|
||||
.segment-checkbox-path { display: inline-block; white-space: nowrap; }
|
||||
.overview-heading { padding: 18px 24px 12px; h2 { display: inline-block; margin: 0 16px 0 0; font-size: 20px; } h2 span { margin: 0 8px; color: #909399; font-weight: 400; } }
|
||||
.overview-content { display: grid; grid-template-columns: minmax(400px, .9fr) minmax(620px, 1.6fr); gap: 28px; padding: 8px 24px 20px; }
|
||||
.overview-meta { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 18px 24px; margin: 0; dt { margin-bottom: 6px; color: #909399; font-size: 13px; } dd { margin: 0; color: #409eff; font-size: 14px; word-break: break-all; } }
|
||||
.overview-meta dd.overview-meta__plain-value { color: #303133; }
|
||||
.route-overview { display: flex; align-items: flex-start; justify-content: flex-end; padding-top: 8px; overflow-x: auto; }
|
||||
.route-overview__node { display: grid; flex: 0 0 130px; justify-items: center; gap: 6px; text-align: center; strong { font-size: 16px; white-space: nowrap; } small { color: #606266; white-space: nowrap; } }
|
||||
.route-badge { display: inline-flex; width: 32px; height: 32px; align-items: center; justify-content: center; border-radius: 4px; background: #67c23a; color: #fff; font-weight: 600; &.start { background: #409eff; } &.end { background: #e6a23c; } }
|
||||
@@ -679,7 +686,7 @@ export default {
|
||||
.transport-type-field :deep(.el-input) { width: 240px; }
|
||||
.goods-heading { display: flex; align-items: center; justify-content: space-between; margin: 0 -24px 12px; padding: 14px 24px; border-top: 1px solid #eff1f7; border-bottom: 1px solid #eff1f7; h3 { margin: 0; padding-left: 12px; border-left: 4px solid #409eff; font-size: 16px; } span { color: #909399; font-size: 13px; } }
|
||||
.freight-heading { margin: 16px 0 12px; h3 { margin: 0; padding-left: 12px; border-left: 4px solid #409eff; font-size: 16px; } }
|
||||
.goods-table { :deep(th.el-table__cell), :deep(td.el-table__cell) { border-color: #eff1f7; } :deep(.el-table__row--striped td.el-table__cell) { background: #fafafa; } :deep(.goods-table__remaining .cell) { white-space: nowrap; } :deep(.goods-table__dispatch .el-input) { width: 100%; min-width: 0; } }
|
||||
.goods-table { :deep(th.el-table__cell), :deep(td.el-table__cell) { border-color: #eff1f7; } :deep(.el-table__row--striped td.el-table__cell) { background: #fafafa; } :deep(.goods-table__remaining .cell) { white-space: nowrap; } :deep(.goods-table__dispatch .el-input) { width: 100%; min-width: 0; } :deep(.goods-table__cargo-type .el-input__inner), :deep(.goods-table__cargo-name .el-select__selected-item) { color: #303133; } }
|
||||
.freight-form { :deep(.freight-unit-select) { width: 92px; } }
|
||||
.carrier-type-form { margin-top: 16px; }
|
||||
.carrier-form { padding-top: 8px; }
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
<div class="route-node-form">
|
||||
<el-form-item label="发货地址" required class="route-address-form-item"
|
||||
><div class="route-address-inputs"
|
||||
><el-input v-model="form.departureName" class="route-address-name" readonly placeholder="发货地址" @click="openAddressMap('departure')" /><el-input v-model="form.departureAddress" class="route-address-detail" readonly placeholder="请输入详细地址" @click="openAddressMap('departure')" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="List" @click="openCommonAddress('departure')" /></el-tooltip></div
|
||||
><el-cascader v-model="addressRegionPaths.departure" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" placeholder="请选择省市区" clearable filterable @visible-change="visible => visible && loadRegionOptions()" @change="value => handleAddressRegionChange('departure', value)" /><el-input v-model="form.departureAddress" class="route-address-detail" readonly placeholder="请输入详细地址" @click="openAddressMap('departure')" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="List" @click="openCommonAddress('departure')" /></el-tooltip></div
|
||||
></el-form-item>
|
||||
<el-form-item label="联系人"
|
||||
><el-input v-model="form.departureContact" placeholder="请输入"
|
||||
@@ -74,9 +74,9 @@
|
||||
<el-form-item label="计划开始日期"
|
||||
><el-date-picker
|
||||
v-model="form.planStartTime"
|
||||
type="datetime"
|
||||
format="YYYY-MM-DD HH:mm"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
type="date"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="请选择"
|
||||
/></el-form-item>
|
||||
</div>
|
||||
@@ -84,12 +84,12 @@
|
||||
</el-step>
|
||||
<el-step v-for="(route, index) in form.routes" :key="index">
|
||||
<template #icon><span class="route-step-icon route-step-icon--middle">经</span></template>
|
||||
<template #title><div class="route-step-title"><span class="route-segment-label">段{{ index + 1 }}</span><span class="route-title-path">{{ getRouteTitle(index).replace(`段${index + 1}:`, '') }}</span><div class="route-transport-control"><span>运输方式</span><el-select v-model="route.transportType" placeholder="请选择"><el-option v-for="type in transports" :key="type" :label="type" :value="type" /></el-select></div></div></template>
|
||||
<template #title><div class="route-step-title"><span class="route-segment-label">段{{ index + 1 }}</span><el-tooltip :content="getRouteTitle(index).replace(`段${index + 1}:`, '')" placement="top"><span class="route-title-path">{{ formatRouteTitle(getRouteTitle(index), index + 1) }}</span></el-tooltip><div class="route-transport-control"><span>运输方式</span><el-select v-model="route.transportType" placeholder="请选择"><el-option v-for="type in transports" :key="type" :label="type" :value="type" /></el-select></div></div></template>
|
||||
<template #description>
|
||||
<div class="route-node-form">
|
||||
<el-form-item label="途经地" required class="route-address-form-item"
|
||||
><div class="route-address-inputs"
|
||||
><el-input v-model="route.departureName" class="route-address-name" readonly :disabled="!route.transportType" placeholder="请先选择运输方式" @click="openAddressMap(`route-${index}`)" /><el-input v-model="route.departureAddress" class="route-address-detail" readonly :disabled="!route.transportType" placeholder="请先选择运输方式" @click="openAddressMap(`route-${index}`)" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="List" :disabled="!route.transportType" @click="openCommonAddress(`route-${index}`)" /></el-tooltip></div
|
||||
><el-cascader v-model="addressRegionPaths[`route-${index}`]" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" :disabled="!route.transportType" placeholder="请选择省市区" clearable filterable @visible-change="visible => visible && loadRegionOptions()" @change="value => handleAddressRegionChange(`route-${index}`, value)" /><el-input v-model="route.departureAddress" class="route-address-detail" readonly :disabled="!route.transportType || !isRoadTransportType(route.transportType)" placeholder="请先选择运输方式" @click="openAddressMap(`route-${index}`)" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="List" :disabled="!route.transportType" @click="openCommonAddress(`route-${index}`)" /></el-tooltip></div
|
||||
></el-form-item>
|
||||
<el-form-item label="联系人"><el-input v-model="route.departureContact" placeholder="请输入" /></el-form-item>
|
||||
<el-form-item label="联系方式"><div class="route-contact-actions"><el-input v-model="route.departurePhone" placeholder="请输入" /><el-tooltip v-if="index === form.routes.length - 1" content="新增途经点" placement="top"><el-button class="route-action-button" type="primary" link @click="addRoute"><el-icon><CirclePlus /></el-icon></el-button></el-tooltip><el-tooltip content="删除途经点" placement="top"><el-button class="route-action-button" type="danger" link @click="removeRoute(index)"><el-icon><Remove /></el-icon></el-button></el-tooltip></div></el-form-item>
|
||||
@@ -98,12 +98,12 @@
|
||||
</el-step>
|
||||
<el-step>
|
||||
<template #icon><span class="route-step-icon route-step-icon--end">终</span></template>
|
||||
<template #title><div class="route-step-title"><span class="route-segment-label">段{{ form.routes.length + 1 }}</span><span class="route-title-path">{{ getFinalRouteTitle().replace(`段${form.routes.length + 1}:`, '') }}</span><div class="route-transport-control"><span>运输方式</span><el-select v-model="form.finalTransportType" placeholder="请选择"><el-option v-for="type in transports" :key="type" :label="type" :value="type" /></el-select></div></div></template>
|
||||
<template #title><div class="route-step-title"><span class="route-segment-label">段{{ form.routes.length + 1 }}</span><el-tooltip :content="getFinalRouteTitle().replace(`段${form.routes.length + 1}:`, '')" placement="top"><span class="route-title-path">{{ formatRouteTitle(getFinalRouteTitle(), form.routes.length + 1) }}</span></el-tooltip><div class="route-transport-control"><span>运输方式</span><el-select v-model="form.finalTransportType" placeholder="请选择"><el-option v-for="type in transports" :key="type" :label="type" :value="type" /></el-select></div></div></template>
|
||||
<template #description>
|
||||
<div class="route-node-form">
|
||||
<el-form-item label="收货地址" required class="route-address-form-item"
|
||||
><div class="route-address-inputs"
|
||||
><el-input v-model="form.arrivalName" class="route-address-name" readonly :disabled="!form.finalTransportType" placeholder="请先选择运输方式" @click="openAddressMap('arrival')" /><el-input v-model="form.arrivalAddress" class="route-address-detail" readonly :disabled="!form.finalTransportType" placeholder="请先选择运输方式" @click="openAddressMap('arrival')" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="List" :disabled="!form.finalTransportType" @click="openCommonAddress('arrival')" /></el-tooltip></div
|
||||
><el-cascader v-model="addressRegionPaths.arrival" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" :disabled="!form.finalTransportType" placeholder="请选择省市区" clearable filterable @visible-change="visible => visible && loadRegionOptions()" @change="value => handleAddressRegionChange('arrival', value)" /><el-input v-model="form.arrivalAddress" class="route-address-detail" readonly :disabled="!form.finalTransportType || !isRoadTransportType(form.finalTransportType)" placeholder="请选择省市区" @click="openAddressMap('arrival')" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="List" :disabled="!form.finalTransportType" @click="openCommonAddress('arrival')" /></el-tooltip></div
|
||||
></el-form-item>
|
||||
<el-form-item label="联系人"
|
||||
><el-input v-model="form.arrivalContact" placeholder="请输入" /></el-form-item>
|
||||
@@ -112,9 +112,9 @@
|
||||
<el-form-item label="计划结束日期"
|
||||
><el-date-picker
|
||||
v-model="form.planEndTime"
|
||||
type="datetime"
|
||||
format="YYYY-MM-DD HH:mm"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
type="date"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="请选择"
|
||||
/></el-form-item>
|
||||
</div>
|
||||
@@ -299,6 +299,31 @@
|
||||
width="1100px"
|
||||
@opened="loadAddressList"
|
||||
>
|
||||
<div class="route-dialog__search common-address-dialog__search">
|
||||
<el-form :model="addressQuery" label-position="right" label-width="auto">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="6">
|
||||
<el-form-item label="地址名称">
|
||||
<el-input v-model="addressQuery.addressName" clearable placeholder="请输入" @keyup.enter="searchAddressList" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="详细地址">
|
||||
<el-input v-model="addressQuery.detailAddress" clearable placeholder="请输入" @keyup.enter="searchAddressList" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="行政区划">
|
||||
<el-input v-model="addressQuery.regionName" clearable placeholder="请输入" @keyup.enter="searchAddressList" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6" class="route-dialog__search-actions">
|
||||
<el-button type="primary" @click="searchAddressList">查询</el-button>
|
||||
<el-button @click="resetAddressList">重置</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</div>
|
||||
<el-table v-loading="addressLoading" :data="addressRows" border height="360">
|
||||
<el-table-column type="index" label="序号" width="70" />
|
||||
<el-table-column prop="addressName" label="地址名称" min-width="150" />
|
||||
@@ -386,10 +411,22 @@
|
||||
<div class="address-pagination"><el-pagination v-model:current-page="stationPage.current" v-model:page-size="stationPage.size" :page-sizes="[10, 20, 50]" layout="total, prev, pager, next, sizes" :total="stationPage.total" @size-change="loadStationList" @current-change="loadStationList" /></div>
|
||||
</el-dialog>
|
||||
<el-dialog v-model="commonCargoVisible" title="选择常用货物" append-to-body width="1100px" @opened="loadCommonCargoList">
|
||||
<el-table v-loading="commonCargoLoading" :data="commonCargoRows" border height="360">
|
||||
<div class="route-dialog__search common-cargo-dialog__search">
|
||||
<el-form :model="commonCargoQuery" label-position="right" label-width="86px">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="6"><el-form-item label="货物名称"><el-input v-model="commonCargoQuery.cargoName" clearable placeholder="请输入" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="货物类型"><el-cascader v-model="commonCargoQuery.cargoTypePath" :options="cargoTypeOptions" :props="cargoTypeCascaderProps" clearable filterable placeholder="请选择" :loading="cargoTypeLoading" @visible-change="visible => visible && loadCargoTypeOptions()" @change="handleCommonCargoTypeChange" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="规格型号"><el-input v-model="commonCargoQuery.specificationModel" clearable placeholder="请输入" /></el-form-item></el-col>
|
||||
<el-col :span="6" class="route-dialog__search-actions"><el-button type="primary" @click="searchCommonCargoList">查询</el-button><el-button @click="resetCommonCargoList">重置</el-button></el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</div>
|
||||
<el-table v-loading="commonCargoLoading" :data="commonCargoRows" border height="360" @selection-change="handleCommonCargoSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" fixed="left" />
|
||||
<el-table-column type="index" label="序号" width="70" /><el-table-column prop="cargoName" label="货物名称" min-width="150" /><el-table-column prop="cargoCode" label="货物编号" min-width="130" /><el-table-column prop="firstCargoTypeName" label="一级货物类型" min-width="150" /><el-table-column prop="secondCargoTypeName" label="二级货物类型" min-width="150" /><el-table-column prop="packageType" label="包装" min-width="100" /><el-table-column prop="brand" label="品牌" min-width="120" /><el-table-column prop="specification" label="规格" min-width="120" /><el-table-column prop="model" label="型号" min-width="120" /><el-table-column label="操作" width="100" fixed="right"><template #default="{ row }"><el-link type="primary" @click="selectCommonCargo(row)">选择</el-link></template></el-table-column>
|
||||
</el-table>
|
||||
<div class="address-pagination"><el-pagination v-model:current-page="commonCargoPage.current" v-model:page-size="commonCargoPage.size" :page-sizes="[10, 20, 50]" layout="total, prev, pager, next, sizes" :total="commonCargoPage.total" @size-change="loadCommonCargoList" @current-change="loadCommonCargoList" /></div>
|
||||
<template #footer><el-button @click="commonCargoVisible = false">关闭</el-button><el-button type="primary" :disabled="!commonCargoSelected.length" @click="confirmCommonCargoSelection">确定</el-button></template>
|
||||
</el-dialog>
|
||||
<el-dialog v-model="cargoImportVisible" title="导入货物" append-to-body width="560px"><div class="cargo-import"><el-upload action="#" accept=".xls,.xlsx" :auto-upload="false" :show-file-list="false" :disabled="cargoImportLoading" :on-change="handleCargoImport"><el-button type="primary" :loading="cargoImportLoading">上传</el-button><template #tip><div class="el-upload__tip">支持 .xlsx、.xls 文件,单个文件不超过 50M</div></template></el-upload><el-button type="primary" plain @click="downloadCargoTemplate">点击下载模板</el-button></div><template #footer><el-button @click="cargoImportVisible = false">关闭</el-button></template></el-dialog>
|
||||
<el-dialog v-model="routeDialogVisible" title="选择线路" width="1280px" @opened="loadRouteList">
|
||||
@@ -485,6 +522,7 @@ import { getList as getAirportMasterList } from '@/api/base/airport-master';
|
||||
import { getList as getPortTerminalList } from '@/api/base/port-terminal';
|
||||
import { getList as getRailwayStationList } from '@/api/base/railway-station';
|
||||
import { getList as getCargoTypeList } from '@/api/base/cargo-type';
|
||||
import { getLazyTree as getRegionLazyTree } from '@/api/base/region';
|
||||
import { getList as getCommonCargoList } from '@/api/business/common-cargo';
|
||||
import { exportBlob, importBlob } from '@/api/common';
|
||||
import { downloadFileByUrl, downloadXls } from '@/utils/util';
|
||||
@@ -534,7 +572,12 @@ export default {
|
||||
addressLoading: false,
|
||||
addressTarget: '',
|
||||
addressRows: [],
|
||||
addressQuery: {},
|
||||
addressPage: { current: 1, size: 10, total: 0 },
|
||||
addressRegionPaths: { departure: [], arrival: [] },
|
||||
regionOptions: [],
|
||||
regionLoading: false,
|
||||
regionRequest: null,
|
||||
stationDialogVisible: false,
|
||||
stationLoading: false,
|
||||
stationQuery: {},
|
||||
@@ -552,6 +595,8 @@ export default {
|
||||
commonCargoVisible: false,
|
||||
commonCargoLoading: false,
|
||||
commonCargoRows: [],
|
||||
commonCargoQuery: {},
|
||||
commonCargoSelected: [],
|
||||
commonCargoPage: { current: 1, size: 10, total: 0 },
|
||||
cargoTypeLoading: false,
|
||||
cargoTypeOptions: [],
|
||||
@@ -625,11 +670,19 @@ export default {
|
||||
emitPath: true,
|
||||
};
|
||||
},
|
||||
regionCascaderProps() {
|
||||
return {
|
||||
value: 'id',
|
||||
label: 'title',
|
||||
children: 'children',
|
||||
emitPath: true,
|
||||
};
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
this.loading = true;
|
||||
try {
|
||||
await Promise.all([this.loadProjects(), this.loadCargoTypeOptions()]);
|
||||
await Promise.all([this.loadProjects(), this.loadCargoTypeOptions(), this.loadRegionOptions()]);
|
||||
if (this.id) {
|
||||
await this.loadDetail(this.id);
|
||||
}
|
||||
@@ -641,6 +694,118 @@ export default {
|
||||
responseData(response) {
|
||||
return response?.data?.data || response?.data || response || {};
|
||||
},
|
||||
async loadRegionOptions() {
|
||||
if (this.regionOptions.length) return this.regionOptions;
|
||||
if (this.regionRequest) return this.regionRequest;
|
||||
this.regionLoading = true;
|
||||
this.regionRequest = getRegionLazyTree()
|
||||
.then(response => {
|
||||
const data = response?.data?.data || response?.data || response || [];
|
||||
this.regionOptions = this.extractChinaRegionOptions(this.buildRegionTree(data));
|
||||
this.syncAddressRegionPaths();
|
||||
return this.regionOptions;
|
||||
})
|
||||
.finally(() => {
|
||||
this.regionLoading = false;
|
||||
this.regionRequest = null;
|
||||
});
|
||||
return this.regionRequest;
|
||||
},
|
||||
buildRegionTree(regions = []) {
|
||||
const flat = [];
|
||||
const collect = list =>
|
||||
(list || []).forEach(item => {
|
||||
flat.push({ ...item, children: undefined });
|
||||
if (item.children?.length) collect(item.children);
|
||||
});
|
||||
collect(regions);
|
||||
const nodeMap = new Map();
|
||||
flat.forEach(item => {
|
||||
const id = item.id ?? item.code ?? item.value;
|
||||
if (id === undefined || id === null) return;
|
||||
nodeMap.set(String(id), {
|
||||
...item,
|
||||
id: String(id),
|
||||
parentId: String(item.parentId ?? item.parentCode ?? ''),
|
||||
children: [],
|
||||
});
|
||||
});
|
||||
const roots = [];
|
||||
nodeMap.forEach(node => {
|
||||
const parent = nodeMap.get(node.parentId);
|
||||
if (parent && parent !== node) parent.children.push(node);
|
||||
else roots.push(node);
|
||||
});
|
||||
const normalize = node => {
|
||||
const children = node.children.map(item => normalize(item));
|
||||
return { ...node, title: node.title || node.name || '', children: children.length ? children : undefined, leaf: !children.length };
|
||||
};
|
||||
return roots.map(item => normalize(item));
|
||||
},
|
||||
extractChinaRegionOptions(regions = []) {
|
||||
const china = regions.find(item => ['中国', '中华人民共和国'].includes(item.title));
|
||||
if (china?.children?.length) return china.children;
|
||||
if (regions.length === 1 && regions[0].children?.length) return regions[0].children;
|
||||
return regions;
|
||||
},
|
||||
findRegionPathByName(options = [], regionName = '', parents = []) {
|
||||
const target = String(regionName || '').replace(/\s+/g, '');
|
||||
if (!target) return [];
|
||||
for (const item of options) {
|
||||
const nextParents = [...parents, item];
|
||||
const name = nextParents.map(region => region.title || region.name || '').join('');
|
||||
if (name === target) return nextParents.map(region => region.id);
|
||||
const path = this.findRegionPathByName(item.children, target, nextParents);
|
||||
if (path.length) return path;
|
||||
}
|
||||
return [];
|
||||
},
|
||||
findRegionPathByCode(options = [], regionCode = '', parents = []) {
|
||||
const target = String(regionCode || '').trim();
|
||||
if (!target) return [];
|
||||
for (const item of options) {
|
||||
const nextParents = [...parents, item];
|
||||
if (String(item.id) === target) return nextParents.map(region => region.id);
|
||||
const path = this.findRegionPathByCode(item.children, target, nextParents);
|
||||
if (path.length) return path;
|
||||
}
|
||||
return [];
|
||||
},
|
||||
regionLabels(path = []) {
|
||||
let options = this.regionOptions;
|
||||
const labels = [];
|
||||
path.forEach(value => {
|
||||
const option = options.find(item => String(item.id) === String(value));
|
||||
if (!option) return;
|
||||
labels.push(option.title || option.name || '');
|
||||
options = option.children || [];
|
||||
});
|
||||
return labels;
|
||||
},
|
||||
handleAddressRegionChange(target, value) {
|
||||
const field = this.addressModel(target);
|
||||
const path = Array.isArray(value) ? value.map(item => String(item)) : [];
|
||||
this.addressRegionPaths = { ...this.addressRegionPaths, [target]: path };
|
||||
if (!field) return;
|
||||
const labels = this.regionLabels(path);
|
||||
field.model[field.name] = labels.join('');
|
||||
field.model[field.regionCode] = path.at(-1) || '';
|
||||
},
|
||||
syncAddressRegionPath(target) {
|
||||
const field = this.addressModel(target);
|
||||
if (!field) return;
|
||||
const codePath = this.findRegionPathByCode(this.regionOptions, field.model[field.regionCode]);
|
||||
const path = codePath.length
|
||||
? codePath
|
||||
: this.findRegionPathByName(this.regionOptions, field.model[field.name]);
|
||||
this.addressRegionPaths = { ...this.addressRegionPaths, [target]: path };
|
||||
},
|
||||
syncAddressRegionPaths() {
|
||||
if (!this.regionOptions.length) return;
|
||||
['departure', 'arrival', ...this.form.routes.map((route, index) => `route-${index}`)].forEach(target => {
|
||||
this.syncAddressRegionPath(target);
|
||||
});
|
||||
},
|
||||
async loadDetail(id) {
|
||||
const res = await api.getDetail(id);
|
||||
const detail = this.responseData(res);
|
||||
@@ -652,6 +817,8 @@ export default {
|
||||
finalSegment.departureName === detail.arrivalName;
|
||||
this.form = {
|
||||
...detail,
|
||||
planStartTime: this.normalizeDateValue(detail.planStartTime),
|
||||
planEndTime: this.normalizeDateValue(detail.planEndTime),
|
||||
routes: hasFinalSegment ? routes.slice(0, -1) : routes.length ? routes : [this.createRoute(0)],
|
||||
finalTransportType: hasFinalSegment
|
||||
? finalSegment.transportType
|
||||
@@ -660,6 +827,7 @@ export default {
|
||||
attachmentsJson: detail.attachmentsJson || '[]',
|
||||
transportOrganizationType: '多式联运',
|
||||
};
|
||||
this.syncAddressRegionPaths();
|
||||
this.restoreGoodsCargoTypePaths();
|
||||
this.attachmentRows = this.parseJsonArray(this.form.attachmentsJson);
|
||||
this.selectedAttachmentRows = [];
|
||||
@@ -675,17 +843,54 @@ export default {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
normalizeDateValue(value) {
|
||||
if (!value) return '';
|
||||
if (value instanceof Date) {
|
||||
const year = value.getFullYear();
|
||||
const month = String(value.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(value.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
return String(value).replace('T', ' ').slice(0, 10);
|
||||
},
|
||||
getRegionDisplay(target, fallback) {
|
||||
const labels = this.regionLabels(this.addressRegionPaths[target] || []);
|
||||
return labels.length ? labels.join('/') : fallback;
|
||||
},
|
||||
getRouteTitle(index) {
|
||||
const departureName =
|
||||
index === 0
|
||||
? this.form.departureName || '发货地址'
|
||||
: this.form.routes[index - 1].departureName || '途经地';
|
||||
return `段${index + 1}:${departureName} - ${this.form.routes[index].departureName || '途经地'}`;
|
||||
? this.getRegionDisplay('departure', this.form.departureName || '发货地址')
|
||||
: this.getRegionDisplay(
|
||||
`route-${index - 1}`,
|
||||
this.form.routes[index - 1].departureName || '途经地'
|
||||
);
|
||||
const arrivalName = this.getRegionDisplay(
|
||||
`route-${index}`,
|
||||
this.form.routes[index].departureName || '途经地'
|
||||
);
|
||||
return `段${index + 1}:${departureName} → ${arrivalName}`;
|
||||
},
|
||||
truncateRouteAddress(value) {
|
||||
const text = String(value || '');
|
||||
return text.length > 6 ? `${text.slice(0, 6)}...` : text;
|
||||
},
|
||||
formatRouteTitle(title, segmentNo) {
|
||||
const fullTitle = String(title || '').replace(`段${segmentNo}:`, '');
|
||||
const separator = ' → ';
|
||||
const separatorIndex = fullTitle.indexOf(separator);
|
||||
if (separatorIndex < 0) return this.truncateRouteAddress(fullTitle);
|
||||
const departure = fullTitle.slice(0, separatorIndex);
|
||||
const arrival = fullTitle.slice(separatorIndex + separator.length);
|
||||
return `${this.truncateRouteAddress(departure)}${separator}${this.truncateRouteAddress(arrival)}`;
|
||||
},
|
||||
getFinalRouteTitle() {
|
||||
return `段${this.form.routes.length + 1}:${this.lastRouteName} - ${
|
||||
this.form.arrivalName || '收货地址'
|
||||
}`;
|
||||
const departureName = this.getRegionDisplay(
|
||||
`route-${this.form.routes.length - 1}`,
|
||||
this.lastRouteName
|
||||
);
|
||||
const arrivalName = this.getRegionDisplay('arrival', this.form.arrivalName || '收货地址');
|
||||
return `段${this.form.routes.length + 1}:${departureName} → ${arrivalName}`;
|
||||
},
|
||||
responseRecords(response) {
|
||||
const data = response?.data?.data || response?.data || response;
|
||||
@@ -730,6 +935,7 @@ export default {
|
||||
phone: 'departurePhone',
|
||||
longitude: 'departureLongitude',
|
||||
latitude: 'departureLatitude',
|
||||
regionCode: 'departureRegionCode',
|
||||
siteCode: 'departureSiteCode',
|
||||
}
|
||||
: null;
|
||||
@@ -743,6 +949,7 @@ export default {
|
||||
phone: `${prefix}Phone`,
|
||||
longitude: `${prefix}Longitude`,
|
||||
latitude: `${prefix}Latitude`,
|
||||
regionCode: `${prefix}RegionCode`,
|
||||
siteCode: `${prefix}SiteCode`,
|
||||
};
|
||||
},
|
||||
@@ -759,6 +966,10 @@ export default {
|
||||
if (type.includes('航空') || type.includes('空运')) return 'air';
|
||||
return 'road';
|
||||
},
|
||||
isRoadTransportType(type) {
|
||||
const value = String(type || '').trim().toLowerCase();
|
||||
return value === 'road' || value.includes('公路');
|
||||
},
|
||||
openCommonAddress(target) {
|
||||
this.addressTarget = target;
|
||||
if (this.addressTransportMode(target) !== 'road') {
|
||||
@@ -767,6 +978,7 @@ export default {
|
||||
this.stationDialogVisible = true;
|
||||
return;
|
||||
}
|
||||
this.addressQuery = {};
|
||||
this.addressPage.current = 1;
|
||||
this.addressDialogVisible = true;
|
||||
},
|
||||
@@ -814,12 +1026,14 @@ export default {
|
||||
const target = this.addressModel(this.addressTarget);
|
||||
if (!target) return;
|
||||
const station = this.normalizeStation(row);
|
||||
target.model[target.name] = station.stationName;
|
||||
target.model[target.name] = station.regionName || station.stationName;
|
||||
target.model[target.address] = station.detailAddress;
|
||||
target.model[target.longitude] = station.longitude || '';
|
||||
target.model[target.latitude] = station.latitude || '';
|
||||
target.model[target.regionCode] = station.regionCode || '';
|
||||
target.model[target.siteCode] = station.stationCode;
|
||||
this.stationDialogVisible = false;
|
||||
this.syncAddressRegionPath(this.addressTarget);
|
||||
},
|
||||
async loadAddressList() {
|
||||
this.addressLoading = true;
|
||||
@@ -827,7 +1041,7 @@ export default {
|
||||
const response = await getCommonAddressList(
|
||||
this.addressPage.current,
|
||||
this.addressPage.size,
|
||||
{ allDept: 0 }
|
||||
{ ...this.addressQuery, allDept: 0 }
|
||||
);
|
||||
const data = response?.data?.data || response?.data || response || {};
|
||||
this.addressRows = data.records || [];
|
||||
@@ -836,18 +1050,33 @@ export default {
|
||||
this.addressLoading = false;
|
||||
}
|
||||
},
|
||||
searchAddressList() {
|
||||
this.addressPage.current = 1;
|
||||
this.loadAddressList();
|
||||
},
|
||||
resetAddressList() {
|
||||
this.addressQuery = {};
|
||||
this.addressPage.current = 1;
|
||||
this.loadAddressList();
|
||||
},
|
||||
selectAddress(row) {
|
||||
const target = this.addressModel(this.addressTarget);
|
||||
if (!target) return;
|
||||
target.model[target.name] = row.addressName || row.regionName || '';
|
||||
target.model[target.name] = row.regionName || row.addressName || '';
|
||||
target.model[target.address] = row.detailAddress || row.address || '';
|
||||
target.model[target.contact] = row.contactName || target.model[target.contact] || '';
|
||||
target.model[target.phone] = row.contactPhone || target.model[target.phone] || '';
|
||||
target.model[target.longitude] = row.longitude || '';
|
||||
target.model[target.latitude] = row.latitude || '';
|
||||
target.model[target.regionCode] = row.regionCode || '';
|
||||
this.addressDialogVisible = false;
|
||||
this.syncAddressRegionPath(this.addressTarget);
|
||||
},
|
||||
openAddressMap(target) {
|
||||
if ((target.startsWith('route-') || target === 'arrival') && !this.isRoadTransportType(this.addressTransportType(target))) {
|
||||
this.$message.info('非公路运输地址不能使用地图选择');
|
||||
return;
|
||||
}
|
||||
const field = this.addressModel(target);
|
||||
if (!field) return;
|
||||
this.mapTarget = target;
|
||||
@@ -855,6 +1084,7 @@ export default {
|
||||
this.mapSelected = {
|
||||
longitude: field.model[field.longitude] || '',
|
||||
latitude: field.model[field.latitude] || '',
|
||||
regionCode: field.model[field.regionCode] || '',
|
||||
detailAddress: this.mapKeyword,
|
||||
regionName: field.model[field.name] || '',
|
||||
};
|
||||
@@ -935,6 +1165,7 @@ export default {
|
||||
latitude,
|
||||
detailAddress: keyword || this.mapKeyword,
|
||||
regionName: '',
|
||||
regionCode: '',
|
||||
};
|
||||
this.renderAddressMarker([longitude, latitude]);
|
||||
this.mapStatus = '已选点,可确认回填';
|
||||
@@ -947,6 +1178,7 @@ export default {
|
||||
this.mapSelected.regionName = [component.province, component.city, component.district]
|
||||
.filter(Boolean)
|
||||
.join('');
|
||||
this.mapSelected.regionCode = component.adcode || '';
|
||||
this.mapKeyword = this.mapSelected.detailAddress;
|
||||
this.mapStatus = '已解析地址,可确认回填';
|
||||
}
|
||||
@@ -967,16 +1199,22 @@ export default {
|
||||
target.model[target.address] = this.mapSelected.detailAddress || this.mapKeyword;
|
||||
target.model[target.longitude] = this.mapSelected.longitude;
|
||||
target.model[target.latitude] = this.mapSelected.latitude;
|
||||
target.model[target.regionCode] = this.mapSelected.regionCode || '';
|
||||
this.mapDialogVisible = false;
|
||||
this.syncAddressRegionPath(this.mapTarget);
|
||||
},
|
||||
openCommonCargoDialog() {
|
||||
this.commonCargoQuery = {};
|
||||
this.commonCargoSelected = [];
|
||||
this.commonCargoPage.current = 1;
|
||||
this.commonCargoVisible = true;
|
||||
},
|
||||
async loadCommonCargoList() {
|
||||
this.commonCargoSelected = [];
|
||||
this.commonCargoLoading = true;
|
||||
try {
|
||||
const response = await getCommonCargoList(this.commonCargoPage.current, this.commonCargoPage.size, { allDept: 0 });
|
||||
const { cargoTypePath, ...query } = this.commonCargoQuery;
|
||||
const response = await getCommonCargoList(this.commonCargoPage.current, this.commonCargoPage.size, { ...query, allDept: 0 });
|
||||
const data = response?.data?.data || response?.data || response || {};
|
||||
this.commonCargoRows = data.records || [];
|
||||
this.commonCargoPage.total = data.total || 0;
|
||||
@@ -984,6 +1222,31 @@ export default {
|
||||
this.commonCargoLoading = false;
|
||||
}
|
||||
},
|
||||
searchCommonCargoList() {
|
||||
this.commonCargoPage.current = 1;
|
||||
this.loadCommonCargoList();
|
||||
},
|
||||
resetCommonCargoList() {
|
||||
this.commonCargoQuery = {};
|
||||
this.commonCargoPage.current = 1;
|
||||
this.loadCommonCargoList();
|
||||
},
|
||||
handleCommonCargoTypeChange(value) {
|
||||
const path = Array.isArray(value) ? value.map(item => String(item)) : [];
|
||||
const firstCargoType = this.getCargoTypeByPath(path.slice(0, 1));
|
||||
const secondCargoType = path.length > 1 ? this.getCargoTypeByPath(path) : null;
|
||||
this.commonCargoQuery = {
|
||||
...this.commonCargoQuery,
|
||||
cargoTypePath: path,
|
||||
firstCargoTypeName: firstCargoType?.label || '',
|
||||
firstCargoTypeCode: firstCargoType?.cargoCode || firstCargoType?.code || '',
|
||||
secondCargoTypeName: secondCargoType?.label || '',
|
||||
secondCargoTypeCode: secondCargoType?.cargoCode || secondCargoType?.code || '',
|
||||
};
|
||||
},
|
||||
handleCommonCargoSelectionChange(rows) {
|
||||
this.commonCargoSelected = rows || [];
|
||||
},
|
||||
async loadCargoTypeOptions() {
|
||||
if (this.cargoTypeOptions.length || this.cargoTypeLoading) return;
|
||||
this.cargoTypeLoading = true;
|
||||
@@ -1133,7 +1396,16 @@ export default {
|
||||
});
|
||||
},
|
||||
selectCommonCargo(row) {
|
||||
const cargo = this.normalizeCargoRow(row, this.resolveCargoTypePath(row));
|
||||
this.appendCommonCargoRows([row]);
|
||||
this.commonCargoVisible = false;
|
||||
},
|
||||
confirmCommonCargoSelection() {
|
||||
if (!this.commonCargoSelected.length) return;
|
||||
this.appendCommonCargoRows(this.commonCargoSelected);
|
||||
this.$message.success(`成功选择${this.commonCargoSelected.length}条常用货物`);
|
||||
this.commonCargoVisible = false;
|
||||
},
|
||||
appendCommonCargoRows(rows = []) {
|
||||
const firstGoods = this.form.goods[0];
|
||||
const isFirstGoodsEmpty =
|
||||
firstGoods &&
|
||||
@@ -1148,12 +1420,11 @@ export default {
|
||||
'remark',
|
||||
].some(key => firstGoods[key]);
|
||||
|
||||
if (isFirstGoodsEmpty) {
|
||||
this.form.goods.splice(0, 1, cargo);
|
||||
} else {
|
||||
this.form.goods.push(cargo);
|
||||
}
|
||||
this.commonCargoVisible = false;
|
||||
rows.forEach((row, index) => {
|
||||
const cargo = this.normalizeCargoRow(row, this.resolveCargoTypePath(row));
|
||||
if (isFirstGoodsEmpty && index === 0) this.form.goods.splice(0, 1, cargo);
|
||||
else this.form.goods.push(cargo);
|
||||
});
|
||||
},
|
||||
resolveCargoTypePath(row = {}) {
|
||||
return (
|
||||
@@ -1275,6 +1546,7 @@ export default {
|
||||
this.form.arrivalAddress = route.arrivalAddress || '';
|
||||
this.form.arrivalContact = route.arrivalContact || '';
|
||||
this.form.arrivalPhone = route.arrivalPhone || '';
|
||||
this.syncAddressRegionPaths();
|
||||
this.form.finalTransportType = '公路运输';
|
||||
if (!this.form.routes.length) this.addRoute();
|
||||
this.routeDialogVisible = false;
|
||||
@@ -1287,6 +1559,7 @@ export default {
|
||||
},
|
||||
removeRoute(index) {
|
||||
this.form.routes.splice(index, 1);
|
||||
this.syncAddressRegionPaths();
|
||||
},
|
||||
addGoods(index) {
|
||||
this.form.goods.splice(index + 1, 0, { quantityUnit: quantityUnitOptions[0] });
|
||||
@@ -1416,6 +1689,8 @@ export default {
|
||||
}
|
||||
const payload = {
|
||||
...this.form,
|
||||
planStartTime: this.normalizeDateValue(this.form.planStartTime),
|
||||
planEndTime: this.normalizeDateValue(this.form.planEndTime),
|
||||
attachmentsJson: JSON.stringify(this.attachmentRows),
|
||||
routes: [
|
||||
...this.form.routes.map((route, index) => ({ ...route, segmentNo: `段${index + 1}` })),
|
||||
@@ -1537,6 +1812,9 @@ export default {
|
||||
padding: 12px 12px 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
.common-cargo-dialog__search {
|
||||
background: #fff;
|
||||
}
|
||||
.route-dialog__search-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
@@ -1638,9 +1916,14 @@ export default {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.route-title-path {
|
||||
display: inline-block;
|
||||
max-width: 24em;
|
||||
overflow: hidden;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.route-transport-control {
|
||||
display: flex;
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
<template>
|
||||
<basic-container class="contract-change-page">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-position="right" label-width="auto">
|
||||
<section class="change-section">
|
||||
<div class="dialog-section-title">基本信息</div>
|
||||
<el-row :gutter="28">
|
||||
<el-col :span="24"><el-form-item label="变更类型"><el-radio-group v-model="form.changeType"><el-radio label="合同信息变更" /><el-radio label="终止合同" /></el-radio-group></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="合同编号"><el-input v-model="form.contractNo" disabled /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="合同名称" prop="contractName"><el-input v-model="form.contractName" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="合同类型"><el-select v-model="form.contractCategory" disabled><el-option label="客户合同" value="客户合同" /><el-option label="承运商合同" value="承运商合同" /></el-select></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="甲方"><el-input v-model="form.partyA" disabled /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="乙方"><el-input v-model="form.partyB" disabled /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="所属项目"><el-input v-model="form.projectName" disabled /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="合同期限"><el-date-picker v-model="period" type="daterange" value-format="YYYY-MM-DD" range-separator="至" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="所属组织"><el-input v-model="form.organizationName" disabled /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="签订日期"><el-date-picker v-model="form.signDate" type="date" value-format="YYYY-MM-DD" disabled /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="合同格式"><el-select v-model="form.contractFormat"><el-option label="电子合同" value="电子合同" /><el-option label="纸质合同" value="纸质合同" /></el-select></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="结算方式"><el-input v-model="form.settlementMode" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="是否需要加盖法人章"><el-select v-model="form.legalSealFlag"><el-option label="是" :value="1" /><el-option label="否" :value="0" /></el-select></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="一式"><div class="inline-field"><el-input-number v-model="form.copyCount" :min="1" controls-position="right" /><span>份</span></div></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="回款账期"><el-input-number v-model="form.paymentDays" :min="0" controls-position="right" /><span class="unit">天</span></el-form-item></el-col>
|
||||
<el-col :span="24"><el-form-item label="备注"><el-input v-model="form.remark" type="textarea" maxlength="2000" show-word-limit /></el-form-item></el-col>
|
||||
</el-row>
|
||||
</section>
|
||||
|
||||
<section class="change-section">
|
||||
<div class="dialog-section-title">合同文件</div>
|
||||
<div class="attachment-head"><vehicle-attachment-upload v-model="contractFileRows" :readonly="false" :file-types="attachmentFileTypes" :max-size="500" :show-tip="false" :show-file-list="false" button-text="上传附件" @change="handleContractFileChange" /><el-button type="primary" :disabled="!contractFileRows.length" @click="handleContractFileBatchDownload">批量下载</el-button></div>
|
||||
<el-table :data="contractFileRows" border class="change-table" @selection-change="selectedContractFiles = $event"><el-table-column type="selection" width="55" /><el-table-column type="index" label="序号" width="70" /><el-table-column label="文件名" min-width="240"><template #default="{ row }"><el-link type="primary" @click="previewAttachment(row, contractFileRows)">{{ row.originalName || row.name }}</el-link></template></el-table-column><el-table-column label="文件大小" width="120"><template #default="{ row }">{{ formatFileSize(row.size) }}</template></el-table-column><el-table-column prop="uploadUserName" label="上传人" width="140" /><el-table-column prop="uploadTime" label="上传时间" width="170" /><el-table-column label="操作" width="100"><template #default="{ $index }"><el-link type="danger" @click="removeContractFile($index)">删除</el-link></template></el-table-column></el-table>
|
||||
</section>
|
||||
|
||||
<section class="change-section">
|
||||
<div class="section-head"><div class="dialog-section-title">计费信息</div><el-button type="primary" plain @click="addPlan">添加</el-button></div>
|
||||
<el-radio-group v-model="feeGenerationMode"><el-radio label="system">系统生成</el-radio><el-radio label="manual">手动生成</el-radio></el-radio-group>
|
||||
<el-table :data="plans" border class="change-table"><el-table-column type="index" label="序号" width="70" /><el-table-column prop="planName" label="方案名称" min-width="220" /><el-table-column label="默认方案" min-width="120"><template #default="{ row }">{{ row.defaultPlan ? '是' : '否' }}</template></el-table-column><el-table-column prop="remark" label="备注" min-width="260" /><el-table-column label="操作" width="180"><template #default="{ row, $index }"><el-link type="primary" @click="editPlan(row, $index)">编辑</el-link><el-link type="danger" @click="plans.splice($index, 1)">删除</el-link></template></el-table-column></el-table>
|
||||
</section>
|
||||
|
||||
<section class="change-section">
|
||||
<div class="dialog-section-title">结算单规则</div>
|
||||
<el-tabs v-model="settlementConfigTab">
|
||||
<el-tab-pane label="预结算配置" name="pre" />
|
||||
<el-tab-pane label="正式结算配置" name="formal" />
|
||||
</el-tabs>
|
||||
<div class="settlement-switch"><span>自动生成结算单</span><el-radio-group v-model="settlementRule.autoGenerate"><el-radio :label="1">开启</el-radio><el-radio :label="0">关闭</el-radio></el-radio-group></div>
|
||||
<el-form v-if="settlementRule.autoGenerate === 1" :model="settlementRule" label-position="right" label-width="auto" class="settlement-form">
|
||||
<el-form-item label="账单起始日期" required><el-date-picker v-model="settlementRule.billStartDate" type="date" placeholder="请选择账单起始日期" format="YYYY-MM-DD" value-format="YYYY-MM-DD" /></el-form-item>
|
||||
<el-form-item label="结算类型" required><el-select v-model="settlementRule.settlementType" placeholder="请选择结算类型" @change="handleSettlementTypeChange"><el-option v-for="item in settlementTypeOptions" :key="item" :label="item" :value="item" /></el-select></el-form-item>
|
||||
<el-form-item v-if="showSettlementBillCycleType" label="账单周期类型" required><el-select v-model="settlementRule.billCycleType" placeholder="请选择账单周期类型" @change="handleCycleTypeChange"><el-option v-for="item in billCycleTypeOptions" :key="item" :label="item" :value="item" /></el-select></el-form-item>
|
||||
<el-form-item v-if="showSettlementBillCutoffDay" label="账单截单日" required><el-select v-model="settlementRule.billCutoffDay" placeholder="请选择账单截单日"><el-option v-for="item in billCutoffDayOptions" :key="item.value" :label="item.label" :value="item.value" /></el-select></el-form-item>
|
||||
<el-form-item v-if="showSettlementCycleDays" label="周期天数" required><el-select v-model="settlementRule.cycleDays" placeholder="请选择周期天数"><el-option v-for="item in cycleDayOptions" :key="item.value" :label="item.label" :value="item.value" /></el-select></el-form-item>
|
||||
</el-form>
|
||||
</section>
|
||||
|
||||
<section class="change-section">
|
||||
<div class="dialog-section-title">付款比例设置</div>
|
||||
<el-table :data="paymentRatioRows" border class="change-table">
|
||||
<el-table-column type="index" label="序号" width="70" />
|
||||
<el-table-column prop="paymentTerm" label="付款笔数" min-width="180" />
|
||||
<el-table-column label="付款比例上限" min-width="220"><template #default="{ row }"><el-input-number v-model="row.ratioLimit" :min="0" :max="100" :precision="2" controls-position="right" /><span>%</span></template></el-table-column>
|
||||
<el-table-column label="备注" min-width="220"><template #default="{ row }"><el-input v-model="row.remark" maxlength="200" /></template></el-table-column>
|
||||
<el-table-column label="操作" width="100"><template #default="{ $index }"><el-link type="danger" @click="paymentRatioRows.splice($index, 1)">删除</el-link></template></el-table-column>
|
||||
</el-table>
|
||||
<el-button type="primary" plain @click="addPaymentRatioRow">添加</el-button>
|
||||
</section>
|
||||
|
||||
<billing-plan-editor v-model="planDialogVisible" :value="planEditor" :index="planEditorIndex" @save="savePlan" />
|
||||
|
||||
<section class="change-section attachment-section">
|
||||
<div class="section-head"><div class="dialog-section-title">其它附件</div><el-button type="primary" plain>批量下载</el-button></div>
|
||||
<el-table :data="attachments" border class="change-table"><el-table-column type="index" label="序号" width="70" /><el-table-column label="文件名" min-width="240"><template #default="{ row }"><el-link type="primary" @click="previewAttachment(row, attachments)">{{ row.originalName || row.name }}</el-link></template></el-table-column><el-table-column prop="description" label="附件描述" min-width="240" /><el-table-column prop="size" label="文件大小" width="120" /><el-table-column prop="userName" label="上传人" width="140" /><el-table-column prop="uploadTime" label="上传时间" width="180" /><el-table-column label="操作" width="100"><template #default="{ $index }"><el-link type="danger" @click="attachments.splice($index, 1)">删除</el-link></template></el-table-column></el-table>
|
||||
<el-upload action="#" :auto-upload="false" multiple :show-file-list="false" @change="handleAttachment"><el-button plain>上传附件</el-button></el-upload>
|
||||
</section>
|
||||
|
||||
<section class="change-section change-reason-section"><div class="dialog-section-title">变更原因</div><el-form-item prop="changeReason"><el-input v-model="form.changeReason" type="textarea" :rows="4" maxlength="2000" show-word-limit placeholder="请输入变更原因" /></el-form-item><div class="dialog-section-title">变更材料</div><el-upload action="#" :auto-upload="false" multiple :show-file-list="false" @change="handleChangeMaterial"><el-button plain>上传变更材料</el-button></el-upload></section>
|
||||
<div class="page-footer"><el-button type="primary" @click="submit">提交</el-button><el-button @click="$router.back()">关闭</el-button></div>
|
||||
</el-form>
|
||||
|
||||
<el-dialog v-model="documentPreviewVisible" :title="previewFile.name || '附件预览'" append-to-body destroy-on-close width="90%" top="4vh">
|
||||
<open-file-viewer v-if="documentPreviewVisible && previewFile.url" :file="previewFile.url" :file-name="previewFile.name" :mime-type="previewFile.mimeType" width="100%" height="72vh" fit="contain" theme="auto" locale="zh-CN" :toolbar="viewerToolbar" :plugins="viewerPlugins" @unsupported="handlePreviewUnsupported" @error="handlePreviewError" />
|
||||
</el-dialog>
|
||||
<el-image-viewer v-if="imagePreviewVisible" :url-list="imagePreviewUrls" :initial-index="imagePreviewIndex" @close="imagePreviewVisible = false" />
|
||||
</basic-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as api from '@/api/business/contract-manage';
|
||||
import BillingPlanEditor from './components/billing-plan-editor.vue';
|
||||
import { ElImageViewer } from 'element-plus';
|
||||
import { OpenFileViewer } from '@open-file-viewer/vue';
|
||||
import { fallbackPlugin, imagePlugin, officePlugin, pdfPlugin, textPlugin } from '@open-file-viewer/core';
|
||||
import '@open-file-viewer/core/style.css';
|
||||
import pdfWorkerSrc from 'pdfjs-dist/build/pdf.worker.mjs?url';
|
||||
|
||||
const viewerPlugins = [
|
||||
imagePlugin(),
|
||||
pdfPlugin({ workerSrc: pdfWorkerSrc, useFetchData: true }),
|
||||
officePlugin({ pdf: { workerSrc: pdfWorkerSrc, useFetchData: true } }),
|
||||
textPlugin(),
|
||||
fallbackPlugin(),
|
||||
];
|
||||
|
||||
export default {
|
||||
components: { BillingPlanEditor, ElImageViewer, OpenFileViewer },
|
||||
data() { return { form: {}, period: [], plans: [], attachments: [], contractFiles: [], contractFileRows: [], selectedContractFiles: [], paymentRatioRows: [], settlementConfigTab: 'pre', changeMaterials: [], imagePreviewVisible: false, imagePreviewUrls: [], imagePreviewIndex: 0, documentPreviewVisible: false, previewFile: {}, viewerPlugins, viewerToolbar: { zoom: true, rotate: true, download: true, fullscreen: true }, feeGenerationMode: 'system', settlementRule: { autoGenerate: 1, settlementType: '月结', billCycleType: '固定截单日', billCutoffDay: 25, cycleDays: '' }, preSettlementConfig: {}, formalSettlementConfig: {}, settlementTypeOptions: ['月结','日结','周结','半月结','固定天数周期结算'], billCycleTypeOptions: ['固定截单日','自然月'], billCutoffDayOptions: Array.from({ length: 31 }, (_, index) => ({ label: `${index + 1}日`, value: index + 1 })), cycleDayOptions: [7,15,30,60].map(value => ({ label: `${value}天`, value })), planDialogVisible: false, planEditor: { planName: '', defaultPlan: false, remark: '', rules: [] }, planEditorIndex: -1, billingElements: ['按重量','按体积','按车辆','按里程','按吨·公里','固定金额(整单一口价)','按数量'], attachmentFileTypes: ['pdf','bmp','jpeg','png','jpg','doc','docx','ppt','pptx','xlsx','xls','eml','msg','zip'], rules: { contractName: [{ required: true, message: '请输入合同名称', trigger: 'blur' }], changeReason: [{ required: true, message: '请输入变更原因', trigger: 'blur' }] } }; },
|
||||
computed: { showSettlementBillCycleType() { return this.settlementRule.settlementType === '月结'; }, showSettlementBillCutoffDay() { return this.showSettlementBillCycleType && this.settlementRule.billCycleType === '固定截单日'; }, showSettlementCycleDays() { return this.settlementRule.settlementType === '固定天数周期结算'; } },
|
||||
mounted() { this.load(); },
|
||||
watch: { settlementConfigTab(tab, oldTab) { if (tab === oldTab) return; if (oldTab === 'pre') this.preSettlementConfig = { ...this.settlementRule }; else this.formalSettlementConfig = { ...this.settlementRule }; this.settlementRule = { ...(tab === 'pre' ? this.preSettlementConfig : this.formalSettlementConfig) }; } },
|
||||
methods: {
|
||||
async load() { const id = this.$route.query.id; if (!id) return; const res = await api.getDetail(id); const data = res.data?.data || res.data || {}; this.form = { ...data, changeType: '合同信息变更' }; this.period = data.startDate && data.endDate ? [data.startDate, data.endDate] : []; this.plans = this.parse(data.billingPlanJson); this.attachments = this.parse(data.attachmentsJson); this.contractFileRows = this.parse(data.contractFileJson); const rules = this.parseObject(data.settlementRuleJson); const pre = this.parseObject(data.preSettlementConfigJson); const formal = this.parseObject(data.formalSettlementConfigJson); const legacy = Object.keys(rules).some(key => !['preSettlementConfig', 'formalSettlementConfig'].includes(key)) ? rules : {}; this.preSettlementConfig = rules.preSettlementConfig || (Object.keys(pre).length ? pre : legacy); this.formalSettlementConfig = rules.formalSettlementConfig || (Object.keys(formal).length ? formal : legacy); this.settlementRule = { ...this.preSettlementConfig }; this.feeGenerationMode = data.feeGenerationMode || (Number(data.billingEnabled) === 0 ? 'manual' : 'system'); this.paymentRatioRows = this.parse(data.paymentRatioJson); },
|
||||
parse(value) { try { const result = JSON.parse(value || '[]'); return Array.isArray(result) ? result : []; } catch { return []; } },
|
||||
parseObject(value) { try { return { autoGenerate: 1, settlementType: '月结', billCycleType: '固定截单日', billCutoffDay: 25, cycleDays: 15, ...(JSON.parse(value || '{}') || {}) }; } catch { return { autoGenerate: 1, settlementType: '月结', billCycleType: '固定截单日', billCutoffDay: 25, cycleDays: 15 }; } },
|
||||
addPlan() { this.planEditorIndex = -1; this.planEditor = { planName: `计费方案${this.plans.length + 1}`, defaultPlan: !this.plans.length, remark: '', rules: [{}] }; this.planDialogVisible = true; },
|
||||
editPlan(row, index) { this.planEditorIndex = index; this.planEditor = JSON.parse(JSON.stringify(row)); this.planDialogVisible = true; },
|
||||
toggleDefaultPlan(value) { if (value) this.plans.forEach(item => { item.defaultPlan = false; }); },
|
||||
savePlan(value, index) { if (index < 0) this.plans.push(value); else this.plans.splice(index, 1, value); if (value.defaultPlan) this.plans.forEach((item, current) => { if (current !== (index < 0 ? this.plans.length - 1 : index)) item.defaultPlan = false; }); },
|
||||
handleSettlementTypeChange(value) { if (value !== '月结') { this.settlementRule.billCycleType = ''; this.settlementRule.billCutoffDay = ''; } else if (!this.settlementRule.billCycleType) this.settlementRule.billCycleType = '固定截单日'; if (value !== '固定天数周期结算') this.settlementRule.cycleDays = ''; },
|
||||
handleCycleTypeChange(value) { if (value === '固定截单日' && !this.settlementRule.billCutoffDay) this.settlementRule.billCutoffDay = 25; if (value !== '固定截单日') this.settlementRule.billCutoffDay = ''; },
|
||||
handleAttachment(event) { const raw = event.raw; if (raw) this.attachments.push({ name: raw.name, size: `${Math.ceil(raw.size / 1024)}KB`, uploadTime: this.$dayjs().format('YYYY-MM-DD HH:mm:ss') }); },
|
||||
handleContractFileChange(list) { this.contractFileRows = (list || []).map(item => ({ ...item, uploadTime: item.uploadTime || this.$dayjs().format('YYYY-MM-DD HH:mm:ss') })); },
|
||||
attachmentUrl(row = {}) { return row.url || row.link || row.fileUrl || row.downloadUrl || row.domain || ''; },
|
||||
attachmentName(row = {}) { return row.originalName || row.name || row.fileName || '附件'; },
|
||||
attachmentExtension(row = {}) { const source = String(this.attachmentName(row) || this.attachmentUrl(row)).split('?')[0]; const index = source.lastIndexOf('.'); return index > -1 ? source.slice(index + 1).toLowerCase() : ''; },
|
||||
isAttachmentImage(row) { return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(this.attachmentExtension(row)); },
|
||||
previewAttachment(row, rows = this.attachments) { const url = this.attachmentUrl(row); if (!url) { this.$message.warning('附件地址为空,无法预览'); return; } if (this.isAttachmentImage(row)) { this.imagePreviewUrls = (rows || []).filter(item => this.isAttachmentImage(item) && this.attachmentUrl(item)).map(item => this.attachmentUrl(item)); this.imagePreviewIndex = Math.max(this.imagePreviewUrls.indexOf(url), 0); this.imagePreviewVisible = true; return; } this.previewFile = { name: this.attachmentName(row), url, mimeType: row.mimeType || row.contentType || '' }; this.documentPreviewVisible = true; },
|
||||
handlePreviewUnsupported() { this.$message.warning('当前文件暂不支持在线预览'); },
|
||||
handlePreviewError() { this.$message.error('附件预览失败'); },
|
||||
removeContractFile(index) { this.contractFileRows.splice(index, 1); },
|
||||
handleContractFileBatchDownload() { (this.selectedContractFiles.length ? this.selectedContractFiles : this.contractFileRows).forEach(row => { if (row.url) window.open(row.url, '_blank'); }); },
|
||||
formatFileSize(value) { const size = Number(value || 0); return size > 1024 * 1024 ? `${(size / 1024 / 1024).toFixed(2)}MB` : `${Math.max(1, Math.ceil(size / 1024))}KB`; },
|
||||
handleChangeMaterial(event) { if (event.raw) this.changeMaterials.push(event.raw); },
|
||||
addPaymentRatioRow() { this.paymentRatioRows.push({ paymentTerm: `第${this.paymentRatioRows.length + 1}笔`, ratioLimit: '', remark: '' }); },
|
||||
async submit() { await this.$refs.formRef.validate(); const total = this.paymentRatioRows.reduce((sum, row) => sum + Number(row.ratioLimit || 0), 0); if (this.paymentRatioRows.length && Math.abs(total - 100) > 0.0001) { this.$message.warning('付款比例上限合计必须等于100%'); return; } const settlementRule = { preSettlementConfig: this.preSettlementConfig, formalSettlementConfig: this.formalSettlementConfig }; await api.submitChange({ ...this.form, startDate: this.period[0], endDate: this.period[1], feeGenerationMode: this.feeGenerationMode, billingEnabled: this.feeGenerationMode === 'system' ? 1 : 0, billingPlanJson: JSON.stringify(this.plans), settlementRuleJson: JSON.stringify(settlementRule), preSettlementConfigJson: JSON.stringify(this.preSettlementConfig), formalSettlementConfigJson: JSON.stringify(this.formalSettlementConfig), paymentRatioJson: JSON.stringify(this.paymentRatioRows), contractFileJson: JSON.stringify(this.contractFileRows), attachmentsJson: JSON.stringify(this.attachments), changeContent: this.form.changeReason, changeReason: this.form.changeReason }); this.$message.success('变更已提交'); this.$router.back(); },
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.contract-change-page { min-height: 100%; background: #fff; }
|
||||
.change-section { padding: 20px 24px; border-bottom: 1px solid #eff1f7; }
|
||||
.dialog-section-title { margin-bottom: 18px; font-size: 16px; font-weight: 600; }
|
||||
.dialog-section-title::before { display: inline-block; width: 4px; height: 16px; margin-right: 8px; vertical-align: -2px; background: #409eff; content: ''; }
|
||||
.section-head { display: flex; align-items: center; justify-content: space-between; }
|
||||
.change-table { margin: 16px 0; }
|
||||
.ratio-tip { margin-bottom: 8px; color: #f56c6c; }
|
||||
.unit { margin-left: 8px; }
|
||||
.inline-field { display: flex; align-items: center; gap: 8px; }
|
||||
.attachment-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
|
||||
.settlement-switch { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
|
||||
.settlement-form { display: grid; grid-template-columns: repeat(3, minmax(240px, 1fr)); gap: 8px 28px; }
|
||||
.change-reason-section { border: 1px dashed #ff8f9a; margin: 20px 16px; }
|
||||
.page-footer { display: flex; gap: 16px; padding: 20px 40px; border-top: 1px solid #eff1f7; }
|
||||
</style>
|
||||
@@ -1,5 +1,11 @@
|
||||
<template>
|
||||
<business-crud-page :api="api" :config="config" :crud-option="option" :menu-width="252" />
|
||||
<business-crud-page
|
||||
:api="api"
|
||||
:config="config"
|
||||
:crud-option="option"
|
||||
:menu-width="252"
|
||||
contract-form-page
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<basic-container class="loading-manage-page">
|
||||
<div class="loading-manage-page__search">
|
||||
<div v-if="!isStandaloneFormPage" class="loading-manage-page__search">
|
||||
<el-form :model="searchForm" label-position="right" label-width="88px">
|
||||
<div class="loading-manage-page__search-grid">
|
||||
<el-form-item label="配载单号">
|
||||
@@ -220,7 +220,7 @@
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="loading-manage-page__toolbar">
|
||||
<div v-if="!isStandaloneFormPage" class="loading-manage-page__toolbar">
|
||||
<div class="loading-manage-page__toolbar-left">
|
||||
<el-button type="primary" @click="openLoadingDialog('add')">新建配载</el-button>
|
||||
<el-button type="primary" plain @click="handleExport">批量导出</el-button>
|
||||
@@ -240,6 +240,7 @@
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
v-if="!isStandaloneFormPage"
|
||||
ref="tableRef"
|
||||
v-loading="loading"
|
||||
:data="tableData"
|
||||
@@ -260,7 +261,7 @@
|
||||
>{{ row.loadingNo }}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="配载子单号" prop="loadingSubNos" min-width="220">
|
||||
<el-table-column label="运单号" prop="loadingSubNos" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<span class="loading-manage-page__sub-nos">
|
||||
<template v-if="splitText(row.loadingSubNos).length">
|
||||
@@ -280,14 +281,11 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="车牌号"
|
||||
prop="vehicleNo"
|
||||
min-width="170"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #header>
|
||||
<span class="loading-manage-page__vehicle-header">车牌号/航班号/船号<br />班列号</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
/>
|
||||
<el-table-column label="司机" prop="driverName" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="联系电话" prop="driverPhone" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="承运类型" prop="carrierType" min-width="120" show-overflow-tooltip />
|
||||
@@ -357,7 +355,7 @@
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="loading-manage-page__pagination">
|
||||
<div v-if="!isStandaloneFormPage" class="loading-manage-page__pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="page.currentPage"
|
||||
v-model:page-size="page.pageSize"
|
||||
@@ -370,14 +368,24 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
width="96%"
|
||||
top="3vh"
|
||||
class="loading-manage-dialog"
|
||||
<component
|
||||
:is="isStandaloneFormPage ? 'div' : 'el-dialog'"
|
||||
:model-value="isStandaloneFormPage ? undefined : dialogVisible"
|
||||
:title="isStandaloneFormPage ? undefined : dialogTitle"
|
||||
:append-to-body="isStandaloneFormPage ? undefined : true"
|
||||
:destroy-on-close="isStandaloneFormPage ? undefined : true"
|
||||
:width="isStandaloneFormPage ? undefined : '96%'"
|
||||
:top="isStandaloneFormPage ? undefined : '3vh'"
|
||||
:class="[
|
||||
'loading-manage-dialog',
|
||||
{ 'loading-manage-form-dialog': isStandaloneFormPage },
|
||||
{ 'loading-manage-form-page': isStandaloneFormPage },
|
||||
]"
|
||||
:modal="isStandaloneFormPage ? undefined : true"
|
||||
:show-close="isStandaloneFormPage ? undefined : true"
|
||||
:close-on-click-modal="isStandaloneFormPage ? undefined : true"
|
||||
:close-on-press-escape="isStandaloneFormPage ? undefined : true"
|
||||
@update:model-value="value => !isStandaloneFormPage && (dialogVisible = value)"
|
||||
@closed="resetLoadingDialog"
|
||||
>
|
||||
<div v-loading="dialogLoading" class="loading-manage-dialog__body">
|
||||
@@ -396,7 +404,7 @@
|
||||
</section-card>
|
||||
<section-card>
|
||||
<template #title><span class="loading-detail__route-title">运输路线<el-link type="primary" @click="openRouteChangeDialog">变更运输路线</el-link></span></template>
|
||||
<div v-if="routeNodes.length" class="loading-detail__steps"><div v-for="(node, index) in routeNodes" :key="node.key || index" class="loading-detail__step"><div class="loading-detail__step-line"><span :class="['loading-detail__step-dot', index === 0 ? 'start' : index === routeNodes.length - 1 ? 'end' : 'middle']">{{ index === 0 ? '起' : index === routeNodes.length - 1 ? '终' : '经' }}</span></div><div class="loading-detail__step-address">{{ node.address }}</div><div class="loading-detail__step-tags"><el-tag v-for="tag in node.tags || []" :key="tag" size="small">{{ tag }}</el-tag></div></div></div><el-empty v-else description="暂无路线" :image-size="60" />
|
||||
<div v-if="routeNodes.length" class="loading-detail__steps"><div v-for="(node, index) in routeNodes" :key="node.key || index" class="loading-detail__step"><div class="loading-detail__step-line"><span :class="['loading-detail__step-dot', index === 0 ? 'start' : index === routeNodes.length - 1 ? 'end' : 'middle']">{{ index === 0 ? '起' : index === routeNodes.length - 1 ? '终' : '经' }}</span></div><div class="loading-detail__step-address">{{ node.displayAddress || formatAddressText(node.address) }}</div><div class="loading-detail__step-tags"><el-tag v-for="tag in node.tags || []" :key="tag" size="small">{{ tag }}</el-tag></div></div></div><el-empty v-else description="暂无路线" :image-size="60" />
|
||||
</section-card>
|
||||
<section-card title="货物信息"><el-table :data="cargoRows" border height="220"><el-table-column type="index" label="序号" width="70"/><el-table-column prop="projectName" label="项目名称" min-width="140"/><el-table-column prop="cargoName" label="货物名称" min-width="140"/><el-table-column prop="cargoType" label="货物类型" min-width="120"/><el-table-column prop="packageType" label="包装" min-width="100"/><el-table-column prop="weight" label="重量(吨)" min-width="110"/><el-table-column prop="volume" label="体积(方)" min-width="110"/><el-table-column prop="quantity" label="数量" min-width="90"/><el-table-column prop="materialCode" label="物料编码" min-width="120"/><el-table-column prop="equipmentCode" label="设备编码" min-width="120"/><el-table-column prop="brand" label="品牌" min-width="120"/><el-table-column prop="specificationModel" label="规格型号" min-width="140"/><el-table-column prop="remark" label="备注" min-width="160"/><el-table-column prop="unitPrice" label="货物单价(元)" min-width="140"/><el-table-column prop="planDate" label="计划日期" min-width="130"/></el-table></section-card>
|
||||
<section-card title="关联运单信息"><el-table :data="selectedWaybillRows" border><el-table-column type="index" label="序号" width="70"/><el-table-column prop="waybillNo" label="运单号" min-width="150"><template #default="{ row }"><el-link type="primary">{{ row.waybillNo || '-' }}</el-link></template></el-table-column><el-table-column label="配载单号" min-width="150"><template #default>{{ dialogForm.loadingNo || '-' }}</template></el-table-column><el-table-column prop="projectName" label="项目名称" min-width="150"/><el-table-column prop="customerName" label="客户" min-width="140"/><el-table-column prop="departureAddress" label="发货地址" min-width="220" show-overflow-tooltip/><el-table-column prop="arrivalAddress" label="到货地址" min-width="220" show-overflow-tooltip/></el-table></section-card>
|
||||
@@ -734,13 +742,21 @@
|
||||
label="发货地"
|
||||
min-width="220"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ formatWaybillAddress(row, 'departure') }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="arrivalAddress"
|
||||
label="到货地"
|
||||
min-width="220"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ formatWaybillAddress(row, 'arrival') }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="110" fixed="right">
|
||||
<template #default="{ $index }">
|
||||
<el-link
|
||||
@@ -766,7 +782,7 @@
|
||||
>
|
||||
<el-icon class="loading-manage-dialog__route-handle"><Rank /></el-icon>
|
||||
<span :class="['loading-manage-dialog__route-type', `loading-manage-dialog__route-type--${routeTypeClass(index)}`]">{{ routeTypeText(index) }}</span>
|
||||
<span class="loading-manage-dialog__route-address">{{ node.address }}</span>
|
||||
<span class="loading-manage-dialog__route-address">{{ node.displayAddress || formatAddressText(node.address) }}</span>
|
||||
<span class="loading-manage-dialog__route-tags">
|
||||
<el-tag v-for="tag in node.tags" :key="tag" size="small">{{ tag }}</el-tag>
|
||||
</span>
|
||||
@@ -804,7 +820,7 @@
|
||||
@change="handleCarrierTypeChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="loading-manage-dialog__grid">
|
||||
<div class="loading-manage-dialog__grid loading-manage-dialog__task-grid">
|
||||
<el-form-item v-if="!['自运', '网货平台'].includes(dialogForm.carrierType)" label="承运商" required>
|
||||
<el-select
|
||||
v-model="dialogForm.carrierName"
|
||||
@@ -893,10 +909,24 @@
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div v-if="isStandaloneFormPage" class="loading-manage-dialog__footer">
|
||||
<template v-if="dialogReadonly">
|
||||
<el-button @click="closeLoadingDialog">关闭</el-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-button @click="closeLoadingDialog">关闭</el-button>
|
||||
<el-button type="primary" plain :loading="dialogSaving === 'draft'" @click="saveDraft">
|
||||
暂存
|
||||
</el-button>
|
||||
<el-button type="primary" :loading="dialogSaving === 'submit'" @click="submitLoading">
|
||||
{{ dialogMode === 'reassign' ? '确认派单' : '确认' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</div>
|
||||
<template v-if="!isStandaloneFormPage" #footer>
|
||||
<div class="loading-manage-dialog__footer">
|
||||
<template v-if="dialogReadonly">
|
||||
<el-button @click="dialogVisible = false">关闭</el-button>
|
||||
<el-button @click="closeLoadingDialog">关闭</el-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-button @click="clearLoadingDialog">清空</el-button>
|
||||
@@ -909,7 +939,7 @@
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</component>
|
||||
<el-dialog v-model="routeChangeVisible" title="变更运输路线" append-to-body destroy-on-close width="78%" class="loading-route-change-dialog">
|
||||
<el-tabs v-model="routeChangeTab">
|
||||
<el-tab-pane label="变更路线" name="change">
|
||||
@@ -1192,19 +1222,51 @@ export default {
|
||||
typeof item === 'string' ? { name: item.split('/').pop(), url: item } : { name: item.name || item.fileName || '附件', url: item.url || item.link || item.fileUrl }
|
||||
);
|
||||
},
|
||||
isStandaloneFormPage() {
|
||||
return (
|
||||
this.$route.path === '/business/loading-manage/form' &&
|
||||
['add', 'edit'].includes(this.$route.query.mode)
|
||||
);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.loadTransportTypeOptions();
|
||||
this.loadTable();
|
||||
if (this.isStandaloneFormPage) {
|
||||
this.syncStandaloneTagTitle();
|
||||
this.initStandaloneFormPage();
|
||||
}
|
||||
const detailId = this.$route.query.detailId;
|
||||
if (detailId) this.openLoadingDialog('view', { id: detailId });
|
||||
},
|
||||
watch: {
|
||||
$route(to, from) {
|
||||
if (this.isStandaloneFormPage) {
|
||||
this.syncStandaloneTagTitle();
|
||||
if (
|
||||
to.query.mode !== from?.query?.mode ||
|
||||
String(to.query.id || '') !== String(from?.query.id || '')
|
||||
) {
|
||||
this.initStandaloneFormPage();
|
||||
}
|
||||
} else if (from?.query?.mode && this.dialogVisible) {
|
||||
this.closeLoadingDialog();
|
||||
}
|
||||
},
|
||||
'$route.query.detailId'(detailId) {
|
||||
if (detailId) this.openLoadingDialog('view', { id: detailId });
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
syncStandaloneTagTitle() {
|
||||
if (!this.isStandaloneFormPage) return;
|
||||
const title = this.$route.query.mode === 'edit' ? '编辑配载管理' : '新增配载管理';
|
||||
if (this.$route.query.name === title) return;
|
||||
this.$store.commit('SET_TAG', {
|
||||
fullPath: this.$route.fullPath,
|
||||
query: { ...this.$route.query, name: title },
|
||||
});
|
||||
},
|
||||
buildQueryParams(form) {
|
||||
const params = { ...form };
|
||||
const [startDateStart, startDateEnd] = params.startDateRange || [];
|
||||
@@ -1333,6 +1395,64 @@ export default {
|
||||
.flatMap(value => this.splitText(value));
|
||||
return [...new Set(values)].join(',') || '-';
|
||||
},
|
||||
formatWaybillAddress(row, type) {
|
||||
const prefix = type === 'departure' ? 'departure' : 'arrival';
|
||||
const address = row[`${prefix}Address`] || row[`${prefix}Name`] || '';
|
||||
const region = this.mergeRouteAddressParts(
|
||||
this.routeProvinceName(row, prefix),
|
||||
row[`${prefix}CityName`] || row[`${prefix}City`],
|
||||
row[`${prefix}DistrictName`] || row[`${prefix}District`]
|
||||
);
|
||||
const source = this.mergeRouteAddressParts(region, '', address);
|
||||
const parts = this.parseWaybillAddress(source);
|
||||
const detailAddress =
|
||||
row[`${prefix}DetailAddress`] ||
|
||||
row[`${prefix}AddressDetail`] ||
|
||||
parts.detailAddress ||
|
||||
address;
|
||||
return [parts.province, parts.city, parts.district, detailAddress]
|
||||
.map(value => String(value || '').trim())
|
||||
.filter(Boolean)
|
||||
.join(' / ') || '-';
|
||||
},
|
||||
parseWaybillAddress(address) {
|
||||
let rest = String(address || '').trim();
|
||||
const result = { province: '', city: '', district: '', detailAddress: '' };
|
||||
if (!rest) return result;
|
||||
|
||||
const municipalityMatch = rest.match(/^(北京市|天津市|上海市|重庆市)/);
|
||||
if (municipalityMatch) {
|
||||
result.province = municipalityMatch[0];
|
||||
result.city = municipalityMatch[0];
|
||||
rest = rest.slice(municipalityMatch[0].length);
|
||||
} else {
|
||||
const provinceMatch = rest.match(/^(.+?(?:省|自治区|特别行政区))/);
|
||||
if (provinceMatch) {
|
||||
result.province = provinceMatch[0];
|
||||
rest = rest.slice(provinceMatch[0].length);
|
||||
}
|
||||
const cityMatch = rest.match(/^(.+?(?:市|自治州|地区|盟))/);
|
||||
if (cityMatch) {
|
||||
result.city = cityMatch[0];
|
||||
rest = rest.slice(cityMatch[0].length);
|
||||
}
|
||||
}
|
||||
|
||||
const districtMatch = rest.match(/^(.+?(?:自治县|自治旗|林区|矿区|新区|开发区|区|县|旗))/);
|
||||
if (districtMatch) {
|
||||
result.district = districtMatch[0];
|
||||
rest = rest.slice(districtMatch[0].length);
|
||||
}
|
||||
result.detailAddress = rest;
|
||||
return result;
|
||||
},
|
||||
formatAddressText(address) {
|
||||
const parts = this.parseWaybillAddress(address);
|
||||
return [parts.province, parts.city, parts.district, parts.detailAddress]
|
||||
.map(value => String(value || '').trim())
|
||||
.filter(Boolean)
|
||||
.join(' / ') || String(address || '').trim() || '-';
|
||||
},
|
||||
mergeRouteAddressParts(region, district, address) {
|
||||
const values = [region, district, address].map(value => String(value || '').trim()).filter(Boolean);
|
||||
if (!values.length) return '';
|
||||
@@ -1366,8 +1486,8 @@ export default {
|
||||
if (status === 'draft') {
|
||||
return [
|
||||
{ type: 'edit', label: '编辑' },
|
||||
{ type: 'delete', label: '删除' },
|
||||
{ type: 'copy', label: '复制' },
|
||||
{ type: 'delete', label: '删除' },
|
||||
];
|
||||
}
|
||||
if (status === 'pending') {
|
||||
@@ -1406,7 +1526,24 @@ export default {
|
||||
};
|
||||
actionMap[type]?.();
|
||||
},
|
||||
initStandaloneFormPage() {
|
||||
if (!this.isStandaloneFormPage) return;
|
||||
this.openLoadingDialog(this.$route.query.mode === 'edit' ? 'edit' : 'add', {
|
||||
id: this.$route.query.id,
|
||||
});
|
||||
},
|
||||
async openLoadingDialog(mode, row) {
|
||||
if (['add', 'edit'].includes(mode) && !this.isStandaloneFormPage) {
|
||||
this.$router.push({
|
||||
path: '/business/loading-manage/form',
|
||||
query: {
|
||||
mode,
|
||||
...(mode === 'edit' && row?.id ? { id: row.id } : {}),
|
||||
name: mode === 'edit' ? '编辑配载管理' : '新增配载管理',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.dialogMode = mode;
|
||||
this.dialogVisible = true;
|
||||
this.dialogLoading = true;
|
||||
@@ -1439,6 +1576,14 @@ export default {
|
||||
this.dialogLoading = false;
|
||||
}
|
||||
},
|
||||
closeLoadingDialog() {
|
||||
if (this.isStandaloneFormPage) {
|
||||
this.$router.$avueRouter?.closeTag?.();
|
||||
this.$router.push({ path: '/business/loading-manage', query: {} });
|
||||
return;
|
||||
}
|
||||
this.dialogVisible = false;
|
||||
},
|
||||
resetLoadingDialog() {
|
||||
this.resetDialogData();
|
||||
this.dialogSaving = '';
|
||||
@@ -1814,18 +1959,18 @@ export default {
|
||||
},
|
||||
buildRouteNodes(rows) {
|
||||
const nodeMap = new Map();
|
||||
const appendNode = (address, tag) => {
|
||||
const appendNode = (address, displayAddress, tag) => {
|
||||
if (!address) return;
|
||||
if (!nodeMap.has(address)) nodeMap.set(address, { address, tags: [] });
|
||||
if (!nodeMap.has(address)) nodeMap.set(address, { address, displayAddress, tags: [] });
|
||||
nodeMap.get(address).tags.push(tag);
|
||||
};
|
||||
rows.forEach((row, rowIndex) => {
|
||||
const departure = row.departureAddress || row.departureName;
|
||||
appendNode(departure, `装${rowIndex + 1}`);
|
||||
appendNode(departure, this.formatWaybillAddress(row, 'departure'), `装${rowIndex + 1}`);
|
||||
});
|
||||
rows.forEach((row, rowIndex) => {
|
||||
const arrival = row.arrivalAddress || row.arrivalName;
|
||||
appendNode(arrival, `卸${rowIndex + 1}`);
|
||||
appendNode(arrival, this.formatWaybillAddress(row, 'arrival'), `卸${rowIndex + 1}`);
|
||||
});
|
||||
return [...nodeMap.values()].map((node, index) => ({
|
||||
...node,
|
||||
@@ -1850,10 +1995,24 @@ export default {
|
||||
const rows = [...this.routeNodes];
|
||||
const [target] = rows.splice(this.routeDragIndex, 1);
|
||||
rows.splice(index, 0, target);
|
||||
this.routeNodes = rows;
|
||||
this.routeDragIndex = -1;
|
||||
if (!this.validateRouteEndpoints(rows)) return;
|
||||
this.routeNodes = rows;
|
||||
this.syncRouteAddressFields();
|
||||
},
|
||||
validateRouteEndpoints(nodes = this.routeNodes) {
|
||||
const startTags = nodes[0]?.tags || [];
|
||||
if (startTags.some(tag => String(tag).startsWith('卸'))) {
|
||||
ElMessage.warning('起点不能设置卸货');
|
||||
return false;
|
||||
}
|
||||
const endTags = nodes[nodes.length - 1]?.tags || [];
|
||||
if (endTags.some(tag => String(tag).startsWith('装'))) {
|
||||
ElMessage.warning('终点不能设置装货');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
syncRouteAddressFields() {
|
||||
const nodes = this.routeNodes.filter(item => item.address);
|
||||
this.dialogForm.departureAddress = nodes[0]?.address || '';
|
||||
@@ -1945,6 +2104,13 @@ export default {
|
||||
if (draftMode) {
|
||||
return true;
|
||||
}
|
||||
if (this.dialogMode === 'add' && this.selectedWaybillRows.length < 2) {
|
||||
ElMessage.warning('请至少选择两条待配载运单');
|
||||
return false;
|
||||
}
|
||||
if (!this.validateRouteEndpoints()) {
|
||||
return false;
|
||||
}
|
||||
if (!this.dialogForm.departureAddress || !this.dialogForm.arrivalAddress) {
|
||||
ElMessage.warning('请确认配载路线');
|
||||
return false;
|
||||
@@ -1995,7 +2161,7 @@ export default {
|
||||
await loadingApi.submit(payload);
|
||||
ElMessage.success('确认成功');
|
||||
}
|
||||
this.dialogVisible = false;
|
||||
this.closeLoadingDialog();
|
||||
this.loadTable();
|
||||
} finally {
|
||||
this.dialogSaving = '';
|
||||
@@ -2249,11 +2415,6 @@ export default {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.loading-manage-page__vehicle-header {
|
||||
line-height: 20px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.loading-manage-page__status {
|
||||
color: #303133;
|
||||
}
|
||||
@@ -2340,7 +2501,7 @@ export default {
|
||||
|
||||
.loading-manage-dialog__waybill-route-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 2fr) minmax(320px, 1fr);
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
margin-bottom: 16px;
|
||||
@@ -2424,6 +2585,23 @@ export default {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.loading-manage-dialog__task-grid {
|
||||
:deep(.el-form-item__label) {
|
||||
flex: 0 0 100px;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
:deep(.el-form-item__content),
|
||||
:deep(.el-input),
|
||||
:deep(.el-select),
|
||||
:deep(.el-autocomplete),
|
||||
:deep(.el-date-editor.el-input),
|
||||
:deep(.el-date-editor.el-input__wrapper) {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.dialog-section-title) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -2503,6 +2681,90 @@ export default {
|
||||
.loading-route-change-dialog__record-content { white-space: normal; overflow-wrap: anywhere; line-height: 22px; }
|
||||
}
|
||||
.loading-common-address-dialog__toolbar { display: flex; gap: 8px; margin-bottom: 12px; }.loading-common-address-dialog__toolbar .el-input { flex: 1; }
|
||||
:global(.loading-manage-form-page) {
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
background: #f5f6fa;
|
||||
}
|
||||
:global(.loading-manage-form-page .loading-manage-dialog__body) {
|
||||
max-height: none;
|
||||
overflow: visible;
|
||||
padding-right: 0;
|
||||
}
|
||||
:global(.loading-manage-form-dialog) {
|
||||
width: 100% !important;
|
||||
min-height: 100%;
|
||||
margin: 0 !important;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
:global(.loading-manage-form-dialog .el-dialog__body) {
|
||||
max-height: none;
|
||||
padding: 20px 24px 96px;
|
||||
background: #f5f6fa;
|
||||
}
|
||||
:global(.el-overlay:has(.loading-manage-form-dialog)) {
|
||||
position: fixed !important;
|
||||
inset: 92px 0 0 230px !important;
|
||||
z-index: 100 !important;
|
||||
overflow: auto;
|
||||
background: transparent;
|
||||
}
|
||||
:global(.el-overlay:has(.loading-manage-form-dialog) .el-overlay-dialog) {
|
||||
min-height: 100%;
|
||||
overflow: visible;
|
||||
}
|
||||
:global(.loading-manage-form-dialog .el-dialog__header) {
|
||||
padding: 18px 24px;
|
||||
margin-right: 0;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #eff1f7;
|
||||
}
|
||||
:global(.loading-manage-form-dialog .el-dialog__footer) {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 230px;
|
||||
z-index: 1001;
|
||||
box-sizing: border-box;
|
||||
padding: 12px 24px;
|
||||
background: #fff;
|
||||
border-top: 1px solid #eff1f7;
|
||||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
:global(.loading-manage-form-page .loading-manage-dialog__footer) {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 230px;
|
||||
z-index: 1001;
|
||||
box-sizing: border-box;
|
||||
min-height: 64px;
|
||||
padding: 12px 24px;
|
||||
background: #fff;
|
||||
border-top: 1px solid #eff1f7;
|
||||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
:global(.avue--collapse .el-overlay:has(.loading-manage-form-dialog)) {
|
||||
left: 60px !important;
|
||||
}
|
||||
:global(.avue-layout--horizontal .el-overlay:has(.loading-manage-form-dialog)) {
|
||||
top: 92px !important;
|
||||
left: 0 !important;
|
||||
}
|
||||
:global(.avue--collapse .loading-manage-form-dialog .el-dialog__footer) {
|
||||
left: 60px;
|
||||
}
|
||||
:global(.avue-layout--horizontal .loading-manage-form-dialog .el-dialog__footer) {
|
||||
left: 0;
|
||||
}
|
||||
:global(.avue--collapse .loading-manage-form-page .loading-manage-dialog__footer) {
|
||||
left: 60px;
|
||||
}
|
||||
:global(.avue-layout--horizontal .loading-manage-form-page .loading-manage-dialog__footer) {
|
||||
left: 0;
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.loading-manage-dialog__grid {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<basic-container class="project-apply-page">
|
||||
<avue-crud
|
||||
v-if="!isProjectFormPage"
|
||||
:option="tableOption"
|
||||
:table-loading="loading"
|
||||
:data="data"
|
||||
@@ -83,22 +84,23 @@
|
||||
</avue-crud>
|
||||
|
||||
<empty-pagination
|
||||
v-if="!isProjectFormPage"
|
||||
:page="page"
|
||||
@size-change="sizeChange"
|
||||
@current-change="currentChange"
|
||||
@load="onLoad(page, query)"
|
||||
/>
|
||||
|
||||
<el-dialog
|
||||
v-model="projectBox"
|
||||
:title="projectDialogTitle"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
width="1440px"
|
||||
top="4vh"
|
||||
class="project-apply-dialog"
|
||||
<component
|
||||
:is="projectFormContainer"
|
||||
v-if="projectBox"
|
||||
v-bind="projectFormContainerProps"
|
||||
@update:model-value="projectBox = $event"
|
||||
@closed="resetProjectDialog"
|
||||
>
|
||||
<div v-if="isProjectFormPage" class="project-apply-page__title">
|
||||
{{ projectDialogTitle }}
|
||||
</div>
|
||||
<el-form
|
||||
ref="projectForm"
|
||||
:model="form"
|
||||
@@ -215,6 +217,7 @@
|
||||
filterable
|
||||
clearable
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
:loading="customerLoading"
|
||||
@visible-change="visible => visible && loadCustomerOptions('客户')"
|
||||
@change="handleCustomerChange"
|
||||
@@ -235,6 +238,7 @@
|
||||
filterable
|
||||
clearable
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
:loading="carrierLoading"
|
||||
@visible-change="visible => visible && loadCustomerOptions('承运商')"
|
||||
@change="handleCarrierChange"
|
||||
@@ -602,11 +606,10 @@
|
||||
</template>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<div
|
||||
class="project-apply-dialog__footer"
|
||||
:class="{ 'project-apply-dialog__footer--change': isChangeDialog }"
|
||||
>
|
||||
<div
|
||||
class="project-apply-dialog__footer"
|
||||
:class="{ 'project-apply-dialog__footer--change': isChangeDialog }"
|
||||
>
|
||||
<template v-if="isChangeDialog">
|
||||
<el-button type="primary" :loading="submitLoading" @click="saveChangeProject">
|
||||
保存
|
||||
@@ -614,10 +617,11 @@
|
||||
<el-button type="primary" :loading="submitLoading" @click="submitChangeProject">
|
||||
提交
|
||||
</el-button>
|
||||
<el-button @click="handleCancelProject">取消</el-button>
|
||||
<el-button v-if="!isProjectFormPage" @click="handleCancelProject">取消</el-button>
|
||||
<el-button v-if="isProjectFormPage" @click="closeProjectForm">返回</el-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-button @click="handleCancelProject">取消</el-button>
|
||||
<el-button v-if="!isProjectFormPage" @click="handleCancelProject">取消</el-button>
|
||||
<el-button
|
||||
v-if="!dialogReadonly"
|
||||
type="primary"
|
||||
@@ -634,10 +638,10 @@
|
||||
>
|
||||
提交
|
||||
</el-button>
|
||||
<el-button v-if="isProjectFormPage" @click="closeProjectForm">返回</el-button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</component>
|
||||
|
||||
<el-dialog
|
||||
v-model="userBox"
|
||||
@@ -1005,6 +1009,24 @@ export default {
|
||||
isChangeDialog() {
|
||||
return this.dialogType === 'change';
|
||||
},
|
||||
isProjectFormPage() {
|
||||
return this.$route.path === '/business/project-apply/form';
|
||||
},
|
||||
projectFormContainer() {
|
||||
return this.isProjectFormPage ? 'div' : 'el-dialog';
|
||||
},
|
||||
projectFormContainerProps() {
|
||||
if (this.isProjectFormPage) return { class: 'project-apply-page-form' };
|
||||
return {
|
||||
modelValue: this.projectBox,
|
||||
title: this.projectDialogTitle,
|
||||
appendToBody: true,
|
||||
destroyOnClose: true,
|
||||
width: '1440px',
|
||||
top: '4vh',
|
||||
class: 'project-apply-dialog',
|
||||
};
|
||||
},
|
||||
isBasicInfoReadonly() {
|
||||
return this.dialogReadonly || this.isChangeDialog;
|
||||
},
|
||||
@@ -1037,6 +1059,9 @@ export default {
|
||||
this.loadDeptOptions();
|
||||
this.loadCargoTypeOptions();
|
||||
this.loadTransportTypeOptions();
|
||||
if (this.isProjectFormPage) {
|
||||
this.openProjectFormPage();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
buildTableOption() {
|
||||
@@ -1055,6 +1080,26 @@ export default {
|
||||
hasPermission(code) {
|
||||
return this.isAdmin || this.validData(this.permission && this.permission[code], false);
|
||||
},
|
||||
openProjectFormPage() {
|
||||
const type = this.$route.query.mode === 'edit' ? 'edit' : 'add';
|
||||
const id = this.$route.query.id;
|
||||
this.dialogType = type;
|
||||
this.dialogReadonly = false;
|
||||
this.projectBox = true;
|
||||
if (type === 'add') {
|
||||
this.applyProjectDetail({ ...emptyForm(), projectType: '普通项目' });
|
||||
this.fillDefaultUsers();
|
||||
return;
|
||||
}
|
||||
this.api.getDetail(id).then(res => this.applyProjectDetail(res.data.data || {}));
|
||||
},
|
||||
closeProjectForm() {
|
||||
if (this.isProjectFormPage) {
|
||||
this.$router.push('/business/project-apply');
|
||||
} else {
|
||||
this.projectBox = false;
|
||||
}
|
||||
},
|
||||
statusValue(row) {
|
||||
return row[this.config.statusProp || 'status'];
|
||||
},
|
||||
@@ -1265,6 +1310,16 @@ export default {
|
||||
});
|
||||
},
|
||||
openProjectDialog(type, row = {}) {
|
||||
if (['add', 'edit'].includes(type)) {
|
||||
this.$router.push({
|
||||
path: '/business/project-apply/form',
|
||||
query:
|
||||
type === 'edit'
|
||||
? { mode: type, id: row.id, name: `编辑${this.config.title}` }
|
||||
: { mode: type, name: `新增${this.config.title}` },
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.dialogType = type;
|
||||
this.dialogReadonly = type === 'view';
|
||||
this.projectBox = true;
|
||||
@@ -1390,7 +1445,7 @@ export default {
|
||||
return;
|
||||
}
|
||||
this.$message.success('操作成功!');
|
||||
this.projectBox = false;
|
||||
this.closeProjectForm();
|
||||
this.onLoad(this.page, this.query);
|
||||
},
|
||||
error => {
|
||||
@@ -1412,7 +1467,7 @@ export default {
|
||||
return;
|
||||
}
|
||||
this.$message.success('保存成功!');
|
||||
this.projectBox = false;
|
||||
this.closeProjectForm();
|
||||
this.onLoad(this.page, this.query);
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -1420,8 +1475,12 @@ export default {
|
||||
});
|
||||
},
|
||||
handleCancelProject() {
|
||||
if (this.isProjectFormPage) {
|
||||
this.closeProjectForm();
|
||||
return;
|
||||
}
|
||||
if (!['add', 'majorSupplement', 'change'].includes(this.dialogType)) {
|
||||
this.projectBox = false;
|
||||
this.closeProjectForm();
|
||||
return;
|
||||
}
|
||||
this.$confirm('确认后直接关闭弹窗,列表无数据变更,是否继续?', '提示', {
|
||||
@@ -1429,7 +1488,7 @@ export default {
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}).then(() => {
|
||||
this.projectBox = false;
|
||||
this.closeProjectForm();
|
||||
});
|
||||
},
|
||||
saveChangeProject() {
|
||||
@@ -1456,7 +1515,7 @@ export default {
|
||||
return;
|
||||
}
|
||||
this.$message.success(`${needSubmit ? '提交' : '保存'}成功!`);
|
||||
this.projectBox = false;
|
||||
this.closeProjectForm();
|
||||
this.onLoad(this.page, this.query);
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -1905,8 +1964,8 @@ export default {
|
||||
|
||||
&__grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
column-gap: 72px;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
column-gap: 24px;
|
||||
row-gap: 0;
|
||||
margin-bottom: 12px;
|
||||
background: #fff;
|
||||
@@ -2042,17 +2101,62 @@ export default {
|
||||
background: #f5f6fa;
|
||||
}
|
||||
|
||||
.project-apply-page-form {
|
||||
min-height: 100%;
|
||||
padding: 20px 24px 92px;
|
||||
background: #f5f6fa;
|
||||
}
|
||||
|
||||
.project-apply-page__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
color: #303133;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
|
||||
&::before {
|
||||
width: 4px;
|
||||
height: 18px;
|
||||
margin-right: 8px;
|
||||
background: #409eff;
|
||||
border-radius: 2px;
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
|
||||
.project-apply-dialog__footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
|
||||
.project-apply-page-form & {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
left: 230px;
|
||||
bottom: 0;
|
||||
z-index: 10;
|
||||
margin: 0;
|
||||
padding: 12px 24px;
|
||||
border-top: 1px solid #eff1f7;
|
||||
background: #fff;
|
||||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
&--change {
|
||||
justify-content: flex-start;
|
||||
padding-left: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.avue--collapse .project-apply-page-form .project-apply-dialog__footer) {
|
||||
left: 60px;
|
||||
}
|
||||
|
||||
:global(.avue-layout--horizontal .project-apply-page-form .project-apply-dialog__footer) {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.project-apply-user-dialog {
|
||||
&__search {
|
||||
display: grid;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<business-crud-page :api="api" :config="config" :crud-option="option" :menu-width="270" />
|
||||
<business-crud-page :api="api" :config="config" :crud-option="option" :menu-width="270" standalone-form-page />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<business-crud-page :api="api" :config="config" :crud-option="option" :detail-id="$route.query.detailId" :menu-width="220" />
|
||||
<business-crud-page :api="api" :config="config" :crud-option="option" :detail-id="$route.query.detailId" :menu-width="220" standalone-form-page />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -92,11 +92,7 @@
|
||||
label="已关联运单"
|
||||
width="115"
|
||||
/><el-table-column prop="unRelatedWaybillCount" label="未关联运单" width="115" />
|
||||
<el-table-column prop="status" label="状态" width="100"
|
||||
><template #default="{ row }">{{
|
||||
Number(row.status) === 1 ? '启用' : '停用'
|
||||
}}</template></el-table-column
|
||||
><el-table-column prop="createTime" label="创建时间" min-width="170" />
|
||||
<el-table-column prop="createTime" label="创建时间" min-width="170" />
|
||||
<el-table-column
|
||||
prop="auditStatus"
|
||||
label="审核状态"
|
||||
@@ -146,6 +142,65 @@
|
||||
</section>
|
||||
</basic-container>
|
||||
|
||||
<el-dialog v-model="detailVisible" width="960px" destroy-on-close>
|
||||
<template #header>
|
||||
<div class="voucher-manage-page__dialog-title">凭证详情</div>
|
||||
</template>
|
||||
<el-descriptions v-loading="detailLoading" :column="2" border>
|
||||
<el-descriptions-item label="项目名称">
|
||||
{{ detailValue(detailRow.projectName) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="凭证批次号">
|
||||
{{ detailValue(detailRow.voucherBatchNo) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="运单批次号" :span="2">
|
||||
{{ detailValue(detailRow.waybillBatchNo) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="文件名称" :span="2">
|
||||
<el-link v-if="detailRow.fileUrl" type="primary" @click="download(detailRow)">
|
||||
{{ detailValue(detailRow.fileName) }}
|
||||
</el-link>
|
||||
<template v-else>{{ detailValue(detailRow.fileName) }}</template>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="上传来源">
|
||||
{{ detailValue(detailRow.uploadSource) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="承运商名称">
|
||||
{{ detailValue(detailRow.carrierName) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="处理状态">
|
||||
{{ detailValue(detailRow.processStatus) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="审核状态">
|
||||
{{ detailValue(detailRow.auditStatus) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="凭证数量">
|
||||
{{ detailValue(detailRow.voucherCount) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="已关联运单">
|
||||
{{ detailValue(detailRow.relatedWaybillCount) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="未关联运单">
|
||||
{{ detailValue(detailRow.unRelatedWaybillCount) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建人">
|
||||
{{ detailValue(detailRow.createUserName) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">
|
||||
{{ detailValue(detailRow.createTime) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="更新人">
|
||||
{{ detailValue(detailRow.updateUserName) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间">
|
||||
{{ detailValue(detailRow.updateTime) }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="detailVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="uploadVisible"
|
||||
:title="editing.id ? '更换运单批次' : '批量导入凭证'"
|
||||
@@ -414,6 +469,9 @@ const loading = ref(false),
|
||||
rows = ref([]),
|
||||
createRange = ref([]),
|
||||
searchExpanded = ref(false),
|
||||
detailVisible = ref(false),
|
||||
detailLoading = ref(false),
|
||||
detailRow = ref({}),
|
||||
uploadVisible = ref(false),
|
||||
batchVisible = ref(false),
|
||||
progressVisible = ref(false),
|
||||
@@ -875,7 +933,21 @@ const removeRow = row =>
|
||||
ElMessage.success('删除成功');
|
||||
load();
|
||||
});
|
||||
const view = row => ElMessage.info(`凭证批次:${row.voucherBatchNo}`);
|
||||
const detailValue = value =>
|
||||
value === undefined || value === null || value === '' ? '-' : value;
|
||||
const view = async row => {
|
||||
detailVisible.value = true;
|
||||
detailLoading.value = true;
|
||||
detailRow.value = { ...row };
|
||||
try {
|
||||
const res = await api.getDetail(row.id);
|
||||
detailRow.value = res.data?.data || row;
|
||||
} catch (error) {
|
||||
ElMessage.error(error.message || '获取凭证详情失败');
|
||||
} finally {
|
||||
detailLoading.value = false;
|
||||
}
|
||||
};
|
||||
const download = row => window.open(row.fileUrl, '_blank');
|
||||
load();
|
||||
</script>
|
||||
@@ -914,6 +986,21 @@ load();
|
||||
padding: 0;
|
||||
min-height: auto;
|
||||
}
|
||||
&__dialog-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #303133;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
|
||||
&::before {
|
||||
width: 4px;
|
||||
height: 18px;
|
||||
background: #409eff;
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
&__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -957,8 +1044,9 @@ load();
|
||||
}
|
||||
&__batch-search {
|
||||
padding: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
margin-bottom: 8px;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 8px;
|
||||
@@ -976,6 +1064,11 @@ load();
|
||||
:deep(.voucher-manage-page__project-select) {
|
||||
width: 280px;
|
||||
}
|
||||
:deep(.voucher-manage-page__batch-search .el-input),
|
||||
:deep(.voucher-manage-page__batch-search .el-input-number),
|
||||
:deep(.voucher-manage-page__batch-search .el-date-editor) {
|
||||
width: 50%;
|
||||
}
|
||||
:deep(.el-table) {
|
||||
--el-table-border-color: #eff1f7;
|
||||
--el-table-row-hover-bg-color: #f5f7fa;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<business-crud-page :api="api" :config="config" :crud-option="option" :detail-id="$route.query.detailId" :menu-width="250" />
|
||||
<business-crud-page :api="api" :config="config" :crud-option="option" :detail-id="$route.query.detailId" :menu-width="250" standalone-form-page />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
Reference in New Issue
Block a user