1、新增总单
2、fix bug
This commit is contained in:
@@ -0,0 +1,549 @@
|
||||
<template>
|
||||
<basic-container class="master-order-page">
|
||||
<template v-if="mode === 'list'">
|
||||
<el-form :model="query" class="master-search" label-width="160px" @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">{{
|
||||
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">
|
||||
<strong>{{ node.name }}</strong>
|
||||
<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>
|
||||
<master-order-editor
|
||||
v-else-if="mode === 'editor'"
|
||||
:id="routeId"
|
||||
@back="goList"
|
||||
@dispatch="goDispatch"
|
||||
/>
|
||||
<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;
|
||||
},
|
||||
},
|
||||
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 blob = await api.exportList(this.query);
|
||||
const url = URL.createObjectURL(new Blob([blob.data || 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: 8px;
|
||||
padding: 12px 12px 4px;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
.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 {
|
||||
background: #fff;
|
||||
.toolbar {
|
||||
padding: 12px 12px 0;
|
||||
}
|
||||
}
|
||||
.master-card {
|
||||
display: flex;
|
||||
min-height: 150px;
|
||||
margin-bottom: 8px;
|
||||
border: 1px solid #eff1f7;
|
||||
background: #fff;
|
||||
.card-info {
|
||||
flex: 0 0 380px;
|
||||
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;
|
||||
}
|
||||
.route-progress {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: auto;
|
||||
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;
|
||||
}
|
||||
}
|
||||
.route-connector {
|
||||
width: clamp(180px, 20vw, 344px);
|
||||
flex: 0 0 clamp(180px, 20vw, 344px);
|
||||
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: 8px !important;
|
||||
background-color: #f1f2f5;
|
||||
}
|
||||
:deep(.el-progress-bar__inner) {
|
||||
background-color: #409eff;
|
||||
}
|
||||
}
|
||||
.card-actions {
|
||||
width: 110px;
|
||||
padding: 16px;
|
||||
header {
|
||||
margin-bottom: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.el-link {
|
||||
margin: 0 8px 8px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
.page-bar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
padding-top: 8px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user