Jelajahi Sumber

feat: 添加 Vue 前端 demo 和 systemd 服务配置

- frontend/vue-demo: 基于原型 HTML 实现的 Vue 3 测试 demo
  - 使用 Vue 3 + Vite + Element Plus + Pinia
  - 支持模板管理、变量编辑、生成任务等功能
  - 适配后端 extract-service API

- scripts/systemd: 后端服务的 systemd 配置文件
  - lingyue-starter.service (主应用, 端口 5232)
  - lingyue-extract.service (模板服务, 端口 8086)
  - lingyue-ner.service (NER 服务, 端口 8001)
  - install-services.sh 一键安装脚本
何文松 1 bulan lalu
induk
melakukan
26159544b4

+ 0 - 0
frontend/to前端:demo仅为后端测试


+ 167 - 0
frontend/vue-demo/README.md

@@ -0,0 +1,167 @@
+# 灵越智报 Vue Demo
+
+基于 Vue 3 + Vite + Element Plus 的前端演示项目,用于测试模板系统 API。
+
+## 技术栈
+
+- **Vue 3** - 渐进式 JavaScript 框架
+- **Vite** - 下一代前端构建工具
+- **Element Plus** - Vue 3 UI 组件库
+- **Pinia** - Vue 状态管理
+- **Vue Router** - 路由管理
+- **Axios** - HTTP 客户端
+- **Sass** - CSS 预处理器
+
+## 功能模块
+
+### 1. 首页 (Home)
+- 统计概览(报告数、模板数、文档数)
+- AI 对话入口
+- 推荐模板展示
+- 快捷操作入口
+
+### 2. 模板管理 (Templates)
+- 模板列表展示
+- 模板搜索和筛选
+- 创建新模板
+- 模板发布、归档、复制、删除
+
+### 3. 编辑器 (Editor)
+- 文档内容编辑
+- **变量标记**:选中文本后右键标记为变量
+- 来源文件管理
+- 变量管理面板
+- 按类别分组显示变量
+- 知识图谱弹窗(开发中)
+
+### 4. 生成记录 (Generations)
+- 生成任务列表
+- 创建新生成任务
+- 执行变量提取
+- 确认并生成文档
+- 下载生成的文档
+
+## 快速开始
+
+### 1. 安装依赖
+
+```bash
+cd frontend/vue-demo
+npm install
+```
+
+### 2. 启动开发服务器
+
+```bash
+npm run dev
+```
+
+访问 http://localhost:5173
+
+### 3. 连接后端 API
+
+Vite 开发服务器已配置代理,将 `/api` 请求转发到 `http://localhost:8086`(extract-service)。
+
+确保后端服务已启动:
+
+```bash
+cd backend/lingyue-starter
+mvn spring-boot:run
+```
+
+## 项目结构
+
+```
+vue-demo/
+├── index.html           # 入口 HTML
+├── package.json         # 项目配置
+├── vite.config.js       # Vite 配置
+├── src/
+│   ├── main.js          # 应用入口
+│   ├── App.vue          # 根组件
+│   ├── api/
+│   │   └── index.js     # API 封装
+│   ├── assets/
+│   │   └── main.scss    # 全局样式
+│   ├── components/      # 公共组件
+│   ├── router/
+│   │   └── index.js     # 路由配置
+│   ├── stores/
+│   │   └── template.js  # 模板状态管理
+│   └── views/
+│       ├── Home.vue           # 首页
+│       ├── Templates.vue      # 模板列表
+│       ├── TemplateDetail.vue # 模板详情
+│       ├── Editor.vue         # 编辑器
+│       ├── Generations.vue    # 生成记录列表
+│       └── GenerationDetail.vue # 生成任务详情
+```
+
+## API 接口
+
+### 模板 API
+| 接口 | 方法 | 说明 |
+|------|------|------|
+| `/api/v1/templates` | GET | 获取模板列表 |
+| `/api/v1/templates/{id}` | GET | 获取模板详情 |
+| `/api/v1/templates` | POST | 创建模板 |
+| `/api/v1/templates/{id}` | PUT | 更新模板 |
+| `/api/v1/templates/{id}` | DELETE | 删除模板 |
+| `/api/v1/templates/{id}/publish` | POST | 发布模板 |
+| `/api/v1/templates/{id}/variables` | GET | 获取变量列表 |
+| `/api/v1/templates/{id}/variables/grouped` | GET | 按类别分组获取变量 |
+
+### 生成任务 API
+| 接口 | 方法 | 说明 |
+|------|------|------|
+| `/api/v1/generations` | GET | 获取任务列表 |
+| `/api/v1/generations/{id}` | GET | 获取任务详情 |
+| `/api/v1/generations` | POST | 创建任务 |
+| `/api/v1/generations/{id}/execute` | POST | 执行提取 |
+| `/api/v1/generations/{id}/confirm` | POST | 确认生成 |
+| `/api/v1/generations/{id}/download` | GET | 下载文档 |
+
+## 变量类别
+
+| 类别 | 标识 | 颜色 | 说明 |
+|------|------|------|------|
+| 核心实体 | entity | 蓝色 #1890ff | 项目名称、公司名等 |
+| 概念/技术 | concept | 紫色 #722ed1 | 技术方案、概念等 |
+| 数据/指标 | data | 绿色 #52c41a | 金额、数量、比例等 |
+| 地点/组织 | location | 橙色 #faad14 | 地点、部门等 |
+| 资源模板 | asset | 粉色 #eb2f96 | 图表、模板等 |
+
+## 开发说明
+
+### Mock 数据
+
+当前版本使用 Mock 数据进行演示,实际使用时需要:
+
+1. 启动后端服务
+2. 移除组件中的 Mock 数据
+3. 使用 API 获取真实数据
+
+### 自定义主题
+
+修改 `src/assets/main.scss` 中的 CSS 变量:
+
+```scss
+:root {
+  --primary: #1890ff;
+  --primary-dark: #096dd9;
+  --primary-light: #e6f7ff;
+  // ...
+}
+```
+
+## 构建部署
+
+```bash
+# 构建生产版本
+npm run build
+
+# 预览构建结果
+npm run preview
+```
+
+构建产物在 `dist/` 目录,可部署到 Nginx 等静态服务器。

+ 13 - 0
frontend/vue-demo/index.html

@@ -0,0 +1,13 @@
+<!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">
+</head>
+<body>
+  <div id="app"></div>
+  <script type="module" src="/src/main.js"></script>
+</body>
+</html>

+ 24 - 0
frontend/vue-demo/package.json

@@ -0,0 +1,24 @@
+{
+  "name": "lingyue-zhibao-demo",
+  "version": "0.1.0",
+  "private": true,
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "vite build",
+    "preview": "vite preview"
+  },
+  "dependencies": {
+    "vue": "^3.4.0",
+    "vue-router": "^4.2.5",
+    "pinia": "^2.1.7",
+    "axios": "^1.6.0",
+    "element-plus": "^2.4.4",
+    "@element-plus/icons-vue": "^2.3.1"
+  },
+  "devDependencies": {
+    "@vitejs/plugin-vue": "^4.5.2",
+    "vite": "^5.0.10",
+    "sass": "^1.69.5"
+  }
+}

+ 204 - 0
frontend/vue-demo/src/App.vue

@@ -0,0 +1,204 @@
+<template>
+  <el-config-provider :locale="zhCn">
+    <div class="app-container">
+      <!-- 顶部导航 -->
+      <header class="app-header">
+        <div class="header-left">
+          <div class="logo" @click="router.push('/')">
+            <div class="logo-icon">📊</div>
+            <span>灵越智报</span>
+          </div>
+          <el-input
+            v-model="searchKeyword"
+            placeholder="搜索报告、模板..."
+            prefix-icon="Search"
+            class="search-input"
+            clearable
+          />
+        </div>
+        <div class="header-right">
+          <el-badge :value="3" class="notification-badge">
+            <el-button :icon="Bell" circle />
+          </el-badge>
+          <el-dropdown trigger="click">
+            <div class="user-menu">
+              <el-avatar :size="32" class="user-avatar">张</el-avatar>
+              <span class="user-name">张三</span>
+            </div>
+            <template #dropdown>
+              <el-dropdown-menu>
+                <el-dropdown-item>个人中心</el-dropdown-item>
+                <el-dropdown-item>系统设置</el-dropdown-item>
+                <el-dropdown-item divided>退出登录</el-dropdown-item>
+              </el-dropdown-menu>
+            </template>
+          </el-dropdown>
+        </div>
+      </header>
+
+      <!-- 主体区域 -->
+      <div class="app-body">
+        <!-- 侧边栏 -->
+        <aside class="app-sidebar" v-if="!isEditorPage">
+          <el-menu
+            :default-active="currentRoute"
+            router
+            class="sidebar-menu"
+          >
+            <el-menu-item index="/">
+              <el-icon><HomeFilled /></el-icon>
+              <span>首页</span>
+            </el-menu-item>
+            <el-menu-item index="/templates">
+              <el-icon><Files /></el-icon>
+              <span>模板管理</span>
+            </el-menu-item>
+            <el-menu-item index="/generations">
+              <el-icon><Document /></el-icon>
+              <span>生成记录</span>
+            </el-menu-item>
+            <el-divider />
+            <el-menu-item index="/help">
+              <el-icon><QuestionFilled /></el-icon>
+              <span>帮助中心</span>
+            </el-menu-item>
+          </el-menu>
+        </aside>
+
+        <!-- 内容区 -->
+        <main class="app-main" :class="{ 'full-width': isEditorPage }">
+          <router-view />
+        </main>
+      </div>
+    </div>
+  </el-config-provider>
+</template>
+
+<script setup>
+import { ref, computed } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
+import { Bell, HomeFilled, Files, Document, QuestionFilled } from '@element-plus/icons-vue'
+import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
+
+const router = useRouter()
+const route = useRoute()
+
+const searchKeyword = ref('')
+
+const currentRoute = computed(() => route.path)
+const isEditorPage = computed(() => route.path.startsWith('/editor'))
+</script>
+
+<style lang="scss">
+.app-container {
+  height: 100vh;
+  display: flex;
+  flex-direction: column;
+}
+
+.app-header {
+  height: 56px;
+  background: #fff;
+  border-bottom: 1px solid #e8e8e8;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 0 20px;
+  flex-shrink: 0;
+}
+
+.header-left {
+  display: flex;
+  align-items: center;
+  gap: 20px;
+}
+
+.logo {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  font-size: 17px;
+  font-weight: 600;
+  color: #1890ff;
+  cursor: pointer;
+
+  .logo-icon {
+    width: 32px;
+    height: 32px;
+    background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
+    border-radius: 8px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    color: white;
+    font-size: 18px;
+  }
+}
+
+.search-input {
+  width: 320px;
+}
+
+.header-right {
+  display: flex;
+  align-items: center;
+  gap: 16px;
+}
+
+.notification-badge {
+  .el-button {
+    font-size: 18px;
+  }
+}
+
+.user-menu {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  cursor: pointer;
+  padding: 4px 8px;
+  border-radius: 20px;
+  
+  &:hover {
+    background: #f5f7fa;
+  }
+}
+
+.user-avatar {
+  background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
+}
+
+.user-name {
+  font-size: 14px;
+  font-weight: 500;
+}
+
+.app-body {
+  flex: 1;
+  display: flex;
+  overflow: hidden;
+}
+
+.app-sidebar {
+  width: 200px;
+  background: #fff;
+  border-right: 1px solid #e8e8e8;
+  flex-shrink: 0;
+
+  .sidebar-menu {
+    border-right: none;
+    height: 100%;
+  }
+}
+
+.app-main {
+  flex: 1;
+  overflow-y: auto;
+  background: #f5f7fa;
+  padding: 20px;
+
+  &.full-width {
+    padding: 0;
+  }
+}
+</style>

+ 205 - 0
frontend/vue-demo/src/api/index.js

@@ -0,0 +1,205 @@
+import axios from 'axios'
+
+const api = axios.create({
+  baseURL: '/api/v1',
+  timeout: 30000,
+  headers: {
+    'Content-Type': 'application/json'
+  }
+})
+
+// 请求拦截器
+api.interceptors.request.use(
+  config => {
+    // 可以在这里添加 token
+    // const token = localStorage.getItem('token')
+    // if (token) {
+    //   config.headers.Authorization = `Bearer ${token}`
+    // }
+    return config
+  },
+  error => Promise.reject(error)
+)
+
+// 响应拦截器
+api.interceptors.response.use(
+  response => {
+    const { data } = response
+    if (data.code === 200) {
+      return data.data
+    }
+    return Promise.reject(new Error(data.msg || '请求失败'))
+  },
+  error => {
+    console.error('API Error:', error)
+    return Promise.reject(error)
+  }
+)
+
+// ==================== 模板 API ====================
+
+export const templateApi = {
+  // 获取模板列表
+  list(page = 1, size = 20) {
+    return api.get('/templates', { params: { page, size } })
+  },
+
+  // 搜索模板
+  search(keyword) {
+    return api.get('/templates/search', { params: { keyword } })
+  },
+
+  // 获取可访问的模板
+  listAccessible() {
+    return api.get('/templates/accessible')
+  },
+
+  // 获取模板详情
+  getById(id) {
+    return api.get(`/templates/${id}`)
+  },
+
+  // 创建模板
+  create(data) {
+    return api.post('/templates', data)
+  },
+
+  // 更新模板
+  update(id, data) {
+    return api.put(`/templates/${id}`, data)
+  },
+
+  // 删除模板
+  delete(id) {
+    return api.delete(`/templates/${id}`)
+  },
+
+  // 发布模板
+  publish(id) {
+    return api.post(`/templates/${id}/publish`)
+  },
+
+  // 归档模板
+  archive(id) {
+    return api.post(`/templates/${id}/archive`)
+  },
+
+  // 复制模板
+  duplicate(id, newName) {
+    return api.post(`/templates/${id}/duplicate`, null, { params: { newName } })
+  }
+}
+
+// ==================== 来源文件 API ====================
+
+export const sourceFileApi = {
+  // 获取模板的来源文件列表
+  list(templateId) {
+    return api.get(`/templates/${templateId}/source-files`)
+  },
+
+  // 添加来源文件定义
+  add(templateId, data) {
+    return api.post(`/templates/${templateId}/source-files`, data)
+  },
+
+  // 更新来源文件定义
+  update(id, data) {
+    return api.put(`/templates/source-files/${id}`, data)
+  },
+
+  // 删除来源文件定义
+  delete(id, force = false) {
+    return api.delete(`/templates/source-files/${id}`, { params: { force } })
+  }
+}
+
+// ==================== 变量 API ====================
+
+export const variableApi = {
+  // 获取模板的变量列表
+  list(templateId, params = {}) {
+    return api.get(`/templates/${templateId}/variables`, { params })
+  },
+
+  // 获取变量按类别分组
+  listGrouped(templateId) {
+    return api.get(`/templates/${templateId}/variables/grouped`)
+  },
+
+  // 添加变量
+  add(templateId, data) {
+    return api.post(`/templates/${templateId}/variables`, data)
+  },
+
+  // 获取变量详情
+  getById(id) {
+    return api.get(`/templates/variables/${id}`)
+  },
+
+  // 更新变量
+  update(id, data) {
+    return api.put(`/templates/variables/${id}`, data)
+  },
+
+  // 删除变量
+  delete(id, force = false) {
+    return api.delete(`/templates/variables/${id}`, { params: { force } })
+  },
+
+  // 预览提取结果
+  preview(id, documentId) {
+    return api.post(`/templates/variables/${id}/preview`, null, { params: { documentId } })
+  },
+
+  // 重排序变量
+  reorder(templateId, orderedIds) {
+    return api.post(`/templates/${templateId}/variables/reorder`, { orderedIds })
+  }
+}
+
+// ==================== 生成任务 API ====================
+
+export const generationApi = {
+  // 获取生成任务列表
+  list(params = {}) {
+    return api.get('/generations', { params })
+  },
+
+  // 获取生成任务详情
+  getById(id) {
+    return api.get(`/generations/${id}`)
+  },
+
+  // 创建生成任务
+  create(data) {
+    return api.post('/generations', data)
+  },
+
+  // 执行变量提取
+  execute(id) {
+    return api.post(`/generations/${id}/execute`)
+  },
+
+  // 获取执行进度
+  getProgress(id) {
+    return api.get(`/generations/${id}/progress`)
+  },
+
+  // 修改变量值
+  updateVariableValue(id, variableName, data) {
+    return api.put(`/generations/${id}/variables/${variableName}`, data)
+  },
+
+  // 确认并生成文档
+  confirm(id) {
+    return api.post(`/generations/${id}/confirm`)
+  },
+
+  // 下载生成文档
+  getDownloadUrl(id) {
+    return `/api/v1/generations/${id}/download`
+  }
+}
+
+export default api

+ 243 - 0
frontend/vue-demo/src/assets/main.scss

