gaoluyang
2026-08-12 c64bb94c7e1758f47e8c946e4656a71bb5fd53e8
feat(system): 新增部门管理和岗位管理的 Excel 导入功能

- 岗位管理 (Post):
- 新增 `import-form.vue` 导入弹窗组件,包含文件上传及是否覆盖选项。
- 在 `post/data.ts` 补充导入表单相关的 Schema 定义。
- 完善 `post/index.ts` API 接口,增加获取导入模板和上传数据的方法。
- 列表页 `index.vue` 增加导入按钮并接入导入组件。

- 部门管理 (Dept):
- 新增 `import-form.vue` 导入弹窗组件。
- 增加细粒度权限控制,导入按钮配置 `auth: ['system:dept:import']` 权限标识。
- 优化弹窗 UI 布局,添加了关于“上级部门填写全路径,为空即为顶级部门”的下置提示文案,并修复了表单标签与上传按钮的垂直对齐问题。
- 增强异常处理逻辑,针对后端返回的 `failureDeptNames` (部分导入失败数据),引入 `notification.warning` 实现导入结果的详细报错展示。
- 完善 `dept/data.ts` 补充导入 Schema,并在 `dept/index.ts` 补齐导入相关 API。
已修改14个文件
已添加2个文件
488 ■■■■ 文件已修改
src/api/system/dept/index.ts 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/post/index.ts 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/md/item/index.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/md/item/modules/form.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/md/item/modules/product-media-list.vue 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/md/workstation/index.vue 42 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/md/workstation/modules/form.vue 34 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/md/workstation/workshop/modules/form.vue 32 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/workorder/index.vue 35 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/workorder/modules/form.vue 33 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/dept/data.ts 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/dept/index.vue 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/dept/modules/import-form.vue 104 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/post/data.ts 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/post/index.vue 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/post/modules/import-form.vue 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/dept/index.ts
@@ -50,3 +50,16 @@
export async function deleteDeptList(ids: number[]) {
  return requestClient.delete(`/system/dept/delete-list?ids=${ids.join(',')}`);
}
/** ä¸‹è½½éƒ¨é—¨å¯¼å…¥æ¨¡æ¿ */
export function importDeptTemplate() {
  return requestClient.download('/system/dept/get-import-template');
}
/** å¯¼å…¥éƒ¨é—¨ */
export function importDept(file: File, updateSupport: boolean) {
  return requestClient.upload('/system/dept/import', {
    file,
    updateSupport,
  });
}
src/api/system/post/index.ts
@@ -61,3 +61,16 @@
    params,
  });
}
/** ä¸‹è½½å²—位导入模板 */
export function importPostTemplate() {
  return requestClient.download('/system/post/get-import-template');
}
/** å¯¼å…¥å²—位 */
export function importPost(file: File, updateSupport: boolean) {
  return requestClient.upload('/system/post/import', {
    file,
    updateSupport,
  });
}
src/views/mes/md/item/index.vue
@@ -25,7 +25,7 @@
  } from "#/api/mes/md/item";
  import { $t } from "#/locales";
  import { MdItemTypeTree } from "#/views/mes/md/item/type/components";
  import { PrinterLabel } from "#/views/wls/barcode/components";
  // import { PrinterLabel } from "#/views/wls/barcode/components";
  import { useGridColumns, useGridFormSchema } from "./data";
  import Form from "./modules/form.vue";
@@ -203,9 +203,9 @@
                    },
                  },
                ]" />
              <PrinterLabel :biz-id="row.id"
              <!-- <PrinterLabel :biz-id="row.id"
                            :biz-code="row.code"
                            biz-type="ITEM" />
                            biz-type="ITEM" /> -->
            </div>
          </template>
        </Grid>
