ZJQ批量创按钮和页作批量导入按钮和页面根据后端文档进行对接接口已测试成功,
12个文件已修改
3个文件已添加
1856 ■■■■■ 已修改文件
.env.production 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
build/vite/proxy.ts 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/routes/modules/contract.ts 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/encryption/signMd5Utils.js 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/http/axios/Axios.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/http/axios/index.ts 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/contract/BatchCreate.vue 432 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/contract/BatchImport.vue 745 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/contract/ContractList.vue 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/copywriting/generated/index.vue 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/demo/page/form/stephetong/Step2.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/demo/page/form/stephetong/Step3.vue 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/demo/page/form/stepxieyi/Step2.vue 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/demo/page/form/stepxieyi/Step3.vue 456 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/semanticwordPD/SemanticWordList.vue 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.env.production
@@ -17,7 +17,7 @@
#后台接口全路径地址(必填)
# VITE_GLOB_DOMAIN_URL=http://127.0.0.1:8080/jeecg-boot
VITE_GLOB_DOMAIN_URL=http://8.145.61.64:8080/jeecg-boot
VITE_GLOB_DOMAIN_URL=http://192.168.31.222:8080/jeecg-boot
# 接口父路径前缀
build/vite/proxy.ts
@@ -28,6 +28,13 @@
      rewrite: (path) => path.replace(new RegExp(`^${prefix}`), ''),
      // https is require secure=false
      ...(isHttps ? { secure: false } : {}),
      // 确保 multipart/form-data 请求的 Content-Type 不会被修改
      onProxyReq: (proxyReq: any, req: any, res: any) => {
        // 如果是 multipart/form-data 请求,保留原始的 Content-Type
        if (req.headers['content-type'] && req.headers['content-type'].includes('multipart/form-data')) {
          proxyReq.setHeader('Content-Type', req.headers['content-type']);
        }
      }
    };
  }
  return ret;
src/router/routes/modules/contract.ts
New file
@@ -0,0 +1,47 @@
import type { AppRouteModule } from '/@/router/types';
import { LAYOUT } from '/@/router/constant';
const contract: AppRouteModule = {
  path: '/contract',
  name: 'Contract',
  component: LAYOUT,
  redirect: '/contract/ContractList',
  meta: {
    orderNo: 10,
    icon: 'ion:document-text-outline',
    title: '合同管理',
  },
  children: [
    {
      path: 'ContractList',
      name: 'ContractList',
      component: () => import('/@/views/contract/ContractList.vue'),
      meta: {
        title: '合同录入',
      },
    },
    {
      path: 'batchCreate',
      name: 'BatchCreate',
      component: () => import('/@/views/contract/BatchCreate.vue'),
      meta: {
        title: '批量创作',
        hideMenu: true,
        currentActiveMenu: '/page-demo/form/stepxieyi',
      },
    },
    {
      path: 'batchImport',
      name: 'BatchImport',
      component: () => import('/@/views/contract/BatchImport.vue'),
      meta: {
        title: '批量导入',
        hideMenu: true,
        currentActiveMenu: '/page-demo/form/stepxieyi',
      },
    },
  ],
};
export default contract;
src/utils/encryption/signMd5Utils.js
@@ -33,7 +33,8 @@
    let urlParams = this.parseQueryString(url);
    let jsonObj = this.mergeObject(urlParams, requestParams);
    //update-begin---author:wangshuai---date:2024-04-16---for:【QQYUN-9005】发送短信加签---
    if (requestBodyParams) {
    // 当requestBodyParams是FormData对象时,跳过合并操作,因为FormData不能直接遍历和序列化
    if (requestBodyParams && !(requestBodyParams instanceof FormData)) {
      jsonObj = this.mergeObject(jsonObj, requestBodyParams);
    }
    //update-end---author:wangshuai---date:2024-04-16---for:【QQYUN-9005】发送短信加签---
src/utils/http/axios/Axios.ts
@@ -201,7 +201,11 @@
  }
  request<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
    // 当data是FormData对象时,不进行克隆操作,直接传递原始的data对象
    let conf: CreateAxiosOptions = cloneDeep(config);
    if (config.data instanceof FormData) {
      conf.data = config.data;
    }
    const transform = this.getTransform();
    const { requestOptions } = this.options;
