@lmy54321/design-system 1.3.4 → 1.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codebuddy/rules/design-system.mdc +0 -1
- package/.codebuddy/rules/init-project.mdc +2 -3
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +3 -1
- package/template/demo.html +17 -0
- package/template/scripts/gen-demo-manifest.mjs +52 -0
- package/template/src/demo-main.tsx +16 -0
- package/template/src/pages/DeployPage.tsx +587 -48
- package/template/src/pages/DevPortal.tsx +9 -14
- package/template/vite.config.ts +39 -1
- package/template/vite.demo.config.ts +21 -0
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
description:
|
|
2
|
+
description: 当用户说"初始化项目"、"新建项目"、"搭建项目"、"项目初始化"、"从零搭建项目"时触发。自动完成 React + Vite + @lmy54321/design-system 的完整初始化。
|
|
3
3
|
alwaysApply: false
|
|
4
4
|
enabled: true
|
|
5
|
-
updatedAt: 2026-03-08T12:00:00.000Z
|
|
6
5
|
provider:
|
|
7
6
|
---
|
|
8
7
|
|
|
@@ -10,7 +9,7 @@ provider:
|
|
|
10
9
|
|
|
11
10
|
> **核心原则:假设用户的电脑是全新的,没有任何开发环境,用户也不懂编程。所有步骤必须 AI 全自动完成,不让用户做任何手动操作。**
|
|
12
11
|
|
|
13
|
-
|
|
12
|
+
按以下步骤自动执行,**无需用户确认每一步**:
|
|
14
13
|
|
|
15
14
|
## 环境要求
|
|
16
15
|
|
package/dist/index.js
CHANGED
package/dist/index.mjs
CHANGED
|
@@ -3740,7 +3740,7 @@ function GridSystemDocs() {
|
|
|
3740
3740
|
}
|
|
3741
3741
|
|
|
3742
3742
|
// index.ts
|
|
3743
|
-
var VERSION = "1.3.
|
|
3743
|
+
var VERSION = "1.3.6";
|
|
3744
3744
|
|
|
3745
3745
|
export { ActionSheet, BottomActionButtons, BottomNavigationBar, BottomSheet, BottomSheetOption, BottomSheetShareItem, BottomToolbar, Btn, BubbleTip, CardDemo, DRAWER_STATES, Dialog, DraggablePanel, EmptyState, GridSystemDocs, ICON_GROUPS, ICON_NAMES, IcExpand, IcPlan, IconFont, IconGallery, ImageWithFallback, Loading, NewsItem, NotificationBar, POIListItem, PoiItem, Push, SearchBox, SegmentedControl, StatGrid, Switch, Tag, TencentMap, Toast, TopToolbar, TypographyDocs, VERSION, cn, hasFilledVariant };
|
|
3746
3746
|
//# sourceMappingURL=index.mjs.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lmy54321/design-system",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.6",
|
|
4
4
|
"description": "A comprehensive React component library and design system based on Tailwind CSS and Motion.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"scripts": {
|
|
27
27
|
"dev": "vite",
|
|
28
28
|
"build": "tsup",
|
|
29
|
+
"build:demo": "vite build --config vite.demo.config.ts && mv demo-dist/demo.html demo-dist/index.html && node scripts/gen-demo-manifest.mjs",
|
|
29
30
|
"build:check": "tsc --noEmit",
|
|
30
31
|
"prepublishOnly": "npm run build",
|
|
31
32
|
"lint": "eslint .",
|
|
@@ -41,6 +42,7 @@
|
|
|
41
42
|
"clsx": "^2.1.0",
|
|
42
43
|
"lucide-react": "^0.330.0",
|
|
43
44
|
"motion": "^12.0.0",
|
|
45
|
+
"qrcode.react": "^4.2.0",
|
|
44
46
|
"react-router-dom": "^7.13.1",
|
|
45
47
|
"tailwind-merge": "^2.2.1",
|
|
46
48
|
"tdesign-icons-react": "^0.6.2"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
|
6
|
+
<meta name="mobile-web-app-capable" content="yes">
|
|
7
|
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
8
|
+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
9
|
+
<meta name="theme-color" content="#4B526B">
|
|
10
|
+
<link rel="manifest" href="/manifest.json">
|
|
11
|
+
<title>Design Demo</title>
|
|
12
|
+
</head>
|
|
13
|
+
<body>
|
|
14
|
+
<div id="root"></div>
|
|
15
|
+
<script type="module" src="/src/demo-main.tsx"></script>
|
|
16
|
+
</body>
|
|
17
|
+
</html>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 构建后脚本:扫描 demo-dist/ 中的所有文件,
|
|
3
|
+
* 生成 demo-manifest.json(路径 + SHA-1,不含文件内容),
|
|
4
|
+
* 供 DeployPage 在浏览器中上传到 Netlify。
|
|
5
|
+
*
|
|
6
|
+
* 文件内容通过 dev server 的 /demo-dist/... 路径按需获取。
|
|
7
|
+
*/
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import crypto from "crypto";
|
|
11
|
+
|
|
12
|
+
const DIST_DIR = path.resolve(process.cwd(), "demo-dist");
|
|
13
|
+
const OUTPUT = path.resolve(process.cwd(), "public/demo-manifest.json");
|
|
14
|
+
|
|
15
|
+
function walkDir(dir, base = "") {
|
|
16
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
17
|
+
const files = [];
|
|
18
|
+
for (const entry of entries) {
|
|
19
|
+
if (entry.name.startsWith(".")) continue;
|
|
20
|
+
const fullPath = path.join(dir, entry.name);
|
|
21
|
+
const relPath = base ? `${base}/${entry.name}` : entry.name;
|
|
22
|
+
if (entry.isDirectory()) {
|
|
23
|
+
files.push(...walkDir(fullPath, relPath));
|
|
24
|
+
} else {
|
|
25
|
+
const buf = fs.readFileSync(fullPath);
|
|
26
|
+
const sha1 = crypto.createHash("sha1").update(buf).digest("hex");
|
|
27
|
+
files.push({
|
|
28
|
+
path: "/" + relPath,
|
|
29
|
+
sha1,
|
|
30
|
+
size: buf.length,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return files;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
console.log("Scanning demo-dist/...");
|
|
38
|
+
const files = walkDir(DIST_DIR);
|
|
39
|
+
const manifest = {
|
|
40
|
+
buildTime: new Date().toISOString(),
|
|
41
|
+
fileCount: files.length,
|
|
42
|
+
totalSize: files.reduce((s, f) => s + f.size, 0),
|
|
43
|
+
files,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
fs.writeFileSync(OUTPUT, JSON.stringify(manifest));
|
|
47
|
+
const manifestSize = Buffer.byteLength(JSON.stringify(manifest));
|
|
48
|
+
console.log(
|
|
49
|
+
`Generated demo-manifest.json: ${files.length} files, ` +
|
|
50
|
+
`total ${(manifest.totalSize / 1024 / 1024).toFixed(1)} MB, ` +
|
|
51
|
+
`manifest ${(manifestSize / 1024).toFixed(1)} KB`
|
|
52
|
+
);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import ReactDOM from "react-dom/client";
|
|
3
|
+
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
|
4
|
+
import App from "./App";
|
|
5
|
+
import "./index.css";
|
|
6
|
+
|
|
7
|
+
// Demo-only 入口:只包含手机预览的内容,排除开发工具页面
|
|
8
|
+
ReactDOM.createRoot(document.getElementById("root")!).render(
|
|
9
|
+
<React.StrictMode>
|
|
10
|
+
<BrowserRouter>
|
|
11
|
+
<Routes>
|
|
12
|
+
<Route path="/*" element={<App />} />
|
|
13
|
+
</Routes>
|
|
14
|
+
</BrowserRouter>
|
|
15
|
+
</React.StrictMode>
|
|
16
|
+
);
|
|
@@ -1,41 +1,441 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
1
|
+
import { useState, useEffect, useRef } from "react";
|
|
2
2
|
import { useNavigate } from "react-router-dom";
|
|
3
|
-
import { Btn, IconFont, cn } from "@lmy54321/design-system";
|
|
3
|
+
import { Btn, IconFont, Toast, cn } from "@lmy54321/design-system";
|
|
4
4
|
import { motion, AnimatePresence } from "motion/react";
|
|
5
|
+
import { QRCodeCanvas } from "qrcode.react";
|
|
5
6
|
|
|
6
|
-
type DeployStatus =
|
|
7
|
+
type DeployStatus =
|
|
8
|
+
| "idle"
|
|
9
|
+
| "configuring"
|
|
10
|
+
| "building"
|
|
11
|
+
| "uploading"
|
|
12
|
+
| "done"
|
|
13
|
+
| "error";
|
|
14
|
+
|
|
15
|
+
interface NetlifyConfig {
|
|
16
|
+
token: string;
|
|
17
|
+
siteId: string;
|
|
18
|
+
siteUrl: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface ManifestFile {
|
|
22
|
+
path: string;
|
|
23
|
+
sha1: string;
|
|
24
|
+
size: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface DemoManifest {
|
|
28
|
+
buildTime: string;
|
|
29
|
+
fileCount: number;
|
|
30
|
+
totalSize: number;
|
|
31
|
+
files: ManifestFile[];
|
|
32
|
+
}
|
|
7
33
|
|
|
8
34
|
const STEPS = [
|
|
9
|
-
{ key: "building", label: "
|
|
10
|
-
{ key: "uploading", label: "
|
|
35
|
+
{ key: "building", label: "加载产物", icon: "system-components" },
|
|
36
|
+
{ key: "uploading", label: "上传部署", icon: "cloud-upload" },
|
|
11
37
|
{ key: "done", label: "部署完成", icon: "check-circle" },
|
|
12
38
|
];
|
|
13
39
|
|
|
40
|
+
const CONFIG_KEY = "netlify-deploy-config";
|
|
41
|
+
|
|
42
|
+
// 从 dev server 的 /demo-dist/... 路径获取文件二进制内容
|
|
43
|
+
async function fetchFileContent(filePath: string): Promise<ArrayBuffer> {
|
|
44
|
+
const res = await fetch(`/demo-dist${filePath}`);
|
|
45
|
+
if (!res.ok) throw new Error(`获取文件失败: ${filePath}`);
|
|
46
|
+
return res.arrayBuffer();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ---- 配置弹窗组件 ----
|
|
50
|
+
function ConfigDialog({
|
|
51
|
+
open,
|
|
52
|
+
onClose,
|
|
53
|
+
onSubmit,
|
|
54
|
+
loading,
|
|
55
|
+
}: {
|
|
56
|
+
open: boolean;
|
|
57
|
+
onClose: () => void;
|
|
58
|
+
onSubmit: (token: string) => void;
|
|
59
|
+
loading: boolean;
|
|
60
|
+
}) {
|
|
61
|
+
const [token, setToken] = useState("");
|
|
62
|
+
const [error, setError] = useState("");
|
|
63
|
+
|
|
64
|
+
if (!open) return null;
|
|
65
|
+
|
|
66
|
+
const handleSubmit = () => {
|
|
67
|
+
if (!token.trim()) {
|
|
68
|
+
setError("请输入 Netlify Access Token");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
setError("");
|
|
72
|
+
onSubmit(token.trim());
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
77
|
+
<div className="absolute inset-0 bg-black/60" onClick={onClose} />
|
|
78
|
+
<motion.div
|
|
79
|
+
initial={{ opacity: 0, scale: 0.95 }}
|
|
80
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
81
|
+
className="bg-white relative rounded-[32px] overflow-hidden w-[380px] p-[24px] shadow-lg"
|
|
82
|
+
>
|
|
83
|
+
<h3 className="text-[20px] font-semibold text-center mb-[6px]">
|
|
84
|
+
配置 Netlify
|
|
85
|
+
</h3>
|
|
86
|
+
<p className="text-[14px] text-muted-foreground text-center mb-[20px]">
|
|
87
|
+
首次使用需要配置,之后链接将保持不变
|
|
88
|
+
</p>
|
|
89
|
+
|
|
90
|
+
{/* 步骤说明 */}
|
|
91
|
+
<div className="bg-black/[0.02] rounded-[16px] p-[16px] mb-[16px]">
|
|
92
|
+
<p className="text-[13px] font-medium text-foreground mb-[8px]">
|
|
93
|
+
获取 Token 步骤:
|
|
94
|
+
</p>
|
|
95
|
+
<ol className="list-decimal list-inside space-y-[4px] text-[12px] text-muted-foreground">
|
|
96
|
+
<li>
|
|
97
|
+
访问{" "}
|
|
98
|
+
<a
|
|
99
|
+
href="https://app.netlify.com/user/applications#personal-access-tokens"
|
|
100
|
+
target="_blank"
|
|
101
|
+
rel="noopener noreferrer"
|
|
102
|
+
className="text-accent underline"
|
|
103
|
+
>
|
|
104
|
+
Netlify 设置页面
|
|
105
|
+
</a>
|
|
106
|
+
</li>
|
|
107
|
+
<li>点击 "New access token"</li>
|
|
108
|
+
<li>输入描述(如 "Design Preview")并创建</li>
|
|
109
|
+
<li>复制生成的 Token 粘贴到下方输入框</li>
|
|
110
|
+
</ol>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
{/* 输入框 */}
|
|
114
|
+
<input
|
|
115
|
+
type="text"
|
|
116
|
+
value={token}
|
|
117
|
+
onChange={(e) => {
|
|
118
|
+
setToken(e.target.value);
|
|
119
|
+
setError("");
|
|
120
|
+
}}
|
|
121
|
+
placeholder="粘贴你的 Netlify Access Token"
|
|
122
|
+
className="w-full h-[48px] px-[16px] rounded-[14px] bg-black/[0.04] text-[14px] text-foreground placeholder:text-muted-foreground outline-none focus:ring-2 focus:ring-accent/30 transition-shadow"
|
|
123
|
+
/>
|
|
124
|
+
{error && (
|
|
125
|
+
<p className="text-[12px] text-destructive mt-[6px]">{error}</p>
|
|
126
|
+
)}
|
|
127
|
+
|
|
128
|
+
{/* 按钮 */}
|
|
129
|
+
<div className="flex gap-[12px] mt-[20px]">
|
|
130
|
+
<button
|
|
131
|
+
onClick={onClose}
|
|
132
|
+
disabled={loading}
|
|
133
|
+
className="flex-1 h-[48px] rounded-[50px] bg-black/[0.06] text-[16px] font-medium active:bg-black/[0.12] transition-colors disabled:opacity-50"
|
|
134
|
+
>
|
|
135
|
+
取消
|
|
136
|
+
</button>
|
|
137
|
+
<button
|
|
138
|
+
onClick={handleSubmit}
|
|
139
|
+
disabled={loading}
|
|
140
|
+
className="flex-1 h-[48px] rounded-[50px] bg-primary text-white text-[16px] font-medium active:bg-primary/80 transition-colors disabled:opacity-50 flex items-center justify-center gap-[6px]"
|
|
141
|
+
>
|
|
142
|
+
{loading ? (
|
|
143
|
+
<>
|
|
144
|
+
<div className="size-[16px] rounded-full border-2 border-white/30 border-t-white animate-spin" />
|
|
145
|
+
验证中...
|
|
146
|
+
</>
|
|
147
|
+
) : (
|
|
148
|
+
"确认配置"
|
|
149
|
+
)}
|
|
150
|
+
</button>
|
|
151
|
+
</div>
|
|
152
|
+
</motion.div>
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ---- 主页面 ----
|
|
14
158
|
export function DeployPage() {
|
|
15
159
|
const navigate = useNavigate();
|
|
16
160
|
const [status, setStatus] = useState<DeployStatus>("idle");
|
|
17
161
|
const [progress, setProgress] = useState(0);
|
|
162
|
+
const [config, setConfig] = useState<NetlifyConfig | null>(null);
|
|
163
|
+
const [showConfigDialog, setShowConfigDialog] = useState(false);
|
|
164
|
+
const [configLoading, setConfigLoading] = useState(false);
|
|
165
|
+
const [deployError, setDeployError] = useState("");
|
|
166
|
+
const [toastMsg, setToastMsg] = useState("");
|
|
167
|
+
const [statusText, setStatusText] = useState("");
|
|
168
|
+
const [manifestReady, setManifestReady] = useState<boolean | null>(null);
|
|
169
|
+
const toastTimer = useRef<ReturnType<typeof setTimeout>>();
|
|
18
170
|
|
|
19
|
-
|
|
171
|
+
// 加载已保存的配置
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
const saved = localStorage.getItem(CONFIG_KEY);
|
|
174
|
+
if (saved) {
|
|
175
|
+
try {
|
|
176
|
+
setConfig(JSON.parse(saved));
|
|
177
|
+
} catch {
|
|
178
|
+
/* ignore */
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// 检查 manifest 是否可用
|
|
182
|
+
fetch("/demo-manifest.json", { method: "HEAD" })
|
|
183
|
+
.then((res) => setManifestReady(res.ok))
|
|
184
|
+
.catch(() => setManifestReady(false));
|
|
185
|
+
}, []);
|
|
186
|
+
|
|
187
|
+
const showToast = (msg: string) => {
|
|
188
|
+
setToastMsg(msg);
|
|
189
|
+
clearTimeout(toastTimer.current);
|
|
190
|
+
toastTimer.current = setTimeout(() => setToastMsg(""), 2500);
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// 保存配置
|
|
194
|
+
const saveConfig = (c: NetlifyConfig) => {
|
|
195
|
+
localStorage.setItem(CONFIG_KEY, JSON.stringify(c));
|
|
196
|
+
setConfig(c);
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// 验证 Token 并创建站点
|
|
200
|
+
const handleConfigSubmit = async (token: string) => {
|
|
201
|
+
setConfigLoading(true);
|
|
202
|
+
try {
|
|
203
|
+
// 1. 验证 Token
|
|
204
|
+
const userRes = await fetch("https://api.netlify.com/api/v1/user", {
|
|
205
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
206
|
+
});
|
|
207
|
+
if (!userRes.ok) throw new Error("Token 无效,请检查后重试");
|
|
208
|
+
|
|
209
|
+
// 2. 检查是否已有站点(复用之前的站点)
|
|
210
|
+
const sitesRes = await fetch(
|
|
211
|
+
"https://api.netlify.com/api/v1/sites?filter=owner",
|
|
212
|
+
{ headers: { Authorization: `Bearer ${token}` } }
|
|
213
|
+
);
|
|
214
|
+
const sites = await sitesRes.json();
|
|
215
|
+
const existingSite = sites.find(
|
|
216
|
+
(s: { name: string }) =>
|
|
217
|
+
s.name && s.name.startsWith("design-preview-")
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
let site;
|
|
221
|
+
if (existingSite) {
|
|
222
|
+
site = existingSite;
|
|
223
|
+
} else {
|
|
224
|
+
// 3. 创建站点
|
|
225
|
+
const siteRes = await fetch("https://api.netlify.com/api/v1/sites", {
|
|
226
|
+
method: "POST",
|
|
227
|
+
headers: {
|
|
228
|
+
Authorization: `Bearer ${token}`,
|
|
229
|
+
"Content-Type": "application/json",
|
|
230
|
+
},
|
|
231
|
+
body: JSON.stringify({ name: `design-preview-${Date.now()}` }),
|
|
232
|
+
});
|
|
233
|
+
if (!siteRes.ok) throw new Error("创建站点失败,请重试");
|
|
234
|
+
site = await siteRes.json();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const newConfig: NetlifyConfig = {
|
|
238
|
+
token,
|
|
239
|
+
siteId: site.id,
|
|
240
|
+
siteUrl: site.ssl_url || site.url,
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
saveConfig(newConfig);
|
|
244
|
+
setShowConfigDialog(false);
|
|
245
|
+
setConfigLoading(false);
|
|
246
|
+
showToast("配置成功,开始部署...");
|
|
247
|
+
|
|
248
|
+
// 自动开始部署
|
|
249
|
+
setTimeout(() => startDeploy(newConfig), 600);
|
|
250
|
+
} catch (err) {
|
|
251
|
+
setConfigLoading(false);
|
|
252
|
+
showToast(err instanceof Error ? err.message : "配置失败");
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
// 执行部署
|
|
257
|
+
const startDeploy = async (useConfig?: NetlifyConfig) => {
|
|
258
|
+
const cfg = useConfig || config;
|
|
259
|
+
if (!cfg) {
|
|
260
|
+
setShowConfigDialog(true);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
setDeployError("");
|
|
20
265
|
setStatus("building");
|
|
21
266
|
setProgress(0);
|
|
267
|
+
setStatusText("加载构建产物...");
|
|
22
268
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
269
|
+
try {
|
|
270
|
+
// 步骤1: 加载 manifest
|
|
271
|
+
const manifestRes = await fetch("/demo-manifest.json");
|
|
272
|
+
if (!manifestRes.ok) {
|
|
273
|
+
throw new Error(
|
|
274
|
+
"构建产物不存在。请先在终端运行 npm run build:demo 构建 Demo 页面。"
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
const manifest: DemoManifest = await manifestRes.json();
|
|
278
|
+
setProgress(15);
|
|
279
|
+
setStatusText(
|
|
280
|
+
`已加载 ${manifest.fileCount} 个文件 (${(manifest.totalSize / 1024 / 1024).toFixed(1)} MB)`
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
// 步骤2: 创建 Netlify deploy(文件哈希清单)
|
|
284
|
+
setStatus("uploading");
|
|
285
|
+
setProgress(20);
|
|
286
|
+
setStatusText("创建部署...");
|
|
287
|
+
|
|
288
|
+
const fileDigests: Record<string, string> = {};
|
|
289
|
+
for (const f of manifest.files) {
|
|
290
|
+
fileDigests[f.path] = f.sha1;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// 同时添加 SPA 重定向规则
|
|
294
|
+
const redirectsContent = "/* /index.html 200\n";
|
|
295
|
+
const redirectsHash = await sha1String(redirectsContent);
|
|
296
|
+
fileDigests["/_redirects"] = redirectsHash;
|
|
297
|
+
|
|
298
|
+
const deployRes = await fetch(
|
|
299
|
+
`https://api.netlify.com/api/v1/sites/${cfg.siteId}/deploys`,
|
|
300
|
+
{
|
|
301
|
+
method: "POST",
|
|
302
|
+
headers: {
|
|
303
|
+
Authorization: `Bearer ${cfg.token}`,
|
|
304
|
+
"Content-Type": "application/json",
|
|
305
|
+
},
|
|
306
|
+
body: JSON.stringify({ files: fileDigests }),
|
|
307
|
+
}
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
if (!deployRes.ok) {
|
|
311
|
+
const errText = await deployRes.text();
|
|
312
|
+
throw new Error(`部署请求失败: ${errText}`);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const deploy = await deployRes.json();
|
|
316
|
+
const requiredHashes = new Set<string>(deploy.required || []);
|
|
317
|
+
setProgress(30);
|
|
318
|
+
|
|
319
|
+
// 步骤3: 上传必需的文件
|
|
320
|
+
const filesToUpload = manifest.files.filter((f) =>
|
|
321
|
+
requiredHashes.has(f.sha1)
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
// 检查 _redirects 是否需要上传
|
|
325
|
+
const needRedirects = requiredHashes.has(redirectsHash);
|
|
28
326
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
327
|
+
const totalUploads = filesToUpload.length + (needRedirects ? 1 : 0);
|
|
328
|
+
let uploaded = 0;
|
|
329
|
+
|
|
330
|
+
setStatusText(
|
|
331
|
+
totalUploads > 0
|
|
332
|
+
? `上传文件 0/${totalUploads}...`
|
|
333
|
+
: "所有文件已在服务器上,跳过上传..."
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
// 上传 _redirects
|
|
337
|
+
if (needRedirects) {
|
|
338
|
+
const encoder = new TextEncoder();
|
|
339
|
+
await fetch(
|
|
340
|
+
`https://api.netlify.com/api/v1/deploys/${deploy.id}/files/_redirects`,
|
|
341
|
+
{
|
|
342
|
+
method: "PUT",
|
|
343
|
+
headers: {
|
|
344
|
+
Authorization: `Bearer ${cfg.token}`,
|
|
345
|
+
"Content-Type": "application/octet-stream",
|
|
346
|
+
},
|
|
347
|
+
body: encoder.encode(redirectsContent),
|
|
348
|
+
}
|
|
349
|
+
);
|
|
350
|
+
uploaded++;
|
|
351
|
+
setProgress(30 + Math.round((uploaded / totalUploads) * 55));
|
|
352
|
+
setStatusText(`上传文件 ${uploaded}/${totalUploads}...`);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// 逐个上传文件(从 dev server 按需获取内容)
|
|
356
|
+
for (const file of filesToUpload) {
|
|
357
|
+
const body = await fetchFileContent(file.path);
|
|
358
|
+
const uploadRes = await fetch(
|
|
359
|
+
`https://api.netlify.com/api/v1/deploys/${deploy.id}/files${file.path}`,
|
|
360
|
+
{
|
|
361
|
+
method: "PUT",
|
|
362
|
+
headers: {
|
|
363
|
+
Authorization: `Bearer ${cfg.token}`,
|
|
364
|
+
"Content-Type": "application/octet-stream",
|
|
365
|
+
},
|
|
366
|
+
body,
|
|
367
|
+
}
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
if (!uploadRes.ok) {
|
|
371
|
+
console.warn(`上传 ${file.path} 失败: ${uploadRes.status}`);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
uploaded++;
|
|
375
|
+
setProgress(30 + Math.round((uploaded / totalUploads) * 55));
|
|
376
|
+
setStatusText(`上传文件 ${uploaded}/${totalUploads}...`);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
setProgress(88);
|
|
380
|
+
setStatusText("等待部署生效...");
|
|
381
|
+
|
|
382
|
+
// 步骤4: 轮询部署状态
|
|
383
|
+
let retries = 0;
|
|
384
|
+
while (retries < 60) {
|
|
385
|
+
const statusRes = await fetch(
|
|
386
|
+
`https://api.netlify.com/api/v1/deploys/${deploy.id}`,
|
|
387
|
+
{ headers: { Authorization: `Bearer ${cfg.token}` } }
|
|
388
|
+
);
|
|
389
|
+
const statusData = await statusRes.json();
|
|
390
|
+
|
|
391
|
+
if (statusData.state === "ready") {
|
|
392
|
+
if (statusData.ssl_url) {
|
|
393
|
+
const updated = { ...cfg, siteUrl: statusData.ssl_url };
|
|
394
|
+
saveConfig(updated);
|
|
395
|
+
}
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
if (statusData.state === "error") {
|
|
399
|
+
throw new Error(
|
|
400
|
+
"Netlify 部署失败:" +
|
|
401
|
+
(statusData.error_message || "未知错误")
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
retries++;
|
|
406
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
407
|
+
setProgress(Math.min(88 + retries, 98));
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (retries >= 60) throw new Error("部署超时,请重试");
|
|
411
|
+
|
|
412
|
+
setProgress(100);
|
|
413
|
+
setStatus("done");
|
|
414
|
+
setStatusText("");
|
|
415
|
+
} catch (err) {
|
|
416
|
+
setDeployError(err instanceof Error ? err.message : "部署失败");
|
|
417
|
+
setStatus("error");
|
|
418
|
+
setStatusText("");
|
|
34
419
|
}
|
|
420
|
+
};
|
|
35
421
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
422
|
+
// 重置配置
|
|
423
|
+
const handleResetConfig = () => {
|
|
424
|
+
localStorage.removeItem(CONFIG_KEY);
|
|
425
|
+
setConfig(null);
|
|
426
|
+
showToast("已重置配置");
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
// 复制链接
|
|
430
|
+
const handleCopyLink = async () => {
|
|
431
|
+
if (config?.siteUrl) {
|
|
432
|
+
try {
|
|
433
|
+
await navigator.clipboard.writeText(config.siteUrl);
|
|
434
|
+
showToast("链接已复制");
|
|
435
|
+
} catch {
|
|
436
|
+
showToast("复制失败,请手动复制");
|
|
437
|
+
}
|
|
438
|
+
}
|
|
39
439
|
};
|
|
40
440
|
|
|
41
441
|
const isDeploying = status === "building" || status === "uploading";
|
|
@@ -52,10 +452,12 @@ export function DeployPage() {
|
|
|
52
452
|
>
|
|
53
453
|
<IconFont name="chevron-left" size="20px" />
|
|
54
454
|
</button>
|
|
55
|
-
<h1 className="text-[24px] font-bold text-foreground"
|
|
455
|
+
<h1 className="text-[24px] font-bold text-foreground">
|
|
456
|
+
部署预览
|
|
457
|
+
</h1>
|
|
56
458
|
</div>
|
|
57
459
|
<p className="text-[14px] text-muted-foreground ml-[48px]">
|
|
58
|
-
|
|
460
|
+
将完整的 Demo 页面部署到线上,手机扫码即可体验
|
|
59
461
|
</p>
|
|
60
462
|
</div>
|
|
61
463
|
</div>
|
|
@@ -74,7 +476,7 @@ export function DeployPage() {
|
|
|
74
476
|
size="16px"
|
|
75
477
|
className="text-[#22C55E] shrink-0 mt-[1px]"
|
|
76
478
|
/>
|
|
77
|
-
|
|
479
|
+
部署完整的 Demo 演示页面(首页/发现/行程/我的),含地图、动效和真实交互
|
|
78
480
|
</li>
|
|
79
481
|
<li className="flex items-start gap-[8px] text-[13px] text-muted-foreground">
|
|
80
482
|
<IconFont
|
|
@@ -93,11 +495,61 @@ export function DeployPage() {
|
|
|
93
495
|
每次部署会覆盖上一次的内容,链接保持不变
|
|
94
496
|
</li>
|
|
95
497
|
</ul>
|
|
498
|
+
|
|
499
|
+
{/* 构建产物状态 */}
|
|
500
|
+
<div className="mt-[16px] pt-[16px] border-t border-black/[0.06]">
|
|
501
|
+
<div className="flex items-center gap-[8px]">
|
|
502
|
+
<div
|
|
503
|
+
className={cn(
|
|
504
|
+
"size-[8px] rounded-full",
|
|
505
|
+
manifestReady === null
|
|
506
|
+
? "bg-muted-foreground animate-pulse"
|
|
507
|
+
: manifestReady
|
|
508
|
+
? "bg-[#22C55E]"
|
|
509
|
+
: "bg-[#F59E0B]"
|
|
510
|
+
)}
|
|
511
|
+
/>
|
|
512
|
+
<span className="text-[12px] text-muted-foreground">
|
|
513
|
+
{manifestReady === null
|
|
514
|
+
? "检查构建产物..."
|
|
515
|
+
: manifestReady
|
|
516
|
+
? "构建产物已就绪,可以部署"
|
|
517
|
+
: "构建产物不存在,请先执行构建 (npm run build:demo)"}
|
|
518
|
+
</span>
|
|
519
|
+
</div>
|
|
520
|
+
</div>
|
|
521
|
+
|
|
522
|
+
{/* 已配置状态 */}
|
|
523
|
+
{config && (
|
|
524
|
+
<div className="mt-[16px] pt-[16px] border-t border-black/[0.06]">
|
|
525
|
+
<div className="flex items-center justify-between flex-wrap gap-[8px]">
|
|
526
|
+
<div className="flex items-center gap-[8px] min-w-0">
|
|
527
|
+
<IconFont
|
|
528
|
+
name="link"
|
|
529
|
+
size="14px"
|
|
530
|
+
className="text-accent shrink-0"
|
|
531
|
+
/>
|
|
532
|
+
<span className="text-[12px] text-muted-foreground shrink-0">
|
|
533
|
+
已配置 Netlify:
|
|
534
|
+
</span>
|
|
535
|
+
<span className="text-[12px] text-accent font-mono truncate">
|
|
536
|
+
{config.siteUrl}
|
|
537
|
+
</span>
|
|
538
|
+
</div>
|
|
539
|
+
<button
|
|
540
|
+
onClick={handleResetConfig}
|
|
541
|
+
className="text-[12px] text-muted-foreground hover:text-destructive transition-colors shrink-0"
|
|
542
|
+
>
|
|
543
|
+
重置配置
|
|
544
|
+
</button>
|
|
545
|
+
</div>
|
|
546
|
+
</div>
|
|
547
|
+
)}
|
|
96
548
|
</div>
|
|
97
549
|
|
|
98
550
|
{/* 部署进度 */}
|
|
99
551
|
<AnimatePresence mode="wait">
|
|
100
|
-
{status === "idle" && (
|
|
552
|
+
{(status === "idle" || status === "configuring") && (
|
|
101
553
|
<motion.div
|
|
102
554
|
key="idle"
|
|
103
555
|
initial={{ opacity: 0 }}
|
|
@@ -113,17 +565,24 @@ export function DeployPage() {
|
|
|
113
565
|
/>
|
|
114
566
|
</div>
|
|
115
567
|
<h3 className="text-[18px] font-medium text-foreground mb-[8px]">
|
|
116
|
-
准备就绪
|
|
568
|
+
{config ? "准备就绪" : "需要配置"}
|
|
117
569
|
</h3>
|
|
118
570
|
<p className="text-[14px] text-muted-foreground mb-[32px]">
|
|
119
|
-
|
|
571
|
+
{config
|
|
572
|
+
? "点击下方按钮开始部署完整 Demo"
|
|
573
|
+
: "首次使用需要配置 Netlify,只需一次"}
|
|
120
574
|
</p>
|
|
121
575
|
<Btn
|
|
122
576
|
variant="primary"
|
|
123
577
|
size="large"
|
|
124
|
-
label="开始部署"
|
|
125
|
-
icon={
|
|
126
|
-
|
|
578
|
+
label={config ? "开始部署" : "配置并部署"}
|
|
579
|
+
icon={
|
|
580
|
+
<IconFont
|
|
581
|
+
name={config ? "cloud-upload" : "setting"}
|
|
582
|
+
size="18px"
|
|
583
|
+
/>
|
|
584
|
+
}
|
|
585
|
+
onClick={() => startDeploy()}
|
|
127
586
|
/>
|
|
128
587
|
</motion.div>
|
|
129
588
|
)}
|
|
@@ -208,14 +667,14 @@ export function DeployPage() {
|
|
|
208
667
|
/>
|
|
209
668
|
</div>
|
|
210
669
|
<p className="text-center text-[13px] text-muted-foreground mt-[12px]">
|
|
211
|
-
{status === "building" ? "
|
|
670
|
+
{statusText || (status === "building" ? "加载产物..." : "上传中...")}{" "}
|
|
212
671
|
{progress}%
|
|
213
672
|
</p>
|
|
214
673
|
</div>
|
|
215
674
|
</motion.div>
|
|
216
675
|
)}
|
|
217
676
|
|
|
218
|
-
{status === "done" && (
|
|
677
|
+
{status === "done" && config && (
|
|
219
678
|
<motion.div
|
|
220
679
|
key="done"
|
|
221
680
|
initial={{ opacity: 0, scale: 0.95 }}
|
|
@@ -233,35 +692,44 @@ export function DeployPage() {
|
|
|
233
692
|
部署成功
|
|
234
693
|
</h3>
|
|
235
694
|
<p className="text-[14px] text-muted-foreground mb-[24px]">
|
|
236
|
-
|
|
695
|
+
手机扫描下方二维码即可预览完整 Demo
|
|
237
696
|
</p>
|
|
238
697
|
|
|
239
|
-
{/*
|
|
698
|
+
{/* 真实二维码 */}
|
|
240
699
|
<div className="inline-block bg-white p-[16px] rounded-[20px] shadow-lg mb-[24px]">
|
|
241
|
-
<
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
<p className="text-[11px] text-muted-foreground">
|
|
249
|
-
二维码预览
|
|
250
|
-
</p>
|
|
251
|
-
</div>
|
|
252
|
-
</div>
|
|
700
|
+
<QRCodeCanvas
|
|
701
|
+
value={config.siteUrl}
|
|
702
|
+
size={180}
|
|
703
|
+
level="M"
|
|
704
|
+
bgColor="#ffffff"
|
|
705
|
+
fgColor="#000000"
|
|
706
|
+
/>
|
|
253
707
|
</div>
|
|
254
708
|
|
|
255
|
-
{/*
|
|
256
|
-
<div
|
|
709
|
+
{/* 可点击链接 */}
|
|
710
|
+
<div
|
|
711
|
+
className="bg-black/[0.02] rounded-[12px] p-[12px] max-w-[400px] mx-auto mb-[8px] cursor-pointer hover:bg-black/[0.04] transition-colors"
|
|
712
|
+
onClick={handleCopyLink}
|
|
713
|
+
>
|
|
257
714
|
<p className="text-[12px] text-muted-foreground mb-[4px]">
|
|
258
|
-
|
|
715
|
+
预览链接(点击复制)
|
|
259
716
|
</p>
|
|
260
717
|
<p className="text-[13px] text-accent font-mono break-all">
|
|
261
|
-
|
|
718
|
+
{config.siteUrl}
|
|
262
719
|
</p>
|
|
263
720
|
</div>
|
|
264
721
|
|
|
722
|
+
{/* 在浏览器中打开 */}
|
|
723
|
+
<a
|
|
724
|
+
href={config.siteUrl}
|
|
725
|
+
target="_blank"
|
|
726
|
+
rel="noopener noreferrer"
|
|
727
|
+
className="inline-flex items-center gap-[4px] text-[12px] text-accent mb-[24px] hover:underline"
|
|
728
|
+
>
|
|
729
|
+
在浏览器中打开
|
|
730
|
+
<IconFont name="jump" size="12px" />
|
|
731
|
+
</a>
|
|
732
|
+
|
|
265
733
|
<div className="flex items-center justify-center gap-[12px]">
|
|
266
734
|
<Btn
|
|
267
735
|
variant="ghost"
|
|
@@ -283,8 +751,79 @@ export function DeployPage() {
|
|
|
283
751
|
</div>
|
|
284
752
|
</motion.div>
|
|
285
753
|
)}
|
|
754
|
+
|
|
755
|
+
{status === "error" && (
|
|
756
|
+
<motion.div
|
|
757
|
+
key="error"
|
|
758
|
+
initial={{ opacity: 0 }}
|
|
759
|
+
animate={{ opacity: 1 }}
|
|
760
|
+
className="text-center py-[48px]"
|
|
761
|
+
>
|
|
762
|
+
<div className="size-[64px] rounded-full bg-destructive/10 flex items-center justify-center mx-auto mb-[16px]">
|
|
763
|
+
<IconFont
|
|
764
|
+
name="close"
|
|
765
|
+
size="32px"
|
|
766
|
+
className="text-destructive"
|
|
767
|
+
/>
|
|
768
|
+
</div>
|
|
769
|
+
<h3 className="text-[20px] font-semibold text-foreground mb-[8px]">
|
|
770
|
+
部署失败
|
|
771
|
+
</h3>
|
|
772
|
+
<p className="text-[14px] text-destructive mb-[24px] max-w-[400px] mx-auto">
|
|
773
|
+
{deployError || "未知错误,请重试"}
|
|
774
|
+
</p>
|
|
775
|
+
<Btn
|
|
776
|
+
variant="primary"
|
|
777
|
+
size="middle"
|
|
778
|
+
label="重试"
|
|
779
|
+
icon={<IconFont name="refresh" size="16px" />}
|
|
780
|
+
onClick={() => {
|
|
781
|
+
setStatus("idle");
|
|
782
|
+
setProgress(0);
|
|
783
|
+
setDeployError("");
|
|
784
|
+
}}
|
|
785
|
+
/>
|
|
786
|
+
</motion.div>
|
|
787
|
+
)}
|
|
286
788
|
</AnimatePresence>
|
|
287
789
|
</div>
|
|
790
|
+
|
|
791
|
+
{/* 配置弹窗 */}
|
|
792
|
+
<ConfigDialog
|
|
793
|
+
open={showConfigDialog}
|
|
794
|
+
onClose={() => setShowConfigDialog(false)}
|
|
795
|
+
onSubmit={handleConfigSubmit}
|
|
796
|
+
loading={configLoading}
|
|
797
|
+
/>
|
|
798
|
+
|
|
799
|
+
{/* Toast */}
|
|
800
|
+
<AnimatePresence>
|
|
801
|
+
{toastMsg && (
|
|
802
|
+
<motion.div
|
|
803
|
+
initial={{ opacity: 0, y: 20 }}
|
|
804
|
+
animate={{ opacity: 1, y: 0 }}
|
|
805
|
+
exit={{ opacity: 0, y: 20 }}
|
|
806
|
+
className="fixed bottom-[32px] left-1/2 -translate-x-1/2 z-[2000]"
|
|
807
|
+
>
|
|
808
|
+
<Toast
|
|
809
|
+
lines={[toastMsg]}
|
|
810
|
+
showIcon={true}
|
|
811
|
+
showClose={false}
|
|
812
|
+
/>
|
|
813
|
+
</motion.div>
|
|
814
|
+
)}
|
|
815
|
+
</AnimatePresence>
|
|
288
816
|
</div>
|
|
289
817
|
);
|
|
290
818
|
}
|
|
819
|
+
|
|
820
|
+
// ---- 工具函数 ----
|
|
821
|
+
|
|
822
|
+
// SHA-1 哈希(字符串版本,用于 _redirects 等文本文件)
|
|
823
|
+
async function sha1String(content: string): Promise<string> {
|
|
824
|
+
const encoder = new TextEncoder();
|
|
825
|
+
const data = encoder.encode(content);
|
|
826
|
+
const hashBuffer = await crypto.subtle.digest("SHA-1", data);
|
|
827
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
828
|
+
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
829
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useNavigate } from "react-router-dom";
|
|
2
|
-
import {
|
|
2
|
+
import { IconFont } from "@lmy54321/design-system";
|
|
3
3
|
import { motion } from "motion/react";
|
|
4
4
|
import designSystemPkg from "@lmy54321/design-system/package.json";
|
|
5
5
|
|
|
@@ -9,7 +9,7 @@ interface PortalCard {
|
|
|
9
9
|
description: string;
|
|
10
10
|
icon: string;
|
|
11
11
|
route: string;
|
|
12
|
-
|
|
12
|
+
iconColor: string;
|
|
13
13
|
badge?: string;
|
|
14
14
|
}
|
|
15
15
|
|
|
@@ -20,15 +20,15 @@ const PORTAL_CARDS: PortalCard[] = [
|
|
|
20
20
|
description: "查看当前项目的业务 Demo,这也是手机预览看到的内容",
|
|
21
21
|
icon: "mobile",
|
|
22
22
|
route: "/demo",
|
|
23
|
-
|
|
23
|
+
iconColor: "text-[#22C55E]",
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
26
|
id: "components",
|
|
27
27
|
title: "组件库",
|
|
28
28
|
description: "浏览所有可用组件的效果和配置项,开发时随时查阅",
|
|
29
|
-
icon: "
|
|
29
|
+
icon: "system-components",
|
|
30
30
|
route: "/components",
|
|
31
|
-
|
|
31
|
+
iconColor: "text-[#367BF6]",
|
|
32
32
|
},
|
|
33
33
|
{
|
|
34
34
|
id: "sync",
|
|
@@ -36,7 +36,7 @@ const PORTAL_CARDS: PortalCard[] = [
|
|
|
36
36
|
description: "检查设计规范是否有更新,可视化对比后选择性合并",
|
|
37
37
|
icon: "refresh",
|
|
38
38
|
route: "/sync",
|
|
39
|
-
|
|
39
|
+
iconColor: "text-[#FFB800]",
|
|
40
40
|
badge: "有更新",
|
|
41
41
|
},
|
|
42
42
|
{
|
|
@@ -45,7 +45,7 @@ const PORTAL_CARDS: PortalCard[] = [
|
|
|
45
45
|
description: "一键打包并部署到手机端,扫码即可预览效果",
|
|
46
46
|
icon: "cloud-upload",
|
|
47
47
|
route: "/deploy",
|
|
48
|
-
|
|
48
|
+
iconColor: "text-[#FF293B]",
|
|
49
49
|
},
|
|
50
50
|
];
|
|
51
51
|
|
|
@@ -94,13 +94,8 @@ export function DevPortal() {
|
|
|
94
94
|
)}
|
|
95
95
|
|
|
96
96
|
{/* Icon */}
|
|
97
|
-
<div
|
|
98
|
-
className={
|
|
99
|
-
"size-[48px] rounded-[16px] flex items-center justify-center mb-[16px]",
|
|
100
|
-
card.colorClass
|
|
101
|
-
)}
|
|
102
|
-
>
|
|
103
|
-
<IconFont name={card.icon} size="24px" className="text-white" />
|
|
97
|
+
<div className="size-[48px] rounded-[16px] bg-black/[0.04] flex items-center justify-center mb-[16px]">
|
|
98
|
+
<IconFont name={card.icon} size="24px" className={card.iconColor} />
|
|
104
99
|
</div>
|
|
105
100
|
|
|
106
101
|
{/* Content */}
|
package/template/vite.config.ts
CHANGED
|
@@ -2,9 +2,47 @@ import { defineConfig } from "vite";
|
|
|
2
2
|
import react from "@vitejs/plugin-react";
|
|
3
3
|
import tailwindcss from "@tailwindcss/vite";
|
|
4
4
|
import path from "path";
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
|
|
7
|
+
// 自定义插件:让 dev server 能提供 demo-dist/ 目录下的文件
|
|
8
|
+
function serveDemoDist() {
|
|
9
|
+
return {
|
|
10
|
+
name: "serve-demo-dist",
|
|
11
|
+
configureServer(server: { middlewares: { use: (fn: (req: { url?: string }, res: { setHeader: (k: string, v: string) => void; end: (data: Buffer) => void }, next: () => void) => void) => void } }) {
|
|
12
|
+
server.middlewares.use((req, res, next) => {
|
|
13
|
+
if (req.url && req.url.startsWith("/demo-dist/")) {
|
|
14
|
+
const filePath = path.resolve(__dirname, "." + req.url);
|
|
15
|
+
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
|
|
16
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
17
|
+
const mimeTypes: Record<string, string> = {
|
|
18
|
+
".html": "text/html",
|
|
19
|
+
".css": "text/css",
|
|
20
|
+
".js": "application/javascript",
|
|
21
|
+
".json": "application/json",
|
|
22
|
+
".png": "image/png",
|
|
23
|
+
".jpg": "image/jpeg",
|
|
24
|
+
".jpeg": "image/jpeg",
|
|
25
|
+
".svg": "image/svg+xml",
|
|
26
|
+
".webp": "image/webp",
|
|
27
|
+
".woff2": "font/woff2",
|
|
28
|
+
".woff": "font/woff",
|
|
29
|
+
};
|
|
30
|
+
res.setHeader(
|
|
31
|
+
"Content-Type",
|
|
32
|
+
mimeTypes[ext] || "application/octet-stream"
|
|
33
|
+
);
|
|
34
|
+
res.end(fs.readFileSync(filePath));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
next();
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
5
43
|
|
|
6
44
|
export default defineConfig({
|
|
7
|
-
plugins: [react(), tailwindcss()],
|
|
45
|
+
plugins: [react(), tailwindcss(), serveDemoDist()],
|
|
8
46
|
resolve: {
|
|
9
47
|
alias: {
|
|
10
48
|
"@": path.resolve(__dirname, "./src"),
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { defineConfig } from "vite";
|
|
2
|
+
import react from "@vitejs/plugin-react";
|
|
3
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
4
|
+
import path from "path";
|
|
5
|
+
|
|
6
|
+
// Demo-only 构建配置:只构建手机预览页面
|
|
7
|
+
export default defineConfig({
|
|
8
|
+
plugins: [react(), tailwindcss()],
|
|
9
|
+
resolve: {
|
|
10
|
+
alias: {
|
|
11
|
+
"@": path.resolve(__dirname, "./src"),
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
build: {
|
|
15
|
+
outDir: "demo-dist",
|
|
16
|
+
emptyOutDir: true,
|
|
17
|
+
rollupOptions: {
|
|
18
|
+
input: path.resolve(__dirname, "demo.html"),
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
});
|