src/views/mes/md/item/modules/form.vue
@@ -141,10 +141,10 @@
      <Tabs.TabPane key="substitute" tab="替代品">
        <Empty description="替代品(待实现)" />
      </Tabs.TabPane>
      <Tabs.TabPane key="sip" tab="SIP">
      <Tabs.TabPane key="sip" tab="检验指导书(SIP)">
        <ProductSipForm :form-type="formType" :item-id="formData.id" />
      </Tabs.TabPane>
      <Tabs.TabPane key="sop" tab="SOP">
      <Tabs.TabPane key="sop" tab="作业指导书(SOP)">
        <ProductSopForm :form-type="formType" :item-id="formData.id" />
      </Tabs.TabPane>
    </Tabs>
src/views/mes/md/item/modules/product-media-list.vue
@@ -48,7 +48,9 @@
);
const isReadOnly = computed(() => props.formType === 'detail'); // æ˜¯å¦åªè¯»
const title = computed(() => props.kind); // å½“前媒体类型标题
const title = computed(() =>
  props.kind === 'SIP' ? '检验指导书(SIP)' : '作业指导书(SOP)',
);
/** å°†å®Œæ•´ URL è½¬ä¸ºç›¸å¯¹è·¯å¾„,确保走前端代理 */
function resolveImageUrl(url?: string): string {
src/views/mes/md/workstation/index.vue
@@ -5,7 +5,7 @@
import { ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
import { BarcodeBizTypeEnum } from '@vben/constants';
// import { BarcodeBizTypeEnum } from '@vben/constants';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message } from 'ant-design-vue';
@@ -17,12 +17,12 @@
  getWorkstationPage,
} from '#/api/mes/md/workstation';
import { $t } from '#/locales';
import { BarcodeDetail } from '#/views/wls/barcode/components';
// import { BarcodeDetail } from '#/views/wls/barcode/components';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const barcodeDetailRef = ref<InstanceType<typeof BarcodeDetail>>(); // æ¡ç è¯¦æƒ…弹窗
// const barcodeDetailRef = ref<InstanceType<typeof BarcodeDetail>>(); // æ¡ç è¯¦æƒ…弹窗
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
@@ -65,17 +65,17 @@
}
/** æŸ¥çœ‹å·¥ä½œç«™æ¡ç  */
function handleBarcode(row: MesMdWorkstationApi.Workstation) {
  if (!row.id) {
    return;
  }
  barcodeDetailRef.value?.openByBusiness(
    row.id,
    BarcodeBizTypeEnum.WORKSTATION,
    row.code,
    row.name,
  );
}
// function handleBarcode(row: MesMdWorkstationApi.Workstation) {
//   if (!row.id) {
//     return;
//   }
//   barcodeDetailRef.value?.openByBusiness(
//     row.id,
//     BarcodeBizTypeEnum.WORKSTATION,
//     row.code,
//     row.name,
//   );
// }
/** å¯¼å‡ºå·¥ä½œç«™ */
async function handleExport() {
@@ -116,7 +116,7 @@
<template>
  <Page auto-content-height><FormModal @success="handleRefresh" />
    <BarcodeDetail ref="barcodeDetailRef" />
    <!-- <BarcodeDetail ref="barcodeDetailRef" /> -->
    <Grid table-title="工作站列表">
      <template #toolbar-tools>
@@ -165,12 +165,12 @@
                confirm: handleDelete.bind(null, row),
              },
            },
            {
              label: '条码',
              type: 'link',
              auth: ['mes:md-workstation:query'],
              onClick: handleBarcode.bind(null, row),
            },
            // {
            //   label: '条码',
            //   type: 'link',
            //   auth: ['mes:md-workstation:query'],
            //   onClick: handleBarcode.bind(null, row),
            // },
          ]"
        />
      </template>
src/views/mes/md/workstation/modules/form.vue
@@ -6,7 +6,7 @@
  import { computed, ref } from "vue";
  import { useVbenModal } from "@vben/common-ui";
  import { BarcodeBizTypeEnum } from "@vben/constants";
  // import { BarcodeBizTypeEnum } from "@vben/constants";
  import { Button, message, Tabs } from "ant-design-vue";
