feat(@vben/web-antd): 安保工单详情左图右表布局改造及工单模块代码质量优化
- 安保详情事件信息改为左图右表布局,新增摄像头名称和处理人手机号字段 - 工单卡片展示处理人手机号,时间字段类型兼容时间戳和格式化字符串 - 工单详情页安保扩展区位置下移,安保工单隐藏重复的执行人行 - 登录后默认设置访问租户为登录租户 - 修复 scoped 暗色模式选择器不生效问题,移除 as any 类型断言 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -78,16 +78,18 @@ export namespace OpsOrderCenterApi {
|
||||
alarmId?: string; // 关联告警ID
|
||||
alarmType?: string; // 告警类型: intrusion/leave_post/fire/fence
|
||||
cameraId?: string; // 摄像头ID
|
||||
cameraName?: string; // 摄像头名称
|
||||
roiId?: string; // ROI区域ID
|
||||
imageUrl?: string; // 告警截图URL
|
||||
assignedUserId?: number; // 处理人user_id
|
||||
assignedUserName?: string; // 处理人姓名
|
||||
assignedUserPhone?: string; // 处理人手机号
|
||||
assignedTeamId?: number; // 班组ID
|
||||
result?: string; // 处理结果描述
|
||||
resultImgUrls?: string; // 处理结果图片URL,JSON数组
|
||||
dispatchedTime?: string; // 派单时间
|
||||
confirmedTime?: string; // 确认时间
|
||||
completedTime?: string; // 完成时间
|
||||
dispatchedTime?: number | string; // 派单时间(number=毫秒时间戳,string=格式化时间)
|
||||
confirmedTime?: number | string; // 确认时间(number=毫秒时间戳,string=格式化时间)
|
||||
completedTime?: number | string; // 完成时间(number=毫秒时间戳,string=格式化时间)
|
||||
}
|
||||
|
||||
/** 工单列表项 */
|
||||
|
||||
@@ -70,6 +70,11 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
accessStore.setAccessToken(accessToken);
|
||||
accessStore.setRefreshToken(refreshToken);
|
||||
|
||||
// 登录成功后,如果未设置访问租户,默认使用登录时选择的租户
|
||||
if (accessStore.tenantId && !accessStore.visitTenantId) {
|
||||
accessStore.setVisitTenantId(accessStore.tenantId);
|
||||
}
|
||||
|
||||
// 获取用户信息并存储到 userStore、accessStore 中
|
||||
// TODO @芋艿:清理掉 accessCodes 相关的逻辑
|
||||
// const [fetchUserInfoResult, accessCodes] = await Promise.all([
|
||||
|
||||
@@ -367,6 +367,12 @@ onMounted(() => {
|
||||
class="info-text assignee-text"
|
||||
>
|
||||
{{ item.assigneeName }}
|
||||
<span
|
||||
v-if="(item.extInfo as OpsOrderCenterApi.SecurityExtInfo)?.assignedUserPhone"
|
||||
class="assignee-phone"
|
||||
>
|
||||
({{ (item.extInfo as OpsOrderCenterApi.SecurityExtInfo).assignedUserPhone }})
|
||||
</span>
|
||||
</span>
|
||||
<span v-else class="info-text info-text--muted">待分配</span>
|
||||
</div>
|
||||
@@ -420,9 +426,9 @@ onMounted(() => {
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<!-- 已推送:提醒 -->
|
||||
<!-- 已推送:提醒(暂不开放) -->
|
||||
<Dropdown
|
||||
v-if="item.status === 'DISPATCHED'"
|
||||
v-if="false && item.status === 'DISPATCHED'"
|
||||
placement="topRight"
|
||||
>
|
||||
<Button size="small" class="action-btn action-btn--notify">
|
||||
@@ -682,6 +688,11 @@ onMounted(() => {
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
.assignee-phone {
|
||||
font-weight: 400;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.assignee-avatar {
|
||||
flex-shrink: 0;
|
||||
font-size: 10px !important;
|
||||
|
||||
@@ -6,7 +6,7 @@ import { computed } from 'vue';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { Card, Descriptions, Image, Tag } from 'ant-design-vue';
|
||||
import { Card, Descriptions, Divider, Image, Tag } from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
SECURITY_ALARM_TYPE_COLOR_MAP,
|
||||
@@ -35,16 +35,19 @@ const resultImages = computed(() => {
|
||||
}
|
||||
});
|
||||
|
||||
/** 告警图片列表(单张也包装成数组供 Image.PreviewGroup 使用) */
|
||||
/** 告警图片列表 */
|
||||
const alarmImages = computed(() => {
|
||||
const url = extInfo.value?.imageUrl;
|
||||
return url ? [url] : [];
|
||||
});
|
||||
|
||||
/** 是否有告警图片 */
|
||||
const hasAlarmImage = computed(() => alarmImages.value.length > 0);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="extInfo" class="security-detail-ext">
|
||||
<!-- 事件信息 + 告警图片 -->
|
||||
<!-- 事件信息 -->
|
||||
<Card class="info-card mb-3">
|
||||
<template #title>
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -56,67 +59,94 @@ const alarmImages = computed(() => {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<Descriptions
|
||||
:column="3"
|
||||
bordered
|
||||
size="small"
|
||||
class="custom-descriptions"
|
||||
>
|
||||
<Descriptions.Item v-if="extInfo.alarmType" label="告警类型">
|
||||
<Tag
|
||||
:color="
|
||||
SECURITY_ALARM_TYPE_COLOR_MAP[extInfo.alarmType] || '#8c8c8c'
|
||||
"
|
||||
>
|
||||
{{
|
||||
SECURITY_ALARM_TYPE_MAP[extInfo.alarmType] || extInfo.alarmType
|
||||
}}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item v-if="extInfo.alarmId" label="告警ID">
|
||||
<code class="meta-code">{{ extInfo.alarmId }}</code>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item v-if="extInfo.cameraId" label="摄像头">
|
||||
<code class="meta-code">{{ extInfo.cameraId }}</code>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item v-if="extInfo.roiId" label="ROI区域">
|
||||
<code class="meta-code">{{ extInfo.roiId }}</code>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item v-if="extInfo.assignedUserName" label="处理人">
|
||||
{{ extInfo.assignedUserName }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item v-if="extInfo.dispatchedTime" label="派单时间">
|
||||
{{ formatDateTime(extInfo.dispatchedTime) }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item v-if="extInfo.confirmedTime" label="确认时间">
|
||||
{{ formatDateTime(extInfo.confirmedTime) }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item v-if="extInfo.completedTime" label="完成时间">
|
||||
{{ formatDateTime(extInfo.completedTime) }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item
|
||||
v-if="alarmImages.length > 0"
|
||||
label="告警图片"
|
||||
:span="3"
|
||||
>
|
||||
<div class="image-gallery">
|
||||
<!-- 左图右表布局 -->
|
||||
<div class="event-layout" :class="{ 'has-image': hasAlarmImage }">
|
||||
<!-- 左侧:告警截图 -->
|
||||
<div v-if="hasAlarmImage" class="event-image">
|
||||
<div class="alarm-img-wrapper">
|
||||
<Image.PreviewGroup>
|
||||
<Image
|
||||
v-for="(url, idx) in alarmImages"
|
||||
:key="idx"
|
||||
:src="url"
|
||||
:alt="`告警图片 ${idx + 1}`"
|
||||
:width="200"
|
||||
:height="150"
|
||||
:style="{
|
||||
objectFit: 'cover',
|
||||
borderRadius: '8px',
|
||||
}"
|
||||
:alt="`告警截图 ${idx + 1}`"
|
||||
:width="'100%'"
|
||||
:height="'100%'"
|
||||
:style="{ objectFit: 'cover' }"
|
||||
/>
|
||||
</Image.PreviewGroup>
|
||||
</div>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<div class="image-caption">
|
||||
<IconifyIcon icon="solar:videocamera-bold-duotone" />
|
||||
{{ extInfo.cameraName || extInfo.cameraId || '监控截图' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:信息表格 -->
|
||||
<div class="event-info">
|
||||
<Descriptions
|
||||
:column="2"
|
||||
bordered
|
||||
size="small"
|
||||
class="custom-descriptions"
|
||||
>
|
||||
<Descriptions.Item v-if="extInfo.alarmType" label="告警类型">
|
||||
<Tag
|
||||
:color="
|
||||
SECURITY_ALARM_TYPE_COLOR_MAP[extInfo.alarmType] || '#8c8c8c'
|
||||
"
|
||||
>
|
||||
{{
|
||||
SECURITY_ALARM_TYPE_MAP[extInfo.alarmType] ||
|
||||
extInfo.alarmType
|
||||
}}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item
|
||||
v-if="extInfo.cameraName || extInfo.cameraId"
|
||||
label="摄像头"
|
||||
>
|
||||
{{ extInfo.cameraName || '' }}
|
||||
<code v-if="extInfo.cameraId" class="meta-code ml-1">
|
||||
{{ extInfo.cameraId }}
|
||||
</code>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item v-if="extInfo.alarmId" label="告警ID">
|
||||
<code class="meta-code">{{ extInfo.alarmId }}</code>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item v-if="extInfo.roiId" label="ROI区域">
|
||||
<code class="meta-code">{{ extInfo.roiId }}</code>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item
|
||||
v-if="extInfo.assignedUserName"
|
||||
label="处理人"
|
||||
>
|
||||
{{ extInfo.assignedUserName }}
|
||||
<span v-if="extInfo.assignedUserPhone" class="phone-text">
|
||||
({{ extInfo.assignedUserPhone }})
|
||||
</span>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item
|
||||
v-if="extInfo.dispatchedTime"
|
||||
label="派单时间"
|
||||
>
|
||||
{{ formatDateTime(extInfo.dispatchedTime) }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item
|
||||
v-if="extInfo.confirmedTime"
|
||||
label="确认时间"
|
||||
>
|
||||
{{ formatDateTime(extInfo.confirmedTime) }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item
|
||||
v-if="extInfo.completedTime"
|
||||
label="完成时间"
|
||||
>
|
||||
{{ formatDateTime(extInfo.completedTime) }}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<!-- 处理结果 -->
|
||||
@@ -131,12 +161,10 @@ const alarmImages = computed(() => {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 结果描述 -->
|
||||
<div v-if="extInfo.result" class="result-content">
|
||||
{{ extInfo.result }}
|
||||
</div>
|
||||
|
||||
<!-- 结果图片 -->
|
||||
<div v-if="resultImages.length > 0" class="result-images-section">
|
||||
<Divider v-if="extInfo.result" class="my-3" />
|
||||
<div class="section-label mb-2">
|
||||
@@ -167,12 +195,70 @@ const alarmImages = computed(() => {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
<style scoped lang="scss">
|
||||
.security-detail-ext {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 代码标签 */
|
||||
/* ========== 左图右表布局 ========== */
|
||||
.event-layout {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
|
||||
/* 无图片时表格占满 */
|
||||
&:not(.has-image) .event-info {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* 左侧告警图片 */
|
||||
.event-image {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
width: 280px;
|
||||
|
||||
.alarm-img-wrapper {
|
||||
width: 100%;
|
||||
height: 190px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 8px;
|
||||
|
||||
:deep(.ant-image) {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.ant-image-img) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.image-caption {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
}
|
||||
|
||||
/* 右侧表格 */
|
||||
.event-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.phone-text {
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.meta-code {
|
||||
padding: 1px 6px;
|
||||
font-size: 12px;
|
||||
@@ -181,7 +267,7 @@ const alarmImages = computed(() => {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 区块标签 */
|
||||
/* ========== 区块标签 ========== */
|
||||
.section-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -190,11 +276,7 @@ const alarmImages = computed(() => {
|
||||
color: rgb(0 0 0 / 65%);
|
||||
}
|
||||
|
||||
:deep(.dark) .section-label {
|
||||
color: rgb(255 255 255 / 65%);
|
||||
}
|
||||
|
||||
/* 图片画廊 */
|
||||
/* ========== 图片画廊 ========== */
|
||||
.image-gallery {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -216,7 +298,7 @@ const alarmImages = computed(() => {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* 处理结果 */
|
||||
/* ========== 处理结果 ========== */
|
||||
.result-content {
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
@@ -225,11 +307,7 @@ const alarmImages = computed(() => {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
:deep(.dark) .result-content {
|
||||
color: rgb(255 255 255 / 85%);
|
||||
}
|
||||
|
||||
/* 表格样式与基础信息统一 */
|
||||
/* ========== 表格样式 ========== */
|
||||
.info-card :deep(.ant-descriptions-item-label) {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
@@ -239,4 +317,34 @@ const alarmImages = computed(() => {
|
||||
.info-card :deep(.ant-descriptions-item-content) {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* ========== 暗色模式 ========== */
|
||||
:global(html.dark) {
|
||||
.event-image {
|
||||
.alarm-img-wrapper {
|
||||
border-color: #303030;
|
||||
}
|
||||
|
||||
.image-caption {
|
||||
color: rgb(255 255 255 / 45%);
|
||||
}
|
||||
}
|
||||
|
||||
.meta-code {
|
||||
color: #4096ff;
|
||||
background: rgb(22 119 255 / 10%);
|
||||
}
|
||||
|
||||
.section-label {
|
||||
color: rgb(255 255 255 / 65%);
|
||||
}
|
||||
|
||||
.result-content {
|
||||
color: rgb(255 255 255 / 85%);
|
||||
}
|
||||
|
||||
.image-gallery :deep(.ant-image) {
|
||||
border-color: #303030;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -947,9 +947,6 @@ onUnmounted(stopPolling);
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<!-- 安保扩展区 -->
|
||||
<SecurityDetailExt v-if="isSecurityOrder" :order="order" />
|
||||
|
||||
<Row :gutter="12" class="info-row">
|
||||
<!-- 左侧 -->
|
||||
<Col :span="showWorkProgress ? 17 : 24">
|
||||
@@ -990,8 +987,8 @@ onUnmounted(stopPolling);
|
||||
<Descriptions.Item v-if="order.triggerDeviceKey" label="触发设备">
|
||||
<code class="device-code">{{ order.triggerDeviceKey }}</code>
|
||||
</Descriptions.Item>
|
||||
<!-- 执行人 -->
|
||||
<Descriptions.Item label="执行人">
|
||||
<!-- 执行人(安保工单在事件信息中已展示,此处不重复) -->
|
||||
<Descriptions.Item v-if="!isSecurityOrder" label="执行人">
|
||||
<div v-if="order.assigneeId" class="flex items-center gap-2">
|
||||
<Avatar :size="24" class="assignee-avatar">
|
||||
{{ order.assigneeName?.charAt(0) || '?' }}
|
||||
@@ -1099,6 +1096,9 @@ onUnmounted(stopPolling);
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<!-- 安保扩展区 -->
|
||||
<SecurityDetailExt v-if="isSecurityOrder" :order="order" />
|
||||
|
||||
<!-- 保洁扩展信息 + 工牌信息 -->
|
||||
<Row v-if="isCleanOrder" :gutter="12">
|
||||
<Col :span="badgeStatus ? 17 : 24">
|
||||
|
||||
@@ -365,6 +365,12 @@ defineExpose({
|
||||
class="info-text assignee-text"
|
||||
>
|
||||
{{ item.assigneeName }}
|
||||
<span
|
||||
v-if="(item.extInfo as OpsOrderCenterApi.SecurityExtInfo)?.assignedUserPhone"
|
||||
class="assignee-phone"
|
||||
>
|
||||
({{ (item.extInfo as OpsOrderCenterApi.SecurityExtInfo).assignedUserPhone }})
|
||||
</span>
|
||||
</span>
|
||||
<span v-else class="info-text info-text--muted">待分配</span>
|
||||
</div>
|
||||
@@ -418,9 +424,9 @@ defineExpose({
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<!-- 已推送:提醒 -->
|
||||
<!-- 已推送:提醒(暂不开放) -->
|
||||
<Dropdown
|
||||
v-if="item.status === 'DISPATCHED'"
|
||||
v-if="false && item.status === 'DISPATCHED'"
|
||||
placement="topRight"
|
||||
>
|
||||
<Button size="small" class="action-btn action-btn--notify">
|
||||
@@ -680,6 +686,11 @@ defineExpose({
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
.assignee-phone {
|
||||
font-weight: 400;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.assignee-avatar {
|
||||
flex-shrink: 0;
|
||||
font-size: 10px !important;
|
||||
|
||||
Reference in New Issue
Block a user