refactor(@vben/web-antd): 提取 usePriorityInfo 公共 composable,消除重复代码

- 将 4 处重复的 getPriorityInfo 函数提取至 work-order/data.ts 中的 usePriorityInfo composable
- 工单中心和保洁模块的 detail/card-view 统一使用公共 composable
- 移除两个 data.ts 中已无引用的 PRIORITY_STYLE_MAP 常量
- 清理不再需要的 DICT_TYPE、useDictStore 导入
- 工单中心 card-view 移除自身 onMounted 加载,改由父组件统一控制

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
lzh
2026-03-15 16:50:41 +08:00
parent 7156e1dd1e
commit 0d32c21e93
6 changed files with 451 additions and 758 deletions

View File

@@ -51,45 +51,10 @@ export const STATUS_TAB_OPTIONS = [
label: '进行中',
statuses: ['DISPATCHED', 'CONFIRMED', 'ARRIVED', 'QUEUED'],
},
{ key: 'PAUSED', label: '已暂停', statuses: ['PAUSED'] },
{ key: 'COMPLETED', label: '已完成', statuses: ['COMPLETED'] },
{ key: 'CANCELLED', label: '已取消', statuses: ['CANCELLED'] },
];
/** 优先级样式映射 */
export const PRIORITY_STYLE_MAP: Record<
number,
{
animation: boolean;
bgColor: string;
color: string;
icon: string;
label: string;
}
> = {
0: {
label: 'P0',
color: '#F44336',
bgColor: '#FFEBEE',
icon: 'lucide:zap',
animation: true,
},
1: {
label: 'P1',
color: '#FF9800',
bgColor: '#FFF3E0',
icon: 'lucide:alert-triangle',
animation: false,
},
2: {
label: 'P2',
color: '#9E9E9E',
bgColor: '#FAFAFA',
icon: 'lucide:info', // Added icon
animation: false,
},
};
/** 工单类型选项 */
export const ORDER_TYPE_OPTIONS = [
{ label: '保洁', value: OpsOrderCenterApi.OrderType.CLEAN },

View File

@@ -18,7 +18,6 @@ import { IconifyIcon } from '@vben/icons';
import { formatDateTime } from '@vben/utils';
import {
Alert,
Avatar,
Button,
Card,
@@ -43,11 +42,12 @@ import {
} from '#/api/ops/cleaning';
import { getOrderDetail } from '#/api/ops/order-center';
import { usePriorityInfo } from '../../../work-order/data';
import CleaningDetailExt from '../components/cleaning-detail-ext.vue';
import {
CLEANING_TYPE_TEXT_MAP,
ORDER_TYPE_COLOR_MAP,
ORDER_TYPE_TEXT_MAP,
PRIORITY_STYLE_MAP,
STATUS_COLOR_MAP,
STATUS_ICON_MAP,
STATUS_TEXT_MAP,
@@ -295,10 +295,7 @@ const [CancelFormModal, cancelFormModalApi] = useVbenModal({
destroyOnClose: true,
});
/** 获取优先级样式 */
function getPriorityStyle(priority: number) {
return PRIORITY_STYLE_MAP[priority] || PRIORITY_STYLE_MAP[2];
}
const { getPriorityInfo } = usePriorityInfo();
/** 计算作业时长 */
const workDuration = computed(() => {
@@ -334,15 +331,6 @@ const isOvertime = computed(() => {
);
});
/** 是否显示离岗警告 */
const showLeaveWarning = computed(() => {
return (
order.value.status === 'ARRIVED' &&
badgeStatus.value &&
!badgeStatus.value.isInArea
);
});
/** 动态生成应该显示的状态步骤(根据 timeline 过滤) */
const visibleSteps = computed(() => {
// 必须显示的节点(主流程)
@@ -639,22 +627,18 @@ onUnmounted(stopPolling);
{{ STATUS_TEXT_MAP[order.status] }}
</Tag>
<span
v-if="order.priority !== 2"
v-if="order.priority != null"
class="priority-badge inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium"
:class="{
'animate-pulse': getPriorityStyle(order.priority)
?.animation,
}"
:style="{
backgroundColor: getPriorityStyle(order.priority)?.bgColor,
color: getPriorityStyle(order.priority)?.color,
'animate-pulse': Number(order.priority) === 0,
}"
:style="getPriorityInfo(order.priority).style"
>
<IconifyIcon
v-if="getPriorityStyle(order.priority)?.icon"
:icon="getPriorityStyle(order.priority)?.icon || ''"
v-if="getPriorityInfo(order.priority).icon"
:icon="getPriorityInfo(order.priority).icon"
/>
{{ getPriorityStyle(order.priority)?.label }}
{{ getPriorityInfo(order.priority).label }}
</span>
</div>
<div class="mt-1.5 flex items-center gap-4 text-sm text-gray-500">
@@ -852,26 +836,6 @@ onUnmounted(stopPolling);
</div>
</Card>
<!-- 离岗警告 -->
<Alert
v-if="showLeaveWarning"
type="warning"
show-icon
class="leave-warning mb-3"
>
<template #message>
<span class="font-medium">保洁员已离开作业区域</span>
</template>
<template #description>
<div class="flex items-center justify-between">
<span>检测到保洁员不在指定区域,请及时确认情况</span>
<Button size="small" type="primary" @click="handleVoiceNotify">
语音提醒
</Button>
</div>
</template>
</Alert>
<Row :gutter="12">
<!-- 左侧 -->
<Col :span="16">
@@ -891,7 +855,7 @@ onUnmounted(stopPolling);
<span>作业进度</span>
</div>
</template>
<Row :gutter="16" align="middle">
<Row :gutter="24" align="middle">
<Col :span="8">
<div class="progress-circle-wrapper">
<Progress
@@ -907,8 +871,8 @@ onUnmounted(stopPolling);
'100%': '#52c41a',
}
"
:stroke-width="6"
:size="100"
:stroke-width="7"
:size="130"
>
<template #format>
<div class="progress-inner">
@@ -1136,6 +1100,13 @@ onUnmounted(stopPolling);
</Descriptions.Item>
</Descriptions>
</Card>
<!-- 保洁扩展信息 + 离岗警告 -->
<CleaningDetailExt
:order="order"
:badge-status="badgeStatus"
@voice-notify="handleVoiceNotify"
/>
</Col>
<!-- 右侧 -->
@@ -1392,7 +1363,7 @@ onUnmounted(stopPolling);
<Button
v-if="
order.priority !== 0 &&
Number(order.priority) !== 0 &&
!['COMPLETED', 'CANCELLED'].includes(order.status)
"
danger
@@ -1709,20 +1680,6 @@ onUnmounted(stopPolling);
border-radius: 6px;
}
/* ========== 离岗警告 ========== */
.leave-warning {
border-left: 3px solid #faad14;
border-radius: 8px;
}
.leave-warning :deep(.ant-alert-message) {
font-size: 14px;
}
.leave-warning :deep(.ant-alert-description) {
font-size: 13px;
}
/* ========== 作业进度卡片 ========== */
.work-progress-card {
background: linear-gradient(135deg, #fff 0%, #f0f7ff 100%);
@@ -1739,50 +1696,52 @@ onUnmounted(stopPolling);
}
.progress-value {
font-size: 22px;
font-size: 26px;
font-weight: 700;
line-height: 1.2;
color: #1677ff;
}
.progress-label {
font-size: 12px;
margin-top: 2px;
font-size: 13px;
color: #8c8c8c;
}
.work-stats-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
gap: 16px;
}
.stat-item {
display: flex;
gap: 10px;
gap: 12px;
align-items: center;
padding: 10px 12px;
padding: 14px 16px;
background: #fff;
border-radius: 8px;
box-shadow: 0 1px 2px rgb(0 0 0 / 4%);
border-radius: 10px;
box-shadow: 0 1px 3px rgb(0 0 0 / 6%);
}
.stat-icon {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
font-size: 16px;
border-radius: 8px;
width: 38px;
height: 38px;
font-size: 18px;
border-radius: 10px;
}
.stat-label {
font-size: 12px;
margin-bottom: 2px;
font-size: 13px;
color: #8c8c8c;
}
.stat-value {
font-size: 14px;
font-size: 15px;
font-weight: 600;
color: #262626;
}

View File

@@ -22,6 +22,7 @@ import dayjs from 'dayjs';
import { getOrderPage } from '#/api/ops/order-center';
import { usePriorityInfo } from '../../../work-order/data';
import {
ORDER_TYPE_COLOR_MAP,
ORDER_TYPE_TEXT_MAP,
@@ -155,59 +156,11 @@ const queryParams = ref({
/** 字典相关 */
const dictStore = useDictStore();
const priorityOptions = computed(() =>
dictStore.getDictOptions(DICT_TYPE.OPS_ORDER_PRIORITY),
);
const sourceTypeOptions = computed(() =>
dictStore.getDictOptions(DICT_TYPE.OPS_TRIGGER_SOURCE),
);
/** 获取优先级信息 */
function getPriorityInfo(priority: number) {
const dictItem = priorityOptions.value.find(
(item) => item.value === String(priority),
);
if (!dictItem) {
return {
label: `P${priority}`,
style: { color: '#8c8c8c', backgroundColor: '#f5f5f5' },
icon: 'lucide:info',
};
}
// 映射颜色类型到样式
let style = { color: '#8c8c8c', backgroundColor: '#f5f5f5' };
switch (dictItem.colorType) {
case 'danger': {
style = { color: '#ff4d4f', backgroundColor: '#fff1f0' };
break;
}
case 'info': {
style = { color: '#1677ff', backgroundColor: '#e6f4ff' };
break;
}
case 'success': {
style = { color: '#52c41a', backgroundColor: '#f6ffed' };
break;
}
case 'warning': {
style = { color: '#fa8c16', backgroundColor: '#fff7e6' };
break;
}
// No default
}
// 图标映射 (硬编码辅助)
let icon = 'lucide:info';
if (priority === 0) icon = 'solar:bolt-bold';
else if (priority === 1) icon = 'lucide:alert-triangle';
return {
label: dictItem.label,
style,
icon,
};
}
const { getPriorityInfo } = usePriorityInfo();
function getSourceTypeInfo(value: string) {
const option = sourceTypeOptions.value.find((item) => item.value === value);
@@ -673,18 +626,19 @@ onMounted(() => {
}
.info-text {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
white-space: normal;
white-space: nowrap;
&--muted {
font-style: italic;
color: #bfbfbf;
}
}
.order-code-styled {
flex: none;
padding: 1px 6px;
font-family: 'SF Mono', Monaco, monospace;
font-size: 12px;

View File

@@ -1,6 +1,11 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { computed } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { useDictStore } from '@vben/stores';
import { OpsOrderCenterApi } from '#/api/ops/order-center';
/** 状态颜色映射 */
@@ -51,44 +56,56 @@ export const STATUS_TAB_OPTIONS = [
label: '进行中',
statuses: ['DISPATCHED', 'CONFIRMED', 'ARRIVED', 'QUEUED'],
},
{ key: 'PAUSED', label: '已暂停', statuses: ['PAUSED'] },
{ key: 'COMPLETED', label: '已完成', statuses: ['COMPLETED'] },
{ key: 'CANCELLED', label: '已取消', statuses: ['CANCELLED'] },
];
/** 优先级样式映射 */
export const PRIORITY_STYLE_MAP: Record<
number,
{
animation: boolean;
bgColor: string;
color: string;
icon: string;
label: string;
/** 获取优先级信息(基于字典),返回 getPriorityInfo 函数 */
export function usePriorityInfo() {
const dictStore = useDictStore();
const priorityOptions = computed(() =>
dictStore.getDictOptions(DICT_TYPE.OPS_ORDER_PRIORITY),
);
function getPriorityInfo(priority: number | string | undefined) {
const dictItem = priorityOptions.value.find(
(item) => item.value === String(priority),
);
if (!dictItem) {
return {
label: `P${priority}`,
style: { color: '#8c8c8c', backgroundColor: '#f5f5f5' },
icon: 'lucide:info',
};
}
let style = { color: '#8c8c8c', backgroundColor: '#f5f5f5' };
switch (dictItem.colorType) {
case 'danger': {
style = { color: '#ff4d4f', backgroundColor: '#fff1f0' };
break;
}
case 'info': {
style = { color: '#1677ff', backgroundColor: '#e6f4ff' };
break;
}
case 'success': {
style = { color: '#52c41a', backgroundColor: '#f6ffed' };
break;
}
case 'warning': {
style = { color: '#fa8c16', backgroundColor: '#fff7e6' };
break;
}
// No default
}
let icon = 'lucide:info';
if (Number(priority) === 0) icon = 'solar:bolt-bold';
else if (Number(priority) === 1) icon = 'lucide:alert-triangle';
return { label: dictItem.label, style, icon };
}
> = {
0: {
label: 'P0',
color: '#F44336',
bgColor: '#FFEBEE',
icon: 'lucide:zap',
animation: true,
},
1: {
label: 'P1',
color: '#FF9800',
bgColor: '#FFF3E0',
icon: 'lucide:alert-triangle',
animation: false,
},
2: {
label: 'P2',
color: '#9E9E9E',
bgColor: '#FAFAFA',
icon: 'lucide:info', // Added icon
animation: false,
},
};
return { getPriorityInfo };
}
/** 工单类型选项 */
export const ORDER_TYPE_OPTIONS = [

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue';
import { computed, ref } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { IconifyIcon } from '@vben/icons';
@@ -28,6 +28,7 @@ import {
STATUS_COLOR_MAP,
STATUS_ICON_MAP,
STATUS_TEXT_MAP,
usePriorityInfo,
} from '../data';
defineOptions({ name: 'WorkOrderCardView' });
@@ -155,59 +156,11 @@ const queryParams = ref({
/** 字典相关 */
const dictStore = useDictStore();
const priorityOptions = computed(() =>
dictStore.getDictOptions(DICT_TYPE.OPS_ORDER_PRIORITY),
);
const sourceTypeOptions = computed(() =>
dictStore.getDictOptions(DICT_TYPE.OPS_TRIGGER_SOURCE),
);
/** 获取优先级信息 */
function getPriorityInfo(priority: number) {
const dictItem = priorityOptions.value.find(
(item) => item.value === String(priority),
);
if (!dictItem) {
return {
label: `P${priority}`,
style: { color: '#8c8c8c', backgroundColor: '#f5f5f5' },
icon: 'lucide:info',
};
}
// 映射颜色类型到样式
let style = { color: '#8c8c8c', backgroundColor: '#f5f5f5' };
switch (dictItem.colorType) {
case 'danger': {
style = { color: '#ff4d4f', backgroundColor: '#fff1f0' };
break;
}
case 'info': {
style = { color: '#1677ff', backgroundColor: '#e6f4ff' };
break;
}
case 'success': {
style = { color: '#52c41a', backgroundColor: '#f6ffed' };
break;
}
case 'warning': {
style = { color: '#fa8c16', backgroundColor: '#fff7e6' };
break;
}
// No default
}
// 图标映射 (硬编码辅助)
let icon = 'lucide:info';
if (priority === 0) icon = 'solar:bolt-bold';
else if (priority === 1) icon = 'lucide:alert-triangle';
return {
label: dictItem.label,
style,
icon,
};
}
const { getPriorityInfo } = usePriorityInfo();
function getSourceTypeInfo(value: string) {
const option = sourceTypeOptions.value.find((item) => item.value === value);
@@ -316,9 +269,7 @@ defineExpose({
},
});
onMounted(() => {
getList();
});
// 数据加载由父组件通过 expose 的 query/reload 方法统一触发
</script>
<template>
@@ -673,18 +624,19 @@ onMounted(() => {
}
.info-text {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
white-space: normal;
white-space: nowrap;
&--muted {
font-style: italic;
color: #bfbfbf;
}
}
.order-code-styled {
flex: none;
padding: 1px 6px;
font-family: 'SF Mono', Monaco, monospace;
font-size: 12px;