@@ -17,7 +17,7 @@
    updateWorkstation,
  } from "#/api/mes/md/workstation";
  import { $t } from "#/locales";
  import { BarcodeDetail } from "#/views/wls/barcode/components";
  // import { BarcodeDetail } from "#/views/wls/barcode/components";
  import { useFormSchema } from "../data";
  import MachineList from "./machine-list.vue";
@@ -28,7 +28,7 @@
  const formType = ref<FormType>("create"); // è¡¨å•模式
  const subTabsName = ref("machine"); // å½“前资源页签
  const formData = ref<MesMdWorkstationApi.Workstation>();
  const barcodeDetailRef = ref<InstanceType<typeof BarcodeDetail>>(); // æ¡ç è¯¦æƒ…弹窗
  // const barcodeDetailRef = ref<InstanceType<typeof BarcodeDetail>>(); // æ¡ç è¯¦æƒ…弹窗
  const isDetail = computed(() => formType.value === "detail"); // æ˜¯å¦æŸ¥çœ‹æ¨¡å¼
  const getTitle = computed(() => {
@@ -53,17 +53,17 @@
  });
  /** æŸ¥çœ‹å·¥ä½œç«™æ¡ç  */
  function handleBarcode() {
    if (!formData.value?.id) {
      return;
    }
    barcodeDetailRef.value?.openByBusiness(
      formData.value.id,
      BarcodeBizTypeEnum.WORKSTATION,
      formData.value.code,
      formData.value.name
    );
  }
  // function handleBarcode() {
  //   if (!formData.value?.id) {
  //     return;
  //   }
  //   barcodeDetailRef.value?.openByBusiness(
  //     formData.value.id,
  //     BarcodeBizTypeEnum.WORKSTATION,
  //     formData.value.code,
  //     formData.value.name
  //   );
  // }
  const [Modal, modalApi] = useVbenModal({
    async onConfirm() {
@@ -146,13 +146,13 @@
    </Tabs>
    <template #prepend-footer>
      <div class="flex flex-auto items-center">
        <Button v-if="isDetail && formData?.id"
        <!-- <Button v-if="isDetail && formData?.id"
                type="primary"
                @click="handleBarcode">
          æŸ¥çœ‹æ¡ç 
        </Button>
        </Button> -->
      </div>
    </template>
    <BarcodeDetail ref="barcodeDetailRef" />
    <!-- <BarcodeDetail ref="barcodeDetailRef" /> -->
  </Modal>
</template>
src/views/mes/md/workstation/workshop/modules/form.vue
@@ -6,7 +6,7 @@
  import { computed, ref } from "vue";
  import { useVbenModal } from "@vben/common-ui";
  import { BarcodeBizTypeEnum } from "@vben/constants";
  // import { BarcodeBizTypeEnum } from "@vben/constants";
  import { Button, message } from "ant-design-vue";
@@ -17,14 +17,14 @@
    updateWorkshop,
  } from "#/api/mes/md/workstation/workshop";
  import { $t } from "#/locales";
  import { BarcodeDetail } from "#/views/wls/barcode/components";
  // import { BarcodeDetail } from "#/views/wls/barcode/components";
  import { useFormSchema } from "../data";
  const emit = defineEmits(["success"]);
  const formType = ref<FormType>("create"); // è¡¨å•模式
  const formData = ref<MesMdWorkshopApi.Workshop>();
  const barcodeDetailRef = ref<InstanceType<typeof BarcodeDetail>>(); // æ¡ç è¯¦æƒ…弹窗
  // const barcodeDetailRef = ref<InstanceType<typeof BarcodeDetail>>(); // æ¡ç è¯¦æƒ…弹窗
  const isDetail = computed(() => formType.value === "detail"); // æ˜¯å¦æŸ¥çœ‹æ¨¡å¼
  const getTitle = computed(() => {
@@ -49,17 +49,17 @@
  });
  /** æŸ¥çœ‹è½¦é—´æ¡ç  */
  function handleBarcode() {
    if (!formData.value?.id) {
      return;
    }
    barcodeDetailRef.value?.openByBusiness(
      formData.value.id,
      BarcodeBizTypeEnum.WORKSHOP,
      formData.value.code,
      formData.value.name
    );
  }
  // function handleBarcode() {
  //   if (!formData.value?.id) {
  //     return;
  //   }
  //   barcodeDetailRef.value?.openByBusiness(
  //     formData.value.id,
  //     BarcodeBizTypeEnum.WORKSHOP,
  //     formData.value.code,
  //     formData.value.name
  //   );
  // }
  const [Modal, modalApi] = useVbenModal({
    async onConfirm() {
@@ -117,9 +117,9 @@
    <template v-if="isDetail && formData?.id"
              #prepend-footer>
      <div class="flex flex-auto items-center">
        <Button @click="handleBarcode">查看条码</Button>
        <!-- <Button @click="handleBarcode">查看条码</Button> -->
      </div>
    </template>
  </Modal>
  <BarcodeDetail ref="barcodeDetailRef" />
  <!-- <BarcodeDetail ref="barcodeDetailRef" /> -->
</template>
src/views/mes/pro/workorder/index.vue
@@ -6,7 +6,6 @@
  import { Page, useVbenModal } from "@vben/common-ui";
  import {
    BarcodeBizTypeEnum,
    MesProWorkOrderStatusEnum,
    MesProWorkOrderTypeEnum,
  } from "@vben/constants";
@@ -23,7 +22,7 @@
  } from "#/api/mes/pro/workorder";
  import { $t } from "#/locales";
  import ArchiveDetail from '#/views/mes/pd/archive/modules/detail.vue';
import { BarcodeDetail } from '#/views/wls/barcode/components';
// import { BarcodeDetail } from '#/views/wls/barcode/components';
  import { useGridColumns, useGridFormSchema } from "./data";
  import Form from "./modules/form.vue";
@@ -34,7 +33,7 @@
  });
  const archiveDetailRef = ref<InstanceType<typeof ArchiveDetail>>();
const barcodeDetailRef = ref<InstanceType<typeof BarcodeDetail>>(); // æ¡ç è¯¦æƒ…弹窗
// const barcodeDetailRef = ref<InstanceType<typeof BarcodeDetail>>(); // æ¡ç è¯¦æƒ…弹窗
  /** åˆ·æ–°è¡¨æ ¼ */
  function handleRefresh() {
@@ -89,14 +88,14 @@
  }
  /** æŸ¥çœ‹è®¢å•条码 */
  function handleBarcode(row: MesProWorkOrderApi.WorkOrder) {
    barcodeDetailRef.value?.openByBusiness(
      row.id!,
      BarcodeBizTypeEnum.WORKORDER,
      row.code,
      row.name
    );
  }
  // function handleBarcode(row: MesProWorkOrderApi.WorkOrder) {
  //   barcodeDetailRef.value?.openByBusiness(
  //     row.id!,
  //     BarcodeBizTypeEnum.WORKORDER,
  //     row.code,
  //     row.name
  //   );
  // }
  /** å¯¼å‡ºè¡¨æ ¼ */
  async function handleExport() {
@@ -221,12 +220,12 @@
                confirm: handleCancel.bind(null, row),
              },
            },
            {
              label: '条码',
              type: 'link',
              auth: ['mes:pro-work-order:query'],
              onClick: handleBarcode.bind(null, row),
            },
            // {
            //   label: '条码',
            //   type: 'link',
            //   auth: ['mes:pro-work-order:query'],
            //   onClick: handleBarcode.bind(null, row),
            // },
            {
              label: '归档',
              type: 'link',
@@ -235,7 +234,7 @@
          ]" />
      </template>
    </Grid>
    <BarcodeDetail ref="barcodeDetailRef" />
    <!-- <BarcodeDetail ref="barcodeDetailRef" /> -->
    <ArchiveDetail ref="archiveDetailRef" />
  </Page>
</template>
src/views/mes/pro/workorder/modules/form.vue
@@ -8,7 +8,6 @@
  import { confirm, useVbenModal } from "@vben/common-ui";
  import {
    BarcodeBizTypeEnum,
    MesProWorkOrderStatusEnum,
  } from "@vben/constants";
@@ -24,7 +23,7 @@
  } from "#/api/mes/pro/workorder";
  import { batchSaveWorkOrderProcess, getWorkOrderProcessListByWorkOrderId } from "#/api/mes/pro/workorder/process";
  import { $t } from "#/locales";
  import { BarcodeDetail } from "#/views/wls/barcode/components";
  // import { BarcodeDetail } from "#/views/wls/barcode/components";
  import { useFormSchema } from "../data";
  import PredictDelivery from "./ai/predict-delivery.vue";
