Files
dusuniot/scripts/gen-antd-css.tsx
T

212 lines
8.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* # 脚本 烘焙 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`);