index.vue 37 KB

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