liu
2026-09-29 982c97ab82f3beb1b4febcf014562eda34b9fc05
feat(ui): 江苏商贸品牌视觉与首页/登录页改版

- 新增「江苏商贸」盾形矢量 Logo(藏青+金)与校园夜景手绘插画
- 登录页改为学术典雅风格:插画铺满品牌区、文案移至左下角、
象牙白表单卡片、金色聚焦态与描边
- 首页改用系统真实数据:我的待办 / 课题申报 / 财务报销 / 课题指南,
新增待办列表与最近课题申报,快捷入口对齐实际菜单
- 移除首页 MES 生产看板模块(本系统无对应数据,原页面恒为 0)
- 清理 Vben 模板默认文案(pageTitle/pageDesc/loginSubtitle/codeSubtitle)
- 注册 JSSM 公司配置并接通 VITE_COMPANY

Co-Authored-By: Claude <noreply@anthropic.com>
已修改10个文件
已添加4个文件
已删除5个文件
2374 ■■■■■ 文件已修改
.env 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.env.development 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
company.config.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
public/logos/JSSM.svg 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
public/static/imgs/jssm-campus.svg 226 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/packages/effects/layouts/src/authentication/authentication.vue 134 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/packages/locales/src/langs/en-US/authentication.json 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/packages/locales/src/langs/zh-CN/authentication.json 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/_core/authentication/login.vue 265 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/dashboard/analytics/data.ts 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/dashboard/analytics/index.vue 159 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/dashboard/analytics/modules/ai-insights.vue 114 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/dashboard/analytics/modules/alert-panel.vue 121 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/dashboard/analytics/modules/biz-cards.vue 218 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/dashboard/analytics/modules/kpi-cards.vue 359 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/dashboard/analytics/modules/production-trend.vue 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/dashboard/analytics/modules/quick-actions.vue 129 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/dashboard/analytics/modules/recent-applications.vue 272 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/dashboard/analytics/modules/todo-panel.vue 208 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.env
@@ -1,5 +1,5 @@
# åº”用标题
VITE_APP_TITLE=工业AI智造平台
VITE_APP_TITLE=江苏商贸
# åº”用命名空间,用于缓存、store等功能的前缀,确保隔离
VITE_APP_NAMESPACE=yudao-vben-antd
.env.development
@@ -1,6 +1,12 @@
# ç«¯å£å·
VITE_PORT=5666
# åº”用标题
VITE_APP_TITLE=江苏商贸
# å½“前公司标识:决定应用名称 / Logo / å›¾æ ‡ï¼ˆè§ company.config.ts)
VITE_COMPANY=JSSM
VITE_BASE=/
# è¯·æ±‚路径
@@ -21,4 +27,4 @@
VITE_APP_DEFAULT_PASSWORD=admin123
# äº§å“è®¾è®¡æ¨¡å—本地 Mock(无后端时演示)
VITE_PD_MOCK=true
VITE_PD_MOCK=true
company.config.ts
@@ -71,4 +71,10 @@
    favicon: '/logos/YZFXLogo.ico',
    apiUrl: 'http://36.213.150.237:9006/admin-api',
  },
  JSSM: {
    name: '江苏商贸',
    logo: '/logos/JSSM.svg',
    favicon: '/logos/JSSM.svg',
    apiUrl: '',
  },
};
public/logos/JSSM.svg
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,38 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" role="img" aria-label="江苏商贸">
  <defs>
    <!-- è—é’主色:左上角略亮,右下角沉下去,形成体积感 -->
    <linearGradient id="jssmNavy" x1="0" y1="0" x2="0.85" y2="1">
      <stop offset="0%" stop-color="#1E4571"/>
      <stop offset="52%" stop-color="#123156"/>
      <stop offset="100%" stop-color="#0A1D35"/>
    </linearGradient>
    <!-- é‡‘色:上亮下深,模拟金属光泽 -->
    <linearGradient id="jssmGold" x1="0" y1="0" x2="0" y2="1">
      <stop offset="0%" stop-color="#EBD07E"/>
      <stop offset="45%" stop-color="#D4AF37"/>
      <stop offset="100%" stop-color="#B8901F"/>
    </linearGradient>
  </defs>
  <!-- ç›¾å½¢å¾½æ ‡ä¸»ä½“ -->
  <path d="M8 14 A6 6 0 0 1 14 8 H50 A6 6 0 0 1 56 14 V32 C56 46 45.5 55.5 32 60 C18.5 55.5 8 46 8 32 Z"
        fill="url(#jssmNavy)"/>
  <!-- å¤–圈金边:深色背景下靠它勾勒轮廓 -->
  <path d="M8 14 A6 6 0 0 1 14 8 H50 A6 6 0 0 1 56 14 V32 C56 46 45.5 55.5 32 60 C18.5 55.5 8 46 8 32 Z"
        fill="none" stroke="url(#jssmGold)" stroke-width="1.5" opacity="0.9"/>
  <!-- å†…圈金线:增加徽章的层次 -->
  <path d="M12.5 15 A3.5 3.5 0 0 1 16 11.5 H48 A3.5 3.5 0 0 1 51.5 15 V32 C51.5 44 42.5 52.5 32 56.5 C21.5 52.5 12.5 44 12.5 32 Z"
        fill="none" stroke="url(#jssmGold)" stroke-width="1" opacity="0.55"/>
  <!-- é¡¶éƒ¨å­¦æœ¯ä¹‹æ˜Ÿ -->
  <path d="M32 15.6 L33.9 19 L32 22.4 L30.1 19 Z" fill="url(#jssmGold)"/>
  <!-- å±•开的书本:左页 -->
  <path d="M31.2 29.5 C27.4 26.1 22.2 24.6 17.8 25.5 V41.4 C22.2 40.5 27.4 42 31.2 45.4 Z"
        fill="url(#jssmGold)"/>
  <!-- å±•开的书本:右页(略暗,拉开左右层次) -->
  <path d="M32.8 29.5 C36.6 26.1 41.8 24.6 46.2 25.5 V41.4 C41.8 40.5 36.6 42 32.8 45.4 Z"
        fill="url(#jssmGold)" opacity="0.78"/>
  <!-- ä¹¦è„Š -->
  <path d="M32 29.5 V45.4" stroke="#0A1D35" stroke-width="1.6" stroke-linecap="round" opacity="0.55"/>
