diff --git a/dashboard/backend/src/routes/www/global.ts b/dashboard/backend/src/routes/www/global.ts
index 6102b7a..990438c 100644
--- a/dashboard/backend/src/routes/www/global.ts
+++ b/dashboard/backend/src/routes/www/global.ts
@@ -29,15 +29,25 @@ router.get('/api/www/global/site', async (_req: Request, res: Response) => {
// PUT /api/www/global/site
router.put('/api/www/global/site', async (req: Request, res: Response) => {
try {
- const { site_title, favicon_url, logo_dark_url, logo_light_url } = req.body;
+ const { site_title, favicon_url, logo_dark_url, logo_light_url, enabled_locales } = req.body;
const [existing] = await db.select().from(wwwSiteSettings).limit(1);
+ // 多语言:前端可能传数组或逗号分隔字符串,白名单过滤后落库;空值保留原配置
+ let localesValue: string | undefined;
+ if (enabled_locales !== undefined) {
+ const list = (Array.isArray(enabled_locales) ? enabled_locales : String(enabled_locales).split(','))
+ .map((v: unknown) => String(v).trim())
+ .filter(v => v === 'zh' || v === 'en');
+ if (list.length > 0) localesValue = [...new Set(list)].join(',');
+ }
+
if (existing) {
await db.update(wwwSiteSettings).set({
siteTitle: site_title ?? existing.siteTitle,
faviconUrl: favicon_url ?? existing.faviconUrl,
logoDarkUrl: logo_dark_url ?? existing.logoDarkUrl,
logoLightUrl: logo_light_url ?? existing.logoLightUrl,
+ enabledLocales: localesValue ?? existing.enabledLocales,
}).where(eq(wwwSiteSettings.id, existing.id));
} else {
await db.insert(wwwSiteSettings).values({
@@ -45,6 +55,7 @@ router.put('/api/www/global/site', async (req: Request, res: Response) => {
faviconUrl: favicon_url || '',
logoDarkUrl: logo_dark_url || '',
logoLightUrl: logo_light_url || '',
+ enabledLocales: localesValue || 'zh,en',
});
}
diff --git a/dashboard/backend/src/schemas/www/globalSettings.ts b/dashboard/backend/src/schemas/www/globalSettings.ts
index 75036d1..78e0cdb 100644
--- a/dashboard/backend/src/schemas/www/globalSettings.ts
+++ b/dashboard/backend/src/schemas/www/globalSettings.ts
@@ -7,6 +7,7 @@ export const wwwSiteSettings = mysqlTable('www_site_settings', {
faviconUrl: varchar('favicon_url', { length: 500 }).notNull().default(''),
logoDarkUrl: varchar('logo_dark_url', { length: 500 }).notNull().default(''),
logoLightUrl: varchar('logo_light_url', { length: 500 }).notNull().default(''),
+ enabledLocales: varchar('enabled_locales', { length: 20 }).notNull().default('zh,en'),
updatedAt: datetime('updated_at').notNull().default(sql`CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP`),
});
diff --git a/dashboard/frontend/patches/vdirs@0.1.8.patch b/dashboard/frontend/patches/vdirs@0.1.8.patch
new file mode 100644
index 0000000..40066ad
--- /dev/null
+++ b/dashboard/frontend/patches/vdirs@0.1.8.patch
@@ -0,0 +1,158 @@
+diff --git a/es/clickoutside.js b/es/clickoutside.js
+index 3dcb41588ad00463f58be7c972882bc9d49c0f06..eef77f163c02a525f2810ab9294cb4179c3bac97 100644
+--- a/es/clickoutside.js
++++ b/es/clickoutside.js
+@@ -13,6 +13,12 @@ const clickoutside = {
+ }
+ },
+ updated(el, { value, modifiers }) {
++ // patched: `updated` may fire without `mounted` (teleport remount), initialize lazily
++ if (!el[ctxKey]) {
++ el[ctxKey] = {
++ handler: undefined
++ };
++ }
+ const ctx = el[ctxKey];
+ if (typeof value === 'function') {
+ if (ctx.handler) {
+@@ -43,6 +49,8 @@ const clickoutside = {
+ }
+ },
+ unmounted(el, { modifiers }) {
++ if (!el[ctxKey])
++ return;
+ const { handler } = el[ctxKey];
+ if (handler) {
+ off('clickoutside', el, handler, {
+diff --git a/es/mousemoveoutside.js b/es/mousemoveoutside.js
+index f15be455deb61f8af83abc285b46f7931fd1cb06..2f2c6ffe9c33ace8e23dc3c51d75420dc563e95e 100644
+--- a/es/mousemoveoutside.js
++++ b/es/mousemoveoutside.js
+@@ -11,6 +11,12 @@ const mousemoveoutside = {
+ }
+ },
+ updated(el, { value }) {
++ // patched: `updated` may fire without `mounted` (teleport remount), initialize lazily
++ if (!el[ctxKey]) {
++ el[ctxKey] = {
++ handler: undefined
++ };
++ }
+ const ctx = el[ctxKey];
+ if (typeof value === 'function') {
+ if (ctx.handler) {
+@@ -33,6 +39,8 @@ const mousemoveoutside = {
+ }
+ },
+ unmounted(el) {
++ if (!el[ctxKey])
++ return;
+ const { handler } = el[ctxKey];
+ if (handler) {
+ off('mousemoveoutside', el, handler);
+diff --git a/es/zindexable/index.js b/es/zindexable/index.js
+index 8066b8c39003c8b09419586b125c5b866eb5c788..b7680bedb5823c2464305081164284c189a426b1 100644
+--- a/es/zindexable/index.js
++++ b/es/zindexable/index.js
+@@ -17,6 +17,13 @@ const zindexable = {
+ updated(el, bindings) {
+ const { value = {} } = bindings;
+ const { zIndex, enabled } = value;
++ // patched: `updated` may fire without `mounted` (teleport remount), initialize lazily
++ if (!el[ctx]) {
++ el[ctx] = {
++ enabled: false,
++ initialized: false
++ };
++ }
+ const cachedEnabled = el[ctx].enabled;
+ if (enabled && !cachedEnabled) {
+ zIndexManager.ensureZIndex(el, zIndex);
+@@ -25,7 +32,7 @@ const zindexable = {
+ el[ctx].enabled = !!enabled;
+ },
+ unmounted(el, bindings) {
+- if (!el[ctx].initialized)
++ if (!el[ctx] || !el[ctx].initialized)
+ return;
+ const { value = {} } = bindings;
+ const { zIndex } = value;
+diff --git a/lib/clickoutside.js b/lib/clickoutside.js
+index 300e65614d1789e1ee477460b58b88aab94b6588..af39ce4e8b5deac36fac9ea8560aba676e852a54 100644
+--- a/lib/clickoutside.js
++++ b/lib/clickoutside.js
+@@ -15,6 +15,12 @@ const clickoutside = {
+ }
+ },
+ updated(el, { value, modifiers }) {
++ // patched: `updated` may fire without `mounted` (teleport remount), initialize lazily
++ if (!el[ctxKey]) {
++ el[ctxKey] = {
++ handler: undefined
++ };
++ }
+ const ctx = el[ctxKey];
+ if (typeof value === 'function') {
+ if (ctx.handler) {
+@@ -45,6 +51,8 @@ const clickoutside = {
+ }
+ },
+ unmounted(el, { modifiers }) {
++ if (!el[ctxKey])
++ return;
+ const { handler } = el[ctxKey];
+ if (handler) {
+ (0, evtd_1.off)('clickoutside', el, handler, {
+diff --git a/lib/mousemoveoutside.js b/lib/mousemoveoutside.js
+index daf5afa22dd0a715c91bf81703161878ffbfb6fd..1608823f4257d924e870710bf55a295255948d74 100644
+--- a/lib/mousemoveoutside.js
++++ b/lib/mousemoveoutside.js
+@@ -13,6 +13,12 @@ const mousemoveoutside = {
+ }
+ },
+ updated(el, { value }) {
++ // patched: `updated` may fire without `mounted` (teleport remount), initialize lazily
++ if (!el[ctxKey]) {
++ el[ctxKey] = {
++ handler: undefined
++ };
++ }
+ const ctx = el[ctxKey];
+ if (typeof value === 'function') {
+ if (ctx.handler) {
+@@ -35,6 +41,8 @@ const mousemoveoutside = {
+ }
+ },
+ unmounted(el) {
++ if (!el[ctxKey])
++ return;
+ const { handler } = el[ctxKey];
+ if (handler) {
+ (0, evtd_1.off)('mousemoveoutside', el, handler);
+diff --git a/lib/zindexable/index.js b/lib/zindexable/index.js
+index ac608e9c13376a0e21f9fea8802c1d7bfb9d1c58..9305912d112a91dc0f6eb354bb2aaa95b3351dca 100644
+--- a/lib/zindexable/index.js
++++ b/lib/zindexable/index.js
+@@ -19,6 +19,13 @@ const zindexable = {
+ updated(el, bindings) {
+ const { value = {} } = bindings;
+ const { zIndex, enabled } = value;
++ // patched: `updated` may fire without `mounted` (teleport remount), initialize lazily
++ if (!el[ctx]) {
++ el[ctx] = {
++ enabled: false,
++ initialized: false
++ };
++ }
+ const cachedEnabled = el[ctx].enabled;
+ if (enabled && !cachedEnabled) {
+ z_index_manager_1.default.ensureZIndex(el, zIndex);
+@@ -27,7 +34,7 @@ const zindexable = {
+ el[ctx].enabled = !!enabled;
+ },
+ unmounted(el, bindings) {
+- if (!el[ctx].initialized)
++ if (!el[ctx] || !el[ctx].initialized)
+ return;
+ const { value = {} } = bindings;
+ const { zIndex } = value;
diff --git a/dashboard/frontend/pnpm-lock.yaml b/dashboard/frontend/pnpm-lock.yaml
index d3c4fe3..278388c 100644
--- a/dashboard/frontend/pnpm-lock.yaml
+++ b/dashboard/frontend/pnpm-lock.yaml
@@ -4,6 +4,9 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
+patchedDependencies:
+ vdirs@0.1.8: d2c61ceb63f1b1d07b884b02d6d92e59099989b7f5a59a39301adad995e046b5
+
importers:
.:
@@ -7516,7 +7519,7 @@ snapshots:
lodash-es: 4.18.1
seemly: 0.3.10
treemate: 0.3.11
- vdirs: 0.1.8(vue@3.5.34(typescript@6.0.3))
+ vdirs: 0.1.8(patch_hash=d2c61ceb63f1b1d07b884b02d6d92e59099989b7f5a59a39301adad995e046b5)(vue@3.5.34(typescript@6.0.3))
vooks: 0.2.12(vue@3.5.34(typescript@6.0.3))
vue: 3.5.34(typescript@6.0.3)
vueuc: 0.4.65(vue@3.5.34(typescript@6.0.3))
@@ -8555,7 +8558,7 @@ snapshots:
vary@1.1.2: {}
- vdirs@0.1.8(vue@3.5.34(typescript@6.0.3)):
+ vdirs@0.1.8(patch_hash=d2c61ceb63f1b1d07b884b02d6d92e59099989b7f5a59a39301adad995e046b5)(vue@3.5.34(typescript@6.0.3)):
dependencies:
evtd: 0.2.4
vue: 3.5.34(typescript@6.0.3)
@@ -8742,7 +8745,7 @@ snapshots:
css-render: 0.15.14
evtd: 0.2.4
seemly: 0.3.10
- vdirs: 0.1.8(vue@3.5.34(typescript@6.0.3))
+ vdirs: 0.1.8(patch_hash=d2c61ceb63f1b1d07b884b02d6d92e59099989b7f5a59a39301adad995e046b5)(vue@3.5.34(typescript@6.0.3))
vooks: 0.2.12(vue@3.5.34(typescript@6.0.3))
vue: 3.5.34(typescript@6.0.3)
diff --git a/dashboard/frontend/pnpm-workspace.yaml b/dashboard/frontend/pnpm-workspace.yaml
index 68c2949..e147270 100644
--- a/dashboard/frontend/pnpm-workspace.yaml
+++ b/dashboard/frontend/pnpm-workspace.yaml
@@ -9,3 +9,5 @@ allowBuilds:
shamefullyHoist: true
ignoreWorkspaceRootCheck: true
linkWorkspacePackages: true
+patchedDependencies:
+ vdirs@0.1.8: patches/vdirs@0.1.8.patch
diff --git a/dashboard/frontend/src/typings/api/www.d.ts b/dashboard/frontend/src/typings/api/www.d.ts
index 7123f4d..5cd5935 100644
--- a/dashboard/frontend/src/typings/api/www.d.ts
+++ b/dashboard/frontend/src/typings/api/www.d.ts
@@ -7,6 +7,8 @@ declare namespace Api {
favicon_url: string;
logo_dark_url: string;
logo_light_url: string;
+ /** 启用的语言(逗号分隔):zh,en */
+ enabled_locales: string;
updated_at?: string;
}
diff --git a/dashboard/frontend/src/views/www/global/basic/index.vue b/dashboard/frontend/src/views/www/global/basic/index.vue
index acfa018..4a1f98f 100644
--- a/dashboard/frontend/src/views/www/global/basic/index.vue
+++ b/dashboard/frontend/src/views/www/global/basic/index.vue
@@ -14,11 +14,23 @@ const formData = reactive({
site_title: '',
favicon_url: '',
logo_dark_url: '',
- logo_light_url: ''
+ logo_light_url: '',
+ enabled_locales: ['zh', 'en'] as string[]
});
+const localeOptions = [
+ { label: '中文', value: 'zh' },
+ { label: '英文', value: 'en' }
+];
+
const rules: FormRules = {
- site_title: [{ required: true, message: '请输入站点标题', trigger: ['blur', 'input'] }]
+ site_title: [{ required: true, message: '请输入站点标题', trigger: ['blur', 'input'] }],
+ enabled_locales: [
+ {
+ validator: (_rule, value: string[]) => (value.length > 0 ? true : new Error('至少选择一种语言')),
+ trigger: ['blur', 'change']
+ }
+ ]
};
async function loadData() {
@@ -30,13 +42,16 @@ async function loadData() {
formData.favicon_url = data.favicon_url || '';
formData.logo_dark_url = data.logo_dark_url || '';
formData.logo_light_url = data.logo_light_url || '';
+ formData.enabled_locales = (data.enabled_locales || 'zh,en').split(',').filter(v => v === 'zh' || v === 'en');
}
}
async function handleSave() {
await formRef.value?.validate();
saving.value = true;
- const { error } = await fetchUpdateSiteSettings({ ...formData });
+ // 多语言以逗号分隔字符串落库
+ const { enabled_locales, ...rest } = formData;
+ const { error } = await fetchUpdateSiteSettings({ ...rest, enabled_locales: enabled_locales.join(',') });
saving.value = false;
if (!error) {
window.$message?.success('保存成功');
@@ -61,6 +76,12 @@ onMounted(loadData);
+
+
+
+
可多选;仅选一种语言时,官网顶部不再显示语言切换按钮
+
+
diff --git a/dashboard/frontend/src/views/www/news/articles/index.vue b/dashboard/frontend/src/views/www/news/articles/index.vue
index e83b64e..51bcd40 100644
--- a/dashboard/frontend/src/views/www/news/articles/index.vue
+++ b/dashboard/frontend/src/views/www/news/articles/index.vue
@@ -2,10 +2,12 @@
import { computed, h, onMounted, reactive, ref } from 'vue';
import { NButton, NSpace, NTag, type DataTableColumns, type FormInst, type FormRules } from 'naive-ui';
import MediaSelectInput from '../../components/MediaSelectInput.vue';
+import RichTextEditor from '../../components/RichTextEditor.vue';
import {
fetchCreateNewsArticle,
fetchDeleteNewsArticle,
fetchGetNews,
+ fetchGetNewsArticle,
fetchUpdateNewsArticle
} from '@/service/api';
@@ -41,7 +43,7 @@ const formData = reactive({
content_zh: '',
content_en: '',
status: 'draft' as string,
- published_at: null as string | null
+ published_at: null as number | null
});
const rules: FormRules = {
@@ -140,8 +142,9 @@ function handleAdd() {
dialogVisible.value = true;
}
-function handleEdit(row: Api.Www.NewsArticle) {
+async function handleEdit(row: Api.Www.NewsArticle) {
dialogTitle.value = '编辑文章';
+ // 列表接口不返回正文,先回填列表字段,再拉详情补全,避免保存时把正文覆盖为空
formData.id = row.id;
formData.title_zh = row.title_zh;
formData.title_en = row.title_en;
@@ -149,11 +152,19 @@ function handleEdit(row: Api.Www.NewsArticle) {
formData.summary_zh = row.summary_zh;
formData.summary_en = row.summary_en;
formData.cover_url = row.cover_url;
- formData.content_zh = row.content_zh;
- formData.content_en = row.content_en;
+ formData.content_zh = '';
+ formData.content_en = '';
formData.status = row.status;
- formData.published_at = row.published_at;
+ formData.published_at = row.published_at ? new Date(row.published_at).getTime() : null;
dialogVisible.value = true;
+
+ const { data, error } = await fetchGetNewsArticle(row.id);
+ if (error || !data || data.id !== formData.id) return;
+ formData.summary_zh = data.summary_zh ?? formData.summary_zh;
+ formData.summary_en = data.summary_en ?? formData.summary_en;
+ formData.cover_url = data.cover_url ?? formData.cover_url;
+ formData.content_zh = data.content_zh || '';
+ formData.content_en = data.content_en || '';
}
function handleDelete(row: Api.Www.NewsArticle) {
@@ -175,7 +186,9 @@ function handleDelete(row: Api.Www.NewsArticle) {
async function handleSubmit() {
await formRef.value?.validate();
submitLoading.value = true;
- const { id, ...payload } = formData;
+ const { id, published_at, ...rest } = formData;
+ // 时间戳 → ISO 字符串(UTC),与后端 DATETIME 存储格式对齐
+ const payload = { ...rest, published_at: published_at ? new Date(published_at).toISOString() : null };
const { error } = id
? await fetchUpdateNewsArticle(id, payload)
: await fetchCreateNewsArticle(payload);
@@ -277,16 +290,16 @@ onMounted(loadData);
-
+
-
+
-
+
diff --git a/dashboard/frontend/src/views/www/page/about/index.vue b/dashboard/frontend/src/views/www/page/about/index.vue
index 261dc68..606d17d 100644
--- a/dashboard/frontend/src/views/www/page/about/index.vue
+++ b/dashboard/frontend/src/views/www/page/about/index.vue
@@ -1,27 +1,90 @@
-
-
-
-
-
- 保存
-
-
-
+
+
+
+
+
+ 保存
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 特性 {{ idx + 1 }}
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + 添加特性
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 保存
+
+
+
+
diff --git a/dashboard/frontend/src/views/www/page/support/index.vue b/dashboard/frontend/src/views/www/page/support/index.vue
index 2a48686..03f0534 100644
--- a/dashboard/frontend/src/views/www/page/support/index.vue
+++ b/dashboard/frontend/src/views/www/page/support/index.vue
@@ -1,27 +1,131 @@
-
-
-
-
-
- 保存
-
-
-
+
+
+
+
+
+ 保存
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 分类 {{ cIdx + 1 }}
+
+ 删除分类
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 问答 {{ iIdx + 1 }}
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + 添加问答
+
+
+
+
+ + 添加分类
+
+
+
+
+
+
+
+
+
+ 资源 {{ idx + 1 }}
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + 添加资源
+
+
+
+
+
+
+
+
+
+ 条目 {{ idx + 1 }}
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + 添加条目
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 保存
+
+
+
+
diff --git a/db/01_global_settings.sql b/db/01_global_settings.sql
index 7de34d6..95d19ee 100644
--- a/db/01_global_settings.sql
+++ b/db/01_global_settings.sql
@@ -13,6 +13,7 @@ CREATE TABLE IF NOT EXISTS `www_site_settings` (
`favicon_url` VARCHAR(500) NOT NULL DEFAULT '' COMMENT 'Favicon 素材路径',
`logo_dark_url` VARCHAR(500) NOT NULL DEFAULT '' COMMENT 'Logo 深色版(浅色区块上用)',
`logo_light_url` VARCHAR(500) NOT NULL DEFAULT '' COMMENT 'Logo 浅色版(深色区块上用)',
+ `enabled_locales` VARCHAR(20) NOT NULL DEFAULT 'zh,en' COMMENT '启用的语言(逗号分隔):zh,en',
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
diff --git a/db/07_pages.sql b/db/07_pages.sql
index 91acafc..cd55f44 100644
--- a/db/07_pages.sql
+++ b/db/07_pages.sql
@@ -105,11 +105,11 @@ CREATE TABLE IF NOT EXISTS `www_support_settings` (
`hero_subtitle_en` VARCHAR(200) NOT NULL DEFAULT '' COMMENT 'Hero 副标题(英文)',
`hero_bg_url` VARCHAR(500) NOT NULL DEFAULT '' COMMENT 'Hero 背景图',
-- Section 1: 常见问题分类
- `s1_faq_categories` JSON COMMENT 'FAQ 分类 JSON: [{icon, name_zh, name_en, desc_zh, desc_en}]',
+ `s1_faq_categories` JSON COMMENT 'FAQ 分类 JSON: [{title_zh, title_en, items: [{question_zh, question_en, answer_zh, answer_en}]}]',
-- Section 2: 下载中心
- `s2_downloads` JSON COMMENT '下载资源 JSON: [{name_zh, name_en, file_url, version}]',
+ `s2_downloads` JSON COMMENT '下载资源 JSON: [{name_zh, name_en, file_url, file_size}]',
-- Section 3: 联系方式
- `s3_contact` JSON COMMENT '联系信息 JSON: [{title_zh, title_en, info_zh, info_en, image_url}]',
+ `s3_contact` JSON COMMENT '联系信息 JSON: [{label_zh, label_en, value_zh, value_en}]',
-- Section 4: CTA
`s4_cta_zh` VARCHAR(100) NOT NULL DEFAULT '' COMMENT 'CTA 按钮(中文)',
`s4_cta_en` VARCHAR(100) NOT NULL DEFAULT '' COMMENT 'CTA 按钮(英文)',
diff --git a/db/migrations/x10_product2_spec_audio.sql b/db/migrations/x10_product2_spec_audio.sql
new file mode 100644
index 0000000..0c1d56e
--- /dev/null
+++ b/db/migrations/x10_product2_spec_audio.sql
@@ -0,0 +1,33 @@
+-- 产品 id=2 参数规格区块(www_section_blocks.id=43)「音频参数」分组(www_spec_groups.id=9)参数项录入
+-- 英文按产品资料原文录入,中文为对应原文;多行参数值以换行分隔;sort_order 按展示顺序递增
+INSERT INTO www_spec_items (spec_group_id, name_zh, name_en, value_zh, value_en, sort_order) VALUES
+(9, '播放和DAC解码', 'Play & DAC',
+ '支持立体声 DSD512,PCM 768KHz 32位',
+ 'support DSD512,PCM 768KHz 32-bit', 0),
+(9, '蓝牙音频输入', 'Bluetooth audio input',
+ '蓝牙 BT5.0,支持 SBC/AAC',
+ 'BT5.0,support AAC/SBC', 1),
+(9, 'USB-B音频输入', 'USB-B audio input',
+ '支持 Windows (10, 11),Android,IOS。支持立体声 DSD512,PCM 768KHz 32位',
+ 'support Windows(10, 11),Android,IOS。support DSD512,PCM 768KHz 32-bit', 2),
+(9, '光纤/同轴音频输入', 'Optical/Coaxial audio input',
+ '支持立体声 PCM 192KHz 24位,DoP64',
+ 'support PCM 192KHz 24-bit,DoP64', 3),
+(9, 'HDMI ARC', 'HDMI ARC',
+ '支持立体声 PCM 192KHz 24位',
+ 'support PCM 192KHz 24-bit', 4),
+(9, '模拟前级音频输入', 'Analog input',
+ 'RCA(非平衡),最大增益 +10dB',
+ 'RCA (unbalance),max gain +10dB', 5),
+(9, '模拟音频输出接口', 'Analog audio output interface',
+ 'XLR(平衡)+ RCA(非平衡)+ 低音炮输出',
+ 'XLR(balanced)+RCA(unbalanced)+subwoofer output', 6),
+(9, 'XLR输出音频特性', 'XLR Audio Output',
+ '输出电平:4.2Vrms@0dBFS\n频率响应:20Hz~20KHz (±0.25dB)\n动态范围:> 128dB\n信噪比:>128dB\nTHD+N:<0.000096% (-120.3dB) @ 不计权\n串扰:>-117dB',
+ 'output level 4.2Vrms@0dBFS\nFrequency Response:20Hz~20KHz(±0.25dB)\nDynamic Range:> 128dB\nSignal-to-Noise Ratio(SNR):>128dB\nTHD+N:<0.000096%(-120.3dB)@ Unweighted\nCrosstalk:>-117dB', 7),
+(9, 'RCA输出音频特性', 'RCA Audio Output',
+ '输出电平:2.1Vrms@0dBFS\n频率响应:20Hz~20KHz (±0.25dB)\n动态范围:> 125dB\n信噪比:>125dB\nTHD+N:<0.00010% (-119dB) @ 不计权\n串扰:>-114dB',
+ 'output level 2.1Vrms@0dBFS\nFrequency Response:20Hz~20KHz(±0.25dB)\nDynamic Range:> 125dB\nSignal-to-Noise Ratio(SNR):>125dB\nTHD+N:<0.00010%(-119dB)@ Unweighted\nCrosstalk:>-114dB', 8),
+(9, '耳机输出音频特性', 'Headphone Audio Output',
+ '输出电平:4-15Vrms@0dBFS\n频率响应:20Hz~20KHz (±0.25dB)\n动态范围:> 128dB\n信噪比:>128dB\nTHD+N:<0.00016% (-116dB) @ 不计权',
+ 'output level 4-15Vrms@0dBFS\nFrequency Response:20Hz~20KHz(±0.25dB)\nDynamic Range:> 128dB\nSignal-to-Noise Ratio(SNR):>128dB\nTHD+N:<0.00016%(-116dB)@ Unweighted', 9);
diff --git a/db/migrations/x11_product2_spec_input.sql b/db/migrations/x11_product2_spec_input.sql
new file mode 100644
index 0000000..7cd7a41
--- /dev/null
+++ b/db/migrations/x11_product2_spec_input.sql
@@ -0,0 +1,48 @@
+-- 产品 id=2 参数规格区块(www_section_blocks.id=43)「输入」分组(www_spec_groups.id=10)参数项录入
+-- 中英文按产品资料录入;同一参数中英文不一致时以英文值为准(含拼写勘误:TIRGGER→TRIGGER、去除中文多余字符)
+INSERT INTO www_spec_items (spec_group_id, name_zh, name_en, value_zh, value_en, sort_order) VALUES
+(10, 'USB-A Port', 'USB-A Port',
+ '仅限更新固件用',
+ 'For Firmware Updates Only', 0),
+(10, 'USB-B Port', 'USB-B Port',
+ 'USB Audio系统兼容性: Windows (10, 11),Android,IOS系统 最高支持立体声 DSD512,PCM 768KHz/32Bit',
+ 'USB Audio System Compatibility: Windows (10, 11), Android, iOS; Supports up to Stereo DSD512, PCM 768kHz/32-Bit', 1),
+(10, 'USB Type-C', 'USB Type-C',
+ 'USB Audio系统兼容性: Windows (10, 11),Android,IOS系统 最高支持立体声 DSD512,PCM 768KHz/32Bit',
+ 'USB Audio System Compatibility: Windows (10, 11), Android, iOS; Supports up to Stereo DSD512, PCM 768kHz/32-Bit', 2),
+(10, '模拟单端(输入)', 'Analog (unbalanced input)',
+ 'RAC*2(非平衡),最大增益 +10dB',
+ 'RAC*2 (unbalanced),Max Gain +10dB', 3),
+(10, '光纤', 'Optical',
+ '最高支持立体声 PCM 192KHz 24Bit、DoP64',
+ 'Support up to PCM 192KHz 24-bit,DoP64', 4),
+(10, '同轴', 'Coaxial',
+ 'RAC*1',
+ 'RAC*1', 5),
+(10, '蓝牙接收5.0', 'Bluetooth input',
+ '蓝牙BT5.0,支持SBC/AAC',
+ 'BT5.0,support AAC/SBC', 6),
+(10, 'HDMI ARC', 'HDMI ARC',
+ '最高支持立体声PCM 192KHz 24Bit',
+ 'support PCM 192KHz 24-bit', 7),
+(10, 'TRIGGER接口', 'TRIGGER Connection',
+ '标准3.5mm耳机插座(TRIGGER IN接口*1+TRIGGER OUT接口*1)',
+ '3.5mm Headphone Jack (TRIGGER IN ×1 + TRIGGER OUT ×1)', 8),
+(10, 'WiFi', 'WiFi',
+ 'WiFi 2.4G / 5G',
+ 'WiFi 2.4G / 5G', 9),
+(10, '音量旋钮', 'Volume Knob',
+ '编码器旋钮',
+ 'Encoder Knob', 10),
+(10, '开机按钮', 'Power Button',
+ '单按静音,长按待机',
+ 'Single Press: Mute, Long Press: Standby', 11),
+(10, '电源开关', 'Power Switch',
+ '船形开关',
+ 'Rocker Switch', 12),
+(10, '电源', 'Power Supply',
+ 'AC 100-240V~50/60Hz',
+ 'AC 100-240V~50/60Hz', 13),
+(10, '遥控器', 'Controller',
+ '红外遥控器12键',
+ '12-Button Infrared Remote Control', 14);
diff --git a/db/migrations/x12_product2_spec_output.sql b/db/migrations/x12_product2_spec_output.sql
new file mode 100644
index 0000000..4a6d805
--- /dev/null
+++ b/db/migrations/x12_product2_spec_output.sql
@@ -0,0 +1,20 @@
+-- 产品 id=2 参数规格区块(www_section_blocks.id=43)「输出」分组(www_spec_groups.id=11)参数项录入
+-- 中英文按产品资料录入;勘误:分组英文标题 Ouput→Output;英文参数名 Unbalalced→Unbalanced
+UPDATE www_spec_groups SET title_en = 'Output' WHERE id = 11 AND title_en = 'Ouput';
+
+INSERT INTO www_spec_items (spec_group_id, name_zh, name_en, value_zh, value_en, sort_order) VALUES
+(11, '平衡线路输出接口', 'Balanced Circuit Output',
+ 'XLR3平衡插座',
+ 'XLR3 Balanced Socket', 0),
+(11, '单端线路输出接口', 'Unbalanced Circuit Output',
+ 'RCA插座*2',
+ 'RCA*2', 1),
+(11, '低音炮输出接口', 'Subwoofer Output',
+ 'RCA插座*2',
+ 'RCA*2', 2),
+(11, '平衡耳机输出接口', 'Balanced Headphone Output',
+ '标准4.4mm插座*1+XLR4平衡插座*1',
+ '4.4mm Socket ×1 + XLR4 Balanced Socket ×1', 3),
+(11, '单端耳机输出接口', 'Unbalanced Headphone Output',
+ '标准6.35mm 插座',
+ '6.35mm Socket', 4);
diff --git a/db/migrations/x13_site_settings_enabled_locales.sql b/db/migrations/x13_site_settings_enabled_locales.sql
new file mode 100644
index 0000000..8c2d5e6
--- /dev/null
+++ b/db/migrations/x13_site_settings_enabled_locales.sql
@@ -0,0 +1,5 @@
+-- x13: 站点信息新增「启用的多语言」配置
+-- 逗号分隔的语言代码(zh,en),决定 www 前台展示哪些语言;
+-- 仅选一个语言时,前台隐藏顶部导航的语言切换按钮。
+ALTER TABLE `www_site_settings`
+ ADD COLUMN `enabled_locales` VARCHAR(20) NOT NULL DEFAULT 'zh,en' COMMENT '启用的语言(逗号分隔):zh,en' AFTER `logo_light_url`;
diff --git a/db/migrations/x9_product2_spec_basic.sql b/db/migrations/x9_product2_spec_basic.sql
new file mode 100644
index 0000000..0831cc7
--- /dev/null
+++ b/db/migrations/x9_product2_spec_basic.sql
@@ -0,0 +1,15 @@
+-- 产品 id=2 参数规格区块(www_section_blocks.id=43)「基本规格」分组(www_spec_groups.id=8)参数项录入
+-- 英文按产品资料原文录入,中文为对应翻译;sort_order 按展示顺序递增
+INSERT INTO www_spec_items (spec_group_id, name_zh, name_en, value_zh, value_en, sort_order) VALUES
+(8, '型号', 'Model', 'X9', 'X9', 0),
+(8, '显示屏', 'Display', '4 英寸 TFT 全贴合触摸屏(960×400)', '4-inch TFT Fully Laminated Touchscreen (960X400)', 1),
+(8, '硬件方案', 'Hardware solution','DAC:AK4191EQ+AK4499EX;蓝牙解码:QCC5125', 'DAC: AK4191EQ+AK4499EX; Bluetooth decoding: QCC5125', 2),
+(8, '音频处理器', 'Audio processor', 'AKM 7739 DSP', 'AKM 7739 DSP', 3),
+(8, '运放芯片', 'Op-Amp chip', 'OPA1612', 'OPA1612', 4),
+(8, '尺寸', 'dimension', '宽:300mm × 长:206mm × 高:65mm', 'Width: 300mm * Length: 206mm * Height: 65mm', 5),
+(8, '电源', 'Power supply', '超低噪声线性电源', 'Ultra-Low Noise Linear Power Supply', 6),
+(8, '额定功率', 'Rated Power', '25W', '25W', 7),
+(8, '包装清单', 'Packing list', '电源线 ×1、遥控器 ×1、USB-B 线缆 ×1、用户手册 ×1', 'Power Cable ×1, Remote Controller ×1, USB-B Cable×1, User Manual ×1', 8),
+(8, '颜色', 'Color', '银色', 'Silver', 9),
+(8, '重量', 'Weight', '3.72kg', '3.72kg', 10),
+(8, '操作方式', 'Operation method', '触摸屏、红外 + 蓝牙遥控、Luxsin App(兼容 Android 和 iOS)', 'Touchscreen, Infrared + Bluetooth Remote Control, Luxsin App (compatible with Android and iOS)', 11);
diff --git a/tmp_home_check.png b/tmp_home_check.png
deleted file mode 100644
index d8bd5b3..0000000
Binary files a/tmp_home_check.png and /dev/null differ
diff --git a/www/app/components/AppFooter.vue b/www/app/components/AppFooter.vue
index f9d3615..c54a199 100644
--- a/www/app/components/AppFooter.vue
+++ b/www/app/components/AppFooter.vue
@@ -43,7 +43,7 @@ const wechatHover = ref(false);
{{ footer.contact_phone }}
-
+
{{ footer.contact_email }}
@@ -92,7 +92,7 @@ const wechatHover = ref(false);
{{ footer?.copyright_text || t('footer.copyright', { year: currentYear }) }}
-
{{ footer.icp_number }}
+
{{ footer.icp_number }}
{{ footer.police_number }}
diff --git a/www/app/components/AppHeader.vue b/www/app/components/AppHeader.vue
index 314d77e..eb232c2 100644
--- a/www/app/components/AppHeader.vue
+++ b/www/app/components/AppHeader.vue
@@ -11,6 +11,32 @@ const appStore = useAppStore();
const { navItems, navAppearance, productSeries, site } = storeToRefs(siteStore);
+// ===== 多语言:按站点配置决定可切换的语言 =====
+// 启用的语言代码;未配置时默认全部语言
+const enabledLocaleCodes = computed(() => {
+ const all: string[] = (unref(locales) || []).map(l => l.code);
+ const raw = site.value?.enabled_locales;
+ if (!raw) return all;
+ const list = raw.split(',').map(s => s.trim()).filter(c => all.includes(c));
+ return list.length ? list : all;
+});
+
+// 实际展示的切换按钮 = i18n locales ∩ 启用列表
+const activeLocales = computed(() => (unref(locales) || []).filter(l => enabledLocaleCodes.value.includes(l.code)));
+
+// 只启用一种语言时不显示切换按钮
+const showLocaleSwitch = computed(() => activeLocales.value.length > 1);
+
+// 当前语言被停用(如只启用中文时访问 /en/*):跳转到首个启用语言的对应页面
+watch(
+ [enabledLocaleCodes, () => locale.value],
+ ([codes]) => {
+ if (!site.value || codes.includes(locale.value)) return;
+ const target = codes[0];
+ if (target) navigateTo(switchLocalePath(target as typeof locale.value));
+ }
+);
+
// 滚动状态:滚动后强制毛玻璃背景 + 深色文字
const { y: scrollY } = useWindowScroll();
const isScrolled = computed(() => scrollY.value > 24);
@@ -186,9 +212,9 @@ function closeMobile() {
-
+
-
+
void) {
{{ isEn ? item.name_en : item.name_zh }}
-
+
{{ isEn ? item.value_en : item.value_zh }}
@@ -127,7 +127,7 @@ function onLeave(el: Element, done: () => void) {
{{ isEn ? item.name_en : item.name_zh }}
-
+
{{ isEn ? item.value_en : item.value_zh }}
diff --git a/www/app/pages/about.vue b/www/app/pages/about.vue
index 18cb60d..16d9a60 100644
--- a/www/app/pages/about.vue
+++ b/www/app/pages/about.vue
@@ -63,10 +63,10 @@ const hasS3 = computed(() => Boolean(s3Title.value || s3Cta.value));
-
+
-
-
+
+
{{ s1Title }}
@@ -79,11 +79,11 @@ const hasS3 = computed(() => Boolean(s3Title.value || s3Cta.value));
-
diff --git a/www/app/types/site.ts b/www/app/types/site.ts
index e2ba619..c2c2866 100644
--- a/www/app/types/site.ts
+++ b/www/app/types/site.ts
@@ -11,6 +11,8 @@ export interface SiteSettings {
favicon_url: string;
logo_dark_url: string;
logo_light_url: string;
+ /** 启用的语言(逗号分隔):zh,en */
+ enabled_locales?: string;
}
export interface FooterSettings {