Files
tms-erp-web/src/views/business/master-order.vue
T
gxwebsoft 899a8c1c2b style(payment): 优化票据搜索条件交互和标签宽度统一
- 票据账册搜索改用 el-check-tag 实现快捷筛选,提升用户交互体验
- 查询按钮增加图标,重置按钮同样添加图标及文字说明
- 调整票据搜索表单标签宽度由160px统一为88px,优化界面一致性
- 全站多处搜索栏表单标签宽度由160px改为88px,涵盖多页面及组件
- 修复业务组件内el-autocomplete输入框宽度适配,避免placeholder被裁剪
- 业务页面货物名称表格列宽由150调整为240,改善显示效果
- 修正系统部门操作列宽度避免按钮竖排溢出问题
- 修改多个表单页和搜索板块样式,确保标签宽度和对齐一致
- 调整表单按钮等样式,提升整体页面美观性和操作友好度
2026-08-26 11:59:49 +08:00

568 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<basic-container class="master-order-page">
<template v-if="mode === 'list'">
<el-form :model="query" class="master-search" label-width="88px" @submit.prevent>
<el-row :gutter="16">
<el-col v-for="field in primaryFields" :key="field.prop" :span="6"
><el-form-item :label="field.label"
><el-input v-model="query[field.prop]" placeholder="请输入" /></el-form-item
></el-col>
<el-col :span="6"
><el-form-item label="状态"
><el-select v-model="query.businessStatus" placeholder="全部"
><el-option label="全部" value="" /><el-option
v-for="item in statuses"
:key="item.value"
:label="item.label"
:value="item.value" /></el-select></el-form-item
></el-col>
<template v-if="searchExpanded">
<el-col v-for="field in secondaryFields" :key="field.prop" :span="6"
><el-form-item :label="field.label"
><el-input v-model="query[field.prop]" placeholder="请输入" /></el-form-item
></el-col>
<el-col :span="6"
><el-form-item label="计划开始日期"
><el-date-picker
v-model="query.planStartRange"
type="datetimerange"
format="YYYY-MM-DD HH:mm"
value-format="YYYY-MM-DD HH:mm:ss"
start-placeholder="请选择"
end-placeholder="请选择" /></el-form-item
></el-col>
<el-col :span="6"
><el-form-item label="计划结束日期"
><el-date-picker
v-model="query.planEndRange"
type="datetimerange"
format="YYYY-MM-DD HH:mm"
value-format="YYYY-MM-DD HH:mm:ss"
start-placeholder="请选择"
end-placeholder="请选择" /></el-form-item
></el-col>
</template>
<el-col :span="24" class="search-actions"
><el-button type="primary" @click="search">查询</el-button
><el-button @click="reset">重置</el-button
><el-link type="primary" @click="searchExpanded = !searchExpanded"
><el-icon><component :is="searchExpanded ? ArrowUp : ArrowDown" /></el-icon
>{{ searchExpanded ? '折叠' : '展开' }}</el-link
></el-col
>
</el-row>
</el-form>
<section class="master-list-panel">
<div class="toolbar">
<el-button type="primary" @click="goCreate()">新建多联总单</el-button
><el-button type="primary" plain @click="download">批量导出</el-button>
</div>
<div v-loading="loading" class="master-cards">
<article v-for="row in records" :key="row.id" class="master-card">
<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 status-text">{{
statusName(row.businessStatus)
}}</el-tag>
</div>
<p class="project-name">{{ row.projectName || '-' }}</p>
<p>{{ timeRange(row) }}</p>
<p>{{ goodsSummary(row) }}</p>
</section>
<section class="route-progress">
<div class="route-track">
<template v-for="(node, index) in routeNodes(row)" :key="node.key">
<div class="route-point">
<span :class="['route-node', node.type]">{{ node.text }}</span>
<div class="route-detail">
<el-tooltip placement="top"><template #content>{{ node.name }}</template><strong>{{ node.name }}</strong></el-tooltip>
<span v-for="line in node.lines" :key="line">{{ line }}</span>
</div>
</div>
<div v-if="index < routeNodes(row).length - 1" class="route-connector">
<small>{{ (row.routeProgress || [])[index]?.transportType }}</small>
<el-progress
:percentage="progress((row.routeProgress || [])[index], row.totalQuantity)"
:show-text="false"
/>
</div>
</template>
</div>
</section>
<aside class="card-actions">
<header>操作</header>
<el-link
v-for="action in actions(row)"
:key="action.text"
:type="action.danger ? 'danger' : 'primary'"
@click="action.run"
>{{ action.text }}</el-link
>
</aside>
</article>
<el-empty v-if="!loading && !records.length" description="暂无数据" />
</div>
<div class="page-bar">
<el-pagination
v-model:current-page="page.current"
v-model:page-size="page.size"
:page-sizes="[10, 20, 50, 100]"
layout="prev, pager, next, total, sizes"
:total="page.total"
@current-change="load"
@size-change="sizeChange"
/>
</div>
</section>
</template>
<template v-else-if="mode === 'editor'">
<div class="archive-page-form__title">{{ masterEditorTitle }}</div>
<master-order-editor
:id="routeId"
@back="goList"
@dispatch="goDispatch"
/>
</template>
<master-order-dispatch v-else-if="mode === 'dispatch'" :id="routeId" @back="goList" />
<master-order-detail v-else :id="routeId" @back="goList" />
<el-dialog v-model="confirm.visible" title="提示" width="400px"
><span>{{ confirm.message }}</span
><template #footer
><el-button @click="confirm.visible = false">关闭</el-button
><el-button type="primary" @click="confirm.run">确认</el-button></template
></el-dialog
>
</basic-container>
</template>
<script>
import * as api from '@/api/business/master-order';
import { ArrowDown, ArrowUp } from '@element-plus/icons-vue';
import MasterOrderEditor from './components/master-order-editor.vue';
import MasterOrderDispatch from './components/master-order-dispatch.vue';
import MasterOrderDetail from './components/master-order-detail.vue';
export default {
components: { MasterOrderEditor, MasterOrderDispatch, MasterOrderDetail },
data() {
return {
ArrowDown,
ArrowUp,
searchExpanded: false,
loading: false,
records: [],
query: { businessStatus: '' },
page: { current: 1, size: 10, total: 0 },
confirm: { visible: false, message: '', run: () => {} },
statuses: [
{ label: '草稿', value: 'draft' },
{ label: '待调度', value: 'waiting_dispatch' },
{ label: '调度中', value: 'dispatching' },
{ label: '调度完成', value: 'completed' },
{ label: '调度关闭', value: 'closed' },
],
primaryFields: [
{ label: '总单号', prop: 'masterNos' },
{ label: '项目名称', prop: 'projectName' },
{ label: '客户名称', prop: 'customerName' },
],
secondaryFields: [
{ label: '货物名称', prop: 'cargoName' },
{ label: '货物类型', prop: 'cargoType' },
{ label: '发货地址', prop: 'departureAddress' },
{ label: '收货地址', prop: 'arrivalAddress' },
],
};
},
computed: {
mode() {
return this.$route.query.mode || 'list';
},
routeId() {
return this.$route.query.id;
},
masterEditorTitle() {
return this.routeId ? '编辑总单' : '新增总单';
},
},
watch: {
'$route.query': {
immediate: true,
handler() {
this.syncTagTitle();
if (this.mode === 'list') this.load();
},
},
},
methods: {
async load() {
this.loading = true;
const params = { ...this.query };
if (params.planStartRange) [params.planStartTime] = params.planStartRange;
if (params.planEndRange) [, params.planEndTime] = params.planEndRange;
try {
const res = await api.getList(this.page.current, this.page.size, params);
const data = res.data?.data || res.data || res;
this.records = data.records || [];
this.page.total = data.total || 0;
} finally {
this.loading = false;
}
},
search() {
this.page.current = 1;
this.load();
},
reset() {
this.query = { businessStatus: '' };
this.search();
},
sizeChange() {
this.page.current = 1;
this.load();
},
goList() {
this.$router.push('/business/master-order');
},
goCreate(id) {
this.$router.push({ path: '/business/master-order', query: { mode: 'editor', id } });
},
goDetail(id) {
this.$router.push({ path: '/business/master-order', query: { mode: 'detail', id } });
},
goDispatch(id) {
this.$router.push({ path: '/business/master-order', query: { mode: 'dispatch', id } });
},
syncTagTitle() {
const title = this.mode === 'detail'
? '总单详情'
: this.mode === 'dispatch'
? '多联调度'
: this.mode === 'editor'
? (this.routeId ? '编辑总单' : '新增总单')
: '总单管理';
this.$store.commit('SET_TAG', { fullPath: this.$route.fullPath, name: title });
this.$router.$avueRouter.setTitle(title);
},
actions(row) {
const result = [
{
text: '复制',
run: async () => {
const res = await api.copy(row.id);
this.$message.success(`复制成功,新总单号:${(res.data || res).masterNo}`);
this.load();
},
},
];
if (['draft', 'waiting_dispatch', 'dispatching', 'completed'].includes(row.businessStatus))
result.unshift({ text: '编辑', run: () => this.goCreate(row.id) });
if (['waiting_dispatch', 'dispatching'].includes(row.businessStatus))
result.unshift({ text: '调度', run: () => this.goDispatch(row.id) });
if (row.businessStatus !== 'draft')
result.unshift({ text: '详情', run: () => this.goDetail(row.id) });
if (['dispatching', 'completed'].includes(row.businessStatus))
result.push({
text: '关闭调度',
run: () =>
this.confirmAction('确认关闭调度?关闭后将不可恢复!', () => api.closeDispatch(row.id)),
});
if (['draft', 'waiting_dispatch'].includes(row.businessStatus))
result.push({
text: '删除',
danger: true,
run: () =>
this.confirmAction('确认删除这条数据?删除后将不可恢复!', () => api.remove(row.id)),
});
return result;
},
confirmAction(message, request) {
this.confirm = {
visible: true,
message,
run: async () => {
await request();
this.confirm.visible = false;
this.$message.success('操作成功');
this.load();
},
};
},
statusName(value) {
return (this.statuses.find(item => item.value === value) || {}).label || '-';
},
statusType(value) {
return {
draft: 'info',
waiting_dispatch: 'warning',
dispatching: 'success',
completed: 'primary',
closed: 'danger',
}[value];
},
timeRange(row) {
return row.planStartTime && row.planEndTime
? `${String(row.planStartTime).slice(0, 16)} - ${String(row.planEndTime).slice(0, 16)}`
: '-';
},
goodsSummary(row) {
const item = (row.goods || [])[0] || {};
if (!item.cargoName && !item.cargoType) return '-';
return `${item.cargoName || '-'}${item.cargoType || '-'} | ${this.number(row.totalQuantity)} 吨`;
},
number(value) {
return Number(value || 0).toFixed(2);
},
routeNumber(value) {
const number = Number(value || 0);
return Number.isInteger(number) ? String(number) : number.toFixed(2);
},
routeArrivedQuantity(route) {
const keys = [
'arrivedQuantity',
'arrivalQuantity',
'arriveQuantity',
'reachedQuantity',
'receiveQuantity',
'unloadQuantity',
];
const key = keys.find(item => route[item] !== undefined && route[item] !== null);
return key ? route[key] : 0;
},
routeNodes(row = {}) {
const routes = row.routeProgress || [];
const total = this.routeNumber(row.totalQuantity);
if (!routes.length) {
return [
{
key: 'start',
type: 'start',
text: '起',
name: row.departureName || row.departureAddress || '-',
lines: [`已调度0/${total}吨`],
},
{
key: 'end',
type: 'end',
text: '终',
name: row.arrivalName || row.arrivalAddress || '-',
lines: [`到达0/${total}吨`],
},
];
}
return [
{
key: 'start',
type: 'start',
text: '起',
name: row.departureName || row.departureAddress || '-',
lines: [`已调度${this.routeNumber(routes[0]?.dispatchedQuantity)}/${total}吨`],
},
...routes.map((route, index) => {
const isEnd = index === routes.length - 1;
const arrived = this.routeNumber(this.routeArrivedQuantity(route));
const dispatched = this.routeNumber(routes[index + 1]?.dispatchedQuantity);
return {
key: route.segmentNo || `route-${index}`,
type: isEnd ? 'end' : 'middle',
text: isEnd ? '终' : '经',
name: route.arrivalName || route.departureName || row.arrivalName || '-',
lines: isEnd
? [`到达${arrived}/${total}吨`]
: [`到达 ${arrived}/${total}吨`, `已调度 ${dispatched}/到达${arrived}/${total}吨`],
};
}),
];
},
progress(route, total) {
return total
? Math.min(100, (Number(route?.dispatchedQuantity || 0) / Number(total)) * 100)
: 0;
},
async download() {
const response = await api.exportList(this.query);
const blob = response.data || response;
if (!(blob instanceof Blob) || !blob.size) {
this.$message.error('导出失败,未生成有效文件');
return;
}
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = '总单运单明细.xlsx';
link.click();
URL.revokeObjectURL(url);
},
},
};
</script>
<style scoped lang="scss">
.master-search {
margin-bottom: 12px;
padding: 12px 12px 4px;
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
.el-form-item {
margin-bottom: 8px;
}
.el-input,
.el-select,
.el-date-editor {
width: 100%;
}
.search-actions {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 12px;
min-height: 40px;
.el-icon {
margin-right: 4px;
}
}
}
.master-list-panel {
.toolbar {
padding: 8px 0;
}
}
.master-card {
display: flex;
min-width: 0;
min-height: 150px;
margin-bottom: 8px;
border: 1px solid #eff1f7;
background: #fff;
.card-info {
flex: 0 0 266px;
padding: 18px 20px;
font-size: 14px;
color: #303133;
.card-title-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
p {
margin: 18px 0 0;
line-height: 1.4;
}
.project-name {
font-weight: 600;
}
}
.status-tag {
flex-shrink: 0;
font-weight: 600;
}
.route-progress {
flex: 1 1 auto;
display: flex;
justify-content: flex-start;
align-items: center;
min-width: 0;
overflow-x: auto;
overflow-y: hidden;
min-height: 170px;
padding: 22px 24px 18px;
border-right: 1px solid #eff1f7;
border-left: 1px solid #eff1f7;
}
.route-track {
display: flex;
align-items: flex-start;
justify-content: center;
min-width: max-content;
}
.route-point {
width: 150px;
flex: 0 0 150px;
text-align: center;
}
.route-node {
display: inline-flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: 6px;
color: #fff;
font-size: 16px;
font-weight: 600;
line-height: 1;
text-align: center;
&.start {
background: #409eff;
}
&.middle {
background: #67c23a;
}
&.end {
background: #e6a23c;
}
}
.route-detail {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 12px;
color: #303133;
font-size: 14px;
line-height: 1.8;
white-space: nowrap;
strong {
font-size: 14px;
font-weight: 600;
line-height: 1.4;
max-width: 180px;
}
}
.route-connector {
width: clamp(180px, 20vw, 266px);
flex: 0 0 clamp(180px, 20vw, 266px);
padding-top: 2px;
margin: 0 -6px;
small {
display: block;
text-align: center;
margin-bottom: 6px;
color: #a8abb2;
font-size: 14px;
font-weight: 600;
line-height: 1.2;
}
:deep(.el-progress-bar__outer) {
height: 6px !important;
background-color: #e4e7ed;
}
:deep(.el-progress-bar__inner) {
background-color: #409eff;
}
}
.card-actions {
width: 88px;
padding: 16px 12px;
header {
margin-bottom: 10px;
font-weight: 600;
font-size: 14px;
text-align: center;
}
.el-link {
display: flex;
width: 100%;
margin: 0 0 8px;
}
}
}
.page-bar {
display: flex;
justify-content: flex-end;
align-items: center;
padding-top: 8px;
}
</style>