yanghao 3 днів тому
батько
коміт
7c4370b0e9
7 змінених файлів з 357 додано та 96 видалено
  1. 1 0
      package.json
  2. 23 0
      pnpm-lock.yaml
  3. 7 0
      src/api/user.ts
  4. 4 15
      src/components/home/CardItem.vue
  5. 1 1
      src/router/index.ts
  6. 319 78
      src/views/flow/index.vue
  7. 2 2
      src/views/login.vue

+ 1 - 0
package.json

@@ -23,6 +23,7 @@
     "@types/qs": "^6.14.0",
     "axios": "^1.13.1",
     "dingtalk-jsapi": "^3.2.5",
+    "echarts": "^6.0.0",
     "element-plus": "^2.11.7",
     "jsencrypt": "^3.5.4",
     "motion-v": "^1.7.4",

+ 23 - 0
pnpm-lock.yaml

@@ -23,6 +23,9 @@ importers:
       dingtalk-jsapi:
         specifier: ^3.2.5
         version: 3.2.5
+      echarts:
+        specifier: ^6.0.0
+        version: 6.0.0
       element-plus:
         specifier: ^2.11.7
         version: 2.11.7(vue@3.5.22(typescript@5.9.3))
@@ -924,6 +927,9 @@ packages:
     resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
     engines: {node: '>= 0.4'}
 
+  echarts@6.0.0:
+    resolution: {integrity: sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==}
+
   electron-to-chromium@1.5.244:
     resolution: {integrity: sha512-OszpBN7xZX4vWMPJwB9illkN/znA8M36GQqQxi6MNy9axWxhOfJyZZJtSLQCpEFLHP2xK33BiWx9aIuIEXVCcw==}
 
@@ -1466,6 +1472,9 @@ packages:
     resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
     engines: {node: '>=6'}
 
+  tslib@2.3.0:
+    resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==}
+
   tslib@2.8.1:
     resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
 
@@ -1650,6 +1659,9 @@ packages:
   yallist@3.1.1:
     resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
 
+  zrender@6.0.0:
+    resolution: {integrity: sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==}
+
 snapshots:
 
   '@babel/code-frame@7.27.1':
@@ -2445,6 +2457,11 @@ snapshots:
       es-errors: 1.3.0
       gopd: 1.2.0
 
+  echarts@6.0.0:
+    dependencies:
+      tslib: 2.3.0
+      zrender: 6.0.0
+
   electron-to-chromium@1.5.244: {}
 
   element-plus@2.11.7(vue@3.5.22(typescript@5.9.3)):
@@ -2948,6 +2965,8 @@ snapshots:
 
   totalist@3.0.1: {}
 
+  tslib@2.3.0: {}
+
   tslib@2.8.1: {}
 
   typescript@5.9.3: {}
@@ -3130,3 +3149,7 @@ snapshots:
       is-wsl: 3.1.0
 
   yallist@3.1.1: {}
+
+  zrender@6.0.0:
+    dependencies:
+      tslib: 2.3.0

+ 7 - 0
src/api/user.ts

@@ -91,3 +91,10 @@ export const getFlows = async (params) => {
     params,
   });
 };
+
+// oa待办任务
+export const getOATasks = async (id) => {
+  return await request.get({
+    url: "/admin-api/portal/todo/oa?workcode=" + id,
+  });
+};

+ 4 - 15
src/components/home/CardItem.vue

