@lmy54321/design-system 1.1.4 → 1.2.0
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/init-project.mdc +75 -40
- package/package.json +2 -1
- package/template/index.html +18 -0
- package/template/public/manifest.json +9 -0
- package/template/src/App.tsx +55 -0
- package/template/src/index.css +4 -0
- package/template/src/main.tsx +28 -0
- package/template/src/pages/CommuteFoodPage.tsx +547 -0
- package/template/src/pages/ComponentLibrary.tsx +1049 -0
- package/template/src/pages/DeployPage.tsx +290 -0
- package/template/src/pages/DevPortal.tsx +161 -0
- package/template/src/pages/ExplorePage.tsx +1013 -0
- package/template/src/pages/MapHome.tsx +388 -0
- package/template/src/pages/MapSearchPage.tsx +529 -0
- package/template/src/pages/MePage.tsx +196 -0
- package/template/src/pages/POIRecommendPage.tsx +676 -0
- package/template/src/pages/PlanDetailPage.tsx +345 -0
- package/template/src/pages/PlanPage.tsx +602 -0
- package/template/src/pages/RouteDetailPage.tsx +276 -0
- package/template/src/pages/SearchFlowDemo.tsx +954 -0
- package/template/src/pages/SyncTool.tsx +688 -0
- package/template/src/pages/TeamTripPage.tsx +697 -0
- package/template/src/pages/TemplateDetailPage.tsx +287 -0
- package/template/tsconfig.json +23 -0
- package/template/vite.config.ts +18 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import React, { useState, useCallback, useEffect } from "react";
|
|
2
|
+
import { motion, AnimatePresence } from "motion/react";
|
|
3
|
+
import { cn } from "@lmy54321/design-system";
|
|
4
|
+
import { Btn } from "@lmy54321/design-system";
|
|
5
|
+
import { Tag } from "@lmy54321/design-system";
|
|
6
|
+
import { IconFont } from "@lmy54321/design-system";
|
|
7
|
+
import { Toast } from "@lmy54321/design-system";
|
|
8
|
+
import { DraggablePanel, DRAWER_STATES } from "@lmy54321/design-system";
|
|
9
|
+
import { TencentMap } from "@lmy54321/design-system";
|
|
10
|
+
|
|
11
|
+
export interface TemplateData {
|
|
12
|
+
icon: string;
|
|
13
|
+
label: string;
|
|
14
|
+
desc: string;
|
|
15
|
+
color: string;
|
|
16
|
+
fullDesc: string;
|
|
17
|
+
duration: string;
|
|
18
|
+
distance: string;
|
|
19
|
+
difficulty: string;
|
|
20
|
+
stops: { name: string; icon: string; dur: string; desc: string }[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface Props {
|
|
24
|
+
template: TemplateData;
|
|
25
|
+
onBack: () => void;
|
|
26
|
+
onCreatePlan: (plan: {
|
|
27
|
+
title: string;
|
|
28
|
+
spots: number;
|
|
29
|
+
image: string;
|
|
30
|
+
stops: { name: string; icon: string; dur: string }[];
|
|
31
|
+
}) => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function TemplateDetailPage({ template, onBack, onCreatePlan }: Props) {
|
|
35
|
+
const [panelState, setPanelState] = useState(DRAWER_STATES.MEDIUM);
|
|
36
|
+
const [toast, setToast] = useState<string | null>(null);
|
|
37
|
+
const [creating, setCreating] = useState(false);
|
|
38
|
+
|
|
39
|
+
const showToast = useCallback((t: string) => setToast(t), []);
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (!toast) return;
|
|
42
|
+
const id = setTimeout(() => setToast(null), 2200);
|
|
43
|
+
return () => clearTimeout(id);
|
|
44
|
+
}, [toast]);
|
|
45
|
+
|
|
46
|
+
const handleCreate = useCallback(() => {
|
|
47
|
+
setCreating(true);
|
|
48
|
+
setTimeout(() => {
|
|
49
|
+
onCreatePlan({
|
|
50
|
+
title: template.label,
|
|
51
|
+
spots: template.stops.length,
|
|
52
|
+
image: "/images/scene/trip-culture.png",
|
|
53
|
+
stops: template.stops,
|
|
54
|
+
});
|
|
55
|
+
}, 800);
|
|
56
|
+
}, [template, onCreatePlan]);
|
|
57
|
+
|
|
58
|
+
/* 计算总时长(小时) */
|
|
59
|
+
const totalHours = template.stops.reduce((sum, s) => {
|
|
60
|
+
const match = s.dur.match(/(\d+(\.\d+)?)/);
|
|
61
|
+
return sum + (match ? parseFloat(match[1]) : 0);
|
|
62
|
+
}, 0);
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<div
|
|
66
|
+
className="relative w-full h-[100dvh] overflow-hidden bg-background"
|
|
67
|
+
style={{ fontFamily: "var(--font-family-sans)" }}
|
|
68
|
+
>
|
|
69
|
+
{/* 地图背景 */}
|
|
70
|
+
<TencentMap className="absolute inset-0 w-full h-full" center={{ lat: 39.909, lng: 116.397 }} />
|
|
71
|
+
|
|
72
|
+
{/* 地图上的站点标记 */}
|
|
73
|
+
<div className="absolute inset-0 pointer-events-none z-[5]">
|
|
74
|
+
{template.stops.map((stop, i) => {
|
|
75
|
+
const angle = (i / template.stops.length) * Math.PI * 1.2 + 0.3;
|
|
76
|
+
const r = 15 + i * 3;
|
|
77
|
+
const top = 45 + Math.sin(angle) * r;
|
|
78
|
+
const left = 30 + (i / (template.stops.length - 1 || 1)) * 40;
|
|
79
|
+
return (
|
|
80
|
+
<div
|
|
81
|
+
key={stop.name}
|
|
82
|
+
className="absolute flex flex-col items-center pointer-events-auto"
|
|
83
|
+
style={{ top: `${top}%`, left: `${left}%`, transform: "translate(-50%,-100%)" }}
|
|
84
|
+
>
|
|
85
|
+
<div className="bg-black/60 text-white text-[9px] font-medium px-[5px] py-[1px] rounded-[5px] mb-[2px] whitespace-nowrap backdrop-blur-sm">
|
|
86
|
+
{stop.name}
|
|
87
|
+
</div>
|
|
88
|
+
<div
|
|
89
|
+
className={cn(
|
|
90
|
+
"size-[22px] rounded-full flex items-center justify-center text-white text-[10px] font-bold shadow-md",
|
|
91
|
+
i === 0 && "ring-2 ring-white/60"
|
|
92
|
+
)}
|
|
93
|
+
style={{ backgroundColor: template.color }}
|
|
94
|
+
>
|
|
95
|
+
{i + 1}
|
|
96
|
+
</div>
|
|
97
|
+
{i < template.stops.length - 1 && (
|
|
98
|
+
<div className="w-[1.5px] h-[20px]" style={{ backgroundColor: `${template.color}40` }} />
|
|
99
|
+
)}
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
})}
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
{/* DraggablePanel */}
|
|
106
|
+
<DraggablePanel
|
|
107
|
+
state={panelState}
|
|
108
|
+
onStateChange={setPanelState}
|
|
109
|
+
customSmallHeight={280}
|
|
110
|
+
customMediumHeight={480}
|
|
111
|
+
topToolbar={{
|
|
112
|
+
mode: "center-title",
|
|
113
|
+
title: template.label,
|
|
114
|
+
showBack: true,
|
|
115
|
+
onBack,
|
|
116
|
+
}}
|
|
117
|
+
showTopToolbarInStates={[DRAWER_STATES.SMALL, DRAWER_STATES.MEDIUM, DRAWER_STATES.LARGE]}
|
|
118
|
+
bottomBar={
|
|
119
|
+
<div className="px-[var(--spacing-base)] pb-[var(--spacing-base)] pt-[var(--spacing-xs)] border-t border-black/[0.04] shrink-0">
|
|
120
|
+
<div className="flex gap-[var(--spacing-xs)]">
|
|
121
|
+
<Btn
|
|
122
|
+
size="large"
|
|
123
|
+
variant="secondary"
|
|
124
|
+
label="收藏模板"
|
|
125
|
+
icon={<IconFont name="star" size="18px" />}
|
|
126
|
+
onClick={() => showToast("已收藏模板")}
|
|
127
|
+
className="flex-1"
|
|
128
|
+
/>
|
|
129
|
+
<Btn
|
|
130
|
+
size="large"
|
|
131
|
+
variant="primary"
|
|
132
|
+
label={creating ? "生成中..." : "一键生成计划"}
|
|
133
|
+
icon={creating ? null : <IconFont name="add" size="18px" />}
|
|
134
|
+
onClick={handleCreate}
|
|
135
|
+
className="flex-[2]"
|
|
136
|
+
disabled={creating}
|
|
137
|
+
/>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
}
|
|
141
|
+
>
|
|
142
|
+
{/* 模板头部信息 */}
|
|
143
|
+
<div className="px-[var(--spacing-base)] shrink-0">
|
|
144
|
+
{/* 彩色头部横幅 */}
|
|
145
|
+
<div className="rounded-[16px] overflow-hidden mb-[12px]">
|
|
146
|
+
<div
|
|
147
|
+
className="relative h-[120px] flex items-end p-[14px]"
|
|
148
|
+
style={{ background: `linear-gradient(135deg, ${template.color}20, ${template.color}08)` }}
|
|
149
|
+
>
|
|
150
|
+
<div className="absolute top-[12px] right-[14px] opacity-10">
|
|
151
|
+
<IconFont name={template.icon} size="80px" style={{ color: template.color }} />
|
|
152
|
+
</div>
|
|
153
|
+
<div className="relative z-[1]">
|
|
154
|
+
<div className="flex items-center gap-[6px] mb-[6px]">
|
|
155
|
+
<div
|
|
156
|
+
className="size-[28px] rounded-[8px] flex items-center justify-center"
|
|
157
|
+
style={{ backgroundColor: `${template.color}20` }}
|
|
158
|
+
>
|
|
159
|
+
<IconFont name={template.icon} size="16px" style={{ color: template.color }} />
|
|
160
|
+
</div>
|
|
161
|
+
<Tag
|
|
162
|
+
label={template.difficulty}
|
|
163
|
+
size="sm"
|
|
164
|
+
className="border-transparent"
|
|
165
|
+
style={{ backgroundColor: `${template.color}15`, color: template.color }}
|
|
166
|
+
/>
|
|
167
|
+
</div>
|
|
168
|
+
<p className="text-[13px] text-foreground/70 leading-[18px]">{template.fullDesc}</p>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
{/* 路线摘要 */}
|
|
174
|
+
<div className="flex items-center gap-[16px] mb-[14px]">
|
|
175
|
+
<div className="flex items-center gap-[4px]">
|
|
176
|
+
<IconFont name="location" size="14px" style={{ color: template.color }} />
|
|
177
|
+
<span className="text-[13px] text-foreground font-medium">{template.stops.length}站</span>
|
|
178
|
+
</div>
|
|
179
|
+
<div className="flex items-center gap-[4px]">
|
|
180
|
+
<IconFont name="map-navigation" size="14px" className="text-muted-foreground" />
|
|
181
|
+
<span className="text-[13px] text-muted-foreground">{template.distance}</span>
|
|
182
|
+
</div>
|
|
183
|
+
<div className="flex items-center gap-[4px]">
|
|
184
|
+
<IconFont name="time" size="14px" className="text-muted-foreground" />
|
|
185
|
+
<span className="text-[13px] text-muted-foreground">{template.duration}</span>
|
|
186
|
+
</div>
|
|
187
|
+
<div className="flex items-center gap-[4px] ml-auto">
|
|
188
|
+
<IconFont name="time" size="13px" className="text-muted-foreground" />
|
|
189
|
+
<span className="text-[13px] text-muted-foreground">约{totalHours}h</span>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
{/* 分割线 */}
|
|
195
|
+
<div className="mx-[var(--spacing-base)] h-px bg-black/[0.06] shrink-0" />
|
|
196
|
+
|
|
197
|
+
{/* 站点列表 */}
|
|
198
|
+
<div className="overflow-y-auto flex-1 min-h-0 px-[var(--spacing-base)] pt-[12px]">
|
|
199
|
+
<p className="text-[14px] font-medium text-foreground mb-[10px]">推荐路线</p>
|
|
200
|
+
{template.stops.map((stop, i) => (
|
|
201
|
+
<motion.div
|
|
202
|
+
key={stop.name}
|
|
203
|
+
className="flex gap-[var(--spacing-sm)]"
|
|
204
|
+
initial={{ opacity: 0, x: -10 }}
|
|
205
|
+
animate={{ opacity: 1, x: 0 }}
|
|
206
|
+
transition={{ delay: 0.06 * i, duration: 0.25 }}
|
|
207
|
+
>
|
|
208
|
+
<div className="flex flex-col items-center w-[24px] shrink-0">
|
|
209
|
+
<div
|
|
210
|
+
className="size-[24px] rounded-full flex items-center justify-center shrink-0 text-white text-[11px] font-bold"
|
|
211
|
+
style={{ backgroundColor: template.color }}
|
|
212
|
+
>
|
|
213
|
+
{i + 1}
|
|
214
|
+
</div>
|
|
215
|
+
{i < template.stops.length - 1 && (
|
|
216
|
+
<div className="w-[1.5px] flex-1 min-h-[24px]" style={{ backgroundColor: `${template.color}30` }} />
|
|
217
|
+
)}
|
|
218
|
+
</div>
|
|
219
|
+
<div className="flex-1 min-w-0 pb-[var(--spacing-sm)]">
|
|
220
|
+
<div className="flex items-center gap-[6px] rounded-[14px] p-[10px] bg-card-muted">
|
|
221
|
+
<div
|
|
222
|
+
className="size-[32px] rounded-[10px] flex items-center justify-center shrink-0"
|
|
223
|
+
style={{ backgroundColor: `${template.color}12` }}
|
|
224
|
+
>
|
|
225
|
+
<IconFont name={stop.icon} size="18px" style={{ color: template.color }} />
|
|
226
|
+
</div>
|
|
227
|
+
<div className="flex-1 min-w-0">
|
|
228
|
+
<span className="text-[14px] font-medium text-foreground leading-[20px] block truncate">{stop.name}</span>
|
|
229
|
+
<span className="text-[12px] text-muted-foreground leading-[16px]">{stop.desc} · {stop.dur}</span>
|
|
230
|
+
</div>
|
|
231
|
+
<IconFont name="chevron-right" size="16px" className="text-muted-foreground shrink-0" />
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
</motion.div>
|
|
235
|
+
))}
|
|
236
|
+
|
|
237
|
+
{/* 模板说明 */}
|
|
238
|
+
<div className="flex items-center gap-[8px] p-[12px] rounded-[14px] bg-accent/[0.04] mt-[4px] mb-[16px]">
|
|
239
|
+
<IconFont name="lightbulb" size="16px" className="text-accent shrink-0" />
|
|
240
|
+
<p className="text-[12px] text-accent/70 leading-[17px]">
|
|
241
|
+
一键生成计划后,可在行程详情中自由调整站点顺序和停留时间
|
|
242
|
+
</p>
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
</DraggablePanel>
|
|
246
|
+
|
|
247
|
+
{/* Toast */}
|
|
248
|
+
<div className="fixed top-[120px] left-0 right-0 z-[200] flex justify-center pointer-events-none">
|
|
249
|
+
<AnimatePresence>
|
|
250
|
+
{toast && (
|
|
251
|
+
<motion.div
|
|
252
|
+
className="pointer-events-auto"
|
|
253
|
+
initial={{ y: -10, opacity: 0 }}
|
|
254
|
+
animate={{ y: 0, opacity: 1 }}
|
|
255
|
+
exit={{ y: -10, opacity: 0 }}
|
|
256
|
+
>
|
|
257
|
+
<Toast lines={[toast]} showIcon showClose onClose={() => setToast(null)} />
|
|
258
|
+
</motion.div>
|
|
259
|
+
)}
|
|
260
|
+
</AnimatePresence>
|
|
261
|
+
</div>
|
|
262
|
+
|
|
263
|
+
{/* 生成中遮罩 */}
|
|
264
|
+
<AnimatePresence>
|
|
265
|
+
{creating && (
|
|
266
|
+
<motion.div
|
|
267
|
+
className="absolute inset-0 z-[300] bg-black/40 flex flex-col items-center justify-center"
|
|
268
|
+
initial={{ opacity: 0 }}
|
|
269
|
+
animate={{ opacity: 1 }}
|
|
270
|
+
exit={{ opacity: 0 }}
|
|
271
|
+
>
|
|
272
|
+
<div className="bg-card rounded-[20px] p-[24px] flex flex-col items-center shadow-xl">
|
|
273
|
+
<div
|
|
274
|
+
className="size-[48px] rounded-full flex items-center justify-center mb-[12px] animate-pulse"
|
|
275
|
+
style={{ backgroundColor: `${template.color}15` }}
|
|
276
|
+
>
|
|
277
|
+
<IconFont name={template.icon} size="24px" style={{ color: template.color }} />
|
|
278
|
+
</div>
|
|
279
|
+
<p className="text-[15px] font-medium text-foreground">正在生成计划...</p>
|
|
280
|
+
<p className="text-[12px] text-muted-foreground mt-[4px]">基于「{template.label}」创建行程</p>
|
|
281
|
+
</div>
|
|
282
|
+
</motion.div>
|
|
283
|
+
)}
|
|
284
|
+
</AnimatePresence>
|
|
285
|
+
</div>
|
|
286
|
+
);
|
|
287
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"useDefineForClassFields": true,
|
|
5
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"moduleResolution": "bundler",
|
|
9
|
+
"allowImportingTsExtensions": true,
|
|
10
|
+
"isolatedModules": true,
|
|
11
|
+
"moduleDetection": "force",
|
|
12
|
+
"noEmit": true,
|
|
13
|
+
"jsx": "react-jsx",
|
|
14
|
+
"strict": true,
|
|
15
|
+
"noUnusedLocals": false,
|
|
16
|
+
"noUnusedParameters": false,
|
|
17
|
+
"noFallthroughCasesInSwitch": true,
|
|
18
|
+
"paths": {
|
|
19
|
+
"@/*": ["./src/*"]
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"include": ["src"]
|
|
23
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
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
|
+
export default defineConfig({
|
|
7
|
+
plugins: [react(), tailwindcss()],
|
|
8
|
+
resolve: {
|
|
9
|
+
alias: {
|
|
10
|
+
"@": path.resolve(__dirname, "./src"),
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
server: {
|
|
14
|
+
host: "0.0.0.0",
|
|
15
|
+
port: 5173,
|
|
16
|
+
allowedHosts: "all",
|
|
17
|
+
},
|
|
18
|
+
});
|