新增 有家相册 uni-app 前端(uniapp/youjiala,H5 / 微信小程序 / App 三端)

从旧 WebVue 移植的地基部分:
- 登录页(RSA+MD5 密码加密、redirect 回跳、中英文切换)与登录后落地页
- 401 / 403 / 404 错误页 + error-panel 通用外壳(文案走 ErrorPage 语言包)
- 登录守卫双链路:uni.addInterceptor 拦站内跳转,页面级 onShow 复核冷启动深链
- 单一请求链路 common/http.interceptor:JWT 唯一来源、401 并发刷新单例、
  业务异常识别、HTTP 403 跳无权限页
- H5 干净短地址:initRouter 前向 __uniRoutes 追加克隆记录(router/h5-clean-url)
- 路由登记表 router/routes.ts(PAGE_PATHS / pageUrl / PAGE_PERMISSIONS)
  与新增页面四步登记约定(src/md/page-routing.md)
- i18n:uView Pro 语言包 + 后端词条合并,双取值(后端 zh-cn/en,包名 zh-CN/en-US)
- 后端接口层 service/ 与数据模型 data/auto/ 为生成器产物,不手改

不入库的内容:
- 摘除脚手架残留的 Harmony 签名密码(manifest.json signingConfigs 与
  harmony-configs/build-profile.json5 的 keyPassword / storePassword 已置空)
- 忽略 node_modules、dist、unpackage、.hbuilderx、.agents、pnpm-lock.yaml
  及根目录临时截图
This commit is contained in:
owenchen committed 2026-10-13 08:27:34 +08:00
1 parent 74757f9eb4
commit 6a8258ac4b
274 files changed
+30291

No files matched your search