@@ -39,7 +38,7 @@
  const formData = ref<MesProWorkOrderApi.WorkOrder>();
  const originalSnapshot = ref("");
  const subTabsName = ref("process");
  const barcodeDetailRef = ref<InstanceType<typeof BarcodeDetail>>();
  // const barcodeDetailRef = ref<InstanceType<typeof BarcodeDetail>>();
  const processListRef = ref<InstanceType<typeof ProcessList>>();
  const currentProductId = ref<number>(); // å½“前选中的产品 ID(用于工序加载)
  const tempProcessList = ref<MesProWorkOrderProcessApi.WorkOrderProcess[]>([]); // ä¸´æ—¶å·¥åºåˆ—表
@@ -147,17 +146,17 @@
  }
  /** æŸ¥çœ‹è®¢å•条码 */
  function handleBarcode() {
    if (!formData.value?.id) {
      return;
    }
    barcodeDetailRef.value?.openByBusiness(
      formData.value.id,
      BarcodeBizTypeEnum.WORKORDER,
      formData.value.code,
      formData.value.name
    );
  }
  // function handleBarcode() {
  //   if (!formData.value?.id) {
  //     return;
  //   }
  //   barcodeDetailRef.value?.openByBusiness(
  //     formData.value.id,
  //     BarcodeBizTypeEnum.WORKORDER,
  //     formData.value.code,
  //     formData.value.name
  //   );
  // }
  /** å·¥åºåˆ—表变化回调 */
  function handleProcessListChange(list: MesProWorkOrderProcessApi.WorkOrderProcess[]) {
@@ -384,13 +383,13 @@
                @click="handleSave">
          ä¿å­˜ä¿®æ”¹
        </Button>
        <Button v-if="formType === 'detail' && formData?.id"
        <!-- <Button v-if="formType === 'detail' && formData?.id"
                @click="handleBarcode">
          æŸ¥çœ‹æ¡ç 
        </Button>
        </Button> -->
        <Button @click="modalApi.close()">取消</Button>
      </div>
    </template>
  </Modal>
  <BarcodeDetail ref="barcodeDetailRef" />
  <!-- <BarcodeDetail ref="barcodeDetailRef" /> -->
</template>
src/views/system/dept/data.ts
@@ -114,6 +114,30 @@
  ];
}
/** éƒ¨é—¨å¯¼å…¥çš„表单 */
export function useImportFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'file',
      label: '部门数据',
      component: 'Upload',
      rules: 'required',
      help: '仅允许导入 xls、xlsx æ ¼å¼æ–‡ä»¶',
    },
    {
      fieldName: 'updateSupport',
      label: '是否覆盖',
      component: 'Switch',
      componentProps: {
        checkedChildren: '是',
        unCheckedChildren: '否',
      },
      rules: z.boolean().default(false),
      help: '是否更新已经存在的部门数据',
    },
  ];
}
/** åˆ—表的字段 */
export function useGridColumns(): VxeTableGridOptions<SystemDeptApi.Dept>['columns'] {
  return [
src/views/system/dept/index.vue
@@ -15,9 +15,15 @@
import { useGridColumns } from './data';
import Form from './modules/form.vue';
import ImportForm from './modules/import-form.vue';
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
const [ImportModal, importModalApi] = useVbenModal({
  connectedComponent: ImportForm,
  destroyOnClose: true,
});
@@ -36,6 +42,11 @@
/** åˆ›å»ºéƒ¨é—¨ */
function handleCreate() {
  formModalApi.setData(null).open();
}
/** å¯¼å…¥éƒ¨é—¨ */
function handleImport() {
  importModalApi.open();
}
/** æ·»åŠ ä¸‹çº§éƒ¨é—¨ */
@@ -129,6 +140,7 @@
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <ImportModal @success="handleRefresh" />
    <Grid table-title="部门列表">
      <template #toolbar-tools>
        <TableAction
@@ -141,6 +153,13 @@
              onClick: handleCreate,
            },
            {
              label: $t('ui.actionTitle.import', ['部门']),
              type: 'primary',
              icon: ACTION_ICON.UPLOAD,
              auth: ['system:dept:import'],
              onClick: handleImport,
            },
            {
              label: isExpanded ? '收缩' : '展开',
              type: 'primary',
              onClick: handleExpand,
src/views/system/dept/modules/import-form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,104 @@
<script lang="ts" setup>
import type { FileType } from 'ant-design-vue/es/upload/interface';
import { h } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message, notification, Upload } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { importDept, importDeptTemplate } from '#/api/system/dept';
import { $t } from '#/locales';
import { useImportFormSchema } from '../data';
const emit = defineEmits(['success']);
const [Form, formApi] = useVbenForm({
  commonConfig: {
    formItemClass: 'col-span-2',
    labelWidth: 120,
  },
  layout: 'horizontal',
  schema: useImportFormSchema(),
  showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    modalApi.lock();
    // æäº¤è¡¨å•
    const data = await formApi.getValues();
    try {
      const response = (await importDept(data.file, data.updateSupport)) as any;
      // å…³é—­å¹¶æç¤º
      await modalApi.close();
      emit('success');
      const resultData = response?.data || response || {};
      const failureNames = resultData.failureDeptNames || {};
      if (Object.keys(failureNames).length > 0) {
        const errorNodes = Object.entries(failureNames).map(([name, reason]) => {
          return h('div', `${name}: ${reason}`);
        });
        notification.warning({
          message: '导入完成,部分部门导入失败',
          description: () => h('div', errorNodes),
          duration: null,
        });
      } else {
        message.success($t('ui.actionMessage.operationSuccess'));
      }
    } finally {
      modalApi.unlock();
    }
  },
});
/** ä¸Šä¼ å‰ */
function beforeUpload(file: FileType) {
  formApi.setFieldValue('file', file);
  return false;
}
/** ä¸‹è½½æ¨¡ç‰ˆ */
async function handleDownload() {
  const data = await importDeptTemplate();
  downloadFileFromBlobPart({ fileName: '部门导入模板.xls', source: data });
}
</script>
<template>
  <Modal title="导入部门" class="w-1/3">
    <Form class="mx-4">
      <template #file>
        <div class="w-full">
          <Upload
            :max-count="1"
            accept=".xls,.xlsx"
            :before-upload="beforeUpload"
          >
            <Button type="primary"> é€‰æ‹© Excel æ–‡ä»¶ </Button>
          </Upload>
        </div>
      </template>
    </Form>
    <div class="mx-4 mt-2 mb-4 text-gray-500 text-xs">
      æç¤ºï¼šæ¨¡æ¿é‡Œçš„“上级部门”请填写全路径(如:总公司/北京分公司)
      <div>上级部门空着即为顶级部门。</div>
    </div>
    <template #prepend-footer>
      <div class="flex flex-auto items-center">
        <Button @click="handleDownload"> ä¸‹è½½å¯¼å…¥æ¨¡æ¿ </Button>
      </div>
    </template>
  </Modal>
