index.vue 40 KB

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