diff --git a/package.json b/package.json
index 002ab2b..2e73cd3 100644
--- a/package.json
+++ b/package.json
@@ -10,8 +10,12 @@
"preview": "vite preview"
},
"dependencies": {
+ "@ant-design/icons": "^6.1.0",
+ "@ant-design/v5-patch-for-react-19": "^1.0.3",
+ "antd": "^5.27.0",
"react": "^19.1.1",
- "react-dom": "^19.1.1"
+ "react-dom": "^19.1.1",
+ "react-router-dom": "^6.30.1"
},
"devDependencies": {
"@eslint/js": "^9.36.0",
diff --git a/src/App.tsx b/src/App.tsx
index 3d7ded3..081172b 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,35 +1,8 @@
-import { useState } from 'react'
-import reactLogo from './assets/react.svg'
-import viteLogo from '/vite.svg'
-import './App.css'
+import React from 'react';
+import LayoutWrapper from './components/LayoutWrapper';
function App() {
- const [count, setCount] = useState(0)
-
- return (
- <>
-
- Vite + React
-
-
-
- Edit src/App.tsx and save to test HMR
-
-
-
- Click on the Vite and React logos to learn more
-
- >
- )
+ return ;
}
-export default App
+export default App;
diff --git a/src/components/HintText.css b/src/components/HintText.css
new file mode 100644
index 0000000..cf451de
--- /dev/null
+++ b/src/components/HintText.css
@@ -0,0 +1,2 @@
+/* 提示信息组件样式 */
+/* 文字样式在 layout.css 的 .hint-text 中定义,此处预留扩展 */
\ No newline at end of file
diff --git a/src/components/HintText.tsx b/src/components/HintText.tsx
new file mode 100644
index 0000000..34fabd3
--- /dev/null
+++ b/src/components/HintText.tsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import './HintText.css';
+
+const HintText: React.FC = () => {
+ return (
+
+ 提示:支持输入多行文本与上传图片。按 Enter 发送,Shift+Enter 换行。
+
+ );
+};
+
+export default HintText;
\ No newline at end of file
diff --git a/src/components/InputPanel.css b/src/components/InputPanel.css
new file mode 100644
index 0000000..d7f9799
--- /dev/null
+++ b/src/components/InputPanel.css
@@ -0,0 +1,19 @@
+/* 输入面板组件样式 */
+.input-panel {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.input-panel .ant-input-outlined,
+.input-panel .ant-input {
+ background: rgba(255,255,255,0.04);
+ color: #e5e7eb;
+}
+
+.input-actions {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 8px;
+}
\ No newline at end of file
diff --git a/src/components/InputPanel.tsx b/src/components/InputPanel.tsx
new file mode 100644
index 0000000..136e8b7
--- /dev/null
+++ b/src/components/InputPanel.tsx
@@ -0,0 +1,64 @@
+import React from 'react';
+import { Input, Upload, message, Button } from 'antd';
+import { PictureOutlined, SendOutlined } from '@ant-design/icons';
+import './InputPanel.css';
+
+const { TextArea } = Input;
+
+const InputPanel: React.FC = () => {
+ const [value, setValue] = React.useState('');
+ const [fileList, setFileList] = React.useState([]);
+
+ const send = () => {
+ const hasText = value.trim().length > 0;
+ const hasImage = fileList.length > 0;
+ if (!hasText && !hasImage) {
+ message.info('请输入内容或上传图片');
+ return;
+ }
+ // 此处替换为真实发送逻辑
+ console.log('发送内容:', { text: value, files: fileList });
+ setValue('');
+ setFileList([]);
+ message.success('已发送');
+ };
+
+ const onKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter') {
+ if (e.shiftKey) {
+ // Shift+Enter 换行(保持默认行为)
+ return;
+ }
+ // Enter 发送
+ e.preventDefault();
+ send();
+ }
+ };
+
+ return (
+
+ );
+};
+
+export default InputPanel;
\ No newline at end of file
diff --git a/src/components/KeyValueList.css b/src/components/KeyValueList.css
new file mode 100644
index 0000000..9e39e63
--- /dev/null
+++ b/src/components/KeyValueList.css
@@ -0,0 +1,17 @@
+/* 键值对动态输入组件样式 */
+.kv-list {
+ margin-top: 8px;
+}
+
+.kv-row {
+ margin-bottom: 8px;
+}
+
+.kv-remove {
+ width: 100%;
+}
+
+.kv-list .ant-input {
+ background: rgba(255,255,255,0.03);
+ color: #e5e7eb;
+}
\ No newline at end of file
diff --git a/src/components/KeyValueList.tsx b/src/components/KeyValueList.tsx
new file mode 100644
index 0000000..fb906c0
--- /dev/null
+++ b/src/components/KeyValueList.tsx
@@ -0,0 +1,113 @@
+import React, { useEffect, useState, useRef } from 'react';
+import { Row, Col, Input, Button } from 'antd';
+import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
+import './KeyValueList.css';
+
+export type DictValue = Record;
+
+type KeyValuePair = { id: string; k: string; v: string };
+
+type Props = {
+ value?: DictValue;
+ onChange?: (value: DictValue) => void;
+};
+
+const KeyValueList: React.FC = ({ value, onChange }) => {
+ const [rows, setRows] = useState([]);
+
+ useEffect(() => {
+ // 初始化时提供一行空输入;之后只合并父值,不再自动新增空行
+ const initializedRef = (KeyValueList as any)._initializedRef || { current: false };
+ (KeyValueList as any)._initializedRef = initializedRef;
+
+ setRows((prev) => {
+ const existingIdByKey = new Map(prev.filter((r) => r.k).map((r) => [r.k, r.id]));
+ const valuePairs: KeyValuePair[] = value
+ ? Object.keys(value).map((key) => ({
+ id: existingIdByKey.get(key) || `${key}-${Date.now()}-${Math.random().toString(36).slice(2)}`,
+ k: key,
+ v: value[key] ?? '',
+ }))
+ : [];
+ const blankRows = prev.filter((r) => !r.k);
+ let merged = [...valuePairs, ...blankRows];
+ if (!initializedRef.current && merged.length === 0) {
+ merged = [{ id: `row-${Date.now()}-${Math.random().toString(36).slice(2)}`, k: '', v: '' }];
+ }
+ initializedRef.current = true;
+ return merged;
+ });
+ }, [value]);
+
+ const emitChange = (nextRows: KeyValuePair[]) => {
+ const dict: DictValue = {};
+ nextRows.forEach((r) => {
+ if (r.k && r.k.trim() !== '') {
+ dict[r.k] = r.v ?? '';
+ }
+ });
+ onChange?.(dict);
+ };
+
+ const updateRow = (id: string, field: 'k' | 'v', val: string) => {
+ const next = rows.map((r) => (r.id === id ? { ...r, [field]: val } : r));
+ setRows(next);
+ emitChange(next);
+ };
+
+ const addRow = () => {
+ const next = [...rows, { id: `row-${Date.now()}-${Math.random().toString(36).slice(2)}`, k: '', v: '' }];
+ setRows(next);
+ // 不触发 onChange,因为字典未变化(空行不入字典)
+ };
+
+ const removeRow = (id: string) => {
+ const removed = rows.find((r) => r.id === id);
+ const next = rows.filter((r) => r.id !== id);
+ setRows(next);
+ if (removed?.k && removed.k.trim() !== '') {
+ emitChange(next);
+ }
+ };
+
+ return (
+
+ );
+};
+
+export default KeyValueList;
\ No newline at end of file
diff --git a/src/components/LayoutWrapper.tsx b/src/components/LayoutWrapper.tsx
new file mode 100644
index 0000000..800ed10
--- /dev/null
+++ b/src/components/LayoutWrapper.tsx
@@ -0,0 +1,58 @@
+import React from 'react';
+import { Layout } from 'antd';
+import { Routes, Route, useNavigate, useLocation } from 'react-router-dom';
+import SiderMenu from './SiderMenu.tsx';
+import MainContent from './MainContent.tsx';
+import ResourceList from './ResourceList.tsx';
+import '../styles/base.css';
+import '../styles/layout.css';
+
+const LayoutWrapper: React.FC = () => {
+ const navigate = useNavigate();
+ const location = useLocation();
+
+ const pathToKey = (path: string) => {
+ switch (path) {
+ case '/resources':
+ return 'menu1';
+ case '/menu2':
+ return 'menu2';
+ default:
+ return 'home';
+ }
+ };
+
+ const selectedKey = pathToKey(location.pathname);
+
+ const handleNavigate = (key: string) => {
+ switch (key) {
+ case 'home':
+ navigate('/');
+ break;
+ case 'menu1':
+ navigate('/resources');
+ break;
+ case 'menu2':
+ navigate('/menu2');
+ break;
+ default:
+ navigate('/');
+ break;
+ }
+ };
+
+ return (
+
+
+
+
+ } />
+ } />
+ 菜单2的内容暂未实现} />
+
+
+
+ );
+};
+
+export default LayoutWrapper;
\ No newline at end of file
diff --git a/src/components/MainContent.css b/src/components/MainContent.css
new file mode 100644
index 0000000..052c744
--- /dev/null
+++ b/src/components/MainContent.css
@@ -0,0 +1,2 @@
+/* 主内容区组件样式(如需) */
+/* 主要样式已在 layout.css 中定义,此处预留以便后续扩展 */
\ No newline at end of file
diff --git a/src/components/MainContent.tsx b/src/components/MainContent.tsx
new file mode 100644
index 0000000..0214dfd
--- /dev/null
+++ b/src/components/MainContent.tsx
@@ -0,0 +1,32 @@
+import React from 'react';
+import { Layout, Typography } from 'antd';
+import PeopleForm from './PeopleForm.tsx';
+import InputPanel from './InputPanel.tsx';
+import HintText from './HintText.tsx';
+import './MainContent.css';
+
+const { Content } = Layout;
+
+const MainContent: React.FC = () => {
+ return (
+
+
+
+ ✨ 有新资源了吗?
+
+
+ 输入个人信息描述,上传图片,我将自动整理资源信息
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default MainContent;
\ No newline at end of file
diff --git a/src/components/PeopleForm.css b/src/components/PeopleForm.css
new file mode 100644
index 0000000..82197f2
--- /dev/null
+++ b/src/components/PeopleForm.css
@@ -0,0 +1,26 @@
+/* 人物信息录入表单样式 */
+.people-form {
+ margin-top: 16px;
+ padding: 20px;
+ border: 1px solid rgba(255,255,255,0.1);
+ border-radius: 12px;
+ background: rgba(255,255,255,0.04);
+}
+
+.people-form .ant-form-item-label > label {
+ color: #cbd5e1;
+}
+
+.people-form .ant-input,
+.people-form .ant-input-affix-wrapper,
+.people-form .ant-select-selector,
+.people-form .ant-input-number,
+.people-form .ant-input-number-input {
+ background: rgba(255,255,255,0.03);
+ color: #e5e7eb;
+}
+
+.people-form .ant-select-selection-item,
+.people-form .ant-select-selection-placeholder {
+ color: #cbd5e1;
+}
\ No newline at end of file
diff --git a/src/components/PeopleForm.tsx b/src/components/PeopleForm.tsx
new file mode 100644
index 0000000..6b7c4cc
--- /dev/null
+++ b/src/components/PeopleForm.tsx
@@ -0,0 +1,87 @@
+import React from 'react';
+import { Form, Input, Select, InputNumber, Button, message, Row, Col } from 'antd';
+import './PeopleForm.css';
+import KeyValueList from './KeyValueList.tsx'
+
+const { TextArea } = Input;
+
+const PeopleForm: React.FC = () => {
+ const [form] = Form.useForm();
+
+ const onFinish = (values: any) => {
+ // 暂时打印内容,模拟提交
+ console.log('People form submit:', values);
+ message.success('表单已提交');
+ form.resetFields();
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default PeopleForm;
\ No newline at end of file
diff --git a/src/components/ResourceList.tsx b/src/components/ResourceList.tsx
new file mode 100644
index 0000000..96d8318
--- /dev/null
+++ b/src/components/ResourceList.tsx
@@ -0,0 +1,612 @@
+import React from 'react';
+import { Layout, Typography, Table, Grid, InputNumber, Button, Space, Tag } from 'antd';
+import type { ColumnsType, ColumnType } from 'antd/es/table';
+import type { FilterDropdownProps } from 'antd/es/table/interface';
+import type { TableProps } from 'antd';
+import { SearchOutlined } from '@ant-design/icons';
+import './MainContent.css';
+
+const { Content } = Layout;
+
+// 数据类型定义
+export type DictValue = Record;
+export type Resource = {
+ id: string;
+ name: string;
+ gender: '男' | '女' | '其他/保密' | string;
+ age: number;
+ height?: number;
+ marital_status?: string;
+ introduction?: DictValue;
+};
+
+// 模拟从后端获取资源列表(真实环境替换为实际接口)
+async function fetchResources(): Promise {
+ try {
+ const res = await fetch('/api/resources');
+ if (!res.ok) throw new Error('network');
+ const data = await res.json();
+ return data as Resource[];
+ } catch (e) {
+ // 回退到 mock 数据,便于本地开发
+ return [
+ {
+ id: '1',
+ name: '张三',
+ gender: '男',
+ age: 28,
+ height: 175,
+ marital_status: '未婚',
+ introduction: {
+ 籍贯: '北京',
+ 职业: '产品经理',
+ 教育: '本科',
+ 爱好: '跑步、旅行',
+ 技能: '数据分析、原型设计',
+ 座右铭: '保持好奇心,持续迭代',
+ 自我评价: '目标感强,善于跨团队沟通与协调。',
+ },
+ },
+ {
+ id: '2',
+ name: '李四',
+ gender: '女',
+ age: 26,
+ height: 165,
+ marital_status: '已婚',
+ introduction: {
+ 籍贯: '上海',
+ 职业: 'UI 设计师',
+ 教育: '硕士',
+ 公司: '知名互联网公司',
+ 爱好: '阅读、咖啡、插画',
+ 技能: '视觉系统、组件库设计',
+ 自我评价: '注重细节,强调一致性与可访问性。',
+ },
+ },
+ {
+ id: '3',
+ name: '王五',
+ gender: '其他/保密',
+ age: 31,
+ height: 180,
+ marital_status: '保密',
+ introduction: {
+ 籍贯: '成都',
+ 职业: '摄影师',
+ 教育: '本科',
+ 爱好: '旅行拍摄、登山',
+ 技能: '人像、风光摄影',
+ 座右铭: '用镜头记录真实与美',
+ 自我评价: '对光线敏感,善于捕捉转瞬即逝的情绪。',
+ },
+ },
+ {
+ id: '4',
+ name: '赵六',
+ gender: '男',
+ age: 22,
+ height: 178,
+ marital_status: '未婚',
+ introduction: {
+ 籍贯: '西安',
+ 职业: '前端开发',
+ 教育: '本科',
+ 技能: 'React、TypeScript、Vite',
+ 证书: '阿里云开发者认证',
+ 项目经验: '电商用户中心、数据可视化面板',
+ 自我评价: '爱钻研,对性能优化和 DX 有热情。爱钻研,对性能优化和 DX 有热情。爱钻研,对性能优化和 DX 有热情。爱钻研,对性能优化和 DX 有热情。爱钻研,对性能优化和 DX 有热情。爱钻研,对性能优化和 DX 有热情。爱钻研,对性能优化和 DX 有热情。',
+ 项目经验2: '电商用户中心、数据可视化面板',
+ 项目经验3: '电商用户中心、数据可视化面板',
+ 项目经验4: '电商用户中心、数据可视化面板',
+ 项目经验5: '电商用户中心、数据可视化面板',
+ 项目经验6: '电商用户中心、数据可视化面板',
+ },
+ },
+ {
+ id: '5',
+ name: '周杰',
+ gender: '男',
+ age: 35,
+ height: 182,
+ marital_status: '已婚',
+ introduction: {
+ 籍贯: '杭州',
+ 职业: '后端开发',
+ 教育: '硕士',
+ 技能: 'Go、gRPC、K8s',
+ 部门: '平台基础设施',
+ 自我评价: '偏工程化,注重稳定性与可观测性。',
+ },
+ },
+ {
+ id: '6',
+ name: '吴敏',
+ gender: '女',
+ age: 29,
+ height: 168,
+ marital_status: '未婚',
+ introduction: {
+ 籍贯: '南京',
+ 职业: '数据分析师',
+ 教育: '本科',
+ 技能: 'SQL、Python、Tableau',
+ 研究方向: '用户增长与留存',
+ 自我评价: '以数据驱动决策,关注可解释性。',
+ },
+ },
+ {
+ id: '7',
+ name: '郑辉',
+ gender: '男',
+ age: 41,
+ height: 170,
+ marital_status: '离异',
+ introduction: {
+ 籍贯: '长沙',
+ 职业: '产品运营',
+ 教育: '本科',
+ 爱好: '篮球、播客',
+ 技能: '活动策划、社区运营',
+ 自我评价: '擅长整合资源并解决复杂协调问题。',
+ },
+ },
+ {
+ id: '8',
+ name: '王芳',
+ gender: '女',
+ age: 33,
+ height: 160,
+ marital_status: '已婚',
+ introduction: {
+ 籍贯: '青岛',
+ 职业: '市场经理',
+ 教育: '本科',
+ 技能: '品牌策略、内容营销',
+ 社交媒体: '微博、抖音、知乎',
+ 自我评价: '善于讲故事并构建品牌资产。',
+ },
+ },
+ {
+ id: '9',
+ name: '刘洋',
+ gender: '男',
+ age: 24,
+ height: 172,
+ marital_status: '未婚',
+ introduction: {
+ 籍贯: '合肥',
+ 职业: '测试工程师',
+ 教育: '本科',
+ 技能: '自动化测试、性能测试',
+ 自我评价: '对边界条件敏感,追求高覆盖与低误报。',
+ },
+ },
+ {
+ id: '10',
+ name: '陈晨',
+ gender: '女',
+ age: 27,
+ height: 163,
+ marital_status: '未婚',
+ introduction: {
+ 籍贯: '武汉',
+ 职业: '人力资源',
+ 教育: '本科',
+ 技能: '招聘、绩效与组织发展',
+ 自我评价: '重视文化与组织氛围建设。',
+ },
+ },
+ {
+ id: '11',
+ name: '孙琪',
+ gender: '女',
+ age: 38,
+ height: 166,
+ marital_status: '已婚',
+ introduction: {
+ 籍贯: '重庆',
+ 职业: '项目经理',
+ 教育: '硕士',
+ 技能: '进度与风险管理',
+ 获奖: '优秀 PM 奖',
+ 自我评价: '以结果为导向,兼顾团队士气。',
+ },
+ },
+ {
+ id: '12',
+ name: '朱莉',
+ gender: '女',
+ age: 30,
+ height: 158,
+ marital_status: '未婚',
+ introduction: {
+ 籍贯: '厦门',
+ 职业: '内容编辑',
+ 教育: '本科',
+ 技能: '选题、采访与写作',
+ 座右铭: '字斟句酌,以小见大',
+ 自我评价: '具叙事力,关注读者反馈。',
+ },
+ },
+ {
+ id: '13',
+ name: '黄磊',
+ gender: '男',
+ age: 45,
+ height: 176,
+ marital_status: '已婚',
+ introduction: {
+ 籍贯: '广州',
+ 职业: '架构师',
+ 教育: '硕士',
+ 技能: '分布式系统、架构治理',
+ 自我评价: '关注可扩展性与长期维护成本。',
+ },
+ },
+ {
+ id: '14',
+ name: '高远',
+ gender: '男',
+ age: 32,
+ height: 181,
+ marital_status: '未婚',
+ introduction: {
+ 籍贯: '沈阳',
+ 职业: '算法工程师',
+ 教育: '博士',
+ 技能: '推荐系统、CTR 预估',
+ 研究方向: '多模态融合',
+ 自我评价: '注重泛化与鲁棒性。',
+ },
+ },
+ {
+ id: '15',
+ name: '曹宁',
+ gender: '男',
+ age: 28,
+ height: 169,
+ marital_status: '未婚',
+ introduction: {
+ 籍贯: '苏州',
+ 职业: '运维工程师',
+ 教育: '本科',
+ 技能: 'Linux、CI/CD、监控',
+ 自我评价: '追求稳定与自动化。',
+ },
+ },
+ {
+ id: '16',
+ name: '韩梅',
+ gender: '女',
+ age: 34,
+ height: 162,
+ marital_status: '已婚',
+ introduction: {
+ 籍贯: '大连',
+ 职业: '销售总监',
+ 教育: '本科',
+ 技能: '大客户拓展、谈判',
+ 自我评价: '结果导向,善于建立信任。',
+ },
+ },
+ {
+ id: '17',
+ name: '秦川',
+ gender: '男',
+ age: 52,
+ height: 174,
+ marital_status: '已婚',
+ introduction: {
+ 籍贯: '洛阳',
+ 职业: '财务主管',
+ 教育: '本科',
+ 技能: '成本控制、风险合规',
+ 自我评价: '稳健务实,注重细节。',
+ },
+ },
+ {
+ id: '18',
+ name: '何静',
+ gender: '女',
+ age: 23,
+ height: 159,
+ marital_status: '未婚',
+ introduction: {
+ 籍贯: '昆明',
+ 职业: '新媒体运营',
+ 教育: '本科',
+ 技能: '短视频策划、社群',
+ 自我评价: '创意活跃,执行力强。',
+ },
+ },
+ {
+ id: '19',
+ name: '吕博',
+ gender: '男',
+ age: 36,
+ height: 183,
+ marital_status: '离异',
+ introduction: {
+ 籍贯: '天津',
+ 职业: '产品专家',
+ 教育: '硕士',
+ 技能: '竞品分析、商业化',
+ 自我评价: '偏战略,长期主义者。',
+ },
+ },
+ {
+ id: '20',
+ name: '沈玉',
+ gender: '女',
+ age: 25,
+ height: 161,
+ marital_status: '未婚',
+ introduction: {
+ 籍贯: '福州',
+ 职业: '交互设计师',
+ 教育: '本科',
+ 技能: '流程设计、可用性测试',
+ 自我评价: '以用户为中心,迭代驱动优化。',
+ },
+ },
+ {
+ id: '21',
+ name: '罗兰',
+ gender: '其他/保密',
+ age: 40,
+ height: 177,
+ marital_status: '保密',
+ introduction: {
+ 籍贯: '呼和浩特',
+ 职业: '翻译',
+ 教育: '硕士',
+ 语言: '中英法德',
+ 自我评价: '严谨细致,语感强。',
+ },
+ },
+ {
+ id: '22',
+ name: '尹峰',
+ gender: '男',
+ age: 29,
+ height: 168,
+ marital_status: '未婚',
+ introduction: {
+ 籍贯: '石家庄',
+ 职业: '安全工程师',
+ 教育: '本科',
+ 技能: '渗透测试、代码审计',
+ 自我评价: '对攻击面敏感,防守与预警并重。',
+ },
+ },
+ {
+ id: '23',
+ name: '邓雅',
+ gender: '女',
+ age: 37,
+ height: 167,
+ marital_status: '已婚',
+ introduction: {
+ 籍贯: '宁波',
+ 职业: '供应链经理',
+ 教育: '硕士',
+ 技能: '精益管理、库存优化',
+ 自我评价: '注重流程化与跨部门协同。',
+ },
+ },
+ {
+ id: '24',
+ name: '侯哲',
+ gender: '男',
+ age: 21,
+ height: 171,
+ marital_status: '未婚',
+ introduction: {
+ 籍贯: '桂林',
+ 职业: '实习生',
+ 教育: '本科在读',
+ 技能: '前端基础、原型制作',
+ 自我评价: '好学上进,快速吸收新知识。',
+ },
+ },
+ ];
+ }
+}
+
+// 数字范围筛选下拉
+function buildNumberRangeFilter(dataIndex: keyof T, label: string): ColumnType {
+ return {
+ title: label,
+ dataIndex,
+ sorter: (a: Resource, b: Resource) => Number((a as any)[dataIndex] ?? 0) - Number((b as any)[dataIndex] ?? 0),
+ filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters }: FilterDropdownProps) => {
+ const [min, max] = String(selectedKeys?.[0] ?? ':').split(':');
+ const [localMin, setLocalMin] = React.useState(min ? Number(min) : undefined);
+ const [localMax, setLocalMax] = React.useState(max ? Number(max) : undefined);
+ return (
+
+
+ setLocalMin(v ?? undefined)}
+ style={{ width: '100%' }}
+ />
+ setLocalMax(v ?? undefined)}
+ style={{ width: '100%' }}
+ />
+
+ }
+ onClick={() => {
+ const key = `${localMin ?? ''}:${localMax ?? ''}`;
+ setSelectedKeys?.([key]);
+ confirm?.();
+ }}
+ >
+ 筛选
+
+
+
+
+
+ );
+ },
+ onFilter: (filterValue: React.Key | boolean, record: Resource) => {
+ const [minStr, maxStr] = String(filterValue).split(':');
+ const min = minStr ? Number(minStr) : undefined;
+ const max = maxStr ? Number(maxStr) : undefined;
+ const val = Number((record as any)[dataIndex] ?? NaN);
+ if (Number.isNaN(val)) return false;
+ if (min !== undefined && val < min) return false;
+ if (max !== undefined && val > max) return false;
+ return true;
+ },
+ } as ColumnType;
+}
+
+const ResourceList: React.FC = () => {
+ const screens = Grid.useBreakpoint();
+ const isMobile = !screens.md;
+ const [loading, setLoading] = React.useState(false);
+ const [data, setData] = React.useState([]);
+ const [pageSize, setPageSize] = React.useState(10);
+ const [pagination, setPagination] = React.useState<{ current: number; pageSize: number }>({ current: 1, pageSize: 10 });
+
+ const handleTableChange: TableProps['onChange'] = (pg) => {
+ setPagination({ current: pg?.current ?? 1, pageSize: pg?.pageSize ?? 10 });
+ };
+
+ React.useEffect(() => {
+ let mounted = true;
+ setLoading(true);
+ fetchResources().then((list) => {
+ if (!mounted) return;
+ setData(list);
+ setLoading(false);
+ });
+ return () => {
+ mounted = false;
+ };
+ }, []);
+
+ const columns: ColumnsType = [
+ {
+ title: '姓名',
+ dataIndex: 'name',
+ key: 'name',
+ render: (text: string) => {text},
+ },
+ {
+ title: '性别',
+ dataIndex: 'gender',
+ key: 'gender',
+ filters: [
+ { text: '男', value: '男' },
+ { text: '女', value: '女' },
+ { text: '其他/保密', value: '其他/保密' },
+ ],
+ onFilter: (value: React.Key | boolean, record: Resource) => String(record.gender) === String(value),
+ render: (g: string) => {
+ const color = g === '男' ? 'blue' : g === '女' ? 'magenta' : 'default';
+ return {g};
+ },
+ },
+ buildNumberRangeFilter('age', '年龄'),
+ // 非移动端显示更多列
+ ...(!isMobile
+ ? [
+ buildNumberRangeFilter('height', '身高'),
+ {
+ title: '婚姻状况',
+ dataIndex: 'marital_status',
+ key: 'marital_status',
+ } as ColumnType,
+ ]
+ : []),
+ ];
+
+ return (
+
+
+
+ 资源列表
+
+
+
+ rowKey="id"
+ loading={loading}
+ columns={columns}
+ dataSource={data}
+ pagination={{
+ ...pagination,
+ showSizeChanger: true,
+ pageSizeOptions: [10, 25, 50, 100],
+ position: ['bottomRight'],
+ total: data.length,
+ showTotal: (total) => `总计 ${total} 条`,
+ }}
+ onChange={handleTableChange}
+ expandable={{
+ expandedRowRender: (record: Resource) => {
+ const intro = record.introduction ? Object.entries(record.introduction) : [];
+ return (
+
+ {isMobile && (
+
+ {record.height !== undefined &&
身高: {record.height}
}
+ {record.marital_status &&
婚姻状况: {record.marital_status}
}
+
+ )}
+ {intro.length > 0 ? (
+
+ {intro.map(([k, v]) => (
+
+ {k}
+ {v}
+
+ ))}
+
+ ) : (
+
暂无介绍
+ )}
+
+ );
+ },
+ }}
+ />
+
+
+ );
+};
+
+export default ResourceList;
\ No newline at end of file
diff --git a/src/components/SiderMenu.css b/src/components/SiderMenu.css
new file mode 100644
index 0000000..75ede25
--- /dev/null
+++ b/src/components/SiderMenu.css
@@ -0,0 +1,19 @@
+/* 侧边菜单组件局部样式 */
+.sider-header { border-bottom: 1px solid rgba(255,255,255,0.08); }
+
+/* 收起时图标水平居中(仅 PC 端 Sider 使用) */
+.sider-header.collapsed { justify-content: center; }
+
+/* 移动端汉堡触发按钮位置 */
+.mobile-menu-trigger {
+ position: fixed;
+ left: 16px;
+ top: 16px;
+ z-index: 1100;
+}
+
+/* 移动端 Drawer 背景与滚动 */
+.mobile-menu-drawer .ant-drawer-body {
+ background: #0f172a; /* 与页面暗色一致 */
+ padding: 0;
+}
\ No newline at end of file
diff --git a/src/components/SiderMenu.tsx b/src/components/SiderMenu.tsx
new file mode 100644
index 0000000..105d8c9
--- /dev/null
+++ b/src/components/SiderMenu.tsx
@@ -0,0 +1,112 @@
+import React from 'react';
+import { Layout, Menu, Grid, Drawer, Button } from 'antd';
+import { CodeOutlined, HomeOutlined, UnorderedListOutlined, AppstoreOutlined, MenuOutlined } from '@ant-design/icons';
+import './SiderMenu.css';
+
+const { Sider } = Layout;
+
+// 新增:支持外部导航回调 + 受控选中态
+type Props = { onNavigate?: (key: string) => void; selectedKey?: string };
+
+const SiderMenu: React.FC = ({ onNavigate, selectedKey }) => {
+ const screens = Grid.useBreakpoint();
+ const isMobile = !screens.md;
+ const [collapsed, setCollapsed] = React.useState(false);
+ const [selectedKeys, setSelectedKeys] = React.useState(['home']);
+ const [mobileOpen, setMobileOpen] = React.useState(false);
+
+ React.useEffect(() => {
+ setCollapsed(isMobile);
+ }, [isMobile]);
+
+ // 根据外部 selectedKey 同步选中态
+ React.useEffect(() => {
+ if (selectedKey) {
+ setSelectedKeys([selectedKey]);
+ }
+ }, [selectedKey]);
+
+ const items = [
+ { key: 'home', label: '首页', icon: },
+ { key: 'menu1', label: '资源列表', icon: },
+ { key: 'menu2', label: '菜单2', icon: },
+ ];
+
+ // 移动端:使用 Drawer 覆盖主内容
+ if (isMobile) {
+ return (
+ <>
+ }
+ onClick={() => setMobileOpen((open) => !open)}
+ />
+ setMobileOpen(false)}
+ styles={{ body: { padding: 0 } }}
+ >
+
+
+ >
+ );
+ }
+
+ // PC 端:保持 Sider 行为不变
+ return (
+ setCollapsed(c)}
+ breakpoint="md"
+ collapsedWidth={64}
+ theme="dark"
+ >
+
+
+ {!collapsed && (
+
+ )}
+
+
+ );
+};
+
+export default SiderMenu;
\ No newline at end of file
diff --git a/src/index.css b/src/index.css
index 08a3ac9..ef78507 100644
--- a/src/index.css
+++ b/src/index.css
@@ -1,68 +1,3 @@
-:root {
- font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
- line-height: 1.5;
- font-weight: 400;
-
- color-scheme: light dark;
- color: rgba(255, 255, 255, 0.87);
- background-color: #242424;
-
- font-synthesis: none;
- text-rendering: optimizeLegibility;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-
-a {
- font-weight: 500;
- color: #646cff;
- text-decoration: inherit;
-}
-a:hover {
- color: #535bf2;
-}
-
-body {
- margin: 0;
- display: flex;
- place-items: center;
- min-width: 320px;
- min-height: 100vh;
-}
-
-h1 {
- font-size: 3.2em;
- line-height: 1.1;
-}
-
-button {
- border-radius: 8px;
- border: 1px solid transparent;
- padding: 0.6em 1.2em;
- font-size: 1em;
- font-weight: 500;
- font-family: inherit;
- background-color: #1a1a1a;
- cursor: pointer;
- transition: border-color 0.25s;
-}
-button:hover {
- border-color: #646cff;
-}
-button:focus,
-button:focus-visible {
- outline: 4px auto -webkit-focus-ring-color;
-}
-
-@media (prefers-color-scheme: light) {
- :root {
- color: #213547;
- background-color: #ffffff;
- }
- a:hover {
- color: #747bff;
- }
- button {
- background-color: #f9f9f9;
- }
-}
+/* 保留最小化基础样式,具体视觉由 base.css 控制 */
+html, body, #root { height: 100%; }
+body { margin: 0; }
diff --git a/src/main.tsx b/src/main.tsx
index bef5202..0a879c6 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -1,10 +1,15 @@
+import '@ant-design/v5-patch-for-react-19'
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
-import './index.css'
+import { BrowserRouter } from 'react-router-dom'
+import 'antd/dist/reset.css'
+import './styles/base.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
-
+
+
+
,
)
diff --git a/src/styles/base.css b/src/styles/base.css
new file mode 100644
index 0000000..8eb6a22
--- /dev/null
+++ b/src/styles/base.css
@@ -0,0 +1,36 @@
+/* 全局基础样式 */
+@supports (font-variation-settings: normal) {
+ :root { font-synthesis-weight: none; }
+}
+
+html, body, #root {
+ height: 100%;
+}
+
+body {
+ margin: 0;
+ background: radial-gradient(1200px 600px at 70% -100px, rgba(92,124,255,0.25) 0%, rgba(92,124,255,0.06) 45%, transparent 60%),
+ radial-gradient(800px 400px at -200px 20%, rgba(120,220,255,0.15) 0%, rgba(120,220,255,0.06) 50%, transparent 70%),
+ #0f172a; /* slate-900 */
+ color: #e5e7eb; /* gray-200 */
+ font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, "Helvetica Neue", Arial, "Noto Sans", "Apple Color Emoji", "Segoe UI Emoji";
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+/* 让 Ant Design 的 Layout 充满视口 */
+.app-root {
+ min-height: 100vh;
+}
+
+/* 统一滚动条样式(轻度) */
+* {
+ scrollbar-width: thin;
+ scrollbar-color: rgba(255,255,255,0.2) transparent;
+}
+
+::-webkit-scrollbar { width: 8px; height: 8px; }
+::-webkit-scrollbar-thumb {
+ background: rgba(255,255,255,0.2);
+ border-radius: 6px;
+}
\ No newline at end of file
diff --git a/src/styles/layout.css b/src/styles/layout.css
new file mode 100644
index 0000000..e59a90e
--- /dev/null
+++ b/src/styles/layout.css
@@ -0,0 +1,54 @@
+/* 布局相关样式 */
+.layout-wrapper {
+ min-height: 100vh;
+}
+
+/* 侧栏头部区域 */
+.sider-header {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 20px 16px 12px 16px;
+ color: #e5e7eb;
+}
+.sider-title {
+ font-weight: 600;
+ font-size: 16px;
+}
+.sider-desc {
+ font-size: 12px;
+ color: #a3a3a3;
+}
+
+/* 主内容区 */
+.main-content {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+}
+.content-body {
+ flex: 1;
+ padding: 32px;
+}
+
+/* 输入面板固定底部 */
+.input-panel-wrapper {
+ position: sticky;
+ bottom: 0;
+ background: linear-gradient(180deg, rgba(15, 23, 42, 0.5) 0%, rgba(15, 23, 42, 0.9) 40%, rgba(15, 23, 42, 1) 100%);
+ backdrop-filter: blur(4px);
+ border-top: 1px solid rgba(255,255,255,0.08);
+ padding: 16px 24px 24px 24px;
+}
+
+.hint-text {
+ margin-top: 10px;
+ font-size: 12px;
+ color: #9ca3af;
+}
+
+/* 小屏优化:输入区域内边距更紧凑 */
+@media (max-width: 768px) {
+ .content-body { padding: 16px; }
+ .input-panel-wrapper { padding: 12px 12px 16px 12px; }
+}
\ No newline at end of file