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