关闭软件过滤条件更新

This commit is contained in:
2026-02-09 16:59:19 +08:00
parent 0dd02a13f6
commit 8d103a91ab
2 changed files with 351 additions and 30 deletions

232
CLAUDE.md Normal file
View File

@@ -0,0 +1,232 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## 项目概述
这是一个基于 Vue 3 的 AI 助手 Web 前端应用Yolo可在浏览器和 Electron 环境中运行。项目主要用于自动化私信和内容管理,支持多账号管理、轮换策略和 AI 自动回复功能。
## 核心技术栈
- **前端框架**: Vue 3使用 Composition API
- **构建工具**: Vite
- **样式**: Tailwind CSS
- **HTTP 请求**: Axios
- **运行环境**: 支持浏览器和 Electron 双环境
## 常用开发命令
```bash
# 安装依赖
npm install
# 本地开发(默认端口 5173
npm run dev
# 生产构建
npm run build
# 预览构建产物
npm run preview
```
## 项目架构
### 目录结构
```
src/
├── api/ # API 接口定义
├── assets/ # 静态资源(图片等)
├── components/ # Vue 组件
├── hooks/ # 可复用的组合式函数
├── layout/ # 布局组件
├── locales/ # 国际化配置
├── pages/ # 页面级组件
├── router/ # 路由配置
├── styles/ # 全局样式
├── types/ # TypeScript 类型定义
├── utils/ # 工具函数
└── views/ # 视图组件
```
### 关键架构模式
#### 双环境支持设计
项目通过 `electronBridge.js` 实现 Web 和 Electron 环境的兼容:
- 使用 `isElectron()` 检测运行环境
- 通过 `window.electronAPI` 访问 Electron 功能
- 非 Electron 环境中 API 调用会安全降级
#### 页面状态管理
应用使用三个主要页面状态:
1. `login` - 登录页面
2. `config` - 配置页面账号、AI 设置、任务配置)
3. `browser` - 浏览器视图9 个视图分为 A/B/C 三组)
状态存储于 `localStorage`
- `user_data` - 用户认证信息
- `autoDm_runConfig` - 自动化运行配置
#### 视图分组系统
- 9 个视图被分为 3 组A/B/C每组 3 个视图
- 视图 ID 映射Tab A (1-3), Tab B (4-5), Tab C (7-9)
- 支持账号轮换和自动切换策略
### API 请求架构
#### 请求拦截器配置
- **开发环境**: `http://192.168.2.22:8101`
- **生产环境**: `https://crawlclient.api.yolozs.com`
- **认证**: 通过 `vvtoken` header登录和租户查询接口除外
- **超时时间**: 60 秒
- **错误码 40400**: 强制退出登录并停止脚本
#### 关键 API 端点
```javascript
// 认证相关
/api/tenant/get-id-by-name - ID
/api/user/doLogin -
/api/tenant/get-expired-time -
// 数据相关
/api/save_data/hosts_info -
/api/save_data/live_host_detail -
/api/common/country_info -
// 账号管理
/api/account/list -
/api/account/managerhosts -
```
### Electron 集成要点
#### 主要 IPC 通道
```javascript
// 视图控制
electronAPI.showViews() - 显示浏览器视图
electronAPI.hideViews() - 隐藏浏览器视图
electronAPI.switchTab(tabId) - 切换标签组
electronAPI.switchToView(viewId) - 切换到指定视图
// 自动化控制
electronAPI.stopTikTokAutomation(viewId) - 停止自动化任务
electronAPI.updateAutomationConfig(config) - 更新自动化配置
electronAPI.clearAllCache() - 清空所有缓存
// 状态监听
electronAPI.onRotationStatusChanged(callback) - 监听轮换状态变化
electronAPI.onGreetingStatsChanged(callback) - 监听问候统计变化
electronAPI.onAutomationLog(callback) - 监听自动化日志
// 系统功能
electronAPI.getAppVersion() - 获取应用版本
electronAPI.checkHealth() - 健康检查
```
#### 健康检查机制
每 5 秒执行一次健康检查:
- 检测到错误码 40400 时自动退出登录
- 仅在 Electron 环境且已登录状态下运行
## 样式和 UI 规范
### Tailwind 自定义配置
项目扩展了以下 Tailwind 主题:
**主色调**
- `primary-500` (#3b82f6) 用于主要操作按钮和强调元素
- 提供完整的 50-900 梯度色阶
**自定义动画**
- `fadeIn` - 淡入效果0.3s
- `slideIn` - 滑入效果0.3s,带 Y 轴位移)
### 设计原则
- 使用渐变背景增强视觉层次:`from-slate-100 to-slate-200`
- 卡片设计使用圆角和阴影:`rounded-2xl shadow-xl`
- 状态指示器使用脉动动画:`animate-pulse`
- 过渡效果统一使用 `transition-all`
## Vite 配置要点
### 路径别名
```javascript
'@' -> src/
```
### 开发服务器
- 端口5173
- 启用网络访问:`host: true`
### 构建配置
- 输出目录:`dist/`
- 禁用 sourcemap生产环境
## 重要约定
### 状态管理
不使用 Vuex/Pinia直接使用
- Composition API 的 `ref`/`reactive` 管理组件状态
- `localStorage` 持久化关键配置
- 事件监听器同步跨组件状态
### 组件通信
- 父子组件:通过 `props``emit` 事件
- 跨页面:通过 `storage` 事件监听 `localStorage` 变化
- Electron 通信:通过 `window.electronAPI` 的 IPC 通道
### 错误处理
- API 错误通过 Axios 拦截器统一处理
- 40400 错误码触发全局登出
- 使用 `try-catch` 保护可能失败的操作(带空 catch 块的是故意忽略错误)
### 代码风格
- 使用 ESLint 的 `eslint-disable-line no-empty` 标记故意为空的 catch 块
- 组件使用 `<script setup>` 语法
- 优先使用 `const` 和箭头函数
- 模板中使用 `:class` 动态绑定样式数组
## 调试和测试
### 环境检测
- 开发环境:`window.location.port === '5173'`
- Electron 环境:检查 `window.electronAPI` 是否存在
### 控制台日志规范
使用 `[组件名]` 前缀标识日志来源:
```javascript
console.log('[App] 收到轮换状态变化:', status)
console.error('[App] 健康检查失败:', error)
```
## 特殊注意事项
1. **更新检查**:应用启动时在 Electron 环境下会强制执行更新检查(`UpdateChecker` 组件)
2. **自动登出**:窗口关闭前(`beforeunload`)会自动清除 `localStorage` 中的用户数据
3. **并行停止任务**:停止所有任务时使用 `Promise.allSettled` 并行处理 9 个视图,避免串行等待
4. **配置持久化**:账号分组配置变化时通过 `storage` 事件跨组件同步
5. **视图映射**:视图 ID 与账号的映射关系通过 `viewAccountMap` 计算(每组 3 个账号)

