519fb92c34
Turn on react-hooks/set-state-in-effect and react-hooks/incompatible-library, then remove effect-driven local state sync patterns across affected pages. Keep behavior stable by deriving values from source state, remounting tab state by role key, and replacing useForm watch with useWatch.
174 lines
5.8 KiB
TypeScript
174 lines
5.8 KiB
TypeScript
"use client"
|
||
|
||
import { Eye, EyeOff, GripVertical } from "lucide-react"
|
||
import Link from "next/link"
|
||
import { type DragEvent, useEffect, useState } from "react"
|
||
import { Button } from "@/components/ui/button"
|
||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||
import { Switch } from "@/components/ui/switch"
|
||
import type { ShopSection } from "@/lib/types"
|
||
import { useShopStore } from "@/store/shops"
|
||
|
||
const sectionLabels: Record<ShopSection["type"], string> = {
|
||
banner: "横幅图片",
|
||
intro: "店铺简介",
|
||
services: "服务列表",
|
||
players: "打手展示",
|
||
announcements: "公告栏",
|
||
reviews: "评价展示",
|
||
}
|
||
|
||
const sectionDescriptions: Record<ShopSection["type"], string> = {
|
||
banner: "店铺顶部的横幅图片",
|
||
intro: "店铺的文字介绍",
|
||
services: "展示店铺提供的服务",
|
||
players: "展示签约打手列表",
|
||
announcements: "店铺公告和活动信息",
|
||
reviews: "展示用户评价",
|
||
}
|
||
|
||
export default function ShopTemplatesPage() {
|
||
const shop = useShopStore((state) => state.shops[0])
|
||
const updateTemplateSections = useShopStore((state) => state.updateTemplateSections)
|
||
const [sections, setSections] = useState<ShopSection[]>(
|
||
[...shop.templateConfig.sections].sort((a, b) => a.order - b.order),
|
||
)
|
||
const [dragIndex, setDragIndex] = useState<number | null>(null)
|
||
const [dropIndex, setDropIndex] = useState<number | null>(null)
|
||
const [showSavedToast, setShowSavedToast] = useState(false)
|
||
|
||
useEffect(() => {
|
||
if (!showSavedToast) {
|
||
return
|
||
}
|
||
|
||
const timer = window.setTimeout(() => {
|
||
setShowSavedToast(false)
|
||
}, 2000)
|
||
|
||
return () => {
|
||
window.clearTimeout(timer)
|
||
}
|
||
}, [showSavedToast])
|
||
|
||
const toggleSection = (type: ShopSection["type"]) => {
|
||
setSections((prev) => prev.map((s) => (s.type === type ? { ...s, enabled: !s.enabled } : s)))
|
||
}
|
||
|
||
const handleDragStart = (index: number) => {
|
||
setDragIndex(index)
|
||
}
|
||
|
||
const handleDragOver = (event: DragEvent<HTMLElement>, index: number) => {
|
||
event.preventDefault()
|
||
if (dragIndex === null || dragIndex === index) {
|
||
setDropIndex(null)
|
||
return
|
||
}
|
||
setDropIndex(index)
|
||
}
|
||
|
||
const handleDrop = (event: DragEvent<HTMLElement>, index: number) => {
|
||
event.preventDefault()
|
||
if (dragIndex === null || dragIndex === index) {
|
||
setDropIndex(null)
|
||
return
|
||
}
|
||
|
||
setSections((prev) => {
|
||
const next = [...prev]
|
||
const [draggedSection] = next.splice(dragIndex, 1)
|
||
next.splice(index, 0, draggedSection)
|
||
return next.map((section, order) => ({ ...section, order }))
|
||
})
|
||
|
||
setDragIndex(null)
|
||
setDropIndex(null)
|
||
}
|
||
|
||
const handleDragEnd = () => {
|
||
setDragIndex(null)
|
||
setDropIndex(null)
|
||
}
|
||
|
||
const handleSaveTemplate = () => {
|
||
updateTemplateSections(shop.id, sections)
|
||
setShowSavedToast(true)
|
||
}
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
{showSavedToast ? (
|
||
<div className="fixed right-6 top-6 z-50 rounded-md border bg-background px-4 py-2 text-sm shadow-md">
|
||
模板已保存
|
||
</div>
|
||
) : null}
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<h1 className="text-2xl font-bold">模板编辑</h1>
|
||
<p className="text-sm text-muted-foreground mt-1">自定义店铺主页的展示内容和顺序</p>
|
||
</div>
|
||
<div className="flex gap-2">
|
||
<Button variant="outline" asChild>
|
||
<Link href={`/shop/${shop.id}`}>预览</Link>
|
||
</Button>
|
||
<Button onClick={handleSaveTemplate}>保存模板</Button>
|
||
</div>
|
||
</div>
|
||
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="text-base">页面组件</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="space-y-2">
|
||
{sections.map((section, index) => (
|
||
<div
|
||
key={section.type}
|
||
className={`flex items-center gap-3 rounded-lg border p-3 transition-colors ${dragIndex === index ? "opacity-50" : "opacity-100"} ${dropIndex === index ? "border-t-2 border-b-2 border-t-primary border-b-primary" : ""}`}
|
||
>
|
||
<button
|
||
type="button"
|
||
draggable
|
||
onDragStart={() => handleDragStart(index)}
|
||
onDragOver={(event) => handleDragOver(event, index)}
|
||
onDrop={(event) => handleDrop(event, index)}
|
||
onDragEnd={handleDragEnd}
|
||
className="flex flex-1 min-w-0 items-center gap-3 text-left"
|
||
>
|
||
<GripVertical className="h-4 w-4 text-muted-foreground cursor-grab shrink-0" />
|
||
<div className="flex-1 min-w-0">
|
||
<div className="flex items-center gap-2">
|
||
<span className="text-sm font-medium">{sectionLabels[section.type]}</span>
|
||
{section.enabled ? (
|
||
<Eye className="h-3.5 w-3.5 text-muted-foreground" />
|
||
) : (
|
||
<EyeOff className="h-3.5 w-3.5 text-muted-foreground" />
|
||
)}
|
||
</div>
|
||
<p className="text-xs text-muted-foreground">
|
||
{sectionDescriptions[section.type]}
|
||
</p>
|
||
</div>
|
||
</button>
|
||
<Switch
|
||
checked={section.enabled}
|
||
onCheckedChange={() => toggleSection(section.type)}
|
||
/>
|
||
</div>
|
||
))}
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="text-base">提示</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="text-sm text-muted-foreground space-y-1">
|
||
<p>关闭开关可隐藏对应组件,不会删除已有内容</p>
|
||
<p>保存后立即生效,访客将看到最新的店铺主页</p>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
)
|
||
}
|