src/utils/http/axios/index.ts
@@ -116,7 +116,20 @@
    
    const params = config.params || {};
    const data = config.data || false;
    formatDate && data && !isString(data) && formatRequestDate(data);
    // 当data是FormData对象时,不调用formatRequestDate,避免破坏FormData结构
    if (formatDate && data && !isString(data) && !(data instanceof FormData)) {
      formatRequestDate(data);
    }
    // 当data是FormData对象时,设置正确的Content-Type
    if (data instanceof FormData) {
      // 确保config.headers是一个对象
      if (!config.headers) {
        config.headers = {};
      }
      // 移除默认的Content-Type,让浏览器自动生成带有边界的Content-Type
      delete config.headers['Content-Type'];
    }
    if (config.method?.toUpperCase() === RequestEnum.GET) {
      if (!isString(params)) {
        // 给 get 请求加上时间戳参数,避免从缓存中拿数据。
@@ -170,7 +183,10 @@
    // 将签名和时间戳,添加在请求接口 Header
    config.headers[ConfigEnum.TIMESTAMP] = signMd5Utils.getTimestamp();
    //update-begin---author:wangshuai---date:2024-04-25---for: 生成签名的时候复制一份,避免影响原来的参数---
    config.headers[ConfigEnum.Sign] = signMd5Utils.getSign(config.url, cloneDeep(config.params), cloneDeep(config.data));
    // 当data是FormData对象时,不进行克隆操作,直接传递原始的data对象
    const clonedParams = cloneDeep(config.params);
    const clonedData = config.data instanceof FormData ? config.data : cloneDeep(config.data);
    config.headers[ConfigEnum.Sign] = signMd5Utils.getSign(config.url, clonedParams, clonedData);
    //update-end---author:wangshuai---date:2024-04-25---for: 生成签名的时候复制一份,避免影响原来的参数---
    //update-end---author:wangshuai---date:2024-04-16---for:【QQYUN-9005】发送短信加签。解决没有token无法加签---
    // update-begin--author:liaozhiyang---date:20240509---for:【issues/1220】登录时,vue3版本不加载字典数据设置无效
src/views/contract/BatchCreate.vue
New file
@@ -0,0 +1,432 @@
<template>
  <div>
    <!-- 批量创作表单 -->
    <div class="form-container">
      <a-form
        ref="formRef"
        :model="formState"
        :rules="rules"
        layout="vertical"
      >
        <!-- 合同信息 -->
        <a-form-item
          label="合同信息"
          name="contractId"
          :required="true"
        >
          <a-select v-model:value="formState.contractId" placeholder="请选择合同" style="width: 300px;">
            <a-select-option v-for="contract in contractList" :key="contract.value" :value="contract.value">
              {{ contract.label }}
            </a-select-option>
          </a-select>
        </a-form-item>
        <!-- 原始文件名 -->
        <a-form-item
          label="原始文件名"
          name="fileName"
          :required="true"
        >
          <a-input v-model:value="formState.fileName" placeholder="请输入原始文件名" style="width: 300px;" />
        </a-form-item>
        <!-- 优势描述/合同背景 -->
        <a-form-item
          label="优势描述"
          name="youshang"
          :required="true"
        >
          <a-textarea v-model:value="formState.youshang" placeholder="请输入优势描述或合同背景" style="width: 300px;" rows="3" />
        </a-form-item>
        <!-- 问题描述/需修改内容 -->
        <a-form-item
          label="问题描述"
          name="wenti"
          :required="true"
        >
          <a-textarea v-model:value="formState.wenti" placeholder="请输入问题描述或需修改内容" style="width: 300px;" rows="3" />
        </a-form-item>
        <!-- 文案修改要求 -->
        <a-form-item
          label="修改要求"
          name="copywritingRequirements"
          :required="true"
        >
          <a-textarea v-model:value="formState.copywritingRequirements" placeholder="请输入文案修改要求" style="width: 300px;" rows="3" />
        </a-form-item>
        <!-- 审核人ID -->
        <a-form-item
          label="审核人ID"
          name="auditor"
          :required="true"
        >
          <a-input v-model:value="formState.auditor" placeholder="请输入审核人ID" style="width: 300px;" />
        </a-form-item>
        <!-- 审核人姓名 -->
        <a-form-item
          label="审核人姓名"
          name="auditorName"
          :required="true"
        >
          <a-input v-model:value="formState.auditorName" placeholder="请输入审核人姓名" style="width: 300px;" />
        </a-form-item>
        <!-- 操作按钮 -->
        <a-form-item>
          <div class="form-actions">
            <a-button type="primary" @click="handleBatchGenerate" :loading="submitLoading" style="width: 120px; height: 40px; font-size: 16px;">
              批量创作
            </a-button>
          </div>
        </a-form-item>
      </a-form>
    </div>
    <!-- 生成结果 -->
    <div class="result-container" v-show="showResult">
      <a-divider orientation="left">生成结果</a-divider>
      <div v-if="createResults.length > 0">
        <a-table :columns="resultColumns" :data-source="createResults" :pagination="false">
          <template #action="{ record }">
            <a-button size="small" style="margin-right: 8px">查看</a-button>
            <a-button size="small" style="margin-right: 8px">编辑</a-button>
            <a-button size="small" type="primary">发布</a-button>
          </template>
        </a-table>
      </div>
      <a-empty v-else description="暂无生成结果" />
    </div>
  </div>
</template>
<script lang="ts" setup>
  import { ref, reactive, onMounted } from 'vue';
  import { useMessage } from '/@/hooks/web/useMessage';
  import { router } from '/@/router';
  import { defHttp } from '/@/utils/http/axios';
  import { getToken } from '/@/utils/auth';
  import { BasicTable } from '/@/components/Table';
  import { useListPage } from '/@/hooks/system/useListPage';
  import { columns, searchFormSchema } from './Contract.data';
  import { list, listByRole, deleteOne, batchDelete, getImportUrl, getExportUrl } from './Contract.api';
  // 列表页相关
  const { registerTable, selectedRowKeys } = useListPage({
    tableProps: {
      columns: columns,
      bordered: true,
      size: 'default',
    },
    searchParams: {},
    getListApi: list,
  });
  // 表单状态
  const formRef = ref();
  const submitLoading = ref(false);
  const showResult = ref(false);
  // 合同列表
  const contractList = ref([]);
  const contractListCache = ref([]);
  // 表单数据
  const formState = reactive({
    contractId: '',
    fileName: '',
    youshang: '',
    wenti: '',
    copywritingRequirements: '',
    auditor: '',
    auditorName: ''
  });
  // 表单验证规则
  const rules = {
    contractId: [{ required: true, message: '请选择合同', trigger: 'change' }],
    fileName: [{ required: true, message: '请输入原始文件名', trigger: 'blur' }],
    youshang: [{ required: true, message: '请输入优势描述', trigger: 'blur' }],
    wenti: [{ required: true, message: '请输入问题描述', trigger: 'blur' }],
    copywritingRequirements: [{ required: true, message: '请输入修改要求', trigger: 'blur' }],
    auditor: [{ required: true, message: '请输入审核人ID', trigger: 'blur' }],
    auditorName: [{ required: true, message: '请输入审核人姓名', trigger: 'blur' }]
  };
  // 创作结果
  const createResults = ref([]);
  // 结果表格列定义
  const resultColumns = [
    {
      title: '序号',
      dataIndex: 'index',
      key: 'index',
      width: 80
    },
    {
      title: '标题',
      dataIndex: 'title',
      key: 'title'
    },
    {
      title: '类型',
      dataIndex: 'type',
      key: 'type',
      width: 120
    },
    {
      title: '状态',
      dataIndex: 'status',
      key: 'status',
      width: 100,
      customRender: (text) => {
        const statusMap = {
          pending: '待处理',
          success: '成功',
          failed: '失败'
        };
        return statusMap[text] || text;
      }
    },
    {
      title: '操作',
      key: 'action',
      width: 200,
      scopedSlots: {
        customRender: 'action'
      }
    }
  ];
  const { createMessage } = useMessage();
  // 加载合同列表
  const loadContractList = async () => {
    try {
      console.log('开始加载合同列表...');
      const response = await defHttp.get({
        url: '/contract/contract/list',
        params: {
          column: 'createTime',
          order: 'desc',
          pageNo: 1,
          pageSize: 200,
        },
      });
      console.log('合同列表接口响应:', response);
      const records = normalizeContractRecords(response);
      console.log('标准化后的合同记录:', records);
      contractListCache.value = records;
      const options = records.map((item: any) => ({
        label: resolveContractLabel(item),
        value: resolveContractValue(item),
      }));
      console.log('生成的合同选项:', options);
      contractList.value = options;
      console.log('合同列表加载完成');
    } catch (error) {
      console.error('加载合同列表失败:', error);
      createMessage.error('加载合同列表失败');
    }
  };
  // 标准化合同记录
  const normalizeContractRecords = (response: any): any[] => {
    if (Array.isArray(response)) return response;
    if (Array.isArray(response?.records)) {
      return response.records;
    }
    if (Array.isArray(response?.result?.records)) {
      return response.result.records;
    }
    if (Array.isArray(response?.data?.records)) {
      return response.data.records;
    }
    return [];
  };
  // 解析合同标签
  const resolveContractLabel = (item: any) => {
    if (item.contractName) return item.contractName;
    if (item.contractNo) return `合同编号:${item.contractNo}`;
    return '未命名合同';
  };
  // 解析合同值
  const resolveContractValue = (item: any) => {
    return item.id || item.contractId || item.contractName || '';
  };
  // 批量生成
  const handleBatchGenerate = async () => {
    if (!formRef.value) return;
    try {
      await formRef.value.validate();
      submitLoading.value = true;
      // 构建请求体
      const requestData = {
        contractId: formState.contractId,
        fileName: formState.fileName,
        youshang: formState.youshang,
        wenti: formState.wenti,
        copywritingRequirements: formState.copywritingRequirements,
        auditor: formState.auditor,
        auditorName: formState.auditorName
      };
      console.log('批量生成请求数据:', requestData);
      // 获取登录 Token
      const token = getToken();
      if (!token) {
        createMessage.error('未登录,请先登录系统');
        submitLoading.value = false;
        return;
      }
      // 使用 XMLHttpRequest 发送请求,避免拦截器干扰
      const url = 'http://192.168.31.222:8080/jeecg-boot/api/excel/batchGenerateCopy';
      console.log('批量生成请求 URL:', url);
      // 发送请求
      const responseData = await new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.onload = function() {
          if (xhr.status >= 200 && xhr.status < 300) {
            try {
              const data = JSON.parse(xhr.responseText);
              console.log('批量生成响应数据:', data);
              resolve(data);
            } catch (error) {
              console.error('解析响应失败:', error);
              reject(new Error('解析响应失败'));
            }
          } else {
            console.error('请求失败:', xhr.status, xhr.statusText);
            reject(new Error(`请求失败: ${xhr.status} ${xhr.statusText}`));
          }
        };
        xhr.onerror = function() {
          console.error('网络错误');
          reject(new Error('网络错误'));
        };
        xhr.open('POST', url, true);
        // 设置请求头
        xhr.setRequestHeader('X-Access-Token', token);
        xhr.setRequestHeader('X-Tenant-Id', '0');
        xhr.setRequestHeader('X-Version', 'v3');
        xhr.setRequestHeader('Accept', 'application/json, text/plain, */*');
        xhr.setRequestHeader('Content-Type', 'application/json');
        // 发送请求
        xhr.send(JSON.stringify(requestData));
      });
      console.log('后端响应数据:', responseData);
      if (responseData && responseData.success) {
        // 处理成功响应
        createResults.value = [
          {
            key: 1,
            index: 1,
            title: `${formState.fileName} - 生成 1`,
            type: '批量创作',
            status: 'success'
          }
        ];
        showResult.value = true;
        createMessage.success('批量创作完成');
      } else {
        // 处理失败响应
        createMessage.error(responseData?.message || responseData?.msg || '批量创作失败');
      }
      submitLoading.value = false;
    } catch (error: any) {
      console.error('批量生成失败:', error);
      // 处理不同类型的错误
      if (error.response) {
        // 服务器返回错误状态码
        if (error.response.status === 401) {
          createMessage.error('Token失效,请重新登录');
        } else {
          createMessage.error(error.response.data?.message || '批量创作失败');
        }
      } else if (error.message) {
        createMessage.error(error.message);
      } else {
        createMessage.error('批量创作失败,请重试');
      }
      submitLoading.value = false;
    }
  };
  // 重置表单
  const handleReset = () => {
    if (formRef.value) {
      formRef.value.resetFields();
    }
    showResult.value = false;
    createResults.value = [];
  };
  // 新增事件
  const handleAdd = () => {
    router.push({
      path: '/page-demo/form/stephetong',
    });
  };
  // 批量创作事件
  const handleBatchCreate = () => {
    router.push('/contract/batchCreate');
  };
  // 批量导入事件
  const handleBatchImport = () => {
    router.push('/contract/batchImport');
  };
  // 页面加载时获取合同列表
  onMounted(() => {
    loadContractList();
  });
