212 lines
8.6 KiB
TypeScript
212 lines
8.6 KiB
TypeScript
/**
|
||
* # 脚本 烘焙 AntD 静态样式
|
||
* 背景:AntD v6 的 CSS-in-JS 样式只在客户端渲染时注入,SSR 首屏会出现
|
||
* "无样式闪烁"(后台打开瞬间按钮/表单是浏览器默认样式)。
|
||
* 本脚本把后台用到的全部 AntD 组件以与 admin.tsx 一致的主题渲染一遍,
|
||
* 用 @ant-design/cssinjs 的 extractStyle 导出静态 CSS,写入 src/styles/antd-baked.css,
|
||
* 由 admin.tsx 直接 import,使 SSR 首屏就带上完整 AntD 样式。
|
||
*
|
||
* 运行:bun scripts/gen-antd-css.tsx(主题变更后需重新执行)
|
||
* 输出到 public/antd-baked.css(静态文件,admin 路由 <link> 引入)。
|
||
* 注意:不能以 JS import 方式引入 —— dev 模式 vite 会把 658KB CSS 注入成
|
||
* <style> 标签,且 dev 预载全部路由模块会把它带到前台每个页面,F12 卡死。
|
||
*/
|
||
import { mkdirSync, writeFileSync } from "node:fs";
|
||
import { renderToString } from "react-dom/server";
|
||
import { renderToString as _r } from "react-dom/server";
|
||
import {
|
||
Alert,
|
||
App,
|
||
Avatar,
|
||
Button,
|
||
Card,
|
||
Col,
|
||
ConfigProvider,
|
||
Descriptions,
|
||
Drawer,
|
||
Dropdown,
|
||
Empty,
|
||
Form,
|
||
Image,
|
||
Input,
|
||
InputNumber,
|
||
Layout,
|
||
Menu,
|
||
Modal,
|
||
Pagination,
|
||
Popconfirm,
|
||
Row,
|
||
Select,
|
||
Space,
|
||
Statistic,
|
||
Switch,
|
||
Table,
|
||
Tabs,
|
||
Tag,
|
||
Tooltip,
|
||
Tree,
|
||
Typography,
|
||
} from "antd";
|
||
import { createCache, StyleProvider, extractStyle } from "@ant-design/cssinjs";
|
||
|
||
/** # 成员变量 与 admin.tsx 完全一致的主题配置(改动后必须重新烘焙)
|
||
* cssVar.key 固定为 "dusun":变量作用域类名确定,烘焙 CSS 与运行时 DOM 一致 **/
|
||
const THEME = { token: { colorPrimary: "#1677ff", fontSize: 16 }, cssVar: { key: "dusun" } };
|
||
|
||
/** # 组件 覆盖后台全部 AntD 组件的样例树(每个组件至少渲染一次以注册其样式) **/
|
||
function Showcase() {
|
||
return (
|
||
<ConfigProvider theme={THEME}>
|
||
<App>
|
||
<Layout style={{ minHeight: 10 }}>
|
||
<Layout.Sider width={10}>
|
||
<Menu
|
||
mode="inline"
|
||
theme="dark"
|
||
items={[
|
||
{ key: "a", label: "A" },
|
||
{ key: "b", label: "B" },
|
||
{ type: "submenu", key: "c", label: "C", children: [{ key: "d", label: "D" }] },
|
||
]}
|
||
/>
|
||
</Layout.Sider>
|
||
<Layout>
|
||
<Layout.Header style={{ height: 10 }}>H</Layout.Header>
|
||
<Layout.Content style={{ padding: 4 }}>
|
||
<Card title="Card" extra={<Button type="primary">主要</Button>}>
|
||
<Space wrap>
|
||
<Button>默认</Button>
|
||
<Button type="primary">主要</Button>
|
||
<Button type="dashed">虚线</Button>
|
||
<Button type="link">链接</Button>
|
||
<Button type="text">文本</Button>
|
||
<Button danger>危险</Button>
|
||
<Button size="small">小</Button>
|
||
<Button type="primary" loading>
|
||
加载
|
||
</Button>
|
||
<Avatar icon={<span />} size="small" />
|
||
<Tag color="blue">蓝</Tag>
|
||
<Tag>默认</Tag>
|
||
<Switch defaultChecked />
|
||
<InputNumber defaultValue={1} />
|
||
</Space>
|
||
<Row gutter={8}>
|
||
<Col span={12}>
|
||
<Form layout="vertical">
|
||
<Form.Item label="邮箱" required>
|
||
<Input placeholder="email" />
|
||
</Form.Item>
|
||
<Form.Item label="数量" validateStatus="error" extra="提示">
|
||
<InputNumber />
|
||
</Form.Item>
|
||
<Form.Item label="选择">
|
||
<Select
|
||
options={[
|
||
{ value: 1, label: "一" },
|
||
{ value: 2, label: "二" },
|
||
]}
|
||
defaultValue={1}
|
||
style={{ width: 120 }}
|
||
/>
|
||
</Form.Item>
|
||
<Form.Item>
|
||
<Button type="primary" htmlType="submit">
|
||
提交
|
||
</Button>
|
||
</Form.Item>
|
||
</Form>
|
||
</Col>
|
||
<Col span={12}>
|
||
<Tabs
|
||
items={[
|
||
{ key: "1", label: "Tab1", children: <p>内容1</p> },
|
||
{ key: "2", label: "Tab2", children: <p>内容2</p> },
|
||
]}
|
||
/>
|
||
<Descriptions title="详情" bordered size="small" column={1}>
|
||
<Descriptions.Item label="订单号">DH-001</Descriptions.Item>
|
||
</Descriptions>
|
||
<Statistic title="询盘" value={12} />
|
||
<Typography.Title level={5}>标题</Typography.Title>
|
||
<Typography.Text type="secondary">说明</Typography.Text>
|
||
<Image src="data:image/gif;base64,R0lGODlhAQABAAAAACw=" width={8} height={8} alt="" />
|
||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="空" />
|
||
</Col>
|
||
</Row>
|
||
<Table
|
||
size="small"
|
||
pagination={{ pageSize: 5, total: 6 }}
|
||
dataSource={[{ k: 1, name: "行" }]}
|
||
rowKey="k"
|
||
columns={[
|
||
{ title: "名称", dataIndex: "name" },
|
||
{
|
||
title: "操作",
|
||
render: () => (
|
||
<Space>
|
||
<Button size="small">编辑</Button>
|
||
<Popconfirm title="确认删除?">
|
||
<Button danger size="small">
|
||
删除
|
||
</Button>
|
||
</Popconfirm>
|
||
</Space>
|
||
),
|
||
},
|
||
]}
|
||
/>
|
||
</Card>
|
||
<Modal open title="弹窗" okText="确定" cancelText="取消">
|
||
内容
|
||
</Modal>
|
||
<Drawer open title="抽屉" width={120}>
|
||
内容
|
||
</Drawer>
|
||
{/* v0.16.0 后台新增组件:Dropdown(用户菜单)/ Tooltip(编辑器工具栏)/ Tree(权限页)/ Alert / Pagination */}
|
||
<Dropdown menu={{ items: [{ key: "a", label: "菜单项" }] }} open>
|
||
<Button>下拉</Button>
|
||
</Dropdown>
|
||
<Tooltip title="提示" open>
|
||
<span>悬停</span>
|
||
</Tooltip>
|
||
<Tree
|
||
checkable
|
||
defaultExpandAll
|
||
treeData={[{ title: "分组", key: "g", children: [{ title: "子项", key: "c" }] }]}
|
||
/>
|
||
<Alert type="info" showIcon message="提示" style={{ marginTop: 8 }} />
|
||
<Pagination size="small" total={50} style={{ marginTop: 8 }} />
|
||
</Layout.Content>
|
||
<Layout.Footer>F</Layout.Footer>
|
||
</Layout>
|
||
</Layout>
|
||
</App>
|
||
</ConfigProvider>
|
||
);
|
||
}
|
||
|
||
// 渲染样例树并提取样式(hashPriority=high → 生成无哈希的 .ant-* 选择器,
|
||
// 与 admin.tsx 的 StyleProvider hashPriority="high" 保持一致)
|
||
const cache = createCache();
|
||
renderToString(
|
||
<StyleProvider cache={cache} hashPriority="high">
|
||
<Showcase />
|
||
</StyleProvider>,
|
||
);
|
||
const rawCss = extractStyle(cache, { plain: true }) as string;
|
||
if (!rawCss || rawCss.length < 10000 || !rawCss.includes(".ant-btn")) {
|
||
console.error("提取结果异常,中止写入。length:", rawCss?.length);
|
||
process.exit(1);
|
||
}
|
||
// 变量作用域块 .dusun{--ant-*} 降为零特异性 :where(.dusun):
|
||
// 运行时切深色时 cssinjs 重新注入的 .dusun 变量块(0,1,0)必须能覆盖烘焙 <link>
|
||
// 里的浅色变量(<link> 在 DOM 中晚于运行时 <style>,同特异性会反盖回去)。
|
||
// :where() 特异性为 0,仅剩烘焙块时(SSR 首屏)仍正常生效。
|
||
const css = rawCss.replace(/\.dusun\{/g, ":where(.dusun){");
|
||
console.log("var scope blocks downgraded:", (rawCss.match(/\.dusun\{/g) ?? []).length);
|
||
mkdirSync(new URL("../public/", import.meta.url), { recursive: true });
|
||
const out = new URL("../public/antd-baked.css", import.meta.url);
|
||
writeFileSync(out, `/* 本文件由 scripts/gen-antd-css.tsx 生成(AntD SSR 静态样式,防首屏闪烁),请勿手改 */\n${css}\n`);
|
||
console.log("written:", out.pathname, `${Math.round(css.length / 1024)}KB`);
|