systemStore

framework 内置的系统配置 Pinia store,composable 名是 useSystemStore(不是 useSystemInfoStore,源码位于 src/runtime/store/system.ts)。

承载后端 /system/info 接口返回的全站配置:SEO header、导航菜单、广告位、热词、上传接口、统计配置等。所有页面直接 useSystemStore() 读取,无需手动 fetch。

基础用法

<script setup lang="ts">
const systemStore = useSystemStore()

// 读取系统信息
const title = computed(() => systemStore.info.header?.site_title ?? '默认站名')
const ogImage = computed(() => systemStore.info.header?.og_image)
</script>

<template>
  <h1>{{ title }}</h1>
  <nav v-if="systemStore.isLoaded">
    <NuxtLink v-for="m in systemStore.menus" :key="m.nav_id" :to="m.link || `/${m.code}`">
      {{ m.name }}
    </NuxtLink>
  </nav>
</template>

自动初始化

framework 内置 system.server.ts plugin 在 SSR 期自动拉 /system/info 写入此 store,业务无需手动调用 fetch。失败时 system.client.ts 在 client hydrate 后兜底重拉。

整站无后端 API 的场景用 systemInfo: false 关闭拉取:

// nuxt.config.ts
swiftConfig: {
  systemInfo: false,                              // 整站不拉
  // systemInfo: { ignorePaths: ['/preview'] },   // 或路径级跳过
}

不存在「systemInfo 路径豁免」这类独立配置项;详见 framework 配置项 §systemInfo。

State

字段类型说明
infoRef<SystemInfo>完整系统信息(含 header / menus / ads / 上传接口 / 统计配置 等)
channelCodeRef<string>渠道码 cookie 引用(持久化 30 天)
shareCodeRef<string>邀请码 cookie 引用(持久化 30 天)

Getters(computed)

字段类型说明
isLoadedRef<boolean>系统信息是否已从服务端加载(false 时 info 为默认兜底值)
menusRef<MenuItem[]>导航菜单列表(info.menus 派生,空数组兜底)
hotKeywordsRef<HotKeyword[]>热词推荐列表(info.keywords 派生)
adsRef<AdSlots | null>广告位配置(info.ads 派生)
imageDomainRef<string>图片 CDN 域名前缀(info.image_domain 派生)

Actions

方法签名说明
setInfo(data)(data: SystemInfo) => void写入完整系统信息并置 _loaded=true(应用初始化时由 system.server 自动调)
setChannelCode(code)(string) => void写入渠道码 cookie(已有值时不覆盖,保护首次归属)
setShareCode(code)(string) => void写入邀请码 cookie(同上,不覆盖)
getChannelCode()() => string读取渠道码(登录接口传参用,未设置时返回 '')
getShareCode()() => string读取邀请码(登录接口传参用)
initCode()() => void从当前路由 query 读取 channelCode / shareCode 写 cookie(仅当未设置时)
reset()() => void重置为默认兜底值(测试用)

SystemInfo 主要类型

interface SystemInfo {
  header?: SystemHeader      // SEO meta(site_title / keywords / description / og_image)
  site_url?: string          // 官方主域名(业务用,如「最新地址」跳转链接;与 useSeo origin 决策无关,详见 framework#87)
  img_key?: string           // 图片加密 key(解密 .bnc 用)
  websocket_key?: string
  image_domain?: string      // 图片 CDN 域名前缀
  upload_image_url?: string  // 图片上传接口
  upload_file_url?: string   // 文件上传接口
  menus?: MenuItem[]
  ads?: AdSlots
  keywords?: HotKeyword[]    // 热词推荐
  domain_statistic?: DomainStatistic[]
  global_statistic?: string  // 全局统计脚本片段
  service_link?: string      // 客服链接
  group_link?: string        // 官方社群
  permanent_url?: string     // 永久域名落地页
  tracker?: { appId?: string; channel?: string; encryptedConfig?: string; endpoint?: string; siteKey?: string }  // 埋点通道下发配置:appId+encryptedConfig=集团 WebSDK 通道、endpoint(+siteKey)=自建通道(#228);两者皆缺则静默不启用
  // ...更多字段(service_email / place_ad / contact_us / center_customer_* 等)
  [key: string]: unknown     // 后端可扩展任意字段
}