</script>
<style lang="less" scoped>
  .form-container {
    max-width: 800px;
    margin: 0 auto;
    padding: 20px;
  }
  .form-actions {
    margin-top: 24px;
    text-align: center;
    width: 100%;
    display: flex;
    justify-content: center;
  }
  .result-container {
    max-width: 800px;
    margin: 20px auto;
    padding: 20px;
  }
</style>
src/views/contract/BatchImport.vue
New file
@@ -0,0 +1,745 @@
<template>
  <div>
    <!-- 批量导入表单 -->
    <div class="form-container">
      <!-- 合同信息 -->
      <div class="form-item">
        <span class="required-mark">*</span>
        <span class="form-label">合同信息</span>
        <a-select
          v-model:value="formState.contractId"
          placeholder="请选择合同"
          style="width: 360px;"
          show-search
          :filter-option="filterOption"
          @change="handleContractChange"
        >
          <a-select-option v-for="contract in contractList" :key="contract.value" :value="contract.value">
            {{ contract.label }}
          </a-select-option>
        </a-select>
      </div>
      <!-- 语义词文件选择 -->
      <div class="form-item">
        <span class="required-mark">*</span>
        <span class="form-label">语义词文件</span>
        <div style="display: flex; align-items: center;">
          <input type="text" readonly placeholder="" style="width: 360px; height: 32px; padding: 0 11px; border: 1px solid #d9d9d9; border-radius: 4px; margin-right: 0; border-right: none; border-top-right-radius: 0; border-bottom-right-radius: 0;" :value="formState.semanticFile || ''" />
          <a-upload
            v-model:file-list="fileList"
            :multiple="false"
            accept=".xlsx,.csv,.txt"
            :before-upload="beforeUpload"
            @change="handleFileChange"
            :auto-upload="false"
          >
            <a-button type="default" style="border-top-left-radius: 0; border-bottom-left-radius: 0;">
              选择
            </a-button>
          </a-upload>
        </div>
      </div>
      <!-- 文案编辑 -->
      <div class="form-item">
        <span class="required-mark">*</span>
        <span class="form-label">文案编辑</span>
        <a-select v-model:value="formState.editor" placeholder="请选择文案编辑" style="width: 360px;">
          <a-select-option v-for="person in personList" :key="person.value" :value="person.value">
            {{ person.label }}
          </a-select-option>
        </a-select>
      </div>
      <!-- 操作按钮 -->
      <div class="form-actions">
        <a-button type="primary" @click="handleImport" :loading="importLoading" style="width: 120px; height: 40px; font-size: 16px;">
          批量导入
        </a-button>
      </div>
    </div>
    <!-- 导入结果 -->
    <div class="result-container" v-show="showResult">
      <a-divider orientation="left">导入结果</a-divider>
      <div class="import-summary">
        <Statistic title="总记录数" :value="importSummary.total" style="margin-right: 32px" />
        <Statistic title="成功数" :value="importSummary.success" prefix=""><template #prefix><span style="color: green">✓</span></template></Statistic>
        <Statistic title="失败数" :value="importSummary.failed" prefix=""><template #prefix><span style="color: red">✗</span></template></Statistic>
        <Statistic title="处理时间" :value="importSummary.duration" suffix="秒" style="margin-left: 32px" />
      </div>
      <div v-if="importErrors.length > 0" class="import-errors">
        <a-divider orientation="left">错误信息</a-divider>
        <a-list :data-source="importErrors" :render-item="renderErrorItem" />
      </div>
    </div>
  </div>
