260720
This commit is contained in:
@@ -0,0 +1,706 @@
|
||||
<template>
|
||||
<ele-modal
|
||||
:width="960"
|
||||
:footer="null"
|
||||
title="项目申报书文件校验"
|
||||
:visible="visible"
|
||||
@update:visible="updateVisible"
|
||||
>
|
||||
<a-spin :spinning="loading">
|
||||
<div class="validator-layout">
|
||||
<div class="upload-panel">
|
||||
<div class="validator-intro">
|
||||
上传项目申报书后,系统将调用项目书质量初审模型,从创新性、可行性、应用前景、文本规范性等维度生成初审报告。
|
||||
</div>
|
||||
|
||||
<div class="upload-label">项目申报书</div>
|
||||
<a-upload-dragger
|
||||
accept=".pdf,.doc,.docx,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
||||
:file-list="projectFileList"
|
||||
:before-upload="beforeProjectUpload"
|
||||
:max-count="1"
|
||||
@remove="removeProjectFile"
|
||||
>
|
||||
<p class="ant-upload-drag-icon">
|
||||
<cloud-upload-outlined />
|
||||
</p>
|
||||
<p class="ant-upload-text">点击或拖拽上传待审项目申报书</p>
|
||||
<p class="ant-upload-hint">支持 PDF、DOC、DOCX,大小不超过 50MB</p>
|
||||
</a-upload-dragger>
|
||||
|
||||
<div class="upload-label optional-label">自定义评分细则</div>
|
||||
<a-upload
|
||||
accept=".pdf,.doc,.docx,.csv,.xls,.xlsx,.txt,.md"
|
||||
:file-list="rubricFileList"
|
||||
:before-upload="beforeRubricUpload"
|
||||
:max-count="1"
|
||||
@remove="removeRubricFile"
|
||||
>
|
||||
<a-button>
|
||||
<template #icon>
|
||||
<upload-outlined />
|
||||
</template>
|
||||
上传评分细则
|
||||
</a-button>
|
||||
</a-upload>
|
||||
|
||||
<div class="action-row">
|
||||
<a-button
|
||||
type="primary"
|
||||
:disabled="!projectFile || loading"
|
||||
@click="startReview"
|
||||
>
|
||||
开始校验
|
||||
</a-button>
|
||||
<a-button :disabled="!loading" @click="stopReview">停止</a-button>
|
||||
<a-button :disabled="loading && !reportMarkdown" @click="clearResult">
|
||||
清空
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="result-panel">
|
||||
<div class="result-toolbar">
|
||||
<div>
|
||||
<div class="result-title">AI 初审报告</div>
|
||||
<div class="result-subtitle">
|
||||
{{ statusText || '报告生成后将在此展示' }}
|
||||
</div>
|
||||
</div>
|
||||
<a-space>
|
||||
<a-tag :color="loading ? 'processing' : reportMarkdown ? 'success' : 'default'">
|
||||
{{ loading ? '生成中' : reportMarkdown ? '已完成' : '待开始' }}
|
||||
</a-tag>
|
||||
<a-button size="small" :disabled="!reportMarkdown" @click="exportReport">
|
||||
导出
|
||||
</a-button>
|
||||
<a-button
|
||||
size="small"
|
||||
:disabled="!reportMarkdown"
|
||||
@click="fullscreenVisible = true"
|
||||
>
|
||||
全屏查看
|
||||
</a-button>
|
||||
<a-button size="small" :disabled="!reportMarkdown" @click="copyReport">
|
||||
复制
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<div ref="reportShellRef" class="report-shell">
|
||||
<div v-if="loading && !reportMarkdown" class="report-streaming">
|
||||
AI 正在分析项目申报书...
|
||||
</div>
|
||||
<ByteMdViewer
|
||||
v-else-if="reportMarkdown"
|
||||
class="report-viewer"
|
||||
:value="reportMarkdown"
|
||||
:plugins="plugins"
|
||||
/>
|
||||
<a-empty v-else description="请先上传项目申报书并开始校验" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</ele-modal>
|
||||
|
||||
<ele-modal
|
||||
:width="'92vw'"
|
||||
:footer="null"
|
||||
title="AI 初审报告"
|
||||
:visible="fullscreenVisible"
|
||||
@update:visible="fullscreenVisible = $event"
|
||||
>
|
||||
<div class="fullscreen-toolbar">
|
||||
<a-space>
|
||||
<a-button :disabled="!reportMarkdown" @click="exportReport">
|
||||
导出
|
||||
</a-button>
|
||||
<a-button :disabled="!reportMarkdown" @click="copyReport">
|
||||
复制
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
<div class="fullscreen-report-shell">
|
||||
<ByteMdViewer
|
||||
v-if="reportMarkdown"
|
||||
class="report-viewer"
|
||||
:value="reportMarkdown"
|
||||
:plugins="plugins"
|
||||
/>
|
||||
<a-empty v-else description="暂无初审报告" />
|
||||
</div>
|
||||
</ele-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import 'github-markdown-css/github-markdown-light.css';
|
||||
import gfm from '@bytemd/plugin-gfm';
|
||||
import zhHansGfm from '@bytemd/plugin-gfm/locales/zh_Hans.json';
|
||||
import highlight from '@bytemd/plugin-highlight-ssr';
|
||||
import 'highlight.js/styles/default.css';
|
||||
import { computed, ref, watch, onBeforeUnmount, nextTick } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import type { UploadProps } from 'ant-design-vue';
|
||||
import { CloudUploadOutlined, UploadOutlined } from '@ant-design/icons-vue';
|
||||
import ByteMdViewer from '@/components/ByteMdViewer/index.vue';
|
||||
import { reviewWxxzxProjectBookQuality } from '@/api/gxmu/wxxzx-form';
|
||||
import { stopMessage } from '@/api/oa/chatgpt';
|
||||
import { API_BASE_URL } from '@/config/setting';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
import { copyText } from '@/utils/common';
|
||||
import type { WxxzxProjectQualityReviewResult } from '@/api/gxmu/wxxzx-form/model';
|
||||
|
||||
const STREAM_END_TEXT = '__END__';
|
||||
const SOCKET_CONNECTED_TEXT = '连接成功';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
}>();
|
||||
|
||||
const loginUser = computed(() => useUserStore().info ?? {});
|
||||
const plugins = ref([
|
||||
gfm({
|
||||
locale: zhHansGfm
|
||||
}),
|
||||
highlight()
|
||||
]);
|
||||
const loading = ref(false);
|
||||
const statusText = ref('');
|
||||
const projectFile = ref<File>();
|
||||
const rubricFile = ref<File>();
|
||||
const reportMarkdown = ref('');
|
||||
const activeTaskId = ref<string>();
|
||||
const streamStarted = ref(false);
|
||||
const fullscreenVisible = ref(false);
|
||||
const reportShellRef = ref<HTMLElement | null>(null);
|
||||
const webSocketRef = ref<WebSocket | null>(null);
|
||||
const wsConnectPromiseRef = ref<Promise<void> | null>(null);
|
||||
const streamResolverRef = ref<{
|
||||
resolve: () => void;
|
||||
reject: (error: Error) => void;
|
||||
} | null>(null);
|
||||
|
||||
const projectFileList = computed(() =>
|
||||
projectFile.value
|
||||
? [
|
||||
{
|
||||
uid: 'project',
|
||||
name: projectFile.value.name,
|
||||
status: 'done' as const
|
||||
}
|
||||
]
|
||||
: []
|
||||
);
|
||||
const rubricFileList = computed(() =>
|
||||
rubricFile.value
|
||||
? [
|
||||
{
|
||||
uid: 'rubric',
|
||||
name: rubricFile.value.name,
|
||||
status: 'done' as const
|
||||
}
|
||||
]
|
||||
: []
|
||||
);
|
||||
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
const beforeProjectUpload: UploadProps['beforeUpload'] = (file) => {
|
||||
if (file.size / 1024 / 1024 > 50) {
|
||||
message.error('项目申报书不能超过 50MB');
|
||||
return false;
|
||||
}
|
||||
projectFile.value = file as File;
|
||||
return false;
|
||||
};
|
||||
|
||||
const beforeRubricUpload: UploadProps['beforeUpload'] = (file) => {
|
||||
if (file.size / 1024 / 1024 > 20) {
|
||||
message.error('评分细则不能超过 20MB');
|
||||
return false;
|
||||
}
|
||||
rubricFile.value = file as File;
|
||||
return false;
|
||||
};
|
||||
|
||||
const removeProjectFile = () => {
|
||||
projectFile.value = undefined;
|
||||
return true;
|
||||
};
|
||||
|
||||
const removeRubricFile = () => {
|
||||
rubricFile.value = undefined;
|
||||
return true;
|
||||
};
|
||||
|
||||
const scrollReportToBottom = () => {
|
||||
nextTick(() => {
|
||||
const el = reportShellRef.value;
|
||||
if (el) {
|
||||
el.scrollTop = el.scrollHeight;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const buildWebSocketUrl = (userId: number) => {
|
||||
if (/^https?:\/\//.test(API_BASE_URL)) {
|
||||
const url = new URL(API_BASE_URL);
|
||||
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
url.pathname = url.pathname.replace(/\/api\/?$/, '');
|
||||
return `${url.toString().replace(/\/$/, '')}/chat/${userId}`;
|
||||
}
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
return `${protocol}//${window.location.host}/chat/${userId}`;
|
||||
};
|
||||
|
||||
const clearStreamResolver = () => {
|
||||
streamResolverRef.value = null;
|
||||
};
|
||||
|
||||
const resolveStream = () => {
|
||||
streamResolverRef.value?.resolve();
|
||||
clearStreamResolver();
|
||||
};
|
||||
|
||||
const rejectStream = (error: Error) => {
|
||||
streamResolverRef.value?.reject(error);
|
||||
clearStreamResolver();
|
||||
};
|
||||
|
||||
const createStreamWaiter = () => {
|
||||
streamStarted.value = false;
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
streamResolverRef.value = { resolve, reject };
|
||||
});
|
||||
};
|
||||
|
||||
const disconnectWebSocket = () => {
|
||||
wsConnectPromiseRef.value = null;
|
||||
const socket = webSocketRef.value;
|
||||
webSocketRef.value = null;
|
||||
if (
|
||||
socket &&
|
||||
(socket.readyState === WebSocket.OPEN ||
|
||||
socket.readyState === WebSocket.CONNECTING)
|
||||
) {
|
||||
socket.close();
|
||||
}
|
||||
};
|
||||
|
||||
const pickReportText = (payload: any): string => {
|
||||
if (!payload) {
|
||||
return '';
|
||||
}
|
||||
if (typeof payload === 'string') {
|
||||
return payload;
|
||||
}
|
||||
const outputs = payload.outputs || payload.data?.outputs || payload.raw?.data?.outputs;
|
||||
const direct =
|
||||
payload.answer ||
|
||||
payload.result ||
|
||||
payload.output ||
|
||||
payload.content ||
|
||||
outputs?.answer ||
|
||||
outputs?.result ||
|
||||
outputs?.output ||
|
||||
outputs?.content ||
|
||||
outputs?.report ||
|
||||
outputs?.markdown;
|
||||
if (typeof direct === 'string') {
|
||||
return direct;
|
||||
}
|
||||
if (outputs) {
|
||||
const firstText = Object.values(outputs).find(
|
||||
(item) => typeof item === 'string' && item.trim()
|
||||
);
|
||||
if (typeof firstText === 'string') {
|
||||
return firstText;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const appendReportChunk = (chunk: string, taskId?: string) => {
|
||||
if (!chunk) {
|
||||
return;
|
||||
}
|
||||
if (taskId && taskId !== '0') {
|
||||
activeTaskId.value = taskId;
|
||||
}
|
||||
if (!streamStarted.value) {
|
||||
streamStarted.value = true;
|
||||
reportMarkdown.value = '';
|
||||
}
|
||||
reportMarkdown.value += chunk;
|
||||
statusText.value = '正在通过 WebSocket 接收流式报告';
|
||||
scrollReportToBottom();
|
||||
};
|
||||
|
||||
const finishStream = (taskId?: string) => {
|
||||
if (taskId && taskId !== '0') {
|
||||
activeTaskId.value = taskId;
|
||||
}
|
||||
loading.value = false;
|
||||
statusText.value = '项目书质量初审完成';
|
||||
scrollReportToBottom();
|
||||
resolveStream();
|
||||
disconnectWebSocket();
|
||||
};
|
||||
|
||||
const handleSocketMessage = (raw: string) => {
|
||||
if (!raw) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const data = JSON.parse(raw);
|
||||
if (data?.stopType && data.stopType !== 'projectQualityReview') {
|
||||
return;
|
||||
}
|
||||
if (data?.answer === STREAM_END_TEXT) {
|
||||
finishStream(data?.taskId);
|
||||
return;
|
||||
}
|
||||
if (data?.taskId && data.taskId !== '0') {
|
||||
activeTaskId.value = data.taskId;
|
||||
}
|
||||
if (data?.loading === 'true' || data?.loading === true) {
|
||||
statusText.value = 'AI 正在检索历史案例并组织评审意见...';
|
||||
return;
|
||||
}
|
||||
if (typeof data?.answer === 'string') {
|
||||
appendReportChunk(data.answer, data?.taskId);
|
||||
}
|
||||
} catch (e) {
|
||||
appendReportChunk(raw);
|
||||
}
|
||||
};
|
||||
|
||||
const connectWebSocket = async () => {
|
||||
const userId = loginUser.value.userId;
|
||||
if (!userId) {
|
||||
return Promise.reject(new Error('未获取到当前登录用户'));
|
||||
}
|
||||
const currentSocket = webSocketRef.value;
|
||||
if (currentSocket?.readyState === WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
(currentSocket?.readyState === WebSocket.OPEN ||
|
||||
currentSocket?.readyState === WebSocket.CONNECTING) &&
|
||||
wsConnectPromiseRef.value
|
||||
) {
|
||||
return wsConnectPromiseRef.value;
|
||||
}
|
||||
let cleanupPending = () => undefined;
|
||||
const connectPromise = new Promise<void>((resolve, reject) => {
|
||||
let settled = false;
|
||||
const socket = new WebSocket(buildWebSocketUrl(userId));
|
||||
cleanupPending = () => {
|
||||
if (wsConnectPromiseRef.value === connectPromise) {
|
||||
wsConnectPromiseRef.value = null;
|
||||
}
|
||||
};
|
||||
socket.onmessage = (event) => {
|
||||
const raw = String(event.data || '');
|
||||
if (raw === SOCKET_CONNECTED_TEXT) {
|
||||
if (!settled) {
|
||||
settled = true;
|
||||
cleanupPending();
|
||||
resolve();
|
||||
}
|
||||
return;
|
||||
}
|
||||
handleSocketMessage(raw);
|
||||
};
|
||||
socket.onclose = () => {
|
||||
if (webSocketRef.value === socket) {
|
||||
webSocketRef.value = null;
|
||||
}
|
||||
if (!settled) {
|
||||
settled = true;
|
||||
cleanupPending();
|
||||
reject(new Error('项目书质量初审连接已关闭'));
|
||||
return;
|
||||
}
|
||||
if (loading.value) {
|
||||
rejectStream(new Error('项目书质量初审连接已关闭'));
|
||||
loading.value = false;
|
||||
}
|
||||
cleanupPending();
|
||||
};
|
||||
socket.onerror = () => {
|
||||
if (!settled) {
|
||||
settled = true;
|
||||
cleanupPending();
|
||||
reject(new Error('项目书质量初审连接失败'));
|
||||
return;
|
||||
}
|
||||
if (loading.value) {
|
||||
rejectStream(new Error('项目书质量初审连接失败'));
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
webSocketRef.value = socket;
|
||||
});
|
||||
wsConnectPromiseRef.value = connectPromise;
|
||||
return connectPromise;
|
||||
};
|
||||
|
||||
const clearResult = () => {
|
||||
reportMarkdown.value = '';
|
||||
statusText.value = '';
|
||||
activeTaskId.value = undefined;
|
||||
streamStarted.value = false;
|
||||
};
|
||||
|
||||
const startReview = async () => {
|
||||
if (!projectFile.value) {
|
||||
message.warning('请先上传项目申报书');
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
clearResult();
|
||||
statusText.value = '正在建立 WebSocket 连接...';
|
||||
try {
|
||||
await connectWebSocket();
|
||||
statusText.value = '连接成功,已提交项目书质量初审任务...';
|
||||
const waitStream = createStreamWaiter();
|
||||
const result = (await reviewWxxzxProjectBookQuality(
|
||||
projectFile.value,
|
||||
rubricFile.value
|
||||
)) as WxxzxProjectQualityReviewResult;
|
||||
const directReport = pickReportText(result);
|
||||
if (!streamStarted.value && directReport) {
|
||||
reportMarkdown.value = directReport;
|
||||
loading.value = false;
|
||||
statusText.value = '项目书质量初审完成';
|
||||
scrollReportToBottom();
|
||||
resolveStream();
|
||||
disconnectWebSocket();
|
||||
} else {
|
||||
await waitStream;
|
||||
}
|
||||
if (!reportMarkdown.value.trim()) {
|
||||
message.warning('接口已返回,但未解析到初审报告内容');
|
||||
return;
|
||||
}
|
||||
message.success('项目书质量初审完成');
|
||||
} catch (e: any) {
|
||||
disconnectWebSocket();
|
||||
clearStreamResolver();
|
||||
loading.value = false;
|
||||
statusText.value = '';
|
||||
message.error(e.message || '项目书质量初审失败');
|
||||
}
|
||||
};
|
||||
|
||||
const stopReview = async () => {
|
||||
try {
|
||||
await stopMessage({
|
||||
taskId: activeTaskId.value,
|
||||
mode: 'projectQualityReview' as any
|
||||
});
|
||||
loading.value = false;
|
||||
statusText.value = '已停止项目书质量初审';
|
||||
resolveStream();
|
||||
disconnectWebSocket();
|
||||
message.success('已停止');
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '停止失败');
|
||||
}
|
||||
};
|
||||
|
||||
const copyReport = () => {
|
||||
if (!reportMarkdown.value.trim()) {
|
||||
message.warning('暂无可复制内容');
|
||||
return;
|
||||
}
|
||||
copyText(reportMarkdown.value);
|
||||
};
|
||||
|
||||
const exportReport = () => {
|
||||
if (!reportMarkdown.value.trim()) {
|
||||
message.warning('暂无可导出内容');
|
||||
return;
|
||||
}
|
||||
const baseName =
|
||||
projectFile.value?.name?.replace(/\.[^.]+$/, '') || '项目书质量初审报告';
|
||||
const safeName = baseName.replace(/[\\/:*?"<>|]/g, '_');
|
||||
const blob = new Blob([reportMarkdown.value], {
|
||||
type: 'text/markdown;charset=utf-8'
|
||||
});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `${safeName}-AI初审报告.md`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
message.success('导出成功');
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(value) => {
|
||||
if (value) {
|
||||
clearResult();
|
||||
} else {
|
||||
disconnectWebSocket();
|
||||
clearStreamResolver();
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
disconnectWebSocket();
|
||||
clearStreamResolver();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.validator-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 330px minmax(0, 1fr);
|
||||
align-items: start;
|
||||
gap: 20px;
|
||||
max-height: calc(100vh - 220px);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.upload-panel {
|
||||
min-width: 0;
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
.validator-intro {
|
||||
margin-bottom: 16px;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.upload-label {
|
||||
margin: 14px 0 8px;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
|
||||
.optional-label {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.action-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.result-panel {
|
||||
min-width: 0;
|
||||
align-self: start;
|
||||
border: 1px solid #edf0f5;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.result-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid #edf0f5;
|
||||
}
|
||||
|
||||
.result-title {
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
}
|
||||
|
||||
.result-subtitle {
|
||||
margin-top: 4px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.report-shell {
|
||||
height: 360px;
|
||||
overflow: auto;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.report-streaming {
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
line-height: 1.8;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.report-viewer :deep(.markdown-body) {
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.fullscreen-toolbar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.fullscreen-report-shell {
|
||||
height: calc(100vh - 210px);
|
||||
overflow: auto;
|
||||
padding: 20px 24px;
|
||||
border: 1px solid #edf0f5;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.fullscreen-report-shell :deep(.markdown-body) {
|
||||
max-width: 980px;
|
||||
margin: 0 auto;
|
||||
font-size: 15px;
|
||||
line-height: 1.9;
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.validator-layout {
|
||||
grid-template-columns: 1fr;
|
||||
max-height: calc(100vh - 180px);
|
||||
}
|
||||
|
||||
.report-shell {
|
||||
height: 320px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-upload.ant-upload-drag) {
|
||||
height: 180px;
|
||||
}
|
||||
|
||||
:deep(.ant-upload.ant-upload-drag .ant-upload) {
|
||||
padding: 24px 16px;
|
||||
}
|
||||
|
||||
:deep(.ant-upload.ant-upload-drag p.ant-upload-drag-icon) {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
:deep(.ant-upload.ant-upload-drag p.ant-upload-text) {
|
||||
margin-bottom: 6px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
:deep(.ant-upload-list) {
|
||||
max-height: 74px;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user