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 @@
-
\ 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
+ }
+}