|
@@ -0,0 +1,4139 @@
|
|
|
|
|
+<!DOCTYPE html>
|
|
|
|
|
+<html lang="zh-CN">
|
|
|
|
|
+<head>
|
|
|
|
|
+ <meta charset="UTF-8">
|
|
|
|
|
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
|
+ <title>灵越智报 - 智能报告生成平台</title>
|
|
|
|
|
+ <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
|
|
|
+ <!-- 引入 iconfont 项目(已替换) -->
|
|
|
|
|
+ <link rel="stylesheet" href="http://at.alicdn.com/t/c/font_2630279_pgouo6fulk.css">
|
|
|
|
|
+ <style>
|
|
|
|
|
+ :root {
|
|
|
|
|
+ --primary: #1890ff;
|
|
|
|
|
+ --primary-dark: #096dd9;
|
|
|
|
|
+ --primary-light: #e6f7ff;
|
|
|
|
|
+ --primary-gradient: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
|
|
|
|
|
+ --white: #ffffff;
|
|
|
|
|
+ --bg: #f5f7fa;
|
|
|
|
|
+ --border: #e8e8e8;
|
|
|
|
|
+ --text1: #262626;
|
|
|
|
|
+ --text2: #595959;
|
|
|
|
|
+ --text3: #8c8c8c;
|
|
|
|
|
+ --success: #52c41a;
|
|
|
|
|
+ --warning: #faad14;
|
|
|
|
|
+ --danger: #ff4d4f;
|
|
|
|
|
+ --ai-gradient: linear-gradient(135deg, #1890ff 0%, #722ed1 100%);
|
|
|
|
|
+ --data-gradient: linear-gradient(135deg, #52c41a 0%, #13c2c2 100%);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /* JavaScript for syncing editor title was moved out of the style block to avoid invalid CSS. */
|
|
|
|
|
+
|
|
|
|
|
+ * { margin: 0; padding: 0; box-sizing: border-box; }
|
|
|
|
|
+ body { font-family: 'Noto Sans SC', sans-serif; background: var(--bg); color: var(--text1); font-size: 14px; line-height: 1.6; height: 100vh; overflow: hidden; }
|
|
|
|
|
+
|
|
|
|
|
+ /* 全局头部 */
|
|
|
|
|
+ .header { position: fixed; top: 0; left: 0; right: 0; height: 56px; background: var(--white); border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; padding: 0 20px; z-index: 1000; }
|
|
|
|
|
+ .header-left { display: flex; align-items: center; gap: 20px; }
|
|
|
|
|
+ .logo { display: flex; align-items: center; gap: 8px; font-size: 17px; font-weight: 600; color: var(--primary); cursor: pointer; }
|
|
|
|
|
+ .logo-icon { width: 32px; height: 32px; background: var(--primary-gradient); border-radius: 8px; display: flex; align-items: center; justify-content: center; color: white; font-size: 18px; }
|
|
|
|
|
+ .search-box { position: relative; width: 320px; }
|
|
|
|
|
+ .search-input { width: 100%; height: 36px; padding: 0 14px 0 36px; border: 1px solid var(--border); border-radius: 18px; font-size: 13px; background: var(--bg); outline: none; transition: all 0.2s; }
|
|
|
|
|
+ .search-input:focus { background: var(--white); border-color: var(--primary); }
|
|
|
|
|
+ .search-icon { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: #8c8c8c; font-size: 14px; }
|
|
|
|
|
+ .header-right { display: flex; align-items: center; gap: 10px; }
|
|
|
|
|
+ .hd-btn { position: relative; width: 36px; height: 36px; border: none; background: transparent; border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 18px; color: var(--text2); transition: all 0.2s; }
|
|
|
|
|
+ .hd-btn:hover { background: var(--bg); color: var(--primary); }
|
|
|
|
|
+ .badge { position: absolute; top: 2px; right: 2px; min-width: 16px; height: 16px; background: var(--danger); color: white; border-radius: 8px; font-size: 10px; display: flex; align-items: center; justify-content: center; }
|
|
|
|
|
+ .user-menu { display: flex; align-items: center; gap: 6px; padding: 4px 8px 4px 4px; border-radius: 20px; cursor: pointer; }
|
|
|
|
|
+ .user-menu:hover { background: var(--bg); }
|
|
|
|
|
+ .avatar { width: 30px; height: 30px; border-radius: 50%; background: var(--primary-gradient); color: white; display: flex; align-items: center; justify-content: center; font-weight: 600; font-size: 12px; }
|
|
|
|
|
+ .user-name { font-size: 13px; font-weight: 500; }
|
|
|
|
|
+
|
|
|
|
|
+ /* 侧边栏 */
|
|
|
|
|
+ .sidebar { position: fixed; top: 56px; left: 0; width: 240px; height: calc(100vh - 56px); background: var(--white); border-right: none; display: flex; flex-direction: column; transition: width 0.3s; z-index: 900; overflow-y: auto; }
|
|
|
|
|
+ .sidebar.hidden { display: none; }
|
|
|
|
|
+ .sidebar-menu { flex: 1; padding: 10px; overflow-y: auto; }
|
|
|
|
|
+ .menu-item { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: 8px; cursor: pointer; transition: all 0.2s; margin-bottom: 2px; position: relative; }
|
|
|
|
|
+ .menu-item:hover { background: var(--primary-light); color: var(--primary); }
|
|
|
|
|
+ .menu-item.active { background: var(--primary-light); color: var(--primary); }
|
|
|
|
|
+ .menu-item.active::before { content: ''; position: absolute; left: 0; top: 50%; transform: translateY(-50%); width: 3px; height: 18px; background: var(--primary); border-radius: 0 2px 2px 0; }
|
|
|
|
|
+ .menu-icon { font-size: 16px; }
|
|
|
|
|
+ .menu-text { font-size: 13px; font-weight: 500; }
|
|
|
|
|
+ .menu-badge { min-width: 18px; height: 18px; padding: 0 5px; background: var(--primary); color: white; border-radius: 9px; font-size: 10px; display: flex; align-items: center; justify-content: center; }
|
|
|
|
|
+
|
|
|
|
|
+ /* 主内容 */
|
|
|
|
|
+ .main { margin-left: 300px; margin-top: 56px; height: calc(100vh - 56px); overflow-y: auto; display:flex; flex-direction:column; }
|
|
|
|
|
+ .page { display: none; padding: 20px; }
|
|
|
|
|
+ .page.active { display: block; }
|
|
|
|
|
+
|
|
|
|
|
+ /* 通用组件 */
|
|
|
|
|
+ .card { background: var(--white); border-radius: 10px; box-shadow: 0 2px 8px rgba(0,0,0,0.06); }
|
|
|
|
|
+ .btn { display: inline-flex; align-items: center; gap: 4px; padding: 7px 14px; border: 1px solid var(--border); background: var(--white); border-radius: 6px; cursor: pointer; font-size: 12px; transition: all 0.2s; }
|
|
|
|
|
+ .btn:hover { border-color: var(--primary); color: var(--primary); }
|
|
|
|
|
+ .btn-primary { background: var(--primary-gradient); color: white; border: none; }
|
|
|
|
|
+ .btn-primary:hover { box-shadow: 0 4px 12px rgba(24,144,255,0.4); }
|
|
|
|
|
+
|
|
|
|
|
+ /* 首页样式 */
|
|
|
|
|
+ .welcome h1 { font-size: 24px; font-weight: 600; margin-bottom: 4px; }
|
|
|
|
|
+ .welcome h1 span { background: var(--ai-gradient); background-clip: text; -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
|
|
|
|
|
+ .welcome p { color: var(--text2); }
|
|
|
|
|
+ .stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin: 20px 0; }
|
|
|
|
|
+ .stat-card { padding: 18px; cursor: pointer; transition: all 0.3s; }
|
|
|
|
|
+ .stat-card:hover { transform: translateY(-4px); box-shadow: 0 8px 20px rgba(0,0,0,0.1); }
|
|
|
|
|
+ .stat-icon { width: 40px; height: 40px; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 20px; margin-bottom: 10px; }
|
|
|
|
|
+ .stat-icon.blue { background: linear-gradient(135deg, #e6f7ff, #bae7ff); }
|
|
|
|
|
+ .stat-icon.purple { background: linear-gradient(135deg, #f9f0ff, #efdbff); }
|
|
|
|
|
+ .stat-icon.green { background: linear-gradient(135deg, #f6ffed, #d9f7be); }
|
|
|
|
|
+ .stat-icon.orange { background: linear-gradient(135deg, #fff7e6, #ffe7ba); }
|
|
|
|
|
+ .stat-value { font-size: 26px; font-weight: 700; }
|
|
|
|
|
+ .stat-label { font-size: 13px; color: var(--text2); margin-bottom: 6px; }
|
|
|
|
|
+ .stat-trend { font-size: 12px; }
|
|
|
|
|
+ .stat-trend.up { color: var(--success); }
|
|
|
|
|
+
|
|
|
|
|
+ /* AI对话入口 */
|
|
|
|
|
+ .ai-card { padding: 20px; margin-bottom: 20px; }
|
|
|
|
|
+ .ai-welcome { background: linear-gradient(135deg, #f0f7ff, #f5f0ff); border-radius: 10px; padding: 16px; margin-bottom: 16px; }
|
|
|
|
|
+ .ai-avatar { width: 44px; height: 44px; background: var(--ai-gradient); border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 22px; margin-bottom: 10px; }
|
|
|
|
|
+ .ai-title { font-size: 14px; font-weight: 600; margin-bottom: 8px; }
|
|
|
|
|
+ .ai-list { list-style: none; margin-bottom: 10px; }
|
|
|
|
|
+ .ai-list li { color: var(--text2); padding: 3px 0; font-size: 13px; }
|
|
|
|
|
+ .ai-list li::before { content: '•'; color: var(--primary); margin-right: 8px; }
|
|
|
|
|
+ .ai-tip { font-size: 12px; color: var(--text3); padding: 8px 12px; background: rgba(255,255,255,0.7); border-radius: 6px; border-left: 3px solid var(--primary); }
|
|
|
|
|
+ .ai-input-wrap { position: relative; margin-bottom: 14px; }
|
|
|
|
|
+ .ai-input { width: 100%; height: 46px; padding: 0 100px 0 18px; border: 2px solid var(--border); border-radius: 23px; font-size: 14px; outline: none; transition: all 0.3s; }
|
|
|
|
|
+ .ai-input:focus { border-color: var(--primary); }
|
|
|
|
|
+ .ai-input-btns { position: absolute; right: 6px; top: 50%; transform: translateY(-50%); display: flex; gap: 4px; }
|
|
|
|
|
+ .ai-input-btn { width: 32px; height: 32px; border: none; background: transparent; border-radius: 50%; cursor: pointer; font-size: 16px; color: var(--text3); }
|
|
|
|
|
+ .ai-input-btn:hover { background: var(--bg); color: var(--primary); }
|
|
|
|
|
+ .ai-input-btn.send { background: var(--primary-gradient); color: white; display: none; }
|
|
|
|
|
+ .ai-input-btn.send.show { display: flex; align-items: center; justify-content: center; }
|
|
|
|
|
+ .thinking-modes { display: flex; gap: 8px; flex-wrap: wrap; }
|
|
|
|
|
+ .mode-btn { padding: 6px 12px; background: var(--bg); border: 1px solid var(--border); border-radius: 18px; font-size: 12px; cursor: pointer; }
|
|
|
|
|
+ .mode-btn:hover { border-color: var(--primary); background: var(--primary-light); }
|
|
|
|
|
+ .mode-btn.active { background: var(--primary-light); border-color: var(--primary); color: var(--primary); }
|
|
|
|
|
+
|
|
|
|
|
+ /* 模板区 */
|
|
|
|
|
+ .section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 14px; }
|
|
|
|
|
+ .section-title { font-size: 15px; font-weight: 600; }
|
|
|
|
|
+ .section-link { font-size: 13px; color: var(--primary); cursor: pointer; }
|
|
|
|
|
+ .tpl-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; margin-bottom: 14px; }
|
|
|
|
|
+ .tpl-card { overflow: hidden; cursor: pointer; transition: all 0.3s; }
|
|
|
|
|
+ .tpl-card:hover { transform: translateY(-4px); box-shadow: 0 8px 20px rgba(0,0,0,0.1); }
|
|
|
|
|
+ .tpl-preview { height: 100px; background: linear-gradient(135deg, #f5f7fa, #e8ecf0); display: flex; align-items: center; justify-content: center; font-size: 36px; }
|
|
|
|
|
+ .tpl-info { padding: 14px; }
|
|
|
|
|
+ .tpl-name { font-weight: 600; margin-bottom: 6px; }
|
|
|
|
|
+ .tpl-meta { display: flex; gap: 12px; font-size: 11px; color: var(--text3); margin-bottom: 10px; }
|
|
|
|
|
+ .tpl-tags { display: flex; gap: 4px; margin-bottom: 10px; }
|
|
|
|
|
+ .tpl-tag { padding: 2px 6px; background: var(--primary-light); color: var(--primary); border-radius: 3px; font-size: 10px; }
|
|
|
|
|
+ .tpl-tag.hot { background: #fff1f0; color: var(--danger); }
|
|
|
|
|
+ .tpl-btn { width: 100%; padding: 8px; background: var(--primary-gradient); color: white; border: none; border-radius: 6px; font-size: 12px; cursor: pointer; }
|
|
|
|
|
+ .quick-actions { display: grid; grid-template-columns: repeat(2, 1fr); gap: 14px; }
|
|
|
|
|
+ .quick-action { display: flex; align-items: center; gap: 12px; padding: 16px; border: 2px dashed var(--border); border-radius: 10px; cursor: pointer; }
|
|
|
|
|
+ .quick-action:hover { border-color: var(--primary); background: var(--primary-light); }
|
|
|
|
|
+ .quick-action-icon { width: 40px; height: 40px; background: var(--bg); border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 20px; }
|
|
|
|
|
+
|
|
|
|
|
+ /* 编辑器专用样式 */
|
|
|
|
|
+ .editor-page { display: none; position: fixed; top: 56px; left: 0; right: 0; bottom: 0; background: var(--bg); z-index: 800; overflow: hidden; }
|
|
|
|
|
+ .editor-page.active { display: block; }
|
|
|
|
|
+
|
|
|
|
|
+ /* 编辑器工具栏 */
|
|
|
|
|
+ .editor-toolbar { height: 56px; background: var(--white); border-bottom: 1px solid var(--border); display: flex; align-items: center; padding: 0 16px; gap: 16px; flex-shrink: 0; }
|
|
|
|
|
+ .back-btn { display: flex; align-items: center; gap: 4px; padding: 8px 12px; border: none; background: transparent; border-radius: 6px; cursor: pointer; font-size: 13px; color: var(--text2); }
|
|
|
|
|
+ .back-btn:hover { background: var(--bg); color: var(--primary); }
|
|
|
|
|
+ .report-title-input { border: none; background: transparent; font-size: 15px; font-weight: 600; padding: 8px 12px; border-radius: 6px; min-width: 300px; outline: none; }
|
|
|
|
|
+ .report-title-input:hover { background: var(--bg); }
|
|
|
|
|
+ .report-title-input:focus { background: var(--white); box-shadow: 0 0 0 2px var(--primary-light); }
|
|
|
|
|
+ .save-status { display: flex; align-items: center; gap: 4px; font-size: 13px; color: var(--success); }
|
|
|
|
|
+ .toolbar-right { margin-left: auto; display: flex; gap: 10px; align-items: center; }
|
|
|
|
|
+ .toolbar-btn { display: flex; align-items: center; gap: 6px; padding: 8px 16px; border: 1px solid var(--border); background: var(--white); border-radius: 6px; cursor: pointer; font-size: 13px; color: var(--text1); transition: all 0.2s; }
|
|
|
|
|
+ .toolbar-btn:hover { border-color: var(--primary); color: var(--primary); background: var(--primary-light); }
|
|
|
|
|
+ .toolbar-btn.primary { background: var(--primary); color: white; border: none; }
|
|
|
|
|
+ .toolbar-btn.primary:hover { background: var(--primary-dark); }
|
|
|
|
|
+ .toolbar-btn .icon { font-size: 14px; }
|
|
|
|
|
+ .toolbar-divider { width: 1px; height: 24px; background: var(--border); }
|
|
|
|
|
+
|
|
|
|
|
+ /* 编辑器主体 */
|
|
|
|
|
+ .editor-body { flex: 1; display: flex; overflow: hidden; min-height: 0; }
|
|
|
|
|
+
|
|
|
|
|
+ /* 当编辑器激活时,使用三列布局:左侧资源、中心编辑、右侧AI助手(与示例一致) */
|
|
|
|
|
+ .editor-page.active .editor-body {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: 300px 1fr 300px;
|
|
|
|
|
+ gap: 16px;
|
|
|
|
|
+ padding: 16px;
|
|
|
|
|
+ /* ensure each column stretches to the full grid height */
|
|
|
|
|
+ align-items: stretch;
|
|
|
|
|
+ align-content: stretch;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+ height: calc(100vh - 56px);
|
|
|
|
|
+ /* ensure grid row(s) fill available space so children can stretch */
|
|
|
|
|
+ grid-auto-rows: 1fr;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /* 左侧项目文件面板 */
|
|
|
|
|
+ .left-panel { width: 300px; background: var(--white); border-right: 1px solid var(--border); display: flex; flex-direction: column; min-height: 0; overflow: hidden; height: 100%; }
|
|
|
|
|
+ .panel-header { padding: 14px 16px; border-bottom: 1px solid var(--border); font-size: 13px; font-weight: 600; color: var(--text1); display: flex; align-items: center; justify-content: space-between; }
|
|
|
|
|
+ .panel-header-tabs .tabs-left { display:flex; gap:8px; align-items:center; }
|
|
|
|
|
+ .panel-tab { padding:8px 14px; border-radius:12px; border:1px solid transparent; background:transparent; cursor:pointer; font-weight:700; font-size:14px; }
|
|
|
|
|
+ .panel-tab.active { background: var(--primary); color: #fff; border-color: rgba(0,0,0,0.04); box-shadow: 0 6px 18px rgba(17,24,39,0.06); }
|
|
|
|
|
+ .panel-header .file-count { margin-left:8px; font-weight:500; color:var(--text3); }
|
|
|
|
|
+ .file-count { font-size: 12px; color: var(--text3); font-weight: normal; }
|
|
|
|
|
+ .panel-body { flex: 1; padding: 12px; display: flex; flex-direction: column; min-height: 0; }
|
|
|
|
|
+ /* docs / recent split: top 40% docs (scrollable), bottom 60% recent (scrollable) */
|
|
|
|
|
+ .docs-area { flex: 4; overflow-y: auto; min-height: 0; }
|
|
|
|
|
+ .recent-area { flex: 6; overflow-y: auto; min-height: 0; margin-top: 8px; }
|
|
|
|
|
+
|
|
|
|
|
+ /* 上传区 */
|
|
|
|
|
+ .upload-zone { border: 2px dashed var(--border); border-radius: 12px; height:40px; padding: 0 12px; text-align: center; cursor: pointer; margin-bottom: 16px; transition: all 0.18s; display:flex; align-items:center; justify-content:center; background: var(--white); }
|
|
|
|
|
+ .upload-zone:hover { border-color: var(--primary); background: var(--primary-light); }
|
|
|
|
|
+ /* 文本式添加资源(居中、大按钮感) */
|
|
|
|
|
+ .upload-zone .add-resource-text { font-size:16px; font-weight:600; color:var(--text1); padding:0 12px; line-height:40px; }
|
|
|
|
|
+ .upload-hint { font-size: 11px; color: var(--text3); display:block; margin-top:8px; text-align:center; }
|
|
|
|
|
+
|
|
|
|
|
+ /* 左侧面板折叠按钮 */
|
|
|
|
|
+ .panel-toggle-btn { background: transparent; border: 1px solid var(--border); border-radius: 8px; padding:6px 8px; cursor:pointer; font-size:14px; color:var(--text2); }
|
|
|
|
|
+ .panel-toggle-btn:hover { border-color: var(--primary); color:var(--primary); }
|
|
|
|
|
+ /* 小浮动展开按钮(当面板折叠时显示) */
|
|
|
|
|
+ .resource-expand-btn { position: fixed; left: 8px; top: 100px; width:40px; height:40px; border-radius:8px; background:var(--white); border:1px solid var(--border); display:flex; align-items:center; justify-content:center; z-index:1400; box-shadow:0 6px 18px rgba(0,0,0,0.08); cursor:pointer; }
|
|
|
|
|
+
|
|
|
|
|
+ /* 在正文中隐藏来源徽章(仅编辑区不显示来源文件标识) */
|
|
|
|
|
+ .editor-content .source-badge { display: none !important; }
|
|
|
|
|
+
|
|
|
|
|
+ /* 新:左侧文档列表样式(图片示例风格) - 内部旧 tab 样式已清理 */
|
|
|
|
|
+ .doc-section-header { display:flex; align-items:center; justify-content:space-between; padding:6px 4px 10px; font-size:13px; font-weight:600; color:var(--text1); }
|
|
|
|
|
+ .badge-count { background: var(--bg); padding:2px 8px; border-radius:12px; font-size:12px; color:var(--primary); }
|
|
|
|
|
+ .doc-list { display:flex; flex-direction:column; gap:10px; padding:4px 0 12px; }
|
|
|
|
|
+ .doc-card { display:flex; gap:10px; align-items:center; padding:10px; background:var(--white); border-radius:10px; border:1px solid var(--border); cursor:pointer; }
|
|
|
|
|
+ .doc-card .doc-thumb { width:40px; height:40px; border-radius:6px; background:var(--bg); display:flex; align-items:center; justify-content:center; font-size:18px; color:var(--text3); }
|
|
|
|
|
+ /* file badge styles for attachment icons (PDF/DOC/XLS/MD) */
|
|
|
|
|
+ .file-badge { display:inline-flex; width:40px; height:40px; border-radius:8px; align-items:center; justify-content:center; color:#fff; font-weight:700; font-size:13px; flex:0 0 40px; }
|
|
|
|
|
+ .file-badge.pdf { background: #ff6b6b; }
|
|
|
|
|
+ .file-badge.doc { background: #4dabf7; }
|
|
|
|
|
+ .file-badge.xls { background: #73d13d; }
|
|
|
|
|
+ .file-badge.md { background: #9254de; }
|
|
|
|
|
+ .doc-card .doc-meta { display:flex; flex-direction:column; }
|
|
|
|
|
+ .doc-card .doc-title { font-weight:600; font-size:13px; color:var(--text1); }
|
|
|
|
|
+ .doc-card .doc-time { font-size:11px; color:var(--text3); margin-top:4px; }
|
|
|
|
|
+ .recent-section { margin-top:12px; padding-top:8px; border-top:1px dashed var(--border); }
|
|
|
|
|
+ .recent-list { display:flex; flex-direction:column; padding-top:8px; }
|
|
|
|
|
+ .recent-item { padding:8px 10px; background:transparent; border:none; font-size:12px; color:var(--text2); border-radius:4px; transition: all 0.2s ease; }
|
|
|
|
|
+ .recent-item:hover { background:#f5f5f5; }
|
|
|
|
|
+
|
|
|
|
|
+ /* 文件预览模态(大弹窗) */
|
|
|
|
|
+ .file-preview-modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.45); display: flex; align-items: center; justify-content: center; z-index: 1600; }
|
|
|
|
|
+ .file-preview-modal { width: 90%; max-width: 1000px; max-height: 90vh; overflow: auto; background: var(--white); border-radius: 14px; box-shadow: 0 30px 80px rgba(10,30,60,0.35); padding: 20px; position: relative; }
|
|
|
|
|
+ .file-preview-modal .modal-header { display:flex; align-items:center; justify-content:space-between; gap:12px; margin-bottom:12px; }
|
|
|
|
|
+ .file-preview-modal .modal-title { font-weight:700; font-size:16px; color:var(--text1); }
|
|
|
|
|
+ .file-preview-modal .modal-body { color:var(--text1); line-height:1.7; white-space:pre-wrap; font-size:14px; }
|
|
|
|
|
+ .file-preview-close { position:absolute; right:12px; top:12px; background:transparent;border:none;font-size:18px;cursor:pointer;color:var(--text3); }
|
|
|
|
|
+
|
|
|
|
|
+ /* 文件项操作按钮(悬浮显示) */
|
|
|
|
|
+ .file-item { position: relative; }
|
|
|
|
|
+ .file-actions { position: absolute; right: 8px; top: 50%; transform: translateY(-50%); display: none; gap:6px; align-items:center; }
|
|
|
|
|
+ .file-item:hover .file-actions { display: flex; }
|
|
|
|
|
+ .file-actions .action-btn { background: var(--white); border: 1px solid var(--border); padding:6px; border-radius:6px; font-size:13px; cursor:pointer; display:flex; align-items:center; justify-content:center; width:34px; height:34px; box-shadow: 0 2px 6px rgba(0,0,0,0.06); }
|
|
|
|
|
+ .file-actions .action-btn:hover { border-color: var(--primary); color: var(--primary); }
|
|
|
|
|
+
|
|
|
|
|
+ /* 解析过程弹出框(更接近示例:大圆角、渐变背景、分段进度) */
|
|
|
|
|
+ .parsing-popover { position: fixed; width: 360px; background: linear-gradient(135deg, #f7fbff 0%, #e6f7ff 100%); border: 1px solid rgba(24,144,255,0.12); border-radius: 14px; box-shadow: 0 18px 48px rgba(15,35,70,0.18); z-index: 1500; padding: 14px; }
|
|
|
|
|
+ .parsing-popover .title { font-weight:700; margin-bottom:10px; color:var(--text1); display:flex; align-items:center; justify-content:space-between; }
|
|
|
|
|
+ .parsing-popover .parsing-progress { display:flex; gap:6px; margin-bottom:12px; }
|
|
|
|
|
+ .parsing-popover .parsing-progress .seg { flex:1; height:8px; background: rgba(255,255,255,0.6); border-radius:6px; transition: all 0.4s; box-shadow: inset 0 -2px 4px rgba(0,0,0,0.03); }
|
|
|
|
|
+ .parsing-popover .parsing-progress .seg.active { background: linear-gradient(90deg, rgba(24,144,255,0.95), rgba(58,150,255,0.8)); box-shadow: 0 6px 14px rgba(24,144,255,0.12); transform: translateY(-2px); }
|
|
|
|
|
+ .parsing-popover .steps-list { display:flex; flex-direction:column; gap:8px; margin-bottom:8px; }
|
|
|
|
|
+ .parsing-popover .step { display:flex; align-items:center; gap:12px; padding:10px 12px; border-radius:10px; background: rgba(255,255,255,0.6); }
|
|
|
|
|
+ .parsing-popover .step .dot { width:14px;height:14px;border-radius:50%;background:var(--bg); border:2px solid rgba(0,0,0,0.06); flex-shrink:0; }
|
|
|
|
|
+ .parsing-popover .step.completed { background: rgba(246,255,238,0.9); }
|
|
|
|
|
+ .parsing-popover .step.completed .dot { background: var(--success); border-color: transparent; box-shadow: 0 6px 20px rgba(82,200,26,0.12); }
|
|
|
|
|
+ .parsing-popover .step.active { background: rgba(240,248,255,0.95); }
|
|
|
|
|
+ .parsing-popover .step.active .dot { background: var(--primary); border-color: transparent; box-shadow: 0 6px 20px rgba(24,144,255,0.12); }
|
|
|
|
|
+ .parsing-popover .step.pending { background: rgba(250,250,250,0.9); }
|
|
|
|
|
+ .parsing-popover .step.pending .dot { background: #f0f0f0; border-color: rgba(0,0,0,0.06); }
|
|
|
|
|
+ .parsing-popover .step .label { font-size:13px; color:var(--text1); }
|
|
|
|
|
+ .parsing-popover .close-btn { position:absolute; right:10px; top:8px; cursor:pointer; font-size:14px; color:var(--text3); }
|
|
|
|
|
+
|
|
|
|
|
+ /* 文件分组 */
|
|
|
|
|
+ .file-group { margin-bottom: 16px; }
|
|
|
|
|
+ .file-group-header { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text3); margin-bottom: 8px; padding: 0 4px; }
|
|
|
|
|
+ .file-group-header .count { background: var(--bg); padding: 1px 6px; border-radius: 8px; font-size: 10px; }
|
|
|
|
|
+
|
|
|
|
|
+ /* 文件项 */
|
|
|
|
|
+ .file-item { display: flex; align-items: center; gap: 10px; padding: 10px 12px; background: var(--white); border: 1px solid var(--border); border-radius: 8px; margin-bottom: 8px; cursor: pointer; transition: all 0.2s; }
|
|
|
|
|
+ .file-item:hover { border-color: var(--primary); background: var(--primary-light); }
|
|
|
|
|
+ .file-item.active { border-color: var(--primary); background: var(--primary-light); }
|
|
|
|
|
+ .file-icon { font-size: 28px; flex-shrink: 0; }
|
|
|
|
|
+ .file-icon.pdf { color: #ff4d4f; }
|
|
|
|
|
+ .file-icon.docx { color: #1890ff; }
|
|
|
|
|
+ .file-icon.xlsx { color: #52c41a; }
|
|
|
|
|
+ .file-icon.md { color: #8c8c8c; }
|
|
|
|
|
+ .file-info { flex: 1; min-width: 0; }
|
|
|
|
|
+ .file-name { font-size: 12px; font-weight: 500; margin-bottom: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
|
|
|
+ .file-meta { display: flex; align-items: center; gap: 8px; font-size: 11px; color: var(--text3); }
|
|
|
|
|
+ .file-status { font-size: 11px; white-space: nowrap; }
|
|
|
|
|
+ .file-status.parsing { color: var(--primary); }
|
|
|
|
|
+ .file-status.done { color: var(--success); }
|
|
|
|
|
+
|
|
|
|
|
+ /* 中间编辑区 */
|
|
|
|
|
+ .center-panel { flex: 1; display: flex; flex-direction: column; background: var(--white); overflow: hidden; min-height: 0; height: 100%; }
|
|
|
|
|
+
|
|
|
|
|
+ /* 编辑器顶部标题栏 */
|
|
|
|
|
+ .editor-title-bar { padding: 16px 24px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 12px; }
|
|
|
|
|
+ .editor-main-title { font-size: 18px; font-weight: 600; flex: 1; cursor: pointer; }
|
|
|
|
|
+ /* 调整:圆角 4px,固定高度 24px,水平内边距保持 8px,垂直居中 */
|
|
|
|
|
+ .report-status { display:inline-block; margin-left:0; padding:0 8px; height:24px; line-height:24px; background:var(--primary-light); color:var(--primary); border-radius:4px; font-size:12px; font-weight:600; }
|
|
|
|
|
+ /* Icon hover tooltip */
|
|
|
|
|
+ .icon-tooltip {
|
|
|
|
|
+ position: fixed;
|
|
|
|
|
+ z-index: 4000;
|
|
|
|
|
+ background: rgba(0,0,0,0.78);
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ padding: 6px 8px;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ pointer-events: none;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ opacity: 0;
|
|
|
|
|
+ transition: opacity 120ms ease;
|
|
|
|
|
+ }
|
|
|
|
|
+ .view-toggle { display: flex; align-items: center; border: 1px solid var(--border); border-radius: 6px; overflow: hidden; }
|
|
|
|
|
+ .view-btn { padding: 8px 16px; border: none; background: var(--white); font-size: 13px; cursor: pointer; color: var(--text2); display: flex; align-items: center; gap: 6px; transition: all 0.2s; }
|
|
|
|
|
+ .view-btn:first-child { border-right: 1px solid var(--border); }
|
|
|
|
|
+ .view-btn:hover { background: var(--bg); }
|
|
|
|
|
+ .view-btn.active { background: var(--primary-light); color: var(--primary); font-weight: 500; }
|
|
|
|
|
+ .graph-btn { width: 36px; height: 36px; border: 1px solid var(--border); background: var(--white); border-radius: 6px; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 18px; margin-left: 8px; transition: all 0.2s; }
|
|
|
|
|
+ .graph-btn:hover { border-color: var(--primary); background: var(--primary-light); }
|
|
|
|
|
+ .graph-btn.active { border-color: var(--primary); background: var(--primary-light); color: var(--primary); }
|
|
|
|
|
+
|
|
|
|
|
+ /* 编辑器内容区 */
|
|
|
|
|
+ .editor-scroll { flex: 1; overflow-y: auto; padding: 40px 48px; }
|
|
|
|
|
+ .editor-content { max-width: 1000px; margin: 0 auto; }
|
|
|
|
|
+ .editor-content h1 { font-size: 24px; font-weight: 700; margin-bottom: 24px; color: var(--text1); }
|
|
|
|
|
+ .editor-content h2 { font-size: 18px; font-weight: 600; margin: 28px 0 16px; color: var(--text1); }
|
|
|
|
|
+ .editor-content h3 { font-size: 15px; font-weight: 600; margin: 20px 0 12px; color: var(--text1); }
|
|
|
|
|
+ .editor-content p { margin-bottom: 16px; line-height: 1.8; color: var(--text1); }
|
|
|
|
|
+
|
|
|
|
|
+ /* 实体高亮标记 */
|
|
|
|
|
+ .entity-highlight { display: inline; padding: 2px 6px; border: 1px solid var(--primary); border-radius: 4px; color: var(--primary); background: var(--primary-light); cursor: pointer; transition: all 0.2s; position: relative; }
|
|
|
|
|
+ .entity-highlight:hover { background: var(--primary); color: white; }
|
|
|
|
|
+
|
|
|
|
|
+ /* 不同类型实体的颜色样式,与右侧栏element-tag保持一致 */
|
|
|
|
|
+ .entity-highlight.entity { border-color: var(--primary); background: var(--primary-light); color: var(--primary); }
|
|
|
|
|
+ .entity-highlight.entity:hover { background: var(--primary); color: white; }
|
|
|
|
|
+
|
|
|
|
|
+ .entity-highlight.concept { border-color: #722ed1; background: #f9f0ff; color: #722ed1; }
|
|
|
|
|
+ .entity-highlight.concept:hover { background: #722ed1; color: white; }
|
|
|
|
|
+
|
|
|
|
|
+ .entity-highlight.data { border-color: var(--success); background: #f6ffed; color: var(--success); }
|
|
|
|
|
+ .entity-highlight.data:hover { background: var(--success); color: white; }
|
|
|
|
|
+
|
|
|
|
|
+ .entity-highlight.location { border-color: var(--warning); background: #fff7e6; color: var(--warning); }
|
|
|
|
|
+ .entity-highlight.location:hover { background: var(--warning); color: white; }
|
|
|
|
|
+
|
|
|
|
|
+ .entity-highlight.asset { border-color: #eb2f96; background: #fff0f6; color: #eb2f96; }
|
|
|
|
|
+ .entity-highlight.asset:hover { background: #eb2f96; color: white; }
|
|
|
|
|
+ /* 来源标识小徽章(AI / 附件 / 人工) */
|
|
|
|
|
+ .source-badge { display:inline-flex; align-items:center; justify-content:center; font-size:10px; padding:2px 6px; margin-left:6px; border-radius:10px; border:1px solid rgba(0,0,0,0.06); background:#fff; color:var(--text2); cursor:pointer; box-shadow:0 1px 2px rgba(0,0,0,0.04); }
|
|
|
|
|
+ .source-badge.ai { background: #fff7e6; color: var(--warning); border-color: rgba(255, 216, 128, 0.4); }
|
|
|
|
|
+ .source-badge.file { background: #f6ffed; color: var(--success); border-color: rgba(166, 230, 149, 0.45); }
|
|
|
|
|
+ .source-badge.manual { background: #f0f5ff; color: var(--primary); border-color: rgba(173, 199, 255, 0.45); }
|
|
|
|
|
+ .source-badge:hover { transform: translateY(-1px); }
|
|
|
|
|
+
|
|
|
|
|
+ /* AI优化建议卡片 */
|
|
|
|
|
+ .ai-suggestion-card { background: #fffbf0; border: 1px solid #ffe7ba; border-radius: 10px; padding: 16px; margin: 16px 0; }
|
|
|
|
|
+ .ai-suggestion-header { display: flex; align-items: center; gap: 6px; margin-bottom: 10px; }
|
|
|
|
|
+ .ai-suggestion-title { font-size: 13px; font-weight: 600; color: var(--warning); }
|
|
|
|
|
+ .ai-suggestion-content { font-size: 12px; line-height: 1.7; color: var(--text1); margin-bottom: 12px; }
|
|
|
|
|
+ .ai-suggestion-list { list-style: none; margin-bottom: 12px; }
|
|
|
|
|
+ .ai-suggestion-list li { padding: 4px 0; font-size: 12px; color: var(--text2); }
|
|
|
|
|
+ .ai-suggestion-list li::before { content: '•'; color: var(--primary); margin-right: 8px; }
|
|
|
|
|
+ .ai-suggestion-actions { display: flex; gap: 8px; }
|
|
|
|
|
+
|
|
|
|
|
+ /* 数据表格 */
|
|
|
|
|
+ .data-table-card { background: var(--white); border: 1px solid var(--border); border-radius: 10px; margin: 16px 0; overflow: hidden; }
|
|
|
|
|
+ .data-table-header { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; border-bottom: 1px solid var(--border); }
|
|
|
|
|
+ .data-table-title { display: flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 600; }
|
|
|
|
|
+ .data-table-source { font-size: 11px; color: var(--text3); }
|
|
|
|
|
+ .data-table { width: 100%; border-collapse: collapse; }
|
|
|
|
|
+ .data-table th { background: var(--bg); padding: 10px 16px; text-align: left; font-size: 12px; font-weight: 600; color: var(--text2); border-bottom: 1px solid var(--border); }
|
|
|
|
|
+ .data-table td { padding: 10px 16px; font-size: 13px; border-bottom: 1px solid var(--border); }
|
|
|
|
|
+ .data-table tr:last-child td { border-bottom: none; }
|
|
|
|
|
+ .data-table tr:hover td { background: var(--primary-light); }
|
|
|
|
|
+
|
|
|
|
|
+ /* 数据引用卡片 */
|
|
|
|
|
+ .data-reference-card { background: linear-gradient(135deg, #f0f9ff, #e6f7ff); border: 1px solid #bae7ff; border-radius: 10px; padding: 16px; margin: 16px 0; }
|
|
|
|
|
+ .reference-header { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
|
|
|
|
|
+ .reference-icon { font-size: 16px; }
|
|
|
|
|
+ .reference-title { font-size: 13px; font-weight: 600; color: var(--primary); }
|
|
|
|
|
+ .reference-content { display: flex; flex-direction: column; gap: 6px; }
|
|
|
|
|
+ .reference-item { display: flex; align-items: center; gap: 8px; font-size: 12px; }
|
|
|
|
|
+ .reference-label { color: var(--text3); font-weight: 500; min-width: 70px; }
|
|
|
|
|
+ .reference-value { color: var(--text1); }
|
|
|
|
|
+ .reference-value.success { color: var(--success); font-weight: 600; }
|
|
|
|
|
+
|
|
|
|
|
+ /* 竞争分析卡片 */
|
|
|
|
|
+ .competition-card { background: linear-gradient(135deg, #fffbe6, #fff7e6); border: 1px solid #ffe7ba; border-radius: 10px; padding: 16px; margin: 16px 0; }
|
|
|
|
|
+ .competition-header { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
|
|
|
|
|
+ .competition-icon { font-size: 16px; }
|
|
|
|
|
+ .competition-title { font-size: 13px; font-weight: 600; color: var(--warning); }
|
|
|
|
|
+ .competition-content { display: flex; flex-direction: column; gap: 10px; }
|
|
|
|
|
+ .competition-item { padding: 12px; background: rgba(255,255,255,0.7); border-radius: 8px; border-left: 3px solid var(--warning); }
|
|
|
|
|
+ .competitor-name { font-weight: 600; margin-bottom: 4px; }
|
|
|
|
|
+ .competitor-share { font-size: 12px; color: var(--text2); margin-bottom: 2px; }
|
|
|
|
|
+ .competitor-strength { font-size: 12px; color: var(--text3); }
|
|
|
|
|
+
|
|
|
|
|
+ /* AI生成内容卡片 */
|
|
|
|
|
+ .ai-generated-card { background: linear-gradient(135deg, #f9f0ff, #f5f0ff); border: 1px solid #efdbff; border-radius: 10px; padding: 16px; margin: 16px 0; }
|
|
|
|
|
+ .ai-generated-header { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
|
|
|
|
|
+ .ai-generated-icon { font-size: 16px; }
|
|
|
|
|
+ .ai-generated-title { font-size: 13px; font-weight: 600; color: var(--primary); }
|
|
|
|
|
+ .ai-generated-content { font-size: 13px; line-height: 1.6; }
|
|
|
|
|
+ .ai-generated-content ul { margin: 8px 0; padding-left: 20px; }
|
|
|
|
|
+ .ai-generated-content li { margin: 4px 0; }
|
|
|
|
|
+ .ai-generated-actions { display: flex; gap: 8px; margin-top: 12px; }
|
|
|
|
|
+
|
|
|
|
|
+ /* 右侧AI助手面板 */
|
|
|
|
|
+ .right-panel { width: 300px; background: var(--white); border-left: 1px solid var(--border); display: flex; flex-direction: column; min-height: 0; overflow: hidden; height: 100%; }
|
|
|
|
|
+ /* split right panel: top = report elements (~40%), bottom = AI assistant (~60%) */
|
|
|
|
|
+ .right-panel .element-section { flex: 4; overflow-y: auto; min-height: 0; }
|
|
|
|
|
+ .right-panel .ai-assistant { flex: 6; overflow-y: auto; min-height: 0; display: flex; flex-direction: column; }
|
|
|
|
|
+
|
|
|
|
|
+ /* 要素管理区 */
|
|
|
|
|
+ .element-section { padding: 16px; border-bottom: 1px dashed var(--border); padding-bottom: 16px !important; }
|
|
|
|
|
+ .element-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
|
|
|
|
|
+ .element-title { font-size: 13px; font-weight: 600; display: flex; align-items: center; gap: 6px; }
|
|
|
|
|
+ .element-count { font-size: 11px; color: var(--text3); font-weight: normal; }
|
|
|
|
|
+
|
|
|
|
|
+ /* 要素标签容器 */
|
|
|
|
|
+ .element-tags-wrap { display: flex; flex-wrap: wrap; gap: 8px; max-height: 200px; overflow-y: auto; padding-right: 4px; padding-bottom: 16px; }
|
|
|
|
|
+ .element-tags-wrap::-webkit-scrollbar { width: 4px; }
|
|
|
|
|
+ .element-tags-wrap::-webkit-scrollbar-track { background: var(--bg); border-radius: 2px; }
|
|
|
|
|
+ .element-tags-wrap::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
|
|
|
|
|
+ .element-tags-wrap::-webkit-scrollbar-thumb:hover { background: var(--text3); }
|
|
|
|
|
+
|
|
|
|
|
+ /* 要素标签样式 */
|
|
|
|
|
+ .element-tag { display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px; background: var(--bg); border: 1px solid var(--border); border-radius: 16px; font-size: 12px; cursor: grab; transition: all 0.2s; user-select: none; }
|
|
|
|
|
+ .element-tag:hover { border-color: var(--primary); background: var(--primary-light); transform: translateY(-1px); }
|
|
|
|
|
+ .element-tag:active { cursor: grabbing; }
|
|
|
|
|
+ .element-tag.dragging { opacity: 0.5; }
|
|
|
|
|
+ .element-tag .tag-icon { font-size: 12px; }
|
|
|
|
|
+ .element-tag .tag-name { font-weight: 500; }
|
|
|
|
|
+ .element-tag.entity { border-left: 3px solid var(--primary); }
|
|
|
|
|
+ .element-tag.concept { border-left: 3px solid #722ed1; }
|
|
|
|
|
+ /* Tabs for elements */
|
|
|
|
|
+ .element-tabs { display:flex; gap:8px; }
|
|
|
|
|
+ .element-tab { padding:6px 12px; border-radius:12px; background:transparent; border:1px solid transparent; font-size:13px; cursor:pointer; color:var(--text2); }
|
|
|
|
|
+ .element-tab.active { background: var(--primary); color:#fff; border-color: rgba(0,0,0,0.04); box-shadow: 0 6px 18px rgba(17,24,39,0.06); }
|
|
|
|
|
+
|
|
|
|
|
+ /* Tag shape / size rules */
|
|
|
|
|
+ .element-tag { height:28px; padding:0 12px; border-radius:2px; display:inline-flex; align-items:center; }
|
|
|
|
|
+ .element-tag .tag-name { line-height:28px; }
|
|
|
|
|
+ .element-tag.dynamic { border-radius:14px; } /* rounded for dynamic */
|
|
|
|
|
+ .element-tag.static { border-radius:2px; } /* slightly rounded for static */
|
|
|
|
|
+ /* module title style (icon + text), placed above header */
|
|
|
|
|
+ .module-title { display:flex; align-items:center; gap:10px; font-size:15px; font-weight:700; color:var(--text1); padding:0; margin-bottom:10px; }
|
|
|
|
|
+ .module-title .module-icon { width:36px; height:36px; border-radius:8px; background: var(--primary-gradient); display:flex; align-items:center; justify-content:center; font-size:18px; color:white; box-shadow: 0 6px 18px rgba(0,0,0,0.06); }
|
|
|
|
|
+ .element-tag.data { border-left: 3px solid var(--success); }
|
|
|
|
|
+ .element-tag.location { border-left: 3px solid var(--warning); }
|
|
|
|
|
+ .element-tag.asset { border-left: 3px solid #eb2f96; }
|
|
|
|
|
+
|
|
|
|
|
+ .element-hint { font-size: 11px; color: var(--text3); margin-top: 10px; text-align: center; }
|
|
|
|
|
+
|
|
|
|
|
+ /* 要素详情弹出框 */
|
|
|
|
|
+ .element-popover { position: fixed; width: 280px; background: var(--white); border-radius: 10px; box-shadow: 0 8px 24px rgba(0,0,0,0.15); z-index: 2000; display: none; }
|
|
|
|
|
+ .element-popover.show { display: block; }
|
|
|
|
|
+ .popover-header { padding: 12px 14px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 10px; }
|
|
|
|
|
+ .popover-icon { width: 32px; height: 32px; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 16px; }
|
|
|
|
|
+ .popover-icon.entity { background: linear-gradient(135deg, #e6f7ff, #bae7ff); }
|
|
|
|
|
+ .popover-icon.data { background: linear-gradient(135deg, #f6ffed, #d9f7be); }
|
|
|
|
|
+ .popover-icon.location { background: linear-gradient(135deg, #fff7e6, #ffe7ba); }
|
|
|
|
|
+ .popover-icon.asset { background: linear-gradient(135deg, #fff0f6, #ffd6e7); }
|
|
|
|
|
+ .popover-title { font-size: 14px; font-weight: 600; flex: 1; }
|
|
|
|
|
+ .popover-close { width: 24px; height: 24px; border: none; background: var(--bg); border-radius: 50%; cursor: pointer; font-size: 12px; }
|
|
|
|
|
+ .popover-close:hover { background: var(--danger); color: white; }
|
|
|
|
|
+ .popover-body { padding: 14px; }
|
|
|
|
|
+ .popover-section { margin-bottom: 10px; }
|
|
|
|
|
+ .popover-section:last-child { margin-bottom: 0; }
|
|
|
|
|
+ .popover-label { font-size: 10px; color: var(--text3); margin-bottom: 4px; text-transform: uppercase; }
|
|
|
|
|
+ .popover-value { font-size: 12px; color: var(--text1); }
|
|
|
|
|
+ .popover-relations { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
|
|
|
+ .popover-relation { padding: 4px 8px; background: var(--bg); border-radius: 4px; font-size: 11px; cursor: pointer; }
|
|
|
|
|
+ .popover-relation:hover { background: var(--primary-light); color: var(--primary); }
|
|
|
|
|
+ .popover-actions { display: flex; gap: 8px; margin-top: 12px; }
|
|
|
|
|
+ .popover-actions .btn { flex: 1; justify-content: center; font-size: 12px; }
|
|
|
|
|
+
|
|
|
|
|
+ /* AI助手区 */
|
|
|
|
|
+ .ai-assistant { position: relative; flex: 1; display: flex; flex-direction: column; overflow: hidden; min-height: 0; }
|
|
|
|
|
+ .ai-header { padding: 12px 16px; border-bottom: none; display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
|
|
|
|
|
+ .ai-avatar-sm { width: 36px; height: 36px; background: var(--ai-gradient); border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 18px; flex-shrink: 0; }
|
|
|
|
|
+ .ai-info { flex: 1; }
|
|
|
|
|
+ .ai-name { font-size: 15px; font-weight: 600; }
|
|
|
|
|
+ .ai-status { font-size: 11px; color: var(--success); }
|
|
|
|
|
+ /* Hide AI subtitle and tabs per user request */
|
|
|
|
|
+ .ai-info .ai-status { display: none; }
|
|
|
|
|
+ .ai-tabs { display: none !important; }
|
|
|
|
|
+
|
|
|
|
|
+ /* AI Tab切换 */
|
|
|
|
|
+ .ai-tabs { display: flex; border-bottom: 1px solid var(--border); flex-shrink: 0; }
|
|
|
|
|
+ .ai-tab { flex: 1; padding: 10px; text-align: center; font-size: 12px; cursor: pointer; color: var(--text2); border-bottom: 2px solid transparent; transition: all 0.2s; display: flex; align-items: center; justify-content: center; gap: 4px; }
|
|
|
|
|
+ .ai-tab:hover { color: var(--primary); }
|
|
|
|
|
+ .ai-tab.active { color: var(--primary); border-bottom-color: var(--primary); }
|
|
|
|
|
+
|
|
|
|
|
+ /* AI消息区 */
|
|
|
|
|
+ /* 消息区:为固定在底部的输入区预留底部空间,避免遮挡最后一条消息 */
|
|
|
|
|
+ .ai-messages { flex: 1; overflow-y: auto; padding: 16px; padding-bottom: 96px; min-height: 0; }
|
|
|
|
|
+ .msg { display: flex; gap: 10px; margin-bottom: 16px; }
|
|
|
|
|
+ .msg.user { flex-direction: row-reverse; }
|
|
|
|
|
+ .msg-avatar { width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px; flex-shrink: 0; }
|
|
|
|
|
+ .msg.ai .msg-avatar { background: var(--ai-gradient); color: white; }
|
|
|
|
|
+ .msg.user .msg-avatar { background: var(--primary); color: white; }
|
|
|
|
|
+ .msg-bubble { max-width: 85%; padding: 10px 14px; border-radius: 12px; font-size: 13px; line-height: 1.6; }
|
|
|
|
|
+ .msg.ai .msg-bubble { background: var(--bg); border-radius: 4px 12px 12px 12px; }
|
|
|
|
|
+ .msg.user .msg-bubble { background: var(--primary); color: white; border-radius: 12px 4px 12px 12px; }
|
|
|
|
|
+
|
|
|
|
|
+ /* AI输入区 */
|
|
|
|
|
+ /* 输入区固定在 AI 面板底部,不随消息滚动 */
|
|
|
|
|
+ .ai-input-area { padding: 12px 16px; border-top: none; background: var(--white); flex-shrink: 0; position: absolute; left: 0; right: 0; bottom: 0; z-index: 4; }
|
|
|
|
|
+ .ai-input-box { background: var(--bg); border: 1px solid var(--border); border-radius: 20px; transition: all 0.2s; }
|
|
|
|
|
+ .ai-input-box:focus-within { border-color: var(--primary); background: var(--white); box-shadow: 0 0 0 3px rgba(24,144,255,0.1); }
|
|
|
|
|
+ .ai-input-box textarea { width: 100%; border: none; background: transparent; resize: none; outline: none; font-size: 13px; line-height: 1.5; padding: 10px 16px; min-height: 40px; max-height: 80px; font-family: inherit; border-radius: 20px; display: block; }
|
|
|
|
|
+ .ai-input-box textarea::placeholder { color: var(--text3); font-size: 14px; }
|
|
|
|
|
+ /* 输入区内布局:左侧图标、中心输入、右侧操作(语音/发送) */
|
|
|
|
|
+ /* Cursor-like AI input: larger rounded, subtle shadow, inline icons */
|
|
|
|
|
+ .ai-input-inner { display:flex; align-items:center; gap:8px; }
|
|
|
|
|
+ .ai-input-left, .ai-input-right { display:flex; gap:6px; align-items:center; flex-shrink:0; }
|
|
|
|
|
+ .ai-input-box { flex:1; background:var(--white); border:1px solid var(--border); border-radius:8px; padding:8px 10px; box-shadow: 0 6px 18px rgba(16,24,40,0.06); display:flex; flex-direction:column; gap:6px; }
|
|
|
|
|
+ .ai-input-label { font-size:12px; color:var(--text3); }
|
|
|
|
|
+ .ai-input-row { display:flex; flex-direction:column; gap:6px; }
|
|
|
|
|
+ .ai-input-row .ai-input-top { display:flex; align-items:center; gap:8px; }
|
|
|
|
|
+ .ai-input-row textarea { flex:1; width:100%; border:none; outline:none; resize:none; font-size:14px; color:var(--text1); background:transparent; line-height:1.4; padding:6px 12px; min-height:20px; max-height:160px; }
|
|
|
|
|
+ .ai-input-toolbar { display:flex; align-items:center; justify-content:space-between; gap:8px; height:28px; }
|
|
|
|
|
+ .ai-input-toolbar .left, .ai-input-toolbar .right { display:flex; gap:12px; align-items:center; }
|
|
|
|
|
+ .ai-input-box textarea::placeholder { color:var(--text3); }
|
|
|
|
|
+ .ai-input-left .ai-icon-btn, .ai-input-right .ai-icon-btn { width:24px; height:24px; border-radius:4px; border:none; background:transparent; display:flex; align-items:center; justify-content:center; cursor:pointer; font-size:12px; color:#8c8c8c; padding:0; transition: all 0.2s ease; }
|
|
|
|
|
+ .ai-input-left .ai-icon-btn:hover, .ai-input-right .ai-icon-btn:hover { background:#f0f0f0; border-color:transparent; color:#555555; }
|
|
|
|
|
+ .ai-send-btn { width:24px; height:24px; border-radius:4px; border:none; background:#f0f0f0; display:flex; align-items:center; justify-content:center; cursor:pointer; font-size:12px; color:#555555; transition: all 0.18s; box-shadow:none; padding:0; }
|
|
|
|
|
+ .ai-send-btn:hover { background:#1890ff; color:#ffffff; }
|
|
|
|
|
+ .ai-send-btn.active { background: var(--primary); color: white; box-shadow: 0 6px 18px rgba(24,144,255,0.16); transform: translateY(-1px); }
|
|
|
|
|
+ .ai-input-caption { font-size:13px; color:var(--text3); margin-bottom:6px; padding-left:6px; }
|
|
|
|
|
+ .ai-input-caption .ai-highlight { color: var(--primary); font-weight:600; margin-left:6px; }
|
|
|
|
|
+ .ai-input-hint { display: flex; align-items: center; justify-content: space-between; padding: 8px 4px 0; font-size: 10px; color: var(--text3); }
|
|
|
|
|
+
|
|
|
|
|
+ /* 动画 */
|
|
|
|
|
+ @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
|
|
|
|
|
+ .parsing-anim { animation: pulse 1.5s infinite; }
|
|
|
|
|
+
|
|
|
|
|
+ /* 可编辑内容区 */
|
|
|
|
|
+ .editor-content[contenteditable="true"] { outline: none; }
|
|
|
|
|
+ .editor-content[contenteditable="true"]:focus { background: #fafbfc; box-shadow: inset 0 0 0 2px var(--primary-light); }
|
|
|
|
|
+ .editor-content[contenteditable="true"] ::selection { background: rgba(24,144,255,0.3); border-radius: 2px; }
|
|
|
|
|
+
|
|
|
|
|
+ /* 文本编辑选中效果 */
|
|
|
|
|
+ .editor-content[contenteditable="true"]:focus p { position: relative; }
|
|
|
|
|
+ .editor-content[contenteditable="true"]:focus p::before {
|
|
|
|
|
+ content: '';
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ left: -20px;
|
|
|
|
|
+ top: 0;
|
|
|
|
|
+ bottom: 0;
|
|
|
|
|
+ width: 3px;
|
|
|
|
|
+ background: var(--primary);
|
|
|
|
|
+ border-radius: 2px;
|
|
|
|
|
+ opacity: 0;
|
|
|
|
|
+ transition: opacity 0.2s;
|
|
|
|
|
+ }
|
|
|
|
|
+ .editor-content[contenteditable="true"]:focus p:hover::before {
|
|
|
|
|
+ opacity: 0.5;
|
|
|
|
|
+ }
|
|
|
|
|
+ .editor-content[contenteditable="true"]:focus p.selected::before {
|
|
|
|
|
+ opacity: 1;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /* 右键菜单 */
|
|
|
|
|
+ .context-menu { position: fixed; min-width: 180px; background: var(--white); border-radius: 10px; box-shadow: 0 8px 24px rgba(0,0,0,0.15); z-index: 3000; display: none; overflow: hidden; }
|
|
|
|
|
+ .context-menu.show { display: block; }
|
|
|
|
|
+ .context-menu-item { display: flex; align-items: center; gap: 10px; padding: 10px 14px; font-size: 13px; cursor: pointer; transition: all 0.15s; }
|
|
|
|
|
+ .context-menu-item:hover { background: var(--primary-light); color: var(--primary); }
|
|
|
|
|
+ .context-menu-item .icon { font-size: 14px; width: 20px; text-align: center; }
|
|
|
|
|
+ .context-menu-item .shortcut { margin-left: auto; font-size: 11px; color: var(--text3); }
|
|
|
|
|
+ .context-menu-divider { height: 1px; background: var(--border); margin: 4px 0; }
|
|
|
|
|
+
|
|
|
|
|
+ /* 数据关系表弹窗 */
|
|
|
|
|
+ .data-relation-modal { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: none; align-items: center; justify-content: center; z-index: 1300; }
|
|
|
|
|
+ .data-relation-modal.show { display: flex; }
|
|
|
|
|
+ .data-relation-card { width: 600px; max-width: 90vw; background: var(--white); border-radius: 16px; overflow: hidden; box-shadow: 0 20px 60px rgba(0,0,0,0.3); }
|
|
|
|
|
+ .data-relation-header { padding: 20px; background: linear-gradient(135deg, #f0f7ff, #f5f0ff); border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 15px; }
|
|
|
|
|
+ .data-relation-icon { width: 48px; height: 48px; background: var(--white); border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 24px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
|
|
|
|
|
+ .data-relation-title { flex: 1; }
|
|
|
|
|
+ .data-relation-title h3 { font-size: 16px; font-weight: 600; margin-bottom: 4px; }
|
|
|
|
|
+ .data-relation-title span { font-size: 12px; color: var(--text3); }
|
|
|
|
|
+ .data-relation-close { width: 32px; height: 32px; border: none; background: rgba(0,0,0,0.05); border-radius: 50%; cursor: pointer; font-size: 16px; display: flex; align-items: center; justify-content: center; }
|
|
|
|
|
+ .data-relation-close:hover { background: var(--danger); color: white; }
|
|
|
|
|
+
|
|
|
|
|
+ .data-relation-body { padding: 20px; max-height: 60vh; overflow-y: auto; }
|
|
|
|
|
+ .relation-section { margin-bottom: 24px; }
|
|
|
|
|
+ .relation-section:last-child { margin-bottom: 0; }
|
|
|
|
|
+ .relation-label { font-size: 14px; font-weight: 600; margin-bottom: 12px; color: var(--text1); display: flex; align-items: center; gap: 6px; }
|
|
|
|
|
+
|
|
|
|
|
+ .relation-table { width: 100%; border-collapse: collapse; border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
|
|
|
|
|
+ .relation-table th { background: var(--bg); padding: 12px; text-align: left; font-size: 12px; font-weight: 600; color: var(--text2); border-bottom: 1px solid var(--border); }
|
|
|
|
|
+ .relation-table td { padding: 12px; border-bottom: 1px solid var(--border); font-size: 13px; }
|
|
|
|
|
+ .relation-table tr:last-child td { border-bottom: none; }
|
|
|
|
|
+ .relation-table tr:hover td { background: var(--primary-light); }
|
|
|
|
|
+
|
|
|
|
|
+ .relation-input { width: 100%; padding: 6px 8px; border: 1px solid var(--border); border-radius: 4px; font-size: 12px; outline: none; }
|
|
|
|
|
+ .relation-input:focus { border-color: var(--primary); }
|
|
|
|
|
+
|
|
|
|
|
+ .relation-tags { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
|
|
|
+ .relation-tag { display: inline-flex; align-items: center; gap: 6px; padding: 6px 10px; background: var(--bg); border: 1px solid var(--border); border-radius: 12px; font-size: 11px; cursor: pointer; transition: all 0.2s; }
|
|
|
|
|
+ .relation-tag:hover { background: var(--primary-light); border-color: var(--primary); }
|
|
|
|
|
+
|
|
|
|
|
+ .data-relation-footer { padding: 16px 20px; background: var(--bg); border-top: 1px solid var(--border); display: flex; justify-content: flex-end; gap: 10px; }
|
|
|
|
|
+
|
|
|
|
|
+ /* 拖拽提示 */
|
|
|
|
|
+ .editor-content.drag-over { background: linear-gradient(135deg, rgba(24,144,255,0.05), rgba(24,144,255,0.1)); }
|
|
|
|
|
+ .editor-content.drag-over::after { content: '释放鼠标插入要素'; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); padding: 12px 24px; background: var(--primary); color: white; border-radius: 8px; font-size: 14px; z-index: 100; }
|
|
|
|
|
+ /* 报告要素弹窗样式 */
|
|
|
|
|
+ .report-elements-modal { position: fixed; inset: 0; display: none; align-items: center; justify-content: center; z-index: 2200; }
|
|
|
|
|
+ .report-elements-modal.show { display: flex; }
|
|
|
|
|
+ .report-elements-card { width: 960px; max-width: 96vw; max-height: 80vh; background: var(--white); border-radius: 12px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); overflow: hidden; display:flex; flex-direction:column; }
|
|
|
|
|
+ .report-elements-header { padding: 12px 16px; border-bottom: 1px solid var(--border); display:flex; align-items:center; gap:12px; }
|
|
|
|
|
+ .report-elements-title { font-size: 16px; font-weight:700; flex:1; }
|
|
|
|
|
+ .report-elements-body { padding: 12px 16px; overflow:auto; flex:1; }
|
|
|
|
|
+ .report-elements-footer { padding: 12px 16px; border-top:1px solid var(--border); display:flex; gap:8px; justify-content:flex-end; background:var(--bg); }
|
|
|
|
|
+ .elements-table { width:100%; border-collapse: collapse; }
|
|
|
|
|
+ .elements-table th, .elements-table td { padding:8px 10px; text-align:left; border-bottom:1px solid var(--border); font-size:13px; vertical-align:middle; }
|
|
|
|
|
+ .elements-table th { background: var(--bg); font-weight:600; font-size:12px; color:var(--text2); }
|
|
|
|
|
+ .elements-search { display:flex; gap:8px; align-items:center; }
|
|
|
|
|
+ .elements-search input { padding:8px 10px; border:1px solid var(--border); border-radius:6px; width:260px; }
|
|
|
|
|
+ .elements-actions .icon-btn { padding: 0; border-radius: 4px; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; }
|
|
|
|
|
+ /* pager styles */
|
|
|
|
|
+ .pager { display:flex; gap:6px; align-items:center; }
|
|
|
|
|
+ .pager-btn { padding:6px 10px; border:1px solid var(--border); background:var(--white); border-radius:6px; cursor:pointer; font-size:13px; }
|
|
|
|
|
+ .pager-btn.active { background:var(--primary); color:white; border-color:var(--primary); }
|
|
|
|
|
+ .page-size-select { padding:6px 8px; border:1px solid var(--border); border-radius:6px; background:var(--white); }
|
|
|
|
|
+
|
|
|
|
|
+ /* Toast容器 */
|
|
|
|
|
+ .toast-container { position: fixed; top: 70px; right: 24px; z-index: 3000; display: flex; flex-direction: column; gap: 8px; }
|
|
|
|
|
+ .toast { display: flex; align-items: center; gap: 10px; padding: 12px 18px; background: var(--white); border-radius: 10px; box-shadow: 0 8px 24px rgba(0,0,0,0.15); transform: translateX(120%); transition: transform 0.3s; min-width: 240px; }
|
|
|
|
|
+ .toast.show { transform: translateX(0); }
|
|
|
|
|
+ .toast.success { border-left: 4px solid var(--success); }
|
|
|
|
|
+ .toast.error { border-left: 4px solid var(--danger); }
|
|
|
|
|
+ .toast.warning { border-left: 4px solid var(--warning); }
|
|
|
|
|
+ .toast.info { border-left: 4px solid var(--primary); }
|
|
|
|
|
+
|
|
|
|
|
+ /* 通知面板 */
|
|
|
|
|
+ .notif-panel { position: fixed; top: 56px; right: 0; width: 360px; height: calc(100vh - 56px); background: var(--white); box-shadow: -4px 0 16px rgba(0,0,0,0.1); transform: translateX(100%); transition: transform 0.3s; z-index: 999; display: flex; flex-direction: column; }
|
|
|
|
|
+
|
|
|
|
|
+ /* 遮罩 */
|
|
|
|
|
+ .overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.3); z-index: 998; display: none; }
|
|
|
|
|
+ .kg-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 1199; display: none; }
|
|
|
|
|
+
|
|
|
|
|
+ /* 导出菜单 */
|
|
|
|
|
+ .export-menu { position: fixed; background: var(--white); border-radius: 10px; box-shadow: 0 8px 24px rgba(0,0,0,0.15); min-width: 180px; display: none; z-index: 2001; overflow: hidden; }
|
|
|
|
|
+
|
|
|
|
|
+ /* 要素关系图谱弹窗 */
|
|
|
|
|
+ .knowledge-graph-modal { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 90%; max-width: 1200px; height: 80%; max-height: 700px; background: var(--white); border-radius: 16px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); display: none; flex-direction: column; z-index: 1200; overflow: hidden; }
|
|
|
|
|
+ .knowledge-graph-modal.show { display: flex; }
|
|
|
|
|
+
|
|
|
|
|
+ .kg-header { padding: 16px 20px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; background: linear-gradient(135deg, #f0f7ff, #f5f0ff); }
|
|
|
|
|
+ .kg-title { display: flex; align-items: center; gap: 8px; font-size: 16px; font-weight: 600; color: var(--primary); }
|
|
|
|
|
+ .kg-icon { font-size: 20px; }
|
|
|
|
|
+ .kg-controls { display: flex; align-items: center; gap: 12px; }
|
|
|
|
|
+ .kg-view-toggle { display: flex; border: 1px solid var(--border); border-radius: 6px; overflow: hidden; }
|
|
|
|
|
+ .kg-view-btn { padding: 6px 12px; border: none; background: var(--white); font-size: 12px; cursor: pointer; color: var(--text2); display: flex; align-items: center; gap: 4px; transition: all 0.2s; }
|
|
|
|
|
+ .kg-view-btn:first-child { border-right: 1px solid var(--border); }
|
|
|
|
|
+ .kg-view-btn:hover { background: var(--bg); }
|
|
|
|
|
+ .kg-view-btn.active { background: var(--primary-light); color: var(--primary); }
|
|
|
|
|
+ .kg-close { width: 28px; height: 28px; border: none; background: rgba(0,0,0,0.05); border-radius: 50%; cursor: pointer; font-size: 16px; display: flex; align-items: center; justify-content: center; }
|
|
|
|
|
+ .kg-close:hover { background: var(--danger); color: white; }
|
|
|
|
|
+
|
|
|
|
|
+ .kg-content { flex: 1; overflow: hidden; }
|
|
|
|
|
+
|
|
|
|
|
+ /* 图谱视图 */
|
|
|
|
|
+ .kg-graph-view { height: 100%; display: flex; flex-direction: column; }
|
|
|
|
|
+ .graph-canvas { flex: 1; position: relative; background: linear-gradient(45deg, #f8f9fa 25%, transparent 25%), linear-gradient(-45deg, #f8f9fa 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #f8f9fa 75%), linear-gradient(-45deg, transparent 75%, #f8f8f8 75%); background-size: 20px 20px; background-position: 0 0, 0 10px, 10px -10px, -10px 0px; }
|
|
|
|
|
+ .graph-node { position: absolute; width: 120px; padding: 12px; background: var(--white); border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); cursor: pointer; transition: all 0.3s; border: 2px solid transparent; text-align: center; }
|
|
|
|
|
+ .graph-node:hover { transform: scale(1.05); box-shadow: 0 8px 24px rgba(0,0,0,0.2); }
|
|
|
|
|
+ .graph-node.core { border-color: var(--primary); background: linear-gradient(135deg, #f0f7ff, #e6f7ff); }
|
|
|
|
|
+ .graph-node.concept { border-color: #722ed1; background: linear-gradient(135deg, #f9f0ff, #efdbff); }
|
|
|
|
|
+ .graph-node.data { border-color: var(--success); background: linear-gradient(135deg, #f6ffed, #d9f7be); }
|
|
|
|
|
+ .graph-node.location { border-color: var(--warning); background: linear-gradient(135deg, #fff7e6, #ffe7ba); }
|
|
|
|
|
+ .graph-node.highlighted { border-color: var(--danger); box-shadow: 0 0 20px rgba(255,77,79,0.4); transform: scale(1.1); }
|
|
|
|
|
+ .node-icon { font-size: 24px; margin-bottom: 6px; }
|
|
|
|
|
+ .node-label { font-size: 12px; font-weight: 600; margin-bottom: 4px; }
|
|
|
|
|
+ .node-type { font-size: 10px; color: var(--text3); }
|
|
|
|
|
+ .graph-lines { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; }
|
|
|
|
|
+
|
|
|
|
|
+ .graph-legend { padding: 12px 16px; background: var(--bg); border-top: 1px solid var(--border); display: flex; gap: 16px; justify-content: center; }
|
|
|
|
|
+ .legend-item { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text2); }
|
|
|
|
|
+ .legend-dot { width: 12px; height: 12px; border-radius: 50%; }
|
|
|
|
|
+ .legend-dot.core { background: var(--primary); }
|
|
|
|
|
+ .legend-dot.concept { background: #722ed1; }
|
|
|
|
|
+ .legend-dot.data { background: var(--success); }
|
|
|
|
|
+ .legend-dot.location { background: var(--warning); }
|
|
|
|
|
+
|
|
|
|
|
+ /* 列表视图 */
|
|
|
|
|
+ .kg-list-view { height: 100%; display: flex; flex-direction: column; }
|
|
|
|
|
+ .list-search { padding: 16px; border-bottom: 1px solid var(--border); }
|
|
|
|
|
+ .list-search-input { width: 100%; padding: 8px 12px; border: 1px solid var(--border); border-radius: 6px; font-size: 13px; outline: none; }
|
|
|
|
|
+ .list-search-input:focus { border-color: var(--primary); }
|
|
|
|
|
+
|
|
|
|
|
+ .entity-categories { flex: 1; overflow-y: auto; }
|
|
|
|
|
+ .category-section { margin-bottom: 16px; }
|
|
|
|
|
+ .category-header { padding: 12px 16px; background: var(--bg); border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 600; color: var(--text1); }
|
|
|
|
|
+ .category-icon { font-size: 16px; }
|
|
|
|
|
+ .entity-item { display: flex; align-items: center; gap: 12px; padding: 12px 16px; border-bottom: 1px solid var(--border); cursor: pointer; transition: all 0.2s; }
|
|
|
|
|
+ .entity-item:hover { background: var(--primary-light); }
|
|
|
|
|
+ .entity-item:last-child { border-bottom: none; }
|
|
|
|
|
+ .entity-icon { width: 36px; height: 36px; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 18px; flex-shrink: 0; }
|
|
|
|
|
+ .entity-info { flex: 1; }
|
|
|
|
|
+ .entity-name { font-size: 13px; font-weight: 600; margin-bottom: 4px; }
|
|
|
|
|
+ .entity-meta { font-size: 11px; color: var(--text3); }
|
|
|
|
|
+ .entity-actions { display: flex; gap: 4px; }
|
|
|
|
|
+ .entity-action-btn { width: 24px; height: 24px; border: none; background: transparent; border-radius: 4px; cursor: pointer; font-size: 12px; display: flex; align-items: center; justify-content: center; transition: all 0.2s; }
|
|
|
|
|
+ .entity-action-btn:hover { background: var(--bg); }
|
|
|
|
|
+
|
|
|
|
|
+ /* 确认对话框 */
|
|
|
|
|
+ .confirm-dialog { position: fixed; inset: 0; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; z-index: 1400; opacity: 0; transition: opacity 0.3s; }
|
|
|
|
|
+ .confirm-dialog.show { opacity: 1; }
|
|
|
|
|
+ .confirm-content { background: var(--white); border-radius: 16px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); min-width: 400px; max-width: 500px; overflow: hidden; transform: scale(0.9); transition: transform 0.3s; }
|
|
|
|
|
+ .confirm-dialog.show .confirm-content { transform: scale(1); }
|
|
|
|
|
+ .confirm-header { padding: 20px; background: linear-gradient(135deg, #f0f7ff, #f5f0ff); border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; }
|
|
|
|
|
+ .confirm-header h3 { font-size: 16px; font-weight: 600; margin: 0; }
|
|
|
|
|
+ .confirm-close { width: 32px; height: 32px; border: none; background: rgba(0,0,0,0.05); border-radius: 50%; cursor: pointer; font-size: 16px; display: flex; align-items: center; justify-content: center; }
|
|
|
|
|
+ .confirm-close:hover { background: var(--danger); color: white; }
|
|
|
|
|
+ .confirm-body { padding: 20px; font-size: 14px; line-height: 1.6; color: var(--text1); }
|
|
|
|
|
+ .confirm-footer { padding: 16px 20px; background: var(--bg); border-top: 1px solid var(--border); display: flex; justify-content: flex-end; gap: 10px; }
|
|
|
|
|
+
|
|
|
|
|
+ /* 响应式 */
|
|
|
|
|
+ @media (max-width: 1024px) {
|
|
|
|
|
+ .sidebar { width: 200px; }
|
|
|
|
|
+ .main { margin-left: 200px; }
|
|
|
|
|
+ .editor-body { flex-direction: column; }
|
|
|
|
|
+ .left-panel, .right-panel { width: 100%; height: auto; max-height: 300px; }
|
|
|
|
|
+ .knowledge-graph-modal { width: 100%; }
|
|
|
|
|
+ .data-relation-modal { width: 95%; }
|
|
|
|
|
+ .confirm-content { min-width: 320px; }
|
|
|
|
|
+ }
|
|
|
|
|
+ </style>
|
|
|
|
|
+ <script>
|
|
|
|
|
+ // === 标题同步:将正文 H1 的文字同步到编辑器顶部小标题与输入中 ===
|
|
|
|
|
+ function syncEditorTitleFromContent() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const h1 = document.querySelector('.editor-content h1');
|
|
|
|
|
+ const toolbarTitle = document.querySelector('.editor-main-title');
|
|
|
|
|
+ const titleInput = document.querySelector('.report-title-input');
|
|
|
|
|
+ if (h1 && toolbarTitle) {
|
|
|
|
|
+ const txt = h1.textContent.trim();
|
|
|
|
|
+ toolbarTitle.textContent = txt;
|
|
|
|
|
+ if (titleInput) titleInput.value = txt;
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.warn('syncEditorTitleFromContent error', e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 同步一次并监听内容变化(以 H1 变动为触发器)
|
|
|
|
|
+ document.addEventListener('DOMContentLoaded', function() {
|
|
|
|
|
+ syncEditorTitleFromContent();
|
|
|
|
|
+ const h1 = document.querySelector('.editor-content h1');
|
|
|
|
|
+ if (h1) {
|
|
|
|
|
+ const ro = new MutationObserver(() => syncEditorTitleFromContent());
|
|
|
|
|
+ ro.observe(h1, { characterData: true, childList: true, subtree: true });
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ </script>
|
|
|
|
|
+</head>
|
|
|
|
|
+<body>
|
|
|
|
|
+ <!-- 全局头部 -->
|
|
|
|
|
+ <header class="topbar" id="topbar">
|
|
|
|
|
+ <div class="topbar-left">
|
|
|
|
|
+ <div class="logo">灵</div>
|
|
|
|
|
+ <div class="brand">灵越智报</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="topbar-right">
|
|
|
|
|
+ <button class="icon-btn" title="搜索"><i class="iconfont icon-SEARCH"></i></button>
|
|
|
|
|
+ <button class="icon-btn notif-btn" title="通知"><i class="iconfont icon-MESSAGE_NOTIFICATION_L"></i><span class="badge">3</span></button>
|
|
|
|
|
+ <div class="avatar">张</div>
|
|
|
|
|
+ <div class="username">张三</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </header>
|
|
|
|
|
+ <style>
|
|
|
|
|
+ .topbar {
|
|
|
|
|
+ height: 56px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ padding: 0 18px;
|
|
|
|
|
+ background: var(--white);
|
|
|
|
|
+ border-bottom: 1px solid var(--border);
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ position: sticky;
|
|
|
|
|
+ top: 0;
|
|
|
|
|
+ z-index: 50;
|
|
|
|
|
+ }
|
|
|
|
|
+ .topbar-left { display:flex;align-items:center;gap:10px;min-width:180px; }
|
|
|
|
|
+ .logo {
|
|
|
|
|
+ width:36px;height:36px;border-radius:8px;background:linear-gradient(135deg,var(--primary) 0%, #69c0ff 100%);display:flex;align-items:center;justify-content:center;color:white;font-weight:700;font-size:18px;
|
|
|
|
|
+ }
|
|
|
|
|
+ .brand { font-weight:600;color:var(--text); font-size:16px; }
|
|
|
|
|
+ .topbar-center { display:none; }
|
|
|
|
|
+ .topbar-right { display:flex; align-items:center; gap:8px; min-width:auto; justify-content:flex-end; }
|
|
|
|
|
+ .topbar-right .notif-btn { margin-right: 8px; }
|
|
|
|
|
+ .avatar { width:28px;height:28px;border-radius:50%;background:var(--primary);color:#fff;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:12px; margin-right: 0; }
|
|
|
|
|
+ .username { color:var(--text); font-size:13px; margin-left: -4px; }
|
|
|
|
|
+ .icon-btn {
|
|
|
|
|
+ background: transparent;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ color: #8c8c8c;
|
|
|
|
|
+ width: 24px;
|
|
|
|
|
+ height: 24px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ transition: all 0.2s ease;
|
|
|
|
|
+ }
|
|
|
|
|
+ .icon-btn:hover {
|
|
|
|
|
+ background: #f0f0f0;
|
|
|
|
|
+ color: #555555;
|
|
|
|
|
+ }
|
|
|
|
|
+ .icon-btn .badge { position:absolute; top:-6px; right:-6px; background:var(--danger); color:white; border-radius:10px; font-size:11px; padding:2px 6px; }
|
|
|
|
|
+ @media (max-width: 900px) {
|
|
|
|
|
+ .brand { display:none; }
|
|
|
|
|
+ }
|
|
|
|
|
+ </style>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 侧边栏 -->
|
|
|
|
|
+ <aside class="sidebar" id="sidebar">
|
|
|
|
|
+ <nav class="sidebar-menu">
|
|
|
|
|
+ <!-- 报告编辑入口已移除,页面打开时直接进入编辑器 -->
|
|
|
|
|
+ </nav>
|
|
|
|
|
+ </aside>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 主内容区 -->
|
|
|
|
|
+ <main class="main" id="mainContent" style="display:none;">
|
|
|
|
|
+ <!-- 首页 -->
|
|
|
|
|
+ <div class="page" id="page-home">
|
|
|
|
|
+ <div class="welcome">
|
|
|
|
|
+ <h1>早上好,张三!<span>智能报告,洞察未来。</span></h1>
|
|
|
|
|
+ <p>今天是个创作的好日子,开始您的智能报告之旅吧</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="stats-grid">
|
|
|
|
|
+ <div class="stat-card card" onclick="void(0)">
|
|
|
|
|
+ <div class="stat-icon blue">📄</div>
|
|
|
|
|
+ <div class="stat-value">12</div>
|
|
|
|
|
+ <div class="stat-label">我的报告</div>
|
|
|
|
|
+ <div class="stat-trend up">↑ 3 本周新增</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="stat-card card" onclick="void(0)">
|
|
|
|
|
+ <div class="stat-icon purple">🎨</div>
|
|
|
|
|
+ <div class="stat-value">15</div>
|
|
|
|
|
+ <div class="stat-label">可用模板</div>
|
|
|
|
|
+ <div class="stat-trend up">↑ 2 新增</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="stat-card card">
|
|
|
|
|
+ <div class="stat-icon green">📚</div>
|
|
|
|
|
+ <div class="stat-value">48</div>
|
|
|
|
|
+ <div class="stat-label">知识文档</div>
|
|
|
|
|
+ <div class="stat-trend">📁 1.2GB</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="stat-card card" onclick="toggleFab()">
|
|
|
|
|
+ <div class="stat-icon orange">💰</div>
|
|
|
|
|
+ <div class="stat-value">¥127.50</div>
|
|
|
|
|
+ <div class="stat-label">本月消耗</div>
|
|
|
|
|
+ <div class="stat-trend">↓ 12%</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- AI对话入口 -->
|
|
|
|
|
+ <div class="ai-card card">
|
|
|
|
|
+ <div class="ai-welcome">
|
|
|
|
|
+ <div class="ai-avatar">🤖</div>
|
|
|
|
|
+ <div class="ai-title">你好!我是灵越AI助手,可以帮你:</div>
|
|
|
|
|
+ <ul class="ai-list">
|
|
|
|
|
+ <li>快速生成各类报告</li>
|
|
|
|
|
+ <li>分析和解读数据</li>
|
|
|
|
|
+ <li>回答业务相关问题</li>
|
|
|
|
|
+ </ul>
|
|
|
|
|
+ <div class="ai-tip">试试输入:"帮我生成一份智慧园区建设项目可行性研究报告"</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="ai-input-wrap">
|
|
|
|
|
+ <input type="text" class="ai-input" placeholder="输入您的需求,或 @知识库 引用资料..." id="homeAiInput" oninput="toggleSendBtn()">
|
|
|
|
|
+ <div class="ai-input-btns">
|
|
|
|
|
+ <button class="ai-input-btn">🎤</button>
|
|
|
|
|
+ <button class="ai-input-btn">📎</button>
|
|
|
|
|
+ <button class="ai-input-btn send" id="homeSendBtn" onclick="handleHomeAi()">➤</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="thinking-modes">
|
|
|
|
|
+ <div class="mode-btn active" onclick="setMode(this)">🧠 深度思考</div>
|
|
|
|
|
+ <div class="mode-btn" onclick="setMode(this)">⚡ 快速回答</div>
|
|
|
|
|
+ <div class="mode-btn" onclick="setMode(this)">🌐 联网搜索</div>
|
|
|
|
|
+ <div class="mode-btn" onclick="setMode(this)">📊 数据分析</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 模板推荐区 -->
|
|
|
|
|
+ <div class="section-header">
|
|
|
|
|
+ <h2 class="section-title">📋 推荐模板</h2>
|
|
|
|
|
+ <span class="section-link" onclick="void(0)">查看全部 →</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="tpl-grid">
|
|
|
|
|
+ <div class="tpl-card card" onclick="void(0)">
|
|
|
|
|
+ <div class="tpl-preview">📊</div>
|
|
|
|
|
+ <div class="tpl-info">
|
|
|
|
|
+ <div class="tpl-name">市场分析报告</div>
|
|
|
|
|
+ <div class="tpl-meta"><span>📊 128次</span><span>⭐ 4.8</span></div>
|
|
|
|
|
+ <div class="tpl-tags"><span class="tpl-tag">官方</span><span class="tpl-tag hot">热门</span></div>
|
|
|
|
|
+ <button class="tpl-btn">使用此模板</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="tpl-card card" onclick="void(0)">
|
|
|
|
|
+ <div class="tpl-preview">🏢</div>
|
|
|
|
|
+ <div class="tpl-info">
|
|
|
|
|
+ <div class="tpl-name">可行性研究报告</div>
|
|
|
|
|
+ <div class="tpl-meta"><span>📊 96次</span><span>⭐ 4.9</span></div>
|
|
|
|
|
+ <div class="tpl-tags"><span class="tpl-tag">官方</span><span class="tpl-tag hot">热门</span></div>
|
|
|
|
|
+ <button class="tpl-btn">使用此模板</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="tpl-card card" onclick="void(0)">
|
|
|
|
|
+ <div class="tpl-preview">📅</div>
|
|
|
|
|
+ <div class="tpl-info">
|
|
|
|
|
+ <div class="tpl-name">项目周报</div>
|
|
|
|
|
+ <div class="tpl-meta"><span>📊 256次</span><span>⭐ 4.9</span></div>
|
|
|
|
|
+ <div class="tpl-tags"><span class="tpl-tag">官方</span></div>
|
|
|
|
|
+ <button class="tpl-btn">使用此模板</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="quick-actions">
|
|
|
|
|
+ <div class="quick-action" onclick="showToast('上传模板', 'info')">
|
|
|
|
|
+ <div class="quick-action-icon">📤</div>
|
|
|
|
|
+ <span>上传新模板</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="quick-action" onclick="showToast('创建模板', 'info')">
|
|
|
|
|
+ <div class="quick-action-icon">🛠️</div>
|
|
|
|
|
+ <span>创建新模板</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 报告记录页 -->
|
|
|
|
|
+ <div class="page" id="page-reports">
|
|
|
|
|
+ <h2>📋 报告记录</h2>
|
|
|
|
|
+ <div style="display:flex;gap:12px;margin:16px 0;">
|
|
|
|
|
+ <select style="padding:8px 12px;border:1px solid var(--border);border-radius:6px;font-size:13px;background:var(--white);"><option>全部状态</option><option>初稿</option><option>审核中</option><option>已定稿</option></select>
|
|
|
|
|
+ <input type="text" placeholder="🔍 搜索报告..." style="flex:1;max-width:280px;padding:8px 12px;border:1px solid var(--border);border-radius:6px;font-size:13px;">
|
|
|
|
|
+ <div style="margin-left:auto;display:flex;gap:6px;">
|
|
|
|
|
+ <button class="btn" style="background:var(--primary-light);border-color:var(--primary);color:var(--primary)">全部</button>
|
|
|
|
|
+ <button class="btn">本周</button>
|
|
|
|
|
+ <button class="btn">本月</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div style="display:flex;flex-direction:column;gap:12px;">
|
|
|
|
|
+ <div class="card" style="padding:18px;">
|
|
|
|
|
+ <div style="display:flex;align-items:center;gap:12px;margin-bottom:10px;">
|
|
|
|
|
+ <span style="font-size:22px;">📄</span>
|
|
|
|
|
+ <span style="flex:1;font-size:15px;font-weight:600;">智慧园区建设项目可行性研究报告</span>
|
|
|
|
|
+ <span style="padding:4px 10px;background:#f6ffed;color:var(--success);border-radius:12px;font-size:11px;">已定稿</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div style="display:flex;gap:20px;font-size:12px;color:var(--text2);margin-bottom:12px;">
|
|
|
|
|
+ <span>📅 2025-12-30</span><span>👤 张三</span><span>🏢 华南事业部</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div style="display:flex;gap:8px;">
|
|
|
|
|
+ <button class="btn btn-primary" onclick="void(0)">查看</button>
|
|
|
|
|
+ <button class="btn" onclick="void(0)">编辑</button>
|
|
|
|
|
+ <button class="btn" onclick="showToast('导出PDF成功', 'success')">导出</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="card" style="padding:18px;">
|
|
|
|
|
+ <div style="display:flex;align-items:center;gap:12px;margin-bottom:10px;">
|
|
|
|
|
+ <span style="font-size:22px;">📄</span>
|
|
|
|
|
+ <span style="flex:1;font-size:15px;font-weight:600;">Q4市场分析报告</span>
|
|
|
|
|
+ <span style="padding:4px 10px;background:#fffbe6;color:var(--warning);border-radius:12px;font-size:11px;">审核中</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div style="display:flex;gap:20px;font-size:12px;color:var(--text2);margin-bottom:12px;">
|
|
|
|
|
+ <span>📅 2025-12-28</span><span>👤 张三</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div style="display:flex;gap:8px;">
|
|
|
|
|
+ <button class="btn btn-primary" onclick="void(0)">查看</button>
|
|
|
|
|
+ <button class="btn" onclick="showToast('已发送催办', 'success')">催办</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 模板管理页 -->
|
|
|
|
|
+ <div class="page" id="page-templates">
|
|
|
|
|
+ <h2>🎨 模板管理</h2>
|
|
|
|
|
+ <div style="display:flex;gap:12px;margin:16px 0;">
|
|
|
|
|
+ <input type="text" placeholder="🔍 搜索模板..." style="width:280px;padding:8px 12px;border:1px solid var(--border);border-radius:6px;font-size:13px;">
|
|
|
|
|
+ <div style="display:flex;gap:6px;">
|
|
|
|
|
+ <span style="padding:8px 16px;background:var(--primary);color:white;border-radius:18px;font-size:12px;cursor:pointer;">全部</span>
|
|
|
|
|
+ <span style="padding:8px 16px;background:var(--bg);border-radius:18px;font-size:12px;cursor:pointer;" onclick="showToast('官方模板', 'info')">官方模板</span>
|
|
|
|
|
+ <span style="padding:8px 16px;background:var(--bg);border-radius:18px;font-size:12px;cursor:pointer;" onclick="showToast('我的模板', 'info')">我的模板</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <button class="btn btn-primary" style="margin-left:auto;">➕ 创建模板</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div style="display:grid;grid-template-columns:repeat(4,1fr);gap:14px;">
|
|
|
|
|
+ <div class="tpl-card card" onclick="void(0)"><div class="tpl-preview">📊</div><div class="tpl-info"><div class="tpl-name">市场分析报告</div><div class="tpl-meta"><span>📊 128次</span></div><div class="tpl-tags"><span class="tpl-tag">官方</span><span class="tpl-tag hot">热门</span></div><button class="tpl-btn">使用</button></div></div>
|
|
|
|
|
+ <div class="tpl-card card" onclick="void(0)"><div class="tpl-preview">🏢</div><div class="tpl-info"><div class="tpl-name">可行性研究报告</div><div class="tpl-meta"><span>📊 96次</span></div><div class="tpl-tags"><span class="tpl-tag">官方</span></div><button class="tpl-btn">使用</button></div></div>
|
|
|
|
|
+ <div class="tpl-card card" onclick="void(0)"><div class="tpl-preview">📅</div><div class="tpl-info"><div class="tpl-name">项目周报</div><div class="tpl-meta"><span>📊 256次</span></div><div class="tpl-tags"><span class="tpl-tag">官方</span></div><button class="tpl-btn">使用</button></div></div>
|
|
|
|
|
+ <div class="tpl-card card" onclick="void(0)"><div class="tpl-preview">💼</div><div class="tpl-info"><div class="tpl-name">尽职调查报告</div><div class="tpl-meta"><span>📊 45次</span></div><div class="tpl-tags"><span class="tpl-tag">行业</span></div><button class="tpl-btn">使用</button></div></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 数据源管理页 -->
|
|
|
|
|
+ <div class="page" id="page-datasources">
|
|
|
|
|
+ <h2>🔗 数据源管理</h2>
|
|
|
|
|
+ <div style="display:flex;gap:12px;margin:16px 0;">
|
|
|
|
|
+ <input type="text" placeholder="🔍 搜索数据源..." style="width:280px;padding:8px 12px;border:1px solid var(--border);border-radius:6px;font-size:13px;">
|
|
|
|
|
+ <button class="btn btn-primary" style="margin-left:auto;">➕ 添加数据源</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div style="display:flex;flex-direction:column;gap:12px;">
|
|
|
|
|
+ <div class="card" style="padding:18px;">
|
|
|
|
|
+ <div style="display:flex;align-items:center;gap:12px;margin-bottom:12px;">
|
|
|
|
|
+ <div style="width:44px;height:44px;background:var(--primary-light);border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:22px;">🗄️</div>
|
|
|
|
|
+ <div style="flex:1;"><div style="font-size:15px;font-weight:600;">销售数据库</div><div style="font-size:12px;color:var(--text3);">MySQL · db.company.com:3306</div></div>
|
|
|
|
|
+ <div style="font-size:12px;color:var(--success);">● 已连接</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div style="display:flex;gap:8px;"><button class="btn">测试连接</button><button class="btn">同步数据</button><button class="btn">查看数据表</button></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="card" style="padding:18px;">
|
|
|
|
|
+ <div style="display:flex;align-items:center;gap:12px;margin-bottom:12px;">
|
|
|
|
|
+ <div style="width:44px;height:44px;background:#f6ffed;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:22px;">🌐</div>
|
|
|
|
|
+ <div style="flex:1;"><div style="font-size:15px;font-weight:600;">市场数据API</div><div style="font-size:12px;color:var(--text3);">REST API · api.marketdata.com</div></div>
|
|
|
|
|
+ <div style="font-size:12px;color:var(--success);">● 已连接</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div style="display:flex;gap:8px;"><button class="btn">测试接口</button><button class="btn">查看文档</button></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </main>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 编辑器页面 -->
|
|
|
|
|
+ <div class="editor-page active" id="page-editor">
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 编辑器主体 -->
|
|
|
|
|
+ <div class="editor-body">
|
|
|
|
|
+ <!-- 左侧项目文件面板 -->
|
|
|
|
|
+ <div class="left-panel">
|
|
|
|
|
+ <div class="panel-header panel-header-tabs">
|
|
|
|
|
+ <div class="tabs-left">
|
|
|
|
|
+ <button class="panel-tab active" id="tabDocsTop" onclick="switchLeftTab('docs')">我的文档</button>
|
|
|
|
|
+ <button class="panel-tab" id="tabFilesTop" onclick="switchLeftTab('files')">我的附件</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div style="display:flex;align-items:center;gap:8px;">
|
|
|
|
|
+ <button class="panel-toggle-btn" title="折叠资源面板" onclick="toggleResourcePanel(this)" style="margin-left:8px;">❮</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="panel-body">
|
|
|
|
|
+ <!-- 内部旧 tabs 已移除 -->
|
|
|
|
|
+
|
|
|
|
|
+ <div class="doc-section" id="leftDocsSection">
|
|
|
|
|
+ <div class="doc-section-header" id="reportsHeader">
|
|
|
|
|
+ <div id="reportsTitle">报告记录 <span id="reportsCount" style="font-weight:600">· 3</span></div>
|
|
|
|
|
+ <div style="display:flex;align-items:center;gap:8px;">
|
|
|
|
|
+ <button class="icon-btn" id="leftSearchBtn" title="搜索"><i class="iconfont icon-SEARCH"></i></button>
|
|
|
|
|
+ <button class="icon-btn" id="newReportBtn" title="新建报告"><i class="iconfont icon-CREATE"></i></button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="docs-area">
|
|
|
|
|
+ <div class="doc-list">
|
|
|
|
|
+ <div class="doc-card file-item" onclick="showFilePreview(this)" data-content="智慧园区建设项目可行性研究报告 原文预览..." data-name="智慧园区建设项目可行性研究报告">
|
|
|
|
|
+ <div class="doc-thumb">📄</div>
|
|
|
|
|
+ <div class="file-actions">
|
|
|
|
|
+ <button class="action-btn" onclick="downloadReport(this)" title="下载">⬇️</button>
|
|
|
|
|
+ <button class="action-btn" onclick="archiveReport(this)" title="归档">🗂️</button>
|
|
|
|
|
+ <button class="action-btn" onclick="deleteReport(this)" title="删除">🗑️</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="doc-meta">
|
|
|
|
|
+ <div class="doc-title">智慧园区建设项目可行性研究报告</div>
|
|
|
|
|
+ <div class="doc-time">2025/12/30 00:00:00</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="doc-card file-item" onclick="showFilePreview(this)" data-content="华东市场分析 原文预览..." data-name="华东市场分析">
|
|
|
|
|
+ <div class="doc-thumb">📄</div>
|
|
|
|
|
+ <div class="file-actions">
|
|
|
|
|
+ <button class="action-btn" onclick="downloadReport(this)" title="下载">⬇️</button>
|
|
|
|
|
+ <button class="action-btn" onclick="archiveReport(this)" title="归档">🗂️</button>
|
|
|
|
|
+ <button class="action-btn" onclick="deleteReport(this)" title="删除">🗑️</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="doc-meta">
|
|
|
|
|
+ <div class="doc-title">华东市场分析</div>
|
|
|
|
|
+ <div class="doc-time">2026/1/26 01:51:44</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="doc-card file-item" onclick="showFilePreview(this)" data-content="季度销售简报 原文预览..." data-name="季度销售简报">
|
|
|
|
|
+ <div class="doc-thumb">📄</div>
|
|
|
|
|
+ <div class="file-actions">
|
|
|
|
|
+ <button class="action-btn" onclick="downloadReport(this)" title="下载">⬇️</button>
|
|
|
|
|
+ <button class="action-btn" onclick="archiveReport(this)" title="归档">🗂️</button>
|
|
|
|
|
+ <button class="action-btn" onclick="deleteReport(this)" title="删除">🗑️</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="doc-meta">
|
|
|
|
|
+ <div class="doc-title">季度销售简报</div>
|
|
|
|
|
+ <div class="doc-time">2026/1/24 13:51:44</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="recent-area">
|
|
|
|
|
+ <div class="recent-section">
|
|
|
|
|
+ <div class="section-title small">最近操作</div>
|
|
|
|
|
+ <div class="recent-list" id="recentList">
|
|
|
|
|
+ <div class="recent-item" title="AI 已生成报告初稿:季度销售简报..."><span class="recent-text">AI 已生成报告初稿:季度销售简报...</span></div>
|
|
|
|
|
+ <div class="recent-item" title="文件 '市场调研数据.pdf' 解析完成"><span class="recent-text">文件 '市场调研数据.pdf' 解析完成</span></div>
|
|
|
|
|
+ <div class="recent-item" title="附件 '财务预测表.xlsx' 解析完成"><span class="recent-text">附件 '财务预测表.xlsx' 解析完成</span></div>
|
|
|
|
|
+ <div class="recent-item" title="用户 张三 上传文件 '公司报表.xlsx'"><span class="recent-text">用户 张三 上传文件 '公司报表.xlsx'</span></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="doc-section" id="leftFilesSection" style="display:none;">
|
|
|
|
|
+ <div class="doc-section-header">
|
|
|
|
|
+ <div>我的附件</div>
|
|
|
|
|
+ <div class="badge-count" id="leftFilesCount">0</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="doc-list">
|
|
|
|
|
+ <!-- Attachments will be listed here -->
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 中间编辑区 -->
|
|
|
|
|
+ <div class="center-panel">
|
|
|
|
|
+ <div class="editor-title-bar" style="display:flex;justify-content:space-between;align-items:center;gap:12px;">
|
|
|
|
|
+ <div style="display:flex;align-items:center;gap:12px;">
|
|
|
|
|
+ <div class="editor-main-title" id="editorMainTitle" role="button" tabindex="0" title="报告标题">智慧园区建设项目可行性研究报告</div>
|
|
|
|
|
+ <span id="reportStatusTag" class="report-status" title="点击切换状态">草稿</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="editor-actions" style="display:flex;align-items:center;gap:8px;">
|
|
|
|
|
+ <button class="icon-btn" id="toggleViewBtn" title="切换:原文/标记" onclick="toggleView()"><i class="iconfont icon-BIG_PROMOTION"></i></button>
|
|
|
|
|
+ <button class="icon-btn" id="reportElementsTopBtn" title="报告要素" onclick="openReportElementsModal()"><i class="iconfont icon-COMPONENTS"></i></button>
|
|
|
|
|
+ <button class="icon-btn" id="exportBtn" title="导出" onclick="showExportMenu(this)"><i class="iconfont icon-UPLOAD"></i></button>
|
|
|
|
|
+ <button class="icon-btn" id="moreBtn" title="更多" onclick="showMoreMenu(this)">⋯</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="editor-scroll">
|
|
|
|
|
+ <!-- 原文视图 -->
|
|
|
|
|
+ <div class="editor-content" id="contentOriginal" contenteditable="true" oncontextmenu="showContextMenu(event)">
|
|
|
|
|
+ <h1>智慧园区建设项目可行性研究报告</h1>
|
|
|
|
|
+
|
|
|
|
|
+ <h2>一、项目背景</h2>
|
|
|
|
|
+ <p>随着数字经济的快速发展,智慧园区已成为推动产业升级和城市现代化的重要载体。本项目旨在构建集智能化管理、低碳绿色、产业协同于一体的新型智慧园区。</p>
|
|
|
|
|
+
|
|
|
|
|
+ <h3>1.1 行业现状</h3>
|
|
|
|
|
+ <p>根据最新市场调研数据显示,2024年中国智慧园区市场规模已达到1,789亿元,同比增长18%,预计2025年将突破2,100亿元。</p>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 数据表格 -->
|
|
|
|
|
+ <div class="data-table-card">
|
|
|
|
|
+ <div class="data-table-header">
|
|
|
|
|
+ <div class="data-table-title">
|
|
|
|
|
+ <span>📊</span>
|
|
|
|
|
+ <span>市场规模数据</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="data-table-source">来源: 市场调研数据.pdf</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <table class="data-table">
|
|
|
|
|
+ <thead>
|
|
|
|
|
+ <tr>
|
|
|
|
|
+ <th>年份</th>
|
|
|
|
|
+ <th>市场规模(亿元)</th>
|
|
|
|
|
+ <th>同比增长</th>
|
|
|
|
|
+ </tr>
|
|
|
|
|
+ </thead>
|
|
|
|
|
+ <tbody>
|
|
|
|
|
+ <tr><td>2022</td><td>1,280</td><td>15.2%</td></tr>
|
|
|
|
|
+ <tr><td>2023</td><td>1,516</td><td>18.4%</td></tr>
|
|
|
|
|
+ <tr><td>2024</td><td>1,789</td><td>18.0%</td></tr>
|
|
|
|
|
+ <tr><td>2025E</td><td>2,100</td><td>17.4%</td></tr>
|
|
|
|
|
+ </tbody>
|
|
|
|
|
+ </table>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <h2>二、项目概述</h2>
|
|
|
|
|
+ <p>本项目位于华南地区核心区域,规划总面积约50万平方米,预计总投资12.5亿元。项目将分三期建设,首期重点打造智能制造产业集群和数字服务中心。</p>
|
|
|
|
|
+
|
|
|
|
|
+ <h3>2.1 项目定位</h3>
|
|
|
|
|
+ <p>本项目致力于打造粤港澳大湾区最具代表性的智慧园区标杆项目,通过引入先进的信息技术和管理理念,实现产业数字化转型和高质量发展。</p>
|
|
|
|
|
+
|
|
|
|
|
+ <h3>2.2 建设内容</h3>
|
|
|
|
|
+ <p>项目建设内容主要包括智慧基础设施建设、产业服务平台搭建、数字化管理平台开发等三大方面,总建筑面积约35万平方米。</p>
|
|
|
|
|
+
|
|
|
|
|
+ <h2>三、投资估算</h2>
|
|
|
|
|
+ <p>项目总投资为12.5亿元,其中建设投资10.2亿元,流动资金2.3亿元。资金来源包括政府投资4亿元、企业自筹6.5亿元、银行贷款2亿元。</p>
|
|
|
|
|
+
|
|
|
|
|
+ <h2>四、市场分析</h2>
|
|
|
|
|
+ <p>当前智慧园区市场需求旺盛,随着产业数字化转型的加速推进,预计未来5年内智慧园区市场规模将保持20%以上的年增长率。</p>
|
|
|
|
|
+
|
|
|
|
|
+ <h2>五、经济效益分析</h2>
|
|
|
|
|
+ <p>项目建设期预计3年,运营期20年。项目建成后预计年营业收入8.6亿元,年利润总额2.4亿元,投资回收期约为6.2年。</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 标记视图 -->
|
|
|
|
|
+ <div class="editor-content" id="contentMarked" style="display:none;" contenteditable="true" oncontextmenu="showContextMenu(event)">
|
|
|
|
|
+ <h1>智慧园区建设项目可行性研究报告</h1>
|
|
|
|
|
+
|
|
|
|
|
+ <h2>一、项目背景</h2>
|
|
|
|
|
+ <p>随着<span class="entity-highlight" onclick="showDataRelationModal('数字经济', '产业趋势', 'concept', '技术方案说明.pdf')" contenteditable="false">数字经济</span>的快速发展,<span class="entity-highlight" onclick="showDataRelationModal('智慧园区', '核心实体', 'entity', '项目可行性研究报告.docx')" contenteditable="false">智慧园区</span>已成为推动<span class="entity-highlight" onclick="showDataRelationModal('产业升级', '概念', 'concept', '项目可行性研究报告.docx')" contenteditable="false">产业升级</span>和<span class="entity-highlight" onclick="showDataRelationModal('城市现代化', '概念', 'concept', '项目可行性研究报告.docx')" contenteditable="false">城市现代化</span>的重要载体。本项目旨在构建集<span class="entity-highlight" onclick="showDataRelationModal('智能化管理', '技术', 'concept', '技术方案说明.pdf')" contenteditable="false">智能化管理</span>、<span class="entity-highlight" onclick="showDataRelationModal('低碳绿色', '概念', 'concept', '项目可行性研究报告.docx')" contenteditable="false">低碳绿色</span>、<span class="entity-highlight" onclick="showDataRelationModal('产业协同', '模式', 'concept', '项目可行性研究报告.docx')" contenteditable="false">产业协同</span>于一体的新型智慧园区。</p>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- AI优化建议卡片 -->
|
|
|
|
|
+ <div class="ai-suggestion-card" id="aiSuggestionCard" contenteditable="false">
|
|
|
|
|
+ <div class="ai-suggestion-header">
|
|
|
|
|
+ <span class="ai-suggestion-icon">💡</span>
|
|
|
|
|
+ <span class="ai-suggestion-title">AI 优化建议</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="ai-suggestion-content">
|
|
|
|
|
+ 此处可补充具体的政策文件引用,增强论述的权威性。已从《市场调研数据.pdf》中提取到《"十四五"数字经济发展规划》等相关政策信息。
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="ai-suggestion-actions">
|
|
|
|
|
+ <button class="suggest-btn accept" onclick="acceptSuggestion()">✓ 采纳建议</button>
|
|
|
|
|
+ <button class="suggest-btn ignore" onclick="ignoreSuggestion()">✕ 忽略</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <h3>1.1 行业现状</h3>
|
|
|
|
|
+ <p>根据最新市场调研数据显示,2024年中国<span class="entity-highlight" onclick="showDataRelationModal('智慧园区', '核心实体', 'entity', '市场调研数据.pdf')" contenteditable="false">智慧园区</span>市场规模已达到<span class="entity-highlight" onclick="showDataRelationModal('1,789亿元', '市场规模数据', 'data', '市场调研数据.pdf')" contenteditable="false">1,789亿元</span>,同比增长<span class="entity-highlight" onclick="showDataRelationModal('18%', '增长率数据', 'data', '市场调研数据.pdf')" contenteditable="false">18%</span>,预计2025年将突破<span class="entity-highlight" onclick="showDataRelationModal('2,100亿元', '预测数据', 'data', '市场调研数据.pdf')" contenteditable="false">2,100亿元</span>。</p>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 数据表格 -->
|
|
|
|
|
+ <div class="data-table-card" contenteditable="false">
|
|
|
|
|
+ <div class="data-table-header">
|
|
|
|
|
+ <div class="data-table-title">
|
|
|
|
|
+ <span>📊</span>
|
|
|
|
|
+ <span>市场规模数据</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="data-table-source">来源: 市场调研数据.pdf</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <table class="data-table">
|
|
|
|
|
+ <thead>
|
|
|
|
|
+ <tr>
|
|
|
|
|
+ <th>年份</th>
|
|
|
|
|
+ <th>市场规模(亿元)</th>
|
|
|
|
|
+ <th>同比增长</th>
|
|
|
|
|
+ </tr>
|
|
|
|
|
+ </thead>
|
|
|
|
|
+ <tbody>
|
|
|
|
|
+ <tr><td>2022</td><td><span class="entity-highlight" onclick="showDataRelationModal('1,280亿元', '历史数据', 'data', '市场调研数据.pdf')" contenteditable="false">1,280</span></td><td><span class="entity-highlight" onclick="showDataRelationModal('15.2%', '增长率', 'data', '市场调研数据.pdf')" contenteditable="false">15.2%</span></td></tr>
|
|
|
|
|
+ <tr><td>2023</td><td><span class="entity-highlight" onclick="showDataRelationModal('1,516亿元', '历史数据', 'data', '市场调研数据.pdf')" contenteditable="false">1,516</span></td><td><span class="entity-highlight" onclick="showDataRelationModal('18.4%', '增长率', 'data', '市场调研数据.pdf')" contenteditable="false">18.4%</span></td></tr>
|
|
|
|
|
+ <tr><td>2024</td><td><span class="entity-highlight" onclick="showDataRelationModal('1,789亿元', '当前数据', 'data', '市场调研数据.pdf')" contenteditable="false">1,789</span></td><td><span class="entity-highlight" onclick="showDataRelationModal('18.0%', '增长率', 'data', '市场调研数据.pdf')" contenteditable="false">18.0%</span></td></tr>
|
|
|
|
|
+ <tr><td><span class="entity-highlight" onclick="showDataRelationModal('2025E', '预测年份', 'data', '市场调研数据.pdf')" contenteditable="false">2025E</span></td><td><span class="entity-highlight" onclick="showDataRelationModal('2,100亿元', '预测数据', 'data', '市场调研数据.pdf')" contenteditable="false">2,100</span></td><td><span class="entity-highlight" onclick="showDataRelationModal('17.4%', '预测增长率', 'data', '市场调研数据.pdf')" contenteditable="false">17.4%</span></td></tr>
|
|
|
|
|
+ </tbody>
|
|
|
|
|
+ </table>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <h2>二、项目概述</h2>
|
|
|
|
|
+ <p>本项目位于<span class="entity-highlight" onclick="showDataRelationModal('华南地区', '地理位置', 'location', '项目可行性研究报告.docx')" contenteditable="false">华南地区</span>核心区域,规划总面积约<span class="entity-highlight" onclick="showDataRelationModal('50万平方米', '面积数据', 'data', '项目可行性研究报告.docx')" contenteditable="false">50万平方米</span>,预计总投资<span class="entity-highlight" onclick="showDataRelationModal('12.5亿元', '投资额', 'data', '项目可行性研究报告.docx')" contenteditable="false">12.5亿元</span>。项目将分<span class="entity-highlight" onclick="showDataRelationModal('三期', '建设周期', 'data', '项目可行性研究报告.docx')" contenteditable="false">三期</span>建设,首期重点打造<span class="entity-highlight" onclick="showDataRelationModal('智能制造产业集群', '产业类型', 'concept', '项目可行性研究报告.docx')" contenteditable="false">智能制造产业集群</span>和<span class="entity-highlight" onclick="showDataRelationModal('数字服务中心', '基础设施', 'concept', '项目可行性研究报告.docx')" contenteditable="false">数字服务中心</span>。</p>
|
|
|
|
|
+
|
|
|
|
|
+ <h3>2.1 项目定位</h3>
|
|
|
|
|
+ <p>本项目致力于打造<span class="entity-highlight" onclick="showDataRelationModal('粤港澳大湾区', '区域', 'location', '项目可行性研究报告.docx')" contenteditable="false">粤港澳大湾区</span>最具代表性的<span class="entity-highlight" onclick="showDataRelationModal('智慧园区标杆项目', '项目类型', 'concept', '项目可行性研究报告.docx')" contenteditable="false">智慧园区标杆项目</span>,通过引入先进的信息技术和管理理念,实现<span class="entity-highlight" onclick="showDataRelationModal('产业数字化转型', '转型目标', 'concept', '项目可行性研究报告.docx')" contenteditable="false">产业数字化转型</span>和<span class="entity-highlight" onclick="showDataRelationModal('高质量发展', '发展目标', 'concept', '项目可行性研究报告.docx')" contenteditable="false">高质量发展</span>。</p>
|
|
|
|
|
+
|
|
|
|
|
+ <h3>2.2 建设内容</h3>
|
|
|
|
|
+ <p>项目建设内容主要包括<span class="entity-highlight" onclick="showDataRelationModal('智慧基础设施', '建设内容', 'concept', '项目可行性研究报告.docx')" contenteditable="false">智慧基础设施</span>建设、<span class="entity-highlight" onclick="showDataRelationModal('产业服务平台', '建设内容', 'concept', '项目可行性研究报告.docx')" contenteditable="false">产业服务平台</span>搭建、<span class="entity-highlight" onclick="showDataRelationModal('数字化管理平台', '建设内容', 'concept', '项目可行性研究报告.docx')" contenteditable="false">数字化管理平台</span>开发等三大方面,总建筑面积约<span class="entity-highlight" onclick="showDataRelationModal('35万平方米', '建筑面积', 'data', '项目可行性研究报告.docx')" contenteditable="false">35万平方米</span>。</p>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 数据引用卡片 -->
|
|
|
|
|
+ <div class="data-reference-card" contenteditable="false">
|
|
|
|
|
+ <div class="reference-header">
|
|
|
|
|
+ <span class="reference-icon">🔗</span>
|
|
|
|
|
+ <span class="reference-title">数据引用验证</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="reference-content">
|
|
|
|
|
+ <div class="reference-item">
|
|
|
|
|
+ <span class="reference-label">来源文档:</span>
|
|
|
|
|
+ <span class="reference-value">技术方案说明.pdf (第3页)</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="reference-item">
|
|
|
|
|
+ <span class="reference-label">验证状态:</span>
|
|
|
|
|
+ <span class="reference-value success">✓ 已验证</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="reference-item">
|
|
|
|
|
+ <span class="reference-label">置信度:</span>
|
|
|
|
|
+ <span class="reference-value">95.8%</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <h2>三、投资估算</h2>
|
|
|
|
|
+ <p>项目总投资为<span class="entity-highlight" onclick="showDataRelationModal('12.5亿元', '总投资', 'data', '财务预测表.xlsx')" contenteditable="false">12.5亿元</span>,其中建设投资<span class="entity-highlight" onclick="showDataRelationModal('10.2亿元', '建设投资', 'data', '财务预测表.xlsx')" contenteditable="false">10.2亿元</span>,流动资金<span class="entity-highlight" onclick="showDataRelationModal('2.3亿元', '流动资金', 'data', '财务预测表.xlsx')" contenteditable="false">2.3亿元</span>。资金来源包括政府投资<span class="entity-highlight" onclick="showDataRelationModal('4亿元', '政府投资', 'data', '财务预测表.xlsx')" contenteditable="false">4亿元</span>、企业自筹<span class="entity-highlight" onclick="showDataRelationModal('6.5亿元', '自筹资金', 'data', '财务预测表.xlsx')" contenteditable="false">6.5亿元</span>、银行贷款<span class="entity-highlight" onclick="showDataRelationModal('2亿元', '贷款资金', 'data', '财务预测表.xlsx')" contenteditable="false">2亿元</span>。</p>
|
|
|
|
|
+
|
|
|
|
|
+ <h2>四、市场分析</h2>
|
|
|
|
|
+ <p>当前<span class="entity-highlight" onclick="showDataRelationModal('智慧园区', '产品', 'entity', '市场调研数据.pdf')" contenteditable="false">智慧园区</span>市场需求旺盛,随着<span class="entity-highlight" onclick="showDataRelationModal('产业数字化转型', '趋势', 'concept', '市场调研数据.pdf')" contenteditable="false">产业数字化转型</span>的加速推进,预计未来<span class="entity-highlight" onclick="showDataRelationModal('5年', '时间周期', 'data', '市场调研数据.pdf')" contenteditable="false">5年</span>内智慧园区市场规模将保持<span class="entity-highlight" onclick="showDataRelationModal('20%', '增长率预测', 'data', '市场调研数据.pdf')" contenteditable="false">20%</span>以上的年增长率。</p>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 竞争分析卡片 -->
|
|
|
|
|
+ <div class="competition-card" contenteditable="false">
|
|
|
|
|
+ <div class="competition-header">
|
|
|
|
|
+ <span class="competition-icon">🏆</span>
|
|
|
|
|
+ <span class="competition-title">竞争格局分析</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="competition-content">
|
|
|
|
|
+ <div class="competition-item">
|
|
|
|
|
+ <div class="competitor-name">领先企业A</div>
|
|
|
|
|
+ <div class="competitor-share">市场份额: 28%</div>
|
|
|
|
|
+ <div class="competitor-strength">优势: 技术领先</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="competition-item">
|
|
|
|
|
+ <div class="competitor-name">新兴企业B</div>
|
|
|
|
|
+ <div class="competitor-share">市场份额: 18%</div>
|
|
|
|
|
+ <div class="competitor-strength">优势: 服务创新</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="competition-item">
|
|
|
|
|
+ <div class="competitor-name">本项目</div>
|
|
|
|
|
+ <div class="competitor-share">目标份额: 15%</div>
|
|
|
|
|
+ <div class="competitor-strength">优势: 区域特色</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <h2>五、经济效益分析</h2>
|
|
|
|
|
+ <p>项目建设期预计<span class="entity-highlight" onclick="showDataRelationModal('3年', '建设期', 'data', '财务预测表.xlsx')" contenteditable="false">3年</span>,运营期<span class="entity-highlight" onclick="showDataRelationModal('20年', '运营期', 'data', '财务预测表.xlsx')" contenteditable="false">20年</span>。项目建成后预计年营业收入<span class="entity-highlight" onclick="showDataRelationModal('8.6亿元', '年收入', 'data', '财务预测表.xlsx')" contenteditable="false">8.6亿元</span>,年利润总额<span class="entity-highlight" onclick="showDataRelationModal('2.4亿元', '年利润', 'data', '财务预测表.xlsx')" contenteditable="false">2.4亿元</span>,投资回收期约为<span class="entity-highlight" onclick="showDataRelationModal('6.2年', '回收期', 'data', '财务预测表.xlsx')" contenteditable="false">6.2年</span>。</p>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 财务预测表格 -->
|
|
|
|
|
+ <div class="data-table-card" contenteditable="false">
|
|
|
|
|
+ <div class="data-table-header">
|
|
|
|
|
+ <div class="data-table-title">
|
|
|
|
|
+ <span>💰</span>
|
|
|
|
|
+ <span>财务预测数据</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="data-table-source">来源: 财务预测表.xlsx</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <table class="data-table">
|
|
|
|
|
+ <thead>
|
|
|
|
|
+ <tr>
|
|
|
|
|
+ <th>年份</th>
|
|
|
|
|
+ <th>营业收入(亿元)</th>
|
|
|
|
|
+ <th>利润总额(亿元)</th>
|
|
|
|
|
+ <th>投资回收</th>
|
|
|
|
|
+ </tr>
|
|
|
|
|
+ </thead>
|
|
|
|
|
+ <tbody>
|
|
|
|
|
+ <tr><td>第1年</td><td><span class="entity-highlight" onclick="showDataRelationModal('6.2亿元', '第1年收入', 'data', '财务预测表.xlsx')" contenteditable="false">6.2</span></td><td><span class="entity-highlight" onclick="showDataRelationModal('1.8亿元', '第1年利润', 'data', '财务预测表.xlsx')" contenteditable="false">1.8</span></td><td>运营初期</td></tr>
|
|
|
|
|
+ <tr><td>第2年</td><td><span class="entity-highlight" onclick="showDataRelationModal('7.4亿元', '第2年收入', 'data', '财务预测表.xlsx')" contenteditable="false">7.4</span></td><td><span class="entity-highlight" onclick="showDataRelationModal('2.1亿元', '第2年利润', 'data', '财务预测表.xlsx')" contenteditable="false">2.1</span></td><td>快速增长</td></tr>
|
|
|
|
|
+ <tr><td>第3年</td><td><span class="entity-highlight" onclick="showDataRelationModal('8.6亿元', '第3年收入', 'data', '财务预测表.xlsx')" contenteditable="false">8.6</span></td><td><span class="entity-highlight" onclick="showDataRelationModal('2.4亿元', '第3年利润', 'data', '财务预测表.xlsx')" contenteditable="false">2.4</span></td><td>达产年份</td></tr>
|
|
|
|
|
+ </tbody>
|
|
|
|
|
+ </table>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- AI生成内容建议 -->
|
|
|
|
|
+ <div class="ai-generated-card" contenteditable="false">
|
|
|
|
|
+ <div class="ai-generated-header">
|
|
|
|
|
+ <span class="ai-generated-icon">✨</span>
|
|
|
|
|
+ <span class="ai-generated-title">AI 生成内容</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="ai-generated-content">
|
|
|
|
|
+ <p><strong>风险分析:</strong></p>
|
|
|
|
|
+ <ul>
|
|
|
|
|
+ <li>技术风险:新兴技术应用的不确定性</li>
|
|
|
|
|
+ <li>市场风险:需求变化和竞争加剧</li>
|
|
|
|
|
+ <li>运营风险:团队建设和管理挑战</li>
|
|
|
|
|
+ <li>财务风险:资金链和投资回收压力</li>
|
|
|
|
|
+ </ul>
|
|
|
|
|
+ <div class="ai-generated-actions">
|
|
|
|
|
+ <button class="btn btn-primary" onclick="insertGeneratedContent()">➕ 应用内容</button>
|
|
|
|
|
+ <button class="btn" onclick="showToast('已忽略AI建议', 'info')">✕ 忽略</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 右侧AI助手面板 -->
|
|
|
|
|
+ <div class="right-panel">
|
|
|
|
|
+ <!-- 报告要素区(上半) -->
|
|
|
|
|
+ <div class="element-section">
|
|
|
|
|
+ <div class="module-title" role="button" aria-label="打开报告要素列表">
|
|
|
|
|
+ <div class="module-icon">📋</div>
|
|
|
|
|
+ <div class="module-text">报告要素</div>
|
|
|
|
|
+ <div class="elements-actions" style="display:flex;align-items:center;gap:8px;margin-left:8px;">
|
|
|
|
|
+ <!-- report elements opener moved to editor actions -->
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="element-header" style="display:flex;align-items:center;justify-content:space-between;padding-top:4px;border-top:1px solid transparent;">
|
|
|
|
|
+ <div class="element-tabs" style="display:flex;align-items:center;gap:8px;">
|
|
|
|
|
+ <div class="element-tab active" id="tabDynamic" onclick="switchElementTab('dynamic')">动态要素</div>
|
|
|
|
|
+ <div class="element-tab" id="tabStatic" onclick="switchElementTab('static')">静态要素</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <button class="icon-btn" id="elementSearchBtn" title="搜索" style="margin-left:8px;"><i class="iconfont icon-SEARCH"></i></button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 要素标签容器:动态 / 静态 两个面板,使用 tab 切换 -->
|
|
|
|
|
+ <div id="dynamicTags" class="element-tags-wrap">
|
|
|
|
|
+ <!-- 动态要素(AI 规则计算产生)示例 -->
|
|
|
|
|
+ <span class="element-tag dynamic" draggable="true" ondragstart="handleTagDragStart(event, '市场上升信号')" ondragend="handleTagDragEnd(event)" onclick="showTagPopover(event, 'dynamic1')">
|
|
|
|
|
+ <span class="tag-icon">📈</span>
|
|
|
|
|
+ <span class="tag-name">市场上升信号</span>
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <span class="element-tag dynamic" draggable="true" ondragstart="handleTagDragStart(event, '增长偏好')" ondragend="handleTagDragEnd(event)" onclick="showTagPopover(event, 'dynamic2')">
|
|
|
|
|
+ <span class="tag-icon">⚡</span>
|
|
|
|
|
+ <span class="tag-name">增长偏好</span>
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div id="staticTags" class="element-tags-wrap" style="display:none;">
|
|
|
|
|
+ <!-- 静态要素:直接来自原文采集(保留原有示例) -->
|
|
|
|
|
+ <span class="element-tag static entity" draggable="true" ondragstart="handleTagDragStart(event, '智慧园区')" ondragend="handleTagDragEnd(event)" onclick="showTagPopover(event, 'smartpark')">
|
|
|
|
|
+ <span class="tag-icon">🏢</span>
|
|
|
|
|
+ <span class="tag-name">智慧园区</span>
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <span class="element-tag static concept" draggable="true" ondragstart="handleTagDragStart(event, '产业升级')" ondragend="handleTagDragEnd(event)" onclick="showTagPopover(event, 'upgrade')">
|
|
|
|
|
+ <span class="tag-icon">📈</span>
|
|
|
|
|
+ <span class="tag-name">产业升级</span>
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <span class="element-tag static concept" draggable="true" ondragstart="handleTagDragStart(event, '城市现代化')" ondragend="handleTagDragEnd(event)" onclick="showTagPopover(event, 'modern')">
|
|
|
|
|
+ <span class="tag-icon">🌆</span>
|
|
|
|
|
+ <span class="tag-name">城市现代化</span>
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <span class="element-tag static concept" draggable="true" ondragstart="handleTagDragStart(event, '智能化管理')" ondragend="handleTagDragEnd(event)" onclick="showTagPopover(event, 'ai')">
|
|
|
|
|
+ <span class="tag-icon">🤖</span>
|
|
|
|
|
+ <span class="tag-name">智能化管理</span>
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <span class="element-tag static data" draggable="true" ondragstart="handleTagDragStart(event, '1,789亿元')" ondragend="handleTagDragEnd(event)" onclick="showTagPopover(event, 'data1')">
|
|
|
|
|
+ <span class="tag-icon">💰</span>
|
|
|
|
|
+ <span class="tag-name">1,789亿元</span>
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <span class="element-tag static data" draggable="true" ondragstart="handleTagDragStart(event, '18%')" ondragend="handleTagDragEnd(event)" onclick="showTagPopover(event, 'data2')">
|
|
|
|
|
+ <span class="tag-icon">📊</span>
|
|
|
|
|
+ <span class="tag-name">18%</span>
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 要素详情弹出框 -->
|
|
|
|
|
+ <div class="element-popover" id="elementPopover">
|
|
|
|
|
+ <div class="popover-header">
|
|
|
|
|
+ <div class="popover-icon entity" id="popoverIcon">🏢</div>
|
|
|
|
|
+ <div class="popover-title" id="popoverTitle">智慧园区</div>
|
|
|
|
|
+ <button class="popover-close" onclick="hideTagPopover()">×</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="popover-body">
|
|
|
|
|
+ <div class="popover-section">
|
|
|
|
|
+ <div class="popover-label">类型</div>
|
|
|
|
|
+ <div class="popover-value" id="popoverType">核心实体</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="popover-section">
|
|
|
|
|
+ <div class="popover-label">来源</div>
|
|
|
|
|
+ <div class="popover-value" id="popoverSource">项目可行性研究报告.docx</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="popover-section">
|
|
|
|
|
+ <div class="popover-label">关联要素</div>
|
|
|
|
|
+ <div class="popover-relations" id="popoverRelations">
|
|
|
|
|
+ <span class="popover-relation">→ 产业升级</span>
|
|
|
|
|
+ <span class="popover-relation">→ 城市现代化</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="popover-actions">
|
|
|
|
|
+ <button class="btn" onclick="showToast('已定位到文档', 'info');hideTagPopover();">📍 定位</button>
|
|
|
|
|
+ <button class="btn btn-primary" onclick="insertTagToEditor();hideTagPopover();">➕ 插入</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- AI助手区 -->
|
|
|
|
|
+ <div class="ai-assistant">
|
|
|
|
|
+ <div class="ai-header">
|
|
|
|
|
+ <div class="ai-avatar-sm">🤖</div>
|
|
|
|
|
+ <div class="ai-info">
|
|
|
|
|
+ <div class="ai-name">AI助手</div>
|
|
|
|
|
+ <div class="ai-status">● 已加载项目上下文</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- AI Tab切换 -->
|
|
|
|
|
+ <div class="ai-tabs">
|
|
|
|
|
+ <div class="ai-tab active" onclick="switchAiTab(this, 'chat')">💬 对话</div>
|
|
|
|
|
+ <div class="ai-tab" onclick="switchAiTab(this, 'suggest')">💡 建议</div>
|
|
|
|
|
+ <div class="ai-tab" onclick="switchAiTab(this, 'memory')">🧠 记忆</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- AI消息区 -->
|
|
|
|
|
+ <div class="ai-messages" id="aiMessages">
|
|
|
|
|
+ <div class="msg ai">
|
|
|
|
|
+ <div class="msg-avatar">🤖</div>
|
|
|
|
|
+ <div class="msg-bubble">您好!我已分析上传的5份文档,构建了项目知识图谱。有什么可以帮您的?</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="msg user">
|
|
|
|
|
+ <div class="msg-avatar">张</div>
|
|
|
|
|
+ <div class="msg-bubble">帮我补充市场分析部分的竞争格局内容</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="msg ai">
|
|
|
|
|
+ <div class="msg-avatar">🤖</div>
|
|
|
|
|
+ <div class="msg-bubble">好的,我已从《市场调研数据.pdf》中提取了竞争格局相关数据。建议如下:</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- AI输入区 -->
|
|
|
|
|
+ <div class="ai-input-area">
|
|
|
|
|
+ <div class="ai-input-inner">
|
|
|
|
|
+ <div class="ai-input-box">
|
|
|
|
|
+ <div class="ai-input-row">
|
|
|
|
|
+ <div class="ai-input-top">
|
|
|
|
|
+ <textarea id="aiTextarea" placeholder="发消息给AI助手" rows="1" onkeydown="handleAiKey(event)" oninput="autoResizeTextarea(this)"></textarea>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="ai-input-toolbar">
|
|
|
|
|
+ <div class="left">
|
|
|
|
|
+ <!-- 1: 自定义图标(icon-CREATE) -->
|
|
|
|
|
+ <button class="ai-icon-btn" id="aiIconCreateBtn" title="上传"><i class="iconfont icon-CREATE"></i></button>
|
|
|
|
|
+ <!-- 2: 将原来的 '@' 替换为 icon-a-SYMBOLS_ -->
|
|
|
|
|
+ <button class="ai-icon-btn" title="@" onclick="showToast('@', 'info')"><i class="iconfont icon-a-SYMBOLS_"></i></button>
|
|
|
|
|
+ <!-- 3: 将 '联网' 替换为 icon-hulianwangoff -->
|
|
|
|
|
+ <button class="ai-icon-btn" title="联网" onclick="showToast('联网搜索(示意)', 'info')"><i class="iconfont icon-hulianwangoff"></i></button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="right">
|
|
|
|
|
+ <!-- 4: 将语音输入图标替换为 icon-01 -->
|
|
|
|
|
+ <button class="ai-icon-btn" title="语音输入" onclick="showToast('语音输入(示意)', 'info')"><i class="iconfont icon-01"></i></button>
|
|
|
|
|
+ <button class="ai-send-btn" id="aiSendBtn" title="发送" onclick="sendAiMsg()">⬆</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <!-- hint removed as requested -->
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 报告要素弹窗 -->
|
|
|
|
|
+ <div class="report-elements-modal" id="reportElementsModal" aria-hidden="true">
|
|
|
|
|
+ <div class="report-elements-card" role="dialog" aria-modal="true" aria-labelledby="reportElementsTitle">
|
|
|
|
|
+ <div class="report-elements-header">
|
|
|
|
|
+ <div class="report-elements-title" id="reportElementsTitle">报告要素</div>
|
|
|
|
|
+ <div class="elements-search">
|
|
|
|
|
+ <input type="text" id="elementsSearchInput" placeholder="搜索要素名称 / 类型..." />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="report-elements-body">
|
|
|
|
|
+ <table class="elements-table" id="elementsTable">
|
|
|
|
|
+ <thead>
|
|
|
|
|
+ <tr>
|
|
|
|
|
+ <th style="width:14%;">名称</th>
|
|
|
|
|
+ <th style="width:18%;">描述</th>
|
|
|
|
|
+ <th style="width:8%;">类型</th>
|
|
|
|
|
+ <th style="width:10%;">要素类型</th>
|
|
|
|
|
+ <th style="width:12%;">原值</th>
|
|
|
|
|
+ <th style="width:12%;">新值</th>
|
|
|
|
|
+ <th style="width:12%;">填充源</th>
|
|
|
|
|
+ <th style="width:8%;">操作</th>
|
|
|
|
|
+ </tr>
|
|
|
|
|
+ </thead>
|
|
|
|
|
+ <tbody id="elementsTbody">
|
|
|
|
|
+ <!-- 动态生成 -->
|
|
|
|
|
+ </tbody>
|
|
|
|
|
+ </table>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="report-elements-footer">
|
|
|
|
|
+ <div style="margin-right:auto;display:flex;gap:12px;align-items:center;">
|
|
|
|
|
+ <!-- 翻页控件已移除:改由分页数字按钮显示(默认显示最多 5 个页码) -->
|
|
|
|
|
+ <div class="pagination" id="elementsPaginationInfo" style="font-size:12px;color:var(--text3);"></div>
|
|
|
|
|
+ <div style="display:flex;align-items:center;gap:8px;margin-left:12px;">
|
|
|
|
|
+ <label style="font-size:12px;color:var(--text3);">每页显示:</label>
|
|
|
|
|
+ <select id="elementsPageSize" class="page-size-select">
|
|
|
|
|
+ <option value="10">10</option>
|
|
|
|
|
+ <option value="20">20</option>
|
|
|
|
|
+ <option value="50">50</option>
|
|
|
|
|
+ </select>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <button class="btn" onclick="closeReportElementsModal()">取消</button>
|
|
|
|
|
+ <button class="btn btn-primary" id="saveElementsBtn">保存全部</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 右键菜单 -->
|
|
|
|
|
+ <div class="context-menu" id="contextMenu">
|
|
|
|
|
+ <div class="context-menu-item" onclick="execContextAction('copy')">
|
|
|
|
|
+ <span class="icon">📋</span>
|
|
|
|
|
+ <span>复制</span>
|
|
|
|
|
+ <span class="shortcut">Ctrl+C</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="context-menu-item" onclick="execContextAction('cut')">
|
|
|
|
|
+ <span class="icon">✂️</span>
|
|
|
|
|
+ <span>剪切</span>
|
|
|
|
|
+ <span class="shortcut">Ctrl+X</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="context-menu-item" onclick="execContextAction('paste')">
|
|
|
|
|
+ <span class="icon">📄</span>
|
|
|
|
|
+ <span>粘贴</span>
|
|
|
|
|
+ <span class="shortcut">Ctrl+V</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="context-menu-divider"></div>
|
|
|
|
|
+ <div class="context-menu-item" onclick="execContextAction('polish')">
|
|
|
|
|
+ <span class="icon">✨</span>
|
|
|
|
|
+ <span>AI 润色</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="context-menu-item" onclick="execContextAction('spell')">
|
|
|
|
|
+ <span class="icon">📝</span>
|
|
|
|
|
+ <span>检查拼写</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="context-menu-divider"></div>
|
|
|
|
|
+ <div class="context-menu-item" onclick="execContextAction('mark')">
|
|
|
|
|
+ <span class="icon">🏷️</span>
|
|
|
|
|
+ <span>标记为要素</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="context-menu-item" onclick="execContextAction('quote')">
|
|
|
|
|
+ <span class="icon">💬</span>
|
|
|
|
|
+ <span>引用到AI助手</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 数据关系表弹窗 -->
|
|
|
|
|
+ <div class="data-relation-modal" id="dataRelationModal">
|
|
|
|
|
+ <div class="data-relation-card">
|
|
|
|
|
+ <div class="data-relation-header">
|
|
|
|
|
+ <div class="data-relation-icon" id="relationIcon">🏷️</div>
|
|
|
|
|
+ <div class="data-relation-title">
|
|
|
|
|
+ <h3 id="relationEntityName">标签数据关系</h3>
|
|
|
|
|
+ <span id="relationEntityType">实体类型 · 数据来源</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <button class="data-relation-close" onclick="closeDataRelationModal()">×</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="data-relation-body">
|
|
|
|
|
+ <div class="relation-section">
|
|
|
|
|
+ <div class="relation-label">📊 数据关系表</div>
|
|
|
|
|
+ <table class="relation-table">
|
|
|
|
|
+ <thead>
|
|
|
|
|
+ <tr>
|
|
|
|
|
+ <th style="width:25%">属性</th>
|
|
|
|
|
+ <th style="width:30%">原始值</th>
|
|
|
|
|
+ <th style="width:30%">当前标签值</th>
|
|
|
|
|
+ <th style="width:15%">操作</th>
|
|
|
|
|
+ </tr>
|
|
|
|
|
+ </thead>
|
|
|
|
|
+ <tbody id="relationTableBody">
|
|
|
|
|
+ <!-- 动态生成 -->
|
|
|
|
|
+ </tbody>
|
|
|
|
|
+ </table>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="relation-section">
|
|
|
|
|
+ <div class="relation-label">🔗 关联要素</div>
|
|
|
|
|
+ <div class="relation-tags" id="relationTags">
|
|
|
|
|
+ <!-- 动态生成关联标签 -->
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="data-relation-footer">
|
|
|
|
|
+ <button class="btn" onclick="showToast('已删除标记', 'info');closeDataRelationModal();">🗑️ 删除标记</button>
|
|
|
|
|
+ <button class="btn" onclick="closeDataRelationModal()">取消</button>
|
|
|
|
|
+ <button class="btn btn-primary" onclick="saveDataRelationChanges()">保存更改</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 导出菜单 -->
|
|
|
|
|
+ <div class="export-menu" id="exportMenu" style="position:fixed;background:var(--white);border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,0.15);min-width:180px;display:none;z-index:2001;overflow:hidden;">
|
|
|
|
|
+ <div style="padding:12px 16px;cursor:pointer;font-size:13px;display:flex;align-items:center;gap:10px;transition:background 0.15s;" onmouseover="this.style.background='var(--primary-light)'" onmouseout="this.style.background=''" onclick="showToast('导出PDF成功', 'success');hideExportMenu()">
|
|
|
|
|
+ <span>📕</span><span>导出为 PDF</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div style="padding:12px 16px;cursor:pointer;font-size:13px;display:flex;align-items:center;gap:10px;transition:background 0.15s;" onmouseover="this.style.background='var(--primary-light)'" onmouseout="this.style.background=''" onclick="showToast('导出Word成功', 'success');hideExportMenu()">
|
|
|
|
|
+ <span>📘</span><span>导出为 Word</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 要素关系图谱弹窗 -->
|
|
|
|
|
+ <div class="knowledge-graph-modal" id="knowledgeGraphModal">
|
|
|
|
|
+ <div class="kg-header">
|
|
|
|
|
+ <div class="kg-title">
|
|
|
|
|
+ <span class="kg-icon">🔗</span>
|
|
|
|
|
+ <span>要素关系图谱</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="kg-controls">
|
|
|
|
|
+ <div class="kg-view-toggle">
|
|
|
|
|
+ <button class="kg-view-btn active" onclick="switchGraphView('graph')" id="graphViewBtn">
|
|
|
|
|
+ <span>🕸️</span>
|
|
|
|
|
+ <span>图谱视图</span>
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button class="kg-view-btn" onclick="switchGraphView('list')" id="listViewBtn">
|
|
|
|
|
+ <span>📋</span>
|
|
|
|
|
+ <span>列表视图</span>
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <button class="kg-close" onclick="closeKnowledgeGraph()">×</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="kg-content">
|
|
|
|
|
+ <!-- 图谱视图 -->
|
|
|
|
|
+ <div class="kg-graph-view" id="graphView">
|
|
|
|
|
+ <div class="graph-canvas" id="graphCanvas">
|
|
|
|
|
+ <div class="graph-node core" style="left: 50%; top: 50%; transform: translate(-50%, -50%);" data-entity="智慧园区">
|
|
|
|
|
+ <div class="node-icon">🏢</div>
|
|
|
|
|
+ <div class="node-label">智慧园区</div>
|
|
|
|
|
+ <div class="node-type">核心实体</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="graph-node concept" style="left: 25%; top: 30%;" data-entity="产业升级">
|
|
|
|
|
+ <div class="node-icon">📈</div>
|
|
|
|
|
+ <div class="node-label">产业升级</div>
|
|
|
|
|
+ <div class="node-type">概念</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="graph-node concept" style="left: 75%; top: 30%;" data-entity="城市现代化">
|
|
|
|
|
+ <div class="node-icon">🌆</div>
|
|
|
|
|
+ <div class="node-label">城市现代化</div>
|
|
|
|
|
+ <div class="node-type">概念</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="graph-node concept" style="left: 15%; top: 70%;" data-entity="智能化管理">
|
|
|
|
|
+ <div class="node-icon">🤖</div>
|
|
|
|
|
+ <div class="node-label">智能化管理</div>
|
|
|
|
|
+ <div class="node-type">技术</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="graph-node data" style="left: 40%; top: 20%;" data-entity="1,789亿元">
|
|
|
|
|
+ <div class="node-icon">💰</div>
|
|
|
|
|
+ <div class="node-label">1,789亿元</div>
|
|
|
|
|
+ <div class="node-type">市场规模</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="graph-node data" style="left: 60%; top: 80%;" data-entity="12.5亿元">
|
|
|
|
|
+ <div class="node-icon">💵</div>
|
|
|
|
|
+ <div class="node-label">12.5亿元</div>
|
|
|
|
|
+ <div class="node-type">投资额</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="graph-node location" style="left: 85%; top: 70%;" data-entity="华南地区">
|
|
|
|
|
+ <div class="node-icon">📍</div>
|
|
|
|
|
+ <div class="node-label">华南地区</div>
|
|
|
|
|
+ <div class="node-type">地理位置</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 关系连线 -->
|
|
|
|
|
+ <svg class="graph-lines" width="100%" height="100%">
|
|
|
|
|
+ <defs>
|
|
|
|
|
+ <marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
|
|
|
|
|
+ <polygon points="0 0, 10 3.5, 0 7" fill="#1890ff" opacity="0.6"/>
|
|
|
|
|
+ </marker>
|
|
|
|
|
+ </defs>
|
|
|
|
|
+ <!-- 核心关系 -->
|
|
|
|
|
+ <line x1="50%" y1="50%" x2="25%" y2="30%" stroke="#1890ff" stroke-width="2" opacity="0.6" marker-end="url(#arrowhead)"/>
|
|
|
|
|
+ <line x1="50%" y1="50%" x2="75%" y2="30%" stroke="#1890ff" stroke-width="2" opacity="0.6" marker-end="url(#arrowhead)"/>
|
|
|
|
|
+ <line x1="50%" y1="50%" x2="15%" y2="70%" stroke="#1890ff" stroke-width="2" opacity="0.6" marker-end="url(#arrowhead)"/>
|
|
|
|
|
+ <line x1="50%" y1="50%" x2="40%" y2="20%" stroke="#1890ff" stroke-width="2" opacity="0.6" marker-end="url(#arrowhead)"/>
|
|
|
|
|
+ <line x1="50%" y1="50%" x2="60%" y2="80%" stroke="#1890ff" stroke-width="2" opacity="0.6" marker-end="url(#arrowhead)"/>
|
|
|
|
|
+ <line x1="50%" y1="50%" x2="85%" y2="70%" stroke="#1890ff" stroke-width="2" opacity="0.6" marker-end="url(#arrowhead)"/>
|
|
|
|
|
+ </svg>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="graph-legend">
|
|
|
|
|
+ <div class="legend-item"><span class="legend-dot core"></span><span>核心实体</span></div>
|
|
|
|
|
+ <div class="legend-item"><span class="legend-dot concept"></span><span>概念</span></div>
|
|
|
|
|
+ <div class="legend-item"><span class="legend-dot data"></span><span>数据</span></div>
|
|
|
|
|
+ <div class="legend-item"><span class="legend-dot location"></span><span>地点</span></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 列表视图 -->
|
|
|
|
|
+ <div class="kg-list-view" id="listView" style="display: none;">
|
|
|
|
|
+ <div class="list-search">
|
|
|
|
|
+ <input type="text" placeholder="🔍 搜索要素..." class="list-search-input" oninput="filterEntities(this.value)">
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="entity-categories">
|
|
|
|
|
+ <div class="category-section">
|
|
|
|
|
+ <div class="category-header">
|
|
|
|
|
+ <span class="category-icon">🏢</span>
|
|
|
|
|
+ <span class="category-title">核心实体 (1)</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="entity-items">
|
|
|
|
|
+ <div class="entity-item" onclick="highlightEntity('智慧园区')">
|
|
|
|
|
+ <div class="entity-icon">🏢</div>
|
|
|
|
|
+ <div class="entity-info">
|
|
|
|
|
+ <div class="entity-name">智慧园区</div>
|
|
|
|
|
+ <div class="entity-meta">6个关联 • 3个来源文档</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="entity-actions">
|
|
|
|
|
+ <button class="entity-action-btn" onclick="locateEntity('智慧园区')">📍</button>
|
|
|
|
|
+ <button class="entity-action-btn" onclick="editEntity('智慧园区')">✏️</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="category-section">
|
|
|
|
|
+ <div class="category-header">
|
|
|
|
|
+ <span class="category-icon">💡</span>
|
|
|
|
|
+ <span class="category-title">概念 (4)</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="entity-items">
|
|
|
|
|
+ <div class="entity-item" onclick="highlightEntity('产业升级')">
|
|
|
|
|
+ <div class="entity-icon">📈</div>
|
|
|
|
|
+ <div class="entity-info">
|
|
|
|
|
+ <div class="entity-name">产业升级</div>
|
|
|
|
|
+ <div class="entity-meta">2个关联 • 2个来源文档</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="entity-actions">
|
|
|
|
|
+ <button class="entity-action-btn" onclick="locateEntity('产业升级')">📍</button>
|
|
|
|
|
+ <button class="entity-action-btn" onclick="editEntity('产业升级')">✏️</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="entity-item" onclick="highlightEntity('城市现代化')">
|
|
|
|
|
+ <div class="entity-icon">🌆</div>
|
|
|
|
|
+ <div class="entity-info">
|
|
|
|
|
+ <div class="entity-name">城市现代化</div>
|
|
|
|
|
+ <div class="entity-meta">2个关联 • 1个来源文档</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="entity-actions">
|
|
|
|
|
+ <button class="entity-action-btn" onclick="locateEntity('城市现代化')">📍</button>
|
|
|
|
|
+ <button class="entity-action-btn" onclick="editEntity('城市现代化')">✏️</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="entity-item" onclick="highlightEntity('智能化管理')">
|
|
|
|
|
+ <div class="entity-icon">🤖</div>
|
|
|
|
|
+ <div class="entity-info">
|
|
|
|
|
+ <div class="entity-name">智能化管理</div>
|
|
|
|
|
+ <div class="entity-meta">1个关联 • 1个来源文档</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="entity-actions">
|
|
|
|
|
+ <button class="entity-action-btn" onclick="locateEntity('智能化管理')">📍</button>
|
|
|
|
|
+ <button class="entity-action-btn" onclick="editEntity('智能化管理')">✏️</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="entity-item" onclick="highlightEntity('低碳绿色')">
|
|
|
|
|
+ <div class="entity-icon">🌱</div>
|
|
|
|
|
+ <div class="entity-info">
|
|
|
|
|
+ <div class="entity-name">低碳绿色</div>
|
|
|
|
|
+ <div class="entity-meta">1个关联 • 1个来源文档</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="entity-actions">
|
|
|
|
|
+ <button class="entity-action-btn" onclick="locateEntity('低碳绿色')">📍</button>
|
|
|
|
|
+ <button class="entity-action-btn" onclick="editEntity('低碳绿色')">✏️</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="category-section">
|
|
|
|
|
+ <div class="category-header">
|
|
|
|
|
+ <span class="category-icon">📊</span>
|
|
|
|
|
+ <span class="category-title">数据 (8)</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="entity-items">
|
|
|
|
|
+ <div class="entity-item" onclick="highlightEntity('1,789亿元')">
|
|
|
|
|
+ <div class="entity-icon">💰</div>
|
|
|
|
|
+ <div class="entity-info">
|
|
|
|
|
+ <div class="entity-name">1,789亿元</div>
|
|
|
|
|
+ <div class="entity-meta">市场规模数据 • 1个来源</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="entity-actions">
|
|
|
|
|
+ <button class="entity-action-btn" onclick="locateEntity('1,789亿元')">📍</button>
|
|
|
|
|
+ <button class="entity-action-btn" onclick="editEntity('1,789亿元')">✏️</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="entity-item" onclick="highlightEntity('18%')">
|
|
|
|
|
+ <div class="entity-icon">📊</div>
|
|
|
|
|
+ <div class="entity-info">
|
|
|
|
|
+ <div class="entity-name">18%</div>
|
|
|
|
|
+ <div class="entity-meta">增长率数据 • 1个来源</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="entity-actions">
|
|
|
|
|
+ <button class="entity-action-btn" onclick="locateEntity('18%')">📍</button>
|
|
|
|
|
+ <button class="entity-action-btn" onclick="editEntity('18%')">✏️</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="entity-item" onclick="highlightEntity('50万平方米')">
|
|
|
|
|
+ <div class="entity-icon">📐</div>
|
|
|
|
|
+ <div class="entity-info">
|
|
|
|
|
+ <div class="entity-name">50万平方米</div>
|
|
|
|
|
+ <div class="entity-meta">面积数据 • 1个来源</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="entity-actions">
|
|
|
|
|
+ <button class="entity-action-btn" onclick="locateEntity('50万平方米')">📍</button>
|
|
|
|
|
+ <button class="entity-action-btn" onclick="editEntity('50万平方米')">✏️</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="entity-item" onclick="highlightEntity('12.5亿元')">
|
|
|
|
|
+ <div class="entity-icon">💵</div>
|
|
|
|
|
+ <div class="entity-info">
|
|
|
|
|
+ <div class="entity-name">12.5亿元</div>
|
|
|
|
|
+ <div class="entity-meta">投资额数据 • 1个来源</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="entity-actions">
|
|
|
|
|
+ <button class="entity-action-btn" onclick="locateEntity('12.5亿元')">📍</button>
|
|
|
|
|
+ <button class="entity-action-btn" onclick="editEntity('12.5亿元')">✏️</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="category-section">
|
|
|
|
|
+ <div class="category-header">
|
|
|
|
|
+ <span class="category-icon">📍</span>
|
|
|
|
|
+ <span class="category-title">地点 (1)</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="entity-items">
|
|
|
|
|
+ <div class="entity-item" onclick="highlightEntity('华南地区')">
|
|
|
|
|
+ <div class="entity-icon">📍</div>
|
|
|
|
|
+ <div class="entity-info">
|
|
|
|
|
+ <div class="entity-name">华南地区</div>
|
|
|
|
|
+ <div class="entity-meta">地理位置 • 1个来源</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="entity-actions">
|
|
|
|
|
+ <button class="entity-action-btn" onclick="locateEntity('华南地区')">📍</button>
|
|
|
|
|
+ <button class="entity-action-btn" onclick="editEntity('华南地区')">✏️</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- FAB资源监控 -->
|
|
|
|
|
+ <div style="position:fixed;bottom:24px;right:24px;z-index:1000;" id="fabContainer">
|
|
|
|
|
+ <div id="fabPanel" style="position:absolute;bottom:60px;right:0;width:260px;background:var(--white);border-radius:12px;box-shadow:0 8px 32px rgba(0,0,0,0.15);display:none;overflow:hidden;">
|
|
|
|
|
+ <div style="padding:14px 16px;background:linear-gradient(135deg, #52c41a 0%, #13c2c2 100%);color:white;font-weight:600;font-size:13px;">📊 资源监控</div>
|
|
|
|
|
+ <div style="padding:14px 16px;">
|
|
|
|
|
+ <div style="margin-bottom:12px;">
|
|
|
|
|
+ <div style="display:flex;justify-content:space-between;margin-bottom:4px;font-size:12px;"><span>Token 消耗</span><span style="font-weight:500;">15.6K / 20K</span></div>
|
|
|
|
|
+ <div style="height:6px;background:var(--bg);border-radius:3px;"><div style="height:100%;width:78%;background:var(--warning);border-radius:3px;"></div></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div style="margin-bottom:12px;">
|
|
|
|
|
+ <div style="display:flex;justify-content:space-between;margin-bottom:4px;font-size:12px;"><span>GPU 显存</span><span style="font-weight:500;">3.6G / 8G</span></div>
|
|
|
|
|
+ <div style="height:6px;background:var(--bg);border-radius:3px;"><div style="height:100%;width:45%;background:var(--success);border-radius:3px;"></div></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div style="display:flex;justify-content:space-between;padding:10px;background:var(--bg);border-radius:8px;">
|
|
|
|
|
+ <div style="text-align:center;"><div style="font-size:16px;font-weight:600;color:var(--primary);">¥3.12</div><div style="font-size:10px;color:var(--text3);">本次会话</div></div>
|
|
|
|
|
+ <div style="text-align:center;"><div style="font-size:16px;font-weight:600;color:var(--primary);">¥127.50</div><div style="font-size:10px;color:var(--text3);">本月累计</div></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <button id="fabBtn" style="width:50px;height:50px;border-radius:50%;background:linear-gradient(135deg, #52c41a 0%, #13c2c2 100%);border:none;cursor:grab;display:flex;align-items:center;justify-content:center;font-size:20px;color:white;box-shadow:0 6px 20px rgba(82,196,26,0.35);">📊</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <script>
|
|
|
|
|
+ (function(){
|
|
|
|
|
+ const container = document.getElementById('fabContainer');
|
|
|
|
|
+ const btn = document.getElementById('fabBtn');
|
|
|
|
|
+ if (!container || !btn) return;
|
|
|
|
|
+
|
|
|
|
|
+ let dragging = false;
|
|
|
|
|
+ let moved = false;
|
|
|
|
|
+ let startX = 0, startY = 0, origLeft = 0, origTop = 0;
|
|
|
|
|
+
|
|
|
|
|
+ // 默认位置:固定在右下角(24px 间距)
|
|
|
|
|
+ container.style.right = '24px';
|
|
|
|
|
+ container.style.bottom = '24px';
|
|
|
|
|
+ container.style.left = '';
|
|
|
|
|
+ container.style.top = '';
|
|
|
|
|
+
|
|
|
|
|
+ function start(e) {
|
|
|
|
|
+ const evt = e.touches ? e.touches[0] : e;
|
|
|
|
|
+ dragging = true;
|
|
|
|
|
+ moved = false;
|
|
|
|
|
+ startX = evt.clientX;
|
|
|
|
|
+ startY = evt.clientY;
|
|
|
|
|
+ const rect = container.getBoundingClientRect();
|
|
|
|
|
+ origLeft = rect.left;
|
|
|
|
|
+ origTop = rect.top;
|
|
|
|
|
+ document.addEventListener('mousemove', onMove);
|
|
|
|
|
+ document.addEventListener('mouseup', end);
|
|
|
|
|
+ document.addEventListener('touchmove', onMove, { passive: false });
|
|
|
|
|
+ document.addEventListener('touchend', end);
|
|
|
|
|
+ container.style.transition = 'none';
|
|
|
|
|
+ btn.style.cursor = 'grabbing';
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function onMove(e) {
|
|
|
|
|
+ if (!dragging) return;
|
|
|
|
|
+ const evt = e.touches ? e.touches[0] : e;
|
|
|
|
|
+ const dx = evt.clientX - startX;
|
|
|
|
|
+ const dy = evt.clientY - startY;
|
|
|
|
|
+ if (Math.abs(dx) > 4 || Math.abs(dy) > 4) moved = true;
|
|
|
|
|
+ const vw = window.innerWidth, vh = window.innerHeight;
|
|
|
|
|
+ const rect = container.getBoundingClientRect();
|
|
|
|
|
+ const w = rect.width, h = rect.height;
|
|
|
|
|
+ let newLeft = origLeft + dx;
|
|
|
|
|
+ let newTop = origTop + dy;
|
|
|
|
|
+ newLeft = Math.max(8, Math.min(vw - w - 8, newLeft));
|
|
|
|
|
+ newTop = Math.max(8, Math.min(vh - h - 8, newTop));
|
|
|
|
|
+ container.style.left = newLeft + 'px';
|
|
|
|
|
+ container.style.top = newTop + 'px';
|
|
|
|
|
+ container.style.right = '';
|
|
|
|
|
+ container.style.bottom = '';
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function end() {
|
|
|
|
|
+ if (!dragging) return;
|
|
|
|
|
+ dragging = false;
|
|
|
|
|
+ document.removeEventListener('mousemove', onMove);
|
|
|
|
|
+ document.removeEventListener('mouseup', end);
|
|
|
|
|
+ document.removeEventListener('touchmove', onMove);
|
|
|
|
|
+ document.removeEventListener('touchend', end);
|
|
|
|
|
+ container.style.transition = '';
|
|
|
|
|
+ btn.style.cursor = 'grab';
|
|
|
|
|
+ // 不持久化位置:拖动仅在当前会话有效,刷新/重新登录后恢复到默认右下角
|
|
|
|
|
+ // if moved, prevent the immediate click from toggling the panel
|
|
|
|
|
+ if (moved) {
|
|
|
|
|
+ const preventer = function(ev) { ev.stopImmediatePropagation(); ev.preventDefault(); btn.removeEventListener('click', preventer, true); };
|
|
|
|
|
+ btn.addEventListener('click', preventer, true);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // replace inline onclick by controlled click handler
|
|
|
|
|
+ btn.addEventListener('click', function(e){
|
|
|
|
|
+ if (dragging || moved) { e.preventDefault(); e.stopPropagation(); return; }
|
|
|
|
|
+ try { toggleFab(); } catch (err) { console.warn('toggleFab not available', err); }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // init and ensure position stays in viewport on resize
|
|
|
|
|
+ window.addEventListener('resize', function(){
|
|
|
|
|
+ try {
|
|
|
|
|
+ const rect = container.getBoundingClientRect();
|
|
|
|
|
+ const vw = window.innerWidth, vh = window.innerHeight;
|
|
|
|
|
+ let left = rect.left, top = rect.top;
|
|
|
|
|
+ const w = rect.width, h = rect.height;
|
|
|
|
|
+ if (left > vw - 8) left = Math.max(8, vw - w - 8);
|
|
|
|
|
+ if (top > vh - 8) top = Math.max(8, vh - h - 8);
|
|
|
|
|
+ container.style.left = left + 'px';
|
|
|
|
|
+ container.style.top = top + 'px';
|
|
|
|
|
+ } catch (e) {}
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ btn.addEventListener('mousedown', start);
|
|
|
|
|
+ btn.addEventListener('touchstart', start, { passive: false });
|
|
|
|
|
+
|
|
|
|
|
+ loadPos();
|
|
|
|
|
+ })();
|
|
|
|
|
+ </script>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 通知面板 -->
|
|
|
|
|
+ <div class="notif-panel" id="notifPanel">
|
|
|
|
|
+ <div style="padding:18px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center;">
|
|
|
|
|
+ <span style="font-size:15px;font-weight:600;">消息通知</span>
|
|
|
|
|
+ <span style="font-size:12px;color:var(--primary);cursor:pointer;" onclick="showToast('已全部标为已读', 'success')">全部已读</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div style="flex:1;overflow-y:auto;">
|
|
|
|
|
+ <div style="padding:14px 18px;border-bottom:1px solid var(--border);background:var(--primary-light);cursor:pointer;" onclick="void(0)">
|
|
|
|
|
+ <div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;">
|
|
|
|
|
+ <span style="width:22px;height:22px;background:#f6ffed;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:11px;">✅</span>
|
|
|
|
|
+ <span style="flex:1;font-weight:500;font-size:13px;">文档解析完成</span>
|
|
|
|
|
+ <span style="font-size:10px;color:var(--text3);">刚刚</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div style="font-size:12px;color:var(--text2);">《市场调研数据.pdf》已解析完成,提取到35个实体</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div style="padding:14px 18px;border-bottom:1px solid var(--border);cursor:pointer;">
|
|
|
|
|
+ <div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;">
|
|
|
|
|
+ <span style="width:22px;height:22px;background:#fffbe6;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:11px;">💬</span>
|
|
|
|
|
+ <span style="flex:1;font-weight:500;font-size:13px;">李四评论了您的报告</span>
|
|
|
|
|
+ <span style="font-size:10px;color:var(--text3);">2小时前</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div style="font-size:12px;color:var(--text2);">建议补充竞争格局分析...</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Toast容器 -->
|
|
|
|
|
+ <div class="toast-container" id="toastContainer"></div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 遮罩 -->
|
|
|
|
|
+ <div class="overlay" id="overlay" onclick="closeAll()"></div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 要素关系图谱遮罩 -->
|
|
|
|
|
+ <div class="kg-overlay" id="kgOverlay" onclick="closeKnowledgeGraph()"></div>
|
|
|
|
|
+
|
|
|
|
|
+ <script>
|
|
|
|
|
+ // === 导航 ===
|
|
|
|
|
+ document.addEventListener('DOMContentLoaded', function(){
|
|
|
|
|
+ try {
|
|
|
|
|
+ const ta = document.getElementById('aiTextarea');
|
|
|
|
|
+ const send = document.getElementById('aiSendBtn');
|
|
|
|
|
+ if (ta && send) {
|
|
|
|
|
+ const update = () => {
|
|
|
|
|
+ if (ta.value.trim().length > 0) send.classList.add('active');
|
|
|
|
|
+ else send.classList.remove('active');
|
|
|
|
|
+ };
|
|
|
|
|
+ ta.addEventListener('input', update);
|
|
|
|
|
+ update();
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) { console.warn('ai input init err', e); }
|
|
|
|
|
+ });
|
|
|
|
|
+ function navTo(page) {
|
|
|
|
|
+ document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
|
|
|
|
|
+ document.querySelectorAll('.menu-item').forEach(m => m.classList.remove('active'));
|
|
|
|
|
+ document.getElementById('page-' + page)?.classList.add('active');
|
|
|
|
|
+ document.querySelector('[data-page="' + page + '"]')?.classList.add('active');
|
|
|
|
|
+ document.getElementById('page-editor').classList.remove('active');
|
|
|
|
|
+ document.getElementById('sidebar').classList.remove('hidden');
|
|
|
|
|
+ document.getElementById('mainContent').style.display = '';
|
|
|
|
|
+ closeAll();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function goHome() { navTo('home'); }
|
|
|
|
|
+
|
|
|
|
|
+ // === 编辑器 ===
|
|
|
|
|
+ function openEditor() {
|
|
|
|
|
+ document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
|
|
|
|
|
+ document.getElementById('page-editor').classList.add('active');
|
|
|
|
|
+ document.getElementById('sidebar').classList.add('hidden');
|
|
|
|
|
+ document.getElementById('mainContent').style.display = 'none';
|
|
|
|
|
+ closeAll();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function closeEditor() { navTo('home'); }
|
|
|
|
|
+
|
|
|
|
|
+ // === 通知 ===
|
|
|
|
|
+ function toggleNotif() {
|
|
|
|
|
+ const panel = document.getElementById('notifPanel');
|
|
|
|
|
+ const overlay = document.getElementById('overlay');
|
|
|
|
|
+ const isOpen = panel.style.transform === 'translateX(0%)';
|
|
|
|
|
+ panel.style.transform = isOpen ? 'translateX(100%)' : 'translateX(0%)';
|
|
|
|
|
+ overlay.style.display = isOpen ? 'none' : 'block';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // === FAB ===
|
|
|
|
|
+ function toggleFab() {
|
|
|
|
|
+ const panel = document.getElementById('fabPanel');
|
|
|
|
|
+ panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // === 思考模式 ===
|
|
|
|
|
+ function setMode(el) {
|
|
|
|
|
+ document.querySelectorAll('.mode-btn').forEach(b => b.classList.remove('active'));
|
|
|
|
|
+ el.classList.add('active');
|
|
|
|
|
+ showToast('已切换到 ' + el.textContent.trim(), 'success');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // === 首页AI输入 ===
|
|
|
|
|
+ function toggleSendBtn() {
|
|
|
|
|
+ const input = document.getElementById('homeAiInput');
|
|
|
|
|
+ const btn = document.getElementById('homeSendBtn');
|
|
|
|
|
+ btn.classList.toggle('show', input.value.trim().length > 0);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function handleHomeAi() {
|
|
|
|
|
+ const input = document.getElementById('homeAiInput');
|
|
|
|
|
+ if (input.value.trim()) {
|
|
|
|
|
+ showToast('AI正在处理...', 'info');
|
|
|
|
|
+ setTimeout(() => openEditor(), 1000);
|
|
|
|
|
+ input.value = '';
|
|
|
|
|
+ document.getElementById('homeSendBtn').classList.remove('show');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // === 要素管理 ===
|
|
|
|
|
+ const tagData = {
|
|
|
|
|
+ smartpark: { icon: '🏢', type: 'entity', name: '智慧园区', typeText: '核心实体', source: '项目可行性研究报告.docx', relations: ['产业升级', '城市现代化', '智能化管理'] },
|
|
|
|
|
+ upgrade: { icon: '📈', type: 'concept', name: '产业升级', typeText: '概念', source: '项目可行性研究报告.docx', relations: ['智慧园区', '城市现代化'] },
|
|
|
|
|
+ modern: { icon: '🌆', type: 'concept', name: '城市现代化', typeText: '概念', source: '项目可行性研究报告.docx', relations: ['智慧园区', '产业升级'] },
|
|
|
|
|
+ ai: { icon: '🤖', type: 'concept', name: '智能化管理', typeText: '技术', source: '技术方案说明.pdf', relations: ['智慧园区', '低碳绿色'] },
|
|
|
|
|
+ green: { icon: '🌱', type: 'concept', name: '低碳绿色', typeText: '概念', source: '项目可行性研究报告.docx', relations: ['智慧园区'] },
|
|
|
|
|
+ location: { icon: '📍', type: 'location', name: '华南地区', typeText: '地点', source: '项目可行性研究报告.docx', relations: ['智慧园区', '50万m²'] },
|
|
|
|
|
+ data1: { icon: '💰', type: 'data', name: '1,789亿元', typeText: '市场规模数据', source: '市场调研数据.pdf', relations: ['智慧园区', '18%'] },
|
|
|
|
|
+ data2: { icon: '📊', type: 'data', name: '18%', typeText: '增长率数据', source: '市场调研数据.pdf', relations: ['1,789亿元'] },
|
|
|
|
|
+ data3: { icon: '📐', type: 'data', name: '50万平方米', typeText: '面积数据', source: '项目可行性研究报告.docx', relations: ['华南地区'] },
|
|
|
|
|
+ data4: { icon: '💵', type: 'data', name: '12.5亿元', typeText: '投资额', source: '财务预测表.xlsx', relations: ['智慧园区'] },
|
|
|
|
|
+ chart: { icon: '📊', type: 'asset', name: '趋势图', typeText: '图表资产', source: '资产库', relations: ['柱状图'] },
|
|
|
|
|
+ template: { icon: '📝', type: 'asset', name: '结论模板', typeText: '文本模板', source: '资产库', relations: ['结论'] }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ let currentTagName = '';
|
|
|
|
|
+
|
|
|
|
|
+ function showTagPopover(event, tagId) {
|
|
|
|
|
+ event.stopPropagation();
|
|
|
|
|
+ const popover = document.getElementById('elementPopover');
|
|
|
|
|
+ const data = tagData[tagId];
|
|
|
|
|
+ if (!data) return;
|
|
|
|
|
+
|
|
|
|
|
+ currentTagName = data.name;
|
|
|
|
|
+ document.getElementById('popoverIcon').className = 'popover-icon ' + data.type;
|
|
|
|
|
+ document.getElementById('popoverIcon').textContent = data.icon;
|
|
|
|
|
+ document.getElementById('popoverTitle').textContent = data.name;
|
|
|
|
|
+ document.getElementById('popoverType').textContent = data.typeText;
|
|
|
|
|
+ document.getElementById('popoverSource').textContent = data.source;
|
|
|
|
|
+
|
|
|
|
|
+ const relationsEl = document.getElementById('popoverRelations');
|
|
|
|
|
+ relationsEl.innerHTML = data.relations.map(r =>
|
|
|
|
|
+ `<span class="popover-relation" onclick="showToast('跳转到: ${r}', 'info')">${r}</span>`
|
|
|
|
|
+ ).join('');
|
|
|
|
|
+
|
|
|
|
|
+ const rect = event.currentTarget.getBoundingClientRect();
|
|
|
|
|
+ popover.style.top = Math.min(rect.bottom + 8, window.innerHeight - 300) + 'px';
|
|
|
|
|
+ popover.style.left = Math.min(rect.left, window.innerWidth - 300) + 'px';
|
|
|
|
|
+ popover.classList.add('show');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function hideTagPopover() {
|
|
|
|
|
+ document.getElementById('elementPopover').classList.remove('show');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // === Sync tags from editor content to right-panel lists ===
|
|
|
|
|
+ function sanitizeId(name) {
|
|
|
|
|
+ return 'tag_' + name.replace(/\s+/g, '_').replace(/[^\w\-]/g, '').toLowerCase();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function rgbToHex(rgb) {
|
|
|
|
|
+ if (!rgb) return '';
|
|
|
|
|
+ const m = rgb.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/i);
|
|
|
|
|
+ if (!m) return '';
|
|
|
|
|
+ const r = parseInt(m[1]), g = parseInt(m[2]), b = parseInt(m[3]);
|
|
|
|
|
+ return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function isColorRed(rgb) {
|
|
|
|
|
+ if (!rgb) return false;
|
|
|
|
|
+ const m = rgb.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/i);
|
|
|
|
|
+ if (!m) return false;
|
|
|
|
|
+ const r = parseInt(m[1]), g = parseInt(m[2]), b = parseInt(m[3]);
|
|
|
|
|
+ return (r > 200 && g < 130 && b < 130);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function syncTagsFromContent() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ console.debug('[debug] syncTagsFromContent start');
|
|
|
|
|
+ const nodes = document.querySelectorAll('.editor-content .entity-highlight');
|
|
|
|
|
+ const dynContainer = document.getElementById('dynamicTags');
|
|
|
|
|
+ const statContainer = document.getElementById('staticTags');
|
|
|
|
|
+ if (!dynContainer || !statContainer) return;
|
|
|
|
|
+
|
|
|
|
|
+ // clear containers
|
|
|
|
|
+ dynContainer.innerHTML = '';
|
|
|
|
|
+ statContainer.innerHTML = '';
|
|
|
|
|
+
|
|
|
|
|
+ nodes.forEach(node => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // extract visible text only (exclude badge/span text)
|
|
|
|
|
+ let rawText = '';
|
|
|
|
|
+ node.childNodes.forEach(n => {
|
|
|
|
|
+ if (n.nodeType === Node.TEXT_NODE) rawText += n.textContent;
|
|
|
|
|
+ else if (n.nodeType === Node.ELEMENT_NODE && n.classList && n.classList.contains('tag-name')) rawText += n.textContent;
|
|
|
|
|
+ });
|
|
|
|
|
+ rawText = (rawText || node.textContent || '').trim();
|
|
|
|
|
+ if (!rawText) {
|
|
|
|
|
+ console.debug('[debug] syncTagsFromContent: skipping empty node', node);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const computed = window.getComputedStyle(node);
|
|
|
|
|
+ let bg = computed.backgroundColor || '';
|
|
|
|
|
+ const border = computed.borderColor || '';
|
|
|
|
|
+ const color = computed.color || '';
|
|
|
|
|
+
|
|
|
|
|
+ console.debug('[debug] tag', rawText, { background: bg, border: border, color: color });
|
|
|
|
|
+ // prefer background if not transparent
|
|
|
|
|
+ if (!bg || bg === 'transparent' || bg.indexOf('0, 0, 0, 0') !== -1) {
|
|
|
|
|
+ bg = border || color || '';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Improved dynamic detection:
|
|
|
|
|
+ // 1) explicit data-source indicating AI/generated
|
|
|
|
|
+ // 2) presence of a .source-badge.ai inside the node
|
|
|
|
|
+ // 3) existing color-based heuristic (fallback)
|
|
|
|
|
+ let isDynamic = false;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const ds = (node.dataset && node.dataset.source) ? String(node.dataset.source) : '';
|
|
|
|
|
+ const category = (node.dataset && node.dataset.category) ? String(node.dataset.category) : '';
|
|
|
|
|
+ if (ds && /ai|生成|AI/i.test(ds)) {
|
|
|
|
|
+ isDynamic = true;
|
|
|
|
|
+ } else if (category && /市场规模数据|data|AI/i.test(category)) {
|
|
|
|
|
+ isDynamic = true;
|
|
|
|
|
+ } else if (node.querySelector && node.querySelector('.source-badge.ai')) {
|
|
|
|
|
+ isDynamic = true;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ isDynamic = isColorRed(bg) || isColorRed(border) || /#ff4d4f/i.test((bg || '') + (border || ''));
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (sigErr) {
|
|
|
|
|
+ console.warn('dynamic detection fallback err', sigErr);
|
|
|
|
|
+ isDynamic = isColorRed(bg) || isColorRed(border) || /#ff4d4f/i.test((bg || '') + (border || ''));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ console.debug('[debug] determined isDynamic=', isDynamic, 'for', rawText);
|
|
|
|
|
+ const id = sanitizeId(rawText);
|
|
|
|
|
+ if (!tagData[id]) {
|
|
|
|
|
+ tagData[id] = { icon: '🏷️', type: 'entity', name: rawText, typeText: '来源: 文本', source: '文档', relations: [] };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const span = document.createElement('span');
|
|
|
|
|
+ span.className = 'element-tag ' + (isDynamic ? 'dynamic' : 'static') + ' mapped';
|
|
|
|
|
+ span.setAttribute('draggable', 'true');
|
|
|
|
|
+ span.onclick = function (e) { e.stopPropagation(); showTagPopover(e, id); };
|
|
|
|
|
+ span.ondragstart = (e) => handleTagDragStart(e, rawText);
|
|
|
|
|
+ span.ondragend = (e) => handleTagDragEnd(e);
|
|
|
|
|
+
|
|
|
|
|
+ // apply color styling to match source highlight (if available)
|
|
|
|
|
+ const fill = rgbToHex(bg);
|
|
|
|
|
+ if (fill) {
|
|
|
|
|
+ span.style.background = fill;
|
|
|
|
|
+ // choose light or dark text based on luminance
|
|
|
|
|
+ const hex = fill.replace('#','');
|
|
|
|
|
+ const r = parseInt(hex.substring(0,2),16), g = parseInt(hex.substring(2,4),16), b = parseInt(hex.substring(4,6),16);
|
|
|
|
|
+ const luminance = (0.299*r + 0.587*g + 0.114*b);
|
|
|
|
|
+ span.style.color = luminance > 160 ? '#000' : '#fff';
|
|
|
|
|
+ span.style.border = '1px solid rgba(0,0,0,0.06)';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ span.innerHTML = `<span class="tag-icon">${tagData[id].icon || '🏷️'}</span><span class="tag-name">${rawText}</span>`;
|
|
|
|
|
+
|
|
|
|
|
+ if (isDynamic) {
|
|
|
|
|
+ dynContainer.appendChild(span);
|
|
|
|
|
+ }
|
|
|
|
|
+ else statContainer.appendChild(span);
|
|
|
|
|
+ } catch (e) { console.warn('syncTagsFromContent node err', e); }
|
|
|
|
|
+ });
|
|
|
|
|
+ // post-process: relocate any static tags that are actually red in the editor
|
|
|
|
|
+ try { relocateRedStaticTags(dynContainer, statContainer); } catch (e) { console.warn('relocateRedStaticTags err', e); }
|
|
|
|
|
+ console.debug('[debug] syncTagsFromContent done', { dynamicCount: dynContainer.children.length, staticCount: statContainer.children.length });
|
|
|
|
|
+ } catch (e) { console.warn('syncTagsFromContent err', e); }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Move any tags currently in static container that are red in the editor into dynamic container
|
|
|
|
|
+ function relocateRedStaticTags(dynContainer, statContainer) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const staticChildren = Array.from(statContainer.children);
|
|
|
|
|
+ if (staticChildren.length === 0) return;
|
|
|
|
|
+
|
|
|
|
|
+ const editorNodes = Array.from(document.querySelectorAll('.editor-content .entity-highlight'));
|
|
|
|
|
+
|
|
|
|
|
+ staticChildren.forEach(child => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const nameEl = child.querySelector('.tag-name');
|
|
|
|
|
+ const tagName = nameEl ? nameEl.textContent.trim() : (child.textContent || '').trim();
|
|
|
|
|
+ if (!tagName) return;
|
|
|
|
|
+
|
|
|
|
|
+ // find corresponding editor node by exact text match (prefer full match)
|
|
|
|
|
+ const sourceNode = editorNodes.find(n => {
|
|
|
|
|
+ const text = (n.textContent || '').trim();
|
|
|
|
|
+ return text === tagName || text.includes(tagName);
|
|
|
|
|
+ });
|
|
|
|
|
+ if (!sourceNode) return;
|
|
|
|
|
+
|
|
|
|
|
+ const cs = window.getComputedStyle(sourceNode);
|
|
|
|
|
+ const bg = cs.backgroundColor || cs.borderColor || cs.color || '';
|
|
|
|
|
+ if (isColorRed(bg)) {
|
|
|
|
|
+ // move element
|
|
|
|
|
+ child.classList.remove('static');
|
|
|
|
|
+ child.classList.add('dynamic');
|
|
|
|
|
+ dynContainer.appendChild(child);
|
|
|
|
|
+ console.debug('[debug] relocated tag to dynamic:', tagName);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) { console.warn('relocate child err', e); }
|
|
|
|
|
+ });
|
|
|
|
|
+ } catch (e) { console.warn('relocateRedStaticTags overall err', e); }
|
|
|
|
|
+ }
|
|
|
|
|
+ // observe editor content changes and sync tags (debounced)
|
|
|
|
|
+ (function() {
|
|
|
|
|
+ let t = null;
|
|
|
|
|
+ document.addEventListener('selectionchange', () => { /* no-op keep event loop active */ });
|
|
|
|
|
+ document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
|
+ document.querySelectorAll('.editor-content').forEach(editor => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const mo = new MutationObserver(() => {
|
|
|
|
|
+ clearTimeout(t);
|
|
|
|
|
+ t = setTimeout(() => {
|
|
|
|
|
+ try { syncTagsFromContent(); } catch(e) { console.warn('debounced sync err', e); }
|
|
|
|
|
+ }, 150);
|
|
|
|
|
+ });
|
|
|
|
|
+ mo.observe(editor, { characterData: true, childList: true, subtree: true });
|
|
|
|
|
+ } catch (e) { console.warn('attach mutation observer err', e); }
|
|
|
|
|
+ });
|
|
|
|
|
+ // initial sync after load
|
|
|
|
|
+ try { syncTagsFromContent(); } catch(e) { console.warn('initial syncTagsFromContent err', e); }
|
|
|
|
|
+ // ensure tabs trigger a sync when clicked
|
|
|
|
|
+ try {
|
|
|
|
|
+ const td = document.getElementById('tabDynamic');
|
|
|
|
|
+ const ts = document.getElementById('tabStatic');
|
|
|
|
|
+ if (td) { td.addEventListener('click', () => { try { syncTagsFromContent(); } catch(e){} }); }
|
|
|
|
|
+ if (ts) { ts.addEventListener('click', () => { try { syncTagsFromContent(); } catch(e){} }); }
|
|
|
|
|
+ } catch (e) { console.warn('attach tab click sync err', e); }
|
|
|
|
|
+ });
|
|
|
|
|
+ })();
|
|
|
|
|
+ // 切换动态/静态要素 Tab
|
|
|
|
|
+ function switchElementTab(tab) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const tabDyn = document.getElementById('tabDynamic');
|
|
|
|
|
+ const tabStat = document.getElementById('tabStatic');
|
|
|
|
|
+ const dyn = document.getElementById('dynamicTags');
|
|
|
|
|
+ const stat = document.getElementById('staticTags');
|
|
|
|
|
+
|
|
|
|
|
+ if (tab === 'dynamic') {
|
|
|
|
|
+ if (tabDyn) tabDyn.classList.add('active');
|
|
|
|
|
+ if (tabStat) tabStat.classList.remove('active');
|
|
|
|
|
+ if (dyn) dyn.style.display = '';
|
|
|
|
|
+ if (stat) stat.style.display = 'none';
|
|
|
|
|
+ try { syncTagsFromContent(); } catch(e){ console.warn('sync on tab switch err', e); }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ if (tabDyn) tabDyn.classList.remove('active');
|
|
|
|
|
+ if (tabStat) tabStat.classList.add('active');
|
|
|
|
|
+ if (dyn) dyn.style.display = 'none';
|
|
|
|
|
+ if (stat) stat.style.display = '';
|
|
|
|
|
+ try { syncTagsFromContent(); } catch(e){ console.warn('sync on tab switch err', e); }
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) { console.warn('switchElementTab err', e); }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function insertTagToEditor() {
|
|
|
|
|
+ if (currentTagName) {
|
|
|
|
|
+ showToast('已插入要素: ' + currentTagName, 'success');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ document.addEventListener('click', function(e) {
|
|
|
|
|
+ if (!e.target.closest('.element-popover') && !e.target.closest('.element-tag')) {
|
|
|
|
|
+ hideTagPopover();
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // === 标签拖拽功能 ===
|
|
|
|
|
+ function handleTagDragStart(event, tagName) {
|
|
|
|
|
+ currentTagName = tagName;
|
|
|
|
|
+ event.currentTarget.classList.add('dragging');
|
|
|
|
|
+ event.dataTransfer.setData('text/plain', tagName);
|
|
|
|
|
+ event.dataTransfer.effectAllowed = 'copy';
|
|
|
|
|
+
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ document.querySelectorAll('.editor-content').forEach(el => {
|
|
|
|
|
+ el.classList.add('drag-over');
|
|
|
|
|
+ });
|
|
|
|
|
+ }, 0);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function handleTagDragEnd(event) {
|
|
|
|
|
+ event.currentTarget.classList.remove('dragging');
|
|
|
|
|
+ document.querySelectorAll('.editor-content').forEach(el => {
|
|
|
|
|
+ el.classList.remove('drag-over');
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 编辑区放置事件
|
|
|
|
|
+ document.addEventListener('DOMContentLoaded', function() {
|
|
|
|
|
+ const editorContents = document.querySelectorAll('.editor-content');
|
|
|
|
|
+ editorContents.forEach(editor => {
|
|
|
|
|
+ editor.addEventListener('dragover', function(e) {
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+ e.dataTransfer.dropEffect = 'copy';
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ editor.addEventListener('drop', function(e) {
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+ const tagName = e.dataTransfer.getData('text/plain');
|
|
|
|
|
+ if (tagName) {
|
|
|
|
|
+ showToast('✓ 已插入要素: ' + tagName, 'success');
|
|
|
|
|
+ }
|
|
|
|
|
+ this.classList.remove('drag-over');
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ editor.addEventListener('dragleave', function(e) {
|
|
|
|
|
+ if (!this.contains(e.relatedTarget)) {
|
|
|
|
|
+ this.classList.remove('drag-over');
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // === 右键菜单 ===
|
|
|
|
|
+ let selectedText = '';
|
|
|
|
|
+ let selectedRange = null;
|
|
|
|
|
+
|
|
|
|
|
+ function showContextMenu(event) {
|
|
|
|
|
+ const selection = window.getSelection();
|
|
|
|
|
+ selectedText = selection.toString().trim();
|
|
|
|
|
+ selectedRange = selection.rangeCount > 0 ? selection.getRangeAt(0) : null;
|
|
|
|
|
+
|
|
|
|
|
+ if (selectedText.length > 0) {
|
|
|
|
|
+ event.preventDefault();
|
|
|
|
|
+ const menu = document.getElementById('contextMenu');
|
|
|
|
|
+ menu.style.top = event.clientY + 'px';
|
|
|
|
|
+ menu.style.left = event.clientX + 'px';
|
|
|
|
|
+ menu.classList.add('show');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function hideContextMenu() {
|
|
|
|
|
+ document.getElementById('contextMenu').classList.remove('show');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function execContextAction(action) {
|
|
|
|
|
+ hideContextMenu();
|
|
|
|
|
+
|
|
|
|
|
+ switch(action) {
|
|
|
|
|
+ case 'copy':
|
|
|
|
|
+ document.execCommand('copy');
|
|
|
|
|
+ showToast('已复制到剪贴板', 'success');
|
|
|
|
|
+ break;
|
|
|
|
|
+ case 'cut':
|
|
|
|
|
+ document.execCommand('cut');
|
|
|
|
|
+ showToast('已剪切', 'success');
|
|
|
|
|
+ break;
|
|
|
|
|
+ case 'paste':
|
|
|
|
|
+ document.execCommand('paste');
|
|
|
|
|
+ break;
|
|
|
|
|
+ case 'polish':
|
|
|
|
|
+ showAiPolishConfirm();
|
|
|
|
|
+ break;
|
|
|
|
|
+ case 'spell':
|
|
|
|
|
+ showSpellCheckConfirm();
|
|
|
|
|
+ break;
|
|
|
|
|
+ case 'mark':
|
|
|
|
|
+ markAsEntity();
|
|
|
|
|
+ break;
|
|
|
|
|
+ case 'quote':
|
|
|
|
|
+ const aiMessages = document.getElementById('aiMessages');
|
|
|
|
|
+ const quotedText = selectedText.length > 50 ? selectedText.substring(0, 50) + '...' : selectedText;
|
|
|
|
|
+ aiMessages.innerHTML += `
|
|
|
|
|
+ <div class="msg user">
|
|
|
|
|
+ <div class="msg-avatar">张</div>
|
|
|
|
|
+ <div class="msg-bubble">
|
|
|
|
|
+ <div style="padding:8px;background:rgba(255,255,255,0.5);border-radius:6px;margin-bottom:8px;font-size:12px;border-left:3px solid rgba(255,255,255,0.8);">
|
|
|
|
|
+ 📝 引用: "${quotedText}"
|
|
|
|
|
+ </div>
|
|
|
|
|
+ 请基于这段内容帮我分析
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ `;
|
|
|
|
|
+ aiMessages.scrollTop = aiMessages.scrollHeight;
|
|
|
|
|
+ showToast('已引用到AI助手', 'success');
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // === AI润色确认 ===
|
|
|
|
|
+ function showAiPolishConfirm() {
|
|
|
|
|
+ const polishedText = `经过AI润色优化:${selectedText.replace(/的/g, '的').replace(/了/g, '了').replace(/和/g, '与')}`;
|
|
|
|
|
+ showConfirmDialog(
|
|
|
|
|
+ 'AI润色确认',
|
|
|
|
|
+ `原文: "${selectedText}"<br><br>润色后: "${polishedText}"`,
|
|
|
|
|
+ () => applyTextReplacement(polishedText),
|
|
|
|
|
+ () => showToast('已取消润色', 'info')
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // === 拼写检查确认 ===
|
|
|
|
|
+ function showSpellCheckConfirm() {
|
|
|
|
|
+ // 模拟拼写检查结果
|
|
|
|
|
+ const correctedText = selectedText.replace(/园区/g, '园区').replace(/智慧/g, '智慧');
|
|
|
|
|
+ const hasErrors = correctedText !== selectedText;
|
|
|
|
|
+
|
|
|
|
|
+ if (hasErrors) {
|
|
|
|
|
+ showConfirmDialog(
|
|
|
|
|
+ '拼写检查结果',
|
|
|
|
|
+ `发现拼写问题:<br>原文: "${selectedText}"<br>更正后: "${correctedText}"`,
|
|
|
|
|
+ () => applyTextReplacement(correctedText),
|
|
|
|
|
+ () => showToast('已忽略拼写建议', 'info')
|
|
|
|
|
+ );
|
|
|
|
|
+ } else {
|
|
|
|
|
+ showToast('✓ 拼写检查完成,未发现错误', 'success');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // === 标记为要素 ===
|
|
|
|
|
+ function markAsEntity() {
|
|
|
|
|
+ if (!selectedRange) return;
|
|
|
|
|
+
|
|
|
|
|
+ const span = document.createElement('span');
|
|
|
|
|
+ span.className = 'entity-highlight entity'; // 默认标记为entity类型
|
|
|
|
|
+ span.contentEditable = 'false';
|
|
|
|
|
+ // 标记来源为人工
|
|
|
|
|
+ span.dataset.source = '人工标记';
|
|
|
|
|
+ span.dataset.category = 'entity';
|
|
|
|
|
+ span.onclick = function(e) { e.stopPropagation(); showDataRelationModal(selectedText, '自定义要素', 'entity', span.dataset.source); };
|
|
|
|
|
+ span.textContent = selectedText;
|
|
|
|
|
+
|
|
|
|
|
+ // 添加来源徽章
|
|
|
|
|
+ const badge = document.createElement('span');
|
|
|
|
|
+ badge.className = 'source-badge manual';
|
|
|
|
|
+ badge.title = '来源:人工标记(点击查看详情)';
|
|
|
|
|
+ badge.textContent = '人工';
|
|
|
|
|
+ badge.onclick = function(e) { e.stopPropagation(); showDataRelationModal(selectedText, '自定义要素', 'entity', span.dataset.source); };
|
|
|
|
|
+ span.appendChild(badge);
|
|
|
|
|
+
|
|
|
|
|
+ selectedRange.deleteContents();
|
|
|
|
|
+ selectedRange.insertNode(span);
|
|
|
|
|
+
|
|
|
|
|
+ showToast(`✓ 已将"${selectedText}"标记为要素`, 'success');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // === 初始化:为已有实体标注添加来源徽章和类型样式(从 onclick 参数或 data-source 推断)===
|
|
|
|
|
+ function initializeSourceBadges() {
|
|
|
|
|
+ const nodes = document.querySelectorAll('.entity-highlight');
|
|
|
|
|
+ nodes.forEach(node => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 如果已经有 badge,就跳过
|
|
|
|
|
+ if (node.querySelector && node.querySelector('.source-badge')) return;
|
|
|
|
|
+
|
|
|
|
|
+ // 优先读取 data-source 和 data-category
|
|
|
|
|
+ let source = node.dataset && node.dataset.source;
|
|
|
|
|
+ let category = node.dataset && node.dataset.category;
|
|
|
|
|
+
|
|
|
|
|
+ // 如果没有 data-source或data-category,从 onclick 属性解析参数
|
|
|
|
|
+ if ((!source || !category) && node.getAttribute('onclick')) {
|
|
|
|
|
+ const onclick = node.getAttribute('onclick');
|
|
|
|
|
+ // 解析 showDataRelationModal('name','type','category','sourceFile')
|
|
|
|
|
+ const m = onclick.match(/showDataRelationModal\(([^)]+)\)/);
|
|
|
|
|
+ if (m && m[1]) {
|
|
|
|
|
+ const parts = m[1].split(',').map(s => s.trim());
|
|
|
|
|
+ if (parts.length >= 4) {
|
|
|
|
|
+ if (!category) category = parts[2].replace(/^['"]|['"]$/g, '');
|
|
|
|
|
+ if (!source) source = parts[3].replace(/^['"]|['"]$/g, '');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 为元素添加类型类名(如果还没有的话)
|
|
|
|
|
+ if (category && !node.classList.contains(category)) {
|
|
|
|
|
+ node.classList.add(category);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 标记类和徽章文字
|
|
|
|
|
+ let cls = 'file', txt = '文件';
|
|
|
|
|
+ if (!source) { cls = 'manual'; txt = '人工'; source = '未知'; }
|
|
|
|
|
+ else if (/ai/i.test(source) || /生成/.test(source) || source === 'AI') { cls = 'ai'; txt = 'AI'; }
|
|
|
|
|
+ else if (/人工|人工标记/.test(source)) { cls = 'manual'; txt = '人工'; }
|
|
|
|
|
+ else { cls = 'file'; txt = source.split('/').pop(); if (txt.length > 10) txt = txt.slice(0,10) + '…'; }
|
|
|
|
|
+
|
|
|
|
|
+ // 保存到dataset
|
|
|
|
|
+ node.dataset.source = source;
|
|
|
|
|
+ node.dataset.category = category || 'entity';
|
|
|
|
|
+
|
|
|
|
|
+ const badge = document.createElement('span');
|
|
|
|
|
+ badge.className = `source-badge ${cls}`;
|
|
|
|
|
+ badge.title = `来源:${source}`;
|
|
|
|
|
+ badge.textContent = txt;
|
|
|
|
|
+ badge.onclick = function(e) { e.stopPropagation(); showDataRelationModal(node.textContent, '自定义要素', category || 'entity', source); };
|
|
|
|
|
+ node.appendChild(badge);
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.error('initializeSourceBadges error', err);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // === 通用确认对话框 ===
|
|
|
|
|
+ function showConfirmDialog(title, content, onConfirm, onCancel) {
|
|
|
|
|
+ // 创建确认对话框
|
|
|
|
|
+ const dialog = document.createElement('div');
|
|
|
|
|
+ dialog.className = 'confirm-dialog';
|
|
|
|
|
+ dialog.innerHTML = `
|
|
|
|
|
+ <div class="confirm-content">
|
|
|
|
|
+ <div class="confirm-header">
|
|
|
|
|
+ <h3>${title}</h3>
|
|
|
|
|
+ <button class="confirm-close" onclick="closeConfirmDialog(this)">×</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="confirm-body">${content}</div>
|
|
|
|
|
+ <div class="confirm-footer">
|
|
|
|
|
+ <button class="btn" onclick="closeConfirmDialog(this); ${onCancel ? 'setTimeout(() => {' + onCancel.toString().replace(/^function\s*\(\)\s*\{/, '').replace(/}$/, '') + '}, 100)' : ''}">取消</button>
|
|
|
|
|
+ <button class="btn btn-primary" onclick="closeConfirmDialog(this); ${onConfirm ? 'setTimeout(() => {' + onConfirm.toString().replace(/^function\s*\(\)\s*\{/, '').replace(/}$/, '') + '}, 100)' : ''}">确认应用</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ `;
|
|
|
|
|
+ document.body.appendChild(dialog);
|
|
|
|
|
+ setTimeout(() => dialog.classList.add('show'), 10);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function closeConfirmDialog(btn) {
|
|
|
|
|
+ const dialog = btn.closest('.confirm-dialog');
|
|
|
|
|
+ dialog.classList.remove('show');
|
|
|
|
|
+ setTimeout(() => dialog.remove(), 300);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // === 文本替换功能 ===
|
|
|
|
|
+ function applyTextReplacement(newText) {
|
|
|
|
|
+ if (selectedRange) {
|
|
|
|
|
+ selectedRange.deleteContents();
|
|
|
|
|
+ selectedRange.insertNode(document.createTextNode(newText));
|
|
|
|
|
+ showToast('✓ 内容已更新', 'success');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 点击其他地方关闭右键菜单
|
|
|
|
|
+ document.addEventListener('click', function(e) {
|
|
|
|
|
+ if (!e.target.closest('.context-menu')) {
|
|
|
|
|
+ hideContextMenu();
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // === 实体标签编辑弹窗 ===
|
|
|
|
|
+ const entityEditData = {
|
|
|
|
|
+ smartpark: {
|
|
|
|
|
+ icon: '🏢', name: '智慧园区', type: 'entity', typeText: '核心实体',
|
|
|
|
|
+ source: '项目可行性研究报告.docx', originalValue: '智慧园区',
|
|
|
|
|
+ relations: ['产业升级', '城市现代化', '智能化管理', '1,789亿元']
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ let currentEditEntityId = null;
|
|
|
|
|
+
|
|
|
|
|
+ function showEntityEditModal(event, entityId) {
|
|
|
|
|
+ event.preventDefault();
|
|
|
|
|
+ event.stopPropagation();
|
|
|
|
|
+
|
|
|
|
|
+ const data = entityEditData[entityId];
|
|
|
|
|
+ if (!data) return;
|
|
|
|
|
+
|
|
|
|
|
+ currentEditEntityId = entityId;
|
|
|
|
|
+
|
|
|
|
|
+ document.getElementById('editEntityIcon').textContent = data.icon;
|
|
|
|
|
+ document.getElementById('editEntityName').textContent = data.name;
|
|
|
|
|
+ document.getElementById('editEntityType').textContent = data.typeText + ' · 来自 ' + data.source;
|
|
|
|
|
+ document.getElementById('editEntityValue').value = data.name;
|
|
|
|
|
+ document.getElementById('editEntityCategory').value = data.type;
|
|
|
|
|
+
|
|
|
|
|
+ const relationsEl = document.getElementById('editEntityRelations');
|
|
|
|
|
+ relationsEl.innerHTML = data.relations.map(r =>
|
|
|
|
|
+ `<span class="popover-relation" onclick="showToast('跳转到: ${r}', 'info')">${r}</span>`
|
|
|
|
|
+ ).join('') + `<span style="padding:3px 8px;background:var(--primary-light);border-radius:4px;font-size:11px;color:var(--primary);cursor:pointer;" onclick="showToast('添加关联', 'info')">+ 添加</span>`;
|
|
|
|
|
+
|
|
|
|
|
+ document.getElementById('entityEditModal').classList.add('show');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function closeEntityEditModal() {
|
|
|
|
|
+ document.getElementById('entityEditModal').classList.remove('show');
|
|
|
|
|
+ currentEditEntityId = null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function saveEntityEdit() {
|
|
|
|
|
+ const newValue = document.getElementById('editEntityValue').value;
|
|
|
|
|
+ const newCategory = document.getElementById('editEntityCategory').value;
|
|
|
|
|
+
|
|
|
|
|
+ if (currentEditEntityId && entityEditData[currentEditEntityId]) {
|
|
|
|
|
+ entityEditData[currentEditEntityId].name = newValue;
|
|
|
|
|
+ entityEditData[currentEditEntityId].type = newCategory;
|
|
|
|
|
+ showToast('✓ 标签已更新: ' + newValue, 'success');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ closeEntityEditModal();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // === AI对话 ===
|
|
|
|
|
+ function handleAiKey(e) {
|
|
|
|
|
+ if (e.key === 'Enter' && !e.shiftKey) {
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+ sendAiMsg();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function autoResizeTextarea(el) {
|
|
|
|
|
+ el.style.height = 'auto';
|
|
|
|
|
+ el.style.height = Math.min(el.scrollHeight, 80) + 'px';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function sendAiMsg() {
|
|
|
|
|
+ const textarea = document.getElementById('aiTextarea');
|
|
|
|
|
+ const msg = textarea.value.trim();
|
|
|
|
|
+ if (!msg) return;
|
|
|
|
|
+
|
|
|
|
|
+ const container = document.getElementById('aiMessages');
|
|
|
|
|
+ container.innerHTML += '<div class="msg user"><div class="msg-avatar">张</div><div class="msg-bubble">' + msg + '</div></div>';
|
|
|
|
|
+ textarea.value = '';
|
|
|
|
|
+ textarea.style.height = 'auto';
|
|
|
|
|
+ container.scrollTop = container.scrollHeight;
|
|
|
|
|
+
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ container.innerHTML += '<div class="msg ai"><div class="msg-avatar">🤖</div><div class="msg-bubble">好的,我来帮您处理这个请求。基于已解析的文档,我找到了相关内容可以补充到报告中。</div></div>';
|
|
|
|
|
+ container.scrollTop = container.scrollHeight;
|
|
|
|
|
+ }, 1000);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // === 视图切换 ===
|
|
|
|
|
+ function switchView(view) {
|
|
|
|
|
+ const originalBtn = document.getElementById('viewOriginal');
|
|
|
|
|
+ const markedBtn = document.getElementById('viewMarked');
|
|
|
|
|
+ const originalContent = document.getElementById('contentOriginal');
|
|
|
|
|
+ const markedContent = document.getElementById('contentMarked');
|
|
|
|
|
+
|
|
|
|
|
+ if (view === 'original') {
|
|
|
|
|
+ if (originalBtn) originalBtn.classList.add('active');
|
|
|
|
|
+ if (markedBtn) markedBtn.classList.remove('active');
|
|
|
|
|
+ if (originalContent) originalContent.style.display = 'block';
|
|
|
|
|
+ if (markedContent) markedContent.style.display = 'none';
|
|
|
|
|
+ showToast('已切换到原文视图 - 显示纯净文档内容', 'info');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ if (originalBtn) originalBtn.classList.remove('active');
|
|
|
|
|
+ if (markedBtn) markedBtn.classList.add('active');
|
|
|
|
|
+ if (originalContent) originalContent.style.display = 'none';
|
|
|
|
|
+ if (markedContent) markedContent.style.display = 'block';
|
|
|
|
|
+ showToast('已切换到标记视图 - 显示AI提取的实体标记和分析内容', 'info');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // === 文件高亮 ===
|
|
|
|
|
+ function highlightFile(el) {
|
|
|
|
|
+ document.querySelectorAll('.file-item').forEach(f => f.classList.remove('active'));
|
|
|
|
|
+ el.classList.add('active');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // === 上传模拟 ===
|
|
|
|
|
+ function simulateUpload() {
|
|
|
|
|
+ showToast('请选择要上传的文件', 'info');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // === 导出菜单 ===
|
|
|
|
|
+ function showExportMenu(btn) {
|
|
|
|
|
+ const menu = document.getElementById('exportMenu');
|
|
|
|
|
+ const rect = btn.getBoundingClientRect();
|
|
|
|
|
+ menu.style.top = (rect.bottom + 8) + 'px';
|
|
|
|
|
+ menu.style.right = (window.innerWidth - rect.right) + 'px';
|
|
|
|
|
+ menu.style.display = 'block';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function hideExportMenu() {
|
|
|
|
|
+ document.getElementById('exportMenu').style.display = 'none';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ document.addEventListener('click', function(e) {
|
|
|
|
|
+ if (!e.target.closest('[onclick*="showExportMenu"]') && !e.target.closest('#exportMenu')) {
|
|
|
|
|
+ hideExportMenu();
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // === AI Tab切换 ===
|
|
|
|
|
+ function switchAiTab(el, tab) {
|
|
|
|
|
+ document.querySelectorAll('.ai-tab').forEach(t => t.classList.remove('active'));
|
|
|
|
|
+ el.classList.add('active');
|
|
|
|
|
+ showToast('切换到 ' + el.textContent.trim(), 'info');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // === AI建议 ===
|
|
|
|
|
+ function acceptSuggestion() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const aiMessages = document.getElementById('aiMessages');
|
|
|
|
|
+ if (aiMessages) {
|
|
|
|
|
+ aiMessages.innerHTML += `<div class="msg user"><div class="msg-avatar">张</div><div class="msg-bubble">✓ 我已采纳建议</div></div>`;
|
|
|
|
|
+ aiMessages.innerHTML += `<div class="msg ai"><div class="msg-avatar">🤖</div><div class="msg-bubble">已根据您的采纳更新内容并插入到文档中。</div></div>`;
|
|
|
|
|
+ aiMessages.scrollTop = aiMessages.scrollHeight;
|
|
|
|
|
+ }
|
|
|
|
|
+ showToast && showToast('✓ 已采纳建议,已发送对话通知', 'success');
|
|
|
|
|
+ const card = document.getElementById('aiSuggestionCard');
|
|
|
|
|
+ if (card) try { card.remove(); } catch (e) {}
|
|
|
|
|
+ } catch (e) { console.warn('acceptSuggestion err', e); }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function ignoreSuggestion() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const aiMessages = document.getElementById('aiMessages');
|
|
|
|
|
+ if (aiMessages) {
|
|
|
|
|
+ aiMessages.innerHTML += `<div class="msg user"><div class="msg-avatar">张</div><div class="msg-bubble">✕ 我已忽略该建议</div></div>`;
|
|
|
|
|
+ aiMessages.innerHTML += `<div class="msg ai"><div class="msg-avatar">🤖</div><div class="msg-bubble">已忽略建议。如需恢复,请在“建议”中查找历史记录。</div></div>`;
|
|
|
|
|
+ aiMessages.scrollTop = aiMessages.scrollHeight;
|
|
|
|
|
+ }
|
|
|
|
|
+ showToast && showToast('已忽略建议(已转为对话)', 'info');
|
|
|
|
|
+ const card = document.getElementById('aiSuggestionCard');
|
|
|
|
|
+ if (card) try { card.remove(); } catch (e) {}
|
|
|
|
|
+ } catch (e) { console.warn('ignoreSuggestion err', e); }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function acceptContentSuggestion() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const aiMessages = document.getElementById('aiMessages');
|
|
|
|
|
+ if (aiMessages) {
|
|
|
|
|
+ aiMessages.innerHTML += `<div class="msg user"><div class="msg-avatar">张</div><div class="msg-bubble">✓ 请添加建议内容</div></div>`;
|
|
|
|
|
+ aiMessages.innerHTML += `<div class="msg ai"><div class="msg-avatar">🤖</div><div class="msg-bubble">已插入竞争格局分析章节,您可以在文档中查看并编辑。</div></div>`;
|
|
|
|
|
+ aiMessages.scrollTop = aiMessages.scrollHeight;
|
|
|
|
|
+ }
|
|
|
|
|
+ showToast && showToast('✓ 已添加建议内容(对话形式)', 'success');
|
|
|
|
|
+ } catch (e) { console.warn('acceptContentSuggestion err', e); }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function insertGeneratedContent() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const aiMessages = document.getElementById('aiMessages');
|
|
|
|
|
+ if (aiMessages) {
|
|
|
|
|
+ aiMessages.innerHTML += `<div class="msg user"><div class="msg-avatar">张</div><div class="msg-bubble">➕ 插入 AI 生成内容</div></div>`;
|
|
|
|
|
+ aiMessages.innerHTML += `<div class="msg ai"><div class="msg-avatar">🤖</div><div class="msg-bubble">已将 AI 生成的风险分析章节插入到报告中。</div></div>`;
|
|
|
|
|
+ aiMessages.scrollTop = aiMessages.scrollHeight;
|
|
|
|
|
+ }
|
|
|
|
|
+ showToast && showToast('✓ 已插入 AI 生成内容(对话形式)', 'success');
|
|
|
|
|
+ const card = document.querySelector('.ai-generated-card');
|
|
|
|
|
+ if (card) try { card.remove(); } catch (e) {}
|
|
|
|
|
+ } catch (e) { console.warn('insertGeneratedContent err', e); }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 将 AI 建议卡片移动到 AI 聊天区(以对话形式呈现),避免页面布局错位
|
|
|
|
|
+ (function(){
|
|
|
|
|
+ function moveCardToChat(cardEl) {
|
|
|
|
|
+ if (!cardEl) return false;
|
|
|
|
|
+ const aiMessages = document.getElementById('aiMessages');
|
|
|
|
|
+ if (!aiMessages) return false;
|
|
|
|
|
+ const title = cardEl.querySelector('.ai-suggestion-title') ? cardEl.querySelector('.ai-suggestion-title').textContent : '';
|
|
|
|
|
+ const contentEl = cardEl.querySelector('.ai-suggestion-content') || cardEl;
|
|
|
|
|
+ const contentHtml = contentEl.innerHTML || cardEl.innerHTML;
|
|
|
|
|
+ const wrapper = document.createElement('div');
|
|
|
|
|
+ wrapper.className = 'msg ai';
|
|
|
|
|
+ wrapper.innerHTML = `<div class="msg-avatar">🤖</div><div class="msg-bubble"><strong>${title}</strong><div style="margin-top:8px;">${contentHtml}</div></div>`;
|
|
|
|
|
+ aiMessages.appendChild(wrapper);
|
|
|
|
|
+ aiMessages.scrollTop = aiMessages.scrollHeight;
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ document.addEventListener('DOMContentLoaded', function(){
|
|
|
|
|
+ try {
|
|
|
|
|
+ const suggest = document.getElementById('aiSuggestionCard');
|
|
|
|
|
+ if (suggest) {
|
|
|
|
|
+ moveCardToChat(suggest);
|
|
|
|
|
+ try { suggest.remove(); } catch(e){}
|
|
|
|
|
+ }
|
|
|
|
|
+ const gen = document.querySelector('.ai-generated-card');
|
|
|
|
|
+ if (gen) {
|
|
|
|
|
+ moveCardToChat(gen);
|
|
|
|
|
+ try { gen.remove(); } catch(e){}
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) { console.warn('move AI suggestion to chat err', e); }
|
|
|
|
|
+ });
|
|
|
|
|
+ })();
|
|
|
|
|
+
|
|
|
|
|
+ // === Toast ===
|
|
|
|
|
+ function showToast(msg, type) {
|
|
|
|
|
+ const box = document.getElementById('toastContainer');
|
|
|
|
|
+ const icons = { success: '✅', error: '❌', info: 'ℹ️', warning: '⚠️' };
|
|
|
|
|
+ const colors = { success: '#f6ffed', error: '#fff1f0', info: 'var(--primary-light)', warning: '#fffbe6' };
|
|
|
|
|
+ const textColors = { success: 'var(--success)', error: 'var(--danger)', info: 'var(--primary)', warning: 'var(--warning)' };
|
|
|
|
|
+ const toast = document.createElement('div');
|
|
|
|
|
+ toast.className = `toast show ${type}`;
|
|
|
|
|
+ toast.innerHTML = '<span style="width:24px;height:24px;border-radius:50%;background:' + colors[type] + ';color:' + textColors[type] + ';display:flex;align-items:center;justify-content:center;font-size:12px;">' + icons[type] + '</span><span style="font-size:13px;margin-left:10px;">' + msg + '</span>';
|
|
|
|
|
+ box.appendChild(toast);
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ toast.classList.remove('show');
|
|
|
|
|
+ setTimeout(() => toast.remove(), 300);
|
|
|
|
|
+ }, 3000);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // === 关闭所有 ===
|
|
|
|
|
+ function closeAll() {
|
|
|
|
|
+ document.getElementById('notifPanel').style.transform = 'translateX(100%)';
|
|
|
|
|
+ document.getElementById('overlay').style.display = 'none';
|
|
|
|
|
+ document.getElementById('fabPanel').style.display = 'none';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // === 数据关系表弹窗 ===
|
|
|
|
|
+ let currentEntityData = {};
|
|
|
|
|
+
|
|
|
|
|
+ function showDataRelationModal(entityName, entityType, entityCategory, sourceFile) {
|
|
|
|
|
+ currentEntityData = {
|
|
|
|
|
+ name: entityName,
|
|
|
|
|
+ type: entityType,
|
|
|
|
|
+ category: entityCategory,
|
|
|
|
|
+ source: sourceFile,
|
|
|
|
|
+ originalValue: entityName,
|
|
|
|
|
+ currentValue: entityName
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 设置弹窗内容
|
|
|
|
|
+ document.getElementById('relationEntityName').textContent = entityName;
|
|
|
|
|
+ document.getElementById('relationEntityType').textContent = entityType + ' · 来自 ' + (sourceFile || '未知来源');
|
|
|
|
|
+
|
|
|
|
|
+ // 动态创建或更新来源信息区域(如果 modal HTML 中不存在)
|
|
|
|
|
+ let sourceInfoEl = document.getElementById('relationSourceInfo');
|
|
|
|
|
+ if (!sourceInfoEl) {
|
|
|
|
|
+ const titleEl = document.querySelector('.data-relation-title');
|
|
|
|
|
+ if (titleEl) {
|
|
|
|
|
+ sourceInfoEl = document.createElement('div');
|
|
|
|
|
+ sourceInfoEl.id = 'relationSourceInfo';
|
|
|
|
|
+ sourceInfoEl.className = 'relation-source';
|
|
|
|
|
+ sourceInfoEl.style = 'margin-top:6px;font-size:12px;color:var(--text3);';
|
|
|
|
|
+ titleEl.appendChild(sourceInfoEl);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (sourceInfoEl) {
|
|
|
|
|
+ const safeSource = sourceFile || '未知来源';
|
|
|
|
|
+ sourceInfoEl.innerHTML = `来源:<a href="#" id="relationSourceLink" onclick="openSourceFile(event, '${safeSource.replace(/'/g, "\\'")}')">查看来源文件</a>`;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 设置图标
|
|
|
|
|
+ const iconEl = document.getElementById('relationIcon');
|
|
|
|
|
+ const icons = { entity: '🏢', concept: '💡', data: '📊', location: '📍' };
|
|
|
|
|
+ iconEl.textContent = icons[entityCategory] || '🏷️';
|
|
|
|
|
+
|
|
|
|
|
+ // 生成关系表
|
|
|
|
|
+ generateRelationTable(entityName, entityType, entityCategory, sourceFile);
|
|
|
|
|
+
|
|
|
|
|
+ // 显示弹窗
|
|
|
|
|
+ document.getElementById('dataRelationModal').classList.add('show');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function generateRelationTable(entityName, entityType, entityCategory, sourceFile) {
|
|
|
|
|
+ const tableBody = document.getElementById('relationTableBody');
|
|
|
|
|
+ const relationTags = document.getElementById('relationTags');
|
|
|
|
|
+
|
|
|
|
|
+ // 模拟关系数据
|
|
|
|
|
+ const relationData = {
|
|
|
|
|
+ '智慧园区': [
|
|
|
|
|
+ { property: '实体名称', original: '智慧园区', current: '智慧园区', canEdit: true },
|
|
|
|
|
+ { property: '实体类型', original: '核心实体', current: '核心实体', canEdit: false },
|
|
|
|
|
+ { property: '数据来源', original: '项目可行性研究报告.docx', current: '项目可行性研究报告.docx', canEdit: false },
|
|
|
|
|
+ { property: '出现次数', original: '8次', current: '8次', canEdit: false }
|
|
|
|
|
+ ],
|
|
|
|
|
+ '产业升级': [
|
|
|
|
|
+ { property: '概念名称', original: '产业升级', current: '产业升级', canEdit: true },
|
|
|
|
|
+ { property: '概念类型', original: '发展概念', current: '发展概念', canEdit: false },
|
|
|
|
|
+ { property: '数据来源', original: '项目可行性研究报告.docx', current: '项目可行性研究报告.docx', canEdit: false },
|
|
|
|
|
+ { property: '关联强度', original: '高', current: '高', canEdit: false }
|
|
|
|
|
+ ],
|
|
|
|
|
+ '1,789亿元': [
|
|
|
|
|
+ { property: '数值', original: '1,789亿元', current: '1,789亿元', canEdit: true },
|
|
|
|
|
+ { property: '数据类型', original: '市场规模', current: '市场规模', canEdit: false },
|
|
|
|
|
+ { property: '数据来源', original: '市场调研数据.pdf', current: '市场调研数据.pdf', canEdit: false },
|
|
|
|
|
+ { property: '置信度', original: '95%', current: '95%', canEdit: false }
|
|
|
|
|
+ ]
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const data = relationData[entityName] ? relationData[entityName].slice() : [
|
|
|
|
|
+ { property: '标签名称', original: entityName, current: entityName, canEdit: true },
|
|
|
|
|
+ { property: '标签类型', original: entityType, current: entityType, canEdit: false },
|
|
|
|
|
+ { property: '数据来源', original: sourceFile || '未知来源', current: sourceFile || '未知来源', canEdit: false }
|
|
|
|
|
+ ];
|
|
|
|
|
+
|
|
|
|
|
+ // 确保展示来源文件(优先使用传入的 sourceFile)
|
|
|
|
|
+ const hasSourceRow = data.some(r => /来源|数据来源/.test(r.property));
|
|
|
|
|
+ if (!hasSourceRow) {
|
|
|
|
|
+ data.unshift({ property: '来源文件', original: sourceFile || '未知来源', current: sourceFile || '未知来源', canEdit: false });
|
|
|
|
|
+ } else if (sourceFile) {
|
|
|
|
|
+ data.forEach(r => {
|
|
|
|
|
+ if (/来源|数据来源/.test(r.property)) {
|
|
|
|
|
+ r.original = sourceFile;
|
|
|
|
|
+ r.current = sourceFile;
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ tableBody.innerHTML = data.map(row => `
|
|
|
|
|
+ <tr>
|
|
|
|
|
+ <td>${row.property}</td>
|
|
|
|
|
+ <td class="original">${row.original}</td>
|
|
|
|
|
+ <td class="current">
|
|
|
|
|
+ ${row.canEdit ?
|
|
|
|
|
+ `<input type="text" class="relation-input" value="${row.current}" onchange="updateEntityValue('${row.property}', this.value)">` :
|
|
|
|
|
+ row.current
|
|
|
|
|
+ }
|
|
|
|
|
+ </td>
|
|
|
|
|
+ <td>
|
|
|
|
|
+ ${row.canEdit ? '<button class="btn" onclick="resetEntityValue(this)" style="font-size:11px;padding:4px 8px;">重置</button>' : '-'}
|
|
|
|
|
+ </td>
|
|
|
|
|
+ </tr>
|
|
|
|
|
+ `).join('');
|
|
|
|
|
+
|
|
|
|
|
+ // 生成关联标签
|
|
|
|
|
+ const relatedEntities = ['产业升级', '城市现代化', '智能化管理', '1,789亿元', '华南地区'];
|
|
|
|
|
+ relationTags.innerHTML = relatedEntities.map(entity => `
|
|
|
|
|
+ <span class="relation-tag" onclick="showDataRelationModal('${entity}', '关联实体', 'entity', '关联文档')">${entity}</span>
|
|
|
|
|
+ `).join('');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function updateEntityValue(property, newValue) {
|
|
|
|
|
+ currentEntityData.currentValue = newValue;
|
|
|
|
|
+ showToast(`已更新 ${property}: ${newValue}`, 'success');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function resetEntityValue(btn) {
|
|
|
|
|
+ const input = btn.closest('tr').querySelector('.relation-input');
|
|
|
|
|
+ input.value = currentEntityData.originalValue;
|
|
|
|
|
+ currentEntityData.currentValue = currentEntityData.originalValue;
|
|
|
|
|
+ showToast('已重置为原始值', 'info');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function saveDataRelationChanges() {
|
|
|
|
|
+ if (currentEntityData.currentValue !== currentEntityData.originalValue) {
|
|
|
|
|
+ showToast(`标签已更新: ${currentEntityData.originalValue} → ${currentEntityData.currentValue}`, 'success');
|
|
|
|
|
+ // 这里可以添加实际的保存逻辑
|
|
|
|
|
+ }
|
|
|
|
|
+ closeDataRelationModal();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function closeDataRelationModal() {
|
|
|
|
|
+ document.getElementById('dataRelationModal').classList.remove('show');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // === 要素关系图谱 ===
|
|
|
|
|
+ function toggleKnowledgeGraph() {
|
|
|
|
|
+ const modal = document.getElementById('knowledgeGraphModal');
|
|
|
|
|
+ const overlay = document.getElementById('kgOverlay');
|
|
|
|
|
+ const btn = document.getElementById('graphBtn');
|
|
|
|
|
+ const isOpen = modal.classList.contains('show');
|
|
|
|
|
+
|
|
|
|
|
+ if (isOpen) {
|
|
|
|
|
+ modal.classList.remove('show');
|
|
|
|
|
+ overlay.style.display = 'none';
|
|
|
|
|
+ if (btn) btn.classList.remove('active');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ modal.classList.add('show');
|
|
|
|
|
+ overlay.style.display = 'block';
|
|
|
|
|
+ if (btn) btn.classList.add('active');
|
|
|
|
|
+ showToast('已打开要素关系图谱', 'info');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function closeKnowledgeGraph() {
|
|
|
|
|
+ const modal = document.getElementById('knowledgeGraphModal');
|
|
|
|
|
+ const overlay = document.getElementById('kgOverlay');
|
|
|
|
|
+ const btn = document.getElementById('graphBtn');
|
|
|
|
|
+ modal.classList.remove('show');
|
|
|
|
|
+ overlay.style.display = 'none';
|
|
|
|
|
+ if (btn) btn.classList.remove('active');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function switchGraphView(view) {
|
|
|
|
|
+ const graphView = document.getElementById('graphView');
|
|
|
|
|
+ const listView = document.getElementById('listView');
|
|
|
|
|
+ const graphBtn = document.getElementById('graphViewBtn');
|
|
|
|
|
+ const listBtn = document.getElementById('listViewBtn');
|
|
|
|
|
+
|
|
|
|
|
+ if (view === 'graph') {
|
|
|
|
|
+ graphView.style.display = 'block';
|
|
|
|
|
+ listView.style.display = 'none';
|
|
|
|
|
+ graphBtn.classList.add('active');
|
|
|
|
|
+ listBtn.classList.remove('active');
|
|
|
|
|
+ showToast('切换到图谱视图', 'info');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ graphView.style.display = 'none';
|
|
|
|
|
+ listView.style.display = 'block';
|
|
|
|
|
+ graphBtn.classList.remove('active');
|
|
|
|
|
+ listBtn.classList.add('active');
|
|
|
|
|
+ showToast('切换到列表视图', 'info');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function filterEntities(query) {
|
|
|
|
|
+ const items = document.querySelectorAll('.entity-item');
|
|
|
|
|
+ const lowerQuery = query.toLowerCase();
|
|
|
|
|
+
|
|
|
|
|
+ items.forEach(item => {
|
|
|
|
|
+ const name = item.querySelector('.entity-name').textContent.toLowerCase();
|
|
|
|
|
+ if (name.includes(lowerQuery) || query === '') {
|
|
|
|
|
+ item.style.display = 'flex';
|
|
|
|
|
+ } else {
|
|
|
|
|
+ item.style.display = 'none';
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function highlightEntity(entityName) {
|
|
|
|
|
+ // 高亮图谱中的节点
|
|
|
|
|
+ document.querySelectorAll('.graph-node').forEach(node => {
|
|
|
|
|
+ node.classList.remove('highlighted');
|
|
|
|
|
+ if (node.dataset.entity === entityName) {
|
|
|
|
|
+ node.classList.add('highlighted');
|
|
|
|
|
+ node.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ showToast('已高亮要素: ' + entityName, 'info');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function locateEntity(entityName) {
|
|
|
|
|
+ closeKnowledgeGraph();
|
|
|
|
|
+ // 切换到标记视图
|
|
|
|
|
+ switchView('marked');
|
|
|
|
|
+ // 滚动到对应位置(这里可以根据实际情况调整)
|
|
|
|
|
+ showToast('已定位到要素: ' + entityName, 'success');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function editEntity(entityName) {
|
|
|
|
|
+ showToast('编辑要素: ' + entityName, 'info');
|
|
|
|
|
+ // 这里可以打开要素编辑弹窗
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 图谱节点点击事件
|
|
|
|
|
+ document.addEventListener('DOMContentLoaded', function() {
|
|
|
|
|
+ document.querySelectorAll('.graph-node').forEach(node => {
|
|
|
|
|
+ node.addEventListener('click', function() {
|
|
|
|
|
+ const entityName = this.dataset.entity;
|
|
|
|
|
+ highlightEntity(entityName);
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 页面加载后初始化来源徽章
|
|
|
|
|
+ document.addEventListener('DOMContentLoaded', function() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (typeof initializeSourceBadges === 'function') initializeSourceBadges();
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.warn('initializeSourceBadges error', e);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // === 段落选中与交互控制 ===
|
|
|
|
|
+ // 将光标所在段落标记为 selected,仅高亮该段
|
|
|
|
|
+ function updateSelectedParagraph() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const sel = window.getSelection();
|
|
|
|
|
+ if (!sel || sel.rangeCount === 0) return;
|
|
|
|
|
+ const anchor = sel.anchorNode;
|
|
|
|
|
+ if (!anchor) return;
|
|
|
|
|
+
|
|
|
|
|
+ // 找到包含光标的段落元素(p 或 div 作段落)
|
|
|
|
|
+ let paragraph = null;
|
|
|
|
|
+ if (anchor.nodeType === Node.TEXT_NODE) paragraph = anchor.parentElement.closest('p, div');
|
|
|
|
|
+ else if (anchor.nodeType === Node.ELEMENT_NODE) paragraph = anchor.closest('p, div');
|
|
|
|
|
+
|
|
|
|
|
+ // 仅在编辑区内生效
|
|
|
|
|
+ document.querySelectorAll('.editor-content p, .editor-content div').forEach(el => el.classList.remove('selected'));
|
|
|
|
|
+ if (paragraph && paragraph.closest('.editor-content')) {
|
|
|
|
|
+ paragraph.classList.add('selected');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.warn('updateSelectedParagraph error', e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 监听选择变化、点击、键盘输入以更新段落选中状态
|
|
|
|
|
+ document.addEventListener('selectionchange', () => {
|
|
|
|
|
+ const active = document.activeElement;
|
|
|
|
|
+ if (active && active.classList && active.classList.contains('editor-content')) {
|
|
|
|
|
+ updateSelectedParagraph();
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 点击编辑区也触发更新(兼容鼠标点击)
|
|
|
|
|
+ document.querySelectorAll('.editor-content').forEach(editor => {
|
|
|
|
|
+ editor.addEventListener('click', updateSelectedParagraph);
|
|
|
|
|
+ editor.addEventListener('keyup', updateSelectedParagraph);
|
|
|
|
|
+ editor.addEventListener('input', updateSelectedParagraph);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 点击页面其它区域时清除段落选中样式
|
|
|
|
|
+ document.addEventListener('click', (e) => {
|
|
|
|
|
+ if (!e.target.closest('.editor-content')) {
|
|
|
|
|
+ document.querySelectorAll('.editor-content p.selected, .editor-content div.selected').forEach(p => p.classList.remove('selected'));
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // === AI润色/拼写确认与文本替换(右键) ===
|
|
|
|
|
+ // showAiPolishConfirm / showSpellCheckConfirm / applyTextReplacement 已定义于右键逻辑区域
|
|
|
|
|
+
|
|
|
|
|
+ // === 初始化 ===
|
|
|
|
|
+ setTimeout(() => showToast('欢迎使用灵越智报平台 🎉', 'success'), 500);
|
|
|
|
|
+
|
|
|
|
|
+ // 更新欢迎语
|
|
|
|
|
+ (function() {
|
|
|
|
|
+ const hour = new Date().getHours();
|
|
|
|
|
+ let greeting = '早上好';
|
|
|
|
|
+ if (hour >= 12 && hour < 18) greeting = '下午好';
|
|
|
|
|
+ else if (hour >= 18) greeting = '晚上好';
|
|
|
|
|
+ const el = document.querySelector('.welcome h1');
|
|
|
|
|
+ if (el) el.innerHTML = greeting + ',张三!<span>智能报告,洞察未来。</span>';
|
|
|
|
|
+ })();
|
|
|
|
|
+
|
|
|
|
|
+ // 模拟解析进度
|
|
|
|
|
+ let progress1 = 65;
|
|
|
|
|
+ setInterval(() => {
|
|
|
|
|
+ progress1 += Math.random() * 3;
|
|
|
|
|
+ if (progress1 >= 100) {
|
|
|
|
|
+ progress1 = 100;
|
|
|
|
|
+ const el = document.querySelector('#parsingFile1 .file-status');
|
|
|
|
|
+ if (el) {
|
|
|
|
|
+ el.textContent = '✓ 已完成';
|
|
|
|
|
+ el.className = 'file-status done';
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const el = document.querySelector('#parsingFile1 .file-status');
|
|
|
|
|
+ if (el && el.classList.contains('parsing')) {
|
|
|
|
|
+ el.textContent = '📊 解析中 ' + Math.floor(progress1) + '%';
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }, 2000);
|
|
|
|
|
+
|
|
|
|
|
+ // === 文件项交互(悬浮图标、解析弹窗、查看/引用/删除) ===
|
|
|
|
|
+ function initializeFileActions() {
|
|
|
|
|
+ document.querySelectorAll('.file-item').forEach(item => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const statusEl = item.querySelector('.file-status');
|
|
|
|
|
+ const statusParsing = statusEl && statusEl.classList.contains('parsing');
|
|
|
|
|
+ const statusDone = statusEl && statusEl.classList.contains('done');
|
|
|
|
|
+
|
|
|
|
|
+ // store demo content for viewing/引用
|
|
|
|
|
+ if (!item.dataset.content) {
|
|
|
|
|
+ item.dataset.content = `【${item.querySelector('.file-name') ? item.querySelector('.file-name').textContent : '文档'}】原文预览:这是用于演示的文档内容片段。`;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // create actions container
|
|
|
|
|
+ let actions = item.querySelector('.file-actions');
|
|
|
|
|
+ if (!actions) {
|
|
|
|
|
+ actions = document.createElement('div');
|
|
|
|
|
+ actions.className = 'file-actions';
|
|
|
|
|
+ item.appendChild(actions);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ actions.innerHTML = '';
|
|
|
|
|
+
|
|
|
|
|
+ if (statusParsing) {
|
|
|
|
|
+ // parsing: show delete icon on hover
|
|
|
|
|
+ const delBtn = document.createElement('div');
|
|
|
|
|
+ delBtn.className = 'action-btn';
|
|
|
|
|
+ delBtn.title = '删除';
|
|
|
|
|
+ delBtn.innerHTML = '🗑️';
|
|
|
|
|
+ delBtn.onclick = (e) => { e.stopPropagation(); deleteFile(item); };
|
|
|
|
|
+ actions.appendChild(delBtn);
|
|
|
|
|
+
|
|
|
|
|
+ // clicking the file shows parsing popover
|
|
|
|
|
+ item.onclick = (e) => { e.stopPropagation(); showParsingPopover(item); };
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // done: show view, quote, delete
|
|
|
|
|
+ const viewBtn = document.createElement('div');
|
|
|
|
|
+ viewBtn.className = 'action-btn';
|
|
|
|
|
+ viewBtn.title = '查看';
|
|
|
|
|
+ viewBtn.innerHTML = '👁️';
|
|
|
|
|
+ viewBtn.onclick = (e) => { e.stopPropagation(); showFilePreview(item); };
|
|
|
|
|
+ actions.appendChild(viewBtn);
|
|
|
|
|
+
|
|
|
|
|
+ const quoteBtn = document.createElement('div');
|
|
|
|
|
+ quoteBtn.className = 'action-btn';
|
|
|
|
|
+ quoteBtn.title = '引用到AI助手';
|
|
|
|
|
+ quoteBtn.innerHTML = '🔗';
|
|
|
|
|
+ quoteBtn.onclick = (e) => { e.stopPropagation(); quoteFileToAI(item); };
|
|
|
|
|
+ actions.appendChild(quoteBtn);
|
|
|
|
|
+
|
|
|
|
|
+ const delBtn = document.createElement('div');
|
|
|
|
|
+ delBtn.className = 'action-btn';
|
|
|
|
|
+ delBtn.title = '删除';
|
|
|
|
|
+ delBtn.innerHTML = '🗑️';
|
|
|
|
|
+ delBtn.onclick = (e) => { e.stopPropagation(); deleteFile(item); };
|
|
|
|
|
+ actions.appendChild(delBtn);
|
|
|
|
|
+
|
|
|
|
|
+ // clicking file selects it (preview)
|
|
|
|
|
+ item.onclick = (e) => { e.stopPropagation(); showFilePreview(item); };
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) { console.error('init file action', err); }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 左侧 tabs 切换
|
|
|
|
|
+ function switchLeftTab(tab) {
|
|
|
|
|
+ const docs = document.getElementById('leftDocsSection');
|
|
|
|
|
+ const files = document.getElementById('leftFilesSection');
|
|
|
|
|
+
|
|
|
|
|
+ // safely toggle primary tab elements (some templates may use different ids)
|
|
|
|
|
+ const tabDocsEl = document.getElementById('tabDocs');
|
|
|
|
|
+ const tabFilesEl = document.getElementById('tabFiles');
|
|
|
|
|
+ if (tabDocsEl) tabDocsEl.classList.toggle('active', tab === 'docs');
|
|
|
|
|
+ if (tabFilesEl) tabFilesEl.classList.toggle('active', tab === 'files');
|
|
|
|
|
+
|
|
|
|
|
+ // sync top header tabs if present (these IDs exist in the DOM)
|
|
|
|
|
+ const topDocs = document.getElementById('tabDocsTop');
|
|
|
|
|
+ const topFiles = document.getElementById('tabFilesTop');
|
|
|
|
|
+ if (topDocs) topDocs.classList.toggle('active', tab === 'docs');
|
|
|
|
|
+ if (topFiles) topFiles.classList.toggle('active', tab === 'files');
|
|
|
|
|
+ // Keep the left panel split into two parts:
|
|
|
|
|
+ // - top: dynamic area (.docs-area) that shows either "我的文档" or "我的附件"
|
|
|
|
|
+ // - bottom: .recent-area (always visible)
|
|
|
|
|
+ const docsArea = docs ? docs.querySelector('.docs-area') : null;
|
|
|
|
|
+ const recentArea = docs ? docs.querySelector('.recent-area') : null;
|
|
|
|
|
+
|
|
|
|
|
+ // Ensure the leftDocsSection is visible (we render top content into docsArea)
|
|
|
|
|
+ if (docs) docs.style.display = '';
|
|
|
|
|
+ if (files) files.style.display = 'none'; // keep files section unused visually
|
|
|
|
|
+ if (recentArea) recentArea.style.display = '';
|
|
|
|
|
+
|
|
|
|
|
+ if (tab === 'docs') {
|
|
|
|
|
+ // show documents list in the top area
|
|
|
|
|
+ if (docsArea) {
|
|
|
|
|
+ docsArea.style.display = '';
|
|
|
|
|
+ // re-render / sort documents if function exists
|
|
|
|
|
+ try { if (typeof updateReportsCountAndSort === 'function') updateReportsCountAndSort(); } catch (e) { console.warn('updateReportsCountAndSort err', e); }
|
|
|
|
|
+ // restore header title to "报告记录"
|
|
|
|
|
+ try {
|
|
|
|
|
+ const reportsTitleEl = document.getElementById('reportsTitle');
|
|
|
|
|
+ const reportsCountEl = document.getElementById('reportsCount');
|
|
|
|
|
+ if (reportsTitleEl && reportsTitleEl.firstChild) reportsTitleEl.firstChild.nodeValue = '报告记录 ';
|
|
|
|
|
+ if (reportsCountEl) {
|
|
|
|
|
+ // updateReportsCountAndSort already sets count, but ensure display format
|
|
|
|
|
+ reportsCountEl.textContent = reportsCountEl.textContent || '· ' + (document.querySelectorAll('#leftDocsSection .doc-card').length || 0);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (ee) { console.warn('restore reports title err', ee); }
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // show attachments list in the same top area
|
|
|
|
|
+ if (docsArea) {
|
|
|
|
|
+ docsArea.style.display = '';
|
|
|
|
|
+ try { if (typeof renderAttachments === 'function') renderAttachments(); } catch (e) { console.warn('renderAttachments err', e); }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 更新报告计数并按时间倒序排列列表
|
|
|
|
|
+ function updateReportsCountAndSort() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const list = document.querySelector('#leftDocsSection .doc-list');
|
|
|
|
|
+ if (!list) return;
|
|
|
|
|
+ const items = Array.from(list.querySelectorAll('.doc-card'));
|
|
|
|
|
+ // parse date from .doc-time like "2026/1/26 11:51:44"
|
|
|
|
|
+ items.sort((a, b) => {
|
|
|
|
|
+ const ta = a.querySelector('.doc-time') ? new Date(a.querySelector('.doc-time').textContent.replace(/-/g, '/')) : new Date(0);
|
|
|
|
|
+ const tb = b.querySelector('.doc-time') ? new Date(b.querySelector('.doc-time').textContent.replace(/-/g, '/')) : new Date(0);
|
|
|
|
|
+ return tb - ta;
|
|
|
|
|
+ });
|
|
|
|
|
+ // re-append in sorted order
|
|
|
|
|
+ items.forEach(it => list.appendChild(it));
|
|
|
|
|
+ // update count
|
|
|
|
|
+ const count = items.length;
|
|
|
|
|
+ const rc = document.getElementById('reportsCount');
|
|
|
|
|
+ if (rc) rc.textContent = '· ' + count;
|
|
|
|
|
+ const leftBadge = document.getElementById('leftDocsCount');
|
|
|
|
|
+ if (leftBadge) leftBadge.textContent = count;
|
|
|
|
|
+ } catch (e) { console.warn('updateReportsCountAndSort err', e); }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ document.addEventListener('DOMContentLoaded', function() {
|
|
|
|
|
+ try { updateReportsCountAndSort(); } catch (e) {}
|
|
|
|
|
+ // wire up search/new buttons (placeholders)
|
|
|
|
|
+ const searchBtn = document.getElementById('leftSearchBtn');
|
|
|
|
|
+ if (searchBtn) searchBtn.onclick = () => { const q = prompt('搜索报告:'); if (q) alert('搜索: ' + q); };
|
|
|
|
|
+ const newBtn = document.getElementById('newReportBtn');
|
|
|
|
|
+ if (newBtn) newBtn.onclick = () => { alert('新建报告(示意)'); };
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ let currentParsingPopover = null;
|
|
|
|
|
+ function showParsingPopover(item) {
|
|
|
|
|
+ closeParsingPopover();
|
|
|
|
|
+ const rect = item.getBoundingClientRect();
|
|
|
|
|
+ const pop = document.createElement('div');
|
|
|
|
|
+ pop.className = 'parsing-popover';
|
|
|
|
|
+ pop.style.left = Math.min(rect.right + 12, window.innerWidth - 340) + 'px';
|
|
|
|
|
+ pop.style.top = Math.max(rect.top, 80) + 'px';
|
|
|
|
|
+ pop.innerHTML = `<div class="close-btn" onclick="closeParsingPopover()">×</div>
|
|
|
|
|
+ <div class="title"><div>解析过程 — ${item.querySelector('.file-name') ? item.querySelector('.file-name').textContent : ''}</div></div>
|
|
|
|
|
+ <div class="parsing-progress">
|
|
|
|
|
+ <span class="seg active"></span>
|
|
|
|
|
+ <span class="seg"></span>
|
|
|
|
|
+ <span class="seg"></span>
|
|
|
|
|
+ <span class="seg"></span>
|
|
|
|
|
+ <span class="seg"></span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="steps-list">
|
|
|
|
|
+ <div class="step active"><div class="dot"></div><div class="label">文档读取</div></div>
|
|
|
|
|
+ <div class="step pending"><div class="dot"></div><div class="label">结构解析</div></div>
|
|
|
|
|
+ <div class="step pending"><div class="dot"></div><div class="label">实体提取</div></div>
|
|
|
|
|
+ <div class="step pending"><div class="dot"></div><div class="label">关系分析</div></div>
|
|
|
|
|
+ <div class="step pending"><div class="dot"></div><div class="label">图谱构建</div></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div style="margin-top:10px;font-size:12px;color:var(--text3);">注:此为演示流程,实际解析时间取决于文档大小与系统性能。</div>`;
|
|
|
|
|
+ document.body.appendChild(pop);
|
|
|
|
|
+ currentParsingPopover = pop;
|
|
|
|
|
+
|
|
|
|
|
+ // simulate progress steps (vertical, completed=green, active=blue, pending=gray)
|
|
|
|
|
+ let idx = 0;
|
|
|
|
|
+ const steps = pop.querySelectorAll('.step');
|
|
|
|
|
+ const segs = pop.querySelectorAll('.parsing-progress .seg');
|
|
|
|
|
+ // ensure initial classes: first active, others pending
|
|
|
|
|
+ steps.forEach((s, i) => {
|
|
|
|
|
+ if (i === 0) { s.classList.remove('pending'); s.classList.add('active'); }
|
|
|
|
|
+ else { s.classList.remove('active'); s.classList.add('pending'); }
|
|
|
|
|
+ });
|
|
|
|
|
+ const t = setInterval(() => {
|
|
|
|
|
+ // mark previous as completed
|
|
|
|
|
+ if (idx > 0 && steps[idx-1]) {
|
|
|
|
|
+ steps[idx-1].classList.remove('active');
|
|
|
|
|
+ steps[idx-1].classList.remove('pending');
|
|
|
|
|
+ steps[idx-1].classList.add('completed');
|
|
|
|
|
+ }
|
|
|
|
|
+ // set current as active
|
|
|
|
|
+ if (idx < steps.length && steps[idx]) {
|
|
|
|
|
+ steps[idx].classList.remove('pending');
|
|
|
|
|
+ steps[idx].classList.remove('completed');
|
|
|
|
|
+ steps[idx].classList.add('active');
|
|
|
|
|
+ }
|
|
|
|
|
+ // update segs
|
|
|
|
|
+ segs.forEach((s, i) => {
|
|
|
|
|
+ if (i < idx) s.classList.add('active');
|
|
|
|
|
+ else s.classList.remove('active');
|
|
|
|
|
+ });
|
|
|
|
|
+ idx++;
|
|
|
|
|
+ if (idx > steps.length) {
|
|
|
|
|
+ clearInterval(t);
|
|
|
|
|
+ // mark last as completed
|
|
|
|
|
+ if (steps[steps.length-1]) {
|
|
|
|
|
+ steps[steps.length-1].classList.remove('active');
|
|
|
|
|
+ steps[steps.length-1].classList.remove('pending');
|
|
|
|
|
+ steps[steps.length-1].classList.add('completed');
|
|
|
|
|
+ }
|
|
|
|
|
+ showToast('解析完成:已生成要素与图谱', 'success');
|
|
|
|
|
+ // update status to done for demo
|
|
|
|
|
+ const statusEl = item.querySelector('.file-status');
|
|
|
|
|
+ if (statusEl) { statusEl.textContent = '✓ 已完成'; statusEl.className = 'file-status done'; }
|
|
|
|
|
+ initializeFileActions();
|
|
|
|
|
+ closeParsingPopover();
|
|
|
|
|
+ }
|
|
|
|
|
+ }, 900);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function closeParsingPopover() {
|
|
|
|
|
+ if (currentParsingPopover) { currentParsingPopover.remove(); currentParsingPopover = null; }
|
|
|
|
|
+ }
|
|
|
|
|
+ // === 本地文件预览映射与本地上下文存储(演示 / 可替换为后端接口) ===
|
|
|
|
|
+ const fileContentsMap = {
|
|
|
|
|
+ '市场调研数据.pdf': '【市场调研数据.pdf】\\n摘要:本报告基于2024年全国范围抽样调研,市场规模、增长率、细分行业表现等关键指标已整理。',
|
|
|
|
|
+ '技术方案说明.pdf': '【技术方案说明.pdf】\\n摘要:本文档描述了平台架构、数据接入与解析流程、实体抽取模型与接口。',
|
|
|
|
|
+ '项目可行性研究报告.docx': '【项目可行性研究报告.docx】\\n原文节选:本项目位于华南地区,规划总面积约50万平方米,预计总投资12.5亿元。',
|
|
|
|
|
+ '财务预测表.xlsx': '【财务预测表.xlsx】\\n表格摘要:收入、利润、投资回收期与分年度预测数据(见表格)。'
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const localKnowledgeContext = {
|
|
|
|
|
+ files: {},
|
|
|
|
|
+ addFile(fileName, content) { this.files[fileName] = content; },
|
|
|
|
|
+ removeFile(fileName) { delete this.files[fileName]; },
|
|
|
|
|
+ listFiles() { return Object.keys(this.files); },
|
|
|
|
|
+ getCombinedContext() { return Object.values(this.files).join('\\n\\n'); }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ function showFilePreview(item) {
|
|
|
|
|
+ // 使用大弹窗模态展示文件预览(不再在右侧显示)
|
|
|
|
|
+ closeFilePreview();
|
|
|
|
|
+ const nameEl = item.querySelector('.file-name');
|
|
|
|
|
+ const fileName = nameEl ? nameEl.textContent.trim() : null;
|
|
|
|
|
+ const content = (fileName && fileContentsMap[fileName]) ? fileContentsMap[fileName] : (item.dataset.content || (fileName ? `原文:${fileName}` : '无预览内容'));
|
|
|
|
|
+
|
|
|
|
|
+ const overlay = document.createElement('div');
|
|
|
|
|
+ overlay.className = 'file-preview-modal-overlay';
|
|
|
|
|
+
|
|
|
|
|
+ const modal = document.createElement('div');
|
|
|
|
|
+ modal.className = 'file-preview-modal';
|
|
|
|
|
+ modal.innerHTML = `
|
|
|
|
|
+ <div class="modal-header">
|
|
|
|
|
+ <div class="modal-title">${fileName || '文件预览'}</div>
|
|
|
|
|
+ <div style="font-size:12px;color:var(--text3)">${new Date().toLocaleString()}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <button class="file-preview-close" onclick="closeFilePreview()">×</button>
|
|
|
|
|
+ <div class="modal-body" id="filePreviewContent">${content}</div>
|
|
|
|
|
+ `;
|
|
|
|
|
+
|
|
|
|
|
+ overlay.appendChild(modal);
|
|
|
|
|
+ document.body.appendChild(overlay);
|
|
|
|
|
+ // prevent scrolling behind modal
|
|
|
|
|
+ document.body.style.overflow = 'hidden';
|
|
|
|
|
+ showToast('已打开文件预览弹窗', 'info');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function closeFilePreview() {
|
|
|
|
|
+ const overlay = document.querySelector('.file-preview-modal-overlay');
|
|
|
|
|
+ if (overlay) overlay.remove();
|
|
|
|
|
+ document.body.style.overflow = '';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 将文件内容加入本地上下文(用于模拟后端检索/聊天上下文)
|
|
|
|
|
+ function quoteFileToAI(item) {
|
|
|
|
|
+ const nameEl = item.querySelector('.file-name');
|
|
|
|
|
+ const fileName = nameEl ? nameEl.textContent.trim() : ('文件_' + Date.now());
|
|
|
|
|
+ const content = (fileName && fileContentsMap[fileName]) ? fileContentsMap[fileName] : (item.dataset.content || fileName);
|
|
|
|
|
+
|
|
|
|
|
+ localKnowledgeContext.addFile(fileName, content);
|
|
|
|
|
+
|
|
|
|
|
+ const aiMessages = document.getElementById('aiMessages');
|
|
|
|
|
+ const msg = `[知识背景引用] 已将 ${fileName} 的内容加入本地上下文。当前上下文文件:${localKnowledgeContext.listFiles().join(', ')}`;
|
|
|
|
|
+ if (aiMessages) {
|
|
|
|
|
+ aiMessages.innerHTML += `
|
|
|
|
|
+ <div class="msg user">
|
|
|
|
|
+ <div class="msg-avatar">张</div>
|
|
|
|
|
+ <div class="msg-bubble">${msg}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ `;
|
|
|
|
|
+ aiMessages.scrollTop = aiMessages.scrollHeight;
|
|
|
|
|
+ }
|
|
|
|
|
+ showToast('已把文件内容加入本地上下文,用于AI检索/问答(模拟)', 'success');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function deleteFile(item) {
|
|
|
|
|
+ const name = item.querySelector('.file-name') ? item.querySelector('.file-name').textContent : '文件';
|
|
|
|
|
+ if (!confirm(`确认删除 "${name}" 吗?`)) return;
|
|
|
|
|
+ item.remove();
|
|
|
|
|
+ showToast(`已删除 ${name}`, 'info');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // === 我的附件(左侧)渲染与操作 ===
|
|
|
|
|
+ // 附件示例数据(显示为样例截图中的文件名与大小,parsedAt 用于排序)
|
|
|
|
|
+ let attachmentsData = [
|
|
|
|
|
+ { name: '市场调研数据.pdf', size: '5.8 MB', parsedAt: '2026-01-27T10:00:00' },
|
|
|
|
|
+ { name: '技术方案说明.pdf', size: '3.6 MB', parsedAt: '2026-01-26T16:00:00' },
|
|
|
|
|
+ { name: '项目可行性研究报告.docx', size: '2.4 MB', parsedAt: '2026-01-25T09:00:00' },
|
|
|
|
|
+ { name: '财务预测表.xlsx', size: '1.2 MB', parsedAt: '2026-01-24T11:30:00' },
|
|
|
|
|
+ { name: '会议纪要.md', size: '48 KB', parsedAt: '2026-01-23T08:00:00' }
|
|
|
|
|
+ ];
|
|
|
|
|
+
|
|
|
|
|
+ function formatParsedAt(iso) {
|
|
|
|
|
+ const d = new Date(iso);
|
|
|
|
|
+ if (isNaN(d)) return iso;
|
|
|
|
|
+ const pad = n => n.toString().padStart(2, '0');
|
|
|
|
|
+ return `${d.getFullYear()}/${d.getMonth()+1}/${d.getDate()} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function getIconForName(name) {
|
|
|
|
|
+ const ext = (name.split('.').pop() || '').toLowerCase();
|
|
|
|
|
+ if (ext === 'xlsx') return '<span class="file-badge xls">XLS</span>';
|
|
|
|
|
+ if (ext === 'docx' || ext === 'doc') return '<span class="file-badge doc">DOC</span>';
|
|
|
|
|
+ if (ext === 'pdf') return '<span class="file-badge pdf">PDF</span>';
|
|
|
|
|
+ if (ext === 'md' || ext === 'txt') return '<span class="file-badge md">MD</span>';
|
|
|
|
|
+ return '<span class="file-badge">FILE</span>';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function renderAttachments() {
|
|
|
|
|
+ // Render into the left files top area by default (original behavior).
|
|
|
|
|
+ // Fallback to leftDocsSection top area only if files area is missing.
|
|
|
|
|
+ let container = document.querySelector('#leftFilesSection .doc-list');
|
|
|
|
|
+ if (!container) container = document.querySelector('#leftDocsSection .docs-area .doc-list');
|
|
|
|
|
+ if (!container) return;
|
|
|
|
|
+
|
|
|
|
|
+ // sort by parsedAt desc
|
|
|
|
|
+ attachmentsData.sort((a, b) => new Date(b.parsedAt) - new Date(a.parsedAt));
|
|
|
|
|
+
|
|
|
|
|
+ container.innerHTML = attachmentsData.map(att => {
|
|
|
|
|
+ return `
|
|
|
|
|
+ <div class="doc-card file-item" data-name="${att.name}" data-parsedat="${att.parsedAt}">
|
|
|
|
|
+ <div class="doc-thumb">${getIconForName(att.name)}</div>
|
|
|
|
|
+ <div class="file-actions" style="display:flex;gap:6px;">
|
|
|
|
|
+ <button class="action-btn" title="查看" onclick="showFilePreview(this.closest('.file-item'))">👁️</button>
|
|
|
|
|
+ <button class="action-btn" title="引用到AI助手" onclick="quoteFileToAI(this.closest('.file-item'))">🔗</button>
|
|
|
|
|
+ <button class="action-btn" title="删除" onclick="deleteAttachment(this.closest('.file-item'))">🗑️</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="doc-meta">
|
|
|
|
|
+ <div class="file-name">${att.name}</div>
|
|
|
|
|
+ <div class="file-time">${att.size} · <span style="color:var(--success);font-weight:600;">✓ 已完成</span></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ `;
|
|
|
|
|
+ }).join('');
|
|
|
|
|
+
|
|
|
|
|
+ // update header title/count to "全部附件"
|
|
|
|
|
+ try {
|
|
|
|
|
+ const reportsTitleEl = document.getElementById('reportsTitle');
|
|
|
|
|
+ const reportsCountEl = document.getElementById('reportsCount');
|
|
|
|
|
+ if (reportsTitleEl && reportsTitleEl.firstChild) reportsTitleEl.firstChild.nodeValue = '全部附件 ';
|
|
|
|
|
+ if (reportsCountEl) reportsCountEl.textContent = '· ' + attachmentsData.length;
|
|
|
|
|
+ } catch (e) { console.warn('update reports title err', e); }
|
|
|
|
|
+
|
|
|
|
|
+ // ensure hover/action wiring consistent
|
|
|
|
|
+ initializeFileActions();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function deleteAttachment(item) {
|
|
|
|
|
+ const name = item.querySelector('.file-name') ? item.querySelector('.file-name').textContent : '文件';
|
|
|
|
|
+ if (!confirm(`确认删除 "${name}" 吗?`)) return;
|
|
|
|
|
+ // remove from DOM
|
|
|
|
|
+ item.remove();
|
|
|
|
|
+ // remove from data model
|
|
|
|
|
+ attachmentsData = attachmentsData.filter(a => a.name !== name);
|
|
|
|
|
+ showToast(`已删除 ${name}`, 'info');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 自动在 DOM 加载后渲染附件列表
|
|
|
|
|
+ document.addEventListener('DOMContentLoaded', function() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ console.debug('[debug] DOMContentLoaded: calling renderAttachments()');
|
|
|
|
|
+ renderAttachments();
|
|
|
|
|
+ } catch (e) { console.warn('renderAttachments err', e); }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 安全绑定:确保顶部左右 tab 按钮可用并在页面刷新后渲染附件(防止浏览器缓存/执行顺序问题)
|
|
|
|
|
+ document.addEventListener('DOMContentLoaded', function() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const tabDocsTop = document.getElementById('tabDocsTop');
|
|
|
|
|
+ const tabFilesTop = document.getElementById('tabFilesTop');
|
|
|
|
|
+ if (tabDocsTop) {
|
|
|
|
|
+ try { tabDocsTop.removeAttribute('onclick'); } catch(e){}
|
|
|
|
|
+ tabDocsTop.addEventListener('click', () => { console.debug('[debug] tabDocsTop clicked'); switchLeftTab('docs'); });
|
|
|
|
|
+ }
|
|
|
|
|
+ if (tabFilesTop) {
|
|
|
|
|
+ try { tabFilesTop.removeAttribute('onclick'); } catch(e){}
|
|
|
|
|
+ tabFilesTop.addEventListener('click', () => { console.debug('[debug] tabFilesTop clicked'); switchLeftTab('files'); });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Force a re-render after a short delay to work around stale caches or race conditions
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ try { console.debug('[debug] delayed renderAttachments() executing'); if (typeof renderAttachments === 'function') renderAttachments(); } catch (e) { console.warn('delayed renderAttachments err', e); }
|
|
|
|
|
+ }, 200);
|
|
|
|
|
+ } catch (err) { console.warn('init tabs binding err', err); }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // call initializer after DOM ready
|
|
|
|
|
+ document.addEventListener('DOMContentLoaded', function() {
|
|
|
|
|
+ try { initializeFileActions(); } catch (e) { console.warn('init file actions err', e); }
|
|
|
|
|
+ try { openEditor(); } catch (e) { console.warn('openEditor init err', e); }
|
|
|
|
|
+ });
|
|
|
|
|
+ // 确保左侧资源默认不选中任何文件
|
|
|
|
|
+ document.addEventListener('DOMContentLoaded', function() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ document.querySelectorAll('.left-panel .file-item.active').forEach(el => el.classList.remove('active'));
|
|
|
|
|
+ } catch (e) { console.warn('clear active', e); }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // === 左侧资源面板 折叠/展开 控制 ===
|
|
|
|
|
+ function toggleResourcePanel(btn) {
|
|
|
|
|
+ const panel = document.querySelector('.left-panel');
|
|
|
|
|
+ if (!panel) return;
|
|
|
|
|
+ const isHidden = panel.dataset && panel.dataset.collapsed === 'true';
|
|
|
|
|
+ if (!isHidden) {
|
|
|
|
|
+ // collapse
|
|
|
|
|
+ panel.dataset.collapsed = 'true';
|
|
|
|
|
+ panel.style.display = 'none';
|
|
|
|
|
+ if (btn) btn.textContent = '❯';
|
|
|
|
|
+ // create small floating expand button
|
|
|
|
|
+ let expand = document.getElementById('resourceExpandBtn');
|
|
|
|
|
+ if (!expand) {
|
|
|
|
|
+ expand = document.createElement('button');
|
|
|
|
|
+ expand.id = 'resourceExpandBtn';
|
|
|
|
|
+ expand.className = 'resource-expand-btn';
|
|
|
|
|
+ expand.title = '展开资源面板';
|
|
|
|
|
+ expand.innerHTML = '≡';
|
|
|
|
|
+ expand.onclick = () => {
|
|
|
|
|
+ expand.remove();
|
|
|
|
|
+ panel.dataset.collapsed = 'false';
|
|
|
|
|
+ panel.style.display = '';
|
|
|
|
|
+ const pbtn = document.querySelector('.panel-toggle-btn');
|
|
|
|
|
+ if (pbtn) pbtn.textContent = '❮';
|
|
|
|
|
+ };
|
|
|
|
|
+ document.body.appendChild(expand);
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // expand
|
|
|
|
|
+ panel.dataset.collapsed = 'false';
|
|
|
|
|
+ panel.style.display = '';
|
|
|
|
|
+ if (btn) btn.textContent = '❮';
|
|
|
|
|
+ const expand = document.getElementById('resourceExpandBtn');
|
|
|
|
|
+ if (expand) expand.remove();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ </script>
|
|
|
|
|
+ <script>
|
|
|
|
|
+ // 页面级别强制:所有导航重定向到编辑器,并确保编辑器在初次打开时是可见的
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 覆盖 navTo,以防止任何后续脚本或事件跳回首页/模板等
|
|
|
|
|
+ window.navTo = function(page) {
|
|
|
|
|
+ try { if (typeof openEditor === 'function') openEditor(); } catch (e) { console.warn('navTo override err', e); }
|
|
|
|
|
+ return;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 隐藏主内容并显示编辑器(双保险,防止样式闪烁)
|
|
|
|
|
+ try {
|
|
|
|
|
+ const mc = document.getElementById('mainContent');
|
|
|
|
|
+ if (mc) mc.style.display = 'none';
|
|
|
|
|
+ const pe = document.getElementById('page-editor');
|
|
|
|
|
+ if (pe) pe.classList.add('active');
|
|
|
|
|
+ // remove active state from other pages
|
|
|
|
|
+ document.querySelectorAll('.page.active').forEach(p => { if (p.id !== 'page-editor') p.classList.remove('active'); });
|
|
|
|
|
+ } catch (e) { console.warn('editor show err', e); }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.warn('page-level override init err', e);
|
|
|
|
|
+ }
|
|
|
|
|
+ </script>
|
|
|
|
|
+ <script>
|
|
|
|
|
+ // 扫描并禁用所有内联 onclick 中的 openEditor/navTo(最终保障)
|
|
|
|
|
+ try {
|
|
|
|
|
+ document.querySelectorAll('[onclick]').forEach(el => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const v = el.getAttribute('onclick') || '';
|
|
|
|
|
+ if (/\bopenEditor\s*\(|\bnavTo\s*\(/.test(v)) {
|
|
|
|
|
+ el.setAttribute('onclick', 'void(0)');
|
|
|
|
|
+ if (el.style) el.style.cursor = 'default';
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {}
|
|
|
|
|
+ });
|
|
|
|
|
+ } catch (e) { console.warn('final disable onclicks err', e); }
|
|
|
|
|
+ </script>
|
|
|
|
|
+ <script>
|
|
|
|
|
+ // 切换原文/标记(函数)
|
|
|
|
|
+ function toggleView() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const ori = document.getElementById('contentOriginal');
|
|
|
|
|
+ const mk = document.getElementById('contentMarked');
|
|
|
|
|
+ const btn = document.getElementById('toggleViewBtn');
|
|
|
|
|
+ if (!ori || !mk || !btn) return;
|
|
|
|
|
+
|
|
|
|
|
+ // 切换视图显示
|
|
|
|
|
+ if (ori.style.display === 'none') {
|
|
|
|
|
+ ori.style.display = '';
|
|
|
|
|
+ mk.style.display = 'none';
|
|
|
|
|
+ } else {
|
|
|
|
|
+ ori.style.display = 'none';
|
|
|
|
|
+ mk.style.display = '';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 切换按钮内的 icon(在两种 icon 之间切换)
|
|
|
|
|
+ try {
|
|
|
|
|
+ const hasBig = btn.querySelector && btn.querySelector('.icon-BIG_PROMOTION');
|
|
|
|
|
+ if (hasBig) {
|
|
|
|
|
+ btn.innerHTML = '<i class="iconfont icon-BASE_INFO"></i>';
|
|
|
|
|
+ } else {
|
|
|
|
|
+ btn.innerHTML = '<i class="iconfont icon-BIG_PROMOTION"></i>';
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (iconErr) {
|
|
|
|
|
+ console.warn('toggleView icon swap err', iconErr);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) { console.warn('toggleView err', e); }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 绑定(以防内联 onclick 被覆写)
|
|
|
|
|
+ document.addEventListener('DOMContentLoaded', function () {
|
|
|
|
|
+ const tv = document.getElementById('toggleViewBtn');
|
|
|
|
|
+ if (tv) {
|
|
|
|
|
+ // remove any broken inline onclick and attach a robust listener
|
|
|
|
|
+ try { tv.removeAttribute('onclick'); } catch(e) {}
|
|
|
|
|
+ tv.addEventListener('click', toggleView);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 搜索按钮 -> 聚焦顶部搜索框
|
|
|
|
|
+ const searchBtn = document.getElementById('leftSearchBtn');
|
|
|
|
|
+ if (searchBtn) {
|
|
|
|
|
+ searchBtn.removeAttribute('onclick');
|
|
|
|
|
+ searchBtn.addEventListener('click', () => {
|
|
|
|
|
+ const topSearch = document.querySelector('.top-search') || document.querySelector('.search-input') || document.querySelector('.top-search-input');
|
|
|
|
|
+ if (topSearch) { topSearch.focus(); if (typeof topSearch.select === 'function') topSearch.select(); }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 新建报告按钮 -> 在左侧插入示例新报告并进入编辑
|
|
|
|
|
+ const newBtn = document.getElementById('newReportBtn');
|
|
|
|
|
+ if (newBtn) {
|
|
|
|
|
+ newBtn.removeAttribute('onclick');
|
|
|
|
|
+ newBtn.addEventListener('click', () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const list = document.querySelector('#leftDocsSection .doc-list');
|
|
|
|
|
+ if (!list) return;
|
|
|
|
|
+ const now = new Date();
|
|
|
|
|
+ const pad = n => n.toString().padStart(2,'0');
|
|
|
|
|
+ const fmt = `${now.getFullYear()}/${now.getMonth()+1}/${now.getDate()} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
|
|
|
|
|
+ const card = document.createElement('div');
|
|
|
|
|
+ card.className = 'doc-card file-item';
|
|
|
|
|
+ card.setAttribute('data-name', '新建报告 - 未命名');
|
|
|
|
|
+ card.innerHTML = `<div class="doc-thumb">📄</div>
|
|
|
|
|
+ <div class="doc-meta" style="width:100%;">
|
|
|
|
|
+ <div class="doc-title">新建报告 - 未命名</div>
|
|
|
|
|
+ <div style="display:flex;gap:10px;margin-top:8px;font-size:13px;color:var(--text2);">
|
|
|
|
|
+ <span>📅 ${fmt}</span><span>👤 我</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>`;
|
|
|
|
|
+ card.addEventListener('click', function(e){ e.stopPropagation(); showFilePreview(this); });
|
|
|
|
|
+ list.insertBefore(card, list.firstChild);
|
|
|
|
|
+ // update count/sort if function exists
|
|
|
|
|
+ if (typeof updateReportsCountAndSort === 'function') updateReportsCountAndSort();
|
|
|
|
|
+ // open editor if available
|
|
|
|
|
+ try { if (typeof openEditor === 'function') openEditor(); } catch(e){}
|
|
|
|
|
+ } catch (e) { console.warn('new report err', e); }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 安全绑定:给每个悬浮操作按钮(若存在)绑定实现(防止 inline 被清空)
|
|
|
|
|
+ document.querySelectorAll('.action-btn').forEach(btn => {
|
|
|
|
|
+ // if already has onclick via attribute, keep; else attach noop handlers for clarity
|
|
|
|
|
+ if (!btn.onclick) {
|
|
|
|
|
+ const t = btn.title || btn.getAttribute('title') || '';
|
|
|
|
|
+ if (t.includes('下载') || t.includes('下载')) btn.addEventListener('click', () => { downloadReport(btn); });
|
|
|
|
|
+ else if (t.includes('归档')) btn.addEventListener('click', () => { archiveReport(btn); });
|
|
|
|
|
+ else if (t.includes('删除')) btn.addEventListener('click', () => { deleteReport(btn); });
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 占位:下载/归档/删除(若页面已定义则不覆盖)
|
|
|
|
|
+ window.downloadReport = window.downloadReport || function(btn){
|
|
|
|
|
+ try{ const card = btn.closest('.doc-card'); const name = card?.getAttribute('data-name')||'报告'; alert('下载:'+name); }catch(e){console.warn(e);}
|
|
|
|
|
+ };
|
|
|
|
|
+ window.archiveReport = window.archiveReport || function(btn){
|
|
|
|
|
+ try{ const card = btn.closest('.doc-card'); if(card) card.classList.toggle('archived'); alert('已切换归档状态'); }catch(e){console.warn(e);}
|
|
|
|
|
+ };
|
|
|
|
|
+ window.deleteReport = window.deleteReport || function(btn){
|
|
|
|
|
+ try{ const card = btn.closest('.doc-card'); const name = card?.getAttribute('data-name')||'报告'; if(!confirm('确认删除 "'+name+'"?')) return; card.remove(); if(typeof updateReportsCountAndSort==='function') updateReportsCountAndSort(); alert('已删除:'+name); }catch(e){console.warn(e);}
|
|
|
|
|
+ };
|
|
|
|
|
+ </script>
|
|
|
|
|
+
|
|
|
|
|
+ <script>
|
|
|
|
|
+ (function(){
|
|
|
|
|
+ let pageSize = 10;
|
|
|
|
|
+ let elements = [], filtered = [], currentPage = 1;
|
|
|
|
|
+
|
|
|
|
|
+ function extractRawText(node) {
|
|
|
|
|
+ let txt = '';
|
|
|
|
|
+ node.childNodes.forEach(n => {
|
|
|
|
|
+ if (n.nodeType === Node.TEXT_NODE) txt += n.textContent;
|
|
|
|
|
+ else if (n.nodeType === Node.ELEMENT_NODE && !n.classList.contains('source-badge')) txt += n.textContent;
|
|
|
|
|
+ });
|
|
|
|
|
+ return (txt || node.textContent || '').trim();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function detectValueType(val) {
|
|
|
|
|
+ if (!val) return '文本';
|
|
|
|
|
+ if (/[¥¥元]|亿元|万/.test(val)) return '金额';
|
|
|
|
|
+ const v = val.replace(/[,,\s]/g,'');
|
|
|
|
|
+ if (/^\d+(\.\d+)?$/.test(v)) return '数字';
|
|
|
|
|
+ return '文本';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function buildElementsFromContent() {
|
|
|
|
|
+ elements = [];
|
|
|
|
|
+ const nodes = document.querySelectorAll('.editor-content .entity-highlight');
|
|
|
|
|
+ let idx = 0;
|
|
|
|
|
+ nodes.forEach(node => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const name = extractRawText(node);
|
|
|
|
|
+ if (!name) return;
|
|
|
|
|
+ const desc = node.getAttribute('title') || '';
|
|
|
|
|
+ const srcBadge = node.querySelector && node.querySelector('.source-badge');
|
|
|
|
|
+ const src = (node.dataset && node.dataset.source) ? node.dataset.source : (srcBadge ? srcBadge.textContent : '未知');
|
|
|
|
|
+ let elemType = '变量';
|
|
|
|
|
+ try {
|
|
|
|
|
+ const ds = (node.dataset && node.dataset.source) ? String(node.dataset.source) : '';
|
|
|
|
|
+ if (ds && /ai|生成|AI/i.test(ds)) elemType = 'AI生成';
|
|
|
|
|
+ else if (node.querySelector && node.querySelector('.source-badge.ai')) elemType = 'AI生成';
|
|
|
|
|
+ } catch(e){}
|
|
|
|
|
+ const valType = detectValueType(name);
|
|
|
|
|
+ const id = 'el_' + (++idx) + '_' + sanitizeId(name);
|
|
|
|
|
+ elements.push({ id, name, description: desc, type: valType, elementType: elemType, originalValue: name, newValue: name, source: src, node });
|
|
|
|
|
+ } catch (e) { console.warn('buildElementsFromContent err', e); }
|
|
|
|
|
+ });
|
|
|
|
|
+ filtered = elements.slice();
|
|
|
|
|
+ currentPage = 1;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function renderElementsList() {
|
|
|
|
|
+ const tbody = document.getElementById('elementsTbody');
|
|
|
|
|
+ const info = document.getElementById('elementsPaginationInfo');
|
|
|
|
|
+ if (!tbody) return;
|
|
|
|
|
+ const start = (currentPage-1)*pageSize;
|
|
|
|
|
+ const pageItems = filtered.slice(start, start+pageSize);
|
|
|
|
|
+ tbody.innerHTML = pageItems.map(it => `
|
|
|
|
|
+ <tr data-id="${it.id}">
|
|
|
|
|
+ <td>${it.name}</td>
|
|
|
|
|
+ <td>${it.description || '-'}</td>
|
|
|
|
|
+ <td>${it.type}</td>
|
|
|
|
|
+ <td>${it.elementType}</td>
|
|
|
|
|
+ <td class="orig">${it.originalValue}</td>
|
|
|
|
|
+ <td class="new"><input type="text" value="${it.newValue}" style="width:100%;padding:6px;border:1px solid var(--border);border-radius:6px;" data-id="${it.id}" /></td>
|
|
|
|
|
+ <td>${it.source}</td>
|
|
|
|
|
+ <td><button class="icon-btn edit-btn" title="编辑" data-id="${it.id}"><i class="iconfont icon-EDIT"></i></button></td>
|
|
|
|
|
+ </tr>
|
|
|
|
|
+ `).join('');
|
|
|
|
|
+ const total = filtered.length;
|
|
|
|
|
+ const totalPages = Math.max(1, Math.ceil(total/pageSize));
|
|
|
|
|
+ info.textContent = `第 ${currentPage} / ${totalPages} 页 • 共 ${total} 条`;
|
|
|
|
|
+ renderPager();
|
|
|
|
|
+ tbody.querySelectorAll('.edit-btn').forEach(btn=>{
|
|
|
|
|
+ btn.addEventListener('click', ()=> {
|
|
|
|
|
+ const id = btn.dataset.id;
|
|
|
|
|
+ const input = tbody.querySelector('input[data-id="'+id+'"]');
|
|
|
|
|
+ if (input) { input.focus(); input.select(); }
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function renderPager() {
|
|
|
|
|
+ const pagerContainer = document.getElementById('elementsPaginationInfo');
|
|
|
|
|
+ if (!pagerContainer) return;
|
|
|
|
|
+ const total = filtered.length;
|
|
|
|
|
+ const totalPages = Math.max(1, Math.ceil(total/pageSize));
|
|
|
|
|
+ // build numeric pager with up to 5 buttons (显示中间区域最多 5 个页码)
|
|
|
|
|
+ const maxButtons = 5;
|
|
|
|
|
+ let start = Math.max(1, currentPage - Math.floor(maxButtons/2));
|
|
|
|
|
+ let end = start + maxButtons - 1;
|
|
|
|
|
+ if (end > totalPages) { end = totalPages; start = Math.max(1, end - maxButtons + 1); }
|
|
|
|
|
+ let html = `<div class="pager">`;
|
|
|
|
|
+ // 恢复小型上一页 / 下一页 按钮(位于数字页码两侧)
|
|
|
|
|
+ html += `<button class="pager-btn" data-action="prev" title="上一页">‹</button>`;
|
|
|
|
|
+ for (let p = start; p <= end; p++) {
|
|
|
|
|
+ html += `<button class="pager-btn ${p===currentPage?'active':''}" data-page="${p}">${p}</button>`;
|
|
|
|
|
+ }
|
|
|
|
|
+ html += `<button class="pager-btn" data-action="next" title="下一页">›</button>`;
|
|
|
|
|
+ html += `</div>`;
|
|
|
|
|
+ pagerContainer.innerHTML = html;
|
|
|
|
|
+ // wire events
|
|
|
|
|
+ pagerContainer.querySelectorAll('.pager-btn').forEach(btn=>{
|
|
|
|
|
+ btn.addEventListener('click', (e)=>{
|
|
|
|
|
+ const action = btn.dataset.action;
|
|
|
|
|
+ if (action === 'first') { currentPage = 1; }
|
|
|
|
|
+ else if (action === 'prev') { currentPage = Math.max(1, currentPage - 1); }
|
|
|
|
|
+ else if (action === 'next') { currentPage = Math.min(totalPages, currentPage + 1); }
|
|
|
|
|
+ else if (action === 'last') { currentPage = totalPages; }
|
|
|
|
|
+ else if (btn.dataset.page) { currentPage = parseInt(btn.dataset.page, 10); }
|
|
|
|
|
+ renderElementsList();
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ // update page size select to current value
|
|
|
|
|
+ const sel = document.getElementById('elementsPageSize');
|
|
|
|
|
+ if (sel) sel.value = String(pageSize);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function openReportElementsModal() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ buildElementsFromContent();
|
|
|
|
|
+ renderElementsList();
|
|
|
|
|
+ document.getElementById('reportElementsModal').classList.add('show');
|
|
|
|
|
+ document.getElementById('reportElementsModal').setAttribute('aria-hidden','false');
|
|
|
|
|
+ } catch (e) { console.warn('openReportElementsModal err', e); }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function closeReportElementsModal() {
|
|
|
|
|
+ const el = document.getElementById('reportElementsModal');
|
|
|
|
|
+ if (el) { el.classList.remove('show'); el.setAttribute('aria-hidden','true'); }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function applySearch() {
|
|
|
|
|
+ const q = (document.getElementById('elementsSearchInput').value || '').toLowerCase().trim();
|
|
|
|
|
+ if (!q) filtered = elements.slice();
|
|
|
|
|
+ else filtered = elements.filter(it => (it.name||'').toLowerCase().includes(q) || (it.type||'').toLowerCase().includes(q) || (it.elementType||'').toLowerCase().includes(q));
|
|
|
|
|
+ currentPage = 1;
|
|
|
|
|
+ renderElementsList();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function changePage(delta) {
|
|
|
|
|
+ const total = filtered.length;
|
|
|
|
|
+ const totalPages = Math.max(1, Math.ceil(total/pageSize));
|
|
|
|
|
+ currentPage = Math.min(Math.max(1, currentPage + delta), totalPages);
|
|
|
|
|
+ renderElementsList();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function saveAllElements() {
|
|
|
|
|
+ const tbody = document.getElementById('elementsTbody');
|
|
|
|
|
+ if (!tbody) return;
|
|
|
|
|
+ tbody.querySelectorAll('input[data-id]').forEach(inp => {
|
|
|
|
|
+ const id = inp.dataset.id;
|
|
|
|
|
+ const item = elements.find(e=>e.id===id);
|
|
|
|
|
+ if (!item) return;
|
|
|
|
|
+ item.newValue = inp.value;
|
|
|
|
|
+ });
|
|
|
|
|
+ elements.forEach(item => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (item.newValue !== item.originalValue && item.node) {
|
|
|
|
|
+ const badge = item.node.querySelector && item.node.querySelector('.source-badge');
|
|
|
|
|
+ while (item.node.firstChild) item.node.removeChild(item.node.firstChild);
|
|
|
|
|
+ item.node.appendChild(document.createTextNode(item.newValue));
|
|
|
|
|
+ if (badge) item.node.appendChild(badge);
|
|
|
|
|
+ item.node.dataset.currentValue = item.newValue;
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) { console.warn('apply element change err', e); }
|
|
|
|
|
+ });
|
|
|
|
|
+ showToast('✓ 要素已保存', 'success');
|
|
|
|
|
+ closeReportElementsModal();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ document.addEventListener('DOMContentLoaded', function(){
|
|
|
|
|
+ const search = document.getElementById('elementsSearchInput');
|
|
|
|
|
+ if (search) search.addEventListener('input', applySearch);
|
|
|
|
|
+ const prev = document.getElementById('elementsPrevBtn');
|
|
|
|
|
+ const next = document.getElementById('elementsNextBtn');
|
|
|
|
|
+ if (prev) prev.addEventListener('click', ()=> changePage(-1));
|
|
|
|
|
+ if (next) next.addEventListener('click', ()=> changePage(1));
|
|
|
|
|
+ const saveBtn = document.getElementById('saveElementsBtn');
|
|
|
|
|
+ if (saveBtn) saveBtn.addEventListener('click', saveAllElements);
|
|
|
|
|
+ const openTopBtn = document.getElementById('reportElementsTopBtn');
|
|
|
|
|
+ if (openTopBtn) openTopBtn.addEventListener('click', openReportElementsModal);
|
|
|
|
|
+ const pageSizeSel = document.getElementById('elementsPageSize');
|
|
|
|
|
+ if (pageSizeSel) {
|
|
|
|
|
+ pageSizeSel.addEventListener('change', function(){
|
|
|
|
|
+ const v = parseInt(this.value, 10) || 10;
|
|
|
|
|
+ pageSize = v;
|
|
|
|
|
+ currentPage = 1;
|
|
|
|
|
+ renderElementsList();
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ window.openReportElementsModal = openReportElementsModal;
|
|
|
|
|
+ window.closeReportElementsModal = closeReportElementsModal;
|
|
|
|
|
+ })();
|
|
|
|
|
+ </script>
|
|
|
|
|
+ <script>
|
|
|
|
|
+ // report status quick toggle
|
|
|
|
|
+ (function(){
|
|
|
|
|
+ const tag = document.getElementById('reportStatusTag');
|
|
|
|
|
+ if (!tag) return;
|
|
|
|
|
+ const statuses = ['草稿','审核中','已定稿'];
|
|
|
|
|
+ let idx = 0;
|
|
|
|
|
+ tag.addEventListener('click', function(){
|
|
|
|
|
+ idx = (idx + 1) % statuses.length;
|
|
|
|
|
+ tag.textContent = statuses[idx];
|
|
|
|
|
+ showToast && showToast('状态已切换:' + statuses[idx], 'info');
|
|
|
|
|
+ });
|
|
|
|
|
+ })();
|
|
|
|
|
+ </script>
|
|
|
|
|
+ <script>
|
|
|
|
|
+ (function(){
|
|
|
|
|
+ let tooltip = null;
|
|
|
|
|
+ function createTooltip() {
|
|
|
|
|
+ tooltip = document.createElement('div');
|
|
|
|
|
+ tooltip.className = 'icon-tooltip';
|
|
|
|
|
+ document.body.appendChild(tooltip);
|
|
|
|
|
+ return tooltip;
|
|
|
|
|
+ }
|
|
|
|
|
+ function showTooltip(el, text) {
|
|
|
|
|
+ if (!tooltip) createTooltip();
|
|
|
|
|
+ tooltip.textContent = text;
|
|
|
|
|
+ tooltip.style.opacity = '1';
|
|
|
|
|
+ const rect = el.getBoundingClientRect();
|
|
|
|
|
+ const top = Math.max(8, rect.top - 36);
|
|
|
|
|
+ const left = rect.left + (rect.width/2) - (tooltip.offsetWidth/2);
|
|
|
|
|
+ tooltip.style.top = top + 'px';
|
|
|
|
|
+ tooltip.style.left = Math.max(8, left) + 'px';
|
|
|
|
|
+ }
|
|
|
|
|
+ function hideTooltip() {
|
|
|
|
|
+ if (!tooltip) return;
|
|
|
|
|
+ tooltip.style.opacity = '0';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ document.addEventListener('DOMContentLoaded', function(){
|
|
|
|
|
+ document.querySelectorAll('.icon-btn, .ai-icon-btn').forEach(btn => {
|
|
|
|
|
+ btn.addEventListener('mouseenter', function(){
|
|
|
|
|
+ const txt = btn.dataset.tooltip || btn.getAttribute('title') || btn.getAttribute('aria-label') || '';
|
|
|
|
|
+ if (txt) {
|
|
|
|
|
+ showTooltip(btn, txt);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ btn.addEventListener('mouseleave', function(){ hideTooltip(); });
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ })();
|
|
|
|
|
+ </script>
|
|
|
|
|
+ </body>
|
|
|
|
|
+<script>
|
|
|
|
|
+ // 切换原文 / 标记(同时切换按钮内 icon)
|
|
|
|
|
+ function toggleView() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const ori = document.getElementById('contentOriginal');
|
|
|
|
|
+ const mk = document.getElementById('contentMarked');
|
|
|
|
|
+ const btn = document.getElementById('toggleViewBtn');
|
|
|
|
|
+ if (!ori || !mk || !btn) return;
|
|
|
|
|
+
|
|
|
|
|
+ if (ori.style.display === 'none') {
|
|
|
|
|
+ ori.style.display = '';
|
|
|
|
|
+ mk.style.display = 'none';
|
|
|
|
|
+ } else {
|
|
|
|
|
+ ori.style.display = 'none';
|
|
|
|
|
+ mk.style.display = '';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const hasBig = btn.querySelector && btn.querySelector('.icon-BIG_PROMOTION');
|
|
|
|
|
+ if (hasBig) {
|
|
|
|
|
+ btn.innerHTML = '<i class="iconfont icon-BASE_INFO"></i>';
|
|
|
|
|
+ } else {
|
|
|
|
|
+ btn.innerHTML = '<i class="iconfont icon-BIG_PROMOTION"></i>';
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (iconErr) {
|
|
|
|
|
+ console.warn('toggleView icon swap err', iconErr);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) { console.warn('toggleView err', e); }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 更多菜单(示意)
|
|
|
|
|
+ function showMoreMenu(btn) { try { showToast && showToast('更多菜单(示意)', 'info'); } catch(e){} }
|
|
|
|
|
+
|
|
|
|
|
+ // 报告操作(占位实现)
|
|
|
|
|
+ window.downloadReport = window.downloadReport || function(btn) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const card = btn.closest('.doc-card');
|
|
|
|
|
+ const name = card?.getAttribute('data-name') || '报告';
|
|
|
|
|
+ showToast && showToast('开始下载:' + name, 'info');
|
|
|
|
|
+ } catch (e) { console.warn(e); }
|
|
|
|
|
+ };
|
|
|
|
|
+ window.archiveReport = window.archiveReport || function(btn) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const card = btn.closest('.doc-card');
|
|
|
|
|
+ if (card) card.classList.toggle('archived');
|
|
|
|
|
+ showToast && showToast('已切换归档状态', 'success');
|
|
|
|
|
+ } catch (e) { console.warn(e); }
|
|
|
|
|
+ };
|
|
|
|
|
+ window.deleteReport = window.deleteReport || function(btn) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const card = btn.closest('.doc-card');
|
|
|
|
|
+ const name = card?.getAttribute('data-name') || '报告';
|
|
|
|
|
+ if (!confirm('确认删除 "' + name + '" 吗?')) return;
|
|
|
|
|
+ card.remove();
|
|
|
|
|
+ if (typeof updateReportsCountAndSort === 'function') updateReportsCountAndSort();
|
|
|
|
|
+ showToast && showToast('已删除:' + name, 'success');
|
|
|
|
|
+ } catch (e) { console.warn(e); }
|
|
|
|
|
+ };
|
|
|
|
|
+ </script>
|
|
|
|
|
+</html>
|