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:
2026-02-28 05:40:21 +08:00
commit 14d29d51c0
2182 changed files with 482509 additions and 0 deletions

View 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已对接完成

View 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>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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}&regenerate=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>

File diff suppressed because it is too large Load Diff

View 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>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff