chore: 初始化仓库
中华文明全图鉴——文物全图系统(PC Web 地图 + NestJS API + 管理后台)。 含三大 IP(文物南迁北归 / 国宝海外回归 / 博物馆手艺人)、AI 文物对话、 文物地图与详情、以及 demo-video-kit 演示视频生成工具。
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Card, Statistic, Row, Col, Typography, Spin } from "antd";
|
||||
import { PicLeftOutlined, BankOutlined, EnvironmentOutlined } from "@ant-design/icons";
|
||||
import { api } from "../api/client";
|
||||
|
||||
interface Stats {
|
||||
total_artifacts: number;
|
||||
total_institutions: number;
|
||||
total_locations: number;
|
||||
}
|
||||
|
||||
export default function Dashboard() {
|
||||
const [stats, setStats] = useState<Stats | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
api
|
||||
.get<Stats>("/api/v1/map/stats")
|
||||
.then(setStats)
|
||||
.catch(() => null)
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Typography.Title level={4} style={{ marginBottom: 24 }}>
|
||||
概览
|
||||
</Typography.Title>
|
||||
{loading ? (
|
||||
<Spin />
|
||||
) : (
|
||||
<Row gutter={16}>
|
||||
<Col span={8}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="已发布文物"
|
||||
value={stats?.total_artifacts ?? 0}
|
||||
prefix={<PicLeftOutlined />}
|
||||
valueStyle={{ color: "#c9a84c" }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="合作机构"
|
||||
value={stats?.total_institutions ?? 0}
|
||||
prefix={<BankOutlined />}
|
||||
valueStyle={{ color: "#c9a84c" }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="地图点位"
|
||||
value={stats?.total_locations ?? 0}
|
||||
prefix={<EnvironmentOutlined />}
|
||||
valueStyle={{ color: "#c9a84c" }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import { useState } from "react";
|
||||
import { Form, Input, Button, message, Typography } from "antd";
|
||||
import { UserOutlined, LockOutlined } from "@ant-design/icons";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { api } from "../api/client";
|
||||
import { saveAuth, AuthUser } from "../store/auth";
|
||||
|
||||
interface LoginResponse {
|
||||
access_token: string;
|
||||
user: AuthUser;
|
||||
}
|
||||
|
||||
export default function Login() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const onFinish = async (values: { username: string; password: string }) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await api.post<LoginResponse>("/api/v1/auth/login", values);
|
||||
saveAuth(res.access_token, res.user);
|
||||
message.success(`欢迎回来,${res.user.nickname ?? res.user.username}`);
|
||||
navigate("/dashboard");
|
||||
} catch (err) {
|
||||
message.error((err as Error).message || "登录失败,请检查账号密码");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
minHeight: "100vh",
|
||||
background: "linear-gradient(135deg, #0f0c1a 0%, #1a1225 50%, #0d1117 100%)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 380,
|
||||
background: "rgba(255,255,255,0.04)",
|
||||
border: "1px solid rgba(201,168,76,0.2)",
|
||||
borderRadius: 12,
|
||||
padding: "40px 36px",
|
||||
boxShadow: "0 8px 40px rgba(0,0,0,0.5)",
|
||||
}}
|
||||
>
|
||||
<div style={{ textAlign: "center", marginBottom: 32 }}>
|
||||
<div style={{ fontSize: 28, marginBottom: 8 }}>🏛️</div>
|
||||
<Typography.Title
|
||||
level={3}
|
||||
style={{ color: "#c9a84c", margin: 0, fontWeight: 700, letterSpacing: 2 }}
|
||||
>
|
||||
中华文明全图鉴
|
||||
</Typography.Title>
|
||||
<Typography.Text style={{ color: "rgba(255,255,255,0.4)", fontSize: 13 }}>
|
||||
管理后台
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
<Form form={form} onFinish={onFinish} size="large" requiredMark={false}>
|
||||
<Form.Item
|
||||
name="username"
|
||||
rules={[{ required: true, message: "请输入邮箱或用户名" }]}
|
||||
>
|
||||
<Input
|
||||
prefix={<UserOutlined style={{ color: "rgba(255,255,255,0.3)" }} />}
|
||||
placeholder="邮箱 / 用户名"
|
||||
autoComplete="username"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="password"
|
||||
rules={[{ required: true, message: "请输入密码" }]}
|
||||
>
|
||||
<Input.Password
|
||||
prefix={<LockOutlined style={{ color: "rgba(255,255,255,0.3)" }} />}
|
||||
placeholder="密码"
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item style={{ marginBottom: 0 }}>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
block
|
||||
loading={loading}
|
||||
style={{ height: 44, fontWeight: 600, letterSpacing: 1 }}
|
||||
>
|
||||
登 录
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import {
|
||||
Table,
|
||||
Input,
|
||||
Select,
|
||||
Space,
|
||||
Tag,
|
||||
Typography,
|
||||
Button,
|
||||
message,
|
||||
Tooltip,
|
||||
} from "antd";
|
||||
import { SearchOutlined, ReloadOutlined } from "@ant-design/icons";
|
||||
import type { ColumnsType } from "antd/es/table";
|
||||
import { api } from "../../api/client";
|
||||
|
||||
interface Artifact {
|
||||
id: string;
|
||||
name: string;
|
||||
category: string;
|
||||
level: string;
|
||||
dynasty: string;
|
||||
story_hook: string;
|
||||
current_institution: string;
|
||||
}
|
||||
|
||||
interface PageMeta {
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
total_pages: number;
|
||||
}
|
||||
|
||||
interface ArtifactListResponse {
|
||||
data: Artifact[];
|
||||
meta: PageMeta;
|
||||
}
|
||||
|
||||
const CATEGORY_LABELS: Record<string, string> = {
|
||||
bronze: "青铜器",
|
||||
painting_calligraphy: "书画",
|
||||
porcelain: "陶瓷",
|
||||
jade: "玉器",
|
||||
gold_silver: "金银器",
|
||||
lacquer: "漆木器",
|
||||
textile: "织绣",
|
||||
stone_carving: "石刻造像",
|
||||
wood_carving: "木雕",
|
||||
dunhuang: "敦煌遗珍",
|
||||
ancient_book: "古籍文献",
|
||||
other: "其他",
|
||||
};
|
||||
|
||||
const LEVEL_COLORS: Record<string, string> = {
|
||||
level_1: "gold",
|
||||
level_2: "blue",
|
||||
level_3: "default",
|
||||
unknown: "default",
|
||||
};
|
||||
const LEVEL_LABELS: Record<string, string> = {
|
||||
level_1: "国家一级",
|
||||
level_2: "国家二级",
|
||||
level_3: "国家三级",
|
||||
unknown: "未定级",
|
||||
};
|
||||
|
||||
export default function ArtifactList() {
|
||||
const [data, setData] = useState<Artifact[]>([]);
|
||||
const [meta, setMeta] = useState<PageMeta>({ total: 0, page: 1, limit: 20, total_pages: 1 });
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [q, setQ] = useState("");
|
||||
const [category, setCategory] = useState<string | undefined>();
|
||||
const [dynasty, setDynasty] = useState<string | undefined>();
|
||||
|
||||
const fetchData = useCallback(
|
||||
async (page = 1) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const params = new URLSearchParams({ page: String(page), limit: "20" });
|
||||
if (q) params.set("q", q);
|
||||
if (category) params.set("category", category);
|
||||
if (dynasty) params.set("dynasty", dynasty);
|
||||
const res = await api.get<ArtifactListResponse>(
|
||||
`/api/v1/artifacts?${params}`
|
||||
);
|
||||
setData(res.data);
|
||||
setMeta(res.meta);
|
||||
} catch (err) {
|
||||
message.error((err as Error).message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[q, category, dynasty]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData(1);
|
||||
}, [fetchData]);
|
||||
|
||||
const columns: ColumnsType<Artifact> = [
|
||||
{
|
||||
title: "文物名称",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
render: (text: string, record) => (
|
||||
<div>
|
||||
<Typography.Text strong>{text}</Typography.Text>
|
||||
{record.story_hook && (
|
||||
<Tooltip title={record.story_hook}>
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
style={{ display: "block", fontSize: 12, maxWidth: 200 }}
|
||||
ellipsis
|
||||
>
|
||||
{record.story_hook}
|
||||
</Typography.Text>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "门类",
|
||||
dataIndex: "category",
|
||||
key: "category",
|
||||
width: 80,
|
||||
render: (cat: string) => CATEGORY_LABELS[cat] ?? cat,
|
||||
},
|
||||
{
|
||||
title: "级别",
|
||||
dataIndex: "level",
|
||||
key: "level",
|
||||
width: 100,
|
||||
render: (lv: string) => (
|
||||
<Tag color={LEVEL_COLORS[lv] ?? "default"}>{LEVEL_LABELS[lv] ?? lv}</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "朝代",
|
||||
dataIndex: "dynasty",
|
||||
key: "dynasty",
|
||||
width: 90,
|
||||
},
|
||||
{
|
||||
title: "所在机构",
|
||||
dataIndex: "current_institution",
|
||||
key: "current_institution",
|
||||
render: (v: string) => v ?? <Typography.Text type="secondary">—</Typography.Text>,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", marginBottom: 16 }}>
|
||||
<Typography.Title level={4} style={{ margin: 0 }}>
|
||||
文物管理
|
||||
</Typography.Title>
|
||||
</div>
|
||||
|
||||
<Space style={{ marginBottom: 16 }} wrap>
|
||||
<Input
|
||||
placeholder="搜索文物名称"
|
||||
prefix={<SearchOutlined />}
|
||||
value={q}
|
||||
onChange={(e) => setQ(e.target.value)}
|
||||
onPressEnter={() => fetchData(1)}
|
||||
style={{ width: 200 }}
|
||||
allowClear
|
||||
onClear={() => setQ("")}
|
||||
/>
|
||||
<Select
|
||||
placeholder="门类"
|
||||
style={{ width: 100 }}
|
||||
allowClear
|
||||
value={category}
|
||||
onChange={setCategory}
|
||||
options={Object.entries(CATEGORY_LABELS).map(([value, label]) => ({
|
||||
value,
|
||||
label,
|
||||
}))}
|
||||
/>
|
||||
<Select
|
||||
placeholder="朝代"
|
||||
style={{ width: 140 }}
|
||||
allowClear
|
||||
value={dynasty}
|
||||
onChange={setDynasty}
|
||||
options={["先秦", "秦汉", "魏晋南北朝", "隋唐", "宋元", "明清"].map((d) => ({
|
||||
value: d,
|
||||
label: d,
|
||||
}))}
|
||||
/>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={() => fetchData(1)}
|
||||
loading={loading}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
<Table
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
loading={loading}
|
||||
pagination={{
|
||||
total: meta.total,
|
||||
current: meta.page,
|
||||
pageSize: meta.limit,
|
||||
showTotal: (t) => `共 ${t} 件`,
|
||||
onChange: (page) => fetchData(page),
|
||||
showSizeChanger: false,
|
||||
}}
|
||||
size="middle"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user