mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-09-05 23:57:19 +08:00
feat: enhance mobile responsiveness across settings and image pages
This commit is contained in:
@@ -1,13 +1,15 @@
|
|||||||
import { useState, useRef, useMemo, useCallback } from 'react';
|
import { useState, useRef, useMemo, useCallback } from 'react';
|
||||||
import { Typography, Button, Dropdown, message } from 'antd';
|
import { Typography, Button, Dropdown, message, Row, Col } from 'antd';
|
||||||
import { SortAscendingOutlined, UploadOutlined } from '@ant-design/icons';
|
import { SortAscendingOutlined, UploadOutlined } from '@ant-design/icons';
|
||||||
import type { PictureResponse } from '../../api';
|
import type { PictureResponse } from '../../api';
|
||||||
import ImageUploadDialog from '../../components/upload/ImageUploadDialog';
|
import ImageUploadDialog from '../../components/upload/ImageUploadDialog';
|
||||||
import ImageGrid from '../../components/image/ImageGrid';
|
import ImageGrid from '../../components/image/ImageGrid';
|
||||||
|
import useIsMobile from '../../hooks/useIsMobile';
|
||||||
|
|
||||||
const { Title } = Typography;
|
const { Title } = Typography;
|
||||||
|
|
||||||
function AllImages() {
|
function AllImages() {
|
||||||
|
const isMobile = useIsMobile();
|
||||||
const [images, setImages] = useState<PictureResponse[]>([]);
|
const [images, setImages] = useState<PictureResponse[]>([]);
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [pageSize, setPageSize] = useState(20);
|
const [pageSize, setPageSize] = useState(20);
|
||||||
@@ -76,60 +78,66 @@ function AllImages() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div style={{
|
<div style={{
|
||||||
marginBottom: 50,
|
marginBottom: isMobile ? 30 : 50,
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
zIndex: 1
|
zIndex: 1
|
||||||
}}>
|
}}>
|
||||||
<div>
|
<Row gutter={[16, 16]} align="middle">
|
||||||
<Title level={2} style={{
|
<Col xs={24} sm={24} md={12} lg={12}>
|
||||||
margin: 0,
|
<Title level={2} style={{
|
||||||
marginBottom: 10,
|
margin: 0,
|
||||||
fontWeight: 600,
|
marginBottom: isMobile ? 5 : 10,
|
||||||
letterSpacing: '0.5px',
|
fontWeight: 600,
|
||||||
fontSize: 32,
|
letterSpacing: '0.5px',
|
||||||
background: 'linear-gradient(120deg, #000000, #444444)',
|
fontSize: isMobile ? 24 : 32,
|
||||||
WebkitBackgroundClip: 'text',
|
background: 'linear-gradient(120deg, #000000, #444444)',
|
||||||
WebkitTextFillColor: 'transparent',
|
WebkitBackgroundClip: 'text',
|
||||||
}}>所有图片</Title>
|
WebkitTextFillColor: 'transparent',
|
||||||
</div>
|
textAlign: isMobile ? 'center' : 'left',
|
||||||
<div style={{ display: 'flex', gap: 12 }}>
|
}}>所有图片</Title>
|
||||||
<Button
|
</Col>
|
||||||
type="primary"
|
<Col xs={24} sm={24} md={12} lg={12}>
|
||||||
icon={<UploadOutlined />}
|
<div style={{
|
||||||
style={{
|
|
||||||
borderRadius: 10,
|
|
||||||
height: 46,
|
|
||||||
padding: '0 24px',
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
gap: isMobile ? 8 : 12,
|
||||||
gap: 8,
|
justifyContent: isMobile ? 'center' : 'flex-end'
|
||||||
fontSize: 15
|
|
||||||
}}
|
|
||||||
onClick={() => setIsUploadDialogVisible(true)}
|
|
||||||
>
|
|
||||||
上传图片
|
|
||||||
</Button>
|
|
||||||
<Dropdown menu={sortMenu} placement="bottomRight">
|
|
||||||
<Button style={{
|
|
||||||
borderRadius: 10,
|
|
||||||
height: 46,
|
|
||||||
border: '1px solid #f0f0f0',
|
|
||||||
padding: '0 24px',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 8,
|
|
||||||
fontSize: 15,
|
|
||||||
boxShadow: '0 2px 8px rgba(0,0,0,0.02)',
|
|
||||||
background: '#ffffff'
|
|
||||||
}}>
|
}}>
|
||||||
<SortAscendingOutlined />
|
<Button
|
||||||
排序方式
|
type="primary"
|
||||||
</Button>
|
icon={<UploadOutlined />}
|
||||||
</Dropdown>
|
style={{
|
||||||
</div>
|
borderRadius: 10,
|
||||||
|
height: isMobile ? 40 : 46,
|
||||||
|
padding: isMobile ? '0 15px' : '0 24px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 8,
|
||||||
|
fontSize: isMobile ? 14 : 15
|
||||||
|
}}
|
||||||
|
onClick={() => setIsUploadDialogVisible(true)}
|
||||||
|
>
|
||||||
|
上传图片
|
||||||
|
</Button>
|
||||||
|
<Dropdown menu={sortMenu} placement="bottomRight">
|
||||||
|
<Button style={{
|
||||||
|
borderRadius: 10,
|
||||||
|
height: isMobile ? 40 : 46,
|
||||||
|
border: '1px solid #f0f0f0',
|
||||||
|
padding: isMobile ? '0 15px' : '0 24px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 8,
|
||||||
|
fontSize: isMobile ? 14 : 15,
|
||||||
|
boxShadow: '0 2px 8px rgba(0,0,0,0.02)',
|
||||||
|
background: '#ffffff'
|
||||||
|
}}>
|
||||||
|
<SortAscendingOutlined />
|
||||||
|
{!isMobile && "排序方式"}
|
||||||
|
</Button>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ImageGrid
|
<ImageGrid
|
||||||
|
|||||||
@@ -19,12 +19,14 @@ import {
|
|||||||
import ImageGrid from '../../components/image/ImageGrid';
|
import ImageGrid from '../../components/image/ImageGrid';
|
||||||
import type { PictureResponse } from '../../api/types';
|
import type { PictureResponse } from '../../api/types';
|
||||||
import { getFilteredTags } from '../../api/tagApi';
|
import { getFilteredTags } from '../../api/tagApi';
|
||||||
|
import useIsMobile from '../../hooks/useIsMobile';
|
||||||
|
|
||||||
const { Title, Text, Paragraph } = Typography;
|
const { Title, Text, Paragraph } = Typography;
|
||||||
const { Search } = Input;
|
const { Search } = Input;
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
|
|
||||||
const PixHub: React.FC = () => {
|
const PixHub: React.FC = () => {
|
||||||
|
const isMobile = useIsMobile();
|
||||||
const [activeCategory, setActiveCategory] = useState('全部');
|
const [activeCategory, setActiveCategory] = useState('全部');
|
||||||
const [sortBy, setSortBy] = useState('newest');
|
const [sortBy, setSortBy] = useState('newest');
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
@@ -84,28 +86,50 @@ const PixHub: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="image-square">
|
<div className="image-square">
|
||||||
<div className="page-header" style={{ marginBottom: 32 }}>
|
<div className="page-header" style={{ marginBottom: isMobile ? 20 : 32 }}>
|
||||||
<Row gutter={[24, 24]} align="middle">
|
<Row gutter={[24, isMobile ? 12 : 24]} align="middle">
|
||||||
<Col lg={10} md={12} sm={24} xs={24}>
|
<Col lg={10} md={12} sm={24} xs={24}>
|
||||||
<Title level={2} style={{ marginBottom: 8, fontWeight: 600 }}>
|
<Title level={2} style={{
|
||||||
|
marginBottom: 8,
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: isMobile ? 24 : 30,
|
||||||
|
textAlign: isMobile ? 'center' : 'left'
|
||||||
|
}}>
|
||||||
图片广场
|
图片广场
|
||||||
<Text style={{ fontSize: 16, fontWeight: 400, marginLeft: 12, color: '#8c8c8c' }}>
|
{!isMobile && (
|
||||||
|
<Text style={{ fontSize: 16, fontWeight: 400, marginLeft: 12, color: '#8c8c8c' }}>
|
||||||
|
探索世界各地的精彩瞬间
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Title>
|
||||||
|
{isMobile && (
|
||||||
|
<Text style={{
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 400,
|
||||||
|
color: '#8c8c8c',
|
||||||
|
display: 'block',
|
||||||
|
textAlign: 'center'
|
||||||
|
}}>
|
||||||
探索世界各地的精彩瞬间
|
探索世界各地的精彩瞬间
|
||||||
</Text>
|
</Text>
|
||||||
</Title>
|
)}
|
||||||
<Paragraph style={{ color: '#666666' }}>
|
<Paragraph style={{
|
||||||
|
color: '#666666',
|
||||||
|
textAlign: isMobile ? 'center' : 'left',
|
||||||
|
marginBottom: isMobile ? 5 : 'inherit'
|
||||||
|
}}>
|
||||||
发现创作者分享的高质量摄影作品,获取灵感,找到你喜爱的风格
|
发现创作者分享的高质量摄影作品,获取灵感,找到你喜爱的风格
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Col lg={14} md={12} sm={24} xs={24}>
|
<Col lg={14} md={12} sm={24} xs={24}>
|
||||||
<Row gutter={[16, 16]} justify="end">
|
<Row gutter={[16, 16]} justify={isMobile ? "center" : "end"}>
|
||||||
<Col lg={16} md={16} sm={16} xs={24}>
|
<Col lg={16} md={16} sm={16} xs={24}>
|
||||||
<Search
|
<Search
|
||||||
placeholder="搜索图片、标签或创作者"
|
placeholder="搜索图片、标签或创作者"
|
||||||
allowClear
|
allowClear
|
||||||
enterButton={<SearchOutlined />}
|
enterButton={<SearchOutlined />}
|
||||||
size="large"
|
size={isMobile ? "middle" : "large"}
|
||||||
onSearch={(value) => setSearchQuery(value)}
|
onSearch={(value) => setSearchQuery(value)}
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
/>
|
/>
|
||||||
@@ -113,7 +137,7 @@ const PixHub: React.FC = () => {
|
|||||||
<Col lg={8} md={8} sm={8} xs={24}>
|
<Col lg={8} md={8} sm={8} xs={24}>
|
||||||
<Select
|
<Select
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
size="large"
|
size={isMobile ? "middle" : "large"}
|
||||||
value={sortBy}
|
value={sortBy}
|
||||||
onChange={(value) => setSortBy(value)}
|
onChange={(value) => setSortBy(value)}
|
||||||
suffixIcon={<FilterOutlined />}
|
suffixIcon={<FilterOutlined />}
|
||||||
@@ -128,18 +152,22 @@ const PixHub: React.FC = () => {
|
|||||||
</Row>
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="category-nav" style={{ marginBottom: 28, overflowX: 'auto' }}>
|
<div className="category-nav" style={{
|
||||||
<Space size={[12, 20]} wrap style={{ justifyContent: 'center' }}>
|
marginBottom: isMobile ? 20 : 28,
|
||||||
|
overflowX: 'auto',
|
||||||
|
paddingBottom: 8
|
||||||
|
}}>
|
||||||
|
<Space size={[isMobile ? 8 : 12, isMobile ? 10 : 20]} wrap style={{ justifyContent: 'center' }}>
|
||||||
{categories.map(category => (
|
{categories.map(category => (
|
||||||
<Button
|
<Button
|
||||||
key={category}
|
key={category}
|
||||||
type={activeCategory === category ? "primary" : "default"}
|
type={activeCategory === category ? "primary" : "default"}
|
||||||
shape="round"
|
shape="round"
|
||||||
size="large"
|
size={isMobile ? "middle" : "large"}
|
||||||
onClick={() => setActiveCategory(category)}
|
onClick={() => setActiveCategory(category)}
|
||||||
style={{
|
style={{
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
minWidth: 80,
|
minWidth: isMobile ? 70 : 80,
|
||||||
boxShadow: activeCategory === category ? '0 4px 12px rgba(0,0,0,0.15)' : 'none',
|
boxShadow: activeCategory === category ? '0 4px 12px rgba(0,0,0,0.15)' : 'none',
|
||||||
}}
|
}}
|
||||||
icon={category === '全部' ? <ThunderboltOutlined /> : null}
|
icon={category === '全部' ? <ThunderboltOutlined /> : null}
|
||||||
@@ -151,12 +179,12 @@ const PixHub: React.FC = () => {
|
|||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Divider style={{ margin: '24px 0' }} />
|
<Divider style={{ margin: isMobile ? '16px 0' : '24px 0' }} />
|
||||||
|
|
||||||
<div className="results-info" style={{ marginBottom: 24 }}>
|
<div className="results-info" style={{ marginBottom: isMobile ? 16 : 24 }}>
|
||||||
<Row justify="space-between" align="middle">
|
<Row justify="space-between" align="middle">
|
||||||
<Col>
|
<Col>
|
||||||
<Text style={{ fontSize: 15 }}>
|
<Text style={{ fontSize: isMobile ? 14 : 15 }}>
|
||||||
找到 <strong>{totalCount}</strong> 张图片
|
找到 <strong>{totalCount}</strong> 张图片
|
||||||
{activeCategory !== '全部' && <span> · {activeCategory}分类</span>}
|
{activeCategory !== '全部' && <span> · {activeCategory}分类</span>}
|
||||||
{searchQuery && <span> · 搜索"{searchQuery}"</span>}
|
{searchQuery && <span> · 搜索"{searchQuery}"</span>}
|
||||||
|
|||||||
@@ -1,219 +0,0 @@
|
|||||||
// 模拟图片数据
|
|
||||||
export const mockImages = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
title: '湖畔日落',
|
|
||||||
url: 'https://images.unsplash.com/photo-1501785888041-af3ef285b470?ixlib=rb-1.2.1&auto=format&fit=crop&w=1000&q=80',
|
|
||||||
category: '风景',
|
|
||||||
tags: ['风景', '日落', '自然'],
|
|
||||||
likes: 342,
|
|
||||||
views: 1205,
|
|
||||||
favorited: false,
|
|
||||||
liked: true,
|
|
||||||
createdAt: '2023-09-15T10:30:00Z',
|
|
||||||
description: '湖边美丽的日落景色,金色的阳光洒在水面上,形成了壮观的景象。',
|
|
||||||
author: {
|
|
||||||
id: 101,
|
|
||||||
name: '王摄影',
|
|
||||||
avatar: 'https://randomuser.me/api/portraits/men/32.jpg',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
title: '都市天际线',
|
|
||||||
url: 'https://images.unsplash.com/photo-1477959858617-67f85cf4f1df?ixlib=rb-1.2.1&auto=format&fit=crop&w=1000&q=80',
|
|
||||||
category: '城市',
|
|
||||||
tags: ['城市', '建筑', '天际线'],
|
|
||||||
likes: 278,
|
|
||||||
views: 890,
|
|
||||||
favorited: true,
|
|
||||||
liked: false,
|
|
||||||
createdAt: '2023-10-02T14:45:00Z',
|
|
||||||
description: '现代城市的壮观天际线,高楼大厦在夕阳映衬下显得格外壮观。',
|
|
||||||
author: {
|
|
||||||
id: 102,
|
|
||||||
name: '城市猎影',
|
|
||||||
avatar: 'https://randomuser.me/api/portraits/women/44.jpg',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
title: '山谷晨雾',
|
|
||||||
url: 'https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?ixlib=rb-1.2.1&auto=format&fit=crop&w=1000&q=80',
|
|
||||||
category: '自然',
|
|
||||||
tags: ['山', '晨雾', '自然'],
|
|
||||||
likes: 432,
|
|
||||||
views: 1560,
|
|
||||||
favorited: false,
|
|
||||||
liked: false,
|
|
||||||
createdAt: '2023-08-05T06:15:00Z',
|
|
||||||
description: '清晨的山谷被薄雾笼罩,阳光穿透云层形成光束,构成一幅绝美的画面。',
|
|
||||||
author: {
|
|
||||||
id: 103,
|
|
||||||
name: '山野行者',
|
|
||||||
avatar: 'https://randomuser.me/api/portraits/men/76.jpg',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
title: '意式咖啡',
|
|
||||||
url: 'https://images.unsplash.com/photo-1513558161293-cdaf765ed2fd?ixlib=rb-1.2.1&auto=format&fit=crop&w=1000&q=80',
|
|
||||||
category: '美食',
|
|
||||||
tags: ['咖啡', '美食', '生活'],
|
|
||||||
likes: 189,
|
|
||||||
views: 745,
|
|
||||||
favorited: true,
|
|
||||||
liked: true,
|
|
||||||
createdAt: '2023-10-12T09:20:00Z',
|
|
||||||
description: '精心制作的意式浓缩咖啡,香气四溢,是一天完美的开始。',
|
|
||||||
author: {
|
|
||||||
id: 104,
|
|
||||||
name: '美食家',
|
|
||||||
avatar: 'https://randomuser.me/api/portraits/women/91.jpg',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
title: '森林小径',
|
|
||||||
url: 'https://images.unsplash.com/photo-1441974231531-c6227db76b6e?ixlib=rb-1.2.1&auto=format&fit=crop&w=1000&q=80',
|
|
||||||
category: '自然',
|
|
||||||
tags: ['森林', '自然', '小径'],
|
|
||||||
likes: 256,
|
|
||||||
views: 920,
|
|
||||||
favorited: false,
|
|
||||||
liked: false,
|
|
||||||
createdAt: '2023-09-28T15:30:00Z',
|
|
||||||
description: '穿过茂密森林的小径,阳光透过树叶形成斑驳的光影效果。',
|
|
||||||
author: {
|
|
||||||
id: 105,
|
|
||||||
name: '自然探索者',
|
|
||||||
avatar: 'https://randomuser.me/api/portraits/men/55.jpg',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 6,
|
|
||||||
title: '古典建筑',
|
|
||||||
url: 'https://images.unsplash.com/photo-1496568816309-51d7c20e3b21?ixlib=rb-1.2.1&auto=format&fit=crop&w=1000&q=80',
|
|
||||||
category: '建筑',
|
|
||||||
tags: ['建筑', '古典', '艺术'],
|
|
||||||
likes: 312,
|
|
||||||
views: 1105,
|
|
||||||
favorited: true,
|
|
||||||
liked: false,
|
|
||||||
createdAt: '2023-07-19T11:40:00Z',
|
|
||||||
description: '充满历史感的欧洲古典建筑,精美的细节和宏伟的设计令人惊叹。',
|
|
||||||
author: {
|
|
||||||
id: 106,
|
|
||||||
name: '建筑师',
|
|
||||||
avatar: 'https://randomuser.me/api/portraits/women/22.jpg',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 7,
|
|
||||||
title: '夏日海滩',
|
|
||||||
url: 'https://images.unsplash.com/photo-1520454974749-611b7248ffdb?ixlib=rb-1.2.1&auto=format&fit=crop&w=1000&q=80',
|
|
||||||
category: '旅行',
|
|
||||||
tags: ['海滩', '旅行', '夏日'],
|
|
||||||
likes: 385,
|
|
||||||
views: 1320,
|
|
||||||
favorited: false,
|
|
||||||
liked: true,
|
|
||||||
createdAt: '2023-08-22T13:50:00Z',
|
|
||||||
description: '阳光明媚的夏日海滩,湛蓝的海水和金色的沙滩构成完美的度假胜地。',
|
|
||||||
author: {
|
|
||||||
id: 107,
|
|
||||||
name: '旅行摄影师',
|
|
||||||
avatar: 'https://randomuser.me/api/portraits/men/29.jpg',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 8,
|
|
||||||
title: '雨后街景',
|
|
||||||
url: 'https://images.unsplash.com/photo-1520116468816-95b69f847357?ixlib=rb-1.2.1&auto=format&fit=crop&w=1000&q=80',
|
|
||||||
category: '城市',
|
|
||||||
tags: ['城市', '雨天', '生活'],
|
|
||||||
likes: 197,
|
|
||||||
views: 876,
|
|
||||||
favorited: true,
|
|
||||||
liked: true,
|
|
||||||
createdAt: '2023-10-05T18:25:00Z',
|
|
||||||
description: '雨后的城市街道,湿漉漉的路面反射着霓虹灯光,营造出独特的都市氛围。',
|
|
||||||
author: {
|
|
||||||
id: 108,
|
|
||||||
name: '城市观察者',
|
|
||||||
avatar: 'https://randomuser.me/api/portraits/women/67.jpg',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 9,
|
|
||||||
title: '宁静湖泊',
|
|
||||||
url: 'https://images.unsplash.com/photo-1508739773434-c26b3d09e071?ixlib=rb-1.2.1&auto=format&fit=crop&w=1000&q=80',
|
|
||||||
category: '风景',
|
|
||||||
tags: ['湖泊', '风景', '宁静'],
|
|
||||||
likes: 274,
|
|
||||||
views: 945,
|
|
||||||
favorited: false,
|
|
||||||
liked: false,
|
|
||||||
createdAt: '2023-08-30T07:10:00Z',
|
|
||||||
description: '清晨宁静的湖泊,平静的湖面如同一面镜子,倒映着周围的山景和天空。',
|
|
||||||
author: {
|
|
||||||
id: 109,
|
|
||||||
name: '风景摄影家',
|
|
||||||
avatar: 'https://randomuser.me/api/portraits/men/42.jpg',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 10,
|
|
||||||
title: '人像写真',
|
|
||||||
url: 'https://images.unsplash.com/photo-1531746020798-e6953c6e8e04?ixlib=rb-1.2.1&auto=format&fit=crop&w=1000&q=80',
|
|
||||||
category: '人像',
|
|
||||||
tags: ['人像', '艺术', '写真'],
|
|
||||||
likes: 364,
|
|
||||||
views: 1230,
|
|
||||||
favorited: true,
|
|
||||||
liked: false,
|
|
||||||
createdAt: '2023-09-18T16:40:00Z',
|
|
||||||
description: '艺术风格的人像摄影,通过光影和构图展现主题的个性与魅力。',
|
|
||||||
author: {
|
|
||||||
id: 110,
|
|
||||||
name: '人像摄影师',
|
|
||||||
avatar: 'https://randomuser.me/api/portraits/women/33.jpg',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 11,
|
|
||||||
title: '繁星夜空',
|
|
||||||
url: 'https://images.unsplash.com/photo-1489549132488-d00b7eee80f1?ixlib=rb-1.2.1&auto=format&fit=crop&w=1000&q=80',
|
|
||||||
category: '自然',
|
|
||||||
tags: ['夜空', '星星', '自然'],
|
|
||||||
likes: 412,
|
|
||||||
views: 1480,
|
|
||||||
favorited: false,
|
|
||||||
liked: true,
|
|
||||||
createdAt: '2023-07-28T22:15:00Z',
|
|
||||||
description: '壮丽的夜空星河,数以万计的星星点缀着黑色的天幕,令人叹为观止。',
|
|
||||||
author: {
|
|
||||||
id: 111,
|
|
||||||
name: '星空摄影师',
|
|
||||||
avatar: 'https://randomuser.me/api/portraits/men/18.jpg',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 12,
|
|
||||||
title: '历史建筑',
|
|
||||||
url: 'https://images.unsplash.com/photo-1526378722484-bd91ca387e72?ixlib=rb-1.2.1&auto=format&fit=crop&w=1000&q=80',
|
|
||||||
category: '建筑',
|
|
||||||
tags: ['建筑', '历史', '艺术'],
|
|
||||||
likes: 287,
|
|
||||||
views: 1050,
|
|
||||||
favorited: true,
|
|
||||||
liked: false,
|
|
||||||
createdAt: '2023-08-12T10:05:00Z',
|
|
||||||
description: '充满历史感的古老建筑,每一块砖石都诉说着岁月的故事。',
|
|
||||||
author: {
|
|
||||||
id: 112,
|
|
||||||
name: '历史摄影',
|
|
||||||
avatar: 'https://randomuser.me/api/portraits/women/52.jpg',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
@@ -11,13 +11,15 @@ interface ConfigGroupProps {
|
|||||||
descriptions: {
|
descriptions: {
|
||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
};
|
};
|
||||||
|
isMobile?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ConfigGroup: React.FC<ConfigGroupProps> = ({
|
const ConfigGroup: React.FC<ConfigGroupProps> = ({
|
||||||
groupName,
|
groupName,
|
||||||
configs,
|
configs,
|
||||||
onSave,
|
onSave,
|
||||||
descriptions
|
descriptions,
|
||||||
|
isMobile = false
|
||||||
}) => {
|
}) => {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
@@ -48,9 +50,10 @@ const ConfigGroup: React.FC<ConfigGroupProps> = ({
|
|||||||
form={form}
|
form={form}
|
||||||
layout="vertical"
|
layout="vertical"
|
||||||
initialValues={configs}
|
initialValues={configs}
|
||||||
|
size={isMobile ? "middle" : "large"}
|
||||||
>
|
>
|
||||||
{Object.keys(configs).map(key => (
|
{Object.keys(configs).map(key => (
|
||||||
<Row key={key} gutter={16} align="middle">
|
<Row key={key} gutter={isMobile ? [8, 8] : [16, 16]} align="middle">
|
||||||
<Col xs={24} lg={16}>
|
<Col xs={24} lg={16}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name={key}
|
name={key}
|
||||||
@@ -72,12 +75,20 @@ const ConfigGroup: React.FC<ConfigGroupProps> = ({
|
|||||||
)}
|
)}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} lg={8} style={{ textAlign: 'right' }}>
|
<Col xs={24} lg={8} style={{
|
||||||
|
textAlign: isMobile ? 'left' : 'right',
|
||||||
|
marginTop: isMobile ? -10 : 0,
|
||||||
|
marginBottom: isMobile ? 10 : 0
|
||||||
|
}}>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<SaveOutlined />}
|
icon={<SaveOutlined />}
|
||||||
onClick={() => handleSaveSingle(key)}
|
onClick={() => handleSaveSingle(key)}
|
||||||
style={{ marginBottom: 24 }}
|
style={{
|
||||||
|
marginBottom: isMobile ? 16 : 24,
|
||||||
|
width: isMobile ? '100%' : 'auto'
|
||||||
|
}}
|
||||||
|
size={isMobile ? "middle" : "large"}
|
||||||
>
|
>
|
||||||
保存
|
保存
|
||||||
</Button>
|
</Button>
|
||||||
@@ -89,8 +100,9 @@ const ConfigGroup: React.FC<ConfigGroupProps> = ({
|
|||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={handleSaveAll}
|
onClick={handleSaveAll}
|
||||||
style={{ marginTop: 16 }}
|
style={{ marginTop: isMobile ? 8 : 16 }}
|
||||||
block
|
block
|
||||||
|
size={isMobile ? "middle" : "large"}
|
||||||
>
|
>
|
||||||
保存所有
|
保存所有
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { Tabs, Layout, Menu } from 'antd';
|
import { Tabs, Layout, Menu, Space } from 'antd';
|
||||||
import { useAuth } from '../../api/AuthContext';
|
import { useAuth } from '../../api/AuthContext';
|
||||||
import { UserRole } from '../../api/types';
|
import { UserRole } from '../../api/types';
|
||||||
import { useState, type SetStateAction } from 'react';
|
import { useState, type SetStateAction } from 'react';
|
||||||
import SystemConfig from './SystemConfig.tsx';
|
import SystemConfig from './SystemConfig.tsx';
|
||||||
import UserProfile from './UserProfile.tsx';
|
import UserProfile from './UserProfile.tsx';
|
||||||
|
import useIsMobile from '../../hooks/useIsMobile';
|
||||||
import {
|
import {
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
SettingOutlined,
|
SettingOutlined,
|
||||||
@@ -16,6 +17,7 @@ const { Sider, Content } = Layout;
|
|||||||
|
|
||||||
function Settings() {
|
function Settings() {
|
||||||
const { hasRole } = useAuth();
|
const { hasRole } = useAuth();
|
||||||
|
const isMobile = useIsMobile();
|
||||||
const [activeMenu, setActiveMenu] = useState('profile');
|
const [activeMenu, setActiveMenu] = useState('profile');
|
||||||
const [activeTab, setActiveTab] = useState('basic');
|
const [activeTab, setActiveTab] = useState('basic');
|
||||||
|
|
||||||
@@ -107,6 +109,44 @@ function Settings() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 手机版使用Tabs作为顶部导航
|
||||||
|
if (isMobile) {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: 0 }}>
|
||||||
|
<Tabs
|
||||||
|
activeKey={activeMenu}
|
||||||
|
onChange={(key) => handleMenuChange(key)}
|
||||||
|
centered
|
||||||
|
size="large"
|
||||||
|
tabBarStyle={{
|
||||||
|
marginBottom: 16,
|
||||||
|
fontWeight: 500,
|
||||||
|
backgroundColor: '#f5f5f5',
|
||||||
|
padding: '8px 0',
|
||||||
|
borderRadius: '8px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{menuItems.map((item) => (
|
||||||
|
<TabPane
|
||||||
|
tab={
|
||||||
|
<Space size={4}>
|
||||||
|
{item?.icon}
|
||||||
|
<span>{item?.label}</span>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
key={item?.key || ''}
|
||||||
|
>
|
||||||
|
<div style={{ padding: '0 4px' }}>
|
||||||
|
{renderContent()}
|
||||||
|
</div>
|
||||||
|
</TabPane>
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 桌面版使用侧边栏
|
||||||
return (
|
return (
|
||||||
<Layout style={{
|
<Layout style={{
|
||||||
background: '#fff',
|
background: '#fff',
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Tabs, Card, message, Spin, Select } from 'antd';
|
|||||||
import { CloudOutlined, DatabaseOutlined } from '@ant-design/icons';
|
import { CloudOutlined, DatabaseOutlined } from '@ant-design/icons';
|
||||||
import { getAllConfigs, setConfig } from '../../api';
|
import { getAllConfigs, setConfig } from '../../api';
|
||||||
import ConfigGroup from './ConfigGroup.tsx';
|
import ConfigGroup from './ConfigGroup.tsx';
|
||||||
|
import useIsMobile from '../../hooks/useIsMobile';
|
||||||
|
|
||||||
const { TabPane } = Tabs;
|
const { TabPane } = Tabs;
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
@@ -14,6 +15,7 @@ interface ConfigStructure {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SystemConfig: React.FC = () => {
|
const SystemConfig: React.FC = () => {
|
||||||
|
const isMobile = useIsMobile();
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [configs, setConfigs] = useState<ConfigStructure>({});
|
const [configs, setConfigs] = useState<ConfigStructure>({});
|
||||||
const [activeKey, setActiveKey] = useState('AI');
|
const [activeKey, setActiveKey] = useState('AI');
|
||||||
@@ -92,13 +94,27 @@ const SystemConfig: React.FC = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card title="系统配置" className="system-config-card">
|
<Card
|
||||||
|
title="系统配置"
|
||||||
|
className="system-config-card"
|
||||||
|
bodyStyle={{
|
||||||
|
padding: isMobile ? '12px 8px' : '24px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div style={{ textAlign: 'center', padding: '20px' }}>
|
<div style={{ textAlign: 'center', padding: '20px' }}>
|
||||||
<Spin tip="加载配置中..." />
|
<Spin tip="加载配置中..." />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Tabs activeKey={activeKey} onChange={setActiveKey}>
|
<Tabs
|
||||||
|
activeKey={activeKey}
|
||||||
|
onChange={setActiveKey}
|
||||||
|
size={isMobile ? "small" : "middle"}
|
||||||
|
tabPosition={isMobile ? "top" : "left"}
|
||||||
|
style={{
|
||||||
|
minHeight: isMobile ? 'auto' : 400
|
||||||
|
}}
|
||||||
|
>
|
||||||
<TabPane tab="AI 设置" key="AI">
|
<TabPane tab="AI 设置" key="AI">
|
||||||
<ConfigGroup
|
<ConfigGroup
|
||||||
groupName="AI"
|
groupName="AI"
|
||||||
@@ -115,6 +131,7 @@ const SystemConfig: React.FC = () => {
|
|||||||
Model: 'AI 模型名称',
|
Model: 'AI 模型名称',
|
||||||
EmbeddingModel: '嵌入向量模型名称'
|
EmbeddingModel: '嵌入向量模型名称'
|
||||||
}}
|
}}
|
||||||
|
isMobile={isMobile}
|
||||||
/>
|
/>
|
||||||
</TabPane>
|
</TabPane>
|
||||||
|
|
||||||
@@ -166,14 +183,28 @@ const SystemConfig: React.FC = () => {
|
|||||||
</TabPane>
|
</TabPane>
|
||||||
|
|
||||||
<TabPane tab="存储设置" key="Storage">
|
<TabPane tab="存储设置" key="Storage">
|
||||||
<div style={{ marginBottom: 20 }}>
|
<div style={{
|
||||||
<span style={{ marginRight: 8, display: 'inline-block', width: 100 }}>默认存储:</span>
|
marginBottom: isMobile ? 16 : 20,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: isMobile ? 'column' : 'row',
|
||||||
|
alignItems: isMobile ? 'flex-start' : 'center',
|
||||||
|
gap: isMobile ? 8 : 0
|
||||||
|
}}>
|
||||||
|
<span style={{
|
||||||
|
marginRight: isMobile ? 0 : 8,
|
||||||
|
display: 'inline-block',
|
||||||
|
width: isMobile ? 'auto' : 100,
|
||||||
|
marginBottom: isMobile ? 4 : 0
|
||||||
|
}}>
|
||||||
|
默认存储:
|
||||||
|
</span>
|
||||||
<Select
|
<Select
|
||||||
value={configs.Storage?.DefaultStorage || 'Telegram'}
|
value={configs.Storage?.DefaultStorage || 'Telegram'}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
handleSaveConfig('Storage', 'DefaultStorage', value);
|
handleSaveConfig('Storage', 'DefaultStorage', value);
|
||||||
}}
|
}}
|
||||||
style={{ width: 200 }}
|
style={{ width: isMobile ? '100%' : 200 }}
|
||||||
|
size={isMobile ? "middle" : "large"}
|
||||||
>
|
>
|
||||||
{storageOptions.map(option => (
|
{storageOptions.map(option => (
|
||||||
<Option key={option.value} value={option.value}>
|
<Option key={option.value} value={option.value}>
|
||||||
@@ -186,14 +217,28 @@ const SystemConfig: React.FC = () => {
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ marginBottom: 20 }}>
|
<div style={{
|
||||||
<span style={{ marginRight: 8, display: 'inline-block', width: 100 }}>配置存储:</span>
|
marginBottom: isMobile ? 16 : 20,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: isMobile ? 'column' : 'row',
|
||||||
|
alignItems: isMobile ? 'flex-start' : 'center',
|
||||||
|
gap: isMobile ? 8 : 0
|
||||||
|
}}>
|
||||||
|
<span style={{
|
||||||
|
marginRight: isMobile ? 0 : 8,
|
||||||
|
display: 'inline-block',
|
||||||
|
width: isMobile ? 'auto' : 100,
|
||||||
|
marginBottom: isMobile ? 4 : 0
|
||||||
|
}}>
|
||||||
|
配置存储:
|
||||||
|
</span>
|
||||||
<Select
|
<Select
|
||||||
value={storageType}
|
value={storageType}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
setStorageType(value);
|
setStorageType(value);
|
||||||
}}
|
}}
|
||||||
style={{ width: 200 }}
|
style={{ width: isMobile ? '100%' : 200 }}
|
||||||
|
size={isMobile ? "middle" : "large"}
|
||||||
>
|
>
|
||||||
{storageOptions.map(option => (
|
{storageOptions.map(option => (
|
||||||
<Option key={option.value} value={option.value}>
|
<Option key={option.value} value={option.value}>
|
||||||
@@ -218,6 +263,7 @@ const SystemConfig: React.FC = () => {
|
|||||||
"TelegramStorageBotToken": 'Telegram 机器人令牌',
|
"TelegramStorageBotToken": 'Telegram 机器人令牌',
|
||||||
"TelegramStorageChatId": 'Telegram 聊天ID'
|
"TelegramStorageChatId": 'Telegram 聊天ID'
|
||||||
}}
|
}}
|
||||||
|
isMobile={isMobile}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</TabPane>
|
</TabPane>
|
||||||
|
|||||||
@@ -2,24 +2,44 @@ import React from 'react';
|
|||||||
import { Card, Form, Input, Button } from 'antd';
|
import { Card, Form, Input, Button } from 'antd';
|
||||||
import { useAuth } from '../../api/AuthContext';
|
import { useAuth } from '../../api/AuthContext';
|
||||||
import UserAvatar from '../../components/UserAvatar';
|
import UserAvatar from '../../components/UserAvatar';
|
||||||
|
import useIsMobile from '../../hooks/useIsMobile';
|
||||||
|
|
||||||
const UserProfile: React.FC = () => {
|
const UserProfile: React.FC = () => {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card title="个人资料" style={{ maxWidth: 600 }}>
|
<Card
|
||||||
<div style={{ display: 'flex', justifyContent: 'center', marginBottom: 24 }}>
|
title="个人资料"
|
||||||
|
style={{
|
||||||
|
maxWidth: '100%',
|
||||||
|
margin: isMobile ? '0 auto' : '0 auto',
|
||||||
|
boxShadow: isMobile ? 'none' : '0 1px 3px rgba(0,0,0,0.1)'
|
||||||
|
}}
|
||||||
|
bodyStyle={{
|
||||||
|
padding: isMobile ? '16px 12px' : '24px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginBottom: isMobile ? 16 : 24
|
||||||
|
}}>
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
size={100}
|
size={isMobile ? 80 : 100}
|
||||||
email={user?.email}
|
email={user?.email}
|
||||||
text={user?.userName}
|
text={user?.userName}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Form layout="vertical" initialValues={{
|
<Form
|
||||||
username: user?.userName || '',
|
layout="vertical"
|
||||||
email: user?.email || '',
|
initialValues={{
|
||||||
}}>
|
username: user?.userName || '',
|
||||||
|
email: user?.email || '',
|
||||||
|
}}
|
||||||
|
size={isMobile ? "middle" : "large"}
|
||||||
|
>
|
||||||
<Form.Item name="username" label="用户名">
|
<Form.Item name="username" label="用户名">
|
||||||
<Input placeholder="用户名" />
|
<Input placeholder="用户名" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -37,7 +57,16 @@ const UserProfile: React.FC = () => {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Button type="primary">保存更改</Button>
|
<Button
|
||||||
|
type="primary"
|
||||||
|
block={isMobile}
|
||||||
|
size={isMobile ? "middle" : "large"}
|
||||||
|
style={{
|
||||||
|
height: isMobile ? 40 : 'auto'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
保存更改
|
||||||
|
</Button>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
Reference in New Issue
Block a user