feat(album): add cover picture functionality to albums and enhance album management API

This commit is contained in:
shiyu
2025-06-09 15:12:50 +08:00
parent 9d9393f9ce
commit e55f311c04
24 changed files with 1496 additions and 251 deletions

View File

@@ -5,18 +5,21 @@ export interface AlbumResponse {
id: number;
name: string;
description: string;
coverImageUrl?: string;
pictureCount: number;
userId: number;
username?: string;
createdAt: Date;
updatedAt: Date;
coverPictureId?: number | null;
coverPicturePath?: string;
coverPictureThumbnailPath?: string;
}
// 创建相册请求
export interface CreateAlbumRequest {
name: string;
description: string;
coverPictureId?: number | null; // 新增封面图片ID
}
// 更新相册请求
@@ -24,6 +27,7 @@ export interface UpdateAlbumRequest {
id: number;
name: string;
description: string;
coverPictureId?: number | null; // 新增封面图片ID
}
// 相册图片操作请求

View File

@@ -0,0 +1,96 @@
import type { AlbumResponse } from './albumApi';
import { fetchApi, type BaseResult, type BatchDeleteResult, type PaginatedResult } from './fetchClient';
import type { PictureResponse } from './pictureApi'; // For pictures within an album
export interface AlbumCreateRequest {
name: string;
description?: string;
coverPictureId?: number | null;
}
export interface AlbumUpdateRequest {
name?: string;
description?: string;
coverPictureId?: number | null;
}
export const getManagementAlbums = async (
page: number = 1,
pageSize: number = 10,
searchQuery?: string,
userId?: number
): Promise<PaginatedResult<AlbumResponse>> => {
const params = new URLSearchParams({
page: page.toString(),
pageSize: pageSize.toString()
});
if (searchQuery) params.append('searchQuery', searchQuery);
if (userId) params.append('userId', userId.toString());
const response = await fetchApi(`/management/album/get_albums?${params.toString()}`);
return response as PaginatedResult<AlbumResponse>;
};
// Get album by ID
export const getManagementAlbumById = async (id: number): Promise<BaseResult<AlbumResponse>> => {
return fetchApi<AlbumResponse>(`/management/album/get_album/${id}`);
};
// Create album
export const createManagementAlbum = async (request: AlbumCreateRequest): Promise<BaseResult<AlbumResponse>> => {
return fetchApi<AlbumResponse>(
'/management/album/create_album',
{
method: 'POST',
body: JSON.stringify(request)
}
);
};
// Update album
export const updateManagementAlbum = async (id: number, request: AlbumUpdateRequest): Promise<BaseResult<AlbumResponse>> => {
return fetchApi<AlbumResponse>(
`/management/album/update_album/${id}`,
{
method: 'POST',
body: JSON.stringify(request)
}
);
};
// Delete album
export const deleteManagementAlbum = async (id: number): Promise<BaseResult<boolean>> => {
return fetchApi<boolean>(
'/management/album/delete_album',
{
method: 'POST',
body: JSON.stringify(id) // Backend expects int id in body
}
);
};
// Batch delete albums
export const batchDeleteManagementAlbums = async (ids: number[]): Promise<BaseResult<BatchDeleteResult>> => {
return fetchApi<BatchDeleteResult>(
'/management/album/batch_delete_albums',
{
method: 'POST',
body: JSON.stringify(ids)
}
);
};
// Get pictures in album
export const getPicturesInAlbum = async (
albumId: number,
page: number = 1,
pageSize: number = 10
): Promise<PaginatedResult<PictureResponse>> => {
const params = new URLSearchParams({
page: page.toString(),
pageSize: pageSize.toString()
});
const response = await fetchApi(`/management/album/${albumId}/pictures?${params.toString()}`);
return response as PaginatedResult<PictureResponse>;
};

View File

@@ -1,5 +1,6 @@
export * from './authApi';
export * from './albumApi';
export * from './albumManagementApi';
export * from './backgroundTaskApi';
export * from './configApi';
export * from './fetchClient';

View File