@@ -0,0 +1,243 @@
+// 全局变量
+:root {
+  --primary: #1890ff;
+  --primary-dark: #096dd9;
+  --primary-light: #e6f7ff;
+  --success: #52c41a;
+  --warning: #faad14;
+  --danger: #ff4d4f;
+  --text-1: #262626;
+  --text-2: #595959;
+  --text-3: #8c8c8c;
+  --border: #e8e8e8;
+  --bg: #f5f7fa;
+}
+
+* {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+}
+
+body {
+  font-family: 'Noto Sans SC', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+  font-size: 14px;
+  line-height: 1.6;
+  color: var(--text-1);
+  background: var(--bg);
+}
+
+// 实体高亮样式
+.entity-highlight {
+  display: inline;
+  padding: 2px 8px;
+  border-radius: 4px;
+  cursor: pointer;
+  transition: all 0.2s;
+  font-weight: 500;
+
+  &.entity {
+    border: 1px solid var(--primary);
+    color: var(--primary);
+    background: rgba(24, 144, 255, 0.1);
+
+    &:hover {
+      background: var(--primary);
+      color: white;
+    }
+  }
+
+  &.concept {
+    border: 1px solid #722ed1;
+    color: #722ed1;
+    background: rgba(114, 46, 209, 0.1);
+
+    &:hover {
+      background: #722ed1;
+      color: white;
+    }
+  }
+
+  &.data {
+    border: 1px solid #52c41a;
+    color: #52c41a;
+    background: rgba(82, 196, 26, 0.1);
+
+    &:hover {
+      background: #52c41a;
+      color: white;
+    }
+  }
+
+  &.location {
+    border: 1px solid #faad14;
+    color: #d48806;
+    background: rgba(250, 173, 20, 0.1);
+
+    &:hover {
+      background: #faad14;
+      color: white;
+    }
+  }
+
+  &.asset {
+    border: 1px solid #eb2f96;
+    color: #eb2f96;
+    background: rgba(235, 47, 150, 0.1);
+
+    &:hover {
+      background: #eb2f96;
+      color: white;
+    }
+  }
+}
+
+// 卡片样式
+.card {
+  background: #fff;
+  border-radius: 10px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
+}
+
+// 页面标题
+.page-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 20px;
+
+  h1 {
+    font-size: 20px;
+    font-weight: 600;
+  }
+}
+
+// 统计卡片
+.stat-card {
+  padding: 18px;
+  cursor: pointer;
+  transition: all 0.3s;
+
+  &: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;
+
+    &.blue { background: linear-gradient(135deg, #e6f7ff, #bae7ff); }
+    &.purple { background: linear-gradient(135deg, #f9f0ff, #efdbff); }
+    &.green { background: linear-gradient(135deg, #f6ffed, #d9f7be); }
+    &.orange { background: linear-gradient(135deg, #fff7e6, #ffe7ba); }
+  }
+
+  .stat-value {
+    font-size: 26px;
+    font-weight: 700;
+  }
+
+  .stat-label {
+    font-size: 13px;
+    color: var(--text-2);
+    margin-bottom: 6px;
+  }
+
+  .stat-trend {
+    font-size: 12px;
+
+    &.up { color: var(--success); }
+    &.down { color: var(--danger); }
+  }
+}
+
+// 模板卡片
+.tpl-card {
+  overflow: hidden;
+  cursor: pointer;
+  transition: all 0.3s;
+
+  &: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(--text-3);
+    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;
+
+    &.hot { background: #fff1f0; color: var(--danger); }
+  }
+}
+
+// 变量标签样式
+.var-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: pointer;
+  transition: all 0.2s;
+  user-select: none;
+
+  &:hover {
+    border-color: var(--primary);
+    background: var(--primary-light);
+    transform: translateY(-1px);
+  }
+
+  &.entity { border-left: 3px solid var(--primary); }
+  &.concept { border-left: 3px solid #722ed1; }
+  &.data { border-left: 3px solid var(--success); }
+  &.location { border-left: 3px solid var(--warning); }
+  &.asset { border-left: 3px solid #eb2f96; }
+
+  .tag-icon { font-size: 12px; }
+  .tag-name { font-weight: 500; }
+}

+ 22 - 0
frontend/vue-demo/src/main.js

@@ -0,0 +1,22 @@
+import { createApp } from 'vue'
+import { createPinia } from 'pinia'
+import ElementPlus from 'element-plus'
+import 'element-plus/dist/index.css'
+import * as ElementPlusIconsVue from '@element-plus/icons-vue'
+
+import App from './App.vue'
+import router from './router'
+import './assets/main.scss'
+
+const app = createApp(App)
+
+// 注册所有 Element Plus 图标
+for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
+  app.component(key, component)
+}
+
+app.use(createPinia())
+app.use(router)
+app.use(ElementPlus)
+
+app.mount('#app')

+ 41 - 0
frontend/vue-demo/src/router/index.js

@@ -0,0 +1,41 @@
+import { createRouter, createWebHistory } from 'vue-router'
+
+const routes = [
+  {
+    path: '/',
+    name: 'Home',
+    component: () => import('@/views/Home.vue')
+  },
+  {
+    path: '/templates',
+    name: 'Templates',
+    component: () => import('@/views/Templates.vue')
+  },
+  {
+    path: '/templates/:id',
+    name: 'TemplateDetail',
+    component: () => import('@/views/TemplateDetail.vue')
+  },
+  {
+    path: '/editor/:templateId',
+    name: 'Editor',
+    component: () => import('@/views/Editor.vue')
+  },
+  {
+    path: '/generations',
+    name: 'Generations',
+    component: () => import('@/views/Generations.vue')
+  },
+  {
+    path: '/generations/:id',
+    name: 'GenerationDetail',
+    component: () => import('@/views/GenerationDetail.vue')
+  }
+]
+
+const router = createRouter({
+  history: createWebHistory(),
+  routes
+})
+
+export default router

+ 170 - 0
frontend/vue-demo/src/stores/template.js

@@ -0,0 +1,170 @@
+import { defineStore } from 'pinia'
+import { ref, computed } from 'vue'
+import { templateApi, sourceFileApi, variableApi } from '@/api'
+
+export const useTemplateStore = defineStore('template', () => {
+  // 状态
+  const templates = ref([])
+  const currentTemplate = ref(null)
+  const sourceFiles = ref([])
+  const variables = ref([])
+  const groupedVariables = ref({})
+  const loading = ref(false)
+
+  // 类别配置
+  const categoryConfig = {
+    entity: { label: '核心实体', color: '#1890ff', icon: '🏢' },
+    concept: { label: '概念/技术', color: '#722ed1', icon: '💡' },
+    data: { label: '数据/指标', color: '#52c41a', icon: '📊' },
+    location: { label: '地点/组织', color: '#faad14', icon: '📍' },
+    asset: { label: '资源模板', color: '#eb2f96', icon: '📑' },
+    other: { label: '其他', color: '#8c8c8c', icon: '📌' }
+  }
+
+  // 计算属性
+  const variableCount = computed(() => variables.value.length)
+  const sourceFileCount = computed(() => sourceFiles.value.length)
+
+  // 方法
+  async function fetchTemplates(page = 1, size = 20) {
+    loading.value = true
+    try {
+      const data = await templateApi.list(page, size)
+      templates.value = data.records || data
+      return data
+    } finally {
+      loading.value = false
+    }
+  }
+
+  async function fetchTemplateDetail(id) {
+    loading.value = true
+    try {
+      currentTemplate.value = await templateApi.getById(id)
+      // 同时获取来源文件和变量
+      await Promise.all([
+        fetchSourceFiles(id),
+        fetchVariables(id)
+      ])
+      return currentTemplate.value
+    } finally {
+      loading.value = false
+    }
+  }
+
+  async function fetchSourceFiles(templateId) {
+    sourceFiles.value = await sourceFileApi.list(templateId)
+    return sourceFiles.value
+  }
+
+  async function fetchVariables(templateId) {
+    variables.value = await variableApi.list(templateId)
+    return variables.value
+  }
+
+  async function fetchGroupedVariables(templateId) {
+    groupedVariables.value = await variableApi.listGrouped(templateId)
+    return groupedVariables.value
+  }
+
+  async function createTemplate(data) {
+    const template = await templateApi.create(data)
+    templates.value.unshift(template)
+    return template
+  }
+
+  async function updateTemplate(id, data) {
+    const template = await templateApi.update(id, data)
+    if (currentTemplate.value?.id === id) {
+      currentTemplate.value = { ...currentTemplate.value, ...template }
+    }
+    return template
+  }
+
+  async function deleteTemplate(id) {
+    await templateApi.delete(id)
+    templates.value = templates.value.filter(t => t.id !== id)
+    if (currentTemplate.value?.id === id) {
+      currentTemplate.value = null
+    }
+  }
+
+  async function addVariable(templateId, data) {
+    const variable = await variableApi.add(templateId, data)
+    variables.value.push(variable)
+    return variable
+  }
+
+  async function updateVariable(id, data) {
+    const variable = await variableApi.update(id, data)
+    const index = variables.value.findIndex(v => v.id === id)
+    if (index !== -1) {
+      variables.value[index] = variable
+    }
+    return variable
+  }
+
+  async function deleteVariable(id) {
+    await variableApi.delete(id)
+    variables.value = variables.value.filter(v => v.id !== id)
+  }
+
+  async function addSourceFile(templateId, data) {
+    const sourceFile = await sourceFileApi.add(templateId, data)
+    sourceFiles.value.push(sourceFile)
+    return sourceFile
+  }
+
+  async function updateSourceFile(id, data) {
+    const sourceFile = await sourceFileApi.update(id, data)
+    const index = sourceFiles.value.findIndex(s => s.id === id)
+    if (index !== -1) {
+      sourceFiles.value[index] = sourceFile
+    }
+    return sourceFile
+  }
+
+  async function deleteSourceFile(id) {
+    await sourceFileApi.delete(id)
+    sourceFiles.value = sourceFiles.value.filter(s => s.id !== id)
+  }
+
+  function resetState() {
+    currentTemplate.value = null
+    sourceFiles.value = []
+    variables.value = []
+    groupedVariables.value = {}
+  }
+
+  return {
+    // 状态
+    templates,
+    currentTemplate,
+    sourceFiles,
+    variables,
+    groupedVariables,
+    loading,
+    categoryConfig,
+
+    // 计算属性
+    variableCount,
+    sourceFileCount,
+
+    // 方法
+    fetchTemplates,
+    fetchTemplateDetail,
+    fetchSourceFiles,
+    fetchVariables,
+    fetchGroupedVariables,
+    createTemplate,
+    updateTemplate,
+    deleteTemplate,
+    addVariable,
+    updateVariable,
+    deleteVariable,
+    addSourceFile,
+    updateSourceFile,
+    deleteSourceFile,
+    resetState
+  }
+})

+ 981 - 0
frontend/vue-demo/src/views/Editor.vue

@@ -0,0 +1,981 @@
+<template>
+  <div class="editor-page">
+    <!-- 工具栏 -->
+    <div class="editor-toolbar">
+      <el-button :icon="ArrowLeft" @click="goBack">返回</el-button>
+      <el-input
+        v-model="reportTitle"
+        class="title-input"
+        placeholder="请输入报告标题"
+      />
+      <span class="save-status" v-if="saved">✓ 已保存</span>
+      <div class="toolbar-right">
+        <el-button :icon="Clock">版本</el-button>
+        <el-button :icon="Share">分享</el-button>
+        <el-divider direction="vertical" />
+        <el-button type="primary" :icon="Check" @click="handleSave">保存</el-button>
+      </div>
+    </div>
+
+    <!-- 主体 -->
+    <div class="editor-body">
+      <!-- 左侧文件面板 -->
+      <div class="left-panel">
+        <div class="panel-header">
+          <span>📁 来源文件</span>
+          <span class="file-count">{{ sourceFiles.length }}个</span>
+        </div>
+        <div class="panel-body">
+          <!-- 上传区 -->
+          <el-upload
+            class="upload-zone"
+            drag
+            action="/api/v1/parse/upload"
+            :on-success="handleFileUpload"
+            :show-file-list="false"
+          >
+            <div class="upload-content">
+              <div class="upload-icon">📄</div>
+              <div class="upload-text">拖拽或点击上传</div>
+              <div class="upload-hint">支持 PDF / Word / Excel</div>
+            </div>
+          </el-upload>
+
+          <!-- 来源文件列表 -->
+          <div class="file-list">
+            <div
+              v-for="file in sourceFiles"
+              :key="file.id"
+              class="file-item"
+              :class="{ active: selectedFile?.id === file.id }"
+              @click="selectFile(file)"
+            >
+              <span class="file-icon">{{ getFileIcon(file) }}</span>
+              <div class="file-info">
+                <div class="file-name">{{ file.alias }}</div>
+                <div class="file-meta">
+                  <span v-if="file.required" class="required">必需</span>
+                  <span v-else>可选</span>
+                </div>
+              </div>
+              <el-button
+                size="small"
+                :icon="Delete"
+                circle
+                @click.stop="removeSourceFile(file)"
+              />
+            </div>
+          </div>
+
+          <!-- 添加来源文件定义 -->
+          <el-button
+            class="add-source-btn"
+            :icon="Plus"
+            @click="showAddSourceDialog = true"
+          >
+            添加来源文件定义
+          </el-button>
+        </div>
+      </div>
+
+      <!-- 中间编辑区 -->
+      <div class="center-panel">
+        <div class="editor-title-bar">
+          <h2>{{ reportTitle }}</h2>
+          <div class="view-toggle">
+            <el-radio-group v-model="viewMode" size="small">
+              <el-radio-button label="edit">📝 编辑</el-radio-button>
+              <el-radio-button label="preview">👁 预览</el-radio-button>
+            </el-radio-group>
+          </div>
+          <el-button :icon="Share" circle @click="showGraphModal = true" />
+        </div>
+
+        <div class="editor-scroll" ref="editorRef">
+          <div
+            class="editor-content"
+            contenteditable="true"
+            @mouseup="handleTextSelection"
+            v-html="documentContent"
+          />
+        </div>
+      </div>
+
+      <!-- 右侧变量面板 -->
+      <div class="right-panel">
+        <!-- 变量管理 -->
+        <div class="element-section">
+          <div class="element-header">
+            <span class="element-title">
+              🏷️ 变量管理
+              <span class="element-count">({{ variables.length }})</span>
+            </span>
+            <el-button size="small" :icon="Plus" @click="showAddVariableDialog = true">
+              添加
+            </el-button>
+          </div>
+          <div class="element-body">
+            <div class="element-tags-wrap">
+              <div
+                v-for="variable in variables"
+                :key="variable.id"
+                class="var-tag"
+                :class="variable.category"
+                @click="editVariable(variable)"
+              >
+                <span class="tag-icon">{{ getCategoryIcon(variable.category) }}</span>
+                <span class="tag-name">{{ variable.displayName }}</span>
+              </div>
+            </div>
+            <div class="element-hint" v-if="variables.length === 0">
+              选中文本后右键标记为变量
+            </div>
+          </div>
+        </div>
+
+        <!-- 按类别分组显示 -->
+        <div class="category-section" v-for="(vars, category) in groupedVariables" :key="category">
+          <div class="category-header">
+            <span
+              class="category-dot"
+              :style="{ background: getCategoryColor(category) }"
+            />
+            <span>{{ getCategoryLabel(category) }}</span>
+            <span class="category-count">{{ vars.length }}</span>
+          </div>
+          <div class="category-items">
+            <div
+              v-for="v in vars"
+              :key="v.id"
+              class="category-item"
+              @click="editVariable(v)"
+            >
+              <span>{{ v.displayName }}</span>
+              <span class="item-value">{{ v.exampleValue || '-' }}</span>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 右键菜单 -->
+    <div
+      v-show="contextMenuVisible"
+      class="context-menu"
+      :style="{ left: contextMenuPos.x + 'px', top: contextMenuPos.y + 'px' }"
+    >
+      <div class="context-menu-item" @click="markAsVariable('entity')">
+        <span class="icon">🏢</span>
+        <span>标记为核心实体</span>
+      </div>
+      <div class="context-menu-item" @click="markAsVariable('concept')">
+        <span class="icon">💡</span>
+        <span>标记为概念/技术</span>
+      </div>
+      <div class="context-menu-item" @click="markAsVariable('data')">
+        <span class="icon">📊</span>
+        <span>标记为数据/指标</span>
+      </div>
+      <div class="context-menu-item" @click="markAsVariable('location')">
+        <span class="icon">📍</span>
+        <span>标记为地点/组织</span>
+      </div>
+      <div class="context-menu-item" @click="markAsVariable('asset')">
+        <span class="icon">📑</span>
+        <span>标记为资源模板</span>
+      </div>
+    </div>
+
+    <!-- 添加来源文件对话框 -->
+    <el-dialog v-model="showAddSourceDialog" title="添加来源文件定义" width="400">
+      <el-form :model="newSourceFile" label-width="80px">
+        <el-form-item label="文件别名" required>
+          <el-input v-model="newSourceFile.alias" placeholder="如:可研批复" />
+        </el-form-item>
+        <el-form-item label="描述">
+          <el-input v-model="newSourceFile.description" placeholder="文件描述" />
+        </el-form-item>
+        <el-form-item label="是否必需">
+          <el-switch v-model="newSourceFile.required" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="showAddSourceDialog = false">取消</el-button>
+        <el-button type="primary" @click="addSourceFile">添加</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 添加/编辑变量对话框 -->
+    <el-dialog v-model="showVariableDialog" :title="editingVariable ? '编辑变量' : '添加变量'" width="500">
+      <el-form :model="variableForm" label-width="100px">
+        <el-form-item label="变量名" required>
+          <el-input v-model="variableForm.name" placeholder="程序用名,如 project_name" />
+        </el-form-item>
+        <el-form-item label="显示名称" required>
+          <el-input v-model="variableForm.displayName" placeholder="用户可见名称" />
+        </el-form-item>
+        <el-form-item label="类别">
+          <el-select v-model="variableForm.category" style="width: 100%">
+            <el-option label="核心实体" value="entity" />
+            <el-option label="概念/技术" value="concept" />
+            <el-option label="数据/指标" value="data" />
+            <el-option label="地点/组织" value="location" />
+            <el-option label="资源模板" value="asset" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="示例值">
+          <el-input v-model="variableForm.exampleValue" placeholder="文档中的原始值" />
+        </el-form-item>
+        <el-form-item label="来源类型">
+          <el-select v-model="variableForm.sourceType" style="width: 100%">
+            <el-option label="从来源文件提取" value="document" />
+            <el-option label="手动输入" value="manual" />
+            <el-option label="引用其他变量" value="reference" />
+            <el-option label="固定值" value="fixed" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="来源文件" v-if="variableForm.sourceType === 'document'">
+          <el-select v-model="variableForm.sourceFileAlias" style="width: 100%">
+            <el-option
+              v-for="sf in sourceFiles"
+              :key="sf.id"
+              :label="sf.alias"
+              :value="sf.alias"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="提取方式" v-if="variableForm.sourceType === 'document'">
+          <el-select v-model="variableForm.extractType" style="width: 100%">
+            <el-option label="直接提取" value="direct" />
+            <el-option label="AI 字段提取" value="ai_extract" />
+            <el-option label="AI 总结" value="ai_summarize" />
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="showVariableDialog = false">取消</el-button>
+        <el-button
+          v-if="editingVariable"
+          type="danger"
+          @click="deleteVariable"
+        >
+          删除
+        </el-button>
+        <el-button type="primary" @click="saveVariable">保存</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 知识图谱弹窗 -->
+    <el-dialog v-model="showGraphModal" title="🔗 知识图谱" width="900">
+      <div class="graph-container">
+        <div class="graph-legend">
+          <div class="legend-title">图例</div>
+          <div class="legend-item">
+            <span class="legend-dot entity"></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 class="graph-body">
+          <div class="graph-placeholder">
+            <el-icon size="64" color="#ccc"><Connection /></el-icon>
+            <p>知识图谱可视化(开发中)</p>
+          </div>
+        </div>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
+import {
+  ArrowLeft, Clock, Share, Check, Plus, Delete, Connection
+} from '@element-plus/icons-vue'
+import { ElMessage } from 'element-plus'
+import { useTemplateStore } from '@/stores/template'
+
+const router = useRouter()
+const route = useRoute()
+const templateStore = useTemplateStore()
+
+const templateId = route.params.templateId
+const reportTitle = ref('智慧园区建设项目可行性研究报告')
+const viewMode = ref('edit')
+const saved = ref(true)
+const editorRef = ref(null)
+
+// 来源文件
+const sourceFiles = ref([
+  { id: '1', alias: '可研批复', required: true },
+  { id: '2', alias: '项目建议书', required: true },
+  { id: '3', alias: '技术方案', required: false }
+])
+const selectedFile = ref(null)
+const showAddSourceDialog = ref(false)
+const newSourceFile = reactive({
+  alias: '',
+  description: '',
+  required: true
+})
+
+// 变量
+const variables = ref([
+  { id: '1', name: 'project_name', displayName: '项目名称', category: 'entity', exampleValue: '智慧园区建设项目', sourceType: 'document' },
+  { id: '2', name: 'total_investment', displayName: '总投资额', category: 'data', exampleValue: '5000万元', sourceType: 'document' },
+  { id: '3', name: 'project_location', displayName: '项目地点', category: 'location', exampleValue: '华南科技园', sourceType: 'document' },
+  { id: '4', name: 'tech_solution', displayName: '技术方案', category: 'concept', exampleValue: '智能化管理平台', sourceType: 'document' }
+])
+const showVariableDialog = ref(false)
+const showAddVariableDialog = ref(false)
+const editingVariable = ref(null)
+const variableForm = reactive({
+  name: '',
+  displayName: '',
+  category: 'entity',
+  exampleValue: '',
+  sourceType: 'document',
+  sourceFileAlias: '',
+  extractType: 'direct'
+})
+
+// 右键菜单
+const contextMenuVisible = ref(false)
+const contextMenuPos = reactive({ x: 0, y: 0 })
+const selectedText = ref('')
+const selectionRange = ref(null)
+
+// 知识图谱
+const showGraphModal = ref(false)
+
+// 文档内容(Mock)
+const documentContent = ref(`
+  <h1>智慧园区建设项目可行性研究报告</h1>
+  
+  <h2>一、项目概述</h2>
+  <p>本报告对<span class="entity-highlight entity" data-var="project_name">智慧园区建设项目</span>进行可行性研究分析。项目位于<span class="entity-highlight location" data-var="project_location">华南科技园</span>,总投资额为<span class="entity-highlight data" data-var="total_investment">5000万元</span>。</p>
+  
+  <h2>二、技术方案</h2>
+  <p>项目采用<span class="entity-highlight concept" data-var="tech_solution">智能化管理平台</span>作为核心技术方案,实现园区的智慧化管理和运营。</p>
+  
+  <h2>三、投资估算</h2>
+  <p>项目总投资预算包括以下几个部分:</p>
+  <ul>
+    <li>基础设施建设:2000万元</li>
+    <li>智能系统集成:1500万元</li>
+    <li>软件平台开发:1000万元</li>
+    <li>其他费用:500万元</li>
+  </ul>
+  
+  <h2>四、效益分析</h2>
+  <p>项目建成后,预计每年可实现运营收入3000万元,投资回收期约为3年。</p>
+`)
+
+// 计算属性
+const groupedVariables = computed(() => {
+  const groups = {}
+  variables.value.forEach(v => {
+    const cat = v.category || 'other'
+    if (!groups[cat]) groups[cat] = []
+    groups[cat].push(v)
+  })
+  return groups
+})
+
+// 方法
+function goBack() {
+  router.back()
+}
+
+function handleSave() {
+  saved.value = true
+  ElMessage.success('保存成功')
+}
+
+function getFileIcon(file) {
+  return '📄'
+}
+
+function selectFile(file) {
+  selectedFile.value = file
+}
+
+function removeSourceFile(file) {
+  sourceFiles.value = sourceFiles.value.filter(f => f.id !== file.id)
+  ElMessage.success('删除成功')
+}
+
+function addSourceFile() {
+  if (!newSourceFile.alias) {
+    ElMessage.warning('请输入文件别名')
+    return
+  }
+  sourceFiles.value.push({
+    id: Date.now().toString(),
+    ...newSourceFile
+  })
+  showAddSourceDialog.value = false
+  Object.assign(newSourceFile, { alias: '', description: '', required: true })
+  ElMessage.success('添加成功')
+}
+
+function getCategoryIcon(category) {
+  const icons = {
+    entity: '🏢',
+    concept: '💡',
+    data: '📊',
+    location: '📍',
+    asset: '📑'
+  }
+  return icons[category] || '📌'
+}
+
+function getCategoryColor(category) {
+  const colors = {
+    entity: '#1890ff',
+    concept: '#722ed1',
+    data: '#52c41a',
+    location: '#faad14',
+    asset: '#eb2f96'
+  }
+  return colors[category] || '#8c8c8c'
+}
+
+function getCategoryLabel(category) {
+  const labels = {
+    entity: '核心实体',
+    concept: '概念/技术',
+    data: '数据/指标',
+    location: '地点/组织',
+    asset: '资源模板'
+  }
+  return labels[category] || '其他'
+}
+
+function editVariable(variable) {
+  editingVariable.value = variable
+  Object.assign(variableForm, variable)
+  showVariableDialog.value = true
+}
+
+function saveVariable() {
+  if (!variableForm.name || !variableForm.displayName) {
+    ElMessage.warning('请填写必要字段')
+    return
+  }
+
+  if (editingVariable.value) {
+    // 更新
+    Object.assign(editingVariable.value, variableForm)
+    ElMessage.success('更新成功')
+  } else {
+    // 新增
+    variables.value.push({
+      id: Date.now().toString(),
+      ...variableForm
+    })
+    ElMessage.success('添加成功')
+  }
+
+  showVariableDialog.value = false
+  resetVariableForm()
+}
+
+function deleteVariable() {
+  if (editingVariable.value) {
+    variables.value = variables.value.filter(v => v.id !== editingVariable.value.id)
+    showVariableDialog.value = false
+    resetVariableForm()
+    ElMessage.success('删除成功')
+  }
+}
+
+function resetVariableForm() {
+  editingVariable.value = null
+  Object.assign(variableForm, {
+    name: '',
+    displayName: '',
+    category: 'entity',
+    exampleValue: '',
+    sourceType: 'document',
+    sourceFileAlias: '',
+    extractType: 'direct'
+  })
+}
+
+function handleTextSelection(event) {
+  const selection = window.getSelection()
+  const text = selection.toString().trim()
+
+  if (text) {
+    selectedText.value = text
+    selectionRange.value = selection.getRangeAt(0)
+    contextMenuPos.x = event.clientX
+    contextMenuPos.y = event.clientY
+    contextMenuVisible.value = true
+  } else {
+    contextMenuVisible.value = false
+  }
+}
+
+function markAsVariable(category) {
+  if (!selectedText.value) return
+
+  // 生成变量名
+  const varName = 'var_' + Date.now()
+
+  // 添加变量
+  variables.value.push({
+    id: Date.now().toString(),
+    name: varName,
+    displayName: selectedText.value.slice(0, 20),
+    category,
+    exampleValue: selectedText.value,
+    sourceType: 'document'
+  })
+
+  // 关闭菜单
+  contextMenuVisible.value = false
+  selectedText.value = ''
+
+  ElMessage.success('变量标记成功')
+}
+
+function handleFileUpload(response) {
+  if (response.code === 200) {
+    ElMessage.success('文件上传成功')
+  }
+}
+
+// 点击其他地方关闭右键菜单
+function handleClickOutside(event) {
+  if (!event.target.closest('.context-menu')) {
+    contextMenuVisible.value = false
+  }
+}
+
+onMounted(() => {
+  document.addEventListener('click', handleClickOutside)
+})
+
+onUnmounted(() => {
+  document.removeEventListener('click', handleClickOutside)
+})
+</script>
+
+<style lang="scss" scoped>
+.editor-page {
+  height: calc(100vh - 56px);
+  display: flex;
+  flex-direction: column;
+  background: var(--bg);
+}
+
+.editor-toolbar {
+  height: 56px;
+  background: #fff;
+  border-bottom: 1px solid var(--border);
+  display: flex;
+  align-items: center;
+  padding: 0 16px;
+  gap: 16px;
+  flex-shrink: 0;
+
+  .title-input {
+    width: 300px;
+
+    :deep(.el-input__wrapper) {
+      box-shadow: none;
+      background: transparent;
+
+      &:hover {
+        background: var(--bg);
+      }
+    }
+  }
+
+  .save-status {
+    color: var(--success);
+    font-size: 13px;
+  }
+
+  .toolbar-right {
+    margin-left: auto;
+    display: flex;
+    gap: 8px;
+    align-items: center;
+  }
+}
+
+.editor-body {
+  flex: 1;
+  display: flex;
+  overflow: hidden;
+}
+
+.left-panel {
+  width: 260px;
+  background: #fff;
+  border-right: 1px solid var(--border);
+  display: flex;
+  flex-direction: column;
+  flex-shrink: 0;
+
+  .panel-header {
+    padding: 14px 16px;
+    border-bottom: 1px solid var(--border);
+    font-size: 13px;
+    font-weight: 600;
+    display: flex;
+    justify-content: space-between;
+
+    .file-count {
+      color: var(--text-3);
+      font-weight: normal;
+    }
+  }
+
+  .panel-body {
+    flex: 1;
+    overflow-y: auto;
+    padding: 12px;
+  }
+}
+
+.upload-zone {
+  border: 2px dashed var(--border);
+  border-radius: 10px;
+  margin-bottom: 16px;
+
+  :deep(.el-upload-dragger) {
+    padding: 20px;
+    border: none;
+    background: transparent;
+  }
+
+  .upload-content {
+    text-align: center;
+  }
+
+  .upload-icon {
+    font-size: 32px;
+    margin-bottom: 8px;
+  }
+
+  .upload-text {
+    font-size: 13px;
+    color: var(--text-2);
+  }
+
+  .upload-hint {
+    font-size: 11px;
+    color: var(--text-3);
+  }
+}
+
+.file-list {
+  margin-bottom: 16px;
+}
+
+.file-item {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  padding: 10px 12px;
+  background: #fff;
+  border: 1px solid var(--border);
+  border-radius: 8px;
+  margin-bottom: 8px;
+  cursor: pointer;
+  transition: all 0.2s;
+
+  &:hover, &.active {
+    border-color: var(--primary);
+    background: var(--primary-light);
+  }
+
+  .file-icon {
+    font-size: 24px;
+  }
+
+  .file-info {
+    flex: 1;
+    min-width: 0;
+
+    .file-name {
+      font-size: 12px;
+      font-weight: 500;
+    }
+
+    .file-meta {
+      font-size: 11px;
+      color: var(--text-3);
+
+      .required {
+        color: var(--danger);
+      }
+    }
+  }
+}
+
+.add-source-btn {
+  width: 100%;
+}
+
+.center-panel {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  background: #fff;
+  overflow: hidden;
+
+  .editor-title-bar {
+    padding: 16px 24px;
+    border-bottom: 1px solid var(--border);
+    display: flex;
+    align-items: center;
+    gap: 12px;
+
+    h2 {
+      flex: 1;
+      font-size: 18px;
+      font-weight: 600;
+    }
+  }
+
+  .editor-scroll {
+    flex: 1;
+    overflow-y: auto;
+    padding: 24px 32px;
+  }
+
+  .editor-content {
+    max-width: 800px;
+    margin: 0 auto;
+    outline: none;
+
+    :deep(h1) {
+      font-size: 24px;
+      font-weight: 700;
+      margin-bottom: 24px;
+    }
+
+    :deep(h2) {
+      font-size: 18px;
+      font-weight: 600;
+      margin: 28px 0 16px;
+    }
+
+    :deep(p) {
+      margin-bottom: 16px;
+      line-height: 1.8;
+    }
+
+    :deep(ul) {
+      margin-bottom: 16px;
+      padding-left: 24px;
+
+      li {
+        margin-bottom: 8px;
+      }
+    }
+  }
+}
+
+.right-panel {
+  width: 320px;
+  background: #fff;
+  border-left: 1px solid var(--border);
+  overflow-y: auto;
+  flex-shrink: 0;
+}
+
+.element-section {
+  border-bottom: 1px solid var(--border);
+
+  .element-header {
+    padding: 14px 16px;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+
+    .element-title {
+      font-size: 13px;
+      font-weight: 600;
+
+      .element-count {
+        color: var(--text-3);
+        font-weight: normal;
+      }
+    }
+  }
+
+  .element-body {
+    padding: 0 16px 16px;
+  }
+
+  .element-tags-wrap {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 8px;
+  }
+
+  .element-hint {
+    font-size: 12px;
+    color: var(--text-3);
+    text-align: center;
+    padding: 20px;
+  }
+}
+
+.category-section {
+  padding: 12px 16px;
+  border-bottom: 1px solid var(--border);
+
+  .category-header {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    font-size: 12px;
+    font-weight: 600;
+    margin-bottom: 10px;
+
+    .category-dot {
+      width: 10px;
+      height: 10px;
+      border-radius: 50%;
+    }
+
+    .category-count {
+      color: var(--text-3);
+      font-weight: normal;
+      background: var(--bg);
+      padding: 2px 8px;
+      border-radius: 10px;
+    }
+  }
+
+  .category-items {
+    .category-item {
+      display: flex;
+      justify-content: space-between;
+      padding: 8px 12px;
+      background: var(--bg);
+      border-radius: 6px;
+      margin-bottom: 6px;
+      cursor: pointer;
+      font-size: 12px;
+      transition: all 0.2s;
+
+      &:hover {
+        background: var(--primary-light);
+      }
+
+      .item-value {
+        color: var(--text-3);
+      }
+    }
+  }
+}
+
+.context-menu {
+  position: fixed;
+  min-width: 180px;
+  background: #fff;
+  border-radius: 10px;
+  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
+  z-index: 3000;
+  overflow: hidden;
+
+  .context-menu-item {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    padding: 10px 14px;
+    font-size: 13px;
+    cursor: pointer;
+    transition: all 0.15s;
+
+    &:hover {
+      background: var(--primary-light);
+      color: var(--primary);
+    }
+
+    .icon {
+      font-size: 14px;
+    }
+  }
+}
+
+.graph-container {
+  height: 500px;
+  position: relative;
+  background: linear-gradient(135deg, #f8fafc 0%, #f0f4f8 100%);
+  border-radius: 8px;
+
+  .graph-legend {
+    position: absolute;
+    top: 16px;
+    left: 16px;
+    background: #fff;
+    border-radius: 8px;
+    padding: 12px 16px;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+
+    .legend-title {
+      font-size: 12px;
+      font-weight: 600;
+      margin-bottom: 8px;
+      color: var(--text-2);
+    }
+
+    .legend-item {
+      display: flex;
+      align-items: center;
+      gap: 8px;
+      font-size: 11px;
+      color: var(--text-2);
+      margin-bottom: 4px;
+    }
+
+    .legend-dot {
+      width: 12px;
+      height: 12px;
+      border-radius: 50%;
+
+      &.entity { background: var(--primary); }
+      &.concept { background: #722ed1; }
+      &.data { background: var(--success); }
+      &.location { background: var(--warning); }
+    }
+  }
+
+  .graph-body {
+    height: 100%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+
+    .graph-placeholder {
+      text-align: center;
+      color: var(--text-3);
+
+      p {
+        margin-top: 12px;
+      }
+    }
+  }
+}
+</style>

+ 342 - 0
frontend/vue-demo/src/views/GenerationDetail.vue

@@ -0,0 +1,342 @@
+<template>
+  <div class="generation-detail-page">
+    <div class="page-header">
+      <div class="header-left">
+        <el-button :icon="ArrowLeft" @click="router.back()">返回</el-button>
+        <h1>{{ generation?.name || '生成任务详情' }}</h1>
+        <el-tag :type="getStatusType(generation?.status)" size="large">
+          {{ getStatusLabel(generation?.status) }}
+        </el-tag>
+      </div>
+      <div class="header-right">
+        <el-button v-if="generation?.status === 'pending'" @click="executeExtraction">
+          开始提取
+        </el-button>
+        <el-button v-if="generation?.status === 'review'" type="success" @click="confirmGeneration">
+          确认生成
+        </el-button>
+        <el-button v-if="generation?.status === 'completed'" type="primary" @click="downloadDocument">
+          下载文档
+        </el-button>
+      </div>
+    </div>
+
+    <el-row :gutter="20">
+      <el-col :span="16">
+        <!-- 变量提取结果 -->
+        <div class="card section">
+          <div class="section-header">
+            <h3>变量提取结果</h3>
+            <el-tag v-if="generation?.status === 'extracting'">
+              提取中 {{ generation?.progress }}%
+            </el-tag>
+          </div>
+
+          <el-progress
+            v-if="generation?.status === 'extracting'"
+            :percentage="generation?.progress"
+            :stroke-width="8"
+            class="progress-bar"
+          />
+
+          <el-table :data="variableValues" stripe>
+            <el-table-column prop="displayName" label="变量名" width="150" />
+            <el-table-column prop="value" label="提取值">
+              <template #default="{ row }">
+                <el-input
+                  v-if="generation?.status === 'review'"
+                  v-model="row.value"
+                  size="small"
+                  @change="markAsModified(row)"
+                />
+                <span v-else>{{ row.value || '-' }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column prop="confidence" label="置信度" width="100">
+              <template #default="{ row }">
+                <el-progress
+                  :percentage="row.confidence * 100"
+                  :stroke-width="6"
+                  :color="getConfidenceColor(row.confidence)"
+                  :show-text="false"
+                />
+              </template>
+            </el-table-column>
+            <el-table-column prop="status" label="状态" width="100">
+              <template #default="{ row }">
+                <el-tag :type="getVariableStatusType(row.status)" size="small">
+                  {{ getVariableStatusLabel(row.status) }}
+                </el-tag>
+              </template>
+            </el-table-column>
+          </el-table>
+        </div>
+      </el-col>
+
+      <el-col :span="8">
+        <!-- 基本信息 -->
+        <div class="card section">
+          <h3>任务信息</h3>
+          <div class="info-item">
+            <span class="info-label">使用模板</span>
+            <span class="info-value">{{ generation?.templateName }}</span>
+          </div>
+          <div class="info-item">
+            <span class="info-label">创建时间</span>
+            <span class="info-value">{{ generation?.createTime }}</span>
+          </div>
+          <div class="info-item">
+            <span class="info-label">变量数量</span>
+            <span class="info-value">{{ variableValues.length }}</span>
+          </div>
+          <div class="info-item">
+            <span class="info-label">任务状态</span>
+            <span class="info-value">
+              <el-tag :type="getStatusType(generation?.status)" size="small">
+                {{ getStatusLabel(generation?.status) }}
+              </el-tag>
+            </span>
+          </div>
+        </div>
+
+        <!-- 来源文件 -->
+        <div class="card section">
+          <h3>来源文件</h3>
+          <div class="source-file-list">
+            <div v-for="(docId, alias) in sourceFileMap" :key="alias" class="source-file-item">
+              <span class="sf-icon">📄</span>
+              <div class="sf-info">
+                <div class="sf-name">{{ alias }}</div>
+                <div class="sf-id">{{ docId }}</div>
+              </div>
+              <el-button size="small" :icon="View">查看</el-button>
+            </div>
+          </div>
+        </div>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
+import { ArrowLeft, View } from '@element-plus/icons-vue'
+import { ElMessage } from 'element-plus'
+
+const router = useRouter()
+const route = useRoute()
+const generationId = route.params.id
+
+// Mock 数据
+const generation = ref({
+  id: generationId,
+  name: '智慧园区建设项目可行性研究报告',
+  templateName: '可行性研究报告',
+  status: 'review',
+  progress: 100,
+  createTime: '2026-01-20 10:30'
+})
+
+const sourceFileMap = ref({
+  '可研批复': 'doc_123',
+  '项目建议书': 'doc_456',
+  '技术方案': 'doc_789'
+})
+
+const variableValues = ref([
+  { name: 'project_name', displayName: '项目名称', value: '智慧园区建设项目', confidence: 0.95, status: 'extracted' },
+  { name: 'total_investment', displayName: '总投资额', value: '5000万元', confidence: 0.88, status: 'extracted' },
+  { name: 'project_location', displayName: '项目地点', value: '华南科技园', confidence: 0.92, status: 'extracted' },
+  { name: 'tech_solution', displayName: '技术方案', value: '智能化管理平台', confidence: 0.78, status: 'low_confidence' },
+  { name: 'completion_date', displayName: '完成日期', value: '', confidence: 0, status: 'pending' }
+])
+
+function getStatusType(status) {
+  const map = {
+    pending: 'info',
+    extracting: 'warning',
+    review: '',
+    completed: 'success',
+    error: 'danger'
+  }
+  return map[status] || 'info'
+}
+
+function getStatusLabel(status) {
+  const map = {
+    pending: '待执行',
+    extracting: '提取中',
+    review: '待确认',
+    completed: '已完成',
+    error: '错误'
+  }
+  return map[status] || status
+}
+
+function getVariableStatusType(status) {
+  const map = {
+    extracted: 'success',
+    modified: 'warning',
+    low_confidence: '',
+    pending: 'info',
+    error: 'danger'
+  }
+  return map[status] || 'info'
+}
+
+function getVariableStatusLabel(status) {
+  const map = {
+    extracted: '已提取',
+    modified: '已修改',
+    low_confidence: '低置信',
+    pending: '待提取',
+    error: '错误'
+  }
+  return map[status] || status
+}
+
+function getConfidenceColor(confidence) {
+  if (confidence >= 0.8) return '#52c41a'
+  if (confidence >= 0.5) return '#faad14'
+  return '#ff4d4f'
+}
+
+function markAsModified(row) {
+  row.status = 'modified'
+}
+
+function executeExtraction() {
+  generation.value.status = 'extracting'
+  generation.value.progress = 0
+
+  const interval = setInterval(() => {
+    generation.value.progress += 10
+    if (generation.value.progress >= 100) {
+      clearInterval(interval)
+      generation.value.status = 'review'
+      ElMessage.success('变量提取完成')
+    }
+  }, 500)
+}
+
+function confirmGeneration() {
+  generation.value.status = 'completed'
+  ElMessage.success('文档生成成功')
+}
+
+function downloadDocument() {
+  ElMessage.info('下载功能开发中...')
+}
+</script>
+
+<style lang="scss" scoped>
+.generation-detail-page {
+  max-width: 1200px;
+  margin: 0 auto;
+}
+
+.page-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 20px;
+
+  .header-left {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+
+    h1 {
+      font-size: 20px;
+      margin: 0;
+    }
+  }
+
+  .header-right {
+    display: flex;
+    gap: 8px;
+  }
+}
+
+.section {
+  padding: 20px;
+  margin-bottom: 20px;
+
+  h3 {
+    font-size: 15px;
+    margin-bottom: 16px;
+    padding-bottom: 10px;
+    border-bottom: 1px solid var(--border);
+  }
+
+  .section-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 16px;
+    padding-bottom: 10px;
+    border-bottom: 1px solid var(--border);
+
+    h3 {
+      margin: 0;
+      border: none;
+      padding: 0;
+    }
+  }
+}
+
+.progress-bar {
+  margin-bottom: 20px;
+}
+
+.info-item {
+  display: flex;
+  justify-content: space-between;
+  padding: 10px 0;
+  border-bottom: 1px solid var(--border);
+
+  &:last-child {
+    border-bottom: none;
+  }
+
+  .info-label {
+    color: var(--text-2);
+  }
+
+  .info-value {
+    font-weight: 500;
+  }
+}
+
+.source-file-list {
+  .source-file-item {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+    padding: 12px;
+    background: var(--bg);
+    border-radius: 8px;
+    margin-bottom: 8px;
+
+    .sf-icon {
+      font-size: 24px;
+    }
+
+    .sf-info {
+      flex: 1;
+
+      .sf-name {
+        font-weight: 600;
+        margin-bottom: 2px;
+      }
+
+      .sf-id {
+        font-size: 11px;
+        color: var(--text-3);
+        font-family: monospace;
+      }
+    }
+  }
+}
+</style>

+ 427 - 0
frontend/vue-demo/src/views/Generations.vue

@@ -0,0 +1,427 @@
+<template>
+  <div class="generations-page">
+    <div class="page-header">
+      <h1>📋 生成记录</h1>
+      <el-button type="primary" :icon="Plus" @click="showCreateDialog = true">
+        新建生成任务
+      </el-button>
+    </div>
+
+    <!-- 筛选 -->
+    <div class="filter-bar">
+      <el-select v-model="statusFilter" placeholder="全部状态" clearable style="width: 140px">
+        <el-option label="待执行" value="pending" />
+        <el-option label="提取中" value="extracting" />
+        <el-option label="待确认" value="review" />
+        <el-option label="已完成" value="completed" />
+        <el-option label="错误" value="error" />
+      </el-select>
+      <el-input
+        v-model="searchKeyword"
+        placeholder="🔍 搜索..."
+        clearable
+        style="width: 240px"
+      />
+      <el-radio-group v-model="timeFilter" style="margin-left: auto">
+        <el-radio-button label="all">全部</el-radio-button>
+        <el-radio-button label="week">本周</el-radio-button>
+        <el-radio-button label="month">本月</el-radio-button>
+      </el-radio-group>
+    </div>
+
+    <!-- 生成记录列表 -->
+    <div class="generation-list" v-loading="loading">
+      <div
+        v-for="gen in filteredGenerations"
+        :key="gen.id"
+        class="generation-card card"
+      >
+        <div class="gen-header">
+          <span class="gen-icon">📄</span>
+          <span class="gen-name">{{ gen.name }}</span>
+          <el-tag :type="getStatusType(gen.status)" size="small">
+            {{ getStatusLabel(gen.status) }}
+          </el-tag>
+        </div>
+        <div class="gen-meta">
+          <span>📅 {{ gen.createTime }}</span>
+          <span>🎨 {{ gen.templateName }}</span>
+          <span>📊 {{ gen.variableCount }} 个变量</span>
+        </div>
+        <div class="gen-progress" v-if="gen.status === 'extracting'">
+          <el-progress :percentage="gen.progress" :stroke-width="6" />
+        </div>
+        <div class="gen-actions">
+          <el-button type="primary" size="small" @click="viewGeneration(gen)">
+            查看详情
+          </el-button>
+          <el-button
+            v-if="gen.status === 'pending'"
+            size="small"
+            @click="executeGeneration(gen)"
+          >
+            开始提取
+          </el-button>
+          <el-button
+            v-if="gen.status === 'review'"
+            type="success"
+            size="small"
+            @click="confirmGeneration(gen)"
+          >
+            确认生成
+          </el-button>
+          <el-button
+            v-if="gen.status === 'completed'"
+            size="small"
+            @click="downloadGeneration(gen)"
+          >
+            下载文档
+          </el-button>
+        </div>
+      </div>
+    </div>
+
+    <!-- 空状态 -->
+    <el-empty v-if="!loading && filteredGenerations.length === 0" description="暂无生成记录">
+      <el-button type="primary" @click="showCreateDialog = true">创建生成任务</el-button>
+    </el-empty>
+
+    <!-- 创建生成任务对话框 -->
+    <el-dialog v-model="showCreateDialog" title="新建生成任务" width="600">
+      <el-form :model="newGeneration" label-width="100px">
+        <el-form-item label="任务名称">
+          <el-input v-model="newGeneration.name" placeholder="可选,默认使用模板名称" />
+        </el-form-item>
+        <el-form-item label="选择模板" required>
+          <el-select v-model="newGeneration.templateId" style="width: 100%" placeholder="请选择模板">
+            <el-option
+              v-for="tpl in templates"
+              :key="tpl.id"
+              :label="tpl.name"
+              :value="tpl.id"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="来源文件" v-if="selectedTemplate">
+          <div class="source-files-upload">
+            <div
+              v-for="sf in selectedTemplate.sourceFiles"
+              :key="sf.id"
+              class="source-file-item"
+            >
+              <div class="sf-info">
+                <span class="sf-name">{{ sf.alias }}</span>
+                <el-tag size="small" v-if="sf.required" type="danger">必需</el-tag>
+              </div>
+              <el-upload
+                :action="`/api/v1/parse/upload`"
+                :on-success="(res) => handleSourceFileUpload(sf.alias, res)"
+                :show-file-list="false"
+              >
+                <el-button size="small" :icon="Upload">
+                  {{ newGeneration.sourceFileMap[sf.alias] ? '已上传' : '上传文件' }}
+                </el-button>
+              </el-upload>
+            </div>
+          </div>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="showCreateDialog = false">取消</el-button>
+        <el-button type="primary" @click="createGeneration" :loading="creating">
+          创建
+        </el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, computed, onMounted } from 'vue'
+import { useRouter } from 'vue-router'
+import { Plus, Upload } from '@element-plus/icons-vue'
+import { ElMessage } from 'element-plus'
+import { generationApi } from '@/api'
+
+const router = useRouter()
+
+const loading = ref(false)
+const creating = ref(false)
+const statusFilter = ref('')
+const searchKeyword = ref('')
+const timeFilter = ref('all')
+const showCreateDialog = ref(false)
+
+// Mock 数据
+const generations = ref([
+  {
+    id: '1',
+    name: '智慧园区建设项目可行性研究报告',
+    templateName: '可行性研究报告',
+    status: 'completed',
+    progress: 100,
+    variableCount: 12,
+    createTime: '2026-01-20 10:30'
+  },
+  {
+    id: '2',
+    name: 'Q4市场分析报告',
+    templateName: '市场分析报告',
+    status: 'review',
+    progress: 100,
+    variableCount: 8,
+    createTime: '2026-01-19 14:20'
+  },
+  {
+    id: '3',
+    name: '新能源项目可研报告',
+    templateName: '可行性研究报告',
+    status: 'extracting',
+    progress: 65,
+    variableCount: 15,
+    createTime: '2026-01-20 16:45'
+  },
+  {
+    id: '4',
+    name: '2026年度预算报告',
+    templateName: '预算报告',
+    status: 'pending',
+    progress: 0,
+    variableCount: 20,
+    createTime: '2026-01-20 09:00'
+  }
+])
+
+const templates = ref([
+  {
+    id: '1',
+    name: '可行性研究报告',
+    sourceFiles: [
+      { id: '1', alias: '可研批复', required: true },
+      { id: '2', alias: '项目建议书', required: true },
+      { id: '3', alias: '技术方案', required: false }
+    ]
+  },
+  {
+    id: '2',
+    name: '市场分析报告',
+    sourceFiles: [
+      { id: '1', alias: '市场数据', required: true },
+      { id: '2', alias: '竞品分析', required: false }
+    ]
+  }
+])
+
+const newGeneration = reactive({
+  name: '',
+  templateId: '',
+  sourceFileMap: {}
+})
+
+const selectedTemplate = computed(() => {
+  return templates.value.find(t => t.id === newGeneration.templateId)
+})
+
+const filteredGenerations = computed(() => {
+  let result = generations.value
+
+  if (statusFilter.value) {
+    result = result.filter(g => g.status === statusFilter.value)
+  }
+
+  if (searchKeyword.value) {
+    result = result.filter(g =>
+      g.name.toLowerCase().includes(searchKeyword.value.toLowerCase())
+    )
+  }
+
+  return result
+})
+
+function getStatusType(status) {
+  const map = {
+    pending: 'info',
+    extracting: 'warning',
+    review: '',
+    completed: 'success',
+    error: 'danger'
+  }
+  return map[status] || 'info'
+}
+
+function getStatusLabel(status) {
+  const map = {
+    pending: '待执行',
+    extracting: '提取中',
+    review: '待确认',
+    completed: '已完成',
+    error: '错误'
+  }
+  return map[status] || status
+}
+
+function viewGeneration(gen) {
+  router.push(`/generations/${gen.id}`)
+}
+
+async function executeGeneration(gen) {
+  try {
+    gen.status = 'extracting'
+    gen.progress = 0
+    
+    // 模拟进度
+    const interval = setInterval(() => {
+      gen.progress += 10
+      if (gen.progress >= 100) {
+        clearInterval(interval)
+        gen.status = 'review'
+      }
+    }, 500)
+    
+    ElMessage.success('开始提取变量')
+  } catch (error) {
+    ElMessage.error('执行失败')
+  }
+}
+
+async function confirmGeneration(gen) {
+  try {
+    gen.status = 'completed'
+    ElMessage.success('文档生成成功')
+  } catch (error) {
+    ElMessage.error('生成失败')
+  }
+}
+
+function downloadGeneration(gen) {
+  ElMessage.info('下载功能开发中...')
+}
+
+function handleSourceFileUpload(alias, response) {
+  if (response.code === 200) {
+    newGeneration.sourceFileMap[alias] = response.data.id
+    ElMessage.success(`${alias} 上传成功`)
+  }
+}
+
+async function createGeneration() {
+  if (!newGeneration.templateId) {
+    ElMessage.warning('请选择模板')
+    return
+  }
+
+  // 检查必需文件
+  const template = selectedTemplate.value
+  const missingFiles = template.sourceFiles
+    .filter(sf => sf.required && !newGeneration.sourceFileMap[sf.alias])
+    .map(sf => sf.alias)
+
+  if (missingFiles.length > 0) {
+    ElMessage.warning(`请上传必需文件: ${missingFiles.join(', ')}`)
+    return
+  }
+
+  creating.value = true
+  try {
+    const gen = {
+      id: Date.now().toString(),
+      name: newGeneration.name || template.name,
+      templateName: template.name,
+      status: 'pending',
+      progress: 0,
+      variableCount: 10,
+      createTime: new Date().toLocaleString()
+    }
+    generations.value.unshift(gen)
+    showCreateDialog.value = false
+    ElMessage.success('生成任务创建成功')
+    
+    // 重置表单
+    Object.assign(newGeneration, { name: '', templateId: '', sourceFileMap: {} })
+  } catch (error) {
+    ElMessage.error('创建失败: ' + error.message)
+  } finally {
+    creating.value = false
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.generations-page {
+  max-width: 1000px;
+  margin: 0 auto;
+}
+
+.filter-bar {
+  display: flex;
+  gap: 12px;
+  margin-bottom: 20px;
+  align-items: center;
+}
+
+.generation-list {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+.generation-card {
+  padding: 18px;
+
+  .gen-header {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+    margin-bottom: 10px;
+
+    .gen-icon {
+      font-size: 22px;
+    }
+
+    .gen-name {
+      flex: 1;
+      font-size: 15px;
+      font-weight: 600;
+    }
+  }
+
+  .gen-meta {
+    display: flex;
+    gap: 20px;
+    font-size: 12px;
+    color: var(--text-2);
+    margin-bottom: 12px;
+  }
+
+  .gen-progress {
+    margin-bottom: 12px;
+  }
+
+  .gen-actions {
+    display: flex;
+    gap: 8px;
+  }
+}
+
+.source-files-upload {
+  .source-file-item {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 10px 12px;
+    background: var(--bg);
+    border-radius: 6px;
+    margin-bottom: 8px;
+
+    .sf-info {
+      display: flex;
+      align-items: center;
+      gap: 8px;
+
+      .sf-name {
+        font-weight: 500;
+      }
+    }
+  }
+}
+</style>

+ 405 - 0
frontend/vue-demo/src/views/Home.vue

@@ -0,0 +1,405 @@
+<template>
+  <div class="home-page">
+    <!-- 欢迎区 -->
+    <div class="welcome-section">
+      <h1>早上好,张三!<span class="gradient-text">智能报告,洞察未来。</span></h1>
+      <p>今天是个创作的好日子,开始您的智能报告之旅吧</p>
+    </div>
+
+    <!-- 统计卡片 -->
+    <el-row :gutter="16" class="stats-row">
+      <el-col :span="6">
+        <div class="stat-card card" @click="router.push('/generations')">
+          <div class="stat-icon blue">📄</div>
+          <div class="stat-value">{{ stats.reportCount }}</div>
+          <div class="stat-label">我的报告</div>
+          <div class="stat-trend up">↑ 3 本周新增</div>
+        </div>
+      </el-col>
+      <el-col :span="6">
+        <div class="stat-card card" @click="router.push('/templates')">
+          <div class="stat-icon purple">🎨</div>
+          <div class="stat-value">{{ stats.templateCount }}</div>
+          <div class="stat-label">可用模板</div>
+          <div class="stat-trend up">↑ 2 新增</div>
+        </div>
+      </el-col>
+      <el-col :span="6">
+        <div class="stat-card card">
+          <div class="stat-icon green">📚</div>
+          <div class="stat-value">{{ stats.documentCount }}</div>
+          <div class="stat-label">知识文档</div>
+          <div class="stat-trend">📁 1.2GB</div>
+        </div>
+      </el-col>
+      <el-col :span="6">
+        <div class="stat-card card">
+          <div class="stat-icon orange">💰</div>
+          <div class="stat-value">¥{{ stats.monthCost }}</div>
+          <div class="stat-label">本月消耗</div>
+          <div class="stat-trend">↓ 12%</div>
+        </div>
+      </el-col>
+    </el-row>
+
+    <!-- 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>
+      <el-input
+        v-model="aiInput"
+        placeholder="输入您的需求,或 @知识库 引用资料..."
+        size="large"
+        class="ai-input"
+      >
+        <template #suffix>
+          <el-button type="primary" :icon="Promotion" circle @click="handleAiSubmit" />
+        </template>
+      </el-input>
+      <div class="thinking-modes">
+        <el-radio-group v-model="thinkingMode" size="small">
+          <el-radio-button label="deep">🧠 深度思考</el-radio-button>
+          <el-radio-button label="quick">⚡ 快速回答</el-radio-button>
+          <el-radio-button label="search">🌐 联网搜索</el-radio-button>
+          <el-radio-button label="data">📊 数据分析</el-radio-button>
+        </el-radio-group>
+      </div>
+    </div>
+
+    <!-- 推荐模板 -->
+    <div class="section">
+      <div class="section-header">
+        <h2>📋 推荐模板</h2>
+        <el-button type="primary" link @click="router.push('/templates')">
+          查看全部 →
+        </el-button>
+      </div>
+      <el-row :gutter="16">
+        <el-col :span="8" v-for="tpl in recommendTemplates" :key="tpl.id">
+          <div class="tpl-card card" @click="useTemplate(tpl)">
+            <div class="tpl-preview">{{ tpl.icon }}</div>
+            <div class="tpl-info">
+              <div class="tpl-name">{{ tpl.name }}</div>
+              <div class="tpl-meta">
+                <span>📊 {{ tpl.useCount }}次</span>
+                <span>⭐ {{ tpl.rating }}</span>
+              </div>
+              <div class="tpl-tags">
+                <span class="tpl-tag" v-if="tpl.isOfficial">官方</span>
+                <span class="tpl-tag hot" v-if="tpl.isHot">热门</span>
+              </div>
+              <el-button type="primary" size="small" class="tpl-btn">
+                使用此模板
+              </el-button>
+            </div>
+          </div>
+        </el-col>
+      </el-row>
+    </div>
+
+    <!-- 快捷操作 -->
+    <el-row :gutter="16" class="quick-actions">
+      <el-col :span="12">
+        <div class="quick-action" @click="showUploadDialog = true">
+          <div class="quick-action-icon">📤</div>
+          <div>
+            <div class="quick-action-title">上传新模板</div>
+            <div class="quick-action-desc">从 Word 文档创建模板</div>
+          </div>
+        </div>
+      </el-col>
+      <el-col :span="12">
+        <div class="quick-action" @click="showCreateDialog = true">
+          <div class="quick-action-icon">🛠️</div>
+          <div>
+            <div class="quick-action-title">创建新模板</div>
+            <div class="quick-action-desc">从空白开始创建</div>
+          </div>
+        </div>
+      </el-col>
+    </el-row>
+
+    <!-- 创建模板对话框 -->
+    <el-dialog v-model="showCreateDialog" title="创建新模板" width="500">
+      <el-form :model="newTemplate" label-width="80px">
+        <el-form-item label="模板名称" required>
+          <el-input v-model="newTemplate.name" placeholder="请输入模板名称" />
+        </el-form-item>
+        <el-form-item label="描述">
+          <el-input
+            v-model="newTemplate.description"
+            type="textarea"
+            :rows="3"
+            placeholder="请输入模板描述"
+          />
+        </el-form-item>
+        <el-form-item label="基础文档" required>
+          <el-upload
+            class="upload-demo"
+            drag
+            action="/api/v1/parse/upload"
+            :on-success="handleUploadSuccess"
+          >
+            <el-icon class="el-icon--upload"><upload-filled /></el-icon>
+            <div class="el-upload__text">
+              拖拽文件到此处,或<em>点击上传</em>
+            </div>
+            <template #tip>
+              <div class="el-upload__tip">
+                支持 Word、PDF 格式
+              </div>
+            </template>
+          </el-upload>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="showCreateDialog = false">取消</el-button>
+        <el-button type="primary" @click="handleCreateTemplate">创建</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted } from 'vue'
+import { useRouter } from 'vue-router'
+import { Promotion, UploadFilled } from '@element-plus/icons-vue'
+import { ElMessage } from 'element-plus'
+import { useTemplateStore } from '@/stores/template'
+
+const router = useRouter()
+const templateStore = useTemplateStore()
+
+const aiInput = ref('')
+const thinkingMode = ref('deep')
+const showUploadDialog = ref(false)
+const showCreateDialog = ref(false)
+
+const stats = reactive({
+  reportCount: 12,
+  templateCount: 15,
+  documentCount: 48,
+  monthCost: 127.50
+})
+
+const newTemplate = reactive({
+  name: '',
+  description: '',
+  baseDocumentId: ''
+})
+
+// Mock 推荐模板数据
+const recommendTemplates = ref([
+  { id: '1', name: '市场分析报告', icon: '📊', useCount: 128, rating: 4.8, isOfficial: true, isHot: true },
+  { id: '2', name: '可行性研究报告', icon: '🏢', useCount: 96, rating: 4.9, isOfficial: true, isHot: true },
+  { id: '3', name: '项目周报', icon: '📅', useCount: 256, rating: 4.9, isOfficial: true, isHot: false }
+])
+
+onMounted(async () => {
+  // 可以从 API 获取真实数据
+  // await templateStore.fetchTemplates()
+})
+
+function handleAiSubmit() {
+  if (!aiInput.value.trim()) {
+    ElMessage.warning('请输入您的需求')
+    return
+  }
+  ElMessage.info('AI 功能开发中...')
+}
+
+function useTemplate(tpl) {
+  router.push(`/editor/${tpl.id}`)
+}
+
+function handleUploadSuccess(response) {
+  if (response.code === 200) {
+    newTemplate.baseDocumentId = response.data.id
+    ElMessage.success('文档上传成功')
+  }
+}
+
+async function handleCreateTemplate() {
+  if (!newTemplate.name) {
+    ElMessage.warning('请输入模板名称')
+    return
+  }
+  if (!newTemplate.baseDocumentId) {
+    ElMessage.warning('请上传基础文档')
+    return
+  }
+
+  try {
+    const template = await templateStore.createTemplate(newTemplate)
+    showCreateDialog.value = false
+    ElMessage.success('模板创建成功')
+    router.push(`/editor/${template.id}`)
+  } catch (error) {
+    ElMessage.error('创建失败: ' + error.message)
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.home-page {
+  max-width: 1200px;
+  margin: 0 auto;
+}
+
+.welcome-section {
+  margin-bottom: 20px;
+
+  h1 {
+    font-size: 24px;
+    font-weight: 600;
+    margin-bottom: 4px;
+  }
+
+  .gradient-text {
+    background: linear-gradient(135deg, #1890ff 0%, #722ed1 100%);
+    -webkit-background-clip: text;
+    -webkit-text-fill-color: transparent;
+  }
+
+  p {
+    color: var(--text-2);
+  }
+}
+
+.stats-row {
+  margin-bottom: 20px;
+}
+
+.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: linear-gradient(135deg, #1890ff 0%, #722ed1 100%);
+    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;
+
+    li {
+      color: var(--text-2);
+      padding: 3px 0;
+      font-size: 13px;
+
+      &::before {
+        content: '•';
+        color: var(--primary);
+        margin-right: 8px;
+      }
+    }
+  }
+
+  .ai-tip {
+    font-size: 12px;
+    color: var(--text-3);
+    padding: 8px 12px;
+    background: rgba(255, 255, 255, 0.7);
+    border-radius: 6px;
+    border-left: 3px solid var(--primary);
+  }
+
+  .ai-input {
+    margin-bottom: 14px;
+  }
+
+  .thinking-modes {
+    display: flex;
+    justify-content: center;
+  }
+}
+
+.section {
+  margin-bottom: 20px;
+
+  .section-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 14px;
+
+    h2 {
+      font-size: 15px;
+      font-weight: 600;
+    }
+  }
+}
+
+.tpl-btn {
+  width: 100%;
+}
+
+.quick-actions {
+  .quick-action {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+    padding: 16px;
+    background: #fff;
+    border: 2px dashed var(--border);
+    border-radius: 10px;
+    cursor: pointer;
+    transition: all 0.2s;
+
+    &: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;
+    }
+
+    .quick-action-title {
+      font-weight: 600;
+      margin-bottom: 2px;
+    }
+
+    .quick-action-desc {
+      font-size: 12px;
+      color: var(--text-3);
+    }
+  }
+}
+</style>

+ 282 - 0
frontend/vue-demo/src/views/TemplateDetail.vue

@@ -0,0 +1,282 @@
+<template>
+  <div class="template-detail-page">
+    <div class="page-header">
+      <div class="header-left">
+        <el-button :icon="ArrowLeft" @click="router.back()">返回</el-button>
+        <h1>{{ template?.name || '模板详情' }}</h1>
+        <el-tag :type="getStatusType(template?.status)">
+          {{ getStatusLabel(template?.status) }}
+        </el-tag>
+      </div>
+      <div class="header-right">
+        <el-button @click="router.push(`/editor/${templateId}`)">编辑模板</el-button>
+        <el-button type="primary" @click="useTemplate">使用模板</el-button>
+      </div>
+    </div>
+
+    <el-row :gutter="20">
+      <el-col :span="16">
+        <!-- 基本信息 -->
+        <div class="card section">
+          <h3>基本信息</h3>
+          <el-descriptions :column="2" border>
+            <el-descriptions-item label="模板名称">{{ template?.name }}</el-descriptions-item>
+            <el-descriptions-item label="状态">{{ getStatusLabel(template?.status) }}</el-descriptions-item>
+            <el-descriptions-item label="使用次数">{{ template?.useCount || 0 }} 次</el-descriptions-item>
+            <el-descriptions-item label="是否公开">{{ template?.isPublic ? '是' : '否' }}</el-descriptions-item>
+            <el-descriptions-item label="创建时间" :span="2">{{ template?.createTime }}</el-descriptions-item>
+            <el-descriptions-item label="描述" :span="2">{{ template?.description || '暂无描述' }}</el-descriptions-item>
+          </el-descriptions>
+        </div>
+
+        <!-- 变量列表 -->
+        <div class="card section">
+          <div class="section-header">
+            <h3>变量列表 ({{ variables.length }})</h3>
+          </div>
+          <el-table :data="variables" stripe>
+            <el-table-column prop="displayName" label="显示名称" width="150" />
+            <el-table-column prop="name" label="变量名" width="150" />
+            <el-table-column prop="category" label="类别" width="120">
+              <template #default="{ row }">
+                <el-tag :color="getCategoryColor(row.category)" effect="dark" size="small">
+                  {{ getCategoryLabel(row.category) }}
+                </el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column prop="sourceType" label="来源类型" width="120">
+              <template #default="{ row }">
+                {{ getSourceTypeLabel(row.sourceType) }}
+              </template>
+            </el-table-column>
+            <el-table-column prop="exampleValue" label="示例值" />
+          </el-table>
+        </div>
+      </el-col>
+
+      <el-col :span="8">
+        <!-- 来源文件定义 -->
+        <div class="card section">
+          <h3>来源文件定义 ({{ sourceFiles.length }})</h3>
+          <div class="source-file-list">
+            <div v-for="sf in sourceFiles" :key="sf.id" class="source-file-item">
+              <span class="sf-icon">📄</span>
+              <div class="sf-info">
+                <div class="sf-name">{{ sf.alias }}</div>
+                <div class="sf-desc">{{ sf.description || '暂无描述' }}</div>
+              </div>
+              <el-tag size="small" :type="sf.required ? 'danger' : 'info'">
+                {{ sf.required ? '必需' : '可选' }}
+              </el-tag>
+            </div>
+          </div>
+        </div>
+
+        <!-- 统计信息 -->
+        <div class="card section">
+          <h3>统计信息</h3>
+          <div class="stat-item">
+            <span class="stat-label">变量总数</span>
+            <span class="stat-value">{{ variables.length }}</span>
+          </div>
+          <div class="stat-item">
+            <span class="stat-label">来源文件</span>
+            <span class="stat-value">{{ sourceFiles.length }}</span>
+          </div>
+          <div class="stat-item">
+            <span class="stat-label">生成次数</span>
+            <span class="stat-value">{{ template?.useCount || 0 }}</span>
+          </div>
+        </div>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
+import { ArrowLeft } from '@element-plus/icons-vue'
+import { ElMessage } from 'element-plus'
+
+const router = useRouter()
+const route = useRoute()
+const templateId = route.params.id
+
+// Mock 数据
+const template = ref({
+  id: templateId,
+  name: '可行性研究报告',
+  status: 'published',
+  useCount: 96,
+  isPublic: true,
+  createTime: '2026-01-15 10:00',
+  description: '适用于各类项目可行性研究报告的生成,包含项目概述、技术方案、投资估算等章节。'
+})
+
+const sourceFiles = ref([
+  { id: '1', alias: '可研批复', description: '项目可研批复文件', required: true },
+  { id: '2', alias: '项目建议书', description: '项目建议书', required: true },
+  { id: '3', alias: '技术方案', description: '技术方案说明', required: false }
+])
+
+const variables = ref([
+  { id: '1', name: 'project_name', displayName: '项目名称', category: 'entity', sourceType: 'document', exampleValue: '智慧园区建设项目' },
+  { id: '2', name: 'total_investment', displayName: '总投资额', category: 'data', sourceType: 'document', exampleValue: '5000万元' },
+  { id: '3', name: 'project_location', displayName: '项目地点', category: 'location', sourceType: 'document', exampleValue: '华南科技园' },
+  { id: '4', name: 'tech_solution', displayName: '技术方案', category: 'concept', sourceType: 'document', exampleValue: '智能化管理平台' }
+])
+
+function getStatusType(status) {
+  const map = { draft: 'info', published: 'success', archived: 'warning' }
+  return map[status] || 'info'
+}
+
+function getStatusLabel(status) {
+  const map = { draft: '草稿', published: '已发布', archived: '已归档' }
+  return map[status] || status
+}
+
+function getCategoryColor(category) {
+  const map = {
+    entity: '#1890ff',
+    concept: '#722ed1',
+    data: '#52c41a',
+    location: '#faad14',
+    asset: '#eb2f96'
+  }
+  return map[category] || '#8c8c8c'
+}
+
+function getCategoryLabel(category) {
+  const map = {
+    entity: '核心实体',
+    concept: '概念/技术',
+    data: '数据/指标',
+    location: '地点/组织',
+    asset: '资源模板'
+  }
+  return map[category] || '其他'
+}
+
+function getSourceTypeLabel(type) {
+  const map = {
+    document: '文档提取',
+    manual: '手动输入',
+    reference: '引用变量',
+    fixed: '固定值'
+  }
+  return map[type] || type
+}
+
+function useTemplate() {
+  router.push(`/generations?templateId=${templateId}`)
+  ElMessage.info('请上传来源文件后开始生成')
+}
+</script>
+
+<style lang="scss" scoped>
+.template-detail-page {
+  max-width: 1200px;
+  margin: 0 auto;
+}
+
+.page-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 20px;
+
+  .header-left {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+
+    h1 {
+      font-size: 20px;
+      margin: 0;
+    }
+  }
+
+  .header-right {
+    display: flex;
+    gap: 8px;
+  }
+}
+
+.section {
+  padding: 20px;
+  margin-bottom: 20px;
+
+  h3 {
+    font-size: 15px;
+    margin-bottom: 16px;
+    padding-bottom: 10px;
+    border-bottom: 1px solid var(--border);
+  }
+
+  .section-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 16px;
+    padding-bottom: 10px;
+    border-bottom: 1px solid var(--border);
+
+    h3 {
+      margin: 0;
+      border: none;
+      padding: 0;
+    }
+  }
+}
+
+.source-file-list {
+  .source-file-item {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+    padding: 12px;
+    background: var(--bg);
+    border-radius: 8px;
+    margin-bottom: 8px;
+
+    .sf-icon {
+      font-size: 24px;
+    }
+
+    .sf-info {
+      flex: 1;
+
+      .sf-name {
+        font-weight: 600;
+        margin-bottom: 2px;
+      }
+
+      .sf-desc {
+        font-size: 12px;
+        color: var(--text-3);
+      }
+    }
+  }
+}
+
+.stat-item {
+  display: flex;
+  justify-content: space-between;
+  padding: 10px 0;
+  border-bottom: 1px solid var(--border);
+
+  &:last-child {
+    border-bottom: none;
+  }
+
+  .stat-label {
+    color: var(--text-2);
+  }
+
+  .stat-value {
+    font-weight: 600;
+  }
+}
+</style>

+ 345 - 0
frontend/vue-demo/src/views/Templates.vue

@@ -0,0 +1,345 @@
+<template>
+  <div class="templates-page">
+    <div class="page-header">
+      <h1>🎨 模板管理</h1>
+      <el-button type="primary" :icon="Plus" @click="showCreateDialog = true">
+        创建模板
+      </el-button>
+    </div>
+
+    <!-- 搜索和筛选 -->
+    <div class="filter-bar">
+      <el-input
+        v-model="searchKeyword"
+        placeholder="🔍 搜索模板..."
+        clearable
+        style="width: 280px"
+        @input="handleSearch"
+      />
+      <el-radio-group v-model="filterType" @change="handleFilterChange">
+        <el-radio-button label="all">全部</el-radio-button>
+        <el-radio-button label="official">官方模板</el-radio-button>
+        <el-radio-button label="mine">我的模板</el-radio-button>
+      </el-radio-group>
+    </div>
+
+    <!-- 模板列表 -->
+    <el-row :gutter="16" v-loading="loading">
+      <el-col :span="6" v-for="tpl in filteredTemplates" :key="tpl.id">
+        <div class="tpl-card card">
+          <div class="tpl-preview" @click="goToDetail(tpl)">
+            {{ getTemplateIcon(tpl) }}
+          </div>
+          <div class="tpl-info">
+            <div class="tpl-name">{{ tpl.name }}</div>
+            <div class="tpl-meta">
+              <span>📊 {{ tpl.useCount || 0 }}次</span>
+              <span>
+                <el-tag size="small" :type="getStatusType(tpl.status)">
+                  {{ getStatusLabel(tpl.status) }}
+                </el-tag>
+              </span>
+            </div>
+            <div class="tpl-tags">
+              <span class="tpl-tag" v-if="tpl.isPublic">公开</span>
+              <span class="tpl-tag" v-else>私有</span>
+            </div>
+            <div class="tpl-actions">
+              <el-button type="primary" size="small" @click="useTemplate(tpl)">
+                使用
+              </el-button>
+              <el-dropdown trigger="click" @command="(cmd) => handleCommand(cmd, tpl)">
+                <el-button size="small" :icon="More" />
+                <template #dropdown>
+                  <el-dropdown-menu>
+                    <el-dropdown-item command="edit">编辑</el-dropdown-item>
+                    <el-dropdown-item command="duplicate">复制</el-dropdown-item>
+                    <el-dropdown-item command="publish" v-if="tpl.status === 'draft'">发布</el-dropdown-item>
+                    <el-dropdown-item command="archive" v-if="tpl.status === 'published'">归档</el-dropdown-item>
+                    <el-dropdown-item command="delete" divided>
+                      <span style="color: #ff4d4f">删除</span>
+                    </el-dropdown-item>
+                  </el-dropdown-menu>
+                </template>
+              </el-dropdown>
+            </div>
+          </div>
+        </div>
+      </el-col>
+    </el-row>
+
+    <!-- 空状态 -->
+    <el-empty v-if="!loading && filteredTemplates.length === 0" description="暂无模板">
+      <el-button type="primary" @click="showCreateDialog = true">创建模板</el-button>
+    </el-empty>
+
+    <!-- 创建模板对话框 -->
+    <el-dialog v-model="showCreateDialog" title="创建新模板" width="500">
+      <el-form :model="newTemplate" label-width="80px">
+        <el-form-item label="模板名称" required>
+          <el-input v-model="newTemplate.name" placeholder="请输入模板名称" />
+        </el-form-item>
+        <el-form-item label="描述">
+          <el-input
+            v-model="newTemplate.description"
+            type="textarea"
+            :rows="3"
+            placeholder="请输入模板描述"
+          />
+        </el-form-item>
+        <el-form-item label="基础文档" required>
+          <el-upload
+            class="upload-demo"
+            drag
+            action="/api/v1/parse/upload"
+            :on-success="handleUploadSuccess"
+            :show-file-list="false"
+          >
+            <div v-if="newTemplate.baseDocumentId" class="upload-success">
+              <el-icon size="40" color="#52c41a"><CircleCheckFilled /></el-icon>
+              <div>文档已上传</div>
+            </div>
+            <template v-else>
+              <el-icon class="el-icon--upload"><UploadFilled /></el-icon>
+              <div class="el-upload__text">
+                拖拽文件到此处,或<em>点击上传</em>
+              </div>
+            </template>
+            <template #tip>
+              <div class="el-upload__tip">
+                支持 Word、PDF 格式,上传后可标记变量
+              </div>
+            </template>
+          </el-upload>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="showCreateDialog = false">取消</el-button>
+        <el-button type="primary" @click="handleCreateTemplate" :loading="creating">
+          创建
+        </el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, computed, onMounted } from 'vue'
+import { useRouter } from 'vue-router'
+import { Plus, More, UploadFilled, CircleCheckFilled } from '@element-plus/icons-vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { useTemplateStore } from '@/stores/template'
+
+const router = useRouter()
+const templateStore = useTemplateStore()
+
+const loading = ref(false)
+const creating = ref(false)
+const searchKeyword = ref('')
+const filterType = ref('all')
+const showCreateDialog = ref(false)
+
+const newTemplate = reactive({
+  name: '',
+  description: '',
+  baseDocumentId: ''
+})
+
+// Mock 数据
+const templates = ref([
+  { id: '1', name: '市场分析报告', status: 'published', useCount: 128, isPublic: true },
+  { id: '2', name: '可行性研究报告', status: 'published', useCount: 96, isPublic: true },
+  { id: '3', name: '项目周报', status: 'draft', useCount: 256, isPublic: false },
+  { id: '4', name: '尽职调查报告', status: 'draft', useCount: 45, isPublic: false }
+])
+
+const filteredTemplates = computed(() => {
+  let result = templates.value
+
+  if (searchKeyword.value) {
+    result = result.filter(t => 
+      t.name.toLowerCase().includes(searchKeyword.value.toLowerCase())
+    )
+  }
+
+  if (filterType.value === 'official') {
+    result = result.filter(t => t.isPublic)
+  } else if (filterType.value === 'mine') {
+    result = result.filter(t => !t.isPublic)
+  }
+
+  return result
+})
+
+onMounted(async () => {
+  loading.value = true
+  try {
+    // 从 API 获取模板列表
+    // await templateStore.fetchTemplates()
+    // templates.value = templateStore.templates
+  } finally {
+    loading.value = false
+  }
+})
+
+function getTemplateIcon(tpl) {
+  const icons = ['📊', '🏢', '📅', '💼', '📋', '📈']
+  return icons[parseInt(tpl.id) % icons.length]
+}
+
+function getStatusType(status) {
+  const map = {
+    draft: 'info',
+    published: 'success',
+    archived: 'warning'
+  }
+  return map[status] || 'info'
+}
+
+function getStatusLabel(status) {
+  const map = {
+    draft: '草稿',
+    published: '已发布',
+    archived: '已归档'
+  }
+  return map[status] || status
+}
+
+function handleSearch() {
+  // 搜索逻辑已在 computed 中处理
+}
+
+function handleFilterChange() {
+  // 筛选逻辑已在 computed 中处理
+}
+
+function goToDetail(tpl) {
+  router.push(`/templates/${tpl.id}`)
+}
+
+function useTemplate(tpl) {
+  router.push(`/editor/${tpl.id}`)
+}
+
+async function handleCommand(command, tpl) {
+  switch (command) {
+    case 'edit':
+      router.push(`/editor/${tpl.id}`)
+      break
+    case 'duplicate':
+      try {
+        await templateStore.createTemplate({
+          ...tpl,
+          name: `${tpl.name} (副本)`
+        })
+        ElMessage.success('复制成功')
+      } catch (error) {
+        ElMessage.error('复制失败')
+      }
+      break
+    case 'publish':
+      try {
+        await templateStore.updateTemplate(tpl.id, { status: 'published' })
+        tpl.status = 'published'
+        ElMessage.success('发布成功')
+      } catch (error) {
+        ElMessage.error('发布失败')
+      }
+      break
+    case 'archive':
+      try {
+        await templateStore.updateTemplate(tpl.id, { status: 'archived' })
+        tpl.status = 'archived'
+        ElMessage.success('归档成功')
+      } catch (error) {
+        ElMessage.error('归档失败')
+      }
+      break
+    case 'delete':
+      ElMessageBox.confirm('确定要删除该模板吗?此操作不可恢复。', '删除确认', {
+        type: 'warning',
+        confirmButtonText: '删除',
+        cancelButtonText: '取消'
+      }).then(async () => {
+        try {
+          await templateStore.deleteTemplate(tpl.id)
+          templates.value = templates.value.filter(t => t.id !== tpl.id)
+          ElMessage.success('删除成功')
+        } catch (error) {
+          ElMessage.error('删除失败')
+        }
+      })
+      break
+  }
+}
+
+function handleUploadSuccess(response) {
+  if (response.code === 200) {
+    newTemplate.baseDocumentId = response.data.id
+    ElMessage.success('文档上传成功')
+  } else {
+    ElMessage.error('上传失败: ' + response.msg)
+  }
+}
+
+async function handleCreateTemplate() {
+  if (!newTemplate.name) {
+    ElMessage.warning('请输入模板名称')
+    return
+  }
+  if (!newTemplate.baseDocumentId) {
+    ElMessage.warning('请上传基础文档')
+    return
+  }
+
+  creating.value = true
+  try {
+    const template = await templateStore.createTemplate(newTemplate)
+    showCreateDialog.value = false
+    ElMessage.success('模板创建成功')
+    router.push(`/editor/${template.id}`)
+  } catch (error) {
+    ElMessage.error('创建失败: ' + error.message)
+  } finally {
+    creating.value = false
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.templates-page {
+  max-width: 1200px;
+  margin: 0 auto;
+}
+
+.filter-bar {
+  display: flex;
+  gap: 16px;
+  margin-bottom: 20px;
+}
+
+.tpl-card {
+  margin-bottom: 16px;
+
+  .tpl-actions {
+    display: flex;
+    gap: 8px;
+    margin-top: 10px;
+
+    .el-button:first-child {
+      flex: 1;
+    }
+  }
+}
+
+.upload-success {
+  text-align: center;
+  color: #52c41a;
+  padding: 20px;
+
+  div {
+    margin-top: 8px;
+    font-size: 14px;
+  }
+}
+</style>

+ 30 - 0
frontend/vue-demo/vite.config.js

@@ -0,0 +1,30 @@
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+
+// ==================== 配置说明 ====================
+// 1. 本地开发(Java在本机): target 设为 'http://localhost:8086'
+// 2. 远程服务器: target 设为 'http://服务器IP:8086'
+// 
+// 修改下方 API_SERVER 变量即可切换
+
+const API_SERVER = process.env.API_SERVER || 'http://localhost:8086'
+
+export default defineConfig({
+  plugins: [vue()],
+  server: {
+    port: 5173,
+    host: true, // 允许局域网访问
+    proxy: {
+      '/api': {
+        target: API_SERVER,
+        changeOrigin: true,
+        secure: false
+      }
+    }
+  },
+  resolve: {
+    alias: {
+      '@': '/src'
+    }
+  }
+})

+ 958 - 0
frontend/vue-demo/yarn.lock

@@ -0,0 +1,958 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@babel/helper-string-parser@^7.27.1":
+  version "7.27.1"
+  resolved "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687"
+  integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==
+
+"@babel/helper-validator-identifier@^7.28.5":
+  version "7.28.5"
+  resolved "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4"
+  integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==
+
+"@babel/parser@^7.28.5":
+  version "7.28.6"
+  resolved "https://registry.npmmirror.com/@babel/parser/-/parser-7.28.6.tgz#f01a8885b7fa1e56dd8a155130226cd698ef13fd"
+  integrity sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==
+  dependencies:
+    "@babel/types" "^7.28.6"
+
+"@babel/types@^7.28.6":
+  version "7.28.6"
+  resolved "https://registry.npmmirror.com/@babel/types/-/types-7.28.6.tgz#c3e9377f1b155005bcc4c46020e7e394e13089df"
+  integrity sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==
+  dependencies:
+    "@babel/helper-string-parser" "^7.27.1"
+    "@babel/helper-validator-identifier" "^7.28.5"
+
+"@ctrl/tinycolor@^3.4.1":
+  version "3.6.1"
+  resolved "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz#b6c75a56a1947cc916ea058772d666a2c8932f31"
+  integrity sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==
+
+"@element-plus/icons-vue@^2.3.1", "@element-plus/icons-vue@^2.3.2":
+  version "2.3.2"
+  resolved "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz#7e9cb231fb738b2056f33e22c3a29e214b538dcf"
+  integrity sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==
+
+"@esbuild/aix-ppc64@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f"
+  integrity sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==
+
+"@esbuild/android-arm64@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz#09d9b4357780da9ea3a7dfb833a1f1ff439b4052"
+  integrity sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==
+
+"@esbuild/android-arm@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz#9b04384fb771926dfa6d7ad04324ecb2ab9b2e28"
+  integrity sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==
+
+"@esbuild/android-x64@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz#29918ec2db754cedcb6c1b04de8cd6547af6461e"
+  integrity sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==
+
+"@esbuild/darwin-arm64@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz#e495b539660e51690f3928af50a76fb0a6ccff2a"
+  integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==
+
+"@esbuild/darwin-x64@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz#c13838fa57372839abdddc91d71542ceea2e1e22"
+  integrity sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==
+
+"@esbuild/freebsd-arm64@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz#646b989aa20bf89fd071dd5dbfad69a3542e550e"
+  integrity sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==
+
+"@esbuild/freebsd-x64@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz#aa615cfc80af954d3458906e38ca22c18cf5c261"
+  integrity sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==
+
+"@esbuild/linux-arm64@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz#70ac6fa14f5cb7e1f7f887bcffb680ad09922b5b"
+  integrity sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==
+
+"@esbuild/linux-arm@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz#fc6fd11a8aca56c1f6f3894f2bea0479f8f626b9"
+  integrity sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==
+
+"@esbuild/linux-ia32@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz#3271f53b3f93e3d093d518d1649d6d68d346ede2"
+  integrity sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==
+
+"@esbuild/linux-loong64@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz#ed62e04238c57026aea831c5a130b73c0f9f26df"
+  integrity sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==
+
+"@esbuild/linux-mips64el@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz#e79b8eb48bf3b106fadec1ac8240fb97b4e64cbe"
+  integrity sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==
+
+"@esbuild/linux-ppc64@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz#5f2203860a143b9919d383ef7573521fb154c3e4"
+  integrity sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==
+
+"@esbuild/linux-riscv64@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz#07bcafd99322d5af62f618cb9e6a9b7f4bb825dc"
+  integrity sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==
+
+"@esbuild/linux-s390x@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz#b7ccf686751d6a3e44b8627ababc8be3ef62d8de"
+  integrity sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==
+
+"@esbuild/linux-x64@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz#6d8f0c768e070e64309af8004bb94e68ab2bb3b0"
+  integrity sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==
+
+"@esbuild/netbsd-x64@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz#bbe430f60d378ecb88decb219c602667387a6047"
+  integrity sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==
+
+"@esbuild/openbsd-x64@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz#99d1cf2937279560d2104821f5ccce220cb2af70"
+  integrity sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==
+
+"@esbuild/sunos-x64@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz#08741512c10d529566baba837b4fe052c8f3487b"
+  integrity sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==
+
+"@esbuild/win32-arm64@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz#675b7385398411240735016144ab2e99a60fc75d"
+  integrity sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==
+
+"@esbuild/win32-ia32@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz#1bfc3ce98aa6ca9a0969e4d2af72144c59c1193b"
+  integrity sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==
+
+"@esbuild/win32-x64@0.21.5":
+  version "0.21.5"
+  resolved "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz#acad351d582d157bb145535db2a6ff53dd514b5c"
+  integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==
+
+"@floating-ui/core@^1.7.3":
+  version "1.7.3"
+  resolved "https://registry.npmmirror.com/@floating-ui/core/-/core-1.7.3.tgz#462d722f001e23e46d86fd2bd0d21b7693ccb8b7"
+  integrity sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==
+  dependencies:
+    "@floating-ui/utils" "^0.2.10"
+
+"@floating-ui/dom@^1.0.1":
+  version "1.7.4"
+  resolved "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.7.4.tgz#ee667549998745c9c3e3e84683b909c31d6c9a77"
+  integrity sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==
+  dependencies:
+    "@floating-ui/core" "^1.7.3"
+    "@floating-ui/utils" "^0.2.10"
+
+"@floating-ui/utils@^0.2.10":
+  version "0.2.10"
+  resolved "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.10.tgz#a2a1e3812d14525f725d011a73eceb41fef5bc1c"
+  integrity sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==
+
+"@jridgewell/sourcemap-codec@^1.5.5":
+  version "1.5.5"
+  resolved "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba"
+  integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==
+
+"@parcel/watcher-android-arm64@2.5.6":
+  version "2.5.6"
+  resolved "https://registry.npmmirror.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz#5f32e0dba356f4ac9a11068d2a5c134ca3ba6564"
+  integrity sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==
+
+"@parcel/watcher-darwin-arm64@2.5.6":
+  version "2.5.6"
+  resolved "https://registry.npmmirror.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz#88d3e720b59b1eceffce98dac46d7c40e8be5e8e"
+  integrity sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==
+
+"@parcel/watcher-darwin-x64@2.5.6":
+  version "2.5.6"
+  resolved "https://registry.npmmirror.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz#bf05d76a78bc15974f15ec3671848698b0838063"
+  integrity sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==
+
+"@parcel/watcher-freebsd-x64@2.5.6":
+  version "2.5.6"
+  resolved "https://registry.npmmirror.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz#8bc26e9848e7303ac82922a5ae1b1ef1bdb48a53"
+  integrity sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==
+
+"@parcel/watcher-linux-arm-glibc@2.5.6":
+  version "2.5.6"
+  resolved "https://registry.npmmirror.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz#1328fee1deb0c2d7865079ef53a2ba4cc2f8b40a"
+  integrity sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==
+
+"@parcel/watcher-linux-arm-musl@2.5.6":
+  version "2.5.6"
+  resolved "https://registry.npmmirror.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz#bad0f45cb3e2157746db8b9d22db6a125711f152"
+  integrity sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==
+
+"@parcel/watcher-linux-arm64-glibc@2.5.6":
+  version "2.5.6"
+  resolved "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz#b75913fbd501d9523c5f35d420957bf7d0204809"
+  integrity sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==
+
+"@parcel/watcher-linux-arm64-musl@2.5.6":
+  version "2.5.6"
+  resolved "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz#da5621a6a576070c8c0de60dea8b46dc9c3827d4"
+  integrity sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==
+
+"@parcel/watcher-linux-x64-glibc@2.5.6":
+  version "2.5.6"
+  resolved "https://registry.npmmirror.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz#ce437accdc4b30f93a090b4a221fd95cd9b89639"
+  integrity sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==
+
+"@parcel/watcher-linux-x64-musl@2.5.6":
+  version "2.5.6"
+  resolved "https://registry.npmmirror.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz#02400c54b4a67efcc7e2327b249711920ac969e2"
+  integrity sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==
+
+"@parcel/watcher-win32-arm64@2.5.6":
+  version "2.5.6"
+  resolved "https://registry.npmmirror.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz#caae3d3c7583ca0a7171e6bd142c34d20ea1691e"
+  integrity sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==
+
+"@parcel/watcher-win32-ia32@2.5.6":
+  version "2.5.6"
+  resolved "https://registry.npmmirror.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz#9ac922550896dfe47bfc5ae3be4f1bcaf8155d6d"
+  integrity sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==
+
+"@parcel/watcher-win32-x64@2.5.6":
+  version "2.5.6"
+  resolved "https://registry.npmmirror.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz#73fdafba2e21c448f0e456bbe13178d8fe11739d"
+  integrity sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==
+
+"@parcel/watcher@^2.4.1":
+  version "2.5.6"
+  resolved "https://registry.npmmirror.com/@parcel/watcher/-/watcher-2.5.6.tgz#3f932828c894f06d0ad9cfefade1756ecc6ef1f1"
+  integrity sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==
+  dependencies:
+    detect-libc "^2.0.3"
+    is-glob "^4.0.3"
+    node-addon-api "^7.0.0"
+    picomatch "^4.0.3"
+  optionalDependencies:
+    "@parcel/watcher-android-arm64" "2.5.6"
+    "@parcel/watcher-darwin-arm64" "2.5.6"
+    "@parcel/watcher-darwin-x64" "2.5.6"
+    "@parcel/watcher-freebsd-x64" "2.5.6"
+    "@parcel/watcher-linux-arm-glibc" "2.5.6"
+    "@parcel/watcher-linux-arm-musl" "2.5.6"
+    "@parcel/watcher-linux-arm64-glibc" "2.5.6"
+    "@parcel/watcher-linux-arm64-musl" "2.5.6"
+    "@parcel/watcher-linux-x64-glibc" "2.5.6"
+    "@parcel/watcher-linux-x64-musl" "2.5.6"
+    "@parcel/watcher-win32-arm64" "2.5.6"
+    "@parcel/watcher-win32-ia32" "2.5.6"
+    "@parcel/watcher-win32-x64" "2.5.6"
+
+"@popperjs/core@npm:@sxzz/popperjs-es@^2.11.7":
+  version "2.11.7"
+  resolved "https://registry.npmmirror.com/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz#a7f69e3665d3da9b115f9e71671dae1b97e13671"
+  integrity sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==
+
+"@rollup/rollup-android-arm-eabi@4.56.0":
+  version "4.56.0"
+  resolved "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.56.0.tgz#067cfcd81f1c1bfd92aefe3ad5ef1523549d5052"
+  integrity sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw==
+
+"@rollup/rollup-android-arm64@4.56.0":
+  version "4.56.0"
+  resolved "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.56.0.tgz#85e39a44034d7d4e4fee2a1616f0bddb85a80517"
+  integrity sha512-lfbVUbelYqXlYiU/HApNMJzT1E87UPGvzveGg2h0ktUNlOCxKlWuJ9jtfvs1sKHdwU4fzY7Pl8sAl49/XaEk6Q==
+
+"@rollup/rollup-darwin-arm64@4.56.0":
+  version "4.56.0"
+  resolved "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.56.0.tgz#17d92fe98f2cc277b91101eb1528b7c0b6c00c54"
+  integrity sha512-EgxD1ocWfhoD6xSOeEEwyE7tDvwTgZc8Bss7wCWe+uc7wO8G34HHCUH+Q6cHqJubxIAnQzAsyUsClt0yFLu06w==
+
+"@rollup/rollup-darwin-x64@4.56.0":
+  version "4.56.0"
+  resolved "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.56.0.tgz#89ae6c66b1451609bd1f297da9384463f628437d"
+  integrity sha512-1vXe1vcMOssb/hOF8iv52A7feWW2xnu+c8BV4t1F//m9QVLTfNVpEdja5ia762j/UEJe2Z1jAmEqZAK42tVW3g==
+
+"@rollup/rollup-freebsd-arm64@4.56.0":
+  version "4.56.0"
+  resolved "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.56.0.tgz#cdbdb9947b26e76c188a31238c10639347413628"
+  integrity sha512-bof7fbIlvqsyv/DtaXSck4VYQ9lPtoWNFCB/JY4snlFuJREXfZnm+Ej6yaCHfQvofJDXLDMTVxWscVSuQvVWUQ==
+
+"@rollup/rollup-freebsd-x64@4.56.0":
+  version "4.56.0"
+  resolved "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.56.0.tgz#9b1458d07b6e040be16ee36d308a2c9520f7f7cc"
+  integrity sha512-KNa6lYHloW+7lTEkYGa37fpvPq+NKG/EHKM8+G/g9WDU7ls4sMqbVRV78J6LdNuVaeeK5WB9/9VAFbKxcbXKYg==
+
+"@rollup/rollup-linux-arm-gnueabihf@4.56.0":
+  version "4.56.0"
+  resolved "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.56.0.tgz#1d50ded7c965d5f125f5832c971ad5b287befef7"
+  integrity sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A==
+
+"@rollup/rollup-linux-arm-musleabihf@4.56.0":
+  version "4.56.0"
+  resolved "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.56.0.tgz#53597e319b7e65990d3bc2a5048097384814c179"
+  integrity sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw==
+
+"@rollup/rollup-linux-arm64-gnu@4.56.0":
+  version "4.56.0"
+  resolved "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.56.0.tgz#597002909dec198ca4bdccb25f043d32db3d6283"
+  integrity sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ==
+
+"@rollup/rollup-linux-arm64-musl@4.56.0":
+  version "4.56.0"
+  resolved "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.56.0.tgz#286f0e0f799545ce288bdc5a7c777261fcba3d54"
+  integrity sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA==
+
+"@rollup/rollup-linux-loong64-gnu@4.56.0":
+  version "4.56.0"
+  resolved "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.56.0.tgz#1fab07fa1a4f8d3697735b996517f1bae0ba101b"
+  integrity sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg==
+
+"@rollup/rollup-linux-loong64-musl@4.56.0":
+  version "4.56.0"
+  resolved "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.56.0.tgz#efc2cb143d6c067f95205482afb177f78ed9ea3d"
+  integrity sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA==
+
+"@rollup/rollup-linux-ppc64-gnu@4.56.0":
+  version "4.56.0"
+  resolved "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.56.0.tgz#e8de8bd3463f96b92b7dfb7f151fd80ffe8a937c"
+  integrity sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw==
+
+"@rollup/rollup-linux-ppc64-musl@4.56.0":
+  version "4.56.0"
+  resolved "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.56.0.tgz#8c508fe28a239da83b3a9da75bcf093186e064b4"
+  integrity sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg==
+
+"@rollup/rollup-linux-riscv64-gnu@4.56.0":
+  version "4.56.0"
+  resolved "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.56.0.tgz#ff6d51976e0830732880770a9e18553136b8d92b"
+  integrity sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew==
+
+"@rollup/rollup-linux-riscv64-musl@4.56.0":
+  version "4.56.0"
+  resolved "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.56.0.tgz#325fb35eefc7e81d75478318f0deee1e4a111493"
+  integrity sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ==
+
+"@rollup/rollup-linux-s390x-gnu@4.56.0":
+  version "4.56.0"
+  resolved "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.56.0.tgz#37410fabb5d3ba4ad34abcfbe9ba9b6288413f30"
+  integrity sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ==
+
+"@rollup/rollup-linux-x64-gnu@4.56.0":
+  version "4.56.0"
+  resolved "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.56.0.tgz#8ef907a53b2042068fc03fcc6a641e2b02276eca"
+  integrity sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw==
+
+"@rollup/rollup-linux-x64-musl@4.56.0":
+  version "4.56.0"
+  resolved "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.56.0.tgz#61b9ba09ea219e0174b3f35a6ad2afc94bdd5662"
+  integrity sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA==
+
+"@rollup/rollup-openbsd-x64@4.56.0":
+  version "4.56.0"
+  resolved "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.56.0.tgz#fc4e54133134c1787d0b016ffdd5aeb22a5effd3"
+  integrity sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA==
+
+"@rollup/rollup-openharmony-arm64@4.56.0":
+  version "4.56.0"
+  resolved "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.56.0.tgz#959ae225b1eeea0cc5b7c9f88e4834330fb6cd09"
+  integrity sha512-LhN/Reh+7F3RCgQIRbgw8ZMwUwyqJM+8pXNT6IIJAqm2IdKkzpCh/V9EdgOMBKuebIrzswqy4ATlrDgiOwbRcQ==
+
+"@rollup/rollup-win32-arm64-msvc@4.56.0":
+  version "4.56.0"
+  resolved "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.56.0.tgz#842acd38869fa1cbdbc240c76c67a86f93444c27"
+  integrity sha512-kbFsOObXp3LBULg1d3JIUQMa9Kv4UitDmpS+k0tinPBz3watcUiV2/LUDMMucA6pZO3WGE27P7DsfaN54l9ing==
+
+"@rollup/rollup-win32-ia32-msvc@4.56.0":
+  version "4.56.0"
+  resolved "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.56.0.tgz#7ab654def4042df44cb29f8ed9d5044e850c66d5"
+  integrity sha512-vSSgny54D6P4vf2izbtFm/TcWYedw7f8eBrOiGGecyHyQB9q4Kqentjaj8hToe+995nob/Wv48pDqL5a62EWtg==
+
+"@rollup/rollup-win32-x64-gnu@4.56.0":
+  version "4.56.0"
+  resolved "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.56.0.tgz#7426cdec1b01d2382ffd5cda83cbdd1c8efb3ca6"
+  integrity sha512-FeCnkPCTHQJFbiGG49KjV5YGW/8b9rrXAM2Mz2kiIoktq2qsJxRD5giEMEOD2lPdgs72upzefaUvS+nc8E3UzQ==
+
+"@rollup/rollup-win32-x64-msvc@4.56.0":
+  version "4.56.0"
+  resolved "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.56.0.tgz#9eec0212732a432c71bde0350bc40b673d15b2db"
+  integrity sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g==
+
+"@types/estree@1.0.8":
+  version "1.0.8"
+  resolved "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e"
+  integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
+
+"@types/lodash-es@^4.17.12":
+  version "4.17.12"
+  resolved "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz#65f6d1e5f80539aa7cfbfc962de5def0cf4f341b"
+  integrity sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==
+  dependencies:
+    "@types/lodash" "*"
+
+"@types/lodash@*", "@types/lodash@^4.17.20":
+  version "4.17.23"
+  resolved "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.23.tgz#c1bb06db218acc8fc232da0447473fc2fb9d9841"
+  integrity sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==
+
+"@types/web-bluetooth@^0.0.20":
+  version "0.0.20"
+  resolved "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz#f066abfcd1cbe66267cdbbf0de010d8a41b41597"
+  integrity sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==
+
+"@vitejs/plugin-vue@^4.5.2":
+  version "4.6.2"
+  resolved "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-4.6.2.tgz#057d2ded94c4e71b94e9814f92dcd9306317aa46"
+  integrity sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw==
+
+"@vue/compiler-core@3.5.27":
+  version "3.5.27"
+  resolved "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.27.tgz#ce4402428e26095586eb889c41f6e172eb3960bd"
+  integrity sha512-gnSBQjZA+//qDZen+6a2EdHqJ68Z7uybrMf3SPjEGgG4dicklwDVmMC1AeIHxtLVPT7sn6sH1KOO+tS6gwOUeQ==
+  dependencies:
+    "@babel/parser" "^7.28.5"
+    "@vue/shared" "3.5.27"
+    entities "^7.0.0"
+    estree-walker "^2.0.2"
+    source-map-js "^1.2.1"
+
+"@vue/compiler-dom@3.5.27":
+  version "3.5.27"
+  resolved "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.27.tgz#32b2bc87f0a652c253986796ace0ed6213093af8"
+  integrity sha512-oAFea8dZgCtVVVTEC7fv3T5CbZW9BxpFzGGxC79xakTr6ooeEqmRuvQydIiDAkglZEAd09LgVf1RoDnL54fu5w==
+  dependencies:
+    "@vue/compiler-core" "3.5.27"
+    "@vue/shared" "3.5.27"
+
+"@vue/compiler-sfc@3.5.27":
+  version "3.5.27"
+  resolved "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.27.tgz#84651b8816bf8e7d6e62fddd14db86efd6d6f1b6"
+  integrity sha512-sHZu9QyDPeDmN/MRoshhggVOWE5WlGFStKFwu8G52swATgSny27hJRWteKDSUUzUH+wp+bmeNbhJnEAel/auUQ==
+  dependencies:
+    "@babel/parser" "^7.28.5"
+    "@vue/compiler-core" "3.5.27"
+    "@vue/compiler-dom" "3.5.27"
+    "@vue/compiler-ssr" "3.5.27"
+    "@vue/shared" "3.5.27"
+    estree-walker "^2.0.2"
+    magic-string "^0.30.21"
+    postcss "^8.5.6"
+    source-map-js "^1.2.1"
+
+"@vue/compiler-ssr@3.5.27":
+  version "3.5.27"
+  resolved "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.27.tgz#b480cad09dacf8f3d9c82b9843402f1a803baee7"
+  integrity sha512-Sj7h+JHt512fV1cTxKlYhg7qxBvack+BGncSpH+8vnN+KN95iPIcqB5rsbblX40XorP+ilO7VIKlkuu3Xq2vjw==
+  dependencies:
+    "@vue/compiler-dom" "3.5.27"
+    "@vue/shared" "3.5.27"
+
+"@vue/devtools-api@^6.6.3", "@vue/devtools-api@^6.6.4":
+  version "6.6.4"
+  resolved "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz#cbe97fe0162b365edc1dba80e173f90492535343"
+  integrity sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==
+
+"@vue/reactivity@3.5.27":
+  version "3.5.27"
+  resolved "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.27.tgz#d870557de1389a27b8abcb7cbfa30978dc69a000"
+  integrity sha512-vvorxn2KXfJ0nBEnj4GYshSgsyMNFnIQah/wczXlsNXt+ijhugmW+PpJ2cNPe4V6jpnBcs0MhCODKllWG+nvoQ==
+  dependencies:
+    "@vue/shared" "3.5.27"
+
+"@vue/runtime-core@3.5.27":
+  version "3.5.27"
+  resolved "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.27.tgz#bb43744ed070166c7d581b849ac22b71a9ccf127"
+  integrity sha512-fxVuX/fzgzeMPn/CLQecWeDIFNt3gQVhxM0rW02Tvp/YmZfXQgcTXlakq7IMutuZ/+Ogbn+K0oct9J3JZfyk3A==
+  dependencies:
+    "@vue/reactivity" "3.5.27"
+    "@vue/shared" "3.5.27"
+
+"@vue/runtime-dom@3.5.27":
+  version "3.5.27"
+  resolved "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.27.tgz#392513252c7ca7e5277240fdc70b8093449127f5"
+  integrity sha512-/QnLslQgYqSJ5aUmb5F0z0caZPGHRB8LEAQ1s81vHFM5CBfnun63rxhvE/scVb/j3TbBuoZwkJyiLCkBluMpeg==
+  dependencies:
+    "@vue/reactivity" "3.5.27"
+    "@vue/runtime-core" "3.5.27"
+    "@vue/shared" "3.5.27"
+    csstype "^3.2.3"
+
+"@vue/server-renderer@3.5.27":
+  version "3.5.27"
+  resolved "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.27.tgz#8137d0d7ec3b59d5992bb04c553775d209dddba7"
+  integrity sha512-qOz/5thjeP1vAFc4+BY3Nr6wxyLhpeQgAE/8dDtKo6a6xdk+L4W46HDZgNmLOBUDEkFXV3G7pRiUqxjX0/2zWA==
+  dependencies:
+    "@vue/compiler-ssr" "3.5.27"
+    "@vue/shared" "3.5.27"
+
+"@vue/shared@3.5.27":
+  version "3.5.27"
+  resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.27.tgz#33a63143d8fb9ca1b3efbc7ecf9bd0ab05f7e06e"
+  integrity sha512-dXr/3CgqXsJkZ0n9F3I4elY8wM9jMJpP3pvRG52r6m0tu/MsAFIe6JpXVGeNMd/D9F4hQynWT8Rfuj0bdm9kFQ==
+
+"@vueuse/core@^10.11.0":
+  version "10.11.1"
+  resolved "https://registry.npmmirror.com/@vueuse/core/-/core-10.11.1.tgz#15d2c0b6448d2212235b23a7ba29c27173e0c2c6"
+  integrity sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==
+  dependencies:
+    "@types/web-bluetooth" "^0.0.20"
+    "@vueuse/metadata" "10.11.1"
+    "@vueuse/shared" "10.11.1"
+    vue-demi ">=0.14.8"
+
+"@vueuse/metadata@10.11.1":
+  version "10.11.1"
+  resolved "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-10.11.1.tgz#209db7bb5915aa172a87510b6de2ca01cadbd2a7"
+  integrity sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==
+
+"@vueuse/shared@10.11.1":
+  version "10.11.1"
+  resolved "https://registry.npmmirror.com/@vueuse/shared/-/shared-10.11.1.tgz#62b84e3118ae6e1f3ff38f4fbe71b0c5d0f10938"
+  integrity sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==
+  dependencies:
+    vue-demi ">=0.14.8"
+
+async-validator@^4.2.5:
+  version "4.2.5"
+  resolved "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz#c96ea3332a521699d0afaaceed510a54656c6339"
+  integrity sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==
+
+asynckit@^0.4.0:
+  version "0.4.0"
+  resolved "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
+  integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
+
+axios@^1.6.0:
+  version "1.13.2"
+  resolved "https://registry.npmmirror.com/axios/-/axios-1.13.2.tgz#9ada120b7b5ab24509553ec3e40123521117f687"
+  integrity sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==
+  dependencies:
+    follow-redirects "^1.15.6"
+    form-data "^4.0.4"
+    proxy-from-env "^1.1.0"
+
+call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6"
+  integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==
+  dependencies:
+    es-errors "^1.3.0"
+    function-bind "^1.1.2"
+
+chokidar@^4.0.0:
+  version "4.0.3"
+  resolved "https://registry.npmmirror.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30"
+  integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==
+  dependencies:
+    readdirp "^4.0.1"
+
+combined-stream@^1.0.8:
+  version "1.0.8"
+  resolved "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
+  integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
+  dependencies:
+    delayed-stream "~1.0.0"
+
+csstype@^3.2.3:
+  version "3.2.3"
+  resolved "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz#ec48c0f3e993e50648c86da559e2610995cf989a"
+  integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==
+
+dayjs@^1.11.19:
+  version "1.11.19"
+  resolved "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.19.tgz#15dc98e854bb43917f12021806af897c58ae2938"
+  integrity sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==
+
+delayed-stream@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
+  integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
+
+detect-libc@^2.0.3:
+  version "2.1.2"
+  resolved "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz#689c5dcdc1900ef5583a4cb9f6d7b473742074ad"
+  integrity sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==
+
+dunder-proto@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a"
+  integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==
+  dependencies:
+    call-bind-apply-helpers "^1.0.1"
+    es-errors "^1.3.0"
+    gopd "^1.2.0"
+
+element-plus@^2.4.4:
+  version "2.13.1"
+  resolved "https://registry.npmmirror.com/element-plus/-/element-plus-2.13.1.tgz#2cc6059da0f0f217f27d657f5140a45ecb0fd221"
+  integrity sha512-eG4BDBGdAsUGN6URH1PixzZb0ngdapLivIk1meghS1uEueLvQ3aljSKrCt5x6sYb6mUk8eGtzTQFgsPmLavQcA==
+  dependencies:
+    "@ctrl/tinycolor" "^3.4.1"
+    "@element-plus/icons-vue" "^2.3.2"
+    "@floating-ui/dom" "^1.0.1"
+    "@popperjs/core" "npm:@sxzz/popperjs-es@^2.11.7"
+    "@types/lodash" "^4.17.20"
+    "@types/lodash-es" "^4.17.12"
+    "@vueuse/core" "^10.11.0"
+    async-validator "^4.2.5"
+    dayjs "^1.11.19"
+    lodash "^4.17.21"
+    lodash-es "^4.17.21"
+    lodash-unified "^1.0.3"
+    memoize-one "^6.0.0"
+    normalize-wheel-es "^1.2.0"
+
+entities@^7.0.0:
+  version "7.0.1"
+  resolved "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz#26e8a88889db63417dcb9a1e79a3f1bc92b5976b"
+  integrity sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==
+
+es-define-property@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa"
+  integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==
+
+es-errors@^1.3.0:
+  version "1.3.0"
+  resolved "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
+  integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
+
+es-object-atoms@^1.0.0, es-object-atoms@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1"
+  integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==
+  dependencies:
+    es-errors "^1.3.0"
+
+es-set-tostringtag@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d"
+  integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==
+  dependencies:
+    es-errors "^1.3.0"
+    get-intrinsic "^1.2.6"
+    has-tostringtag "^1.0.2"
+    hasown "^2.0.2"
+
+esbuild@^0.21.3:
+  version "0.21.5"
+  resolved "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d"
+  integrity sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==
+  optionalDependencies:
+    "@esbuild/aix-ppc64" "0.21.5"
+    "@esbuild/android-arm" "0.21.5"
+    "@esbuild/android-arm64" "0.21.5"
+    "@esbuild/android-x64" "0.21.5"
+    "@esbuild/darwin-arm64" "0.21.5"
+    "@esbuild/darwin-x64" "0.21.5"
+    "@esbuild/freebsd-arm64" "0.21.5"
+    "@esbuild/freebsd-x64" "0.21.5"
+    "@esbuild/linux-arm" "0.21.5"
+    "@esbuild/linux-arm64" "0.21.5"
+    "@esbuild/linux-ia32" "0.21.5"
+    "@esbuild/linux-loong64" "0.21.5"
+    "@esbuild/linux-mips64el" "0.21.5"
+    "@esbuild/linux-ppc64" "0.21.5"
+    "@esbuild/linux-riscv64" "0.21.5"
+    "@esbuild/linux-s390x" "0.21.5"
+    "@esbuild/linux-x64" "0.21.5"
+    "@esbuild/netbsd-x64" "0.21.5"
+    "@esbuild/openbsd-x64" "0.21.5"
+    "@esbuild/sunos-x64" "0.21.5"
+    "@esbuild/win32-arm64" "0.21.5"
+    "@esbuild/win32-ia32" "0.21.5"
+    "@esbuild/win32-x64" "0.21.5"
+
+estree-walker@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
+  integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
+
+follow-redirects@^1.15.6:
+  version "1.15.11"
+  resolved "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.11.tgz#777d73d72a92f8ec4d2e410eb47352a56b8e8340"
+  integrity sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==
+
+form-data@^4.0.4:
+  version "4.0.5"
+  resolved "https://registry.npmmirror.com/form-data/-/form-data-4.0.5.tgz#b49e48858045ff4cbf6b03e1805cebcad3679053"
+  integrity sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==
+  dependencies:
+    asynckit "^0.4.0"
+    combined-stream "^1.0.8"
+    es-set-tostringtag "^2.1.0"
+    hasown "^2.0.2"
+    mime-types "^2.1.12"
+
+fsevents@~2.3.2, fsevents@~2.3.3:
+  version "2.3.3"
+  resolved "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
+  integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
+
+function-bind@^1.1.2:
+  version "1.1.2"
+  resolved "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
+  integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
+
+get-intrinsic@^1.2.6:
+  version "1.3.0"
+  resolved "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01"
+  integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==
+  dependencies:
+    call-bind-apply-helpers "^1.0.2"
+    es-define-property "^1.0.1"
+    es-errors "^1.3.0"
+    es-object-atoms "^1.1.1"
+    function-bind "^1.1.2"
+    get-proto "^1.0.1"
+    gopd "^1.2.0"
+    has-symbols "^1.1.0"
+    hasown "^2.0.2"
+    math-intrinsics "^1.1.0"
+
+get-proto@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1"
+  integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==
+  dependencies:
+    dunder-proto "^1.0.1"
+    es-object-atoms "^1.0.0"
+
+gopd@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1"
+  integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==
+
+has-symbols@^1.0.3, has-symbols@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338"
+  integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==
+
+has-tostringtag@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc"
+  integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==
+  dependencies:
+    has-symbols "^1.0.3"
+
+hasown@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003"
+  integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
+  dependencies:
+    function-bind "^1.1.2"
+
+immutable@^5.0.2:
+  version "5.1.4"
+  resolved "https://registry.npmmirror.com/immutable/-/immutable-5.1.4.tgz#e3f8c1fe7b567d56cf26698f31918c241dae8c1f"
+  integrity sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==
+
+is-extglob@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
+  integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
+
+is-glob@^4.0.3:
+  version "4.0.3"
+  resolved "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
+  integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
+  dependencies:
+    is-extglob "^2.1.1"
+
+lodash-es@^4.17.21:
+  version "4.17.23"
+  resolved "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.23.tgz#58c4360fd1b5d33afc6c0bbd3d1149349b1138e0"
+  integrity sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==
+
+lodash-unified@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.npmmirror.com/lodash-unified/-/lodash-unified-1.0.3.tgz#80b1eac10ed2eb02ed189f08614a29c27d07c894"
+  integrity sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==
+
+lodash@^4.17.21:
+  version "4.17.23"
+  resolved "https://registry.npmmirror.com/lodash/-/lodash-4.17.23.tgz#f113b0378386103be4f6893388c73d0bde7f2c5a"
+  integrity sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==
+
+magic-string@^0.30.21:
+  version "0.30.21"
+  resolved "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz#56763ec09a0fa8091df27879fd94d19078c00d91"
+  integrity sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==
+  dependencies:
+    "@jridgewell/sourcemap-codec" "^1.5.5"
+
+math-intrinsics@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9"
+  integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==
+
+memoize-one@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045"
+  integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==
+
+mime-db@1.52.0:
+  version "1.52.0"
+  resolved "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
+  integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
+
+mime-types@^2.1.12:
+  version "2.1.35"
+  resolved "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
+  integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
+  dependencies:
+    mime-db "1.52.0"
+
+nanoid@^3.3.11:
+  version "3.3.11"
+  resolved "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b"
+  integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==
+
+node-addon-api@^7.0.0:
+  version "7.1.1"
+  resolved "https://registry.npmmirror.com/node-addon-api/-/node-addon-api-7.1.1.tgz#1aba6693b0f255258a049d621329329322aad558"
+  integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==
+
+normalize-wheel-es@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz#0fa2593d619f7245a541652619105ab076acf09e"
+  integrity sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==
+
+picocolors@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
+  integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
+
+picomatch@^4.0.3:
+  version "4.0.3"
+  resolved "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042"
+  integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==
+
+pinia@^2.1.7:
+  version "2.3.1"
+  resolved "https://registry.npmmirror.com/pinia/-/pinia-2.3.1.tgz#54c476675b72f5abcfafa24a7582531ea8c23d94"
+  integrity sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==
+  dependencies:
+    "@vue/devtools-api" "^6.6.3"
+    vue-demi "^0.14.10"
+
+postcss@^8.4.43, postcss@^8.5.6:
+  version "8.5.6"
+  resolved "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz#2825006615a619b4f62a9e7426cc120b349a8f3c"
+  integrity sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==
+  dependencies:
+    nanoid "^3.3.11"
+    picocolors "^1.1.1"
+    source-map-js "^1.2.1"
+
+proxy-from-env@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
+  integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
+
+readdirp@^4.0.1:
+  version "4.1.2"
+  resolved "https://registry.npmmirror.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d"
+  integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==
+
+rollup@^4.20.0:
+  version "4.56.0"
+  resolved "https://registry.npmmirror.com/rollup/-/rollup-4.56.0.tgz#65959d13cfbd7e48b8868c05165b1738f0143862"
+  integrity sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg==
+  dependencies:
+    "@types/estree" "1.0.8"
+  optionalDependencies:
+    "@rollup/rollup-android-arm-eabi" "4.56.0"
+    "@rollup/rollup-android-arm64" "4.56.0"
+    "@rollup/rollup-darwin-arm64" "4.56.0"
+    "@rollup/rollup-darwin-x64" "4.56.0"
+    "@rollup/rollup-freebsd-arm64" "4.56.0"
+    "@rollup/rollup-freebsd-x64" "4.56.0"
+    "@rollup/rollup-linux-arm-gnueabihf" "4.56.0"
+    "@rollup/rollup-linux-arm-musleabihf" "4.56.0"
+    "@rollup/rollup-linux-arm64-gnu" "4.56.0"
+    "@rollup/rollup-linux-arm64-musl" "4.56.0"
+    "@rollup/rollup-linux-loong64-gnu" "4.56.0"
+    "@rollup/rollup-linux-loong64-musl" "4.56.0"
+    "@rollup/rollup-linux-ppc64-gnu" "4.56.0"
+    "@rollup/rollup-linux-ppc64-musl" "4.56.0"
+    "@rollup/rollup-linux-riscv64-gnu" "4.56.0"
+    "@rollup/rollup-linux-riscv64-musl" "4.56.0"
+    "@rollup/rollup-linux-s390x-gnu" "4.56.0"
+    "@rollup/rollup-linux-x64-gnu" "4.56.0"
+    "@rollup/rollup-linux-x64-musl" "4.56.0"
+    "@rollup/rollup-openbsd-x64" "4.56.0"
+    "@rollup/rollup-openharmony-arm64" "4.56.0"
+    "@rollup/rollup-win32-arm64-msvc" "4.56.0"
+    "@rollup/rollup-win32-ia32-msvc" "4.56.0"
+    "@rollup/rollup-win32-x64-gnu" "4.56.0"
+    "@rollup/rollup-win32-x64-msvc" "4.56.0"
+    fsevents "~2.3.2"
+
+sass@^1.69.5:
+  version "1.97.3"
+  resolved "https://registry.npmmirror.com/sass/-/sass-1.97.3.tgz#9cb59339514fa7e2aec592b9700953ac6e331ab2"
+  integrity sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==
+  dependencies:
+    chokidar "^4.0.0"
+    immutable "^5.0.2"
+    source-map-js ">=0.6.2 <2.0.0"
+  optionalDependencies:
+    "@parcel/watcher" "^2.4.1"
+
+"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
+  integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
+
+vite@^5.0.10:
+  version "5.4.21"
+  resolved "https://registry.npmmirror.com/vite/-/vite-5.4.21.tgz#84a4f7c5d860b071676d39ba513c0d598fdc7027"
+  integrity sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==
+  dependencies:
+    esbuild "^0.21.3"
+    postcss "^8.4.43"
+    rollup "^4.20.0"
+  optionalDependencies:
+    fsevents "~2.3.3"
+
+vue-demi@>=0.14.8, vue-demi@^0.14.10:
+  version "0.14.10"
+  resolved "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz#afc78de3d6f9e11bf78c55e8510ee12814522f04"
+  integrity sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==
+
+vue-router@^4.2.5:
+  version "4.6.4"
+  resolved "https://registry.npmmirror.com/vue-router/-/vue-router-4.6.4.tgz#a0a9cb9ef811a106d249e4bb9313d286718020d8"
+  integrity sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==
+  dependencies:
+    "@vue/devtools-api" "^6.6.4"
+
+vue@^3.4.0:
+  version "3.5.27"
+  resolved "https://registry.npmmirror.com/vue/-/vue-3.5.27.tgz#e55fd941b614459ab2228489bc19d1692e05876c"
+  integrity sha512-aJ/UtoEyFySPBGarREmN4z6qNKpbEguYHMmXSiOGk69czc+zhs0NF6tEFrY8TZKAl8N/LYAkd4JHVd5E/AsSmw==
+  dependencies:
+    "@vue/compiler-dom" "3.5.27"
+    "@vue/compiler-sfc" "3.5.27"
+    "@vue/runtime-dom" "3.5.27"
+    "@vue/server-renderer" "3.5.27"
+    "@vue/shared" "3.5.27"

+ 84 - 0
scripts/install-services.sh

@@ -0,0 +1,84 @@
+#!/bin/bash
+
+# ============================================
+# 灵越智报 - Systemd 服务安装脚本
+# ============================================
+
+set -e
+
+# 颜色定义
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+NC='\033[0m'
+
+log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
+log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
+log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
+
+# 获取脚本所在目录
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+SYSTEMD_DIR="${SCRIPT_DIR}/systemd"
+LOG_DIR="/var/log/lingyue"
+
+# 检查 root 权限
+if [ "$EUID" -ne 0 ]; then
+    log_error "请使用 root 用户运行此脚本"
+    log_info "使用: sudo $0"
+    exit 1
+fi
+
+# 创建日志目录
+log_info "创建日志目录: ${LOG_DIR}"
+mkdir -p ${LOG_DIR}
+chmod 755 ${LOG_DIR}
+
+# 复制服务文件
+log_info "复制服务文件到 /etc/systemd/system/"
+cp ${SYSTEMD_DIR}/lingyue-starter.service /etc/systemd/system/
+cp ${SYSTEMD_DIR}/lingyue-extract.service /etc/systemd/system/
+cp ${SYSTEMD_DIR}/lingyue-ner.service /etc/systemd/system/
+
+# 重新加载 systemd
+log_info "重新加载 systemd 配置"
+systemctl daemon-reload
+
+# 询问是否启用服务
+echo ""
+echo "是否启用服务开机自启?"
+echo "1) lingyue-starter (主应用,端口 5232)"
+echo "2) lingyue-extract (模板服务,端口 8086)"
+echo "3) lingyue-ner (NER 服务,端口 8001)"
+echo ""
+
+read -p "启用 lingyue-starter? [y/N]: " enable_starter
+if [[ "$enable_starter" =~ ^[Yy]$ ]]; then
+    systemctl enable lingyue-starter
+    log_info "lingyue-starter 已启用开机自启"
+fi
+
+read -p "启用 lingyue-extract? [y/N]: " enable_extract
+if [[ "$enable_extract" =~ ^[Yy]$ ]]; then
+    systemctl enable lingyue-extract
+    log_info "lingyue-extract 已启用开机自启"
+fi
+
+read -p "启用 lingyue-ner? [y/N]: " enable_ner
+if [[ "$enable_ner" =~ ^[Yy]$ ]]; then
+    systemctl enable lingyue-ner
+    log_info "lingyue-ner 已启用开机自启"
+fi
+
+echo ""
+log_info "安装完成!"
+echo ""
+echo "常用命令:"
+echo "  查看状态: systemctl status lingyue-starter"
+echo "  启动服务: systemctl start lingyue-starter"
+echo "  停止服务: systemctl stop lingyue-starter"
+echo "  查看日志: journalctl -u lingyue-starter -f"
+echo ""
+echo "服务端口:"
+echo "  lingyue-starter: 5232"
+echo "  lingyue-extract: 8086"
+echo "  lingyue-ner:     8001"

+ 144 - 0
scripts/systemd/README.md

@@ -0,0 +1,144 @@
+# Systemd 服务配置
+
+本目录包含灵越智报各服务的 systemd 配置文件。
+
+## 服务列表
+
+| 服务文件 | 服务名 | 端口 | 说明 |
+|---------|--------|------|------|
+| `lingyue-starter.service` | lingyue-starter | 5232 | Java 主应用(单体启动器) |
+| `lingyue-extract.service` | lingyue-extract | 8086 | 模板系统服务 |
+| `lingyue-ner.service` | lingyue-ner | 8001 | NER Python 服务 |
+
+## 安装步骤
+
+### 1. 复制服务文件到 systemd 目录
+
+```bash
+sudo cp scripts/systemd/*.service /etc/systemd/system/
+```
+
+### 2. 重新加载 systemd 配置
+
+```bash
+sudo systemctl daemon-reload
+```
+
+### 3. 启用服务(开机自启)
+
+```bash
+# 启用主应用
+sudo systemctl enable lingyue-starter
+
+# 启用模板服务(可选,如果单独运行)
+sudo systemctl enable lingyue-extract
+
+# 启用 NER 服务
+sudo systemctl enable lingyue-ner
+```
+
+### 4. 启动服务
+
+```bash
+# 启动主应用
+sudo systemctl start lingyue-starter
+
+# 启动 NER 服务
+sudo systemctl start lingyue-ner
+```
+
+## 常用命令
+
+```bash
+# 查看服务状态
+sudo systemctl status lingyue-starter
+sudo systemctl status lingyue-extract
+sudo systemctl status lingyue-ner
+
+# 启动服务
+sudo systemctl start lingyue-starter
+
+# 停止服务
+sudo systemctl stop lingyue-starter
+
+# 重启服务
+sudo systemctl restart lingyue-starter
+
+# 查看实时日志
+sudo journalctl -u lingyue-starter -f
+
+# 查看最近 100 行日志
+sudo journalctl -u lingyue-starter -n 100
+```
+
+## 日志位置
+
+日志文件存储在 `/var/log/lingyue/` 目录:
+
+```
+/var/log/lingyue/
+├── lingyue-starter.log        # 主应用日志
+├── lingyue-starter-error.log  # 主应用错误日志
+├── extract-service.log        # 模板服务日志
+├── extract-service-error.log  # 模板服务错误日志
+├── ner-service.log            # NER 服务日志
+└── ner-service-error.log      # NER 服务错误日志
+```
+
+创建日志目录:
+
+```bash
+sudo mkdir -p /var/log/lingyue
+sudo chmod 755 /var/log/lingyue
+```
+
+## 修改配置
+
+如需修改配置(如端口、数据库连接等),编辑对应的 `.service` 文件中的 `Environment` 部分:
+
+```ini
+# 数据库配置
+Environment=DB_HOST=localhost
+Environment=DB_PORT=5432
+Environment=DB_NAME=lingyue_zhibao
+Environment=DB_USERNAME=lingyue
+Environment=DB_PASSWORD=123123
+```
+
+修改后重新加载并重启:
+
+```bash
+sudo systemctl daemon-reload
+sudo systemctl restart lingyue-starter
+```
+
+## 健康检查
+
+```bash
+# 主应用健康检查
+curl http://localhost:5232/actuator/health
+
+# 模板服务健康检查
+curl http://localhost:8086/api/v1/extract/health
+
+# NER 服务健康检查
+curl http://localhost:8001/health
+```
+
+## 一键脚本
+
+也可以使用项目根目录的 `server-deploy.sh` 进行管理:
+
+```bash
+# 查看状态
+./server-deploy.sh status
+
+# 启动所有服务
+./server-deploy.sh start
+
+# 停止所有服务
+./server-deploy.sh stop
+
+# 重启
+./server-deploy.sh restart
+```

+ 39 - 0
scripts/systemd/lingyue-extract.service

@@ -0,0 +1,39 @@
+[Unit]
+Description=Lingyue Zhibao - Extract Service (Template System)
+Documentation=https://code.salesmap.tech/hewensong/lingyue-zhibao
+After=network.target postgresql.service
+
+[Service]
+Type=simple
+User=root
+WorkingDirectory=/mnt/win_home/lingyue-zhibao/backend
+
+# Java 启动命令
+ExecStart=/usr/bin/java \
+    -Xms512m -Xmx1g \
+    -XX:+UseG1GC \
+    -Dfile.encoding=UTF-8 \
+    -Dserver.port=8086 \
+    -jar extract-service/target/extract-service.jar
+
+# 重启策略
+Restart=always
+RestartSec=10
+
+# 日志
+StandardOutput=append:/var/log/lingyue/extract-service.log
+StandardError=append:/var/log/lingyue/extract-service-error.log
+
+# 环境变量
+Environment=JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
+Environment=SPRING_PROFILES_ACTIVE=prod
+
+# 数据库配置
+Environment=DB_HOST=localhost
+Environment=DB_PORT=5432
+Environment=DB_NAME=lingyue_zhibao
+Environment=DB_USERNAME=lingyue
+Environment=DB_PASSWORD=123123
+
+[Install]
+WantedBy=multi-user.target

+ 34 - 0
scripts/systemd/lingyue-ner.service

@@ -0,0 +1,34 @@
+[Unit]
+Description=Lingyue Zhibao - NER Python Service
+Documentation=https://code.salesmap.tech/hewensong/lingyue-zhibao
+After=network.target
+
+[Service]
+Type=simple
+User=root
+WorkingDirectory=/mnt/win_home/lingyue-zhibao/python-services/ner-service
+
+# 使用虚拟环境启动
+ExecStart=/mnt/win_home/lingyue-zhibao/python-services/ner-service/venv/bin/uvicorn \
+    app.main:app \
+    --host 0.0.0.0 \
+    --port 8001
+
+# 重启策略
+Restart=always
+RestartSec=10
+
+# 日志
+StandardOutput=append:/var/log/lingyue/ner-service.log
+StandardError=append:/var/log/lingyue/ner-service-error.log
+
+# 环境变量
+Environment=NER_MODEL=rule
+Environment=LOG_LEVEL=INFO
+Environment=MAX_TEXT_LENGTH=50000
+
+# DeepSeek API (如需使用)
+# Environment=DEEPSEEK_API_KEY=your_api_key_here
+
+[Install]
+WantedBy=multi-user.target

+ 53 - 0
scripts/systemd/lingyue-starter.service

@@ -0,0 +1,53 @@
+[Unit]
+Description=Lingyue Zhibao - Main Application (lingyue-starter)
+Documentation=https://code.salesmap.tech/hewensong/lingyue-zhibao
+After=network.target postgresql.service redis-server.service rabbitmq-server.service
+
+[Service]
+Type=simple
+User=root
+WorkingDirectory=/mnt/win_home/lingyue-zhibao/backend
+
+# Java 启动命令
+ExecStart=/usr/bin/java \
+    -Xms1g -Xmx2g \
+    -XX:+UseG1GC \
+    -XX:+HeapDumpOnOutOfMemoryError \
+    -XX:HeapDumpPath=/var/log/lingyue/heapdump.hprof \
+    -Dfile.encoding=UTF-8 \
+    -jar lingyue-starter/target/lingyue-starter.jar
+
+# 重启策略
+Restart=always
+RestartSec=10
+
+# 日志
+StandardOutput=append:/var/log/lingyue/lingyue-starter.log
+StandardError=append:/var/log/lingyue/lingyue-starter-error.log
+
+# 环境变量
+Environment=JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
+Environment=SPRING_PROFILES_ACTIVE=prod
+
+# 数据库配置
+Environment=DB_HOST=localhost
+Environment=DB_PORT=5432
+Environment=DB_NAME=lingyue_zhibao
+Environment=DB_USERNAME=lingyue
+Environment=DB_PASSWORD=123123
+
+# Redis 配置
+Environment=REDIS_HOST=localhost
+Environment=REDIS_PORT=6379
+
+# RabbitMQ 配置
+Environment=RABBITMQ_HOST=localhost
+Environment=RABBITMQ_PORT=5672
+Environment=RABBITMQ_USERNAME=admin
+Environment=RABBITMQ_PASSWORD=admin123
+
+# NER 服务配置
+Environment=NER_SERVICE_URL=http://localhost:8001
+
+[Install]
+WantedBy=multi-user.target