feat(@vben/web-antd): 保洁工单模块增强,拆分组件并新增区域筛选

拆分保洁工单操作和详情扩展为独立组件,新增 config.ts 配置文件;
列表页增加区域筛选功能,详情页优化进度条和日志展示样式。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
lzh
2026-03-13 11:15:07 +08:00
parent bf13067812
commit d9a192bd07
8 changed files with 663 additions and 54 deletions

View File

@@ -0,0 +1,77 @@
<script setup lang="ts">
import type { OpsOrderCenterApi } from '#/api/ops/order-center';
import { IconifyIcon } from '@vben/icons';
import { Button } from 'ant-design-vue';
import { sendDeviceMessage } from '#/api/iot/device/device';
import { manualCompleteOrder } from '#/api/ops/cleaning';
import { message } from 'ant-design-vue';
defineOptions({ name: 'CleaningActions' });
const props = defineProps<{
order: OpsOrderCenterApi.OrderDetail;
}>();
const emit = defineEmits<{
refresh: [];
}>();
async function handleVoiceNotify() {
if (!props.order.assigneeId) return;
try {
await sendDeviceMessage({
deviceId: props.order.assigneeId,
method: 'voice.broadcast',
params: { content: `请注意:${props.order.title}` },
});
message.success('语音提醒已发送');
} catch {
message.error('发送失败');
}
}
async function handleManualComplete() {
try {
await manualCompleteOrder(props.order.id!);
message.success('工单已完成');
emit('refresh');
} catch {
message.error('操作失败');
}
}
</script>
<template>
<!-- 已推送状态语音提醒 -->
<Button
v-if="order.status === 'DISPATCHED'"
type="primary"
block
class="action-btn"
@click="handleVoiceNotify"
>
<IconifyIcon icon="solar:volume-loud-bold-duotone" class="mr-1" />
语音提醒
</Button>
<!-- 作业中状态手动完成 + 语音提醒 -->
<template v-if="order.status === 'ARRIVED'">
<Button
type="primary"
block
class="action-btn action-btn-success"
@click="handleManualComplete"
>
<IconifyIcon icon="solar:check-circle-bold-duotone" class="mr-1" />
手动完成
</Button>
<Button block class="action-btn" @click="handleVoiceNotify">
<IconifyIcon icon="solar:volume-loud-bold-duotone" class="mr-1" />
语音提醒
</Button>
</template>
</template>

View File