@@ -0,0 +1,364 @@
import React, { useState, useEffect, useCallback } from 'react';
import {
Table, Button, Card, Input, Space, Modal,
message, Typography, Popconfirm, Row, Col, Image,
AutoComplete, Form, Divider, Select, Tag
} from 'antd';
import {
BookOutlined, DeleteOutlined, SearchOutlined, ExclamationCircleOutlined,
ReloadOutlined, FilterOutlined, ClearOutlined, PlusOutlined, EditOutlined, PictureOutlined
} from '@ant-design/icons';
import {
getManagementAlbums, deleteManagementAlbum, batchDeleteManagementAlbums,
createManagementAlbum, updateManagementAlbum,
type AlbumCreateRequest, type AlbumUpdateRequest
} from '../../../api/albumManagementApi';
import { getUsers, getManagementPictures, type AlbumResponse } from '../../../api'; // Renamed to avoid conflict
import { useOutletContext } from 'react-router';
import type { Breakpoint } from 'antd';
const { Title, Text } = Typography;
const { confirm } = Modal;
interface PictureOption {
value: number;
label: string;
thumbnailPath?: string;
}
const AlbumManagement: React.FC = () => {
const { isMobile } = useOutletContext<{ isMobile: boolean }>();
const [albums, setAlbums] = useState<AlbumResponse[]>([]);
const [userOptions, setUserOptions] = useState<{ value: number; label: string }[]>([]);
const [pictureOptions, setPictureOptions] = useState<PictureOption[]>([]);
const [loading, setLoading] = useState(false);
const [total, setTotal] = useState(0);
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const [searchQuery, setSearchQuery] = useState('');
const [selectedUserId, setSelectedUserId] = useState<number | undefined>();
const [showFilters, setShowFilters] = useState(false);
const [filterForm] = Form.useForm();
const [isModalVisible, setIsModalVisible] = useState(false);
const [editingAlbum, setEditingAlbum] = useState<AlbumResponse | null>(null);
const [albumForm] = Form.useForm<AlbumCreateRequest | AlbumUpdateRequest>();
const searchUsers = useCallback(async (searchValue: string) => {
if (!searchValue.trim()) {
setUserOptions([]);
return;
}
try {
const response = await getUsers({ page: 1, pageSize: 20, searchQuery: searchValue });
if (response.success && response.data) {
setUserOptions(response.data.map(user => ({ value: user.id, label: `${user.userName} (${user.email})` })));
}
} catch (error) { console.error('Error searching users:', error); }
}, []);
const searchPicturesForCover = useCallback(async (searchValue: string) => {
if (!searchValue.trim() && pictureOptions.length > 5) return; // Avoid frequent calls if not searching
try {
const response = await getManagementPictures(1, 20, searchValue);
if (response.success && response.data) {
setPictureOptions(response.data.map(pic => ({
value: pic.id,
label: `${pic.name || '未命名图片'} (ID: ${pic.id})`,
thumbnailPath: pic.thumbnailPath || pic.path,
})));
}
} catch (error) {
console.error('Error searching pictures for cover:', error);
message.error('搜索封面图片失败');
}
}, [pictureOptions.length]);
const fetchAlbums = useCallback(async (
page = currentPage, size = pageSize, query = searchQuery, userId = selectedUserId
) => {
setLoading(true);
try {
const response = await getManagementAlbums(page, size, query, userId);
if (response.success && response.data) {
setAlbums(response.data || []);
setTotal(response.totalCount || 0);
} else {
message.error(response.message || '获取相册列表失败');
}
} catch (error) {
message.error('获取相册列表失败,请检查网络连接');
} finally {
setLoading(false);
}
}, [currentPage, pageSize, searchQuery, selectedUserId]);
useEffect(() => {
fetchAlbums();
searchPicturesForCover(''); // Initial load for picture options
}, [fetchAlbums, searchPicturesForCover]);
const handlePageChange = (page: number, size?: number) => {
setCurrentPage(page);
if (size) setPageSize(size);
fetchAlbums(page, size || pageSize);
};
const handleQuickSearch = (value: string) => {
setSearchQuery(value);
setCurrentPage(1);
fetchAlbums(1, pageSize, value, selectedUserId);
};
const handleFilter = async () => {
const values = await filterForm.validateFields();
setSearchQuery(values.searchQuery || '');
setSelectedUserId(values.userId);
setCurrentPage(1);
fetchAlbums(1, pageSize, values.searchQuery, values.userId);
};
const handleClearFilters = () => {
filterForm.resetFields();
setSearchQuery('');
setSelectedUserId(undefined);
setUserOptions([]);
setCurrentPage(1);
fetchAlbums(1, pageSize, '', undefined);
};
const showCreateModal = () => {
setEditingAlbum(null);
albumForm.resetFields();
albumForm.setFieldsValue({ coverPictureId: null }); // Ensure coverPictureId is null or undefined
setIsModalVisible(true);
};
const showEditModal = (album: AlbumResponse) => {
setEditingAlbum(album);
const formValues = {
name: album.name,
description: album.description,
coverPictureId: album.coverPictureId, // 使用正确的 coverPictureId
};
// 如果存在封面图片ID并且该图片不在当前选项中则尝试添加它以便Select可以正确显示
if (album.coverPictureId && (album.coverPictureThumbnailPath || album.coverPicturePath)) {
const existingOption = pictureOptions.find(opt => opt.value === album.coverPictureId);
if (!existingOption) {
// 为了在Select中显示当前封面需要一个标签。
// 理想情况下AlbumResponse会包含封面图片的名称。
// 此处使用文件名或ID作为后备标签。
const pictureLabel = album.name ? `${album.name} (封面)` : `图片ID: ${album.coverPictureId}`;
const newPictureOption: PictureOption = {
value: album.coverPictureId,
label: pictureLabel, // 使用相册名或ID作为临时标签
thumbnailPath: album.coverPictureThumbnailPath || album.coverPicturePath,
};
// 将当前封面图片添加到选项列表的开头以便在Select中显示
setPictureOptions(prevOptions => [newPictureOption, ...prevOptions.filter(opt => opt.value !== album.coverPictureId)]);
}
}
albumForm.setFieldsValue(formValues);
setIsModalVisible(true);
};
const handleModalOk = async () => {
try {
const values = await albumForm.validateFields() as AlbumCreateRequest | AlbumUpdateRequest;
setLoading(true);
let response;
if (editingAlbum) {
response = await updateManagementAlbum(editingAlbum.id, values);
} else {
response = await createManagementAlbum(values as AlbumCreateRequest);
}
if (response.success) {
message.success(editingAlbum ? '相册更新成功' : '相册创建成功');
setIsModalVisible(false);
fetchAlbums();
} else {
message.error(response.message || (editingAlbum ? '更新失败' : '创建失败'));
}
} catch (errorInfo) {
console.log('Validate Failed:', errorInfo);
message.error('请检查表单输入');
} finally {
setLoading(false);
}
};
const handleDelete = async (id: number) => {
try {
const response = await deleteManagementAlbum(id);
if (response.success) {
message.success('相册删除成功');
fetchAlbums();
} else {
message.error(response.message || '删除相册失败');
}
} catch (error) { message.error('删除相册失败'); }
};
const handleBatchDelete = async () => {
if (selectedRowKeys.length === 0) {
message.warning('请选择要删除的相册');
return;
}
confirm({
title: `确定要删除 ${selectedRowKeys.length} 个相册吗?`,
icon: <ExclamationCircleOutlined />,
content: '此操作不可逆,相册将被永久删除 (相册内图片不会被删除)',
async onOk() {
try {
const response = await batchDeleteManagementAlbums(selectedRowKeys as number[]);
if (response.success && response.data) {
message.success(`成功删除 ${response.data.successCount} 个相册`);
if (response.data.failedCount > 0) {
message.warning(`${response.data.failedCount} 个相册删除失败`);
}
setSelectedRowKeys([]);
fetchAlbums();
} else {
message.error(response.message || '批量删除失败');
}
} catch (error) { message.error('批量删除失败'); }
}
});
};
const columns = [
{ title: 'ID', dataIndex: 'id', key: 'id', responsive: ['md'] as Breakpoint[] },
{
title: '封面', dataIndex: 'coverPictureThumbnailPath', key: 'cover',
render: (path?: string, record?: AlbumResponse) => (
path ? <Image width={50} height={50} src={path} style={{ objectFit: 'cover', borderRadius: 4 }} />
: record?.coverPicturePath ? <Image width={50} height={50} src={record.coverPicturePath} style={{ objectFit: 'cover', borderRadius: 4 }} />
: <PictureOutlined style={{ fontSize: 30, color: '#ccc' }}/>
),
},
{ title: '名称', dataIndex: 'name', key: 'name' },
{ title: '描述', dataIndex: 'description', key: 'description', responsive: ['md'] as Breakpoint[], render: (desc: string) => desc || '-'},
{ title: '图片数', dataIndex: 'pictureCount', key: 'pictureCount', render: (count: number) => <Tag>{count}</Tag> },
{ title: '用户', dataIndex: 'username', key: 'username', responsive: ['lg'] as Breakpoint[] },
{ title: '创建时间', dataIndex: 'createdAt', key: 'createdAt', responsive: ['lg'] as Breakpoint[], render: (date: Date) => new Date(date).toLocaleString() },
{
title: '操作', key: 'action',
render: (_: any, record: AlbumResponse) => (
<Space size="small" wrap>
<Button type="link" icon={<EditOutlined />} onClick={() => showEditModal(record)}></Button>
<Popconfirm title="确定删除此相册?" onConfirm={() => handleDelete(record.id)}>
<Button type="link" danger icon={<DeleteOutlined />}></Button>
</Popconfirm>
{/* Add 'Set Cover' and 'Manage Pictures' buttons here later */}
</Space>
),
},
];
return (
<div className="album-management">
<Row gutter={[16, 16]} align="middle" justify="space-between">
<Col>
<Space align="center">
<BookOutlined style={{ fontSize: 24 }} />
<Title level={2} style={{ margin: 0 }}></Title>
</Space>
<Text type="secondary" style={{ marginTop: 8, display: 'block' }}>
</Text>
</Col>
</Row>
<Card style={{ marginTop: 16 }}>
<Row gutter={[16, 16]} justify="space-between" style={{ marginBottom: 16 }}>
<Col xs={24} sm={12} md={16}>
<Space wrap>
<Button type="primary" icon={<PlusOutlined />} onClick={showCreateModal}></Button>
<Button danger icon={<DeleteOutlined />} onClick={handleBatchDelete} disabled={selectedRowKeys.length === 0}></Button>
<Button icon={<ReloadOutlined />} onClick={() => fetchAlbums()}></Button>
<Button icon={<FilterOutlined />} onClick={() => setShowFilters(!showFilters)} type={showFilters ? 'primary' : 'default'}></Button>
</Space>
</Col>
<Col xs={24} sm={12} md={8}>
<Input.Search placeholder="搜索相册名称或描述" allowClear enterButton={<SearchOutlined />} onSearch={handleQuickSearch} value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} />
</Col>
</Row>
{showFilters && (
<>
<Card size="small" style={{ marginBottom: 16, backgroundColor: '#fafafa' }}>
<Form form={filterForm} layout="inline" onFinish={handleFilter} initialValues={{ searchQuery, userId: selectedUserId }}>
<Form.Item name="searchQuery" label="关键词"><Input placeholder="名称或描述" style={{ width: 200 }} /></Form.Item>
<Form.Item name="userId" label="所属用户">
<AutoComplete style={{ width: 250 }} options={userOptions} onSearch={searchUsers} placeholder="输入用户名或邮箱" allowClear filterOption={false} />
</Form.Item>
<Form.Item><Space><Button type="primary" htmlType="submit" icon={<SearchOutlined />}></Button><Button icon={<ClearOutlined />} onClick={handleClearFilters}></Button></Space></Form.Item>
</Form>
</Card>
<Divider style={{ margin: '16px 0' }} />
</>
)}
<Table
rowKey="id"
columns={columns}
dataSource={albums}
loading={loading}
pagination={{ current: currentPage, pageSize, total, showSizeChanger: true, showQuickJumper: true, onChange: handlePageChange, showTotal: (total) => `${total}` }}
rowSelection={{ selectedRowKeys, onChange: (keys) => setSelectedRowKeys(keys) }}
size={isMobile ? "small" : "middle"}
scroll={{ x: 'max-content' }}
/>
</Card>
<Modal
title={editingAlbum ? '编辑相册' : '创建新相册'}
open={isModalVisible}
onOk={handleModalOk}
onCancel={() => setIsModalVisible(false)}
confirmLoading={loading}
destroyOnClose
width={600}
>
<Form form={albumForm} layout="vertical" name="albumForm">
<Form.Item name="name" label="相册名称" rules={[{ required: true, message: '请输入相册名称' }]}>
<Input />
</Form.Item>
<Form.Item name="description" label="相册描述">
<Input.TextArea rows={3} />
</Form.Item>
<Form.Item name="coverPictureId" label="封面图片 (可选)">
<Select
showSearch
allowClear
placeholder="搜索并选择封面图片"
onSearch={searchPicturesForCover}
filterOption={false} // Server-side search
notFoundContent={loading ? "搜索中..." : "无匹配图片"}
options={pictureOptions}
optionRender={(option) => (
<Space>
{option.data?.thumbnailPath && <Image src={option.data.thumbnailPath} width={30} height={30} preview={false} style={{objectFit: 'cover'}}/>}
<span>{option.label}</span>
</Space>
)}
/>
</Form.Item>
</Form>
</Modal>
</div>
);
};
export default AlbumManagement;

