Merge remote-tracking branch 'websoft/master'

# Conflicts:
#	src/views/business/components/business-crud-page.vue
This commit is contained in:
2026-08-25 00:11:14 +08:00
36 changed files with 560 additions and 330 deletions
@@ -1,7 +1,7 @@
<template>
<div v-loading="loading" class="master-detail">
<section v-if="master" class="detail-overview">
<div class="detail-heading"><div><h2>多联总单详情 <span>|</span> {{ master.masterNo }}</h2><el-tag :type="statusType(master.businessStatus)">{{ statusName(master.businessStatus) }}</el-tag><el-tag v-if="transportFlowLabel" type="info" class="transport-flow-tag">{{ transportFlowLabel }}</el-tag></div><el-button @click="$emit('back')">返回</el-button></div>
<div class="detail-heading"><div><h2>多联总单详情 <span>|</span> {{ master.masterNo }}</h2><el-tag :type="statusType(master.businessStatus)" class="status-text">{{ statusName(master.businessStatus) }}</el-tag><el-tag v-if="transportFlowLabel" type="info" class="transport-flow-tag">{{ transportFlowLabel }}</el-tag></div><el-button @click="$emit('back')">返回</el-button></div>
<dl class="detail-meta"><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 class="detail-meta__attachments"><dt>附件</dt><dd><template v-if="attachments.length"><el-link v-for="file in attachments" :key="file.url || file.link || file.name || file.originalName" type="primary" @click="previewAttachment(file)">{{ attachmentName(file) }}</el-link></template><span v-else>-</span></dd></div></dl>
<div class="route-map"><template v-for="(route, index) in segments" :key="route.segmentNo"><div class="route-map__node"><span :class="['route-badge', index ? 'middle' : 'start']">{{ index ? '经' : '起' }}</span><strong>{{ route.departureName || '-' }}</strong><small>{{ route.departureAddress || '-' }}</small><small class="route-map__progress">{{ index ? `已到达 ${quantity(segments[index - 1].arrivedQuantity)}/${quantity(master.totalQuantity)}` : `已调度 ${quantity(route.dispatchedQuantity)}/${quantity(master.totalQuantity)}` }}</small><small v-if="index" class="route-map__progress">已调度 {{ quantity(route.dispatchedQuantity) }}/{{ quantity(master.totalQuantity) }}</small></div><div class="route-map__line"></div><div v-if="index === segments.length - 1" class="route-map__node"><span class="route-badge end">终</span><strong>{{ route.arrivalName || '-' }}</strong><small>{{ route.arrivalAddress || '-' }}</small><small class="route-map__progress">已到达 {{ quantity(route.arrivedQuantity) }}/{{ quantity(master.totalQuantity) }}</small></div></template></div>
</section>
@@ -17,7 +17,7 @@
<header @click="toggleSegment(route.segmentNo)"><h3><span class="segment-index">{{ segmentNumber(route, index) }}</span><span>{{ route.departureName || '-' }} {{ route.arrivalName || '-' }}</span><span class="segment-detail__transport-type">{{ segmentTransportType(route) || '-' }}</span><span class="segment-detail__progress-bar"><span :style="{ width: `${segmentProgress(route)}%` }"></span></span><span class="segment-detail__progress">已调度 <em>{{ quantity(route.dispatchedQuantity) }}</em> / {{ quantity(plannedQuantity(route)) }} {{ plannedUnit(route) }}</span></h3><div class="segment-detail__header-right"><el-tooltip :content="isSegmentExpanded(route.segmentNo) ? '折叠' : '展开'" placement="top"><el-button circle :icon="isSegmentExpanded(route.segmentNo) ? ArrowUp : ArrowDown" @click.stop="toggleSegment(route.segmentNo)" /></el-tooltip></div></header>
<div v-show="isSegmentExpanded(route.segmentNo)" class="segment-execution">
<div class="segment-stats"><div><span>总量</span><strong>{{ quantity(master.totalQuantity) }}<small></small></strong></div><div><span>已调度</span><strong class="dispatched">{{ quantity(route.dispatchedQuantity) }}<small></small></strong></div><div><span>剩余</span><strong class="remaining">{{ quantity(remainingQuantity(route)) }}<small></small></strong></div></div>
<el-table :data="route.waybills || []" border class="waybill-table"><el-table-column label="运单号" min-width="180"><template #default="{ row }"><el-link type="primary" @click="openWaybill(row)">{{ row.waybillNo }}</el-link></template></el-table-column><el-table-column prop="carrierName" label="承运商" min-width="190" /><el-table-column label="司机/车牌" min-width="180"><template #default="{ row }">{{ driverVehicle(row) }}</template></el-table-column><el-table-column prop="cargoType" label="货物类型" min-width="120" /><el-table-column label="数量(吨)" width="130"><template #default="{ row }">{{ quantity(row.quantity) }}</template></el-table-column><el-table-column label="状态" width="130"><template #default="{ row }"><el-tag :type="waybillStatusType(row.businessStatus)" size="small">{{ waybillStatusName(row.businessStatus) }}</el-tag></template></el-table-column><el-table-column prop="createTime" label="创建时间" min-width="180" /></el-table>
<el-table :data="route.waybills || []" border class="waybill-table"><el-table-column label="运单号" min-width="180"><template #default="{ row }"><el-link type="primary" @click="openWaybill(row)">{{ row.waybillNo }}</el-link></template></el-table-column><el-table-column prop="carrierName" label="承运商" min-width="190" /><el-table-column label="司机/车牌" min-width="180"><template #default="{ row }">{{ driverVehicle(row) }}</template></el-table-column><el-table-column prop="cargoType" label="货物类型" min-width="120" /><el-table-column label="数量(吨)" width="130"><template #default="{ row }">{{ quantity(row.quantity) }}</template></el-table-column><el-table-column label="状态" width="130"><template #default="{ row }"><el-tag :type="waybillStatusType(row.businessStatus)" size="small" class="status-text">{{ waybillStatusName(row.businessStatus) }}</el-tag></template></el-table-column><el-table-column prop="createTime" label="创建时间" min-width="180" /></el-table>
<el-empty v-if="!(route.waybills || []).length" description="暂无运单明细" :image-size="56" />
<div v-if="(route.transportPlans || []).length" class="transport-plan-detail"><h4>关联计划单</h4><el-table :data="route.transportPlans" border><el-table-column label="计划单号" min-width="160"><template #default="{ row }"><el-link type="primary" @click="openPlan(row)">{{ row.planNo }}</el-link></template></el-table-column><el-table-column prop="planName" label="计划名称" min-width="180" /><el-table-column prop="transportType" label="运输方式" width="130" /><el-table-column prop="planStartDate" label="计划开始日期" width="130" /><el-table-column prop="planEndDate" label="计划结束日期" width="130" /><el-table-column prop="businessStatus" label="状态" width="110" /></el-table></div>
</div>
@@ -30,15 +30,15 @@
<el-table-column prop="cargoName" label="货物名称" min-width="150" />
<el-table-column prop="cargoType" label="货物类型" min-width="130" />
<el-table-column prop="packageType" label="包装" min-width="120" />
<el-table-column label="重量(吨)" min-width="110"><template #default="{ row }">{{ row.weight ?? row.weightTon ?? (row.quantityUnit === '吨' ? row.quantity : '-') }}</template></el-table-column>
<el-table-column label="体积(方)" min-width="110"><template #default="{ row }">{{ row.volume ?? row.volumeCubic ?? '-' }}</template></el-table-column>
<el-table-column label="重量(吨)" min-width="110"><template #default="{ row }">{{ row.weight ?? row.weightTon ?? (row.quantityUnit === '吨' ? row.quantity : '-') }}</template></el-table-column>
<el-table-column label="体积(方)" min-width="110"><template #default="{ row }">{{ row.volume ?? row.volumeCubic ?? '-' }}</template></el-table-column>
<el-table-column label="数量" min-width="100"><template #default="{ row }">{{ row.quantity ?? '-' }}{{ row.quantityUnit ? ` ${row.quantityUnit}` : '' }}</template></el-table-column>
<el-table-column prop="materialCode" label="物料编码" min-width="130" />
<el-table-column prop="deviceCode" label="设备编码" min-width="130" />
<el-table-column prop="brand" label="品牌" min-width="120" />
<el-table-column label="规格型号" min-width="150"><template #default="{ row }">{{ [row.specification, row.model].filter(Boolean).join('/') || '-' }}</template></el-table-column>
<el-table-column prop="remark" label="备注" min-width="160" />
<el-table-column label="货物单价(元)" min-width="130"><template #default="{ row }">{{ row.unitPrice ?? '-' }}</template></el-table-column>
<el-table-column label="货物单价(元)" min-width="130"><template #default="{ row }">{{ row.unitPrice ?? '-' }}</template></el-table-column>
<el-table-column label="计划日期" min-width="130"><template #default="{ row }">{{ row.planDate || row.planStartDate || '-' }}</template></el-table-column>
</el-table>
<el-empty v-if="!(master.goods || []).length" description="暂无货物信息" :image-size="56" />
@@ -4,7 +4,7 @@
<div class="overview-heading">
<div>
<h2>多联总单调度 <span>|</span> {{ master.masterNo }}</h2>
<el-tag :type="statusType(master.businessStatus)">{{ statusName(master.businessStatus) }}</el-tag>
<el-tag :type="statusType(master.businessStatus)" class="status-text">{{ statusName(master.businessStatus) }}</el-tag>
</div>
</div>
<div class="overview-content">
@@ -159,7 +159,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)"><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>
@@ -78,6 +78,7 @@
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
placeholder="请选择"
style="width: 200px"
/></el-form-item>
</div>
</template>
@@ -116,6 +117,7 @@
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
placeholder="请选择"
style="width: 200px"
/></el-form-item>
</div>
</template>
@@ -428,7 +430,27 @@
<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="cargoImportVisible" title="导入货物" append-to-body width="560px">
<section-card>
<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>
</section-card>
<template #footer><el-button @click="cargoImportVisible = false">关闭</el-button></template>
</el-dialog>
<el-dialog v-model="routeDialogVisible" title="选择线路" width="1280px" @opened="loadRouteList">
<div class="route-dialog__search">
<el-form :model="routeQuery" label-position="right" label-width="86px">
@@ -1975,6 +1997,15 @@ export default {
margin-top: 8px;
color: #606266;
}
.cargo-import {
display: flex;
flex-direction: column;
gap: 16px;
.el-upload__tip {
margin-top: 8px;
line-height: 1.5;
}
}
@media screen and (max-width: 1440px) {
.route-node-form {
grid-template-columns: repeat(2, minmax(320px, 1fr));
@@ -17,8 +17,8 @@
<el-col :span="6"><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="6"><el-form-item label="结算方式"><el-input v-model="form.settlementMode" /></el-form-item></el-col>
<el-col :span="6"><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="6"><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="6"><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="6"><el-form-item label="一式(份)"><div class="inline-field"><el-input-number v-model="form.copyCount" :min="1" controls-position="right" /></div></el-form-item></el-col>
<el-col :span="6"><el-form-item label="回款账期(天)"><el-input-number v-model="form.paymentDays" :min="0" controls-position="right" /></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>
@@ -56,7 +56,7 @@
<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-number v-model="row.ratioLimit" :min="0" :max="100" :precision="2" controls-position="right" /></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>
+3 -2
View File
@@ -395,7 +395,7 @@
<div class="loading-detail__summary">
<div class="loading-detail__heading">配载单详情</div>
<div>{{ dialogForm.loadingNo || '-' }}</div>
<div><el-tag :type="detailStatusType">{{ statusText(dialogForm) }}</el-tag></div>
<div><el-tag :type="detailStatusType" class="status-text">{{ statusText(dialogForm) }}</el-tag></div>
<div><el-tag type="warning">{{ transportTypeLabel(dialogForm.transportType) }}</el-tag></div>
</div>
<div class="loading-detail__task-row">
@@ -871,7 +871,7 @@
<el-form-item v-if="dialogForm.carrierType !== '承运商'" label="押运人手机号">
<el-input v-model="dialogForm.escortPhone" clearable placeholder="请输入" />
</el-form-item>
<el-form-item label="里程(km)">
<el-form-item label="里程km">
<el-input
v-model="dialogForm.mileage"
clearable
@@ -2474,6 +2474,7 @@ export default {
<style lang="scss" scoped>
.loading-manage-page {
margin-bottom: 60px;
.loading-manage-page__search {
padding: 12px 12px 4px;
margin-bottom: 20px;
+1 -1
View File
@@ -62,7 +62,7 @@
<section class="card-info">
<div class="card-title-row">
<el-link type="primary" @click="goDetail(row.id)">{{ row.masterNo }}</el-link
><el-tag :type="statusType(row.businessStatus)" class="status-tag">{{
><el-tag :type="statusType(row.businessStatus)" class="status-tag status-text">{{
statusName(row.businessStatus)
}}</el-tag>
</div>
+4 -2
View File
@@ -44,7 +44,7 @@
</template>
<template #status="{ row }">
<el-tag :type="statusTagType(row.status)">
<el-tag :type="statusTagType(row.status)" class="status-text">
{{ row.statusName || displayStatus(row.status) }}
</el-tag>
</template>
@@ -1198,7 +1198,9 @@ export default {
&__timeline-line {
position: absolute;
top: 22px;
// 圆点中心位置 = padding-top(12) + dot 半径(7) = 19px
// 线 height 3px,因此 top 需为 19 - 1.5 = 17.5px,使线视觉中心与圆点中心对齐
top: 17.5px;
right: 58px;
left: 58px;
height: 3px;
+33 -74
View File
@@ -55,9 +55,7 @@
</template>
<template #approvalStatus="{ row }">
<el-tag :type="statusTagType(row.approvalStatus)">
{{ displayStatus(row, 'approvalStatus') }}
</el-tag>
{{ displayStatus(row, 'approvalStatus') }}
</template>
<template #menu="{ row, index }">
@@ -98,7 +96,7 @@
@update:model-value="projectBox = $event"
@closed="resetProjectDialog"
>
<div v-if="isProjectFormPage" class="project-apply-page__title">
<div v-if="isProjectFormPage" class="archive-page-form__title">
{{ projectDialogTitle }}
</div>
<el-form
@@ -178,6 +176,7 @@
<el-form-item label="承办部门" prop="undertakeDeptId">
<el-cascader
v-model="form.undertakeDeptId"
style="width: 100%"
placeholder="请选择承办部门"
clearable
filterable
@@ -1074,24 +1073,38 @@ export default {
delBtn: false,
// 操作列收窄:本项目操作按钮较少,320px 过宽,调整为 220px(链接会自动换行兜底)
menuWidth: 180,
column: (option.column || []).map(column => ({ ...column, display: false })),
column: (option.column || []).map(column => ({
...column,
display: false,
align: 'center',
headerAlign: 'center',
})),
};
},
hasPermission(code) {
return this.isAdmin || this.validData(this.permission && this.permission[code], false);
},
openProjectFormPage() {
const type = this.$route.query.mode === 'edit' ? 'edit' : 'add';
const type = this.$route.query.mode || 'add';
const id = this.$route.query.id;
this.dialogType = type;
this.dialogReadonly = false;
this.dialogReadonly = type === 'view';
this.projectBox = true;
if (type === 'add') {
this.applyProjectDetail({ ...emptyForm(), projectType: '普通项目' });
if (['add', 'majorSupplement'].includes(type)) {
this.applyProjectDetail({
...emptyForm(),
projectType: type === 'majorSupplement' ? '重大项目' : '普通项目',
});
this.fillDefaultUsers();
return;
}
this.api.getDetail(id).then(res => this.applyProjectDetail(res.data.data || {}));
this.api.getDetail(id).then(res => {
const detail = res.data.data || {};
this.applyProjectDetail({
...detail,
changeType: type === 'change' ? this.resolveChangeType(detail) : detail.changeType,
});
});
},
closeProjectForm() {
if (this.isProjectFormPage) {
@@ -1140,25 +1153,6 @@ export default {
const item = column && (column.dicData || []).find(dic => dic.value === row[prop]);
return item ? item.label : row[prop] || '未知';
},
statusTagType(status) {
if (
[
1,
'pending',
'waiting_dispatch',
'processing',
'dispatching',
'approved',
'change_approved',
].includes(status)
) {
return 'success';
}
if ([2, 'cancelled', 'withdrawn', 'draft'].includes(status)) return 'info';
if (['rejected', 'change_rejected'].includes(status)) return 'danger';
if (status === 'completed') return 'primary';
return 'warning';
},
onLoad(page, params = {}) {
this.loading = true;
this.api
@@ -1310,34 +1304,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;
const query = { mode: type };
if (['edit', 'view', 'change'].includes(type)) {
query.id = row.id;
}
this.dialogType = type;
this.dialogReadonly = type === 'view';
this.projectBox = true;
if (type === 'add' || type === 'majorSupplement') {
this.applyProjectDetail({
...emptyForm(),
projectType: type === 'majorSupplement' ? '重大项目' : '普通项目',
});
this.fillDefaultUsers();
return;
}
this.api.getDetail(row.id).then(res => {
const detail = res.data.data || {};
this.applyProjectDetail({
...detail,
changeType: type === 'change' ? this.resolveChangeType(detail) : detail.changeType,
});
});
if (type === 'add') query.name = `新增${this.config.title}`;
else if (type === 'edit') query.name = `编辑${this.config.title}`;
else if (type === 'view') query.name = `查看${this.config.title}`;
else if (type === 'majorSupplement') query.name = '重大项目补录';
else if (type === 'change') query.name = '项目变更';
this.$router.push({ path: '/business/project-apply/form', query });
},
resetProjectDialog() {
this.$refs.projectForm?.clearValidate();
@@ -2110,28 +2086,11 @@ export default {
.project-apply-page-form {
min-height: 100%;
margin-bottom: 60px;
//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;
+13 -1
View File
@@ -146,7 +146,7 @@
<template #header>
<div class="voucher-manage-page__dialog-title">凭证详情</div>
</template>
<el-descriptions v-loading="detailLoading" :column="2" border>
<el-descriptions v-loading="detailLoading" :column="2" border class="voucher-manage-page__detail-desc">
<el-descriptions-item label="项目名称">
{{ detailValue(detailRow.projectName) }}
</el-descriptions-item>
@@ -1076,5 +1076,17 @@ load();
:deep(.el-table__body tr:nth-child(even) > td) {
background: #fafafa;
}
&__detail-desc {
:deep(.el-descriptions__table) {
table-layout: fixed;
th, td {
width: 50%;
}
.el-descriptions__label,
.el-descriptions__content {
width: 50%;
}
}
}
}
</style>