功能: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