refactor: 转vite
82
README.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# 图生视频 H5 应用
|
||||
|
||||
这是一个从 Taro 小程序转换而来的 Vite + React H5 应用。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 图片风格转换
|
||||
- 模板选择和预览
|
||||
- 历史记录管理
|
||||
- 多语言支持
|
||||
- 响应式设计
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **框架**: React 18
|
||||
- **构建工具**: Vite 5
|
||||
- **路由**: React Router 6
|
||||
- **状态管理**: Redux Toolkit
|
||||
- **样式**: CSS + Tailwind CSS
|
||||
- **类型检查**: TypeScript
|
||||
|
||||
## 开发环境
|
||||
|
||||
### 安装依赖
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
### 启动开发服务器
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
应用将在 http://localhost:3000 启动
|
||||
|
||||
### 构建生产版本
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
|
||||
### 预览生产构建
|
||||
|
||||
```bash
|
||||
pnpm preview
|
||||
```
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── components/ # 可复用组件
|
||||
├── pages/ # 页面组件
|
||||
├── hooks/ # 自定义 Hooks
|
||||
├── store/ # Redux 状态管理
|
||||
├── utils/ # 工具函数
|
||||
├── i18n/ # 国际化
|
||||
├── platforms/ # 平台适配层
|
||||
└── sdk/ # API SDK
|
||||
|
||||
public/
|
||||
└── assets/ # 静态资源
|
||||
```
|
||||
|
||||
## 主要变更
|
||||
|
||||
从 Taro 小程序转换为 Vite H5 应用的主要变更:
|
||||
|
||||
1. **构建工具**: Taro → Vite
|
||||
2. **组件系统**: Taro 组件 → 标准 HTML 元素
|
||||
3. **路由系统**: Taro 路由 → React Router
|
||||
4. **API 调用**: Taro API → Web API
|
||||
5. **样式系统**: 保持原有 CSS,适配标准 HTML
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 静态资源已迁移到 `public/assets/` 目录
|
||||
- 所有 Taro 特定的 API 调用已替换为 Web 标准 API
|
||||
- 保持了原有的业务逻辑和状态管理
|
||||
- 支持现代浏览器,建议使用 Chrome/Safari/Firefox 最新版本
|
||||
@@ -1,12 +0,0 @@
|
||||
// babel-preset-taro 更多选项和默认值:
|
||||
// https://docs.taro.zone/docs/next/babel-config
|
||||
module.exports = {
|
||||
presets: [
|
||||
['taro', {
|
||||
framework: 'react',
|
||||
ts: true,
|
||||
compiler: 'vite',
|
||||
useBuiltIns: process.env.TARO_ENV === 'h5' ? 'usage' : false
|
||||
}]
|
||||
]
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import type { UserConfigExport } from "@tarojs/cli"
|
||||
|
||||
export default {
|
||||
|
||||
mini: {},
|
||||
h5: {
|
||||
devServer: {
|
||||
fs: {
|
||||
allow: ['..']
|
||||
},
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'https://bowongai-test--text-video-agent-fastapi-app.modal.run',
|
||||
changeOrigin: true,
|
||||
secure: true,
|
||||
configure: (proxy, _options) => {
|
||||
proxy.on('error', (err, _req, _res) => {
|
||||
console.log('proxy error', err);
|
||||
});
|
||||
proxy.on('proxyReq', (proxyReq, req, _res) => {
|
||||
console.log('Sending Request to the Target:', req.method, req.url);
|
||||
});
|
||||
proxy.on('proxyRes', (proxyRes, req, _res) => {
|
||||
console.log('Received Response from the Target:', proxyRes.statusCode, req.url);
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} satisfies UserConfigExport<'vite'>
|
||||
141
config/index.ts
@@ -1,141 +0,0 @@
|
||||
import { defineConfig, type UserConfigExport } from '@tarojs/cli';
|
||||
import { UnifiedWebpackPluginV5 } from 'weapp-tailwindcss/webpack';
|
||||
|
||||
import devConfig from './dev';
|
||||
import prodConfig from './prod';
|
||||
|
||||
// https://taro-docs.jd.com/docs/next/config#defineconfig-辅助函数
|
||||
export default defineConfig<'vite'>(async merge => {
|
||||
// 不同平台的 appId 配置
|
||||
const appIds = {
|
||||
weapp: process.env.TARO_APP_ID_WEAPP || 'your-weapp-appid',
|
||||
tt: process.env.TARO_APP_ID_TT || 'your-tt-appid',
|
||||
alipay: process.env.TARO_APP_ID_ALIPAY || 'your-alipay-appid',
|
||||
swan: process.env.TARO_APP_ID_SWAN || 'your-swan-appid',
|
||||
qq: process.env.TARO_APP_ID_QQ || 'your-qq-appid',
|
||||
jd: process.env.TARO_APP_ID_JD || 'your-jd-appid',
|
||||
};
|
||||
|
||||
const baseConfig: UserConfigExport<'vite'> = {
|
||||
projectName: 'bw-mini-app',
|
||||
date: '2025-9-1',
|
||||
designWidth: 750,
|
||||
deviceRatio: {
|
||||
640: 2.34 / 2,
|
||||
750: 1,
|
||||
375: 2,
|
||||
828: 1.81 / 2,
|
||||
},
|
||||
sourceRoot: 'src',
|
||||
outputRoot: process.env.TARO_ENV ? `dist/${process.env.TARO_ENV}` : 'dist',
|
||||
plugins: ['@tarojs/plugin-generator'],
|
||||
defineConstants: {},
|
||||
copy: {
|
||||
patterns: [
|
||||
{
|
||||
from: 'src/assets/icons/',
|
||||
to: 'assets/icons/',
|
||||
},
|
||||
],
|
||||
options: {},
|
||||
},
|
||||
framework: 'react',
|
||||
compiler: 'vite',
|
||||
mini: {
|
||||
postcss: {
|
||||
pxtransform: {
|
||||
enable: true,
|
||||
config: {},
|
||||
},
|
||||
cssModules: {
|
||||
enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
|
||||
config: {
|
||||
namingPattern: 'module', // 转换模式,取值为 global/module
|
||||
generateScopedName: '[name]__[local]___[hash:base64:5]',
|
||||
},
|
||||
},
|
||||
},
|
||||
webpackChain(chain) {
|
||||
chain.merge({
|
||||
plugin: {
|
||||
install: {
|
||||
plugin: UnifiedWebpackPluginV5,
|
||||
args: [
|
||||
{
|
||||
appType: 'taro',
|
||||
// 下面个配置,会开启 rem -> rpx 的转化
|
||||
rem2rpx: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
// 小程序平台特定配置
|
||||
weapp: {
|
||||
outputRoot: 'dist/weapp',
|
||||
appId: appIds.weapp,
|
||||
},
|
||||
tt: {
|
||||
outputRoot: 'dist/tt',
|
||||
appId: appIds.tt,
|
||||
},
|
||||
alipay: {
|
||||
outputRoot: 'dist/alipay',
|
||||
appId: appIds.alipay,
|
||||
},
|
||||
swan: {
|
||||
outputRoot: 'dist/swan',
|
||||
appId: appIds.swan,
|
||||
},
|
||||
qq: {
|
||||
outputRoot: 'dist/qq',
|
||||
appId: appIds.qq,
|
||||
},
|
||||
jd: {
|
||||
outputRoot: 'dist/jd',
|
||||
appId: appIds.jd,
|
||||
},
|
||||
h5: {
|
||||
publicPath: '/',
|
||||
staticDirectory: 'static',
|
||||
|
||||
miniCssExtractPluginOption: {
|
||||
ignoreOrder: true,
|
||||
filename: 'css/[name].[hash].css',
|
||||
chunkFilename: 'css/[name].[chunkhash].css',
|
||||
},
|
||||
postcss: {
|
||||
autoprefixer: {
|
||||
enable: true,
|
||||
config: {},
|
||||
},
|
||||
cssModules: {
|
||||
enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
|
||||
config: {
|
||||
namingPattern: 'module', // 转换模式,取值为 global/module
|
||||
generateScopedName: '[name]__[local]___[hash:base64:5]',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
rn: {
|
||||
appName: 'taroDemo',
|
||||
postcss: {
|
||||
cssModules: {
|
||||
enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
process.env.BROWSERSLIST_ENV = process.env.NODE_ENV;
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
// 本地开发构建配置(不混淆压缩)
|
||||
return merge({}, baseConfig, devConfig);
|
||||
}
|
||||
// 生产构建配置(默认开启压缩混淆等)
|
||||
return merge({}, baseConfig, prodConfig);
|
||||
});
|
||||
@@ -1,9 +0,0 @@
|
||||
import type { UserConfigExport } from '@tarojs/cli';
|
||||
|
||||
export default {
|
||||
mini: {},
|
||||
h5: {
|
||||
// 确保产物为 es5
|
||||
legacy: true,
|
||||
},
|
||||
} satisfies UserConfigExport<'vite'>;
|
||||
13
index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/assets/icons/logo.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>图生视频</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
63
package.json
@@ -12,31 +12,15 @@
|
||||
"scripts": {
|
||||
"claude": "claude --dangerously-skip-permissions",
|
||||
"prepare": "husky",
|
||||
"postinstall": "npx weapp-tw patch",
|
||||
"new": "taro new",
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"build:check": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:coverage": "jest --coverage",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "eslint src --ext .ts,.tsx --fix",
|
||||
"build:weapp": "taro build --type weapp",
|
||||
"build:swan": "taro build --type swan",
|
||||
"build:alipay": "taro build --type alipay",
|
||||
"build:tt": "taro build --type tt",
|
||||
"build:h5": "taro build --type h5",
|
||||
"build:rn": "taro build --type rn",
|
||||
"build:qq": "taro build --type qq",
|
||||
"build:jd": "taro build --type jd",
|
||||
"build:harmony-hybrid": "taro build --type harmony-hybrid",
|
||||
"dev:weapp": "npm run build:weapp -- --watch",
|
||||
"dev:swan": "npm run build:swan -- --watch",
|
||||
"dev:alipay": "npm run build:alipay -- --watch",
|
||||
"dev:tt": "npm run build:tt -- --watch",
|
||||
"dev:h5": "npm run build:h5 -- --watch",
|
||||
"dev:rn": "npm run build:rn -- --watch",
|
||||
"dev:qq": "npm run build:qq -- --watch",
|
||||
"dev:jd": "npm run build:jd -- --watch",
|
||||
"dev:harmony-hybrid": "npm run build:harmony-hybrid -- --watch"
|
||||
"lint": "eslint src --ext .ts,.tsx --fix"
|
||||
},
|
||||
"browserslist": {
|
||||
"development": [
|
||||
@@ -51,52 +35,30 @@
|
||||
},
|
||||
"author": "",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.24.4",
|
||||
"@reduxjs/toolkit": "^2.9.0",
|
||||
"@stripe/stripe-js": "^7.9.0",
|
||||
"@tarojs/components": "4.1.6",
|
||||
"@tarojs/helper": "4.1.6",
|
||||
"@tarojs/plugin-framework-react": "4.1.6",
|
||||
"@tarojs/plugin-platform-alipay": "4.1.6",
|
||||
"@tarojs/plugin-platform-h5": "4.1.6",
|
||||
"@tarojs/plugin-platform-harmony-hybrid": "4.1.6",
|
||||
"@tarojs/plugin-platform-jd": "4.1.6",
|
||||
"@tarojs/plugin-platform-qq": "4.1.6",
|
||||
"@tarojs/plugin-platform-swan": "4.1.6",
|
||||
"@tarojs/plugin-platform-tt": "4.1.6",
|
||||
"@tarojs/plugin-platform-weapp": "4.1.6",
|
||||
"@tarojs/react": "4.1.6",
|
||||
"@tarojs/runtime": "4.1.6",
|
||||
"@tarojs/shared": "4.1.6",
|
||||
"@tarojs/taro": "4.1.6",
|
||||
"@types/node": "^24.5.2",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router-dom": "^6.26.0",
|
||||
"redux": "^5.0.1",
|
||||
"redux-thunk": "^3.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.24.4",
|
||||
"@babel/plugin-transform-class-properties": "7.25.9",
|
||||
"@babel/preset-react": "^7.24.1",
|
||||
"@commitlint/cli": "^19.8.1",
|
||||
"@commitlint/config-conventional": "^19.8.1",
|
||||
"@tarojs/cli": "4.1.6",
|
||||
"@tarojs/plugin-generator": "4.1.6",
|
||||
"@tarojs/test-utils-react": "^0.1.1",
|
||||
"@tarojs/vite-runner": "4.1.6",
|
||||
"@testing-library/jest-dom": "^6.8.0",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/minimatch": "^5",
|
||||
"@types/react": "^18.0.0",
|
||||
"@types/react-dom": "^18.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"@vitejs/plugin-react": "^4.3.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"babel-jest": "^30.1.2",
|
||||
"babel-preset-taro": "4.1.6",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-taro": "4.1.6",
|
||||
"eslint-plugin-react": "^7.34.1",
|
||||
"eslint-plugin-react-hooks": "^4.4.0",
|
||||
"husky": "^9.1.7",
|
||||
@@ -105,14 +67,11 @@
|
||||
"jest-environment-jsdom": "^30.1.2",
|
||||
"lint-staged": "^16.1.2",
|
||||
"postcss": "^8.4.38",
|
||||
"react-refresh": "^0.14.0",
|
||||
"stylelint": "^16.4.0",
|
||||
"stylelint-config-standard": "^38.0.0",
|
||||
"tailwindcss": "^4.1.12",
|
||||
"terser": "^5.30.4",
|
||||
"ts-jest": "^29.4.1",
|
||||
"typescript": "^5.4.5",
|
||||
"vite": "^4.2.0",
|
||||
"weapp-tailwindcss": "^4.2.6"
|
||||
"vite": "^5.4.0"
|
||||
}
|
||||
}
|
||||
|
||||
8092
pnpm-lock.yaml
generated
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"miniprogramRoot": "./dist/weapp",
|
||||
"projectname": "bw-mini-app",
|
||||
"description": "图生图 风格转换 ",
|
||||
"appid": "wxb51f0b0c3aad7cdf",
|
||||
"setting": {
|
||||
"urlCheck": true,
|
||||
"es6": true,
|
||||
"enhance": false,
|
||||
"compileHotReLoad": false,
|
||||
"postcss": false,
|
||||
"minified": false
|
||||
},
|
||||
"compileType": "miniprogram"
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"setting": {
|
||||
"urlCheck": true,
|
||||
"bigPackageSizeSupport": false,
|
||||
"compileHotReLoad": true
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"miniprogramRoot": "./dist/tt",
|
||||
"projectname": "bw-mini-app",
|
||||
"appid": "ttbfd9c96420ec8f8201"
|
||||
}
|
||||
BIN
public/assets/icons/activate/fuwuchengnuo.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
public/assets/icons/activate/fuwuwangdian.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
public/assets/icons/activate/fuwuzixun-weixuanzhong.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
public/assets/icons/activate/fuwuzixun.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
public/assets/icons/activate/guohushenqing.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
public/assets/icons/activate/qifeijiaona.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
public/assets/icons/activate/quanbuyewu-weixuanzhong.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
public/assets/icons/activate/quanbuyewu.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
public/assets/icons/activate/rexiandianhua.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
public/assets/icons/activate/shoufeibiaozhun.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
public/assets/icons/activate/shoulijindu-xuanzhong.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
public/assets/icons/activate/shoulijindu.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
public/assets/icons/activate/shoulijindu_1.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
public/assets/icons/activate/shouye-weixuanzhong.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
public/assets/icons/activate/shouye.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
public/assets/icons/activate/wode-weixuanzhong.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
public/assets/icons/activate/wode.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
public/assets/icons/activate/wodedizhi.png
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
public/assets/icons/activate/wodetongzhi.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
public/assets/icons/activate/yewuzhinan.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
public/assets/icons/activate/yijianfankui.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
public/assets/icons/activate/yonghubaozhuang.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
public/assets/icons/activate/yonghugaizhuang.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
public/assets/icons/activate/yuangongshibie.png
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
public/assets/icons/activate/yuyuedianhuo.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
public/assets/icons/activate/yuyuekaitong.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
public/assets/icons/activate/yuyueweixiu.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
public/assets/icons/activate/yuyuezhongxin.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
public/assets/icons/activate/zhangdanchaxun.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
public/assets/icons/cloud.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
public/assets/icons/default/fuwuchengnuo.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
public/assets/icons/default/fuwuwangdian.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
public/assets/icons/default/fuwuzixun-weixuanzhong.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
public/assets/icons/default/fuwuzixun.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
public/assets/icons/default/guohushenqing.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
public/assets/icons/default/qifeijiaona.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
public/assets/icons/default/quanbuyewu-weixuanzhong.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
public/assets/icons/default/quanbuyewu.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
public/assets/icons/default/rexiandianhua.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
public/assets/icons/default/shoufeibiaozhun.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
public/assets/icons/default/shoulijindu-xuanzhong.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
public/assets/icons/default/shoulijindu.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
public/assets/icons/default/shoulijindu_1.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
public/assets/icons/default/wode-weixuanzhong.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
public/assets/icons/default/wode.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
public/assets/icons/default/wodedizhi.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
public/assets/icons/default/wodetongzhi.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
public/assets/icons/default/yewuzhinan.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
public/assets/icons/default/yijianfankui.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
public/assets/icons/default/yonghubaozhuang.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
public/assets/icons/default/yonghugaizhuang.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
public/assets/icons/default/yuangongshibie.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
public/assets/icons/default/yuyuedianhuo.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
public/assets/icons/default/yuyuekaitong.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
public/assets/icons/default/yuyueweixiu.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
public/assets/icons/default/yuyuezhongxin.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
public/assets/icons/default/zhangdanchaxun.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
public/assets/icons/funnel.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
public/assets/icons/logo.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
public/assets/icons/photo.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
public/assets/icons/playground-selected.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
public/assets/icons/playground.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
public/assets/icons/user-selected.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
public/assets/icons/user.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
public/assets/images/Figure.jpg
Normal file
|
After Width: | Height: | Size: 189 KiB |
1
public/assets/templates/3d-animation.png
Normal file
@@ -0,0 +1 @@
|
||||
# 3D动画缩略图 - 需要替换为实际图片
|
||||
1
public/assets/templates/animation.png
Normal file
@@ -0,0 +1 @@
|
||||
# 动画效果缩略图 - 需要替换为实际图片
|
||||
1
public/assets/templates/art-style.png
Normal file
@@ -0,0 +1 @@
|
||||
# 艺术风格缩略图 - 需要替换为实际图片
|
||||
1
public/assets/templates/cartoon.png
Normal file
@@ -0,0 +1 @@
|
||||
# 卡通风格缩略图 - 需要替换为实际图片
|
||||
1
public/assets/templates/sketch.png
Normal file
@@ -0,0 +1 @@
|
||||
# 素描风格缩略图 - 需要替换为实际图片
|
||||
1
public/assets/templates/video-gen.png
Normal file
@@ -0,0 +1 @@
|
||||
# 视频生成缩略图 - 需要替换为实际图片
|
||||
1
public/assets/templates/vintage.png
Normal file
@@ -0,0 +1 @@
|
||||
# 复古滤镜缩略图 - 需要替换为实际图片
|
||||
1
public/assets/templates/watercolor.png
Normal file
@@ -0,0 +1 @@
|
||||
# 水彩画缩略图 - 需要替换为实际图片
|
||||
@@ -1,4 +1,4 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
// 移除Taro依赖,使用Web API
|
||||
import { Dispatch } from 'redux'
|
||||
import { HistoryRecord } from '../store/types'
|
||||
import * as types from '../constants/history'
|
||||
@@ -84,7 +84,7 @@ export const loadRecords = () => {
|
||||
return async (dispatch: Dispatch) => {
|
||||
dispatch(loadRecordsRequest())
|
||||
try {
|
||||
const savedRecords = Taro.getStorageSync(STORAGE_KEY) || []
|
||||
const savedRecords = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]')
|
||||
dispatch(loadRecordsSuccess(savedRecords))
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : '加载历史记录失败'
|
||||
@@ -100,7 +100,7 @@ export const addRecord = (recordData: Omit<HistoryRecord, 'id' | 'createTime' |
|
||||
try {
|
||||
const id = `record_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
||||
const now = new Date().toISOString()
|
||||
|
||||
|
||||
const newRecord: HistoryRecord = {
|
||||
...recordData,
|
||||
id,
|
||||
@@ -110,12 +110,9 @@ export const addRecord = (recordData: Omit<HistoryRecord, 'id' | 'createTime' |
|
||||
|
||||
const currentRecords = getState().history.records
|
||||
const updatedRecords = [newRecord, ...currentRecords]
|
||||
|
||||
await Taro.setStorage({
|
||||
key: STORAGE_KEY,
|
||||
data: updatedRecords
|
||||
})
|
||||
|
||||
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(updatedRecords))
|
||||
|
||||
dispatch(addRecordSuccess(newRecord))
|
||||
return id
|
||||
} catch (error) {
|
||||
@@ -133,7 +130,7 @@ export const updateRecord = (id: string, updates: Partial<HistoryRecord>) => {
|
||||
try {
|
||||
const currentRecords = getState().history.records
|
||||
const recordIndex = currentRecords.findIndex((record: HistoryRecord) => record.id === id)
|
||||
|
||||
|
||||
if (recordIndex === -1) {
|
||||
throw new Error('记录不存在')
|
||||
}
|
||||
@@ -147,10 +144,7 @@ export const updateRecord = (id: string, updates: Partial<HistoryRecord>) => {
|
||||
const updatedRecords = [...currentRecords]
|
||||
updatedRecords[recordIndex] = updatedRecord
|
||||
|
||||
await Taro.setStorage({
|
||||
key: STORAGE_KEY,
|
||||
data: updatedRecords
|
||||
})
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(updatedRecords))
|
||||
|
||||
dispatch(updateRecordSuccess(updatedRecord))
|
||||
} catch (error) {
|
||||
@@ -169,10 +163,7 @@ export const deleteRecord = (id: string) => {
|
||||
const currentRecords = getState().history.records
|
||||
const updatedRecords = currentRecords.filter((record: HistoryRecord) => record.id !== id)
|
||||
|
||||
await Taro.setStorage({
|
||||
key: STORAGE_KEY,
|
||||
data: updatedRecords
|
||||
})
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(updatedRecords))
|
||||
|
||||
dispatch(deleteRecordSuccess(id))
|
||||
} catch (error) {
|
||||
@@ -188,7 +179,7 @@ export const clearRecords = () => {
|
||||
return async (dispatch: Dispatch) => {
|
||||
dispatch(clearRecordsRequest())
|
||||
try {
|
||||
await Taro.removeStorage({ key: STORAGE_KEY })
|
||||
localStorage.removeItem(STORAGE_KEY)
|
||||
dispatch(clearRecordsSuccess())
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : '清空记录失败'
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
// 注意:由于小程序的限制,tabBar文本和页面标题需要在运行时动态设置
|
||||
// 这里保留中文作为默认值,实际的国际化会在运行时处理
|
||||
export default defineAppConfig({
|
||||
pages: [
|
||||
'pages/home/index', // 新的模板卡片首页
|
||||
'pages/history/index', // 历史记录页面
|
||||
'pages/friends-photo/index', // 好友合照页面
|
||||
'pages/result/index',
|
||||
],
|
||||
tabBar: {
|
||||
color: '#8E9BAE',
|
||||
selectedColor: '#1D1F22',
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderStyle: 'black',
|
||||
list: [
|
||||
{
|
||||
pagePath: 'pages/home/index',
|
||||
text: '游乐场', // 运行时会被替换为对应语言
|
||||
iconPath: './assets/icons/playground.png',
|
||||
selectedIconPath: './assets/icons/playground-selected.png',
|
||||
},
|
||||
{
|
||||
pagePath: 'pages/history/index',
|
||||
text: '我的', // 运行时会被替换为对应语言
|
||||
iconPath: './assets/icons/user.png',
|
||||
selectedIconPath: './assets/icons/user-selected.png',
|
||||
},
|
||||
],
|
||||
},
|
||||
window: {
|
||||
backgroundTextStyle: 'light',
|
||||
navigationBarBackgroundColor: '#fff',
|
||||
navigationBarTitleText: '图生视频', // 运行时会被替换为对应语言
|
||||
navigationBarTextStyle: 'black',
|
||||
},
|
||||
permission: {
|
||||
'scope.album': {
|
||||
desc: '用于保存生成的图片和视频到相册', // 运行时会被替换为对应语言
|
||||
},
|
||||
},
|
||||
});
|
||||
70
src/app.css
@@ -1,4 +1,66 @@
|
||||
#app{
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
/* 全局样式 */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
|
||||
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue,
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
#root {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.app {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.app-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
padding-bottom: 60px; /* 为底部导航留出空间 */
|
||||
}
|
||||
|
||||
/* 加载状态样式 */
|
||||
.app-loading {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid #e5e5e5;
|
||||
border-top: 4px solid #000;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 1rem;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
127
src/app.tsx
@@ -1,48 +1,97 @@
|
||||
import { PropsWithChildren } from 'react'
|
||||
import { useLaunch } from '@tarojs/taro'
|
||||
import { Provider } from 'react-redux'
|
||||
import configStore from './store'
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Routes, Route, Navigate, useLocation } from 'react-router-dom';
|
||||
import { authService } from './services';
|
||||
import { initLanguageFromStorage } from './i18n/utils';
|
||||
import { i18nManager } from './i18n/manager';
|
||||
|
||||
import './app.css'
|
||||
import { createPlatformFactory } from './platforms'
|
||||
import { initLanguageFromStorage } from './i18n/utils'
|
||||
import { i18nManager } from './i18n/manager'
|
||||
// Page components
|
||||
import Home from './pages/home';
|
||||
import History from './pages/history';
|
||||
import FriendsPhoto from './pages/friends-photo';
|
||||
import Result from './pages/result';
|
||||
|
||||
const store = configStore()
|
||||
// Bottom navigation component
|
||||
import BottomNavigation from './components/BottomNavigation';
|
||||
|
||||
function App({ children }: PropsWithChildren<any>) {
|
||||
useLaunch(async () => {
|
||||
// 初始化国际化
|
||||
try {
|
||||
await initLanguageFromStorage()
|
||||
await i18nManager.initializeApp()
|
||||
} catch (error) {
|
||||
console.error('i18n初始化失败:', error)
|
||||
}
|
||||
import './app.css';
|
||||
|
||||
const authorize = createPlatformFactory().createAuthorize()
|
||||
// const payment = createPlatformFactory().createPayment()
|
||||
try {
|
||||
// 检查登录状态,包括OAuth 2.0回调处理
|
||||
const isLoggedIn = await authorize.checkLogin()
|
||||
if (!isLoggedIn) {
|
||||
// 可以根据需要决定是否自动跳转登录
|
||||
await authorize.login()
|
||||
function App() {
|
||||
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const location = useLocation();
|
||||
|
||||
// Pages that don't need to show bottom navigation
|
||||
const hideBottomNavPages = ['/friends-photo', '/result'];
|
||||
|
||||
useEffect(() => {
|
||||
const initApp = async () => {
|
||||
try {
|
||||
// Initialize internationalization
|
||||
await initLanguageFromStorage();
|
||||
await i18nManager.initializeApp();
|
||||
|
||||
// Check login status, including OAuth 2.0 callback handling
|
||||
const isLoggedIn = await authService.checkLogin();
|
||||
setIsAuthenticated(isLoggedIn);
|
||||
|
||||
if (!isLoggedIn) {
|
||||
console.log('User not logged in, preparing to redirect to login page');
|
||||
// Delay a bit before redirecting to let user see loading state
|
||||
setTimeout(() => {
|
||||
authService.login();
|
||||
}, 1000);
|
||||
} else {
|
||||
console.log('User already logged in');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Application initialization failed:', error);
|
||||
setIsAuthenticated(false);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
// const result = await payment.pay(`character_figurine_v1`, ``)
|
||||
// console.log({ result })
|
||||
} catch (error) {
|
||||
console.error('登录检查失败:', error)
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
initApp();
|
||||
}, []);
|
||||
|
||||
// Show loading state
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="app-loading">
|
||||
<div className="loading-container">
|
||||
<div className="loading-spinner"></div>
|
||||
<div className="loading-text">Initializing application...</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// If not authenticated, show login prompt
|
||||
if (isAuthenticated === false) {
|
||||
return (
|
||||
<div className="app-loading">
|
||||
<div className="loading-container">
|
||||
<div className="loading-spinner"></div>
|
||||
<div className="loading-text">Redirecting to login page...</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Provider store={store}>
|
||||
{children}
|
||||
</Provider>
|
||||
)
|
||||
<div className="app">
|
||||
<div className="app-content">
|
||||
<Routes>
|
||||
<Route path="/" element={<Navigate to="/home" replace />} />
|
||||
<Route path="/home" element={<Home />} />
|
||||
<Route path="/history" element={<History />} />
|
||||
<Route path="/friends-photo" element={<FriendsPhoto />} />
|
||||
<Route path="/result" element={<Result />} />
|
||||
</Routes>
|
||||
</div>
|
||||
{!hideBottomNavPages.includes(location.pathname) && <BottomNavigation />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default App
|
||||
export default App;
|
||||
|
||||
46
src/components/BottomNavigation/index.css
Normal file
@@ -0,0 +1,46 @@
|
||||
.bottom-navigation {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 60px;
|
||||
background-color: #fff;
|
||||
border-top: 1px solid #e5e5e5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
z-index: 1000;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.tab-item:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.tab-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.tab-text {
|
||||
font-size: 12px;
|
||||
color: #8E9BAE;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.tab-item.active .tab-text {
|
||||
color: #1D1F22;
|
||||
font-weight: 500;
|
||||
}
|
||||
53
src/components/BottomNavigation/index.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import React from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useI18n } from '../../hooks/useI18n';
|
||||
import './index.css';
|
||||
|
||||
const BottomNavigation: React.FC = () => {
|
||||
const { t } = useI18n();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
path: '/home',
|
||||
text: t('navigation.playground'),
|
||||
icon: '/assets/icons/playground.png',
|
||||
selectedIcon: '/assets/icons/playground-selected.png',
|
||||
},
|
||||
{
|
||||
path: '/history',
|
||||
text: t('navigation.mine'),
|
||||
icon: '/assets/icons/user.png',
|
||||
selectedIcon: '/assets/icons/user-selected.png',
|
||||
},
|
||||
];
|
||||
|
||||
const handleTabClick = (path: string) => {
|
||||
navigate(path);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bottom-navigation">
|
||||
{tabs.map((tab) => {
|
||||
const isActive = location.pathname === tab.path;
|
||||
return (
|
||||
<div
|
||||
key={tab.path}
|
||||
className={`tab-item ${isActive ? 'active' : ''}`}
|
||||
onClick={() => handleTabClick(tab.path)}
|
||||
>
|
||||
<img
|
||||
className="tab-icon"
|
||||
src={isActive ? tab.selectedIcon : tab.icon}
|
||||
alt={tab.text}
|
||||
/>
|
||||
<span className="tab-text">{tab.text}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BottomNavigation;
|
||||
@@ -2,9 +2,7 @@
|
||||
* 语言切换组件
|
||||
*/
|
||||
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import { useState } from 'react';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { useI18n } from '../../hooks/useI18n';
|
||||
import { Language } from '../../i18n';
|
||||
import { i18nManager } from '../../i18n/manager';
|
||||
|
||||
@@ -1,25 +1,21 @@
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import { useI18n } from '../../hooks/useI18n';
|
||||
import './index.css';
|
||||
|
||||
interface LoadingOverlayProps {
|
||||
children?: React.ReactNode;
|
||||
visible: boolean;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
const LoadingOverlay: React.FC<LoadingOverlayProps> = ({ children }) => {
|
||||
const { t } = useI18n();
|
||||
const LoadingOverlay: React.FC<LoadingOverlayProps> = ({
|
||||
visible,
|
||||
text = '加载中...'
|
||||
}) => {
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<View className="loading-overlay">
|
||||
<View className="loading-container">
|
||||
<View className="loading-spinner" />
|
||||
<View className="loading-text">
|
||||
<Text className="loading-title">{t('loading.aiGenerating')}</Text>
|
||||
<Text className="loading-desc">{t('loading.pleaseWaitDesc')}</Text>
|
||||
</View>
|
||||
{children}
|
||||
</View>
|
||||
</View>
|
||||
<div className="loading-overlay">
|
||||
<div className="loading-content">
|
||||
<div className="loading-spinner"></div>
|
||||
<div className="loading-text">{text}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -24,17 +24,17 @@
|
||||
position: relative;
|
||||
padding: 0;
|
||||
flex: 1;
|
||||
min-height: 480px;
|
||||
border-radius: 32px;
|
||||
min-height: 280px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.merged-image-container {
|
||||
position: relative;
|
||||
border-radius: 32px;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 480px;
|
||||
min-height: 280px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 480px;
|
||||
min-height: 280px;
|
||||
}
|
||||
|
||||
.overlay-layer {
|
||||
@@ -56,7 +56,7 @@
|
||||
.full-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 480px;
|
||||
min-height: 280px;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
transform: translateZ(0);
|
||||
@@ -139,7 +139,7 @@
|
||||
background: #0000004d;
|
||||
backdrop-filter: blur(5px);
|
||||
color: #fff;
|
||||
font-size: 24px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
border-radius: 100px;
|
||||
border: 1px solid #ffffff4d;
|
||||
@@ -147,7 +147,7 @@
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding: 12px 16px;
|
||||
padding: 8px 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -157,18 +157,21 @@
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
min-height: 480px;
|
||||
min-height: 280px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.single-video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 480px;
|
||||
min-height: 280px;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
display: block;
|
||||
border-radius: 16px;
|
||||
position: relative;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
transform: translateZ(0);
|
||||
backface-visibility: hidden;
|
||||
@@ -183,6 +186,7 @@
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
@@ -190,16 +194,20 @@
|
||||
.full-video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 480px;
|
||||
min-height: 280px;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
transform: translateZ(0);
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.watermark-text {
|
||||
color: rgb(255 255 255 / 50%);
|
||||
font-size: 20px;
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { View, Text, Image, Video } from '@tarojs/components';
|
||||
import { useState, useRef, useMemo } from 'react';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { Template } from '../../store/types';
|
||||
import { useI18n } from '../../hooks/useI18n';
|
||||
import './index.css';
|
||||
@@ -37,16 +35,12 @@ export default function TemplateCard({ template, onClick }: TemplateCardProps) {
|
||||
// 获取容器信息
|
||||
const getContainerInfo = () => {
|
||||
return new Promise(resolve => {
|
||||
const query = Taro.createSelectorQuery();
|
||||
query
|
||||
.select(`#${containerId}`)
|
||||
.boundingClientRect(rect => {
|
||||
if (rect) {
|
||||
setContainerInfo(rect);
|
||||
resolve(rect);
|
||||
}
|
||||
})
|
||||
.exec();
|
||||
const element = document.getElementById(containerId);
|
||||
if (element) {
|
||||
const rect = element.getBoundingClientRect();
|
||||
setContainerInfo(rect);
|
||||
resolve(rect);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -83,89 +77,69 @@ export default function TemplateCard({ template, onClick }: TemplateCardProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<View className="template-card" onClick={handleClick}>
|
||||
<div className="template-card" onClick={handleClick}>
|
||||
{/* 根据output类型显示不同的内容 */}
|
||||
{isOutputVideo ? (
|
||||
// 当output是视频时,只显示单个视频
|
||||
<View className="single-video-container">
|
||||
<Image className="video-poster" src={template.inputExampleUrl || ``} mode="aspectFill" />
|
||||
<Video
|
||||
className="single-video"
|
||||
src={template.outputExampleUrl || ``}
|
||||
autoplay
|
||||
muted
|
||||
loop
|
||||
objectFit="cover"
|
||||
showPlayBtn={false}
|
||||
showCenterPlayBtn={false}
|
||||
showFullscreenBtn={false}
|
||||
controls={false}
|
||||
/>
|
||||
<div className="single-video-container">
|
||||
<img className="video-poster" src={template.inputExampleUrl || ``} alt="poster" />
|
||||
<video className="single-video" src={template.outputExampleUrl || ``} autoPlay muted loop style={{ objectFit: 'cover' }} controls={false} />
|
||||
{/* 模板名称悬浮 - 视频底部 */}
|
||||
<View className="name-overlay">
|
||||
<Text className="name-badge">✨{template.name}</Text>
|
||||
</View>
|
||||
<View className="watermark">
|
||||
<Text className="watermark-text">{t('templates.aiGeneratedContent')}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<div className="name-overlay">
|
||||
<span className="name-badge">✨{template.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
// 原有的图片对比逻辑
|
||||
<View className="image-comparison">
|
||||
<View id={containerId} className="merged-image-container" ref={containerRef} onTouchMove={handleTouchMove} onTouchEnd={handleTouchEnd}>
|
||||
<div className="image-comparison">
|
||||
<div id={containerId} className="merged-image-container" ref={containerRef} onTouchMove={handleTouchMove} onTouchEnd={handleTouchEnd}>
|
||||
{/* 原图层 - 完整图片/视频 */}
|
||||
<View className="image-layer">
|
||||
<div className="image-layer">
|
||||
{isInputVideo ? (
|
||||
<Video
|
||||
<video
|
||||
className="full-video"
|
||||
src={template.inputExampleUrl || ``}
|
||||
autoplay
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
objectFit="cover"
|
||||
showPlayBtn={false}
|
||||
showCenterPlayBtn={false}
|
||||
showFullscreenBtn={false}
|
||||
style={{ objectFit: 'cover' }}
|
||||
controls={false}
|
||||
/>
|
||||
) : (
|
||||
<Image className="full-image" src={template.inputExampleUrl || ``} mode="aspectFill" lazyLoad />
|
||||
<img className="full-image" src={template.inputExampleUrl || ``} alt="input" loading="lazy" />
|
||||
)}
|
||||
</View>
|
||||
</div>
|
||||
|
||||
{/* 效果图层 - 完整图片,通过遮罩显示右半部分 */}
|
||||
<View
|
||||
<div
|
||||
className="image-layer overlay-layer"
|
||||
style={{
|
||||
clipPath: `polygon(${splitPosition}% 0%, 100% 0%, 100% 100%, ${splitPosition}% 100%)`,
|
||||
}}
|
||||
>
|
||||
<Image className="full-image" src={template.outputExampleUrl || ``} mode="aspectFill" lazyLoad />
|
||||
</View>
|
||||
<img className="full-image" src={template.outputExampleUrl || ``} alt="output" loading="lazy" />
|
||||
</div>
|
||||
|
||||
{/* 可拖拽的分割线 */}
|
||||
<View
|
||||
<div
|
||||
className="split-line"
|
||||
style={{ left: `${splitPosition}%` }}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchMove={handleTouchMove}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
>
|
||||
<View className="split-handle">
|
||||
<Text className="split-icon">⟷</Text>
|
||||
</View>
|
||||
</View>
|
||||
<div className="split-handle">
|
||||
<span className="split-icon">⟷</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 模板名称悬浮 - 图片底部 */}
|
||||
<View className="name-overlay">
|
||||
<Text className="name-badge">✨{template.name}</Text>
|
||||
</View>
|
||||
<View className="watermark">
|
||||
<Text className="watermark-text">{t('templates.aiGeneratedContent')}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<div className="name-overlay">
|
||||
<span className="name-badge">✨{template.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</View>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
85
src/components/UserInfo/index.css
Normal file
@@ -0,0 +1,85 @@
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.user-info-loading {
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
margin-right: 1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.user-avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.avatar-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #007aff;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.user-details {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.user-email {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.logout-button {
|
||||
background: #ff3b30;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.logout-button:hover {
|
||||
background: #d70015;
|
||||
}
|
||||
|
||||
.logout-button:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
67
src/components/UserInfo/index.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { authService } from '../../services';
|
||||
import './index.css';
|
||||
|
||||
interface UserInfoProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function UserInfo({ className }: UserInfoProps) {
|
||||
const [userInfo, setUserInfo] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const loadUserInfo = async () => {
|
||||
try {
|
||||
const info = await authService.getUserInfo();
|
||||
setUserInfo(info);
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadUserInfo();
|
||||
}, []);
|
||||
|
||||
const handleLogout = async () => {
|
||||
if (confirm('确定要退出登录吗?')) {
|
||||
await authService.logout();
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className={`user-info ${className || ''}`}>
|
||||
<div className="user-info-loading">加载中...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!userInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`user-info ${className || ''}`}>
|
||||
<div className="user-avatar">
|
||||
{userInfo.picture ? (
|
||||
<img src={userInfo.picture} alt="用户头像" />
|
||||
) : (
|
||||
<div className="avatar-placeholder">
|
||||
{userInfo.name?.charAt(0) || 'U'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="user-details">
|
||||
<div className="user-name">{userInfo.name || '未知用户'}</div>
|
||||
<div className="user-email">{userInfo.email || ''}</div>
|
||||
</div>
|
||||
<button className="logout-button" onClick={handleLogout}>
|
||||
退出
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,5 +2,6 @@
|
||||
|
||||
export { useAd } from './useAd'
|
||||
export { useSdk, useServerSdk } from './useSdk'
|
||||
export { useUploadVideo } from './useUploadVideo'
|
||||
// 暂时注释掉有Taro依赖的hook
|
||||
// export { useUploadVideo } from './useUploadVideo'
|
||||
export { useI18n } from './useI18n'
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import { createPlatformFactory, RewardedVideoAd, RewardedVideoCloseCb, RewardedVideoErrorCb } from "../platforms";
|
||||
import { useState, useCallback } from 'react';
|
||||
|
||||
// 广告奖励回调函数类型
|
||||
type AdRewardCallback = () => void;
|
||||
@@ -17,187 +16,29 @@ interface UseAdOptions {
|
||||
onReward?: AdRewardCallback; // 观看完整广告后的奖励回调
|
||||
onClose?: (isEnded: boolean) => void; // 广告关闭回调,传入是否完整观看
|
||||
}
|
||||
// 创建平台广告实例
|
||||
|
||||
// H5版本的广告Hook - 不需要广告功能,直接给予奖励
|
||||
export function useAd(options?: UseAdOptions): UseAdReturn {
|
||||
const factory = createPlatformFactory()
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [adAvailable, setAdAvailable] = useState(false);
|
||||
const [adLoaded, setAdLoaded] = useState(false); // 广告是否已加载完成
|
||||
const adRef = useRef<RewardedVideoAd | null>(null);
|
||||
const platform = factory.getPlatform()
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [adAvailable] = useState(false); // H5版本不支持广告
|
||||
|
||||
// 检查当前平台是否支持广告
|
||||
const isAdSupportedPlatform = platform === 'bytedance';
|
||||
|
||||
useEffect(() => {
|
||||
// 如果当前平台不支持广告,直接设置为不可用状态
|
||||
if (!isAdSupportedPlatform) {
|
||||
console.log(`当前平台 ${platform} 未开通广告,跳过广告初始化`);
|
||||
setAdAvailable(false);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
adRef.current = factory.createRewardedVideoAd();
|
||||
const ad = adRef.current!;
|
||||
|
||||
// 检查广告是否可用
|
||||
if (ad.canUse && !ad.canUse()) {
|
||||
console.warn('广告功能不可用,可能是流量主未开通');
|
||||
setAdAvailable(false);
|
||||
setLoading(false);
|
||||
return () => { }; // 返回空的清理函数
|
||||
}
|
||||
|
||||
setAdAvailable(true);
|
||||
|
||||
// 广告关闭回调处理
|
||||
const onClose: RewardedVideoCloseCb = (res) => {
|
||||
console.log('广告关闭:', res);
|
||||
setLoading(false);
|
||||
|
||||
// 广告播放后需要重新加载
|
||||
setAdLoaded(false);
|
||||
if (adRef.current) {
|
||||
console.log('广告播放结束,重新加载下一个广告');
|
||||
adRef.current.load();
|
||||
}
|
||||
|
||||
// 判断用户是否完成播放
|
||||
const isEnded = Boolean(res?.isEnded);
|
||||
|
||||
if (isEnded) {
|
||||
// 播放完成的业务逻辑
|
||||
console.log('用户观看完整广告,给予奖励');
|
||||
|
||||
// 执行奖励回调
|
||||
options?.onReward?.();
|
||||
|
||||
// 可以在这里处理以下业务:
|
||||
// 1. 发放奖励(积分、道具等)
|
||||
// 2. 解锁功能或内容
|
||||
// 3. 统计完成观看数据
|
||||
// 4. 触发下一步操作
|
||||
|
||||
} else {
|
||||
// 未完成播放的处理
|
||||
console.log('用户未完整观看广告');
|
||||
|
||||
// 可以处理以下场景:
|
||||
// 1. 提示用户观看完整广告才能获得奖励
|
||||
// 2. 记录未完成播放的统计
|
||||
// 3. 可能的重试提示
|
||||
}
|
||||
|
||||
// 执行关闭回调,传入是否完整观看
|
||||
options?.onClose?.(isEnded);
|
||||
}
|
||||
// 广告加载错误回调处理
|
||||
const onError: RewardedVideoErrorCb = (res) => {
|
||||
console.error('广告错误:', res);
|
||||
setLoading(false);
|
||||
setAdLoaded(false); // 加载失败,重置加载状态
|
||||
|
||||
// 根据错误类型判断广告是否不可用
|
||||
const errorCode = res?.errCode;
|
||||
if (errorCode && ['1004', '1008', '1009', 1004, 1008, 1009].includes(errorCode)) {
|
||||
console.warn('广告服务不可用,可能是流量主未开通或被暂停');
|
||||
setAdAvailable(false);
|
||||
}
|
||||
}
|
||||
|
||||
// 广告加载成功回调处理
|
||||
const onLoad = () => {
|
||||
console.log('广告加载成功');
|
||||
setLoading(false);
|
||||
setAdLoaded(true); // 标记广告已加载完成
|
||||
}
|
||||
// 绑定广告事件监听
|
||||
ad.onLoad(onLoad);
|
||||
ad.onClose(onClose);
|
||||
ad.onError(onError);
|
||||
|
||||
// 立即预加载广告
|
||||
console.log('开始预加载广告');
|
||||
ad.load();
|
||||
|
||||
// 清理函数:组件卸载时移除事件监听和销毁广告实例
|
||||
return () => {
|
||||
if (adRef.current) {
|
||||
adRef.current.offClose(onClose)
|
||||
adRef.current.offError(onError)
|
||||
adRef.current.offLoad(onLoad)
|
||||
adRef.current.destroy();
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('初始化广告失败:', error);
|
||||
setAdAvailable(false);
|
||||
setLoading(false);
|
||||
return () => { }; // 返回空的清理函数
|
||||
}
|
||||
}, [options, isAdSupportedPlatform, platform]);
|
||||
|
||||
// 显示广告方法
|
||||
// 显示广告方法 - H5版本直接给予奖励
|
||||
const showAd = useCallback(() => {
|
||||
// 如果当前平台不支持广告,直接给予奖励并触发关闭回调
|
||||
if (!isAdSupportedPlatform) {
|
||||
console.log(`当前平台 ${platform} 未开通广告,直接给予奖励`);
|
||||
console.log('H5版本不支持广告,直接给予奖励');
|
||||
setLoading(true);
|
||||
|
||||
// 模拟广告播放时间
|
||||
setTimeout(() => {
|
||||
setLoading(false);
|
||||
options?.onReward?.();
|
||||
options?.onClose?.(true); // 模拟广告播放完成
|
||||
return;
|
||||
}
|
||||
}, 1000);
|
||||
}, [options]);
|
||||
|
||||
if (!adAvailable) {
|
||||
console.warn('广告不可用,跳过广告播放');
|
||||
// 如果广告不可用,直接给予奖励
|
||||
options?.onReward?.();
|
||||
options?.onClose?.(true); // 模拟广告播放完成
|
||||
return;
|
||||
}
|
||||
|
||||
if (!adLoaded) {
|
||||
console.warn('广告尚未加载完成,请稍等');
|
||||
// 广告还在加载中,先尝试重新加载
|
||||
if (adRef.current) {
|
||||
console.log('重新加载广告');
|
||||
adRef.current.load();
|
||||
setLoading(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (adRef.current) {
|
||||
setLoading(true);
|
||||
try {
|
||||
adRef.current.show();
|
||||
} catch (error) {
|
||||
console.error('显示广告失败:', error);
|
||||
setLoading(false);
|
||||
// 如果显示失败,也给予奖励作为降级处理
|
||||
options?.onReward?.();
|
||||
options?.onClose?.(true); // 模拟广告播放完成
|
||||
}
|
||||
}
|
||||
}, [adAvailable, adLoaded, options, isAdSupportedPlatform, platform]);
|
||||
|
||||
// 加载广告方法
|
||||
// 加载广告方法 - H5版本无需实际加载
|
||||
const loadAd = useCallback(() => {
|
||||
// 如果当前平台不支持广告,直接跳过加载
|
||||
if (!isAdSupportedPlatform) {
|
||||
console.log(`当前平台 ${platform} 未开通广告,跳过广告加载`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (adRef.current) {
|
||||
setLoading(true);
|
||||
setAdLoaded(false); // 重置加载状态
|
||||
console.log('手动加载广告');
|
||||
adRef.current.load();
|
||||
}
|
||||
}, [isAdSupportedPlatform, platform]);
|
||||
console.log('H5版本不需要加载广告');
|
||||
}, []);
|
||||
|
||||
return {
|
||||
showAd,
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { SdkServer } from "../sdk/sdk-server";
|
||||
import { bowongAI } from "../sdk/bowongAISDK";
|
||||
|
||||
|
||||
export function useSdk() {
|
||||
return bowongAI;
|
||||
// H5版本暂时返回空对象,广告相关功能不需要
|
||||
return {};
|
||||
}
|
||||
|
||||
export function useServerSdk(){
|
||||
return new SdkServer()
|
||||
}
|
||||
return new SdkServer();
|
||||
}
|
||||
|
||||
@@ -97,9 +97,6 @@ export interface LanguageResources {
|
||||
generating: string;
|
||||
startGenerating: string;
|
||||
};
|
||||
templates: {
|
||||
aiGeneratedContent: string;
|
||||
};
|
||||
loading: {
|
||||
aiGenerating: string;
|
||||
pleaseWaitDesc: string;
|
||||
|
||||