master
2358328281@qq.com 2 months ago
parent 64af1aa70e
commit 7f2103a36b

@ -0,0 +1,5 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Frame">
<path id="Arrow 1" d="M9.25 2C9.25 1.58579 9.58579 1.25 10 1.25C10.4142 1.25 10.75 1.58579 10.75 2L10 2L9.25 2ZM10.5303 17.5303C10.2374 17.8232 9.76256 17.8232 9.46967 17.5303L4.6967 12.7574C4.40381 12.4645 4.40381 11.9896 4.6967 11.6967C4.98959 11.4038 5.46447 11.4038 5.75736 11.6967L10 15.9393L14.2426 11.6967C14.5355 11.4038 15.0104 11.4038 15.3033 11.6967C15.5962 11.9896 15.5962 12.4645 15.3033 12.7574L10.5303 17.5303ZM10 2L10.75 2L10.75 17L10 17L9.25 17L9.25 2L10 2Z" fill="#FC0505"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 616 B

@ -0,0 +1,5 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Frame">
<path id="Arrow 1" d="M9.25 18C9.25 18.4142 9.58579 18.75 10 18.75C10.4142 18.75 10.75 18.4142 10.75 18L10 18L9.25 18ZM10.5303 2.46967C10.2374 2.17678 9.76256 2.17678 9.46967 2.46967L4.6967 7.24264C4.40381 7.53553 4.40381 8.01041 4.6967 8.3033C4.98959 8.59619 5.46447 8.59619 5.75736 8.3033L10 4.06066L14.2426 8.3033C14.5355 8.59619 15.0104 8.59619 15.3033 8.3033C15.5962 8.01041 15.5962 7.53553 15.3033 7.24264L10.5303 2.46967ZM10 18L10.75 18L10.75 3L10 3L9.25 3L9.25 18L10 18Z" fill="#34C759"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 620 B

@ -0,0 +1,15 @@
<svg width="88" height="88" viewBox="0 0 88 88" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Frame" clip-path="url(#clip0_1500_984)">
<path id="Vector" d="M88 44C88 55.6695 83.3643 66.8611 75.1127 75.1127C66.8611 83.3643 55.6695 88 44 88C32.3305 88 21.1389 83.3643 12.8873 75.1127C4.6357 66.8611 0 55.6695 0 44C0 32.3305 4.6357 21.1389 12.8873 12.8873C21.1389 4.6357 32.3305 0 44 0C55.6695 0 66.8611 4.6357 75.1127 12.8873C83.3643 21.1389 88 32.3305 88 44Z" fill="url(#paint0_linear_1500_984)"/>
<path id="Vector_2" d="M62.7635 24.2976H52.3967V22.4213C52.3967 21.9053 52.1856 21.4362 51.8572 21.0844C51.5054 20.756 51.0363 20.5449 50.5203 20.5449C49.4883 20.5449 48.644 21.3893 48.644 22.4213V24.2976H39.2622V22.4213C39.2622 21.9053 39.0511 21.4362 38.7228 21.0844C38.371 20.756 37.9019 20.5449 37.3859 20.5449C36.3539 20.5449 35.5095 21.3893 35.5095 22.4213V24.2976H25.2365C22.6565 24.2976 20.5457 26.4085 20.5457 28.9885V62.7627C20.5457 65.3427 22.6565 67.4536 25.2365 67.4536H62.7635C65.3435 67.4536 67.4543 65.3427 67.4543 62.7627V28.9885C67.4543 26.3851 65.3435 24.2976 62.7635 24.2976ZM53.757 45.9694L42.1002 57.6966L42.0298 57.767C41.936 57.8608 41.8422 57.9311 41.7484 58.0015C41.6546 58.0719 41.5373 58.1188 41.42 58.1657C41.2558 58.2361 41.0682 58.283 40.904 58.283H40.7164C40.2473 58.283 39.7782 58.0953 39.4029 57.7435L39.356 57.6966L34.1961 52.4897C33.469 51.7627 33.469 50.5899 34.1961 49.8863C34.5714 49.511 35.0404 49.3469 35.4861 49.3469C35.9552 49.3469 36.4242 49.5345 36.7995 49.8863L40.7164 53.8266L51.1536 43.3425C51.8807 42.6155 53.0534 42.6155 53.757 43.3425C54.1792 43.6709 54.3199 44.1634 54.3199 44.6325C54.3199 45.1016 54.1323 45.5942 53.757 45.9694ZM63.7017 33.6794H24.2983V28.9885C24.2983 28.4725 24.7205 28.0503 25.2365 28.0503H35.5095V29.9267C35.5095 30.9587 36.3539 31.803 37.3859 31.803C38.4179 31.803 39.2622 30.9587 39.2622 29.9267V28.0503H48.644V29.9267C48.644 30.9587 49.4883 31.803 50.5203 31.803C51.5523 31.803 52.3967 30.9587 52.3967 29.9267V28.0503H62.7635C63.2795 28.0503 63.7017 28.4725 63.7017 28.9885V33.6794Z" fill="white"/>
</g>
<defs>
<linearGradient id="paint0_linear_1500_984" x1="44" y1="0" x2="44" y2="88" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFAD28"/>
<stop offset="1" stop-color="#FF8511"/>
</linearGradient>
<clipPath id="clip0_1500_984">
<rect width="88" height="88" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

@ -0,0 +1,19 @@
<svg width="88" height="88" viewBox="0 0 88 88" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Frame" clip-path="url(#clip0_1500_987)">
<path id="Vector" d="M88 44C88 55.6695 83.3643 66.8611 75.1127 75.1127C66.8611 83.3643 55.6695 88 44 88C32.3305 88 21.1389 83.3643 12.8873 75.1127C4.6357 66.8611 0 55.6695 0 44C0 32.3305 4.6357 21.1389 12.8873 12.8873C21.1389 4.6357 32.3305 0 44 0C55.6695 0 66.8611 4.6357 75.1127 12.8873C83.3643 21.1389 88 32.3305 88 44Z" fill="url(#paint0_linear_1500_987)"/>
<g id="Frame_2">
<path id="Vector_2" d="M33.7447 31.4282H52.5492C53.4969 31.4343 54.4106 31.0809 55.0997 30.4416C55.4313 30.1342 55.6955 29.7634 55.8758 29.352C56.0562 28.9406 56.1489 28.4972 56.1483 28.0493V20.9336H48.95C48.7078 19.5496 47.9684 18.2969 46.8661 17.4029C45.7637 16.509 44.3716 16.0331 42.9425 16.0618C41.5786 16.1084 40.2737 16.6202 39.252 17.5092C38.2304 18.3982 37.5557 19.609 37.3438 20.9336H30.1455V28.058C30.1449 28.506 30.2377 28.9493 30.418 29.3607C30.5983 29.7721 30.8625 30.1429 31.1942 30.4503C31.8832 31.0896 32.7969 31.4431 33.7447 31.4369V31.4282Z" fill="white"/>
<path id="Vector_3" d="M59.7209 48.8027C57.5551 48.801 55.4374 49.4304 53.6358 50.6113C51.8341 51.7922 50.4294 53.4715 49.5994 55.4368C48.7694 57.4022 48.5513 59.5652 48.9727 61.6523C49.3942 63.7395 50.4362 65.6569 51.967 67.1621C53.4979 68.6673 55.4487 69.6926 57.5728 70.1084C59.6968 70.5241 61.8987 70.3116 63.8998 69.4977C65.9009 68.6838 67.6113 67.3052 68.8147 65.5361C70.0182 63.767 70.6605 61.687 70.6605 59.5592C70.6605 56.7079 69.5082 53.9733 67.4569 51.9563C65.4056 49.9393 62.6231 48.8051 59.7209 48.8027ZM66.9281 57.6821L60.5296 63.6802C60.3846 63.8554 60.2017 63.9966 59.9941 64.0936C59.7866 64.1906 59.5597 64.2409 59.3299 64.2409C59.1 64.2409 58.8731 64.1906 58.6656 64.0936C58.458 63.9966 58.2751 63.8554 58.1301 63.6802L54.1311 59.9259C53.9459 59.7997 53.7946 59.6314 53.6902 59.4352C53.5857 59.2391 53.5311 59.021 53.5311 58.7996C53.5311 58.5782 53.5857 58.3602 53.6902 58.164C53.7946 57.9679 53.9459 57.7995 54.1311 57.6733C54.2761 57.4982 54.459 57.3569 54.6665 57.2599C54.874 57.1629 55.101 57.1126 55.3308 57.1126C55.5606 57.1126 55.7875 57.1629 55.9951 57.2599C56.2026 57.3569 56.3855 57.4982 56.5305 57.6733L59.3299 60.2926L64.5286 55.4208C64.6736 55.2456 64.8565 55.1044 65.0641 55.0074C65.2716 54.9104 65.4985 54.86 65.7284 54.86C65.9582 54.86 66.1851 54.9104 66.3926 55.0074C66.6002 55.1044 66.7831 55.2456 66.9281 55.4208C67.1145 55.5474 67.2668 55.7167 67.3717 55.9141C67.4766 56.1115 67.5309 56.331 67.5301 56.5537C67.5292 56.7764 67.4731 56.9955 67.3667 57.1921C67.2602 57.3887 67.1066 57.5569 66.9192 57.6821H66.9281Z" fill="white"/>
<path id="Vector_4" d="M45.9996 61.7421C45.8835 61.1424 45.8093 60.5357 45.7774 59.926H29.3457C29.0844 59.9436 28.8223 59.9082 28.5755 59.822C28.3287 59.7358 28.1027 59.6007 27.9113 59.4251C27.7199 59.2495 27.5673 59.0371 27.463 58.8011C27.3586 58.5651 27.3048 58.3106 27.3048 58.0533C27.3048 57.796 27.3586 57.5414 27.463 57.3054C27.5673 57.0695 27.7199 56.8571 27.9113 56.6814C28.1027 56.5058 28.3287 56.3707 28.5755 56.2845C28.8223 56.1984 29.0844 56.163 29.3457 56.1805H46.1418C46.2573 55.8574 46.3817 55.5344 46.515 55.2114C47.152 53.3357 48.1984 51.6193 49.581 50.1824H29.3457C29.0844 50.1999 28.8223 50.1645 28.5755 50.0783C28.3287 49.9921 28.1027 49.857 27.9113 49.6814C27.7199 49.5058 27.5673 49.2934 27.463 49.0574C27.3586 48.8214 27.3048 48.5669 27.3048 48.3096C27.3048 48.0523 27.3586 47.7978 27.463 47.5618C27.5673 47.3258 27.7199 47.1134 27.9113 46.9378C28.1027 46.7621 28.3287 46.627 28.5755 46.5409C28.8223 46.4547 29.0844 46.4193 29.3457 46.4368H55.8728C57.8148 45.8848 59.8567 45.7597 61.8534 46.0706C63.8501 46.3814 65.7526 47.1205 67.4257 48.2354L68.181 48.6807V27.6828C68.1777 26.7844 67.9866 25.8962 67.6198 25.0732C67.2529 24.2502 66.718 23.51 66.0482 22.8983C64.6692 21.622 62.8417 20.9182 60.9472 20.9338H59.7475V28.0583C59.7486 28.9542 59.5632 29.8408 59.2025 30.6636C58.8418 31.4865 58.3135 32.2281 57.6502 32.8428C56.2711 34.1191 54.4437 34.8229 52.5491 34.8073H33.7447C31.8501 34.8229 30.0227 34.1191 28.6436 32.8428C27.9803 32.2281 27.452 31.4865 27.0913 30.6636C26.7306 29.8408 26.5452 28.9542 26.5463 28.0583V20.9338H24.5379C22.6434 20.9182 20.8159 21.622 19.4369 22.8983C18.7735 23.513 18.2453 24.2546 17.8846 25.0775C17.5239 25.9003 17.3384 26.7869 17.3396 27.6828V65.182C17.3396 66.0765 17.5257 66.9614 17.8863 67.7827C18.247 68.6039 18.7746 69.3441 19.4369 69.9579C20.8146 71.2373 22.6422 71.9443 24.5379 71.931H54.0066C51.9194 71.0142 50.0959 69.6039 48.6987 67.826C47.3015 66.048 46.3742 63.9579 45.9996 61.7421Z" fill="white"/>
</g>
</g>
<defs>
<linearGradient id="paint0_linear_1500_987" x1="44" y1="0" x2="44" y2="88" gradientUnits="userSpaceOnUse">
<stop stop-color="#24DE80"/>
<stop offset="1" stop-color="#0ACB5C"/>
</linearGradient>
<clipPath id="clip0_1500_987">
<rect width="88" height="88" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

@ -0,0 +1,5 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Frame">
<path id="Vector" d="M15.8333 16.834H4.16667V7.66732H15.8333M15.8333 3.50065H15V2.66732C15 2.20708 14.6269 1.83398 14.1667 1.83398C13.7064 1.83398 13.3333 2.20708 13.3333 2.66732V3.50065H6.66667V2.66732C6.66667 2.20708 6.29357 1.83398 5.83333 1.83398C5.3731 1.83398 5 2.20708 5 2.66732V3.50065H4.16667C3.72464 3.50065 3.30072 3.67625 2.98816 3.98881C2.67559 4.30137 2.5 4.72529 2.5 5.16732V16.834C2.5 17.276 2.67559 17.6999 2.98816 18.0125C3.30072 18.3251 3.72464 18.5007 4.16667 18.5007H15.8333C16.2754 18.5007 16.6993 18.3251 17.0118 18.0125C17.3244 17.6999 17.5 17.276 17.5 16.834V5.16732C17.5 4.72529 17.3244 4.30137 17.0118 3.98881C16.6993 3.67625 16.2754 3.50065 15.8333 3.50065ZM13.3333 10.659C13.5773 10.4151 13.5773 10.0196 13.3333 9.77565C13.0894 9.53173 12.6939 9.53173 12.45 9.77565L8.825 13.4007L7.5 12.0757C7.25607 11.8317 6.86059 11.8317 6.61667 12.0757C6.37274 12.3196 6.37274 12.7151 6.61667 12.959L8.33426 14.6766C8.60529 14.9476 9.04471 14.9476 9.31574 14.6766L13.3333 10.659Z" fill="#FF8D28"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@ -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;
}
}
}

