Skip to content

Commit f36cffd

Browse files
committed
feat: complete HaloLight Qwik implementation
1 parent a56e02c commit f36cffd

11 files changed

Lines changed: 1382 additions & 79 deletions

File tree

.env.example

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,13 @@ VITE_API_URL=/api
99
VITE_MOCK=true
1010

1111
# 演示账号
12-
VITE_DEMO_EMAIL=admin@example.com
12+
VITE_DEMO_EMAIL=admin@halolight.h7ml.cn
1313
VITE_DEMO_PASSWORD=123456
1414
VITE_SHOW_DEMO_HINT=true
1515

1616
# 应用配置
1717
VITE_APP_TITLE=Admin Pro
1818
VITE_BRAND_NAME=Halolight
19+
20+
# 注册功能开关(默认关闭)
21+
VITE_ENABLE_REGISTRATION=false

src/components/dashboard/GridItem.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ import type { DashboardWidget, DashboardLayout } from "~/types/dashboard";
55
import { StatsWidget } from "../widgets/StatsWidget";
66
import { ChartWidget } from "../widgets/ChartWidget";
77
import { EChartsWidget } from "../widgets/EChartsWidget";
8+
import { TasksWidget } from "../widgets/TasksWidget";
9+
import { RecentUsersWidget } from "../widgets/RecentUsersWidget";
10+
import { NotificationsWidget } from "../widgets/NotificationsWidget";
11+
import { CalendarWidget } from "../widgets/CalendarWidget";
12+
import { QuickActionsWidget } from "../widgets/QuickActionsWidget";
813

