| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160 |
- <script setup lang="ts">
- import { computed, ref, watch } from 'vue'
- import FormField from './FormField.vue'
- import UploadPanel from './UploadPanel.vue'
- import type { AssignableUser } from '@/services/workorders'
- export interface WorkOrderActionPayload {
- content?: string
- files: string[]
- handlerId?: number
- verifyUser?: number
- remarks?: string
- planFinishTime?: string
- delayedDeadline?: string
- delayedReason?: string
- passed?: boolean
- approved?: boolean
- }
- const props = withDefaults(defineProps<{ modelValue: boolean; action: string; domain: 'workorder' | 'warning'; handlerOptions?: AssignableUser[]; reviewerOptions?: AssignableUser[]; defaultReviewerId?: number; submitting?: boolean }>(), { handlerOptions: () => [], reviewerOptions: () => [], submitting: false })
- const emit = defineEmits<{ 'update:modelValue': [value: boolean]; submitted: [action: string, payload: WorkOrderActionPayload] }>()
- const text = ref('')
- const remark = ref('')
- const dateTime = ref('')
- const person = ref('')
- const reviewer = ref('')
- const handlerId = ref<number>()
- const reviewerId = ref<number>()
- const files = ref<string[]>([])
- const result = ref('通过')
- const disposer = ref('张伟')
- const checker = ref('赵敏')
- watch(() => props.action, reset)
- watch(() => props.modelValue, value => { if (value) reset() })
- function reset() {
- text.value = ''
- remark.value = ''
- dateTime.value = ''
- person.value = ''
- reviewer.value = ''
- handlerId.value = undefined
- reviewerId.value = undefined
- files.value = []
- result.value = '通过'
- if (props.action === '派单' && props.defaultReviewerId) {
- const currentUser = props.reviewerOptions.find(user => user.userId === props.defaultReviewerId)
- if (currentUser) {
- reviewerId.value = currentUser.userId
- reviewer.value = userLabel(currentUser)
- }
- }
- }
- const title = computed(() => {
- const map: Record<string, string> = { '取消工单':'取消工单','派单':'工单派单','核查接单':'核查接单','开始处理':props.domain === 'warning' ? '开始处置' : '工单处置','工单处置':'工单处置','申请延期':'申请延期','提交验收':'提交验收','验收审核':'工单验收','延期审核':'延期审核','发布':'发布预警','接收':'接收预警','提交处置':'提交处置','核查':'核查' }
- return map[props.action] || props.action
- })
- const needsUpload = computed(() => ['取消工单','核查接单','开始处理','工单处置','申请延期','提交验收','验收审核','提交处置','核查'].includes(props.action))
- function close() { emit('update:modelValue', false) }
- function userLabel(user: AssignableUser) { return user.deptName ? `${user.nickName}(${user.deptName})` : user.nickName }
- function pickUser(kind: 'handler' | 'reviewer') {
- const options = kind === 'handler' ? props.handlerOptions : props.reviewerOptions
- if (!options.length) { uni.showToast({ title: '暂无可选人员', icon: 'none' }); return }
- uni.showActionSheet({ itemList: options.map(userLabel), success:r => {
- const selected = options[r.tapIndex]
- if (kind === 'handler') { handlerId.value = selected.userId; person.value = userLabel(selected) }
- else { reviewerId.value = selected.userId; reviewer.value = userLabel(selected) }
- } })
- }
- function invalid(message: string) { uni.showToast({ title: message, icon: 'none' }); return false }
- function isFutureDateTime(value: string) {
- const timestamp = Date.parse(value.replace(' ', 'T'))
- return Number.isFinite(timestamp) && timestamp > Date.now()
- }
- function submit() {
- const content = text.value.trim()
- if (props.domain === 'workorder') {
- if (props.action === '派单' && (!handlerId.value || !reviewerId.value)) return invalid('请选择处置人和核验人')
- if (props.action === '派单' && dateTime.value && !isFutureDateTime(dateTime.value)) return invalid('计划完成时间必须晚于当前时间')
- if (['核查接单','工单处置','开始处理','取消工单','提交验收'].includes(props.action) && !content) return invalid('请填写操作内容')
- if (['核查接单','提交验收'].includes(props.action) && !files.value.length) return invalid('请上传现场佐证材料')
- if (props.action === '申请延期' && (!dateTime.value.trim() || !content)) return invalid('请填写延期时间和原因')
- if (props.action === '申请延期' && !isFutureDateTime(dateTime.value)) return invalid('延期截止时间必须晚于当前时间')
- }
- const payload: WorkOrderActionPayload = {
- content,
- files: files.value,
- handlerId: handlerId.value,
- verifyUser: reviewerId.value,
- remarks: remark.value.trim(),
- planFinishTime: dateTime.value.trim() || undefined,
- delayedDeadline: dateTime.value.trim() || undefined,
- delayedReason: content,
- passed: result.value === '通过',
- approved: result.value === '通过',
- }
- emit('submitted', props.action, payload)
- if (props.domain === 'warning') close()
- }
- </script>
- <template>
- <view v-if="modelValue" class="drawer-root">
- <view class="mask" @tap="close"></view>
- <view class="drawer">
- <view class="drawer-head"><text>{{ title }}</text><text class="close" @tap="close">×</text></view>
- <scroll-view class="drawer-body" scroll-y>
- <template v-if="action === '派单'">
- <FormField v-model="person" label="处置人" type="select" required @select="pickUser('handler')" />
- <FormField v-model="reviewer" label="核验人" type="select" required @select="pickUser('reviewer')" />
- <FormField v-model="remark" label="派单备注" type="textarea" placeholder="可填写故障要点、现场注意事项等派单说明" />
- <FormField v-model="dateTime" label="计划完成时间" type="datetime" future-only placeholder="请选择" />
- </template>
- <template v-else-if="action === '核查接单'">
- <FormField v-model="text" label="核查结论" type="textarea" required placeholder="请如实填写现场核查结论,注明故障是否属实、现场现状等内容" />
- </template>
- <template v-else-if="action === '取消工单'">
- <FormField v-model="text" label="取消原因" type="textarea" required placeholder="请详细说明工单取消事由" />
- </template>
- <template v-else-if="action === '申请延期'">
- <FormField v-model="dateTime" label="延期截止时间" type="datetime" future-only required placeholder="请选择延期截止时间" />
- <FormField v-model="text" label="延期原因" type="textarea" required placeholder="请填写延期原因" />
- </template>
- <template v-else-if="action === '提交验收'">
- <FormField v-model="text" label="验收说明" type="textarea" required placeholder="请填报维修处置完成情况" />
- </template>
- <template v-else-if="action === '验收审核' || action === '延期审核'">
- <view class="choice-field"><text class="field-label"><text class="required-mark">*</text>{{ action === '验收审核' ? '验收结果' : '审核结果' }}</text><view class="choice-row" :class="{ 'audit-compact': action === '延期审核' }"><button :class="{active:result==='通过'}" @tap="result='通过'">{{ action === '验收审核' ? '验收通过(结案)' : '审核通过' }}</button><button :class="{active:result==='驳回'}" @tap="result='驳回'">{{ action === '验收审核' ? '验收驳回' : '审核驳回' }}</button></view></view>
- <FormField v-model="text" :label="action === '验收审核' ? '验收意见' : '审核意见'" type="textarea" placeholder="请输入意见" />
- </template>
- <template v-else-if="action === '发布'">
- <view class="choice-field"><text class="field-label"><b>*</b>处置人</text><view class="choice-row chips"><button v-for="name in ['张伟','李强','王芳']" :key="name" :class="{active:disposer===name}" @tap="disposer=name">{{ name }}</button></view></view>
- <view class="choice-field"><text class="field-label"><b>*</b>核查人</text><view class="choice-row chips"><button v-for="name in ['赵敏','刘洋','陈静']" :key="name" :class="{active:checker===name}" @tap="checker=name">{{ name }}</button></view></view>
- </template>
- <template v-else-if="action === '接收'">
- <view class="confirm-box"><text>确认接收该预警?</text><text>接收后预警将进入处置流程,并记录当前操作人及接收时间。</text></view>
- </template>
- <template v-else-if="action === '核查'">
- <view class="choice-field"><text class="field-label"><b>*</b>核查结果</text><view class="choice-row"><button :class="{active:result==='通过'}" @tap="result='通过'">已解除</button><button :class="{active:result==='驳回'}" @tap="result='驳回'">驳回继续处置</button></view></view>
- <FormField v-model="text" label="备注" type="textarea" placeholder="请输入核查意见" />
- </template>
- <template v-else>
- <FormField v-model="text" label="处置内容" type="textarea" :required="domain === 'workorder'" :placeholder="action === '提交处置' ? '请输入处置结论' : '请记录故障原因、处置方案、实施措施及处理结果'" />
- </template>
- <view v-if="needsUpload" class="upload-field"><text class="field-label"><text v-if="['核查接单','提交验收'].includes(action)" class="required-mark">*</text>{{ ['核查接单','提交验收'].includes(action) ? '现场佐证材料' : '上传附件(可选)' }}</text><UploadPanel v-model="files" :max-size-mb="10" /><text class="upload-tip">支持 png / jpg / jpeg,单个文件不超过 10MB</text></view>
- </scroll-view>
- <view class="drawer-actions"><button class="cancel" :disabled="submitting" @tap="close">取消</button><button class="submit" :disabled="submitting" @tap="submit">{{ submitting ? '正在提交...' : action === '派单' ? '确认派单' : action === '发布' ? '确认发布' : action === '接收' ? '确认接收' : '确认提交' }}</button></view>
- </view>
- </view>
- </template>
- <style scoped lang="scss">
- .drawer-root{position:fixed;inset:0;z-index:100}.mask{position:absolute;inset:0;background:rgba(5,18,35,.55);animation:drawer-mask-in .24s ease-out both}.drawer{position:absolute;right:0;bottom:0;left:0;overflow:hidden;border-radius:32rpx 32rpx 0 0;background:#fff;box-shadow:0 -12rpx 40rpx rgba(4,20,38,.2);animation:drawer-slide-in .28s cubic-bezier(.22,.78,.28,1) both}.drawer-head{display:flex;align-items:center;justify-content:space-between;height:96rpx;padding:0 30rpx;color:#122b48;border-bottom:1rpx solid #edf1f5;font-size:31rpx;font-weight:700}.close{display:flex;align-items:center;justify-content:center;width:58rpx;height:58rpx;color:#7f8c9b;border-radius:50%;background:#f2f5f8;font-size:42rpx;font-weight:300}.drawer-body{max-height:62vh;padding:5rpx 30rpx 25rpx}.choice-field,.upload-field{padding:23rpx 0;border-bottom:1rpx solid #edf1f5}.field-label{display:block;margin-bottom:16rpx;color:#34485f;font-size:27rpx}.required-mark{margin-right:6rpx;color:#e34d59}.choice-row{display:grid;grid-template-columns:1fr 1fr;gap:14rpx}.choice-row.chips{grid-template-columns:repeat(3,1fr)}.choice-row button{min-height:70rpx;margin:0;padding:8rpx 12rpx;color:#52647a;border:1rpx solid #d9e1ea;border-radius:999rpx;background:#fff;font-size:23rpx;line-height:1.35}.choice-row button.active{color:#08264c;border-color:#08264c;background:#eef3f8;font-weight:600}.upload-tip{display:block;margin-top:14rpx;color:#97a3b1;font-size:21rpx}.confirm-box{display:flex;flex-direction:column;margin:25rpx 0;padding:26rpx;color:#2e435b;border-radius:16rpx;background:#f3f7fb;font-size:29rpx;font-weight:650}.confirm-box text:last-child{margin-top:12rpx;color:#7d8c9e;font-size:23rpx;font-weight:400;line-height:1.6}.drawer-actions{display:grid;grid-template-columns:1fr 1fr;gap:18rpx;padding:18rpx 26rpx calc(18rpx + env(safe-area-inset-bottom));border-top:1rpx solid #edf1f5;background:#fff}.drawer-actions button{height:80rpx;margin:0;border-radius:999rpx;font-size:27rpx;line-height:80rpx}.cancel{color:#43566d;border:1rpx solid #d7e0e9;background:#fff}.submit{color:#fff;background:#08264c}@keyframes drawer-slide-in{from{transform:translateY(105%)}to{transform:translateY(0)}}@keyframes drawer-mask-in{from{opacity:0}to{opacity:1}}
- .audit-compact{display:flex;align-items:center;gap:14rpx}.audit-compact button{display:flex;flex:0 0 180rpx;align-items:center;justify-content:center;width:180rpx;height:62rpx;min-height:62rpx;padding:0 16rpx;border-radius:12rpx;line-height:1}.field-label b{margin-right:6rpx;color:#e34d59}
- </style>
|