From 7b0c52c4bfcd2c2bbcbb1cccdcfbe9f79027224b Mon Sep 17 00:00:00 2001
From: zhangwencui <1064582902@qq.com>
Date: 星期三, 28 十月 2026 12:43:12 +0800
Subject: [PATCH] 一些项目的首页修改
---
src/pages/hrm/attendance-clock/index.vue | 4
src/pages/workbench/index.vue | 41
src/pages/home/lanyu-paper.vue | 1083 ++++++++++++++++++++++++++++++++++++++
src/stores/theme.ts | 36 +
src/config/company.ts | 29
src/pages/home/index.vue | 6
src/pages/home/changhui-power.vue | 129 ++++
src/manifest.json | 6
src/pages/home/kangyi-intelligent.vue | 174 ++++++
src/pages/home/use-company-dashboard.ts | 158 +++++
10 files changed, 1,637 insertions(+), 29 deletions(-)
diff --git a/src/config/company.ts b/src/config/company.ts
index ef2b5ff..a9a261e 100644
--- a/src/config/company.ts
+++ b/src/config/company.ts
@@ -1,7 +1,7 @@
-export type HomeVariant = "classic" | "north-tech" | "xingao-power" | "zhongyi-assembly";
+export type HomeVariant = "classic" | "changhui-power" | "kangyi-intelligent" | "lanyu-paper" | "north-tech" | "xingao-power" | "zhongyi-assembly";
/** 鎵撳寘鐩爣锛氭敼 `ACTIVE_COMPANY` 涓�琛岋紝棣栭〉鐗堟湰涓庡悗绔湴鍧�鍚屾椂鍒囨崲銆� */
-export type CompanyCode = "main" | "trt" | "beifang" | "xingao" | "zhongyi";
+export type CompanyCode = "main" | "trt" | "changhui" | "kangyi" | "lanyu" | "beifang" | "xingao" | "zhongyi";
export interface AppCompanyConfig {
/** 鍏徃鏍囪瘑锛屼笌 CompanyCode 涓�鑷达紝渚夸簬鎵撴棩蹇� / 缂撳瓨鍛藉悕绌洪棿 */
@@ -16,7 +16,7 @@
}
/**
- * 浜斿鐜瀵圭収琛ㄣ��
+ * 鍏鐜瀵圭収琛ㄣ��
* - apiBaseUrl 缁熶竴浠� `/admin-api` 缁撳熬锛坹udao 鍚庡彴鎺ュ彛鍓嶇紑锛夈��
* - webBaseUrl锛氬彧鏈変富鍒嗘敮锛圡OM 鏅鸿兘鏅洪�狅級鏄棦鏈夐厤缃紙9501锛夛紱鍏朵綑鎸変富鍒嗘敮锛圡OM 鏅鸿兘鏅洪�狅級鈥滄帴鍙g鍙� +1鈥濈殑閮ㄧ讲瑙勫緥
* 鎺ㄥ锛�9500 鈫� 9501锛夈��9017 / 9027 / 9025 瀹炴祴鑳芥墦寮�鍚庡彴锛�1.95.78.124:9007 鐩墠杩炰笉涓�
@@ -37,6 +37,27 @@
homeVariant: "classic",
apiBaseUrl: "http://36.213.150.237:9016/admin-api",
webBaseUrl: "http://36.213.150.237:9017",
+ },
+ changhui: {
+ key: "changhui",
+ name: "闀胯緣鐢靛姏",
+ homeVariant: "changhui-power",
+ apiBaseUrl: "http://101.245.92.159:9006/admin-api",
+ webBaseUrl: "http://101.245.92.159:9006",
+ },
+ kangyi: {
+ key: "kangyi",
+ name: "搴锋剰鏅鸿兘",
+ homeVariant: "kangyi-intelligent",
+ apiBaseUrl: "http://101.245.92.159:9006/admin-api",
+ webBaseUrl: "http://101.245.92.159:9008",
+ },
+ lanyu: {
+ key: "lanyu",
+ name: "钃濈窘绾镐笟",
+ homeVariant: "lanyu-paper",
+ apiBaseUrl: "http://36.213.150.237:9020/admin-api",
+ webBaseUrl: "http://36.213.150.237:9019",
},
beifang: {
key: "beifang",
@@ -62,6 +83,6 @@
};
/** 鈽� 鎵撳寘寮�鍏筹細鍙敼杩欎竴琛� */
-export const ACTIVE_COMPANY: CompanyCode = "xingao";
+export const ACTIVE_COMPANY: CompanyCode = "changhui";
export const APP_COMPANY: AppCompanyConfig = COMPANY_PROFILES[ACTIVE_COMPANY];
diff --git a/src/manifest.json b/src/manifest.json
index 3a03981..2b5373b 100644
--- a/src/manifest.json
+++ b/src/manifest.json
@@ -1,7 +1,7 @@
{
- "name" : "鏂伴珮鐢靛姏",
- "appid" : "__UNI__4CEFF85",
- "description" : "鏂伴珮鐢靛姏",
+ "name" : "闀胯緣鐢靛姏",
+ "appid" : "__UNI__59EDE0D",
+ "description" : "闀胯緣鐢靛姏",
"versionName" : "1.0.0",
"versionCode" : 100,
"transformPx" : false,
diff --git a/src/pages/home/changhui-power.vue b/src/pages/home/changhui-power.vue
new file mode 100644
index 0000000..e51ae6e
--- /dev/null
+++ b/src/pages/home/changhui-power.vue
@@ -0,0 +1,129 @@
+<script setup lang="ts">
+import { APP_COMPANY } from "../../config/company";
+import { useCompanyDashboard } from "./use-company-dashboard";
+
+const {
+ equipmentRate, formatDate, goLogin, goMessages, greeting, isLoggedIn, loadDashboard,
+ loadError, loading, notifyState, openRoute, outputChange, qualityRate, statusPercent,
+ summary, today, totalWorkOrders, trend, trendMax, userName, visibleActions, workOrderStatus,
+} = useCompanyDashboard();
+</script>
+
+<template>
+ <view class="changhui-page">
+ <template v-if="isLoggedIn">
+ <view class="page-header">
+ <view class="header-bar">
+ <view class="company-brand">
+ <view class="brand-mark">CH</view>
+ <view class="brand-copy">
+ <text>{{ APP_COMPANY.name }}</text>
+ <text>鐢熶骇绠$悊宸ヤ綔鍙�</text>
+ </view>
+ </view>
+ <view class="message-button" @click="goMessages">
+ <text>娑堟伅</text>
+ <view v-if="notifyState.unreadCount" class="message-badge">{{ notifyState.unreadCount > 99 ? '99+' : notifyState.unreadCount }}</view>
+ </view>
+ </view>
+
+ <view class="welcome-row">
+ <view>
+ <text class="welcome-title">{{ greeting }}锛寋{ userName }}</text>
+ <text class="welcome-date">{{ today }}</text>
+ </view>
+ <view class="running-state"><i />鐢熶骇杩愯涓�</view>
+ </view>
+ </view>
+
+ <view class="page-body">
+ <view v-if="loadError" class="error-card" @click="loadDashboard">
+ <text>{{ loadError }}</text><text>鐐瑰嚮閲嶆柊鍔犺浇</text>
+ </view>
+
+ <template v-else>
+ <view class="overview-card">
+ <view class="card-heading">
+ <view><text>浠婃棩姒傚喌</text><text>{{ loading ? '鏁版嵁鏇存柊涓�' : '瀹炴椂鏁版嵁' }}</text></view>
+ <text>鈥�</text>
+ </view>
+ <view class="overview-grid">
+ <view class="overview-item" @click="openRoute('/pages/mes/pro/workorder/index')">
+ <strong>{{ summary?.workOrderActiveCount ?? '--' }}</strong><text>杩涜涓伐鍗�</text>
+ </view>
+ <view class="overview-item" @click="openRoute('/pages/mes/pro/feedback/index')">
+ <strong>{{ summary?.todayOutput ?? '--' }}</strong><text>浠婃棩浜ч噺</text>
+ </view>
+ <view class="overview-item">
+ <strong>{{ qualityRate }}<small>%</small></strong><text>璐ㄩ噺鍚堟牸鐜�</text>
+ </view>
+ <view class="overview-item">
+ <strong>{{ equipmentRate }}<small>%</small></strong><text>璁惧杩愯鐜�</text>
+ </view>
+ </view>
+ <view class="overview-note">
+ <text>寰呮帓浜� {{ summary?.workOrderPrepareCount ?? 0 }} 鍗�</text>
+ <text :class="outputChange >= 0 ? 'positive' : 'negative'">杈冩槰鏃� {{ outputChange >= 0 ? '+' : '' }}{{ outputChange }}</text>
+ </view>
+ </view>
+
+ <view class="section-heading">
+ <text>蹇嵎鍔熻兘</text>
+ <text @click="openRoute('/pages/workbench/index')">鍏ㄩ儴鍔熻兘</text>
+ </view>
+ <view class="quick-card">
+ <view v-for="item in visibleActions" :key="item.component" class="quick-item" @click="openRoute(item.route)">
+ <view :class="['quick-icon', `tone-${item.tone}`]">{{ item.mark }}</view>
+ <text>{{ item.name }}</text>
+ </view>
+ </view>
+
+ <view class="section-heading"><text>鐢熶骇瓒嬪娍</text><text>杩戜竷鏃�</text></view>
+ <view class="trend-card">
+ <view v-if="trend.length" class="trend-chart">
+ <view v-for="item in trend" :key="item.date" class="trend-column">
+ <text>{{ item.quantity }}</text>
+ <view class="trend-track"><view :style="{ height: `${Math.max(item.quantity / trendMax * 100, 4)}%` }" /></view>
+ <text>{{ formatDate(item.date) }}</text>
+ </view>
+ </view>
+ <view v-else class="empty-state">鏆傛棤鐢熶骇瓒嬪娍鏁版嵁</view>
+ </view>
+
+ <view class="section-heading"><text>宸ュ崟鐘舵��</text><text>鍏� {{ totalWorkOrders }} 鍗�</text></view>
+ <view class="status-card">
+ <view v-for="(item, index) in workOrderStatus" :key="item.status" class="status-row">
+ <view class="status-name"><i :class="`dot-${index % 4}`" />{{ item.statusName }}</view>
+ <view class="status-track"><view :class="`bar-${index % 4}`" :style="{ width: `${statusPercent(item.count)}%` }" /></view>
+ <strong>{{ item.count }}</strong>
+ </view>
+ <view v-if="!workOrderStatus.length" class="empty-state">鏆傛棤宸ュ崟鐘舵�佹暟鎹�</view>
+ </view>
+
+ <view class="repair-card" @click="openRoute('/pages/mes/dv/repair/index')">
+ <view class="repair-icon">淇�</view>
+ <view><text>璁惧缁翠慨浠诲姟</text><text>鏌ョ湅寰呭鐞嗙殑璁惧淇濋殰浠诲姟</text></view>
+ <view class="repair-count"><strong>{{ summary?.repairActiveCount ?? 0 }}</strong><text>椤瑰緟澶勭悊</text></view>
+ <text class="repair-arrow">鈥�</text>
+ </view>
+ </template>
+ </view>
+ </template>
+
+ <view v-else class="login-page">
+ <view class="login-mark">CH</view>
+ <text>{{ APP_COMPANY.name }}</text>
+ <text>鐢熶骇绠$悊绉诲姩宸ヤ綔鍙�</text>
+ <button @click="goLogin">鐧诲綍绯荤粺</button>
+ </view>
+ </view>
+</template>
+
+<style scoped lang="scss">
+.changhui-page{min-height:100vh;padding-bottom:150rpx;background:#f3f6fa;color:#1d2b3c}.page-header{padding:calc(var(--status-bar-height) + 22rpx) 30rpx 34rpx;background:linear-gradient(135deg,#1264b4,#2485d5);color:#fff}.header-bar,.company-brand,.welcome-row,.overview-note,.section-heading,.status-row,.repair-card{display:flex;align-items:center}.header-bar,.welcome-row,.section-heading{justify-content:space-between}.company-brand{gap:15rpx}.brand-mark{width:60rpx;height:60rpx;display:flex;align-items:center;justify-content:center;border-radius:12rpx;background:#fff;color:#176dbc;font-size:21rpx;font-weight:800}.brand-copy text{display:block}.brand-copy text:first-child{font-size:29rpx;font-weight:700}.brand-copy text:last-child{margin-top:2rpx;color:rgba(255,255,255,.72);font-size:19rpx}.message-button{position:relative;padding:13rpx 18rpx;border-radius:24rpx;background:rgba(255,255,255,.15);font-size:20rpx}.message-badge{position:absolute;right:-8rpx;top:-9rpx;min-width:25rpx;height:25rpx;padding:0 5rpx;box-sizing:border-box;border-radius:14rpx;background:#ff6b59;color:#fff;font-size:15rpx;line-height:25rpx;text-align:center}.welcome-row{margin-top:42rpx}.welcome-title,.welcome-date{display:block}.welcome-title{font-size:35rpx;font-weight:700}.welcome-date{margin-top:9rpx;color:rgba(255,255,255,.76);font-size:21rpx}.running-state{padding:11rpx 15rpx;border-radius:8rpx;background:rgba(255,255,255,.14);font-size:18rpx}.running-state i{display:inline-block;width:9rpx;height:9rpx;margin-right:8rpx;border-radius:50%;background:#7ef0bb}
+.page-body{padding:24rpx 26rpx}.overview-card,.quick-card,.trend-card,.status-card,.repair-card{border:1rpx solid #e4ebf2;border-radius:18rpx;background:#fff;box-shadow:0 6rpx 22rpx rgba(34,70,105,.05)}.overview-card{padding:24rpx}.card-heading{display:flex;align-items:center;justify-content:space-between}.card-heading>view text{display:block}.card-heading>view text:first-child{font-size:27rpx;font-weight:700}.card-heading>view text:last-child{margin-top:4rpx;color:#94a2b2;font-size:18rpx}.card-heading>text{color:#9aa8b8;font-size:34rpx}.overview-grid{display:grid;grid-template-columns:1fr 1fr;margin-top:23rpx;border-top:1rpx solid #edf1f5;border-left:1rpx solid #edf1f5}.overview-item{padding:23rpx 20rpx;border-right:1rpx solid #edf1f5;border-bottom:1rpx solid #edf1f5}.overview-item strong,.overview-item text{display:block}.overview-item strong{color:#1d5f9e;font-size:40rpx;line-height:1.15}.overview-item small{margin-left:3rpx;font-size:18rpx}.overview-item text{margin-top:7rpx;color:#758495;font-size:19rpx}.overview-note{justify-content:space-between;padding-top:18rpx;color:#8492a2;font-size:18rpx}.positive{color:#1aa578}.negative{color:#e06055}
+.section-heading{margin:31rpx 3rpx 14rpx}.section-heading>text:first-child{font-size:27rpx;font-weight:700}.section-heading>text:last-child{color:#8997a7;font-size:19rpx}.quick-card{display:grid;grid-template-columns:repeat(3,1fr);padding:12rpx 8rpx}.quick-item{padding:17rpx 4rpx;display:flex;align-items:center;flex-direction:column}.quick-icon{width:68rpx;height:68rpx;display:flex;align-items:center;justify-content:center;border-radius:18rpx;color:#2875bd;background:#eaf4ff;font-size:24rpx;font-weight:700}.quick-item>text{margin-top:11rpx;color:#3b4857;font-size:20rpx}.tone-teal,.tone-green{color:#178b76;background:#e7f7f2}.tone-violet{color:#6955ba;background:#f0edff}.tone-amber{color:#ad7723;background:#fff4df}.tone-cyan{color:#1c8199;background:#e7f6fa}.tone-slate{color:#5c7085;background:#edf2f6}
+.trend-card{padding:24rpx 18rpx}.trend-chart{height:235rpx;display:flex;align-items:flex-end;gap:9rpx}.trend-column{height:100%;min-width:0;display:flex;flex:1;align-items:center;flex-direction:column;justify-content:flex-end}.trend-column>text:first-child{height:30rpx;color:#3e668a;font-size:16rpx}.trend-column>text:last-child{margin-top:9rpx;color:#98a5b3;font-size:16rpx}.trend-track{width:25rpx;height:145rpx;display:flex;align-items:flex-end;border-radius:6rpx;background:#eef3f7}.trend-track view{width:100%;border-radius:6rpx;background:linear-gradient(180deg,#62afea,#267bc3)}.status-card{padding:16rpx 22rpx}.status-row{gap:16rpx;min-height:68rpx}.status-name{width:145rpx;color:#526273;font-size:19rpx}.status-name i{display:inline-block;width:9rpx;height:9rpx;margin-right:9rpx;border-radius:50%}.dot-0,.bar-0{background:#3288cf}.dot-1,.bar-1{background:#35aa86}.dot-2,.bar-2{background:#e2a547}.dot-3,.bar-3{background:#8d79c8}.status-track{height:8rpx;flex:1;overflow:hidden;border-radius:5rpx;background:#edf1f5}.status-track view{height:100%;border-radius:inherit}.status-row strong{width:42rpx;color:#30465b;font-size:20rpx;text-align:right}.empty-state{padding:43rpx 0;color:#98a5b3;font-size:20rpx;text-align:center}
+.repair-card{position:relative;margin-top:22rpx;padding:22rpx}.repair-icon{width:62rpx;height:62rpx;display:flex;align-items:center;justify-content:center;flex:none;border-radius:16rpx;color:#b5761f;background:#fff2db;font-size:22rpx;font-weight:700}.repair-card>view:nth-child(2){margin-left:16rpx}.repair-card>view:nth-child(2) text{display:block}.repair-card>view:nth-child(2) text:first-child{font-size:23rpx;font-weight:650}.repair-card>view:nth-child(2) text:last-child{margin-top:5rpx;color:#8b98a6;font-size:18rpx}.repair-count{margin-left:auto;text-align:right}.repair-count strong,.repair-count text{display:block}.repair-count strong{color:#cb7b16;font-size:29rpx}.repair-count text{color:#9b8b75;font-size:16rpx}.repair-arrow{margin-left:13rpx;color:#a6b1bd;font-size:32rpx}.error-card{padding:25rpx;border-radius:15rpx;background:#fff1ef;color:#ca5548}.error-card text{display:block;font-size:21rpx}.error-card text:last-child{margin-top:5rpx;color:#a87973;font-size:18rpx}
+.login-page{min-height:calc(100vh - 150rpx);display:flex;align-items:center;flex-direction:column;justify-content:center;background:#f3f6fa}.login-mark{width:112rpx;height:112rpx;display:flex;align-items:center;justify-content:center;border-radius:26rpx;color:#fff;background:linear-gradient(135deg,#176abb,#42a0e5);box-shadow:0 14rpx 30rpx rgba(30,111,181,.2);font-size:33rpx;font-weight:800}.login-page>text:nth-child(2){margin-top:26rpx;color:#23384d;font-size:34rpx;font-weight:700}.login-page>text:nth-child(3){margin-top:7rpx;color:#8a99a8;font-size:21rpx}.login-page button{width:360rpx;margin-top:45rpx;border:0;border-radius:40rpx;color:#fff;background:#2178c5;font-size:24rpx}.login-page button::after{border:0}
+</style>
diff --git a/src/pages/home/index.vue b/src/pages/home/index.vue
index b0c171f..351b979 100644
--- a/src/pages/home/index.vue
+++ b/src/pages/home/index.vue
@@ -1,6 +1,9 @@
<script setup lang="ts">
import { APP_COMPANY } from "../../config/company";
+import ChanghuiPowerHome from "./changhui-power.vue";
import ClassicHome from "./classic.vue";
+import KangyiIntelligentHome from "./kangyi-intelligent.vue";
+import LanyuPaperHome from "./lanyu-paper.vue";
import NorthTechHome from "./north-tech.vue";
import XingaoPowerHome from "./xingao-power.vue";
import ZhongyiAssemblyHome from "./zhongyi-assembly.vue";
@@ -8,7 +11,10 @@
<template>
<XingaoPowerHome v-if="APP_COMPANY.homeVariant === 'xingao-power'" />
+ <ChanghuiPowerHome v-else-if="APP_COMPANY.homeVariant === 'changhui-power'" />
<NorthTechHome v-else-if="APP_COMPANY.homeVariant === 'north-tech'" />
<ZhongyiAssemblyHome v-else-if="APP_COMPANY.homeVariant === 'zhongyi-assembly'" />
+ <KangyiIntelligentHome v-else-if="APP_COMPANY.homeVariant === 'kangyi-intelligent'" />
+ <LanyuPaperHome v-else-if="APP_COMPANY.homeVariant === 'lanyu-paper'" />
<ClassicHome v-else />
</template>
diff --git a/src/pages/home/kangyi-intelligent.vue b/src/pages/home/kangyi-intelligent.vue
new file mode 100644
index 0000000..fa31ae9
--- /dev/null
+++ b/src/pages/home/kangyi-intelligent.vue
@@ -0,0 +1,174 @@
+<script setup lang="ts">
+import { APP_COMPANY } from "../../config/company";
+import { useAppTheme } from "../../stores/theme";
+import { useCompanyDashboard } from "./use-company-dashboard";
+
+const { themeStyle } = useAppTheme();
+
+const {
+ equipmentRate, formatDate, goLogin, goMessages, greeting, isLoggedIn, loadDashboard,
+ loadError, loading, notifyState, openRoute, outputChange, qualityRate, statusPercent,
+ summary, today, totalWorkOrders, trend, trendMax, userName, visibleActions, workOrderStatus,
+} = useCompanyDashboard();
+</script>
+
+<template>
+ <view class="kangyi-page" :style="themeStyle">
+ <template v-if="isLoggedIn">
+ <view class="topbar">
+ <view class="wordmark">
+ <view class="wordmark-symbol"><i /><i /><i /></view>
+ <view><text>{{ APP_COMPANY.name }}</text><text>鏅鸿兘鍒堕�犲崗鍚屽钩鍙�</text></view>
+ </view>
+ <view class="top-message" @click="goMessages">
+ <text>娑堟伅</text><i v-if="notifyState.unreadCount" />
+ </view>
+ </view>
+
+ <view class="main-content">
+ <view class="hello-block">
+ <view><text>{{ greeting }}锛寋{ userName }}</text><text>{{ today }}</text></view>
+ <view class="online-label"><i />绯荤粺鍦ㄧ嚎</view>
+ </view>
+
+ <view v-if="loadError" class="error-block" @click="loadDashboard">
+ <text>{{ loadError }}</text><text>鐐瑰嚮閲嶆柊鍔犺浇</text>
+ </view>
+
+ <template v-else>
+ <view class="focus-board">
+ <view class="focus-main" @click="openRoute('/pages/mes/pro/feedback/index')">
+ <view class="focus-label"><i />浠婃棩鐢熶骇</view>
+ <strong>{{ summary?.todayOutput ?? '--' }}</strong>
+ <text>浠婃棩绱浜ч噺</text>
+ <view :class="['change-label', outputChange >= 0 ? 'change-up' : 'change-down']">
+ 杈冩槰鏃� {{ outputChange >= 0 ? '+' : '' }}{{ outputChange }}
+ </view>
+ </view>
+ <view class="focus-side">
+ <view @click="openRoute('/pages/mes/pro/workorder/index')">
+ <text>杩涜涓伐鍗�</text><strong>{{ summary?.workOrderActiveCount ?? '--' }}</strong><small>寰呮帓浜� {{ summary?.workOrderPrepareCount ?? 0 }}</small>
+ </view>
+ <view @click="openRoute('/pages/mes/dv/repair/index')">
+ <text>璁惧寰呯淮淇�</text><strong>{{ summary?.repairActiveCount ?? 0 }}</strong><small>淇濋殰浠诲姟</small>
+ </view>
+ </view>
+ </view>
+
+ <view class="data-strip">
+ <view><text>璐ㄩ噺鍚堟牸鐜�</text><strong>{{ qualityRate }}%</strong></view>
+ <i />
+ <view><text>璁惧杩愯鐜�</text><strong>{{ equipmentRate }}%</strong></view>
+ <i />
+ <view><text>宸插畬鎴愬伐鍗�</text><strong>{{ summary?.workOrderFinishedCount ?? 0 }}</strong></view>
+ </view>
+
+ <view class="section-title">
+ <view><text>鎴戠殑宸ヤ綔</text><text>甯哥敤涓氬姟鍏ュ彛</text></view>
+ <text @click="openRoute('/pages/workbench/index')">宸ヤ綔鍙� 鈥�</text>
+ </view>
+ <view class="work-list">
+ <view v-for="(item,index) in visibleActions" :key="item.component" class="work-item" @click="openRoute(item.route)">
+ <view class="work-number">{{ String(index + 1).padStart(2, '0') }}</view>
+ <view :class="['work-mark', `mark-${index % 3}`]">{{ item.mark }}</view>
+ <view class="work-copy"><text>{{ item.name }}</text><text>{{ item.detail }}</text></view>
+ <text class="work-arrow">鈥�</text>
+ </view>
+ </view>
+
+ <view class="section-title">
+ <view><text>鐢熶骇鑺傚</text><text>杩戜竷鏃ュ疄闄呬骇鍑�</text></view>
+ <text>{{ loading ? '鏇存柊涓�' : '宸插悓姝�' }}</text>
+ </view>
+ <view class="rhythm-card">
+ <view v-if="trend.length" class="rhythm-list">
+ <view v-for="item in trend" :key="item.date" class="rhythm-row">
+ <text>{{ formatDate(item.date) }}</text>
+ <view class="rhythm-track"><view :style="{ width: `${Math.max(item.quantity / trendMax * 100, 3)}%` }" /></view>
+ <strong>{{ item.quantity }}</strong>
+ </view>
+ </view>
+ <view v-else class="empty-state">鏆傛棤鐢熶骇瓒嬪娍鏁版嵁</view>
+ </view>
+
+ <view class="section-title">
+ <view><text>宸ュ崟杩涘害</text><text>鍚勯樁娈典换鍔″垎甯�</text></view>
+ <text>鍏� {{ totalWorkOrders }} 鍗�</text>
+ </view>
+ <view class="order-card">
+ <view v-for="(item,index) in workOrderStatus" :key="item.status" class="order-row">
+ <view :class="['order-tag', `tag-${index % 4}`]">{{ item.statusName.slice(0, 1) }}</view>
+ <view class="order-info">
+ <view><text>{{ item.statusName }}</text><strong>{{ item.count }} 鍗�</strong></view>
+ <view class="order-track"><view :class="`order-bar-${index % 4}`" :style="{ width: `${statusPercent(item.count)}%` }" /></view>
+ </view>
+ </view>
+ <view v-if="!workOrderStatus.length" class="empty-state">鏆傛棤宸ュ崟鐘舵�佹暟鎹�</view>
+ </view>
+ </template>
+ </view>
+ </template>
+
+ <view v-else class="login-view">
+ <view class="login-symbol"><i /><i /><i /></view>
+ <text>{{ APP_COMPANY.name }}</text>
+ <text>鏅鸿兘鍒堕�犲崗鍚屽钩鍙�</text>
+ <button @click="goLogin">杩涘叆宸ヤ綔鍙�</button>
+ </view>
+ </view>
+</template>
+
+<style scoped lang="scss">
+.kangyi-page{min-height:100vh;padding-bottom:150rpx;background:#f4f5f7;color:#202b33}.topbar,.wordmark,.hello-block,.section-title,.work-item,.rhythm-row,.order-row,.order-info>view:first-child{display:flex;align-items:center}.topbar{justify-content:space-between;padding:calc(var(--status-bar-height) + 22rpx) 28rpx 20rpx;background:#fff;border-bottom:1rpx solid #eceef1}.wordmark{gap:14rpx}.wordmark-symbol,.login-symbol{position:relative;background:#183f49}.wordmark-symbol{width:60rpx;height:60rpx;border-radius:9rpx}.wordmark-symbol i,.login-symbol i{position:absolute;border-radius:50%;background:#6ad7bf}.wordmark-symbol i:nth-child(1),.login-symbol i:nth-child(1){width:13rpx;height:13rpx;left:14rpx;top:24rpx}.wordmark-symbol i:nth-child(2),.login-symbol i:nth-child(2){width:9rpx;height:9rpx;left:31rpx;top:13rpx}.wordmark-symbol i:nth-child(3),.login-symbol i:nth-child(3){width:11rpx;height:11rpx;right:11rpx;bottom:14rpx}.wordmark-symbol:before,.wordmark-symbol:after,.login-symbol:before,.login-symbol:after{position:absolute;height:2rpx;content:"";transform-origin:left center;background:#6ad7bf}.wordmark-symbol:before,.login-symbol:before{width:22rpx;left:22rpx;top:27rpx;transform:rotate(-32deg)}.wordmark-symbol:after,.login-symbol:after{width:19rpx;left:33rpx;top:24rpx;transform:rotate(54deg)}.wordmark text{display:block}.wordmark text:first-child{font-size:28rpx;font-weight:750;letter-spacing:1rpx}.wordmark text:last-child{margin-top:3rpx;color:#8b959c;font-size:17rpx}.top-message{position:relative;padding:12rpx 16rpx;border-radius:7rpx;color:#4f626b;background:#f1f4f5;font-size:19rpx}.top-message i{position:absolute;right:-3rpx;top:-4rpx;width:11rpx;height:11rpx;border:3rpx solid #fff;border-radius:50%;background:#e76161}
+.main-content{padding:24rpx 27rpx}.hello-block{justify-content:space-between;padding:10rpx 3rpx 24rpx}.hello-block>view:first-child text{display:block}.hello-block>view:first-child text:first-child{font-size:34rpx;font-weight:700}.hello-block>view:first-child text:last-child{margin-top:7rpx;color:#89939a;font-size:20rpx}.online-label{padding:9rpx 13rpx;border-radius:6rpx;color:#357966;background:#e5f4ef;font-size:17rpx}.online-label i{display:inline-block;width:8rpx;height:8rpx;margin-right:7rpx;border-radius:50%;background:#36a581}.focus-board{display:grid;grid-template-columns:1.35fr .9fr;overflow:hidden;border-radius:16rpx;background:#183f49;box-shadow:0 12rpx 26rpx rgba(28,58,66,.13);color:#fff}.focus-main{position:relative;min-height:287rpx;padding:28rpx;border-right:1rpx solid rgba(255,255,255,.12)}.focus-label{color:#9fd5ca;font-size:19rpx}.focus-label i{display:inline-block;width:5rpx;height:19rpx;margin-right:9rpx;border-radius:3rpx;background:#65d6bc}.focus-main>strong{display:block;margin-top:27rpx;font-size:70rpx;line-height:1}.focus-main>text{display:block;margin-top:8rpx;color:#bdd0d4;font-size:19rpx}.change-label{position:absolute;left:27rpx;bottom:24rpx;padding:7rpx 11rpx;border-radius:5rpx;font-size:17rpx}.change-up{color:#99e6d3;background:rgba(78,197,167,.14)}.change-down{color:#ffc5ba;background:rgba(226,105,82,.14)}.focus-side{display:flex;flex-direction:column}.focus-side>view{min-height:143rpx;box-sizing:border-box;padding:23rpx}.focus-side>view:first-child{border-bottom:1rpx solid rgba(255,255,255,.12)}.focus-side text,.focus-side strong,.focus-side small{display:block}.focus-side text{color:#a8c1c6;font-size:17rpx}.focus-side strong{margin:5rpx 0;color:#fff;font-size:38rpx}.focus-side small{color:#73969e;font-size:16rpx}.data-strip{display:flex;align-items:center;margin-top:15rpx;padding:20rpx 10rpx;border:1rpx solid #e6e9eb;border-radius:13rpx;background:#fff}.data-strip>view{min-width:0;flex:1;text-align:center}.data-strip text,.data-strip strong{display:block}.data-strip text{color:#90999f;font-size:16rpx}.data-strip strong{margin-top:4rpx;color:#293b43;font-size:25rpx}.data-strip>i{width:1rpx;height:44rpx;background:#e9ecee}
+.section-title{justify-content:space-between;margin:32rpx 3rpx 14rpx}.section-title>view text{display:block}.section-title>view text:first-child{font-size:27rpx;font-weight:700}.section-title>view text:last-child{margin-top:3rpx;color:#9aa2a8;font-size:16rpx}.section-title>text{color:#76878d;font-size:18rpx}.work-list{display:grid;grid-template-columns:1fr 1fr;gap:11rpx}.work-item{position:relative;min-width:0;padding:20rpx 15rpx;border:1rpx solid #e4e8ea;border-radius:12rpx;background:#fff}.work-number{position:absolute;right:11rpx;top:8rpx;color:#e4e8e9;font-size:17rpx;font-weight:700}.work-mark{width:49rpx;height:49rpx;display:flex;align-items:center;justify-content:center;flex:none;border-radius:9rpx;font-size:19rpx;font-weight:700}.mark-0{color:#267866;background:#e4f3ef}.mark-1{color:#476d90;background:#e9f0f6}.mark-2{color:#8c6a34;background:#f7efdf}.work-copy{min-width:0;margin-left:12rpx}.work-copy text{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.work-copy text:first-child{font-size:20rpx;font-weight:650}.work-copy text:last-child{margin-top:3rpx;color:#9ba3a8;font-size:15rpx}.work-arrow{margin-left:auto;color:#b6bdc1;font-size:27rpx}
+.rhythm-card,.order-card{padding:20rpx;border:1rpx solid #e4e8ea;border-radius:14rpx;background:#fff}.rhythm-list{display:flex;flex-direction:column;gap:17rpx}.rhythm-row>text{width:73rpx;color:#7e898f;font-size:16rpx}.rhythm-track{height:12rpx;flex:1;overflow:hidden;border-radius:6rpx;background:#eef1f2}.rhythm-track view{height:100%;border-radius:inherit;background:#4caa96}.rhythm-row strong{width:60rpx;color:#34464e;font-size:18rpx;text-align:right}.order-card{display:grid;grid-template-columns:1fr 1fr;gap:20rpx 18rpx}.order-row{min-width:0}.order-tag{width:46rpx;height:46rpx;display:flex;align-items:center;justify-content:center;flex:none;border-radius:9rpx;font-size:18rpx;font-weight:700}.tag-0{color:#277d6a;background:#e2f3ee}.tag-1{color:#456f96;background:#e5eef6}.tag-2{color:#a27630;background:#f8efdc}.tag-3{color:#865f8e;background:#f1e9f3}.order-info{min-width:0;flex:1;margin-left:11rpx}.order-info>view:first-child{justify-content:space-between}.order-info text{overflow:hidden;color:#5e6b71;font-size:16rpx;text-overflow:ellipsis;white-space:nowrap}.order-info strong{margin-left:5rpx;color:#34464e;font-size:17rpx;white-space:nowrap}.order-track{height:5rpx;margin-top:8rpx;overflow:hidden;border-radius:4rpx;background:#edf0f1}.order-track view{height:100%;border-radius:inherit}.order-bar-0{background:#46a892}.order-bar-1{background:#5c8ab4}.order-bar-2{background:#d4a557}.order-bar-3{background:#a47bac}.empty-state{grid-column:1/-1;padding:38rpx 0;color:#a0a8ad;font-size:19rpx;text-align:center}.error-block{padding:23rpx;border-radius:12rpx;color:#b8554d;background:#fcedeb}.error-block text{display:block;font-size:20rpx}.error-block text:last-child{margin-top:5rpx;color:#a8807c;font-size:17rpx}
+.login-view{min-height:calc(100vh - 150rpx);display:flex;align-items:center;flex-direction:column;justify-content:center;background:#f4f5f7}.login-symbol{width:120rpx;height:120rpx;border-radius:18rpx}.login-symbol i:nth-child(1){width:20rpx;height:20rpx;left:28rpx;top:51rpx}.login-symbol i:nth-child(2){width:15rpx;height:15rpx;left:62rpx;top:27rpx}.login-symbol i:nth-child(3){width:18rpx;height:18rpx;right:21rpx;bottom:28rpx}.login-symbol:before{width:44rpx;left:45rpx;top:56rpx}.login-symbol:after{width:38rpx;left:67rpx;top:50rpx}.login-view>text:nth-child(2){margin-top:27rpx;font-size:35rpx;font-weight:750}.login-view>text:nth-child(3){margin-top:7rpx;color:#8c969c;font-size:21rpx}.login-view button{width:360rpx;margin-top:43rpx;border:0;border-radius:8rpx;color:#fff;background:#183f49;font-size:24rpx}.login-view button::after{border:0}
+
+/* 涓庡簲鐢ㄥ唴鍏朵粬椤甸潰鍏辩敤鍚屼竴濂椾富棰樸�佸崱鐗囧拰鐣欑櫧瑙勫垯锛涘竷灞�浠嶄繚鐣欏悍鎰忕増鏈殑浠诲姟鍨嬬粨鏋勩�� */
+.kangyi-page{
+ box-sizing:border-box;
+ background:radial-gradient(circle at 88% 1%,var(--color-primary-border),transparent 31%),linear-gradient(180deg,var(--color-primary-soft) 0%,#f7f8fc 38%,var(--color-bg-page) 100%);
+ color:#263246;
+}
+.kangyi-page,.kangyi-page view,.kangyi-page text,.kangyi-page button{box-sizing:border-box}
+.topbar{padding:calc(var(--status-bar-height) + 24rpx) 30rpx 18rpx;background:transparent;border-bottom:0}
+.wordmark-symbol,.login-symbol{background:linear-gradient(145deg,var(--color-primary),var(--color-primary-light))}
+.wordmark-symbol{border-radius:18rpx;box-shadow:0 8rpx 18rpx rgba(59,55,104,.14)}
+.wordmark-symbol i,.login-symbol i{background:#fff}
+.wordmark-symbol:before,.wordmark-symbol:after,.login-symbol:before,.login-symbol:after{background:rgba(255,255,255,.82)}
+.wordmark text:last-child{color:#79869a}
+.top-message{display:flex;width:66rpx;height:66rpx;align-items:center;justify-content:center;padding:0;border:1rpx solid rgba(220,225,235,.92);border-radius:50%;color:#627086;background:rgba(255,255,255,.88);box-shadow:0 8rpx 20rpx rgba(49,60,82,.08)}
+.top-message i{right:6rpx;top:5rpx}
+.main-content{padding:8rpx 28rpx 0}
+.hello-block{padding:10rpx 5rpx 26rpx}
+.hello-block>view:first-child text:first-child{font-size:37rpx}
+.hello-block>view:first-child text:last-child{color:#738197;font-size:21rpx}
+.online-label{border-radius:18rpx;color:var(--color-primary-dark);background:rgba(255,255,255,.72)}
+.online-label i{background:var(--color-primary)}
+.focus-board{border-radius:22rpx;background:linear-gradient(145deg,var(--color-primary-dark),var(--color-primary-light));box-shadow:0 16rpx 32rpx rgba(64,57,118,.16)}
+.focus-label,.focus-main>text,.focus-side text{color:rgba(255,255,255,.72)}
+.focus-label i{background:#fff}
+.focus-side small{color:rgba(255,255,255,.52)}
+.change-label{border-radius:12rpx}
+.change-up{color:#fff;background:rgba(255,255,255,.16)}
+.change-down{color:#fff;background:rgba(176,52,75,.25)}
+.data-strip,.work-item,.rhythm-card,.order-card{border:1rpx solid rgba(223,228,238,.92);background:rgba(255,255,255,.88);box-shadow:0 8rpx 20rpx rgba(39,51,74,.04)}
+.data-strip,.work-item,.rhythm-card,.order-card{border-radius:18rpx}
+.data-strip{padding:22rpx 10rpx}
+.data-strip text{color:#8390a2;font-size:17rpx}
+.data-strip strong{color:#334155}
+.section-title{margin-top:34rpx}
+.section-title>view text:first-child{font-size:28rpx}
+.section-title>view text:last-child{color:#929cac;font-size:17rpx}
+.work-item{padding:20rpx 16rpx}
+.work-mark{border-radius:14rpx}
+.mark-0{color:var(--color-primary);background:var(--color-primary-soft)}
+.rhythm-track{background:#e9edf4}
+.rhythm-track view{background:linear-gradient(90deg,var(--color-primary-light),var(--color-primary))}
+.order-track{background:#e9edf4}
+.order-bar-0,.order-bar-1,.order-bar-2,.order-bar-3{background:linear-gradient(90deg,var(--color-primary-light),var(--color-primary))}
+.login-view{background:linear-gradient(180deg,var(--color-primary-soft),var(--color-bg-page))}
+.login-symbol{border-radius:28rpx}
+.login-view button{height:84rpx;border-radius:42rpx;background:var(--color-primary);line-height:84rpx}
+</style>
diff --git a/src/pages/home/lanyu-paper.vue b/src/pages/home/lanyu-paper.vue
new file mode 100644
index 0000000..595779e
--- /dev/null
+++ b/src/pages/home/lanyu-paper.vue
@@ -0,0 +1,1083 @@
+<script setup lang="ts">
+ import { computed, ref } from "vue";
+ import { onPullDownRefresh, onShow } from "@dcloudio/uni-app";
+
+ import {
+ getHomeSummary,
+ getProductionTrend,
+ getWorkOrderStatus,
+ type HomeSummary,
+ type ProductionTrend,
+ type WorkOrderStatus,
+ } from "../../api/home";
+ import type { AuthMenu } from "../../api/auth";
+ import { appRouteMap } from "../../config/app-routes";
+ import { APP_COMPANY } from "../../config/company";
+ import { useAuth } from "../../stores/auth";
+ import { useNotify } from "../../stores/notify";
+ import { useAppTheme } from "../../stores/theme";
+
+ const { state, isLoggedIn, restoreUser } = useAuth();
+ const { state: notifyState, refreshUnreadCount } = useNotify();
+ const { themeStyle } = useAppTheme();
+
+ const summary = ref<HomeSummary | null>(null);
+ const trend = ref<ProductionTrend[]>([]);
+ const workOrderStatus = ref<WorkOrderStatus[]>([]);
+ const loading = ref(false);
+ const loadError = ref("");
+
+ const userName = computed(
+ () => state.user?.nickname || state.user?.username || "鍚屼簨"
+ );
+ const greeting = computed(() => {
+ const hour = new Date().getHours();
+ if (hour < 6) return "澶滄繁浜�";
+ if (hour < 9) return "鏃╀笂濂�";
+ if (hour < 12) return "涓婂崍濂�";
+ if (hour < 14) return "涓崍濂�";
+ if (hour < 18) return "涓嬪崍濂�";
+ return "鏅氫笂濂�";
+ });
+ const today = computed(() => {
+ const date = new Date();
+ const weeks = ["鏃�", "涓�", "浜�", "涓�", "鍥�", "浜�", "鍏�"];
+ return `${date.getMonth() + 1}鏈�${date.getDate()}鏃� 鏄熸湡${
+ weeks[date.getDay()]
+ }`;
+ });
+ const qualityRate = computed(() => {
+ const data = summary.value;
+ if (!data) return "0";
+ const total = data.todayQualifiedQuantity + data.todayUnqualifiedQuantity;
+ return total ? ((data.todayQualifiedQuantity / total) * 100).toFixed(1) : "0";
+ });
+ const equipmentRate = computed(() => {
+ const data = summary.value;
+ if (!data?.machineryTotal) return "0";
+ return ((data.machineryProducing / data.machineryTotal) * 100).toFixed(0);
+ });
+ const outputChange = computed(() => {
+ const data = summary.value;
+ if (!data) return 0;
+ return data.todayOutput - data.yesterdayOutput;
+ });
+ const trendMax = computed(() =>
+ Math.max(...trend.value.map(item => item.quantity), 1)
+ );
+ const totalWorkOrders = computed(() =>
+ workOrderStatus.value.reduce((total, item) => total + item.count, 0)
+ );
+
+ interface QuickAction {
+ name: string;
+ detail: string;
+ icon: string;
+ tone: string;
+ route: string;
+ component: string;
+ }
+
+ const quickActions: QuickAction[] = [
+ {
+ name: "鐢熶骇鎶ュ伐",
+ detail: "鐧昏浜у嚭",
+ icon: "report",
+ tone: "blue",
+ route: "/pages/mes/pro/feedback/index",
+ component: "mes/pro/feedback/index",
+ },
+ {
+ name: "鐢熶骇宸ュ崟",
+ detail: "鏌ョ湅杩涘害",
+ icon: "order",
+ tone: "violet",
+ route: "/pages/mes/pro/workorder/index",
+ component: "mes/pro/workorder/index",
+ },
+ {
+ name: "璐ㄩ噺绠$悊",
+ detail: "璐ㄦ璁板綍",
+ icon: "quality",
+ tone: "orange",
+ route: "/pages/mes/qc/ledger/index",
+ component: "mes/qc/ledger/index",
+ },
+ {
+ name: "璁惧鐐规",
+ detail: "璁惧鐘舵��",
+ icon: "device",
+ tone: "green",
+ route: "/pages/mes/dv/checkrecord/index",
+ component: "mes/dv/checkrecord/index",
+ },
+ {
+ name: "鐗╂枡搴撳瓨",
+ detail: "搴撳瓨鏌ヨ",
+ icon: "stock",
+ tone: "cyan",
+ route: "/pages/wls/material-stock/index",
+ component: "wls/materialstock/index",
+ },
+ {
+ name: "浠撳簱绠$悊",
+ detail: "浠撳簱璧勬枡",
+ icon: "stock",
+ tone: "cyan",
+ route: "/pages/wls/warehouse/index",
+ component: "wls/warehouse/index",
+ },
+ {
+ name: "閲囪喘鐢宠",
+ detail: "鐢宠绠$悊",
+ icon: "report",
+ tone: "violet",
+ route: "/pages/purchase/request/index",
+ component: "erp/purchase/request/index",
+ },
+ {
+ name: "閲囪喘璁㈠崟",
+ detail: "璁㈠崟绠$悊",
+ icon: "order",
+ tone: "violet",
+ route: "/pages/purchase/order/index",
+ component: "erp/purchase/order/index",
+ },
+ {
+ name: "閿�鍞鍗�",
+ detail: "璁㈠崟绠$悊",
+ icon: "order",
+ tone: "blue",
+ route: "/pages/sale/order/index",
+ component: "erp/sale/order/index",
+ },
+ { name: "鍛樺伐绠$悊", detail: "鍛樺伐妗f", icon: "device", tone: "green", route: "/pages/hrm/employee/index", component: "hrm/employee/index" },
+ { name: "璇峰亣鐢宠", detail: "鍙戣捣鐢宠", icon: "report", tone: "orange", route: "/pages/hrm/leave/index", component: "hrm/leave/index" },
+ { name: "璋冨矖鐢宠", detail: "鍙戣捣鐢宠", icon: "order", tone: "cyan", route: "/pages/hrm/transfer/index", component: "hrm/transfer/index" },
+ { name: "绂昏亴鐢宠", detail: "鍙戣捣鐢宠", icon: "report", tone: "red", route: "/pages/hrm/resignation/index", component: "hrm/resignation/index" },
+ { name: "鍔犵彮鐢宠", detail: "鍙戣捣鐢宠", icon: "quality", tone: "blue", route: "/pages/hrm/overtime/index", component: "hrm/overtime/index" },
+ // 钃濈窘绾镐笟鏆備笉灞曠ず鑰冨嫟鎵撳崱锛涙仮澶嶆椂鍙栨秷涓嬮潰鍏ュ彛鐨勬敞閲娿��
+ // {
+ // name: "鑰冨嫟鎵撳崱",
+ // detail: "涓婁笅鐝墦鍗�",
+ // icon: "device",
+ // tone: "green",
+ // route: "/pages/hrm/attendance-clock/index",
+ // component: "hrm/attendance/clock/index",
+ // },
+ ];
+ const ATTENDANCE_COMPONENT = "hrm/attendance/clock/index";
+
+ function collectMenuComponents(menus: AuthMenu[]) {
+ const components = new Set<string>();
+ const visit = (items: AuthMenu[]) =>
+ items.forEach(menu => {
+ if (!menu.visible) return;
+ if (menu.component) components.add(menu.component);
+ if (menu.children?.length) visit(menu.children);
+ });
+ visit(menus);
+ return components;
+ }
+
+ const menuComponents = computed(() => collectMenuComponents(state.menus));
+ const hasMenu = (component: string) => menuComponents.value.has(component);
+ const routeMenus = computed(() => {
+ const result: Array<{ name: string; component: string }> = [];
+ const visit = (items: AuthMenu[]) =>
+ items.forEach(menu => {
+ if (!menu.visible) return;
+ if (menu.component && appRouteMap[menu.component])
+ result.push({ name: menu.name, component: menu.component });
+ if (menu.children?.length) visit(menu.children);
+ });
+ visit(state.menus);
+ return result;
+ });
+ function fallbackStyle(name: string, component: string) {
+ const source = `${name} ${component}`;
+ if (/璐ㄩ噺|妫�楠寍缂洪櫡|涓嶅悎鏍�/.test(source))
+ return { icon: "quality", tone: "orange" };
+ if (/璁惧|鐐规|淇濆吇|缁翠慨|鍛樺伐|鑰冨嫟/.test(source))
+ return { icon: "device", tone: "green" };
+ if (/浠搢搴撳瓨|鐗╂枡|鍏ュ簱|鍑哄簱|鍙戣揣|璋冩嫧/.test(source))
+ return { icon: "stock", tone: "cyan" };
+ if (/閲囪喘|渚涘簲鍟唡浠樻/.test(source)) return { icon: "order", tone: "violet" };
+ if (/绂昏亴|寮傚父|閫�/.test(source)) return { icon: "report", tone: "red" };
+ if (/鐢宠|娴佺▼|瀹℃壒|鎶ュ伐/.test(source))
+ return { icon: "report", tone: "blue" };
+ return { icon: "order", tone: "blue" };
+ }
+ const visibleQuickActions = computed(() => {
+ const preferred = quickActions.filter(
+ item => item.component !== ATTENDANCE_COMPONENT && hasMenu(item.component)
+ );
+ const used = new Set(preferred.map(item => item.component));
+ const fallback = routeMenus.value
+ .filter(item => item.component !== ATTENDANCE_COMPONENT && !used.has(item.component))
+ .map(item => ({
+ name: item.name,
+ detail: "蹇�熻繘鍏�",
+ route: appRouteMap[item.component],
+ component: item.component,
+ ...fallbackStyle(item.name, item.component),
+ }));
+ return preferred.concat(fallback).slice(0, 6);
+ });
+ const canViewWorkOrder = computed(() => hasMenu("mes/pro/workorder/index"));
+ const canViewFeedback = computed(() => hasMenu("mes/pro/feedback/index"));
+ const canViewQuality = computed(() => hasMenu("mes/qc/ledger/index"));
+ const canViewMachinery = computed(() => hasMenu("mes/dv/machinery/index"));
+ const canViewRepair = computed(() => hasMenu("mes/dv/repair/index"));
+
+ const tabRoutes = [
+ "/pages/home/index",
+ "/pages/workbench/index",
+ "/pages/message/index",
+ "/pages/profile/index",
+ ];
+
+ function getAccessToken() {
+ return state.accessToken;
+ }
+
+ async function loadDashboard() {
+ const accessToken = getAccessToken();
+ if (!accessToken || loading.value) return;
+
+ loading.value = true;
+ loadError.value = "";
+ try {
+ const [summaryData, trendData, statusData] = await Promise.all([
+ getHomeSummary(accessToken),
+ getProductionTrend(accessToken),
+ getWorkOrderStatus(accessToken),
+ ]);
+ summary.value = summaryData;
+ trend.value = trendData;
+ workOrderStatus.value = statusData;
+ } catch (error) {
+ loadError.value =
+ error instanceof Error ? error.message : "棣栭〉鏁版嵁鍔犺浇澶辫触";
+ } finally {
+ loading.value = false;
+ }
+ }
+
+ async function refreshPage() {
+ await restoreUser();
+ try {
+ await refreshUnreadCount();
+ } catch {
+ // 鏈娑堟伅鍔犺浇澶辫触涓嶅奖鍝嶉椤靛睍绀�
+ }
+ await loadDashboard();
+ }
+
+ function goMessages() {
+ uni.switchTab({ url: "/pages/message/index" });
+ }
+
+ function goLogin() {
+ uni.navigateTo({ url: "/pages/login/index" });
+ }
+
+ function openRoute(route?: string) {
+ if (!route) {
+ return;
+ }
+ if (tabRoutes.includes(route)) {
+ uni.switchTab({ url: route });
+ return;
+ }
+ uni.navigateTo({ url: route });
+ }
+
+ function formatDate(value: string) {
+ const matched = value.match(/(\d{1,2})[-/](\d{1,2})$/);
+ return matched ? `${matched[1]}/${matched[2]}` : value.slice(5);
+ }
+
+ onShow(() => {
+ refreshPage();
+ });
+
+ onPullDownRefresh(async () => {
+ await refreshPage();
+ uni.stopPullDownRefresh();
+ });
+</script>
+
+<template>
+ <view class="page" :style="themeStyle">
+ <view class="home-hero">
+ <view class="topbar">
+ <view class="welcome-copy">
+ <text class="brand-name">{{ APP_COMPANY.name }}</text>
+ <text class="greeting">{{ greeting }}锛寋{ userName }}</text>
+ <text class="date">{{ today }} 路 鏅洪�犺繍钀ユ瑙�</text>
+ </view>
+ <view class="notice-button"
+ @click="goMessages">
+ <view class="bell">
+ <view />
+ </view>
+ <view v-if="notifyState.unreadCount > 0"
+ class="notice-dot" />
+ </view>
+ </view>
+ <template v-if="isLoggedIn">
+ <view v-if="canViewWorkOrder || canViewFeedback"
+ class="overview-card">
+ <view class="overview-heading">
+ <view>
+ <text>鐢熶骇鎬昏</text>
+ <text>瀹炴椂鎺屾彙鐜板満杩愯鐘舵��</text>
+ </view>
+ <view class="live-status">
+ <view />杩愯涓�
+ </view>
+ </view>
+ <view class="overview-main">
+ <view v-if="canViewWorkOrder"
+ class="main-kpi"
+ @click="openRoute('/pages/mes/pro/workorder/index')">
+ <text>杩涜涓伐鍗�</text>
+ <text>{{ summary?.workOrderActiveCount ?? '--' }}</text>
+ <text>寰呮帓浜� {{ summary?.workOrderPrepareCount ?? 0 }} 鍗�</text>
+ </view>
+ <view v-if="canViewWorkOrder && canViewFeedback"
+ class="overview-divider" />
+ <view v-if="canViewFeedback"
+ class="main-kpi"
+ @click="openRoute('/pages/mes/pro/feedback/index')">
+ <text>浠婃棩浜ч噺</text>
+ <text>{{ summary?.todayOutput ?? '--' }}</text>
+ <text :class="{ increase: outputChange >= 0, decrease: outputChange < 0 }">
+ 杈冩槰鏃� {{ outputChange >= 0 ? '+' : '' }}{{ outputChange }} 浠�
+ </text>
+ </view>
+ </view>
+ </view>
+ </template>
+ </view>
+ <template v-if="isLoggedIn">
+ <view v-if="loadError"
+ class="error-state"
+ @click="loadDashboard">
+ <text>{{ loadError }}</text>
+ <text>鐐瑰嚮閲嶆柊鍔犺浇</text>
+ </view>
+ <template v-else>
+ <view v-if="canViewQuality || canViewMachinery"
+ class="section-heading">
+ <text>鍏抽敭鎸囨爣</text>
+ <text v-if="loading">鏇存柊涓�</text>
+ </view>
+ <view v-if="canViewQuality || canViewMachinery"
+ class="metrics-grid">
+ <view v-if="canViewQuality"
+ class="metric-item"
+ @click="openRoute('/pages/mes/qc/ledger/index')">
+ <view class="metric-icon quality-icon">
+ <view />
+ </view>
+ <view>
+ <text>璐ㄩ噺鍚堟牸鐜�</text>
+ <text>{{ qualityRate }}<small>%</small></text>
+ <text>涓嶈壇 {{ summary?.todayUnqualifiedQuantity ?? 0 }} 浠�</text>
+ </view>
+ </view>
+ <view v-if="canViewMachinery"
+ class="metric-item"
+ @click="openRoute('/pages/mes/dv/machinery/index')">
+ <view class="metric-icon device-icon">
+ <view />
+ </view>
+ <view>
+ <text>璁惧杩愯鐜�</text>
+ <text>{{ equipmentRate }}<small>%</small></text>
+ <text>鍋滄満 {{ summary?.machineryStop ?? 0 }} 鍙�</text>
+ </view>
+ </view>
+ </view>
+ <view v-if="canViewRepair"
+ class="section-heading"><text>鐜板満棰勮</text><text>浼樺厛澶勭悊</text></view>
+ <view v-if="canViewRepair"
+ class="alert-strip">
+ <view class="alert-item repair-alert"
+ @click="openRoute('/pages/mes/dv/repair/index')">
+ <view class="alert-symbol">+</view>
+ <view><text>缁翠慨宸ュ崟</text><text>{{ summary?.repairActiveCount ?? 0 }} <small>椤瑰緟澶勭悊</small></text></view>
+ <text class="arrow">鈥�</text>
+ </view>
+ </view>
+ <view class="section-heading"><text>杩� 7 鏃ョ敓浜ц秼鍔�</text><text>浜ч噺</text></view>
+ <view class="trend-panel">
+ <view v-if="trend.length"
+ class="bar-chart">
+ <view v-for="item in trend"
+ :key="item.date"
+ class="bar-item">
+ <text>{{ item.quantity }}</text>
+ <view class="bar-track">
+ <view class="bar-fill"
+ :style="{ height: `${Math.max((item.quantity / trendMax) * 100, 5)}%` }" />
+ </view>
+ <text>{{ formatDate(item.date) }}</text>
+ </view>
+ </view>
+ <view v-else
+ class="compact-empty">鏆傛棤杩� 7 鏃ョ敓浜ф暟鎹�</view>
+ </view>
+ <view class="section-heading"><text>宸ュ崟鐘舵��</text><text>{{ totalWorkOrders }} 鍗�</text></view>
+ <view class="status-panel">
+ <view v-if="workOrderStatus.length"
+ class="status-list">
+ <view v-for="item in workOrderStatus"
+ :key="item.status"
+ class="status-row">
+ <text>{{ item.statusName }}</text>
+ <view>
+ <view :style="{ width: `${totalWorkOrders ? Math.max((item.count / totalWorkOrders) * 100, 4) : 0}%` }" />
+ </view>
+ <text>{{ item.count }}</text>
+ </view>
+ </view>
+ <view v-else
+ class="compact-empty">鏆傛棤宸ュ崟鐘舵�佹暟鎹�</view>
+ </view>
+ <view v-if="visibleQuickActions.length"
+ class="section-heading"><text>蹇嵎鎿嶄綔</text><text @click="openRoute('/pages/workbench/index')">鍏ㄩ儴搴旂敤</text></view>
+ <view v-if="visibleQuickActions.length"
+ class="quick-grid">
+ <view v-for="item in visibleQuickActions"
+ :key="item.name"
+ class="quick-item"
+ @click="openRoute(item.route)">
+ <view class="quick-icon"
+ :class="`${item.tone} ${item.icon}`">
+ <view />
+ </view>
+ <text>{{ item.name }}</text>
+ <text>{{ item.detail }}</text>
+ </view>
+ </view>
+ </template>
+ </template>
+ <view v-else
+ class="login-state">
+ <view class="login-art">
+ <view />
+ <view />
+ <view />
+ </view>
+ <text>鐧诲綍鍚庢煡鐪嬬敓浜ц繍钀ユ暟鎹�</text>
+ <text>宸ュ崟銆佽川閲忋�佽澶囧拰鐜板満棰勮灏嗗疄鏃跺悓姝�</text>
+ <button @click="goLogin">绔嬪嵆鐧诲綍</button>
+ </view>
+ </view>
+</template>
+
+<style lang="scss" scoped>
+ .page {
+ box-sizing: border-box;
+ overflow-x: hidden;
+ min-height: 100vh;
+ padding: calc(var(--status-bar-height) + 22rpx) 28rpx 156rpx;
+ background: radial-gradient(
+ circle at 86% 2%,
+ var(--color-primary-border, #bfdbff),
+ transparent 34%
+ ),
+ linear-gradient(180deg, var(--color-primary-soft, #eef5ff) 0%, #f7f8fc 42%, #f3f7fb 68%, #eef3f9 100%);
+ }
+ .page,
+ .page view,
+ .page text,
+ .page button {
+ box-sizing: border-box;
+ }
+ .topbar,
+ .overview-heading,
+ .overview-main,
+ .section-heading,
+ .metrics-grid,
+ .alert-strip,
+ .alert-item,
+ .bar-chart,
+ .status-row {
+ display: flex;
+ }
+ .topbar {
+ align-items: center;
+ justify-content: space-between;
+ padding: 8rpx 8rpx 30rpx;
+ }
+ .welcome-copy text,
+ .overview-heading text,
+ .main-kpi text,
+ .metric-item text,
+ .alert-item text,
+ .quick-item text,
+ .login-state text {
+ display: block;
+ }
+ .greeting {
+ color: #10233d;
+ font-size: 42rpx;
+ font-weight: 700;
+ line-height: 1.18;
+ }
+ .brand-name {
+ display: inline-block;
+ margin-bottom: 10rpx;
+ color: var(--color-primary, #1769d8);
+ font-size: 23rpx;
+ font-weight: 700;
+ }
+ .date {
+ margin-top: 13rpx;
+ color: #6f7f95;
+ font-size: 23rpx;
+ }
+ .home-hero {
+ position: relative;
+ overflow: visible;
+ margin-bottom: 34rpx;
+ padding: 12rpx 4rpx 0;
+ }
+ .home-hero > view {
+ position: relative;
+ z-index: 1;
+ }
+ .notice-button {
+ position: relative;
+ display: flex;
+ width: 76rpx;
+ height: 76rpx;
+ align-items: center;
+ justify-content: center;
+ background: rgba(255, 255, 255, 0.86);
+ border: 1rpx solid rgba(218, 229, 243, 0.92);
+ border-radius: 50%;
+ box-shadow: 0 10rpx 24rpx rgba(44, 77, 112, 0.1);
+ }
+ .bell {
+ position: relative;
+ width: 26rpx;
+ height: 28rpx;
+ border: 3rpx solid #23344f;
+ border-top-left-radius: 16rpx;
+ border-top-right-radius: 16rpx;
+ border-bottom: 0;
+ }
+ .bell::before {
+ position: absolute;
+ right: -7rpx;
+ bottom: -4rpx;
+ left: -7rpx;
+ height: 3rpx;
+ content: "";
+ background: #23344f;
+ border-radius: 3rpx;
+ }
+ .bell view {
+ position: absolute;
+ bottom: -9rpx;
+ left: 8rpx;
+ width: 7rpx;
+ height: 7rpx;
+ background: #23344f;
+ border-radius: 50%;
+ }
+ .notice-dot {
+ position: absolute;
+ top: 15rpx;
+ right: 16rpx;
+ width: 12rpx;
+ height: 12rpx;
+ background: #ef5c5c;
+ border: 3rpx solid rgba(255, 255, 255, 0.9);
+ border-radius: 50%;
+ }
+ .overview-card {
+ overflow: hidden;
+ padding: 30rpx 28rpx;
+ color: #14243c;
+ background: rgba(255, 255, 255, 0.82);
+ border: 1rpx solid rgba(218, 229, 243, 0.8);
+ border-radius: 24rpx;
+ box-shadow: 0 12rpx 26rpx rgba(44, 77, 112, 0.08);
+ }
+ .overview-heading {
+ align-items: flex-start;
+ justify-content: space-between;
+ }
+ .overview-heading text:first-child {
+ color: #11243d;
+ font-size: 30rpx;
+ font-weight: 650;
+ }
+ .overview-heading text:last-child {
+ margin-top: 7rpx;
+ color: #718198;
+ font-size: 21rpx;
+ }
+ .live-status {
+ display: flex;
+ align-items: center;
+ gap: 8rpx;
+ padding: 8rpx 13rpx;
+ color: #12835a;
+ font-size: 19rpx;
+ background: #e5f7ee;
+ border-radius: 20rpx;
+ }
+ .live-status view {
+ width: 10rpx;
+ height: 10rpx;
+ background: #71e2a8;
+ border-radius: 50%;
+ }
+ .overview-main {
+ margin-top: 30rpx;
+ }
+ .main-kpi {
+ flex: 1;
+ }
+ .main-kpi text:first-child {
+ color: #697991;
+ font-size: 22rpx;
+ }
+ .main-kpi text:nth-child(2) {
+ margin-top: 3rpx;
+ color: var(--color-primary, #1769d8);
+ font-size: 53rpx;
+ font-weight: 700;
+ line-height: 1.12;
+ }
+ .main-kpi text:last-child {
+ margin-top: 9rpx;
+ color: #7a8798;
+ font-size: 20rpx;
+ }
+ .overview-divider {
+ width: 1rpx;
+ margin: 6rpx 30rpx;
+ background: #dfe8f3;
+ }
+ .increase {
+ color: #12945f !important;
+ }
+ .decrease {
+ color: #d64e4e !important;
+ }
+ .section-heading {
+ align-items: center;
+ justify-content: space-between;
+ margin: 42rpx 3rpx 18rpx;
+ color: #263247;
+ font-size: 30rpx;
+ font-weight: 700;
+ }
+ .section-heading > text:last-child:not(:first-child) {
+ color: #7d899a;
+ font-size: 22rpx;
+ font-weight: 400;
+ }
+ .metrics-grid {
+ gap: 14rpx;
+ }
+ .metric-item {
+ display: flex;
+ flex: 1;
+ min-width: 0;
+ min-height: 126rpx;
+ align-items: center;
+ gap: 16rpx;
+ padding: 20rpx 18rpx;
+ background: rgba(255, 255, 255, 0.58);
+ border: 1rpx solid rgba(221, 230, 242, 0.92);
+ border-radius: 16rpx;
+ }
+ .metric-icon {
+ position: relative;
+ display: flex;
+ width: 48rpx;
+ height: 48rpx;
+ flex: none;
+ align-items: center;
+ justify-content: center;
+ border-radius: 14rpx;
+ }
+ .metric-icon view {
+ width: 20rpx;
+ height: 20rpx;
+ border: 3rpx solid currentColor;
+ border-radius: 50%;
+ }
+ .quality-icon {
+ color: #dc8a16;
+ background: #fff1da;
+ }
+ .device-icon {
+ color: #248f63;
+ background: #e1f5eb;
+ }
+ .device-icon view {
+ border-radius: 5rpx;
+ }
+ .metric-item text:first-child {
+ color: #7c8999;
+ font-size: 19rpx;
+ }
+ .metric-item text:nth-child(2) {
+ margin-top: 2rpx;
+ color: #263348;
+ font-size: 32rpx;
+ font-weight: 700;
+ line-height: 1.1;
+ }
+ .metric-item small,
+ .alert-item small {
+ font-size: 19rpx;
+ font-weight: 400;
+ }
+ .metric-item text:last-child {
+ margin-top: 6rpx;
+ color: #96a0ad;
+ font-size: 18rpx;
+ }
+ .alert-strip {
+ position: relative;
+ overflow: hidden;
+ align-items: stretch;
+ background: linear-gradient(90deg, #fff7f7 0, #fff 48%, #fffbf4 100%);
+ border: 1rpx solid rgba(245, 218, 210, 0.9);
+ border-radius: 20rpx;
+ box-shadow: 0 14rpx 30rpx rgba(154, 74, 49, 0.08);
+ }
+ .alert-strip::before {
+ width: 8rpx;
+ content: "";
+ background: linear-gradient(180deg, #ff7373, #f2aa45);
+ }
+ .alert-item {
+ flex: 1;
+ min-width: 0;
+ align-items: center;
+ padding: 25rpx 17rpx;
+ }
+ .alert-symbol {
+ display: flex;
+ width: 43rpx;
+ height: 43rpx;
+ flex: none;
+ align-items: center;
+ justify-content: center;
+ margin-right: 13rpx;
+ font-size: 28rpx;
+ font-weight: 700;
+ border-radius: 50%;
+ }
+ .repair-alert .alert-symbol {
+ color: #d98513;
+ background: #fff0da;
+ }
+ .alert-item text:first-of-type {
+ color: #758294;
+ font-size: 20rpx;
+ }
+ .alert-item text:last-of-type {
+ margin-top: 3rpx;
+ color: #313d50;
+ font-size: 30rpx;
+ font-weight: 700;
+ }
+ .alert-divider {
+ width: 1rpx;
+ margin: 22rpx 0;
+ background: #edf0f5;
+ }
+ .arrow {
+ margin-left: auto;
+ color: #a1abb8;
+ font-size: 34rpx;
+ }
+ .trend-panel,
+ .status-panel {
+ padding: 24rpx 22rpx;
+ background: rgba(255, 255, 255, 0.72);
+ border: 1rpx solid rgba(223, 232, 245, 0.9);
+ border-radius: 18rpx;
+ }
+ .bar-chart {
+ height: 190rpx;
+ align-items: flex-end;
+ justify-content: space-between;
+ gap: 12rpx;
+ }
+ .bar-item {
+ display: flex;
+ height: 100%;
+ flex: 1;
+ min-width: 0;
+ align-items: center;
+ flex-direction: column;
+ justify-content: flex-end;
+ }
+ .bar-item text:first-child {
+ height: 28rpx;
+ overflow: hidden;
+ color: #657387;
+ font-size: 18rpx;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ .bar-track {
+ display: flex;
+ width: 100%;
+ height: 118rpx;
+ align-items: flex-end;
+ justify-content: center;
+ margin: 4rpx 0 8rpx;
+ background: #e9f0f8;
+ border-radius: 8rpx;
+ }
+ .bar-fill {
+ width: 58%;
+ min-height: 8rpx;
+ background: linear-gradient(180deg, var(--color-primary-light, #4b8ee8), var(--color-primary, #1769d8));
+ border-radius: 8rpx 8rpx 3rpx 3rpx;
+ }
+ .bar-item text:last-child {
+ color: #8a96a6;
+ font-size: 18rpx;
+ }
+ .compact-empty {
+ padding: 40rpx 0;
+ color: #99a3b1;
+ font-size: 22rpx;
+ text-align: center;
+ }
+ .status-list {
+ display: flex;
+ flex-direction: column;
+ gap: 18rpx;
+ }
+ .status-row {
+ align-items: center;
+ gap: 15rpx;
+ }
+ .status-row > text:first-child {
+ width: 112rpx;
+ overflow: hidden;
+ color: #5e6b7c;
+ font-size: 22rpx;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ .status-row > view {
+ height: 12rpx;
+ flex: 1;
+ overflow: hidden;
+ background: #e8eff7;
+ border-radius: 12rpx;
+ }
+ .status-row > view > view {
+ height: 100%;
+ background: linear-gradient(90deg, var(--color-primary-light, #4b8ee8), var(--color-primary, #1769d8));
+ border-radius: 12rpx;
+ }
+ .status-row > text:last-child {
+ width: 34rpx;
+ color: #334155;
+ font-size: 22rpx;
+ font-weight: 600;
+ text-align: right;
+ }
+ .quick-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 17rpx;
+ }
+ .quick-item {
+ min-height: 158rpx;
+ padding: 20rpx 12rpx;
+ background: #fff;
+ border-radius: 18rpx;
+ box-shadow: 0 8rpx 18rpx rgba(30, 53, 82, 0.04);
+ }
+ .quick-icon {
+ position: relative;
+ display: flex;
+ width: 52rpx;
+ height: 52rpx;
+ align-items: center;
+ justify-content: center;
+ border-radius: 15rpx;
+ }
+ .quick-icon::before,
+ .quick-icon::after,
+ .quick-icon view {
+ position: absolute;
+ box-sizing: border-box;
+ content: "";
+ }
+ .quick-icon::before {
+ width: 23rpx;
+ height: 23rpx;
+ border: 3rpx solid currentColor;
+ border-radius: 5rpx;
+ }
+ .quick-icon::after {
+ width: 11rpx;
+ height: 3rpx;
+ background: currentColor;
+ }
+ .blue {
+ color: var(--color-primary, #1769d8);
+ background: var(--color-primary-soft, #eef5ff);
+ }
+ .violet {
+ color: #695bd8;
+ background: #eeecff;
+ }
+ .orange {
+ color: #da8619;
+ background: #fff0db;
+ }
+ .red {
+ color: #dc5757;
+ background: #ffeded;
+ }
+ .green {
+ color: #269464;
+ background: #e2f5eb;
+ }
+ .cyan {
+ color: #238eaa;
+ background: #e0f5fa;
+ }
+ .report::after {
+ transform: rotate(-45deg);
+ }
+ .order::before {
+ border-radius: 50%;
+ }
+ .quality::after {
+ width: 12rpx;
+ height: 7rpx;
+ background: transparent;
+ border-bottom: 3rpx solid currentColor;
+ border-left: 3rpx solid currentColor;
+ transform: rotate(-45deg);
+ }
+ .andon::before {
+ border-radius: 50%;
+ }
+ .andon::after {
+ width: 3rpx;
+ height: 13rpx;
+ }
+ .device::before {
+ width: 25rpx;
+ height: 19rpx;
+ border-radius: 4rpx;
+ }
+ .stock::before {
+ transform: rotate(45deg);
+ border-radius: 3rpx;
+ }
+ .quick-item text:nth-child(2) {
+ margin-top: 14rpx;
+ overflow: hidden;
+ color: #3a4659;
+ font-size: 23rpx;
+ font-weight: 600;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ .quick-item text:last-child {
+ margin-top: 4rpx;
+ overflow: hidden;
+ color: #929cab;
+ font-size: 18rpx;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ .error-state {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-top: 24rpx;
+ padding: 24rpx;
+ color: #b55050;
+ font-size: 23rpx;
+ background: #fff2f2;
+ border-radius: 16rpx;
+ }
+ .error-state text:last-child {
+ color: #d56767;
+ font-size: 21rpx;
+ }
+ .login-state {
+ display: flex;
+ align-items: center;
+ flex-direction: column;
+ padding-top: 180rpx;
+ text-align: center;
+ }
+ .login-art {
+ position: relative;
+ width: 126rpx;
+ height: 108rpx;
+ margin-bottom: 35rpx;
+ background: #e3efff;
+ border-radius: 30rpx;
+ }
+ .login-art view {
+ position: absolute;
+ background: #2d75d6;
+ border-radius: 10rpx;
+ }
+ .login-art view:nth-child(1) {
+ bottom: 24rpx;
+ left: 28rpx;
+ width: 15rpx;
+ height: 29rpx;
+ }
+ .login-art view:nth-child(2) {
+ bottom: 24rpx;
+ left: 55rpx;
+ width: 15rpx;
+ height: 47rpx;
+ }
+ .login-art view:nth-child(3) {
+ bottom: 24rpx;
+ left: 82rpx;
+ width: 15rpx;
+ height: 63rpx;
+ }
+ .login-state text:nth-of-type(1) {
+ color: #364358;
+ font-size: 30rpx;
+ font-weight: 600;
+ }
+ .login-state text:nth-of-type(2) {
+ margin-top: 12rpx;
+ color: #929cab;
+ font-size: 23rpx;
+ }
+ .login-state button {
+ width: 360rpx;
+ height: 84rpx;
+ margin-top: 42rpx;
+ color: #fff;
+ font-size: 27rpx;
+ line-height: 84rpx;
+ background: var(--color-primary, #1769d8);
+ border-radius: 42rpx;
+ }
+</style>
diff --git a/src/pages/home/use-company-dashboard.ts b/src/pages/home/use-company-dashboard.ts
new file mode 100644
index 0000000..63b13f5
--- /dev/null
+++ b/src/pages/home/use-company-dashboard.ts
@@ -0,0 +1,158 @@
+import { computed, ref } from "vue";
+import { onPullDownRefresh, onShow } from "@dcloudio/uni-app";
+
+import type { AuthMenu } from "../../api/auth";
+import {
+ getHomeSummary,
+ getProductionTrend,
+ getWorkOrderStatus,
+ type HomeSummary,
+ type ProductionTrend,
+ type WorkOrderStatus,
+} from "../../api/home";
+import { appRouteMap } from "../../config/app-routes";
+import { useAuth } from "../../stores/auth";
+import { useNotify } from "../../stores/notify";
+
+export interface CompanyHomeAction {
+ name: string;
+ detail: string;
+ mark: string;
+ tone: string;
+ route: string;
+ component: string;
+}
+
+const ATTENDANCE_COMPONENT = "hrm/attendance/clock/index";
+const actions: CompanyHomeAction[] = [
+ { name: "鑰冨嫟鎵撳崱", detail: "涓婁笅鐝墦鍗�", mark: "鍕�", tone: "teal", route: "/pages/hrm/attendance-clock/index", component: ATTENDANCE_COMPONENT },
+ { name: "鐢熶骇鎶ュ伐", detail: "鐧昏鐢熶骇浜у嚭", mark: "鎶�", tone: "blue", route: "/pages/mes/pro/feedback/index", component: "mes/pro/feedback/index" },
+ { name: "鐢熶骇宸ュ崟", detail: "璺熻釜浣滀笟杩涘害", mark: "鍗�", tone: "violet", route: "/pages/mes/pro/workorder/index", component: "mes/pro/workorder/index" },
+ { name: "璐ㄩ噺绠$悊", detail: "鏌ョ湅璐ㄦ璁板綍", mark: "璐�", tone: "amber", route: "/pages/mes/qc/ledger/index", component: "mes/qc/ledger/index" },
+ { name: "璁惧鐐规", detail: "鎺屾彙璁惧鐘舵��", mark: "妫�", tone: "green", route: "/pages/mes/dv/checkrecord/index", component: "mes/dv/checkrecord/index" },
+ { name: "鐗╂枡搴撳瓨", detail: "鏌ヨ搴撳瓨鍙拌处", mark: "搴�", tone: "cyan", route: "/pages/wls/material-stock/index", component: "wls/materialstock/index" },
+ { name: "閲囪喘璁㈠崟", detail: "閲囪喘涓氬姟绠$悊", mark: "閲�", tone: "violet", route: "/pages/purchase/order/index", component: "erp/purchase/order/index" },
+ { name: "閿�鍞鍗�", detail: "閿�鍞笟鍔$鐞�", mark: "閿�", tone: "blue", route: "/pages/sale/order/index", component: "erp/sale/order/index" },
+];
+
+function collectMenuComponents(menus: AuthMenu[]) {
+ const result = new Set<string>();
+ const visit = (items: AuthMenu[]) => items.forEach(item => {
+ if (!item.visible) return;
+ if (item.component) result.add(item.component);
+ if (item.children?.length) visit(item.children);
+ });
+ visit(menus);
+ return result;
+}
+
+export function useCompanyDashboard() {
+ const { state, isLoggedIn, restoreUser } = useAuth();
+ const { state: notifyState, refreshUnreadCount } = useNotify();
+ const summary = ref<HomeSummary | null>(null);
+ const trend = ref<ProductionTrend[]>([]);
+ const workOrderStatus = ref<WorkOrderStatus[]>([]);
+ const loading = ref(false);
+ const loadError = ref("");
+
+ const userName = computed(() => state.user?.nickname || state.user?.username || "鍚屼簨");
+ const greeting = computed(() => {
+ const hour = new Date().getHours();
+ if (hour < 6) return "澶滄繁浜�";
+ if (hour < 9) return "鏃╀笂濂�";
+ if (hour < 12) return "涓婂崍濂�";
+ if (hour < 14) return "涓崍濂�";
+ if (hour < 18) return "涓嬪崍濂�";
+ return "鏅氫笂濂�";
+ });
+ const today = computed(() => {
+ const value = new Date();
+ const weeks = ["鏃�", "涓�", "浜�", "涓�", "鍥�", "浜�", "鍏�"];
+ return `${value.getMonth() + 1}鏈�${value.getDate()}鏃� 鏄熸湡${weeks[value.getDay()]}`;
+ });
+ const qualityRate = computed(() => {
+ if (!summary.value) return "0.0";
+ const total = summary.value.todayQualifiedQuantity + summary.value.todayUnqualifiedQuantity;
+ return total ? (summary.value.todayQualifiedQuantity / total * 100).toFixed(1) : "0.0";
+ });
+ const equipmentRate = computed(() => {
+ if (!summary.value?.machineryTotal) return "0";
+ return (summary.value.machineryProducing / summary.value.machineryTotal * 100).toFixed(0);
+ });
+ const outputChange = computed(() => (summary.value?.todayOutput || 0) - (summary.value?.yesterdayOutput || 0));
+ const trendMax = computed(() => Math.max(...trend.value.map(item => item.quantity), 1));
+ const totalWorkOrders = computed(() => workOrderStatus.value.reduce((total, item) => total + item.count, 0));
+ const menuComponents = computed(() => collectMenuComponents(state.menus));
+ const visibleActions = computed(() => {
+ const preferred = actions.filter(item => item.component === ATTENDANCE_COMPONENT || menuComponents.value.has(item.component));
+ const used = new Set(preferred.map(item => item.component));
+ const fallback: CompanyHomeAction[] = [];
+ const visit = (items: AuthMenu[]) => items.forEach(item => {
+ if (!item.visible) return;
+ if (item.component && appRouteMap[item.component] && !used.has(item.component)) {
+ used.add(item.component);
+ fallback.push({
+ name: item.name,
+ detail: "蹇�熻繘鍏�",
+ mark: item.name.slice(0, 1),
+ tone: "slate",
+ route: appRouteMap[item.component],
+ component: item.component,
+ });
+ }
+ if (item.children?.length) visit(item.children);
+ });
+ visit(state.menus);
+ return preferred.concat(fallback).slice(0, 6);
+ });
+
+ const tabRoutes = ["/pages/home/index", "/pages/workbench/index", "/pages/message/index", "/pages/profile/index"];
+ function openRoute(route?: string) {
+ if (!route) return;
+ if (tabRoutes.includes(route)) uni.switchTab({ url: route });
+ else uni.navigateTo({ url: route });
+ }
+ function goMessages() { uni.switchTab({ url: "/pages/message/index" }); }
+ function goLogin() { uni.navigateTo({ url: "/pages/login/index" }); }
+ function formatDate(value: string) {
+ const matched = value.match(/(\d{1,2})[-/](\d{1,2})$/);
+ return matched ? `${matched[1]}/${matched[2]}` : value.slice(5);
+ }
+ function statusPercent(count: number) {
+ return totalWorkOrders.value ? Math.max(count / totalWorkOrders.value * 100, 4) : 0;
+ }
+
+ async function loadDashboard() {
+ if (!state.accessToken || loading.value) return;
+ loading.value = true;
+ loadError.value = "";
+ try {
+ const [summaryData, trendData, statusData] = await Promise.all([
+ getHomeSummary(state.accessToken),
+ getProductionTrend(state.accessToken),
+ getWorkOrderStatus(state.accessToken),
+ ]);
+ summary.value = summaryData;
+ trend.value = trendData;
+ workOrderStatus.value = statusData;
+ } catch (cause) {
+ loadError.value = cause instanceof Error ? cause.message : "棣栭〉鏁版嵁鍔犺浇澶辫触";
+ } finally {
+ loading.value = false;
+ }
+ }
+ async function refreshPage() {
+ await restoreUser();
+ try { await refreshUnreadCount(); } catch { /* 娑堟伅寮傚父涓嶅奖鍝嶉椤� */ }
+ await loadDashboard();
+ }
+
+ onShow(refreshPage);
+ onPullDownRefresh(async () => { await refreshPage(); uni.stopPullDownRefresh(); });
+
+ return {
+ equipmentRate, formatDate, goLogin, goMessages, greeting, isLoggedIn, loadDashboard,
+ loadError, loading, notifyState, openRoute, outputChange, qualityRate, statusPercent,
+ summary, today, totalWorkOrders, trend, trendMax, userName, visibleActions, workOrderStatus,
+ };
+}
diff --git a/src/pages/hrm/attendance-clock/index.vue b/src/pages/hrm/attendance-clock/index.vue
index db759fd..5c8df9c 100644
--- a/src/pages/hrm/attendance-clock/index.vue
+++ b/src/pages/hrm/attendance-clock/index.vue
@@ -77,6 +77,10 @@
const isInsecureH5 = computed(() => {
// #ifdef H5
return typeof window !== "undefined" && !window.isSecureContext;
+ // #endif
+ // #ifndef H5
+ return false;
+ // #endif
});
const locationStatusText = computed(() => {
if (!locationRequired.value) return "褰撳墠瑙勫垯鏈惎鐢ㄤ綅缃檺鍒�";
diff --git a/src/pages/workbench/index.vue b/src/pages/workbench/index.vue
index eff3570..c0c6e89 100644
--- a/src/pages/workbench/index.vue
+++ b/src/pages/workbench/index.vue
@@ -215,27 +215,30 @@
const apps = computed(() => {
const menuApps = flattenMenus(state.menus);
+ // 钃濈窘绾镐笟鏆傛椂闅愯棌鑰冨嫟鎵撳崱锛涙仮澶嶆椂绉婚櫎涓嬮潰鐨勮繃婊わ紝骞舵仮澶� attendance 鍚堝苟閫昏緫銆�
+ const visibleMenuApps = APP_COMPANY.key === "lanyu"
+ ? menuApps.filter(app => app.component !== "hrm/attendance/clock/index")
+ : menuApps;
// 鈥滄垜鐨勭敵璇封�濇ā鍧楁殏鏃堕殣钘忥紱鎭㈠鏃跺彇娑堜笅涓�琛屾敞閲婏紝骞跺湪杩斿洖鍊间腑閲嶆柊鍚堝苟 selfServiceApps銆�
// const selfServiceApps = getSelfServiceApps(menuApps);
- const attendance =
- !isModuleEnabled("鑰冨嫟瑙勫垯") ||
- menuApps.some(app => app.component === "hrm/attendance/clock/index")
- ? []
- : [
- {
- id: -6009,
- name: "鑰冨嫟鎵撳崱",
- group: "浜哄姏璧勬簮",
- component: "hrm/attendance/clock/index",
- componentName: "HrmAttendanceClock",
- developed: true,
- tone: "cyan",
- icon: "system",
- shape: "shape-5",
- mark: "mark-9",
- },
- ];
- return menuApps.concat(attendance);
+ // const attendance =
+ // !isModuleEnabled("鑰冨嫟瑙勫垯") ||
+ // visibleMenuApps.some(app => app.component === "hrm/attendance/clock/index")
+ // ? []
+ // : [{
+ // id: -6009,
+ // name: "鑰冨嫟鎵撳崱",
+ // group: "浜哄姏璧勬簮",
+ // component: "hrm/attendance/clock/index",
+ // componentName: "HrmAttendanceClock",
+ // developed: true,
+ // tone: "cyan",
+ // icon: "system",
+ // shape: "shape-5",
+ // mark: "mark-9",
+ // }];
+ // 鎭㈠鍏ュ彛鏃跺皢杩斿洖鍊兼敼涓� visibleMenuApps.concat(attendance)銆�
+ return visibleMenuApps;
});
const groups = computed(() => [
"鍏ㄩ儴",
diff --git a/src/stores/theme.ts b/src/stores/theme.ts
index a016e30..703ec3a 100644
--- a/src/stores/theme.ts
+++ b/src/stores/theme.ts
@@ -1,6 +1,7 @@
import { computed, readonly, ref } from "vue";
+import { APP_COMPANY } from "../config/company";
-export type ThemeKey = "blue" | "green" | "purple";
+export type ThemeKey = "blue" | "electric" | "green" | "aurora" | "purple";
export interface AppTheme {
key: ThemeKey;
@@ -15,7 +16,7 @@
cardBackground: string;
}
-export const THEME_STORAGE_KEY = "mom_app_theme";
+export const THEME_STORAGE_KEY = `mom_app_theme_${APP_COMPANY.key}`;
export const appThemes: AppTheme[] = [
{
@@ -43,6 +44,30 @@
cardBackground: "#ffffff",
},
{
+ key: "electric",
+ name: "鐢靛姏娣辫摑",
+ description: "鍐峰郴娓呮櫚锛岀獊鍑虹數鍔涜繍琛岀姸鎬�",
+ primary: "#0879e8",
+ primaryDark: "#0756ac",
+ primaryLight: "#47a9ff",
+ primarySoft: "#e9f5ff",
+ primaryBorder: "#b7ddff",
+ pageBackground: "#f2f7fb",
+ cardBackground: "#ffffff",
+ },
+ {
+ key: "aurora",
+ name: "鏅鸿兘鏋佸厜",
+ description: "绱潚娓愬彉锛屼綋鐜版櫤鑳界鎶�鎰�",
+ primary: "#7257df",
+ primaryDark: "#5640b7",
+ primaryLight: "#9a84f3",
+ primarySoft: "#f2efff",
+ primaryBorder: "#d9d0ff",
+ pageBackground: "#f7f5fc",
+ cardBackground: "#ffffff",
+ },
+ {
key: "purple",
name: "绉戞妧绱�",
description: "灞傛椴滄槑锛岄�傚悎鏁版嵁鍦烘櫙",
@@ -56,7 +81,12 @@
},
];
-const defaultTheme = appThemes[0];
+const companyDefaultThemeKey: ThemeKey = APP_COMPANY.key === "changhui"
+ ? "electric"
+ : APP_COMPANY.key === "kangyi"
+ ? "aurora"
+ : "blue";
+const defaultTheme = appThemes.find(item => item.key === companyDefaultThemeKey) || appThemes[0];
const currentTheme = ref<AppTheme>(defaultTheme);
function findTheme(key?: string) {
--
Gitblit v1.9.3