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 模式