</template>
src/views/system/post/data.ts
@@ -68,6 +68,30 @@
  ];
}
/** å²—位导入的表单 */
export function useImportFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'file',
      label: '岗位数据',
      component: 'Upload',
      rules: 'required',
      help: '仅允许导入 xls、xlsx æ ¼å¼æ–‡ä»¶',
    },
    {
      fieldName: 'updateSupport',
      label: '是否覆盖',
      component: 'Switch',
      componentProps: {
        checkedChildren: '是',
        unCheckedChildren: '否',
      },
      rules: z.boolean().default(false),
      help: '是否更新已经存在的岗位数据',
    },
  ];
}
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
src/views/system/post/index.vue
@@ -20,9 +20,15 @@
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
import ImportForm from './modules/import-form.vue';
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
const [ImportModal, importModalApi] = useVbenModal({
  connectedComponent: ImportForm,
  destroyOnClose: true,
});
@@ -40,6 +46,11 @@
/** åˆ›å»ºå²—位 */
function handleCreate() {
  formModalApi.setData(null).open();
}
/** å¯¼å…¥å²—位 */
function handleImport() {
  importModalApi.open();
}
/** ç¼–辑岗位 */
@@ -126,6 +137,7 @@
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <ImportModal @success="handleRefresh" />
    <Grid table-title="岗位列表">
      <template #toolbar-tools>
        <TableAction
