feat: 集成图片审核功能到首页业务流程
**核心功能**: - 在首页模板点击流程中集成图片内容审核 - 只有审核通过的图片才能进入业务处理环节 - 审核不通过时显示详细的错误信息和建议 **用户体验优化**: - 添加详细的进度状态提示:上传中 → 审核中 → 审核通过 → 处理中 - 不同阶段的 loading 文案和错误处理 - 防重复点击机制,确保流程的稳定性 **技术实现**: - 使用 LoadingState 类型管理详细的加载状态 - 集成 useImageDetectionTaskManager hook 处理审核逻辑 - Promise 封装审核流程,便于错误处理和状态管理 - 审核结果缓存机制,提升性能 **错误处理**: - 上传失败、审核失败、业务处理失败的分类错误提示 - 审核不通过时的详细原因说明(违规/复审/不确定) - 统一的错误边界处理机制 **修复内容**: - 修复演示页面的 TypeScript 类型错误 - 修复审核结果缓存 key 不匹配问题 - 优化审核结果传递逻辑
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
|
||||
export default defineAppConfig({
|
||||
pages: [
|
||||
'pages/imageaudit-demo/index',
|
||||
'pages/home/index', // 新的模板卡片首页
|
||||
'pages/history/index', // 历史记录页面
|
||||
'pages/generate/index', // 生成处理页面
|
||||
|
||||
@@ -46,14 +46,14 @@ const ImageAudit: React.FC<ImageAuditProps> = ({
|
||||
});
|
||||
|
||||
const tempFilePath = res.tempFilePaths[0];
|
||||
|
||||
|
||||
// 显示本地图片预览
|
||||
setImageUrl(tempFilePath);
|
||||
|
||||
|
||||
// 开始上传
|
||||
setIsUploading(true);
|
||||
setUploadProgress(0);
|
||||
|
||||
|
||||
try {
|
||||
const uploadedUrl = await imageUploader.upload({
|
||||
filePath: tempFilePath,
|
||||
@@ -61,11 +61,11 @@ const ImageAudit: React.FC<ImageAuditProps> = ({
|
||||
setUploadProgress(progress);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// 上传成功,更新为服务器URL
|
||||
setImageUrl(uploadedUrl);
|
||||
onImageUpload?.(uploadedUrl);
|
||||
|
||||
|
||||
Taro.showToast({
|
||||
title: '图片上传成功',
|
||||
icon: 'success'
|
||||
@@ -154,6 +154,7 @@ const ImageAudit: React.FC<ImageAuditProps> = ({
|
||||
|
||||
// 获取审核结果文本
|
||||
const getAuditResultText = (conclusion?: AuditConclusion): string => {
|
||||
console.log({ conclusion })
|
||||
switch (conclusion) {
|
||||
case AuditConclusion.PASS:
|
||||
return '通过';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useEffect, useRef, useCallback } from 'react';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { AsyncTaskManager, TaskStatus, TaskConfig } from '../utils/AsyncTaskManager';
|
||||
|
||||
/**
|
||||
* 平台类型枚举
|
||||
*/
|
||||
@@ -44,7 +43,7 @@ export enum AuditConclusion {
|
||||
*/
|
||||
export interface ImageAuditResult {
|
||||
taskId: string;
|
||||
platform: PlatformType;
|
||||
platform?: PlatformType;
|
||||
status: AuditStatus;
|
||||
conclusion?: AuditConclusion; // 审核结果
|
||||
imageUrl: string;
|
||||
@@ -111,33 +110,35 @@ export function useImageDetectionTaskManager() {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
const { status, conclusion } = response.data;
|
||||
|
||||
// 映射审核状态到任务状态
|
||||
const taskStatus = mapAuditStatusToTaskStatus(status as AuditStatus);
|
||||
|
||||
// 如果审核完成,缓存结果
|
||||
if (status === AuditStatus.COMPLETED) {
|
||||
const auditResult: ImageAuditResult = {
|
||||
taskId,
|
||||
platform,
|
||||
status,
|
||||
conclusion: conclusion as AuditConclusion,
|
||||
imageUrl: '', // 从任务数据中获取
|
||||
result: response.data.result,
|
||||
processingTime: response.data.processingTime,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
const cacheKey = `${platform}_${taskId}`;
|
||||
auditCacheRef.current.set(cacheKey, auditResult);
|
||||
}
|
||||
|
||||
return taskStatus;
|
||||
if (![200, 201].includes(response.statusCode)) {
|
||||
throw new Error(`提交图片审核任务失败: ${response.data?.message || '未知错误'}`);
|
||||
}
|
||||
throw new Error(`API调用失败: ${response.statusCode}`);
|
||||
const data = response.data;
|
||||
const code = data?.code;
|
||||
if (code !== 200) {
|
||||
throw new Error(`提交图片审核任务失败: ${response.data?.message || '未知错误'}`);
|
||||
}
|
||||
const { status, conclusion } = data.data;
|
||||
// 映射审核状态到任务状态
|
||||
const taskStatus = mapAuditStatusToTaskStatus(status as AuditStatus);
|
||||
// 如果审核完成,缓存结果
|
||||
if (status === AuditStatus.COMPLETED) {
|
||||
const auditResult: ImageAuditResult = {
|
||||
taskId,
|
||||
platform,
|
||||
status,
|
||||
conclusion: conclusion as AuditConclusion,
|
||||
imageUrl: '', // 从任务数据中获取
|
||||
result: response.data.result,
|
||||
processingTime: response.data.processingTime,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
console.log(`task status: `, auditResult)
|
||||
// 使用 taskId 作为唯一标识,这样 onSuccess 回调可以找到结果
|
||||
const taskCacheKey = `task_${taskId}`;
|
||||
auditCacheRef.current.set(taskCacheKey, auditResult);
|
||||
}
|
||||
return taskStatus;
|
||||
} catch (error) {
|
||||
console.error('检查图片审核任务状态失败:', error);
|
||||
throw error;
|
||||
@@ -217,11 +218,17 @@ export function useImageDetectionTaskManager() {
|
||||
}
|
||||
});
|
||||
|
||||
if (response.statusCode !== 200) {
|
||||
if (![200, 201].includes(response.statusCode)) {
|
||||
throw new Error(`提交图片审核任务失败: ${response.data?.message || '未知错误'}`);
|
||||
}
|
||||
|
||||
const { taskId } = response.data;
|
||||
const data = response.data;
|
||||
const code = data?.code;
|
||||
if (code !== 200) {
|
||||
throw new Error(`提交图片审核任务失败: ${response.data?.message || '未知错误'}`);
|
||||
}
|
||||
const task = data.data;
|
||||
console.log(`response data is: `, task)
|
||||
const { taskId } = task;
|
||||
|
||||
// 创建任务配置
|
||||
const config: TaskConfig = {
|
||||
@@ -231,7 +238,6 @@ export function useImageDetectionTaskManager() {
|
||||
timeout: options.timeout || 300000, // 自定义超时时间或默认5分钟
|
||||
onProgress: (status, data) => {
|
||||
console.log(`图片审核任务 ${taskId} 状态: ${status}`);
|
||||
|
||||
// 显示不同状态的UI反馈
|
||||
switch (status) {
|
||||
case TaskStatus.RUNNING:
|
||||
@@ -257,18 +263,34 @@ export function useImageDetectionTaskManager() {
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
onProgress?.(status, data?.progress);
|
||||
},
|
||||
onSuccess: async (result) => {
|
||||
console.log(`图片审核任务 ${taskId} 成功完成:`, result);
|
||||
|
||||
// 缓存结果
|
||||
if (options.enableCache) {
|
||||
auditCacheRef.current.set(cacheKey, result);
|
||||
// 从缓存中获取完整的审核结果
|
||||
const taskCacheKey = `task_${taskId}`;
|
||||
const auditResult = auditCacheRef.current.get(taskCacheKey);
|
||||
console.log(`audit result is: ${taskCacheKey}`, auditResult)
|
||||
if (auditResult) {
|
||||
auditResult.imageUrl = processedImageUrl; // 设置图片URL
|
||||
// 同时以 imageUrl 为 key 缓存结果,用于后续的缓存查询
|
||||
if (options.enableCache) {
|
||||
auditCacheRef.current.set(cacheKey, auditResult);
|
||||
}
|
||||
onSuccess?.(auditResult);
|
||||
} else {
|
||||
console.warn(`未找到任务 ${taskId} 的缓存结果`);
|
||||
// 创建基本的结果对象
|
||||
const basicResult: ImageAuditResult = {
|
||||
taskId,
|
||||
platform,
|
||||
status: AuditStatus.COMPLETED,
|
||||
conclusion: AuditConclusion.UNCERTAIN,
|
||||
imageUrl: processedImageUrl,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
onSuccess?.(basicResult);
|
||||
}
|
||||
|
||||
onSuccess?.(result);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(`图片审核任务 ${taskId} 失败:`, error);
|
||||
@@ -283,10 +305,8 @@ export function useImageDetectionTaskManager() {
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 启动任务监控
|
||||
await taskManagerRef.current.startTask(config);
|
||||
|
||||
return taskId;
|
||||
|
||||
} catch (error) {
|
||||
@@ -436,52 +456,7 @@ async function preprocessImage(
|
||||
* 获取当前运行平台
|
||||
*/
|
||||
function getCurrentPlatform(): PlatformType {
|
||||
try {
|
||||
const env = Taro.getEnv();
|
||||
|
||||
console.log('当前平台环境:', env);
|
||||
|
||||
switch (env) {
|
||||
case 'WEAPP':
|
||||
return PlatformType.WECHAT;
|
||||
case 'ALIPAY':
|
||||
return PlatformType.ALIPAY;
|
||||
case 'SWAN':
|
||||
return PlatformType.BAIDU;
|
||||
case 'TT':
|
||||
return PlatformType.BYTEDANCE;
|
||||
case 'JD':
|
||||
return PlatformType.JD;
|
||||
case 'QQ':
|
||||
return PlatformType.QQ;
|
||||
case 'QUICKAPP':
|
||||
return PlatformType.KUAISHOU;
|
||||
case 'RN':
|
||||
return PlatformType.RN;
|
||||
case 'WEB':
|
||||
case 'H5':
|
||||
return PlatformType.H5;
|
||||
default:
|
||||
// 针对抖音小程序的特殊处理
|
||||
if (typeof tt !== 'undefined') {
|
||||
console.log('检测到抖音小程序环境');
|
||||
return PlatformType.BYTEDANCE;
|
||||
}
|
||||
// 如果在浏览器环境
|
||||
if (typeof window !== 'undefined') {
|
||||
return PlatformType.H5;
|
||||
}
|
||||
console.warn('未知平台类型,使用默认字节跳动平台:', env);
|
||||
return PlatformType.BYTEDANCE; // 默认使用字节跳动平台而不是H5
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('无法检测平台类型,使用默认字节跳动平台:', error);
|
||||
// 针对抖音小程序的兜底处理
|
||||
if (typeof tt !== 'undefined') {
|
||||
return PlatformType.BYTEDANCE;
|
||||
}
|
||||
return PlatformType.BYTEDANCE; // 默认使用字节跳动平台
|
||||
}
|
||||
return PlatformType.BYTEDANCE;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,19 +4,23 @@ import Taro, { navigateTo } from '@tarojs/taro'
|
||||
import { useAppDispatch, useAppSelector } from '../../hooks/redux'
|
||||
import { selectTemplates } from '../../selectors/template'
|
||||
import { initTemplates } from '../../actions/template'
|
||||
import { addRecord, updateRecord } from '../../actions/history'
|
||||
import { Template } from '../../store/types'
|
||||
import TemplateCard from '../../components/TemplateCard'
|
||||
import { useSdk, useServerSdk } from '../../hooks/index'
|
||||
import { useImageDetectionTaskManager, ImageAuditResult, AuditConclusion } from '../../hooks/useImageDetectionTaskManager'
|
||||
|
||||
import './index.css'
|
||||
|
||||
// 定义详细的加载状态类型
|
||||
type LoadingState = false | 'uploading' | 'auditing' | 'processing'
|
||||
|
||||
export default function Home() {
|
||||
const dispatch = useAppDispatch()
|
||||
const templates = useAppSelector(selectTemplates)
|
||||
const sdk = useSdk()
|
||||
const serverSdk = useServerSdk()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const { submitAuditTask } = useImageDetectionTaskManager()
|
||||
const [loadingState, setLoadingState] = useState<LoadingState>(false)
|
||||
const [refreshing, setRefreshing] = useState(false)
|
||||
|
||||
const loadTemplates = async () => {
|
||||
@@ -56,18 +60,55 @@ export default function Home() {
|
||||
loadTemplates()
|
||||
}, [dispatch])
|
||||
|
||||
const handleTemplateClick = async (template: Template) => {
|
||||
if (loading) return // 防止重复点击
|
||||
try {
|
||||
setLoading(true)
|
||||
// 获取加载状态的显示文本
|
||||
const getLoadingText = (state: LoadingState): string => {
|
||||
switch (state) {
|
||||
case 'uploading':
|
||||
return '上传图片中...'
|
||||
case 'auditing':
|
||||
return '内容审核中...'
|
||||
case 'processing':
|
||||
return '处理图片中...'
|
||||
default:
|
||||
return '处理中...'
|
||||
}
|
||||
}
|
||||
|
||||
// 选择并上传图片
|
||||
const url = await sdk.chooseAndUploadImage({
|
||||
// 处理审核失败
|
||||
const handleAuditFailure = (auditResult: ImageAuditResult) => {
|
||||
const messages = {
|
||||
[AuditConclusion.REJECT]: '图片内容不符合规范,请重新选择符合规范的图片',
|
||||
[AuditConclusion.REVIEW]: '图片需要人工审核,请稍后重试或更换其他图片',
|
||||
[AuditConclusion.UNCERTAIN]: '图片审核结果不确定,请重新选择图片'
|
||||
}
|
||||
|
||||
const message = messages[auditResult.conclusion!] || '图片审核失败,请重新选择图片'
|
||||
|
||||
Taro.showModal({
|
||||
title: '图片审核未通过',
|
||||
content: message,
|
||||
confirmText: '我知道了',
|
||||
showCancel: false,
|
||||
success: () => {
|
||||
console.log('用户确认审核失败信息')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleTemplateClick = async (template: Template) => {
|
||||
if (loadingState) return // 防止重复点击
|
||||
|
||||
try {
|
||||
// 第一步:选择并上传图片
|
||||
setLoadingState('uploading')
|
||||
Taro.showLoading({
|
||||
title: getLoadingText('uploading'),
|
||||
mask: true
|
||||
})
|
||||
|
||||
const imageUrl = await sdk.chooseAndUploadImage({
|
||||
onImageSelected: () => {
|
||||
Taro.showLoading({
|
||||
title: '上传图片中...',
|
||||
mask: true
|
||||
})
|
||||
console.log('用户选择了图片')
|
||||
},
|
||||
onProgress: (progress) => {
|
||||
Taro.showLoading({
|
||||
@@ -77,43 +118,112 @@ export default function Home() {
|
||||
}
|
||||
})
|
||||
|
||||
// 执行模板处理
|
||||
// 第二步:图片内容审核
|
||||
setLoadingState('auditing')
|
||||
Taro.showLoading({
|
||||
title: '处理图片中...',
|
||||
title: getLoadingText('auditing'),
|
||||
mask: true
|
||||
})
|
||||
try {
|
||||
const taskId = await serverSdk.executeTemplate({
|
||||
imageUrl: url,
|
||||
templateCode: template.code
|
||||
})
|
||||
|
||||
// 隐藏loading
|
||||
Taro.hideLoading()
|
||||
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)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
// 异步提交后直接跳转到generate页面
|
||||
navigateTo({
|
||||
url: `/pages/generate/index?taskId=${taskId}&templateCode=${template.code}`
|
||||
})
|
||||
} catch (e) {
|
||||
// 处理失败
|
||||
// 第三步:检查审核结果
|
||||
if (auditResult.conclusion === AuditConclusion.PASS) {
|
||||
// 审核通过,显示成功提示
|
||||
Taro.showToast({
|
||||
title: '请等待生成中的任务完成后再尝试',
|
||||
icon: 'none',
|
||||
title: '审核通过',
|
||||
icon: 'success',
|
||||
duration: 1000
|
||||
})
|
||||
|
||||
// 短暂延迟后继续业务处理
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
// 第四步:执行业务逻辑
|
||||
setLoadingState('processing')
|
||||
Taro.showLoading({
|
||||
title: getLoadingText('processing'),
|
||||
mask: true
|
||||
})
|
||||
|
||||
try {
|
||||
const taskId = await serverSdk.executeTemplate({
|
||||
imageUrl,
|
||||
templateCode: template.code
|
||||
})
|
||||
|
||||
// 隐藏loading
|
||||
Taro.hideLoading()
|
||||
|
||||
// 跳转到生成页面
|
||||
navigateTo({
|
||||
url: `/pages/generate/index?taskId=${taskId}&templateCode=${template.code}`
|
||||
})
|
||||
|
||||
} catch (businessError) {
|
||||
// 业务处理失败
|
||||
Taro.hideLoading()
|
||||
Taro.showToast({
|
||||
title: '请等待生成中的任务完成后再尝试',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
})
|
||||
}
|
||||
|
||||
} else {
|
||||
// 审核不通过
|
||||
Taro.hideLoading()
|
||||
handleAuditFailure(auditResult)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
// 统一错误处理
|
||||
Taro.hideLoading()
|
||||
|
||||
if (loadingState === 'uploading') {
|
||||
Taro.showToast({
|
||||
title: '图片上传失败,请重试',
|
||||
icon: 'error',
|
||||
duration: 2000
|
||||
})
|
||||
} else if (loadingState === 'auditing') {
|
||||
Taro.showToast({
|
||||
title: '图片审核失败,请重试',
|
||||
icon: 'error',
|
||||
duration: 2000
|
||||
})
|
||||
} else {
|
||||
Taro.showToast({
|
||||
title: error instanceof Error ? error.message : '处理失败',
|
||||
icon: 'error',
|
||||
duration: 2000
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
// 隐藏loading
|
||||
Taro.hideLoading()
|
||||
// 显示错误提示
|
||||
Taro.showToast({
|
||||
title: error instanceof Error ? error.message : '处理失败',
|
||||
icon: 'error',
|
||||
duration: 2000
|
||||
})
|
||||
|
||||
console.error('模板处理错误:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setLoadingState(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -92,15 +92,16 @@ const ImageAuditDemo: React.FC = () => {
|
||||
</Text>
|
||||
<Text
|
||||
className='history-result'
|
||||
style={{ color: getResultColor(item.result.conclusion) }}
|
||||
style={{ color: getResultColor(item.result?.conclusion) }}
|
||||
>
|
||||
{item.result.conclusion === 'PASS' && '✅ 审核通过'}
|
||||
{item.result.conclusion === 'REJECT' && '❌ 审核拒绝'}
|
||||
{item.result.conclusion === 'REVIEW' && '⚠️ 人工复审'}
|
||||
{item.result.conclusion === 'UNCERTAIN' && '❓ 结果不确定'}
|
||||
{item.result?.conclusion === 'pass' && '✅ 审核通过'}
|
||||
{item.result?.conclusion === 'reject' && '❌ 审核拒绝'}
|
||||
{item.result?.conclusion === 'review' && '⚠️ 人工复审'}
|
||||
{item.result?.conclusion === 'uncertain' && '❓ 结果不确定'}
|
||||
{!item.result?.conclusion && '❓ 结果未知'}
|
||||
</Text>
|
||||
</View>
|
||||
{item.result.result && (
|
||||
{item.result?.result && (
|
||||
<View className='history-details'>
|
||||
<Text className='history-score'>
|
||||
安全评分: {Math.round((item.result.result.score || 0) * 100)}
|
||||
|
||||
Reference in New Issue
Block a user