From 7dc8d2a5289654c7307875100b2c380519ccc1d4 Mon Sep 17 00:00:00 2001 From: WuYongJie Date: Sat, 22 Nov 2025 16:20:05 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=BB=9F=E4=B8=80=E5=BC=B9=E7=AA=97?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=20&=20=E9=A6=96=E9=A1=B5UI=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 替换所有浏览器原生弹窗(confirm/alert/prompt) - 创建 ConfirmDialog、AlertDialog、PromptDialog 组件 - 创建 DialogProvider 和 useDialog Hook - 替换所有 admin 组件、首页、分享页面中的原生弹窗 - 素材预览面板统一 - 创建 MaterialPreviewModal 组件 - 使用统一弹窗组件,统一退出逻辑 - 首页UI统一到三层架构 - 迁移主要按钮和输入框样式 - 迁移筛选弹窗中的所有按钮样式 - 使用 useThemeStyles() Hook - 更新文档 - 更新优化记录、优化计划、后续修改、更新日志 --- app/admin/_components/CategoriesTab.tsx | 23 +++- app/admin/_components/DepartmentsTab.tsx | 25 +++- app/admin/_components/InvitationsTab.tsx | 9 +- app/admin/_components/MaterialsTab.tsx | 9 +- app/admin/_components/TemplateManager.tsx | 10 +- app/admin/_components/UsersTab.tsx | 11 +- app/admin/page.tsx | 16 +-- app/page.tsx | 158 ++++++++++++---------- app/providers.tsx | 7 +- app/share/[id]/page.tsx | 38 +++++- components/ChangePasswordModal.tsx | 8 +- components/DanmakuWidget.tsx | 14 +- components/MaterialPreviewModal.tsx | 60 ++++++++ components/ui/AlertDialog.tsx | 65 +++++++++ components/ui/ConfirmDialog.tsx | 79 +++++++++++ components/ui/DialogProvider.tsx | 118 ++++++++++++++++ components/ui/PromptDialog.tsx | 101 ++++++++++++++ hooks/useAlert.ts | 44 ++++++ hooks/useConfirm.ts | 97 +++++++++++++ 优化计划与优先级.md | 33 +++-- 优化记录.md | 25 +++- 后续修改.txt | 4 +- 更新日志.md | 68 ++++++++++ 23 files changed, 899 insertions(+), 123 deletions(-) create mode 100644 components/MaterialPreviewModal.tsx create mode 100644 components/ui/AlertDialog.tsx create mode 100644 components/ui/ConfirmDialog.tsx create mode 100644 components/ui/DialogProvider.tsx create mode 100644 components/ui/PromptDialog.tsx create mode 100644 hooks/useAlert.ts create mode 100644 hooks/useConfirm.ts diff --git a/app/admin/_components/CategoriesTab.tsx b/app/admin/_components/CategoriesTab.tsx index b81a93a..b8fbdd8 100644 --- a/app/admin/_components/CategoriesTab.tsx +++ b/app/admin/_components/CategoriesTab.tsx @@ -2,6 +2,7 @@ import { useState, useEffect, useCallback } from 'react' import { useThemeStyles, useTheme, cn } from '@/lib/ui-styles' +import { useDialog } from '@/components/ui/DialogProvider' import type { Category, Scene, Platform, ToastFn } from './types' interface CategoriesTabProps { @@ -12,6 +13,7 @@ interface CategoriesTabProps { export default function CategoriesTab({ showToast, onDataChange }: CategoriesTabProps) { const styles = useThemeStyles() const theme = useTheme() + const { confirm } = useDialog() const [categories, setCategories] = useState([]) const [scenes, setScenes] = useState([]) const [platforms, setPlatforms] = useState([]) @@ -155,7 +157,12 @@ export default function CategoriesTab({ showToast, onDataChange }: CategoriesTab } const handleDeleteCategory = async (id: string) => { - if (!confirm('确定要删除这个使用店铺分类吗?')) return + const confirmed = await confirm({ + title: '确认删除', + message: '确定要删除这个使用店铺分类吗?', + variant: 'danger', + }) + if (!confirmed) return try { const response = await fetch(`/api/categories?id=${id}`, { @@ -177,7 +184,12 @@ export default function CategoriesTab({ showToast, onDataChange }: CategoriesTab } const handleDeleteScene = async (id: string) => { - if (!confirm('确定要删除这个场景类别吗?')) return + const confirmed = await confirm({ + title: '确认删除', + message: '确定要删除这个场景类别吗?', + variant: 'danger', + }) + if (!confirmed) return try { const response = await fetch(`/api/scenes?id=${id}`, { @@ -199,7 +211,12 @@ export default function CategoriesTab({ showToast, onDataChange }: CategoriesTab } const handleDeletePlatform = async (id: string) => { - if (!confirm('确定要删除这个平台分类吗?')) return + const confirmed = await confirm({ + title: '确认删除', + message: '确定要删除这个平台分类吗?', + variant: 'danger', + }) + if (!confirmed) return try { const response = await fetch(`/api/platforms?id=${id}`, { diff --git a/app/admin/_components/DepartmentsTab.tsx b/app/admin/_components/DepartmentsTab.tsx index 73a3ce0..7a8b7ba 100644 --- a/app/admin/_components/DepartmentsTab.tsx +++ b/app/admin/_components/DepartmentsTab.tsx @@ -2,6 +2,7 @@ import { useCallback, useMemo, useState } from 'react' import { useThemeStyles, useTheme, cn } from '@/lib/ui-styles' +import { useDialog } from '@/components/ui/DialogProvider' import type { Department, DepartmentNode, ToastFn } from './types' import { buildDepartmentTree, filterDepartmentTree } from '../_utils/departmentTree' @@ -22,6 +23,7 @@ export default function DepartmentsTab({ }: DepartmentsTabProps) { const styles = useThemeStyles() const theme = useTheme() + const { confirm, prompt } = useDialog() const [departmentSearchTerm, setDepartmentSearchTerm] = useState('') const [collapsedDepartments, setCollapsedDepartments] = useState>(new Set()) const [renamingDepartmentId, setRenamingDepartmentId] = useState(null) @@ -67,7 +69,11 @@ export default function DepartmentsTab({ }, []) const handleRootAdd = async () => { - const input = window.prompt('为「主公司」添加一级分支,例如:一部、设计部、技术部等', '') + const input = await prompt({ + title: '添加一级分支', + message: '为「主公司」添加一级分支,例如:一部、设计部、技术部等', + placeholder: '请输入分支名称', + }) if (!input) return const trimmed = input.trim() if (!trimmed) { @@ -83,10 +89,12 @@ export default function DepartmentsTab({ } const handleAddSubDepartment = async (parentName: string) => { - const input = window.prompt( - `为「${parentName}」添加子分支\n\n建议直接输入子分支名,例如:短视频、直播组等`, - `${parentName}-`, - ) + const input = await prompt({ + title: '添加子分支', + message: `为「${parentName}」添加子分支\n\n建议直接输入子分支名,例如:短视频、直播组等`, + defaultValue: `${parentName}-`, + placeholder: '请输入子分支名称', + }) if (!input) return const trimmed = input.trim() if (!trimmed) { @@ -120,7 +128,12 @@ export default function DepartmentsTab({ } const handleDelete = async (deptId: string, fullName: string) => { - if (!confirm(`确定要删除部门「${fullName}」吗?`)) return + const confirmed = await confirm({ + title: '确认删除', + message: `确定要删除部门「${fullName}」吗?`, + variant: 'danger', + }) + if (!confirmed) return await onDeleteDepartment(deptId, fullName) } diff --git a/app/admin/_components/InvitationsTab.tsx b/app/admin/_components/InvitationsTab.tsx index 3a2b3c6..abe0fa9 100644 --- a/app/admin/_components/InvitationsTab.tsx +++ b/app/admin/_components/InvitationsTab.tsx @@ -2,6 +2,7 @@ import { useState, useEffect, useCallback } from 'react' import { useThemeStyles, useTheme, cn } from '@/lib/ui-styles' +import { useDialog } from '@/components/ui/DialogProvider' import { formatDateTimeLocale } from '../_utils/formatters' import type { Invitation, ToastFn } from './types' @@ -12,6 +13,7 @@ interface InvitationsTabProps { export default function InvitationsTab({ showToast }: InvitationsTabProps) { const styles = useThemeStyles() const theme = useTheme() + const { confirm } = useDialog() const [invitations, setInvitations] = useState([]) const [showCreateInvitationModal, setShowCreateInvitationModal] = useState(false) const [newInvitationMaxUses, setNewInvitationMaxUses] = useState(1) @@ -104,7 +106,12 @@ export default function InvitationsTab({ showToast }: InvitationsTabProps) { } const handleDeleteInvitation = async (id: string) => { - if (!confirm('确定要删除这个邀请链接吗?')) return + const confirmed = await confirm({ + title: '确认删除', + message: '确定要删除这个邀请链接吗?', + variant: 'danger', + }) + if (!confirmed) return try { const response = await fetch(`/api/invitations/${id}`, { diff --git a/app/admin/_components/MaterialsTab.tsx b/app/admin/_components/MaterialsTab.tsx index b198cba..e844cbc 100644 --- a/app/admin/_components/MaterialsTab.tsx +++ b/app/admin/_components/MaterialsTab.tsx @@ -4,6 +4,7 @@ import { useState, useEffect, useCallback, useMemo } from 'react' import Image from 'next/image' import EditMaterialModal from '@/components/EditMaterialModal' import { useThemeStyles, useTheme, cn } from '@/lib/ui-styles' +import { useDialog } from '@/components/ui/DialogProvider' import type { Material, ToastFn } from './types' import LoadingState from './ui/LoadingState' @@ -13,6 +14,7 @@ interface MaterialsTabProps { export default function MaterialsTab({ showToast }: MaterialsTabProps) { const styles = useThemeStyles() + const { confirm } = useDialog() const theme = useTheme() const [materials, setMaterials] = useState([]) const [loading, setLoading] = useState(true) @@ -42,7 +44,12 @@ export default function MaterialsTab({ showToast }: MaterialsTabProps) { }, [fetchMaterials]) const handleDeleteMaterial = async (id: string) => { - if (!confirm('确定要删除这个素材吗?')) return + const confirmed = await confirm({ + title: '确认删除', + message: '确定要删除这个素材吗?', + variant: 'danger', + }) + if (!confirmed) return try { const response = await fetch(`/api/materials?id=${id}`, { diff --git a/app/admin/_components/TemplateManager.tsx b/app/admin/_components/TemplateManager.tsx index ed03bce..90872d9 100644 --- a/app/admin/_components/TemplateManager.tsx +++ b/app/admin/_components/TemplateManager.tsx @@ -3,6 +3,7 @@ import { DragEvent, useCallback, useEffect, useMemo, useState } from 'react' import { formatDateTimeShort } from '../_utils/formatters' import { useThemeStyles, cn } from '@/lib/ui-styles' +import { useDialog } from '@/components/ui/DialogProvider' import { animations } from '@/lib/ui-animations' import TemplateManagerHeader from './template-manager/TemplateManagerHeader' import BuiltInTemplateGrid from './template-manager/BuiltInTemplateGrid' @@ -423,6 +424,7 @@ const requestJson = async (input: RequestInfo, init?: RequestInit): Promise< export default function TemplateManager({ showToast }: { showToast: ToastFn }) { const styles = useThemeStyles() + const { confirm } = useDialog() const [templates, setTemplates] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) @@ -1244,7 +1246,13 @@ export default function TemplateManager({ showToast }: { showToast: ToastFn }) { } const handleDelete = async (templateId: string) => { - if (deletingId || !confirm('确定删除该模板吗?')) return + if (deletingId) return + const confirmed = await confirm({ + title: '确认删除', + message: '确定删除该模板吗?', + variant: 'danger', + }) + if (!confirmed) return setDeletingId(templateId) try { await requestJson(`/api/admin/upload-templates/${templateId}`, { method: 'DELETE' }) diff --git a/app/admin/_components/UsersTab.tsx b/app/admin/_components/UsersTab.tsx index c815094..a88f5b2 100644 --- a/app/admin/_components/UsersTab.tsx +++ b/app/admin/_components/UsersTab.tsx @@ -2,6 +2,7 @@ import { useCallback, useMemo, useState } from 'react' import { useThemeStyles, useTheme, cn } from '@/lib/ui-styles' +import { useDialog } from '@/components/ui/DialogProvider' import type { Department, DepartmentNode, DepartmentUserGrouping, User, UserTreeNode } from './types' import { buildDepartmentTree } from '../_utils/departmentTree' import DepartmentHierarchy from './users/DepartmentHierarchy' @@ -189,14 +190,20 @@ export default function UsersTab({ handleCloseEditModal() }, [editingUser, onToggleDownload, handleCloseEditModal]) + const { confirm } = useDialog() const handleDeleteUserInternal = useCallback(async () => { if (!editingUser) return - if (!confirm('确定要删除这个用户吗?该用户的所有素材也会被删除。')) return + const confirmed = await confirm({ + title: '确认删除', + message: '确定要删除这个用户吗?该用户的所有素材也会被删除。', + variant: 'danger', + }) + if (!confirmed) return const success = await onDeleteUser(editingUser.id) if (success) { handleCloseEditModal() } - }, [editingUser, onDeleteUser, handleCloseEditModal]) + }, [editingUser, onDeleteUser, handleCloseEditModal, confirm]) const showTransferButton = useMemo( () => Boolean(currentIsSuperAdmin && editingUser && editingUser.email !== currentUserEmail), diff --git a/app/admin/page.tsx b/app/admin/page.tsx index 120328c..4ed2577 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -383,7 +383,7 @@ const departmentCollator = useMemo(() => getDepartmentCollator(), []) aria-controls="invitations-panel" id="invitations-tab" onClick={() => setActiveTab('invitations')} - onKeyDown={(e) => { + onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault() setActiveTab('invitations') @@ -400,16 +400,16 @@ const departmentCollator = useMemo(() => getDepartmentCollator(), []) ? 'bg-gradient-to-r from-indigo-400 via-purple-400 to-pink-400' : 'bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500', animations.slideInFromLeft)} /> )} - + )} - -
-
- {previewImage.title} e.stopPropagation()} - /> -
-

{previewImage.title}

-
- - - )} + setPreviewImage(null)} + imageUrl={previewImage?.url || ''} + title={previewImage?.title} + /> {/* Share Notification */} {showShareNotification && ( @@ -1342,14 +1356,14 @@ export default function Home() { onClick={() => setShowFilterModal(false)} >
e.stopPropagation()} >
-

筛选选项

+

筛选选项

+ } + > +
+ {typeof message === 'string' ? ( +

{message}

+ ) : ( + message + )} +
+ + ) +} + diff --git a/components/ui/ConfirmDialog.tsx b/components/ui/ConfirmDialog.tsx new file mode 100644 index 0000000..0d5bead --- /dev/null +++ b/components/ui/ConfirmDialog.tsx @@ -0,0 +1,79 @@ +'use client' + +import { ReactNode } from 'react' +import Modal from './Modal' +import { useThemeStyles } from '@/lib/ui-styles' + +interface ConfirmDialogProps { + isOpen: boolean + onClose: () => void + onConfirm: () => void + title: string + message: string | ReactNode + confirmText?: string + cancelText?: string + variant?: 'default' | 'danger' + loading?: boolean +} + +export default function ConfirmDialog({ + isOpen, + onClose, + onConfirm, + title, + message, + confirmText = '确认', + cancelText = '取消', + variant = 'default', + loading = false, +}: ConfirmDialogProps) { + const styles = useThemeStyles() + + const handleConfirm = () => { + if (!loading) { + onConfirm() + } + } + + return ( + + + + + } + > +
+ {typeof message === 'string' ? ( +

{message}

+ ) : ( + message + )} +
+
+ ) +} + diff --git a/components/ui/DialogProvider.tsx b/components/ui/DialogProvider.tsx new file mode 100644 index 0000000..110813b --- /dev/null +++ b/components/ui/DialogProvider.tsx @@ -0,0 +1,118 @@ +'use client' + +import { createContext, useContext, ReactNode } from 'react' +import ConfirmDialog from './ConfirmDialog' +import AlertDialog from './AlertDialog' +import PromptDialog from './PromptDialog' +import { useConfirm, usePrompt } from '@/hooks/useConfirm' +import { useAlert } from '@/hooks/useAlert' + +interface DialogContextType { + confirm: (options: { + title?: string + message: string + confirmText?: string + cancelText?: string + variant?: 'default' | 'danger' + }) => Promise + alert: (options: { + title?: string + message: string + buttonText?: string + variant?: 'default' | 'success' | 'warning' | 'error' + } | string) => void + prompt: (options: { + title?: string + message: string + defaultValue?: string + placeholder?: string + confirmText?: string + cancelText?: string + }) => Promise +} + +const DialogContext = createContext(null) + +export function useDialog() { + const context = useContext(DialogContext) + if (!context) { + throw new Error('useDialog must be used within DialogProvider') + } + return context +} + +interface DialogProviderProps { + children: ReactNode +} + +export function DialogProvider({ children }: DialogProviderProps) { + const confirmHook = useConfirm() + const alertHook = useAlert() + const promptHook = usePrompt() + + const confirm = async (options: { + title?: string + message: string + confirmText?: string + cancelText?: string + variant?: 'default' | 'danger' + }): Promise => { + return confirmHook.confirm(options) + } + + const alert = (options: { + title?: string + message: string + buttonText?: string + variant?: 'default' | 'success' | 'warning' | 'error' + } | string) => { + alertHook.alert(options) + } + + const prompt = async (options: { + title?: string + message: string + defaultValue?: string + placeholder?: string + confirmText?: string + cancelText?: string + }): Promise => { + return promptHook.prompt(options) + } + + return ( + + {children} + + + + + ) +} + diff --git a/components/ui/PromptDialog.tsx b/components/ui/PromptDialog.tsx new file mode 100644 index 0000000..8b57c84 --- /dev/null +++ b/components/ui/PromptDialog.tsx @@ -0,0 +1,101 @@ +'use client' + +import { useState, useEffect, useRef } from 'react' +import Modal from './Modal' +import { useThemeStyles } from '@/lib/ui-styles' + +interface PromptDialogProps { + isOpen: boolean + onClose: () => void + onConfirm: (value: string) => void + title: string + message: string + defaultValue?: string + placeholder?: string + confirmText?: string + cancelText?: string +} + +export default function PromptDialog({ + isOpen, + onClose, + onConfirm, + title, + message, + defaultValue = '', + placeholder = '', + confirmText = '确认', + cancelText = '取消', +}: PromptDialogProps) { + const styles = useThemeStyles() + const [value, setValue] = useState(defaultValue) + const inputRef = useRef(null) + + useEffect(() => { + if (isOpen) { + setValue(defaultValue) + // 延迟聚焦,确保弹窗已渲染 + setTimeout(() => { + inputRef.current?.focus() + inputRef.current?.select() + }, 100) + } + }, [isOpen, defaultValue]) + + const handleConfirm = () => { + if (value.trim()) { + onConfirm(value.trim()) + onClose() + } + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + handleConfirm() + } else if (e.key === 'Escape') { + onClose() + } + } + + return ( + + + + + } + > +
+

{message}

+ setValue(e.target.value)} + onKeyDown={handleKeyDown} + placeholder={placeholder} + className={styles.inputStyles.base} + /> +
+
+ ) +} + diff --git a/hooks/useAlert.ts b/hooks/useAlert.ts new file mode 100644 index 0000000..099ac74 --- /dev/null +++ b/hooks/useAlert.ts @@ -0,0 +1,44 @@ +'use client' + +import { useState, useCallback } from 'react' + +interface AlertOptions { + title?: string + message: string + buttonText?: string + variant?: 'default' | 'success' | 'warning' | 'error' +} + +export function useAlert() { + const [isOpen, setIsOpen] = useState(false) + const [options, setOptions] = useState({ message: '' }) + + const alert = useCallback((opts: AlertOptions | string) => { + if (typeof opts === 'string') { + setOptions({ + title: '提示', + message: opts, + }) + } else { + setOptions({ + title: opts.title || '提示', + message: opts.message, + buttonText: opts.buttonText, + variant: opts.variant, + }) + } + setIsOpen(true) + }, []) + + const close = useCallback(() => { + setIsOpen(false) + }, []) + + return { + isOpen, + options, + alert, + close, + } +} + diff --git a/hooks/useConfirm.ts b/hooks/useConfirm.ts new file mode 100644 index 0000000..03254f1 --- /dev/null +++ b/hooks/useConfirm.ts @@ -0,0 +1,97 @@ +'use client' + +import { useState, useCallback } from 'react' + +interface ConfirmOptions { + title?: string + message: string + confirmText?: string + cancelText?: string + variant?: 'default' | 'danger' +} + +interface PromptOptions { + title?: string + message: string + defaultValue?: string + placeholder?: string + confirmText?: string + cancelText?: string +} + +export function useConfirm() { + const [isOpen, setIsOpen] = useState(false) + const [options, setOptions] = useState({ message: '' }) + const [resolveCallback, setResolveCallback] = useState<((value: boolean) => void) | null>(null) + + const confirm = useCallback((opts: ConfirmOptions): Promise => { + return new Promise((resolve) => { + setOptions(opts) + setIsOpen(true) + setResolveCallback(() => resolve) + }) + }, []) + + const handleConfirm = useCallback(() => { + setIsOpen(false) + if (resolveCallback) { + resolveCallback(true) + setResolveCallback(null) + } + }, [resolveCallback]) + + const handleCancel = useCallback(() => { + setIsOpen(false) + if (resolveCallback) { + resolveCallback(false) + setResolveCallback(null) + } + }, [resolveCallback]) + + return { + isOpen, + options, + confirm, + handleConfirm, + handleCancel, + } +} + +export function usePrompt() { + const [isOpen, setIsOpen] = useState(false) + const [options, setOptions] = useState({ message: '' }) + const [resolveCallback, setResolveCallback] = useState<((value: string | null) => void) | null>(null) + + const prompt = useCallback((opts: PromptOptions): Promise => { + return new Promise((resolve) => { + setOptions(opts) + setIsOpen(true) + setResolveCallback(() => resolve) + }) + }, []) + + const handleConfirm = useCallback((value: string) => { + setIsOpen(false) + if (resolveCallback) { + resolveCallback(value) + setResolveCallback(null) + } + }, [resolveCallback]) + + const handleCancel = useCallback(() => { + setIsOpen(false) + if (resolveCallback) { + resolveCallback(null) + setResolveCallback(null) + } + }, [resolveCallback]) + + return { + isOpen, + options, + prompt, + handleConfirm, + handleCancel, + } +} + diff --git a/优化计划与优先级.md b/优化计划与优先级.md index 0777114..c4e1777 100644 --- a/优化计划与优先级.md +++ b/优化计划与优先级.md @@ -34,7 +34,10 @@ - ✅ 提供 `useModal` Hook(`hooks/useModal.ts`)统一管理弹窗状态 - ✅ 完整的ARIA标签和键盘导航支持 - ✅ 样式参考苹果设计语言,遵循统一视觉方案 -- ⏳ 替换所有浏览器原生弹窗(confirm/alert)- 待后续逐步替换 +- ✅ 替换所有浏览器原生弹窗(confirm/alert/prompt) + - ✅ 创建了 ConfirmDialog、AlertDialog、PromptDialog 组件 + - ✅ 创建了 DialogProvider 和 useDialog Hook + - ✅ 替换了所有 admin 组件、首页、分享页面中的原生弹窗 **依赖关系**:无(基础组件) **影响范围**:全局(所有需要弹窗的地方) @@ -44,16 +47,18 @@ #### 1.2 素材预览面板统一 ⭐⭐⭐⭐⭐ **需求编号**:15 **优先级**:最高 +**状态**:✅ 已完成 **原因**: - 统一退出逻辑和交互体验 - 必须使用统一弹窗组件(依赖1.1) - 提升用户体验一致性 **实施要点**: -- 使用统一弹窗组件重构素材预览面板 -- 统一退出逻辑(ESC键、点击遮罩、关闭按钮) -- 使用三层架构样式系统 -- 确保深浅色模式适配 +- ✅ 创建了 MaterialPreviewModal 组件 +- ✅ 使用统一弹窗组件重构素材预览面板 +- ✅ 统一退出逻辑(ESC键、点击遮罩、关闭按钮) +- ✅ 使用三层架构样式系统 +- ✅ 确保深浅色模式适配 **依赖关系**:依赖 1.1(统一弹窗组件) **影响范围**:首页素材预览 @@ -84,15 +89,19 @@ #### 1.4 首页UI统一到三层架构 ⭐⭐⭐⭐ **需求编号**:22 **优先级**:高 +**状态**:✅ 已完成(主要部分) **原因**: - 统一UI架构,确保视觉一致性 - 首页是核心页面,必须遵循三层架构 **实施要点**: -- 将首页所有UI组件迁移到三层架构 -- 使用 `useThemeStyles()` Hook -- 移除所有 `dark:` 前缀样式 -- 确保深浅色模式完美适配 +- ✅ 迁移了主要按钮样式(上传按钮、筛选按钮) +- ✅ 迁移了搜索输入框样式 +- ✅ 迁移了筛选弹窗中的所有按钮样式(类型分页、使用店铺、场景类别、平台分类、上传月份) +- ✅ 迁移了筛选弹窗容器样式 +- ✅ 迁移了筛选弹窗关闭按钮样式 +- ✅ 使用 `useThemeStyles()` Hook +- ⏳ 部分文本颜色和标签样式仍使用直接 Tailwind 类(可后续继续优化) **依赖关系**:无 **影响范围**:首页所有UI组件 @@ -448,10 +457,10 @@ ## 📊 实施路线图 ### 第一阶段:基础组件与UI统一(2-3周) -1. ✅ 创建统一弹窗组件(1.1)- 已完成基础组件,待替换原生弹窗 -2. ⏳ 素材预览面板统一(1.2) +1. ✅ 创建统一弹窗组件(1.1)- 已完成,包括替换所有原生弹窗 +2. ✅ 素材预览面板统一(1.2)- 已完成 3. ✅ 首页素材展示响应式优化(1.3)- 已完成 -4. ⏳ 首页UI统一到三层架构(1.4) +4. ✅ 首页UI统一到三层架构(1.4)- 主要部分已完成 5. ⏳ 统一UI三层架构通用组件库(1.5) 6. ⏳ 深浅色主题适配优化(1.6) 7. ⏳ 低饱和度炫彩元素设计(1.7) diff --git a/优化记录.md b/优化记录.md index d6af241..b38ae67 100644 --- a/优化记录.md +++ b/优化记录.md @@ -295,10 +295,15 @@ export default function MyComponent() { - ✅ 支持多种尺寸(small, medium, large, xlarge, fullscreen) - ✅ 支持ESC键关闭、点击遮罩关闭 - ✅ 完整的ARIA标签和键盘导航支持 - - ⏳ 替换所有浏览器原生弹窗(confirm/alert) -2. ⏳ 素材预览面板统一 - - 使用统一弹窗组件 - - 统一退出逻辑 + - ✅ 替换所有浏览器原生弹窗(confirm/alert/prompt) + - ✅ 创建了 ConfirmDialog、AlertDialog、PromptDialog 组件 + - ✅ 创建了 DialogProvider 和 useDialog Hook + - ✅ 替换了所有 admin 组件、首页、分享页面中的原生弹窗 +2. ✅ 素材预览面板统一 + - ✅ 创建了 MaterialPreviewModal 组件 + - ✅ 使用统一弹窗组件 + - ✅ 统一退出逻辑(ESC键、点击遮罩、关闭按钮) + - ✅ 使用三层架构样式系统 3. ✅ 首页素材展示响应式优化 - ✅ 修复卡片比例固定问题(使用 `aspect-[3/4]` 替代固定高度 `h-[400px]`) - ✅ 添加容器边距(`px-4 sm:px-6 lg:px-8`)避免卡片贴到浏览器边界 @@ -306,10 +311,18 @@ export default function MyComponent() { - ✅ 优化图片 sizes 属性,提升响应式性能 - ✅ 统一使用 `slate` 系列颜色,确保深浅色模式适配 +4. ✅ 首页UI统一到三层架构(主要部分) + - ✅ 迁移了主要按钮样式(上传按钮、筛选按钮) + - ✅ 迁移了搜索输入框样式 + - ✅ 迁移了筛选弹窗中的所有按钮样式 + - ✅ 迁移了筛选弹窗容器样式 + - ✅ 迁移了筛选弹窗关闭按钮样式 + - ⏳ 部分文本颜色和标签样式仍使用直接 Tailwind 类(可后续继续优化) + **优先级 2:数据一致性与核心功能** -4. ⏳ 用户信息修改后同步素材信息 +5. ⏳ 用户信息修改后同步素材信息 - 在 `useUserOperations` 中扩展 -5. ⏳ 模板管理整合到上传和分类 +6. ⏳ 模板管理整合到上传和分类 - 严格检查模板管理面板逻辑 - 将模板系统串起前后端 diff --git a/后续修改.txt b/后续修改.txt index 4c4784b..9b79bec 100644 --- a/后续修改.txt +++ b/后续修改.txt @@ -15,15 +15,17 @@ 13.将外观设置,移动到点击头像弹出的菜单里.同样前面设置一个图标 14.现在模板管理已经设计好了首先就是首页的上传素材面板,其中的素材类型就可以改成素材模板了,列表直接拉取模板管理中的模板名称。素材模板选择后就切换到对应的素材模板界面,该显示什么字段就是什么字段。然后再系统管理中的分类管理,就不使用现有的“店铺分类”和“场景类别”了,而是使用模板管理中的下拉元素中的文字做分类。这样就把前端后端都通过上传模板串起来了。你有什么更好的建议是符合项目宗旨的都可以提出来。模板管理的面板逻辑,你可以再检查一遍,要严格一些,因为这个关系到很多东西。 15.点击素材后的素材预览面板也要统一,包括退出逻辑也是一样要统一 + ✅ 已完成:创建了 MaterialPreviewModal 组件,统一了首页的预览弹窗样式 16.创建统一弹窗,不要用浏览器自带的弹窗,有点丑。 ✅ 已完成:已创建统一弹窗组件 components/ui/Modal.tsx 和 useModal Hook - ⏳ 待完成:替换所有浏览器原生弹窗(confirm/alert) + ✅ 已完成:已替换所有浏览器原生弹窗(confirm/alert/prompt),创建了 ConfirmDialog、AlertDialog、PromptDialog 组件和 DialogProvider 17.优化一下深浅色主题的适配 18.设计语言遵循苹果语言,重点用低饱和度的炫彩元素 19.首页将筛选内的类型分页功能,放到首页,放到首页后分TAG显示,第一个TAG是全部。这个类型分页的tag也是根据上传模板设置,所以之前说模板管理很重要。TAG的样式要设计的好看大气符合整体的风格 20.系统监控中的用户总数,活跃用户,总素材数,本月新用户,本月新素材,增加点击跳转到对应的数据显示 21.系统监控中的素材上传趋势卡片前面增加当月上传趋势 22.首页的UI也更绝统一UI的三层架构统一一下, + ✅ 已完成:主要交互组件已迁移到三层架构(按钮、输入框、筛选弹窗等) 23.统一UI的三层架构内,新增一些通用组件用于不同的需求 diff --git a/更新日志.md b/更新日志.md index c8589e5..e58ecce 100644 --- a/更新日志.md +++ b/更新日志.md @@ -1,3 +1,71 @@ +## 🧾 2025-11-22 · 「统一弹窗系统 & 首页UI优化」版本 🎨 + +本次更新完成了统一弹窗系统的全面替换,统一了素材预览面板,并开始将首页UI迁移到三层架构系统。 + +--- + +### 🎯 统一弹窗系统 + +- ✨ **替换浏览器原生弹窗** + - ✅ 创建了 `ConfirmDialog`、`AlertDialog`、`PromptDialog` 组件 + - ✅ 创建了 `DialogProvider` 和 `useDialog` Hook + - ✅ 替换了所有 `confirm`、`alert`、`prompt` 调用 + - ✅ 涉及文件:所有 admin 组件、首页、分享页面、弹幕组件等 + - ✅ 统一的视觉样式,符合三层架构设计 + - ✅ 支持多种变体(default、danger、success、warning、error) + +- 🔧 **DialogProvider 全局管理** + - 在 `app/providers.tsx` 中添加了 `DialogProvider` + - 提供全局的 `confirm`、`alert`、`prompt` 方法 + - 所有组件可通过 `useDialog()` Hook 使用统一弹窗 + +--- + +### 🖼️ 素材预览面板统一 + +- ✨ **MaterialPreviewModal 组件** + - ✅ 创建了 `components/MaterialPreviewModal.tsx` 统一预览组件 + - ✅ 使用统一的 Modal 组件 + - ✅ 统一退出逻辑(ESC键、点击遮罩、关闭按钮) + - ✅ 使用三层架构样式系统 + - ✅ 确保深浅色模式完美适配 + +- 📦 **应用范围** + - ✅ 首页素材预览已统一使用新组件 + - ✅ 替换了原有的自定义预览弹窗实现 + +--- + +### 🎨 首页UI统一到三层架构 + +- ✨ **主要组件迁移** + - ✅ 迁移了上传按钮样式(使用 `buttonStyles.primary`) + - ✅ 迁移了搜索输入框样式(使用 `inputStyles.base`) + - ✅ 迁移了筛选按钮样式(使用 `buttonStyles.secondary`) + - ✅ 迁移了筛选弹窗中的所有按钮样式 + - 类型分页按钮 + - 使用店铺按钮 + - 场景类别按钮 + - 平台分类按钮 + - 上传月份按钮 + - ✅ 迁移了筛选弹窗容器样式(使用 `cardStyles.base`) + - ✅ 迁移了筛选弹窗关闭按钮样式(使用 `buttonStyles.ghost`) + +- 📝 **后续优化** + - ⏳ 部分文本颜色和标签样式仍使用直接 Tailwind 类(可后续继续优化) + - ⏳ 素材卡片中的标签样式可进一步统一 + +--- + +### 📚 文档更新 + +- 📝 **优化记录更新** + - ✅ 更新了 `优化记录.md` - 记录本次完成的优化 + - ✅ 更新了 `优化计划与优先级.md` - 标记已完成的任务 + - ✅ 更新了 `后续修改.txt` - 更新需求进度 + +--- + ## 🧾 2025-01-XX · 「Admin页面重构 & UI架构优化」版本 🎨 本次更新对Admin管理页面进行了全面重构,建立了统一UI三层架构系统,大幅提升了代码可维护性和用户体验。