Skip to content

Commit bcd8cb7

Browse files
committed
feat: add media page
1 parent d2ef878 commit bcd8cb7

File tree

5 files changed

+968
-2
lines changed

5 files changed

+968
-2
lines changed

src/pages/api/cloudinary/images.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import type { NextApiRequest, NextApiResponse } from 'next';
2+
import cloudinary from './cloudinaryConfig';
3+
4+
export interface CloudinaryImage {
5+
public_id: string;
6+
secure_url: string;
7+
format: string;
8+
bytes: number;
9+
width: number;
10+
height: number;
11+
created_at: string;
12+
folder?: string;
13+
tags?: string[];
14+
context?: Record<string, string>;
15+
}
16+
17+
export interface ImageListResponse {
18+
images: CloudinaryImage[];
19+
next_cursor?: string;
20+
total_count: number;
21+
}
22+
23+
export default async function handler(
24+
req: NextApiRequest,
25+
res: NextApiResponse<ImageListResponse | { error: string }>
26+
) {
27+
if (req.method !== 'GET') {
28+
return res.status(405).json({ error: 'Method not allowed' });
29+
}
30+
31+
try {
32+
const {
33+
folder,
34+
max_results = '20',
35+
next_cursor,
36+
search,
37+
} = req.query;
38+
39+
let expression = 'resource_type:image';
40+
41+
// 如果指定了文件夹
42+
if (folder && typeof folder === 'string') {
43+
expression += ` AND folder:${folder}`;
44+
}
45+
46+
// 如果有搜索关键词
47+
if (search && typeof search === 'string') {
48+
expression += ` AND filename:*${search}*`;
49+
}
50+
51+
const options: any = {
52+
expression,
53+
max_results: parseInt(max_results as string, 10),
54+
sort_by: [['created_at', 'desc']],
55+
with_field: ['context', 'tags'],
56+
};
57+
58+
if (next_cursor && typeof next_cursor === 'string') {
59+
options.next_cursor = next_cursor;
60+
}
61+
62+
const result = await cloudinary.search.expression(expression)
63+
.sort_by('created_at', 'desc')
64+
.max_results(parseInt(max_results as string, 10))
65+
.with_field('context')
66+
.with_field('tags')
67+
.next_cursor(next_cursor as string)
68+
.execute();
69+
70+
const response: ImageListResponse = {
71+
images: result.resources || [],
72+
total_count: result.total_count || 0,
73+
};
74+
75+
if (result.next_cursor) {
76+
response.next_cursor = result.next_cursor;
77+
}
78+
79+
res.status(200).json(response);
80+
} catch (error) {
81+
console.error('获取图片列表失败:', error);
82+
res.status(500).json({ error: '获取图片列表失败' });
83+
}
84+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import type { NextApiRequest, NextApiResponse } from 'next';
2+
import cloudinary from './cloudinaryConfig';
3+
4+
interface UpdateImageRequest {
5+
public_id: string;
6+
tags?: string[];
7+
context?: Record<string, string>;
8+
}
9+
10+
export default async function handler(
11+
req: NextApiRequest,
12+
res: NextApiResponse
13+
) {
14+
if (req.method !== 'POST') {
15+
return res.status(405).json({ error: 'Method not allowed' });
16+
}
17+
18+
try {
19+
const { public_id, tags, context }: UpdateImageRequest = req.body;
20+
21+
if (!public_id) {
22+
return res.status(400).json({ error: 'public_id is required' });
23+
}
24+
25+
const updateOptions: any = {};
26+
27+
if (tags) {
28+
updateOptions.tags = tags;
29+
}
30+
31+
if (context) {
32+
updateOptions.context = context;
33+
}
34+
35+
const result = await cloudinary.uploader.explicit(public_id, {
36+
type: 'upload',
37+
...updateOptions,
38+
});
39+
40+
res.status(200).json({
41+
success: true,
42+
public_id: result.public_id,
43+
tags: result.tags,
44+
context: result.context
45+
});
46+
} catch (error) {
47+
console.error('更新图片元数据失败:', error);
48+
res.status(500).json({ error: '更新图片元数据失败' });
49+
}
50+
}

src/pages/dashboard/index.tsx

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
Pagination,
1515
App as AntdApp
1616
} from 'antd'
17-
import { FileText, Eye, Clock } from 'lucide-react'
17+
import { FileText, Eye, Clock, ImageIcon } from 'lucide-react'
1818
import Link from 'next/link'
1919
import dayjs from 'dayjs'
2020
import styles from './index.module.css'
@@ -26,7 +26,7 @@ import { useSession } from 'next-auth/react'
2626
import { parseMd } from '@/utils/posts'
2727