@ -0,0 +1,241 @@
<!--
共用"奖励结果"弹窗组件顶层 el-dialog不嵌入任何父级避免被父级 overflow 隐藏
- 用于抽奖 / 签到 / 提交工单 等场景的成功反馈
- 通过 useRewardResultStore 控制显隐
- Props:
title - 标题文案 "恭喜抽中" / "签到成功" / "提交工单"
icon - 大图标 url
points - 本次获得的积分0 / null 时不显示积分区块
batchGoal - 兑换目标积分默认 500
batchCurrent - 当前积分默认 0
rewardName - 兑换物名称默认 "康小虎 AI 吉祥物"
- Events:
claim - "开心收下"
viewAccount - "查看我的资产"
-->
<template>
<el-dialog :model-value="visible" @update:model-value="onDialogUpdate" :show-close="false"
:close-on-click-modal="true" width="auto" align-center append-to-body class="reward-result-dialog"
@closed="onClosed">
<div class="reward-result">
<div class="main-wrap">
<div class="result-icon-wrap">
<img :src="icon" class="result-icon" alt="" />
</div>
<div class="result-title">{{ title }}</div>
<div v-if="showPoints" class="result-points">
<span class="plus">+</span>
<span class="value">{{ points }}&nbsp;&nbsp;</span>
<span class="unit">积分</span>
</div>
<div class="batch-wrap">
<div class="batch-info">
<span class="batch-label">距离兑换 {{ rewardName }}</span>
<span class="batch-remaining">还差{{ batchRemaining }}</span>
</div>
<div class="progress-bar">
<div class="progress-fill" :style="{ width: batchPercent + '%' }" />
</div>
</div>
</div>
<div class="result-actions">
<el-button type="primary" plain class="btn-secondary" @click="onClaim"></el-button>
<el-button type="primary" class="btn-primary" @click="onViewAccount"></el-button>
</div>
</div>
</el-dialog>
</template>
<script setup>
import { computed } from "vue";
import { useRewardResultStore } from "@/stores/rewardResult";
const store = useRewardResultStore();
const visible = computed({
get: () => store.visible,
set: (v) => {
if (!v) store.close();
},
});
const title = computed(() => store.title);
const icon = computed(() => store.icon);
const points = computed(() => store.points);
const batchGoal = computed(() => store.batchGoal);
const batchCurrent = computed(() => store.batchCurrent);
const rewardName = computed(() => store.rewardName);
const showPoints = computed(() => Number(points.value) > 0);
const batchRemaining = computed(() =>
Math.max(0, Number(batchGoal.value) - Number(batchCurrent.value)),
);
const batchPercent = computed(() => {
const goal = Number(batchGoal.value) || 0;
if (goal <= 0) return 0;
return Math.min(100, Math.round((Number(batchCurrent.value) / goal) * 100));
});
const onDialogUpdate = (val) => {
if (!val) store.close();
};
const onClosed = () => {
// , open
store.clear();
};
const onClaim = () => store.close();
const onViewAccount = () => {
store.close();
// :
store.requestViewAccount();
};
</script>
<style lang="scss">
// ===== keyframes =====
@keyframes resultPop {
0% {
transform: scale(0.5);
opacity: 0;
}
60% {
transform: scale(1.1);
opacity: 1;
}
100% {
transform: scale(1);
opacity: 1;
}
}
// ===== =====
.reward-result-dialog {
.el-dialog__header {
display: none;
}
.el-dialog__body {
padding: 0;
}
}
// ===== LotteryDialog .lottery-result =====
.reward-result {
width: 458px;
background: #FFFFFF;
box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.06);
border-radius: 24px;
overflow: hidden;
.main-wrap {
padding: 30px 46px 20px;
display: flex;
flex-direction: column;
align-items: center;
.result-icon-wrap {
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
animation: resultPop 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.result-icon {
width: 110px;
height: 110px;
}
.result-title {
font-weight: 600;
font-size: 24px;
color: #1D2129;
margin: 16px auto;
}
.result-points {
font-weight: 600;
font-size: 24px;
color: #FF8D28;
.plus {
font-size: 15px;
}
.unit {
font-size: 16px;
font-weight: 500;
}
}
.batch-wrap {
margin-top: 16px;
background: #F9FAFF;
border-radius: 10px 10px 10px 10px;
width: 100%;
padding: 18px 15px;
.batch-info {
display: flex;
justify-content: space-between;
font-size: 14px;
.batch-label {
color: #666666;
}
.batch-remaining {
color: #FF8D28;
}
}
.progress-bar {
margin-top: 6px;
height: 6px;
background: #E2E2E2;
border-radius: 3px;
overflow: hidden;
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #FF7800 0%, #FF8D28 100%);
transition: width 0.6s cubic-bezier(0.22, 1, 0.36, 1);
}
}
}
}
.result-actions {
background: #F9FAFF;
display: flex;
border-top: 1px solid #E0E0E0;
align-items: center;
justify-content: flex-end;
padding: 16px 24px;
--el-color-primary: #6155F5;
--el-button-border-color: #6155F5;
--el-button-hover-bg-color: #6155F5;
--el-border-color: #6155F5;
button {
width: 126px;
height: 38px;
border-radius: 8px;
font-size: 14px;
font-weight: normal;
transition: all 0.2s;
border-color: #6155F5;
&:hover {
background: #6155F5;
}
}
}
}
</style>

