feat: 统一弹窗系统 & 首页UI优化
Some checks failed
CI Check / check (push) Has been cancelled

- 替换所有浏览器原生弹窗(confirm/alert/prompt)
  - 创建 ConfirmDialog、AlertDialog、PromptDialog 组件
  - 创建 DialogProvider 和 useDialog Hook
  - 替换所有 admin 组件、首页、分享页面中的原生弹窗

- 素材预览面板统一
  - 创建 MaterialPreviewModal 组件
  - 使用统一弹窗组件,统一退出逻辑

- 首页UI统一到三层架构
  - 迁移主要按钮和输入框样式
  - 迁移筛选弹窗中的所有按钮样式
  - 使用 useThemeStyles() Hook

- 更新文档
  - 更新优化记录、优化计划、后续修改、更新日志
This commit is contained in:
WuYongJie 2025-11-22 16:20:05 +08:00
parent 4ca4bbd767
commit 7dc8d2a528
23 changed files with 899 additions and 123 deletions

View File

@ -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<Category[]>([])
const [scenes, setScenes] = useState<Scene[]>([])
const [platforms, setPlatforms] = useState<Platform[]>([])
@ -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}`, {

View File

@ -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<Set<string>>(new Set())
const [renamingDepartmentId, setRenamingDepartmentId] = useState<string | null>(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)
}

View File

@ -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<Invitation[]>([])
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}`, {

View File

@ -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<Material[]>([])
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}`, {

View File

@ -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 <T,>(input: RequestInfo, init?: RequestInit): Promise<
export default function TemplateManager({ showToast }: { showToast: ToastFn }) {
const styles = useThemeStyles()
const { confirm } = useDialog()
const [templates, setTemplates] = useState<UploadTemplateDto[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(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' })

View File

@ -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),

View File

@ -10,7 +10,9 @@ import Header from '@/components/Header'
import RankingPanel from '@/components/RankingPanel'
import AIChatWidget from '@/components/AIChatWidget'
import ChangePasswordModal from '@/components/ChangePasswordModal'
import MaterialPreviewModal from '@/components/MaterialPreviewModal'
import { useThemeStyles } from '@/lib/ui-styles'
import { useDialog } from '@/components/ui/DialogProvider'
interface Material {
id: string
@ -44,6 +46,7 @@ export default function Home() {
const { data: session, status } = useSession()
const router = useRouter()
const styles = useThemeStyles()
const { alert } = useDialog()
const [materials, setMaterials] = useState<Material[]>([])
const [loading, setLoading] = useState(true)
const [showUploadModal, setShowUploadModal] = useState(false)
@ -208,7 +211,11 @@ export default function Home() {
if (url) {
window.location.href = url
} else {
alert('未配置内网地址,请联系管理员设置 NEXT_PUBLIC_INTRANET_URL')
alert({
title: '提示',
message: '未配置内网地址,请联系管理员设置 NEXT_PUBLIC_INTRANET_URL',
variant: 'warning',
})
}
}
@ -368,10 +375,18 @@ export default function Home() {
if (shareUrl) {
try {
await navigator.clipboard.writeText(shareUrl)
alert('复制成功!')
alert({
title: '成功',
message: '复制成功!',
variant: 'success',
})
setShowShareModal(false)
} catch (error) {
alert('复制失败,请手动复制')
alert({
title: '失败',
message: '复制失败,请手动复制',
variant: 'error',
})
}
}
}
@ -418,15 +433,27 @@ export default function Home() {
fetchMaterials()
} catch (error) {
console.error('下载处理失败:', error)
alert('下载失败,请稍后重试')
alert({
title: '下载失败',
message: '下载失败,请稍后重试',
variant: 'error',
})
}
} else {
try {
const errorText = xhr.responseText
const error = JSON.parse(errorText)
alert(error.error || '下载失败')
alert({
title: '下载失败',
message: error.error || '下载失败',
variant: 'error',
})
} catch {
alert('下载失败')
alert({
title: '下载失败',
message: '下载失败',
variant: 'error',
})
}
}
setDownloadingId(null)
@ -434,7 +461,11 @@ export default function Home() {
})
xhr.addEventListener('error', () => {
alert('下载失败,请稍后重试')
alert({
title: '下载失败',
message: '下载失败,请稍后重试',
variant: 'error',
})
setDownloadingId(null)
setDownloadProgress(0)
})
@ -459,11 +490,19 @@ export default function Home() {
fetchMaterials()
} else {
const error = await response.json()
alert(error.error || '操作失败')
alert({
title: '操作失败',
message: error.error || '操作失败',
variant: 'error',
})
}
} catch (error) {
console.error('Like error:', error)
alert('操作失败,请稍后重试')
alert({
title: '操作失败',
message: '操作失败,请稍后重试',
variant: 'error',
})
}
}
@ -536,7 +575,7 @@ export default function Home() {
<div className="flex justify-end items-center mb-6">
<button
onClick={() => setShowUploadModal(true)}
className="px-6 py-3 rounded-apple text-white font-medium transition-all hover:opacity-90 shadow-apple"
className={`${styles.buttonStyles.primary} ${styles.buttonModifiers.compact}`}
style={{ backgroundColor: themeColor }}
>
<span className="mr-2">+</span>
@ -552,7 +591,7 @@ export default function Home() {
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
placeholder="搜索SPU、店铺、场景类别、作者..."
className="w-full pl-12 pr-4 py-3 rounded-apple border border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-white focus:border-gray-900 dark:focus:border-gray-400 focus:ring-2 focus:ring-gray-200 dark:focus:ring-gray-600 outline-none transition-all"
className={`${styles.inputStyles.base} pl-12 pr-4`}
/>
<svg
className="absolute left-4 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400"
@ -581,7 +620,7 @@ export default function Home() {
{/* 筛选按钮 */}
<button
onClick={() => setShowFilterModal(true)}
className="px-6 py-3 rounded-apple border border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-white hover:bg-gray-50 dark:hover:bg-gray-700 transition-all flex items-center gap-2 relative"
className={`${styles.buttonStyles.secondary} ${styles.buttonModifiers.compact} flex items-center gap-2 relative`}
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
@ -1293,37 +1332,12 @@ export default function Home() {
)}
{/* Image Preview Modal */}
{previewImage && (
<div
className="fixed inset-0 z-[100] bg-black/90 flex items-center justify-center p-4"
onClick={() => setPreviewImage(null)}
>
<div className="relative max-w-7xl max-h-[90vh] w-full h-full flex items-center justify-center">
<button
onClick={() => setPreviewImage(null)}
className="absolute top-4 right-4 z-10 bg-white/10 hover:bg-white/20 text-white rounded-full p-2 transition-all"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<div className="text-center w-full">
<div className="relative w-full max-w-5xl h-[80vh] mx-auto">
<Image
src={previewImage.url}
alt={previewImage.title}
fill
unoptimized
sizes="(max-width: 768px) 100vw, 60vw"
className="object-contain"
onClick={(e) => e.stopPropagation()}
<MaterialPreviewModal
isOpen={!!previewImage}
onClose={() => setPreviewImage(null)}
imageUrl={previewImage?.url || ''}
title={previewImage?.title}
/>
</div>
<p className="text-white mt-4 text-lg font-medium">{previewImage.title}</p>
</div>
</div>
</div>
)}
{/* Share Notification */}
{showShareNotification && (
@ -1342,14 +1356,14 @@ export default function Home() {
onClick={() => setShowFilterModal(false)}
>
<div
className="bg-white dark:bg-gray-800 rounded-xl p-6 max-w-4xl w-full max-h-[90vh] overflow-y-auto shadow-2xl"
className={`${styles.cardStyles.base} max-w-4xl w-full max-h-[90vh] overflow-y-auto`}
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center justify-between mb-6">
<h3 className="text-xl font-semibold text-gray-900 dark:text-white">筛选选项</h3>
<h3 className="text-xl font-semibold text-slate-900 dark:text-white">筛选选项</h3>
<button
onClick={() => setShowFilterModal(false)}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
className={`${styles.buttonStyles.ghost} ${styles.buttonModifiers.icon}`}
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
@ -1360,14 +1374,14 @@ export default function Home() {
<div className="space-y-6">
{/* 类型分页 */}
<div>
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">类型分页</h4>
<h4 className="text-sm font-medium mb-3 text-slate-700 dark:text-slate-300">类型分页</h4>
<div className="flex flex-wrap gap-2">
<button
onClick={() => setSelectedMaterialType('')}
className={`px-4 py-2 rounded-full text-sm font-medium transition-all ${
className={`${styles.buttonModifiers.compactXs} ${styles.buttonModifiers.pill} font-medium transition-all ${
selectedMaterialType === ''
? 'text-white'
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
: `${styles.buttonStyles.secondary}`
}`}
style={selectedMaterialType === '' ? { backgroundColor: themeColor } : {}}
>
@ -1377,10 +1391,10 @@ export default function Home() {
<button
key={type.value}
onClick={() => setSelectedMaterialType(type.value === selectedMaterialType ? '' : type.value)}
className={`px-4 py-2 rounded-full text-sm font-medium transition-all ${
className={`${styles.buttonModifiers.compactXs} ${styles.buttonModifiers.pill} font-medium transition-all ${
selectedMaterialType === type.value
? 'text-white'
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
: `${styles.buttonStyles.secondary}`
}`}
style={selectedMaterialType === type.value ? { backgroundColor: themeColor } : {}}
>
@ -1392,14 +1406,14 @@ export default function Home() {
{/* 使用店铺 */}
<div>
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">使用店铺</h4>
<h4 className="text-sm font-medium text-slate-700 dark:text-slate-300 mb-3">使用店铺</h4>
<div className="flex flex-wrap gap-2">
<button
onClick={() => setSelectedCategory('')}
className={`px-4 py-2 rounded-full text-sm font-medium transition-all ${
className={`${styles.buttonModifiers.compactXs} ${styles.buttonModifiers.pill} font-medium transition-all ${
selectedCategory === ''
? 'text-white'
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
: `${styles.buttonStyles.secondary}`
}`}
style={selectedCategory === '' ? { backgroundColor: themeColor } : {}}
>
@ -1409,10 +1423,10 @@ export default function Home() {
<button
key={category.id}
onClick={() => setSelectedCategory(category.name === selectedCategory ? '' : category.name)}
className={`px-4 py-2 rounded-full text-sm font-medium transition-all ${
className={`${styles.buttonModifiers.compactXs} ${styles.buttonModifiers.pill} font-medium transition-all ${
selectedCategory === category.name
? 'text-white'
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
: `${styles.buttonStyles.secondary}`
}`}
style={selectedCategory === category.name ? { backgroundColor: themeColor } : {}}
>
@ -1424,14 +1438,14 @@ export default function Home() {
{/* 场景类别 */}
<div>
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">场景类别</h4>
<h4 className="text-sm font-medium text-slate-700 dark:text-slate-300 mb-3">场景类别</h4>
<div className="flex flex-wrap gap-2">
<button
onClick={() => setSelectedScene('')}
className={`px-4 py-2 rounded-full text-sm font-medium transition-all ${
className={`${styles.buttonModifiers.compactXs} ${styles.buttonModifiers.pill} font-medium transition-all ${
selectedScene === ''
? 'text-white'
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
: `${styles.buttonStyles.secondary}`
}`}
style={selectedScene === '' ? { backgroundColor: themeColor } : {}}
>
@ -1441,10 +1455,10 @@ export default function Home() {
<button
key={scene.id}
onClick={() => setSelectedScene(scene.name === selectedScene ? '' : scene.name)}
className={`px-4 py-2 rounded-full text-sm font-medium transition-all ${
className={`${styles.buttonModifiers.compactXs} ${styles.buttonModifiers.pill} font-medium transition-all ${
selectedScene === scene.name
? 'text-white'
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
: `${styles.buttonStyles.secondary}`
}`}
style={selectedScene === scene.name ? { backgroundColor: themeColor } : {}}
>
@ -1457,14 +1471,14 @@ export default function Home() {
{/* 平台分类 */}
{platforms.length > 0 && (
<div>
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">平台分类</h4>
<h4 className="text-sm font-medium text-slate-700 dark:text-slate-300 mb-3">平台分类</h4>
<div className="flex flex-wrap gap-2">
<button
onClick={() => setSelectedPlatform('')}
className={`px-4 py-2 rounded-full text-sm font-medium transition-all ${
className={`${styles.buttonModifiers.compactXs} ${styles.buttonModifiers.pill} font-medium transition-all ${
selectedPlatform === ''
? 'text-white'
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
: `${styles.buttonStyles.secondary}`
}`}
style={selectedPlatform === '' ? { backgroundColor: themeColor } : {}}
>
@ -1474,10 +1488,10 @@ export default function Home() {
<button
key={platform.id}
onClick={() => setSelectedPlatform(platform.name === selectedPlatform ? '' : platform.name)}
className={`px-4 py-2 rounded-full text-sm font-medium transition-all ${
className={`${styles.buttonModifiers.compactXs} ${styles.buttonModifiers.pill} font-medium transition-all ${
selectedPlatform === platform.name
? 'text-white'
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
: `${styles.buttonStyles.secondary}`
}`}
style={selectedPlatform === platform.name ? { backgroundColor: themeColor } : {}}
>
@ -1490,14 +1504,14 @@ export default function Home() {
{/* 上传月份 */}
<div>
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">上传月份</h4>
<h4 className="text-sm font-medium text-slate-700 dark:text-slate-300 mb-3">上传月份</h4>
<div className="flex flex-wrap gap-2">
<button
onClick={() => setSelectedMonth('')}
className={`px-4 py-2 rounded-full text-sm font-medium transition-all ${
className={`${styles.buttonModifiers.compactXs} ${styles.buttonModifiers.pill} font-medium transition-all ${
selectedMonth === ''
? 'text-white'
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
: `${styles.buttonStyles.secondary}`
}`}
style={selectedMonth === '' ? { backgroundColor: themeColor } : {}}
>
@ -1507,10 +1521,10 @@ export default function Home() {
<button
key={month.value}
onClick={() => setSelectedMonth(month.value === selectedMonth ? '' : month.value)}
className={`px-4 py-2 rounded-full text-sm font-medium transition-all ${
className={`${styles.buttonModifiers.compactXs} ${styles.buttonModifiers.pill} font-medium transition-all ${
selectedMonth === month.value
? 'text-white'
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
: `${styles.buttonStyles.secondary}`
}`}
style={selectedMonth === month.value ? { backgroundColor: themeColor } : {}}
>

View File

@ -2,12 +2,17 @@
import { SessionProvider } from 'next-auth/react'
import { ReactNode } from 'react'
import { DialogProvider } from '@/components/ui/DialogProvider'
interface ProvidersProps {
children: ReactNode
}
export default function Providers({ children }: ProvidersProps) {
return <SessionProvider>{children}</SessionProvider>
return (
<SessionProvider>
<DialogProvider>{children}</DialogProvider>
</SessionProvider>
)
}

View File

@ -6,6 +6,7 @@ import { useRouter, useParams } from 'next/navigation'
import Image from 'next/image'
import { format } from 'date-fns'
import Header from '@/components/Header'
import { useDialog } from '@/components/ui/DialogProvider'
interface Material {
id: string
@ -34,6 +35,7 @@ interface Material {
export default function SharePage() {
const { data: session, status } = useSession()
const router = useRouter()
const { alert } = useDialog()
const params = useParams()
const materialId = params.id as string
@ -53,12 +55,20 @@ export default function SharePage() {
const data = await response.json()
setMaterial(data)
} else {
alert('素材不存在或已被删除')
alert({
title: '错误',
message: '素材不存在或已被删除',
variant: 'error',
})
router.push('/')
}
} catch (error) {
console.error('Error fetching material:', error)
alert('加载失败')
alert({
title: '加载失败',
message: '加载失败',
variant: 'error',
})
router.push('/')
} finally {
setLoading(false)
@ -182,17 +192,29 @@ export default function SharePage() {
window.URL.revokeObjectURL(url)
} catch (error) {
console.error('下载处理失败:', error)
alert('下载失败,请稍后重试')
alert({
title: '下载失败',
message: '下载失败,请稍后重试',
variant: 'error',
})
}
} else {
alert('下载失败')
alert({
title: '下载失败',
message: '下载失败',
variant: 'error',
})
}
setDownloading(false)
setDownloadProgress(0)
})
xhr.addEventListener('error', () => {
alert('下载失败,请稍后重试')
alert({
title: '下载失败',
message: '下载失败,请稍后重试',
variant: 'error',
})
setDownloading(false)
setDownloadProgress(0)
})
@ -339,7 +361,11 @@ export default function SharePage() {
{/* AI改图(占位,默认隐藏。后续由开关控制显示) */}
{aiEditEnabled && (
<button
onClick={() => alert('AI改图功能待开放')}
onClick={() => alert({
title: '提示',
message: 'AI改图功能待开放',
variant: 'warning',
})}
className="w-full mt-3 py-2.5 bg-purple-600 text-white rounded-xl text-sm font-medium hover:bg-purple-700 transition-colors"
>
AI改图(预留)

View File

@ -1,6 +1,7 @@
'use client'
import { useState } from 'react'
import { useDialog } from '@/components/ui/DialogProvider'
interface ChangePasswordModalProps {
isOpen: boolean
@ -15,6 +16,7 @@ export default function ChangePasswordModal({
isRequired = false,
onSuccess
}: ChangePasswordModalProps) {
const { alert } = useDialog()
const [oldPassword, setOldPassword] = useState('')
const [newPassword, setNewPassword] = useState('')
const [confirmPassword, setConfirmPassword] = useState('')
@ -70,7 +72,11 @@ export default function ChangePasswordModal({
}
// 显示成功消息
alert('密码修改成功!')
alert({
title: '成功',
message: '密码修改成功!',
variant: 'success',
})
} else {
setError(data.error || '修改密码失败')
}

View File

@ -2,6 +2,7 @@
import { useState, useEffect, useRef } from 'react'
import { useSession } from 'next-auth/react'
import { useDialog } from '@/components/ui/DialogProvider'
interface Danmaku {
id: string
@ -12,6 +13,7 @@ interface Danmaku {
export default function DanmakuWidget() {
const { data: session } = useSession()
const { alert } = useDialog()
const [isOpen, setIsOpen] = useState(false)
const [content, setContent] = useState('')
const [danmakus, setDanmakus] = useState<Danmaku[]>([])
@ -89,11 +91,19 @@ export default function DanmakuWidget() {
setTimeout(scrollToBottom, 100)
} else {
const error = await response.json()
alert(error.error || '发送失败')
alert({
title: '发送失败',
message: error.error || '发送失败',
variant: 'error',
})
}
} catch (error) {
console.error('Send danmaku error:', error)
alert('发送失败,请稍后重试')
alert({
title: '发送失败',
message: '发送失败,请稍后重试',
variant: 'error',
})
} finally {
setSending(false)
}

View File

@ -0,0 +1,60 @@
'use client'
import { useRef } from 'react'
import Image from 'next/image'
import Modal from './ui/Modal'
interface MaterialPreviewModalProps {
isOpen: boolean
onClose: () => void
imageUrl: string
title?: string
description?: string
}
export default function MaterialPreviewModal({
isOpen,
onClose,
imageUrl,
title,
description,
}: MaterialPreviewModalProps) {
const imageRef = useRef<HTMLDivElement>(null)
return (
<Modal
isOpen={isOpen}
onClose={onClose}
size="fullscreen"
closeOnOverlayClick={true}
closeOnEscape={true}
showCloseButton={true}
className="bg-black/90"
>
<div className="relative w-full h-full flex items-center justify-center p-4 -m-6">
<div className="text-center w-full max-w-7xl">
<div
ref={imageRef}
className="relative w-full max-w-5xl h-[80vh] mx-auto mb-4"
>
<Image
src={imageUrl}
alt={title || '素材预览'}
fill
unoptimized
sizes="(max-width: 768px) 100vw, 60vw"
className="object-contain"
/>
</div>
{title && (
<p className="text-white text-lg font-medium">{title}</p>
)}
{description && (
<p className="text-white/80 text-sm mt-2">{description}</p>
)}
</div>
</div>
</Modal>
)
}

View File

@ -0,0 +1,65 @@
'use client'
import { ReactNode } from 'react'
import Modal from './Modal'
import { useThemeStyles } from '@/lib/ui-styles'
interface AlertDialogProps {
isOpen: boolean
onClose: () => void
title: string
message: string | ReactNode
buttonText?: string
variant?: 'default' | 'success' | 'warning' | 'error'
}
export default function AlertDialog({
isOpen,
onClose,
title,
message,
buttonText = '确定',
variant = 'default',
}: AlertDialogProps) {
const styles = useThemeStyles()
const getButtonStyle = () => {
switch (variant) {
case 'success':
return styles.buttonStyles.success
case 'warning':
return styles.buttonStyles.secondary
case 'error':
return styles.buttonStyles.danger
default:
return styles.buttonStyles.primary
}
}
return (
<Modal
isOpen={isOpen}
onClose={onClose}
title={title}
size="small"
footer={
<button
type="button"
onClick={onClose}
className={`${getButtonStyle()} ${styles.buttonModifiers.compact}`}
>
{buttonText}
</button>
}
>
<div className="py-2">
{typeof message === 'string' ? (
<p className="text-slate-700 dark:text-slate-200">{message}</p>
) : (
message
)}
</div>
</Modal>
)
}

View File

@ -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 (
<Modal
isOpen={isOpen}
onClose={onClose}
title={title}
size="small"
closeOnOverlayClick={!loading}
closeOnEscape={!loading}
footer={
<>
<button
type="button"
onClick={onClose}
disabled={loading}
className={`${styles.buttonStyles.secondary} ${styles.buttonModifiers.compact}`}
>
{cancelText}
</button>
<button
type="button"
onClick={handleConfirm}
disabled={loading}
className={`${
variant === 'danger' ? styles.buttonStyles.danger : styles.buttonStyles.primary
} ${styles.buttonModifiers.compact}`}
>
{loading ? '处理中...' : confirmText}
</button>
</>
}
>
<div className="py-2">
{typeof message === 'string' ? (
<p className="text-slate-700 dark:text-slate-200">{message}</p>
) : (
message
)}
</div>
</Modal>
)
}

View File

@ -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<boolean>
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<string | null>
}
const DialogContext = createContext<DialogContextType | null>(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<boolean> => {
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<string | null> => {
return promptHook.prompt(options)
}
return (
<DialogContext.Provider value={{ confirm, alert, prompt }}>
{children}
<ConfirmDialog
isOpen={confirmHook.isOpen}
onClose={confirmHook.handleCancel}
onConfirm={confirmHook.handleConfirm}
title={confirmHook.options.title || '确认'}
message={confirmHook.options.message}
confirmText={confirmHook.options.confirmText}
cancelText={confirmHook.options.cancelText}
variant={confirmHook.options.variant}
/>
<AlertDialog
isOpen={alertHook.isOpen}
onClose={alertHook.close}
title={alertHook.options.title || '提示'}
message={alertHook.options.message}
buttonText={alertHook.options.buttonText}
variant={alertHook.options.variant}
/>
<PromptDialog
isOpen={promptHook.isOpen}
onClose={promptHook.handleCancel}
onConfirm={promptHook.handleConfirm}
title={promptHook.options.title || '输入'}
message={promptHook.options.message}
defaultValue={promptHook.options.defaultValue}
placeholder={promptHook.options.placeholder}
confirmText={promptHook.options.confirmText}
cancelText={promptHook.options.cancelText}
/>
</DialogContext.Provider>
)
}

View File

@ -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<HTMLInputElement>(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 (
<Modal
isOpen={isOpen}
onClose={onClose}
title={title}
size="small"
footer={
<>
<button
type="button"
onClick={onClose}
className={`${styles.buttonStyles.secondary} ${styles.buttonModifiers.compact}`}
>
{cancelText}
</button>
<button
type="button"
onClick={handleConfirm}
disabled={!value.trim()}
className={`${styles.buttonStyles.primary} ${styles.buttonModifiers.compact}`}
>
{confirmText}
</button>
</>
}
>
<div className="py-2 space-y-3">
<p className="text-slate-700 dark:text-slate-200 whitespace-pre-line">{message}</p>
<input
ref={inputRef}
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={placeholder}
className={styles.inputStyles.base}
/>
</div>
</Modal>
)
}

44
hooks/useAlert.ts Normal file
View File

@ -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<AlertOptions>({ 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,
}
}

97
hooks/useConfirm.ts Normal file
View File

@ -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<ConfirmOptions>({ message: '' })
const [resolveCallback, setResolveCallback] = useState<((value: boolean) => void) | null>(null)
const confirm = useCallback((opts: ConfirmOptions): Promise<boolean> => {
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<PromptOptions>({ message: '' })
const [resolveCallback, setResolveCallback] = useState<((value: string | null) => void) | null>(null)
const prompt = useCallback((opts: PromptOptions): Promise<string | null> => {
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,
}
}

View File

@ -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)

View File

@ -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. ⏳ 模板管理整合到上传和分类
- 严格检查模板管理面板逻辑
- 将模板系统串起前后端

View File

@ -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的三层架构内,新增一些通用组件用于不同的需求

View File

@ -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三层架构系统,大幅提升了代码可维护性和用户体验。