interface SystemHeader {
  site_title?: string
  keywords?: string          // SEO 关键词(逗号分隔)
  description?: string       // SEO 描述
  og_image?: string          // OG / Twitter 分享封面
  /** 旧后端字段,og_image 的长期兼容兜底(后端协议兼容、非 framework 废弃 API;新代码一律用 og_image) */
  img?: string
}

完整字段列表见 src/runtime/store/system.ts 顶部 interface 定义。

渠道码 / 邀请码语义(重要)

setChannelCode / setShareCode 的"已有值不覆盖"语义仅针对同一会话内的多次访问(保护首次渠道归属)。跨用户场景由 userStore.logout() 清空 cookie 来重置 —— 切勿改成"总是覆盖",否则会破坏首次归属保护。

initCode() 安全策略:

  • 种植时点在 client(fw#212):由 system.client.ts 在 hydration 早期调用,SSR 期不写渠道 cookie——SSR 写入会被 ISR 缓存整组重放 Set-Cookie、绕过「已有值不覆盖」保护
  • 防御性收紧:?switch=1 路径下(guest middleware 的"已登录切换账号"旁路)不接受 URL 注入的渠道码 / 邀请码,阻断反常构造链接(如 /login?switch=1&channelCode=111)对未绑定用户的渠道码种植

与 /system/info 接口约定

后端实际返回示例(按 framework 内 frameworkInternalRequest 解密后的 data 字段):

{
  "header": {
    "site_title": "Swift Web 示例",
    "keywords": "示例,关键词",
    "description": "SEO 描述",
    "og_image": "https://cdn.example.com/og-default.jpg"
  },
  "site_url": "https://example.com",
  "img_key": "aes_image_decrypt_key",
  "image_domain": "https://img.example.com",
  "menus": [{ "name": "首页", "nav_id": "1", "code": "home", "link": "", "extend": "" }],
  "upload_image_url": "https://upload.example.com/image",
  "ads": { /* ... */ }
}

SSR 拉取不带渠道参数

system.server.ts 拉 /system/info 时不带 channel_code / share_code(避免 ISR 路径上首访者参数烤进缓存广播)。后端按"默认渠道"返回;业务侧如需渠道差异化 systemInfo,应在 client hydrate 后由 auth.client.ts 完成 channel cookie 写入后显式触发二次拉取(此能力暂未内置,待业务有真实需求再实现)。

详见 渲染模式快速指南 §渠道差异化。

相关

  • userStore — 用户身份 + token + 设备 ID
  • useSeo — SEO meta 注入器(不从 systemStore.info.header 隐式兜底,SEO 字段全显式;header 字段仍供业务读取,与 SEO 解耦)
  • systemInfo ModuleOptions — 拉取开关 + 自定义端点

注意事项

  • info 初始是兜底默认值,不是空 —— 未从服务端加载时是 createDefaultSystemInfo() 的结构完整的默认对象。判断「数据到了没」要用 isLoaded getter,别靠 info 是否为空(永远不为空),也别靠某个字段有没有值。
  • 渠道码 / 邀请码「首次写入即锁定」,后续不覆盖 —— setChannelCode / setShareCode 在已有值时故意不覆盖,保证渠道归属算首次来源;initCode() 从当前路由 query 读取写入,cookie 持久 30 天。想改现有值需要先清 cookie,直接再调 set 是无效的。
  • SEO 不从本 store 隐式兜底 —— useSeo() 的 title / description 一律取页面显式传入;info.header 里的 SEO 相关字段要用,得由页面自己读出来再传进 useSeo()(数据流:后端 → store → 页面 → useSeo)。framework 绝不绕开 useSeo() 改 head。
  • ISR 页面上它是「全站共享配置」而非用户态 —— 站点配置对所有访客一致,进 ISR 缓存是安全的;但渠道 / 邀请码是随访客而变的,别把它们渲染进 ISR 页面的 HTML(会把首访者的渠道码烙给所有人)。