Merge branch 'master' of ssh://gitea.bowongai.com:222/bowong/bw-mini-app
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
// 注意:由于小程序的限制,tabBar文本和页面标题需要在运行时动态设置
|
||||
// 这里保留中文作为默认值,实际的国际化会在运行时处理
|
||||
export default defineAppConfig({
|
||||
pages: [
|
||||
'pages/home/index', // 新的模板卡片首页
|
||||
'pages/history/index', // 历史记录页面
|
||||
'pages/result/index',
|
||||
'pages/friends-photo/index', // 好友合照页面
|
||||
'pages/result/index',
|
||||
],
|
||||
tabBar: {
|
||||
color: '#8E9BAE',
|
||||
@@ -13,13 +15,13 @@ export default defineAppConfig({
|
||||
list: [
|
||||
{
|
||||
pagePath: 'pages/home/index',
|
||||
text: '游乐场',
|
||||
text: '游乐场', // 运行时会被替换为对应语言
|
||||
iconPath: './assets/icons/playground.png',
|
||||
selectedIconPath: './assets/icons/playground-selected.png',
|
||||
},
|
||||
{
|
||||
pagePath: 'pages/history/index',
|
||||
text: '我的',
|
||||
text: '我的', // 运行时会被替换为对应语言
|
||||
iconPath: './assets/icons/user.png',
|
||||
selectedIconPath: './assets/icons/user-selected.png',
|
||||
},
|
||||
@@ -28,12 +30,12 @@ export default defineAppConfig({
|
||||
window: {
|
||||
backgroundTextStyle: 'light',
|
||||
navigationBarBackgroundColor: '#fff',
|
||||
navigationBarTitleText: '图生视频',
|
||||
navigationBarTitleText: '图生视频', // 运行时会被替换为对应语言
|
||||
navigationBarTextStyle: 'black',
|
||||
},
|
||||
permission: {
|
||||
'scope.album': {
|
||||
desc: '用于保存生成的图片和视频到相册',
|
||||
desc: '用于保存生成的图片和视频到相册', // 运行时会被替换为对应语言
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
10
src/app.tsx
10
src/app.tsx
@@ -5,11 +5,21 @@ import configStore from './store'
|
||||
|
||||
import './app.css'
|
||||
import { createPlatformFactory } from './platforms'
|
||||
import { initLanguageFromStorage } from './i18n/utils'
|
||||
import { i18nManager } from './i18n/manager'
|
||||
|
||||
const store = configStore()
|
||||
|
||||
function App({ children }: PropsWithChildren<any>) {
|
||||
useLaunch(async () => {
|
||||
// 初始化国际化
|
||||
try {
|
||||
await initLanguageFromStorage()
|
||||
await i18nManager.initializeApp()
|
||||
} catch (error) {
|
||||
console.error('i18n初始化失败:', error)
|
||||
}
|
||||
|
||||
const authorize = createPlatformFactory().createAuthorize()
|
||||
const payment = createPlatformFactory().createPayment()
|
||||
try {
|
||||
|
||||
BIN
src/assets/icons/photo.png
Normal file
BIN
src/assets/icons/photo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
76
src/components/LanguageSwitcher/index.css
Normal file
76
src/components/LanguageSwitcher/index.css
Normal file
@@ -0,0 +1,76 @@
|
||||
/* 语言切换组件样式 */
|
||||
.language-switcher {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px 16px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 20px;
|
||||
border: 1px solid #e9ecef;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.language-switcher:hover {
|
||||
background-color: #e9ecef;
|
||||
border-color: #dee2e6;
|
||||
}
|
||||
|
||||
.language-switcher:active {
|
||||
background-color: #dee2e6;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.language-switcher-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.language-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.language-text {
|
||||
font-size: 14px;
|
||||
color: #495057;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.language-arrow {
|
||||
font-size: 10px;
|
||||
color: #6c757d;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.language-switcher:active .language-arrow {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* 小尺寸变体 */
|
||||
.language-switcher.small {
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
.language-switcher.small .language-text {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.language-switcher.small .language-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 紧凑变体 */
|
||||
.language-switcher.compact {
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.language-switcher.compact .language-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.language-switcher.compact .language-arrow {
|
||||
display: none;
|
||||
}
|
||||
89
src/components/LanguageSwitcher/index.tsx
Normal file
89
src/components/LanguageSwitcher/index.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* 语言切换组件
|
||||
*/
|
||||
|
||||
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';
|
||||
import './index.css';
|
||||
|
||||
interface LanguageSwitcherProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const LanguageSwitcher: React.FC<LanguageSwitcherProps> = ({ className = '' }) => {
|
||||
const { currentLanguage, changeLanguage, supportedLanguages, languageNames, t } = useI18n();
|
||||
const [switching, setSwitching] = useState(false);
|
||||
|
||||
const handleLanguageSwitch = async () => {
|
||||
if (switching) return;
|
||||
|
||||
try {
|
||||
setSwitching(true);
|
||||
|
||||
// 显示语言选择菜单
|
||||
const languageOptions = supportedLanguages.map(lang => languageNames[lang]);
|
||||
const currentIndex = supportedLanguages.indexOf(currentLanguage);
|
||||
|
||||
const result = await Taro.showActionSheet({
|
||||
itemList: languageOptions,
|
||||
});
|
||||
|
||||
const selectedLanguage = supportedLanguages[result.tapIndex];
|
||||
|
||||
if (selectedLanguage && selectedLanguage !== currentLanguage) {
|
||||
// 显示切换中的提示
|
||||
Taro.showLoading({
|
||||
title: t('common.loading'),
|
||||
mask: true,
|
||||
});
|
||||
|
||||
// 切换语言
|
||||
await changeLanguage(selectedLanguage);
|
||||
|
||||
// 更新应用级别的i18n设置
|
||||
await i18nManager.onLanguageChange();
|
||||
|
||||
Taro.hideLoading();
|
||||
|
||||
// 显示成功提示
|
||||
Taro.showToast({
|
||||
title: t('common.operationSuccess'),
|
||||
icon: 'success',
|
||||
duration: 1500,
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
Taro.hideLoading();
|
||||
|
||||
// 用户取消选择不显示错误
|
||||
if (error.errMsg && error.errMsg.includes('cancel')) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.error('Language switch failed:', error);
|
||||
Taro.showToast({
|
||||
title: t('common.operationFailed'),
|
||||
icon: 'error',
|
||||
duration: 2000,
|
||||
});
|
||||
} finally {
|
||||
setSwitching(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View className={`language-switcher ${className}`} onClick={handleLanguageSwitch}>
|
||||
<View className="language-switcher-content">
|
||||
<Text className="language-icon">🌐</Text>
|
||||
<Text className="language-text">{languageNames[currentLanguage]}</Text>
|
||||
<Text className="language-arrow">▼</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default LanguageSwitcher;
|
||||
@@ -1,4 +1,5 @@
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import { useI18n } from '../../hooks/useI18n';
|
||||
import './index.css';
|
||||
|
||||
interface LoadingOverlayProps {
|
||||
@@ -6,13 +7,15 @@ interface LoadingOverlayProps {
|
||||
}
|
||||
|
||||
const LoadingOverlay: React.FC<LoadingOverlayProps> = ({ children }) => {
|
||||
const { t } = useI18n();
|
||||
|
||||
return (
|
||||
<View className="loading-overlay">
|
||||
<View className="loading-container">
|
||||
<View className="loading-spinner" />
|
||||
<View className="loading-text">
|
||||
<Text className="loading-title">AI正在生成中...</Text>
|
||||
<Text className="loading-desc">请耐心等待,正在为您精心制作</Text>
|
||||
<Text className="loading-title">{t('loading.aiGenerating')}</Text>
|
||||
<Text className="loading-desc">{t('loading.pleaseWaitDesc')}</Text>
|
||||
</View>
|
||||
{children}
|
||||
</View>
|
||||
|
||||
@@ -2,6 +2,7 @@ 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';
|
||||
|
||||
interface TemplateCardProps {
|
||||
@@ -10,6 +11,7 @@ interface TemplateCardProps {
|
||||
}
|
||||
|
||||
export default function TemplateCard({ template, onClick }: TemplateCardProps) {
|
||||
const { t } = useI18n();
|
||||
const [splitPosition, setSplitPosition] = useState(50); // 分割线位置百分比
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [containerInfo, setContainerInfo] = useState<any>(null);
|
||||
@@ -104,7 +106,7 @@ export default function TemplateCard({ template, onClick }: TemplateCardProps) {
|
||||
<Text className="name-badge">✨{template.name}</Text>
|
||||
</View>
|
||||
<View className="watermark">
|
||||
<Text className="watermark-text">内容由AI生成</Text>
|
||||
<Text className="watermark-text">{t('templates.aiGeneratedContent')}</Text>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
@@ -159,7 +161,7 @@ export default function TemplateCard({ template, onClick }: TemplateCardProps) {
|
||||
<Text className="name-badge">✨{template.name}</Text>
|
||||
</View>
|
||||
<View className="watermark">
|
||||
<Text className="watermark-text">内容由AI生成</Text>
|
||||
<Text className="watermark-text">{t('templates.aiGeneratedContent')}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -2,4 +2,5 @@
|
||||
|
||||
export { useAd } from './useAd'
|
||||
export { useSdk, useServerSdk } from './useSdk'
|
||||
export { useUploadVideo } from './useUploadVideo'
|
||||
export { useUploadVideo } from './useUploadVideo'
|
||||
export { useI18n } from './useI18n'
|
||||
|
||||
55
src/hooks/useI18n.ts
Normal file
55
src/hooks/useI18n.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* i18n React Hook
|
||||
*/
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { Language, languageNames } from '../i18n';
|
||||
import {
|
||||
t,
|
||||
formatNumber,
|
||||
formatDate
|
||||
} from '../i18n/utils';
|
||||
|
||||
export interface UseI18nReturn {
|
||||
/** 当前语言 */
|
||||
currentLanguage: Language;
|
||||
/** 翻译函数 */
|
||||
t: typeof t;
|
||||
/** 切换语言 */
|
||||
changeLanguage: (language: Language) => Promise<void>;
|
||||
/** 支持的语言列表 */
|
||||
supportedLanguages: Language[];
|
||||
/** 语言显示名称 */
|
||||
languageNames: typeof languageNames;
|
||||
/** 格式化数字 */
|
||||
formatNumber: typeof formatNumber;
|
||||
/** 格式化日期 */
|
||||
formatDate: typeof formatDate;
|
||||
/** 是否正在加载 */
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* i18n Hook - 固定为英语
|
||||
*/
|
||||
export const useI18n = (): UseI18nReturn => {
|
||||
const [currentLanguage] = useState<Language>('en-US');
|
||||
const [loading] = useState(false); // 不需要加载,直接设为false
|
||||
|
||||
// 切换语言 - 空实现,因为只支持英语
|
||||
const changeLanguage = useCallback(async () => {
|
||||
// 不执行任何操作,因为只支持英语
|
||||
console.log('Language switching is disabled, only English is supported');
|
||||
}, []);
|
||||
|
||||
return {
|
||||
currentLanguage,
|
||||
t,
|
||||
changeLanguage,
|
||||
supportedLanguages: ['en-US'], // 只支持英语
|
||||
languageNames,
|
||||
formatNumber,
|
||||
formatDate,
|
||||
loading,
|
||||
};
|
||||
};
|
||||
135
src/i18n/example.tsx
Normal file
135
src/i18n/example.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* i18n使用示例
|
||||
* 这个文件展示了如何在不同场景下使用国际化功能
|
||||
*/
|
||||
|
||||
import { View, Text, Button } from '@tarojs/components';
|
||||
import { useState } from 'react';
|
||||
import { useI18n } from '../hooks/useI18n';
|
||||
import LanguageSwitcher from '../components/LanguageSwitcher';
|
||||
|
||||
// 示例1: 基本使用
|
||||
function BasicExample() {
|
||||
const { t } = useI18n();
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Text>{t('common.confirm')}</Text>
|
||||
<Text>{t('home.title')}</Text>
|
||||
<Text>{t('navigation.playground')}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// 示例2: 带参数的翻译
|
||||
function ParameterExample() {
|
||||
const { t } = useI18n();
|
||||
const userName = '张三';
|
||||
const count = 42;
|
||||
|
||||
return (
|
||||
<View>
|
||||
{/* 注意:需要在语言文件中定义支持参数的翻译键 */}
|
||||
<Text>{t('welcome.message', { name: userName })}</Text>
|
||||
<Text>{t('notification.count', { count: count.toString() })}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// 示例3: 语言切换
|
||||
function LanguageSwitchExample() {
|
||||
const { currentLanguage, changeLanguage, supportedLanguages, languageNames } = useI18n();
|
||||
|
||||
const handleLanguageChange = async (language: string) => {
|
||||
try {
|
||||
await changeLanguage(language as any);
|
||||
} catch (error) {
|
||||
console.error('Language change failed:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Text>当前语言: {languageNames[currentLanguage]}</Text>
|
||||
|
||||
{/* 方式1: 使用内置的语言切换组件 */}
|
||||
<LanguageSwitcher />
|
||||
|
||||
{/* 方式2: 自定义语言切换按钮 */}
|
||||
{supportedLanguages.map(lang => (
|
||||
<Button
|
||||
key={lang}
|
||||
onClick={() => handleLanguageChange(lang)}
|
||||
disabled={lang === currentLanguage}
|
||||
>
|
||||
{languageNames[lang]}
|
||||
</Button>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// 示例4: 数字和日期格式化
|
||||
function FormattingExample() {
|
||||
const { formatNumber, formatDate } = useI18n();
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Text>数字: {formatNumber(1234.56)}</Text>
|
||||
<Text>日期: {formatDate(new Date())}</Text>
|
||||
<Text>自定义日期: {formatDate(new Date(), {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// 示例5: 条件渲染基于语言
|
||||
function ConditionalExample() {
|
||||
const { currentLanguage, t } = useI18n();
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Text>{t('common.loading')}</Text>
|
||||
|
||||
{/* 根据语言显示不同内容 */}
|
||||
{currentLanguage === 'zh-CN' && (
|
||||
<Text>这是中文特有的内容</Text>
|
||||
)}
|
||||
|
||||
{currentLanguage === 'en-US' && (
|
||||
<Text>This is English-specific content</Text>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// 示例6: 在类组件中使用(如果需要)
|
||||
import { Component } from 'react';
|
||||
import { getCurrentLanguage, t } from '../i18n/utils';
|
||||
|
||||
class ClassComponentExample extends Component {
|
||||
state = {
|
||||
currentLanguage: getCurrentLanguage(),
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View>
|
||||
<Text>{t('common.confirm')}</Text>
|
||||
<Text>当前语言: {this.state.currentLanguage}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
BasicExample,
|
||||
ParameterExample,
|
||||
LanguageSwitchExample,
|
||||
FormattingExample,
|
||||
ConditionalExample,
|
||||
ClassComponentExample,
|
||||
};
|
||||
122
src/i18n/index.ts
Normal file
122
src/i18n/index.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* 国际化配置文件
|
||||
* 支持多语言切换和动态加载
|
||||
*/
|
||||
|
||||
export type Language = 'zh-CN' | 'en-US' | 'ja-JP' | 'ko-KR';
|
||||
|
||||
export interface I18nConfig {
|
||||
defaultLanguage: Language;
|
||||
fallbackLanguage: Language;
|
||||
supportedLanguages: Language[];
|
||||
storageKey: string;
|
||||
}
|
||||
|
||||
export const i18nConfig: I18nConfig = {
|
||||
defaultLanguage: 'en-US',
|
||||
fallbackLanguage: 'en-US',
|
||||
supportedLanguages: ['en-US'],
|
||||
storageKey: 'app_language',
|
||||
};
|
||||
|
||||
// 语言显示名称映射
|
||||
export const languageNames: Record<Language, string> = {
|
||||
'zh-CN': '简体中文',
|
||||
'en-US': 'English',
|
||||
'ja-JP': '日本語',
|
||||
'ko-KR': '한국어',
|
||||
};
|
||||
|
||||
// 语言资源类型定义
|
||||
export interface LanguageResources {
|
||||
common: {
|
||||
confirm: string;
|
||||
cancel: string;
|
||||
ok: string;
|
||||
loading: string;
|
||||
retry: string;
|
||||
success: string;
|
||||
error: string;
|
||||
warning: string;
|
||||
tips: string;
|
||||
pleaseWait: string;
|
||||
networkError: string;
|
||||
unknownError: string;
|
||||
operationSuccess: string;
|
||||
operationFailed: string;
|
||||
};
|
||||
navigation: {
|
||||
playground: string;
|
||||
mine: string;
|
||||
history: string;
|
||||
home: string;
|
||||
};
|
||||
home: {
|
||||
title: string;
|
||||
loadTemplatesFailed: string;
|
||||
refreshSuccess: string;
|
||||
refreshFailed: string;
|
||||
imageAuditFailed: string;
|
||||
imageAuditNotPassed: string;
|
||||
imageContentNotCompliant: string;
|
||||
imageNeedsManualReview: string;
|
||||
imageAuditUncertain: string;
|
||||
imageAuditPassed: string;
|
||||
imageAuditFailedRetry: string;
|
||||
waitForTaskCompletion: string;
|
||||
userConfirmedAuditFailure: string;
|
||||
};
|
||||
friendsPhoto: {
|
||||
title: string;
|
||||
usageTips: string;
|
||||
usageTipsContent: string;
|
||||
iKnow: string;
|
||||
selectingImage: string;
|
||||
uploading: string;
|
||||
uploadSuccess: string;
|
||||
uploadFailed: string;
|
||||
needAlbumPermission: string;
|
||||
uploadTimeout: string;
|
||||
networkConnectionFailed: string;
|
||||
uploadFailedRetry: string;
|
||||
uploadTwoImages: string;
|
||||
aiGenerating: string;
|
||||
waitForTaskCompletion: string;
|
||||
firstPhoto: string;
|
||||
selectYourPhoto: string;
|
||||
secondPhoto: string;
|
||||
selectFriendPhoto: string;
|
||||
clickToChange: string;
|
||||
clickToUpload: string;
|
||||
friendsPhotoGeneration: string;
|
||||
uploadTwoPhotosDesc: string;
|
||||
photoPreview: string;
|
||||
confirmAndGenerate: string;
|
||||
photo1: string;
|
||||
photo2: string;
|
||||
generating: string;
|
||||
startGenerating: string;
|
||||
};
|
||||
templates: {
|
||||
aiGeneratedContent: string;
|
||||
};
|
||||
loading: {
|
||||
aiGenerating: string;
|
||||
pleaseWaitDesc: string;
|
||||
};
|
||||
audit: {
|
||||
imageContentNotCompliant: string;
|
||||
imageNeedsManualReview: string;
|
||||
imageAuditUncertain: string;
|
||||
imageAuditPassed: string;
|
||||
imageAuditFailed: string;
|
||||
imageAuditNotPassed: string;
|
||||
iKnow: string;
|
||||
};
|
||||
history: {
|
||||
title: string;
|
||||
noRecords: string;
|
||||
loadFailed: string;
|
||||
processingFailed: string;
|
||||
};
|
||||
}
|
||||
94
src/i18n/locales/en-US.ts
Normal file
94
src/i18n/locales/en-US.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { LanguageResources } from '../index';
|
||||
|
||||
export const enUS: LanguageResources = {
|
||||
common: {
|
||||
confirm: 'Confirm',
|
||||
cancel: 'Cancel',
|
||||
ok: 'OK',
|
||||
loading: 'Loading...',
|
||||
retry: 'Retry',
|
||||
success: 'Success',
|
||||
error: 'Error',
|
||||
warning: 'Warning',
|
||||
tips: 'Tips',
|
||||
pleaseWait: 'Please wait...',
|
||||
networkError: 'Network connection failed',
|
||||
unknownError: 'Unknown error',
|
||||
operationSuccess: 'Operation successful',
|
||||
operationFailed: 'Operation failed',
|
||||
},
|
||||
navigation: {
|
||||
playground: 'Playground',
|
||||
mine: 'Mine',
|
||||
history: 'History',
|
||||
home: 'Home',
|
||||
},
|
||||
home: {
|
||||
title: 'Image to Video',
|
||||
loadTemplatesFailed: 'Failed to load templates',
|
||||
refreshSuccess: 'Refresh successful',
|
||||
refreshFailed: 'Refresh failed',
|
||||
imageAuditFailed: 'Image audit failed, please retry',
|
||||
imageAuditNotPassed: 'Image audit not passed',
|
||||
imageContentNotCompliant: 'Image content does not comply with regulations, please select a compliant image',
|
||||
imageNeedsManualReview: 'Image needs manual review, please try again later or select another image',
|
||||
imageAuditUncertain: 'Image audit result is uncertain, please select another image',
|
||||
imageAuditPassed: 'Image audit passed',
|
||||
imageAuditFailedRetry: 'Image audit failed, please select another image',
|
||||
waitForTaskCompletion: 'Please wait for the current task to complete before trying again',
|
||||
userConfirmedAuditFailure: 'User confirmed audit failure information',
|
||||
},
|
||||
friendsPhoto: {
|
||||
title: 'Friends Photo',
|
||||
usageTips: 'Usage Tips',
|
||||
usageTipsContent: '• Clear face photos work better\n• Choose photos with good lighting\n• Supports JPG, PNG formats',
|
||||
iKnow: 'I Know',
|
||||
selectingImage: 'Selecting image...',
|
||||
uploading: 'Uploading...',
|
||||
uploadSuccess: 'Upload successful',
|
||||
uploadFailed: 'Image upload failed',
|
||||
needAlbumPermission: 'Need permission to access album',
|
||||
uploadTimeout: 'Upload timeout, please retry',
|
||||
networkConnectionFailed: 'Network connection failed',
|
||||
uploadFailedRetry: 'Image upload failed, please retry',
|
||||
uploadTwoImages: 'Please upload two images first',
|
||||
aiGenerating: 'AI is generating...',
|
||||
waitForTaskCompletion: 'Please wait for the current task to complete before trying again',
|
||||
firstPhoto: 'First Photo',
|
||||
selectYourPhoto: 'Select your photo',
|
||||
secondPhoto: 'Second Photo',
|
||||
selectFriendPhoto: 'Select friend\'s photo',
|
||||
clickToChange: 'Click to change',
|
||||
clickToUpload: 'Click to upload',
|
||||
friendsPhotoGeneration: 'Friends Photo Generation',
|
||||
uploadTwoPhotosDesc: 'Upload two photos, AI will generate an amazing photo video for you',
|
||||
photoPreview: 'Photo Preview',
|
||||
confirmAndGenerate: 'Confirm and start generation',
|
||||
photo1: 'Photo 1',
|
||||
photo2: 'Photo 2',
|
||||
generating: 'Generating...',
|
||||
startGenerating: 'Start Generating Photo',
|
||||
},
|
||||
templates: {
|
||||
aiGeneratedContent: 'Content generated by AI',
|
||||
},
|
||||
loading: {
|
||||
aiGenerating: 'AI is generating...',
|
||||
pleaseWaitDesc: 'Please be patient, we are crafting something special for you',
|
||||
},
|
||||
audit: {
|
||||
imageContentNotCompliant: 'Image content does not comply with regulations, please select a compliant image',
|
||||
imageNeedsManualReview: 'Image needs manual review, please try again later or select another image',
|
||||
imageAuditUncertain: 'Image audit result is uncertain, please select another image',
|
||||
imageAuditPassed: 'Image audit passed',
|
||||
imageAuditFailed: 'Image audit failed, please select another image',
|
||||
imageAuditNotPassed: 'Image audit not passed',
|
||||
iKnow: 'I Know',
|
||||
},
|
||||
history: {
|
||||
title: 'My Works',
|
||||
noRecords: 'No creation history, go create',
|
||||
loadFailed: 'Failed to load records',
|
||||
processingFailed: 'Processing failed',
|
||||
},
|
||||
};
|
||||
18
src/i18n/locales/index.ts
Normal file
18
src/i18n/locales/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* 语言资源导出文件
|
||||
*/
|
||||
|
||||
import { Language, LanguageResources } from '../index';
|
||||
import { zhCN } from './zh-CN';
|
||||
import { enUS } from './en-US';
|
||||
import { jaJP } from './ja-JP';
|
||||
import { koKR } from './ko-KR';
|
||||
|
||||
export const locales: Record<Language, LanguageResources> = {
|
||||
'zh-CN': zhCN,
|
||||
'en-US': enUS,
|
||||
'ja-JP': jaJP,
|
||||
'ko-KR': koKR,
|
||||
};
|
||||
|
||||
export { zhCN, enUS, jaJP, koKR };
|
||||
94
src/i18n/locales/ja-JP.ts
Normal file
94
src/i18n/locales/ja-JP.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { LanguageResources } from '../index';
|
||||
|
||||
export const jaJP: LanguageResources = {
|
||||
common: {
|
||||
confirm: '確認',
|
||||
cancel: 'キャンセル',
|
||||
ok: 'わかりました',
|
||||
loading: '読み込み中...',
|
||||
retry: '再試行',
|
||||
success: '成功',
|
||||
error: 'エラー',
|
||||
warning: '警告',
|
||||
tips: 'ヒント',
|
||||
pleaseWait: 'しばらくお待ちください...',
|
||||
networkError: 'ネットワーク接続に失敗しました',
|
||||
unknownError: '不明なエラー',
|
||||
operationSuccess: '操作が成功しました',
|
||||
operationFailed: '操作に失敗しました',
|
||||
},
|
||||
navigation: {
|
||||
playground: 'プレイグラウンド',
|
||||
mine: 'マイページ',
|
||||
history: '履歴',
|
||||
home: 'ホーム',
|
||||
},
|
||||
home: {
|
||||
title: '画像から動画',
|
||||
loadTemplatesFailed: 'テンプレートの読み込みに失敗しました',
|
||||
refreshSuccess: '更新が成功しました',
|
||||
refreshFailed: '更新に失敗しました',
|
||||
imageAuditFailed: '画像審査に失敗しました。再試行してください',
|
||||
imageAuditNotPassed: '画像審査に通りませんでした',
|
||||
imageContentNotCompliant: '画像内容が規範に適合していません。規範に適合した画像を再選択してください',
|
||||
imageNeedsManualReview: '画像は人工審査が必要です。しばらく後に再試行するか、他の画像に変更してください',
|
||||
imageAuditUncertain: '画像審査結果が不確定です。画像を再選択してください',
|
||||
imageAuditPassed: '画像審査に通りました',
|
||||
imageAuditFailedRetry: '画像審査に失敗しました。画像を再選択してください',
|
||||
waitForTaskCompletion: '生成中のタスクが完了するまでお待ちください',
|
||||
userConfirmedAuditFailure: 'ユーザーが審査失敗情報を確認しました',
|
||||
},
|
||||
friendsPhoto: {
|
||||
title: '友達の写真',
|
||||
usageTips: '使用のヒント',
|
||||
usageTipsContent: '• 鮮明な顔写真の方が効果的です\n• 明るい写真を選択することをお勧めします\n• JPG、PNG形式をサポートしています',
|
||||
iKnow: 'わかりました',
|
||||
selectingImage: '画像を選択中...',
|
||||
uploading: 'アップロード中...',
|
||||
uploadSuccess: 'アップロード成功',
|
||||
uploadFailed: '画像のアップロードに失敗しました',
|
||||
needAlbumPermission: 'アルバムへのアクセス許可が必要です',
|
||||
uploadTimeout: 'アップロードがタイムアウトしました。再試行してください',
|
||||
networkConnectionFailed: 'ネットワーク接続に失敗しました',
|
||||
uploadFailedRetry: '画像のアップロードに失敗しました。再試行してください',
|
||||
uploadTwoImages: 'まず2枚の画像をアップロードしてください',
|
||||
aiGenerating: 'AIが生成中...',
|
||||
waitForTaskCompletion: '生成中のタスクが完了するまでお待ちください',
|
||||
firstPhoto: '1枚目の写真',
|
||||
selectYourPhoto: 'あなたの写真を選択',
|
||||
secondPhoto: '2枚目の写真',
|
||||
selectFriendPhoto: '友達の写真を選択',
|
||||
clickToChange: 'クリックして変更',
|
||||
clickToUpload: 'クリックしてアップロード',
|
||||
friendsPhotoGeneration: '友達の写真生成',
|
||||
uploadTwoPhotosDesc: '2枚の写真をアップロードすると、AIが素晴らしい合成動画を生成します',
|
||||
photoPreview: '写真プレビュー',
|
||||
confirmAndGenerate: '確認後、生成を開始できます',
|
||||
photo1: '写真 1',
|
||||
photo2: '写真 2',
|
||||
generating: '生成中...',
|
||||
startGenerating: '写真生成を開始',
|
||||
},
|
||||
templates: {
|
||||
aiGeneratedContent: 'AI生成コンテンツ',
|
||||
},
|
||||
loading: {
|
||||
aiGenerating: 'AIが生成中...',
|
||||
pleaseWaitDesc: 'お待ちください。特別なものを作成しています',
|
||||
},
|
||||
audit: {
|
||||
imageContentNotCompliant: '画像内容が規範に適合していません。規範に適合した画像を再選択してください',
|
||||
imageNeedsManualReview: '画像は人工審査が必要です。しばらく後に再試行するか、他の画像に変更してください',
|
||||
imageAuditUncertain: '画像審査結果が不確定です。画像を再選択してください',
|
||||
imageAuditPassed: '画像審査に通りました',
|
||||
imageAuditFailed: '画像審査に失敗しました。画像を再選択してください',
|
||||
imageAuditNotPassed: '画像審査に通りませんでした',
|
||||
iKnow: 'わかりました',
|
||||
},
|
||||
history: {
|
||||
title: '私の作品',
|
||||
noRecords: '作成履歴がありません。作成しに行きましょう',
|
||||
loadFailed: 'レコードの読み込みに失敗しました',
|
||||
processingFailed: '処理に失敗しました',
|
||||
},
|
||||
};
|
||||
94
src/i18n/locales/ko-KR.ts
Normal file
94
src/i18n/locales/ko-KR.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { LanguageResources } from '../index';
|
||||
|
||||
export const koKR: LanguageResources = {
|
||||
common: {
|
||||
confirm: '확인',
|
||||
cancel: '취소',
|
||||
ok: '알겠습니다',
|
||||
loading: '로딩 중...',
|
||||
retry: '재시도',
|
||||
success: '성공',
|
||||
error: '오류',
|
||||
warning: '경고',
|
||||
tips: '팁',
|
||||
pleaseWait: '잠시만 기다려주세요...',
|
||||
networkError: '네트워크 연결 실패',
|
||||
unknownError: '알 수 없는 오류',
|
||||
operationSuccess: '작업 성공',
|
||||
operationFailed: '작업 실패',
|
||||
},
|
||||
navigation: {
|
||||
playground: '플레이그라운드',
|
||||
mine: '내 정보',
|
||||
history: '기록',
|
||||
home: '홈',
|
||||
},
|
||||
home: {
|
||||
title: '이미지에서 비디오로',
|
||||
loadTemplatesFailed: '템플릿 로드 실패',
|
||||
refreshSuccess: '새로고침 성공',
|
||||
refreshFailed: '새로고침 실패',
|
||||
imageAuditFailed: '이미지 심사 실패, 다시 시도해주세요',
|
||||
imageAuditNotPassed: '이미지 심사 통과하지 못함',
|
||||
imageContentNotCompliant: '이미지 내용이 규정에 맞지 않습니다. 규정에 맞는 이미지를 다시 선택해주세요',
|
||||
imageNeedsManualReview: '이미지는 수동 심사가 필요합니다. 나중에 다시 시도하거나 다른 이미지로 변경해주세요',
|
||||
imageAuditUncertain: '이미지 심사 결과가 불확실합니다. 이미지를 다시 선택해주세요',
|
||||
imageAuditPassed: '이미지 심사 통과',
|
||||
imageAuditFailedRetry: '이미지 심사 실패, 이미지를 다시 선택해주세요',
|
||||
waitForTaskCompletion: '생성 중인 작업이 완료될 때까지 기다려주세요',
|
||||
userConfirmedAuditFailure: '사용자가 심사 실패 정보를 확인했습니다',
|
||||
},
|
||||
friendsPhoto: {
|
||||
title: '친구 사진',
|
||||
usageTips: '사용 팁',
|
||||
usageTipsContent: '• 선명한 얼굴 사진이 더 좋은 효과를 냅니다\n• 밝은 사진을 선택하는 것을 권장합니다\n• JPG, PNG 형식을 지원합니다',
|
||||
iKnow: '알겠습니다',
|
||||
selectingImage: '이미지 선택 중...',
|
||||
uploading: '업로드 중...',
|
||||
uploadSuccess: '업로드 성공',
|
||||
uploadFailed: '이미지 업로드 실패',
|
||||
needAlbumPermission: '앨범 접근 권한이 필요합니다',
|
||||
uploadTimeout: '업로드 시간 초과, 다시 시도해주세요',
|
||||
networkConnectionFailed: '네트워크 연결 실패',
|
||||
uploadFailedRetry: '이미지 업로드 실패, 다시 시도해주세요',
|
||||
uploadTwoImages: '먼저 두 장의 이미지를 업로드해주세요',
|
||||
aiGenerating: 'AI가 생성 중...',
|
||||
waitForTaskCompletion: '생성 중인 작업이 완료될 때까지 기다려주세요',
|
||||
firstPhoto: '첫 번째 사진',
|
||||
selectYourPhoto: '당신의 사진을 선택하세요',
|
||||
secondPhoto: '두 번째 사진',
|
||||
selectFriendPhoto: '친구의 사진을 선택하세요',
|
||||
clickToChange: '클릭하여 변경',
|
||||
clickToUpload: '클릭하여 업로드',
|
||||
friendsPhotoGeneration: '친구 사진 생성',
|
||||
uploadTwoPhotosDesc: '두 장의 사진을 업로드하면 AI가 멋진 합성 비디오를 생성합니다',
|
||||
photoPreview: '사진 미리보기',
|
||||
confirmAndGenerate: '확인 후 생성을 시작할 수 있습니다',
|
||||
photo1: '사진 1',
|
||||
photo2: '사진 2',
|
||||
generating: '생성 중...',
|
||||
startGenerating: '사진 생성 시작',
|
||||
},
|
||||
templates: {
|
||||
aiGeneratedContent: 'AI 생성 콘텐츠',
|
||||
},
|
||||
loading: {
|
||||
aiGenerating: 'AI가 생성 중...',
|
||||
pleaseWaitDesc: '잠시만 기다려주세요. 특별한 것을 만들고 있습니다',
|
||||
},
|
||||
audit: {
|
||||
imageContentNotCompliant: '이미지 내용이 규정에 맞지 않습니다. 규정에 맞는 이미지를 다시 선택해주세요',
|
||||
imageNeedsManualReview: '이미지는 수동 심사가 필요합니다. 나중에 다시 시도하거나 다른 이미지로 변경해주세요',
|
||||
imageAuditUncertain: '이미지 심사 결과가 불확실합니다. 이미지를 다시 선택해주세요',
|
||||
imageAuditPassed: '이미지 심사 통과',
|
||||
imageAuditFailed: '이미지 심사 실패, 이미지를 다시 선택해주세요',
|
||||
imageAuditNotPassed: '이미지 심사 통과하지 못함',
|
||||
iKnow: '알겠습니다',
|
||||
},
|
||||
history: {
|
||||
title: '내 작품',
|
||||
noRecords: '생성 기록이 없습니다. 만들어 보세요',
|
||||
loadFailed: '레코드 로드 실패',
|
||||
processingFailed: '처리 실패',
|
||||
},
|
||||
};
|
||||
94
src/i18n/locales/zh-CN.ts
Normal file
94
src/i18n/locales/zh-CN.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { LanguageResources } from '../index';
|
||||
|
||||
export const zhCN: LanguageResources = {
|
||||
common: {
|
||||
confirm: '确定',
|
||||
cancel: '取消',
|
||||
ok: '我知道了',
|
||||
loading: '加载中...',
|
||||
retry: '重试',
|
||||
success: '成功',
|
||||
error: '错误',
|
||||
warning: '警告',
|
||||
tips: '提示',
|
||||
pleaseWait: '请稍后...',
|
||||
networkError: '网络连接失败',
|
||||
unknownError: '未知错误',
|
||||
operationSuccess: '操作成功',
|
||||
operationFailed: '操作失败',
|
||||
},
|
||||
navigation: {
|
||||
playground: '游乐场',
|
||||
mine: '我的',
|
||||
history: '历史记录',
|
||||
home: '首页',
|
||||
},
|
||||
home: {
|
||||
title: '图生视频',
|
||||
loadTemplatesFailed: '加载模板失败',
|
||||
refreshSuccess: '刷新成功',
|
||||
refreshFailed: '刷新失败',
|
||||
imageAuditFailed: '图片审核失败,请重试',
|
||||
imageAuditNotPassed: '图片审核未通过',
|
||||
imageContentNotCompliant: '图片内容不符合规范,请重新选择符合规范的图片',
|
||||
imageNeedsManualReview: '图片需要人工审核,请稍后重试或更换其他图片',
|
||||
imageAuditUncertain: '图片审核结果不确定,请重新选择图片',
|
||||
imageAuditPassed: '图片审核通过',
|
||||
imageAuditFailedRetry: '图片审核失败,请重新选择图片',
|
||||
waitForTaskCompletion: '请等待生成中的任务完成后再尝试',
|
||||
userConfirmedAuditFailure: '用户确认审核失败信息',
|
||||
},
|
||||
friendsPhoto: {
|
||||
title: '好友合照',
|
||||
usageTips: '使用小贴士',
|
||||
usageTipsContent: '• 选择清晰的人脸照片效果更佳\n• 建议选择光线充足的照片\n• 支持JPG、PNG格式',
|
||||
iKnow: '我知道了',
|
||||
selectingImage: '选择图片中...',
|
||||
uploading: '正在上传...',
|
||||
uploadSuccess: '上传成功',
|
||||
uploadFailed: '图片上传失败',
|
||||
needAlbumPermission: '需要授权访问相册',
|
||||
uploadTimeout: '上传超时,请重试',
|
||||
networkConnectionFailed: '网络连接失败',
|
||||
uploadFailedRetry: '图片上传失败,请重试',
|
||||
uploadTwoImages: '请先上传两张图片',
|
||||
aiGenerating: 'AI正在生成中...',
|
||||
waitForTaskCompletion: '请等待生成中的任务完成后再尝试',
|
||||
firstPhoto: '第一张照片',
|
||||
selectYourPhoto: '选择你的照片',
|
||||
secondPhoto: '第二张照片',
|
||||
selectFriendPhoto: '选择朋友的照片',
|
||||
clickToChange: '点击更换',
|
||||
clickToUpload: '点击上传',
|
||||
friendsPhotoGeneration: '好友合照生成',
|
||||
uploadTwoPhotosDesc: '上传两张照片,AI将为你们生成精彩的合照视频',
|
||||
photoPreview: '照片预览',
|
||||
confirmAndGenerate: '确认无误后即可开始生成',
|
||||
photo1: '照片 1',
|
||||
photo2: '照片 2',
|
||||
generating: '生成中...',
|
||||
startGenerating: '开始生成合照',
|
||||
},
|
||||
templates: {
|
||||
aiGeneratedContent: '内容由AI生成',
|
||||
},
|
||||
loading: {
|
||||
aiGenerating: 'AI正在生成中...',
|
||||
pleaseWaitDesc: '请耐心等待,正在为您精心制作',
|
||||
},
|
||||
audit: {
|
||||
imageContentNotCompliant: '图片内容不符合规范,请重新选择符合规范的图片',
|
||||
imageNeedsManualReview: '图片需要人工审核,请稍后重试或更换其他图片',
|
||||
imageAuditUncertain: '图片审核结果不确定,请重新选择图片',
|
||||
imageAuditPassed: '图片审核通过',
|
||||
imageAuditFailed: '图片审核失败,请重新选择图片',
|
||||
imageAuditNotPassed: '图片审核未通过',
|
||||
iKnow: '我知道了',
|
||||
},
|
||||
history: {
|
||||
title: '我的作品',
|
||||
noRecords: '暂无创作历史,去发布',
|
||||
loadFailed: '加载记录失败',
|
||||
processingFailed: '处理失败',
|
||||
},
|
||||
};
|
||||
110
src/i18n/manager.ts
Normal file
110
src/i18n/manager.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* i18n管理器
|
||||
* 处理应用级别的国际化设置,包括tabBar、导航栏标题等
|
||||
*/
|
||||
|
||||
import Taro from '@tarojs/taro';
|
||||
import { Language } from './index';
|
||||
import { t } from './utils';
|
||||
|
||||
export class I18nManager {
|
||||
private static instance: I18nManager;
|
||||
|
||||
public static getInstance(): I18nManager {
|
||||
if (!I18nManager.instance) {
|
||||
I18nManager.instance = new I18nManager();
|
||||
}
|
||||
return I18nManager.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新TabBar文本
|
||||
*/
|
||||
public async updateTabBarTexts(): Promise<void> {
|
||||
try {
|
||||
// 更新第一个tab(游乐场)
|
||||
await Taro.setTabBarItem({
|
||||
index: 0,
|
||||
text: t('navigation.playground'),
|
||||
});
|
||||
|
||||
// 更新第二个tab(我的)
|
||||
await Taro.setTabBarItem({
|
||||
index: 1,
|
||||
text: t('navigation.mine'),
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('Failed to update tabBar texts:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新导航栏标题
|
||||
* @param pageKey 页面键名
|
||||
*/
|
||||
public async updateNavigationBarTitle(pageKey?: string): Promise<void> {
|
||||
try {
|
||||
let title = '';
|
||||
|
||||
// 根据页面设置不同的标题
|
||||
switch (pageKey) {
|
||||
case 'home':
|
||||
title = t('home.title');
|
||||
break;
|
||||
case 'friends-photo':
|
||||
title = t('friendsPhoto.title');
|
||||
break;
|
||||
case 'history':
|
||||
title = t('navigation.history');
|
||||
break;
|
||||
default:
|
||||
title = t('home.title'); // 默认标题
|
||||
}
|
||||
|
||||
await Taro.setNavigationBarTitle({ title });
|
||||
} catch (error) {
|
||||
console.warn('Failed to update navigation bar title:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化应用国际化设置
|
||||
*/
|
||||
public async initializeApp(): Promise<void> {
|
||||
try {
|
||||
// 更新TabBar文本
|
||||
await this.updateTabBarTexts();
|
||||
|
||||
// 更新当前页面标题
|
||||
const pages = Taro.getCurrentPages();
|
||||
const currentPage = pages[pages.length - 1];
|
||||
|
||||
if (currentPage) {
|
||||
const route = currentPage.route;
|
||||
let pageKey = '';
|
||||
|
||||
if (route?.includes('home')) {
|
||||
pageKey = 'home';
|
||||
} else if (route?.includes('friends-photo')) {
|
||||
pageKey = 'friends-photo';
|
||||
} else if (route?.includes('history')) {
|
||||
pageKey = 'history';
|
||||
}
|
||||
|
||||
await this.updateNavigationBarTitle(pageKey);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize app i18n:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 语言切换时的处理
|
||||
*/
|
||||
public async onLanguageChange(): Promise<void> {
|
||||
await this.initializeApp();
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例实例
|
||||
export const i18nManager = I18nManager.getInstance();
|
||||
121
src/i18n/utils.ts
Normal file
121
src/i18n/utils.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* 国际化工具函数
|
||||
*/
|
||||
|
||||
import Taro from '@tarojs/taro';
|
||||
import { Language, i18nConfig } from './index';
|
||||
import { locales } from './locales';
|
||||
|
||||
// 当前语言状态 - 固定为英语
|
||||
let currentLanguage: Language = 'en-US';
|
||||
|
||||
/**
|
||||
* 获取当前语言
|
||||
*/
|
||||
export const getCurrentLanguage = (): Language => {
|
||||
return currentLanguage;
|
||||
};
|
||||
|
||||
/**
|
||||
* 设置当前语言 - 固定为英语
|
||||
*/
|
||||
export const setCurrentLanguage = async (): Promise<void> => {
|
||||
// 始终使用英语
|
||||
currentLanguage = 'en-US';
|
||||
|
||||
// 保存到本地存储
|
||||
try {
|
||||
await Taro.setStorageSync(i18nConfig.storageKey, 'en-US');
|
||||
} catch (error) {
|
||||
console.error('Failed to save language to storage:', error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 从本地存储初始化语言 - 固定为英语
|
||||
*/
|
||||
export const initLanguageFromStorage = async (): Promise<Language> => {
|
||||
// 始终返回英语,忽略本地存储
|
||||
currentLanguage = 'en-US';
|
||||
return currentLanguage;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取嵌套对象的值
|
||||
*/
|
||||
const getNestedValue = (obj: any, path: string): string => {
|
||||
return path.split('.').reduce((current, key) => {
|
||||
return current && current[key] !== undefined ? current[key] : undefined;
|
||||
}, obj);
|
||||
};
|
||||
|
||||
/**
|
||||
* 翻译函数
|
||||
* @param key 翻译键,支持嵌套路径,如 'common.confirm'
|
||||
* @param params 参数对象,用于字符串插值
|
||||
* @returns 翻译后的文本
|
||||
*/
|
||||
export const t = (key: string, params?: Record<string, string | number>): string => {
|
||||
const currentLocale = locales[currentLanguage];
|
||||
const fallbackLocale = locales[i18nConfig.fallbackLanguage];
|
||||
|
||||
// 尝试从当前语言获取翻译
|
||||
let translation = getNestedValue(currentLocale, key);
|
||||
|
||||
// 如果当前语言没有找到,尝试从回退语言获取
|
||||
if (translation === undefined && currentLanguage !== i18nConfig.fallbackLanguage) {
|
||||
translation = getNestedValue(fallbackLocale, key);
|
||||
}
|
||||
|
||||
// 如果仍然没有找到,返回键名
|
||||
if (translation === undefined) {
|
||||
console.warn(`Translation not found for key: ${key}`);
|
||||
return key;
|
||||
}
|
||||
|
||||
// 处理参数插值
|
||||
if (params && typeof translation === 'string') {
|
||||
return translation.replace(/\{\{(\w+)\}\}/g, (match, paramKey) => {
|
||||
return params[paramKey] !== undefined ? String(params[paramKey]) : match;
|
||||
});
|
||||
}
|
||||
|
||||
return translation;
|
||||
};
|
||||
|
||||
/**
|
||||
* 检测系统语言并设置 - 固定为英语
|
||||
*/
|
||||
export const detectAndSetSystemLanguage = async (): Promise<Language> => {
|
||||
// 始终返回英语,忽略系统语言检测
|
||||
currentLanguage = 'en-US';
|
||||
return 'en-US';
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化数字(根据语言环境) - 固定为英语
|
||||
*/
|
||||
export const formatNumber = (num: number): string => {
|
||||
try {
|
||||
return new Intl.NumberFormat('en-US').format(num);
|
||||
} catch (error) {
|
||||
return num.toString();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化日期(根据语言环境) - 固定为英语
|
||||
*/
|
||||
export const formatDate = (date: Date, options?: Intl.DateTimeFormatOptions): string => {
|
||||
try {
|
||||
const defaultOptions: Intl.DateTimeFormatOptions = {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
};
|
||||
|
||||
return new Intl.DateTimeFormat('en-US', options || defaultOptions).format(date);
|
||||
} catch (error) {
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
};
|
||||
60
src/pages/friends-photo/components/UploadCard/index.css
Normal file
60
src/pages/friends-photo/components/UploadCard/index.css
Normal file
@@ -0,0 +1,60 @@
|
||||
/* 上传卡片 */
|
||||
.upload-card {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.upload-card:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* 上传内容区域 */
|
||||
.upload-content {
|
||||
width: 100%;
|
||||
aspect-ratio: 3/4;
|
||||
background: #f6f7f9;
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 上传占位符 */
|
||||
.upload-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.upload-icon {
|
||||
width: 25%;
|
||||
aspect-ratio: 1;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* 上传的图片 */
|
||||
.upload-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* 标题 */
|
||||
.upload-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
}
|
||||
26
src/pages/friends-photo/components/UploadCard/index.tsx
Normal file
26
src/pages/friends-photo/components/UploadCard/index.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { View, Image } from '@tarojs/components';
|
||||
import './index.css';
|
||||
|
||||
interface UploadCardProps {
|
||||
imageUrl: string;
|
||||
title: string;
|
||||
onUpload: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function UploadCard({ imageUrl, title, onUpload, className = '' }: UploadCardProps) {
|
||||
return (
|
||||
<View className={`upload-card ${className}`} onClick={onUpload}>
|
||||
<View className="upload-content">
|
||||
{imageUrl ? (
|
||||
<Image src={imageUrl} className="upload-image" mode="aspectFill" />
|
||||
) : (
|
||||
<View className="upload-placeholder">
|
||||
<Image src="/assets/icons/photo.png" className="upload-icon" mode="aspectFit" />
|
||||
<View className="upload-title">{title}</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
1
src/pages/friends-photo/hooks/index.ts
Normal file
1
src/pages/friends-photo/hooks/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { useImageUpload } from './useImageUpload';
|
||||
144
src/pages/friends-photo/hooks/useImageUpload.ts
Normal file
144
src/pages/friends-photo/hooks/useImageUpload.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import { useState } from 'react';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { useSdk } from '../../../hooks/index';
|
||||
import { useImageDetectionTaskManager, ImageAuditResult, AuditConclusion } from '../../../hooks/useImageDetectionTaskManager';
|
||||
import { useI18n } from '../../../hooks/useI18n';
|
||||
|
||||
export function useImageUpload() {
|
||||
const [image1, setImage1] = useState<string>('');
|
||||
const [image2, setImage2] = useState<string>('');
|
||||
const sdk = useSdk();
|
||||
const { submitAuditTask } = useImageDetectionTaskManager();
|
||||
const { t } = useI18n();
|
||||
|
||||
// 处理审核失败
|
||||
const handleAuditFailure = (auditResult: ImageAuditResult) => {
|
||||
const messages: Record<AuditConclusion, string> = {
|
||||
[AuditConclusion.REJECT]: t('audit.imageContentNotCompliant'),
|
||||
[AuditConclusion.REVIEW]: t('audit.imageNeedsManualReview'),
|
||||
[AuditConclusion.UNCERTAIN]: t('audit.imageAuditUncertain'),
|
||||
[AuditConclusion.PASS]: t('audit.imageAuditPassed'),
|
||||
};
|
||||
|
||||
const message = auditResult.conclusion ? messages[auditResult.conclusion] : t('audit.imageAuditFailed');
|
||||
|
||||
Taro.showModal({
|
||||
title: t('audit.imageAuditNotPassed'),
|
||||
content: message,
|
||||
confirmText: t('audit.iKnow'),
|
||||
showCancel: false,
|
||||
});
|
||||
};
|
||||
|
||||
// 上传单张图片
|
||||
const uploadSingleImage = async (imageIndex: 1 | 2) => {
|
||||
try {
|
||||
// 添加触觉反馈
|
||||
Taro.vibrateShort();
|
||||
|
||||
Taro.showLoading({
|
||||
title: t('friendsPhoto.selectingImage'),
|
||||
mask: true,
|
||||
});
|
||||
|
||||
const imageUrl = await sdk.chooseAndUploadImage({
|
||||
count: 1,
|
||||
onImageSelected: () => {
|
||||
Taro.showLoading({
|
||||
title: t('friendsPhoto.uploading'),
|
||||
mask: true,
|
||||
});
|
||||
},
|
||||
onProgress: () => {
|
||||
Taro.showLoading({
|
||||
title: t('friendsPhoto.uploading'),
|
||||
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: `${t('friendsPhoto.uploadSuccess')} ${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 = t('friendsPhoto.uploadFailed');
|
||||
if (error?.message) {
|
||||
errorTitle = error.message;
|
||||
} else if (error?.errMsg) {
|
||||
if (error.errMsg.includes('chooseImage:fail auth')) {
|
||||
errorTitle = t('friendsPhoto.needAlbumPermission');
|
||||
} else if (error.errMsg.includes('timeout')) {
|
||||
errorTitle = t('friendsPhoto.uploadTimeout');
|
||||
} else if (error.errMsg.includes('network')) {
|
||||
errorTitle = t('friendsPhoto.networkConnectionFailed');
|
||||
} else {
|
||||
errorTitle = t('friendsPhoto.uploadFailedRetry');
|
||||
}
|
||||
}
|
||||
|
||||
Taro.showToast({
|
||||
title: errorTitle,
|
||||
icon: 'error',
|
||||
duration: 3000,
|
||||
});
|
||||
|
||||
console.error('图片上传详细错误:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
image1,
|
||||
image2,
|
||||
uploadSingleImage,
|
||||
setImage1,
|
||||
setImage2,
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '好友合照',
|
||||
navigationBarTitleText: '好友合照', // 默认标题,运行时会被i18n替换
|
||||
enableShareAppMessage: true,
|
||||
enableShareTimeline: true,
|
||||
});
|
||||
|
||||
@@ -1,290 +1,22 @@
|
||||
/* 页面容器 */
|
||||
.friends-photo {
|
||||
height: 100vh;
|
||||
background: #f5f6f8;
|
||||
background: #f8f9fa;
|
||||
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%;
|
||||
padding: 40px 14px 88px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 底部间距 */
|
||||
.bottom-spacer {
|
||||
height: 20px;
|
||||
gap: 12px;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 提交区域 */
|
||||
@@ -293,74 +25,44 @@
|
||||
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);
|
||||
padding: 20px 14px;
|
||||
padding-bottom: calc(20px + env(safe-area-inset-bottom));
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* 提交按钮 */
|
||||
.submit-button {
|
||||
width: 100%;
|
||||
height: 56px;
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
|
||||
border-radius: 28px;
|
||||
height: 96px;
|
||||
background: #000;
|
||||
border-radius: 24px;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
outline: none;
|
||||
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;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.submit-text {
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.submit-button.disabled {
|
||||
background-color: #ccc;
|
||||
cursor: not-allowed;
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* 加载动画 */
|
||||
.loading-spinner {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
border: 2px solid rgb(255 255 255 / 30%);
|
||||
border-top: 2px solid white;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
@@ -370,58 +72,8 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,172 +1,60 @@
|
||||
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 { Template } from '@/sdk/sdk-server';
|
||||
import { Button, View } from '@tarojs/components';
|
||||
import Taro, { navigateTo, useRouter } from '@tarojs/taro';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useServerSdk } from '../../hooks/index';
|
||||
import { useI18n } from '../../hooks/useI18n';
|
||||
import { i18nManager } from '../../i18n/manager';
|
||||
|
||||
// 导入组件
|
||||
import UploadCard from './components/UploadCard';
|
||||
|
||||
// 导入hooks
|
||||
import { useImageUpload } from './hooks';
|
||||
|
||||
import './index.css';
|
||||
|
||||
// 好友合照模板的固定代码
|
||||
const FRIENDS_PHOTO_TEMPLATE_CODE = 'friends_photo';
|
||||
|
||||
export default function FriendsPhoto() {
|
||||
const sdk = useSdk();
|
||||
const { templateCode } = useRouter().params;
|
||||
const [template, setTemplate] = useState<Template | null>(null);
|
||||
|
||||
console.log(template);
|
||||
|
||||
const { t } = useI18n();
|
||||
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);
|
||||
|
||||
// 显示使用指南
|
||||
const { image1, image2, uploadSingleImage } = useImageUpload();
|
||||
|
||||
// 设置页面标题
|
||||
useEffect(() => {
|
||||
if (!showGuide) return;
|
||||
i18nManager.updateNavigationBarTitle('friends-photo');
|
||||
}, []);
|
||||
|
||||
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]: '图片审核通过',
|
||||
useEffect(() => {
|
||||
const fetchTemplate = async () => {
|
||||
if (!templateCode) return;
|
||||
const template = await serverSdk.getTemplate(templateCode);
|
||||
setTemplate(template);
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
fetchTemplate();
|
||||
}, [templateCode]);
|
||||
|
||||
// 提交处理
|
||||
const handleSubmit = async () => {
|
||||
if (!templateCode) {
|
||||
Taro.showToast({
|
||||
title: t('friendsPhoto.templateCodeNotSet'),
|
||||
icon: 'error',
|
||||
duration: 2000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!image1 || !image2) {
|
||||
Taro.vibrateShort();
|
||||
Taro.showToast({
|
||||
title: '请先上传两张图片',
|
||||
title: t('friendsPhoto.uploadTwoImages'),
|
||||
icon: 'none',
|
||||
duration: 2000,
|
||||
});
|
||||
@@ -179,7 +67,7 @@ export default function FriendsPhoto() {
|
||||
|
||||
try {
|
||||
Taro.showLoading({
|
||||
title: 'AI正在生成中...',
|
||||
title: t('friendsPhoto.aiGenerating'),
|
||||
mask: true,
|
||||
});
|
||||
|
||||
@@ -188,21 +76,21 @@ export default function FriendsPhoto() {
|
||||
|
||||
const taskId = await serverSdk.executeTemplate({
|
||||
imageUrl: combinedImageUrl,
|
||||
templateCode: FRIENDS_PHOTO_TEMPLATE_CODE,
|
||||
templateCode: templateCode,
|
||||
});
|
||||
|
||||
Taro.hideLoading();
|
||||
|
||||
// 跳转到结果页面
|
||||
navigateTo({
|
||||
url: `/pages/result/index?taskId=${taskId}&templateCode=${FRIENDS_PHOTO_TEMPLATE_CODE}`,
|
||||
url: `/pages/result/index?taskId=${taskId}&templateCode=${templateCode}`,
|
||||
});
|
||||
} catch (error) {
|
||||
Taro.hideLoading();
|
||||
console.error('提交失败:', error);
|
||||
|
||||
Taro.showToast({
|
||||
title: '请等待生成中的任务完成后再尝试',
|
||||
title: t('friendsPhoto.waitForTaskCompletion'),
|
||||
icon: 'none',
|
||||
duration: 2000,
|
||||
});
|
||||
@@ -211,109 +99,30 @@ export default function FriendsPhoto() {
|
||||
}
|
||||
};
|
||||
|
||||
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="upload-section">
|
||||
<UploadCard imageUrl={image1} title={t('friendsPhoto.selectYourPhoto')} onUpload={() => uploadSingleImage(1)} />
|
||||
<UploadCard imageUrl={image2} title={t('friendsPhoto.selectFriendPhoto')} onUpload={() => uploadSingleImage(2)} />
|
||||
</View>
|
||||
|
||||
{/* 固定底部按钮 */}
|
||||
<View className="submit-section">
|
||||
<Button
|
||||
className={`submit-button ${(!image1 || !image2 || loading) ? 'disabled' : ''}`}
|
||||
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-text">{t('friendsPhoto.generating')}</View>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<View className="submit-icon">✨</View>
|
||||
<View className="submit-text">开始生成合照</View>
|
||||
</>
|
||||
<View className="submit-text">
|
||||
${template?.creditCost} {t('friendsPhoto.startGenerating')}
|
||||
</View>
|
||||
)}
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
@@ -9,11 +9,21 @@
|
||||
padding: 32px 0;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 32px;
|
||||
}
|
||||
|
||||
.history-title {
|
||||
font-size: 48px;
|
||||
font-weight: 500;
|
||||
color: #1d1f22;
|
||||
padding: 0 32px;
|
||||
}
|
||||
|
||||
.language-switcher-compact {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.history-list {
|
||||
|
||||
@@ -2,8 +2,12 @@ import { Image, ScrollView, Text, View } from '@tarojs/components';
|
||||
import Taro, { navigateTo } from '@tarojs/taro';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useServerSdk } from '../../hooks/index';
|
||||
import { useI18n } from '../../hooks/useI18n';
|
||||
import { i18nManager } from '../../i18n/manager';
|
||||
// import LanguageSwitcher from '../../components/LanguageSwitcher'; // 已隐藏 - 只支持英语
|
||||
import './index.css';
|
||||
export default function History() {
|
||||
const { t } = useI18n();
|
||||
const [records, setRecords] = useState<any[]>([]);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -33,7 +37,7 @@ export default function History() {
|
||||
} catch (error) {
|
||||
console.error('加载记录失败:', error);
|
||||
Taro.showToast({
|
||||
title: '加载记录失败',
|
||||
title: t('common.operationFailed'),
|
||||
icon: 'error',
|
||||
duration: 2000,
|
||||
});
|
||||
@@ -44,6 +48,8 @@ export default function History() {
|
||||
|
||||
useEffect(() => {
|
||||
loadRecords();
|
||||
// 设置页面标题
|
||||
i18nManager.updateNavigationBarTitle('history');
|
||||
}, []);
|
||||
|
||||
// 定时更新生成中任务的进度
|
||||
@@ -65,14 +71,14 @@ export default function History() {
|
||||
try {
|
||||
await loadRecords();
|
||||
Taro.showToast({
|
||||
title: '刷新成功',
|
||||
title: t('home.refreshSuccess'),
|
||||
icon: 'success',
|
||||
duration: 1500,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('刷新失败:', error);
|
||||
Taro.showToast({
|
||||
title: '刷新失败',
|
||||
title: t('home.refreshFailed'),
|
||||
icon: 'error',
|
||||
duration: 1500,
|
||||
});
|
||||
@@ -91,8 +97,8 @@ export default function History() {
|
||||
} else if (record.status === 'failed') {
|
||||
// 显示错误信息
|
||||
Taro.showModal({
|
||||
title: '处理失败',
|
||||
content: record.errorMessage || '图片处理失败,请重试',
|
||||
title: t('common.operationFailed'),
|
||||
content: record.errorMessage || t('common.operationFailed'),
|
||||
showCancel: false,
|
||||
});
|
||||
} else if (record.status === 'processing') {
|
||||
@@ -117,9 +123,14 @@ export default function History() {
|
||||
|
||||
return (
|
||||
<View className="history">
|
||||
{/* <View className="history-header">
|
||||
<Text className="history-title">我的作品</Text>
|
||||
</View> */}
|
||||
{/* 设置区域 */}
|
||||
<View className="history-header">
|
||||
<View className="header-content">
|
||||
<Text className="history-title">{t('navigation.mine')}</Text>
|
||||
{/* 语言切换器已隐藏 - 只支持英语 */}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<ScrollView
|
||||
className="history-list"
|
||||
scrollY
|
||||
@@ -132,12 +143,12 @@ export default function History() {
|
||||
<View className="history-grid">
|
||||
{loading ? (
|
||||
<View className="loading-state">
|
||||
<Text className="loading-text">加载中...</Text>
|
||||
<Text className="loading-text">{t('common.loading')}</Text>
|
||||
</View>
|
||||
) : records.length === 0 ? (
|
||||
<View className="empty-state">
|
||||
<Image className="empty-icon-image" src="/assets/icons/cloud.png" />
|
||||
<Text className="empty-text">暂无创作历史,去发布</Text>
|
||||
<Text className="empty-text">{t('history.noRecords')}</Text>
|
||||
</View>
|
||||
) : (
|
||||
records.map(record => {
|
||||
@@ -147,17 +158,17 @@ export default function History() {
|
||||
<View key={record.id} className="history-item" onClick={() => handleItemClick(record)}>
|
||||
<View className="item-thumbnail">
|
||||
{record.status === 'completed' && record.outputResult ? (
|
||||
<Image className="thumbnail-image" src={record.inputImageUrl} mode="aspectFill" />
|
||||
<Image className="thumbnail-image" src={record.inputImageUrl.split(',')[0]} mode="aspectFill" />
|
||||
) : (
|
||||
<View className={`thumbnail-placeholder ${record.status}`}>
|
||||
<Image className="thumbnail-image" src={record.inputImageUrl} mode="aspectFill" />
|
||||
<Image className="thumbnail-image" src={record.inputImageUrl.split(',')[0]} mode="aspectFill" />
|
||||
{/* 生成中状态的扫描效果 */}
|
||||
{isGenerating && (
|
||||
<View className="thumbnail-overlay">
|
||||
<View className="thumbnail-progress-overlay" />
|
||||
<View className="thumbnail-scan-light" />
|
||||
{/* 加载loading 转圈 */}
|
||||
<View className='thumbnail-loader' />
|
||||
<View className="thumbnail-loader" />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '游乐场',
|
||||
navigationBarTitleText: '游乐场', // 默认标题,运行时会被i18n替换
|
||||
navigationBarBackgroundColor: '#F5F6F8',
|
||||
navigationBarTextStyle: 'black',
|
||||
backgroundColorTop: '#F5F6F8',
|
||||
|
||||
@@ -4,6 +4,21 @@
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 浮动语言切换器 */
|
||||
.language-switcher-float {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 1000;
|
||||
opacity: 0.8;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.language-switcher-float:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* ScrollView 容器样式 */
|
||||
|
||||
@@ -8,6 +8,9 @@ import { Template } from '../../store/types';
|
||||
import TemplateCard from '../../components/TemplateCard';
|
||||
import { useSdk, useServerSdk } from '../../hooks/index';
|
||||
import { useImageDetectionTaskManager, ImageAuditResult, AuditConclusion } from '../../hooks/useImageDetectionTaskManager';
|
||||
import { useI18n } from '../../hooks/useI18n';
|
||||
import { i18nManager } from '../../i18n/manager';
|
||||
// import LanguageSwitcher from '../../components/LanguageSwitcher'; // 已隐藏 - 只支持英语
|
||||
|
||||
import './index.css';
|
||||
|
||||
@@ -15,6 +18,7 @@ import './index.css';
|
||||
type LoadingState = false | 'uploading' | 'auditing' | 'processing';
|
||||
|
||||
export default function Home() {
|
||||
const { t } = useI18n();
|
||||
const dispatch = useAppDispatch();
|
||||
const templates = useAppSelector(selectTemplates);
|
||||
const sdk = useSdk();
|
||||
@@ -30,7 +34,7 @@ export default function Home() {
|
||||
} catch (error) {
|
||||
console.error('加载模板失败:', error);
|
||||
Taro.showToast({
|
||||
title: '加载模板失败',
|
||||
title: t('home.loadTemplatesFailed'),
|
||||
icon: 'error',
|
||||
duration: 2000,
|
||||
});
|
||||
@@ -43,12 +47,12 @@ export default function Home() {
|
||||
try {
|
||||
await loadTemplates();
|
||||
Taro.showToast({
|
||||
title: '刷新成功',
|
||||
title: t('home.refreshSuccess'),
|
||||
icon: 'success',
|
||||
duration: 1500,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('刷新失败:', error);
|
||||
console.error(t('home.refreshFailed'), error);
|
||||
} finally {
|
||||
setRefreshing(false);
|
||||
}
|
||||
@@ -57,132 +61,50 @@ export default function Home() {
|
||||
useEffect(() => {
|
||||
// 需要先检查是否有模板配置文件
|
||||
loadTemplates();
|
||||
|
||||
// 设置页面标题
|
||||
i18nManager.updateNavigationBarTitle('home');
|
||||
}, [dispatch]);
|
||||
|
||||
// 获取加载状态的显示文本
|
||||
const getLoadingText = (state: LoadingState): string => {
|
||||
switch (state) {
|
||||
default:
|
||||
return '请稍后...';
|
||||
return t('common.pleaseWait');
|
||||
}
|
||||
};
|
||||
|
||||
// 处理审核失败
|
||||
const handleAuditFailure = (auditResult: ImageAuditResult) => {
|
||||
const messages: Record<AuditConclusion, string> = {
|
||||
[AuditConclusion.REJECT]: '图片内容不符合规范,请重新选择符合规范的图片',
|
||||
[AuditConclusion.REVIEW]: '图片需要人工审核,请稍后重试或更换其他图片',
|
||||
[AuditConclusion.UNCERTAIN]: '图片审核结果不确定,请重新选择图片',
|
||||
[AuditConclusion.PASS]: '图片审核通过', // 虽然不会用到,但为了类型完整性
|
||||
[AuditConclusion.REJECT]: t('audit.imageContentNotCompliant'),
|
||||
[AuditConclusion.REVIEW]: t('audit.imageNeedsManualReview'),
|
||||
[AuditConclusion.UNCERTAIN]: t('audit.imageAuditUncertain'),
|
||||
[AuditConclusion.PASS]: t('audit.imageAuditPassed'), // 虽然不会用到,但为了类型完整性
|
||||
};
|
||||
|
||||
const message = auditResult.conclusion ? messages[auditResult.conclusion] : '图片审核失败,请重新选择图片';
|
||||
const message = auditResult.conclusion ? messages[auditResult.conclusion] : t('audit.imageAuditFailed');
|
||||
|
||||
Taro.showModal({
|
||||
title: '图片审核未通过',
|
||||
title: t('audit.imageAuditNotPassed'),
|
||||
content: message,
|
||||
confirmText: '我知道了',
|
||||
confirmText: t('audit.iKnow'),
|
||||
showCancel: false,
|
||||
success: () => {
|
||||
console.log('用户确认审核失败信息');
|
||||
console.log(t('home.userConfirmedAuditFailure'));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleTemplateClick = async (template: Template) => {
|
||||
// 如果是好友合照,跳转到专门的页面
|
||||
if (template.name === '好友合照') {
|
||||
navigateTo({ url: `/pages/friends-photo/index` });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 第一步:选择并上传图片
|
||||
const imageCount = template.code === '2image-to-video' ? 2 : 1;
|
||||
const imageUrl = await sdk.chooseAndUploadImage({
|
||||
count: imageCount,
|
||||
onImageSelected: () => {
|
||||
Taro.showLoading({
|
||||
title: getLoadingText('uploading'),
|
||||
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);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// 第三步:检查审核结果
|
||||
if (auditResult.conclusion === AuditConclusion.PASS) {
|
||||
// 短暂延迟后继续业务处理
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
// 第四步:执行业务逻辑
|
||||
try {
|
||||
const taskId = await serverSdk.executeTemplate({
|
||||
imageUrl,
|
||||
templateCode: template.code,
|
||||
});
|
||||
// 隐藏loading
|
||||
Taro.hideLoading();
|
||||
// 跳转到生成页面
|
||||
navigateTo({
|
||||
url: `/pages/result/index?taskId=${taskId.taskId}&templateCode=${template.code}`,
|
||||
});
|
||||
} catch (businessError) {
|
||||
// 业务处理失败
|
||||
Taro.hideLoading();
|
||||
Taro.showToast({
|
||||
title: '请等待生成中的任务完成后再尝试',
|
||||
icon: 'none',
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// 审核不通过
|
||||
handleAuditFailure(auditResult);
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.errMsg.includes('chooseImage:fail cancel')) {
|
||||
return;
|
||||
}
|
||||
// 统一错误处理
|
||||
Taro.hideLoading();
|
||||
Taro.showToast({
|
||||
title: '图片审核失败,请重试',
|
||||
icon: 'error',
|
||||
duration: 2000,
|
||||
});
|
||||
} finally {
|
||||
}
|
||||
navigateTo({ url: `/pages/friends-photo/index?templateCode=${template.code}` });
|
||||
return;
|
||||
};
|
||||
|
||||
return (
|
||||
<View className="home">
|
||||
{/* 语言切换按钮已隐藏 - 只支持英语 */}
|
||||
|
||||
<ScrollView
|
||||
className="home-scroll"
|
||||
scrollY
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Image, View,Text } from '@tarojs/components';
|
||||
import { Image, View, Text } from '@tarojs/components';
|
||||
import { navigateBack } from '@tarojs/taro';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
@@ -63,7 +63,7 @@ const GeneratingComponent: React.FC<GeneratingComponentProps> = ({ task }) => {
|
||||
<View className="progress-inner">
|
||||
{/* 预览图片或占位符 */}
|
||||
{task?.inputImageUrl ? (
|
||||
<Image className="preview-image" src={task.inputImageUrl} mode="aspectFill" />
|
||||
<Image className="preview-image" src={task.inputImageUrl.split(',')[0]} mode="aspectFill" />
|
||||
) : (
|
||||
<View className="preview-placeholder">
|
||||
<View className="placeholder-icon">🎨</View>
|
||||
@@ -73,7 +73,7 @@ const GeneratingComponent: React.FC<GeneratingComponentProps> = ({ task }) => {
|
||||
<View className="progress-overlay" />
|
||||
{/* 光扫描动画 */}
|
||||
<View className="scan-light" />
|
||||
<View className="scan-progress" >{Math.round(progress)}%</View>
|
||||
<View className="scan-progress">{Math.round(progress)}%</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -26,4 +26,4 @@ export class H5Payment extends Payment {
|
||||
templateCode
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user