# finance-front **Repository Path**: lanjing12138/finance-front ## Basic Information - **Project Name**: finance-front - **Description**: 未来智财-PC端 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-05-20 - **Last Updated**: 2026-05-20 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 未来智财 · 开发规格文档 > **应用名称**:未来智财 > **Slogan**:你的私人财务助理——基于 Dify 智能体工作流的企业级对话应用 > **技术框架**:Electron(桌面端)+ React(渲染层) > **目标平台**:Windows / macOS > **文档用途**:交付 Claude Code 完整实现 --- ## 目录 1. [项目概览](#1-项目概览) 2. [技术栈与依赖](#2-技术栈与依赖) 3. [目录结构](#3-目录结构) 4. [功能模块详述](#4-功能模块详述) - 4.1 登录界面 - 4.2 主界面布局 - 4.3 智能体对话窗口 - 4.4 多模态输入框 - 4.5 Dify API 对接 - 4.6 历史对话管理 - 4.7 用户头像与菜单 - 4.8 设置窗口 - 4.9 主题切换 5. [数据模型](#5-数据模型) 6. [IPC 通信协议](#6-ipc-通信协议) 7. [Dify API 集成规范](#7-dify-api-集成规范) 8. [UI/UX 设计规范](#8-uiux-设计规范) 9. [构建与打包](#9-构建与打包) 10. [开发任务清单](#10-开发任务清单) --- ## 1. 项目概览 ### 1.1 产品定位 「未来智财」是一款面向企业用户的桌面端财务智能体对话应用。用户通过文字、语音、文档、图片等多模态方式向财务智能体提问,应用将请求转发至后端 Dify 工作流,并以流式方式展示回复内容。 ### 1.2 核心功能 | 功能 | 描述 | |------|------| | 账号登录 | 启动弹出登录对话框,UI 展示,接口预留,可跳过 | | 智能体对话 | 主界面聊天窗口,支持流式输出 | | 多模态输入 | 文字 / 语音 / 文档(doc/docx/xls/xlsx/ppt/pptx)/ 图片(jpg/png 等) | | Dify 对接 | 转发至可配置的 Dify API,流式接收回复 | | 历史对话 | 左侧栏展示历史列表,点击切换,支持新建对话 | | 主题切换 | 亮色 / 暗色模式,手动或跟随系统 | | 用户菜单 | 左下角头像,点击展开退出登录 / 设置 | | 设置窗口 | 开机自启、Dify API 配置、关于信息 | --- ## 2. 技术栈与依赖 ### 2.1 核心依赖 ```json { "dependencies": { "electron": "^30.0.0", "react": "^18.3.0", "react-dom": "^18.3.0", "electron-store": "^10.0.0", "electron-updater": "^6.1.0", "marked": "^12.0.0", "highlight.js": "^11.9.0", "uuid": "^10.0.0" }, "devDependencies": { "@vitejs/plugin-react": "^4.3.0", "vite": "^5.4.0", "electron-builder": "^24.13.0", "concurrently": "^8.2.0" } } ``` ### 2.2 关键 Electron API 参考官方文档: - 主进程:`app`, `BrowserWindow`, `ipcMain`, `shell`, `dialog`, `Tray`, `Menu` - 渲染进程:`ipcRenderer`, `contextBridge` - 系统集成:`nativeTheme`(主题)、`app.setLoginItemSettings`(开机自启) --- ## 3. 目录结构 ``` 未来智财/ ├── package.json ├── vite.config.js # Vite 渲染层配置 ├── electron.vite.config.js # electron-vite 配置(可选) │ ├── electron/ # 主进程代码 │ ├── main.js # 入口:创建窗口、注册 IPC │ ├── preload.js # contextBridge 暴露 API │ ├── store.js # electron-store 封装 │ ├── dify.js # Dify API 请求(流式) │ ├── tray.js # 系统托盘(可选) │ └── updater.js # 自动更新(可选) │ ├── src/ # 渲染进程(React) │ ├── main.jsx # React 入口 │ ├── App.jsx # 根组件:路由 Login / Main │ ├── index.css # 全局样式 + CSS 变量 │ │ │ ├── pages/ │ │ ├── LoginPage.jsx # 登录界面 │ │ └── MainPage.jsx # 主界面 │ │ │ ├── components/ │ │ ├── Sidebar/ │ │ │ ├── Sidebar.jsx # 左侧栏容器 │ │ │ ├── ConversationList.jsx # 历史对话列表 │ │ │ └── UserAvatar.jsx # 用户头像 + 菜单 │ │ │ │ │ ├── Chat/ │ │ │ ├── ChatWindow.jsx # 对话消息列表 │ │ │ ├── MessageBubble.jsx # 单条消息气泡 │ │ │ └── TypingIndicator.jsx # 流式输出中动效 │ │ │ │ │ ├── Input/ │ │ │ ├── InputBar.jsx # 输入框主容器 │ │ │ ├── VoiceButton.jsx # 语音输入按钮 │ │ │ └── FileUploader.jsx # 文件上传按钮 │ │ │ │ │ └── Settings/ │ │ ├── SettingsModal.jsx # 设置弹窗 │ │ ├── DifyConfig.jsx # Dify 配置面板 │ │ └── AboutPanel.jsx # 关于面板 │ │ │ ├── store/ │ │ ├── useAppStore.js # Zustand 全局状态(或 useContext) │ │ └── useTheme.js # 主题状态管理 │ │ │ └── utils/ │ ├── markdown.js # Markdown 渲染工具 │ └── fileHelper.js # 文件类型校验工具 │ ├── assets/ │ ├── icon.png # 应用图标 │ └── logo.svg # 品牌 Logo │ └── dist/ # 构建输出(gitignore) ``` --- ## 4. 功能模块详述 ### 4.1 登录界面 **触发时机**:应用启动后,在主窗口显示前弹出登录对话框(`BrowserWindow` modal 或渲染层覆盖层)。 **UI 元素**: - 应用 Logo + 名称「未来智财」 - Slogan 文字 - 用户名输入框(`placeholder="请输入账号"`) - 密码输入框(`type="password"`,`placeholder="请输入密码"`) - 「登录」按钮(点击后提示"功能开发中",直接跳转主界面) - 「跳过,直接进入」文字链接(点击直接进入主界面) - 底部版权信息 **实现要点**: ```jsx // LoginPage.jsx 核心逻辑 const handleLogin = () => { // 接口预留,暂不调用后端 // TODO: 调用真实登录接口 onLoginSuccess({ username, avatar: null }); }; const handleSkip = () => { onLoginSuccess({ username: '访客', avatar: null }); }; ``` **状态持久化**:登录状态保存至 `electron-store`,下次启动时若已登录则直接跳过登录页(可在设置中退出登录重置)。 --- ### 4.2 主界面布局 ``` ┌─────────────────────────────────────────────────────┐ │ 标题栏(自定义 frameless + 拖拽区域) │ ├────────────┬────────────────────────────────────────┤ │ │ 顶部:Logo + Slogan + 主题切换按钮 │ │ 左侧栏 ├────────────────────────────────────────┤ │ (240px) │ │ │ │ 对话消息列表区域 │ │ 历史对话 │ (可滚动,flex-grow) │ │ 列表 │ │ │ ├────────────────────────────────────────┤ │ │ 输入框区域(固定底部) │ ├────────────┤ │ │ 用户头像 │ │ │ + 菜单 │ │ └────────────┴────────────────────────────────────────┘ ``` **窗口配置**(`electron/main.js`): ```javascript const mainWindow = new BrowserWindow({ width: 1200, height: 800, minWidth: 900, minHeight: 600, frame: false, // 自定义标题栏 titleBarStyle: 'hidden', trafficLightPosition: { x: 16, y: 16 }, // macOS 红绿灯位置 webPreferences: { preload: path.join(__dirname, 'preload.js'), contextIsolation: true, nodeIntegration: false, }, show: false, // 加载完成后再显示,避免白屏 }); mainWindow.once('ready-to-show', () => mainWindow.show()); ``` --- ### 4.3 智能体对话窗口 **ChatWindow.jsx 功能**: - 消息列表竖向排列,用户消息靠右,AI 消息靠左 - 支持 Markdown 渲染(代码高亮、表格、列表) - 流式输出时末尾显示打字光标动效(`|` 闪烁) - 新消息自动滚动到底部(`scrollIntoView`) - 空对话时展示欢迎语和功能提示卡片 **MessageBubble.jsx 属性**: ```typescript interface MessageBubbleProps { role: 'user' | 'assistant'; content: string; // 支持 Markdown files?: UploadedFile[]; // 附带文件列表 timestamp: number; isStreaming?: boolean; // 是否正在流式输出 } ``` **Markdown 渲染**: ```javascript // utils/markdown.js import { marked } from 'marked'; import hljs from 'highlight.js'; marked.setOptions({ highlight: (code, lang) => { const language = hljs.getLanguage(lang) ? lang : 'plaintext'; return hljs.highlight(code, { language }).value; }, breaks: true, }); export const renderMarkdown = (text) => marked.parse(text); ``` --- ### 4.4 多模态输入框 **InputBar.jsx 布局**: ``` ┌──────────────────────────────────────────────────┐ │ 已上传文件预览(横向滚动缩略图列表) │ ├──────────────────────────────────────────────────┤ │ [📎] [🎤] 文本输入区(自动扩展高度) [发送▶] │ └──────────────────────────────────────────────────┘ ``` **支持的输入类型**: | 类型 | 说明 | |------|------| | 文本 | `textarea`,`Shift+Enter` 换行,`Enter` 发送 | | 语音 | 点击麦克风按钮,使用 Web Speech API 转文字,回填输入框 | | 文件上传 | 点击附件按钮,打开文件选择对话框 | | 拖放 | 拖拽文件到输入框区域自动上传 | **文件类型白名单**(`utils/fileHelper.js`): ```javascript export const ALLOWED_EXTENSIONS = [ // 文档 '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', // 图片 '.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', ]; export const MAX_FILE_SIZE_MB = 20; // 单文件最大 20MB export const validateFile = (file) => { const ext = path.extname(file.name).toLowerCase(); if (!ALLOWED_EXTENSIONS.includes(ext)) { return { valid: false, error: `不支持的文件类型:${ext}` }; } if (file.size > MAX_FILE_SIZE_MB * 1024 * 1024) { return { valid: false, error: `文件超过 ${MAX_FILE_SIZE_MB}MB 限制` }; } return { valid: true }; }; ``` **语音输入实现**(`VoiceButton.jsx`): ```javascript const startRecording = () => { const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; if (!SpeechRecognition) { alert('当前浏览器不支持语音输入'); return; } const recognition = new SpeechRecognition(); recognition.lang = 'zh-CN'; recognition.continuous = false; recognition.interimResults = false; recognition.onresult = (e) => { const transcript = e.results[0][0].transcript; onTranscript(transcript); // 回填到输入框 }; recognition.start(); setIsRecording(true); }; ``` --- ### 4.5 Dify API 对接 #### 4.5.1 主进程侧实现(`electron/dify.js`) 所有 HTTP 请求在**主进程**发起(规避渲染进程 CORS 限制)。 ```javascript // electron/dify.js const { net } = require('electron'); /** * 发送对话请求并通过 IPC 流式推送内容到渲染进程 * @param {BrowserWindow} win - 主窗口 * @param {Object} config - { baseUrl, apiKey } * @param {Object} payload - { query, conversationId, files, userId } */ async function sendDifyMessage(win, config, payload) { const { baseUrl, apiKey } = config; const url = `${baseUrl}/v1/chat-messages`; const body = { inputs: {}, query: payload.query, response_mode: 'streaming', conversation_id: payload.conversationId || '', user: payload.userId || 'electron-user', files: payload.files || [], }; const request = net.request({ method: 'POST', url, headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json', }, }); request.on('response', (response) => { let buffer = ''; response.on('data', (chunk) => { buffer += chunk.toString(); const lines = buffer.split('\n'); buffer = lines.pop(); // 保留不完整行 for (const line of lines) { if (line.startsWith('data: ')) { const data = line.slice(6).trim(); if (data === '[DONE]') { win.webContents.send('dify:stream-done'); return; } try { const parsed = JSON.parse(data); // Dify 流式事件类型 if (parsed.event === 'message') { win.webContents.send('dify:stream-chunk', { answer: parsed.answer, conversationId: parsed.conversation_id, messageId: parsed.message_id, }); } else if (parsed.event === 'message_end') { win.webContents.send('dify:stream-done', { conversationId: parsed.conversation_id, metadata: parsed.metadata, }); } else if (parsed.event === 'error') { win.webContents.send('dify:stream-error', parsed); } } catch (e) { // 忽略解析错误 } } } }); response.on('error', (err) => { win.webContents.send('dify:stream-error', { message: err.message }); }); }); request.on('error', (err) => { win.webContents.send('dify:stream-error', { message: err.message }); }); request.write(JSON.stringify(body)); request.end(); } /** * 上传文件到 Dify 获取 file_id */ async function uploadFileToDify(config, filePath, mimeType) { // 使用 form-data 上传,返回 { id, name, size, mime_type } // 实现略,使用 net.request 发送 multipart/form-data } module.exports = { sendDifyMessage, uploadFileToDify }; ``` #### 4.5.2 IPC 注册(`electron/main.js`) ```javascript const { sendDifyMessage, uploadFileToDify } = require('./dify'); const store = require('./store'); // 发送消息 ipcMain.handle('dify:send', async (event, payload) => { const config = store.get('difyConfig'); if (!config?.baseUrl || !config?.apiKey) { return { error: 'Dify 未配置,请在设置中填写 API 地址和 Key' }; } await sendDifyMessage(mainWindow, config, payload); return { ok: true }; }); // 上传文件 ipcMain.handle('dify:upload-file', async (event, { filePath, mimeType }) => { const config = store.get('difyConfig'); return await uploadFileToDify(config, filePath, mimeType); }); // 停止生成(发送 stop 请求到 Dify) ipcMain.handle('dify:stop', async (event, { taskId }) => { const config = store.get('difyConfig'); // POST /v1/chat-messages/:task_id/stop }); ``` #### 4.5.3 渲染进程监听(`useAppStore.js`) ```javascript // 启动流式监听 useEffect(() => { const removeChunk = window.electronAPI.on('dify:stream-chunk', (data) => { setMessages(prev => { const last = prev[prev.length - 1]; if (last?.role === 'assistant' && last.isStreaming) { return [...prev.slice(0, -1), { ...last, content: last.content + data.answer }]; } return prev; }); }); const removeDone = window.electronAPI.on('dify:stream-done', (data) => { setMessages(prev => { const last = prev[prev.length - 1]; if (last?.role === 'assistant') { return [...prev.slice(0, -1), { ...last, isStreaming: false }]; } return prev; }); if (data?.conversationId) setCurrentConversationId(data.conversationId); }); return () => { removeChunk(); removeDone(); }; }, []); ``` --- ### 4.6 历史对话管理 **数据来源**:对话列表持久化在 `electron-store`,每次对话完成后同步写入。 **Sidebar.jsx 功能**: - 顶部「+ 新建对话」按钮 - 历史对话列表(`ConversationList.jsx`) - 每项显示:对话标题(取第一条用户消息前 20 字)、时间 - 当前激活项高亮 - 右键或悬浮显示「删除」按钮 - 列表可滚动,超出高度时显示滚动条 **对话状态管理**: ```javascript // store/useAppStore.js(Zustand 示例) const useAppStore = create((set, get) => ({ conversations: [], // 历史对话列表 currentConversationId: null,// 当前对话 ID messages: [], // 当前对话消息 createNewConversation: () => { const id = uuid(); const newConv = { id, title: '新对话', createdAt: Date.now(), messages: [] }; set(state => ({ conversations: [newConv, ...state.conversations], currentConversationId: id, messages: [], })); window.electronAPI.saveConversations([newConv, ...get().conversations]); }, switchConversation: (id) => { const conv = get().conversations.find(c => c.id === id); set({ currentConversationId: id, messages: conv?.messages || [] }); }, deleteConversation: (id) => { const updated = get().conversations.filter(c => c.id !== id); set({ conversations: updated }); if (get().currentConversationId === id) { set({ currentConversationId: null, messages: [] }); } window.electronAPI.saveConversations(updated); }, })); ``` --- ### 4.7 用户头像及菜单 **UserAvatar.jsx**: - 固定在左侧栏底部 - 显示圆形头像(暂用首字母占位,颜色随机固定) - 显示用户名(截断超长文字) - 点击弹出 Popover 菜单: - ⚙️ 设置 - 🚪 退出登录 ```jsx const UserAvatar = ({ user, onOpenSettings, onLogout }) => { const initial = user.username?.[0]?.toUpperCase() || 'U'; return (