init local code
This commit is contained in:
@@ -0,0 +1,319 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref, watch } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { Message } from "view-ui-plus";
|
||||
import Utils from "@/utils/index";
|
||||
import { siteApi, themeApi, themePreviewUrl } from "@/api/module/cms";
|
||||
import { useCmsStore } from "@/store";
|
||||
import config from "@/assets/config/host.config";
|
||||
import { SiteVo, ThemeVo } from "@/types/cms";
|
||||
import { formModelFromTheme, FormModelValue, mergeThemeParamsIntoConfigJson, themeParamsFromConfigJson } 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";
|
||||
import SiteWizard from "@/views/cms/SiteWizard.vue";
|
||||
|
||||
/**
|
||||
* 站点管理(方案 §9.1 + §6.2 主题选择增强)。
|
||||
* 学校化管理(docs/09 §3.3):默认只显示当前学校站点(主站+院系子站);
|
||||
* 超管可经工作台「全部站点总览」(?scope=all)查看全局列表。
|
||||
* 新建 → 三步向导(选模板→微调预览→确认);已建站点 → 编辑弹窗内
|
||||
* 主题卡片单选 + 参数微调表单,保存带 themeParams(服务端合并进 config_json,方案 §5.3)。
|
||||
* config_json 结构化编辑见 SiteConfigForm(docs/09 §3.1):
|
||||
* 已知键分组表单 + 扩展参数动态行,不再手写 JSON;theme_params 仍由 ThemeParamForm 自动合并。
|
||||
*/
|
||||
const store = useCmsStore();
|
||||
const route = useRoute();
|
||||
|
||||
/** 超管全局视图(工作台入口带入 scope=all) */
|
||||
const scopeAll = computed(() => route.query.scope === "all" && store.isSuperAdmin);
|
||||
|
||||
const rows = ref<SiteVo[]>([]);
|
||||
const loading = ref(false);
|
||||
const tableHeight = Utils.useTableHeight({ isOperat: true });
|
||||
|
||||
/** 主题元数据映射(列表皮肤列缩略名 + 编辑表单) */
|
||||
const themes = ref<ThemeVo[]>([]);
|
||||
const themeMeta = computed(() => {
|
||||
const map: Record<string, ThemeVo> = {};
|
||||
themes.value.forEach(t => (map[t.name] = t));
|
||||
return map;
|
||||
});
|
||||
|
||||
const columns = [
|
||||
{ title: "站点编码", key: "code", width: 140 },
|
||||
{ title: "站点名称", key: "name", minWidth: 200, tooltip: true },
|
||||
{ title: "学校", key: "schName", width: 150, tooltip: true },
|
||||
{
|
||||
title: "所属主站",
|
||||
key: "parentSiteId",
|
||||
width: 150,
|
||||
tooltip: true,
|
||||
render: (h: any, p: any) => h("span", p.row.parentSiteId ? p.row.parentName || `ID ${p.row.parentSiteId}` : "顶层站点"),
|
||||
},
|
||||
{
|
||||
title: "皮肤包",
|
||||
key: "theme",
|
||||
width: 150,
|
||||
render: (h: any, p: any) => {
|
||||
const t = themeMeta.value[p.row.theme || "default"];
|
||||
return h("span", { style: "display:inline-flex;align-items:center;gap:6px" }, [
|
||||
t
|
||||
? h("img", { src: t.screenshotUrl, style: "width:44px;height:28px;object-fit:cover;border-radius:3px;border:1px solid #eee", alt: "" })
|
||||
: h("span", { style: "display:inline-block;width:44px;height:28px;background:#f0f2f5;border-radius:3px" }),
|
||||
h("span", t ? `${t.title}(${p.row.theme})` : p.row.theme || "default"),
|
||||
]);
|
||||
},
|
||||
},
|
||||
{ title: "门户路径", key: "portalPath", minWidth: 200, tooltip: true, render: (h: any, p: any) => h("span", `/portal/${p.row.code}/`) },
|
||||
{
|
||||
title: "状态",
|
||||
key: "status",
|
||||
width: 90,
|
||||
align: "center",
|
||||
render: (h: any, p: any) =>
|
||||
h(Tag, { color: p.row.status === 1 ? "success" : "default" }, () =>
|
||||
p.row.status === 1 ? "启用" : "停用"
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
width: 150,
|
||||
fixed: "right",
|
||||
align: "center",
|
||||
render: (h: any, p: any) => [
|
||||
Utils.operatingHandle(h, "编辑", "icon-edit", () => openEdit(p.row)),
|
||||
Utils.operatingHandle(h, "门户", "icon-browse", () => openPortal(p.row)),
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
/** 新建向导 */
|
||||
const wizardVisible = ref(false);
|
||||
|
||||
/** 编辑弹窗 */
|
||||
const modalVisible = ref(false);
|
||||
const saving = ref(false);
|
||||
const formRef = ref();
|
||||
const configFormRef = ref();
|
||||
const form = reactive({
|
||||
id: undefined as number | undefined,
|
||||
code: "",
|
||||
parentSiteId: undefined as number | undefined,
|
||||
schId: undefined as number | undefined,
|
||||
schName: "",
|
||||
name: "",
|
||||
theme: "default",
|
||||
configJson: "",
|
||||
status: 1,
|
||||
});
|
||||
const editThemeParams = ref<FormModelValue>({});
|
||||
const editTheme = computed(() => themeMeta.value[form.theme]);
|
||||
|
||||
/** 编辑内预览 */
|
||||
const previewVisible = ref(false);
|
||||
|
||||
/** 可选主站:顶层站点且非自身(院系子站层级固定两层) */
|
||||
const parentOptions = computed(() =>
|
||||
rows.value.filter(r => !r.parentSiteId && r.id !== form.id)
|
||||
);
|
||||
|
||||
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 run() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const list = (await siteApi.list().send()) || [];
|
||||
// 学校上下文过滤(docs/09 §3.3):默认仅当前学校;超管 scope=all 时显示全部
|
||||
rows.value = scopeAll.value
|
||||
? list
|
||||
: list.filter(s => !store.currentSchool || s.schId === store.currentSchool!.schId);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadThemes() {
|
||||
try {
|
||||
themes.value = (await themeApi.list().send()) || [];
|
||||
} catch {
|
||||
themes.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
function openWizard() {
|
||||
wizardVisible.value = true;
|
||||
}
|
||||
|
||||
function onWizardCreated() {
|
||||
run();
|
||||
}
|
||||
|
||||
function openEdit(row: SiteVo) {
|
||||
Object.assign(form, {
|
||||
id: row.id,
|
||||
code: row.code,
|
||||
parentSiteId: row.parentSiteId || undefined,
|
||||
schId: row.schId,
|
||||
schName: row.schName || "",
|
||||
name: row.name,
|
||||
theme: themeMeta.value[row.theme || "default"] ? row.theme! : "default",
|
||||
configJson: row.configJson || "",
|
||||
status: row.status,
|
||||
});
|
||||
const t = editTheme.value;
|
||||
editThemeParams.value = t ? themeParamsFromConfigJson(row.configJson, t) : {};
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function pickTheme(name: string) {
|
||||
if (form.theme === name) return;
|
||||
form.theme = name;
|
||||
const t = editTheme.value;
|
||||
editThemeParams.value = t ? formModelFromTheme(t, editThemeParams.value) : {};
|
||||
}
|
||||
|
||||
async function save() {
|
||||
const valid = await formRef.value?.validate();
|
||||
if (!valid) return;
|
||||
const configValid = await configFormRef.value?.validate();
|
||||
if (!configValid) return;
|
||||
saving.value = true;
|
||||
try {
|
||||
const t = editTheme.value;
|
||||
const themeParams = t ? editThemeParams.value : {};
|
||||
await siteApi
|
||||
.save({
|
||||
id: form.id,
|
||||
code: form.code,
|
||||
parentSiteId: form.parentSiteId || 0,
|
||||
schId: form.schId,
|
||||
schName: form.schName || undefined,
|
||||
name: form.name,
|
||||
theme: form.theme || undefined,
|
||||
configJson: t ? mergeThemeParamsIntoConfigJson(form.configJson || "", t, themeParams) : form.configJson || undefined,
|
||||
status: form.status,
|
||||
})
|
||||
.send();
|
||||
Message.success("保存成功!");
|
||||
modalVisible.value = false;
|
||||
run();
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openPortal(row: SiteVo) {
|
||||
window.open(`${config.serviceHost}/portal/${row.code}/`, "_blank");
|
||||
}
|
||||
|
||||
function openEditPreview() {
|
||||
previewVisible.value = true;
|
||||
}
|
||||
|
||||
/** 全局视图返回学校工作台 */
|
||||
function goBackWorkbench() {
|
||||
window.location.hash = "#/workbench";
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
run();
|
||||
loadThemes();
|
||||
});
|
||||
|
||||
// 学校上下文切换即时重载(docs/09 §3.3)
|
||||
watch(() => store.currentSchool?.schId, () => run());
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-wrapper">
|
||||
<div class="main-container">
|
||||
<div class="fix" v-if="scopeAll">
|
||||
<Alert type="info" show-icon style="width: 100%">
|
||||
全局站点视图:显示全部学校的站点(含各校主站与院系子站)。
|
||||
<a href="javascript:void(0);" @click="goBackWorkbench">返回学校工作台</a>
|
||||
</Alert>
|
||||
</div>
|
||||
<div class="fix">
|
||||
<Button class="fl" type="primary" size="large" @click="openWizard">
|
||||
<Icon type="ios-add" />
|
||||
新建站点(向导)
|
||||
</Button>
|
||||
</div>
|
||||
<Table :columns="columns" :data="rows" :height="tableHeight" :loading="loading" no-data-text="暂无站点" />
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<Modal v-model="modalVisible" title="编辑站点" :mask-closable="false" width="720px">
|
||||
<Form ref="formRef" :model="form" :rules="rules" :label-width="110">
|
||||
<FormItem label="站点编码" prop="code">
|
||||
<Input v-model="form.code" :disabled="!!form.id" placeholder="URL 路径用,如 demo" />
|
||||
</FormItem>
|
||||
<FormItem label="站点名称" prop="name">
|
||||
<Input v-model="form.name" :maxlength="128" placeholder="如 XX学校门户网站" />
|
||||
</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>
|
||||
<p class="form-tip">院系子站挂在主站下,门户 header 右上出现"专业院系"下拉;层级固定两层。</p>
|
||||
</FormItem>
|
||||
<FormItem label="学校ID">
|
||||
<InputNumber v-model="form.schId" :min="1" placeholder="UserBase sys_group.id" style="width: 200px" />
|
||||
</FormItem>
|
||||
<FormItem label="学校名称">
|
||||
<Input v-model="form.schName" :maxlength="128" />
|
||||
</FormItem>
|
||||
<FormItem label="皮肤包">
|
||||
<ThemePicker :themes="themes" :model-value="form.theme" @update:model-value="pickTheme" />
|
||||
<Button size="small" class="preview-btn" :disabled="!editTheme" @click="openEditPreview">预览本皮肤</Button>
|
||||
<ThemeParamForm :theme="editTheme" v-model="editThemeParams" />
|
||||
<p class="form-tip">换肤/改参数保存后门户立即生效(缓存已自动失效)。</p>
|
||||
</FormItem>
|
||||
<FormItem label="状态">
|
||||
<RadioGroup v-model="form.status">
|
||||
<Radio :label="1">启用</Radio>
|
||||
<Radio :label="0">停用</Radio>
|
||||
</RadioGroup>
|
||||
</FormItem>
|
||||
</Form>
|
||||
<!-- 站点配置结构化表单(独立 Form 分区,v-model 直绑 configJson) -->
|
||||
<SiteConfigForm ref="configFormRef" v-model="form.configJson" />
|
||||
<template #footer>
|
||||
<Button @click="modalVisible = false">取消</Button>
|
||||
<Button type="primary" :loading="saving" @click="save">保存</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
|
||||
<!-- 编辑预览弹窗(iframe 加载 /api/theme/preview,no-store) -->
|
||||
<Modal v-model="previewVisible" title="皮肤预览" :mask-closable="true" width="980px" footer-hide>
|
||||
<ThemePreviewFrame v-if="previewVisible && editTheme" :site-code="form.code" :theme="form.theme" :theme-params="editThemeParams" />
|
||||
</Modal>
|
||||
|
||||
<!-- 新建站点三步向导 -->
|
||||
<SiteWizard v-model:visible="wizardVisible" :sites="rows" @created="onWizardCreated" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.form-tip {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.preview-btn {
|
||||
margin: 8px 0 0;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user