@@ -218,22 +218,11 @@ const handleView = async (item: Item) => {
 
   if (item.label === "客户管理(CRM)") {
     if (userStore.getUser.username && getAccessToken()) {
-      // window.open(
-      //   "https://crm-tencent.xiaoshouyi.com/global/sso/callback/00APEB9EEEA9B2E338B686B7ECFA8585808C.action?token=" +
-      //     getAccessToken(),
-      //   "_blank",
-      // );
-
-      const newTab = window.open("", "_blank");
-
-      newTab!.location.href =
+      window.open(
         "https://crm-tencent.xiaoshouyi.com/global/sso/callback/00APEB9EEEA9B2E338B686B7ECFA8585808C.action?token=" +
-        getAccessToken();
-
-      setTimeout(function () {
-        newTab!.location.href =
-          "https://login.xiaoshouyi.com/auc/oauth2/auth?response_type=code&client_id=loginClientId_1000&redirect_uri=https%3A%2F%2Fcrm-tencent.xiaoshouyi.com%2Fneologin%2Fskip%2Fv2%2Fauc%2Foauth2%2Ftoken%2Finfo&access_type=offline&scope=crm#/entityGrid/customerTable__c?objectApiKey=customerTable__c";
-      }, 1000);
+          getAccessToken(),
+        "_blank",
+      );
     } else {
       router.push({ path: "/login" });
     }

+ 1 - 1
src/router/index.ts

@@ -92,7 +92,7 @@ const parseURL = (
   return { basePath, paramsObject };
 };
 
-const whiteList = ["/", "/login", "/social-login", "/auth-redirect", "/ehr"];
+const whiteList = ["/", "/login", "/social-login", "/auth-redirect"];
 router.beforeEach(async (to, from, next) => {
   // 设置页面标题
   const title = to.meta.title as string;

+ 319 - 78
src/views/flow/index.vue

@@ -8,18 +8,28 @@
           今天是 {{ new Date().toLocaleDateString() }}。您有 5 条流程待处理。
         </p>
       </div>
-      <div class="hero-accent" aria-hidden="true"></div>
+      <div class="opration">
+        <el-button type="primary" size="default" color="#02409b"
+          ><Icon icon="mdi:plus-thick" class="icon pr-1" />快速发起</el-button
+        >
+        <el-button type="default" size="default"
+          ><Icon
+            icon="mdi:chart-bar"
+            class="icon pr-1"
+          />查看效率报表</el-button
+        >
+      </div>
     </section>
 
     <!-- 任务统计 -->
     <section class="total">
       <div class="total-card" v-for="(item, index) in stats" :key="index">
         <div class="card-icon" :style="{ backgroundColor: item.bgcolor }">
-          <Icon :icon="item.icon" />
+          <Icon :icon="item.icon" :color="item.color" />
         </div>
         <div class="card-content">
           <p class="card-title">{{ item.title }}</p>
-          <p class="card-number">{{ item.number }}</p>
+          <p class="card-number">{{ oaTasks.todoCount }}</p>
         </div>
         <div v-if="item.extra" class="card-extra">
           {{ item.extra }}
@@ -28,46 +38,32 @@
     </section>
 
     <div class="content">
-      <div class="tabs" role="tablist" aria-label="EHR模块">
+      <div class="tabs-container" role="tablist" aria-label="EHR模块">
         <button
-          class="tab"
+          class="el-tab-item"
           type="button"
           role="tab"
-          :class="{ active: activeKey === 'all' }"
+          :class="{ 'is-active': activeKey === 'all' }"
           :aria-selected="activeKey === 'all'"
           @click="setAll"
         >
-          全部
+          <span class="tab-label">全部</span>
         </button>
         <button
           v-for="tab in tabs"
           :key="tab.groupName"
-          class="tab"
-          :class="{ active: tab.groupName === activeKey }"
+          class="el-tab-item"
+          :class="{ 'is-active': tab.groupName === activeKey }"
           type="button"
           role="tab"
           :aria-selected="tab.groupName === activeKey"
           @click="getById(tab)"
         >
-          <span class="tab-title">{{ tab.groupName }}</span>
-          <span class="tab-sub">{{ tab.remark }}</span>
+          <span class="tab-label">{{ tab.groupName }}</span>
         </button>
       </div>
 
-      <div class="panel" role="tabpanel">
-        <div class="panel-head">
-          <div>
-            <p class="panel-title">{{ activeTab.groupName }}</p>
-            <p class="panel-subtitle">{{ activeTab.remark }}</p>
-          </div>
-          <div class="panel-meta">
-            <span class="panel-count"
-              >{{ activeTab.flowRespVOS.length }} 项</span
-            >
-            <span class="panel-note">流程与表单</span>
-          </div>
-        </div>
-
+      <div role="tabpanel">
         <div class="items-grid">
           <div
             v-for="item in activeTab.flowRespVOS"
@@ -89,7 +85,12 @@
       </div>
     </div>
 
-    <!-- 修复宽度:自适应包裹按钮 -->
+    <div class="charts-container">
+      <!-- 折线图 -->
+      <div class="chart-item" ref="lineChartRef"></div>
+      <!-- 环形图 -->
+      <div class="chart-item" ref="pieChartRef"></div>
+    </div>
 
     <Footer />
   </div>
@@ -98,14 +99,173 @@
 <script setup>
 import Header from "@components/home/header.vue";
 import Footer from "@components/home/Footer.vue";
-import { computed, ref, onMounted } from "vue";
+import { computed, ref, onMounted, onBeforeUnmount, nextTick } from "vue";
 import { Icon } from "@iconify/vue";
-import { getFlows, ssoLogin } from "@/api/user";
+import { getFlows, ssoLogin, getOATasks } from "@/api/user";
 import { useUserStore } from "@/stores/useUserStore";
 import { getAccessToken } from "@/utils/auth";
+import * as echarts from "echarts";
 
 const userStore = useUserStore();
 
+const lineChartInstance = ref(null);
+let lineChartRef = ref(null);
+let pieChartRef = ref(null);
+const pieChartInstance = ref(null);
+let chartResizeObserver = null;
+let chartInitTimer = null;
+
+const initChartsSafe = (attempt = 0) => {
+  const lineDom = lineChartRef.value;
+  const pieDom = pieChartRef.value;
+
+  if (!lineDom || !pieDom) return;
+
+  const lineRect = lineDom.getBoundingClientRect();
+  const pieRect = pieDom.getBoundingClientRect();
+  const isLineReady = lineRect.width > 0 && lineRect.height > 0;
+  const isPieReady = pieRect.width > 0 && pieRect.height > 0;
+
+  if (isLineReady && isPieReady) {
+    initLineChart();
+    initPieChart();
+    handleResize();
+    return;
+  }
+
+  if (attempt < 8) {
+    chartInitTimer = window.setTimeout(() => {
+      initChartsSafe(attempt + 1);
+    }, 120);
+  }
+};
+
+// 模拟数据 - 请根据实际 API 返回的数据调整
+const lineChartData = {
+  title: "流程处理趋势 (30天)",
+  xAxis: ["03-27", "03-28", "03-29", "03-30", "03-31", "04-01", "04-02"],
+  yAxis: [12, 18, 15, 20, 27, 24, 31],
+};
+
+const pieChartData = {
+  title: "流程类型占比",
+  seriesData: [
+    { name: "财务报销", value: 35, itemStyle: { color: "#409eff" } },
+    { name: "行政办公", value: 25, itemStyle: { color: "#f56c6c" } },
+    { name: "IT技术", value: 20, itemStyle: { color: "#9a66ff" } },
+    { name: "人力资源", value: 15, itemStyle: { color: "#e6a23c" } },
+    { name: "业务申请", value: 5, itemStyle: { color: "#50c878" } },
+  ],
+};
+
+// 初始化图表
+const initLineChart = () => {
+  const chartDom = lineChartRef.value;
+  if (!chartDom) return;
+
+  const chart = echarts.init(chartDom);
+  lineChartInstance.value = chart; // 保存实例
+
+  chart.setOption({
+    title: {
+      text: lineChartData.title,
+      left: "10",
+      top: 20,
+
+      textStyle: {
+        fontSize: 16,
+        fontWeight: "bold",
+        color: "#333",
+      },
+    },
+    // ... 原有的 option 配置保持不变 ...
+    tooltip: {
+      trigger: "axis",
+      axisPointer: { type: "shadow" },
+    },
+    grid: {
+      left: "3%",
+      right: "4%",
+      bottom: "10%",
+      containLabel: true,
+    },
+    xAxis: {
+      type: "category",
+      data: lineChartData.xAxis,
+      axisLabel: { formatter: (value) => value },
+    },
+    yAxis: {
+      type: "value",
+      splitLine: { show: true, lineStyle: { color: "#eee" } },
+    },
+    series: [
+      {
+        name: "处理数量",
+        type: "line",
+        smooth: true,
+        areaStyle: {
+          opacity: 0.3,
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+            { offset: 0, color: "#409eff" },
+            { offset: 1, color: "#b3d8ff" },
+          ]),
+        },
+        lineStyle: { width: 3, color: "#2563eb" },
+        symbol: "circle",
+        symbolSize: 8,
+        data: lineChartData.yAxis,
+      },
+    ],
+  });
+};
+
+const initPieChart = () => {
+  const chartDom = pieChartRef.value;
+  if (!chartDom) return;
+
+  const chart = echarts.init(chartDom);
+  pieChartInstance.value = chart; // 保存实例
+
+  chart.setOption({
+    title: {
+      text: pieChartData.title,
+      left: "10",
+      top: 20,
+
+      textStyle: {
+        fontSize: 16,
+        fontWeight: "bold",
+        color: "#333",
+      },
+    },
+    // ... 原有的 option 配置保持不变 ...
+    tooltip: {
+      trigger: "item",
+      formatter: "{a} <br/>{b}: {c} ({d}%)",
+    },
+    legend: { show: false },
+    series: [
+      {
+        name: "流程类型",
+        type: "pie",
+        radius: ["50%", "70%"],
+        avoidLabelOverlap: false,
+        label: { show: false, position: "center" },
+        emphasis: {
+          label: { show: true, fontSize: "14", fontWeight: "bold" },
+        },
+        labelLine: { show: false },
+        data: pieChartData.seriesData,
+      },
+    ],
+  });
+};
+
+const handleResize = () => {
+  lineChartInstance.value?.resize();
+  pieChartInstance.value?.resize();
+};
+
 const tabs = ref([]);
 
 const activeKey = ref("all");
@@ -136,12 +296,14 @@ const stats = [
     number: "05",
     extra: "+2 今日",
     bgcolor: "#fff7ed",
+    color: "#f59e0b",
   },
   {
     icon: "mdi:check-circle-outline",
     title: "已办事项",
     number: "128",
     bgcolor: "#eff6ff",
+    color: "#2563eb",
   },
   {
     icon: "mdi:arrow-right-bold-box-outline",
@@ -149,12 +311,14 @@ const stats = [
     number: "42",
     extra: "85% 准时",
     bgcolor: "#eff6ff",
+    color: "#2563eb",
   },
   {
     icon: "mdi:file-document-outline",
     title: "草箱箱",
     number: "03",
-    bgcolor: "#fef3c7",
+    bgcolor: "#f8fafc",
+    color: "#475569",
   },
 ];
 
@@ -176,47 +340,95 @@ const getById = (tab) => {
 };
 
 const go = async (item) => {
-  console.log("跳转", item);
   if (userStore.getUser.username && getAccessToken()) {
-    const res = await ssoLogin({
-      username: userStore.getUser.username,
-    });
+    if (item.type === "OA") {
+      const res = await ssoLogin({
+        username: userStore.getUser.username,
+      });
 
-    if (res) {
+      if (res) {
+        const newTab = window.open("", "_blank");
+
+        newTab.location.href = item.indexUrl + "?ssoToken=" + res + "#/main";
+
+        setTimeout(function () {
+          newTab.location.href = item.flowUrl;
+        }, 0);
+      }
+    }
+
+    if (item.type === "CRM") {
       const newTab = window.open("", "_blank");
 
-      newTab.location.href = item.indexUrl + "?ssoToken=" + res + "#/main";
+      newTab.location.href =
+        item.indexUrl +
+        "/global/sso/callback/00APEB9EEEA9B2E338B686B7ECFA8585808C.action?token=" +
+        getAccessToken();
 
       setTimeout(function () {
         newTab.location.href = item.flowUrl;
-      }, 1000);
+      }, 0);
     }
   } else {
     router.push({ path: "/login" });
   }
 };
 
+let oaTasks = ref([]);
 onMounted(async () => {
   getAll();
+  // 等待 DOM 与样式生效,避免移动端首屏尺寸为 0
+  await nextTick();
+  requestAnimationFrame(() => {
+    initChartsSafe();
+    // 添加监听
+    window.addEventListener("resize", handleResize);
+    // 使用 ResizeObserver 监听容器尺寸变化(移动端更稳定)
+    if (typeof ResizeObserver !== "undefined") {
+      chartResizeObserver = new ResizeObserver(() => {
+        handleResize();
+      });
+      if (lineChartRef.value) chartResizeObserver.observe(lineChartRef.value);
+      if (pieChartRef.value) chartResizeObserver.observe(pieChartRef.value);
+    }
+  });
+
+  if (userStore.getUser.username) {
+    const res = await getOATasks(userStore.getUser.username);
+    oaTasks.value = res;
+  }
+});
+
+// 组件卸载时移除监听,防止内存泄漏
+onBeforeUnmount(() => {
+  window.removeEventListener("resize", handleResize);
+  chartResizeObserver?.disconnect();
+  if (chartInitTimer) window.clearTimeout(chartInitTimer);
+  // 可选:销毁 echarts 实例
+  lineChartInstance.value?.dispose();
+  pieChartInstance.value?.dispose();
 });
 </script>
 
 <style scoped>
-:global(body) {
-  background-color: #f4f4f2;
-}
-
 /* .ehr-page {
   color: #1f2a37;
   background: linear-gradient(180deg, #f4f4f2 0%, #f7f6f3 50%, #f2f1ef 100%);
   min-height: 100vh;
 } */
 
+:global(body) {
+  background-color: #f8fafc;
+}
+
 .hero {
   position: relative;
   padding: 72px 6vw 48px;
   overflow: hidden;
   margin-top: 20px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
 }
 
 .hero-inner {
@@ -244,11 +456,7 @@ onMounted(async () => {
   right: -140px;
   width: 360px;
   height: 360px;
-  background: radial-gradient(
-    circle at 30% 30%,
-    rgba(2, 64, 155, 0.25),
-    transparent 65%
-  );
+
   border-radius: 50%;
   opacity: 0.9;
   pointer-events: none;
@@ -259,48 +467,54 @@ onMounted(async () => {
   /* height: 80vh; */
 }
 
-.tabs {
-  display: grid;
-  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
-  gap: 16px;
-  margin-bottom: 28px;
+.tabs-container {
+  display: flex;
+  align-items: center;
+  border-bottom: 1px solid #e4e7ed; /* Element Plus 标准的分割线颜色 */
+  margin-bottom: 20px;
+  padding-left: 0;
+  overflow-x: auto; /* 防止Tab过多时溢出 */
 }
 
-.tab {
-  padding: 18px 20px;
-  border-radius: 14px;
-  /* border: 1px solid #e5e7eb; */
-  background: #ffffff;
-  text-align: left;
-  transition: all 0.25s ease;
+.el-tab-item {
+  position: relative;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  padding: 0 20px;
+  height: 40px; /* 标准高度 */
+  font-size: 14px;
+  color: #64748b; /* Element Plus 主要文字颜色 */
+  background-color: transparent;
+  border: none;
+  border-bottom: 2px solid transparent; /* 用于激活态的下划线 */
   cursor: pointer;
-  box-shadow: 0 12px 24px rgba(15, 23, 42, 0.05);
+  transition: all 0.3s;
+  margin-right: 0;
+  outline: none;
+  flex-shrink: 0; /* 防止压缩 */
 }
 
-.tab:hover {
-  transform: translateY(-2px);
-  border-color: #02409b;
+.el-tab-item:hover {
+  color: #02409b; /* Element Plus 主题蓝 */
 }
 
-.tab.active {
-  background: linear-gradient(135deg, #02409b 0%, #0b2f6d 60%, #0b1f45 100%);
-  border-color: transparent;
-  color: #f9fafb;
-  box-shadow: 0 18px 36px rgba(2, 64, 155, 0.25);
+.el-tab-item.is-active {
+  color: #02409b; /* 激活态文字颜色 */
+  font-weight: 500;
+  border-bottom-color: #02409b; /* 激活态下划线 */
 }
 
-.tab-title {
-  display: block;
-  font-weight: 600;
-  font-size: 16px;
-  margin-bottom: 6px;
+.tab-label {
+  line-height: 1;
+  font-weight: bold;
 }
 
 .tab-sub {
-  display: block;
+  margin-left: 8px;
   font-size: 12px;
-  color: inherit;
-  opacity: 0.75;
+  color: #909399; /* 次要文字颜色 */
+  transform: scale(0.9);
 }
 
 .panel {
@@ -358,18 +572,22 @@ onMounted(async () => {
   padding: 22px 22px 18px;
   border-radius: 22px;
   background: #ffffff;
-  border: 1px solid #edf0f5;
-  box-shadow: 0 14px 30px rgba(15, 23, 42, 0.06);
+  border: 1px solid rgb(241, 245, 249);
+  box-shadow:
+    rgba(0, 0, 0, 0) 0px 0px 0px 0px,
+    rgba(0, 0, 0, 0) 0px 0px 0px 0px,
+    rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
   transition:
     transform 0.2s ease,
     border-color 0.2s ease,
     box-shadow 0.2s ease;
   cursor: pointer;
+  border-bottom: 1px solid rgb(241, 245, 249);
 }
 
 .item-card:hover {
   transform: translateY(-3px);
-  border-color: rgba(2, 64, 155, 0.25);
+
   box-shadow: 0 18px 36px rgba(15, 23, 42, 0.08);
   color: #02409b !important;
 }
@@ -470,7 +688,7 @@ onMounted(async () => {
 .total {
   display: flex;
   gap: 16px;
-  padding: 0 6vw 80px;
+  padding: 0 6vw 50px;
   /* margin-bottom: 24px; */
 }
 
@@ -539,4 +757,27 @@ onMounted(async () => {
   margin-top: 8px;
   text-align: right;
 }
+
+.charts-container {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 24px;
+  padding: 0 6vw;
+  margin-bottom: 80px;
+  width: 100%;
+  box-sizing: border-box;
+}
+
+.chart-item {
+  flex: 1;
+  /* 桌面端最小宽度,防止过度挤压 */
+  min-width: 300px;
+  /* 必须设置固定高度,ECharts 需要明确的高度才能渲染 */
+  height: 350px;
+  border-radius: 16px;
+  background-color: #ffffff;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
+  overflow: hidden;
+  position: relative;
+}
 </style>

+ 2 - 2
src/views/login.vue

@@ -21,7 +21,7 @@
         <h1 class="text-2xl font-bold text-center">登录</h1>
 
         <!-- 用户名密码登陆 -->
-        <div>
+        <!-- <div>
           <el-form
             :model="form"
             :rules="rules"
@@ -62,7 +62,7 @@
               >
             </div>
           </div>
-        </div>
+        </div> -->
 
         <!-- 钉钉登陆 -->
         <div class="text-center">