更新website后端
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* 站点设置接口
|
||||
*
|
||||
* 数据来源:后端 yz_platform_site_setting(platform 端「官网管理 - 站点设置」)。
|
||||
* 返回字段名与官网模板标签一一对应,因此 SPA 官网与 themes 直出模板展示的
|
||||
* 站点信息始终一致:
|
||||
* sitename {yz:sitename} 站点名称
|
||||
* logo {yz:logo} 深色 Logo(浅色背景)
|
||||
* logow {yz:logow} 浅色 Logo(深色背景)
|
||||
* icp {yz:icp} ICP 备案号
|
||||
* policeicp {yz:policeicp} 公安备案号
|
||||
* copyright {yz:copyright} 版权信息
|
||||
* privacy {yz:privacy} 隐私政策链接
|
||||
* terms {yz:terms} 使用条款链接
|
||||
*/
|
||||
|
||||
/**
|
||||
* 后端返回的站点设置:key 就是模板标签名
|
||||
* 完全数据驱动 —— 表里有什么 tag 就返回什么,新增设置项前端无需改动。
|
||||
* 下列为常用项的类型提示,其余标签通过索引签名访问即可。
|
||||
*/
|
||||
export type SiteSetting = Record<string, string> & {
|
||||
/** {yz:sitename} 站点名称 */
|
||||
sitename?: string;
|
||||
/** {yz:subtitle} 副标题 */
|
||||
subtitle?: string;
|
||||
/** {yz:logo} 深色 Logo,用于浅色背景(如页头) */
|
||||
logo?: string;
|
||||
/** {yz:logow} 浅色 Logo,用于深色背景(如页脚) */
|
||||
logow?: string;
|
||||
/** {yz:icp} ICP 备案号 */
|
||||
icp?: string;
|
||||
/** {yz:policeicp} 公安备案号 */
|
||||
policeicp?: string;
|
||||
/** {yz:copyright} 版权信息 */
|
||||
copyright?: string;
|
||||
/** {yz:privacy} 隐私政策链接 */
|
||||
privacy?: string;
|
||||
/** {yz:terms} 使用条款链接 */
|
||||
terms?: string;
|
||||
};
|
||||
|
||||
/** 接口前缀:开发期由 vite 代理 /api 到后端,生产可用 VITE_API_BASE_URL 覆盖 */
|
||||
const API_BASE = (import.meta as any).env?.VITE_API_BASE_URL || "/api";
|
||||
|
||||
/** 拉取当前域名的站点设置;接口不可用时返回 null,由调用方保留默认配置 */
|
||||
export async function fetchSiteSetting(): Promise<SiteSetting | null> {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/site/setting`, {
|
||||
headers: { Accept: "application/json" },
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
const body = await res.json();
|
||||
if (body?.code !== 200 || !body.data) return null;
|
||||
return body.data as SiteSetting;
|
||||
} catch (error) {
|
||||
console.warn("[site] 站点设置获取失败,使用默认配置", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,20 @@
|
||||
/**
|
||||
* 站点级公共配置:品牌、导航、页脚
|
||||
* 后续接入后端 CMS 时,只需替换此处的数据来源即可。
|
||||
*
|
||||
* 站点信息的数据来源已切换为后端「官网管理 - 站点设置」(应用启动时由 loadSiteInfo 拉取),
|
||||
* 与官网模板 themes/ 中使用的标签同源,括号内为对应标签:
|
||||
* sitename 站点名称 {yz:sitename}
|
||||
* logoDark 深色 Logo {yz:logo}
|
||||
* logoLight 浅色 Logo {yz:logow}
|
||||
* icp ICP 备案号 {yz:icp}
|
||||
* policeIcp 公安备案号 {yz:policeicp}
|
||||
* copyright 版权信息 {yz:copyright}
|
||||
* privacy 隐私政策链接 {yz:privacy}
|
||||
* terms 使用条款链接 {yz:terms}
|
||||
* 后端未返回的字段保留文件内默认值。
|
||||
*/
|
||||
import { computed, reactive } from "vue";
|
||||
import { fetchSiteSetting } from "@/api/site";
|
||||
|
||||
export interface NavItem {
|
||||
label: string;
|
||||
@@ -13,7 +26,7 @@ export interface FooterColumn {
|
||||
links: NavItem[];
|
||||
}
|
||||
|
||||
export const siteInfo = {
|
||||
export const siteInfo = reactive({
|
||||
brandMark: "YZ",
|
||||
brandName: "云泽科技",
|
||||
brandSub: "YUNZER TECH",
|
||||
@@ -25,7 +38,63 @@ export const siteInfo = {
|
||||
postalCode: "100193",
|
||||
copyright:
|
||||
"COPYRIGHT (C) 2026 YUNZER TECHNOLOGY CO., LTD. ALL RIGHTS RESERVED.",
|
||||
};
|
||||
|
||||
// ↓↓↓ 以下字段由「官网管理 - 站点设置」下发(对应官网模板标签) ↓↓↓
|
||||
/** {yz:sitename} 站点名称 */
|
||||
sitename: "",
|
||||
/** {yz:subtitle} 副标题 */
|
||||
subtitle: "",
|
||||
/** {yz:logo} 深色 Logo,用于浅色背景(页头) */
|
||||
logoDark: "",
|
||||
/** {yz:logow} 浅色 Logo,用于深色背景(页脚) */
|
||||
logoLight: "",
|
||||
/** {yz:icp} ICP 备案号 */
|
||||
icp: "",
|
||||
/** {yz:policeicp} 公安备案号 */
|
||||
policeIcp: "",
|
||||
/** {yz:privacy} 隐私政策链接 */
|
||||
privacy: "",
|
||||
/** {yz:terms} 使用条款链接 */
|
||||
terms: "",
|
||||
});
|
||||
|
||||
/**
|
||||
* 站点设置标签值:key 就是模板标签名,与后端 {yz:xxx} 完全一致
|
||||
* 供 v-yz 指令解析(<p v-yz>{yz:icp}</p>),未配置或后端未返回时输出空字符串
|
||||
*/
|
||||
export const siteTags = reactive<Record<string, string>>({});
|
||||
|
||||
/**
|
||||
* 拉取站点设置并回填 siteInfo(应用启动时调用一次)。
|
||||
* 接口异常时静默处理,页面继续使用默认配置。
|
||||
*/
|
||||
export async function loadSiteInfo(): Promise<void> {
|
||||
const data = await fetchSiteSetting();
|
||||
if (!data) return;
|
||||
|
||||
// {yz:sitename} → 页头品牌名
|
||||
if (data.sitename) {
|
||||
siteInfo.sitename = data.sitename;
|
||||
siteInfo.brandName = data.sitename;
|
||||
}
|
||||
if (data.subtitle) siteInfo.subtitle = data.subtitle;
|
||||
if (data.logo) siteInfo.logoDark = data.logo;
|
||||
if (data.logow) siteInfo.logoLight = data.logow;
|
||||
// 公司信息:站点设置里有值就覆盖本地默认(页脚联系栏、页脚品牌全称),没填则保留默认
|
||||
if (data.companyname) siteInfo.fullName = data.companyname;
|
||||
if (data.phone) siteInfo.phone = data.phone;
|
||||
if (data.email) siteInfo.email = data.email;
|
||||
if (data.address) siteInfo.address = data.address;
|
||||
if (data.icp) siteInfo.icp = data.icp;
|
||||
if (data.policeicp) siteInfo.policeIcp = data.policeicp;
|
||||
if (data.copyright) siteInfo.copyright = data.copyright;
|
||||
if (data.privacy) siteInfo.privacy = data.privacy;
|
||||
if (data.terms) siteInfo.terms = data.terms;
|
||||
|
||||
// 标签值:直接以接口返回的「标签名 -> 值」为准(v-yz 指令解析用)。
|
||||
// 数据驱动:表里新增的设置项会自动进入 siteTags,模板里写 {yz:新tag} 即可,无需改代码。
|
||||
Object.assign(siteTags, data);
|
||||
}
|
||||
|
||||
/** 顶部工具条链接 */
|
||||
export const utilityLinks: NavItem[] = [
|
||||
@@ -65,9 +134,16 @@ export const footerColumns: FooterColumn[] = [
|
||||
},
|
||||
];
|
||||
|
||||
/** 页脚底部合规链接 */
|
||||
export const policyLinks: NavItem[] = [
|
||||
{ label: "隐私政策", path: "/policy/privacy" },
|
||||
{ label: "使用条款", path: "/policy/terms" },
|
||||
/**
|
||||
* 页脚底部合规链接
|
||||
* 隐私政策 {yz:privacy} / 使用条款 {yz:terms} 取自站点设置,未配置时回退站内默认页面
|
||||
*/
|
||||
export const policyLinks = computed<NavItem[]>(() => [
|
||||
{ label: "隐私政策", path: siteInfo.privacy || "/policy/privacy" },
|
||||
{ label: "使用条款", path: siteInfo.terms || "/policy/terms" },
|
||||
{ label: "网站地图", path: "/policy/sitemap" },
|
||||
];
|
||||
]);
|
||||
|
||||
/** 是否为站外链接(站点设置里可能填完整 URL) */
|
||||
export const isExternalLink = (path: string): boolean =>
|
||||
/^https?:\/\//i.test(path);
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* {yz:xxx} 站点标签指令(dedecms 风格)
|
||||
*
|
||||
* 用法:在模板里直接写标签,元素加 v-yz 即可
|
||||
* <p v-yz>{yz:icp}</p>
|
||||
* <p v-yz>备案号:{yz:icp} | {yz:policeicp}</p>
|
||||
*
|
||||
* 元素内所有 {yz:标签名} 会被替换为「官网管理 - 站点设置」中对应的值,
|
||||
* 未配置的标签输出空字符串(与后端 themes 模板行为一致)。
|
||||
*
|
||||
* 注意:指令通过 innerHTML 替换内容,因此元素内只能放纯文本/HTML 片段,
|
||||
* 不要放子组件或需要响应式绑定的内容;需要条件渲染的地方请用 siteInfo.xxx。
|
||||
*/
|
||||
import { watchEffect, type Directive } from "vue";
|
||||
import { siteTags } from "@/config/site";
|
||||
|
||||
/** 与后端 tagengine 的全局标签正则保持一致 */
|
||||
const TAG_PATTERN = /\{yz:([a-zA-Z_]\w*)\}/g;
|
||||
|
||||
/** 每个元素对应的 watchEffect 停止函数 */
|
||||
const stops = new WeakMap<HTMLElement, () => void>();
|
||||
|
||||
/** 依据当前标签值渲染元素内容;始终基于首次保存的原文,避免重复替换 */
|
||||
function render(el: HTMLElement): void {
|
||||
if (!el.dataset.yzRaw) {
|
||||
el.dataset.yzRaw = el.innerHTML;
|
||||
}
|
||||
const raw = el.dataset.yzRaw;
|
||||
|
||||
el.innerHTML = raw.replace(
|
||||
TAG_PATTERN,
|
||||
(_match: string, key: string) => siteTags[key] ?? ""
|
||||
);
|
||||
}
|
||||
|
||||
export const yz: Directive<HTMLElement> = {
|
||||
mounted(el) {
|
||||
// watchEffect 会追踪 render 中读取到的标签值;
|
||||
// 站点设置是异步拉取的,数据到位后会自动重新渲染
|
||||
stops.set(el, watchEffect(() => render(el)));
|
||||
},
|
||||
unmounted(el) {
|
||||
stops.get(el)?.();
|
||||
stops.delete(el);
|
||||
},
|
||||
};
|
||||
|
||||
export default yz;
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { footerColumns, policyLinks, siteInfo } from "@/config/site";
|
||||
import { footerColumns, isExternalLink, policyLinks, siteInfo } from "@/config/site";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -8,8 +8,19 @@ import { footerColumns, policyLinks, siteInfo } from "@/config/site";
|
||||
<!-- 品牌与联系方式 -->
|
||||
<div class="footer-brand">
|
||||
<RouterLink to="/" class="footer-brand__logo">
|
||||
<span class="footer-brand__mark">{{ siteInfo.brandMark }}</span>
|
||||
<span class="footer-brand__name">{{ siteInfo.fullName }}</span>
|
||||
<!-- 站点设置 - 浅色 Logo({yz:logow}),未配置时回退品牌文字标记 -->
|
||||
<img
|
||||
v-if="siteInfo.logoLight"
|
||||
:src="siteInfo.logoLight"
|
||||
:alt="siteInfo.brandName"
|
||||
class="footer-brand__logo-img"
|
||||
/>
|
||||
<template v-else>
|
||||
<span class="footer-brand__mark">{{ siteInfo.brandMark }}</span>
|
||||
<span class="footer-brand__name">{{
|
||||
siteInfo.fullName || siteInfo.brandName
|
||||
}}</span>
|
||||
</template>
|
||||
</RouterLink>
|
||||
|
||||
<ul class="footer-info">
|
||||
@@ -17,9 +28,12 @@ import { footerColumns, policyLinks, siteInfo } from "@/config/site";
|
||||
<span>地址</span>{{ siteInfo.address }}(邮编
|
||||
{{ siteInfo.postalCode }})
|
||||
</li>
|
||||
<li>
|
||||
<span>备案</span>京ICP备 00000000 号-1 | 京公网安备
|
||||
11010800000000 号
|
||||
<!-- 站点设置 - ICP 备案({yz:icp})与公安备案({yz:policeicp}) -->
|
||||
<li v-if="siteInfo.icp || siteInfo.policeIcp">
|
||||
<span>备案</span>
|
||||
{{ siteInfo.icp }}
|
||||
<template v-if="siteInfo.icp && siteInfo.policeIcp"> | </template>
|
||||
{{ siteInfo.policeIcp }}
|
||||
</li>
|
||||
<li>
|
||||
<span>联系</span>服务热线:{{ siteInfo.phone }} · 商务合作:{{
|
||||
@@ -48,12 +62,22 @@ import { footerColumns, policyLinks, siteInfo } from "@/config/site";
|
||||
|
||||
<div class="site-footer__bottom">
|
||||
<div class="container site-footer__bottom-inner">
|
||||
<p class="copyright">{{ siteInfo.copyright }}</p>
|
||||
<!-- 站点设置 - ICP 备案号:标签模式,{yz:icp} 由 v-yz 指令替换为站点设置中的值 -->
|
||||
<p class="copyright" v-yz>{yz:icp}</p>
|
||||
|
||||
<div class="footer-end">
|
||||
<ul class="policy-links">
|
||||
<li v-for="link in policyLinks" :key="link.path">
|
||||
<RouterLink :to="link.path">{{ link.label }}</RouterLink>
|
||||
<!-- 站点设置 - 隐私政策({yz:privacy})/ 使用条款({yz:terms}),可能为站外完整 URL -->
|
||||
<a
|
||||
v-if="isExternalLink(link.path)"
|
||||
:href="link.path"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
{{ link.label }}
|
||||
</a>
|
||||
<RouterLink v-else :to="link.path">{{ link.label }}</RouterLink>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -204,6 +228,14 @@ import { footerColumns, policyLinks, siteInfo } from "@/config/site";
|
||||
margin-bottom: 26px;
|
||||
}
|
||||
|
||||
/* 站点设置下发的浅色 Logo {yz:logow} */
|
||||
&__logo-img {
|
||||
height: 38px;
|
||||
width: auto;
|
||||
max-width: 210px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
&__mark {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -48,10 +48,19 @@ watch(
|
||||
<div class="header-main">
|
||||
<div class="container header-main__inner">
|
||||
<RouterLink to="/" class="brand" aria-label="返回首页">
|
||||
<span class="brand__mark">{{ siteInfo.brandMark }}</span>
|
||||
<!-- 站点设置 - 深色 Logo({yz:logo}),未配置时回退品牌文字标记 -->
|
||||
<img
|
||||
v-if="siteInfo.logoDark"
|
||||
:src="siteInfo.logoDark"
|
||||
:alt="siteInfo.brandName"
|
||||
class="brand__logo"
|
||||
/>
|
||||
<span v-else class="brand__mark">{{ siteInfo.brandMark }}</span>
|
||||
<span class="brand__text">
|
||||
<!-- 站点设置 - 站点名称({yz:sitename}) -->
|
||||
<strong>{{ siteInfo.brandName }}</strong>
|
||||
<em>{{ siteInfo.brandSub }}</em>
|
||||
<!-- 站点设置 - 副标题({yz:subtitle}) -->
|
||||
<em>{{ siteInfo.subtitle || siteInfo.brandSub }}</em>
|
||||
</span>
|
||||
</RouterLink>
|
||||
|
||||
@@ -184,6 +193,14 @@ watch(
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
|
||||
/* 站点设置下发的深色 Logo {yz:logo} */
|
||||
&__logo {
|
||||
height: 42px;
|
||||
width: auto;
|
||||
max-width: 190px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
&__mark {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -201,7 +218,7 @@ watch(
|
||||
&__text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
line-height: 1.15;
|
||||
line-height: 1.4;
|
||||
|
||||
strong {
|
||||
font-size: 20px;
|
||||
|
||||
@@ -2,8 +2,16 @@ import { createApp } from "vue";
|
||||
import App from "./App.vue";
|
||||
import router from "./router"; // 配置路由
|
||||
import store from "./store"; // 配置vuex
|
||||
import { loadSiteInfo } from "./config/site"; // 站点设置:品牌/Logo/备案/版权/隐私政策等(与官网模板标签同源)
|
||||
import { yz } from "./directives/yz"; // {yz:xxx} 站点标签解析指令
|
||||
import "./styles/index.less";
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
// 注册 {yz:xxx} 标签指令:模板里写 <p v-yz>{yz:icp}</p> 即可
|
||||
app.directive("yz", yz);
|
||||
|
||||
// 站点设置先于渲染发起,接口异常时静默回退到 config/site.ts 内的默认配置
|
||||
void loadSiteInfo();
|
||||
|
||||
app.use(router).use(store).mount("#app");
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//index.ts
|
||||
import { createRouter, createWebHashHistory, type RouteRecordRaw } from "vue-router";
|
||||
import { siteInfo } from "@/config/site"; // 站点设置(站点名称 {yz:sitename} 等)
|
||||
|
||||
const DefaultLayout = () => import("../layout/DefaultLayout.vue");
|
||||
const HomeView = () => import("../views/home/index.vue");
|
||||
@@ -192,10 +193,11 @@ const router = createRouter({
|
||||
},
|
||||
});
|
||||
|
||||
/** 根据路由 meta 同步浏览器标题 */
|
||||
/** 根据路由 meta 同步浏览器标题(站点名称取自站点设置 {yz:sitename}) */
|
||||
router.afterEach((to) => {
|
||||
const title = (to.meta.title as string | undefined) ?? "";
|
||||
document.title = title ? `${title} | 云泽科技` : "云泽科技 | 官方网站";
|
||||
const siteName = siteInfo.brandName || siteInfo.sitename;
|
||||
document.title = title ? `${title} | ${siteName}` : `${siteName} | 官方网站`;
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -8,6 +8,7 @@ import PageHero from "@/components/PageHero.vue";
|
||||
import BlockRenderer from "@/components/BlockRenderer.vue";
|
||||
import ContentCard from "@/components/ContentCard.vue";
|
||||
import AppIcon from "@/components/AppIcon.vue";
|
||||
import { siteInfo } from "@/config/site";
|
||||
|
||||
/**
|
||||
* 通用详情页
|
||||
@@ -57,10 +58,11 @@ async function load() {
|
||||
related.value = recommend;
|
||||
loading.value = false;
|
||||
|
||||
// 详情页标题跟随内容变化
|
||||
// 详情页标题跟随内容变化(站点名称取自站点设置 {yz:sitename})
|
||||
const siteName = siteInfo.brandName || siteInfo.sitename;
|
||||
document.title = detail
|
||||
? `${detail.title} | 云泽科技`
|
||||
: `${current.title} | 云泽科技`;
|
||||
? `${detail.title} | ${siteName}`
|
||||
: `${current.title} | ${siteName}`;
|
||||
}
|
||||
|
||||
watch(() => `${section.value.key}|${slug.value}`, load, { immediate: true });
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import heroImage from "@/assets/images/hero-cover.svg";
|
||||
|
||||
const crumbs = ["首页", "关于云泽", "企业简介"];
|
||||
import { siteInfo } from "@/config/site";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -13,7 +12,8 @@ const crumbs = ["首页", "关于云泽", "企业简介"];
|
||||
|
||||
<div class="container hero__inner">
|
||||
|
||||
<h1 class="hero__title">云泽科技</h1>
|
||||
<!-- 站点设置 - 站点名称({yz:sitename}) -->
|
||||
<h1 class="hero__title">{{ siteInfo.brandName }}</h1>
|
||||
|
||||
<p class="hero__desc">
|
||||
以云计算与数据智能为核心,为企业提供从咨询规划、产品研发到落地运维的<br />
|
||||
|
||||
@@ -6,6 +6,7 @@ import { fetchSinglePage } from "@/api/content";
|
||||
import PageHero from "@/components/PageHero.vue";
|
||||
import BlockRenderer from "@/components/BlockRenderer.vue";
|
||||
import AppIcon from "@/components/AppIcon.vue";
|
||||
import { siteInfo } from "@/config/site";
|
||||
|
||||
/**
|
||||
* 通用单页
|
||||
@@ -29,10 +30,11 @@ watch(
|
||||
page.value = (await fetchSinglePage(key.value)) ?? null;
|
||||
loading.value = false;
|
||||
|
||||
// 单页标题跟随内容变化
|
||||
// 单页标题跟随内容变化(站点名称取自站点设置 {yz:sitename})
|
||||
const siteName = siteInfo.brandName || siteInfo.sitename;
|
||||
document.title = page.value
|
||||
? `${page.value.title} | 云泽科技`
|
||||
: "云泽科技 | 官方网站";
|
||||
? `${page.value.title} | ${siteName}`
|
||||
: `${siteName} | 官方网站`;
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user