chore: initialize qiming workspace repository

This commit is contained in:
Codex
2026-05-29 14:22:48 +08:00
commit bfd67a0f2c
10750 changed files with 1885711 additions and 0 deletions

View File

@@ -0,0 +1,431 @@
import { computed, reactive } from "vue";
import { SUCCESS_CODE } from "@/constants/codes.constants";
import {
DEFAULT_LANG,
I18N_LANG_LIST_STORAGE_KEY,
I18N_LANG_MAP_STORAGE_KEY,
I18N_LANG_STORAGE_KEY,
} from "@/constants/i18n.constants";
import {
getLocalFallback,
resolveLiteralKey,
toBundleKey,
} from "@/constants/i18n.local.constants";
import { apiI18nLangList, apiI18nQuery, apiI18nSetLang } from "@/servers/i18n";
import type { I18nLangDto, I18nState } from "@/types/interfaces/i18n";
import { syncThirdPartyLocales } from "@/utils/i18n-third-party";
const parseJSON = <T = any,>(value: string, fallback: T): T => {
if (!value) return fallback;
try {
return JSON.parse(value) as T;
} catch (error) {
return fallback;
}
};
const isH5Platform = (): boolean => {
// #ifdef H5 || WEB
return true;
// #endif
return false;
};
const isMpWeixinPlatform = (): boolean => {
// #ifdef MP-WEIXIN
return true;
// #endif
return false;
};
const getBrowserLang = (): string => {
// #ifdef H5 || WEB
const navLang = navigator?.language || DEFAULT_LANG;
return navLang;
// #endif
// #ifdef MP-WEIXIN
try {
const sysLang = uni.getSystemInfoSync().language;
if (sysLang) return sysLang;
} catch (e) {}
// #endif
return DEFAULT_LANG;
};
export const normalizeLang = (lang: string): string => {
const source = (lang || "").trim();
if (!source) return DEFAULT_LANG;
const bundleKey = toBundleKey(source);
// 将 bundle key 转换为 PascalCase 显示格式
if (bundleKey === "zh-cn") return "zh-CN";
if (bundleKey === "zh-tw") return "zh-TW";
if (bundleKey === "zh-hk") return "zh-HK";
if (bundleKey === "en-us") return "en-US";
// 未知语言保持原始归一化形式
return source.replaceAll("_", "-");
};
const getStoredLang = (): string => {
const cachedLang = uni.getStorageSync(I18N_LANG_STORAGE_KEY);
if (cachedLang) return normalizeLang(cachedLang);
return "";
};
const setStoredLang = (lang: string) => {
uni.setStorageSync(I18N_LANG_STORAGE_KEY, normalizeLang(lang));
};
const setStoredLangMap = (langMap: Record<string, string>) => {
uni.setStorageSync(I18N_LANG_MAP_STORAGE_KEY, JSON.stringify(langMap || {}));
};
const setStoredLangList = (langList: I18nLangDto[]) => {
uni.setStorageSync(
I18N_LANG_LIST_STORAGE_KEY,
JSON.stringify(langList || []),
);
};
const getCachedLangMap = (): Record<string, string> => {
const text = uni.getStorageSync(I18N_LANG_MAP_STORAGE_KEY) || "";
return parseJSON<Record<string, string>>(text, {});
};
const getCachedLangList = (): I18nLangDto[] => {
const text = uni.getStorageSync(I18N_LANG_LIST_STORAGE_KEY) || "";
return parseJSON<I18nLangDto[]>(text, []);
};
const sortLangList = (list: I18nLangDto[]): I18nLangDto[] => {
const filtered = (list || []).filter((item) => item?.status === 1);
return filtered.sort((a, b) => {
if (a.sort !== b.sort) return a.sort - b.sort;
return a.id - b.id;
});
};
const getDefaultLangFromList = (list: I18nLangDto[]): string => {
const defaultItem = list.find((item) => item?.isDefault === 1);
return normalizeLang(defaultItem?.lang || DEFAULT_LANG);
};
export const i18nState = reactive<I18nState>({
currentLang: DEFAULT_LANG,
langMap: {},
langList: [],
loaded: false,
loading: false,
});
let loadI18nTask: Promise<void> | null = null;
let loadLangListTask: Promise<void> | null = null;
const TAB_BAR_I18N_ITEMS: Array<{ index: number; key: string }> = [
{ index: 0, key: "Mobile.Nav.home" },
{ index: 1, key: "Mobile.Nav.unionRecord" },
{ index: 2, key: "Mobile.Nav.agent" },
{ index: 3, key: "Mobile.Nav.app" },
];
const fillTemplate = (
source: string,
params: Record<string, string | number> = {},
): string => {
let output = source || "";
Object.keys(params || {}).forEach((key) => {
const val = String(params[key] ?? "");
output = output.replace(new RegExp(`\\{${key}\\}`, "g"), val);
});
return output;
};
const pickLangForInit = (): string => {
const cacheLang = getStoredLang();
const browserLang = normalizeLang(getBrowserLang());
return normalizeLang(cacheLang || browserLang || DEFAULT_LANG);
};
const fetchLangList = async (): Promise<I18nLangDto[]> => {
// 允许在 H5 和微信小程序环境调用接口
if (isH5Platform() || isMpWeixinPlatform()) {
try {
const res = await apiI18nLangList();
if (res?.code === SUCCESS_CODE && Array.isArray(res?.data)) {
const sortedList = sortLangList(res.data);
setStoredLangList(sortedList);
return sortedList;
}
} catch (error) {
console.error("fetchLangList failed:", error);
}
}
return sortLangList(getCachedLangList());
};
const fetchLangMap = async (lang: string = "", active: boolean = false): Promise<Record<string, string>> => {
// 允许在 H5 和微信小程序环境调用接口
if (!isH5Platform() && !isMpWeixinPlatform()) {
return {};
}
// 仅在主动切换 (active=true) 且有有效 lang 时才透传给后端,
// 否则传空字符串,后端将返回默认翻译而不会修改用户配置。
const targetLang = (active && lang) ? normalizeLang(lang) : "";
try {
// lang 为空时由后端按用户已保存语言/命中逻辑返回对应词典
const res = await apiI18nQuery(targetLang);
// 只有成功且有数据才返回数据其余失败、异常、data=null统统返回空对象以触发兜底逻辑
if (res?.code === SUCCESS_CODE && res?.data) {
return res.data;
}
} catch (error) {
console.error("fetchLangMap failed:", error);
}
return {};
};
export const bootstrapI18nCache = () => {
const lang = getStoredLang();
i18nState.currentLang = normalizeLang(lang || pickLangForInit());
i18nState.langMap = getCachedLangMap();
i18nState.langList = sortLangList(getCachedLangList());
i18nState.loaded = Object.keys(i18nState.langMap || {}).length > 0;
syncThirdPartyLocales(i18nState.currentLang);
applyTabBarI18n();
};
const resolveMessage = (
lang: string,
keyOrText: string,
fallback: string = "",
): string => {
const key = resolveLiteralKey(keyOrText);
// 1. 优先尝试从服务端下发的语言包匹配
const langMap = i18nState.langMap;
if (langMap != null) {
const fromServer = langMap[key];
// 如果 key 存在(即便 value 是空字符串)
if (fromServer !== undefined) {
// 如果 value 为空字符串,则返回 key 本身;否则返回翻译值(保持同 PC 端一致)
return fromServer === "" ? key : fromServer;
}
}
// 2. 映射未命中或原生端通过本地包兜底
const currentLangKey = toBundleKey(i18nState.currentLang || DEFAULT_LANG);
// 如果当前是中文类语言,优先尝试本地中文包兜底(实现原生端默认中文需求)
if (currentLangKey.startsWith("zh")) {
const zhFallbackValue = getLocalFallback(currentLangKey, key);
if (zhFallbackValue) return zhFallbackValue;
}
// 3. 全局英语兜底(仅当非中文且远程失败,或中文本地包仍缺失时)
const enFallbackValue = getLocalFallback("en-us", key);
if (enFallbackValue) return enFallbackValue;
// 3. 最后兜底返回
return fallback || keyOrText;
};
export const t = (
key: string,
params: Record<string, string | number> = {},
fallback: string = "",
): string => {
const lang = normalizeLang(i18nState.currentLang || DEFAULT_LANG);
const value = resolveMessage(lang, key, fallback);
return fillTemplate(value, params);
};
export const translateText = (
text: string,
params: Record<string, string | number> = {},
): string => {
if (!text) return "";
return t(text, params, text);
};
export const applyTabBarI18n = () => {
// #ifndef MP-WEIXIN || H5 || WEB
return;
// #endif
TAB_BAR_I18N_ITEMS.forEach((item) => {
try {
uni.setTabBarItem({
index: item.index,
text: t(item.key),
fail: (err) => {
// Ignore tabBar sync failures on non-tab pages.
},
});
} catch (error) {
// Ignore tabBar sync failures on non-tab pages.
}
});
};
export const loadI18n = async (active: boolean = false) => {
if (loadI18nTask) {
await loadI18nTask;
return;
}
loadI18nTask = (async () => {
i18nState.loading = true;
try {
const initLang = pickLangForInit();
let targetLang = initLang;
// 如果翻译包为空(接口失败或返回 null统一强制回退到本地英语态作为安全兜底
i18nState.currentLang = normalizeLang(targetLang);
const langMap = await fetchLangMap(targetLang, active);
// 如果翻译包为空(接口失败或返回 null且非中文环境统一回退到本地英语态作为安全兜底
if (Object.keys(langMap).length === 0 && !targetLang.toLowerCase().startsWith("zh")) {
i18nState.currentLang = normalizeLang("en-us");
i18nState.langMap = {};
} else {
i18nState.currentLang = normalizeLang(targetLang);
i18nState.langMap = langMap;
}
i18nState.loaded = true;
setStoredLang(i18nState.currentLang);
setStoredLangMap(i18nState.langMap);
syncThirdPartyLocales(i18nState.currentLang);
applyTabBarI18n();
} finally {
i18nState.loading = false;
}
})();
try {
await loadI18nTask;
} finally {
loadI18nTask = null;
}
};
/**
* 确保语言列表已加载(按需加载)
*/
export const ensureLangList = async () => {
if (loadLangListTask) {
await loadLangListTask;
return;
}
loadLangListTask = (async () => {
try {
const list = await fetchLangList();
i18nState.langList = list;
} catch (error) {
console.error("ensureLangList failed:", error);
}
})();
try {
await loadLangListTask;
} finally {
loadLangListTask = null;
}
};
export const setLanguage = async (lang: string): Promise<boolean> => {
const targetLang = normalizeLang(lang);
if (!targetLang) return false;
const previousLang = i18nState.currentLang;
const previousLangMap = i18nState.langMap;
try {
const queryResult = await apiI18nQuery(targetLang);
// 接口请求失败或业务码错误,执行统一英语兜底
if (!queryResult || queryResult.code !== SUCCESS_CODE || !queryResult.data) {
i18nState.currentLang = normalizeLang("en-us");
i18nState.langMap = {};
} else {
i18nState.currentLang = targetLang;
i18nState.langMap = queryResult.data;
}
setStoredLang(i18nState.currentLang);
setStoredLangMap(i18nState.langMap);
syncThirdPartyLocales(i18nState.currentLang);
applyTabBarI18n();
return true;
} catch (error) {
console.error("setLanguage failed, perform English fallback:", error);
// 接口异常,执行统一英语兜底
i18nState.currentLang = normalizeLang("en-us");
i18nState.langMap = {};
setStoredLang(i18nState.currentLang);
setStoredLangMap(i18nState.langMap);
syncThirdPartyLocales(i18nState.currentLang);
applyTabBarI18n();
return true; // 即使异常也视为“处理成功(切换到了兜底态)”以避免业务阻塞
}
};
export const syncLanguageFromUser = async (
lang: string = "",
): Promise<void> => {
const userLang = normalizeLang(lang);
if (!userLang) return;
if (userLang === normalizeLang(i18nState.currentLang)) {
setStoredLang(userLang);
if (Object.keys(i18nState.langMap || {}).length === 0) {
const latestLangMap = await fetchLangMap(userLang, false);
// 如果翻译包为空且本地已为空,强制开启英语兜底状态
if (Object.keys(latestLangMap).length === 0) {
i18nState.currentLang = normalizeLang("en-us");
i18nState.langMap = {};
} else {
i18nState.langMap = latestLangMap;
}
setStoredLangMap(i18nState.langMap);
setStoredLang(i18nState.currentLang);
syncThirdPartyLocales(i18nState.currentLang);
applyTabBarI18n();
}
return;
}
i18nState.currentLang = userLang;
setStoredLang(userLang);
const latestLangMap = await fetchLangMap(userLang, false);
// 如果同步中翻译包为空(接口异常等),统一回退到英语
if (Object.keys(latestLangMap).length === 0) {
i18nState.currentLang = normalizeLang("en-us");
i18nState.langMap = {};
} else {
i18nState.langMap = latestLangMap;
}
setStoredLangMap(i18nState.langMap);
setStoredLang(i18nState.currentLang);
syncThirdPartyLocales(i18nState.currentLang);
applyTabBarI18n();
};
export const useI18n = () => {
return {
i18nState,
t,
translateText,
currentLang: computed(() => i18nState.currentLang),
langList: computed(() => sortLangList(i18nState.langList || [])),
loadI18n,
ensureLangList,
setLanguage,
syncLanguageFromUser,
};
};