fix(common): 🐛 modify the location of the remote login window
fix packaging failure due to insufficient memory in js and node
This commit is contained in:
6
.github/workflows/debug-build.yml
vendored
6
.github/workflows/debug-build.yml
vendored
@@ -55,9 +55,13 @@ jobs:
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
run: pnpm install
|
||||
|
||||
- name: Generate component typings
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
run: pnpm vite build --emptyOutDir
|
||||
|
||||
- name: Build Vite + Tauri
|
||||
@@ -81,5 +85,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||
# 增加 Node.js 内存限制,避免构建时内存溢出
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
with:
|
||||
releaseId: "debug-build"
|
||||
|
||||
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -38,6 +38,8 @@ jobs:
|
||||
run: pnpm install
|
||||
|
||||
- name: Generate frontend build (typings & assets)
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
run: pnpm vite build --emptyOutDir
|
||||
|
||||
- name: Upload generated component typings
|
||||
@@ -109,6 +111,8 @@ jobs:
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
run: pnpm install
|
||||
|
||||
# 安装 Rust
|
||||
@@ -131,6 +135,8 @@ jobs:
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
# 使用之前配置的私钥密码
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||
# 增加 Node.js 内存限制,避免构建时内存溢出
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
with:
|
||||
tagName: v__VERSION__ #这个动作会自动将\_\_VERSION\_\_替换为app version
|
||||
releaseName: 'v__VERSION__'
|
||||
|
||||
5
.vscode/extensions.json
vendored
5
.vscode/extensions.json
vendored
@@ -7,6 +7,9 @@
|
||||
"tauri-apps.tauri-vscode",
|
||||
"1yib.rust-bundle",
|
||||
"ms-vscode.vscode-typescript-next",
|
||||
"biomejs.biome"
|
||||
"biomejs.biome",
|
||||
"github.vscode-github-actions",
|
||||
"fill-labs.dependi",
|
||||
"usernamehw.errorlens"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,249 +0,0 @@
|
||||
# AI 流式数据渲染完成
|
||||
|
||||
## ✅ 已完成的功能
|
||||
|
||||
### 1. 后端 SSE 数据接收 ✅
|
||||
- Rust 后端成功接收 SSE 流式数据
|
||||
- 正确解析服务器返回的 JSON 格式数据
|
||||
- 通过 Tauri 事件系统发送到前端
|
||||
|
||||
### 2. 前端数据解析 ✅
|
||||
- 前端成功接收 Tauri 事件
|
||||
- 正确解析 JSON 数据,提取 `data.receive.content` 字段
|
||||
- 实时更新消息内容
|
||||
|
||||
### 3. UI 实时渲染 ✅
|
||||
- 添加消息列表显示
|
||||
- 实时更新 AI 回复内容
|
||||
- 流式光标动画效果
|
||||
- 自动滚动到底部
|
||||
|
||||
## 📊 数据流程
|
||||
|
||||
```
|
||||
服务器 SSE 流
|
||||
↓
|
||||
data:{"success":true,"data":{"receive":{"content":"JavaScript"}}}
|
||||
↓
|
||||
Rust 后端接收并解析
|
||||
↓
|
||||
emit('ai-stream-event', { data: JSON字符串 })
|
||||
↓
|
||||
前端 listen('ai-stream-event')
|
||||
↓
|
||||
解析 JSON.parse(chunk)
|
||||
↓
|
||||
提取 data.data.receive.content
|
||||
↓
|
||||
更新 messageList[index].content
|
||||
↓
|
||||
Vue 响应式更新 UI
|
||||
↓
|
||||
实时显示在聊天界面
|
||||
```
|
||||
|
||||
## 🎨 UI 功能
|
||||
|
||||
### 消息显示
|
||||
- ✅ 用户消息:右对齐,绿色气泡
|
||||
- ✅ AI 消息:左对齐,默认气泡
|
||||
- ✅ 流式光标:闪烁的光标效果 `▋`
|
||||
- ✅ 自动滚动:新消息自动滚动到底部
|
||||
|
||||
### 消息结构
|
||||
```typescript
|
||||
interface Message {
|
||||
type: 'user' | 'assistant'
|
||||
content: string
|
||||
streaming?: boolean // 是否正在流式输出
|
||||
timestamp?: number
|
||||
}
|
||||
```
|
||||
|
||||
## 🔧 关键代码修改
|
||||
|
||||
### Chat.vue 主要改动
|
||||
|
||||
1. **添加消息列表状态**
|
||||
```typescript
|
||||
const messageList = ref<Message[]>([])
|
||||
const chatContainerRef = ref<HTMLElement | null>(null)
|
||||
```
|
||||
|
||||
2. **实时更新消息内容**
|
||||
```typescript
|
||||
onChunk: (chunk: string) => {
|
||||
try {
|
||||
const data = JSON.parse(chunk)
|
||||
if (data.success && data.data?.receive?.content) {
|
||||
messageList.value[aiMessageIndex].content = data.data.receive.content
|
||||
scrollToBottom()
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('解析JSON失败:', e)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **消息列表渲染**
|
||||
```vue
|
||||
<template v-for="(message, index) in messageList" :key="index">
|
||||
<!-- 用户消息 -->
|
||||
<n-flex v-if="message.type === 'user'" :size="6" justify="end">
|
||||
...
|
||||
</n-flex>
|
||||
|
||||
<!-- AI消息 -->
|
||||
<n-flex v-else :size="6">
|
||||
<div class="bubble">
|
||||
<span v-if="message.streaming" class="streaming-cursor">
|
||||
{{ message.content }}
|
||||
</span>
|
||||
<span v-else>{{ message.content }}</span>
|
||||
</div>
|
||||
</n-flex>
|
||||
</template>
|
||||
```
|
||||
|
||||
4. **流式光标动画**
|
||||
```css
|
||||
.streaming-cursor::after {
|
||||
content: '▋';
|
||||
animation: blink 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 50% { opacity: 1; }
|
||||
51%, 100% { opacity: 0; }
|
||||
}
|
||||
```
|
||||
|
||||
## 📝 服务器数据格式
|
||||
|
||||
服务器返回的 SSE 数据格式:
|
||||
```
|
||||
data:{"success":true,"code":200,"data":{"send":{"id":"89799684200960","type":"user","content":"什么是js"},"receive":{"id":"89799684200961","type":"assistant","content":"JavaScript"}},"msg":"","path":null,"version":null,"timestamp":"1761736321008"}
|
||||
|
||||
data:{"success":true,"code":200,"data":{"send":{"id":"89799684200960","type":"user","content":"什么是js"},"receive":{"id":"89799684200961","type":"assistant","content":"非常适合"}},"msg":"","path":null,"version":null,"timestamp":"1761736321008"}
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
每个数据块包含:
|
||||
- `success`: 是否成功
|
||||
- `data.send`: 用户发送的消息
|
||||
- `data.receive.content`: AI 回复的内容(逐步累积)
|
||||
- `timestamp`: 时间戳
|
||||
|
||||
## 🎯 测试步骤
|
||||
|
||||
1. **启动应用**
|
||||
```bash
|
||||
pnpm tauri dev
|
||||
```
|
||||
|
||||
2. **打开 ChatBot 窗口**
|
||||
- 点击左侧 Robot 插件图标
|
||||
|
||||
3. **选择 AI 模型**
|
||||
- 点击"当前模型"标签
|
||||
- 选择一个可用的模型
|
||||
|
||||
4. **发送消息**
|
||||
- 在输入框输入问题,例如:"什么是 JavaScript?"
|
||||
- 点击发送或按回车
|
||||
|
||||
5. **观察效果**
|
||||
- ✅ 用户消息立即显示在右侧
|
||||
- ✅ AI 消息占位符出现在左侧
|
||||
- ✅ AI 回复内容逐字逐句实时更新
|
||||
- ✅ 流式光标闪烁效果
|
||||
- ✅ 自动滚动到最新消息
|
||||
- ✅ 流结束后光标消失
|
||||
|
||||
## 🐛 调试信息
|
||||
|
||||
### 前端控制台输出
|
||||
```
|
||||
🎯 Chat页面收到AI发送请求: {内容: '什么是js', 当前模型: 'gpt-3.5-turbo', ...}
|
||||
🚀 开始发送AI消息: {内容: '什么是js', 模型: 'gpt-3.5-turbo', ...}
|
||||
📨 收到AI流式数据: JavaScript
|
||||
📨 收到AI流式数据: 非常适合
|
||||
📨 收到AI流式数据: 开发
|
||||
...
|
||||
✅ AI流式响应完成
|
||||
✅ AI消息发送成功
|
||||
```
|
||||
|
||||
### Rust 后端日志
|
||||
```
|
||||
[INFO] 🤖 开始发送 AI 流式消息请求, conversation_id: xxx, request_id: xxx
|
||||
[INFO] 📡 SSE Request URL: http://192.168.1.37:18760/ai/chat/message/send-stream
|
||||
[INFO] ✅ SSE 连接已建立,开始监听流式数据...
|
||||
[INFO] 🔍 收到原始数据块 (长度: 365): "data:{...}"
|
||||
[INFO] 📨 收到 SSE 数据 (无空格): {"success":true,...}
|
||||
...
|
||||
[INFO] ✅ SSE 流正常结束,总内容长度: xxx
|
||||
[INFO] 🏁 SSE 流处理完成
|
||||
```
|
||||
|
||||
## 🎉 功能演示
|
||||
|
||||
### 流式输出效果
|
||||
```
|
||||
用户: 什么是 JavaScript?
|
||||
|
||||
AI: J▋ (0.1秒)
|
||||
AI: JavaScript▋ (0.2秒)
|
||||
AI: JavaScript 是▋ (0.3秒)
|
||||
AI: JavaScript 是一种▋ (0.4秒)
|
||||
...
|
||||
AI: JavaScript 是一种非常适合开发单页应用,用户的操作不需要... (完成)
|
||||
```
|
||||
|
||||
## 📋 文件清单
|
||||
|
||||
### 修改的文件
|
||||
- ✅ `src/plugins/robot/views/Chat.vue` - 添加消息列表和实时渲染
|
||||
- ✅ `src-tauri/src/command/ai_command.rs` - 添加调试日志
|
||||
|
||||
### 新增的文件
|
||||
- ✅ `AI_STREAM_COMPLETE.md` - 本文档
|
||||
|
||||
## 🚀 后续优化建议
|
||||
|
||||
1. **消息持久化**
|
||||
- 将消息保存到数据库
|
||||
- 刷新后恢复历史消息
|
||||
|
||||
2. **Markdown 渲染**
|
||||
- 支持代码高亮
|
||||
- 支持表格、列表等格式
|
||||
|
||||
3. **消息操作**
|
||||
- 复制消息
|
||||
- 删除消息
|
||||
- 重新生成
|
||||
|
||||
4. **性能优化**
|
||||
- 虚拟滚动(消息过多时)
|
||||
- 消息分页加载
|
||||
|
||||
5. **用户体验**
|
||||
- 停止生成按钮
|
||||
- 消息加载动画
|
||||
- 错误重试机制
|
||||
|
||||
## ✨ 总结
|
||||
|
||||
AI 流式数据功能已经完全实现并可以正常工作!
|
||||
|
||||
- ✅ 后端成功接收 SSE 流式数据
|
||||
- ✅ 前端成功解析 JSON 数据
|
||||
- ✅ UI 实时渲染 AI 回复
|
||||
- ✅ 流式光标动画效果
|
||||
- ✅ 自动滚动到最新消息
|
||||
- ✅ 完整的错误处理
|
||||
|
||||
现在你可以在聊天界面看到 AI 的回复像打字一样逐字逐句地出现,就像真人在输入一样!🎉
|
||||
|
||||
@@ -1,214 +0,0 @@
|
||||
# AI 流式数据接入实现文档
|
||||
|
||||
## 概述
|
||||
|
||||
本文档描述了 AI 流式数据接入功能的实现,该功能通过 HTTP 请求建立 SSE (Server-Sent Events) 连接,实时接收 AI 返回的流式数据。
|
||||
|
||||
## 实现架构
|
||||
|
||||
### 1. 后端 (Rust/Tauri)
|
||||
|
||||
#### 文件位置
|
||||
- `src-tauri/src/command/ai_command.rs` - AI 命令处理
|
||||
- `src-tauri/Cargo.toml` - 添加了 `reqwest-eventsource` 依赖
|
||||
|
||||
#### 核心功能
|
||||
- **命令**: `ai_message_send_stream`
|
||||
- **功能**:
|
||||
1. 发送 HTTP POST 请求到 AI 服务器
|
||||
2. 建立 SSE 连接
|
||||
3. 监听流式数据
|
||||
4. 通过 Tauri 事件系统将数据发送到前端
|
||||
|
||||
#### 事件类型
|
||||
```rust
|
||||
pub struct SseStreamEvent {
|
||||
pub event_type: String, // "chunk" | "done" | "error"
|
||||
pub data: Option<String>,
|
||||
pub error: Option<String>,
|
||||
pub request_id: String,
|
||||
}
|
||||
```
|
||||
|
||||
#### 工作流程
|
||||
1. 接收前端请求参数(conversationId, content, useContext)
|
||||
2. 使用 `reqwest` 发送 HTTP POST 请求
|
||||
3. 使用 `reqwest-eventsource` 建立 SSE 连接
|
||||
4. 在后台任务中监听 SSE 事件流
|
||||
5. 每收到一个数据块,发送 `chunk` 事件到前端
|
||||
6. 流结束时,发送 `done` 事件
|
||||
7. 出错时,发送 `error` 事件
|
||||
|
||||
### 2. 前端 (Vue/TypeScript)
|
||||
|
||||
#### 文件位置
|
||||
- `src/utils/ImRequestUtils.ts` - 流式 API 封装
|
||||
- `src/plugins/robot/views/Chat.vue` - AI 聊天界面
|
||||
- `src/enums/index.ts` - 添加了 `AI_MESSAGE_SEND_STREAM` 命令枚举
|
||||
|
||||
#### 核心功能
|
||||
- **函数**: `messageSendStream`
|
||||
- **特点**:
|
||||
1. 使用 Promise 包装整个 SSE 流程
|
||||
2. 监听 Tauri 的 `ai-stream-event` 事件
|
||||
3. 提供回调函数支持(onChunk, onDone, onError)
|
||||
4. 流结束后 resolve Promise
|
||||
|
||||
#### 使用示例
|
||||
```typescript
|
||||
const fullResponse = await messageSendStream(
|
||||
{
|
||||
conversationId: 'chat-123',
|
||||
content: '你好,AI',
|
||||
useContext: true
|
||||
},
|
||||
{
|
||||
onChunk: (chunk: string) => {
|
||||
// 实时接收数据块
|
||||
console.log('收到数据:', chunk)
|
||||
},
|
||||
onDone: (fullContent: string) => {
|
||||
// 流结束
|
||||
console.log('完整内容:', fullContent)
|
||||
},
|
||||
onError: (error: string) => {
|
||||
// 错误处理
|
||||
console.error('错误:', error)
|
||||
}
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
## 数据流向
|
||||
|
||||
```
|
||||
前端 (Chat.vue)
|
||||
↓ 调用 messageSendStream()
|
||||
↓
|
||||
前端 (ImRequestUtils.ts)
|
||||
↓ invoke(AI_MESSAGE_SEND_STREAM)
|
||||
↓
|
||||
后端 (ai_command.rs)
|
||||
↓ 发送 HTTP POST 请求
|
||||
↓
|
||||
AI 服务器
|
||||
↓ 建立 SSE 连接
|
||||
↓ 返回流式数据
|
||||
↓
|
||||
后端 (ai_command.rs)
|
||||
↓ emit('ai-stream-event', chunk)
|
||||
↓
|
||||
前端 (ImRequestUtils.ts)
|
||||
↓ listen('ai-stream-event')
|
||||
↓ 触发回调 onChunk()
|
||||
↓ 累积数据
|
||||
↓ 流结束时 resolve(fullContent)
|
||||
↓
|
||||
前端 (Chat.vue)
|
||||
↓ 显示完整响应
|
||||
```
|
||||
|
||||
## 关键特性
|
||||
|
||||
### 1. 同步监听
|
||||
- HTTP 请求发送后,立即在 Rust 后端建立 SSE 连接
|
||||
- 使用 `tokio::spawn` 在后台异步处理流式数据
|
||||
- 不阻塞主线程
|
||||
|
||||
### 2. 事件驱动
|
||||
- 使用 Tauri 的事件系统进行前后端通信
|
||||
- 通过 `request_id` 区分不同的请求
|
||||
- 支持多个并发请求
|
||||
|
||||
### 3. Promise 包装
|
||||
- 前端使用 Promise 包装整个流程
|
||||
- 流结束时自动 resolve
|
||||
- 出错时自动 reject
|
||||
- 支持 async/await 语法
|
||||
|
||||
### 4. 回调支持
|
||||
- `onChunk`: 实时接收数据块,可用于 UI 更新
|
||||
- `onDone`: 流结束时调用
|
||||
- `onError`: 错误处理
|
||||
|
||||
## 测试步骤
|
||||
|
||||
### 1. 编译项目
|
||||
```bash
|
||||
# 安装依赖
|
||||
cd src-tauri
|
||||
cargo build
|
||||
|
||||
# 或者直接运行
|
||||
npm run tauri dev
|
||||
```
|
||||
|
||||
### 2. 测试流程
|
||||
1. 打开应用
|
||||
2. 进入 ChatBot 窗口(robot 插件)
|
||||
3. 选择一个 AI 模型
|
||||
4. 输入消息并发送
|
||||
5. 观察控制台输出:
|
||||
- 应该看到 "🚀 开始发送AI消息"
|
||||
- 应该看到 "📨 收到AI流式数据块" (多次)
|
||||
- 应该看到 "✅ AI流式响应完成"
|
||||
- 应该看到 "✅ AI消息发送成功"
|
||||
|
||||
### 3. 验证点
|
||||
- [ ] HTTP 请求成功发送
|
||||
- [ ] SSE 连接成功建立
|
||||
- [ ] 能够接收流式数据块
|
||||
- [ ] 数据块正确传递到前端
|
||||
- [ ] Promise 在流结束后正确 resolve
|
||||
- [ ] 错误情况下 Promise 正确 reject
|
||||
- [ ] 多个并发请求互不干扰
|
||||
|
||||
## 注意事项
|
||||
|
||||
### 1. 窗口 Label
|
||||
- AI 聊天窗口的 label 是 `robot`
|
||||
- 事件会发送到所有窗口,通过 `request_id` 过滤
|
||||
|
||||
### 2. 错误处理
|
||||
- 网络错误会触发 `error` 事件
|
||||
- SSE 连接关闭会触发 `done` 事件
|
||||
- 需要区分正常结束和异常结束
|
||||
|
||||
### 3. 内存管理
|
||||
- 每个请求都会创建一个后台任务
|
||||
- 任务在流结束后自动清理
|
||||
- 前端监听器在 Promise resolve/reject 后自动取消
|
||||
|
||||
### 4. 并发请求
|
||||
- 使用唯一的 `request_id` 区分不同请求
|
||||
- 支持多个并发流式请求
|
||||
- 每个请求独立处理,互不影响
|
||||
|
||||
## 后续优化建议
|
||||
|
||||
1. **UI 实时更新**: 在 `onChunk` 回调中实时更新聊天界面,显示 AI 正在输出的内容
|
||||
2. **取消请求**: 添加取消流式请求的功能
|
||||
3. **重试机制**: 添加自动重试机制
|
||||
4. **进度指示**: 添加更详细的进度指示
|
||||
5. **错误分类**: 区分不同类型的错误(网络错误、服务器错误等)
|
||||
6. **性能优化**: 对于大量数据块,考虑批量处理
|
||||
|
||||
## 相关文件清单
|
||||
|
||||
### Rust 后端
|
||||
- `src-tauri/src/command/ai_command.rs` (新建)
|
||||
- `src-tauri/src/command/mod.rs` (修改)
|
||||
- `src-tauri/src/lib.rs` (修改)
|
||||
- `src-tauri/Cargo.toml` (修改)
|
||||
|
||||
### 前端
|
||||
- `src/utils/ImRequestUtils.ts` (修改)
|
||||
- `src/plugins/robot/views/Chat.vue` (修改)
|
||||
- `src/enums/index.ts` (修改)
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **后端**: Rust, Tauri, reqwest, reqwest-eventsource, tokio
|
||||
- **前端**: Vue 3, TypeScript, Tauri API
|
||||
- **通信**: Tauri Event System, SSE (Server-Sent Events)
|
||||
|
||||
4
package.json
vendored
4
package.json
vendored
@@ -20,7 +20,7 @@
|
||||
"========= 启动vue(tauri项目会连带执行不需要单独执行) =========": "",
|
||||
"dev": "vite",
|
||||
"========= 打包vue(tauri项目会连带执行不需要单独执行) =========": "",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"build": "NODE_OPTIONS=--max-old-space-size=4096 vue-tsc --noEmit && NODE_OPTIONS=--max-old-space-size=4096 vite build",
|
||||
"========= 启动HuLa桌面应用程序 =========": "",
|
||||
"tauri:dev": "tauri dev",
|
||||
"========= 启动HuLa桌面应用程序(简化命令) =========": "",
|
||||
@@ -140,7 +140,7 @@
|
||||
"vue-virtual-scroller": "2.0.0-beta.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.2.7",
|
||||
"@biomejs/biome": "^2.3.4",
|
||||
"@commitlint/cli": "^19.8.1",
|
||||
"@commitlint/config-conventional": "^19.8.1",
|
||||
"@iconify/vue": "^5.0.0",
|
||||
|
||||
82
pnpm-lock.yaml
generated
vendored
82
pnpm-lock.yaml
generated
vendored
@@ -187,8 +187,8 @@ importers:
|
||||
version: 2.0.0-beta.8(vue@3.5.22(typescript@5.9.3))
|
||||
devDependencies:
|
||||
'@biomejs/biome':
|
||||
specifier: ^2.2.7
|
||||
version: 2.2.7
|
||||
specifier: ^2.3.4
|
||||
version: 2.3.4
|
||||
'@commitlint/cli':
|
||||
specifier: ^19.8.1
|
||||
version: 19.8.1(@types/node@24.6.2)(typescript@5.9.3)
|
||||
@@ -480,59 +480,59 @@ packages:
|
||||
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@biomejs/biome@2.2.7':
|
||||
resolution: {integrity: sha512-1a8j0UP1vXVUf3UzMZEJ/zS2VgAG6wU6Cuh/I764sUGI+MCnJs/9WaojHYBDCxCMLTgU60/WqnYof85emXmSBA==}
|
||||
'@biomejs/biome@2.3.4':
|
||||
resolution: {integrity: sha512-TU08LXjBHdy0mEY9APtEtZdNQQijXUDSXR7IK1i45wgoPD5R0muK7s61QcFir6FpOj/RP1+YkPx5QJlycXUU3w==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
hasBin: true
|
||||
|
||||
'@biomejs/cli-darwin-arm64@2.2.7':
|
||||
resolution: {integrity: sha512-xBUUsebnO2/Qj1v7eZmKUy2ZcFkZ4/jLUkxN02Qup1RPoRaiW9AKXHrqS3L7iX6PzofHY2xuZ+Pb9kAcpoe0qA==}
|
||||
'@biomejs/cli-darwin-arm64@2.3.4':
|
||||
resolution: {integrity: sha512-w40GvlNzLaqmuWYiDU6Ys9FNhJiclngKqcGld3iJIiy2bpJ0Q+8n3haiaC81uTPY/NA0d8Q/I3Z9+ajc14102Q==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@biomejs/cli-darwin-x64@2.2.7':
|
||||
resolution: {integrity: sha512-vsY4NhmxqgfLJufr9XUnC+yGUPJiXAc1mz6FcjaAmuIuLwfghN4uQO7hnW2AneGyoi2mNe9Jbvf6Qtq4AjzrFg==}
|
||||
'@biomejs/cli-darwin-x64@2.3.4':
|
||||
resolution: {integrity: sha512-3s7TLVtjJ7ni1xADXsS7x7GMUrLBZXg8SemXc3T0XLslzvqKj/dq1xGeBQ+pOWQzng9MaozfacIHdK2UlJ3jGA==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@biomejs/cli-linux-arm64-musl@2.2.7':
|
||||
resolution: {integrity: sha512-FrTwvKO/7t5HbVTvhlMOTOVQLAcR7r4O4iFQhEpZXUtBfosHqrX/JJlX7daPawoe14MDcCu9CDg0zLVpTuDvuQ==}
|
||||
'@biomejs/cli-linux-arm64-musl@2.3.4':
|
||||
resolution: {integrity: sha512-IruVGQRwMURivWazchiq7gKAqZSFs5so6gi0hJyxk7x6HR+iwZbO2IxNOqyLURBvL06qkIHs7Wffl6Bw30vCbQ==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@biomejs/cli-linux-arm64@2.2.7':
|
||||
resolution: {integrity: sha512-nUdco104rjV9dULi1VssQ5R/kX2jE/Z2sDjyqS+siV9sTQda0DwmEUixFNRCWvZJRRiZUWhgiDFJ4n7RowO8Mg==}
|
||||
'@biomejs/cli-linux-arm64@2.3.4':
|
||||
resolution: {integrity: sha512-y7efHyyM2gYmHy/AdWEip+VgTMe9973aP7XYKPzu/j8JxnPHuSUXftzmPhkVw0lfm4ECGbdBdGD6+rLmTgNZaA==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@biomejs/cli-linux-x64-musl@2.2.7':
|
||||
resolution: {integrity: sha512-MnsysF5s/iLC5wnYvuMseOy+m8Pd4bWG1uwlVyy2AUbfjAVUgtbYbboc5wMXljFrDY7e6rLjLTR4S2xqDpGlQg==}
|
||||
'@biomejs/cli-linux-x64-musl@2.3.4':
|
||||
resolution: {integrity: sha512-mzKFFv/w66e4/jCobFmD3kymCqG+FuWE7sVa4Yjqd9v7qt2UhXo67MSZKY9Ih18V2IwPzRKQPCw6KwdZs6AXSA==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@biomejs/cli-linux-x64@2.2.7':
|
||||
resolution: {integrity: sha512-tPTcGAIEOOZrj2tQ7fdraWlaxNKApBw6l4In8wQQV1IyxnAexqi0hykHzKEX8hKKctf5gxGBfNCzyIvqpj4CFQ==}
|
||||
'@biomejs/cli-linux-x64@2.3.4':
|
||||
resolution: {integrity: sha512-gKfjWR/6/dfIxPJCw8REdEowiXCkIpl9jycpNVHux8aX2yhWPLjydOshkDL6Y/82PcQJHn95VCj7J+BRcE5o1Q==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@biomejs/cli-win32-arm64@2.2.7':
|
||||
resolution: {integrity: sha512-h5D1jhwA2b7cFXerYiJfXHSzzAMFFoEDL5Mc2BgiaEw0iaSgSso/3Nc6FbOR55aTQISql+IpB4PS7JoV26Gdbw==}
|
||||
'@biomejs/cli-win32-arm64@2.3.4':
|
||||
resolution: {integrity: sha512-5TJ6JfVez+yyupJ/iGUici2wzKf0RrSAxJhghQXtAEsc67OIpdwSKAQboemILrwKfHDi5s6mu7mX+VTCTUydkw==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@biomejs/cli-win32-x64@2.2.7':
|
||||
resolution: {integrity: sha512-URqAJi0kONyBKG4V9NVafHLDtm6IHmF4qPYi/b6x7MD6jxpWeJiTCO6R5+xDlWckX2T/OGv6Yq3nkz6s0M8Ykw==}
|
||||
'@biomejs/cli-win32-x64@2.3.4':
|
||||
resolution: {integrity: sha512-FGCijXecmC4IedQ0esdYNlMpx0Jxgf4zceCaMu6fkjWyjgn50ZQtMiqZZQ0Q/77yqPxvtkgZAvt5uGw0gAAjig==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
@@ -1289,8 +1289,8 @@ packages:
|
||||
resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
'@jridgewell/source-map@0.3.6':
|
||||
resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==}
|
||||
'@jridgewell/source-map@0.3.11':
|
||||
resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==}
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.0':
|
||||
resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
|
||||
@@ -5643,39 +5643,39 @@ snapshots:
|
||||
|
||||
'@bcoe/v8-coverage@1.0.2': {}
|
||||
|
||||
'@biomejs/biome@2.2.7':
|
||||
'@biomejs/biome@2.3.4':
|
||||
optionalDependencies:
|
||||
'@biomejs/cli-darwin-arm64': 2.2.7
|
||||
'@biomejs/cli-darwin-x64': 2.2.7
|
||||
'@biomejs/cli-linux-arm64': 2.2.7
|
||||
'@biomejs/cli-linux-arm64-musl': 2.2.7
|
||||
'@biomejs/cli-linux-x64': 2.2.7
|
||||
'@biomejs/cli-linux-x64-musl': 2.2.7
|
||||
'@biomejs/cli-win32-arm64': 2.2.7
|
||||
'@biomejs/cli-win32-x64': 2.2.7
|
||||
'@biomejs/cli-darwin-arm64': 2.3.4
|
||||
'@biomejs/cli-darwin-x64': 2.3.4
|
||||
'@biomejs/cli-linux-arm64': 2.3.4
|
||||
'@biomejs/cli-linux-arm64-musl': 2.3.4
|
||||
'@biomejs/cli-linux-x64': 2.3.4
|
||||
'@biomejs/cli-linux-x64-musl': 2.3.4
|
||||
'@biomejs/cli-win32-arm64': 2.3.4
|
||||
'@biomejs/cli-win32-x64': 2.3.4
|
||||
|
||||
'@biomejs/cli-darwin-arm64@2.2.7':
|
||||
'@biomejs/cli-darwin-arm64@2.3.4':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-darwin-x64@2.2.7':
|
||||
'@biomejs/cli-darwin-x64@2.3.4':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-linux-arm64-musl@2.2.7':
|
||||
'@biomejs/cli-linux-arm64-musl@2.3.4':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-linux-arm64@2.2.7':
|
||||
'@biomejs/cli-linux-arm64@2.3.4':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-linux-x64-musl@2.2.7':
|
||||
'@biomejs/cli-linux-x64-musl@2.3.4':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-linux-x64@2.2.7':
|
||||
'@biomejs/cli-linux-x64@2.3.4':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-win32-arm64@2.2.7':
|
||||
'@biomejs/cli-win32-arm64@2.3.4':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-win32-x64@2.2.7':
|
||||
'@biomejs/cli-win32-x64@2.3.4':
|
||||
optional: true
|
||||
|
||||
'@braintree/sanitize-url@7.1.1': {}
|
||||
@@ -6292,7 +6292,7 @@ snapshots:
|
||||
|
||||
'@jridgewell/set-array@1.2.1': {}
|
||||
|
||||
'@jridgewell/source-map@0.3.6':
|
||||
'@jridgewell/source-map@0.3.11':
|
||||
dependencies:
|
||||
'@jridgewell/gen-mapping': 0.3.13
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
@@ -10058,7 +10058,7 @@ snapshots:
|
||||
|
||||
terser@5.37.0:
|
||||
dependencies:
|
||||
'@jridgewell/source-map': 0.3.6
|
||||
'@jridgewell/source-map': 0.3.11
|
||||
acorn: 8.15.0
|
||||
commander: 2.20.3
|
||||
source-map-support: 0.5.21
|
||||
|
||||
6
src-tauri/Cargo.lock
generated
6
src-tauri/Cargo.lock
generated
@@ -2799,7 +2799,7 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "hula"
|
||||
version = "3.0.2"
|
||||
version = "3.0.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-walkdir",
|
||||
@@ -7152,9 +7152,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
|
||||
|
||||
[[package]]
|
||||
name = "tauri"
|
||||
version = "2.9.1"
|
||||
version = "2.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9871670c6711f50fddd4e20350be6b9dd6e6c2b5d77d8ee8900eb0d58cd837a"
|
||||
checksum = "8bceb52453e507c505b330afe3398510e87f428ea42b6e76ecb6bd63b15965b5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "hula"
|
||||
version = "3.0.2"
|
||||
version = "3.0.3"
|
||||
description = "hula"
|
||||
authors = ["nongyehong"]
|
||||
license = ""
|
||||
@@ -36,7 +36,7 @@ cc = "1.0"
|
||||
|
||||
[dependencies]
|
||||
# Tauri 官方依赖
|
||||
tauri = { version = "2.9.1", features = [
|
||||
tauri = { version = "2.9.2", features = [
|
||||
"protocol-asset",
|
||||
"macos-private-api",
|
||||
"tray-icon",
|
||||
|
||||
31
src/App.vue
31
src/App.vue
@@ -21,7 +21,6 @@ import {
|
||||
ThemeEnum,
|
||||
ChangeTypeEnum,
|
||||
MittEnum,
|
||||
ModalEnum,
|
||||
OnlineEnum,
|
||||
RoomTypeEnum
|
||||
} from '@/enums'
|
||||
@@ -34,7 +33,6 @@ import { useSettingStore } from '@/stores/setting.ts'
|
||||
import { isDesktop, isIOS, isMobile, isWindows } from '@/utils/PlatformConstants'
|
||||
import LockScreen from '@/views/LockScreen.vue'
|
||||
import { unreadCountManager } from '@/utils/UnreadCountManager'
|
||||
|
||||
import {
|
||||
type LoginSuccessResType,
|
||||
type OnStatusChangeType,
|
||||
@@ -49,6 +47,8 @@ import { useAnnouncementStore } from '@/stores/announcement'
|
||||
import { useFeedStore } from '@/stores/feed'
|
||||
import { useFeedNotificationStore } from '@/stores/feedNotification'
|
||||
import type { MarkItemType, RevokedMsgType, UserItem } from '@/services/types.ts'
|
||||
import * as ImRequestUtils from '@/utils/ImRequestUtils'
|
||||
import { REMOTE_LOGIN_INFO_KEY } from '@/common/constants'
|
||||
|
||||
const mobileRtcCallFloatCell = isMobile()
|
||||
? defineAsyncComponent(() => import('@/mobile/components/RtcCallFloatCell.vue'))
|
||||
@@ -69,7 +69,7 @@ const router = useRouter()
|
||||
// 只在桌面端初始化窗口管理功能
|
||||
const { createWebviewWindow } = isDesktop() ? useWindow() : { createWebviewWindow: () => {} }
|
||||
const settingStore = useSettingStore()
|
||||
const { themes, lockScreen, page } = storeToRefs(settingStore)
|
||||
const { themes, lockScreen, page, login } = storeToRefs(settingStore)
|
||||
// 全局快捷键管理
|
||||
const { initializeGlobalShortcut, cleanupGlobalShortcut } = useGlobalShortcut()
|
||||
|
||||
@@ -276,11 +276,9 @@ useMitt.on(WsResponseMessageType.ROOM_INFO_CHANGE, async (data: { roomId: string
|
||||
|
||||
useMitt.on(WsResponseMessageType.TOKEN_EXPIRED, async (wsTokenExpire: WsTokenExpire) => {
|
||||
if (Number(userUid.value) === Number(wsTokenExpire.uid) && userStore.userInfo!.client === wsTokenExpire.client) {
|
||||
const { useLogin } = await import('@/hooks/useLogin')
|
||||
const { resetLoginState, logout } = useLogin()
|
||||
if (isMobile()) {
|
||||
// 移动端处理:立即清空登录数据并跳转到登录页,然后显示弹窗
|
||||
const { useLogin } = await import('@/hooks/useLogin')
|
||||
const { resetLoginState, logout } = useLogin()
|
||||
|
||||
try {
|
||||
// 1. 先重置登录状态(不请求接口,只清理本地)
|
||||
await resetLoginState()
|
||||
@@ -314,13 +312,18 @@ useMitt.on(WsResponseMessageType.TOKEN_EXPIRED, async (wsTokenExpire: WsTokenExp
|
||||
// 桌面端处理:聚焦主窗口并显示远程登录弹窗
|
||||
const home = await WebviewWindow.getByLabel('home')
|
||||
await home?.setFocus()
|
||||
|
||||
useMitt.emit(MittEnum.LEFT_MODAL_SHOW, {
|
||||
type: ModalEnum.REMOTE_LOGIN,
|
||||
props: {
|
||||
ip: wsTokenExpire.ip
|
||||
}
|
||||
})
|
||||
const remoteIp = wsTokenExpire.ip || '未知IP'
|
||||
localStorage.setItem(
|
||||
REMOTE_LOGIN_INFO_KEY,
|
||||
JSON.stringify({
|
||||
ip: remoteIp,
|
||||
timestamp: Date.now()
|
||||
})
|
||||
)
|
||||
await ImRequestUtils.logout({ autoLogin: login.value.autoLogin })
|
||||
await resetLoginState()
|
||||
await logout()
|
||||
useMitt.emit(MittEnum.LOGIN_REMOTE_MODAL)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -6,3 +6,6 @@ export const MAX_FOOTER_HEIGHT = 390
|
||||
export const MIN_FOOTER_HEIGHT = 200
|
||||
/** 顶部选项栏高度 */
|
||||
export const TOOLBAR_HEIGHT = 40
|
||||
|
||||
/** 异地登录信息存储键 */
|
||||
export const REMOTE_LOGIN_INFO_KEY = 'REMOTE_LOGIN_INFO'
|
||||
|
||||
@@ -82,6 +82,8 @@ export enum MittEnum {
|
||||
OPEN_GROUP_NICKNAME_MODAL = 'openGroupNicknameModal',
|
||||
/** 左边菜单弹窗 */
|
||||
LEFT_MODAL_SHOW = 'leftModalShow',
|
||||
/** 登录窗口异地登录弹窗 */
|
||||
LOGIN_REMOTE_MODAL = 'loginRemoteModal',
|
||||
/** 触发home窗口事件 */
|
||||
HOME_WINDOW_RESIZE = 'homeWindowResize',
|
||||
/** @ AT */
|
||||
|
||||
@@ -187,7 +187,11 @@ export const useWindow = () => {
|
||||
width: number,
|
||||
height: number,
|
||||
parent: string,
|
||||
payload?: Record<string, any>
|
||||
payload?: Record<string, any>,
|
||||
options?: {
|
||||
minWidth?: number
|
||||
minHeight?: number
|
||||
}
|
||||
) => {
|
||||
// 移动端不支持窗口管理
|
||||
if (!isDesktop()) {
|
||||
@@ -211,8 +215,8 @@ export const useWindow = () => {
|
||||
height: height,
|
||||
resizable: false,
|
||||
center: true,
|
||||
minWidth: 500,
|
||||
minHeight: 500,
|
||||
minWidth: options?.minWidth ?? 500,
|
||||
minHeight: options?.minHeight ?? 500,
|
||||
focus: true,
|
||||
minimizable: false,
|
||||
parent: parentWindow ? parentWindow : parent,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { emit } from '@tauri-apps/api/event'
|
||||
import {
|
||||
type FormInst,
|
||||
NAvatar,
|
||||
@@ -15,6 +14,8 @@ import {
|
||||
NTimeline,
|
||||
NTimelineItem
|
||||
} from 'naive-ui'
|
||||
import type { PropType } from 'vue'
|
||||
import { emit } from '@tauri-apps/api/event'
|
||||
import { EventEnum } from '@/enums'
|
||||
import { handRelativeTime } from '@/utils/Day.ts'
|
||||
import './style.scss'
|
||||
@@ -22,33 +23,17 @@ import { getVersion } from '@tauri-apps/api/app'
|
||||
import { confirm } from '@tauri-apps/plugin-dialog'
|
||||
import { relaunch } from '@tauri-apps/plugin-process'
|
||||
import { check } from '@tauri-apps/plugin-updater'
|
||||
import { useLogin } from '@/hooks/useLogin'
|
||||
import { useSettingStore } from '@/stores/setting.ts'
|
||||
import { useUserStore } from '@/stores/user.ts'
|
||||
import { AvatarUtils } from '@/utils/AvatarUtils'
|
||||
import * as ImRequestUtils from '@/utils/ImRequestUtils'
|
||||
import { isMac } from '@/utils/PlatformConstants'
|
||||
import { REMOTE_LOGIN_INFO_KEY } from '@/common/constants'
|
||||
|
||||
const { logout: sysLogout, resetLoginState } = useLogin()
|
||||
const formRef = ref<FormInst | null>()
|
||||
const formValue = ref({
|
||||
lockPassword: ''
|
||||
})
|
||||
export const modalShow = ref(false)
|
||||
export const remotelogin = ref({
|
||||
loading: false,
|
||||
async logout() {
|
||||
remotelogin.value.loading = true
|
||||
const settingStore = useSettingStore()
|
||||
const { login } = storeToRefs(settingStore)
|
||||
// token已在后端清空,只需要返回登录页
|
||||
await ImRequestUtils.logout({ autoLogin: login.value.autoLogin })
|
||||
await resetLoginState()
|
||||
await sysLogout()
|
||||
modalShow.value = false
|
||||
remotelogin.value.loading = false
|
||||
}
|
||||
})
|
||||
export const lock = ref({
|
||||
loading: false,
|
||||
rules: {
|
||||
@@ -432,26 +417,41 @@ export const RemoteLogin = defineComponent({
|
||||
ip: {
|
||||
type: String,
|
||||
default: '未知IP'
|
||||
},
|
||||
onConfirm: {
|
||||
type: Function as PropType<() => void | Promise<void>>,
|
||||
default: void 0
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const userStore = useUserStore()
|
||||
const handleConfirm = async () => {
|
||||
modalShow.value = false
|
||||
if (props.onConfirm) {
|
||||
// 关闭对应子窗口
|
||||
await props.onConfirm()
|
||||
}
|
||||
}
|
||||
setInterval(() => {
|
||||
localStorage.removeItem(REMOTE_LOGIN_INFO_KEY)
|
||||
}, 300)
|
||||
|
||||
return () => (
|
||||
<NModal
|
||||
v-model:show={modalShow.value}
|
||||
maskClosable={false}
|
||||
class="w-350px border-rd-8px select-none cursor-default">
|
||||
<div class="bg-[--bg-popover] w-360px h-full p-6px box-border flex flex-col">
|
||||
class="w-350px h-310px border-rd-8px select-none cursor-default">
|
||||
<div class="bg-[--bg-popover] size-full p-6px box-border flex flex-col">
|
||||
{isMac() ? (
|
||||
<div
|
||||
onClick={remotelogin.value.logout}
|
||||
onClick={handleConfirm}
|
||||
class="mac-close relative size-13px shadow-inner bg-#ed6a5eff rounded-50% select-none">
|
||||
<svg class="hidden size-7px color-#000 select-none absolute top-3px left-3px">
|
||||
<use href="#close"></use>
|
||||
</svg>
|
||||
</div>
|
||||
) : (
|
||||
<svg onClick={remotelogin.value.logout} class="w-12px h-12px ml-a cursor-pointer select-none">
|
||||
<svg onClick={handleConfirm} class="w-12px h-12px ml-a cursor-pointer select-none text-[--text-color]">
|
||||
<use href="#close"></use>
|
||||
</svg>
|
||||
)}
|
||||
@@ -460,7 +460,7 @@ export const RemoteLogin = defineComponent({
|
||||
<span class="text-(14px [--text-color])">下线通知</span>
|
||||
|
||||
<div class="relative">
|
||||
<img class="rounded-full size-72px" src={AvatarUtils.getAvatarUrl(userStore.userInfo!.avatar!)} />
|
||||
<img class="rounded-full size-72px" src={AvatarUtils.getAvatarUrl(userStore.userInfo?.avatar ?? '')} />
|
||||
<div class="absolute inset-0 bg-[--avatar-hover-bg] backdrop-blur-[2px] rounded-full flex items-center justify-center">
|
||||
<svg class="size-34px text-white animate-pulse">
|
||||
<use href="#cloudError"></use>
|
||||
@@ -473,14 +473,8 @@ export const RemoteLogin = defineComponent({
|
||||
登录,如非本人登录,请尽快修改密码,建议联系管理员
|
||||
</div>
|
||||
</NFlex>
|
||||
<NButton
|
||||
disabled={remotelogin.value.loading}
|
||||
loading={remotelogin.value.loading}
|
||||
onClick={remotelogin.value.logout}
|
||||
style={{ color: '#fff' }}
|
||||
class="w-full"
|
||||
color="#13987f">
|
||||
重新登录
|
||||
<NButton onClick={handleConfirm} style={{ color: '#fff' }} class="w-full" color="#13987f">
|
||||
知道了
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -588,6 +588,11 @@ const getCommonRoutes = (): Array<RouteRecordRaw> => [
|
||||
path: '/modal-privacyAgreement',
|
||||
name: 'modal-privacyAgreement',
|
||||
component: () => import('@/views/agreementWindow/Privacy.vue')
|
||||
},
|
||||
{
|
||||
path: '/modal-remoteLogin',
|
||||
name: 'modal-remoteLogin',
|
||||
component: () => import('@/views/loginWindow/RemoteLoginModal.vue')
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -192,8 +192,10 @@ import { useGuideStore } from '@/stores/guide'
|
||||
import { useLoginHistoriesStore } from '@/stores/loginHistory.ts'
|
||||
import { useSettingStore } from '@/stores/setting.ts'
|
||||
import { useUserStore } from '@/stores/user.ts'
|
||||
import { MittEnum } from '@/enums'
|
||||
import { REMOTE_LOGIN_INFO_KEY } from '@/common/constants'
|
||||
import { AvatarUtils } from '@/utils/AvatarUtils'
|
||||
import { isCompatibility, isMac } from '@/utils/PlatformConstants'
|
||||
import { isCompatibility, isDesktop, isMac } from '@/utils/PlatformConstants'
|
||||
import { clearListener } from '@/utils/ReadCountQueue'
|
||||
import { useLogin } from '@/hooks/useLogin'
|
||||
|
||||
@@ -311,6 +313,44 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
const openRemoteLoginModal = async (ip?: string) => {
|
||||
if (!isDesktop()) {
|
||||
return
|
||||
}
|
||||
const payloadIp = ip ?? '未知IP'
|
||||
await createModalWindow(
|
||||
'异地登录提醒',
|
||||
'modal-remoteLogin',
|
||||
350,
|
||||
310,
|
||||
'login',
|
||||
{
|
||||
ip: payloadIp
|
||||
},
|
||||
{
|
||||
minWidth: 350,
|
||||
minHeight: 310
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const handleCachedRemoteLoginModal = () => {
|
||||
const cached = localStorage.getItem(REMOTE_LOGIN_INFO_KEY)
|
||||
if (!cached) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(cached) as { ip?: string }
|
||||
openRemoteLoginModal(parsed?.ip)
|
||||
} catch (error) {
|
||||
localStorage.removeItem(REMOTE_LOGIN_INFO_KEY)
|
||||
}
|
||||
}
|
||||
|
||||
useMitt.on(MittEnum.LOGIN_REMOTE_MODAL, (payload: { ip?: string }) => {
|
||||
openRemoteLoginModal(payload?.ip)
|
||||
})
|
||||
|
||||
/** 删除账号列表内容 */
|
||||
const delAccount = (item: UserInfoType) => {
|
||||
// 获取删除前账户列表的长度
|
||||
@@ -373,6 +413,7 @@ const enterKey = (e: KeyboardEvent) => {
|
||||
}
|
||||
|
||||
onBeforeMount(async () => {
|
||||
handleCachedRemoteLoginModal()
|
||||
// 始终初始化托盘菜单状态为false
|
||||
isTrayMenuShow.value = false
|
||||
|
||||
|
||||
83
src/views/loginWindow/RemoteLoginModal.vue
Normal file
83
src/views/loginWindow/RemoteLoginModal.vue
Normal file
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<n-config-provider :theme="lightTheme" class="remote-login-modal size-full select-none">
|
||||
<RemoteLogin :ip="ip" :on-confirm="handleConfirm" />
|
||||
</n-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getCurrentWebviewWindow, WebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||||
import { lightTheme } from 'naive-ui'
|
||||
import { RemoteLogin, modalShow } from '@/layout/left/model.tsx'
|
||||
import { REMOTE_LOGIN_INFO_KEY } from '@/common/constants'
|
||||
|
||||
const ip = ref('未知IP')
|
||||
let currentWindow: WebviewWindow | null = null
|
||||
let parentWindow: WebviewWindow | null = null
|
||||
let unlistenClose: (() => void) | undefined
|
||||
|
||||
const handleStorageChange = (event: StorageEvent) => {
|
||||
if (event.key !== REMOTE_LOGIN_INFO_KEY || !event.newValue) {
|
||||
modalShow.value = false
|
||||
currentWindow?.close()
|
||||
return
|
||||
}
|
||||
const parsed = JSON.parse(event.newValue) as { ip?: string }
|
||||
if (parsed?.ip) {
|
||||
ip.value = parsed.ip
|
||||
}
|
||||
}
|
||||
|
||||
const assignIpFromCache = () => {
|
||||
const cached = localStorage.getItem(REMOTE_LOGIN_INFO_KEY)
|
||||
if (!cached) return
|
||||
const parsed = JSON.parse(cached) as { ip?: string }
|
||||
if (parsed?.ip) {
|
||||
ip.value = parsed.ip
|
||||
}
|
||||
}
|
||||
|
||||
const handleConfirm = async () => {
|
||||
modalShow.value = false
|
||||
localStorage.removeItem(REMOTE_LOGIN_INFO_KEY)
|
||||
await parentWindow?.setEnabled(true)
|
||||
await currentWindow?.close()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
window.addEventListener('storage', handleStorageChange)
|
||||
modalShow.value = true
|
||||
currentWindow = await getCurrentWebviewWindow()
|
||||
parentWindow = await WebviewWindow.getByLabel('login')
|
||||
assignIpFromCache()
|
||||
await currentWindow.show()
|
||||
if (currentWindow) {
|
||||
unlistenClose = await currentWindow.onCloseRequested(async () => {
|
||||
modalShow.value = false
|
||||
localStorage.removeItem(REMOTE_LOGIN_INFO_KEY)
|
||||
await parentWindow?.setEnabled(true)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(async () => {
|
||||
modalShow.value = false
|
||||
window.removeEventListener('storage', handleStorageChange)
|
||||
localStorage.removeItem(REMOTE_LOGIN_INFO_KEY)
|
||||
if (unlistenClose) {
|
||||
await unlistenClose()
|
||||
unlistenClose = undefined
|
||||
}
|
||||
await parentWindow?.setEnabled(true)
|
||||
currentWindow = null
|
||||
parentWindow = null
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.remote-login-modal {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
}
|
||||
</style>
|
||||
@@ -1,43 +0,0 @@
|
||||
## Default Permission
|
||||
|
||||
Default permissions for the plugin
|
||||
|
||||
#### This default permission set includes the following:
|
||||
|
||||
- `allow-ping`
|
||||
|
||||
## Permission Table
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>Identifier</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`hula:allow-ping`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the ping command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`hula:deny-ping`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the ping command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
Reference in New Issue
Block a user