fix(shop): persist settings through backend

This commit is contained in:
zetaloop
2026-04-25 14:49:36 +08:00
parent 358bfc7ac9
commit fc0b754056
4 changed files with 226 additions and 89 deletions
+81 -42
View File
@@ -7,50 +7,99 @@ import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
import { Separator } from "@/components/ui/separator" import { Separator } from "@/components/ui/separator"
import { Textarea } from "@/components/ui/textarea" import { Textarea } from "@/components/ui/textarea"
import { resolveOwnerShop } from "@/lib/domain/resolve-current-shop" import { addShopAnnouncement, deleteShopAnnouncement, updateShop } from "@/lib/api"
import { toApiError } from "@/lib/errors"
import { useMyShop } from "@/lib/hooks/use-my-shop"
import { notifyInfo, notifySuccess } from "@/lib/toast"
import type { Shop } from "@/lib/types" import type { Shop } from "@/lib/types"
import { useAuthStore } from "@/store/auth"
import { useShopStore } from "@/store/shops"
import { DollarSign, Edit, ExternalLink, ListOrdered, Star, Users } from "lucide-react" import { DollarSign, Edit, ExternalLink, ListOrdered, Star, Users } from "lucide-react"
import Link from "next/link" import Link from "next/link"
import { useState } from "react" import { useState } from "react"
export default function ShopManagementPage() { export default function ShopManagementPage() {
const userId = useAuthStore((state) => state.user?.id) const { shop, setShop, loading, error, refreshShop } = useMyShop()
const shops = useShopStore((state) => state.shops)
const shop = resolveOwnerShop(userId, shops) if (loading) {
const updateShop = useShopStore((state) => state.updateShop) return <div className="text-sm text-muted-foreground">...</div>
const updateAnnouncement = useShopStore((state) => state.updateAnnouncement) }
const addAnnouncement = useShopStore((state) => state.addAnnouncement)
if (error) {
return <div className="text-sm text-muted-foreground">{error}</div>
}
if (!shop) { if (!shop) {
return <div className="text-sm text-muted-foreground"></div> return <div className="text-sm text-muted-foreground"></div>
} }
return ( return (
<ShopManagementContent <ShopManagementContent key={shop.id} shop={shop} setShop={setShop} refreshShop={refreshShop} />
key={shop.id}
shop={shop}
updateShop={updateShop}
updateAnnouncement={updateAnnouncement}
addAnnouncement={addAnnouncement}
/>
) )
} }
function ShopManagementContent({ function ShopManagementContent({
shop, shop,
updateShop, setShop,
updateAnnouncement, refreshShop,
addAnnouncement,
}: { }: {
shop: Shop shop: Shop
updateShop: (shopId: string, patch: Partial<Omit<Shop, "id" | "owner">>) => void setShop: (shop: Shop | null) => void
updateAnnouncement: (shopId: string, index: number, announcement: string) => void refreshShop: () => Promise<Shop | null>
addAnnouncement: (shopId: string, announcement: string) => void
}) { }) {
const [name, setName] = useState(shop.name) const [name, setName] = useState(shop.name)
const [description, setDescription] = useState(shop.description) const [description, setDescription] = useState(shop.description)
const [saving, setSaving] = useState(false)
const handleSave = async () => {
setSaving(true)
try {
const nextShop = await updateShop(shop.id, {
name,
description,
commissionType: shop.commissionType,
commissionValue: shop.commissionValue,
allowMultiShop: shop.allowMultiShop,
allowIndependentOrders: shop.allowIndependentOrders,
dispatchMode: shop.dispatchMode,
})
setShop(nextShop)
notifySuccess("店铺信息已保存")
} catch (error) {
notifyInfo(toApiError(error).msg)
} finally {
setSaving(false)
}
}
const handleAddAnnouncement = async () => {
const next = window.prompt("", "")
if (next === null) return
const value = next.trim()
if (!value) return
setSaving(true)
try {
await addShopAnnouncement(shop.id, value)
await refreshShop()
notifySuccess("公告已添加")
} catch (error) {
notifyInfo(toApiError(error).msg)
} finally {
setSaving(false)
}
}
const handleDeleteAnnouncement = async (index: number) => {
setSaving(true)
try {
await deleteShopAnnouncement(shop.id, index)
await refreshShop()
notifySuccess("公告已删除")
} catch (error) {
notifyInfo(toApiError(error).msg)
} finally {
setSaving(false)
}
}
return ( return (
<div className="container mx-auto max-w-6xl px-4 py-8 space-y-8"> <div className="container mx-auto max-w-6xl px-4 py-8 space-y-8">
@@ -126,12 +175,10 @@ function ShopManagementContent({
/> />
</div> </div>
<Button <Button
onClick={() => onClick={() => {
updateShop(shop.id, { void handleSave()
name, }}
description, disabled={saving}
})
}
> >
<Edit className="mr-1 h-4 w-4" /> <Edit className="mr-1 h-4 w-4" />
@@ -144,7 +191,7 @@ function ShopManagementContent({
<CardTitle className="text-base"></CardTitle> <CardTitle className="text-base"></CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-3"> <CardContent className="space-y-3">
{shop.announcements.map((announcement) => ( {shop.announcements.map((announcement, index) => (
<div <div
key={announcement} key={announcement}
className="flex items-center justify-between rounded-md border p-3" className="flex items-center justify-between rounded-md border p-3"
@@ -154,16 +201,11 @@ function ShopManagementContent({
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => { onClick={() => {
const next = window.prompt("", announcement) void handleDeleteAnnouncement(index)
if (next === null) return
const value = next.trim()
if (!value) return
const index = shop.announcements.indexOf(announcement)
if (index < 0) return
updateAnnouncement(shop.id, index, value)
}} }}
disabled={saving}
> >
</Button> </Button>
</div> </div>
))} ))}
@@ -172,12 +214,9 @@ function ShopManagementContent({
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => { onClick={() => {
const next = window.prompt("", "") void handleAddAnnouncement()
if (next === null) return
const value = next.trim()
if (!value) return
addAnnouncement(shop.id, value)
}} }}
disabled={saving}
> >
</Button> </Button>
+41 -24
View File
@@ -12,54 +12,71 @@ import {
SelectValue, SelectValue,
} from "@/components/ui/select" } from "@/components/ui/select"
import { Switch } from "@/components/ui/switch" import { Switch } from "@/components/ui/switch"
import { resolveOwnerShop } from "@/lib/domain/resolve-current-shop" import { updateShop } from "@/lib/api"
import { toApiError } from "@/lib/errors"
import { useMyShop } from "@/lib/hooks/use-my-shop"
import { notifyInfo, notifySuccess } from "@/lib/toast"
import type { Shop } from "@/lib/types" import type { Shop } from "@/lib/types"
import { useAuthStore } from "@/store/auth"
import { useShopStore } from "@/store/shops"
import { Save } from "lucide-react" import { Save } from "lucide-react"
import { useState } from "react" import { useState } from "react"
export default function ShopRulesPage() { export default function ShopRulesPage() {
const userId = useAuthStore((state) => state.user?.id) const { shop, setShop, loading, error } = useMyShop()
const shops = useShopStore((state) => state.shops)
const shop = resolveOwnerShop(userId, shops) if (loading) {
const updateShop = useShopStore((state) => state.updateShop) return <div className="text-sm text-muted-foreground">...</div>
}
if (error) {
return <div className="text-sm text-muted-foreground">{error}</div>
}
if (!shop) { if (!shop) {
return <div className="text-sm text-muted-foreground"></div> return <div className="text-sm text-muted-foreground"></div>
} }
return <ShopRulesForm key={shop.id} shop={shop} updateShop={updateShop} /> return <ShopRulesForm key={shop.id} shop={shop} setShop={setShop} />
} }
function ShopRulesForm({ function ShopRulesForm({ shop, setShop }: { shop: Shop; setShop: (shop: Shop | null) => void }) {
shop,
updateShop,
}: {
shop: Shop
updateShop: (shopId: string, patch: Partial<Omit<Shop, "id" | "owner">>) => void
}) {
const [commissionType, setCommissionType] = useState<Shop["commissionType"]>(shop.commissionType) const [commissionType, setCommissionType] = useState<Shop["commissionType"]>(shop.commissionType)
const [commissionValue, setCommissionValue] = useState(shop.commissionValue) const [commissionValue, setCommissionValue] = useState(shop.commissionValue)
const [allowMultiShop, setAllowMultiShop] = useState(shop.allowMultiShop) const [allowMultiShop, setAllowMultiShop] = useState(shop.allowMultiShop)
const [allowIndependentOrders, setAllowIndependentOrders] = useState(shop.allowIndependentOrders) const [allowIndependentOrders, setAllowIndependentOrders] = useState(shop.allowIndependentOrders)
const [dispatchMode, setDispatchMode] = useState<Shop["dispatchMode"]>(shop.dispatchMode) const [dispatchMode, setDispatchMode] = useState<Shop["dispatchMode"]>(shop.dispatchMode)
const [saving, setSaving] = useState(false)
const handleSave = () => { const handleSave = async () => {
updateShop(shop.id, { setSaving(true)
commissionType, try {
commissionValue, const nextShop = await updateShop(shop.id, {
allowMultiShop, name: shop.name,
allowIndependentOrders, description: shop.description,
dispatchMode, commissionType,
}) commissionValue,
allowMultiShop,
allowIndependentOrders,
dispatchMode,
})
setShop(nextShop)
notifySuccess("规则已保存")
} catch (error) {
notifyInfo(toApiError(error).msg)
} finally {
setSaving(false)
}
} }
return ( return (
<div className="container mx-auto max-w-4xl px-4 py-8 space-y-8"> <div className="container mx-auto max-w-4xl px-4 py-8 space-y-8">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h1 className="text-2xl font-bold"></h1> <h1 className="text-2xl font-bold"></h1>
<Button onClick={handleSave}> <Button
onClick={() => {
void handleSave()
}}
disabled={saving}
>
<Save className="mr-2 h-4 w-4" /> <Save className="mr-2 h-4 w-4" />
</Button> </Button>
@@ -3,10 +3,12 @@
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Switch } from "@/components/ui/switch" import { Switch } from "@/components/ui/switch"
import { resolveOwnerShop } from "@/lib/domain/resolve-current-shop" import { updateShopTemplate } from "@/lib/api"
import { getShopSections } from "@/lib/domain/shop-template"
import { toApiError } from "@/lib/errors"
import { useMyShop } from "@/lib/hooks/use-my-shop"
import { notifyInfo } from "@/lib/toast"
import type { Shop, ShopSection } from "@/lib/types" import type { Shop, ShopSection } from "@/lib/types"
import { useAuthStore } from "@/store/auth"
import { useShopStore } from "@/store/shops"
import { Eye, EyeOff, GripVertical } from "lucide-react" import { Eye, EyeOff, GripVertical } from "lucide-react"
import Link from "next/link" import Link from "next/link"
import { type DragEvent, useEffect, useState } from "react" import { type DragEvent, useEffect, useState } from "react"
@@ -30,37 +32,35 @@ const sectionDescriptions: Record<ShopSection["type"], string> = {
} }
export default function ShopTemplatesPage() { export default function ShopTemplatesPage() {
const userId = useAuthStore((state) => state.user?.id) const { shop, setShop, loading, error } = useMyShop()
const shops = useShopStore((state) => state.shops)
const shop = resolveOwnerShop(userId, shops) if (loading) {
const updateTemplateSections = useShopStore((state) => state.updateTemplateSections) return <div className="text-sm text-muted-foreground">...</div>
}
if (error) {
return <div className="text-sm text-muted-foreground">{error}</div>
}
if (!shop) { if (!shop) {
return <div className="text-sm text-muted-foreground"></div> return <div className="text-sm text-muted-foreground"></div>
} }
return ( return <ShopTemplatesEditor key={shop.id} shop={shop} setShop={setShop} />
<ShopTemplatesEditor
key={shop.id}
shop={shop}
updateTemplateSections={updateTemplateSections}
/>
)
} }
function ShopTemplatesEditor({ function ShopTemplatesEditor({
shop, shop,
updateTemplateSections, setShop,
}: { }: {
shop: Shop shop: Shop
updateTemplateSections: (shopId: string, sections: ShopSection[]) => void setShop: (shop: Shop | null) => void
}) { }) {
const [sections, setSections] = useState<ShopSection[]>( const [sections, setSections] = useState<ShopSection[]>(getShopSections(shop))
[...shop.templateConfig.sections].sort((a, b) => a.order - b.order),
)
const [dragIndex, setDragIndex] = useState<number | null>(null) const [dragIndex, setDragIndex] = useState<number | null>(null)
const [dropIndex, setDropIndex] = useState<number | null>(null) const [dropIndex, setDropIndex] = useState<number | null>(null)
const [showSavedToast, setShowSavedToast] = useState(false) const [showSavedToast, setShowSavedToast] = useState(false)
const [saving, setSaving] = useState(false)
useEffect(() => { useEffect(() => {
if (!showSavedToast) { if (!showSavedToast) {
@@ -116,9 +116,21 @@ function ShopTemplatesEditor({
setDropIndex(null) setDropIndex(null)
} }
const handleSaveTemplate = () => { const handleSaveTemplate = async () => {
updateTemplateSections(shop.id, sections) setSaving(true)
setShowSavedToast(true) try {
const templateConfig = {
...shop.templateConfig,
sections,
}
await updateShopTemplate(shop.id, templateConfig)
setShop({ ...shop, templateConfig })
setShowSavedToast(true)
} catch (error) {
notifyInfo(toApiError(error).msg)
} finally {
setSaving(false)
}
} }
return ( return (
@@ -137,7 +149,14 @@ function ShopTemplatesEditor({
<Button variant="outline" asChild> <Button variant="outline" asChild>
<Link href={`/shop/${shop.id}`}></Link> <Link href={`/shop/${shop.id}`}></Link>
</Button> </Button>
<Button onClick={handleSaveTemplate}></Button> <Button
onClick={() => {
void handleSaveTemplate()
}}
disabled={saving}
>
</Button>
</div> </div>
</div> </div>
+62
View File
@@ -0,0 +1,62 @@
"use client"
import { getMyShop } from "@/lib/api/shops"
import { toApiError } from "@/lib/errors"
import type { Shop } from "@/lib/types"
import { useCallback, useEffect, useState } from "react"
export function useMyShop() {
const [shop, setShop] = useState<Shop | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const refreshShop = useCallback(async () => {
setLoading(true)
setError(null)
try {
const nextShop = (await getMyShop()) ?? null
setShop(nextShop)
return nextShop
} catch (error) {
setShop(null)
setError(
error instanceof Error && error.message === "UNAUTHORIZED"
? "请先登录"
: toApiError(error).msg,
)
return null
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
let cancelled = false
getMyShop()
.then((nextShop) => {
if (cancelled) return
setShop(nextShop ?? null)
})
.catch((error) => {
if (cancelled) return
setShop(null)
setError(
error instanceof Error && error.message === "UNAUTHORIZED"
? "请先登录"
: toApiError(error).msg,
)
})
.finally(() => {
if (cancelled) return
setLoading(false)
})
return () => {
cancelled = true
}
}, [])
return { shop, setShop, loading, error, refreshShop }
}