You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
crmeb/app/pages/supply_chain/dispatch/index.vue

838 lines
22 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<z-paging ref="paging" v-model="records" @query="queryList" :layout-only="activeTab === 'form'">
<template #top>
<!-- 顶部标签切换 -->
<view class="tabs" v-if="checkPermi('send_order') && parentStatus !== '99'">
<view
class="tab"
:class="activeTab === 'records' ? 'active' : ''"
@click="activeTab = 'records'"
>
派单记录
</view>
<view
class="tab"
:class="activeTab === 'form' ? 'active' : ''"
@click="switchToForm"
>
新增派单
</view>
</view>
</template>
<!-- 派单记录列表 -->
<view class="tab-content" v-show="activeTab === 'records'">
<view
class="dispatch-card"
v-for="(item, index) in records"
:key="index"
>
<view class="card-header">
<text class="dispatch-status" :class="dispatchStatusClassMap[item.status] || ''">
{{ getDispatchStatusText(item.status) }}
</text>
<text class="dispatch-time">{{ item.assignTime || '' }}</text>
</view>
<view class="card-body">
<view class="row">
<text class="label">派单人</text>
<text class="value">{{ item.assignerName || '-' }}</text>
</view>
<view class="row">
<text class="label">执行人</text>
<text class="value">{{ item.executorName || '-' }}</text>
</view>
<view class="row">
<text class="label">执行人部门</text>
<text class="value">{{ item.executorDeptName || '-' }}</text>
</view>
<view class="row">
<text class="label">联系方式</text>
<text class="value">{{ item.phone || '-' }}</text>
</view>
<view class="row">
<text class="label">工作内容</text>
<text class="value">{{ item.assigneContext || '-' }}</text>
</view>
<view class="row">
<text class="label">备注</text>
<text class="value">{{ item.dispatchNote || '-' }}</text>
</view>
<view class="row">
<text class="label">预计完成时间</text>
<text class="value">{{ item.expectedCompleteTime || '-' }}</text>
</view>
<view class="row" v-if="item.completeTime">
<text class="label">实际完成时间</text>
<text class="value">{{ item.completeTime }}</text>
</view>
<view class="row" v-if="item.handleContent">
<text class="label">处理内容</text>
<text class="value">{{ item.handleContent }}</text>
</view>
<view class="row" v-if="item.consumables">
<text class="label">耗材</text>
<text class="value">{{ item.consumables }}</text>
</view>
<view class="row" v-if="item.costAmount">
<text class="label">费用</text>
<text class="value">¥{{ item.costAmount }}</text>
</view>
<!-- <view class="row">
<text class="label">业主确认</text>
<text class="value">{{ item.ownerConfirm || '否' }}</text>
</view> -->
<!-- 处理结果图片 -->
<view class="image-section" v-if="item.files && item.files.length">
<text class="image-title">处理图片</text>
<view class="image-grid">
<view
class="image-item"
v-for="(file, imgIndex) in item.files"
:key="imgIndex"
@click="previewImages(item.files, imgIndex)"
>
<image
:src="HTTP_ADMIN_URL + '/' + (file.url || file.attDir || file)"
mode="aspectFill"
></image>
</view>
</view>
</view>
</view>
<!-- 处理按钮 -->
<view class="card-footer" v-if="item.status !== '2' && parentStatus !== '99' && String(item.executorId) === String(currentAdminId)">
<text class="handle-btn" @click="openHandlePopup(item)">处理</text>
</view>
</view>
</view>
<!-- 处理弹窗 -->
<HandlePopup ref="handlePopup" @submit="handleDispatchSubmit" />
<!-- 新增派单表单 / 处理表单 -->
<view class="tab-content" v-show="activeTab === 'form'">
<view class="form-card">
<!-- 部门选择 -->
<view class="form-item">
<text class="label">执行部门</text>
<view class="picker-row" @click="openDeptPicker">
<text class="value" v-if="form.executorDeptName">
{{ form.executorDeptName }}
</text>
<text class="placeholder" v-else>请选择执行部门</text>
<text class="iconfont icon-xiangyou"></text>
</view>
</view>
<!-- 执行人选择 -->
<view class="form-item">
<text class="label">执行人</text>
<view class="picker-row" @click="openExecutorPicker">
<text class="value" v-if="form.executorName">
{{ form.executorName }}
</text>
<text class="placeholder" v-else>请选择执行人</text>
<text class="iconfont icon-xiangyou"></text>
</view>
</view>
<!-- 联系电话 -->
<view class="form-item">
<text class="label">联系电话</text>
<input
class="input"
type="text"
v-model="form.phone"
placeholder="请输入联系电话"
maxlength="20"
/>
</view>
<!-- 工作内容 -->
<view class="form-item">
<text class="label">工作内容</text>
<textarea
class="textarea"
v-model="form.handleContent"
placeholder="请输入工作内容"
maxlength="500"
:auto-height="true"
/>
<text class="count">{{ (form.handleContent || '').length }}/500</text>
</view>
<!-- 备注 -->
<view class="form-item">
<text class="label">备注</text>
<textarea
class="textarea"
v-model="form.dispatchNote"
placeholder="请输入备注信息"
maxlength="200"
:auto-height="true"
/>
<text class="count">{{ (form.dispatchNote || '').length }}/200</text>
</view>
<!-- 预计完成时间 -->
<view class="form-item">
<text class="label">预计完成时间</text>
<view class="picker-row" @click="openDatePicker">
<text class="value" v-if="form.expectedCompleteTime">
{{ form.expectedCompleteTime }}
</text>
<text class="placeholder" v-else>请选择预计完成时间</text>
<text class="iconfont icon-xiangyou"></text>
</view>
</view>
</view>
<view class="submit-section">
<view class="submit-btn" @click="submitForm">
提交派单
</view>
</view>
</view>
<!-- 部门选择弹窗 -->
<uni-popup ref="deptPopup" type="bottom">
<view class="popup-content">
<view class="popup-header">
<text class="popup-title">选择部门</text>
<text class="popup-close" @click="$refs.deptPopup.close()">×</text>
</view>
<scroll-view scroll-y class="popup-list">
<view
class="popup-item"
v-for="item in flatDeptList"
:key="item.deptId"
@click="chooseDept(item)"
:style="{ paddingLeft: (item.level * 40 + 30) + 'rpx' }"
>
<text>{{ item.deptName }}</text>
</view>
</scroll-view>
</view>
</uni-popup>
<!-- 执行人选择弹窗 -->
<uni-popup ref="executorPopup" type="bottom">
<view class="popup-content">
<view class="popup-header">
<text class="popup-title">选择执行人</text>
<text class="popup-close" @click="$refs.executorPopup.close()">×</text>
</view>
<scroll-view scroll-y class="popup-list">
<view
class="popup-item"
v-for="item in filteredUsers"
:key="item.id"
@click="chooseExecutor(item)"
>
<text class="user-name">{{ item.realName }}</text>
<text class="user-dept">{{ item.deptNames ? item.deptNames.join(',') : '' }}</text>
</view>
<view class="empty" v-if="filteredUsers.length === 0">
请先选择部门
</view>
</scroll-view>
</view>
</uni-popup>
<!-- 日期选择弹窗 -->
<uni-calendar
ref="calendar"
:insert="false"
:range="false"
:start-date="today"
@confirm="onDateConfirm"
/>
</z-paging>
</template>
<script>
import { listMaintenanceDispatch, createMaintenanceDispatch, updateMaintenanceDispatch, getDeptTree, listAdmins } from '@/api/property.js';
import { HTTP_ADMIN_URL } from '@/config/app';
import HandlePopup from './components/HandlePopup.vue';
import { checkPermi } from '@/utils/auth/permission.js';
export default {
components: { HandlePopup },
computed: {
// 当前管理用户的ID
currentAdminId() {
return this.$store.getters.adminInfo?.id;
}
},
data() {
return {
checkPermi,
HTTP_ADMIN_URL,
orderId: '',
parentStatus: '', // 上级报修单状态
activeTab: 'records',
records: [],
loading: false,
dispatchStatusClassMap: {
0: 'pending',
1: 'doing',
2: 'done'
},
form: {
executorId: '',
executorName: '',
executorDept: '',
executorDeptName: '',
phone: '',
handleContent: '',
dispatchNote: '',
expectedCompleteTime: ''
},
deptList: [],
flatDeptList: [],
userList: [],
filteredUsers: [],
today: ''
};
},
onLoad(options) {
this.orderId = options.orderId || '';
this.parentStatus = options.status || '';
const now = new Date();
this.today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
},
onShow() {
// if (this.activeTab === 'records' && this.$refs.paging) {
// this.$refs.paging.reload();
// }
},
methods: {
queryList(pageNo, pageSize) {
listMaintenanceDispatch({
page: pageNo,
limit: pageSize,
orderId: this.orderId
}).then((res) => {
const list = res?.data?.list || [];
this.$refs.paging.complete(list);
}).catch((err) => {
console.error('获取派单记录失败:', err);
this.$refs.paging.complete(false);
});
},
getDispatchStatusText(status) {
const statusMap = {
0: '待处理',
1: '处理中',
2: '已完成'
};
return statusMap[status] || '未知状态';
},
switchToForm() {
this.activeTab = 'form';
},
previewImages(files, index) {
if (!files || !files.length) return;
const urls = files.map(file => {
const path = file.url || file.attDir || file;
return this.HTTP_ADMIN_URL + '/' + path;
});
uni.previewImage({ current: index, urls });
},
openHandlePopup(item) {
this.$refs.handlePopup.open(item);
},
async handleDispatchSubmit(updateData) {
try {
uni.showLoading({ title: '提交中...', mask: true });
await updateMaintenanceDispatch(updateData);
uni.showToast({ title: '处理完成', icon: 'success' });
this.$refs.paging.reload();
} catch (e) {
console.error('处理失败:', e);
uni.showToast({ title: typeof e === 'string' ? e : '处理失败', icon: 'none' });
} finally {
uni.hideLoading();
}
},
async openDeptPicker() {
if (this.deptList.length === 0) {
await this.fetchDeptTree();
}
this.$refs.deptPopup.open();
},
async fetchDeptTree() {
try {
const res = await getDeptTree();
this.deptList = res?.data || [];
this.flatDeptList = this.flattenDept(this.deptList);
} catch (e) {
console.error('获取部门树失败:', e);
}
},
flattenDept(depts, level = 0) {
let result = [];
depts.forEach(dept => {
result.push({ ...dept, level });
if (dept.children && dept.children.length > 0) {
result = result.concat(this.flattenDept(dept.children, level + 1));
}
});
return result;
},
chooseDept(item) {
this.form.executorDept = String(item.deptId);
this.form.executorDeptName = item.deptName;
this.form.executorId = '';
this.form.executorName = '';
this.filteredUsers = [];
this.$refs.deptPopup.close();
this.filterUsersByDept(item.deptId);
},
async filterUsersByDept(deptId) {
try {
if (this.userList.length === 0) {
const res = await listAdmins({ limit: 999 });
this.userList = res?.data?.list || [];
}
this.filteredUsers = this.userList.filter(user => {
return user.depts && user.depts.includes(String(deptId));
});
} catch (e) {
console.error('获取用户列表失败:', e);
}
},
openExecutorPicker() {
if (!this.form.executorDept) {
uni.showToast({ title: '请先选择部门', icon: 'none' });
return;
}
this.$refs.executorPopup.open();
},
chooseExecutor(item) {
this.form.executorId = String(item.id);
this.form.executorName = item.realName;
if (item.phone) {
this.form.phone = item.phone;
}
this.$refs.executorPopup.close();
},
openDatePicker() {
this.$refs.calendar.open();
},
onDateConfirm(e) {
this.form.expectedCompleteTime = e.fulldate + ' 00:00:00';
},
getCurrentTime() {
const now = new Date();
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
},
async submitForm() {
if (!this.form.executorId) {
uni.showToast({ title: '请选择执行人', icon: 'none' });
return;
}
if (!this.form.handleContent.trim()) {
uni.showToast({ title: '请输入工作内容', icon: 'none' });
return;
}
try {
uni.showLoading({ title: '提交中...', mask: true });
const payload = {
orderId: this.orderId,
assignerId: 1,
executorId: this.form.executorId,
executorDept: this.form.executorDept,
phone: this.form.phone,
assigneContext: this.form.handleContent,
dispatchNote: this.form.dispatchNote,
expectedCompleteTime: this.form.expectedCompleteTime,
status: '1',
assignTime: this.getCurrentTime(),
files: []
};
await createMaintenanceDispatch(payload);
uni.showToast({ title: '派单成功', icon: 'success' });
this.activeTab = 'records';
this.$refs.paging.reload();
this.resetForm();
} catch (e) {
console.error('提交失败:', e);
uni.showToast({ title: typeof e === 'string' ? e : '提交失败', icon: 'none' });
} finally {
uni.hideLoading();
}
},
resetForm() {
this.form = {
executorId: '',
executorName: '',
executorDept: '',
executorDeptName: '',
phone: '',
handleContent: '',
dispatchNote: '',
expectedCompleteTime: ''
};
this.images = [];
}
}
};
</script>
<style lang="scss">
.page {
min-height: 100vh;
background: #f6f7fb;
}
.tabs {
display: flex;
background: #fff;
border-radius: 16rpx;
padding: 8rpx;
margin: 24rpx;
.tab {
flex: 1;
text-align: center;
padding: 20rpx 0;
font-size: 28rpx;
color: #6b7280;
border-radius: 12rpx;
&.active {
background: #3b82f6;
color: #fff;
font-weight: 600;
}
}
}
.tab-content {
padding: 0 24rpx;
}
.dispatch-card {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.06);
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
.dispatch-status {
padding: 6rpx 18rpx;
border-radius: 20rpx;
font-size: 22rpx;
}
.dispatch-status.pending {
background: #E6F7FF;
color: #409EFF;
}
.dispatch-status.doing {
background: #FFF7E6;
color: #FA8C16;
}
.dispatch-status.done {
background: #F6FFED;
color: #52C41A;
}
.dispatch-time {
font-size: 22rpx;
color: #999;
}
}
.card-body {
.row {
display: flex;
margin-bottom: 10rpx;
.label {
width: 160rpx;
font-size: 24rpx;
color: #666;
}
.value {
flex: 1;
font-size: 24rpx;
color: #333;
}
}
.image-section {
margin-top: 16rpx;
.image-title {
display: block;
font-size: 22rpx;
color: #666;
margin-bottom: 10rpx;
}
.image-grid {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
.image-item {
width: 160rpx;
height: 160rpx;
border-radius: 8rpx;
overflow: hidden;
image {
width: 100%;
height: 100%;
}
}
}
}
}
.card-footer {
display: flex;
justify-content: flex-end;
margin-top: 20rpx;
padding-top: 20rpx;
border-top: 1rpx solid #f0f0f0;
.handle-btn {
padding: 12rpx 40rpx;
background: #52C41A;
color: #fff;
border-radius: 24rpx;
font-size: 26rpx;
}
}
}
.form-card {
background: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 30rpx;
.form-item {
margin-bottom: 30rpx;
.label {
display: block;
font-size: 28rpx;
color: #374151;
margin-bottom: 12rpx;
font-weight: 500;
}
.input {
width: 100%;
height: 80rpx;
background: #f9fafb;
border-radius: 12rpx;
padding: 0 20rpx;
font-size: 28rpx;
box-sizing: border-box;
}
.textarea {
width: 100%;
min-height: 160rpx;
background: #f9fafb;
border-radius: 12rpx;
padding: 20rpx;
font-size: 28rpx;
box-sizing: border-box;
line-height: 1.6;
}
.count {
display: block;
margin-top: 10rpx;
text-align: right;
font-size: 22rpx;
color: #999;
}
.picker-row {
height: 80rpx;
background: #f9fafb;
border-radius: 12rpx;
padding: 0 20rpx;
display: flex;
align-items: center;
justify-content: space-between;
.value {
font-size: 28rpx;
color: #333;
}
.placeholder {
font-size: 28rpx;
color: #999;
}
.iconfont {
font-size: 26rpx;
color: #ccc;
}
}
.upload-section {
.upload-list {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
.upload-item {
width: 180rpx;
height: 180rpx;
position: relative;
border-radius: 12rpx;
overflow: hidden;
image {
width: 100%;
height: 100%;
}
.delete-btn {
position: absolute;
top: 10rpx;
right: 10rpx;
width: 40rpx;
height: 40rpx;
background: rgba(0, 0, 0, 0.6);
color: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
}
}
.upload-btn {
width: 180rpx;
height: 180rpx;
border: 2rpx dashed #d9d9d9;
border-radius: 12rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #fafafa;
.iconfont {
font-size: 48rpx;
color: #bfbfbf;
margin-bottom: 8rpx;
}
text {
font-size: 24rpx;
color: #999;
}
}
}
.hint {
display: block;
margin-top: 16rpx;
font-size: 22rpx;
color: #999;
}
}
}
}
.submit-section {
.submit-btn {
height: 88rpx;
background: #3b82f6;
color: #fff;
border-radius: 44rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 30rpx;
font-weight: 600;
box-shadow: 0 4rpx 12rpx rgba(59, 130, 246, 0.35);
margin: 0 24rpx 30rpx;
}
}
.popup-content {
width: 100%;
max-height: 70vh;
background: #fff;
border-radius: 20rpx 20rpx 0 0;
padding: 24rpx 30rpx 40rpx;
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
.popup-title {
font-size: 30rpx;
font-weight: 600;
color: #333;
}
.popup-close {
font-size: 50rpx;
color: #999;
line-height: 1;
}
}
.popup-list {
max-height: 60vh;
.popup-item {
padding: 24rpx 30rpx;
border-bottom: 1rpx solid #f0f0f0;
font-size: 28rpx;
color: #333;
.user-name {
margin-right: 16rpx;
}
.user-dept {
font-size: 24rpx;
color: #999;
}
}
.empty {
text-align: center;
padding: 40rpx 0;
font-size: 26rpx;
color: #999;
}
}
}
</style>