feat(router): 排除AI模块页面并添加访问权限控制
- 在构建时排除 ../views/ai/**/*.vue 文件以避免生成不可访问的AI分包
- 为仪表板预警面板组件添加访问权限检查
- 为业务卡片组件添加采购、销售、库存、BPM待办权限控制
- 更新考勤打卡功能以使用GPS定位代替手动输入地点
- 在工艺路线配置中添加仓库、库区、库位选择功能
- 为质量控制表单添加必填验证规则
- 移除AI相关功能组件以适配轻量版后端
- 添加用户昵称字段到用户管理模块
- 更新采购申请表单以包含定金和税额字段
- 移除物料管理中的替代品标签页
- 添加考勤规则中的定位打卡配置选项
| | |
| | | supplierId?: number; // 供应商编号 |
| | | supplierName?: string; // 供应商名称 |
| | | discountPercent?: number; // 优惠率 |
| | | depositPrice: number; // 定金金额 |
| | | attachmentList?: { id: number; name?: string; url?: string }[]; // 附件列表 |
| | | remark?: string; // 备注 |
| | | totalCount?: number; // 总数量 |
| | | totalPrice?: number; // 总金额 |
| | | totalTaxPrice?: number; // 税额合计 |
| | | productNames?: string; // 产品名称列表 |
| | | orderId?: number; // 生成的采购订单编号 |
| | | orderNo?: string; // 生成的采购订单号 |
| | |
| | | return requestClient.download('/hrm/attendance/record/export-excel', { params }); |
| | | } |
| | | |
| | | export interface AttendanceClockLocation { |
| | | longitude: number; |
| | | latitude: number; |
| | | accuracy: number; |
| | | coordinateSystem: 'WGS84'; |
| | | } |
| | | |
| | | /** 上班打卡 */ |
| | | export function clockIn(params: { location: string }) { |
| | | export function clockIn(data: AttendanceClockLocation) { |
| | | return requestClient.post<HrmAttendanceRecordApi.AttendanceRecord>( |
| | | '/hrm/attendance/record/clock-in', |
| | | null, |
| | | { params }, |
| | | data, |
| | | ); |
| | | } |
| | | |
| | | /** 下班打卡 */ |
| | | export function clockOut(params: { location: string }) { |
| | | export function clockOut(data: AttendanceClockLocation) { |
| | | return requestClient.post<HrmAttendanceRecordApi.AttendanceRecord>( |
| | | '/hrm/attendance/record/clock-out', |
| | | null, |
| | | { params }, |
| | | data, |
| | | ); |
| | | } |
| | | } |
| | |
| | | lateAllowMinutes?: number; |
| | | earlyAllowMinutes?: number; |
| | | overtimeThreshold?: number; |
| | | locationName?: string; |
| | | locationAddress?: string; |
| | | longitude?: number; |
| | | latitude?: number; |
| | | radiusMeters?: number; |
| | | locationRequired?: boolean; |
| | | coordinateSystem?: 'WGS84'; |
| | | status?: number; |
| | | remark?: string; |
| | | createTime?: string; |
| | |
| | | file, |
| | | updateSupport, |
| | | }); |
| | | } |
| | | } |
| | |
| | | itemId?: number; |
| | | itemCode?: string; |
| | | itemName?: string; |
| | | warehouseId?: number; |
| | | warehouseName?: string; |
| | | locationId?: number; |
| | | locationName?: string; |
| | | areaId?: number; |
| | | areaName?: string; |
| | | specification?: string; |
| | | unitName?: string; |
| | | quantity?: number; |
| | |
| | | } |
| | | |
| | | async function generateAccess(options: GenerateMenuAndRoutesOptions) { |
| | | const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue'); |
| | | // 轻量版后端不包含 AI 模块,构建阶段即排除 AI 页面,避免生成不可访问的 AI 分包。 |
| | | const pageMap: ComponentRecordType = import.meta.glob([ |
| | | '../views/**/*.vue', |
| | | '!../views/ai/**/*.vue', |
| | | ]); |
| | | const accessStore = useAccessStore(); |
| | | |
| | | const layoutMap: ComponentRecordType = { |
| | |
| | | getSaleQuotationPage, |
| | | } from '#/api/crm/saleQuotation'; |
| | | import { $t } from '#/locales'; |
| | | import { isAiFeatureEnabled } from '#/utils/features'; |
| | | |
| | | import type { CrmSaleQuotationAiApi } from '#/api/crm/saleQuotation/ai'; |
| | | |
| | | import { nextTick, ref } from 'vue'; |
| | | |
| | | import { QUOTATION_STATUS, useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | import OcrUpload from './modules/ocr-upload.vue'; |
| | | |
| | | const { push } = useRouter(); |
| | | |
| | |
| | | connectedComponent: Form, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const ocrUploadRef = ref<InstanceType<typeof OcrUpload>>(); |
| | | |
| | | /** 刷新表格 */ |
| | | function handleRefresh() { |
| | |
| | | formModalApi.setData(null).open(); |
| | | } |
| | | |
| | | /** AI 识别录入 */ |
| | | function handleOcrCreate() { |
| | | ocrUploadRef.value?.open(); |
| | | } |
| | | |
| | | /** OCR 识别结果确认 */ |
| | | function handleOcrSuccess(data: CrmSaleQuotationAiApi.OcrResultVO) { |
| | | nextTick(() => { |
| | | formModalApi.setData({ ocrData: data } as any).open(); |
| | | }); |
| | | } |
| | | |
| | | const toolbarActions: ActionItem[] = [ |
| | | { |
| | | label: $t('ui.actionTitle.create', ['报价单']), |
| | |
| | | onClick: handleCreate, |
| | | }, |
| | | ]; |
| | | if (isAiFeatureEnabled) { |
| | | toolbarActions.push({ |
| | | label: 'AI 识别录入', |
| | | type: 'default', |
| | | auth: ['crm:sale-quotation:create'], |
| | | onClick: handleOcrCreate, |
| | | }); |
| | | } |
| | | |
| | | /** 编辑报价单 */ |
| | | function handleEdit(row: CrmSaleQuotationApi.SaleQuotation) { |
| | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <FormModal @success="handleRefresh" /> |
| | | <OcrUpload |
| | | v-if="isAiFeatureEnabled" |
| | | ref="ocrUploadRef" |
| | | @success="handleOcrSuccess" |
| | | /> |
| | | <Grid> |
| | | <template #toolbar-tools> |
| | | <TableAction :actions="toolbarActions" /> |
| | |
| | | import { useRouter } from 'vue-router'; |
| | | |
| | | import { Page } from '@vben/common-ui'; |
| | | import { useAccess } from '@vben/access'; |
| | | |
| | | import { Card, Col, Row } from 'ant-design-vue'; |
| | | |
| | |
| | | defineOptions({ name: 'DashboardAnalytics' }); |
| | | |
| | | const router = useRouter(); |
| | | const { hasAccessByCodes } = useAccess(); |
| | | const summary = ref(defaultSummary); |
| | | |
| | | const canViewProduction = computed(() => |
| | | hasAccessByCodes(['mes:pro-work-order:query', 'mes:pro-feedback:query']), |
| | | ); |
| | | const canViewAlerts = computed(() => |
| | | hasAccessByCodes([ |
| | | 'mes:pro-work-order:query', |
| | | 'mes:pro-andon-record:query', |
| | | 'mes:dv-repair:query', |
| | | ]), |
| | | ); |
| | | const canViewMesSummary = computed(() => |
| | | hasAccessByCodes([ |
| | | 'mes:pro-work-order:query', |
| | | 'mes:pro-feedback:query', |
| | | 'mes:qc-ipqc:query', |
| | | 'mes:dv-machinery:query', |
| | | 'mes:pro-andon-record:query', |
| | | 'mes:dv-repair:query', |
| | | ]), |
| | | ); |
| | | |
| | | const greeting = computed(() => { |
| | | const hour = new Date().getHours(); |
| | |
| | | } |
| | | |
| | | async function loadSummary() { |
| | | if (!canViewMesSummary.value) return; |
| | | summary.value = await getHomeSummary(); |
| | | } |
| | | |
| | |
| | | </div> |
| | | |
| | | <!-- MES 生产 KPI --> |
| | | <KpiCards :summary="summary" @navigate="handleNavigate" /> |
| | | <KpiCards v-if="canViewMesSummary" :summary="summary" @navigate="handleNavigate" /> |
| | | |
| | | <!-- ERP/WMS/BPM 经营 KPI --> |
| | | <BizCards @navigate="handleNavigate" /> |
| | | |
| | | <!-- 生产趋势 + 预警面板 --> |
| | | <Row :gutter="16" class="mb-4"> |
| | | <Col :lg="14" :md="24" :sm="24" :xl="14" :xs="24" class="mb-4"> |
| | | <Row v-if="canViewProduction || canViewAlerts" :gutter="16" class="mb-4"> |
| | | <Col v-if="canViewProduction" :lg="canViewAlerts ? 14 : 24" :md="24" :sm="24" :xl="canViewAlerts ? 14 : 24" :xs="24" class="mb-4"> |
| | | <div class="glass-card rounded-2xl p-5"> |
| | | <h3 class="mb-4 text-base font-semibold text-gray-800 dark:text-gray-100"> |
| | | 生产趋势(近7天) |
| | |
| | | <ProductionTrend /> |
| | | </div> |
| | | </Col> |
| | | <Col :lg="10" :md="24" :sm="24" :xl="10" :xs="24" class="mb-4"> |
| | | <Col v-if="canViewAlerts" :lg="canViewProduction ? 10 : 24" :md="24" :sm="24" :xl="canViewProduction ? 10 : 24" :xs="24" class="mb-4"> |
| | | <AlertPanel |
| | | :andon-active-count="summary.andonActiveCount" |
| | | :repair-active-count="summary.repairActiveCount" |
| | |
| | | import type { MesHomeApi } from '#/api/mes/home'; |
| | | import type { EchartsUIType } from '@vben/plugins/echarts'; |
| | | |
| | | import { onMounted, ref } from 'vue'; |
| | | import { computed, onMounted, ref } from 'vue'; |
| | | |
| | | import { CountTo } from '@vben/common-ui'; |
| | | import { useAccess } from '@vben/access'; |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | import { EchartsUI, useEcharts } from '@vben/plugins/echarts'; |
| | | |
| | |
| | | const emit = defineEmits<{ |
| | | navigate: [name: string]; |
| | | }>(); |
| | | const { hasAccessByCodes } = useAccess(); |
| | | const canViewWorkOrder = computed(() => hasAccessByCodes(['mes:pro-work-order:query'])); |
| | | const canViewAndon = computed(() => hasAccessByCodes(['mes:pro-andon-record:query'])); |
| | | const canViewRepair = computed(() => hasAccessByCodes(['mes:dv-repair:query'])); |
| | | |
| | | const chartRef = ref<EchartsUIType>(); |
| | | const { renderEcharts } = useEcharts(chartRef); |
| | | const loading = ref(false); |
| | | |
| | | async function loadStatus() { |
| | | if (!canViewWorkOrder.value) return; |
| | | loading.value = true; |
| | | try { |
| | | const data = await getWorkOrderStatusDistribution(); |
| | |
| | | <template> |
| | | <div class="space-y-4"> |
| | | <!-- 预警卡片行 --> |
| | | <div class="grid grid-cols-2 gap-3"> |
| | | <div v-if="canViewAndon || canViewRepair" class="grid gap-3" :class="canViewAndon && canViewRepair ? 'grid-cols-2' : 'grid-cols-1'"> |
| | | <div |
| | | v-if="canViewAndon" |
| | | class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0 p-4" |
| | | @click="emit('navigate', 'MesProAndon')" |
| | | @click="emit('navigate', 'MesProAndonRecord')" |
| | | > |
| | | <div class="relative z-10 flex items-center gap-3"> |
| | | <div class="flex size-10 items-center justify-center rounded-lg bg-red-50 text-red-600 shadow-sm dark:bg-red-500/15 dark:text-red-400"> |
| | |
| | | </div> |
| | | </div> |
| | | <div |
| | | v-if="canViewRepair" |
| | | class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0 p-4" |
| | | @click="emit('navigate', 'MesProRepair')" |
| | | @click="emit('navigate', 'MesDvRepair')" |
| | | > |
| | | <div class="relative z-10 flex items-center gap-3"> |
| | | <div class="flex size-10 items-center justify-center rounded-lg bg-amber-50 text-amber-600 shadow-sm dark:bg-amber-500/15 dark:text-amber-400"> |
| | |
| | | </div> |
| | | |
| | | <!-- 工单状态图 --> |
| | | <div class="glass-tilt rounded-2xl p-5"> |
| | | <div v-if="canViewWorkOrder" class="glass-tilt rounded-2xl p-5"> |
| | | <h4 class="mb-3 text-sm font-semibold text-gray-800 dark:text-gray-100"> |
| | | 工单状态分布 |
| | | </h4> |
| | |
| | | <script lang="ts" setup> |
| | | import { onMounted, ref } from 'vue'; |
| | | |
| | | import { useAccess } from '@vben/access'; |
| | | import { CountTo } from '@vben/common-ui'; |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | |
| | |
| | | const emit = defineEmits<{ |
| | | navigate: [name: string]; |
| | | }>(); |
| | | const { hasAccessByCodes } = useAccess(); |
| | | const canPurchase = hasAccessByCodes(['erp:purchase-order:query']); |
| | | const canSale = hasAccessByCodes(['erp:sale-order:query']); |
| | | const canStock = hasAccessByCodes(['mes:wm-material-stock:query']); |
| | | const canBpmTodo = hasAccessByCodes(['bpm:task:query']); |
| | | |
| | | const purchaseCount = ref(0); |
| | | const saleCount = ref(0); |
| | |
| | | } |
| | | |
| | | onMounted(() => { |
| | | loadPurchase(); |
| | | loadSale(); |
| | | loadStock(); |
| | | loadBpmTodo(); |
| | | if (canPurchase) loadPurchase(); |
| | | if (canSale) loadSale(); |
| | | if (canStock) loadStock(); |
| | | if (canBpmTodo) loadBpmTodo(); |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Row :gutter="16"> |
| | | <!-- 采购订单 --> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <Col v-if="canPurchase" :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <div |
| | | class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0" |
| | | :style="tiltStyle('purchase')" |
| | |
| | | </Col> |
| | | |
| | | <!-- 销售订单 --> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <Col v-if="canSale" :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <div |
| | | class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0" |
| | | :style="tiltStyle('sale')" |
| | |
| | | </Col> |
| | | |
| | | <!-- 物料库存 --> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <Col v-if="canStock" :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <div |
| | | class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0" |
| | | :style="tiltStyle('stock')" |
| | |
| | | </Col> |
| | | |
| | | <!-- BPM 待办 --> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <Col v-if="canBpmTodo" :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <div |
| | | class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0" |
| | | :style="tiltStyle('bpm')" |
| | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { CountTo } from '@vben/common-ui'; |
| | | import { useAccess } from '@vben/access'; |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | |
| | | import { Col, Row } from 'ant-design-vue'; |
| | |
| | | const props = defineProps<{ |
| | | summary: MesHomeApi.Summary; |
| | | }>(); |
| | | const { hasAccessByCodes } = useAccess(); |
| | | |
| | | const canViewWorkOrder = computed(() => hasAccessByCodes(['mes:pro-work-order:query'])); |
| | | const canViewFeedback = computed(() => hasAccessByCodes(['mes:pro-feedback:query'])); |
| | | const canViewQuality = computed(() => hasAccessByCodes([ |
| | | 'mes:qc-iqc:query', 'mes:qc-ipqc:query', 'mes:qc-oqc:query', 'mes:qc-rqc:query', |
| | | ])); |
| | | const canViewEquipment = computed(() => hasAccessByCodes(['mes:dv-machinery:query'])); |
| | | |
| | | const emit = defineEmits<{ |
| | | navigate: [name: string]; |
| | |
| | | <template> |
| | | <Row :gutter="16"> |
| | | <!-- 生产订单 --> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <Col v-if="canViewWorkOrder" :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <div |
| | | class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0" |
| | | :style="tiltStyle('order')" |
| | |
| | | </Col> |
| | | |
| | | <!-- 今日产量 --> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <Col v-if="canViewFeedback" :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <div |
| | | class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0" |
| | | :style="tiltStyle('output')" |
| | |
| | | </Col> |
| | | |
| | | <!-- 质量合格率 --> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <Col v-if="canViewQuality" :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <div |
| | | class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0" |
| | | :style="tiltStyle('quality')" |
| | |
| | | </Col> |
| | | |
| | | <!-- 设备运行率 --> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <Col v-if="canViewEquipment" :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <div |
| | | class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0" |
| | | :style="tiltStyle('equip')" |
| | |
| | | <script lang="ts" setup> |
| | | import { computed } from 'vue'; |
| | | |
| | | import { useAccess } from '@vben/access'; |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | |
| | | defineOptions({ name: 'DashboardQuickActions' }); |
| | |
| | | route: string; |
| | | gradient: string; |
| | | shadow: string; |
| | | permissions: string[]; |
| | | } |
| | | |
| | | const actions: QuickAction[] = [ |
| | | { title: '生产工单', icon: 'lucide:file-text', route: 'MesProWorkOrder', gradient: 'from-blue-500 to-cyan-500', shadow: 'shadow-blue-500/15' }, |
| | | { title: '生产报工', icon: 'lucide:check-square', route: 'MesProFeedback', gradient: 'from-emerald-500 to-green-500', shadow: 'shadow-emerald-500/15' }, |
| | | { title: '质检管理', icon: 'lucide:clipboard-check', route: 'MesQcRecord', gradient: 'from-orange-500 to-amber-500', shadow: 'shadow-orange-500/15' }, |
| | | { title: '设备点检', icon: 'lucide:settings', route: 'MesDvMachinery', gradient: 'from-violet-500 to-purple-500', shadow: 'shadow-violet-500/15' }, |
| | | { title: '生产排程', icon: 'lucide:calendar-range', route: 'MesProScheduling', gradient: 'from-cyan-500 to-teal-500', shadow: 'shadow-cyan-500/15' }, |
| | | { title: '安灯呼叫', icon: 'lucide:bell-ring', route: 'MesProAndon', gradient: 'from-red-500 to-rose-500', shadow: 'shadow-red-500/15' }, |
| | | { title: '物料库存', icon: 'lucide:package-search', route: 'WlsMaterialStock', gradient: 'from-indigo-500 to-blue-500', shadow: 'shadow-indigo-500/15' }, |
| | | { title: '工艺路线', icon: 'lucide:route', route: 'MesPdRouting', gradient: 'from-teal-500 to-emerald-500', shadow: 'shadow-teal-500/15' }, |
| | | { title: '数据报表', icon: 'lucide:bar-chart-big', route: 'MesHomeIndex', gradient: 'from-pink-500 to-rose-500', shadow: 'shadow-pink-500/15' }, |
| | | const { hasAccessByCodes } = useAccess(); |
| | | const allActions: QuickAction[] = [ |
| | | { title: '生产工单', icon: 'lucide:file-text', route: 'MesProWorkOrder', gradient: 'from-blue-500 to-cyan-500', shadow: 'shadow-blue-500/15', permissions: ['mes:pro-work-order:query'] }, |
| | | { title: '生产报工', icon: 'lucide:check-square', route: 'MesProFeedback', gradient: 'from-emerald-500 to-green-500', shadow: 'shadow-emerald-500/15', permissions: ['mes:pro-feedback:query'] }, |
| | | { title: '质检管理', icon: 'lucide:clipboard-check', route: 'MesQcIqc', gradient: 'from-orange-500 to-amber-500', shadow: 'shadow-orange-500/15', permissions: ['mes:qc-iqc:query', 'mes:qc-ipqc:query', 'mes:qc-oqc:query', 'mes:qc-rqc:query'] }, |
| | | { title: '设备台账', icon: 'lucide:settings', route: 'MesDvMachinery', gradient: 'from-violet-500 to-purple-500', shadow: 'shadow-violet-500/15', permissions: ['mes:dv-machinery:query'] }, |
| | | { title: '员工排班', icon: 'lucide:calendar-range', route: 'MesCalScheduleDetail', gradient: 'from-cyan-500 to-teal-500', shadow: 'shadow-cyan-500/15', permissions: ['mes:cal-schedule-detail:query'] }, |
| | | { title: '安灯呼叫', icon: 'lucide:bell-ring', route: 'MesProAndonRecord', gradient: 'from-red-500 to-rose-500', shadow: 'shadow-red-500/15', permissions: ['mes:pro-andon-record:query'] }, |
| | | { title: '物料库存', icon: 'lucide:package-search', route: 'MesWmMaterialStock', gradient: 'from-indigo-500 to-blue-500', shadow: 'shadow-indigo-500/15', permissions: ['mes:wm-material-stock:query'] }, |
| | | { title: '工艺路线', icon: 'lucide:route', route: 'MesProRoute', gradient: 'from-teal-500 to-emerald-500', shadow: 'shadow-teal-500/15', permissions: ['mes:pro-route:query'] }, |
| | | ]; |
| | | const actions = computed(() => |
| | | allActions.filter((action) => hasAccessByCodes(action.permissions)), |
| | | ); |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="glass-card rounded-2xl p-5"> |
| | | <div v-if="actions.length" class="glass-card rounded-2xl p-5"> |
| | | <h3 class="mb-4 text-sm font-semibold text-gray-800 dark:text-gray-100">快捷操作</h3> |
| | | <div class="grid grid-cols-3 gap-3 sm:grid-cols-9"> |
| | | <div class="grid grid-cols-2 gap-3 sm:grid-cols-4 lg:grid-cols-8"> |
| | | <div |
| | | v-for="action in actions" |
| | | :key="action.route" |
| | |
| | | fieldName: 'depositPrice', |
| | | label: '定金', |
| | | component: 'InputNumber', |
| | | defaultValue: 0, |
| | | componentProps: { |
| | | placeholder: '请输入定金', |
| | | min: 0, |
| | | precision: 2, |
| | | disabled: formType === 'detail', |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'totalTaxPrice', |
| | | label: '税额', |
| | | component: 'InputNumber', |
| | | componentProps: { |
| | | placeholder: '根据明细税率自动计算', |
| | | min: 0, |
| | | precision: 2, |
| | | disabled: true, |
| | | }, |
| | | }, |
| | | { |
| | |
| | | formatter: 'formatAmount2', |
| | | }, |
| | | { |
| | | field: 'totalTaxPrice', |
| | | title: '税额', |
| | | formatter: 'formatAmount2', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'taxPercent', |
| | | title: '税率(%)', |
| | | minWidth: 100, |
| | |
| | | formApi.setDisabled(formType.value === 'detail'); |
| | | formApi.updateSchema(useFormSchema(formType.value)); |
| | | if (!data || !data.id) { |
| | | formData.value = { |
| | | requestTime: '', |
| | | depositPrice: 0, |
| | | items: [], |
| | | }; |
| | | await formApi.setFieldValue('depositPrice', 0); |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | |
| | | </template> |
| | | </Form> |
| | | </Modal> |
| | | </template> |
| | | </template> |
| | |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { ErpSaleOrderApi } from '#/api/erp/sale/order'; |
| | | |
| | | import { onMounted, ref } from 'vue'; |
| | | import { computed, onMounted, ref } from 'vue'; |
| | | import { useRoute, useRouter } from 'vue-router'; |
| | | |
| | | import { Page, useVbenModal } from '@vben/common-ui'; |
| | | import { useUserStore } from '@vben/stores'; |
| | | import { downloadFileFromBlobPart, isEmpty } from '@vben/utils'; |
| | | |
| | | import { message, Tag, Tooltip } from 'ant-design-vue'; |
| | |
| | | |
| | | const router = useRouter(); |
| | | const route = useRoute(); |
| | | const userStore = useUserStore(); |
| | | const isSuperAdmin = computed(() => |
| | | userStore.userRoles?.includes('super_admin'), |
| | | ); |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | |
| | | type: 'link', |
| | | icon: ACTION_ICON.AUDIT, |
| | | auth: ['erp:sale-order:update-status'], |
| | | ifShow: () => row.status === 20, |
| | | ifShow: () => row.status === 20 && isSuperAdmin.value, |
| | | popConfirm: { |
| | | title: `确认反审批${row.no}吗?`, |
| | | confirm: handleUpdateStatus.bind(null, row, 10), |
| | |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | import type { AttendanceClockLocation } from '#/api/hrm/attendance/record'; |
| | | import { clockIn, clockOut } from '#/api/hrm/attendance/record'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | const location = ref('公司前台'); |
| | | const loading = ref(false); |
| | | |
| | | function getCurrentLocation(): Promise<AttendanceClockLocation> { |
| | | if (!navigator.geolocation) { |
| | | return Promise.reject(new Error('当前浏览器不支持定位,请使用支持 GPS 的浏览器')); |
| | | } |
| | | return new Promise((resolve, reject) => { |
| | | navigator.geolocation.getCurrentPosition( |
| | | (position) => resolve({ |
| | | longitude: position.coords.longitude, |
| | | latitude: position.coords.latitude, |
| | | accuracy: position.coords.accuracy, |
| | | coordinateSystem: 'WGS84', |
| | | }), |
| | | () => reject(new Error('定位失败,请开启定位权限后重试(非 localhost 需使用 HTTPS)')), |
| | | { enableHighAccuracy: true, maximumAge: 0, timeout: 15_000 }, |
| | | ); |
| | | }); |
| | | } |
| | | |
| | | /** 上班打卡 */ |
| | | async function handleClockIn() { |
| | | loading.value = true; |
| | | try { |
| | | await clockIn({ location: location.value }); |
| | | await clockIn(await getCurrentLocation()); |
| | | message.success('上班打卡成功'); |
| | | emit('success'); |
| | | } catch (e: any) { |
| | |
| | | async function handleClockOut() { |
| | | loading.value = true; |
| | | try { |
| | | await clockOut({ location: location.value }); |
| | | await clockOut(await getCurrentLocation()); |
| | | message.success('下班打卡成功'); |
| | | emit('success'); |
| | | } catch (e: any) { |
| | |
| | | } |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | location.value = '公司前台'; |
| | | } |
| | | }, |
| | | onOpenChange() {}, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal title="打卡" class="w-[400px]"> |
| | | <div class="p-4"> |
| | | <div class="mb-4"> |
| | | <label class="mr-2">打卡地点:</label> |
| | | <a-input |
| | | v-model:value="location" |
| | | placeholder="请输入打卡地点" |
| | | style="width: 200px" |
| | | /> |
| | | <div class="mb-4 text-sm text-gray-500"> |
| | | 点击打卡后将获取当前 GPS 位置,并按考勤规则中的经纬度和打卡半径校验。 |
| | | </div> |
| | | <div class="flex gap-4"> |
| | | <a-button |
| | |
| | | </div> |
| | | </div> |
| | | </Modal> |
| | | </template> |
| | | </template> |
| | |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'locationRequired', |
| | | label: '定位打卡', |
| | | component: 'Switch', |
| | | defaultValue: false, |
| | | componentProps: { |
| | | checkedChildren: '启用', |
| | | unCheckedChildren: '关闭', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'locationName', |
| | | label: '打卡地点', |
| | | component: 'Input', |
| | | componentProps: { placeholder: '例如:新高电力厂区' }, |
| | | dependencies: { |
| | | triggerFields: ['locationRequired'], |
| | | show: (values) => Boolean(values.locationRequired), |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'locationAddress', |
| | | label: '地点地址', |
| | | component: 'Input', |
| | | componentProps: { placeholder: '请输入详细地址,便于员工识别' }, |
| | | dependencies: { |
| | | triggerFields: ['locationRequired'], |
| | | show: (values) => Boolean(values.locationRequired), |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'longitude', |
| | | label: '经度(WGS84)', |
| | | component: 'InputNumber', |
| | | componentProps: { min: -180, max: 180, precision: 7, style: { width: '100%' } }, |
| | | dependencies: { |
| | | triggerFields: ['locationRequired'], |
| | | show: (values) => Boolean(values.locationRequired), |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'latitude', |
| | | label: '纬度(WGS84)', |
| | | component: 'InputNumber', |
| | | componentProps: { min: -90, max: 90, precision: 7, style: { width: '100%' } }, |
| | | dependencies: { |
| | | triggerFields: ['locationRequired'], |
| | | show: (values) => Boolean(values.locationRequired), |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'radiusMeters', |
| | | label: '打卡半径(米)', |
| | | component: 'InputNumber', |
| | | defaultValue: 100, |
| | | componentProps: { min: 10, max: 5000, precision: 0, style: { width: '100%' } }, |
| | | help: '最小可设 10 米;实际精度取决于手机 GPS 与环境', |
| | | dependencies: { |
| | | triggerFields: ['locationRequired'], |
| | | show: (values) => Boolean(values.locationRequired), |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'coordinateSystem', |
| | | label: '坐标系', |
| | | component: 'Input', |
| | | defaultValue: 'WGS84', |
| | | componentProps: { disabled: true }, |
| | | dependencies: { |
| | | triggerFields: ['locationRequired'], |
| | | show: (values) => Boolean(values.locationRequired), |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | | label: '状态', |
| | | component: 'RadioGroup', |
| | |
| | | { field: 'earlyAllowMinutes', title: '早退允许(分钟)', width: 120 }, |
| | | { field: 'overtimeThreshold', title: '加班起算(小时)', width: 120 }, |
| | | { |
| | | field: 'locationName', |
| | | title: '打卡范围', |
| | | minWidth: 180, |
| | | formatter: ({ row }) => row.locationRequired |
| | | ? `${row.locationName || '-'}(${row.radiusMeters || 0}米)` |
| | | : '不限制位置', |
| | | }, |
| | | { |
| | | field: 'status', |
| | | title: '状态', |
| | | width: 80, |
| | |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | | } |
| | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { BarcodeBizTypeEnum } from '@vben/constants'; |
| | | |
| | | import { Button, Empty, message, Tabs } from 'ant-design-vue'; |
| | | import { Button, message, Tabs } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { createItem, getItem, updateItem } from '#/api/mes/md/item'; |
| | |
| | | :item-id="formData.id" |
| | | :item-or-product="currentItemOrProduct" |
| | | /> |
| | | </Tabs.TabPane> |
| | | <Tabs.TabPane key="substitute" tab="替代品"> |
| | | <Empty description="替代品(待实现)" /> |
| | | </Tabs.TabPane> |
| | | <Tabs.TabPane key="sip" tab="检验指导书(SIP)"> |
| | | <ProductSipForm :form-type="formType" :item-id="formData.id" /> |
| | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { createMps, getMps, updateMps } from '#/api/mes/pro/mps'; |
| | | import { $t } from '#/locales'; |
| | | import { isAiFeatureEnabled } from '#/utils/features'; |
| | | |
| | | import PredictDelivery from '../../workorder/modules/ai/predict-delivery.vue'; |
| | | import PredictDuration from '../../workorder/modules/ai/predict-duration.vue'; |
| | | import PredictMaterial from '../../workorder/modules/ai/predict-material.vue'; |
| | | import PredictRisk from '../../workorder/modules/ai/predict-risk.vue'; |
| | | import { MpsStatusEnum, useFormSchema } from '../data'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | |
| | | |
| | | const isEditable = computed(() => ['create', 'edit'].includes(formType.value)); |
| | | |
| | | const canUseAi = computed(() => isAiFeatureEnabled && !!formData.value?.id); |
| | | |
| | | const getTitle = computed(() => { |
| | | switch (formType.value) { |
| | |
| | | </Descriptions.Item> |
| | | </Descriptions> |
| | | </div> |
| | | <template #prepend-footer> |
| | | <div v-if="canUseAi" class="flex items-center gap-2"> |
| | | <PredictMaterial :mps-id="formData!.id" :work-order-id="formData!.workOrderId" /> |
| | | <PredictDuration :mps-id="formData!.id" :work-order-id="formData!.workOrderId" /> |
| | | <PredictRisk :mps-id="formData!.id" :work-order-id="formData!.workOrderId" /> |
| | | <PredictDelivery :mps-id="formData!.id" :work-order-id="formData!.workOrderId" /> |
| | | </div> |
| | | </template> |
| | | </Modal> |
| | | </template> |
| | |
| | | |
| | | import { z } from '#/adapter/form'; |
| | | import { generateAutoCode } from '#/api/mes/md/autocode/record'; |
| | | import { getWarehouseSimpleList } from '#/api/mes/wm/warehouse'; |
| | | import { getWarehouseAreaSimpleList } from '#/api/mes/wm/warehouse/area'; |
| | | import { getWarehouseLocationSimpleList } from '#/api/mes/wm/warehouse/location'; |
| | | import { |
| | | MdItemSelect, |
| | | MdProductBomSelect, |
| | |
| | | /** 工艺路线工序明细表单 */ |
| | | export function useRouteProcessFormSchema( |
| | | processOptions: Array<{ label: string; value: number }>, |
| | | productIds: number[] = [], |
| | | ): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | |
| | | unCheckedChildren: '否', |
| | | }, |
| | | rules: z.boolean().default(false), |
| | | }, |
| | | { |
| | | fieldName: 'outputItemIds', |
| | | label: '产出产品', |
| | | component: markRaw(MdItemSelect), |
| | | componentProps: { |
| | | multiple: true, |
| | | productIds, |
| | | placeholder: '中间工序选半成品,末道工序选全部成品', |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'prepareTime', |
| | |
| | | }, |
| | | { field: 'prepareTime', title: '准备时间(分)', width: 110 }, |
| | | { field: 'waitTime', title: '等待时间(分)', width: 110 }, |
| | | { field: 'outputItemNames', title: '产出产品', minWidth: 180 }, |
| | | { |
| | | field: 'colorCode', |
| | | title: '甘特图颜色', |
| | |
| | | { field: 'specification', title: '规格型号', width: 150 }, |
| | | { field: 'unitName', title: '单位', width: 80 }, |
| | | { field: 'quantity', title: '生产数量', width: 100 }, |
| | | { field: 'warehouseName', title: '报工仓库', width: 130 }, |
| | | { field: 'locationName', title: '库区', width: 120 }, |
| | | { field: 'areaName', title: '库位', width: 120 }, |
| | | { |
| | | field: 'productionTime', |
| | | title: '生产用时', |
| | |
| | | /** 工艺路线产品表单 */ |
| | | export function useRouteProductFormSchema( |
| | | onItemChange?: (item: any) => void, |
| | | getFormApi?: () => VbenFormApi, |
| | | ): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | |
| | | rules: z.number().default(1), |
| | | }, |
| | | { |
| | | fieldName: 'warehouseId', |
| | | label: '报工仓库', |
| | | component: 'ApiSelect', |
| | | componentProps: { |
| | | api: getWarehouseSimpleList, |
| | | labelField: 'name', |
| | | valueField: 'id', |
| | | placeholder: '请选择报工产出仓库', |
| | | onChange: async () => { |
| | | await getFormApi?.().setFieldValue('locationId', undefined); |
| | | await getFormApi?.().setFieldValue('areaId', undefined); |
| | | }, |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'locationId', |
| | | label: '报工库区', |
| | | component: 'Select', |
| | | dependencies: { |
| | | triggerFields: ['warehouseId'], |
| | | disabled: (values) => !values.warehouseId, |
| | | async componentProps(values) { |
| | | const options = values.warehouseId |
| | | ? await getWarehouseLocationSimpleList(values.warehouseId) |
| | | : []; |
| | | return { |
| | | fieldNames: { label: 'name', value: 'id' }, |
| | | options, |
| | | placeholder: '请选择报工产出库区', |
| | | onChange: async () => getFormApi?.().setFieldValue('areaId', undefined), |
| | | }; |
| | | }, |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'areaId', |
| | | label: '报工库位', |
| | | component: 'Select', |
| | | dependencies: { |
| | | triggerFields: ['locationId'], |
| | | disabled: (values) => !values.locationId, |
| | | async componentProps(values) { |
| | | const options = values.locationId |
| | | ? await getWarehouseAreaSimpleList(values.locationId) |
| | | : []; |
| | | return { |
| | | fieldNames: { label: 'name', value: 'id' }, |
| | | options, |
| | | placeholder: '请选择报工产出库位', |
| | | }; |
| | | }, |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'productionTime', |
| | | label: '生产用时', |
| | | component: 'InputNumber', |
| | |
| | | getRouteProcess, |
| | | updateRouteProcess, |
| | | } from '#/api/mes/pro/route/process'; |
| | | import { getRouteProductListByRoute } from '#/api/mes/pro/route/product'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useRouteProcessFormSchema } from '../data'; |
| | |
| | | }); |
| | | |
| | | /** 加载工序选项后再生成 schema(避免下拉空选项) */ |
| | | async function loadSchema(): Promise<VbenFormSchema[]> { |
| | | async function loadSchema(routeId: number): Promise<VbenFormSchema[]> { |
| | | const list = await getProcessSimpleList(); |
| | | const options = (list || []).map((item) => ({ |
| | | label: item.name ?? '', |
| | | value: item.id!, |
| | | })); |
| | | return useRouteProcessFormSchema(options); |
| | | const products = routeId ? await getRouteProductListByRoute(routeId) : []; |
| | | return useRouteProcessFormSchema( |
| | | options, |
| | | products.map((item) => item.itemId!).filter(Boolean), |
| | | ); |
| | | } |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | |
| | | formData.value = undefined; |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | // 工序下拉依赖远程数据,schema 在弹窗打开时再生成 |
| | | const schema = await loadSchema(); |
| | | formApi.setState({ schema }); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | // 加载数据 |
| | | const data = modalApi.getData<{ |
| | | id?: number; |
| | | maxSort?: number; |
| | |
| | | if (!data) { |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | // 工序下拉依赖远程数据,schema 在弹窗打开时再生成 |
| | | const schema = await loadSchema(data.routeId); |
| | | formApi.setState({ schema }); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | // 加载数据 |
| | | if (!data.id) { |
| | | // 新增时,默认序号 = maxSort + 1,并给一个默认甘特图颜色 |
| | | await formApi.setValues({ |
| | |
| | | labelWidth: 100, |
| | | }, |
| | | layout: 'horizontal', |
| | | schema: useRouteProductFormSchema(handleItemChange), |
| | | schema: useRouteProductFormSchema(handleItemChange, () => formApi), |
| | | showDefaultActions: false, |
| | | wrapperClass: 'grid-cols-2', |
| | | }); |
| | |
| | | } from "#/api/mes/pro/workorder"; |
| | | import { batchSaveWorkOrderProcess, getWorkOrderProcessListByWorkOrderId } from "#/api/mes/pro/workorder/process"; |
| | | import { $t } from "#/locales"; |
| | | import { isAiFeatureEnabled } from "#/utils/features"; |
| | | // import { BarcodeDetail } from "#/views/wls/barcode/components"; |
| | | |
| | | import { useFormSchema } from "../data"; |
| | | import PredictDelivery from "./ai/predict-delivery.vue"; |
| | | import PredictDuration from "./ai/predict-duration.vue"; |
| | | import PredictMaterial from "./ai/predict-material.vue"; |
| | | import PredictRisk from "./ai/predict-risk.vue"; |
| | | import ItemList from "./item-list.vue"; |
| | | import ProcessList from "./process-list.vue"; |
| | | |
| | |
| | | () => |
| | | formType.value === "update" && |
| | | formData.value?.status === MesProWorkOrderStatusEnum.PREPARE |
| | | ); |
| | | const canUseAi = computed( |
| | | () => |
| | | isAiFeatureEnabled && |
| | | formData.value?.id && |
| | | (formData.value?.status === MesProWorkOrderStatusEnum.PREPARE || |
| | | formData.value?.status === MesProWorkOrderStatusEnum.CONFIRMED) |
| | | ); |
| | | const getTitle = computed(() => { |
| | | const isChild = !!formData.value?.parentId; |
| | |
| | | </template> |
| | | <template #prepend-footer> |
| | | <div class="flex flex-auto flex-col gap-2"> |
| | | <div v-if="canUseAi" class="flex items-center gap-2"> |
| | | <PredictMaterial :work-order-id="formData!.id" /> |
| | | <PredictDuration :work-order-id="formData!.id" /> |
| | | <PredictRisk :work-order-id="formData!.id" /> |
| | | <PredictDelivery :work-order-id="formData!.id" /> |
| | | </div> |
| | | <div v-if="canUseAi && showMainBtn" class="border-t border-gray-200" /> |
| | | <div class="flex items-center"> |
| | | <Button v-if="showMainBtn" |
| | | type="primary" |
| | |
| | | options: getDictOptions(DICT_TYPE.MES_QC_CHECK_RESULT, 'number'), |
| | | placeholder: '请选择检测结果', |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'remark', |
| | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { createIpqc, finishIpqc, getIpqc, updateIpqc } from '#/api/mes/qc/ipqc'; |
| | | import { $t } from '#/locales'; |
| | | import { isAiFeatureEnabled } from '#/utils/features'; |
| | | |
| | | import { QcIndicatorResultList } from '../../indicatorresult/components'; |
| | | import MesQcAiSuggest from '../../components/ai-suggest.vue'; |
| | | import MesQcAiAssistant from '../../components/ai-assistant.vue'; |
| | | import { useFormSchema } from '../data'; |
| | | import LineList from './line-list.vue'; |
| | | |
| | |
| | | <Button v-if="canFinish" type="primary" @click="handleFinish"> |
| | | 完成 |
| | | </Button> |
| | | <MesQcAiSuggest |
| | | v-if="isAiFeatureEnabled && canFinish && formData?.id" |
| | | :qc-id="formData.id" |
| | | :qc-type="MesQcTypeEnum.IPQC" |
| | | /> |
| | | <MesQcAiAssistant v-if="isAiFeatureEnabled" /> |
| | | </div> |
| | | </template> |
| | | </Modal> |
| | |
| | | options: getDictOptions(DICT_TYPE.MES_QC_CHECK_RESULT, 'number'), |
| | | placeholder: '请选择检测结果', |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'remark', |
| | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { createIqc, finishIqc, getIqc, updateIqc } from '#/api/mes/qc/iqc'; |
| | | import { $t } from '#/locales'; |
| | | import { isAiFeatureEnabled } from '#/utils/features'; |
| | | |
| | | import { QcIndicatorResultList } from '../../indicatorresult/components'; |
| | | import MesQcAiSuggest from '../../components/ai-suggest.vue'; |
| | | import MesQcAiAssistant from '../../components/ai-assistant.vue'; |
| | | import { useFormSchema } from '../data'; |
| | | import LineList from './line-list.vue'; |
| | | |
| | |
| | | <Button v-if="canFinish" type="primary" @click="handleFinish"> |
| | | 完成 |
| | | </Button> |
| | | <MesQcAiSuggest |
| | | v-if="isAiFeatureEnabled && canFinish && formData?.id" |
| | | :qc-id="formData.id" |
| | | :qc-type="MesQcTypeEnum.IQC" |
| | | /> |
| | | <MesQcAiAssistant v-if="isAiFeatureEnabled" /> |
| | | </div> |
| | | </template> |
| | | </Modal> |
| | |
| | | options: getDictOptions(DICT_TYPE.MES_QC_CHECK_RESULT, 'number'), |
| | | placeholder: '请选择检测结果', |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'remark', |
| | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { createOqc, finishOqc, getOqc, updateOqc } from '#/api/mes/qc/oqc'; |
| | | import { $t } from '#/locales'; |
| | | import { isAiFeatureEnabled } from '#/utils/features'; |
| | | |
| | | import { QcIndicatorResultList } from '../../indicatorresult/components'; |
| | | import MesQcAiSuggest from '../../components/ai-suggest.vue'; |
| | | import MesQcAiAssistant from '../../components/ai-assistant.vue'; |
| | | import { useFormSchema } from '../data'; |
| | | import LineList from './line-list.vue'; |
| | | |
| | |
| | | <Button v-if="canFinish" type="primary" @click="handleFinish"> |
| | | 完成 |
| | | </Button> |
| | | <MesQcAiSuggest |
| | | v-if="isAiFeatureEnabled && canFinish && formData?.id" |
| | | :qc-id="formData.id" |
| | | :qc-type="MesQcTypeEnum.OQC" |
| | | /> |
| | | <MesQcAiAssistant v-if="isAiFeatureEnabled" /> |
| | | </div> |
| | | </template> |
| | | </Modal> |
| | |
| | | options: getDictOptions(DICT_TYPE.MES_QC_CHECK_RESULT, 'number'), |
| | | placeholder: '请选择检测结果', |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'remark', |
| | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { createRqc, finishRqc, getRqc, updateRqc } from '#/api/mes/qc/rqc'; |
| | | import { $t } from '#/locales'; |
| | | import { isAiFeatureEnabled } from '#/utils/features'; |
| | | |
| | | import { QcIndicatorResultList } from '../../indicatorresult/components'; |
| | | import MesQcAiSuggest from '../../components/ai-suggest.vue'; |
| | | import MesQcAiAssistant from '../../components/ai-assistant.vue'; |
| | | import { useFormSchema } from '../data'; |
| | | import LineList from './line-list.vue'; |
| | | |
| | |
| | | <Button v-if="canFinish" type="primary" @click="handleFinish"> |
| | | 完成 |
| | | </Button> |
| | | <MesQcAiSuggest |
| | | v-if="isAiFeatureEnabled && canFinish && formData?.id" |
| | | :qc-id="formData.id" |
| | | :qc-type="MesQcTypeEnum.RQC" |
| | | /> |
| | | <MesQcAiAssistant v-if="isAiFeatureEnabled" /> |
| | | </div> |
| | | </template> |
| | | </Modal> |
| | |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'nickname', |
| | | label: '用户昵称', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请输入用户昵称', |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | { |
| | | label: '用户密码', |
| | | fieldName: 'password', |
| | | component: 'InputPassword', |