feat: 新增路线详情关联管家顾问及相关功能

详细变更如下:
- 更新 .gitignore 文件,添加 pnpm-store 忽略规则
- 新增数据库迁移脚本,为 DetailRecord 添加可空的 conciergeAdvisorId 字段用于关联管家顾问
- 完善 Admin 后台玩法详情编辑器,支持选择关联的管家顾问并校验合法性
- 公共 API 支持返回已启用的管家顾问完整数据,不在详情表中冗余存储管家资料
- 小程序端新增详情页联系管家入口、个人页最近浏览历史功能
- 更新所有相关文档与测试用例,修复下拉选择框的 z-index 样式问题
This commit is contained in:
duanshuwen committed 2026-08-22 08:26:23 +08:00
1 parent e7b8c5bf43
commit b7a81964c9
34 files changed
+739 -63

No files matched your search

+1
View File
@@ -133,6 +133,7 @@ export type DetailRecord = {
excluded: string[];
notes: string[];
gallery: string[];
conciergeAdvisorId: string | null;
isActive: boolean;
sortOrder: number;
createdAt?: string;
@@ -1,6 +1,13 @@
import type { DetailCreate } from "@/api";
import type { ConciergeAdvisor, DetailCreate } from "@/api";
import { ImageGalleryUploader } from "@/components/admin/ImageGalleryUploader";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
@@ -15,9 +22,11 @@ type DetailArrayField = (typeof detailArrayFields)[number][0];
export function WanfaRouteDetailFields({
detail,
conciergeAdvisors,
onChange,
}: {
detail: DetailCreate;
conciergeAdvisors: ConciergeAdvisor[];
onChange: <K extends keyof DetailCreate>(key: K, value: DetailCreate[K]) => void;
}) {
const updateLines = (key: DetailArrayField, value: string) => {
@@ -107,6 +116,28 @@ export function WanfaRouteDetailFields({
/>
</label>
))}
<div className="grid gap-2 text-sm font-medium text-slate-800 sm:col-span-2">
<span>联系管家</span>
<p className="-mt-1 text-xs font-normal leading-5 text-slate-500">
详情页只保存顾问 ID,前台会读取管家领域的最新头像、二维码和服务详情。
</p>
<Select
value={detail.conciergeAdvisorId ?? "__none__"}
onValueChange={(value) => onChange("conciergeAdvisorId", value === "__none__" ? null : value)}
>
<SelectTrigger aria-label="选择联系管家">
<SelectValue placeholder="不配置管家" />
</SelectTrigger>
<SelectContent position="item-aligned">
<SelectItem value="__none__">不配置管家</SelectItem>
{conciergeAdvisors.map((advisor) => (
<SelectItem key={advisor.id} value={advisor.id}>
{advisor.name} · {advisor.role}{advisor.isActive ? "" : "(已停用)"}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="sm:col-span-2">
<ImageGalleryUploader
id="wanfa-detail-gallery"
@@ -1,7 +1,7 @@
import { useState } from "react";
import { Save, X } from "lucide-react";
import type { DetailCreate, DetailRecord, WanfaRoute, WanfaRouteCreate } from "@/api";
import type { ConciergeAdvisor, DetailCreate, DetailRecord, WanfaRoute, WanfaRouteCreate } from "@/api";
import { SingleImageUploader } from "@/components/admin/SingleImageUploader";
import { WanfaRouteDetailFields } from "@/components/admin/WanfaRouteDetailFields";
import { Button } from "@/components/ui/button";
@@ -28,6 +28,7 @@ const emptyDetail = (): DetailCreate => ({
excluded: [],
notes: [],
gallery: [],
conciergeAdvisorId: null,
isActive: true,
});
@@ -56,6 +57,7 @@ const detailDraft = (detail?: DetailRecord): DetailCreate =>
excluded: detail.excluded ?? [],
notes: detail.notes ?? [],
gallery: detail.gallery ?? [],
conciergeAdvisorId: detail.conciergeAdvisorId ?? null,
isActive: detail.isActive,
sortOrder: detail.sortOrder,
}
@@ -71,6 +73,7 @@ export function WanfaRouteEditor({
categoryLabel,
initialRoute,
initialDetail,
conciergeAdvisors,
saving,
onSave,
onClose,
@@ -79,6 +82,7 @@ export function WanfaRouteEditor({
categoryLabel: string;
initialRoute?: WanfaRoute;
initialDetail?: DetailRecord;
conciergeAdvisors: ConciergeAdvisor[];
saving: boolean;
onSave: (input: WanfaRouteEditorValue) => void;
onClose: () => void;
@@ -221,7 +225,11 @@ export function WanfaRouteEditor({
group="wanfa"
/>
</div>
<WanfaRouteDetailFields detail={draft.detail} onChange={updateDetail} />
<WanfaRouteDetailFields
detail={draft.detail}
conciergeAdvisors={conciergeAdvisors}
onChange={updateDetail}
/>
{error ? <p className="text-sm leading-5 text-rose-600">{error}</p> : null}
</div>
<footer className="flex justify-end gap-3 border-t border-slate-100 px-5 py-4">
@@ -38,7 +38,9 @@ export function WanfaRouteRail({
{category ? (
<div className="mapped-list">
{category.routes.map((route, index) => {
const hasDetail = details.some((detail) => detail.key === route.id);
const detail = details.find((item) => item.key === route.id);
const hasDetail = Boolean(detail);
const hasConcierge = Boolean(detail?.conciergeAdvisorId);
return (
<div className="mapped-list-row" key={route.id}>
<div className="mapped-list-main">
@@ -54,6 +56,13 @@ export function WanfaRouteRail({
>
{hasDetail ? "详情已配置" : "详情未配置"}
</span>
{hasDetail ? (
<span
className={`hidden rounded-full px-2.5 py-1 text-xs font-semibold lg:inline-flex ${hasConcierge ? "bg-sky-50 text-sky-700" : "bg-slate-100 text-slate-500"}`}
>
{hasConcierge ? "管家已配置" : "未配管家"}
</span>
) : null}
<Button
type="button"
variant="ghost"
@@ -65,7 +65,7 @@ const SelectContent = React.forwardRef<
ref={ref}
position={position}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-lg border border-slate-200 bg-white text-slate-950 shadow-lg outline-none data-[state=closed]:opacity-0 data-[state=open]:opacity-100",
"relative z-[70] max-h-96 min-w-[8rem] overflow-hidden rounded-lg border border-slate-200 bg-white text-slate-950 shadow-lg outline-none data-[state=closed]:opacity-0 data-[state=open]:opacity-100",
position === "popper" && "translate-y-1",
className,
)}
@@ -6,6 +6,7 @@ import {
createWanfaRoute,
deleteWanfaCategory,
deleteWanfaRoute,
getConciergeAdvisors,
getDetails,
getWanfaCategories,
reorderWanfaCategories,
@@ -14,7 +15,7 @@ import {
updateWanfaCategory,
updateWanfaRoute,
} from "@/api";
import type { DetailRecord, WanfaCategory, WanfaRoute } from "@/api";
import type { ConciergeAdvisor, DetailRecord, WanfaCategory, WanfaRoute } from "@/api";
import { WanfaCategoryEditor } from "@/components/admin/WanfaCategoryEditor";
import { WanfaCategoryRail } from "@/components/admin/WanfaCategoryRail";
import { WanfaConfirmDialog } from "@/components/admin/WanfaConfirmDialog";
@@ -46,6 +47,7 @@ function swap<T>(items: T[], index: number, direction: -1 | 1) {
export function WanfaPage({ notify }: { notify: Notify }) {
const [categories, setCategories] = useState<WanfaCategory[] | null>(null);
const [details, setDetails] = useState<DetailRecord[]>([]);
const [conciergeAdvisors, setConciergeAdvisors] = useState<ConciergeAdvisor[]>([]);
const [selectedCategoryId, setSelectedCategoryId] = useState("");
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
@@ -63,10 +65,15 @@ export function WanfaPage({ notify }: { notify: Notify }) {
const loadCategories = async () => {
setLoading(true);
try {
const [categoryResponse, detailResponse] = await Promise.all([getWanfaCategories(), getDetails()]);
const [categoryResponse, detailResponse, conciergeResponse] = await Promise.all([
getWanfaCategories(),
getDetails(),
getConciergeAdvisors(),
]);
const nextCategories = categoryResponse.categories ?? [];
setCategories(nextCategories);
setDetails(detailResponse.details ?? []);
setConciergeAdvisors(conciergeResponse.advisors ?? []);
setSelectedCategoryId((current) =>
nextCategories.some((category) => category.id === current)
? current
@@ -325,6 +332,7 @@ export function WanfaPage({ notify }: { notify: Notify }) {
? details.find((detail) => detail.key === routeEditor.route?.id)
: undefined
}
conciergeAdvisors={conciergeAdvisors}
saving={savingKey === "route"}
onSave={(input) => void saveRoute(input)}
onClose={() => setRouteEditor(null)}