670 lines
15 KiB
Vue
670 lines
15 KiB
Vue
<template>
|
||
<view class="express-page">
|
||
<!-- 顶部搜索与新增 -->
|
||
<view class="search-bar">
|
||
<view class="search-container">
|
||
<view class="search-input">
|
||
<u-search v-model="searchKeyword" placeholder="搜索物流编号" @confirm="onSearch" @keyup.enter="onSearch"
|
||
class="custom-search-input" :show-action="false" />
|
||
</view>
|
||
<view class="add-button" @click="handleAdd">
|
||
<u-icon name="plus" :color="$im-primary" size="18"></u-icon>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<!-- 列表区 -->
|
||
<scroll-view scroll-y class="express-list" @scrolltolower="loadMore">
|
||
<view v-for="item in expressList" :key="item.expressId" class="express-card">
|
||
<!-- 第一行:物流公司、单号、状态 -->
|
||
<view class="card-header new-header">
|
||
<text class="express-company">{{ item.expressType || '-' }}</text>
|
||
<text class="express-code">{{ item.expressCode || '-' }}</text>
|
||
<text class="status" :class="'status-' + item.status">{{ statusText(item.status) }}</text>
|
||
</view>
|
||
<!-- 第二行:发货方向(对方->负责人)及联系方式 -->
|
||
<view class="row row-between row-arrow">
|
||
<view class="row-party">
|
||
<text class="party-label">{{ item.supplyName || '-' }}</text>
|
||
<text class="party-phone">{{ item.supplyPhone || '-' }}</text>
|
||
</view>
|
||
<text class="arrow">→</text>
|
||
<view class="row-party">
|
||
<text class="party-label">{{ item.ownerName || '-' }}</text>
|
||
<text class="party-phone">{{ item.ownerPhone || '-' }}</text>
|
||
</view>
|
||
</view>
|
||
<!-- 第三行:计划到货时间和剩余天数 -->
|
||
<view class="row row-between">
|
||
<text class="plan-date">计划到货:{{ item.planDate || '-' }}</text>
|
||
<ExpressRemainTime :planDate="item.planDate" :status="item.status" />
|
||
</view>
|
||
<!-- 第四行:物流状态编辑器 -->
|
||
<view class="row">
|
||
<ExpressStatusEditor :lastStatus.sync="item.lastStatus" :lastUpdateTime.sync="item.lastUpdateTime"
|
||
:expressType="item.expressType" @save="val => handleStatusSave(item, val)" />
|
||
</view>
|
||
<!-- 第五行:备注 -->
|
||
<view class="row remark-row">
|
||
<text class="remark-label">备注:</text>
|
||
<text class="remark-content">{{ item.remark || '-' }}</text>
|
||
</view>
|
||
<!-- 第六行:操作按钮 -->
|
||
<view class="card-actions new-actions">
|
||
<!-- 未确认:确认、修改 -->
|
||
<template v-if="item.status === 0">
|
||
<view class="action-btn action-confirm" @click="handleUpdateStatus(item, 1)">确认</view>
|
||
<view class="action-btn action-edit" @click="handleUpdate(item)">修改</view>
|
||
</template>
|
||
<!-- 进行中:完成、同步、异常 -->
|
||
<template v-else-if="item.status === 1">
|
||
<view class="action-btn action-finish" @click="handleUpdateStatus(item, 2)">完成</view>
|
||
<view class="action-btn action-sync" @click="handleRefresh(item)">同步</view>
|
||
<view class="action-btn action-exception" @click="handleUpdateStatus(item, 3)">异常</view>
|
||
</template>
|
||
<!-- 已完成:无操作按钮,仅显示删除 -->
|
||
<template v-else-if="item.status === 2">
|
||
<!-- 空 -->
|
||
</template>
|
||
<!-- 异常:同步 -->
|
||
<template v-else-if="item.status === 3">
|
||
<view class="action-btn action-sync" @click="handleRefresh(item)">同步</view>
|
||
</template>
|
||
<!-- 编辑和删除按钮始终显示 -->
|
||
<view class="action-btn action-edit" @click="handleUpdate(item)">编辑</view>
|
||
<view class="action-btn action-delete" @click="handleDelete(item)">删除</view>
|
||
</view>
|
||
</view>
|
||
<view v-if="loadingMore" class="loading-more">加载中...</view>
|
||
<view v-if="noMore" class="no-more">没有更多了</view>
|
||
</scroll-view>
|
||
|
||
|
||
<!-- 新增/编辑弹窗 -->
|
||
<uni-popup ref="editPopup" type="bottom">
|
||
<view class="popup-content popup-bottom">
|
||
<view class="form-row">
|
||
<text>物流编号</text>
|
||
<uni-easyinput v-model="form.expressCode" placeholder="请输入物流编号" />
|
||
</view>
|
||
<view class="form-row">
|
||
<text>对方姓名</text>
|
||
<uni-easyinput v-model="form.supplyName" placeholder="请输入对方姓名" />
|
||
</view>
|
||
<view class="form-row">
|
||
<text>对方手机</text>
|
||
<uni-easyinput v-model="form.supplyPhone" placeholder="请输入对方手机" />
|
||
</view>
|
||
<view class="form-row">
|
||
<text>负责人</text>
|
||
<oa-user-select v-model="form.ownerId" />
|
||
</view>
|
||
<view class="form-row">
|
||
<text>负责人手机</text>
|
||
<uni-easyinput v-model="form.ownerPhone" placeholder="请输入负责人手机" />
|
||
</view>
|
||
<view class="form-row">
|
||
<text>计划到货</text>
|
||
<uni-easyinput v-model="form.planDate" placeholder="请输入计划到货时间" />
|
||
</view>
|
||
<view class="form-row">
|
||
<text>物流公司</text>
|
||
<oa-dict-select v-model="form.expressType" :dictType="'oa_express_type'" placeholder="请选择物流公司" />
|
||
</view>
|
||
<view class="form-row">
|
||
<text>备注</text>
|
||
<uni-easyinput v-model="form.remark" placeholder="请输入备注" />
|
||
</view>
|
||
<view class="popup-actions">
|
||
<uni-button type="primary" @click="submitForm">保存</uni-button>
|
||
<uni-button @click="cancel">取消</uni-button>
|
||
</view>
|
||
</view>
|
||
</uni-popup>
|
||
|
||
<!-- 异常登记弹窗 -->
|
||
<uni-popup ref="exceptionPopup" type="bottom">
|
||
<view class="popup-content popup-bottom">
|
||
<view class="form-row">
|
||
<text>问题描述</text>
|
||
<uni-easyinput v-model="exceptionForm.description" placeholder="请输入问题描述" />
|
||
</view>
|
||
<view class="form-row">
|
||
<text>汇报时间</text>
|
||
<uni-easyinput v-model="exceptionForm.reportTime" placeholder="请选择汇报时间" />
|
||
</view>
|
||
<view class="form-row">
|
||
<text>备注</text>
|
||
<uni-easyinput v-model="exceptionForm.remark" placeholder="请输入备注" />
|
||
</view>
|
||
<view class="popup-actions">
|
||
<uni-button type="primary" @click="submitException">确 定</uni-button>
|
||
</view>
|
||
</view>
|
||
</uni-popup>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import { addExpress, delExpress, getExpress, listExpress, updateExpress } from '@/api/oa/express';
|
||
import ExpressStatusEditor from './components/ExpressStatusEditor.vue';
|
||
import ExpressRemainTime from './components/ExpressRemainTime.vue';
|
||
import { addExpressQuestion } from '@/api/oa/expressQuestion';
|
||
|
||
export default {
|
||
components: {
|
||
ExpressStatusEditor,
|
||
ExpressRemainTime,
|
||
},
|
||
data() {
|
||
return {
|
||
expressList: [],
|
||
pageNum: 1,
|
||
pageSize: 10,
|
||
total: 0,
|
||
loadingMore: false,
|
||
noMore: false,
|
||
form: {
|
||
ownerUser: {},
|
||
},
|
||
expressDetail: {},
|
||
loading: false,
|
||
exceptionForm: {
|
||
description: '',
|
||
reportTime: '',
|
||
remark: ''
|
||
},
|
||
currentExceptionItem: null,
|
||
};
|
||
},
|
||
onLoad() {
|
||
this.getList();
|
||
},
|
||
methods: {
|
||
handleStatusSave(row, val) {
|
||
row.lastStatus = val.lastStatus;
|
||
row.lastUpdateTime = val.lastUpdateTime;
|
||
updateExpress(row).then(() => {
|
||
uni.showToast({ title: '保存成功', icon: 'success' });
|
||
this.getList();
|
||
});
|
||
},
|
||
getList(isLoadMore = false) {
|
||
if (this.loadingMore) return;
|
||
this.loadingMore = true;
|
||
listExpress({ pageNum: this.pageNum, pageSize: this.pageSize }).then(res => {
|
||
const rows = res.rows || [];
|
||
if (isLoadMore) {
|
||
this.expressList = this.expressList.concat(rows);
|
||
} else {
|
||
this.expressList = rows;
|
||
}
|
||
this.total = res.total || 0;
|
||
this.noMore = rows.length < this.pageSize;
|
||
}).finally(() => {
|
||
this.loadingMore = false;
|
||
});
|
||
},
|
||
loadMore() {
|
||
if (this.noMore || this.loadingMore) return;
|
||
this.pageNum++;
|
||
this.getList(true);
|
||
},
|
||
handleAdd() {
|
||
this.form = { ownerUser: {} };
|
||
this.$refs.editPopup.open();
|
||
},
|
||
handleUpdate(item) {
|
||
getExpress(item.expressId).then(res => {
|
||
const data = res.data || {};
|
||
this.form = {
|
||
...data,
|
||
ownerUser: data.ownerUser || {},
|
||
};
|
||
this.$refs.editPopup.open();
|
||
});
|
||
},
|
||
handleDelete(item) {
|
||
uni.showModal({
|
||
title: '提示',
|
||
content: '确定要删除该物流单吗?',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
delExpress(item.expressId).then(() => {
|
||
uni.showToast({ title: '删除成功', icon: 'success' });
|
||
this.pageNum = 1;
|
||
this.getList();
|
||
});
|
||
}
|
||
}
|
||
});
|
||
},
|
||
submitForm() {
|
||
// 提交时将负责人信息拆分到 ownerName/ownerPhone 字段
|
||
const submitData = { ...this.form };
|
||
if (submitData.expressId) {
|
||
updateExpress(submitData).then(() => {
|
||
uni.showToast({ title: '修改成功', icon: 'success' });
|
||
this.$refs.editPopup.close();
|
||
this.pageNum = 1;
|
||
this.getList();
|
||
});
|
||
} else {
|
||
addExpress(submitData).then(() => {
|
||
uni.showToast({ title: '新增成功', icon: 'success' });
|
||
this.$refs.editPopup.close();
|
||
this.pageNum = 1;
|
||
this.getList();
|
||
});
|
||
}
|
||
},
|
||
cancel() {
|
||
this.$refs.editPopup.close();
|
||
},
|
||
handleDetail(item) {
|
||
getExpress(item.expressId).then(res => {
|
||
this.expressDetail = res.data || {};
|
||
this.$refs.detailPopup.open();
|
||
});
|
||
},
|
||
closeDetailPopup() {
|
||
this.$refs.detailPopup.close();
|
||
},
|
||
handleUpdateStatus(item, status) {
|
||
if (status === 3) {
|
||
this.currentExceptionItem = item;
|
||
this.$refs.exceptionPopup.open();
|
||
this.exceptionForm = {
|
||
description: '',
|
||
reportTime: this.formatDate(new Date()),
|
||
remark: ''
|
||
};
|
||
return;
|
||
}
|
||
item.status = status;
|
||
updateExpress(item).then(() => {
|
||
uni.showToast({ title: '状态更新成功', icon: 'success' });
|
||
this.getList();
|
||
});
|
||
},
|
||
submitException() {
|
||
const item = this.currentExceptionItem;
|
||
addExpressQuestion({
|
||
expressId: item.expressId,
|
||
description: this.exceptionForm.description,
|
||
reportTime: this.exceptionForm.reportTime,
|
||
remark: this.exceptionForm.remark
|
||
}).then(() => {
|
||
item.status = 3;
|
||
return updateExpress(item);
|
||
}).then(() => {
|
||
uni.showToast({ title: '异常登记成功', icon: 'success' });
|
||
this.getList();
|
||
}).finally(() => {
|
||
this.$refs.exceptionPopup.close();
|
||
this.exceptionForm = {
|
||
description: '',
|
||
reportTime: '',
|
||
remark: ''
|
||
};
|
||
this.currentExceptionItem = null;
|
||
});
|
||
},
|
||
formatDate(date) {
|
||
const pad = n => n < 10 ? '0' + n : n;
|
||
return date.getFullYear() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate()) + ' '
|
||
+ pad(date.getHours()) + ':' + pad(date.getMinutes()) + ':' + pad(date.getSeconds());
|
||
},
|
||
handleRefresh(item) {
|
||
// 实现同步逻辑
|
||
uni.showToast({ title: '同步成功', icon: 'success' });
|
||
this.getList();
|
||
},
|
||
statusText(status) {
|
||
switch (status) {
|
||
case 0: return '未确认';
|
||
case 1: return '进行中';
|
||
case 2: return '已完成';
|
||
case 3: return '异常';
|
||
default: return '-';
|
||
}
|
||
},
|
||
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style scoped>
|
||
.express-page {
|
||
padding: 20rpx;
|
||
}
|
||
|
||
.express-list {
|
||
max-height: 100vh;
|
||
}
|
||
|
||
.express-card {
|
||
background: #fff;
|
||
border-radius: 16rpx;
|
||
margin-bottom: 20rpx;
|
||
box-shadow: 0 2rpx 8rpx #eee;
|
||
padding: 20rpx;
|
||
}
|
||
|
||
.card-header {
|
||
display: none;
|
||
}
|
||
|
||
.new-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 20rpx;
|
||
font-weight: bold;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.express-company {
|
||
color: #409EFF;
|
||
font-size: 30rpx;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.express-code {
|
||
color: #333;
|
||
font-size: 28rpx;
|
||
}
|
||
|
||
.status {
|
||
margin-left: auto;
|
||
font-size: 26rpx;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.status-0 {
|
||
color: #faad14;
|
||
}
|
||
|
||
.status-1 {
|
||
color: #1890ff;
|
||
}
|
||
|
||
.status-2 {
|
||
color: #52c41a;
|
||
}
|
||
|
||
.status-3 {
|
||
color: #f5222d;
|
||
}
|
||
|
||
.row {
|
||
display: flex;
|
||
align-items: center;
|
||
margin: 16rpx 0;
|
||
border-bottom: 1rpx solid #f0f0f0;
|
||
padding-bottom: 8rpx;
|
||
}
|
||
|
||
.express-card .row:last-of-type {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.row-between {
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.row-arrow {
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.row-party {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2rpx;
|
||
}
|
||
|
||
.party-label {
|
||
color: #333;
|
||
font-size: 26rpx;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.party-phone {
|
||
color: #888;
|
||
font-size: 24rpx;
|
||
}
|
||
|
||
.arrow {
|
||
color: #bbb;
|
||
font-size: 32rpx;
|
||
margin: 0 8rpx;
|
||
}
|
||
|
||
.plan-date {
|
||
color: #666;
|
||
font-size: 24rpx;
|
||
}
|
||
|
||
.remark-row {
|
||
background: #f7f7f7;
|
||
border-radius: 8rpx;
|
||
padding: 8rpx 12rpx;
|
||
margin-top: 6rpx;
|
||
border-bottom: 1rpx solid #f0f0f0;
|
||
margin-bottom: 16rpx;
|
||
padding-bottom: 8rpx;
|
||
}
|
||
|
||
.remark-label {
|
||
color: #999;
|
||
font-size: 24rpx;
|
||
}
|
||
|
||
.remark-content {
|
||
color: #333;
|
||
font-size: 24rpx;
|
||
}
|
||
|
||
.card-actions {
|
||
display: none;
|
||
}
|
||
|
||
.new-actions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 16rpx;
|
||
margin-top: 12rpx;
|
||
}
|
||
|
||
.action-btn {
|
||
padding: 8rpx 28rpx;
|
||
border-radius: 8rpx;
|
||
font-size: 26rpx;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
margin-left: 0;
|
||
transition: background 0.2s;
|
||
}
|
||
|
||
.action-confirm {
|
||
background: #f6ffed;
|
||
color: #52c41a;
|
||
border: 1rpx solid #b7eb8f;
|
||
}
|
||
|
||
.action-confirm:active {
|
||
background: #d9f7be;
|
||
}
|
||
|
||
.action-finish {
|
||
background: #e6f7ff;
|
||
color: #1890ff;
|
||
border: 1rpx solid #91d5ff;
|
||
}
|
||
|
||
.action-finish:active {
|
||
background: #bae7ff;
|
||
}
|
||
|
||
.action-sync {
|
||
background: #fffbe6;
|
||
color: #faad14;
|
||
border: 1rpx solid #ffe58f;
|
||
}
|
||
|
||
.action-sync:active {
|
||
background: #fff1b8;
|
||
}
|
||
|
||
.action-exception {
|
||
background: #fff7e6;
|
||
color: #fa541c;
|
||
border: 1rpx solid #ffd591;
|
||
}
|
||
|
||
.action-exception:active {
|
||
background: #ffe7ba;
|
||
}
|
||
|
||
.action-edit {
|
||
background: #f6ffed;
|
||
color: #389e0d;
|
||
border: 1rpx solid #b7eb8f;
|
||
}
|
||
|
||
.action-edit:active {
|
||
background: #d9f7be;
|
||
}
|
||
|
||
.action-detail {
|
||
background: #f0f5ff;
|
||
color: #2f54eb;
|
||
border: 1rpx solid #adc6ff;
|
||
}
|
||
|
||
.action-detail:active {
|
||
background: #d6e4ff;
|
||
}
|
||
|
||
/* 删除按钮:红色文字,边框和背景更警示 */
|
||
.action-delete {
|
||
background: #fff1f0;
|
||
color: #f5222d;
|
||
border: 1rpx solid #ffa39e;
|
||
}
|
||
|
||
.action-delete:active {
|
||
background: #ffccc7;
|
||
}
|
||
|
||
.loading-more,
|
||
.no-more {
|
||
text-align: center;
|
||
color: #888;
|
||
padding: 20rpx 0;
|
||
}
|
||
|
||
.popup-content {
|
||
padding: 20rpx;
|
||
}
|
||
|
||
.popup-bottom {
|
||
width: 100vw;
|
||
background: #fff;
|
||
border-top-left-radius: 24rpx;
|
||
border-top-right-radius: 24rpx;
|
||
box-sizing: border-box;
|
||
z-index: 9999;
|
||
position: relative;
|
||
}
|
||
|
||
.form-row {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.form-row text {
|
||
width: 120rpx;
|
||
}
|
||
|
||
.popup-actions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 20rpx;
|
||
margin-top: 20rpx;
|
||
}
|
||
|
||
.detail-row {
|
||
display: flex;
|
||
margin-bottom: 10rpx;
|
||
}
|
||
|
||
/* 新增按钮样式 */
|
||
.add-btn-wrapper {
|
||
position: fixed;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 60rpx;
|
||
display: flex;
|
||
justify-content: center;
|
||
z-index: 100;
|
||
}
|
||
|
||
.add-btn {
|
||
width: 100rpx;
|
||
height: 100rpx;
|
||
background: #409EFF;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
box-shadow: 0 4rpx 16rpx #b3d8ff;
|
||
cursor: pointer;
|
||
transition: background 0.2s;
|
||
}
|
||
|
||
.add-btn:active {
|
||
background: #66b1ff;
|
||
}
|
||
|
||
.add-btn .iconfont {
|
||
color: #fff;
|
||
font-size: 60rpx;
|
||
font-weight: bold;
|
||
line-height: 1;
|
||
}
|
||
|
||
.search-bar {
|
||
padding: 20rpx;
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 100;
|
||
position: relative;
|
||
}
|
||
|
||
.search-container {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 20rpx;
|
||
}
|
||
|
||
.task-type-button-container {
|
||
position: relative;
|
||
}
|
||
|
||
.search-input {
|
||
flex: 1;
|
||
}
|
||
|
||
.task-type-button,
|
||
.add-button {
|
||
width: 60rpx;
|
||
height: 60rpx;
|
||
background-color: transparent;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
</style>
|