/** * # 路由 /admin/inquiries 询盘管理 * 状态 Tab 过滤 + 表格;行点击抽屉查看完整 fields JSON;操作按钮标记已读/已回复/垃圾。 * 数据接口复用 src/server/inquiries.ts 的 listInquiries / updateInquiryStatus。 */ import { createFileRoute, useRouter } from "@tanstack/react-router"; import { App, Button, Card, Descriptions, Drawer, Input, Space, Table, Tabs, Tag } from "antd"; import { CheckOutlined, SearchOutlined, SendOutlined, StopOutlined } from "@ant-design/icons"; import { useState } from "react"; import { listInquiries, updateInquiryStatus } from "~/server/inquiries"; import type { inquiries } from "~/db/schema"; import { AdminEmpty, Forbidden, INQUIRY_STATUS_COLOR, INQUIRY_STATUS_LABEL, Json, PageHeader, StatusTag, fmtDateTime, guardAdmin, tablePagination, type AdminSearch, } from "~/components/admin/common"; export const Route = createFileRoute("/admin/inquiries")({ validateSearch: (search: Record): AdminSearch => { // 只保留显式传入的参数,缺省时不回写 URL(避免 307 归一化重定向) const out: AdminSearch = {}; const page = Math.floor(Number(search.page)) || 0; if (page > 1) out.page = page; if (typeof search.q === "string" && search.q) out.q = search.q; if (typeof search.status === "string" && search.status) out.status = search.status; return out; }, loaderDeps: ({ search }) => ({ search }), loader: ({ deps }) => guardAdmin(() => listInquiries({ data: { page: deps.search.page, status: deps.search.status || undefined, q: deps.search.q || undefined } }), ), component: InquiriesPage, }); /** # 成员变量 询盘行类型(fields/utm 收窄为 Json 后的形状) **/ type InquiryRow = Omit & { fields: Json; utm: Json }; /** # 函数 询盘管理页面组件 */ function InquiriesPage() { const data = Route.useLoaderData(); const search = Route.useSearch(); const navigate = Route.useNavigate(); const router = useRouter(); const { message } = App.useApp(); const [q, setQ] = useState(search.q ?? ""); /** # 成员变量 当前抽屉中查看的询盘记录(null = 关闭) **/ const [viewing, setViewing] = useState(null); if (!data) return ; /** # 函数 更新查询参数 */ function updateSearch(patch: AdminSearch) { void navigate({ search: (prev) => ({ ...prev, ...patch, page: 1 }) }); } /** # 函数 更新询盘状态并刷新 */ async function handleStatus(id: number, status: string) { await updateInquiryStatus({ data: { id, status } }); message.success("状态已更新"); await router.invalidate(); } return ( } value={q} onChange={(e) => setQ(e.target.value)} onPressEnter={() => updateSearch({ q })} style={{ width: 240 }} /> } /> updateSearch({ status: key })} items={[ { key: "", label: "全部" }, ...(["new", "read", "replied", "spam", "archived"] as const).map((s) => ({ key: s, label: INQUIRY_STATUS_LABEL[s] })), ]} /> }} onRow={(r) => ({ onClick: () => setViewing(r), style: { cursor: "pointer" } })} pagination={tablePagination(data.total, data.page, data.pageSize, (p) => void navigate({ search: (prev) => ({ ...prev, page: p }) }), )} columns={[ { title: "ID", dataIndex: "id", width: 70 }, { title: "时间", dataIndex: "createdAt", width: 150, render: (v: string) => fmtDateTime(v) }, { title: "姓名", dataIndex: "name", width: 130, ellipsis: true }, { title: "邮箱", dataIndex: "email", width: 200, ellipsis: true }, { title: "公司", dataIndex: "company", width: 160, ellipsis: true }, { title: "国家", dataIndex: "country", width: 110, ellipsis: true }, { title: "感兴趣产品", dataIndex: "productInterest", ellipsis: true }, { title: "来源", dataIndex: "formName", width: 150, ellipsis: true }, { title: "状态", dataIndex: "status", width: 100, render: (v: string) => , }, { title: "操作", key: "actions", width: 130, render: (_, r) => ( e.stopPropagation()}> {r.status !== "read" && r.status !== "replied" && (