Browse Source

修改与优化

ai_dev
zhouhaibin 4 days ago
parent
commit
9d8920047c
  1. 195
      public/configs/contractTaskConfigs.json
  2. 250
      public/configs/documentTaskConfigs.json
  3. 186
      public/configs/tenderTaskConfigs.json
  4. 57
      src/api/contractReview/ContractualClauseConfig/index.ts
  5. 48
      src/api/contractReview/ContractualClauseConfig/model.ts
  6. 57
      src/api/contractReview/ContractualClauseType/index.ts
  7. 43
      src/api/contractReview/ContractualClauseType/model.ts
  8. 35
      src/api/contractReview/ContractualTasks/index.ts
  9. 265
      src/components/UniversalResultDrawer.vue
  10. 157
      src/configs/jsonConfigLoader.ts
  11. 119
      src/configs/taskConfigs.ts
  12. 924
      src/views/contractReview/ContractClauseConfig/index.vue
  13. 740
      src/views/contractReview/ContractualTasks/components/ConsistencyContent.vue
  14. 9
      src/views/contractReview/ContractualTasks/components/InferenceReview.vue
  15. 47
      src/views/tenderReview/TenderTask/TenderTask.data.ts
  16. 18
      src/views/tenderReview/TenderTask/index.vue

195
public/configs/contractTaskConfigs.json

@ -0,0 +1,195 @@
{
"contractSubstantiveReview": {
"taskType": "contractSubstantiveReview",
"name": "实质性审查",
"mode": "tabs",
"pdfConfig": {
"layout": "single",
"sources": [
{ "id": "contract", "title": "合同文件", "apiField": "id" }
]
},
"tabs": [
{
"key": "substantive",
"label": "实质性审查",
"pdfConfig": {
"layout": "single",
"sources": [
{ "id": "contract", "title": "合同文件", "apiField": "id" }
]
},
"fields": [
{
"field": "originalText",
"title": "原文",
"dataType": "string",
"displayType": "markdown",
"pdfSource": "contract",
"required": true
},
{
"field": "modifiedContent",
"title": "修改建议",
"dataType": "string",
"displayType": "markdown",
"showComparison": true,
"comparisonConfig": {
"comparisonField": "modificationDisplay",
"comparisonTitle": "修改情况展示",
"showButton": true
}
},
{
"field": "reviewBasis",
"title": "审查依据",
"dataType": "json",
"displayType": "markdown",
"jsonConfig": {
"extractFields": ["review_points", "review_content"],
"separator": ":"
}
}
]
}
]
},
"contractComplianceReview": {
"taskType": "contractComplianceReview",
"name": "合规性审查",
"mode": "tabs",
"pdfConfig": {
"layout": "single",
"sources": [
{ "id": "contract", "title": "合同文件", "apiField": "id" }
]
},
"tabs": [
{
"key": "compliance",
"label": "合规性审查",
"pdfConfig": {
"layout": "single",
"sources": [
{ "id": "contract", "title": "合同文件", "apiField": "id" }
]
},
"fields": [
{
"field": "originalText",
"title": "原文",
"dataType": "string",
"displayType": "markdown",
"pdfSource": "contract",
"required": true
},
{
"field": "reviewBasis",
"title": "法规依据",
"dataType": "json",
"displayType": "markdown",
"jsonConfig": {
"extractFields": ["review_points"],
"separator": ":",
"fieldProcessors": {
"review_points": "arrayJoin"
}
}
}
]
}
]
},
"contractConsistencyReview": {
"taskType": "contractConsistencyReview",
"name": "一致性审查",
"mode": "tabs",
"pdfConfig": {
"layout": "split",
"sources": [
{ "id": "contract", "title": "合同文件", "apiField": "id" },
{ "id": "bid", "title": "招标文件", "apiField": "id" }
]
},
"tabs": [
{
"key": "consistency",
"label": "一致性审查",
"pdfConfig": {
"layout": "split",
"sources": [
{ "id": "contract", "title": "合同文件", "apiField": "id" },
{ "id": "bid", "title": "招标文件", "apiField": "id" }
]
},
"fields": [
{
"field": "originalText",
"title": "合同原文",
"dataType": "string",
"displayType": "markdown",
"pdfSource": "contract",
"required": true
},
{
"field": "comparedText",
"title": "招标文件对应内容",
"dataType": "string",
"displayType": "markdown",
"pdfSource": "bid",
"required": true
}
]
}
]
},
"contractReview": {
"taskType": "contractReview",
"name": "合同审查",
"mode": "single",
"pdfConfig": {
"layout": "single",
"sources": [
{ "id": "contract", "title": "合同文件", "apiField": "id" }
]
},
"fields": [
{
"field": "originalText",
"title": "原文",
"dataType": "string",
"displayType": "markdown",
"pdfSource": "contract",
"required": true
},
{
"field": "existingIssues",
"title": "存在问题",
"dataType": "string",
"displayType": "markdown"
},
{
"field": "modifiedContent",
"title": "修改建议",
"dataType": "string",
"displayType": "markdown",
"showComparison": true,
"comparisonConfig": {
"comparisonField": "modificationDisplay",
"comparisonTitle": "修改情况展示",
"showButton": true
}
},
{
"field": "reviewBasis",
"title": "审查依据",
"dataType": "json",
"displayType": "markdown",
"jsonConfig": {
"extractFields": ["review_points", "review_content"],
"separator": ":"
}
}
]
}
}

250
public/configs/documentTaskConfigs.json

@ -0,0 +1,250 @@
{
"checkDocumentError": {
"taskType": "checkDocumentError",
"name": "文字错误检查",
"mode": "single",
"pdfConfig": {
"layout": "single",
"sources": [
{ "id": "document", "title": "文档", "apiField": "id" }
]
},
"fields": [
{
"field": "originalText",
"title": "原文",
"dataType": "string",
"displayType": "markdown",
"pdfSource": "document",
"required": true
},
{
"field": "modifiedContent",
"title": "修改建议",
"dataType": "string",
"displayType": "markdown",
"showComparison": true,
"comparisonConfig": {
"comparisonField": "modificationDisplay",
"comparisonTitle": "修改情况展示",
"showButton": true
}
}
]
},
"checkRepeatText": {
"taskType": "checkRepeatText",
"name": "文档相似性检查",
"mode": "single",
"pdfConfig": {
"layout": "single",
"sources": [
{ "id": "document", "title": "文档", "apiField": "id" }
]
},
"fields": [
{
"field": "originalText",
"title": "第一段原文",
"dataType": "string",
"displayType": "markdown",
"pdfSource": "document"
},
{
"field": "comparedText",
"title": "第二段原文",
"dataType": "string",
"displayType": "markdown",
"pdfSource": "document"
},
{
"field": "modificationDisplay",
"title": "相似情况",
"dataType": "string",
"displayType": "markdown"
}
]
},
"allCheckRepeatText": {
"taskType": "allCheckRepeatText",
"name": "全文档相似性检查",
"mode": "single",
"pdfConfig": {
"layout": "single",
"sources": [
{ "id": "document", "title": "文档", "apiField": "id" }
]
},
"fields": [
{
"field": "originalText",
"title": "第一段原文",
"dataType": "string",
"displayType": "markdown",
"pdfSource": "document"
},
{
"field": "comparedText",
"title": "第二段原文",
"dataType": "string",
"displayType": "markdown",
"pdfSource": "document"
},
{
"field": "modificationDisplay",
"title": "相似情况",
"dataType": "string",
"displayType": "markdown"
}
]
},
"checkCompanyName": {
"taskType": "checkCompanyName",
"name": "公司名称检查",
"mode": "single",
"pdfConfig": {
"layout": "single",
"sources": [
{ "id": "document", "title": "文档", "apiField": "id" }
]
},
"fields": [
{
"field": "modificationDisplay",
"title": "相关原文",
"dataType": "string",
"displayType": "markdown",
"pdfSource": "document"
}
]
},
"checkTitleName": {
"taskType": "checkTitleName",
"name": "文档结构检查",
"mode": "single",
"pdfConfig": {
"layout": "single",
"sources": [
{ "id": "document", "title": "文档", "apiField": "id" }
]
},
"fields": [
{
"field": "modificationDisplay",
"title": "结构检查情况",
"dataType": "string",
"displayType": "markdown"
}
]
},
"checkPlaceName": {
"taskType": "checkPlaceName",
"name": "地名检查",
"mode": "single",
"pdfConfig": {
"layout": "single",
"sources": [
{ "id": "document", "title": "文档", "apiField": "id" }
]
},
"fields": [
{
"field": "modificationDisplay",
"title": "相关原文",
"dataType": "string",
"displayType": "markdown",
"pdfSource": "document"
}
]
},
"policyBases": {
"taskType": "policyBases",
"name": "建设依据检查",
"mode": "single",
"pdfConfig": {
"layout": "single",
"sources": [
{ "id": "document", "title": "文档", "apiField": "id" }
]
},
"fields": [
{
"field": "originalText",
"title": "系统名称",
"dataType": "string",
"displayType": "markdown",
"pdfSource": "document"
},
{
"field": "reviewBasis",
"title": "系统描述",
"dataType": "json",
"displayType": "markdown",
"jsonConfig": {
"extractFields": ["review_points", "review_content"],
"separator": ":",
"fieldProcessors": {
"review_points": "extractFirst"
}
}
}
]
},
"tenderReview": {
"taskType": "tenderReview",
"name": "投标文件审核",
"mode": "single",
"pdfConfig": {
"layout": "single",
"sources": [
{ "id": "document", "title": "文档", "apiField": "id" }
]
},
"fields": [
{
"field": "issueName",
"title": "合规性分类-具体问题(示例:资质审查-业绩要求)",
"dataType": "string",
"displayType": "markdown",
"pdfSource": "document"
},
{
"field": "originalText",
"title": "原文",
"dataType": "string",
"displayType": "markdown",
"pdfSource": "document"
},
{
"field": "reviewBasis",
"title": "相关法律法规政策",
"dataType": "json",
"displayType": "markdown",
"jsonConfig": {
"extractFields": ["review_points"],
"separator": ":",
"fieldProcessors": {
"review_points": "arrayJoinNewLine"
}
}
},
{
"field": "modifiedContent",
"title": "修改建议",
"dataType": "string",
"displayType": "markdown",
"pdfSource": "document"
},
{
"field": "reviewBasis",
"title": "风险等级(红色预警:直接废标项,橙色风险:可能引发投诉项)",
"dataType": "json",
"displayType": "markdown",
"jsonConfig": {
"extractFields": ["risk_level"],
"separator": ":"
}
}
]
}
}

186
public/configs/tenderTaskConfigs.json

