refactor(ops): 工单详情调整
This commit is contained in:
@@ -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' }, '-');
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user