index.vue 43 KB

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