</svg>
public/static/imgs/jssm-campus.svg
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,226 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 900" preserveAspectRatio="xMidYMid slice" role="img" aria-label="学术校园夜景">
  <defs>
    <!-- å¤œç©ºï¼šä¸Šæ·±ä¸‹æµ…的藏青 -->
    <linearGradient id="sky" x1="0" y1="0" x2="0.25" y2="1">
      <stop offset="0%" stop-color="#08182C"/>
      <stop offset="42%" stop-color="#0F2440"/>
      <stop offset="72%" stop-color="#14375F"/>
      <stop offset="100%" stop-color="#0B1C33"/>
    </linearGradient>
    <!-- æœˆå…‰å…‰æ™• -->
    <radialGradient id="moonGlow" cx="0.5" cy="0.5" r="0.5">
      <stop offset="0%" stop-color="#EBD07E" stop-opacity="0.30"/>
      <stop offset="45%" stop-color="#D4AF37" stop-opacity="0.10"/>
      <stop offset="100%" stop-color="#D4AF37" stop-opacity="0"/>
    </radialGradient>
    <!-- å»ºç­‘受光面:左亮右暗 -->
    <linearGradient id="facade" x1="0" y1="0" x2="1" y2="0.35">
      <stop offset="0%" stop-color="#1E4571"/>
      <stop offset="55%" stop-color="#16375C"/>
      <stop offset="100%" stop-color="#0E2743"/>
    </linearGradient>
    <!-- ç©¹é¡¶ -->
    <linearGradient id="dome" x1="0.15" y1="0" x2="0.85" y2="1">
      <stop offset="0%" stop-color="#2A5B8C"/>
      <stop offset="60%" stop-color="#17395E"/>
      <stop offset="100%" stop-color="#0D2340"/>
    </linearGradient>
    <!-- é‡‘色窗光 -->
    <linearGradient id="winLight" x1="0" y1="0" x2="0" y2="1">
      <stop offset="0%" stop-color="#EBD07E"/>
      <stop offset="100%" stop-color="#C9A227"/>
    </linearGradient>
    <!-- æ°´é¢ -->
    <linearGradient id="pool" x1="0" y1="0" x2="0" y2="1">
      <stop offset="0%" stop-color="#0C2036"/>
      <stop offset="100%" stop-color="#061320"/>
    </linearGradient>
    <!-- æ°´é¢é‡‘色倒影 -->
    <linearGradient id="reflect" x1="0" y1="0" x2="0" y2="1">
      <stop offset="0%" stop-color="#D4AF37" stop-opacity="0.22"/>
      <stop offset="100%" stop-color="#D4AF37" stop-opacity="0"/>
    </linearGradient>
    <!-- åº•部压暗,给文字留出可读区域 -->
    <linearGradient id="vignette" x1="0" y1="0" x2="0" y2="1">
      <stop offset="0%" stop-color="#08182C" stop-opacity="0"/>
      <stop offset="62%" stop-color="#08182C" stop-opacity="0.35"/>
      <stop offset="100%" stop-color="#08182C" stop-opacity="0.85"/>
    </linearGradient>
  </defs>
  <!-- ===== å¤œç©º ===== -->
  <rect width="900" height="900" fill="url(#sky)"/>
  <!-- æœˆäº®ä¸Žå…‰æ™• -->
  <circle cx="686" cy="176" r="210" fill="url(#moonGlow)"/>
  <circle cx="686" cy="176" r="46" fill="#EBD07E" opacity="0.92"/>
  <circle cx="686" cy="176" r="46" fill="none" stroke="#F5E7C0" stroke-width="1" opacity="0.5"/>
  <!-- æœˆé¢æš—部,让它不是纯色圆 -->
  <circle cx="672" cy="164" r="38" fill="#D4AF37" opacity="0.18"/>
  <circle cx="700" cy="188" r="14" fill="#08182C" opacity="0.12"/>
  <circle cx="672" cy="196" r="9" fill="#08182C" opacity="0.10"/>
  <!-- æ˜Ÿç‚¹ -->
  <g fill="#EBD07E">
    <circle cx="120" cy="96" r="1.8" opacity="0.55"/>
    <circle cx="238" cy="152" r="1.3" opacity="0.42"/>
    <circle cx="352" cy="82" r="2.1" opacity="0.6"/>
    <circle cx="470" cy="134" r="1.4" opacity="0.4"/>
    <circle cx="560" cy="72" r="1.7" opacity="0.5"/>
    <circle cx="800" cy="118" r="1.5" opacity="0.45"/>
    <circle cx="176" cy="228" r="1.2" opacity="0.32"/>
    <circle cx="420" cy="206" r="1.6" opacity="0.44"/>
    <circle cx="742" cy="268" r="1.3" opacity="0.34"/>
    <circle cx="86" cy="330" r="1.5" opacity="0.3"/>
    <circle cx="598" cy="184" r="1.1" opacity="0.3"/>
    <circle cx="300" cy="286" r="1.2" opacity="0.28"/>
  </g>
  <!-- ===== è¿œå±± ===== -->
  <path d="M0 556 L118 484 L214 534 L322 452 L430 528 L548 470 L662 540 L784 486 L900 552 L900 900 L0 900 Z"
        fill="#0B1E36" opacity="0.75"/>
  <!-- ===== è¿œæ™¯æ¥¼ç¾¤ ===== -->
  <g fill="#0D2440" opacity="0.85">
    <rect x="42" y="520" width="46" height="72"/>
    <rect x="98" y="546" width="34" height="46"/>
    <rect x="748" y="530" width="42" height="62"/>
    <rect x="800" y="556" width="30" height="36"/>
  </g>
  <!-- ===== ä¸»æ¥¼ï¼šå°é˜¶ ===== -->
  <g>
    <rect x="238" y="742" width="424" height="13" fill="#132E4E"/>
    <rect x="256" y="729" width="388" height="13" fill="#17395E"/>
    <rect x="274" y="716" width="352" height="13" fill="#1B4168"/>
  </g>
  <!-- ===== ä¸»æ¥¼ï¼šæŸ±å»Š ===== -->
  <rect x="286" y="560" width="328" height="156" fill="url(#facade)"/>
  <!-- ===== å±‹é¡¶å±±èŠ±ï¼ˆä¸‰è§’æ¥£ï¼‰ ===== -->
  <path d="M286 560 L450 470 L614 560 Z" fill="#17395E"/>
  <path d="M286 560 L450 470 L614 560 Z" fill="none" stroke="#D4AF37" stroke-width="2" opacity="0.55"/>
  <rect x="272" y="556" width="356" height="12" fill="#1B4168"/>
  <!-- å±±èŠ±å†…çš„åœ†å¾½ -->
  <circle cx="450" cy="528" r="24" fill="#0E2743" stroke="#D4AF37" stroke-width="1.6" opacity="0.9"/>
  <circle cx="450" cy="528" r="13" fill="none" stroke="#D4AF37" stroke-width="1.2" opacity="0.65"/>
  <circle cx="450" cy="528" r="4.5" fill="#D4AF37" opacity="0.85"/>
  <!-- ===== ç©¹é¡¶ ===== -->
  <path d="M450 470 A62 62 0 0 1 512 470 Z" fill="url(#dome)" transform="translate(-62,0)"/>
  <path d="M388 470 A62 62 0 0 1 512 470 Z" fill="url(#dome)"/>
  <path d="M388 470 A62 62 0 0 1 512 470" fill="none" stroke="#D4AF37" stroke-width="1.6" opacity="0.6"/>
  <rect x="444" y="382" width="12" height="28" fill="#17395E"/>
  <circle cx="450" cy="376" r="8" fill="#D4AF37" opacity="0.9"/>
  <!-- å¡”尖光点 -->
  <circle cx="450" cy="376" r="18" fill="#EBD07E" opacity="0.16"/>
  <!-- ===== å…­æ ¹ç«‹æŸ± ===== -->
  <g fill="#1B4168">
    <rect x="302" y="578" width="26" height="138"/>
    <rect x="356" y="578" width="26" height="138"/>
    <rect x="410" y="578" width="26" height="138"/>
    <rect x="464" y="578" width="26" height="138"/>
    <rect x="518" y="578" width="26" height="138"/>
    <rect x="572" y="578" width="26" height="138"/>
  </g>
  <!-- æŸ±å¤´ä¸ŽæŸ±ç¡€ -->
  <g fill="#24507C">
    <rect x="298" y="572" width="34" height="10"/>
    <rect x="352" y="572" width="34" height="10"/>
    <rect x="406" y="572" width="34" height="10"/>
    <rect x="460" y="572" width="34" height="10"/>
    <rect x="514" y="572" width="34" height="10"/>
    <rect x="568" y="572" width="34" height="10"/>
    <rect x="300" y="708" width="30" height="10"/>
    <rect x="354" y="708" width="30" height="10"/>
    <rect x="408" y="708" width="30" height="10"/>
    <rect x="462" y="708" width="30" height="10"/>
    <rect x="516" y="708" width="30" height="10"/>
    <rect x="570" y="708" width="30" height="10"/>
  </g>
  <!-- ===== æŸ±é—´çª—å…‰ ===== -->
  <g fill="url(#winLight)">
    <rect x="332" y="612" width="20" height="34" opacity="0.82"/>
    <rect x="386" y="612" width="20" height="34" opacity="0.62"/>
    <rect x="440" y="612" width="20" height="34" opacity="0.9"/>
    <rect x="494" y="612" width="20" height="34" opacity="0.58"/>
    <rect x="548" y="612" width="20" height="34" opacity="0.78"/>
  </g>
  <g fill="url(#winLight)">
    <rect x="332" y="662" width="20" height="30" opacity="0.55"/>
    <rect x="386" y="662" width="20" height="30" opacity="0.72"/>
    <rect x="440" y="662" width="20" height="30" opacity="0.6"/>
    <rect x="494" y="662" width="20" height="30" opacity="0.8"/>
    <rect x="548" y="662" width="20" height="30" opacity="0.5"/>
  </g>
  <!-- ä¸­å¤®å¤§é—¨ -->
  <path d="M432 716 L432 676 A18 18 0 0 1 468 676 L468 716 Z" fill="#0E2743"/>
  <path d="M432 716 L432 676 A18 18 0 0 1 468 676 L468 716" fill="none" stroke="#D4AF37" stroke-width="1.4" opacity="0.6"/>
  <rect x="448" y="676" width="1.6" height="40" fill="#D4AF37" opacity="0.45"/>
  <!-- é—¨å‰çš„两盏壁灯 -->
  <circle cx="414" cy="700" r="5" fill="#EBD07E" opacity="0.9"/>
  <circle cx="414" cy="700" r="14" fill="#EBD07E" opacity="0.16"/>
  <circle cx="486" cy="700" r="5" fill="#EBD07E" opacity="0.9"/>
  <circle cx="486" cy="700" r="14" fill="#EBD07E" opacity="0.16"/>
  <!-- ===== ä¸¤ä¾§æ ‘木 ===== -->
  <g fill="#0A1B2F">
    <!-- å·¦æ ‘ -->
    <rect x="146" y="700" width="10" height="60"/>
    <ellipse cx="151" cy="666" rx="52" ry="42"/>
    <ellipse cx="120" cy="690" rx="34" ry="28"/>
    <ellipse cx="182" cy="688" rx="30" ry="26"/>
    <!-- å³æ ‘ -->
    <rect x="742" y="704" width="10" height="56"/>
    <ellipse cx="747" cy="672" rx="48" ry="40"/>
    <ellipse cx="718" cy="694" rx="32" ry="26"/>
    <ellipse cx="776" cy="692" rx="28" ry="24"/>
  </g>
  <!-- æ ‘冠受光边 -->
  <g fill="none" stroke="#D4AF37" stroke-width="1.1" opacity="0.18">
    <ellipse cx="151" cy="666" rx="52" ry="42"/>
    <ellipse cx="747" cy="672" rx="48" ry="40"/>
  </g>
  <!-- ===== å‰æ™¯æ°´é¢ ===== -->
  <rect x="0" y="755" width="900" height="145" fill="url(#pool)"/>
  <rect x="0" y="755" width="900" height="1.6" fill="#D4AF37" opacity="0.22"/>
  <!-- å»ºç­‘在水面的倒影 -->
  <g opacity="0.5">
    <rect x="300" y="758" width="300" height="66" fill="url(#facade)" opacity="0.35"/>
    <rect x="330" y="758" width="18" height="52" fill="#D4AF37" opacity="0.16"/>
    <rect x="384" y="758" width="18" height="52" fill="#D4AF37" opacity="0.12"/>
    <rect x="438" y="758" width="18" height="52" fill="#D4AF37" opacity="0.2"/>
    <rect x="492" y="758" width="18" height="52" fill="#D4AF37" opacity="0.12"/>
    <rect x="546" y="758" width="18" height="52" fill="#D4AF37" opacity="0.16"/>
  </g>
  <rect x="0" y="755" width="900" height="120" fill="url(#reflect)"/>
  <!-- æ°´é¢æ³¢çº¹ -->
  <g stroke="#EBD07E" stroke-width="1.2" opacity="0.16" stroke-linecap="round">
    <line x1="286" y1="792" x2="356" y2="792"/>
    <line x1="404" y1="806" x2="490" y2="806"/>
    <line x1="520" y1="786" x2="596" y2="786"/>
    <line x1="330" y1="826" x2="420" y2="826"/>
    <line x1="470" y1="842" x2="556" y2="842"/>
    <line x1="180" y1="812" x2="238" y2="812"/>
    <line x1="660" y1="820" x2="726" y2="820"/>
  </g>
  <!-- ===== åº•部压暗 ===== -->
  <rect width="900" height="900" fill="url(#vignette)"/>
