feat(mini-app, docs): 新增小程序详情页,更新项目文档与规范
新增全套关联组件:DetailHero、DetailOverviewCard、DetailInfoCard、DetailMediaGallery、DetailActionBar及ConciergeContactSheet。 新增详情页数据处理工具类detailPresentation.ts,处理商品展示数据的格式化与默认值兼容。 更新项目文档:补充三个领域API补充文档引用,优化AGENTS.md中的前端代码规范与docs文档列表。
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,6 +1,5 @@
|
||||
node_modules/
|
||||
dist/
|
||||
docs/
|
||||
apps/*/dist/
|
||||
.env
|
||||
.env.local
|
||||
|
||||
@@ -268,7 +268,7 @@ yarn dev:mp-weixin
|
||||
- TypeScript 项目使用 strict 模式,新增代码应有明确类型,避免 `any`。
|
||||
- `WonderQ-MiniAPP` 使用 Vue 3 SFC、`<script setup lang="ts">` 和 uni-app 组件,不引入 React。
|
||||
- `WonderQ-Admin-UI` 使用 React 函数组件和 Hooks,不引入 class component。
|
||||
- 样式优先使用项目已集成的 TailwindCSS;管理端同时遵守 `src/styles.css` 的设计变量和已有类名体系。
|
||||
- `WonderQ-Admin-UI` 优先使用 TailwindCSS 工具类完成布局、间距、颜色、字体、边框、响应式和交互状态;重复样式优先使用 `@apply` 或可复用基础组件封装。`src/styles.css` 作为 Tailwind 入口和全局样式层,仅保留设计变量、基础重置、复杂选择器、伪元素以及 Tailwind 无法清晰表达的兼容规则;新增自定义 CSS 前应先确认 Tailwind 无法合理表达,并在代码附近说明原因。
|
||||
- `WonderQ-MiniAPP` 页面和组件默认只能使用 TailwindCSS 工具类完成布局、间距、颜色、字体、边框、响应式及交互状态;禁止新增自定义 `<style>` 或 `<style scoped>`。仅在 uni-app 平台兼容性、伪元素或 Tailwind 无法表达的特殊规则下允许例外,并在代码附近说明原因。
|
||||
- `WonderQ-MiniAPP` 的响应式样式优先使用 TailwindCSS 断点和任意值变体,避免在单文件组件中新增独立媒体查询;重复的视觉规则应提取为全局工具类或可复用组件。
|
||||
- `WonderQ-MiniAPP` 移动端采用简约、内容优先的视觉风格:以白色和低饱和中性色为基础,使用绿色作为主操作色,避免大面积渐变、金色渐变按钮、装饰性光晕和重阴影。
|
||||
@@ -369,6 +369,7 @@ yarn dev:mp-weixin
|
||||
|
||||
- 是否误读或输出敏感信息。
|
||||
- 是否改动了未授权文件或锁定文件。
|
||||
- 新增或修改 Admin UI 样式是否优先使用 TailwindCSS,是否避免无必要的页面级自定义 CSS。
|
||||
- 是否保持 Public API、Admin API、页面模块契约一致。
|
||||
- 是否遗漏鉴权、参数校验、错误响应、审计或删除冲突处理。
|
||||
- 是否处理 loading、empty、error、retry 和移动端适配。
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<view
|
||||
class="flex w-full shrink-0 items-center gap-2 border-t border-[#e8e2da] bg-white/95 px-3 pb-[calc(10px+env(safe-area-inset-bottom))] pt-2">
|
||||
<view class="flex w-full shrink-0 items-center gap-1">
|
||||
<view class="tap-feedback m-0 flex h-12 w-12 flex-col items-center justify-center p-0 text-[#81776f]"
|
||||
@click="$emit('call')">
|
||||
<uni-icons type="phone" :size="18" color="#81776f" />
|
||||
<text class="mt-0.5 text-[9px]">电话</text>
|
||||
</view>
|
||||
|
||||
<view class="tap-feedback m-0 flex h-12 w-12 flex-col items-center justify-center p-0 text-[#81776f]"
|
||||
@click="$emit('concierge')">
|
||||
<uni-icons type="headphones" :size="18" color="#81776f" />
|
||||
<text class="mt-0.5 text-[9px]">管家</text>
|
||||
</view>
|
||||
|
||||
<view class="tap-feedback relative m-0 flex h-12 w-12 flex-col items-center justify-center p-0 text-[#81776f]">
|
||||
<uni-icons type="redo" :size="18" color="#81776f" />
|
||||
<text class="mt-0.5 text-[9px]">分享</text>
|
||||
<button open-type="share" plain hover-class="none" aria-label="分享"
|
||||
class="absolute inset-0 z-10 m-0 h-full w-full border-0 bg-transparent p-0 opacity-0" />
|
||||
</view>
|
||||
|
||||
<view class="ml-auto flex min-w-0 items-center gap-2">
|
||||
<view class="min-w-0">
|
||||
<text class="block text-[10px] text-[#9b9188]">参考起价</text>
|
||||
<text class="mt-0.5 block whitespace-nowrap text-[18px] font-bold leading-none text-[#e96635]">¥{{
|
||||
price }}万</text>
|
||||
</view>
|
||||
<view
|
||||
class="tap-feedback flex items-center justify-center m-0 h-11 rounded-full bg-[#e96635] px-4 text-[13px] font-semibold text-white shadow-[0_6px_16px_rgba(233,102,53,0.22)]"
|
||||
@click="$emit('booking')">立即预订</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { formatWan } from "@/lib/data";
|
||||
|
||||
defineProps<{
|
||||
price: number;
|
||||
favorite: boolean;
|
||||
}>();
|
||||
|
||||
defineEmits<{
|
||||
call: [];
|
||||
concierge: [];
|
||||
booking: [];
|
||||
}>();
|
||||
</script>
|
||||
30
WonderQ-MiniAPP/src/pages/detail/components/DetailHero.vue
Normal file
30
WonderQ-MiniAPP/src/pages/detail/components/DetailHero.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<view class="relative h-[286px] overflow-hidden bg-[#d8d5cf]">
|
||||
<image class="absolute inset-0 h-full w-full" :src="image" mode="aspectFill" />
|
||||
|
||||
<view class="absolute bottom-10 left-4 right-4 z-10 text-white">
|
||||
<view class="flex items-center gap-2">
|
||||
<text class="flex items-center justify-center rounded-[20px] bg-[#e96635] px-2 py-1 text-[10px] font-semibold">
|
||||
{{ presentation.eyebrow }}
|
||||
</text>
|
||||
<text
|
||||
class="flex items-center justify-center rounded-[20px] text-[10px] bg-[#333333] text-white px-2 py-1 font-semibold">
|
||||
{{ presentation.duration }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { DetailPresentation } from "./detailPresentation";
|
||||
|
||||
defineProps<{
|
||||
image: string;
|
||||
presentation: DetailPresentation;
|
||||
}>();
|
||||
|
||||
defineEmits<{
|
||||
back: [];
|
||||
}>();
|
||||
</script>
|
||||
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<view
|
||||
class="mx-3 mb-3 rounded-[12px] border border-[#e8e2da] bg-white px-4 py-4 shadow-[0_4px_16px_rgba(63,48,37,0.04)]">
|
||||
<view class="flex items-center gap-2">
|
||||
<view class="h-4 w-1 rounded-full bg-[#e96635]" />
|
||||
<text class="text-[16px] font-bold text-[#282421]">{{ title }}</text>
|
||||
<text v-if="eyebrow" class="text-[10px] tracking-[0.12em] text-[#aaa097]">{{ eyebrow }}</text>
|
||||
</view>
|
||||
|
||||
<view v-if="paragraphs?.length" class="mt-3 space-y-2">
|
||||
<text v-for="paragraph in paragraphs" :key="paragraph" class="block text-[12px] leading-5 text-[#6d655e]">{{
|
||||
paragraph }}</text>
|
||||
</view>
|
||||
|
||||
<view v-if="bullets?.length" class="mt-3 space-y-2">
|
||||
<view v-for="item in bullets" :key="item" class="flex items-start gap-2">
|
||||
<uni-icons type="checkbox" :size="20" color="#e96635" />
|
||||
<text class="text-[12px] leading-5 text-[#5f5852]">{{ item }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="included?.length || excluded?.length" class="mt-3 grid grid-cols-2 gap-2">
|
||||
<view class="rounded-[8px] bg-[#fff8f3] p-3">
|
||||
<text class="block text-[11px] font-semibold text-[#e96635]">费用包含</text>
|
||||
<view class="mt-2 space-y-1.5">
|
||||
<text v-for="item in included" :key="item" class="block text-[11px] leading-4 text-[#6d655e]">✓ {{ item
|
||||
}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="rounded-[8px] bg-[#f7f6f3] p-3">
|
||||
<text class="block text-[11px] font-semibold text-[#7b7168]">费用不含</text>
|
||||
<view class="mt-2 space-y-1.5">
|
||||
<text v-for="item in excluded" :key="item" class="block text-[11px] leading-4 text-[#6d655e]">× {{ item
|
||||
}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="empty" class="mt-3 rounded-[8px] bg-[#faf8f5] px-3 py-4 text-center">
|
||||
<text class="block text-[13px] font-semibold text-[#413a34]">暂无评价</text>
|
||||
<text class="mt-1 block text-[11px] text-[#a09890]">完成一次旅行后,欢迎留下你的体验</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
title: string;
|
||||
eyebrow?: string;
|
||||
paragraphs?: string[];
|
||||
bullets?: string[];
|
||||
included?: string[];
|
||||
excluded?: string[];
|
||||
empty?: boolean;
|
||||
}>();
|
||||
</script>
|
||||
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<view class="px-3 pb-4">
|
||||
<view class="overflow-hidden rounded-xl bg-white shadow-[0_4px_16px_rgba(63,48,37,0.04)]">
|
||||
<view class="space-y-2">
|
||||
<image v-for="(image, index) in images" :key="`${image}-${index}`" class="block w-full bg-[#eeeae4]"
|
||||
:src="image" mode="widthFix" lazy-load />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
images: string[];
|
||||
}>();
|
||||
</script>
|
||||
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<view
|
||||
class="relative z-10 -mt-4 mx-3 rounded-[12px] border border-[#e8e2da] bg-white p-4 shadow-[0_8px_24px_rgba(63,48,37,0.08)]">
|
||||
<text class="block text-[22px] font-bold leading-7 text-[#24211f]">{{ presentation.title }}</text>
|
||||
|
||||
<view class="mt-4 flex items-end justify-between gap-3 border-t border-[#f0ebe5] pt-3">
|
||||
<view>
|
||||
<text class="block text-[11px] text-[#a69d95]">参考起价</text>
|
||||
<view class="mt-1 flex items-baseline gap-1">
|
||||
<text class="text-[25px] font-bold leading-none text-[#e96635]">¥{{ formatWan(product.price) }}</text>
|
||||
<text class="text-[11px] text-[#8f857d]">万起/人</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mt-4 grid grid-cols-3 gap-2">
|
||||
<view v-for="item in quickFacts" :key="item.label" class="rounded-[8px] bg-[#f8f6f2] px-2 py-2.5">
|
||||
<text class="block text-[10px] text-[#a39a91]">{{ item.label }}</text>
|
||||
<text class="mt-1 block truncate text-[12px] font-medium text-[#3d3833]">{{ item.value }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { toRefs } from "vue";
|
||||
import { formatWan } from "@/lib/data";
|
||||
import type { Product } from "@/lib/types";
|
||||
import type { DetailPresentation } from "./detailPresentation";
|
||||
|
||||
const props = defineProps<{
|
||||
product: Product;
|
||||
meta: { location: string; summary: string };
|
||||
presentation: DetailPresentation;
|
||||
favorite: boolean;
|
||||
}>();
|
||||
|
||||
defineEmits<{
|
||||
toggleFavorite: [];
|
||||
}>();
|
||||
|
||||
const quickFacts = [
|
||||
{ label: "出行方式", value: "小包团" },
|
||||
{ label: "适合人数", value: "2-8人" },
|
||||
{ label: "服务方式", value: "管家跟进" },
|
||||
];
|
||||
|
||||
const { product, meta, presentation, favorite } = toRefs(props);
|
||||
</script>
|
||||
@@ -0,0 +1,93 @@
|
||||
import { stripTitle } from "@/lib/data";
|
||||
import type { Product } from "@/lib/types";
|
||||
|
||||
export type DetailPresentation = {
|
||||
eyebrow: string;
|
||||
duration: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
intro: string;
|
||||
highlights: string[];
|
||||
included: string[];
|
||||
excluded: string[];
|
||||
notes: string[];
|
||||
gallery: string[];
|
||||
};
|
||||
|
||||
const fallbackGallery = [
|
||||
"https://www.zurnal24.si/media/img/5e/d5/9526a56dba168aa136f3.jpeg",
|
||||
"https://genk.mediacdn.vn/139269124445442048/2024/4/27/10-23-sinkhole-1714189653945948438879.jpg",
|
||||
"https://p2.cri.cn/M00/89/21/rBABC2aHnWeACxj5AAAAAAAAAAA159.1000x566.jpg",
|
||||
"https://dimg04.c-ctrip.com/images/0EQ5712000ca7t504EC0E_W_640_10000.jpg?proc=autoorient",
|
||||
];
|
||||
|
||||
function getDuration(title: string) {
|
||||
return title.match(/\d+天\d+晚/)?.[0] ?? "5天4晚";
|
||||
}
|
||||
|
||||
function getTextBlocks(product: Product) {
|
||||
return (product.detailSections ?? [])
|
||||
.flatMap((section) => section.blocks)
|
||||
.filter(
|
||||
(block): block is { type: "text"; text: string } => block.type === "text",
|
||||
)
|
||||
.map((block) => block.text.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
export function createDetailPresentation(product: Product): DetailPresentation {
|
||||
const title = stripTitle(product.title);
|
||||
const combinedText = `${product.title} ${product.summary ?? ""}`;
|
||||
const isCaveRoute =
|
||||
combinedText.includes("洞") || combinedText.includes("探险");
|
||||
const detailTexts = getTextBlocks(product);
|
||||
const apiImages = (product.images ?? [])
|
||||
.slice()
|
||||
.sort((left, right) => left.sortOrder - right.sortOrder)
|
||||
.map((image) => image.url.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
return {
|
||||
eyebrow: product.tags[0] || "玩法推荐",
|
||||
duration: getDuration(product.title),
|
||||
title: isCaveRoute ? "地心探险大环线" : title,
|
||||
subtitle:
|
||||
product.subtitle || `${product.destinationName || "贵州"}·小包团路线`,
|
||||
intro:
|
||||
detailTexts[0] ||
|
||||
product.summary ||
|
||||
"沿着贵州山地的自然纹理深入探索,把核心景观、在地体验和轻户外节奏安排在一条线路里。",
|
||||
highlights: isCaveRoute
|
||||
? [
|
||||
"深入喀斯特洞穴与地下河,安排专业向导陪同",
|
||||
"小团出行,按同行人的体力和兴趣灵活调整",
|
||||
"山野咖啡与自然景观穿插,留出松弛的停留时间",
|
||||
"行程前由服务管家确认天气、装备和接送细节",
|
||||
]
|
||||
: [
|
||||
"核心景观串联,减少无效往返和重复换乘",
|
||||
"小团出行,按同行人的节奏灵活调整",
|
||||
"在地体验与舒适住宿合理衔接",
|
||||
"行程前由服务管家确认天气、装备和接送细节",
|
||||
],
|
||||
included: [
|
||||
"行程内用车与接送服务",
|
||||
"列明景点门票和体验项目",
|
||||
"服务管家行前确认与途中跟进",
|
||||
"行程内住宿及方案中注明的服务",
|
||||
],
|
||||
excluded: [
|
||||
"往返大交通及个人消费",
|
||||
"未列明餐食和自选体验",
|
||||
"因个人原因产生的额外费用",
|
||||
],
|
||||
notes: [
|
||||
"贵州多山多雨,请准备防滑鞋、轻便雨具和薄外套。",
|
||||
"溶洞、漂流、徒步等体验会根据天气和同行人体力适当调整。",
|
||||
"页面价格为参考起价,最终方案以出行日期、人数和资源确认结果为准。",
|
||||
],
|
||||
gallery: Array.from(
|
||||
new Set([product.image, ...apiImages, ...fallbackGallery]),
|
||||
).slice(0, 6),
|
||||
};
|
||||
}
|
||||
111
WonderQ-MiniAPP/src/pages/detail/index.vue
Normal file
111
WonderQ-MiniAPP/src/pages/detail/index.vue
Normal file
@@ -0,0 +1,111 @@
|
||||
<template>
|
||||
<view class="wq-phone flex h-screen flex-col bg-[#f5f3ef]">
|
||||
<scroll-view scroll-y class="min-h-0 flex-1">
|
||||
<DetailHero :image="product.image" :presentation="presentation" @back="goBack()" />
|
||||
|
||||
<DetailOverviewCard :product="product" :meta="meta" :presentation="presentation" :favorite="favorite"
|
||||
@toggle-favorite="toggleFavorite" />
|
||||
|
||||
<view class="mt-4">
|
||||
<DetailInfoCard title="详细介绍" eyebrow="ABOUT THIS ROUTE" :paragraphs="introParagraphs" />
|
||||
<DetailInfoCard title="行程亮点" eyebrow="HIGHLIGHTS" :bullets="presentation.highlights" />
|
||||
<DetailInfoCard title="费用明细" eyebrow="PRICE DETAILS" :paragraphs="priceParagraphs"
|
||||
:included="presentation.included" :excluded="presentation.excluded" />
|
||||
<DetailInfoCard title="注意事项" eyebrow="NOTES" :paragraphs="presentation.notes" />
|
||||
<DetailInfoCard title="用户评价" eyebrow="REVIEWS" empty />
|
||||
</view>
|
||||
|
||||
<DetailMediaGallery :images="presentation.gallery" />
|
||||
</scroll-view>
|
||||
|
||||
<DetailActionBar :price="1.68" :favorite="favorite" @call="goCall" @concierge="goConcierge"
|
||||
@booking="goBooking(product)" />
|
||||
|
||||
<ConciergeContactSheet :open="Boolean(selectedAdvisor)" :advisor="selectedAdvisor" @close="handleConciergeClosed" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
import { onLoad, onShareAppMessage, onShow } from "@dcloudio/uni-app";
|
||||
import DetailActionBar from "./components/DetailActionBar.vue";
|
||||
import DetailHero from "./components/DetailHero.vue";
|
||||
import DetailInfoCard from "./components/DetailInfoCard.vue";
|
||||
import DetailMediaGallery from "./components/DetailMediaGallery.vue";
|
||||
import DetailOverviewCard from "./components/DetailOverviewCard.vue";
|
||||
import ConciergeContactSheet from "@/components/ConciergeContactSheet.vue";
|
||||
import type { ConciergeAdvisor } from "../concierge/components/conciergeTypes";
|
||||
import { createDetailPresentation } from "./components/detailPresentation";
|
||||
import { getDetailMeta, getProductDetailSections, productKey } from "@/lib/data";
|
||||
import { goBack, goBooking } from "@/lib/navigation";
|
||||
import { findProduct, isProductFavorite, loadAppData, rememberProduct, toggleFavoriteProduct } from "@/lib/store";
|
||||
import type { Product } from "@/lib/types";
|
||||
|
||||
const product = ref<Product>(findProduct());
|
||||
|
||||
onLoad((query) => {
|
||||
product.value = findProduct(typeof query?.product === "string" ? decodeURIComponent(query.product) : undefined);
|
||||
rememberProduct(product.value);
|
||||
});
|
||||
|
||||
onShow(() => {
|
||||
void loadAppData();
|
||||
});
|
||||
|
||||
onShareAppMessage(() => ({
|
||||
title: presentation.value.title,
|
||||
path: `/pages/detail/index?product=${encodeURIComponent(productKey(product.value))}`,
|
||||
imageUrl: product.value.image,
|
||||
}));
|
||||
|
||||
const meta = computed(() => getDetailMeta(product.value));
|
||||
const presentation = computed(() => createDetailPresentation(product.value));
|
||||
const detailSections = computed(() => getProductDetailSections(product.value, meta.value));
|
||||
const favorite = computed(() => isProductFavorite(product.value));
|
||||
|
||||
const introParagraphs = computed(() => getSectionTexts(["overview", "itinerary"], presentation.value.intro));
|
||||
const priceParagraphs = computed(() => getSectionTexts(["service", "price"], "最终价格会根据出行日期、人数、酒店和体验资源确认后生成。"));
|
||||
|
||||
function getSectionTexts(keys: string[], fallback: string) {
|
||||
const paragraphs = detailSections.value
|
||||
.filter((section) => keys.includes(section.key))
|
||||
.flatMap((section) => section.blocks)
|
||||
.filter((block): block is { type: "text"; text: string } => block.type === "text")
|
||||
.map((block) => block.text.trim())
|
||||
.filter(Boolean);
|
||||
return paragraphs.length ? paragraphs : [fallback];
|
||||
}
|
||||
|
||||
function toggleFavorite() {
|
||||
const wasFavorite = favorite.value;
|
||||
toggleFavoriteProduct(product.value);
|
||||
uni.showToast({ title: wasFavorite ? "已取消收藏" : "已收藏", icon: "none" });
|
||||
}
|
||||
|
||||
|
||||
|
||||
function goCall() {
|
||||
uni.makePhoneCall({ phoneNumber: '13800000000' });
|
||||
}
|
||||
|
||||
function goConcierge() {
|
||||
selectedAdvisor.value = conciergeAdvisor;
|
||||
}
|
||||
|
||||
function handleConciergeClosed() {
|
||||
selectedAdvisor.value = null;
|
||||
}
|
||||
|
||||
const conciergeAdvisor: ConciergeAdvisor = {
|
||||
avatar: "https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&fm=jpg&q=80&w=240&h=240",
|
||||
name: "Amanda",
|
||||
role: "SENIOR TRAVEL ADVISOR",
|
||||
qrImage: "https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=WonderQ%20Concierge",
|
||||
details: [
|
||||
{ icon: "calendar", label: "服务经验:8年" },
|
||||
{ icon: "navigate", label: "擅长领域:瀑降 / 探洞" },
|
||||
],
|
||||
};
|
||||
|
||||
const selectedAdvisor = ref<ConciergeAdvisor | null>(null);
|
||||
</script>
|
||||
@@ -9,15 +9,21 @@
|
||||
1. `backend-api-service.md`
|
||||
2. `backend-plan.md`
|
||||
3. `admin-api-requirements.md`
|
||||
4. `module-config-api.md`
|
||||
5. `public-api.md`
|
||||
4. `wanfa-api.md`
|
||||
5. `concierge-api.md`
|
||||
6. `detail-api.md`
|
||||
7. `module-config-api.md`
|
||||
8. `public-api.md`
|
||||
|
||||
管理前端开发:
|
||||
|
||||
1. `integration-workflow.md`
|
||||
2. `admin-api-requirements.md`
|
||||
3. `module-config-api.md`
|
||||
4. `development-status.md`
|
||||
3. `wanfa-api.md`
|
||||
4. `concierge-api.md`
|
||||
5. `detail-api.md`
|
||||
6. `module-config-api.md`
|
||||
7. `development-status.md`
|
||||
|
||||
MiniAPP 前台开发:
|
||||
|
||||
@@ -36,12 +42,18 @@ MiniAPP 前台开发:
|
||||
| `backend-plan.md` | 当前 FastAPI 后端定位、业务模块、近期优先级和安全部署原则 | 后端 |
|
||||
| `admin-api-requirements.md` | Admin UI 必需的 Admin API 主契约 | 后端、管理前端 |
|
||||
| `module-config-api.md` | 页面模块配置 CRUD 的唯一细节契约 | 后端、管理前端 |
|
||||
| `wanfa-api.md` | 玩法分类和路线管理 API 的补充契约 | 后端、管理前端 |
|
||||
| `concierge-api.md` | 管家顾问资料管理 API 的补充契约 | 后端、管理前端 |
|
||||
| `detail-api.md` | 详情展示内容管理 API 的补充契约 | 后端、管理前端 |
|
||||
| `public-api.md` | MiniAPP 对接后端的 Public API 契约 | 后端、MiniAPP |
|
||||
|
||||
## 文档边界
|
||||
|
||||
- `public-api.md` 是 MiniAPP 对接后端的唯一 Public API 契约。
|
||||
- `admin-api-requirements.md` 是 Admin UI 对接后端的主契约。
|
||||
- `wanfa-api.md` 是玩法分类和路线管理的 Admin API 补充契约。
|
||||
- `concierge-api.md` 是管家顾问资料管理的 Admin API 补充契约。
|
||||
- `detail-api.md` 是详情展示内容管理的 Admin API 补充契约。
|
||||
- `module-config-api.md` 是页面模块 CRUD 细节的唯一权威文档。
|
||||
- `integration-workflow.md` 只写联调流程,不重复接口字段。
|
||||
- `development-status.md` 只记录当前状态,不替代测试结果。
|
||||
@@ -51,4 +63,4 @@ MiniAPP 前台开发:
|
||||
|
||||
- 文档示例不得写入真实 Token、JWT secret、客服链接、企业 ID、手机号或生产环境变量值。
|
||||
- `.env`、`.env.local` 和生产配置不进入文档目录。
|
||||
- 涉及重置数据、发布、回滚、迁移或生产操作的文档,需要明确风险和验证方式。
|
||||
- 涉及重置数据、发布、回滚、迁移或生产操作的文档,需要明确风险和验证方式。
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
|
||||
本文档描述 `WonderQ-Admin-UI` 当前使用的 Admin API。接口负责站点内容维护、素材、发布和需求线索管理。
|
||||
|
||||
玩法分类和路线管理的字段、嵌套路由、排序与删除约束见 [wanfa-api.md](./wanfa-api.md)。该文档是本主契约的玩法领域补充,适用端为 `WonderQ-Admin` 和 `WonderQ-Admin-UI`。
|
||||
|
||||
管家顾问资料管理的字段、图片、排序与删除约束见 [concierge-api.md](./concierge-api.md)。该文档是本主契约的管家领域补充,适用端为 `WonderQ-Admin` 和 `WonderQ-Admin-UI`。
|
||||
|
||||
详情展示内容的字段、图片、排序与商品领域隔离约束见 [detail-api.md](./detail-api.md)。该文档是本主契约的详情领域补充,适用端为 `WonderQ-Admin` 和 `WonderQ-Admin-UI`。
|
||||
|
||||
## 通用约定
|
||||
|
||||
- API 前缀:`/api/admin`。
|
||||
|
||||
301
docs/concierge-api.md
Normal file
301
docs/concierge-api.md
Normal file
@@ -0,0 +1,301 @@
|
||||
# 管家管理 Admin API
|
||||
|
||||
> 适用端:`WonderQ-Admin`、`WonderQ-Admin-UI`。
|
||||
>
|
||||
> 状态:待实现契约。本文件参考 `WonderQ-MiniAPP/src/pages/concierge/components/conciergeTypes.ts` 定义管家顾问资料,以及管理端需要的查询、维护和排序接口。它不是 MiniAPP Public API 文档。
|
||||
|
||||
## 领域边界
|
||||
|
||||
管家管理只维护管家顾问卡片资料:
|
||||
|
||||
- 头像、姓名和职位。
|
||||
- 服务详情条目,包括图标和说明文案。
|
||||
- 添加管家时展示的二维码。
|
||||
- 顾问启用状态和展示顺序。
|
||||
|
||||
本接口不负责:
|
||||
|
||||
- 需求线索、客户、客服会话或登录。
|
||||
- 订单、预订、商品和商品图片关联。
|
||||
- 管家页面 Hero 文案和服务原则内容。
|
||||
|
||||
当前 `WonderQ-MiniAPP/src/pages/concierge/index.vue` 中的 Hero、原则和顾问数据仍是本地静态内容;本契约落地后,Admin UI 维护顾问数据,MiniAPP 再通过独立的 Public API 消费已发布内容。
|
||||
|
||||
## 接口清单
|
||||
|
||||
API 前缀为 `/api/admin`,除登录接口外均需要后台 JWT。
|
||||
|
||||
| 方法 | 路径 | 用途 |
|
||||
| --- | --- | --- |
|
||||
| `GET` | `/api/admin/concierge/advisors` | 获取全部管家顾问 |
|
||||
| `POST` | `/api/admin/concierge/advisors` | 新增管家顾问 |
|
||||
| `PATCH` | `/api/admin/concierge/advisors/{advisorId}` | 编辑管家顾问 |
|
||||
| `DELETE` | `/api/admin/concierge/advisors/{advisorId}` | 删除管家顾问 |
|
||||
| `PATCH` | `/api/admin/concierge/advisors/reorder` | 调整管家顾问展示顺序 |
|
||||
|
||||
## 通用约定
|
||||
|
||||
- 请求和响应使用 JSON,字段使用 camelCase。
|
||||
- 所有管理接口需要 `Authorization: Bearer <admin-jwt>`。
|
||||
- `GET` 默认返回启用和停用的全部顾问,按 `sortOrder` 升序返回,供管理端完整维护。
|
||||
- 创建、编辑、删除和排序成功后写入审计日志,再提交事务。
|
||||
- 变更接口返回最新顾问对象;排序接口返回排序后的 `items`。
|
||||
- ID 由后端生成并作为非空字符串返回。
|
||||
- 空列表返回 `[]`,不能返回 `null` 或省略字段。
|
||||
- 失败响应沿用 Admin API 约定,包含 `message`、`code` 和可选的 `details`。
|
||||
|
||||
## 数据类型
|
||||
|
||||
`conciergeTypes.ts` 是 MiniAPP 渲染模型,当前没有 `id`、`isActive` 和 `sortOrder`。为了支持 Admin UI 编辑、删除和排序,Admin API 在渲染字段之外增加管理元数据;MiniAPP Public API 适配时可以移除这些管理字段。
|
||||
|
||||
```ts
|
||||
type ConciergeDetail = {
|
||||
icon: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
type ConciergeAdvisorContent = {
|
||||
avatar: string;
|
||||
name: string;
|
||||
role: string;
|
||||
details: ConciergeDetail[];
|
||||
qrImage: string;
|
||||
};
|
||||
|
||||
type ConciergeAdvisorRecord = ConciergeAdvisorContent & {
|
||||
id: string;
|
||||
isActive: boolean;
|
||||
sortOrder: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
type ConciergeAdvisorCreate = {
|
||||
avatar: string;
|
||||
name: string;
|
||||
role: string;
|
||||
details: ConciergeDetail[];
|
||||
qrImage: string;
|
||||
isActive?: boolean;
|
||||
sortOrder?: number;
|
||||
};
|
||||
|
||||
type ConciergeAdvisorPatch = Partial<ConciergeAdvisorCreate>;
|
||||
|
||||
type ConciergeAdvisorListResponse = {
|
||||
advisors: ConciergeAdvisorRecord[];
|
||||
};
|
||||
|
||||
type ConciergeReorderRequest = {
|
||||
itemIds: string[];
|
||||
};
|
||||
```
|
||||
|
||||
## 字段约束
|
||||
|
||||
| 字段 | 类型 | 必填 | 约束和用途 |
|
||||
| --- | --- | --- | --- |
|
||||
| `id` | `string` | 响应必填 | 顾问稳定标识,由后端生成。Admin UI 不使用姓名作为编辑、删除或 React `key`。 |
|
||||
| `avatar` | `string` | 是 | 顾问头像 URL,前台按圆形头像展示。 |
|
||||
| `name` | `string` | 是 | 顾问姓名,去除首尾空白后不得为空。 |
|
||||
| `role` | `string` | 是 | 顾问职位或英文职称,去除首尾空白后不得为空。 |
|
||||
| `details` | `ConciergeDetail[]` | 是 | 服务详情列表,保留数组顺序;允许为空数组。 |
|
||||
| `details[].icon` | `string` | 是 | `uni-icons` 使用的图标名称,例如 `calendar`、`navigate`。 |
|
||||
| `details[].label` | `string` | 是 | 服务详情文案,去除首尾空白后不得为空。 |
|
||||
| `qrImage` | `string` | 是 | 添加管家时展示的二维码图片 URL。接口只保存图片 URL,不保存二维码原始 payload。 |
|
||||
| `isActive` | `boolean` | 响应必填 | 是否在已发布前台内容中展示,创建默认 `true`。 |
|
||||
| `sortOrder` | `number` | 响应必填 | 非负整数,数值越小越靠前;创建时未传则追加到末尾。 |
|
||||
| `createdAt` | `string` | 响应必填 | ISO 8601 创建时间。 |
|
||||
| `updatedAt` | `string` | 响应必填 | ISO 8601 最后更新时间。 |
|
||||
|
||||
头像和二维码应使用已上传素材的最终 URL。服务端应校验必填文本、URL 格式、`details` 数组结构和 `sortOrder` 非负整数;具体文本最大长度由后端 schema 统一定义,并同步到 Admin UI 表单校验。
|
||||
|
||||
## 接口详情
|
||||
|
||||
### 获取全部顾问
|
||||
|
||||
```http
|
||||
GET /api/admin/concierge/advisors
|
||||
Authorization: Bearer <admin-jwt>
|
||||
```
|
||||
|
||||
成功响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"advisors": [
|
||||
{
|
||||
"id": "advisor-001",
|
||||
"avatar": "https://example.test/assets/advisor-avatar.jpg",
|
||||
"name": "示例顾问",
|
||||
"role": "SENIOR TRAVEL ADVISOR",
|
||||
"details": [
|
||||
{ "icon": "calendar", "label": "服务经验:8年" },
|
||||
{ "icon": "navigate", "label": "擅长领域:自然探索" }
|
||||
],
|
||||
"qrImage": "https://example.test/assets/advisor-qr.png",
|
||||
"isActive": true,
|
||||
"sortOrder": 0,
|
||||
"createdAt": "2026-01-01T00:00:00Z",
|
||||
"updatedAt": "2026-01-01T00:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 新增顾问
|
||||
|
||||
```http
|
||||
POST /api/admin/concierge/advisors
|
||||
Authorization: Bearer <admin-jwt>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
请求:
|
||||
|
||||
```json
|
||||
{
|
||||
"avatar": "https://example.test/assets/advisor-avatar.jpg",
|
||||
"name": "示例顾问",
|
||||
"role": "SENIOR TRAVEL ADVISOR",
|
||||
"details": [
|
||||
{ "icon": "calendar", "label": "服务经验:8年" },
|
||||
{ "icon": "navigate", "label": "擅长领域:自然探索" }
|
||||
],
|
||||
"qrImage": "https://example.test/assets/advisor-qr.png",
|
||||
"isActive": true
|
||||
}
|
||||
```
|
||||
|
||||
成功返回 `201` 和新建的 `ConciergeAdvisorRecord`。未传 `sortOrder` 时追加到当前最大顺序之后。
|
||||
|
||||
### 编辑顾问
|
||||
|
||||
```http
|
||||
PATCH /api/admin/concierge/advisors/{advisorId}
|
||||
Authorization: Bearer <admin-jwt>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
请求体为 `ConciergeAdvisorPatch`,只更新提交的字段。例如只更新服务详情:
|
||||
|
||||
```json
|
||||
{
|
||||
"details": [
|
||||
{ "icon": "calendar", "label": "服务经验:10年" },
|
||||
{ "icon": "navigate", "label": "擅长领域:亲子与自然探索" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
成功返回更新后的 `ConciergeAdvisorRecord`。不存在的顾问返回 `404 CONCIERGE_ADVISOR_NOT_FOUND`。
|
||||
|
||||
### 删除顾问
|
||||
|
||||
```http
|
||||
DELETE /api/admin/concierge/advisors/{advisorId}
|
||||
Authorization: Bearer <admin-jwt>
|
||||
```
|
||||
|
||||
删除成功返回:
|
||||
|
||||
```json
|
||||
{ "id": "advisor-001" }
|
||||
```
|
||||
|
||||
删除后应重新规范化剩余顾问的 `sortOrder`,从 `0` 开始连续编号。若业务要求至少保留一名启用顾问,服务端在删除最后一名启用顾问时返回 `409 CONCIERGE_LAST_ACTIVE_ADVISOR`,否则允许删除并由前台处理空状态。
|
||||
|
||||
### 调整顾问顺序
|
||||
|
||||
```http
|
||||
PATCH /api/admin/concierge/advisors/reorder
|
||||
Authorization: Bearer <admin-jwt>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
请求必须完整包含当前全部顾问 ID,不能重复:
|
||||
|
||||
```json
|
||||
{ "itemIds": ["advisor-002", "advisor-001"] }
|
||||
```
|
||||
|
||||
成功响应:
|
||||
|
||||
```json
|
||||
{ "items": [] }
|
||||
```
|
||||
|
||||
其中 `items` 为更新 `sortOrder` 后的 `ConciergeAdvisorRecord[]`。缺少 ID、包含未知 ID 或出现重复 ID 时返回 `400 CONCIERGE_REORDER_INVALID`。
|
||||
|
||||
## 图片与素材
|
||||
|
||||
Admin UI 使用现有素材上传接口获取图片 URL:
|
||||
|
||||
```http
|
||||
POST /api/admin/media-assets/upload
|
||||
```
|
||||
|
||||
建议管家页面上传时使用 `group=concierge`,头像和二维码分别将返回的 `url` 写入 `avatar` 和 `qrImage`。API 不接受 base64 图片,也不在管家表中复制图片二进制内容。
|
||||
|
||||
二维码必须作为图片 URL 保存,不能把真实个人微信号、手机号或二维码 payload 写入文档、前端类型或接口日志。
|
||||
|
||||
## Admin UI 对接要求
|
||||
|
||||
Admin UI 应按以下方式调用:
|
||||
|
||||
1. 进入管家页面时调用 `GET /api/admin/concierge/advisors`,按 `sortOrder` 渲染全部顾问。
|
||||
2. 列表展示头像、姓名、职位、详情数量、启用状态和编辑/删除操作。
|
||||
3. 新增和编辑表单维护头像、姓名、职位、详情列表、二维码和启用状态。
|
||||
4. `details` 使用可增删的重复字段编辑器,提交时保留用户排列顺序,不把多个详情拼成一个字符串。
|
||||
5. 上移或下移顾问时提交完整顾问 ID 列表,不直接修改本地 `sortOrder` 后假设保存成功。
|
||||
6. 删除前要求二次确认;处理最后一名启用顾问的 `409` 提示。
|
||||
7. 处理 `401`、`404`、`409`、`422` 和 `5xx`,保存或排序请求进行中禁用重复提交。
|
||||
8. 图片上传失败时不得提交旧草稿中的空 URL;表单应保留其他已填写字段,允许用户重试上传。
|
||||
|
||||
建议的 Admin UI API 封装函数:
|
||||
|
||||
```ts
|
||||
getConciergeAdvisors();
|
||||
createConciergeAdvisor(input: ConciergeAdvisorCreate);
|
||||
updateConciergeAdvisor(advisorId: string, input: ConciergeAdvisorPatch);
|
||||
deleteConciergeAdvisor(advisorId: string);
|
||||
reorderConciergeAdvisors(itemIds: string[]);
|
||||
```
|
||||
|
||||
## 与当前 MiniAPP 类型的映射
|
||||
|
||||
当前 `conciergeTypes.ts` 的 `ConciergeAdvisor` 仅包含前台渲染字段:
|
||||
|
||||
```ts
|
||||
type ConciergeAdvisor = {
|
||||
avatar: string;
|
||||
name: string;
|
||||
role: string;
|
||||
details: Array<{ icon: string; label: string }>;
|
||||
qrImage: string;
|
||||
};
|
||||
```
|
||||
|
||||
Admin API 返回的 `ConciergeAdvisorRecord` 可以通过以下方式映射为前台模型:
|
||||
|
||||
```ts
|
||||
const advisor: ConciergeAdvisor = {
|
||||
avatar: record.avatar,
|
||||
name: record.name,
|
||||
role: record.role,
|
||||
details: record.details,
|
||||
qrImage: record.qrImage,
|
||||
};
|
||||
```
|
||||
|
||||
只有 `isActive === true` 的记录进入已发布 Public 内容;`id`、`sortOrder`、`createdAt` 和 `updatedAt` 属于管理元数据,不应要求前台组件展示。
|
||||
|
||||
## 后端落地边界
|
||||
|
||||
本契约落地时需要由 `WonderQ-Admin` 补充对应 ORM 模型、迁移、schema、路由、序列化和审计日志;由 `WonderQ-Admin-UI` 补充 API 类型、请求封装、管家列表、表单和排序交互。当前后端没有 `/api/admin/concierge/*` 路由,Admin UI 的管家入口仍是空态,MiniAPP 顾问数据仍在 `index.vue` 中静态定义。
|
||||
|
||||
相关文档:
|
||||
|
||||
- [Admin API 主契约](./admin-api-requirements.md)
|
||||
- [玩法管理 Admin API](./wanfa-api.md)
|
||||
- [管家数据类型](../WonderQ-MiniAPP/src/pages/concierge/components/conciergeTypes.ts)
|
||||
294
docs/detail-api.md
Normal file
294
docs/detail-api.md
Normal file
@@ -0,0 +1,294 @@
|
||||
# 详情展示管理 Admin API
|
||||
|
||||
> 适用端:`WonderQ-Admin`、`WonderQ-Admin-UI`。
|
||||
>
|
||||
> 状态:待实现契约。本文件参考 `WonderQ-MiniAPP/src/pages/detail/components/detailPresentation.ts` 定义详情页展示数据。当前 `detailPresentation.ts` 仍依赖已移除的 `Product` 类型,后端也没有详情管理接口;本契约采用与商品领域解耦的详情展示模型,不恢复 Product、ProductImage 或订单关联。
|
||||
|
||||
## 领域边界
|
||||
|
||||
详情管理只维护详情页可编辑的展示内容:
|
||||
|
||||
- 详情页识别键、标题眉标、出行时长和标题文案。
|
||||
- 详情介绍、行程亮点、费用包含、费用不含和注意事项。
|
||||
- 详情页图片画廊及图片顺序。
|
||||
- 启用状态和详情列表顺序。
|
||||
|
||||
本接口不负责:
|
||||
|
||||
- 商品、商品价格、商品库存或商品详情表。
|
||||
- Product、ProductImage 或任何商品外键。
|
||||
- 订单、预订、收藏、评价或线索。
|
||||
- 详情页底部的电话、管家联系和预订动作。
|
||||
|
||||
`key` 是详情展示内容自己的稳定业务键,不得设计为 Product ID 外键。详情页如何从玩法、页面入口或其他前台上下文定位 `key`,由前台导航契约另行约定。
|
||||
|
||||
## 与 `detailPresentation.ts` 的关系
|
||||
|
||||
`detailPresentation.ts` 当前是前台展示适配器,不是持久化模型:
|
||||
|
||||
- `eyebrow`、`duration`、`title`、`subtitle`、`intro`、`highlights`、`included`、`excluded`、`notes`、`gallery` 组成最终展示对象。
|
||||
- 当前实现从 `Product` 的标题、摘要、标签、详情区块和图片数组推导部分字段。
|
||||
- 当前实现对洞穴/探险路线生成另一组固定亮点,并使用固定费用说明和注意事项。
|
||||
- 当前实现会将主图、接口图片和 fallback 图片去重后截取前 6 张。
|
||||
|
||||
新的 Admin API 应直接维护最终展示字段;Admin UI 不应复刻这些推导逻辑,也不应依赖已移除的 Product 字段。
|
||||
|
||||
## 接口清单
|
||||
|
||||
API 前缀为 `/api/admin`,除登录接口外均需要后台 JWT。
|
||||
|
||||
| 方法 | 路径 | 用途 |
|
||||
| --- | --- | --- |
|
||||
| `GET` | `/api/admin/details` | 获取全部详情展示配置 |
|
||||
| `GET` | `/api/admin/details/{detailId}` | 获取单个详情展示配置 |
|
||||
| `POST` | `/api/admin/details` | 新增详情展示配置 |
|
||||
| `PATCH` | `/api/admin/details/{detailId}` | 编辑详情展示配置 |
|
||||
| `DELETE` | `/api/admin/details/{detailId}` | 删除详情展示配置 |
|
||||
| `PATCH` | `/api/admin/details/reorder` | 调整详情展示配置顺序 |
|
||||
|
||||
## 通用约定
|
||||
|
||||
- 请求和响应使用 JSON,字段使用 camelCase。
|
||||
- 所有管理接口需要 `Authorization: Bearer <admin-jwt>`。
|
||||
- `GET /details` 返回启用和停用的全部配置,按 `sortOrder` 升序返回。
|
||||
- 创建、编辑、删除和排序成功后写入审计日志,再提交事务。
|
||||
- 创建和编辑返回最新详情对象;排序接口返回排序后的 `items`。
|
||||
- ID 由后端生成并作为非空字符串返回;`key` 由调用方提供并保持稳定。
|
||||
- 空列表返回 `[]`,不能返回 `null` 或省略字段。
|
||||
- 失败响应沿用 Admin API 约定,包含 `message`、`code` 和可选的 `details`。
|
||||
|
||||
## 数据类型
|
||||
|
||||
以下内容字段与 `DetailPresentation` 保持兼容,管理端补充 `id`、`key`、状态和审计时间:
|
||||
|
||||
```ts
|
||||
type DetailPresentation = {
|
||||
eyebrow: string;
|
||||
duration: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
intro: string;
|
||||
highlights: string[];
|
||||
included: string[];
|
||||
excluded: string[];
|
||||
notes: string[];
|
||||
gallery: string[];
|
||||
};
|
||||
|
||||
type DetailRecord = DetailPresentation & {
|
||||
id: string;
|
||||
key: string;
|
||||
isActive: boolean;
|
||||
sortOrder: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
type DetailCreate = DetailPresentation & {
|
||||
key: string;
|
||||
isActive?: boolean;
|
||||
sortOrder?: number;
|
||||
};
|
||||
|
||||
type DetailPatch = Partial<DetailCreate>;
|
||||
|
||||
type DetailListResponse = {
|
||||
details: DetailRecord[];
|
||||
};
|
||||
|
||||
type DetailReorderRequest = {
|
||||
itemIds: string[];
|
||||
};
|
||||
```
|
||||
|
||||
## 字段约束
|
||||
|
||||
| 字段 | 类型 | 必填 | 约束和用途 |
|
||||
| --- | --- | --- | --- |
|
||||
| `id` | `string` | 响应必填 | 后端生成的记录 ID,仅供管理端识别记录。 |
|
||||
| `key` | `string` | 是 | 详情展示稳定键;建议使用小写字母、数字和中划线,例如 `classic-panorama`。不得关联 Product 表。 |
|
||||
| `eyebrow` | `string` | 是 | 详情页顶部眉标,例如“玩法推荐”。 |
|
||||
| `duration` | `string` | 是 | 展示用时长,例如“5天4晚”;接口保存最终文案,不要求前端从标题正则提取。 |
|
||||
| `title` | `string` | 是 | 详情页主标题。 |
|
||||
| `subtitle` | `string` | 是 | 详情页副标题或目的地说明。 |
|
||||
| `intro` | `string` | 是 | “详细介绍”区域的主介绍文案。 |
|
||||
| `highlights` | `string[]` | 是 | “行程亮点”列表,保留数组顺序。 |
|
||||
| `included` | `string[]` | 是 | “费用包含”列表,保留数组顺序。 |
|
||||
| `excluded` | `string[]` | 是 | “费用不含”列表,保留数组顺序。 |
|
||||
| `notes` | `string[]` | 是 | “注意事项”列表,保留数组顺序。 |
|
||||
| `gallery` | `string[]` | 是 | 详情图片 URL 列表,按展示顺序返回;建议最多 6 张以匹配当前前台逻辑。 |
|
||||
| `isActive` | `boolean` | 响应必填 | 是否进入已发布前台内容,创建默认 `true`。 |
|
||||
| `sortOrder` | `number` | 响应必填 | 非负整数,数值越小越靠前;创建时未传则追加到末尾。 |
|
||||
| `createdAt` | `string` | 响应必填 | ISO 8601 创建时间。 |
|
||||
| `updatedAt` | `string` | 响应必填 | ISO 8601 最后更新时间。 |
|
||||
|
||||
服务端应校验 `key` 唯一、文本字段去除首尾空白后不为空、数组元素为非空字符串、`gallery` 为 URL 列表、`sortOrder` 为非负整数。具体文本最大长度由后端 schema 统一定义,并同步到 Admin UI 表单校验。
|
||||
|
||||
## 接口详情
|
||||
|
||||
### 获取详情列表
|
||||
|
||||
```http
|
||||
GET /api/admin/details
|
||||
Authorization: Bearer <admin-jwt>
|
||||
```
|
||||
|
||||
成功响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"details": [
|
||||
{
|
||||
"id": "detail-001",
|
||||
"key": "classic-panorama",
|
||||
"eyebrow": "玩法推荐",
|
||||
"duration": "5天4晚",
|
||||
"title": "经典贵州全景",
|
||||
"subtitle": "贵州·瀑布、苗寨、古城与山地风光",
|
||||
"intro": "沿着贵州山地的自然纹理深入探索。",
|
||||
"highlights": ["核心景观串联", "小团出行,按同行人节奏调整"],
|
||||
"included": ["行程内用车与接送服务"],
|
||||
"excluded": ["往返大交通及个人消费"],
|
||||
"notes": ["贵州多山多雨,请准备防滑鞋和轻便雨具。"],
|
||||
"gallery": ["https://example.test/assets/detail-01.jpg"],
|
||||
"isActive": true,
|
||||
"sortOrder": 0,
|
||||
"createdAt": "2026-01-01T00:00:00Z",
|
||||
"updatedAt": "2026-01-01T00:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 获取单个详情
|
||||
|
||||
```http
|
||||
GET /api/admin/details/{detailId}
|
||||
Authorization: Bearer <admin-jwt>
|
||||
```
|
||||
|
||||
成功返回单个 `DetailRecord`。记录不存在返回 `404 DETAIL_NOT_FOUND`。
|
||||
|
||||
### 新增详情
|
||||
|
||||
```http
|
||||
POST /api/admin/details
|
||||
Authorization: Bearer <admin-jwt>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
请求体为 `DetailCreate`。成功返回 `201` 和新建的 `DetailRecord`;未传 `sortOrder` 时追加到当前列表末尾。
|
||||
|
||||
### 编辑详情
|
||||
|
||||
```http
|
||||
PATCH /api/admin/details/{detailId}
|
||||
Authorization: Bearer <admin-jwt>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
请求体为 `DetailPatch`,只更新提交的字段。修改 `key` 时仍须保证全局唯一。成功返回更新后的 `DetailRecord`;记录不存在返回 `404 DETAIL_NOT_FOUND`,`key` 冲突返回 `409 DETAIL_KEY_EXISTS`。
|
||||
|
||||
### 删除详情
|
||||
|
||||
```http
|
||||
DELETE /api/admin/details/{detailId}
|
||||
Authorization: Bearer <admin-jwt>
|
||||
```
|
||||
|
||||
成功返回:
|
||||
|
||||
```json
|
||||
{ "id": "detail-001" }
|
||||
```
|
||||
|
||||
删除后应重新规范化剩余记录的 `sortOrder`,从 `0` 开始连续编号。详情记录没有 Product、订单或线索外键,因此删除不触发跨领域级联操作。
|
||||
|
||||
### 调整详情顺序
|
||||
|
||||
```http
|
||||
PATCH /api/admin/details/reorder
|
||||
Authorization: Bearer <admin-jwt>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
请求必须完整包含当前全部详情 ID,不能重复:
|
||||
|
||||
```json
|
||||
{ "itemIds": ["detail-002", "detail-001"] }
|
||||
```
|
||||
|
||||
成功返回:
|
||||
|
||||
```json
|
||||
{ "items": [] }
|
||||
```
|
||||
|
||||
其中 `items` 为更新 `sortOrder` 后的 `DetailRecord[]`。缺少 ID、包含未知 ID 或出现重复 ID 时返回 `400 DETAIL_REORDER_INVALID`。
|
||||
|
||||
## 图片与素材
|
||||
|
||||
Admin UI 使用现有素材上传接口获取图片 URL:
|
||||
|
||||
```http
|
||||
POST /api/admin/media-assets/upload
|
||||
```
|
||||
|
||||
建议详情页图片使用 `group=detail`,将返回的 `url` 按用户排列顺序写入 `gallery`。接口只保存图片 URL,不创建 ProductImage 表或商品图片关联。
|
||||
|
||||
当前 `detailPresentation.ts` 的 fallback 图片属于前台兜底逻辑。Admin API 正式接入后,Admin UI 不应把 fallback 图片自动写入数据库;应由运营人员明确上传和排序详情图片。
|
||||
|
||||
## Admin UI 对接要求
|
||||
|
||||
Admin UI 应按以下方式调用:
|
||||
|
||||
1. 进入详情管理页时调用 `GET /api/admin/details`,按 `sortOrder` 渲染详情列表。
|
||||
2. 新增和编辑表单维护 `key`、眉标、时长、标题、副标题、介绍和四组列表文案。
|
||||
3. `highlights`、`included`、`excluded`、`notes` 使用可增删的重复字段编辑器,提交时保留数组顺序,不拼接成换行字符串。
|
||||
4. 使用图片上传接口维护 `gallery`,支持新增、删除和调整图片顺序。
|
||||
5. 上移或下移详情时提交完整详情 ID 列表,不直接修改本地 `sortOrder` 后假设保存成功。
|
||||
6. 删除前要求二次确认;删除成功后以接口返回或重新查询的数据更新列表。
|
||||
7. 处理 `401`、`404`、`409`、`422` 和 `5xx`,保存、上传或排序进行中禁用重复提交。
|
||||
8. 详情编辑器不得出现 Product ID、ProductImage ID、库存、订单或预订字段。
|
||||
|
||||
建议的 Admin UI API 封装函数:
|
||||
|
||||
```ts
|
||||
getDetails();
|
||||
getDetail(detailId: string);
|
||||
createDetail(input: DetailCreate);
|
||||
updateDetail(detailId: string, input: DetailPatch);
|
||||
deleteDetail(detailId: string);
|
||||
reorderDetails(itemIds: string[]);
|
||||
```
|
||||
|
||||
## 与前台展示模型的映射
|
||||
|
||||
Admin API 返回的 `DetailRecord` 可以映射为 `DetailPresentation`:
|
||||
|
||||
```ts
|
||||
const presentation: DetailPresentation = {
|
||||
eyebrow: record.eyebrow,
|
||||
duration: record.duration,
|
||||
title: record.title,
|
||||
subtitle: record.subtitle,
|
||||
intro: record.intro,
|
||||
highlights: record.highlights,
|
||||
included: record.included,
|
||||
excluded: record.excluded,
|
||||
notes: record.notes,
|
||||
gallery: record.gallery,
|
||||
};
|
||||
```
|
||||
|
||||
接入时应优先使用接口已保存的最终文案和图片顺序,不再依赖 `stripTitle`、标题时长正则、洞穴路线分支或 fallbackGallery 生成同一字段。
|
||||
|
||||
## 后端落地边界
|
||||
|
||||
本契约落地时需要由 `WonderQ-Admin` 补充对应 ORM 模型、迁移、schema、路由、序列化和审计日志;由 `WonderQ-Admin-UI` 补充 API 类型、请求封装、详情列表、编辑器、图片管理和排序交互。当前后端没有 `/api/admin/details` 路由,Admin UI 没有详情管理入口,`detailPresentation.ts` 也不是可直接作为后端契约的完整类型来源。
|
||||
|
||||
相关文档:
|
||||
|
||||
- [Admin API 主契约](./admin-api-requirements.md)
|
||||
- [管家管理 Admin API](./concierge-api.md)
|
||||
- [详情展示适配器](../WonderQ-MiniAPP/src/pages/detail/components/detailPresentation.ts)
|
||||
0
docs/guanjia-api.md
Normal file
0
docs/guanjia-api.md
Normal file
0
docs/home-api.md
Normal file
0
docs/home-api.md
Normal file
325
docs/wanfa-api.md
Normal file
325
docs/wanfa-api.md
Normal file
@@ -0,0 +1,325 @@
|
||||
# 玩法管理 Admin API
|
||||
|
||||
> 适用端:`WonderQ-Admin`、`WonderQ-Admin-UI`。
|
||||
>
|
||||
> 状态:待实现契约。本文件参考 `WonderQ-MiniAPP/src/pages/play/components/playData.ts` 定义玩法分类和路线的数据结构,以及管理端需要的查询、维护和排序接口。它不是 MiniAPP Public API 文档。
|
||||
|
||||
## 领域边界
|
||||
|
||||
玩法管理只维护玩法分类和路线卡片内容:
|
||||
|
||||
- 分类名称和展示顺序。
|
||||
- 路线标题、副标题、封面、路线数量和需求关键词。
|
||||
- 分类与路线的新增、编辑、删除和排序。
|
||||
|
||||
玩法领域不负责:
|
||||
|
||||
- 商品、商品详情、预订或订单。
|
||||
- 目的地实体和目的地维护。
|
||||
- 线索创建或线索跟进。
|
||||
- Product、ProductImage 或其他已移除商品关联表。
|
||||
|
||||
`routeCount` 只是前台卡片展示数量,不是 Product 表的外键或实时关联统计。
|
||||
|
||||
## 接口清单
|
||||
|
||||
API 前缀为 `/api/admin`,除登录接口外均需要后台 JWT。
|
||||
|
||||
| 方法 | 路径 | 用途 |
|
||||
| --- | --- | --- |
|
||||
| `GET` | `/api/admin/wanfa/categories` | 获取全部玩法分类及其路线 |
|
||||
| `POST` | `/api/admin/wanfa/categories` | 新增玩法分类 |
|
||||
| `PATCH` | `/api/admin/wanfa/categories/{categoryId}` | 编辑玩法分类 |
|
||||
| `DELETE` | `/api/admin/wanfa/categories/{categoryId}` | 删除玩法分类 |
|
||||
| `PATCH` | `/api/admin/wanfa/categories/reorder` | 调整玩法分类顺序 |
|
||||
| `POST` | `/api/admin/wanfa/categories/{categoryId}/routes` | 新增分类路线 |
|
||||
| `PATCH` | `/api/admin/wanfa/categories/{categoryId}/routes/{routeId}` | 编辑分类路线 |
|
||||
| `DELETE` | `/api/admin/wanfa/categories/{categoryId}/routes/{routeId}` | 删除分类路线 |
|
||||
| `PATCH` | `/api/admin/wanfa/categories/{categoryId}/routes/reorder` | 调整分类内路线顺序 |
|
||||
|
||||
## 通用约定
|
||||
|
||||
- 请求和响应使用 JSON,字段使用 camelCase。
|
||||
- 所有管理接口需要 `Authorization: Bearer <admin-jwt>`。
|
||||
- 创建、编辑、删除和排序成功后写入审计日志,再提交事务。
|
||||
- 变更接口返回最新变更对象;排序接口返回排序后的 `items`。
|
||||
- ID 由后端生成并作为非空字符串返回。初始化迁移时应优先保留 `playData.ts` 中已有的稳定 ID。
|
||||
- 分类和路线的数组顺序就是管理端和前台的展示顺序,接口不要求前端依赖 `sortOrder` 字段。
|
||||
- 空集合返回 `[]`,不能返回 `null` 或省略字段。
|
||||
- 失败响应沿用 Admin API 约定,包含 `message`、`code` 和可选的 `details`。
|
||||
|
||||
## 数据类型
|
||||
|
||||
以下类型与 `playData.ts` 保持字段兼容。管理端接口不应向前台模型强制增加商品 ID、预订 ID 或数据库关联字段。
|
||||
|
||||
```ts
|
||||
type WanfaConfig = {
|
||||
categories: WanfaCategory[];
|
||||
};
|
||||
|
||||
type WanfaCategory = {
|
||||
id: string;
|
||||
label: string;
|
||||
routes: WanfaRoute[];
|
||||
};
|
||||
|
||||
type WanfaRoute = {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
image: string;
|
||||
routeCount: number;
|
||||
demandKeyword: string;
|
||||
};
|
||||
|
||||
type WanfaCategoryCreate = {
|
||||
label: string;
|
||||
};
|
||||
|
||||
type WanfaCategoryPatch = {
|
||||
label?: string;
|
||||
};
|
||||
|
||||
type WanfaRouteCreate = {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
image: string;
|
||||
routeCount: number;
|
||||
demandKeyword: string;
|
||||
};
|
||||
|
||||
type WanfaRoutePatch = Partial<WanfaRouteCreate>;
|
||||
|
||||
type WanfaReorderRequest = {
|
||||
itemIds: string[];
|
||||
};
|
||||
```
|
||||
|
||||
## 字段约束
|
||||
|
||||
| 字段 | 类型 | 必填 | 约束和用途 |
|
||||
| --- | --- | --- | --- |
|
||||
| `category.id` | `string` | 响应必填 | 分类稳定标识,例如 `family-route`。创建时由后端生成。 |
|
||||
| `category.label` | `string` | 是 | 左侧分类显示名称,去除首尾空白后不得为空。 |
|
||||
| `category.routes` | `WanfaRoute[]` | 响应必填 | 当前分类下的路线,按展示顺序返回。 |
|
||||
| `route.id` | `string` | 响应必填 | 路线稳定标识,例如 `family-water`。创建时由后端生成。 |
|
||||
| `route.title` | `string` | 是 | 路线卡片标题,去除首尾空白后不得为空。 |
|
||||
| `route.subtitle` | `string` | 是 | 路线卡片副标题或目的地说明。 |
|
||||
| `route.image` | `string` | 是 | 可直接用于图片组件的封面 URL。 |
|
||||
| `route.routeCount` | `number` | 是 | 卡片右下角展示的路线数量,服务端应校验为不小于 0 的整数。 |
|
||||
| `route.demandKeyword` | `string` | 是 | 跳转需求页时可使用的预填关键词,去除首尾空白后不得为空。 |
|
||||
|
||||
字段最大长度应由后端 schema 统一定义,并同步到 Admin UI 表单校验;在未形成统一长度常量前,前端不能通过截断文本代替服务端校验。
|
||||
|
||||
## 接口详情
|
||||
|
||||
### 获取分类及路线
|
||||
|
||||
```http
|
||||
GET /api/admin/wanfa/categories
|
||||
Authorization: Bearer <admin-jwt>
|
||||
```
|
||||
|
||||
成功响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"categories": [
|
||||
{
|
||||
"id": "family-route",
|
||||
"label": "亲子路线",
|
||||
"routes": [
|
||||
{
|
||||
"id": "family-water",
|
||||
"title": "亲子玩水",
|
||||
"subtitle": "贵州·轻松节奏与自然课堂",
|
||||
"image": "https://example.test/assets/family-water.jpg",
|
||||
"routeCount": 4,
|
||||
"demandKeyword": "亲子玩水"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 新增分类
|
||||
|
||||
```http
|
||||
POST /api/admin/wanfa/categories
|
||||
Authorization: Bearer <admin-jwt>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
请求:
|
||||
|
||||
```json
|
||||
{ "label": "亲子路线" }
|
||||
```
|
||||
|
||||
成功返回 `201` 和新分类对象,初始 `routes` 为 `[]`,并追加到分类列表末尾。
|
||||
|
||||
### 编辑分类
|
||||
|
||||
```http
|
||||
PATCH /api/admin/wanfa/categories/{categoryId}
|
||||
Authorization: Bearer <admin-jwt>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
请求只允许修改分类名称:
|
||||
|
||||
```json
|
||||
{ "label": "家庭路线" }
|
||||
```
|
||||
|
||||
成功返回更新后的 `WanfaCategory`。不存在的分类返回 `404 WANFA_CATEGORY_NOT_FOUND`。
|
||||
|
||||
### 删除分类
|
||||
|
||||
```http
|
||||
DELETE /api/admin/wanfa/categories/{categoryId}
|
||||
Authorization: Bearer <admin-jwt>
|
||||
```
|
||||
|
||||
为避免误删路线,分类仍包含路线时不得级联删除,应返回 `409 WANFA_CATEGORY_NOT_EMPTY`。删除前由 Admin UI 提示先移除分类内路线。删除成功返回:
|
||||
|
||||
```json
|
||||
{ "id": "family-route" }
|
||||
```
|
||||
|
||||
### 分类排序
|
||||
|
||||
```http
|
||||
PATCH /api/admin/wanfa/categories/reorder
|
||||
Authorization: Bearer <admin-jwt>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
请求必须完整包含当前全部分类 ID,不能重复:
|
||||
|
||||
```json
|
||||
{ "itemIds": ["photo-route", "family-route", "healing-route"] }
|
||||
```
|
||||
|
||||
成功返回:
|
||||
|
||||
```json
|
||||
{ "items": [] }
|
||||
```
|
||||
|
||||
其中 `items` 为排序后的 `WanfaCategory[]`。缺少 ID、包含未知 ID 或出现重复 ID 时返回 `400 WANFA_CATEGORY_REORDER_INVALID`。
|
||||
|
||||
### 新增路线
|
||||
|
||||
```http
|
||||
POST /api/admin/wanfa/categories/{categoryId}/routes
|
||||
Authorization: Bearer <admin-jwt>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
请求:
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "亲子玩水",
|
||||
"subtitle": "贵州·轻松节奏与自然课堂",
|
||||
"image": "https://example.test/assets/family-water.jpg",
|
||||
"routeCount": 4,
|
||||
"demandKeyword": "亲子玩水"
|
||||
}
|
||||
```
|
||||
|
||||
成功返回 `201` 和新建的 `WanfaRoute`,并追加到对应分类路线末尾。分类不存在返回 `404 WANFA_CATEGORY_NOT_FOUND`。
|
||||
|
||||
### 编辑路线
|
||||
|
||||
```http
|
||||
PATCH /api/admin/wanfa/categories/{categoryId}/routes/{routeId}
|
||||
Authorization: Bearer <admin-jwt>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
请求体为 `WanfaRoutePatch`,只更新提交的字段。成功返回更新后的 `WanfaRoute`;分类或路线不存在时分别返回 `404 WANFA_CATEGORY_NOT_FOUND` 或 `404 WANFA_ROUTE_NOT_FOUND`。
|
||||
|
||||
### 删除路线
|
||||
|
||||
```http
|
||||
DELETE /api/admin/wanfa/categories/{categoryId}/routes/{routeId}
|
||||
Authorization: Bearer <admin-jwt>
|
||||
```
|
||||
|
||||
成功返回 `{ "id": "..." }`。删除后,分类内路线保持原有相对顺序。
|
||||
|
||||
### 分类内路线排序
|
||||
|
||||
```http
|
||||
PATCH /api/admin/wanfa/categories/{categoryId}/routes/reorder
|
||||
Authorization: Bearer <admin-jwt>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
请求必须完整包含该分类当前全部路线 ID,不能重复:
|
||||
|
||||
```json
|
||||
{ "itemIds": ["family-grassland", "family-water", "family-village"] }
|
||||
```
|
||||
|
||||
成功返回排序后的路线:
|
||||
|
||||
```json
|
||||
{ "items": [] }
|
||||
```
|
||||
|
||||
分类不存在返回 `404 WANFA_CATEGORY_NOT_FOUND`;路线 ID 不完整、重复或不属于该分类时返回 `400 WANFA_ROUTE_REORDER_INVALID`。
|
||||
|
||||
## 当前本地数据映射
|
||||
|
||||
迁移初始数据时,分类和路线应按以下 ID 与顺序导入:
|
||||
|
||||
| 分类 ID | 分类名称 | 路线 ID(当前顺序) |
|
||||
| --- | --- | --- |
|
||||
| `family-route` | 亲子路线 | `family-water`、`family-village`、`family-grassland` |
|
||||
| `photo-route` | 旅拍路线 | `miao-photo`、`peak-photo`、`terrace-photo` |
|
||||
| `healing-route` | 疗愈路线 | `mountain-healing`、`hot-spring-healing`、`river-healing` |
|
||||
| `team-building` | 团建 | `team-challenge`、`team-stream`、`team-culture` |
|
||||
| `guizhou-panorama` | 贵州全景 | `classic-panorama`、`mountain-panorama`、`wild-panorama` |
|
||||
| `private-custom` | 私人定制 | `private-family`、`private-business`、`private-wild` |
|
||||
|
||||
图片别名的当前解析规则位于 `playData.ts` 的 `routeImageByAsset` 和 `resolveRouteImage`:已配置别名解析为完整远程 URL,完整 `http` URL 直接使用,其他值按 `/assets/guizhou/{image}.jpg` 解析。Admin API 建议保存最终 URL,Admin UI 通过现有媒体上传接口获取 URL 后再提交 `image`。
|
||||
|
||||
## Admin UI 对接要求
|
||||
|
||||
Admin UI 应按以下方式调用:
|
||||
|
||||
1. 进入玩法管理页时调用 `GET /api/admin/wanfa/categories`,以返回数组顺序渲染分类和路线。
|
||||
2. 添加分类调用 `POST /categories`;编辑和删除分类分别调用对应 `PATCH`、`DELETE`。
|
||||
3. 添加、编辑和删除路线使用分类嵌套路由。
|
||||
4. 上移或下移分类时提交完整分类 ID 列表;上移或下移路线时提交完整路线 ID 列表。
|
||||
5. 每次变更成功后以接口返回数据更新本地状态;必要时重新请求列表,不直接拼接数据库字段。
|
||||
6. 删除非空分类前展示阻止性提示,不自动级联删除路线。
|
||||
7. 处理 `401`、`404`、`409`、`422` 和 `5xx`,并在保存中禁用重复提交。
|
||||
|
||||
建议的 Admin UI API 封装函数:
|
||||
|
||||
```ts
|
||||
getWanfaCategories();
|
||||
createWanfaCategory(input: WanfaCategoryCreate);
|
||||
updateWanfaCategory(categoryId: string, input: WanfaCategoryPatch);
|
||||
deleteWanfaCategory(categoryId: string);
|
||||
reorderWanfaCategories(itemIds: string[]);
|
||||
createWanfaRoute(categoryId: string, input: WanfaRouteCreate);
|
||||
updateWanfaRoute(categoryId: string, routeId: string, input: WanfaRoutePatch);
|
||||
deleteWanfaRoute(categoryId: string, routeId: string);
|
||||
reorderWanfaRoutes(categoryId: string, itemIds: string[]);
|
||||
```
|
||||
|
||||
## 后端落地边界
|
||||
|
||||
本契约落地时需要由 `WonderQ-Admin` 补充对应 ORM 模型、迁移、schema、路由、序列化和审计日志;由 `WonderQ-Admin-UI` 补充 API 类型、请求封装、表单和排序交互。接口实现前不要把 `playData.ts` 的静态数据误认为已存在的数据库或 API 数据。
|
||||
|
||||
相关文档:
|
||||
|
||||
- [Admin API 主契约](./admin-api-requirements.md)
|
||||
- [页面模块配置契约](./module-config-api.md)
|
||||
- [玩法本地数据](../WonderQ-MiniAPP/src/pages/play/components/playData.ts)
|
||||
Reference in New Issue
Block a user