微信修改

This commit is contained in:
2025-12-22 19:16:53 +08:00
parent ae16757984
commit cfb160cf09
70 changed files with 4697 additions and 1839 deletions

View File

@@ -1,403 +0,0 @@
.detail-container {
height: 100vh;
display: flex;
flex-direction: column;
background-color: #F5F5F5;
}
.detail-content {
flex: 1;
padding: 16px;
padding-bottom: 80px;
}
.info-card {
background-color: #FFFFFF;
margin-bottom: 16px;
padding: 16px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #F0F0F0;
}
.card-title {
font-size: 18px;
font-weight: 600;
color: #333333;
}
.status-tag {
padding: 6px 12px;
border-radius: 16px;
}
.status-pending {
background-color: #FFF3E0;
color: #F57C00;
}
.status-processing {
background-color: #E3F2FD;
color: #1976D2;
}
.status-completed {
background-color: #E8F5E8;
color: #388E3C;
}
.status-cancelled {
background-color: #FFEBEE;
color: #D32F2F;
}
.status-text {
font-size: 14px;
font-weight: 500;
}
.info-item {
display: flex;
align-items: flex-start;
margin-bottom: 12px;
}
.label {
width: 80px;
font-size: 14px;
color: #666666;
line-height: 1.4;
}
.value {
flex: 1;
font-size: 14px;
color: #333333;
line-height: 1.4;
}
.priority {
font-weight: 500;
}
.priority-normal {
color: #666666;
}
.priority-urgent {
color: #FF9800;
}
.priority-emergency {
color: #F44336;
}
.description {
font-size: 16px;
color: #333333;
line-height: 1.6;
}
.image-gallery {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.gallery-image {
width: 80px;
height: 80px;
border-radius: 8px;
}
.progress-text {
font-size: 14px;
font-weight: 500;
color: #1976D2;
}
.progress-container {
margin: 12px 0;
}
.progress-bar {
width: 100%;
height: 6px;
background-color: #E0E0E0;
border-radius: 3px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background-color: #1976D2;
border-radius: 3px;
transition: width 0.3s ease;
}
.progress-desc {
font-size: 12px;
color: #666666;
}
.timeline {
position: relative;
}
.timeline::before {
content: '';
position: absolute;
left: 10px;
top: 0;
bottom: 0;
width: 2px;
background-color: #E0E0E0;
}
.timeline-item {
position: relative;
display: flex;
align-items: flex-start;
margin-bottom: 20px;
}
.timeline-dot {
width: 20px;
height: 20px;
border-radius: 10px;
margin-right: 16px;
border: 3px solid #FFFFFF;
box-shadow: 0 0 0 2px #E0E0E0;
flex-shrink: 0;
}
.dot-create {
background-color: #4CAF50;
}
.dot-accept {
background-color: #2196F3;
}
.dot-processing {
background-color: #FF9800;
}
.dot-complete {
background-color: #4CAF50;
}
.timeline-content {
flex: 1;
padding-top: 2px;
}
.record-title {
font-size: 16px;
font-weight: 500;
color: #333333;
line-height: 1.4;
}
.record-desc {
display: block;
font-size: 14px;
color: #666666;
margin: 4px 0;
line-height: 1.4;
}
.record-meta {
display: flex;
justify-content: space-between;
margin-top: 8px;
}
.record-time {
font-size: 12px;
color: #999999;
}
.record-operator {
font-size: 12px;
color: #999999;
}
.rating-section {
text-align: center;
}
.stars {
margin-bottom: 12px;
}
.star {
font-size: 24px;
color: #FFD700;
margin: 0 2px;
}
.rating-text {
font-size: 14px;
color: #666666;
line-height: 1.4;
}
.bottom-actions {
background-color: #FFFFFF;
padding: 16px;
border-top: 1px solid #E0E0E0;
display: flex;
gap: 12px;
}
.action-btn {
flex: 1;
height: 44px;
border-radius: 22px;
font-size: 16px;
font-weight: 500;
border: none;
}
.action-btn.primary {
background-color: #1976D2;
color: #FFFFFF;
}
.action-btn.secondary {
background-color: #F0F0F0;
color: #666666;
}
.rating-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
}
.modal-content {
width: 90%;
max-width: 400px;
background-color: #FFFFFF;
border-radius: 12px;
overflow: hidden;
}
.modal-header {
padding: 20px 16px 16px;
border-bottom: 1px solid #F0F0F0;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-title {
font-size: 18px;
font-weight: 600;
color: #333333;
}
.close-btn {
width: 28px;
height: 28px;
border-radius: 14px;
background-color: #F0F0F0;
display: flex;
align-items: center;
justify-content: center;
}
.close-icon {
color: #666666;
font-size: 20px;
line-height: 1;
}
.rating-form {
padding: 20px 16px;
}
.form-label {
display: block;
font-size: 16px;
color: #333333;
margin-bottom: 12px;
}
.star-rating {
text-align: center;
margin-bottom: 20px;
}
.rating-star {
font-size: 32px;
color: #E0E0E0;
margin: 0 4px;
}
.rating-star.active {
color: #FFD700;
}
.rating-textarea {
width: 100%;
min-height: 80px;
padding: 12px;
border: 1px solid #E0E0E0;
border-radius: 8px;
font-size: 14px;
resize: none;
}
.char-count {
color: #999999;
font-size: 12px;
text-align: right;
margin-top: 4px;
}
.modal-actions {
padding: 16px;
border-top: 1px solid #F0F0F0;
display: flex;
gap: 12px;
}
.modal-btn {
flex: 1;
height: 40px;
border-radius: 20px;
font-size: 16px;
border: none;
}
.modal-btn.cancel {
background-color: #F0F0F0;
color: #666666;
}
.modal-btn.confirm {
background-color: #1976D2;
color: #FFFFFF;
}
.modal-btn[disabled] {
background-color: #CCCCCC;
color: #999999;
}

View File