914
interface GridItemProps {
1015
widget: DashboardWidget;
@@ -80,12 +85,21 @@ export const GridItem = component$<GridItemProps>(
8085
/>
8186
);
8287

83-
// TODO: 实现其他组件类型
8488
case "recent-users":
89+
return <RecentUsersWidget title={widget.title} />;
90+
8591
case "notifications":
92+
return <NotificationsWidget title={widget.title} />;
93+
8694
case "tasks":
95+
return <TasksWidget title={widget.title} />;
96+
8797
case "calendar":
98+
return <CalendarWidget title={widget.title} />;
99+
88100
case "quick-actions":
101+
return <QuickActionsWidget title={widget.title} />;
102+
89103
default:
90104
return (
91105
<div class="h-full flex items-center justify-center text-gray-500">
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
import { component$, useSignal, useVisibleTask$ } from "@builder.io/qwik";
2+
3+
interface CalendarEvent {
4+
id: string;
5+
title: string;
6+
start: string;
7+
end?: string;
8+
type: "meeting" | "task" | "event" | "reminder";
9+
}
10+
11+
interface CalendarWidgetProps {
12+
title?: string;
13+
}
14+
15+
/**
16+
* 日历小部件
17+
* 显示今日日程和即将到来的事件
18+
*/
19+
export const CalendarWidget = component$<CalendarWidgetProps>(
20+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
21+
({ title = "今日日程" }) => {
22+
const events = useSignal<CalendarEvent[]>([]);
23+
const isLoading = useSignal(true);
24+
const today = useSignal("");
25+
26+
// eslint-disable-next-line qwik/no-use-visible-task
27+
useVisibleTask$(async () => {
28+
try {
29+
// 获取今天的日期
30+
const now = new Date();
31+
const year = now.getFullYear();
32+
const month = String(now.getMonth() + 1).padStart(2, "0");
33+
const day = String(now.getDate()).padStart(2, "0");
34+
today.value = `${year}-${month}-${day}`;
35+
36+
// 模拟 API 调用
37+
await new Promise((resolve) => setTimeout(resolve, 500));
38+
events.value = [
39+
{
40+
id: "1",
41+
title: "团队站会",
42+
start: `${today.value}T09:00:00`,
43+
end: `${today.value}T09:30:00`,
44+
type: "meeting",
45+
},
46+
{
47+
id: "2",
48+
title: "产品设计评审",
49+
start: `${today.value}T10:30:00`,
50+
end: `${today.value}T12:00:00`,
51+
type: "meeting",
52+
},
53+
{
54+
id: "3",
55+
title: "完成需求文档",
56+
start: `${today.value}T14:00:00`,
57+
end: `${today.value}T16:00:00`,
58+
type: "task",
59+
},
60+
{
61+
id: "4",
62+
title: "客户演示",
63+
start: `${today.value}T16:30:00`,
64+
end: `${today.value}T17:30:00`,
65+
type: "event",
66+
},
67+
{
68+
id: "5",
69+
title: "提交周报",
70+
start: `${today.value}T18:00:00`,
71+
type: "reminder",
72+
},
73+
];
74+
} finally {
75+
isLoading.value = false;
76+
}
77+
});
78+
79+
const formatTime = (dateString: string) => {
80+
const date = new Date(dateString);
81+
const hours = String(date.getHours()).padStart(2, "0");
82+
const minutes = String(date.getMinutes()).padStart(2, "0");
83+
return `${hours}:${minutes}`;
84+
};
85+
86+
const formatTimeRange = (start: string, end?: string) => {
87+
const startTime = formatTime(start);
88+
if (!end) return startTime;
89+
const endTime = formatTime(end);
90+
return `${startTime} - ${endTime}`;
91+
};
92+
93+
const getTypeIcon = (type: string) => {
94+
switch (type) {
95+
case "meeting":
96+
return (
97+
<svg
98+
class="w-4 h-4"
99+
fill="none"
100+
stroke="currentColor"
101+
viewBox="0 0 24 24"
102+
>
103+
<path
104+
stroke-linecap="round"
105+
stroke-linejoin="round"
106+
stroke-width="2"
107+
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
108+
/>
109+
</svg>
110+
);
111+
case "task":
112+
return (
113+
<svg
114+
class="w-4 h-4"
115+
fill="none"
116+
stroke="currentColor"
117+
viewBox="0 0 24 24"
118+
>
119+
<path
120+
stroke-linecap="round"
121+
stroke-linejoin="round"
122+
stroke-width="2"
123+
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"
124+
/>
125+
</svg>
126+
);
127+
case "event":
128+
return (
129+
<svg
130+
class="w-4 h-4"
131+
fill="none"
132+
stroke="currentColor"
133+
viewBox="0 0 24 24"
134+
>
135+
<path
136+
stroke-linecap="round"
137+
stroke-linejoin="round"
138+
stroke-width="2"
139+
d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z"
140+
/>
141+
</svg>
142+
);
143+
default:
144+
return (
145+
<svg
146+
class="w-4 h-4"
147+
fill="none"
148+
stroke="currentColor"
149+
viewBox="0 0 24 24"
150+
>
151+
<path
152+
stroke-linecap="round"
153+
stroke-linejoin="round"
154+
stroke-width="2"
155+
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"
156+
/>
157+
</svg>
158+
);
159+
}
160+
};
161+
162+
const getTypeColor = (type: string) => {
163+
switch (type) {
164+
case "meeting":
165+
return "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400";
166+
case "task":
167+
return "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400";
168+
case "event":
169+
return "bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400";
170+
default:
171+
return "bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400";
172+
}
173+
};
174+
175+
const getTypeLabel = (type: string) => {
176+
switch (type) {
177+
case "meeting":
178+
return "会议";
179+
case "task":
180+
return "任务";
181+
case "event":
182+
return "活动";
183+
default:
184+
return "提醒";
185+
}
186+
};
187+
188+
return (
189+
<div class="h-full">
190+
{isLoading.value ? (
191+
<div class="flex items-center justify-center h-32 text-gray-500">
192+
加载中...
193+
</div>
194+
) : events.value.length === 0 ? (
195+
<div class="flex flex-col items-center justify-center h-32 text-gray-500">
196+
<svg
197+
class="w-12 h-12 mb-2 text-gray-400"
198+
fill="none"
199+
stroke="currentColor"
200+
viewBox="0 0 24 24"
201+
>
202+
<path
203+
stroke-linecap="round"
204+
stroke-linejoin="round"
205+
stroke-width="2"
206+
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
207+
/>
208+
</svg>
209+
<p class="text-sm">暂无日程</p>
210+
</div>
211+
) : (
212+
<div class="space-y-3">
213+
{events.value.map((event) => (
214+
<div
215+
key={event.id}
216+
class="flex items-start gap-3 p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors border border-gray-100 dark:border-gray-700"
217+
>
218+
<div
219+
class={`w-8 h-8 rounded-lg flex items-center justify-center ${getTypeColor(event.type)}`}
220+
>
221+
{getTypeIcon(event.type)}
222+
</div>
223+
<div class="flex-1 min-w-0">
224+
<p class="text-sm font-medium text-gray-900 dark:text-white">
225+
{event.title}
226+
</p>
227+
<div class="flex items-center gap-2 mt-1">
228+
<span class="text-xs text-gray-500 dark:text-gray-400">
229+
{formatTimeRange(event.start, event.end)}
230+
</span>
231+
<span
232+
class={`text-xs px-1.5 py-0.5 rounded ${getTypeColor(event.type)}`}
233+
>
234+
{getTypeLabel(event.type)}
235+
</span>
236+
</div>
237+
</div>
238+
</div>
239+
))}
240+
</div>
241+
)}
242+
</div>
243+
);
244+
},
245+
);

0 commit comments

Comments
 (0)