View File

@@ -59,8 +59,7 @@
<!-- 卡片头部 -->
<div class="flex items-center justify-between mb-6">
<div class="flex items-center gap-2">
<span
class="w-1 h-4 rounded-full bg-gradient-to-b from-blue-500 to-green-500" />
<span class="w-1 h-4 rounded-full bg-gradient-to-b from-blue-500 to-green-500" />
<span class="font-medium text-gray-900">运行配置</span>
</div>
@@ -86,8 +85,7 @@
@click="handleStart(config.accountGroups.indexOf(group))"
class="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-600 transition-colors flex items-center justify-between group/item">
<span>运行 {{ group.name }}</span>
<span
class="text-xs text-gray-400 group-hover/item:text-blue-400">
<span class="text-xs text-gray-400 group-hover/item:text-blue-400">
{{ config.accountGroups.indexOf(group) + 1 }}
</span>
</button>
@@ -152,15 +150,16 @@
class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 focus:outline-none p-1">
<svg v-if="showPasswordMap[`${gIndex}-${aIndex}`]" class="w-4 h-4"
fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round"
stroke-width="2"
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
<svg v-else class="w-4 h-4" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
<path stroke-linecap="round" stroke-linejoin="round"
stroke-width="2"
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
</svg>
</button>
@@ -183,16 +182,16 @@
<!-- 轮换设置 -->
<div class="mt-6 space-y-4">
<div class="flex items-center gap-4">
<label class="text-sm font-medium text-gray-700 w-28">国家语言</label>
<select :value="config.lang || 'en'"
@change="updateConfig('lang', $event.target.value)"
class="flex-1 px-3 py-2 text-sm text-gray-900 rounded-lg border border-gray-300 focus:border-blue-500 focus:outline-none bg-white">
<option v-for="lang in languageList" :key="lang.code" :value="lang.code">
{{ lang.name }} ({{ lang.code }})
</option>
</select>
</div>
<div class="flex items-center gap-4">
<label class="text-sm font-medium text-gray-700 w-28">国家语言</label>
<select :value="config.lang || 'en'"
@change="updateConfig('lang', $event.target.value)"
class="flex-1 px-3 py-2 text-sm text-gray-900 rounded-lg border border-gray-300 focus:border-blue-500 focus:outline-none bg-white">
<option v-for="lang in languageList" :key="lang.code" :value="lang.code">
{{ lang.name }} ({{ lang.code }})
</option>
</select>
</div>
<div class="flex items-center gap-4">
<label class="text-sm font-medium text-gray-700 w-28">轮换账号组</label>
<label class="relative inline-flex items-center cursor-pointer">
@@ -219,7 +218,7 @@
</label>
</div>
</div>
<div class="flex items-center gap-4">
<label class="text-sm font-medium text-gray-700 w-28">轮换间隔(分钟)</label>
<input type="number" min="1"
@@ -324,7 +323,37 @@
<HostListDialog :visible="showHostDialog" @close="showHostDialog = false" @save="() => { }" />
<!-- 打招呼内容弹窗 -->
<GreetingDialog :visible="showGreetingDialog" @close="showGreetingDialog = false" @confirm="handleGreetingConfirm" />
<GreetingDialog :visible="showGreetingDialog" @close="showGreetingDialog = false"
@confirm="handleGreetingConfirm" />
<!-- 预热 Loading 遮罩 -->
<transition name="fade">
<div v-if="warmingUp"
class="fixed inset-0 z-[9999] flex items-center justify-center bg-black/30 backdrop-blur-sm">
<div
class="bg-white/90 rounded-2xl shadow-2xl border border-white/60 px-6 py-5 flex items-center gap-4">
<!-- spinner -->
<div class="w-10 h-10 rounded-full border-4 border-gray-200 border-t-blue-500 animate-spin"></div>
<div class="space-y-1">
<div class="text-sm font-semibold text-gray-900">正在预热视图</div>
<div class="text-xs text-gray-500">
这会提升后台视图渲染稳定性请稍候
</div>
<!-- 可选进度小点点动画 -->
<div class="flex gap-1 pt-1">
<span
class="w-1.5 h-1.5 rounded-full bg-blue-400 animate-bounce [animation-delay:-0.2s]"></span>
<span
class="w-1.5 h-1.5 rounded-full bg-blue-400 animate-bounce [animation-delay:-0.1s]"></span>
<span class="w-1.5 h-1.5 rounded-full bg-blue-400 animate-bounce"></span>
</div>
</div>
</div>
</div>
</transition>
</div>
</template>
@@ -489,18 +518,24 @@ const loadConfig = async () => {
if (isElectronEnv) {
const saved = await window.electronAPI.loadRunConfig()
if (saved) {
config.value = { ...config.value, ...saved }
localStorage.setItem(CONFIG_KEY, JSON.stringify(saved))
// 排除 filters 字段,因为过滤器配置由 HostListDialog 独立管理
const { filters, ...runConfig } = saved
config.value = { ...config.value, ...runConfig }
localStorage.setItem(CONFIG_KEY, JSON.stringify(runConfig))
} else {
const localSaved = localStorage.getItem(CONFIG_KEY)
if (localSaved) {
config.value = { ...config.value, ...JSON.parse(localSaved) }
const parsed = JSON.parse(localSaved)
const { filters, ...runConfig } = parsed
config.value = { ...config.value, ...runConfig }
}
}
} else {
const localSaved = localStorage.getItem(CONFIG_KEY)
if (localSaved) {
config.value = { ...config.value, ...JSON.parse(localSaved) }
const parsed = JSON.parse(localSaved)
const { filters, ...runConfig } = parsed
config.value = { ...config.value, ...runConfig }
}
}
} catch (e) {
@@ -517,7 +552,11 @@ const saveToLocalStorage = () => {
const saveToFile = async () => {
if (!isElectronEnv) return
try {
await window.electronAPI.saveRunConfig(JSON.parse(JSON.stringify(config.value)))
// 只保存运行配置,不包含过滤器相关配置(过滤器由 HostListDialog 独立管理)
const configToSave = JSON.parse(JSON.stringify(config.value))
// 确保不覆盖 filters 配置
delete configToSave.filters
await window.electronAPI.saveRunConfig(configToSave)
} catch (e) {
console.error('保存配置失败:', e)
}
@@ -631,6 +670,7 @@ const handleSleepTimeInput = (val) => {
config.value.sleepTime = parseInt(val) || 0
}
}
const warmingUp = ref(false)
// Start/Stop
const handleStart = async (specificGroupIndex) => {
@@ -669,9 +709,7 @@ const handleStart = async (specificGroupIndex) => {
sleepTime: config.value.sleepTime,
inviteThreshold: config.value.inviteThreshold,
prologueList,
maxAnchorCount: config.value.maxAnchorCount,
rotationEnabled: config.value.rotateEnabled,
rotationIntervalMinutes: config.value.switchMinutes,
needTranslate: config.value.needTranslate, // 添加翻译开关配置
maxAnchorCount: config.value.maxAnchorCount,
rotationEnabled: config.value.rotateEnabled,
rotationIntervalMinutes: config.value.switchMinutes,
@@ -697,8 +735,8 @@ const handleStart = async (specificGroupIndex) => {
const cleanAccount = JSON.parse(JSON.stringify(acc))
startTasks.push({
viewId: currentViewId,
account: {
...cleanAccount,
account: {
...cleanAccount,
group: group.name,
lang: config.value.lang || 'en' // 传递语言配置
},
@@ -709,18 +747,58 @@ const handleStart = async (specificGroupIndex) => {
}
}
await Promise.allSettled(
// 预热所有视图,确保后台视图完成渲染,解决自动化执行失败问题
warmingUp.value = true
try {
console.log('[ConfigPage] 预热所有视图...')
await window.electronAPI.warmUpViews()
console.log('[ConfigPage] 视图预热完成')
} catch (e) {
console.warn('[ConfigPage] 视图预热失败,继续启动:', e)
}
const results = await Promise.allSettled(
startTasks.map(async ({ viewId, account, delay }) => {
await new Promise(r => setTimeout(r, delay))
return window.electronAPI.startTikTokAutomation(viewId, account)
})
)
// 检查启动结果
const failedResults = results
.map((r, i) => ({ result: r, task: startTasks[i] }))
.filter(({ result }) => {
if (result.status === 'rejected') return true
if (result.status === 'fulfilled' && !result.value.success) return true
return false
})
// 如果全部失败,显示错误并不跳转
if (failedResults.length === startTasks.length) {
const firstError = failedResults[0]
let errorMsg = '启动失败'
if (firstError.result.status === 'rejected') {
errorMsg = firstError.result.reason?.message || '启动失败'
} else if (firstError.result.status === 'fulfilled') {
errorMsg = firstError.result.value.error || '启动失败'
}
alert(`启动失败:${errorMsg}`)
return
}
// 如果部分失败,显示警告但继续
if (failedResults.length > 0) {
console.warn(`部分账号启动失败: ${failedResults.length}/${startTasks.length}`)
}
setIsRunning(true)
currentGroupIndex.value = activeGroupIndex
const status = await window.electronAPI.getRotationStatus()
rotationStatus.value = status
handleStatusChange(status)
warmingUp.value = false //关闭遮罩
emit('goToBrowser')
}
@@ -786,3 +864,14 @@ const togglePasswordVisibility = (gIndex, aIndex) => {
showPasswordMap.value[key] = !showPasswordMap.value[key]
}
</script>
<style scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>