工单详情

This commit is contained in:
2025-12-24 18:22:13 +08:00
parent 6109bc2505
commit a613eb4fa1
15 changed files with 1790 additions and 739 deletions

View File

@@ -1,248 +0,0 @@
.workcase-creator-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 999;
display: flex;
flex-direction: column;
justify-content: flex-end;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.workcase-creator {
max-height: 85vh;
background-color: #FFFFFF;
border-top-left-radius: 16px;
border-top-right-radius: 16px;
display: flex;
flex-direction: column;
animation: slideUp 0.3s ease;
}
@keyframes slideUp {
from {
transform: translateY(100%);
}
to {
transform: translateY(0);
}
}
.creator-header {
background-color: #FFFFFF;
padding: 20px 16px 16px;
border-bottom: 1px solid #F0F0F0;
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
}
.creator-header::before {
content: '';
position: absolute;
top: 8px;
left: 50%;
transform: translateX(-50%);
width: 36px;
height: 4px;
background-color: #E0E0E0;
border-radius: 2px;
}
.header-title {
color: #1F2329;
font-size: 18px;
font-weight: 600;
}
.close-btn {
width: 28px;
height: 28px;
border-radius: 14px;
background-color: #F5F5F5;
display: flex;
align-items: center;
justify-content: center;
}
.close-icon {
color: #8F959E;
font-size: 20px;
line-height: 1;
}
.creator-content {
flex: 1;
background-color: #FFFFFF;
padding: 16px;
}
.form-item {
margin-bottom: 24px;
}
.label {
display: block;
color: #333333;
font-size: 16px;
font-weight: 500;
margin-bottom: 8px;
}
.input, .textarea {
width: 100%;
padding: 12px 16px;
border: 1px solid #E0E0E0;
border-radius: 8px;
font-size: 16px;
background-color: #FAFAFA;
}
.textarea {
min-height: 100px;
resize: none;
}
.char-count {
color: #999999;
font-size: 12px;
text-align: right;
margin-top: 4px;
}
.picker {
width: 100%;
}
.picker-content {
padding: 12px 16px;
border: 1px solid #E0E0E0;
border-radius: 8px;
background-color: #FAFAFA;
display: flex;
justify-content: space-between;
align-items: center;
}
.picker-text {
color: #333333;
font-size: 16px;
}
.picker-arrow {
color: #999999;
font-size: 18px;
}
.upload-area {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.upload-item {
position: relative;
width: 80px;
height: 80px;
}
.upload-image {
width: 100%;
height: 100%;
border-radius: 8px;
}
.delete-btn {
position: absolute;
top: -6px;
right: -6px;
width: 20px;
height: 20px;
border-radius: 10px;
background-color: #FF5722;
display: flex;
align-items: center;
justify-content: center;
}
.delete-icon {
color: #FFFFFF;
font-size: 14px;
line-height: 1;
}
.upload-btn {
width: 80px;
height: 80px;
border: 1px dashed #CCCCCC;
border-radius: 8px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #FAFAFA;
}
.upload-plus {
color: #999999;
font-size: 24px;
line-height: 1;
}
.upload-text {
color: #999999;
font-size: 12px;
margin-top: 4px;
}
.upload-tip {
color: #999999;
font-size: 12px;
margin-top: 8px;
}
.creator-footer {
background-color: #FFFFFF;
padding: 16px;
padding-bottom: calc(16px + env(safe-area-inset-bottom));
border-top: 1px solid #F0F0F0;
display: flex;
gap: 12px;
}
.cancel-btn, .submit-btn {
flex: 1;
height: 44px;
border-radius: 22px;
font-size: 16px;
font-weight: 500;
border: none;
}
.cancel-btn {
background-color: #F5F5F5;
color: #666666;
}
.submit-btn {
background-color: #5B8FF9;
color: #FFFFFF;
}
.submit-btn.is-disabled {
background-color: #CCCCCC;
color: #999999;
}

View File

@@ -1,206 +0,0 @@
<template>
<view class="workcase-creator-mask" @tap="onClose" v-if="show">
<view class="workcase-creator" @tap.stop>
<view class="creator-header">
<text class="header-title">创建工单</text>
</view>
<scroll-view class="creator-content" scroll-y="true">
<view class="form-item">
<text class="label">工单标题</text>
<input class="input" v-model="form.title" placeholder="请输入工单标题" maxlength="50" />
</view>
<view class="form-item">
<text class="label">问题分类</text>
<picker class="picker" :value="categoryIndex" :range="categories" @change="onCategoryChange">
<view class="picker-content">
<text class="picker-text">{{categories[categoryIndex]}}</text>
<text class="picker-arrow">></text>
</view>
</picker>
</view>
<view class="form-item">
<text class="label">紧急程度</text>
<picker class="picker" :value="priorityIndex" :range="priorities" @change="onPriorityChange">
<view class="picker-content">
<text class="picker-text">{{priorities[priorityIndex]}}</text>
<text class="picker-arrow">></text>
</view>
</picker>
</view>
<view class="form-item">
<text class="label">问题描述</text>
<textarea class="textarea" v-model="form.description" placeholder="请详细描述遇到的问题..." maxlength="500" />
<text class="char-count">{{form.description.length}}/500</text>
</view>
<view class="form-item">
<text class="label">联系方式</text>
<input class="input" v-model="form.contact" placeholder="请输入您的联系方式" />
</view>
<view class="form-item">
<text class="label">上传图片</text>
<view class="upload-area">
<view class="upload-item" v-for="(item, index) in form.images" :key="index">
<image class="upload-image" :src="item" mode="aspectFill" />
<view class="delete-btn" @tap="deleteImage(index)">
<text class="delete-icon">×</text>
</view>
</view>
<view class="upload-btn" v-if="form.images.length < 3" @tap="chooseImage">
<text class="upload-plus">+</text>
<text class="upload-text">添加图片</text>
</view>
</view>
<text class="upload-tip">最多上传3张图片</text>
</view>
</scroll-view>
<view class="creator-footer">
<button class="cancel-btn" @tap="onCancel">取消</button>
<button class="submit-btn" :class="{ 'is-disabled': !canSubmit }" @tap="onSubmit" :disabled="!canSubmit">提交工单</button>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed, defineProps, defineEmits } from 'vue'
// 接口定义
interface WorkcaseForm {
title : string
description : string
contact : string
images : string[]
}
interface WorkcaseData extends WorkcaseForm {
category : string
priority : string
}
// Props 定义
interface Props {
show ?: boolean
}
const props = withDefaults(defineProps<Props>(), {
show: false
})
// Emits 定义
const emits = defineEmits<{
close : []
success : [data: WorkcaseData]
}>()
// 响应式数据
const form = ref<WorkcaseForm>({
title: '',
description: '',
contact: '',
images: []
})
const categories = ref<string[]>(['设施报修', '环境卫生', '交通问题', '安全隐患', '其他问题'])
const categoryIndex = ref<number>(0)
const priorities = ref<string[]>(['一般', '紧急', '非常紧急'])
const priorityIndex = ref<number>(0)
// 计算属性
const canSubmit = computed(() => {
return form.value.title.trim() &&
form.value.description.trim() &&
form.value.contact.trim()
})
// 方法定义
function onClose() {
emits('close')
}
function onCancel() {
resetForm()
emits('close')
}
function onCategoryChange(e : any) {
categoryIndex.value = e.detail.value
}
function onPriorityChange(e : any) {
priorityIndex.value = e.detail.value
}
function chooseImage() {
uni.chooseImage({
count: 3 - form.value.images.length,
sizeType: ['compressed'],
sourceType: ['camera', 'album'],
success: (res) => {
form.value.images.push(...res.tempFilePaths)
},
fail: (err) => {
console.log('选择图片失败:', err)
}
})
}
function deleteImage(index : number) {
form.value.images.splice(index, 1)
}
function onSubmit() {
if (!canSubmit.value) {
uni.showToast({
title: '请完善必填信息',
icon: 'none'
})
return
}
const workcaseData : WorkcaseData = {
title: form.value.title.trim(),
category: categories.value[categoryIndex.value],
priority: priorities.value[priorityIndex.value],
description: form.value.description.trim(),
contact: form.value.contact.trim(),
images: form.value.images
}
uni.showLoading({
title: '提交中...'
})
// 模拟提交
setTimeout(() => {
uni.hideLoading()
uni.showToast({
title: '工单提交成功',
icon: 'success'
})
resetForm()
emits('success', workcaseData)
}, 1500)
}
function resetForm() {
form.value = {
title: '',
description: '',
contact: '',
images: []
}
categoryIndex.value = 0
priorityIndex.value = 0
}
</script>
<style lang="scss" scoped>
@import './WorkcaseCreator.scss';
</style>

