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:
@@ -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 },
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user