Initial commit: MSH System\n\n- msh_single_uniapp: Vue 2 + UniApp 前端(微信小程序/H5/App/支付宝小程序)\n- msh_crmeb_22: Spring Boot 2.2 后端(C端API/管理端/业务逻辑)\n- models-integration: AI服务集成(Coze/KieAI/腾讯ASR)\n- docs: 产品文档与设计稿
This commit is contained in:
727
msh_single_uniapp/pages/ai-generate/README.md
Normal file
727
msh_single_uniapp/pages/ai-generate/README.md
Normal file
@@ -0,0 +1,727 @@
|
||||
# AI生成图片功能模块 - 页面文档
|
||||
|
||||
## 📁 目录结构
|
||||
|
||||
```
|
||||
pages/ai-generate/
|
||||
├── index.vue # 主生成页面(配置和输入)
|
||||
├── result.vue # 结果展示页面
|
||||
├── inspiration.vue # 灵感广场页面
|
||||
├── history.vue # 历史记录页面
|
||||
└── README.md # 说明文档
|
||||
```
|
||||
|
||||
## 📄 页面说明
|
||||
|
||||
### 1. index.vue - 主生成页面
|
||||
|
||||
**功能描述:**
|
||||
AI图片生成的主界面,用户可以配置生成参数并提交生成请求。
|
||||
|
||||
**主要功能模块:**
|
||||
- ✅ 参考图上传(最多3张)
|
||||
- ✅ 提示词输入(文本/语音),
|
||||
腾讯云语音识别API文档:https://cloud.tencent.com/document/product/1093/35637
|
||||
API SecretId: AKIDf9OM3TdWBZqqZ1C7k6B0Ypqb6KIzQaT5
|
||||
- ✅ AI模型选择(4.0 / 3.1 / 3.0 / 2.1 / 2.0 PRO)
|
||||
- ✅ 生成比例选择(9:16 / 3:4 / 2:3 / 1:1 / 3:2)
|
||||
- ✅ 图片质量选择(2K / 4K)
|
||||
- ✅ 联想词助手开关
|
||||
- ✅ 生成类型切换(图片/视频)
|
||||
|
||||
**页面路由:**
|
||||
```
|
||||
/pages/ai-generate/index
|
||||
```
|
||||
|
||||
**跳转示例:**
|
||||
```javascript
|
||||
// 基础跳转
|
||||
uni.navigateTo({
|
||||
url: '/pages/ai-generate/index'
|
||||
});
|
||||
|
||||
// 带模板ID跳转(从灵感广场)
|
||||
uni.navigateTo({
|
||||
url: '/pages/ai-generate/index?templateId=123'
|
||||
});
|
||||
|
||||
// 带草稿ID跳转(继续编辑)
|
||||
uni.navigateTo({
|
||||
url: '/pages/ai-generate/index?draftId=456'
|
||||
});
|
||||
```
|
||||
|
||||
**UI特点:**
|
||||
- 深色主题(渐变背景:#000000 → #1a1a1a)
|
||||
- 毛玻璃效果(backdrop-filter: blur)
|
||||
- 圆角卡片设计(16px border-radius)
|
||||
- 主题色:亮绿色(#42ca4d)
|
||||
|
||||
---
|
||||
|
||||
### 2. result.vue - 结果展示页面
|
||||
|
||||
**功能描述:**
|
||||
展示AI生成的图片结果,支持预览、下载、分享等操作。
|
||||
|
||||
**主要功能模块:**
|
||||
- ✅ 生成进度显示(生成中状态)
|
||||
- ✅ 2x2宫格图片展示
|
||||
- ✅ 图片预览(Swiper全屏预览)
|
||||
- ✅ 生成信息展示(提示词、参数)
|
||||
- ✅ 重新编辑
|
||||
- ✅ 再次生成
|
||||
- ✅ 下载保存
|
||||
- ✅ 分享功能
|
||||
- ✅ 应用到商品
|
||||
|
||||
**页面路由:**
|
||||
```
|
||||
/pages/ai-generate/result
|
||||
```
|
||||
|
||||
**跳转示例:**
|
||||
```javascript
|
||||
// 新生成跳转(显示生成动画)
|
||||
uni.navigateTo({
|
||||
url: '/pages/ai-generate/result?generate=true'
|
||||
});
|
||||
|
||||
// 查看历史记录
|
||||
uni.navigateTo({
|
||||
url: '/pages/ai-generate/result?historyId=123'
|
||||
});
|
||||
```
|
||||
|
||||
**UI特点:**
|
||||
- 宫格布局(2列,gap: 12px)
|
||||
- 图片标签(模型版本、比例)
|
||||
- 加载动画(旋转spinner)
|
||||
- 底部固定操作栏
|
||||
|
||||
---
|
||||
|
||||
### 3. inspiration.vue - 灵感广场页面
|
||||
|
||||
**功能描述:**
|
||||
展示AI创作灵感作品,用户可以浏览、收藏、使用模板。
|
||||
|
||||
**主要功能模块:**
|
||||
- ✅ 分类标签切换
|
||||
- ✅ 瀑布流布局
|
||||
- ✅ 作品卡片展示
|
||||
- ✅ 点赞统计
|
||||
- ✅ 一键使用模板
|
||||
- ✅ 下拉刷新
|
||||
- ✅ 上拉加载更多
|
||||
- ✅ 底部Tab导航
|
||||
|
||||
**页面路由:**
|
||||
```
|
||||
/pages/ai-generate/inspiration
|
||||
```
|
||||
|
||||
**跳转示例:**
|
||||
```javascript
|
||||
// 直接跳转
|
||||
uni.navigateTo({
|
||||
url: '/pages/ai-generate/inspiration'
|
||||
});
|
||||
|
||||
// 切换到指定分类
|
||||
uni.navigateTo({
|
||||
url: '/pages/ai-generate/inspiration?category=banner'
|
||||
});
|
||||
```
|
||||
|
||||
**UI特点:**
|
||||
- 瀑布流布局(2列自适应高度)
|
||||
- 固定顶部导航(带分类标签横滑)
|
||||
- 固定底部Tab栏
|
||||
- 作品卡片圆角(16px)
|
||||
- 懒加载图片
|
||||
|
||||
**分类列表:**
|
||||
- 🔥 热门玩法
|
||||
- ✨ 视频特效
|
||||
- 📷 人像写真
|
||||
- 商品海报
|
||||
- 活动Banner
|
||||
- 节日主题
|
||||
|
||||
---
|
||||
|
||||
### 4. history.vue - 历史记录页面
|
||||
|
||||
**功能描述:**
|
||||
展示用户的AI生成历史记录,支持查看、管理、再次生成。
|
||||
|
||||
**主要功能模块:**
|
||||
- ✅ Tab筛选(全部/已完成/草稿)
|
||||
- ✅ 历史列表展示
|
||||
- ✅ 生成状态显示
|
||||
- ✅ 查看详情
|
||||
- ✅ 再次生成
|
||||
- ✅ 下载保存
|
||||
- ✅ 分享
|
||||
- ✅ 删除记录
|
||||
- ✅ 清空历史
|
||||
- ✅ 空状态展示
|
||||
|
||||
**页面路由:**
|
||||
```
|
||||
/pages/ai-generate/history
|
||||
```
|
||||
|
||||
**跳转示例:**
|
||||
```javascript
|
||||
uni.navigateTo({
|
||||
url: '/pages/ai-generate/history'
|
||||
});
|
||||
```
|
||||
|
||||
**UI特点:**
|
||||
- 列表项布局(左图右文)
|
||||
- 状态标识(生成中/失败/草稿)
|
||||
- 进度条显示(生成中状态)
|
||||
- 操作菜单(底部弹窗)
|
||||
- 空状态引导
|
||||
|
||||
**记录状态:**
|
||||
- ⏳ `processing` - 生成中
|
||||
- ✅ `completed` - 已完成
|
||||
- ❌ `failed` - 失败
|
||||
- 📝 `draft` - 草稿
|
||||
|
||||
---
|
||||
|
||||
## 🎨 UI设计规范
|
||||
|
||||
### 色彩系统
|
||||
|
||||
| 用途 | 颜色值 | 说明 |
|
||||
|------|--------|------|
|
||||
| 主题色 | #42ca4d | 亮绿色,用于按钮、高亮 |
|
||||
| 背景渐变起 | #000000 | 纯黑色 |
|
||||
| 背景渐变止 | #1a1a1a | 深灰色 |
|
||||
| 卡片背景 | rgba(26, 31, 46, 0.6) | 半透明深色 |
|
||||
| 文字主色 | #ffffff | 白色 |
|
||||
| 文字次色 | #8f9bb3 | 灰蓝色 |
|
||||
| 文字辅助 | #6b7589 | 暗灰色 |
|
||||
| 边框色 | rgba(255, 255, 255, 0.05) | 半透明白色 |
|
||||
|
||||
### 圆角规范
|
||||
|
||||
| 元素 | 圆角值 |
|
||||
|------|--------|
|
||||
| 大卡片 | 16px |
|
||||
| 中卡片 | 12px |
|
||||
| 小卡片 | 8px |
|
||||
| 按钮 | 12px |
|
||||
| 圆形按钮 | 50% |
|
||||
|
||||
### 间距规范
|
||||
|
||||
| 类型 | 间距值 |
|
||||
|------|--------|
|
||||
| 页面边距 | 16px |
|
||||
| 卡片间距 | 12px |
|
||||
| 元素间距 | 8px |
|
||||
| 小间距 | 4px |
|
||||
|
||||
### 字体规范
|
||||
|
||||
| 用途 | 字号 | 字重 |
|
||||
|------|------|------|
|
||||
| 标题 | 17-20px | 600 |
|
||||
| 正文 | 14-15px | 400 |
|
||||
| 辅助文字 | 12-13px | 400 |
|
||||
| 小文字 | 10-11px | 400 |
|
||||
|
||||
---
|
||||
|
||||
## 🔧 技术实现
|
||||
|
||||
### 依赖组件
|
||||
|
||||
本模块使用uni-app原生组件,无需额外依赖:
|
||||
|
||||
- `<scroll-view>` - 滚动容器
|
||||
- `<swiper>` - 轮播图(用于图片预览)
|
||||
- `<textarea>` - 多行文本输入
|
||||
- `<switch>` - 开关
|
||||
- `<image>` - 图片显示
|
||||
|
||||
### 核心功能实现
|
||||
|
||||
#### 1. 参考图上传
|
||||
|
||||
```javascript
|
||||
// 选择图片
|
||||
uni.chooseImage({
|
||||
count: 3 - this.referenceImages.length,
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: (res) => {
|
||||
this.referenceImages.push(...res.tempFilePaths);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### 2. 语音输入
|
||||
|
||||
```javascript
|
||||
// 开始录音(需要调用实际的语音识别API)
|
||||
startVoiceInput() {
|
||||
this.isRecording = true;
|
||||
// TODO: 调用语音识别API
|
||||
uni.showToast({
|
||||
title: '钟意啦~',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. 瀑布流布局
|
||||
|
||||
```javascript
|
||||
// 分配数据到左右列
|
||||
distributeData(newData) {
|
||||
newData.forEach(item => {
|
||||
if (this.leftColumnData.length <= this.rightColumnData.length) {
|
||||
this.leftColumnData.push(item);
|
||||
} else {
|
||||
this.rightColumnData.push(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. 图片预览
|
||||
|
||||
```javascript
|
||||
// 全屏预览(使用Swiper)
|
||||
previewImage(index) {
|
||||
this.currentPreviewIndex = index;
|
||||
this.showPreview = true;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔌 API接口集成点
|
||||
|
||||
以下是需要对接的API接口列表(当前为TODO状态):
|
||||
|
||||
|
||||
|
||||
|
||||
### 生成相关
|
||||
|
||||
```javascript
|
||||
// 1. 提交生成任务
|
||||
POST /api/ai/image/generate
|
||||
Request: {
|
||||
prompt, referenceImages, model, aspectRatio, quality, count
|
||||
}
|
||||
|
||||
// 2. 查询生成状态
|
||||
GET /api/ai/image/status/{taskId}
|
||||
|
||||
// 3. 上传参考图
|
||||
POST /api/upload/image
|
||||
```
|
||||
|
||||
### 历史记录
|
||||
|
||||
```javascript
|
||||
// 4. 获取历史列表
|
||||
GET /api/ai/history/list?page=1&limit=20&status=all
|
||||
|
||||
// 5. 删除历史记录
|
||||
DELETE /api/ai/history/{id}
|
||||
|
||||
// 6. 保存草稿
|
||||
POST /api/ai/draft/save
|
||||
```
|
||||
|
||||
### 灵感广场
|
||||
|
||||
```javascript
|
||||
// 7. 获取灵感作品列表
|
||||
GET /api/ai/inspiration/list?category=hot&page=1
|
||||
|
||||
// 8. 获取作品详情
|
||||
GET /api/ai/inspiration/detail/{id}
|
||||
|
||||
// 9. 点赞作品
|
||||
POST /api/ai/inspiration/like/{id}
|
||||
```
|
||||
|
||||
### 其他
|
||||
|
||||
```javascript
|
||||
// 10. 语音识别
|
||||
POST /api/voice/recognize
|
||||
|
||||
// 11. 图片下载
|
||||
GET /api/ai/image/download/{id}
|
||||
|
||||
// 12. 分享生成
|
||||
POST /api/share/generate
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📱 页面路由配置
|
||||
|
||||
需要在 `pages.json` 中添加以下配置:
|
||||
|
||||
```json
|
||||
{
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/ai-generate/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "AI图片生成",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/ai-generate/result",
|
||||
"style": {
|
||||
"navigationBarTitleText": "生成结果",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/ai-generate/inspiration",
|
||||
"style": {
|
||||
"navigationBarTitleText": "灵感广场",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/ai-generate/history",
|
||||
"style": {
|
||||
"navigationBarTitleText": "历史创作",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 使用流程
|
||||
|
||||
### 标准生成流程
|
||||
|
||||
```
|
||||
1. 用户进入生成页面(index.vue)
|
||||
↓
|
||||
2. [可选] 上传参考图
|
||||
↓
|
||||
3. 输入提示词(文本或语音)
|
||||
↓
|
||||
4. 选择模型、比例、质量
|
||||
↓
|
||||
5. 点击"生成"按钮
|
||||
↓
|
||||
6. 跳转到结果页(result.vue)
|
||||
↓
|
||||
7. 显示生成进度
|
||||
↓
|
||||
8. 生成完成,展示结果
|
||||
↓
|
||||
9. 下载/分享/应用
|
||||
```
|
||||
|
||||
### 模板快速生成
|
||||
|
||||
```
|
||||
1. 用户进入灵感广场(inspiration.vue)
|
||||
↓
|
||||
2. 浏览作品
|
||||
↓
|
||||
3. 点击"一键替换"
|
||||
↓
|
||||
4. 自动跳转到生成页(index.vue)
|
||||
↓
|
||||
5. 自动填充模板参数
|
||||
↓
|
||||
6. [可选] 微调修改
|
||||
↓
|
||||
7. 生成图片
|
||||
```
|
||||
|
||||
### 历史记录查看
|
||||
|
||||
```
|
||||
1. 用户进入历史记录(history.vue)
|
||||
↓
|
||||
2. 查看历史列表
|
||||
↓
|
||||
3. 点击记录
|
||||
↓
|
||||
4. 跳转到结果页查看详情
|
||||
↓
|
||||
5. [可选] 再次生成/下载/分享
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
### 1. 图片处理
|
||||
|
||||
- 参考图上传前建议压缩(使用 `sizeType: ['compressed']`)
|
||||
- 图片预览使用懒加载(`lazy-load`)
|
||||
- 大图使用缩略图优先显示
|
||||
|
||||
### 2. 性能优化
|
||||
|
||||
- 瀑布流使用虚拟列表(数据量大时)
|
||||
- 图片使用CDN加速
|
||||
- 接口请求添加防抖
|
||||
|
||||
### 3. 兼容性
|
||||
|
||||
- 所有页面使用 `navigationStyle: "custom"` 自定义导航栏
|
||||
- 考虑刘海屏适配(使用 `statusBarHeight`)
|
||||
- 底部安全区适配(`padding-bottom: env(safe-area-inset-bottom)`)
|
||||
|
||||
### 4. 待完成功能
|
||||
|
||||
- [x] API接口对接(已完成)
|
||||
- [x] 语音识别集成(已完成 - 腾讯云ASR)
|
||||
- [ ] 图片下载功能
|
||||
- [ ] 分享功能
|
||||
- [ ] 图片编辑功能
|
||||
- [ ] 批量操作
|
||||
|
||||
---
|
||||
|
||||
## 语音识别API接口对接 ✅
|
||||
|
||||
### 实现状态
|
||||
**已完成对接**,使用腾讯云语音识别服务(ASR)。
|
||||
|
||||
### 功能说明
|
||||
用户点击麦克风按钮录音后,系统会:
|
||||
1. 上传录音文件到服务器
|
||||
2. 创建语音识别任务
|
||||
3. 轮询查询识别结果(最多30次,每次间隔2秒)
|
||||
4. 将识别文本自动填充到提示词输入框
|
||||
|
||||
### 技术实现
|
||||
|
||||
#### API接口封装
|
||||
在 `utils/api.js` 中已添加:
|
||||
- `createAsrTask()` - 创建语音识别任务
|
||||
- `queryAsrStatus()` - 查询识别任务状态
|
||||
|
||||
#### 核心方法
|
||||
在 `pages/ai-generate/index.vue` 中已实现:
|
||||
- `recognizeSpeech()` - 主识别流程
|
||||
- `pollAsrResult()` - 轮询查询结果
|
||||
- `parseAsrResult()` - 解析识别文本
|
||||
|
||||
### 使用示例
|
||||
|
||||
```javascript
|
||||
// 开始录音
|
||||
startVoiceInput() {
|
||||
// 检查权限并开始录音
|
||||
this.recorderManager.start({
|
||||
duration: 60000,
|
||||
sampleRate: 16000,
|
||||
numberOfChannels: 1,
|
||||
format: 'mp3'
|
||||
});
|
||||
}
|
||||
|
||||
// 停止录音并识别
|
||||
async stopVoiceInput() {
|
||||
this.recorderManager.stop();
|
||||
// 录音结束后自动调用 handleRecordResult
|
||||
// 然后调用 recognizeSpeech 进行识别
|
||||
}
|
||||
```
|
||||
|
||||
### API接口详情
|
||||
|
||||
#### 1.创建录音文件识别任务
|
||||
**接口地址:** https://jxz.uj345.cc/models/api/tencent/asr/create-task
|
||||
|
||||
**请求方法:** POST
|
||||
|
||||
**请求参数:**
|
||||
```json
|
||||
{
|
||||
"url": "https://jxz.uj345.cc/static/images/xhs-wechat.mp3",
|
||||
"engineModelType": "16k_zh",
|
||||
"channelNum": 1,
|
||||
"resTextFormat": 0,
|
||||
"sourceType": 0,
|
||||
"filterDirty": false,
|
||||
"filterModal": false,
|
||||
"convertNumMode": false,
|
||||
"wordInfo": false
|
||||
}
|
||||
```
|
||||
|
||||
**响应示例:**
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"taskId": "13473617411",
|
||||
"status": 0,
|
||||
"statusStr": "等待处理",
|
||||
"result": null,
|
||||
"audioDuration": null,
|
||||
"errorCode": 0,
|
||||
"errorMsg": "成功",
|
||||
"resultDetail": null,
|
||||
"requestId": "07ef98e3-8238-4cb8-8d81-b807af3b3bb8"
|
||||
}
|
||||
}
|
||||
|
||||
#### 2.查询语音识别任务状态
|
||||
**接口地址:** https://jxz.uj345.cc/models/api/tencent/asr/query-status/{taskId}
|
||||
|
||||
**请求方法:** GET
|
||||
|
||||
**路径参数:**
|
||||
- `taskId` - 任务ID(从创建任务接口返回)
|
||||
|
||||
**请求示例:**
|
||||
```
|
||||
GET https://jxz.uj345.cc/models/api/tencent/asr/query-status/13473617411
|
||||
```
|
||||
|
||||
**响应示例:**
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"taskId": "13473617411",
|
||||
"status": 2,
|
||||
"statusStr": "识别成功",
|
||||
"result": "[0:0.000,0:11.580] 兄弟们,全自动小红书创作智能体来了,小金帮我找一下小红书上关于常州老房翻新的最新爆款笔记。\n[0:29.600,0:38.160] 帮我把第八条雨后森林的笔记改写一下。\n[0:48.720,0:50.780] 好的,帮我发布一下。\n[1:3.800,1:8.260] AI智能体控制微信聊天机器人。\n[1:32.440,1:41.929] 老板们安排10个AI智能体员工给你做牛马,每天帮你谈单,做自媒体账号,你可以找客户喝茶啦。\n",
|
||||
"audioDuration": 101.92981,
|
||||
"errorCode": 0,
|
||||
"errorMsg": "",
|
||||
"resultDetail": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 状态码说明
|
||||
|
||||
| 状态码 | 说明 |
|
||||
|--------|------|
|
||||
| 0 | 等待处理 |
|
||||
| 1 | 处理中 |
|
||||
| 2 | 识别成功 |
|
||||
| 3 | 识别失败 |
|
||||
|
||||
### 识别流程说明
|
||||
|
||||
```
|
||||
用户点击麦克风
|
||||
↓
|
||||
开始录音(最长60秒)
|
||||
↓
|
||||
用户点击停止
|
||||
↓
|
||||
上传录音文件到服务器
|
||||
↓
|
||||
创建ASR识别任务
|
||||
↓
|
||||
轮询查询任务状态(每2秒查询一次,最多30次)
|
||||
↓
|
||||
状态 = 2(识别成功)
|
||||
↓
|
||||
解析识别文本(去除时间戳)
|
||||
↓
|
||||
自动填充到提示词输入框
|
||||
```
|
||||
|
||||
### 注意事项
|
||||
|
||||
1. **录音权限**:首次使用需要用户授权录音权限
|
||||
2. **录音格式**:MP3格式,采样率16000Hz,单声道
|
||||
3. **录音时长**:最短1秒,最长60秒
|
||||
4. **识别超时**:最多等待60秒(30次 × 2秒)
|
||||
5. **网络要求**:需要稳定的网络连接
|
||||
6. **平台支持**:
|
||||
- ✅ 微信小程序
|
||||
- ✅ APP
|
||||
- ❌ H5(浏览器限制)
|
||||
|
||||
### 错误处理
|
||||
|
||||
| 错误 | 说明 | 处理方式 |
|
||||
|------|------|----------|
|
||||
| 录音时间太短 | 录音时长 < 1秒 | 提示用户重新录音 |
|
||||
| 上传失败 | 网络异常 | 提示用户检查网络 |
|
||||
| 识别超时 | 60秒内未完成 | 提示用户重试 |
|
||||
| 识别失败 | 音频质量问题 | 建议在安静环境录音 |
|
||||
| 权限拒绝 | 用户未授权 | 引导用户开启权限 |
|
||||
|
||||
---
|
||||
|
||||
## 📦 静态资源
|
||||
|
||||
需要准备的图片资源:
|
||||
|
||||
```
|
||||
static/images/
|
||||
├── model-4.0.png # AI模型4.0图标
|
||||
├── model-3.1.png # AI模型3.1图标
|
||||
├── model-3.0.png # AI模型3.0图标
|
||||
├── model-2.1.png # AI模型2.1图标
|
||||
├── model-2.0.png # AI模型2.0图标
|
||||
├── avatar-default.png # 默认头像
|
||||
├── empty-history.png # 历史记录空状态
|
||||
└── demo-result-*.jpg # 示例结果图(用于演示)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
1. 将4个页面文件放入 `pages/ai-generate/` 目录
|
||||
2. 在 `pages.json` 中添加路由配置
|
||||
3. 准备静态资源图片
|
||||
4. 配置图标字体(iconfont)
|
||||
5. 运行项目查看效果
|
||||
|
||||
```bash
|
||||
# H5
|
||||
npm run dev:h5
|
||||
|
||||
# 微信小程序
|
||||
npm run dev:mp-weixin
|
||||
|
||||
# App
|
||||
在HBuilderX中运行
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📞 联系方式
|
||||
|
||||
如有问题,请联系项目负责人。
|
||||
|
||||
---
|
||||
|
||||
**最后更新:** 2025年11月5日
|
||||
**版本:** v1.1 - 语音识别API已对接完成
|
||||
|
||||
683
msh_single_uniapp/pages/ai-generate/agent.vue
Normal file
683
msh_single_uniapp/pages/ai-generate/agent.vue
Normal file
@@ -0,0 +1,683 @@
|
||||
<template>
|
||||
<view class="agent-container" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<!-- 顶部导航 -->
|
||||
<view class="nav-bar">
|
||||
<view class="nav-title">智能体</view>
|
||||
</view>
|
||||
|
||||
<scroll-view scroll-y class="main-content" :style="{ height: scrollViewHeight }">
|
||||
<!-- Banner区域 -->
|
||||
<view class="banner-section">
|
||||
<view class="banner-content">
|
||||
<view class="banner-left">
|
||||
<view class="banner-icon">
|
||||
<image src="/static/images/logo-white.png" mode="aspectFit" class="logo-img"></image>
|
||||
</view>
|
||||
</view>
|
||||
<view class="banner-right">
|
||||
<view class="chat-bubble">
|
||||
<text class="bubble-title">我是鲸小智</text>
|
||||
<text class="bubble-desc">您的专属旧改助理!</text>
|
||||
</view>
|
||||
<view class="sparkle-group">
|
||||
<text class="sparkle sparkle-1">✨</text>
|
||||
<text class="sparkle sparkle-2">✨</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<view class="search-section">
|
||||
<view class="search-bar">
|
||||
<text class="iconfont icon-sousuo search-icon"></text>
|
||||
<input
|
||||
type="text"
|
||||
class="search-input"
|
||||
placeholder="搜索您想要的内容"
|
||||
placeholder-class="search-placeholder"
|
||||
v-model="searchText"
|
||||
@confirm="handleSearch"
|
||||
/>
|
||||
<view class="chat-icon-wrapper">
|
||||
<text class="iconfont icon-kefu chat-icon"></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- AI设计 -->
|
||||
<view class="grid-section">
|
||||
<view class="section-header">
|
||||
<view class="section-title-wrapper">
|
||||
<text class="section-title">AI设计</text>
|
||||
<text class="section-icon">✦</text>
|
||||
</view>
|
||||
<view class="section-more" @click="goToMore('design')">
|
||||
<text>更多</text>
|
||||
<text class="iconfont icon-xiangyou"></text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="grid-content">
|
||||
<view
|
||||
v-for="(item, index) in designTools"
|
||||
:key="index"
|
||||
class="grid-item"
|
||||
@click="handleToolClick(item)"
|
||||
>
|
||||
<view class="item-icon-box" :class="item.bgClass">
|
||||
<!-- 使用 emoji 或 iconfont 作为临时图标 -->
|
||||
<text class="item-emoji">{{ item.emoji }}</text>
|
||||
</view>
|
||||
<view class="item-info">
|
||||
<text class="item-name">{{ item.name }}</text>
|
||||
<text class="item-desc">{{ item.desc }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- AI视频 -->
|
||||
<view class="grid-section grid-section-bottom">
|
||||
<view class="section-header">
|
||||
<view class="section-title-wrapper">
|
||||
<text class="section-title">AI视频</text>
|
||||
<text class="section-icon">✦</text>
|
||||
</view>
|
||||
<view class="section-more" @click="goToMore('video')">
|
||||
<text>更多</text>
|
||||
<text class="iconfont icon-xiangyou"></text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="grid-content">
|
||||
<view
|
||||
v-for="(item, index) in videoTools"
|
||||
:key="index"
|
||||
class="grid-item"
|
||||
@click="handleToolClick(item)"
|
||||
>
|
||||
<view class="item-icon-box" :class="item.bgClass">
|
||||
<text class="item-emoji">{{ item.emoji }}</text>
|
||||
</view>
|
||||
<view class="item-info">
|
||||
<text class="item-name">{{ item.name }}</text>
|
||||
<text class="item-desc">{{ item.desc }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部占位 -->
|
||||
<view class="bottom-placeholder"></view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部导航栏 -->
|
||||
<view class="bottom-tabbar">
|
||||
<view
|
||||
v-for="(tab, index) in tabs"
|
||||
:key="index"
|
||||
:class="['tab-item', currentTab === index ? 'active' : '']"
|
||||
@click="switchTab(index)"
|
||||
>
|
||||
<!-- 想象图标 - 使用纯CSS绘制 -->
|
||||
<view v-if="tab.name === '想象'" class="tab-icon-imagine">
|
||||
<view class="snowflake">
|
||||
<view class="snowflake-line snowflake-line-1"></view>
|
||||
<view class="snowflake-line snowflake-line-2"></view>
|
||||
<view class="snowflake-line snowflake-line-3"></view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 字体图标 -->
|
||||
<text v-else class="iconfont" :class="tab.icon"></text>
|
||||
<text class="tab-label">{{ tab.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
statusBarHeight: 0,
|
||||
scrollViewHeight: '100vh',
|
||||
searchText: '',
|
||||
currentTab: 2, // 默认为智能体
|
||||
|
||||
tabs: [
|
||||
{ name: '推荐', icon: 'icon-shouye' },
|
||||
{ name: '想象', icon: 'icon-faxian' },
|
||||
{ name: '智能体', icon: 'icon-kefujiedai' },
|
||||
{ name: '我的', icon: 'icon-wode' }
|
||||
],
|
||||
|
||||
designTools: [
|
||||
{ name: '一键设计', desc: '快速生成设计方案', emoji: '⚡', bgClass: 'bg-yellow', path: '/pages/ai-generate/design' },
|
||||
{ name: '一句话设计', desc: '描述需求即刻生成', emoji: '💬', bgClass: 'bg-grey', path: '/pages/ai-generate/index' },
|
||||
{ name: '毛坯装修', desc: '空间规划全屋设计', emoji: '🏗️', bgClass: 'bg-blue', path: '' },
|
||||
{ name: '家具更换', desc: '智能替换家具单品', emoji: '🛋️', bgClass: 'bg-indigo', path: '' },
|
||||
{ name: '局部修图', desc: '精准修改局部区域', emoji: '✂️', bgClass: 'bg-grey', path: '' },
|
||||
{ name: '风格转换', desc: '一键切换装修风格', emoji: '🎨', bgClass: 'bg-orange', path: '' }
|
||||
],
|
||||
|
||||
videoTools: [
|
||||
{ name: '一句话生视频', desc: '描述想法即刻成片', emoji: '💬', bgClass: 'bg-blue-dark', path: '/pages/ai-generate/oneclick' },
|
||||
{ name: '文生视频', desc: '文字描述生成视频', emoji: '📝', bgClass: 'bg-purple', path: '/pages/ai-generate/oneclick' },
|
||||
{ name: '图生视频', desc: '图片一键生成视频', emoji: '🖼️', bgClass: 'bg-green', path: '/pages/ai-generate/oneclick?type=image' },
|
||||
{ name: '智能分镜', desc: '自动规划视频镜头', emoji: '🎬', bgClass: 'bg-cyan', path: '' },
|
||||
{ name: '视频延长', desc: '智能延长视频时长', emoji: '⏱️', bgClass: 'bg-pink', path: '' },
|
||||
{ name: '自动配乐', desc: '智能匹配背景音乐', emoji: '🎵', bgClass: 'bg-teal', path: '' }
|
||||
]
|
||||
};
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
const systemInfo = uni.getSystemInfoSync();
|
||||
this.statusBarHeight = systemInfo.statusBarHeight || 0;
|
||||
|
||||
// 计算滚动区域高度 (tabbar height approx 50px)
|
||||
const navHeight = 44; // Custom nav height
|
||||
const tabBarHeight = 50;
|
||||
this.scrollViewHeight = `calc(100vh - ${this.statusBarHeight + navHeight + tabBarHeight}px)`;
|
||||
},
|
||||
|
||||
methods: {
|
||||
goToMore(type) {
|
||||
uni.showToast({
|
||||
title: '查看全部功能',
|
||||
icon: 'none'
|
||||
});
|
||||
},
|
||||
|
||||
handleSearch() {
|
||||
if (this.searchText) {
|
||||
uni.showToast({
|
||||
title: '搜索功能开发中: ' + this.searchText,
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
handleToolClick(item) {
|
||||
if (item.path) {
|
||||
uni.navigateTo({
|
||||
url: item.path,
|
||||
fail: (err) => {
|
||||
console.error('Navigate failed', err);
|
||||
uni.showToast({ title: '功能开发中', icon: 'none' });
|
||||
}
|
||||
});
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '功能即将上线',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
switchTab(index) {
|
||||
this.currentTab = index;
|
||||
// Handle navigation if needed
|
||||
if (index === 1) {
|
||||
uni.navigateTo({ url: '/pages/ai-generate/inspiration' });
|
||||
} else if (index === 3) {
|
||||
uni.switchTab({ url: '/pages/user/index' });
|
||||
} else if (index === 0) {
|
||||
uni.switchTab({ url: '/pages/index/index' });
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.agent-container {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
background: #000000;
|
||||
color: #ffffff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.nav-title {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
padding: 0 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.banner-section {
|
||||
margin: 16px 0 20px;
|
||||
padding: 0 16px;
|
||||
|
||||
.banner-content {
|
||||
background: linear-gradient(135deg, rgba(26, 60, 46, 0.4) 0%, rgba(20, 40, 32, 0.6) 100%);
|
||||
border-radius: 20px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
border: 1px solid rgba(66, 202, 77, 0.15);
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
|
||||
|
||||
.banner-left {
|
||||
flex-shrink: 0;
|
||||
|
||||
.banner-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background: linear-gradient(135deg, rgba(66, 202, 77, 0.2) 0%, rgba(56, 176, 69, 0.3) 100%);
|
||||
border-radius: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4px 12px rgba(66, 202, 77, 0.2);
|
||||
|
||||
.logo-img {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.banner-right {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 16px;
|
||||
position: relative;
|
||||
|
||||
.chat-bubble {
|
||||
flex: 1;
|
||||
background: linear-gradient(135deg, rgba(66, 202, 77, 0.15) 0%, rgba(56, 176, 69, 0.1) 100%);
|
||||
border: 1px solid rgba(66, 202, 77, 0.3);
|
||||
border-radius: 16px;
|
||||
padding: 12px 16px;
|
||||
position: relative;
|
||||
|
||||
// 气泡尾巴
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
border-width: 8px 8px 8px 0;
|
||||
border-color: transparent rgba(66, 202, 77, 0.3) transparent transparent;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -6px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
border-width: 7px 7px 7px 0;
|
||||
border-color: transparent rgba(66, 202, 77, 0.15) transparent transparent;
|
||||
}
|
||||
|
||||
.bubble-title {
|
||||
display: block;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
margin-bottom: 4px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.bubble-desc {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
line-height: 1.3;
|
||||
}
|
||||
}
|
||||
|
||||
.sparkle-group {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
|
||||
.sparkle {
|
||||
font-size: 20px;
|
||||
animation: sparkle 2s ease-in-out infinite;
|
||||
|
||||
&.sparkle-1 {
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
&.sparkle-2 {
|
||||
animation-delay: 0.5s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes sparkle {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
transform: scale(1) rotate(0deg);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.6;
|
||||
transform: scale(1.2) rotate(20deg);
|
||||
}
|
||||
}
|
||||
|
||||
.search-section {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.search-bar {
|
||||
background: #1a1a1a;
|
||||
border-radius: 24px;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
|
||||
.search-icon {
|
||||
font-size: 18px;
|
||||
color: #666;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.search-placeholder {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.chat-icon-wrapper {
|
||||
border-left: 1px solid #333;
|
||||
padding-left: 12px;
|
||||
margin-left: 8px;
|
||||
|
||||
.chat-icon {
|
||||
font-size: 20px;
|
||||
color: #42ca4d;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.grid-section-bottom {
|
||||
margin-bottom: 80px !important;
|
||||
}
|
||||
|
||||
.grid-section {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.section-title-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #ffffff;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.section-icon {
|
||||
color: #42ca4d;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.section-more {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
|
||||
.iconfont {
|
||||
font-size: 12px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.grid-content {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
|
||||
.grid-item {
|
||||
background: #1a1a1a;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.item-icon-box {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 12px;
|
||||
|
||||
.item-emoji {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
&.bg-yellow { background: rgba(255, 193, 7, 0.1); }
|
||||
&.bg-grey { background: rgba(158, 158, 158, 0.1); }
|
||||
&.bg-blue { background: rgba(33, 150, 243, 0.1); }
|
||||
&.bg-indigo { background: rgba(63, 81, 181, 0.1); }
|
||||
&.bg-red { background: rgba(244, 67, 54, 0.1); }
|
||||
&.bg-orange { background: rgba(255, 152, 0, 0.1); }
|
||||
&.bg-blue-dark { background: rgba(13, 71, 161, 0.1); }
|
||||
&.bg-purple { background: rgba(156, 39, 176, 0.1); }
|
||||
&.bg-green { background: rgba(76, 175, 80, 0.1); }
|
||||
&.bg-cyan { background: rgba(0, 188, 212, 0.1); }
|
||||
&.bg-pink { background: rgba(233, 30, 99, 0.1); }
|
||||
&.bg-teal { background: rgba(0, 150, 136, 0.1); }
|
||||
}
|
||||
|
||||
.item-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
.item-name {
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
margin-bottom: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.item-desc {
|
||||
font-size: 10px;
|
||||
color: #666;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-placeholder {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
// 底部导航栏
|
||||
.bottom-tabbar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
height: calc(50px + env(safe-area-inset-bottom));
|
||||
box-sizing: border-box;
|
||||
align-items: center;
|
||||
background: linear-gradient(180deg, rgba(20, 24, 34, 0.96) 0%, rgba(10, 12, 18, 0.98) 100%);
|
||||
backdrop-filter: blur(20px);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
z-index: 999;
|
||||
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.35), 0 -12px 24px rgba(0, 0, 0, 0.25);
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, rgba(255,255,255,0.18), rgba(255,255,255,0.06), rgba(255,255,255,0.18));
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
border-radius: 12px;
|
||||
padding: 2px 6px;
|
||||
margin: 0px 20px;
|
||||
transition: all 180ms ease;
|
||||
position: relative;
|
||||
|
||||
.iconfont {
|
||||
font-size: 22px;
|
||||
color: #8f9bb3;
|
||||
transition: color 180ms ease, transform 180ms ease, text-shadow 180ms ease;
|
||||
}
|
||||
|
||||
.tab-icon-imagine {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
|
||||
.snowflake {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
position: relative;
|
||||
|
||||
.snowflake-line {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 18px;
|
||||
height: 2px;
|
||||
background: #8f9bb3;
|
||||
transform-origin: center;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 6px;
|
||||
height: 2px;
|
||||
background: #8f9bb3;
|
||||
}
|
||||
|
||||
&::before {
|
||||
top: -4px;
|
||||
left: 2px;
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
&::after {
|
||||
top: 4px;
|
||||
left: 2px;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
}
|
||||
|
||||
.snowflake-line-1 {
|
||||
transform: translate(-50%, -50%) rotate(0deg);
|
||||
}
|
||||
|
||||
.snowflake-line-2 {
|
||||
transform: translate(-50%, -50%) rotate(60deg);
|
||||
}
|
||||
|
||||
.snowflake-line-3 {
|
||||
transform: translate(-50%, -50%) rotate(120deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tab-label {
|
||||
font-size: 10px;
|
||||
color: #8f9bb3;
|
||||
transition: color 180ms ease, text-shadow 180ms ease;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: radial-gradient(circle at 50% 20%, rgba(255,255,255,0.16) 0%, rgba(255,255,255,0.06) 42%, rgba(255,255,255,0) 60%),
|
||||
linear-gradient(180deg, rgba(255,255,255,0.10) 0%, rgba(255,255,255,0.02) 100%);
|
||||
box-shadow: inset 0 2px 6px rgba(255,255,255,0.12), 0 6px 12px rgba(0,0,0,0.35);
|
||||
border: 1px solid rgba(255,255,255,0.08);
|
||||
transform: translateY(-1px);
|
||||
|
||||
.iconfont,
|
||||
.tab-label {
|
||||
color: #42ca4d;
|
||||
text-shadow: 0 0 8px rgba(66, 202, 77, 0.45);
|
||||
}
|
||||
|
||||
.tab-icon-imagine {
|
||||
.snowflake-line {
|
||||
background: #42ca4d;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
background: #42ca4d;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
1059
msh_single_uniapp/pages/ai-generate/assets.vue
Normal file
1059
msh_single_uniapp/pages/ai-generate/assets.vue
Normal file
File diff suppressed because it is too large
Load Diff
1573
msh_single_uniapp/pages/ai-generate/design.vue
Normal file
1573
msh_single_uniapp/pages/ai-generate/design.vue
Normal file
File diff suppressed because it is too large
Load Diff
759
msh_single_uniapp/pages/ai-generate/effect.vue
Normal file
759
msh_single_uniapp/pages/ai-generate/effect.vue
Normal file
@@ -0,0 +1,759 @@
|
||||
<template>
|
||||
<view class="effect-container" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<!-- 顶部导航 -->
|
||||
<view class="nav-bar">
|
||||
<view class="nav-left" @click="goBack">
|
||||
<text class="iconfont icon-fanhui"></text>
|
||||
</view>
|
||||
<view class="nav-title">设计前后效果</view>
|
||||
<view class="nav-right" @click="toggleFavorite">
|
||||
<text class="iconfont" :class="isFavorited ? 'icon-aixin' : 'icon-aixin1'"></text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view scroll-y class="main-content" :style="{ height: scrollViewHeight }">
|
||||
|
||||
<!-- 改造前后对比卡片 -->
|
||||
<view class="comparison-section">
|
||||
<view
|
||||
class="comparison-card"
|
||||
id="comparisonCard"
|
||||
@touchstart="onTouchStart"
|
||||
@touchmove="onTouchMove"
|
||||
@touchend="onTouchEnd"
|
||||
>
|
||||
<!-- 改造后(底层全图) -->
|
||||
<view class="comparison-item after-item-full">
|
||||
<image
|
||||
:src="afterImage"
|
||||
mode="aspectFill"
|
||||
class="comparison-image"
|
||||
@error="onImageError('after')"
|
||||
></image>
|
||||
<view class="comparison-tag after-tag">
|
||||
<text>改造前</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 改造前(遮罩层,根据dividerPosition裁剪) -->
|
||||
<view
|
||||
class="comparison-item before-item-overlay"
|
||||
:style="{ width: dividerPosition + '%' }"
|
||||
>
|
||||
<image
|
||||
:src="beforeImage"
|
||||
mode="aspectFill"
|
||||
class="comparison-image"
|
||||
@error="onImageError('before')"
|
||||
></image>
|
||||
<view class="comparison-tag before-tag">
|
||||
<text>改造后</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分隔线和控制按钮 -->
|
||||
<view
|
||||
class="divider-wrapper"
|
||||
:style="{ left: dividerPosition + '%' }"
|
||||
@touchstart.stop="onDividerTouchStart"
|
||||
@touchmove.stop="onDividerTouchMove"
|
||||
@touchend.stop="onDividerTouchEnd"
|
||||
>
|
||||
<view class="divider-line"></view>
|
||||
<view class="divider-btn" @click.stop="toggleComparison">
|
||||
<view class="pause-icon">
|
||||
<view class="pause-bar"></view>
|
||||
<view class="pause-bar"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 生成提示词 -->
|
||||
<view class="prompt-section">
|
||||
<view class="prompt-header">
|
||||
<text class="prompt-title">生成提示词</text>
|
||||
</view>
|
||||
<view class="prompt-content">
|
||||
<text class="prompt-text">{{ promptText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bottom-placeholder"></view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<view class="footer-actions">
|
||||
<view class="action-btn" @click="regenerate">
|
||||
<view class="action-icon">
|
||||
<text class="iconfont icon-shuaxin"></text>
|
||||
</view>
|
||||
<text class="action-text">重新生成</text>
|
||||
</view>
|
||||
|
||||
<view class="action-btn" @click="downloadSave">
|
||||
<view class="action-icon">
|
||||
<text class="iconfont icon-xiazai1"></text>
|
||||
</view>
|
||||
<text class="action-text">下载保存</text>
|
||||
</view>
|
||||
|
||||
<view class="action-btn" @click="share">
|
||||
<view class="action-icon">
|
||||
<text class="iconfont icon-fenxiang"></text>
|
||||
</view>
|
||||
<text class="action-text">分享</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from '@/api/models-api.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
statusBarHeight: 0,
|
||||
scrollViewHeight: '100vh',
|
||||
|
||||
isFavorited: false,
|
||||
isPlaying: false,
|
||||
|
||||
beforeImage: '',
|
||||
afterImage: '',
|
||||
promptText: '将卧室改造成现代简约北欧风格,采用温暖的木质色调和柔和的中性色,保留原有空间布局,增加自然光线和绿植元素,营造舒适宁静的氛围',
|
||||
|
||||
articleId: null,
|
||||
articleData: null,
|
||||
|
||||
// 拖动相关
|
||||
dividerPosition: 50, // 分隔线位置百分比 (0-100)
|
||||
isDragging: false,
|
||||
startX: 0,
|
||||
cardWidth: 0
|
||||
};
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
const systemInfo = uni.getSystemInfoSync();
|
||||
this.statusBarHeight = systemInfo.statusBarHeight || 0;
|
||||
|
||||
const navHeight = 44;
|
||||
const footerHeight = 100;
|
||||
this.scrollViewHeight = `calc(100vh - ${this.statusBarHeight + navHeight + footerHeight}px)`;
|
||||
|
||||
// 获取传递的参数
|
||||
if (options.id) {
|
||||
this.articleId = options.id;
|
||||
this.loadArticleData();
|
||||
}
|
||||
|
||||
if (options.beforeImage) {
|
||||
this.beforeImage = decodeURIComponent(options.beforeImage);
|
||||
}
|
||||
|
||||
if (options.afterImage) {
|
||||
this.afterImage = decodeURIComponent(options.afterImage);
|
||||
}
|
||||
|
||||
if (options.promptText) {
|
||||
this.promptText = decodeURIComponent(options.promptText);
|
||||
}
|
||||
|
||||
// 获取卡片宽度
|
||||
this.$nextTick(() => {
|
||||
setTimeout(() => {
|
||||
const query = uni.createSelectorQuery().in(this);
|
||||
query.select('#comparisonCard').boundingClientRect(data => {
|
||||
if (data) {
|
||||
this.cardWidth = data.width;
|
||||
}
|
||||
}).exec();
|
||||
}, 300);
|
||||
});
|
||||
},
|
||||
|
||||
onUnload() {
|
||||
// 清理动画定时器
|
||||
this.stopComparisonAnimation();
|
||||
},
|
||||
|
||||
methods: {
|
||||
goBack() {
|
||||
// 清理动画定时器
|
||||
this.stopComparisonAnimation();
|
||||
uni.navigateBack();
|
||||
},
|
||||
|
||||
toggleFavorite() {
|
||||
this.isFavorited = !this.isFavorited;
|
||||
uni.showToast({
|
||||
title: this.isFavorited ? '已收藏' : '已取消收藏',
|
||||
icon: 'none',
|
||||
duration: 1500
|
||||
});
|
||||
},
|
||||
|
||||
toggleComparison() {
|
||||
this.isPlaying = !this.isPlaying;
|
||||
|
||||
if (this.isPlaying) {
|
||||
// 开始播放对比动画
|
||||
this.startComparisonAnimation();
|
||||
} else {
|
||||
// 暂停动画
|
||||
this.stopComparisonAnimation();
|
||||
}
|
||||
},
|
||||
|
||||
startComparisonAnimation() {
|
||||
// 自动左右滑动对比
|
||||
let direction = 1; // 1: 向右, -1: 向左
|
||||
|
||||
this.animationTimer = setInterval(() => {
|
||||
this.dividerPosition += direction * 2;
|
||||
|
||||
if (this.dividerPosition >= 80) {
|
||||
direction = -1;
|
||||
} else if (this.dividerPosition <= 20) {
|
||||
direction = 1;
|
||||
}
|
||||
}, 30);
|
||||
},
|
||||
|
||||
stopComparisonAnimation() {
|
||||
if (this.animationTimer) {
|
||||
clearInterval(this.animationTimer);
|
||||
this.animationTimer = null;
|
||||
}
|
||||
},
|
||||
|
||||
// 触摸开始
|
||||
onTouchStart(e) {
|
||||
if (this.isPlaying) {
|
||||
this.stopComparisonAnimation();
|
||||
this.isPlaying = false;
|
||||
}
|
||||
},
|
||||
|
||||
onTouchMove(e) {
|
||||
// 卡片区域的触摸移动(可选)
|
||||
},
|
||||
|
||||
onTouchEnd(e) {
|
||||
// 触摸结束
|
||||
},
|
||||
|
||||
// 分隔线拖动
|
||||
onDividerTouchStart(e) {
|
||||
this.isDragging = true;
|
||||
this.startX = e.touches[0].clientX;
|
||||
|
||||
// 停止自动播放动画
|
||||
if (this.isPlaying) {
|
||||
this.stopComparisonAnimation();
|
||||
this.isPlaying = false;
|
||||
}
|
||||
|
||||
// 获取卡片宽度
|
||||
const query = uni.createSelectorQuery().in(this);
|
||||
query.select('#comparisonCard').boundingClientRect(data => {
|
||||
if (data) {
|
||||
this.cardWidth = data.width;
|
||||
}
|
||||
}).exec();
|
||||
},
|
||||
|
||||
onDividerTouchMove(e) {
|
||||
if (!this.isDragging) return;
|
||||
|
||||
const currentX = e.touches[0].clientX;
|
||||
const deltaX = currentX - this.startX;
|
||||
|
||||
// 计算新的位置百分比
|
||||
if (this.cardWidth > 0) {
|
||||
const deltaPercent = (deltaX / this.cardWidth) * 100;
|
||||
let newPosition = this.dividerPosition + deltaPercent;
|
||||
|
||||
// 限制范围 0-100
|
||||
if (newPosition < 0) newPosition = 0;
|
||||
if (newPosition > 100) newPosition = 100;
|
||||
|
||||
this.dividerPosition = newPosition;
|
||||
this.startX = currentX;
|
||||
}
|
||||
},
|
||||
|
||||
onDividerTouchEnd(e) {
|
||||
this.isDragging = false;
|
||||
},
|
||||
|
||||
onImageError(type) {
|
||||
console.error(`${type} image load error`);
|
||||
uni.showToast({
|
||||
title: `${type === 'before' ? '改造前' : '改造后'}图片加载失败`,
|
||||
icon: 'none'
|
||||
});
|
||||
},
|
||||
|
||||
async loadArticleData() {
|
||||
if (!this.articleId) return;
|
||||
|
||||
try {
|
||||
uni.showLoading({ title: '加载中...' });
|
||||
const response = await api.getArticleById(this.articleId);
|
||||
const data = response.data || response;
|
||||
this.articleData = data;
|
||||
|
||||
// 设置图片和提示词
|
||||
if (data.imageInput && !this.beforeImage) {
|
||||
this.beforeImage = data.imageInput;
|
||||
}
|
||||
if (data.videoUrl && !this.afterImage) {
|
||||
this.afterImage = data.videoUrl;
|
||||
}
|
||||
if (data.prompt && !this.promptText) {
|
||||
this.promptText = data.prompt;
|
||||
} else if (data.synopsis && !this.promptText) {
|
||||
this.promptText = data.synopsis;
|
||||
}
|
||||
|
||||
uni.hideLoading();
|
||||
} catch (error) {
|
||||
console.error('Load article data error:', error);
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: '加载数据失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
regenerate() {
|
||||
uni.showModal({
|
||||
title: '重新生成',
|
||||
content: '确定要重新生成设计效果吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 跳转到设计页面,携带当前参数
|
||||
const params = {
|
||||
beforeImage: encodeURIComponent(this.beforeImage),
|
||||
promptText: encodeURIComponent(this.promptText)
|
||||
};
|
||||
const queryString = Object.keys(params)
|
||||
.map(key => `${key}=${params[key]}`)
|
||||
.join('&');
|
||||
|
||||
uni.navigateTo({
|
||||
url: `/pages/ai-generate/design?${queryString}®enerate=true`
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
downloadSave() {
|
||||
if (!this.afterImage) {
|
||||
uni.showToast({
|
||||
title: '暂无图片可下载',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
uni.showLoading({ title: '下载中...', mask: true });
|
||||
|
||||
uni.downloadFile({
|
||||
url: this.afterImage,
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
uni.saveImageToPhotosAlbum({
|
||||
filePath: res.tempFilePath,
|
||||
success: () => {
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: '已保存到相册',
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
});
|
||||
},
|
||||
fail: (error) => {
|
||||
uni.hideLoading();
|
||||
if ((error.errMsg || '').includes('auth')) {
|
||||
uni.showModal({
|
||||
title: '需要相册权限',
|
||||
content: '请在设置中开启相册权限',
|
||||
confirmText: '去设置',
|
||||
success: (modalRes) => {
|
||||
if (modalRes.confirm) {
|
||||
uni.openSetting();
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '保存失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: '下载失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: '下载失败,请重试',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
share() {
|
||||
uni.showActionSheet({
|
||||
itemList: ['分享到微信', '分享到朋友圈', '复制链接'],
|
||||
success: (res) => {
|
||||
if (res.tapIndex === 0) {
|
||||
uni.showToast({ title: '分享到微信', icon: 'none' });
|
||||
} else if (res.tapIndex === 1) {
|
||||
uni.showToast({ title: '分享到朋友圈', icon: 'none' });
|
||||
} else if (res.tapIndex === 2) {
|
||||
this.copyLink();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
copyLink() {
|
||||
const shareUrl = this.articleId
|
||||
? `https://yourapp.com/pages/ai-generate/effect?id=${this.articleId}`
|
||||
: 'https://yourapp.com/pages/ai-generate/effect';
|
||||
uni.setClipboardData({
|
||||
data: shareUrl,
|
||||
success: () => {
|
||||
uni.showToast({
|
||||
title: '链接已复制',
|
||||
icon: 'success'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.effect-container {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
background: #000000;
|
||||
color: #ffffff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
|
||||
.nav-left, .nav-right {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 17px;
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
color: #ffffff;
|
||||
font-size: 20px;
|
||||
|
||||
&.icon-aixin {
|
||||
color: #ff6b6b;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
// 对比卡片
|
||||
.comparison-section {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.comparison-card {
|
||||
width: 100%;
|
||||
height: 600px; // 从400px增加到600px,增加了50%的高度
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: #1a1a1a;
|
||||
touch-action: none; // 防止触摸时页面滚动
|
||||
|
||||
// 改造后(底层全图)
|
||||
.after-item-full {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.comparison-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.comparison-tag {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
backdrop-filter: blur(10px);
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
color: #ffffff;
|
||||
z-index: 5;
|
||||
}
|
||||
}
|
||||
|
||||
// 改造前(遮罩层)
|
||||
.before-item-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
z-index: 8;
|
||||
transition: width 0.05s linear; // 添加平滑过渡
|
||||
|
||||
.comparison-image {
|
||||
width: 100vw; // 使用视口宽度保证图片不变形
|
||||
max-width: none;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.comparison-tag {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
left: 12px;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
backdrop-filter: blur(10px);
|
||||
background: rgba(66, 202, 77, 0.9);
|
||||
color: #ffffff;
|
||||
z-index: 5;
|
||||
}
|
||||
}
|
||||
|
||||
.divider-wrapper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 44px; // 增加触摸区域
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
cursor: ew-resize; // 鼠标样式
|
||||
transition: left 0.05s linear; // 添加平滑过渡
|
||||
|
||||
.divider-line {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 4px;
|
||||
background: #42ca4d;
|
||||
box-shadow: 0 0 8px rgba(66, 202, 77, 0.6);
|
||||
}
|
||||
|
||||
.divider-btn {
|
||||
position: relative;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: #42ca4d;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4px 12px rgba(66, 202, 77, 0.4);
|
||||
border: 2px solid #ffffff;
|
||||
|
||||
.iconfont {
|
||||
color: #ffffff;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.pause-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
|
||||
.pause-bar {
|
||||
width: 3px;
|
||||
height: 14px;
|
||||
background: #ffffff;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 提示词区域
|
||||
.prompt-section {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.prompt-header {
|
||||
margin-bottom: 12px;
|
||||
|
||||
.prompt-title {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
.prompt-content {
|
||||
background: rgba(26, 26, 26, 0.6);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
border: 1px solid #333;
|
||||
|
||||
.prompt-text {
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
line-height: 1.6;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-placeholder {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
// 底部操作按钮
|
||||
.footer-actions {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 50px;
|
||||
background: linear-gradient(0deg, rgba(0, 0, 0, 0.95) 80%, rgba(0, 0, 0, 0.8) 100%);
|
||||
backdrop-filter: blur(20px);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
padding: 12px 16px 30px;
|
||||
z-index: 100;
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
// background: linear-gradient(135deg, rgba(66, 202, 77, 0.15) 0%, rgba(66, 202, 77, 0.08) 100%);
|
||||
// border: 1px solid rgba(66, 202, 77, 0.3);
|
||||
border-radius: 20px;
|
||||
margin: 4px 6px;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
.action-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #42ca4d 0%, #38b045 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 8px rgba(66, 202, 77, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
.iconfont {
|
||||
font-size: 16px;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
.action-text {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.96);
|
||||
background: linear-gradient(135deg, rgba(66, 202, 77, 0.25) 0%, rgba(66, 202, 77, 0.15) 100%);
|
||||
border-color: rgba(66, 202, 77, 0.5);
|
||||
|
||||
.action-icon {
|
||||
transform: scale(0.9);
|
||||
box-shadow: 0 1px 4px rgba(66, 202, 77, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
// 为不同按钮添加特定样式
|
||||
&:first-child {
|
||||
.action-icon {
|
||||
background: linear-gradient(135deg, #42ca4d 0%, #38b045 100%);
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
.action-icon {
|
||||
background: linear-gradient(135deg, #4caf50 0%, #45a049 100%);
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
.action-icon {
|
||||
background: linear-gradient(135deg, #5cb85c 0%, #4cae4c 100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
1062
msh_single_uniapp/pages/ai-generate/history.vue
Normal file
1062
msh_single_uniapp/pages/ai-generate/history.vue
Normal file
File diff suppressed because it is too large
Load Diff
569
msh_single_uniapp/pages/ai-generate/image.vue
Normal file
569
msh_single_uniapp/pages/ai-generate/image.vue
Normal file
@@ -0,0 +1,569 @@
|
||||
<template>
|
||||
<view class="image-container" @touchstart="onTouchStart" @touchmove="onTouchMove" @touchend="onTouchEnd">
|
||||
<view class="media-wrapper">
|
||||
<swiper class="image-swiper" :current="currentIndex" :circular="true" :autoplay="false" @change="onSwiperChange">
|
||||
<block v-for="(img, idx) in images" :key="idx">
|
||||
<swiper-item>
|
||||
<view class="image-slide" @click="onImageTap(idx)">
|
||||
<view class="zoom-wrapper" :style="{ transform: 'scale(' + ((scaleMap[idx] || 1)) + ')' }">
|
||||
<image class="display-image"
|
||||
:src="img.displayUrl"
|
||||
mode="aspectFill"
|
||||
lazy-load
|
||||
@load="onImageLoad(idx)"
|
||||
@error="onImageError(idx)" />
|
||||
</view>
|
||||
</view>
|
||||
</swiper-item>
|
||||
</block>
|
||||
</swiper>
|
||||
</view>
|
||||
|
||||
<view class="top-controls" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<view class="left-controls">
|
||||
<view class="back-btn" @click="onBack">
|
||||
<text class="icon-back">‹</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bottom-info">
|
||||
<view class="article-status" v-if="isLoadingArticle || articleError">
|
||||
<view class="loading-indicator" v-if="isLoadingArticle">
|
||||
<text class="loading-text">正在加载图片详情...</text>
|
||||
</view>
|
||||
<view class="error-message" v-if="articleError">
|
||||
<text class="error-text">{{ articleError }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="creator-info">
|
||||
<image class="creator-avatar" :src="creatorAvatar" @click="onCreatorTap" />
|
||||
<view class="creator-details">
|
||||
<text class="creator-name">{{ creatorName }}</text>
|
||||
<view :class="['follow-btn', isFollowing ? 'following' : '']" @click="onFollow">
|
||||
{{ isFollowing ? '已关注' : '关注' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="image-info" v-if="imageDescription">
|
||||
<view :class="['image-description', isDescriptionExpanded ? 'expanded' : '']" @click="onToggleDescription">
|
||||
{{ imageDescription }}
|
||||
</view>
|
||||
<view class="expand-toggle" @click="onToggleDescription" v-if="imageDescription.length > 50">
|
||||
<text class="iconfont" :class="isDescriptionExpanded ? 'icon-xiangshang' : 'icon-xiangxia'"></text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="interaction-area">
|
||||
<view class="like-section">
|
||||
<view :class="['like-btn', isLiked ? 'liked' : '']" @click="onLike">
|
||||
<text class="heart-icon">{{ isLiked ? '❤️' : '🤍' }}</text>
|
||||
</view>
|
||||
<text class="like-count">{{ likeCount }}</text>
|
||||
</view>
|
||||
<view class="action-buttons">
|
||||
<view class="consult-btn" @click="onConsult">立即咨询</view>
|
||||
<view class="action-btn" @click="onCreateSimilar">做同款</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="ai-notice">
|
||||
<text class="ai-notice-text">内容由AI生成</text>
|
||||
</view>
|
||||
|
||||
<view class="page-indicator">
|
||||
<text class="indicator-text">{{ currentIndex + 1 }} / {{ images.length }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="floating-actions">
|
||||
<view class="floating-btn download-btn" @click="onDownload">
|
||||
<text class="floating-label">下载</text>
|
||||
</view>
|
||||
<view class="floating-btn share-btn" @click="onShare">
|
||||
<text class="floating-label">分享</text>
|
||||
</view>
|
||||
<view class="floating-btn compare-btn" @click="onCompare">
|
||||
<text class="floating-label">对比</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from '@/api/models-api.js';
|
||||
import Cache from '@/utils/cache';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
images: [],
|
||||
currentIndex: 0,
|
||||
currentTime: '00:25',
|
||||
creatorName: '开心海浪',
|
||||
creatorAvatar: '/static/images/creator-avatar.png',
|
||||
isFollowing: false,
|
||||
imageDescription: '',
|
||||
isDescriptionExpanded: false,
|
||||
isLiked: false,
|
||||
likeCount: 6830,
|
||||
isLoadingArticle: false,
|
||||
articleError: null,
|
||||
articleData: null,
|
||||
currentArticleId: null,
|
||||
touchStart: null,
|
||||
touchDistance: 0,
|
||||
scaleMap: {},
|
||||
minScale: 1,
|
||||
maxScale: 3
|
||||
,statusBarHeight: 0
|
||||
};
|
||||
},
|
||||
computed: mapGetters(['chatUrl']),
|
||||
onLoad(options) {
|
||||
const articleId = options.id;
|
||||
this.setData({ currentArticleId: articleId });
|
||||
const sys = uni.getSystemInfoSync();
|
||||
const sbh = sys.statusBarHeight;
|
||||
this.statusBarHeight = (sbh && sbh > 0) ? sbh : (sys.platform === 'ios' ? 44 : 24);
|
||||
this.statusBarHeight +=30;
|
||||
const cached = Cache.getItem('imageDetail_' + articleId);
|
||||
if (cached) {
|
||||
this.initFromData(cached);
|
||||
}
|
||||
if (articleId) {
|
||||
this.loadArticleDetail(articleId);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setData(data) {
|
||||
let that = this;
|
||||
Object.keys(data).forEach(key => { that[key] = data[key]; });
|
||||
},
|
||||
isImageUrl(url) {
|
||||
if (!url) return false;
|
||||
const exts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg'];
|
||||
const u = (url.split('?')[0].split('#')[0] || '').toLowerCase();
|
||||
return exts.some(ext => u.endsWith(ext));
|
||||
},
|
||||
// 处理头像URL,添加前缀
|
||||
getAvatarUrl(avatarUrl) {
|
||||
if (!avatarUrl || avatarUrl === '/static/images/avatar-default.png') {
|
||||
return '/static/images/avatar-default.png';
|
||||
}
|
||||
// 如果已经是完整URL,直接返回
|
||||
if (avatarUrl.startsWith('http://') || avatarUrl.startsWith('https://')) {
|
||||
return avatarUrl;
|
||||
}
|
||||
// 如果是相对路径,添加前缀
|
||||
const prefix = 'https://uthink2025.oss-cn-shanghai.aliyuncs.com/';
|
||||
// 如果URL已经以/开头,去掉开头的/
|
||||
const cleanUrl = avatarUrl.startsWith('/') ? avatarUrl.substring(1) : avatarUrl;
|
||||
return prefix + cleanUrl;
|
||||
},
|
||||
initFromData(data) {
|
||||
console.log("===initFromData===",data);
|
||||
const imgs = [];
|
||||
const vUrl = data.videoUrl || '';
|
||||
const iUrl = data.imageInput || data.image_input || '';
|
||||
if (this.isImageUrl(vUrl)) {
|
||||
imgs.push({ displayUrl: vUrl, originalUrl: vUrl });
|
||||
}
|
||||
if (iUrl) {
|
||||
imgs.push({ displayUrl: iUrl, originalUrl: iUrl });
|
||||
}
|
||||
if (!imgs.length) {
|
||||
// 尝试从 imageOutput 获取图片
|
||||
let imageOutput = data.imageOutput || '';
|
||||
if (imageOutput) {
|
||||
if (typeof imageOutput === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(imageOutput);
|
||||
if (Array.isArray(parsed)) {
|
||||
parsed.forEach(u => imgs.push({ displayUrl: u, originalUrl: u }));
|
||||
} else {
|
||||
imgs.push({ displayUrl: parsed, originalUrl: parsed });
|
||||
}
|
||||
} catch (e) {
|
||||
imgs.push({ displayUrl: imageOutput, originalUrl: imageOutput });
|
||||
}
|
||||
} else if (Array.isArray(imageOutput)) {
|
||||
imageOutput.forEach(u => imgs.push({ displayUrl: u, originalUrl: u }));
|
||||
}
|
||||
}
|
||||
if (!imgs.length) {
|
||||
const list = data.images || data.image_urls || [];
|
||||
if (Array.isArray(list) && list.length) {
|
||||
list.forEach(u => imgs.push({ displayUrl: u, originalUrl: u }));
|
||||
} else {
|
||||
const u = data.cover || '';
|
||||
if (u) imgs.push({ displayUrl: u, originalUrl: u });
|
||||
}
|
||||
}
|
||||
}
|
||||
const like = data.likeCount || data.visit || 0;
|
||||
|
||||
// 获取作者头像
|
||||
const authorAvatar = this.getAvatarUrl(data.authorAvatar || data.avatar || '');
|
||||
|
||||
// 获取描述信息,优先使用 prompt,然后是 title、content
|
||||
const description = data.prompt || data.title || data.content || data.synopsis || '';
|
||||
|
||||
// 获取作者名称
|
||||
const authorName = data.authorName || data.author || this.creatorName;
|
||||
|
||||
this.setData({
|
||||
images: imgs,
|
||||
likeCount: like,
|
||||
imageDescription: description,
|
||||
creatorName: authorName,
|
||||
creatorAvatar: authorAvatar
|
||||
});
|
||||
},
|
||||
loadArticleDetail(articleId) {
|
||||
this.setData({ isLoadingArticle: true, articleError: null });
|
||||
api.getArticleById(articleId).then(response => {
|
||||
const data = response.data || response;
|
||||
this.articleData = data;
|
||||
Cache.setItem({ name: 'imageDetail_' + articleId, value: data, expires: 3600 * 1000 });
|
||||
this.initFromData(data);
|
||||
this.setData({ isLoadingArticle: false });
|
||||
}).catch(error => {
|
||||
this.setData({ articleError: error.message || '获取图片详情失败', isLoadingArticle: false });
|
||||
uni.showToast({ title: '获取图片详情失败', icon: 'none', duration: 2000 });
|
||||
});
|
||||
},
|
||||
onImageLoad(idx) {
|
||||
this.scaleMap[idx] = 1;
|
||||
},
|
||||
onImageError(idx) {
|
||||
uni.showToast({ title: '图片加载失败', icon: 'none' });
|
||||
},
|
||||
onImageTap(idx) {
|
||||
const urls = this.images.map(i => i.originalUrl);
|
||||
uni.previewImage({ urls, current: idx });
|
||||
},
|
||||
onSwiperChange(e) {
|
||||
const i = e.detail.current || 0;
|
||||
this.setData({ currentIndex: i });
|
||||
},
|
||||
onTouchStart(e) {
|
||||
if (!e.touches || e.touches.length < 1) return;
|
||||
if (e.touches.length === 2) {
|
||||
const d = this.distance(e.touches[0], e.touches[1]);
|
||||
this.touchDistance = d;
|
||||
} else {
|
||||
this.touchStart = e.touches[0];
|
||||
}
|
||||
},
|
||||
onTouchMove(e) {
|
||||
if (e.touches && e.touches.length === 2) {
|
||||
const d = this.distance(e.touches[0], e.touches[1]);
|
||||
const delta = d - this.touchDistance;
|
||||
const cur = this.scaleMap[this.currentIndex] || 1;
|
||||
let next = cur + delta / 300;
|
||||
if (next < this.minScale) next = this.minScale;
|
||||
if (next > this.maxScale) next = this.maxScale;
|
||||
this.scaleMap[this.currentIndex] = next;
|
||||
this.touchDistance = d;
|
||||
}
|
||||
},
|
||||
onTouchEnd() {
|
||||
this.touchStart = null;
|
||||
this.touchDistance = 0;
|
||||
},
|
||||
distance(p1, p2) {
|
||||
const dx = p1.clientX - p2.clientX;
|
||||
const dy = p1.clientY - p2.clientY;
|
||||
return Math.sqrt(dx * dx + dy * dy);
|
||||
},
|
||||
onDownload() {
|
||||
if (!this.images.length) {
|
||||
uni.showToast({ title: '无可下载图片', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
const url = this.images[this.currentIndex].originalUrl;
|
||||
uni.showLoading({ title: '下载中...', mask: true });
|
||||
uni.downloadFile({
|
||||
url,
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
this.compressAndSave(res.tempFilePath);
|
||||
} else {
|
||||
uni.hideLoading();
|
||||
uni.showToast({ title: '下载失败', icon: 'none' });
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
uni.hideLoading();
|
||||
uni.showToast({ title: '下载失败,请重试', icon: 'none' });
|
||||
}
|
||||
});
|
||||
},
|
||||
compressAndSave(path) {
|
||||
uni.compressImage({
|
||||
src: path,
|
||||
quality: 80,
|
||||
success: (res) => {
|
||||
const filePath = res.tempFilePath || path;
|
||||
uni.saveImageToPhotosAlbum({
|
||||
filePath,
|
||||
success: () => {
|
||||
uni.hideLoading();
|
||||
uni.showToast({ title: '已保存到相册', icon: 'success', duration: 2000 });
|
||||
},
|
||||
fail: (error) => {
|
||||
uni.hideLoading();
|
||||
if ((error.errMsg || '').includes('auth')) {
|
||||
uni.showModal({
|
||||
title: '需要相册权限',
|
||||
content: '请在设置中开启相册权限',
|
||||
confirmText: '去设置',
|
||||
success: (modalRes) => { if (modalRes.confirm) uni.openSetting(); }
|
||||
});
|
||||
} else {
|
||||
uni.showToast({ title: '保存失败', icon: 'none' });
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
fail: () => {
|
||||
uni.saveImageToPhotosAlbum({ filePath: path });
|
||||
}
|
||||
});
|
||||
},
|
||||
onShare() {
|
||||
uni.showActionSheet({
|
||||
itemList: ['分享到微信', '分享到朋友圈', '复制链接'],
|
||||
success: (res) => {
|
||||
if (res.tapIndex === 2) this.copyLink();
|
||||
}
|
||||
});
|
||||
},
|
||||
onCompare() {
|
||||
// 跳转到设计效果页
|
||||
const params = {
|
||||
id: this.currentArticleId || ''
|
||||
};
|
||||
|
||||
// 如果有图片数据,传递图片URL
|
||||
if (this.images && this.images.length > 0) {
|
||||
if (this.images.length >= 2) {
|
||||
// 如果有两张图片,第一张作为改造前,第二张作为改造后
|
||||
params.beforeImage = encodeURIComponent(this.images[0].originalUrl || this.images[0].displayUrl);
|
||||
params.afterImage = encodeURIComponent(this.images[1].originalUrl || this.images[1].displayUrl);
|
||||
} else if (this.images.length === 1) {
|
||||
// 如果只有一张图片,作为改造后
|
||||
params.afterImage = encodeURIComponent(this.images[0].originalUrl || this.images[0].displayUrl);
|
||||
}
|
||||
}
|
||||
|
||||
// 传递提示词
|
||||
if (this.imageDescription) {
|
||||
params.promptText = encodeURIComponent(this.imageDescription);
|
||||
} else if (this.articleData && this.articleData.prompt) {
|
||||
params.promptText = encodeURIComponent(this.articleData.prompt);
|
||||
}
|
||||
|
||||
const queryString = Object.keys(params)
|
||||
.filter(key => params[key])
|
||||
.map(key => `${key}=${params[key]}`)
|
||||
.join('&');
|
||||
|
||||
uni.navigateTo({
|
||||
url: `/pages/ai-generate/effect?${queryString}`,
|
||||
fail: (err) => {
|
||||
console.error('Navigate to effect page failed:', err);
|
||||
uni.showToast({
|
||||
title: '跳转失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
onBack() {
|
||||
uni.navigateBack();
|
||||
},
|
||||
copyLink() {
|
||||
const shareUrl = this.currentArticleId
|
||||
? `https://yourapp.com/pages/ai-generate/image?id=${this.currentArticleId}`
|
||||
: 'https://yourapp.com/pages/ai-generate/image';
|
||||
uni.setClipboardData({ data: shareUrl, success: () => { uni.showToast({ title: '链接已复制', icon: 'success' }); } });
|
||||
},
|
||||
onToggleDescription() {
|
||||
this.isDescriptionExpanded = !this.isDescriptionExpanded;
|
||||
},
|
||||
onCreatorTap() {
|
||||
uni.showToast({ title: `查看${this.creatorName}的主页`, icon: 'none' });
|
||||
},
|
||||
onFollow() {
|
||||
const isFollowing = !this.isFollowing;
|
||||
this.setData({ isFollowing });
|
||||
uni.showToast({ title: isFollowing ? '关注成功' : '取消关注', icon: 'success' });
|
||||
},
|
||||
onLike() {
|
||||
const isLiked = !this.isLiked;
|
||||
const likeCount = isLiked ? this.likeCount + 1 : this.likeCount - 1;
|
||||
this.setData({ isLiked, likeCount });
|
||||
if (isLiked) uni.vibrateShort();
|
||||
},
|
||||
onCreateSimilar() {
|
||||
const description = this.imageDescription || '';
|
||||
const encodedDescription = encodeURIComponent(description);
|
||||
|
||||
// 获取imageInput作为参考图
|
||||
let referenceImage = '';
|
||||
if (this.articleData && this.articleData.imageInput) {
|
||||
referenceImage = this.articleData.imageInput;
|
||||
} else if (this.images && this.images.length > 0) {
|
||||
// 如果没有imageInput,使用第一张图片
|
||||
referenceImage = this.images[0].originalUrl || this.images[0].displayUrl;
|
||||
}
|
||||
|
||||
const params = {
|
||||
description: encodedDescription,
|
||||
from: 'image',
|
||||
articleId: this.currentArticleId || ''
|
||||
};
|
||||
|
||||
if (referenceImage) {
|
||||
params.referenceImage = encodeURIComponent(referenceImage);
|
||||
}
|
||||
|
||||
const queryString = Object.keys(params)
|
||||
.filter(key => params[key])
|
||||
.map(key => `${key}=${params[key]}`)
|
||||
.join('&');
|
||||
|
||||
uni.navigateTo({
|
||||
url: `/pages/ai-generate/index?${queryString}`
|
||||
});
|
||||
},
|
||||
onConsult() {
|
||||
// #ifdef MP-WEIXIN
|
||||
// 微信小程序环境,打开微信客服
|
||||
wx.openCustomerServiceChat({
|
||||
extInfo: {
|
||||
url: 'https://work.weixin.qq.com/kfid/your_kfid' // 替换为实际的客服链接
|
||||
},
|
||||
corpId: 'your_corp_id', // 替换为实际的企业ID
|
||||
success: (res) => {
|
||||
console.log('打开客服成功', res);
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('打开客服失败', err);
|
||||
// 降级方案:跳转到客服页面
|
||||
const url = this.chatUrl;
|
||||
if (url) {
|
||||
uni.navigateTo({ url: `/pages/users/web_page/index?webUel=${encodeURIComponent(url)}&title=客服` });
|
||||
} else {
|
||||
uni.showToast({ title: '暂无客服', icon: 'none' });
|
||||
}
|
||||
}
|
||||
});
|
||||
// #endif
|
||||
|
||||
// #ifndef MP-WEIXIN
|
||||
// 非微信环境,使用原有逻辑
|
||||
const url = this.chatUrl;
|
||||
if (!url) {
|
||||
uni.showToast({ title: '暂无客服链接', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
uni.navigateTo({ url: `/pages/users/web_page/index?webUel=${encodeURIComponent(url)}&title=客服` });
|
||||
// #endif
|
||||
},
|
||||
onShareAppMessage() {
|
||||
let title = '精彩图片分享';
|
||||
if (this.articleData && this.articleData.title) {
|
||||
title = this.articleData.title;
|
||||
if (title.length > 28) title = title.substring(0, 25) + '...';
|
||||
} else if (this.creatorName) {
|
||||
title = `${this.creatorName}的作品`;
|
||||
}
|
||||
let imageUrl = (this.images[0] && this.images[0].originalUrl) || '/static/images/video-share.png';
|
||||
const path = this.currentArticleId ? `/pages/ai-generate/image?id=${this.currentArticleId}` : '/pages/ai-generate/assets';
|
||||
return { title, path, imageUrl };
|
||||
},
|
||||
onShareTimeline() {
|
||||
let title = '精彩图片分享';
|
||||
if (this.articleData && this.articleData.title) {
|
||||
title = this.articleData.title;
|
||||
if (title.length > 20) title = title.substring(0, 17) + '...';
|
||||
} else if (this.creatorName) {
|
||||
title = `${this.creatorName}的作品`;
|
||||
}
|
||||
let imageUrl = (this.images[0] && this.images[0].originalUrl) || '/static/images/video-share.png';
|
||||
return { title, imageUrl };
|
||||
},
|
||||
onPullDownRefresh() {
|
||||
if (this.currentArticleId) this.loadArticleDetail(this.currentArticleId);
|
||||
setTimeout(() => { uni.stopPullDownRefresh(); }, 800);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.image-container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: #000000;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.media-wrapper { position: absolute; top: 0; left: 0; right: 0; bottom: 0; }
|
||||
.image-swiper { width: 100%; height: 100%; }
|
||||
.image-swiper swiper-item { height: 100%; }
|
||||
.image-slide { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; }
|
||||
.zoom-wrapper { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; transition: transform 0.1s linear; }
|
||||
.display-image { width: 100%; height: 100%; }
|
||||
.top-controls { position: absolute; top: 0; left: 0; right: 0; height: 48px; display: flex; justify-content: space-between; align-items: flex-end; padding: 0 16px 16px; background: linear-gradient(180deg, rgba(0,0,0,0.6) 0%, transparent 100%); z-index: 10; }
|
||||
.left-controls { display: flex; align-items: center; }
|
||||
.back-btn { width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; margin-right: 12px; }
|
||||
.icon-back { font-size: 24px; color: #ffffff; font-weight: bold; }
|
||||
.time-display { font-size: 14px; color: #ffffff; font-weight: 500; }
|
||||
.bottom-info { position: absolute; bottom: 40rpx; left: 0; right: 0; padding: 11px; background: linear-gradient(0deg, rgba(0,0,0,0.8) 0%, transparent 100%); z-index: 10; }
|
||||
.article-status { margin-bottom: 16px; padding: 12px 16px; background: rgba(0, 0, 0, 0.6); border-radius: 8px; }
|
||||
.loading-text { color: #ffffff; font-size: 14px; opacity: 0.8; }
|
||||
.error-text { color: #ff6b6b; font-size: 14px; }
|
||||
.creator-info { display: flex; align-items: center; margin-bottom: 12px; }
|
||||
.creator-avatar { width: 40px; height: 40px; border-radius: 20px; margin-right: 12px; border: 2px solid rgba(255, 255, 255, 0.3); }
|
||||
.creator-details { flex: 1; display: flex; align-items: center; justify-content: space-between; }
|
||||
.creator-name { font-size: 16px; color: #ffffff; font-weight: 500; }
|
||||
.follow-btn { padding: 6px 16px; background: #ffffff; color: #333333; border-radius: 16px; font-size: 14px; font-weight: 500; transition: all 0.3s ease; }
|
||||
.follow-btn.following { background: rgba(255, 255, 255, 0.3); color: #ffffff; }
|
||||
.image-info { margin-bottom: 16px; }
|
||||
.image-description { font-size: 14px; color: #ffffff; line-height: 1.4; opacity: 0.9; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; overflow: hidden; text-overflow: ellipsis; word-break: break-all; cursor: pointer; }
|
||||
.image-description.expanded { -webkit-line-clamp: unset; line-clamp: unset; overflow: visible; }
|
||||
.expand-toggle { display: flex; justify-content: center; align-items: center; margin-top: 8px; padding: 4px 0; cursor: pointer; }
|
||||
.expand-toggle .iconfont { font-size: 16px; color: rgba(255, 255, 255, 0.7); transition: all 0.3s ease; }
|
||||
.expand-toggle:active .iconfont { color: rgba(255, 255, 255, 1); transform: scale(1.1); }
|
||||
.interaction-area { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
|
||||
.like-section { display: flex; align-items: center; }
|
||||
.like-btn { width: 44px; height: 44px; display: flex; align-items: center; justify-content: center; margin-right: 8px; transition: transform 0.2s ease; }
|
||||
.like-btn:active { transform: scale(1.2); }
|
||||
.heart-icon { font-size: 24px; }
|
||||
.like-count { font-size: 16px; color: #ffffff; font-weight: 500; }
|
||||
.action-buttons { display: flex; align-items: center; gap: 12px; }
|
||||
.floating-actions { position: fixed; right: 16px; bottom: 35%; display: flex; flex-direction: column; gap: 20px; z-index: 100; }
|
||||
.floating-btn { width: 42px; height: 42px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px; background: linear-gradient(135deg, rgba(0, 0, 0, 0.75) 0%, rgba(0, 0, 0, 0.25) 100%); border: 2px solid rgba(255, 255, 255, 0.15); border-radius: 50%; backdrop-filter: blur(20px); transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4), 0 4px 8px rgba(0, 0, 0, 0.2); }
|
||||
.download-btn { background: linear-gradient(135deg, rgba(76, 175, 80, 0.2) 0%, rgba(56, 142, 60, 0.4) 100%); border-color: rgba(139, 195, 74, 0.3); }
|
||||
.share-btn { background: linear-gradient(135deg, rgba(33, 150, 243, 0.2) 0%, rgba(25, 118, 210, 0.4) 100%); border-color: rgba(100, 181, 246, 0.3); }
|
||||
.floating-label { font-size: 12px; color: #ffffff; font-weight: 500; letter-spacing: 0.3px; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); }
|
||||
.consult-btn { padding: 12px 24px; background: rgba(255, 255, 255, 0.15); color: #ffffff; border: 1px solid rgba(255, 255, 255, 0.5); border-radius: 24px; font-size: 14px; font-weight: 500; transition: transform 0.2s ease, background 0.2s ease; }
|
||||
.consult-btn:active { transform: scale(0.95); background: rgba(255, 255, 255, 0.25); }
|
||||
.action-btn { padding: 12px 24px; background: #ffffff; color: #333333; border-radius: 24px; font-size: 14px; font-weight: 500; transition: transform 0.2s ease; }
|
||||
.action-btn:active { transform: scale(0.95); }
|
||||
.ai-notice { position: absolute; bottom: 180rpx; left: 14.5%; transform: translateX(-50%); display: flex; align-items: center; justify-content: center; gap: 8rpx; padding: 12rpx 24rpx; background: rgba(0, 0, 0, 0.3); border: 1rpx solid rgba(66, 202, 77, 0.5); border-radius: 40rpx; backdrop-filter: blur(10rpx); z-index: 100; }
|
||||
.ai-notice { position: fixed; width: 100px;
|
||||
height: 30px; top: 99px; left: 72%; transform: none; }
|
||||
.ai-notice-text { font-size: 20rpx; color: #42ca4d; line-height: 1.4; }
|
||||
.page-indicator { display: flex; justify-content: center; align-items: center; }
|
||||
.indicator-text { font-size: 12px; color: rgba(255,255,255,0.8); }
|
||||
@keyframes likeAnimation { 0% { transform: scale(1); } 50% { transform: scale(1.3); } 100% { transform: scale(1); } }
|
||||
.like-btn.liked .heart-icon { animation: likeAnimation 0.6s ease; }
|
||||
</style>
|
||||
1381
msh_single_uniapp/pages/ai-generate/index.vue
Normal file
1381
msh_single_uniapp/pages/ai-generate/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
1092
msh_single_uniapp/pages/ai-generate/inspiration.vue
Normal file
1092
msh_single_uniapp/pages/ai-generate/inspiration.vue
Normal file
File diff suppressed because it is too large
Load Diff
1486
msh_single_uniapp/pages/ai-generate/oneclick.vue
Normal file
1486
msh_single_uniapp/pages/ai-generate/oneclick.vue
Normal file
File diff suppressed because it is too large
Load Diff
1142
msh_single_uniapp/pages/ai-generate/result.vue
Normal file
1142
msh_single_uniapp/pages/ai-generate/result.vue
Normal file
File diff suppressed because it is too large
Load Diff
1374
msh_single_uniapp/pages/ai-generate/video.vue
Normal file
1374
msh_single_uniapp/pages/ai-generate/video.vue
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user