refactor: 清理废弃模块并重构管理端与全端代码

- 移除所有废弃的旧首页配置模块,包括destinationHero、demand相关、HomeExperience、vehicleService等表结构与代码,新增数据库迁移删除遗留表
- 重构管理端后台布局为标准RuoYi风格,新增Sidebar、Navbar、SettingsDrawer等组件,替换旧的过渡菜单实现
- 调整移动端响应断点为768px,更新布局相关测试用例,新增布局组件测试
- 删除OperationsTools发布/重置页面,移除/tools菜单入口,清理对应的API调用与测试代码
- 重构玩法模块编辑器:替换旧的图片手动输入框为图片上传组件,移除冗余的取消按钮
- 新增线索查询offset参数支持,完善查询参数处理逻辑
- 优化rbac菜单构建逻辑,新增可见性控制参数
- 修正管家编辑器提示文案,优化系统资源编辑器表单初始化逻辑
- 清理小程序端废弃的体验推荐组件与相关测试代码
- 更新文档与种子脚本,匹配新的API契约与使用说明
This commit is contained in:
duanshuwen committed 2026-08-26 23:24:28 +08:00
1 parent f6c5ad589f
commit fbf2c772a8
63 files changed
+2272 -1991

No files matched your search

+170 -187
View File
@@ -2,63 +2,76 @@
import { computed, onMounted, ref } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import {
createHomePlayRecommendation,
createHomeTeamBuilding,
createHomeWildArchive,
createSiteConfigItem,
deleteHomePlayRecommendation,
deleteHomeTeamBuilding,
deleteHomeWildArchive,
deleteSiteConfigItem,
getHomePlayRecommendations,
getHomeTeamBuildings,
getHomeWildArchives,
getSiteConfig,
getWanfa,
reorderHomePlayRecommendations,
reorderHomeTeamBuildings,
reorderHomeWildArchives,
reorderSiteConfigItems,
updateHomePlayRecommendation,
updateHomeTeamBuilding,
updateHomeWildArchive,
updateSiteConfigItem,
} from "@/api/client";
import HomeContentEditor, { type HomeContentDraft, type HomeContentKind } from "@/components/home/HomeContentEditor.vue";
import SiteConfigEditor from "@/components/site-config/SiteConfigEditor.vue";
import {
compactSiteItemPayload,
createEmptySiteItemDraft,
getSiteModuleDefinition,
siteItemDraft,
siteModuleDefinitions,
validateSiteItemPayload,
} from "@/lib/site-config";
import { compactSiteItemPayload, createEmptySiteItemDraft, getSiteModuleDefinition, siteItemDraft, validateSiteItemPayload } from "@/lib/site-config";
import { useAuthStore } from "@/stores/auth";
import type { SiteConfigItem, SiteItemPatch, SiteModule } from "@/types";
import type { HomeTeamBuilding, HomeWanfaRecommendation, HomeWildArchive, SiteConfigItem, SiteItemPatch, WanfaCategory } from "@/types";
type SiteModule = "heroSlides" | "vehicleOptions";
type HomeModule = SiteModule | HomeContentKind;
const auth = useAuthStore();
const activeModule = ref<SiteModule>("heroSlides");
const moduleItems = ref<Record<SiteModule, SiteConfigItem[]>>({
heroSlides: [],
destinationHero: [],
demandHero: [],
demandFeatureCards: [],
demandForm: [],
vehicleOptions: [],
vehicleService: [],
});
const activeModule = ref<HomeModule>("heroSlides");
const siteItems = ref<Record<SiteModule, SiteConfigItem[]>>({ heroSlides: [], vehicleOptions: [] });
const homeItems = ref<Record<HomeContentKind, Array<HomeWanfaRecommendation | HomeTeamBuilding | HomeWildArchive>>>({ recommendation: [], team: [], wild: [] });
const categories = ref<WanfaCategory[]>([]);
const loading = ref(true);
const loadError = ref<string | null>(null);
const editorOpen = ref(false);
const siteEditorOpen = ref(false);
const editingId = ref<string | null>(null);
const draft = ref<SiteItemPatch>({});
const homeDraft = ref<HomeContentDraft>(emptyHomeDraft("recommendation"));
const siteDraft = ref<SiteItemPatch>({});
const saving = ref(false);
const deletingId = ref<string | null>(null);
const reorderingId = ref<string | null>(null);
const activeDefinition = computed(() => getSiteModuleDefinition(activeModule.value));
const activeItems = computed(() => moduleItems.value[activeModule.value]);
const isCreating = computed(() => editingId.value === null);
const canCreate = computed(() => activeDefinition.value.singleton ? activeItems.value.length === 0 : true);
const isSiteModule = computed(() => activeModule.value === "heroSlides" || activeModule.value === "vehicleOptions");
const activeSiteModule = computed(() => activeModule.value as SiteModule);
const activeHomeKind = computed(() => activeModule.value as HomeContentKind);
const activeSiteItems = computed(() => siteItems.value[activeSiteModule.value]);
const activeHomeItems = computed(() => homeItems.value[activeHomeKind.value]);
const activeItems = computed(() => isSiteModule.value ? activeSiteItems.value : activeHomeItems.value);
const activeDefinition = computed(() => getSiteModuleDefinition(activeSiteModule.value));
const creating = computed(() => editingId.value === null);
function emptyHomeDraft(kind: HomeContentKind): HomeContentDraft {
if (kind === "recommendation") return { categoryId: "", isActive: true, sortOrder: 0 };
if (kind === "team") return { tag: "", title: "", description: "", image: "", demandKeyword: "", detailSubtitle: "", detailParagraphs: [], isActive: true, sortOrder: 0 };
return { title: "", image: "", images: [], demandKeyword: "", isActive: true, sortOrder: 0 };
}
async function loadConfig() {
loading.value = true;
loadError.value = null;
try {
const config = await getSiteConfig();
moduleItems.value = {
heroSlides: config.heroSlides,
destinationHero: config.destinationHero,
demandHero: config.demandHero,
demandFeatureCards: config.demandFeatureCards,
demandForm: config.demandForm,
vehicleOptions: config.vehicleOptions,
vehicleService: config.vehicleService,
};
const [config, wanfa, recommendations, teamBuildings, wildArchives] = await Promise.all([getSiteConfig(), getWanfa(), getHomePlayRecommendations(), getHomeTeamBuildings(), getHomeWildArchives()]);
siteItems.value = { heroSlides: config.heroSlides, vehicleOptions: config.vehicleOptions };
categories.value = wanfa;
homeItems.value = { recommendation: recommendations.items, team: teamBuildings.items, wild: wildArchives.items };
} catch (error) {
loadError.value = error instanceof Error ? error.message : "首页配置加载失败";
} finally {
@@ -66,205 +79,175 @@ async function loadConfig() {
}
}
function startCreate() {
if (!auth.hasPermission("admin:site-config") || !canCreate.value) return;
editingId.value = null;
draft.value = createEmptySiteItemDraft(activeModule.value, activeItems.value.length);
editorOpen.value = true;
}
function startEdit(item: SiteConfigItem) {
if (!auth.hasPermission("admin:site-config")) return;
editingId.value = item.id;
draft.value = siteItemDraft(item);
editorOpen.value = true;
}
function closeEditor() {
if (saving.value) return;
editorOpen.value = false;
siteEditorOpen.value = false;
editingId.value = null;
draft.value = {};
homeDraft.value = emptyHomeDraft(activeHomeKind.value);
siteDraft.value = {};
}
async function saveItem() {
if (saving.value || !auth.hasPermission("admin:site-config")) return;
const payload = compactSiteItemPayload(activeModule.value, draft.value);
const validationError = validateSiteItemPayload(activeModule.value, payload);
if (validationError) {
ElMessage.warning(validationError);
return;
}
const primaryValue = activeDefinition.value.primaryField === "introTitle" ? payload.introTitle : payload.title;
if (!String(primaryValue ?? "").trim()) {
ElMessage.warning(`请先填写${activeDefinition.value.primaryField === "introTitle" ? "服务标题" : "标题"}`);
return;
function startCreate() {
if (!auth.hasPermission("admin:site-config")) return;
editingId.value = null;
if (isSiteModule.value) {
siteDraft.value = createEmptySiteItemDraft(activeSiteModule.value, activeSiteItems.value.length);
siteEditorOpen.value = true;
} else {
homeDraft.value = { ...emptyHomeDraft(activeHomeKind.value), sortOrder: activeHomeItems.value.length };
editorOpen.value = true;
}
}
function startEdit(item: SiteConfigItem | HomeWanfaRecommendation | HomeTeamBuilding | HomeWildArchive) {
if (!auth.hasPermission("admin:site-config")) return;
editingId.value = item.id;
if (isSiteModule.value) {
siteDraft.value = siteItemDraft(item as SiteConfigItem);
siteEditorOpen.value = true;
} else {
const kind = activeHomeKind.value;
if (kind === "recommendation") {
const recommendation = item as HomeWanfaRecommendation;
homeDraft.value = { categoryId: recommendation.categoryId, isActive: recommendation.isActive, sortOrder: recommendation.sortOrder };
} else if (kind === "team") {
const team = item as HomeTeamBuilding;
homeDraft.value = { tag: team.tag, title: team.title, description: team.description, image: team.image, demandKeyword: team.demandKeyword, detailSubtitle: team.detailSubtitle ?? "", detailParagraphs: [...(team.detailParagraphs ?? [])], isActive: team.isActive, sortOrder: team.sortOrder };
} else {
const wild = item as HomeWildArchive;
homeDraft.value = { title: wild.title, image: wild.image, images: [...(wild.images ?? [])], demandKeyword: wild.demandKeyword, isActive: wild.isActive, sortOrder: wild.sortOrder };
}
editorOpen.value = true;
}
}
async function saveSiteItem() {
const payload = compactSiteItemPayload(activeSiteModule.value, siteDraft.value);
const validationError = validateSiteItemPayload(activeSiteModule.value, payload);
if (validationError) return ElMessage.warning(validationError);
if (!String(payload.title ?? "").trim()) return ElMessage.warning("请先填写标题");
saving.value = true;
try {
const saved = editingId.value
? await updateSiteConfigItem(activeModule.value, editingId.value, payload)
: await createSiteConfigItem(activeModule.value, payload);
replaceItem(saved);
const saved = editingId.value ? await updateSiteConfigItem(activeSiteModule.value, editingId.value, payload) : await createSiteConfigItem(activeSiteModule.value, payload);
const next = siteItems.value[activeSiteModule.value].filter((item) => item.id !== saved.id);
siteItems.value[activeSiteModule.value] = [...next, saved].sort((left, right) => ("sortOrder" in left ? left.sortOrder : 0) - ("sortOrder" in right ? right.sortOrder : 0));
ElMessage.success(editingId.value ? "配置已保存" : "配置已新增");
closeEditor();
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : "配置保存失败");
} finally {
saving.value = false;
}
} catch (error) { ElMessage.error(error instanceof Error ? error.message : "配置保存失败"); } finally { saving.value = false; }
}
async function removeItem(item: SiteConfigItem) {
if (deletingId.value || !auth.hasPermission("admin:site-config")) return;
async function saveHomeItem() {
const kind = activeHomeKind.value;
if (kind === "recommendation" && !homeDraft.value.categoryId) return ElMessage.warning("请选择玩法分类");
if (kind !== "recommendation" && (!homeDraft.value.title?.trim() || !homeDraft.value.image?.trim() || !homeDraft.value.demandKeyword?.trim())) return ElMessage.warning("请填写标题、图片和需求关键词");
if (kind === "wild" && !homeDraft.value.images?.length) return ElMessage.warning("极境视界至少需要一张详情图片");
saving.value = true;
try {
await ElMessageBox.confirm(`确认删除「${itemTitle(item)}」吗?删除后会同步影响前台展示。`, "删除站点配置", { type: "warning" });
} catch {
return;
}
const saved = await saveHomeByKind(kind);
homeItems.value[kind] = [...activeHomeItems.value.filter((item) => item.id !== saved.id), saved].sort((left, right) => left.sortOrder - right.sortOrder) as typeof activeHomeItems.value;
ElMessage.success(editingId.value ? "首页配置已保存" : "首页配置已新增");
closeEditor();
} catch (error) { ElMessage.error(error instanceof Error ? error.message : "首页配置保存失败"); } finally { saving.value = false; }
}
async function saveHomeByKind() {
const kind = activeHomeKind.value;
if (kind === "recommendation") {
const input = { categoryId: homeDraft.value.categoryId ?? "", isActive: homeDraft.value.isActive, sortOrder: homeDraft.value.sortOrder };
return editingId.value ? updateHomePlayRecommendation(editingId.value, input) : createHomePlayRecommendation(input);
}
if (kind === "team") {
const input = { tag: homeDraft.value.tag ?? "", title: homeDraft.value.title ?? "", description: homeDraft.value.description ?? "", image: homeDraft.value.image ?? "", demandKeyword: homeDraft.value.demandKeyword ?? "", detailSubtitle: homeDraft.value.detailSubtitle || null, detailParagraphs: homeDraft.value.detailParagraphs ?? [], isActive: homeDraft.value.isActive, sortOrder: homeDraft.value.sortOrder };
return editingId.value ? updateHomeTeamBuilding(editingId.value, input) : createHomeTeamBuilding(input);
}
const input = { title: homeDraft.value.title ?? "", image: homeDraft.value.image ?? "", images: homeDraft.value.images ?? [], demandKeyword: homeDraft.value.demandKeyword ?? "", isActive: homeDraft.value.isActive, sortOrder: homeDraft.value.sortOrder };
return editingId.value ? updateHomeWildArchive(editingId.value, input) : createHomeWildArchive(input);
}
async function removeItem(item: SiteConfigItem | HomeWanfaRecommendation | HomeTeamBuilding | HomeWildArchive) {
if (deletingId.value) return;
try { await ElMessageBox.confirm(`确认删除「${itemTitle(item)}」吗?删除后会同步影响首页展示。`, "删除首页配置", { type: "warning" }); } catch { return; }
deletingId.value = item.id;
try {
await deleteSiteConfigItem(activeModule.value, item.id);
moduleItems.value[activeModule.value] = activeItems.value.filter((candidate) => candidate.id !== item.id);
if (editingId.value === item.id) closeEditor();
if (isSiteModule.value) await deleteSiteConfigItem(activeSiteModule.value, item.id);
else if (activeHomeKind.value === "recommendation") await deleteHomePlayRecommendation(item.id);
else if (activeHomeKind.value === "team") await deleteHomeTeamBuilding(item.id);
else await deleteHomeWildArchive(item.id);
if (isSiteModule.value) siteItems.value[activeSiteModule.value] = activeSiteItems.value.filter((candidate) => candidate.id !== item.id);
else homeItems.value[activeHomeKind.value] = activeHomeItems.value.filter((candidate) => candidate.id !== item.id) as typeof activeHomeItems.value;
ElMessage.success("配置已删除");
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : "配置删除失败");
} finally {
deletingId.value = null;
}
} catch (error) { ElMessage.error(error instanceof Error ? error.message : "配置删除失败"); } finally { deletingId.value = null; }
}
async function moveItem(item: SiteConfigItem, direction: -1 | 1) {
if (reorderingId.value || !activeDefinition.value.ordered || !auth.hasPermission("admin:site-config")) return;
async function moveItem(item: SiteConfigItem | HomeWanfaRecommendation | HomeTeamBuilding | HomeWildArchive, direction: -1 | 1) {
const items = activeItems.value;
const currentIndex = items.findIndex((candidate) => candidate.id === item.id);
const nextIndex = currentIndex + direction;
if (currentIndex < 0 || nextIndex < 0 || nextIndex >= items.length) return;
const nextIds = items.map((candidate) => candidate.id);
[nextIds[currentIndex], nextIds[nextIndex]] = [nextIds[nextIndex], nextIds[currentIndex]];
const index = items.findIndex((candidate) => candidate.id === item.id);
const nextIndex = index + direction;
if (index < 0 || nextIndex < 0 || nextIndex >= items.length || reorderingId.value) return;
const ids = items.map((candidate) => candidate.id);
[ids[index], ids[nextIndex]] = [ids[nextIndex], ids[index]];
reorderingId.value = item.id;
try {
const result = await reorderSiteConfigItems(activeModule.value, nextIds);
moduleItems.value[activeModule.value] = result.items;
const result = isSiteModule.value ? await reorderSiteConfigItems(activeSiteModule.value, ids) : await reorderHomeByKind(activeHomeKind.value, ids);
if (isSiteModule.value) siteItems.value[activeSiteModule.value] = result.items as SiteConfigItem[];
else homeItems.value[activeHomeKind.value] = result.items as typeof activeHomeItems.value;
ElMessage.success("展示顺序已更新");
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : "排序更新失败");
} finally {
reorderingId.value = null;
}
} catch (error) { ElMessage.error(error instanceof Error ? error.message : "排序更新失败"); } finally { reorderingId.value = null; }
}
function replaceItem(saved: SiteConfigItem) {
const items = moduleItems.value[activeModule.value];
const index = items.findIndex((item) => item.id === saved.id);
moduleItems.value[activeModule.value] = index < 0
? [...items, saved].sort(sortByOrder)
: items.map((item) => item.id === saved.id ? saved : item).sort(sortByOrder);
function reorderHomeByKind(kind: HomeContentKind, ids: string[]) {
if (kind === "recommendation") return reorderHomePlayRecommendations(ids);
if (kind === "team") return reorderHomeTeamBuildings(ids);
return reorderHomeWildArchives(ids);
}
function sortByOrder(left: SiteConfigItem, right: SiteConfigItem) {
return ("sortOrder" in left ? left.sortOrder : 0) - ("sortOrder" in right ? right.sortOrder : 0);
}
function itemTitle(item: SiteConfigItem) {
if ("introTitle" in item) return item.introTitle;
if ("submitLabel" in item) return item.submitLabel;
function itemTitle(item: SiteConfigItem | HomeWanfaRecommendation | HomeTeamBuilding | HomeWildArchive) {
if ("categoryLabel" in item) return item.categoryLabel;
if ("tag" in item) return item.title;
return item.title;
}
function itemDescription(item: SiteConfigItem) {
if ("introTitle" in item) return item.intro;
if ("submitLabel" in item) return item.chips.join("、") || "需求表单字段配置";
return "description" in item ? item.description : item.kicker;
function itemDescription(item: SiteConfigItem | HomeWanfaRecommendation | HomeTeamBuilding | HomeWildArchive) {
if ("categoryLabel" in item) return `${item.routeCount} 条路线`;
if ("tag" in item) return item.description;
if ("demandKeyword" in item) return item.demandKeyword;
return "";
}
function itemImage(item: SiteConfigItem) {
function itemImage(item: SiteConfigItem | HomeWanfaRecommendation | HomeTeamBuilding | HomeWildArchive) {
return "image" in item ? item.image : null;
}
function itemStatus(item: SiteConfigItem) {
return item.isActive ? "启用中" : "已停用";
}
onMounted(loadConfig);
</script>
<template>
<div class="page-stack">
<div class="page-intro site-config-intro">
<div>
<p class="eyebrow">CONTENT / HOME</p>
<h2>首页配置</h2>
<p>迁移首页站点配置的读取、写入、删除和排序能力,保存后会直接影响对应前台模块。</p>
</div>
<div class="page-intro-actions"><el-button v-permission="'admin:media:read'" plain @click="$router.push('/media')">媒体素材</el-button><el-button v-permission="'admin:site-config'" plain @click="$router.push('/tools')">发布与重置</el-button><el-button v-permission="'admin:site-config'" type="primary" :disabled="!canCreate" @click="startCreate">新增{{ activeDefinition.label }}</el-button></div>
</div>
<el-alert v-if="!loading && !loadError" title="站点配置模块均已接入统一写入入口;玩法路线与详情内容在“玩法与路线”页面单独维护。" type="info" :closable="false" show-icon />
<div class="page-intro site-config-intro"><div><p class="eyebrow">CONTENT / HOME</p><h2>首页配置</h2><p>统一维护顶部轮播、玩法推荐、万趣用车、团队共创和极境视界;保存后直接同步首页展示。</p></div></div>
<el-skeleton v-if="loading" :rows="10" animated />
<el-result v-else-if="loadError" icon="error" title="首页配置加载失败" :sub-title="loadError">
<template #extra><el-button type="primary" @click="loadConfig">重试</el-button></template>
</el-result>
<el-result v-else-if="loadError" icon="error" title="首页配置加载失败" :sub-title="loadError"><template #extra><el-button type="primary" @click="loadConfig">重试</el-button></template></el-result>
<template v-else>
<el-tabs v-model="activeModule" class="site-config-tabs" @tab-change="closeEditor">
<el-tab-pane v-for="definition in siteModuleDefinitions" :key="definition.id" :name="definition.id" :label="definition.label">
<div class="site-config-panel">
<div class="panel-heading">
<div>
<strong>{{ definition.label }}</strong>
<span>{{ definition.description }}</span>
</div>
<el-button v-permission="'admin:site-config'" plain type="primary" :disabled="definition.singleton && activeItems.length > 0" @click="startCreate">新增配置</el-button>
</div>
<el-empty v-if="!activeItems.length" description="暂无配置记录">
<el-button v-permission="'admin:site-config'" type="primary" @click="startCreate">创建第一条配置</el-button>
</el-empty>
<el-table v-else :data="activeItems" stripe>
<el-table-column label="内容" min-width="260">
<template #default="{ row }">
<div class="site-item-content">
<el-image v-if="itemImage(row)" class="site-item-thumb" :src="itemImage(row) ?? undefined" fit="cover" />
<div>
<strong>{{ itemTitle(row) || "未命名配置" }}</strong>
<small>{{ itemDescription(row) || "暂无补充说明" }}</small>
</div>
</div>
</template>
</el-table-column>
<el-table-column label="状态" width="110">
<template #default="{ row }"><el-tag :type="row.isActive ? 'success' : 'info'" effect="plain">{{ itemStatus(row) }}</el-tag></template>
</el-table-column>
<el-table-column v-if="definition.ordered" prop="sortOrder" label="排序" width="80" />
<el-table-column label="操作" width="260" fixed="right">
<template #default="{ row, $index }">
<el-button v-permission="'admin:site-config'" link type="primary" @click="startEdit(row)">编辑</el-button>
<template v-if="definition.ordered"><el-button v-permission="'admin:site-config'" link :disabled="$index === 0 || reorderingId === row.id" :loading="reorderingId === row.id" @click="moveItem(row, -1)">上移</el-button><el-button v-permission="'admin:site-config'" link :disabled="$index === activeItems.length - 1 || reorderingId === row.id" @click="moveItem(row, 1)">下移</el-button></template>
<el-button v-permission="'admin:site-config'" link type="danger" :loading="deletingId === row.id" @click="removeItem(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-tab-pane>
<el-tab-pane name="heroSlides" label="顶部轮播" />
<el-tab-pane name="recommendation" label="玩法推荐" />
<el-tab-pane name="vehicleOptions" label="万趣用车" />
<el-tab-pane name="team" label="团队共创" />
<el-tab-pane name="wild" label="极境视界" />
</el-tabs>
<el-drawer v-model="editorOpen" class="site-config-drawer" size="min(560px, 100%)" :with-header="false" :close-on-press-escape="!saving" :show-close="false">
<SiteConfigEditor
v-if="editorOpen"
:module="activeModule"
:draft="draft"
:creating="isCreating"
:saving="saving"
@update:draft="draft = $event"
@save="saveItem"
@cancel="closeEditor"
/>
</el-drawer>
<el-card shadow="never" class="panel-card home-content-panel">
<div class="panel-heading"><div><strong>{{ isSiteModule ? activeDefinition.label : ({ recommendation: "玩法推荐", team: "团队共创", wild: "极境视界" } as Record<HomeContentKind, string>)[activeHomeKind] }}</strong><span>{{ activeItems.length }} 条配置</span></div><el-button v-permission="'admin:site-config'" type="primary" plain @click="startCreate">新增配置</el-button></div>
<el-empty v-if="!activeItems.length" description="暂无配置记录"><el-button v-permission="'admin:site-config'" type="primary" @click="startCreate">创建第一条配置</el-button></el-empty>
<el-table v-else :data="activeItems" stripe>
<el-table-column label="内容" min-width="300"><template #default="{ row }"><div class="site-item-content"><el-image v-if="itemImage(row)" class="site-item-thumb" :src="itemImage(row) ?? undefined" fit="cover" /><div><strong>{{ itemTitle(row) }}</strong><small>{{ itemDescription(row) }}</small></div></div></template></el-table-column>
<el-table-column v-if="activeModule === 'recommendation'" prop="routeCount" label="路线数" width="90" />
<el-table-column v-if="activeModule !== 'recommendation'" prop="sortOrder" label="排序" width="80" />
<el-table-column label="状态" width="100"><template #default="{ row }"><el-tag :type="row.isActive ? 'success' : 'info'" effect="plain">{{ row.isActive ? "启用中" : "已停用" }}</el-tag></template></el-table-column>
<el-table-column label="操作" width="250" fixed="right"><template #default="{ row, $index }"><el-button v-permission="'admin:site-config'" link type="primary" @click="startEdit(row)">编辑</el-button><el-button v-permission="'admin:site-config'" link :disabled="$index === 0 || reorderingId === row.id" @click="moveItem(row, -1)">上移</el-button><el-button v-permission="'admin:site-config'" link :disabled="$index === activeItems.length - 1 || reorderingId === row.id" @click="moveItem(row, 1)">下移</el-button><el-button v-permission="'admin:site-config'" link type="danger" :loading="deletingId === row.id" @click="removeItem(row)">删除</el-button></template></el-table-column>
</el-table>
</el-card>
<el-drawer v-model="siteEditorOpen" class="site-config-drawer" size="min(560px, 100%)" :with-header="false" :show-close="false" :close-on-press-escape="!saving"><SiteConfigEditor v-if="siteEditorOpen" :module="activeSiteModule" :draft="siteDraft" :creating="creating" :saving="saving" @update:draft="siteDraft = $event" @save="saveSiteItem" @cancel="closeEditor" /></el-drawer>
<el-drawer v-model="editorOpen" class="site-config-drawer" size="min(560px, 100%)" :with-header="false" :show-close="false" :close-on-press-escape="!saving"><HomeContentEditor v-if="editorOpen" :kind="activeHomeKind" :draft="homeDraft" :categories="categories" :creating="creating" :saving="saving" @update:draft="homeDraft = $event" @save="saveHomeItem" @cancel="closeEditor" /></el-drawer>
</template>
</div>
</template>