index.vue 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647
  1. <template>
  2. <div class="ehr-page">
  3. <Header />
  4. <div class="banner pt-2">
  5. <section class="hero">
  6. <div class="hero-inner">
  7. <!-- 判断上下午 -->
  8. <h1 class="hero-title pt-5">
  9. {{ getGreeting() }},{{ userStore.getUser.nickname }}
  10. </h1>
  11. <p class="hero-desc">
  12. 今天是 {{ new Date().toLocaleDateString() }}。您有
  13. {{ stats[0].number }}条流程待处理。
  14. </p>
  15. </div>
  16. </section>
  17. <!-- 任务统计 -->
  18. <section class="total">
  19. <div class="total-card" v-for="(item, index) in stats" :key="index">
  20. <el-popover
  21. placement="top"
  22. :width="100"
  23. trigger="hover"
  24. popper-class="glass-popover"
  25. :disabled="getDetailList(index).length === 0"
  26. transition="el-zoom-in-left"
  27. >
  28. <template #reference>
  29. <div
  30. class="card-wrapper flex items-center justify-center gap-4 flex-col md:flex-row"
  31. >
  32. <!-- ... 图标和内容 ... -->
  33. <div class="">
  34. <img
  35. v-if="item.title === '我的待办'"
  36. src="../../assets//images/todo.png"
  37. alt=""
  38. class="h-15 w-15"
  39. />
  40. <img
  41. v-if="item.title === '已办事项'"
  42. src="../../assets//images/done.png"
  43. alt=""
  44. class="h-15 w-15"
  45. />
  46. </div>
  47. <div class="card-content">
  48. <p class="card-title">{{ item.title }}</p>
  49. <el-skeleton
  50. :rows="1"
  51. :animated="true"
  52. :loading="statsLoading"
  53. >
  54. <template #template>
  55. <el-skeleton-item
  56. variant="text"
  57. style="
  58. width: 60%;
  59. height: 32px;
  60. border-radius: 50px;
  61. background: #0a193d;
  62. "
  63. />
  64. </template>
  65. <p class="card-number">{{ item.number }}</p>
  66. </el-skeleton>
  67. <p
  68. v-if="item.title === '我的待办'"
  69. class="text-[#6b6f99] !text-sm"
  70. >
  71. 待处理的流程
  72. </p>
  73. <p v-else class="text-[#6b6f99] !text-sm">已完成的流程</p>
  74. </div>
  75. </div>
  76. </template>
  77. <div class="detail-list">
  78. <div
  79. v-for="(task, idx) in getDetailList(index)"
  80. :key="idx"
  81. class="detail-item"
  82. @click="handleDetailClick(task, item.title)"
  83. >
  84. <span class="detail-name">{{ task.name }}</span>
  85. <span class="detail-val">{{ task.value }}</span>
  86. </div>
  87. <div v-if="getDetailList(index).length === 0" class="empty-tip">
  88. 暂无详细数据
  89. </div>
  90. </div>
  91. </el-popover>
  92. </div>
  93. </section>
  94. </div>
  95. <div class="content mt-5">
  96. <div class="search-bar">
  97. <div class="search-input">
  98. <Icon icon="mdi:magnify" class="search-icon" />
  99. <input
  100. v-model.trim="searchKeyword"
  101. type="text"
  102. class="search-field"
  103. placeholder="搜索流程名称"
  104. />
  105. </div>
  106. <span v-if="searchKeyword" class="search-meta">
  107. 共找到 {{ displayedFlows.length }} 条结果
  108. </span>
  109. </div>
  110. <div class="tabs-wrapper">
  111. <!-- 左侧箭头 -->
  112. <button
  113. class="scroll-arrow left"
  114. @click="scrollTabs('left')"
  115. :disabled="isLeftDisabled"
  116. >
  117. <Icon icon="mdi:chevron-left" />
  118. </button>
  119. <!-- Tab 列表容器 -->
  120. <div
  121. class="tabs-container"
  122. ref="tabsContainerRef"
  123. role="tablist"
  124. aria-label="EHR模块"
  125. >
  126. <button
  127. class="el-tab-item"
  128. type="button"
  129. role="tab"
  130. :class="{ 'is-active': activeKey === 'all' }"
  131. :aria-selected="activeKey === 'all'"
  132. @click="setAll"
  133. >
  134. <span class="tab-label">全部</span>
  135. </button>
  136. <button
  137. v-for="tab in tabs"
  138. :key="tab.groupName"
  139. class="el-tab-item"
  140. :class="{ 'is-active': tab.groupName === activeKey }"
  141. type="button"
  142. role="tab"
  143. :aria-selected="tab.groupName === activeKey"
  144. @click="getById(tab)"
  145. >
  146. <span class="tab-label">{{ tab.groupName }}</span>
  147. </button>
  148. </div>
  149. <!-- 右侧箭头 -->
  150. <button
  151. class="scroll-arrow right"
  152. @click="scrollTabs('right')"
  153. :disabled="!isLeftDisabled"
  154. >
  155. <Icon icon="mdi:chevron-right" />
  156. </button>
  157. </div>
  158. <div role="tabpanel">
  159. <div v-if="displayedFlows.length" class="items-grid">
  160. <div
  161. v-for="item in displayedFlows"
  162. :key="item.id"
  163. class="item-card"
  164. :style="{
  165. '--item-accent': getIconTheme(item).color,
  166. '--item-accent-soft': getIconTheme(item).softColor,
  167. '--item-accent-hover': getIconTheme(item).hoverColor,
  168. }"
  169. @click="go(item)"
  170. >
  171. <div class="item-type-badge">
  172. <span>{{ item.type }}</span>
  173. </div>
  174. <div class="item-top">
  175. <div
  176. class="item-icon"
  177. :style="{
  178. // 将主题色作为渐变的主色调传入,或者直接在这里写死渐变逻辑
  179. '--theme-color': getIconTheme(item).color,
  180. '--theme-soft': getIconTheme(item).softColor,
  181. }"
  182. >
  183. <Icon
  184. :icon="item.icon || 'mdi:file-document-outline'"
  185. :color="getIconTheme(item).color"
  186. />
  187. </div>
  188. </div>
  189. <div class="item-body">
  190. <h3 class="item-name">
  191. {{ item.flowName }}
  192. </h3>
  193. </div>
  194. </div>
  195. </div>
  196. <div v-else class="empty-state">
  197. <Icon icon="mdi:file-search-outline" class="empty-icon" />
  198. <p class="empty-title">没有找到相关流程</p>
  199. <p class="empty-desc">试试换个名称关键词搜索</p>
  200. </div>
  201. </div>
  202. </div>
  203. <Footer />
  204. </div>
  205. </template>
  206. <script setup>
  207. import Header from "@components/home/header.vue";
  208. import Footer from "@components/home/Footer.vue";
  209. import { computed, ref, onMounted, onBeforeUnmount, nextTick } from "vue";
  210. import { Icon } from "@iconify/vue";
  211. import {
  212. getFlows,
  213. ssoLogin,
  214. getOATasks,
  215. getCRMTasks,
  216. getSRMTasks,
  217. } from "@/api/user";
  218. import { useUserStore } from "@/stores/useUserStore";
  219. import { getAccessToken } from "@/utils/auth";
  220. import { useRouter } from "vue-router";
  221. import dd from "dingtalk-jsapi";
  222. const router = useRouter();
  223. import { ElLoading, ElMessage } from "element-plus";
  224. const userStore = useUserStore();
  225. // 1. 定义 Ref
  226. const tabsContainerRef = ref(null);
  227. const isLeftDisabled = ref(true);
  228. const isRightDisabled = ref(true);
  229. // 2. 滚动逻辑
  230. const scrollTabs = (direction) => {
  231. const container = tabsContainerRef.value;
  232. if (!container) return;
  233. // 每次滚动的距离,可以根据需求调整,例如 200px 或 container.clientWidth / 2
  234. const scrollAmount = 200;
  235. if (direction === "left") {
  236. container.scrollBy({ left: -scrollAmount, behavior: "smooth" });
  237. } else {
  238. container.scrollBy({ left: scrollAmount, behavior: "smooth" });
  239. }
  240. };
  241. // 3. 更新箭头禁用状态
  242. const updateArrowState = () => {
  243. const container = tabsContainerRef.value;
  244. if (!container) return;
  245. const { scrollLeft, scrollWidth, clientWidth } = container;
  246. // 如果滚动条在最左边(容差1px),禁用左箭头
  247. isLeftDisabled.value = scrollLeft <= 1;
  248. // 如果滚动条在最右边(容差1px),禁用右箭头
  249. // Math.ceil 处理小数像素问题
  250. isRightDisabled.value =
  251. Math.ceil(scrollLeft + clientWidth) >= scrollWidth - 1;
  252. };
  253. // 4. 监听滚动事件
  254. const handleTabScroll = () => {
  255. updateArrowState();
  256. };
  257. const getGreeting = () => {
  258. const hour = new Date().getHours();
  259. if (hour < 12) return "早上好";
  260. if (hour < 18) return "下午好";
  261. return "晚上好";
  262. };
  263. const getDetailList = (index) => {
  264. switch (index) {
  265. case 0: // 我的待办
  266. return todoCount.value;
  267. case 1: // 已办事项
  268. return doneCount.value;
  269. case 2: // 发起流程
  270. return startCount.value;
  271. case 3: // 草稿箱
  272. return drafts.value;
  273. default:
  274. return [];
  275. }
  276. };
  277. const tabs = ref([]);
  278. const activeKey = ref("all");
  279. const searchKeyword = ref("");
  280. const iconPalette = [
  281. {
  282. color: "#2563eb",
  283. softColor: "rgba(37, 99, 235, 0.12)",
  284. hoverColor: "rgba(37, 99, 235, 0.18)",
  285. },
  286. {
  287. color: "#db2777",
  288. softColor: "rgba(219, 39, 119, 0.12)",
  289. hoverColor: "rgba(219, 39, 119, 0.18)",
  290. },
  291. {
  292. color: "#ea580c",
  293. softColor: "rgba(234, 88, 12, 0.12)",
  294. hoverColor: "rgba(234, 88, 12, 0.18)",
  295. },
  296. {
  297. color: "#059669",
  298. softColor: "rgba(5, 150, 105, 0.12)",
  299. hoverColor: "rgba(5, 150, 105, 0.18)",
  300. },
  301. {
  302. color: "#7c3aed",
  303. softColor: "rgba(124, 58, 237, 0.12)",
  304. hoverColor: "rgba(124, 58, 237, 0.18)",
  305. },
  306. {
  307. color: "#0f766e",
  308. softColor: "rgba(15, 118, 110, 0.12)",
  309. hoverColor: "rgba(15, 118, 110, 0.18)",
  310. },
  311. {
  312. color: "#dc2626",
  313. softColor: "rgba(220, 38, 38, 0.12)",
  314. hoverColor: "rgba(220, 38, 38, 0.18)",
  315. },
  316. {
  317. color: "#ca8a04",
  318. softColor: "rgba(202, 138, 4, 0.12)",
  319. hoverColor: "rgba(202, 138, 4, 0.18)",
  320. },
  321. ];
  322. const hashText = (text = "") =>
  323. String(text)
  324. .split("")
  325. .reduce((hash, char) => hash * 31 + char.charCodeAt(0), 0);
  326. const getIconTheme = (item) => {
  327. const seed = item?.id ?? item?.flowName ?? item?.type ?? "";
  328. const index = Math.abs(hashText(seed)) % iconPalette.length;
  329. return iconPalette[index];
  330. };
  331. const allTab = computed(() => {
  332. const flowRespVOS = tabs.value.flatMap((tab) => tab.flowRespVOS || []);
  333. return {
  334. groupName: "全部",
  335. remark: "全部流程",
  336. flowRespVOS,
  337. };
  338. });
  339. const activeTab = computed(() => {
  340. if (activeKey.value === "all") {
  341. return allTab.value;
  342. }
  343. return (
  344. tabs.value.find((tab) => tab.groupName === activeKey.value) || allTab.value
  345. );
  346. });
  347. const displayedFlows = computed(() => {
  348. const keyword = searchKeyword.value.trim().toLowerCase();
  349. if (!keyword) {
  350. return activeTab.value.flowRespVOS || [];
  351. }
  352. return (allTab.value.flowRespVOS || []).filter((item) =>
  353. String(item?.flowName || "")
  354. .toLowerCase()
  355. .includes(keyword),
  356. );
  357. });
  358. const getAll = async () => {
  359. const res = await getFlows({
  360. pageNo: 1,
  361. pageSize: 99,
  362. });
  363. tabs.value = res;
  364. };
  365. const setAll = () => {
  366. activeKey.value = "all";
  367. };
  368. const getById = (tab) => {
  369. activeKey.value = tab.groupName;
  370. };
  371. const go = async (item) => {
  372. if (userStore.getUser.username && getAccessToken()) {
  373. if (item.type === "OA") {
  374. const res = await ssoLogin({
  375. username: userStore.getUser.username,
  376. });
  377. if (res) {
  378. const ua = window.navigator.userAgent.toLowerCase();
  379. if (ua.includes("dingtalk") || ua.includes("dingtalkwork")) {
  380. // 钉钉环境
  381. // const loading = ElLoading.service({
  382. // lock: true,
  383. // text: "正在跳转,请稍候...",
  384. // background: "rgba(0, 0, 0, 0.7)",
  385. // });
  386. // const targetUrl1 = item.indexUrl + "?ssoToken=" + res + "#/main";
  387. // const targetUrl2 = item.flowUrl;
  388. // dd.biz.util.openLink({
  389. // url: targetUrl1,
  390. // onSuccess: () => {
  391. // setTimeout(() => {
  392. // dd.biz.util.openLink({
  393. // url: targetUrl2,
  394. // onSuccess: () => {
  395. // loading.close();
  396. // },
  397. // onFail: (err) => {
  398. // loading.close();
  399. // ElMessage.error("跳转失败,请重试");
  400. // },
  401. // });
  402. // }, 2000);
  403. // },
  404. // onFail: (err) => {
  405. // loading.close();
  406. // ElMessage.error("打开链接失败,请重试");
  407. // },
  408. // });
  409. if (window.dd) {
  410. const targetUrl = item.appUrl;
  411. if (targetUrl) {
  412. dd.biz.util.openLink({
  413. url: targetUrl,
  414. onSuccess: () => {},
  415. onFail: (err) => {
  416. // loading.close();
  417. ElMessage.error("打开链接失败,请重试");
  418. },
  419. });
  420. } else {
  421. const targetUrl1 = item.indexUrl + "?ssoToken=" + res + "#/main";
  422. const targetUrl2 = item.flowUrl;
  423. dd.biz.util.openLink({
  424. url: targetUrl1,
  425. onSuccess: () => {
  426. setTimeout(() => {
  427. dd.biz.util.openLink({
  428. url: targetUrl2,
  429. onSuccess: () => {},
  430. onFail: (err) => {
  431. ElMessage.error("跳转失败,请重试");
  432. },
  433. });
  434. }, 2000);
  435. },
  436. onFail: (err) => {
  437. ElMessage.error("打开链接失败,请重试");
  438. },
  439. });
  440. }
  441. } else if (window.DingTalkPC) {
  442. const loading = ElLoading.service({
  443. lock: true,
  444. text: "正在跳转,请稍候...",
  445. background: "rgba(0, 0, 0, 0.7)",
  446. });
  447. const newTab = window.open("", "_blank");
  448. newTab.location.href =
  449. item.indexUrl + "?ssoToken=" + res + "#/main";
  450. setTimeout(() => {
  451. newTab.location.href = item.flowUrl;
  452. setTimeout(() => {
  453. loading.close();
  454. }, 500);
  455. }, 100);
  456. } else {
  457. console.log("❌ 非钉钉环境");
  458. }
  459. } else {
  460. // 浏览器环境
  461. const loading = ElLoading.service({
  462. lock: true,
  463. text: "正在跳转,请稍候...",
  464. background: "rgba(0, 0, 0, 0.7)",
  465. });
  466. const newTab = window.open("", "_blank");
  467. newTab.location.href = item.indexUrl + "?ssoToken=" + res + "#/main";
  468. setTimeout(() => {
  469. newTab.location.href = item.flowUrl;
  470. setTimeout(() => {
  471. loading.close();
  472. }, 500);
  473. }, 100);
  474. }
  475. }
  476. }
  477. if (item.type === "CRM") {
  478. const ua = window.navigator.userAgent.toLowerCase();
  479. if (ua.includes("dingtalk") || ua.includes("dingtalkwork")) {
  480. dd.biz.util.openLink({
  481. url:
  482. item.indexUrl +
  483. "/global/sso/callback/00APEB9EEEA9B2E338B686B7ECFA8585808C.action?token=" +
  484. getAccessToken(), // 先跳你的 SSO 链接
  485. onSuccess: () => {
  486. // 延迟跳目标业务地址(和你原来 setTimeout 逻辑一致)
  487. setTimeout(() => {
  488. dd.biz.util.openLink({ url: item.flowUrl });
  489. }, 100);
  490. },
  491. });
  492. } else {
  493. const newTab = window.open("", "_blank");
  494. newTab.location.href =
  495. item.indexUrl +
  496. "/global/sso/callback/00APEB9EEEA9B2E338B686B7ECFA8585808C.action?token=" +
  497. getAccessToken();
  498. setTimeout(function () {
  499. newTab.location.href = item.flowUrl;
  500. }, 100);
  501. }
  502. }
  503. } else {
  504. router.push({ path: "/login" });
  505. }
  506. };
  507. const handleDetailClick = (task, categoryTitle) => {
  508. console.log(`点击了 ${categoryTitle} 中的 ${task.name}: ${task.value}`);
  509. // 示例:根据类型跳转
  510. if (task.name === "OA" && categoryTitle === "我的待办") {
  511. router.push({
  512. path: "/todo-list",
  513. query: { type: task.name.toLowerCase() },
  514. });
  515. }
  516. if (task.name === "OA" && categoryTitle === "已办事项") {
  517. router.push({
  518. path: "/oa-done-list",
  519. query: { type: task.name.toLowerCase() },
  520. });
  521. }
  522. if (task.name === "CRM" && categoryTitle === "我的待办") {
  523. router.push({
  524. path: "/crm-todo-list",
  525. query: { type: task.name.toLowerCase() },
  526. });
  527. }
  528. if (task.name === "CRM" && categoryTitle === "已办事项") {
  529. router.push({
  530. path: "/crm-done-list",
  531. query: { type: task.name.toLowerCase() },
  532. });
  533. }
  534. if (task.name === "SRM" && categoryTitle === "我的待办") {
  535. router.push({
  536. path: "/srm-todo-list",
  537. query: { type: task.name.toLowerCase() },
  538. });
  539. }
  540. if (task.name === "SRM" && categoryTitle === "已办事项") {
  541. router.push({
  542. path: "/srm-done-list",
  543. query: { type: task.name.toLowerCase() },
  544. });
  545. }
  546. };
  547. let oaTasks = ref([]);
  548. let crmTasks = ref([]);
  549. let srmTasks = ref([]);
  550. const statsLoading = ref(true);
  551. const stats = ref([
  552. {
  553. icon: "mdi:clock-outline",
  554. title: "我的待办",
  555. number: 0,
  556. extra: "+2 今日",
  557. bgcolor: "#fff7ed",
  558. color: "#f59e0b",
  559. },
  560. {
  561. icon: "mdi:check-circle-outline",
  562. title: "已办事项",
  563. number: 0,
  564. bgcolor: "#eff6ff",
  565. color: "#2563eb",
  566. },
  567. ]);
  568. const todoCount = ref([
  569. { name: "OA", value: 0 },
  570. { name: "CRM", value: 0 },
  571. { name: "SRM", value: 0 },
  572. ]);
  573. // 已办事项
  574. const doneCount = ref([
  575. { name: "OA", value: 0 },
  576. { name: "CRM", value: 0 },
  577. { name: "SRM", value: 0 },
  578. ]);
  579. onMounted(async () => {
  580. getAll();
  581. // 等待 DOM 与样式生效,避免移动端首屏尺寸为 0
  582. await nextTick();
  583. updateArrowState();
  584. // 添加滚动监听
  585. if (tabsContainerRef.value) {
  586. tabsContainerRef.value.addEventListener("scroll", handleTabScroll);
  587. }
  588. if (userStore.getUser.username) {
  589. try {
  590. const res = await getOATasks({
  591. id: userStore.getUser.username,
  592. pageNum: 1,
  593. pageSize: 99,
  594. });
  595. oaTasks.value = res;
  596. const crmRes = await getCRMTasks({
  597. id: userStore.getUser.username,
  598. type: "pending",
  599. pageNum: 1,
  600. pageSize: 10,
  601. });
  602. crmTasks.value = crmRes;
  603. // const srmRes = await getSRMTasks({
  604. // id: userStore.getUser.username,
  605. // pageNum: 1,
  606. // pageSize: 10,
  607. // });
  608. // srmTasks.value = srmRes;
  609. stats.value[0].number =
  610. Number(oaTasks.value.todoCount) + Number(crmTasks.value.todoCount);
  611. todoCount.value = [
  612. { name: "OA", value: oaTasks.value.todoCount ?? 0 },
  613. { name: "CRM", value: crmTasks.value.todoCount ?? 0 },
  614. // { name: "SRM", value: srmTasks.value.todoCount ?? 0 },
  615. ];
  616. // *****************已办事项统计*************************
  617. const crmDoneRes = await getCRMTasks({
  618. id: userStore.getUser.username,
  619. type: "approved",
  620. pageNum: 1,
  621. pageSize: 10,
  622. });
  623. // const srmDoneRes = await getSRMTasks({
  624. // id: userStore.getUser.username,
  625. // pageNum: 1,
  626. // pageSize: 10,
  627. // });
  628. stats.value[1].number =
  629. Number(oaTasks.value.doneCount) + Number(crmDoneRes.todoCount);
  630. doneCount.value = [
  631. { name: "OA", value: oaTasks.value.doneCount ?? 0 },
  632. { name: "CRM", value: crmDoneRes.todoCount ?? 0 },
  633. // { name: "SRM", value: srmDoneRes.doneCount ?? 0 },
  634. ];
  635. } finally {
  636. statsLoading.value = false;
  637. }
  638. setInterval(
  639. async () => {
  640. const res = await getOATasks({
  641. id: userStore.getUser.username,
  642. pageNum: 1,
  643. pageSize: 10,
  644. });
  645. oaTasks.value = res;
  646. const crmRes = await getCRMTasks({
  647. id: userStore.getUser.username,
  648. type: "pending",
  649. pageNum: 1,
  650. pageSize: 10,
  651. });
  652. crmTasks.value = crmRes;
  653. // const srmRes = await getSRMTasks({
  654. // id: userStore.getUser.username,
  655. // pageNum: 1,
  656. // pageSize: 10,
  657. // });
  658. // srmTasks.value = srmRes;
  659. stats.value[0].number =
  660. Number(oaTasks.value.todoCount) + Number(crmTasks.value.todoCount);
  661. todoCount.value = [
  662. { name: "OA", value: oaTasks.value.todoCount ?? 0 },
  663. { name: "CRM", value: crmTasks.value.todoCount ?? 0 },
  664. // { name: "SRM", value: srmTasks.value.todoCount ?? 0 },
  665. ];
  666. const crmDoneRes = await getCRMTasks({
  667. id: userStore.getUser.username,
  668. type: "approved",
  669. pageNum: 1,
  670. pageSize: 10,
  671. });
  672. // const srmDoneRes = await getSRMTasks({
  673. // id: userStore.getUser.username,
  674. // pageNum: 1,
  675. // pageSize: 10,
  676. // });
  677. stats.value[1].number =
  678. Number(oaTasks.value.doneCount) + Number(crmDoneRes.todoCount);
  679. doneCount.value = [
  680. { name: "OA", value: oaTasks.value.doneCount ?? 0 },
  681. { name: "CRM", value: crmDoneRes.todoCount ?? 0 },
  682. // { name: "SRM", value: srmDoneRes.doneCount ?? 0 },
  683. ];
  684. },
  685. 10 * 60 * 1000,
  686. ); // 每5分钟刷新一次
  687. } else {
  688. statsLoading.value = false;
  689. }
  690. });
  691. // 组件卸载时移除监听,防止内存泄漏
  692. onBeforeUnmount(() => {
  693. if (tabsContainerRef.value) {
  694. tabsContainerRef.value.removeEventListener("scroll", handleTabScroll);
  695. }
  696. });
  697. </script>
  698. <style scoped>
  699. .ehr-page {
  700. --portal-text: #17345f;
  701. --portal-text-muted: #5f6f83;
  702. --portal-text-soft: #7f8fa6;
  703. --portal-title: #163867;
  704. --portal-subtitle: rgba(61, 92, 135, 0.86);
  705. --portal-line: rgba(126, 156, 201, 0.24);
  706. --portal-card: rgba(255, 255, 255, 0.82);
  707. --portal-card-2: rgba(248, 251, 255, 0.94);
  708. --portal-card-3: rgba(240, 246, 255, 0.88);
  709. --portal-card-4: rgba(231, 239, 251, 0.92);
  710. --portal-nav-bg: rgba(255, 255, 255, 0.72);
  711. --portal-nav-hover: rgba(219, 232, 252, 0.8);
  712. --portal-input-bg: rgba(255, 255, 255, 0.7);
  713. --portal-input-hover: rgba(255, 255, 255, 0.92);
  714. --portal-shadow: 0 18px 40px rgba(23, 52, 95, 0.12);
  715. --portal-shadow-strong: 0 24px 60px rgba(23, 52, 95, 0.16);
  716. --portal-accent: #245edb;
  717. --portal-accent-2: #4e8cff;
  718. --portal-accent-soft: rgba(36, 94, 219, 0.14);
  719. --portal-todo-bg: rgba(242, 247, 255, 0.94);
  720. --portal-todo-hover: rgba(228, 238, 252, 0.95);
  721. --portal-number-todo: #e15a5a;
  722. --portal-number-done: #2da04d;
  723. color: var(--portal-text);
  724. background:
  725. radial-gradient(
  726. circle at 18% 12%,
  727. rgba(83, 126, 255, 0.14),
  728. transparent 22%
  729. ),
  730. radial-gradient(
  731. circle at 82% 20%,
  732. rgba(71, 148, 255, 0.14),
  733. transparent 20%
  734. ),
  735. radial-gradient(
  736. circle at 50% 100%,
  737. rgba(97, 142, 247, 0.12),
  738. transparent 28%
  739. ),
  740. linear-gradient(180deg, #eef3f9 0%, #f7faff 46%, #eef3f9 100%);
  741. }
  742. :global([data-theme="dark"] .ehr-page) {
  743. --portal-text: #eaf1ff;
  744. --portal-text-muted: rgba(234, 241, 255, 0.95);
  745. --portal-text-soft: #8a9ab0;
  746. --portal-title: #f4f7ff;
  747. --portal-subtitle: rgba(188, 205, 255, 0.82);
  748. --portal-line: rgba(97, 129, 206, 0.28);
  749. --portal-card: rgba(10, 19, 43, 0.8);
  750. --portal-card-2: rgba(12, 24, 52, 0.92);
  751. --portal-card-3: rgba(17, 25, 48, 0.8);
  752. --portal-card-4: rgba(15, 24, 45, 0.82);
  753. --portal-nav-bg: rgba(10, 19, 43, 0.8);
  754. --portal-nav-hover: rgba(28, 40, 72, 0.8);
  755. --portal-input-bg: rgba(255, 255, 255, 0.08);
  756. --portal-input-hover: rgba(255, 255, 255, 0.12);
  757. --portal-shadow: 0 16px 34px rgba(0, 0, 0, 0.22);
  758. --portal-shadow-strong: 0 24px 60px rgba(0, 0, 0, 0.38);
  759. --portal-accent: #6e7dff;
  760. --portal-accent-2: #8d4dff;
  761. --portal-accent-soft: rgba(110, 125, 255, 0.16);
  762. --portal-todo-bg: #070e20;
  763. --portal-todo-hover: rgba(28, 40, 72, 0.8);
  764. --portal-number-todo: #f56c6c;
  765. --portal-number-done: #ffffff;
  766. color: var(--portal-text);
  767. background:
  768. radial-gradient(
  769. circle at 18% 12%,
  770. rgba(79, 82, 221, 0.34),
  771. transparent 22%
  772. ),
  773. radial-gradient(circle at 82% 20%, rgba(28, 95, 255, 0.2), transparent 20%),
  774. radial-gradient(
  775. circle at 50% 100%,
  776. rgba(103, 46, 255, 0.16),
  777. transparent 28%
  778. ),
  779. linear-gradient(180deg, #040814 0%, #060d1d 46%, #040814 100%);
  780. }
  781. .banner {
  782. position: relative;
  783. margin-top: 20px; /* 使用 margin 代替 top 以避免脱离文档流导致的重叠 */
  784. height: auto; /* 允许高度自适应,或者设置一个最小高度 */
  785. min-height: 300px;
  786. /* 设置背景图 */
  787. background: url("../../assets//images/flwoBanner.png"); /* 或者使用变量 if defined in script */
  788. background-size: cover;
  789. background-repeat: no-repeat;
  790. background-position: center;
  791. border: 2px solid #061338;
  792. }
  793. .hero {
  794. position: relative;
  795. padding: 20px 5vw 48px;
  796. overflow: hidden;
  797. display: flex;
  798. justify-content: space-between;
  799. align-items: center;
  800. }
  801. .hero-inner {
  802. max-width: 920px;
  803. }
  804. .hero-title {
  805. font-size: clamp(18px, 2vw, 22px);
  806. line-height: 1.2;
  807. margin: 16px 0 12px;
  808. color: #fff;
  809. font-weight: bold;
  810. }
  811. .hero-desc {
  812. font-size: 16px;
  813. color: #a7a0b1;
  814. max-width: 720px;
  815. line-height: 1.8;
  816. }
  817. .hero-accent {
  818. position: absolute;
  819. top: -120px;
  820. right: -140px;
  821. width: 360px;
  822. height: 360px;
  823. border-radius: 50%;
  824. opacity: 0.9;
  825. pointer-events: none;
  826. }
  827. .content {
  828. padding: 0 5vw 80px;
  829. /* height: 80vh; */
  830. }
  831. .search-bar {
  832. display: flex;
  833. align-items: center;
  834. justify-content: space-between;
  835. gap: 16px;
  836. margin-bottom: 18px;
  837. }
  838. .search-input {
  839. display: flex;
  840. align-items: center;
  841. gap: 10px;
  842. width: min(420px, 100%);
  843. padding: 0 16px;
  844. height: 36px;
  845. border-radius: 20px;
  846. background: transparent;
  847. border: 1px solid #0a1c43;
  848. box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
  849. transition:
  850. border-color 0.2s ease,
  851. box-shadow 0.2s ease;
  852. }
  853. .search-input:focus-within {
  854. border-color: rgba(2, 64, 155, 0.28);
  855. box-shadow: 0 12px 28px rgba(2, 64, 155, 0.1);
  856. }
  857. .search-icon {
  858. flex-shrink: 0;
  859. font-size: 18px;
  860. color: #94a3b8;
  861. }
  862. .search-field {
  863. width: 100%;
  864. border: none;
  865. background: transparent;
  866. outline: none;
  867. font-size: 14px;
  868. color: #0f172a;
  869. }
  870. .search-field::placeholder {
  871. color: #94a3b8;
  872. }
  873. .search-meta {
  874. flex-shrink: 0;
  875. font-size: 13px;
  876. color: #64748b;
  877. }
  878. .el-tab-item {
  879. position: relative;
  880. display: inline-flex;
  881. align-items: center;
  882. justify-content: center;
  883. padding: 0 20px;
  884. height: 40px; /* 标准高度 */
  885. font-size: 14px;
  886. color: var(--text-tertiary); /* Element Plus 主要文字颜色 */
  887. background-color: transparent;
  888. border: none;
  889. border-bottom: 1.5px solid transparent; /* 用于激活态的下划线 */
  890. cursor: pointer;
  891. transition: all 0.3s;
  892. margin-right: 0;
  893. outline: none;
  894. flex-shrink: 0; /* 防止压缩 */
  895. }
  896. .el-tab-item.is-active {
  897. color: #fff !important;
  898. /* background: linear-gradient(
  899. 180deg,
  900. rgba(92, 103, 238, 0.32),
  901. rgba(36, 53, 118, 0.3)
  902. ) !important; */
  903. background: linear-gradient(
  904. 360deg,
  905. #003be0 0%,
  906. rgba(10, 65, 227, 0.6) 10%,
  907. #001c71 40%,
  908. #000c33 80%
  909. ) !important;
  910. padding-bottom: 22px;
  911. padding-top: 22px;
  912. border-radius: 0;
  913. position: relative;
  914. }
  915. .el-tab-item.is-active::before {
  916. content: "";
  917. position: absolute;
  918. left: 50%;
  919. bottom: -2px;
  920. width: 100%;
  921. height: 2px;
  922. transform: translateX(-50%);
  923. border-radius: 999px;
  924. background: linear-gradient(
  925. to right,
  926. #5887f8 0%,
  927. #69b5f8 30%,
  928. #fff 50%,
  929. #69b5f8 70%,
  930. #5887f8 100%
  931. );
  932. box-shadow: 0 0 12px rgba(112, 120, 255, 0.95);
  933. }
  934. .el-tab-item.is-active {
  935. color: #02409b; /* 激活态文字颜色 */
  936. font-weight: 500;
  937. border-bottom-color: #02409b; /* 激活态下划线 */
  938. }
  939. .tab-label {
  940. line-height: 1;
  941. }
  942. .tab-sub {
  943. margin-left: 8px;
  944. font-size: 12px;
  945. color: #909399; /* 次要文字颜色 */
  946. transform: scale(0.9);
  947. }
  948. .panel {
  949. background: #ffffff;
  950. border-radius: 24px;
  951. padding: 28px 28px 32px;
  952. box-shadow: 0 26px 48px rgba(15, 23, 42, 0.08);
  953. }
  954. .panel-head {
  955. display: flex;
  956. align-items: flex-start;
  957. justify-content: space-between;
  958. gap: 24px;
  959. padding-bottom: 20px;
  960. margin-bottom: 24px;
  961. }
  962. .panel-title {
  963. font-size: 22px;
  964. color: #111827;
  965. margin-bottom: 6px;
  966. }
  967. .panel-subtitle {
  968. color: #6b7280;
  969. font-size: 14px;
  970. }
  971. .panel-meta {
  972. text-align: right;
  973. color: #6b7280;
  974. font-size: 12px;
  975. }
  976. .panel-count {
  977. display: block;
  978. font-size: 20px;
  979. font-weight: 600;
  980. color: #111827;
  981. }
  982. .items-grid {
  983. display: grid;
  984. grid-template-columns: repeat(auto-fill, 240px);
  985. justify-content: flex-start;
  986. gap: 16px;
  987. }
  988. .item-card {
  989. position: relative;
  990. --item-accent: #2563eb;
  991. --item-accent-soft: rgba(37, 99, 235, 0.12);
  992. --item-accent-hover: rgba(37, 99, 235, 0.18);
  993. --item-border-color: #262360; /* 默认值,防止变量未传入时出错 */
  994. display: flex;
  995. align-items: center;
  996. gap: 18px;
  997. padding: 18px 22px 18px;
  998. border-radius: 10px;
  999. background: var(--portal-card);
  1000. /* 修改这里:使用变量作为边框颜色 */
  1001. /* border: 1px solid var(--item-border-color); */
  1002. box-shadow:
  1003. rgba(0, 0, 0, 0) 0px 0px 0px 0px,
  1004. rgba(0, 0, 0, 0) 0px 0px 0px 0px,
  1005. rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
  1006. transition:
  1007. transform 0.2s ease,
  1008. border-color 0.2s ease,
  1009. box-shadow 0.2s ease,
  1010. background-color 0.2s ease;
  1011. cursor: pointer;
  1012. }
  1013. .item-type-badge {
  1014. position: absolute;
  1015. top: 12px; /* 距离顶部的距离 */
  1016. right: 12px; /* 距离右侧的距离 */
  1017. z-index: 10; /* 确保在最上层 */
  1018. }
  1019. .item-type-badge span {
  1020. font-size: 10px;
  1021. color: #869ac2; /* 原有颜色 */
  1022. text-transform: uppercase;
  1023. letter-spacing: 0.05em;
  1024. transition: color 0.2s ease;
  1025. }
  1026. .item-card:hover {
  1027. transform: translateY(-3px);
  1028. box-shadow: 0 18px 36px rgba(15, 23, 42, 0.08);
  1029. color: #02409b !important;
  1030. }
  1031. .item-top {
  1032. display: flex;
  1033. align-items: flex-start;
  1034. justify-content: space-between;
  1035. }
  1036. .item-body {
  1037. display: flex;
  1038. flex-direction: column;
  1039. gap: 8px;
  1040. }
  1041. .item-icon {
  1042. width: 42px;
  1043. height: 42px;
  1044. border-radius: 12px;
  1045. display: grid;
  1046. place-items: center;
  1047. font-size: 20px;
  1048. /* --- 新增:毛玻璃渐变效果 --- */
  1049. background: linear-gradient(
  1050. 135deg,
  1051. rgba(255, 255, 255, 0.1) 0%,
  1052. rgba(255, 255, 255, 0.02) 100%
  1053. );
  1054. /* 2. 边框:增加一个极细的半透明白色边框,增强玻璃边缘感 */
  1055. border: 1px solid rgba(255, 255, 255, 0.1);
  1056. /* 3. 毛玻璃核心属性:模糊背景元素 */
  1057. backdrop-filter: blur(10px);
  1058. -webkit-backdrop-filter: blur(10px);
  1059. /* 4. 阴影:增加一点内阴影和外阴影,提升立体感 */
  1060. box-shadow:
  1061. inset 0 0 0 1px rgba(255, 255, 255, 0.1),
  1062. 0 0 12px var(--theme-soft); /* 这里用主题色做外发光 */
  1063. /* 原有过渡效果 */
  1064. transition:
  1065. transform 0.2s ease,
  1066. background-color 0.2s ease,
  1067. box-shadow 0.2s ease,
  1068. border-color 0.2s ease;
  1069. }
  1070. /* 保持原有的 Hover 效果,但需要适配新的玻璃风格 */
  1071. .item-card:hover .item-icon {
  1072. /* Hover 时:背景变为实心主题色,或者更亮的玻璃效果 */
  1073. background: var(--theme-color);
  1074. border-color: transparent;
  1075. transform: translateY(-2px);
  1076. box-shadow: 0 10px 20px -12px var(--theme-color);
  1077. }
  1078. .item-card:hover .item-icon :deep(svg) {
  1079. color: #ffffff !important;
  1080. }
  1081. .item-role {
  1082. font-size: 12px;
  1083. letter-spacing: 0.2em;
  1084. color: #c7ced9;
  1085. font-weight: 600;
  1086. }
  1087. .item-name {
  1088. font-size: 14px;
  1089. line-height: 1.45;
  1090. /* font-weight: 600; */
  1091. }
  1092. .item-desc {
  1093. font-size: 13px;
  1094. color: #6b7280;
  1095. line-height: 1.6;
  1096. }
  1097. .empty-state {
  1098. display: flex;
  1099. flex-direction: column;
  1100. align-items: center;
  1101. justify-content: center;
  1102. gap: 10px;
  1103. min-height: 240px;
  1104. border-radius: 22px;
  1105. background: rgba(255, 255, 255, 0.72);
  1106. border: 1px dashed #dbe3ee;
  1107. color: #64748b;
  1108. }
  1109. .empty-icon {
  1110. font-size: 42px;
  1111. color: #94a3b8;
  1112. }
  1113. .empty-title {
  1114. font-size: 16px;
  1115. font-weight: 600;
  1116. color: #334155;
  1117. }
  1118. .empty-desc {
  1119. font-size: 13px;
  1120. color: #94a3b8;
  1121. }
  1122. .item-footer {
  1123. display: flex;
  1124. align-items: center;
  1125. justify-content: space-between;
  1126. color: #94a3b8;
  1127. font-size: 12px;
  1128. }
  1129. .item-usage {
  1130. display: inline-flex;
  1131. align-items: center;
  1132. gap: 8px;
  1133. }
  1134. .item-flame {
  1135. width: 18px;
  1136. height: 18px;
  1137. border-radius: 50%;
  1138. display: grid;
  1139. place-items: center;
  1140. background: rgba(255, 108, 0, 0.12);
  1141. color: #ff6c00;
  1142. font-size: 12px;
  1143. }
  1144. .item-arrow {
  1145. font-size: 22px;
  1146. color: #cbd5e1;
  1147. }
  1148. @media (max-width: 720px) {
  1149. .hero {
  1150. padding: 56px 7vw 36px;
  1151. }
  1152. .search-bar {
  1153. flex-direction: column;
  1154. align-items: stretch;
  1155. }
  1156. .search-input {
  1157. width: 100%;
  1158. }
  1159. .panel-head {
  1160. flex-direction: column;
  1161. align-items: flex-start;
  1162. }
  1163. .panel-meta {
  1164. text-align: left;
  1165. }
  1166. .items-grid {
  1167. grid-template-columns: 1fr;
  1168. }
  1169. }
  1170. .total {
  1171. display: flex;
  1172. gap: 16px;
  1173. padding: 0 6vw 50px;
  1174. /* margin-bottom: 24px; */
  1175. }
  1176. .total-card {
  1177. position: relative; /* 必须设置为 relative,作为伪元素的定位基准 */
  1178. width: 300px;
  1179. /* 内部背景保持深色半透明 */
  1180. background: rgba(5, 15, 46, 0.85);
  1181. border-radius: 16px;
  1182. padding: 20px;
  1183. backdrop-filter: blur(8px);
  1184. -webkit-backdrop-filter: blur(8px);
  1185. /* 移除原有的普通边框和阴影,由伪元素接管视觉效果 */
  1186. border: none;
  1187. box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); /* 增加一点投影增强立体感 */
  1188. transition: transform 0.3s ease;
  1189. cursor: pointer;
  1190. overflow: hidden; /* 关键:隐藏伪元素溢出的部分 */
  1191. z-index: 1; /* 确保内容在发光边框之上 */
  1192. }
  1193. /* 霓虹发光边框层 */
  1194. .total-card::before {
  1195. content: "";
  1196. position: absolute;
  1197. top: -50%;
  1198. left: -50%;
  1199. width: 200%;
  1200. height: 200%;
  1201. /* 彩色圆锥渐变:包含蓝、紫、粉、青等霓虹色 */
  1202. background: conic-gradient(
  1203. transparent,
  1204. #02409b,
  1205. #00d2ff,
  1206. #7c3aed,
  1207. #db2777,
  1208. transparent 30%
  1209. );
  1210. animation: rotate-border 4s linear infinite; /* 旋转动画 */
  1211. z-index: -2;
  1212. }
  1213. /* 内部遮罩层:用于覆盖中间区域,只露出边缘形成边框 */
  1214. .total-card::after {
  1215. content: "";
  1216. position: absolute;
  1217. inset: 1px; /* 这里控制边框宽度,2px 即边框宽 */
  1218. background: rgba(
  1219. 5,
  1220. 15,
  1221. 46,
  1222. 0.92
  1223. ); /* 与卡片背景一致,稍微不透明一点以遮盖旋转背景 */
  1224. border-radius: 14px; /* 比父容器小一点 */
  1225. z-index: -1;
  1226. }
  1227. /* 悬停时增强发光效果 */
  1228. .total-card:hover {
  1229. transform: translateY(-4px);
  1230. box-shadow: 0 0 20px rgba(2, 64, 155, 0.4); /* 整体外发光 */
  1231. }
  1232. /* 悬停时加速旋转或改变亮度(可选) */
  1233. .total-card:hover::before {
  1234. filter: brightness(1.2); /* 悬停时更亮 */
  1235. }
  1236. /* 定义旋转动画 */
  1237. @keyframes rotate-border {
  1238. 0% {
  1239. transform: rotate(0deg);
  1240. }
  1241. 100% {
  1242. transform: rotate(360deg);
  1243. }
  1244. }
  1245. /* 保持原有内部文字样式不变,但确保它们在最上层 */
  1246. .card-icon {
  1247. width: 40px;
  1248. height: 40px;
  1249. border-radius: 12px;
  1250. background: #f9f9f9;
  1251. display: flex;
  1252. align-items: center;
  1253. justify-content: center;
  1254. margin-bottom: 12px;
  1255. position: relative; /* 确保图标层级 */
  1256. z-index: 2;
  1257. }
  1258. .card-icon svg {
  1259. width: 20px;
  1260. height: 20px;
  1261. color: var(--primary-color);
  1262. }
  1263. .card-content {
  1264. flex: 1;
  1265. text-align: left;
  1266. }
  1267. .card-title {
  1268. font-size: 14px;
  1269. color: #a7a0b1; /* 调整为浅色以适配深色背景 */
  1270. margin-bottom: 4px;
  1271. }
  1272. .card-number {
  1273. font-size: 28px;
  1274. font-weight: 600;
  1275. color: #ffffff; /* 数字改为白色 */
  1276. }
  1277. .card-extra {
  1278. font-size: 12px;
  1279. color: #10b981;
  1280. margin-top: 8px;
  1281. text-align: right;
  1282. }
  1283. .charts-container {
  1284. display: flex;
  1285. flex-wrap: wrap;
  1286. gap: 24px;
  1287. padding: 0 6vw;
  1288. margin-bottom: 80px;
  1289. width: 100%;
  1290. box-sizing: border-box;
  1291. }
  1292. .chart-item {
  1293. flex: 1;
  1294. /* 桌面端最小宽度,防止过度挤压 */
  1295. min-width: 300px;
  1296. /* 必须设置固定高度,ECharts 需要明确的高度才能渲染 */
  1297. height: 350px;
  1298. border-radius: 16px;
  1299. background-color: #ffffff;
  1300. box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
  1301. overflow: hidden;
  1302. position: relative;
  1303. }
  1304. :global(.glass-popover) {
  1305. background: rgba(0, 0, 0, 0.6) !important;
  1306. backdrop-filter: blur(10px);
  1307. -webkit-backdrop-filter: blur(10px);
  1308. height: 200px !important;
  1309. width: 100px !important;
  1310. overflow-y: hidden !important;
  1311. box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1) !important;
  1312. border-radius: 8px !important;
  1313. padding: 10px !important;
  1314. color: #fff !important;
  1315. z-index: 2000 !important; /* 确保在最上层 */
  1316. top: 15% !important; /* 根据需要调整位置 */
  1317. }
  1318. :global(.glass-popover .el-popper__arrow::before) {
  1319. background: transparent !important;
  1320. /* border: 1px solid rgba(255, 255, 255, 0.1); */
  1321. border: none !important;
  1322. }
  1323. .detail-list {
  1324. padding: 5px 0;
  1325. }
  1326. .detail-item {
  1327. display: flex;
  1328. justify-content: space-between;
  1329. align-items: center;
  1330. margin-bottom: 8px;
  1331. font-size: 14px;
  1332. color: #e0e0e0; /* 浅色文字 */
  1333. line-height: 1.5;
  1334. padding: 6px 8px;
  1335. border-radius: 4px;
  1336. cursor: pointer; /* 鼠标变成手型 */
  1337. transition: background-color 0.2s ease;
  1338. }
  1339. .detail-item:last-child {
  1340. margin-bottom: 0;
  1341. }
  1342. /* 鼠标悬浮背景变灰 */
  1343. .detail-item:hover {
  1344. background-color: rgba(
  1345. 255,
  1346. 255,
  1347. 255,
  1348. 0.15
  1349. ); /* 半透明白色,视觉上为灰色高亮 */
  1350. }
  1351. .detail-name {
  1352. color: #ccc;
  1353. }
  1354. .detail-val {
  1355. font-weight: bold;
  1356. color: #fff;
  1357. }
  1358. .empty-tip {
  1359. text-align: center;
  1360. color: #aaa;
  1361. font-size: 12px;
  1362. padding: 10px 0;
  1363. }
  1364. .item-arrow {
  1365. transition: all 0.3s ease;
  1366. color: #cbd5e1; /* 默认颜色 */
  1367. }
  1368. /* 2. 当卡片悬浮时,改变箭头的样式 */
  1369. .item-card:hover .item-arrow {
  1370. color: #02409b; /* 变蓝 (使用你主题中的蓝色) */
  1371. transform: translateX(4px); /* 向右平移 4px */
  1372. }
  1373. .tabs-wrapper {
  1374. position: relative;
  1375. width: 100%;
  1376. display: flex;
  1377. align-items: center;
  1378. margin-bottom: 20px;
  1379. }
  1380. /* 修改:原有的 .tabs-container 去掉 margin-bottom,因为现在由 wrapper 控制间距 */
  1381. .tabs-container {
  1382. display: flex;
  1383. align-items: center;
  1384. border-bottom: 1px solid #192754;
  1385. padding-left: 0;
  1386. overflow-x: auto;
  1387. scroll-behavior: smooth;
  1388. scrollbar-width: none; /* Firefox */
  1389. -ms-overflow-style: none; /* IE 10+ */
  1390. flex: 1; /* 占据中间剩余空间 */
  1391. mask-image: linear-gradient(
  1392. to right,
  1393. transparent,
  1394. black 20px,
  1395. black 98%,
  1396. transparent
  1397. ); /* 可选:添加两侧渐变遮罩效果 */
  1398. -webkit-mask-image: linear-gradient(
  1399. to right,
  1400. transparent,
  1401. black 20px,
  1402. black 98%,
  1403. transparent
  1404. );
  1405. }
  1406. .tabs-container::-webkit-scrollbar {
  1407. display: none;
  1408. }
  1409. /* 新增:箭头按钮样式 */
  1410. .scroll-arrow {
  1411. position: absolute;
  1412. top: 50%;
  1413. transform: translateY(-50%);
  1414. z-index: 10;
  1415. width: 30px;
  1416. height: 30px;
  1417. border-radius: 50%;
  1418. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  1419. display: flex;
  1420. align-items: center;
  1421. justify-content: center;
  1422. cursor: pointer;
  1423. color: #64748b;
  1424. transition: all 0.2s ease;
  1425. opacity: 0; /* 默认隐藏,鼠标悬停容器或需要时显示,或者始终显示但禁用时变灰 */
  1426. visibility: hidden;
  1427. }
  1428. /* 当容器被悬停时显示箭头,或者你可以去掉这个限制让它始终显示 */
  1429. .tabs-wrapper:hover .scroll-arrow {
  1430. opacity: 1;
  1431. visibility: visible;
  1432. }
  1433. .scroll-arrow.left {
  1434. left: 0;
  1435. }
  1436. .scroll-arrow.right {
  1437. right: 0;
  1438. }
  1439. .scroll-arrow:hover:not(:disabled) {
  1440. color: #02409b;
  1441. }
  1442. .scroll-arrow:disabled {
  1443. /* background-color: #f8fafc; */
  1444. color: #cbd5e1;
  1445. cursor: not-allowed;
  1446. box-shadow: none;
  1447. border-color: #f1f5f9;
  1448. opacity: 0.6;
  1449. visibility: visible; /* 禁用时也要可见以展示状态 */
  1450. }
  1451. /* 确保 Tab 项不会被箭头遮挡太多,可以在容器两侧加一点 padding */
  1452. .tabs-container {
  1453. /* 在原有样式基础上增加左右 padding,防止第一个和最后一个 Tab 被箭头完全遮住 */
  1454. padding: 0 40px;
  1455. }
  1456. </style>