From fc363cc9c5c0b4df19c076cf7c5a916a8e213871 Mon Sep 17 00:00:00 2001 From: Dream Hunter Date: Sat, 5 Sep 2026 00:15:38 +0800 Subject: [PATCH] feat: support runtime frontend configuration (#1135) --- CHANGELOG.md | 1 + CHANGELOG_EN.md | 1 + frontend/.env.example | 1 + frontend/index.html | 1 + frontend/src/App.vue | 9 +-- frontend/src/__tests__/config.test.js | 71 +++++++++++++++++++ frontend/src/api/index.js | 3 +- .../src/components/AddressCredentialModal.vue | 3 +- frontend/src/config.ts | 39 ++++++++++ frontend/src/i18n/utils.ts | 7 +- vitepress-docs/docs/.vitepress/en.ts | 1 + vitepress-docs/docs/.vitepress/zh.ts | 1 + .../docs/en/guide/actions/github-action.md | 2 +- vitepress-docs/docs/en/guide/cli/pages.md | 8 +++ .../docs/en/guide/feature/google-ads.md | 38 ++++++---- vitepress-docs/docs/en/guide/frontend-vars.md | 44 ++++++++++++ vitepress-docs/docs/en/guide/ui/pages.md | 57 ++++++++++----- .../docs/zh/guide/actions/github-action.md | 2 +- vitepress-docs/docs/zh/guide/cli/pages.md | 8 +++ .../docs/zh/guide/feature/google-ads.md | 38 ++++++---- vitepress-docs/docs/zh/guide/frontend-vars.md | 44 ++++++++++++ vitepress-docs/docs/zh/guide/ui/pages.md | 57 ++++++++++----- 22 files changed, 364 insertions(+), 72 deletions(-) create mode 100644 frontend/src/__tests__/config.test.js create mode 100644 frontend/src/config.ts create mode 100644 vitepress-docs/docs/en/guide/frontend-vars.md create mode 100644 vitepress-docs/docs/zh/guide/frontend-vars.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 8479b07..e6d59f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ ### Features +- feat: |Frontend| 新增 `VITE_DEFAULT_LANG` 构建变量,并支持通过 `index.html` 运行时配置覆盖前端设置 - feat: |邮件| 新增可选的已读/未读状态,支持点击邮件自动已读和手动切换状态 - feat: |Admin| 数据库页面新增 D1 存储容量展示,支持选择并保存 Free 或 Workers Paid 套餐,对比当前数据库大小和容量上限 - feat: |Admin| 创建邮箱页面支持一键生成随机邮箱名称(issue #1126) diff --git a/CHANGELOG_EN.md b/CHANGELOG_EN.md index 2442753..d2ef948 100644 --- a/CHANGELOG_EN.md +++ b/CHANGELOG_EN.md @@ -10,6 +10,7 @@ ### Features +- feat: |Frontend| Add the `VITE_DEFAULT_LANG` build variable and support overriding frontend settings through runtime configuration in `index.html` - feat: |Mail| Add optional read/unread status with click-to-read and manual status switching - feat: |Admin| Add D1 storage capacity details to the database page, with persistent Free and Workers Paid plan selection and a comparison between the current database size and capacity limit - feat: |Admin| Add one-click random email-name generation to the address creation page (issue #1126) diff --git a/frontend/.env.example b/frontend/.env.example index e2bdffe..63a0aa8 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -1,3 +1,4 @@ VITE_API_BASE=https://temp-email-api.xxx.xxx +VITE_DEFAULT_LANG=zh VITE_CF_WEB_ANALY_TOKEN= VITE_IS_TELEGRAM=false diff --git a/frontend/index.html b/frontend/index.html index f32967b..79164f8 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -14,6 +14,7 @@ + diff --git a/frontend/src/App.vue b/frontend/src/App.vue index d51764c..7bc3adb 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -12,12 +12,13 @@ import Footer from './views/Footer.vue'; import { api } from './api' import { getNaiveLocaleConfig } from './i18n/naive-locale' import { DEFAULT_LOCALE, isSupportedLocale } from './i18n/utils' +import { APP_CONFIG } from './config' const { isDark, loading, useSideMargin, telegramApp, isTelegram } = useGlobalState() -const adClient = import.meta.env.VITE_GOOGLE_AD_CLIENT; -const adSlot = import.meta.env.VITE_GOOGLE_AD_SLOT; +const adClient = APP_CONFIG.GOOGLE_AD_CLIENT; +const adSlot = APP_CONFIG.GOOGLE_AD_SLOT; const { locale } = useI18n({ useScope: 'global' }); const theme = computed(() => isDark.value ? darkTheme : null) const localeConfig = computed(() => getNaiveLocaleConfig(isSupportedLocale(locale.value) ? locale.value : DEFAULT_LOCALE)) @@ -47,7 +48,7 @@ onMounted(async () => { console.error(error); } - const token = import.meta.env.VITE_CF_WEB_ANALY_TOKEN; + const token = APP_CONFIG.CF_WEB_ANALY_TOKEN; const exist = document.querySelector('script[src="https://static.cloudflareinsights.com/beacon.min.js"]') !== null if (token && !exist) { @@ -66,7 +67,7 @@ onMounted(async () => { // check if telegram is enabled - const enableTelegram = import.meta.env.VITE_IS_TELEGRAM; + const enableTelegram = APP_CONFIG.IS_TELEGRAM; if ( (typeof enableTelegram === 'boolean' && enableTelegram === true) || diff --git a/frontend/src/__tests__/config.test.js b/frontend/src/__tests__/config.test.js new file mode 100644 index 0000000..9178b9b --- /dev/null +++ b/frontend/src/__tests__/config.test.js @@ -0,0 +1,71 @@ +// @vitest-environment jsdom + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +const setRuntimeConfig = (config) => { + const element = document.createElement('script') + element.id = 'app-config' + element.type = 'application/json' + element.textContent = JSON.stringify(config) + document.head.appendChild(element) +} + +describe('APP_CONFIG', () => { + beforeEach(() => { + vi.resetModules() + vi.stubEnv('VITE_API_BASE', 'https://build.example.com') + vi.stubEnv('VITE_DEFAULT_LANG', 'zh') + vi.stubEnv('VITE_IS_TELEGRAM', 'false') + }) + + afterEach(() => { + document.querySelector('#app-config')?.remove() + vi.unstubAllEnvs() + }) + + it('uses build settings when runtime settings are absent', async () => { + const { APP_CONFIG } = await import('../config') + + expect(APP_CONFIG.API_BASE).toBe('https://build.example.com') + expect(APP_CONFIG.DEFAULT_LANG).toBe('zh') + }) + + it('overrides only settings provided by index.html', async () => { + setRuntimeConfig({ API_BASE: 'https://runtime.example.com', DEFAULT_LANG: 'en' }) + + const { APP_CONFIG } = await import('../config') + + expect(APP_CONFIG.API_BASE).toBe('https://runtime.example.com') + expect(APP_CONFIG.DEFAULT_LANG).toBe('en') + }) + + it('allows an explicit empty runtime value', async () => { + setRuntimeConfig({ API_BASE: '' }) + + const { APP_CONFIG } = await import('../config') + + expect(APP_CONFIG.API_BASE).toBe('') + expect(APP_CONFIG.DEFAULT_LANG).toBe('zh') + }) + + it('falls back to build settings for invalid runtime value types', async () => { + setRuntimeConfig({ API_BASE: {}, DEFAULT_LANG: 1, IS_TELEGRAM: [] }) + + const { APP_CONFIG } = await import('../config') + + expect(APP_CONFIG.API_BASE).toBe('https://build.example.com') + expect(APP_CONFIG.DEFAULT_LANG).toBe('zh') + expect(APP_CONFIG.IS_TELEGRAM).toBe('false') + }) + + it('reads runtime settings only once', async () => { + setRuntimeConfig({ DEFAULT_LANG: 'en' }) + const firstImport = await import('../config') + + document.querySelector('#app-config').textContent = JSON.stringify({ DEFAULT_LANG: 'de' }) + const secondImport = await import('../config') + + expect(secondImport.APP_CONFIG).toBe(firstImport.APP_CONFIG) + expect(secondImport.APP_CONFIG.DEFAULT_LANG).toBe('en') + }) +}) diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index 9eeee9b..30b481b 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -6,8 +6,9 @@ import i18n from '../i18n' import { getFingerprint } from '../utils/fingerprint' import { safeBearerHeader, safeHeaderValue } from '../utils/headers' import { sanitizeHtml } from '../utils/sanitize-html' +import { APP_CONFIG } from '../config' -const API_BASE = import.meta.env.VITE_API_BASE || ""; +const API_BASE = APP_CONFIG.API_BASE || ""; const { loading, auth, jwt, settings, openSettings, userOpenSettings, userSettings, announcement, diff --git a/frontend/src/components/AddressCredentialModal.vue b/frontend/src/components/AddressCredentialModal.vue index a752c21..8f13302 100644 --- a/frontend/src/components/AddressCredentialModal.vue +++ b/frontend/src/components/AddressCredentialModal.vue @@ -1,6 +1,7 @@ ``` + +See [Manual ZIP Deployment](/en/guide/ui/pages) for the complete steps. diff --git a/vitepress-docs/docs/en/guide/frontend-vars.md b/vitepress-docs/docs/en/guide/frontend-vars.md new file mode 100644 index 0000000..e428dc4 --- /dev/null +++ b/vitepress-docs/docs/en/guide/frontend-vars.md @@ -0,0 +1,44 @@ +# Frontend Variables + +Frontend configuration is public in the browser. Do not put passwords, API keys, or other secrets in these values. + +## Configuration Methods + +### ENV + +For CLI deployment, add the variables to `frontend/.env.prod`. For Worker Assets or Page Functions deployment, use `frontend/.env.pages.local`. For the GitHub Actions `Deploy Frontend` workflow, put the same content in the `FRONTEND_ENV` repository secret. The `Deploy Frontend with page function` workflow does not read this secret. + +```ini +VITE_API_BASE=https://temp-email-api.example.com +VITE_DEFAULT_LANG=en +``` + +See [CLI Deployment](/en/guide/cli/pages) and [GitHub Actions Deployment](/en/guide/actions/github-action) for complete steps. + +### index.html + +When using a prebuilt frontend ZIP, edit `app-config` in `index.html`. Field names do not use the `VITE_` prefix: + +```html + +``` + +Fields set in `app-config` override ENV build values. Omitted fields, or a missing `app-config` tag, continue to use ENV build values. See [Manual ZIP Deployment](/en/guide/ui/pages) for complete steps. + +## Variable Reference + +No frontend variable is required for every deployment method. `VITE_API_BASE` is required only when the frontend and backend are deployed separately and the browser must request the Worker domain directly. Leave it empty for same-origin deployments such as Worker Assets or Page Functions. + +| ENV Variable | `app-config` Field | Required | Type | Default | Description | +| --- | --- | --- | --- | --- | --- | +| `VITE_API_BASE` | `API_BASE` | Depends on deployment | Text | Empty | Backend API root URL beginning with `https://` and without a trailing `/`; an empty value uses the same-origin API | +| `VITE_DEFAULT_LANG` | `DEFAULT_LANG` | No | Text | `zh` | Default language: `zh`, `en`, `es`, `pt-BR`, `ja`, or `de` | +| `VITE_CF_WEB_ANALY_TOKEN` | `CF_WEB_ANALY_TOKEN` | No | Text | Empty | Cloudflare Web Analytics Token | +| `VITE_IS_TELEGRAM` | `IS_TELEGRAM` | No | Boolean | `false` | Whether to enable Telegram Mini App; see [Telegram Configuration](/en/guide/feature/telegram) | +| `VITE_GOOGLE_AD_CLIENT` | `GOOGLE_AD_CLIENT` | No | Text | Empty | Google AdSense Client ID; see [Google Ads Configuration](/en/guide/feature/google-ads) | +| `VITE_GOOGLE_AD_SLOT` | `GOOGLE_AD_SLOT` | No | Text | Empty | Google AdSense Slot ID; see [Google Ads Configuration](/en/guide/feature/google-ads) | diff --git a/vitepress-docs/docs/en/guide/ui/pages.md b/vitepress-docs/docs/en/guide/ui/pages.md index 83c7856..75c1f48 100644 --- a/vitepress-docs/docs/en/guide/ui/pages.md +++ b/vitepress-docs/docs/en/guide/ui/pages.md @@ -68,24 +68,34 @@ const generate = async () => { const arrayBuffer = await response.arrayBuffer(); var zip = new JSZip(); await zip.loadAsync(arrayBuffer); - let target_path = "" - const directory = zip.folder("assets"); - if (directory) { - for (const [relativePath, zipEntry] of Object.entries(directory.files)) { - console.log(relativePath); - if (relativePath.startsWith("assets/index-") && relativePath.endsWith(".js")){ - let content = await zipEntry.async("string"); - content = content.replaceAll("https://temp-email-api.xxx.xxx", normalizedDomain); - target_path = relativePath; - zip.file(relativePath, content); - break; - } - } - } - if (!target_path) { - errorMessage.value = "Could not find the frontend entry file. Generation failed" + const indexEntry = zip.file("index.html"); + if (!indexEntry) { + errorMessage.value = "Could not find index.html. Generation failed" return } + const content = await indexEntry.async("string"); + const configPattern = /( + ``` + + `API_BASE` is the backend API root URL without a trailing `/`; `DEFAULT_LANG` supports `zh`, `en`, `es`, `pt-BR`, `ja`, and `de`. You can also set `CF_WEB_ANALY_TOKEN`, `IS_TELEGRAM`, `GOOGLE_AD_CLIENT`, and `GOOGLE_AD_SLOT`; see [Frontend Variables](/en/guide/frontend-vars) for their purpose and values. Fields present in `app-config` override the corresponding settings built into JavaScript, while omitted fields keep their existing settings. + 4. Select `Pages`, click `Create Pages`, modify the name, upload the downloaded zip package > [!warning] Important: SPA Mode diff --git a/vitepress-docs/docs/zh/guide/actions/github-action.md b/vitepress-docs/docs/zh/guide/actions/github-action.md index 4f4342b..09ca361 100644 --- a/vitepress-docs/docs/zh/guide/actions/github-action.md +++ b/vitepress-docs/docs/zh/guide/actions/github-action.md @@ -44,7 +44,7 @@ | 名称 | 说明 | | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | - | `FRONTEND_ENV` | `Deploy Frontend` workflow 使用的前端配置文件,请复制 `frontend/.env.example` 的内容,[并参考此处修改](/zh/guide/cli/pages.html)。如果是前后端分离直连 Worker,`VITE_API_BASE` 应填写后端 Worker API 根地址,并且以 `https://` 开头、末尾不要带 `/`。地址配置错误时,常见现象是前端报 `map` 错误或接口返回 `405` | + | `FRONTEND_ENV` | `Deploy Frontend` workflow 使用的前端配置文件,请复制 `frontend/.env.example` 的内容,并参考 [前端变量说明](/zh/guide/frontend-vars) 修改 | | `FRONTEND_NAME` | 你在 Cloudflare Pages 创建的项目名称,可通过 [用户界面](https://temp-mail-docs.awsl.uk/zh/guide/ui/pages.html) 或者 [命令行](https://temp-mail-docs.awsl.uk/zh/guide/cli/pages.html) 创建 | | `FRONTEND_BRANCH` | (可选) pages 部署的分支,可不配置,默认 `production` | | `PAGE_TOML` | (可选) 仅供 `Deploy Frontend with page function` workflow 使用。通过 page functions 转发后端请求时需要配置,请复制 `pages/wrangler.toml` 的内容,并根据实际情况修改 `service` 字段为你的 worker 后端名称。这个 workflow 会以 Pages 模式构建前端并走同域请求,因此不会读取 `FRONTEND_ENV` | diff --git a/vitepress-docs/docs/zh/guide/cli/pages.md b/vitepress-docs/docs/zh/guide/cli/pages.md index 87f3f1f..6fe3167 100644 --- a/vitepress-docs/docs/zh/guide/cli/pages.md +++ b/vitepress-docs/docs/zh/guide/cli/pages.md @@ -29,6 +29,12 @@ cp .env.example .env.prod 例如: `VITE_API_BASE=https://xxx.xxx.workers.dev` +可通过 `VITE_DEFAULT_LANG` 设置前端默认语言,支持 `zh`、`en`、`es`、`pt-BR`、`ja`、`de`,未配置或配置无效时使用 `zh`。 + +例如: `VITE_DEFAULT_LANG=en` + +其他配置项请查看 [前端变量说明](/zh/guide/frontend-vars)。 + ```bash pnpm build --emptyOutDir # 第一次部署会提示创建项目, production 分支请填写 production @@ -47,6 +53,8 @@ pnpm run deploy 如果你的 worker 后端 名称不为 `cloudflare_temp_email` 请修改 `pages/wrangler.toml` +如需设置默认语言,在 `frontend/.env.pages.local` 中添加 `VITE_DEFAULT_LANG=en`。同域请求不需要设置 `VITE_API_BASE`,其他配置项请查看 [前端变量说明](/zh/guide/frontend-vars)。 + ```bash cd frontend pnpm install diff --git a/vitepress-docs/docs/zh/guide/feature/google-ads.md b/vitepress-docs/docs/zh/guide/feature/google-ads.md index 8dd5db0..dcacc47 100644 --- a/vitepress-docs/docs/zh/guide/feature/google-ads.md +++ b/vitepress-docs/docs/zh/guide/feature/google-ads.md @@ -1,17 +1,17 @@ # 给网页增加 Google Ads -## 命令行部署 +Google Ads 使用以下两个变量,具体值请参考 [Google AdSense](https://www.google.com/adsense/start/): -修改 `.env.prod` 文件 - -增加下列两个变量, 具体的值请参考 [Google AdSense](https://www.google.com/adsense/start/) 的说明 - -```txt +```ini VITE_GOOGLE_AD_CLIENT=ca-pub-123456 VITE_GOOGLE_AD_SLOT=123456 ``` -然后执行下列命令, 重新部署 pages 即可. +任意一个变量为空时都不会加载广告。变量类型和默认值请查看 [前端变量说明](/zh/guide/frontend-vars)。 + +## CLI 部署 + +在 `frontend/.env.prod` 中添加上述两个变量,然后重新构建并部署: ```bash pnpm build --emptyOutDir @@ -19,11 +19,25 @@ pnpm build --emptyOutDir pnpm run deploy ``` -## GitHub Action 部署 +完整步骤请查看 [CLI 部署前端](/zh/guide/cli/pages)。 -修改 `FRONTEND_ENV`, 增加下列两个变量, 具体的值请参考 [Google AdSense](https://www.google.com/adsense/start/) 的说明, 重新部署 pages 即可. +## GitHub Actions 部署 -```txt -VITE_GOOGLE_AD_CLIENT=ca-pub-123456 -VITE_GOOGLE_AD_SLOT=123456 +在已有的 `FRONTEND_ENV` Secret 中添加上述两个变量,然后重新运行 `Deploy Frontend` workflow。 + +完整步骤请查看 [GitHub Actions 部署](/zh/guide/actions/github-action)。 + +## 手动 ZIP 部署 + +编辑压缩包中 `index.html` 的 `app-config`,使用不带 `VITE_` 前缀的字段名: + +```html + ``` + +完整步骤请查看 [手动 ZIP 部署](/zh/guide/ui/pages)。 diff --git a/vitepress-docs/docs/zh/guide/frontend-vars.md b/vitepress-docs/docs/zh/guide/frontend-vars.md new file mode 100644 index 0000000..12b08bc --- /dev/null +++ b/vitepress-docs/docs/zh/guide/frontend-vars.md @@ -0,0 +1,44 @@ +# 前端变量说明 + +前端配置会公开在浏览器中,请勿填写密码、API 密钥等敏感信息。 + +## 配置方式 + +### ENV + +通过 CLI 部署时,将变量写入 `frontend/.env.prod`;Worker Assets 或 Page Functions 部署写入 `frontend/.env.pages.local`。通过 GitHub Actions 的 `Deploy Frontend` workflow 部署时,将相同内容写入 Repository Secret `FRONTEND_ENV`;`Deploy Frontend with page function` workflow 不读取该 Secret。 + +```ini +VITE_API_BASE=https://temp-email-api.example.com +VITE_DEFAULT_LANG=en +``` + +完整步骤请查看 [CLI 部署](/zh/guide/cli/pages) 和 [GitHub Actions 部署](/zh/guide/actions/github-action)。 + +### index.html + +使用构建好的前端 ZIP 时,可以编辑 `index.html` 中的 `app-config`。字段名不带 `VITE_` 前缀: + +```html + +``` + +`app-config` 中填写的字段会覆盖 ENV 构建值;未填写字段或不存在 `app-config` 标签时继续使用 ENV 构建值。完整步骤请查看 [手动 ZIP 部署](/zh/guide/ui/pages)。 + +## 变量列表 + +没有所有部署方式都必须配置的前端变量。`VITE_API_BASE` 仅在前后端分离、前端需要直接请求 Worker 域名时配置;Worker Assets 或 Page Functions 等同域部署应留空。 + +| ENV 变量 | `app-config` 字段 | 是否必须 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | --- | --- | +| `VITE_API_BASE` | `API_BASE` | 视部署方式 | 文本 | 空 | 以 `https://` 开头的后端 API 根地址,不要带结尾 `/`;空值表示使用同域 API | +| `VITE_DEFAULT_LANG` | `DEFAULT_LANG` | 否 | 文本 | `zh` | 默认语言,支持 `zh`、`en`、`es`、`pt-BR`、`ja`、`de` | +| `VITE_CF_WEB_ANALY_TOKEN` | `CF_WEB_ANALY_TOKEN` | 否 | 文本 | 空 | Cloudflare Web Analytics Token | +| `VITE_IS_TELEGRAM` | `IS_TELEGRAM` | 否 | 布尔值 | `false` | 是否启用 Telegram Mini App,详见 [Telegram 配置](/zh/guide/feature/telegram) | +| `VITE_GOOGLE_AD_CLIENT` | `GOOGLE_AD_CLIENT` | 否 | 文本 | 空 | Google AdSense Client ID,详见 [Google Ads 配置](/zh/guide/feature/google-ads) | +| `VITE_GOOGLE_AD_SLOT` | `GOOGLE_AD_SLOT` | 否 | 文本 | 空 | Google AdSense Slot ID,详见 [Google Ads 配置](/zh/guide/feature/google-ads) | diff --git a/vitepress-docs/docs/zh/guide/ui/pages.md b/vitepress-docs/docs/zh/guide/ui/pages.md index ee35379..1e5e162 100644 --- a/vitepress-docs/docs/zh/guide/ui/pages.md +++ b/vitepress-docs/docs/zh/guide/ui/pages.md @@ -68,24 +68,34 @@ const generate = async () => { const arrayBuffer = await response.arrayBuffer(); var zip = new JSZip(); await zip.loadAsync(arrayBuffer); - let target_path = "" - const directory = zip.folder("assets"); - if (directory) { - for (const [relativePath, zipEntry] of Object.entries(directory.files)) { - console.log(relativePath); - if (relativePath.startsWith("assets/index-") && relativePath.endsWith(".js")){ - let content = await zipEntry.async("string"); - content = content.replaceAll("https://temp-email-api.xxx.xxx", normalizedDomain); - target_path = relativePath; - zip.file(relativePath, content); - break; - } - } - } - if (!target_path) { - errorMessage.value = "没有找到前端入口文件,生成失败" + const indexEntry = zip.file("index.html"); + if (!indexEntry) { + errorMessage.value = "没有找到 index.html,生成失败" return } + const content = await indexEntry.async("string"); + const configPattern = /( + ``` + + `API_BASE` 是后端 API 根地址,不要带结尾 `/`;`DEFAULT_LANG` 支持 `zh`、`en`、`es`、`pt-BR`、`ja`、`de`。还可以设置 `CF_WEB_ANALY_TOKEN`、`IS_TELEGRAM`、`GOOGLE_AD_CLIENT`、`GOOGLE_AD_SLOT`,用途和取值请查看 [前端变量说明](/zh/guide/frontend-vars)。`app-config` 中存在的字段会覆盖构建进 JS 的对应配置,未填写的字段继续使用原配置。 + 4. 选择 `Pages`,点击 `Create Pages`, 修改名称,上传下载的 zip 包 > [!warning] 重要:SPA 模式