云
2026-10-25 b7ffa7395cddfeb10f88d70a81567371f62021c3
feat(router): 排除AI模块页面并添加访问权限控制

- 在构建时排除 ../views/ai/**/*.vue 文件以避免生成不可访问的AI分包
- 为仪表板预警面板组件添加访问权限检查
- 为业务卡片组件添加采购、销售、库存、BPM待办权限控制
- 更新考勤打卡功能以使用GPS定位代替手动输入地点
- 在工艺路线配置中添加仓库、库区、库位选择功能
- 为质量控制表单添加必填验证规则
- 移除AI相关功能组件以适配轻量版后端
- 添加用户昵称字段到用户管理模块
- 更新采购申请表单以包含定金和税额字段
- 移除物料管理中的替代品标签页
- 添加考勤规则中的定位打卡配置选项
已修改31个文件
553 ■■■■■ 文件已修改
src/api/erp/purchase/request/index.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/attendance/record/index.ts 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/attendance/rule/index.ts 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pro/route/product/index.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/access.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/saleQuotation/index.vue 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/dashboard/analytics/index.vue 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/dashboard/analytics/modules/alert-panel.vue 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/dashboard/analytics/modules/biz-cards.vue 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/dashboard/analytics/modules/kpi-cards.vue 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/dashboard/analytics/modules/quick-actions.vue 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/request/data.ts 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/request/modules/form.vue 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/sale/order/index.vue 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/attendance/record/modules/clock-panel.vue 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/attendance/rule/data.ts 87 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/md/item/modules/form.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/mps/modules/form.vue 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/route/data.ts 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/route/modules/process-form.vue 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/route/modules/product-form.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/workorder/modules/form.vue 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/ipqc/data.ts 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/ipqc/modules/form.vue 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/iqc/data.ts 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/iqc/modules/form.vue 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/oqc/data.ts 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/oqc/modules/form.vue 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/rqc/data.ts 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/rqc/modules/form.vue 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/user/data.ts 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/erp/purchase/request/index.ts
@@ -17,10 +17,12 @@
    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; // 生成的采购订单号
src/api/hrm/attendance/record/index.ts
@@ -79,20 +79,25 @@
  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,
  );
}
}
src/api/hrm/attendance/rule/index.ts
@@ -14,6 +14,13 @@
    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;
@@ -73,4 +80,4 @@
    file,
    updateSupport,
  });
}
}
src/api/mes/pro/route/product/index.ts
@@ -8,6 +8,12 @@
    itemId?: number;
    itemCode?: string;
    itemName?: string;
    warehouseId?: number;
    warehouseName?: string;
    locationId?: number;
    locationName?: string;
    areaId?: number;
    areaName?: string;
    specification?: string;
    unitName?: string;
    quantity?: number;
src/router/access.ts
@@ -40,7 +40,11 @@
}
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 = {
src/views/crm/saleQuotation/index.vue
@@ -16,15 +16,9 @@
  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();
@@ -32,8 +26,6 @@
  connectedComponent: Form,
  destroyOnClose: true,
});
const ocrUploadRef = ref<InstanceType<typeof OcrUpload>>();
/** 刷新表格 */
function handleRefresh() {
@@ -45,18 +37,6 @@
  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', ['报价单']),
@@ -66,14 +46,6 @@
    onClick: handleCreate,
  },
];
if (isAiFeatureEnabled) {
  toolbarActions.push({
    label: 'AI 识别录入',
    type: 'default',
    auth: ['crm:sale-quotation:create'],
    onClick: handleOcrCreate,
  });
}
/** 编辑报价单 */
function handleEdit(row: CrmSaleQuotationApi.SaleQuotation) {
@@ -166,11 +138,6 @@
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <OcrUpload
      v-if="isAiFeatureEnabled"
      ref="ocrUploadRef"
      @success="handleOcrSuccess"
    />
    <Grid>
      <template #toolbar-tools>
        <TableAction :actions="toolbarActions" />
src/views/dashboard/analytics/index.vue
@@ -3,6 +3,7 @@
import { useRouter } from 'vue-router';
import { Page } from '@vben/common-ui';
import { useAccess } from '@vben/access';
import { Card, Col, Row } from 'ant-design-vue';
@@ -18,7 +19,29 @@
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();
@@ -41,6 +64,7 @@
}
async function loadSummary() {
  if (!canViewMesSummary.value) return;
  summary.value = await getHomeSummary();
}
@@ -71,14 +95,14 @@
    </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天)
