ZJQ合同停止服务和恢复服务标签修复数据仓bug ,财务审核加了一个已掉词,客户列表还加了服务状态标签
8个文件已修改
970 ■■■■ 已修改文件
src/views/contract/Contract.api.ts 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/contract/Contract.data.ts 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/contract/ContractList.vue 30 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/datacabin/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/demo/page/desc/detailkehu/index.vue 632 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/demo/page/desc/editkehu/index.vue 228 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/semanticword/SemanticWord.api.ts 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/semanticword/SemanticWord.data.ts 31 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/contract/Contract.api.ts
@@ -38,32 +38,40 @@
export const semanticWordList = Api.semanticWordList;
/**
 * 查询单个详情
 * @param params
 * @param params 查询参数
 * 添加successMessageMode: 'none'选项,防止显示默认的成功提示
 * 避免在测试环境出现徽章提示
 */
export const queryById = (params) =>
  defHttp.get({url: Api.queryById, params});
  defHttp.get({url: Api.queryById, params}, { successMessageMode: 'none' });
/**
 * 列表接口
 * @param params
 * @param params 查询参数
 * 添加successMessageMode: 'none'选项,防止显示默认的成功提示
 * 避免在测试环境出现徽章提示
 */
export const list = (params) =>
  defHttp.get({url: Api.list, params});
  defHttp.get({url: Api.list, params}, { successMessageMode: 'none' });
/**
 * 根据角色获取合同列表
 * @param role 角色编号
 * @param user 用户名(销售角色必填)
 * @param createBy 创建人(代理商角色使用)
 * 添加successMessageMode: 'none'选项,防止显示默认的成功提示
 * 避免在测试环境出现徽章提示
 */
export const listByRole = (params: { role?: string; user?: string; createBy?: string }) =>
  defHttp.get({url: Api.list, params});
  defHttp.get({url: Api.list, params}, { successMessageMode: 'none' });
/**
 * 获取合同到期数据
 * @param params
 * @param params 查询参数
 * 添加successMessageMode: 'none'选项,防止显示默认的成功提示
 * 避免在测试环境出现徽章提示
 */
export const getExpiringData = (params) =>
  defHttp.get({url: Api.expiring, params});
  defHttp.get({url: Api.expiring, params}, { successMessageMode: 'none' });
