10 changed files with 1732 additions and 32 deletions
@ -0,0 +1,515 @@ |
|||||
|
<template> |
||||
|
<BasicDrawer |
||||
|
v-bind="$attrs" |
||||
|
@register="registerDrawer" |
||||
|
showFooter |
||||
|
title="文档审核结果" |
||||
|
width="80%" |
||||
|
:canFullscreen="true" |
||||
|
> |
||||
|
<template #default> |
||||
|
<div class="document-review-container"> |
||||
|
<div class="tasks-container"> |
||||
|
<Collapse v-model:activeKey="activeCollapseKeys" class="tasks-collapse"> |
||||
|
<CollapsePanel |
||||
|
v-for="(category, index) in filteredTaskResultDetail" |
||||
|
:key="index.toString()" |
||||
|
:header="category.name + ' (' + category.results.length + ')'" |
||||
|
> |
||||
|
<div class="category-items"> |
||||
|
<Collapse v-model:activeKey="activeItemKeys" class="items-collapse"> |
||||
|
<CollapsePanel |
||||
|
v-for="(item, idx) in category.results" |
||||
|
:key="getItemKey(index, idx, item)" |
||||
|
> |
||||
|
<template #header> |
||||
|
<div class="item-header-content"> |
||||
|
<div class="item-info"> |
||||
|
<span class="item-serial">{{ item.serialNumber }}</span> |
||||
|
<span class="item-title">{{ item.issueName }}</span> |
||||
|
</div> |
||||
|
<div class="item-actions"> |
||||
|
<Switch |
||||
|
:checked="item.isRead === '1'" |
||||
|
checked-children="已读" |
||||
|
un-checked-children="未读" |
||||
|
:loading="loading && currentOpId === item.id && currentOpField === 'isRead'" |
||||
|
@change="(checked: any) => handleStatusChange(item.id!, 'isRead', checked ? '1' : '0')" |
||||
|
/> |
||||
|
<Switch |
||||
|
class="ml-3" |
||||
|
:checked="item.isAdopted === '1'" |
||||
|
checked-children="已采纳" |
||||
|
un-checked-children="未采纳" |
||||
|
:loading="loading && currentOpId === item.id && currentOpField === 'isAdopted'" |
||||
|
@change="(checked: any) => handleStatusChange(item.id!, 'isAdopted', checked ? '1' : '0')" |
||||
|
/> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
<div class="item-content"> |
||||
|
<!-- 基于配置动态渲染内容区域 --> |
||||
|
<template v-for="(section, sectionIndex) in getContentSections(category.type, item)" :key="sectionIndex"> |
||||
|
<div v-if="getItemValue(item, section.field)" class="content-section"> |
||||
|
<div class="section-title">{{ section.title }}:</div> |
||||
|
<div class="section-content markdown-content" v-html="renderContent(section, getItemValue(item, section.field))"></div> |
||||
|
<!-- 特殊处理审查要点列表 --> |
||||
|
<div v-if="section.type === 'reviewPointsList' && section.field === 'reviewBasis' && item.reviewBasis && item.reviewBasis.reviewPoints && item.reviewBasis.reviewPoints.length"> |
||||
|
<Divider style="margin: 8px 0" /> |
||||
|
<ul class="review-points"> |
||||
|
<li v-for="(point, pointIdx) in item.reviewBasis.reviewPoints" :key="pointIdx"> |
||||
|
{{ point }} |
||||
|
</li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
</div> |
||||
|
</CollapsePanel> |
||||
|
</Collapse> |
||||
|
</div> |
||||
|
</CollapsePanel> |
||||
|
</Collapse> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
<template #footer> |
||||
|
<div class="drawer-footer"> |
||||
|
<div class="status-switches"> |
||||
|
<Switch |
||||
|
:checked="expandReadItems" |
||||
|
checked-children="展开已读" |
||||
|
un-checked-children="折叠已读" |
||||
|
@change="(checked: any) => toggleExpandReadItems(checked)" |
||||
|
/> |
||||
|
<Switch |
||||
|
class="ml-3" |
||||
|
:checked="expandAdoptedItems" |
||||
|
checked-children="展开已采纳" |
||||
|
un-checked-children="折叠已采纳" |
||||
|
@change="(checked: any) => toggleExpandAdoptedItems(checked)" |
||||
|
/> |
||||
|
</div> |
||||
|
<Button type="primary" @click="handleClose">关闭</Button> |
||||
|
</div> |
||||
|
</template> |
||||
|
</BasicDrawer> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts" setup> |
||||
|
import { ref, watch, onMounted, computed } from 'vue'; |
||||
|
import { BasicDrawer, useDrawerInner } from '@/components/Drawer'; |
||||
|
import { DocumentTaskResultDetailVO } from '@/api/documentReview/DocumentTaskResults/model'; |
||||
|
import { updateResultItemStatus } from '@/api/documentReview/DocumentTaskResults'; |
||||
|
import { message } from 'ant-design-vue'; |
||||
|
import { Switch, Tabs, TabPane, Divider, Button, Collapse, CollapsePanel } from 'ant-design-vue'; |
||||
|
import MarkdownIt from 'markdown-it'; |
||||
|
|
||||
|
const md = new MarkdownIt({ |
||||
|
html: true, |
||||
|
linkify: true, |
||||
|
typographer: true, |
||||
|
}); |
||||
|
|
||||
|
const activeCollapseKeys = ref<string[]>([]); |
||||
|
const activeItemKeys = ref<string[]>([]); |
||||
|
const taskResultDetail = ref<DocumentTaskResultDetailVO[]>([]); |
||||
|
const taskInfo = ref<Recordable>({}); |
||||
|
const loading = ref<boolean>(false); |
||||
|
const currentOpId = ref<string>(''); |
||||
|
const currentOpField = ref<string>(''); |
||||
|
const expandReadItems = ref<boolean>(false); |
||||
|
const expandAdoptedItems = ref<boolean>(false); |
||||
|
|
||||
|
// 内容区域配置,定义不同类型任务显示哪些内容 |
||||
|
interface ContentSectionConfig { |
||||
|
field: string; |
||||
|
title: string; |
||||
|
type?: 'markdown' | 'text' | 'reviewPointsList'; |
||||
|
nestedFields?: string[]; |
||||
|
} |
||||
|
|
||||
|
// 全局默认配置,所有类型都默认显示这些字段 |
||||
|
const defaultContentSections: ContentSectionConfig[] = [ |
||||
|
{ field: 'originalText', title: '原文', type: 'markdown' }, |
||||
|
{ field: 'comparedText', title: '比对原文', type: 'markdown' }, |
||||
|
{ field: 'modifiedContent', title: '修改后内容', type: 'markdown' }, |
||||
|
{ field: 'modificationDisplay', title: '修改情况', type: 'markdown' }, |
||||
|
{ field: 'existingIssues', title: '存在问题', type: 'markdown' }, |
||||
|
{ |
||||
|
field: 'reviewBasis', |
||||
|
title: '审查依据', |
||||
|
type: 'reviewPointsList', |
||||
|
nestedFields: ['reviewContent', 'reviewPoints'] |
||||
|
} |
||||
|
]; |
||||
|
|
||||
|
// 不同类型任务的特定配置,可以覆盖或扩展默认配置 |
||||
|
const taskTypeContentConfig: Record<string, ContentSectionConfig[]> = { |
||||
|
// 示例:合同审核特定配置 |
||||
|
'contract': [ |
||||
|
{ field: 'originalText', title: '合同原文', type: 'markdown' }, |
||||
|
{ field: 'legalIssues', title: '法律问题', type: 'markdown' }, |
||||
|
{ field: 'suggestions', title: '修改建议', type: 'markdown' }, |
||||
|
{ |
||||
|
field: 'reviewBasis', |
||||
|
title: '法律依据', |
||||
|
type: 'reviewPointsList', |
||||
|
nestedFields: ['reviewContent', 'reviewPoints'] |
||||
|
} |
||||
|
], |
||||
|
// 可以添加更多任务类型的配置 |
||||
|
}; |
||||
|
|
||||
|
// 根据任务类型获取内容配置 |
||||
|
const getContentSections = (taskType: string, item: any): ContentSectionConfig[] => { |
||||
|
// 如果存在特定类型的配置则使用,否则使用默认配置 |
||||
|
return taskTypeContentConfig[taskType] || defaultContentSections; |
||||
|
}; |
||||
|
|
||||
|
// 从项目中获取特定字段的值 |
||||
|
const getItemValue = (item: any, field: string): any => { |
||||
|
if (!item || !field) return null; |
||||
|
|
||||
|
// 支持嵌套字段 如 "reviewBasis.reviewContent" |
||||
|
if (field.includes('.')) { |
||||
|
const parts = field.split('.'); |
||||
|
let value = item; |
||||
|
for (const part of parts) { |
||||
|
if (value && typeof value === 'object') { |
||||
|
value = value[part]; |
||||
|
} else { |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
return value; |
||||
|
} |
||||
|
|
||||
|
return item[field]; |
||||
|
}; |
||||
|
|
||||
|
// 根据配置渲染内容 |
||||
|
const renderContent = (section: ContentSectionConfig, value: any): string => { |
||||
|
if (!value) return ''; |
||||
|
|
||||
|
if (section.type === 'markdown') { |
||||
|
return renderMarkdown(value); |
||||
|
} |
||||
|
|
||||
|
if (section.type === 'reviewPointsList' && section.nestedFields) { |
||||
|
let content = ''; |
||||
|
|
||||
|
// 渲染reviewContent部分 |
||||
|
if (section.nestedFields.includes('reviewContent') && value.reviewContent) { |
||||
|
content += renderMarkdown(value.reviewContent); |
||||
|
} |
||||
|
|
||||
|
return content; |
||||
|
} |
||||
|
|
||||
|
return value.toString(); |
||||
|
}; |
||||
|
|
||||
|
// 根据条件过滤展示的结果 |
||||
|
const filteredTaskResultDetail = computed(() => { |
||||
|
if (taskResultDetail.value.length <= 0) { |
||||
|
return []; |
||||
|
} |
||||
|
|
||||
|
// 如果只有两个或更少的类别,或者第一个类别的results数量<=5个,只展示第一个类别 |
||||
|
if (taskResultDetail.value.length <= 2 || |
||||
|
(taskResultDetail.value[0] && taskResultDetail.value[0].results && |
||||
|
taskResultDetail.value[0].results.length <= 5)) { |
||||
|
return [taskResultDetail.value[0]]; |
||||
|
} |
||||
|
|
||||
|
return taskResultDetail.value; |
||||
|
}); |
||||
|
|
||||
|
// 生成审核项的唯一key |
||||
|
const getItemKey = (categoryIndex: number, itemIndex: number, item: any) => { |
||||
|
return item.id ? item.id : `${categoryIndex}-${itemIndex}`; |
||||
|
}; |
||||
|
|
||||
|
// 根据过滤条件更新折叠状态 |
||||
|
const updateActiveItemKeys = () => { |
||||
|
const newActiveKeys: string[] = []; |
||||
|
|
||||
|
filteredTaskResultDetail.value.forEach((category, categoryIndex) => { |
||||
|
category.results.forEach((item, itemIndex) => { |
||||
|
const itemKey = getItemKey(categoryIndex, itemIndex, item); |
||||
|
const isRead = item.isRead === '1'; |
||||
|
const isAdopted = item.isAdopted === '1'; |
||||
|
|
||||
|
// 如果项目未读且未采纳,或符合展开条件,则展开 |
||||
|
if ((!isRead && !isAdopted) || |
||||
|
(isRead && expandReadItems.value) || |
||||
|
(isAdopted && expandAdoptedItems.value)) { |
||||
|
newActiveKeys.push(itemKey); |
||||
|
} |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
activeItemKeys.value = newActiveKeys; |
||||
|
}; |
||||
|
|
||||
|
// 切换展开已读项 |
||||
|
const toggleExpandReadItems = (checked: any) => { |
||||
|
expandReadItems.value = !!checked; |
||||
|
updateActiveItemKeys(); |
||||
|
}; |
||||
|
|
||||
|
// 切换展开已采纳项 |
||||
|
const toggleExpandAdoptedItems = (checked: any) => { |
||||
|
expandAdoptedItems.value = !!checked; |
||||
|
updateActiveItemKeys(); |
||||
|
}; |
||||
|
|
||||
|
const renderMarkdown = (text) => { |
||||
|
if (!text) return ''; |
||||
|
return md.render(text); |
||||
|
}; |
||||
|
|
||||
|
const [registerDrawer, { closeDrawer, setDrawerProps }] = useDrawerInner(async (data) => { |
||||
|
setDrawerProps({ loading: true }); |
||||
|
try { |
||||
|
if (data?.taskResultDetail) { |
||||
|
taskResultDetail.value = data.taskResultDetail; |
||||
|
taskInfo.value = data.taskInfo || {}; |
||||
|
|
||||
|
// 默认展开第一个分类 |
||||
|
if (taskResultDetail.value.length > 0) { |
||||
|
activeCollapseKeys.value = ['0']; |
||||
|
} |
||||
|
|
||||
|
// 初始化项目折叠状态 |
||||
|
updateActiveItemKeys(); |
||||
|
} |
||||
|
} catch (error) { |
||||
|
console.error('初始化详情抽屉时出错', error); |
||||
|
} finally { |
||||
|
setDrawerProps({ loading: false }); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
async function handleStatusChange(id: string, field: 'isRead' | 'isAdopted', value: '0' | '1') { |
||||
|
if (!id) { |
||||
|
message.error('缺少记录ID,无法更新状态'); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
loading.value = true; |
||||
|
currentOpId.value = id; |
||||
|
currentOpField.value = field; |
||||
|
|
||||
|
try { |
||||
|
await updateResultItemStatus(id, field, value); |
||||
|
message.success(`状态更新成功`); |
||||
|
|
||||
|
// 更新本地状态 |
||||
|
taskResultDetail.value.forEach(category => { |
||||
|
category.results.forEach(item => { |
||||
|
if (item.id === id) { |
||||
|
item[field] = value; |
||||
|
} |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
// 更新折叠状态 |
||||
|
updateActiveItemKeys(); |
||||
|
} catch (error) { |
||||
|
console.error('更新状态失败', error); |
||||
|
message.error('更新状态失败'); |
||||
|
} finally { |
||||
|
loading.value = false; |
||||
|
currentOpId.value = ''; |
||||
|
currentOpField.value = ''; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function handleClose() { |
||||
|
closeDrawer(); |
||||
|
} |
||||
|
|
||||
|
// 监听状态变化,更新折叠状态 |
||||
|
watch([expandReadItems, expandAdoptedItems], () => { |
||||
|
updateActiveItemKeys(); |
||||
|
}); |
||||
|
|
||||
|
onMounted(() => { |
||||
|
// 初始化后执行的代码 |
||||
|
}); |
||||
|
</script> |
||||
|
|
||||
|
<style lang="less" scoped> |
||||
|
:deep(.ant-drawer-body) { |
||||
|
padding: 16px; |
||||
|
height: calc(100% - 100px); |
||||
|
overflow: auto; |
||||
|
} |
||||
|
|
||||
|
.document-review-container { |
||||
|
height: 100%; |
||||
|
} |
||||
|
|
||||
|
.tasks-container { |
||||
|
height: 100%; |
||||
|
overflow: auto; |
||||
|
} |
||||
|
|
||||
|
.tasks-collapse { |
||||
|
margin-bottom: 16px; |
||||
|
|
||||
|
:deep(.ant-collapse-header) { |
||||
|
font-weight: 500; |
||||
|
padding: 12px 16px; |
||||
|
background-color: #f8f8f8; |
||||
|
} |
||||
|
|
||||
|
:deep(.ant-collapse-content-box) { |
||||
|
padding: 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.items-collapse { |
||||
|
:deep(.ant-collapse-item) { |
||||
|
margin-bottom: 12px; |
||||
|
border: 1px solid #e8e8e8; |
||||
|
border-radius: 4px !important; |
||||
|
overflow: hidden; |
||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); |
||||
|
background-color: #fff; |
||||
|
} |
||||
|
|
||||
|
:deep(.ant-collapse-header) { |
||||
|
padding: 8px 16px !important; |
||||
|
background-color: #f8f8f8; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.item-header-content { |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
width: 100%; |
||||
|
} |
||||
|
|
||||
|
.item-info { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
} |
||||
|
|
||||
|
.item-serial { |
||||
|
margin-right: 12px; |
||||
|
font-weight: bold; |
||||
|
color: #1890ff; |
||||
|
} |
||||
|
|
||||
|
.item-title { |
||||
|
font-weight: 500; |
||||
|
font-size: 16px; |
||||
|
} |
||||
|
|
||||
|
.item-actions { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
margin-left: 16px; |
||||
|
} |
||||
|
|
||||
|
.item-content { |
||||
|
padding: 16px; |
||||
|
} |
||||
|
|
||||
|
.content-section { |
||||
|
margin-bottom: 16px; |
||||
|
} |
||||
|
|
||||
|
.section-title { |
||||
|
font-weight: 500; |
||||
|
margin-bottom: 8px; |
||||
|
color: #333; |
||||
|
} |
||||
|
|
||||
|
.section-content { |
||||
|
padding: 0 0 0 24px; |
||||
|
background-color: #f9fbfd; |
||||
|
border-radius: 4px; |
||||
|
padding: 12px; |
||||
|
} |
||||
|
|
||||
|
.markdown-content { |
||||
|
:deep(p) { |
||||
|
margin-bottom: 8px; |
||||
|
} |
||||
|
|
||||
|
:deep(ul), :deep(ol) { |
||||
|
padding-left: 24px; |
||||
|
} |
||||
|
|
||||
|
:deep(h1), :deep(h2), :deep(h3), :deep(h4), :deep(h5), :deep(h6) { |
||||
|
margin-top: 16px; |
||||
|
margin-bottom: 8px; |
||||
|
} |
||||
|
|
||||
|
:deep(code) { |
||||
|
background-color: #f5f5f5; |
||||
|
padding: 2px 4px; |
||||
|
border-radius: 4px; |
||||
|
} |
||||
|
|
||||
|
:deep(pre) { |
||||
|
background-color: #f5f5f5; |
||||
|
padding: 12px; |
||||
|
border-radius: 4px; |
||||
|
overflow: auto; |
||||
|
} |
||||
|
|
||||
|
:deep(blockquote) { |
||||
|
border-left: 4px solid #ddd; |
||||
|
padding-left: 16px; |
||||
|
color: #666; |
||||
|
margin: 16px 0; |
||||
|
} |
||||
|
|
||||
|
:deep(table) { |
||||
|
border-collapse: collapse; |
||||
|
width: 100%; |
||||
|
margin: 16px 0; |
||||
|
|
||||
|
th, td { |
||||
|
border: 1px solid #e8e8e8; |
||||
|
padding: 8px 12px; |
||||
|
} |
||||
|
|
||||
|
th { |
||||
|
background-color: #f8f8f8; |
||||
|
font-weight: 500; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.review-points { |
||||
|
list-style-type: disc; |
||||
|
padding-left: 24px; |
||||
|
|
||||
|
li { |
||||
|
margin-bottom: 4px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.ml-3 { |
||||
|
margin-left: 12px; |
||||
|
} |
||||
|
|
||||
|
.drawer-footer { |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
width: 100%; |
||||
|
} |
||||
|
|
||||
|
.status-switches { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,674 @@ |
|||||
|
<template> |
||||
|
<BasicDrawer |
||||
|
v-bind="$attrs" |
||||
|
@register="registerDrawer" |
||||
|
showFooter |
||||
|
title="文档审核结果" |
||||
|
width="50%" |
||||
|
:canFullscreen="true" |
||||
|
> |
||||
|
<template #default> |
||||
|
<div class="document-review-container"> |
||||
|
<div class="tasks-container"> |
||||
|
<Tabs v-model:activeKey="activeCollapseKeys[0]"> |
||||
|
<TabPane |
||||
|
v-for="(category, index) in filteredTaskResultDetail" |
||||
|
:key="index.toString()" |
||||
|
:tab="category.name + ' (' + category.results.length + ')'" |
||||
|
> |
||||
|
<div class="category-items"> |
||||
|
<div class="items-card-list"> |
||||
|
<Card |
||||
|
v-for="(item, idx) in category.results" |
||||
|
:key="getItemKey(index, idx, item)" |
||||
|
class="item-card" |
||||
|
:bordered="true" |
||||
|
:bodyStyle="{ padding: activeItemKeys.includes(getItemKey(index, idx, item)) ? '16px' : '0', height: activeItemKeys.includes(getItemKey(index, idx, item)) ? 'auto' : '0', overflow: 'hidden', transition: 'all 0.3s' }" |
||||
|
> |
||||
|
<template #title> |
||||
|
<div class="item-header-content" @click="toggleItemExpand(index, idx, item)"> |
||||
|
<div class="item-info"> |
||||
|
<span class="item-serial">{{ item.serialNumber }}</span> |
||||
|
<span class="item-title">{{ item.existingIssues }}</span> |
||||
|
</div> |
||||
|
<div class="item-actions"> |
||||
|
<Switch |
||||
|
:checked="item.isRead === '1'" |
||||
|
checked-children="已读" |
||||
|
un-checked-children="未读" |
||||
|
:loading="loading && currentOpId === item.id && currentOpField === 'isRead'" |
||||
|
@change="(checked: any) => handleStatusChange(item.id!, 'isRead', checked ? '1' : '0')" |
||||
|
@click.stop |
||||
|
/> |
||||
|
<Switch |
||||
|
class="ml-3" |
||||
|
:checked="item.isAdopted === '1'" |
||||
|
checked-children="已采纳" |
||||
|
un-checked-children="未采纳" |
||||
|
:loading="loading && currentOpId === item.id && currentOpField === 'isAdopted'" |
||||
|
@change="(checked: any) => handleStatusChange(item.id!, 'isAdopted', checked ? '1' : '0')" |
||||
|
@click.stop |
||||
|
/> |
||||
|
<DownOutlined v-if="!activeItemKeys.includes(getItemKey(index, idx, item))" class="expand-icon ml-3" /> |
||||
|
<UpOutlined v-else class="expand-icon ml-3" /> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
<div class="item-content" v-if="activeItemKeys.includes(getItemKey(index, idx, item))"> |
||||
|
<!-- 基于配置动态渲染内容区域 --> |
||||
|
<template v-for="(section, sectionIndex) in getContentSections(receivedTaskType, item)" :key="sectionIndex"> |
||||
|
<div v-if="getItemValue(item, section.field)" class="content-section"> |
||||
|
<div class="section-title"> |
||||
|
{{ section.title }}: |
||||
|
<Button |
||||
|
type="link" |
||||
|
size="small" |
||||
|
class="copy-btn" |
||||
|
@click.stop="copyContent(getItemValue(item, section.field))" |
||||
|
v-if="getItemValue(item, section.field)" |
||||
|
> |
||||
|
<CopyOutlined /> 复制 |
||||
|
</Button> |
||||
|
</div> |
||||
|
<div class="section-content markdown-content" v-html="renderContent(section, getItemValue(item, section.field))"></div> |
||||
|
<!-- 特殊处理审查要点列表 --> |
||||
|
<div v-if="section.type === 'reviewPointsList' && section.field === 'reviewBasis' && item.reviewBasis && item.reviewBasis.reviewPoints && item.reviewBasis.reviewPoints.length"> |
||||
|
<Divider style="margin: 8px 0" /> |
||||
|
<ul class="review-points"> |
||||
|
<li v-for="(point, pointIdx) in item.reviewBasis.reviewPoints" :key="pointIdx"> |
||||
|
{{ point }} |
||||
|
</li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
</div> |
||||
|
</Card> |
||||
|
</div> |
||||
|
</div> |
||||
|
</TabPane> |
||||
|
</Tabs> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
<template #footer> |
||||
|
<div class="drawer-footer"> |
||||
|
<div class="status-switches"> |
||||
|
<Switch |
||||
|
:checked="expandReadItems" |
||||
|
checked-children="展开已读" |
||||
|
un-checked-children="折叠已读" |
||||
|
@change="(checked: any) => toggleExpandReadItems(checked)" |
||||
|
/> |
||||
|
<Switch |
||||
|
class="ml-3" |
||||
|
:checked="expandAdoptedItems" |
||||
|
checked-children="展开已采纳" |
||||
|
un-checked-children="折叠已采纳" |
||||
|
@change="(checked: any) => toggleExpandAdoptedItems(checked)" |
||||
|
/> |
||||
|
</div> |
||||
|
<Button type="primary" @click="handleClose">关闭</Button> |
||||
|
</div> |
||||
|
</template> |
||||
|
</BasicDrawer> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts" setup> |
||||
|
import { ref, watch, onMounted, computed, nextTick } from 'vue'; |
||||
|
import { BasicDrawer, useDrawerInner } from '@/components/Drawer'; |
||||
|
import { DocumentTaskResultDetailVO } from '@/api/documentReview/DocumentTaskResults/model'; |
||||
|
import { updateResultItemStatus, DocumentTaskResultDownload } from '@/api/documentReview/DocumentTaskResults'; |
||||
|
import { message, Alert, Button, Card } from 'ant-design-vue'; |
||||
|
import { Switch, Divider, Tabs, TabPane } from 'ant-design-vue'; |
||||
|
import { DownOutlined, UpOutlined, CopyOutlined } from '@ant-design/icons-vue'; |
||||
|
import MarkdownIt from 'markdown-it'; |
||||
|
|
||||
|
const md = new MarkdownIt({ |
||||
|
html: true, |
||||
|
linkify: true, |
||||
|
typographer: true, |
||||
|
breaks: true, |
||||
|
}); |
||||
|
|
||||
|
const activeCollapseKeys = ref<string[]>(['0']); |
||||
|
const activeItemKeys = ref<string[]>([]); |
||||
|
const taskResultDetail = ref<DocumentTaskResultDetailVO[]>([]); |
||||
|
const taskInfo = ref<Recordable>({}); |
||||
|
const loading = ref<boolean>(false); |
||||
|
const currentOpId = ref<string>(''); |
||||
|
const currentOpField = ref<string>(''); |
||||
|
const expandReadItems = ref<boolean>(false); |
||||
|
const expandAdoptedItems = ref<boolean>(false); |
||||
|
const receivedTaskType = ref<string>(''); |
||||
|
|
||||
|
// 内容区域配置,定义不同类型任务显示哪些内容 |
||||
|
interface ContentSectionConfig { |
||||
|
field: string; |
||||
|
title: string; |
||||
|
type?: 'markdown' | 'text' | 'reviewPointsList'; |
||||
|
nestedFields?: string[]; |
||||
|
} |
||||
|
|
||||
|
// 全局默认配置,所有类型都默认显示这些字段 |
||||
|
const defaultContentSections: ContentSectionConfig[] = [ |
||||
|
{ field: 'originalText', title: '原文', type: 'markdown' }, |
||||
|
{ field: 'comparedText', title: '比对原文', type: 'markdown' }, |
||||
|
{ field: 'modifiedContent', title: '修改后内容', type: 'markdown' }, |
||||
|
{ field: 'modificationDisplay', title: '修改情况', type: 'markdown' }, |
||||
|
{ field: 'existingIssues', title: '存在问题', type: 'markdown' }, |
||||
|
{ |
||||
|
field: 'reviewBasis', |
||||
|
title: '审查依据', |
||||
|
type: 'reviewPointsList', |
||||
|
nestedFields: ['reviewContent', 'reviewPoints'] |
||||
|
} |
||||
|
]; |
||||
|
|
||||
|
// 不同类型任务的特定配置,可以覆盖或扩展默认配置 |
||||
|
const taskTypeContentConfig: Record<string, ContentSectionConfig[]> = { |
||||
|
// 示例:合同审核特定配置 |
||||
|
'contract': [ |
||||
|
{ field: 'originalText', title: '合同原文', type: 'markdown' }, |
||||
|
{ field: 'legalIssues', title: '法律问题', type: 'markdown' }, |
||||
|
{ field: 'suggestions', title: '修改建议', type: 'markdown' }, |
||||
|
{ |
||||
|
field: 'reviewBasis', |
||||
|
title: '法律依据', |
||||
|
type: 'reviewPointsList', |
||||
|
nestedFields: ['reviewContent', 'reviewPoints'] |
||||
|
} |
||||
|
], |
||||
|
"checkCompanyName": [ |
||||
|
{ field: 'modificationDisplay', title: '相关原文', type: 'markdown' }, |
||||
|
], |
||||
|
"checkTitleName": [ |
||||
|
{ field: 'modificationDisplay', title: '相关原文', type: 'markdown' }, |
||||
|
], |
||||
|
"checkPlaceName": [ |
||||
|
{ field: 'modificationDisplay', title: '相关原文', type: 'markdown' }, |
||||
|
], |
||||
|
"checkRepeatText": [ |
||||
|
{ field: 'originalText', title: '第一段原文', type: 'markdown' }, |
||||
|
{ field: 'comparedText', title: '第二段原文', type: 'markdown' }, |
||||
|
{ field: 'modificationDisplay', title: '相似情况', type: 'markdown' }, |
||||
|
], |
||||
|
"checkDocumentError": [ |
||||
|
{ field: 'originalText', title: '原文', type: 'markdown' }, |
||||
|
{ field: 'modifiedContent', title: '修改建议', type: 'markdown' }, |
||||
|
{ field: 'modificationDisplay', title: '修改情况', type: 'markdown' }, |
||||
|
], |
||||
|
// 可以添加更多任务类型的配置,如checkDocumentError、schemEvaluation等 |
||||
|
}; |
||||
|
|
||||
|
// 根据任务类型获取内容配置 |
||||
|
const getContentSections = (taskType: string | undefined, item: any): ContentSectionConfig[] => { |
||||
|
// 如果未提供类型或类型不存在,使用默认配置 |
||||
|
if (!taskType) return defaultContentSections; |
||||
|
// 如果存在特定类型的配置则使用,否则使用默认配置 |
||||
|
return taskTypeContentConfig[taskType] || defaultContentSections; |
||||
|
}; |
||||
|
|
||||
|
// 从项目中获取特定字段的值 |
||||
|
const getItemValue = (item: any, field: string): any => { |
||||
|
if (!item || !field) return null; |
||||
|
|
||||
|
// 支持嵌套字段 如 "reviewBasis.reviewContent" |
||||
|
if (field.includes('.')) { |
||||
|
const parts = field.split('.'); |
||||
|
let value = item; |
||||
|
for (const part of parts) { |
||||
|
if (value && typeof value === 'object') { |
||||
|
value = value[part]; |
||||
|
} else { |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
return value; |
||||
|
} |
||||
|
|
||||
|
return item[field]; |
||||
|
}; |
||||
|
|
||||
|
// 根据配置渲染内容 |
||||
|
const renderContent = (section: ContentSectionConfig, value: any): string => { |
||||
|
if (!value) return ''; |
||||
|
|
||||
|
if (section.type === 'markdown') { |
||||
|
return renderMarkdown(value); |
||||
|
} |
||||
|
|
||||
|
if (section.type === 'reviewPointsList' && section.nestedFields) { |
||||
|
let content = ''; |
||||
|
|
||||
|
// 渲染reviewContent部分 |
||||
|
if (section.nestedFields.includes('reviewContent') && value.reviewContent) { |
||||
|
content += renderMarkdown(value.reviewContent); |
||||
|
} |
||||
|
|
||||
|
return content; |
||||
|
} |
||||
|
|
||||
|
return value.toString().replace(/\n/g, '<br>'); |
||||
|
}; |
||||
|
|
||||
|
// 根据条件过滤展示的结果 |
||||
|
const filteredTaskResultDetail = computed(() => { |
||||
|
if (taskResultDetail.value.length <= 0) { |
||||
|
return []; |
||||
|
} |
||||
|
|
||||
|
// 如果只有两个或更少的类别,或者第一个类别的results数量<=5个,只展示第一个类别 |
||||
|
if (taskResultDetail.value.length <= 2 || |
||||
|
(taskResultDetail.value[0] && taskResultDetail.value[0].results && |
||||
|
taskResultDetail.value[0].results.length <= 5)) { |
||||
|
return [taskResultDetail.value[0]]; |
||||
|
} |
||||
|
|
||||
|
return taskResultDetail.value; |
||||
|
}); |
||||
|
|
||||
|
// 生成审核项的唯一key |
||||
|
const getItemKey = (categoryIndex: number, itemIndex: number, item: any) => { |
||||
|
return item.id ? item.id : `${categoryIndex}-${itemIndex}`; |
||||
|
}; |
||||
|
|
||||
|
// 根据过滤条件更新折叠状态 |
||||
|
const updateActiveItemKeys = () => { |
||||
|
const newActiveKeys: string[] = []; |
||||
|
|
||||
|
filteredTaskResultDetail.value.forEach((category, categoryIndex) => { |
||||
|
category.results.forEach((item, itemIndex) => { |
||||
|
const itemKey = getItemKey(categoryIndex, itemIndex, item); |
||||
|
const isRead = item.isRead === '1'; |
||||
|
const isAdopted = item.isAdopted === '1'; |
||||
|
|
||||
|
// 如果项目未读且未采纳,或符合展开条件,则展开 |
||||
|
if ((!isRead && !isAdopted) || |
||||
|
(isRead && expandReadItems.value) || |
||||
|
(isAdopted && expandAdoptedItems.value)) { |
||||
|
newActiveKeys.push(itemKey); |
||||
|
} |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
activeItemKeys.value = newActiveKeys; |
||||
|
}; |
||||
|
|
||||
|
// 切换展开已读项 |
||||
|
const toggleExpandReadItems = (checked: any) => { |
||||
|
expandReadItems.value = !!checked; |
||||
|
updateActiveItemKeys(); |
||||
|
}; |
||||
|
|
||||
|
// 切换展开已采纳项 |
||||
|
const toggleExpandAdoptedItems = (checked: any) => { |
||||
|
expandAdoptedItems.value = !!checked; |
||||
|
updateActiveItemKeys(); |
||||
|
}; |
||||
|
|
||||
|
// 切换项目展开/折叠 |
||||
|
const toggleItemExpand = (categoryIndex: number, itemIndex: number, item: any) => { |
||||
|
const itemKey = getItemKey(categoryIndex, itemIndex, item); |
||||
|
const index = activeItemKeys.value.indexOf(itemKey); |
||||
|
|
||||
|
if (index > -1) { |
||||
|
activeItemKeys.value.splice(index, 1); |
||||
|
} else { |
||||
|
activeItemKeys.value.push(itemKey); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
const renderMarkdown = (text) => { |
||||
|
if (!text) return ''; |
||||
|
|
||||
|
// 确保在Markdown处理前将普通换行符转换为Markdown换行(添加两个空格后的换行) |
||||
|
// 这样即使不是段落分隔的换行也能正确显示 |
||||
|
const processedText = text.replace(/\n/g, ' \n'); |
||||
|
return md.render(processedText); |
||||
|
}; |
||||
|
|
||||
|
const [registerDrawer, { closeDrawer, setDrawerProps }] = useDrawerInner(async (data) => { |
||||
|
setDrawerProps({ loading: true }); |
||||
|
try { |
||||
|
if (data?.taskResultDetail) { |
||||
|
// 存储接收到的任务类型 |
||||
|
receivedTaskType.value = data.taskInfo.taskName || ''; |
||||
|
console.log('receivedTaskType', receivedTaskType.value); |
||||
|
// 不再修改每个category的type属性 |
||||
|
taskResultDetail.value = data.taskResultDetail; |
||||
|
taskInfo.value = data.taskInfo || {}; |
||||
|
|
||||
|
// 默认选中第一个分类 |
||||
|
if (taskResultDetail.value.length > 0) { |
||||
|
activeCollapseKeys.value = ['0']; |
||||
|
} |
||||
|
|
||||
|
// 初始化项目折叠状态 |
||||
|
updateActiveItemKeys(); |
||||
|
} |
||||
|
} catch (error) { |
||||
|
console.error('初始化详情抽屉时出错', error); |
||||
|
} finally { |
||||
|
setDrawerProps({ loading: false }); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
async function handleStatusChange(id: string, field: 'isRead' | 'isAdopted', value: '0' | '1') { |
||||
|
if (!id) { |
||||
|
message.error('缺少记录ID,无法更新状态'); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
loading.value = true; |
||||
|
currentOpId.value = id; |
||||
|
currentOpField.value = field; |
||||
|
|
||||
|
try { |
||||
|
await updateResultItemStatus(id, field, value); |
||||
|
// message.success(`状态更新成功`); |
||||
|
|
||||
|
// 更新本地状态 |
||||
|
taskResultDetail.value.forEach(category => { |
||||
|
category.results.forEach(item => { |
||||
|
if (item.id === id) { |
||||
|
item[field] = value; |
||||
|
} |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
// 更新折叠状态 |
||||
|
updateActiveItemKeys(); |
||||
|
} catch (error) { |
||||
|
console.error('更新状态失败', error); |
||||
|
message.error('更新状态失败'); |
||||
|
} finally { |
||||
|
loading.value = false; |
||||
|
currentOpId.value = ''; |
||||
|
currentOpField.value = ''; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function handleClose() { |
||||
|
closeDrawer(); |
||||
|
} |
||||
|
|
||||
|
// 监听状态变化,更新折叠状态 |
||||
|
watch([expandReadItems, expandAdoptedItems], () => { |
||||
|
updateActiveItemKeys(); |
||||
|
}); |
||||
|
|
||||
|
// 复制内容到剪贴板 |
||||
|
const copyContent = (content: any) => { |
||||
|
if (!content) return; |
||||
|
|
||||
|
// 如果是对象,可能需要特殊处理 |
||||
|
let textToCopy = ''; |
||||
|
|
||||
|
if (typeof content === 'object') { |
||||
|
// 例如 reviewBasis 对象的特殊处理 |
||||
|
if (content.reviewContent) { |
||||
|
textToCopy = content.reviewContent; |
||||
|
|
||||
|
// 如果有审查要点,也添加到复制内容中 |
||||
|
if (content.reviewPoints && content.reviewPoints.length) { |
||||
|
textToCopy += '\n\n审查要点:\n'; |
||||
|
content.reviewPoints.forEach((point, index) => { |
||||
|
textToCopy += `${index + 1}. ${point}\n`; |
||||
|
}); |
||||
|
} |
||||
|
} else { |
||||
|
// 其他对象类型转为JSON字符串 |
||||
|
try { |
||||
|
textToCopy = JSON.stringify(content, null, 2); |
||||
|
} catch (e) { |
||||
|
textToCopy = String(content); |
||||
|
} |
||||
|
} |
||||
|
} else { |
||||
|
// 字符串或其他类型直接转换 |
||||
|
textToCopy = String(content); |
||||
|
} |
||||
|
|
||||
|
// 复制到剪贴板 |
||||
|
navigator.clipboard.writeText(textToCopy) |
||||
|
.then(() => { |
||||
|
message.success('内容已复制到剪贴板'); |
||||
|
}) |
||||
|
.catch((err) => { |
||||
|
console.error('复制失败:', err); |
||||
|
message.error('复制失败,请手动选择并复制'); |
||||
|
|
||||
|
// 备用复制方法(兼容性更好但不太推荐) |
||||
|
const textarea = document.createElement('textarea'); |
||||
|
textarea.value = textToCopy; |
||||
|
document.body.appendChild(textarea); |
||||
|
textarea.select(); |
||||
|
try { |
||||
|
document.execCommand('copy'); |
||||
|
message.success('内容已复制到剪贴板'); |
||||
|
} catch (e) { |
||||
|
console.error('备用复制方法失败:', e); |
||||
|
message.error('复制失败,请手动选择并复制'); |
||||
|
} |
||||
|
document.body.removeChild(textarea); |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
onMounted(() => { |
||||
|
// 初始化后执行的代码 |
||||
|
}); |
||||
|
</script> |
||||
|
|
||||
|
<style lang="less" scoped> |
||||
|
:deep(.ant-drawer-body) { |
||||
|
padding: 16px; |
||||
|
height: calc(100% - 100px); |
||||
|
overflow: auto; |
||||
|
} |
||||
|
|
||||
|
.document-review-container { |
||||
|
height: 100%; |
||||
|
} |
||||
|
|
||||
|
.tasks-container { |
||||
|
height: 100%; |
||||
|
overflow: auto; |
||||
|
} |
||||
|
|
||||
|
:deep(.ant-tabs-nav) { |
||||
|
margin-bottom: 16px; |
||||
|
} |
||||
|
|
||||
|
:deep(.ant-tabs-tab) { |
||||
|
padding: 8px 16px; |
||||
|
font-weight: 500; |
||||
|
} |
||||
|
|
||||
|
.items-card-list { |
||||
|
:deep(.ant-card) { |
||||
|
margin-bottom: 12px; |
||||
|
border: 1px solid #e8e8e8; |
||||
|
border-radius: 4px !important; |
||||
|
overflow: hidden; |
||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); |
||||
|
background-color: #fff; |
||||
|
} |
||||
|
|
||||
|
:deep(.ant-card-head) { |
||||
|
padding: 0 !important; |
||||
|
background-color: #f8f8f8; |
||||
|
min-height: 40px; |
||||
|
|
||||
|
.ant-card-head-title { |
||||
|
padding: 8px 16px !important; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
:deep(.ant-card-body) { |
||||
|
transition: all 0.3s ease; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.item-card { |
||||
|
width: 100%; |
||||
|
margin-bottom: 10px; |
||||
|
cursor: pointer; |
||||
|
} |
||||
|
|
||||
|
.expand-icon { |
||||
|
font-size: 12px; |
||||
|
color: rgba(0, 0, 0, 0.45); |
||||
|
} |
||||
|
|
||||
|
.item-header-content { |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
width: 100%; |
||||
|
} |
||||
|
|
||||
|
.item-info { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
flex: 1; |
||||
|
overflow: hidden; |
||||
|
text-overflow: ellipsis; |
||||
|
white-space: nowrap; |
||||
|
} |
||||
|
|
||||
|
.item-serial { |
||||
|
margin-right: 12px; |
||||
|
font-weight: bold; |
||||
|
color: #1890ff; |
||||
|
} |
||||
|
|
||||
|
.item-title { |
||||
|
font-weight: 500; |
||||
|
font-size: 14px; |
||||
|
flex: 1; |
||||
|
overflow: hidden; |
||||
|
text-overflow: ellipsis; |
||||
|
white-space: nowrap; |
||||
|
} |
||||
|
|
||||
|
.item-actions { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
margin-left: 16px; |
||||
|
flex-shrink: 0; |
||||
|
} |
||||
|
|
||||
|
.item-content { |
||||
|
padding: 16px; |
||||
|
} |
||||
|
|
||||
|
.content-section { |
||||
|
margin-bottom: 16px; |
||||
|
} |
||||
|
|
||||
|
.section-title { |
||||
|
font-weight: 500; |
||||
|
margin-bottom: 8px; |
||||
|
color: #333; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: space-between; |
||||
|
} |
||||
|
|
||||
|
.copy-btn { |
||||
|
padding: 0 4px; |
||||
|
font-size: 12px; |
||||
|
height: 24px; |
||||
|
|
||||
|
:deep(.anticon) { |
||||
|
font-size: 12px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.section-content { |
||||
|
padding: 0 0 0 24px; |
||||
|
background-color: #f9fbfd; |
||||
|
border-radius: 4px; |
||||
|
padding: 12px; |
||||
|
white-space: pre-wrap; |
||||
|
max-height: 300px; |
||||
|
overflow: auto; |
||||
|
} |
||||
|
|
||||
|
.markdown-content { |
||||
|
:deep(p) { |
||||
|
margin-bottom: 8px; |
||||
|
} |
||||
|
|
||||
|
:deep(ul), :deep(ol) { |
||||
|
padding-left: 24px; |
||||
|
} |
||||
|
|
||||
|
:deep(h1), :deep(h2), :deep(h3), :deep(h4), :deep(h5), :deep(h6) { |
||||
|
margin-top: 16px; |
||||
|
margin-bottom: 8px; |
||||
|
} |
||||
|
|
||||
|
:deep(code) { |
||||
|
background-color: #f5f5f5; |
||||
|
padding: 2px 4px; |
||||
|
border-radius: 4px; |
||||
|
} |
||||
|
|
||||
|
:deep(pre) { |
||||
|
background-color: #f5f5f5; |
||||
|
padding: 12px; |
||||
|
border-radius: 4px; |
||||
|
overflow: auto; |
||||
|
} |
||||
|
|
||||
|
:deep(blockquote) { |
||||
|
border-left: 4px solid #ddd; |
||||
|
padding-left: 16px; |
||||
|
color: #666; |
||||
|
margin: 16px 0; |
||||
|
} |
||||
|
|
||||
|
:deep(table) { |
||||
|
border-collapse: collapse; |
||||
|
width: 100%; |
||||
|
margin: 16px 0; |
||||
|
|
||||
|
th, td { |
||||
|
border: 1px solid #e8e8e8; |
||||
|
padding: 8px 12px; |
||||
|
} |
||||
|
|
||||
|
th { |
||||
|
background-color: #f8f8f8; |
||||
|
font-weight: 500; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.review-points { |
||||
|
list-style-type: disc; |
||||
|
padding-left: 24px; |
||||
|
|
||||
|
li { |
||||
|
margin-bottom: 4px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.ml-3 { |
||||
|
margin-left: 12px; |
||||
|
} |
||||
|
|
||||
|
.drawer-footer { |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
width: 100%; |
||||
|
} |
||||
|
|
||||
|
.status-switches { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,151 @@ |
|||||
|
import { BasicColumn } from '@/components/Table'; |
||||
|
import { FormSchema } from '@/components/Form'; |
||||
|
import { getDictOptions } from '@/utils/dict'; |
||||
|
import { useRender } from '@/hooks/component/useRender'; |
||||
|
import { uploadDocument } from '@/api/documentReview/DocumentTasks'; |
||||
|
import { useUserStore } from '@/store/modules/user'; |
||||
|
import { RoleEnum } from '@/enums/roleEnum'; |
||||
|
|
||||
|
const { roleList } = useUserStore(); |
||||
|
export const formSchemas: FormSchema[] = [ |
||||
|
// {
|
||||
|
// label: '模型所属行业',
|
||||
|
// field: 'taskIndustry',
|
||||
|
// component: 'Select',
|
||||
|
// componentProps: {
|
||||
|
// options: getDictOptions('model_industry')
|
||||
|
// },
|
||||
|
// },
|
||||
|
// {
|
||||
|
// label: '模型所属区域',
|
||||
|
// field: 'taskRegion',
|
||||
|
// component: 'Select',
|
||||
|
// componentProps: {
|
||||
|
// options: getDictOptions('model_region')
|
||||
|
// },
|
||||
|
// },
|
||||
|
{ |
||||
|
label: '任务名称', |
||||
|
field: 'taskNameList', |
||||
|
component: 'Select', |
||||
|
componentProps: { |
||||
|
options: getDictOptions('tender_review'), |
||||
|
mode: 'multiple', |
||||
|
defaultValue:"inconsistency" |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
label: '文档名称', |
||||
|
field: 'documentName', |
||||
|
component: 'Input', |
||||
|
}, |
||||
|
{ |
||||
|
label: '状态', |
||||
|
field: 'progressStatus', |
||||
|
component: 'Select', |
||||
|
componentProps: { |
||||
|
options: getDictOptions('document_task_status'), |
||||
|
}, |
||||
|
}, |
||||
|
]; |
||||
|
|
||||
|
const { renderDict } = useRender(); |
||||
|
export const columns: BasicColumn[] = [ |
||||
|
{ |
||||
|
title: '任务名称', |
||||
|
dataIndex: 'taskName', |
||||
|
customRender: ({ value }) => renderDict(value, 'tender_review'), |
||||
|
|
||||
|
}, |
||||
|
{ |
||||
|
title: '文档名称', |
||||
|
dataIndex: 'documentName', |
||||
|
}, |
||||
|
{ |
||||
|
title: '模型所属区域', |
||||
|
dataIndex: 'taskRegion', |
||||
|
customRender: ({ value }) => renderDict(value, 'model_region'), |
||||
|
}, |
||||
|
{ |
||||
|
title: '模型所属行业', |
||||
|
dataIndex: 'taskIndustry', |
||||
|
customRender: ({ value }) => renderDict(value, 'model_industry'), |
||||
|
}, |
||||
|
{ |
||||
|
title: '上传时间', |
||||
|
dataIndex: 'createTime', |
||||
|
}, |
||||
|
{ |
||||
|
title: '提交人', |
||||
|
dataIndex: 'createUser', |
||||
|
auth: 'documentReview:DocumentTasks:tableShow', |
||||
|
}, |
||||
|
{ |
||||
|
title: '任务耗时', |
||||
|
dataIndex: 'taskDuration', |
||||
|
}, |
||||
|
{ |
||||
|
title: '状态', |
||||
|
dataIndex: 'progressStatus', |
||||
|
customRender: ({ value }) => renderDict(value, 'document_task_status'), |
||||
|
}, |
||||
|
]; |
||||
|
|
||||
|
export const modalSchemas: FormSchema[] = [ |
||||
|
{ |
||||
|
label: '模型所属区域', |
||||
|
field: 'taskRegion', |
||||
|
required: true, |
||||
|
component: 'Select', |
||||
|
// componentProps: {
|
||||
|
// options: taskRegionPermission(),
|
||||
|
// defaultValue:"normal",
|
||||
|
// },
|
||||
|
componentProps: () => { |
||||
|
const isSuperAdmin = roleList.includes(RoleEnum.SUPER_ADMIN); |
||||
|
let options = getDictOptions('model_region'); |
||||
|
if (!isSuperAdmin) { |
||||
|
// 如果不是超级管理员,移除 label 带有 '#' 的项
|
||||
|
options = options.filter((option) => !option.label.includes('#')); |
||||
|
} |
||||
|
return { |
||||
|
options: options, |
||||
|
defaultValue: 'normal', |
||||
|
}; |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
label: '模型所属行业', |
||||
|
field: 'taskIndustry', |
||||
|
required: true, |
||||
|
component: 'Select', |
||||
|
componentProps: { |
||||
|
options: getDictOptions('model_industry'), |
||||
|
defaultValue: 'normal', |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
label: '任务名称', |
||||
|
field: 'taskNameList', |
||||
|
required: true, |
||||
|
component: 'Select', |
||||
|
componentProps: { |
||||
|
options: getDictOptions('tender_review') , |
||||
|
mode: 'multiple', |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
label: '文档名称', |
||||
|
field: 'ossId', |
||||
|
required: true, |
||||
|
component: 'Upload', |
||||
|
componentProps: { |
||||
|
accept: ['.docx', '.doc', '.wps'], |
||||
|
maxSize: 500, |
||||
|
multiple: false, |
||||
|
resultField: 'ossId', |
||||
|
api: uploadDocument, |
||||
|
beforeUploadPrompt:"严禁在本互联网非涉密平台处理、传输国家秘密。请再次确认您上传的文件资料不涉及国家秘密。" |
||||
|
}, |
||||
|
}, |
||||
|
]; |
@ -0,0 +1,72 @@ |
|||||
|
<template> |
||||
|
<BasicModal |
||||
|
v-bind="$attrs" |
||||
|
:title="title" |
||||
|
@register="registerInnerModal" |
||||
|
@ok="handleSubmit" |
||||
|
@cancel="resetForm" |
||||
|
> |
||||
|
<BasicForm @register="registerForm" /> |
||||
|
|
||||
|
</BasicModal> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
import { BasicModal, useModalInner } from '@/components/Modal'; |
||||
|
import { BasicForm, useForm } from '@/components/Form'; |
||||
|
import { computed, ref, unref } from 'vue'; |
||||
|
import { TenderTaskInfo, TenderTaskAdd, TenderTaskUpdate } from '@/api/tenderReview/TenderTask'; |
||||
|
import { modalSchemas } from './TenderTask.data'; |
||||
|
import { ModelUserPromptssettingInfoByUserId } from '@/api/modelConfiguration/ModelUserPromptssetting/index'; |
||||
|
|
||||
|
defineOptions({ name: 'TenderTaskModal' }); |
||||
|
|
||||
|
const emit = defineEmits(['register', 'reload']); |
||||
|
|
||||
|
const isUpdate = ref<boolean>(false); |
||||
|
const title = computed<string>(() => { |
||||
|
return isUpdate.value ? '编辑招标审核任务' : '新增招标审核任务'; |
||||
|
}); |
||||
|
|
||||
|
const [registerInnerModal, { modalLoading, closeModal }] = useModalInner( |
||||
|
async (data: { record?: Recordable; update: boolean }) => { |
||||
|
modalLoading(true); |
||||
|
const settings = await ModelUserPromptssettingInfoByUserId(); |
||||
|
await setFieldsValue(settings); |
||||
|
const { record, update } = data; |
||||
|
isUpdate.value = update; |
||||
|
if (update && record) { |
||||
|
await setFieldsValue(record); |
||||
|
} |
||||
|
modalLoading(false); |
||||
|
}, |
||||
|
); |
||||
|
|
||||
|
const [registerForm, { setFieldsValue, resetForm, validate }] = useForm({ |
||||
|
labelWidth: 100, |
||||
|
showActionButtonGroup: false, |
||||
|
baseColProps: { span: 24 }, |
||||
|
schemas: modalSchemas, |
||||
|
}); |
||||
|
|
||||
|
async function handleSubmit() { |
||||
|
try { |
||||
|
modalLoading(true); |
||||
|
const data = await validate(); |
||||
|
data['ossId'] = data['ossId'][0]; |
||||
|
if (unref(isUpdate)) { |
||||
|
await TenderTaskUpdate(data); |
||||
|
} else { |
||||
|
await TenderTaskAdd(data); |
||||
|
} |
||||
|
emit('reload'); |
||||
|
closeModal(); |
||||
|
await resetForm(); |
||||
|
} catch (e) { |
||||
|
} finally { |
||||
|
modalLoading(false); |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped></style> |
@ -0,0 +1,181 @@ |
|||||
|
<template> |
||||
|
<PageWrapper dense> |
||||
|
<BasicTable @register="registerTable"> |
||||
|
<template #toolbar> |
||||
|
<a-button |
||||
|
@click=" |
||||
|
downloadExcel(TenderTaskExport, '招标摘要任务数据', getForm().getFieldsValue()) |
||||
|
" |
||||
|
v-auth="'productManagement:TenderTask:export'" |
||||
|
>导出</a-button |
||||
|
> |
||||
|
<a-button |
||||
|
type="primary" |
||||
|
danger |
||||
|
@click="multipleRemove(TenderTaskRemove)" |
||||
|
:disabled="!selected" |
||||
|
v-auth="'productManagement:TenderTask:remove'" |
||||
|
>删除</a-button |
||||
|
> |
||||
|
<a-button |
||||
|
type="primary" |
||||
|
@click="handleAdd" |
||||
|
v-auth="'productManagement:TenderTask:add'" |
||||
|
>新增</a-button |
||||
|
> |
||||
|
</template> |
||||
|
<template #bodyCell="{ column, record }"> |
||||
|
<template v-if="column.key === 'action'"> |
||||
|
<TableAction |
||||
|
stopButtonPropagation |
||||
|
:actions="[ |
||||
|
{ |
||||
|
label: '详情', |
||||
|
icon: IconEnum.EDIT, |
||||
|
type: 'primary', |
||||
|
ghost: true, |
||||
|
ifShow: () => { |
||||
|
if ( |
||||
|
record.progressStatus != 'PENDING' && |
||||
|
record.progressStatus != 'STARTED' && |
||||
|
record.progressStatus != 'REVOKED' |
||||
|
) { |
||||
|
return true; |
||||
|
} else { |
||||
|
return false; |
||||
|
} |
||||
|
}, |
||||
|
onClick: handleDetail.bind(null, record), |
||||
|
}, |
||||
|
{ |
||||
|
label: '下载', |
||||
|
icon: IconEnum.DOWNLOAD, |
||||
|
type: 'primary', |
||||
|
color: 'success', |
||||
|
ghost: true, |
||||
|
ifShow: () => { |
||||
|
if ( |
||||
|
record.progressStatus != 'PENDING' && |
||||
|
record.progressStatus != 'STARTED' && |
||||
|
record.progressStatus != 'REVOKED' |
||||
|
) { |
||||
|
return true; |
||||
|
} else { |
||||
|
return false; |
||||
|
} |
||||
|
}, |
||||
|
onClick: handleDownload.bind(null, record), |
||||
|
}, |
||||
|
{ |
||||
|
label: '终止任务', |
||||
|
icon: IconEnum.DELETE, |
||||
|
type: 'primary', |
||||
|
danger: true, |
||||
|
ghost: true, |
||||
|
ifShow: () => { |
||||
|
if (record.progressStatus == 'PENDING') { |
||||
|
return true; |
||||
|
} else { |
||||
|
return false; |
||||
|
} |
||||
|
}, |
||||
|
popConfirm: { |
||||
|
placement: 'left', |
||||
|
title: '是否终止当前任务?', |
||||
|
confirm: handleStop.bind(null, record), |
||||
|
}, |
||||
|
}, |
||||
|
]" |
||||
|
/> |
||||
|
</template> |
||||
|
</template> |
||||
|
</BasicTable> |
||||
|
<TenderTaskModal @register="registerModal" @reload="reload" /> |
||||
|
<DocsDrawer @register="registerDrawer" /> |
||||
|
</PageWrapper> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
import { PageWrapper } from '@/components/Page'; |
||||
|
import { BasicTable, useTable, TableAction } from '@/components/Table'; |
||||
|
import { |
||||
|
TenderTaskList, |
||||
|
TenderTaskExport, |
||||
|
TenderTaskRemove, |
||||
|
} from '@/api/tenderReview/TenderTask'; |
||||
|
import { downloadExcel } from '@/utils/file/download'; |
||||
|
import { useModal } from '@/components/Modal'; |
||||
|
import TenderTaskModal from './TenderTaskModal.vue'; |
||||
|
import { formSchemas, columns } from './TenderTask.data'; |
||||
|
import { IconEnum } from '@/enums/appEnum'; |
||||
|
import DocsDrawer from '@/views/documentReview/DocumentTasks/DocsDrawer.vue'; |
||||
|
import { useDrawer } from '@/components/Drawer'; |
||||
|
import { DocumentTasksStop } from '@/api/documentReview/DocumentTasks'; |
||||
|
import { |
||||
|
DocumentTaskResultsInfoByTaskId, |
||||
|
DocumentTaskResultDownload, |
||||
|
} from '@/api/documentReview/DocumentTaskResults'; |
||||
|
const [registerDrawer, { openDrawer }] = useDrawer(); |
||||
|
|
||||
|
defineOptions({ name: 'TenderTask' }); |
||||
|
|
||||
|
const [registerTable, { reload, multipleRemove, selected, getForm }] = useTable({ |
||||
|
rowSelection: { |
||||
|
type: 'checkbox', |
||||
|
}, |
||||
|
title: '招标审核任务列表', |
||||
|
api: TenderTaskList, |
||||
|
showIndexColumn: false, |
||||
|
rowKey: 'id', |
||||
|
useSearchForm: true, |
||||
|
formConfig: { |
||||
|
schemas: formSchemas, |
||||
|
baseColProps: { |
||||
|
xs: 24, |
||||
|
sm: 24, |
||||
|
md: 24, |
||||
|
lg: 6, |
||||
|
}, |
||||
|
}, |
||||
|
columns: columns, |
||||
|
actionColumn: { |
||||
|
width: 200, |
||||
|
title: '操作', |
||||
|
key: 'action', |
||||
|
fixed: 'right', |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
const [registerModal, { openModal }] = useModal(); |
||||
|
|
||||
|
async function handleDetail(record: Recordable) { |
||||
|
try { |
||||
|
let res = await DocumentTaskResultsInfoByTaskId(record.id); |
||||
|
|
||||
|
openDrawer(true, { value: res.result, type: 'markdown' }); |
||||
|
console.log('res', res); |
||||
|
} catch (ex) { |
||||
|
openDrawer(true, { value: '加载失败,请刷新页面', type: 'markdown' }); |
||||
|
} |
||||
|
//根据record.id查询结果详情 |
||||
|
} |
||||
|
|
||||
|
async function handleStop(record: Recordable) { |
||||
|
await DocumentTasksStop(record.id); |
||||
|
await reload(); |
||||
|
} |
||||
|
|
||||
|
function handleAdd() { |
||||
|
openModal(true, { update: false }); |
||||
|
} |
||||
|
async function handleDownload(record: Recordable) { |
||||
|
await DocumentTaskResultDownload([record.id]); |
||||
|
await reload(); |
||||
|
} |
||||
|
async function handleDelete(record: Recordable) { |
||||
|
await TenderTaskRemove([record.id]); |
||||
|
await reload(); |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped></style> |
Loading…
Reference in new issue