664 lines
17 KiB
Vue
664 lines
17 KiB
Vue
<template>
|
||
<view class="container">
|
||
<view class="content">
|
||
<!-- 选择区域 -->
|
||
<view class="form-item">
|
||
<view class="label" @click="showTypeSelect = true" style="margin-bottom: 0;">
|
||
<u-icon name="list" color="#3B8CFF" size="38"></u-icon>
|
||
选择区域
|
||
<view class="select-box">
|
||
{{ workOrderArea || '请选择' }}
|
||
<u-icon name="arrow-down" color="#999" size="30"></u-icon>
|
||
</view>
|
||
</view>
|
||
<u-picker :show="showTypeSelect" :columns="areaList" keyName="title" @confirm="handleTypeConfirm"
|
||
@cancel="showTypeSelect = false">
|
||
</u-picker>
|
||
</view>
|
||
|
||
<!-- 工单分类 -->
|
||
<view class="form-item">
|
||
<view class="label" @click="showSelect = true" style="margin-bottom: 0;">
|
||
<u-icon name="list" color="#3B8CFF" size="38"></u-icon>
|
||
工单类型
|
||
<view class="select-box">
|
||
{{ workOrderCategory || '请选择' }}
|
||
<u-icon name="arrow-down" color="#999" size="30"></u-icon>
|
||
</view>
|
||
</view>
|
||
<u-picker :show="showSelect" :columns="categoryList" keyName="label" @confirm="handleConfirm"
|
||
@cancel="handleCancel">
|
||
</u-picker>
|
||
</view>
|
||
|
||
<view class="form-item">
|
||
<view class="label">
|
||
<u-icon name="order" color="#3B8CFF" size="38"></u-icon>
|
||
工单内容
|
||
</view>
|
||
<textarea v-model="workOrderContent" placeholder="请输入工单详细内容..." class="textarea" />
|
||
</view>
|
||
|
||
<!-- 联系人信息 -->
|
||
<view class="form-item contact-info">
|
||
<view class="contact-row">
|
||
<view class="label">
|
||
<u-icon name="account" color="#3B8CFF" size="38"></u-icon>
|
||
联系人
|
||
</view>
|
||
<input v-model="concatName" placeholder="请输入联系人姓名" class="contact-input" />
|
||
</view>
|
||
<view class="contact-row">
|
||
<view class="label">
|
||
<u-icon name="phone" color="#3B8CFF" size="38"></u-icon>
|
||
电话
|
||
</view>
|
||
<input
|
||
v-model="customerPhone"
|
||
placeholder="请输入联系人电话"
|
||
class="contact-input"
|
||
type="number"
|
||
@blur="validatePhone"
|
||
/>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 上传照片/视频 - 完全重写的上传组件 -->
|
||
<view class="form-item">
|
||
<view class="label">
|
||
<u-icon name="photo" color="#3B8CFF" size="38"></u-icon>
|
||
上传照片/视频
|
||
</view>
|
||
<view class="upload-area">
|
||
<view class="upload-list">
|
||
<view class="upload-item" v-for="(item, index) in fileList" :key="index">
|
||
<image v-if="!item.isVideo" :src="item.url" mode="aspectFill" @click="previewImage(index)"></image>
|
||
<video v-else :src="item.url" controls></video>
|
||
<view class="delete-btn" @click="handleDelete(index)">
|
||
<u-icon name="close" color="#fff" size="24"></u-icon>
|
||
</view>
|
||
</view>
|
||
<view class="upload-btn" @click="showUploadAction" v-if="fileList.length < 8">
|
||
<u-icon name="plus" size="40" color="#c0c4cc"></u-icon>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<text class="note">注:照片支持分批上传,但最大数量为8</text>
|
||
</view>
|
||
|
||
<!-- 工单地点 -->
|
||
<view class="form-item">
|
||
<view class="label">
|
||
<u-icon name="map" color="#3B8CFF" size="38"></u-icon>
|
||
维修地点
|
||
</view>
|
||
<textarea v-model="workOrderLocation" placeholder="请输入详细工作地点..." class="textarea" />
|
||
</view>
|
||
|
||
<!-- 提交按钮 -->
|
||
<view class="submit-button">
|
||
<u-button type="primary" @click="handleSubmit" shape="circle">提交工单</u-button>
|
||
</view>
|
||
</view>
|
||
<Footer></Footer>
|
||
|
||
<!-- 上传操作菜单 -->
|
||
<u-action-sheet
|
||
:list="actionList"
|
||
v-model="showActionSheet"
|
||
@click="handleActionClick"
|
||
></u-action-sheet>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import Footer from '@/components/footer_common.vue';
|
||
import { get, post } from '@/utils/request';
|
||
import { IMAGE_BASE_URL, BASE_URL } from '@/utils/config';
|
||
import uActionSheet from 'uview-ui/components/u-action-sheet/u-action-sheet';
|
||
|
||
export default {
|
||
components: {
|
||
Footer,uActionSheet
|
||
},
|
||
data() {
|
||
return {
|
||
workOrderContent: '',
|
||
concatName: '',
|
||
customerPhone: '',
|
||
fileList: [],
|
||
workOrderCategory: '',
|
||
workOrderCategoryID: '',
|
||
workOrderArea: '',
|
||
workOrderAreaID: '',
|
||
showSelect: false,
|
||
showTypeSelect: false,
|
||
categoryList: [],
|
||
workOrderLocation: '',
|
||
expectedTime: '',
|
||
uploadedImages: [],
|
||
uploadedVideos: [],
|
||
videoFormats: ['mp4', 'mov', 'avi', 'wmv', 'mpeg', '3gp', 'flv', 'mkv'],
|
||
areaList: [],
|
||
|
||
// 上传相关
|
||
showActionSheet: false,
|
||
actionList: [
|
||
{ text: '拍照', value: 'camera' },
|
||
{ text: '从相册选择', value: 'album' }
|
||
]
|
||
};
|
||
},
|
||
mounted() {
|
||
this.getOrderList();
|
||
this.getOrderTypeList();
|
||
},
|
||
methods: {
|
||
// 新增手机号验证方法
|
||
validatePhone() {
|
||
if (!this.customerPhone) return true;
|
||
const phoneReg = /^1[3-9]\d{9}$/;
|
||
if (!phoneReg.test(this.customerPhone)) {
|
||
uni.showToast({
|
||
title: '请输入正确的手机号码',
|
||
icon: 'none'
|
||
});
|
||
return false;
|
||
}
|
||
return true;
|
||
},
|
||
// 显示上传选项
|
||
showUploadAction() {
|
||
uni.showActionSheet({
|
||
itemList: ['拍照', '从相册选择'],
|
||
success: (res) => {
|
||
if (res.tapIndex === 0) {
|
||
this.chooseMedia('camera');
|
||
} else {
|
||
this.chooseMedia('album');
|
||
}
|
||
}
|
||
});
|
||
},
|
||
|
||
// 处理上传选项选择
|
||
handleActionClick(index) {
|
||
const action = this.actionList[index].value;
|
||
if (action === 'camera') {
|
||
this.chooseMedia('camera');
|
||
} else {
|
||
this.chooseMedia('album');
|
||
}
|
||
},
|
||
|
||
// 选择媒体文件
|
||
async chooseMedia(sourceType) {
|
||
try {
|
||
// 1. 选择图片
|
||
const res = await new Promise((resolve, reject) => {
|
||
uni.chooseImage({
|
||
count: 8 - this.fileList.length,
|
||
sourceType: [sourceType === 'camera' ? 'camera' : 'album'],
|
||
success: resolve,
|
||
fail: reject
|
||
});
|
||
});
|
||
|
||
// 2. 添加到预览列表
|
||
const newFiles = res.tempFilePaths.map(url => ({
|
||
url,
|
||
isVideo: false,
|
||
status: 'uploading'
|
||
}));
|
||
this.fileList = [...this.fileList, ...newFiles];
|
||
|
||
// 3. 逐个上传文件
|
||
for (const file of newFiles) {
|
||
await this.uploadFile(file);
|
||
}
|
||
|
||
} catch (err) {
|
||
console.error('选择图片失败:', err);
|
||
uni.showToast({ title: '选择图片失败', icon: 'none' });
|
||
}
|
||
},
|
||
_chooseMedia(sourceType) {
|
||
uni.chooseMedia({
|
||
count: 8 - this.fileList.length,
|
||
mediaType: ['image', 'video'],
|
||
sourceType: [sourceType],
|
||
success: async (res) => {
|
||
// 添加到预览列表
|
||
const newFiles = res.tempFiles.map(file => ({
|
||
url: file.tempFilePath,
|
||
isVideo: file.fileType === 'video',
|
||
status: 'uploading' // 新增上传状态
|
||
}));
|
||
this.fileList = [...this.fileList, ...newFiles];
|
||
|
||
// 逐个上传文件
|
||
for (const file of newFiles) {
|
||
await this.uploadFile(file); // 等待上传完成
|
||
}
|
||
}
|
||
});
|
||
},
|
||
|
||
// 上传文件
|
||
uploadFiles(files) {
|
||
files.forEach(file => {
|
||
this.uploadFile(file).then(res => {
|
||
const index = this.fileList.findIndex(item => item.url === file.url);
|
||
if (index !== -1) {
|
||
this.$set(this.fileList, index, {
|
||
...file,
|
||
status: 'success',
|
||
serverUrl: res
|
||
});
|
||
|
||
if (file.isVideo) {
|
||
this.uploadedVideos.push(res.replace(BASE_URL, ''));
|
||
} else {
|
||
this.uploadedImages.push(res.replace(BASE_URL, ''));
|
||
}
|
||
}
|
||
}).catch(err => {
|
||
const index = this.fileList.findIndex(item => item.url === file.url);
|
||
if (index !== -1) {
|
||
this.$set(this.fileList, index, {
|
||
...file,
|
||
status: 'failed'
|
||
});
|
||
}
|
||
console.error('上传失败:', err);
|
||
});
|
||
});
|
||
},
|
||
|
||
// 单个文件上传
|
||
async uploadFile(file) {
|
||
try {
|
||
const res = await new Promise((resolve, reject) => {
|
||
uni.uploadFile({
|
||
url: `${IMAGE_BASE_URL}/api/v1/upload`,
|
||
filePath: file.url,
|
||
name: 'file',
|
||
header: {
|
||
'Authorization': `Bearer ${uni.getStorageSync('token')}`,
|
||
'Content-Type': 'multipart/form-data'
|
||
},
|
||
success: (uploadRes) => {
|
||
try {
|
||
const data = JSON.parse(uploadRes.data);
|
||
if (data.success) {
|
||
resolve(data.data); // 使用服务器返回的完整URL
|
||
} else {
|
||
reject(data.message || '上传失败');
|
||
}
|
||
} catch (e) {
|
||
reject('解析响应失败');
|
||
}
|
||
},
|
||
fail: (err) => reject(err)
|
||
});
|
||
});
|
||
|
||
// 更新文件状态
|
||
const index = this.fileList.findIndex(f => f.url === file.url);
|
||
if (index !== -1) {
|
||
this.$set(this.fileList, index, {
|
||
...file,
|
||
status: 'success',
|
||
serverUrl: res // 存储服务器返回的URL
|
||
});
|
||
|
||
// 添加到提交数组
|
||
if (file.isVideo) {
|
||
this.uploadedVideos.push(res);
|
||
} else {
|
||
this.uploadedImages.push(res);
|
||
}
|
||
}
|
||
|
||
} catch (err) {
|
||
console.error('上传失败:', err);
|
||
const index = this.fileList.findIndex(f => f.url === file.url);
|
||
if (index !== -1) {
|
||
this.$set(this.fileList, index, {
|
||
...file,
|
||
status: 'failed',
|
||
error: err.message || err
|
||
});
|
||
}
|
||
uni.showToast({
|
||
title: `上传失败: ${err.message || err}`,
|
||
icon: 'none'
|
||
});
|
||
}
|
||
},
|
||
|
||
// 预览图片
|
||
previewImage(index) {
|
||
const images = this.fileList
|
||
.filter(item => !item.isVideo)
|
||
.map(item => item.url);
|
||
|
||
uni.previewImage({
|
||
current: index,
|
||
urls: images
|
||
});
|
||
},
|
||
|
||
// 删除文件
|
||
handleDelete(index) {
|
||
const file = this.fileList[index];
|
||
|
||
if (file.serverUrl) {
|
||
if (file.isVideo) {
|
||
const videoIndex = this.uploadedVideos.indexOf(file.serverUrl.replace(BASE_URL, ''));
|
||
if (videoIndex !== -1) this.uploadedVideos.splice(videoIndex, 1);
|
||
} else {
|
||
const imageIndex = this.uploadedImages.indexOf(file.serverUrl.replace(BASE_URL, ''));
|
||
if (imageIndex !== -1) this.uploadedImages.splice(imageIndex, 1);
|
||
}
|
||
}
|
||
|
||
this.fileList.splice(index, 1);
|
||
},
|
||
|
||
async handleSubmit() {
|
||
// 在提交前验证手机号
|
||
if (!this.validatePhone()) {
|
||
return;
|
||
}
|
||
if (!this.workOrderContent) {
|
||
uni.showToast({
|
||
title: '请填写工单内容',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (!this.concatName) {
|
||
uni.showToast({
|
||
title: '请填写联系人姓名',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (!this.customerPhone) {
|
||
uni.showToast({
|
||
title: '请填写联系电话',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (!this.workOrderCategory) {
|
||
uni.showToast({
|
||
title: '请选择工单类型',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (!this.workOrderLocation) {
|
||
uni.showToast({
|
||
title: '请填写工作地点',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
const submitData = {
|
||
title: this.workOrderContent,
|
||
concatName: this.concatName,
|
||
customerPhone: this.customerPhone,
|
||
orderTypeId: this.workOrderCategoryID,
|
||
orderAreaId: this.workOrderAreaID,
|
||
address: this.workOrderLocation,
|
||
images: this.uploadedImages,
|
||
videos: this.uploadedVideos,
|
||
};
|
||
|
||
console.log('提交数据:', submitData);
|
||
|
||
const res = await post('/api/v1/app_auth/work-order', submitData);
|
||
|
||
uni.hideLoading();
|
||
|
||
if (res && res.success) {
|
||
uni.showToast({
|
||
title: '提交成功',
|
||
icon: 'success',
|
||
duration: 2000
|
||
});
|
||
this.resetForm();
|
||
setTimeout(() => {
|
||
uni.navigateTo({
|
||
url: `/pages/myTickets/index`,
|
||
});
|
||
}, 1000)
|
||
} else {
|
||
uni.showToast({
|
||
title: res?.message || '提交失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
},
|
||
|
||
resetForm() {
|
||
this.workOrderContent = '';
|
||
this.concatName = '';
|
||
this.customerPhone = '';
|
||
this.workOrderCategoryID = '';
|
||
this.workOrderCategory = '';
|
||
this.workOrderLocation = '';
|
||
this.fileList = [];
|
||
this.uploadedImages = [];
|
||
this.uploadedVideos = [];
|
||
this.workOrderArea = '';
|
||
this.workOrderAreaID = '';
|
||
},
|
||
|
||
handleConfirm(e) {
|
||
this.workOrderCategory = e.value[0].label;
|
||
this.workOrderCategoryID = e.value[0].id;
|
||
this.showSelect = false;
|
||
},
|
||
|
||
handleTypeConfirm(e) {
|
||
this.workOrderArea = e.value[0].title;
|
||
this.workOrderAreaID = e.value[0].id;
|
||
this.showTypeSelect = false;
|
||
},
|
||
|
||
handleCancel() {
|
||
this.showSelect = false;
|
||
},
|
||
|
||
async getOrderList() {
|
||
try {
|
||
const res = await get('/api/v1/apps/work-order-type');
|
||
if (res?.success) {
|
||
this.categoryList = [
|
||
[...res.data]
|
||
]
|
||
}
|
||
} catch (err) {
|
||
console.error('获取工单类型失败:', err);
|
||
uni.showToast({
|
||
title: '获取工单类型失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
},
|
||
|
||
async getOrderTypeList() {
|
||
try {
|
||
const res = await get('/api/v1/apps/work-order-area');
|
||
if (res?.success) {
|
||
this.areaList = [
|
||
[...res.data]
|
||
]
|
||
}
|
||
} catch (err) {
|
||
console.error('获取工作区域失败:', err);
|
||
uni.showToast({
|
||
title: '获取工作区域失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
}
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.container {
|
||
width: 100%;
|
||
min-height: 100vh;
|
||
background: #f5f7fa;
|
||
padding-bottom: 120rpx;
|
||
|
||
.content {
|
||
padding: 20rpx;
|
||
|
||
.form-item {
|
||
background: #fff;
|
||
border-radius: 16rpx;
|
||
padding: 24rpx;
|
||
margin-bottom: 24rpx;
|
||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
|
||
|
||
.label {
|
||
font-size: 32rpx;
|
||
color: #333;
|
||
font-weight: 500;
|
||
margin-bottom: 24rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
|
||
.u-icon {
|
||
margin-right: 12rpx;
|
||
}
|
||
}
|
||
|
||
.textarea {
|
||
width: 95%;
|
||
height: 180rpx;
|
||
padding: 16rpx;
|
||
background: #f8f8f8;
|
||
border-radius: 8rpx;
|
||
font-size: 28rpx;
|
||
}
|
||
|
||
.voice-icon {
|
||
position: absolute;
|
||
right: 40rpx;
|
||
bottom: 40rpx;
|
||
}
|
||
|
||
.select-box {
|
||
margin-left: auto;
|
||
color: #666;
|
||
display: flex;
|
||
align-items: center;
|
||
font-size: 28rpx;
|
||
|
||
.u-icon {
|
||
margin-left: 8rpx;
|
||
}
|
||
}
|
||
|
||
.upload-area {
|
||
margin-top: 16rpx;
|
||
|
||
.upload-list {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
margin: -5rpx;
|
||
|
||
.upload-item, .upload-btn {
|
||
width: 160rpx;
|
||
height: 160rpx;
|
||
margin: 5rpx;
|
||
position: relative;
|
||
background: #f8f8f8;
|
||
border-radius: 8rpx;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
overflow: hidden;
|
||
|
||
image, video {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.delete-btn {
|
||
position: absolute;
|
||
right: 0;
|
||
top: 0;
|
||
width: 40rpx;
|
||
height: 40rpx;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
border-radius: 0 0 0 8rpx;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
}
|
||
|
||
.upload-btn {
|
||
border: 1rpx dashed #c0c4cc;
|
||
}
|
||
}
|
||
}
|
||
|
||
.note {
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
margin-top: 24rpx;
|
||
display: block;
|
||
}
|
||
}
|
||
|
||
.contact-info {
|
||
.contact-row {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 20rpx;
|
||
|
||
&:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.label {
|
||
margin-bottom: 0;
|
||
width: 160rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.contact-input {
|
||
flex: 1;
|
||
padding: 16rpx;
|
||
background: #f8f8f8;
|
||
border-radius: 8rpx;
|
||
font-size: 28rpx;
|
||
}
|
||
}
|
||
}
|
||
|
||
.submit-button {
|
||
margin-top: 40rpx;
|
||
|
||
.u-button {
|
||
height: 88rpx;
|
||
font-size: 32rpx;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style> |