index.vue 31 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336
  1. <template>
  2. <div class="ehr-page">
  3. <Header />
  4. <section class="hero">
  5. <div class="hero-inner">
  6. <!-- 判断上下午 -->
  7. <h1 class="hero-title">
  8. {{ getGreeting() }},{{ userStore.getUser.nickname }}
  9. </h1>
  10. <p class="hero-desc">
  11. 今天是 {{ new Date().toLocaleDateString() }}。您有
  12. {{ stats[0].number }}条流程待处理。
  13. </p>
  14. </div>
  15. </section>
  16. <!-- 任务统计 -->
  17. <section class="total">
  18. <div class="total-card" v-for="(item, index) in stats" :key="index">
  19. <el-popover
  20. placement="top"
  21. :width="100"
  22. trigger="hover"
  23. popper-class="glass-popover"
  24. :disabled="getDetailList(index).length === 0"
  25. transition="el-zoom-in-left"
  26. >
  27. <template #reference>
  28. <div class="card-wrapper">
  29. <!-- ... 图标和内容 ... -->
  30. <div class="card-icon" :style="{ backgroundColor: item.bgcolor }">
  31. <Icon :icon="item.icon" :color="item.color" />
  32. </div>
  33. <div class="card-content">
  34. <p class="card-title">{{ item.title }}</p>
  35. <el-skeleton :rows="1" :animated="true" :loading="statsLoading">
  36. <template #template>
  37. <el-skeleton-item
  38. variant="text"
  39. style="width: 60%; height: 32px; border-radius: 50px"
  40. />
  41. </template>
  42. <p class="card-number">{{ item.number }}</p>
  43. </el-skeleton>
  44. </div>
  45. </div>
  46. </template>
  47. <div class="detail-list">
  48. <div
  49. v-for="(task, idx) in getDetailList(index)"
  50. :key="idx"
  51. class="detail-item"
  52. @click="handleDetailClick(task, item.title)"
  53. >
  54. <span class="detail-name">{{ task.name }}</span>
  55. <span class="detail-val">{{ task.value }}</span>
  56. </div>
  57. <div v-if="getDetailList(index).length === 0" class="empty-tip">
  58. 暂无详细数据
  59. </div>
  60. </div>
  61. </el-popover>
  62. </div>
  63. </section>
  64. <div class="content">
  65. <div class="search-bar">
  66. <div class="search-input">
  67. <Icon icon="mdi:magnify" class="search-icon" />
  68. <input
  69. v-model.trim="searchKeyword"
  70. type="text"
  71. class="search-field"
  72. placeholder="搜索流程名称"
  73. />
  74. </div>
  75. <span v-if="searchKeyword" class="search-meta">
  76. 共找到 {{ displayedFlows.length }} 条结果
  77. </span>
  78. </div>
  79. <div class="tabs-container" role="tablist" aria-label="EHR模块">
  80. <button
  81. class="el-tab-item"
  82. type="button"
  83. role="tab"
  84. :class="{ 'is-active': activeKey === 'all' }"
  85. :aria-selected="activeKey === 'all'"
  86. @click="setAll"
  87. >
  88. <span class="tab-label">全部</span>
  89. </button>
  90. <button
  91. v-for="tab in tabs"
  92. :key="tab.groupName"
  93. class="el-tab-item"
  94. :class="{ 'is-active': tab.groupName === activeKey }"
  95. type="button"
  96. role="tab"
  97. :aria-selected="tab.groupName === activeKey"
  98. @click="getById(tab)"
  99. >
  100. <span class="tab-label">{{ tab.groupName }}</span>
  101. </button>
  102. </div>
  103. <div role="tabpanel">
  104. <div v-if="displayedFlows.length" class="items-grid">
  105. <div
  106. v-for="item in displayedFlows"
  107. :key="item.id"
  108. class="item-card"
  109. :style="{
  110. '--item-accent': getIconTheme(item).color,
  111. '--item-accent-soft': getIconTheme(item).softColor,
  112. '--item-accent-hover': getIconTheme(item).hoverColor,
  113. }"
  114. @click="go(item)"
  115. >
  116. <div class="item-top">
  117. <div
  118. class="item-icon"
  119. :style="{ backgroundColor: getIconTheme(item).softColor }"
  120. >
  121. <Icon
  122. :icon="item.icon || 'mdi:file-document-outline'"
  123. :color="getIconTheme(item).color"
  124. />
  125. </div>
  126. <div>
  127. <span
  128. class="text-[10px] font-bold text-slate-300 uppercase tracking-widest group-hover:text-blue-200"
  129. >{{ item.type }}</span
  130. >
  131. </div>
  132. </div>
  133. <div class="item-body">
  134. <h3 class="item-name font-bold text-slate-900">
  135. {{ item.flowName }}
  136. </h3>
  137. <p class="item-desc">{{ item.remark || "暂无描述" }}</p>
  138. </div>
  139. <!-- <div class="flex justify-between">
  140. <div class="item-time flex items-center gap-2">
  141. <svg
  142. xmlns="http://www.w3.org/2000/svg"
  143. width="1em"
  144. height="1em"
  145. viewBox="0 0 24 24"
  146. >
  147. <g
  148. fill="none"
  149. stroke="#f97316"
  150. stroke-linecap="round"
  151. stroke-linejoin="round"
  152. stroke-width="1.5"
  153. >
  154. <path
  155. d="M15.362 5.214A8.252 8.252 0 0 1 12 21A8.25 8.25 0 0 1 6.038 7.047A8.3 8.3 0 0 0 9 9.601a8.98 8.98 0 0 1 3.361-6.867a8.2 8.2 0 0 0 3 2.48"
  156. ></path>
  157. <path
  158. d="M12 18a3.75 3.75 0 0 0 .495-7.468a6 6 0 0 0-1.925 3.547a6 6 0 0 1-2.133-1.001A3.75 3.75 0 0 0 12 18"
  159. ></path>
  160. </g>
  161. </svg>
  162. <span class="text-[12px] text-[#babdd1]">提交流程</span>
  163. </div>
  164. <Icon icon="mdi-light:chevron-right" class="item-arrow w-6 h-6" />
  165. </div> -->
  166. </div>
  167. </div>
  168. <div v-else class="empty-state">
  169. <Icon icon="mdi:file-search-outline" class="empty-icon" />
  170. <p class="empty-title">没有找到相关流程</p>
  171. <p class="empty-desc">试试换个名称关键词搜索</p>
  172. </div>
  173. </div>
  174. </div>
  175. <Footer />
  176. </div>
  177. </template>
  178. <script setup>
  179. import Header from "@components/home/header.vue";
  180. import Footer from "@components/home/Footer.vue";
  181. import { computed, ref, onMounted, onBeforeUnmount, nextTick } from "vue";
  182. import { Icon } from "@iconify/vue";
  183. import { getFlows, ssoLogin, getOATasks, getCRMTasks } from "@/api/user";
  184. import { useUserStore } from "@/stores/useUserStore";
  185. import { getAccessToken } from "@/utils/auth";
  186. import * as echarts from "echarts";
  187. import { useRouter } from "vue-router";
  188. import dd from "dingtalk-jsapi";
  189. const router = useRouter();
  190. import { ElLoading, ElMessage } from "element-plus";
  191. const userStore = useUserStore();
  192. const lineChartInstance = ref(null);
  193. let lineChartRef = ref(null);
  194. let pieChartRef = ref(null);
  195. const pieChartInstance = ref(null);
  196. let chartResizeObserver = null;
  197. let chartInitTimer = null;
  198. const initChartsSafe = (attempt = 0) => {
  199. const lineDom = lineChartRef.value;
  200. const pieDom = pieChartRef.value;
  201. if (!lineDom || !pieDom) return;
  202. const lineRect = lineDom.getBoundingClientRect();
  203. const pieRect = pieDom.getBoundingClientRect();
  204. const isLineReady = lineRect.width > 0 && lineRect.height > 0;
  205. const isPieReady = pieRect.width > 0 && pieRect.height > 0;
  206. if (isLineReady && isPieReady) {
  207. initLineChart();
  208. initPieChart();
  209. handleResize();
  210. return;
  211. }
  212. if (attempt < 8) {
  213. chartInitTimer = window.setTimeout(() => {
  214. initChartsSafe(attempt + 1);
  215. }, 120);
  216. }
  217. };
  218. const getGreeting = () => {
  219. const hour = new Date().getHours();
  220. if (hour < 12) return "早上好";
  221. if (hour < 18) return "下午好";
  222. return "晚上好";
  223. };
  224. // 模拟数据 - 请根据实际 API 返回的数据调整
  225. const lineChartData = {
  226. title: "流程处理趋势 (30天)",
  227. xAxis: ["03-27", "03-28", "03-29", "03-30", "03-31", "04-01", "04-02"],
  228. yAxis: [12, 18, 15, 20, 27, 24, 31],
  229. };
  230. const pieChartData = {
  231. title: "流程类型占比",
  232. seriesData: [
  233. { name: "财务报销", value: 35, itemStyle: { color: "#409eff" } },
  234. { name: "行政办公", value: 25, itemStyle: { color: "#f56c6c" } },
  235. { name: "IT技术", value: 20, itemStyle: { color: "#9a66ff" } },
  236. { name: "人力资源", value: 15, itemStyle: { color: "#e6a23c" } },
  237. { name: "业务申请", value: 5, itemStyle: { color: "#50c878" } },
  238. ],
  239. };
  240. const getDetailList = (index) => {
  241. switch (index) {
  242. case 0: // 我的待办
  243. return todoCount.value;
  244. case 1: // 已办事项
  245. return doneCount.value;
  246. case 2: // 发起流程
  247. return startCount.value;
  248. case 3: // 草稿箱
  249. return drafts.value;
  250. default:
  251. return [];
  252. }
  253. };
  254. // 初始化图表
  255. const initLineChart = () => {
  256. const chartDom = lineChartRef.value;
  257. if (!chartDom) return;
  258. const chart = echarts.init(chartDom);
  259. lineChartInstance.value = chart; // 保存实例
  260. chart.setOption({
  261. title: {
  262. text: lineChartData.title,
  263. left: "10",
  264. top: 20,
  265. textStyle: {
  266. fontSize: 16,
  267. fontWeight: "bold",
  268. color: "#333",
  269. },
  270. },
  271. // ... 原有的 option 配置保持不变 ...
  272. tooltip: {
  273. trigger: "axis",
  274. axisPointer: { type: "shadow" },
  275. },
  276. grid: {
  277. left: "3%",
  278. right: "4%",
  279. bottom: "10%",
  280. containLabel: true,
  281. },
  282. xAxis: {
  283. type: "category",
  284. data: lineChartData.xAxis,
  285. axisLabel: { formatter: (value) => value },
  286. },
  287. yAxis: {
  288. type: "value",
  289. splitLine: { show: true, lineStyle: { color: "#eee" } },
  290. },
  291. series: [
  292. {
  293. name: "处理数量",
  294. type: "line",
  295. smooth: true,
  296. areaStyle: {
  297. opacity: 0.3,
  298. color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
  299. { offset: 0, color: "#409eff" },
  300. { offset: 1, color: "#b3d8ff" },
  301. ]),
  302. },
  303. lineStyle: { width: 3, color: "#2563eb" },
  304. symbol: "circle",
  305. symbolSize: 8,
  306. data: lineChartData.yAxis,
  307. },
  308. ],
  309. });
  310. };
  311. const initPieChart = () => {
  312. const chartDom = pieChartRef.value;
  313. if (!chartDom) return;
  314. const chart = echarts.init(chartDom);
  315. pieChartInstance.value = chart; // 保存实例
  316. chart.setOption({
  317. title: {
  318. text: pieChartData.title,
  319. left: "10",
  320. top: 20,
  321. textStyle: {
  322. fontSize: 16,
  323. fontWeight: "bold",
  324. color: "#333",
  325. },
  326. },
  327. // ... 原有的 option 配置保持不变 ...
  328. tooltip: {
  329. trigger: "item",
  330. formatter: "{a} <br/>{b}: {c} ({d}%)",
  331. },
  332. legend: { show: false },
  333. series: [
  334. {
  335. name: "流程类型",
  336. type: "pie",
  337. radius: ["50%", "70%"],
  338. avoidLabelOverlap: false,
  339. label: { show: false, position: "center" },
  340. emphasis: {
  341. label: { show: true, fontSize: "14", fontWeight: "bold" },
  342. },
  343. labelLine: { show: false },
  344. data: pieChartData.seriesData,
  345. },
  346. ],
  347. });
  348. };
  349. const handleResize = () => {
  350. lineChartInstance.value?.resize();
  351. pieChartInstance.value?.resize();
  352. };
  353. const tabs = ref([]);
  354. const activeKey = ref("all");
  355. const searchKeyword = ref("");
  356. const iconPalette = [
  357. {
  358. color: "#2563eb",
  359. softColor: "rgba(37, 99, 235, 0.12)",
  360. hoverColor: "rgba(37, 99, 235, 0.18)",
  361. },
  362. {
  363. color: "#db2777",
  364. softColor: "rgba(219, 39, 119, 0.12)",
  365. hoverColor: "rgba(219, 39, 119, 0.18)",
  366. },
  367. {
  368. color: "#ea580c",
  369. softColor: "rgba(234, 88, 12, 0.12)",
  370. hoverColor: "rgba(234, 88, 12, 0.18)",
  371. },
  372. {
  373. color: "#059669",
  374. softColor: "rgba(5, 150, 105, 0.12)",
  375. hoverColor: "rgba(5, 150, 105, 0.18)",
  376. },
  377. {
  378. color: "#7c3aed",
  379. softColor: "rgba(124, 58, 237, 0.12)",
  380. hoverColor: "rgba(124, 58, 237, 0.18)",
  381. },
  382. {
  383. color: "#0f766e",
  384. softColor: "rgba(15, 118, 110, 0.12)",
  385. hoverColor: "rgba(15, 118, 110, 0.18)",
  386. },
  387. {
  388. color: "#dc2626",
  389. softColor: "rgba(220, 38, 38, 0.12)",
  390. hoverColor: "rgba(220, 38, 38, 0.18)",
  391. },
  392. {
  393. color: "#ca8a04",
  394. softColor: "rgba(202, 138, 4, 0.12)",
  395. hoverColor: "rgba(202, 138, 4, 0.18)",
  396. },
  397. ];
  398. const hashText = (text = "") =>
  399. String(text)
  400. .split("")
  401. .reduce((hash, char) => hash * 31 + char.charCodeAt(0), 0);
  402. const getIconTheme = (item) => {
  403. const seed = item?.id ?? item?.flowName ?? item?.type ?? "";
  404. const index = Math.abs(hashText(seed)) % iconPalette.length;
  405. return iconPalette[index];
  406. };
  407. const allTab = computed(() => {
  408. const flowRespVOS = tabs.value.flatMap((tab) => tab.flowRespVOS || []);
  409. return {
  410. groupName: "全部",
  411. remark: "全部流程",
  412. flowRespVOS,
  413. };
  414. });
  415. const activeTab = computed(() => {
  416. if (activeKey.value === "all") {
  417. return allTab.value;
  418. }
  419. return (
  420. tabs.value.find((tab) => tab.groupName === activeKey.value) || allTab.value
  421. );
  422. });
  423. const displayedFlows = computed(() => {
  424. const keyword = searchKeyword.value.trim().toLowerCase();
  425. if (!keyword) {
  426. return activeTab.value.flowRespVOS || [];
  427. }
  428. return (allTab.value.flowRespVOS || []).filter((item) =>
  429. String(item?.flowName || "")
  430. .toLowerCase()
  431. .includes(keyword),
  432. );
  433. });
  434. const getAll = async () => {
  435. const res = await getFlows({
  436. pageNo: 1,
  437. pageSize: 99,
  438. });
  439. tabs.value = res;
  440. };
  441. const setAll = () => {
  442. activeKey.value = "all";
  443. };
  444. const getById = (tab) => {
  445. activeKey.value = tab.groupName;
  446. };
  447. const go = async (item) => {
  448. if (userStore.getUser.username && getAccessToken()) {
  449. if (item.type === "OA") {
  450. const res = await ssoLogin({
  451. username: userStore.getUser.username,
  452. });
  453. if (res) {
  454. const ua = window.navigator.userAgent.toLowerCase();
  455. if (ua.includes("dingtalk") || ua.includes("dingtalkwork")) {
  456. // 钉钉环境
  457. const loading = ElLoading.service({
  458. lock: true,
  459. text: "正在跳转,请稍候...",
  460. background: "rgba(0, 0, 0, 0.7)",
  461. });
  462. const targetUrl1 = item.indexUrl + "?ssoToken=" + res + "#/main";
  463. const targetUrl2 = item.flowUrl;
  464. dd.biz.util.openLink({
  465. url: targetUrl1,
  466. onSuccess: () => {
  467. setTimeout(() => {
  468. dd.biz.util.openLink({
  469. url: targetUrl2,
  470. onSuccess: () => {
  471. loading.close();
  472. },
  473. onFail: (err) => {
  474. loading.close();
  475. ElMessage.error("跳转失败,请重试");
  476. },
  477. });
  478. }, 2000);
  479. },
  480. onFail: (err) => {
  481. loading.close();
  482. ElMessage.error("打开链接失败,请重试");
  483. },
  484. });
  485. } else {
  486. // 浏览器环境
  487. const loading = ElLoading.service({
  488. lock: true,
  489. text: "正在跳转,请稍候...",
  490. background: "rgba(0, 0, 0, 0.7)",
  491. });
  492. const newTab = window.open("", "_blank");
  493. newTab.location.href = item.indexUrl + "?ssoToken=" + res + "#/main";
  494. setTimeout(() => {
  495. newTab.location.href = item.flowUrl;
  496. setTimeout(() => {
  497. loading.close();
  498. }, 500);
  499. }, 100);
  500. }
  501. }
  502. }
  503. if (item.type === "CRM") {
  504. const ua = window.navigator.userAgent.toLowerCase();
  505. if (ua.includes("dingtalk") || ua.includes("dingtalkwork")) {
  506. dd.biz.util.openLink({
  507. url:
  508. item.indexUrl +
  509. "/global/sso/callback/00APEB9EEEA9B2E338B686B7ECFA8585808C.action?token=" +
  510. getAccessToken(), // 先跳你的 SSO 链接
  511. onSuccess: () => {
  512. // 延迟跳目标业务地址(和你原来 setTimeout 逻辑一致)
  513. setTimeout(() => {
  514. dd.biz.util.openLink({ url: item.flowUrl });
  515. }, 100);
  516. },
  517. });
  518. } else {
  519. const newTab = window.open("", "_blank");
  520. newTab.location.href =
  521. item.indexUrl +
  522. "/global/sso/callback/00APEB9EEEA9B2E338B686B7ECFA8585808C.action?token=" +
  523. getAccessToken();
  524. setTimeout(function () {
  525. newTab.location.href = item.flowUrl;
  526. }, 100);
  527. }
  528. }
  529. } else {
  530. router.push({ path: "/login" });
  531. }
  532. };
  533. const handleDetailClick = (task, categoryTitle) => {
  534. console.log(`点击了 ${categoryTitle} 中的 ${task.name}: ${task.value}`);
  535. // 示例:根据类型跳转
  536. if (task.name === "OA" && categoryTitle === "我的待办") {
  537. router.push({
  538. path: "/todo-list",
  539. query: { type: task.name.toLowerCase() },
  540. });
  541. }
  542. if (task.name === "OA" && categoryTitle === "已办事项") {
  543. router.push({
  544. path: "/oa-done-list",
  545. query: { type: task.name.toLowerCase() },
  546. });
  547. }
  548. if (task.name === "CRM" && categoryTitle === "我的待办") {
  549. router.push({
  550. path: "/crm-todo-list",
  551. query: { type: task.name.toLowerCase() },
  552. });
  553. }
  554. if (task.name === "CRM" && categoryTitle === "已办事项") {
  555. router.push({
  556. path: "/crm-done-list",
  557. query: { type: task.name.toLowerCase() },
  558. });
  559. }
  560. };
  561. let oaTasks = ref([]);
  562. let crmTasks = ref([]);
  563. const statsLoading = ref(true);
  564. const stats = ref([
  565. {
  566. icon: "mdi:clock-outline",
  567. title: "我的待办",
  568. number: 0,
  569. extra: "+2 今日",
  570. bgcolor: "#fff7ed",
  571. color: "#f59e0b",
  572. },
  573. {
  574. icon: "mdi:check-circle-outline",
  575. title: "已办事项",
  576. number: 0,
  577. bgcolor: "#eff6ff",
  578. color: "#2563eb",
  579. },
  580. ]);
  581. const todoCount = ref([
  582. { name: "OA", value: 0 },
  583. { name: "CRM", value: 0 },
  584. ]);
  585. // 已办事项
  586. const doneCount = ref([
  587. { name: "OA", value: 0 },
  588. { name: "CRM", value: 0 },
  589. ]);
  590. onMounted(async () => {
  591. getAll();
  592. // 等待 DOM 与样式生效,避免移动端首屏尺寸为 0
  593. await nextTick();
  594. requestAnimationFrame(() => {
  595. initChartsSafe();
  596. // 添加监听
  597. window.addEventListener("resize", handleResize);
  598. // 使用 ResizeObserver 监听容器尺寸变化(移动端更稳定)
  599. if (typeof ResizeObserver !== "undefined") {
  600. chartResizeObserver = new ResizeObserver(() => {
  601. handleResize();
  602. });
  603. if (lineChartRef.value) chartResizeObserver.observe(lineChartRef.value);
  604. if (pieChartRef.value) chartResizeObserver.observe(pieChartRef.value);
  605. }
  606. });
  607. if (userStore.getUser.username) {
  608. try {
  609. const res = await getOATasks({
  610. id: userStore.getUser.username,
  611. pageNum: 1,
  612. pageSize: 99,
  613. });
  614. oaTasks.value = res;
  615. const crmRes = await getCRMTasks({
  616. id: userStore.getUser.username,
  617. type: "pending",
  618. pageNum: 1,
  619. pageSize: 10,
  620. });
  621. crmTasks.value = crmRes;
  622. stats.value[0].number =
  623. Number(oaTasks.value.todoCount) + Number(crmTasks.value.todoCount);
  624. todoCount.value = [
  625. { name: "OA", value: oaTasks.value.todoCount ?? 0 },
  626. { name: "CRM", value: crmTasks.value.todoCount ?? 0 },
  627. ];
  628. // *****************已办事项统计*************************
  629. const crmDoneRes = await getCRMTasks({
  630. id: userStore.getUser.username,
  631. type: "approved",
  632. pageNum: 1,
  633. pageSize: 10,
  634. });
  635. stats.value[1].number =
  636. Number(oaTasks.value.doneCount) + Number(crmDoneRes.todoCount);
  637. doneCount.value = [
  638. { name: "OA", value: oaTasks.value.doneCount ?? 0 },
  639. { name: "CRM", value: crmDoneRes.todoCount ?? 0 },
  640. ];
  641. } finally {
  642. statsLoading.value = false;
  643. }
  644. setInterval(
  645. async () => {
  646. const res = await getOATasks({
  647. id: userStore.getUser.username,
  648. pageNum: 1,
  649. pageSize: 10,
  650. });
  651. oaTasks.value = res;
  652. const crmRes = await getCRMTasks({
  653. id: userStore.getUser.username,
  654. type: "pending",
  655. pageNum: 1,
  656. pageSize: 10,
  657. });
  658. crmTasks.value = crmRes;
  659. stats.value[0].number =
  660. Number(oaTasks.value.todoCount) + Number(crmTasks.value.todoCount);
  661. todoCount.value = [
  662. { name: "OA", value: oaTasks.value.todoCount ?? 0 },
  663. { name: "CRM", value: crmTasks.value.todoCount ?? 0 },
  664. ];
  665. const crmDoneRes = await getCRMTasks({
  666. id: userStore.getUser.username,
  667. type: "approved",
  668. pageNum: 1,
  669. pageSize: 10,
  670. });
  671. stats.value[1].number =
  672. Number(oaTasks.value.doneCount) + Number(crmDoneRes.todoCount);
  673. doneCount.value = [
  674. { name: "OA", value: oaTasks.value.doneCount ?? 0 },
  675. { name: "CRM", value: crmDoneRes.todoCount ?? 0 },
  676. ];
  677. },
  678. 10 * 60 * 1000,
  679. ); // 每5分钟刷新一次
  680. } else {
  681. statsLoading.value = false;
  682. }
  683. });
  684. // 组件卸载时移除监听,防止内存泄漏
  685. onBeforeUnmount(() => {
  686. window.removeEventListener("resize", handleResize);
  687. chartResizeObserver?.disconnect();
  688. if (chartInitTimer) window.clearTimeout(chartInitTimer);
  689. // 可选:销毁 echarts 实例
  690. lineChartInstance.value?.dispose();
  691. pieChartInstance.value?.dispose();
  692. });
  693. </script>
  694. <style scoped>
  695. /* .ehr-page {
  696. color: #1f2a37;
  697. background: linear-gradient(180deg, #f4f4f2 0%, #f7f6f3 50%, #f2f1ef 100%);
  698. min-height: 100vh;
  699. } */
  700. :global(body) {
  701. background-color: #f8fafc;
  702. }
  703. .hero {
  704. position: relative;
  705. padding: 72px 6vw 48px;
  706. overflow: hidden;
  707. margin-top: 20px;
  708. display: flex;
  709. justify-content: space-between;
  710. align-items: center;
  711. }
  712. .hero-inner {
  713. max-width: 920px;
  714. }
  715. .hero-title {
  716. font-size: clamp(18px, 2vw, 22px);
  717. line-height: 1.2;
  718. margin: 16px 0 12px;
  719. color: #111827;
  720. font-weight: bold;
  721. }
  722. .hero-desc {
  723. font-size: 16px;
  724. color: #4b5563;
  725. max-width: 720px;
  726. line-height: 1.8;
  727. }
  728. .hero-accent {
  729. position: absolute;
  730. top: -120px;
  731. right: -140px;
  732. width: 360px;
  733. height: 360px;
  734. border-radius: 50%;
  735. opacity: 0.9;
  736. pointer-events: none;
  737. }
  738. .content {
  739. padding: 0 6vw 80px;
  740. /* height: 80vh; */
  741. }
  742. .search-bar {
  743. display: flex;
  744. align-items: center;
  745. justify-content: space-between;
  746. gap: 16px;
  747. margin-bottom: 18px;
  748. }
  749. .search-input {
  750. display: flex;
  751. align-items: center;
  752. gap: 10px;
  753. width: min(420px, 100%);
  754. padding: 0 16px;
  755. height: 46px;
  756. border-radius: 14px;
  757. background: #ffffff;
  758. border: 1px solid #e2e8f0;
  759. box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
  760. transition:
  761. border-color 0.2s ease,
  762. box-shadow 0.2s ease;
  763. }
  764. .search-input:focus-within {
  765. border-color: rgba(2, 64, 155, 0.28);
  766. box-shadow: 0 12px 28px rgba(2, 64, 155, 0.1);
  767. }
  768. .search-icon {
  769. flex-shrink: 0;
  770. font-size: 18px;
  771. color: #94a3b8;
  772. }
  773. .search-field {
  774. width: 100%;
  775. border: none;
  776. background: transparent;
  777. outline: none;
  778. font-size: 14px;
  779. color: #0f172a;
  780. }
  781. .search-field::placeholder {
  782. color: #94a3b8;
  783. }
  784. .search-meta {
  785. flex-shrink: 0;
  786. font-size: 13px;
  787. color: #64748b;
  788. }
  789. .tabs-container {
  790. display: flex;
  791. align-items: center;
  792. border-bottom: 1px solid #e4e7ed; /* Element Plus 标准的分割线颜色 */
  793. margin-bottom: 20px;
  794. padding-left: 0;
  795. overflow-x: auto; /* 防止Tab过多时溢出 */
  796. }
  797. .el-tab-item {
  798. position: relative;
  799. display: inline-flex;
  800. align-items: center;
  801. justify-content: center;
  802. padding: 0 20px;
  803. height: 40px; /* 标准高度 */
  804. font-size: 14px;
  805. color: #64748b; /* Element Plus 主要文字颜色 */
  806. background-color: transparent;
  807. border: none;
  808. border-bottom: 2px solid transparent; /* 用于激活态的下划线 */
  809. cursor: pointer;
  810. transition: all 0.3s;
  811. margin-right: 0;
  812. outline: none;
  813. flex-shrink: 0; /* 防止压缩 */
  814. }
  815. .el-tab-item:hover {
  816. color: #02409b; /* Element Plus 主题蓝 */
  817. }
  818. .el-tab-item.is-active {
  819. color: #02409b; /* 激活态文字颜色 */
  820. font-weight: 500;
  821. border-bottom-color: #02409b; /* 激活态下划线 */
  822. }
  823. .tab-label {
  824. line-height: 1;
  825. font-weight: bold;
  826. }
  827. .tab-sub {
  828. margin-left: 8px;
  829. font-size: 12px;
  830. color: #909399; /* 次要文字颜色 */
  831. transform: scale(0.9);
  832. }
  833. .panel {
  834. background: #ffffff;
  835. border-radius: 24px;
  836. padding: 28px 28px 32px;
  837. box-shadow: 0 26px 48px rgba(15, 23, 42, 0.08);
  838. }
  839. .panel-head {
  840. display: flex;
  841. align-items: flex-start;
  842. justify-content: space-between;
  843. gap: 24px;
  844. padding-bottom: 20px;
  845. margin-bottom: 24px;
  846. }
  847. .panel-title {
  848. font-size: 22px;
  849. color: #111827;
  850. margin-bottom: 6px;
  851. }
  852. .panel-subtitle {
  853. color: #6b7280;
  854. font-size: 14px;
  855. }
  856. .panel-meta {
  857. text-align: right;
  858. color: #6b7280;
  859. font-size: 12px;
  860. }
  861. .panel-count {
  862. display: block;
  863. font-size: 20px;
  864. font-weight: 600;
  865. color: #111827;
  866. }
  867. .items-grid {
  868. display: grid;
  869. grid-template-columns: repeat(auto-fill, 240px);
  870. justify-content: flex-start;
  871. gap: 16px;
  872. }
  873. .item-card {
  874. --item-accent: #2563eb;
  875. --item-accent-soft: rgba(37, 99, 235, 0.12);
  876. --item-accent-hover: rgba(37, 99, 235, 0.18);
  877. display: flex;
  878. flex-direction: column;
  879. gap: 18px;
  880. padding: 22px 22px 18px;
  881. border-radius: 22px;
  882. background: #ffffff;
  883. border: 1px solid rgb(241, 245, 249);
  884. box-shadow:
  885. rgba(0, 0, 0, 0) 0px 0px 0px 0px,
  886. rgba(0, 0, 0, 0) 0px 0px 0px 0px,
  887. rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
  888. transition:
  889. transform 0.2s ease,
  890. border-color 0.2s ease,
  891. box-shadow 0.2s ease,
  892. background-color 0.2s ease;
  893. cursor: pointer;
  894. border-bottom: 1px solid rgb(241, 245, 249);
  895. }
  896. .item-card:hover {
  897. transform: translateY(-3px);
  898. box-shadow: 0 18px 36px rgba(15, 23, 42, 0.08);
  899. color: #02409b !important;
  900. }
  901. .item-top {
  902. display: flex;
  903. align-items: flex-start;
  904. justify-content: space-between;
  905. }
  906. .item-body {
  907. display: flex;
  908. flex-direction: column;
  909. gap: 8px;
  910. }
  911. .item-icon {
  912. width: 52px;
  913. height: 52px;
  914. border-radius: 16px;
  915. display: grid;
  916. place-items: center;
  917. font-size: 20px;
  918. transition:
  919. transform 0.2s ease,
  920. background-color 0.2s ease,
  921. box-shadow 0.2s ease;
  922. }
  923. .item-icon :deep(svg) {
  924. width: 24px;
  925. height: 24px;
  926. transition: color 0.2s ease;
  927. }
  928. .item-card:hover .item-icon {
  929. background: var(--item-accent) !important;
  930. transform: translateY(-2px);
  931. box-shadow: 0 10px 20px -12px var(--item-accent);
  932. }
  933. .item-card:hover .item-icon :deep(svg) {
  934. color: #ffffff !important;
  935. }
  936. .item-role {
  937. font-size: 12px;
  938. letter-spacing: 0.2em;
  939. color: #c7ced9;
  940. font-weight: 600;
  941. }
  942. .item-name {
  943. font-size: 16px;
  944. line-height: 1.45;
  945. /* font-weight: 600; */
  946. }
  947. .item-desc {
  948. font-size: 13px;
  949. color: #6b7280;
  950. line-height: 1.6;
  951. }
  952. .empty-state {
  953. display: flex;
  954. flex-direction: column;
  955. align-items: center;
  956. justify-content: center;
  957. gap: 10px;
  958. min-height: 240px;
  959. border-radius: 22px;
  960. background: rgba(255, 255, 255, 0.72);
  961. border: 1px dashed #dbe3ee;
  962. color: #64748b;
  963. }
  964. .empty-icon {
  965. font-size: 42px;
  966. color: #94a3b8;
  967. }
  968. .empty-title {
  969. font-size: 16px;
  970. font-weight: 600;
  971. color: #334155;
  972. }
  973. .empty-desc {
  974. font-size: 13px;
  975. color: #94a3b8;
  976. }
  977. .item-footer {
  978. display: flex;
  979. align-items: center;
  980. justify-content: space-between;
  981. color: #94a3b8;
  982. font-size: 12px;
  983. }
  984. .item-usage {
  985. display: inline-flex;
  986. align-items: center;
  987. gap: 8px;
  988. }
  989. .item-flame {
  990. width: 18px;
  991. height: 18px;
  992. border-radius: 50%;
  993. display: grid;
  994. place-items: center;
  995. background: rgba(255, 108, 0, 0.12);
  996. color: #ff6c00;
  997. font-size: 12px;
  998. }
  999. .item-arrow {
  1000. font-size: 22px;
  1001. color: #cbd5e1;
  1002. }
  1003. @media (max-width: 720px) {
  1004. .hero {
  1005. padding: 56px 7vw 36px;
  1006. }
  1007. .search-bar {
  1008. flex-direction: column;
  1009. align-items: stretch;
  1010. }
  1011. .search-input {
  1012. width: 100%;
  1013. }
  1014. .panel-head {
  1015. flex-direction: column;
  1016. align-items: flex-start;
  1017. }
  1018. .panel-meta {
  1019. text-align: left;
  1020. }
  1021. .items-grid {
  1022. grid-template-columns: 1fr;
  1023. }
  1024. }
  1025. .total {
  1026. display: flex;
  1027. gap: 16px;
  1028. padding: 0 6vw 50px;
  1029. /* margin-bottom: 24px; */
  1030. }
  1031. .total-card {
  1032. /* flex: 1; */
  1033. width: 300px;
  1034. background: #ffffff;
  1035. border-radius: 16px;
  1036. padding: 20px;
  1037. box-shadow:
  1038. rgba(0, 0, 0, 0.05) 0px 1px 3px 0px,
  1039. rgba(0, 0, 0, 0) 0px 0px 0px 0px,
  1040. rgba(0, 0, 0, 0.05) 0px 1px 2px 0px,
  1041. rgba(0, 0, 0, 0.05) 0px 2px 4px -1px,
  1042. rgba(0, 0, 0, 0.05) 0px 4px 6px -2px;
  1043. transition:
  1044. transform 0.2s ease,
  1045. box-shadow 0.2s ease;
  1046. cursor: pointer;
  1047. /* border: 1px solid #e5e7eb; */
  1048. /* border-top: solid 5px #02409b; */
  1049. overflow: visible;
  1050. }
  1051. .total-card:hover {
  1052. transform: translateY(-4px);
  1053. box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1);
  1054. color: #02409b !important;
  1055. }
  1056. .card-icon {
  1057. width: 40px;
  1058. height: 40px;
  1059. border-radius: 12px;
  1060. background: #f9f9f9;
  1061. display: flex;
  1062. align-items: center;
  1063. justify-content: center;
  1064. margin-bottom: 12px;
  1065. }
  1066. .card-icon svg {
  1067. width: 20px;
  1068. height: 20px;
  1069. color: var(--primary-color);
  1070. }
  1071. .card-content {
  1072. flex: 1;
  1073. text-align: left;
  1074. }
  1075. .card-title {
  1076. font-size: 14px;
  1077. color: #6b7280;
  1078. margin-bottom: 4px;
  1079. }
  1080. .card-number {
  1081. font-size: 28px;
  1082. font-weight: 600;
  1083. /* color: #111827; */
  1084. }
  1085. .card-extra {
  1086. font-size: 12px;
  1087. color: #10b981;
  1088. margin-top: 8px;
  1089. text-align: right;
  1090. }
  1091. .charts-container {
  1092. display: flex;
  1093. flex-wrap: wrap;
  1094. gap: 24px;
  1095. padding: 0 6vw;
  1096. margin-bottom: 80px;
  1097. width: 100%;
  1098. box-sizing: border-box;
  1099. }
  1100. .chart-item {
  1101. flex: 1;
  1102. /* 桌面端最小宽度,防止过度挤压 */
  1103. min-width: 300px;
  1104. /* 必须设置固定高度,ECharts 需要明确的高度才能渲染 */
  1105. height: 350px;
  1106. border-radius: 16px;
  1107. background-color: #ffffff;
  1108. box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
  1109. overflow: hidden;
  1110. position: relative;
  1111. }
  1112. :global(.glass-popover) {
  1113. background: rgba(0, 0, 0, 0.6) !important;
  1114. backdrop-filter: blur(10px);
  1115. -webkit-backdrop-filter: blur(10px);
  1116. height: 200px !important;
  1117. width: 100px !important;
  1118. overflow-y: hidden !important;
  1119. box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1) !important;
  1120. border-radius: 8px !important;
  1121. padding: 10px !important;
  1122. color: #fff !important;
  1123. z-index: 2000 !important; /* 确保在最上层 */
  1124. top: 15% !important; /* 根据需要调整位置 */
  1125. }
  1126. :global(.glass-popover .el-popper__arrow::before) {
  1127. background: transparent !important;
  1128. /* border: 1px solid rgba(255, 255, 255, 0.1); */
  1129. border: none !important;
  1130. }
  1131. .detail-list {
  1132. padding: 5px 0;
  1133. }
  1134. .detail-item {
  1135. display: flex;
  1136. justify-content: space-between;
  1137. align-items: center;
  1138. margin-bottom: 8px;
  1139. font-size: 14px;
  1140. color: #e0e0e0; /* 浅色文字 */
  1141. line-height: 1.5;
  1142. padding: 6px 8px;
  1143. border-radius: 4px;
  1144. cursor: pointer; /* 鼠标变成手型 */
  1145. transition: background-color 0.2s ease;
  1146. }
  1147. .detail-item:last-child {
  1148. margin-bottom: 0;
  1149. }
  1150. /* 鼠标悬浮背景变灰 */
  1151. .detail-item:hover {
  1152. background-color: rgba(
  1153. 255,
  1154. 255,
  1155. 255,
  1156. 0.15
  1157. ); /* 半透明白色,视觉上为灰色高亮 */
  1158. }
  1159. .detail-name {
  1160. color: #ccc;
  1161. }
  1162. .detail-val {
  1163. font-weight: bold;
  1164. color: #fff;
  1165. }
  1166. .empty-tip {
  1167. text-align: center;
  1168. color: #aaa;
  1169. font-size: 12px;
  1170. padding: 10px 0;
  1171. }
  1172. .item-arrow {
  1173. transition: all 0.3s ease;
  1174. color: #cbd5e1; /* 默认颜色 */
  1175. }
  1176. /* 2. 当卡片悬浮时,改变箭头的样式 */
  1177. .item-card:hover .item-arrow {
  1178. color: #02409b; /* 变蓝 (使用你主题中的蓝色) */
  1179. transform: translateX(4px); /* 向右平移 4px */
  1180. }
  1181. </style>