feat(@vben/web-antd): 完善保洁工单管理模块功能
- 工单看板:优化数据展示和交互逻辑 - 工单列表:增强筛选和分页功能 - 分配表单:改进表单验证和用户体验 - 卡片视图:优化布局和视觉效果 - 数据配置:更新常量定义和类型 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,7 +13,7 @@ import { getQuickStats } from '#/api/ops/order-center';
|
||||
defineOptions({ name: 'CleaningWorkOrderDashboard' });
|
||||
|
||||
// ========== 模拟数据开关 ==========
|
||||
const USE_MOCK_DATA = true;
|
||||
const USE_MOCK_DATA = false;
|
||||
|
||||
// ========== 数据类型定义 ==========
|
||||
interface DashboardStats {
|
||||
@@ -22,8 +22,8 @@ interface DashboardStats {
|
||||
inProgressCount: number;
|
||||
completedTodayCount: number;
|
||||
completedTotalCount: number;
|
||||
onlineCleanerCount: number;
|
||||
totalCleanerCount: number;
|
||||
onlineBadgeCount: number;
|
||||
totalBadgeCount: number;
|
||||
|
||||
// 趋势数据(7天)
|
||||
trendData: {
|
||||
@@ -99,8 +99,8 @@ const statsData = ref<DashboardStats>({
|
||||
inProgressCount: 0,
|
||||
completedTodayCount: 0,
|
||||
completedTotalCount: 0,
|
||||
onlineCleanerCount: 0,
|
||||
totalCleanerCount: 0,
|
||||
onlineBadgeCount: 0,
|
||||
totalBadgeCount: 0,
|
||||
trendData: {
|
||||
dates: [],
|
||||
createdData: [],
|
||||
@@ -131,8 +131,8 @@ const MOCK_STATS: DashboardStats = {
|
||||
inProgressCount: 15,
|
||||
completedTodayCount: 42,
|
||||
completedTotalCount: 1586,
|
||||
onlineCleanerCount: 12,
|
||||
totalCleanerCount: 18,
|
||||
onlineBadgeCount: 12,
|
||||
totalBadgeCount: 18,
|
||||
trendData: {
|
||||
dates: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
|
||||
createdData: [45, 52, 38, 65, 48, 55, 42],
|
||||
@@ -914,7 +914,7 @@ async function loadStats() {
|
||||
pendingCount: quickStats.pendingCount,
|
||||
inProgressCount: quickStats.inProgressCount,
|
||||
completedTodayCount: quickStats.completedTodayCount,
|
||||
onlineCleanerCount: quickStats.onlineCleanerCount,
|
||||
onlineBadgeCount: quickStats.onlineBadgeCount,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ export const PRIORITY_STYLE_MAP: Record<
|
||||
label: 'P2',
|
||||
color: '#9E9E9E',
|
||||
bgColor: '#FAFAFA',
|
||||
icon: '',
|
||||
icon: 'lucide:info', // Added icon
|
||||
animation: false,
|
||||
},
|
||||
};
|
||||
@@ -137,6 +137,8 @@ export const TRIGGER_SOURCE_OPTIONS = [
|
||||
/** 触发来源文本映射 */
|
||||
export const TRIGGER_SOURCE_TEXT_MAP: Record<string, string> = {
|
||||
IOT_BEACON: '蓝牙信标',
|
||||
IOT_TRAFFIC: '客流阈值',
|
||||
TRAFFIC: '客流阈值',
|
||||
PEOPLE_FLOW: '客流阈值',
|
||||
MANUAL: '手动创建',
|
||||
};
|
||||
|
||||
@@ -115,9 +115,11 @@ function handleTabChange(key: number | string) {
|
||||
const keyStr = String(key);
|
||||
activeTab.value = keyStr;
|
||||
const tabConfig = STATUS_TAB_OPTIONS.find((t) => t.key === keyStr);
|
||||
queryParams.value.status = (tabConfig?.statuses ?? undefined) as
|
||||
| OpsOrderCenterApi.OrderStatus[]
|
||||
| undefined;
|
||||
if (tabConfig) {
|
||||
queryParams.value.status = tabConfig.statuses as
|
||||
| OpsOrderCenterApi.OrderStatus[]
|
||||
| undefined;
|
||||
}
|
||||
handleSearch();
|
||||
}
|
||||
|
||||
@@ -175,7 +177,7 @@ async function handleNotify(
|
||||
|
||||
try {
|
||||
await sendDeviceNotify({
|
||||
cleanerId: row.assigneeId,
|
||||
badgeId: row.assigneeId,
|
||||
type:
|
||||
type === 'voice'
|
||||
? OpsCleaningApi.NotifyType.VOICE
|
||||
@@ -212,7 +214,8 @@ function handleStatClick(statKey: string) {
|
||||
'DISPATCHED',
|
||||
'CONFIRMED',
|
||||
'ARRIVED',
|
||||
] as OpsOrderCenterApi.OrderStatus[];
|
||||
'QUEUED',
|
||||
];
|
||||
break;
|
||||
}
|
||||
case 'pendingCount': {
|
||||
@@ -240,11 +243,18 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
}: {
|
||||
page: { currentPage: number; pageSize: number };
|
||||
}) => {
|
||||
return await getOrderPage({
|
||||
const params = {
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...queryParams.value,
|
||||
});
|
||||
};
|
||||
|
||||
// 处理状态数组
|
||||
// 如果后端接收 List<String>,通常不需要手动 join(','),直接传数组即可
|
||||
// axios/requestClient 会自动处理为 status=A&status=B 的形式
|
||||
// 这里移除 .join(',') 的逻辑,直接保留数组
|
||||
|
||||
return await getOrderPage(params);
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useVbenModal } from '@vben/common-ui';
|
||||
import { Avatar, Badge, Card, message, Spin } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { getCleanerStatusList } from '#/api/ops/cleaning';
|
||||
import { getBadgeStatusList } from '#/api/ops/cleaning';
|
||||
import { assignOrder } from '#/api/ops/order-center';
|
||||
|
||||
defineOptions({ name: 'WorkOrderAssignForm' });
|
||||
@@ -23,7 +23,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
orderId.value = data.orderId;
|
||||
orderCode.value = data.orderCode;
|
||||
}
|
||||
await loadCleaners();
|
||||
await loadBadges();
|
||||
}
|
||||
},
|
||||
onConfirm: handleSubmit,
|
||||
@@ -32,9 +32,9 @@ const [Modal, modalApi] = useVbenModal({
|
||||
const orderId = ref<number>();
|
||||
const orderCode = ref<string>('');
|
||||
const loading = ref(false);
|
||||
const cleanerLoading = ref(false);
|
||||
const cleanerList = ref<OpsCleaningApi.CleanerStatusItem[]>([]);
|
||||
const selectedCleanerId = ref<number>();
|
||||
const badgeLoading = ref(false);
|
||||
const badgeList = ref<OpsCleaningApi.BadgeStatusItem[]>([]);
|
||||
const selectedBadgeId = ref<number>();
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
schema: [
|
||||
@@ -51,22 +51,22 @@ const [Form, formApi] = useVbenForm({
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
/** 加载保洁员列表 */
|
||||
async function loadCleaners() {
|
||||
cleanerLoading.value = true;
|
||||
/** 加载工牌列表 */
|
||||
async function loadBadges() {
|
||||
badgeLoading.value = true;
|
||||
try {
|
||||
const res = await getCleanerStatusList();
|
||||
cleanerList.value = res.list || [];
|
||||
const res = await getBadgeStatusList();
|
||||
badgeList.value = res || [];
|
||||
} catch {
|
||||
cleanerList.value = [];
|
||||
badgeList.value = [];
|
||||
} finally {
|
||||
cleanerLoading.value = false;
|
||||
badgeLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 选择保洁员 */
|
||||
function selectCleaner(cleaner: OpsCleaningApi.CleanerStatusItem) {
|
||||
selectedCleanerId.value = cleaner.userId;
|
||||
/** 选择工牌 */
|
||||
function selectBadge(badge: OpsCleaningApi.BadgeStatusItem) {
|
||||
selectedBadgeId.value = badge.deviceId;
|
||||
}
|
||||
|
||||
/** 获取状态颜色 */
|
||||
@@ -100,8 +100,8 @@ function getBatteryColor(level: number) {
|
||||
|
||||
/** 提交表单 */
|
||||
async function handleSubmit() {
|
||||
if (!selectedCleanerId.value) {
|
||||
message.warning('请选择执行人');
|
||||
if (!selectedBadgeId.value) {
|
||||
message.warning('请选择执行工牌');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ async function handleSubmit() {
|
||||
const formData = await formApi.getValues();
|
||||
await assignOrder({
|
||||
orderId: orderId.value!,
|
||||
assigneeId: selectedCleanerId.value,
|
||||
assigneeId: selectedBadgeId.value,
|
||||
remark: formData.remark,
|
||||
});
|
||||
message.success('派单成功');
|
||||
@@ -121,9 +121,9 @@ async function handleSubmit() {
|
||||
}
|
||||
}
|
||||
|
||||
/** 可用保洁员(优先显示空闲) */
|
||||
const sortedCleaners = computed(() => {
|
||||
return [...cleanerList.value].toSorted((a, b) => {
|
||||
/** 可用工牌(优先显示空闲) */
|
||||
const sortedBadges = computed(() => {
|
||||
return [...badgeList.value].toSorted((a, b) => {
|
||||
const order: Record<string, number> = {
|
||||
IDLE: 0,
|
||||
BUSY: 1,
|
||||
@@ -147,65 +147,60 @@ const sortedCleaners = computed(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 保洁员选择 -->
|
||||
<!-- 工牌选择 -->
|
||||
<div class="mb-4">
|
||||
<div class="mb-2 text-sm font-medium">选择执行人</div>
|
||||
<Spin :spinning="cleanerLoading">
|
||||
<div class="mb-2 text-sm font-medium">选择执行工牌</div>
|
||||
<Spin :spinning="badgeLoading">
|
||||
<div
|
||||
v-if="sortedCleaners.length > 0"
|
||||
class="cleaner-grid grid max-h-64 gap-3 overflow-y-auto"
|
||||
v-if="sortedBadges.length > 0"
|
||||
class="badge-grid grid max-h-64 gap-3 overflow-y-auto"
|
||||
>
|
||||
<Card
|
||||
v-for="cleaner in sortedCleaners"
|
||||
:key="cleaner.userId"
|
||||
v-for="badge in sortedBadges"
|
||||
:key="badge.deviceId"
|
||||
size="small"
|
||||
class="cleaner-card cursor-pointer transition-all"
|
||||
class="badge-card cursor-pointer transition-all"
|
||||
:class="[
|
||||
selectedCleanerId === cleaner.userId
|
||||
selectedBadgeId === badge.deviceId
|
||||
? 'border-primary ring-2 ring-primary/20'
|
||||
: 'hover:border-gray-300',
|
||||
cleaner.status === 'OFFLINE' ? 'opacity-50' : '',
|
||||
badge.status === 'OFFLINE' ? 'opacity-50' : '',
|
||||
]"
|
||||
@click="selectCleaner(cleaner)"
|
||||
@click="selectBadge(badge)"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<Badge
|
||||
:color="getStatusColor(cleaner.status)"
|
||||
:offset="[-4, 28]"
|
||||
>
|
||||
<Badge :color="getStatusColor(badge.status)" :offset="[-4, 28]">
|
||||
<Avatar size="small" class="bg-blue-500">
|
||||
{{ cleaner.userName?.charAt(0) }}
|
||||
{{ badge.deviceKey?.charAt(0) || '?' }}
|
||||
</Avatar>
|
||||
</Badge>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="truncate font-medium">{{
|
||||
cleaner.userName
|
||||
badge.deviceKey
|
||||
}}</span>
|
||||
<span
|
||||
class="ml-2 text-xs"
|
||||
:style="{ color: getStatusColor(cleaner.status) }"
|
||||
:style="{ color: getStatusColor(badge.status) }"
|
||||
>
|
||||
{{ getStatusText(cleaner.status) }}
|
||||
{{ getStatusText(badge.status) }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mt-1 flex items-center justify-between text-xs text-gray-400"
|
||||
>
|
||||
<span>{{ cleaner.currentAreaName || '未知区域' }}</span>
|
||||
<span>{{ badge.currentAreaName || '未知区域' }}</span>
|
||||
<span
|
||||
:style="{ color: getBatteryColor(cleaner.batteryLevel) }"
|
||||
:style="{ color: getBatteryColor(badge.batteryLevel) }"
|
||||
>
|
||||
{{ cleaner.batteryLevel }}%
|
||||
{{ badge.batteryLevel }}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<div v-else class="py-8 text-center text-gray-400">
|
||||
暂无可用保洁员
|
||||
</div>
|
||||
<div v-else class="py-8 text-center text-gray-400">暂无可用工牌</div>
|
||||
</Spin>
|
||||
</div>
|
||||
|
||||
@@ -217,11 +212,11 @@ const sortedCleaners = computed(() => {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.assign-form {
|
||||
.cleaner-grid {
|
||||
.badge-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.cleaner-card {
|
||||
.badge-card {
|
||||
border-radius: 8px;
|
||||
|
||||
:deep(.ant-card-body) {
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import type { OpsOrderCenterApi } from '#/api/ops/order-center';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { useDictStore } from '@vben/stores';
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
Col,
|
||||
Dropdown,
|
||||
@@ -15,16 +14,17 @@ import {
|
||||
Pagination,
|
||||
Popconfirm,
|
||||
Row,
|
||||
Spin,
|
||||
Tag,
|
||||
Tooltip,
|
||||
} from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { getOrderPage } from '#/api/ops/order-center';
|
||||
|
||||
import {
|
||||
ORDER_TYPE_COLOR_MAP,
|
||||
ORDER_TYPE_TEXT_MAP,
|
||||
PRIORITY_STYLE_MAP,
|
||||
STATUS_COLOR_MAP,
|
||||
STATUS_ICON_MAP,
|
||||
STATUS_TEXT_MAP,
|
||||
@@ -43,7 +43,7 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
|
||||
// ========== 模拟数据开关 ==========
|
||||
const USE_MOCK_DATA = true;
|
||||
const USE_MOCK_DATA = false;
|
||||
|
||||
// 模拟工单数据
|
||||
const MOCK_ORDER_LIST: OpsOrderCenterApi.OrderItem[] = [
|
||||
@@ -153,6 +153,101 @@ const queryParams = ref({
|
||||
pageSize: 12,
|
||||
});
|
||||
|
||||
/** 字典相关 */
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
function getSourceTypeInfo(value: string) {
|
||||
const option = sourceTypeOptions.value.find((item) => item.value === value);
|
||||
if (!option) return { label: value, color: 'default' };
|
||||
|
||||
// 优先使用字典配置的 cssClass 作为颜色 (如果匹配 Ant Design Tag 的预设颜色)
|
||||
// 或者如果你的字典有专门的 color 字段也可以在这里处理
|
||||
if (option.cssClass) {
|
||||
return { label: option.label, color: option.cssClass };
|
||||
}
|
||||
|
||||
let color = 'default';
|
||||
switch (option.colorType) {
|
||||
case 'danger': {
|
||||
color = 'error';
|
||||
break;
|
||||
}
|
||||
case 'info': {
|
||||
color = 'default';
|
||||
break;
|
||||
}
|
||||
case 'primary': {
|
||||
color = 'processing';
|
||||
break;
|
||||
}
|
||||
case 'success': {
|
||||
color = 'success';
|
||||
break;
|
||||
}
|
||||
case 'warning': {
|
||||
color = 'warning';
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
color = option.colorType || 'default';
|
||||
}
|
||||
}
|
||||
return { label: option.label, color };
|
||||
}
|
||||
|
||||
/** 获取工单列表 */
|
||||
async function getList() {
|
||||
loading.value = true;
|
||||
@@ -181,11 +276,6 @@ function handlePageChange(page: number, pageSize: number) {
|
||||
getList();
|
||||
}
|
||||
|
||||
/** 获取优先级样式 */
|
||||
function getPriorityStyle(priority: number) {
|
||||
return PRIORITY_STYLE_MAP[priority] || PRIORITY_STYLE_MAP[2];
|
||||
}
|
||||
|
||||
/** 判断是否为紧急工单 */
|
||||
function isUrgent(priority: number) {
|
||||
return priority === 0;
|
||||
@@ -202,18 +292,20 @@ function isInProgress(status: string) {
|
||||
}
|
||||
|
||||
/** 格式化时间显示 */
|
||||
function formatTime(time: string) {
|
||||
function formatTime(time: Date | string) {
|
||||
if (!time) return '-';
|
||||
const date = new Date(time);
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - date.getTime();
|
||||
const minutes = Math.floor(diff / 60_000);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
|
||||
if (minutes < 1) return '刚刚';
|
||||
if (minutes < 60) return `${minutes}分钟前`;
|
||||
if (hours < 24) return `${hours}小时前`;
|
||||
return time.slice(5, 16);
|
||||
try {
|
||||
const date = dayjs(time);
|
||||
if (!date.isValid()) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
// 格式化为 YYYY-MM-DD HH:mm:ss
|
||||
return date.format('YYYY-MM-DD HH:mm:ss');
|
||||
} catch {
|
||||
return '-';
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
@@ -232,7 +324,7 @@ onMounted(() => {
|
||||
<template>
|
||||
<div class="work-order-card-view">
|
||||
<!-- 工单卡片列表 -->
|
||||
<div v-loading="loading" class="card-grid">
|
||||
<Spin :spinning="loading" class="card-grid">
|
||||
<Row v-if="list.length > 0" :gutter="[12, 12]">
|
||||
<Col
|
||||
v-for="item in list"
|
||||
@@ -266,15 +358,14 @@ onMounted(() => {
|
||||
}}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="item.priority !== 2"
|
||||
class="priority-tag"
|
||||
:class="`priority-${item.priority}`"
|
||||
:style="getPriorityInfo(item.priority).style"
|
||||
>
|
||||
<IconifyIcon
|
||||
v-if="item.priority === 0"
|
||||
icon="solar:bolt-bold"
|
||||
v-if="getPriorityInfo(item.priority).icon"
|
||||
:icon="getPriorityInfo(item.priority).icon"
|
||||
/>
|
||||
{{ getPriorityStyle(item.priority)?.label }}
|
||||
{{ getPriorityInfo(item.priority).label }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -287,34 +378,63 @@ onMounted(() => {
|
||||
|
||||
<!-- 信息行 -->
|
||||
<div class="info-rows">
|
||||
<!-- 工单编号 -->
|
||||
<div class="info-row">
|
||||
<IconifyIcon
|
||||
icon="solar:bill-list-bold-duotone"
|
||||
class="info-icon"
|
||||
/>
|
||||
<span class="info-text order-code-styled">
|
||||
{{ item.orderCode }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 位置信息 -->
|
||||
<div class="info-row">
|
||||
<IconifyIcon
|
||||
icon="solar:map-point-bold-duotone"
|
||||
class="info-icon"
|
||||
/>
|
||||
<span class="info-text">{{
|
||||
item.location || '未指定位置'
|
||||
}}</span>
|
||||
<span class="info-text location-text">
|
||||
{{ item.location || '未指定位置' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 执行人 -->
|
||||
<div class="info-row">
|
||||
<IconifyIcon
|
||||
icon="solar:user-bold-duotone"
|
||||
class="info-icon"
|
||||
/>
|
||||
<template v-if="item.assigneeName">
|
||||
<Avatar :size="16" class="assignee-avatar">
|
||||
{{ item.assigneeName.charAt(0) }}
|
||||
</Avatar>
|
||||
<span class="info-text">{{ item.assigneeName }}</span>
|
||||
</template>
|
||||
<span
|
||||
v-if="item.assigneeName"
|
||||
class="info-text assignee-text"
|
||||
>
|
||||
{{ item.assigneeName }}
|
||||
</span>
|
||||
<span v-else class="info-text info-text--muted">待分配</span>
|
||||
</div>
|
||||
|
||||
<!-- 触发来源 -->
|
||||
<div v-if="item.sourceType" class="info-row">
|
||||
<IconifyIcon
|
||||
icon="solar:radar-bold-duotone"
|
||||
class="info-icon"
|
||||
/>
|
||||
<Tag
|
||||
:color="getSourceTypeInfo(item.sourceType).color"
|
||||
class="source-tag"
|
||||
:bordered="false"
|
||||
>
|
||||
{{ getSourceTypeInfo(item.sourceType).label }}
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部:元信息 + 操作按钮 -->
|
||||
<div class="card-footer">
|
||||
<!-- 左侧:类型 + 编号 + 时间 -->
|
||||
<!-- 左侧:类型 + 时间 -->
|
||||
<div class="card-meta">
|
||||
<Tag
|
||||
:color="ORDER_TYPE_COLOR_MAP[item.orderType]"
|
||||
@@ -322,7 +442,6 @@ onMounted(() => {
|
||||
>
|
||||
{{ ORDER_TYPE_TEXT_MAP[item.orderType] }}
|
||||
</Tag>
|
||||
<span class="order-code">#{{ item.orderCode.slice(-6) }}</span>
|
||||
<span class="create-time">{{
|
||||
formatTime(item.createTime)
|
||||
}}</span>
|
||||
@@ -409,7 +528,7 @@ onMounted(() => {
|
||||
|
||||
<!-- 空状态 -->
|
||||
<Empty v-else description="暂无工单数据" class="my-16" />
|
||||
</div>
|
||||
</Spin>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div v-if="list.length > 0" class="pagination-wrapper">
|
||||
@@ -451,8 +570,9 @@ onMounted(() => {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 190px;
|
||||
padding: 14px;
|
||||
height: 100%;
|
||||
min-height: 200px;
|
||||
padding: 12px;
|
||||
cursor: pointer;
|
||||
background: #fff;
|
||||
border: 1px solid #f0f0f0;
|
||||
@@ -482,7 +602,7 @@ onMounted(() => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.status-info {
|
||||
@@ -510,15 +630,8 @@ onMounted(() => {
|
||||
border-radius: 4px;
|
||||
|
||||
&.priority-0 {
|
||||
color: #ff4d4f;
|
||||
background: #fff1f0;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
&.priority-1 {
|
||||
color: #fa8c16;
|
||||
background: #fff7e6;
|
||||
}
|
||||
}
|
||||
|
||||
// 主体
|
||||
@@ -531,12 +644,12 @@ onMounted(() => {
|
||||
|
||||
.card-title {
|
||||
display: -webkit-box;
|
||||
margin: 0 0 10px;
|
||||
margin: 0 0 8px;
|
||||
overflow: hidden;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-line-clamp: 2;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
line-height: 1.5;
|
||||
color: #262626;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
@@ -545,32 +658,53 @@ onMounted(() => {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
color: #595959;
|
||||
|
||||
.info-icon {
|
||||
flex-shrink: 0;
|
||||
font-size: 14px;
|
||||
font-size: 15px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.info-text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
word-break: break-all;
|
||||
white-space: normal;
|
||||
|
||||
&--muted {
|
||||
font-style: italic;
|
||||
color: #bfbfbf;
|
||||
}
|
||||
}
|
||||
|
||||
.order-code-styled {
|
||||
padding: 1px 6px;
|
||||
font-family: 'SF Mono', Monaco, monospace;
|
||||
font-size: 12px;
|
||||
color: #595959;
|
||||
background: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.location-text {
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.assignee-text {
|
||||
font-weight: 500;
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
.assignee-avatar {
|
||||
flex-shrink: 0;
|
||||
font-size: 10px !important;
|
||||
@@ -578,6 +712,13 @@ onMounted(() => {
|
||||
color: #fff;
|
||||
background: #1677ff;
|
||||
}
|
||||
|
||||
.source-tag {
|
||||
margin: 0;
|
||||
font-size: 11px;
|
||||
line-height: 18px;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
// 底部区域
|
||||
@@ -586,8 +727,8 @@ onMounted(() => {
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-top: 10px;
|
||||
margin-top: auto;
|
||||
padding-top: 8px;
|
||||
margin-top: 10px;
|
||||
border-top: 1px solid #f5f5f5;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user