功能:H5 工单详情页

- 展示告警截图、类型、级别、摄像头、时间
- 处理中状态:处理描述输入+拍照上传+提交按钮
- 已完成状态:展示处理结果(只读)
- 移动端适配,企微内打开
This commit is contained in:
2026-03-23 12:03:00 +08:00
parent 6dca2a68c0
commit bf304e5dfd

View File

@@ -0,0 +1,404 @@
<script setup lang="ts">
/**
* H5 工单详情页
*
* 企微卡片点击跳转到此页面,保安可以:
* - 查看告警截图和详情
* - 填写处理描述
* - 拍照上传处理后照片
* - 提交完成工单
*/
import { onMounted, ref } from 'vue';
import { useRoute } from 'vue-router';
import {
Button,
Card,
Image,
Input,
message,
Result,
Spin,
Tag,
Upload,
} from 'ant-design-vue';
import { CameraOutlined, UploadOutlined } from '@ant-design/icons-vue';
defineOptions({ name: 'WorkOrderDetail' });
const route = useRoute();
const alarmId = ref('');
const loading = ref(true);
const submitting = ref(false);
const detail = ref<any>(null);
const error = ref('');
// 处理表单
const remark = ref('');
const uploadedImages = ref<string[]>([]);
const uploading = ref(false);
// API 基础地址vsp-service
const API_BASE = import.meta.env.VITE_VSP_SERVICE_URL || 'http://124.221.55.225:8000';
/** 加载工单详情 */
async function loadDetail() {
loading.value = true;
error.value = '';
try {
const resp = await fetch(
`${API_BASE}/api/work-order/detail?alarmId=${alarmId.value}`,
);
const data = await resp.json();
if (data.code === 0) {
detail.value = data.data;
} else {
error.value = data.msg || '加载失败';
}
} catch (e: any) {
error.value = e.message || '网络异常';
} finally {
loading.value = false;
}
}
/** 上传图片 */
async function handleUpload(file: File) {
uploading.value = true;
try {
const formData = new FormData();
formData.append('file', file);
const resp = await fetch(`${API_BASE}/api/work-order/upload-image`, {
method: 'POST',
body: formData,
});
const data = await resp.json();
if (data.code === 0 && data.data?.url) {
uploadedImages.value.push(data.data.url);
message.success('图片上传成功');
} else {
message.error(data.msg || '上传失败');
}
} catch {
message.error('图片上传失败');
} finally {
uploading.value = false;
}
return false; // 阻止 antd 默认上传
}
/** 提交处理结果 */
async function handleSubmit() {
if (!remark.value.trim()) {
message.warning('请填写处理描述');
return;
}
submitting.value = true;
try {
const resp = await fetch(`${API_BASE}/api/work-order/submit`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
alarmId: alarmId.value,
result: remark.value.trim(),
resultImgUrls:
uploadedImages.value.length > 0 ? uploadedImages.value : undefined,
}),
});
const data = await resp.json();
if (data.code === 0) {
message.success('提交成功');
await loadDetail(); // 刷新状态
} else {
message.error(data.msg || '提交失败');
}
} catch {
message.error('提交失败,请重试');
} finally {
submitting.value = false;
}
}
/** 删除已上传图片 */
function removeImage(index: number) {
uploadedImages.value.splice(index, 1);
}
onMounted(() => {
alarmId.value = (route.query.alarmId as string) || '';
if (alarmId.value) {
loadDetail();
} else {
loading.value = false;
error.value = '缺少告警ID参数';
}
});
</script>
<template>
<div class="work-order-page">
<!-- 加载中 -->
<div v-if="loading" class="center-box">
<Spin size="large" tip="加载中..." />
</div>
<!-- 错误 -->
<Result v-else-if="error" status="error" :title="error" />
<!-- 工单详情 -->
<div v-else-if="detail" class="detail-container">
<!-- 头部状态 -->
<div class="status-bar">
<Tag
:color="
detail.status === 'completed'
? 'green'
: detail.status === 'false_alarm'
? 'default'
: detail.status === 'processing'
? 'blue'
: 'orange'
"
class="status-tag"
>
{{
detail.status === 'pending'
? '待处理'
: detail.status === 'processing'
? '处理中'
: detail.status === 'completed'
? '已完成'
: '误报'
}}
</Tag>
<span class="order-id" v-if="detail.orderId">
工单 #{{ detail.orderId }}
</span>
</div>
<!-- 告警信息 -->
<Card size="small" class="info-card">
<div class="info-row">
<span class="label">告警类型</span>
<span>{{ detail.alarmType }}</span>
</div>
<div class="info-row">
<span class="label">告警级别</span>
<Tag
:color="
detail.alarmLevel === '紧急'
? 'red'
: detail.alarmLevel === '重要'
? 'orange'
: 'blue'
"
>
{{ detail.alarmLevel }}
</Tag>
</div>
<div class="info-row">
<span class="label">摄像头</span>
<span>{{ detail.cameraName }}</span>
</div>
<div class="info-row">
<span class="label">告警时间</span>
<span>{{ detail.eventTime }}</span>
</div>
</Card>
<!-- 告警截图 -->
<Card v-if="detail.snapshotUrl" size="small" title="告警截图" class="info-card">
<Image :src="detail.snapshotUrl" :width="'100%'" />
</Card>
<!-- 已完成/误报显示处理结果 -->
<Card
v-if="detail.status === 'completed' || detail.status === 'false_alarm'"
size="small"
title="处理结果"
class="info-card"
>
<div class="info-row">
<span class="label">处理人</span>
<span>{{ detail.handler || '-' }}</span>
</div>
<div class="info-row">
<span class="label">处理时间</span>
<span>{{ detail.handledAt || '-' }}</span>
</div>
<div v-if="detail.handleRemark" class="info-row">
<span class="label">处理描述</span>
<span>{{ detail.handleRemark }}</span>
</div>
</Card>
<!-- 处理中显示提交表单 -->
<Card
v-if="detail.status === 'processing' || detail.status === 'pending'"
size="small"
title="提交处理结果"
class="info-card"
>
<div class="form-section">
<div class="form-label">处理描述 *</div>
<Input.TextArea
v-model:value="remark"
placeholder="请描述处理情况..."
:rows="3"
:maxlength="500"
show-count
/>
</div>
<div class="form-section">
<div class="form-label">上传现场照片可选</div>
<div class="upload-area">
<div
v-for="(img, idx) in uploadedImages"
:key="idx"
class="uploaded-item"
>
<Image :src="img" :width="80" :height="80" />
<span class="remove-btn" @click="removeImage(idx)">×</span>
</div>
<Upload
:before-upload="handleUpload"
:show-upload-list="false"
accept="image/*"
>
<div class="upload-btn">
<CameraOutlined style="font-size: 24px" />
<span>拍照/选图</span>
</div>
</Upload>
</div>
</div>
<Button
type="primary"
block
size="large"
:loading="submitting"
:disabled="!remark.trim()"
@click="handleSubmit"
>
提交处理结果
</Button>
</Card>
</div>
</div>
</template>
<style scoped>
.work-order-page {
min-height: 100vh;
background: #f5f5f5;
padding: 12px;
}
.center-box {
display: flex;
justify-content: center;
align-items: center;
min-height: 60vh;
}
.detail-container {
max-width: 500px;
margin: 0 auto;
}
.status-bar {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.status-tag {
font-size: 14px;
padding: 4px 12px;
}
.order-id {
font-size: 12px;
color: #999;
}
.info-card {
margin-bottom: 12px;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 0;
border-bottom: 1px solid #f0f0f0;
}
.info-row:last-child {
border-bottom: none;
}
.label {
color: #666;
font-size: 13px;
}
.form-section {
margin-bottom: 16px;
}
.form-label {
font-size: 13px;
color: #333;
margin-bottom: 8px;
}
.upload-area {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.uploaded-item {
position: relative;
}
.remove-btn {
position: absolute;
top: -6px;
right: -6px;
width: 18px;
height: 18px;
background: #ff4d4f;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
cursor: pointer;
}
.upload-btn {
width: 80px;
height: 80px;
border: 1px dashed #d9d9d9;
border-radius: 8px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
color: #999;
font-size: 12px;
cursor: pointer;
}
.upload-btn:hover {
border-color: #1890ff;
color: #1890ff;
}
</style>