+53
View File
@@ -0,0 +1,53 @@
export const APP_INFO: {
name: string;
version: string;
buildTime: string;
platform: string;
} = {
name: "uViewPro(跨平台UI组件库)",
version: "1.0.9",
buildTime: "2025-12-23",
platform: getPlatform(),
};
export const UVIEW_PRO_INFO = {
name: "uview-pro",
version: "0.4.7",
buildTime: "2025-12-19",
};
function getPlatform() {
let platform = uni.getSystemInfoSync().platform;
if (platform) {
return platform;
}
// #ifdef H5
platform = "H5";
// #endif
// #ifdef MP-WEIXIN
platform = "微信小程序";
// #endif
// #ifdef MP-ALIPAY
platform = "支付宝小程序";
// #endif
// #ifdef APP-PLUS
platform = "App";
// #endif
// #ifdef APP-HARMONY
platform = "HarmonyOS";
// #endif
platform = "未知平台";
return platform;
}
export const ONBOARDING_STORAGE_BASE_KEY = `guide-onboarded-${APP_INFO.version}`;
export const GUIDE_TABS_KEY = `guide-page-tabs-${APP_INFO.version}`;
export const GUIDE_FAB_KEY = `guide-page-fab-${APP_INFO.version}`;
export const GUIDE_TABBAR_KEY = `guide-page-tabbar-${APP_INFO.version}`;
export const GUIDE_THEME_SWITCHER_KEY = `guide-theme-switcher-${APP_INFO.version}`;
export const GUIDE_EXPERIENCE_KEY = `guide-experience-entry-${APP_INFO.version}`;
@@ -0,0 +1,517 @@
import type {
RequestConfig,
RequestInterceptor,
RequestMeta,
RequestOptions,
} from "uview-pro";
import type { JwtInfo } from "@/data/auto/jwtInfo";
import { http } from "uview-pro";
import { toBackendLocale } from "@/common/locale-const";
import { WebSitePolicy } from "@/policy/custom/webSitePolicy";
import {
currentRealPath,
errorPageUrl,
pageUrl,
resolvePageName,
toRealPath,
} from "@/router/routes";
/**
* 全项目唯一请求层(基于 uview-pro httpPlugin,main.ts 已注册)
* 整合了原 common/request 模块的全部能力:
* - JWT 登录态存取 + Authorization 头
* - 401 并发刷新令牌单例(refreshPromise)+ _retry 防递归
* - 业务异常识别(.NET CloudBuilder 的 ValidatedException / ServiceErrorException)
* - meta 驱动的 loading / toast 开关
* - uploadFile / downloadFile 文件传输工具
*/
// ==================== 全局配置 ====================
// 后端地址唯一来源:policy/custom/webSitePolicy(代码生成器维护,改地址只动那里)
// 与旧系统 WebVue 同逻辑:开发构建用 WebApiRootDev,发布构建用 WebApiRoot
// App 端开发构建(真机/模拟器)连不上宿主机的 localhost,改用 WebApiRootDevApp
// 小程序端需在平台配置 https 合法域名;本地 H5 联调如遇 CORS 需后端放行开发端口
let devBaseUrl = WebSitePolicy.WebApiRootDev;
// #ifdef APP-PLUS
devBaseUrl = WebSitePolicy.WebApiRootDevApp;
// #endif
export const baseUrl = import.meta.env.DEV
? devBaseUrl
: WebSitePolicy.WebApiRoot;
// 登录页真实路径:getCurrentPages() 拿到的 route 永远是 pages.json 里的 path,
// 不含 H5 运行时补的短地址别名,故路径比较只能用真实路径,跳转 URL 才走 pageUrl()
const loginPage = toRealPath("login");
const timeout = 60000;
// ==================== 登录态(JWT) ====================
// JwtInfo 使用后端对齐的权威定义(@/data/auto/jwtInfo:AccessToken / RefreshToken / CreatedTime),
// 此处再导出,保持 import { JwtInfo } from '@/common/http.interceptor' 的调用方兼容
export type { JwtInfo };
const JWT_STORAGE_KEY = "request-jwt-info";
/** 读取登录令牌信息 */
export function getJwtInfo(): JwtInfo | null {
return (uni.getStorageSync(JWT_STORAGE_KEY) as JwtInfo) || null;
}
/** 登录成功后写入令牌(供登录页调用) */
export function setLoginJwt(jwt: JwtInfo) {
uni.setStorageSync(JWT_STORAGE_KEY, jwt);
}
/** 清除登录态 */
export function clearLoginState() {
uni.removeStorageSync(JWT_STORAGE_KEY);
}
function getToken(): string {
return getJwtInfo()?.AccessToken ?? "";
}
/**
* 认证失败统一收尾:必要时清登录态并跳登录页;
* 已经在登录页就不再跳,避免死循环
*/
function goLogin(clearState = true) {
if (clearState)
clearLoginState();
const pages = getCurrentPages();
const current = pages[pages.length - 1];
const currentPath = current ? `/${current.route}` : "";
if (currentPath !== loginPage)
uni.reLaunch({ url: pageUrl("login") });
}
/**
* 权限不足统一收尾:跳 403 页并把出错地址用 ?from= 带过去。
* 这里不复用 router/nav-guard 的 jumpToError:nav-guard 依赖本模块读登录态,
* 反向 import 会形成循环依赖;本页已在 403 时不再跳,避免页面自身请求 403 导致来回刷
*/
function goForbidden() {
const real = currentRealPath();
const name = resolvePageName(real);
if (name === "error403") {
return;
}
// from 是给用户看的地址:H5 用短地址,其它端用真实路径
const url = errorPageUrl("error403", name ? pageUrl(name) : real);
uni.redirectTo({ url, fail: () => uni.reLaunch({ url }) });
}
// ==================== 401 并发刷新(原 axios 封装的精华设计) ====================
/**
* 正在进行的刷新请求 Promise。
* 多个请求同时 401 时共用同一次刷新:后端每次刷新都会换新 RefreshToken,
* 各自刷新会让先完成的那个把其它请求携带的旧 RefreshToken 作废,
* 结果被判定「无效的令牌,请重新登录」而把人踢下线。
*/
let refreshPromise: Promise<JwtInfo> | null = null;
/** 真正发起刷新:直接走 uni.request 绕开 http 单例,防止刷新自身 401 无限递归 */
function doRefreshToken(): Promise<JwtInfo> {
const data = getJwtInfo();
if (!data)
return Promise.reject(new Error("无登录态,无法刷新令牌"));
return new Promise<JwtInfo>((resolve, reject) => {
uni.request({
url: `${baseUrl}/Auth/RefreshToken`,
method: "POST",
data,
timeout,
success: (res) => {
if (res.statusCode >= 200 && res.statusCode < 300 && res.data) {
const jwt = res.data as JwtInfo;
setLoginJwt(jwt);
resolve(jwt);
}
else {
// 带上 status:goLogin 据此区分「服务端拒绝」与「网络抖动」
const err: any = new Error("刷新令牌失败");
err.status = res.statusCode;
reject(err);
}
},
fail: (err: any) => reject(err), // 网络类异常无 status,保留登录态
});
});
}
/** 取得「共用的刷新动作」:并发 401 只刷新一次 */
function refreshTokenOnce(): Promise<JwtInfo> {
if (!refreshPromise) {
refreshPromise = doRefreshToken().finally(() => {
refreshPromise = null;
});
}
return refreshPromise;
}
// ==================== 提示与业务异常 ====================
/** 从后端错误体里提取可读文案(兼容 .NET 序列化与常见 {message,msg} 格式) */
function extractErrorMessage(data: any): string {
if (data == null)
return "";
if (typeof data === "string")
return data;
return data.Message ?? data.message ?? data.msg ?? "";
}
/**
* 发给后端的 Culture / locale 头统一取值(与旧系统 CulturePolicy 字面一致:zh-cn / en)。
* uni.getLocale() 在 H5 常返回 zh-Hans 等 BCP-47 变体,直传会让服务端
* ServiceMessageManager.ComposeMessage 查不到词条,抛空 Message 异常,
* 前端只能看到兜底文案,故统一归一化(取值定义见 common/locale-const.ts)。
*/
export function backendLocale(): string {
return toBackendLocale(String(uni.getLocale?.() ?? ""));
}
/** 长错误用 modal 完整展示,短错误用 toast */
function notifyError(detail: string, fallback = "请求失败") {
const content = detail || fallback;
if (content.length > 14) {
uni.showModal({
title: fallback,
content: content.slice(0, 200),
showCancel: false,
});
}
else {
uni.showToast({ title: content, icon: "none" });
}
}
/**
* 业务异常识别:HTTP 成功但 body 是 .NET 异常对象时命中
* @returns 命中返回异常消息(调用方需抛出),未命中返回空串
*/
function detectBusinessException(data: any): string {
if (!data || typeof data !== "object" || !data.SourceCode)
return "";
const msg = extractErrorMessage(data) || "业务处理失败";
if (data.SourceCode === "CloudBuilder.Core.ValidatedException") {
uni.showModal({
title: "业务校验失败",
content: msg.slice(0, 200),
showCancel: false,
});
return msg;
}
if (data.SourceCode === "CloudBuilder.Core.ServiceErrorException") {
notifyError(msg, "服务异常");
return msg;
}
return "";
}
// ==================== 全局配置(httpPlugin 注册用) ====================
const httpRequestConfig: RequestConfig = {
baseUrl,
header: {
"content-type": "application/json",
},
timeout,
meta: {
originalData: true, // 拦截器收到完整响应(含 statusCode/header),自行解包
toast: true, // 出错默认提示,单次请求可传 meta.toast=false 关闭
loading: false, // 默认不显示 loading,单次请求传 meta.loading=true 开启
},
};
// ==================== 请求 / 响应拦截器(httpPlugin 注册用) ====================
const httpInterceptor: RequestInterceptor = {
// 请求拦截器:loading + method 归一化 + Authorization + locale
request: (config: RequestOptions) => {
const meta: RequestMeta = config.meta || {};
if (meta.loading) {
uni.showLoading({ title: "加载中...", mask: true });
}
// 归一化 method 大写:兼容代码生成的 service 层(传小写 "post"/"get",uni.request 要求大写)
config.method = String(
config.method || "GET",
).toUpperCase() as RequestOptions["method"];
const token = getToken();
if (token) {
// ASP.NET Core JWT Bearer 认证要求 Authorization 带 "Bearer " 前缀(旧系统同)
config.header.Authorization = token.startsWith("Bearer ")
? token
: `Bearer ${token}`;
}
config.header.locale = backendLocale();
return config;
},
// 响应拦截器(async:支持 401 等待刷新后重试)
response: async (response: any) => {
const meta: RequestMeta = response.config?.meta || {};
if (meta.loading) {
uni.hideLoading();
}
const { statusCode, data: rawData, errMsg } = response;
const config = response.config;
// 网络层错误
if (errMsg && errMsg.includes("Failed to connect")) {
meta.toast && notifyError("", "网络错误");
throw new Error("网络错误");
}
if (errMsg && errMsg.includes("request:fail")) {
meta.toast && notifyError("", "请求错误:未知");
throw new Error("请求错误:未知");
}
// 401 且未重试过:共用刷新 + 重试
if (statusCode === 401 && !config._retry) {
if (!getToken()) {
// 本来就没有登录态,直接去登录页
goLogin(true);
throw new Error("登录已失效,请重新登录");
}
// _retry 标记:重试请求再 401 时落入下方非 2xx 分支,防止无限递归
config._retry = true;
// ① 只把「刷新令牌」放进 try:刷新失败(服务端拒绝)才清登录态跳登录页;
// 网络抖动(无 status)保留登录态,避免一次断网把用户踢下线
try {
await refreshTokenOnce();
}
catch (refreshError: any) {
goLogin(!!(refreshError && refreshError.status));
throw refreshError;
}
// ② 重试放在 try 之外:它自身的失败(500/403 等)只算这次业务请求失败,
// 不能当成「刷新失败」,否则会误清已成功续期用户的登录态
return await http.request(config);
}
// 204:后端约定「无内容但业务自行处理」,静默透传
if (statusCode === 204)
return rawData;
// 请求错误(非 2xx)
if (!(statusCode >= 200 && statusCode < 300)) {
// 403:权限不足,跳 403 页列出原因(401 走上面的续期 / 回登录页链路,不进错误页)
if (statusCode === 403) {
goForbidden();
}
// 异常体优先:.NET 后端常以 500 + JSON 异常返回业务错误
const bizMsg = detectBusinessException(rawData);
if (bizMsg)
throw new Error(bizMsg);
const errorMessage = `请求错误[${statusCode}]`;
meta.toast && notifyError(extractErrorMessage(rawData), errorMessage);
const err: any = new Error(`${errorMessage}:${errMsg ?? ""}`);
err.status = statusCode; // 结构化状态码,供调用方按 401/403/500 分支处理
err.data = rawData;
throw err;
}
// 2xx 但 body 是业务异常
const bizMsg = detectBusinessException(rawData);
if (bizMsg)
throw new Error(bizMsg);
// 成功:剥壳返回 JSON body(service/* 经 utils/request 直接拿到后端实体对象)
return rawData;
},
};
// ==================== 文件传输工具(不属于主链路,无拦截器逻辑) ====================
export interface UploadOptions {
url: string;
/** 本地文件路径(chooseImage/chooseVideo 等获得) */
filePath: string;
/** 文件对应的 form-key,后端按此名接收(默认 'file') */
name?: string;
formData?: Record<string, any>;
header?: Record<string, any>;
}
/** 上传文件(uni.uploadFile 封装,自动携带 token,失败 reject 可感知) */
export function uploadFile(options: UploadOptions): Promise<any> {
const { url, filePath, name = "file", formData, header = {} } = options;
const fullUrl = url.indexOf("http") === 0 ? url : baseUrl + url;
const token = getToken();
return new Promise((resolve, reject) => {
uni.uploadFile({
url: fullUrl,
filePath,
name,
formData,
timeout,
header: token ? { Authorization: token, ...header } : header,
success: (res) => {
if (res.statusCode < 200 || res.statusCode >= 300) {
notifyError("", `上传失败[${res.statusCode}]`);
reject(new Error(`上传失败[${res.statusCode}]`));
return;
}
let data: any = res.data;
try {
data = typeof res.data === "string" ? JSON.parse(res.data) : res.data;
}
catch {
/* 非 JSON 响应保持原样 */
}
const bizMsg = detectBusinessException(data);
if (bizMsg)
reject(new Error(bizMsg));
else resolve(data);
},
fail: (err: any) => {
notifyError(err.errMsg ?? "", "上传失败");
reject(err);
},
});
});
}
export interface DownloadOptions {
url: string;
method?: "GET" | "POST";
data?: any;
/** 下载文件名前缀(自动追加时间戳与扩展名) */
filename: string;
extension?: string;
}
/** arraybuffer 按 UTF-8 解码(H5 用 TextDecoder,小程序简化实现) */
function abToString(buffer: ArrayBuffer): string {
if (typeof TextDecoder !== "undefined")
return new TextDecoder("utf-8").decode(new Uint8Array(buffer));
const bytes = new Uint8Array(buffer);
let result = "";
for (let i = 0; i < bytes.length;) {
const c = bytes[i];
if (c < 0x80) {
result += String.fromCharCode(c);
i += 1;
}
else if (c < 0xE0) {
result += String.fromCharCode(((c & 0x1F) << 6) | (bytes[i + 1] & 0x3F));
i += 2;
}
else {
result += String.fromCharCode(
((c & 0x0F) << 12)
| ((bytes[i + 1] & 0x3F) << 6)
| (bytes[i + 2] & 0x3F),
);
i += 3;
}
}
return result;
}
/** 生成 yyyyMMddHHmmss 时间戳(零依赖,替代 dayjs) */
function timestamp(): string {
const d = new Date();
const p = (n: number) => String(n).padStart(2, "0");
return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
}
/**
* 下载并打开文件(默认按 Excel 处理)
* 关键防御:先探测 content-type——后端报错时返回的是 JSON 而不是文件
*/
export async function downloadFile(options: DownloadOptions): Promise<void> {
const { url, method = "POST", data, filename, extension = "xlsx" } = options;
const fullUrl = url.indexOf("http") === 0 ? url : baseUrl + url;
const token = getToken();
const res = await new Promise<UniApp.RequestSuccessCallbackResult>(
(resolve, reject) => {
uni.request({
url: fullUrl,
method,
data,
timeout,
responseType: "arraybuffer",
header: token ? { Authorization: token } : {},
success: resolve,
fail: err => reject(new Error(err.errMsg || "下载失败")),
});
},
);
const header = res.header as Record<string, any>;
const contentType = String(
header["content-type"] || header.Content_Type || header.ContentType || "",
);
if (contentType.includes("application/json")) {
const text
= typeof res.data === "string"
? res.data
: abToString(res.data as ArrayBuffer);
let msg = "下载失败";
try {
msg = extractErrorMessage(JSON.parse(text)) || msg;
}
catch {
/* 解析失败沿用默认文案 */
}
notifyError(msg, "下载失败");
throw new Error(msg);
}
if (res.statusCode < 200 || res.statusCode >= 300) {
notifyError("", `下载失败[${res.statusCode}]`);
throw new Error(`下载失败[${res.statusCode}]`);
}
const fileName = `${filename}${timestamp()}.${extension}`;
// #ifdef H5
// H5:blob + 隐藏 <a> 触发浏览器下载
const downloadUrl = URL.createObjectURL(new Blob([res.data as ArrayBuffer]));
const link = document.createElement("a");
link.href = downloadUrl;
link.download = fileName;
document.body.appendChild(link);
link.click();
link.remove();
URL.revokeObjectURL(downloadUrl);
// #endif
// #ifndef H5
// 小程序/App:写入本地用户目录后调起系统文档预览
const fs = uni.getFileSystemManager();
// 写入目录按平台区分:微信用 uni.env.USER_DATA_PATH(官方类型未声明故转 any);
// App 端无 uni.env,写 plus 私有目录 _doc/(openDocument 同样支持该相对路径)
let userDataPath = "";
// #ifdef MP-WEIXIN
userDataPath = (uni as any).env?.USER_DATA_PATH ?? "";
// #endif
// #ifdef APP-PLUS
userDataPath = "_doc";
// #endif
const filePath = `${userDataPath}/${fileName}`;
await new Promise<void>((resolve, reject) => {
fs.writeFile({
filePath,
data: res.data as ArrayBuffer,
success: () => resolve(),
fail: (err: any) => reject(new Error(err.errMsg || "写入文件失败")),
});
});
await new Promise<void>((resolve, reject) => {
uni.openDocument({
filePath,
showMenu: true,
success: () => resolve(),
fail: (err: any) => reject(new Error(err.errMsg || "无法打开文件")),
});
});
// #endif
}
// 导出(main.ts httpPlugin 注册用)
export { httpInterceptor, httpRequestConfig };
@@ -0,0 +1,33 @@
/**
* 语言 / Culture 取值:区分「后端 Culture 线值」与「uView Pro 语言包 name」两套
*
* - 后端(旧 CulturePolicy)严格字面:中文 "zh-cn"、英文 "en"。
* 发给后端的 locale 头 / 登录体 Culture 用这套,防止后端精确匹配失败。
* - uView Pro 内置语言包 name 固定为 "zh-CN" / "en-US"(大小写敏感)。
* 前端自定义语言包的 name 必须与之完全相同,config-provider 才能把自定义文案
* 与内置组件文案(picker / calendar / upload …)按 name 合并;
* 若写成 "zh-cn" 会被当成另一种语言,导致内置组件文案丢失 + 语言选择器出现重复项。
*/
/** 发给后端的 Culture 线值 */
export const BACKEND_LOCALE_ZH = "zh-cn";
export const BACKEND_LOCALE_EN = "en";
/** uView Pro 语言包 name(必须匹配内置大小写,勿改成 zh-cn / en) */
export const UI_LOCALE_ZH = "zh-CN";
export const UI_LOCALE_EN = "en-US";
/** 前端默认选中的语言包 name */
export const DEFAULT_LOCALE = UI_LOCALE_ZH;
/** 归一化任意 locale 变体(zh-Hans / zh-CN / en-US …)到后端 Culture 线值 */
export function toBackendLocale(value: string): string {
const l = String(value || "").toLowerCase();
return l.startsWith("en") ? BACKEND_LOCALE_EN : BACKEND_LOCALE_ZH;
}
/** 归一化任意 locale 变体(含 DB 的 Lang)到 uView 语言包 name */
export function toUiLocale(value: string): string {
const l = String(value || "").toLowerCase();
return l.startsWith("en") ? UI_LOCALE_EN : UI_LOCALE_ZH;
}
+36
View File
@@ -0,0 +1,36 @@
// 跨端密码加密工具(H5 / 小程序 / App 均可用)
// 说明:原 jsencrypt 依赖浏览器 window.crypto,小程序/App 无 DOM 会失败;
// wxmp-rsa 是 jsencrypt 同源 fork(jsbn 纯 JS 实现,随机数走 Math.random 兜底),
// 无任何浏览器全局依赖,API 与 jsencrypt 完全一致,一套代码全端运行。
import CryptoJS from "crypto-js";
import { JSEncrypt } from "wxmp-rsa";
/** MD5 摘要(小写 hex),与旧系统 WebVue 的 CryptoJS.MD5(...).toString() 输出一致 */
export function md5Hex(text: string): string {
return CryptoJS.MD5(text).toString();
}
/**
* RSA 公钥加密(PKCS#1 v1.5),返回 base64 密文
* @param plaintext 待加密明文
* @param publicKeyPem 后端 /Auth/GetRsaPublicKey 返回的 PEM 公钥(无换行的裸格式也可)
* @returns 加密失败返回空串,调用方需判空兜底
*/
export function rsaEncrypt(plaintext: string, publicKeyPem: string): string {
const encryptor = new JSEncrypt();
encryptor.setPublicKey(publicKeyPem);
// encrypt 失败返回 false(非 nullish),用 || 兜底为空串
return encryptor.encrypt(plaintext) || "";
}
/**
* 登录密码密文:RSA( Guid + MD5(密码) ),算法与旧系统 WebVue 登录完全一致
* Guid 同时作为请求体 LoginData.Guid 发送,服务端解密后比对前缀防重放
*/
export function encryptLoginPassword(
password: string,
guid: string,
publicKeyPem: string,
): string {
return rsaEncrypt(guid + md5Hex(password), publicKeyPem);
}
+12
View File
@@ -0,0 +1,12 @@
:deep(.code-text) {
font-family: 'Monaco', 'Consolas', monospace;
white-space: pre-wrap;
line-height: 1.4;
}
.code-block {
background: $u-bg-gray-light;
padding: 24rpx;
border-radius: 8rpx;
margin: 16rpx 0;
}
@@ -0,0 +1,116 @@
// 此自定义uView Pro主题生成于2025-11-29 13:18
// 地址:https://uviewpro.cn/zh/guide/themeGenerate.html
export default [
// 主题 1: 紫色
{
name: "purple",
label: "霞光紫",
color: {
primary: "#7c3aed",
error: "#f43f5e",
warning: "#f59e0b",
success: "#10b981",
info: "#6b7280",
primaryLight: "#f3e8ff",
errorLight: "#ffe4e6",
warningLight: "#fef3c7",
successLight: "#d1fae5",
infoLight: "#f3f4f6",
primaryDark: "#6d28d9",
errorDark: "#e11d48",
warningDark: "#d97706",
successDark: "#059669",
infoDark: "#4b5563",
primaryDisabled: "#c4b5fd",
errorDisabled: "#fbcfe8",
warningDisabled: "#fde68a",
successDisabled: "#a7f3d0",
infoDisabled: "#d1d5db",
},
},
// 主题 2: 绿色
{
name: "green",
label: "清翠绿",
color: {
primary: "#059669",
error: "#dc2626",
warning: "#eab308",
success: "#16a34a",
info: "#78716c",
primaryLight: "#ecfdf5",
errorLight: "#fee2e2",
warningLight: "#fefce8",
successLight: "#dcfce7",
infoLight: "#fafaf9",
primaryDark: "#047857",
errorDark: "#b91c1c",
warningDark: "#ca8a04",
successDark: "#15803d",
infoDark: "#57534e",
primaryDisabled: "#6ee7b7",
errorDisabled: "#fca5a5",
warningDisabled: "#facc15",
successDisabled: "#86efac",
infoDisabled: "#e7e5e4",
},
},
// 主题 3: 橙色
{
name: "orange",
label: "暖阳橙",
color: {
primary: "#f97316",
error: "#ef4444",
warning: "#fbbf24",
success: "#22c55e",
info: "#6366f1",
primaryLight: "#ffedd5",
errorLight: "#fee2e2",
warningLight: "#fef3c7",
successLight: "#dcfce7",
infoLight: "#e0e7ff",
primaryDark: "#ea580c",
errorDark: "#dc2626",
warningDark: "#f59e0b",
successDark: "#16a34a",
infoDark: "#4f46e5",
primaryDisabled: "#fed7aa",
errorDisabled: "#fecaca",
warningDisabled: "#fde68a",
successDisabled: "#86efac",
infoDisabled: "#c7d2fe",
},
},
// 主题 4: 深蓝 / 黑蓝色系
{
name: "dark",
label: "午夜蓝",
color: {
primary: "#0b3d91",
error: "#ef5350",
warning: "#ffa726",
success: "#66bb6a",
info: "#2196f3",
primaryLight: "#081a33",
errorLight: "#5e1914",
warningLight: "#663c00",
successLight: "#1b3a1b",
infoLight: "#003d66",
primaryDark: "#062a57",
errorDark: "#c62828",
warningDark: "#e65100",
successDark: "#2e7d32",
infoDark: "#01579b",
primaryDisabled: "#274a7a",
errorDisabled: "#ef9a9a",
warningDisabled: "#ffb74d",
successDisabled: "#81c784",
infoDisabled: "#4fc3f7",
},
},
];