feat: 添加图标
243
TODO.md
Normal file
@@ -0,0 +1,243 @@
|
||||
# 小程序重构开发计划
|
||||
|
||||
## 项目概述
|
||||
将现有的图生图/图生视频小程序重构为模板卡片式首页 + 历史记录页面的架构。
|
||||
|
||||
## 开发阶段规划
|
||||
|
||||
### 阶段一:项目架构重构 (优先级:高)
|
||||
|
||||
#### 1.1 导航结构调整
|
||||
- [ ] 修改 `app.config.ts` 添加tabBar配置
|
||||
- 首页 (pages/home/index)
|
||||
- 历史记录 (pages/history/index)
|
||||
- [ ] 移除原有的 result 页面跳转逻辑
|
||||
- [ ] 更新页面路由配置
|
||||
|
||||
#### 1.2 页面文件结构创建
|
||||
```
|
||||
src/pages/
|
||||
├── home/ # 新首页 (模板卡片)
|
||||
│ ├── index.tsx
|
||||
│ ├── index.config.ts
|
||||
│ └── index.css
|
||||
├── history/ # 历史记录页面
|
||||
│ ├── index.tsx
|
||||
│ ├── index.config.ts
|
||||
│ └── index.css
|
||||
├── generate/ # 生成处理页面 (原index页面重构)
|
||||
│ ├── index.tsx
|
||||
│ ├── index.config.ts
|
||||
│ └── index.css
|
||||
└── result/ # 结果页面 (保留并优化)
|
||||
├── index.tsx
|
||||
├── index.config.ts
|
||||
└── index.css
|
||||
```
|
||||
|
||||
### 阶段二:数据存储和状态管理 (优先级:高)
|
||||
|
||||
#### 2.1 本地存储设计
|
||||
- [ ] 设计历史记录数据结构
|
||||
```typescript
|
||||
interface HistoryRecord {
|
||||
id: string
|
||||
type: 'image-to-image' | 'image-to-video'
|
||||
templateName: string
|
||||
inputImage: string
|
||||
outputResult: string | string[]
|
||||
status: 'generating' | 'completed' | 'failed'
|
||||
createTime: string
|
||||
updateTime: string
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.2 状态管理
|
||||
- [ ] 创建 `src/store/` 目录
|
||||
- [ ] 实现 Zustand 状态管理
|
||||
- [ ] `src/store/historyStore.ts` - 历史记录管理
|
||||
- [ ] `src/store/templateStore.ts` - 模板配置管理
|
||||
- [ ] 添加数据持久化功能
|
||||
|
||||
### 阶段三:模板卡片首页开发 (优先级:高)
|
||||
|
||||
#### 3.1 模板配置和数据
|
||||
- [ ] 设计8个模板的配置数据
|
||||
```typescript
|
||||
interface Template {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
type: 'image-to-image' | 'image-to-video'
|
||||
thumbnail: string
|
||||
category: string
|
||||
}
|
||||
```
|
||||
- [ ] 添加模板缩略图资源
|
||||
- [ ] 创建模板配置文件 `src/config/templates.ts`
|
||||
|
||||
#### 3.2 首页组件开发
|
||||
- [ ] 创建模板卡片组件 `src/components/TemplateCard/`
|
||||
- [ ] 支持2列网格布局
|
||||
- [ ] 卡片hover效果
|
||||
- [ ] 模板名称和描述显示
|
||||
- [ ] 实现首页布局 `src/pages/home/index.tsx`
|
||||
- [ ] 2列4行网格布局
|
||||
- [ ] 模板卡片点击跳转到生成页面
|
||||
- [ ] 添加首页样式 `src/pages/home/index.css`
|
||||
|
||||
### 阶段四:生成页面重构 (优先级:中)
|
||||
|
||||
#### 4.1 原index页面重构
|
||||
- [ ] 将 `src/pages/index/` 重命名为 `src/pages/generate/`
|
||||
- [ ] 修改生成页面接收模板参数
|
||||
```typescript
|
||||
// 从路由参数获取模板信息
|
||||
interface GeneratePageParams {
|
||||
templateId: string
|
||||
templateName: string
|
||||
templateType: 'image-to-image' | 'image-to-video'
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.2 生成流程优化
|
||||
- [ ] 添加模板信息显示
|
||||
- [ ] 优化上传和生成流程
|
||||
- [ ] 添加生成进度到历史记录
|
||||
- [ ] 生成完成后更新历史记录状态
|
||||
|
||||
### 阶段五:历史记录页面开发 (优先级:中)
|
||||
|
||||
#### 5.1 历史记录页面布局
|
||||
- [ ] 创建历史记录列表组件 `src/components/HistoryList/`
|
||||
- [ ] 实现记录卡片组件 `src/components/HistoryCard/`
|
||||
- [ ] 显示模板名称、生成时间
|
||||
- [ ] 生成状态指示器
|
||||
- [ ] 结果预览图
|
||||
|
||||
#### 5.2 历史记录功能
|
||||
- [ ] 实现历史记录分页加载
|
||||
- [ ] 添加筛选功能 (按类型、状态)
|
||||
- [ ] 添加删除记录功能
|
||||
- [ ] 点击记录查看详细结果
|
||||
|
||||
### 阶段六:组件优化和重构 (优先级:中)
|
||||
|
||||
#### 6.1 现有组件适配
|
||||
- [ ] 优化 `UploadButton` 组件
|
||||
- [ ] 适配不同模板类型
|
||||
- [ ] 添加模板上下文信息
|
||||
- [ ] 优化 `LoadingOverlay` 组件
|
||||
- [ ] 显示当前模板信息
|
||||
- [ ] 显示生成进度百分比
|
||||
- [ ] 优化 `ErrorOverlay` 组件
|
||||
- [ ] 提供重试和返回首页选项
|
||||
|
||||
#### 6.2 新增通用组件
|
||||
- [ ] 创建 `NavigationBar` 组件 (如需要自定义导航)
|
||||
- [ ] 创建 `StatusBadge` 组件 (状态指示器)
|
||||
- [ ] 创建 `ImagePreview` 组件 (图片预览)
|
||||
|
||||
### 阶段七:用户体验优化 (优先级:低)
|
||||
|
||||
#### 7.1 交互优化
|
||||
- [ ] 添加页面间过渡动画
|
||||
- [ ] 添加加载骨架屏
|
||||
- [ ] 优化触摸反馈
|
||||
- [ ] 添加操作确认对话框
|
||||
|
||||
#### 7.2 性能优化
|
||||
- [ ] 实现图片懒加载
|
||||
- [ ] 添加历史记录虚拟滚动
|
||||
- [ ] 优化打包体积
|
||||
- [ ] 添加缓存策略
|
||||
|
||||
### 阶段八:测试和文档 (优先级:低)
|
||||
|
||||
#### 8.1 单元测试 (遵循TDD原则)
|
||||
- [ ] 为历史记录Store添加测试
|
||||
- [ ] 为模板配置添加测试
|
||||
- [ ] 为通用工具函数添加测试
|
||||
- [ ] 为核心业务逻辑添加测试
|
||||
|
||||
#### 8.2 集成测试
|
||||
- [ ] 测试页面跳转流程
|
||||
- [ ] 测试数据持久化
|
||||
- [ ] 测试错误处理
|
||||
|
||||
## 技术实现细节
|
||||
|
||||
### 路由配置更新
|
||||
```typescript
|
||||
// app.config.ts
|
||||
export default defineAppConfig({
|
||||
pages: [
|
||||
'pages/home/index', // 新首页
|
||||
'pages/generate/index', // 生成页面
|
||||
'pages/result/index', // 结果页面
|
||||
'pages/history/index' // 历史记录页面
|
||||
],
|
||||
tabBar: {
|
||||
list: [
|
||||
{
|
||||
pagePath: 'pages/home/index',
|
||||
text: '首页',
|
||||
iconPath: 'assets/icons/home.png',
|
||||
selectedIconPath: 'assets/icons/home-active.png'
|
||||
},
|
||||
{
|
||||
pagePath: 'pages/history/index',
|
||||
text: '历史记录',
|
||||
iconPath: 'assets/icons/history.png',
|
||||
selectedIconPath: 'assets/icons/history-active.png'
|
||||
}
|
||||
]
|
||||
},
|
||||
// ...其他配置
|
||||
})
|
||||
```
|
||||
|
||||
### 数据流设计
|
||||
```
|
||||
首页模板选择 → 生成页面(携带模板信息) → 生成过程记录到历史 → 完成后更新历史状态 → 历史页面展示
|
||||
```
|
||||
|
||||
## 开发优先级说明
|
||||
|
||||
### 高优先级 (第1-3周)
|
||||
- 导航结构和基础页面创建
|
||||
- 数据存储结构设计
|
||||
- 模板卡片首页实现
|
||||
|
||||
### 中优先级 (第4-6周)
|
||||
- 生成页面重构
|
||||
- 历史记录页面开发
|
||||
- 组件优化
|
||||
|
||||
### 低优先级 (第7-8周)
|
||||
- 用户体验优化
|
||||
- 性能优化
|
||||
- 测试覆盖
|
||||
|
||||
## 里程碑检查点
|
||||
|
||||
1. **Week 2**: 基础页面结构和导航完成
|
||||
2. **Week 4**: 模板首页和基础功能完成
|
||||
3. **Week 6**: 历史记录功能完整实现
|
||||
4. **Week 8**: 完整功能测试和优化完成
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **遵循TDD开发模式** - 先写测试,后写实现
|
||||
2. **保持代码简洁** - 每个文件不超过500行
|
||||
3. **类型安全** - 严格使用TypeScript类型定义
|
||||
4. **平台兼容** - 确保微信小程序、H5、APP多端兼容
|
||||
5. **用户体验** - 保持流畅的交互体验
|
||||
6. **错误处理** - 完善的错误边界和用户反馈
|
||||
|
||||
## 依赖和资源需求
|
||||
|
||||
- **图标资源**: tabBar图标、模板缩略图
|
||||
- **样式主题**: 统一的设计风格和颜色规范
|
||||
- **API适配**: 确保现有SDK接口兼容新的页面流程
|
||||
- **存储方案**: 本地存储和可能的云端同步需求
|
||||
305
mini-app.md
Normal file
@@ -0,0 +1,305 @@
|
||||
# Taro 抖音小程序发布视频解决方案
|
||||
|
||||
## 概述
|
||||
|
||||
抖音小程序提供了官方的视频发布能力,允许用户通过小程序直接拍摄并发布视频到抖音。在 Taro 框架中,我们需要通过特定的方式来调用这个官方 API。
|
||||
|
||||
## 核心原理
|
||||
|
||||
抖音小程序的视频发布功能基于两个核心要素:
|
||||
1. **Button 组件**:设置 `open-type="uploadDouyinVideo"`
|
||||
2. **Page 钩子**:在页面中配置 `onUploadDouyinVideo` 钩子函数
|
||||
|
||||
## 实现方案
|
||||
|
||||
### 方案一:函数组件 + useReady Hook(推荐)
|
||||
|
||||
```typescript
|
||||
import React, { useState } from 'react';
|
||||
import { View, Button, Text } from '@tarojs/components';
|
||||
import Taro, { useReady } from '@tarojs/taro';
|
||||
|
||||
const VideoPublishPage: React.FC = () => {
|
||||
const [uploadStatus, setUploadStatus] = useState<string>('');
|
||||
|
||||
useReady(() => {
|
||||
// 获取当前页面实例
|
||||
const pages = Taro.getCurrentPages();
|
||||
const currentPage = pages[pages.length - 1];
|
||||
|
||||
// 配置抖音视频上传钩子
|
||||
currentPage.onUploadDouyinVideo = function(uploadOptions) {
|
||||
console.log('抖音视频上传参数:', uploadOptions);
|
||||
|
||||
setUploadStatus('正在处理视频...');
|
||||
|
||||
// 处理上传结果
|
||||
if (uploadOptions.errMsg) {
|
||||
if (uploadOptions.errMsg.includes('ok')) {
|
||||
setUploadStatus('视频发布成功!');
|
||||
Taro.showToast({
|
||||
title: '发布成功',
|
||||
icon: 'success'
|
||||
});
|
||||
} else {
|
||||
setUploadStatus('视频发布失败');
|
||||
Taro.showToast({
|
||||
title: '发布失败',
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<View className="video-publish-container">
|
||||
<View className="header">
|
||||
<Text className="title">发布抖音视频</Text>
|
||||
</View>
|
||||
|
||||
<View className="content">
|
||||
{/* 关键:Button 组件设置 openType */}
|
||||
<Button
|
||||
openType="uploadDouyinVideo"
|
||||
className="upload-button"
|
||||
type="primary"
|
||||
>
|
||||
拍摄并发布视频
|
||||
</Button>
|
||||
|
||||
{uploadStatus && (
|
||||
<View className="status">
|
||||
<Text>{uploadStatus}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default VideoPublishPage;
|
||||
```
|
||||
|
||||
### 方案二:类组件实现
|
||||
|
||||
```typescript
|
||||
import { Component } from 'react';
|
||||
import { View, Button } from '@tarojs/components';
|
||||
import Taro from '@tarojs/taro';
|
||||
|
||||
class VideoPublishPage extends Component {
|
||||
|
||||
onReady() {
|
||||
// 在页面 onReady 生命周期中配置钩子
|
||||
this.$instance.page.onUploadDouyinVideo = (uploadOptions) => {
|
||||
console.log('抖音视频上传参数:', uploadOptions);
|
||||
this.handleVideoUpload(uploadOptions);
|
||||
};
|
||||
}
|
||||
|
||||
handleVideoUpload = (uploadOptions: any) => {
|
||||
console.log('处理视频上传:', uploadOptions);
|
||||
|
||||
Taro.showToast({
|
||||
title: '视频发布中...',
|
||||
icon: 'loading'
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View className="video-publish-page">
|
||||
<Button openType="uploadDouyinVideo">
|
||||
发布抖音视频
|
||||
</Button>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default VideoPublishPage;
|
||||
```
|
||||
|
||||
### 方案三:自定义 Hook 封装
|
||||
|
||||
```typescript
|
||||
// hooks/useUploadDouyinVideo.ts
|
||||
import { useReady } from '@tarojs/taro';
|
||||
import Taro from '@tarojs/taro';
|
||||
|
||||
interface UploadOptions {
|
||||
errMsg?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export const useUploadDouyinVideo = (
|
||||
onUpload: (options: UploadOptions) => void
|
||||
) => {
|
||||
useReady(() => {
|
||||
const pages = Taro.getCurrentPages();
|
||||
const currentPage = pages[pages.length - 1];
|
||||
|
||||
if (currentPage) {
|
||||
currentPage.onUploadDouyinVideo = onUpload;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 使用示例
|
||||
import React from 'react';
|
||||
import { View, Button } from '@tarojs/components';
|
||||
import { useUploadDouyinVideo } from '../hooks/useUploadDouyinVideo';
|
||||
|
||||
const VideoPublishPage: React.FC = () => {
|
||||
|
||||
useUploadDouyinVideo((uploadOptions) => {
|
||||
console.log('视频上传参数:', uploadOptions);
|
||||
// 处理上传逻辑
|
||||
});
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Button openType="uploadDouyinVideo">
|
||||
发布抖音视频
|
||||
</Button>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## 配置要求
|
||||
|
||||
### 1. app.config.ts 配置
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
pages: [
|
||||
'pages/video-publish/index'
|
||||
],
|
||||
window: {
|
||||
backgroundTextStyle: 'light',
|
||||
navigationBarBackgroundColor: '#fff',
|
||||
navigationBarTitleText: '抖音视频发布',
|
||||
navigationBarTextStyle: 'black'
|
||||
},
|
||||
// 抖音小程序权限配置
|
||||
permission: {
|
||||
'scope.camera': {
|
||||
desc: '需要使用摄像头拍摄视频'
|
||||
},
|
||||
'scope.writePhotosAlbum': {
|
||||
desc: '需要保存视频到相册'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 样式配置
|
||||
|
||||
```scss
|
||||
// video-publish/index.scss
|
||||
.video-publish-container {
|
||||
padding: 40px 32px;
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 60px;
|
||||
|
||||
.title {
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
.upload-button {
|
||||
width: 100%;
|
||||
height: 88px;
|
||||
font-size: 32px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.status {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
|
||||
text {
|
||||
color: #666;
|
||||
font-size: 28px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 关键要点
|
||||
|
||||
### 1. 必须配置的两个要素
|
||||
- **Button 组件**:`openType="uploadDouyinVideo"`
|
||||
- **钩子函数**:`onUploadDouyinVideo`
|
||||
|
||||
### 2. 钩子函数配置时机
|
||||
- 必须在页面的 `onReady` 生命周期或之后配置
|
||||
- 函数组件使用 `useReady` Hook
|
||||
- 类组件在 `onReady` 方法中配置
|
||||
|
||||
### 3. 参数说明
|
||||
```typescript
|
||||
// uploadOptions 参数结构
|
||||
interface UploadOptions {
|
||||
errMsg: string; // 上传结果信息
|
||||
// 其他抖音官方返回的参数
|
||||
}
|
||||
```
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 问题1:钩子函数不触发
|
||||
|
||||
**解决方案**:确保在正确的时机配置钩子
|
||||
|
||||
```typescript
|
||||
useReady(() => {
|
||||
// 延迟配置,确保页面完全加载
|
||||
setTimeout(() => {
|
||||
const pages = Taro.getCurrentPages();
|
||||
const currentPage = pages[pages.length - 1];
|
||||
|
||||
if (currentPage) {
|
||||
currentPage.onUploadDouyinVideo = function(uploadOptions) {
|
||||
console.log('视频上传回调:', uploadOptions);
|
||||
};
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
```
|
||||
|
||||
### 问题2:Button 点击无反应
|
||||
|
||||
**检查清单**:
|
||||
- [ ] `openType` 属性是否正确设置为 `"uploadDouyinVideo"`
|
||||
- [ ] 是否在真实的抖音小程序环境中测试
|
||||
- [ ] 权限配置是否正确
|
||||
|
||||
### 问题3:Taro 版本兼容性
|
||||
|
||||
**建议**:
|
||||
- 使用 Taro 3.x 以上版本
|
||||
- 确保抖音小程序基础库版本支持该功能
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **测试环境**:该功能需要在真实的抖音小程序环境中测试,开发工具可能无法完全模拟
|
||||
2. **权限申请**:需要在小程序后台申请相应的视频发布权限
|
||||
3. **用户体验**:建议添加加载状态和错误处理
|
||||
4. **版本兼容**:确保 Taro 版本和抖音小程序基础库版本的兼容性
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [抖音小程序官方文档 - 发布抖音视频](https://developer.open-douyin.com/docs/resource/zh-CN/mini-app/develop/api/open-interface/video-capacity/upload-douyin-video)
|
||||
- [Taro GitHub Issue #13882](https://github.com/NervJS/taro/issues/13882)
|
||||
@@ -67,7 +67,8 @@
|
||||
"@tarojs/shared": "4.1.6",
|
||||
"@tarojs/taro": "4.1.6",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
"react-dom": "^18.0.0",
|
||||
"zustand": "^5.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.24.4",
|
||||
|
||||
26
pnpm-lock.yaml
generated
@@ -62,6 +62,9 @@ importers:
|
||||
react-dom:
|
||||
specifier: ^18.0.0
|
||||
version: 18.3.1(react@18.3.1)
|
||||
zustand:
|
||||
specifier: ^5.0.8
|
||||
version: 5.0.8(@types/react@18.3.24)(react@18.3.1)
|
||||
devDependencies:
|
||||
'@babel/core':
|
||||
specifier: ^7.24.4
|
||||
@@ -6143,6 +6146,24 @@ packages:
|
||||
yup@1.7.0:
|
||||
resolution: {integrity: sha512-VJce62dBd+JQvoc+fCVq+KZfPHr+hXaxCcVgotfwWvlR0Ja3ffYKaJBT8rptPOSKOGJDCUnW2C2JWpud7aRP6Q==}
|
||||
|
||||
zustand@5.0.8:
|
||||
resolution: {integrity: sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==}
|
||||
engines: {node: '>=12.20.0'}
|
||||
peerDependencies:
|
||||
'@types/react': '>=18.0.0'
|
||||
immer: '>=9.0.6'
|
||||
react: '>=18.0.0'
|
||||
use-sync-external-store: '>=1.2.0'
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
immer:
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
use-sync-external-store:
|
||||
optional: true
|
||||
|
||||
snapshots:
|
||||
|
||||
'@adobe/css-tools@4.4.4': {}
|
||||
@@ -13069,3 +13090,8 @@ snapshots:
|
||||
tiny-case: 1.0.3
|
||||
toposort: 2.0.2
|
||||
type-fest: 2.19.0
|
||||
|
||||
zustand@5.0.8(@types/react@18.3.24)(react@18.3.1):
|
||||
optionalDependencies:
|
||||
'@types/react': 18.3.24
|
||||
react: 18.3.1
|
||||
|
||||
@@ -1,12 +1,36 @@
|
||||
|
||||
export default defineAppConfig({
|
||||
pages: [
|
||||
'pages/index/index',
|
||||
'pages/home/index', // 新的模板卡片首页
|
||||
'pages/history/index', // 历史记录页面
|
||||
'pages/generate/index', // 生成处理页面
|
||||
'pages/index/index', // 保留原有首页,暂时作为模板页面
|
||||
'pages/result/index'
|
||||
],
|
||||
tabBar: {
|
||||
color: '#999999',
|
||||
selectedColor: '#007AFF',
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderStyle: 'black',
|
||||
list: [
|
||||
{
|
||||
pagePath: 'pages/home/index',
|
||||
text: '首页',
|
||||
iconPath: 'assets/icons/default/shouye.png',
|
||||
selectedIconPath: 'assets/icons/activate/shouye.png'
|
||||
},
|
||||
{
|
||||
pagePath: 'pages/history/index',
|
||||
text: '历史记录',
|
||||
iconPath: 'assets/icons/default/shoulijindu.png',
|
||||
selectedIconPath: 'assets/icons/activate/shoulijindu.png'
|
||||
}
|
||||
]
|
||||
},
|
||||
window: {
|
||||
backgroundTextStyle: 'light',
|
||||
navigationBarBackgroundColor: '#fff',
|
||||
navigationBarTitleText: 'WeChat',
|
||||
navigationBarTitleText: '图生视频',
|
||||
navigationBarTextStyle: 'black'
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
BIN
src/assets/icons/activate/fuwuchengnuo.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
src/assets/icons/activate/fuwuwangdian.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
src/assets/icons/activate/fuwuzixun-weixuanzhong.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
src/assets/icons/activate/fuwuzixun.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
src/assets/icons/activate/guohushenqing.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
src/assets/icons/activate/qifeijiaona.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
src/assets/icons/activate/quanbuyewu-weixuanzhong.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
src/assets/icons/activate/quanbuyewu.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
src/assets/icons/activate/rexiandianhua.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
src/assets/icons/activate/shoufeibiaozhun.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
src/assets/icons/activate/shoulijindu-xuanzhong.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
src/assets/icons/activate/shoulijindu.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
src/assets/icons/activate/shoulijindu_1.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
src/assets/icons/activate/shouye-weixuanzhong.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
src/assets/icons/activate/shouye.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
src/assets/icons/activate/wode-weixuanzhong.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
src/assets/icons/activate/wode.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
src/assets/icons/activate/wodedizhi.png
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
src/assets/icons/activate/wodetongzhi.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
src/assets/icons/activate/yewuzhinan.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
src/assets/icons/activate/yijianfankui.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
src/assets/icons/activate/yonghubaozhuang.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
src/assets/icons/activate/yonghugaizhuang.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
src/assets/icons/activate/yuangongshibie.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
src/assets/icons/activate/yuyuedianhuo.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
src/assets/icons/activate/yuyuekaitong.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
src/assets/icons/activate/yuyueweixiu.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
src/assets/icons/activate/yuyuezhongxin.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
src/assets/icons/activate/zhangdanchaxun.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
src/assets/icons/default/fuwuchengnuo.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
src/assets/icons/default/fuwuwangdian.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
src/assets/icons/default/fuwuzixun-weixuanzhong.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
src/assets/icons/default/fuwuzixun.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
src/assets/icons/default/guohushenqing.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
src/assets/icons/default/qifeijiaona.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
src/assets/icons/default/quanbuyewu-weixuanzhong.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
src/assets/icons/default/quanbuyewu.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
src/assets/icons/default/rexiandianhua.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
src/assets/icons/default/shoufeibiaozhun.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
src/assets/icons/default/shoulijindu-xuanzhong.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
src/assets/icons/default/shoulijindu.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
src/assets/icons/default/shoulijindu_1.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
src/assets/icons/default/shouye-weixuanzhong.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
src/assets/icons/default/shouye.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
src/assets/icons/default/wode-weixuanzhong.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
src/assets/icons/default/wode.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
src/assets/icons/default/wodedizhi.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
src/assets/icons/default/wodetongzhi.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
src/assets/icons/default/yewuzhinan.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
src/assets/icons/default/yijianfankui.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
src/assets/icons/default/yonghubaozhuang.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
src/assets/icons/default/yonghugaizhuang.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
src/assets/icons/default/yuangongshibie.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
src/assets/icons/default/yuyuedianhuo.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
src/assets/icons/default/yuyuekaitong.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
src/assets/icons/default/yuyueweixiu.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
src/assets/icons/default/yuyuezhongxin.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
src/assets/icons/default/zhangdanchaxun.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
1
src/assets/templates/3d-animation.png
Normal file
@@ -0,0 +1 @@
|
||||
# 3D动画缩略图 - 需要替换为实际图片
|
||||
1
src/assets/templates/animation.png
Normal file
@@ -0,0 +1 @@
|
||||
# 动画效果缩略图 - 需要替换为实际图片
|
||||
1
src/assets/templates/art-style.png
Normal file
@@ -0,0 +1 @@
|
||||
# 艺术风格缩略图 - 需要替换为实际图片
|
||||
1
src/assets/templates/cartoon.png
Normal file
@@ -0,0 +1 @@
|
||||
# 卡通风格缩略图 - 需要替换为实际图片
|
||||
1
src/assets/templates/sketch.png
Normal file
@@ -0,0 +1 @@
|
||||
# 素描风格缩略图 - 需要替换为实际图片
|
||||
1
src/assets/templates/video-gen.png
Normal file
@@ -0,0 +1 @@
|
||||
# 视频生成缩略图 - 需要替换为实际图片
|
||||
1
src/assets/templates/vintage.png
Normal file
@@ -0,0 +1 @@
|
||||
# 复古滤镜缩略图 - 需要替换为实际图片
|
||||
1
src/assets/templates/watercolor.png
Normal file
@@ -0,0 +1 @@
|
||||
# 水彩画缩略图 - 需要替换为实际图片
|
||||
104
src/components/TemplateCard/index.css
Normal file
@@ -0,0 +1,104 @@
|
||||
.template-card {
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 160px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.template-card:active {
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.template-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(135deg, rgba(0, 122, 255, 0.05), rgba(90, 200, 250, 0.05));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.template-thumbnail {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, #007AFF, #5AC8FA);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 12px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.template-icon {
|
||||
font-size: 20px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.template-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.template-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.template-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
line-height: 1.3;
|
||||
margin-bottom: 6px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.template-desc {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 12px;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.template-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.template-type {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.template-category {
|
||||
font-size: 11px;
|
||||
color: #8E8E93;
|
||||
font-weight: 400;
|
||||
}
|
||||
57
src/components/TemplateCard/index.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { View, Text, Image } from '@tarojs/components'
|
||||
import { Template } from '../../store/types'
|
||||
import './index.css'
|
||||
|
||||
interface TemplateCardProps {
|
||||
template: Template
|
||||
onClick: (template: Template) => void
|
||||
}
|
||||
|
||||
export default function TemplateCard({ template, onClick }: TemplateCardProps) {
|
||||
const handleClick = () => {
|
||||
onClick(template)
|
||||
}
|
||||
|
||||
const getTypeText = (type: Template['type']) => {
|
||||
return type === 'image-to-video' ? '图生视频' : '图生图'
|
||||
}
|
||||
|
||||
const getTypeColor = (type: Template['type']) => {
|
||||
return type === 'image-to-video' ? '#FF6B35' : '#007AFF'
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='template-card' onClick={handleClick}>
|
||||
<View className='template-thumbnail'>
|
||||
{/* 暂时使用emoji作为占位符,后续替换为真实图片 */}
|
||||
<Text className='template-icon'>
|
||||
{template.type === 'image-to-video' ? '🎬' : '🎨'}
|
||||
</Text>
|
||||
{/*
|
||||
<Image
|
||||
className='template-image'
|
||||
src={template.thumbnail}
|
||||
mode='aspectFill'
|
||||
/>
|
||||
*/}
|
||||
</View>
|
||||
|
||||
<View className='template-info'>
|
||||
<Text className='template-name'>{template.name}</Text>
|
||||
<Text className='template-desc'>{template.description}</Text>
|
||||
<View className='template-meta'>
|
||||
<Text
|
||||
className='template-type'
|
||||
style={{
|
||||
color: getTypeColor(template.type),
|
||||
backgroundColor: `${getTypeColor(template.type)}15`
|
||||
}}
|
||||
>
|
||||
{getTypeText(template.type)}
|
||||
</Text>
|
||||
<Text className='template-category'>{template.category}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
84
src/config/templates.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { Template } from '../store/types'
|
||||
|
||||
// 模板配置数据
|
||||
export const TEMPLATE_CONFIG: Template[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: '艺术风格',
|
||||
description: '将照片转为艺术风格',
|
||||
type: 'image-to-image',
|
||||
thumbnail: '/assets/templates/art-style.png',
|
||||
category: '风格转换'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: '视频生成',
|
||||
description: '图片生成动态视频',
|
||||
type: 'image-to-video',
|
||||
thumbnail: '/assets/templates/video-gen.png',
|
||||
category: '视频生成'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: '卡通风格',
|
||||
description: '真人照片卡通化',
|
||||
type: 'image-to-image',
|
||||
thumbnail: '/assets/templates/cartoon.png',
|
||||
category: '风格转换'
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: '复古滤镜',
|
||||
description: '添加复古质感',
|
||||
type: 'image-to-image',
|
||||
thumbnail: '/assets/templates/vintage.png',
|
||||
category: '滤镜效果'
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: '动画效果',
|
||||
description: '制作动画视频',
|
||||
type: 'image-to-video',
|
||||
thumbnail: '/assets/templates/animation.png',
|
||||
category: '视频生成'
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
name: '素描风格',
|
||||
description: '转换为素描效果',
|
||||
type: 'image-to-image',
|
||||
thumbnail: '/assets/templates/sketch.png',
|
||||
category: '风格转换'
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
name: '水彩画',
|
||||
description: '水彩画风格转换',
|
||||
type: 'image-to-image',
|
||||
thumbnail: '/assets/templates/watercolor.png',
|
||||
category: '风格转换'
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
name: '3D动画',
|
||||
description: '生成3D动画效果',
|
||||
type: 'image-to-video',
|
||||
thumbnail: '/assets/templates/3d-animation.png',
|
||||
category: '视频生成'
|
||||
}
|
||||
]
|
||||
|
||||
// 根据类型获取模板
|
||||
export const getTemplatesByType = (type: 'image-to-image' | 'image-to-video'): Template[] => {
|
||||
return TEMPLATE_CONFIG.filter(template => template.type === type)
|
||||
}
|
||||
|
||||
// 根据分类获取模板
|
||||
export const getTemplatesByCategory = (category: string): Template[] => {
|
||||
return TEMPLATE_CONFIG.filter(template => template.category === category)
|
||||
}
|
||||
|
||||
// 根据ID获取模板
|
||||
export const getTemplateById = (id: string): Template | undefined => {
|
||||
return TEMPLATE_CONFIG.find(template => template.id === id)
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
|
||||
|
||||
export { useAd } from './useAd'
|
||||
export { useSdk } from './useSdk'
|
||||
export { useSdk } from './useSdk'
|
||||
export { useUploadVideo } from './useUploadVideo'
|
||||
20
src/hooks/useUploadVideo.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { useReady } from '@tarojs/taro';
|
||||
import Taro from '@tarojs/taro';
|
||||
|
||||
interface UploadOptions {
|
||||
errMsg?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export const useUploadVideo = (
|
||||
onUpload: (options: UploadOptions) => void
|
||||
) => {
|
||||
useReady(() => {
|
||||
const pages = Taro.getCurrentPages();
|
||||
const currentPage = pages[pages.length - 1];
|
||||
|
||||
if (currentPage) {
|
||||
currentPage.onUploadDouyinVideo = onUpload;
|
||||
}
|
||||
});
|
||||
};
|
||||
8
src/pages/generate/index.config.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '生成中',
|
||||
navigationBarBackgroundColor: '#2c3e50',
|
||||
navigationBarTextStyle: 'white',
|
||||
backgroundColorTop: '#2c3e50',
|
||||
backgroundColorBottom: '#34495e',
|
||||
backgroundColor: '#2c3e50'
|
||||
})
|
||||
4
src/pages/generate/index.css
Normal file
@@ -0,0 +1,4 @@
|
||||
.generate {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
11
src/pages/generate/index.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import './index.css'
|
||||
|
||||
// 暂时的占位页面,后续会迁移原有的生成逻辑
|
||||
export default function Generate() {
|
||||
return (
|
||||
<View className='generate'>
|
||||
<Text>生成处理页面(开发中)</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
8
src/pages/history/index.config.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '历史记录',
|
||||
navigationBarBackgroundColor: '#2c3e50',
|
||||
navigationBarTextStyle: 'white',
|
||||
backgroundColorTop: '#2c3e50',
|
||||
backgroundColorBottom: '#34495e',
|
||||
backgroundColor: '#2c3e50'
|
||||
})
|
||||
134
src/pages/history/index.css
Normal file
@@ -0,0 +1,134 @@
|
||||
.history {
|
||||
background-color: #f8f9fa;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.history-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 16px 16px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.history-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
font-size: 16px;
|
||||
color: #007AFF;
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.history-list {
|
||||
height: calc(100vh - 160px);
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 80px 20px;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 60px;
|
||||
display: block;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 18px;
|
||||
color: #333;
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.empty-desc {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.history-item {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.item-thumbnail {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background: linear-gradient(135deg, #007AFF, #5AC8FA);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.thumbnail-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.item-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.item-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.item-status {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
padding: 4px 8px;
|
||||
border-radius: 10px;
|
||||
background: rgba(142, 142, 147, 0.1);
|
||||
margin-left: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.item-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.item-type {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
background: rgba(0, 122, 255, 0.1);
|
||||
color: #007AFF;
|
||||
padding: 3px 8px;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.item-time {
|
||||
font-size: 12px;
|
||||
color: #8E8E93;
|
||||
}
|
||||
123
src/pages/history/index.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import { View, Text, ScrollView } from '@tarojs/components'
|
||||
import { useEffect } from 'react'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useHistoryStore } from '../../store'
|
||||
import './index.css'
|
||||
|
||||
export default function History() {
|
||||
const {
|
||||
records,
|
||||
loadRecords,
|
||||
clearRecords
|
||||
} = useHistoryStore()
|
||||
|
||||
useEffect(() => {
|
||||
loadRecords()
|
||||
}, [loadRecords])
|
||||
|
||||
const clearHistory = () => {
|
||||
Taro.showModal({
|
||||
title: '确认清空',
|
||||
content: '是否清空所有历史记录?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await clearRecords()
|
||||
Taro.showToast({
|
||||
title: '清空成功',
|
||||
icon: 'success'
|
||||
})
|
||||
} catch (error) {
|
||||
Taro.showToast({
|
||||
title: '清空失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
switch (status) {
|
||||
case 'generating': return '生成中'
|
||||
case 'completed': return '已完成'
|
||||
case 'failed': return '生成失败'
|
||||
default: return '未知状态'
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'generating': return '#FF9500'
|
||||
case 'completed': return '#34C759'
|
||||
case 'failed': return '#FF3B30'
|
||||
default: return '#8E8E93'
|
||||
}
|
||||
}
|
||||
|
||||
const formatTime = (timeStr: string) => {
|
||||
const date = new Date(timeStr)
|
||||
return date.toLocaleString('zh-CN', {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='history'>
|
||||
<View className='history-header'>
|
||||
<Text className='history-title'>历史记录</Text>
|
||||
{records.length > 0 && (
|
||||
<Text className='clear-btn' onClick={clearHistory}>清空</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<ScrollView
|
||||
className='history-list'
|
||||
scrollY
|
||||
refresherEnabled
|
||||
refresherTriggered={false}
|
||||
>
|
||||
{records.length === 0 ? (
|
||||
<View className='empty-state'>
|
||||
<Text className='empty-icon'>📝</Text>
|
||||
<Text className='empty-text'>暂无历史记录</Text>
|
||||
<Text className='empty-desc'>开始创作你的第一个作品吧</Text>
|
||||
</View>
|
||||
) : (
|
||||
records.map((record) => (
|
||||
<View key={record.id} className='history-item'>
|
||||
<View className='item-thumbnail'>
|
||||
<Text className='thumbnail-icon'>
|
||||
{record.type === 'image-to-video' ? '🎬' : '🖼️'}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View className='item-content'>
|
||||
<View className='item-header'>
|
||||
<Text className='item-title'>{record.templateName}</Text>
|
||||
<Text
|
||||
className='item-status'
|
||||
style={{ color: getStatusColor(record.status) }}
|
||||
>
|
||||
{getStatusText(record.status)}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View className='item-info'>
|
||||
<Text className='item-type'>
|
||||
{record.type === 'image-to-video' ? '图生视频' : '图生图'}
|
||||
</Text>
|
||||
<Text className='item-time'>{formatTime(record.createTime)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</ScrollView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
8
src/pages/home/index.config.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '首页',
|
||||
navigationBarBackgroundColor: '#2c3e50',
|
||||
navigationBarTextStyle: 'white',
|
||||
backgroundColorTop: '#2c3e50',
|
||||
backgroundColorBottom: '#34495e',
|
||||
backgroundColor: '#2c3e50'
|
||||
})
|
||||
32
src/pages/home/index.css
Normal file
@@ -0,0 +1,32 @@
|
||||
.home {
|
||||
padding: 20px 16px;
|
||||
background-color: #f8f9fa;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.home-header {
|
||||
text-align: center;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.home-title {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.home-subtitle {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.template-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
43
src/pages/home/index.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { useEffect } from 'react'
|
||||
import { navigateTo } from '@tarojs/taro'
|
||||
import { useTemplateStore, Template } from '../../store/index'
|
||||
import TemplateCard from '../../components/TemplateCard'
|
||||
import './index.css'
|
||||
|
||||
export default function Home() {
|
||||
const { templates, initTemplates, setSelectedTemplate } = useTemplateStore()
|
||||
|
||||
useEffect(() => {
|
||||
initTemplates()
|
||||
}, [initTemplates])
|
||||
|
||||
const handleTemplateClick = (template: Template) => {
|
||||
// 设置选中的模板
|
||||
setSelectedTemplate(template)
|
||||
|
||||
// 跳转到原有的功能页面,保持老功能可用
|
||||
navigateTo({
|
||||
url: `/pages/index/index?templateId=${template.id}&templateName=${encodeURIComponent(template.name)}&templateType=${template.type}`
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='home'>
|
||||
<View className='home-header'>
|
||||
<Text className='home-title'>选择模板</Text>
|
||||
<Text className='home-subtitle'>选择你喜欢的风格模板</Text>
|
||||
</View>
|
||||
|
||||
<View className='template-grid'>
|
||||
{templates.map((template) => (
|
||||
<TemplateCard
|
||||
key={template.id}
|
||||
template={template}
|
||||
onClick={handleTemplateClick}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -2,10 +2,11 @@ import { View } from '@tarojs/components'
|
||||
import { useState } from 'react'
|
||||
import { navigateTo } from '@tarojs/taro'
|
||||
import './index.css'
|
||||
import { useSdk } from '../../hooks/index'
|
||||
import { useSdk, useUploadVideo } from '../../hooks/index'
|
||||
import UploadButton from '../../components/UploadButton'
|
||||
import LoadingOverlay from '../../components/LoadingOverlay'
|
||||
import ErrorOverlay from '../../components/ErrorOverlay'
|
||||
import { createPlatformFactory } from '../../platforms'
|
||||
|
||||
type PageStep = 'upload' | 'loading' | 'error'
|
||||
|
||||
@@ -21,8 +22,36 @@ export default function Index() {
|
||||
error: null
|
||||
})
|
||||
|
||||
const [videoPath, setVidePath] = useState<string>()
|
||||
|
||||
useUploadVideo((res) => {
|
||||
console.log(res)
|
||||
return {
|
||||
videoPath: videoPath,
|
||||
titleConfig: {
|
||||
title: `视频发布`
|
||||
},
|
||||
success() {
|
||||
console.log("视频发布/挂载成功");
|
||||
},
|
||||
fail() {
|
||||
console.log("视频发布/挂载失败");
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const imageToVideo = async () => {
|
||||
const imgUrl = `https://cdn.roasmax.cn/upload/966d254b1859453e80e7de5f155a06fb.png`
|
||||
console.log(`img url is : ${imgUrl}`)
|
||||
const res = await sdk.imageToVideo({ img_url: imgUrl, duration: 2 })
|
||||
console.log({ imageToVideoResult: res })
|
||||
const tempFilePath = await createPlatformFactory().createMedia().downloadFile(res).then(res => res.tempFilePath)
|
||||
setVidePath(tempFilePath)
|
||||
}
|
||||
|
||||
const chooseAndGenerateImage = async () => {
|
||||
try {
|
||||
return await imageToVideo()
|
||||
// 选择图片,选择完成后会自动触发loading状态
|
||||
const task_id = await sdk.chooseAndGenerateImage({
|
||||
onImageSelected: () => {
|
||||
@@ -52,6 +81,8 @@ export default function Index() {
|
||||
setState({ step: 'upload', error: null })
|
||||
}
|
||||
|
||||
|
||||
|
||||
const renderCurrentStep = () => {
|
||||
switch (state.step) {
|
||||
case 'upload':
|
||||
|
||||
@@ -166,7 +166,13 @@ export abstract class UserInfo {
|
||||
abstract checkSession(): Promise<ICheckSession>;
|
||||
}
|
||||
|
||||
|
||||
export interface IDownloadFile {
|
||||
errMsg: string;
|
||||
header: { [key: string]: string };
|
||||
profile: {};
|
||||
statusCode: number;
|
||||
tempFilePath: string;
|
||||
}
|
||||
export abstract class Media {
|
||||
abstract downloadFile(url: string): Promise<void>;
|
||||
abstract downloadFile(url: string): Promise<IDownloadFile>;
|
||||
}
|
||||
@@ -7,7 +7,8 @@ import {
|
||||
UserInfo,
|
||||
IUserProfile,
|
||||
ICheckSession,
|
||||
Media
|
||||
Media,
|
||||
IDownloadFile
|
||||
} from "./core";
|
||||
|
||||
/**
|
||||
@@ -244,7 +245,7 @@ export class UserInfoTT extends UserInfo {
|
||||
|
||||
|
||||
export class MediaTT extends Media {
|
||||
downloadFile(url: string): Promise<void> {
|
||||
downloadFile(url: string): Promise<IDownloadFile> {
|
||||
return new Promise((resolve, reject) => {
|
||||
tt.downloadFile({
|
||||
url, success: (res: any) => {
|
||||
|
||||
@@ -7,7 +7,8 @@ import {
|
||||
UserInfo,
|
||||
IUserProfile,
|
||||
ICheckSession,
|
||||
Media
|
||||
Media,
|
||||
IDownloadFile
|
||||
} from "./core";
|
||||
|
||||
/**
|
||||
@@ -243,7 +244,7 @@ export class UserInfoWeApp extends UserInfo {
|
||||
|
||||
|
||||
export class MediaWeApp extends Media {
|
||||
downloadFile(url: string): Promise<void> {
|
||||
downloadFile(url: string): Promise<IDownloadFile> {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.downloadFile({
|
||||
url, success: (res: any) => {
|
||||
|
||||
137
src/store/historyStore.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import { create } from 'zustand'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { HistoryRecord } from './types'
|
||||
|
||||
interface HistoryStore {
|
||||
records: HistoryRecord[]
|
||||
loading: boolean
|
||||
error: string | null
|
||||
|
||||
// Actions
|
||||
loadRecords: () => Promise<void>
|
||||
addRecord: (record: Omit<HistoryRecord, 'id' | 'createTime' | 'updateTime'>) => Promise<string>
|
||||
updateRecord: (id: string, updates: Partial<HistoryRecord>) => Promise<void>
|
||||
deleteRecord: (id: string) => Promise<void>
|
||||
clearRecords: () => Promise<void>
|
||||
getRecordById: (id: string) => HistoryRecord | undefined
|
||||
getRecordsByStatus: (status: HistoryRecord['status']) => HistoryRecord[]
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'historyRecords'
|
||||
|
||||
export const useHistoryStore = create<HistoryStore>((set, get) => ({
|
||||
records: [],
|
||||
loading: false,
|
||||
error: null,
|
||||
|
||||
// 加载历史记录
|
||||
loadRecords: async () => {
|
||||
set({ loading: true, error: null })
|
||||
try {
|
||||
const savedRecords = Taro.getStorageSync(STORAGE_KEY) || []
|
||||
set({ records: savedRecords, loading: false })
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : '加载历史记录失败'
|
||||
set({ error: errorMsg, loading: false })
|
||||
console.error('加载历史记录失败:', error)
|
||||
}
|
||||
},
|
||||
|
||||
// 添加新记录
|
||||
addRecord: async (recordData) => {
|
||||
const id = `record_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
||||
const now = new Date().toISOString()
|
||||
|
||||
const newRecord: HistoryRecord = {
|
||||
...recordData,
|
||||
id,
|
||||
createTime: now,
|
||||
updateTime: now
|
||||
}
|
||||
|
||||
try {
|
||||
const currentRecords = get().records
|
||||
const updatedRecords = [newRecord, ...currentRecords]
|
||||
|
||||
await Taro.setStorage({
|
||||
key: STORAGE_KEY,
|
||||
data: updatedRecords
|
||||
})
|
||||
|
||||
set({ records: updatedRecords })
|
||||
return id
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : '添加记录失败'
|
||||
set({ error: errorMsg })
|
||||
console.error('添加记录失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
// 更新记录
|
||||
updateRecord: async (id, updates) => {
|
||||
try {
|
||||
const currentRecords = get().records
|
||||
const updatedRecords = currentRecords.map(record =>
|
||||
record.id === id
|
||||
? { ...record, ...updates, updateTime: new Date().toISOString() }
|
||||
: record
|
||||
)
|
||||
|
||||
await Taro.setStorage({
|
||||
key: STORAGE_KEY,
|
||||
data: updatedRecords
|
||||
})
|
||||
|
||||
set({ records: updatedRecords })
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : '更新记录失败'
|
||||
set({ error: errorMsg })
|
||||
console.error('更新记录失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
// 删除记录
|
||||
deleteRecord: async (id) => {
|
||||
try {
|
||||
const currentRecords = get().records
|
||||
const updatedRecords = currentRecords.filter(record => record.id !== id)
|
||||
|
||||
await Taro.setStorage({
|
||||
key: STORAGE_KEY,
|
||||
data: updatedRecords
|
||||
})
|
||||
|
||||
set({ records: updatedRecords })
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : '删除记录失败'
|
||||
set({ error: errorMsg })
|
||||
console.error('删除记录失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
// 清空所有记录
|
||||
clearRecords: async () => {
|
||||
try {
|
||||
await Taro.removeStorage({ key: STORAGE_KEY })
|
||||
set({ records: [] })
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : '清空记录失败'
|
||||
set({ error: errorMsg })
|
||||
console.error('清空记录失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
// 获取单个记录
|
||||
getRecordById: (id) => {
|
||||
return get().records.find(record => record.id === id)
|
||||
},
|
||||
|
||||
// 按状态筛选记录
|
||||
getRecordsByStatus: (status) => {
|
||||
return get().records.filter(record => record.status === status)
|
||||
}
|
||||
}))
|
||||
4
src/store/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// 统一导出所有store
|
||||
export * from './types'
|
||||
export * from './historyStore'
|
||||
export * from './templateStore'
|
||||
39
src/store/templateStore.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { create } from 'zustand'
|
||||
import { Template } from './types'
|
||||
import { TEMPLATE_CONFIG } from '../config/templates'
|
||||
|
||||
interface TemplateStore {
|
||||
templates: Template[]
|
||||
selectedTemplate: Template | null
|
||||
|
||||
// Actions
|
||||
initTemplates: () => void
|
||||
getTemplateById: (id: string) => Template | undefined
|
||||
setSelectedTemplate: (template: Template | null) => void
|
||||
getTemplatesByType: (type: 'image-to-image' | 'image-to-video') => Template[]
|
||||
}
|
||||
|
||||
export const useTemplateStore = create<TemplateStore>((set, get) => ({
|
||||
templates: [],
|
||||
selectedTemplate: null,
|
||||
|
||||
// 初始化模板数据
|
||||
initTemplates: () => {
|
||||
set({ templates: TEMPLATE_CONFIG })
|
||||
},
|
||||
|
||||
// 根据ID获取模板
|
||||
getTemplateById: (id) => {
|
||||
return get().templates.find(template => template.id === id)
|
||||
},
|
||||
|
||||
// 设置选中的模板
|
||||
setSelectedTemplate: (template) => {
|
||||
set({ selectedTemplate: template })
|
||||
},
|
||||
|
||||
// 根据类型筛选模板
|
||||
getTemplatesByType: (type) => {
|
||||
return get().templates.filter(template => template.type === type)
|
||||
}
|
||||
}))
|
||||
28
src/store/types.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
// 数据类型定义
|
||||
export interface HistoryRecord {
|
||||
id: string
|
||||
type: 'image-to-image' | 'image-to-video'
|
||||
templateId: string
|
||||
templateName: string
|
||||
inputImage: string
|
||||
outputResult?: string | string[]
|
||||
status: 'generating' | 'completed' | 'failed'
|
||||
createTime: string
|
||||
updateTime: string
|
||||
errorMessage?: string
|
||||
}
|
||||
|
||||
export interface Template {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
type: 'image-to-image' | 'image-to-video'
|
||||
thumbnail: string
|
||||
category: string
|
||||
}
|
||||
|
||||
export interface GenerateParams {
|
||||
templateId: string
|
||||
imageUrl: string
|
||||
additionalParams?: Record<string, any>
|
||||
}
|
||||