From afe5024a3af9590efcec06db47b7ef42a2b9c3f1 Mon Sep 17 00:00:00 2001 From: xun082 <73689580+xun082@users.noreply.github.com> Date: Tue, 17 Jun 2025 10:44:09 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=9A=80=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E6=8E=A7=E5=88=B6=E5=8F=B0=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/auth/email/page.tsx | 248 +++++++++++++++++++------ src/app/dashboard/calendar/page.tsx | 188 +++++++++++++++++++ src/app/dashboard/contacts/page.tsx | 196 +++++++++++++++++++ src/app/dashboard/layout.tsx | 139 ++++++++++++++ src/app/dashboard/meeting/page.tsx | 176 ++++++++++++++++++ src/app/dashboard/messages/page.tsx | 121 ++++++++++++ src/app/dashboard/more/page.tsx | 132 +++++++++++++ src/app/dashboard/page.tsx | 248 +++++++++++++++++++++++++ src/app/dashboard/settings/page.tsx | 103 ++++++++++ src/app/docs/layout.tsx | 31 +++- src/app/page.tsx | 230 +++++++++++++++++++++-- src/components/layout/Header/index.tsx | 17 ++ src/middleware.ts | 29 ++- src/styles/index.css | 6 +- 14 files changed, 1769 insertions(+), 95 deletions(-) create mode 100644 src/app/dashboard/calendar/page.tsx create mode 100644 src/app/dashboard/contacts/page.tsx create mode 100644 src/app/dashboard/layout.tsx create mode 100644 src/app/dashboard/meeting/page.tsx create mode 100644 src/app/dashboard/messages/page.tsx create mode 100644 src/app/dashboard/more/page.tsx create mode 100644 src/app/dashboard/page.tsx create mode 100644 src/app/dashboard/settings/page.tsx diff --git a/src/app/auth/email/page.tsx b/src/app/auth/email/page.tsx index 46936718..c960a6be 100644 --- a/src/app/auth/email/page.tsx +++ b/src/app/auth/email/page.tsx @@ -1,9 +1,12 @@ 'use client'; -import React, { useState } from 'react'; -import { Mail } from 'lucide-react'; +import React, { useState, useEffect, useRef, useCallback } from 'react'; +import { Mail, AlertCircle, CheckCircle } from 'lucide-react'; import { useRouter } from 'next/navigation'; import { toast } from 'sonner'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -12,22 +15,72 @@ import authApi from '@/services/auth'; import { saveAuthData } from '@/utils/cookie'; import { ErrorHandler } from '@/services/request'; +// 验证码长度配置 +const CODE_LENGTH = 6; + +// 表单验证 schema +const emailLoginSchema = z.object({ + email: z.string().min(1, '请输入邮箱地址').email('请输入有效的邮箱地址').max(100, '邮箱地址过长'), + code: z + .string() + .min(1, '请输入验证码') + .length(CODE_LENGTH, `验证码必须是${CODE_LENGTH}位数字`) + .regex(/^\d+$/, '验证码只能包含数字'), +}); + +type EmailLoginFormData = z.infer; + export default function EmailLoginPage() { const router = useRouter(); - const [email, setEmail] = useState(''); - const [code, setCode] = useState(''); - const [isLoading, setIsLoading] = useState(false); const [countdown, setCountdown] = useState(0); + const [isSubmitting, setIsSubmitting] = useState(false); + const [isSendingCode, setIsSendingCode] = useState(false); + + // 使用 ref 保存定时器 ID,避免内存泄漏 + const timerRef = useRef(null); + + // 使用 react-hook-form + zod + const { + register, + handleSubmit, + watch, + formState: { errors, isValid }, + trigger, + setValue, + } = useForm({ + resolver: zodResolver(emailLoginSchema), + mode: 'onChange', + defaultValues: { + email: '', + code: '', + }, + }); + + const watchedEmail = watch('email'); + + // 清理定时器的函数 + const clearTimer = useCallback(() => { + if (timerRef.current) { + clearInterval(timerRef.current); + timerRef.current = null; + } + }, []); + + // 组件卸载时清理定时器 + useEffect(() => { + return () => { + clearTimer(); + }; + }, [clearTimer]); const errorHandler: ErrorHandler = { - onError: () => { + onError: (error) => { + console.error('请求错误:', error); toast.error('请求失败,请稍后重试'); }, - unauthorized: () => { - toast.error('未登录或登录已过期,请重新登录'); - }, + forbidden: () => { - toast.error('没有权限'); + toast.error('验证码错误或已失效'); }, serverError: () => { toast.error('服务器错误,请稍后再试'); @@ -40,60 +93,120 @@ export default function EmailLoginPage() { }, }; + // 开始倒计时 + const startCountdown = useCallback(() => { + clearTimer(); // 清理之前的定时器 + setCountdown(60); + + timerRef.current = setInterval(() => { + setCountdown((prev) => { + if (prev <= 1) { + clearTimer(); + + return 0; + } + + return prev - 1; + }); + }, 1000); + }, [clearTimer]); + + // 发送验证码 const handleSendCode = async () => { - if (!email) { - toast.error('请输入邮箱地址'); + // 先验证邮箱字段 + const emailValid = await trigger('email'); + if (!emailValid || !watchedEmail) { return; } - setIsLoading(true); + // 防止重复提交 + if (isSendingCode || countdown > 0) { + return; + } - const { data } = await authApi.sendEmailCode(email, errorHandler); - setIsLoading(false); + setIsSendingCode(true); - if (data?.data.success) { - setCountdown(60); + const { data, error } = await authApi.sendEmailCode(watchedEmail, errorHandler); - const timer = setInterval(() => { - setCountdown((prev) => { - if (prev <= 1) { - clearInterval(timer); + if (error) { + console.error('发送验证码失败:', error); + setIsSendingCode(false); - return 0; - } + return; + } - return prev - 1; - }); - }, 1000); + if (data && data.code === 201) { + toast.success('验证码已发送', { + description: `验证码已发送到 ${watchedEmail},请查收`, + }); + startCountdown(); } else { - toast.error(data?.data.message || '发送验证码失败'); + toast.error(data?.message || '发送验证码失败'); } + + setIsSendingCode(false); }; - const handleEmailLogin = async (e: React.FormEvent) => { - e.preventDefault(); + // 处理登录提交 + const onSubmit = async (data: EmailLoginFormData) => { + // 防止重复提交 + if (isSubmitting) { + return; + } + + setIsSubmitting(true); + + console.log('开始登录请求:', { email: data.email, code: data.code }); + + const { data: responseData, error } = await authApi.emailCodeLogin( + { email: data.email, code: data.code }, + errorHandler, + ); - if (!email || !code) { - toast.error('请输入邮箱和验证码'); + console.log('登录响应:', { responseData, error }); + + if (error) { + console.error('登录失败:', error); + setIsSubmitting(false); return; } - setIsLoading(true); + if (responseData && responseData.code === 201) { + toast.success('登录成功!', { + description: '正在跳转到首页...', + }); - const { data } = await authApi.emailCodeLogin({ email, code }, errorHandler); - setIsLoading(false); + saveAuthData(responseData.data); - if (data?.data.success) { - saveAuthData(data.data); + // 清理定时器和状态 + clearTimer(); - router.push('/'); + // 延迟跳转,让用户看到成功消息 + setTimeout(() => { + router.push('/'); + }, 1000); } else { - toast.error(data?.data.message || '登录失败'); + console.error('登录响应异常:', responseData); + toast.error(responseData?.message || '登录失败,请检查验证码是否正确'); } + + setIsSubmitting(false); + }; + + // 处理验证码输入变化(只允许数字) + const handleCodeChange = (e: React.ChangeEvent) => { + const value = e.target.value.replace(/\D/g, '').slice(0, CODE_LENGTH); + setValue('code', value, { shouldValidate: true }); }; + // 检查发送验证码按钮是否应该禁用 + const isSendCodeDisabled = !watchedEmail || !!errors.email || isSendingCode || countdown > 0; + + // 检查登录按钮是否应该禁用 + const isLoginDisabled = !isValid || isSubmitting; + return (
@@ -102,47 +215,73 @@ export default function EmailLoginPage() {

请输入您的邮箱和验证码

-
+
- + setEmail(e.target.value)} - required + placeholder="请输入邮箱地址" + {...register('email')} + className={ + errors.email ? 'border-red-500 focus:border-red-500 focus:ring-red-500' : '' + } + autoComplete="email" /> + {errors.email && ( +
+ + {errors.email.message} +
+ )}
+
setCode(e.target.value)} - required + placeholder={`请输入${CODE_LENGTH}位验证码`} + {...register('code')} + onChange={handleCodeChange} + className={ + errors.code ? 'border-red-500 focus:border-red-500 focus:ring-red-500' : '' + } + maxLength={CODE_LENGTH} + autoComplete="one-time-code" />
+ {errors.code && ( +
+ + {errors.code.message} +
+ )} + {countdown > 0 && !errors.code && ( +
+ + 验证码已发送,{countdown}秒后可重新发送 +
+ )}
+
@@ -151,6 +290,7 @@ export default function EmailLoginPage() { variant="link" className="text-gray-500 hover:text-gray-700" onClick={() => router.push('/auth')} + disabled={isSubmitting} > 返回登录页 diff --git a/src/app/dashboard/calendar/page.tsx b/src/app/dashboard/calendar/page.tsx new file mode 100644 index 00000000..09f6a0a1 --- /dev/null +++ b/src/app/dashboard/calendar/page.tsx @@ -0,0 +1,188 @@ +import { ChevronLeft, ChevronRight, Plus } from 'lucide-react'; + +const mockEvents = [ + { + id: 1, + title: '产品评审会议', + time: '09:00 - 10:30', + type: 'meeting', + color: 'bg-blue-500', + }, + { + id: 2, + title: '技术分享', + time: '14:00 - 15:00', + type: 'presentation', + color: 'bg-green-500', + }, + { + id: 3, + title: '一对一沟通', + time: '16:00 - 16:30', + type: 'meeting', + color: 'bg-purple-500', + }, +]; + +export default function CalendarPage() { + const today = new Date(); + const currentMonth = today.toLocaleDateString('zh-CN', { year: 'numeric', month: 'long' }); + + return ( +
+ {/* 左侧侧边栏 */} +
+
+ +
+ + {/* 小日历 */} +
+
+

{currentMonth}

+
+ + +
+
+ + {/* 简化的小日历 */} +
+ {['日', '一', '二', '三', '四', '五', '六'].map((day) => ( +
+ {day} +
+ ))} + {Array.from({ length: 35 }, (_, i) => { + const date = i - 5; // 简化处理 + + return ( + + ); + })} +
+
+ + {/* 今日日程 */} +
+

今日日程

+
+ {mockEvents.map((event) => ( +
+
+
+

{event.title}

+

{event.time}

+
+
+ ))} +
+
+
+ + {/* 主日历视图 */} +
+ {/* 头部 */} +
+
+
+

日历

+
+ + + {currentMonth} + + +
+
+ +
+ +
+ + + +
+
+
+
+ + {/* 日历网格 */} +
+
+ {/* 星期头部 */} +
+ {['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'].map((day) => ( +
+ {day} +
+ ))} +
+ + {/* 日期网格 */} +
+ {Array.from({ length: 35 }, (_, i) => { + const date = i - 5; // 简化处理 + const isToday = date === today.getDate(); + + return ( +
+
31 + ? 'text-gray-300' + : isToday + ? 'bg-blue-600 text-white rounded-full w-6 h-6 flex items-center justify-center' + : 'text-gray-900' + }`} + > + {date > 0 && date <= 31 ? date : ''} +
+ + {/* 示例事件 */} + {date === today.getDate() && ( +
+
+ 产品评审 +
+
+ 技术分享 +
+
+ )} +
+ ); + })} +
+
+
+
+
+ ); +} diff --git a/src/app/dashboard/contacts/page.tsx b/src/app/dashboard/contacts/page.tsx new file mode 100644 index 00000000..3f40ee0b --- /dev/null +++ b/src/app/dashboard/contacts/page.tsx @@ -0,0 +1,196 @@ +import { Search, Plus, Building, Phone, Mail } from 'lucide-react'; + +const mockContacts = [ + { + id: 1, + name: '张三', + avatar: '👨‍💼', + title: '产品经理', + department: '产品部', + phone: '138-0000-0001', + email: 'zhangsan@company.com', + status: 'online', + }, + { + id: 2, + name: '李四', + avatar: '👩‍💻', + title: '前端工程师', + department: '技术部', + phone: '138-0000-0002', + email: 'lisi@company.com', + status: 'away', + }, + { + id: 3, + name: '王五', + avatar: '👨‍🎨', + title: 'UI设计师', + department: '设计部', + phone: '138-0000-0003', + email: 'wangwu@company.com', + status: 'offline', + }, + { + id: 4, + name: '赵六', + avatar: '👩‍💼', + title: '运营专员', + department: '运营部', + phone: '138-0000-0004', + email: 'zhaoliu@company.com', + status: 'online', + }, + { + id: 5, + name: '孙七', + avatar: '👨‍💻', + title: '后端工程师', + department: '技术部', + phone: '138-0000-0005', + email: 'sunqi@company.com', + status: 'online', + }, +]; + +const departments = [ + { name: '全公司', count: 25, icon: '🏢' }, + { name: '技术部', count: 8, icon: '💻' }, + { name: '产品部', count: 5, icon: '📱' }, + { name: '设计部', count: 4, icon: '🎨' }, + { name: '运营部', count: 4, icon: '📊' }, + { name: '销售部', count: 4, icon: '💼' }, +]; + +const getStatusColor = (status: string) => { + switch (status) { + case 'online': + return 'bg-green-500'; + case 'away': + return 'bg-yellow-500'; + case 'offline': + return 'bg-gray-400'; + default: + return 'bg-gray-400'; + } +}; + +const getStatusText = (status: string) => { + switch (status) { + case 'online': + return '在线'; + case 'away': + return '离开'; + case 'offline': + return '离线'; + default: + return '未知'; + } +}; + +export default function ContactsPage() { + return ( +
+ {/* 左侧部门列表 */} +
+
+

通讯录

+ + {/* 搜索框 */} +
+ + +
+
+ + {/* 部门列表 */} +
+ {departments.map((dept, index) => ( + + ))} +
+
+ + {/* 右侧联系人列表 */} +
+ {/* 头部操作栏 */} +
+
+
+ +

全公司

+ ({mockContacts.length} 人) +
+ +
+
+ + {/* 联系人列表 */} +
+
+ {mockContacts.map((contact) => ( +
+
+
+
+ {contact.avatar} +
+
+
+ +
+

{contact.name}

+

{contact.title}

+

{contact.department}

+

{getStatusText(contact.status)}

+
+
+ +
+
+ + {contact.phone} +
+
+ + {contact.email} +
+
+ +
+ + +
+
+ ))} +
+
+
+
+ ); +} diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx new file mode 100644 index 00000000..ce48932b --- /dev/null +++ b/src/app/dashboard/layout.tsx @@ -0,0 +1,139 @@ +'use client'; + +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; +import { ReactNode } from 'react'; +import { + MessageCircle, + Users, + FileText, + Calendar, + Video, + Settings, + MoreHorizontal, +} from 'lucide-react'; + +interface NavItem { + name: string; + href: string; + icon: ReactNode; + external?: boolean; +} + +const navItems: NavItem[] = [ + { + name: '消息', + href: '/dashboard/messages', + icon: , + }, + { + name: '日历', + href: '/dashboard/calendar', + icon: , + }, + { + name: '文档', + href: '/docs', + icon: , + external: true, + }, + { + name: '视频会议', + href: '/dashboard/meeting', + icon: