/** * # 路由 /admin/products/$id 产品编辑页 * 基础字段 + 特色图/图集媒体选择(远程搜索媒体库)+ 属性值多选(保存时重建 * product_attribute_map)+ 产品分类多选(product_terms)。id 为 new 时新建。 */ import { createFileRoute, useNavigate, useRouter } from "@tanstack/react-router"; import { useState } from "react"; import { App, Button, Card, Form, Input, InputNumber, Select, Space, Switch } from "antd"; import { ArrowLeftOutlined, SaveOutlined } from "@ant-design/icons"; import { adminGetProduct, adminSaveProduct, adminSearchMedia } from "~/server/admin"; import { Forbidden, guardAdmin } from "~/components/admin/common"; export const Route = createFileRoute("/admin/products/$id")({ loader: ({ params }) => guardAdmin(async () => { // new → id 0(同时作为权限探测,未登录抛 FORBIDDEN) const id = params.id === "new" ? 0 : Number(params.id); const [data, mediaOptions] = await Promise.all([adminGetProduct({ data: { id } }), adminSearchMedia({ data: { limit: 30 } })]); return { ...data, mediaOptions }; }), component: ProductEditPage, }); /** # 类型 媒体选择器选项 **/ type MediaOption = { id: number; path: string; url: string }; /** # 函数 文件路径转显示名(取 basename) */ function mediaLabel(path: string): string { return path.split("/").pop() || path; } /** # 组件 媒体选择器(AntD Select 远程搜索媒体库,支持单选/多选) * 参数 options:初始选项;multiple:是否多选;value/onChange 受控由 Form.Item 接管 */ function MediaSelect(props: { options: MediaOption[]; multiple?: boolean; placeholder?: string }) { const [opts, setOpts] = useState(props.options); /** # 函数 远程搜索媒体(防抖 300ms) */ function handleSearch(q: string) { window.clearTimeout((handleSearch as unknown as { _t?: number })._t); (handleSearch as unknown as { _t?: number })._t = window.setTimeout(() => { adminSearchMedia({ data: { q: q || undefined, limit: 30 } }) .then((rows) => setOpts(rows)) .catch(() => {}); }, 300); } return ( !initialGalleryOptions.some((g) => g.id === m.id))]} multiple /> {attributes.length > 0 && ( {attributes.map((a) => ( ({ value: c.id, label: c.name }))} /> ); }