功能:H5 工单详情页
- 展示告警截图、类型、级别、摄像头、时间 - 处理中状态:处理描述输入+拍照上传+提交按钮 - 已完成状态:展示处理结果(只读) - 移动端适配,企微内打开
This commit is contained in:
404
apps/web-antd/src/views/work-order/index.vue
Normal file
404
apps/web-antd/src/views/work-order/index.vue
Normal 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>
|
||||||
Reference in New Issue
Block a user