diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f650315
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,27 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.pnpm-debug.log*
+
+# env files
+.env*
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
\ No newline at end of file
diff --git a/app/api/database-structure/route.ts b/app/api/database-structure/route.ts
index 84ee395..c90e98f 100644
--- a/app/api/database-structure/route.ts
+++ b/app/api/database-structure/route.ts
@@ -1,3 +1,5 @@
+export const dynamic = "force-dynamic"
+
import { NextResponse } from "next/server"
import { getDatabases, getDatabaseStructure } from "@/lib/mongodb-mock-connector" // 更新导入路径
diff --git a/app/api/users/route.ts b/app/api/users/route.ts
index fe85bff..3156936 100644
--- a/app/api/users/route.ts
+++ b/app/api/users/route.ts
@@ -1,6 +1,6 @@
import { NextResponse, NextRequest } from "next/server"
import type { TrafficUser } from "@/types/traffic"
-import { addUser, filterUsers, getDistinctTags, getUserById, queryUsers, type UserStatus } from "@/lib/mock-users"
+import { addUser, filterUsers, getDistinctTags, getUserById, queryUsers, type UserStatus, MOCK_USERS } from "@/lib/mock-users"
// 中文名字生成器数据
const familyNames = [
@@ -197,30 +197,26 @@ function parseArrayParam(v: string | null) {
return v.split(",").map((s) => s.trim()).filter(Boolean)
}
-export async function GET(req: NextRequest) {
- const { searchParams } = new URL(req.url)
+export const dynamic = "force-dynamic"
- // 详情优先
- const id = searchParams.get('id')
- if (id) {
- const detail = getUserById(id)
- return NextResponse.json({ data: detail }, { headers: { 'Cache-Control': 'no-store' } })
- }
+export async function GET(request: Request) {
+ const { searchParams } = new URL(request.url)
+ const keyword = (searchParams.get("q") || "").trim()
- // 列表
- const q = searchParams.get('q') ?? undefined
- const tagsStr = searchParams.get('tags') ?? ''
- const statusStr = searchParams.get('status') ?? ''
- const rfmMin = Number(searchParams.get('rfmMin') ?? 0)
- const rfmMax = Number(searchParams.get('rfmMax') ?? 100)
- const page = Number(searchParams.get('page') ?? 1)
- const pageSize = Number(searchParams.get('pageSize') ?? 20)
+ const filtered = keyword
+ ? MOCK_USERS.filter(
+ (u) =>
+ u.name.includes(keyword) ||
+ (u.nickname && u.nickname.includes(keyword)) ||
+ u.tags.some((t) => t.includes(keyword)),
+ )
+ : MOCK_USERS
- const tags = tagsStr ? tagsStr.split(',').filter(Boolean) : undefined
- const status = statusStr ? (statusStr.split(',').filter(Boolean) as any) : undefined
-
- const result = queryUsers({ q, tags, status, rfmMin, rfmMax, page, pageSize })
- return NextResponse.json(result, { headers: { 'Cache-Control': 'no-store' } })
+ return NextResponse.json({
+ success: true,
+ total: filtered.length,
+ items: filtered,
+ })
}
export async function POST(req: NextRequest) {
diff --git a/app/page.tsx b/app/page.tsx
index abc0cdf..1f5b9eb 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,7 +1,7 @@
"use client"
import { useState, useEffect } from "react"
-import { Search, Users, TrendingUp, Database, RefreshCw, BarChart3, Activity, Globe } from 'lucide-react'
+import { Search, Users, TrendingUp, Database, RefreshCw, BarChart3, Activity, Globe, Smartphone, Brain } from 'lucide-react'
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
@@ -10,6 +10,7 @@ import { useRouter } from "next/navigation"
import { Toaster } from "@/components/ui/toaster"
import UserSearch from '@/components/home/user-search'
import UserList from '@/components/home/user-list'
+import Link from "next/link"
interface SystemStats {
userCount: number
@@ -36,6 +37,8 @@ function StatCard({ icon, label, value }: { icon: React.ReactNode; label: string
)
}
+export const dynamic = "force-static"
+
export default function OverviewPage() {
const router = useRouter()
const [searchQuery, setSearchQuery] = useState("")
@@ -113,6 +116,33 @@ export default function OverviewPage() {
return num.toString()
}
+ const tiles = [
+ {
+ href: "/user-portrait",
+ title: "用户画像",
+ desc: "管理与分群、标签与画像洞察",
+ icon: ,
+ },
+ {
+ href: "/devices",
+ title: "设备管理",
+ desc: "查看设备状态与执行任务",
+ icon: ,
+ },
+ {
+ href: "/data-platform",
+ title: "数据中台",
+ desc: "数据接入、质量监控与查询",
+ icon: ,
+ },
+ {
+ href: "/ai-assistant",
+ title: "AI 助手",
+ desc: "智能问答与自动化报表",
+ icon: ,
+ },
+ ]
+
return (
@@ -302,58 +332,21 @@ export default function OverviewPage() {
{/* 快速访问入口 */}
-
-
router.push("/data-platform")}
- >
-
-
-
-
- 数据中台
- 数据源管理与AI模型
-
-
-
-
router.push("/user-portrait")}
- >
-
-
-
-
- 用户画像
- 用户管理与标签体系
-
-
-
-
router.push("/ai-assistant")}
- >
-
-
-
-
- AI智能助手
- 数据分析与报告生成
-
-
-
-
router.push("/intelligent-search")}
- >
-
-
-
-
- 智能搜索
- 全局搜索与AI分析
-
-
+
+ {tiles.map((t) => (
+
+
+ {t.title}
+ {t.icon}
+
+
+ {t.desc}
+
+
+
+ ))}
{/* 快速指标示例(可后续接入真实数据) */}
diff --git a/app/user-portrait/page.tsx b/app/user-portrait/page.tsx
index 9142f78..08ea357 100644
--- a/app/user-portrait/page.tsx
+++ b/app/user-portrait/page.tsx
@@ -1,239 +1,101 @@
-"use client"
-
-import { useEffect, useMemo, useState } from "react"
-import Link from "next/link"
-import MobileHeader from "@/app/components/MobileHeader"
-import BottomNav from "@/app/components/BottomNav"
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
-import { Card, CardContent } from "@/components/ui/card"
-import { Button } from "@/components/ui/button"
+import { Suspense } from "react"
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
-import { Badge } from "@/components/ui/badge"
-import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
-import { Label } from "@/components/ui/label"
-import { Checkbox } from "@/components/ui/checkbox"
-import { Search, Filter, Plus } from 'lucide-react'
-import FilterDrawer, { type FilterValues } from "@/components/user-portrait/filter-drawer"
+import { Button } from "@/components/ui/button"
+import Link from "next/link"
-type User = {
- id: string
- name: string
- phone: string
- email: string
- tags: string[]
- rfmScore: number
- lastActivity: string
- status: "活跃" | "沉睡" | "已封禁"
+export const dynamic = "force-dynamic"
+
+async function fetchUsers(q: string) {
+ const url = q ? `/api/users?q=${encodeURIComponent(q)}` : "/api/users"
+ const res = await fetch(url, { cache: "no-store" })
+ if (!res.ok) return { items: [], total: 0 }
+ return res.json()
}
-type UsersResponse = { success: true; data: { items: User[]; total: number; page: number; pageSize: number } }
-
-export default function UserPortraitPage() {
- const [users, setUsers] = useState
([])
- const [total, setTotal] = useState(0)
-
- const [searchQuery, setSearchQuery] = useState("")
- const [isAddingUser, setIsAddingUser] = useState(false)
- const [newUser, setNewUser] = useState({ name: "", phone: "", email: "", tags: [] as string[] })
-
- const [filterOpen, setFilterOpen] = useState(false)
- const [allTags, setAllTags] = useState([])
- const [filters, setFilters] = useState({ tags: [], status: [], rfm: [0, 100] })
-
- const queryString = useMemo(() => {
- const p = new URLSearchParams()
- if (searchQuery) p.set("q", searchQuery)
- if (filters.tags.length) p.set("tags", filters.tags.join(","))
- if (filters.status.length) p.set("status", filters.status.join(","))
- p.set("rfmMin", String(filters.rfm[0]))
- p.set("rfmMax", String(filters.rfm[1]))
- p.set("page", "1")
- p.set("pageSize", "50")
- return p.toString()
- }, [searchQuery, filters])
-
- useEffect(() => {
- fetch(`/api/users?${queryString}`)
- .then((r) => r.json())
- .then((res: UsersResponse) => {
- if (res?.success) {
- setUsers(res.data.items)
- setTotal(res.data.total)
- }
- })
- .catch(() => {})
- }, [queryString])
-
- useEffect(() => {
- fetch("/api/users?meta=tags")
- .then((r) => r.json())
- .then((res: any) => setAllTags(res?.data?.tags ?? []))
- .catch(() => {})
- }, [])
-
- const handleAddUser = async () => {
- const resp = await fetch("/api/users", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(newUser),
- })
- const data = await resp.json()
- if (data?.success) {
- setIsAddingUser(false)
- setNewUser({ name: "", phone: "", email: "", tags: [] })
- // 触发刷新
- fetch(`/api/users?${queryString}`)
- .then((r) => r.json())
- .then((res: UsersResponse) => {
- if (res?.success) {
- setUsers(res.data.items)
- setTotal(res.data.total)
- }
- })
- }
- }
-
+function Tabs({ active = "users" }: { active?: "users" | "tags" }) {
return (
-
-
{}} title="用户画像" />
-
-
-
-
-
-
-
-
- 用户管理
- 标签管理
-
-
-
-
-
-
- setSearchQuery(e.target.value)} />
-
-
-
-
-
-
- {users.map((u) => (
-
-
-
-
-
- {u.name}
-
-
- 最后活跃 {new Date(u.lastActivity).toLocaleDateString("zh-CN")}
-
-
-
-
{u.phone}
-
{u.email}
-
-
-
- {u.tags.slice(0, 2).map((t) => (
- {t}
- ))}
- {u.tags.length > 2 && (
- +{u.tags.length - 2}
- )}
-
-
-
{u.rfmScore}
-
-
- {u.status}
-
-
-
-
-
- ))}
-
-
-
-
- 标签管理将在接入数据字典后提供配置与统计
-
-
-
-
-
-
-
-
- {/* 筛选抽屉 */}
- setFilters(v)}
- />
-
- {/* 添加用户 */}
-
+
)
}
+
+async function UsersList({ q }: { q: string }) {
+ const { items } = await fetchUsers(q)
+ return (
+
+ {items.map((u: any) => (
+
+
+ {u.name}{u.nickname ? ` · ${u.nickname}` : ""}
+ 最后活跃:{new Date(u.lastActive).toLocaleString()}
+
+
+
+

+
+ {u.tags?.slice(0, 3).map((t: string) => (
+ {t}
+ ))}
+
+
+
+
+
+ ))}
+ {items.length === 0 && (
+
暂无用户数据
+ )}
+
+ )
+}
+
+export default async function UserPortraitPage({
+ searchParams,
+}: {
+ searchParams: { q?: string }
+}) {
+ const q = (searchParams?.q || "").trim()
+
+ return (
+
+
+
+ 用户画像
+ 管理与分群
+
+
+
+
+ 加载中...}>
+ {/* @ts-expect-error Async Server Component */}
+
+
+
+
+
+ )
+}
diff --git a/components/user-portrait/filter-drawer.tsx b/components/user-portrait/filter-drawer.tsx
index 39c313a..3230354 100644
--- a/components/user-portrait/filter-drawer.tsx
+++ b/components/user-portrait/filter-drawer.tsx
@@ -1,19 +1,19 @@
"use client"
-import { useEffect, useState } from "react"
-import { X } from 'lucide-react'
+import { useMemo, useState } from "react"
+import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
import { Checkbox } from "@/components/ui/checkbox"
import { Label } from "@/components/ui/label"
-import { Input } from "@/components/ui/input"
export type FilterValues = {
tags: string[]
- status: Array<"活跃" | "沉睡" | "已封禁">
+ status: string[]
rfm: [number, number]
}
-interface Props {
+type Props = {
open: boolean
onOpenChange: (v: boolean) => void
allTags: string[]
@@ -21,131 +21,122 @@ interface Props {
onApply: (v: FilterValues) => void
}
-export default function FilterDrawer({ open, onOpenChange, allTags, value, onApply }: Props) {
+const STATUS_OPTIONS = ["活跃", "沉睡", "已封禁"] as const
+
+export default function FilterDrawer({
+ open,
+ onOpenChange,
+ allTags,
+ value,
+ onApply,
+}: Props) {
const [local, setLocal] = useState(value)
- useEffect(() => setLocal(value), [value, open])
+ // 同步外部变更
+ useMemo(() => setLocal(value), [value])
- const toggleTag = (t: string, checked: boolean) => {
- setLocal((prev) => ({
- ...prev,
- tags: checked ? Array.from(new Set([...prev.tags, t])) : prev.tags.filter((x) => x !== t),
- }))
- }
-
- const toggleStatus = (s: "活跃" | "沉睡" | "已封禁", checked: boolean) => {
- setLocal((prev) => ({
- ...prev,
- status: checked ? Array.from(new Set([...prev.status, s])) : prev.status.filter((x) => x !== s),
- }))
- }
-
- const apply = () => {
- onApply(local)
- onOpenChange(false)
- }
-
- const reset = () => {
- const init: FilterValues = { tags: [], status: [], rfm: [0, 100] }
- setLocal(init)
- onApply(init)
- }
+ const toggleArrayVal = (arr: string[], val: string, checked: boolean) =>
+ checked ? Array.from(new Set([...arr, val])) : arr.filter((x) => x !== val)
return (
-
-
onOpenChange(false)}
- />
-