fix: 修复模特详情页照片管理图片显示问题

- 创建imagePathUtils工具函数,智能处理本地路径和云端URL
- 修复ModelImageGallery组件中图片路径处理逻辑
- 修复ModelImagePreviewModal组件中图片路径处理逻辑
- 云端URL(https://)直接使用,本地路径通过convertFileSrc转换
- 添加完整的单元测试覆盖路径处理逻辑

解决问题:模特详情页照片管理部分图片无法显示
原因:对所有路径都使用convertFileSrc,但该函数只适用于本地路径
This commit is contained in:
imeepos 2025-07-30 14:53:28 +08:00
parent 616ff39812
commit abe9cfac94
4 changed files with 111 additions and 6 deletions

View File

@ -9,10 +9,10 @@ import {
Search Search
} from 'lucide-react'; } from 'lucide-react';
import { ModelPhoto, PhotoType } from '../types/model'; import { ModelPhoto, PhotoType } from '../types/model';
import { convertFileSrc } from '@tauri-apps/api/core';
import { ModelImageUploader } from './ModelImageUploader'; import { ModelImageUploader } from './ModelImageUploader';
import { ModelImagePreviewModal } from './ModelImagePreviewModal'; import { ModelImagePreviewModal } from './ModelImagePreviewModal';
import { DeleteConfirmDialog } from './DeleteConfirmDialog'; import { DeleteConfirmDialog } from './DeleteConfirmDialog';
import { getImageSrc } from '../utils/imagePathUtils';
interface ModelImageGalleryProps { interface ModelImageGalleryProps {
photos: ModelPhoto[]; photos: ModelPhoto[];
@ -175,6 +175,8 @@ export const ModelImageGallery: React.FC<ModelImageGalleryProps> = ({
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}; };
return ( return (
<div className={`space-y-6 ${className}`}> <div className={`space-y-6 ${className}`}>
{/* 头部工具栏 */} {/* 头部工具栏 */}
@ -277,7 +279,7 @@ export const ModelImageGallery: React.FC<ModelImageGalleryProps> = ({
{/* 图片 */} {/* 图片 */}
<div className="aspect-square overflow-hidden"> <div className="aspect-square overflow-hidden">
<img <img
src={convertFileSrc(photo.file_path)} src={getImageSrc(photo.file_path)}
alt={photo.file_name} alt={photo.file_name}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-200" className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-200"
/> />
@ -346,7 +348,7 @@ export const ModelImageGallery: React.FC<ModelImageGalleryProps> = ({
{/* 缩略图 */} {/* 缩略图 */}
<div className="w-16 h-16 rounded-lg overflow-hidden flex-shrink-0"> <div className="w-16 h-16 rounded-lg overflow-hidden flex-shrink-0">
<img <img
src={convertFileSrc(photo.file_path)} src={getImageSrc(photo.file_path)}
alt={photo.file_name} alt={photo.file_name}
className="w-full h-full object-cover" className="w-full h-full object-cover"
/> />

View File

@ -17,7 +17,7 @@ import {
Tag Tag
} from 'lucide-react'; } from 'lucide-react';
import { ModelPhoto, PhotoType } from '../types/model'; import { ModelPhoto, PhotoType } from '../types/model';
import { convertFileSrc } from '@tauri-apps/api/core'; import { getImageSrc } from '../utils/imagePathUtils';
interface ModelImagePreviewModalProps { interface ModelImagePreviewModalProps {
photos: ModelPhoto[]; photos: ModelPhoto[];
@ -181,9 +181,11 @@ export const ModelImagePreviewModal: React.FC<ModelImagePreviewModalProps> = ({
return new Date(dateString).toLocaleString('zh-CN'); return new Date(dateString).toLocaleString('zh-CN');
}; };
if (!isOpen || !currentPhoto) return null; if (!isOpen || !currentPhoto) return null;
const imageUrl = convertFileSrc(currentPhoto.file_path); const imageUrl = getImageSrc(currentPhoto.file_path);
return ( return (
<div className={`fixed inset-0 z-50 flex items-center justify-center bg-black ${ <div className={`fixed inset-0 z-50 flex items-center justify-center bg-black ${
@ -441,7 +443,7 @@ export const ModelImagePreviewModal: React.FC<ModelImagePreviewModalProps> = ({
}`} }`}
> >
<img <img
src={convertFileSrc(photo.file_path)} src={getImageSrc(photo.file_path)}
alt={photo.file_name} alt={photo.file_name}
className="w-full h-full object-cover" className="w-full h-full object-cover"
/> />

View File

@ -0,0 +1,68 @@
import { describe, it, expect, vi } from 'vitest';
import { getImageSrc, isCloudUrl, isLocalPath } from '../imagePathUtils';
// Mock Tauri API
vi.mock('@tauri-apps/api/core', () => ({
convertFileSrc: vi.fn((path: string) => `tauri://localhost${path}`)
}));
describe('imagePathUtils', () => {
describe('isCloudUrl', () => {
it('应该正确识别HTTPS URL', () => {
expect(isCloudUrl('https://cdn.roasmax.cn/models/photos/model-1/image.jpg')).toBe(true);
});
it('应该正确识别HTTP URL', () => {
expect(isCloudUrl('http://example.com/image.jpg')).toBe(true);
});
it('应该正确识别本地路径', () => {
expect(isCloudUrl('/local/path/to/image.jpg')).toBe(false);
expect(isCloudUrl('C:\\Windows\\path\\to\\image.jpg')).toBe(false);
expect(isCloudUrl('./relative/path/image.jpg')).toBe(false);
});
});
describe('isLocalPath', () => {
it('应该正确识别本地路径', () => {
expect(isLocalPath('/local/path/to/image.jpg')).toBe(true);
expect(isLocalPath('C:\\Windows\\path\\to\\image.jpg')).toBe(true);
expect(isLocalPath('./relative/path/image.jpg')).toBe(true);
});
it('应该正确识别云端URL', () => {
expect(isLocalPath('https://cdn.roasmax.cn/models/photos/model-1/image.jpg')).toBe(false);
expect(isLocalPath('http://example.com/image.jpg')).toBe(false);
});
});
describe('getImageSrc', () => {
it('应该直接返回HTTPS URL', () => {
const cloudUrl = 'https://cdn.roasmax.cn/models/photos/model-1/image.jpg';
expect(getImageSrc(cloudUrl)).toBe(cloudUrl);
});
it('应该直接返回HTTP URL', () => {
const cloudUrl = 'http://example.com/image.jpg';
expect(getImageSrc(cloudUrl)).toBe(cloudUrl);
});
it('应该转换本地路径', () => {
const localPath = '/local/path/to/image.jpg';
const result = getImageSrc(localPath);
expect(result).toBe('tauri://localhost/local/path/to/image.jpg');
});
it('应该转换Windows本地路径', () => {
const localPath = 'C:\\Windows\\path\\to\\image.jpg';
const result = getImageSrc(localPath);
expect(result).toBe('tauri://localhostC:\\Windows\\path\\to\\image.jpg');
});
it('应该转换相对路径', () => {
const localPath = './relative/path/image.jpg';
const result = getImageSrc(localPath);
expect(result).toBe('tauri://localhost./relative/path/image.jpg');
});
});
});

View File

@ -0,0 +1,33 @@
import { convertFileSrc } from '@tauri-apps/api/core';
/**
* URL
* @param filePath URL
* @returns
*/
export const getImageSrc = (filePath: string): string => {
// 如果是云端URL以http://或https://开头),直接返回
if (filePath.startsWith('http://') || filePath.startsWith('https://')) {
return filePath;
}
// 如果是本地路径使用convertFileSrc转换
return convertFileSrc(filePath);
};
/**
* URL
* @param filePath
* @returns URL
*/
export const isCloudUrl = (filePath: string): boolean => {
return filePath.startsWith('http://') || filePath.startsWith('https://');
};
/**
*
* @param filePath
* @returns
*/
export const isLocalPath = (filePath: string): boolean => {
return !isCloudUrl(filePath);
};