diff --git a/src/store/useUserStore.ts b/src/store/useUserStore.ts new file mode 100644 index 0000000..4f4cba8 --- /dev/null +++ b/src/store/useUserStore.ts @@ -0,0 +1,129 @@ +/** + * 用户状态管理 - Zustand Store + */ + +import { create } from 'zustand' +import { persist } from 'zustand/middleware' +import type { User } from '@types/index' +import { login as apiLogin, getUserById } from '@services/api' + +interface UserState { + user: User | null + isAuthenticated: boolean + isLoading: boolean + + // Actions + login: (username: string, password: string) => Promise + logout: () => void + updateUser: (userData: Partial) => void + addFavorite: (heritageId: string) => void + removeFavorite: (heritageId: string) => void + followInheritor: (inheritorId: string) => void + unfollowInheritor: (inheritorId: string) => void + enrollCourse: (courseId: string) => void +} + +export const useUserStore = create()( + persist( + (set, get) => ({ + user: null, + isAuthenticated: false, + isLoading: false, + + login: async (username: string, password: string) => { + set({ isLoading: true }) + try { + const user = await apiLogin(username, password) + if (user) { + set({ user, isAuthenticated: true, isLoading: false }) + return true + } + set({ isLoading: false }) + return false + } catch (error) { + console.error('Login failed:', error) + set({ isLoading: false }) + return false + } + }, + + logout: () => { + set({ user: null, isAuthenticated: false }) + }, + + updateUser: (userData: Partial) => { + const { user } = get() + if (user) { + set({ user: { ...user, ...userData } }) + } + }, + + addFavorite: (heritageId: string) => { + const { user } = get() + if (user && !user.favorites.includes(heritageId)) { + set({ + user: { + ...user, + favorites: [...user.favorites, heritageId], + }, + }) + } + }, + + removeFavorite: (heritageId: string) => { + const { user } = get() + if (user) { + set({ + user: { + ...user, + favorites: user.favorites.filter((id) => id !== heritageId), + }, + }) + } + }, + + followInheritor: (inheritorId: string) => { + const { user } = get() + if (user && !user.followedInheritors.includes(inheritorId)) { + set({ + user: { + ...user, + followedInheritors: [...user.followedInheritors, inheritorId], + }, + }) + } + }, + + unfollowInheritor: (inheritorId: string) => { + const { user } = get() + if (user) { + set({ + user: { + ...user, + followedInheritors: user.followedInheritors.filter((id) => id !== inheritorId), + }, + }) + } + }, + + enrollCourse: (courseId: string) => { + const { user } = get() + if (user && !user.enrolledCourses.includes(courseId)) { + set({ + user: { + ...user, + enrolledCourses: [...user.enrolledCourses, courseId], + }, + }) + } + }, + }), + { + name: 'user-storage', // localStorage key + partialize: (state) => ({ + user: state.user, + isAuthenticated: state.isAuthenticated, + }), + } + ) +) diff --git a/src/theme/components.ts b/src/theme/components.ts new file mode 100644 index 0000000..53651a6 --- /dev/null +++ b/src/theme/components.ts @@ -0,0 +1,384 @@ +/** + * 非遗文化传承网站 - 组件级主题定制 + * 覆盖 Ant Design 全组件族的样式 + */ + +export const componentTokens = { + // ===== 通用组件 ===== + Button: { + colorPrimary: '#C8363D', + colorPrimaryHover: '#A82E34', + colorPrimaryActive: '#8B252B', + primaryShadow: '0 2px 0 rgba(200, 54, 61, 0.1)', + defaultShadow: '0 2px 0 rgba(0, 0, 0, 0.02)', + controlHeight: 40, + controlHeightLG: 48, + controlHeightSM: 32, + borderRadius: 8, + borderRadiusLG: 12, + borderRadiusSM: 6, + fontWeight: 500, + }, + + FloatButton: { + colorPrimary: '#C8363D', + colorPrimaryHover: '#A82E34', + boxShadow: '0 4px 16px rgba(200, 54, 61, 0.15)', + }, + + Typography: { + colorTextHeading: '#2C2C2C', + colorText: '#2C2C2C', + fontSizeHeading1: 38, + fontSizeHeading2: 30, + fontSizeHeading3: 24, + fontSizeHeading4: 20, + fontSizeHeading5: 16, + fontWeightStrong: 600, + }, + + // ===== 布局组件 ===== + Layout: { + headerBg: '#FFFFFF', + headerHeight: 64, + headerPadding: '0 50px', + footerBg: '#2C2C2C', + footerPadding: '48px 50px', + bodyBg: '#FAFAF8', + siderBg: '#FFFFFF', + }, + + Divider: { + colorSplit: '#E8E3DB', + marginLG: 24, + }, + + Space: { + marginXS: 8, + marginSM: 12, + marginMD: 16, + marginLG: 24, + marginXL: 32, + }, + + // ===== 导航组件 ===== + Menu: { + itemBg: 'transparent', + itemColor: '#666666', + itemSelectedBg: 'rgba(200, 54, 61, 0.08)', + itemSelectedColor: '#C8363D', + itemHoverBg: 'rgba(200, 54, 61, 0.05)', + itemHoverColor: '#C8363D', + itemActiveBg: 'rgba(200, 54, 61, 0.1)', + itemHeight: 48, + itemBorderRadius: 8, + iconSize: 18, + fontSize: 14, + }, + + Breadcrumb: { + linkColor: '#666666', + linkHoverColor: '#C8363D', + lastItemColor: '#2C2C2C', + fontSize: 14, + }, + + Pagination: { + itemActiveBg: '#C8363D', + itemActiveColorDisabled: '#FFFFFF', + colorPrimary: '#C8363D', + colorPrimaryHover: '#A82E34', + itemLinkBg: '#FFFFFF', + itemBg: '#FFFFFF', + borderRadius: 8, + colorText: '#1a1a1a', + colorTextDisabled: '#d9d9d9', + }, + + Tabs: { + itemColor: '#666666', + itemSelectedColor: '#C8363D', + itemHoverColor: '#C8363D', + itemActiveColor: '#C8363D', + inkBarColor: '#C8363D', + cardBg: '#F5F0E8', + cardPadding: '12px 16px', + titleFontSize: 16, + }, + + Steps: { + finishIconBorderColor: '#C8363D', + colorPrimary: '#C8363D', + }, + + // ===== 数据录入组件 ===== + Input: { + borderRadius: 8, + controlHeight: 40, + controlHeightLG: 48, + controlHeightSM: 32, + colorBorder: '#E8E3DB', + colorBgContainer: '#FFFFFF', + colorTextPlaceholder: '#CCCCCC', + activeBorderColor: '#C8363D', + hoverBorderColor: '#C8363D', + activeShadow: '0 0 0 2px rgba(200, 54, 61, 0.1)', + }, + + InputNumber: { + borderRadius: 8, + controlHeight: 40, + handleVisible: true, + }, + + Select: { + borderRadius: 8, + controlHeight: 40, + optionSelectedBg: 'rgba(200, 54, 61, 0.08)', + optionActiveBg: 'rgba(200, 54, 61, 0.05)', + optionSelectedColor: '#C8363D', + }, + + Checkbox: { + borderRadiusSM: 4, + colorPrimary: '#C8363D', + colorPrimaryHover: '#A82E34', + }, + + Radio: { + colorPrimary: '#C8363D', + dotSize: 10, + }, + + Switch: { + colorPrimary: '#C8363D', + colorPrimaryHover: '#A82E34', + }, + + Slider: { + trackBg: '#C8363D', + trackHoverBg: '#A82E34', + handleColor: '#C8363D', + handleActiveColor: '#8B252B', + dotBorderColor: '#E8E3DB', + dotActiveBorderColor: '#C8363D', + }, + + DatePicker: { + borderRadius: 8, + controlHeight: 40, + cellActiveWithRangeBg: 'rgba(200, 54, 61, 0.1)', + cellHoverBg: 'rgba(200, 54, 61, 0.05)', + }, + + Rate: { + colorFillContent: '#E8E3DB', + starColor: '#D4A574', + starSize: 20, + }, + + Form: { + labelColor: '#2C2C2C', + labelFontSize: 14, + labelHeight: 32, + labelRequiredMarkColor: '#FF4D4F', + itemMarginBottom: 24, + }, + + Upload: { + colorBorder: '#E8E3DB', + colorBorderHover: '#C8363D', + colorPrimary: '#C8363D', + }, + + // ===== 数据展示组件 ===== + Card: { + borderRadiusLG: 12, + boxShadowTertiary: '0 2px 12px rgba(0, 0, 0, 0.08)', + headerBg: '#FFFFFF', + headerFontSize: 16, + headerHeight: 48, + paddingLG: 24, + colorBorderSecondary: '#E8E3DB', + }, + + Carousel: { + dotHeight: 8, + dotWidth: 24, + dotWidthActive: 32, + dotGap: 8, + }, + + Collapse: { + headerBg: '#F5F0E8', + headerPadding: '12px 16px', + contentBg: '#FFFFFF', + contentPadding: '16px', + borderRadiusLG: 12, + }, + + Descriptions: { + labelBg: '#F5F0E8', + titleColor: '#2C2C2C', + contentColor: '#666666', + itemPaddingBottom: 16, + }, + + Empty: { + colorTextDescription: '#999999', + fontSize: 14, + }, + + Image: { + previewOperationColor: '#FFFFFF', + previewOperationColorDisabled: 'rgba(255, 255, 255, 0.3)', + }, + + List: { + itemPadding: '12px 0', + itemPaddingSM: '8px 16px', + itemPaddingLG: '16px 24px', + }, + + Table: { + headerBg: '#F5F0E8', + headerColor: '#2C2C2C', + rowHoverBg: '#FFF9F0', + rowSelectedBg: 'rgba(200, 54, 61, 0.05)', + rowSelectedHoverBg: 'rgba(200, 54, 61, 0.08)', + borderColor: '#E8E3DB', + headerSplitColor: '#E8E3DB', + borderRadius: 12, + cellPaddingBlock: 16, + cellFontSize: 14, + }, + + Tag: { + defaultBg: '#F5F0E8', + defaultColor: '#666666', + borderRadiusSM: 6, + fontSizeSM: 12, + }, + + Timeline: { + dotBorderWidth: 2, + dotBg: '#FFFFFF', + tailColor: '#E8E3DB', + tailWidth: 2, + }, + + Tooltip: { + colorBgSpotlight: 'rgba(44, 44, 44, 0.9)', + borderRadius: 8, + }, + + Statistic: { + titleFontSize: 14, + contentFontSize: 24, + fontFamily: 'monospace', + }, + + Badge: { + colorError: '#C8363D', + dotSize: 8, + statusSize: 8, + }, + + Avatar: { + borderRadius: 8, + containerSize: 40, + containerSizeLG: 48, + containerSizeSM: 32, + }, + + // ===== 反馈组件 ===== + Alert: { + borderRadiusLG: 12, + colorInfoBg: '#E6F7FF', + colorSuccessBg: '#F6FFED', + colorWarningBg: '#FFFBE6', + colorErrorBg: '#FFF1F0', + defaultPadding: '12px 16px', + }, + + Modal: { + headerBg: '#FFFFFF', + contentBg: '#FFFFFF', + footerBg: '#FFFFFF', + borderRadiusLG: 12, + boxShadow: '0 6px 48px rgba(0, 0, 0, 0.12)', + titleColor: '#2C2C2C', + titleFontSize: 18, + }, + + Drawer: { + colorBgElevated: '#FFFFFF', + paddingLG: 24, + footerPaddingBlock: 12, + footerPaddingInline: 16, + }, + + Message: { + contentBg: 'rgba(44, 44, 44, 0.9)', + contentPadding: '10px 16px', + borderRadiusLG: 8, + }, + + Notification: { + width: 384, + borderRadiusLG: 12, + boxShadow: '0 6px 24px rgba(0, 0, 0, 0.12)', + paddingContentHorizontal: 24, + paddingContentVertical: 16, + }, + + Progress: { + defaultColor: '#C8363D', + circleTextColor: '#2C2C2C', + remainingColor: '#F5F0E8', + }, + + Result: { + titleFontSize: 24, + subtitleFontSize: 14, + iconFontSize: 72, + }, + + Skeleton: { + color: '#F5F0E8', + colorGradientEnd: 'rgba(245, 240, 232, 0.2)', + }, + + Spin: { + colorPrimary: '#C8363D', + dotSize: 20, + dotSizeSM: 14, + dotSizeLG: 32, + }, + + Popconfirm: { + borderRadiusLG: 12, + minWidth: 280, + }, + + // ===== 其他组件 ===== + Anchor: { + linkPaddingBlock: 4, + linkPaddingInlineStart: 16, + }, + + Segmented: { + borderRadius: 8, + itemSelectedBg: '#C8363D', + itemSelectedColor: '#FFFFFF', + itemHoverBg: 'rgba(200, 54, 61, 0.05)', + }, + + Watermark: { + colorFill: 'rgba(0, 0, 0, 0.05)', + fontSize: 16, + }, +} + +// 导出类型 +export type ComponentTokens = typeof componentTokens diff --git a/src/theme/index.ts b/src/theme/index.ts new file mode 100644 index 0000000..8c655b3 --- /dev/null +++ b/src/theme/index.ts @@ -0,0 +1,27 @@ +/** + * 非遗文化传承网站 - 主题配置入口 + * 整合 Token 和组件样式,用于 ConfigProvider + */ + +import type { ThemeConfig } from 'antd' +import { themeTokens } from './tokens' +import { componentTokens } from './components' + +/** + * Ant Design 主题配置 + * 基于中国传统色彩体系,打造具有非遗文化特色的视觉风格 + */ +export const heritageTheme: ThemeConfig = { + token: themeTokens, + components: componentTokens, + cssVar: true, // 启用 CSS 变量 + hashed: true, // 启用样式哈希 +} + +// 导出子模块 +export { themeTokens } from './tokens' +export { componentTokens } from './components' + +// 导出类型 +export type { ThemeTokens } from './tokens' +export type { ComponentTokens } from './components' diff --git a/src/theme/tokens.ts b/src/theme/tokens.ts new file mode 100644 index 0000000..de78959 --- /dev/null +++ b/src/theme/tokens.ts @@ -0,0 +1,90 @@ +/** + * 非遗文化传承网站 - 主题 Token 配置 + * 基于中国传统色彩体系设计 + */ + +export const themeTokens = { + // ===== 主色调 ===== + colorPrimary: '#C8363D', // 朱砂红 - 象征传统文化的热情与活力 + colorInfo: '#4A5F7F', // 青黛蓝 - 象征深邃与智慧 + colorSuccess: '#52C41A', // 成功绿 + colorWarning: '#FAAD14', // 警告黄 + colorError: '#FF4D4F', // 错误红 + + // ===== 辅助色 ===== + colorAccent: '#D4A574', // 金沙黄 - 象征精湛与珍贵 + colorAuxiliary1: '#2A5E4D', // 墨绿 - 象征沉稳与生命力 + colorAuxiliary2: '#4A5F7F', // 青黛蓝 + + // ===== 背景色系统 ===== + colorBgBase: '#FAFAF8', // 宣纸色 - 基础背景 + colorBgContainer: '#FFFFFF', // 纯白 - 容器背景 + colorBgElevated: '#FFFFFF', // 浮层背景 + colorBgLayout: '#F5F0E8', // 浅米黄 - 布局背景 + colorBgSection: '#F5F0E8', // Section 背景 + + // ===== 文本色系统 ===== + colorTextBase: '#2C2C2C', // 深灰黑 - 主文本 + colorText: '#2C2C2C', // 主文本 + colorTextSecondary: '#666666', // 中灰 - 次要文本 + colorTextTertiary: '#999999', // 浅灰 - 辅助文本 + colorTextQuaternary: '#CCCCCC', // 极浅灰 - 占位文本 + + // ===== 边框色 ===== + colorBorder: '#E8E3DB', // 主边框色 + colorBorderSecondary: '#F0EBE3', // 次要边框色 + + // ===== 圆角系统 ===== + borderRadius: 8, // 基础圆角 + borderRadiusLG: 12, // 大圆角 + borderRadiusSM: 6, // 小圆角 + borderRadiusXS: 4, // 极小圆角 + + // ===== 字体系统 ===== + fontSize: 14, // 基础字号 + fontSizeLG: 16, // 大字号 + fontSizeSM: 12, // 小字号 + fontSizeHeading1: 38, // 标题1 + fontSizeHeading2: 30, // 标题2 + fontSizeHeading3: 24, // 标题3 + fontSizeHeading4: 20, // 标题4 + fontSizeHeading5: 16, // 标题5 + + // 字体家族 + fontFamily: `-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, + 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', + 'Noto Color Emoji', 'Noto Sans SC', 'Microsoft YaHei'`, + + fontFamilySerif: `'Noto Serif SC', 'Songti SC', Georgia, serif`, + + // ===== 行高 ===== + lineHeight: 1.5715, + lineHeightHeading1: 1.2, + lineHeightHeading2: 1.3, + lineHeightHeading3: 1.35, + + // ===== 阴影系统 ===== + boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)', + boxShadowSecondary: '0 4px 16px rgba(0, 0, 0, 0.12)', + boxShadowTertiary: '0 6px 24px rgba(0, 0, 0, 0.16)', + + // ===== 控件高度 ===== + controlHeight: 40, // 基础控件高度 + controlHeightLG: 48, // 大控件高度 + controlHeightSM: 32, // 小控件高度 + controlHeightXS: 24, // 极小控件高度 + + // ===== 动画 ===== + motionUnit: 0.1, + motionBase: 0, + motionEaseInOut: 'cubic-bezier(0.645, 0.045, 0.355, 1)', + motionEaseOut: 'cubic-bezier(0.215, 0.61, 0.355, 1)', + + // ===== 其他 ===== + wireframe: false, // 关闭线框模式 + zIndexBase: 0, + zIndexPopupBase: 1000, +} + +// 导出类型 +export type ThemeTokens = typeof themeTokens diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..e95c7bc --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,317 @@ +/** + * 非遗文化传承网站 - TypeScript 类型定义 + */ + +// ===== 非遗项目类型 ===== +export interface HeritageItem { + id: string + name: string + category: HeritageCategory + province: string + city?: string + level: HeritageLevel + coverImage: string + images?: string[] + description: string + history: string + skills: string + significance: string + inheritors: string[] // 传承人ID列表 + relatedWorks?: Work[] + videoUrl?: string + virtualTourUrl?: string + status: 'active' | 'endangered' | 'revived' + tags: string[] + viewCount: number + likeCount: number + createdAt: string + updatedAt: string +} + +// 非遗分类 +export type HeritageCategory = + | 'folk-literature' // 民间文学 + | 'traditional-music' // 传统音乐 + | 'traditional-dance' // 传统舞蹈 + | 'traditional-opera' // 传统戏剧 + | 'folk-art' // 曲艺 + | 'sports-acrobatics' // 传统体育、游艺与杂技 + | 'traditional-craft' // 传统技艺 + | 'traditional-medicine'// 传统医药 + | 'folk-custom' // 民俗 + | 'traditional-art' // 传统美术 + +// 非遗级别 +export type HeritageLevel = + | 'world' // 世界级 + | 'national' // 国家级 + | 'provincial' // 省级 + | 'municipal' // 市级 + | 'county' // 县级 + +// ===== 传承人类型 ===== +export interface Inheritor { + id: string + name: string + avatar: string + coverImage?: string + gender: 'male' | 'female' + birthYear: number + province: string + city?: string + level: 'national' | 'provincial' | 'municipal' + heritageItems: string[] // 关联的非遗项目ID + title: string // 称号:如"国家级代表性传承人" + bio: string + masterSkills: string + achievements: Achievement[] + awards: Award[] + works: Work[] + videos: Video[] + contactInfo?: ContactInfo + socialMedia?: SocialMedia + followers: number + viewCount: number + createdAt: string + updatedAt: string +} + +export interface Achievement { + id: string + title: string + description: string + date: string + images?: string[] +} + +export interface Award { + id: string + name: string + level: string + year: number + organization: string +} + +export interface Work { + id: string + name: string + image: string + description: string + year: number + materials?: string + dimensions?: string + price?: number +} + +export interface Video { + id: string + title: string + cover: string + url: string + duration: number // 秒 + description?: string + viewCount: number + publishDate: string +} + +export interface ContactInfo { + phone?: string + email?: string + address?: string + website?: string +} + +export interface SocialMedia { + weibo?: string + wechat?: string + douyin?: string + bilibili?: string +} + +// ===== 活动与资讯类型 ===== +export interface NewsArticle { + id: string + title: string + subtitle?: string + cover: string + category: 'exhibition' | 'activity' | 'policy' | 'research' | 'story' + content: string + summary: string + author: string + publishDate: string + tags: string[] + viewCount: number + likeCount: number + relatedHeritage?: string[] + relatedInheritors?: string[] +} + +export interface Event { + id: string + title: string + cover: string + type: 'exhibition' | 'workshop' | 'performance' | 'lecture' | 'festival' + location: string + address: string + startDate: string + endDate: string + startTime?: string + endTime?: string + description: string + organizer: string + capacity?: number + enrolled: number + price: number + isFree: boolean + tags: string[] + status: 'upcoming' | 'ongoing' | 'finished' | 'cancelled' + registrationRequired: boolean + contactInfo: ContactInfo + relatedHeritage?: string[] + images: string[] + viewCount: number +} + +// ===== 用户相关类型 ===== +export interface User { + id: string + username: string + nickname: string + avatar: string + email: string + phone?: string + bio?: string + favorites: string[] // 收藏的非遗项目ID + followedInheritors: string[] // 关注的传承人ID + enrolledCourses: string[] // 已报名课程ID + registeredEvents: string[] // 已报名活动ID + points: number + level: number + createdAt: string +} + +// ===== 体验相关类型 ===== +export interface VirtualTour { + id: string + title: string + cover: string + description: string + panoramaUrl: string + hotspots: Hotspot[] + heritageId: string +} + +export interface Hotspot { + id: string + position: { x: number; y: number; z: number } + title: string + description: string + mediaType: 'image' | 'video' | 'audio' | 'text' + mediaUrl?: string +} + +export interface Workshop { + id: string + title: string + cover: string + heritageId: string + inheritorId: string + location: string + address: string + duration: number // 小时 + maxParticipants: number + enrolled: number + price: number + dates: string[] // 可预约日期 + description: string + whatToLearn: string[] + requirements: string[] + providedMaterials: string[] +} + +// ===== 统计数据类型 ===== +export interface Statistics { + totalHeritageItems: number + totalInheritors: number + totalProvinces: number + totalCities: number + worldHeritage: number + nationalHeritage: number + provincialHeritage: number + endangeredCount: number + activePreservation: number +} + +// ===== 通用类型 ===== +export interface PaginationParams { + page: number + pageSize: number +} + +export interface PaginationResult { + data: T[] + total: number + page: number + pageSize: number + totalPages: number +} + +export interface FilterParams { + category?: HeritageCategory | HeritageCategory[] + level?: HeritageLevel | HeritageLevel[] + province?: string | string[] + type?: string | string[] + status?: string | string[] + search?: string + sortBy?: 'name' | 'viewCount' | 'likeCount' | 'createdAt' + sortOrder?: 'asc' | 'desc' +} + +export interface ApiResponse { + code: number + message: string + data: T + timestamp: number +} + +// ===== 评论类型 ===== +export interface Comment { + id: string + userId: string + userName: string + userAvatar: string + targetType: 'heritage' | 'inheritor' | 'news' + targetId: string + content: string + rating?: number // 1-5 评分 + images?: string[] + likeCount: number + replyCount: number + replies?: Comment[] + createdAt: string + updatedAt: string +} + +// ===== 搜索相关类型 ===== +export interface SearchResult { + type: 'heritage' | 'inheritor' | 'news' + id: string + title: string + subtitle?: string + cover: string + description: string + tags: string[] + score: number // 搜索相关度评分 +} + +export interface SearchResults { + heritages: HeritageItem[] + inheritors: Inheritor[] + news: NewsArticle[] +} + +export interface SearchHistory { + id: string + keyword: string + timestamp: string +}