Files
WonderQ-Project/WonderQ-Admin-UI-Vue/src/pages/HomePage.vue
T
duanshuwen efab6d1398 feat(admin): 新增全套运营管理功能,修复核心路由bug及后端seed问题
修复Vue Admin布局中RouterView未绑定唯一key的问题,解决RBAC系统资源页面切换后内容不刷新的ISSUE-001故障,新增routeViewKey工具函数区分路由实例
完善路由配置,新增admin空路径路由,修复路由命名空路径告警
新增媒体管理、运维发布/重置页面,补全全站路由注册与动态路由过滤逻辑
新增玩法、线索、管家、系统资源管理的全套工具函数、业务组件及单元测试用例
修复后端app/seed.py中媒体资源重复检测未考虑会话内pending资源的bug,新增对应测试验证修复逻辑
修正alembic迁移版本0015的依赖关系,删除废弃的旧迁移文件
优化后端系统接口的字典序列化逻辑,简化重复代码
添加QA验收报告与相关测试截图,完善项目测试覆盖
2026-08-26 18:48:48 +08:00

271 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, onMounted, ref } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import {
createSiteConfigItem,
deleteSiteConfigItem,
getSiteConfig,
reorderSiteConfigItems,
updateSiteConfigItem,
} from "@/api/client";
import SiteConfigEditor from "@/components/site-config/SiteConfigEditor.vue";
import {
compactSiteItemPayload,
createEmptySiteItemDraft,
getSiteModuleDefinition,
siteItemDraft,
siteModuleDefinitions,
validateSiteItemPayload,
} from "@/lib/site-config";
import { useAuthStore } from "@/stores/auth";
import type { SiteConfigItem, SiteItemPatch, SiteModule } from "@/types";
const auth = useAuthStore();
const activeModule = ref<SiteModule>("heroSlides");
const moduleItems = ref<Record<SiteModule, SiteConfigItem[]>>({
heroSlides: [],
destinationHero: [],
demandHero: [],
demandFeatureCards: [],
demandForm: [],
vehicleOptions: [],
vehicleService: [],
});
const loading = ref(true);
const loadError = ref<string | null>(null);
const editorOpen = ref(false);
const editingId = ref<string | null>(null);
const draft = 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);
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,
};
} catch (error) {
loadError.value = error instanceof Error ? error.message : "首页配置加载失败";
} finally {
loading.value = false;
}
}
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;
editingId.value = null;
draft.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;
}
saving.value = true;
try {
const saved = editingId.value
? await updateSiteConfigItem(activeModule.value, editingId.value, payload)
: await createSiteConfigItem(activeModule.value, payload);
replaceItem(saved);
ElMessage.success(editingId.value ? "配置已保存" : "配置已新增");
closeEditor();
} 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;
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();
ElMessage.success("配置已删除");
} 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;
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]];
reorderingId.value = item.id;
try {
const result = await reorderSiteConfigItems(activeModule.value, nextIds);
moduleItems.value[activeModule.value] = result.items;
ElMessage.success("展示顺序已更新");
} 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 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;
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 itemImage(item: SiteConfigItem) {
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 />
<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>
<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-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>
</template>
</div>
</template>