View File

@@ -98,9 +98,6 @@
</view>
</view>
<!-- 工单创建弹窗 -->
<WorkcaseCreator v-if="showWorkcaseCreator" :show="showWorkcaseCreator"
@close="hideCreator" @success="onWorkcaseCreated" />
</view>
<!-- #ifdef APP -->
</scroll-view>
@@ -109,7 +106,6 @@
<script setup lang="ts">
import { ref, reactive, computed, nextTick, onMounted, onUnmounted, watch } from 'vue'
import WorkcaseCreator from '@/components/WorkcaseCreator/WorkcaseCreator.uvue'
import type { ChatRoomMessageVO, CustomerVO, ChatMemberVO, TbChatRoomMessageDTO } from '@/types/workcase'
import { workcaseChatAPI } from '@/api/workcase'
import { wsClient } from '@/utils/websocket'
@@ -122,7 +118,6 @@ const workcaseId = ref<string>('')
const roomName = ref<string>('聊天室')
const inputText = ref<string>('')
const scrollTop = ref<number>(0)
const showWorkcaseCreator = ref<boolean>(false)
const loading = ref<boolean>(false)
const sending = ref<boolean>(false)
const loadingMore = ref<boolean>(false)
@@ -204,24 +199,23 @@ const totalMembers = computed<MemberDisplay[]>(() => {
// 生命周期
onMounted(() => {
uni.getSystemInfo({
success: (res) => {
// #ifdef MP-WEIXIN
try {
const menuButtonInfo = uni.getMenuButtonBoundingClientRect()
headerPaddingTop.value = menuButtonInfo.top
headerTotalHeight.value = menuButtonInfo.bottom + 8
} catch (e) {
headerPaddingTop.value = res.statusBarHeight || 44
headerTotalHeight.value = (res.statusBarHeight || 44) + 44
}
// #endif
// #ifndef MP-WEIXIN
headerPaddingTop.value = res.statusBarHeight || 44
headerTotalHeight.value = (res.statusBarHeight || 44) + 44
// #endif
}
})
const windowInfo = uni.getWindowInfo()
const statusBarHeight = windowInfo.statusBarHeight || 44
// #ifdef MP-WEIXIN
try {
const menuButtonInfo = uni.getMenuButtonBoundingClientRect()
headerPaddingTop.value = menuButtonInfo.top
headerTotalHeight.value = menuButtonInfo.bottom + 8
} catch (e) {
headerPaddingTop.value = statusBarHeight
headerTotalHeight.value = statusBarHeight + 44
}
// #endif
// #ifndef MP-WEIXIN
headerPaddingTop.value = statusBarHeight
headerTotalHeight.value = statusBarHeight + 44
// #endif
// 获取页面参数
const pages = getCurrentPages()
@@ -448,30 +442,38 @@ function scrollToBottom() {
// 处理工单操作
function handleWorkcaseAction() {
console.log('[handleWorkcaseAction] 开始执行')
console.log('[handleWorkcaseAction] workcaseId:', workcaseId.value)
console.log('[handleWorkcaseAction] roomId:', roomId.value)
if (workcaseId.value) {
const url = `/pages/workcase/workcaseDetail/workcaseDetail?workcaseId=${workcaseId.value}`
console.log('[handleWorkcaseAction] 查看工单跳转URL:', url)
uni.navigateTo({
url: `/pages/workcase/workcaseDetail/workcaseDetail?workcaseId=${workcaseId.value}`
url: url,
success: () => {
console.log('[handleWorkcaseAction] 跳转成功')
},
fail: (err) => {
console.error('[handleWorkcaseAction] 跳转失败:', err)
}
})
} else {
showWorkcaseCreator.value = true
// 跳转到创建工单页面
const url = `/pages/workcase/workcaseDetail/workcaseDetail?mode=create&roomId=${roomId.value}`
console.log('[handleWorkcaseAction] 创建工单跳转URL:', url)
uni.navigateTo({
url: url,
success: () => {
console.log('[handleWorkcaseAction] 跳转成功')
},
fail: (err) => {
console.error('[handleWorkcaseAction] 跳转失败:', err)
}
})
}
}
// 隐藏工单创建器
function hideCreator() {
showWorkcaseCreator.value = false
}
// 工单创建成功
function onWorkcaseCreated(data: any) {
hideCreator()
workcaseId.value = data.workcaseId || 'new-workcase'
uni.showToast({
title: '工单创建成功',
icon: 'success'
})
}
// 发起会议
function startMeeting() {
uni.navigateTo({

View File

@@ -69,6 +69,9 @@ const chatRooms = ref<ChatRoomVO[]>([])
// 生命周期
onMounted(() => {
const windowInfo = uni.getWindowInfo()
const statusBarHeight = windowInfo.statusBarHeight || 20
// #ifdef MP-WEIXIN
try {
const menuButton = uni.getMenuButtonBoundingClientRect()
@@ -76,14 +79,12 @@ onMounted(() => {
capsuleHeight.value = menuButton.height
navHeight.value = menuButton.bottom + 8
} catch (e) {
const sysInfo = uni.getSystemInfoSync()
navPaddingTop.value = sysInfo.statusBarHeight || 20
navPaddingTop.value = statusBarHeight
navHeight.value = navPaddingTop.value + 44
}
// #endif
// #ifndef MP-WEIXIN
const sysInfo = uni.getSystemInfoSync()
navPaddingTop.value = sysInfo.statusBarHeight || 20
navPaddingTop.value = statusBarHeight
navHeight.value = navPaddingTop.value + 44
// #endif

View File

@@ -109,15 +109,12 @@
</view>
</view>
</view>
<!-- 工单创建弹窗 -->
<WorkcaseCreator v-if="showWorkcaseCreator" :show="showWorkcaseCreator" @close="hideCreator"
@success="onWorkcaseCreated" />
</view>
</template>
<script setup lang="ts">
import { ref, nextTick, onMounted } from 'vue'
import WorkcaseCreator from '@/components/WorkcaseCreator/WorkcaseCreator.uvue'
import { guestAPI, aiChatAPI, workcaseChatAPI } from '@/api'
import type { TbWorkcaseDTO } from '@/types'
import { AGENT_ID } from '@/config'
@@ -233,38 +230,22 @@
title: '智能助手'
})
// 获取系统信息和安全区域
uni.getSystemInfo({
success: (res) => {
console.log('系统信息:', res)
console.log('状态栏高度:', res.statusBarHeight)
statusBarHeight.value = res.statusBarHeight || 0
console.log('安全区域:', res.safeArea)
console.log('安全区域insets:', res.safeAreaInsets)
// #ifdef MP-WEIXIN
// 获取胶囊按钮信息仅小程序计算header位置
try {
const menuButtonInfo = uni.getMenuButtonBoundingClientRect()
console.log('胶囊按钮信息:', menuButtonInfo)
// 计算header的paddingTop和总高度
// paddingTop = 胶囊按钮的top值
// 总高度 = 胶囊按钮bottom值
headerPaddingTop.value = menuButtonInfo.top
headerTotalHeight.value = menuButtonInfo.bottom
console.log('header paddingTop:', headerPaddingTop.value)
console.log('header totalHeight:', headerTotalHeight.value)
} catch (e) {
console.log('获取胶囊按钮信息失败:', e)
// 使用默认值
headerPaddingTop.value = 44
headerTotalHeight.value = 76
}
// #endif
}
})
// 获取窗口信息
const windowInfo = uni.getWindowInfo()
statusBarHeight.value = windowInfo.statusBarHeight || 0
// #ifdef MP-WEIXIN
// 获取胶囊按钮信息仅小程序计算header位置
try {
const menuButtonInfo = uni.getMenuButtonBoundingClientRect()
headerPaddingTop.value = menuButtonInfo.top
headerTotalHeight.value = menuButtonInfo.bottom
} catch (e) {
// 使用默认值
headerPaddingTop.value = 44
headerTotalHeight.value = 76
}
// #endif
})
// 发送消息

View File

@@ -1,6 +1,6 @@
.page {
min-height: 100vh;
background: #f4f5f7;
background: #f8fafc;
}
.nav {
@@ -17,6 +17,7 @@
padding-bottom: 16rpx;
box-sizing: border-box;
z-index: 100;
border-bottom: 1rpx solid #e5e7eb;
}
.nav-back {
@@ -50,11 +51,64 @@
}
.content {
margin-top: 176rpx;
padding: 20rpx 24rpx;
padding-bottom: 60rpx;
min-height: calc(100vh - 176rpx);
box-sizing: border-box;
padding: 24rpx;
padding-bottom: 140rpx;
}
// 工单头部卡片
.header-card {
background: #fff;
border-radius: 16rpx;
padding: 32rpx 24rpx;
margin-bottom: 24rpx;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.header-info {
flex: 1;
}
.workcase-id {
font-size: 40rpx;
font-weight: 700;
color: #4b87ff;
margin-bottom: 16rpx;
display: block;
}
.header-tags {
display: flex;
flex-direction: row;
gap: 16rpx;
}
.status-badge,
.urgency-badge {
padding: 8rpx 16rpx;
border-radius: 8rpx;
font-size: 24rpx;
font-weight: 500;
}
.badge-text {
font-size: 24rpx;
}
.header-action {
flex-shrink: 0;
margin-left: 24rpx;
}
.action-btn {
padding: 12rpx 24rpx;
background: #4b87ff;
color: #fff;
border-radius: 8rpx;
font-size: 26rpx;
font-weight: 500;
}
.section {
@@ -88,43 +142,102 @@
color: #222;
}
.info-card {
display: flex;
flex-direction: column;
// 表单容器
.form-container {
background: #fff;
border-radius: 16rpx;
overflow: hidden;
}
.info-item {
.form-item {
padding: 24rpx;
border-bottom: 1rpx solid #f3f4f6;
&:last-child {
border-bottom: none;
}
}
.form-label {
display: block;
font-size: 26rpx;
color: #6b7280;
margin-bottom: 16rpx;
font-weight: 500;
}
.form-input {
width: 100%;
height: 68rpx;
padding: 0 24rpx;
background: #f9fafb;
border: 1rpx solid #e5e7eb;
border-radius: 8rpx;
font-size: 28rpx;
color: #111827;
}
.form-textarea {
width: 100%;
min-height: 200rpx;
padding: 16rpx 24rpx;
background: #f9fafb;
border: 1rpx solid #e5e7eb;
border-radius: 8rpx;
font-size: 28rpx;
color: #111827;
line-height: 1.5;
}
.form-value {
display: block;
font-size: 28rpx;
color: #111827;
line-height: 1.5;
}
.form-picker {
width: 100%;
}
.picker-content {
display: flex;
flex-direction: row;
align-items: center;
padding: 16rpx 0;
justify-content: space-between;
height: 68rpx;
padding: 0 24rpx;
background: #f9fafb;
border: 1rpx solid #e5e7eb;
border-radius: 8rpx;
}
.info-item.column {
flex-direction: column;
align-items: flex-start;
}
.info-label {
width: 160rpx;
font-size: 26rpx;
color: #999;
flex-shrink: 0;
}
.info-value {
.picker-text {
flex: 1;
font-size: 26rpx;
color: #222;
text-align: right;
font-size: 28rpx;
color: #111827;
}
.info-desc {
font-size: 26rpx;
color: #222;
line-height: 1.6;
margin-top: 8rpx;
text-align: left;
.picker-arrow {
font-size: 32rpx;
color: #9ca3af;
margin-left: 16rpx;
}
// 铭牌照片
.nameplate-photo {
width: 100%;
max-width: 400rpx;
height: 300rpx;
border-radius: 12rpx;
overflow: hidden;
border: 1rpx solid #e5e7eb;
background: #f9fafb;
}
.nameplate-image {
width: 100%;
height: 100%;
}
.status-tag {
@@ -192,25 +305,88 @@
font-size: 24rpx;
}
.photo-list {
.section-title {
display: flex;
flex-direction: row;
align-items: center;
font-size: 30rpx;
font-weight: 600;
color: #222;
margin-bottom: 24rpx;
}
.title-icon {
font-size: 32rpx;
margin-right: 12rpx;
}
// 照片网格
.photos-grid {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 16rpx;
}
.photo-item {
width: 160rpx;
height: 160rpx;
position: relative;
width: 200rpx;
height: 200rpx;
border-radius: 12rpx;
overflow: hidden;
background: #f5f5f5;
border: 1rpx solid #e5e7eb;
}
.photo-item image {
.photo-image {
width: 100%;
height: 100%;
}
.photo-upload {
width: 200rpx;
height: 200rpx;
border: 2rpx dashed #d1d5db;
border-radius: 12rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #f9fafb;
}
.upload-plus {
font-size: 60rpx;
color: #9ca3af;
line-height: 1;
margin-bottom: 8rpx;
}
.upload-text {
font-size: 24rpx;
color: #9ca3af;
}
.upload-tip {
display: block;
font-size: 24rpx;
color: #9ca3af;
margin-top: 16rpx;
}
// 图片删除按钮
.photo-delete {
position: absolute;
top: 4rpx;
right: 4rpx;
width: 44rpx;
height: 44rpx;
background: rgba(0, 0, 0, 0.6);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.timeline {
position: relative;
}
@@ -227,63 +403,129 @@
}
.timeline-dot {
width: 16rpx;
height: 16rpx;
background: #d0d5dd;
width: 32rpx;
height: 32rpx;
border-radius: 50%;
margin-right: 20rpx;
border: 4rpx solid #fff;
box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
margin-right: 24rpx;
margin-top: 8rpx;
flex-shrink: 0;
}
.timeline-dot.active {
background: #173294;
.dot-system {
background: #60a5fa;
}
.dot-manager {
background: #fb923c;
}
.dot-engineer {
background: #34d399;
}
.timeline-line {
position: absolute;
left: 6rpx;
top: 28rpx;
left: 14rpx;
top: 44rpx;
width: 4rpx;
height: calc(100% - 20rpx);
background: #e5ebff;
height: calc(100% - 32rpx);
background: #f3f4f6;
}
.timeline-content {
.timeline-body {
flex: 1;
flex-direction: column;
background: #fff;
border: 1rpx solid #e5e7eb;
border-radius: 12rpx;
padding: 20rpx 24rpx;
}
.timeline-header {
display: flex;
flex-direction: row;
align-items: center;
gap: 12rpx;
margin-bottom: 8rpx;
}
.timeline-time {
font-size: 26rpx;
color: #173294;
font-weight: 600;
margin-right: 16rpx;
}
.timeline-date {
font-size: 24rpx;
color: #999;
}
.timeline-title {
display: block;
.timeline-actor {
font-size: 28rpx;
color: #222;
font-weight: 500;
margin-bottom: 6rpx;
font-weight: 700;
color: #111827;
}
.timeline-action {
font-size: 28rpx;
color: #6b7280;
}
.timeline-desc {
display: block;
font-size: 24rpx;
color: #666;
font-size: 26rpx;
color: #6b7280;
line-height: 1.5;
margin-bottom: 8rpx;
}
.timeline-time {
display: block;
font-size: 24rpx;
color: #9ca3af;
}
// 底部占位
.footer-placeholder {
height: 120rpx;
}
// 底部操作栏
.footer-actions {
position: fixed;
left: 0;
right: 0;
bottom: 0;
background: #fff;
border-top: 1rpx solid #e5e7eb;
padding: 24rpx;
display: flex;
flex-direction: row;
gap: 24rpx;
z-index: 99;
}
.action-button {
flex: 1;
height: 80rpx;
border-radius: 8rpx;
display: flex;
align-items: center;
justify-content: center;
border: 1rpx solid #e5e7eb;
background: #fff;
&.primary {
background: #f59e0b;
border-color: #f59e0b;
.button-text {
color: #fff;
}
}
&.success {
background: #10b981;
border-color: #10b981;
.button-text {
color: #fff;
}
}
}
.button-text {
font-size: 28rpx;
color: #6b7280;
font-weight: 500;
}

View File

@@ -3,120 +3,186 @@
<scroll-view style="flex:1">
<!-- #endif -->
<view class="page">
<!-- 自定义导航栏 -->
<!-- 导航栏 -->
<view class="nav" :style="{ paddingTop: headerPaddingTop + 'px', height: headerTotalHeight + 'px' }">
<view class="nav-back" @tap="goBack">
<view class="nav-back-icon"></view>
</view>
<text class="nav-title">工单详情</text>
<text class="nav-title">{{ mode === 'create' ? '创建工单' : '工单详情' }}</text>
<view class="nav-capsule"></view>
</view>
<!-- 内容区域 -->
<scroll-view class="content" scroll-y="true" :style="{ marginTop: headerTotalHeight + 'px' }">
<!-- 工单信息 -->
<view class="section">
<view class="section-title">
<view class="title-bar"></view>
<text class="title-text">工单信息</text>
<!-- 工单头部 -->
<view class="header-card">
<view class="header-info">
<text class="workcase-id">{{ workcase.workcaseId }}</text>
<view class="header-tags">
<view class="status-badge" :class="getStatusClass(workcase.status)">
<text class="badge-text">{{ getStatusText(workcase.status) }}</text>
</view>
<view class="urgency-badge" :class="workcase.emergency === 'emergency' ? 'urgent' : 'normal'">
<text class="badge-text">{{ workcase.emergency === 'emergency' ? '紧急' : '普通' }}</text>
</view>
</view>
</view>
<view class="info-card">
<view class="info-item">
<text class="info-label">工单号</text>
<text class="info-value">{{ workcase.workcaseId }}</text>
</view>
<view class="info-item">
<text class="info-label">工单状态</text>
<view class="status-tag" :class="getStatusClass(workcase.status)">
<text class="tag-text">{{ getStatusText(workcase.status) }}</text>
</view>
</view>
<view class="info-item">
<text class="info-label">客户姓名</text>
<text class="info-value">{{ workcase.username || '-' }}</text>
</view>
<view class="info-item">
<text class="info-label">联系电话</text>
<text class="info-value">{{ workcase.phone || '-' }}</text>
</view>
<view class="info-item">
<text class="info-label">紧急程度</text>
<view class="level-tag" :class="workcase.emergency === 'emergency' ? 'urgent' : 'normal'">
<text class="level-text">{{ workcase.emergency === 'emergency' ? '紧急' : '普通' }}</text>
</view>
</view>
<view class="info-item">
<text class="info-label">设备型号</text>
<text class="info-value">{{ workcase.device || '-' }}</text>
</view>
<view class="info-item">
<text class="info-label">设备序列号</text>
<text class="info-value">{{ workcase.deviceCode || '-' }}</text>
</view>
<view class="header-action" v-if="workcase.workcaseId" @tap="handleViewChat">
<text class="action-btn">查看对话</text>
</view>
</view>
<!-- 故障信息 -->
<!-- 工单信息表单 -->
<view class="section">
<view class="section-title">
<view class="title-bar"></view>
<text class="title-text">故障信息</text>
</view>
<view class="info-card">
<view class="info-item">
<text class="info-label">故障类型</text>
<text class="info-value">{{ workcase.type || '-' }}</text>
<view class="form-container">
<!-- 客户姓名 -->
<view class="form-item">
<text class="form-label">客户姓名</text>
<input v-if="mode === 'create'" class="form-input" v-model="workcase.username" placeholder="请输入客户姓名" />
<text v-else class="form-value">{{ workcase.username || '-' }}</text>
</view>
<view class="info-item column">
<text class="info-label">故障描述</text>
<text class="info-desc">{{ workcase.remark || '暂无描述' }}</text>
<!-- 联系电话 -->
<view class="form-item">
<text class="form-label">联系电话</text>
<input v-if="mode === 'create'" class="form-input" v-model="workcase.phone" placeholder="请输入联系电话" />
<text v-else class="form-value">{{ workcase.phone || '-' }}</text>
</view>
<view class="info-item">
<text class="info-label">创建时间</text>
<text class="info-value">{{ workcase.createTime || '-' }}</text>
<!-- 设备名称 -->
<view class="form-item">
<text class="form-label">设备名称</text>
<input v-if="mode === 'create'" class="form-input" v-model="workcase.device" placeholder="请输入设备名称" />
<text v-else class="form-value">{{ workcase.device || '-' }}</text>
</view>
<view class="info-item">
<text class="info-label">处理人</text>
<text class="info-value">{{ workcase.processor || '待分配' }}</text>
<!-- 故障类型 -->
<view class="form-item">
<text class="form-label">故障类型</text>
<picker v-if="mode === 'create'" class="form-picker" :value="typeIndex" :range="faultTypes" @change="onTypeChange">
<view class="picker-content">
<text class="picker-text">{{ workcase.type || '请选择故障类型' }}</text>
<text class="picker-arrow">></text>
</view>
</picker>
<text v-else class="form-value">{{ workcase.type || '-' }}</text>
</view>
<!-- 紧急程度 -->
<view class="form-item" v-if="mode === 'create'">
<text class="form-label">紧急程度</text>
<picker class="form-picker" :value="emergencyIndex" :range="emergencies" @change="onEmergencyChange">
<view class="picker-content">
<text class="picker-text">{{ emergencies[emergencyIndex] }}</text>
<text class="picker-arrow">></text>
</view>
</picker>
</view>
<!-- 现场地址 -->
<view class="form-item">
<text class="form-label">现场地址</text>
<input v-if="mode === 'create'" class="form-input" v-model="workcase.address" placeholder="请输入现场地址" />
<text v-else class="form-value">{{ workcase.address || '-' }}</text>
</view>
<!-- 故障描述 -->
<view class="form-item">
<text class="form-label">故障描述</text>
<textarea v-if="mode === 'create'" class="form-textarea" v-model="workcase.description" placeholder="请详细描述故障现象..." maxlength="500" />
<text v-else class="form-value">{{ workcase.description || '-' }}</text>
</view>
<!-- 设备铭牌 -->
<view class="form-item" v-if="mode !== 'create'">
<text class="form-label">设备铭牌</text>
<text class="form-value">{{ workcase.deviceNamePlate || '-' }}</text>
</view>
<!-- 铭牌照片 -->
<view class="form-item" v-if="mode !== 'create' && workcase.deviceNamePlateImg">
<text class="form-label">铭牌照片</text>
<view class="nameplate-photo" @tap="previewNameplateImage">
<image class="nameplate-image" :src="workcase.deviceNamePlateImg" mode="aspectFit" />
</view>
</view>
<!-- 处理人 -->
<view class="form-item" v-if="mode !== 'create'">
<text class="form-label">处理人</text>
<text class="form-value">{{ workcase.processorName || '未指派' }}</text>
</view>
<!-- 创建时间 -->
<view class="form-item" v-if="mode !== 'create'">
<text class="form-label">创建时间</text>
<text class="form-value">{{ workcase.createTime || '-' }}</text>
</view>
</view>
</view>
<!-- 故障图片 -->
<view class="section" v-if="workcase.imgs && workcase.imgs.length > 0">
<view class="section">
<view class="section-title">
<view class="title-bar"></view>
<view class="title-icon">📷</view>
<text class="title-text">故障图片</text>
</view>
<view class="photo-list">
<view class="photo-item" v-for="(img, index) in workcase.imgs" :key="index">
<image :src="img" mode="aspectFill" @tap="previewImage(img)" />
<view class="photos-grid">
<view class="photo-item" v-for="(img, index) in workcase.imgs" :key="index" @tap="previewFaultImages(index)">
<image class="photo-image" :src="img" mode="aspectFill" />
<view v-if="mode === 'create'" class="photo-delete" @tap.stop="deleteFaultImage(index)">
<text class="delete-icon">×</text>
</view>
</view>
<view v-if="mode === 'create' && workcase.imgs.length < 9" class="photo-upload" @tap="chooseFaultImages">
<text class="upload-plus">+</text>
<text class="upload-text">添加图片</text>
</view>
</view>
<text v-if="mode === 'create'" class="upload-tip">最多上传9张图片支持拍照或从相册选择</text>
</view>
<!-- 处理记录 -->
<view class="section">
<view class="section" v-if="mode !== 'create' && processList.length > 0">
<view class="section-title">
<view class="title-bar"></view>
<text class="title-text">处理记录</text>
</view>
<view class="timeline">
<view class="timeline-item" v-for="(item, index) in processList" :key="index">
<view class="timeline-dot" :class="{ active: index === 0 }"></view>
<view class="timeline-dot" :class="getTimelineDotClass(item.status)"></view>
<view class="timeline-line" v-if="index < processList.length - 1"></view>
<view class="timeline-content">
<view class="timeline-body">
<view class="timeline-header">
<text class="timeline-time">{{ item.time }}</text>
<text class="timeline-date">{{ item.date }}</text>
<text class="timeline-actor">{{ item.actor }}</text>
<text class="timeline-action">{{ item.action }}</text>
</view>
<text class="timeline-title">{{ item.title }}</text>
<text class="timeline-desc" v-if="item.desc">{{ item.desc }}</text>
<text class="timeline-time">{{ item.time }}</text>
</view>
</view>
</view>
</view>
<!-- 底部占位 -->
<view class="footer-placeholder"></view>
</scroll-view>
<!-- 底部操作栏 -->
<view class="footer-actions">
<view class="action-button" @tap="goBack">
<text class="button-text">{{ mode === 'create' ? '取消' : '关闭' }}</text>
</view>
<view class="action-button primary" v-if="mode === 'create'" @tap="submitWorkcase">
<text class="button-text">提交工单</text>
</view>
<view class="action-button primary" v-if="mode === 'view' && workcase.status === 'pending'" @tap="handleAssign">
<text class="button-text">指派工程师</text>
</view>
<view class="action-button success" v-if="mode === 'view' && workcase.status === 'processing'" @tap="handleComplete">
<text class="button-text">完成工单</text>
</view>
</view>
</view>
<!-- #ifdef APP -->
</scroll-view>
@@ -125,80 +191,165 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import type { TbWorkcaseDTO } from '@/types/workcase'
// 接口定义
interface ProcessItem {
time: string
date: string
title: string
status: 'system' | 'manager' | 'engineer'
actor: string
action: string
desc?: string
time: string
}
// 响应式数据
const headerPaddingTop = ref<number>(44)
const headerTotalHeight = ref<number>(88)
const workcaseId = ref<string>('')
const mode = ref<'view' | 'create'>('view')
// 表单选项
const faultTypes = ref<string[]>(['电气系统故障', '机械故障', '控制系统故障', '油路系统故障', '其他故障'])
const typeIndex = ref<number>(0)
const emergencies = ref<string[]>(['普通', '紧急'])
const emergencyIndex = ref<number>(0)
// 工单数据
const workcase = ref<TbWorkcaseDTO>({
workcaseId: 'TH20241217001',
workcaseId: 'W0202501130001',
userId: '1',
username: '李经理',
phone: '13800138001',
type: '控制系统故障',
username: '张伟',
phone: '138****5678',
type: '电气系统故障',
device: 'TH-500GF',
deviceCode: 'TH20230501001',
deviceCode: 'TH-500GF-2023-001',
deviceNamePlate: 'TH-500GF',
deviceNamePlateImg: 'https://via.placeholder.com/400x300?text=设备铭牌',
address: '江西省南昌市红谷滩区xxx路xxx号',
description: '发电机启动后电压不稳定,波动范围较大,影响正常使用',
emergency: 'emergency',
status: 'processing',
processor: '张三',
remark: '发电机组无法启动控制面板显示E03错误代码',
createTime: '2024-12-17 15:30:00',
processorName: '小李',
createTime: '2025-01-13 09:30:00',
imgs: []
})
// 处理记录
const processList = ref<ProcessItem[]>([
{ time: '16:45', date: '2024-12-17', title: '更换控制器主板', desc: '' },
{ time: '16:30', date: '2024-12-17', title: '发现控制器主板故障', desc: '经检测,主板供电模块损坏' },
{ time: '16:15', date: '2024-12-17', title: '到达现场,开始检查设备', desc: '' },
{ time: '15:45', date: '2024-12-17', title: '工程师张三已接单', desc: '' },
{ time: '15:30', date: '2024-12-17', title: '工单已创建', desc: '' }
{
status: 'engineer',
actor: '小李',
action: '开始处理',
desc: '已联系客户,计划今日上门检修',
time: '2025-01-13 08:15:00'
},
{
status: 'manager',
actor: '管理员',
action: '指派工程师',
desc: '指派给小李工程师处理',
time: '2025-01-12 15:00:00'
},
{
status: 'system',
actor: '系统',
action: '工单创建',
desc: '客户通过小电对话提交',
time: '2025-01-12 14:20:00'
}
])
// 生命周期
onMounted(() => {
uni.getSystemInfo({
success: (res) => {
// #ifdef MP-WEIXIN
try {
const menuButtonInfo = uni.getMenuButtonBoundingClientRect()
headerPaddingTop.value = menuButtonInfo.top
headerTotalHeight.value = menuButtonInfo.bottom + 8
} catch (e) {
headerPaddingTop.value = res.statusBarHeight || 44
headerTotalHeight.value = (res.statusBarHeight || 44) + 44
onLoad((options: any) => {
// 处理 mode 参数
if (options.mode === 'create') {
mode.value = 'create'
// 从 storage 读取登录信息
try {
let loginDomainRaw = uni.getStorageSync('loginDomain')
let username = ''
let phone = ''
if (loginDomainRaw) {
// 如果是字符串,需要先解析
let loginDomain = loginDomainRaw
if (typeof loginDomainRaw === 'string') {
loginDomain = JSON.parse(loginDomainRaw)
}
// 尝试多种可能的字段路径
username = loginDomain.userInfo?.username ||
loginDomain.user?.username ||
loginDomain.username || ''
phone = loginDomain.user?.phone ||
loginDomain.userInfo?.phone ||
loginDomain.phone || ''
}
// 创建模式,初始化表单并填充用户信息
workcase.value = {
username: username,
phone: phone,
device: '',
type: '',
address: '',
description: '',
emergency: 'normal',
imgs: []
}
} catch (error) {
console.error('读取用户信息失败:', error)
// 初始化空表单
workcase.value = {
username: '',
phone: '',
device: '',
type: '',
address: '',
description: '',
emergency: 'normal',
imgs: []
}
// #endif
// #ifndef MP-WEIXIN
headerPaddingTop.value = res.statusBarHeight || 44
headerTotalHeight.value = (res.statusBarHeight || 44) + 44
// #endif
}
})
// 获取页面参数
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1] as any
if (currentPage && currentPage.options && currentPage.options.workcaseId) {
workcaseId.value = currentPage.options.workcaseId
} else if (options.workcaseId) {
// 查看模式
workcaseId.value = options.workcaseId
loadWorkcaseDetail(workcaseId.value)
}
// 处理 roomId 参数
if (options.roomId) {
// TODO: 可以将 roomId 关联到工单
}
})
onMounted(() => {
const windowInfo = uni.getWindowInfo()
const statusBarHeight = windowInfo.statusBarHeight || 44
// #ifdef MP-WEIXIN
try {
const menuButtonInfo = uni.getMenuButtonBoundingClientRect()
headerPaddingTop.value = menuButtonInfo.top
headerTotalHeight.value = menuButtonInfo.bottom + 8
} catch (e) {
headerPaddingTop.value = statusBarHeight
headerTotalHeight.value = statusBarHeight + 44
}
// #endif
// #ifndef MP-WEIXIN
headerPaddingTop.value = statusBarHeight
headerTotalHeight.value = statusBarHeight + 44
// #endif
})
// 加载工单详情
function loadWorkcaseDetail(id: string) {
console.log('加载工单详情:', id)
// TODO: 调用 workcaseAPI.getWorkcaseById(id) 获取数据
}
@@ -222,14 +373,131 @@ function getStatusText(status?: string): string {
}
}
// 预览图片
function previewImage(url: string) {
uni.previewImage({
urls: workcase.value.imgs || [],
current: url
// 获取时间线圆点样式类
function getTimelineDotClass(status: string): string {
switch (status) {
case 'system': return 'dot-system'
case 'manager': return 'dot-manager'
case 'engineer': return 'dot-engineer'
default: return 'dot-system'
}
}
// 查看对话
function handleViewChat() {
uni.navigateTo({
url: `/pages/chatRoom/chatRoom/chatRoom?roomId=${workcase.value.workcaseId}`
})
}
// 指派工程师
function handleAssign() {
uni.showToast({
title: '指派工程师',
icon: 'none'
})
// TODO: 实现指派逻辑
}
// 完成工单
function handleComplete() {
uni.showModal({
title: '完成确认',
content: '确认完成该工单?',
success: (res) => {
if (res.confirm) {
// TODO: 调用 API 完成工单
uni.showToast({
title: '工单已完成',
icon: 'success'
})
}
}
})
}
// 表单选择器事件
function onTypeChange(e: any) {
typeIndex.value = e.detail.value
workcase.value.type = faultTypes.value[e.detail.value]
}
function onEmergencyChange(e: any) {
emergencyIndex.value = e.detail.value
workcase.value.emergency = emergencyIndex.value === 0 ? 'normal' : 'emergency'
}
// 选择故障图片
function chooseFaultImages() {
uni.chooseImage({
count: 9 - (workcase.value.imgs?.length || 0),
sizeType: ['compressed'],
sourceType: ['camera', 'album'],
success: (res) => {
if (!workcase.value.imgs) {
workcase.value.imgs = []
}
workcase.value.imgs.push(...res.tempFilePaths)
},
fail: (err) => {
console.log('选择图片失败:', err)
}
})
}
// 删除故障图片
function deleteFaultImage(index: number) {
if (workcase.value.imgs) {
workcase.value.imgs.splice(index, 1)
}
}
// 预览铭牌照片
function previewNameplateImage() {
if (!workcase.value.deviceNamePlateImg) return
uni.previewImage({
urls: [workcase.value.deviceNamePlateImg],
current: 0
})
}
// 预览故障图片
function previewFaultImages(index: number) {
if (!workcase.value.imgs || workcase.value.imgs.length === 0) return
uni.previewImage({
urls: workcase.value.imgs,
current: index
})
}
// 提交工单
function submitWorkcase() {
// 校验必填项
if (!workcase.value.username || !workcase.value.phone || !workcase.value.description) {
uni.showToast({
title: '请填写必填项',
icon: 'none'
})
return
}
uni.showLoading({
title: '提交中...'
})
// TODO: 调用 API 提交工单
setTimeout(() => {
uni.hideLoading()
uni.showToast({
title: '工单创建成功',
icon: 'success'
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
}, 1000)
}
// 返回上一页
function goBack() {
uni.navigateBack()

View File

@@ -140,24 +140,23 @@ const filteredOrders = computed(() => {
// 生命周期
onMounted(() => {
uni.getSystemInfo({
success: (res) => {
// #ifdef MP-WEIXIN
try {
const menuButtonInfo = uni.getMenuButtonBoundingClientRect()
headerPaddingTop.value = menuButtonInfo.top
headerTotalHeight.value = menuButtonInfo.bottom + 8
} catch (e) {
headerPaddingTop.value = res.statusBarHeight || 44
headerTotalHeight.value = (res.statusBarHeight || 44) + 44
}
// #endif
// #ifndef MP-WEIXIN
headerPaddingTop.value = res.statusBarHeight || 44
headerTotalHeight.value = (res.statusBarHeight || 44) + 44
// #endif
}
})
const windowInfo = uni.getWindowInfo()
const statusBarHeight = windowInfo.statusBarHeight || 44
// #ifdef MP-WEIXIN
try {
const menuButtonInfo = uni.getMenuButtonBoundingClientRect()
headerPaddingTop.value = menuButtonInfo.top
headerTotalHeight.value = menuButtonInfo.bottom + 8
} catch (e) {
headerPaddingTop.value = statusBarHeight
headerTotalHeight.value = statusBarHeight + 44
}
// #endif
// #ifndef MP-WEIXIN
headerPaddingTop.value = statusBarHeight
headerTotalHeight.value = statusBarHeight + 44
// #endif
// TODO: 实际调用API获取工单列表
loadWorkcaseList()