32 changed files with 5464 additions and 1278 deletions
@ -0,0 +1,98 @@ |
|||
import { defHttp } from '@/utils/http/axios'; |
|||
import { ID, IDS, commonExport } from '@/api/base'; |
|||
import { ContractualCaseFilesVO, ContractualCaseFilesForm, ContractualCaseFilesQuery } from './model'; |
|||
|
|||
/** |
|||
* 查询合同案例文件列表 |
|||
* @param params |
|||
* @returns |
|||
*/ |
|||
export function ContractualCaseFilesList(params?: ContractualCaseFilesQuery) { |
|||
return defHttp.get<ContractualCaseFilesVO[]>({ url: '/productManagement/ContractualCaseFiles/list', params }); |
|||
} |
|||
|
|||
/** |
|||
* 导出合同案例文件列表 |
|||
* @param params |
|||
* @returns |
|||
*/ |
|||
export function ContractualCaseFilesExport(params?: ContractualCaseFilesQuery) { |
|||
return commonExport('/productManagement/ContractualCaseFiles/export', params ?? {}); |
|||
} |
|||
|
|||
/** |
|||
* 查询合同案例文件详细 |
|||
* @param id id |
|||
* @returns |
|||
*/ |
|||
export function ContractualCaseFilesInfo(id: ID) { |
|||
return defHttp.get<ContractualCaseFilesVO>({ url: '/productManagement/ContractualCaseFiles/' + id }); |
|||
} |
|||
|
|||
/** |
|||
* 新增合同案例文件 |
|||
* @param data |
|||
* @returns |
|||
*/ |
|||
export function ContractualCaseFilesAdd(data: ContractualCaseFilesForm) { |
|||
return defHttp.postWithMsg<void>({ url: '/productManagement/ContractualCaseFiles', data }); |
|||
} |
|||
|
|||
/** |
|||
* 更新合同案例文件 |
|||
* @param data |
|||
* @returns |
|||
*/ |
|||
export function ContractualCaseFilesUpdate(data: ContractualCaseFilesForm) { |
|||
return defHttp.putWithMsg<void>({ url: '/productManagement/ContractualCaseFiles', data }); |
|||
} |
|||
|
|||
/** |
|||
* 更新合同案例文件状态 |
|||
* @param id 主键 |
|||
* @param status 状态 |
|||
* @returns |
|||
*/ |
|||
export function ContractualCaseFilesUpdateStatus(id: ID, status: string) { |
|||
return defHttp.putWithMsg<void>({ |
|||
url: '/productManagement/ContractualCaseFiles/updateStatus', |
|||
data: { id, isEffective: status } |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 删除合同案例文件 |
|||
* @param id id |
|||
* @returns |
|||
*/ |
|||
export function ContractualCaseFilesRemove(id: ID | IDS) { |
|||
return defHttp.deleteWithMsg<void>({ url: '/productManagement/ContractualCaseFiles/' + id },); |
|||
} |
|||
|
|||
/** |
|||
* 获取案例条款列表 |
|||
* @param id 案例文件ID |
|||
* @returns |
|||
*/ |
|||
export function ContractualCaseFilesArticles(id: ID) { |
|||
return defHttp.get<any[]>({ url: '/productManagement/ContractualCaseFiles/articles/' + id }); |
|||
} |
|||
|
|||
/** |
|||
* 生成案例PDF文档(查看详情) |
|||
* @param id 案例文件ID |
|||
* @returns PDF响应数据 |
|||
*/ |
|||
export function ContractualCaseFilesViewPdf(id: ID) { |
|||
return defHttp.get( |
|||
{ |
|||
url: '/productManagement/ContractualCaseFiles/view/' + id, |
|||
responseType: 'blob', |
|||
timeout: 1000 * 60 * 10, |
|||
headers: { |
|||
Accept: 'application/pdf', |
|||
} |
|||
}, |
|||
{ isReturnNativeResponse: true } |
|||
); |
|||
} |
@ -0,0 +1,76 @@ |
|||
import type { BaseEntity, PageQuery } from '@/api/base'; |
|||
|
|||
export interface ContractualCaseFilesVO extends BaseEntity { |
|||
/** 主键ID */ |
|||
id?: string | number; |
|||
|
|||
/** 案例名称 */ |
|||
caseName?: string; |
|||
|
|||
/** 案例描述 */ |
|||
caseDescription?: string; |
|||
|
|||
/** 案例类型 */ |
|||
caseType?: string; |
|||
|
|||
/** 发布日期 */ |
|||
publishDate?: string; |
|||
|
|||
/** 是否有效(Y:有效 N:无效) */ |
|||
isEffective?: string; |
|||
|
|||
/** 处理状态 */ |
|||
progressStatus?: string; |
|||
|
|||
/** OSS文件ID */ |
|||
ossId?: string | number; |
|||
} |
|||
|
|||
export interface ContractualCaseFilesForm extends BaseEntity { |
|||
/** 主键ID */ |
|||
id?: string | number; |
|||
|
|||
/** 案例名称 */ |
|||
caseName?: string; |
|||
|
|||
/** 案例描述 */ |
|||
caseDescription?: string; |
|||
|
|||
/** 案例类型 */ |
|||
caseType?: string; |
|||
|
|||
/** 发布日期 */ |
|||
publishDate?: string; |
|||
|
|||
/** 是否有效(Y:有效 N:无效) */ |
|||
isEffective?: string; |
|||
|
|||
/** 处理状态 */ |
|||
progressStatus?: string; |
|||
|
|||
/** OSS文件ID */ |
|||
ossId?: string | number; |
|||
} |
|||
|
|||
export interface ContractualCaseFilesQuery extends PageQuery { |
|||
/** 案例名称 */ |
|||
caseName?: string; |
|||
|
|||
/** 案例类型 */ |
|||
caseType?: string; |
|||
|
|||
/** 发布日期 */ |
|||
publishDate?: string; |
|||
|
|||
/** 日期范围查询-开始日期 */ |
|||
publishDateStart?: string; |
|||
|
|||
/** 日期范围查询-结束日期 */ |
|||
publishDateEnd?: string; |
|||
|
|||
/** 是否有效(Y:有效 N:无效) */ |
|||
isEffective?: string; |
|||
|
|||
/** 处理状态 */ |
|||
progressStatus?: string; |
|||
} |
@ -0,0 +1,98 @@ |
|||
import { defHttp } from '@/utils/http/axios'; |
|||
import { ID, IDS, commonExport } from '@/api/base'; |
|||
import { ContractualRegulationNamesVO, ContractualRegulationNamesForm, ContractualRegulationNamesQuery } from './model'; |
|||
|
|||
/** |
|||
* 查询合同法规名称列表 |
|||
* @param params |
|||
* @returns |
|||
*/ |
|||
export function ContractualRegulationNamesList(params?: ContractualRegulationNamesQuery) { |
|||
return defHttp.get<ContractualRegulationNamesVO[]>({ url: '/productManagement/ContractualRegulationNames/list', params }); |
|||
} |
|||
|
|||
/** |
|||
* 导出合同法规名称列表 |
|||
* @param params |
|||
* @returns |
|||
*/ |
|||
export function ContractualRegulationNamesExport(params?: ContractualRegulationNamesQuery) { |
|||
return commonExport('/productManagement/ContractualRegulationNames/export', params ?? {}); |
|||
} |
|||
|
|||
/** |
|||
* 查询合同法规名称详细 |
|||
* @param id id |
|||
* @returns |
|||
*/ |
|||
export function ContractualRegulationNamesInfo(id: ID) { |
|||
return defHttp.get<ContractualRegulationNamesVO>({ url: '/productManagement/ContractualRegulationNames/' + id }); |
|||
} |
|||
|
|||
/** |
|||
* 新增合同法规名称 |
|||
* @param data |
|||
* @returns |
|||
*/ |
|||
export function ContractualRegulationNamesAdd(data: ContractualRegulationNamesForm) { |
|||
return defHttp.postWithMsg<void>({ url: '/productManagement/ContractualRegulationNames', data }); |
|||
} |
|||
|
|||
/** |
|||
* 更新合同法规名称 |
|||
* @param data |
|||
* @returns |
|||
*/ |
|||
export function ContractualRegulationNamesUpdate(data: ContractualRegulationNamesForm) { |
|||
return defHttp.putWithMsg<void>({ url: '/productManagement/ContractualRegulationNames', data }); |
|||
} |
|||
|
|||
/** |
|||
* 更新合同法规名称状态 |
|||
* @param id 主键 |
|||
* @param status 状态 |
|||
* @returns |
|||
*/ |
|||
export function ContractualRegulationNamesUpdateStatus(id: ID, status: string) { |
|||
return defHttp.putWithMsg<void>({ |
|||
url: '/productManagement/ContractualRegulationNames/updateStatus', |
|||
data: { id, isEffective: status } |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 删除合同法规名称 |
|||
* @param id id |
|||
* @returns |
|||
*/ |
|||
export function ContractualRegulationNamesRemove(id: ID | IDS) { |
|||
return defHttp.deleteWithMsg<void>({ url: '/productManagement/ContractualRegulationNames/' + id },); |
|||
} |
|||
|
|||
/** |
|||
* 获取法规条款列表 |
|||
* @param id 法规ID |
|||
* @returns |
|||
*/ |
|||
export function ContractualRegulationNamesArticles(id: ID) { |
|||
return defHttp.get<any[]>({ url: '/productManagement/ContractualRegulationNames/articles/' + id }); |
|||
} |
|||
|
|||
/** |
|||
* 生成法规PDF文档(查看详情) |
|||
* @param id 法规ID |
|||
* @returns PDF响应数据 |
|||
*/ |
|||
export function ContractualRegulationNamesViewPdf(id: ID) { |
|||
return defHttp.get( |
|||
{ |
|||
url: '/productManagement/ContractualRegulationNames/view/' + id, |
|||
responseType: 'blob', |
|||
timeout: 1000 * 60 * 10, |
|||
headers: { |
|||
Accept: 'application/pdf', |
|||
} |
|||
}, |
|||
{ isReturnNativeResponse: true } |
|||
); |
|||
} |
@ -0,0 +1,98 @@ |
|||
import { BaseEntity, PageQuery } from '@/api/base'; |
|||
|
|||
export interface ContractualRegulationNamesVO { |
|||
/** |
|||
* 主键ID |
|||
*/ |
|||
id: string | number; |
|||
|
|||
/** |
|||
* 法规名称 |
|||
*/ |
|||
regulationName: string; |
|||
|
|||
/** |
|||
* 法规描述 |
|||
*/ |
|||
regulationDescription: string; |
|||
|
|||
/** |
|||
* 发布日期 |
|||
*/ |
|||
publishDate: string; |
|||
|
|||
/** |
|||
* 是否有效(Y:有效 N:无效) |
|||
*/ |
|||
isEffective: string; |
|||
|
|||
/** |
|||
* 当前状态 |
|||
*/ |
|||
progressStatus?: string; |
|||
|
|||
/** |
|||
* OSS文件ID |
|||
*/ |
|||
ossId?: string | number; |
|||
} |
|||
|
|||
export interface ContractualRegulationNamesForm extends BaseEntity { |
|||
/** |
|||
* 主键ID |
|||
*/ |
|||
id?: string | number; |
|||
|
|||
/** |
|||
* 法规名称 |
|||
*/ |
|||
regulationName?: string; |
|||
|
|||
/** |
|||
* 法规描述 |
|||
*/ |
|||
regulationDescription?: string; |
|||
|
|||
/** |
|||
* 发布日期 |
|||
*/ |
|||
publishDate?: string; |
|||
|
|||
/** |
|||
* 是否有效(Y:有效 N:无效) |
|||
*/ |
|||
isEffective?: string; |
|||
|
|||
/** |
|||
* 当前状态 |
|||
*/ |
|||
progressStatus?: string; |
|||
|
|||
/** |
|||
* OSS文件ID |
|||
*/ |
|||
ossId?: string | number; |
|||
} |
|||
|
|||
export interface ContractualRegulationNamesQuery extends PageQuery { |
|||
|
|||
/** |
|||
* 法规名称 |
|||
*/ |
|||
regulationName?: string; |
|||
|
|||
/** |
|||
* 发布日期 |
|||
*/ |
|||
publishDate?: string; |
|||
|
|||
/** |
|||
* 是否有效(Y:有效 N:无效) |
|||
*/ |
|||
isEffective?: string; |
|||
|
|||
/** |
|||
* 日期范围参数 |
|||
*/ |
|||
params?: any; |
|||
} |
@ -0,0 +1,133 @@ |
|||
import { defHttp } from '@/utils/http/axios'; |
|||
import { ID, IDS } from '@/api/base'; |
|||
import { ContractualTaskResultDetailVO } from './model'; |
|||
import { message } from 'ant-design-vue'; |
|||
|
|||
/** |
|||
* 通用文件下载函数 |
|||
* @param url 下载地址 |
|||
* @param onError 错误处理回调 |
|||
* @returns Promise<boolean> 下载是否成功 |
|||
*/ |
|||
export async function useDownload( |
|||
url: string, |
|||
onError?: (error: any) => void |
|||
): Promise<boolean> { |
|||
try { |
|||
const response = await defHttp.get( |
|||
{ |
|||
url, |
|||
responseType: 'blob', |
|||
timeout: 60000, // 设置较长的超时时间
|
|||
}, |
|||
{ |
|||
isReturnNativeResponse: true, |
|||
// 自定义错误处理
|
|||
errorMessageMode: 'none', |
|||
} |
|||
); |
|||
|
|||
// 检查响应类型
|
|||
const contentType = response.headers['content-type']; |
|||
if (contentType && contentType.includes('application/json')) { |
|||
// 如果返回的是JSON(通常是错误信息),转换并抛出
|
|||
const reader = new FileReader(); |
|||
reader.onload = () => { |
|||
const error = JSON.parse(reader.result as string); |
|||
message.error(error.message || '下载失败'); |
|||
onError?.(error); |
|||
}; |
|||
reader.readAsText(response.data); |
|||
return false; |
|||
} |
|||
|
|||
// 获取文件名
|
|||
const contentDisposition = response.headers['content-disposition']; |
|||
let fileName = ''; |
|||
|
|||
if (contentDisposition) { |
|||
const matches = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/); |
|||
if (matches && matches[1]) { |
|||
fileName = decodeURIComponent(matches[1].replace(/['"]/g, '')); |
|||
} |
|||
} |
|||
|
|||
// 创建Blob对象
|
|||
const blob = new Blob([response.data], { |
|||
type: contentType || 'application/octet-stream' |
|||
}); |
|||
|
|||
if ((window.navigator as any).msSaveOrOpenBlob) { |
|||
// 针对IE的处理
|
|||
(window.navigator as any).msSaveOrOpenBlob(blob, fileName); |
|||
} else { |
|||
// 现代浏览器的处理
|
|||
const url = window.URL.createObjectURL(blob); |
|||
const link = document.createElement('a'); |
|||
link.href = url; |
|||
link.download = fileName; |
|||
link.style.display = 'none'; |
|||
document.body.appendChild(link); |
|||
link.click(); |
|||
document.body.removeChild(link); |
|||
window.URL.revokeObjectURL(url); |
|||
} |
|||
|
|||
return true; |
|||
} catch (error: any) { |
|||
console.error('下载失败:', error); |
|||
message.error(error.message || '下载失败,请稍后重试'); |
|||
onError?.(error); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 根据任务id查询详细分类的合同任务结果 |
|||
* @param taskId 任务ID |
|||
* @returns 详细的合同任务结果列表 |
|||
*/ |
|||
export function getDetailResultsByTaskId(taskId: ID) { |
|||
return defHttp.get<ContractualTaskResultDetailVO[]>({ url: '/productManagement/ContractualTaskResults/taskDetail/' + taskId }); |
|||
} |
|||
|
|||
/** |
|||
* 下载合同任务结果 |
|||
* @param id id |
|||
* @returns |
|||
*/ |
|||
export function ContractualTaskResultDownload(id: ID | IDS) { |
|||
return useDownload(`/productManagement/ContractualTaskResults/downloadResult/${id}`); |
|||
} |
|||
|
|||
/** |
|||
* 获取合同任务PDF文件流 |
|||
* @param taskId 任务ID |
|||
* @returns Promise<Blob> |
|||
*/ |
|||
export function getPdfStream(taskId: ID): Promise<Blob> { |
|||
return defHttp.get( |
|||
{ |
|||
url: `/productManagement/ContractualTaskResults/getPdfStream/${taskId}`, |
|||
responseType: 'blob', |
|||
timeout:600000 |
|||
}, |
|||
{ |
|||
isReturnNativeResponse: true, |
|||
errorMessageMode: 'none', |
|||
} |
|||
).then(response => response.data); |
|||
} |
|||
|
|||
/** |
|||
* 更新合同任务结果项的状态(已读/采纳) |
|||
* @param id 结果项ID |
|||
* @param field 字段名(isRead/isAdopted) |
|||
* @param value 值(0/1) |
|||
* @returns |
|||
*/ |
|||
export function updateResultItemStatus(id: ID, field: 'isRead' | 'isAdopted', value: '0' | '1') { |
|||
return defHttp.putWithMsg<void>({ |
|||
url: `/productManagement/ContractualTaskResults/updateResultItemStatus/${id}/${field}/${value}` |
|||
}); |
|||
} |
@ -0,0 +1,99 @@ |
|||
import { BaseEntity, PageQuery } from '@/api/base'; |
|||
|
|||
export interface ContractualTaskResultsVO { |
|||
/** |
|||
* 任务结果 |
|||
*/ |
|||
result?: string; |
|||
} |
|||
|
|||
export interface ContractualTaskResultDetailVO { |
|||
/** |
|||
* 分类名称 |
|||
*/ |
|||
name: string; |
|||
/** |
|||
* 结果列表 |
|||
*/ |
|||
results: { |
|||
/** |
|||
* 唯一标识ID |
|||
*/ |
|||
id?: string; |
|||
/** |
|||
* 序号 |
|||
*/ |
|||
serialNumber?: number; |
|||
/** |
|||
* 问题点名称 |
|||
*/ |
|||
issueName?: string; |
|||
/** |
|||
* 原文 |
|||
*/ |
|||
originalText?: string; |
|||
/** |
|||
* 比对原文 |
|||
*/ |
|||
comparedText?: string; |
|||
/** |
|||
* 修改后的内容 |
|||
*/ |
|||
modifiedContent?: string; |
|||
/** |
|||
* 展示修改情况 |
|||
*/ |
|||
modificationDisplay?: string; |
|||
/** |
|||
* 存在的问题 |
|||
*/ |
|||
existingIssues?: string; |
|||
/** |
|||
* 审查依据 |
|||
*/ |
|||
reviewBasis?: { |
|||
/** |
|||
* 审查内容 |
|||
*/ |
|||
reviewContent?: string; |
|||
/** |
|||
* 审查点 |
|||
*/ |
|||
reviewPoints?: string[]; |
|||
}; |
|||
/** |
|||
* 是否已读 |
|||
*/ |
|||
isRead?: string; |
|||
/** |
|||
* 是否采纳 |
|||
*/ |
|||
isAdopted?: string; |
|||
}[]; |
|||
} |
|||
|
|||
export interface ContractualTaskResultsForm extends BaseEntity { |
|||
} |
|||
|
|||
export interface ContractualTaskResultsQuery extends PageQuery { |
|||
|
|||
/** |
|||
* id |
|||
*/ |
|||
id?: string | number; |
|||
|
|||
/** |
|||
* 任务id |
|||
*/ |
|||
contractualTaskId?: string | number; |
|||
|
|||
/** |
|||
* 任务结果 |
|||
*/ |
|||
result?: string; |
|||
|
|||
/** |
|||
* 日期范围参数 |
|||
*/ |
|||
params?: any; |
|||
} |
Before Width: | Height: | Size: 368 B |
@ -0,0 +1,497 @@ |
|||
<template> |
|||
<Modal |
|||
:open="visible" |
|||
title="新增合同案例文件" |
|||
@ok="handleSubmit" |
|||
@cancel="handleCancel" |
|||
:confirmLoading="loading" |
|||
width="600px" |
|||
> |
|||
<Form |
|||
ref="formRef" |
|||
:model="formData" |
|||
:rules="rules" |
|||
layout="vertical" |
|||
> |
|||
<!-- 文件上传 --> |
|||
<FormItem label="案例文件" name="caseFile" :required="!hasUploadedFile"> |
|||
<div class="upload-box" :class="{'file-preview': fileList && fileList.length > 0}"> |
|||
<template v-if="fileList && fileList.length > 0"> |
|||
<!-- 文件预览 --> |
|||
<div class="file-info"> |
|||
<FileTextOutlined class="file-icon" /> |
|||
<div class="file-details"> |
|||
<p class="file-name">{{ fileList[0].name }}</p> |
|||
<div class="file-progress" v-if="uploading"> |
|||
<Progress :percent="uploadPercent" size="small" status="active" /> |
|||
</div> |
|||
<p class="file-status" v-else> |
|||
<CheckCircleFilled class="status-icon success" /> 上传成功 |
|||
<Button type="link" class="remove-btn" @click="removeFile">删除</Button> |
|||
</p> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
<template v-else> |
|||
<!-- 上传区域 --> |
|||
<Upload |
|||
:fileList="fileList" |
|||
:customRequest="customUploadRequest" |
|||
:beforeUpload="beforeUpload" |
|||
:showUploadList="false" |
|||
:maxCount="1" |
|||
:multiple="false" |
|||
name="file" |
|||
accept=".doc,.docx" |
|||
:disabled="uploading" |
|||
> |
|||
<div class="upload-content"> |
|||
<div class="upload-icon"> |
|||
<UploadOutlined class="upload-arrow-icon" /> |
|||
</div> |
|||
<div class="upload-text-container"> |
|||
<p class="upload-text"> |
|||
<a class="upload-link">选择案例文件</a> |
|||
</p> |
|||
<p class="upload-hint">支持DOC、DOCX格式文件,最大500MB</p> |
|||
</div> |
|||
</div> |
|||
</Upload> |
|||
</template> |
|||
</div> |
|||
</FormItem> |
|||
|
|||
<!-- 案例名称 --> |
|||
<FormItem label="案例名称" name="caseName" required> |
|||
<Input |
|||
v-model:value="formData.caseName" |
|||
:disabled="caseNameDisabled" |
|||
:placeholder="caseNamePlaceholder" |
|||
/> |
|||
</FormItem> |
|||
|
|||
<!-- 案例描述 --> |
|||
<FormItem label="案例描述" name="caseDescription" required> |
|||
<TextArea |
|||
v-model:value="formData.caseDescription" |
|||
placeholder="请输入案例描述" |
|||
:rows="4" |
|||
show-count |
|||
:maxlength="500" |
|||
/> |
|||
</FormItem> |
|||
|
|||
<!-- 案例类型 --> |
|||
<FormItem label="案例类型" name="caseType"> |
|||
<Input v-model:value="formData.caseType" placeholder="请输入案例类型" /> |
|||
</FormItem> |
|||
|
|||
<!-- 发布日期 --> |
|||
<FormItem label="发布日期" name="publishDate" required> |
|||
<DatePicker |
|||
v-model:value="formData.publishDate" |
|||
format="YYYY-MM-DD" |
|||
valueFormat="YYYY-MM-DD" |
|||
style="width: 100%" |
|||
/> |
|||
</FormItem> |
|||
|
|||
<!-- 是否有效 --> |
|||
<FormItem label="是否有效" name="isEffective"> |
|||
<Select |
|||
v-model:value="formData.isEffective" |
|||
:options="effectiveOptions" |
|||
/> |
|||
</FormItem> |
|||
</Form> |
|||
</Modal> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
import { ref, reactive, computed, watch } from 'vue'; |
|||
import { |
|||
Modal, |
|||
Form, |
|||
FormItem, |
|||
Input, |
|||
DatePicker, |
|||
Select, |
|||
Upload, |
|||
Button, |
|||
Progress, |
|||
message |
|||
} from 'ant-design-vue'; |
|||
import { |
|||
UploadOutlined, |
|||
FileTextOutlined, |
|||
CheckCircleFilled |
|||
} from '@ant-design/icons-vue'; |
|||
import { ContractualCaseFilesAdd } from '@/api/contractReview/ContractualCaseFiles'; |
|||
import { uploadApi } from '@/api/upload'; |
|||
import { ossRemove } from '@/api/system/oss'; |
|||
import { getDictOptions } from '@/utils/dict'; |
|||
import { UploadFileParams } from '#/axios'; |
|||
import dayjs from 'dayjs'; |
|||
const { TextArea } = Input; |
|||
|
|||
interface Props { |
|||
visible: boolean; |
|||
} |
|||
|
|||
const props = defineProps<Props>(); |
|||
const emit = defineEmits(['update:visible', 'success']); |
|||
|
|||
// 状态变量 |
|||
const loading = ref(false); |
|||
const fileList = ref<any[]>([]); |
|||
const uploading = ref(false); |
|||
const uploadPercent = ref(0); |
|||
const currentOssId = ref<string | null>(null); |
|||
const formRef = ref(); |
|||
|
|||
// 表单数据 |
|||
const formData = reactive({ |
|||
caseName: '', |
|||
caseDescription: '', |
|||
caseType: '', |
|||
publishDate: dayjs(), |
|||
isEffective: 'Y', |
|||
progressStatus: 'PENDING' |
|||
}); |
|||
|
|||
// 计算属性 |
|||
const hasUploadedFile = computed(() => { |
|||
return !!currentOssId.value; |
|||
}); |
|||
|
|||
const caseNameDisabled = computed(() => { |
|||
return !currentOssId.value; |
|||
}); |
|||
|
|||
const caseNamePlaceholder = computed(() => { |
|||
return currentOssId.value ? '可编辑案例名称' : '请先上传案例文件'; |
|||
}); |
|||
|
|||
// 表单验证规则 - 动态规则 |
|||
const rules = computed(() => ({ |
|||
caseFile: hasUploadedFile.value ? [] : [{ required: true, message: '请上传案例文件' }], |
|||
caseName: [{ required: true, message: '请输入案例名称' }], |
|||
caseDescription: [{ required: true, message: '请输入案例描述' }], |
|||
publishDate: [{ required: true, message: '请选择发布日期' }], |
|||
})); |
|||
|
|||
// 字典选项 |
|||
const effectiveOptions = getDictOptions('sys_yes_no'); |
|||
|
|||
// 监听弹窗关闭,重置表单 |
|||
watch(() => props.visible, (newVal) => { |
|||
if (!newVal) { |
|||
resetForm(); |
|||
} |
|||
}); |
|||
|
|||
// 文件上传相关处理 |
|||
function customUploadRequest(options: any) { |
|||
const { file, onSuccess, onError } = options; |
|||
|
|||
uploading.value = true; |
|||
uploadPercent.value = 0; |
|||
|
|||
const uploadParams: UploadFileParams = { |
|||
name: 'file', |
|||
file: file, |
|||
data: {}, |
|||
}; |
|||
|
|||
fileList.value = [ |
|||
{ |
|||
uid: '1', |
|||
name: file.name, |
|||
status: 'uploading', |
|||
url: URL.createObjectURL(file), |
|||
}, |
|||
]; |
|||
|
|||
uploadApi( |
|||
uploadParams, |
|||
(progressEvent) => { |
|||
if (progressEvent.total) { |
|||
const percent = Math.round((progressEvent.loaded / progressEvent.total) * 100); |
|||
uploadPercent.value = percent; |
|||
} |
|||
} |
|||
).then((res) => { |
|||
if (fileList.value && fileList.value.length > 0) { |
|||
fileList.value[0].status = 'done'; |
|||
fileList.value[0].response = res; |
|||
if (res && res.ossId) { |
|||
currentOssId.value = res.ossId; |
|||
const fileName = file.name; |
|||
const nameWithoutExt = fileName.substring(0, fileName.lastIndexOf('.')) || fileName; |
|||
formData.caseName = nameWithoutExt; |
|||
|
|||
// 文件上传成功后,清除文件字段的验证错误 |
|||
formRef.value?.clearValidate('caseFile'); |
|||
} |
|||
} |
|||
uploading.value = false; |
|||
uploadPercent.value = 100; |
|||
message.success(`${file.name} 文件上传成功`); |
|||
onSuccess(res); |
|||
}).catch((err) => { |
|||
uploading.value = false; |
|||
fileList.value = []; |
|||
currentOssId.value = null; |
|||
message.error(`文件上传失败: ${err.message || '未知错误'}`); |
|||
onError(err); |
|||
}); |
|||
} |
|||
|
|||
function beforeUpload(file: File) { |
|||
const isDoc = file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || |
|||
file.type === 'application/msword'; |
|||
|
|||
if (!isDoc) { |
|||
message.error('只能上传 DOC/DOCX 格式的文件!'); |
|||
return false; |
|||
} |
|||
|
|||
const isLt500M = file.size / 1024 / 1024 < 500; |
|||
if (!isLt500M) { |
|||
message.error('文件大小不能超过 500MB!'); |
|||
return false; |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
function removeFile() { |
|||
if (uploading.value) { |
|||
message.warning('文件正在上传中,请稍后再试'); |
|||
return; |
|||
} |
|||
|
|||
if (currentOssId.value) { |
|||
ossRemove([currentOssId.value]) |
|||
.then(() => { |
|||
fileList.value = []; |
|||
uploadPercent.value = 0; |
|||
currentOssId.value = null; |
|||
formData.caseName = ''; |
|||
message.success('文件已删除'); |
|||
}) |
|||
.catch((err) => { |
|||
message.error(`文件删除失败: ${err.message || '未知错误'}`); |
|||
}); |
|||
} else { |
|||
fileList.value = []; |
|||
uploadPercent.value = 0; |
|||
formData.caseName = ''; |
|||
message.success('文件已删除'); |
|||
} |
|||
} |
|||
|
|||
// 过滤空值参数 |
|||
function filterEmptyParams(params: any) { |
|||
const filteredParams: any = {}; |
|||
Object.keys(params).forEach(key => { |
|||
const value = params[key]; |
|||
if (value !== null && value !== undefined && value !== '') { |
|||
filteredParams[key] = value; |
|||
} |
|||
}); |
|||
return filteredParams; |
|||
} |
|||
|
|||
// 提交表单 |
|||
async function handleSubmit() { |
|||
try { |
|||
await formRef.value?.validate(); |
|||
} catch (error) { |
|||
return; |
|||
} |
|||
|
|||
if (!currentOssId.value) { |
|||
message.warning('请上传案例文件'); |
|||
return; |
|||
} |
|||
|
|||
loading.value = true; |
|||
try { |
|||
const baseData = { |
|||
...formData, |
|||
publishDate: formData.publishDate ? formData.publishDate.format('YYYY-MM-DD') : '', |
|||
ossId: currentOssId.value |
|||
}; |
|||
|
|||
const submitData = filterEmptyParams(baseData); |
|||
|
|||
await ContractualCaseFilesAdd(submitData); |
|||
message.success('新增成功'); |
|||
|
|||
// TODO: 预留代码空位 - 发起解析API请求 |
|||
// await parseCaseFile(currentOssId.value); |
|||
|
|||
emit('update:visible', false); |
|||
emit('success'); |
|||
resetForm(); |
|||
} catch (error) { |
|||
message.error('新增失败'); |
|||
} finally { |
|||
loading.value = false; |
|||
} |
|||
} |
|||
|
|||
// 取消 |
|||
function handleCancel() { |
|||
emit('update:visible', false); |
|||
resetForm(); |
|||
} |
|||
|
|||
// 重置表单 |
|||
function resetForm() { |
|||
Object.assign(formData, { |
|||
caseName: '', |
|||
caseDescription: '', |
|||
caseType: '', |
|||
publishDate: dayjs(), |
|||
isEffective: 'Y', |
|||
progressStatus: 'PENDING' |
|||
}); |
|||
fileList.value = []; |
|||
uploadPercent.value = 0; |
|||
currentOssId.value = null; |
|||
formRef.value?.clearValidate(); |
|||
} |
|||
</script> |
|||
|
|||
<style lang="less" scoped> |
|||
/* 文件上传样式 */ |
|||
.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; |
|||
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; |
|||
} |
|||
} |
|||
|
|||
.upload-content { |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: center; |
|||
width: 100%; |
|||
} |
|||
|
|||
.upload-icon { |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
margin-bottom: 16px; |
|||
background-color: #e6fffb; |
|||
width: 50px; |
|||
height: 50px; |
|||
border-radius: 50%; |
|||
} |
|||
|
|||
.upload-arrow-icon { |
|||
font-size: 24px; |
|||
color: #13c2c2; |
|||
} |
|||
|
|||
.upload-text-container { |
|||
text-align: center; |
|||
} |
|||
|
|||
.upload-text { |
|||
font-size: 16px; |
|||
color: #333; |
|||
margin-bottom: 8px; |
|||
} |
|||
|
|||
.upload-link { |
|||
color: #13c2c2; |
|||
cursor: pointer; |
|||
text-decoration: none; |
|||
font-weight: 600; |
|||
} |
|||
|
|||
.upload-link:hover { |
|||
text-decoration: underline; |
|||
} |
|||
|
|||
.upload-hint { |
|||
color: #888; |
|||
font-size: 14px; |
|||
margin-top: 8px; |
|||
} |
|||
|
|||
.file-info { |
|||
display: flex; |
|||
align-items: center; |
|||
width: 100%; |
|||
max-width: 800px; |
|||
} |
|||
|
|||
.file-icon { |
|||
font-size: 32px; |
|||
color: #13c2c2; |
|||
margin-right: 16px; |
|||
} |
|||
|
|||
.file-details { |
|||
flex: 1; |
|||
} |
|||
|
|||
.file-name { |
|||
font-size: 16px; |
|||
font-weight: 500; |
|||
margin: 0 0 8px 0; |
|||
color: #333; |
|||
} |
|||
|
|||
.file-progress { |
|||
margin: 0 0 8px 0; |
|||
width: 100%; |
|||
} |
|||
|
|||
.file-status { |
|||
font-size: 14px; |
|||
color: #666; |
|||
margin: 0; |
|||
display: flex; |
|||
align-items: center; |
|||
} |
|||
|
|||
.status-icon { |
|||
margin-right: 6px; |
|||
} |
|||
|
|||
.status-icon.success { |
|||
color: #52c41a; |
|||
} |
|||
|
|||
.remove-btn { |
|||
padding: 0; |
|||
margin-left: 12px; |
|||
font-size: 14px; |
|||
} |
|||
</style> |
@ -0,0 +1,460 @@ |
|||
<template> |
|||
<Drawer |
|||
v-model:open="visible" |
|||
title="修改合同案例" |
|||
width="80%" |
|||
:destroyOnClose="true" |
|||
class="edit-case-drawer" |
|||
placement="right" |
|||
> |
|||
<template #extra> |
|||
<Space> |
|||
<Button @click="handleCancel">取消</Button> |
|||
<Button type="primary" @click="handleSubmit" :loading="loading">保存</Button> |
|||
</Space> |
|||
</template> |
|||
|
|||
<div class="edit-container"> |
|||
<!-- 左侧PDF预览 --> |
|||
<div class="pdf-preview-section"> |
|||
<div class="section-title"> |
|||
<FileTextOutlined /> |
|||
<span>案例文档预览</span> |
|||
<Button type="link" size="small" @click="refreshPdf" :loading="pdfLoading"> |
|||
<ReloadOutlined /> |
|||
</Button> |
|||
</div> |
|||
|
|||
<div class="pdf-container"> |
|||
<div v-if="pdfLoading" class="loading-container"> |
|||
<Spin size="large" tip="正在加载文档..." /> |
|||
</div> |
|||
|
|||
<div v-else-if="pdfError" class="error-container"> |
|||
<Result |
|||
status="error" |
|||
:title="pdfError" |
|||
sub-title="请检查网络连接或联系管理员" |
|||
> |
|||
<template #extra> |
|||
<Button type="primary" @click="refreshPdf">重新加载</Button> |
|||
</template> |
|||
</Result> |
|||
</div> |
|||
|
|||
<div v-else-if="pdfUrl" class="pdf-embed-container"> |
|||
<embed |
|||
:src="pdfUrl" |
|||
type="application/pdf" |
|||
width="100%" |
|||
height="100%" |
|||
style="border: none; display: block;" |
|||
/> |
|||
</div> |
|||
|
|||
<div v-else class="no-pdf-container"> |
|||
<Empty description="暂无PDF文档" /> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 右侧修改表单 --> |
|||
<div class="edit-form-section"> |
|||
<div class="section-title"> |
|||
<EditOutlined /> |
|||
<span>修改信息</span> |
|||
</div> |
|||
|
|||
<div class="form-container"> |
|||
<Form |
|||
ref="formRef" |
|||
:model="formData" |
|||
:rules="rules" |
|||
layout="vertical" |
|||
class="edit-form" |
|||
> |
|||
<!-- 案例名称 --> |
|||
<FormItem label="案例名称" name="caseName" required> |
|||
<Input |
|||
v-model:value="formData.caseName" |
|||
placeholder="请输入案例名称" |
|||
size="large" |
|||
/> |
|||
</FormItem> |
|||
|
|||
<!-- 案例描述 --> |
|||
<FormItem label="案例描述" name="caseDescription"> |
|||
<TextArea |
|||
v-model:value="formData.caseDescription" |
|||
placeholder="请输入案例描述" |
|||
:rows="6" |
|||
show-count |
|||
:maxlength="500" |
|||
size="large" |
|||
/> |
|||
</FormItem> |
|||
|
|||
<!-- 案例类型 --> |
|||
<FormItem label="案例类型" name="caseType"> |
|||
<Input |
|||
v-model:value="formData.caseType" |
|||
placeholder="请输入案例类型" |
|||
size="large" |
|||
/> |
|||
</FormItem> |
|||
|
|||
<!-- 发布日期 --> |
|||
<FormItem label="发布日期" name="publishDate"> |
|||
<DatePicker |
|||
v-model:value="formData.publishDate" |
|||
format="YYYY-MM-DD" |
|||
valueFormat="YYYY-MM-DD" |
|||
style="width: 100%" |
|||
size="large" |
|||
/> |
|||
</FormItem> |
|||
|
|||
<!-- 是否有效 --> |
|||
<FormItem label="是否有效" name="isEffective"> |
|||
<Select |
|||
v-model:value="formData.isEffective" |
|||
:options="effectiveOptions" |
|||
size="large" |
|||
/> |
|||
</FormItem> |
|||
</Form> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</Drawer> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
import { ref, reactive, computed, watch, onUnmounted } from 'vue'; |
|||
import { |
|||
Drawer, |
|||
Form, |
|||
FormItem, |
|||
Input, |
|||
DatePicker, |
|||
Select, |
|||
Button, |
|||
Space, |
|||
Spin, |
|||
Result, |
|||
Empty, |
|||
message |
|||
} from 'ant-design-vue'; |
|||
import { |
|||
FileTextOutlined, |
|||
EditOutlined, |
|||
ReloadOutlined |
|||
} from '@ant-design/icons-vue'; |
|||
import { |
|||
ContractualCaseFilesInfo, |
|||
ContractualCaseFilesUpdate, |
|||
ContractualCaseFilesViewPdf |
|||
} from '@/api/contractReview/ContractualCaseFiles'; |
|||
import { getDictOptions } from '@/utils/dict'; |
|||
import dayjs from 'dayjs'; |
|||
|
|||
const { TextArea } = Input; |
|||
|
|||
interface Props { |
|||
visible: boolean; |
|||
caseFileId?: number | string; |
|||
} |
|||
|
|||
const props = withDefaults(defineProps<Props>(), { |
|||
visible: false, |
|||
caseFileId: undefined |
|||
}); |
|||
|
|||
const emit = defineEmits<{ |
|||
'update:visible': [value: boolean]; |
|||
'success': []; |
|||
}>(); |
|||
|
|||
// 状态变量 |
|||
const loading = ref(false); |
|||
const pdfLoading = ref(false); |
|||
const pdfError = ref(''); |
|||
const pdfUrl = ref(''); |
|||
const formRef = ref(); |
|||
|
|||
// 表单数据 |
|||
const formData = reactive({ |
|||
id: undefined as string | number | undefined, |
|||
caseName: '', |
|||
caseDescription: '', |
|||
caseType: '', |
|||
publishDate: '', |
|||
isEffective: 'Y' |
|||
}); |
|||
|
|||
// 计算属性 |
|||
const visible = computed({ |
|||
get: () => props.visible, |
|||
set: (value) => emit('update:visible', value) |
|||
}); |
|||
|
|||
// 表单验证规则 |
|||
const rules = computed(() => ({ |
|||
caseName: [{ required: true, message: '请输入案例名称', trigger: 'blur' }], |
|||
})); |
|||
|
|||
// 字典选项 |
|||
const effectiveOptions = getDictOptions('sys_yes_no'); |
|||
|
|||
// 监听弹窗显示 |
|||
watch(() => props.visible, (newVisible) => { |
|||
if (newVisible && props.caseFileId) { |
|||
loadCaseData(); |
|||
loadPdf(); |
|||
} else if (!newVisible) { |
|||
resetForm(); |
|||
cleanupPdfUrl(); |
|||
} |
|||
}); |
|||
|
|||
// 加载案例数据 |
|||
async function loadCaseData() { |
|||
if (!props.caseFileId) return; |
|||
|
|||
try { |
|||
const result = await ContractualCaseFilesInfo(props.caseFileId); |
|||
if (result) { |
|||
formData.id = result.id; |
|||
formData.caseName = result.caseName || ''; |
|||
formData.caseDescription = result.caseDescription || ''; |
|||
formData.caseType = result.caseType || ''; |
|||
formData.publishDate = result.publishDate || ''; |
|||
formData.isEffective = result.isEffective || 'Y'; |
|||
} |
|||
} catch (error: any) { |
|||
message.error('加载案例信息失败: ' + (error.message || '未知错误')); |
|||
} |
|||
} |
|||
|
|||
// 加载PDF |
|||
async function loadPdf() { |
|||
if (!props.caseFileId) return; |
|||
|
|||
pdfLoading.value = true; |
|||
pdfError.value = ''; |
|||
|
|||
try { |
|||
const pdfResponse = await ContractualCaseFilesViewPdf(props.caseFileId); |
|||
|
|||
if (pdfResponse?.data) { |
|||
// 清理之前的URL |
|||
cleanupPdfUrl(); |
|||
|
|||
// 创建新的Blob URL |
|||
const blob = new Blob([pdfResponse.data], { type: 'application/pdf' }); |
|||
pdfUrl.value = URL.createObjectURL(blob); |
|||
} else { |
|||
pdfError.value = 'PDF数据格式错误'; |
|||
} |
|||
} catch (err: any) { |
|||
pdfError.value = err.message || '加载PDF失败'; |
|||
console.error('加载PDF失败:', err); |
|||
} finally { |
|||
pdfLoading.value = false; |
|||
} |
|||
} |
|||
|
|||
// 刷新PDF |
|||
function refreshPdf() { |
|||
loadPdf(); |
|||
} |
|||
|
|||
// 提交表单 |
|||
async function handleSubmit() { |
|||
try { |
|||
await formRef.value?.validate(); |
|||
} catch (error) { |
|||
return; |
|||
} |
|||
|
|||
loading.value = true; |
|||
try { |
|||
const submitData = { |
|||
...formData, |
|||
publishDate: formData.publishDate ? dayjs(formData.publishDate).format('YYYY-MM-DD') : '' |
|||
}; |
|||
|
|||
await ContractualCaseFilesUpdate(submitData); |
|||
message.success('修改成功'); |
|||
|
|||
emit('update:visible', false); |
|||
emit('success'); |
|||
} catch (error: any) { |
|||
message.error('修改失败: ' + (error.message || '未知错误')); |
|||
} finally { |
|||
loading.value = false; |
|||
} |
|||
} |
|||
|
|||
// 取消 |
|||
function handleCancel() { |
|||
emit('update:visible', false); |
|||
} |
|||
|
|||
// 重置表单 |
|||
function resetForm() { |
|||
Object.assign(formData, { |
|||
id: undefined, |
|||
caseName: '', |
|||
caseDescription: '', |
|||
caseType: '', |
|||
publishDate: '', |
|||
isEffective: 'Y' |
|||
}); |
|||
formRef.value?.clearValidate(); |
|||
pdfError.value = ''; |
|||
} |
|||
|
|||
// 清理Blob URL |
|||
function cleanupPdfUrl() { |
|||
if (pdfUrl.value && pdfUrl.value.startsWith('blob:')) { |
|||
URL.revokeObjectURL(pdfUrl.value); |
|||
pdfUrl.value = ''; |
|||
} |
|||
} |
|||
|
|||
// 组件销毁时清理 |
|||
onUnmounted(() => { |
|||
cleanupPdfUrl(); |
|||
}); |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.edit-case-drawer { |
|||
:deep(.ant-drawer-content) { |
|||
height: 100vh; |
|||
} |
|||
|
|||
:deep(.ant-drawer-body) { |
|||
padding: 0; |
|||
height: calc(100vh - 55px); |
|||
} |
|||
} |
|||
|
|||
.edit-container { |
|||
display: flex; |
|||
height: 100%; |
|||
gap: 1px; |
|||
background: #f0f2f5; |
|||
} |
|||
|
|||
.pdf-preview-section { |
|||
flex: 1; |
|||
background: #fff; |
|||
display: flex; |
|||
flex-direction: column; |
|||
} |
|||
|
|||
.edit-form-section { |
|||
width: 400px; |
|||
background: #fff; |
|||
display: flex; |
|||
flex-direction: column; |
|||
border-left: 1px solid #e8e8e8; |
|||
} |
|||
|
|||
.section-title { |
|||
padding: 16px 20px; |
|||
font-size: 16px; |
|||
font-weight: 500; |
|||
color: #262626; |
|||
border-bottom: 1px solid #e8e8e8; |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 8px; |
|||
background: #fafafa; |
|||
|
|||
span { |
|||
flex: 1; |
|||
} |
|||
} |
|||
|
|||
.pdf-container { |
|||
flex: 1; |
|||
position: relative; |
|||
background: #f5f5f5; |
|||
|
|||
.loading-container { |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
height: 100%; |
|||
} |
|||
|
|||
.error-container { |
|||
padding: 40px; |
|||
height: 100%; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
} |
|||
|
|||
.pdf-embed-container { |
|||
height: 100%; |
|||
padding: 16px; |
|||
|
|||
embed { |
|||
border-radius: 6px; |
|||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); |
|||
} |
|||
} |
|||
|
|||
.no-pdf-container { |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
height: 100%; |
|||
} |
|||
} |
|||
|
|||
.form-container { |
|||
flex: 1; |
|||
padding: 20px; |
|||
overflow-y: auto; |
|||
} |
|||
|
|||
.edit-form { |
|||
.ant-form-item { |
|||
margin-bottom: 24px; |
|||
} |
|||
|
|||
.ant-form-item-label > label { |
|||
font-weight: 500; |
|||
color: #262626; |
|||
} |
|||
} |
|||
|
|||
/* 响应式设计 */ |
|||
@media (max-width: 1400px) { |
|||
.edit-form-section { |
|||
width: 380px; |
|||
} |
|||
} |
|||
|
|||
@media (max-width: 1200px) { |
|||
.edit-container { |
|||
flex-direction: column; |
|||
} |
|||
|
|||
.pdf-preview-section { |
|||
height: 50%; |
|||
} |
|||
|
|||
.edit-form-section { |
|||
width: 100%; |
|||
height: 50%; |
|||
border-left: none; |
|||
border-top: 1px solid #e8e8e8; |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,382 @@ |
|||
<template> |
|||
<Drawer |
|||
v-model:open="visible" |
|||
:title="modalTitle" |
|||
width="70%" |
|||
:destroyOnClose="true" |
|||
class="case-view-drawer" |
|||
placement="right" |
|||
> |
|||
<template #extra> |
|||
<Space> |
|||
<Button type="primary" @click="downloadPdf" :loading="downloading"> |
|||
<template #icon><DownloadOutlined /></template> |
|||
下载PDF |
|||
</Button> |
|||
<Button @click="refreshPdf"> |
|||
<template #icon><ReloadOutlined /></template> |
|||
刷新 |
|||
</Button> |
|||
</Space> |
|||
</template> |
|||
|
|||
<div class="case-view-container"> |
|||
<!-- PDF预览区域 --> |
|||
<div class="pdf-container"> |
|||
<div v-if="loading" class="loading-container"> |
|||
<Spin size="large" tip="正在加载案例文档..." /> |
|||
</div> |
|||
|
|||
<div v-else-if="error" class="error-container"> |
|||
<Result |
|||
status="error" |
|||
:title="error" |
|||
sub-title="请检查网络连接或联系管理员" |
|||
> |
|||
<template #extra> |
|||
<Button type="primary" @click="refreshPdf">重新加载</Button> |
|||
</template> |
|||
</Result> |
|||
</div> |
|||
|
|||
<div v-else class="pdf-embed-container"> |
|||
<embed |
|||
:src="pdfUrl" |
|||
type="application/pdf" |
|||
width="100%" |
|||
height="100%" |
|||
style="border: none; display: block;" |
|||
/> |
|||
<!-- 备选方案:如果embed不支持,显示iframe --> |
|||
<iframe |
|||
v-if="showIframe" |
|||
:src="pdfUrl" |
|||
width="100%" |
|||
height="100%" |
|||
style="border: none; display: block;" |
|||
></iframe> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 条款列表(备选展示方式) --> |
|||
<div v-if="showArticlesList" class="articles-list"> |
|||
<Divider>案例条款详情</Divider> |
|||
<div v-for="(article, index) in articles" :key="article.id" class="article-item"> |
|||
<div class="article-title">第{{ index + 1 }}条</div> |
|||
<div class="article-content">{{ article.articleContent }}</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</Drawer> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
import { ref, computed, watch, onUnmounted } from 'vue'; |
|||
import { |
|||
Drawer, |
|||
Button, |
|||
Space, |
|||
Spin, |
|||
Result, |
|||
Divider, |
|||
message |
|||
} from 'ant-design-vue'; |
|||
import { |
|||
DownloadOutlined, |
|||
ReloadOutlined |
|||
} from '@ant-design/icons-vue'; |
|||
import { |
|||
ContractualCaseFilesViewPdf, |
|||
ContractualCaseFilesArticles |
|||
} from '@/api/contractReview/ContractualCaseFiles'; |
|||
|
|||
interface Props { |
|||
visible: boolean; |
|||
caseFileId?: number | string; |
|||
caseName?: string; |
|||
} |
|||
|
|||
const props = withDefaults(defineProps<Props>(), { |
|||
visible: false, |
|||
caseFileId: undefined, |
|||
caseName: '' |
|||
}); |
|||
|
|||
const emit = defineEmits<{ |
|||
'update:visible': [value: boolean]; |
|||
}>(); |
|||
|
|||
// 状态变量 |
|||
const loading = ref(false); |
|||
const downloading = ref(false); |
|||
const error = ref(''); |
|||
const articles = ref<any[]>([]); |
|||
const pdfUrl = ref(''); |
|||
const showIframe = ref(false); |
|||
const showArticlesList = ref(false); |
|||
|
|||
// 计算属性 |
|||
const visible = computed({ |
|||
get: () => props.visible, |
|||
set: (value) => emit('update:visible', value) |
|||
}); |
|||
|
|||
const modalTitle = computed(() => { |
|||
return props.caseName ? `案例详情 - ${props.caseName}` : '案例详情'; |
|||
}); |
|||
|
|||
// 监听弹窗显示 |
|||
watch(() => props.visible, (newVisible) => { |
|||
if (newVisible && props.caseFileId) { |
|||
loadCaseDetail(); |
|||
} else if (!newVisible) { |
|||
// 弹窗关闭时清理资源 |
|||
cleanupPdfUrl(); |
|||
error.value = ''; |
|||
showIframe.value = false; |
|||
showArticlesList.value = false; |
|||
} |
|||
}); |
|||
|
|||
// 加载案例详情 |
|||
async function loadCaseDetail() { |
|||
if (!props.caseFileId) return; |
|||
|
|||
loading.value = true; |
|||
error.value = ''; |
|||
|
|||
try { |
|||
// 并行加载PDF和条款列表 |
|||
const [pdfResponse, articlesResult] = await Promise.all([ |
|||
ContractualCaseFilesViewPdf(props.caseFileId), |
|||
ContractualCaseFilesArticles(props.caseFileId) |
|||
]); |
|||
|
|||
console.log('PDF响应:', pdfResponse); |
|||
|
|||
// 处理PDF Blob |
|||
if (pdfResponse?.data) { |
|||
console.log('PDF加载成功, 创建Blob'); |
|||
|
|||
// 清理之前的URL |
|||
if (pdfUrl.value && pdfUrl.value.startsWith('blob:')) { |
|||
URL.revokeObjectURL(pdfUrl.value); |
|||
} |
|||
|
|||
// 创建新的Blob URL |
|||
const blob = new Blob([pdfResponse.data], { type: 'application/pdf' }); |
|||
pdfUrl.value = URL.createObjectURL(blob); |
|||
console.log('PDF URL创建成功:', pdfUrl.value); |
|||
} else { |
|||
throw new Error('PDF数据格式错误'); |
|||
} |
|||
|
|||
// 处理条款列表 |
|||
articles.value = articlesResult || []; |
|||
|
|||
} catch (err: any) { |
|||
error.value = err.message || '加载案例详情失败'; |
|||
console.error('加载案例详情失败:', err); |
|||
} finally { |
|||
loading.value = false; |
|||
} |
|||
} |
|||
|
|||
// 刷新PDF |
|||
async function refreshPdf() { |
|||
if (!props.caseFileId) return; |
|||
|
|||
loading.value = true; |
|||
error.value = ''; |
|||
|
|||
try { |
|||
const pdfResponse = await ContractualCaseFilesViewPdf(props.caseFileId); |
|||
|
|||
if (pdfResponse?.data) { |
|||
// 清理之前的URL |
|||
if (pdfUrl.value && pdfUrl.value.startsWith('blob:')) { |
|||
URL.revokeObjectURL(pdfUrl.value); |
|||
} |
|||
// 创建新的Blob URL |
|||
const blob = new Blob([pdfResponse.data], { type: 'application/pdf' }); |
|||
pdfUrl.value = URL.createObjectURL(blob); |
|||
} else { |
|||
throw new Error('PDF数据格式错误'); |
|||
} |
|||
} catch (err: any) { |
|||
error.value = err.message || '刷新PDF失败'; |
|||
console.error('刷新PDF失败:', err); |
|||
} finally { |
|||
loading.value = false; |
|||
} |
|||
} |
|||
|
|||
// 下载PDF |
|||
async function downloadPdf() { |
|||
if (!props.caseFileId) return; |
|||
|
|||
downloading.value = true; |
|||
try { |
|||
const pdfResponse = await ContractualCaseFilesViewPdf(props.caseFileId); |
|||
|
|||
if (pdfResponse?.data) { |
|||
const blob = new Blob([pdfResponse.data], { type: 'application/pdf' }); |
|||
const url = URL.createObjectURL(blob); |
|||
const link = document.createElement('a'); |
|||
link.href = url; |
|||
link.download = `${props.caseName || '案例文档'}.pdf`; |
|||
document.body.appendChild(link); |
|||
link.click(); |
|||
document.body.removeChild(link); |
|||
|
|||
// 清理临时URL |
|||
URL.revokeObjectURL(url); |
|||
message.success('PDF下载已开始'); |
|||
} else { |
|||
throw new Error('PDF数据格式错误'); |
|||
} |
|||
} catch (err: any) { |
|||
message.error('下载失败: ' + (err.message || '未知错误')); |
|||
console.error('下载PDF失败:', err); |
|||
} finally { |
|||
downloading.value = false; |
|||
} |
|||
} |
|||
|
|||
// 切换显示模式 |
|||
function toggleDisplayMode() { |
|||
showArticlesList.value = !showArticlesList.value; |
|||
} |
|||
|
|||
// PDF加载错误处理 |
|||
function handlePdfError() { |
|||
showIframe.value = true; |
|||
} |
|||
|
|||
// 清理Blob URL |
|||
function cleanupPdfUrl() { |
|||
if (pdfUrl.value && pdfUrl.value.startsWith('blob:')) { |
|||
URL.revokeObjectURL(pdfUrl.value); |
|||
pdfUrl.value = ''; |
|||
} |
|||
} |
|||
|
|||
// 组件销毁时清理 |
|||
onUnmounted(() => { |
|||
cleanupPdfUrl(); |
|||
}); |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.case-view-drawer { |
|||
:deep(.ant-drawer-content) { |
|||
height: 100vh; |
|||
} |
|||
|
|||
:deep(.ant-drawer-body) { |
|||
padding: 0; |
|||
height: calc(100vh - 55px); |
|||
display: flex; |
|||
flex-direction: column; |
|||
} |
|||
|
|||
.case-view-container { |
|||
height: 100%; |
|||
display: flex; |
|||
flex-direction: column; |
|||
padding: 16px; |
|||
} |
|||
|
|||
.toolbar { |
|||
margin-bottom: 16px; |
|||
padding: 12px; |
|||
background: #fafafa; |
|||
border-radius: 6px; |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
flex-shrink: 0; |
|||
} |
|||
|
|||
.pdf-container { |
|||
flex: 1; |
|||
position: relative; |
|||
min-height: 0; |
|||
|
|||
.loading-container { |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
height: 100%; |
|||
} |
|||
|
|||
.error-container { |
|||
padding: 40px; |
|||
height: 100%; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
} |
|||
|
|||
.pdf-embed-container { |
|||
border: 1px solid #d9d9d9; |
|||
border-radius: 6px; |
|||
overflow: hidden; |
|||
height: 100%; |
|||
|
|||
embed, iframe { |
|||
display: block; |
|||
height: 100% !important; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.articles-list { |
|||
margin-top: 24px; |
|||
flex-shrink: 0; |
|||
max-height: 300px; |
|||
overflow-y: auto; |
|||
|
|||
.article-item { |
|||
margin-bottom: 24px; |
|||
padding: 16px; |
|||
border: 1px solid #e8e8e8; |
|||
border-radius: 6px; |
|||
background: #fafafa; |
|||
|
|||
.article-title { |
|||
font-weight: bold; |
|||
font-size: 16px; |
|||
color: #262626; |
|||
margin-bottom: 12px; |
|||
padding-bottom: 8px; |
|||
border-bottom: 1px solid #e8e8e8; |
|||
} |
|||
|
|||
.article-content { |
|||
line-height: 1.8; |
|||
color: #595959; |
|||
text-align: justify; |
|||
white-space: pre-wrap; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/* 响应式设计 */ |
|||
@media (max-width: 1200px) { |
|||
.case-view-drawer { |
|||
:deep(.ant-drawer) { |
|||
width: 80% !important; |
|||
} |
|||
} |
|||
} |
|||
|
|||
@media (max-width: 768px) { |
|||
.case-view-drawer { |
|||
:deep(.ant-drawer) { |
|||
width: 90% !important; |
|||
} |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,458 @@ |
|||
<template> |
|||
<PageWrapper dense> |
|||
<!-- 搜索表单 --> |
|||
<Card class="search-card"> |
|||
<Form |
|||
:model="searchForm" |
|||
layout="inline" |
|||
class="search-form" |
|||
> |
|||
<FormItem label="案例名称"> |
|||
<Input |
|||
v-model:value="searchForm.caseName" |
|||
placeholder="请输入案例名称" |
|||
style="width: 200px" |
|||
allowClear |
|||
/> |
|||
</FormItem> |
|||
<FormItem label="案例类型"> |
|||
<Input |
|||
v-model:value="searchForm.caseType" |
|||
placeholder="请输入案例类型" |
|||
style="width: 150px" |
|||
allowClear |
|||
/> |
|||
</FormItem> |
|||
<FormItem label="发布日期"> |
|||
<RangePicker |
|||
v-model:value="searchForm.publishDateRange" |
|||
format="YYYY-MM-DD" |
|||
valueFormat="YYYY-MM-DD" |
|||
style="width: 240px" |
|||
allowClear |
|||
:placeholder="['开始日期', '结束日期']" |
|||
/> |
|||
</FormItem> |
|||
<FormItem label="是否有效"> |
|||
<Select |
|||
v-model:value="searchForm.isEffective" |
|||
placeholder="请选择状态" |
|||
style="width: 120px" |
|||
allowClear |
|||
:options="effectiveOptions" |
|||
/> |
|||
</FormItem> |
|||
<FormItem> |
|||
<Space> |
|||
<Button type="primary" @click="handleSearch">查询</Button> |
|||
<Button @click="resetSearch">重置</Button> |
|||
</Space> |
|||
</FormItem> |
|||
</Form> |
|||
</Card> |
|||
|
|||
<!-- 表格区域 --> |
|||
<Card class="table-card"> |
|||
<!-- 表格标题和操作按钮 --> |
|||
<div class="table-header"> |
|||
<div class="table-title">合同案例文件列表</div> |
|||
<div class="table-actions"> |
|||
<Space> |
|||
<Button |
|||
type="primary" |
|||
danger |
|||
@click="multipleRemove" |
|||
:disabled="!selectedRowKeys.length" |
|||
v-auth="'productManagement:ContractualCaseFiles:remove'" |
|||
> |
|||
删除 |
|||
</Button> |
|||
<Button type="primary" @click="handleAdd" v-auth="'productManagement:ContractualCaseFiles:add'"> |
|||
新增 |
|||
</Button> |
|||
<Button @click="handleRefresh"> |
|||
<template #icon><ReloadOutlined /></template> |
|||
</Button> |
|||
</Space> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 数据表格 --> |
|||
<Table |
|||
:dataSource="dataSource" |
|||
:columns="tableColumns" |
|||
:loading="loading" |
|||
:pagination="pagination" |
|||
:rowSelection="rowSelection" |
|||
rowKey="id" |
|||
@change="handleTableChange" |
|||
size="middle" |
|||
/> |
|||
</Card> |
|||
|
|||
<!-- 新增弹窗 --> |
|||
<AddModal |
|||
v-model:visible="modalVisible" |
|||
@success="loadData" |
|||
/> |
|||
|
|||
<!-- 查看弹窗 --> |
|||
<ViewModal |
|||
v-model:visible="viewModalVisible" |
|||
:caseFileId="selectedRecord?.id" |
|||
:caseName="selectedRecord?.caseName" |
|||
/> |
|||
|
|||
<!-- 编辑弹窗 --> |
|||
<EditModal |
|||
v-model:visible="editModalVisible" |
|||
:caseFileId="selectedRecord?.id" |
|||
:caseName="selectedRecord?.caseName" |
|||
@success="loadData" |
|||
/> |
|||
</PageWrapper> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
import { ref, reactive, onMounted, h } from 'vue'; |
|||
import { PageWrapper } from '@/components/Page'; |
|||
import { |
|||
Table, |
|||
Button, |
|||
Space, |
|||
Card, |
|||
Form, |
|||
FormItem, |
|||
Input, |
|||
DatePicker, |
|||
Select, |
|||
Switch, |
|||
Modal, |
|||
message |
|||
} from 'ant-design-vue'; |
|||
import { |
|||
ReloadOutlined, |
|||
EyeOutlined, |
|||
DeleteOutlined, |
|||
EditOutlined |
|||
} from '@ant-design/icons-vue'; |
|||
import { |
|||
ContractualCaseFilesList, |
|||
ContractualCaseFilesRemove, |
|||
ContractualCaseFilesUpdateStatus |
|||
} from '@/api/contractReview/ContractualCaseFiles'; |
|||
import { getDictOptions } from '@/utils/dict'; |
|||
import AddModal from './AddModal.vue'; |
|||
import ViewModal from './ViewModal.vue'; |
|||
import EditModal from './EditModal.vue'; |
|||
const { RangePicker } = DatePicker; |
|||
import { useRender } from '@/hooks/component/useRender'; |
|||
|
|||
const { renderDict } = useRender(); |
|||
defineOptions({ name: 'ContractualCaseFiles' }); |
|||
|
|||
// 状态变量 |
|||
const loading = ref(false); |
|||
const dataSource = ref<any[]>([]); |
|||
const selectedRowKeys = ref<any[]>([]); |
|||
const modalVisible = ref(false); |
|||
const viewModalVisible = ref(false); |
|||
const editModalVisible = ref(false); |
|||
const selectedRecord = ref<any>(null); |
|||
const searchForm = reactive({ |
|||
caseName: '', |
|||
caseType: '', |
|||
publishDateRange: null as any, |
|||
isEffective: undefined as string | undefined |
|||
}); |
|||
|
|||
// 分页配置 |
|||
const pagination = reactive({ |
|||
current: 1, |
|||
pageSize: 10, |
|||
total: 0, |
|||
showSizeChanger: true, |
|||
showQuickJumper: true, |
|||
showTotal: (total: number) => `共 ${total} 条数据`, |
|||
}); |
|||
|
|||
// 行选择配置 |
|||
const rowSelection = { |
|||
selectedRowKeys: selectedRowKeys.value, |
|||
onChange: (keys: any[]) => { |
|||
selectedRowKeys.value = keys; |
|||
}, |
|||
}; |
|||
|
|||
// 字典选项 |
|||
const effectiveOptions = getDictOptions('sys_yes_no'); |
|||
|
|||
// 开关状态切换 |
|||
async function handleSwitchChange(record: any) { |
|||
if (record._switching) return; |
|||
|
|||
record._switching = true; |
|||
try { |
|||
const newStatus = record.isEffective === 'Y' ? 'N' : 'Y'; |
|||
await ContractualCaseFilesUpdateStatus(record.id, newStatus); |
|||
record.isEffective = newStatus; |
|||
message.success('状态更新成功'); |
|||
} catch (error) { |
|||
message.error('状态更新失败'); |
|||
} finally { |
|||
record._switching = false; |
|||
} |
|||
} |
|||
|
|||
// 表格列配置 |
|||
const tableColumns = [ |
|||
{ |
|||
title: '案例名称', |
|||
dataIndex: 'caseName', |
|||
key: 'caseName', |
|||
width: 180, |
|||
}, |
|||
{ |
|||
title: '案例描述', |
|||
dataIndex: 'caseDescription', |
|||
key: 'caseDescription', |
|||
width: 200, |
|||
ellipsis: true, |
|||
}, |
|||
{ |
|||
title: '案例类型', |
|||
dataIndex: 'caseType', |
|||
key: 'caseType', |
|||
width: 120, |
|||
}, |
|||
{ |
|||
title: '发布日期', |
|||
dataIndex: 'publishDate', |
|||
key: 'publishDate', |
|||
width: 120, |
|||
}, |
|||
{ |
|||
title: '是否有效', |
|||
dataIndex: 'isEffective', |
|||
key: 'isEffective', |
|||
width: 120, |
|||
customRender: ({ record }: any) => { |
|||
return h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px' } }, [ |
|||
h(Switch, { |
|||
checked: record.isEffective === 'Y', |
|||
loading: record._switching, |
|||
onChange: () => handleSwitchChange(record) |
|||
}), |
|||
h('span', {}, record.isEffective === 'Y' ? '有效' : '无效') |
|||
]); |
|||
}, |
|||
}, |
|||
{ |
|||
title: '当前状态', |
|||
dataIndex: 'progressStatus', |
|||
key: 'progressStatus', |
|||
width: 120, |
|||
customRender: ({ value }) => renderDict(value, 'file_progress_status'), |
|||
}, |
|||
{ |
|||
title: '操作', |
|||
key: 'action', |
|||
fixed: 'right' as const, |
|||
width: 180, |
|||
customRender: ({ record }: any) => { |
|||
return h(Space, {}, [ |
|||
h(Button, { |
|||
type: 'primary', |
|||
size: 'small', |
|||
icon: h(EyeOutlined), |
|||
onClick: () => handleView(record) |
|||
}, '查看'), |
|||
h(Button, { |
|||
type: 'default', |
|||
size: 'small', |
|||
icon: h(EditOutlined), |
|||
onClick: () => handleEdit(record) |
|||
}, '修改'), |
|||
h(Button, { |
|||
type: 'primary', |
|||
danger: true, |
|||
size: 'small', |
|||
icon: h(DeleteOutlined), |
|||
onClick: () => handleDelete(record) |
|||
}, '删除') |
|||
]); |
|||
}, |
|||
} |
|||
]; |
|||
|
|||
// 过滤空值参数 |
|||
function filterEmptyParams(params: any) { |
|||
const filteredParams: any = {}; |
|||
Object.keys(params).forEach(key => { |
|||
const value = params[key]; |
|||
if (value !== null && value !== undefined && value !== '') { |
|||
filteredParams[key] = value; |
|||
} |
|||
}); |
|||
return filteredParams; |
|||
} |
|||
|
|||
// 加载数据 |
|||
async function loadData() { |
|||
loading.value = true; |
|||
try { |
|||
const baseParams: any = { |
|||
caseName: searchForm.caseName, |
|||
caseType: searchForm.caseType, |
|||
pageNum: pagination.current, |
|||
pageSize: pagination.pageSize, |
|||
}; |
|||
|
|||
// 处理日期范围参数 |
|||
if (searchForm.publishDateRange && searchForm.publishDateRange.length === 2) { |
|||
baseParams.publishDateStart = searchForm.publishDateRange[0]; |
|||
baseParams.publishDateEnd = searchForm.publishDateRange[1]; |
|||
} |
|||
|
|||
// 过滤空值参数 |
|||
const params = filterEmptyParams(baseParams); |
|||
|
|||
const result: any = await ContractualCaseFilesList(params); |
|||
dataSource.value = result.rows || []; |
|||
pagination.total = result.total || 0; |
|||
} catch (error) { |
|||
message.error('加载数据失败'); |
|||
} finally { |
|||
loading.value = false; |
|||
} |
|||
} |
|||
|
|||
// 表格变化处理 |
|||
function handleTableChange(pag: any) { |
|||
pagination.current = pag.current; |
|||
pagination.pageSize = pag.pageSize; |
|||
loadData(); |
|||
} |
|||
|
|||
// 搜索处理 |
|||
function handleSearch() { |
|||
pagination.current = 1; |
|||
loadData(); |
|||
} |
|||
|
|||
// 重置搜索 |
|||
function resetSearch() { |
|||
Object.assign(searchForm, { |
|||
caseName: '', |
|||
caseType: '', |
|||
publishDateRange: null, |
|||
isEffective: undefined |
|||
}); |
|||
pagination.current = 1; |
|||
loadData(); |
|||
} |
|||
|
|||
// 刷新 |
|||
function handleRefresh() { |
|||
loadData(); |
|||
} |
|||
|
|||
// 新增 |
|||
function handleAdd() { |
|||
modalVisible.value = true; |
|||
} |
|||
|
|||
// 查看详情 |
|||
function handleView(record: any) { |
|||
selectedRecord.value = record; |
|||
viewModalVisible.value = true; |
|||
} |
|||
|
|||
// 编辑 |
|||
function handleEdit(record: any) { |
|||
selectedRecord.value = record; |
|||
editModalVisible.value = true; |
|||
} |
|||
|
|||
// 单个删除 |
|||
async function handleDelete(record: any) { |
|||
Modal.confirm({ |
|||
title: '删除确认', |
|||
content: `是否删除案例《${record.caseName}》?`, |
|||
onOk: async () => { |
|||
try { |
|||
await ContractualCaseFilesRemove([record.id]); |
|||
message.success('删除成功'); |
|||
loadData(); |
|||
} catch (error) { |
|||
message.error('删除失败'); |
|||
} |
|||
}, |
|||
}); |
|||
} |
|||
|
|||
// 批量删除 |
|||
async function multipleRemove() { |
|||
if (!selectedRowKeys.value.length) { |
|||
message.warning('请选择要删除的记录'); |
|||
return; |
|||
} |
|||
|
|||
const selectedRecords = dataSource.value.filter((item: any) => |
|||
selectedRowKeys.value.includes(item.id) |
|||
); |
|||
const caseNames = selectedRecords.map((item: any) => item.caseName).join('、'); |
|||
|
|||
Modal.confirm({ |
|||
title: '删除确认', |
|||
content: `是否删除案例《${caseNames}》?`, |
|||
onOk: async () => { |
|||
try { |
|||
await ContractualCaseFilesRemove(selectedRowKeys.value); |
|||
message.success('删除成功'); |
|||
selectedRowKeys.value = []; |
|||
loadData(); |
|||
} catch (error) { |
|||
message.error('删除失败'); |
|||
} |
|||
}, |
|||
}); |
|||
} |
|||
|
|||
// 页面加载时获取数据 |
|||
onMounted(() => { |
|||
loadData(); |
|||
}); |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.search-card { |
|||
margin-bottom: 16px; |
|||
} |
|||
|
|||
.search-form { |
|||
margin-bottom: 0; |
|||
} |
|||
|
|||
.table-card { |
|||
background: #fff; |
|||
} |
|||
|
|||
.table-header { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
margin-bottom: 16px; |
|||
} |
|||
|
|||
.table-title { |
|||
font-size: 16px; |
|||
font-weight: 500; |
|||
} |
|||
|
|||
:deep(.ant-table) { |
|||
.ant-table-tbody > tr > td { |
|||
padding: 12px 8px; |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,476 @@ |
|||
<template> |
|||
<Modal |
|||
:open="visible" |
|||
title="新增合同法规名称" |
|||
@ok="handleSubmit" |
|||
@cancel="handleCancel" |
|||
:confirmLoading="loading" |
|||
width="600px" |
|||
> |
|||
<Form |
|||
ref="formRef" |
|||
:model="formData" |
|||
:rules="rules" |
|||
layout="vertical" |
|||
> |
|||
<!-- 文件上传 --> |
|||
<FormItem label="法规文件" name="regulationFile" :required="!hasUploadedFile"> |
|||
<div class="upload-box" :class="{'file-preview': fileList && fileList.length > 0}"> |
|||
<template v-if="fileList && fileList.length > 0"> |
|||
<!-- 文件预览 --> |
|||
<div class="file-info"> |
|||
<FileTextOutlined class="file-icon" /> |
|||
<div class="file-details"> |
|||
<p class="file-name">{{ fileList[0].name }}</p> |
|||
<div class="file-progress" v-if="uploading"> |
|||
<Progress :percent="uploadPercent" size="small" status="active" /> |
|||
</div> |
|||
<p class="file-status" v-else> |
|||
<CheckCircleFilled class="status-icon success" /> 上传成功 |
|||
<Button type="link" class="remove-btn" @click="removeFile">删除</Button> |
|||
</p> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
<template v-else> |
|||
<!-- 上传区域 --> |
|||
<Upload |
|||
:fileList="fileList" |
|||
:customRequest="customUploadRequest" |
|||
:beforeUpload="beforeUpload" |
|||
:showUploadList="false" |
|||
:maxCount="1" |
|||
:multiple="false" |
|||
name="file" |
|||
accept=".doc,.docx" |
|||
:disabled="uploading" |
|||
> |
|||
<div class="upload-content"> |
|||
<div class="upload-icon"> |
|||
<UploadOutlined class="upload-arrow-icon" /> |
|||
</div> |
|||
<div class="upload-text-container"> |
|||
<p class="upload-text"> |
|||
<a class="upload-link">选择法规文件</a> |
|||
</p> |
|||
<p class="upload-hint">支持DOC、DOCX格式文件,最大500MB</p> |
|||
</div> |
|||
</div> |
|||
</Upload> |
|||
</template> |
|||
</div> |
|||
</FormItem> |
|||
|
|||
<!-- 法规名称 --> |
|||
<FormItem label="法规名称" name="regulationName" required> |
|||
<Input |
|||
v-model:value="formData.regulationName" |
|||
:disabled="regulationNameDisabled" |
|||
:placeholder="regulationNamePlaceholder" |
|||
/> |
|||
</FormItem> |
|||
|
|||
<!-- 法规描述 --> |
|||
<FormItem label="法规描述" name="regulationDescription" required> |
|||
<TextArea |
|||
v-model:value="formData.regulationDescription" |
|||
placeholder="请输入法规描述" |
|||
:rows="4" |
|||
show-count |
|||
:maxlength="500" |
|||
/> |
|||
</FormItem> |
|||
|
|||
<!-- 发布日期 --> |
|||
<FormItem label="发布日期" name="publishDate" required> |
|||
<DatePicker |
|||
v-model:value="formData.publishDate" |
|||
format="YYYY-MM-DD" |
|||
valueFormat="YYYY-MM-DD" |
|||
style="width: 100%" |
|||
/> |
|||
</FormItem> |
|||
|
|||
<!-- 是否有效 --> |
|||
<FormItem label="是否有效" name="isEffective"> |
|||
<Select |
|||
v-model:value="formData.isEffective" |
|||
:options="effectiveOptions" |
|||
/> |
|||
</FormItem> |
|||
</Form> |
|||
</Modal> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
import { ref, reactive, computed, watch } from 'vue'; |
|||
import { |
|||
Modal, |
|||
Form, |
|||
FormItem, |
|||
Input, |
|||
DatePicker, |
|||
Select, |
|||
Upload, |
|||
Button, |
|||
Progress, |
|||
message |
|||
} from 'ant-design-vue'; |
|||
import { |
|||
UploadOutlined, |
|||
FileTextOutlined, |
|||
CheckCircleFilled |
|||
} from '@ant-design/icons-vue'; |
|||
import { ContractualRegulationNamesAdd } from '@/api/contractReview/ContractualRegulationNames'; |
|||
import { uploadApi } from '@/api/upload'; |
|||
import { ossRemove } from '@/api/system/oss'; |
|||
import { getDictOptions } from '@/utils/dict'; |
|||
import { UploadFileParams } from '#/axios'; |
|||
import dayjs from 'dayjs'; |
|||
const { TextArea } = Input; |
|||
|
|||
interface Props { |
|||
visible: boolean; |
|||
} |
|||
|
|||
const props = defineProps<Props>(); |
|||
const emit = defineEmits(['update:visible', 'success']); |
|||
|
|||
// 状态变量 |
|||
const loading = ref(false); |
|||
const fileList = ref<any[]>([]); |
|||
const uploading = ref(false); |
|||
const uploadPercent = ref(0); |
|||
const currentOssId = ref<string | null>(null); |
|||
const formRef = ref(); |
|||
|
|||
// 表单数据 |
|||
const formData = reactive({ |
|||
regulationName: '', |
|||
regulationDescription: '', |
|||
publishDate: dayjs(), |
|||
isEffective: 'Y', |
|||
progressStatus: 'PENDING' |
|||
}); |
|||
|
|||
// 计算属性 |
|||
const hasUploadedFile = computed(() => { |
|||
return !!currentOssId.value; |
|||
}); |
|||
|
|||
const regulationNameDisabled = computed(() => { |
|||
return !currentOssId.value; |
|||
}); |
|||
|
|||
const regulationNamePlaceholder = computed(() => { |
|||
return currentOssId.value ? '可编辑法规名称' : '请先上传法规文件'; |
|||
}); |
|||
|
|||
// 表单验证规则 - 动态规则 |
|||
const rules = computed(() => ({ |
|||
regulationFile: hasUploadedFile.value ? [] : [{ required: true, message: '请上传法规文件' }], |
|||
regulationName: [{ required: true, message: '请输入法规名称' }], |
|||
regulationDescription: [{ required: true, message: '请输入法规描述' }], |
|||
publishDate: [{ required: true, message: '请选择发布日期' }], |
|||
})); |
|||
|
|||
// 字典选项 |
|||
const effectiveOptions = getDictOptions('sys_yes_no'); |
|||
|
|||
// 监听弹窗关闭,重置表单 |
|||
watch(() => props.visible, (newVal) => { |
|||
if (!newVal) { |
|||
resetForm(); |
|||
} |
|||
}); |
|||
|
|||
// 文件上传相关处理 |
|||
function customUploadRequest(options: any) { |
|||
const { file, onSuccess, onError } = options; |
|||
|
|||
uploading.value = true; |
|||
uploadPercent.value = 0; |
|||
|
|||
const uploadParams: UploadFileParams = { |
|||
name: 'file', |
|||
file: file, |
|||
data: {}, |
|||
}; |
|||
|
|||
fileList.value = [ |
|||
{ |
|||
uid: '1', |
|||
name: file.name, |
|||
status: 'uploading', |
|||
url: URL.createObjectURL(file), |
|||
}, |
|||
]; |
|||
|
|||
uploadApi( |
|||
uploadParams, |
|||
(progressEvent) => { |
|||
if (progressEvent.total) { |
|||
const percent = Math.round((progressEvent.loaded / progressEvent.total) * 100); |
|||
uploadPercent.value = percent; |
|||
} |
|||
} |
|||
).then((res) => { |
|||
if (fileList.value && fileList.value.length > 0) { |
|||
fileList.value[0].status = 'done'; |
|||
fileList.value[0].response = res; |
|||
if (res && res.ossId) { |
|||
currentOssId.value = res.ossId; |
|||
const fileName = file.name; |
|||
const nameWithoutExt = fileName.substring(0, fileName.lastIndexOf('.')) || fileName; |
|||
formData.regulationName = nameWithoutExt; |
|||
|
|||
// 文件上传成功后,清除文件字段的验证错误 |
|||
formRef.value?.clearValidate('regulationFile'); |
|||
} |
|||
} |
|||
uploading.value = false; |
|||
uploadPercent.value = 100; |
|||
message.success(`${file.name} 文件上传成功`); |
|||
onSuccess(res); |
|||
}).catch((err) => { |
|||
uploading.value = false; |
|||
fileList.value = []; |
|||
currentOssId.value = null; |
|||
message.error(`文件上传失败: ${err.message || '未知错误'}`); |
|||
onError(err); |
|||
}); |
|||
} |
|||
|
|||
function beforeUpload(file: File) { |
|||
const isDoc = file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || |
|||
file.type === 'application/msword'; |
|||
|
|||
if (!isDoc) { |
|||
message.error('只能上传 DOC/DOCX 格式的文件!'); |
|||
return false; |
|||
} |
|||
|
|||
const isLt500M = file.size / 1024 / 1024 < 500; |
|||
if (!isLt500M) { |
|||
message.error('文件大小不能超过 500MB!'); |
|||
return false; |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
function removeFile() { |
|||
if (uploading.value) { |
|||
message.warning('文件正在上传中,请稍后再试'); |
|||
return; |
|||
} |
|||
|
|||
if (currentOssId.value) { |
|||
ossRemove([currentOssId.value]) |
|||
.then(() => { |
|||
fileList.value = []; |
|||
uploadPercent.value = 0; |
|||
currentOssId.value = null; |
|||
formData.regulationName = ''; |
|||
message.success('文件已删除'); |
|||
}) |
|||
.catch((err) => { |
|||
message.error(`文件删除失败: ${err.message || '未知错误'}`); |
|||
}); |
|||
} else { |
|||
fileList.value = []; |
|||
uploadPercent.value = 0; |
|||
formData.regulationName = ''; |
|||
message.success('文件已删除'); |
|||
} |
|||
} |
|||
|
|||
// 提交表单 |
|||
async function handleSubmit() { |
|||
try { |
|||
await formRef.value?.validate(); |
|||
} catch (error) { |
|||
return; |
|||
} |
|||
|
|||
if (!currentOssId.value) { |
|||
message.warning('请上传法规文件'); |
|||
return; |
|||
} |
|||
|
|||
loading.value = true; |
|||
try { |
|||
const submitData = { |
|||
...formData, |
|||
publishDate: formData.publishDate ? formData.publishDate.format('YYYY-MM-DD') : '', |
|||
ossId: currentOssId.value |
|||
}; |
|||
|
|||
await ContractualRegulationNamesAdd(submitData); |
|||
message.success('新增成功'); |
|||
|
|||
// TODO: 预留代码空位 - 发起解析API请求 |
|||
// await parseRegulationFile(currentOssId.value); |
|||
|
|||
emit('update:visible', false); |
|||
emit('success'); |
|||
resetForm(); |
|||
} catch (error) { |
|||
message.error('新增失败'); |
|||
} finally { |
|||
loading.value = false; |
|||
} |
|||
} |
|||
|
|||
// 取消 |
|||
function handleCancel() { |
|||
emit('update:visible', false); |
|||
resetForm(); |
|||
} |
|||
|
|||
// 重置表单 |
|||
function resetForm() { |
|||
Object.assign(formData, { |
|||
regulationName: '', |
|||
regulationDescription: '', |
|||
publishDate: dayjs(), |
|||
isEffective: 'Y', |
|||
progressStatus: 'PENDING' |
|||
}); |
|||
fileList.value = []; |
|||
uploadPercent.value = 0; |
|||
currentOssId.value = null; |
|||
formRef.value?.clearValidate(); |
|||
} |
|||
</script> |
|||
|
|||
<style lang="less" scoped> |
|||
/* 文件上传样式 */ |
|||
.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; |
|||
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; |
|||
} |
|||
} |
|||
|
|||
.upload-content { |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: center; |
|||
width: 100%; |
|||
} |
|||
|
|||
.upload-icon { |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
margin-bottom: 16px; |
|||
background-color: #e6fffb; |
|||
width: 50px; |
|||
height: 50px; |
|||
border-radius: 50%; |
|||
} |
|||
|
|||
.upload-arrow-icon { |
|||
font-size: 24px; |
|||
color: #13c2c2; |
|||
} |
|||
|
|||
.upload-text-container { |
|||
text-align: center; |
|||
} |
|||
|
|||
.upload-text { |
|||
font-size: 16px; |
|||
color: #333; |
|||
margin-bottom: 8px; |
|||
} |
|||
|
|||
.upload-link { |
|||
color: #13c2c2; |
|||
cursor: pointer; |
|||
text-decoration: none; |
|||
font-weight: 600; |
|||
} |
|||
|
|||
.upload-link:hover { |
|||
text-decoration: underline; |
|||
} |
|||
|
|||
.upload-hint { |
|||
color: #888; |
|||
font-size: 14px; |
|||
margin-top: 8px; |
|||
} |
|||
|
|||
.file-info { |
|||
display: flex; |
|||
align-items: center; |
|||
width: 100%; |
|||
max-width: 800px; |
|||
} |
|||
|
|||
.file-icon { |
|||
font-size: 32px; |
|||
color: #13c2c2; |
|||
margin-right: 16px; |
|||
} |
|||
|
|||
.file-details { |
|||
flex: 1; |
|||
} |
|||
|
|||
.file-name { |
|||
font-size: 16px; |
|||
font-weight: 500; |
|||
margin: 0 0 8px 0; |
|||
color: #333; |
|||
} |
|||
|
|||
.file-progress { |
|||
margin: 0 0 8px 0; |
|||
width: 100%; |
|||
} |
|||
|
|||
.file-status { |
|||
font-size: 14px; |
|||
color: #666; |
|||
margin: 0; |
|||
display: flex; |
|||
align-items: center; |
|||
} |
|||
|
|||
.status-icon { |
|||
margin-right: 6px; |
|||
} |
|||
|
|||
.status-icon.success { |
|||
color: #52c41a; |
|||
} |
|||
|
|||
.remove-btn { |
|||
padding: 0; |
|||
margin-left: 12px; |
|||
font-size: 14px; |
|||
} |
|||
</style> |
@ -0,0 +1,108 @@ |
|||
import { BasicColumn } from '@/components/Table'; |
|||
import { FormSchema } from '@/components/Form'; |
|||
import { getDictOptions } from '@/utils/dict'; |
|||
import { h } from 'vue'; |
|||
import { useRender } from '@/hooks/component/useRender'; |
|||
|
|||
const { renderDict } = useRender(); |
|||
|
|||
export const formSchemas: FormSchema[] = [ |
|||
{ |
|||
label: '法规名称', |
|||
field: 'regulationName', |
|||
component: 'Input', |
|||
}, |
|||
{ |
|||
label: '发布日期', |
|||
field: 'publishDate', |
|||
component: 'DatePicker', |
|||
componentProps: { |
|||
showTime: true, |
|||
format: 'YYYY-MM-DD', |
|||
valueFormat: 'YYYY-MM-DD', |
|||
}, |
|||
}, |
|||
{ |
|||
label: '是否有效', |
|||
field: 'isEffective', |
|||
component: 'Select', |
|||
componentProps: { |
|||
options: getDictOptions('sys_yes_no'), |
|||
}, |
|||
}, |
|||
]; |
|||
|
|||
export const columns: BasicColumn[] = [ |
|||
{ |
|||
title: '主键ID', |
|||
dataIndex: 'id', |
|||
ifShow: false, |
|||
}, |
|||
{ |
|||
title: '法规名称', |
|||
dataIndex: 'regulationName', |
|||
}, |
|||
{ |
|||
title: '发布日期', |
|||
dataIndex: 'publishDate', |
|||
}, |
|||
{ |
|||
title: '是否有效', |
|||
dataIndex: 'isEffective', |
|||
width: 120, |
|||
customRender: ({ record }) => { |
|||
return h('ASwitch', { |
|||
checked: record.isEffective === 'Y', |
|||
loading: record._switching, |
|||
onClick: () => handleSwitchChange(record) |
|||
}); |
|||
}, |
|||
},{ |
|||
title: '当前状态', |
|||
dataIndex: 'progressStatus', |
|||
customRender: ({ value }) => renderDict(value, 'file_progress_status'), |
|||
} |
|||
]; |
|||
|
|||
export const modalSchemas: FormSchema[] = [ |
|||
{ |
|||
label: '主键ID', |
|||
field: 'id', |
|||
required: false, |
|||
component: 'Input', |
|||
show: false, |
|||
}, |
|||
{ |
|||
label: '法规名称', |
|||
field: 'regulationName', |
|||
required: true, |
|||
component: 'Input', |
|||
componentProps: { |
|||
disabled: true, |
|||
placeholder: '请先上传法规文件', |
|||
}, |
|||
}, |
|||
{ |
|||
label: '发布日期', |
|||
field: 'publishDate', |
|||
required: true, |
|||
component: 'DatePicker', |
|||
componentProps: { |
|||
showTime: true, |
|||
format: 'YYYY-MM-DD', |
|||
valueFormat: 'YYYY-MM-DD', |
|||
}, |
|||
}, |
|||
{ |
|||
label: '是否有效', |
|||
field: 'isEffective', |
|||
required: false, |
|||
component: 'Select', |
|||
componentProps: { |
|||
options: getDictOptions('sys_yes_no'), |
|||
}, |
|||
} |
|||
]; |
|||
|
|||
// 声明处理开关变化的函数(需要在使用的组件中实现)
|
|||
declare function handleSwitchChange(record: any): void; |
@ -0,0 +1,450 @@ |
|||
<template> |
|||
<Drawer |
|||
v-model:open="visible" |
|||
title="修改合同法规" |
|||
width="80%" |
|||
:destroyOnClose="true" |
|||
class="edit-regulation-drawer" |
|||
placement="right" |
|||
> |
|||
<template #extra> |
|||
<Space> |
|||
<Button @click="handleCancel">取消</Button> |
|||
<Button type="primary" @click="handleSubmit" :loading="loading">保存</Button> |
|||
</Space> |
|||
</template> |
|||
|
|||
<div class="edit-container"> |
|||
<!-- 左侧PDF预览 --> |
|||
<div class="pdf-preview-section"> |
|||
<div class="section-title"> |
|||
<FileTextOutlined /> |
|||
<span>法规文档预览</span> |
|||
<Button type="link" size="small" @click="refreshPdf" :loading="pdfLoading"> |
|||
<ReloadOutlined /> |
|||
</Button> |
|||
</div> |
|||
|
|||
<div class="pdf-container"> |
|||
<div v-if="pdfLoading" class="loading-container"> |
|||
<Spin size="large" tip="正在加载文档..." /> |
|||
</div> |
|||
|
|||
<div v-else-if="pdfError" class="error-container"> |
|||
<Result |
|||
status="error" |
|||
:title="pdfError" |
|||
sub-title="请检查网络连接或联系管理员" |
|||
> |
|||
<template #extra> |
|||
<Button type="primary" @click="refreshPdf">重新加载</Button> |
|||
</template> |
|||
</Result> |
|||
</div> |
|||
|
|||
<div v-else-if="pdfUrl" class="pdf-embed-container"> |
|||
<embed |
|||
:src="pdfUrl" |
|||
type="application/pdf" |
|||
width="100%" |
|||
height="100%" |
|||
style="border: none; display: block;" |
|||
/> |
|||
</div> |
|||
|
|||
<div v-else class="no-pdf-container"> |
|||
<Empty description="暂无PDF文档" /> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 右侧修改表单 --> |
|||
<div class="edit-form-section"> |
|||
<div class="section-title"> |
|||
<EditOutlined /> |
|||
<span>修改信息</span> |
|||
</div> |
|||
|
|||
<div class="form-container"> |
|||
<Form |
|||
ref="formRef" |
|||
:model="formData" |
|||
:rules="rules" |
|||
layout="vertical" |
|||
class="edit-form" |
|||
> |
|||
<!-- 法规名称 --> |
|||
<FormItem label="法规名称" name="regulationName" required> |
|||
<Input |
|||
v-model:value="formData.regulationName" |
|||
placeholder="请输入法规名称" |
|||
size="large" |
|||
/> |
|||
</FormItem> |
|||
|
|||
<!-- 法规描述 --> |
|||
<FormItem label="法规描述" name="regulationDescription" required> |
|||
<TextArea |
|||
v-model:value="formData.regulationDescription" |
|||
placeholder="请输入法规描述" |
|||
:rows="6" |
|||
show-count |
|||
:maxlength="500" |
|||
size="large" |
|||
/> |
|||
</FormItem> |
|||
|
|||
<!-- 发布日期 --> |
|||
<FormItem label="发布日期" name="publishDate" required> |
|||
<DatePicker |
|||
v-model:value="formData.publishDate" |
|||
format="YYYY-MM-DD" |
|||
valueFormat="YYYY-MM-DD" |
|||
style="width: 100%" |
|||
size="large" |
|||
/> |
|||
</FormItem> |
|||
|
|||
<!-- 是否有效 --> |
|||
<FormItem label="是否有效" name="isEffective"> |
|||
<Select |
|||
v-model:value="formData.isEffective" |
|||
:options="effectiveOptions" |
|||
size="large" |
|||
/> |
|||
</FormItem> |
|||
</Form> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</Drawer> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
import { ref, reactive, computed, watch, onUnmounted } from 'vue'; |
|||
import { |
|||
Drawer, |
|||
Form, |
|||
FormItem, |
|||
Input, |
|||
DatePicker, |
|||
Select, |
|||
Button, |
|||
Space, |
|||
Spin, |
|||
Result, |
|||
Empty, |
|||
message |
|||
} from 'ant-design-vue'; |
|||
import { |
|||
FileTextOutlined, |
|||
EditOutlined, |
|||
ReloadOutlined |
|||
} from '@ant-design/icons-vue'; |
|||
import { |
|||
ContractualRegulationNamesInfo, |
|||
ContractualRegulationNamesUpdate, |
|||
ContractualRegulationNamesViewPdf |
|||
} from '@/api/contractReview/ContractualRegulationNames'; |
|||
import { getDictOptions } from '@/utils/dict'; |
|||
import dayjs from 'dayjs'; |
|||
|
|||
const { TextArea } = Input; |
|||
|
|||
interface Props { |
|||
visible: boolean; |
|||
regulationId?: number | string; |
|||
} |
|||
|
|||
const props = withDefaults(defineProps<Props>(), { |
|||
visible: false, |
|||
regulationId: undefined |
|||
}); |
|||
|
|||
const emit = defineEmits<{ |
|||
'update:visible': [value: boolean]; |
|||
'success': []; |
|||
}>(); |
|||
|
|||
// 状态变量 |
|||
const loading = ref(false); |
|||
const pdfLoading = ref(false); |
|||
const pdfError = ref(''); |
|||
const pdfUrl = ref(''); |
|||
const formRef = ref(); |
|||
|
|||
// 表单数据 |
|||
const formData = reactive({ |
|||
id: undefined as string | number | undefined, |
|||
regulationName: '', |
|||
regulationDescription: '', |
|||
publishDate: '', |
|||
isEffective: 'Y' |
|||
}); |
|||
|
|||
// 计算属性 |
|||
const visible = computed({ |
|||
get: () => props.visible, |
|||
set: (value) => emit('update:visible', value) |
|||
}); |
|||
|
|||
// 表单验证规则 |
|||
const rules = computed(() => ({ |
|||
regulationName: [{ required: true, message: '请输入法规名称', trigger: 'blur' }], |
|||
regulationDescription: [{ required: true, message: '请输入法规描述', trigger: 'blur' }], |
|||
publishDate: [{ required: true, message: '请选择发布日期', trigger: 'change' }], |
|||
})); |
|||
|
|||
// 字典选项 |
|||
const effectiveOptions = getDictOptions('sys_yes_no'); |
|||
|
|||
// 监听弹窗显示 |
|||
watch(() => props.visible, (newVisible) => { |
|||
if (newVisible && props.regulationId) { |
|||
loadRegulationData(); |
|||
loadPdf(); |
|||
} else if (!newVisible) { |
|||
resetForm(); |
|||
cleanupPdfUrl(); |
|||
} |
|||
}); |
|||
|
|||
// 加载法规数据 |
|||
async function loadRegulationData() { |
|||
if (!props.regulationId) return; |
|||
|
|||
try { |
|||
const result = await ContractualRegulationNamesInfo(props.regulationId); |
|||
if (result) { |
|||
formData.id = result.id; |
|||
formData.regulationName = result.regulationName || ''; |
|||
formData.regulationDescription = result.regulationDescription || ''; |
|||
formData.publishDate = result.publishDate || ''; |
|||
formData.isEffective = result.isEffective || 'Y'; |
|||
} |
|||
} catch (error: any) { |
|||
message.error('加载法规信息失败: ' + (error.message || '未知错误')); |
|||
} |
|||
} |
|||
|
|||
// 加载PDF |
|||
async function loadPdf() { |
|||
if (!props.regulationId) return; |
|||
|
|||
pdfLoading.value = true; |
|||
pdfError.value = ''; |
|||
|
|||
try { |
|||
const pdfResponse = await ContractualRegulationNamesViewPdf(props.regulationId); |
|||
|
|||
if (pdfResponse?.data) { |
|||
// 清理之前的URL |
|||
cleanupPdfUrl(); |
|||
|
|||
// 创建新的Blob URL |
|||
const blob = new Blob([pdfResponse.data], { type: 'application/pdf' }); |
|||
pdfUrl.value = URL.createObjectURL(blob); |
|||
} else { |
|||
pdfError.value = 'PDF数据格式错误'; |
|||
} |
|||
} catch (err: any) { |
|||
pdfError.value = err.message || '加载PDF失败'; |
|||
console.error('加载PDF失败:', err); |
|||
} finally { |
|||
pdfLoading.value = false; |
|||
} |
|||
} |
|||
|
|||
// 刷新PDF |
|||
function refreshPdf() { |
|||
loadPdf(); |
|||
} |
|||
|
|||
// 提交表单 |
|||
async function handleSubmit() { |
|||
try { |
|||
await formRef.value?.validate(); |
|||
} catch (error) { |
|||
return; |
|||
} |
|||
|
|||
loading.value = true; |
|||
try { |
|||
const submitData = { |
|||
...formData, |
|||
publishDate: formData.publishDate ? dayjs(formData.publishDate).format('YYYY-MM-DD') : '' |
|||
}; |
|||
|
|||
await ContractualRegulationNamesUpdate(submitData); |
|||
message.success('修改成功'); |
|||
|
|||
emit('update:visible', false); |
|||
emit('success'); |
|||
} catch (error: any) { |
|||
message.error('修改失败: ' + (error.message || '未知错误')); |
|||
} finally { |
|||
loading.value = false; |
|||
} |
|||
} |
|||
|
|||
// 取消 |
|||
function handleCancel() { |
|||
emit('update:visible', false); |
|||
} |
|||
|
|||
// 重置表单 |
|||
function resetForm() { |
|||
Object.assign(formData, { |
|||
id: undefined, |
|||
regulationName: '', |
|||
regulationDescription: '', |
|||
publishDate: '', |
|||
isEffective: 'Y' |
|||
}); |
|||
formRef.value?.clearValidate(); |
|||
pdfError.value = ''; |
|||
} |
|||
|
|||
// 清理Blob URL |
|||
function cleanupPdfUrl() { |
|||
if (pdfUrl.value && pdfUrl.value.startsWith('blob:')) { |
|||
URL.revokeObjectURL(pdfUrl.value); |
|||
pdfUrl.value = ''; |
|||
} |
|||
} |
|||
|
|||
// 组件销毁时清理 |
|||
onUnmounted(() => { |
|||
cleanupPdfUrl(); |
|||
}); |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.edit-regulation-drawer { |
|||
:deep(.ant-drawer-content) { |
|||
height: 100vh; |
|||
} |
|||
|
|||
:deep(.ant-drawer-body) { |
|||
padding: 0; |
|||
height: calc(100vh - 55px); |
|||
} |
|||
} |
|||
|
|||
.edit-container { |
|||
display: flex; |
|||
height: 100%; |
|||
gap: 1px; |
|||
background: #f0f2f5; |
|||
} |
|||
|
|||
.pdf-preview-section { |
|||
flex: 1; |
|||
background: #fff; |
|||
display: flex; |
|||
flex-direction: column; |
|||
} |
|||
|
|||
.edit-form-section { |
|||
width: 400px; |
|||
background: #fff; |
|||
display: flex; |
|||
flex-direction: column; |
|||
border-left: 1px solid #e8e8e8; |
|||
} |
|||
|
|||
.section-title { |
|||
padding: 16px 20px; |
|||
font-size: 16px; |
|||
font-weight: 500; |
|||
color: #262626; |
|||
border-bottom: 1px solid #e8e8e8; |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 8px; |
|||
background: #fafafa; |
|||
|
|||
span { |
|||
flex: 1; |
|||
} |
|||
} |
|||
|
|||
.pdf-container { |
|||
flex: 1; |
|||
position: relative; |
|||
background: #f5f5f5; |
|||
|
|||
.loading-container { |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
height: 100%; |
|||
} |
|||
|
|||
.error-container { |
|||
padding: 40px; |
|||
height: 100%; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
} |
|||
|
|||
.pdf-embed-container { |
|||
height: 100%; |
|||
padding: 16px; |
|||
|
|||
embed { |
|||
border-radius: 6px; |
|||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); |
|||
} |
|||
} |
|||
|
|||
.no-pdf-container { |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
height: 100%; |
|||
} |
|||
} |
|||
|
|||
.form-container { |
|||
flex: 1; |
|||
padding: 20px; |
|||
overflow-y: auto; |
|||
} |
|||
|
|||
.edit-form { |
|||
.ant-form-item { |
|||
margin-bottom: 24px; |
|||
} |
|||
|
|||
.ant-form-item-label > label { |
|||
font-weight: 500; |
|||
color: #262626; |
|||
} |
|||
} |
|||
|
|||
/* 响应式设计 */ |
|||
@media (max-width: 1400px) { |
|||
.edit-form-section { |
|||
width: 380px; |
|||
} |
|||
} |
|||
|
|||
@media (max-width: 1200px) { |
|||
.edit-container { |
|||
flex-direction: column; |
|||
} |
|||
|
|||
.pdf-preview-section { |
|||
height: 50%; |
|||
} |
|||
|
|||
.edit-form-section { |
|||
width: 100%; |
|||
height: 50%; |
|||
border-left: none; |
|||
border-top: 1px solid #e8e8e8; |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,382 @@ |
|||
<template> |
|||
<Drawer |
|||
v-model:open="visible" |
|||
:title="modalTitle" |
|||
width="70%" |
|||
:destroyOnClose="true" |
|||
class="regulation-view-drawer" |
|||
placement="right" |
|||
> |
|||
<template #extra> |
|||
<Space> |
|||
<Button type="primary" @click="downloadPdf" :loading="downloading"> |
|||
<template #icon><DownloadOutlined /></template> |
|||
下载PDF |
|||
</Button> |
|||
<Button @click="refreshPdf"> |
|||
<template #icon><ReloadOutlined /></template> |
|||
刷新 |
|||
</Button> |
|||
</Space> |
|||
</template> |
|||
|
|||
<div class="regulation-view-container"> |
|||
<!-- PDF预览区域 --> |
|||
<div class="pdf-container"> |
|||
<div v-if="loading" class="loading-container"> |
|||
<Spin size="large" tip="正在加载法规文档..." /> |
|||
</div> |
|||
|
|||
<div v-else-if="error" class="error-container"> |
|||
<Result |
|||
status="error" |
|||
:title="error" |
|||
sub-title="请检查网络连接或联系管理员" |
|||
> |
|||
<template #extra> |
|||
<Button type="primary" @click="refreshPdf">重新加载</Button> |
|||
</template> |
|||
</Result> |
|||
</div> |
|||
|
|||
<div v-else class="pdf-embed-container"> |
|||
<embed |
|||
:src="pdfUrl" |
|||
type="application/pdf" |
|||
width="100%" |
|||
height="100%" |
|||
style="border: none; display: block;" |
|||
/> |
|||
<!-- 备选方案:如果embed不支持,显示iframe --> |
|||
<iframe |
|||
v-if="showIframe" |
|||
:src="pdfUrl" |
|||
width="100%" |
|||
height="100%" |
|||
style="border: none; display: block;" |
|||
></iframe> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 条款列表(备选展示方式) --> |
|||
<div v-if="showArticlesList" class="articles-list"> |
|||
<Divider>法规条款详情</Divider> |
|||
<div v-for="(article, index) in articles" :key="article.id" class="article-item"> |
|||
<div class="article-title">第{{ index + 1 }}条</div> |
|||
<div class="article-content">{{ article.articleContent }}</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</Drawer> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
import { ref, computed, watch, onUnmounted } from 'vue'; |
|||
import { |
|||
Drawer, |
|||
Button, |
|||
Space, |
|||
Spin, |
|||
Result, |
|||
Divider, |
|||
message |
|||
} from 'ant-design-vue'; |
|||
import { |
|||
DownloadOutlined, |
|||
ReloadOutlined |
|||
} from '@ant-design/icons-vue'; |
|||
import { |
|||
ContractualRegulationNamesViewPdf, |
|||
ContractualRegulationNamesArticles |
|||
} from '@/api/contractReview/ContractualRegulationNames'; |
|||
|
|||
interface Props { |
|||
visible: boolean; |
|||
regulationId?: number | string; |
|||
regulationName?: string; |
|||
} |
|||
|
|||
const props = withDefaults(defineProps<Props>(), { |
|||
visible: false, |
|||
regulationId: undefined, |
|||
regulationName: '' |
|||
}); |
|||
|
|||
const emit = defineEmits<{ |
|||
'update:visible': [value: boolean]; |
|||
}>(); |
|||
|
|||
// 状态变量 |
|||
const loading = ref(false); |
|||
const downloading = ref(false); |
|||
const error = ref(''); |
|||
const articles = ref<any[]>([]); |
|||
const pdfUrl = ref(''); |
|||
const showIframe = ref(false); |
|||
const showArticlesList = ref(false); |
|||
|
|||
// 计算属性 |
|||
const visible = computed({ |
|||
get: () => props.visible, |
|||
set: (value) => emit('update:visible', value) |
|||
}); |
|||
|
|||
const modalTitle = computed(() => { |
|||
return props.regulationName ? `法规详情 - ${props.regulationName}` : '法规详情'; |
|||
}); |
|||
|
|||
// 监听弹窗显示 |
|||
watch(() => props.visible, (newVisible) => { |
|||
if (newVisible && props.regulationId) { |
|||
loadRegulationDetail(); |
|||
} else if (!newVisible) { |
|||
// 弹窗关闭时清理资源 |
|||
cleanupPdfUrl(); |
|||
error.value = ''; |
|||
showIframe.value = false; |
|||
showArticlesList.value = false; |
|||
} |
|||
}); |
|||
|
|||
// 加载法规详情 |
|||
async function loadRegulationDetail() { |
|||
if (!props.regulationId) return; |
|||
|
|||
loading.value = true; |
|||
error.value = ''; |
|||
|
|||
try { |
|||
// 并行加载PDF和条款列表 |
|||
const [pdfResponse, articlesResult] = await Promise.all([ |
|||
ContractualRegulationNamesViewPdf(props.regulationId), |
|||
ContractualRegulationNamesArticles(props.regulationId) |
|||
]); |
|||
|
|||
console.log('PDF响应:', pdfResponse); |
|||
|
|||
// 处理PDF Blob |
|||
if (pdfResponse?.data) { |
|||
console.log('PDF加载成功, 创建Blob'); |
|||
|
|||
// 清理之前的URL |
|||
if (pdfUrl.value && pdfUrl.value.startsWith('blob:')) { |
|||
URL.revokeObjectURL(pdfUrl.value); |
|||
} |
|||
|
|||
// 创建新的Blob URL |
|||
const blob = new Blob([pdfResponse.data], { type: 'application/pdf' }); |
|||
pdfUrl.value = URL.createObjectURL(blob); |
|||
console.log('PDF URL创建成功:', pdfUrl.value); |
|||
} else { |
|||
throw new Error('PDF数据格式错误'); |
|||
} |
|||
|
|||
// 处理条款列表 |
|||
articles.value = articlesResult || []; |
|||
|
|||
} catch (err: any) { |
|||
error.value = err.message || '加载法规详情失败'; |
|||
console.error('加载法规详情失败:', err); |
|||
} finally { |
|||
loading.value = false; |
|||
} |
|||
} |
|||
|
|||
// 刷新PDF |
|||
async function refreshPdf() { |
|||
if (!props.regulationId) return; |
|||
|
|||
loading.value = true; |
|||
error.value = ''; |
|||
|
|||
try { |
|||
const pdfResponse = await ContractualRegulationNamesViewPdf(props.regulationId); |
|||
|
|||
if (pdfResponse?.data) { |
|||
// 清理之前的URL |
|||
if (pdfUrl.value && pdfUrl.value.startsWith('blob:')) { |
|||
URL.revokeObjectURL(pdfUrl.value); |
|||
} |
|||
// 创建新的Blob URL |
|||
const blob = new Blob([pdfResponse.data], { type: 'application/pdf' }); |
|||
pdfUrl.value = URL.createObjectURL(blob); |
|||
} else { |
|||
throw new Error('PDF数据格式错误'); |
|||
} |
|||
} catch (err: any) { |
|||
error.value = err.message || '刷新PDF失败'; |
|||
console.error('刷新PDF失败:', err); |
|||
} finally { |
|||
loading.value = false; |
|||
} |
|||
} |
|||
|
|||
// 下载PDF |
|||
async function downloadPdf() { |
|||
if (!props.regulationId) return; |
|||
|
|||
downloading.value = true; |
|||
try { |
|||
const pdfResponse = await ContractualRegulationNamesViewPdf(props.regulationId); |
|||
|
|||
if (pdfResponse?.data) { |
|||
const blob = new Blob([pdfResponse.data], { type: 'application/pdf' }); |
|||
const url = URL.createObjectURL(blob); |
|||
const link = document.createElement('a'); |
|||
link.href = url; |
|||
link.download = `${props.regulationName || '法规文档'}.pdf`; |
|||
document.body.appendChild(link); |
|||
link.click(); |
|||
document.body.removeChild(link); |
|||
|
|||
// 清理临时URL |
|||
URL.revokeObjectURL(url); |
|||
message.success('PDF下载已开始'); |
|||
} else { |
|||
throw new Error('PDF数据格式错误'); |
|||
} |
|||
} catch (err: any) { |
|||
message.error('下载失败: ' + (err.message || '未知错误')); |
|||
console.error('下载PDF失败:', err); |
|||
} finally { |
|||
downloading.value = false; |
|||
} |
|||
} |
|||
|
|||
// 切换显示模式 |
|||
function toggleDisplayMode() { |
|||
showArticlesList.value = !showArticlesList.value; |
|||
} |
|||
|
|||
// PDF加载错误处理 |
|||
function handlePdfError() { |
|||
showIframe.value = true; |
|||
} |
|||
|
|||
// 清理Blob URL |
|||
function cleanupPdfUrl() { |
|||
if (pdfUrl.value && pdfUrl.value.startsWith('blob:')) { |
|||
URL.revokeObjectURL(pdfUrl.value); |
|||
pdfUrl.value = ''; |
|||
} |
|||
} |
|||
|
|||
// 组件销毁时清理 |
|||
onUnmounted(() => { |
|||
cleanupPdfUrl(); |
|||
}); |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.regulation-view-drawer { |
|||
:deep(.ant-drawer-content) { |
|||
height: 100vh; |
|||
} |
|||
|
|||
:deep(.ant-drawer-body) { |
|||
padding: 0; |
|||
height: calc(100vh - 55px); |
|||
display: flex; |
|||
flex-direction: column; |
|||
} |
|||
|
|||
.regulation-view-container { |
|||
height: 100%; |
|||
display: flex; |
|||
flex-direction: column; |
|||
padding: 16px; |
|||
} |
|||
|
|||
.toolbar { |
|||
margin-bottom: 16px; |
|||
padding: 12px; |
|||
background: #fafafa; |
|||
border-radius: 6px; |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
flex-shrink: 0; |
|||
} |
|||
|
|||
.pdf-container { |
|||
flex: 1; |
|||
position: relative; |
|||
min-height: 0; |
|||
|
|||
.loading-container { |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
height: 100%; |
|||
} |
|||
|
|||
.error-container { |
|||
padding: 40px; |
|||
height: 100%; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
} |
|||
|
|||
.pdf-embed-container { |
|||
border: 1px solid #d9d9d9; |
|||
border-radius: 6px; |
|||
overflow: hidden; |
|||
height: 100%; |
|||
|
|||
embed, iframe { |
|||
display: block; |
|||
height: 100% !important; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.articles-list { |
|||
margin-top: 24px; |
|||
flex-shrink: 0; |
|||
max-height: 300px; |
|||
overflow-y: auto; |
|||
|
|||
.article-item { |
|||
margin-bottom: 24px; |
|||
padding: 16px; |
|||
border: 1px solid #e8e8e8; |
|||
border-radius: 6px; |
|||
background: #fafafa; |
|||
|
|||
.article-title { |
|||
font-weight: bold; |
|||
font-size: 16px; |
|||
color: #262626; |
|||
margin-bottom: 12px; |
|||
padding-bottom: 8px; |
|||
border-bottom: 1px solid #e8e8e8; |
|||
} |
|||
|
|||
.article-content { |
|||
line-height: 1.8; |
|||
color: #595959; |
|||
text-align: justify; |
|||
white-space: pre-wrap; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/* 响应式设计 */ |
|||
@media (max-width: 1200px) { |
|||
.regulation-view-drawer { |
|||
:deep(.ant-drawer) { |
|||
width: 80% !important; |
|||
} |
|||
} |
|||
} |
|||
|
|||
@media (max-width: 768px) { |
|||
.regulation-view-drawer { |
|||
:deep(.ant-drawer) { |
|||
width: 90% !important; |
|||
} |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,442 @@ |
|||
<template> |
|||
<PageWrapper dense> |
|||
<!-- 搜索表单 --> |
|||
<Card class="search-card"> |
|||
<Form |
|||
:model="searchForm" |
|||
layout="inline" |
|||
class="search-form" |
|||
> |
|||
<FormItem label="法规名称"> |
|||
<Input |
|||
v-model:value="searchForm.regulationName" |
|||
placeholder="请输入法规名称" |
|||
style="width: 200px" |
|||
allowClear |
|||
/> |
|||
</FormItem> |
|||
<FormItem label="发布日期"> |
|||
<RangePicker |
|||
v-model:value="searchForm.publishDateRange" |
|||
format="YYYY-MM-DD" |
|||
valueFormat="YYYY-MM-DD" |
|||
style="width: 240px" |
|||
allowClear |
|||
:placeholder="['开始日期', '结束日期']" |
|||
/> |
|||
</FormItem> |
|||
<FormItem label="是否有效"> |
|||
<Select |
|||
v-model:value="searchForm.isEffective" |
|||
placeholder="请选择状态" |
|||
style="width: 120px" |
|||
allowClear |
|||
:options="effectiveOptions" |
|||
/> |
|||
</FormItem> |
|||
<FormItem> |
|||
<Space> |
|||
<Button type="primary" @click="handleSearch">查询</Button> |
|||
<Button @click="resetSearch">重置</Button> |
|||
</Space> |
|||
</FormItem> |
|||
</Form> |
|||
</Card> |
|||
|
|||
<!-- 表格区域 --> |
|||
<Card class="table-card"> |
|||
<!-- 表格标题和操作按钮 --> |
|||
<div class="table-header"> |
|||
<div class="table-title">合同法规文件列表</div> |
|||
<div class="table-actions"> |
|||
<Space> |
|||
<Button |
|||
type="primary" |
|||
danger |
|||
@click="multipleRemove" |
|||
:disabled="!selectedRowKeys.length" |
|||
v-auth="'productManagement:ContractualRegulationNames:remove'" |
|||
> |
|||
删除 |
|||
</Button> |
|||
<Button type="primary" @click="handleAdd" v-auth="'productManagement:ContractualRegulationNames:add'"> |
|||
新增 |
|||
</Button> |
|||
<Button @click="handleRefresh"> |
|||
<template #icon><ReloadOutlined /></template> |
|||
</Button> |
|||
</Space> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 数据表格 --> |
|||
<Table |
|||
:dataSource="dataSource" |
|||
:columns="tableColumns" |
|||
:loading="loading" |
|||
:pagination="pagination" |
|||
:rowSelection="rowSelection" |
|||
rowKey="id" |
|||
@change="handleTableChange" |
|||
size="middle" |
|||
/> |
|||
</Card> |
|||
|
|||
<!-- 新增弹窗 --> |
|||
<AddModal |
|||
v-model:visible="modalVisible" |
|||
@success="loadData" |
|||
/> |
|||
|
|||
<!-- 查看弹窗 --> |
|||
<ViewModal |
|||
v-model:visible="viewModalVisible" |
|||
:regulationId="selectedRecord?.id" |
|||
:regulationName="selectedRecord?.regulationName" |
|||
/> |
|||
|
|||
<!-- 修改弹窗 --> |
|||
<EditModal |
|||
v-model:visible="editModalVisible" |
|||
:regulationId="selectedRecord?.id" |
|||
@success="loadData" |
|||
/> |
|||
</PageWrapper> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
import { ref, reactive, onMounted, h } from 'vue'; |
|||
import { PageWrapper } from '@/components/Page'; |
|||
import { |
|||
Table, |
|||
Button, |
|||
Space, |
|||
Card, |
|||
Form, |
|||
FormItem, |
|||
Input, |
|||
DatePicker, |
|||
Select, |
|||
Switch, |
|||
Modal, |
|||
message |
|||
} from 'ant-design-vue'; |
|||
import { |
|||
ReloadOutlined, |
|||
EyeOutlined, |
|||
DeleteOutlined, |
|||
EditOutlined |
|||
} from '@ant-design/icons-vue'; |
|||
import { |
|||
ContractualRegulationNamesList, |
|||
ContractualRegulationNamesRemove, |
|||
ContractualRegulationNamesUpdateStatus |
|||
} from '@/api/contractReview/ContractualRegulationNames'; |
|||
import { getDictOptions } from '@/utils/dict'; |
|||
import AddModal from './AddModal.vue'; |
|||
import ViewModal from './ViewModal.vue'; |
|||
import EditModal from './EditModal.vue'; |
|||
const { RangePicker } = DatePicker; |
|||
import { useRender } from '@/hooks/component/useRender'; |
|||
|
|||
const { renderDict } = useRender(); |
|||
defineOptions({ name: 'ContractualRegulationNames' }); |
|||
|
|||
// 状态变量 |
|||
const loading = ref(false); |
|||
const dataSource = ref<any[]>([]); |
|||
const selectedRowKeys = ref<any[]>([]); |
|||
const modalVisible = ref(false); |
|||
const viewModalVisible = ref(false); |
|||
const editModalVisible = ref(false); |
|||
const selectedRecord = ref<any>(null); |
|||
const searchForm = reactive({ |
|||
regulationName: '', |
|||
publishDateRange: null as any, |
|||
isEffective: undefined as string | undefined |
|||
}); |
|||
|
|||
// 分页配置 |
|||
const pagination = reactive({ |
|||
current: 1, |
|||
pageSize: 10, |
|||
total: 0, |
|||
showSizeChanger: true, |
|||
showQuickJumper: true, |
|||
showTotal: (total: number) => `共 ${total} 条数据`, |
|||
}); |
|||
|
|||
// 行选择配置 |
|||
const rowSelection = { |
|||
selectedRowKeys: selectedRowKeys.value, |
|||
onChange: (keys: any[]) => { |
|||
selectedRowKeys.value = keys; |
|||
}, |
|||
}; |
|||
|
|||
// 字典选项 |
|||
const effectiveOptions = getDictOptions('sys_yes_no'); |
|||
|
|||
// 开关状态切换 |
|||
async function handleSwitchChange(record: any) { |
|||
if (record._switching) return; |
|||
|
|||
record._switching = true; |
|||
try { |
|||
const newStatus = record.isEffective === 'Y' ? 'N' : 'Y'; |
|||
await ContractualRegulationNamesUpdateStatus(record.id, newStatus); |
|||
record.isEffective = newStatus; |
|||
message.success('状态更新成功'); |
|||
} catch (error) { |
|||
message.error('状态更新失败'); |
|||
} finally { |
|||
record._switching = false; |
|||
} |
|||
} |
|||
|
|||
// 表格列配置 - 保持原本字段 |
|||
const tableColumns = [ |
|||
{ |
|||
title: '法规名称', |
|||
dataIndex: 'regulationName', |
|||
key: 'regulationName', |
|||
width: 180, |
|||
}, |
|||
{ |
|||
title: '法规描述', |
|||
dataIndex: 'regulationDescription', |
|||
key: 'regulationDescription', |
|||
width: 200, |
|||
ellipsis: true, |
|||
}, |
|||
{ |
|||
title: '发布日期', |
|||
dataIndex: 'publishDate', |
|||
key: 'publishDate', |
|||
width: 150, |
|||
}, |
|||
{ |
|||
title: '是否有效', |
|||
dataIndex: 'isEffective', |
|||
key: 'isEffective', |
|||
width: 120, |
|||
customRender: ({ record }: any) => { |
|||
return h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px' } }, [ |
|||
h(Switch, { |
|||
checked: record.isEffective === 'Y', |
|||
loading: record._switching, |
|||
onChange: () => handleSwitchChange(record) |
|||
}), |
|||
h('span', {}, record.isEffective === 'Y' ? '有效' : '无效') |
|||
]); |
|||
}, |
|||
}, |
|||
{ |
|||
title: '当前状态', |
|||
dataIndex: 'progressStatus', |
|||
key: 'progressStatus', |
|||
width: 120, |
|||
customRender: ({ value }) => renderDict(value, 'file_progress_status'), |
|||
|
|||
}, |
|||
{ |
|||
title: '操作', |
|||
key: 'action', |
|||
fixed: 'right' as const, |
|||
width: 180, |
|||
customRender: ({ record }: any) => { |
|||
return h(Space, {}, [ |
|||
h(Button, { |
|||
type: 'primary', |
|||
size: 'small', |
|||
icon: h(EyeOutlined), |
|||
onClick: () => handleView(record) |
|||
}, '查看'), |
|||
h(Button, { |
|||
type: 'default', |
|||
size: 'small', |
|||
icon: h(EditOutlined), |
|||
onClick: () => handleEdit(record) |
|||
}, '修改'), |
|||
h(Button, { |
|||
type: 'primary', |
|||
danger: true, |
|||
size: 'small', |
|||
icon: h(DeleteOutlined), |
|||
onClick: () => handleDelete(record) |
|||
}, '删除') |
|||
]); |
|||
}, |
|||
} |
|||
]; |
|||
|
|||
// 过滤空值参数 |
|||
function filterEmptyParams(params: any) { |
|||
const filteredParams: any = {}; |
|||
Object.keys(params).forEach(key => { |
|||
const value = params[key]; |
|||
if (value !== null && value !== undefined && value !== '') { |
|||
filteredParams[key] = value; |
|||
} |
|||
}); |
|||
return filteredParams; |
|||
} |
|||
|
|||
// 加载数据 |
|||
async function loadData() { |
|||
loading.value = true; |
|||
try { |
|||
const baseParams: any = { |
|||
regulationName: searchForm.regulationName, |
|||
isEffective: searchForm.isEffective, |
|||
pageNum: pagination.current, |
|||
pageSize: pagination.pageSize, |
|||
}; |
|||
|
|||
// 处理日期范围参数 |
|||
if (searchForm.publishDateRange && searchForm.publishDateRange.length === 2) { |
|||
baseParams.publishDateStart = searchForm.publishDateRange[0]; |
|||
baseParams.publishDateEnd = searchForm.publishDateRange[1]; |
|||
} |
|||
|
|||
// 过滤空值参数 |
|||
const params = filterEmptyParams(baseParams); |
|||
|
|||
const result: any = await ContractualRegulationNamesList(params); |
|||
dataSource.value = result.rows || []; |
|||
pagination.total = result.total || 0; |
|||
} catch (error) { |
|||
message.error('加载数据失败'); |
|||
} finally { |
|||
loading.value = false; |
|||
} |
|||
} |
|||
|
|||
// 表格变化处理 |
|||
function handleTableChange(pag: any) { |
|||
pagination.current = pag.current; |
|||
pagination.pageSize = pag.pageSize; |
|||
loadData(); |
|||
} |
|||
|
|||
// 搜索处理 |
|||
function handleSearch() { |
|||
pagination.current = 1; |
|||
loadData(); |
|||
} |
|||
|
|||
// 重置搜索 |
|||
function resetSearch() { |
|||
Object.assign(searchForm, { |
|||
regulationName: '', |
|||
publishDateRange: null, |
|||
isEffective: undefined |
|||
}); |
|||
pagination.current = 1; |
|||
loadData(); |
|||
} |
|||
|
|||
// 刷新 |
|||
function handleRefresh() { |
|||
loadData(); |
|||
} |
|||
|
|||
// 新增 |
|||
function handleAdd() { |
|||
modalVisible.value = true; |
|||
} |
|||
|
|||
// 查看详情 |
|||
function handleView(record: any) { |
|||
selectedRecord.value = record; |
|||
viewModalVisible.value = true; |
|||
} |
|||
|
|||
// 修改法规 |
|||
function handleEdit(record: any) { |
|||
selectedRecord.value = record; |
|||
editModalVisible.value = true; |
|||
} |
|||
|
|||
// 单个删除 |
|||
async function handleDelete(record: any) { |
|||
Modal.confirm({ |
|||
title: '删除确认', |
|||
content: `是否删除《${record.regulationName}》?`, |
|||
onOk: async () => { |
|||
try { |
|||
await ContractualRegulationNamesRemove([record.id]); |
|||
message.success('删除成功'); |
|||
loadData(); |
|||
} catch (error) { |
|||
message.error('删除失败'); |
|||
} |
|||
}, |
|||
}); |
|||
} |
|||
|
|||
// 批量删除 |
|||
async function multipleRemove() { |
|||
if (!selectedRowKeys.value.length) { |
|||
message.warning('请选择要删除的记录'); |
|||
return; |
|||
} |
|||
|
|||
const selectedRecords = dataSource.value.filter((item: any) => |
|||
selectedRowKeys.value.includes(item.id) |
|||
); |
|||
const regulationNames = selectedRecords.map((item: any) => item.regulationName).join('、'); |
|||
|
|||
Modal.confirm({ |
|||
title: '删除确认', |
|||
content: `是否删除《${regulationNames}》?`, |
|||
onOk: async () => { |
|||
try { |
|||
await ContractualRegulationNamesRemove(selectedRowKeys.value); |
|||
message.success('删除成功'); |
|||
selectedRowKeys.value = []; |
|||
loadData(); |
|||
} catch (error) { |
|||
message.error('删除失败'); |
|||
} |
|||
}, |
|||
}); |
|||
} |
|||
|
|||
// 页面加载时获取数据 |
|||
onMounted(() => { |
|||
loadData(); |
|||
}); |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.search-card { |
|||
margin-bottom: 16px; |
|||
} |
|||
|
|||
.search-form { |
|||
margin-bottom: 0; |
|||
} |
|||
|
|||
.table-card { |
|||
background: #fff; |
|||
} |
|||
|
|||
.table-header { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
margin-bottom: 16px; |
|||
} |
|||
|
|||
.table-title { |
|||
font-size: 16px; |
|||
font-weight: 500; |
|||
} |
|||
|
|||
:deep(.ant-table) { |
|||
.ant-table-tbody > tr > td { |
|||
padding: 12px 8px; |
|||
} |
|||
} |
|||
</style> |
Loading…
Reference in new issue