@@ -86,7 +110,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"
src/views/dashboard/analytics/modules/alert-panel.vue
@@ -2,9 +2,10 @@
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';
@@ -22,12 +23,17 @@
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();
@@ -67,10 +73,11 @@
<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">
@@ -86,8 +93,9 @@
        </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">
@@ -105,7 +113,7 @@
    </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>
src/views/dashboard/analytics/modules/biz-cards.vue
@@ -1,6 +1,7 @@
<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';
@@ -16,6 +17,11 @@
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);
@@ -75,17 +81,17 @@
}
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')"
@@ -112,7 +118,7 @@
    </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')"
@@ -139,7 +145,7 @@
    </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')"
@@ -166,7 +172,7 @@
    </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')"
src/views/dashboard/analytics/modules/kpi-cards.vue
@@ -4,6 +4,7 @@
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';
@@ -13,6 +14,14 @@
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];
@@ -59,7 +68,7 @@
<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')"
@@ -90,7 +99,7 @@
    </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')"
@@ -121,7 +130,7 @@
    </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')"
@@ -154,7 +163,7 @@
    </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')"
src/views/dashboard/analytics/modules/quick-actions.vue
@@ -1,4 +1,7 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { useAccess } from '@vben/access';
import { IconifyIcon } from '@vben/icons';
defineOptions({ name: 'DashboardQuickActions' });
@@ -13,25 +16,29 @@
  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"
src/views/erp/purchase/request/data.ts
@@ -84,11 +84,24 @@
      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,
      },
    },
    {
@@ -180,6 +193,12 @@
      formatter: 'formatAmount2',
    },
    {
      field: 'totalTaxPrice',
      title: '税额',
      formatter: 'formatAmount2',
      minWidth: 120,
    },
    {
      field: 'taxPercent',
      title: '税率(%)',
      minWidth: 100,
src/views/erp/purchase/request/modules/form.vue
@@ -108,6 +108,12 @@
    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();
@@ -148,4 +154,4 @@
      </template>
    </Form>
  </Modal>
</template>
</template>
src/views/erp/sale/order/index.vue
@@ -2,10 +2,11 @@
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';
@@ -35,6 +36,10 @@
const router = useRouter();
const route = useRoute();
const userStore = useUserStore();
const isSuperAdmin = computed(() =>
  userStore.userRoles?.includes('super_admin'),
);
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
@@ -356,7 +361,7 @@
              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),
src/views/hrm/attendance/record/modules/clock-panel.vue
@@ -5,18 +5,36 @@
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) {
@@ -30,7 +48,7 @@
async function handleClockOut() {
  loading.value = true;
  try {
    await clockOut({ location: location.value });
    await clockOut(await getCurrentLocation());
    message.success('下班打卡成功');
    emit('success');
  } catch (e: any) {
@@ -41,24 +59,15 @@
}
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
@@ -79,4 +88,4 @@
      </div>
    </div>
  </Modal>
</template>
</template>
src/views/hrm/attendance/rule/data.ts
@@ -106,6 +106,83 @@
      },
    },
    {
      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',
@@ -186,6 +263,14 @@
    { 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,
@@ -203,4 +288,4 @@
      },
    },
  ];
}
}
src/views/mes/md/item/modules/form.vue
@@ -8,7 +8,7 @@
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';
@@ -137,9 +137,6 @@
          :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" />
