refactor(ops): 工单详情调整

This commit is contained in:
lzh
2026-02-03 22:52:30 +08:00
parent ce3e57e398
commit 7acfbfb433
5 changed files with 166 additions and 82 deletions

View File

@@ -418,18 +418,15 @@ export function useGridColumns(): VxeTableGridOptions<OpsAreaApi.BusArea>['colum
(o) => o.value === row.areaLevel,
);
return option
? h(
Badge,
{
status:
option.color === 'red' ? 'error' : (
option.color === 'orange'
? 'warning'
: 'success'
),
text: option.label,
},
)
? h(Badge, {
status:
option.color === 'red'
? 'error'
: option.color === 'orange'
? 'warning'
: 'success',
text: option.label,
})
: h('span', { class: 'text-gray-400' }, '-');
},
},

View File

@@ -291,7 +291,9 @@ function getRelationTypeColor(type: string) {
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span class="text-sm text-gray-700 dark:text-gray-300">管理区域内所有绑定的IoT设备及其配置</span>
<span class="text-sm text-gray-700 dark:text-gray-300"
>管理区域内所有绑定的IoT设备及其配置</span
>
</div>
<Button
type="primary"

View File

@@ -22,18 +22,17 @@ import {
message,
Progress,
Row,
Space,
Spin,
Tag,
Tooltip,
} from 'ant-design-vue';
import { sendDeviceMessage } from '#/api/iot/device/device';
import {
getBadgeRealtimeStatus,
getOrderBusinessLogs,
getOrderTimeline,
manualCompleteOrder,
sendDeviceNotify,
} from '#/api/ops/cleaning';
import { getOrderDetail } from '#/api/ops/order-center';
@@ -292,12 +291,18 @@ function getPriorityStyle(priority: number) {
const workDuration = computed(() => {
if (!order.value.extInfo?.arrivedTime) return 0;
const arrived = new Date(order.value.extInfo.arrivedTime).getTime();
return Math.floor((Date.now() - arrived) / 60_000);
// 如果已完成,使用完成时间;否则使用当前时间
const endTime = order.value.endTime
? new Date(order.value.endTime).getTime()
: Date.now();
return Math.floor((endTime - arrived) / 60_000);
});
/** 计算作业进度 */
const workProgress = computed(() => {
if (!order.value.extInfo?.expectedDuration) return 0;
// 如果已完成显示100%
if (order.value.status === 'COMPLETED') return 100;
return Math.min(
Math.round(
(workDuration.value / order.value.extInfo.expectedDuration) * 100,
@@ -307,7 +312,15 @@ const workProgress = computed(() => {
});
/** 是否超时 */
const isOvertime = computed(() => workProgress.value >= 100);
const isOvertime = computed(() => {
// 已完成状态不判断超时
if (order.value.status === 'COMPLETED') return false;
// 已用时长超过预计时长才算超时
return (
order.value.extInfo?.expectedDuration &&
workDuration.value > order.value.extInfo.expectedDuration
);
});
/** 是否显示离岗警告 */
const showLeaveWarning = computed(() => {
@@ -412,11 +425,9 @@ async function loadBadgeStatus() {
return;
}
try {
if (USE_MOCK_DATA) {
badgeStatus.value = { ...MOCK_BADGE_STATUS };
} else {
badgeStatus.value = await getBadgeRealtimeStatus(order.value.assigneeId);
}
badgeStatus.value = USE_MOCK_DATA
? { ...MOCK_BADGE_STATUS }
: await getBadgeRealtimeStatus(order.value.assigneeId);
} catch (error) {
console.error('❌ 工牌状态加载失败:', error);
badgeStatus.value = null;
@@ -463,10 +474,12 @@ function handleCancel() {
async function handleVoiceNotify() {
if (!order.value.assigneeId) return;
try {
await sendDeviceNotify({
badgeId: order.value.assigneeId,
type: 'VOICE' as OpsCleaningApi.NotifyType,
content: `请注意:${order.value.title}`,
await sendDeviceMessage({
deviceId: order.value.assigneeId,
method: 'voice.broadcast',
params: {
content: `请注意:${order.value.title}`,
},
});
message.success('语音提醒已发送');
} catch {
@@ -474,19 +487,6 @@ async function handleVoiceNotify() {
}
}
async function handleVibrateNotify() {
if (!order.value.assigneeId) return;
try {
await sendDeviceNotify({
badgeId: order.value.assigneeId,
type: 'VIBRATE' as OpsCleaningApi.NotifyType,
});
message.success('震动提醒已发送');
} catch {
message.error('发送失败');
}
}
async function handleManualComplete() {
try {
await manualCompleteOrder(order.value.id!);
@@ -687,6 +687,8 @@ onUnmounted(() => {
'node-completed': index < currentStepIndex,
'node-current':
index === currentStepIndex && order.status !== 'CANCELLED',
'node-completed-current':
index === currentStepIndex && order.status === 'COMPLETED',
'node-pending': index > currentStepIndex,
'node-cancelled': order.status === 'CANCELLED',
}"
@@ -697,18 +699,30 @@ onUnmounted(() => {
</div>
</Tooltip>
<div class="node-label">{{ step.title }}</div>
<div v-if="index < currentStepIndex" class="node-time">
<div
v-if="
index < currentStepIndex ||
(index === currentStepIndex && order.status === 'COMPLETED')
"
class="node-time"
>
{{
timeline.find((t) => t.status === step.key)?.time
? formatRelativeTime(
timeline.find((t) => t.status === step.key)?.time || '',
)
: ''
: step.key === 'PENDING'
? formatRelativeTime(order.createTime)
: step.key === 'COMPLETED' && order.endTime
? formatRelativeTime(order.endTime)
: ''
}}
</div>
<div
v-else-if="
index === currentStepIndex && order.status !== 'CANCELLED'
index === currentStepIndex &&
order.status !== 'CANCELLED' &&
order.status !== 'COMPLETED'
"
class="node-badge"
>
@@ -781,14 +795,9 @@ onUnmounted(() => {
<template #description>
<div class="flex items-center justify-between">
<span>检测到保洁员不在指定区域,请及时确认情况</span>
<Space>
<Button size="small" type="primary" @click="handleVoiceNotify">
语音提醒
</Button>
<Button size="small" @click="handleVibrateNotify">
震动提醒
</Button>
</Space>
<Button size="small" type="primary" @click="handleVoiceNotify">
语音提醒
</Button>
</div>
</template>
</Alert>
@@ -798,7 +807,9 @@ onUnmounted(() => {
<Col :span="16">
<!-- 作业进度卡片 -->
<Card
v-if="order.extInfo && order.status === 'ARRIVED'"
v-if="
order.extInfo && ['ARRIVED', 'COMPLETED'].includes(order.status)
"
class="work-progress-card mb-3"
>
<template #title>
@@ -816,10 +827,16 @@ onUnmounted(() => {
<Progress
type="circle"
:percent="workProgress"
:stroke-color="{
'0%': isOvertime ? '#ff4d4f' : '#1677ff',
'100%': isOvertime ? '#ff7875' : '#52c41a',
}"
:stroke-color="
isOvertime
? '#ff4d4f'
: workProgress >= 100
? '#52c41a'
: {
'0%': '#1677ff',
'100%': '#52c41a',
}
"
:stroke-width="6"
:size="100"
>
@@ -827,7 +844,13 @@ onUnmounted(() => {
<div class="progress-inner">
<div
class="progress-value"
:class="{ 'text-red-500': isOvertime }"
:style="{
color: isOvertime
? '#ff4d4f'
: workProgress >= 100
? '#52c41a'
: '#1677ff',
}"
>
{{ workProgress }}%
</div>
@@ -988,8 +1011,17 @@ onUnmounted(() => {
{{ formatDateTime(order.createTime) }}
</span>
</Descriptions.Item>
<Descriptions.Item v-if="order.triggerDeviceKey" label="触发设备">
<code class="device-code">{{ order.triggerDeviceKey }}</code>
<Descriptions.Item
v-if="order.extInfo?.arrivedTime"
label="到岗时间"
>
<span class="flex items-center gap-1">
<IconifyIcon
icon="solar:user-rounded-bold-duotone"
class="text-green-400"
/>
{{ formatDateTime(order.extInfo.arrivedTime) }}
</span>
</Descriptions.Item>
<Descriptions.Item v-if="order.startTime" label="开始时间">
<span class="flex items-center gap-1">
@@ -1000,6 +1032,38 @@ onUnmounted(() => {
{{ formatDateTime(order.startTime) }}
</span>
</Descriptions.Item>
<Descriptions.Item v-if="order.endTime" label="完成时间">
<span class="flex items-center gap-1">
<IconifyIcon
icon="solar:check-read-bold-duotone"
class="text-blue-400"
/>
{{ formatDateTime(order.endTime) }}
</span>
</Descriptions.Item>
<Descriptions.Item
v-if="order.updateTime && order.endTime !== order.updateTime"
label="更新时间"
>
<span class="flex items-center gap-1">
<IconifyIcon
icon="solar:refresh-circle-bold-duotone"
class="text-gray-400"
/>
{{ formatDateTime(order.updateTime) }}
</span>
</Descriptions.Item>
<Descriptions.Item v-if="order.triggerDeviceKey" label="触发设备">
<code class="device-code">{{ order.triggerDeviceKey }}</code>
</Descriptions.Item>
<Descriptions.Item
v-if="order.extInfo?.cleaningType"
label="作业类型"
>
<span>
{{ CLEANING_TYPE_TEXT_MAP[order.extInfo.cleaningType] }}
</span>
</Descriptions.Item>
</Descriptions>
</Card>
</Col>
@@ -1172,7 +1236,7 @@ onUnmounted(() => {
</Card>
<!-- 操作 -->
<Card class="actions-card">
<Card v-if="order.status !== 'COMPLETED'" class="actions-card">
<template #title>
<div class="flex items-center gap-2">
<IconifyIcon
@@ -1218,13 +1282,6 @@ onUnmounted(() => {
/>
语音提醒
</Button>
<Button block class="action-btn" @click="handleVibrateNotify">
<IconifyIcon
icon="solar:smartphone-vibration-bold-duotone"
class="mr-1"
/>
震动提醒
</Button>
<Button danger block class="action-btn" @click="handleCancel">
<IconifyIcon
icon="solar:close-circle-bold-duotone"
@@ -1247,12 +1304,12 @@ onUnmounted(() => {
/>
手动完成
</Button>
<Button block class="action-btn" @click="handleVibrateNotify">
<Button block class="action-btn" @click="handleVoiceNotify">
<IconifyIcon
icon="solar:smartphone-vibration-bold-duotone"
icon="solar:volume-loud-bold-duotone"
class="mr-1"
/>
震动提醒
语音提醒
</Button>
<Button block class="action-btn" @click="handleCancel">
<IconifyIcon
@@ -1470,6 +1527,14 @@ onUnmounted(() => {
transition: all 0.3s;
}
.node-current .node-icon {
color: #fff;
background: linear-gradient(135deg, #1677ff 0%, #4096ff 100%);
border-color: #1677ff;
box-shadow: 0 0 0 3px rgb(22 119 255 / 15%);
animation: pulse-glow 2s infinite;
}
.node-completed .node-icon {
color: #fff;
background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
@@ -1477,12 +1542,13 @@ onUnmounted(() => {
box-shadow: 0 2px 6px rgb(82 196 26 / 25%);
}
.node-current .node-icon {
/* 已完成的当前节点(绿色覆盖蓝色) */
.progress-node.node-completed-current .node-icon {
color: #fff;
background: linear-gradient(135deg, #1677ff 0%, #4096ff 100%);
border-color: #1677ff;
box-shadow: 0 0 0 3px rgb(22 119 255 / 15%);
animation: pulse-glow 2s infinite;
background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
border-color: #52c41a;
box-shadow: 0 2px 6px rgb(82 196 26 / 25%);
animation: none;
}
.node-pending .node-icon {
@@ -1506,6 +1572,18 @@ onUnmounted(() => {
color: #52c41a;
}
.node-completed .node-time {
color: #52c41a;
}
.node-completed-current .node-label {
color: #52c41a;
}
.node-completed-current .node-time {
color: #52c41a;
}
.node-current .node-label {
font-weight: 600;
color: #1677ff;
@@ -1522,6 +1600,7 @@ onUnmounted(() => {
align-items: center;
padding: 2px 8px;
font-size: 12px;
font-weight: 500;
color: #1677ff;
background: #e6f4ff;
border-radius: 10px;

View File

@@ -150,7 +150,7 @@ const list = ref<OpsOrderCenterApi.OrderItem[]>([]);
const total = ref(0);
const queryParams = ref({
pageNo: 1,
pageSize: 12,
pageSize: 8,
});
/** 字典相关 */
@@ -534,13 +534,11 @@ onMounted(() => {
<div v-if="list.length > 0" class="pagination-wrapper">
<Pagination
v-model:current="queryParams.pageNo"
v-model:page-size="queryParams.pageSize"
:page-size="8"
:total="total"
:show-total="(t) => `${t}`"
size="small"
show-quick-jumper
show-size-changer
:page-size-options="['12', '24', '36', '48']"
@change="handlePageChange"
/>
</div>

View File

@@ -141,7 +141,9 @@ defineExpose({ refresh: loadStats });
<div class="stats-content">
<div
class="stats-icon"
style="--icon-color: #ff4d4f; --icon-bg: #fff1f0"
style="
--icon-color: #ff4d4f; --icon-bg: #fff1f0"
>
<IconifyIcon icon="solar:clock-circle-bold-duotone" />
</div>
@@ -163,7 +165,9 @@ defineExpose({ refresh: loadStats });
<div class="stats-content">
<div
class="stats-icon"
style="--icon-color: #1677ff; --icon-bg: #e6f4ff"
style="
--icon-color: #1677ff; --icon-bg: #e6f4ff"
>
<IconifyIcon icon="solar:play-circle-bold-duotone" />
</div>
@@ -185,7 +189,9 @@ defineExpose({ refresh: loadStats });
<div class="stats-content">
<div
class="stats-icon"
style="--icon-color: #52c41a; --icon-bg: #f6ffed"
style="
--icon-color: #52c41a; --icon-bg: #f6ffed"
>
<IconifyIcon icon="solar:check-circle-bold-duotone" />
</div>
@@ -205,7 +211,9 @@ defineExpose({ refresh: loadStats });
<div class="stats-content">
<div
class="stats-icon"
style="--icon-color: #722ed1; --icon-bg: #f9f0ff"
style="
--icon-color: #722ed1; --icon-bg: #f9f0ff"
>
<IconifyIcon
icon="solar:users-group-two-rounded-bold-duotone"