feat(web): 搭建 Web 壳层与 UI 包基线
React 19 + Vite 6 + Router 7 + TanStack Query + Zustand;Semi 二次封装与 Remix 图标统一出口;顶部模块 + 左侧功能菜单布局、Cloud Blue 主题、登录页与 401/403/404/500 页面;请求层含 token 注入、单飞刷新与统一错误提示。
This commit is contained in:
1 parent
23067edb8f
commit
073d210a92
51 files changed
+4452
No files matched your search
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"name": "@lingxi/ui",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.cjs"
|
||||
},
|
||||
"./icons": {
|
||||
"types": "./dist/icons.d.ts",
|
||||
"import": "./dist/icons.js",
|
||||
"require": "./dist/icons.cjs"
|
||||
},
|
||||
"./theme/tokens.css": "./dist/theme/tokens.css",
|
||||
"./styles.css": "./dist/styles.css",
|
||||
"./semi.css": "./dist/semi.css"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"icons": [
|
||||
"dist/icons.d.ts"
|
||||
],
|
||||
"*": [
|
||||
"dist/index.d.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup src/index.ts src/icons.ts --format esm,cjs --dts --clean --sourcemap && node scripts/copy-assets.mjs",
|
||||
"dev": "tsup src/index.ts src/icons.ts --format esm,cjs --dts --watch",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@douyinfe/semi-ui-19": "2.104.0",
|
||||
"@remixicon/react": "4.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "19.3.0",
|
||||
"@types/react-dom": "19.3.0",
|
||||
"react": "19.3.1",
|
||||
"react-dom": "19.3.1",
|
||||
"tsup": "8.5.1",
|
||||
"typescript": "5.9.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { cp, mkdir } from 'node:fs/promises';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
|
||||
await mkdir(resolve(root, 'dist/theme'), { recursive: true });
|
||||
await cp(resolve(root, 'src/theme/tokens.css'), resolve(root, 'dist/theme/tokens.css'));
|
||||
await cp(resolve(root, 'src/styles/components.css'), resolve(root, 'dist/styles.css'));
|
||||
await cp(
|
||||
resolve(root, 'node_modules/@douyinfe/semi-ui-19/dist/css/semi.min.css'),
|
||||
resolve(root, 'dist/semi.css'),
|
||||
);
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Table } from '@douyinfe/semi-ui-19';
|
||||
import type { TableProps } from '@douyinfe/semi-ui-19/lib/es/table/interface';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import { PageEmpty } from './PageEmpty.js';
|
||||
|
||||
export interface DataTableProps<T extends Record<string, unknown>> {
|
||||
columns: NonNullable<TableProps<T>['columns']>;
|
||||
dataSource: T[];
|
||||
loading?: boolean;
|
||||
rowKey?: TableProps<T>['rowKey'];
|
||||
pagination?: TableProps<T>['pagination'];
|
||||
empty?: ReactNode;
|
||||
size?: TableProps<T>['size'];
|
||||
scroll?: TableProps<T>['scroll'];
|
||||
className?: string;
|
||||
onRow?: TableProps<T>['onRow'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表表格统一封装:加载态 + 空态 + 行 key 约定。
|
||||
* 业务页只负责列定义与数据,不重复写 Empty / loading。
|
||||
*/
|
||||
export function DataTable<T extends Record<string, unknown>>({
|
||||
columns,
|
||||
dataSource,
|
||||
loading = false,
|
||||
rowKey = 'id',
|
||||
pagination = false,
|
||||
empty,
|
||||
size = 'middle',
|
||||
scroll,
|
||||
className,
|
||||
onRow,
|
||||
}: DataTableProps<T>): ReactNode {
|
||||
return (
|
||||
<Table<T>
|
||||
className={`lx-data-table${className === undefined ? '' : ` ${className}`}`}
|
||||
columns={columns}
|
||||
dataSource={dataSource}
|
||||
loading={loading}
|
||||
rowKey={rowKey}
|
||||
pagination={pagination}
|
||||
size={size}
|
||||
scroll={scroll}
|
||||
onRow={onRow}
|
||||
empty={empty ?? <PageEmpty />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Button } from '@douyinfe/semi-ui-19';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import { IconError, IconRefresh } from '../icons.js';
|
||||
|
||||
export interface ErrorStateProps {
|
||||
title?: string;
|
||||
message?: string;
|
||||
/** 服务端返回的 trace_id;500 类错误必须展示(工程规范 04) */
|
||||
traceId?: string;
|
||||
onRetry?: () => void;
|
||||
retryText?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/** 统一错误态:说明 + trace_id + 重试 */
|
||||
export function ErrorState({
|
||||
title = '加载失败',
|
||||
message = '请稍后重试;若持续失败,请把 trace_id 提供给管理员。',
|
||||
traceId,
|
||||
onRetry,
|
||||
retryText = '重新加载',
|
||||
className,
|
||||
}: ErrorStateProps): ReactNode {
|
||||
return (
|
||||
<div className={`lx-error-state${className === undefined ? '' : ` ${className}`}`}>
|
||||
<span className="lx-error-state__icon" aria-hidden="true">
|
||||
<IconError size={32} />
|
||||
</span>
|
||||
<h3 className="lx-error-state__title">{title}</h3>
|
||||
<p className="lx-error-state__message">{message}</p>
|
||||
{traceId === undefined || traceId.length === 0 ? null : (
|
||||
<p className="lx-error-state__trace">trace_id: {traceId}</p>
|
||||
)}
|
||||
{onRetry === undefined ? null : (
|
||||
<Button icon={<IconRefresh />} onClick={onRetry} className="lx-error-state__retry">
|
||||
{retryText}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Modal } from '@douyinfe/semi-ui-19';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export interface FormModalProps {
|
||||
visible: boolean;
|
||||
title: ReactNode;
|
||||
children: ReactNode;
|
||||
onCancel: () => void;
|
||||
onOk: () => void;
|
||||
confirmLoading?: boolean;
|
||||
okText?: string;
|
||||
cancelText?: string;
|
||||
width?: number | string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/** 表单弹窗统一封装:确定 / 取消 / 提交中状态 */
|
||||
export function FormModal({
|
||||
visible,
|
||||
title,
|
||||
children,
|
||||
onCancel,
|
||||
onOk,
|
||||
confirmLoading = false,
|
||||
okText = '确定',
|
||||
cancelText = '取消',
|
||||
width = 560,
|
||||
className,
|
||||
}: FormModalProps): ReactNode {
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
title={title}
|
||||
onCancel={onCancel}
|
||||
onOk={onOk}
|
||||
confirmLoading={confirmLoading}
|
||||
okText={okText}
|
||||
cancelText={cancelText}
|
||||
width={width}
|
||||
className={`lx-form-modal${className === undefined ? '' : ` ${className}`}`}
|
||||
closeOnEsc
|
||||
maskClosable={false}
|
||||
>
|
||||
{children}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Empty } from '@douyinfe/semi-ui-19';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import { IconAsset } from '../icons.js';
|
||||
|
||||
export interface PageEmptyProps {
|
||||
title?: ReactNode;
|
||||
description?: ReactNode;
|
||||
action?: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/** 统一空态:标题 + 说明 + 可选主操作 */
|
||||
export function PageEmpty({
|
||||
title = '暂无数据',
|
||||
description,
|
||||
action,
|
||||
className,
|
||||
}: PageEmptyProps): ReactNode {
|
||||
return (
|
||||
<div className={`lx-empty${className === undefined ? '' : ` ${className}`}`}>
|
||||
<Empty image={<IconAsset size={40} />} title={title} description={description}>
|
||||
{action === undefined ? null : <div className="lx-empty__action">{action}</div>}
|
||||
</Empty>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Skeleton } from '@douyinfe/semi-ui-19';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export interface PageSkeletonProps {
|
||||
loading?: boolean;
|
||||
rows?: number;
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/** 统一加载骨架:页面级加载用段落占位,避免布局跳动 */
|
||||
export function PageSkeleton({
|
||||
loading = true,
|
||||
rows = 6,
|
||||
children,
|
||||
className,
|
||||
}: PageSkeletonProps): ReactNode {
|
||||
return (
|
||||
<Skeleton
|
||||
loading={loading}
|
||||
placeholder={<Skeleton.Paragraph rows={rows} />}
|
||||
className={`lx-page-skeleton${className === undefined ? '' : ` ${className}`}`}
|
||||
>
|
||||
{children}
|
||||
</Skeleton>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Button, Form, Space } from '@douyinfe/semi-ui-19';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import { IconRefresh, IconSearch } from '../icons.js';
|
||||
|
||||
export interface QueryFormProps {
|
||||
children: ReactNode;
|
||||
/** 查询动作;由页面自行发起请求 */
|
||||
onSubmit: () => void;
|
||||
onReset?: () => void;
|
||||
loading?: boolean;
|
||||
/** 额外的右侧操作(如「新建」) */
|
||||
extra?: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/** 列表页查询表单:字段一行内排列,查询 / 重置固位;筛选行不超过两行(设计约束) */
|
||||
export function QueryForm({
|
||||
children,
|
||||
onSubmit,
|
||||
onReset,
|
||||
loading = false,
|
||||
extra,
|
||||
className,
|
||||
}: QueryFormProps): ReactNode {
|
||||
return (
|
||||
<Form
|
||||
className={`lx-query-form${className === undefined ? '' : ` ${className}`}`}
|
||||
layout="horizontal"
|
||||
onSubmit={() => {
|
||||
onSubmit();
|
||||
}}
|
||||
>
|
||||
<div className="lx-query-form__fields">{children}</div>
|
||||
<Space className="lx-query-form__actions" spacing={8}>
|
||||
<Button
|
||||
htmlType="submit"
|
||||
theme="solid"
|
||||
type="primary"
|
||||
icon={<IconSearch />}
|
||||
loading={loading}
|
||||
>
|
||||
查询
|
||||
</Button>
|
||||
<Button
|
||||
icon={<IconRefresh />}
|
||||
onClick={() => {
|
||||
onReset?.();
|
||||
}}
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
{extra}
|
||||
</Space>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export { DataTable } from './DataTable.js';
|
||||
export { ErrorState } from './ErrorState.js';
|
||||
export { FormModal } from './FormModal.js';
|
||||
export { PageEmpty } from './PageEmpty.js';
|
||||
export { PageSkeleton } from './PageSkeleton.js';
|
||||
export { QueryForm } from './QueryForm.js';
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Remix Icon 统一出口(AGENTS §3.3:业务代码只能从这里按语义名导入)
|
||||
* 约定:对外只暴露 Icon* 语义名;新增图标时在这里做一次映射,不要在业务代码里直接引图标库。
|
||||
*/
|
||||
export {
|
||||
RiAddLine as IconAdd,
|
||||
RiAlertLine as IconAlert,
|
||||
RiAppsLine as IconApps,
|
||||
RiFolderOpenLine as IconAsset,
|
||||
RiArrowLeftLine as IconBack,
|
||||
RiCalendarLine as IconCalendar,
|
||||
RiBarChartLine as IconChart,
|
||||
RiCheckLine as IconCheck,
|
||||
RiArrowDownSLine as IconChevronDown,
|
||||
RiArrowRightSLine as IconChevronRight,
|
||||
RiTimeLine as IconClock,
|
||||
RiCloseLine as IconClose,
|
||||
RiFileCopyLine as IconCopy,
|
||||
RiDashboardLine as IconDashboard,
|
||||
RiDeleteBinLine as IconDelete,
|
||||
RiSendPlaneLine as IconDelivery,
|
||||
RiBookOpenLine as IconDocs,
|
||||
RiDownloadLine as IconDownload,
|
||||
RiDragMoveLine as IconDrag,
|
||||
RiEditLine as IconEdit,
|
||||
RiErrorWarningLine as IconError,
|
||||
RiExportLine as IconExport,
|
||||
RiFilter3Line as IconFilter,
|
||||
RiGlobalLine as IconGlobal,
|
||||
RiLayoutGridLine as IconGrid,
|
||||
RiHome5Line as IconHome,
|
||||
RiImageAddLine as IconImageAdd,
|
||||
RiLoader4Line as IconLoading,
|
||||
RiLockLine as IconLock,
|
||||
RiLogoutBoxRLine as IconLogout,
|
||||
RiMailLine as IconMail,
|
||||
RiImageLine as IconMaterial,
|
||||
RiMenuLine as IconMenu,
|
||||
RiMessage3Line as IconMessage,
|
||||
RiMoreLine as IconMore,
|
||||
RiMusicLine as IconMusic,
|
||||
RiNotification3Line as IconNotification,
|
||||
RiPaletteLine as IconPalette,
|
||||
RiPauseLine as IconPause,
|
||||
RiPlayLine as IconPlay,
|
||||
RiRefreshLine as IconRefresh,
|
||||
RiRobotLine as IconRobot,
|
||||
RiSaveLine as IconSave,
|
||||
RiFileTextLine as IconScript,
|
||||
RiSearchLine as IconSearch,
|
||||
RiServerLine as IconServer,
|
||||
RiSettings3Line as IconSettings,
|
||||
RiShareLine as IconShare,
|
||||
RiShieldKeyholeLine as IconShield,
|
||||
RiStackLine as IconStack,
|
||||
RiStarLine as IconStar,
|
||||
RiTaskLine as IconTask,
|
||||
RiThumbUpLine as IconThumbUp,
|
||||
RiUploadLine as IconUpload,
|
||||
RiUserLine as IconUser,
|
||||
RiVideoLine as IconVideo,
|
||||
RiEyeLine as IconView,
|
||||
} from '@remixicon/react';
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './components/index.js';
|
||||
export * from './locale.js';
|
||||
export * from './notify.js';
|
||||
export * from './theme/tokens.js';
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { Locale } from '@douyinfe/semi-ui-19/lib/es/locale/interface';
|
||||
import zhCN from '@douyinfe/semi-ui-19/lib/es/locale/source/zh_CN';
|
||||
|
||||
/** 全站默认中文语言包(Semi LocaleProvider) */
|
||||
export const localeZhCN: Locale = zhCN;
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Toast } from '@douyinfe/semi-ui-19';
|
||||
|
||||
/** 统一成功提示 */
|
||||
export function notifySuccess(content: string): void {
|
||||
Toast.success({ content, duration: 2 });
|
||||
}
|
||||
|
||||
/** 统一错误提示;带 trace_id 时一并展示,便于用户报障(工程规范 04) */
|
||||
export function notifyError(message: string, traceId?: string): void {
|
||||
Toast.error({
|
||||
content:
|
||||
traceId === undefined || traceId.length === 0 ? message : `${message}(trace: ${traceId})`,
|
||||
duration: 4,
|
||||
});
|
||||
}
|
||||
|
||||
export function notifyWarning(content: string): void {
|
||||
Toast.warning({ content, duration: 3 });
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/* 灵犀 UI 包组件样式:只使用 tokens.css 中的语义变量,不写颜色字面量 */
|
||||
.lx-empty {
|
||||
padding: 48px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.lx-empty__action {
|
||||
margin-top: var(--lx-space-4);
|
||||
}
|
||||
|
||||
.lx-error-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--lx-space-2);
|
||||
padding: 64px var(--lx-space-6);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.lx-error-state__icon {
|
||||
color: var(--lx-color-danger);
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.lx-error-state__title {
|
||||
margin: var(--lx-space-2) 0 0;
|
||||
color: var(--lx-color-text-primary);
|
||||
font-size: var(--lx-font-size-lg);
|
||||
line-height: var(--lx-line-height-tight);
|
||||
}
|
||||
|
||||
.lx-error-state__message {
|
||||
margin: 0;
|
||||
max-width: 42ch;
|
||||
color: var(--lx-color-text-secondary);
|
||||
font-size: var(--lx-font-size-sm);
|
||||
}
|
||||
|
||||
.lx-error-state__trace {
|
||||
margin: 0;
|
||||
color: var(--lx-color-text-tertiary);
|
||||
font-family: var(--lx-font-mono);
|
||||
font-size: var(--lx-font-size-xs);
|
||||
}
|
||||
|
||||
.lx-error-state__retry {
|
||||
margin-top: var(--lx-space-3);
|
||||
}
|
||||
|
||||
.lx-page-skeleton {
|
||||
padding: var(--lx-space-4) 0;
|
||||
}
|
||||
|
||||
.lx-query-form {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: var(--lx-space-4);
|
||||
margin-bottom: var(--lx-space-4);
|
||||
}
|
||||
|
||||
.lx-query-form__fields {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--lx-space-2) var(--lx-space-4);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.lx-query-form__actions {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.lx-data-table .semi-table-container {
|
||||
border: 1px solid var(--lx-color-border);
|
||||
border-radius: var(--lx-radius-md);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.lx-data-table .semi-table-thead > .semi-table-row > .semi-table-row-head {
|
||||
background: var(--lx-color-bg-panel);
|
||||
color: var(--lx-color-text-secondary);
|
||||
font-size: var(--lx-font-size-xs);
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.lx-data-table .semi-table-tbody > .semi-table-row > .semi-table-row-cell {
|
||||
border-bottom: 1px solid var(--lx-color-border);
|
||||
}
|
||||
|
||||
.lx-data-table .semi-table-tbody > .semi-table-row:hover > .semi-table-row-cell {
|
||||
background: var(--lx-color-bg-hover);
|
||||
}
|
||||
|
||||
.lx-form-modal .semi-modal-content {
|
||||
background: var(--lx-color-bg-elevated);
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
:root {
|
||||
/* 方向 A · 云白蓝(Cloud Blue):传统企业后台骨架 + 现代浅色质感 */
|
||||
--lx-color-bg-app: #f5f7fa;
|
||||
--lx-color-bg-panel: #ffffff;
|
||||
--lx-color-bg-elevated: #ffffff;
|
||||
--lx-color-bg-subtle: #fafbfc;
|
||||
--lx-color-bg-hover: #f2f4f7;
|
||||
--lx-color-bg-active: #e8f1ff;
|
||||
--lx-color-border: #e5e8ef;
|
||||
--lx-color-border-strong: #d0d5dd;
|
||||
|
||||
--lx-color-text-primary: #1f2329;
|
||||
--lx-color-text-secondary: #646a73;
|
||||
--lx-color-text-tertiary: #8f959e;
|
||||
--lx-color-text-inverse: #ffffff;
|
||||
|
||||
--lx-color-brand: #1677ff;
|
||||
--lx-color-brand-hover: #4096ff;
|
||||
--lx-color-brand-pressed: #0958d9;
|
||||
--lx-color-brand-soft: #e8f1ff;
|
||||
--lx-color-on-brand: #ffffff;
|
||||
--lx-color-focus: #1677ff;
|
||||
|
||||
--lx-color-success: #00b42a;
|
||||
--lx-color-warning: #ff7d00;
|
||||
--lx-color-danger: #f53f3f;
|
||||
--lx-color-info: #1677ff;
|
||||
|
||||
--lx-color-chart-1: #1677ff;
|
||||
--lx-color-chart-2: #00b42a;
|
||||
--lx-color-chart-3: #ff7d00;
|
||||
--lx-color-chart-4: #722ed1;
|
||||
--lx-color-chart-5: #eb2f96;
|
||||
|
||||
/* 字体 */
|
||||
--lx-font-sans:
|
||||
'PingFang SC', 'HarmonyOS Sans SC', 'Microsoft YaHei', -apple-system, BlinkMacSystemFont,
|
||||
'Segoe UI', sans-serif;
|
||||
--lx-font-mono: 'SF Mono', 'JetBrains Mono', 'Roboto Mono', ui-monospace, monospace;
|
||||
|
||||
--lx-font-size-xs: 12px;
|
||||
--lx-font-size-sm: 13px;
|
||||
--lx-font-size-md: 14px;
|
||||
--lx-font-size-lg: 16px;
|
||||
--lx-font-size-xl: 20px;
|
||||
--lx-font-size-display: 28px;
|
||||
|
||||
--lx-line-height-tight: 1.25;
|
||||
--lx-line-height-normal: 1.55;
|
||||
|
||||
/* 间距(4px 基线) */
|
||||
--lx-space-1: 4px;
|
||||
--lx-space-2: 8px;
|
||||
--lx-space-3: 12px;
|
||||
--lx-space-4: 16px;
|
||||
--lx-space-5: 20px;
|
||||
--lx-space-6: 24px;
|
||||
--lx-space-8: 32px;
|
||||
--lx-space-10: 40px;
|
||||
|
||||
/* 结构尺寸:顶部模块菜单 + 左侧功能菜单 */
|
||||
--lx-topbar-height: 56px;
|
||||
--lx-sidebar-width: 216px;
|
||||
--lx-sidebar-collapsed-width: 56px;
|
||||
--lx-content-padding: 24px;
|
||||
|
||||
--lx-radius-sm: 4px;
|
||||
--lx-radius-md: 6px;
|
||||
--lx-radius-lg: 10px;
|
||||
--lx-radius-pill: 999px;
|
||||
|
||||
--lx-shadow-panel: 0 1px 2px rgba(31, 35, 41, 0.04);
|
||||
--lx-shadow-overlay: 0 12px 32px rgba(31, 35, 41, 0.14);
|
||||
--lx-shadow-focus: 0 0 0 3px rgba(22, 119, 255, 0.16);
|
||||
|
||||
--lx-motion-fast: 120ms;
|
||||
--lx-motion-base: 180ms;
|
||||
--lx-ease-out: cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
}
|
||||
|
||||
/* Semi Design 变量桥接:业务侧只使用 --lx-*,Semi 组件跟随同一套 token */
|
||||
body[theme-mode='light'] {
|
||||
--semi-color-primary: var(--lx-color-brand);
|
||||
--semi-color-primary-hover: var(--lx-color-brand-hover);
|
||||
--semi-color-primary-active: var(--lx-color-brand-pressed);
|
||||
--semi-color-primary-disabled: var(--lx-color-border-strong);
|
||||
--semi-color-primary-light-default: var(--lx-color-brand-soft);
|
||||
--semi-color-primary-light-hover: #dbe9ff;
|
||||
--semi-color-primary-light-active: #cfe1ff;
|
||||
|
||||
--semi-color-bg-0: var(--lx-color-bg-app);
|
||||
--semi-color-bg-1: var(--lx-color-bg-panel);
|
||||
--semi-color-bg-2: var(--lx-color-bg-panel);
|
||||
--semi-color-bg-3: var(--lx-color-bg-panel);
|
||||
--semi-color-bg-4: var(--lx-color-bg-hover);
|
||||
|
||||
--semi-color-text-0: var(--lx-color-text-primary);
|
||||
--semi-color-text-1: var(--lx-color-text-primary);
|
||||
--semi-color-text-2: var(--lx-color-text-secondary);
|
||||
--semi-color-text-3: var(--lx-color-text-tertiary);
|
||||
|
||||
--semi-color-border: var(--lx-color-border);
|
||||
--semi-color-fill-0: var(--lx-color-bg-hover);
|
||||
--semi-color-fill-1: #eef1f5;
|
||||
--semi-color-fill-2: var(--lx-color-bg-active);
|
||||
|
||||
--semi-color-danger: var(--lx-color-danger);
|
||||
--semi-color-success: var(--lx-color-success);
|
||||
--semi-color-warning: var(--lx-color-warning);
|
||||
--semi-color-info: var(--lx-color-info);
|
||||
|
||||
--semi-color-focus-border: var(--lx-color-focus);
|
||||
--semi-color-shadow: var(--lx-shadow-overlay);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* 主题 token 的 JS 副本(供 JS 计算场景使用,如 ECharts / Canvas)
|
||||
* 注意:颜色字面量只允许出现在 packages/ui/src/theme/(工程规范 03 §3.2)。
|
||||
*/
|
||||
export const themeTokens = {
|
||||
colorBrand: '#f5a524',
|
||||
colorBrandHover: '#ffb84d',
|
||||
colorSuccess: '#3fb950',
|
||||
colorWarning: '#d29922',
|
||||
colorDanger: '#f85149',
|
||||
colorInfo: '#58a6ff',
|
||||
chartColors: ['#f5a524', '#58a6ff', '#3fb950', '#d29922', '#a371f7'],
|
||||
} as const;
|
||||
|
||||
export type ThemeTokens = typeof themeTokens;
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"noEmit": true,
|
||||
"types": []
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Reference in new issue
Block a user