</template>
<script lang="ts" setup>
  import { ref, reactive, h, onMounted } from 'vue';
  import { useMessage } from '/@/hooks/web/useMessage';
  import { router } from '/@/router';
  import { defHttp } from '/@/utils/http/axios';
  import { getToken } from '/@/utils/auth';
  import { BasicTable } from '/@/components/Table';
  import { useListPage } from '/@/hooks/system/useListPage';
  import { columns, searchFormSchema } from './Contract.data';
  import { list, listByRole, deleteOne, batchDelete, getImportUrl, getExportUrl } from './Contract.api';
  import { Statistic } from 'ant-design-vue';
  // 列表页相关
  const { registerTable, selectedRowKeys } = useListPage({
    tableProps: {
      columns: columns,
      bordered: true,
      size: 'default',
    },
    searchParams: {},
    getListApi: list,
  });
  // 表单状态
  const formRef = ref();
  const importLoading = ref(false);
  const showResult = ref(false);
  const showPreview = ref(false);
  // 合同列表
  const contractList = ref([]);
  const contractListCache = ref([]);
  // 人员列表
  const personList = ref([]);
  // 文件上传 - 确保只存储有效文件
  const fileList = ref([]);
  // 表单数据
  const formState = reactive({
    contractId: '',
    semanticFile: '',
    editor: ''
  });
  // 表单验证规则
  const rules = {
    contractId: [{ required: true, message: '请选择合同', trigger: 'change' }],
    semanticFile: [{ required: true, message: '请选择语义词文件', trigger: 'change' }],
    editor: [{ required: true, message: '请选择文案编辑', trigger: 'change' }]
  };
  // 导入结果
  const importSummary = reactive({
    total: 0,
    success: 0,
    failed: 0,
    duration: 0
  });
  // 错误信息
  const importErrors = ref([]);
  // 预览数据
  const previewData = ref([]);
  const previewColumns = [
    {
      title: '大类',
      dataIndex: 'category',
      key: 'category'
    },
    {
      title: '语义词',
      dataIndex: 'word',
      key: 'word'
    },
    {
      title: '露出词',
      dataIndex: 'outWord',
      key: 'outWord'
    },
    {
      title: '创作人员',
      dataIndex: 'creator',
      key: 'creator'
    }
  ];
  // 结果数据
  const successItems = ref([]);
  const resultColumns = [
    {
      title: '大类',
      dataIndex: 'category',
      key: 'category'
    },
    {
      title: '语义词',
      dataIndex: 'word',
      key: 'word'
    },
    {
      title: '露出词',
      dataIndex: 'outWord',
      key: 'outWord'
    },
    {
      title: '创作人员',
      dataIndex: 'creator',
      key: 'creator'
    },
    {
      title: '分配状态',
      dataIndex: 'assignStatus',
      key: 'assignStatus'
    }
  ];
  const { createMessage } = useMessage();
  // 归一化合同记录
  const normalizeContractRecords = (response: any) => {
    if (Array.isArray(response)) {
      return response;
    }
    if (Array.isArray(response?.records)) {
      return response.records;
    }
    if (Array.isArray(response?.result?.records)) {
      return response.result.records;
    }
    if (Array.isArray(response?.data?.records)) {
      return response.data.records;
    }
    return [];
  };
  // 解析合同标签
  const resolveContractLabel = (item: any) => {
    if (item.contractName) return item.contractName;
    if (item.contractNo) return `合同编号:${item.contractNo}`;
    return '未命名合同';
  };
  // 解析合同值
  const resolveContractValue = (item: any) => {
    return item.id || item.contractId || item.contractName || '';
  };
  // 获取合同选项
  const getContractOptions = async () => {
    try {
      const response = await defHttp.get({
        url: '/contract/contract/list',
        params: {
          column: 'createTime',
          order: 'desc',
          pageNo: 1,
          pageSize: 200,
        },
      });
      const records = normalizeContractRecords(response);
      contractListCache.value = records;
      const options = records.map((item: any) => ({
        label: resolveContractLabel(item),
        value: resolveContractValue(item),
      }));
      contractList.value = options;
      return options;
    } catch (error) {
      console.error('获取合同列表失败:', error);
      contractListCache.value = [];
      contractList.value = [];
      return [];
    }
  };
  // 根据ID获取合同详情
  const getContractDetailById = (contractId: string) => {
    return contractListCache.value.find(
      (item: any) =>
        item.id === contractId ||
        item.contractId === contractId ||
        (!!contractId && contractId === resolveContractValue(item))
    );
  };
  // 过滤选项
  const filterOption = (input: string, option: any) => {
    return (option?.label || '').toLowerCase().includes(input.toLowerCase());
  };
  // 合同选择变化
  const handleContractChange = (value: string) => {
    if (!value) return;
    const contract = getContractDetailById(value);
    if (contract) {
      localStorage.setItem('selectedContractInfo', JSON.stringify(contract));
    }
  };
  // 文件上传前校验 - 严格限制文件类型和大小
  const beforeUpload = (file) => {
    const allowedTypes = ['.xlsx', '.csv', '.txt'];
    const fileExtension = file.name.substring(file.name.lastIndexOf('.'));
    if (!allowedTypes.includes(fileExtension)) {
      createMessage.error('只支持上传 Excel、CSV、文本文件');
      return false;
    }
    if (file.size > 10 * 1024 * 1024) {
      createMessage.error('文件大小不能超过 10MB');
      return false;
    }
    // 直接返回false,阻止自动上传,只保留文件信息
    return false;
  };
  // 文件上传变化 - 只保留有效、未被删除的文件
  const handleFileChange = (info) => {
    // 严格过滤:只保留状态为 'done' 或未上传的有效文件,排除已删除的
    const newFileList = info.fileList.filter(file => file.status !== 'removed');
    fileList.value = newFileList;
    if (newFileList.length > 0) {
      formState.semanticFile = newFileList[0].name;
    } else {
      formState.semanticFile = '';
    }
    console.log('有效文件列表:', newFileList);
    console.log('当前选中文件名:', formState.semanticFile);
  };
  // 下载模板
  const downloadTemplate = () => {
    try {
      const exportUrl = `/contract/contract/exportXls`;
      const params = {
        template: true,
        fileName: '语义词导入模板'
      };
      const queryString = Object.entries(params)
        .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
        .join('&');
      const fullUrl = `${exportUrl}?${queryString}`;
      const link = document.createElement('a');
      link.href = fullUrl;
      link.download = '语义词导入模板.xlsx';
      link.style.display = 'none';
      document.body.appendChild(link);
      link.click();
      document.body.removeChild(link);
      createMessage.success('模板下载成功');
    } catch (error) {
      console.error('模板下载失败:', error);
      createMessage.error('模板下载失败,请稍后重试');
    }
  };
  // 补全完整的处理导入数据方法
  const processImportData = (data) => {
    return data.map((item, index) => {
      return {
        id: index + 1,
        category: item.category || item['大类'] || item['品牌(仅比价/省钱类)'] || '',
        word: item.word || item['语义词'] || item['仅比价/省'] || '',
        question: item.question || item['语义问法'] || '',
        outWord: item.outWord || item['露出词'] || '',
        creator: item.creator || item['创作人员'] || '',
        status: '待分配'
      };
    });
  };
  // 分配语义词人员
  const assignSemanticWordPerson = async (semanticWord) => {
    try {
      const response = await defHttp.post({
        url: '/semanticword/assignPerson',
        data: semanticWord
      });
      return response.success;
    } catch (error) {
      console.error('分配人员失败:', error);
      return false;
    }
  };
  // 终极优化:handleImport 方法(强制确保有效File对象,去掉有问题的签名)
  const handleImport = async () => {
    try {
      // 1. 前置表单验证(严格校验)
      if (!formState.contractId) {
        createMessage.error('请选择合同');
        return;
      }
      if (fileList.value.length === 0 || !fileList.value[0]?.originFileObj) {
        createMessage.error('请选择有效的语义词文件(Excel/CSV/TXT)');
        return;
      }
      if (!formState.editor) {
        createMessage.error('请选择文案编辑');
        return;
      }
      // 2. 初始化状态
      importLoading.value = true;
      importSummary.total = 0;
      importSummary.success = 0;
      importSummary.failed = 0;
      importSummary.duration = 0;
      importErrors.value = [];
      showResult.value = false;
      // 3. 强制获取 AntD Upload 原始 File 对象(核心!确保是有效File实例)
      const file = fileList.value[0];
      const fileObj = file.originFileObj; // 使用 originFileObj,这是 AntD Upload 组件的原始文件对象
      // 双重校验:确保 fileObj 是 File 实例
      if (!(fileObj instanceof File)) {
        createMessage.error('获取文件失败,请重新选择文件');
        importLoading.value = false;
        return;
      }
      console.log('有效File对象:', fileObj);
      console.log('文件名:', fileObj.name);
      console.log('文件大小:', fileObj.size);
      // 4. 构建业务参数
      const importParam = {
        contractId: formState.contractId,
        ranking: "3", // 签约排名写死为3
        status: "7", // 状态写死为7
        remark: "批量订单", // 备注信息写死为批量订单
        month: "12", // 合作月份写死为12个月
        price: "0", // 价格写死为0
        acceptindicator: "80" // 验收指标写死为80
      };
      console.log('业务参数:', importParam);
      // 5. 构建 FormData - 确保正确创建 FormData 对象
      const formData = new FormData();
      // 验证 fileObj 是真正的 File 对象
      if (!(fileObj instanceof File) && !(fileObj instanceof Blob)) {
        createMessage.error('文件对象无效,请重新选择文件');
        importLoading.value = false;
        return;
      }
      // 添加文件到 FormData(关键:必须使用 File 对象,不能是其他格式)
      formData.append('file', fileObj, fileObj.name);
      // 添加业务参数(必须序列化为 JSON 字符串)
      formData.append('importParam', JSON.stringify(importParam));
      // 调试信息:验证 FormData 内容
      console.log('FormData 内容验证:');
      console.log('- file 字段:', formData.get('file'));
      console.log('- importParam 字段:', formData.get('importParam'));
      console.log('- file 类型:', fileObj instanceof File ? 'File' : fileObj instanceof Blob ? 'Blob' : typeof fileObj);
      // 6. 获取登录 Token
      const token = getToken();
      if (!token) {
        createMessage.error('未登录,请先登录系统');
        importLoading.value = false;
        return;
      }
      // 7. 发送请求 - 使用 XMLHttpRequest 手动构建 multipart/form-data 请求
      // 这样可以完全控制请求体和请求头,确保 Content-Type 被正确设置
      const url = 'http://192.168.31.222:8080/jeecg-boot/api/excel/importContract';
      console.log('===== [v9] 手动构建 multipart/form-data 请求(完全控制) =====');
      console.log('请求 URL:', url);
      console.log('文件对象:', fileObj);
      console.log('业务参数:', importParam);
      // 手动构建 multipart/form-data 请求
      const responseData = await new Promise((resolve, reject) => {
        // 生成随机边界
        const boundary = '----WebKitFormBoundary' + Math.random().toString(36).substr(2, 16);
        // 构建请求体的各个部分
        const parts = [];
        // 1. 添加业务参数部分
        parts.push(
          `--${boundary}\r\n`,
          'Content-Disposition: form-data; name="importParam"\r\n',
          'Content-Type: application/json\r\n',
          '\r\n',
          JSON.stringify(importParam),
          '\r\n'
        );
        // 2. 添加文件部分
        parts.push(
          `--${boundary}\r\n`,
          `Content-Disposition: form-data; name="file"; filename="${encodeURIComponent(fileObj.name)}"\r\n`,
          'Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\r\n',
          'Content-Transfer-Encoding: binary\r\n',
          '\r\n'
        );
        // 3. 读取文件内容
        const fileReader = new FileReader();
        fileReader.onload = function(e) {
          if (!e.target?.result) {
            reject(new Error('读取文件失败'));
            return;
          }
          const fileContent = e.target.result;
          // 4. 添加文件内容和结束边界
          const allParts = [...parts];
          // 添加文件内容(使用 ArrayBuffer)
          allParts.push(fileContent);
          // 添加结束边界
          allParts.push(
            '\r\n',
            `--${boundary}--\r\n`
          );
          // 5. 创建最终的请求体 Blob
          const requestBody = new Blob(allParts, { type: `multipart/form-data; boundary=${boundary}` });
          // 6. 创建 XMLHttpRequest
          const xhr = new XMLHttpRequest();
          // 监听请求完成
          xhr.onload = function() {
            if (xhr.status >= 200 && xhr.status < 300) {
              try {
                const data = JSON.parse(xhr.responseText);
                console.log('响应数据:', data);
                resolve(data);
              } catch (error) {
                console.error('解析响应失败:', error);
                reject(new Error('解析响应失败'));
              }
            } else {
              console.error('请求失败:', xhr.status, xhr.statusText);
              reject(new Error(`请求失败: ${xhr.status} ${xhr.statusText}`));
            }
          };
          // 监听网络错误
          xhr.onerror = function() {
            console.error('网络错误');
            reject(new Error('网络错误'));
          };
          // 监听请求进度(可选)
          xhr.upload.onprogress = function(event) {
            if (event.lengthComputable) {
              const percentComplete = (event.loaded / event.total) * 100;
              console.log('上传进度:', percentComplete.toFixed(2) + '%');
            }
          };
          // 打开请求
          xhr.open('POST', url, true);
          // 7. 设置请求头
          xhr.setRequestHeader('X-Access-Token', token);
          xhr.setRequestHeader('X-Tenant-Id', '0');
          xhr.setRequestHeader('X-Version', 'v3');
          xhr.setRequestHeader('Accept', 'application/json, text/plain, */*');
          // 手动设置正确的 Content-Type,包含边界
          xhr.setRequestHeader('Content-Type', `multipart/form-data; boundary=${boundary}`);
          // 设置 Content-Length
          xhr.setRequestHeader('Content-Length', requestBody.size.toString());
          // 8. 发送请求
          xhr.send(requestBody);
        };
        // 读取文件为 ArrayBuffer
        fileReader.readAsArrayBuffer(fileObj);
      });
      console.log('后端响应数据:', responseData);
      const response = { data: responseData };
      // 9. 解析后端响应结果
      if (responseData && responseData.success) {
        // 适配 JeecgBoot 后端返回格式
        importSummary.total = responseData.result?.total || responseData.data?.total || responseData.total || 0;
        importSummary.success = responseData.result?.success || responseData.data?.success || responseData.success || 0;
        importSummary.failed = responseData.result?.failed || responseData.data?.failed || responseData.failed || 0;
        importSummary.duration = responseData.result?.duration || responseData.data?.duration || 0;
        importErrors.value = responseData.result?.errors || responseData.data?.errors || responseData.errors || responseData.result?.errorList || responseData.data?.errorList || [];
        // 无错误但失败数大于0的兜底提示
        if (importErrors.value.length === 0 && importSummary.failed > 0) {
          importErrors.value = [{ key: 1, message: '部分记录导入失败,具体原因请查看后端日志或联系管理员' }];
        }
        createMessage.success('批量导入处理完成');
      } else {
        // 导入失败处理
        importSummary.total = 1;
        importSummary.success = 0;
        importSummary.failed = 1;
        importErrors.value = [{
          key: 1,
          message: responseData?.message || responseData?.msg || '导入失败,后端返回异常信息'
        }];
        createMessage.error(responseData?.message || responseData?.msg || '批量导入失败');
      }
      // 显示导入结果
      showResult.value = true;
    } catch (error) {
      // 全局异常捕获
      console.error('导入过程中捕获异常:', error);
      // axios 错误响应格式:error.response.data 包含后端返回的错误信息
      const errorData = error.response?.data || error;
      const errorMessage = errorData?.message || errorData?.msg || error.message || '网络异常、文件无效或接口调用失败,请稍后重试';
      importSummary.total = 1;
      importSummary.success = 0;
      importSummary.failed = 1;
      importErrors.value = [{
        key: 1,
        message: errorMessage
      }];
      showResult.value = true;
      createMessage.error(errorMessage);
    } finally {
      // 确保无论成功失败,都关闭加载状态
      importLoading.value = false;
    }
  };
  // 渲染错误项
  const renderErrorItem = (error) => {
    return h('a-list-item', null, [
      h('a-list-item-meta', {
        description: error.message || '未知错误'
      })
    ]);
  };
  // 获取人员列表
  const getPersonList = async () => {
    try {
      const response = await defHttp.get({
        url: '/sys/user/listAll',
        params: {
          pageNo: 1,
          pageSize: 200,
        },
      });
      const records = normalizeContractRecords(response);
      const persons = records.map((item: any) => ({
        value: item.id || item.userId || '',
        label: item.realname || item.username || '未知用户'
      })).filter(item => item.value && item.label);
      personList.value = persons;
      console.log('获取人员列表成功:', persons);
    } catch (error) {
      console.error('获取人员列表失败:', error);
      createMessage.error('获取人员列表失败,使用模拟数据');
      const mockPersons = [
        { value: '1', label: '张三' },
        { value: '2', label: '李四' },
        { value: '3', label: '王五' },
        { value: '4', label: '赵六' }
      ];
      personList.value = mockPersons;
    }
  };
  // 页面挂载时初始化数据
  onMounted(() => {
    getContractOptions();
    getPersonList();
  });