src/views/mes/pro/mps/modules/form.vue
@@ -12,12 +12,6 @@
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']);
@@ -26,7 +20,6 @@
const isEditable = computed(() => ['create', 'edit'].includes(formType.value));
const canUseAi = computed(() => isAiFeatureEnabled && !!formData.value?.id);
const getTitle = computed(() => {
  switch (formType.value) {
@@ -178,13 +171,5 @@
        </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>
src/views/mes/pro/route/data.ts
@@ -18,6 +18,9 @@
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,
@@ -180,6 +183,7 @@
/** 工艺路线工序明细表单 */
export function useRouteProcessFormSchema(
  processOptions: Array<{ label: string; value: number }>,
  productIds: number[] = [],
): VbenFormSchema[] {
  return [
    {
@@ -250,6 +254,17 @@
        unCheckedChildren: '否',
      },
      rules: z.boolean().default(false),
    },
    {
      fieldName: 'outputItemIds',
      label: '产出产品',
      component: markRaw(MdItemSelect),
      componentProps: {
        multiple: true,
        productIds,
        placeholder: '中间工序选半成品,末道工序选全部成品',
      },
      formItemClass: 'col-span-2',
    },
    {
      fieldName: 'prepareTime',
@@ -323,6 +338,7 @@
    },
    { field: 'prepareTime', title: '准备时间(分)', width: 110 },
    { field: 'waitTime', title: '等待时间(分)', width: 110 },
    { field: 'outputItemNames', title: '产出产品', minWidth: 180 },
    {
      field: 'colorCode',
      title: '甘特图颜色',
@@ -347,6 +363,9 @@
    { 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: '生产用时',
@@ -384,6 +403,7 @@
/** 工艺路线产品表单 */
export function useRouteProductFormSchema(
  onItemChange?: (item: any) => void,
  getFormApi?: () => VbenFormApi,
): VbenFormSchema[] {
  return [
    {
@@ -419,6 +439,63 @@
      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',
src/views/mes/pro/route/modules/process-form.vue
@@ -15,6 +15,7 @@
  getRouteProcess,
  updateRouteProcess,
} from '#/api/mes/pro/route/process';
import { getRouteProductListByRoute } from '#/api/mes/pro/route/product';
import { $t } from '#/locales';
import { useRouteProcessFormSchema } from '../data';
@@ -40,13 +41,17 @@
});
/** 加载工序选项后再生成 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({
@@ -76,15 +81,6 @@
      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;
@@ -93,6 +89,15 @@
    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({
src/views/mes/pro/route/modules/product-form.vue
@@ -46,7 +46,7 @@
    labelWidth: 100,
  },
  layout: 'horizontal',
  schema: useRouteProductFormSchema(handleItemChange),
  schema: useRouteProductFormSchema(handleItemChange, () => formApi),
  showDefaultActions: false,
  wrapperClass: 'grid-cols-2',
});
src/views/mes/pro/workorder/modules/form.vue
@@ -23,14 +23,9 @@
  } 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";
@@ -54,13 +49,6 @@
    () =>
      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;
@@ -363,13 +351,6 @@
    </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"
src/views/mes/qc/ipqc/data.ts
@@ -342,6 +342,7 @@
        options: getDictOptions(DICT_TYPE.MES_QC_CHECK_RESULT, 'number'),
        placeholder: '请选择检测结果',
      },
      rules: 'required',
    },
    {
      fieldName: 'remark',
src/views/mes/qc/ipqc/modules/form.vue
@@ -13,11 +13,8 @@
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';
@@ -245,12 +242,6 @@
        <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>
src/views/mes/qc/iqc/data.ts
@@ -254,6 +254,7 @@
        options: getDictOptions(DICT_TYPE.MES_QC_CHECK_RESULT, 'number'),
        placeholder: '请选择检测结果',
      },
      rules: 'required',
    },
    {
      fieldName: 'remark',
src/views/mes/qc/iqc/modules/form.vue
@@ -13,11 +13,8 @@
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';
@@ -246,12 +243,6 @@
        <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>
src/views/mes/qc/oqc/data.ts
@@ -279,6 +279,7 @@
        options: getDictOptions(DICT_TYPE.MES_QC_CHECK_RESULT, 'number'),
        placeholder: '请选择检测结果',
      },
      rules: 'required',
    },
    {
      fieldName: 'remark',
src/views/mes/qc/oqc/modules/form.vue
@@ -13,11 +13,8 @@
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';
@@ -249,12 +246,6 @@
        <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>
src/views/mes/qc/rqc/data.ts
@@ -246,6 +246,7 @@
        options: getDictOptions(DICT_TYPE.MES_QC_CHECK_RESULT, 'number'),
        placeholder: '请选择检测结果',
      },
      rules: 'required',
    },
    {
      fieldName: 'remark',
src/views/mes/qc/rqc/modules/form.vue
@@ -13,11 +13,8 @@
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';
@@ -245,12 +242,6 @@
        <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>
src/views/system/user/data.ts
@@ -34,6 +34,15 @@
      rules: 'required',
    },
    {
      fieldName: 'nickname',
      label: '用户昵称',
      component: 'Input',
      componentProps: {
        placeholder: '请输入用户昵称',
        allowClear: true,
      },
    },
    {
      label: '用户密码',
      fieldName: 'password',
      component: 'InputPassword',