feat(W4): 站点path全链路-portalUrlOf/previewPortalUrl纯函数,站点列/向导地址预览/预览frame sitePath,store携带path,mock嵌套路由,T-W4-16..18 绿51/51
This commit is contained in:
@@ -4,9 +4,10 @@ import { useRoute, useRouter } from "vue-router";
|
||||
import { Message } from "view-ui-plus";
|
||||
import { Editor, Toolbar } from "@wangeditor/editor-for-vue";
|
||||
import "@wangeditor/editor/dist/css/style.css";
|
||||
import { articleApi, categoryApi, portalBase, uploadUrl } from "@/api/module/cms";
|
||||
import { articleApi, categoryApi, uploadUrl } from "@/api/module/cms";
|
||||
import { useCmsStore } from "@/store";
|
||||
import config from "@/assets/config/host.config";
|
||||
import { portalUrlOf } from "@/utils/sitePath";
|
||||
import { CategoryTreeNode } from "@/types/cms";
|
||||
|
||||
const route = useRoute();
|
||||
@@ -205,7 +206,7 @@ async function saveAndPublish() {
|
||||
/** 预览:仅已发布文章在门户可见 */
|
||||
function preview() {
|
||||
if (!editId.value || !store.currentSite) return;
|
||||
window.open(`${portalBase(store.currentSite.code)}/a/${editId.value}`, "_blank");
|
||||
window.open(`${config.serviceHost}${portalUrlOf(store.currentSite)}a/${editId.value}`, "_blank");
|
||||
}
|
||||
|
||||
onMounted(load);
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useCmsStore } from "@/store";
|
||||
import config from "@/assets/config/host.config";
|
||||
import { CategoryTreeNode } from "@/types/cms";
|
||||
import { rootLabelOf, wrapTreeWithRoot } from "@/utils/categoryTree";
|
||||
import { portalUrlOf } from "@/utils/sitePath";
|
||||
|
||||
/* UserBase group/index 同款交互:图标工具栏(新增/编辑/子组排序/删除)+ ztree(W2:搜索/收起展开按钮移除) */
|
||||
const store = useCmsStore();
|
||||
@@ -216,14 +217,13 @@ function goNewArticle(categoryId: number) {
|
||||
window.location.hash = `#/sch/${store.schoolCode}/article/edit?categoryId=${categoryId}`;
|
||||
}
|
||||
|
||||
/** 门户预览:新开 /portal/{siteCode}/c/{id} */
|
||||
/** 门户预览:新开 /portal/{path}/c/{id}(W4 嵌套路径) */
|
||||
function openPreview(node: any) {
|
||||
const code = store.currentSite?.code || store.schoolCode;
|
||||
if (!code) {
|
||||
if (!store.currentSite) {
|
||||
Message.warning("当前无站点上下文,无法预览");
|
||||
return;
|
||||
}
|
||||
window.open(`${config.serviceHost}/portal/${code}/c/${node.id}`, "_blank");
|
||||
window.open(`${config.serviceHost}${portalUrlOf(store.currentSite)}c/${node.id}`, "_blank");
|
||||
}
|
||||
|
||||
/** 外链栏目直达 */
|
||||
|
||||
@@ -4,6 +4,7 @@ import { categoryApi, articleApi, bannerApi, linkApi, siteApi } from "@/api/modu
|
||||
import { useCmsStore } from "@/store";
|
||||
import { CategoryTreeNode, ArticlePageItem, BannerVo, LinkVo, SiteVo } from "@/types/cms";
|
||||
import config from "@/assets/config/host.config";
|
||||
import { portalUrlOf } from "@/utils/sitePath";
|
||||
|
||||
/**
|
||||
* 门户预览(SPA 内简版门户):mock 模式下「预览门户」跳转到这里,
|
||||
@@ -36,9 +37,9 @@ const colleges = computed(() => {
|
||||
return sites.value.filter(s => s.parentSiteId === anchor && s.status === 1);
|
||||
});
|
||||
|
||||
/** 切换到该院系的真实门户(mock 模式由 mock 中间件承接,真实模式为 SSR 门户) */
|
||||
function openCollege(code: string) {
|
||||
window.open(`${config.serviceHost}/portal/${code}/`, "_blank");
|
||||
/** 切换到该院系的真实门户(mock 模式由 mock 中间件承接,真实模式为 SSR 门户;W4 嵌套 path) */
|
||||
function openCollege(site: SiteVo) {
|
||||
window.open(`${config.serviceHost}${portalUrlOf(site)}`, "_blank");
|
||||
}
|
||||
const linkGroups = computed(() => {
|
||||
const map = new Map<string, LinkVo[]>();
|
||||
@@ -129,7 +130,7 @@ onBeforeUnmount(stopCarousel);
|
||||
<span class="colleges-toggle">专业院系 <i>▾</i></span>
|
||||
<ul class="colleges-menu">
|
||||
<li v-for="c in colleges" :key="c.id" :class="{ on: c.id === store.siteId }">
|
||||
<a href="javascript:;" @click="openCollege(c.code)" :title="c.name">{{ c.name }}</a>
|
||||
<a href="javascript:;" @click="openCollege(c)" :title="c.name">{{ c.name }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { Message } from "view-ui-plus";
|
||||
import { schoolApi, siteApi, themeApi, portalBase } from "@/api/module/cms";
|
||||
import { schoolApi, siteApi, themeApi } from "@/api/module/cms";
|
||||
import { useCmsStore } from "@/store";
|
||||
import config from "@/assets/config/host.config";
|
||||
import { SchoolCard, SchoolOption, ThemeVo } from "@/types/cms";
|
||||
import { portalUrlOf } from "@/utils/sitePath";
|
||||
import ThemePicker from "@/components/theme/ThemePicker.vue";
|
||||
|
||||
/**
|
||||
@@ -62,7 +63,7 @@ function enter(card: SchoolCard) {
|
||||
}
|
||||
|
||||
function openPortal(card: SchoolCard) {
|
||||
window.open(`${portalBase(card.mainSiteCode)}/`, "_blank");
|
||||
window.open(`${config.serviceHost}${portalUrlOf({ path: card.mainSitePath, code: card.mainSiteCode })}`, "_blank");
|
||||
}
|
||||
|
||||
/** 超管全局站点视图(借道当前/第一个学校上下文 + scope=all) */
|
||||
|
||||
@@ -3,7 +3,8 @@ import { computed, onMounted, reactive, ref, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { Message } from "view-ui-plus";
|
||||
import Utils from "@/utils/index";
|
||||
import { assetUrl, siteApi, themeApi, themePreviewUrl } from "@/api/module/cms";
|
||||
import { assetUrl, siteApi, themeApi } from "@/api/module/cms";
|
||||
import { portalUrlOf } from "@/utils/sitePath";
|
||||
import { useCmsStore } from "@/store";
|
||||
import config from "@/assets/config/host.config";
|
||||
import { SiteVo, ThemeVo } from "@/types/cms";
|
||||
@@ -84,7 +85,7 @@ const columns = [
|
||||
]);
|
||||
},
|
||||
},
|
||||
{ title: "门户路径", key: "portalPath", minWidth: 200, tooltip: true, render: (h: any, p: any) => h("span", `/portal/${p.row.code}/`) },
|
||||
{ title: "门户路径", key: "portalPath", minWidth: 200, tooltip: true, render: (h: any, p: any) => h("span", portalUrlOf(p.row)) },
|
||||
{
|
||||
title: "状态",
|
||||
key: "status",
|
||||
@@ -175,6 +176,8 @@ async function loadThemes() {
|
||||
}
|
||||
}
|
||||
|
||||
const editSitePath = ref<string>("");
|
||||
|
||||
function openWizard() {
|
||||
wizardVisible.value = true;
|
||||
}
|
||||
@@ -184,6 +187,7 @@ function onWizardCreated() {
|
||||
}
|
||||
|
||||
function openEdit(row: SiteVo) {
|
||||
editSitePath.value = row.path || row.code;
|
||||
Object.assign(form, {
|
||||
id: row.id,
|
||||
code: row.code,
|
||||
@@ -264,7 +268,7 @@ async function save() {
|
||||
}
|
||||
|
||||
function openPortal(row: SiteVo) {
|
||||
window.open(`${config.serviceHost}/portal/${row.code}/`, "_blank");
|
||||
window.open(`${config.serviceHost}${portalUrlOf(row)}`, "_blank");
|
||||
}
|
||||
|
||||
/** 行级「进入管理」(docs/09 §3.3 子站管理入口):切当前操作站点后落文章列表;
|
||||
@@ -287,7 +291,7 @@ function enterManage(row: SiteVo) {
|
||||
rows.value.filter(s => s.id === main.id || s.parentSiteId === main.id)
|
||||
);
|
||||
}
|
||||
store.setCurrentSite({ id: row.id, code: row.code, name: row.name });
|
||||
store.setCurrentSite({ id: row.id, code: row.code, path: row.path || row.code, name: row.name });
|
||||
router.push(`/sch/${main.code}/article`);
|
||||
}
|
||||
|
||||
@@ -387,7 +391,7 @@ watch(() => store.currentSchool?.schId, () => run());
|
||||
|
||||
<!-- 编辑预览弹窗(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" />
|
||||
<ThemePreviewFrame v-if="previewVisible && editTheme" :site-path="editSitePath" :theme="form.theme" :theme-params="editThemeParams" />
|
||||
</Modal>
|
||||
|
||||
<!-- 新建站点三步向导(B1:校级锁定为本校主站下建院系子站) -->
|
||||
|
||||
@@ -4,6 +4,7 @@ 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 { previewPortalUrl } from "@/utils/sitePath";
|
||||
import ThemePicker from "@/components/theme/ThemePicker.vue";
|
||||
import ThemeParamForm from "@/components/theme/ThemeParamForm.vue";
|
||||
import ThemePreviewFrame from "@/components/theme/ThemePreviewFrame.vue";
|
||||
@@ -42,13 +43,13 @@ const form = reactive({
|
||||
|
||||
const themeName = ref("default");
|
||||
const themeParams = ref<FormModelValue>({});
|
||||
const sourceSiteCode = ref<string>("");
|
||||
const sourceSitePath = 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 canPreview = computed(() => !!sourceSitePath.value && !!themeName.value);
|
||||
|
||||
const previewTick = ref(0);
|
||||
const previewParams = ref<FormModelValue>({});
|
||||
@@ -60,6 +61,14 @@ const parentOptions = computed(() =>
|
||||
: props.sites.filter(s => !s.parentSiteId)
|
||||
);
|
||||
|
||||
/** W4(G5):门户地址预览——随上级与编码输入联动(子站=上级地址/编码;顶层=编码) */
|
||||
const wizardParent = computed(() =>
|
||||
form.parentSiteId ? props.sites.find(s => s.id === form.parentSiteId) : undefined
|
||||
);
|
||||
const previewUrl = computed(() =>
|
||||
previewPortalUrl(wizardParent.value?.path ?? wizardParent.value?.code, form.code)
|
||||
);
|
||||
|
||||
function debouncePreview() {
|
||||
previewParams.value = { ...themeParams.value };
|
||||
previewTick.value = Date.now();
|
||||
@@ -79,7 +88,7 @@ watch(
|
||||
if (!v) return;
|
||||
step.value = 1;
|
||||
Object.assign(form, { code: "", parentSiteId: props.lockParentId, schId: undefined, schName: "", name: "", status: 1, configJson: "" });
|
||||
sourceSiteCode.value = sourceSites.value[0]?.code || "";
|
||||
sourceSitePath.value = sourceSites.value[0]?.path || sourceSites.value[0]?.code || "";
|
||||
if (!themes.value.length) await loadThemes();
|
||||
themeName.value = "default";
|
||||
const t = selectedTheme.value;
|
||||
@@ -176,7 +185,7 @@ async function createSite() {
|
||||
<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>
|
||||
<Option v-for="s in sourceSites" :key="s.id" :value="s.path || s.code">{{ s.name }}({{ s.path || s.code }})</Option>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<ThemeParamForm :theme="selectedTheme" v-model="themeParams" />
|
||||
@@ -184,7 +193,7 @@ async function createSite() {
|
||||
<div class="right">
|
||||
<ThemePreviewFrame
|
||||
v-if="canPreview"
|
||||
:site-code="sourceSiteCode"
|
||||
:site-path="sourceSitePath"
|
||||
:theme="themeName"
|
||||
:key="previewTick"
|
||||
:theme-params="previewParams"
|
||||
@@ -207,6 +216,10 @@ async function createSite() {
|
||||
<FormItem label="站点名称" prop="name">
|
||||
<Input v-model="form.name" :maxlength="128" placeholder="如 XX学校门户网站" style="width: 320px" />
|
||||
</FormItem>
|
||||
<FormItem label="门户地址预览">
|
||||
<span class="portal-preview-url">{{ previewUrl || "输入编码后自动生成" }}</span>
|
||||
<div class="pbu-tip">子站地址 = 上级地址 / 编码;创建后经站点管理「门户路径」列直达。</div>
|
||||
</FormItem>
|
||||
<FormItem label="上级站点">
|
||||
<Select v-model="form.parentSiteId" :disabled="!!props.lockParentId" placeholder="空=顶层校站点;选主站则为院系子站" style="width: 320px">
|
||||
<Option v-for="s in parentOptions" :key="s.id" :value="s.id">{{ s.name }}({{ s.code }})</Option>
|
||||
@@ -288,6 +301,11 @@ async function createSite() {
|
||||
.info-form {
|
||||
margin-top: 4px;
|
||||
}
|
||||
.portal-preview-url {
|
||||
color: #2d8cf0;
|
||||
font-family: Consolas, Menlo, monospace;
|
||||
word-break: break-all;
|
||||
}
|
||||
.cfg-form {
|
||||
margin-top: 12px;
|
||||
padding-top: 4px;
|
||||
|
||||
Reference in New Issue
Block a user