</script>
<style lang="less" scoped>
  .form-container {
    max-width: 800px;
    margin: 0 auto;
    padding: 20px;
    display: flex;
    flex-direction: column;
    align-items: center;
  }
  .form-item {
    margin-bottom: 24px;
    display: flex;
    align-items: center;
    width: 100%;
    max-width: 600px;
  }
  .form-label {
    width: 100px;
    font-size: 14px;
  }
  .form-item .form-label {
    margin-right: 16px;
  }
  .required-mark {
    color: #ff4d4f;
    margin-right: 4px;
  }
  .form-actions {
    margin-top: 24px;
    text-align: center;
    width: 100%;
    display: flex;
    justify-content: center;
  }
  .result-container {
    max-width: 800px;
    margin: 20px auto;
    padding: 20px;
  }
  .import-summary {
    display: flex;
    align-items: center;
    margin-bottom: 16px;
    flex-wrap: wrap;
  }
  .import-errors {
    margin-top: 16px;
  }
</style>
src/views/contract/ContractList.vue
@@ -5,6 +5,8 @@
      <!--插槽:table标题-->
      <template #tableTitle>
        <a-button type="primary" v-auth="'contract:contract:add'" @click="handleAdd" preIcon="ant-design:plus-outlined" style="margin-top: 11px"> 新增</a-button>
        <a-button type="primary" v-auth="'contract:contract:batchCreate'" style="margin-left: 8px; margin-top: 11px" @click="handleBatchCreate"> 批量创作</a-button>
        <a-button type="primary" v-auth="'contract:contract:batchImport'" style="margin-left: 8px; margin-top: 11px" @click="handleBatchImport"> 批量导入</a-button>
        <!-- <a-button  v-auth="'contract:contract:bindAgent'" type="primary" style="margin-left: 8px" preIcon="ant-design:user-add-outlined" @click="openBindAgentModal"> 绑定代理商</a-button>
        <a-button  v-auth="'contract:contract:unbindAgent'" danger style="margin-left: 8px" preIcon="ant-design:user-delete-outlined" @click="handleUnbindAgent"> 解除绑定</a-button> -->
        <!-- <a-button  type="primary" v-auth="'contract:contract:exportXls'"  preIcon="ant-design:export-outlined" @click="onExportXls"> 导出</a-button> -->
