Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c5b910f61 | ||
|
|
9d890478d8 | ||
|
|
bc33c158bb | ||
|
|
33b08433b5 | ||
|
|
a869ea0827 |
@@ -3,5 +3,11 @@ import type { UserConfigExport } from "@tarojs/cli"
|
|||||||
export default {
|
export default {
|
||||||
|
|
||||||
mini: {},
|
mini: {},
|
||||||
h5: {}
|
h5: {
|
||||||
|
devServer: {
|
||||||
|
fs: {
|
||||||
|
allow: ['..']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} satisfies UserConfigExport<'vite'>
|
} satisfies UserConfigExport<'vite'>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { defineConfig, type UserConfigExport } from '@tarojs/cli'
|
import { defineConfig, type UserConfigExport } from '@tarojs/cli';
|
||||||
import { UnifiedWebpackPluginV5 } from 'weapp-tailwindcss/webpack'
|
import { UnifiedWebpackPluginV5 } from 'weapp-tailwindcss/webpack';
|
||||||
|
|
||||||
import devConfig from './dev'
|
import devConfig from './dev';
|
||||||
import prodConfig from './prod'
|
import prodConfig from './prod';
|
||||||
|
|
||||||
// https://taro-docs.jd.com/docs/next/config#defineconfig-辅助函数
|
// https://taro-docs.jd.com/docs/next/config#defineconfig-辅助函数
|
||||||
export default defineConfig<'vite'>(async (merge) => {
|
export default defineConfig<'vite'>(async merge => {
|
||||||
// 不同平台的 appId 配置
|
// 不同平台的 appId 配置
|
||||||
const appIds = {
|
const appIds = {
|
||||||
weapp: process.env.TARO_APP_ID_WEAPP || 'your-weapp-appid',
|
weapp: process.env.TARO_APP_ID_WEAPP || 'your-weapp-appid',
|
||||||
@@ -13,8 +13,8 @@ export default defineConfig<'vite'>(async (merge) => {
|
|||||||
alipay: process.env.TARO_APP_ID_ALIPAY || 'your-alipay-appid',
|
alipay: process.env.TARO_APP_ID_ALIPAY || 'your-alipay-appid',
|
||||||
swan: process.env.TARO_APP_ID_SWAN || 'your-swan-appid',
|
swan: process.env.TARO_APP_ID_SWAN || 'your-swan-appid',
|
||||||
qq: process.env.TARO_APP_ID_QQ || 'your-qq-appid',
|
qq: process.env.TARO_APP_ID_QQ || 'your-qq-appid',
|
||||||
jd: process.env.TARO_APP_ID_JD || 'your-jd-appid'
|
jd: process.env.TARO_APP_ID_JD || 'your-jd-appid',
|
||||||
}
|
};
|
||||||
|
|
||||||
const baseConfig: UserConfigExport<'vite'> = {
|
const baseConfig: UserConfigExport<'vite'> = {
|
||||||
projectName: 'bw-mini-app',
|
projectName: 'bw-mini-app',
|
||||||
@@ -24,24 +24,20 @@ export default defineConfig<'vite'>(async (merge) => {
|
|||||||
640: 2.34 / 2,
|
640: 2.34 / 2,
|
||||||
750: 1,
|
750: 1,
|
||||||
375: 2,
|
375: 2,
|
||||||
828: 1.81 / 2
|
828: 1.81 / 2,
|
||||||
},
|
},
|
||||||
sourceRoot: 'src',
|
sourceRoot: 'src',
|
||||||
outputRoot: process.env.TARO_ENV ? `dist/${process.env.TARO_ENV}` : 'dist',
|
outputRoot: process.env.TARO_ENV ? `dist/${process.env.TARO_ENV}` : 'dist',
|
||||||
plugins: [
|
plugins: ['@tarojs/plugin-generator'],
|
||||||
"@tarojs/plugin-generator"
|
defineConstants: {},
|
||||||
],
|
|
||||||
defineConstants: {
|
|
||||||
},
|
|
||||||
copy: {
|
copy: {
|
||||||
patterns: [
|
patterns: [
|
||||||
{
|
{
|
||||||
from: 'src/assets/icons/',
|
from: 'src/assets/icons/',
|
||||||
to: 'assets/icons/'
|
to: 'assets/icons/',
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
options: {
|
options: {},
|
||||||
}
|
|
||||||
},
|
},
|
||||||
framework: 'react',
|
framework: 'react',
|
||||||
compiler: 'vite',
|
compiler: 'vite',
|
||||||
@@ -49,57 +45,57 @@ export default defineConfig<'vite'>(async (merge) => {
|
|||||||
postcss: {
|
postcss: {
|
||||||
pxtransform: {
|
pxtransform: {
|
||||||
enable: true,
|
enable: true,
|
||||||
config: {
|
config: {},
|
||||||
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
cssModules: {
|
cssModules: {
|
||||||
enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
|
enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
|
||||||
config: {
|
config: {
|
||||||
namingPattern: 'module', // 转换模式,取值为 global/module
|
namingPattern: 'module', // 转换模式,取值为 global/module
|
||||||
generateScopedName: '[name]__[local]___[hash:base64:5]'
|
generateScopedName: '[name]__[local]___[hash:base64:5]',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
webpackChain(chain) {
|
webpackChain(chain) {
|
||||||
chain.merge({
|
chain.merge({
|
||||||
plugin: {
|
plugin: {
|
||||||
install: {
|
install: {
|
||||||
plugin: UnifiedWebpackPluginV5,
|
plugin: UnifiedWebpackPluginV5,
|
||||||
args: [{
|
args: [
|
||||||
|
{
|
||||||
appType: 'taro',
|
appType: 'taro',
|
||||||
// 下面个配置,会开启 rem -> rpx 的转化
|
// 下面个配置,会开启 rem -> rpx 的转化
|
||||||
rem2rpx: true
|
rem2rpx: true,
|
||||||
}]
|
},
|
||||||
}
|
],
|
||||||
}
|
},
|
||||||
})
|
},
|
||||||
}
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
// 小程序平台特定配置
|
// 小程序平台特定配置
|
||||||
weapp: {
|
weapp: {
|
||||||
outputRoot: 'dist/weapp',
|
outputRoot: 'dist/weapp',
|
||||||
appId: appIds.weapp
|
appId: appIds.weapp,
|
||||||
},
|
},
|
||||||
tt: {
|
tt: {
|
||||||
outputRoot: 'dist/tt',
|
outputRoot: 'dist/tt',
|
||||||
appId: appIds.tt
|
appId: appIds.tt,
|
||||||
},
|
},
|
||||||
alipay: {
|
alipay: {
|
||||||
outputRoot: 'dist/alipay',
|
outputRoot: 'dist/alipay',
|
||||||
appId: appIds.alipay
|
appId: appIds.alipay,
|
||||||
},
|
},
|
||||||
swan: {
|
swan: {
|
||||||
outputRoot: 'dist/swan',
|
outputRoot: 'dist/swan',
|
||||||
appId: appIds.swan
|
appId: appIds.swan,
|
||||||
},
|
},
|
||||||
qq: {
|
qq: {
|
||||||
outputRoot: 'dist/qq',
|
outputRoot: 'dist/qq',
|
||||||
appId: appIds.qq
|
appId: appIds.qq,
|
||||||
},
|
},
|
||||||
jd: {
|
jd: {
|
||||||
outputRoot: 'dist/jd',
|
outputRoot: 'dist/jd',
|
||||||
appId: appIds.jd
|
appId: appIds.jd,
|
||||||
},
|
},
|
||||||
h5: {
|
h5: {
|
||||||
publicPath: '/',
|
publicPath: '/',
|
||||||
@@ -108,20 +104,20 @@ export default defineConfig<'vite'>(async (merge) => {
|
|||||||
miniCssExtractPluginOption: {
|
miniCssExtractPluginOption: {
|
||||||
ignoreOrder: true,
|
ignoreOrder: true,
|
||||||
filename: 'css/[name].[hash].css',
|
filename: 'css/[name].[hash].css',
|
||||||
chunkFilename: 'css/[name].[chunkhash].css'
|
chunkFilename: 'css/[name].[chunkhash].css',
|
||||||
},
|
},
|
||||||
postcss: {
|
postcss: {
|
||||||
autoprefixer: {
|
autoprefixer: {
|
||||||
enable: true,
|
enable: true,
|
||||||
config: {}
|
config: {},
|
||||||
},
|
},
|
||||||
cssModules: {
|
cssModules: {
|
||||||
enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
|
enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
|
||||||
config: {
|
config: {
|
||||||
namingPattern: 'module', // 转换模式,取值为 global/module
|
namingPattern: 'module', // 转换模式,取值为 global/module
|
||||||
generateScopedName: '[name]__[local]___[hash:base64:5]'
|
generateScopedName: '[name]__[local]___[hash:base64:5]',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
rn: {
|
rn: {
|
||||||
@@ -129,17 +125,17 @@ export default defineConfig<'vite'>(async (merge) => {
|
|||||||
postcss: {
|
postcss: {
|
||||||
cssModules: {
|
cssModules: {
|
||||||
enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
|
enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
process.env.BROWSERSLIST_ENV = process.env.NODE_ENV
|
process.env.BROWSERSLIST_ENV = process.env.NODE_ENV;
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
// 本地开发构建配置(不混淆压缩)
|
// 本地开发构建配置(不混淆压缩)
|
||||||
return merge({}, baseConfig, devConfig)
|
return merge({}, baseConfig, devConfig);
|
||||||
}
|
}
|
||||||
// 生产构建配置(默认开启压缩混淆等)
|
// 生产构建配置(默认开启压缩混淆等)
|
||||||
return merge({}, baseConfig, prodConfig)
|
return merge({}, baseConfig, prodConfig);
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export default defineAppConfig({
|
|||||||
'pages/home/index', // 新的模板卡片首页
|
'pages/home/index', // 新的模板卡片首页
|
||||||
'pages/history/index', // 历史记录页面
|
'pages/history/index', // 历史记录页面
|
||||||
'pages/result/index',
|
'pages/result/index',
|
||||||
|
'pages/friends-photo/index', // 好友合照页面
|
||||||
],
|
],
|
||||||
tabBar: {
|
tabBar: {
|
||||||
color: '#8E9BAE',
|
color: '#8E9BAE',
|
||||||
@@ -13,14 +14,14 @@ export default defineAppConfig({
|
|||||||
{
|
{
|
||||||
pagePath: 'pages/home/index',
|
pagePath: 'pages/home/index',
|
||||||
text: '游乐场',
|
text: '游乐场',
|
||||||
iconPath: 'assets/icons/playground.png',
|
iconPath: './assets/icons/playground.png',
|
||||||
selectedIconPath: 'assets/icons/playground-selected.png',
|
selectedIconPath: './assets/icons/playground-selected.png',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
pagePath: 'pages/history/index',
|
pagePath: 'pages/history/index',
|
||||||
text: '我的',
|
text: '我的',
|
||||||
iconPath: 'assets/icons/user.png',
|
iconPath: './assets/icons/user.png',
|
||||||
selectedIconPath: 'assets/icons/user-selected.png',
|
selectedIconPath: './assets/icons/user-selected.png',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 11 KiB |
5
src/pages/friends-photo/index.config.ts
Normal file
5
src/pages/friends-photo/index.config.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export default definePageConfig({
|
||||||
|
navigationBarTitleText: '好友合照',
|
||||||
|
enableShareAppMessage: true,
|
||||||
|
enableShareTimeline: true,
|
||||||
|
});
|
||||||
427
src/pages/friends-photo/index.css
Normal file
427
src/pages/friends-photo/index.css
Normal file
@@ -0,0 +1,427 @@
|
|||||||
|
/* 页面容器 */
|
||||||
|
.friends-photo {
|
||||||
|
height: 100vh;
|
||||||
|
background: #f5f6f8;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滚动容器 */
|
||||||
|
.friends-photo-scroll {
|
||||||
|
flex: 1;
|
||||||
|
height: 100%;
|
||||||
|
padding-bottom: 100px; /* 为固定底部按钮留空间 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 介绍卡片 */
|
||||||
|
.intro-card {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
margin: 20px;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 24px;
|
||||||
|
box-shadow: 0 8px 32px rgba(102, 126, 234, 0.3);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-card::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -50%;
|
||||||
|
right: -50%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 0%, transparent 70%);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-icon {
|
||||||
|
font-size: 36px;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 12px;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-text {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 上传区域 */
|
||||||
|
.upload-section {
|
||||||
|
padding: 0 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 上传卡片 */
|
||||||
|
.upload-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-card:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-card-content {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 上传信息 */
|
||||||
|
.upload-info {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1d1f22;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-description {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #8e9bae;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 上传区域 */
|
||||||
|
.upload-area {
|
||||||
|
width: 100%;
|
||||||
|
height: 160px;
|
||||||
|
border: 2px dashed #e5e7eb;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #fafbfc;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-area:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-area.has-image {
|
||||||
|
border: 2px solid #3b82f6;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 上传占位符 */
|
||||||
|
.upload-placeholder {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-icon {
|
||||||
|
font-size: 32px;
|
||||||
|
color: #8e9bae;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-hint {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #8e9bae;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 上传的图片 */
|
||||||
|
.upload-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图片覆盖层 */
|
||||||
|
.image-overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-area.has-image:active .image-overlay {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-text {
|
||||||
|
color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 预览卡片 */
|
||||||
|
.preview-card {
|
||||||
|
background: white;
|
||||||
|
margin: 24px 20px 0;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||||
|
animation: slideUp 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-header {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1d1f22;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #8e9bae;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 预览网格 */
|
||||||
|
.preview-grid {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-item {
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-image {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-label {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 8px;
|
||||||
|
left: 8px;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
color: white;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-connector {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #3b82f6;
|
||||||
|
background: rgba(59, 130, 246, 0.1);
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 底部间距 */
|
||||||
|
.bottom-spacer {
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 提交区域 */
|
||||||
|
.submit-section {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 20px;
|
||||||
|
background: white;
|
||||||
|
border-top: 1px solid #e5e7eb;
|
||||||
|
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.08);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 提交按钮 */
|
||||||
|
.submit-button {
|
||||||
|
width: 100%;
|
||||||
|
height: 56px;
|
||||||
|
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
|
||||||
|
border-radius: 28px;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 6px 24px rgba(59, 130, 246, 0.4);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-button::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -50%;
|
||||||
|
left: -50%;
|
||||||
|
width: 200%;
|
||||||
|
height: 200%;
|
||||||
|
background: linear-gradient(45deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||||
|
transform: rotate(45deg);
|
||||||
|
transition: transform 0.6s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-button:not(.disabled):active::before {
|
||||||
|
transform: rotate(45deg) translateX(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-button.disabled {
|
||||||
|
background: #d1d5db;
|
||||||
|
box-shadow: none;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-button:not(.disabled):active {
|
||||||
|
transform: translateY(2px);
|
||||||
|
box-shadow: 0 3px 16px rgba(59, 130, 246, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-text {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载动画 */
|
||||||
|
.loading-spinner {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-top: 2px solid white;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式适配 */
|
||||||
|
@media (max-width: 375px) {
|
||||||
|
.intro-card {
|
||||||
|
margin: 16px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-section {
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-card {
|
||||||
|
margin: 20px 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-section {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 深色模式适配 */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.friends-photo {
|
||||||
|
background: #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-card,
|
||||||
|
.preview-card {
|
||||||
|
background: #2a2a2a;
|
||||||
|
border: 1px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-title {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-description {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-area {
|
||||||
|
background: #333;
|
||||||
|
border-color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-section {
|
||||||
|
background: #2a2a2a;
|
||||||
|
border-color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
322
src/pages/friends-photo/index.tsx
Normal file
322
src/pages/friends-photo/index.tsx
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
import { View, Image, Button, ScrollView } from '@tarojs/components';
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import Taro, { navigateTo } from '@tarojs/taro';
|
||||||
|
import { useSdk, useServerSdk } from '../../hooks/index';
|
||||||
|
import { useImageDetectionTaskManager, ImageAuditResult, AuditConclusion } from '../../hooks/useImageDetectionTaskManager';
|
||||||
|
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
// 好友合照模板的固定代码
|
||||||
|
const FRIENDS_PHOTO_TEMPLATE_CODE = 'friends_photo';
|
||||||
|
|
||||||
|
export default function FriendsPhoto() {
|
||||||
|
const sdk = useSdk();
|
||||||
|
const serverSdk = useServerSdk();
|
||||||
|
const { submitAuditTask } = useImageDetectionTaskManager();
|
||||||
|
|
||||||
|
const [image1, setImage1] = useState<string>('');
|
||||||
|
const [image2, setImage2] = useState<string>('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [showGuide, setShowGuide] = useState(true);
|
||||||
|
|
||||||
|
// 显示使用指南
|
||||||
|
useEffect(() => {
|
||||||
|
if (!showGuide) return;
|
||||||
|
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
Taro.showModal({
|
||||||
|
title: '使用小贴士',
|
||||||
|
content: '• 选择清晰的人脸照片效果更佳\n• 建议选择光线充足的照片\n• 支持JPG、PNG格式',
|
||||||
|
confirmText: '我知道了',
|
||||||
|
showCancel: false,
|
||||||
|
success: () => {
|
||||||
|
setShowGuide(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [showGuide]);
|
||||||
|
|
||||||
|
// 处理审核失败
|
||||||
|
const handleAuditFailure = (auditResult: ImageAuditResult) => {
|
||||||
|
const messages: Record<AuditConclusion, string> = {
|
||||||
|
[AuditConclusion.REJECT]: '图片内容不符合规范,请重新选择符合规范的图片',
|
||||||
|
[AuditConclusion.REVIEW]: '图片需要人工审核,请稍后重试或更换其他图片',
|
||||||
|
[AuditConclusion.UNCERTAIN]: '图片审核结果不确定,请重新选择图片',
|
||||||
|
[AuditConclusion.PASS]: '图片审核通过',
|
||||||
|
};
|
||||||
|
|
||||||
|
const message = auditResult.conclusion ? messages[auditResult.conclusion] : '图片审核失败,请重新选择图片';
|
||||||
|
|
||||||
|
Taro.showModal({
|
||||||
|
title: '图片审核未通过',
|
||||||
|
content: message,
|
||||||
|
confirmText: '我知道了',
|
||||||
|
showCancel: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 上传单张图片
|
||||||
|
const uploadSingleImage = async (imageIndex: 1 | 2) => {
|
||||||
|
try {
|
||||||
|
// 添加触觉反馈
|
||||||
|
Taro.vibrateShort();
|
||||||
|
|
||||||
|
Taro.showLoading({
|
||||||
|
title: '选择图片中...',
|
||||||
|
mask: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const imageUrl = await sdk.chooseAndUploadImage({
|
||||||
|
count: 1,
|
||||||
|
onImageSelected: () => {
|
||||||
|
Taro.showLoading({
|
||||||
|
title: '正在上传...',
|
||||||
|
mask: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onProgress: () => {
|
||||||
|
Taro.showLoading({
|
||||||
|
title: '上传中...',
|
||||||
|
mask: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 审核图片
|
||||||
|
const auditResult = await new Promise<ImageAuditResult>((resolve, reject) => {
|
||||||
|
submitAuditTask(
|
||||||
|
{
|
||||||
|
imageUrl,
|
||||||
|
options: {
|
||||||
|
enableCache: true,
|
||||||
|
timeout: 30000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
(status, progress) => {
|
||||||
|
console.log('审核进度:', status, progress);
|
||||||
|
},
|
||||||
|
result => {
|
||||||
|
console.log('审核成功:', result);
|
||||||
|
resolve(result);
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
console.error('审核失败:', error);
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Taro.hideLoading();
|
||||||
|
|
||||||
|
if (auditResult.conclusion === AuditConclusion.PASS) {
|
||||||
|
if (imageIndex === 1) {
|
||||||
|
setImage1(imageUrl);
|
||||||
|
} else {
|
||||||
|
setImage2(imageUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 成功反馈
|
||||||
|
Taro.vibrateShort();
|
||||||
|
Taro.showToast({
|
||||||
|
title: `图片${imageIndex}上传成功`,
|
||||||
|
icon: 'success',
|
||||||
|
duration: 1500,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
handleAuditFailure(auditResult);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
Taro.hideLoading();
|
||||||
|
|
||||||
|
// 用户取消选择图片,不显示错误提示
|
||||||
|
if (error.errMsg && error.errMsg.includes('chooseImage:fail cancel')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提供更详细的错误提示
|
||||||
|
let errorTitle = '图片上传失败';
|
||||||
|
if (error?.message) {
|
||||||
|
errorTitle = error.message;
|
||||||
|
} else if (error?.errMsg) {
|
||||||
|
if (error.errMsg.includes('chooseImage:fail auth')) {
|
||||||
|
errorTitle = '需要授权访问相册';
|
||||||
|
} else if (error.errMsg.includes('timeout')) {
|
||||||
|
errorTitle = '上传超时,请重试';
|
||||||
|
} else if (error.errMsg.includes('network')) {
|
||||||
|
errorTitle = '网络连接失败';
|
||||||
|
} else {
|
||||||
|
errorTitle = '图片上传失败,请重试';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Taro.showToast({
|
||||||
|
title: errorTitle,
|
||||||
|
icon: 'error',
|
||||||
|
duration: 3000,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.error('图片上传详细错误:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 提交处理
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!image1 || !image2) {
|
||||||
|
Taro.vibrateShort();
|
||||||
|
Taro.showToast({
|
||||||
|
title: '请先上传两张图片',
|
||||||
|
icon: 'none',
|
||||||
|
duration: 2000,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交时的触觉反馈
|
||||||
|
Taro.vibrateShort();
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Taro.showLoading({
|
||||||
|
title: 'AI正在生成中...',
|
||||||
|
mask: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 将两个图片URL用逗号分隔
|
||||||
|
const combinedImageUrl = `${image1},${image2}`;
|
||||||
|
|
||||||
|
const taskId = await serverSdk.executeTemplate({
|
||||||
|
imageUrl: combinedImageUrl,
|
||||||
|
templateCode: FRIENDS_PHOTO_TEMPLATE_CODE,
|
||||||
|
});
|
||||||
|
|
||||||
|
Taro.hideLoading();
|
||||||
|
|
||||||
|
// 跳转到结果页面
|
||||||
|
navigateTo({
|
||||||
|
url: `/pages/result/index?taskId=${taskId}&templateCode=${FRIENDS_PHOTO_TEMPLATE_CODE}`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
Taro.hideLoading();
|
||||||
|
console.error('提交失败:', error);
|
||||||
|
|
||||||
|
Taro.showToast({
|
||||||
|
title: '请等待生成中的任务完成后再尝试',
|
||||||
|
icon: 'none',
|
||||||
|
duration: 2000,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderUploadCard = (imageIndex: 1 | 2, imageUrl: string, title: string, description: string) => (
|
||||||
|
<View className="upload-card">
|
||||||
|
<View className="upload-card-content">
|
||||||
|
<View className="upload-info">
|
||||||
|
<View className="upload-title">{title}</View>
|
||||||
|
<View className="upload-description">{description}</View>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
className={`upload-area ${imageUrl ? 'has-image' : ''}`}
|
||||||
|
onClick={() => uploadSingleImage(imageIndex)}
|
||||||
|
>
|
||||||
|
{imageUrl ? (
|
||||||
|
<>
|
||||||
|
<Image src={imageUrl} className="upload-image" mode="aspectFill" />
|
||||||
|
<View className="image-overlay">
|
||||||
|
<View className="overlay-content">
|
||||||
|
<View className="change-icon">🔄</View>
|
||||||
|
<View className="change-text">点击更换</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<View className="upload-placeholder">
|
||||||
|
<View className="upload-icon">📷</View>
|
||||||
|
<View className="upload-hint">点击上传</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className="friends-photo">
|
||||||
|
<ScrollView
|
||||||
|
className="friends-photo-scroll"
|
||||||
|
scrollY
|
||||||
|
enhanced
|
||||||
|
showScrollbar={false}
|
||||||
|
enablePassive
|
||||||
|
bounces
|
||||||
|
>
|
||||||
|
{/* 顶部介绍卡片 */}
|
||||||
|
<View className="intro-card">
|
||||||
|
<View className="intro-content">
|
||||||
|
<View className="intro-icon">👫</View>
|
||||||
|
<View className="intro-text">
|
||||||
|
<View className="intro-title">好友合照生成</View>
|
||||||
|
<View className="intro-subtitle">
|
||||||
|
上传两张照片,AI将为你们生成精彩的合照视频
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 上传区域 */}
|
||||||
|
<View className="upload-section">
|
||||||
|
{renderUploadCard(1, image1, "第一张照片", "选择你的照片")}
|
||||||
|
{renderUploadCard(2, image2, "第二张照片", "选择朋友的照片")}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 预览区域 */}
|
||||||
|
{image1 && image2 && (
|
||||||
|
<View className="preview-card">
|
||||||
|
<View className="preview-header">
|
||||||
|
<View className="preview-title">📸 照片预览</View>
|
||||||
|
<View className="preview-subtitle">确认无误后即可开始生成</View>
|
||||||
|
</View>
|
||||||
|
<View className="preview-grid">
|
||||||
|
<View className="preview-item">
|
||||||
|
<Image src={image1} className="preview-image" mode="aspectFill" />
|
||||||
|
<View className="preview-label">照片 1</View>
|
||||||
|
</View>
|
||||||
|
<View className="preview-connector">+</View>
|
||||||
|
<View className="preview-item">
|
||||||
|
<Image src={image2} className="preview-image" mode="aspectFill" />
|
||||||
|
<View className="preview-label">照片 2</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 底部间距 */}
|
||||||
|
<View className="bottom-spacer" />
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
{/* 固定底部按钮 */}
|
||||||
|
<View className="submit-section">
|
||||||
|
<Button
|
||||||
|
className={`submit-button ${(!image1 || !image2 || loading) ? 'disabled' : ''}`}
|
||||||
|
disabled={!image1 || !image2 || loading}
|
||||||
|
onClick={handleSubmit}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<View className="loading-spinner" />
|
||||||
|
<View className="submit-text">生成中...</View>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<View className="submit-icon">✨</View>
|
||||||
|
<View className="submit-text">开始生成合照</View>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -90,10 +90,17 @@ export default function Home() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleTemplateClick = async (template: Template) => {
|
const handleTemplateClick = async (template: Template) => {
|
||||||
|
// 如果是好友合照,跳转到专门的页面
|
||||||
|
if (template.name === '好友合照') {
|
||||||
|
navigateTo({ url: `/pages/friends-photo/index` });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 第一步:选择并上传图片
|
// 第一步:选择并上传图片
|
||||||
|
const imageCount = 1;
|
||||||
const imageUrl = await sdk.chooseAndUploadImage({
|
const imageUrl = await sdk.chooseAndUploadImage({
|
||||||
|
count: imageCount,
|
||||||
onImageSelected: () => {
|
onImageSelected: () => {
|
||||||
Taro.showLoading({
|
Taro.showLoading({
|
||||||
title: getLoadingText('uploading'),
|
title: getLoadingText('uploading'),
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ const GeneratingComponent: React.FC<GeneratingComponentProps> = ({ task }) => {
|
|||||||
<View
|
<View
|
||||||
className="progress-fill"
|
className="progress-fill"
|
||||||
style={{
|
style={{
|
||||||
background: `conic-gradient(#ff9500 ${progress * 3.6}deg, transparent 0deg)`,
|
background: `conic-gradient(#FFCF5F ${progress * 3.6}deg, transparent 0deg)`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<View className="progress-inner">
|
<View className="progress-inner">
|
||||||
@@ -81,6 +81,7 @@ const GeneratingComponent: React.FC<GeneratingComponentProps> = ({ task }) => {
|
|||||||
{/* 状态文本 */}
|
{/* 状态文本 */}
|
||||||
<View className="status-section">
|
<View className="status-section">
|
||||||
<View className="status-title">
|
<View className="status-title">
|
||||||
|
<Image className="status-icon-image" src="/assets/icons/funnel.png" />
|
||||||
<Text className="status-title-text">生成中...</Text>
|
<Text className="status-title-text">生成中...</Text>
|
||||||
</View>
|
</View>
|
||||||
<View className="status-subtitle">内容生成中,请耐心等待</View>
|
<View className="status-subtitle">内容生成中,请耐心等待</View>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ declare const qq: any;
|
|||||||
* 支持的平台类型
|
* 支持的平台类型
|
||||||
* 目前支持:字节跳动小程序(tt)
|
* 目前支持:字节跳动小程序(tt)
|
||||||
*/
|
*/
|
||||||
export type Platform = 'bytedance' | 'wechat' | 'alipay' | 'swan' | 'qq';
|
export type Platform = 'bytedance' | 'wechat' | 'alipay' | 'swan' | 'qq' | 'h5';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 平台适配器工厂类
|
* 平台适配器工厂类
|
||||||
@@ -54,6 +54,8 @@ export class PlatformFactory {
|
|||||||
throw new Error(`百度智能小程序平台暂未实现`);
|
throw new Error(`百度智能小程序平台暂未实现`);
|
||||||
case 'qq':
|
case 'qq':
|
||||||
throw new Error(`QQ 小程序平台暂未实现`);
|
throw new Error(`QQ 小程序平台暂未实现`);
|
||||||
|
case 'h5':
|
||||||
|
throw new Error(`H5平台暂未实现`);
|
||||||
default:
|
default:
|
||||||
throw new Error(`不支持的平台类型: ${this.platform}`);
|
throw new Error(`不支持的平台类型: ${this.platform}`);
|
||||||
}
|
}
|
||||||
@@ -133,7 +135,7 @@ export class PlatformFactory {
|
|||||||
if (typeof qq !== 'undefined') {
|
if (typeof qq !== 'undefined') {
|
||||||
return 'qq';
|
return 'qq';
|
||||||
}
|
}
|
||||||
throw new Error('无法检测到支持的小程序平台环境');
|
return 'h5';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { PlatformFactory } from "../platforms/factory";
|
||||||
import Taro from "@tarojs/taro"
|
import Taro from "@tarojs/taro"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -51,7 +52,7 @@ interface ImageToVideoParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isGetFileInfoSuccessCallbackResult(val: any): val is Taro.getFileInfo.SuccessCallbackResult {
|
export function isGetFileInfoSuccessCallbackResult(val: any): val is Taro.getFileInfo.SuccessCallbackResult {
|
||||||
return val && Reflect.has(val, 'digest')
|
return val && typeof val.size === 'number' && !val.errMsg
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -74,11 +75,25 @@ export class BowongAISDK {
|
|||||||
const url = `${this.baseUrl}/api/file/upload/s3`;
|
const url = `${this.baseUrl}/api/file/upload/s3`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 获取文件信息
|
// 检测环境并使用不同的上传策略
|
||||||
const fileInfo = await Taro.getFileInfo({ filePath });
|
if (PlatformFactory.detectPlatform() === 'h5') {
|
||||||
if (!isGetFileInfoSuccessCallbackResult(fileInfo)) {
|
return await this._uploadForH5(params);
|
||||||
throw new Error(fileInfo.errMsg)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 小程序环境的原有逻辑
|
||||||
|
// 获取文件信息
|
||||||
|
let fileInfo;
|
||||||
|
try {
|
||||||
|
fileInfo = await Taro.getFileInfo({ filePath });
|
||||||
|
if (!isGetFileInfoSuccessCallbackResult(fileInfo)) {
|
||||||
|
console.warn('获取文件信息失败,使用默认值:', fileInfo.errMsg);
|
||||||
|
fileInfo = { size: 0 };
|
||||||
|
}
|
||||||
|
} catch (getFileInfoError) {
|
||||||
|
console.warn('getFileInfo调用失败,使用默认值:', getFileInfoError);
|
||||||
|
fileInfo = { size: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
// 自动生成文件名(如果未提供)
|
// 自动生成文件名(如果未提供)
|
||||||
const fileName = name || `upload_${Date.now()}.${this._getFileExtension(filePath)}`;
|
const fileName = name || `upload_${Date.now()}.${this._getFileExtension(filePath)}`;
|
||||||
|
|
||||||
@@ -138,8 +153,105 @@ export class BowongAISDK {
|
|||||||
console.log('文件上传成功:', result.data);
|
console.log('文件上传成功:', result.data);
|
||||||
return result.data;
|
return result.data;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error('文件上传失败:', error);
|
console.error('文件上传失败:', error);
|
||||||
|
|
||||||
|
// 提供更详细的错误信息
|
||||||
|
let errorMessage = '文件上传失败';
|
||||||
|
if (error?.errMsg) {
|
||||||
|
if (error.errMsg.includes('timeout')) {
|
||||||
|
errorMessage = '上传超时,请检查网络连接';
|
||||||
|
} else if (error.errMsg.includes('network')) {
|
||||||
|
errorMessage = '网络连接失败,请检查网络';
|
||||||
|
} else if (error.errMsg.includes('size')) {
|
||||||
|
errorMessage = '文件过大,请选择较小的图片';
|
||||||
|
} else if (error.errMsg.includes('cors') || error.errMsg.includes('CORS')) {
|
||||||
|
errorMessage = 'H5环境跨域错误,请联系开发者';
|
||||||
|
} else {
|
||||||
|
errorMessage = `上传失败: ${error.errMsg}`;
|
||||||
|
}
|
||||||
|
} else if (error?.message) {
|
||||||
|
if (error.message.includes('cors') || error.message.includes('CORS')) {
|
||||||
|
errorMessage = 'H5环境跨域错误,请联系开发者';
|
||||||
|
} else {
|
||||||
|
errorMessage = error.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* H5环境专用上传方法
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private async _uploadForH5(params: UploadParams): Promise<string> {
|
||||||
|
const { filePath, name, type } = params;
|
||||||
|
const url = `${this.baseUrl}/api/file/upload/s3`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// H5环境中,filePath实际上是File对象或Blob URL
|
||||||
|
let file: File;
|
||||||
|
|
||||||
|
if (typeof filePath === 'string') {
|
||||||
|
// 如果是blob URL,需要先获取blob
|
||||||
|
const response = await fetch(filePath);
|
||||||
|
const blob = await response.blob();
|
||||||
|
const fileName = name || `upload_${Date.now()}.${this._getFileExtension(filePath)}`;
|
||||||
|
file = new File([blob], fileName, { type: type || blob.type });
|
||||||
|
} else if (filePath && typeof filePath === 'object' && 'name' in filePath && 'size' in filePath && 'type' in filePath) {
|
||||||
|
file = filePath as File;
|
||||||
|
} else {
|
||||||
|
throw new Error('H5环境下文件路径格式不正确');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建FormData
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
formData.append('filename', file.name);
|
||||||
|
formData.append('type', file.type);
|
||||||
|
|
||||||
|
console.log('H5环境开始上传文件:', {
|
||||||
|
fileName: file.name,
|
||||||
|
fileSize: file.size,
|
||||||
|
fileType: file.type
|
||||||
|
});
|
||||||
|
|
||||||
|
// 使用fetch API上传,添加CORS支持
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
// 不要手动设置Content-Type,让浏览器自动设置multipart/form-data边界
|
||||||
|
},
|
||||||
|
mode: 'cors', // 明确指定CORS模式
|
||||||
|
credentials: 'omit' // 不发送cookie
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: 文件上传失败`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json() as ApiResponse<string>;
|
||||||
|
|
||||||
|
// 检查业务状态码
|
||||||
|
if (!result.status) {
|
||||||
|
throw new Error(result.msg || '文件上传失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('H5环境文件上传成功:', result.data);
|
||||||
|
return result.data;
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('H5环境文件上传失败:', error);
|
||||||
|
|
||||||
|
// 特殊处理CORS错误
|
||||||
|
if (error.name === 'TypeError' && error.message.includes('fetch')) {
|
||||||
|
throw new Error('网络错误:可能是CORS问题,请确保服务器支持跨域请求');
|
||||||
|
}
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -413,17 +525,22 @@ export class BowongAISDK {
|
|||||||
const url = `${this.baseUrl}/api/custom/image/submit/task`;
|
const url = `${this.baseUrl}/api/custom/image/submit/task`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 改为选择图片
|
|
||||||
|
|
||||||
// 如果提供了本地文件路径,使用 uploadFile
|
// 如果提供了本地文件路径,使用 uploadFile
|
||||||
if (img_file) {
|
if (img_file) {
|
||||||
|
// 检测环境并使用不同的上传策略
|
||||||
|
if (PlatformFactory.detectPlatform() === 'h5') {
|
||||||
|
return await this._generateImageForH5(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 小程序环境的原有逻辑
|
||||||
const response = await new Promise<Taro.uploadFile.SuccessCallbackResult>((resolve, reject) => {
|
const response = await new Promise<Taro.uploadFile.SuccessCallbackResult>((resolve, reject) => {
|
||||||
Taro.uploadFile({
|
Taro.uploadFile({
|
||||||
url,
|
url,
|
||||||
filePath: img_file,
|
filePath: img_file,
|
||||||
name: 'img_file',
|
name: 'img_file',
|
||||||
header: {
|
header: {
|
||||||
'accept': 'application/json'
|
'accept': 'application/json',
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
},
|
},
|
||||||
formData: {
|
formData: {
|
||||||
prompt,
|
prompt,
|
||||||
@@ -458,6 +575,92 @@ export class BowongAISDK {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* H5环境专用图像生成方法
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private async _generateImageForH5(params: GenerateImageParams): Promise<string> {
|
||||||
|
const {
|
||||||
|
prompt,
|
||||||
|
model_name = 'gemini-2.5-flash-image-preview',
|
||||||
|
aspect_ratio = '9:16',
|
||||||
|
mode = 'turbo',
|
||||||
|
webhook_flag = false,
|
||||||
|
img_file
|
||||||
|
} = params;
|
||||||
|
|
||||||
|
const url = `${this.baseUrl}/api/custom/image/submit/task`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// H5环境中,img_file实际上是File对象或Blob URL
|
||||||
|
let file: File;
|
||||||
|
|
||||||
|
if (typeof img_file === 'string') {
|
||||||
|
// 如果是blob URL,需要先获取blob
|
||||||
|
const response = await fetch(img_file);
|
||||||
|
const blob = await response.blob();
|
||||||
|
const fileName = `generate_${Date.now()}.${this._getFileExtension(img_file)}`;
|
||||||
|
file = new File([blob], fileName, { type: blob.type });
|
||||||
|
} else if (img_file && typeof img_file === 'object' && 'name' in img_file && 'size' in img_file && 'type' in img_file) {
|
||||||
|
file = img_file as File;
|
||||||
|
} else {
|
||||||
|
throw new Error('H5环境下图片文件格式不正确');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建FormData
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('img_file', file);
|
||||||
|
formData.append('prompt', prompt);
|
||||||
|
formData.append('model_name', model_name);
|
||||||
|
formData.append('aspect_ratio', aspect_ratio);
|
||||||
|
formData.append('mode', mode.toString());
|
||||||
|
formData.append('webhook_flag', webhook_flag.toString());
|
||||||
|
formData.append('img_url', '');
|
||||||
|
|
||||||
|
console.log('H5环境开始图像生成:', {
|
||||||
|
fileName: file.name,
|
||||||
|
fileSize: file.size,
|
||||||
|
fileType: file.type,
|
||||||
|
prompt: prompt.substring(0, 100) + '...'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 使用fetch API上传,添加CORS支持
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
headers: {
|
||||||
|
'accept': 'application/json',
|
||||||
|
// 不要手动设置Content-Type,让浏览器自动设置multipart/form-data边界
|
||||||
|
},
|
||||||
|
mode: 'cors', // 明确指定CORS模式
|
||||||
|
credentials: 'omit' // 不发送cookie
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: 图像生成请求失败`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json() as ApiResponse<string>;
|
||||||
|
|
||||||
|
if (!result.status) {
|
||||||
|
throw new Error(result.msg || '图像生成请求失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('H5环境图像生成任务提交成功:', result.data);
|
||||||
|
return result.data;
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('H5环境图像生成失败:', error);
|
||||||
|
|
||||||
|
// 特殊处理CORS错误
|
||||||
|
if (error.name === 'TypeError' && error.message.includes('fetch')) {
|
||||||
|
throw new Error('网络错误:可能是CORS问题,请确保服务器支持跨域请求');
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询任务状态
|
* 查询任务状态
|
||||||
* @param taskId 任务ID
|
* @param taskId 任务ID
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export interface Template {
|
|||||||
inputExample: string; // 原始图片
|
inputExample: string; // 原始图片
|
||||||
outputExample: string; // 输出图片
|
outputExample: string; // 输出图片
|
||||||
tags: string[]; // 标签数组
|
tags: string[]; // 标签数组
|
||||||
|
templateType: string; // 模板类型
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,167 +0,0 @@
|
|||||||
# Redux Store 使用指南
|
|
||||||
|
|
||||||
本项目已从 Zustand 迁移到 Redux Toolkit,以下是使用指南。
|
|
||||||
|
|
||||||
## 目录结构
|
|
||||||
|
|
||||||
```
|
|
||||||
src/
|
|
||||||
├── store/
|
|
||||||
│ ├── index.ts # Store 配置
|
|
||||||
│ └── types.ts # 类型定义
|
|
||||||
├── constants/ # Action 类型常量
|
|
||||||
│ ├── history.ts
|
|
||||||
│ └── template.ts
|
|
||||||
├── actions/ # Action creators
|
|
||||||
│ ├── history.ts
|
|
||||||
│ └── template.ts
|
|
||||||
├── reducers/ # Reducers
|
|
||||||
│ ├── index.ts
|
|
||||||
│ ├── history.ts
|
|
||||||
│ └── template.ts
|
|
||||||
├── selectors/ # 选择器
|
|
||||||
│ ├── history.ts
|
|
||||||
│ └── template.ts
|
|
||||||
└── hooks/
|
|
||||||
└── redux.ts # 类型化的 hooks
|
|
||||||
```
|
|
||||||
|
|
||||||
## 基本使用
|
|
||||||
|
|
||||||
### 1. 在组件中使用 Redux
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import { useAppDispatch, useAppSelector } from '../../hooks/redux'
|
|
||||||
import { selectHistoryRecords, selectHistoryLoading } from '../../selectors/history'
|
|
||||||
import { loadRecords, addRecord } from '../../actions/history'
|
|
||||||
|
|
||||||
export default function MyComponent() {
|
|
||||||
const dispatch = useAppDispatch()
|
|
||||||
const records = useAppSelector(selectHistoryRecords)
|
|
||||||
const loading = useAppSelector(selectHistoryLoading)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
dispatch(loadRecords())
|
|
||||||
}, [dispatch])
|
|
||||||
|
|
||||||
const handleAddRecord = async () => {
|
|
||||||
try {
|
|
||||||
await dispatch(addRecord({
|
|
||||||
type: 'image-to-image',
|
|
||||||
templateId: '1',
|
|
||||||
templateName: '艺术风格',
|
|
||||||
inputImage: 'path/to/image.jpg',
|
|
||||||
status: 'generating'
|
|
||||||
}))
|
|
||||||
} catch (error) {
|
|
||||||
console.error('添加记录失败:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View>
|
|
||||||
{loading && <Text>加载中...</Text>}
|
|
||||||
{records.map(record => (
|
|
||||||
<View key={record.id}>
|
|
||||||
<Text>{record.templateName}</Text>
|
|
||||||
</View>
|
|
||||||
))}
|
|
||||||
<Button onClick={handleAddRecord}>添加记录</Button>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 历史记录管理
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
// 加载历史记录
|
|
||||||
dispatch(loadRecords())
|
|
||||||
|
|
||||||
// 添加新记录
|
|
||||||
dispatch(addRecord({
|
|
||||||
type: 'image-to-video',
|
|
||||||
templateId: '2',
|
|
||||||
templateName: '视频生成',
|
|
||||||
inputImage: 'path/to/image.jpg',
|
|
||||||
status: 'generating'
|
|
||||||
}))
|
|
||||||
|
|
||||||
// 更新记录
|
|
||||||
dispatch(updateRecord('record_id', {
|
|
||||||
status: 'completed',
|
|
||||||
outputResult: 'path/to/result.mp4'
|
|
||||||
}))
|
|
||||||
|
|
||||||
// 删除记录
|
|
||||||
dispatch(deleteRecord('record_id'))
|
|
||||||
|
|
||||||
// 清空所有记录
|
|
||||||
dispatch(clearRecords())
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 模板管理
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
// 初始化模板
|
|
||||||
import { TEMPLATE_CONFIG } from '../../config/templates'
|
|
||||||
dispatch(initTemplates(TEMPLATE_CONFIG))
|
|
||||||
|
|
||||||
// 设置选中的模板
|
|
||||||
dispatch(setSelectedTemplate(template))
|
|
||||||
|
|
||||||
// 获取模板列表
|
|
||||||
const templates = useAppSelector(selectTemplates)
|
|
||||||
|
|
||||||
// 获取选中的模板
|
|
||||||
const selectedTemplate = useAppSelector(selectSelectedTemplate)
|
|
||||||
|
|
||||||
// 根据类型筛选模板
|
|
||||||
const videoTemplates = useAppSelector(state =>
|
|
||||||
selectTemplatesByType(state, 'image-to-video')
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 选择器使用
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
// 基本选择器
|
|
||||||
const records = useAppSelector(selectHistoryRecords)
|
|
||||||
const loading = useAppSelector(selectHistoryLoading)
|
|
||||||
const error = useAppSelector(selectHistoryError)
|
|
||||||
|
|
||||||
// 带参数的选择器
|
|
||||||
const record = useAppSelector(state => selectRecordById(state, 'record_id'))
|
|
||||||
const generatingRecords = useAppSelector(state =>
|
|
||||||
selectRecordsByStatus(state, 'generating')
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 迁移说明
|
|
||||||
|
|
||||||
### 从 Zustand 迁移到 Redux
|
|
||||||
|
|
||||||
原来的 Zustand 用法:
|
|
||||||
```tsx
|
|
||||||
// 旧的 Zustand 用法
|
|
||||||
const { records, loadRecords, addRecord } = useHistoryStore()
|
|
||||||
```
|
|
||||||
|
|
||||||
新的 Redux 用法:
|
|
||||||
```tsx
|
|
||||||
// 新的 Redux 用法
|
|
||||||
const dispatch = useAppDispatch()
|
|
||||||
const records = useAppSelector(selectHistoryRecords)
|
|
||||||
|
|
||||||
// 调用 actions
|
|
||||||
dispatch(loadRecords())
|
|
||||||
dispatch(addRecord(recordData))
|
|
||||||
```
|
|
||||||
|
|
||||||
## 注意事项
|
|
||||||
|
|
||||||
1. 所有异步操作都通过 Redux Thunk 处理
|
|
||||||
2. 使用类型化的 hooks (`useAppDispatch`, `useAppSelector`)
|
|
||||||
3. 选择器函数提供了类型安全的状态访问
|
|
||||||
4. 所有状态更新都是不可变的
|
|
||||||
5. 错误处理通过 Redux 状态管理
|
|
||||||
@@ -12,7 +12,7 @@ export interface HistoryRecord {
|
|||||||
errorMessage?: string
|
errorMessage?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Template } from '../sdk/sdk-server'
|
export type { Template } from '../sdk/sdk-server'
|
||||||
export interface GenerateParams {
|
export interface GenerateParams {
|
||||||
templateId: string
|
templateId: string
|
||||||
imageUrl: string
|
imageUrl: string
|
||||||
|
|||||||
@@ -1,168 +0,0 @@
|
|||||||
import Taro from '@tarojs/taro';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 上传参数接口
|
|
||||||
*/
|
|
||||||
export interface UploadParams {
|
|
||||||
filePath: string;
|
|
||||||
name?: string;
|
|
||||||
type?: string;
|
|
||||||
onProgress?: (progress: number) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* API响应接口
|
|
||||||
*/
|
|
||||||
interface ApiResponse<T = any> {
|
|
||||||
status: boolean;
|
|
||||||
msg: string;
|
|
||||||
data: T;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取文件信息成功回调结果类型守卫
|
|
||||||
*/
|
|
||||||
function isGetFileInfoSuccessCallbackResult(
|
|
||||||
result: any
|
|
||||||
): result is Taro.getFileInfo.SuccessCallbackResult {
|
|
||||||
return result && typeof result.size === 'number';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 图片上传工具类
|
|
||||||
*/
|
|
||||||
export class ImageUploader {
|
|
||||||
private baseUrl: string;
|
|
||||||
|
|
||||||
constructor(baseUrl: string = 'https://bowongai-test--text-video-agent-fastapi-app.modal.run') {
|
|
||||||
this.baseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 上传文件
|
|
||||||
*/
|
|
||||||
async upload(params: UploadParams): Promise<string> {
|
|
||||||
const { filePath, name, type, onProgress } = params;
|
|
||||||
const url = `${this.baseUrl}/api/file/upload/s3`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 获取文件信息
|
|
||||||
let fileInfo;
|
|
||||||
try {
|
|
||||||
fileInfo = await Taro.getFileInfo({ filePath });
|
|
||||||
if (!isGetFileInfoSuccessCallbackResult(fileInfo)) {
|
|
||||||
console.warn('获取文件信息失败,使用默认值');
|
|
||||||
fileInfo = { size: 0 };
|
|
||||||
}
|
|
||||||
} catch (getFileInfoError) {
|
|
||||||
console.warn('getFileInfo调用失败,使用默认值:', getFileInfoError);
|
|
||||||
fileInfo = { size: 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
// 自动生成文件名(如果未提供)
|
|
||||||
const fileName = name || `upload_${Date.now()}.${this._getFileExtension(filePath)}`;
|
|
||||||
|
|
||||||
// 自动判断文件类型(如果未提供)
|
|
||||||
const fileType = type || this._getMimeType(filePath);
|
|
||||||
|
|
||||||
console.log('开始上传文件:', {
|
|
||||||
fileName,
|
|
||||||
fileSize: fileInfo.size,
|
|
||||||
fileType
|
|
||||||
});
|
|
||||||
|
|
||||||
// 使用 Taro.uploadFile 进行文件上传
|
|
||||||
const response = await new Promise<Taro.uploadFile.SuccessCallbackResult>((resolve, reject) => {
|
|
||||||
const uploadTask = Taro.uploadFile({
|
|
||||||
url,
|
|
||||||
filePath,
|
|
||||||
name: 'file', // 服务端接收的字段名
|
|
||||||
header: {
|
|
||||||
'Accept': 'application/json'
|
|
||||||
},
|
|
||||||
formData: {
|
|
||||||
filename: fileName,
|
|
||||||
type: fileType
|
|
||||||
},
|
|
||||||
success: resolve,
|
|
||||||
fail: reject
|
|
||||||
});
|
|
||||||
|
|
||||||
// 监听上传进度
|
|
||||||
if (onProgress) {
|
|
||||||
uploadTask.progress((res) => {
|
|
||||||
const progress = Math.round((res.totalBytesSent / res.totalBytesExpectedToSend) * 100);
|
|
||||||
onProgress(progress);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 检查 HTTP 状态码
|
|
||||||
if (response.statusCode !== 200) {
|
|
||||||
throw new Error(`HTTP ${response.statusCode}: 文件上传失败`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析响应数据
|
|
||||||
let result: ApiResponse<string>;
|
|
||||||
try {
|
|
||||||
result = JSON.parse(response.data) as ApiResponse<string>;
|
|
||||||
} catch (parseError) {
|
|
||||||
throw new Error('服务器响应格式错误');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查业务状态码
|
|
||||||
if (!result.status) {
|
|
||||||
throw new Error(result.msg || '文件上传失败');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('文件上传成功:', result.data);
|
|
||||||
return result.data;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('文件上传失败:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取文件扩展名
|
|
||||||
*/
|
|
||||||
private _getFileExtension(filePath: string): string {
|
|
||||||
const parts = filePath.split('.');
|
|
||||||
return parts.length > 1 ? parts[parts.length - 1].toLowerCase() : 'jpg';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取MIME类型
|
|
||||||
*/
|
|
||||||
private _getMimeType(filePath: string): string {
|
|
||||||
const ext = this._getFileExtension(filePath);
|
|
||||||
const mimeTypes: Record<string, string> = {
|
|
||||||
'jpg': 'image/jpeg',
|
|
||||||
'jpeg': 'image/jpeg',
|
|
||||||
'png': 'image/png',
|
|
||||||
'gif': 'image/gif',
|
|
||||||
'webp': 'image/webp',
|
|
||||||
'bmp': 'image/bmp',
|
|
||||||
'svg': 'image/svg+xml'
|
|
||||||
};
|
|
||||||
return mimeTypes[ext] || 'image/jpeg';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置基础URL
|
|
||||||
*/
|
|
||||||
setBaseUrl(baseUrl: string): void {
|
|
||||||
this.baseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取基础URL
|
|
||||||
*/
|
|
||||||
getBaseUrl(): string {
|
|
||||||
return this.baseUrl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 导出默认实例
|
|
||||||
export const imageUploader = new ImageUploader();
|
|
||||||
Reference in New Issue
Block a user