2828
const { Title, Text } = Typography
29-
type ActiveTab = 'events' | 'articles'
29+
type ActiveTab = 'events' | 'articles' | 'media'
3030

3131
export default function DashboardPage() {
3232
const { message } = AntdApp.useApp()
@@ -118,6 +118,11 @@ export default function DashboardPage() {
118118
key: 'articles',
119119
icon: <FileText className={styles.menuIcon} />,
120120
label: '我的文章'
121+
},
122+
{
123+
key: 'media',
124+
icon: <ImageIcon className={styles.menuIcon} />,
125+
label: '媒体管理'
121126
}
122127
]
123128

@@ -351,6 +356,41 @@ export default function DashboardPage() {
351356
</Card>
352357
)
353358
}
359+
360+
if (activeTab === 'media') {
361+
return (
362+
<Card className={styles.contentCard}>
363+
<div className={styles.cardHeader}>
364+
<Title level={3} className={styles.cardTitle}>
365+
<ImageIcon className={styles.cardIcon} />
366+
媒体管理
367+
</Title>
368+
</div>
369+
<Divider />
370+
<div style={{ padding: '20px 0', textAlign: 'center' }}>
371+
<ImageIcon size={48} style={{ color: '#1890ff', marginBottom: 16 }} />
372+
<Title level={4} style={{ marginBottom: 8 }}>Cloudinary 图片管理</Title>
373+
<Text type="secondary" style={{ display: 'block', marginBottom: 24 }}>
374+
管理您上传的所有图片,支持查看、编辑、删除和复制链接
375+
</Text>
376+
<Link href="/media">
377+
<button style={{
378+
background: '#1890ff',
379+
color: 'white',
380+
border: 'none',
381+
borderRadius: '6px',
382+
padding: '12px 24px',
383+
fontSize: '16px',
384+
cursor: 'pointer',
385+
transition: 'background-color 0.3s'
386+
}}>
387+
进入媒体库
388+
</button>
389+
</Link>
390+
</div>
391+
</Card>
392+
)
393+
}
354394
}
355395

356396
return (

src/pages/media/index.module.css

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
.mediaManager {
2+
padding: 24px;
3+
background: #f5f5f5;
4+
min-height: 100vh;
5+
}
6+
7+
.header {
8+
display: flex;
9+
justify-content: space-between;
10+
align-items: center;
11+
margin-bottom: 24px;
12+
}
13+
14+
.title {
15+
margin: 0;
16+
color: #1f2937;
17+
}
18+
19+
.filterSection {
20+
margin-bottom: 24px;
21+
padding: 16px;
22+
background: white;
23+
border-radius: 8px;
24+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
25+
}
26+
27+
.tableContainer {
28+
background: white;
29+
border-radius: 8px;
30+
overflow: hidden;
31+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
32+
}
33+
34+
.imagePreview {
35+
border-radius: 4px;
36+
transition: all 0.3s ease;
37+
}
38+
39+
.imagePreview:hover {
40+
transform: scale(1.05);
41+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
42+
}
43+
44+
.actionButton {
45+
transition: all 0.3s ease;
46+
}
47+
48+
.actionButton:hover {
49+
transform: translateY(-1px);
50+
}
51+
52+
.uploadArea {
53+
border: 2px dashed #d9d9d9;
54+
border-radius: 8px;
55+
transition: all 0.3s ease;
56+
}
57+
58+
.uploadArea:hover {
59+
border-color: #1890ff;
60+
background: #f0f8ff;
61+
}
62+
63+
.previewModal .imageContainer {
64+
display: flex;
65+
justify-content: center;
66+
margin-bottom: 16px;
67+
}
68+
69+
.previewModal .imageDetails {
70+
background: #fafafa;
71+
padding: 16px;
72+
border-radius: 8px;
73+
}
74+
75+
.editModal .imageThumb {
76+
display: flex;
77+
justify-content: center;
78+
margin-bottom: 16px;
79+
}
80+
81+
.editModal .formItem {
82+
margin-bottom: 16px;
83+
}
84+
85+
.statsCard {
86+
text-align: center;
87+
padding: 16px;
88+
}
89+
90+
.statsNumber {
91+
font-size: 24px;
92+
font-weight: bold;
93+
color: #1890ff;
94+
}
95+
96+
.statsLabel {
97+
color: #8c8c8c;
98+
margin-top: 8px;
99+
}

0 commit comments

Comments
 (0)