feat(@vben/web-antd): 安保工单告警图片优化及专用派单表单
- 告警图片改为缩略图展示(200x150),添加圆角和悬停放大效果 - 新增安保专用派单表单,基于分页用户列表选择执行人 - 工单中心列表页和详情页均根据工单类型路由到对应派单表单 - 搜索防抖300ms,组件卸载时清理定时器防止内存泄漏 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -96,7 +96,7 @@ const alarmImages = computed(() => {
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item
|
||||
v-if="alarmImages.length > 0"
|
||||
label="告警截图"
|
||||
label="告警图片"
|
||||
:span="3"
|
||||
>
|
||||
<div class="image-gallery">
|
||||
@@ -105,9 +105,13 @@ const alarmImages = computed(() => {
|
||||
v-for="(url, idx) in alarmImages"
|
||||
:key="idx"
|
||||
:src="url"
|
||||
:alt="`告警截图 ${idx + 1}`"
|
||||
class="gallery-image"
|
||||
:style="{ maxHeight: '360px', objectFit: 'contain' }"
|
||||
:alt="`告警图片 ${idx + 1}`"
|
||||
:width="200"
|
||||
:height="150"
|
||||
:style="{
|
||||
objectFit: 'cover',
|
||||
borderRadius: '8px',
|
||||
}"
|
||||
/>
|
||||
</Image.PreviewGroup>
|
||||
</div>
|
||||
@@ -142,17 +146,19 @@ const alarmImages = computed(() => {
|
||||
/>
|
||||
处理图片({{ resultImages.length }}张)
|
||||
</div>
|
||||
<div class="image-gallery image-gallery--grid">
|
||||
<div class="image-gallery">
|
||||
<Image.PreviewGroup>
|
||||
<Image
|
||||
v-for="(url, idx) in resultImages"
|
||||
:key="idx"
|
||||
:src="url"
|
||||
:alt="`处理结果图片 ${idx + 1}`"
|
||||
class="gallery-image-thumb"
|
||||
:width="160"
|
||||
:height="120"
|
||||
:style="{ objectFit: 'cover' }"
|
||||
:style="{
|
||||
objectFit: 'cover',
|
||||
borderRadius: '8px',
|
||||
}"
|
||||
/>
|
||||
</Image.PreviewGroup>
|
||||
</div>
|
||||
@@ -190,33 +196,26 @@ const alarmImages = computed(() => {
|
||||
|
||||
/* 图片画廊 */
|
||||
.image-gallery {
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.image-gallery--grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.gallery-image {
|
||||
.image-gallery :deep(.ant-image) {
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.gallery-image-thumb {
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 8px;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.gallery-image-thumb:hover {
|
||||
.image-gallery :deep(.ant-image:hover) {
|
||||
transform: scale(1.03);
|
||||
}
|
||||
|
||||
.image-gallery :deep(.ant-image-img) {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* 处理结果 */
|
||||
.result-content {
|
||||
font-size: 14px;
|
||||
|
||||
@@ -55,6 +55,7 @@ import {
|
||||
} from '../data';
|
||||
import AssignForm from '../modules/assign-form.vue';
|
||||
import CancelForm from '../modules/cancel-form.vue';
|
||||
import SecurityAssignForm from '../modules/security-assign-form.vue';
|
||||
import UpgradePriorityForm from '../modules/upgrade-priority-form.vue';
|
||||
|
||||
defineOptions({ name: 'WorkOrderDetail' });
|
||||
@@ -286,6 +287,10 @@ const [AssignFormModal, assignFormModalApi] = useVbenModal({
|
||||
connectedComponent: AssignForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
const [SecurityAssignFormModal, securityAssignFormModalApi] = useVbenModal({
|
||||
connectedComponent: SecurityAssignForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
const [UpgradePriorityFormModal, upgradePriorityFormModalApi] = useVbenModal({
|
||||
connectedComponent: UpgradePriorityForm,
|
||||
destroyOnClose: true,
|
||||
@@ -317,7 +322,7 @@ const orderImages = computed(() => {
|
||||
const ext = order.value.extInfo;
|
||||
if (!ext) return [];
|
||||
const images: string[] = [];
|
||||
// 安保告警截图已在事件信息卡片中展示,此处不再重复
|
||||
// 安保告警图片已在事件信息卡片中展示,此处不再重复
|
||||
if (
|
||||
!isSecurityOrder.value &&
|
||||
(ext as OpsOrderCenterApi.SecurityExtInfo).imageUrl
|
||||
@@ -489,9 +494,23 @@ async function handleBack() {
|
||||
}, 100);
|
||||
}
|
||||
function handleAssign() {
|
||||
assignFormModalApi
|
||||
.setData({ orderId: order.value.id, orderCode: order.value.orderCode })
|
||||
.open();
|
||||
if (isSecurityOrder.value) {
|
||||
securityAssignFormModalApi
|
||||
.setData({
|
||||
orderId: order.value.id,
|
||||
orderCode: order.value.orderCode,
|
||||
location: order.value.location,
|
||||
description: order.value.description,
|
||||
})
|
||||
.open();
|
||||
} else {
|
||||
assignFormModalApi
|
||||
.setData({
|
||||
orderId: order.value.id,
|
||||
orderCode: order.value.orderCode,
|
||||
})
|
||||
.open();
|
||||
}
|
||||
}
|
||||
function handleUpgrade() {
|
||||
upgradePriorityFormModalApi
|
||||
@@ -588,6 +607,7 @@ onUnmounted(stopPolling);
|
||||
<template>
|
||||
<Page>
|
||||
<AssignFormModal @success="handleRefresh" />
|
||||
<SecurityAssignFormModal @success="handleRefresh" />
|
||||
<UpgradePriorityFormModal @success="handleRefresh" />
|
||||
<CancelFormModal @success="handleRefresh" />
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
import AssignForm from './modules/assign-form.vue';
|
||||
import CancelForm from './modules/cancel-form.vue';
|
||||
import CardView from './modules/card-view.vue';
|
||||
import SecurityAssignForm from './modules/security-assign-form.vue';
|
||||
import StatsBar from './modules/stats-bar.vue';
|
||||
import UpgradePriorityForm from './modules/upgrade-priority-form.vue';
|
||||
|
||||
@@ -64,6 +65,10 @@ const [AssignFormModal, assignFormModalApi] = useVbenModal({
|
||||
connectedComponent: AssignForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
const [SecurityAssignFormModal, securityAssignFormModalApi] = useVbenModal({
|
||||
connectedComponent: SecurityAssignForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
const [UpgradePriorityFormModal, upgradePriorityFormModalApi] = useVbenModal({
|
||||
connectedComponent: UpgradePriorityForm,
|
||||
@@ -143,9 +148,20 @@ function handleDetail(id: number) {
|
||||
|
||||
/** 打开派单表单 */
|
||||
function handleAssign(row: OpsOrderCenterApi.OrderItem) {
|
||||
assignFormModalApi
|
||||
.setData({ orderId: row.id, orderCode: row.orderCode })
|
||||
.open();
|
||||
if (row.orderType === 'SECURITY') {
|
||||
securityAssignFormModalApi
|
||||
.setData({
|
||||
orderId: row.id,
|
||||
orderCode: row.orderCode,
|
||||
location: row.location,
|
||||
description: row.title,
|
||||
})
|
||||
.open();
|
||||
} else {
|
||||
assignFormModalApi
|
||||
.setData({ orderId: row.id, orderCode: row.orderCode })
|
||||
.open();
|
||||
}
|
||||
}
|
||||
|
||||
/** 升级优先级 */
|
||||
@@ -300,6 +316,7 @@ onActivated(() => {
|
||||
<Page auto-content-height>
|
||||
<!-- 模态框 -->
|
||||
<AssignFormModal @success="handleRefresh" />
|
||||
<SecurityAssignFormModal @success="handleRefresh" />
|
||||
<UpgradePriorityFormModal @success="handleRefresh" />
|
||||
<CancelFormModal @success="handleRefresh" />
|
||||
|
||||
|
||||
@@ -0,0 +1,342 @@
|
||||
<script setup lang="ts">
|
||||
import type { SystemUserApi } from '#/api/system/user';
|
||||
|
||||
import { onUnmounted, ref, watch } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { Avatar, Empty, Input, message, Pagination, Spin } from 'ant-design-vue';
|
||||
|
||||
import { assignOrder } from '#/api/ops/order-center';
|
||||
import { getUserPage } from '#/api/system/user';
|
||||
|
||||
defineOptions({ name: 'SecurityAssignForm' });
|
||||
|
||||
const emit = defineEmits<{ success: [] }>();
|
||||
|
||||
interface ModalData {
|
||||
orderId: number;
|
||||
orderCode: string;
|
||||
location?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const modalData = ref<ModalData>({
|
||||
orderId: 0,
|
||||
orderCode: '',
|
||||
});
|
||||
const loading = ref(false);
|
||||
const userLoading = ref(false);
|
||||
const userList = ref<SystemUserApi.User[]>([]);
|
||||
const total = ref(0);
|
||||
const currentPage = ref(1);
|
||||
const pageSize = ref(20);
|
||||
const selectedUserId = ref<number>();
|
||||
const searchValue = ref('');
|
||||
const remark = ref('');
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
onOpenChange: async (isOpen) => {
|
||||
if (isOpen) {
|
||||
const data = modalApi.getData<ModalData>();
|
||||
if (data) {
|
||||
modalData.value = data;
|
||||
}
|
||||
selectedUserId.value = undefined;
|
||||
searchValue.value = '';
|
||||
remark.value = '';
|
||||
currentPage.value = 1;
|
||||
await loadUsers();
|
||||
}
|
||||
},
|
||||
onConfirm: handleSubmit,
|
||||
});
|
||||
|
||||
/** 加载用户分页 */
|
||||
async function loadUsers() {
|
||||
userLoading.value = true;
|
||||
try {
|
||||
const res = await getUserPage({
|
||||
pageNo: currentPage.value,
|
||||
pageSize: pageSize.value,
|
||||
nickname: searchValue.value.trim() || undefined,
|
||||
});
|
||||
userList.value = res?.list || [];
|
||||
total.value = res?.total || 0;
|
||||
} catch {
|
||||
userList.value = [];
|
||||
total.value = 0;
|
||||
} finally {
|
||||
userLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索防抖 */
|
||||
let searchTimer: ReturnType<typeof setTimeout>;
|
||||
watch(searchValue, () => {
|
||||
clearTimeout(searchTimer);
|
||||
searchTimer = setTimeout(() => {
|
||||
currentPage.value = 1;
|
||||
loadUsers();
|
||||
}, 300);
|
||||
});
|
||||
|
||||
/** 翻页 */
|
||||
function handlePageChange(page: number) {
|
||||
currentPage.value = page;
|
||||
loadUsers();
|
||||
}
|
||||
|
||||
/** 选择用户 */
|
||||
function selectUser(user: SystemUserApi.User) {
|
||||
selectedUserId.value = user.id;
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
clearTimeout(searchTimer);
|
||||
});
|
||||
|
||||
/** 提交 */
|
||||
async function handleSubmit() {
|
||||
if (!selectedUserId.value) {
|
||||
message.warning('请选择执行人');
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
modalApi.setState({ confirmLoading: true });
|
||||
try {
|
||||
await assignOrder({
|
||||
orderId: modalData.value.orderId,
|
||||
assigneeId: selectedUserId.value,
|
||||
remark: remark.value.trim() || undefined,
|
||||
});
|
||||
message.success('派单成功');
|
||||
modalApi.close();
|
||||
emit('success');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
modalApi.setState({ confirmLoading: false });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal title="分配安保执行人" class="w-[580px]">
|
||||
<!-- 工单信息 -->
|
||||
<div class="sa-order-info">
|
||||
<div class="sa-info-row">
|
||||
<IconifyIcon icon="solar:document-text-bold-duotone" class="sa-info-icon" />
|
||||
<span class="sa-info-label">工单编号</span>
|
||||
<span class="sa-info-value">{{ modalData.orderCode }}</span>
|
||||
</div>
|
||||
<div v-if="modalData.location" class="sa-info-row">
|
||||
<IconifyIcon icon="solar:map-point-bold-duotone" class="sa-info-icon" />
|
||||
<span class="sa-info-label">位置</span>
|
||||
<span class="sa-info-value">{{ modalData.location }}</span>
|
||||
</div>
|
||||
<div v-if="modalData.description" class="sa-info-row">
|
||||
<IconifyIcon icon="solar:notes-bold-duotone" class="sa-info-icon" />
|
||||
<span class="sa-info-label">描述</span>
|
||||
<span class="sa-info-value sa-info-desc">{{ modalData.description }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 人员选择 -->
|
||||
<div class="sa-section">
|
||||
<div class="sa-section-title">选择执行人</div>
|
||||
<Input
|
||||
v-model:value="searchValue"
|
||||
placeholder="搜索人员姓名"
|
||||
allow-clear
|
||||
class="mb-2"
|
||||
>
|
||||
<template #prefix>
|
||||
<IconifyIcon icon="lucide:search" class="size-4 text-gray-400" />
|
||||
</template>
|
||||
</Input>
|
||||
<Spin :spinning="userLoading">
|
||||
<div v-if="userList.length > 0" class="sa-list">
|
||||
<div
|
||||
v-for="user in userList"
|
||||
:key="user.id"
|
||||
class="sa-item"
|
||||
:class="{ 'sa-item--active': selectedUserId === user.id }"
|
||||
@click="selectUser(user)"
|
||||
>
|
||||
<Avatar :size="28" class="sa-avatar">
|
||||
{{ user.nickname?.charAt(0) || '?' }}
|
||||
</Avatar>
|
||||
<span class="sa-name">{{ user.nickname }}</span>
|
||||
<IconifyIcon
|
||||
v-if="selectedUserId === user.id"
|
||||
icon="lucide:check"
|
||||
class="sa-check"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Empty
|
||||
v-else-if="!userLoading"
|
||||
:image="Empty.PRESENTED_IMAGE_SIMPLE"
|
||||
description="暂无匹配人员"
|
||||
/>
|
||||
</Spin>
|
||||
<div v-if="total > pageSize" class="sa-pagination">
|
||||
<Pagination
|
||||
:current="currentPage"
|
||||
:page-size="pageSize"
|
||||
:total="total"
|
||||
size="small"
|
||||
simple
|
||||
@change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 派单备注 -->
|
||||
<div class="sa-section">
|
||||
<div class="sa-section-title">派单备注</div>
|
||||
<Input.TextArea
|
||||
v-model:value="remark"
|
||||
placeholder="请输入派单备注(选填)"
|
||||
:rows="3"
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sa-order-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
margin-bottom: 12px;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.sa-info-row {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: flex-start;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.sa-info-icon {
|
||||
flex-shrink: 0;
|
||||
margin-top: 2px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.sa-info-label {
|
||||
flex-shrink: 0;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.sa-info-value {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
color: #333;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.sa-info-desc {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.sa-section {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.sa-section-title {
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.sa-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 6px;
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sa-item {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
padding: 6px 10px;
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.sa-item:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.sa-item--active {
|
||||
font-weight: 600;
|
||||
color: #1677ff;
|
||||
background: #e6f4ff;
|
||||
border-color: #91caff;
|
||||
}
|
||||
|
||||
.sa-avatar {
|
||||
flex-shrink: 0;
|
||||
font-size: 12px;
|
||||
background: #1677ff;
|
||||
}
|
||||
|
||||
.sa-item--active .sa-avatar {
|
||||
background: #0958d9;
|
||||
}
|
||||
|
||||
.sa-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
font-size: 13px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sa-check {
|
||||
flex-shrink: 0;
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
.sa-pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
/* dark mode */
|
||||
html.dark .sa-order-info {
|
||||
background: rgb(255 255 255 / 6%);
|
||||
}
|
||||
|
||||
html.dark .sa-info-value {
|
||||
color: rgb(255 255 255 / 85%);
|
||||
}
|
||||
|
||||
html.dark .sa-item:hover {
|
||||
background: rgb(255 255 255 / 8%);
|
||||
}
|
||||
|
||||
html.dark .sa-item--active {
|
||||
color: #69b1ff;
|
||||
background: rgb(22 119 255 / 15%);
|
||||
border-color: rgb(22 119 255 / 40%);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user