@@ -140,7 +142,7 @@
  </div>
</template>
<script lang="ts" name="contract-contract" setup>
<script lang="ts" setup>
  import { ref, reactive, computed, onMounted, watch } from 'vue';
  import { BasicTable, TableAction } from '/@/components/Table';
  import { useListPage } from '/@/hooks/system/useListPage';
@@ -577,6 +579,20 @@
  }
  /**
   * 批量创作事件
   */
  function handleBatchCreate() {
    router.push('/contract/batchCreate');
  }
  /**
   * 批量导入事件
   */
  function handleBatchImport() {
    router.push('/contract/batchImport');
  }
  /**
   * 编辑事件
   */
  function handleEdit(record: Recordable) {
src/views/copywriting/generated/index.vue
@@ -425,36 +425,49 @@
      async loadAttachmentFiles(contractId) {
        this.attachmentLoading = true;
        try {
          const result = await defHttp.get({
          const response = await defHttp.get({
            url: '/contract/contract/queryContractFileByMainId',
            params: { id: contractId },
          });
          console.log('附件文件数据:', result);
          console.log('附件文件数据:', response);
          if (result && Array.isArray(result)) {
            // 过滤出附件文件(根据fileType为'合同附件')
          // 处理响应数据,支持多种数据结构
          let fileList = [];
          if (Array.isArray(response)) {
            fileList = response;
          } else if (response && response.result && Array.isArray(response.result)) {
            fileList = response.result;
          } else if (response && response.data && Array.isArray(response.data)) {
            fileList = response.data;
          }
          if (fileList.length > 0) {
            // 过滤出附件文件(同时支持'合同文件'和'合同附件'类型)
            const attachmentFileList = [];
            result.forEach((file) => {
              if (file.fileType === '合同附件' && file.appendixFile) {
            fileList.forEach((file) => {
              // 同时支持两种文件类型
              if ((file.fileType === '合同文件' || file.fileType === '合同附件') && file.appendixFile) {
                const files = file.appendixFile.split(',');
                // .map((file) => ({
                //   id: file.id,
                //   name: file.appendixFile || '附件文件',
                //   url: file.appendixFile,
                // }));
                files.forEach((fileName, index) => {
                  // 跳过空文件名
                  if (!fileName || fileName.trim() === '') return;
                  const uniqueId = `${file.id}_${index}`;
                  attachmentFileList.push({
                    id: uniqueId,
                    name: fileName,
                    url: fileName,
                    originalId: file.id,
                    fileType: file.fileType
                  });
                });
              }
            });
            this.attachmentFiles = attachmentFileList;
            console.log('处理后的附件文件:', attachmentFileList);
            // 如果有附件,默认选择第一个
            if (attachmentFileList.length > 0) {
@@ -467,6 +480,7 @@
                  url: attachmentFileList[0].url || '',
                },
              ];
              console.log('默认选择的附件:', this.jianli);
            }
          }
        } catch (error) {
src/views/demo/page/form/stephetong/Step2.vue
@@ -41,8 +41,8 @@
        actionColOptions: {
          span: 14,
        },
      // 隐藏内置“重置”按钮,改用自定义“上一步”按钮
      showResetButton: false,
        // 隐藏内置“重置”按钮,改用自定义“上一步”按钮
        showResetButton: false,
        submitButtonOptions: {
          text: '下一步',
        },
src/views/demo/page/form/stephetong/Step3.vue
@@ -8,7 +8,7 @@
    <a-divider />
    <!-- 表单:移除了重复的类型/备注字段,仅保留必要功能 -->
    <BasicForm @register="register">
      <!-- 自定义“上一步”按钮,避免触发 BasicForm 的 reset 清空本步骤数据 -->
      <!-- 自定义"上一步"按钮,避免触发 BasicForm 的 reset 清空本步骤数据 -->
      <template #resetBefore>
        <a-button class="mr-2" :disabled="loading" @click="goPrev">上一步</a-button>
      </template>
@@ -79,7 +79,7 @@
        actionColOptions: {
          span: 14,
        },
      // 使用自定义“上一步”按钮,因此不显示内置重置按钮
      // 使用自定义"上一步"按钮,因此不显示内置重置按钮
      showResetButton: false,
        submitButtonOptions: {
          text: '提交',
@@ -286,4 +286,59 @@
  .step3 {
    margin: 0 auto;
  }
  .tab-header {
    margin-bottom: 20px;
  }
  .tab-header :deep(.ant-btn) {
    margin-right: 10px;
  }
  .batch-import-container {
    padding: 20px 0;
  }
  .batch-import-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 20px;
  }
  .batch-import-header h3 {
    margin: 0;
    font-size: 18px;
    font-weight: 600;
  }
  .batch-import-form {
    margin-top: 20px;
  }
  .file-upload-container {
    display: flex;
    align-items: center;
  }
  .file-upload-container :deep(.ant-btn) {
    margin-left: 10px;
  }
  .upload-hint {
    margin-top: 8px;
    color: #1890ff;
  }
  .import-summary {
    margin-top: 20px;
  }
  .import-errors {
    margin-top: 20px;
  }
  .import-errors :deep(.ant-alert) {
    margin-bottom: 10px;
  }
</style>
src/views/demo/page/form/stepxieyi/Step2.vue
@@ -9,6 +9,7 @@
  import { BasicForm, useForm } from '/@/components/Form';
  import { step2Schemas, getContractDetailById } from './data';
  import { Divider, message } from 'ant-design-vue';
  import { useRouter } from 'vue-router';
  export default defineComponent({
    name: 'Step2Form',
@@ -18,6 +19,8 @@
    },
    emits: ['next', 'prev'],
    setup(_, { emit }) {
      const router = useRouter();
      const [register, { validate }] = useForm({
        labelWidth: 120,
        schemas: step2Schemas,
@@ -48,7 +51,11 @@
          localStorage.setItem('selectedContractInfo', JSON.stringify(contractDetail));
          message.success(`已选择合同:${contractDetail.contractName || '未命名合同'}`);
          // 改回原来的跳转逻辑
          emit('next', contractDetail);
          // 跳转到批量生成页面(暂时注释掉)
          // router.push('/contract/batchCreate');
        } catch (error) {}
      }
      return { register };
src/views/demo/page/form/stepxieyi/Step3.vue
@@ -8,11 +8,111 @@
      :message="`当前合同:${currentContract.contractName || '未命名合同'}`"
      :description="currentContract.customerName ? `客户:${currentContract.customerName}` : ''"
    />
    <ACard title="语义词添加" :bordered="false">
      <PersonTable ref="tableRef" />
    </ACard>
    <ADivider />
    <BasicForm @register="register" />
    <!-- 操作切换 -->
    <div class="operation-tabs mb-4">
      <a-tabs v-model:active-key="activeTab">
        <a-tab-pane key="manual" tab="手动添加">
          <ACard :bordered="false">
            <div class="card-header" style="display: flex; align-items: center; margin-bottom: 16px;">
              <h3 style="margin: 0; font-size: 16px; font-weight: 600;">语义词添加</h3>
              <a-button type="primary" style="margin-left: 16px;" @click="switchToBatchImport">批量导入</a-button>
            </div>
            <PersonTable ref="tableRef" />
          </ACard>
          <ADivider />
          <BasicForm @register="register" />
        </a-tab-pane>
        <a-tab-pane key="batch" tab="批量导入">
          <ACard :bordered="false">
            <div class="card-header" style="display: flex; align-items: center; margin-bottom: 16px;">
              <h3 style="margin: 0; font-size: 16px; font-weight: 600;">批量导入</h3>
              <a-button style="margin-left: 16px;" @click="switchToManual">返回手动添加</a-button>
            </div>
            <!-- 批量导入表单 -->
            <div class="batch-import-form">
              <!-- 合同信息 -->
              <div class="form-item mb-4">
                <span class="required-mark">*</span>
                <span class="form-label">合同信息</span>
                <a-select
                  v-model:value="batchForm.contractId"
                  placeholder="请选择合同"
                  style="width: 360px;"
                  show-search
                  :filter-option="filterOption"
                  @change="handleContractChange"
                >
                  <a-select-option v-for="contract in contractList" :key="contract.value" :value="contract.value">
                    {{ contract.label }}
                  </a-select-option>
                </a-select>
              </div>
              <!-- 语义词文件选择 -->
              <div class="form-item mb-4">
                <span class="required-mark">*</span>
                <span class="form-label">语义词文件</span>
                <div style="display: flex; align-items: center;">
                  <input type="text" readonly placeholder="" style="width: 360px; height: 32px; padding: 0 11px; border: 1px solid #d9d9d9; border-radius: 4px; margin-right: 0; border-right: none; border-top-right-radius: 0; border-bottom-right-radius: 0;" :value="batchForm.semanticFile || ''" />
                  <a-upload
                    v-model:file-list="fileList"
                    :multiple="false"
                    accept=".xlsx,.csv,.txt"
                    :before-upload="beforeUpload"
                    @change="handleFileChange"
                  >
                    <a-button type="default" style="border-top-left-radius: 0; border-bottom-left-radius: 0;">
                      选择
                    </a-button>
                  </a-upload>
                </div>
                <div style="margin-top: 8px; font-size: 12px; color: #666;">
                  <a href="#" @click.prevent="downloadTemplate">语义词导入模板.xlsx</a>
                </div>
              </div>
              <!-- 文案编辑 -->
              <div class="form-item mb-4">
                <span class="required-mark">*</span>
                <span class="form-label">文案编辑</span>
                <a-select v-model:value="batchForm.editor" placeholder="请选择文案编辑" style="width: 360px;">
                  <a-select-option v-for="person in personList" :key="person.value" :value="person.value">
                    {{ person.label }}
                  </a-select-option>
                </a-select>
              </div>
              <!-- 操作按钮 -->
              <div class="form-actions mt-6">
                <a-button type="primary" @click="handleImport" :loading="importLoading" style="width: 120px; height: 40px; font-size: 16px;">
                  批量导入
                </a-button>
              </div>
            </div>
            <!-- 导入结果 -->
            <div class="result-container" v-show="showResult" style="margin-top: 20px;">
              <a-divider orientation="left">导入结果</a-divider>
              <div class="import-summary">
                <a-statistic title="总记录数" :value="importSummary.total" style="margin-right: 32px" />
                <a-statistic title="成功数" :value="importSummary.success" prefix=""><template #prefix><span style="color: green">✓</span></template></a-statistic>
                <a-statistic title="失败数" :value="importSummary.failed" prefix=""><template #prefix><span style="color: red">✗</span></template></a-statistic>
                <a-statistic title="处理时间" :value="importSummary.duration" suffix="秒" style="margin-left: 32px" />
              </div>
              <div v-if="importErrors.length > 0" class="import-errors" style="margin-top: 16px;">
                <a-divider orientation="left">错误信息</a-divider>
                <a-list :data-source="importErrors" :render-item="renderErrorItem" />
              </div>
            </div>
          </ACard>
        </a-tab-pane>
      </a-tabs>
    </div>
    <!-- 调试按钮 -->
    <!-- <div style="margin-top: 20px; text-align: center;">
      <a-button type="primary" @click="debugTableData">调试表格数据</a-button>
@@ -20,12 +120,14 @@
  </div>
</template>
<script lang="ts">
  import { defineComponent, onMounted, ref } from 'vue';
  import { defineComponent, onMounted, ref, reactive } from 'vue';
  import { BasicForm, useForm } from '/@/components/Form';
  import { step3Schemas } from './data';
  import PersonTable from './PersonTable.vue';
  import { Divider, Card, message, Alert } from 'ant-design-vue';
  import { Divider, Card, message, Alert, Tabs, Upload, Button, Select, Statistic, List } from 'ant-design-vue';
  import { addSemanticWordsToContractApi } from '/@/api/demo/semanticWordApi';
  import { defHttp } from '/@/utils/http/axios';
  import { router } from '/@/router';
  export default defineComponent({
    name: 'Step3Form',
@@ -35,6 +137,14 @@
      ADivider: Divider,
      ACard: Card,
      AAlert: Alert,
      ATabs: Tabs,
      ATabPane: Tabs.TabPane,
      AUpload: Upload,
      AButton: Button,
      ASelect: Select,
      ASelectOption: Select.Option,
      AStatistic: Statistic,
      AList: List,
    },
    props: {
      // 移除targetFileId属性,因为父组件没有传递
@@ -44,6 +154,35 @@
    setup(_, { emit }) {
      const tableRef = ref<{ getTableData: () => any } | null>(null);
      const currentContract = ref<Record<string, any> | null>(null);
      // 批量导入相关状态
      const activeTab = ref('manual');
      const contractList = ref([]);
      const contractListCache = ref([]);
      const fileList = ref([]);
      const importLoading = ref(false);
      const showResult = ref(false);
      // 批量导入表单数据
      const batchForm = reactive({
        contractId: '',
        semanticFile: '',
        editor: ''
      });
      // 人员列表
      const personList = ref([]);
      // 导入结果
      const importSummary = reactive({
        total: 0,
        success: 0,
        failed: 0,
        duration: 0
      });
      // 错误信息
      const importErrors = ref([]);
      const readSelectedContract = () => {
        try {
@@ -64,6 +203,8 @@
      onMounted(() => {
        readSelectedContract();
        getContractOptions();
        getPersonList();
      });
      const [register, { validate, setProps }] = useForm({
@@ -304,7 +445,306 @@
        }
      }
      return { register, tableRef, debugTableData, currentContract };
      // 切换到批量导入
      function switchToBatchImport() {
        // 跳转到批量导入页面
        router.push('/contract/batchImport');
      }
      // 切换到手动添加
      function switchToManual() {
        activeTab.value = 'manual';
      }
      // 归一化合同记录
      const normalizeContractRecords = (response: any) => {
        if (Array.isArray(response)) {
          return response;
        }
        if (Array.isArray(response?.records)) {
          return response.records;
        }
        if (Array.isArray(response?.result?.records)) {
          return response.result.records;
        }
        if (Array.isArray(response?.data?.records)) {
          return response.data.records;
        }
        return [];
      };
      // 解析合同标签
      const resolveContractLabel = (item: any) => {
        if (item.contractName) return item.contractName;
        if (item.contractNo) return `合同编号:${item.contractNo}`;
        return '未命名合同';
      };
      // 解析合同值
      const resolveContractValue = (item: any) => {
        return item.id || item.contractId || item.contractName || '';
      };
      // 获取合同选项
      const getContractOptions = async () => {
        try {
          const response = await defHttp.get({
            url: '/contract/contract/list',
            params: {
              column: 'createTime',
              order: 'desc',
              pageNo: 1,
              pageSize: 200,
            },
          });
          const records = normalizeContractRecords(response);
          contractListCache.value = records;
          const options = records.map((item: any) => ({
            label: resolveContractLabel(item),
            value: resolveContractValue(item),
          }));
          contractList.value = options;
        } catch (error) {
          console.error('获取合同列表失败:', error);
          message.error('获取合同列表失败');
        }
      };
      // 获取人员列表
      const getPersonList = async () => {
        try {
          // 调用后端API获取人员列表
          const response = await defHttp.get({
            url: '/sys/user/listAll',
            params: {
              pageNo: 1,
              pageSize: 200,
            },
          });
          // 处理响应数据
          const records = normalizeContractRecords(response);
          // 转换为下拉框选项格式
          const persons = records.map((item: any) => ({
            value: item.id || item.userId || '',
            label: item.realname || item.username || '未知用户'
          })).filter(item => item.value && item.label);
          personList.value = persons;
        } catch (error) {
          console.error('获取人员列表失败:', error);
          message.error('获取人员列表失败');
          // 失败时使用模拟数据
          const mockPersons = [
            { value: '1', label: '张三' },
            { value: '2', label: '李四' },
            { value: '3', label: '王五' },
            { value: '4', label: '赵六' }
          ];
          personList.value = mockPersons;
        }
      };
      // 过滤选项
      const filterOption = (input: string, option: any) => {
        return (option?.label || '').toLowerCase().includes(input.toLowerCase());
      };
      // 合同选择变化
      const handleContractChange = (value: string) => {
        if (!value) return;
        const contract = getContractDetailById(value);
        if (contract) {
          // 将合同信息存储到localStorage,与语义词录入页面保持一致
          localStorage.setItem('selectedContractInfo', JSON.stringify(contract));
        }
      };
      // 根据ID获取合同详情
      const getContractDetailById = (contractId: string) => {
        return contractListCache.value.find(
          (item: any) =>
            item.id === contractId ||
            item.contractId === contractId ||
            resolveContractValue(item) === contractId
        );
      };
      // 文件上传前校验
      const beforeUpload = (file) => {
        const allowedTypes = ['.xlsx', '.csv', '.txt'];
        const fileExtension = file.name.substring(file.name.lastIndexOf('.'));
        if (!allowedTypes.includes(fileExtension)) {
          message.error('只支持上传 Excel、CSV、文本文件');
          return false;
        }
        if (file.size > 10 * 1024 * 1024) {
          message.error('文件大小不能超过 10MB');
          return false;
        }
        // 阻止自动上传,只需要文件信息
        return false;
      };
      // 文件上传变化
      const handleFileChange = (info) => {
        // 过滤掉已上传或已删除的文件,只保留正在上传的文件
        const newFileList = info.fileList.filter(file => file.status !== 'removed');
        fileList.value = newFileList;
        if (newFileList.length > 0) {
          batchForm.semanticFile = newFileList[0].name;
        } else {
          batchForm.semanticFile = '';
        }
      };
      // 下载模板
      const downloadTemplate = () => {
        message.info('模板下载功能开发中');
      };
      // 开始导入
      const handleImport = async () => {
        try {
          // 表单验证
          if (!batchForm.contractId) {
            message.error('请选择合同');
            return;
          }
          if (fileList.value.length === 0) {
            message.error('请先选择语义词文件');
            return;
          }
          if (!batchForm.editor) {
            message.error('请选择文案编辑');
            return;
          }
          importLoading.value = true;
          // 构建表单数据
          const formData = new FormData();
          // 添加importParam参数
          const importParam = {
            contractId: batchForm.contractId,
            ranking: "3", // 签约排名写死3
            status: "1", // 状态:已派单
            remark: "批量订单", // 备注信息:批量订单
            month: "12", // 合作月份:12个月
            price: "0", // 价格:0
            acceptindicator: "80" // 验收指标:80
          };
          formData.append('importParam', JSON.stringify(importParam));
          // 添加文件参数
          if (fileList.value.length > 0) {
            const file = fileList.value[0];
            // 确保使用正确的文件对象
            const fileObj = file.originFileObj || file.file || file;
            formData.append('file', fileObj);
          }
          console.log('开始调用导入接口...');
          // 调用后端接口
          const response = await defHttp.post({
            url: '/api/excel/importContract',
            data: formData,
            headers: {
              'X-Tenant-Id': '0',
              'X-Version': 'v3'
            },
            timeout: 60000, // 上传文件可能需要较长时间
            requestOptions: {
              withToken: true // 确保携带token
            }
          });
          console.log('接口响应:', response);
          // 处理响应
          if (response.success) {
            // 更新导入结果
            importSummary.total = 1;
            importSummary.success = 1;
            importSummary.failed = 0;
            importSummary.duration = 1;
            // 清空错误信息
            importErrors.value = [];
            // 显示结果
            showResult.value = true;
            importLoading.value = false;
            message.success('批量导入完成');
          } else {
            // 处理错误
            importSummary.total = 1;
            importSummary.success = 0;
            importSummary.failed = 1;
            importSummary.duration = 1;
            // 更新错误信息
            importErrors.value = [{
              key: 1,
              message: response.message || '导入失败'
            }];
            // 显示结果
            showResult.value = true;
            importLoading.value = false;
            message.error(response.message || '导入失败');
          }
        } catch (error) {
          console.error('导入失败:', error);
          importLoading.value = false;
          message.error('导入失败,请检查网络连接或登录状态');
        }
      };
      // 渲染错误项
      const renderErrorItem = (error) => {
        return {
          description: error.message
        };
      };
      return {
        register,
        tableRef,
        debugTableData,
        currentContract,
        activeTab,
        switchToBatchImport,
        switchToManual,
        batchForm,
        contractList,
        fileList,
        importLoading,
        showResult,
        importSummary,
        importErrors,
        personList,
        filterOption,
        handleContractChange,
        beforeUpload,
        handleFileChange,
        downloadTemplate,
        handleImport,
        renderErrorItem
      };
    },
  });
</script>
src/views/semanticwordPD/SemanticWordList.vue
@@ -20,6 +20,8 @@
        </a-dropdown>
        <!-- 高级查询 -->
        <super-query :config="superQueryConfig" @search="handleSuperQuery" />
        <!-- 批量创作 -->
        <a-button type="primary" style="margin-left: 8px" @click="handleBatchCreate">批量创作</a-button>
      </template>
      <!--操作栏-->
      <template #action="{ record }">
@@ -232,6 +234,20 @@
      // }
    ];
  }
  /**
   * 批量创作事件
   */
  function handleBatchCreate() {
    router.push('/contract/batchCreate');
  }
  /**
   * 批量导入事件
   */
  function handleBatchImport() {
    router.push('/contract/batchImport');
  }
</script>
<style lang="less" scoped>