|
|
@@ -139,6 +139,7 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
+
|
|
|
</div>
|
|
|
|
|
|
<!-- 左侧拖拽分隔条 -->
|
|
|
@@ -153,6 +154,7 @@
|
|
|
<div class="welcome">
|
|
|
<h1>{{ greetingText }},{{ userName }}!<span>智能报告,洞察未来。</span></h1>
|
|
|
<p>今天是个创作的好日子,开始您的智能报告之旅吧</p>
|
|
|
+ <p class="welcome-version">v0.2.1</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -979,12 +981,14 @@
|
|
|
<!-- 规则工作流弹窗 -->
|
|
|
<el-dialog
|
|
|
v-model="showRuleWorkflow"
|
|
|
- :title="workflowTargetRule ? `编辑规则 - ${workflowTargetElement?.elementName || workflowTargetRule.elementKey}` : '新建规则'"
|
|
|
fullscreen
|
|
|
:close-on-click-modal="false"
|
|
|
:show-close="false"
|
|
|
class="rule-workflow-dialog"
|
|
|
>
|
|
|
+ <template #header="{ }">
|
|
|
+ <span style="display: none;"></span>
|
|
|
+ </template>
|
|
|
<RuleWorkflow
|
|
|
v-if="showRuleWorkflow && currentProjectId"
|
|
|
:key="workflowTargetRule?.id || 'new'"
|
|
|
@@ -1045,16 +1049,31 @@ const leftPanelTab = ref('projects')
|
|
|
const sidebarShowAll = ref(false)
|
|
|
|
|
|
const recentActivities = computed(() => {
|
|
|
- // 基于项目列表生成最近操作(后续可接入真实 API)
|
|
|
+ // 基于项目列表生成最近操作 + 额外 mock 数据
|
|
|
const acts = []
|
|
|
- for (const p of projects.value.slice(0, 5)) {
|
|
|
+
|
|
|
+ // 从项目列表生成
|
|
|
+ for (const p of projects.value.slice(0, 3)) {
|
|
|
acts.push({
|
|
|
- text: `${p.title}`,
|
|
|
+ text: `打开项目「${p.title}」`,
|
|
|
source: `@${userName.value}`,
|
|
|
time: formatTime(p.updatedAt || p.createdAt)
|
|
|
})
|
|
|
}
|
|
|
- return acts
|
|
|
+
|
|
|
+ // 额外 mock 数据 - 模拟各种操作类型
|
|
|
+ const mockActivities = [
|
|
|
+ { text: '修改要素「评审对象」的值', source: '@张三', time: '10分钟前' },
|
|
|
+ { text: '执行规则「评审得分提取」', source: '@系统', time: '15分钟前' },
|
|
|
+ { text: '上传附件「核心要素评审记录表.docx」', source: '@李四', time: '30分钟前' },
|
|
|
+ { text: '新建规则「评审期自动计算」', source: '@张三', time: '1小时前' },
|
|
|
+ { text: '导出报告「成都院复审报告」', source: '@王五', time: '2小时前' },
|
|
|
+ { text: '修改要素「评审结论级别」的值', source: '@李四', time: '3小时前' },
|
|
|
+ { text: '删除附件「旧版评审表.xlsx」', source: '@张三', time: '昨天 16:30' },
|
|
|
+ { text: '创建项目「西南院标准化复审」', source: '@管理员', time: '昨天 10:15' },
|
|
|
+ ]
|
|
|
+
|
|
|
+ return [...acts, ...mockActivities].slice(0, 10)
|
|
|
})
|
|
|
|
|
|
const projects = ref([])
|
|
|
@@ -1162,12 +1181,50 @@ function scrollToElement(item) {
|
|
|
const docPaper = docPaperRef.value
|
|
|
if (!docPaper) return
|
|
|
|
|
|
- const selector = `[data-element-key="${item.elementKey}"]`
|
|
|
+ // 高亮系统使用 data-elem-key 属性
|
|
|
+ const selector = `[data-elem-key="${item.elementKey}"]`
|
|
|
const el = docPaper.querySelector(selector)
|
|
|
if (el) {
|
|
|
+ // 使用 IntersectionObserver 监听元素进入视口后再闪烁
|
|
|
+ const observer = new IntersectionObserver((entries) => {
|
|
|
+ entries.forEach(entry => {
|
|
|
+ if (entry.isIntersecting) {
|
|
|
+ observer.disconnect()
|
|
|
+ // 元素已在视口中,延迟一点再闪烁确保滚动稳定
|
|
|
+ setTimeout(() => {
|
|
|
+ el.classList.add('elem-flash')
|
|
|
+ setTimeout(() => el.classList.remove('elem-flash'), 1500)
|
|
|
+ // 触发点击以打开弹窗
|
|
|
+ el.click()
|
|
|
+ }, 100)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }, { threshold: 0.5 })
|
|
|
+
|
|
|
+ observer.observe(el)
|
|
|
el.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
|
|
- // 触发点击以打开弹窗
|
|
|
- el.click()
|
|
|
+
|
|
|
+ // 超时保护:如果 3 秒内没有触发,强制执行
|
|
|
+ setTimeout(() => {
|
|
|
+ observer.disconnect()
|
|
|
+ }, 3000)
|
|
|
+ } else {
|
|
|
+ // 如果没有找到高亮元素,尝试在文档中搜索值文本
|
|
|
+ if (item.valueText) {
|
|
|
+ const textToFind = item.valueText.slice(0, 50) // 取前50个字符搜索
|
|
|
+ const walker = document.createTreeWalker(docPaper, NodeFilter.SHOW_TEXT, null, false)
|
|
|
+ let node
|
|
|
+ while (node = walker.nextNode()) {
|
|
|
+ if (node.textContent.includes(textToFind)) {
|
|
|
+ const range = document.createRange()
|
|
|
+ range.selectNodeContents(node)
|
|
|
+ const rect = range.getBoundingClientRect()
|
|
|
+ node.parentElement?.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
|
|
+ break
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ElMessage.info(`未找到要素「${item.elementName}」的高亮位置`)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -1572,11 +1629,11 @@ async function openSourceInViewer(inp, rule = null) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // 查找附件
|
|
|
- const att = attachments.value.find(a => a.id === attId)
|
|
|
- console.log('[openSourceInViewer] 找到附件:', att)
|
|
|
+ // 查找附件(兼容 id 类型差异)
|
|
|
+ const att = attachments.value.find(a => String(a.id) === String(attId))
|
|
|
+ console.log('[openSourceInViewer] 找到附件:', att, '| attachments:', attachments.value.map(a => ({ id: a.id, type: typeof a.id })))
|
|
|
if (!att) {
|
|
|
- ElMessage.warning('未找到来源附件')
|
|
|
+ ElMessage.warning('未找到来源附件,sourceNodeId=' + attId)
|
|
|
return
|
|
|
}
|
|
|
|
|
|
@@ -2978,7 +3035,14 @@ async function loadAttachmentFile(att, fallbackName = 'file') {
|
|
|
const resp = await fetch(url, { headers })
|
|
|
if (!resp.ok) throw new Error(`下载失败(${resp.status})`)
|
|
|
const blob = await resp.blob()
|
|
|
- const file = new File([blob], att.displayName || att.fileName || fallbackName, {
|
|
|
+ // 优先使用带扩展名的fileName,否则用displayName+扩展名
|
|
|
+ let fileName = att.fileName || fallbackName
|
|
|
+ if (!fileName && att.displayName && att.fileType) {
|
|
|
+ fileName = `${att.displayName}.${att.fileType}`
|
|
|
+ } else if (!fileName && att.displayName) {
|
|
|
+ fileName = att.displayName
|
|
|
+ }
|
|
|
+ const file = new File([blob], fileName, {
|
|
|
type: blob.type || 'application/octet-stream'
|
|
|
})
|
|
|
attachmentFileCache.set(att.id, file)
|
|
|
@@ -3237,7 +3301,8 @@ async function parseZipEntry(zf) {
|
|
|
zf.parseResult = result.html || ''
|
|
|
zf.isHtml = true
|
|
|
} else if (ext === 'pdf' || ['png', 'jpg', 'jpeg'].includes(ext)) {
|
|
|
- // PDF/图片: 提取为 blob,发送到 GPU 解析服务
|
|
|
+ // PDF/图片: GPU 解析服务当前不可用
|
|
|
+ throw new Error('PDF/图片解析需要 GPU 服务,暂不可用')
|
|
|
const blob = await zipEntry.async('blob')
|
|
|
const mimeMap = { pdf: 'application/pdf', png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg' }
|
|
|
const file = new File([blob], zf.name.split('/').pop(), { type: mimeMap[ext] || 'application/octet-stream' })
|
|
|
@@ -3498,6 +3563,16 @@ async function handleParseAttachment(att) {
|
|
|
return
|
|
|
}
|
|
|
|
|
|
+ // PDF/图片:GPU 解析服务当前不可用,提前拦截
|
|
|
+ const gpuExts = ['pdf', 'png', 'jpg', 'jpeg', 'gif', 'bmp']
|
|
|
+ if (gpuExts.includes(ext)) {
|
|
|
+ const label = ext === 'pdf' ? 'PDF' : '图片'
|
|
|
+ state.status = 'idle'
|
|
|
+ state.progress = ''
|
|
|
+ ElMessage.warning(`${label}文件解析需要 GPU 服务,暂不可用`)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
try {
|
|
|
// 1. 获取后端持久化的原始文件
|
|
|
state.status = 'uploading'
|
|
|
@@ -4764,6 +4839,12 @@ onMounted(async () => {
|
|
|
color: var(--text-3);
|
|
|
line-height: 1.6;
|
|
|
}
|
|
|
+
|
|
|
+ .welcome-version {
|
|
|
+ margin-top: 24px;
|
|
|
+ font-size: 12px;
|
|
|
+ color: var(--text-4, #bbb);
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -7173,9 +7254,31 @@ onMounted(async () => {
|
|
|
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1);
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
</style>
|
|
|
|
|
|
<style lang="scss">
|
|
|
+// ==========================================
|
|
|
+// 点击要素时的闪烁效果(非 scoped,应用于动态生成的 HTML)
|
|
|
+// ==========================================
|
|
|
+.elem-flash {
|
|
|
+ animation: elem-flash-anim 0.4s ease-in-out 3 !important;
|
|
|
+ position: relative;
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes elem-flash-anim {
|
|
|
+ 0%, 100% {
|
|
|
+ outline: 3px solid rgba(24, 144, 255, 0.4);
|
|
|
+ outline-offset: 2px;
|
|
|
+ background-color: rgba(24, 144, 255, 0.1) !important;
|
|
|
+ }
|
|
|
+ 50% {
|
|
|
+ outline: 5px solid rgba(24, 144, 255, 0.9);
|
|
|
+ outline-offset: 4px;
|
|
|
+ background-color: rgba(24, 144, 255, 0.25) !important;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
// ==========================================
|
|
|
// 附件/规则居中悬浮弹窗样式
|
|
|
// ==========================================
|
|
|
@@ -7817,19 +7920,21 @@ onMounted(async () => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// 规则工作流弹窗样式
|
|
|
+// 规则工作流弹窗样式 - 完全隐藏 header
|
|
|
.rule-workflow-dialog {
|
|
|
- :deep(.el-dialog__body) {
|
|
|
- padding: 0;
|
|
|
- height: 100vh;
|
|
|
- overflow: hidden;
|
|
|
+ &.el-dialog .el-dialog__header,
|
|
|
+ .el-dialog__header,
|
|
|
+ > .el-dialog__header,
|
|
|
+ > header {
|
|
|
+ display: none !important;
|
|
|
}
|
|
|
|
|
|
- :deep(.el-dialog__header) {
|
|
|
- display: none !important;
|
|
|
- height: 0 !important;
|
|
|
+ &.el-dialog .el-dialog__body,
|
|
|
+ .el-dialog__body,
|
|
|
+ > .el-dialog__body {
|
|
|
padding: 0 !important;
|
|
|
- margin: 0 !important;
|
|
|
+ height: 100vh !important;
|
|
|
+ overflow: hidden !important;
|
|
|
}
|
|
|
}
|
|
|
</style>
|