From e5592043478ad63c789a9de75b55d343af6f4f01 Mon Sep 17 00:00:00 2001 From: zetaloop Date: Sat, 25 Apr 2026 14:31:04 +0800 Subject: [PATCH] feat(services): manage services through backend --- .../dashboard/services/new/page.tsx | 73 ++++++-- app/(dashboard)/dashboard/services/page.tsx | 170 ++++++++++++------ lib/api/index.ts | 9 +- lib/api/services.ts | 55 +++++- 4 files changed, 236 insertions(+), 71 deletions(-) diff --git a/app/(dashboard)/dashboard/services/new/page.tsx b/app/(dashboard)/dashboard/services/new/page.tsx index 012272e..0074449 100644 --- a/app/(dashboard)/dashboard/services/new/page.tsx +++ b/app/(dashboard)/dashboard/services/new/page.tsx @@ -12,13 +12,20 @@ import { SelectValue, } from "@/components/ui/select" import { Textarea } from "@/components/ui/textarea" -import { getGameById, listGames } from "@/lib/api" +import { + createPlayerService, + getGameById, + getServiceById, + listGames, + listPlayers, + updatePlayerService, +} from "@/lib/api" import { resolveOwnerShop } from "@/lib/domain/resolve-current-shop" +import { toApiError } from "@/lib/errors" import { GameIcon } from "@/lib/game-icons" +import { notifyInfo, notifySuccess } from "@/lib/toast" import type { Game, PlayerService } from "@/lib/types" import { useAuthStore } from "@/store/auth" -import { usePlayerStore } from "@/store/players" -import { useServiceStore } from "@/store/services" import { useShopStore } from "@/store/shops" import { standardSchemaResolver } from "@hookform/resolvers/standard-schema" import { ArrowLeft } from "lucide-react" @@ -45,15 +52,14 @@ export default function NewServicePage() { const userId = useAuthStore((state) => state.user?.id) const currentRole = useAuthStore((state) => state.currentRole) const shops = useShopStore((state) => state.shops) - const players = usePlayerStore((state) => state.players) - const services = useServiceStore((state) => state.services) - const createService = useServiceStore((state) => state.createService) - const updateService = useServiceStore((state) => state.updateService) + const [players, setPlayers] = useState>>([]) + const [editingService, setEditingService] = useState(undefined) + const [loadingService, setLoadingService] = useState(Boolean(serviceId)) const ownerShop = resolveOwnerShop(userId, shops) const scopedPlayerIds = currentRole === "player" ? userId - ? [userId] + ? players.filter((player) => player.user.id === userId).map((player) => player.id) : [] : currentRole === "owner" ? ownerShop @@ -61,9 +67,6 @@ export default function NewServicePage() { : [] : [] const scopedPlayerIdSet = new Set(scopedPlayerIds) - const editingService = services.find( - (service) => service.id === serviceId && scopedPlayerIdSet.has(service.playerId), - ) const targetPlayerId = editingService?.playerId ?? scopedPlayerIds[0] const { register, @@ -84,6 +87,42 @@ export default function NewServicePage() { }, }) + useEffect(() => { + let cancelled = false + + listPlayers() + .then((items) => { + if (!cancelled) setPlayers(items) + }) + .catch((error) => { + if (!cancelled) notifyInfo(toApiError(error).msg) + }) + + return () => { + cancelled = true + } + }, []) + + useEffect(() => { + if (!serviceId) return + let cancelled = false + + getServiceById(serviceId) + .then((service) => { + if (!cancelled) setEditingService(service) + }) + .catch((error) => { + if (!cancelled) notifyInfo(toApiError(error).msg) + }) + .finally(() => { + if (!cancelled) setLoadingService(false) + }) + + return () => { + cancelled = true + } + }, [serviceId]) + useEffect(() => { if (!editingService) return setValue("gameId", editingService.gameId) @@ -117,7 +156,11 @@ export default function NewServicePage() { } }, []) - if (serviceId && !editingService) { + if (loadingService) { + return
加载中...
+ } + + if (serviceId && (!editingService || !scopedPlayerIdSet.has(editingService.playerId))) { return
服务不存在或当前身份不可编辑
} @@ -145,9 +188,11 @@ export default function NewServicePage() { } if (editingService) { - updateService(editingService.id, payload) + await updatePlayerService(editingService.id, payload) + notifySuccess("服务已保存") } else { - createService(payload) + await createPlayerService(payload) + notifySuccess("服务已发布") } router.push("/dashboard/services") diff --git a/app/(dashboard)/dashboard/services/page.tsx b/app/(dashboard)/dashboard/services/page.tsx index a7a9a88..b4619db 100644 --- a/app/(dashboard)/dashboard/services/page.tsx +++ b/app/(dashboard)/dashboard/services/page.tsx @@ -11,37 +11,90 @@ import { TableHeader, TableRow, } from "@/components/ui/table" +import { deletePlayerService, listPlayers, listServices } from "@/lib/api" import { resolveOwnerShop } from "@/lib/domain/resolve-current-shop" +import { toApiError } from "@/lib/errors" +import { notifyInfo, notifySuccess } from "@/lib/toast" +import type { Player, PlayerService } from "@/lib/types" import { useAuthStore } from "@/store/auth" -import { usePlayerStore } from "@/store/players" -import { useServiceStore } from "@/store/services" import { useShopStore } from "@/store/shops" import { Edit, Plus, Trash2 } from "lucide-react" import Link from "next/link" +import { useCallback, useEffect, useMemo, useState } from "react" export default function ServicesPage() { const userId = useAuthStore((state) => state.user?.id) const currentRole = useAuthStore((state) => state.currentRole) const shops = useShopStore((state) => state.shops) - const players = usePlayerStore((state) => state.players) - const services = useServiceStore((state) => state.services) - const deleteService = useServiceStore((state) => state.deleteService) + const [players, setPlayers] = useState([]) + const [services, setServices] = useState([]) + const [loading, setLoading] = useState(true) const ownerShop = resolveOwnerShop(userId, shops) - const scopedPlayerIds = - currentRole === "player" - ? userId - ? [userId] - : [] - : currentRole === "owner" - ? ownerShop - ? players.filter((player) => player.shopId === ownerShop.id).map((player) => player.id) - : [] - : [] + const loadData = useCallback(async () => { + try { + const [nextPlayers, nextServices] = await Promise.all([listPlayers(), listServices()]) + setPlayers(nextPlayers) + setServices(nextServices) + } catch (error) { + notifyInfo(toApiError(error).msg) + } finally { + setLoading(false) + } + }, []) + + useEffect(() => { + let cancelled = false + + Promise.all([listPlayers(), listServices()]) + .then(([nextPlayers, nextServices]) => { + if (cancelled) return + setPlayers(nextPlayers) + setServices(nextServices) + }) + .catch((error) => { + if (cancelled) return + notifyInfo(toApiError(error).msg) + }) + .finally(() => { + if (cancelled) return + setLoading(false) + }) + + return () => { + cancelled = true + } + }, []) + + const scopedPlayerIdSet = useMemo(() => { + if (currentRole === "player") { + const player = players.find((item) => item.user.id === userId) + return new Set(player ? [player.id] : []) + } + + if (currentRole === "owner" && ownerShop) { + return new Set( + players.filter((player) => player.shopId === ownerShop.id).map((player) => player.id), + ) + } + + return new Set() + }, [currentRole, ownerShop, players, userId]) - const scopedPlayerIdSet = new Set(scopedPlayerIds) const scopedServices = services.filter((service) => scopedPlayerIdSet.has(service.playerId)) + async function handleDelete(service: PlayerService) { + if (!scopedPlayerIdSet.has(service.playerId)) return + + try { + await deletePlayerService(service.id) + notifySuccess("服务已删除") + await loadData() + } catch (error) { + notifyInfo(toApiError(error).msg) + } + } + return (
@@ -71,44 +124,57 @@ export default function ServicesPage() { - {scopedServices.map((service) => ( - - {service.title} - - {service.gameName} - - - ¥{service.price}/{service.unit} - - {service.rankRange ?? "-"} - -
- {service.availability.map((a) => ( -
{a}
- ))} -
-
- -
- - -
+ {loading ? ( + + + 加载中... - ))} + ) : scopedServices.length === 0 ? ( + + + 暂无服务 + + + ) : ( + scopedServices.map((service) => ( + + {service.title} + + {service.gameName} + + + ¥{service.price}/{service.unit} + + {service.rankRange ?? "-"} + +
+ {service.availability.map((a) => ( +
{a}
+ ))} +
+
+ +
+ + +
+
+
+ )) + )}
diff --git a/lib/api/index.ts b/lib/api/index.ts index de565d9..520bf83 100644 --- a/lib/api/index.ts +++ b/lib/api/index.ts @@ -13,7 +13,14 @@ export { getOrderById, listOrders, listOrdersByConsumer } from "./orders" export { getPlayerById, listPlayers, listPlayersByShop } from "./players" export { getPostById, listPosts, listPostsByAuthor, togglePostLike } from "./posts" export { listReviews, listReviewsByOrder, listReviewsByTargetUser } from "./reviews" -export { getServiceById, listServices, listServicesByPlayer } from "./services" +export { + createPlayerService, + deletePlayerService, + getServiceById, + listServices, + listServicesByPlayer, + updatePlayerService, +} from "./services" export { getShopById, getShopByOwnerId, listShops } from "./shops" export { getWalletBalance, diff --git a/lib/api/services.ts b/lib/api/services.ts index c48cc0f..9c730e8 100644 --- a/lib/api/services.ts +++ b/lib/api/services.ts @@ -4,7 +4,7 @@ import type { PlayerService } from "@/lib/types" import { httpJson } from "./http" type Paginated = { - items: T[] + items: T[] | null meta: { total: number offset: number @@ -12,10 +12,37 @@ type Paginated = { } } +function itemsFrom(value: Paginated | T[]): T[] { + if (Array.isArray(value)) return value + return value.items ?? [] +} + +export type ServiceInput = { + gameId: string + title: string + description: string + price: number + unit: string + rankRange?: string + availability: string[] +} + +function serviceJson(input: ServiceInput) { + return { + gameId: Number(input.gameId), + title: input.title, + description: input.description, + price: input.price, + unit: input.unit, + rankRange: input.rankRange, + availability: input.availability, + } +} + export async function listServices(): Promise { const res = await httpJson("/api/v1/services", { cache: "no-store" }) if (typeof res === "object" && res !== null && "items" in res) { - return (res as Paginated).items + return itemsFrom(res as Paginated) } return res as PlayerService[] } @@ -44,7 +71,27 @@ export async function listServicesByPlayer(playerId: string): Promise).items + return itemsFrom(res as Paginated) } - return res as PlayerService[] + return itemsFrom(res) +} + +export async function createPlayerService(input: ServiceInput): Promise { + return httpJson("/api/v1/services", { + method: "POST", + json: serviceJson(input), + }) +} + +export async function updatePlayerService(serviceId: string, input: ServiceInput): Promise { + await httpJson(`/api/v1/services/${encodeURIComponent(serviceId)}`, { + method: "PUT", + json: serviceJson(input), + }) +} + +export async function deletePlayerService(serviceId: string): Promise { + await httpJson(`/api/v1/services/${encodeURIComponent(serviceId)}`, { + method: "DELETE", + }) }