@@ -0,0 +1,396 @@
<script setup lang="ts">
import type { OpsCleaningApi } from '#/api/ops/cleaning';
import type { OpsOrderCenterApi } from '#/api/ops/order-center';
import { computed } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { formatDateTime } from '@vben/utils';
import {
Alert,
Button,
Card,
Col,
Descriptions,
Progress,
Row,
Tag,
} from 'ant-design-vue';
import { CLEANING_TYPE_TEXT_MAP } from '../config';
defineOptions({ name: 'CleaningDetailExt' });
const props = defineProps<{
badgeStatus: null | OpsCleaningApi.BadgeRealtimeStatus;
order: OpsOrderCenterApi.OrderDetail;
}>();
const emit = defineEmits<{
voiceNotify: [];
}>();
/** 保洁扩展信息(类型安全) */
const extInfo = computed(() => {
return props.order.extInfo as OpsOrderCenterApi.CleaningExtInfo | undefined;
});
/** 计算作业时长 */
const workDuration = computed(() => {
if (!extInfo.value?.arrivedTime) return 0;
const arrived = new Date(extInfo.value.arrivedTime).getTime();
const endTime = props.order.endTime
? new Date(props.order.endTime).getTime()
: Date.now();
return Math.floor((endTime - arrived) / 60_000);
});
/** 计算作业进度 */
const workProgress = computed(() => {
if (!extInfo.value?.expectedDuration) return 0;
return Math.min(
Math.round((workDuration.value / extInfo.value.expectedDuration) * 100),
100,
);
});
/** 是否超时 */
const isOvertime = computed(() => {
if (props.order.status === 'COMPLETED') return false;
return (
extInfo.value?.expectedDuration &&
workDuration.value > extInfo.value.expectedDuration
);
});
/** 是否显示离岗警告 */
const showLeaveWarning = computed(() => {
return (
props.order.status === 'ARRIVED' &&
props.badgeStatus &&
!props.badgeStatus.isInArea
);
});
/** 获取电池颜色 */
function getBatteryColor(level: null | number) {
if (level === null) return '#d9d9d9';
if (level <= 20) return '#f5222d';
if (level <= 50) return '#fa8c16';
return '#52c41a';
}
/** 获取工牌状态文本 */
function getBadgeStatusText(status: string) {
const s = status?.toUpperCase();
switch (s) {
case 'BUSY':
return '作业中';
case 'IDLE':
return '空闲';
case 'OFFLINE':
return '离线';
case 'PAUSED':
return '暂停';
default:
return status || '未知';
}
}
function isBusyStatus(status: string) {
return status?.toUpperCase() === 'BUSY';
}
function formatRelativeTime(time: string) {
const now = Date.now();
const target = new Date(time).getTime();
const diff = now - target;
if (diff < 60_000) return '刚刚';
if (diff < 3_600_000) return `${Math.floor(diff / 60_000)} 分钟前`;
if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)} 小时前`;
return formatDateTime(time);
}
</script>
<template>
<!-- 离岗警告 -->
<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="emit('voiceNotify')">
语音提醒
</Button>
</div>
</template>
</Alert>
<!-- 作业进度卡片 -->
<Card
v-if="extInfo && ['ARRIVED', 'COMPLETED'].includes(order.status)"
class="work-progress-card mb-3"
>
<template #title>
<div class="flex items-center gap-2">
<IconifyIcon
icon="solar:chart-2-bold-duotone"
class="text-blue-500"
/>
<span>作业进度</span>
</div>
</template>
<Row :gutter="16" align="middle">
<Col :span="8">
<div class="progress-circle-wrapper">
<Progress
type="circle"
:percent="workProgress"
:stroke-color="
isOvertime
? '#ff4d4f'
: workProgress >= 100
? '#52c41a'
: { '0%': '#1677ff', '100%': '#52c41a' }
"
:stroke-width="6"
:size="100"
>
<template #format>
<div class="progress-inner">
<div
class="progress-value"
:style="{
color: isOvertime
? '#ff4d4f'
: workProgress >= 100
? '#52c41a'
: '#1677ff',
}"
>
{{ workProgress }}%
</div>
<div class="progress-label">完成度</div>
</div>
</template>
</Progress>
</div>
</Col>
<Col :span="16">
<div class="work-stats-grid">
<div class="stat-item">
<div class="stat-icon bg-blue-50 dark:bg-blue-900/30">
<IconifyIcon
icon="solar:clipboard-list-bold-duotone"
class="text-blue-500"
/>
</div>
<div class="stat-content">
<div class="stat-label">作业类型</div>
<div class="stat-value">
{{ CLEANING_TYPE_TEXT_MAP[extInfo.cleaningType!] || '-' }}
</div>
</div>
</div>
<div class="stat-item">
<div class="stat-icon bg-green-50 dark:bg-green-900/30">
<IconifyIcon
icon="solar:clock-circle-bold-duotone"
class="text-green-500"
/>
</div>
<div class="stat-content">
<div class="stat-label">预计时长</div>
<div class="stat-value">
{{ extInfo.expectedDuration }} 分钟
</div>
</div>
</div>
<div class="stat-item">
<div
class="stat-icon"
:class="
isOvertime
? 'bg-red-50 dark:bg-red-900/30'
: 'bg-orange-50 dark:bg-orange-900/30'
"
>
<IconifyIcon
icon="solar:stopwatch-bold-duotone"
:class="isOvertime ? 'text-red-500' : 'text-orange-500'"
/>
</div>
<div class="stat-content">
<div class="stat-label">已用时长</div>
<div
class="stat-value"
:class="{ 'text-red-500': isOvertime }"
>
{{ workDuration }} 分钟
<Tag v-if="isOvertime" color="error" size="small" class="ml-1">
超时
</Tag>
</div>
</div>
</div>
<div class="stat-item">
<div class="stat-icon bg-purple-50 dark:bg-purple-900/30">
<IconifyIcon
icon="solar:hourglass-line-bold-duotone"
class="text-purple-500"
/>
</div>
<div class="stat-content">
<div class="stat-label">剩余时间</div>
<div
class="stat-value"
:class="{ 'text-red-500': isOvertime }"
>
{{
isOvertime
? `已超时 ${workDuration - (extInfo.expectedDuration || 0)} 分钟`
: `${(extInfo.expectedDuration || 0) - workDuration} 分钟`
}}
</div>
</div>
</div>
</div>
</Col>
</Row>
</Card>
<!-- 保洁专有基础信息字段 -->
<template v-if="extInfo?.arrivedTime">
<Descriptions.Item label="到岗时间">
<span class="flex items-center gap-1">
<IconifyIcon
icon="solar:user-rounded-bold-duotone"
class="text-green-400"
/>
{{ formatDateTime(extInfo.arrivedTime) }}
</span>
</Descriptions.Item>
</template>
<template v-if="extInfo?.cleaningType">
<Descriptions.Item label="作业类型">
<span>{{ CLEANING_TYPE_TEXT_MAP[extInfo.cleaningType] }}</span>
</Descriptions.Item>
</template>
<!-- 工牌状态面板(右侧面板内容) -->
<template v-if="badgeStatus">
<div class="badge-status-panel">
<div class="panel-header">
<IconifyIcon
icon="solar:bluetooth-wave-bold-duotone"
class="text-blue-500"
/>
<span>工牌实时状态</span>
<div class="pulse-dot"></div>
</div>
<div class="badge-stats">
<div class="badge-stat-item">
<div class="badge-stat-icon">
<IconifyIcon
icon="solar:map-point-wave-bold-duotone"
:class="badgeStatus.isInArea ? 'text-green-500' : 'text-orange-500'"
/>
</div>
<div class="badge-stat-content">
<div class="badge-stat-label">位置状态</div>
<div class="flex items-center gap-2">
<Tag
:color="badgeStatus.isInArea ? 'success' : 'warning'"
size="small"
>
{{ badgeStatus.isInArea ? '在区域内' : '已离开' }}
</Tag>
<span v-if="badgeStatus.areaName" class="text-xs text-gray-500">
{{ badgeStatus.areaName }}
</span>
</div>
</div>
</div>
<div class="badge-stat-item">
<div class="badge-stat-icon">
<IconifyIcon
icon="solar:battery-charge-bold-duotone"
:style="{ color: getBatteryColor(badgeStatus.batteryLevel) }"
/>
</div>
<div class="badge-stat-content">
<div class="badge-stat-label">电池电量</div>
<div v-if="badgeStatus.batteryLevel != null">
<div class="battery-bar">
<div
class="battery-fill"
:style="{
width: `${badgeStatus.batteryLevel}%`,
backgroundColor: getBatteryColor(badgeStatus.batteryLevel),
}"
></div>
</div>
<span
class="battery-text"
:style="{ color: getBatteryColor(badgeStatus.batteryLevel) }"
>
{{ badgeStatus.batteryLevel }}%
</span>
</div>
<span v-else class="text-xs text-gray-400">未知</span>
</div>
</div>
<div class="badge-stat-item">
<div class="badge-stat-icon">
<IconifyIcon
icon="solar:wifi-router-bold-duotone"
class="text-blue-500"
/>
</div>
<div class="badge-stat-content">
<div class="badge-stat-label">信号强度</div>
<div v-if="badgeStatus.rssi != null" class="signal-strength">
<div
v-for="i in 4"
:key="i"
class="signal-bar"
:class="{ active: badgeStatus.rssi > -70 + (i - 1) * 15 }"
></div>
<span class="signal-value">{{ badgeStatus.rssi }} dBm</span>
</div>
<span v-else class="text-xs text-gray-400">未知</span>
</div>
</div>
<div class="badge-stat-item">
<div class="badge-stat-icon">
<IconifyIcon
icon="solar:pulse-2-bold-duotone"
class="text-green-500"
/>
</div>
<div class="badge-stat-content">
<div class="badge-stat-label">最后心跳</div>
<div class="heartbeat-time">
{{ formatRelativeTime(badgeStatus.lastHeartbeatTime) }}
</div>
</div>
</div>
</div>
</div>
<!-- 执行人卡片中工牌状态Tag -->
<Tag
:color="isBusyStatus(badgeStatus.status) ? 'processing' : 'default'"
class="status-badge"
>
{{ getBadgeStatusText(badgeStatus.status) }}
</Tag>
</template>
</template>

View File

@@ -0,0 +1,15 @@
import { OpsOrderCenterApi } from '#/api/ops/order-center';
/** 保洁类型选项 */
export const CLEANING_TYPE_OPTIONS = [
{ label: '日常', value: OpsOrderCenterApi.CleaningType.ROUTINE },
{ label: '深度', value: OpsOrderCenterApi.CleaningType.DEEP },
{ label: '应急', value: OpsOrderCenterApi.CleaningType.EMERGENCY },
];
/** 保洁类型文本映射 */
export const CLEANING_TYPE_TEXT_MAP: Record<string, string> = {
ROUTINE: '日常',
DEEP: '深度',
EMERGENCY: '应急',
};

View File

@@ -135,6 +135,10 @@ export const TRIGGER_SOURCE_OPTIONS = [
{ label: '蓝牙信标', value: OpsOrderCenterApi.TriggerSource.IOT_BEACON },
{ label: '客流阈值', value: OpsOrderCenterApi.TriggerSource.PEOPLE_FLOW },
{ label: '手动创建', value: OpsOrderCenterApi.TriggerSource.MANUAL },
{ label: '视频告警', value: OpsOrderCenterApi.TriggerSource.VIDEO_ALARM },
{ label: '门禁告警', value: OpsOrderCenterApi.TriggerSource.ACCESS_ALARM },
{ label: '巡更告警', value: OpsOrderCenterApi.TriggerSource.PATROL_ALARM },
{ label: '紧急按钮', value: OpsOrderCenterApi.TriggerSource.PANIC_BUTTON },
];
/** 触发来源文本映射 */
@@ -144,6 +148,10 @@ export const TRIGGER_SOURCE_TEXT_MAP: Record<string, string> = {
TRAFFIC: '客流阈值',
PEOPLE_FLOW: '客流阈值',
MANUAL: '手动创建',
VIDEO_ALARM: '视频告警',
ACCESS_ALARM: '门禁告警',
PATROL_ALARM: '巡更告警',
PANIC_BUTTON: '紧急按钮',
};
/** 保洁类型选项 */
@@ -160,6 +168,22 @@ export const CLEANING_TYPE_TEXT_MAP: Record<string, string> = {
EMERGENCY: '应急',
};
/** 安保告警类型文本映射 */
export const SECURITY_ALARM_TYPE_MAP: Record<string, string> = {
intrusion: '入侵检测',
leave_post: '离岗检测',
fire: '火焰检测',
fence: '电子围栏',
};
/** 安保告警类型颜色映射 */
export const SECURITY_ALARM_TYPE_COLOR_MAP: Record<string, string> = {
intrusion: '#f5222d',
leave_post: '#fa8c16',
fire: '#ff4d4f',
fence: '#faad14',
};
/** 列表表格列定义 */
export function useGridColumns(): VxeTableGridOptions['columns'] {
return [

View File

@@ -733,15 +733,17 @@ onUnmounted(stopPolling);
<!-- 横向进度指示器 -->
<div class="progress-steps-wrapper">
<div class="progress-line">
<div
class="progress-line-fill"
:style="{
width: `${Math.max(0, (currentStepIndex / (visibleSteps.length - 1)) * 100)}%`,
}"
></div>
</div>
<div class="progress-nodes">
<!-- 进度线背景 + 填充 -->
<div class="progress-line">
<div
class="progress-line-fill"
:class="{ 'line-all-completed': order.status === 'COMPLETED' }"
:style="{
width: `${Math.max(0, (currentStepIndex / (visibleSteps.length - 1)) * 100)}%`,
}"
></div>
</div>
<div
v-for="(step, index) in visibleSteps"
:key="step.key"
@@ -749,7 +751,7 @@ onUnmounted(stopPolling);
:class="{
'node-completed': index < currentStepIndex,
'node-current':
index === currentStepIndex && order.status !== 'CANCELLED',
index === currentStepIndex && !['CANCELLED', 'COMPLETED'].includes(order.status),
'node-completed-current':
index === currentStepIndex && order.status === 'COMPLETED',
'node-pending': index > currentStepIndex,
@@ -822,17 +824,19 @@ onUnmounted(stopPolling);
</div>
<div class="log-simple-content">
<div class="log-simple-header">
<span
class="log-simple-title"
:style="{ color: getLogTypeConfig(log.type).color }"
>
{{ log.title }}
<span class="log-simple-title-line">
<span
class="log-simple-title"
:style="{ color: getLogTypeConfig(log.type).color }"
>
{{ log.title }}
</span>
<span v-if="log.content" class="log-simple-msg">{{ log.content }}</span>
</span>
<span class="log-simple-time">{{
formatRelativeTime(log.time)
}}</span>
</div>
<div class="log-simple-text">{{ log.content }}</div>
</div>
</div>
</div>
@@ -1543,14 +1547,22 @@ onUnmounted(stopPolling);
/* 横向进度条 */
.progress-steps-wrapper {
position: relative;
padding: 0 20px;
padding: 0 72px;
overflow: hidden;
}
.progress-nodes {
position: relative;
display: flex;
justify-content: space-between;
}
.progress-line {
position: absolute;
top: 16px;
right: 56px;
left: 56px;
right: 0;
left: 0;
z-index: 0;
height: 3px;
background: #e8e8e8;
border-radius: 2px;
@@ -1561,20 +1573,22 @@ onUnmounted(stopPolling);
background: linear-gradient(90deg, #52c41a 0%, #1677ff 100%);
border-radius: 2px;
transition: width 0.5s ease;
}
.progress-nodes {
position: relative;
z-index: 1;
display: flex;
justify-content: space-between;
&.line-all-completed {
background: #52c41a;
}
}
.progress-node {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
gap: 6px;
align-items: center;
width: 0;
overflow: visible;
white-space: nowrap;
}
.node-icon {
@@ -1792,74 +1806,111 @@ onUnmounted(stopPolling);
/* ========== 展开日志区域样式 ========== */
.logs-expand-section {
padding-top: 12px;
padding-top: 0;
margin-top: 12px;
border-top: 1px dashed #e8e8e8;
animation: slide-down 0.2s ease;
}
.logs-simple-list {
max-height: 240px;
padding-left: 2px;
max-height: 280px;
padding: 4px 4px 0;
overflow-x: hidden;
overflow-y: auto;
}
.log-simple-item {
position: relative;
display: flex;
gap: 10px;
gap: 12px;
padding: 8px 10px;
border-radius: 6px;
cursor: default;
transition: background-color 0.15s;
&:hover {
background: #f5f5f5;
}
&:last-child .log-simple-line {
display: none;
}
}
.log-simple-left {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
width: 10px;
padding-top: 5px;
flex-shrink: 0;
width: 12px;
margin: -8px 0;
padding: 8px 0;
}
.log-simple-dot {
position: relative;
z-index: 1;
flex-shrink: 0;
width: 8px;
height: 8px;
margin-top: 6px;
border-radius: 50%;
box-shadow: 0 0 0 2px #fff;
box-shadow: 0 0 0 3px #fff;
}
.log-simple-line {
flex: 1;
position: absolute;
top: 0;
bottom: 0;
left: 50%;
width: 1px;
min-height: 16px;
margin: 3px 0;
background: #e8e8e8;
transform: translateX(-50%);
}
.log-simple-item:first-child .log-simple-line {
top: 22px;
}
.log-simple-content {
flex: 1;
min-width: 0;
padding: 2px 0 10px;
}
.log-simple-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 2px;
gap: 12px;
line-height: 20px;
}
.log-simple-title-line {
display: flex;
gap: 6px;
align-items: center;
min-width: 0;
}
.log-simple-title {
font-size: 13px;
font-weight: 500;
flex-shrink: 0;
}
.log-simple-msg {
font-size: 12px;
color: #8c8c8c;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.log-simple-time {
font-size: 12px;
color: #8c8c8c;
}
.log-simple-text {
font-size: 12px;
line-height: 1.5;
color: #595959;
color: #bfbfbf;
flex-shrink: 0;
}
.logs-empty {

View File

@@ -22,6 +22,7 @@ import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { OpsCleaningApi, sendDeviceNotify } from '#/api/ops/cleaning';
import { getOrderPage } from '#/api/ops/order-center';
import AreaFilterDrawer from '../../components/AreaFilterDrawer.vue';
import {
ORDER_TYPE_COLOR_MAP,
ORDER_TYPE_OPTIONS,
@@ -44,6 +45,8 @@ const router = useRouter();
const viewMode = ref<'card' | 'list'>('card');
const activeTab = ref('ALL');
const showAdvancedFilter = ref(false);
const showAreaFilter = ref(false);
const selectedAreaId = ref<number | undefined>(undefined);
const cardViewRef = ref();
const statsBarRef = ref();
@@ -80,6 +83,7 @@ const queryParams = ref({
priority: undefined as OpsOrderCenterApi.Priority | undefined,
orderCode: '',
title: '',
areaId: undefined as number | undefined,
});
/** 搜索 */
@@ -105,7 +109,9 @@ function handleReset() {
priority: undefined,
orderCode: '',
title: '',
areaId: undefined,
};
selectedAreaId.value = undefined;
activeTab.value = 'ALL';
handleSearch();
}
@@ -191,6 +197,13 @@ async function handleNotify(
}
}
/** 区域筛选确认 */
function handleAreaConfirm(id: number | undefined) {
selectedAreaId.value = id;
queryParams.value.areaId = id;
handleSearch();
}
/** 处理统计卡片点击 */
function handleStatClick(statKey: string) {
// 重置筛选条件
@@ -200,6 +213,7 @@ function handleStatClick(statKey: string) {
priority: undefined,
orderCode: '',
title: '',
areaId: queryParams.value.areaId,
};
switch (statKey) {
@@ -280,6 +294,13 @@ onMounted(() => {
<UpgradePriorityFormModal @success="handleRefresh" />
<CancelFormModal @success="handleRefresh" />
<!-- 区域筛选抽屉 -->
<AreaFilterDrawer
v-model:open="showAreaFilter"
v-model:model-value="selectedAreaId"
@confirm="handleAreaConfirm"
/>
<!-- 快速统计栏 -->
<StatsBar ref="statsBarRef" @stat-click="handleStatClick" />
@@ -341,6 +362,15 @@ onMounted(() => {
<IconifyIcon icon="solar:filter-bold" />
</Button>
<!-- 区域筛选按钮 -->
<Button
class="action-btn"
:class="{ 'action-btn--active': selectedAreaId !== undefined }"
@click="showAreaFilter = true"
>
<IconifyIcon icon="solar:map-point-bold" />
</Button>
<!-- 刷新按钮 -->
<Button class="action-btn" @click="handleRefresh">
<IconifyIcon icon="solar:refresh-bold" />

View File

@@ -11,19 +11,19 @@ import AreaTree from './AreaTree.vue';
const props = withDefaults(
defineProps<{
/** 控制抽屉显示 */
open?: boolean;
/** 外部已确认的区域 ID */
modelValue?: number | undefined;
/** 控制抽屉显示 */
open?: boolean;
}>(),
{ open: false, modelValue: undefined },
);
const emit = defineEmits<{
'update:open': [value: boolean];
'update:modelValue': [id: number | undefined];
/** 确认选择后触发 */
confirm: [id: number | undefined];
'update:modelValue': [id: number | undefined];
'update:open': [value: boolean];
}>();
const areaTreeRef = ref<InstanceType<typeof AreaTree>>();
@@ -47,7 +47,10 @@ watch(
/** 选中区域的完整路径名称 */
const selectedAreaPath = computed(() => {
if (!tempSelectedArea.value?.id) return '';
return areaTreeRef.value?.getAreaPath(tempSelectedArea.value.id) ?? tempSelectedArea.value.areaName;
return (
areaTreeRef.value?.getAreaPath(tempSelectedArea.value.id) ??
tempSelectedArea.value.areaName
);
});
/** 树节点选中 */
@@ -83,7 +86,11 @@ function handleClose() {
title="区域筛选"
placement="right"
:width="320"
:body-style="{ padding: '12px 16px', display: 'flex', flexDirection: 'column' }"
:body-style="{
padding: '12px 16px',
display: 'flex',
flexDirection: 'column',
}"
:header-style="{ padding: '12px 16px' }"
:footer-style="{ padding: '10px 16px' }"
@close="handleClose"
@@ -110,7 +117,11 @@ function handleClose() {
<IconifyIcon icon="solar:restart-bold" class="btn-icon" />
重置
</Button>
<Button type="primary" :disabled="!tempSelectedArea" @click="handleConfirm">
<Button
type="primary"
:disabled="!tempSelectedArea"
@click="handleConfirm"
>
<IconifyIcon icon="solar:check-circle-bold" class="btn-icon" />
确认
</Button>
@@ -133,8 +144,8 @@ function handleClose() {
}
.hint-icon {
font-size: 16px;
flex-shrink: 0;
font-size: 16px;
}
.hint-text {

View File

@@ -21,14 +21,19 @@ const props = withDefaults(
/** 外部控制选中的 key 列表(单选模式) */
selectedKeys?: number[];
}>(),
{ activeOnly: true, checkable: false, checkedKeys: () => [], selectedKeys: undefined },
{
activeOnly: true,
checkable: false,
checkedKeys: () => [],
selectedKeys: undefined,
},
);
const emit = defineEmits<{
/** 选中区域节点,传出完整节点对象;取消选中时传 null */
select: [area: null | OpsAreaApi.BusArea];
/** checkable 模式下勾选变化 */
check: [checkedKeys: number[], halfCheckedKeys: number[]];
/** 选中区域节点,传出完整节点对象;取消选中时传 null */
select: [area: null | OpsAreaApi.BusArea];
/** 同步 checkedKeys 到父组件 */
'update:checkedKeys': [keys: number[]];
}>();