@ -0,0 +1,186 @@
{
"bidConsistencyReview": {
"taskType": "bidConsistencyReview",
"name": "投标文件综合分析",
"mode": "tabs",
"tabs": [
{
"key": "文档元数据",
"label": "文档元数据",
"fields": [
{
"field": "originalText",
"title": "文档名称",
"dataType": "string",
"displayType": "markdown",
"pdfSource": "bid",
"required": true
},
{
"field": "reviewBasis",
"title": "详细元数据信息",
"dataType": "json",
"displayType": "markdown",
"jsonConfig": {
"extractFields": ["review_content", "review_points"],
"separator": ":",
"fieldProcessors": {
"review_points": "arrayJoinNewLine"
}
}
}
]
},
{
"key": "重点关注相似内容",
"label": "重点关注相似内容",
"fields": [
{
"field": "reviewBasis",
"title": "各文档具体内容",
"dataType": "json",
"displayType": "markdown",
"jsonConfig": {
"extractFields": ["review_points"],
"separator": ":",
"fieldProcessors": {
"review_points": "arrayJoinNewLine"
}
},
"showComparison": true,
"comparisonConfig": {
"comparisonField": "modificationDisplay",
"comparisonTitle": "修改情况展示",
"showButton": true
}
},
{
"field": "reviewBasis",
"title": "错别字",
"dataType": "json",
"displayType": "markdown",
"jsonConfig": {
"extractFields": ["error_review_points"],
"separator": ":",
"fieldProcessors": {
"error_review_points": "arrayJoinNewLine"
}
}
}
]
},
{
"key": "错别字",
"label": "错别字",
"fields": [
{
"field": "originalText",
"title": "文档1原文",
"dataType": "string",
"displayType": "markdown",
"pdfSource": "bid",
"required": true
},
{
"field": "comparedText",
"title": "文档2原文",
"dataType": "string",
"displayType": "markdown",
"pdfSource": "bid",
"required": true
},
{
"field": "reviewBasis",
"title": "错误详情",
"dataType": "json",
"displayType": "markdown",
"jsonConfig": {
"extractFields": ["review_content", "review_points"],
"separator": ":",
"fieldProcessors": {
"review_points": "arrayJoin"
}
}
}
]
},
{
"key": "一般相似内容",
"label": "一般相似内容",
"fields": [
{
"field": "modificationDisplay",
"title": "涉及文档",
"dataType": "string",
"displayType": "markdown"
},
{
"field": "reviewBasis",
"title": "各文档具体内容",
"dataType": "json",
"displayType": "markdown",
"jsonConfig": {
"extractFields": ["review_content", "review_points"],
"separator": ":",
"fieldProcessors": {
"review_points": "arrayJoinNewLine"
}
}
}
]
}
]
},
"tenderComplianceReview": {
"taskType": "tenderComplianceReview",
"name": "招投标合规性审查",
"mode": "single",
"fields": [
{
"field": "originalText",
"title": "原文",
"dataType": "string",
"displayType": "markdown",
"pdfSource": "bid",
"required": true
},
{
"field": "reviewBasis",
"title": "审查意见",
"dataType": "json",
"displayType": "markdown",
"jsonConfig": {
"extractFields": ["review_content"],
"separator": ":"
}
},
{
"field": "reviewBasis",
"title": "审查要点",
"dataType": "json",
"displayType": "markdown",
"jsonConfig": {
"extractFields": ["review_points"],
"separator": ":",
"fieldProcessors": {
"review_points": "arrayJoinNewLine"
}
}
},
{
"field": "modifiedContent",
"title": "修改建议",
"dataType": "string",
"displayType": "markdown"
},
{
"field": "modificationDisplay",
"title": "修改情况",
"dataType": "string",
"displayType": "markdown"
}
]
}
}

57
src/api/contractReview/ContractualClauseConfig/index.ts

@ -0,0 +1,57 @@
import { defHttp } from '@/utils/http/axios';
import { ID, IDS, commonExport } from '@/api/base';
import { ContractualClauseConfigVO, ContractualClauseConfigForm, ContractualClauseConfigQuery } from './model';
/**
*
* @param params
* @returns
*/
export function ContractualClauseConfigList(params?: ContractualClauseConfigQuery) {
return defHttp.get<ContractualClauseConfigVO[]>({ url: '/contractreview/contractualClauseConfig/list', params });
}
/**
*
* @param params
* @returns
*/
export function ContractualClauseConfigExport(params?: ContractualClauseConfigQuery) {
return commonExport('/contractreview/contractualClauseConfig/export', params ?? {});
}
/**
*
* @param id id
* @returns
*/
export function ContractualClauseConfigInfo(id: ID) {
return defHttp.get<ContractualClauseConfigVO>({ url: '/contractreview/contractualClauseConfig/' + id });
}
/**
*
* @param data
* @returns
*/
export function ContractualClauseConfigAdd(data: ContractualClauseConfigForm) {
return defHttp.postWithMsg<void>({ url: '/contractreview/contractualClauseConfig', data });
}
/**
*
* @param data
* @returns
*/
export function ContractualClauseConfigUpdate(data: ContractualClauseConfigForm) {
return defHttp.putWithMsg<void>({ url: '/contractreview/contractualClauseConfig', data });
}
/**
*
* @param id id
* @returns
*/
export function ContractualClauseConfigRemove(id: ID | IDS) {
return defHttp.deleteWithMsg<void>({ url: '/contractreview/contractualClauseConfig/' + id });
}

48
src/api/contractReview/ContractualClauseConfig/model.ts

@ -0,0 +1,48 @@
/**
*
*/
export interface ContractualClauseConfigQuery {
pageNum?: number;
pageSize?: number;
contractualClauseTypeId?: number;
clauseName?: string;
isRequired?: boolean;
status?: number;
orderByColumn?: string;
isAsc?: string;
}
/**
*
*/
export interface ContractualClauseConfigVO {
id?: number;
contractualClauseTypeId?: number;
clauseName?: string;
clauseDescription?: string;
isRequired?: boolean;
keywords?: string;
sortOrder?: number;
status?: number;
tenantId?: string;
delFlag?: string;
createDept?: number;
createBy?: number;
createTime?: string;
updateBy?: number;
updateTime?: string;
}
/**
*
*/
export interface ContractualClauseConfigForm {
id?: number;
contractualClauseTypeId?: number;
clauseName?: string;
clauseDescription?: string;
isRequired?: boolean;
keywords?: string;
sortOrder?: number;
status?: number;
}

57
src/api/contractReview/ContractualClauseType/index.ts

@ -0,0 +1,57 @@
import { defHttp } from '@/utils/http/axios';
import { ID, IDS, commonExport } from '@/api/base';
import { ContractualClauseTypeVO, ContractualClauseTypeForm, ContractualClauseTypeQuery } from './model';
/**
*
* @param params
* @returns
*/
export function ContractualClauseTypeList(params?: ContractualClauseTypeQuery) {
return defHttp.get<ContractualClauseTypeVO[]>({ url: '/contractreview/contractualClauseType/list', params });
}
/**
*
* @param params
* @returns
*/
export function ContractualClauseTypeExport(params?: ContractualClauseTypeQuery) {
return commonExport('/contractreview/contractualClauseType/export', params ?? {});
}
/**
*
* @param id id
* @returns
*/
export function ContractualClauseTypeInfo(id: ID) {
return defHttp.get<ContractualClauseTypeVO>({ url: '/contractreview/contractualClauseType/' + id });
}
/**
*
* @param data
* @returns
*/
export function ContractualClauseTypeAdd(data: ContractualClauseTypeForm) {
return defHttp.postWithMsg<void>({ url: '/contractreview/contractualClauseType', data });
}
/**
*
* @param data
* @returns
*/
export function ContractualClauseTypeUpdate(data: ContractualClauseTypeForm) {
return defHttp.putWithMsg<void>({ url: '/contractreview/contractualClauseType', data });
}
/**
*
* @param id id
* @returns
*/
export function ContractualClauseTypeRemove(id: ID | IDS) {
return defHttp.deleteWithMsg<void>({ url: '/contractreview/contractualClauseType/' + id });
}

43
src/api/contractReview/ContractualClauseType/model.ts

@ -0,0 +1,43 @@
/**
*
*/
export interface ContractualClauseTypeQuery {
pageNum?: number;
pageSize?: number;
typeName?: string;
typeCode?: string;
status?: number;
orderByColumn?: string;
isAsc?: string;
}
/**
*
*/
export interface ContractualClauseTypeVO {
id?: number;
typeName?: string;
typeCode?: string;
description?: string;
sortOrder?: number;
status?: number;
tenantId?: string;
delFlag?: string;
createDept?: number;
createBy?: number;
createTime?: string;
updateBy?: number;
updateTime?: string;
}
/**
*
*/
export interface ContractualClauseTypeForm {
id?: number;
typeName?: string;
typeCode?: string;
description?: string;
sortOrder?: number;
status?: number;
}

35
src/api/contractReview/ContractualTasks/index.ts

