Merge branch 'master' into dev_新高电力
# Conflicts:
# src/config/company.ts
# src/manifest.json
# src/pages/hrm/attendance-clock/index.vue
# src/pages/workbench/index.vue
| | |
| | | id?: number; userId?: number; userName?: string; deptId?: number; deptName?: string; phone?: string; date?: string; |
| | | clockInTime?: string; clockOutTime?: string; clockInType?: number; clockInTypeName?: string; clockOutType?: number; |
| | | clockOutTypeName?: string; workHours?: number; overtimeHours?: number; location?: string; dataSource?: number; |
| | | leaveFlag?: boolean; leaveHours?: number; leaveTypes?: string; |
| | | dataSourceName?: string; remark?: string; createTime?: string; |
| | | } |
| | | export interface AttendanceClockable { |
| | |
| | | export interface AttendanceMyStatus { |
| | | userId?: number; employeeId?: number; employeeName?: string; deptId?: number; deptName?: string; |
| | | date?: string; serverTime?: string; workDay?: boolean; restDayReason?: string | null; |
| | | onLeave?: boolean; leaveHours?: number; leaveTypeNames?: string; |
| | | rule?: AttendanceRule | null; record?: AttendanceRecord | null; clockable?: AttendanceClockable; |
| | | } |
| | | export interface AttendanceClockRequest { |
| | |
| | | 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 ä¸è´ï¼ä¾¿äºææ¥å¿ / ç¼åå½åç©ºé´ */ |
| | |
| | | } |
| | | |
| | | /** |
| | | * äºå¥ç¯å¢å¯¹ç
§è¡¨ã |
| | | * å
«å¥ç¯å¢å¯¹ç
§è¡¨ã |
| | | * - apiBaseUrl ç»ä¸ä»¥ `/admin-api` ç»å°¾ï¼yudao åå°æ¥å£åç¼ï¼ã |
| | | * - webBaseUrlï¼åªæä¸»åæ¯ï¼MOM æºè½æºé ï¼æ¯æ¢æé
ç½®ï¼9501ï¼ï¼å
¶ä½æä¸»åæ¯ï¼MOM æºè½æºé ï¼âæ¥å£ç«¯å£ +1âçé¨ç½²è§å¾ |
| | | * æ¨å¯¼ï¼9500 â 9501ï¼ã9017 / 9027 / 9025 宿µè½æå¼åå°ï¼1.95.78.124:9007 ç®åè¿ä¸ä¸ |
| | |
| | | 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", |
| | | name: "åæ¹ç§æ", |
| | |
| | | import type { SheetOption } from "../../../mes/process-design/shared"; |
| | | |
| | | const { state } = useAuth(); |
| | | const SELECTED_DEFINITION_KEY = "bpm_create_selected_definition"; |
| | | const selectedDefinition = ref<BpmProcessDefinition>(); |
| | | |
| | | const definitionId = ref(""); |
| | | /** è°ç¨æ¹æ¾å¼ä¼ å
¥çæµç¨å®ä¹ idï¼ä»åè¡¨é¡µéæ© / éæ°åèµ·æ¶è§£æåºçææ°çæ¬ï¼ */ |
| | |
| | | |
| | | // å®ä¹ä¼å
级ï¼è°ç¨æ¹æ¾å¼æå®ç idï¼å·²æ¯ææ°çæ¬ï¼ |
| | | // > å®ä¾ key 对åºçææ°çæ¬ > å®ä¾ç»å®çå®ä¹ id |
| | | let def: BpmProcessDefinition | undefined; |
| | | if (explicitDefinitionId.value) { |
| | | let def: BpmProcessDefinition | undefined = selectedDefinition.value; |
| | | if (!def?.id && explicitDefinitionId.value) { |
| | | def = await getProcessDefinition(state.accessToken, { id: explicitDefinitionId.value }); |
| | | } |
| | | if (!def?.id && instanceKey) { |
| | |
| | | explicitDefinitionId.value = query?.id ? String(query.id) : ""; |
| | | definitionId.value = explicitDefinitionId.value; |
| | | processInstanceId.value = query?.processInstanceId ? String(query.processInstanceId) : ""; |
| | | try { |
| | | const cached = uni.getStorageSync(SELECTED_DEFINITION_KEY) as BpmProcessDefinition | undefined; |
| | | if (cached?.id && String(cached.id) === explicitDefinitionId.value) { |
| | | selectedDefinition.value = cached; |
| | | } |
| | | uni.removeStorageSync(SELECTED_DEFINITION_KEY); |
| | | } catch { |
| | | selectedDefinition.value = undefined; |
| | | } |
| | | initForm(); |
| | | }); |
| | | |
| | |
| | | import { toast } from "../../shared"; |
| | | |
| | | const { state } = useAuth(); |
| | | const SELECTED_DEFINITION_KEY = "bpm_create_selected_definition"; |
| | | |
| | | const categories = ref<BpmCategory[]>([]); |
| | | const definitions = ref<BpmProcessDefinition[]>([]); |
| | |
| | | toast("æµç¨å®ä¹ä¿¡æ¯ä¸å®æ´"); |
| | | return; |
| | | } |
| | | // ä¸ Web 端ä¸è´ï¼ç´æ¥å¤ç¨å表æ¥å£å·²ç»è¿åç表åé
ç½®ã |
| | | // æµç¨å®ä¹è¯¦æ
æ¥å£å¼å¸¸æ¶ï¼ä¹ä¸åºé»æå¡«å表åã |
| | | uni.setStorageSync(SELECTED_DEFINITION_KEY, definition); |
| | | uni.navigateTo({ |
| | | url: `/pages/bpm/process-instance/create/form?id=${encodeURIComponent(String(definition.id))}`, |
| | | }); |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script setup lang="ts"> |
| | | import { APP_COMPANY } from "../../config/company"; |
| | | import { useCompanyDashboard } from "./use-company-dashboard"; |
| | | |
| | | const { |
| | | attendanceOverview, equipmentRate, formatDate, goLogin, goMessages, greeting, isLoggedIn, loadDashboard, |
| | | loadError, loading, notifyState, openRoute, outputChange, qualityRate, statusPercent, |
| | | summary, supplyOverview, 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 v-if="attendanceOverview" class="section-heading"><text>æ¬æåºå¤</text><text @click="openRoute('/pages/hrm/attendance-summary/index')">æ¥çæ±æ»</text></view> |
| | | <view v-if="attendanceOverview" class="metric-card"> |
| | | <view><strong>{{ attendanceOverview.headCount }}</strong><text>ç»è®¡äººæ°</text></view> |
| | | <view><strong>{{ attendanceOverview.attendanceRate }}<small>%</small></strong><text>åºå¤ç</text></view> |
| | | <view><strong>{{ attendanceOverview.averageActualDays }}</strong><text>人ååºå¤å¤©æ°</text></view> |
| | | <view><strong>{{ attendanceOverview.lateCount }}</strong><text>è¿å°æ¬¡æ°</text></view> |
| | | <view><strong>{{ attendanceOverview.absentCount }}</strong><text>ç¼ºå¤æ¬¡æ°</text></view> |
| | | <view><strong>{{ attendanceOverview.leaveHours }}</strong><text>请åå°æ¶</text></view> |
| | | </view> |
| | | |
| | | <view v-if="supplyOverview" class="section-heading"><text>åºåä¸éè´</text><text>宿¶æ±æ»</text></view> |
| | | <view v-if="supplyOverview" class="metric-card supply-card"> |
| | | <view @click="openRoute('/pages/wls/material-stock/index')"><strong>{{ supplyOverview.stockQuantity }}</strong><text>å¨åºæ»é</text></view> |
| | | <view @click="openRoute('/pages/wls/material-stock/index')"><strong>{{ supplyOverview.availableQuantity }}</strong><text>å¯ç¨åºå</text></view> |
| | | <view @click="openRoute('/pages/purchase/order/index')"><strong>{{ supplyOverview.purchaseTotal }}</strong><text>éè´è®¢å</text></view> |
| | | <view @click="openRoute('/pages/purchase/order/index')"><strong>{{ supplyOverview.pendingPurchaseTotal }}</strong><text>å¾
å¤çéè´</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} |
| | | .metric-card{display:grid;grid-template-columns:repeat(3,1fr);overflow:hidden;border:1rpx solid #e4ebf2;border-radius:18rpx;background:#fff;box-shadow:0 6rpx 22rpx rgba(34,70,105,.05)}.metric-card>view{padding:22rpx 10rpx;border-right:1rpx solid #edf1f5;border-bottom:1rpx solid #edf1f5;text-align:center}.metric-card>view:nth-child(3n){border-right:0}.metric-card strong,.metric-card text{display:block}.metric-card strong{color:#1d5f9e;font-size:31rpx}.metric-card small{font-size:17rpx}.metric-card text{margin-top:7rpx;color:#7d8c9d;font-size:18rpx}.supply-card{grid-template-columns:repeat(2,1fr)}.supply-card>view:nth-child(3n){border-right:1rpx solid #edf1f5}.supply-card>view:nth-child(2n){border-right:0} |
| | | </style> |
| | |
| | | <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"; |
| | |
| | | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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: "å工档æ¡", 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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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 { getAttendanceSummaryPage } from "../../api/hrm"; |
| | | import { getPurchaseOrderPage } from "../../api/purchase"; |
| | | import { getMaterialStockPage } from "../../api/wls"; |
| | | 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; |
| | | } |
| | | |
| | | export interface AttendanceOverview { |
| | | headCount: number; |
| | | attendanceRate: string; |
| | | averageActualDays: string; |
| | | lateCount: number; |
| | | absentCount: number; |
| | | leaveHours: number; |
| | | } |
| | | |
| | | export interface SupplyOverview { |
| | | stockQuantity: number; |
| | | availableQuantity: number; |
| | | purchaseTotal: number; |
| | | pendingPurchaseTotal: number; |
| | | } |
| | | |
| | | 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 attendanceOverview = ref<AttendanceOverview | null>(null); |
| | | const supplyOverview = ref<SupplyOverview | null>(null); |
| | | 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 now = new Date(); |
| | | const yearMonth = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`; |
| | | const [summaryData, trendData, statusData, attendanceData, stockData, purchaseData, pendingPurchaseData] = await Promise.all([ |
| | | getHomeSummary(state.accessToken), |
| | | getProductionTrend(state.accessToken), |
| | | getWorkOrderStatus(state.accessToken), |
| | | getAttendanceSummaryPage(state.accessToken, { pageNo: 1, pageSize: 200, yearMonth }).catch(() => null), |
| | | getMaterialStockPage(state.accessToken, { pageNo: 1, pageSize: 200 }).catch(() => null), |
| | | getPurchaseOrderPage(state.accessToken, { pageNo: 1, pageSize: 1 }).catch(() => null), |
| | | getPurchaseOrderPage(state.accessToken, { pageNo: 1, pageSize: 1, status: 10 }).catch(() => null), |
| | | ]); |
| | | summary.value = summaryData; |
| | | trend.value = trendData; |
| | | workOrderStatus.value = statusData; |
| | | if (attendanceData) { |
| | | const rows = attendanceData.list; |
| | | const workDays = rows.reduce((total, item) => total + Number(item.workDays || 0), 0); |
| | | const actualDays = rows.reduce((total, item) => total + Number(item.actualDays || 0), 0); |
| | | attendanceOverview.value = { |
| | | headCount: attendanceData.total, |
| | | attendanceRate: workDays ? (actualDays / workDays * 100).toFixed(1) : "0.0", |
| | | averageActualDays: rows.length ? (actualDays / rows.length).toFixed(1) : "0.0", |
| | | lateCount: rows.reduce((total, item) => total + Number(item.lateCount || 0), 0), |
| | | absentCount: rows.reduce((total, item) => total + Number(item.absentCount || 0), 0), |
| | | leaveHours: rows.reduce((total, item) => total + Number(item.leaveHours || 0), 0), |
| | | }; |
| | | } |
| | | if (stockData || purchaseData || pendingPurchaseData) { |
| | | const stockRows = stockData?.list || []; |
| | | supplyOverview.value = { |
| | | stockQuantity: stockRows.reduce((total, item) => total + Number(item.quantity || 0), 0), |
| | | availableQuantity: stockRows.reduce((total, item) => total + Number(item.availableQuantity || 0), 0), |
| | | purchaseTotal: purchaseData?.total || 0, |
| | | pendingPurchaseTotal: pendingPurchaseData?.total || 0, |
| | | }; |
| | | } |
| | | } 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 { |
| | | attendanceOverview, equipmentRate, formatDate, goLogin, goMessages, greeting, isLoggedIn, loadDashboard, |
| | | loadError, loading, notifyState, openRoute, outputChange, qualityRate, statusPercent, |
| | | summary, supplyOverview, today, totalWorkOrders, trend, trendMax, userName, visibleActions, workOrderStatus, |
| | | }; |
| | | } |
| | |
| | | mark: string; |
| | | tone: string; |
| | | component: string; |
| | | permanent?: boolean; |
| | | } |
| | | |
| | | const peopleCards: DashboardCard[] = [ |
| | |
| | | ]; |
| | | |
| | | const quickEntries: QuickEntry[] = [ |
| | | { name: "è夿å¡", detail: "ä¸ä¸çæå¡", mark: "å¤", tone: "orange", component: "hrm/attendance/clock/index" }, |
| | | { name: "è夿å¡", detail: "ä¸ä¸çæå¡", mark: "å¤", tone: "orange", component: "hrm/attendance/clock/index", permanent: true }, |
| | | // 人äºç³è¯·å
¥å£æä¸å¨é¦é¡µå±ç¤ºã |
| | | // { name: "请åç³è¯·", detail: "æäº¤è¯·å", mark: "å", tone: "blue", component: "hrm/leave/index" }, |
| | | // { name: "å çç³è¯·", detail: "æäº¤å ç", mark: "ç", tone: "violet", component: "hrm/overtime/index" }, |
| | |
| | | const route = routeFor(component); |
| | | if (route) uni.navigateTo({ url: route }); |
| | | } |
| | | function openQuickEntry(item: QuickEntry) { |
| | | const route = item.permanent ? appRouteMap[item.component] : routeFor(item.component); |
| | | if (route) uni.navigateTo({ url: route }); |
| | | } |
| | | |
| | | const userName = computed(() => state.user?.nickname || state.user?.username || "åäº"); |
| | | const greeting = computed(() => { |
| | |
| | | const weeks = ["æ¥", "ä¸", "äº", "ä¸", "å", "äº", "å
"]; |
| | | return `${value.getMonth() + 1}æ${value.getDate()}æ¥ ææ${weeks[value.getDay()]}`; |
| | | }); |
| | | const availableQuickEntries = computed(() => quickEntries.filter(item => canOpen(item.component))); |
| | | const availableQuickEntries = computed(() => quickEntries.filter(item => item.permanent || canOpen(item.component))); |
| | | |
| | | function displayCount(key: string) { |
| | | const value = counts.value[key]; |
| | |
| | | <view class="section quick-section"> |
| | | <view class="section-head"><view><text>å¿«æ·æä½</text><text>åå·¥èªå©æå¡</text></view><text @click="goWorkbench">å
¨é¨åºç¨</text></view> |
| | | <view v-if="availableQuickEntries.length" class="quick-grid"> |
| | | <view v-for="item in availableQuickEntries" :key="item.component" class="quick-card" @click="openComponent(item.component)"> |
| | | <view v-for="item in availableQuickEntries" :key="item.component" class="quick-card" @click="openQuickEntry(item)"> |
| | | <view :class="['quick-mark', item.tone]">{{ item.mark }}</view><text>{{ item.name }}</text><text>{{ item.detail }}</text> |
| | | </view> |
| | | </view> |
| | |
| | | const isInsecureH5 = computed(() => { |
| | | // #ifdef H5 |
| | | return typeof window !== "undefined" && !window.isSecureContext; |
| | | // #endif |
| | | // #ifndef H5 |
| | | return false; |
| | | // #endif |
| | | }); |
| | | const locationStatusText = computed(() => { |
| | | if (!locationRequired.value) return "å½åè§åæªå¯ç¨ä½ç½®éå¶"; |
| | |
| | | "å½åç¨æ·" |
| | | ); |
| | | const department = computed(() => status.value?.deptName || "æªåé
é¨é¨"); |
| | | const onLeave = computed(() => status.value?.onLeave === true || record.value?.leaveFlag === true); |
| | | const leaveText = computed(() => { |
| | | const names = status.value?.leaveTypeNames || "请å"; |
| | | const hours = Number(status.value?.leaveHours ?? record.value?.leaveHours ?? 0); |
| | | return hours > 0 ? `${names} ${hours.toFixed(2)} å°æ¶` : names; |
| | | }); |
| | | const dateLabel = computed(() => { |
| | | const rest = |
| | | status.value && status.value.workDay === false |
| | |
| | | <text class="smart-hint">{{ punch.hint }}</text> |
| | | <text class="rule-time">çæ¬¡ {{ rule?.workStartTime || "--:--" }} - {{ rule?.workEndTime || "--:--" }}</text> |
| | | </view> |
| | | <view v-if="onLeave" class="leave-alert"> |
| | | <strong>仿¥å·²è¯·åï¼å¦éä¸ç坿£å¸¸æå¡</strong> |
| | | <text>{{ leaveText }} · 请åå½å¤©æå¡ä¸è®¡è¿å°ãæ©éæç¼ºå¡</text> |
| | | </view> |
| | | <!-- #ifdef H5 --> |
| | | <view v-if="locationRequired && mapVisible" |
| | | class="map-panel"> |
| | |
| | | align-items: center; |
| | | padding: 38rpx 0 30rpx; |
| | | } |
| | | .leave-alert { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 8rpx; |
| | | padding: 20rpx 24rpx; |
| | | margin-bottom: 20rpx; |
| | | color: #245a91; |
| | | background: #eaf4ff; |
| | | border: 1rpx solid #b8d8f7; |
| | | border-radius: 14rpx; |
| | | } |
| | | .leave-alert strong { font-size: 27rpx; } |
| | | .leave-alert text { color: #5d7894; font-size: 23rpx; } |
| | | .time { |
| | | display: block; |
| | | font-size: 66rpx; |
| | |
| | | const pageSize = 15; |
| | | const canQuery = computed(() => hasPermission("hrm:attendance-record:query")); |
| | | const hasMore = computed(() => rows.value.length < total.value); |
| | | const leaveNames: Record<string, string> = { "1":"äºå", "2":"ç
å", "3":"å¹´å", "4":"å©å", "5":"产å", "6":"丧å" }; |
| | | function leaveText(row: AttendanceRecord) { |
| | | if (!row.leaveFlag) return ""; |
| | | const names = String(row.leaveTypes || "").split(",").map(type => leaveNames[type.trim()] || type.trim()).filter(Boolean).join("ã") || "请å"; |
| | | return Number(row.leaveHours) > 0 ? `${names} ${Number(row.leaveHours).toFixed(2)}h` : names; |
| | | } |
| | | async function load(reset = true) { |
| | | if (!state.accessToken || !canQuery.value || loading.value) return; |
| | | if (reset) { pageNo.value = 1; total.value = 0; } |
| | |
| | | onShow(() => load(true)); |
| | | onPullDownRefresh(() => load(true)); |
| | | onReachBottom(() => { if (hasMore.value) load(false); }); |
| | | </script><template><view class="page"><template v-if="canQuery"><view class="search"><input v-model="keyword" placeholder="æç´¢åå·¥å§å" @confirm="load"/><text @click="load">æç´¢</text></view><view v-for="row in rows" :key="row.id" class="card"><view class="head"><view class="head-main"><text class="name">{{row.userName||'--'}}</text><text class="sub">{{row.deptName||'--'}} · {{row.date||'--'}}</text></view><text class="tag tag-blue">{{row.dataSourceName||'èå¤'}}</text></view><view class="grid"><text>ä¸çï¼{{row.clockInTime||'--'}}</text><text>ä¸çï¼{{row.clockOutTime||'--'}}</text><text>ä¸çç¶æï¼{{row.clockInTypeName||'--'}}</text><text>ä¸çç¶æï¼{{row.clockOutTypeName||'--'}}</text><text>å·¥æ¶ï¼{{row.workHours??0}} å°æ¶</text><text>å çï¼{{row.overtimeHours??0}} å°æ¶</text><text>å°ç¹ï¼{{row.location||'--'}}</text></view></view><view v-if="!rows.length" class="state">{{loading?'å è½½ä¸...':'ææ èå¤è®°å½'}}</view><view v-else class="state more">{{loading ? "å è½½ä¸..." : hasMore ? "䏿å è½½æ´å¤" : "æ²¡ææ´å¤äº"}}</view></template><view v-else class="state">ææ èå¤è®°å½æ¥çæé</view></view></template><style scoped lang="scss">@import "../list.scss";</style> |
| | | </script><template><view class="page"><template v-if="canQuery"><view class="search"><input v-model="keyword" placeholder="æç´¢åå·¥å§å" @confirm="load"/><text @click="load">æç´¢</text></view><view v-for="row in rows" :key="row.id" class="card"><view class="head"><view class="head-main"><text class="name">{{row.userName||'--'}}</text><text class="sub">{{row.deptName||'--'}} · {{row.date||'--'}}</text></view><view class="tags"><text v-if="row.leaveFlag" class="tag tag-blue">{{leaveText(row)}}</text><text class="tag tag-blue">{{row.dataSourceName||'èå¤'}}</text></view></view><view class="grid"><text>ä¸çï¼{{row.clockInTime||'--'}}</text><text>ä¸çï¼{{row.clockOutTime||'--'}}</text><text>ä¸çç¶æï¼{{row.clockInTypeName||'--'}}</text><text>ä¸çç¶æï¼{{row.clockOutTypeName||'--'}}</text><text>å·¥æ¶ï¼{{row.workHours??0}} å°æ¶</text><text>å çï¼{{row.overtimeHours??0}} å°æ¶</text><text>å°ç¹ï¼{{row.location||'--'}}</text></view></view><view v-if="!rows.length" class="state">{{loading?'å è½½ä¸...':'ææ èå¤è®°å½'}}</view><view v-else class="state more">{{loading ? "å è½½ä¸..." : hasMore ? "䏿å è½½æ´å¤" : "æ²¡ææ´å¤äº"}}</view></template><view v-else class="state">ææ èå¤è®°å½æ¥çæé</view></view></template><style scoped lang="scss">@import "../list.scss";</style> |
| | |
| | | .page{min-height:100vh;padding:24rpx 30rpx 70rpx;box-sizing:border-box;background:#f6f7f9}.section-title{padding:18rpx 6rpx 12rpx;color:#728197;font-size:23rpx}.form-card{overflow:hidden;background:#fff;border-radius:14rpx}.field{display:flex;align-items:center;min-height:96rpx;padding:0 24rpx;gap:18rpx;border-bottom:1rpx solid #eef1f5}.field>text:first-child{width:205rpx;flex:none;color:#33455d;font-size:27rpx}.field input,.field picker,.picker,.value{min-width:0;flex:1;color:#1e3049;font-size:27rpx}.field input,.value{text-align:right}.picker{display:flex;width:100%;align-items:center;justify-content:flex-end;gap:10rpx}.picker text:first-child{min-width:0;overflow:hidden;text-align:right;text-overflow:ellipsis;white-space:nowrap}.picker text:last-child{color:#8092a7;font-size:36rpx}.datetime{display:flex;min-width:0;flex:1;justify-content:flex-end;gap:16rpx}.datetime picker{flex:none}.datetime text{color:#1e3049;font-size:25rpx}.column{align-items:flex-start;flex-direction:column;padding:20rpx 24rpx}.column>text:first-child{width:auto}.column textarea{width:100%;height:150rpx;font-size:27rpx}.required{color:#e23b3b}.save{margin-top:32rpx;color:#fff;background:var(--color-primary, #1769d8);border-radius:10rpx}.save::after{border:0}.state{padding:38rpx;color:#8795a8;text-align:center} |
| | | .page{min-height:100vh;padding:24rpx 30rpx 70rpx;box-sizing:border-box;background:#f6f7f9}.section-title{padding:18rpx 6rpx 12rpx;color:#728197;font-size:23rpx}.form-card{overflow:hidden;background:#fff;border-radius:14rpx}.field{display:flex;align-items:center;min-height:96rpx;padding:0 24rpx;gap:18rpx;border-bottom:1rpx solid #eef1f5}.field>text:first-child{width:205rpx;flex:none;color:#33455d;font-size:27rpx}.field input,.field picker,.picker,.value{min-width:0;flex:1;color:#1e3049;font-size:27rpx}.field input,.value{text-align:right}.picker{display:flex;width:100%;align-items:center;justify-content:flex-end;gap:10rpx}.picker text:first-child{min-width:0;overflow:hidden;text-align:right;text-overflow:ellipsis;white-space:nowrap}.picker text:last-child{color:#8092a7;font-size:36rpx}.datetime{display:flex;min-width:0;flex:1;justify-content:flex-end;gap:16rpx}.datetime picker{flex:none}.datetime text{color:#1e3049;font-size:25rpx}.column{align-items:flex-start;flex-direction:column;padding:20rpx 24rpx}.column>text:first-child{width:auto}.column textarea{width:100%;height:150rpx;font-size:27rpx}.required{color:#e23b3b}.form-actions{display:flex;gap:20rpx;margin-top:32rpx}.form-actions .save{min-width:0;flex:1;margin-top:0}.save{margin-top:32rpx;color:#fff;background:var(--color-primary, #1769d8);border-radius:10rpx}.save.secondary{color:var(--color-primary, #1769d8);background:#fff;border:1rpx solid var(--color-primary, #1769d8)}.save::after{border:0}.state{padding:38rpx;color:#8795a8;text-align:center} |
| | |
| | | <script setup lang="ts"> |
| | | import { computed, reactive, ref } from "vue"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import { createLeave, getHrmDicts, getLeave, updateLeave, type HrmAttachment, type Leave } from "../../../api/hrm"; |
| | | import { createLeave, getAutoApprovalPolicy, getHrmDicts, getLeave, getLeaveProcesses, submitLeave, updateLeave, type ApproveProcess, type HrmAttachment, type Leave } from "../../../api/hrm"; |
| | | import type { DictData } from "../../../api/crm"; |
| | | import { useAuth } from "../../../stores/auth"; |
| | | import { HRM_DICT, label, options, setDateTime } from "../shared"; |
| | |
| | | const id = ref<number>(); |
| | | const mode = ref("edit"); |
| | | const saving = ref(false); |
| | | const submitting = ref(false); |
| | | const dicts = ref<DictData[]>([]); |
| | | const attachments = ref<HrmAttachment[]>([]); |
| | | const form = reactive<Leave>({ userId: undefined, leaveType: undefined, startTime: "", endTime: "", duration: 0.5, reason: "", blobIds: [] }); |
| | | const readonly = computed(() => mode.value === "detail"); |
| | | const canSave = computed(() => !readonly.value && (id.value ? hasPermission("hrm:leave:update") : hasPermission("hrm:leave:create"))); |
| | | const canSubmit = computed(() => canSave.value && hasPermission("hrm:leave:submit")); |
| | | const typeOptions = computed(() => options(dicts.value, HRM_DICT.leaveType)); |
| | | const userText = computed(() => form.userName || state.user?.nickname || state.user?.username || "å½åç»å½ç¨æ·"); |
| | | |
| | | async function save() { |
| | | function validateForm() { |
| | | if (!form.userId || form.leaveType === undefined || !form.startTime || !form.endTime || !form.duration || !form.reason?.trim()) { |
| | | return uni.showToast({ title: "è¯·å®æ´å¡«åå¿
填项", icon: "none" }); |
| | | uni.showToast({ title: "è¯·å®æ´å¡«åå¿
填项", icon: "none" }); |
| | | return false; |
| | | } |
| | | if (new Date(form.endTime.replace(/-/g, "/")) <= new Date(form.startTime.replace(/-/g, "/"))) { |
| | | return uni.showToast({ title: "ç»ææ¶é´åºæäºå¼å§æ¶é´", icon: "none" }); |
| | | uni.showToast({ title: "ç»ææ¶é´åºæäºå¼å§æ¶é´", icon: "none" }); |
| | | return false; |
| | | } |
| | | if (!state.accessToken || saving.value) return; |
| | | return true; |
| | | } |
| | | |
| | | async function persistDraft() { |
| | | if (!state.accessToken) throw new Error("ç»å½ç¶æå·²å¤±æ"); |
| | | const data = { ...form, id: id.value, userId: state.user?.id }; |
| | | if (id.value) await updateLeave(state.accessToken, data); |
| | | else id.value = await createLeave(state.accessToken, data); |
| | | if (!id.value) throw new Error("è稿ä¿å失败"); |
| | | const latest = await getLeave(state.accessToken, id.value); |
| | | Object.assign(form, latest, { blobIds: latest.attachmentList?.map(item => item.id) || latest.blobIds || [] }); |
| | | attachments.value = latest.attachmentList || []; |
| | | return latest; |
| | | } |
| | | |
| | | function chooseProcess(processes: ApproveProcess[]) { |
| | | return new Promise<ApproveProcess | undefined>(resolve => { |
| | | uni.showActionSheet({ |
| | | title: "è¯·éæ©è¯·åå®¡æ¹æµç¨", |
| | | itemList: processes.map(item => item.name || item.key), |
| | | success: result => resolve(processes[result.tapIndex]), |
| | | fail: () => resolve(undefined), |
| | | }); |
| | | }); |
| | | } |
| | | |
| | | async function save() { |
| | | if (!validateForm() || !state.accessToken || saving.value || submitting.value) return; |
| | | saving.value = true; |
| | | try { |
| | | const data = { ...form, id: id.value, userId: state.user?.id }; |
| | | if (id.value) await updateLeave(state.accessToken, data); |
| | | else id.value = await createLeave(state.accessToken, data); |
| | | await persistDraft(); |
| | | uni.showToast({ title: "ä¿åæå", icon: "success" }); |
| | | setTimeout(() => uni.navigateBack(), 500); |
| | | } catch (error) { |
| | | uni.showToast({ title: error instanceof Error ? error.message : "ä¿å失败", icon: "none" }); |
| | | } finally { |
| | | saving.value = false; |
| | | } |
| | | } |
| | | |
| | | async function saveAndSubmit() { |
| | | if (!validateForm() || !state.accessToken || saving.value || submitting.value) return; |
| | | submitting.value = true; |
| | | let draftSaved = false; |
| | | try { |
| | | const latest = await persistDraft(); |
| | | draftSaved = true; |
| | | if (!latest.id) throw new Error("æªè·åå°ææ°ç³è¯·ä¿¡æ¯"); |
| | | |
| | | const automaticApproval = await getAutoApprovalPolicy(state.accessToken).catch(() => false); |
| | | let processKey = "hrm_leave"; |
| | | if (!automaticApproval) { |
| | | const processes = await getLeaveProcesses(state.accessToken); |
| | | if (!processes.length) throw new Error("ææ å¯ç¨å®¡æ¹æµç¨"); |
| | | const process = await chooseProcess(processes); |
| | | if (!process) { |
| | | uni.showToast({ title: "å·²ä¿åè稿", icon: "success" }); |
| | | return; |
| | | } |
| | | processKey = process.key; |
| | | } |
| | | await submitLeave(state.accessToken, latest.id, processKey); |
| | | uni.showToast({ title: "æäº¤æå", icon: "success" }); |
| | | setTimeout(() => uni.navigateBack(), 500); |
| | | } catch (error) { |
| | | const message = error instanceof Error ? error.message : "æäº¤å¤±è´¥"; |
| | | uni.showToast({ title: draftSaved ? `è稿已ä¿åï¼${message}` : message, icon: "none" }); |
| | | } finally { |
| | | submitting.value = false; |
| | | } |
| | | } |
| | | |
| | |
| | | </view> |
| | | <view class="field"><text>è¯·åæ¶é¿ <text class="required">*</text></text><input v-model.number="form.duration" :disabled="readonly" type="digit" placeholder="åä½ï¼å¤©" /></view> |
| | | <view class="field column"><text>请ååå <text class="required">*</text></text><textarea v-model="form.reason" :disabled="readonly" maxlength="500" placeholder="请è¾å
¥è¯·ååå " /></view> |
| | | <HrmAttachmentField v-model="form.blobIds" v-model:files="attachments" :token="state.accessToken" :readonly="readonly" /> |
| | | <HrmAttachmentField class="leave-attachment" v-model="form.blobIds" v-model:files="attachments" :token="state.accessToken" :readonly="readonly" /> |
| | | <view class="field column"><text>夿³¨</text><textarea v-model="form.remark" :disabled="readonly" placeholder="请è¾å
¥å¤æ³¨" /></view> |
| | | </view> |
| | | <button v-if="canSave" class="save" :loading="saving" @click="save">ä¿åè稿</button> |
| | | <view v-if="canSave" class="form-actions"> |
| | | <button class="save secondary" :loading="saving" :disabled="submitting" @click="save">ä¿åè稿</button> |
| | | <button v-if="canSubmit" class="save" :loading="submitting" :disabled="saving" @click="saveAndSubmit">ç´æ¥æäº¤</button> |
| | | </view> |
| | | <button v-else-if="readonly&&form.status===0&&hasPermission('hrm:leave:update')" class="save" @click="mode='edit'">ç¼è¾</button> |
| | | </view> |
| | | </template> |
| | | |
| | | <style lang="scss" scoped>@import "../form.scss";</style> |
| | | <style lang="scss" scoped> |
| | | @import "../form.scss"; |
| | | .leave-attachment{box-sizing:border-box;padding-right:24rpx;padding-left:24rpx} |
| | | </style> |
| | |
| | | const total = ref(0); |
| | | const loading = ref(false); |
| | | const error = ref(""); |
| | | const canQuery = computed(() => hasPermission("hrm:leave:query")); |
| | | const canCreate = computed(() => hasPermission("hrm:leave:create")); |
| | | const hasMore = computed(() => rows.value.length < total.value); |
| | | |
| | | const isOwn = (row: Leave) => Number(row.userId) === Number(state.user?.id); |
| | | const canOperate = (row: Leave) => isOwn(row); |
| | | |
| | | async function load(reset = false) { |
| | | if (!state.accessToken || loading.value || !canQuery.value) return; |
| | | if (!state.accessToken || loading.value) return; |
| | | if (reset) { pageNo.value = 1; rows.value = []; } |
| | | loading.value = true; |
| | | error.value = ""; |
| | | try { |
| | | if (!dicts.value.length) dicts.value = await getHrmDicts(state.accessToken); |
| | | const data = await getLeavePage(state.accessToken, { pageNo: pageNo.value, pageSize: 15, userId: state.user?.id }); |
| | | const data = await getLeavePage(state.accessToken, { |
| | | pageNo: pageNo.value, |
| | | pageSize: 15, |
| | | userId: state.user?.id, |
| | | }); |
| | | const page = data.list || []; |
| | | total.value = data.total || 0; |
| | | rows.value = reset ? page : rows.value.concat(page); |
| | |
| | | |
| | | function chooseProcess(processes: ApproveProcess[]) { |
| | | return new Promise<ApproveProcess | undefined>(resolve => { |
| | | if (processes.length === 1) return resolve(processes[0]); |
| | | uni.showActionSheet({ |
| | | title: "è¯·éæ©è¯·åå®¡æ¹æµç¨", |
| | | itemList: processes.map(item => item.name || item.key), |
| | | success: result => resolve(processes[result.tapIndex]), |
| | | fail: () => resolve(undefined) |
| | |
| | | } |
| | | |
| | | async function submit(row: Leave) { |
| | | if (!state.accessToken || !row.id) return; |
| | | if (!state.accessToken || !row.id || !canOperate(row)) return; |
| | | try { |
| | | const automaticApproval = await getAutoApprovalPolicy(state.accessToken).catch(() => false); |
| | | let processKey = "hrm_leave"; |
| | |
| | | } |
| | | |
| | | function remove(row: Leave) { |
| | | if (!state.accessToken || !row.id) return; |
| | | if (!state.accessToken || !row.id || !canOperate(row)) return; |
| | | uni.showModal({ |
| | | title: "å é¤ç³è¯·", |
| | | content: "ç¡®å®å é¤è¯¥è¯·åç³è¯·åï¼", |
| | |
| | | |
| | | <template> |
| | | <view class="page"> |
| | | <view v-if="!canQuery" class="state">ææ è¯·åç³è¯·æ¥çæé</view> |
| | | <view v-if="error" class="state error" @click="load(true)">{{ error }}</view> |
| | | <view v-for="row in rows" :key="row.id" class="card"> |
| | | <view class="head"><view><text class="name">{{ row.userName || "--" }}ç请åç³è¯·</text><text class="sub">ç³è¯·ç¼å· {{ row.id }} · {{ row.deptName || "æªåé
é¨é¨" }}</text></view><text :class="['tag', auditClass(row.status)]">{{ label(dicts, HRM_DICT.auditStatus, row.status) }}</text></view> |
| | | <view class="grid"><text>ç±»åï¼{{ label(dicts, HRM_DICT.leaveType, row.leaveType) }}</text><text>æ¶é¿ï¼{{ row.duration ?? "--" }} 天</text><text>å¼å§ï¼{{ displayDate(row.startTime) }}</text><text>ç»æï¼{{ displayDate(row.endTime) }}</text></view> |
| | | <view class="actions"><button @click="go(row.id, 'detail')">详æ
</button><button v-if="row.status===0&&hasPermission('hrm:leave:update')" @click="go(row.id)">ç¼è¾</button><button v-if="row.status===0&&hasPermission('hrm:leave:submit')" @click="submit(row)">æäº¤</button><button v-if="row.status===0&&hasPermission('hrm:leave:delete')" class="danger" @click="remove(row)">å é¤</button></view> |
| | | <view class="actions"><button @click="go(row.id, 'detail')">详æ
</button><button v-if="canOperate(row)&&row.status===0&&hasPermission('hrm:leave:update')" @click="go(row.id)">ç¼è¾</button><button v-if="canOperate(row)&&row.status===0&&hasPermission('hrm:leave:submit')" @click="submit(row)">æäº¤</button><button v-if="canOperate(row)&&(row.status===0||row.status===40)&&hasPermission('hrm:leave:delete')" class="danger" @click="remove(row)">å é¤</button></view> |
| | | </view> |
| | | <view v-if="canQuery&&!rows.length&&!loading" class="state">ææ æ¬äººæäº¤ç请åç³è¯·</view> |
| | | <view v-if="!rows.length&&!loading&&!error" class="state">ææ æ¬äººæäº¤ç请åç³è¯·</view> |
| | | <view v-else-if="rows.length" class="more">{{ hasMore ? (loading ? "å è½½ä¸..." : "䏿å è½½æ´å¤") : "æ²¡ææ´å¤äº" }}</view> |
| | | <button v-if="canCreate" class="fab" @click="go()"><view /></button> |
| | | </view> |
| | |
| | | .page{min-height:100vh;padding: 20rpx 30rpx 132rpx;box-sizing:border-box;background:#f3f6fb}.search{display:flex;align-items:center;height:80rpx;padding:0 20rpx;margin-bottom:18rpx;background:#fff;border-radius:12rpx}.search input{min-width:0;flex:1;font-size:27rpx}.search text{padding-left:18rpx;color:var(--color-primary, #1769d8);font-size:24rpx}.card{padding:24rpx 22rpx 20rpx;margin-bottom:18rpx;background:#fff;border-radius:14rpx;box-shadow:0 8rpx 20rpx rgba(43,69,107,.07)}.head{display:flex;align-items:flex-start;justify-content:space-between;gap:16rpx}.head>view{display:flex;min-width:0;flex:1;flex-direction:column;gap:8rpx}.name{overflow:hidden;color:#20334c;font-size:30rpx;font-weight:700;text-overflow:ellipsis;white-space:nowrap}.sub{color:#78879b;font-size:23rpx}.tag{flex:none;padding:6rpx 15rpx;font-size:22rpx;border-radius:999rpx}.tag-on{color:#0e9663;background:#e9fbf2}.tag-off{color:#66778c;background:#f0f3f7}.tag-blue{color:var(--color-primary, #1769d8);background:var(--color-primary-soft, #eef6ff)}.tag-danger{color:#c65050;background:#fff0f0}.grid{display:grid;grid-template-columns:1fr 1fr;gap:11rpx;padding:16rpx 0;margin-top:15rpx;color:#66778c;font-size:23rpx;border-top:1rpx solid #edf1f6;border-bottom:1rpx solid #edf1f6}.grid text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:12rpx;padding-top:16rpx}.actions button{min-height:50rpx;padding:0 16rpx;margin:0;color:var(--color-primary, #1769d8);font-size:22rpx;line-height:48rpx;background:var(--color-primary-soft, #f5f8ff);border:1rpx solid var(--color-primary-border, #8ab3ee);border-radius:8rpx}.actions .danger{color:#c65050;background:#fff7f7;border-color:#e3a0a0}.actions button::after,.fab::after{border:0}.state,.more{display:block;padding:45rpx 0;color:#8795a8;font-size:26rpx;text-align:center}.error{color:#c65050}.fab{position:fixed;right:32rpx;bottom:56rpx;display:flex;width:100rpx;height:100rpx;align-items:center;justify-content:center;padding:0;margin:0;background:var(--color-primary, #1769d8);border-radius:50%;box-shadow:0 14rpx 28rpx rgba(23,105,216,.32)}.fab view{position:relative;width:38rpx;height:38rpx}.fab view::before,.fab view::after{position:absolute;content:"";background:#fff}.fab view::before{top:17rpx;width:38rpx;height:4rpx}.fab view::after{left:17rpx;width:4rpx;height:38rpx} |
| | | .page{min-height:100vh;padding: 20rpx 30rpx 132rpx;box-sizing:border-box;background:#f3f6fb}.search{display:flex;align-items:center;height:80rpx;padding:0 20rpx;margin-bottom:18rpx;background:#fff;border-radius:12rpx}.search input{min-width:0;flex:1;font-size:27rpx}.search text{padding-left:18rpx;color:var(--color-primary, #1769d8);font-size:24rpx}.card{padding:24rpx 22rpx 20rpx;margin-bottom:18rpx;background:#fff;border-radius:14rpx;box-shadow:0 8rpx 20rpx rgba(43,69,107,.07)}.head{display:flex;align-items:flex-start;justify-content:space-between;gap:16rpx}.head>view{display:flex;min-width:0;flex:1;flex-direction:column;gap:8rpx}.name{overflow:hidden;color:#20334c;font-size:30rpx;font-weight:700;text-overflow:ellipsis;white-space:nowrap}.sub{color:#78879b;font-size:23rpx}.tag{flex:none;padding:6rpx 15rpx;font-size:22rpx;border-radius:999rpx}.tag-on{color:#0e9663;background:#e9fbf2}.tag-off{color:#66778c;background:#f0f3f7}.tag-blue{color:var(--color-primary, #1769d8);background:var(--color-primary-soft, #eef6ff)}.tag-danger{color:#c65050;background:#fff0f0}.grid{display:grid;grid-template-columns:1fr 1fr;gap:11rpx;padding:16rpx 0;margin-top:15rpx;color:#66778c;font-size:23rpx;border-top:1rpx solid #edf1f6;border-bottom:1rpx solid #edf1f6}.grid text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:12rpx;padding-top:16rpx}.actions button{min-height:50rpx;padding:0 16rpx;margin:0;color:var(--color-primary, #1769d8);font-size:22rpx;line-height:48rpx;background:var(--color-primary-soft, #f5f8ff);border:1rpx solid var(--color-primary-border, #8ab3ee);border-radius:8rpx}.actions .danger{color:#c65050;background:#fff7f7;border-color:#e3a0a0}.actions button::after,.fab::after{border:0}.state,.more{display:block;padding:45rpx 0;color:#8795a8;font-size:26rpx;text-align:center}.error{color:#c65050}.fab{position:fixed;right:32rpx;bottom:56rpx;display:flex;width:100rpx;height:100rpx;align-items:center;justify-content:center;padding:0;margin:0;background:var(--color-primary, #1769d8);border-radius:50%;box-shadow:0 14rpx 28rpx rgba(23,105,216,.32)}.fab view{position:relative;width:38rpx;height:38rpx}.fab view::before,.fab view::after{position:absolute;content:"";background:#fff}.fab view::before{top:17rpx;left:0;width:38rpx;height:4rpx}.fab view::after{top:0;left:17rpx;width:4rpx;height:38rpx} |
| | |
| | | import { useAuth } from "../../../stores/auth"; |
| | | import { HRM_DICT, label, options } from "../shared"; |
| | | import HrmAttachmentField from "../HrmAttachmentField.vue"; |
| | | import PickerSheet from "../../mes/process-design/components/PickerSheet.vue"; |
| | | |
| | | const { hasPermission, state } = useAuth(); |
| | | const id = ref<number>(); const mode = ref("edit"); const saving = ref(false); |
| | | const handoverVisible = ref(false); |
| | | const dicts = ref<DictData[]>([]); const employees = ref<EmployeeSimple[]>([]); const attachments = ref<HrmAttachment[]>([]); |
| | | const form = reactive<Resignation>({ userId: undefined, resignationType: undefined, expectedDate: "", reason: "", handoverUserId: undefined, handoverRemark: "", blobIds: [], remark: "" }); |
| | | const readonly = computed(() => mode.value === "detail"); |
| | | const canSave = computed(() => !readonly.value && (id.value ? hasPermission("hrm:resignation:update") : hasPermission("hrm:resignation:create"))); |
| | | const typeOptions = computed(() => options(dicts.value, HRM_DICT.resignationType)); |
| | | const handoverText = computed(() => employees.value.find(item => item.id === form.handoverUserId)?.name || form.handoverUserName || "è¯·éæ©äº¤æ¥äºº"); |
| | | function selectHandover(index: number) { const item = employees.value[index]; form.handoverUserId = item?.id; form.handoverUserName = item?.name; } |
| | | function selectHandover(keys: Array<number | string>) { const item = employees.value.find(row => row.id === Number(keys[0])); form.handoverUserId = item?.id; form.handoverUserName = item?.name; } |
| | | async function save() { |
| | | if (form.resignationType === undefined || !form.expectedDate || !form.reason?.trim()) return uni.showToast({ title: "è¯·å®æ´å¡«åå¿
填项", icon: "none" }); |
| | | if (!state.accessToken || saving.value) return; saving.value = true; |
| | |
| | | <view class="field"><text>ç³è¯·äºº</text><text class="value">{{ form.userName || state.user?.nickname || "å½åç»å½ç¨æ·" }}</text></view> |
| | | <view class="field"><text>离èç±»å <text class="required">*</text></text><picker v-if="!readonly" :range="typeOptions" range-key="label" @change="form.resignationType=Number(typeOptions[$event.detail.value].value)"><view class="picker"><text>{{ label(dicts,HRM_DICT.resignationType,form.resignationType,"è¯·éæ©ç¦»èç±»å") }}</text><text>âº</text></view></picker><text v-else class="value">{{ form.resignationTypeName || label(dicts,HRM_DICT.resignationType,form.resignationType) }}</text></view> |
| | | <view class="field"><text>é¢è®¡ç¦»èæ¥æ <text class="required">*</text></text><picker v-if="!readonly" mode="date" :value="form.expectedDate" @change="form.expectedDate=$event.detail.value"><view class="picker"><text>{{ form.expectedDate || "è¯·éæ©æ¥æ" }}</text><text>âº</text></view></picker><text v-else class="value">{{ form.expectedDate || "--" }}</text></view> |
| | | <view class="field"><text>交æ¥äºº</text><picker v-if="!readonly" :range="employees" range-key="name" @change="selectHandover(Number($event.detail.value))"><view class="picker"><text>{{ handoverText }}</text><text>âº</text></view></picker><text v-else class="value">{{ handoverText }}</text></view> |
| | | <view class="field"><text>交æ¥äºº</text><view v-if="!readonly" class="picker" @click="handoverVisible=true"><text>{{ handoverText }}</text><text>âº</text></view><text v-else class="value">{{ handoverText }}</text></view> |
| | | <view class="field column"><text>离èåå <text class="required">*</text></text><textarea v-model="form.reason" :disabled="readonly" maxlength="500" placeholder="请è¾å
¥ç¦»èåå " /></view> |
| | | <view class="field column"><text>交æ¥è¯´æ</text><textarea v-model="form.handoverRemark" :disabled="readonly" maxlength="500" placeholder="请è¾å
¥äº¤æ¥è¯´æ" /></view> |
| | | <HrmAttachmentField v-model="form.blobIds" v-model:files="attachments" :token="state.accessToken" :readonly="readonly" /> |
| | | <view class="field column"><text>夿³¨</text><textarea v-model="form.remark" :disabled="readonly" placeholder="请è¾å
¥å¤æ³¨" /></view> |
| | | </view><button v-if="canSave" class="save" :loading="saving" @click="save">ä¿åè稿</button><button v-else-if="readonly&&form.status===0&&hasPermission('hrm:resignation:update')" class="save" @click="mode='edit'">ç¼è¾</button></view></template> |
| | | </view><button v-if="canSave" class="save" :loading="saving" @click="save">ä¿åè稿</button><button v-else-if="readonly&&form.status===0&&hasPermission('hrm:resignation:update')" class="save" @click="mode='edit'">ç¼è¾</button><PickerSheet v-model:show="handoverVisible" title="éæ©äº¤æ¥äºº" :items="employees.map(item=>({key:item.id||0,label:item.name||'--',sub:item.employeeNo}))" :selected-keys="form.handoverUserId?[form.handoverUserId]:[]" search-placeholder="æç´¢å§åæå·¥å·" @confirm="selectHandover" /></view></template> |
| | | <style lang="scss" scoped>@import "../form.scss";</style> |
| | |
| | | const picked = ref<Array<number | string>>([]); |
| | | |
| | | const pickedSet = computed(() => new Set(picked.value.map(String))); |
| | | const displayedItems = computed(() => { |
| | | const value = keyword.value.trim().toLocaleLowerCase(); |
| | | if (!value) return props.items; |
| | | return props.items.filter(item => [item.label, item.sub, item.extra].some(text => String(text || "").toLocaleLowerCase().includes(value))); |
| | | }); |
| | | |
| | | function isPicked(key: number | string) { |
| | | return pickedSet.value.has(String(key)); |
| | |
| | | </view> |
| | | <scroll-view class="sheet-body" scroll-y lower-threshold="60" @scrolltolower="onScrollToLower"> |
| | | <view v-if="loading && !items.length" class="sheet-state">å è½½ä¸...</view> |
| | | <template v-else-if="items.length"> |
| | | <view v-for="item in items" :key="item.key" class="sheet-row" @click="toggle(item)"> |
| | | <template v-else-if="displayedItems.length"> |
| | | <view v-for="item in displayedItems" :key="item.key" class="sheet-row" @click="toggle(item)"> |
| | | <view class="sheet-row-main"> |
| | | <text class="sheet-row-label">{{ item.label }}</text> |
| | | <text v-if="item.sub" class="sheet-row-sub">{{ item.sub }}</text> |
| | |
| | | } from "../../../../api/quality"; |
| | | import { useAuth } from "../../../../stores/auth"; |
| | | import { dateText, dictLabel, dictOptions } from "../../dv/shared"; |
| | | import PickerSheet from "../../process-design/components/PickerSheet.vue"; |
| | | import { usePagedList } from "../paged"; |
| | | |
| | | const { hasPermission, state } = useAuth(); |
| | |
| | | const activeTab = ref<"line" | "result" | "defect">("line"); |
| | | const dicts = ref<DictData[]>([]); |
| | | const users = ref<UserSimple[]>([]); |
| | | const userSheet = ref(false); |
| | | const workOrders = ref<WorkOrder[]>([]); |
| | | const workstations = ref<MesWorkstation[]>([]); |
| | | const tasks = ref<ProTask[]>([]); |
| | |
| | | <view class="field hint-field"><text>ä¸åæ ¼åæ°é <text class="required">*</text></text><input :value="String(form.unqualifiedQuantity ?? '')" :disabled="readonly" type="digit" placeholder="请è¾å
¥ä¸åæ ¼åæ°é" @input="form.unqualifiedQuantity=Number($event.detail.value||0)" /></view> |
| | | <view class="field-hint">ä¸åæ ¼åæ°é大äº0æ¶ï¼ä¿ååå°èªå¨çæä¸åæ ¼å¤çå</view> |
| | | <template v-if="Number(form.unqualifiedQuantity || 0) > 0"><view class="field"><text>å·¥åºæ°é <text class="required">*</text></text><input :value="String(form.laborScrapQuantity ?? '')" :disabled="readonly" type="digit" placeholder="请è¾å
¥å·¥åºæ°é" @input="form.laborScrapQuantity=Number($event.detail.value||0);syncUnqualified()" /></view><view class="field"><text>æåºæ°é <text class="required">*</text></text><input :value="String(form.materialScrapQuantity ?? '')" :disabled="readonly" type="digit" placeholder="请è¾å
¥æåºæ°é" @input="form.materialScrapQuantity=Number($event.detail.value||0);syncUnqualified()" /></view><view class="field"><text>å
¶ä»åºåæ°é <text class="required">*</text></text><input :value="String(form.otherScrapQuantity ?? '')" :disabled="readonly" type="digit" placeholder="请è¾å
¥å
¶ä»åºåæ°é" @input="form.otherScrapQuantity=Number($event.detail.value||0);syncUnqualified()" /></view></template> |
| | | <view class="field"><text>æ£æµäººå <text class="required">*</text></text><picker v-if="!readonly" :range="users" range-key="nickname" @change="form.inspectorUserId=users[$event.detail.value].id;form.inspectorNickname=users[$event.detail.value].nickname"><view class="picker"><text>{{ userName() }}</text><text>âº</text></view></picker><text v-else class="value">{{ userName() }}</text></view> |
| | | <view class="field"><text>æ£æµäººå <text class="required">*</text></text><view v-if="!readonly" class="picker" @click="userSheet=true"><text>{{ userName() }}</text><text>âº</text></view><text v-else class="value">{{ userName() }}</text></view> |
| | | <view class="field"><text>æ£æµæ¥æ <text class="required">*</text></text><picker v-if="!readonly" mode="date" :value="String(form.inspectDate || '')" @change="form.inspectDate=$event.detail.value"><view class="picker"><text>{{ dateText(form.inspectDate).replace('--','è¯·éæ©æ£æµæ¥æ') }}</text><text>âº</text></view></picker><text v-else class="value">{{ dateText(form.inspectDate) }}</text></view> |
| | | <view class="field"><text>æ£æµç»æ</text><picker v-if="!readonly" :range="checkResults" range-key="label" @change="form.checkResult=Number(checkResults[$event.detail.value].value)"><view class="picker"><text>{{ dictLabel(dicts,'mes_qc_check_result',form.checkResult,'è¯·éæ©æ£æµç»æ') }}</text><text>âº</text></view></picker><text v-else class="value">{{ dictLabel(dicts,'mes_qc_check_result',form.checkResult) }}</text></view> |
| | | <view v-if="id" class="field"><text>缺é·ç»è®¡</text><text class="value">è´å½ {{ form.criticalQuantity ?? 0 }} / 严é {{ form.majorQuantity ?? 0 }} / 轻微 {{ form.minorQuantity ?? 0 }}</text></view> |
| | |
| | | <button class="save in-modal" @click="saveDefect">ä¿å</button> |
| | | </template> |
| | | </view></view> |
| | | <PickerSheet v-model:show="userSheet" title="éæ©æ£æµäººå" :items="users.map(item=>({key:item.id,label:item.nickname||item.username||String(item.id)}))" :selected-keys="form.inspectorUserId?[form.inspectorUserId]:[]" search-placeholder="æç´¢äººå" @confirm="keys=>{const item=users.find(row=>row.id===Number(keys[0]));form.inspectorUserId=item?.id;form.inspectorNickname=item?.nickname}" /> |
| | | </view> |
| | | </template> |
| | | |
| | |
| | | const templates = ref<Array<{ templateId?: number; templateName?: string; templateCode?: string }>>([]); |
| | | const form = reactive<Iqc>({ qualifiedQuantity: 0, unqualifiedQuantity: 0 }); |
| | | const materialSheet = ref(false); |
| | | const userSheet = ref(false); |
| | | |
| | | /** 产åç©æåéï¼å
³é®åè¿ç¨æ£ç´¢ + 䏿å è½½æ´å¤ï¼æ¿ä»£åå
䏿¬¡æ 200 æ¡ç pickerï¼ */ |
| | | const materialPicker = usePagedList<MesMdItem>( |
| | |
| | | <view class="field hint-field"><text>ä¸åæ ¼åæ°é <text class="required">*</text></text><input :value="String(form.unqualifiedQuantity ?? '')" :disabled="readonly" type="digit" placeholder="请è¾å
¥ä¸åæ ¼åæ°é" @input="form.unqualifiedQuantity=Number($event.detail.value||0)" /></view> |
| | | <view class="field-hint">ä¸åæ ¼åæ°é大äº0æ¶ï¼ä¿ååå°èªå¨çæä¸åæ ¼å¤çå</view> |
| | | <view class="field"><text>æ¥ææ¥æ <text class="required">*</text></text><picker v-if="!readonly" mode="date" :value="String(form.receiveDate || '')" @change="form.receiveDate=$event.detail.value"><view class="picker"><text>{{ dateText(form.receiveDate).replace('--','è¯·éæ©æ¥ææ¥æ') }}</text><text>âº</text></view></picker><text v-else class="value">{{ dateText(form.receiveDate) }}</text></view> |
| | | <view class="field"><text>æ£æµäººå <text class="required">*</text></text><picker v-if="!readonly" :range="users" range-key="nickname" @change="form.inspectorUserId=users[$event.detail.value].id;form.inspectorNickname=users[$event.detail.value].nickname"><view class="picker"><text>{{ userName() }}</text><text>âº</text></view></picker><text v-else class="value">{{ userName() }}</text></view> |
| | | <view class="field"><text>æ£æµäººå <text class="required">*</text></text><view v-if="!readonly" class="picker" @click="userSheet=true"><text>{{ userName() }}</text><text>âº</text></view><text v-else class="value">{{ userName() }}</text></view> |
| | | <view class="field"><text>æ£æµæ¥æ <text class="required">*</text></text><picker v-if="!readonly" mode="date" :value="String(form.inspectDate || '')" @change="form.inspectDate=$event.detail.value"><view class="picker"><text>{{ dateText(form.inspectDate).replace('--','è¯·éæ©æ£æµæ¥æ') }}</text><text>âº</text></view></picker><text v-else class="value">{{ dateText(form.inspectDate) }}</text></view> |
| | | <view class="field"><text>æ£æµç»æ</text><picker v-if="!readonly" :range="checkResults" range-key="label" @change="form.checkResult=Number(checkResults[$event.detail.value].value)"><view class="picker"><text>{{ dictLabel(dicts,'mes_qc_check_result',form.checkResult,'è¯·éæ©æ£æµç»æ') }}</text><text>âº</text></view></picker><text v-else class="value">{{ dictLabel(dicts,'mes_qc_check_result',form.checkResult) }}</text></view> |
| | | <view v-if="id" class="field"><text>缺é·ç»è®¡</text><text class="value">è´å½ {{ form.criticalQuantity ?? 0 }} / 严é {{ form.majorQuantity ?? 0 }} / 轻微 {{ form.minorQuantity ?? 0 }}</text></view> |
| | |
| | | @load-more="materialPicker.loadMore" |
| | | @confirm="pickMaterial" |
| | | /> |
| | | <PickerSheet v-model:show="userSheet" title="éæ©æ£æµäººå" :items="users.map(item=>({key:item.id,label:item.nickname||item.username||String(item.id)}))" :selected-keys="form.inspectorUserId?[form.inspectorUserId]:[]" search-placeholder="æç´¢äººå" @confirm="keys=>{const item=users.find(row=>row.id===Number(keys[0]));form.inspectorUserId=item?.id;form.inspectorNickname=item?.nickname}" /> |
| | | </view> |
| | | </template> |
| | | |
| | |
| | | const templates = ref<Array<{ templateId?: number; templateName?: string; templateCode?: string }>>([]); |
| | | const form = reactive<Oqc>({ qualifiedQuantity: 0, unqualifiedQuantity: 0 }); |
| | | const materialSheet = ref(false); |
| | | const userSheet = ref(false); |
| | | |
| | | /** 产åç©æåéï¼å
³é®åè¿ç¨æ£ç´¢ + 䏿å è½½æ´å¤ï¼æ¿ä»£åå
䏿¬¡æ 200 æ¡ç pickerï¼ */ |
| | | const materialPicker = usePagedList<MesMdItem>( |
| | |
| | | <view class="field hint-field"><text>ä¸åæ ¼åæ°é <text class="required">*</text></text><input :value="String(form.unqualifiedQuantity ?? '')" :disabled="readonly" type="digit" placeholder="请è¾å
¥ä¸åæ ¼åæ°é" @input="form.unqualifiedQuantity=Number($event.detail.value||0)" /></view> |
| | | <view class="field-hint">ä¸åæ ¼åæ°é大äº0æ¶ï¼ä¿ååå°èªå¨çæä¸åæ ¼å¤çå</view> |
| | | <view class="field"><text>åºè´§æ¥æ <text class="required">*</text></text><picker v-if="!readonly" mode="date" :value="String(form.outDate || '')" @change="form.outDate=$event.detail.value"><view class="picker"><text>{{ dateText(form.outDate).replace('--','è¯·éæ©åºè´§æ¥æ') }}</text><text>âº</text></view></picker><text v-else class="value">{{ dateText(form.outDate) }}</text></view> |
| | | <view class="field"><text>æ£æµäººå <text class="required">*</text></text><picker v-if="!readonly" :range="users" range-key="nickname" @change="form.inspectorUserId=users[$event.detail.value].id;form.inspectorNickname=users[$event.detail.value].nickname"><view class="picker"><text>{{ userName() }}</text><text>âº</text></view></picker><text v-else class="value">{{ userName() }}</text></view> |
| | | <view class="field"><text>æ£æµäººå <text class="required">*</text></text><view v-if="!readonly" class="picker" @click="userSheet=true"><text>{{ userName() }}</text><text>âº</text></view><text v-else class="value">{{ userName() }}</text></view> |
| | | <view class="field"><text>æ£æµæ¥æ <text class="required">*</text></text><picker v-if="!readonly" mode="date" :value="String(form.inspectDate || '')" @change="form.inspectDate=$event.detail.value"><view class="picker"><text>{{ dateText(form.inspectDate).replace('--','è¯·éæ©æ£æµæ¥æ') }}</text><text>âº</text></view></picker><text v-else class="value">{{ dateText(form.inspectDate) }}</text></view> |
| | | <view class="field"><text>æ£æµç»æ</text><picker v-if="!readonly" :range="checkResults" range-key="label" @change="form.checkResult=Number(checkResults[$event.detail.value].value)"><view class="picker"><text>{{ dictLabel(dicts,'mes_qc_check_result',form.checkResult,'è¯·éæ©æ£æµç»æ') }}</text><text>âº</text></view></picker><text v-else class="value">{{ dictLabel(dicts,'mes_qc_check_result',form.checkResult) }}</text></view> |
| | | <view v-if="id" class="field"><text>缺é·ç»è®¡</text><text class="value">è´å½ {{ form.criticalQuantity ?? 0 }} / 严é {{ form.majorQuantity ?? 0 }} / 轻微 {{ form.minorQuantity ?? 0 }}</text></view> |
| | |
| | | @load-more="materialPicker.loadMore" |
| | | @confirm="pickMaterial" |
| | | /> |
| | | <PickerSheet v-model:show="userSheet" title="éæ©æ£æµäººå" :items="users.map(item=>({key:item.id,label:item.nickname||item.username||String(item.id)}))" :selected-keys="form.inspectorUserId?[form.inspectorUserId]:[]" search-placeholder="æç´¢äººå" @confirm="keys=>{const item=users.find(row=>row.id===Number(keys[0]));form.inspectorUserId=item?.id;form.inspectorNickname=item?.nickname}" /> |
| | | </view> |
| | | </template> |
| | | |
| | |
| | | const templates = ref<Array<{ templateId?: number; templateName?: string; templateCode?: string }>>([]); |
| | | const form = reactive<Rqc>({ qualifiedQuantity: 0, unqualifiedQuantity: 0 }); |
| | | const materialSheet = ref(false); |
| | | const userSheet = ref(false); |
| | | |
| | | /** 产åç©æåéï¼å
³é®åè¿ç¨æ£ç´¢ + 䏿å è½½æ´å¤ï¼æ¿ä»£åå
䏿¬¡æ 200 æ¡ç pickerï¼ */ |
| | | const materialPicker = usePagedList<MesMdItem>( |
| | |
| | | <view class="field hint-field"><text>ä¸åæ ¼åæ°é <text class="required">*</text></text><input :value="String(form.unqualifiedQuantity ?? '')" :disabled="readonly" type="digit" placeholder="请è¾å
¥ä¸åæ ¼åæ°é" @input="form.unqualifiedQuantity=Number($event.detail.value||0)" /></view> |
| | | <view class="field-hint">ä¸åæ ¼åæ°é大äº0æ¶ï¼ä¿ååå°èªå¨çæä¸åæ ¼å¤çå</view> |
| | | |
| | | <view class="field"><text>æ£æµäººå <text class="required">*</text></text><picker v-if="!readonly" :range="users" range-key="nickname" @change="form.inspectorUserId=users[$event.detail.value].id;form.inspectorNickname=users[$event.detail.value].nickname"><view class="picker"><text>{{ userName() }}</text><text>âº</text></view></picker><text v-else class="value">{{ userName() }}</text></view> |
| | | <view class="field"><text>æ£æµäººå <text class="required">*</text></text><view v-if="!readonly" class="picker" @click="userSheet=true"><text>{{ userName() }}</text><text>âº</text></view><text v-else class="value">{{ userName() }}</text></view> |
| | | <view class="field"><text>æ£æµæ¥æ <text class="required">*</text></text><picker v-if="!readonly" mode="date" :value="String(form.inspectDate || '')" @change="form.inspectDate=$event.detail.value"><view class="picker"><text>{{ dateText(form.inspectDate).replace('--','è¯·éæ©æ£æµæ¥æ') }}</text><text>âº</text></view></picker><text v-else class="value">{{ dateText(form.inspectDate) }}</text></view> |
| | | <view class="field"><text>æ£æµç»æ</text><picker v-if="!readonly" :range="checkResults" range-key="label" @change="form.checkResult=Number(checkResults[$event.detail.value].value)"><view class="picker"><text>{{ dictLabel(dicts,'mes_qc_check_result',form.checkResult,'è¯·éæ©æ£æµç»æ') }}</text><text>âº</text></view></picker><text v-else class="value">{{ dictLabel(dicts,'mes_qc_check_result',form.checkResult) }}</text></view> |
| | | <view v-if="id" class="field"><text>缺é·ç»è®¡</text><text class="value">è´å½ {{ form.criticalQuantity ?? 0 }} / 严é {{ form.majorQuantity ?? 0 }} / 轻微 {{ form.minorQuantity ?? 0 }}</text></view> |
| | |
| | | @load-more="materialPicker.loadMore" |
| | | @confirm="pickMaterial" |
| | | /> |
| | | <PickerSheet v-model:show="userSheet" title="éæ©æ£æµäººå" :items="users.map(item=>({key:item.id,label:item.nickname||item.username||String(item.id)}))" :selected-keys="form.inspectorUserId?[form.inspectorUserId]:[]" search-placeholder="æç´¢äººå" @confirm="keys=>{const item=users.find(row=>row.id===Number(keys[0]));form.inspectorUserId=item?.id;form.inspectorNickname=item?.nickname}" /> |
| | | </view> |
| | | </template> |
| | | |
| | |
| | | const loadError = ref(""); |
| | | const activeGroup = ref("å
¨é¨"); |
| | | const hiddenAppNames = [ |
| | | "请åç³è¯·", |
| | | "è°å²ç³è¯·", |
| | | "离èç³è¯·", |
| | | "å çç³è¯·", |
| | |
| | | "MES é¦é¡µ", |
| | | ]; |
| | | const hiddenComponentPatterns = [ |
| | | /^hrm\/leave\/index$/i, |
| | | /^hrm\/transfer\/index$/i, |
| | | /^hrm\/resignation\/index$/i, |
| | | /^hrm\/overtime\/index$/i, |
| | |
| | | |
| | | const apps = computed(() => { |
| | | const menuApps = flattenMenus(state.menus); |
| | | // âæçç³è¯·âæ¨¡åææ¶éèï¼æ¢å¤æ¶åæ¶ä¸ä¸è¡æ³¨éï¼å¹¶å¨è¿åå¼ä¸éæ°åå¹¶ selfServiceAppsã |
| | | // const selfServiceApps = getSelfServiceApps(menuApps); |
| | | // è¯·åæ¯ææåå·¥é½å¯ä½¿ç¨çèªå©ç³è¯·ï¼æèåæéæ¶ä½¿ç¨æ£å¼èåï¼æ²¡ææ¶è¡¥å
âæçç³è¯·âå
¥å£ã |
| | | const selfServiceApps = getSelfServiceApps(menuApps).filter( |
| | | item => item.component === "hrm/leave/index" |
| | | ); |
| | | const attendance = |
| | | !isModuleEnabled("èå¤è§å") || |
| | | menuApps.some(app => app.component === "hrm/attendance/clock/index") |
| | |
| | | mark: "mark-9", |
| | | }, |
| | | ]; |
| | | return menuApps.concat(attendance); |
| | | return menuApps.concat(selfServiceApps, attendance); |
| | | }); |
| | | const groups = computed(() => [ |
| | | "å
¨é¨", |
| | |
| | | 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; |
| | |
| | | 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[] = [ |
| | | { |
| | |
| | | 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: "屿¬¡é²æï¼éåæ°æ®åºæ¯", |
| | |
| | | }, |
| | | ]; |
| | | |
| | | 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) { |