- 替换所有浏览器原生弹窗(confirm/alert/prompt) - 创建 ConfirmDialog、AlertDialog、PromptDialog 组件 - 创建 DialogProvider 和 useDialog Hook - 替换所有 admin 组件、首页、分享页面中的原生弹窗 - 素材预览面板统一 - 创建 MaterialPreviewModal 组件 - 使用统一弹窗组件,统一退出逻辑 - 首页UI统一到三层架构 - 迁移主要按钮和输入框样式 - 迁移筛选弹窗中的所有按钮样式 - 使用 useThemeStyles() Hook - 更新文档 - 更新优化记录、优化计划、后续修改、更新日志
This commit is contained in:
parent
4ca4bbd767
commit
7dc8d2a528
@ -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}`, {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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}`, {
|
||||
|
||||
@ -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}`, {
|
||||
|
||||
@ -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' })
|
||||
|
||||
@ -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),
|
||||
|
||||
154
app/page.tsx
154
app/page.tsx
@ -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 } : {}}
|
||||
>
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -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改图(预留)
|
||||
|
||||
@ -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 || '修改密码失败')
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
60
components/MaterialPreviewModal.tsx
Normal file
60
components/MaterialPreviewModal.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
||||
65
components/ui/AlertDialog.tsx
Normal file
65
components/ui/AlertDialog.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
||||
79
components/ui/ConfirmDialog.tsx
Normal file
79
components/ui/ConfirmDialog.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
||||
118
components/ui/DialogProvider.tsx
Normal file
118
components/ui/DialogProvider.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
||||
101
components/ui/PromptDialog.tsx
Normal file
101
components/ui/PromptDialog.tsx
Normal 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
44
hooks/useAlert.ts
Normal 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
97
hooks/useConfirm.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
|
||||
33
优化计划与优先级.md
33
优化计划与优先级.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)
|
||||
|
||||
25
优化记录.md
25
优化记录.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. ⏳ 模板管理整合到上传和分类
|
||||
- 严格检查模板管理面板逻辑
|
||||
- 将模板系统串起前后端
|
||||
|
||||
|
||||
4
后续修改.txt
4
后续修改.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的三层架构内,新增一些通用组件用于不同的需求
|
||||
|
||||
|
||||
|
||||
68
更新日志.md
68
更新日志.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三层架构系统,大幅提升了代码可维护性和用户体验。
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user