From 6e67acca66c6c7da2a568f6578030497a231d8dd Mon Sep 17 00:00:00 2001 From: WangLeo <690854599@qq.com> Date: Mon, 1 Jun 2026 15:50:23 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8F=90=E4=BA=A4=E6=9C=80=E5=90=8E=E4=B8=80?= =?UTF-8?q?=E6=AC=A1=E7=A8=B3=E5=AE=9A=E7=89=88=EF=BC=8C=E4=B8=8B=E4=B8=AA?= =?UTF-8?q?=E6=8F=90=E4=BA=A4=E5=87=86=E5=A4=87=E4=BF=AE=E6=94=B9=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E8=AF=B7=E6=B1=82=E8=BF=9E=E6=8E=A5=E5=88=B0=E6=96=B0?= =?UTF-8?q?=E7=89=88=E7=AE=97=E5=8A=9B=E8=BD=AC=E5=8F=91=E5=90=8E=E7=AB=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 78 +++++++ out.txt | 178 ++++++-------- src/components/canvas/index.vue | 221 +++++++++++------- .../virtual-scroller/VirtualScroller.vue | 60 +++-- src/config/runninghub/index.js | 4 + src/utils/websocket copy.js | 189 +++++++++++++++ 6 files changed, 504 insertions(+), 226 deletions(-) create mode 100644 CLAUDE.md create mode 100644 src/utils/websocket copy.js diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..089acf2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,78 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## 常用命令 + +```bash +pnpm dev # 启动 Vite 开发服务器 +pnpm build # 生产构建 +pnpm preview # 预览生产构建 +``` + +## 技术栈 + +Vue 3 (Composition API) + Vite 7 + Pinia + Vue Router + Element Plus + Less + pnpm + +## 架构概览 + +这是一个 AI 绘画/视频生成的前端操作平台,通过 WebSocket 连接后端和第三方 AI 平台(RunningHub)提交生成任务并接收结果。 + +### 关键目录 + +``` +src/ +├── main.js # 入口:创建 Vue 应用,安装 Pinia/Router/VueVirtualScroller +├── router/index.js # 路由定义 + token 验证守卫 +├── stores/ # Pinia 状态管理 +│ ├── user.js # 用户认证、信息、免费次数 +│ ├── display.js # 生成历史列表、UI 状态(滚动、画布等) +│ └── param.js # 占位 store(当前为空) +├── apis/ # HTTP API 模块 +│ ├── auth/ # 登录/登出/用户信息/验证码 +│ └── display/ # 获取历史列表/收藏/删除 +├── components/ # 通用组件 +│ ├── dialogBox/ # 生成参数输入面板(核心交互入口),含模型选择、比例、上传等子组件 +│ ├── virtual-scroller/# 虚拟滚动列表组件(自定义实现) +│ ├── canvas/ # 图片画布编辑(圆/矩形选区,局部重绘) +│ └── ... +├── views/ # 页面 +│ ├── home/index.vue # 主页面容器(dialogBox + display) +│ ├── home/display/ # 历史记录展示区 +│ └── login/ # 登录页(跳转外部登录) +├── utils/ +│ ├── request.js # Axios 实例,拦截器处理 token 和不同服务的 baseURL 路由 +│ ├── websocket.js # WebSocket 生成任务的核心流程(心跳、提交流程、结果处理) +│ ├── createTask.js # 根据配置构造任务 payload +│ ├── modelConfig.js # 从远程 JSON 加载模型配置,localStorage 每日缓存 +│ ├── auth.ts # token 存取工具(localStorage) +│ └── encrypt.ts # 加密工具(Base64/MD5/RSA/AES) +├── config/ +│ ├── index.js # 平台配置入口 +│ └── runninghub/ # RunningHub 平台适配器:Payload 构造和 Result 解析 +└── config/ # 项目根目录下 + └── plugins.js # Vite 插件配置(自动导入/组件注册/图标) +``` + +### 核心数据流 + +1. 用户在 `dialogBox` 中设置参数(模型、提示词、比例、上传图片等) +2. 点击生成 → `websocket.js:generate()` 被调用 +3. 先通过 `createTask.js` 调用 `config/runninghub` 的 `Playload()` 构造任务数据(从远程 JSON 加载 workflow 配置) +4. 建立 WebSocket 连接,经过握手协议(`please give me taskId` → `OK! Please continue.`)提交任务 +5. 任务排队中 → `displayStore.addGeneratingItem()` 在前端列表中插入 "生成中" 条目 +6. 完成后 WebSocket 关闭(code=1000 reason=success) → `getTask()` 解析结果 URL → `updateItemToSuccess()` 更新列表 + +### 自动导入 + +- `unplugin-auto-import` 自动导入 Vue/VRouter/Pinia API,无需在 `.vue` 文件中手动 `import { ref, computed, watch } from 'vue'` +- `unplugin-vue-components` 自动注册 `src/components/` 下的组件和 Element Plus 组件 +- Element Plus 图标通过 `unplugin-icons` 按需加载 + +### 环境变量 + +有两套环境文件。`VITE_API_BASE_URL` 定义主 API 地址,请求拦截器根据 URL 前缀自动切换不同的后端服务(主服务/支付服务/AIGC 工作流服务)。`VITE_API_WORKFLOW_WS` 定义 WebSocket 地址。 + +### 路由守卫 + +`src/router/index.js` 的 `beforeEach` 守卫检查 token 存在性和有效性(调用 `/auth/check/token`),无效则跳转 `/login`。支持通过 URL query `?token=xxx` 传入 token。 diff --git a/out.txt b/out.txt index 4a1b822..17c0420 100644 --- a/out.txt +++ b/out.txt @@ -1,113 +1,65 @@ -
生成图片
index
重新编辑
再次生成
删除该批次
图片编辑
index
重新编辑
再次生成
删除该批次
生成图片
index
重新编辑
再次生成
删除该批次
生成图片
index
重新编辑
再次生成
删除该批次
图片生成
index
重新编辑
再次生成
删除该批次
生成图片
index
重新编辑
再次生成
删除该批次
生成图片
index
重新编辑
再次生成
删除该批次
生成图片
index
重新编辑
再次生成
删除该批次
生成图片
index
重新编辑
再次生成
删除该批次
生成图片
index
重新编辑
再次生成
删除该批次
生成图片
index
重新编辑
再次生成
删除该批次
生成图片
index
重新编辑
再次生成
删除该批次
生成图片
index
index
index
index
重新编辑
再次生成
删除该批次
\ No newline at end of file + 接口文档 + + 根据平台编码获取可学官方模型 + + 请求 + + GET /suanli/v1/platforms/:code/models + + ┌──────┬────────┬──────┬─────────────────────────────────────────────────────┐ + │ 参数 │ 类型 │ 必填 │ 说明 │ + ├──────┼────────┼──────┼─────────────────────────────────────────────────────┤ + │ code │ string │ 是 │ 平台编码(platform_identifiers.code),URL 路径参数 │ + └──────┴────────┴──────┴─────────────────────────────────────────────────────┘ + + 请求头 + + Authorization: + + ▎ 无需 Bearer 前缀。 + + 响应 + + 成功 + + { + "code": 0, + "data": { + "platform": { + "id": "uuid", + "code": "openai", + "name": "OpenAI" + }, + "models": [ + { + "id": "uuid", + "name": "gpt-4", + "display_name": "GPT-4", + "category": "llm", + "billing_unit": "token", + "unit_price": 0.03, + "billing_mode": "post", + "plugin_code": null, + "endpoint": null, + "sort_order": 1, + "is_public": 1, + "owner_org_id": null + } + ] + } + } + + ┌───────────────┬──────────────────────────────────────────────────────────────────────────────────────────┐ + │ 字段 │ 说明 │ + ├───────────────┼──────────────────────────────────────────────────────────────────────────────────────────┤ + │ data.platform │ 平台信息 │ + ├───────────────┼──────────────────────────────────────────────────────────────────────────────────────────┤ + │ data.models │ 该平台下 owner_type=platform 且 status=active 的模型列表,按 sort_order、created_at 排序 │ + └───────────────┴──────────────────────────────────────────────────────────────────────────────────────────┘ + + 平台不存在或已禁用 + + { + "code": 1, + "message": "平台不存在或已禁用" + } \ No newline at end of file diff --git a/src/components/canvas/index.vue b/src/components/canvas/index.vue index cc353ab..8f218d2 100644 --- a/src/components/canvas/index.vue +++ b/src/components/canvas/index.vue @@ -545,96 +545,6 @@ const handleClose = () => { promptHistoryIndex.value = -1 } -const handleSend = async () => { - if (isSending.value) return - if (!inputText.value) { - ElMessage.error('请输入提示词') - return - } - - isSending.value = true - - try { - const canvas = canvasRef.value - const imageData = canvas.toDataURL('image/png') - - const imgs = [] - if (imageData) { - imgs.push({ name: 'image_1', url: imageData }) - } - - allReferenceImages.value.forEach((img, index) => { - imgs.push({ name: `image_${index + 2}`, url: img }) - }) - - const uploadImg = async (imgItem) => { - if (!imgItem.url.startsWith('data:') && !imgItem.url.startsWith('blob:')) { - return imgItem - } - - const response = await fetch(imgItem.url) - const blob = await response.blob() - const file = new File([blob], `${imgItem.name}.png`, { type: 'image/png' }) - - const formData = new FormData() - formData.append('file', file) - - try { - const result = await request({ - url: import.meta.env.VITE_API_WORKFLOW_UPLOAD, - method: 'POST', - data: formData, - headers: { - 'Content-Type': 'multipart/form-data' - } - }) - if (result.success || result.code === 0) { - return { name: imgItem.name, url: result.url } - } - return imgItem - } catch (error) { - console.error('上传失败:', error) - return imgItem - } - } - - const uploadedImgs = await Promise.all(imgs.map(uploadImg)) - - const generateData = { - model: 'banana', - modelType: 'edit', - prompt: inputText.value, - proportion: '比例自动' - } - const data = { - type: props.type, - modelType: 'edit', - AIGC: 'Painting', - platform: 'runninghub', - modelName: 'banana', - quantity: 1, - free: useUserStore().freeTimes, - params: [ - { name: 'prompt', data: inputText.value + '并且去除掉图1中的框' }, - { name: 'index', data: 1 }, - ], - imgs: uploadedImgs, - result: JSON.stringify(generateData) - } - - emit('send', { - image: imageData, - text: inputText.value, - shapes: shapes.value - }) - - await generate(data, generateData) - handleClose() - } finally { - isSending.value = false - } -} - const removeReferenceImage = (index) => { allReferenceImages.value.splice(index, 1) } @@ -719,6 +629,137 @@ const handleBrushConfirm = () => { selectedReferenceImages.value = [] currentEditingContent.value = '' } + +const getImageAspectRatio = () => { + if (!bgImage.value) { + return null + } + + const width = bgImage.value.width + const height = bgImage.value.height + + const aspectRatios = [ + { ratio: '4:3', value: 4 / 3 }, + { ratio: '16:9', value: 16 / 9 }, + { ratio: '9:16', value: 9 / 16 }, + { ratio: '3:4', value: 3 / 4 }, + { ratio: '1:1', value: 1 / 1 }, + { ratio: '2:3', value: 2 / 3 }, + { ratio: '3:2', value: 3 / 2 } + ] + + const currentRatio = width / height + + let closest = aspectRatios[0] + let minDiff = Math.abs(currentRatio - closest.value) + + for (const item of aspectRatios) { + const diff = Math.abs(currentRatio - item.value) + if (diff < minDiff) { + minDiff = diff + closest = item + } + } + + return { + width, + height, + aspectRatio: closest.ratio + } +} + +const handleSend = async () => { + if (isSending.value) return + if (!inputText.value) { + ElMessage.error('请输入提示词') + return + } + + isSending.value = true + + try { + const canvas = canvasRef.value + const imageData = canvas.toDataURL('image/png') + + const imgs = [] + if (imageData) { + imgs.push({ name: 'image_1', url: imageData }) + } + + allReferenceImages.value.forEach((img, index) => { + imgs.push({ name: `image_${index + 2}`, url: img }) + }) + + const uploadImg = async (imgItem) => { + if (!imgItem.url.startsWith('data:') && !imgItem.url.startsWith('blob:')) { + return imgItem + } + + const response = await fetch(imgItem.url) + const blob = await response.blob() + const file = new File([blob], `${imgItem.name}.png`, { type: 'image/png' }) + + const formData = new FormData() + formData.append('file', file) + + try { + const result = await request({ + url: import.meta.env.VITE_API_WORKFLOW_UPLOAD, + method: 'POST', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) + if (result.success || result.code === 0) { + return { name: imgItem.name, url: result.url } + } + return imgItem + } catch (error) { + console.error('上传失败:', error) + return imgItem + } + } + + const uploadedImgs = await Promise.all(imgs.map(uploadImg)) + + const proportion = getImageAspectRatio() + + const generateData = { + model: 'GPT-image2.0', + modelType: 'edit', + prompt: inputText.value, + proportion: '比例自动' + } + const data = { + type: props.type, + modelType: 'edit', + AIGC: 'Painting', + platform: 'runninghub', + modelName: 'GPT', + quantity: 1, + free: useUserStore().freeTimes, + params: [ + { name: 'prompt', data: inputText.value + '并且去除掉图1中的框' }, + { name: 'index', data: 1 }, + { name: 'proportion', data: proportion?.aspectRatio || '4:3' }, + ], + imgs: uploadedImgs, + result: JSON.stringify(generateData) + } + + emit('send', { + image: imageData, + text: inputText.value, + shapes: shapes.value + }) + + await generate(data, generateData) + handleClose() + } finally { + isSending.value = false + } +}