</svg>
src/packages/effects/layouts/src/authentication/authentication.vue
@@ -7,7 +7,6 @@
import { Copyright } from '../basic/copyright';
import AuthenticationFormView from './form.vue';
import SloganIcon from './icons/slogan.vue';
import Toolbar from './toolbar.vue';
interface Props {
@@ -103,34 +102,39 @@
      </div>
    </slot>
    <!-- ç³»ç»Ÿä»‹ç» -->
    <!-- ç³»ç»Ÿä»‹ç»ï¼šæ ¡å›­å¤œæ™¯æ’画铺满 + å·¦ä¸‹è§’品牌文案 -->
    <div v-if="!authPanelCenter" class="relative hidden w-0 flex-1 lg:block">
      <div
        class="absolute inset-0 size-full bg-background-deep dark:bg-[#070709]"
      >
        <div class="login-background absolute top-0 left-0 size-full"></div>
        <!-- æ‰‹ç»˜æ ¡å›­æ’画;外部传入 sloganImage æ—¶ä¼˜å…ˆç”¨å¤–部图 -->
        <img
          v-if="sloganImage"
          alt=""
          class="campus-illustration"
          :src="sloganImage"
        />
        <img
          v-else
          alt=""
          class="campus-illustration"
          src="/static/imgs/jssm-campus.svg"
        />
        <!-- åŽ‹æš—å å±‚ï¼šåº•éƒ¨åŠ é‡ï¼Œä¿è¯æ–‡å­—å¯è¯» -->
        <div class="campus-overlay"></div>
        <div class="academic-bg-pattern"></div>
        <div
          :key="authPanelLeft ? 'left' : authPanelRight ? 'right' : 'center'"
          class="mr-20 flex-col-center h-full"
          class="campus-content"
          :class="{
            'enter-x': authPanelLeft,
            '-enter-x': authPanelRight,
          }"
        >
          <template v-if="sloganImage">
            <img
              :alt="appName"
              :src="sloganImage"
              class="h-64 w-2/5 animate-float"
            />
          </template>
          <SloganIcon v-else :alt="appName" class="h-64 w-2/5 animate-float" />
          <div class="text-1xl mt-6 font-sans text-foreground lg:text-2xl">
            {{ pageTitle }}
          </div>
          <div class="mt-2 dark:text-muted-foreground">
            {{ pageDescription }}
          </div>
          <div class="campus-rule"></div>
          <div class="academic-brand-title">{{ pageTitle }}</div>
          <div class="academic-brand-desc">{{ pageDescription }}</div>
        </div>
      </div>
    </div>
@@ -138,6 +142,8 @@
    <!-- ä¸­å¿ƒè®¤è¯é¢æ¿ -->
    <div v-if="authPanelCenter" class="relative flex-center w-full">
      <div class="login-background absolute top-0 left-0 size-full"></div>
      <!-- å­¦æœ¯é£Žè£…饰网格背景 -->
      <div class="academic-bg-pattern"></div>
      <AuthenticationFormView
        class="w-full rounded-3xl pb-20 shadow-float shadow-primary/5 md:w-2/3 md:bg-background lg:w-1/2 xl:w-[36%]"
        data-side="bottom"