@@ -1,507 +0,0 @@
<template>
<view class="detail-container">
<scroll-view class="detail-content" scroll-y="true">
<!-- 工单基本信息 -->
<view class="info-card">
<view class="card-header">
<text class="card-title">基本信息</text>
<view class="status-tag" :class="getStatusClass(workcase.status)">
<text class="status-text">{{workcase.statusText}}</text>
</view>
</view>
<view class="info-item">
<text class="label">工单标题</text>
<text class="value">{{workcase.title}}</text>
</view>
<view class="info-item">
<text class="label">工单编号</text>
<text class="value">{{workcase.number}}</text>
</view>
<view class="info-item">
<text class="label">问题分类</text>
<text class="value">{{workcase.category}}</text>
</view>
<view class="info-item">
<text class="label">紧急程度</text>
<text class="value priority" :class="getPriorityClass(workcase.priority)">
{{workcase.priority}}
</text>
</view>
<view class="info-item">
<text class="label">联系方式</text>
<text class="value">{{workcase.contact}}</text>
</view>
<view class="info-item">
<text class="label">创建时间</text>
<text class="value">{{formatDateTime(workcase.createTime)}}</text>
</view>
</view>
<!-- 问题描述 -->
<view class="info-card">
<view class="card-header">
<text class="card-title">问题描述</text>
</view>
<text class="description">{{workcase.description}}</text>
</view>
<!-- 图片附件 -->
<view class="info-card" v-if="workcase.images && workcase.images.length > 0">
<view class="card-header">
<text class="card-title">相关图片</text>
</view>
<view class="image-gallery">
<image
class="gallery-image"
v-for="(image, index) in workcase.images"
:key="index"
:src="image"
mode="aspectFill"
@tap="previewImage(index)"
/>
</view>
</view>
<!-- 处理进度 -->
<view class="info-card" v-if="workcase.status === 'processing'">
<view class="card-header">
<text class="card-title">处理进度</text>
<text class="progress-text">{{workcase.progress}}%</text>
</view>
<view class="progress-container">
<view class="progress-bar">
<view class="progress-fill" :style="'width: ' + workcase.progress + '%'"></view>
</view>
</view>
<text class="progress-desc">{{getProgressDesc(workcase.progress)}}</text>
</view>
<!-- 处理记录 -->
<view class="info-card">
<view class="card-header">
<text class="card-title">处理记录</text>
</view>
<view class="timeline">
<view
class="timeline-item"
v-for="(record, index) in workcase.records"
:key="index"
>
<view class="timeline-dot" :class="getRecordDotClass(record.type)"></view>
<view class="timeline-content">
<text class="record-title">{{record.title}}</text>
<text class="record-desc" v-if="record.description">{{record.description}}</text>
<view class="record-meta">
<text class="record-time">{{formatDateTime(record.time)}}</text>
<text class="record-operator">{{record.operator}}</text>
</view>
</view>
</view>
</view>
</view>
<!-- 客服评价 -->
<view class="info-card" v-if="workcase.rating">
<view class="card-header">
<text class="card-title">服务评价</text>
</view>
<view class="rating-section">
<view class="stars">
<text
class="star"
v-for="i in 5"
:key="i"
:class="{active: i <= workcase.rating.score}"
>★</text>
</view>
<text class="rating-text">{{workcase.rating.comment}}</text>
</view>
</view>
</scroll-view>
<!-- 底部操作栏 -->
<view class="bottom-actions">
<button
class="action-btn secondary"
@tap="contactService"
>
联系客服
</button>
<button
class="action-btn primary"
v-if="workcase.status === 'pending'"
@tap="cancelWorkcase"
>
取消工单
</button>
<button
class="action-btn primary"
v-if="workcase.status === 'processing'"
@tap="confirmComplete"
>
确认完成
</button>
<button
class="action-btn primary"
v-if="workcase.status === 'completed' && !workcase.rating"
@tap="showRating"
>
服务评价
</button>
</view>
<!-- 评价弹窗 -->
<view class="rating-modal" v-if="showRatingModal">
<view class="modal-content">
<view class="modal-header">
<text class="modal-title">服务评价</text>
<view class="close-btn" @tap="hideRating">
<text class="close-icon">×</text>
</view>
</view>
<view class="rating-form">
<text class="form-label">请为本次服务打分</text>
<view class="star-rating">
<text
class="rating-star"
v-for="i in 5"
:key="i"
:class="{active: i <= ratingScore}"
@tap="setRating(i)"
>★</text>
</view>
<text class="form-label">评价内容</text>
<textarea
class="rating-textarea"
v-model="ratingComment"
placeholder="请输入您的评价内容..."
maxlength="200"
/>
<text class="char-count">{{ratingComment.length}}/200</text>
</view>
<view class="modal-actions">
<button class="modal-btn cancel" @tap="hideRating">取消</button>
<button
class="modal-btn confirm"
@tap="submitRating"
:disabled="ratingScore === 0"
>提交</button>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
// 接口定义
interface Workcase {
id: string
number: string
title: string
category: string
priority: string
status: 'pending' | 'processing' | 'completed' | 'cancelled'
statusText: string
description: string
contact: string
progress: number
createTime: Date
updateTime: Date
images?: string[]
records: ProcessRecord[]
rating?: Rating
}
interface ProcessRecord {
type: string
title: string
description?: string
time: Date
operator: string
}
interface Rating {
score: number
comment: string
}
// 响应式数据
const workcaseId = ref<string | null>(null)
const workcase = ref<Workcase>({} as Workcase)
const showRatingModal = ref<boolean>(false)
const ratingScore = ref<number>(0)
const ratingComment = ref<string>('')
// 生命周期
onMounted(() => {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
workcaseId.value = currentPage.options?.id || '1'
loadWorkcaseDetail()
})
// 方法定义
async function loadWorkcaseDetail() {
try {
// 模拟获取工单详情
workcase.value = getMockWorkcase()
// 设置页面标题
uni.setNavigationBarTitle({
title: workcase.value.title
})
} catch (error) {
uni.showToast({
title: '加载失败',
icon: 'error'
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
}
}
function getMockWorkcase(): Workcase {
return {
id: workcaseId.value || '1',
number: `WC2024${String(workcaseId.value || '1').padStart(4, '0')}`,
title: '小区公园路灯不亮需要维修',
category: '设施报修',
priority: '紧急',
status: 'processing',
statusText: '处理中',
description: '小区公园内的路灯已经连续三天不亮了,影响居民夜间出行安全。路灯位置在公园主干道上,希望能够尽快派人维修。',
contact: '138****5678',
progress: 65,
createTime: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000),
updateTime: new Date(),
images: [
'/static/workcase1.jpg',
'/static/workcase2.jpg'
],
records: [
{
type: 'create',
title: '工单创建',
description: '用户提交工单,问题已记录',
time: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000),
operator: '系统'
},
{
type: 'accept',
title: '工单受理',
description: '客服已受理,安排相关人员处理',
time: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000 + 30 * 60 * 1000),
operator: '客服小王'
},
{
type: 'processing',
title: '现场勘查',
description: '维修人员已到达现场,正在检查路灯故障原因',
time: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000),
operator: '维修师傅张三'
},
{
type: 'processing',
title: '配件采购',
description: '故障原因确认为灯泡损坏,正在采购替换配件',
time: new Date(Date.now() - 12 * 60 * 60 * 1000),
operator: '维修师傅张三'
}
]
}
}
// 获取状态样式
function getStatusClass(status: string) {
return {
'status-pending': status === 'pending',
'status-processing': status === 'processing',
'status-completed': status === 'completed',
'status-cancelled': status === 'cancelled'
}
}
// 获取优先级样式
function getPriorityClass(priority: string) {
return {
'priority-normal': priority === '一般',
'priority-urgent': priority === '紧急',
'priority-emergency': priority === '非常紧急'
}
}
// 获取记录点样式
function getRecordDotClass(type: string) {
return {
'dot-create': type === 'create',
'dot-accept': type === 'accept',
'dot-processing': type === 'processing',
'dot-complete': type === 'complete'
}
}
// 获取进度描述
function getProgressDesc(progress: number): string {
if (progress < 25) {
return '刚刚开始处理'
} else if (progress < 50) {
return '正在积极处理中'
} else if (progress < 75) {
return '处理进展顺利'
} else if (progress < 100) {
return '即将完成处理'
} else {
return '处理已完成'
}
}
// 格式化日期时间
function formatDateTime(date: Date): string {
const d = new Date(date)
const year = d.getFullYear()
const month = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
const hour = String(d.getHours()).padStart(2, '0')
const minute = String(d.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day} ${hour}:${minute}`
}
// 预览图片
function previewImage(index: number) {
uni.previewImage({
current: index,
urls: workcase.value.images || []
})
}
// 联系客服
function contactService() {
uni.showActionSheet({
itemList: ['拨打电话', '在线客服'],
success: (res) => {
if (res.tapIndex === 0) {
uni.makePhoneCall({
phoneNumber: '400-123-4567'
})
} else {
uni.navigateTo({
url: '/pages/index/index'
})
}
}
})
}
// 取消工单
function cancelWorkcase() {
uni.showModal({
title: '确认取消',
content: '确定要取消此工单吗?取消后无法恢复。',
success: (res) => {
if (res.confirm) {
workcase.value.status = 'cancelled'
workcase.value.statusText = '已取消'
// 添加取消记录
workcase.value.records.push({
type: 'cancel',
title: '工单取消',
description: '用户主动取消工单',
time: new Date(),
operator: '用户'
})
uni.showToast({
title: '工单已取消',
icon: 'success'
})
}
}
})
}
// 确认完成
function confirmComplete() {
uni.showModal({
title: '确认完成',
content: '确认问题已经得到解决?',
success: (res) => {
if (res.confirm) {
workcase.value.status = 'completed'
workcase.value.statusText = '已完成'
workcase.value.progress = 100
// 添加完成记录
workcase.value.records.push({
type: 'complete',
title: '工单完成',
description: '用户确认问题已解决',
time: new Date(),
operator: '用户'
})
uni.showToast({
title: '工单已完成',
icon: 'success'
})
}
}
})
}
// 显示评价弹窗
function showRating() {
showRatingModal.value = true
ratingScore.value = 0
ratingComment.value = ''
}
// 隐藏评价弹窗
function hideRating() {
showRatingModal.value = false
}
// 设置评分
function setRating(score: number) {
ratingScore.value = score
}
// 提交评价
function submitRating() {
if (ratingScore.value === 0) {
uni.showToast({
title: '请选择评分',
icon: 'none'
})
return
}
workcase.value.rating = {
score: ratingScore.value,
comment: ratingComment.value || '用户未填写评价内容'
}
hideRating()
uni.showToast({
title: '评价提交成功',
icon: 'success'
})
}
</script>
<style lang="scss" scoped>
@import './detail.scss';
</style>

View File

@@ -1,293 +0,0 @@
.workcase-list-container {
height: 100vh;
background-color: #F5F5F5;
display: flex;
flex-direction: column;
}
.filter-bar {
background-color: #FFFFFF;
padding: 12px 16px;
display: flex;
gap: 12px;
border-bottom: 1px solid #E0E0E0;
}
.filter-picker {
flex: 1;
}
.picker-content {
padding: 8px 12px;
border: 1px solid #E0E0E0;
border-radius: 6px;
background-color: #F8F8F8;
display: flex;
justify-content: space-between;
align-items: center;
}
.picker-text {
color: #333333;
font-size: 14px;
}
.picker-arrow {
color: #999999;
font-size: 12px;
}
.workcase-list {
flex: 1;
padding: 0 16px;
}
.stats-card {
background-color: #FFFFFF;
margin: 16px 0;
padding: 16px;
border-radius: 12px;
display: flex;
justify-content: space-around;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.stat-item {
text-align: center;
}
.stat-number {
display: block;
font-size: 24px;
font-weight: bold;
color: #1976D2;
line-height: 1.2;
}
.stat-label {
font-size: 12px;
color: #666666;
margin-top: 4px;
}
.workcase-card {
background-color: #FFFFFF;
margin-bottom: 12px;
padding: 16px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.card-header {
margin-bottom: 12px;
}
.title-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 8px;
}
.workcase-title {
flex: 1;
font-size: 16px;
font-weight: 500;
color: #333333;
line-height: 1.4;
}
.status-tag {
padding: 4px 8px;
border-radius: 12px;
margin-left: 12px;
}
.status-pending {
background-color: #FFF3E0;
color: #F57C00;
}
.status-processing {
background-color: #E3F2FD;
color: #1976D2;
}
.status-completed {
background-color: #E8F5E8;
color: #388E3C;
}
.status-cancelled {
background-color: #FFEBEE;
color: #D32F2F;
}
.status-text {
font-size: 12px;
font-weight: 500;
}
.workcase-id {
font-size: 12px;
color: #999999;
}
.card-content {
margin-bottom: 12px;
}
.info-row {
display: flex;
align-items: center;
margin-bottom: 4px;
}
.info-label {
font-size: 14px;
color: #666666;
width: 80px;
}
.info-value {
font-size: 14px;
color: #333333;
flex: 1;
}
.priority {
font-weight: 500;
}
.priority-normal {
color: #666666;
}
.priority-urgent {
color: #FF9800;
}
.priority-emergency {
color: #F44336;
}
.card-description {
margin-bottom: 12px;
}
.description-text {
font-size: 14px;
color: #666666;
line-height: 1.4;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
line-clamp: 2;
overflow: hidden;
}
.card-actions {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
.action-btn {
flex: 1;
height: 32px;
border-radius: 16px;
font-size: 14px;
border: none;
}
.action-btn.primary {
background-color: #1976D2;
color: #FFFFFF;
}
.action-btn.secondary {
background-color: #F0F0F0;
color: #666666;
}
.progress-bar {
width: 100%;
height: 4px;
background-color: #E0E0E0;
border-radius: 2px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background-color: #1976D2;
border-radius: 2px;
transition: width 0.3s ease;
}
.empty-state {
text-align: center;
padding: 60px 20px;
}
.empty-icon {
width: 120px;
height: 120px;
margin-bottom: 16px;
}
.empty-text {
font-size: 16px;
color: #999999;
margin-bottom: 24px;
}
.create-btn {
width: 140px;
height: 44px;
background-color: #1976D2;
color: #FFFFFF;
border-radius: 22px;
border: none;
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
}
.create-icon {
font-size: 20px;
font-weight: bold;
}
.load-more {
text-align: center;
padding: 20px;
}
.load-text {
color: #999999;
font-size: 14px;
}
.fab {
position: fixed;
bottom: 80px;
right: 20px;
width: 56px;
height: 56px;
border-radius: 28px;
background-color: #1976D2;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 12px rgba(25, 118, 210, 0.4);
z-index: 10;
}
.fab-icon {
color: #FFFFFF;
font-size: 24px;
font-weight: bold;
}

View File

@@ -1,443 +0,0 @@
<template>
<view class="workcase-list-container">
<!-- 筛选栏 -->
<view class="filter-bar">
<picker
class="filter-picker"
:value="statusIndex"
:range="statusOptions"
@change="onStatusChange"
>
<view class="picker-content">
<text class="picker-text">{{statusOptions[statusIndex]}}</text>
<text class="picker-arrow">▼</text>
</view>
</picker>
<picker
class="filter-picker"
:value="categoryIndex"
:range="categoryOptions"
@change="onCategoryChange"
>
<view class="picker-content">
<text class="picker-text">{{categoryOptions[categoryIndex]}}</text>
<text class="picker-arrow">▼</text>
</view>
</picker>
</view>
<!-- 工单列表 -->
<scroll-view
class="workcase-list"
scroll-y="true"
@scrolltolower="loadMore"
:refresher-enabled="true"
@refresherrefresh="onRefresh"
:refresher-triggered="isRefreshing"
>
<!-- 统计信息 -->
<view class="stats-card">
<view class="stat-item">
<text class="stat-number">{{stats.total}}</text>
<text class="stat-label">总工单</text>
</view>
<view class="stat-item">
<text class="stat-number">{{stats.pending}}</text>
<text class="stat-label">待处理</text>
</view>
<view class="stat-item">
<text class="stat-number">{{stats.processing}}</text>
<text class="stat-label">处理中</text>
</view>
<view class="stat-item">
<text class="stat-number">{{stats.completed}}</text>
<text class="stat-label">已完成</text>
</view>
</view>
<!-- 工单卡片列表 -->
<view
class="workcase-card"
v-for="workcase in displayList"
:key="workcase.id"
@tap="goToDetail(workcase)"
>
<view class="card-header">
<view class="title-row">
<text class="workcase-title">{{workcase.title}}</text>
<view class="status-tag" :class="getStatusClass(workcase.status)">
<text class="status-text">{{workcase.statusText}}</text>
</view>
</view>
<text class="workcase-id">工单编号:{{workcase.number}}</text>
</view>
<view class="card-content">
<view class="info-row">
<text class="info-label">分类:</text>
<text class="info-value">{{workcase.category}}</text>
</view>
<view class="info-row">
<text class="info-label">紧急程度:</text>
<text class="info-value priority" :class="getPriorityClass(workcase.priority)">
{{workcase.priority}}
</text>
</view>
<view class="info-row">
<text class="info-label">创建时间:</text>
<text class="info-value">{{formatTime(workcase.createTime)}}</text>
</view>
</view>
<view class="card-description">
<text class="description-text">{{workcase.description}}</text>
</view>
<!-- 操作按钮 -->
<view class="card-actions" v-if="workcase.status !== 'completed'">
<button
class="action-btn secondary"
v-if="workcase.status === 'pending'"
@tap.stop="cancelWorkcase(workcase)"
>
取消工单
</button>
<button
class="action-btn primary"
v-if="workcase.status === 'processing'"
@tap.stop="confirmComplete(workcase)"
>
确认完成
</button>
<button
class="action-btn secondary"
@tap.stop="contactService(workcase)"
>
联系客服
</button>
</view>
<!-- 进度条 -->
<view class="progress-bar" v-if="workcase.status === 'processing'">
<view class="progress-fill" :style="'width: ' + workcase.progress + '%'"></view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-if="displayList.length === 0 && !isLoading">
<image class="empty-icon" src="/static/empty-workcase.png" mode="aspectFit" />
<text class="empty-text">暂无工单记录</text>
<button class="create-btn" @tap="createWorkcase">
<text class="create-icon">+</text>
<text>创建工单</text>
</button>
</view>
<!-- 加载更多 -->
<view class="load-more" v-if="hasMore">
<text class="load-text">{{isLoading ? '加载中...' : '上拉加载更多'}}</text>
</view>
</scroll-view>
<!-- 悬浮按钮 -->
<view class="fab" @tap="createWorkcase">
<text class="fab-icon">+</text>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
// 接口定义
interface Workcase {
id: number
number: string
title: string
description: string
category: string
priority: string
status: 'pending' | 'processing' | 'completed' | 'cancelled'
statusText: string
progress: number
createTime: Date
updateTime: Date
}
interface Stats {
total: number
pending: number
processing: number
completed: number
}
// 响应式数据
const workcaseList = ref<Workcase[]>([])
const displayList = ref<Workcase[]>([])
const statusOptions = ref<string[]>(['全部状态', '待处理', '处理中', '已完成', '已取消'])
const statusIndex = ref<number>(0)
const categoryOptions = ref<string[]>(['全部分类', '设施报修', '环境卫生', '交通问题', '安全隐患', '其他问题'])
const categoryIndex = ref<number>(0)
const stats = ref<Stats>({
total: 0,
pending: 0,
processing: 0,
completed: 0
})
const isLoading = ref<boolean>(false)
const isRefreshing = ref<boolean>(false)
const hasMore = ref<boolean>(true)
const page = ref<number>(1)
// 生命周期
onMounted(() => {
loadData()
})
// 方法定义
// 加载数据
async function loadData() {
isLoading.value = true
try {
// 模拟数据
const mockData = generateMockData()
workcaseList.value = mockData
updateDisplayList()
updateStats()
} catch (error) {
uni.showToast({
title: '加载失败',
icon: 'error'
})
} finally {
isLoading.value = false
isRefreshing.value = false
}
}
// 生成模拟数据
function generateMockData(): Workcase[] {
const categories = ['设施报修', '环境卫生', '交通问题', '安全隐患', '其他问题']
const priorities = ['一般', '紧急', '非常紧急']
const statuses: ('pending' | 'processing' | 'completed' | 'cancelled')[] = ['pending', 'processing', 'completed', 'cancelled']
const statusTexts = {
pending: '待处理',
processing: '处理中',
completed: '已完成',
cancelled: '已取消'
}
const mockList: Workcase[] = []
for (let i = 1; i <= 15; i++) {
const status = statuses[Math.floor(Math.random() * statuses.length)]
mockList.push({
id: i,
number: `WC${new Date().getFullYear()}${String(i).padStart(4, '0')}`,
title: `测试工单${i}`,
description: `这是一个测试工单的描述内容,描述了具体的问题情况...`,
category: categories[Math.floor(Math.random() * categories.length)],
priority: priorities[Math.floor(Math.random() * priorities.length)],
status: status,
statusText: statusTexts[status],
progress: status === 'processing' ? Math.floor(Math.random() * 80) + 10 : 0,
createTime: new Date(Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000),
updateTime: new Date()
})
}
return mockList
}
// 更新显示列表
function updateDisplayList() {
let filtered = [...workcaseList.value]
// 状态筛选
if (statusIndex.value > 0) {
const statusMap: Record<number, string> = {
1: 'pending',
2: 'processing',
3: 'completed',
4: 'cancelled'
}
filtered = filtered.filter(item => item.status === statusMap[statusIndex.value])
}
// 分类筛选
if (categoryIndex.value > 0) {
const category = categoryOptions.value[categoryIndex.value]
filtered = filtered.filter(item => item.category === category)
}
displayList.value = filtered
}
// 更新统计信息
function updateStats() {
stats.value = {
total: workcaseList.value.length,
pending: workcaseList.value.filter(item => item.status === 'pending').length,
processing: workcaseList.value.filter(item => item.status === 'processing').length,
completed: workcaseList.value.filter(item => item.status === 'completed').length
}
}
// 状态筛选改变
function onStatusChange(e: any) {
statusIndex.value = e.detail.value
updateDisplayList()
}
// 分类筛选改变
function onCategoryChange(e: any) {
categoryIndex.value = e.detail.value
updateDisplayList()
}
// 下拉刷新
function onRefresh() {
isRefreshing.value = true
page.value = 1
loadData()
}
// 加载更多
function loadMore() {
if (!hasMore.value || isLoading.value) return
page.value++
// 模拟加载更多
setTimeout(() => {
if (page.value > 3) {
hasMore.value = false
}
}, 1000)
}
// 跳转到详情页
function goToDetail(workcase: Workcase) {
uni.navigateTo({
url: `/pages/workcase/detail?id=${workcase.id}`
})
}
// 获取状态样式
function getStatusClass(status: string) {
return {
'status-pending': status === 'pending',
'status-processing': status === 'processing',
'status-completed': status === 'completed',
'status-cancelled': status === 'cancelled'
}
}
// 获取优先级样式
function getPriorityClass(priority: string) {
return {
'priority-normal': priority === '一般',
'priority-urgent': priority === '紧急',
'priority-emergency': priority === '非常紧急'
}
}
// 格式化时间
function formatTime(date: Date) {
const now = new Date()
const diff = now.getTime() - new Date(date).getTime()
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
if (days === 0) {
const hours = Math.floor(diff / (1000 * 60 * 60))
if (hours === 0) {
const minutes = Math.floor(diff / (1000 * 60))
return `${minutes}分钟前`
}
return `${hours}小时前`
} else if (days === 1) {
return '昨天'
} else if (days < 7) {
return `${days}天前`
} else {
return new Date(date).toLocaleDateString()
}
}
// 取消工单
function cancelWorkcase(workcase: Workcase) {
uni.showModal({
title: '确认取消',
content: `确定要取消工单"${workcase.title}"吗?`,
success: (res) => {
if (res.confirm) {
workcase.status = 'cancelled'
workcase.statusText = '已取消'
updateStats()
updateDisplayList()
uni.showToast({
title: '工单已取消',
icon: 'success'
})
}
}
})
}
// 确认完成
function confirmComplete(workcase: Workcase) {
uni.showModal({
title: '确认完成',
content: `确认工单"${workcase.title}"已处理完成?`,
success: (res) => {
if (res.confirm) {
workcase.status = 'completed'
workcase.statusText = '已完成'
workcase.progress = 100
updateStats()
updateDisplayList()
uni.showToast({
title: '工单已完成',
icon: 'success'
})
}
}
})
}
// 联系客服
function contactService(workcase: Workcase) {
uni.showActionSheet({
itemList: ['拨打电话', '在线客服', '查看进度'],
success: (res) => {
switch (res.tapIndex) {
case 0:
uni.makePhoneCall({
phoneNumber: '400-123-4567'
})
break
case 1:
uni.navigateTo({
url: '/pages/index/index'
})
break
case 2:
goToDetail(workcase)
break
}
}
})
}
// 创建工单
function createWorkcase() {
uni.switchTab({
url: '/pages/index/index'
})
}
</script>
<style lang="scss" scoped>
@import './list.scss';
</style>

View File

@@ -0,0 +1,284 @@
.page {
min-height: 100vh;
background: #f4f5f7;
}
.nav {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 176rpx;
padding-top: 88rpx;
background: #fff;
display: flex;
align-items: center;
justify-content: space-between;
padding-left: 24rpx;
padding-right: 24rpx;
box-sizing: border-box;
z-index: 100;
}
.nav-back {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
}
.back-arrow {
width: 20rpx;
height: 20rpx;
border-left: 4rpx solid #222;
border-bottom: 4rpx solid #222;
transform: rotate(45deg);
}
.nav-title {
font-size: 34rpx;
font-weight: 600;
color: #222;
}
.nav-capsule {
width: 174rpx;
height: 64rpx;
background: #f5f5f5;
border-radius: 32rpx;
}
.content {
margin-top: 176rpx;
padding: 20rpx 24rpx;
padding-bottom: 60rpx;
min-height: calc(100vh - 176rpx);
box-sizing: border-box;
}
.section {
background: #fff;
border-radius: 20rpx;
padding: 24rpx;
margin-bottom: 20rpx;
}
.section-title {
display: flex;
align-items: center;
font-size: 30rpx;
font-weight: 600;
color: #222;
margin-bottom: 20rpx;
}
.title-bar {
width: 6rpx;
height: 28rpx;
background: #173294;
margin-right: 12rpx;
border-radius: 3rpx;
}
.title-text {
font-size: 30rpx;
font-weight: 600;
color: #222;
}
.info-card {
display: flex;
flex-direction: column;
}
.info-item {
display: flex;
align-items: center;
padding: 16rpx 0;
}
.info-item.column {
flex-direction: column;
align-items: flex-start;
}
.info-label {
width: 160rpx;
font-size: 26rpx;
color: #999;
flex-shrink: 0;
}
.info-value {
flex: 1;
font-size: 26rpx;
color: #222;
text-align: right;
}
.info-desc {
font-size: 26rpx;
color: #222;
line-height: 1.6;
margin-top: 8rpx;
text-align: left;
}
.status-tag {
padding: 4rpx 12rpx;
border-radius: 6rpx;
font-weight: 600;
}
.status-pending {
background: #fff7e6;
color: #faad14;
}
.status-pending .tag-text {
color: #faad14;
}
.status-processing {
background: #e7f7ea;
color: #3abe59;
}
.status-processing .tag-text {
color: #3abe59;
}
.status-done {
background: #f0f0f0;
color: #999;
}
.status-done .tag-text {
color: #999;
}
.tag-text {
font-size: 24rpx;
font-weight: 600;
}
.level-tag {
padding: 4rpx 12rpx;
border-radius: 6rpx;
}
.level-tag.urgent {
background: #fff1f0;
color: #ff4d4f;
}
.level-tag.urgent .level-text {
color: #ff4d4f;
}
.level-tag.normal {
background: #e6f7ff;
color: #1890ff;
}
.level-tag.normal .level-text {
color: #1890ff;
}
.level-text {
font-size: 24rpx;
}
.photo-list {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.photo-item {
width: 160rpx;
height: 160rpx;
border-radius: 12rpx;
overflow: hidden;
background: #f5f5f5;
}
.photo-item image {
width: 100%;
height: 100%;
}
.timeline {
position: relative;
}
.timeline-item {
display: flex;
position: relative;
padding-bottom: 32rpx;
}
.timeline-item:last-child {
padding-bottom: 0;
}
.timeline-dot {
width: 16rpx;
height: 16rpx;
background: #d0d5dd;
border-radius: 50%;
margin-right: 20rpx;
margin-top: 8rpx;
flex-shrink: 0;
}
.timeline-dot.active {
background: #173294;
}
.timeline-line {
position: absolute;
left: 6rpx;
top: 28rpx;
width: 4rpx;
height: calc(100% - 20rpx);
background: #e5ebff;
}
.timeline-content {
flex: 1;
}
.timeline-header {
display: flex;
align-items: center;
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;
font-size: 28rpx;
color: #222;
font-weight: 500;
margin-bottom: 6rpx;
}
.timeline-desc {
display: block;
font-size: 24rpx;
color: #666;
line-height: 1.5;
}

View File

@@ -0,0 +1,241 @@
<template>
<!-- #ifdef APP -->
<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">
<text class="nav-back-icon">←</text>
</view>
<text class="nav-title">工单详情</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>
<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>
</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>
<view class="info-item column">
<text class="info-label">故障描述</text>
<text class="info-desc">{{ workcase.remark || '暂无描述' }}</text>
</view>
<view class="info-item">
<text class="info-label">创建时间</text>
<text class="info-value">{{ workcase.createTime || '-' }}</text>
</view>
<view class="info-item">
<text class="info-label">处理人</text>
<text class="info-value">{{ workcase.processor || '待分配' }}</text>
</view>
</view>
</view>
<!-- 故障图片 -->
<view class="section" v-if="workcase.imgs && workcase.imgs.length > 0">
<view class="section-title">
<view class="title-bar"></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>
</view>
</view>
<!-- 处理记录 -->
<view class="section">
<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-line" v-if="index < processList.length - 1"></view>
<view class="timeline-content">
<view class="timeline-header">
<text class="timeline-time">{{ item.time }}</text>
<text class="timeline-date">{{ item.date }}</text>
</view>
<text class="timeline-title">{{ item.title }}</text>
<text class="timeline-desc" v-if="item.desc">{{ item.desc }}</text>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
<!-- #ifdef APP -->
</scroll-view>
<!-- #endif -->
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import type { TbWorkcaseDTO } from '@/types/workcase'
// 接口定义
interface ProcessItem {
time: string
date: string
title: string
desc?: string
}
// 响应式数据
const headerPaddingTop = ref<number>(44)
const headerTotalHeight = ref<number>(88)
const workcaseId = ref<string>('')
// 工单数据
const workcase = ref<TbWorkcaseDTO>({
workcaseId: 'TH20241217001',
userId: '1',
username: '李经理',
phone: '13800138001',
type: '控制系统故障',
device: 'TH-500GF',
deviceCode: 'TH20230501001',
emergency: 'emergency',
status: 'processing',
processor: '张三',
remark: '发电机组无法启动控制面板显示E03错误代码',
createTime: '2024-12-17 15: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: '' }
])
// 生命周期
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 pages = getCurrentPages()
const currentPage = pages[pages.length - 1] as any
if (currentPage && currentPage.options && currentPage.options.workcaseId) {
workcaseId.value = currentPage.options.workcaseId
loadWorkcaseDetail(workcaseId.value)
}
})
// 加载工单详情
function loadWorkcaseDetail(id: string) {
console.log('加载工单详情:', id)
// TODO: 调用 workcaseAPI.getWorkcaseById(id) 获取数据
}
// 获取状态样式类
function getStatusClass(status?: string): string {
switch (status) {
case 'pending': return 'status-pending'
case 'processing': return 'status-processing'
case 'done': return 'status-done'
default: return 'status-pending'
}
}
// 获取状态文本
function getStatusText(status?: string): string {
switch (status) {
case 'pending': return '待处理'
case 'processing': return '处理中'
case 'done': return '已完成'
default: return '未知'
}
}
// 预览图片
function previewImage(url: string) {
uni.previewImage({
urls: workcase.value.imgs || [],
current: url
})
}
// 返回上一页
function goBack() {
uni.navigateBack()
}
</script>
<style lang="scss" scoped>
@import "./workcaseDetail.scss";
</style>

View File

@@ -0,0 +1,289 @@
.page {
min-height: 100vh;
background: #f4f5f7;
}
.nav {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 176rpx;
padding-top: 88rpx;
background: #fff;
display: flex;
align-items: center;
justify-content: flex-start;
padding-left: 24rpx;
padding-right: 24rpx;
box-sizing: border-box;
z-index: 100;
}
.nav-back {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
}
.back-arrow {
width: 20rpx;
height: 20rpx;
border-left: 4rpx solid #222;
border-bottom: 4rpx solid #222;
transform: rotate(45deg);
}
.nav-title {
font-size: 34rpx;
font-weight: 600;
color: #222;
flex: 1;
text-align: center;
padding-right: 174rpx;
box-sizing: border-box;
}
.nav-capsule {
width: 174rpx;
height: 64rpx;
border-radius: 32rpx;
margin-left: auto;
}
.tabs {
position: fixed;
// top: 176rpx;
left: 0;
right: 0;
height: 100rpx;
background: #fff;
display: flex;
align-items: center;
z-index: 100;
}
.tab-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
font-size: 30rpx;
color: #999;
}
.tab-item.active {
color: #173294;
font-weight: 600;
}
.tab-line {
width: 48rpx;
height: 6rpx;
background: #173294;
border-radius: 3rpx;
margin-top: 8rpx;
}
.tab-text {
font-size: 30rpx;
}
.tab-item.active .tab-text {
color: #173294;
font-weight: 600;
}
.list {
margin-top: 276rpx;
padding: 20rpx 24rpx;
padding-bottom: 60rpx;
box-sizing: border-box;
min-height: calc(100vh - 276rpx);
}
.card {
background: #fff;
border-radius: 20rpx;
margin-bottom: 20rpx;
overflow: hidden;
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.card-title {
display: flex;
align-items: center;
font-size: 30rpx;
font-weight: 600;
color: #222;
}
.title-bar {
width: 6rpx;
height: 28rpx;
background: #173294;
margin-right: 12rpx;
border-radius: 3rpx;
}
.title-text {
font-size: 30rpx;
font-weight: 600;
color: #222;
}
.status-tag {
font-size: 24rpx;
font-weight: 600;
padding: 6rpx 16rpx;
border-radius: 8rpx;
}
.status-pending {
background: #fff7e6;
color: #faad14;
}
.status-pending .tag-text {
color: #faad14;
}
.status-processing {
background: #e7f7ea;
color: #3abe59;
}
.status-processing .tag-text {
color: #3abe59;
}
.status-done {
background: #f0f0f0;
color: #999;
}
.status-done .tag-text {
color: #999;
}
.tag-text {
font-size: 24rpx;
font-weight: 600;
}
.card-body {
padding: 24rpx;
}
.fault-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20rpx;
}
.fault-title {
font-size: 28rpx;
font-weight: 600;
color: #222;
}
.level-tag {
font-size: 24rpx;
padding: 4rpx 12rpx;
border-radius: 6rpx;
}
.level-tag.urgent {
background: #fff1f0;
color: #ff4d4f;
border: 1rpx solid rgba(255,77,79,0.3);
}
.level-tag.urgent .level-text {
color: #ff4d4f;
}
.level-tag.normal {
background: #e6f7ff;
color: #1890ff;
border: 1rpx solid rgba(24,144,255,0.3);
}
.level-tag.normal .level-text {
color: #1890ff;
}
.level-text {
font-size: 24rpx;
}
.info-row {
display: flex;
align-items: center;
margin-bottom: 16rpx;
}
.info-label {
width: 140rpx;
font-size: 26rpx;
color: #999;
}
.info-value {
flex: 1;
font-size: 26rpx;
color: #222;
}
.card-footer {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 24rpx;
border-top: 1rpx solid #f0f0f0;
}
.create-time {
font-size: 24rpx;
color: #999;
}
.detail-btn {
padding: 12rpx 24rpx;
border: 2rpx solid #173294;
border-radius: 32rpx;
font-size: 24rpx;
color: #173294;
font-weight: 600;
}
.detail-text {
font-size: 24rpx;
color: #173294;
font-weight: 600;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 0;
}
.empty-text {
font-size: 28rpx;
color: #999;
}

View File

@@ -0,0 +1,213 @@
<template>
<!-- #ifdef APP -->
<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">
<text class="nav-back-icon">←</text>
</view>
<text class="nav-title">我的工单</text>
<view class="nav-capsule"></view>
</view>
<!-- Tab切换 -->
<view class="tabs" :style="{ marginTop: headerTotalHeight + 'px' }">
<view class="tab-item" :class="{ active: activeTab === 'all' }" @tap="changeTab('all')">
<text class="tab-text">全部</text>
</view>
<view class="tab-item" :class="{ active: activeTab === 'pending' }" @tap="changeTab('pending')">
<text class="tab-text">待处理</text>
</view>
<view class="tab-item" :class="{ active: activeTab === 'processing' }" @tap="changeTab('processing')">
<text class="tab-text">处理中</text>
</view>
<view class="tab-item" :class="{ active: activeTab === 'done' }" @tap="changeTab('done')">
<text class="tab-text">已完成</text>
</view>
</view>
<!-- 工单列表 -->
<scroll-view class="list" scroll-y="true">
<view class="card" v-for="(item, index) in filteredOrders" :key="index">
<view class="card-header">
<view class="card-title">
<view class="title-bar"></view>
<text class="title-text">工单号:{{ item.workcaseId }}</text>
</view>
<view class="status-tag" :class="getStatusClass(item.status)">
<text class="tag-text">{{ getStatusText(item.status) }}</text>
</view>
</view>
<view class="card-body">
<view class="fault-row">
<text class="fault-title">{{ item.type || '故障报修' }}</text>
<view class="level-tag" :class="item.emergency === 'emergency' ? 'urgent' : 'normal'">
<text class="level-text">{{ item.emergency === 'emergency' ? '紧急' : '普通' }}</text>
</view>
</view>
<view class="info-row">
<text class="info-label">设备型号</text>
<text class="info-value">{{ item.device || '-' }}</text>
</view>
<view class="info-row">
<text class="info-label">联系人</text>
<text class="info-value">{{ item.username || '-' }}</text>
</view>
<view class="info-row">
<text class="info-label">联系电话</text>
<text class="info-value">{{ item.phone || '-' }}</text>
</view>
</view>
<view class="card-footer">
<text class="create-time">{{ item.createTime || '' }}</text>
<view class="detail-btn" @tap="goDetail(item.workcaseId)">
<text class="detail-text">查看详情</text>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-if="filteredOrders.length === 0">
<text class="empty-text">暂无工单数据</text>
</view>
</scroll-view>
</view>
<!-- #ifdef APP -->
</scroll-view>
<!-- #endif -->
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import type { TbWorkcaseDTO } from '@/types/workcase'
// 响应式数据
const headerPaddingTop = ref<number>(44)
const headerTotalHeight = ref<number>(88)
const activeTab = ref<string>('all')
// 模拟工单数据
const orders = ref<TbWorkcaseDTO[]>([
{
workcaseId: 'TH20241217001',
userId: '1',
username: '李经理',
phone: '13800138001',
type: '控制系统故障',
device: 'TH-500GF',
deviceCode: 'TH20230501001',
emergency: 'emergency',
status: 'processing',
createTime: '2024-12-17 15:30:00'
},
{
workcaseId: 'TH20241217002',
userId: '2',
username: '王工',
phone: '13800138002',
type: '发动机故障',
device: 'TH-300GF',
deviceCode: 'TH20230502001',
emergency: 'normal',
status: 'pending',
createTime: '2024-12-17 14:20:00'
},
{
workcaseId: 'TH20241216001',
userId: '3',
username: '张总',
phone: '13800138003',
type: '电气系统故障',
device: 'TH-800GF',
deviceCode: 'TH20230503001',
emergency: 'normal',
status: 'done',
createTime: '2024-12-16 09:15:00'
}
])
// 计算属性根据tab筛选工单
const filteredOrders = computed(() => {
if (activeTab.value === 'all') {
return orders.value
}
return orders.value.filter(o => o.status === activeTab.value)
})
// 生命周期
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
}
})
// TODO: 实际调用API获取工单列表
loadWorkcaseList()
})
// 加载工单列表
function loadWorkcaseList() {
// TODO: 调用 workcaseAPI.getWorkcaseList() 获取数据
console.log('加载工单列表')
}
// 切换Tab
function changeTab(tab: string) {
activeTab.value = tab
}
// 获取状态样式类
function getStatusClass(status?: string): string {
switch (status) {
case 'pending': return 'status-pending'
case 'processing': return 'status-processing'
case 'done': return 'status-done'
default: return 'status-pending'
}
}
// 获取状态文本
function getStatusText(status?: string): string {
switch (status) {
case 'pending': return '待处理'
case 'processing': return '处理中'
case 'done': return '已完成'
default: return '未知'
}
}
// 返回上一页
function goBack() {
uni.navigateBack()
}
// 跳转到工单详情
function goDetail(workcaseId?: string) {
if (!workcaseId) return
uni.navigateTo({
url: `/pages/workcase/workcaseDetail/workcaseDetail?workcaseId=${workcaseId}`
})
}
</script>
<style lang="scss" scoped>
@import "./workcaseList.scss";
</style>