index.vue 40 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691
  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 #0a1c43;
  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. --item-border-color: #262360; /* 默认值,防止变量未传入时出错 */
  1029. display: flex;
  1030. align-items: center;
  1031. gap: 18px;
  1032. padding: 18px 22px 18px;
  1033. border-radius: 10px;
  1034. background: rgb(11, 15, 46);
  1035. /* 修改这里:使用变量作为边框颜色 */
  1036. border: 1px solid var(--item-border-color);
  1037. box-shadow:
  1038. rgba(0, 0, 0, 0) 0px 0px 0px 0px,
  1039. rgba(0, 0, 0, 0) 0px 0px 0px 0px,
  1040. rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
  1041. transition:
  1042. transform 0.2s ease,
  1043. border-color 0.2s ease,
  1044. box-shadow 0.2s ease,
  1045. background-color 0.2s ease;
  1046. cursor: pointer;
  1047. }
  1048. .item-type-badge {
  1049. position: absolute;
  1050. top: 12px; /* 距离顶部的距离 */
  1051. right: 12px; /* 距离右侧的距离 */
  1052. z-index: 10; /* 确保在最上层 */
  1053. }
  1054. .item-type-badge span {
  1055. font-size: 10px;
  1056. color: #869ac2; /* 原有颜色 */
  1057. text-transform: uppercase;
  1058. letter-spacing: 0.05em;
  1059. transition: color 0.2s ease;
  1060. }
  1061. .item-card:hover {
  1062. transform: translateY(-3px);
  1063. box-shadow: 0 18px 36px rgba(15, 23, 42, 0.08);
  1064. color: #02409b !important;
  1065. }
  1066. .item-top {
  1067. display: flex;
  1068. align-items: flex-start;
  1069. justify-content: space-between;
  1070. }
  1071. .item-body {
  1072. display: flex;
  1073. flex-direction: column;
  1074. gap: 8px;
  1075. }
  1076. .item-icon {
  1077. width: 42px;
  1078. height: 42px;
  1079. border-radius: 12px;
  1080. display: grid;
  1081. place-items: center;
  1082. font-size: 20px;
  1083. /* --- 新增:毛玻璃渐变效果 --- */
  1084. background: linear-gradient(
  1085. 135deg,
  1086. rgba(255, 255, 255, 0.1) 0%,
  1087. rgba(255, 255, 255, 0.02) 100%
  1088. );
  1089. /* 2. 边框:增加一个极细的半透明白色边框,增强玻璃边缘感 */
  1090. border: 1px solid rgba(255, 255, 255, 0.1);
  1091. /* 3. 毛玻璃核心属性:模糊背景元素 */
  1092. backdrop-filter: blur(10px);
  1093. -webkit-backdrop-filter: blur(10px);
  1094. /* 4. 阴影:增加一点内阴影和外阴影,提升立体感 */
  1095. box-shadow:
  1096. inset 0 0 0 1px rgba(255, 255, 255, 0.1),
  1097. 0 0 12px var(--theme-soft); /* 这里用主题色做外发光 */
  1098. /* 原有过渡效果 */
  1099. transition:
  1100. transform 0.2s ease,
  1101. background-color 0.2s ease,
  1102. box-shadow 0.2s ease,
  1103. border-color 0.2s ease;
  1104. }
  1105. /* 保持原有的 Hover 效果,但需要适配新的玻璃风格 */
  1106. .item-card:hover .item-icon {
  1107. /* Hover 时:背景变为实心主题色,或者更亮的玻璃效果 */
  1108. background: var(--theme-color);
  1109. border-color: transparent;
  1110. transform: translateY(-2px);
  1111. box-shadow: 0 10px 20px -12px var(--theme-color);
  1112. }
  1113. .item-card:hover .item-icon :deep(svg) {
  1114. color: #ffffff !important;
  1115. }
  1116. .item-role {
  1117. font-size: 12px;
  1118. letter-spacing: 0.2em;
  1119. color: #c7ced9;
  1120. font-weight: 600;
  1121. }
  1122. .item-name {
  1123. font-size: 14px;
  1124. line-height: 1.45;
  1125. /* font-weight: 600; */
  1126. }
  1127. .item-desc {
  1128. font-size: 13px;
  1129. color: #6b7280;
  1130. line-height: 1.6;
  1131. }
  1132. .empty-state {
  1133. display: flex;
  1134. flex-direction: column;
  1135. align-items: center;
  1136. justify-content: center;
  1137. gap: 10px;
  1138. min-height: 240px;
  1139. border-radius: 22px;
  1140. background: rgba(255, 255, 255, 0.72);
  1141. border: 1px dashed #dbe3ee;
  1142. color: #64748b;
  1143. }
  1144. .empty-icon {
  1145. font-size: 42px;
  1146. color: #94a3b8;
  1147. }
  1148. .empty-title {
  1149. font-size: 16px;
  1150. font-weight: 600;
  1151. color: #334155;
  1152. }
  1153. .empty-desc {
  1154. font-size: 13px;
  1155. color: #94a3b8;
  1156. }
  1157. .item-footer {
  1158. display: flex;
  1159. align-items: center;
  1160. justify-content: space-between;
  1161. color: #94a3b8;
  1162. font-size: 12px;
  1163. }
  1164. .item-usage {
  1165. display: inline-flex;
  1166. align-items: center;
  1167. gap: 8px;
  1168. }
  1169. .item-flame {
  1170. width: 18px;
  1171. height: 18px;
  1172. border-radius: 50%;
  1173. display: grid;
  1174. place-items: center;
  1175. background: rgba(255, 108, 0, 0.12);
  1176. color: #ff6c00;
  1177. font-size: 12px;
  1178. }
  1179. .item-arrow {
  1180. font-size: 22px;
  1181. color: #cbd5e1;
  1182. }
  1183. @media (max-width: 720px) {
  1184. .hero {
  1185. padding: 56px 7vw 36px;
  1186. }
  1187. .search-bar {
  1188. flex-direction: column;
  1189. align-items: stretch;
  1190. }
  1191. .search-input {
  1192. width: 100%;
  1193. }
  1194. .panel-head {
  1195. flex-direction: column;
  1196. align-items: flex-start;
  1197. }
  1198. .panel-meta {
  1199. text-align: left;
  1200. }
  1201. .items-grid {
  1202. grid-template-columns: 1fr;
  1203. }
  1204. }
  1205. .total {
  1206. display: flex;
  1207. gap: 16px;
  1208. padding: 0 6vw 50px;
  1209. /* margin-bottom: 24px; */
  1210. }
  1211. .total-card {
  1212. position: relative; /* 必须设置为 relative,作为伪元素的定位基准 */
  1213. width: 300px;
  1214. /* 内部背景保持深色半透明 */
  1215. background: rgba(5, 15, 46, 0.85);
  1216. border-radius: 16px;
  1217. padding: 20px;
  1218. backdrop-filter: blur(8px);
  1219. -webkit-backdrop-filter: blur(8px);
  1220. /* 移除原有的普通边框和阴影,由伪元素接管视觉效果 */
  1221. border: none;
  1222. box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); /* 增加一点投影增强立体感 */
  1223. transition: transform 0.3s ease;
  1224. cursor: pointer;
  1225. overflow: hidden; /* 关键:隐藏伪元素溢出的部分 */
  1226. z-index: 1; /* 确保内容在发光边框之上 */
  1227. }
  1228. /* 霓虹发光边框层 */
  1229. .total-card::before {
  1230. content: "";
  1231. position: absolute;
  1232. top: -50%;
  1233. left: -50%;
  1234. width: 200%;
  1235. height: 200%;
  1236. /* 彩色圆锥渐变:包含蓝、紫、粉、青等霓虹色 */
  1237. background: conic-gradient(
  1238. transparent,
  1239. #02409b,
  1240. #00d2ff,
  1241. #7c3aed,
  1242. #db2777,
  1243. transparent 30%
  1244. );
  1245. animation: rotate-border 4s linear infinite; /* 旋转动画 */
  1246. z-index: -2;
  1247. }
  1248. /* 内部遮罩层:用于覆盖中间区域,只露出边缘形成边框 */
  1249. .total-card::after {
  1250. content: "";
  1251. position: absolute;
  1252. inset: 1px; /* 这里控制边框宽度,2px 即边框宽 */
  1253. background: rgba(
  1254. 5,
  1255. 15,
  1256. 46,
  1257. 0.92
  1258. ); /* 与卡片背景一致,稍微不透明一点以遮盖旋转背景 */
  1259. border-radius: 14px; /* 比父容器小一点 */
  1260. z-index: -1;
  1261. }
  1262. /* 悬停时增强发光效果 */
  1263. .total-card:hover {
  1264. transform: translateY(-4px);
  1265. box-shadow: 0 0 20px rgba(2, 64, 155, 0.4); /* 整体外发光 */
  1266. }
  1267. /* 悬停时加速旋转或改变亮度(可选) */
  1268. .total-card:hover::before {
  1269. filter: brightness(1.2); /* 悬停时更亮 */
  1270. }
  1271. /* 定义旋转动画 */
  1272. @keyframes rotate-border {
  1273. 0% {
  1274. transform: rotate(0deg);
  1275. }
  1276. 100% {
  1277. transform: rotate(360deg);
  1278. }
  1279. }
  1280. /* 保持原有内部文字样式不变,但确保它们在最上层 */
  1281. .card-icon {
  1282. width: 40px;
  1283. height: 40px;
  1284. border-radius: 12px;
  1285. background: #f9f9f9;
  1286. display: flex;
  1287. align-items: center;
  1288. justify-content: center;
  1289. margin-bottom: 12px;
  1290. position: relative; /* 确保图标层级 */
  1291. z-index: 2;
  1292. }
  1293. .card-icon svg {
  1294. width: 20px;
  1295. height: 20px;
  1296. color: var(--primary-color);
  1297. }
  1298. .card-content {
  1299. flex: 1;
  1300. text-align: left;
  1301. }
  1302. .card-title {
  1303. font-size: 14px;
  1304. color: #a7a0b1; /* 调整为浅色以适配深色背景 */
  1305. margin-bottom: 4px;
  1306. }
  1307. .card-number {
  1308. font-size: 28px;
  1309. font-weight: 600;
  1310. color: #ffffff; /* 数字改为白色 */
  1311. }
  1312. .card-extra {
  1313. font-size: 12px;
  1314. color: #10b981;
  1315. margin-top: 8px;
  1316. text-align: right;
  1317. }
  1318. .charts-container {
  1319. display: flex;
  1320. flex-wrap: wrap;
  1321. gap: 24px;
  1322. padding: 0 6vw;
  1323. margin-bottom: 80px;
  1324. width: 100%;
  1325. box-sizing: border-box;
  1326. }
  1327. .chart-item {
  1328. flex: 1;
  1329. /* 桌面端最小宽度,防止过度挤压 */
  1330. min-width: 300px;
  1331. /* 必须设置固定高度,ECharts 需要明确的高度才能渲染 */
  1332. height: 350px;
  1333. border-radius: 16px;
  1334. background-color: #ffffff;
  1335. box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
  1336. overflow: hidden;
  1337. position: relative;
  1338. }
  1339. :global(.glass-popover) {
  1340. background: rgba(0, 0, 0, 0.6) !important;
  1341. backdrop-filter: blur(10px);
  1342. -webkit-backdrop-filter: blur(10px);
  1343. height: 200px !important;
  1344. width: 100px !important;
  1345. overflow-y: hidden !important;
  1346. box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1) !important;
  1347. border-radius: 8px !important;
  1348. padding: 10px !important;
  1349. color: #fff !important;
  1350. z-index: 2000 !important; /* 确保在最上层 */
  1351. top: 15% !important; /* 根据需要调整位置 */
  1352. }
  1353. :global(.glass-popover .el-popper__arrow::before) {
  1354. background: transparent !important;
  1355. /* border: 1px solid rgba(255, 255, 255, 0.1); */
  1356. border: none !important;
  1357. }
  1358. .detail-list {
  1359. padding: 5px 0;
  1360. }
  1361. .detail-item {
  1362. display: flex;
  1363. justify-content: space-between;
  1364. align-items: center;
  1365. margin-bottom: 8px;
  1366. font-size: 14px;
  1367. color: #e0e0e0; /* 浅色文字 */
  1368. line-height: 1.5;
  1369. padding: 6px 8px;
  1370. border-radius: 4px;
  1371. cursor: pointer; /* 鼠标变成手型 */
  1372. transition: background-color 0.2s ease;
  1373. }
  1374. .detail-item:last-child {
  1375. margin-bottom: 0;
  1376. }
  1377. /* 鼠标悬浮背景变灰 */
  1378. .detail-item:hover {
  1379. background-color: rgba(
  1380. 255,
  1381. 255,
  1382. 255,
  1383. 0.15
  1384. ); /* 半透明白色,视觉上为灰色高亮 */
  1385. }
  1386. .detail-name {
  1387. color: #ccc;
  1388. }
  1389. .detail-val {
  1390. font-weight: bold;
  1391. color: #fff;
  1392. }
  1393. .empty-tip {
  1394. text-align: center;
  1395. color: #aaa;
  1396. font-size: 12px;
  1397. padding: 10px 0;
  1398. }
  1399. .item-arrow {
  1400. transition: all 0.3s ease;
  1401. color: #cbd5e1; /* 默认颜色 */
  1402. }
  1403. /* 2. 当卡片悬浮时,改变箭头的样式 */
  1404. .item-card:hover .item-arrow {
  1405. color: #02409b; /* 变蓝 (使用你主题中的蓝色) */
  1406. transform: translateX(4px); /* 向右平移 4px */
  1407. }
  1408. .tabs-wrapper {
  1409. position: relative;
  1410. width: 100%;
  1411. display: flex;
  1412. align-items: center;
  1413. margin-bottom: 20px;
  1414. }
  1415. /* 修改:原有的 .tabs-container 去掉 margin-bottom,因为现在由 wrapper 控制间距 */
  1416. .tabs-container {
  1417. display: flex;
  1418. align-items: center;
  1419. border-bottom: 1px solid #192754;
  1420. padding-left: 0;
  1421. overflow-x: auto;
  1422. scroll-behavior: smooth;
  1423. scrollbar-width: none; /* Firefox */
  1424. -ms-overflow-style: none; /* IE 10+ */
  1425. flex: 1; /* 占据中间剩余空间 */
  1426. mask-image: linear-gradient(
  1427. to right,
  1428. transparent,
  1429. black 20px,
  1430. black 98%,
  1431. transparent
  1432. ); /* 可选:添加两侧渐变遮罩效果 */
  1433. -webkit-mask-image: linear-gradient(
  1434. to right,
  1435. transparent,
  1436. black 20px,
  1437. black 98%,
  1438. transparent
  1439. );
  1440. }
  1441. .tabs-container::-webkit-scrollbar {
  1442. display: none;
  1443. }
  1444. /* 新增:箭头按钮样式 */
  1445. .scroll-arrow {
  1446. position: absolute;
  1447. top: 50%;
  1448. transform: translateY(-50%);
  1449. z-index: 10;
  1450. width: 30px;
  1451. height: 30px;
  1452. border-radius: 50%;
  1453. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  1454. display: flex;
  1455. align-items: center;
  1456. justify-content: center;
  1457. cursor: pointer;
  1458. color: #64748b;
  1459. transition: all 0.2s ease;
  1460. opacity: 0; /* 默认隐藏,鼠标悬停容器或需要时显示,或者始终显示但禁用时变灰 */
  1461. visibility: hidden;
  1462. }
  1463. /* 当容器被悬停时显示箭头,或者你可以去掉这个限制让它始终显示 */
  1464. .tabs-wrapper:hover .scroll-arrow {
  1465. opacity: 1;
  1466. visibility: visible;
  1467. }
  1468. .scroll-arrow.left {
  1469. left: 0;
  1470. }
  1471. .scroll-arrow.right {
  1472. right: 0;
  1473. }
  1474. .scroll-arrow:hover:not(:disabled) {
  1475. color: #02409b;
  1476. }
  1477. .scroll-arrow:disabled {
  1478. /* background-color: #f8fafc; */
  1479. color: #cbd5e1;
  1480. cursor: not-allowed;
  1481. box-shadow: none;
  1482. border-color: #f1f5f9;
  1483. opacity: 0.6;
  1484. visibility: visible; /* 禁用时也要可见以展示状态 */
  1485. }
  1486. /* 确保 Tab 项不会被箭头遮挡太多,可以在容器两侧加一点 padding */
  1487. .tabs-container {
  1488. /* 在原有样式基础上增加左右 padding,防止第一个和最后一个 Tab 被箭头完全遮住 */
  1489. padding: 0 40px;
  1490. }
  1491. </style>