@@ -172,25 +178,85 @@
</template>
<style scoped>
.login-background {
  background: linear-gradient(
    154deg,
    #07070915 30%,
    hsl(var(--primary) / 30%) 48%,
    #07070915 64%
  );
  filter: blur(100px);
/* æ ¡å›­æ’画铺满左侧品牌区;object-position ç•¥åä¸Šï¼Œä¿ä½å»ºç­‘主体 */
.campus-illustration {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 34%;
}
/* å å±‚:底部压暗给文字,左侧略暗以突出白色标题 */
.campus-overlay {
  position: absolute;
  inset: 0;
  background:
    linear-gradient(
      180deg,
      rgba(8, 24, 44, 0.12) 0%,
      rgba(8, 24, 44, 0.22) 42%,
      rgba(8, 24, 44, 0.86) 100%
    ),
    linear-gradient(
      90deg,
      rgba(8, 24, 44, 0.4) 0%,
      rgba(8, 24, 44, 0) 58%
    );
}
/* è£…饰网格:压到几乎不可见的金色细线 */
.academic-bg-pattern {
  position: absolute;
  inset: 0;
  background-image:
    linear-gradient(rgba(212, 175, 55, 0.03) 1px, transparent 1px),
    linear-gradient(90deg, rgba(212, 175, 55, 0.03) 1px, transparent 1px);
  background-size: 72px 72px;
  pointer-events: none;
}
/* å“ç‰Œæ–‡æ¡ˆï¼šå®šä½åˆ°å·¦ä¸‹è§’ */
.campus-content {
  position: absolute;
  bottom: 68px;
  left: 56px;
  z-index: 10;
  max-width: 64%;
}
/* æ ‡é¢˜ä¸Šæ–¹çš„金色短横线 */
.campus-rule {
  width: 46px;
  height: 2px;
  margin-bottom: 20px;
  border-radius: 2px;
  background: linear-gradient(90deg, #e3c567, #b8901f);
}
/* å“ç‰ŒåŒºæ ‡é¢˜ï¼šç±³ç™½ï¼Œé˜´å½±æ”¶æ•›ä»¥ä¿æŒåº„重 */
.academic-brand-title {
  color: #f5f0e4 !important;
  font-size: 26px;
  font-weight: 600;
  letter-spacing: 5px;
  text-shadow: 0 1px 10px rgba(0, 0, 0, 0.4);
}
.academic-brand-desc {
  margin-top: 12px;
  color: rgba(212, 175, 55, 0.85) !important;
  font-size: 13.5px;
  letter-spacing: 3px;
}
.dark {
  .login-background {
    background: linear-gradient(
      154deg,
      #07070915 30%,
      hsl(var(--primary) / 20%) 48%,
      #07070915 64%
    );
    filter: blur(100px);
  .academic-bg-pattern {
    background-image:
      linear-gradient(rgba(212, 175, 55, 0.045) 1px, transparent 1px),
      linear-gradient(90deg, rgba(212, 175, 55, 0.045) 1px, transparent 1px);
    background-size: 72px 72px;
  }
}
</style>
src/packages/locales/src/langs/en-US/authentication.json
@@ -1,10 +1,10 @@
{
  "welcomeBack": "Welcome Back",
  "pageTitle": "Plug-and-play Admin system",
  "pageDesc": "Efficient, versatile frontend template",
  "pageTitle": "Unified Management Platform",
  "pageDesc": "Collaboration Â· Data-driven Â· Standardized Process",
  "loginSuccess": "Login Successful",
  "loginSuccessDesc": "Welcome Back",
  "loginSubtitle": "Enter your account details to manage your projects",
  "loginSubtitle": "Enter your account details to sign in",
  "selectAccount": "Quick Select Account",
  "username": "Username",
  "password": "Password",
@@ -46,7 +46,7 @@
  "githubLogin": "Github Login",
  "googleLogin": "Google Login",
  "dingdingLogin": "Dingding Login",
  "codeSubtitle": "Enter your phone number to start managing your project",
  "codeSubtitle": "Enter your mobile number to sign in",
  "code": "Security code",
  "codeTip": "Security code required {0} characters",
  "mobile": "Mobile",
@@ -67,4 +67,4 @@
    "alignLeft": "Align Left",
    "alignRight": "Align Right"
  }
}
}
src/packages/locales/src/langs/zh-CN/authentication.json
@@ -1,10 +1,10 @@
{
  "welcomeBack": "欢迎回来",
  "pageTitle": "开箱即用的大型中后台管理系统",
  "pageDesc": "工程化、高性能、跨组件库的前端模版",
  "pageTitle": "统一管理平台",
  "pageDesc": "协同办公 Â· æ•°æ®å†³ç­– Â· æµç¨‹è§„范",
  "loginSuccess": "登录成功",
  "loginSuccessDesc": "欢迎回来",
  "loginSubtitle": "请输入您的账户信息以开始管理您的项目",
  "loginSubtitle": "请输入账号信息登录统一管理平台",
  "selectAccount": "快速选择账号",
  "username": "账号",
  "password": "密码",
@@ -46,7 +46,7 @@
  "githubLogin": "Github登录",
  "googleLogin": "Google登录",
  "dingdingLogin": "钉钉登录",
  "codeSubtitle": "请输入您的手机号码以开始管理您的项目",
  "codeSubtitle": "请输入手机号码登录统一管理平台",
  "code": "验证码",
  "codeTip": "请输入{0}位验证码",
  "mobile": "手机号码",
@@ -67,4 +67,4 @@
    "alignLeft": "居左",
    "alignRight": "居右"
  }
}
}
src/views/_core/authentication/login.vue
@@ -22,6 +22,9 @@
defineOptions({ name: 'Login' });
// vite.config.ts æ³¨å…¥çš„公司 Logo è·¯å¾„
declare const __APP_LOGO__: string;
const authStore = useAuthStore();
const accessStore = useAccessStore();
const tenantEnable = isTenantEnable();
@@ -31,6 +34,12 @@
const verifyRef = ref();
const captchaType = 'blockPuzzle'; // éªŒè¯ç ç±»åž‹ï¼š'blockPuzzle' | 'clickWord'
/** åº”用标题(登录页品牌文字) */
const appTitle = import.meta.env.VITE_APP_TITLE || '江苏商贸';
/** å“ç‰Œå¾½æ ‡ï¼šå¤ç”¨ vite æ³¨å…¥çš„公司 Logo,缺省回落到内置图标 */
const brandLogo = __APP_LOGO__ || '/logos/JSSM.svg';
/** èŽ·å–ç§Ÿæˆ·åˆ—è¡¨ */
const tenantList = ref<AuthApi.TenantResult[]>([]); // ç§Ÿæˆ·åˆ—表
@@ -107,25 +116,6 @@
const formSchema = computed((): VbenFormSchema[] => {
  return [
    // {
    //   component: 'VbenSelect',
    //   componentProps: {
    //     options: tenantList.value.map((item) => ({
    //       label: item.name,
    //       value: item.id.toString(),
    //     })),
    //     placeholder: $t('authentication.tenantTip'),
    //   },
    //   fieldName: 'tenantId',
    //   label: $t('authentication.tenant'),
    //   dependencies: {
    //     triggerFields: ['tenantId'],
    //     if: tenantEnable,
    //     trigger(values) {
    //       accessStore.setTenantId(values.tenantId ? Number(values.tenantId) : null);
    //     },
    //   },
    // },
    {
      component: 'VbenInput',
      componentProps: {
@@ -155,7 +145,14 @@
</script>
<template>
  <div>
  <div class="academic-login-wrapper">
    <!-- é¡¶éƒ¨å“ç‰Œæ ‡è¯†ï¼šå¾½æ ‡ + å¹³å°å + é‡‘色分隔线 -->
    <div class="academic-login-header">
      <img :alt="appTitle" :src="brandLogo" class="academic-brand-logo" />
      <h2 class="academic-brand-name">{{ appTitle }}</h2>
      <div class="academic-decoration-line"></div>
    </div>
    <AuthenticationLogin
      ref="loginRef"
      :form-schema="formSchema"
@@ -166,29 +163,39 @@
      :show-register="false"
      :show-forget-password="false"
      :show-doc-link="false"
      class="academic-login-form"
      @submit="handleLogin"
    >
      <!-- è‡ªå®šä¹‰æ ‡é¢˜ï¼šè¦†ç›–欢迎语 -->
      <template #title>
        <div class="academic-title-area">
          <h3 class="academic-title">账号登录</h3>
          <p class="academic-subtitle">{{ $t('authentication.loginSubtitle') }}</p>
        </div>
      </template>
      <!-- ç¬¬ä¸‰æ–¹ç™»å½• -->
      <template #third-party-login>
        <div class="mt-4">
          <div class="mb-4 flex items-center justify-between">
            <span class="w-[35%] border-b border-input dark:border-gray-600"></span>
            <span class="text-center text-xs text-muted-foreground">
              å…¶ä»–登录方式
            </span>
            <span class="w-[35%] border-b border-input dark:border-gray-600"></span>
        <div class="academic-third-party">
          <div class="academic-divider">
            <span class="academic-divider-line"></span>
            <span class="academic-divider-text">其他登录方式</span>
            <span class="academic-divider-line"></span>
          </div>
          <VbenButton
            class="w-full"
            class="academic-wxcp-btn"
            variant="outline"
            :loading="wxcpLoading"
            @click="handleWxcpLogin"
          >
            <SvgWeChatIcon class="mr-2 size-5" />
            <SvgWeChatIcon class="mr-2 size-4" />
            ä¼ä¸šå¾®ä¿¡ç™»å½•
          </VbenButton>
        </div>
      </template>
    </AuthenticationLogin>
    <!-- éªŒè¯ç ï¼ˆæ»‘块拼图),captchaEnable æ—¶å¯ç”¨ -->
    <Verification
      ref="verifyRef"
      v-if="captchaEnable"
@@ -201,3 +208,203 @@
    />
  </div>
</template>
<style scoped>
/* ========== å­¦æœ¯ç§‘研风格登录页样式 ========== */
.academic-login-wrapper {
  @apply relative flex flex-col w-full;
  min-height: 460px;
}
/* ---- é¡¶éƒ¨å“ç‰Œæ ‡è¯† ---- */
.academic-login-header {
  @apply flex flex-col items-center mb-8;
}
.academic-brand-logo {
  width: 52px;
  height: 52px;
  margin-bottom: 16px;
  filter: drop-shadow(0 3px 10px rgba(15, 36, 64, 0.14));
}
.academic-brand-name {
  font-size: 19px;
  font-weight: 600;
  letter-spacing: 5px;
  /* æœ«å­—右侧会多出一份字距,用等量缩进抵消,视觉才真正居中 */
  text-indent: 5px;
  color: #0f2440;
  margin: 0 0 14px;
}
.academic-decoration-line {
  position: relative;
  width: 58px;
  height: 2px;
  background: linear-gradient(
    90deg,
    transparent,
    #d4af37 32%,
    #d4af37 68%,
    transparent
  );
  border-radius: 2px;
}
/* é‡‘线中央的菱形点缀 */
.academic-decoration-line::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: 6px;
  height: 6px;
  background: #d4af37;
  border-radius: 1px;
  transform: translate(-50%, -50%) rotate(45deg);
}
/* ---- è‡ªå®šä¹‰æ ‡é¢˜ ---- */
.academic-title-area {
  @apply text-left mb-6;
}
.academic-title {
  font-size: 26px;
  font-weight: 600;
  color: #0f2440;
  letter-spacing: 2px;
  margin: 0 0 8px 0;
  line-height: 1.35;
}
.academic-subtitle {
  font-size: 13.5px;
  color: #8a94a6;
  margin: 0;
  letter-spacing: 0.3px;
}
/* ---- è¦†ç›–默认标题 ---- */
:deep(.academic-login-form .mb-7) {
  display: none;
}
/* ---- è¾“入框:暖白底 + é‡‘色聚焦 ---- */
:deep(.academic-login-form .vben-input) {
  border-radius: 8px;
  border-color: #e6e2d8;
  background: #fdfcfa;
  transition: all 0.25s ease;
  height: 48px;
}
:deep(.academic-login-form .vben-input:hover) {
  border-color: #c9b77a;
}
:deep(.academic-login-form .vben-input:focus-within) {
  border-color: #d4af37;
  background: #fff;
  box-shadow: 0 0 0 3px rgba(212, 175, 55, 0.13);
}
/* ---- ç™»å½•按钮:藏青渐变 + é‡‘色描边与文字 ---- */
:deep(.academic-login-form .w-full > button),
:deep(.academic-login-form button[aria-label="login"]) {
  height: 48px;
  border-radius: 8px;
  background: linear-gradient(135deg, #1e4571, #0f2440) !important;
  border: 1px solid rgba(212, 175, 55, 0.45) !important;
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 6px;
  text-indent: 6px;
  box-shadow: 0 6px 18px rgba(15, 36, 64, 0.22);
  transition: all 0.3s ease;
  color: #f5e7c0 !important;
}
/* æ‚¬åœåªåŠ æ·±æè¾¹ä¸ŽæŠ•å½±ï¼Œä¸åšä½ç§»ï¼Œä¿æŒå…‹åˆ¶ */
:deep(.academic-login-form .w-full > button:hover),
:deep(.academic-login-form button[aria-label="login"]:hover) {
  box-shadow:
    0 8px 22px rgba(15, 36, 64, 0.26),
    0 0 0 1px rgba(212, 175, 55, 0.6);
}
:deep(.academic-login-form .w-full > button:active),
:deep(.academic-login-form button[aria-label="login"]:active) {
  box-shadow: 0 3px 12px rgba(15, 36, 64, 0.24);
}
/* ---- è®°ä½è´¦å· / å¿˜è®°å¯†ç  ---- */
:deep(.academic-login-form .vben-link) {
  color: #b8901f;
  font-weight: 500;
  transition: color 0.2s;
}
:deep(.academic-login-form .vben-link:hover) {
  color: #8a6a12;
}
/* ---- ç¬¬ä¸‰æ–¹ç™»å½• ---- */
.academic-third-party {
  @apply mt-5;
}
.academic-divider {
  @apply flex items-center gap-3 mb-4;
}
.academic-divider-line {
  flex: 1;
  height: 1px;
  background: linear-gradient(90deg, transparent, #e2e8f0, transparent);
}
.academic-divider-text {
  @apply text-xs;
  color: #94a3b8;
  white-space: nowrap;
  letter-spacing: 0.5px;
}
.academic-wxcp-btn {
  @apply w-full rounded-lg;
  height: 44px;
  border-color: #e6e2d8;
  color: #44546a;
  font-weight: 500;
  transition: all 0.3s;
}
.academic-wxcp-btn:hover {
  border-color: #d4af37;
  color: #0f2440;
  background: rgba(212, 175, 55, 0.06);
}
/* ---- æš—色适配 ---- */
:root.dark .academic-title {
  color: #f0ead8;
}
:root.dark .academic-brand-name {
  color: #f0ead8;
}
:root.dark .academic-subtitle {
  color: #8a94a6;
}
:root.dark .academic-decoration-line {
  background: linear-gradient(90deg, #8bb4f0, #4a7ad4);
}
:root.dark .academic-footer-text {
  color: #64748b;
}
</style>
src/views/dashboard/analytics/data.ts
ÎļþÒÑɾ³ý
src/views/dashboard/analytics/index.vue
@@ -1,25 +1,23 @@
<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue';
import { computed } from 'vue';
import { useRouter } from 'vue-router';
import { Page } from '@vben/common-ui';
import { Card, Col, Row } from 'ant-design-vue';
import { Col, Row } from 'ant-design-vue';
import { getHomeSummary } from '#/api/mes/home';
import { defaultSummary } from './data';
import AiInsights from './modules/ai-insights.vue';
import AlertPanel from './modules/alert-panel.vue';
import BizCards from './modules/biz-cards.vue';
import KpiCards from './modules/kpi-cards.vue';
import ProductionTrend from './modules/production-trend.vue';
import QuickActions from './modules/quick-actions.vue';
import RecentApplications from './modules/recent-applications.vue';
import TodoPanel from './modules/todo-panel.vue';
defineOptions({ name: 'DashboardAnalytics' });
// vite.config.ts æ³¨å…¥çš„公司 Logo è·¯å¾„
declare const __APP_LOGO__: string;
const brandLogo = __APP_LOGO__ || '/logos/JSSM.svg';
const router = useRouter();
const summary = ref(defaultSummary);
const greeting = computed(() => {
  const hour = new Date().getHours();
@@ -40,94 +38,103 @@
function handleNavigate(name: string) {
  router.push({ name });
}
async function loadSummary() {
  summary.value = await getHomeSummary();
}
onMounted(() => {
  loadSummary();
});
</script>
<template>
  <Page auto-content-height>
    <!-- æ¬¢è¿Žæ  -->
    <div class="glass-card mb-6 flex items-center justify-between rounded-2xl p-6">
      <div>
        <h1 class="bg-gradient-to-r from-blue-600 via-purple-600 to-pink-500 bg-clip-text text-2xl font-bold text-transparent dark:from-blue-400 dark:via-purple-400 dark:to-pink-400">
          {{ greeting }},欢迎回来
        </h1>
        <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">{{ today }}</p>
      </div>
      <div class="hidden items-center gap-2 sm:flex">
        <div class="flex items-center gap-1.5 rounded-full bg-emerald-50 px-3 py-1.5 text-xs font-medium text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-400">
          <span class="relative flex size-2">
            <span class="absolute inline-flex size-full animate-ping rounded-full bg-emerald-400 opacity-75" />
            <span class="relative inline-flex size-2 rounded-full bg-emerald-500" />
          </span>
    <!-- æ¬¢è¿Žæ ï¼šè—é’底 + é‡‘色描边 -->
    <div class="elite-hero mb-6">
      <img alt="" class="elite-hero-mark" :src="brandLogo" />
      <div class="relative z-10 flex flex-wrap items-center justify-between gap-4">
        <div>
          <h1 class="elite-hero-title">{{ greeting }},欢迎回来</h1>
          <p class="elite-hero-date">{{ today }}</p>
        </div>
        <div class="elite-status">
          <span class="elite-status-dot"></span>
          ç³»ç»Ÿè¿è¡Œä¸­
        </div>
      </div>
    </div>
    <!-- MES ç”Ÿäº§ KPI -->
    <KpiCards :summary="summary" @navigate="handleNavigate" />
    <!-- ç³»ç»ŸæŒ‡æ ‡ -->
    <KpiCards @navigate="handleNavigate" />
    <!-- ERP/WMS/BPM ç»è¥ KPI -->
    <BizCards @navigate="handleNavigate" />
    <!-- AI æ™ºèƒ½åˆ†æž -->
    <AiInsights
      :active-order-count="summary.workOrderActiveCount"
      @navigate="handleNavigate"
    />
    <!-- ç”Ÿäº§è¶‹åŠ¿ + é¢„警面板 -->
    <!-- å¾…办 + å¿«æ·å…¥å£ -->
    <Row :gutter="16" class="mb-4">
      <Col :lg="14" :md="24" :sm="24" :xl="14" :xs="24" class="mb-4">
        <div class="glass-card rounded-2xl p-5">
          <h3 class="mb-4 text-base font-semibold text-gray-800 dark:text-gray-100">
            ç”Ÿäº§è¶‹åŠ¿ï¼ˆè¿‘7天)
          </h3>
          <ProductionTrend />
        </div>
        <TodoPanel @navigate="handleNavigate" />
      </Col>
      <Col :lg="10" :md="24" :sm="24" :xl="10" :xs="24" class="mb-4">
        <AlertPanel
          :repair-active-count="summary.repairActiveCount"
          @navigate="handleNavigate"
        />
      </Col>
    </Row>
    <!-- å¿«æ·æ“ä½œ -->
    <Row :gutter="16" class="mb-4">
      <Col :span="24">
        <QuickActions @navigate="handleNavigate" />
      </Col>
    </Row>
    <!-- æœ€è¿‘课题申报 -->
    <RecentApplications @navigate="handleNavigate" />
  </Page>
</template>
<style scoped>
.glass-card {
  background: linear-gradient(135deg, rgba(255,255,255,0.8), rgba(255,255,255,0.5));
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  border: 1px solid rgba(255,255,255,0.6);
  box-shadow:
    0 8px 32px rgba(0,0,0,0.04),
    0 2px 8px rgba(0,0,0,0.02),
    inset 0 1px 0 rgba(255,255,255,0.8);
/* ===== æ¬¢è¿Žæ  ===== */
.elite-hero {
  position: relative;
  overflow: hidden;
  border-radius: 12px;
  padding: 30px 34px;
  background:
    radial-gradient(circle at 88% 12%, rgba(212, 175, 55, 0.08), transparent 50%),
    linear-gradient(135deg, #123156 0%, #0f2440 58%, #0b1c33 100%);
  border: 1px solid rgba(212, 175, 55, 0.24);
  box-shadow: 0 4px 18px rgba(15, 36, 64, 0.12);
}
.dark .glass-card {
  background: linear-gradient(135deg, rgba(30,30,50,0.7), rgba(20,20,40,0.5));
  border: 1px solid rgba(255,255,255,0.08);
  box-shadow:
    0 8px 32px rgba(0,0,0,0.3),
    0 2px 8px rgba(0,0,0,0.2),
    inset 0 1px 0 rgba(255,255,255,0.05);
/* å³ä¾§æ·¡æ·¡çš„徽标水印 */
.elite-hero-mark {
  position: absolute;
  top: 50%;
  right: 168px;
  width: 118px;
  height: 118px;
  pointer-events: none;
  opacity: 0.05;
  transform: translateY(-50%);
}
.elite-hero-title {
  margin: 0;
  font-size: 22px;
  font-weight: 600;
  letter-spacing: 2px;
  color: #f5f0e4;
}
.elite-hero-date {
  margin: 8px 0 0;
  font-size: 13px;
  letter-spacing: 1px;
  color: rgba(212, 175, 55, 0.85);
}
.elite-status {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 7px 16px;
  font-size: 12.5px;
  letter-spacing: 1px;
  color: #f0e2b8;
  background: rgba(212, 175, 55, 0.12);
  border: 1px solid rgba(212, 175, 55, 0.38);
  border-radius: 999px;
}
/* é™æ€çŠ¶æ€ç‚¹ï¼Œä¸åšè„‰å†²åŠ¨ç”» */
.elite-status-dot {
  display: inline-flex;
  width: 7px;
  height: 7px;
  border-radius: 999px;
  background: #d4af37;
}
</style>
src/views/dashboard/analytics/modules/ai-insights.vue
ÎļþÒÑɾ³ý
src/views/dashboard/analytics/modules/alert-panel.vue
ÎļþÒÑɾ³ý
src/views/dashboard/analytics/modules/biz-cards.vue
ÎļþÒÑɾ³ý
src/views/dashboard/analytics/modules/kpi-cards.vue
@@ -1,210 +1,251 @@
<script lang="ts" setup>
import type { MesHomeApi } from '#/api/mes/home';
import { computed, ref } from 'vue';
import { onMounted, ref } from 'vue';
import { CountTo } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
import { Col, Row } from 'ant-design-vue';
defineOptions({ name: 'DashboardKpiCards' });
import { getTaskTodoPage } from '#/api/bpm/task';
import { getReimbursementPage } from '#/api/finance/reimbursement';
import { getApplicationPage } from '#/api/research/application';
import { getGuidelinePage } from '#/api/research/guideline';
const props = defineProps<{
  summary: MesHomeApi.Summary;
}>();
defineOptions({ name: 'DashboardKpiCards' });
const emit = defineEmits<{
  navigate: [name: string];
}>();
const qualityRate = computed(() => {
  const total =
    props.summary.todayQualifiedQuantity +
    props.summary.todayUnqualifiedQuantity;
  return total === 0 ? 0 : (props.summary.todayQualifiedQuantity / total) * 100;
const todoCount = ref(0);
const applicationCount = ref(0);
const reimbursementCount = ref(0);
const guidelineCount = ref(0);
async function loadTodo() {
  try {
    const result = await getTaskTodoPage({ pageNo: 1, pageSize: 1 });
    todoCount.value = result.total ?? 0;
  } catch {
    /* æ— æƒé™æˆ–接口异常,保持 0 */
  }
}
async function loadApplication() {
  try {
    const result = await getApplicationPage({ pageNo: 1, pageSize: 1 });
    applicationCount.value = result.total ?? 0;
  } catch {
    /* æ— æƒé™æˆ–接口异常,保持 0 */
  }
}
async function loadReimbursement() {
  try {
    const result = await getReimbursementPage({ pageNo: 1, pageSize: 1 });
    reimbursementCount.value = result.total ?? 0;
  } catch {
    /* æ— æƒé™æˆ–接口异常,保持 0 */
  }
}
async function loadGuideline() {
  try {
    const result = await getGuidelinePage({ pageNo: 1, pageSize: 1 });
    guidelineCount.value = result.total ?? 0;
  } catch {
    /* æ— æƒé™æˆ–接口异常,保持 0 */
  }
}
onMounted(() => {
  loadTodo();
  loadApplication();
  loadReimbursement();
  loadGuideline();
});
const equipmentRate = computed(() => {
  if (props.summary.machineryTotal === 0) return 0;
  return (props.summary.machineryProducing / props.summary.machineryTotal) * 100;
});
/** 3D å€¾æ–œæ•ˆæžœ */
interface TiltState { x: number; y: number }
const tilts = ref<Record<string, TiltState>>({});
function onMouseMove(e: MouseEvent, id: string) {
  const el = e.currentTarget as HTMLElement;
  const rect = el.getBoundingClientRect();
  const x = ((e.clientX - rect.left) / rect.width - 0.5) * 2;
  const y = ((e.clientY - rect.top) / rect.height - 0.5) * 2;
  tilts.value[id] = { x: x * -6, y: y * 6 };
}
function onMouseLeave(id: string) {
  tilts.value[id] = { x: 0, y: 0 };
}
function tiltStyle(id: string) {
  const t = tilts.value[id] ?? { x: 0, y: 0 };
  return {
    transform: `perspective(600px) rotateX(${t.y}deg) rotateY(${t.x}deg)`,
    transition: t.x === 0 && t.y === 0 ? 'transform 0.5s ease' : 'transform 0.1s ease',
  };
}
</script>
<template>
  <Row :gutter="16">
    <!-- ç”Ÿäº§è®¢å• -->
    <!-- æˆ‘的待办 -->
    <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4">
      <div
        class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0"
        :style="tiltStyle('order')"
        @mousemove="(e: MouseEvent) => onMouseMove(e, 'order')"
        @mouseleave="onMouseLeave('order')"
        @click="emit('navigate', 'MesProWorkOrder')"
        class="elite-card group cursor-pointer"
        @click="emit('navigate', 'BpmTaskTodo')"
      >
        <div class="relative z-10 flex items-center gap-4 p-5">
          <div class="flex size-12 flex-shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-blue-500 to-cyan-400 shadow-lg shadow-blue-500/25 transition-transform duration-300 group-hover:scale-110">
            <IconifyIcon class="size-6 text-white" icon="lucide:clipboard-list" />
        <div class="flex items-center gap-4 p-5">
          <div class="elite-icon-tile">
            <IconifyIcon class="size-6" icon="lucide:list-todo" />
          </div>
          <div class="min-w-0 flex-1">
            <div class="mb-1 text-xs font-medium text-gray-500 dark:text-gray-400">生产订单</div>
            <div class="flex items-baseline gap-1">
              <CountTo class="text-2xl font-bold leading-tight text-blue-600 dark:text-blue-400" :end-val="summary.workOrderActiveCount" :duration="1200" />
              <span class="text-xs text-gray-400">进行中</span>
            </div>
            <div class="mt-1 flex items-center gap-1 text-xs text-gray-400">
              <span class="inline-block size-1.5 rounded-full bg-amber-400" /> å¾…排产 {{ summary.workOrderPrepareCount }}
              <span class="mx-1">|</span>
              <span class="inline-block size-1.5 rounded-full bg-emerald-400" /> å·²å®Œæˆ {{ summary.workOrderFinishedCount }}
            </div>
          </div>
        </div>
        <div class="absolute inset-0 -z-0 bg-gradient-to-br from-blue-500/5 to-cyan-500/5 opacity-0 transition-opacity duration-300 group-hover:opacity-100" />
        <div class="absolute -right-6 -top-6 size-24 rounded-full bg-blue-400/10 blur-xl transition-all duration-300 group-hover:scale-150" />
      </div>
    </Col>
    <!-- ä»Šæ—¥äº§é‡ -->
    <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4">
      <div
        class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0"
        :style="tiltStyle('output')"
        @mousemove="(e: MouseEvent) => onMouseMove(e, 'output')"
        @mouseleave="onMouseLeave('output')"
        @click="emit('navigate', 'MesProFeedback')"
      >
        <div class="relative z-10 flex items-center gap-4 p-5">
          <div class="flex size-12 flex-shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-emerald-500 to-green-400 shadow-lg shadow-emerald-500/25 transition-transform duration-300 group-hover:scale-110">
            <IconifyIcon class="size-6 text-white" icon="lucide:trending-up" />
          </div>
          <div class="min-w-0 flex-1">
            <div class="mb-1 text-xs font-medium text-gray-500 dark:text-gray-400">今日产量</div>
            <div class="flex items-baseline gap-1">
              <CountTo class="text-2xl font-bold leading-tight text-emerald-600 dark:text-emerald-400" :end-val="summary.todayOutput" :duration="1200" />
              <span class="text-xs text-gray-400">ä»¶</span>
            </div>
            <div class="mt-1 text-xs text-gray-400">
              æ˜¨æ—¥ {{ summary.yesterdayOutput }} ä»¶
              <span v-if="summary.yesterdayOutput > 0 && summary.todayOutput >= summary.yesterdayOutput" class="text-emerald-500">↑</span>
              <span v-else-if="summary.yesterdayOutput > 0" class="text-red-500">↓</span>
            </div>
          </div>
        </div>
        <div class="absolute inset-0 -z-0 bg-gradient-to-br from-emerald-500/5 to-green-500/5 opacity-0 transition-opacity duration-300 group-hover:opacity-100" />
        <div class="absolute -right-6 -top-6 size-24 rounded-full bg-emerald-400/10 blur-xl transition-all duration-300 group-hover:scale-150" />
      </div>
    </Col>
    <!-- è´¨é‡åˆæ ¼çއ -->
    <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4">
      <div
        class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0"
        :style="tiltStyle('quality')"
        @mousemove="(e: MouseEvent) => onMouseMove(e, 'quality')"
        @mouseleave="onMouseLeave('quality')"
        @click="emit('navigate', 'MesQcRecord')"
      >
        <div class="relative z-10 flex items-center gap-4 p-5">
          <div class="flex size-12 flex-shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-orange-500 to-amber-400 shadow-lg shadow-orange-500/25 transition-transform duration-300 group-hover:scale-110">
            <IconifyIcon class="size-6 text-white" icon="lucide:shield-check" />
          </div>
          <div class="min-w-0 flex-1">
            <div class="mb-1 text-xs font-medium text-gray-500 dark:text-gray-400">质量合格率</div>
            <div class="elite-label">我的待办</div>
            <div class="flex items-baseline gap-1">
              <CountTo
                class="text-2xl font-bold leading-tight"
                :class="qualityRate >= 95 ? 'text-emerald-600 dark:text-emerald-400' : qualityRate >= 80 ? 'text-orange-600 dark:text-orange-400' : 'text-red-600 dark:text-red-400'"
                :decimals="1" :end-val="qualityRate" :duration="1200"
                class="elite-value"
                :class="todoCount > 0 ? 'text-[#b8901f] dark:text-[#e3c567]' : ''"
                :duration="1200"
                :end-val="todoCount"
              />
              <span class="text-xs text-gray-400">%</span>
              <span class="elite-unit">项</span>
            </div>
            <div class="mt-1 text-xs text-gray-400">
              åˆæ ¼ {{ summary.todayQualifiedQuantity }} | ä¸è‰¯ {{ summary.todayUnqualifiedQuantity }}
            </div>
            <div class="elite-meta">审批中心 Â· å¾…处理任务</div>
          </div>
        </div>
        <div class="absolute inset-0 -z-0 bg-gradient-to-br from-orange-500/5 to-amber-500/5 opacity-0 transition-opacity duration-300 group-hover:opacity-100" />
        <div class="absolute -right-6 -top-6 size-24 rounded-full bg-orange-400/10 blur-xl transition-all duration-300 group-hover:scale-150" />
      </div>
    </Col>
    <!-- è®¾å¤‡è¿è¡Œçއ -->
    <!-- è¯¾é¢˜ç”³æŠ¥ -->
    <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4">
      <div
        class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0"
        :style="tiltStyle('equip')"
        @mousemove="(e: MouseEvent) => onMouseMove(e, 'equip')"
        @mouseleave="onMouseLeave('equip')"
        @click="emit('navigate', 'MesDvMachinery')"
        class="elite-card group cursor-pointer"
        @click="emit('navigate', 'ResearchApplication')"
      >
        <div class="relative z-10 flex items-center gap-4 p-5">
          <div class="flex size-12 flex-shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-violet-500 to-purple-400 shadow-lg shadow-violet-500/25 transition-transform duration-300 group-hover:scale-110">
            <IconifyIcon class="size-6 text-white" icon="lucide:cpu" />
        <div class="flex items-center gap-4 p-5">
          <div class="elite-icon-tile">
            <IconifyIcon class="size-6" icon="lucide:file-text" />
          </div>
          <div class="min-w-0 flex-1">
            <div class="mb-1 text-xs font-medium text-gray-500 dark:text-gray-400">设备运行率</div>
            <div class="elite-label">课题申报</div>
            <div class="flex items-baseline gap-1">
              <CountTo class="text-2xl font-bold leading-tight text-violet-600 dark:text-violet-400" :decimals="1" :end-val="equipmentRate" :duration="1200" />
              <span class="text-xs text-gray-400">%</span>
              <CountTo
                class="elite-value"
                :duration="1200"
                :end-val="applicationCount"
              />
              <span class="elite-unit">项</span>
            </div>
            <div class="mt-1 flex items-center gap-1 text-xs text-gray-400">
              <span class="text-red-400">停机 {{ summary.machineryStop }}</span>
              <span class="mx-1">|</span>
              <span class="text-amber-400">维护 {{ summary.machineryMaintenance }}</span>
            </div>
            <div class="elite-meta">课题管理 Â· ç”³æŠ¥ä¹¦</div>
          </div>
        </div>
        <div class="absolute inset-0 -z-0 bg-gradient-to-br from-violet-500/5 to-purple-500/5 opacity-0 transition-opacity duration-300 group-hover:opacity-100" />
        <div class="absolute -right-6 -top-6 size-24 rounded-full bg-violet-400/10 blur-xl transition-all duration-300 group-hover:scale-150" />
      </div>
    </Col>
    <!-- è´¢åŠ¡æŠ¥é”€ -->
    <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4">
      <div
        class="elite-card group cursor-pointer"
        @click="emit('navigate', 'FinanceReimbursement')"
      >
        <div class="flex items-center gap-4 p-5">
          <div class="elite-icon-tile">
            <IconifyIcon class="size-6" icon="lucide:receipt" />
          </div>
          <div class="min-w-0 flex-1">
            <div class="elite-label">财务报销</div>
            <div class="flex items-baseline gap-1">
              <CountTo
                class="elite-value"
                :duration="1200"
                :end-val="reimbursementCount"
              />
              <span class="elite-unit">单</span>
            </div>
            <div class="elite-meta">财务报销 Â· æŠ¥é”€å•</div>
          </div>
        </div>
      </div>
    </Col>
    <!-- è¯¾é¢˜æŒ‡å— -->
    <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4">
      <div
        class="elite-card group cursor-pointer"
        @click="emit('navigate', 'ResearchGuideline')"
      >
        <div class="flex items-center gap-4 p-5">
          <div class="elite-icon-tile">
            <IconifyIcon class="size-6" icon="lucide:book-open" />
          </div>
          <div class="min-w-0 flex-1">
            <div class="elite-label">课题指南</div>
            <div class="flex items-baseline gap-1">
              <CountTo
                class="elite-value"
                :duration="1200"
                :end-val="guidelineCount"
              />
              <span class="elite-unit">份</span>
            </div>
            <div class="elite-meta">课题管理 Â· ç”³æŠ¥æŒ‡å—</div>
          </div>
        </div>
      </div>
    </Col>
  </Row>
</template>
<style scoped>
.glass-tilt {
  background: linear-gradient(135deg, rgba(255,255,255,0.85), rgba(255,255,255,0.55));
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  border: 1px solid rgba(255,255,255,0.6);
  box-shadow:
    0 4px 24px rgba(0,0,0,0.04),
    0 1px 4px rgba(0,0,0,0.02),
    inset 0 1px 0 rgba(255,255,255,0.8);
  transform-style: preserve-3d;
/* ===== å­¦æœ¯å…¸é›…卡片 ===== */
.elite-card {
  background: #fdfcfa;
  border: 1px solid rgba(212, 175, 55, 0.22);
  border-radius: 14px;
  box-shadow: 0 2px 14px rgba(15, 36, 64, 0.05);
  transition:
    border-color 0.3s ease,
    box-shadow 0.3s ease;
}
.dark .glass-tilt {
  background: linear-gradient(135deg, rgba(30,30,55,0.75), rgba(20,20,40,0.5));
  border: 1px solid rgba(255,255,255,0.08);
  box-shadow:
    0 4px 24px rgba(0,0,0,0.3),
    0 1px 4px rgba(0,0,0,0.2),
    inset 0 1px 0 rgba(255,255,255,0.04);
.elite-card:hover {
  border-color: rgba(212, 175, 55, 0.5);
  box-shadow: 0 6px 20px rgba(15, 36, 64, 0.09);
}
.dark .elite-card {
  background: #101a2b;
  border-color: rgba(212, 175, 55, 0.2);
  box-shadow: 0 2px 14px rgba(0, 0, 0, 0.3);
}
/* å›¾æ ‡åº•:藏青渐变 + é‡‘色图标 */
.elite-icon-tile {
  display: flex;
  width: 48px;
  height: 48px;
  flex-shrink: 0;
  align-items: center;
  justify-content: center;
  border-radius: 12px;
  color: #e3c567;
  background: linear-gradient(135deg, #1e4571, #0f2440);
  border: 1px solid rgba(212, 175, 55, 0.3);
  box-shadow: 0 4px 12px rgba(15, 36, 64, 0.18);
  transition: border-color 0.3s ease;
}
/* æ‚¬åœåªè®©é‡‘色描边微微提亮,不做缩放 */
.group:hover .elite-icon-tile {
  border-color: rgba(212, 175, 55, 0.6);
}
.elite-label {
  margin-bottom: 4px;
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 1px;
  color: #8a94a6;
}
.elite-value {
  font-size: 24px;
  font-weight: 700;
  line-height: 1.2;
  color: #0f2440;
}
.dark .elite-value {
  color: #e3c567;
}
.elite-unit {
  font-size: 12px;
  color: #8a94a6;
}
.elite-meta {
  margin-top: 4px;
  font-size: 12px;
  color: #8a94a6;
}
</style>
src/views/dashboard/analytics/modules/production-trend.vue
ÎļþÒÑɾ³ý
src/views/dashboard/analytics/modules/quick-actions.vue
@@ -11,62 +11,125 @@
  title: string;
  icon: string;
  route: string;
  gradient: string;
  shadow: string;
}
/** å¿«æ·å…¥å£ï¼šå¯¹åº”系统实际菜单(审批中心 / è¯¾é¢˜ç®¡ç† / è´¢åŠ¡æŠ¥é”€ / é€šçŸ¥å…¬å‘Šï¼‰ */
const actions: QuickAction[] = [
  { title: '生产工单', icon: 'lucide:file-text', route: 'MesProWorkOrder', gradient: 'from-blue-500 to-cyan-500', shadow: 'shadow-blue-500/15' },
  { title: '生产报工', icon: 'lucide:check-square', route: 'MesProFeedback', gradient: 'from-emerald-500 to-green-500', shadow: 'shadow-emerald-500/15' },
  { title: '质检管理', icon: 'lucide:clipboard-check', route: 'MesQcRecord', gradient: 'from-orange-500 to-amber-500', shadow: 'shadow-orange-500/15' },
  { title: '设备点检', icon: 'lucide:settings', route: 'MesDvMachinery', gradient: 'from-violet-500 to-purple-500', shadow: 'shadow-violet-500/15' },
  { title: '生产排程', icon: 'lucide:calendar-range', route: 'MesProScheduling', gradient: 'from-cyan-500 to-teal-500', shadow: 'shadow-cyan-500/15' },
  { title: '物料库存', icon: 'lucide:package-search', route: 'WlsMaterialStock', gradient: 'from-indigo-500 to-blue-500', shadow: 'shadow-indigo-500/15' },
  { title: '工艺路线', icon: 'lucide:route', route: 'MesPdRouting', gradient: 'from-teal-500 to-emerald-500', shadow: 'shadow-teal-500/15' },
  { title: '数据报表', icon: 'lucide:bar-chart-big', route: 'MesHomeIndex', gradient: 'from-pink-500 to-rose-500', shadow: 'shadow-pink-500/15' },
  { title: '发起流程', icon: 'lucide:circle-plus', route: 'BpmProcessInstanceCreate' },
  { title: '待办任务', icon: 'lucide:list-todo', route: 'BpmTaskTodo' },
  { title: '已办任务', icon: 'lucide:circle-check-big', route: 'BpmTaskDone' },
  { title: '我的流程', icon: 'lucide:git-branch', route: 'BpmProcessInstance' },
  { title: '课题申报', icon: 'lucide:file-text', route: 'ResearchApplication' },
  { title: '课题指南', icon: 'lucide:book-open', route: 'ResearchGuideline' },
  { title: '财务报销', icon: 'lucide:receipt', route: 'FinanceReimbursement' },
  { title: '通知公告', icon: 'lucide:bell', route: 'BpmOaNotice' },
];
</script>
<template>
  <div class="glass-card rounded-2xl p-5">
    <h3 class="mb-4 text-sm font-semibold text-gray-800 dark:text-gray-100">快捷操作</h3>
    <div class="grid grid-cols-4 gap-3 sm:grid-cols-8">
  <div class="elite-card p-5">
    <div class="elite-head">
      <h3 class="elite-title">
        <span class="elite-bar"></span>
        å¿«æ·å…¥å£
      </h3>
    </div>
    <div class="grid grid-cols-4 gap-3">
      <div
        v-for="action in actions"
        :key="action.route"
        class="group flex cursor-pointer flex-col items-center gap-2 rounded-xl p-3 transition-all duration-300 hover:-translate-y-1"
        class="elite-action group flex cursor-pointer flex-col items-center gap-2 rounded-xl p-3 transition-colors duration-300"
        @click="emit('navigate', action.route)"
      >
        <div
          class="flex size-10 items-center justify-center rounded-xl bg-gradient-to-br transition-shadow duration-300 group-hover:shadow-lg"
          :class="[action.gradient, action.shadow]"
          class="elite-icon flex size-10 items-center justify-center rounded-xl transition-shadow duration-300"
        >
          <IconifyIcon :icon="action.icon" class="size-5 text-white" />
          <IconifyIcon :icon="action.icon" class="size-5 text-[#e3c567]" />
        </div>
        <span class="text-xs font-medium text-gray-600 dark:text-gray-400">{{ action.title }}</span>
        <span class="elite-action-label">{{ action.title }}</span>
      </div>
    </div>
  </div>
</template>
<style scoped>
.glass-card {
  background: linear-gradient(135deg, rgba(255,255,255,0.85), rgba(255,255,255,0.55));
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  border: 1px solid rgba(255,255,255,0.6);
  box-shadow:
    0 4px 24px rgba(0,0,0,0.04),
    0 1px 4px rgba(0,0,0,0.02),
    inset 0 1px 0 rgba(255,255,255,0.8);
.elite-card {
  background: #fdfcfa;
  border: 1px solid rgba(212, 175, 55, 0.22);
  border-radius: 14px;
  box-shadow: 0 2px 14px rgba(15, 36, 64, 0.05);
  transition:
    border-color 0.3s ease,
    box-shadow 0.3s ease;
}
.dark .glass-card {
  background: linear-gradient(135deg, rgba(30,30,55,0.75), rgba(20,20,40,0.5));
  border: 1px solid rgba(255,255,255,0.08);
.elite-card:hover {
  border-color: rgba(212, 175, 55, 0.5);
  box-shadow: 0 6px 20px rgba(15, 36, 64, 0.09);
}
.dark .elite-card {
  background: #101a2b;
  border-color: rgba(212, 175, 55, 0.2);
  box-shadow: 0 2px 14px rgba(0, 0, 0, 0.3);
}
.elite-head {
  display: flex;
  align-items: center;
  margin-bottom: 16px;
}
.elite-title {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 0;
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 1px;
  color: #0f2440;
}
.dark .elite-title {
  color: #f0ead8;
}
.elite-bar {
  display: inline-block;
  width: 3px;
  height: 15px;
  border-radius: 2px;
  background: linear-gradient(180deg, #e3c567, #b8901f);
}
/* å›¾æ ‡åº•:藏青渐变 + é‡‘色图标 */
.elite-icon {
  color: #e3c567;
  background: linear-gradient(135deg, #1e4571, #0f2440);
  border: 1px solid rgba(212, 175, 55, 0.3);
  box-shadow: 0 4px 12px rgba(15, 36, 64, 0.18);
}
/* æ‚¬åœåªæäº®é‡‘色描边,不做位移 */
.elite-action:hover .elite-icon {
  box-shadow:
    0 4px 24px rgba(0,0,0,0.3),
    0 1px 4px rgba(0,0,0,0.2),
    inset 0 1px 0 rgba(255,255,255,0.04);
    0 0 0 1px rgba(212, 175, 55, 0.7),
    0 4px 14px rgba(15, 36, 64, 0.22);
}
.elite-action-label {
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.5px;
  color: #8a94a6;
}
.elite-action:hover .elite-action-label {
  color: #b8901f;
}
.dark .elite-action:hover .elite-action-label {
  color: #e3c567;
}
</style>
src/views/dashboard/analytics/modules/recent-applications.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,272 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { getApplicationPage } from '#/api/research/application';
defineOptions({ name: 'DashboardRecentApplications' });
const emit = defineEmits<{
  navigate: [name: string];
}>();
interface Row {
  id?: number;
  no?: string;
  title: string;
  applicantName: string;
  status?: number;
  createTime?: string;
}
/** çŠ¶æ€ç  â†’ å±•示文案与配色,与 research_application_status å­—典保持一致 */
const STATUS_MAP: Record<number, { text: string; tone: string }> = {
  0: { text: '草稿', tone: 'gray' },
  15: { text: '审批中', tone: 'gold' },
  20: { text: '已立项', tone: 'green' },
  35: { text: '已驳回', tone: 'red' },
};
function statusOf(code?: number) {
  if (code === undefined || code === null) {
    return { text: '—', tone: 'gray' };
  }
  return STATUS_MAP[code] ?? { text: String(code), tone: 'gray' };
}
const loading = ref(true);
const rows = ref<Row[]>([]);
async function load() {
  loading.value = true;
  try {
    const result = await getApplicationPage({ pageNo: 1, pageSize: 6 });
    rows.value = (result.list ?? []) as Row[];
  } catch {
    rows.value = [];
  } finally {
    loading.value = false;
  }
}
onMounted(load);
</script>
<template>
  <div class="elite-section">
    <div class="elite-head">
      <h3 class="elite-title">
        <span class="elite-bar"></span>
        æœ€è¿‘课题申报
      </h3>
      <span class="elite-more" @click="emit('navigate', 'ResearchApplication')">
        å…¨éƒ¨
        <IconifyIcon class="size-3.5" icon="lucide:chevron-right" />
      </span>
    </div>
    <div v-if="loading" class="elite-empty">加载中…</div>
    <div v-else-if="rows.length === 0" class="elite-empty">
      <IconifyIcon class="mb-2 size-8 opacity-40" icon="lucide:file-text" />
      <div>暂无课题申报记录</div>
    </div>
    <table v-else class="elite-table">
      <thead>
        <tr>
          <th class="col-title">课题名称</th>
          <th class="col-applicant">申报人</th>
          <th class="col-status">状态</th>
          <th class="col-time">申报时间</th>
        </tr>
      </thead>
      <tbody>
        <tr
          v-for="row in rows"
          :key="row.id ?? row.no"
          @click="emit('navigate', 'ResearchApplication')"
        >
          <td class="col-title">
            <span class="elite-ellipsis">{{ row.title }}</span>
          </td>
          <td class="col-applicant">{{ row.applicantName || '—' }}</td>
          <td class="col-status">
            <span class="elite-chip" :class="`tone-${statusOf(row.status).tone}`">
              {{ statusOf(row.status).text }}
            </span>
          </td>
          <td class="col-time">{{ row.createTime || '—' }}</td>
        </tr>
      </tbody>
    </table>
  </div>
</template>
<style scoped>
.elite-section {
  border-radius: 14px;
  padding: 20px;
  background: #fdfcfa;
  border: 1px solid rgba(212, 175, 55, 0.22);
  box-shadow: 0 2px 14px rgba(15, 36, 64, 0.05);
}
.dark .elite-section {
  background: #101a2b;
  border-color: rgba(212, 175, 55, 0.2);
  box-shadow: 0 2px 14px rgba(0, 0, 0, 0.3);
}
.elite-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 14px;
}
.elite-title {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 0;
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 1px;
  color: #0f2440;
}
.dark .elite-title {
  color: #f0ead8;
}
.elite-bar {
  display: inline-block;
  width: 3px;
  height: 15px;
  border-radius: 2px;
  background: linear-gradient(180deg, #e3c567, #b8901f);
}
.elite-more {
  display: inline-flex;
  cursor: pointer;
  align-items: center;
  gap: 2px;
  font-size: 12px;
  color: #b8901f;
  transition: color 0.2s ease;
}
.elite-more:hover {
  color: #8a6a12;
}
.elite-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 34px 0;
  font-size: 13px;
  color: #8a94a6;
}
.elite-table {
  width: 100%;
  border-collapse: collapse;
  table-layout: fixed;
}
.elite-table th {
  padding: 0 8px 10px;
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 1px;
  color: #8a94a6;
  text-align: left;
  border-bottom: 1px solid rgba(15, 36, 64, 0.08);
}
.elite-table td {
  padding: 12px 8px;
  font-size: 13.5px;
  color: #0f2440;
  border-bottom: 1px solid rgba(15, 36, 64, 0.05);
}
.dark .elite-table td {
  color: #e6e9ef;
}
.elite-table tbody tr {
  cursor: pointer;
  transition: background-color 0.2s ease;
}
.elite-table tbody tr:hover {
  background: rgba(212, 175, 55, 0.07);
}
.elite-table tbody tr:last-child td {
  border-bottom: none;
}
.col-title {
  width: 44%;
}
.col-applicant {
  width: 18%;
}
.col-status {
  width: 16%;
}
.col-time {
  width: 22%;
  color: #8a94a6;
}
.elite-ellipsis {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* çŠ¶æ€æ ‡ç­¾ï¼šé‡‘/绿/红/灰四档 */
.elite-chip {
  display: inline-block;
  padding: 2px 10px;
  font-size: 12px;
  border-radius: 999px;
  border: 1px solid transparent;
}
.tone-gold {
  color: #b8901f;
  background: rgba(212, 175, 55, 0.1);
  border-color: rgba(212, 175, 55, 0.35);
}
.tone-green {
  color: #2f6b4f;
  background: rgba(47, 107, 79, 0.1);
  border-color: rgba(47, 107, 79, 0.3);
}
.tone-red {
  color: #b4453c;
  background: rgba(180, 69, 60, 0.1);
  border-color: rgba(180, 69, 60, 0.3);
}
.tone-gray {
  color: #8a94a6;
  background: rgba(138, 148, 166, 0.1);
  border-color: rgba(138, 148, 166, 0.28);
}
</style>
src/views/dashboard/analytics/modules/todo-panel.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,208 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { getTaskTodoPage } from '#/api/bpm/task';
defineOptions({ name: 'DashboardTodoPanel' });
const emit = defineEmits<{
  navigate: [name: string];
}>();
interface TodoItem {
  id: string;
  name: string;
  createTime?: number;
}
const loading = ref(true);
const items = ref<TodoItem[]>([]);
/** createTime ä¸ºæ¯«ç§’时间戳,格式化成易读文本 */
function formatTime(ts?: number) {
  if (!ts) return '-';
  const d = new Date(ts);
  const p = (n: number) => String(n).padStart(2, '0');
  return `${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
}
async function load() {
  loading.value = true;
  try {
    const result = await getTaskTodoPage({ pageNo: 1, pageSize: 6 });
    items.value = (result.list ?? []).map((t: any) => ({
      id: String(t.id),
      name: t.name ?? '未命名任务',
      createTime: t.createTime,
    }));
  } catch {
    items.value = [];
  } finally {
    loading.value = false;
  }
}
onMounted(load);
</script>
<template>
  <div class="elite-section h-full">
    <div class="elite-head">
      <h3 class="elite-title">
        <span class="elite-bar"></span>
        æˆ‘的待办
      </h3>
      <span class="elite-more" @click="emit('navigate', 'BpmTaskTodo')">
        å…¨éƒ¨
        <IconifyIcon class="size-3.5" icon="lucide:chevron-right" />
      </span>
    </div>
    <!-- éª¨æž¶ -->
    <div v-if="loading" class="elite-empty">加载中…</div>
    <!-- ç©ºçŠ¶æ€ -->
    <div v-else-if="items.length === 0" class="elite-empty">
      <IconifyIcon class="mb-2 size-8 opacity-40" icon="lucide:inbox" />
      <div>暂无待办任务</div>
    </div>
    <!-- åˆ—表 -->
    <ul v-else class="elite-list">
      <li
        v-for="item in items"
        :key="item.id"
        class="elite-row"
        @click="emit('navigate', 'BpmTaskTodo')"
      >
        <span class="elite-dot"></span>
        <span class="elite-row-name">{{ item.name }}</span>
        <span class="elite-row-time">{{ formatTime(item.createTime) }}</span>
      </li>
    </ul>
  </div>
</template>
<style scoped>
.elite-section {
  border-radius: 14px;
  padding: 20px;
  background: #fdfcfa;
  border: 1px solid rgba(212, 175, 55, 0.22);
  box-shadow: 0 2px 14px rgba(15, 36, 64, 0.05);
}
.dark .elite-section {
  background: #101a2b;
  border-color: rgba(212, 175, 55, 0.2);
  box-shadow: 0 2px 14px rgba(0, 0, 0, 0.3);
}
.elite-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 14px;
}
.elite-title {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 0;
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 1px;
  color: #0f2440;
}
.dark .elite-title {
  color: #f0ead8;
}
.elite-bar {
  display: inline-block;
  width: 3px;
  height: 15px;
  border-radius: 2px;
  background: linear-gradient(180deg, #e3c567, #b8901f);
}
.elite-more {
  display: inline-flex;
  cursor: pointer;
  align-items: center;
  gap: 2px;
  font-size: 12px;
  color: #b8901f;
  transition: color 0.2s ease;
}
.elite-more:hover {
  color: #8a6a12;
}
.elite-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 34px 0;
  font-size: 13px;
  color: #8a94a6;
}
.elite-list {
  margin: 0;
  padding: 0;
  list-style: none;
}
.elite-row {
  display: flex;
  cursor: pointer;
  align-items: center;
  gap: 10px;
  padding: 11px 10px;
  border-radius: 8px;
  border-bottom: 1px solid rgba(15, 36, 64, 0.05);
  transition: background-color 0.2s ease;
}
.elite-row:last-child {
  border-bottom: none;
}
.elite-row:hover {
  background: rgba(212, 175, 55, 0.07);
}
.elite-dot {
  width: 6px;
  height: 6px;
  flex-shrink: 0;
  border-radius: 999px;
  background: #d4af37;
}
.elite-row-name {
  overflow: hidden;
  flex: 1;
  font-size: 13.5px;
  color: #0f2440;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.dark .elite-row-name {
  color: #e6e9ef;
}
.elite-row-time {
  flex-shrink: 0;
  font-size: 12px;
  color: #8a94a6;
}
</style>