Files
im-uniapp/pages/workbench/task/create.vue
2025-07-11 18:05:05 +08:00

973 lines
22 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>
<view class="create-task-container">
<!-- 表单内容 -->
<view class="form-container">
<form @submit="submitForm">
<!-- 任务主题 -->
<view class="form-item">
<view class="form-label">
<text class="label-text">任务主题</text>
<text class="required">*</text>
</view>
<input
v-model="form.taskTitle"
class="form-input"
placeholder="请输入任务主题"
maxlength="100"
/>
</view>
<!-- 工作类型 -->
<view class="form-item">
<view class="form-label">
<text class="label-text">工作类型</text>
<text class="required">*</text>
</view>
<picker
@change="onTaskTypeChange"
:value="taskTypeIndex"
:range="taskTypeOptions"
range-key="label"
class="form-picker"
>
<view class="picker-text">
{{ taskTypeOptions[taskTypeIndex] ? taskTypeOptions[taskTypeIndex].label : '请选择工作类型' }}
</view>
</picker>
</view>
<!-- 任务模式 -->
<view class="form-item">
<view class="form-label">
<text class="label-text">任务模式</text>
</view>
<view class="radio-group">
<view
class="radio-item"
:class="{ active: form.status === 0 }"
@click="form.status = 0"
>
<view class="radio-circle"></view>
<text class="radio-text">单任务模式</text>
</view>
<view
class="radio-item"
:class="{ active: form.status === 1 }"
@click="form.status = 1"
>
<view class="radio-circle"></view>
<text class="radio-text">报工模式</text>
</view>
</view>
</view>
<!-- 报工周期 -->
<view class="form-item" v-if="form.status === 1">
<view class="form-label">
<text class="label-text">报工周期</text>
</view>
<input
v-model="form.timeGap"
type="number"
class="form-input"
placeholder="请输入报工周期"
min="1"
max="10"
/>
</view>
<!-- 任务时间 -->
<view class="form-item">
<view class="form-label">
<text class="label-text">开始时间</text>
<text class="required">*</text>
</view>
<picker
mode="date"
@change="onBeginTimeChange"
class="form-picker"
>
<view class="picker-text">
{{ form.beginTime || '请选择开始时间' }}
</view>
</picker>
</view>
<view class="form-item" v-if="form.status === 0">
<view class="form-label">
<text class="label-text">结束时间</text>
<text class="required">*</text>
</view>
<picker
mode="date"
@change="onFinishTimeChange"
class="form-picker"
>
<view class="picker-text">
{{ form.finishTime || '请选择结束时间' }}
</view>
</picker>
</view>
<!-- 优先级 -->
<view class="form-item">
<view class="form-label">
<text class="label-text">优先级</text>
</view>
<view class="radio-group">
<view
class="radio-item"
:class="{ active: form.taskGrade === '0' }"
@click="form.taskGrade = '0'"
>
<view class="radio-circle"></view>
<text class="radio-text">一般</text>
</view>
<view
class="radio-item"
:class="{ active: form.taskGrade === '1' }"
@click="form.taskGrade = '1'"
>
<view class="radio-circle"></view>
<text class="radio-text">中等</text>
</view>
<view
class="radio-item"
:class="{ active: form.taskGrade === '2' }"
@click="form.taskGrade = '2'"
>
<view class="radio-circle"></view>
<text class="radio-text">紧急</text>
</view>
</view>
</view>
<!-- 关联项目 -->
<view class="form-item">
<view class="form-label">
<text class="label-text">关联项目</text>
</view>
<picker
@change="onProjectChange"
:value="projectIndex"
:range="projectList"
range-key="projectName"
class="form-picker"
>
<view class="picker-text">
{{ projectIndex === -1 ? '请选择项目' : projectList[projectIndex].projectName }}
</view>
</picker>
<view v-if="projectIndex !== -1" class="selected-project" style="margin-top: 10rpx;">
<text>{{ projectList[projectIndex].projectName }}</text>
<text class="remove-btn" @click="removeProject" style="color:#ff4757;margin-left:20rpx;">移除</text>
</view>
</view>
<!-- 执行人 -->
<view class="form-item">
<view class="form-label">
<text class="label-text">执行人</text>
<text class="required">*</text>
</view>
<!-- 已选用户标签 -->
<view v-if="selectedUsers.length > 0" class="user-tags">
<view v-for="user in selectedUsers" :key="user.userId" class="user-tag">
<text>{{ user.nickName }}</text>
<text class="remove-btn" @click="removeUser(user)">×</text>
</view>
</view>
<button type="primary" size="mini" @click="openUserPopup">点击选择</button>
<!-- 弹窗 -->
<uni-popup ref="userPopup" type="center">
<view class="user-select-popup">
<view class="popup-header">
<text>选择执行人</text>
<text class="close-btn" @click="closeUserPopup">×</text>
</view>
<scroll-view scroll-y class="user-list" style="height: 400rpx;">
<view
v-for="user in userList"
:key="user.userId"
class="user-row"
@click="toggleUser(user)"
>
<checkbox :checked="isSelected(user)" />
<text>{{ user.nickName }}</text>
<text class="user-dept">{{ user.deptName }}</text>
</view>
</scroll-view>
<view class="popup-footer">
<button type="primary" size="mini" @click="confirmUserSelect">确定</button>
</view>
</view>
</uni-popup>
</view>
<!-- 详细描述 -->
<view class="form-item">
<view class="form-label">
<text class="label-text">详细描述</text>
</view>
<textarea
v-model="form.content"
class="form-textarea"
placeholder="请输入详细描述"
maxlength="1000"
auto-height
/>
</view>
<!-- 附件 -->
<view class="form-item">
<view class="form-label">
<text class="label-text">附件</text>
</view>
<view class="file-upload">
<view class="upload-btn" @click="chooseFile">
<text class="upload-icon">+</text>
<text class="upload-text">选择文件</text>
</view>
<view class="file-list" v-if="fileList.length > 0">
<view
class="file-item"
v-for="(file, index) in fileList"
:key="index"
>
<text class="file-name">{{ file.name }}</text>
<text class="file-size">{{ formatFileSize(file.size) }}</text>
<text class="remove-file" @click="removeFile(index)">×</text>
</view>
</view>
</view>
</view>
<!-- 提交按钮 -->
<view class="submit-container">
<button
type="primary"
class="submit-btn"
:loading="loading"
@click="submitForm"
>
{{ loading ? '提交中...' : '提交' }}
</button>
</view>
</form>
</view>
</view>
</template>
<script>
import { addTask } from '@/api/oa/task'
import { listProject } from '@/api/oa/project'
import { selectUser, deptTreeSelect } from '@/api/oa/user'
import { uploadImage } from '@/api/common/upload'
export default {
name: 'CreateTask',
data() {
return {
loading: false,
form: {
taskTitle: '',
taskType: '',
status: 0,
timeGap: 3,
beginTime: '',
finishTime: '',
taskGrade: '0',
projectId: '',
workerIds: '',
content: '',
accessory: ''
},
taskTypeOptions: [
{ label: '技术审查', value: 1 },
{ label: '初步设计', value: 2 },
{ label: '图纸详细设计', value: 3 },
{ label: 'PLC程序设计', value: 4 },
{ label: '画面设计', value: 5 },
{ label: '调试(传动、打点、精度)', value: 6 },
{ label: '验收资料', value: 7 },
{ label: '验收报告/差旅记录', value: 8 },
{ label: '项目结项', value: 9 }
],
taskTypeIndex: 0,
projectList: [],
projectIndex: -1,
userList: [],
selectedUsers: [],
// userPopupVisible: false, // 不再需要
fileList: [],
uploadedFiles: []
}
},
onLoad() {
this.initData()
},
methods: {
// 初始化数据
async initData() {
await Promise.all([
this.getProjectList(),
this.getUserList()
])
},
// 获取项目列表
async getProjectList() {
try {
const res = await listProject({
pageNum: 1,
pageSize: 999
})
this.projectList = res.rows || []
} catch (error) {
console.error('获取项目列表失败:', error)
uni.showToast({
title: '获取项目列表失败',
icon: 'none'
})
}
},
// 获取用户列表
async getUserList() {
try {
// const res = await selectUser({ pageNum: 1, pageSize: 999, deptId: 100 })
const resr = await deptTreeSelect()
// this.userList = res.rows || []
} catch (error) {
console.error('获取用户列表失败:', error)
uni.showToast({
title: '获取用户列表失败',
icon: 'none'
})
}
},
// 工作类型选择
onTaskTypeChange(e) {
this.taskTypeIndex = e.detail.value
this.form.taskType = this.taskTypeOptions[this.taskTypeIndex].value
},
// 开始时间选择
onBeginTimeChange(e) {
this.form.beginTime = e.detail.value + ' 00:00:00'
},
// 结束时间选择
onFinishTimeChange(e) {
this.form.finishTime = e.detail.value + ' 23:59:59'
},
// 项目选择
onProjectChange(e) {
this.projectIndex = e.detail.value
this.form.projectId = this.projectList[this.projectIndex].projectId
},
// 移除项目
removeProject() {
this.projectIndex = -1
this.form.projectId = ''
},
// 打开用户选择器
openUserPopup() {
this.$refs.userPopup.open()
},
// 关闭用户选择器
closeUserPopup() {
this.$refs.userPopup.close()
},
// 勾选/取消用户
toggleUser(user) {
const index = this.selectedUsers.findIndex(u => u.userId === user.userId)
if (index > -1) {
this.selectedUsers.splice(index, 1)
} else {
this.selectedUsers.push(user)
}
},
// 判断用户是否已选
isSelected(user) {
return this.selectedUsers.some(u => u.userId === user.userId)
},
// 移除已选用户
removeUser(user) {
const index = this.selectedUsers.findIndex(u => u.userId === user.userId)
if (index > -1) {
this.selectedUsers.splice(index, 1)
}
},
// 确认选择
confirmUserSelect() {
this.form.workerIds = this.selectedUsers.map(u => u.userId).join(',')
this.closeUserPopup()
},
// 选择文件
chooseFile() {
uni.chooseFile({
count: 5,
type: 'all',
success: (res) => {
this.fileList = [...this.fileList, ...res.tempFiles]
}
})
},
// 移除文件
removeFile(index) {
this.fileList.splice(index, 1)
},
// 格式化文件大小
formatFileSize(size) {
if (size < 1024) {
return size + 'B'
} else if (size < 1024 * 1024) {
return (size / 1024).toFixed(2) + 'KB'
} else {
return (size / (1024 * 1024)).toFixed(2) + 'MB'
}
},
// 上传文件
async uploadFiles() {
const uploadPromises = this.fileList.map(async (file) => {
try {
const result = await uploadImage(file.path)
return {
originalName: file.name,
url: result.url,
ossId: result.ossId
}
} catch (error) {
console.error('文件上传失败:', error)
throw error
}
})
return Promise.all(uploadPromises)
},
// 表单验证
validateForm() {
if (!this.form.taskTitle.trim()) {
uni.showToast({
title: '请输入任务主题',
icon: 'none'
})
return false
}
if (!this.form.taskType) {
uni.showToast({
title: '请选择工作类型',
icon: 'none'
})
return false
}
if (!this.form.beginTime) {
uni.showToast({
title: '请选择开始时间',
icon: 'none'
})
return false
}
if (this.form.status === 0 && !this.form.finishTime) {
uni.showToast({
title: '请选择结束时间',
icon: 'none'
})
return false
}
if (this.form.workerIds === '') {
uni.showToast({
title: '请选择执行人',
icon: 'none'
})
return false
}
return true
},
// 提交表单
async submitForm() {
if (!this.validateForm()) {
return
}
this.loading = true
try {
// 上传文件
if (this.fileList.length > 0) {
const uploadedFiles = await this.uploadFiles()
this.form.accessory = uploadedFiles.map(file => file.ossId).join(',')
}
// 提交任务
await addTask(this.form)
uni.showToast({
title: '任务创建成功',
icon: 'success'
})
// 返回上一页
setTimeout(() => {
uni.navigateBack()
}, 1500)
} catch (error) {
console.error('创建任务失败:', error)
uni.showToast({
title: '创建任务失败',
icon: 'none'
})
} finally {
this.loading = false
}
}
}
}
</script>
<style lang="scss" scoped>
.create-task-container {
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
background-color: #f5f5f5;
padding-bottom: 120rpx;
}
.form-container {
max-width: 90vw;
margin: 0 auto;
box-sizing: border-box;
padding: 30rpx 20rpx;
}
.form-item {
border-radius: 12rpx;
box-sizing: border-box;
padding: 30rpx;
margin-bottom: 20rpx;
}
.form-label {
display: flex;
align-items: center;
margin-bottom: 20rpx;
.label-text {
font-size: 28rpx;
color: #333;
font-weight: 500;
}
.required {
color: #ff4757;
margin-left: 8rpx;
}
}
.form-input {
width: 100%;
height: 80rpx;
border: 1rpx solid #ddd;
border-radius: 8rpx;
padding: 0 20rpx;
font-size: 28rpx;
background-color: #fff;
}
.form-picker {
width: 100%;
height: 80rpx;
border: 1rpx solid #ddd;
border-radius: 8rpx;
display: flex;
align-items: center;
padding: 0 20rpx;
background-color: #fff;
.picker-text {
font-size: 28rpx;
color: #333;
}
}
.radio-group {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
}
.radio-item {
display: flex;
align-items: center;
padding: 20rpx 30rpx;
border: 1rpx solid #ddd;
border-radius: 8rpx;
background-color: #fff;
&.active {
border-color: #007aff;
background-color: #f0f8ff;
.radio-circle {
background-color: #007aff;
&::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 12rpx;
height: 12rpx;
background-color: #fff;
border-radius: 50%;
}
}
}
.radio-circle {
width: 32rpx;
height: 32rpx;
border: 2rpx solid #ddd;
border-radius: 50%;
margin-right: 16rpx;
position: relative;
}
.radio-text {
font-size: 28rpx;
color: #333;
}
}
.user-selector {
min-height: 80rpx;
border: 1rpx solid #ddd;
border-radius: 8rpx;
padding: 20rpx;
background-color: #fff;
.selected-users {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.user-tag {
display: flex;
align-items: center;
background-color: #f0f8ff;
border: 1rpx solid #007aff;
border-radius: 20rpx;
padding: 8rpx 16rpx;
.user-name {
font-size: 24rpx;
color: #007aff;
margin-right: 8rpx;
}
.remove-btn {
font-size: 24rpx;
color: #007aff;
font-weight: bold;
}
}
.placeholder {
font-size: 28rpx;
color: #999;
}
}
.form-textarea {
width: 100%;
min-height: 200rpx;
border: 1rpx solid #ddd;
border-radius: 8rpx;
padding: 20rpx;
font-size: 28rpx;
background-color: #fff;
line-height: 1.5;
}
.file-upload {
.upload-btn {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 160rpx;
border: 2rpx dashed #ddd;
border-radius: 8rpx;
background-color: #fafafa;
.upload-icon {
font-size: 48rpx;
color: #999;
margin-bottom: 16rpx;
}
.upload-text {
font-size: 28rpx;
color: #999;
}
}
.file-list {
margin-top: 20rpx;
}
.file-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx;
background-color: #f8f9fa;
border-radius: 8rpx;
margin-bottom: 16rpx;
.file-name {
font-size: 28rpx;
color: #333;
flex: 1;
margin-right: 16rpx;
}
.file-size {
font-size: 24rpx;
color: #999;
margin-right: 16rpx;
}
.remove-file {
font-size: 32rpx;
color: #ff4757;
font-weight: bold;
}
}
}
.submit-container {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #fff;
padding: 30rpx;
border-top: 1rpx solid #eee;
.submit-btn {
width: 100%;
height: 88rpx;
background-color: #007aff;
color: #fff;
border-radius: 8rpx;
font-size: 32rpx;
font-weight: 500;
}
}
.user-popup {
background-color: #fff;
border-radius: 20rpx 20rpx 0 0;
max-height: 80vh;
display: flex;
flex-direction: column;
.popup-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx;
border-bottom: 1rpx solid #eee;
.popup-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.close-btn {
font-size: 40rpx;
color: #999;
font-weight: bold;
}
}
.user-list {
flex: 1;
overflow-y: auto;
padding: 0 30rpx;
}
.user-item {
display: flex;
align-items: center;
padding: 30rpx 0;
border-bottom: 1rpx solid #f5f5f5;
.user-avatar {
margin-right: 20rpx;
.avatar-img {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
}
}
.user-info {
flex: 1;
.user-nickname {
font-size: 28rpx;
color: #333;
font-weight: 500;
display: block;
}
.user-dept {
font-size: 24rpx;
color: #999;
margin-top: 8rpx;
display: block;
}
}
.user-check {
width: 40rpx;
height: 40rpx;
border: 2rpx solid #007aff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background-color: #007aff;
.check-icon {
color: #fff;
font-size: 24rpx;
font-weight: bold;
}
}
}
.popup-footer {
padding: 30rpx;
border-top: 1rpx solid #eee;
.confirm-btn {
width: 100%;
height: 88rpx;
background-color: #007aff;
color: #fff;
border-radius: 8rpx;
font-size: 32rpx;
font-weight: 500;
}
}
}
.user-tags {
display: flex;
flex-wrap: wrap;
margin-bottom: 10rpx;
}
.user-tag {
background: #e6f7ff;
color: #007aff;
border-radius: 20rpx;
padding: 6rpx 16rpx;
margin-right: 10rpx;
margin-bottom: 10rpx;
display: flex;
align-items: center;
}
.remove-btn {
margin-left: 8rpx;
color: #ff4757;
font-weight: bold;
cursor: pointer;
}
.user-select-popup {
width: 90vw;
max-width: 700rpx;
background: #fff;
border-radius: 16rpx;
overflow: hidden;
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx;
border-bottom: 1rpx solid #eee;
font-size: 32rpx;
font-weight: bold;
}
.close-btn {
font-size: 40rpx;
color: #999;
font-weight: bold;
}
.popup-body {
display: flex;
height: 400rpx;
}
.dept-tree {
width: 180rpx;
border-right: 1rpx solid #eee;
background: #f8f8f8;
padding: 10rpx 0;
}
.dept-item {
padding: 16rpx 24rpx;
cursor: pointer;
}
.dept-item.active {
background: #e6f7ff;
color: #007aff;
}
.dept-children {
margin-left: 10rpx;
}
.user-list {
flex: 1;
padding: 10rpx 0 10rpx 10rpx;
overflow-y: auto;
}
.user-row {
display: flex;
align-items: center;
padding: 10rpx 0;
border-bottom: 1rpx solid #f5f5f5;
}
.user-row .user-dept {
margin-left: 16rpx;
color: #999;
font-size: 24rpx;
}
.popup-footer {
padding: 20rpx;
text-align: right;
}
</style>