/**
 * 删除单个
src/views/contract/Contract.data.ts
@@ -11,7 +11,10 @@
   {
    title: '合同名称',
    align:"center",
    dataIndex: 'contractName'
    dataIndex: 'contractName',
    slots: {
      customRender: 'contractNameWithStatus'
    }
   },
   {
    title: '语义词',
@@ -79,6 +82,17 @@
      dictCode:"yuyici_status",
    },
  },
  {
    label: "服务状态",
    field: "isDropService",
    component: 'Select',
    componentProps:{
      options: [
        { label: '服务中', value: '否' },
        { label: '已停止', value: '是' },
      ],
    },
  },
];
//表单数据
export const formSchema: FormSchema[] = [
src/views/contract/ContractList.vue
@@ -33,6 +33,18 @@
      <template #action="{ record }">
        <TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)" />
      </template>
      <!--合同名称带服务状态标签-->
      <template #contractNameWithStatus="{ record }">
        <div class="contract-name-with-status">
          <a-tag
            :color="record.isDropService === '是' ? 'red' : 'green'"
            style="margin-right: 8px"
          >
            {{ record.isDropService === '是' ? '已停止' : '服务中' }}
          </a-tag>
          <span>{{ record.contractName }}</span>
        </div>
      </template>
    </BasicTable>
    <!-- 表单区域 -->
    <ContractModal @register="registerModal" @success="handleSuccess"></ContractModal>
@@ -265,17 +277,17 @@
        schemas: searchFormSchema,
        autoSubmitOnEnter: true,
        showAdvancedButton: true,
        autoAdvancedCol: 4,
        autoAdvancedCol: 5,
        fieldMapToNumber: [],
        fieldMapToTime: [],
        compact: true,
        baseColProps: {
          xs: 24,
          sm: 4,
          md: 4,
          lg: 4,
          xl: 4,
          xxl: 4,
          sm: 3,
          md: 3,
          lg: 3,
          xl: 3,
          xxl: 3,
        },
        rowProps: {
          gutter: 8,
@@ -841,4 +853,10 @@
      padding: 40px 0;
    }
  }
  .contract-name-with-status {
    display: flex;
    align-items: center;
    justify-content: center;
  }
</style>
src/views/datacabin/index.vue
@@ -974,7 +974,7 @@
    // 3. 语义词总数统计
    const semanticWordRes = await defHttp.get({ url: '/semanticword/semanticWord/count', params }, { successMessageMode: 'none' });
    // 4. 文章总数统计
    // 4. 文章总数统计隐藏选项
    const copywritingRes = await defHttp.get({ url: '/copywriting/copywriting/count', params }, { successMessageMode: 'none' });
    // 更新统计数据
src/views/demo/page/desc/detailkehu/index.vue
@@ -1,41 +1,615 @@
<template>
  <PageWrapper title="欢迎" contentBackground>
    <div class="welcome-container">
      <div class="welcome-content">
        <h1 class="welcome-title">您好,欢迎访问</h1>
        <h2 class="welcome-system-name">艾亿欧AIEO智能业务管理系统!</h2>
  <PageWrapper title="客户详情页" contentBackground>
    <a-card title="基本信息">
      <a-descriptions title="" :column="3">
        <a-descriptions-item label="企业名称">
          <span>{{ formData.customer?.enterpriseName || '-' }}</span>
        </a-descriptions-item>
        <a-descriptions-item label="行业">
          <span>{{ formData.customer?.industry || '-' }}</span>
        </a-descriptions-item>
        <a-descriptions-item label="客户地址">
          <span>{{ formData.customer?.customerAddress || '-' }}</span>
        </a-descriptions-item>
        <a-descriptions-item label="对接人">
          <span>{{ formData.customer?.contactPerson || '-' }}</span>
        </a-descriptions-item>
        <a-descriptions-item label="客户电话">
          <span>{{ formData.customer?.customerPhone || '-' }}</span>
        </a-descriptions-item>
        <a-descriptions-item label="客户邮箱">
          <span>{{ formData.customer?.customerEmail || '-' }}</span>
        </a-descriptions-item>
      </a-descriptions>
    </a-card>
    <a-card title="合同信息">
      <a-descriptions title="" :column="1">
        <a-descriptions-item label="紧急状态">
          <span>{{ formData.emergencyStatus === 1 ? '紧急' : formData.emergencyStatus === 3 ? '正常' : '-' }}</span>
        </a-descriptions-item>
        <a-descriptions-item label="合作周期">
          <span>{{ formData.month ? `${formData.month}月` : '-' }}</span>
        </a-descriptions-item>
        <!-- <a-descriptions-item label="合作开始时间">
          <span>{{ formData.startDate || '-' }}</span>
        </a-descriptions-item>
        <a-descriptions-item label="合作结束时间">
          <span>{{ formData.endDate || '-' }}</span>
        </a-descriptions-item> -->
        <a-descriptions-item label="合同">
          <div v-if="contractFiles.length > 0">
            <div v-for="file in contractFiles" :key="file.uid" class="file-item">
              <div class="file-header">
                <a @click="handleDownload(file)" style="display: inline-block">{{ getFileName(file) }}</a>
                <a @click="toggleFilePreview(file)" class="preview-toggle">
                  {{ isFilePreviewOpen(file) ? '收起' : '[预览]' }}
                </a>
              </div>
              <div v-if="isFilePreviewOpen(file)" class="docx-preview">
                <div v-if="isReviewFile(file) && getFileUrl(file)">
                  <vue-office-docx :src="getFileUrl(file)" style="width: 100%; height: 600px;" v-if="isDocxFile(file)"/>
                  <vue-office-excel :src="getFileUrl(file)" style="width: 100%; height: 600px;" v-if="isXlsxFile(file)"/>
                  <vue-office-pdf :src="getFileUrl(file)" style="width: 100%; height: 600px;" v-if="isPdfFile(file)"/>
                  <template v-if="isTxtFile(file)">
                    <a-spin :spinning="isTxtLoading(file)">
                      <pre class="code-area">{{ getTxtContent(file) }}</pre>
                    </a-spin>
                  </template>
                  <template v-if="isImageFile(file)">
                    <img :src="getFileUrl(file)" style="width: 100%; height: 600px;" />
                  </template>
                </div>
                <div v-else class="preview-not-supported">
                  <a-empty description="当前文件类型的预览功能不支持!" />
                </div>
              </div>
            </div>
          </div>
          <div v-else>
            <span>-</span>
          </div>
        </a-descriptions-item>
        <a-descriptions-item label="附件">
          <div v-if="attachmentFiles.length > 0">
            <div v-for="file in attachmentFiles" :key="file.uid" class="file-item">
              <div class="file-header">
                <a @click="handleDownload(file)" style="display: inline-block">{{ getFileName(file) }}</a>
                <a @click="toggleFilePreview(file)" class="preview-toggle">
                  {{ isFilePreviewOpen(file) ? '收起' : '[预览]' }}
                </a>
              </div>
              <div v-if="isFilePreviewOpen(file)" class="docx-preview">
                <div v-if="isReviewFile(file) && getFileUrl(file)">
                  <vue-office-docx :src="getFileUrl(file)" style="width: 100%; height: 600px;" v-if="isDocxFile(file)"/>
                  <vue-office-excel :src="getFileUrl(file)" style="width: 100%; height: 600px;" v-if="isXlsxFile(file)"/>
                  <vue-office-pdf :src="getFileUrl(file)" style="width: 100%; height: 600px;" v-if="isPdfFile(file)"/>
                  <template v-if="isTxtFile(file)">
                    <a-spin :spinning="isTxtLoading(file)">
                      <pre class="code-area">{{ getTxtContent(file) }}</pre>
                    </a-spin>
                  </template>
                  <template v-if="isImageFile(file)">
                    <img :src="getFileUrl(file)" style="width: 100%; height: 600px;" />
                  </template>
                </div>
                <div v-else class="preview-not-supported">
                  <a-empty description="当前文件类型的预览功能不支持!" />
                </div>
              </div>
            </div>
          </div>
          <div v-else>
            <span>-</span>
          </div>
        </a-descriptions-item>
      </a-descriptions>
    </a-card>
    <a-card title="语义词信息">
      <div v-if="semanticWords.length > 0">
        <a-descriptions title="" :column="1">
          <a-descriptions-item v-for="word in semanticWords" :key="word.id" :label="`语义词 ${semanticWords.indexOf(word) + 1}`">
            <div style="display: flex; flex-direction: column; gap: 12px; width: 100%">
              <div style="display: flex; gap: 16px; width: 100%">
                <span style="flex: 1;"><strong>类型:</strong>{{ word.category === '1' ? '品牌' : word.category === '2' ? '地区词' : '全国词' }}</span>
                <span style="flex: 1;"><strong>语义词:</strong>{{ word.word || '-' }}</span>
                <span style="flex: 1;"><strong>露出词:</strong>{{ word.outWord || '-' }}</span>
                <span style="flex: 1;"><strong>价格:</strong>{{ word.price ? `${formatPrice(word.price)}元` : '-' }}</span>
                <span style="flex: 1;"><strong>合作周期:</strong>{{ word.month ? `${word.month}月` : '-' }}</span>
                <span style="flex: 1;"><strong>签约排名:</strong>{{ formatRanking(word.ranking) }}</span>
                <span style="flex: 1;"><strong>验收指标(>=):</strong>{{ word.acceptindicator ? `${word.acceptindicator}%` : '-' }}</span>
              </div>
            </div>
          </a-descriptions-item>
        </a-descriptions>
      </div>
      <div v-else>
        <a-empty description="暂无语义词数据" />
      </div>
    </a-card>
    <!-- 返回按钮 -->
    <div class="action-buttons">
      <a-button type="default" @click="handleBack" class="back-btn">返回</a-button>
    </div>
  </PageWrapper>
</template>
<script lang="ts">
  import { defineComponent, reactive, ref, onMounted } from 'vue';
  import { PageWrapper } from '/@/components/Page';
  import { message } from 'ant-design-vue';
  import { useRoute, useRouter } from 'vue-router';
  import { defHttp } from '/@/utils/http/axios';
  import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
  import VueOfficeDocx from '@vue-office/docx';
  import VueOfficeExcel from '@vue-office/excel';
  import VueOfficePdf from '@vue-office/pdf';
  import '@vue-office/docx/lib/v3/index.css';
  import '@vue-office/excel/lib/v3/index.css';
<script lang="ts" setup>
// 欢迎页面组件
  export default defineComponent({
    name: 'DescDetailKehu',
    components: { PageWrapper, VueOfficeDocx, VueOfficeExcel, VueOfficePdf },
    setup() {
      const route = useRoute();
      const router = useRouter();
      // 初始数据
      const initialData = {
        id: '',
        createTime: '',
        updateBy: null,
        updateTime: null,
        agentsId: null,
        agentsName: null,
        contractFileList: null,
        contractName: '',
        createBy: '',
        customer: {
          id: '',
          createBy: '',
          createTime: '',
          updateBy: null,
          updateTime: null,
          agentsName: null,
          contactPerson: '',
          contract: null,
          customerAddress: '',
          customerEmail: '',
          customerPhone: '',
          enterpriseName: '',
          industry: '',
          semanticWordList: null,
        },
        customerName: '',
        endDate: '',
        rejectionReasons: null,
        reviewStatus: '',
        semanticWordList: null,
        startDate: '',
        sysOrgCode: '',
        emergencyStatus: 3, // 默认正常状态
        month: null, // 合作周期(月)
      };
      // 响应式表单数据
      const formData = reactive({ ...initialData });
      // 语义词列表
      const semanticWords = ref<any[]>([]);
      // 合同文件列表
      const contractFiles = ref<any[]>([]);
      const attachmentFiles = ref<any[]>([]);
      // 文件预览展开状态(使用文件 uid 作为 key)
      const filePreviewState = reactive<Record<string, boolean>>({});
      const txtContentMap = reactive<Record<string, string>>({});
      const txtLoadingMap = reactive<Record<string, boolean>>({});
      // 加载客户数据
      const loadCustomerData = async () => {
        const id = route.query.id as string;
        if (id) {
          try {
            // 调用API根据id获取客户数据
            const result = await defHttp.get({
              url: '/contract/contract/queryById',
              params: { id },
            });
            console.log('客户详情数据:', result);
            if (result) {
              const customerData = result;
              // 填充表单数据
              Object.assign(formData, customerData);
              // 确保 emergencyStatus 正确设置(1=紧急,3=正常)
              const emergencyStatus = Number(customerData.emergencyStatus);
              if (emergencyStatus === 1 || emergencyStatus === 3) {
                formData.emergencyStatus = emergencyStatus;
              } else {
                formData.emergencyStatus = 3; // 默认正常状态
              }
              // 加载合同文件数据
              await loadContractFiles(id);
              // 加载语义词数据
              await loadSemanticWords(id);
            } else {
              message.error('加载客户详情数据失败');
            }
          } catch (error) {
            console.error('加载客户详情数据失败:', error);
            message.error('加载客户详情数据失败');
          }
        } else {
          message.error('缺少客户ID参数');
        }
      };
      // 加载合同文件数据
      const loadContractFiles = async (id: string) => {
        try {
          const result = await defHttp.get({
            url: '/contract/contract/queryContractFileByMainId',
            params: { id },
          });
          console.log('合同文件数据:', result);
          // 这里可以根据返回的数据结构处理合同文件
          if (result && Array.isArray(result)) {
            // 根据 fileType 区分合同文件和附件
            const contractFileList: any[] = [];
            const attachmentFileList: any[] = [];
            result.forEach((file: any) => {
              const fileItem = {
                uid: file.id,
                name: file.appendixFile || '文件',
                status: 'done',
                url: file.appendixFile,
              };
              if (file.fileType === '合同文件') {
                contractFileList.push(fileItem);
              } else if (file.fileType === '合同附件') {
                attachmentFileList.push(fileItem);
              }
            });
            contractFiles.value = contractFileList;
            attachmentFiles.value = attachmentFileList;
          }
        } catch (error) {
          console.error('加载合同文件失败:', error);
        }
      };
      // 加载语义词数据
      const loadSemanticWords = async (id: string) => {
        try {
          const result = await defHttp.get({
            url: '/contract/contract/querySemanticWordByMainId',
            params: { id },
          });
          console.log('语义词数据:', result);
          // 这里可以根据返回的数据结构处理语义词
          if (result && Array.isArray(result)) {
            // 将语义词列表保存到响应式变量中
            semanticWords.value = result;
          }
        } catch (error) {
          console.error('加载语义词失败:', error);
        }
      };
      onMounted(() => {
        loadCustomerData();
      });
      // 工具方法:获取文件名(去掉 temp/)
      const getFileName = (file: any) => {
        const name = file.name || '';
        if (name.startsWith('temp/')) {
          return name.substring(5);
        }
        return name;
      };
      // 工具方法:获取文件的完整访问 URL
      const getFileUrl = (file: any) => {
        if (!file.url) {
          return '';
        }
        return getFileAccessHttpUrl(file.url);
      };
      // 统一获取文件 key,避免重复逻辑
      const getFileKey = (file: any) => file?.uid || file?.id || file?.url || file?.name || '';
      // 读取 TXT 文本内容用于预览
      const fetchTxtContent = async (file: any) => {
        const fileKey = getFileKey(file);
        if (!fileKey || txtLoadingMap[fileKey]) return;
        txtLoadingMap[fileKey] = true;
        try {
          const url = getFileUrl(file);
          if (!url) throw new Error('TXT 文件地址不存在');
          const resp = await fetch(url);
          if (!resp.ok) throw new Error(`请求失败:${resp.status}`);
          const text = await resp.text();
          txtContentMap[fileKey] = text || '(文件为空)';
        } catch (error) {
          console.error('TXT 文件加载失败', error);
          txtContentMap[fileKey] = 'TXT 文件预览失败,请稍后重试';
          message.error('TXT 文件预览失败');
        } finally {
          txtLoadingMap[fileKey] = false;
        }
      };
      const isReviewFile = (file: any) => {
        if (!file || !file.url) {
          return false;
        }
        const fileName = file.name || file.url || '';
        const lowerName = fileName.toLowerCase();
        return lowerName.endsWith('.docx') || lowerName.endsWith('.xlsx') || lowerName.endsWith('.pdf') || lowerName.endsWith('.txt') ||lowerName.endsWith('.jpg') || lowerName.endsWith('.jpeg') || lowerName.endsWith('.png') || lowerName.endsWith('.gif') || lowerName.endsWith('.bmp') || lowerName.endsWith('.webp');
      };
      // 判断是否为 DOCX 文件
      const isDocxFile = (file: any) => {
        if (!file) {
          return false;
        }
        const fileName = file.name || file.url || '';
        const lowerName = fileName.toLowerCase();
        return lowerName.endsWith('.docx');
      };
      // 判断是否为 XLSX 文件
      const isXlsxFile = (file: any) => {
        if (!file) {
          return false;
        }
        const fileName = file.name || file.url || '';
        const lowerName = fileName.toLowerCase();
        return lowerName.endsWith('.xlsx');
      };
      // 判断是否为 PDF 文件
      const isPdfFile = (file: any) => {
        if (!file) {
          return false;
        }
        const fileName = file.name || file.url || '';
        const lowerName = fileName.toLowerCase();
        return lowerName.endsWith('.pdf');
      };
      // 判断是否为 TXT 文件
      const isTxtFile = (file: any) => {
        if (!file) {
          return false;
        }
        const fileName = file.name || file.url || '';
        const lowerName = fileName.toLowerCase();
        return lowerName.endsWith('.txt');
      };
      // 判断是否为图片文件
      const isImageFile = (file: any) => {
        if (!file) {
          return false;
        }
        const fileName = file.name || file.url || '';
        const lowerName = fileName.toLowerCase();
        return lowerName.endsWith('.jpg') || lowerName.endsWith('.jpeg') || lowerName.endsWith('.png') || lowerName.endsWith('.gif') || lowerName.endsWith('.bmp') || lowerName.endsWith('.webp');
      };
      const getTxtContent = (file: any) => {
        const key = getFileKey(file);
        return txtContentMap[key] || '加载中...';
      };
      const isTxtLoading = (file: any) => {
        const key = getFileKey(file);
        return !!txtLoadingMap[key];
      };
      // 切换文件预览状态
      const toggleFilePreview = (file: any) => {
        const fileId = getFileKey(file);
        if (!fileId) return;
        const nextState = !filePreviewState[fileId];
        filePreviewState[fileId] = nextState;
        // 打开 TXT 时加载内容
        if (nextState && isTxtFile(file) && !txtContentMap[fileId]) {
          fetchTxtContent(file);
        }
      };
      // 检查文件是否展开预览
      const isFilePreviewOpen = (file: any) => {
        const fileId = getFileKey(file);
        return filePreviewState[fileId] || false;
      };
      // 文件下载功能
      const handleDownload = (file: any) => {
        if (!file.url) {
          message.error('文件链接不存在');
          return;
        }
        try {
          // 创建一个隐藏的a标签进行下载
          const link = document.createElement('a');
          link.href = getFileAccessHttpUrl(file.url);
          link.download = getFileName(file);
          link.target = '_blank';
          link.style.display = 'none';
          // 添加到DOM中
          document.body.appendChild(link);
          // 触发点击事件
          link.click();
          // 清理DOM
          document.body.removeChild(link);
        } catch (error) {
          console.error('文件下载失败:', error);
          message.error('文件下载失败');
        }
      };
      // 返回功能
      const handleBack = () => {
        router.back();
      };
      // 格式化价格
      const formatPrice = (price: number) => {
        if (!price) return '-';
        return price.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
      };
      // 格式化签约排名
      const formatRanking = (ranking: any) => {
        if (ranking === 1 || ranking === 2 || ranking === 3) {
          return ranking.toString();
        } else if (ranking === '保展现不保排名') {
          return '保展现不保排名';
        }
        return '-';
      };
      return {
        formData,
        contractFiles,
        attachmentFiles,
        semanticWords,
        handleDownload,
        handleBack,
        getFileName,
        getFileUrl,
        isReviewFile,
        isDocxFile,
        isXlsxFile,
        isPdfFile,
        isTxtFile,
        isImageFile,
        getTxtContent,
        isTxtLoading,
        toggleFilePreview,
        isFilePreviewOpen,
        formatPrice,
        formatRanking,
      };
    },
  });
</script>
<style lang="less" scoped>
.welcome-container {
  min-height: calc(100vh - 200px);
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: #f0f2f5;
}
  .desc-wrap {
    padding: 16px;
    background-color: @component-background;
  }
.welcome-content {
  text-align: center;
  padding: 40px;
}
  .action-buttons {
    position: fixed;
    right: 24px;
    bottom: 24px;
    display: flex;
    gap: 12px;
    z-index: 1000;
.welcome-title {
  font-size: 32px;
  color: #333;
  margin-bottom: 20px;
}
    .back-btn {
      min-width: 80px;
      height: 40px;
      border-radius: 6px;
      font-weight: 500;
      background-color: #f5f5f5;
      border-color: #d9d9d9;
      color: #666;
.welcome-system-name {
  font-size: 40px;
  color: #1890ff;
  font-weight: bold;
}
      &:hover {
        background-color: #e6f7ff;
        border-color: #40a9ff;
        color: #40a9ff;
      }
    }
  }
  // 文件下载链接样式
  a {
    color: #1890ff;
    text-decoration: none;
    cursor: pointer;
    transition: color 0.3s;
    &:hover {
      color: #40a9ff;
      text-decoration: underline;
    }
    &:active {
      color: #096dd9;
    }
  }
  .file-item {
    &:last-child {
      border-bottom: none;
    }
  }
  .file-header {
    display: flex;
    align-items: center;
  }
  .preview-toggle {
    margin-left: 8px;
    color: #1890ff;
    cursor: pointer;
    font-size: 14px;
    user-select: none;
    &:hover {
      color: #40a9ff;
      text-decoration: underline;
    }
  }
  .docx-preview {
    margin-top: 12px;
    border: 1px solid #e8e8e8;
    border-radius: 4px;
    overflow: hidden;
    background: #fff;
  }
  .preview-not-supported {
    padding: 40px 20px;
    text-align: center;
    min-height: 200px;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .code-area {
    margin: 0;
    padding: 12px;
    background: #e2e8f0;
    color: #0f172a;
    border-radius: 4px;
    font-size: 13px;
    line-height: 1.6;
    white-space: pre-wrap;
    word-break: break-word;
    min-height: 120px;
    max-height: 600px;
    overflow: auto;
  }
</style>
src/views/demo/page/desc/editkehu/index.vue
@@ -174,9 +174,18 @@
    </a-card>
    <!-- 右下角操作按钮 -->
    <div class="action-buttons">
      <a-button type="danger" @click="handleStopService" class="stop-btn">停止服务</a-button>
      <a-button type="default" @click="handleReset" class="reset-btn">重置</a-button>
      <a-button type="primary" @click="handleSave" class="save-btn">再次提交</a-button>
      <!-- 返回按钮始终显示 -->
      <!-- 功能:返回上一页,与财务审核详情页面的返回按钮保持一致 -->
      <!-- 样式:与财务审核详情页面的返回按钮样式保持一致 -->
      <a-button type="default" @click="handleBack" class="back-btn">返回</a-button>
      <!-- 只有当服务未停止时显示停止服务按钮 -->
      <a-button v-if="!isServiceStopped" type="danger" @click="handleStopService" class="stop-btn">停止服务</a-button>
      <!-- 只有当服务已停止时显示恢复服务按钮 -->
      <a-button v-if="isServiceStopped" type="success" @click="handleResumeService" class="resume-btn">恢复服务</a-button>
      <!-- 重置按钮始终显示 -->
      <!-- <a-button type="default" @click="handleReset" class="reset-btn">重置</a-button> -->
      <!-- 只有当服务未停止时显示再次提交按钮 -->
      <a-button v-if="!isServiceStopped" type="primary" @click="handleSave" class="save-btn">再次提交</a-button>
    </div>
    
    <!-- 停止服务确认弹窗 -->
@@ -192,6 +201,22 @@
      <div style="padding: 20px 0; text-align: center;">
        <p style="font-size: 16px; margin-bottom: 20px;">是否确认停止服务?</p>
        <p style="color: #666; font-size: 14px;">停止服务后,相关服务将立即终止</p>
      </div>
    </a-modal>
    <!-- 恢复服务确认弹窗 -->
    <a-modal
      v-model:visible="resumeServiceModalVisible"
      title="恢复服务确认"
      width="500px"
      ok-text="确认恢复"
      cancel-text="取消"
      @ok="confirmResumeService"
      centered
    >
      <div style="padding: 20px 0; text-align: center;">
        <p style="font-size: 16px; margin-bottom: 20px;">是否确认恢复服务?</p>
        <p style="color: #666; font-size: 14px;">恢复服务后,相关服务将重新启用</p>
      </div>
    </a-modal>
  </PageWrapper>
@@ -220,6 +245,16 @@
      // 停止服务弹窗可见性
      const stopServiceModalVisible = ref(false);
      // 恢复服务弹窗可见性
      const resumeServiceModalVisible = ref(false);
      // 服务状态计算
      const isServiceStopped = computed(() => {
        // 根据formData中的服务状态判断
        // 假设formData中有一个字段表示服务状态,例如isDropService
        return formData.isDropService === '是';
      });
      // 原始数据,用于重置
      const originalData = ref<any>({});
@@ -261,6 +296,7 @@
        sysOrgCode: '',
        emergencyStatus: 3, // 默认正常状态
        month: null, // 合作周期(月)
        isDropService: '', // 服务状态,默认为空,表示服务中
      };
      // 响应式表单数据
@@ -663,6 +699,11 @@
        stopServiceModalVisible.value = true;
      };
      // 处理恢复服务按钮点击
      const handleResumeService = () => {
        resumeServiceModalVisible.value = true;
      };
      // 确认停止服务
      const confirmStopService = async () => {
        const id = route.query.id as string;
@@ -673,12 +714,16 @@
        try {
          // 调用后端停止服务接口
          // 添加successMessageMode: 'none'选项,防止显示默认的成功提示
          // 只显示自定义的提示信息,避免出现两条提示
          const result = await defHttp.post({
            url: '/contract/contract/dropService',
            data: { 
              id,
              isDropService: '是'
            },
          }, {
            successMessageMode: 'none' // 防止显示默认的成功提示,只显示自定义的提示信息
          });
          
          if (result) {
@@ -696,6 +741,53 @@
        }
      };
      // 确认恢复服务
      const confirmResumeService = async () => {
        const id = route.query.id as string;
        if (!id) {
          message.error('缺少合同ID参数');
          return;
        }
        try {
          // 调用后端恢复服务接口
          // 添加successMessageMode: 'none'选项,防止显示默认的成功提示
          // 只显示自定义的提示信息,避免出现两条提示
          const result = await defHttp.post({
            url: '/contract/contract/dropService',
            data: {
              id,
              isDropService: '否'
            },
          }, {
            successMessageMode: 'none' // 防止显示默认的成功提示,只显示自定义的提示信息
          });
          if (result) {
            message.success('服务已成功恢复');
            // 重新加载客户数据以更新状态
            await loadCustomerData();
          } else {
            message.error('恢复服务失败');
          }
        } catch (error) {
          console.error('恢复服务失败:', error);
          message.error('恢复服务失败');
        } finally {
          resumeServiceModalVisible.value = false;
        }
      };
      /**
       * 处理返回按钮点击
       * 功能:返回上一页,与财务审核详情页面的返回按钮保持一致
       * 修改原因:为了与财务审核详情页面的返回按钮行为保持一致,使用router.back()返回上一页
       * 而不是固定跳转到合同列表页面
       */
      const handleBack = () => {
        router.back();
      };
      return {
        isEditing,
        formData,
@@ -705,6 +797,8 @@
        displayAttachmentFiles,
        semanticWords,
        stopServiceModalVisible,
        resumeServiceModalVisible,
        isServiceStopped,
        beforeUpload,
        customUpload,
        handleFileChange,
@@ -712,6 +806,9 @@
        handleSave,
        handleStopService,
        confirmStopService,
        handleResumeService,
        confirmResumeService,
        handleBack,
        getFileName,
        resolveFileUrl,
        downloadFile,
@@ -729,59 +826,88 @@
  }
  .action-buttons {
    position: fixed;
    right: 24px;
    bottom: 24px;
    display: flex;
    gap: 12px;
    z-index: 1000;
      position: fixed;
      right: 24px;
      bottom: 24px;
      display: flex;
      gap: 12px;
      z-index: 1000;
    .reset-btn,
    .save-btn {
      min-width: 80px;
      height: 40px;
      border-radius: 6px;
      font-weight: 500;
    }
      .back-btn,
      .reset-btn,
      .save-btn {
        min-width: 80px;
        height: 40px;
        border-radius: 6px;
        font-weight: 500;
      }
    .reset-btn {
      background-color: #f5f5f5;
      border-color: #d9d9d9;
      color: #666;
      .back-btn {
        background-color: #f5f5f5;
        border-color: #d9d9d9;
        color: #666;
      &:hover {
        background-color: #e6f7ff;
        border-color: #40a9ff;
        color: #40a9ff;
        &:hover {
          background-color: #e6f7ff;
          border-color: #40a9ff;
          color: #40a9ff;
        }
      }
      .reset-btn {
        background-color: #f5f5f5;
        border-color: #d9d9d9;
        color: #666;
        &:hover {
          background-color: #e6f7ff;
          border-color: #40a9ff;
          color: #40a9ff;
        }
      }
      .save-btn {
        background-color: #1890ff;
        border-color: #1890ff;
        &:hover {
          background-color: #40a9ff;
          border-color: #40a9ff;
        }
      }
      .stop-btn {
        min-width: 80px;
        height: 40px;
        border-radius: 6px;
        font-weight: 500;
        background-color: #fff1f0;
        border-color: #ffccc7;
        color: #cf1322;
        &:hover {
          background-color: #fff2f0;
          border-color: #ff7875;
          color: #ff4d4f;
        }
      }
      .resume-btn {
        min-width: 80px;
        height: 40px;
        border-radius: 6px;
        font-weight: 500;
        background-color: #f6ffed;
        border-color: #b7eb8f;
        color: #52c41a;
        &:hover {
          background-color: #f6ffed;
          border-color: #95de64;
          color: #73d13d;
        }
      }
    }
    .save-btn {
      background-color: #1890ff;
      border-color: #1890ff;
      &:hover {
        background-color: #40a9ff;
        border-color: #40a9ff;
      }
    }
    .stop-btn {
      min-width: 80px;
      height: 40px;
      border-radius: 6px;
      font-weight: 500;
      background-color: #fff1f0;
      border-color: #ffccc7;
      color: #cf1322;
      &:hover {
        background-color: #fff2f0;
        border-color: #ff7875;
        color: #ff4d4f;
      }
    }
  }
  .file-row {
    margin-top: 4px;
src/views/semanticword/SemanticWord.api.ts
@@ -23,13 +23,18 @@
export const getImportUrl = Api.importExcel;
/**
 * 列表接口
 * @param params
 * @param params 查询参数
 * 添加successMessageMode: 'none'选项,防止显示默认的成功提示
 * 避免在测试环境出现徽章提示
 * createBy 参数已在 beforeFetch 中处理,这里直接传递参数
 * 如果 params 中没有 createBy,说明是管理员,不需要添加
 * 如果 params 中有 createBy,说明是非管理员,已由 beforeFetch 设置
 */
export const list = (params: any) => {
  // createBy 参数已在 beforeFetch 中处理,这里直接传递参数
  // 如果 params 中没有 createBy,说明是管理员,不需要添加
  // 如果 params 中有 createBy,说明是非管理员,已由 beforeFetch 设置
  return defHttp.get({ url: Api.list, params });
  return defHttp.get({ url: Api.list, params }, { successMessageMode: 'none' });
};
/**
src/views/semanticword/SemanticWord.data.ts
@@ -53,20 +53,23 @@
      //colProps: {span: 6},
      },
    {
      label: "状态",
      field: 'status',
      component: 'Select',
      componentProps:{
          // 仅保留以下四个状态作为查询选项
          options: [
            { label: '待审核', value: '1' },
            { label: '已通过', value: '3' },
            { label: '已驳回', value: '2' },
            { label: '已上词', value: '5' },
          ],
      },
      //colProps: {span: 6},
      },
    label: "状态",
    field: 'status',
    component: 'Select',
    componentProps:{
      // 保留以下五个状态作为查询选项
      // 添加"已掉词"选项,值为'6',用于查询已掉词的语义词数据
      // 点击查询时,会根据选择的状态筛选语义词数据
      options: [
        { label: '待审核', value: '1' },
        { label: '已通过', value: '3' },
        { label: '已驳回', value: '2' },
        { label: '已上词', value: '5' },
        { label: '已掉词', value: '6' }, // 新增"已掉词"选项,用于查询已掉词的语义词数据
      ],
    },
    //colProps: {span: 6},
  },
    // {
  //     label: "文案编辑",
  //     field: 'changer',