You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1380 lines
46 KiB
1380 lines
46 KiB
<template>
|
|
<Drawer
|
|
v-bind="$attrs"
|
|
:visible="visible"
|
|
@close="handleClose"
|
|
:title="config?.name || '审核结果'"
|
|
:width="width"
|
|
:closable="true"
|
|
:maskClosable="true"
|
|
:destroyOnClose="true"
|
|
>
|
|
<div class="universal-review-container">
|
|
<div class="split-layout">
|
|
<!-- 左侧PDF预览 -->
|
|
<div class="pdf-preview-wrapper" v-if="config?.pdfConfig && pdfLayoutConfig">
|
|
<ReviewPdfContainer
|
|
ref="pdfContainerRef"
|
|
:config="pdfLayoutConfig"
|
|
:taskInfo="taskInfo"
|
|
:getPdfStream="getPdfStream"
|
|
@pdfLoaded="handlePdfLoaded"
|
|
@pdfError="handlePdfError"
|
|
/>
|
|
</div>
|
|
|
|
<!-- 右侧审核结果 -->
|
|
<div class="content-container">
|
|
<!-- 多标签模式 -->
|
|
<div v-if="config?.mode === 'tabs'" class="tabs-container">
|
|
<div class="review-type-tabs" v-if="availableTabs.length > 0">
|
|
<Tabs v-model:activeKey="currentTab" @change="handleTabChange">
|
|
<TabPane
|
|
v-for="tab in availableTabs"
|
|
:key="tab.key"
|
|
:tab="tab.label"
|
|
/>
|
|
</Tabs>
|
|
</div>
|
|
<div class="tab-content">
|
|
<div class="category-items" v-if="filteredResults.length > 0">
|
|
<div class="items-card-list">
|
|
<template v-for="(category, categoryIndex) in filteredResults" :key="categoryIndex">
|
|
<Card
|
|
v-for="(item, idx) in category.results"
|
|
:key="getItemKey(categoryIndex, idx, item)"
|
|
class="item-card"
|
|
:bordered="true"
|
|
:bodyStyle="{ padding: activeItemKeys.includes(getItemKey(categoryIndex, idx, item)) ? '16px' : '0', height: activeItemKeys.includes(getItemKey(categoryIndex, idx, item)) ? 'auto' : '0', overflow: 'hidden', transition: 'all 0.3s' }"
|
|
>
|
|
<template #title>
|
|
<div class="item-header-content" @click="toggleItemExpand(categoryIndex, 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(categoryIndex, 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(categoryIndex, idx, item))">
|
|
<template v-for="(section, sectionIndex) in getContentSections(item)" :key="sectionIndex">
|
|
<div v-if="getItemValue(item, section.field)" class="content-section">
|
|
<div class="section-title">
|
|
{{ section.title }}:
|
|
<div class="title-actions">
|
|
<Button
|
|
v-if="shouldShowComparison(section, item) && shouldShowComparisonButton(section)"
|
|
type="link"
|
|
size="small"
|
|
class="comparison-btn"
|
|
@click.stop="toggleComparison(getItemId(item, categoryIndex, idx))"
|
|
>
|
|
<EyeOutlined v-if="!comparisonVisible[getItemId(item, categoryIndex, idx)]" />
|
|
<EyeInvisibleOutlined v-else />
|
|
{{ comparisonVisible[getItemId(item, categoryIndex, idx)] ? '隐藏对比' : '显示对比' }}
|
|
</Button>
|
|
<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>
|
|
<div
|
|
class="section-content markdown-content"
|
|
v-html="renderContent(section, getItemValue(item, section.field))"
|
|
@click="locateByText(getItemValue(item, section.field), item, section, categoryIndex, idx)"
|
|
></div>
|
|
|
|
<!-- 页面按钮 - 只在有pdfSource且存在多页时显示 -->
|
|
<div
|
|
v-if="supportsPdfLocation(section) && fieldPageButtons[getFieldKey(item, section, categoryIndex, idx)]"
|
|
class="page-buttons"
|
|
>
|
|
<span class="page-buttons-label">定位到页面:</span>
|
|
<Button
|
|
v-for="page in fieldPageButtons[getFieldKey(item, section, categoryIndex, idx)]"
|
|
:key="page"
|
|
type="link"
|
|
size="small"
|
|
class="page-btn"
|
|
@click.stop="goToPageByButton(page, section)"
|
|
>
|
|
第{{ page }}页
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 对比内容展示 - 只在对比模式下显示 -->
|
|
<div
|
|
v-if="shouldShowComparison(section, item) &&
|
|
hasComparisonContent(item, section) &&
|
|
comparisonVisible[getItemId(item, categoryIndex, idx)]"
|
|
class="content-section comparison-section"
|
|
>
|
|
<div class="section-title">
|
|
{{ getComparisonTitle(section) }}:
|
|
<Button
|
|
type="link"
|
|
size="small"
|
|
class="copy-btn"
|
|
@click.stop="copyContent(getComparisonContent(item, section))"
|
|
>
|
|
<CopyOutlined /> 复制
|
|
</Button>
|
|
</div>
|
|
<div
|
|
class="section-content markdown-content comparison-content"
|
|
v-html="renderMarkdown(getComparisonContent(item, section))"
|
|
@click="locateByText(getComparisonContent(item, section), item, section, categoryIndex, idx)"
|
|
></div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</Card>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 单一模式 -->
|
|
<div v-else class="single-content">
|
|
<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(item)" :key="sectionIndex">
|
|
<div v-if="getItemValue(item, section.field)" class="content-section">
|
|
<div class="section-title">
|
|
{{ section.title }}:
|
|
<div class="title-actions">
|
|
<Button
|
|
v-if="shouldShowComparison(section, item) && shouldShowComparisonButton(section)"
|
|
type="link"
|
|
size="small"
|
|
class="comparison-btn"
|
|
@click.stop="toggleComparison(getItemId(item, index, idx))"
|
|
>
|
|
<EyeOutlined v-if="!comparisonVisible[getItemId(item, index, idx)]" />
|
|
<EyeInvisibleOutlined v-else />
|
|
{{ comparisonVisible[getItemId(item, index, idx)] ? '隐藏对比' : '显示对比' }}
|
|
</Button>
|
|
<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>
|
|
<div
|
|
class="section-content markdown-content"
|
|
v-html="renderContent(section, getItemValue(item, section.field))"
|
|
v-if="getItemValue(item, section.field)"
|
|
@click="locateByText(getItemValue(item, section.field), item, section, index, idx)"
|
|
></div>
|
|
|
|
<!-- 页面按钮 - 只在有pdfSource且存在多页时显示 -->
|
|
<div
|
|
v-if="supportsPdfLocation(section) && fieldPageButtons[getFieldKey(item, section, index, idx)]"
|
|
class="page-buttons"
|
|
>
|
|
<span class="page-buttons-label">定位到页面:</span>
|
|
<Button
|
|
v-for="page in fieldPageButtons[getFieldKey(item, section, index, idx)]"
|
|
:key="page"
|
|
type="link"
|
|
size="small"
|
|
class="page-btn"
|
|
@click.stop="goToPageByButton(page, section)"
|
|
>
|
|
第{{ page }}页
|
|
</Button>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- 对比内容展示 - 只在对比模式下显示 -->
|
|
<div
|
|
v-if="shouldShowComparison(section, item) &&
|
|
hasComparisonContent(item, section) &&
|
|
comparisonVisible[getItemId(item, index, idx)]"
|
|
class="content-section comparison-section"
|
|
>
|
|
<div class="section-title">
|
|
{{ getComparisonTitle(section) }}:
|
|
<Button
|
|
type="link"
|
|
size="small"
|
|
class="copy-btn"
|
|
@click.stop="copyContent(getComparisonContent(item, section))"
|
|
>
|
|
<CopyOutlined /> 复制
|
|
</Button>
|
|
</div>
|
|
<div
|
|
class="section-content markdown-content comparison-content"
|
|
v-html="renderMarkdown(getComparisonContent(item, section))"
|
|
@click="locateByText(getComparisonContent(item, section), item, section, index, idx)"
|
|
></div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</TabPane>
|
|
</Tabs>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<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>
|
|
</Drawer>
|
|
</template>
|
|
|
|
<script lang="ts" setup>
|
|
import { ref, computed, watch, nextTick, type PropType } from 'vue';
|
|
import { Drawer, Button, Card, Switch, Tabs, TabPane } from 'ant-design-vue';
|
|
import { DownOutlined, UpOutlined, CopyOutlined, EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons-vue';
|
|
import { message } from 'ant-design-vue';
|
|
import MarkdownIt from 'markdown-it';
|
|
import ReviewPdfContainer from '@/components/Preview/src/PdfPreview/ReviewPdfContainer.vue';
|
|
import {
|
|
getTaskConfig,
|
|
shouldShowComparison,
|
|
getFieldPdfSource,
|
|
supportsPdfLocation,
|
|
getComparisonField,
|
|
getComparisonTitle,
|
|
shouldShowComparisonButton,
|
|
type TaskConfig,
|
|
type FieldConfig
|
|
} from '@/configs/taskConfigs';
|
|
|
|
// 定义数据接口
|
|
interface TaskResultItem {
|
|
id?: string;
|
|
serialNumber?: string;
|
|
existingIssues?: string;
|
|
isRead?: string;
|
|
isAdopted?: string;
|
|
originalText?: string;
|
|
modifiedContent?: string;
|
|
modificationDisplay?: string;
|
|
reviewBasis?: any;
|
|
comparedText?: string;
|
|
[key: string]: any;
|
|
}
|
|
|
|
interface TaskResultCategory {
|
|
name: string;
|
|
results: TaskResultItem[];
|
|
}
|
|
|
|
const props = defineProps({
|
|
visible: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
title: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
width: {
|
|
type: [String, Number],
|
|
default: '95%'
|
|
},
|
|
taskType: {
|
|
type: String,
|
|
required: true
|
|
},
|
|
taskResultDetail: {
|
|
type: Array as PropType<TaskResultCategory[]>,
|
|
default: () => []
|
|
},
|
|
taskInfo: {
|
|
type: Object,
|
|
default: () => ({})
|
|
},
|
|
getPdfStream: {
|
|
type: Function as PropType<(taskId: string) => Promise<Blob>>,
|
|
required: true
|
|
},
|
|
updateResultItemStatus: {
|
|
type: Function as PropType<(id: string, field: string, value: string) => Promise<void>>,
|
|
required: true
|
|
}
|
|
});
|
|
|
|
const emit = defineEmits(['update:visible', 'close']);
|
|
|
|
const md = new MarkdownIt({
|
|
html: true,
|
|
linkify: true,
|
|
typographer: true,
|
|
breaks: true,
|
|
});
|
|
|
|
// 获取任务配置
|
|
const config = computed(() => getTaskConfig(props.taskType));
|
|
|
|
// 基础状态管理
|
|
const activeCollapseKeys = ref<string[]>(['0']);
|
|
const activeItemKeys = ref<string[]>([]);
|
|
const loading = ref<boolean>(false);
|
|
const currentOpId = ref<string>('');
|
|
const currentOpField = ref<string>('');
|
|
const expandReadItems = ref<boolean>(false);
|
|
const expandAdoptedItems = ref<boolean>(false);
|
|
|
|
// 多标签模式相关状态
|
|
const currentTab = ref<string>('');
|
|
const comparisonVisible = ref<Record<string, boolean>>({});
|
|
|
|
// 页面选择相关状态(仅保留必要的)
|
|
const currentSelectText = ref<string>('');
|
|
const currentSelectFieldConfig = ref<FieldConfig | null>(null);
|
|
const currentSelectItem = ref<TaskResultItem | null>(null);
|
|
const currentSelectCategoryIndex = ref<number | null>(null);
|
|
const currentSelectItemIndex = ref<number | null>(null);
|
|
const fieldPageButtons = ref<Record<string, number[]>>({});
|
|
|
|
const pdfContainerRef = ref<InstanceType<typeof ReviewPdfContainer> | null>(null);
|
|
|
|
// PDF布局配置(支持tabs模式下动态切换)
|
|
const pdfLayoutConfig = computed(() => {
|
|
if (!config.value) return null;
|
|
|
|
let pdfConfig = config.value.pdfConfig;
|
|
|
|
// 如果是tabs模式,优先使用当前tab的PDF配置
|
|
if (config.value.mode === 'tabs' && currentTab.value) {
|
|
// 根据当前tab名称匹配配置中的tab
|
|
// 通过数据的name(中文)匹配配置中的label
|
|
const currentTabConfig = config.value.tabs?.find(tab => tab.label === currentTab.value);
|
|
if (currentTabConfig?.pdfConfig) {
|
|
pdfConfig = currentTabConfig.pdfConfig;
|
|
}
|
|
}
|
|
|
|
if (!pdfConfig) return null;
|
|
|
|
// 创建适配ReviewTypeConfig接口的配置对象
|
|
return {
|
|
type: config.value.taskType,
|
|
name: config.value.name,
|
|
pdfLayout: pdfConfig.layout,
|
|
pdfSources: pdfConfig.sources,
|
|
fields: [] // 暂时使用空数组避免类型冲突
|
|
} as any;
|
|
});
|
|
|
|
// 可用标签页(直接基于数据生成)
|
|
const availableTabs = computed(() => {
|
|
if (config.value?.mode !== 'tabs' || !props.taskResultDetail || props.taskResultDetail.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
// 在tabs模式下,跳过第一个"全部"数据,只显示实际的分类
|
|
const dataToProcess = props.taskResultDetail.slice(1); // 跳过第一个数据
|
|
|
|
// 根据剩余数据生成标签页
|
|
return dataToProcess.map((category, index) => {
|
|
let label = `${category.name} (${category.results?.length || 0})`;
|
|
|
|
// 特殊处理:如果是实质性审查,加上审查立场
|
|
if (category.name === '实质性审查' && props.taskInfo?.contractPartyRole) {
|
|
label = `(${props.taskInfo.contractPartyRole})实质性审查 (${category.results?.length || 0})`;
|
|
}
|
|
|
|
return {
|
|
key: category.name, // 使用数据的name作为key
|
|
label: label,
|
|
dataIndex: index + 1, // 记录在原数据中的索引(+1因为跳过了第一个)
|
|
name: category.name
|
|
};
|
|
});
|
|
});
|
|
|
|
// 过滤后的结果(用于标签模式)
|
|
const filteredResults = computed(() => {
|
|
if (config.value?.mode !== 'tabs' || !props.taskResultDetail || !currentTab.value) {
|
|
return [];
|
|
}
|
|
|
|
// 在tabs模式下,跳过第一个"全部"数据,只从实际分类中查找
|
|
const dataToSearch = props.taskResultDetail.slice(1); // 跳过第一个数据
|
|
|
|
// 根据当前选中的tab name,找到对应的数据类别
|
|
const targetCategory = dataToSearch.find(category => category.name === currentTab.value);
|
|
|
|
if (targetCategory) {
|
|
return [targetCategory];
|
|
}
|
|
|
|
// 如果没找到对应数据,返回空数组
|
|
return [];
|
|
});
|
|
|
|
// 单一模式的过滤结果
|
|
const filteredTaskResultDetail = computed(() => {
|
|
if (config.value?.mode === 'tabs') return [];
|
|
|
|
if (props.taskResultDetail.length <= 0) {
|
|
return [];
|
|
}
|
|
|
|
// 如果只有两个或更少的类别,或者第一个类别的results数量<=5个,只展示第一个类别
|
|
if (props.taskResultDetail.length <= 2 ||
|
|
(props.taskResultDetail[0] && props.taskResultDetail[0].results &&
|
|
props.taskResultDetail[0].results.length <= 5)) {
|
|
return [props.taskResultDetail[0]];
|
|
}
|
|
|
|
return props.taskResultDetail;
|
|
});
|
|
|
|
// 获取当前字段配置
|
|
const getContentSections = (item: TaskResultItem): FieldConfig[] => {
|
|
if (config.value?.mode === 'tabs') {
|
|
// 多标签模式:根据当前标签页名称匹配配置中的tab
|
|
// 通过数据的name(中文)匹配配置中的label
|
|
const currentTabConfig = config.value.tabs?.find(tab => tab.label === currentTab.value);
|
|
return (currentTabConfig?.fields || []).filter(field => {
|
|
if (field.displayCondition) {
|
|
return field.displayCondition(item);
|
|
}
|
|
return getItemValue(item, field.field);
|
|
});
|
|
} else {
|
|
// 单一模式:使用配置的字段
|
|
return (config.value?.fields || []).filter(field => {
|
|
if (field.displayCondition) {
|
|
return field.displayCondition(item);
|
|
}
|
|
return getItemValue(item, field.field);
|
|
});
|
|
}
|
|
};
|
|
|
|
// 从项目中获取特定字段的值
|
|
const getItemValue = (item: TaskResultItem, field: string): any => {
|
|
if (!item || !field) return null;
|
|
|
|
// 支持嵌套字段
|
|
if (field.includes('.')) {
|
|
const parts = field.split('.');
|
|
let value: any = item;
|
|
for (const part of parts) {
|
|
if (value && typeof value === 'object') {
|
|
value = value[part];
|
|
} else if (part === 'reviewBasis' && typeof value === 'string') {
|
|
try {
|
|
value = JSON.parse(value);
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
return value;
|
|
}
|
|
|
|
// 如果直接访问reviewBasis字段且为字符串,尝试解析
|
|
if (field === 'reviewBasis' && typeof item[field] === 'string') {
|
|
try {
|
|
return JSON.parse(item[field]);
|
|
} catch (e) {
|
|
return item[field];
|
|
}
|
|
}
|
|
|
|
return item[field];
|
|
};
|
|
|
|
// 根据配置渲染内容
|
|
const renderContent = (section: FieldConfig, value: any): string => {
|
|
if (!value) return '';
|
|
|
|
if (section.displayType === 'markdown') {
|
|
if (section.dataType === 'json' && section.jsonConfig) {
|
|
return renderJsonContent(value, section.jsonConfig);
|
|
}
|
|
return renderMarkdown(value);
|
|
}
|
|
|
|
return value.toString().replace(/\n/g, '<br>');
|
|
};
|
|
|
|
// 渲染JSON内容
|
|
const renderJsonContent = (value: any, jsonConfig: any): string => {
|
|
let jsonObj = value;
|
|
|
|
// 如果是字符串,尝试解析为JSON
|
|
if (typeof value === 'string') {
|
|
try {
|
|
jsonObj = JSON.parse(value);
|
|
} catch (e) {
|
|
return value.toString().replace(/\n/g, '<br>');
|
|
}
|
|
}
|
|
|
|
const values: string[] = [];
|
|
const separator = jsonConfig.separator || ':';
|
|
|
|
jsonConfig.extractFields.forEach((fieldName: string) => {
|
|
if (jsonObj[fieldName]) {
|
|
let fieldValue = jsonObj[fieldName];
|
|
|
|
// 应用字段处理器
|
|
if (jsonConfig.fieldProcessors && jsonConfig.fieldProcessors[fieldName]) {
|
|
fieldValue = jsonConfig.fieldProcessors[fieldName](fieldValue);
|
|
} else {
|
|
// 默认处理:如果是数组取第一个元素
|
|
if (Array.isArray(fieldValue) && fieldValue.length > 0) {
|
|
fieldValue = fieldValue[0];
|
|
}
|
|
}
|
|
|
|
if (fieldValue) {
|
|
values.push(String(fieldValue));
|
|
}
|
|
}
|
|
});
|
|
|
|
return renderMarkdown(values.join(separator));
|
|
};
|
|
|
|
const renderMarkdown = (text: any) => {
|
|
if (!text) return '';
|
|
|
|
const processedText = String(text).replace(/\n/g, ' \n');
|
|
return md.render(processedText);
|
|
};
|
|
|
|
// 生成审核项的唯一key
|
|
const getItemKey = (categoryIndex: number, itemIndex: number, item: TaskResultItem): string => {
|
|
return item.id ? String(item.id) : `${categoryIndex}-${itemIndex}`;
|
|
};
|
|
|
|
// 获取项目的唯一标识符
|
|
const getItemId = (item: TaskResultItem, categoryIndex: number, itemIndex: number): string => {
|
|
return item.id ? String(item.id) : getItemKey(categoryIndex, itemIndex, item);
|
|
};
|
|
|
|
// 切换对比显示状态
|
|
const toggleComparison = (itemId: string) => {
|
|
comparisonVisible.value[itemId] = !comparisonVisible.value[itemId];
|
|
};
|
|
|
|
// 切换项目展开/折叠
|
|
const toggleItemExpand = (categoryIndex: number, itemIndex: number, item: TaskResultItem) => {
|
|
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 updateActiveItemKeys = () => {
|
|
const newActiveKeys: string[] = [];
|
|
const dataToProcess = config.value?.mode === 'tabs' ? filteredResults.value : filteredTaskResultDetail.value;
|
|
|
|
dataToProcess.forEach((category: TaskResultCategory, categoryIndex: number) => {
|
|
if (category?.results) {
|
|
category.results.forEach((item: TaskResultItem, itemIndex: number) => {
|
|
const itemKey = getItemKey(categoryIndex, itemIndex, item);
|
|
const isRead = item.isRead === '1';
|
|
const isAdopted = item.isAdopted === '1';
|
|
|
|
const shouldExpand =
|
|
(!isRead && !isAdopted) ||
|
|
(isRead && expandReadItems.value) ||
|
|
(isAdopted && expandAdoptedItems.value);
|
|
|
|
if (shouldExpand) {
|
|
newActiveKeys.push(itemKey);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
activeItemKeys.value = newActiveKeys;
|
|
};
|
|
|
|
// 切换展开已读项
|
|
const toggleExpandReadItems = (checked: any) => {
|
|
expandReadItems.value = !!checked;
|
|
updateActiveItemKeys();
|
|
};
|
|
|
|
// 切换展开已采纳项
|
|
const toggleExpandAdoptedItems = (checked: any) => {
|
|
expandAdoptedItems.value = !!checked;
|
|
updateActiveItemKeys();
|
|
};
|
|
|
|
// 标签页切换
|
|
const handleTabChange = (activeKey: string | number) => {
|
|
currentTab.value = String(activeKey);
|
|
nextTick(() => {
|
|
updateActiveItemKeys();
|
|
// 通知PDF容器配置已变化
|
|
if (pdfContainerRef.value) {
|
|
pdfContainerRef.value.$forceUpdate && pdfContainerRef.value.$forceUpdate();
|
|
}
|
|
});
|
|
};
|
|
|
|
// 状态变更处理
|
|
const handleStatusChange = async (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 props.updateResultItemStatus(id, field, value);
|
|
|
|
// 更新本地状态
|
|
const dataToUpdate = config.value?.mode === 'tabs' ? props.taskResultDetail : filteredTaskResultDetail.value;
|
|
dataToUpdate.forEach((category: TaskResultCategory) => {
|
|
if (category?.results) {
|
|
category.results.forEach((item: TaskResultItem) => {
|
|
if (item.id === id) {
|
|
item[field] = value;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
updateActiveItemKeys();
|
|
} catch (error) {
|
|
console.error('更新状态失败', error);
|
|
message.error('更新状态失败');
|
|
} finally {
|
|
loading.value = false;
|
|
currentOpId.value = '';
|
|
currentOpField.value = '';
|
|
}
|
|
};
|
|
|
|
// 复制内容到剪贴板
|
|
const copyContent = (content: any) => {
|
|
if (!content) return;
|
|
|
|
let textToCopy = '';
|
|
|
|
if (typeof content === 'string') {
|
|
try {
|
|
const parsed = JSON.parse(content);
|
|
if (parsed && typeof parsed === 'object') {
|
|
const values: string[] = [];
|
|
const fields = ['reviewPoints', 'reviewContent', 'review_content'];
|
|
|
|
fields.forEach(fieldName => {
|
|
if (parsed[fieldName]) {
|
|
let fieldValue = parsed[fieldName];
|
|
|
|
if (Array.isArray(fieldValue) && fieldValue.length > 0) {
|
|
fieldValue = fieldValue[0];
|
|
}
|
|
|
|
if (fieldValue) {
|
|
values.push(String(fieldValue));
|
|
}
|
|
}
|
|
});
|
|
|
|
if (values.length > 0) {
|
|
textToCopy = values.join(':');
|
|
} else {
|
|
textToCopy = content;
|
|
}
|
|
} else {
|
|
textToCopy = content;
|
|
}
|
|
} catch (e) {
|
|
textToCopy = content;
|
|
}
|
|
} else if (typeof content === 'object') {
|
|
if (content.reviewContent) {
|
|
textToCopy = content.reviewContent;
|
|
|
|
if (content.reviewPoints && content.reviewPoints.length) {
|
|
textToCopy += '\n\n审查要点:\n';
|
|
content.reviewPoints.forEach((point: any, index: number) => {
|
|
textToCopy += `${index + 1}. ${point}\n`;
|
|
});
|
|
}
|
|
} else {
|
|
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 getFieldKey = (item: TaskResultItem, fieldConfig: FieldConfig, categoryIndex: number, itemIndex: number): string => {
|
|
const itemId = getItemId(item, categoryIndex, itemIndex);
|
|
return `${itemId}_${fieldConfig.field}`;
|
|
};
|
|
|
|
// 定位功能 - 优化版,利用现有弹窗机制并保存页面信息
|
|
const locateByText = async (text: string, item?: TaskResultItem, fieldConfig?: FieldConfig, categoryIndex?: number, itemIndex?: number) => {
|
|
if (!pdfContainerRef.value || !text || !text.trim()) return;
|
|
|
|
// 检查字段是否支持PDF定位
|
|
if (fieldConfig && !supportsPdfLocation(fieldConfig)) {
|
|
console.log('Field does not support PDF location:', fieldConfig.field);
|
|
return;
|
|
}
|
|
|
|
// 获取PDF源
|
|
let pdfSource: string | undefined;
|
|
if (fieldConfig) {
|
|
const source = getFieldPdfSource(fieldConfig);
|
|
pdfSource = source || undefined;
|
|
}
|
|
|
|
// 保存当前定位的字段信息,以便在检测到多页面时使用
|
|
if (fieldConfig && item && categoryIndex !== undefined && itemIndex !== undefined) {
|
|
currentSelectText.value = text;
|
|
currentSelectFieldConfig.value = fieldConfig;
|
|
currentSelectItem.value = item;
|
|
currentSelectCategoryIndex.value = categoryIndex;
|
|
currentSelectItemIndex.value = itemIndex;
|
|
|
|
// 清理当前字段的页面按钮数据,重新获取
|
|
const fieldKey = getFieldKey(item, fieldConfig, categoryIndex, itemIndex);
|
|
console.log('fieldKey', fieldKey);
|
|
delete fieldPageButtons.value[fieldKey];
|
|
console.log('fieldPageButtons', fieldPageButtons.value);
|
|
}
|
|
|
|
// 检查是否已经有页面按钮,如果有直接使用第一个页面
|
|
if (fieldConfig && item && categoryIndex !== undefined && itemIndex !== undefined) {
|
|
const fieldKey = getFieldKey(item, fieldConfig, categoryIndex, itemIndex);
|
|
if (fieldPageButtons.value[fieldKey] && fieldPageButtons.value[fieldKey].length > 0) {
|
|
// 有页面按钮时,使用第一个页面进行定位
|
|
try {
|
|
await pdfContainerRef.value.locateByText(text, pdfSource);
|
|
return;
|
|
} catch (error) {
|
|
console.error('PDF定位失败:', error);
|
|
}
|
|
}
|
|
}
|
|
|
|
try {
|
|
// 先尝试用完整文本进行定位
|
|
await pdfContainerRef.value.locateByText(text, pdfSource);
|
|
|
|
// 延迟检查是否出现了页面选择弹窗
|
|
setTimeout(() => {
|
|
checkForPageModal();
|
|
}, 300); // 等待弹窗显示
|
|
} catch (error) {
|
|
console.error('完整文本定位失败,尝试使用前20个字符定位:', error);
|
|
|
|
// 如果完整文本定位失败,尝试用前10个字符再次定位
|
|
const shortText = text.trim().substring(0,25);
|
|
if (shortText && shortText !== text.trim()) {
|
|
try {
|
|
console.log('使用前25个字符进行定位:', shortText);
|
|
message.warning('完整文本定位失败,尝试使用前25个字符定位');
|
|
await pdfContainerRef.value.locateByText(shortText, pdfSource);
|
|
|
|
// 延迟检查是否出现了页面选择弹窗
|
|
setTimeout(() => {
|
|
checkForPageModal();
|
|
}, 300); // 等待弹窗显示
|
|
} catch (shortError) {
|
|
console.error('前25个字符定位也失败:', shortError);
|
|
message.warning('文本定位失败,该内容可能不在PDF中');
|
|
}
|
|
} else {
|
|
message.warning('文本定位失败,该内容可能不在PDF中');
|
|
}
|
|
}
|
|
};
|
|
|
|
// 通过页面按钮跳转
|
|
const goToPageByButton = async (page: number, fieldConfig: FieldConfig) => {
|
|
if (!pdfContainerRef.value) return;
|
|
|
|
// 获取PDF源
|
|
let pdfSource: string | undefined;
|
|
if (fieldConfig) {
|
|
const source = getFieldPdfSource(fieldConfig);
|
|
pdfSource = source || undefined;
|
|
}
|
|
|
|
// 使用PDF定位功能,传入当前选中的文本
|
|
if (currentSelectText.value) {
|
|
try {
|
|
// 使用新的直接定位方法,避免弹窗选择
|
|
await pdfContainerRef.value.locateByTextAndPage(currentSelectText.value, page, pdfSource);
|
|
message.success(`已定位到第${page}页`);
|
|
} catch (error) {
|
|
console.error('页面跳转失败:', error);
|
|
message.error('页面跳转失败');
|
|
}
|
|
}
|
|
};
|
|
|
|
// 检测并提取页面选择弹窗中的页面信息
|
|
const checkForPageModal = () => {
|
|
const modal = document.querySelector('.ant-modal:not(.ant-modal-hidden)');
|
|
if (modal && modal.textContent?.includes('请选择要跳转的页码')) {
|
|
// 提取页面信息
|
|
const pages: number[] = [];
|
|
const modalButtons = document.querySelectorAll('.ant-modal:not(.ant-modal-hidden) .ant-btn');
|
|
|
|
modalButtons.forEach(button => {
|
|
const text = button.textContent || '';
|
|
console.log('button', button.textContent);
|
|
const match = text.match(/第(\d+)页/);
|
|
if (match) {
|
|
pages.push(parseInt(match[1]));
|
|
}
|
|
});
|
|
console.log('pages', pages);
|
|
// 如果有字段信息,创建页面按钮
|
|
if (pages.length > 0 && currentSelectFieldConfig.value && currentSelectItem.value &&
|
|
currentSelectCategoryIndex.value !== null && currentSelectItemIndex.value !== null) {
|
|
const fieldKey = getFieldKey(
|
|
currentSelectItem.value,
|
|
currentSelectFieldConfig.value,
|
|
currentSelectCategoryIndex.value,
|
|
currentSelectItemIndex.value
|
|
);
|
|
|
|
// 保存页面信息到状态变量
|
|
fieldPageButtons.value[fieldKey] = pages;
|
|
}
|
|
}
|
|
};
|
|
|
|
// 初始化数据
|
|
const initData = async () => {
|
|
loading.value = true;
|
|
try {
|
|
if (props.taskResultDetail) {
|
|
if (config.value?.mode === 'tabs' && availableTabs.value.length > 0) {
|
|
// 使用第一个数据类别的名称作为默认tab
|
|
currentTab.value = availableTabs.value[0].key;
|
|
} else {
|
|
if (props.taskResultDetail.length > 0) {
|
|
activeCollapseKeys.value = ['0'];
|
|
}
|
|
}
|
|
|
|
await nextTick();
|
|
updateActiveItemKeys();
|
|
}
|
|
} catch (error) {
|
|
console.error('初始化详情抽屉时出错', error);
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
// 事件处理
|
|
const handleClose = () => {
|
|
// 清理页面按钮状态
|
|
fieldPageButtons.value = {};
|
|
currentSelectText.value = '';
|
|
currentSelectFieldConfig.value = null;
|
|
currentSelectItem.value = null;
|
|
currentSelectCategoryIndex.value = null;
|
|
currentSelectItemIndex.value = null;
|
|
|
|
emit('update:visible', false);
|
|
emit('close');
|
|
};
|
|
|
|
const handlePdfLoaded = (data: any) => {
|
|
console.log('PDF加载成功:', data);
|
|
};
|
|
|
|
const handlePdfError = (error: any) => {
|
|
console.error('PDF加载失败:', error);
|
|
};
|
|
|
|
// 监听变化
|
|
watch(() => props.visible, (newVal) => {
|
|
if (newVal && props.taskResultDetail) {
|
|
initData();
|
|
}
|
|
});
|
|
|
|
watch([expandReadItems, expandAdoptedItems], () => {
|
|
updateActiveItemKeys();
|
|
});
|
|
|
|
// 监听PDF配置变化,当配置变化时强制刷新PDF容器
|
|
watch(() => pdfLayoutConfig.value, (newConfig, oldConfig) => {
|
|
if (newConfig && oldConfig &&
|
|
(newConfig.pdfLayout !== oldConfig.pdfLayout ||
|
|
JSON.stringify(newConfig.pdfSources) !== JSON.stringify(oldConfig.pdfSources))) {
|
|
nextTick(() => {
|
|
if (pdfContainerRef.value) {
|
|
// 强制PDF容器重新渲染
|
|
pdfContainerRef.value.$forceUpdate && pdfContainerRef.value.$forceUpdate();
|
|
}
|
|
});
|
|
}
|
|
}, { deep: true });
|
|
|
|
// 检查是否有对比内容(支持配置化对比字段)
|
|
const hasComparisonContent = (item: TaskResultItem, fieldConfig: FieldConfig): boolean => {
|
|
const comparisonField = getComparisonField(fieldConfig);
|
|
return !!(item[comparisonField] && item[comparisonField].trim());
|
|
};
|
|
|
|
// 获取对比内容
|
|
const getComparisonContent = (item: TaskResultItem, fieldConfig: FieldConfig): string => {
|
|
const comparisonField = getComparisonField(fieldConfig);
|
|
return item[comparisonField] || '';
|
|
};
|
|
</script>
|
|
|
|
<style lang="less" scoped>
|
|
:deep(.ant-drawer-body) {
|
|
height: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
padding: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.universal-review-container,
|
|
.content-container,
|
|
.tabs-container,
|
|
.single-content,
|
|
:deep(.ant-tabs),
|
|
:deep(.ant-tabs-content),
|
|
:deep(.ant-tabs-tabpane),
|
|
.category-items {
|
|
height: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
|
|
:deep(.ant-tabs-nav) {
|
|
flex-shrink: 0;
|
|
background: #fff;
|
|
z-index: 2;
|
|
}
|
|
|
|
.items-card-list {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 16px;
|
|
min-height: 0;
|
|
}
|
|
|
|
:deep(.ant-tabs-tab) {
|
|
padding: 8px 16px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.item-card {
|
|
width: 100%;
|
|
margin-bottom: 10px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.item-card :deep(.ant-card-head-title) {
|
|
overflow: visible !important;
|
|
white-space: normal !important;
|
|
text-overflow: unset !important;
|
|
}
|
|
|
|
.expand-icon {
|
|
font-size: 12px;
|
|
color: rgba(0, 0, 0, 0.45);
|
|
}
|
|
|
|
.item-header-content {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
width: 100%;
|
|
}
|
|
|
|
.item-info {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
flex: 1;
|
|
overflow: visible;
|
|
text-overflow: unset;
|
|
white-space: normal;
|
|
}
|
|
|
|
.item-serial {
|
|
margin-right: 12px;
|
|
font-weight: bold;
|
|
color: #1890ff;
|
|
}
|
|
|
|
.item-title {
|
|
font-weight: 500;
|
|
font-size: 14px;
|
|
flex: 1;
|
|
overflow: visible;
|
|
text-overflow: unset;
|
|
white-space: normal;
|
|
word-wrap: break-word;
|
|
word-break: break-word;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.item-actions {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
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;
|
|
}
|
|
|
|
.title-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.comparison-btn {
|
|
padding: 0 8px;
|
|
font-size: 12px;
|
|
height: 24px;
|
|
border: 1px solid #d9d9d9;
|
|
border-radius: 4px;
|
|
|
|
:deep(.anticon) {
|
|
font-size: 12px;
|
|
margin-right: 4px;
|
|
}
|
|
|
|
&:hover {
|
|
border-color: #40a9ff;
|
|
color: #40a9ff;
|
|
}
|
|
}
|
|
|
|
.copy-btn {
|
|
padding: 0 4px;
|
|
font-size: 12px;
|
|
height: 24px;
|
|
|
|
:deep(.anticon) {
|
|
font-size: 12px;
|
|
}
|
|
}
|
|
|
|
.section-content {
|
|
padding: 12px;
|
|
background-color: #f9fbfd;
|
|
border-radius: 4px;
|
|
white-space: pre-wrap;
|
|
max-height: 300px;
|
|
overflow: auto;
|
|
}
|
|
|
|
.comparison-section {
|
|
margin-top: 12px;
|
|
border-top: 1px dashed #e8e8e8;
|
|
padding-top: 12px;
|
|
}
|
|
|
|
.comparison-content {
|
|
background-color: #fff7e6;
|
|
border: 1px solid #ffd591;
|
|
border-radius: 4px;
|
|
|
|
&:hover {
|
|
background-color: #fff1d6;
|
|
}
|
|
}
|
|
|
|
.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;
|
|
}
|
|
}
|
|
}
|
|
|
|
.ml-3 {
|
|
margin-left: 12px;
|
|
}
|
|
|
|
.page-buttons {
|
|
margin-top: 8px;
|
|
padding: 8px 12px;
|
|
background-color: #f0f9ff;
|
|
border: 1px solid #bfdbfe;
|
|
border-radius: 4px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.page-buttons-label {
|
|
font-size: 12px;
|
|
color: #6b7280;
|
|
margin-right: 4px;
|
|
}
|
|
|
|
.page-btn {
|
|
font-size: 12px;
|
|
height: 24px;
|
|
padding: 0 8px;
|
|
background-color: #ffffff;
|
|
border: 1px solid #d1d5db;
|
|
border-radius: 3px;
|
|
margin: 0 2px;
|
|
|
|
&:hover {
|
|
background-color: #dbeafe;
|
|
border-color: #3b82f6;
|
|
color: #1d4ed8;
|
|
}
|
|
}
|
|
|
|
.drawer-footer {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
width: 100%;
|
|
}
|
|
|
|
.status-switches {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.split-layout {
|
|
display: flex;
|
|
height: 100%;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.pdf-preview-wrapper {
|
|
width: 50%;
|
|
height: 100%;
|
|
border-right: 1px solid #e8e8e8;
|
|
}
|
|
|
|
.content-container {
|
|
width: 50%;
|
|
height: 100%;
|
|
overflow: hidden;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.review-type-tabs {
|
|
flex-shrink: 0;
|
|
border-bottom: 1px solid #e8e8e8;
|
|
background: #fff;
|
|
|
|
:deep(.ant-tabs-nav) {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
:deep(.ant-tabs-tab) {
|
|
padding: 8px 16px;
|
|
font-weight: 500;
|
|
color: #666;
|
|
|
|
&.ant-tabs-tab-active {
|
|
color: #1890ff;
|
|
font-weight: 600;
|
|
}
|
|
}
|
|
}
|
|
|
|
.tab-content {
|
|
flex: 1;
|
|
overflow: hidden;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
</style>
|