init local code
This commit is contained in:
@@ -0,0 +1,287 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref, watch } from "vue";
|
||||
import { Message } from "view-ui-plus";
|
||||
import { siteApi, themeApi } from "@/api/module/cms";
|
||||
import { SiteVo, ThemeVo } from "@/types/cms";
|
||||
import { formModelFromTheme, FormModelValue, mergeThemeParamsIntoConfigJson } from "@/utils/themeParamForm";
|
||||
import ThemePicker from "@/components/theme/ThemePicker.vue";
|
||||
import ThemeParamForm from "@/components/theme/ThemeParamForm.vue";
|
||||
import ThemePreviewFrame from "@/components/theme/ThemePreviewFrame.vue";
|
||||
import SiteConfigForm from "@/views/cms/components/SiteConfigForm.vue";
|
||||
|
||||
/**
|
||||
* 新建站点三步向导(方案 §6.1):①选模板 → ②微调+实时预览 → ③站点信息+确认。
|
||||
* 预览内容源:当前启用站点(默认第一个);无启用站点时隐藏预览并提示。
|
||||
* 向导中途关闭不产生任何数据;确认后创建成功才落库。
|
||||
*/
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
sites: SiteVo[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:visible", v: boolean): void;
|
||||
(e: "created"): void;
|
||||
}>();
|
||||
|
||||
const step = ref(1);
|
||||
const themes = ref<ThemeVo[]>([]);
|
||||
const themesLoading = ref(false);
|
||||
|
||||
const form = reactive({
|
||||
code: "",
|
||||
parentSiteId: undefined as number | undefined,
|
||||
schId: undefined as number | undefined,
|
||||
schName: "",
|
||||
name: "",
|
||||
status: 1,
|
||||
configJson: "",
|
||||
});
|
||||
|
||||
const themeName = ref("default");
|
||||
const themeParams = ref<FormModelValue>({});
|
||||
const sourceSiteCode = ref<string>("");
|
||||
const saving = ref(false);
|
||||
|
||||
/** 内容源候选:启用中的站点(顶层/子站皆可作内容源) */
|
||||
const sourceSites = computed(() => props.sites.filter(s => s.status === 1));
|
||||
const selectedTheme = computed(() => themes.value.find(t => t.name === themeName.value));
|
||||
const canPreview = computed(() => !!sourceSiteCode.value && !!themeName.value);
|
||||
|
||||
const previewTick = ref(0);
|
||||
const previewParams = ref<FormModelValue>({});
|
||||
|
||||
const parentOptions = computed(() => props.sites.filter(s => !s.parentSiteId));
|
||||
|
||||
function debouncePreview() {
|
||||
previewParams.value = { ...themeParams.value };
|
||||
previewTick.value = Date.now();
|
||||
}
|
||||
|
||||
watch(themeName, async () => {
|
||||
const t = selectedTheme.value;
|
||||
if (t) themeParams.value = formModelFromTheme(t);
|
||||
debouncePreview();
|
||||
});
|
||||
|
||||
watch(themeParams, debouncePreview, { deep: true });
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
async v => {
|
||||
if (!v) return;
|
||||
step.value = 1;
|
||||
Object.assign(form, { code: "", parentSiteId: undefined, schId: undefined, schName: "", name: "", status: 1, configJson: "" });
|
||||
sourceSiteCode.value = sourceSites.value[0]?.code || "";
|
||||
if (!themes.value.length) await loadThemes();
|
||||
themeName.value = "default";
|
||||
const t = selectedTheme.value;
|
||||
themeParams.value = t ? formModelFromTheme(t) : {};
|
||||
debouncePreview();
|
||||
},
|
||||
);
|
||||
|
||||
async function loadThemes() {
|
||||
themesLoading.value = true;
|
||||
try {
|
||||
themes.value = (await themeApi.list().send()) || [];
|
||||
if (!themes.value.some(t => t.name === "default") && themes.value.length) {
|
||||
themeName.value = themes.value[0].name;
|
||||
}
|
||||
} finally {
|
||||
themesLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function pickTheme(name: string) {
|
||||
themeName.value = name;
|
||||
}
|
||||
|
||||
function next() {
|
||||
// 站点信息表单在第 3 步才挂载,步进本身不校验;校验放在「创建站点」时(见 createSite)
|
||||
step.value += 1;
|
||||
}
|
||||
|
||||
function prev() {
|
||||
if (step.value > 1) step.value -= 1;
|
||||
}
|
||||
|
||||
const formRef = ref();
|
||||
const configFormRef = ref();
|
||||
const rules = {
|
||||
code: [
|
||||
{ required: true, message: "站点编码不得为空", trigger: "blur" },
|
||||
{ pattern: /^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$/, message: "仅限小写字母/数字/短横线", trigger: "blur" },
|
||||
],
|
||||
name: [{ required: true, message: "站点名称不得为空", trigger: "blur" }],
|
||||
};
|
||||
|
||||
async function createSite() {
|
||||
const valid = await formRef.value?.validate();
|
||||
if (!valid) return;
|
||||
const configValid = await configFormRef.value?.validate();
|
||||
if (!configValid) return;
|
||||
saving.value = true;
|
||||
try {
|
||||
const configJson = mergeThemeParamsIntoConfigJson(form.configJson || "", selectedTheme.value!, themeParams.value);
|
||||
await siteApi
|
||||
.save({
|
||||
code: form.code,
|
||||
parentSiteId: form.parentSiteId || 0,
|
||||
schId: form.schId,
|
||||
schName: form.schName || undefined,
|
||||
name: form.name,
|
||||
theme: themeName.value,
|
||||
configJson,
|
||||
status: form.status,
|
||||
})
|
||||
.send();
|
||||
Message.success("站点创建成功!");
|
||||
emit("update:visible", false);
|
||||
emit("created");
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
:model-value="visible"
|
||||
:title="`新建站点向导 · 第 ${step} / 3 步`"
|
||||
:mask-closable="false"
|
||||
width="980px"
|
||||
@update:model-value="emit('update:visible', $event)"
|
||||
@on-cancel="emit('update:visible', false)"
|
||||
>
|
||||
<div class="wizard">
|
||||
<!-- ① 选模板 -->
|
||||
<div v-if="step === 1" class="wizard-step">
|
||||
<p class="step-tip">选择一个皮肤包作为网站风格(后续可在站点管理中微调主色等参数)。</p>
|
||||
<ThemePicker v-if="themes.length" :themes="themes" :model-value="themeName" @update:model-value="pickTheme" />
|
||||
<p v-else-if="themesLoading" class="loading">主题列表加载中…</p>
|
||||
<p v-else class="loading">主题列表为空 —— 请检查后端 /api/theme/list 是否可用。</p>
|
||||
</div>
|
||||
|
||||
<!-- ② 微调 + 实时预览 -->
|
||||
<div v-else-if="step === 2" class="wizard-step step2">
|
||||
<div class="left">
|
||||
<FormItem label="预览内容源" :label-width="90">
|
||||
<Select v-model="sourceSiteCode" style="width: 260px">
|
||||
<Option v-for="s in sourceSites" :key="s.id" :value="s.code">{{ s.name }}({{ s.code }})</Option>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<ThemeParamForm :theme="selectedTheme" v-model="themeParams" />
|
||||
</div>
|
||||
<div class="right">
|
||||
<ThemePreviewFrame
|
||||
v-if="canPreview"
|
||||
:site-code="sourceSiteCode"
|
||||
:theme="themeName"
|
||||
:key="previewTick"
|
||||
:theme-params="previewParams"
|
||||
/>
|
||||
<div v-else class="no-preview">
|
||||
暂无启用站点可作为内容源 —— 请先在站点管理创建一个启用站点,或初始化演示数据。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ③ 站点信息 + 确认 -->
|
||||
<div v-else class="wizard-step step3">
|
||||
<p class="step-tip">
|
||||
站点将使用皮肤包 <b>{{ selectedTheme?.title }}</b>({{ themeName }}),并携带上述微调参数。
|
||||
</p>
|
||||
<Form ref="formRef" :model="form" :rules="rules" :label-width="110" class="info-form">
|
||||
<FormItem label="站点编码" prop="code">
|
||||
<Input v-model="form.code" placeholder="URL 路径用,如 demo" style="width: 320px" />
|
||||
</FormItem>
|
||||
<FormItem label="站点名称" prop="name">
|
||||
<Input v-model="form.name" :maxlength="128" placeholder="如 XX学校门户网站" style="width: 320px" />
|
||||
</FormItem>
|
||||
<FormItem label="上级站点">
|
||||
<Select v-model="form.parentSiteId" clearable placeholder="空=顶层校站点;选主站则为院系子站" style="width: 320px">
|
||||
<Option v-for="s in parentOptions" :key="s.id" :value="s.id">{{ s.name }}({{ s.code }})</Option>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem label="学校ID">
|
||||
<InputNumber v-model="form.schId" :min="1" placeholder="UserBase sys_group.id" style="width: 200px" />
|
||||
<!-- B-8:顶层站 schId 必填(校级角色按此匹配管理权限);院系子站留空自动继承主站 -->
|
||||
<div class="pbu-tip">
|
||||
顶层站点必填(否则建成即校级角色不可管理);选了上级站点的院系子站可留空,自动继承主站。
|
||||
</div>
|
||||
</FormItem>
|
||||
<FormItem label="学校名称">
|
||||
<Input v-model="form.schName" :maxlength="128" style="width: 320px" />
|
||||
</FormItem>
|
||||
<FormItem label="状态">
|
||||
<RadioGroup v-model="form.status">
|
||||
<Radio :label="1">启用</Radio>
|
||||
<Radio :label="0">停用</Radio>
|
||||
</RadioGroup>
|
||||
</FormItem>
|
||||
</Form>
|
||||
<!-- 站点配置结构化表单(docs/09 §3.1,替代原 JSON textarea) -->
|
||||
<SiteConfigForm ref="configFormRef" v-model="form.configJson" class="cfg-form" />
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button v-if="step > 1" @click="prev">上一步</Button>
|
||||
<Button v-if="step < 3" type="primary" :disabled="!themes.length" @click="next">下一步</Button>
|
||||
<Button v-else type="primary" :loading="saving" @click="createSite">创建站点</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.wizard-step {
|
||||
min-height: 300px;
|
||||
}
|
||||
.step-tip {
|
||||
color: #888;
|
||||
font-size: 13px;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
.pbu-tip {
|
||||
color: #888;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.loading {
|
||||
color: #999;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
padding: 60px 0;
|
||||
}
|
||||
.step2 {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
.left {
|
||||
width: 340px;
|
||||
flex: none;
|
||||
}
|
||||
.right {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
.no-preview {
|
||||
height: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #999;
|
||||
font-size: 13px;
|
||||
border: 1px dashed #e5e6eb;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.info-form {
|
||||
margin-top: 4px;
|
||||
}
|
||||
.cfg-form {
|
||||
margin-top: 12px;
|
||||
padding-top: 4px;
|
||||
border-top: 1px dashed #e5e6eb;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user