Commit 15c0667c authored by 罗超's avatar 罗超

实现自定义页面动态加载语言

parent 5e6bcbb9
{
"openapi": "3.0.1",
"info": {
"title": "OTA平台项目",
"description": "",
"version": "1.0.0"
},
"tags": [
{
"name": "WebSiteConfig"
}
],
"paths": {
"/api/app/web-site-config/home-page": {
"get": {
"summary": "根据域名获取首页Page",
"deprecated": false,
"description": "",
"tags": [
"WebSiteConfig"
],
"parameters": [
{
"name": "Type",
"in": "query",
"description": "站点类型 1PC 2小程序",
"required": false,
"schema": {
"type": "integer",
"format": "int32"
}
},
{
"name": "PageType",
"in": "query",
"description": "页面类型 1首页 2住宿 3交通 枚举列表",
"required": false,
"schema": {
"type": "integer",
"format": "int32"
}
},
{
"name": "Language",
"in": "query",
"description": "页面语言 en,vi,zh-CN,zh-Hant 单个",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "Domain",
"in": "query",
"description": "域名",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "__tenant",
"in": "header",
"description": "租户ID或租户名称(留空表示默认租户)",
"required": false,
"example": "",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Acme.WebSite.Application.Contracts.Dtos.WebSitePageOutputDto"
}
}
},
"headers": {}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Volo.Abp.Http.RemoteServiceErrorResponse"
}
}
},
"headers": {}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Volo.Abp.Http.RemoteServiceErrorResponse"
}
}
},
"headers": {}
},
"403": {
"description": "Forbidden",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Volo.Abp.Http.RemoteServiceErrorResponse"
}
}
},
"headers": {}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Volo.Abp.Http.RemoteServiceErrorResponse"
}
}
},
"headers": {}
},
"500": {
"description": "Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Volo.Abp.Http.RemoteServiceErrorResponse"
}
}
},
"headers": {}
},
"501": {
"description": "Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Volo.Abp.Http.RemoteServiceErrorResponse"
}
}
},
"headers": {}
}
},
"security": []
}
}
},
"components": {
"schemas": {
"Volo.Abp.Http.RemoteServiceValidationErrorInfo": {
"type": "object",
"properties": {
"message": {
"type": "string",
"nullable": true
},
"members": {
"type": "array",
"items": {
"type": "string"
},
"nullable": true
}
},
"additionalProperties": false
},
"Volo.Abp.Http.RemoteServiceErrorInfo": {
"type": "object",
"properties": {
"code": {
"type": "string",
"nullable": true
},
"message": {
"type": "string",
"nullable": true
},
"details": {
"type": "string",
"nullable": true
},
"data": {
"type": "object",
"additionalProperties": {
"type": "string"
},
"properties": {},
"nullable": true
},
"validationErrors": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Volo.Abp.Http.RemoteServiceValidationErrorInfo"
},
"nullable": true
}
},
"additionalProperties": false
},
"Acme.WebSite.Application.Contracts.Dtos.TemplateData": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Key (如果需要查询 商品,请Key包含 商品关键字)",
"nullable": true
},
"plugData": {
"description": "模板数据 (内容前端自定义)",
"type": "null"
},
"isShelves": {
"type": "integer",
"description": "是否上架[1-上架]",
"format": "int32"
},
"sortNum": {
"type": "integer",
"description": "排序",
"format": "int32"
}
},
"additionalProperties": false,
"description": "内容"
},
"Volo.Abp.Http.RemoteServiceErrorResponse": {
"type": "object",
"properties": {
"error": {
"$ref": "#/components/schemas/Volo.Abp.Http.RemoteServiceErrorInfo"
}
},
"additionalProperties": false
},
"Acme.WebSite.Application.Contracts.Dtos.WebSitePageOutputDto": {
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"type": {
"type": "integer",
"description": "类型 1网站PC 2小程序",
"format": "int32"
},
"pageType": {
"type": "integer",
"description": "页面类型 1首页 2住宿 3交通 枚举列表",
"format": "int32"
},
"pageName": {
"type": "string",
"description": "页面名称",
"nullable": true
},
"language": {
"type": "string",
"description": "页面语言 en,vi,zh-CN,zh-Hant 可多选",
"nullable": true
},
"pageDataList": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Acme.WebSite.Application.Contracts.Dtos.TemplateData"
},
"description": "内容数据",
"nullable": true
},
"createTime": {
"type": "string",
"description": "创建时间",
"nullable": true
},
"updateTime": {
"type": "string",
"description": "更新时间",
"nullable": true
}
},
"additionalProperties": false,
"description": "输出DTO"
}
},
"securitySchemes": {}
},
"servers": []
}
\ No newline at end of file
/**
* 页面翻译服务
* 用于与后端页面翻译接口交互
*/
import Api, { type HttpResponse } from '@/api/OtaRequest'
import type {
BatchGetTranslationsInputDto,
BatchGetTranslationsOutputDto
} from '@/types/translation/pageTranslation'
class PageTranslationService {
/**
* 批量获取翻译(按哈希)
* @param data 请求数据
*/
static async BatchGetTranslationsAsync(
data: BatchGetTranslationsInputDto
): Promise<HttpResponse<BatchGetTranslationsOutputDto>> {
return Api.post('/page-translation/batch-get', data)
}
}
export default PageTranslationService
/**
* 页面翻译相关类型定义
*/
/**
* 需要翻译的文本
*/
export interface TextToTranslateDto {
hash?: string | null;
text?: string | null;
fieldPath?: string | null;
sourceLanguage?: string | null;
}
/**
* 翻译结果项
*/
export interface TranslationItemDto {
text?: string | null;
isManualEdit?: boolean;
source?: string | null;
}
/**
* 批量请求翻译输入
*/
export interface BatchRequestTranslationsInputDto {
pageId?: string | null;
texts?: TextToTranslateDto[] | null;
targetLanguages?: string[] | null;
}
/**
* 批量请求翻译输出
*/
export interface BatchRequestTranslationsOutputDto {
success: boolean;
message?: string | null;
}
/**
* 批量获取翻译输入
*/
export interface BatchGetTranslationsInputDto {
hashes?: string[] | null;
language?: string | null;
fallbackLanguage?: string | null;
}
/**
* 批量获取翻译输出
*/
export interface BatchGetTranslationsOutputDto {
translations?: Record<string, TranslationItemDto> | null;
}
/**
* 批量更新翻译请求中的单条更新项
*/
export interface TranslationUpdateItemDto {
hash?: string | null;
language?: string | null;
translatedText?: string | null;
isManualEdit?: boolean;
}
/**
* 冲突项信息(未强制覆盖的人工翻译)
*/
export interface ConflictItemDto {
hash?: string | null;
language?: string | null;
existingText?: string | null;
reason?: string | null;
}
/**
* 批量更新翻译输入
*/
export interface BatchUpdateTranslationsInputDto {
pageId?: string | null;
updates?: TranslationUpdateItemDto[] | null;
forceOverwrite?: boolean;
}
/**
* 批量更新翻译输出
*/
export interface BatchUpdateTranslationsOutputDto {
success: boolean;
message?: string | null;
updated?: TranslationUpdateItemDto[] | null;
conflicts?: ConflictItemDto[] | null;
}
/**
* 页面翻译相关类型定义
*/
/**
* 需要翻译的文本
*/
export interface TextToTranslateDto {
hash?: string | null;
text?: string | null;
fieldPath?: string | null;
sourceLanguage?: string | null;
}
/**
* 翻译结果项
*/
export interface TranslationItemDto {
text?: string | null;
isManualEdit?: boolean;
source?: string | null;
}
/**
* 批量请求翻译输入
*/
export interface BatchRequestTranslationsInputDto {
pageId?: string | null;
texts?: TextToTranslateDto[] | null;
targetLanguages?: string[] | null;
}
/**
* 批量请求翻译输出
*/
export interface BatchRequestTranslationsOutputDto {
success: boolean;
message?: string | null;
}
/**
* 批量获取翻译输入
*/
export interface BatchGetTranslationsInputDto {
hashes?: string[] | null;
language?: string | null;
fallbackLanguage?: string | null;
}
/**
* 批量获取翻译输出
*/
export interface BatchGetTranslationsOutputDto {
translations?: Record<string, TranslationItemDto> | null;
}
/**
* 批量更新翻译请求中的单条更新项
*/
export interface TranslationUpdateItemDto {
hash?: string | null;
language?: string | null;
translatedText?: string | null;
isManualEdit?: boolean;
}
/**
* 冲突项信息(未强制覆盖的人工翻译)
*/
export interface ConflictItemDto {
hash?: string | null;
language?: string | null;
existingText?: string | null;
reason?: string | null;
}
/**
* 批量更新翻译输入
*/
export interface BatchUpdateTranslationsInputDto {
pageId?: string | null;
updates?: TranslationUpdateItemDto[] | null;
forceOverwrite?: boolean;
}
/**
* 批量更新翻译输出
*/
export interface BatchUpdateTranslationsOutputDto {
success: boolean;
message?: string | null;
updated?: TranslationUpdateItemDto[] | null;
conflicts?: ConflictItemDto[] | null;
}
/**
* 定义需要翻译的字段映射
* 格式:组件类型 -> 字段路径数组
* 字段路径支持嵌套,如 'button.text' 或 'items[].title'
*/
export const TRANSLATABLE_FIELDS: Record<string, string[]> = {
// 文本组件
'text': ['content'],
// 轮播图组件(含 slide 文本)
'carousel': [
'images[].content.title',
'images[].content.subtitle',
'images[].content.description',
'images[].content.buttonText',
'images[].alt',
// 内置搜索框(如果启用)
'searchBox.config.placeholder',
'searchBox.config.buttonText',
'searchBox.config.hotSearches.title',
'searchBox.config.hotSearches.tags[]',
'searchBox.config.hotDestinations.title',
],
// 手风琴画廊组件
'accordion-gallery': [
'items[].title',
'items[].tags[].label',
],
// 卡片组件
'card': [
'title',
'subtitle',
'description',
'buttonText',
],
// 搜索框组件(独立使用时)
'searchBox': [
'placeholder',
'buttonText',
'hotSearches.title',
'hotSearches.tags[]',
'hotDestinations.title',
],
// 图片画廊组件
'image-gallery': [
'items[].title',
'items[].description',
],
// 浮动广告(目前无文本,留空占位)
'floating-ad': [],
// 容器/空白等(无文本)
'grid-container': [],
'spacer': [],
}
/**
* 检查字段路径是否匹配翻译规则
* @param componentType 组件类型
* @param fieldPath 字段路径(如 'title', 'button.text', 'items[].title')
*/
export function isTranslatableField(componentType: string, fieldPath: string): boolean {
const fields = TRANSLATABLE_FIELDS[componentType] || []
return fields.some(pattern => {
// 支持精确匹配和通配符匹配
if (pattern === fieldPath) return true
// 支持数组通配符,如 'items[].title' 匹配 'items[0].title'
const regex = new RegExp('^' + pattern.replace(/\[\]/g, '\\[\\d+\\]') + '$')
return regex.test(fieldPath)
})
}
/**
* 获取组件的所有可翻译字段路径
*/
export function getTranslatableFields(componentType: string): string[] {
return TRANSLATABLE_FIELDS[componentType] || []
}
/**
* 国际化处理工具
* 用于在保存/加载页面数据时处理多语言文本
*/
import type { ComponentNode } from '@/types/pageBuilder'
import { getTranslatableFields, isTranslatableField } from './i18nFieldMap'
/**
* 翻译键格式:i18n:page:{pageId}:comp:{compId}:{fieldPath}
*/
export function generateTranslationKey(
pageId: string,
componentId: string,
fieldPath: string
): string {
return `i18n:page:${pageId}:comp:${componentId}:${fieldPath}`
}
/**
* 解析翻译键
*/
export function parseTranslationKey(key: string): {
pageId: string
componentId: string
fieldPath: string
} | null {
const match = key.match(/^i18n:page:(.+):comp:(.+):(.+)$/)
if (!match) return null
return {
pageId: match[1],
componentId: match[2],
fieldPath: match[3]
}
}
/**
* 判断是否为翻译键
*/
export function isTranslationKey(value: any): boolean {
return typeof value === 'string' && value.startsWith('i18n:page:')
}
/**
* 递归提取对象中的文本并替换为翻译键
* @param obj 要处理的对象
* @param componentType 组件类型
* @param pageId 页面ID
* @param componentId 组件ID
* @param fieldPath 当前字段路径(用于嵌套)
* @param languages 支持的语言列表
*/
function extractTextsRecursive(
obj: any,
componentType: string,
pageId: string,
componentId: string,
fieldPath: string = '',
languages: string[] = []
): { obj: any; translations: Array<{ key: string; texts: Record<string, string> }> } {
const translations: Array<{ key: string; texts: Record<string, string> }> = []
if (obj === null || obj === undefined) {
return { obj, translations }
}
if (Array.isArray(obj)) {
const newArray: any[] = []
obj.forEach((item, index) => {
const result = extractTextsRecursive(
item,
componentType,
pageId,
componentId,
`${fieldPath}[${index}]`,
languages
)
newArray.push(result.obj)
translations.push(...result.translations)
})
return { obj: newArray, translations }
}
if (typeof obj === 'object') {
const newObj: any = {}
for (const [key, value] of Object.entries(obj)) {
const currentPath = fieldPath ? `${fieldPath}.${key}` : key
// 检查是否为可翻译字段
if (typeof value === 'string' && value.trim() && isTranslatableField(componentType, currentPath)) {
// 生成翻译键
const translationKey = generateTranslationKey(pageId, componentId, currentPath)
// 创建翻译记录(所有语言都使用当前文本作为初始值)
const texts: Record<string, string> = {}
languages.forEach(lang => {
texts[lang] = value
})
translations.push({ key: translationKey, texts })
// 替换为翻译键
newObj[key] = translationKey
} else {
// 递归处理嵌套对象
const result = extractTextsRecursive(
value,
componentType,
pageId,
componentId,
currentPath,
languages
)
newObj[key] = result.obj
translations.push(...result.translations)
}
}
return { obj: newObj, translations }
}
return { obj, translations }
}
/**
* 提取组件树中的所有文本并替换为翻译键
* @param components 组件树
* @param pageId 页面ID
* @param languages 支持的语言列表
* @returns 处理后的组件树和翻译记录
*/
export function extractAndReplaceTexts(
components: ComponentNode[],
pageId: string,
languages: string[] = ['zh-CN']
): {
components: ComponentNode[]
translations: Array<{ key: string; texts: Record<string, string> }>
} {
const allTranslations: Array<{ key: string; texts: Record<string, string> }> = []
const processedComponents: ComponentNode[] = []
function processComponent(component: ComponentNode): ComponentNode {
const { obj: processedProps, translations } = extractTextsRecursive(
component.props,
component.type,
pageId,
component.id,
'',
languages
)
allTranslations.push(...translations)
const processedComponent: ComponentNode = {
...component,
props: processedProps,
}
// 递归处理子组件
if (component.children && component.children.length > 0) {
processedComponent.children = component.children.map(child => processComponent(child))
}
return processedComponent
}
components.forEach(comp => {
processedComponents.push(processComponent(comp))
})
return {
components: processedComponents,
translations: allTranslations
}
}
/**
* 将翻译键替换为实际文本
* @param obj 要处理的对象
* @param translationMap 翻译映射表 { translationKey: translatedText }
*/
function replaceKeysRecursive(obj: any, translationMap: Record<string, string>): any {
if (obj === null || obj === undefined) {
return obj
}
if (Array.isArray(obj)) {
return obj.map(item => replaceKeysRecursive(item, translationMap))
}
if (typeof obj === 'object') {
const newObj: any = {}
for (const [key, value] of Object.entries(obj)) {
if (typeof value === 'string' && isTranslationKey(value)) {
// 替换为翻译文本,如果找不到则保留原键
newObj[key] = translationMap[value] || value
} else {
newObj[key] = replaceKeysRecursive(value, translationMap)
}
}
return newObj
}
// 如果是字符串且是翻译键,也尝试替换
if (typeof obj === 'string' && isTranslationKey(obj)) {
return translationMap[obj] || obj
}
return obj
}
/**
* 将组件树中的翻译键替换为实际文本
* @param components 组件树
* @param translationMap 翻译映射表 { translationKey: translatedText }
*/
export function replaceKeysWithTexts(
components: ComponentNode[],
translationMap: Record<string, string>
): ComponentNode[] {
return components.map(component => {
const processedComponent: ComponentNode = {
...component,
props: replaceKeysRecursive(component.props, translationMap),
}
// 递归处理子组件
if (component.children && component.children.length > 0) {
processedComponent.children = replaceKeysWithTexts(component.children, translationMap)
}
return processedComponent
})
}
/**
* 从组件树中收集所有翻译键
*/
export function collectTranslationKeys(components: ComponentNode[]): string[] {
const keys: string[] = []
function collectFromObj(obj: any) {
if (obj === null || obj === undefined) return
if (Array.isArray(obj)) {
obj.forEach(item => collectFromObj(item))
return
}
if (typeof obj === 'object') {
for (const value of Object.values(obj)) {
if (typeof value === 'string' && isTranslationKey(value)) {
keys.push(value)
} else {
collectFromObj(value)
}
}
}
}
components.forEach(component => {
collectFromObj(component.props)
if (component.children) {
collectFromObj(component.children)
}
})
return [...new Set(keys)] // 去重
}
/**
* 页面翻译工具函数
* 用于提取文本、计算哈希、构建字段路径等
*/
import CryptoJS from 'crypto-js'
import type { ComponentNode } from '@/types/pageBuilder'
import { isTranslatableField } from './i18nFieldMap'
import type { TextToTranslateDto, TranslationItemDto } from '@/types/page-builder/pageTranslation'
/**
* 计算文本的 MD5 哈希值
*/
export function calculateTextHash(text: string): string {
// 与后端对齐:SHA256 + Base64
return CryptoJS.SHA256(text).toString(CryptoJS.enc.Base64)
}
/**
* 递归提取对象中的可翻译文本
* @param obj 要处理的对象
* @param componentType 组件类型
* @param fieldPath 当前字段路径(用于嵌套)
* @param sourceLanguage 源语言
* @returns 提取的文本列表
*/
function extractTextsRecursive(
obj: any,
componentType: string,
fieldPath: string = '',
sourceLanguage: string = 'zh-CN'
): TextToTranslateDto[] {
const texts: TextToTranslateDto[] = []
if (obj === null || obj === undefined) {
return texts
}
if (Array.isArray(obj)) {
obj.forEach((item, index) => {
const result = extractTextsRecursive(
item,
componentType,
`${fieldPath}[${index}]`,
sourceLanguage
)
texts.push(...result)
})
return texts
}
if (typeof obj === 'object') {
for (const [key, value] of Object.entries(obj)) {
const currentPath = fieldPath ? `${fieldPath}.${key}` : key
if (typeof value === 'string' && value.trim() && isTranslatableField(componentType, currentPath)) {
const text = value // 保留原始文本参与哈希,避免与后端不一致
const hash = calculateTextHash(text)
texts.push({
hash,
text,
fieldPath: currentPath,
sourceLanguage,
})
} else if (typeof value === 'object') {
// 递归处理嵌套对象
const result = extractTextsRecursive(
value,
componentType,
currentPath,
sourceLanguage
)
texts.push(...result)
}
}
return texts
}
return texts
}
/**
* 从组件树中提取所有需要翻译的文本
* @param components 组件树
* @param sourceLanguage 源语言(默认 zh-CN)
* @returns 提取的文本列表
*/
export function extractTranslatableTexts(
components: ComponentNode[],
sourceLanguage?: string
): TextToTranslateDto[] {
const lang = sourceLanguage && sourceLanguage.trim() ? sourceLanguage : 'zh-CN'
const allTexts: TextToTranslateDto[] = []
function processComponent(component: ComponentNode, parentPath: string = '') {
// 提取当前组件的文本
const texts = extractTextsRecursive(
component.props,
component.type,
'',
lang
)
allTexts.push(...texts)
// 递归处理子组件
if (component.children && component.children.length > 0) {
component.children.forEach((child) => {
processComponent(child)
})
}
}
components.forEach((component, index) => {
processComponent(component)
})
// 去重(基于 hash)
const uniqueTexts = new Map<string, TextToTranslateDto>()
allTexts.forEach(text => {
if (text.hash && !uniqueTexts.has(text.hash)) {
uniqueTexts.set(text.hash, text)
}
})
return Array.from(uniqueTexts.values())
}
/**
* 从组件树中收集所有文本哈希
* @param components 组件树
* @returns 哈希值数组
*/
export function collectTextHashes(components: ComponentNode[]): string[] {
const hashes = new Set<string>()
function collectFromObj(
obj: any,
componentType: string,
fieldPath: string = ''
) {
if (obj === null || obj === undefined) return
if (Array.isArray(obj)) {
obj.forEach((item, index) =>
collectFromObj(item, componentType, `${fieldPath}[${index}]`)
)
return
}
if (typeof obj === 'object') {
for (const [key, value] of Object.entries(obj)) {
const path = fieldPath ? `${fieldPath}.${key}` : key
if (typeof value === 'string' && value.trim()) {
// 仅收集可翻译字段
if (isTranslatableField(componentType, path)) {
const hash = calculateTextHash(value) // 保留原始文本参与哈希
hashes.add(hash)
}
} else {
collectFromObj(value, componentType, path)
}
}
return
}
}
function processComponent(component: ComponentNode) {
collectFromObj(component.props, component.type, '')
if (component.children && component.children.length > 0) {
component.children.forEach(child => processComponent(child))
}
}
components.forEach(component => processComponent(component))
return Array.from(hashes)
}
/**
* 从源组件与目标组件中收集哈希与对应文本(用于翻译保存)
* @param sourceComponents 源语言组件树(用于计算 hash)
* @param targetComponents 目标语言组件树(用于获取当前翻译文本)
* @param sourceLanguage 源语言(仅用于一致性,当前未使用)
* @returns { hash, text, fieldPath } 列表
*/
export function collectTranslationsByHash(
sourceComponents: ComponentNode[],
targetComponents: ComponentNode[],
sourceLanguage: string
): Array<{ hash: string; text: string; fieldPath: string }> {
const result: Array<{ hash: string; text: string; fieldPath: string }> = []
function traversePair(
sourceNode: any,
targetNode: any,
componentType: string,
fieldPath: string = ''
) {
if (sourceNode === null || sourceNode === undefined) return
// 数组:按索引对齐
if (Array.isArray(sourceNode)) {
sourceNode.forEach((item, index) => {
const tgt = Array.isArray(targetNode) ? targetNode[index] : undefined
traversePair(item, tgt, componentType, `${fieldPath}[${index}]`)
})
return
}
// 对象
if (typeof sourceNode === 'object') {
for (const [key, srcVal] of Object.entries(sourceNode)) {
const path = fieldPath ? `${fieldPath}.${key}` : key
const tgtVal = targetNode && typeof targetNode === 'object' ? (targetNode as any)[key] : undefined
if (typeof srcVal === 'string' && srcVal.trim() && isTranslatableField(componentType, path)) {
const hash = calculateTextHash(srcVal)
const text = typeof tgtVal === 'string' ? tgtVal : srcVal
result.push({ hash, text, fieldPath: path })
} else {
traversePair(srcVal, tgtVal, componentType, path)
}
}
return
}
}
function processPair(sourceComponent: ComponentNode, targetComponent?: ComponentNode) {
traversePair(sourceComponent.props, targetComponent?.props, sourceComponent.type, '')
// 子组件按索引对齐
const sourceChildren = sourceComponent.children || []
const targetChildren = targetComponent?.children || []
sourceChildren.forEach((child, idx) => {
processPair(child, targetChildren[idx])
})
}
sourceComponents.forEach((comp, idx) => {
processPair(comp, targetComponents[idx])
})
return result
}
/**
* 根据哈希值替换组件树中的文本
* @param components 组件树
* @param translations 翻译映射 { hash: translatedText }
*/
export function applyTranslations(
components: ComponentNode[],
translations: Record<string, TranslationItemDto>
): ComponentNode[] {
function replaceTextRecursive(obj: any): any {
if (obj === null || obj === undefined) {
return obj
}
if (Array.isArray(obj)) {
return obj.map(item => replaceTextRecursive(item))
}
if (typeof obj === 'object') {
const newObj: any = {}
for (const [key, value] of Object.entries(obj)) {
if (typeof value === 'string' && value.trim()) {
const hash = calculateTextHash(value)
// 如果找到翻译,则替换
const translationItem = translations[hash]
if (translationItem && translationItem.text) {
newObj[key] = translationItem.text
} else {
newObj[key] = value
}
} else {
newObj[key] = replaceTextRecursive(value)
}
}
return newObj
}
return obj
}
return components.map(component => {
const processedComponent: ComponentNode = {
...component,
props: replaceTextRecursive(component.props),
}
// 递归处理子组件
if (component.children && component.children.length > 0) {
processedComponent.children = applyTranslations(
component.children,
translations
)
}
return processedComponent
})
}
<template>
<div class="page-content">
<dynamic-page :page-data="pageData" />
<dynamic-page :page-data="translatedPageData || pageData" />
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import WebSitePageService from '@/services/WebSitePageService'
import i18n from '@/i18n'
import DynamicPage from '@/components/page-builder/DynamicPage.vue'
const pageData = ref<any>(null)
const getPageData = async () => {
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import WebSitePageService from '@/services/WebSitePageService'
import DynamicPage from '@/components/page-builder/DynamicPage.vue'
import { mapLocaleToBackend } from '@/i18n'
import { collectTextHashes, applyTranslations } from '@/utils/pageTranslation'
import PageTranslationService from '@/services/TranslationService'
const pageData = ref<any>(null)
const translatedPageData = ref<any>(null)
const { locale } = useI18n()
// 按目标语言缓存翻译结果,减少重复请求
const translationsCache = new Map<string, Record<string, any>>()
async function loadPageData() {
try {
const response = await WebSitePageService.getHomePageAsync({
Type: 1,
PageType: 10,
Language: i18n.global.locale.value as string,
Domain: window.location.hostname,
})
if(response.pageDataList && response.pageDataList.length > 0) {
pageData.value = JSON.parse(response.pageDataList[0].plugData as string)
console.log(pageData.value)
}
const response = await WebSitePageService.getHomePageAsync({
Type: 1,
PageType: 10,
Language: locale.value as string,
Domain: window.location.hostname,
})
if (response.pageDataList && response.pageDataList.length > 0) {
pageData.value = JSON.parse(response.pageDataList[0].plugData as string)
await applyI18nTranslations()
}
} catch (error) {
console.error('加载首页数据失败:', error)
}
}
getPageData()
</script>
<style scoped>
.home-skeleton {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
.skeleton-container {
}
async function applyI18nTranslations() {
if (!pageData.value?.components) {
translatedPageData.value = pageData.value
return
}
const sourceLang = pageData.value.metadata?.sourceLanguage || 'zh-CN'
const targetLang = mapLocaleToBackend(locale.value || 'zh-CN')
// 同源语言直接用原数据
if (targetLang === sourceLang) {
translatedPageData.value = pageData.value
return
}
// 缓存命中
const cached = translationsCache.get(targetLang)
if (cached) {
translatedPageData.value = {
...pageData.value,
components: applyTranslations(pageData.value.components, cached),
}
return
}
const hashes = collectTextHashes(pageData.value.components)
if (!hashes.length) {
translatedPageData.value = pageData.value
return
}
try {
const resp = await PageTranslationService.BatchGetTranslationsAsync({
hashes,
language: targetLang,
fallbackLanguage: sourceLang,
})
const translations = resp.data?.translations || resp.translations || {}
translationsCache.set(targetLang, translations)
translatedPageData.value = {
...pageData.value,
components: applyTranslations(pageData.value.components, translations),
}
} catch (err) {
console.error('加载翻译失败:', err)
translatedPageData.value = pageData.value
}
}
// 首次加载
loadPageData()
// 语言切换时重新应用翻译
watch(
() => locale.value,
() => {
if (pageData.value) {
applyI18nTranslations()
} else {
loadPageData()
}
}
)
</script>
<style scoped>
.page-content {
width: 100%;
}
.skeleton-content {
margin-top: 20px;
}
</style>
\ No newline at end of file
}
</style>
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment