From 7f2103a36b56aeafb3baad01284b4f1637fe8130 Mon Sep 17 00:00:00 2001 From: "2358328281@qq.com" <邮箱地址> Date: Fri, 3 Jul 2026 15:36:01 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=9D=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../image/hospital/ponits/arrow-down.svg | 5 + src/assets/image/hospital/ponits/arrow-up.svg | 5 + .../image/hospital/ponits/icon-sign-large.svg | 15 + .../hospital/ponits/icon-submit-large.svg | 19 + .../image/hospital/ponits/rili-small.svg | 5 + src/assets/style/admin-common.scss | 16 + src/components/RewardResultDialog.vue | 241 +++++++ src/components/layout/AdminLayout.vue | 119 +++- src/stores/assetDialog.js | 44 ++ src/stores/rewardResult.js | 66 ++ src/stores/signInDialog.js | 129 ++++ .../accounts/components/AccountEditDialog.vue | 77 +- src/views/admin/hospitals/list.vue | 5 +- .../orders/components/OrderFormDialog.vue | 28 - src/views/admin/orders/list.vue | 10 +- src/views/admin/performance/score.vue | 2 +- src/views/hospital/LotteryDialog.vue | 314 ++------- src/views/hospital/SignInDialog.vue | 45 +- .../hospital/orders/OrderDetailDialog.vue | 391 +++++++++++ src/views/hospital/orders/OrderFormDialog.vue | 659 +++++++----------- src/views/hospital/orders/list.vue | 532 +++++++------- src/views/hospital/points.vue | 28 +- src/views/hospital/workbench.vue | 433 ++++++++---- 23 files changed, 1973 insertions(+), 1215 deletions(-) create mode 100644 src/assets/image/hospital/ponits/arrow-down.svg create mode 100644 src/assets/image/hospital/ponits/arrow-up.svg create mode 100644 src/assets/image/hospital/ponits/icon-sign-large.svg create mode 100644 src/assets/image/hospital/ponits/icon-submit-large.svg create mode 100644 src/assets/image/hospital/ponits/rili-small.svg create mode 100644 src/components/RewardResultDialog.vue create mode 100644 src/stores/assetDialog.js create mode 100644 src/stores/rewardResult.js create mode 100644 src/stores/signInDialog.js create mode 100644 src/views/hospital/orders/OrderDetailDialog.vue diff --git a/src/assets/image/hospital/ponits/arrow-down.svg b/src/assets/image/hospital/ponits/arrow-down.svg new file mode 100644 index 0000000..e979da1 --- /dev/null +++ b/src/assets/image/hospital/ponits/arrow-down.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/image/hospital/ponits/arrow-up.svg b/src/assets/image/hospital/ponits/arrow-up.svg new file mode 100644 index 0000000..839cfe9 --- /dev/null +++ b/src/assets/image/hospital/ponits/arrow-up.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/image/hospital/ponits/icon-sign-large.svg b/src/assets/image/hospital/ponits/icon-sign-large.svg new file mode 100644 index 0000000..ba5b0e5 --- /dev/null +++ b/src/assets/image/hospital/ponits/icon-sign-large.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/assets/image/hospital/ponits/icon-submit-large.svg b/src/assets/image/hospital/ponits/icon-submit-large.svg new file mode 100644 index 0000000..634ce0b --- /dev/null +++ b/src/assets/image/hospital/ponits/icon-submit-large.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/image/hospital/ponits/rili-small.svg b/src/assets/image/hospital/ponits/rili-small.svg new file mode 100644 index 0000000..695bc5b --- /dev/null +++ b/src/assets/image/hospital/ponits/rili-small.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/style/admin-common.scss b/src/assets/style/admin-common.scss index 5bcd420..d8b6e86 100644 --- a/src/assets/style/admin-common.scss +++ b/src/assets/style/admin-common.scss @@ -144,6 +144,14 @@ $primaryLightBlue: #3289fb; padding: 8px 30px !important; line-height: 18px !important; border-radius: 8px !important; + cursor: pointer; + + + &.disabled-done { + opacity: 0.3; + pointer-events: none; + cursor: not-allowed; + } &.smaller { padding: 6px 15px !important; @@ -180,4 +188,12 @@ $primaryLightBlue: #3289fb; --el-button-text-color: #00c0e8 !important; } } + + &.yellow-sign { + --el-button-border-color: #ff383c !important; + + &.is-plain { + --el-button-text-color: #ff383c !important; + } + } } diff --git a/src/components/RewardResultDialog.vue b/src/components/RewardResultDialog.vue new file mode 100644 index 0000000..f41cc49 --- /dev/null +++ b/src/components/RewardResultDialog.vue @@ -0,0 +1,241 @@ + + + + + + + + + {{ title }} + + + + {{ points }} + 积分 + + + + 距离兑换 {{ rewardName }} + 还差{{ batchRemaining }}分 + + + + + + + + 开心收下 + 查看我的资产 + + + + + + + + diff --git a/src/components/layout/AdminLayout.vue b/src/components/layout/AdminLayout.vue index cbdc3bc..2a56a06 100644 --- a/src/components/layout/AdminLayout.vue +++ b/src/components/layout/AdminLayout.vue @@ -59,7 +59,7 @@ - + @@ -90,11 +90,29 @@ + + + + + + + + + \ No newline at end of file + diff --git a/src/views/hospital/SignInDialog.vue b/src/views/hospital/SignInDialog.vue index 6294b8f..39d0afa 100644 --- a/src/views/hospital/SignInDialog.vue +++ b/src/views/hospital/SignInDialog.vue @@ -6,7 +6,7 @@ - 连续签到{{ streakDays }}天 + 连续签到{{ streakDays }}天 排名第{{ rank }}名 暂无排名 @@ -70,8 +70,11 @@ import { computed, ref, watch } from "vue"; import dayjs from "dayjs"; import { ElMessage } from "element-plus"; -import { signIn } from "@/service/modular/hospital"; +import { signIn, getPoints } from "@/service/modular/hospital"; +import { useSignInDialogStore } from "@/stores/signInDialog"; +import { useRewardResultStore } from "@/stores/rewardResult"; import checkIcon from "@/assets/image/hospital/ponits/coin.svg"; +import iconSign from "@/assets/image/hospital/ponits/icon-sign-large.svg"; const props = defineProps({ modelValue: { type: Boolean, default: false }, @@ -141,6 +144,8 @@ watch( ); const submitting = ref(false); +const signInDialogStore = useSignInDialogStore(); +const rewardResultStore = useRewardResultStore(); const onDialogUpdate = (val) => { if (submitting.value) return; @@ -151,6 +156,16 @@ const onClosed = () => { submitting.value = false; }; +// 取最新积分(用于结果区进度条) +const fetchLatestPoints = async () => { + try { + const res = unwrap(await getPoints()); + return Number(res?.totalPoints) || 0; + } catch (_) { + return 0; + } +}; + // ============ 签到 ============ const handleSignIn = async () => { if (props.signedIn) { @@ -163,8 +178,28 @@ const handleSignIn = async () => { ElMessage.success("签到成功! +1 积分 🎉"); // 重新生成 7 天(刷新签到状态) days.value = buildSignInDays(props.consecutiveSignInDays, props.signedIn); - emit("success"); + + // 取最新积分 + 关闭签到弹窗 + 打开全局奖励结果弹窗 + const totalPoints = await fetchLatestPoints(); + + // 先关闭签到弹窗(在父层),再下一帧 open 全局结果 emit("update:modelValue", false); + + setTimeout(() => { + rewardResultStore.open({ + title: "签到成功", + icon: iconSign, + points: 1, + batchGoal: 500, + batchCurrent: totalPoints, + rewardName: "康小虎 AI 吉祥物", + }); + }, 100); + + // 通知外层 signInDialogStore 触发 onSigned 回调(同步刷新 summary) + signInDialogStore.fireSigned(); + + emit("success"); } catch (e) { ElMessage.error(e?.message || "签到失败"); } finally { @@ -344,8 +379,6 @@ const handleSignIn = async () => { } } - - .day-points-text { color: #BCBCBC; } @@ -368,7 +401,7 @@ const handleSignIn = async () => { line-height: 42px; text-align: center; margin: 0 auto; - + &:hover:not(:disabled) { transform: translateY(-1px); box-shadow: 0 6px 14px rgba(255, 138, 40, 0.4); diff --git a/src/views/hospital/orders/OrderDetailDialog.vue b/src/views/hospital/orders/OrderDetailDialog.vue new file mode 100644 index 0000000..b9bd1e6 --- /dev/null +++ b/src/views/hospital/orders/OrderDetailDialog.vue @@ -0,0 +1,391 @@ + + $emit('update:modelValue', v)" @open="onOpen" @closed="cleanupPreview"> + + + {{ $t('modal.detail') }} + + + + + + + + + 工单号 + {{ detail.orderNo }} + + + 当前状态 + {{ detail.status }} + + + 优先级 + {{ detail.priority }} + + + 服务类型 + {{ detail.serviceType }} + + + + + + 问题名称 + {{ detail.title }} + + + + + + 问题描述 + + + + + + + + + 附件 + + + + + + + {{ f.fileName }} + + + + + + + + + 提出科室 + {{ detail.department || '-' }} + + + 提出人员 + {{ detail.submitter || '-' }} + + + 提交时间 + {{ detail.createdAt?.split('T').join(' ') || '-' }} + + + + + + + {{ $t('orderDetail.statusChange') }} + + + {{ log.action }} · {{ log.operator }} + + + + + + + + + + + + + (previewVisible = v)" @close="cleanupPreview" + @download="onPreviewDownload" /> + + + + + + \ No newline at end of file diff --git a/src/views/hospital/orders/OrderFormDialog.vue b/src/views/hospital/orders/OrderFormDialog.vue index 6100946..c1f6323 100644 --- a/src/views/hospital/orders/OrderFormDialog.vue +++ b/src/views/hospital/orders/OrderFormDialog.vue @@ -1,52 +1,30 @@ - - + + $emit('update:modelValue', v)" @closed="onClosed"> - - {{ isEdit() ? '编辑工单' : $t('modal.submit') }} - - + {{ isEdit() ? '编辑工单' : '新建工单' }} + - - - - - - - - {{ $t('label.colorTag') }} - - - - - - - - - - - - - - - - - - - - 高 - 中 - 低 - + + + + + + + + + + + - + + + + style="width: 100%"> @@ -56,18 +34,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + {{ form.title.length + }}/300 + + + + + + + {{ $t('label.colorTag') }} + + + + + + + + {{ $t('label.priority') }} + + + {{ value }} + {{ value }} + {{ value }} + + + + + {{ opt.label }} + + + + - - - - - - - - - + :on-exceed="handleAttachmentExceed" :on-remove="handleAttachmentRemove" + :on-change="handleAttachmentChange"> + 添加文件 {{ $t('label.attachmentTip') }} - @@ -97,57 +126,12 @@ - - - - - - - ✦ 奖励已发放 - - 🪙 - - - +1 积分 - - 🎉 到账通知 - 1 积分已放入您的钱包 - - - - 🏆 距兑换「康小虎 AI 吉祥物」 - - - 还差 {{ rewardDistance }} 分 - - 已达标 🎉 - - - - - - - 当前 {{ rewardTotalPoints }} 分 - - - - - - 开心收下 - - - 查看我的资产 → - - - - @@ -171,8 +155,8 @@ import { updateHospitalOrder, getPoints, } from "@/service/modular/hospital"; - -const EXCHANGE_GOAL = 500; +import { useRewardResultStore } from "@/stores/rewardResult"; +import iconSubmit from "@/assets/image/hospital/ponits/icon-submit-large.svg"; const props = defineProps({ modelValue: { type: Boolean, default: false }, @@ -189,6 +173,8 @@ const submitting = ref(false); // 已加载的原始附件 filePath 集合(用于判断用户是否需要保留原附件) const originalAttachments = ref([]); +const rewardResultStore = useRewardResultStore(); + const getAttachmentFileName = (path) => { if (!path) return ""; return String(path).replace(/\\/g, "/").split("/").pop() || ""; @@ -200,7 +186,7 @@ const fillFormFromDetail = (detail) => { form.title = detail.title || ""; form.serviceType = detail.serviceType || ""; form.priority = detail.priority || "中"; - form.feedbackchannel = detail.feedbackChannel || ""; + form.feedbackchannel = detail.feedbackChannel || detail.feedbackchannel || ""; form.department = detail.department || ""; form.colorTag = detail.colorTag || "#667eea"; form.description = detail.description || ""; @@ -220,6 +206,7 @@ const fillFormFromDetail = (detail) => { uid: `attachment-${props.orderId}-0`, name: fileName, status: "success", + size: first.fileSize || 0, }, ]; return; @@ -290,21 +277,17 @@ const colorOptions = [ { label: "红", value: "#ff4d4f" }, ]; const colorPickerVisible = ref(false); -const colorPickerRef = ref(); -const toggleColorPicker = () => { - colorPickerVisible.value = !colorPickerVisible.value; -}; const selectColor = (val) => { form.colorTag = val; colorPickerVisible.value = false; }; -const onDocClick = (e) => { - if (!colorPickerVisible.value) return; - const el = colorPickerRef.value; - if (el && !el.contains(e.target)) { - colorPickerVisible.value = false; - } -}; + +// 优先级可选值(带高/中/低配色 cls) +const priorityOptions = [ + { label: "高", value: "高", cls: "high" }, + { label: "中", value: "中", cls: "medium" }, + { label: "低", value: "低", cls: "low" }, +]; const MAX_FILE_SIZE = 50 * 1024 * 1024; const MAX_PASTE_IMAGE_SIZE = 5 * 1024 * 1024; @@ -426,46 +409,15 @@ const handleEditorCreated = (editor) => { }); }; -// ============== 提交后奖励弹窗 ============== -const rewardVisible = ref(false); -const rewardTotalPoints = ref(0); -const rewardProgress = ref(0); -const rewardDistance = ref(0); - -const playCoinAnimation = () => { - nextTick(() => { - const coin = document.querySelector(".reward-coin"); - if (!coin) return; - coin.classList.remove("spin"); - void coin.offsetWidth; - coin.classList.add("spin"); - }); -}; - -const showRewardDialog = async () => { - let total = 0; +// ============== 取最新积分 ============== +const fetchLatestPoints = async () => { try { const res = await getPoints(); const data = res?.data ?? res ?? {}; - total = Number(data.totalPoints) || 0; + return Number(data.totalPoints) || 0; } catch (_) { - // 刷新失败不阻塞 + return 0; } - rewardTotalPoints.value = total; - rewardProgress.value = Math.min(100, Math.round((total / EXCHANGE_GOAL) * 100)); - rewardDistance.value = Math.max(0, EXCHANGE_GOAL - total); - rewardVisible.value = true; - playCoinAnimation(); -}; - -const onRewardClose = () => { - rewardVisible.value = false; - emit("success"); -}; - -const goMyAssets = () => { - rewardVisible.value = false; - emit("success", { gotoAssets: true }); }; const submit = async () => { @@ -493,7 +445,20 @@ const submit = async () => { } else { await createHospitalOrder(formData); ElMessage.success($t('msg.submitSuccess')); - showRewardDialog(); + // 取最新积分,关闭自己,打开全局奖励结果弹窗 + const totalPoints = await fetchLatestPoints(); + emit("update:modelValue", false); + setTimeout(() => { + rewardResultStore.open({ + title: "提交工单", + icon: iconSubmit, + points: 1, + batchGoal: 500, + batchCurrent: totalPoints, + rewardName: "康小虎 AI 吉祥物", + }); + }, 100); + emit("success", { isCreate: true }); } } finally { submitting.value = false; @@ -505,10 +470,6 @@ const onCancel = () => { emit("update:modelValue", false); }; -const onDialogUpdate = (val) => { - emit("update:modelValue", val); -}; - const onClosed = () => { // 关闭时重置表单(编辑模式下重新打开会重新加载详情) form.title = ""; @@ -535,8 +496,6 @@ watch( // 新增模式:默认提交时间为当前时间 form.createdat = nowDatetime(); } - // 绑定全局点击关闭颜色面板 - document.addEventListener("click", onDocClick); }, ); @@ -550,7 +509,6 @@ const removePasteListener = () => { }; onBeforeUnmount(() => { - document.removeEventListener("click", onDocClick); removePasteListener(); pasteHandlerRef = null; const editor = editorRef.value; @@ -561,54 +519,92 @@ onBeforeUnmount(() => { + + diff --git a/src/views/hospital/orders/list.vue b/src/views/hospital/orders/list.vue index 0d80302..4cf1fc2 100644 --- a/src/views/hospital/orders/list.vue +++ b/src/views/hospital/orders/list.vue @@ -1,216 +1,119 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {{ - $t("btn.query") - }} - {{ $t("btn.reset") }} - {{ - $t("btn.quickSubmit") - }} - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ + $t("btn.query") + }} + {{ $t("btn.reset") }} + + 新建工单 + + + + + + + + + {{ row.priority }} + + + + + + + + + + + + {{ row.completedAt || "-" }} + + + + + {{ row.status }} + + + + + + {{ + $t("btn.view") + }} + {{ $t("btn.edit") }} - {{ $t("btn.cancel") }} - - - - - - - - + {{ + $t("btn.cancel") }} + + + + + + + + + + @@ -223,6 +126,7 @@ import { ElMessage, ElMessageBox } from "element-plus"; import { useI18n } from "vue-i18n"; import { getHospitalOrders, cancelHospitalOrder } from "@/service/modular/hospital"; import { useOrderDialogStore } from "@/stores/orderDialog"; +import OrderDetailDialog from "./OrderDetailDialog.vue"; const { t: $t } = useI18n(); const route = useRoute(); @@ -240,37 +144,44 @@ const filters = reactive({ keyword: "", }); -const priorityType = (p) => - p === "高" ? "danger" : p === "中" ? "warning" : "info"; -const statusType = (s) => - ({ 待处理: "warning", 处理中: "primary", 已完成: "success", 已取消: "info" })[ - s - ] || "info"; +const priorityClass = (p) => { + if (p === "高") return "high"; + if (p === "中") return "medium"; + if (p === "低") return "low"; + return "info"; +}; +const statusClass = (s) => { + if (s === "已完成") return "done"; + if (s === "待处理") return "pending"; + if (s === "处理中") return "processing"; + if (s === "已取消") return "cancel"; + return "info"; +}; // 工单总览统计 -const stats = reactive({ - total: 0, - pending: 0, - completed: 0, - highPriority: 0, -}); +// const stats = reactive({ +// total: 0, +// pending: 0, +// completed: 0, +// highPriority: 0, +// }); // 当前选中的总览卡片:total | pending | completed | high -const activeStat = ref("total"); -const parseStats = (res) => { - let payload = res?.data; - if (typeof payload === "string") { - try { - payload = JSON.parse(payload); - } catch (_) { - payload = {}; - } - } +// const activeStat = ref("total"); +// const parseStats = (res) => { +// let payload = res?.data; +// if (typeof payload === "string") { +// try { +// payload = JSON.parse(payload); +// } catch (_) { +// payload = {}; +// } +// } - stats.completed = Number(payload?.TotalStatus1 ?? 0); - stats.pending = Number(payload?.TotalStatus2 ?? 0); - stats.total = stats.completed + stats.pending; - stats.highPriority = Number(payload?.TotalPriority ?? 0); -}; +// stats.completed = Number(payload?.TotalStatus1 ?? 0); +// stats.pending = Number(payload?.TotalStatus2 ?? 0); +// stats.total = stats.completed + stats.pending; +// stats.highPriority = Number(payload?.TotalPriority ?? 0); +// }; const loadList = async () => { loading.value = true; @@ -293,7 +204,7 @@ const loadList = async () => { } }); total.value = res?.total || 0; - parseStats(res); + // parseStats(res); } finally { loading.value = false; } @@ -387,7 +298,15 @@ const clearListState = () => { } }; -const goDetail = (id) => router.push(`/hospital/orders/detail/${id}`); +const goDetail = (id) => { + // "查看" 改为弹窗:复用 OrderDetailDialog + currentOrderId.value = id; + detailVisible.value = true; +}; + +// 工单详情弹窗状态 +const currentOrderId = ref(""); +const detailVisible = ref(false); // 工单表单弹窗:通过全局 store 控制(弹窗本体挂在 AdminLayout) const orderDialogStore = useOrderDialogStore(); @@ -457,85 +376,106 @@ onBeforeUnmount(() => { + + diff --git a/src/views/hospital/points.vue b/src/views/hospital/points.vue index fff0e81..a071221 100644 --- a/src/views/hospital/points.vue +++ b/src/views/hospital/points.vue @@ -28,15 +28,17 @@ - + 本月 +{{ monthPoints }} - + + 今日 - +{{ todayPoints }} + {{ todayPoints >= 0 ? '+' + todayPoints : todayPoints }} @@ -165,7 +167,8 @@ - + 填写收件信息,即可兑换「{{ REWARD_NAME }}」 @@ -198,19 +201,15 @@ - - - + :batch-current="Number(summary.rewardCount) || 5" @success="onLotterySuccess" /> + + @@ -283,7 +282,6 @@ import { ElMessage, ElMessageBox } from "element-plus"; import dayjs from "dayjs"; import SvgIcon from "@/components/SvgIcon/index.vue"; import LotteryDialog from "@/views/hospital/LotteryDialog.vue"; -import AssetsDialog from "@/views/hospital/AssetsDialog.vue"; import SignInDialog from "@/views/hospital/SignInDialog.vue"; import { useOrderDialogStore } from "@/stores/orderDialog"; import { @@ -428,8 +426,8 @@ const onSignInSuccess = async () => { }; // 操作:抽奖(只负责打开弹窗,抽奖逻辑在 LotteryDialog 中) +// “查看我的资产”由 RewardResultDialog 内部触发全局 AssetsDialog,不再需要本地 assetsVisible const lotteryVisible = ref(false); -const assetsVisible = ref(false); const handleDraw = () => { if (summary.value.isDraw) { ElMessage.info("今日已抽奖,明天再来吧!"); @@ -440,10 +438,6 @@ const handleDraw = () => { const onLotterySuccess = async () => { await loadAll(); }; -const onLotteryViewAccount = () => { - // 抽奖结果页的"查看我的账户" -> 打开资产弹窗 - assetsVisible.value = true; -}; // 今日积分明细(按来源分类汇总) const todayBreakdown = computed(() => { diff --git a/src/views/hospital/workbench.vue b/src/views/hospital/workbench.vue index 333fc57..b9578a8 100644 --- a/src/views/hospital/workbench.vue +++ b/src/views/hospital/workbench.vue @@ -1,27 +1,33 @@ - + - - {{ $t('common.thisMonth') }} - {{ $t('common.thisQuarter') }} - {{ $t('common.thisYear') }} - {{ $t('common.customDate') }} - - + + + + {{ t.label }} + + {{ + $t('common.customDate') + }} + + + + 查询 + + - - - - - + + + - + {{ card.value }} {{ card.label }} @@ -29,22 +35,30 @@ - - - - {{ $t('chart.title') }} - - - {{ $t('chart.bar') }} - - - {{ $t('chart.line') }} - - + + + {{ $t('chart.title') }} + + + {{ $t('chart.bar') }} + + + {{ $t('chart.line') }} + + + + + + + + {{ $t('chart.submit') }} - - - + + + {{ $t('chart.complete') }} + + + @@ -53,7 +67,10 @@ import dayjs from "dayjs"; import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue"; import { dateRangeShortcuts } from "@/utils/dateShortcuts"; import { useRouter } from "vue-router"; -import { Tickets, Check, Bell, Warning } from "@element-plus/icons-vue"; +import iconAll from "@/assets/image/admin/orders/all.svg"; +import iconPending from "@/assets/image/admin/orders/pending.svg"; +import iconComplete from "@/assets/image/admin/orders/complete.svg"; +import iconHigh from "@/assets/image/admin/orders/high.svg"; import VChart from "vue-echarts"; import { use } from "echarts/core"; import { CanvasRenderer } from "echarts/renderers"; @@ -72,10 +89,33 @@ use([CanvasRenderer, LineChart, BarChart, GridComponent, TooltipComponent, Legen const { t: $t } = useI18n(); +// 时间筛选 tab(不包含"本周",自定义点查询才触发) +const periodTabs = [ + { label: $t('common.thisMonth'), value: "month" }, + { label: $t('common.thisQuarter'), value: "quarter" }, + { label: $t('common.thisYear'), value: "year" }, +]; + const period = ref("month"); +const isCustom = ref(false); const dateRange = ref([]); const chartType = ref("line"); +const onPickPeriod = (val) => { + isCustom.value = false; + dateRange.value = []; + period.value = val; + loadStats(); +}; + +const onPickCustom = () => { + isCustom.value = true; +}; + +const onSearch = () => { + loadStats(); +}; + const stats = ref({ totalOrders: 0, completedOrders: 0, @@ -90,11 +130,12 @@ const chartRef = ref(null); const overlaySize = ref({ w: 0, h: 0 }); const stripeTargets = ref([]); +// 顺序、文案与 src/views/admin/orders/list.vue 的 .order-stats 保持一致 const cards = computed(() => [ - { key: "total", label: $t('stat.total'), value: stats.value.totalOrders, icon: Tickets, color: "#5B8FF9", bg: "rgba(91,143,249,0.12)" }, - { key: "completed", label: $t('stat.completed'), value: stats.value.completedOrders, icon: Check, color: "#5AD8A6", bg: "rgba(90,216,166,0.12)" }, - { key: "pending", label: $t('stat.pending'), value: stats.value.pendingOrders, icon: Bell, color: "#F6BD16", bg: "rgba(246,189,22,0.12)" }, - { key: "highPriority", label: $t('stat.highPriority'), value: stats.value.highPriorityOrders, icon: Warning, color: "#F4664A", bg: "rgba(244,102,74,0.12)" }, + { key: "total", label: $t('stat.totalOrders'), value: stats.value.totalOrders, icon: iconAll }, + { key: "pending", label: $t('stat.pendingOrders'), value: stats.value.pendingOrders, icon: iconPending }, + { key: "completed", label: $t('stat.completedOrders'), value: stats.value.completedOrders, icon: iconComplete }, + { key: "highPriority", label: $t('stat.highPriorityOrders'), value: stats.value.highPriorityOrders, icon: iconHigh }, ]); const allStripeLines = computed(() => { @@ -125,10 +166,10 @@ const goToOrders = (card) => { }; // ==================== ECharts 配置 ==================== -const BAR_W = 14; -const CORNER_R = BAR_W / 8; -const WRAP_THICK = BAR_W / 10; -const WRAP_BORDER = "#eff2fe"; +const BAR_W = 59; +const CORNER_R = 8; +const WRAP_THICK = 8; +const WRAP_BORDER = "#EFF2FE"; const buildLinearGradient = (top, bottom) => ({ type: "linear", @@ -146,7 +187,10 @@ const chartOption = computed(() => { const complete = data.map((d) => d.completeCount); const isLine = chartType.value === "line"; const isBar = chartType.value === "bar"; - const showSlider = dates.length > 20; + // > 7 组才显示滚动条;用 startValue/endValue 锁定最后 7 组,避免百分比误差 + const showSlider = dates.length > 7; + const sliderStartValue = showSlider ? dates[dates.length - 7] : undefined; + const sliderEndValue = showSlider ? dates[dates.length - 1] : undefined; const maxValue = Math.max(0, ...submit, ...complete); const yMax = maxValue > 0 ? Math.ceil(maxValue / 2) * 2 : 4; @@ -167,6 +211,15 @@ const chartOption = computed(() => { lineStyle: { color: submitTopColor, width: 2 }, itemStyle: { color: submitTopColor }, areaStyle: { color: "rgba(50,137,251,0.08)" }, + label: { + show: true, + position: "top", + distance: 8, + color: submitTopColor, + fontWeight: 400, + fontSize: 14, + formatter: (p) => p.value, + }, }, { name: $t('chart.complete'), @@ -178,6 +231,15 @@ const chartOption = computed(() => { lineStyle: { color: completeTopColor, width: 2 }, itemStyle: { color: completeTopColor }, areaStyle: { color: "rgba(52,199,89,0.08)" }, + label: { + show: true, + position: "top", + distance: 8, + color: completeTopColor, + fontWeight: 400, + fontSize: 14, + formatter: (p) => p.value, + }, }, ]; @@ -205,21 +267,13 @@ const chartOption = computed(() => { }); return { - grid: { left: 30, right: 70, top: 50, bottom: showSlider ? 60 : 30 }, + grid: { left: 30, right: 30, top: 50, bottom: showSlider ? 60 : 30 }, tooltip: { trigger: "axis" }, - legend: { - orient: "vertical", - right: 10, - top: "middle", - itemWidth: 10, - itemHeight: 10, - itemGap: 16, - textStyle: { fontSize: 12, color: "#606266" }, - }, xAxis: { type: "category", data: dates, - boundaryGap: !isLine, + // 折线/柱状图统一带 boundaryGap,让首末坐标点/柱距边留出与柱状图一致的间距 + boundaryGap: true, ...(isBar ? { barCategoryGap: "7%" } : {}), axisLine: { lineStyle: { color: "#e4e7ed" } }, }, @@ -231,10 +285,24 @@ const chartOption = computed(() => { axisLine: { show: false }, splitLine: { lineStyle: { color: "#f0f2f5" } }, }, - dataZoom: [ - { type: "inside", xAxisIndex: 0, zoomOnMouseWheel: true, moveOnMouseMove: true, moveOnMouseWheel: false }, - ...(showSlider ? [{ type: "slider", xAxisIndex: 0, bottom: 10, height: 20, start: 0, end: 100, showDetail: false }] : []), - ], + dataZoom: showSlider + ? [ + { + type: "slider", + xAxisIndex: 0, + bottom: 0, + height: 14, + startValue: sliderStartValue, + endValue: sliderEndValue, + zoomLock: true, + showDetail: false, + showDataShadow: false, + fillerColor: "rgba(50,137,251,0.12)", + borderColor: "transparent", + backgroundColor: "#F2F3F5", + }, + ] + : [], series: isLine ? lineSeries : [ { ...makeBarSeries(submit, submitTopColor, submitWrapColor), name: $t('chart.submit') }, { ...makeBarSeries(complete, completeTopColor, completeWrapColor), name: $t('chart.complete') }, @@ -324,10 +392,13 @@ const buildMockTrendData = (startDate, endDate) => { }; const loadStats = async () => { - const params = { period: period.value }; - if (period.value === "custom" && dateRange.value?.length === 2) { + // 自定义模式以 isCustom 为准(自定义需点查询才进这里,与 score.vue 一致) + const params = {}; + if (isCustom.value && dateRange.value?.length === 2) { params.start = dateRange.value[0]; params.end = dateRange.value[1]; + } else if (!isCustom.value) { + params.period = period.value; } try { const data = await getWorkbench(params); @@ -356,57 +427,74 @@ onMounted(loadStats); gap: 16px; } -.filter-bar { - background: #fff; - padding: 12px 16px; - border-radius: 6px; -} - +// 统计卡片:与 src/views/admin/orders/list.vue .order-stats 同款(workbench 无 active 态,省略相关样式) .stat-cards { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; + margin-top: 5px; } -.stat-cards .stat-card { - background: #fff; - border-radius: 6px; - padding: 20px; +.stat-card { + position: relative; display: flex; align-items: center; - gap: 16px; + gap: 18px; + padding: 18px 24px; + background: #fff; + border-radius: 16px 16px 16px 16px; + border: 1px solid #e0e0e0; + transition: + transform 0.2s ease, + box-shadow 0.2s ease, + background 0.2s ease; cursor: pointer; - transition: box-shadow 0.2s, transform 0.2s; + user-select: none; } -.stat-cards .stat-card:hover { - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); +.stat-card:hover { transform: translateY(-2px); + box-shadow: 0 6px 18px rgba(0, 0, 0, 0.08); } -.stat-cards .stat-icon { - width: 48px; - height: 48px; - border-radius: 8px; +.stat-icon { display: flex; align-items: center; justify-content: center; + line-height: 1; +} + +.stat-icon img { + width: 48px; + height: 48px; + object-fit: contain; } -.stat-cards .stat-info .stat-value { - font-size: 24px; - font-weight: 600; - color: #303133; +// 右侧文字区(value + label 垂直堆叠,column-reverse 让 value 在 label 上方) +.stat-card .right { + display: flex; + flex-direction: column-reverse; + gap: 2px; + min-width: 0; +} + +.stat-value { + font-size: 26px; + font-weight: 700; + color: #333; + line-height: 1.1; } -.stat-cards .stat-info .stat-label { +.stat-label { font-size: 13px; - color: #909399; + color: #999; margin-top: 4px; } -.trend-card :deep(.el-card__header) { - padding: 12px 16px; +.trend-card { + border-radius: 16px 16px 16px 16px; + border: 1px solid #E0E0E0; + padding: 21px 16px; } .trend-card .card-header { @@ -415,70 +503,104 @@ onMounted(loadStats); justify-content: space-between; } -.trend-card .card-header .card-title { - font-size: 15px; - font-weight: 600; - color: #303133; -} +// 类 el-switch 分段控件:外层浅紫底,内项平分,激活态由 ::before 滑动白块承载(带过渡动画) .trend-card .card-header .chart-switch { + position: relative; display: inline-flex; - background: #f5f7fa; - border-radius: 4px; - padding: 2px; + width: 220px; + height: 31px; + padding: 4px; + background: #F9FAFF; + border-radius: 8px; + overflow: hidden; +} + +.trend-card .card-header .chart-switch::before { + content: ""; + position: absolute; + top: 4px; + left: 4px; + width: calc(50% - 4px); + height: calc(100% - 8px); + background: #FFFFFF; + border-radius: 6px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04); + transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); + z-index: 0; +} + +.trend-card .card-header .chart-switch.is-line::before { + transform: translateX(100%); } .trend-card .card-header .chart-switch .switch-item { + flex: 1; + height: 100%; display: inline-flex; align-items: center; - gap: 4px; - padding: 4px 10px; - font-size: 12px; - color: #606266; + justify-content: center; + font-size: 14px; + font-weight: 400; + color: #666666; cursor: pointer; - border-radius: 3px; - transition: all 0.2s; user-select: none; + position: relative; + z-index: 1; + transition: color 0.3s ease; } -.trend-card .card-header .chart-switch .switch-item .switch-icon { - width: 10px; - height: 10px; - background: #5AD8A6; - border-radius: 1px; +.trend-card .card-header .chart-switch .switch-item:hover { + color: #3289FB; } -.trend-card .card-header .chart-switch .switch-item .switch-icon.line { - background: #5B8FF9; +.trend-card .card-header .chart-switch .switch-item.active { + color: #3289FB; } -.trend-card .card-header .chart-switch .switch-item:hover { - color: #409eff; +.trend-card .chart-wrap { + position: relative; } -.trend-card .card-header .chart-switch .switch-item.active { - background: #409eff; - color: #fff; +.trend-card .chart { + height: 340px; } -.trend-card .card-header .chart-switch .switch-item.active .switch-icon { - background: #fff; +// 自定义图例:水平居中,色块 + 文字两部分,提交/完成配色与图表系列保持一致 +.trend-card .chart-legend { + display: flex; + justify-content: center; + align-items: center; + gap: 30px; + padding-top: 12px; } -.trend-card .card-header .chart-switch .switch-item.active .switch-icon.line { - background: #fff; +.trend-card .chart-legend .legend-item { + display: inline-flex; + align-items: center; + gap: 4px; } -.trend-card .card-header .chart-switch .switch-item.active:hover { - color: #fff; +.trend-card .chart-legend .legend-dot { + display: inline-block; + width: 28px; + height: 16px; + border-radius: 4px; } -.trend-card .chart-wrap { - position: relative; +.trend-card .chart-legend .legend-dot--submit { + background: #3289FB; } -.trend-card .chart { - height: 340px; +.trend-card .chart-legend .legend-dot--complete { + background: #34C759; +} + +.trend-card .chart-legend .legend-text { + font-size: 14px; + font-weight: 400; + color: #171F2A; + line-height: 1; } .trend-card .stripe-overlay { @@ -487,4 +609,75 @@ onMounted(loadStats); left: 0; pointer-events: none; } + + + \ No newline at end of file