diff --git a/config/config.ts b/config/config.ts
index 40d383ad..d5b0ee52 100644
--- a/config/config.ts
+++ b/config/config.ts
@@ -193,7 +193,16 @@ export default defineConfig({
include: ['src/pages/**/_mock.ts'],
exclude: ['mock/requestRecord.mock.js'],
},
- utoopack: {},
+ utoopack: {
+ module: {
+ rules: {
+ '*.md': {
+ loaders: [{ loader: join(__dirname, 'md-raw-loader.cjs') }],
+ as: '*.js',
+ },
+ },
+ },
+ },
requestRecord: {},
exportStatic: {},
define: {
diff --git a/config/md-raw-loader.cjs b/config/md-raw-loader.cjs
new file mode 100644
index 00000000..abb9bbc3
--- /dev/null
+++ b/config/md-raw-loader.cjs
@@ -0,0 +1,4 @@
+module.exports = function mdRawLoader(source) {
+ this.cacheable?.();
+ return `export default ${JSON.stringify(String(source))};`;
+};
\ No newline at end of file
diff --git a/docs/cheatsheet.en-US.ts b/docs/cheatsheet.en-US.ts
deleted file mode 100644
index b87c1f2d..00000000
--- a/docs/cheatsheet.en-US.ts
+++ /dev/null
@@ -1,475 +0,0 @@
-const content = `# Ant Design Pro Cheatsheet
-
-
-
-## Getting Started
-
-**Requirements:** Node.js >= 20
-
-**Create a project:**
-
-\`\`\`bash
-git clone --depth 1 https://github.com/ant-design/ant-design-pro.git my-project
-cd my-project
-npm install
-\`\`\`
-
-The project offers two modes:
-
-- **Full mode**: Includes all demo pages (Dashboard, Forms, Lists, Access, etc.), great for reference and learning
-- **Simple mode**: Only keeps login page and basic layout, ideal for starting from scratch
-
-Switch to simple mode:
-
-\`\`\`bash
-git add -A && git commit -m "chore: save before simple" # Commit first to allow revert
-npm run simple # Remove demo pages and unused deps
-npm install # Update dependencies
-\`\`\`
-
-> 💡 Start with full mode to learn the project structure, then switch to simple mode for development.
-
-**Directory structure:**
-
-\`\`\`
-├── config/ # Configuration (routes, proxy, theme)
-│ ├── config.ts # Main config
-│ ├── routes.ts # Route definitions
-│ ├── defaultSettings.ts # Layout & theme settings
-│ └── proxy.ts # Dev proxy config
-├── mock/ # Mock data
-├── src/
-│ ├── components/ # Shared components
-│ ├── locales/ # i18n resources
-│ ├── models/ # Global data models
-│ ├── services/ # API service layer
-│ ├── utils/ # Utility functions
-│ ├── access.ts # Permission definitions
-│ └── app.tsx # Runtime configuration
-├── docs/ # Project documentation
-└── types/ # Type declarations
-\`\`\`
-
-**Common commands:**
-
-| Command | Description |
-|---------|-------------|
-| \`npm start\` | Start dev server (UMI_ENV=dev, with Mock) |
-| \`npm run dev\` | Start dev server (no Mock) |
-| \`npm run start:dev\` | Same as dev, UMI_ENV=dev, no Mock |
-| \`npm run start:no-mock\` | Start without Mock |
-| \`npm run start:pre\` | Pre-production environment |
-| \`npm run start:test\` | Test environment |
-| \`npm run build\` | Build for production |
-| \`npm run preview\` | Build and preview locally (port 8000) |
-| \`npm run analyze\` | Analyze bundle size |
-| \`npm run lint\` | Lint (Biome + TypeScript) |
-| \`npm run biome\` | Auto-fix with Biome |
-| \`npm test\` | Run tests |
-| \`npm run test:coverage\` | Test with coverage |
-| \`npm run openapi\` | Generate API code from OpenAPI schema |
-| \`npm run simple\` | Strip demo pages and unused deps |
-
-> 💡 \`UMI_ENV\` switches environment configs, mapping to different proxy rules in \`config/proxy.ts\`.
-
-> 💡 \`npm run simple\` removes demo pages (dashboard, form, list etc.) and unused dependencies (plots, etc.), replacing with minimal routes. Ideal for starting from scratch. **Commit your code first so you can revert if needed.**
-
-**Build tool:** This project uses [utoopack](https://github.com/utooland/utoo) (a next-gen bundler powered by Turbopack) as the default build tool, configured via the \`utoopack\` field in \`config/config.ts\`. utoopack is Webpack-compatible and supports \`module.rules\` for custom loaders.
-
-→ See [umi Getting Started](https://umijs.org/docs/guides/getting-started), [utoo Docs](https://utoo.land)
-
-## Routes & Menu
-
-**Route config** is in \`config/routes.ts\`:
-
-\`\`\`ts
-export default [
- {
- path: '/welcome',
- name: 'welcome', // maps to menu.welcome i18n key
- icon: 'home',
- component: './Welcome',
- },
- {
- path: '/admin',
- name: 'admin',
- icon: 'crown',
- access: 'canAdmin', // route-level access control
- routes: [...],
- },
- { path: '/', redirect: '/dashboard/analysis' },
- { component: '404', path: './*' },
-];
-\`\`\`
-
-**Route navigation:**
-
-\`\`\`tsx
-import { useNavigate, useParams, useLocation } from '@umijs/max';
-
-const navigate = useNavigate();
-navigate('/dashboard'); // navigate
-navigate(-1); // go back
-
-const { id } = useParams(); // dynamic param /user/:id
-const location = useLocation(); // current route info
-\`\`\`
-
-**Menu & access:** The \`access\` field in route config controls menu visibility — unauthorized routes won't appear in the menu.
-
-> 💡 The \`name\` field is automatically mapped to \`menu.xxx\` i18n keys. Configure translations in \`src/locales/\`.
-
-→ See [umi Routes](https://umijs.org/docs/guides/routes), [Umi Max Layout & Menu](https://umijs.org/docs/max/layout-menu)
-
-## Layout
-
-**ProLayout config** is in \`config/defaultSettings.ts\`:
-
-\`\`\`ts
-export default {
- navTheme: 'light', // nav theme: light / dark
- colorPrimary: '#1890ff', // primary color
- layout: 'mix', // layout mode: side / top / mix
- contentWidth: 'Fluid', // content width: Fluid / Fixed
- fixSiderbar: true, // fixed sidebar
-};
-\`\`\`
-
-**Layout modes:**
-- \`side\` — Side navigation
-- \`top\` — Top navigation
-- \`mix\` — Top + side mixed navigation
-
-**Page container:**
-
-\`\`\`tsx
-import { PageContainer } from '@ant-design/pro-components';
-
-const Page = () => (
-
- {/* Page content */}
-
-);
-\`\`\`
-
-**Custom areas:** Top-right \`src/components/RightContent\`, footer \`src/components/Footer\`.
-
-→ See [Umi Max Layout & Menu](https://umijs.org/docs/max/layout-menu)
-
-## Data Flow
-
-**useModel — lightweight global state:** Create a file in \`src/models/\` to auto-register:
-
-\`\`\`ts
-// src/models/counter.ts
-import { useState } from 'react';
-
-export default function useCounter() {
- const [count, setCount] = useState(0);
- const increment = () => setCount(c => c + 1);
- return { count, increment };
-}
-\`\`\`
-
-\`\`\`tsx
-// Use in any component
-import { useModel } from '@umijs/max';
-
-const { count, increment } = useModel('counter');
-\`\`\`
-
-**useRequest — data fetching:**
-
-\`\`\`tsx
-import { useRequest } from '@umijs/max';
-
-const { data, loading, error } = useRequest(getUserInfo);
-\`\`\`
-
-**React Query — server state management:**
-
-\`\`\`tsx
-import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
-
-// Query
-const { data, isLoading } = useQuery({
- queryKey: ['user', id],
- queryFn: () => getUser(id),
-});
-
-// Mutation
-const mutation = useMutation({
- mutationFn: updateUser,
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['user'] });
- },
-});
-\`\`\`
-
-**Initial state — getInitialState:** Define in \`src/app.tsx\`, accessible globally:
-
-\`\`\`tsx
-// src/app.tsx
-export async function getInitialState() {
- const currentUser = await fetchUserInfo();
- return { currentUser };
-}
-
-// Use in components
-import { useModel } from '@umijs/max';
-const { initialState } = useModel('@@initialState');
-\`\`\`
-
-> 💡 \`getInitialState\` runs once on app startup, ideal for fetching global info (user identity, permissions).
-
-→ See [Umi Max Data Flow](https://umijs.org/docs/max/data-flow)
-
-## Request
-
-**Request config** is in \`src/app.tsx\`:
-
-\`\`\`ts
-export const request: RequestConfig = {
- baseURL: 'https://api.example.com',
- timeout: 10000,
- requestInterceptors: [], // request interceptors
- responseInterceptors: [], // response interceptors
-};
-\`\`\`
-
-**Error handling** is in \`src/requestErrorConfig.ts\`, customize error code mapping and notification logic.
-
-**Using request:**
-
-\`\`\`tsx
-import { request } from '@umijs/max';
-
-// GET
-const data = await request('/api/users', { params: { page: 1 } });
-
-// POST
-await request('/api/users', { method: 'POST', data: { name: 'test' } });
-\`\`\`
-
-**OpenAPI code generation:**
-
-\`\`\`bash
-npm run openapi
-\`\`\`
-
-Auto-generates API calling code under \`src/services/\` based on \`config/openapi.json\`.
-
-> 💡 Generated code uses \`import { request } from '@umijs/max'\` directly — no manual wrapping needed.
-
-→ See [Umi Max Request](https://umijs.org/docs/max/request)
-
-## Access Control
-
-**Define permissions** in \`src/access.ts\`:
-
-\`\`\`ts
-export default function access(initialState: { currentUser?: API.CurrentUser }) {
- const { currentUser } = initialState;
- return {
- canAdmin: currentUser?.access === 'admin',
- canUser: !!currentUser,
- };
-}
-\`\`\`
-
-**Route-level access:** Add \`access\` field in route config:
-
-\`\`\`ts
-{ path: '/admin', access: 'canAdmin' }
-\`\`\`
-
-**Component-level access:**
-
-\`\`\`tsx
-import { Access, useAccess } from '@umijs/max';
-
-// Declarative
-
-
-
-
-// Imperative
-const access = useAccess();
-if (access.canAdmin) { /* ... */ }
-\`\`\`
-
-→ See [Umi Max Access](https://umijs.org/docs/max/access)
-
-## Internationalization
-
-**Config** in \`config/config.ts\`:
-
-\`\`\`ts
-locale: {
- default: 'zh-CN',
- antd: true, // sync antd component locale
- baseNavigator: true, // follow browser language
-},
-\`\`\`
-
-**File structure:**
-
-\`\`\`
-src/locales/
-├── zh-CN.ts # Chinese entry
-├── zh-CN/
-│ ├── menu.ts # Menu translations
-│ ├── pages.ts # Page translations
-│ └── ...
-├── en-US.ts # English entry
-└── en-US/
- └── ...
-\`\`\`
-
-**Usage:**
-
-\`\`\`tsx
-import { useIntl, FormattedMessage } from '@umijs/max';
-
-// Hook
-const intl = useIntl();
-intl.formatMessage({ id: 'menu.welcome' });
-
-// Component
-
-\`\`\`
-
-**Switch locale:**
-
-\`\`\`tsx
-import { setLocale } from '@umijs/max';
-setLocale('en-US', false); // false = no page reload
-\`\`\`
-
-→ See [Umi Max i18n](https://umijs.org/docs/max/i18n)
-
-## Styling
-
-**CSS Modules:** Name files \`*.module.less\` or \`*.module.css\`:
-
-\`\`\`css
-/* example.module.less */
-.container { padding: 24px; }
-.title { font-size: 16px; }
-\`\`\`
-
-\`\`\`tsx
-import styles from './example.module.less';
-
-\`\`\`
-
-**antd-style (CSS-in-JS):**
-
-\`\`\`tsx
-import { createStyles } from 'antd-style';
-
-const useStyles = createStyles(({ token, css }) => ({
- card: css\`
- background: \${token.colorBgContainer};
- border-radius: \${token.borderRadiusLG}px;
- \`,
-}));
-
-const { styles } = useStyles();
-
-\`\`\`
-
-**Tailwind CSS (v4):** Use directly in className:
-
-\`\`\`tsx
-
-\`\`\`
-
-**Dynamic theme:** Set in \`config/config.ts\` \`antd\` config:
-
-\`\`\`ts
-antd: {
- configProvider: {
- theme: {
- token: {
- colorPrimary: '#1890ff',
- borderRadius: 6,
- },
- },
- },
-},
-\`\`\`
-
-Use SettingDrawer in dev mode to switch themes in real-time.
-
-> 💡 Three styling approaches can coexist: Tailwind for layout, CSS Modules for component styles, antd-style when consuming theme tokens.
-
-→ See [umi Styling](https://umijs.org/docs/guides/styling), [Umi Max antd Dynamic Theme](https://umijs.org/docs/max/antd#动态主题)
-
-## Testing & Debugging
-
-**Jest testing:**
-
-\`\`\`bash
-npm test # Run all tests
-npm run test:coverage # With coverage report
-npm run test:update # Update snapshots
-\`\`\`
-
-Test files go next to the component, named \`*.test.ts(x)\`.
-
-**Mock data:** Create files in \`mock/\`:
-
-\`\`\`ts
-// mock/user.ts
-export default {
- 'GET /api/currentUser': { name: 'Serati Ma', access: 'admin' },
- 'POST /api/login': (req, res) => { res.end('ok'); },
-};
-\`\`\`
-
-Umi auto-registers mocks, active in dev mode.
-
-**Proxy config** is in \`config/proxy.ts\`:
-
-\`\`\`ts
-export default {
- dev: {
- '/api/': {
- target: 'http://localhost:8080',
- changeOrigin: true,
- },
- },
-};
-\`\`\`
-
-> 💡 Use \`MOCK=none\` to skip mock and proxy to backend: \`npm run start:no-mock\`.
-
-→ See [umi Testing](https://umijs.org/docs/guides/testing), [umi Mock](https://umijs.org/docs/guides/mock)
-
-## FAQ
-
-**Q: How to disable Mock?**
-\`npm run start:no-mock\` or \`cross-env MOCK=none max dev\`
-
-**Q: How to change the primary color?**
-Edit \`colorPrimary\` in \`config/defaultSettings.ts\`. Use SettingDrawer for live preview in dev mode.
-
-**Q: How to add a new page?**
-1. Create component in \`src/pages/\` 2. Add route in \`config/routes.ts\` 3. Add menu translation in \`src/locales/\` (if needed)
-
-**Q: How to add global state?**
-Create a file in \`src/models/\` exporting a custom Hook, then use \`useModel('filename')\` in components.
-
-**Q: How to deploy?**
-\`npm run build\` generates \`dist/\`. Deploy to any static file server. Set \`publicPath\` for non-root deployments.
-
-**Q: How to use OpenAPI code generation?**
-1. Configure \`openAPI\` in \`config/config.ts\` 2. Run \`npm run openapi\` 3. Code is auto-generated under \`src/services/\`
-
-→ See [umi FAQ](https://umijs.org/docs/guides/faq)`;
-
-export default content;
diff --git a/docs/cheatsheet.zh-CN.ts b/docs/cheatsheet.zh-CN.ts
deleted file mode 100644
index c59a46f2..00000000
--- a/docs/cheatsheet.zh-CN.ts
+++ /dev/null
@@ -1,475 +0,0 @@
-const content = `# Ant Design Pro Cheatsheet
-
-
-
-## 快速开始
-
-**环境要求:** Node.js >= 20
-
-**创建项目:**
-
-\`\`\`bash
-git clone --depth 1 https://github.com/ant-design/ant-design-pro.git my-project
-cd my-project
-npm install
-\`\`\`
-
-项目提供两种模式:
-
-- **完整模式**:包含所有示例页面(Dashboard、表单、列表、权限等),适合参考学习
-- **精简模式**:仅保留登录页和基础布局,适合从零开发
-
-切换精简模式:
-
-\`\`\`bash
-git add -A && git commit -m "chore: save before simple" # 先提交,以便回退
-npm run simple # 删除示例页面和多余依赖
-npm install # 更新依赖
-\`\`\`
-
-> 💡 建议先用完整模式熟悉项目结构,再切换精简模式开始开发。
-
-**目录结构:**
-
-\`\`\`
-├── config/ # 配置文件(路由、代理、主题)
-│ ├── config.ts # 主配置
-│ ├── routes.ts # 路由配置
-│ ├── defaultSettings.ts # 布局主题设置
-│ └── proxy.ts # 开发代理配置
-├── mock/ # Mock 数据
-├── src/
-│ ├── components/ # 公共组件
-│ ├── locales/ # 国际化资源
-│ ├── models/ # 全局数据模型
-│ ├── services/ # API 服务层
-│ ├── utils/ # 工具函数
-│ ├── access.ts # 权限定义
-│ └── app.tsx # 运行时配置
-├── docs/ # 项目文档
-└── types/ # 类型声明
-\`\`\`
-
-**常用命令:**
-
-| 命令 | 说明 |
-|------|------|
-| \`npm start\` | 启动开发服务器(UMI_ENV=dev,带 Mock) |
-| \`npm run dev\` | 启动开发服务器(无 Mock) |
-| \`npm run start:dev\` | 同 dev,UMI_ENV=dev,无 Mock |
-| \`npm run start:no-mock\` | 无 Mock 启动 |
-| \`npm run start:pre\` | 预发布环境启动 |
-| \`npm run start:test\` | 测试环境启动 |
-| \`npm run build\` | 构建生产产物 |
-| \`npm run preview\` | 构建并本地预览(端口 8000) |
-| \`npm run analyze\` | 构建产物体积分析 |
-| \`npm run lint\` | 代码检查(Biome + TypeScript) |
-| \`npm run biome\` | Biome 自动修复 |
-| \`npm test\` | 运行测试 |
-| \`npm run test:coverage\` | 测试覆盖率 |
-| \`npm run openapi\` | 根据 OpenAPI 生成 API 代码 |
-| \`npm run simple\` | 精简模式(删除示例页面和多余依赖) |
-
-> 💡 \`UMI_ENV\` 用于切换环境配置,对应 \`config/proxy.ts\` 中的不同代理规则。
-
-> 💡 \`npm run simple\` 会删除示例页面(dashboard、form、list 等)和多余依赖(plots 等),替换为精简路由,适合从零开始开发。**建议先提交代码,以便需要时回退。**
-
-**构建工具:** 本项目使用 [utoopack](https://github.com/utooland/utoo)(基于 Turbopack 的新一代打包器)作为默认构建工具,通过 \`config/config.ts\` 中的 \`utoopack\` 字段配置。utoopack 兼容 Webpack 配置格式,支持 \`module.rules\` 配置自定义加载器。
-
-→ 更多内容见 [umi 入门指南](https://umijs.org/docs/guides/getting-started)、[utoo 文档](https://utoo.land)
-
-## 路由与菜单
-
-**路由配置** 位于 \`config/routes.ts\`:
-
-\`\`\`ts
-export default [
- {
- path: '/welcome',
- name: 'welcome', // 对应 menu.welcome 国际化 key
- icon: 'home',
- component: './Welcome',
- },
- {
- path: '/admin',
- name: 'admin',
- icon: 'crown',
- access: 'canAdmin', // 路由级权限控制
- routes: [...],
- },
- { path: '/', redirect: '/dashboard/analysis' },
- { component: '404', path: './*' },
-];
-\`\`\`
-
-**路由导航:**
-
-\`\`\`tsx
-import { useNavigate, useParams, useLocation } from '@umijs/max';
-
-const navigate = useNavigate();
-navigate('/dashboard'); // 跳转
-navigate(-1); // 后退
-
-const { id } = useParams(); // 获取动态参数 /user/:id
-const location = useLocation(); // 当前路由信息
-\`\`\`
-
-**菜单与权限联动:** 路由配置中 \`access\` 字段控制菜单可见性,未授权路由不会出现在菜单中。
-
-> 💡 \`name\` 字段自动映射为 \`menu.xxx\` 国际化 key,在 \`src/locales/\` 中配置翻译。
-
-→ 更多内容见 [umi 路由文档](https://umijs.org/docs/guides/routes)、[Umi Max 布局与菜单](https://umijs.org/docs/max/layout-menu)
-
-## 布局
-
-**ProLayout 配置** 位于 \`config/defaultSettings.ts\`:
-
-\`\`\`ts
-export default {
- navTheme: 'light', // 导航主题:light / dark
- colorPrimary: '#1890ff', // 主题色
- layout: 'mix', // 布局模式:side / top / mix
- contentWidth: 'Fluid', // 内容宽度:Fluid / Fixed
- fixSiderbar: true, // 固定侧边栏
-};
-\`\`\`
-
-**布局模式:**
-- \`side\` — 左侧导航
-- \`top\` — 顶部导航
-- \`mix\` — 顶部 + 侧边混合导航
-
-**页面容器:**
-
-\`\`\`tsx
-import { PageContainer } from '@ant-design/pro-components';
-
-const Page = () => (
-
- {/* 页面内容 */}
-
-);
-\`\`\`
-
-**自定义区域:** 右上角 \`src/components/RightContent\`,底部 \`src/components/Footer\`。
-
-→ 更多内容见 [Umi Max 布局与菜单](https://umijs.org/docs/max/layout-menu)
-
-## 数据流
-
-**useModel — 轻量全局状态:** 在 \`src/models/\` 下创建文件即自动注册:
-
-\`\`\`ts
-// src/models/counter.ts
-import { useState } from 'react';
-
-export default function useCounter() {
- const [count, setCount] = useState(0);
- const increment = () => setCount(c => c + 1);
- return { count, increment };
-}
-\`\`\`
-
-\`\`\`tsx
-// 任意组件中使用
-import { useModel } from '@umijs/max';
-
-const { count, increment } = useModel('counter');
-\`\`\`
-
-**useRequest — 数据请求:**
-
-\`\`\`tsx
-import { useRequest } from '@umijs/max';
-
-const { data, loading, error } = useRequest(getUserInfo);
-\`\`\`
-
-**React Query — 服务端状态管理:**
-
-\`\`\`tsx
-import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
-
-// 查询
-const { data, isLoading } = useQuery({
- queryKey: ['user', id],
- queryFn: () => getUser(id),
-});
-
-// 变更
-const mutation = useMutation({
- mutationFn: updateUser,
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['user'] });
- },
-});
-\`\`\`
-
-**初始状态 — getInitialState:** 在 \`src/app.tsx\` 中定义,全局可访问:
-
-\`\`\`tsx
-// src/app.tsx
-export async function getInitialState() {
- const currentUser = await fetchUserInfo();
- return { currentUser };
-}
-
-// 组件中使用
-import { useModel } from '@umijs/max';
-const { initialState } = useModel('@@initialState');
-\`\`\`
-
-> 💡 \`getInitialState\` 在应用启动时执行一次,适合获取全局信息(如用户身份、权限)。
-
-→ 更多内容见 [Umi Max 数据流](https://umijs.org/docs/max/data-flow)
-
-## 请求
-
-**请求配置** 位于 \`src/app.tsx\`:
-
-\`\`\`ts
-export const request: RequestConfig = {
- baseURL: 'https://api.example.com',
- timeout: 10000,
- requestInterceptors: [], // 请求拦截器
- responseInterceptors: [], // 响应拦截器
-};
-\`\`\`
-
-**错误处理** 位于 \`src/requestErrorConfig.ts\`,可自定义错误码映射和提示逻辑。
-
-**使用请求:**
-
-\`\`\`tsx
-import { request } from '@umijs/max';
-
-// GET
-const data = await request('/api/users', { params: { page: 1 } });
-
-// POST
-await request('/api/users', { method: 'POST', data: { name: 'test' } });
-\`\`\`
-
-**OpenAPI 代码生成:**
-
-\`\`\`bash
-npm run openapi
-\`\`\`
-
-根据 \`config/openapi.json\` 自动生成 \`src/services/\` 下的 API 调用代码。
-
-> 💡 生成后的代码直接用 \`import { request } from '@umijs/max'\` 发起请求,无需手动封装。
-
-→ 更多内容见 [Umi Max 请求](https://umijs.org/docs/max/request)
-
-## 权限
-
-**定义权限** 在 \`src/access.ts\`:
-
-\`\`\`ts
-export default function access(initialState: { currentUser?: API.CurrentUser }) {
- const { currentUser } = initialState;
- return {
- canAdmin: currentUser?.access === 'admin',
- canUser: !!currentUser,
- };
-}
-\`\`\`
-
-**路由级权限:** 在路由配置中添加 \`access\` 字段:
-
-\`\`\`ts
-{ path: '/admin', access: 'canAdmin' }
-\`\`\`
-
-**组件级权限:**
-
-\`\`\`tsx
-import { Access, useAccess } from '@umijs/max';
-
-// 声明式
-
-
-
-
-// 命令式
-const access = useAccess();
-if (access.canAdmin) { /* ... */ }
-\`\`\`
-
-→ 更多内容见 [Umi Max 权限](https://umijs.org/docs/max/access)
-
-## 国际化
-
-**配置** 在 \`config/config.ts\`:
-
-\`\`\`ts
-locale: {
- default: 'zh-CN',
- antd: true, // 同步 antd 组件语言
- baseNavigator: true, // 跟随浏览器语言
-},
-\`\`\`
-
-**文件结构:**
-
-\`\`\`
-src/locales/
-├── zh-CN.ts # 中文入口
-├── zh-CN/
-│ ├── menu.ts # 菜单翻译
-│ ├── pages.ts # 页面翻译
-│ └── ...
-├── en-US.ts # 英文入口
-└── en-US/
- └── ...
-\`\`\`
-
-**使用方式:**
-
-\`\`\`tsx
-import { useIntl, FormattedMessage } from '@umijs/max';
-
-// Hook 方式
-const intl = useIntl();
-intl.formatMessage({ id: 'menu.welcome' });
-
-// 组件方式
-
-\`\`\`
-
-**切换语言:**
-
-\`\`\`tsx
-import { setLocale } from '@umijs/max';
-setLocale('en-US', false); // false = 不刷新页面
-\`\`\`
-
-→ 更多内容见 [Umi Max 国际化](https://umijs.org/docs/max/i18n)
-
-## 样式
-
-**CSS Modules:** 文件命名为 \`*.module.less\` 或 \`*.module.css\`:
-
-\`\`\`css
-/* example.module.less */
-.container { padding: 24px; }
-.title { font-size: 16px; }
-\`\`\`
-
-\`\`\`tsx
-import styles from './example.module.less';
-
-\`\`\`
-
-**antd-style(CSS-in-JS):**
-
-\`\`\`tsx
-import { createStyles } from 'antd-style';
-
-const useStyles = createStyles(({ token, css }) => ({
- card: css\`
- background: \${token.colorBgContainer};
- border-radius: \${token.borderRadiusLG}px;
- \`,
-}));
-
-const { styles } = useStyles();
-
-\`\`\`
-
-**Tailwind CSS(v4):** 直接在 className 中使用:
-
-\`\`\`tsx
-
-\`\`\`
-
-**动态主题:** 在 \`config/config.ts\` 的 \`antd\` 配置中设置:
-
-\`\`\`ts
-antd: {
- configProvider: {
- theme: {
- token: {
- colorPrimary: '#1890ff',
- borderRadius: 6,
- },
- },
- },
-},
-\`\`\`
-
-开发环境可通过右下角 SettingDrawer 实时切换主题。
-
-> 💡 三种样式方案可以共存:Tailwind 适合布局、CSS Modules 适合组件样式、antd-style 适合需要消费主题 token 的场景。
-
-→ 更多内容见 [umi 样式文档](https://umijs.org/docs/guides/styling)、[Umi Max antd 动态主题](https://umijs.org/docs/max/antd#动态主题)
-
-## 测试 & 调试
-
-**Jest 测试:**
-
-\`\`\`bash
-npm test # 运行所有测试
-npm run test:coverage # 带覆盖率报告
-npm run test:update # 更新快照
-\`\`\`
-
-测试文件放在对应组件目录下,命名为 \`*.test.ts(x)\`。
-
-**Mock 数据:** 在 \`mock/\` 目录下创建文件:
-
-\`\`\`ts
-// mock/user.ts
-export default {
- 'GET /api/currentUser': { name: 'Serati Ma', access: 'admin' },
- 'POST /api/login': (req, res) => { res.end('ok'); },
-};
-\`\`\`
-
-Umi 自动注册 mock,开发模式下生效。
-
-**代理配置** 位于 \`config/proxy.ts\`:
-
-\`\`\`ts
-export default {
- dev: {
- '/api/': {
- target: 'http://localhost:8080',
- changeOrigin: true,
- },
- },
-};
-\`\`\`
-
-> 💡 用 \`MOCK=none\` 启动可跳过 Mock,直接代理到后端:\`npm run start:no-mock\`。
-
-→ 更多内容见 [umi 测试](https://umijs.org/docs/guides/testing)、[umi Mock](https://umijs.org/docs/guides/mock)
-
-## FAQ
-
-**Q: 如何关闭 Mock?**
-\`npm run start:no-mock\` 或 \`cross-env MOCK=none max dev\`
-
-**Q: 如何修改主题色?**
-修改 \`config/defaultSettings.ts\` 的 \`colorPrimary\`,开发时可用 SettingDrawer 实时调整。
-
-**Q: 如何添加新页面?**
-1. 在 \`src/pages/\` 下创建组件 2. 在 \`config/routes.ts\` 添加路由 3. 在 \`src/locales/\` 添加菜单翻译(如需)
-
-**Q: 如何添加全局状态?**
-在 \`src/models/\` 下创建文件,导出自定义 Hook,组件中通过 \`useModel('文件名')\` 使用。
-
-**Q: 如何部署?**
-\`npm run build\` 生成 \`dist/\` 目录,部署到任意静态服务器。配置 \`publicPath\` 处理非根目录部署。
-
-**Q: 如何使用 OpenAPI 代码生成?**
-1. 在 \`config/config.ts\` 配置 \`openAPI\` 2. 运行 \`npm run openapi\` 3. 自动生成 \`src/services/\` 下的代码
-
-→ 更多内容见 [umi FAQ](https://umijs.org/docs/guides/faq)`;
-
-export default content;
diff --git a/jest.config.ts b/jest.config.ts
index f61808c0..9a36fc1b 100644
--- a/jest.config.ts
+++ b/jest.config.ts
@@ -10,6 +10,7 @@ export default async (): Promise => {
...config,
testPathIgnorePatterns: ['/node_modules/', '/.worktrees/'],
moduleNameMapper: {
+ '\\.md$': '/tests/__mocks__/raw.js',
...(config.moduleNameMapper || {}),
'^mermaid$': '/tests/__mocks__/mermaid.js',
},
diff --git a/src/pages/Welcome-dark.css b/src/pages/Welcome-dark.css
index 2a5c2b42..78a9a751 100644
--- a/src/pages/Welcome-dark.css
+++ b/src/pages/Welcome-dark.css
@@ -1,67 +1,67 @@
/* Dark mode: override highlight.js github-gist (white) with GitHub Dark theme */
-.dark .welcome-markdown .x-markdown pre {
+[data-theme="dark"] .welcome-markdown .x-markdown pre {
background: var(--md-pre-bg);
border-color: var(--md-pre-border);
}
-.dark .welcome-markdown .x-markdown pre code.hljs {
+[data-theme="dark"] .welcome-markdown .x-markdown pre code.hljs {
background: transparent;
color: #e6edf3;
}
/* Keywords: if, const, import, return, etc. */
-.dark .welcome-markdown .x-markdown .hljs-keyword,
-.dark .welcome-markdown .x-markdown .hljs-selector-tag,
-.dark .welcome-markdown .x-markdown .hljs-literal,
-.dark .welcome-markdown .x-markdown .hljs-section,
-.dark .welcome-markdown .x-markdown .hljs-link {
+[data-theme="dark"] .welcome-markdown .x-markdown .hljs-keyword,
+[data-theme="dark"] .welcome-markdown .x-markdown .hljs-selector-tag,
+[data-theme="dark"] .welcome-markdown .x-markdown .hljs-literal,
+[data-theme="dark"] .welcome-markdown .x-markdown .hljs-section,
+[data-theme="dark"] .welcome-markdown .x-markdown .hljs-link {
color: #ff7b72;
}
/* Strings, types, attributes */
-.dark .welcome-markdown .x-markdown .hljs-string,
-.dark .welcome-markdown .x-markdown .hljs-title,
-.dark .welcome-markdown .x-markdown .hljs-name,
-.dark .welcome-markdown .x-markdown .hljs-type,
-.dark .welcome-markdown .x-markdown .hljs-attribute,
-.dark .welcome-markdown .x-markdown .hljs-symbol,
-.dark .welcome-markdown .x-markdown .hljs-bullet,
-.dark .welcome-markdown .x-markdown .hljs-addition,
-.dark .welcome-markdown .x-markdown .hljs-variable,
-.dark .welcome-markdown .x-markdown .hljs-template-tag,
-.dark .welcome-markdown .x-markdown .hljs-template-variable {
+[data-theme="dark"] .welcome-markdown .x-markdown .hljs-string,
+[data-theme="dark"] .welcome-markdown .x-markdown .hljs-title,
+[data-theme="dark"] .welcome-markdown .x-markdown .hljs-name,
+[data-theme="dark"] .welcome-markdown .x-markdown .hljs-type,
+[data-theme="dark"] .welcome-markdown .x-markdown .hljs-attribute,
+[data-theme="dark"] .welcome-markdown .x-markdown .hljs-symbol,
+[data-theme="dark"] .welcome-markdown .x-markdown .hljs-bullet,
+[data-theme="dark"] .welcome-markdown .x-markdown .hljs-addition,
+[data-theme="dark"] .welcome-markdown .x-markdown .hljs-variable,
+[data-theme="dark"] .welcome-markdown .x-markdown .hljs-template-tag,
+[data-theme="dark"] .welcome-markdown .x-markdown .hljs-template-variable {
color: #a5d6ff;
}
/* Comments, quotes, deletions */
-.dark .welcome-markdown .x-markdown .hljs-comment,
-.dark .welcome-markdown .x-markdown .hljs-quote,
-.dark .welcome-markdown .x-markdown .hljs-deletion,
-.dark .welcome-markdown .x-markdown .hljs-meta {
+[data-theme="dark"] .welcome-markdown .x-markdown .hljs-comment,
+[data-theme="dark"] .welcome-markdown .x-markdown .hljs-quote,
+[data-theme="dark"] .welcome-markdown .x-markdown .hljs-deletion,
+[data-theme="dark"] .welcome-markdown .x-markdown .hljs-meta {
color: #8b949e;
}
/* Numbers */
-.dark .welcome-markdown .x-markdown .hljs-number {
+[data-theme="dark"] .welcome-markdown .x-markdown .hljs-number {
color: #79c0ff;
}
/* Built-in types, class names */
-.dark .welcome-markdown .x-markdown .hljs-built_in,
-.dark .welcome-markdown .x-markdown .hljs-class .hljs-title,
-.dark .welcome-markdown .x-markdown .hljs-title.class_ {
+[data-theme="dark"] .welcome-markdown .x-markdown .hljs-built_in,
+[data-theme="dark"] .welcome-markdown .x-markdown .hljs-class .hljs-title,
+[data-theme="dark"] .welcome-markdown .x-markdown .hljs-title.class_ {
color: #ffa657;
}
/* RegExp, emphasis */
-.dark .welcome-markdown .x-markdown .hljs-regexp,
-.dark .welcome-markdown .x-markdown .hljs-emphasis {
+[data-theme="dark"] .welcome-markdown .x-markdown .hljs-regexp,
+[data-theme="dark"] .welcome-markdown .x-markdown .hljs-emphasis {
color: #7ee787;
}
/* Inline code */
-.dark .welcome-markdown .x-markdown code {
+[data-theme="dark"] .welcome-markdown .x-markdown code {
background: var(--md-code-bg);
border-color: var(--md-code-border);
color: #e6edf3;
diff --git a/src/pages/Welcome.css b/src/pages/Welcome.css
index daf470ea..0a513451 100644
--- a/src/pages/Welcome.css
+++ b/src/pages/Welcome.css
@@ -14,7 +14,7 @@
--md-tr-hover-bg: rgba(0, 0, 0, 0.02);
}
-.dark .welcome-markdown .x-markdown {
+[data-theme="dark"] .welcome-markdown .x-markdown {
--md-code-bg: rgba(255, 255, 255, 0.08);
--md-code-border: rgba(255, 255, 255, 0.1);
--md-pre-bg: #161b22;
diff --git a/src/pages/Welcome.tsx b/src/pages/Welcome.tsx
index d205d61c..87d1c1a1 100644
--- a/src/pages/Welcome.tsx
+++ b/src/pages/Welcome.tsx
@@ -1,8 +1,8 @@
import { PageContainer } from '@ant-design/pro-components';
import XMarkdown from '@ant-design/x-markdown';
import '@ant-design/x-markdown/es/XMarkdown/index.css';
-import enUS from '@root/docs/cheatsheet.en-US';
-import zhCN from '@root/docs/cheatsheet.zh-CN';
+import enUS from '@root/docs/cheatsheet.en-US.md';
+import zhCN from '@root/docs/cheatsheet.zh-CN.md';
import { getLocale, useIntl, useModel } from '@umijs/max';
import { Card } from 'antd';
import hljs from 'highlight.js';
@@ -28,9 +28,7 @@ const InfoCard: React.FC = ({ title, index, desc, href }) => (
{title}
-
- {desc}
-
+
{desc}
@@ -147,14 +145,17 @@ const Welcome: React.FC = () => {
const normalizedLocale = locale.toLowerCase();
const content =
mdContent[locale] ??
- (normalizedLocale.startsWith('zh') ? mdContent['zh-CN'] : mdContent['en-US']);
+ (normalizedLocale.startsWith('zh')
+ ? mdContent['zh-CN']
+ : mdContent['en-US']);
const { initialState } = useModel('@@initialState');
const isDark = initialState?.settings?.navTheme === 'realDark';
return (
@@ -185,4 +186,4 @@ const Welcome: React.FC = () => {
);
};
-export default Welcome;
\ No newline at end of file
+export default Welcome;
diff --git a/src/typings.d.ts b/src/typings.d.ts
index 793a45e5..6191ba4b 100644
--- a/src/typings.d.ts
+++ b/src/typings.d.ts
@@ -10,6 +10,10 @@ declare module '*.jpeg';
declare module '*.gif';
declare module '*.bmp';
declare module '*.tiff';
+declare module '*.md' {
+ const content: string;
+ export default content;
+}
declare module 'omit.js';
declare module 'mockjs';
diff --git a/tailwind.config.js b/tailwind.config.js
index a9b5d194..a403104a 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -1,4 +1,3 @@
module.exports = {
content: ['./src/**/*.tsx'],
- darkMode: 'class',
};
diff --git a/tests/__mocks__/raw.js b/tests/__mocks__/raw.js
new file mode 100644
index 00000000..9f0aea47
--- /dev/null
+++ b/tests/__mocks__/raw.js
@@ -0,0 +1 @@
+module.exports = 'mock-md-content';