feat: 集成图片审核功能到首页业务流程

**核心功能**:
- 在首页模板点击流程中集成图片内容审核
- 只有审核通过的图片才能进入业务处理环节
- 审核不通过时显示详细的错误信息和建议

**用户体验优化**:
- 添加详细的进度状态提示:上传中 → 审核中 → 审核通过 → 处理中
- 不同阶段的 loading 文案和错误处理
- 防重复点击机制,确保流程的稳定性

**技术实现**:
- 使用 LoadingState 类型管理详细的加载状态
- 集成 useImageDetectionTaskManager hook 处理审核逻辑
- Promise 封装审核流程,便于错误处理和状态管理
- 审核结果缓存机制,提升性能

**错误处理**:
- 上传失败、审核失败、业务处理失败的分类错误提示
- 审核不通过时的详细原因说明(违规/复审/不确定)
- 统一的错误边界处理机制

**修复内容**:
- 修复演示页面的 TypeScript 类型错误
- 修复审核结果缓存 key 不匹配问题
- 优化审核结果传递逻辑
This commit is contained in:
imeepos
2025-09-08 16:08:29 +08:00
parent 075854099e
commit 2495f40eea
5 changed files with 224 additions and 138 deletions

View File

@@ -1,7 +1,6 @@
export default defineAppConfig({
pages: [
'pages/imageaudit-demo/index',
'pages/home/index', // 新的模板卡片首页
'pages/history/index', // 历史记录页面
'pages/generate/index', // 生成处理页面

View File

@@ -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 '通过';

View File

@@ -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;
}
/**

View File

@@ -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)
}
}

View File

@@ -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)}