更新navigation里的权限控制 & 更新客户认领查询逻辑

This commit is contained in:
Eamon
2026-04-01 13:14:28 +08:00
parent be03cf6440
commit 545435d095
6 changed files with 261 additions and 215 deletions

View File

@@ -18,3 +18,7 @@ export async function bindAdmin(params: {
user_id: params.id,
})
}
export async function getPageUser(params: object) {
return callByUser<User>("/api/admin/user/get", params)
}

View File

@@ -112,6 +112,7 @@ export default function Appbar(props: { admin: Admin }) {
admin: "管理员",
permissions: "权限列表",
discount: "折扣管理",
statistics: "数据统计",
}
return labels[path] || path

View File

@@ -23,6 +23,7 @@ import Link from "next/link"
import { usePathname } from "next/navigation"
import { createContext, type ReactNode, useContext, useState } from "react"
import { twJoin } from "tailwind-merge"
import { Auth } from "@/components/auth"
import { Button } from "@/components/ui/button"
import { ScrollArea } from "@/components/ui/scroll-area"
import { Separator } from "@/components/ui/separator"
@@ -32,6 +33,21 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
import {
ScopeAdminRead,
ScopeAdminRoleRead,
ScopeBatchRead,
ScopeBillRead,
ScopeChannelRead,
ScopeCouponRead,
ScopeDiscountRead,
ScopePermissionRead,
ScopeProductRead,
ScopeResourceRead,
ScopeTradeRead,
ScopeUserRead,
ScopeUserReadOne,
} from "@/lib/scopes"
// Navigation Context
interface NavigationContextType {
@@ -78,13 +94,16 @@ interface NavItemProps {
href: string
icon: LucideIcon
label: string
requiredScope?: string
}
function NavItem({ href, icon: Icon, label }: NavItemProps) {
function NavItem({ href, icon: Icon, label, requiredScope }: NavItemProps) {
// console.log(requiredScope, "requiredScope")
const { collapsed, isActive } = useNavigation()
const active = isActive(href)
const linkContent = (
let linkContent = (
<Link
href={href}
className={`flex items-center ${
@@ -101,19 +120,25 @@ function NavItem({ href, icon: Icon, label }: NavItemProps) {
)
if (collapsed) {
return (
<li>
<Tooltip>
<TooltipTrigger asChild>{linkContent}</TooltipTrigger>
<TooltipContent side="right">
<p>{label}</p>
</TooltipContent>
</Tooltip>
</li>
linkContent = (
<Tooltip>
<TooltipTrigger asChild>{linkContent}</TooltipTrigger>
<TooltipContent side="right">
<p>{label}</p>
</TooltipContent>
</Tooltip>
)
}
return <li>{linkContent}</li>
if (requiredScope) {
linkContent = (
<Auth scope={requiredScope}>
<li>{linkContent}</li>
</Auth>
)
}
return linkContent
}
// NavSeparator Component
@@ -129,6 +154,109 @@ function NavSeparator() {
)
}
const menuSections: { title: string; items: NavItemProps[] }[] = [
{
title: "概览",
items: [
{ href: "/", icon: Home, label: "首页" },
{ href: "/statistics", icon: BarChart3, label: "数据统计" },
],
},
{
title: "客户",
items: [
{
href: "/user",
icon: Users,
label: "客户认领",
requiredScope: ScopeUserReadOne,
},
{
href: "/cust",
icon: ContactRound,
label: "客户管理",
requiredScope: ScopeUserRead,
},
{
href: "/trade",
icon: Activity,
label: "交易明细",
requiredScope: ScopeTradeRead,
},
{
href: "/billing",
icon: DollarSign,
label: "账单详情",
requiredScope: ScopeBillRead,
},
],
},
{
title: "运营",
items: [
{
href: "/product",
icon: ShoppingBag,
label: "产品管理",
requiredScope: ScopeProductRead,
},
{
href: "/discount",
icon: SquarePercent,
label: "折扣管理",
requiredScope: ScopeDiscountRead,
},
{
href: "/coupon",
icon: TicketPercent,
label: "优惠券",
requiredScope: ScopeCouponRead,
},
{
href: "/resources",
icon: Package,
label: "套餐管理",
requiredScope: ScopeResourceRead,
},
{
href: "/batch",
icon: ClipboardList,
label: "提取记录",
requiredScope: ScopeBatchRead,
},
{
href: "/channel",
icon: Code,
label: "IP管理",
requiredScope: ScopeChannelRead,
},
],
},
{
title: "系统",
items: [
{
href: "/admin",
icon: Shield,
label: "管理员",
requiredScope: ScopeAdminRead,
},
{
href: "/roles",
icon: KeyRound,
label: "角色列表",
requiredScope: ScopeAdminRoleRead,
},
{
href: "/permissions",
icon: Shield,
label: "权限列表",
requiredScope: ScopePermissionRead,
},
],
},
]
// Main Navigation Component
export default function Navigation() {
const [collapsed, setCollapsed] = useState(false)
@@ -168,56 +296,16 @@ export default function Navigation() {
{/* Navigation Menu */}
<ScrollArea className="flex-1 py-3">
<nav className="space-y-3">
{/* 概览 */}
<NavGroup title="概览">
<NavItem href="/" icon={Home} label="首页" />
<NavItem href="/statistics" icon={BarChart3} label="数据统计" />
</NavGroup>
{/*<NavSeparator />*/}
{/* IP 资源 */}
{/*<NavGroup title="IP 资源">
<NavItem href="/proxy/nodes" icon={Globe} label="节点列表" />
<NavItem href="/proxy/pools" icon={Server} label="IP池管理" />
</NavGroup>*/}
<NavSeparator />
{/* 客户 */}
<NavGroup title="客户">
<NavItem href="/user" icon={Users} label="客户认领" />
<NavItem href="/cust" icon={ContactRound} label="客户管理" />
<NavItem href="/trade" icon={Activity} label="交易明细" />
<NavItem href="/billing" icon={DollarSign} label="账单详情" />
</NavGroup>
<NavSeparator />
{/* 运营 */}
<NavGroup title="运营">
<NavItem href="/product" icon={ShoppingBag} label="产品管理" />
<NavItem
href="/discount"
icon={SquarePercent}
label="折扣管理"
/>
<NavItem href="/coupon" icon={TicketPercent} label="优惠券" />
<NavItem href="/resources" icon={Package} label="套餐管理" />
<NavItem href="/batch" icon={ClipboardList} label="提取记录" />
<NavItem href="/channel" icon={Code} label="IP管理" />
</NavGroup>
<NavSeparator />
{/* 系统 */}
<NavGroup title="系统">
{/*<NavItem href="/settings" icon={Settings} label="系统设置" />*/}
<NavItem href="/admin" icon={Shield} label="管理员" />
<NavItem href="/roles" icon={KeyRound} label="角色列表" />
<NavItem href="/permissions" icon={Shield} label="权限列表" />
{/*<NavItem href="/logs" icon={FileText} label="系统日志" />*/}
</NavGroup>
{menuSections.map((section, idx) => (
<div key={section.title}>
<NavGroup title={section.title}>
{section.items.map(item => (
<NavItem key={item.label} {...item} />
))}
</NavGroup>
{idx !== menuSections.length - 1 && <NavSeparator />}
</div>
))}
</nav>
</ScrollArea>

View File

@@ -3,9 +3,10 @@ import { zodResolver } from "@hookform/resolvers/zod"
import { format } from "date-fns"
import { Suspense, useCallback, useState } from "react"
import { Controller, useForm } from "react-hook-form"
import { toast } from "sonner"
import { z } from "zod"
import { bindAdmin, getPageUsers } from "@/actions/user"
import { DataTable, useDataTable } from "@/components/data-table"
import { bindAdmin, getPageUser } from "@/actions/user"
import { DataTable } from "@/components/data-table"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
@@ -15,66 +16,84 @@ import {
FieldLabel,
} from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { useFetch } from "@/hooks/data"
import type { User } from "@/models/user"
type FilterValues = {
interface UserQueryParams {
account?: string
name?: string
identified?: boolean
enabled?: boolean
assigned?: boolean
}
const filterSchema = z.object({
account: z.string().optional(),
name: z.string().optional(),
identified: z.string().optional(),
enabled: z.string().optional(),
assigned: z.string().optional(),
})
type FormValues = z.infer<typeof filterSchema>
export default function UserPage() {
const [filters, setFilters] = useState<FilterValues>({})
const [userList, setUserList] = useState<User[]>([])
const [loading, setLoading] = useState(false)
const { control, handleSubmit, reset } = useForm<FormValues>({
resolver: zodResolver(filterSchema),
defaultValues: {
account: "",
name: "",
identified: "all",
enabled: "all",
assigned: "all",
},
})
const fetchUsers = useCallback(
(page: number, size: number) => getPageUsers({ page, size, ...filters }),
[filters],
const fetchUsers = useCallback(async (filters: UserQueryParams = {}) => {
setLoading(true)
try {
const res = await getPageUser(filters)
console.log(res, "res")
if (res.success) {
const req = [{ ...res.data }]
setUserList(req)
} else {
toast.error(res.message || "获取用户失败")
setUserList([])
}
} catch (error) {
const message = error instanceof Error ? error.message : error
toast.error(`获取管理员失败: ${message}`)
} finally {
setLoading(false)
}
}, [])
const bind = useCallback(
async (id: number) => {
try {
const res = await bindAdmin({ id })
if (res.success) {
toast.success("用户已认领")
fetchUsers()
} else {
toast.error(res.message || "认领失败")
}
} catch (error) {
const message = error instanceof Error ? error.message : error
toast.error(`认领请求失败: ${message}`)
}
},
[fetchUsers],
)
const table = useDataTable<User>(fetchUsers)
const bind = useFetch(table, (id: number) => bindAdmin({ id }), {
done: "用户已认领",
fail: "用户认领失败",
})
const onFilter = handleSubmit((data: FormValues) => {
const params: UserQueryParams = {}
const onFilter = handleSubmit(data => {
const result: FilterValues = {}
if (data.account) result.account = data.account
if (data.name) result.name = data.name
if (data.identified && data.identified !== "all")
result.identified = data.identified === "1"
if (data.enabled && data.enabled !== "all")
result.enabled = data.enabled === "1"
if (data.assigned && data.assigned !== "all")
result.assigned = data.assigned === "has"
setFilters(result)
table.pagination.onPageChange(1)
if (data.account?.trim()) params.account = data.account.trim()
if (data.name?.trim()) params.name = data.name.trim()
const hasValidFilter = Object.keys(params).length > 0
if (hasValidFilter) {
fetchUsers(params)
} else {
setUserList([])
setLoading(false)
toast.info("请输入筛选条件后再查询")
}
})
return (
@@ -110,69 +129,6 @@ export default function UserPage() {
</Field>
)}
/>
{/* <Controller
name="identified"
control={control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid} className="w-24">
<FieldLabel>实名状态</FieldLabel>
<Select value={field.value} onValueChange={field.onChange}>
<SelectTrigger>
<SelectValue placeholder="全部" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">全部</SelectItem>
<SelectItem value="1">已认证</SelectItem>
<SelectItem value="0">未认证</SelectItem>
</SelectContent>
</Select>
<FieldError>{fieldState.error?.message}</FieldError>
</Field>
)}
/>
<Controller
name="enabled"
control={control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid} className="w-24">
<FieldLabel>账号状态</FieldLabel>
<Select value={field.value} onValueChange={field.onChange}>
<SelectTrigger>
<SelectValue placeholder="全部" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">全部</SelectItem>
<SelectItem value="1">正常</SelectItem>
<SelectItem value="0">禁用</SelectItem>
</SelectContent>
</Select>
<FieldError>{fieldState.error?.message}</FieldError>
</Field>
)}
/>
<Controller
name="assigned"
control={control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid} className="w-24">
<FieldLabel>认领状态</FieldLabel>
<Select value={field.value} onValueChange={field.onChange}>
<SelectTrigger>
<SelectValue placeholder="全部" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">全部</SelectItem>
<SelectItem value="has">已认领</SelectItem>
<SelectItem value="none">未认领</SelectItem>
</SelectContent>
</Select>
<FieldError>{fieldState.error?.message}</FieldError>
</Field>
)}
/> */}
</div>
<FieldGroup className="flex-row justify-start mt-4 gap-2">
@@ -181,15 +137,9 @@ export default function UserPage() {
type="button"
variant="outline"
onClick={() => {
reset({
account: "",
name: "",
identified: "all",
enabled: "all",
assigned: "all",
})
setFilters({})
table.pagination.onPageChange(1)
reset()
setUserList([])
setLoading(false)
}}
>
@@ -199,7 +149,8 @@ export default function UserPage() {
<Suspense>
<DataTable<User>
{...table}
data={userList || []}
status={loading ? "load" : "done"}
columns={[
{ header: "ID", accessorKey: "id" },
{ header: "账号", accessorKey: "username" },

View File

@@ -23,7 +23,7 @@ export type DataTableProps<T> = {
data: T[]
status: "load" | "done" | "fail"
columns: ColumnDef<T>[]
pagination: PaginationProps
pagination?: PaginationProps
classNames?: {
root?: string
headRow?: string
@@ -39,9 +39,9 @@ export function DataTable<T extends Record<string, unknown>>(
columns: props.columns,
getCoreRowModel: getCoreRowModel(),
manualPagination: true,
rowCount: props.pagination.total,
rowCount: props.pagination?.total,
state: {
pagination: {
pagination: props.pagination && {
pageIndex: props.pagination.page,
pageSize: props.pagination.size,
},
@@ -160,7 +160,7 @@ export function DataTable<T extends Record<string, unknown>>(
</div>
{/* 分页器 */}
<Pagination {...props.pagination} />
{props.pagination && <Pagination {...props.pagination} />}
</div>
)
}

View File

@@ -1,65 +1,67 @@
// 权限
export const ScopePermission = "permission";
export const ScopePermissionRead = "permission:read"; // 读取权限列表
export const ScopePermissionWrite = "permission:write"; // 写入权限
export const ScopePermission = "permission"
export const ScopePermissionRead = "permission:read" // 读取权限列表
export const ScopePermissionWrite = "permission:write" // 写入权限
// 管理员角色
export const ScopeAdminRole = "admin_role";
export const ScopeAdminRoleRead = "admin_role:read"; // 读取管理员角色列表
export const ScopeAdminRoleWrite = "admin_role:write"; // 写入管理员角色
export const ScopeAdminRole = "admin_role"
export const ScopeAdminRoleRead = "admin_role:read" // 读取管理员角色列表
export const ScopeAdminRoleWrite = "admin_role:write" // 写入管理员角色
// 管理员
export const ScopeAdmin = "admin";
export const ScopeAdminRead = "admin:read"; // 读取管理员列表
export const ScopeAdminWrite = "admin:write"; // 写入管理员
export const ScopeAdmin = "admin"
export const ScopeAdminRead = "admin:read" // 读取管理员列表
export const ScopeAdminWrite = "admin:write" // 写入管理员
// 产品
export const ScopeProduct = "product";
export const ScopeProductRead = "product:read"; // 读取产品列表
export const ScopeProductWrite = "product:write"; // 写入产品
export const ScopeProduct = "product"
export const ScopeProductRead = "product:read" // 读取产品列表
export const ScopeProductWrite = "product:write" // 写入产品
// 产品套餐
export const ScopeProductSku = "product_sku";
export const ScopeProductSkuRead = "product_sku:read"; // 读取产品套餐列表
export const ScopeProductSkuWrite = "product_sku:write"; // 写入产品套餐
export const ScopeProductSku = "product_sku"
export const ScopeProductSkuRead = "product_sku:read" // 读取产品套餐列表
export const ScopeProductSkuWrite = "product_sku:write" // 写入产品套餐
// 折扣
export const ScopeDiscount = "discount";
export const ScopeDiscountRead = "discount:read"; // 读取折扣列表
export const ScopeDiscountWrite = "discount:write"; // 写入折扣
export const ScopeDiscount = "discount"
export const ScopeDiscountRead = "discount:read" // 读取折扣列表
export const ScopeDiscountWrite = "discount:write" // 写入折扣
// 用户套餐
export const ScopeResource = "resource";
export const ScopeResourceRead = "resource:read"; // 读取用户套餐列表
export const ScopeResourceWrite = "resource:write"; // 写入用户套餐
export const ScopeResource = "resource"
export const ScopeResourceRead = "resource:read" // 读取用户套餐列表
export const ScopeResourceWrite = "resource:write" // 写入用户套餐
// 用户
export const ScopeUser = "user";
export const ScopeUserRead = "user:read"; // 读取用户列表
export const ScopeUserWrite = "user:write"; // 写入用户
export const ScopeUserWriteBalance = "user:write:balance"; // 写入用户余额
export const ScopeUser = "user"
export const ScopeUserRead = "user:read" // 读取用户列表
export const ScopeUserWrite = "user:write" // 写入用户
export const ScopeUserWriteBalance = "user:write:balance" // 写入用户余额
export const ScopeUserReadOne = "user:read:one" // 读取单个用户
export const ScopeUserWriteBind = "user:write:bind" // 认领用户
// 优惠券
export const ScopeCoupon = "coupon";
export const ScopeCouponRead = "coupon:read"; // 读取优惠券列表
export const ScopeCouponWrite = "coupon:write"; // 写入优惠券
export const ScopeCoupon = "coupon"
export const ScopeCouponRead = "coupon:read" // 读取优惠券列表
export const ScopeCouponWrite = "coupon:write" // 写入优惠券
// 批次
export const ScopeBatch = "batch";
export const ScopeBatchRead = "batch:read"; // 读取批次列表
export const ScopeBatchWrite = "batch:write"; // 写入批次
export const ScopeBatch = "batch"
export const ScopeBatchRead = "batch:read" // 读取批次列表
export const ScopeBatchWrite = "batch:write" // 写入批次
// IP
export const ScopeChannel = "channel";
export const ScopeChannelRead = "channel:read"; // 读取 IP 列表
export const ScopeChannelWrite = "channel:write"; // 写入 IP
export const ScopeChannel = "channel"
export const ScopeChannelRead = "channel:read" // 读取 IP 列表
export const ScopeChannelWrite = "channel:write" // 写入 IP
// 交易
export const ScopeTrade = "trade";
export const ScopeTradeRead = "trade:read"; // 读取交易列表
export const ScopeTradeWrite = "trade:write"; // 写入交易
export const ScopeTrade = "trade"
export const ScopeTradeRead = "trade:read" // 读取交易列表
export const ScopeTradeWrite = "trade:write" // 写入交易
// 账单
export const ScopeBill = "bill";
export const ScopeBillRead = "bill:read"; // 读取账单列表
export const ScopeBillWrite = "bill:write"; // 写入账单
export const ScopeBill = "bill"
export const ScopeBillRead = "bill:read" // 读取账单列表
export const ScopeBillWrite = "bill:write" // 写入账单