@ -59,7 +59,7 @@
</div>
</header>
<main class="admin-content">
<main class="admin-content">
<slot />
</main>
@ -90,11 +90,29 @@
<!-- 修改密码弹窗管理端 / 医院端共用根据 role 走对应接口 -->
<PasswordDialog v-model="passwordDialogVisible" :role="passwordRole" />
<!-- 全局签到弹窗医院端登录后自动触发未签到时弹出 -->
<SignInDialog v-model="signInDialogStore.visible"
:signed-in="signInDialogStore.summary.isSignIn"
:total-points="signInDialogStore.summary.totalPoints"
:consecutive-sign-in-days="signInDialogStore.summary.consecutiveSignInDays"
:sign-in-rank="signInDialogStore.summary.signInRank"
:beat-percent="signInDialogStore.summary.beatPercent"
@success="onSignInDialogSuccess" />
<!-- 全局奖励结果弹窗抽奖/签到/提交工单 共用 -->
<RewardResultDialog />
<!-- 全局查看我的资产弹窗 RewardResultDialog 中点击"查看我的资产"触发 -->
<AssetsDialog v-model="assetDialogStore.visible" :total-points="assetDialogStore.totalPoints"
:exchange-goal="assetDialogStore.exchangeGoal" :reward-name="assetDialogStore.rewardName"
:today-order="assetDialogStore.todayOrder" :today-sign-in="assetDialogStore.todaySignIn"
:today-draw="assetDialogStore.todayDraw" />
</div>
</template>
<script setup>
import { computed, ref } from "vue";
import { computed, onMounted, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import { ElMessageBox } from "element-plus";
import { ArrowDown } from "@element-plus/icons-vue";
@ -102,8 +120,16 @@ import SvgIcon from "@/components/SvgIcon/index.vue";
import { useUserStore } from "@/stores/api/user";
import { useThemeStore } from "@/stores/theme";
import { useOrderDialogStore } from "@/stores/orderDialog";
import { useSignInDialogStore } from "@/stores/signInDialog";
import { useRewardResultStore } from "@/stores/rewardResult";
import { useAssetDialogStore } from "@/stores/assetDialog";
import OrderFormDialog from "@/views/hospital/orders/OrderFormDialog.vue";
import PasswordDialog from "@/components/PasswordDialog.vue";
import SignInDialog from "@/views/hospital/SignInDialog.vue";
import RewardResultDialog from "@/components/RewardResultDialog.vue";
import AssetsDialog from "@/views/hospital/AssetsDialog.vue";
import { getPoints, getPointsDetails } from "@/service/modular/hospital";
import dayjs from "dayjs";
// import SvgIcon fill
//
@ -136,6 +162,9 @@ const router = useRouter();
const userStore = useUserStore();
const themeStore = useThemeStore();
const orderDialogStore = useOrderDialogStore();
const signInDialogStore = useSignInDialogStore();
const rewardResultStore = useRewardResultStore();
const assetDialogStore = useAssetDialogStore();
//
const isHospitalMode = computed(() => route.path.startsWith("/hospital"));
@ -146,6 +175,14 @@ const onOrderDialogSuccess = () => {
window.dispatchEvent(new CustomEvent("hospital-order-saved"));
};
// ( SignInDialog fireSigned + open reward result)
// summary /hospital/points
const onSignInDialogSuccess = () => {
signInDialogStore.refreshSummary();
// ()
window.dispatchEvent(new CustomEvent("hospital-signin-success"));
};
//
const themeDialogVisible = ref(false);
@ -154,6 +191,69 @@ const passwordDialogVisible = ref(false);
//
const passwordRole = computed(() => (isHospitalMode.value ? "hospital" : "admin"));
// ()
onMounted(() => {
// 1) () -> +
// (SignInDialog + result; summary)
if (typeof signInDialogStore.onSigned !== "function" || !signInDialogStore.onSigned.__layoutHook) {
const hook = async () => {
await signInDialogStore.refreshSummary();
};
hook.__layoutHook = true;
signInDialogStore.onSigned = hook;
}
// 2) "" -> AssetsDialog
if (typeof rewardResultStore.onViewAccount !== "function" || !rewardResultStore.onViewAccount.__layoutHook) {
const hook = async () => {
await openAssetsDialogWithFreshData();
};
hook.__layoutHook = true;
rewardResultStore.onViewAccount = hook;
}
// 3) : layout
if (!userStore.token) return;
if (userStore.userInfo?.userType !== "Hospital") return;
signInDialogStore.checkAndShow();
});
// AssetsDialog
const openAssetsDialogWithFreshData = async () => {
try {
const [summaryRes, detailsRes] = await Promise.all([
getPoints(),
getPointsDetails({ page: 1, pageSize: 50 }),
]);
const summary = summaryRes?.data ?? summaryRes ?? {};
const detailsData = detailsRes?.data ?? detailsRes ?? {};
const list = detailsData.list || [];
const today = dayjs().format("YYYY-MM-DD");
const sumBy = (test) =>
list
.filter(
(r) =>
r.createdAt &&
String(r.createdAt).startsWith(today) &&
test(r.source),
)
.reduce(
(s, r) => s + Math.abs(Number(r.changeAmount) || 0),
0,
);
assetDialogStore.open({
totalPoints: Number(summary.totalPoints) || 0,
todayOrder: sumBy((s) => /工单|order/i.test(s || "")),
todaySignIn: sumBy((s) => /签到|/i.test(s || "")),
todayDraw: sumBy((s) => /抽奖|/i.test(s || "")),
});
} catch (_) {
assetDialogStore.open();
}
};
// scss
const presetThemes = [
{
@ -300,13 +400,6 @@ const handleCommand = async (cmd) => {
border: 1px solid rgba(0, 0, 0, 0.1);
background: linear-gradient(to right, #f9faff 0, #f9faff 75%, #a6cffe 100%);
// .header-left {
// display: flex;
// align-items: center;
// height: 100%;
// flex: 1;
// min-width: 0;
.logo-bar {
display: flex;
align-items: center;
@ -317,7 +410,6 @@ const handleCommand = async (cmd) => {
white-space: nowrap;
height: 100%;
//
.logo-text {
background: linear-gradient(to right, #3fc5fd, #3b90fa);
-webkit-background-clip: text;
@ -337,7 +429,6 @@ const handleCommand = async (cmd) => {
min-width: 0;
}
//
.header-quick-submit {
margin-left: 14px;
}
@ -366,8 +457,6 @@ const handleCommand = async (cmd) => {
}
}
// }
.header-right {
display: flex;
align-items: center;
@ -430,7 +519,7 @@ const handleCommand = async (cmd) => {
}
}
.admin-content {
.admin-content {
flex: 1;
overflow-x: hidden;
overflow-y: auto;
@ -557,4 +646,4 @@ const handleCommand = async (cmd) => {
}
}
}
</style>
</style>

@ -0,0 +1,44 @@
import { defineStore } from "pinia";
import { ref } from "vue";
/**
* 全局"查看我的资产"弹窗状态AdminLayout 承载 AssetsDialog
* - 三个场景(抽奖/签到/提交工单) RewardResultDialog 中点击"查看我的资产"时调用 open
*/
export const useAssetDialogStore = defineStore("assetDialog", () => {
const visible = ref(false);
// 透传给 AssetsDialog 的 props
const totalPoints = ref(0);
const exchangeGoal = ref(500);
const rewardName = ref("康小虎 AI 吉祥物");
const todayOrder = ref(0);
const todaySignIn = ref(0);
const todayDraw = ref(0);
const open = (opts = {}) => {
totalPoints.value = Number(opts.totalPoints) || 0;
exchangeGoal.value = Number(opts.exchangeGoal) || 500;
rewardName.value = opts.rewardName || "康小虎 AI 吉祥物";
todayOrder.value = Number(opts.todayOrder) || 0;
todaySignIn.value = Number(opts.todaySignIn) || 0;
todayDraw.value = Number(opts.todayDraw) || 0;
visible.value = true;
};
const close = () => {
visible.value = false;
};
return {
visible,
totalPoints,
exchangeGoal,
rewardName,
todayOrder,
todaySignIn,
todayDraw,
open,
close,
};
});

@ -0,0 +1,66 @@
import { defineStore } from "pinia";
import { ref } from "vue";
/**
* 全局"奖励结果"弹窗状态AdminLayout 承载组件
* - 抽奖 / 签到 / 提交工单 三个场景成功后统一通过此 store 打开
* - 用户点击"查看我的资产",触发 requestViewAccount 事件,由父级打开 AssetsDialog
*/
export const useRewardResultStore = defineStore("rewardResult", () => {
const visible = ref(false);
const title = ref("恭喜抽中");
const icon = ref("");
const points = ref(0);
const batchGoal = ref(500);
const batchCurrent = ref(0);
const rewardName = ref("康小虎 AI 吉祥物");
// 触发"查看我的资产"事件,AdminLayout 监听后打开 AssetsDialog
const onViewAccount = ref(null);
const open = (opts = {}) => {
title.value = opts.title || "恭喜抽中";
icon.value = opts.icon || "";
points.value = Number(opts.points) || 0;
batchGoal.value = Number(opts.batchGoal) || 500;
batchCurrent.value = Number(opts.batchCurrent) || 0;
rewardName.value = opts.rewardName || "康小虎 AI 吉祥物";
visible.value = true;
};
const close = () => {
visible.value = false;
};
// 关闭动画结束后清理数据
const clear = () => {
title.value = "恭喜抽中";
icon.value = "";
points.value = 0;
batchGoal.value = 500;
batchCurrent.value = 0;
rewardName.value = "康小虎 AI 吉祥物";
};
const requestViewAccount = () => {
if (typeof onViewAccount.value === "function") {
onViewAccount.value();
}
};
return {
visible,
title,
icon,
points,
batchGoal,
batchCurrent,
rewardName,
onViewAccount,
open,
close,
clear,
requestViewAccount,
};
});

@ -0,0 +1,129 @@
import { defineStore } from "pinia";
import { ref } from "vue";
import dayjs from "dayjs";
import { getPoints } from "@/service/modular/hospital";
// sessionStorage key当天是否已经处理过弹过 / 已签到不再弹)
const POPUP_FLAG_KEY = "csm_signin_popup_date";
/**
* 全局签到弹窗状态AdminLayout 承载组件登录后由 layout 触发
* - open() 打开
* - close() 关闭 + 标记今日已处理
* - checkAndShow() 登录后调用 summary 判断是否需要弹
* - refreshSummary() 签到成功后刷新 summary 数据
* - fireSigned() 签到成功后由 SignInDialog 调用,触发 onSigned 回调
*/
export const useSignInDialogStore = defineStore("signInDialog", () => {
const visible = ref(false);
const loading = ref(false);
// 透传给 SignInDialog 的字段(来自 /hospital/points 接口 summary
const summary = ref({
isSignIn: false,
totalPoints: 0,
consecutiveSignInDays: 0,
signInRank: 0,
beatPercent: 0,
});
// 签到成功回调AdminLayout 注册,用于打开 RewardResultDialog
const onSigned = ref(null);
const todayKey = () => dayjs().format("YYYY-MM-DD");
// 当天是否已经处理过(弹过 / 已签到跳过)
const hasHandledToday = () => {
try {
return sessionStorage.getItem(POPUP_FLAG_KEY) === todayKey();
} catch {
return false;
}
};
const markHandledToday = () => {
try {
sessionStorage.setItem(POPUP_FLAG_KEY, todayKey());
} catch {
/* sessionStorage 不可用时静默忽略 */
}
};
const unwrap = (res) => res?.data ?? res ?? {};
const setSummary = (data) => {
summary.value = {
isSignIn: !!data?.isSignIn,
totalPoints: Number(data?.totalPoints) || 0,
consecutiveSignInDays: Number(data?.consecutiveSignInDays) || 0,
signInRank: Number(data?.signInRank) || 0,
beatPercent: Number(data?.beatPercent) || 0,
};
};
const open = () => {
visible.value = true;
};
const close = () => {
visible.value = false;
// 用户主动关闭:标记今天不再弹(避免反复打扰)
markHandledToday();
};
/**
* 登录后 / 路由进入 layout 时调用
* 1) sessionStorage 已标记今天处理过 不再请求接口不弹
* 2) getPoints summary更新 store
* 3) 今日未签到才弹已签到不弹但仍标记今天已处理
*/
const checkAndShow = async () => {
if (hasHandledToday()) return;
if (loading.value) return;
loading.value = true;
try {
const res = unwrap(await getPoints());
setSummary(res);
if (!summary.value.isSignIn) {
open();
}
// 不论是否弹,都标记今天已处理
markHandledToday();
} catch (_) {
// 接口失败时不弹也不标记,下次重试
} finally {
loading.value = false;
}
};
// 签到成功后刷新 summary不弹窗仅更新数据
const refreshSummary = async () => {
try {
const res = unwrap(await getPoints());
setSummary(res);
return res;
} catch (_) {
/* 静默失败,不影响签到成功提示 */
return null;
}
};
// 触发签到成功回调
const fireSigned = () => {
if (typeof onSigned.value === "function") {
onSigned.value();
}
};
return {
visible,
loading,
summary,
onSigned,
open,
close,
checkAndShow,
refreshSummary,
fireSigned,
};
});

@ -1,13 +1,6 @@
<template>
<el-dialog
:model-value="visible"
class="account-edit-dialog"
top="6vh"
width="640px"
destroy-on-close
:close-on-click-modal="false"
@update:model-value="(v) => !v && close()"
>
<el-dialog :model-value="visible" class="account-edit-dialog" top="6vh" width="640px" destroy-on-close
:close-on-click-modal="false" @update:model-value="(v) => !v && close()">
<div class="body-wrap">
<div class="body-header">
<span class="body-title">
@ -17,13 +10,7 @@
</div>
<div class="account-edit" v-loading="loading">
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="100px"
label-position="right"
>
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px" label-position="right">
<!-- 医院账号字段编辑时 医院名称/账号(只读)/姓名/性别新增时 医院名称/账号/密码/姓名/性别 -->
<template v-if="props.userType === 'Hospital'">
<!-- 新增模式 -->
@ -31,19 +18,9 @@
<el-row :gutter="20">
<el-col :span="12">
<el-form-item :label="$t('table.hospital')" prop="hospitalId">
<el-select
v-model="form.hospitalId"
:placeholder="$t('msg.pleaseSelectHospital')"
filterable
clearable
style="width: 100%"
>
<el-option
v-for="item in hospitalOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
<el-select v-model="form.hospitalId" :placeholder="$t('msg.pleaseSelectHospital')" filterable
clearable style="width: 100%">
<el-option v-for="item in hospitalOptions" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
</el-col>
@ -56,12 +33,8 @@
<el-row :gutter="20">
<el-col :span="12">
<el-form-item :label="$t('label.password')" prop="password">
<el-input
v-model="form.password"
:type="showPwd ? 'text' : 'password'"
autocomplete="new-password"
:placeholder="$t('placeholder.password')"
>
<el-input v-model="form.password" :type="showPwd ? 'text' : 'password'" autocomplete="new-password"
:placeholder="$t('placeholder.password')">
<template #suffix>
<el-icon class="pwd-toggle-icon" @click="showPwd = !showPwd">
<View v-if="showPwd" />
@ -93,20 +66,9 @@
<el-row :gutter="20">
<el-col :span="12">
<el-form-item :label="$t('table.hospital')" prop="hospitalId">
<el-select
:key="`hospital-${hospitalOptions.length}`"
v-model="form.hospitalId"
:placeholder="$t('msg.pleaseSelectHospital')"
filterable
clearable
style="width: 100%"
>
<el-option
v-for="item in hospitalOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
<el-select :key="`hospital-${hospitalOptions.length}`" v-model="form.hospitalId"
:placeholder="$t('msg.pleaseSelectHospital')" filterable clearable style="width: 100%">
<el-option v-for="item in hospitalOptions" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
</el-col>
@ -146,12 +108,8 @@
</el-col>
<el-col :span="12">
<el-form-item :label="$t('label.password')" prop="password">
<el-input
v-model="form.password"
:type="showPwd ? 'text' : 'password'"
autocomplete="new-password"
:placeholder="$t('placeholder.password')"
>
<el-input v-model="form.password" :type="showPwd ? 'text' : 'password'" autocomplete="new-password"
:placeholder="$t('placeholder.password')">
<template #suffix>
<el-icon class="pwd-toggle-icon" @click="showPwd = !showPwd">
<View v-if="showPwd" />
@ -394,7 +352,12 @@ watch(
if (vis) {
showPwd.value = false;
resetForm();
if (id) fillFormFromRow();
if (id) { fillFormFromRow() }
else {
if (props.userType === 'Hospital') {
loadHospitals();
}
}
}
},
{ immediate: true }
@ -434,10 +397,12 @@ watch(
width: 30px !important;
min-width: 30px !important;
}
.el-input.is-show-password .el-input__inner {
padding-right: 50px !important;
box-sizing: border-box;
}
// /
.el-input.is-show-password,
.el-input.is-show-password .el-input__inner,

@ -245,7 +245,10 @@ const viewReport = async (attachmentPath) => {
}
};
onMounted(() => { loadList(), hospitalStore.ensureLoaded(); });
onMounted(() => {
hospitalStore.ensureLoaded();
loadList();
});
</script>
<style scoped lang="scss">

@ -538,34 +538,6 @@ onBeforeUnmount(() => {
<style lang="scss">
.admin-order-form-dialog {
.body-wrap {
.body-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 27px;
.body-title {
font-size: 18px;
font-weight: 600;
}
.close-btn {
width: 24px;
height: 24px;
cursor: pointer;
}
}
}
.body-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 24px;
}
.rich-editor {
width: 100%;
background: #fff;

@ -203,14 +203,6 @@ const filters = reactive({
keyword: "",
});
const loadHospitals = async () => {
try {
const res = await getAdminHospitals({ page: 1, pageSize: 1000 });
hospitals.value = res?.list || [];
} catch (_) {
hospitals.value = [];
}
};
const statusClass = (s) =>
s === "已完成" ? "done" : s === "待处理" ? "pending" : 'info';
@ -351,7 +343,7 @@ const onResize = () => {
viewportWidth.value = window.innerWidth - 120;
};
onMounted(() => {
loadHospitals();
hospitalStore.ensureLoaded();
loadList();
window.addEventListener("resize", onResize);
});

@ -376,7 +376,7 @@ onMounted(() => {
border-radius: 8px 8px 8px 8px !important;
border: 1px solid #E0E0E0 !important;
overflow: hidden;
height: 42px !important;
height: 44px !important;
}
}
}

@ -1,67 +1,37 @@
<template>
<el-dialog :model-value="modelValue" @update:model-value="onDialogUpdate" :show-close="false"
:close-on-click-modal="true" width="420px" align-center append-to-body class="lottery-dialog" @closed="onClosed">
<Transition name="lottery-fade" mode="out-in">
<!-- ============ 抽奖网格 ============ -->
<div v-if="!showResult" key="grid" class="lottery-main">
<div class="lottery-header">
<h2 class="lottery-title">积分抽奖</h2>
<p class="lottery-subtitle">顺时针转动高亮 · 停在哪格就领对应奖励</p>
</div>
<div class="lottery-grid">
<template v-for="(item, idx) in gridItems" :key="idx">
<!-- 中心抽奖按钮 -->
<div v-if="item.type === 'draw'" class="lottery-cell lottery-cell--draw"
:class="{ 'is-disabled': isDrawing || !drawable, 'is-drawed': !drawable }" @click="startDraw">
</div>
<!-- 普通积分格 -->
<div v-else class="lottery-cell" :class="{
'is-active': activeIndex === idx && isDrawing,
'is-winner': winnerIndex === idx && !isDrawing,
}">
<span class="points-value">{{ item.points }}积分</span>
</div>
</template>
</div>
<div class="lottery-main">
<div class="lottery-header">
<h2 class="lottery-title">积分抽奖</h2>
<p class="lottery-subtitle">顺时针转动高亮 · 停在哪格就领对应奖励</p>
</div>
<!-- ============ 结果弹窗 ============ -->
<div v-else key="result" class="lottery-result">
<div class="main-wrap">
<div class="result-icon-wrap">
<img :src="resultIcon" class="result-icon" alt="" />
<div class="lottery-grid">
<template v-for="(item, idx) in gridItems" :key="idx">
<!-- 中心抽奖按钮 -->
<div v-if="item.type === 'draw'" class="lottery-cell lottery-cell--draw"
:class="{ 'is-disabled': isDrawing || !drawable, 'is-drawed': !drawable }" @click="startDraw">
</div>
<div class="result-title">恭喜抽中</div>
<div class="result-points">
<span class="plus">+</span>
<span class="value">{{ winningPoints }}&nbsp;&nbsp;</span>
<span class="unit">积分</span>
<!-- 普通积分格 -->
<div v-else class="lottery-cell" :class="{
'is-active': activeIndex === idx && isDrawing,
'is-winner': winnerIndex === idx && !isDrawing,
}">
<span class="points-value">{{ item.points }}积分</span>
</div>
<div class="batch-wrap">
<div class="batch-info">
<span class="batch-label">距离兑换 康小虎 AI 吉祥物</span>
<span class="batch-remaining">还差{{ batchRemaining }}</span>
</div>
<div class="progress-bar">
<div class="progress-fill" :style="{ width: batchPercent + '%' }" />
</div>
</div>
</div>
<div class="result-actions">
<el-button type="primary" plain class="btn-secondary" @click="handleClaim"></el-button>
<el-button type="primary" class="btn-primary" @click="handleViewAccount"></el-button>
</div>
</template>
</div>
</Transition>
</div>
</el-dialog>
</template>
<script setup>
import { computed, ref } from "vue";
import { computed, ref, watch } from "vue";
import { ElMessage } from "element-plus";
import { draw } from "@/service/modular/hospital";
import resultIcon from "@/assets/image/hospital/ponits/icon-lottery-large.svg";
import { draw, getPoints } from "@/service/modular/hospital";
import { useRewardResultStore } from "@/stores/rewardResult";
import iconLottery from "@/assets/image/hospital/ponits/icon-lottery-large.svg";
const props = defineProps({
modelValue: { type: Boolean, default: false },
@ -81,6 +51,8 @@ const emit = defineEmits([
const unwrap = (res) => res?.data ?? res ?? {};
const rewardResultStore = useRewardResultStore();
// ============ ============
const gridItems = [
{ type: "points", points: 1 },
@ -101,14 +73,6 @@ const isDrawing = ref(false);
const activeIndex = ref(-1);
const winnerIndex = ref(-1);
const winningPoints = ref(0);
const showResult = ref(false);
const batchRemaining = computed(() =>
Math.max(0, props.batchGoal - props.batchCurrent),
);
const batchPercent = computed(() =>
Math.min(100, Math.round((props.batchCurrent / props.batchGoal) * 100)),
);
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
@ -119,10 +83,20 @@ const findIndexByPoints = (points) => {
);
};
// ============ ============
const fetchLatestPoints = async () => {
try {
const res = unwrap(await getPoints());
return Number(res?.totalPoints) || 0;
} catch (_) {
return 0;
}
};
// ============ ============
const startDraw = async () => {
if (isDrawing.value) return;
if (!props.drawable) {
ElMessage.info("今日已抽奖,明天再来吧!");
return;
@ -132,7 +106,6 @@ const startDraw = async () => {
isDrawing.value = true;
winnerIndex.value = -1;
winningPoints.value = 0;
showResult.value = false;
activeIndex.value = -1;
//
@ -180,12 +153,10 @@ const startDraw = async () => {
return;
}
//
//
const points = Number(
apiResult.points ?? apiResult.score ?? apiResult ?? 0,
) ;
);
//
const targetIdx = findIndexByPoints(points);
@ -204,11 +175,27 @@ const startDraw = async () => {
winnerIndex.value = targetIdx;
winningPoints.value = points;
//
// : -> ""
await wait(500);
showResult.value = true;
isDrawing.value = false;
console.log('线上');
//
const totalPoints = await fetchLatestPoints();
// ,( overflow )
emit("update:modelValue", false);
// open
setTimeout(() => {
rewardResultStore.open({
title: "恭喜抽中",
icon: iconLottery,
points,
batchGoal: 500,
batchCurrent: totalPoints,
rewardName: "康小虎 AI 吉祥物",
});
}, 100);
emit("success", { points });
};
@ -227,16 +214,6 @@ const onClosed = () => {
activeIndex.value = -1;
winnerIndex.value = -1;
winningPoints.value = 0;
showResult.value = false;
};
const handleClaim = () => {
emit("update:modelValue", false);
};
const handleViewAccount = () => {
emit("viewAccount");
emit("update:modelValue", false);
};
</script>
@ -256,23 +233,6 @@ const handleViewAccount = () => {
}
}
@keyframes resultPop {
0% {
transform: scale(0.5);
opacity: 0;
}
60% {
transform: scale(1.1);
opacity: 1;
}
100% {
transform: scale(1);
opacity: 1;
}
}
/* ============ 所有样式都嵌套在 .lottery-dialog 下 ============ */
.lottery-dialog {
// ===== =====
@ -297,22 +257,6 @@ const handleViewAccount = () => {
align-items: center;
}
// ===== =====
.lottery-fade-enter-active,
.lottery-fade-leave-active {
transition: opacity 0.3s ease, transform 0.3s ease;
}
.lottery-fade-enter-from {
opacity: 0;
transform: scale(0.96);
}
.lottery-fade-leave-to {
opacity: 0;
transform: scale(1.04);
}
// ===== =====
.lottery-main {
padding: 86px 82px 54px 74px;
@ -395,151 +339,5 @@ const handleViewAccount = () => {
}
}
}
// ===== =====
.lottery-result {
width: 458px;
// height: 412px;
background: #FFFFFF;
box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.06);
border-radius: 24px;
overflow: hidden;
.main-wrap {
padding: 30px 46px 20px;
display: flex;
flex-direction: column;
align-items: center;
.result-icon-wrap {
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
animation: resultPop 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.result-icon {
width: 110px;
height: 110px;
}
.result-title {
font-weight: 600;
font-size: 24px;
color: #1D2129;
margin: 16px auto;
}
.result-points {
font-weight: 600;
font-size: 24px;
color: #FF8D28;
.plus {
font-size: 15px;
}
.unit {
font-size: 16px;
font-weight: 500;
}
}
.batch-wrap {
margin-top: 16px;
background: #F9FAFF;
border-radius: 10px 10px 10px 10px;
width: 100%;
padding: 18px 15px;
.batch-info {
display: flex;
justify-content: space-between;
font-size: 14px;
.batch-label {
color: #666666;
}
.batch-remaining {
color: #FF8D28;
}
}
.progress-bar {
margin-top: 6px;
height: 6px;
background: #E2E2E2;
border-radius: 3px;
overflow: hidden;
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #FF7800 0%, #FF8D28 100%);
transition: width 0.6s cubic-bezier(0.22, 1, 0.36, 1);
}
}
}
}
.result-actions {
background: #F9FAFF;
display: flex;
border-top: 1px solid #E0E0E0;
display: flex;
align-items: center;
// gap: 12px;
justify-content: flex-end;
padding: 16px 24px;
--el-color-primary: #6155F5;
// --el-button-text-color: #6155F5;
// --el-button-bg-color: #6155F5;
--el-button-border-color: #6155F5;
// --el-button-hover-text-color: #6155F5;
--el-button-hover-bg-color: #6155F5;
--el-border-color: #6155F5;
// --el-button-hover-border-color: #6155F5;
// --el-button-active-text-color: #FFFFFF;
button {
// flex: 1;
width: 126px;
height: 38px;
border-radius: 8px;
font-size: 14px;
font-weight: normal;
transition: all 0.2s;
border-color: #6155F5;
&:hover {
background: #6155F5;
}
}
// .btn-secondary {
// background: #FFFFFF;
// color: #6E41FD;
// border-color: #6E41FD;
// &:hover {
// background: #F3EDFE;
// }
// }
// .btn-primary {
// background: linear-gradient(180deg, #AE6EFE 0%, #6E41FD 100%);
// color: #FFFFFF;
// border-color: transparent;
// &:hover {
// opacity: 0.92;
// transform: translateY(-1px);
// box-shadow: 0 4px 12px rgba(110, 65, 253, 0.35);
// }
// }
}
}
}
</style>
</style>

@ -6,7 +6,7 @@
<div class="signin-header">
<h3 class="signin-title" :class="{ 'is-signed': signedIn }">
<template v-if="signedIn">
连续签到<span class="title-num">{{ streakDays }}</span>
连续签到<span class="title-num">{{ streakDays }}</span>
<template v-if="rank > 0">排名第<span class="title-num">{{ rank }}</span></template>
<template v-else></template>
</template>
@ -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);

@ -0,0 +1,391 @@
<template>
<el-dialog :model-value="modelValue" class="hospital-order-detail-dialog" width="900px" :close-on-click-modal="true"
top="5vh" @update:model-value="(v) => $emit('update:modelValue', v)" @open="onOpen" @closed="cleanupPreview">
<div class="body-wrap" v-loading="loading">
<div class="body-header">
<div class="body-title">{{ $t('modal.detail') }}</div>
<img src="@/assets/image/close-btn.svg" alt="" class="close-btn" @click="closeDialog" />
</div>
<div v-if="detail.id" class="detail-container">
<!-- 第一行4 -->
<div class="detail-row row-4">
<div class="detail-item">
<div class="label">工单号</div>
<div class="value">{{ detail.orderNo }}</div>
</div>
<div class="detail-item">
<div class="label">当前状态</div>
<div class="value">{{ detail.status }}</div>
</div>
<div class="detail-item">
<div class="label">优先级</div>
<div class="value">{{ detail.priority }}</div>
</div>
<div class="detail-item">
<div class="label">服务类型</div>
<div class="value">{{ detail.serviceType }}</div>
</div>
</div>
<!-- 第二行1 -->
<div class="detail-row row-1">
<div class="detail-item">
<div class="label">问题名称</div>
<div class="value">{{ detail.title }}</div>
</div>
</div>
<!-- 第三行1 -->
<div class="detail-row row-1">
<div class="detail-item">
<div class="label">问题描述</div>
<div class="value" style="max-height: 300px; overflow: auto;">
<div class="html-content" v-html="detail.description || '—'"></div>
</div>
</div>
</div>
<!-- 第四行1 -->
<div class="detail-row row-1">
<div class="detail-item">
<div class="label">附件</div>
<div class="value">
<el-empty style="padding:0;" v-if="!detail.attachments?.length"
:description="$t('orderDetail.noAttachment')" :image-size="80" />
<div v-else class="files">
<el-button v-for="(f, i) in detail.attachments" :key="i" link type="primary"
class="file-item" @click="viewReport(f.filePath, f.fileName)">
<el-icon>
<Document />
</el-icon>{{ f.fileName }}
</el-button>
</div>
</div>
</div>
</div>
<!-- 第五行3不展示"提出医院"医院端默认就是当前登录医院 -->
<div class="detail-row row-3">
<div class="detail-item">
<div class="label">提出科室</div>
<div class="value">{{ detail.department || '-' }}</div>
</div>
<div class="detail-item">
<div class="label">提出人员</div>
<div class="value">{{ detail.submitter || '-' }}</div>
</div>
<div class="detail-item">
<div class="label">提交时间</div>
<div class="value">{{ detail.createdAt?.split('T').join(' ') || '-' }}</div>
</div>
</div>
</div>
<div class="section" v-if="detail.logs?.length">
<div class="section-title">{{ $t('orderDetail.statusChange') }}</div>
<el-timeline>
<el-timeline-item v-for="(log, i) in detail.logs || []" :key="i" :timestamp="log.createdAt"
placement="top">
<div class="log-title">{{ log.action }} · {{ log.operator }}</div>
<div class="log-remark" v-html="log.remark"></div>
</el-timeline-item>
</el-timeline>
</div>
</div>
<div class="body-footer">
<el-button type="primary" plain style="margin-left:auto" @click="closeDialog">
取消
</el-button>
<el-button v-if="detail.status !== '' && detail.status !== ''" type="primary"
@click="editVisible = true">
{{ $t('btn.edit') }}
</el-button>
</div>
<!-- 编辑工单弹窗复用医院端 OrderFormDialog传入 orderId -->
<OrderFormDialog v-model="editVisible" :order-id="orderId" @success="onEditSuccess" />
<!-- 附件预览弹窗 -->
<FilePreviewDialog :visible="previewVisible" :blob-url="previewUrl" :blob="previewBlob" :file-name="previewName"
:file-type="previewType" @update:visible="(v) => (previewVisible = v)" @close="cleanupPreview"
@download="onPreviewDownload" />
</el-dialog>
</template>
<script setup>
import { ref, watch } from "vue";
import { Document } from "@element-plus/icons-vue";
import { ElMessage } from "element-plus";
import { useI18n } from "vue-i18n";
import { getHospitalOrderDetail } from "@/service/modular/hospital";
import { getUploadFile } from "@/service/modular/upload";
import FilePreviewDialog from "@/components/FilePreviewDialog.vue";
import OrderFormDialog from "./OrderFormDialog.vue";
const { t: $t } = useI18n();
const props = defineProps({
modelValue: { type: Boolean, default: false },
orderId: { type: [String, Number], default: "" },
});
const emit = defineEmits(["update:modelValue", "updated"]);
const loading = ref(false);
const detail = ref({});
const priorityType = (p) => (p === "高" ? "danger" : p === "中" ? "warning" : "info");
const statusType = (s) =>
({ 待处理: "warning", 处理中: "primary", 已完成: "success", 已取消: "info" })[s] || "info";
const loadDetail = async () => {
if (!props.orderId) return;
loading.value = true;
try {
detail.value = await getHospitalOrderDetail(props.orderId);
} finally {
loading.value = false;
}
};
const onOpen = () => {
loadDetail();
};
watch(
() => props.orderId,
() => {
if (props.modelValue) loadDetail();
},
);
const closeDialog = () => emit("update:modelValue", false);
const previewVisible = ref(false);
const previewUrl = ref("");
const previewBlob = ref(null);
const previewName = ref("");
const previewType = ref(""); // 'pdf' | 'image' | ''
const cleanupPreview = () => {
if (previewUrl.value) {
URL.revokeObjectURL(previewUrl.value);
previewUrl.value = "";
}
previewBlob.value = null;
previewName.value = "";
previewType.value = "";
};
// /+blob mime
const getFileType = (fileName, blob) => {
const name = String(fileName || "");
const ext = name.split(".").pop()?.toLowerCase() || "";
const mime = blob?.type || "";
if (ext === "pdf" || mime === "application/pdf") return "pdf";
if (
["jpg", "jpeg", "png", "gif", "bmp", "webp", "svg"].includes(ext) ||
(mime && mime.startsWith("image/"))
) {
return "image";
}
return "other";
};
// blob
const downloadBlob = (blob, fileName) => {
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = fileName || "download";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
setTimeout(() => URL.revokeObjectURL(url), 0);
};
// FilePreviewDialog
const onPreviewDownload = ({ blob, fileName }) => {
if (!blob) return;
downloadBlob(blob, fileName);
};
const viewReport = async (attachmentPath, originalName) => {
if (!attachmentPath) return;
const path = String(attachmentPath);
const downloadName = originalName || path;
try {
const blob = await getUploadFile(path);
const type = getFileType(downloadName, blob);
// PDF /
if (type === "pdf" || type === "image") {
cleanupPreview();
previewBlob.value = blob;
previewUrl.value = URL.createObjectURL(blob);
previewName.value = downloadName;
previewType.value = type;
previewVisible.value = true;
} else {
downloadBlob(blob, downloadName);
}
} catch (e) {
ElMessage.error(e?.message || $t('msg.failed'));
}
};
// ============== ==============
const editVisible = ref(false);
// list
const onEditSuccess = () => {
editVisible.value = false;
loadDetail();
emit("updated");
};
</script>
<style lang="scss">
.hospital-order-detail-dialog {
max-width: 90vw;
min-width: 70vw;
.body-wrap {
padding: 24px;
.body-header {
margin-bottom: 16px;
}
.detail-container {
.detail-row+.detail-row {
margin-top: 15px;
}
.detail-row {
display: grid;
gap: 15px;
&.row-1 {
grid-template-columns: 1fr;
}
&.row-3 {
grid-template-columns: repeat(3, 1fr);
}
&.row-4 {
grid-template-columns: repeat(4, 1fr);
}
.detail-item {
display: flex;
align-items: center;
gap: 10px;
.label {
font-weight: 400;
font-size: 14px;
color: #1D2129;
width: 4em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex-shrink: 0;
flex-grow: 0;
}
.value {
flex-shrink: 1;
flex-grow: 1;
font-weight: 400;
font-size: 14px;
color: #666666;
padding: 10px;
background: #F8F8F8;
border-radius: 8px 8px 8px 8px;
border: 1px solid #E0E0E0;
.html-content {
background: #fafafa;
padding: 12px;
border-radius: 4px;
min-height: 60px;
line-height: 1.6;
max-width: 100%;
img {
max-width: 100% !important;
}
}
}
}
}
}
}
}
.card-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
.title {
font-size: 16px;
font-weight: 600;
margin-right: 8px;
}
}
.section {
margin-top: 20px;
.section-title {
font-size: 14px;
font-weight: 600;
color: #303133;
margin-bottom: 10px;
}
}
.files {
display: flex;
flex-direction: column;
gap: 8px;
.file-item {
display: inline-flex;
align-items: center;
gap: 4px;
}
}
.log-title {
font-weight: 600;
}
.log-remark {
color: #606266;
margin-top: 4px;
}
.preview-iframe {
width: 100%;
height: 80vh;
border: 0;
}
.preview-image {
display: block;
max-width: 100%;
max-height: 80vh;
margin: 0 auto;
object-fit: contain;
}
</style>

@ -1,52 +1,30 @@
<template>
<el-dialog :model-value="modelValue" width="720px" :close-on-click-modal="true" destroy-on-close
class="hospital-order-form-dialog" @update:model-value="onDialogUpdate" @closed="onClosed">
<template>
<el-dialog :model-value="modelValue" class="hospital-order-form-dialog" :close-on-click-modal="true"
@update:model-value="(v) => $emit('update:modelValue', v)" @closed="onClosed">
<div class="body-wrap">
<div class="body-header">
<div class="body-title">
{{ isEdit() ? '编辑工单' : $t('modal.submit') }}
</div>
<img src="@/assets/image/close-btn.svg" alt="" class="close-btn" @click="onCancel">
<div class="body-title">{{ isEdit() ? '编辑工单' : '新建工单' }}</div>
<img src="@/assets/image/close-btn.svg" alt="" class="close-btn" @click="onCancel" />
</div>
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
<el-form-item :label="$t('label.title')" prop="title">
<div class="title-row">
<el-input v-model="form.title" :placeholder="$t('placeholder.title')" maxlength="100" show-word-limit />
<div class="color-tag-picker" ref="colorPickerRef">
<div class="color-tag-preview" :style="{ background: form.colorTag || '#dcdfe6' }"
:class="{ 'is-empty': !form.colorTag }" @click.stop="toggleColorPicker"></div>
<div v-show="colorPickerVisible" class="color-tag-dropdown" @click.stop>
<div class="color-tag-title">{{ $t('label.colorTag') }}</div>
<div class="color-tag-options">
<div v-for="c in colorOptions" :key="c.value" class="color-tag-option"
:class="{ active: form.colorTag === c.value }" :style="{ background: c.value }" :title="c.label"
@click="selectColor(c.value)"></div>
</div>
</div>
</div>
</div>
</el-form-item>
<el-form-item :label="$t('label.type')" prop="serviceType">
<el-select v-model="form.serviceType" :placeholder="$t('placeholder.type')" clearable style="width: 240px">
<el-option label="故障报修" value="故障报修" />
<el-option label="业务咨询" value="业务咨询" />
<el-option label="需求变更" value="需求变更" />
<el-option label="其他" value="其他" />
</el-select>
</el-form-item>
<el-form-item label-width="0">
<div class="inline-row">
<el-form-item :label="$t('label.priority')" prop="priority" class="inline-item">
<el-radio-group v-model="form.priority">
<el-radio-button label="高"></el-radio-button>
<el-radio-button label="中"></el-radio-button>
<el-radio-button label="低"></el-radio-button>
</el-radio-group>
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
<!-- 第一行 2 服务类型 + 反馈渠道医院端没有"所属医院" -->
<el-row :gutter="20">
<el-col :span="12">
<el-form-item :label="$t('label.type')" prop="serviceType">
<el-select v-model="form.serviceType" :placeholder="$t('placeholder.type')" clearable
style="width: 100%">
<el-option label="故障报修" value="故障报修" />
<el-option label="业务咨询" value="业务咨询" />
<el-option label="需求变更" value="需求变更" />
<el-option label="其他" value="其他" />
</el-select>
</el-form-item>
<el-form-item :label="$t('label.feedbackchannel')" prop="feedbackchannel" class="inline-item">
</el-col>
<el-col :span="12">
<el-form-item :label="$t('label.feedbackchannel')" prop="feedbackchannel">
<el-select v-model="form.feedbackchannel" :placeholder="$t('placeholder.feedbackchannel')" clearable
style="width: 200px">
style="width: 100%">
<el-option label="微信群" value="微信群" />
<el-option label="微信" value="微信" />
<el-option label="电话" value="电话" />
@ -56,18 +34,69 @@
<el-option label="其他" value="其他" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<!-- 第二行 3 科室 + 提单人 + 提交时间 -->
<el-row :gutter="20">
<el-col :span="8">
<el-form-item :label="$t('label.dept')" prop="department">
<el-input v-model="form.department" :placeholder="$t('placeholder.dept')" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item :label="$t('label.name')" prop="submitter">
<el-input v-model="form.submitter" :placeholder="$t('placeholder.name')" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item :label="$t('label.createat')" prop="createdat">
<el-date-picker v-model="form.createdat" type="datetime" :placeholder="$t('placeholder.createat')"
value-format="YYYY-MM-DD HH:mm:ss" format="YYYY-MM-DD HH:mm:ss" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<!-- 标题 + 颜色标签 + 优先级 同一行 -->
<el-form-item :label="$t('label.title')" prop="title">
<div class="title-row">
<el-input v-model="form.title" :placeholder="$t('placeholder.title')" maxlength="300"
class="title-input">
<template #suffix>
<span class="title-counter"><span :style="{ color: form.colorTag || '#dcdfe6' }">{{ form.title.length
}}</span>/300</span>
<el-popover v-model:visible="colorPickerVisible" :width="180" placement="bottom-end" trigger="click"
:show-arrow="false" popper-class="color-tag-popper">
<template #reference>
<el-badge :color="form.colorTag || '#dcdfe6'" is-dot class="item">
<img src="@/assets/image/admin/orders/sepan.svg" alt="" class="title-sepan" />
</el-badge>
</template>
<div class="color-tag-title">{{ $t('label.colorTag') }}</div>
<div class="color-tag-options">
<div v-for="c in colorOptions" :key="c.value" class="color-tag-option"
:class="{ active: form.colorTag === c.value }" :style="{ background: c.value }" :title="c.label"
@click="selectColor(c.value)"></div>
</div>
</el-popover>
</template>
</el-input>
<div class="priority-wrap">
<div class="priority-label">{{ $t('label.priority') }}</div>
<el-select v-model="form.priority" class="priority-select" :teleported="false">
<template #label="{ label, value }">
<div class="priority-row high" v-if="value === '高'">{{ value }}</div>
<div class="priority-row medium" v-if="value === '中'">{{ value }}</div>
<div class="priority-row low" v-if="value === '低'">{{ value }}</div>
</template>
<el-option v-for="opt in priorityOptions" :key="opt.value" :value="opt.value">
<span class="priority-option">
<span class="priority-dot" :class="opt.cls"></span>
<span class="priority-option-label">{{ opt.label }}</span>
</span>
</el-option>
</el-select>
</div>
</div>
</el-form-item>
<el-form-item :label="$t('label.dept')" prop="department">
<el-input v-model="form.department" :placeholder="$t('placeholder.dept')" />
</el-form-item>
<el-form-item :label="$t('label.name')" prop="submitter">
<el-input v-model="form.submitter" :placeholder="$t('placeholder.name')" />
</el-form-item>
<el-form-item :label="$t('label.createat')" prop="createdat">
<el-date-picker v-model="form.createdat" type="datetime" :placeholder="$t('placeholder.createat')"
value-format="YYYY-MM-DD HH:mm:ss" format="YYYY-MM-DD HH:mm:ss" style="width: 100%" />
</el-form-item>
<el-form-item :label="$t('label.description')" prop="description">
<div class="rich-editor">
<Toolbar :editor="editorRef" :default-config="toolbarConfig" mode="default"
@ -79,12 +108,12 @@
</el-form-item>
<el-form-item class="fujian" :label="$t('label.attachment')">
<el-upload class="upload-wrap" v-model:file-list="form.files" :auto-upload="false" :limit="1"
:on-exceed="handleAttachmentExceed" :on-remove="handleAttachmentRemove" :on-change="handleAttachmentChange">
:on-exceed="handleAttachmentExceed" :on-remove="handleAttachmentRemove"
:on-change="handleAttachmentChange">
<div class="upload-btn">
<div class="left">+ 添加文件</div>
<div class="right">{{ $t('label.attachmentTip') }}</div>
</div>
<template #file="{ file }">
<div class="file-item">
<img class="file-icon" src="@/assets/image/admin/orders/file.svg" alt="">
@ -97,57 +126,12 @@
</el-form-item>
</el-form>
</div>
<div class="body-footer">
<el-button @click="onCancel">{{ $t('common.cancel') }}</el-button>
<el-button type="primary" :loading="submitting" @click="submit">
{{ $t('common.submit') }}
</el-button>
</div>
<!-- 提交工单后 +1 积分奖励弹窗 -->
<el-dialog v-model="rewardVisible" width="360px" :show-close="false" :close-on-click-modal="false"
:close-on-press-escape="false" append-to-body class="reward-dialog">
<div class="reward-content">
<div class="reward-badge"> 奖励已发放</div>
<div class="coin-wrapper">
<span class="reward-coin">🪙</span>
</div>
<div class="points-number">
+1 <small>积分</small>
</div>
<div class="reward-title">🎉 到账通知</div>
<div class="reward-sub">1 积分已放入您的钱包</div>
<div class="reward-progress">
<div class="reward-progress-label">
<span>🏆 距兑换康小虎 AI 吉祥物</span>
<span class="reward-distance">
<template v-if="rewardDistance > 0">
还差 {{ rewardDistance }}
</template>
<template v-else> 🎉</template>
</span>
</div>
<div class="reward-bar-bg">
<div class="reward-bar-fill" :style="{ width: rewardProgress + '%' }"></div>
</div>
<div class="reward-current">
当前 {{ rewardTotalPoints }}
</div>
</div>
<div class="reward-actions">
<el-button class="reward-btn reward-btn-received" @click="onRewardClose">
开心收下
</el-button>
<el-button class="reward-btn reward-btn-assets" type="warning" @click="goMyAssets">
查看我的资产
</el-button>
</div>
</div>
</el-dialog>
</el-dialog>
</template>
@ -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(() => {
<style lang="scss">
.hospital-order-form-dialog {
.body-wrap {
padding: 24px;
}
.body-header {
margin-bottom: 18px;
}
.rich-editor {
border: 1px solid #dcdfe6;
border-radius: 4px;
width: 100%;
overflow: hidden;
background: #fff;
border-radius: 4px;
border: 1px solid #dcdfe6;
overflow: hidden;
}
.rich-editor :deep(.w-e-toolbar) {
background: #fafafa;
border-bottom: 1px solid #dcdfe6;
}
.rich-editor :deep(.w-e-text-container) {
background: #fff;
}
// +
.priority-row {
&.high {
--current-color: #ff383c;
background: #ffebec;
}
&.medium {
--current-color: #ff8d28;
background: #ffefe2;
}
&.low {
--current-color: #3289fb;
background: #eaf3ff;
}
width: 50px;
height: 30px;
border-radius: 8px 8px 8px 8px;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
&::before {
content: "";
display: block;
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--current-color);
}
color: var(--current-color);
}
// + + admin
.title-row {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
}
.title-row .el-input {
.title-row .title-input {
flex: 1;
.el-input__wrapper {
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
}
}
// +
.inline-row {
.title-row :deep(.el-input__suffix) {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0 24px;
width: 100%;
gap: 8px;
}
.inline-row .inline-item {
margin-bottom: 0 !important;
.title-counter {
font-size: 13px;
color: #909399;
margin-right: 10px;
}
.inline-row .inline-item :deep(.el-form-item__content) {
display: flex;
align-items: center;
.title-sepan {
width: 16px;
height: 16px;
display: block;
cursor: pointer;
}
.color-tag-picker {
@ -616,85 +612,96 @@ onBeforeUnmount(() => {
flex-shrink: 0;
}
.color-tag-preview {
width: 28px;
height: 28px;
border-radius: 6px;
cursor: pointer;
border: 2px solid #fff;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1);
transition:
transform 0.2s ease,
box-shadow 0.2s ease;
// admin label + select
.priority-wrap {
display: flex;
align-items: center;
flex-shrink: 0;
border-radius: 8px;
height: 40px;
}
.color-tag-preview:hover {
transform: scale(1.08);
.priority-label {
height: 40px;
border-top: 1px solid #ebeef5;
border-bottom: 1px solid #ebeef5;
background: #F9FAFF;
color: #BCBCBC;
font-weight: 400;
font-size: 14px;
white-space: nowrap;
padding: 0 14px;
line-height: 40px;
}
.color-tag-preview.is-empty {
background-image: linear-gradient(135deg,
#f5f5f5 25%,
transparent 25%,
transparent 50%,
#f5f5f5 50%,
#f5f5f5 75%,
transparent 75%);
background-size: 8px 8px;
box-shadow: 0 0 0 1px #dcdfe6;
.priority-select {
width: 110px;
}
.color-tag-dropdown {
position: absolute;
top: calc(100% + 8px);
left: 0;
z-index: 10;
background: #fff;
border-radius: 10px;
padding: 12px 14px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
min-width: 180px;
.priority-select .el-select__wrapper {
background: transparent;
padding: 5px 13px;
display: flex;
justify-content: space-between;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
}
.color-tag-title {
font-size: 13px;
font-weight: 600;
color: #333;
margin-bottom: 10px;
.priority-select :deep(.el-select__placeholder) {
color: #303133;
}
.color-tag-options {
display: flex;
.priority-option {
display: inline-flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.color-tag-option {
width: 24px;
height: 24px;
border-radius: 6px;
cursor: pointer;
border: 2px solid transparent;
transition:
transform 0.2s ease,
border-color 0.2s ease;
.priority-dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
}
.priority-dot.high {
background: #ff383c;
}
.priority-dot.medium {
background: #ff8d28;
}
.color-tag-option:hover {
transform: scale(1.12);
.priority-dot.low {
background: #3289fb;
}
.color-tag-option.active {
border-color: #1890ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
.priority-option-label {
font-size: 14px;
color: #303133;
}
//
:deep(.fujian .el-form-item__content > div) {
width: 100% !important;
}
}
</style>
<style lang="scss">
.hospital-order-form-dialog {
width: 1168px;
overflow: visible;
.body-footer {
border-radius: 0 0 24px 24px;
}
--el-component-size: 40px;
.el-select__wrapper {
height: 40px;
}
// ============== admin ==============
.fujian {
.upload-wrap {
width: 100%;
@ -753,177 +760,13 @@ onBeforeUnmount(() => {
}
}
}
}
// ============== ==============
:deep(.reward-dialog) {
.el-dialog__header {
display: none;
}
.el-dialog__body {
padding: 0;
}
}
.reward-content {
text-align: center;
padding: 24px 24px 22px;
}
.reward-badge {
display: inline-block;
background: #fef3e6;
color: #c47a2e;
font-size: 12px;
font-weight: 600;
padding: 4px 16px;
border-radius: 40px;
margin-bottom: 8px;
}
.coin-wrapper {
height: 80px;
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 4px;
}
.reward-coin {
font-size: 64px;
display: inline-block;
filter: drop-shadow(0 8px 16px rgba(255, 193, 7, 0.3));
}
.reward-coin.spin {
animation: rewardCoinSpin 0.8s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}
@keyframes rewardCoinSpin {
0% {
transform: rotateY(0deg) scale(0.2);
opacity: 0;
}
60% {
transform: rotateY(720deg) scale(1.2);
opacity: 1;
}
100% {
transform: rotateY(720deg) scale(1);
opacity: 1;
}
}
.points-number {
font-size: 48px;
font-weight: 800;
color: #f5a623;
line-height: 1.1;
margin-top: -4px;
}
.points-number small {
font-size: 20px;
font-weight: 600;
color: #b8863a;
}
.reward-title {
font-size: 22px;
font-weight: 700;
color: #1f140e;
margin: 4px 0 6px;
}
.reward-sub {
font-size: 14px;
color: #7a6a5c;
margin-bottom: 16px;
}
.reward-progress {
background: #f8f4f0;
border-radius: 16px;
padding: 14px 16px;
margin-bottom: 18px;
text-align: left;
}
.reward-progress-label {
display: flex;
justify-content: space-between;
font-size: 13px;
color: #4d3e32;
font-weight: 500;
gap: 8px;
}
.reward-distance {
color: #f5a623;
font-weight: 700;
flex-shrink: 0;
}
.reward-bar-bg {
width: 100%;
height: 6px;
background: #e6ddd5;
border-radius: 10px;
margin-top: 8px;
overflow: hidden;
}
.reward-bar-fill {
height: 100%;
background: linear-gradient(90deg, #f9b84a, #f5a623);
border-radius: 10px;
transition: width 0.6s ease;
}
.reward-current {
margin-top: 8px;
font-size: 12px;
color: #8a7a6a;
text-align: right;
}
.reward-actions {
display: flex;
gap: 10px;
}
.reward-btn {
flex: 1;
padding: 12px 0 !important;
font-size: 14px !important;
font-weight: 600 !important;
border-radius: 60px !important;
}
.reward-btn-received {
background: #f0ebe6 !important;
border-color: #f0ebe6 !important;
color: #4d3e32 !important;
}
.reward-btn-received:hover {
background: #e6ddd5 !important;
border-color: #e6ddd5 !important;
}
.reward-btn-assets {
background: #f9b84a !important;
border-color: #f9b84a !important;
color: #1f140e !important;
box-shadow: 0 4px 14px rgba(249, 184, 74, 0.25);
}
.reward-btn-assets:hover {
background: #f5ac2e !important;
border-color: #f5ac2e !important;
transform: translateY(-1px);
}
// popover teleport body scoped
.color-tag-popper {
padding: 12px 14px !important;
border-radius: 10px !important;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12) !important;
min-width: 180px;
}
</style>

@ -1,216 +1,119 @@
<template>
<div class="order-list">
<el-card shadow="never">
<el-form :inline="true" :model="filters" class="filter-form">
<el-form-item :label="$t('table.status')">
<el-select
v-model="filters.status"
:placeholder="$t('common.all')"
clearable
style="width: 140px"
>
<el-option label="待处理" value="待处理" />
<el-option label="已完成" value="已完成" />
<el-option label="取消" value="取消" />
</el-select>
</el-form-item>
<el-form-item :label="$t('table.priority')">
<el-select
v-model="filters.priority"
:placeholder="$t('common.all')"
clearable
style="width: 120px"
>
<el-option label="高" value="高" />
<el-option label="中" value="中" />
<el-option label="低" value="低" />
</el-select>
</el-form-item>
<el-form-item :label="$t('table.type')">
<el-select
v-model="filters.type"
:placeholder="$t('common.all')"
clearable
style="width: 120px"
>
<el-option label="故障问题" value="故障问题" />
<el-option label="使用咨询" value="使用咨询" />
<el-option label="功能需求" value="功能需求" />
<el-option label="其他" value="其他" />
</el-select>
</el-form-item>
<el-form-item :label="$t('placeholder.keyword')">
<el-input
v-model="filters.keyword"
:placeholder="$t('placeholder.keyword')"
clearable
style="width: 200px"
/>
</el-form-item>
<el-form-item :label="$t('table.time')">
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator="-"
:start-placeholder="$t('common.startDate')"
:end-placeholder="$t('common.endDate')"
value-format="YYYY-MM-DD"
:shortcuts="dateRangeShortcuts"
style="width: 240px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSearch">{{
$t("btn.query")
}}</el-button>
<el-button @click="onReset">{{ $t("btn.reset") }}</el-button>
<el-button type="success" @click="goQuick">{{
$t("btn.quickSubmit")
}}</el-button>
</el-form-item>
</el-form>
<!-- 工单总览 -->
<div class="order-stats">
<div
class="stat-card stat-total"
:class="{ active: activeStat === 'total' }"
@click="onStatClick('total')"
>
<template>
<div class="order-list admin-page-card">
<el-form :inline="true" :model="filters" class="admin-filter-form filter-form">
<el-form-item :label="''">
<el-input v-model="filters.keyword" :placeholder="$t('placeholder.keyword')" clearable style="width: 200px" />
</el-form-item>
<el-form-item :label="''">
<el-select v-model="filters.status" :placeholder="'全部状态'" clearable style="width: 140px">
<el-option label="待处理" value="待处理" />
<el-option label="已完成" value="已完成" />
<!-- <el-option label="取消" value="取消" /> -->
</el-select>
</el-form-item>
<el-form-item :label="''">
<el-select v-model="filters.priority" :placeholder="'全部优先级'" clearable style="width: 120px">
<el-option label="高" value="高" />
<el-option label="中" value="中" />
<el-option label="低" value="低" />
</el-select>
</el-form-item>
<el-form-item :label="''">
<el-select v-model="filters.type" :placeholder="'全部类型'" clearable style="width: 120px">
<el-option label="故障问题" value="故障问题" />
<el-option label="使用咨询" value="使用咨询" />
<el-option label="功能需求" value="功能需求" />
<el-option label="其他" value="其他" />
</el-select>
</el-form-item>
<el-form-item :label="$t('table.time')">
<el-date-picker v-model="dateRange" type="daterange" range-separator="-"
:start-placeholder="$t('common.startDate')" :end-placeholder="$t('common.endDate')" value-format="YYYY-MM-DD"
:shortcuts="dateRangeShortcuts" style="width: 240px" />
</el-form-item>
<el-button type="primary" plain @click="onSearch">{{
$t("btn.query")
}}</el-button>
<el-button @click="onReset">{{ $t("btn.reset") }}</el-button>
<el-button type="primary" @click="goQuick">+ </el-button>
</el-form>
<!-- 工单总览 -->
<!-- <div class="order-stats">
<div class="stat-card stat-total" :class="{ active: activeStat === 'total' }" @click="onStatClick('total')">
<div class="stat-icon">📋</div>
<div class="stat-value">{{ stats.total }}</div>
<div class="stat-label">{{ $t("stat.totalOrders") }}</div>
</div>
<div
class="stat-card stat-pending"
:class="{ active: activeStat === 'pending' }"
@click="onStatClick('pending')"
>
<div class="stat-card stat-pending" :class="{ active: activeStat === 'pending' }"
@click="onStatClick('pending')">
<div class="stat-icon"></div>
<div class="stat-value">{{ stats.pending }}</div>
<div class="stat-label">{{ $t("stat.pendingOrders") }}</div>
</div>
<div
class="stat-card stat-completed"
:class="{ active: activeStat === 'completed' }"
@click="onStatClick('completed')"
>
<div class="stat-card stat-completed" :class="{ active: activeStat === 'completed' }"
@click="onStatClick('completed')">
<div class="stat-icon"></div>
<div class="stat-value">{{ stats.completed }}</div>
<div class="stat-label">{{ $t("stat.completedOrders") }}</div>
</div>
<div
class="stat-card stat-high"
:class="{ active: activeStat === 'high' }"
@click="onStatClick('high')"
>
<div class="stat-card stat-high" :class="{ active: activeStat === 'high' }" @click="onStatClick('high')">
<div class="stat-icon">🔴</div>
<div class="stat-value">{{ stats.highPriority }}</div>
<div class="stat-label">{{ $t("stat.highPriorityOrders") }}</div>
</div>
</div>
<el-table :data="list" v-loading="loading" border stripe>
<el-table-column :label="$t('table.id')" prop="orderNo" width="160" />
<el-table-column :label="$t('table.priority')" width="90">
<template #default="{ row }">
<el-tag :type="priorityType(row.priority)" size="small">{{
row.priority
}}</el-tag>
</template>
</el-table-column>
<el-table-column
:label="$t('table.title')"
prop="title"
min-width="300"
show-overflow-tooltip
/>
<el-table-column
:label="$t('table.type')"
prop="serviceType"
width="120"
/>
<el-table-column
:label="$t('table.dept')"
prop="department"
min-width="120"
show-overflow-tooltip
/>
<el-table-column
:label="$t('table.submitter')"
prop="submitter"
min-width="100"
show-overflow-tooltip
/>
<el-table-column
:label="$t('table.feedbackchannel')"
prop="feedbackChannel"
min-width="100"
show-overflow-tooltip
/>
<el-table-column
:label="$t('table.registrar')"
prop="registrarName"
min-width="100"
show-overflow-tooltip
/>
<el-table-column
:label="$t('table.handler')"
prop="processName"
min-width="100"
show-overflow-tooltip
/>
<el-table-column
:label="$t('table.handleTime')"
prop="completedAt"
width="170"
>
<template #default="{ row }">
{{ row.completedAt || "-" }}
</template>
</el-table-column>
<el-table-column :label="$t('table.status')" width="100">
<template #default="{ row }">
<el-tag :type="statusType(row.status)" size="small">{{
row.status
}}</el-tag>
</template>
</el-table-column>
<el-table-column
:label="$t('table.time')"
prop="createdAt"
width="170"
/>
<el-table-column :label="$t('table.action')" width="160" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="goDetail(row.id)">{{
$t("btn.view")
}}</el-button>
<el-button link type="primary" @click="goEdit(row.id)">{{
</div> -->
<el-table class="admin-table" :data="list" v-loading="loading" stripe
header-cell-class-name="admin-table-header-cell">
<el-table-column :label="$t('table.id')" prop="orderNo" width="160" />
<el-table-column :label="$t('table.priority')" width="90">
<template #default="{ row }">
<div class="priority-row" :class="priorityClass(row.priority)">{{ row.priority }}</div>
</template>
</el-table-column>
<el-table-column :label="$t('table.title')" prop="title" min-width="250" show-overflow-tooltip />
<el-table-column :label="$t('table.type')" prop="serviceType" width="auto" />
<el-table-column :label="$t('table.dept')" prop="department" min-width="120" show-overflow-tooltip />
<el-table-column :label="$t('table.submitter')" prop="submitter" min-width="100" show-overflow-tooltip />
<el-table-column :label="$t('table.feedbackchannel')" prop="feedbackChannel" min-width="100"
show-overflow-tooltip />
<el-table-column :label="$t('table.registrar')" prop="registrarName" min-width="100" show-overflow-tooltip />
<el-table-column :label="$t('table.handler')" prop="processName" min-width="100" show-overflow-tooltip />
<el-table-column :label="$t('table.handleTime')" prop="completedAt" width="165">
<template #default="{ row }">
{{ row.completedAt || "-" }}
</template>
</el-table-column>
<el-table-column :label="$t('table.status')" width="100">
<template #default="{ row }">
<div class="status-pill" :class="statusClass(row.status)">{{ row.status }}</div>
</template>
</el-table-column>
<el-table-column :label="$t('table.time')" prop="createdAt" width="160" />
<el-table-column :label="$t('table.action')" min-width="250" fixed="right">
<template #default="{ row }">
<el-button class="purple-sign smaller" plain @click="goDetail(row.id)">{{
$t("btn.view")
}}</el-button>
<el-button class="rosein-sign smaller" plain :class="{ 'disabled-done': row.status === '已完成' }"
@click="goEdit(row.id)">{{
$t("btn.edit")
}}</el-button>
<el-button
link
type="danger"
v-if="row.status === '待处理'"
@click="cancel(row)"
>{{ $t("btn.cancel") }}</el-button
>
</template>
</el-table-column>
</el-table>
<div class="pagination">
<el-pagination
v-model:current-page="page"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="loadList"
@current-change="loadList"
/>
</div>
</el-card>
<el-button class="yellow-sign smaller" plain :class="{ 'disabled-done': row.status === '已完成' }"
@click="cancel(row)">{{
$t("btn.cancel") }}</el-button>
</template>
</el-table-column>
</el-table>
<div class="admin-pagination">
<el-pagination v-model:current-page="page" v-model:page-size="pageSize" :page-sizes="[10, 20, 50]" :total="total"
layout="total, sizes, prev, pager, next, jumper" @size-change="loadList" @current-change="loadList" />
</div>
<!-- 工单详情 弹窗查看操作直接打开弹窗编辑成功后刷新列表 -->
<OrderDetailDialog v-model="detailVisible" :order-id="currentOrderId" @updated="loadList" />
</div>
</template>
@ -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(() => {
</script>
<style scoped lang="scss">
@use "@/assets/style/admin-common.scss" as *;
.filter-form {
margin-bottom: 12px;
}
.pagination {
margin-top: 16px;
display: flex;
justify-content: flex-end;
}
flex-wrap: wrap;
gap: 12px;
align-items: center;
width: 1800px;
//
.order-stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 16px;
:deep(.el-form-item),
:deep(.el-button) {
margin: 0 0 0 0;
}
:deep(.el-button:last-child) {
margin-left: auto;
}
}
.stat-card {
position: relative;
// admin/orders/list.vue
.priority-row {
width: 50px;
height: 30px;
border-radius: 8px 8px 8px 8px;
display: flex;
align-items: center;
gap: 14px;
padding: 18px 20px;
background: #fff;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
border-left: 4px solid #d9d9d9;
transition:
transform 0.2s ease,
box-shadow 0.2s ease,
background 0.2s ease;
cursor: pointer;
user-select: none;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.08);
}
.stat-card.active {
background: #f0f7ff;
box-shadow: 0 4px 14px rgba(24, 144, 255, 0.18);
transform: translateY(-2px);
}
.stat-card.active.stat-pending {
background: #fff7e6;
box-shadow: 0 4px 14px rgba(250, 140, 22, 0.18);
}
.stat-card.active.stat-completed {
background: #f6ffed;
box-shadow: 0 4px 14px rgba(82, 196, 26, 0.18);
}
.stat-card.active.stat-high {
background: #fff1f0;
box-shadow: 0 4px 14px rgba(255, 77, 79, 0.18);
}
.stat-total {
border-left-color: #1890ff;
}
.stat-pending {
border-left-color: #fa8c16;
}
.stat-completed {
border-left-color: #52c41a;
}
.stat-high {
border-left-color: #ff4d4f;
}
.stat-icon {
font-size: 28px;
line-height: 1;
}
.stat-value {
font-size: 26px;
font-weight: 700;
color: #333;
line-height: 1.1;
justify-content: center;
gap: 4px;
color: var(--current-color);
font-weight: 500;
font-size: 14px;
&::before {
content: "";
display: block;
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--current-color);
}
&.high {
--current-color: #ff383c;
background: #ffebec;
}
&.medium {
--current-color: #ff8d28;
background: #ffefe2;
}
&.low {
--current-color: #3289fb;
background: #eaf3ff;
}
&.info {
--current-color: #999999;
background: #f0f0f0;
}
}
.stat-label {
font-size: 13px;
color: #999;
margin-top: 4px;
//
.status-pill {
width: 66px;
height: 30px;
text-align: center;
line-height: 30px;
border-radius: 8px 8px 8px 8px;
font-weight: 500;
font-size: 14px;
&.done {
background: #E8FFEE;
color: #34C759;
}
&.pending {
background: #FFEFE2;
color: #FF8D28;
}
&.processing {
background: #EAF3FF;
color: #3289FB;
}
&.cancel {
background: #F0F0F0;
color: #999999;
}
&.info {
background: #F0F0F0;
color: #666666;
}
}
</style>
<style lang="scss">
@use "@/assets/style/admin-common.scss" as *;
</style>

@ -28,15 +28,17 @@
<div class="bottom">
<div class="points-card__stats">
<div class="stat-item">
<img src="@/assets/image/hospital/ponits/icon-check-orange.svg" class="stat-icon" alt="" />
<img src="@/assets/image/hospital/ponits/rili-small.svg" class="stat-icon" alt="" />
<span class="stat-label">本月</span>
<strong>+{{ monthPoints }}</strong>
</div>
<div class="stat-divider"></div>
<div class="stat-item">
<img src="@/assets/image/hospital/ponits/icon-check-orange.svg" class="stat-icon" alt="" />
<img v-if="todayPoints >= 0" src="@/assets/image/hospital/ponits/arrow-up.svg" class="stat-icon"
alt="" />
<img v-else src="@/assets/image/hospital/ponits/arrow-down.svg" class="stat-icon" alt="" />
<span class="stat-label">今日</span>
<strong>+{{ todayPoints }}</strong>
<strong>{{ todayPoints >= 0 ? '+' + todayPoints : todayPoints }}</strong>
</div>
</div>
</div>
@ -165,7 +167,8 @@
</el-card>
<!-- ========== 兑换表单弹窗 ========== -->
<el-dialog v-model="exchangeVisible" style="padding: 40px;" title="🎁 兑换礼物" width="520px" :close-on-click-modal="false" append-to-body>
<el-dialog v-model="exchangeVisible" style="padding: 40px;" title="🎁 兑换礼物" width="520px"
:close-on-click-modal="false" append-to-body>
<div class="redeem-sub">
填写收件信息,即可兑换{{ REWARD_NAME }}
</div>
@ -198,19 +201,15 @@
<!-- ========== 抽奖弹窗 ========== -->
<LotteryDialog v-model="lotteryVisible" :drawable="!summary.isDraw" :batch-goal="50"
:batch-current="Number(summary.rewardCount) || 5" @success="onLotterySuccess"
@view-account="onLotteryViewAccount" />
<!-- ========== 我的资产弹窗 ========== -->
<AssetsDialog v-model="assetsVisible" :total-points="totalPoints" :exchange-goal="EXCHANGE_GOAL"
:reward-name="REWARD_NAME" :today-order="todayBreakdown.order" :today-sign-in="todayBreakdown.signIn"
:today-draw="todayBreakdown.draw" />
:batch-current="Number(summary.rewardCount) || 5" @success="onLotterySuccess" />
<!-- ========== 签到弹窗 ========== -->
<SignInDialog v-model="signInVisible" :signed-in="!!summary.isSignIn" :total-points="totalPoints"
:consecutive-sign-in-days="consecutiveSignInDays" :sign-in-rank="signInRank" :beat-percent="beatPercent"
@success="onSignInSuccess" />
<!-- 我的资产弹窗已迁到 AdminLayout 全局(AssetDialogStore 控制)避免重复弹窗 -->
<!-- ========== 兑换记录弹窗 ========== -->
<el-dialog v-model="redeemLogVisible" class="redeem-log-dialog" append-to-body>
<div class="redeem-body">
@ -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(() => {

@ -1,27 +1,33 @@
<template>
<div class="workbench">
<div class="workbench admin-page-card">
<!-- 时间筛选 -->
<div class="filter-bar">
<el-radio-group v-model="period" size="small" @change="loadStats">
<el-radio-button label="month">{{ $t('common.thisMonth') }}</el-radio-button>
<el-radio-button label="quarter">{{ $t('common.thisQuarter') }}</el-radio-button>
<el-radio-button label="year">{{ $t('common.thisYear') }}</el-radio-button>
<el-radio-button label="custom">{{ $t('common.customDate') }}</el-radio-button>
</el-radio-group>
<el-date-picker v-if="period === 'custom'" v-model="dateRange" type="daterange" range-separator="-"
:start-placeholder="$t('common.startDate')" :end-placeholder="$t('common.endDate')" value-format="YYYY-MM-DD"
:shortcuts="dateRangeShortcuts" size="small" style="margin-left: 12px" @change="loadStats" />
<div class="period-bar">
<div class="period-tabs">
<el-button v-for="t in periodTabs" :key="t.value"
:class="['period-tab', { active: period === t.value && !isCustom }]" plain @click="onPickPeriod(t.value)">
{{ t.label }}
</el-button>
<el-button :class="['period-tab', { active: isCustom }]" plain @click="onPickCustom">{{
$t('common.customDate')
}}</el-button>
</div>
<div v-if="isCustom" class="custom-range">
<el-date-picker v-model="dateRange" type="daterange" range-separator="-"
:start-placeholder="$t('common.startDate')" :end-placeholder="$t('common.endDate')"
value-format="YYYY-MM-DD" style="width: 240px" />
<el-button type="primary" class="search-btn" plain @click="onSearch"></el-button>
</div>
</div>
</div>
<!-- 统计卡片 -->
<div class="stat-cards">
<div class="stat-card" v-for="card in cards" :key="card.label" @click="goToOrders(card)">
<div class="stat-icon" :style="{ background: card.bg }">
<el-icon :size="22" :color="card.color">
<component :is="card.icon" />
</el-icon>
<div class="stat-card" v-for="card in cards" :key="card.key" @click="goToOrders(card)">
<div class="stat-icon">
<img :src="card.icon" alt="" />
</div>
<div class="stat-info">
<div class="right">
<div class="stat-value">{{ card.value }}</div>
<div class="stat-label">{{ card.label }}</div>
</div>
@ -29,22 +35,30 @@
</div>
<!-- 趋势图 -->
<el-card class="trend-card" shadow="never">
<template #header>
<div class="card-header">
<span class="card-title">{{ $t('chart.title') }}</span>
<div class="chart-switch">
<span class="switch-item" :class="{ active: chartType === 'bar' }" @click="chartType = 'bar'">
<i class="switch-icon bar" /> {{ $t('chart.bar') }}
</span>
<span class="switch-item" :class="{ active: chartType === 'line' }" @click="chartType = 'line'">
<i class="switch-icon line" /> {{ $t('chart.line') }}
</span>
</div>
<div class="trend-card">
<div class="card-header">
<span class="border-title">{{ $t('chart.title') }}</span>
<div class="chart-switch" :class="{ 'is-line': chartType === 'line' }">
<span class="switch-item" :class="{ active: chartType === 'bar' }" @click="chartType = 'bar'">
{{ $t('chart.bar') }}
</span>
<span class="switch-item" :class="{ active: chartType === 'line' }" @click="chartType = 'line'">
{{ $t('chart.line') }}
</span>
</div>
</div>
<v-chart ref="chartRef" class="chart" :option="chartOption" autoresize />
<div class="chart-legend">
<div class="legend-item">
<span class="legend-dot legend-dot--submit" />
<span class="legend-text">{{ $t('chart.submit') }}</span>
</div>
</template>
<v-chart ref="chartRef" class="chart" :option="chartOption" autoresize />
</el-card>
<div class="legend-item">
<span class="legend-dot legend-dot--complete" />
<span class="legend-text">{{ $t('chart.complete') }}</span>
</div>
</div>
</div>
</div>
</template>
@ -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;
}
</style>
<style lang="scss">
@use "@/assets/style/admin-common.scss" as *;
.workbench {
.filter-bar {
background: #fff;
.period-bar {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 16px;
.period-tabs {
display: flex;
gap: 10px;
}
.period-tab {
background: #FFFFFF !important;
border-radius: 8px 8px 8px 8px !important;
border: 1px solid #E0E0E0 !important;
font-weight: normal !important;
font-size: 16px !important;
color: #1D2129 !important;
line-height: 22px !important;
width: auto !important;
height: auto !important;
padding: 10px 24px !important;
&:hover,
&.active {
background: #3289FB !important;
border-color: #3289FB !important;
color: #fff !important;
}
}
.search-btn {
font-size: 16px !important;
line-height: 22px !important;
width: auto !important;
height: auto !important;
padding: 10px 24px !important;
&:hover,
&.active {
background: #3289FB !important;
border-color: #3289FB !important;
color: #fff !important;
}
}
.custom-range {
display: flex;
align-items: center;
gap: 10px;
.el-date-editor {
background: #FFFFFF !important;
border-radius: 8px 8px 8px 8px !important;
border: 1px solid #E0E0E0 !important;
overflow: hidden;
height: 44px !important;
}
}
}
}
}
</style>
Loading…
Cancel
Save