From a6750929892997ad1199d78972103870cf06ca8a Mon Sep 17 00:00:00 2001 From: duanshuwen Date: Sat, 4 Jul 2026 14:15:40 +0800 Subject: [PATCH] feat: add leads page and refresh ui components Overhaul all core UI components (button, input, select, textarea, badge, alert, card) to use consistent tailwind color classes, add smooth transitions and hover states for better interactivity. Add TypeScript types, updated API functions, and utility helpers for lead management with filtering and pagination support. Build a fully functional leads management page with status and source filters, lead data formatting (travel date, budget, headcount), loading states, empty states, and a polished responsive table layout. Add comprehensive admin sidebar, workspace, and leads table styling to the global CSS. --- src/api.ts | 21 +- src/components/ui/alert.tsx | 10 +- src/components/ui/badge.tsx | 14 +- src/components/ui/button.tsx | 23 +- src/components/ui/card.tsx | 8 +- src/components/ui/input.tsx | 2 +- src/components/ui/select.tsx | 2 +- src/components/ui/textarea.tsx | 2 +- src/pages/leads/LeadsPage.tsx | 156 +++- src/pages/leads/lead-helpers.ts | 91 +++ src/styles.css | 1196 ++++++++++++++++++++++++++++++- 11 files changed, 1451 insertions(+), 74 deletions(-) create mode 100644 src/pages/leads/lead-helpers.ts diff --git a/src/api.ts b/src/api.ts index 3fc7ab1..e7741b9 100644 --- a/src/api.ts +++ b/src/api.ts @@ -53,18 +53,29 @@ export type ProductDetailSection = { blocks: ProductDetailBlock[]; }; +export type LeadStatus = "new" | "assigned" | "contacted" | "planning" | "won" | "invalid"; + export type Lead = { id: string; destination?: string | null; phone: string; + travelDate?: string | null; + peopleCount?: number | null; + budgetMin?: number | null; + budgetMax?: number | null; note?: string | null; sourcePage?: string | null; - status: "new" | "assigned" | "contacted" | "planning" | "won" | "invalid"; + status: LeadStatus; createdAt: string; sourceProduct?: { id: string; title: string } | null; assignedUser?: { id: string; name: string } | null; }; +export type LeadListParams = { + status?: LeadStatus; + take?: number; +}; + export type MediaAsset = { id: string; url: string; @@ -314,8 +325,12 @@ export async function reorderSiteConfigItems(module: SiteModule, itemIds: string }); } -export async function getLeads() { - return request<{ items: Lead[] }>("/api/admin/leads"); +export async function getLeads(params: LeadListParams = {}) { + const search = new URLSearchParams(); + if (params.status) search.set("status", params.status); + if (params.take) search.set("take", String(params.take)); + const query = search.toString(); + return request<{ items: Lead[] }>(`/api/admin/leads${query ? `?${query}` : ""}`); } export async function updateLeadStatus(id: string, status: Lead["status"]) { diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx index 5606800..c725296 100644 --- a/src/components/ui/alert.tsx +++ b/src/components/ui/alert.tsx @@ -3,13 +3,13 @@ import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@/lib/utils"; -const alertVariants = cva("relative w-full rounded-lg border p-4 text-sm", { +const alertVariants = cva("relative w-full rounded-lg border px-4 py-3 text-sm leading-6 shadow-sm", { variants: { variant: { - default: "border-[var(--border)] bg-[var(--card)] text-[var(--card-foreground)]", - warning: "border-[var(--warning-border)] bg-[var(--warning-muted)] text-[var(--warning)]", - destructive: "border-[var(--destructive-border)] bg-[var(--destructive-muted)] text-[var(--destructive)]", - success: "border-[var(--success-border)] bg-[var(--success-muted)] text-[var(--success)]", + default: "border-slate-200 bg-white text-slate-700", + warning: "border-amber-200 bg-amber-50 text-amber-800", + destructive: "border-red-200 bg-red-50 text-red-700", + success: "border-emerald-200 bg-emerald-50 text-emerald-700", }, }, defaultVariants: { diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx index 9efb7f3..6c40e1a 100644 --- a/src/components/ui/badge.tsx +++ b/src/components/ui/badge.tsx @@ -3,15 +3,15 @@ import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@/lib/utils"; -const badgeVariants = cva("inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors", { +const badgeVariants = cva("inline-flex items-center rounded-full border px-2.5 py-1 text-xs font-bold leading-none transition-colors", { variants: { variant: { - default: "border-transparent bg-[var(--primary)] text-[var(--primary-foreground)]", - secondary: "border-transparent bg-[var(--secondary)] text-[var(--secondary-foreground)]", - outline: "border-[var(--border)] text-[var(--foreground)]", - success: "border-transparent bg-[var(--success-muted)] text-[var(--success)]", - warning: "border-transparent bg-[var(--warning-muted)] text-[var(--warning)]", - muted: "border-transparent bg-[var(--muted)] text-[var(--muted-foreground)]", + default: "border-teal-200 bg-teal-50 text-teal-700", + secondary: "border-slate-200 bg-slate-100 text-slate-700", + outline: "border-slate-300 bg-white text-slate-700", + success: "border-emerald-200 bg-emerald-50 text-emerald-700", + warning: "border-amber-200 bg-amber-50 text-amber-800", + muted: "border-slate-200 bg-slate-100 text-slate-500", }, }, defaultVariants: { diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index e68aa8a..697bc80 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -5,22 +5,25 @@ import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@/lib/utils"; const buttonVariants = cva( - "inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--background)] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + "inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-semibold transition-[background-color,border-color,color,box-shadow,transform] duration-150 ease-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--background)] active:scale-[0.98] disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-45 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", { variants: { variant: { - default: "bg-[var(--primary)] text-[var(--primary-foreground)] hover:bg-[var(--primary-hover)]", - destructive: "bg-[var(--destructive)] text-[var(--destructive-foreground)] hover:bg-[var(--destructive-hover)]", - outline: "border border-[var(--input)] bg-[var(--background)] hover:bg-[var(--accent)] hover:text-[var(--accent-foreground)]", - secondary: "bg-[var(--secondary)] text-[var(--secondary-foreground)] hover:bg-[var(--secondary-hover)]", - ghost: "hover:bg-[var(--accent)] hover:text-[var(--accent-foreground)]", + default: "border border-[var(--primary)] bg-[var(--primary)] text-[var(--primary-foreground)] shadow-sm hover:bg-[var(--primary-hover)] hover:shadow-md", + destructive: + "border border-[var(--destructive)] bg-[var(--destructive)] text-[var(--destructive-foreground)] shadow-sm hover:bg-[var(--destructive-hover)] hover:shadow-md", + outline: + "border border-slate-300 bg-white text-slate-800 shadow-sm hover:border-slate-400 hover:bg-slate-50 hover:text-slate-950 hover:shadow-md", + secondary: + "border border-slate-200 bg-slate-100 text-slate-900 shadow-sm hover:bg-slate-200", + ghost: "text-slate-700 hover:bg-slate-100 hover:text-slate-950", link: "text-[var(--primary)] underline-offset-4 hover:underline", }, size: { - default: "h-10 px-4 py-2", - sm: "h-8 rounded-md px-3 text-xs", - lg: "h-11 rounded-md px-8", - icon: "h-9 w-9", + default: "h-11 px-4 py-2", + sm: "h-9 rounded-lg px-3 text-xs", + lg: "h-12 rounded-xl px-8", + icon: "h-10 w-10", }, }, defaultVariants: { diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx index 6a85405..258253e 100644 --- a/src/components/ui/card.tsx +++ b/src/components/ui/card.tsx @@ -3,22 +3,22 @@ import * as React from "react"; import { cn } from "@/lib/utils"; const Card = React.forwardRef>(({ className, ...props }, ref) => ( -
+
)); Card.displayName = "Card"; const CardHeader = React.forwardRef>(({ className, ...props }, ref) => ( -
+
)); CardHeader.displayName = "CardHeader"; const CardTitle = React.forwardRef>(({ className, ...props }, ref) => ( -

+

)); CardTitle.displayName = "CardTitle"; const CardDescription = React.forwardRef>(({ className, ...props }, ref) => ( -

+

)); CardDescription.displayName = "CardDescription"; diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx index b79867e..aab5b8f 100644 --- a/src/components/ui/input.tsx +++ b/src/components/ui/input.tsx @@ -6,7 +6,7 @@ const Input = React.forwardRef>(({ className, ...props }, ref) => (