| | |
| | | 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 { |
| | |
| | | }, |
| | | changhui: { |
| | | key: "changhui", |
| | | name: "长辉电力", |
| | | name: "新疆长辉电力", |
| | | homeVariant: "changhui-power", |
| | | apiBaseUrl: "http://101.245.92.159:9006/admin-api", |
| | | webBaseUrl: "http://101.245.92.159:9006", |
| | |
| | | { |
| | | "name" : "长辉电力", |
| | | "name" : "新疆长辉电力", |
| | | "appid" : "__UNI__59EDE0D", |
| | | "description" : "长辉电力", |
| | | "description" : "新疆长辉电力", |
| | | "versionName" : "1.0.0", |
| | | "versionCode" : 100, |
| | | "transformPx" : false, |
| | |
| | | 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))}`, |
| | | }); |
| | |
| | | import { useCompanyDashboard } from "./use-company-dashboard"; |
| | | |
| | | const { |
| | | equipmentRate, formatDate, goLogin, goMessages, greeting, isLoggedIn, loadDashboard, |
| | | attendanceOverview, equipmentRate, formatDate, goLogin, goMessages, greeting, isLoggedIn, loadDashboard, |
| | | loadError, loading, notifyState, openRoute, outputChange, qualityRate, statusPercent, |
| | | summary, today, totalWorkOrders, trend, trendMax, userName, visibleActions, workOrderStatus, |
| | | summary, supplyOverview, today, totalWorkOrders, trend, trendMax, userName, visibleActions, workOrderStatus, |
| | | } = useCompanyDashboard(); |
| | | </script> |
| | | |
| | |
| | | </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"> |
| | |
| | | .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> |
| | |
| | | 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"; |
| | |
| | | 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 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(""); |
| | | |
| | |
| | | loading.value = true; |
| | | loadError.value = ""; |
| | | try { |
| | | const [summaryData, trendData, statusData] = await Promise.all([ |
| | | 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 { |
| | |
| | | onPullDownRefresh(async () => { await refreshPage(); uni.stopPullDownRefresh(); }); |
| | | |
| | | return { |
| | | equipmentRate, formatDate, goLogin, goMessages, greeting, isLoggedIn, loadDashboard, |
| | | attendanceOverview, equipmentRate, formatDate, goLogin, goMessages, greeting, isLoggedIn, loadDashboard, |
| | | loadError, loading, notifyState, openRoute, outputChange, qualityRate, statusPercent, |
| | | summary, today, totalWorkOrders, trend, trendMax, userName, visibleActions, workOrderStatus, |
| | | summary, supplyOverview, today, totalWorkOrders, trend, trendMax, userName, visibleActions, workOrderStatus, |
| | | }; |
| | | } |
| | |
| | | "当前用户" |
| | | ); |
| | | 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); |
| | | // 蓝羽纸业暂时隐藏考勤打卡;恢复时移除下面的过滤,并恢复 attendance 合并逻辑。 |
| | | const visibleMenuApps = APP_COMPANY.key === "lanyu" |
| | | ? menuApps.filter(app => app.component !== "hrm/attendance/clock/index") |
| | | : menuApps; |
| | | // “我的申请”模块暂时隐藏;恢复时取消下一行注释,并在返回值中重新合并 selfServiceApps。 |
| | | // const selfServiceApps = getSelfServiceApps(menuApps); |
| | | // const attendance = |
| | | // !isModuleEnabled("考勤规则") || |
| | | // visibleMenuApps.some(app => app.component === "hrm/attendance/clock/index") |
| | | // ? [] |
| | | // : [{ |
| | | // id: -6009, |
| | | // name: "考勤打卡", |
| | | // group: "人力资源", |
| | | // component: "hrm/attendance/clock/index", |
| | | // componentName: "HrmAttendanceClock", |
| | | // developed: true, |
| | | // tone: "cyan", |
| | | // icon: "system", |
| | | // shape: "shape-5", |
| | | // mark: "mark-9", |
| | | // }]; |
| | | // 恢复入口时将返回值改为 visibleMenuApps.concat(attendance)。 |
| | | return visibleMenuApps; |
| | | // 请假是所有员工都可使用的自助申请;有菜单权限时使用正式菜单,没有时补充“我的申请”入口。 |
| | | const selfServiceApps = getSelfServiceApps(menuApps).filter( |
| | | item => item.component === "hrm/leave/index" |
| | | ); |
| | | const attendance = |
| | | !isModuleEnabled("考勤规则") || |
| | | menuApps.some(app => app.component === "hrm/attendance/clock/index") |
| | | ? [] |
| | | : [ |
| | | { |
| | | id: -6009, |
| | | name: "考勤打卡", |
| | | group: "人力资源", |
| | | component: "hrm/attendance/clock/index", |
| | | componentName: "HrmAttendanceClock", |
| | | developed: true, |
| | | tone: "cyan", |
| | | icon: "system", |
| | | shape: "shape-5", |
| | | mark: "mark-9", |
| | | }, |
| | | ]; |
| | | return menuApps.concat(selfServiceApps, attendance); |
| | | }); |
| | | const groups = computed(() => [ |
| | | "全部", |