@@ -145,6 +157,13 @@
              onClick: handleExport,
            },
            {
              label: $t('ui.actionTitle.import'),
              type: 'primary',
              icon: ACTION_ICON.UPLOAD,
              auth: ['system:post:import'],
              onClick: handleImport,
            },
            {
              label: $t('ui.actionTitle.deleteBatch'),
              type: 'primary',
              danger: true,
src/views/system/post/modules/import-form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,82 @@
<script lang="ts" setup>
import type { FileType } from 'ant-design-vue/es/upload/interface';
import { useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message, Upload } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { importPost, importPostTemplate } from '#/api/system/post';
import { $t } from '#/locales';
import { useImportFormSchema } from '../data';
const emit = defineEmits(['success']);
const [Form, formApi] = useVbenForm({
  commonConfig: {
    formItemClass: 'col-span-2',
    labelWidth: 120,
  },
  layout: 'horizontal',
  schema: useImportFormSchema(),
  showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    modalApi.lock();
    // æäº¤è¡¨å•
    const data = await formApi.getValues();
    try {
      await importPost(data.file, data.updateSupport);
      // å…³é—­å¹¶æç¤º
      await modalApi.close();
      emit('success');
      message.success($t('ui.actionMessage.operationSuccess'));
    } finally {
      modalApi.unlock();
    }
  },
});
/** ä¸Šä¼ å‰ */
function beforeUpload(file: FileType) {
  formApi.setFieldValue('file', file);
  return false;
}
/** ä¸‹è½½æ¨¡ç‰ˆ */
async function handleDownload() {
  const data = await importPostTemplate();
  downloadFileFromBlobPart({ fileName: '岗位导入模板.xls', source: data });
}
</script>
<template>
  <Modal title="导入岗位" class="w-1/3">
    <Form class="mx-4">
      <template #file>
        <div class="w-full">
          <Upload
            :max-count="1"
            accept=".xls,.xlsx"
            :before-upload="beforeUpload"
          >
            <Button type="primary"> é€‰æ‹© Excel æ–‡ä»¶ </Button>
          </Upload>
        </div>
      </template>
    </Form>
    <template #prepend-footer>
      <div class="flex flex-auto items-center">
        <Button @click="handleDownload"> ä¸‹è½½å¯¼å…¥æ¨¡æ¿ </Button>
      </div>
    </template>
  </Modal>
</template>