@ -1,6 +1,8 @@
import { defHttp } from '@/utils/http/axios';
import { ID, IDS, commonExport } from '@/api/base';
import { ContractualTasksVO, ContractualTasksForm, ContractualTasksQuery, StartContractReviewRequest } from './model';
import { UploadFileParams } from '#/axios';
import { AxiosProgressEvent } from 'axios';
/**
*
@ -65,7 +67,11 @@ export function AnalyzeContract(ossId: string) {
return defHttp.get<any>({ url: '/contractreview/contractualTasks/analyzeContract', params: { ossId } });
}
/**
*
*
* @param data
*/
export function StartReview(data: StartContractReviewRequest) {
return defHttp.postWithMsg<any>({
url: '/contractreview/contractualTasks/startReview',
@ -74,5 +80,32 @@ export function StartReview(data: StartContractReviewRequest) {
});
}
/**
*
* @description: Upload contract document
*/
export function uploadContract(
params: UploadFileParams,
onUploadProgress?: (progressEvent: AxiosProgressEvent) => void,
) {
return defHttp.uploadFile<any>(
{
url: '/contractreview/contractualTasks/upload',
onUploadProgress,
timeout: 1000 * 60 * 10, // 10分钟超时
},
params,
);
}
/**
*
* @param ossId OSS文件ID
* @returns
*/
export function ContractFileRemove(ossId: string) {
return defHttp.deleteWithMsg<void>({ url: '/contractreview/contractualTasks/ossRemoveById/' + ossId });
}
// 导出类型定义
export type { StartContractReviewRequest } from './model';

265
src/components/UniversalResultDrawer.vue

@ -10,9 +10,9 @@
:destroyOnClose="true"
>
<div class="universal-review-container">
<div class="split-layout">
<div class="split-layout" :class="{ 'no-pdf-layout': !pdfLayoutConfig }">
<!-- 左侧PDF预览 -->
<div class="pdf-preview-wrapper" v-if="config?.pdfConfig && pdfLayoutConfig">
<div class="pdf-preview-wrapper" v-if="pdfLayoutConfig">
<ReviewPdfContainer
ref="pdfContainerRef"
:config="pdfLayoutConfig"
@ -24,9 +24,9 @@
</div>
<!-- 右侧审核结果 -->
<div class="content-container">
<div class="content-container" :class="{ 'full-width': !pdfLayoutConfig }">
<!-- 多标签模式 -->
<div v-if="config?.mode === 'tabs'" class="tabs-container">
<div v-if="config && config.mode === 'tabs'" class="tabs-container">
<div class="review-type-tabs" v-if="availableTabs.length > 0">
<Tabs v-model:activeKey="currentTab" @change="handleTabChange">
<TabPane
@ -331,6 +331,7 @@
import ReviewPdfContainer from '@/components/Preview/src/PdfPreview/ReviewPdfContainer.vue';
import {
getTaskConfig,
getTaskConfigSync,
shouldShowComparison,
getFieldPdfSource,
supportsPdfLocation,
@ -406,7 +407,17 @@
});
//
const config = computed(() => getTaskConfig(props.taskType));
const config = ref<TaskConfig | null>(null);
//
const loadConfig = async () => {
try {
config.value = await getTaskConfig(props.taskType);
} catch (error) {
console.error('加载配置失败,使用同步配置:', error);
config.value = getTaskConfigSync(props.taskType);
}
};
//
const activeCollapseKeys = ref<string[]>(['0']);
@ -447,7 +458,10 @@
}
}
if (!pdfConfig) return null;
// pdfConfig
if (!pdfConfig || !pdfConfig.layout || !pdfConfig.sources || pdfConfig.sources.length === 0) {
return null;
}
// ReviewTypeConfig
return {
@ -459,36 +473,95 @@
} as any;
});
//
//
const availableTabs = computed(() => {
if (config.value?.mode !== 'tabs' || !props.taskResultDetail || props.taskResultDetail.length === 0) {
if (!config.value || config.value.mode !== 'tabs' || !props.taskResultDetail || props.taskResultDetail.length === 0) {
return [];
}
// 使
const dataToProcess = props.taskResultDetail;
// tabs退
if (!config.value.tabs || config.value.tabs.length === 0) {
console.warn('配置中未定义 tabs,使用数据生成标签页');
return props.taskResultDetail.map((category, index) => {
let label = `${category.name} (${category.results?.length || 0})`;
//
if (category.name === '实质性审查' && props.taskInfo?.contractPartyRole) {
label = `${props.taskInfo.contractPartyRole})实质性审查 (${category.results?.length || 0})`;
}
return {
key: category.name,
label: label,
dataIndex: index,
name: category.name
};
});
}
// tabs
const availableTabsWithData: any[] = [];
//
return dataToProcess.map((category, index) => {
let label = `${category.name} (${category.results?.length || 0})`;
config.value.tabs.forEach((tabConfig, configIndex) => {
//
const correspondingDataCategory = props.taskResultDetail.find(category => {
//
const matches = [
category.name === tabConfig.label,
category.name === tabConfig.key,
category.name.includes(tabConfig.label),
tabConfig.label.includes(category.name),
//
category.name.replace(/[()\(\)\s]/g, '') === tabConfig.label.replace(/[()\(\)\s]/g, ''),
//
(category.name.includes('审查') && tabConfig.label.includes('审查') &&
(category.name.includes(tabConfig.label.replace('审查', '')) ||
tabConfig.label.includes(category.name.replace('审查', ''))))
];
const isMatch = matches.some(Boolean);
if (isMatch) {
console.log(`匹配成功: 数据"${category.name}" <-> 配置"${tabConfig.label}" (key: ${tabConfig.key})`);
}
return isMatch;
});
//
if (category.name === '实质性审查' && props.taskInfo?.contractPartyRole) {
label = `${props.taskInfo.contractPartyRole})实质性审查 (${category.results?.length || 0})`;
if (correspondingDataCategory) {
let label = `${correspondingDataCategory.name} (${correspondingDataCategory.results?.length || 0})`;
//
if (correspondingDataCategory.name === '实质性审查' && props.taskInfo?.contractPartyRole) {
label = `${props.taskInfo.contractPartyRole})实质性审查 (${correspondingDataCategory.results?.length || 0})`;
}
//
const dataIndex = props.taskResultDetail.findIndex(category => category === correspondingDataCategory);
availableTabsWithData.push({
key: correspondingDataCategory.name, // 使namekey
label: label,
dataIndex: dataIndex,
name: correspondingDataCategory.name,
configIndex: configIndex, //
tabConfig: tabConfig // tab
});
} else {
console.log(`未找到匹配的数据分类: 配置"${tabConfig.label}" (key: ${tabConfig.key})`);
}
return {
key: category.name, // 使namekey
label: label,
dataIndex: index, //
name: category.name
};
});
//
availableTabsWithData.sort((a, b) => a.configIndex - b.configIndex);
console.log('Generated tabs based on config order:', availableTabsWithData.map(tab =>
`${tab.configIndex}: ${tab.label}`));
return availableTabsWithData;
});
//
const filteredResults = computed(() => {
if (config.value?.mode !== 'tabs' || !props.taskResultDetail || !currentTab.value) {
if (!config.value || config.value.mode !== 'tabs' || !props.taskResultDetail || !currentTab.value) {
return [];
}
@ -508,7 +581,7 @@
//
const filteredTaskResultDetail = computed(() => {
if (config.value?.mode === 'tabs') return [];
if (!config.value || config.value.mode === 'tabs') return [];
if (props.taskResultDetail.length <= 0) {
return [];
@ -524,25 +597,104 @@
return props.taskResultDetail;
});
//
const hasValidData = (value: any, fieldConfig?: FieldConfig): boolean => {
if (value === null || value === undefined) {
return false;
}
if (typeof value === 'string') {
return value.trim() !== '';
}
if (Array.isArray(value)) {
return value.length > 0;
}
if (typeof value === 'object') {
// JSONextractFields
if (fieldConfig && fieldConfig.dataType === 'json' && fieldConfig.jsonConfig?.extractFields) {
const extractFields = fieldConfig.jsonConfig.extractFields;
//
return extractFields.some((fieldName: string) => {
const fieldValue = value[fieldName];
if (!fieldValue) return false;
//
let processedValue = fieldValue;
if (fieldConfig.jsonConfig?.fieldProcessors && fieldConfig.jsonConfig.fieldProcessors[fieldName]) {
const processor = fieldConfig.jsonConfig.fieldProcessors[fieldName];
//
if (typeof processor === 'function') {
processedValue = processor(fieldValue);
}
//
else if (typeof processor === 'string') {
if (processor === 'arrayJoinNewLine' && Array.isArray(fieldValue)) {
processedValue = fieldValue.length > 0 ? fieldValue.join('\n') : '';
} else if (processor === 'arrayJoin' && Array.isArray(fieldValue)) {
processedValue = fieldValue.length > 0 ? fieldValue.join(', ') : '';
} else if (processor === 'extractFirst' && Array.isArray(fieldValue)) {
processedValue = fieldValue.length > 0 ? fieldValue[0] : '';
}
}
} else {
//
if (Array.isArray(fieldValue) && fieldValue.length > 0) {
processedValue = fieldValue.join('\n');
}
}
return hasValidData(processedValue);
});
}
//
return Object.keys(value).length > 0 && Object.values(value).some(v => hasValidData(v));
}
return true;
};
//
const getContentSections = (item: TaskResultItem): FieldConfig[] => {
if (config.value?.mode === 'tabs') {
// tab
// namelabel
const currentTabConfig = config.value.tabs?.find(tab => tab.label === currentTab.value);
if (!config.value) return [];
if (config.value.mode === 'tabs') {
// 使
let currentTabConfig: any = null;
// availableTabs
const currentTabData = availableTabs.value.find(tab => tab.key === currentTab.value);
if (currentTabData && currentTabData.tabConfig) {
currentTabConfig = currentTabData.tabConfig;
} else {
// 退
currentTabConfig = config.value.tabs?.find(tab =>
tab.label === currentTab.value ||
tab.key === currentTab.value ||
tab.label.includes(currentTab.value) ||
currentTab.value.includes(tab.label)
);
}
return (currentTabConfig?.fields || []).filter(field => {
if (field.displayCondition) {
return field.displayCondition(item);
}
return getItemValue(item, field.field);
const fieldValue = getItemValue(item, field.field);
return hasValidData(fieldValue, field);
});
} else {
// 使
return (config.value?.fields || []).filter(field => {
return (config.value.fields || []).filter(field => {
if (field.displayCondition) {
return field.displayCondition(item);
}
return getItemValue(item, field.field);
const fieldValue = getItemValue(item, field.field);
return hasValidData(fieldValue, field);
});
}
};
@ -619,11 +771,36 @@
//
if (jsonConfig.fieldProcessors && jsonConfig.fieldProcessors[fieldName]) {
fieldValue = jsonConfig.fieldProcessors[fieldName](fieldValue);
const processor = jsonConfig.fieldProcessors[fieldName];
//
if (typeof processor === 'function') {
fieldValue = processor(fieldValue);
}
//
else if (typeof processor === 'string') {
if (processor === 'arrayJoinNewLine' && Array.isArray(fieldValue)) {
fieldValue = fieldValue.join('\n');
} else if (processor === 'arrayJoin' && Array.isArray(fieldValue)) {
fieldValue = fieldValue.join(', ');
} else if (processor === 'extractFirst' && Array.isArray(fieldValue)) {
fieldValue = fieldValue.length > 0 ? fieldValue[0] : '';
} else {
//
if (Array.isArray(fieldValue) && fieldValue.length > 0) {
fieldValue = fieldValue.join('\n');
}
}
} else {
//
if (Array.isArray(fieldValue) && fieldValue.length > 0) {
fieldValue = fieldValue.join('\n');
}
}
} else {
//
//
if (Array.isArray(fieldValue) && fieldValue.length > 0) {
fieldValue = fieldValue[0];
fieldValue = fieldValue.join('\n');
}
}
@ -632,7 +809,6 @@
}
}
});
return renderMarkdown(values.join(separator));
};
@ -673,7 +849,7 @@
//
const updateActiveItemKeys = () => {
const newActiveKeys: string[] = [];
const dataToProcess = config.value?.mode === 'tabs' ? filteredResults.value : filteredTaskResultDetail.value;
const dataToProcess = config.value && config.value.mode === 'tabs' ? filteredResults.value : filteredTaskResultDetail.value;
dataToProcess.forEach((category: TaskResultCategory, categoryIndex: number) => {
if (category?.results) {
@ -736,7 +912,7 @@
await props.updateResultItemStatus(id, field, value);
//
const dataToUpdate = config.value?.mode === 'tabs' ? props.taskResultDetail : filteredTaskResultDetail.value;
const dataToUpdate = config.value && config.value.mode === 'tabs' ? props.taskResultDetail : filteredTaskResultDetail.value;
dataToUpdate.forEach((category: TaskResultCategory) => {
if (category?.results) {
category.results.forEach((item: TaskResultItem) => {
@ -974,8 +1150,11 @@
const initData = async () => {
loading.value = true;
try {
//
await loadConfig();
if (props.taskResultDetail) {
if (config.value?.mode === 'tabs' && availableTabs.value.length > 0) {
if (config.value && config.value.mode === 'tabs' && availableTabs.value.length > 0) {
// 使tab
currentTab.value = availableTabs.value[0].key;
} else {
@ -1336,6 +1515,10 @@
overflow: hidden;
}
.split-layout.no-pdf-layout {
justify-content: center;
}
.pdf-preview-wrapper {
width: 50%;
height: 100%;
@ -1350,6 +1533,10 @@
flex-direction: column;
}
.content-container.full-width {
width: 100%;
}
.review-type-tabs {
flex-shrink: 0;
border-bottom: 1px solid #e8e8e8;

157
src/configs/jsonConfigLoader.ts

@ -0,0 +1,157 @@
// JSON配置加载器 - 支持从public目录动态加载配置文件
import type { TaskConfig } from './taskConfigTypes';
// 缓存已加载的配置
const configCache = new Map<string, Record<string, TaskConfig>>();
/**
* -
*/
const FIELD_PROCESSORS: Record<string, (value: any) => string> = {
arrayFirst: (value) => Array.isArray(value) && value.length > 0 ? value[0] : value,
arrayJoin: (value) => Array.isArray(value) ? value.join(', ') : value,
arrayJoinNewLine: (value) => Array.isArray(value) ? value.join('\n') : value,
extractFirst: (value) => Array.isArray(value) && value.length > 0 ? value[0] : value,
};
/**
* JSON配置中的字段处理器
*/
function processFieldProcessors(config: any): any {
if (!config) return config;
if (Array.isArray(config)) {
return config.map(processFieldProcessors);
}
if (typeof config === 'object') {
const processed: any = {};
for (const [key, value] of Object.entries(config)) {
if (key === 'fieldProcessors' && typeof value === 'object') {
// 转换字符串处理器为实际函数
const processors: Record<string, (value: any) => string> = {};
for (const [fieldName, processorName] of Object.entries(value as Record<string, string>)) {
if (typeof processorName === 'string' && FIELD_PROCESSORS[processorName]) {
processors[fieldName] = FIELD_PROCESSORS[processorName];
}
}
processed[key] = processors;
} else {
processed[key] = processFieldProcessors(value);
}
}
return processed;
}
return config;
}
/**
* public目录加载JSON配置文件
*/
async function loadJsonConfig(fileName: string): Promise<Record<string, TaskConfig> | null> {
try {
// 检查缓存
if (configCache.has(fileName)) {
return configCache.get(fileName)!;
}
// 从public目录加载配置文件
const response = await fetch(`/configs/${fileName}`);
if (!response.ok) {
console.warn(`配置文件 ${fileName} 加载失败:`, response.status, response.statusText);
return null;
}
const rawConfig = await response.json();
// 处理字段处理器
const processedConfig = processFieldProcessors(rawConfig);
// 缓存配置
configCache.set(fileName, processedConfig);
console.log(`成功加载JSON配置文件: ${fileName}`);
return processedConfig;
} catch (error) {
console.warn(`加载JSON配置文件 ${fileName} 时出错:`, error);
return null;
}
}
/**
* -
*/
export function clearConfigCache(): void {
configCache.clear();
console.log('配置缓存已清空');
}
/**
*
*/
export async function reloadConfig(fileName: string): Promise<Record<string, TaskConfig> | null> {
configCache.delete(fileName);
return await loadJsonConfig(fileName);
}
/**
*
*/
export async function loadDocumentTaskConfigs(): Promise<Record<string, TaskConfig> | null> {
return await loadJsonConfig('documentTaskConfigs.json');
}
/**
*
*/
export async function loadContractTaskConfigs(): Promise<Record<string, TaskConfig> | null> {
return await loadJsonConfig('contractTaskConfigs.json');
}
/**
*
*/
export async function loadTenderTaskConfigs(): Promise<Record<string, TaskConfig> | null> {
return await loadJsonConfig('tenderTaskConfigs.json');
}
/**
*
*/
function validateTaskConfig(config: any): boolean {
if (!config || typeof config !== 'object') return false;
const requiredFields = ['taskType', 'name', 'mode'];
for (const field of requiredFields) {
if (!config[field]) return false;
}
if (config.mode === 'tabs' && !config.tabs) return false;
if (config.mode === 'single' && !config.fields) return false;
return true;
}
/**
* JSON配置是否可用
*/
export async function isJsonConfigAvailable(): Promise<boolean> {
try {
const response = await fetch('/configs/documentTaskConfigs.json', { method: 'HEAD' });
return response.ok;
} catch {
return false;
}
}
// 导出配置加载函数
export {
loadJsonConfig,
validateTaskConfig
};

119
src/configs/taskConfigs.ts

@ -4,6 +4,13 @@ import type { TaskConfig, FieldConfig } from './taskConfigTypes';
import { DOCUMENT_TASK_CONFIGS, getDocumentTaskConfig } from './documentTaskConfigs';
import { CONTRACT_TASK_CONFIGS, getContractTaskConfig } from './contractTaskConfigs';
import { TENDER_TASK_CONFIGS, getTenderTaskConfig } from './tenderTaskConfigs';
import {
loadDocumentTaskConfigs,
loadContractTaskConfigs,
loadTenderTaskConfigs,
isJsonConfigAvailable,
clearConfigCache
} from './jsonConfigLoader';
// 合并所有任务配置
const ALL_TASK_CONFIGS: Record<string, TaskConfig> = {
@ -12,8 +19,73 @@ const ALL_TASK_CONFIGS: Record<string, TaskConfig> = {
...TENDER_TASK_CONFIGS
};
// 统一的获取任务配置函数
export function getTaskConfig(taskType: string): TaskConfig | null {
// JSON配置缓存
let jsonConfigsLoaded = false;
let jsonDocumentConfigs: Record<string, TaskConfig> | null = null;
let jsonContractConfigs: Record<string, TaskConfig> | null = null;
let jsonTenderConfigs: Record<string, TaskConfig> | null = null;
/**
* JSON配置加载
*/
async function initJsonConfigs(): Promise<void> {
if (jsonConfigsLoaded) return;
try {
// 检查JSON配置是否可用
const isAvailable = await isJsonConfigAvailable();
if (!isAvailable) {
console.log('JSON配置文件不可用,使用默认TypeScript配置');
jsonConfigsLoaded = true;
return;
}
// 并行加载所有JSON配置文件
const [docConfigs, contractConfigs, tenderConfigs] = await Promise.all([
loadDocumentTaskConfigs(),
loadContractTaskConfigs(),
loadTenderTaskConfigs()
]);
jsonDocumentConfigs = docConfigs;
jsonContractConfigs = contractConfigs;
jsonTenderConfigs = tenderConfigs;
jsonConfigsLoaded = true;
const loadedCount = [docConfigs, contractConfigs, tenderConfigs].filter(Boolean).length;
console.log(`成功加载 ${loadedCount} 个JSON配置文件`);
} catch (error) {
console.error('加载JSON配置时出错:', error);
jsonConfigsLoaded = true; // 设置为已加载,避免重复尝试
}
}
/**
* JSON配置中获取任务配置
*/
function getJsonTaskConfig(taskType: string): TaskConfig | null {
// 依次检查各个JSON配置
if (jsonDocumentConfigs?.[taskType]) {
return jsonDocumentConfigs[taskType];
}
if (jsonContractConfigs?.[taskType]) {
return jsonContractConfigs[taskType];
}
if (jsonTenderConfigs?.[taskType]) {
return jsonTenderConfigs[taskType];
}
return null;
}
/**
* TypeScript配置中获取任务配置
*/
function getTypescriptTaskConfig(taskType: string): TaskConfig | null {
// 首先尝试从文档任务配置中获取
let config = getDocumentTaskConfig(taskType);
if (config) return config;
@ -29,6 +101,49 @@ export function getTaskConfig(taskType: string): TaskConfig | null {
return null;
}
// 统一的获取任务配置函数
export async function getTaskConfig(taskType: string): Promise<TaskConfig | null> {
// 确保JSON配置已初始化
await initJsonConfigs();
// 优先从JSON配置获取
const jsonConfig = getJsonTaskConfig(taskType);
if (jsonConfig) {
console.log(`使用JSON配置: ${taskType}`);
return jsonConfig;
}
// 回退到TypeScript配置
const tsConfig = getTypescriptTaskConfig(taskType);
if (tsConfig) {
console.log(`使用TypeScript配置: ${taskType}`);
return tsConfig;
}
return null;
}
/**
*
* TypeScript配置
*/
export function getTaskConfigSync(taskType: string): TaskConfig | null {
console.warn('使用同步配置获取,建议迁移到 getTaskConfig 异步版本');
return getTypescriptTaskConfig(taskType);
}
/**
* JSON配置
*/
export async function reloadJsonConfigs(): Promise<void> {
clearConfigCache();
jsonConfigsLoaded = false;
jsonDocumentConfigs = null;
jsonContractConfigs = null;
jsonTenderConfigs = null;
await initJsonConfigs();
}
// 获取所有可用配置
export function getAvailableConfigs(): TaskConfig[] {
return Object.values(ALL_TASK_CONFIGS);

924
src/views/contractReview/ContractClauseConfig/index.vue

@ -0,0 +1,924 @@
<template>
<PageWrapper dense>
<div class="container">
<!-- 左侧合同类型 -->
<div class="contract-type-sidebar w-64 border-r border-gray-200 p-4">
<div class="sidebar-title font-bold text-lg mb-4">合同类型</div>
<Button type="primary" class="w-full mb-4" @click="handleCreateType">
<template #icon><PlusOutlined /></template>
新建类型
</Button>
<div class="type-list overflow-y-auto">
<div
v-for="contractType in contractTypes"
:key="contractType.id"
:class="['type-item p-3 cursor-pointer rounded mb-2 flex justify-between',
{ 'selected': selectedTypeId === contractType.id }]"
>
<div class="flex-1" @click="selectContractType(contractType.id)">
<div class="flex justify-between items-center">
<div class="type-name font-medium">{{ contractType.typeName }}</div>
<div class="text-xs text-gray-400" v-if="clauseCounts.get(contractType.id || 0)">
{{ clauseCounts.get(contractType.id || 0) }}条款
</div>
</div>
<div class="type-code text-sm text-gray-500">{{ contractType.typeCode }}</div>
</div>
<div class="flex space-x-1">
<Button
type="text"
size="small"
@click.stop="handleEditType(contractType)"
>
<template #icon><EditOutlined /></template>
</Button>
<Button
type="text"
size="small"
danger
@click.stop="confirmDeleteType(contractType)"
v-if="contractTypes.length > 1"
>
<template #icon><DeleteOutlined /></template>
</Button>
</div>
</div>
</div>
</div>
<!-- 右侧条款配置表格 -->
<div class="clause-config-content flex-1 p-4">
<div class="content-header flex justify-between items-center mb-4">
<div>
<h2 class="text-xl font-bold">合同条款配置</h2>
<div class="text-sm text-gray-500 mt-1" v-if="selectedTypeName">
当前类型{{ selectedTypeName }}
</div>
</div>
<div class="actions flex space-x-2">
<Button type="primary" danger @click="handleBatchDelete" :disabled="!hasSelectedClauses">
<template #icon><DeleteOutlined /></template>
批量删除
</Button>
<Button type="primary" @click="handleAddClause">
<template #icon><PlusOutlined /></template>
新建条款
</Button>
</div>
</div>
<!-- 条款配置表格 -->
<div class="clause-table-wrapper" v-if="selectedTypeId">
<Table
:columns="columns"
:data-source="currentClauses"
:row-selection="rowSelection"
:pagination="pagination"
:scroll="{ y: 'calc(100vh - 400px)' }"
:row-key="(record) => record.id"
size="middle"
bordered
>
<!-- 条款名称列 -->
<template #clauseName="{ record }">
<div class="clause-name-cell">
<div class="clause-title font-medium">{{ record.clauseName }}</div>
</div>
</template>
<!-- 条款描述列 -->
<template #clauseDescription="{ record }">
<div class="description-cell">
<Tooltip :title="record.clauseDescription" placement="topLeft">
<div class="description-text">{{ record.clauseDescription }}</div>
</Tooltip>
</div>
</template>
<!-- 是否必查列 -->
<template #isRequired="{ record }">
<Tag :color="record.isRequired ? 'success' : 'default'">
{{ record.isRequired ? '必查' : '可选' }}
</Tag>
</template>
<!-- 关键词列 -->
<template #keywords="{ record }">
<div class="keywords-cell">
<Tag
v-for="keyword in record.keywords.split(',').slice(0, 3)"
:key="keyword"
size="small"
class="keyword-tag"
>
{{ keyword }}
</Tag>
<Tooltip v-if="record.keywords.split(',').length > 3" :title="record.keywords">
<Tag size="small" color="blue">
+{{ record.keywords.split(',').length - 3 }}
</Tag>
</Tooltip>
</div>
</template>
<!-- 状态列 -->
<template #status="{ record }">
<Tag :color="record.status === 1 ? 'success' : 'error'">
{{ record.status === 1 ? '启用' : '禁用' }}
</Tag>
</template>
<!-- 操作列 -->
<template #action="{ record }">
<div class="action-buttons flex space-x-2">
<Button type="link" size="small" @click="handleEditClause(record)">
编辑
</Button>
<Button type="link" size="small" danger @click="confirmDeleteClause(record)">
删除
</Button>
</div>
</template>
</Table>
</div>
<!-- 空状态 -->
<div v-else class="empty-state text-center py-12">
<div class="text-gray-400 text-6xl mb-4">📋</div>
<div class="text-gray-500 text-lg">请选择一个合同类型查看条款配置</div>
</div>
</div>
</div>
<!-- 删除确认模态框 -->
<Modal
v-model:visible="deleteModalVisible"
:title="deleteModalTitle"
@ok="handleDeleteConfirm"
@cancel="deleteModalVisible = false"
okText="确认删除"
cancelText="取消"
okType="danger"
>
<p>{{ deleteModalContent }}</p>
</Modal>
<!-- 合同类型表单模态框 -->
<Modal
v-model:visible="typeModalVisible"
:title="typeModalTitle"
@ok="handleTypeSave"
@cancel="typeModalVisible = false"
okText="保存"
cancelText="取消"
width="600px"
>
<Form
:model="typeForm"
ref="typeFormRef"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
>
<FormItem label="类型名称" name="type_name" :rules="[{ required: true, message: '请输入类型名称' }]">
<Input v-model:value="typeForm.type_name" placeholder="请输入类型名称" />
</FormItem>
<FormItem label="类型编码" name="type_code" :rules="[{ required: true, message: '请输入类型编码' }]">
<Input v-model:value="typeForm.type_code" placeholder="请输入类型编码" />
</FormItem>
<FormItem label="描述" name="description">
<Textarea v-model:value="typeForm.description" placeholder="请输入描述信息" :rows="3" />
</FormItem>
<FormItem label="排序" name="sort_order">
<InputNumber v-model:value="typeForm.sort_order" placeholder="请输入排序" style="width: 100%" />
</FormItem>
<FormItem label="状态" name="status">
<Select v-model:value="typeForm.status" placeholder="请选择状态">
<SelectOption :value="1">启用</SelectOption>
<SelectOption :value="0">禁用</SelectOption>
</Select>
</FormItem>
</Form>
</Modal>
<!-- 条款配置表单模态框 -->
<Modal
v-model:visible="clauseModalVisible"
:title="clauseModalTitle"
@ok="handleClauseSave"
@cancel="clauseModalVisible = false"
okText="保存"
cancelText="取消"
width="800px"
>
<Form
:model="clauseForm"
ref="clauseFormRef"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
>
<FormItem label="条款名称" name="clause_name" :rules="[{ required: true, message: '请输入条款名称' }]">
<Input v-model:value="clauseForm.clause_name" placeholder="请输入条款名称" />
</FormItem>
<FormItem label="条款描述" name="clause_description" :rules="[{ required: true, message: '请输入条款描述' }]">
<Textarea v-model:value="clauseForm.clause_description" placeholder="请输入条款内容描述" :rows="4" />
</FormItem>
<FormItem label="是否必查" name="is_required">
<Select v-model:value="clauseForm.is_required" placeholder="请选择是否必查">
<SelectOption :value="true">必查</SelectOption>
<SelectOption :value="false">可选</SelectOption>
</Select>
</FormItem>
<FormItem label="关键词" name="keywords" :rules="[{ required: true, message: '请输入关键词' }]">
<Textarea
v-model:value="clauseForm.keywords"
placeholder="请输入关键词,多个关键词用英文逗号分隔"
:rows="3"
/>
</FormItem>
<FormItem label="排序" name="sort_order">
<InputNumber v-model:value="clauseForm.sort_order" placeholder="请输入排序" style="width: 100%" />
</FormItem>
<FormItem label="状态" name="status">
<Select v-model:value="clauseForm.status" placeholder="请选择状态">
<SelectOption :value="1">启用</SelectOption>
<SelectOption :value="0">禁用</SelectOption>
</Select>
</FormItem>
</Form>
</Modal>
</PageWrapper>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, reactive } from 'vue';
import { PlusOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons-vue';
import {
Button, Modal, message, Form, FormItem, Input, InputNumber,
Select, SelectOption, Textarea, Table, Tag, Tooltip
} from 'ant-design-vue';
import { FormInstance } from 'ant-design-vue/es/form';
import {
ContractualClauseTypeList, ContractualClauseTypeAdd, ContractualClauseTypeUpdate, ContractualClauseTypeRemove
} from '@/api/contractReview/ContractualClauseType';
import {
ContractualClauseConfigList, ContractualClauseConfigAdd, ContractualClauseConfigUpdate, ContractualClauseConfigRemove
} from '@/api/contractReview/ContractualClauseConfig';
import { ContractualClauseTypeVO, ContractualClauseTypeForm } from '@/api/contractReview/ContractualClauseType/model';
import { ContractualClauseConfigVO, ContractualClauseConfigForm } from '@/api/contractReview/ContractualClauseConfig/model';
defineOptions({ name: 'ContractClauseConfig' });
//
const contractTypes = ref<ContractualClauseTypeVO[]>([]);
const clauseConfigs = ref<ContractualClauseConfigVO[]>([]);
const clauseCounts = ref<Map<number, number>>(new Map());
//
const selectedTypeId = ref<number | null>(null);
const selectedClauseIds = ref<number[]>([]);
//
const columns = [
{
title: '条款名称',
dataIndex: 'clauseName',
key: 'clauseName',
width: 150,
fixed: 'left' as const,
slots: { customRender: 'clauseName' }
},
{
title: '条款描述',
dataIndex: 'clauseDescription',
key: 'clauseDescription',
width: 300,
slots: { customRender: 'clauseDescription' }
},
{
title: '是否必查',
dataIndex: 'isRequired',
key: 'isRequired',
width: 100,
align: 'center' as const,
slots: { customRender: 'isRequired' }
},
{
title: '关键词',
dataIndex: 'keywords',
key: 'keywords',
width: 250,
slots: { customRender: 'keywords' }
},
{
title: '排序',
dataIndex: 'sortOrder',
key: 'sortOrder',
width: 80,
align: 'center' as const
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 80,
align: 'center' as const,
slots: { customRender: 'status' }
},
{
title: '操作',
key: 'action',
width: 120,
fixed: 'right' as const,
align: 'center' as const,
slots: { customRender: 'action' }
}
];
//
const rowSelection = computed(() => ({
selectedRowKeys: selectedClauseIds.value,
onChange: (selectedRowKeys: any[], selectedRows: any[]) => {
selectedClauseIds.value = selectedRowKeys as number[];
},
getCheckboxProps: (record: ContractualClauseConfigVO) => ({
disabled: false,
name: record.clauseName || '',
}),
}));
//
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
pageSizeOptions: ['10', '20', '50', '100'],
showTotal: (total: number, range: [number, number]) => `${total} 条记录`,
onChange: (page: number, size: number) => {
pagination.current = page;
pagination.pageSize = size;
selectedClauseIds.value = []; //
loadClauseConfigs();
},
onShowSizeChange: (current: number, size: number) => {
pagination.current = 1;
pagination.pageSize = size;
selectedClauseIds.value = []; //
loadClauseConfigs();
}
});
//
const deleteModalVisible = ref(false);
const deleteModalTitle = ref('');
const deleteModalContent = ref('');
const deleteType = ref<'type' | 'clause'>('clause');
const itemToDelete = ref<any>(null);
const typeModalVisible = ref(false);
const typeModalTitle = ref('新建合同类型');
const typeForm = reactive({
id: undefined,
type_name: '',
type_code: '',
description: '',
sort_order: 0,
status: 1
});
const typeFormRef = ref<FormInstance>();
const clauseModalVisible = ref(false);
const clauseModalTitle = ref('新建条款');
const clauseForm = reactive({
id: undefined,
contractual_clause_type_id: undefined,
clause_name: '',
clause_description: '',
is_required: true,
keywords: '',
sort_order: 0,
status: 1
});
const clauseFormRef = ref<FormInstance>();
//
const selectedTypeName = computed(() => {
if (!selectedTypeId.value) return '';
const type = contractTypes.value.find(t => t.id === selectedTypeId.value);
return type ? type.typeName : '';
});
const currentClauses = computed(() => {
return clauseConfigs.value;
});
const hasSelectedClauses = computed(() => {
return selectedClauseIds.value.length > 0;
});
//
const loadContractTypes = async () => {
try {
const res = await ContractualClauseTypeList({});
const data = res as any;
if (data && Array.isArray(data)) {
contractTypes.value = data;
//
await loadClauseCounts();
//
if (data.length > 0 && data[0].id) {
selectedTypeId.value = data[0].id;
await loadClauseConfigs();
}
}
} catch (error) {
console.error('加载合同类型失败', error);
message.error('加载合同类型失败');
}
};
const loadClauseCounts = async () => {
const counts = new Map<number, number>();
for (const type of contractTypes.value) {
if (type.id) {
try {
const res = await ContractualClauseConfigList({ contractualClauseTypeId: type.id });
const data = res as any;
let count = 0;
if (data && typeof data === 'object') {
if (data.rows && Array.isArray(data.rows)) {
count = data.total || data.rows.length;
} else if (Array.isArray(data)) {
count = data.length;
}
}
counts.set(type.id, count);
} catch (error) {
console.warn(`加载类型 ${type.typeName} 的条款数量失败`, error);
counts.set(type.id, 0);
}
}
}
clauseCounts.value = counts;
};
const loadClauseConfigs = async () => {
if (!selectedTypeId.value) {
clauseConfigs.value = [];
pagination.total = 0;
return;
}
try {
const params = {
contractualClauseTypeId: selectedTypeId.value,
pageNum: pagination.current,
pageSize: pagination.pageSize
};
const res = await ContractualClauseConfigList(params);
const data = res as any;
//
if (data && typeof data === 'object') {
if (data.rows && Array.isArray(data.rows)) {
// { rows: [], total: number }
clauseConfigs.value = data.rows;
pagination.total = data.total || 0;
} else if (Array.isArray(data)) {
//
clauseConfigs.value = data;
pagination.total = data.length;
} else {
clauseConfigs.value = [];
pagination.total = 0;
}
} else {
clauseConfigs.value = [];
pagination.total = 0;
}
} catch (error) {
console.error('加载条款配置失败', error);
message.error('加载条款配置失败');
clauseConfigs.value = [];
pagination.total = 0;
}
};
//
const selectContractType = async (typeId: number) => {
selectedTypeId.value = typeId;
selectedClauseIds.value = [];
//
pagination.current = 1;
await loadClauseConfigs();
};
const handleCreateType = () => {
Object.assign(typeForm, {
id: undefined,
type_name: '',
type_code: '',
description: '',
sort_order: 0,
status: 1
});
typeModalTitle.value = '新建合同类型';
typeModalVisible.value = true;
};
const handleEditType = (record: ContractualClauseTypeVO) => {
//
Object.assign(typeForm, {
id: record.id,
type_name: record.typeName,
type_code: record.typeCode,
description: record.description,
sort_order: record.sortOrder,
status: record.status
});
typeModalTitle.value = '编辑合同类型';
typeModalVisible.value = true;
};
const handleAddClause = () => {
if (!selectedTypeId.value) {
message.warning('请先选择合同类型');
return;
}
Object.assign(clauseForm, {
id: undefined,
contractual_clause_type_id: selectedTypeId.value,
clause_name: '',
clause_description: '',
is_required: true,
keywords: '',
sort_order: 0,
status: 1
});
clauseModalTitle.value = '新建条款';
clauseModalVisible.value = true;
};
const handleEditClause = (record: ContractualClauseConfigVO) => {
//
Object.assign(clauseForm, {
id: record.id,
contractual_clause_type_id: record.contractualClauseTypeId,
clause_name: record.clauseName,
clause_description: record.clauseDescription,
is_required: record.isRequired,
keywords: record.keywords,
sort_order: record.sortOrder,
status: record.status
});
clauseModalTitle.value = '编辑条款';
clauseModalVisible.value = true;
};
const confirmDeleteType = async (type: ContractualClauseTypeVO) => {
try {
//
const res = await ContractualClauseConfigList({ contractualClauseTypeId: type.id });
const data = res as any;
let hasClause = false;
if (data && typeof data === 'object') {
if (data.rows && Array.isArray(data.rows)) {
hasClause = data.rows.length > 0;
} else if (Array.isArray(data)) {
hasClause = data.length > 0;
}
}
if (hasClause) {
message.warning(`合同类型 "${type.typeName}" 下还存在条款配置,无法删除!请先删除相关条款。`);
return;
}
//
deleteModalTitle.value = '删除合同类型';
deleteModalContent.value = `确定要删除合同类型 "${type.typeName}" 吗?此操作不可恢复。`;
itemToDelete.value = type;
deleteType.value = 'type';
deleteModalVisible.value = true;
} catch (error) {
console.error('检查关联条款失败', error);
message.error('检查关联条款失败,无法删除');
}
};
const confirmDeleteClause = (clause: ContractualClauseConfigVO) => {
deleteModalTitle.value = '删除条款';
deleteModalContent.value = `确定要删除条款 "${clause.clauseName}" 吗?此操作不可恢复。`;
itemToDelete.value = clause;
deleteType.value = 'clause';
deleteModalVisible.value = true;
};
const handleBatchDelete = () => {
if (selectedClauseIds.value.length === 0) {
message.warning('请先选择要删除的条款');
return;
}
deleteModalTitle.value = '批量删除条款';
deleteModalContent.value = `确定要删除选中的 ${selectedClauseIds.value.length} 个条款吗?此操作不可恢复。`;
deleteType.value = 'clause';
deleteModalVisible.value = true;
itemToDelete.value = { batchDelete: true };
};
const handleDeleteConfirm = async () => {
try {
if (deleteType.value === 'type') {
//
if (itemToDelete.value?.id) {
await ContractualClauseTypeRemove(itemToDelete.value.id);
message.success('删除合同类型成功');
await loadContractTypes();
}
} else {
//
if (itemToDelete.value?.batchDelete) {
//
if (selectedClauseIds.value.length > 0) {
await ContractualClauseConfigRemove(selectedClauseIds.value.join(','));
message.success(`成功删除 ${selectedClauseIds.value.length} 个条款`);
selectedClauseIds.value = [];
}
} else {
//
if (itemToDelete.value?.id) {
await ContractualClauseConfigRemove(itemToDelete.value.id);
message.success('删除条款成功');
}
}
await loadClauseConfigs();
await loadClauseCounts();
}
} catch (error) {
console.error('删除失败', error);
message.error('删除失败');
}
deleteModalVisible.value = false;
};
const handleTypeSave = async () => {
try {
await typeFormRef.value?.validate();
// API
const apiData = {
id: typeForm.id,
typeName: typeForm.type_name,
typeCode: typeForm.type_code,
description: typeForm.description,
sortOrder: typeForm.sort_order,
status: typeForm.status
};
if (typeForm.id) {
await ContractualClauseTypeUpdate(apiData);
message.success('更新合同类型成功');
} else {
await ContractualClauseTypeAdd(apiData);
message.success('新增合同类型成功');
}
typeModalVisible.value = false;
await loadContractTypes();
} catch (error) {
console.error('保存合同类型失败', error);
message.error('保存合同类型失败');
}
};
const handleClauseSave = async () => {
try {
await clauseFormRef.value?.validate();
// API
const apiData = {
id: clauseForm.id,
contractualClauseTypeId: clauseForm.contractual_clause_type_id,
clauseName: clauseForm.clause_name,
clauseDescription: clauseForm.clause_description,
isRequired: clauseForm.is_required,
keywords: clauseForm.keywords,
sortOrder: clauseForm.sort_order,
status: clauseForm.status
};
if (clauseForm.id) {
await ContractualClauseConfigUpdate(apiData);
message.success('更新条款成功');
} else {
await ContractualClauseConfigAdd(apiData);
message.success('新增条款成功');
}
clauseModalVisible.value = false;
await loadClauseConfigs();
await loadClauseCounts();
} catch (error) {
console.error('保存条款失败', error);
message.error('保存条款失败');
}
};
//
onMounted(async () => {
await loadContractTypes();
});
</script>
<style scoped>
.container {
min-height: calc(100vh - 120px);
height: calc(100vh - 120px);
background-color: #fff;
display: flex;
width: 100%;
max-width: none;
}
.contract-type-sidebar {
min-width: 240px;
width: 240px;
height: 100%;
display: flex;
flex-direction: column;
}
.type-list {
flex: 1;
overflow-y: auto;
padding-right: 4px;
}
.clause-config-content {
flex: 1;
height: 100%;
min-width: 0;
display: flex;
flex-direction: column;
}
.clause-table-wrapper {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
}
.clause-table-wrapper :deep(.ant-table-wrapper) {
flex: 1;
display: flex;
flex-direction: column;
}
.clause-table-wrapper :deep(.ant-table-container) {
flex: 1;
display: flex;
flex-direction: column;
}
.clause-table-wrapper :deep(.ant-table-body) {
flex: 1;
overflow-y: auto;
}
.clause-table-wrapper :deep(.ant-pagination) {
margin-top: 16px;
text-align: right;
flex-shrink: 0;
}
.type-item {
transition: all 0.3s;
border-radius: 6px;
margin-bottom: 4px;
}
.type-item:hover {
background-color: #f0f7ff !important;
}
.type-item.selected {
background-color: #e6f7ff !important;
border-left: 3px solid #1890ff;
}
.type-name {
font-size: 14px;
line-height: 1.4;
}
.type-code {
font-size: 12px;
margin-top: 2px;
}
.clause-name-cell .clause-title {
font-size: 14px;
color: #1890ff;
cursor: pointer;
}
.clause-name-cell .clause-title:hover {
text-decoration: underline;
}
.description-cell {
max-width: 300px;
}
.description-text {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.4;
}
.keywords-cell {
display: flex;
flex-wrap: wrap;
gap: 4px;
max-width: 250px;
}
.keyword-tag {
margin: 0;
}
.action-buttons {
justify-content: center;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
background-color: #fafafa;
border-radius: 8px;
margin: 16px;
}
/* 滚动条样式 */
.type-list::-webkit-scrollbar {
width: 6px;
}
.type-list::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
.type-list::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
.type-list::-webkit-scrollbar-thumb:hover {
background: #a1a1a1;
}
/* 确保PageWrapper不限制宽度 */
:deep(.ant-page-wrapper) {
padding: 0 !important;
}
:deep(.ant-page-wrapper-content) {
margin: 0 !important;
padding: 16px !important;
}
/* 表格样式优化 */
:deep(.ant-table-tbody > tr > td) {
padding: 12px 8px;
}
:deep(.ant-table-thead > tr > th) {
background-color: #fafafa;
font-weight: 600;
}
:deep(.ant-table-row:hover > td) {
background-color: #f5f5f5;
}
</style>

740
src/views/contractReview/ContractualTasks/components/ConsistencyContent.vue

@ -1,53 +1,186 @@
<template>
<div class="consistency-content">
<!-- 招投标文件上传 -->
<div class="section file-upload-section">
<h3 class="section-title">上传招投标文件</h3>
<p class="section-description">上传需要与合同进行一致性对比的招投标文件</p>
<div class="upload-box" :class="{'file-preview': bidFileList && bidFileList.length > 0}">
<template v-if="bidFileList && bidFileList.length > 0">
<!-- 文件预览 -->
<div class="file-info">
<FileTextOutlined class="file-icon" />
<div class="file-details">
<p class="file-name">{{ bidFileList[0].name }}</p>
<div class="file-progress" v-if="uploading">
<Progress :percent="uploadPercent" size="small" status="active" />
<!-- 文件上传和合同类型选择 -->
<div class="section upload-and-type-section">
<div class="upload-type-row">
<!-- 招投标文件上传 -->
<div class="upload-column">
<h3 class="section-title">上传招投标文件</h3>
<p class="section-description">上传需要与合同进行一致性对比的招投标文件</p>
<div class="upload-area">
<AUpload
:fileList="[]"
:customRequest="customUploadRequest"
:beforeUpload="beforeUpload"
:showUploadList="false"
:maxCount="1"
:multiple="false"
name="file"
accept=".pdf,.doc,.docx"
:disabled="uploading"
>
<AButton
type="primary"
:loading="uploading"
:disabled="!!(bidFileList && bidFileList.length > 0)"
size="large"
>
<template #icon v-if="!uploading">
<UpOutlined />
</template>
{{ uploading ? '上传中...' : (bidFileList && bidFileList.length > 0 ? '已上传文件' : '选择文件') }}
</AButton>
</AUpload>
<!-- 文件状态显示 -->
<div class="file-status-area" v-if="(bidFileList && bidFileList.length > 0) || uploading">
<div class="file-info-compact">
<FileTextOutlined class="file-icon-small" />
<span class="file-name-small">{{ bidFileList?.[0]?.name || '上传中...' }}</span>
<AButton
type="link"
size="small"
danger
@click="removeBidFile"
v-if="!uploading"
>
删除
</AButton>
</div>
<p class="file-status" v-else>
<CheckCircleFilled class="status-icon success" /> 上传成功
<AButton type="link" class="remove-btn" @click="removeBidFile">删除</AButton>
</p>
<div class="file-progress-compact" v-if="uploading">
<Progress
:percent="uploadPercent"
size="small"
status="active"
:show-info="false"
/>
</div>
<div class="file-success" v-else-if="bidFileList && bidFileList.length > 0">
<CheckCircleFilled class="success-icon" />
<span class="success-text">上传成功</span>
</div>
</div>
</div>
</div>
<!-- 合同类型选择 -->
<div class="type-column">
<h3 class="section-title">
<ContainerOutlined class="title-icon" />
合同类型选择
</h3>
<p class="section-description">选择要进行一致性校验的合同类型</p>
<div class="contract-type-selector">
<Select
v-model:value="selectedContractTypeId"
placeholder="请选择合同类型"
size="large"
style="width: 100%"
:loading="contractTypesLoading"
@change="handleContractTypeChange"
>
<SelectOption
v-for="contractType in contractTypes"
:key="contractType.id"
:value="contractType.id"
>
<div class="contract-type-option">
<div class="type-name">{{ contractType.typeName }}</div>
<div class="type-code">{{ contractType.typeCode }}</div>
</div>
</SelectOption>
</Select>
</div>
</div>
</div>
</div>
<!-- 条款配置选择 -->
<div class="section clause-config-section" v-if="selectedContractTypeId">
<h3 class="section-title">
<CheckSquareOutlined class="title-icon" />
条款配置选择
</h3>
<p class="section-description">选择需要进行一致性校验的具体条款</p>
<div class="clause-select-content">
<div class="clause-select-row">
<!-- 必要校验条件 -->
<div class="clause-select-column">
<div class="clause-select-group">
<div class="group-header">
<div class="group-title">
<ExclamationCircleOutlined class="group-icon required" />
必要校验条件
<Tag color="orange" size="small">{{ requiredClauses.length }}</Tag>
</div>
<div class="group-actions">
<AButton
type="link"
size="small"
@click="toggleAllRequired(!allRequiredSelected)"
>
{{ allRequiredSelected ? '取消全选' : '全选' }}
</AButton>
</div>
</div>
<div class="group-description">
这些条款是该合同类型的核心要素建议全部选择进行校验
</div>
<Select
v-model:value="selectedRequiredClauseIds"
mode="multiple"
placeholder="请选择必要校验条件"
style="width: 100%"
:max-tag-count="3"
:show-search="true"
:filter-option="filterOption"
:options="requiredClauseOptions"
/>
</div>
</div>
</template>
<template v-else>
<!-- 上传区域 -->
<AUpload
:fileList="bidFileList"
:customRequest="customUploadRequest"
:beforeUpload="beforeUpload"
:showUploadList="false"
:maxCount="1"
:multiple="false"
name="file"
accept=".pdf,.doc,.docx"
draggable
>
<div class="upload-content">
<div class="upload-icon">
<UpOutlined class="upload-arrow-icon" />
<!-- 非必选条款 -->
<div class="clause-select-column">
<div class="clause-select-group">
<div class="group-header">
<div class="group-title">
<InfoCircleOutlined class="group-icon optional" />
非必选条款
<Tag color="blue" size="small">{{ optionalClauses.length }}</Tag>
</div>
<div class="group-actions">
<AButton
type="link"
size="small"
@click="toggleAllOptional(!allOptionalSelected)"
>
{{ allOptionalSelected ? '取消全选' : '全选' }}
</AButton>
</div>
</div>
<div class="upload-text-container">
<p class="upload-text">
<a class="upload-link">选择招投标文件</a>
</p>
<p class="upload-hint">支持PDFDOCDOCX格式文件</p>
<div class="group-description">
这些条款为可选校验项可根据实际需求选择
</div>
<Select
v-model:value="selectedOptionalClauseIds"
mode="multiple"
placeholder="请选择非必选条款"
style="width: 100%"
:max-tag-count="3"
:show-search="true"
:filter-option="filterOption"
:options="optionalClauseOptions"
/>
</div>
</AUpload>
</template>
</div>
</div>
</div>
</div>
@ -71,18 +204,26 @@
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { Input, Progress, Button, Upload } from 'ant-design-vue';
import { ref, computed, onMounted } from 'vue';
import { Input, Progress, Button, Upload, Select, SelectOption, Tag, Empty } from 'ant-design-vue';
import {
UpOutlined,
FileTextOutlined,
CheckCircleFilled
CheckCircleFilled,
ContainerOutlined,
CheckSquareOutlined,
ExclamationCircleOutlined,
InfoCircleOutlined
} from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
import type { UploadProps } from 'ant-design-vue';
import { uploadApi } from '@/api/upload';
import { ossRemove } from '@/api/system/oss';
import { UploadFileParams } from '#/axios';
import { ContractualClauseTypeList } from '@/api/contractReview/ContractualClauseType';
import { ContractualClauseConfigList } from '@/api/contractReview/ContractualClauseConfig';
import type { ContractualClauseTypeVO } from '@/api/contractReview/ContractualClauseType/model';
import type { ContractualClauseConfigVO } from '@/api/contractReview/ContractualClauseConfig/model';
//
const AButton = Button;
@ -95,6 +236,54 @@
const currentBidOssId = ref<string | null>(null);
const specialFocus = ref<string>(''); //
//
const contractTypes = ref<ContractualClauseTypeVO[]>([]);
const contractTypesLoading = ref(false);
const selectedContractTypeId = ref<number | undefined>(undefined);
//
const allClauses = ref<ContractualClauseConfigVO[]>([]);
const selectedRequiredClauseIds = ref<number[]>([]);
const selectedOptionalClauseIds = ref<number[]>([]);
//
const requiredClauses = computed(() => {
return allClauses.value.filter(clause => clause.isRequired === true && clause.status === 1);
});
const optionalClauses = computed(() => {
return allClauses.value.filter(clause => clause.isRequired === false && clause.status === 1);
});
const allRequiredSelected = computed(() => {
return requiredClauses.value.length > 0 &&
requiredClauses.value.every(clause => selectedRequiredClauseIds.value.includes(clause.id!));
});
const allOptionalSelected = computed(() => {
return optionalClauses.value.length > 0 &&
optionalClauses.value.every(clause => selectedOptionalClauseIds.value.includes(clause.id!));
});
//
const requiredClauseOptions = computed(() => {
return requiredClauses.value.map(clause => ({
value: clause.id!,
label: clause.clauseName!,
title: clause.clauseDescription || '',
keywords: clause.keywords || ''
}));
});
const optionalClauseOptions = computed(() => {
return optionalClauses.value.map(clause => ({
value: clause.id!,
label: clause.clauseName!,
title: clause.clauseDescription || '',
keywords: clause.keywords || ''
}));
});
//
interface UploadResponse {
ossId: string;
@ -208,6 +397,105 @@
}
}
//
const loadContractTypes = async () => {
try {
contractTypesLoading.value = true;
const res = await ContractualClauseTypeList({ status: 1 });
const data = res as any;
if (data && Array.isArray(data)) {
contractTypes.value = data;
// ""
const generalContract = data.find(item =>
item.typeName?.includes('通用') ||
item.typeCode?.toLowerCase().includes('general') ||
item.typeCode?.toLowerCase().includes('common')
);
if (generalContract?.id) {
selectedContractTypeId.value = generalContract.id;
await loadClauseConfigs(generalContract.id);
} else if (data.length > 0 && data[0].id) {
//
selectedContractTypeId.value = data[0].id;
await loadClauseConfigs(data[0].id);
}
}
} catch (error) {
console.error('加载合同类型失败', error);
message.error('加载合同类型失败');
} finally {
contractTypesLoading.value = false;
}
};
//
const loadClauseConfigs = async (contractTypeId: number) => {
try {
const res = await ContractualClauseConfigList({
contractualClauseTypeId: contractTypeId,
status: 1
});
const data = res as any;
if (data && Array.isArray(data)) {
allClauses.value = data;
//
const requiredClauseIds = data
.filter(clause => clause.isRequired === true)
.map(clause => clause.id!)
.filter(id => id !== undefined);
selectedRequiredClauseIds.value = requiredClauseIds;
//
selectedOptionalClauseIds.value = [];
}
} catch (error) {
console.error('加载条款配置失败', error);
message.error('加载条款配置失败');
}
};
//
const handleContractTypeChange = async (value: any) => {
const contractTypeId = typeof value === 'number' ? value : Number(value);
if (contractTypeId && !isNaN(contractTypeId)) {
selectedContractTypeId.value = contractTypeId;
await loadClauseConfigs(contractTypeId);
}
};
// /
const toggleAllRequired = (selectAll: boolean) => {
if (selectAll) {
selectedRequiredClauseIds.value = requiredClauses.value.map(clause => clause.id!);
} else {
selectedRequiredClauseIds.value = [];
}
};
const toggleAllOptional = (selectAll: boolean) => {
if (selectAll) {
selectedOptionalClauseIds.value = optionalClauses.value.map(clause => clause.id!);
} else {
selectedOptionalClauseIds.value = [];
}
};
//
const filterOption = (input: string, option: any) => {
const searchText = input.toLowerCase();
const label = option.label?.toLowerCase() || '';
const title = option.title?.toLowerCase() || '';
const keywords = option.keywords?.toLowerCase() || '';
return label.includes(searchText) ||
title.includes(searchText) ||
keywords.includes(searchText);
};
//
const getData = () => {
//
@ -215,14 +503,35 @@
message.warning('请上传招投标文件');
return null;
}
//
if (!selectedContractTypeId.value) {
message.warning('请选择合同类型');
return null;
}
//
const totalSelectedClauses = selectedRequiredClauseIds.value.length + selectedOptionalClauseIds.value.length;
if (totalSelectedClauses === 0) {
message.warning('请至少选择一个校验条款');
return null;
}
return {
type: 'consistency',
bidDocumentOssId: currentBidOssId.value, // ossId
bidDocumentOssId: currentBidOssId.value, // ossId
contractTypeId: selectedContractTypeId.value, // ID
requiredClauseIds: selectedRequiredClauseIds.value, // ID
optionalClauseIds: selectedOptionalClauseIds.value, // ID
specialNote: specialFocus.value || undefined, //
};
};
//
onMounted(async () => {
await loadContractTypes();
});
// getData
defineExpose({
getData
@ -237,11 +546,9 @@
//
.section {
margin-bottom: 24px;
border-bottom: 1px dashed #eee;
padding-bottom: 20px;
&:last-child {
border-bottom: none;
margin-bottom: 0;
}
}
@ -259,153 +566,306 @@
line-height: 1.4;
}
//
.upload-box {
border: 1px dashed #ddd;
border-radius: 8px;
padding: 20px;
text-align: center;
cursor: pointer;
transition: border-color 0.3s;
width: 100%;
height: 200px;
//
.upload-and-type-section {
border-bottom: 1px dashed #eee;
padding-bottom: 20px;
}
.upload-type-row {
display: flex;
align-items: center;
justify-content: center;
background-color: #f9f9f9;
&:hover {
border-color: #13c2c2;
background-color: rgba(19, 194, 194, 0.02);
}
&.file-preview {
border: 2px solid #e6fffb;
background-color: #f0f8ff;
cursor: default;
}
gap: 24px;
align-items: flex-start;
}
.upload-column {
flex: 1;
min-width: 0;
}
.upload-content {
.type-column {
flex: 1;
min-width: 0;
}
//
.upload-area {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
align-items: flex-start;
gap: 12px;
}
.file-status-area {
width: 100%;
margin-top: 8px;
}
.upload-icon {
.file-info-compact {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 16px;
background-color: #e6fffb;
width: 50px;
height: 50px;
border-radius: 50%;
gap: 8px;
margin-bottom: 8px;
}
.upload-arrow-icon {
font-size: 24px;
.file-icon-small {
font-size: 16px;
color: #13c2c2;
}
.upload-text-container {
text-align: center;
.file-name-small {
font-size: 14px;
color: #333;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.upload-text {
font-size: 16px;
color: #333;
.file-progress-compact {
margin-bottom: 8px;
}
.upload-link {
color: #13c2c2;
cursor: pointer;
text-decoration: none;
font-weight: 600;
.file-success {
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
}
.upload-link:hover {
text-decoration: underline;
.success-icon {
color: #52c41a;
font-size: 16px;
}
.upload-hint {
color: #888;
font-size: 14px;
margin-top: 8px;
.success-text {
color: #52c41a;
font-weight: 500;
}
//
.focus-input {
margin-top: 12px;
:deep(.ant-input) {
font-size: 14px !important;
border-radius: 6px !important;
&:focus {
border-color: #13c2c2;
box-shadow: 0 0 0 2px rgba(19, 194, 194, 0.2);
}
}
:deep(.ant-input-data-count) {
color: #999;
font-size: 12px;
}
}
//
.title-icon {
margin-right: 8px;
color: #13c2c2;
}
//
.contract-type-section {
.contract-type-selector {
margin-top: 12px;
:deep(.ant-select-selector) {
border-radius: 6px;
padding: 8px 12px;
min-height: 44px;
&:hover {
border-color: #13c2c2;
}
}
:deep(.ant-select-focused .ant-select-selector) {
border-color: #13c2c2;
box-shadow: 0 0 0 2px rgba(19, 194, 194, 0.2);
}
}
}
/* 文件预览区域 */
.file-info {
.contract-type-option {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
max-width: 800px;
.type-name {
font-size: 14px;
font-weight: 500;
color: #333;
}
.type-code {
font-size: 12px;
color: #888;
margin-left: 8px;
}
}
.file-icon {
font-size: 32px;
color: #13c2c2;
margin-right: 16px;
//
.clause-config-section {
.clause-select-content {
margin-top: 16px;
}
}
.file-details {
flex: 1;
.clause-select-row {
display: flex;
gap: 24px;
align-items: flex-start;
}
.file-name {
font-size: 16px;
font-weight: 500;
margin: 0 0 8px 0;
color: #333;
.clause-select-column {
flex: 1;
min-width: 0;
}
.file-progress {
margin: 0 0 8px 0;
width: 100%;
.clause-select-group {
padding: 16px;
background: #fafafa;
border-radius: 8px;
border: 1px solid #f0f0f0;
&:first-child {
background: #fff7e6;
border-color: #ffd591;
}
&:last-child {
background: #f6ffed;
border-color: #b7eb8f;
}
}
.file-status {
font-size: 14px;
color: #666;
margin: 0;
.group-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.status-icon {
margin-right: 6px;
.group-title {
display: flex;
align-items: center;
font-size: 16px;
font-weight: 600;
color: #333;
.group-icon {
margin-right: 8px;
font-size: 18px;
&.required {
color: #fa8c16;
}
&.optional {
color: #52c41a;
}
}
}
.status-icon.success {
color: #52c41a;
.group-actions {
display: flex;
align-items: center;
}
.remove-btn {
padding: 0;
margin-left: 12px;
.group-description {
color: #666;
margin-bottom: 12px;
font-size: 14px;
line-height: 1.5;
font-style: italic;
}
//
.focus-input {
margin-top: 12px;
:deep(.ant-input) {
font-size: 14px !important;
border-radius: 6px !important;
//
.clause-select-group {
:deep(.ant-select) {
.ant-select-selector {
border-radius: 6px;
min-height: 40px;
&:hover {
border-color: #13c2c2;
}
}
&:focus {
&.ant-select-focused .ant-select-selector {
border-color: #13c2c2;
box-shadow: 0 0 0 2px rgba(19, 194, 194, 0.2);
}
.ant-select-selection-overflow {
align-items: center;
}
.ant-select-selection-item {
background: #e6fffb;
border: 1px solid #87e8de;
border-radius: 4px;
color: #13c2c2;
font-size: 12px;
height: 24px;
line-height: 22px;
.ant-select-selection-item-remove {
color: #13c2c2;
&:hover {
color: #ff4d4f;
}
}
}
}
:deep(.ant-input-data-count) {
color: #999;
font-size: 12px;
:deep(.ant-select-dropdown) {
.ant-select-item {
padding: 8px 12px;
&:hover {
background-color: #f0fffe;
}
&.ant-select-item-option-selected {
background-color: #e6fffb;
color: #13c2c2;
font-weight: 500;
}
}
}
}
//
@media (max-width: 1200px) {
.clause-select-row {
flex-direction: column;
gap: 16px;
}
}
@media (max-width: 768px) {
//
.upload-type-row {
flex-direction: column;
gap: 16px;
}
.upload-column, .type-column {
width: 100%;
}
//
.clause-select-row {
flex-direction: column;
gap: 12px;
}
}
</style>

9
src/views/contractReview/ContractualTasks/components/InferenceReview.vue

@ -74,8 +74,7 @@
import { UploadOutlined as UpOutlined, FileTextOutlined, CheckCircleFilled } from '@ant-design/icons-vue';
import { Button, Upload } from 'ant-design-vue';
import type { UploadProps } from 'ant-design-vue';
import { uploadDocument } from '@/api/documentReview/DocumentTasks';
import { ossRemove } from '@/api/system/oss';
import { uploadContract, ContractFileRemove } from '@/api/contractReview/ContractualTasks';
import { UploadFileParams } from '#/axios';
import { useModal } from '@/components/Modal';
import ReviewConfigDialog from './ReviewConfigDialog.vue';
@ -144,7 +143,7 @@
];
// API
uploadDocument(
uploadContract(
uploadParams,
(progressEvent) => {
//
@ -210,8 +209,8 @@
// 使OSS ID
if (currentOssId.value) {
// IDS
ossRemove([currentOssId.value])
// 使
ContractFileRemove(currentOssId.value)
.then(() => {
fileList.value = [];
uploadPercent.value = 0;

47
src/views/tenderReview/TenderTask/TenderTask.data.ts

@ -10,25 +10,18 @@ export const formSchemas: FormSchema[] = [
field: 'taskNameList',
component: 'Select',
componentProps: {
options: [{
label: '招标文件文本审核',
value: 'sjjbidAnalysis',
},
{
label: '招标文件图片审核',
value: 'sjjbidImageAnalysis',
}],
options: getDictOptions('tender_review'),
mode: 'multiple',
},
},
{
label: '标文件名称',
field: 'tenderDocumentName',
label: '投标文件名称',
field: 'bidDocumentName',
component: 'Input',
},
{
label: '标文件名称',
field: 'bidDocumentName',
label: '标文件名称',
field: 'tenderDocumentName',
component: 'Input',
},
{
@ -53,14 +46,15 @@ const { renderDict } = useRender();
// 父表列配置
export const columns: BasicColumn[] = [
{
title: '招标文件名称',
dataIndex: 'tenderDocumentName',
},
{
title: '投标文件名称',
dataIndex: 'bidDocumentName',
},
{
title: '招标文件名称',
dataIndex: 'tenderDocumentName',
},
{
title: '上传时间',
dataIndex: 'createTime',
@ -80,14 +74,7 @@ export const childColumns: BasicColumn[] = [
{
title: '任务名称',
dataIndex: 'taskName',
customRender: ({ value, record }) => {
if (value === 'sjjbidAnalysis') {
return '招标文件文本审核';
} else if (value === 'sjjbidImageAnalysis') {
return '招标文件图片审核';
}
return value;
},
customRender: ({ value }) => renderDict(value, 'tender_review'),
},
{
title: '任务类型',
@ -113,14 +100,7 @@ export const modalSchemas: FormSchema[] = [
required: true,
component: 'Select',
componentProps: {
options: [{
label: '招标文件文本审核',
value: 'sjjbidAnalysis',
},
{
label: '招标文件图片审核',
value: 'sjjbidImageAnalysis',
}],
options: getDictOptions('tender_review'),
mode: 'multiple',
},
},
@ -140,10 +120,9 @@ export const modalSchemas: FormSchema[] = [
{
label: '投标文件集',
field: 'bidDocZipOssId',
required: true,
component: 'Upload',
componentProps: {
accept: ['.zip'],
accept: ['.zip','.docx', '.pdf'],
maxSize: 1000,
multiple: false,
resultField: 'ossId',

18
src/views/tenderReview/TenderTask/index.vue

@ -60,14 +60,11 @@
color: 'error',
ghost: true,
ifShow: () => {
if (record.progress.includes('100%')) {
if(record.delFile=='Y'){
if(record.deleteFlag=='Y'){
return false;
}
return true;
} else {
return false;
}
},
onClick: handleDeleteFile.bind(null, record),
},
@ -277,7 +274,16 @@
}
async function handleDeleteFile(record: Recordable) {
await TenderTaskDeleteFile(record.childrenTasks[0].ossId);
// ossId使ID使ID
const childTask = record.childrenTasks[0];
const ossId = childTask.tenderDocOssId || childTask.bidDocZipOssId;
if (!ossId) {
console.error('未找到有效的文件ID');
return;
}
await TenderTaskDeleteFile(ossId);
await reload();
}

Loading…
Cancel
Save