View File

@@ -11,7 +11,7 @@ import {
import type { ColumnsType } from 'antd/es/table';
import { useOutletContext } from 'react-router';
import { useNavigate } from 'react-router';
import { getUsers, getManagementPictures } from '../../../api';
import { getUsers, getManagementPictures, getManagementAlbums } from '../../../api';
import type { UserResponse, PictureResponse } from '../../../api';
const { Title, Text } = Typography;
@@ -21,8 +21,6 @@ interface DashboardStats {
totalAlbums: number;
totalPhotos: number;
storageUsagePercentage: number;
newUsersToday: number;
newPhotosToday: number;
softwareVersion: string;
systemVersion: string;
cpuArchitecture: string;
@@ -37,8 +35,6 @@ const AdminDashboard: React.FC = () => {
totalAlbums: 0,
totalPhotos: 0,
storageUsagePercentage: 0,
newUsersToday: 0,
newPhotosToday: 0,
softwareVersion: 'N/A',
systemVersion: 'N/A',
cpuArchitecture: 'N/A'
@@ -85,40 +81,32 @@ const AdminDashboard: React.FC = () => {
}
};
// 计算今日新增数据
const calculateTodayStats = (users: UserResponse[], photos: PictureResponse[]) => {
const today = new Date();
today.setHours(0, 0, 0, 0);
const newUsersToday = users.filter(user => {
const userDate = new Date(user.createdAt);
userDate.setHours(0, 0, 0, 0);
return userDate.getTime() === today.getTime();
}).length;
const newPhotosToday = photos.filter(photo => {
const photoDate = new Date(photo.createdAt);
photoDate.setHours(0, 0, 0, 0);
return photoDate.getTime() === today.getTime();
}).length;
setStats(prev => ({
...prev,
newUsersToday,
newPhotosToday
}));
// 获取相册总数
const fetchTotalAlbums = async () => {
try {
// 我们只需要总数,所以 pageSize 可以设为 1
const response = await getManagementAlbums(1, 1);
if (response.success) {
setStats(prev => ({
...prev,
totalAlbums: response.totalCount || 0
}));
}
} catch (error) {
console.error('Error fetching total albums:', error);
message.error('获取相册总数失败');
}
};
useEffect(() => {
const loadData = async () => {
setLoading(true);
try {
await Promise.all([fetchRecentUsers(), fetchRecentPhotos()]);
await Promise.all([fetchRecentUsers(), fetchRecentPhotos(), fetchTotalAlbums()]);
// 设置其他静态统计数据
setStats(prev => ({
...prev,
totalAlbums: 348, // 相册功能暂未实现,使用模拟数据
storageUsagePercentage: 68,
softwareVersion: 'Foxel Dev 尝鲜版',
systemVersion: 'Fedora 42',
@@ -134,13 +122,6 @@ const AdminDashboard: React.FC = () => {
loadData();
}, []);
// 当用户和图片数据都加载完成后计算今日统计
useEffect(() => {
if (recentUsers.length > 0 && recentPhotos.length > 0) {
calculateTodayStats(recentUsers, recentPhotos);
}
}, [recentUsers, recentPhotos]);
const userColumns = useMemo<ColumnsType<UserResponse>>(() => [
{
title: '用户名',
@@ -237,11 +218,6 @@ const AdminDashboard: React.FC = () => {
title="用户总数"
value={stats.totalUsers}
prefix={<UserOutlined />}
suffix={
<Tag color="green" style={{ marginLeft: 8 }}>
<ArrowUpOutlined /> {stats.newUsersToday}
</Tag>
}
/>
</Card>
</Col>
@@ -260,11 +236,6 @@ const AdminDashboard: React.FC = () => {
title="照片总数"
value={stats.totalPhotos}
prefix={<PictureOutlined />}
suffix={
<Tag color="green" style={{ marginLeft: 8 }}>
<ArrowUpOutlined /> {stats.newPhotosToday}
</Tag>
}
/>
</Card>
</Col>

View File

@@ -2,7 +2,8 @@ import { useState, useEffect, useCallback, useRef } from 'react';
import { useParams, useNavigate, useOutletContext } from 'react-router';
import {
Typography, Button, Spin, Empty, message,
Popconfirm, Modal, Form, Input} from 'antd';
Popconfirm, Modal, Form, Input, InputNumber, Select // Added InputNumber, Select
} from 'antd';
import {
EditOutlined, DeleteOutlined, PlusOutlined} from '@ant-design/icons';
import { getAlbumById, deleteAlbum, favoritePicture, unfavoritePicture, addPicturesToAlbum, updateAlbum } from '../../api';
@@ -28,7 +29,8 @@ function AlbumDetail() {
const [selectedPictures, setSelectedPictures] = useState<number[]>([]);
const [editForm] = Form.useForm();
const [submitting, setSubmitting] = useState(false);
const [refreshTrigger, setRefreshTrigger] = useState(0); // 添加刷新触发器
const [refreshTrigger, setRefreshTrigger] = useState(0);
const [albumPicturesForSelect, setAlbumPicturesForSelect] = useState<PictureResponse[]>([]); // 用于编辑时选择封面
const loadAlbum = async () => {
if (!id) return;
@@ -37,10 +39,11 @@ function AlbumDetail() {
const result = await getAlbumById(parseInt(id));
if (result.success && result.data) {
setAlbum(result.data);
// 添加检查确保 updateBreadcrumbTitle 是一个函数
if (typeof updateBreadcrumbTitle === 'function') {
updateBreadcrumbTitle(result.data.name);
}
// 假设相册详情API返回了图片列表或者需要另外获取
// setAlbumPicturesForSelect(result.data.pictures || []);
} else {
message.error(result.message || '获取相册失败');
}
@@ -54,7 +57,22 @@ function AlbumDetail() {
useEffect(() => {
loadAlbum();
}, [id]);
// 如果需要单独获取相册图片用于选择器:
// if (id) fetchPicturesForAlbumSelect(parseInt(id));
}, [id, refreshTrigger]); // refreshTrigger 确保编辑后重新加载图片列表(如果需要)
// 示例:获取相册内图片用于选择封面的函数
// const fetchPicturesForAlbumSelect = async (albumId: number) => {
// try {
// // 替换为实际获取相册内图片的API调用
// // const picturesResult = await getPicturesInAlbum(albumId, 1, 200); // 获取相册内所有图片
// // if (picturesResult.success && picturesResult.data) {
// // setAlbumPicturesForSelect(picturesResult.data);
// // }
// } catch (error) {
// message.error('获取相册内图片列表失败');
// }
// };
const handleDeleteAlbum = async () => {
if (!album) return;
@@ -152,8 +170,12 @@ function AlbumDetail() {
if (album) {
editForm.setFieldsValue({
name: album.name,
description: album.description || ''
description: album.description || '',
coverPictureId: album.coverPictureId // 设置当前封面ID
});
// 如果 album.pictures 存在,可以用它来填充选择器
// 或者在打开模态框时调用 fetchPicturesForAlbumSelect(album.id)
// setAlbumPicturesForSelect(album.pictures || []); // 假设 album 对象包含 pictures 数组
setIsEditModalVisible(true);
}
};
@@ -164,12 +186,13 @@ function AlbumDetail() {
try {
setSubmitting(true);
const values = await editForm.validateFields();
const values = await editForm.validateFields(); // values 包含 name, description, coverPictureId
const result = await updateAlbum({
id: album.id,
name: values.name,
description: values.description
description: values.description,
coverPictureId: values.coverPictureId // 传递封面ID
});
if (result.success) {
@@ -350,7 +373,8 @@ function AlbumDetail() {
layout="vertical"
initialValues={{
name: album.name,
description: album.description || ''
description: album.description || '',
coverPictureId: album.coverPictureId // 初始化表单的封面ID
}}
>
<Form.Item
@@ -371,6 +395,36 @@ function AlbumDetail() {
showCount
/>
</Form.Item>
<Form.Item
name="coverPictureId"
label="封面图片 (可选)"
tooltip="从当前相册中选择一张图片作为封面。实际应用中应为图片选择器。"
>
{/*
实际应用中替换为图片选择器,例如:
<Select
showSearch
allowClear
placeholder="选择封面图片"
// loading={loadingAlbumPicturesForSelect}
filterOption={(input, option) =>
option?.label.toLowerCase().includes(input.toLowerCase())
}
options={albumPicturesForSelect.map(p => ({
value: p.id,
label: p.name || `图片 ${p.id}`,
thumbnail: p.thumbnailPath || p.path
}))}
// optionRender={(option) => (
// <Space>
// {option.data.thumbnail && <img src={option.data.thumbnail} alt={option.label} style={{width: 24, height: 24, objectFit: 'cover'}}/>}
// <span>{option.label}</span>
// </Space>
// )}
/>
*/}
<InputNumber placeholder="输入封面图片ID" style={{ width: '100%' }} />
</Form.Item>
</Form>
</Modal>
</div>

View File

@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react';
import { Typography, Row, Col, Card, Button, Modal, Form, Input, Spin, Empty, message, Popconfirm } from 'antd';
import { Typography, Row, Col, Card, Button, Modal, Form, Input, Spin, Empty, message, Popconfirm, InputNumber, Select } from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined, PictureOutlined } from '@ant-design/icons';
import { getAlbums, createAlbum, updateAlbum, deleteAlbum } from '../../api';
import type { AlbumResponse, CreateAlbumRequest, UpdateAlbumRequest } from '../../api';
@@ -43,7 +43,7 @@ function Albums() {
const handleCreateAlbum = async (values: CreateAlbumRequest) => {
try {
const result = await createAlbum(values);
const result = await createAlbum(values); // values 现在可以包含 coverPictureId
if (result.success && result.data) {
message.success('相册创建成功');
setIsCreateModalVisible(false);
@@ -58,14 +58,16 @@ function Albums() {
}
};
const handleEditAlbum = async (values: UpdateAlbumRequest) => {
const handleEditAlbum = async (values: Omit<UpdateAlbumRequest, 'id'>) => { // Form values don't include id
if (!currentAlbum) return;
try {
const result = await updateAlbum({
const requestData: UpdateAlbumRequest = {
...values,
id: currentAlbum.id
});
id: currentAlbum.id,
// coverPictureId 来自表单 values.coverPictureId
};
const result = await updateAlbum(requestData);
if (result.success && result.data) {
message.success('相册更新成功');
@@ -101,7 +103,8 @@ function Albums() {
setCurrentAlbum(album);
editForm.setFieldsValue({
name: album.name,
description: album.description
description: album.description,
coverPictureId: album.coverPictureId // 设置当前封面ID
});
setIsEditModalVisible(true);
};
@@ -188,12 +191,16 @@ function Albums() {
bodyStyle={{ padding: '20px' }}
cover={
<Link to={`/albums/${album.id}`}>
{album.coverImageUrl ? (
<img alt={album.name} src={album.coverImageUrl} style={{
height: 180,
width: '100%',
objectFit: 'cover'
}} />
{album.coverPictureThumbnailPath || album.coverPicturePath ? (
<img
alt={album.name}
src={album.coverPictureThumbnailPath || album.coverPicturePath}
style={{
height: 180,
width: '100%',
objectFit: 'cover'
}}
/>
) : (
<div style={{
height: 180,
@@ -271,6 +278,32 @@ function Albums() {
>
<TextArea placeholder="描述一下这个相册" rows={4} />
</Form.Item>
<Form.Item
name="coverPictureId"
label="封面图片 (可选)"
tooltip="输入图片ID。实际应用中应为图片选择器。"
>
{/*
实际应用中替换为图片选择器,例如:
<Select
showSearch
allowClear
placeholder="选择封面图片"
loading={loadingUserPictures}
filterOption={(input, option) =>
option?.label.toLowerCase().includes(input.toLowerCase())
}
options={userPictures.map(p => ({ value: p.id, label: p.name || `图片 ${p.id}`, thumbnail: p.thumbnailPath }))}
// optionRender={(option) => (
// <Space>
// {option.data.thumbnail && <img src={option.data.thumbnail} alt={option.label} style={{width: 24, height: 24, objectFit: 'cover'}}/>}
// <span>{option.label}</span>
// </Space>
// )}
/>
*/}
<InputNumber placeholder="输入封面图片ID" style={{ width: '100%' }} />
</Form.Item>
<Form.Item style={{ marginBottom: 0, textAlign: 'right' }}>
<Button style={{ marginRight: 8 }} onClick={() => {
setIsCreateModalVisible(false);
@@ -314,6 +347,18 @@ function Albums() {
>
<TextArea placeholder="相册描述" rows={4} />
</Form.Item>
<Form.Item
name="coverPictureId"
label="封面图片 (可选)"
tooltip="输入图片ID。实际应用中应为图片选择器图片源为当前相册内图片或用户所有图片。"
>
{/*
实际应用中替换为图片选择器。
如果从当前相册选择,需要获取相册内图片列表。
如果像管理员一样从所有图片选择,则使用类似创建时的逻辑。
*/}
<InputNumber placeholder="输入封面图片ID" style={{ width: '100%' }} />
</Form.Item>
<Form.Item style={{ marginBottom: 0, textAlign: 'right' }}>
<Button style={{ marginRight: 8 }} onClick={() => {
setIsEditModalVisible(false);

View File

@@ -26,6 +26,7 @@ import PictureManagement from '../pages/admin/pictures/Index';
import UserDetail from '../pages/admin/users/UserDetail';
import AdminLogManagement from '../pages/admin/log/Index';
import StorageManagementPage from '../pages/admin/storage/StorageManagement';
import AlbumManagement from '../pages/admin/album/Index';
export interface RouteConfig {
path: string;
@@ -175,6 +176,18 @@ const routes: RouteConfig[] = [
title: '图片管理'
}
},
{
path: 'albums-admin',
key: 'admin-album',
icon: <FolderOutlined />,
label: '相册管理',
element: <AlbumManagement />,
area: 'admin',
groupLabel: '内容管理',
breadcrumb: {
title: '相册管理'
}
},
{
path: 'log',
key: 'admin-log',