mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-04 07:06:36 +08:00
裁剪
swagger 移除 merchant swagger 改造首页内容为通用后台管理系统定位 - 修改首页标题从 'LINUX DO Credit' 改为 'Modern Platform' - 更新副标题为 '为二次开发而生' - 更新首页描述为通用平台的特点 - 更新首页特性标签为 '开箱即用、高度可扩展、工业级基建' - 修改展示卡片为技术栈和二次开发相关 - 更新开发者示例代码为通用的注册和 API Key 获取示例 - 更新页脚品牌名为 'Modern Platform' - 调整页脚导航链接为通用平台相关内容 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> 去除遗留 裁剪 移除 /api/v1/user/pay-key 相关代码 - 删除后端 UpdatePayKey 处理器函数和 UpdatePayKeyRequest 结构体 - 删除 User 模型中的 PayKey 字段 - 删除 User.VerifyPayKey 方法 - 删除 EncryptPayKeyFailed 错误常量 - 删除 /api/v1/user/pay-key PUT 路由 - 删除 OAuth 返回中的 IsPayKey 字段 - 删除前端 UserService.updatePayKey 方法 - 删除前端所有支付密钥 UI 和逻辑 - 更新相关的导出和注释 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> 去除遗留 api 修正 系统配置 前端裁剪 后端裁剪 init
This commit is contained in:
@@ -1,378 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { ManagePage, ManageDetailPanel } from "@/components/common/general/manage-pannel"
|
||||
import { Input } from "@/components/ui/input"
|
||||
|
||||
import { formatDateTime } from "@/lib/utils"
|
||||
import type { UserPayConfig } from "@/lib/services"
|
||||
import { useAdmin } from "@/contexts/admin-context"
|
||||
|
||||
|
||||
/**
|
||||
* 积分配置详情面板组件
|
||||
* 显示积分配置的详细信息和编辑面板
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <PayConfigDetailPanel
|
||||
* config={config}
|
||||
* editData={editData}
|
||||
* onEditDataChange={onEditDataChange}
|
||||
* onSave={onSave}
|
||||
* saving={saving}
|
||||
* />
|
||||
* ```
|
||||
* @param {UserPayConfig} config - 积分配置
|
||||
* @param {Partial<UserPayConfig>} editData - 编辑数据
|
||||
* @param {function} onEditDataChange - 编辑数据改变回调
|
||||
* @param {function} onSave - 保存回调
|
||||
* @param {boolean} saving - 是否正在保存
|
||||
* @returns {React.ReactNode} 积分配置详情面板组件
|
||||
*/
|
||||
function PayConfigDetailPanel({
|
||||
config,
|
||||
editData,
|
||||
onEditDataChange,
|
||||
onSave,
|
||||
saving
|
||||
}: {
|
||||
config: UserPayConfig | null
|
||||
editData: Partial<UserPayConfig>
|
||||
onEditDataChange: (field: keyof UserPayConfig, value: UserPayConfig[keyof UserPayConfig]) => void
|
||||
onSave: () => void
|
||||
saving: boolean
|
||||
}) {
|
||||
|
||||
|
||||
return (
|
||||
<ManageDetailPanel
|
||||
isEmpty={!config}
|
||||
onSave={onSave}
|
||||
saving={saving}
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-0">
|
||||
<div className="border border-dashed rounded-l-lg">
|
||||
<div className="px-3 py-2 flex items-center justify-between border-b border-dashed last:border-b-0">
|
||||
<label className="text-xs font-medium text-muted-foreground">配置等级</label>
|
||||
<p className="text-xs text-muted-foreground">Level {config?.level}</p>
|
||||
</div>
|
||||
|
||||
<div className="px-3 py-2 flex items-center justify-between border-b border-dashed last:border-b-0">
|
||||
<label className="text-xs font-medium text-muted-foreground">配置ID</label>
|
||||
<p className="text-xs text-muted-foreground">{config?.id}</p>
|
||||
</div>
|
||||
|
||||
<div className="px-3 py-2 flex items-center justify-between border-b border-dashed last:border-b-0">
|
||||
<label className="text-xs font-medium text-muted-foreground">创建时间</label>
|
||||
<p className="text-xs text-muted-foreground">{config ? formatDateTime(config.created_at) : ''}</p>
|
||||
</div>
|
||||
|
||||
<div className="px-3 py-2 flex items-center justify-between border-b border-dashed">
|
||||
<label className="text-xs font-medium text-muted-foreground">更新时间</label>
|
||||
<p className="text-xs text-muted-foreground">{config ? formatDateTime(config.updated_at) : ''}</p>
|
||||
</div>
|
||||
|
||||
<div className="px-3 py-2 flex items-center justify-between border-b border-dashed last:border-b-0" aria-hidden="true">
|
||||
<span className="text-xs text-muted-foreground"> </span>
|
||||
<span className="text-xs text-muted-foreground"> </span>
|
||||
</div>
|
||||
|
||||
<div className="px-3 py-2 flex items-center justify-between border-b border-dashed last:border-b-0" aria-hidden="true">
|
||||
<span className="text-xs text-muted-foreground"> </span>
|
||||
<span className="text-xs text-muted-foreground"> </span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border border-dashed rounded-r-lg border-l-0">
|
||||
<div className="px-3 py-2 flex items-center justify-between border-b border-dashed last:border-b-0">
|
||||
<label className="text-xs font-medium text-muted-foreground">最低分数</label>
|
||||
<div className="flex items-center gap-1">
|
||||
<Input
|
||||
type="number"
|
||||
step="1"
|
||||
min="0"
|
||||
value={editData.min_score !== undefined ? editData.min_score.toString() : (config?.min_score?.toString() || '')}
|
||||
placeholder={editData.min_score === undefined && !config?.min_score ? '必需' : ''}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
if (value === '') {
|
||||
onEditDataChange('min_score', 0)
|
||||
return
|
||||
}
|
||||
|
||||
const numValue = parseInt(value)
|
||||
if (isNaN(numValue)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (numValue >= 0) {
|
||||
onEditDataChange('min_score', numValue)
|
||||
}
|
||||
}}
|
||||
className="text-xs text-right h-4 w-16 px-0 rounded-none border-none shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 placeholder:text-[12px]"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">LDC</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-3 py-2 flex items-center justify-between border-b border-dashed last:border-b-0">
|
||||
<label className="text-xs font-medium text-muted-foreground">最高分数</label>
|
||||
<div className="flex items-center gap-1">
|
||||
<Input
|
||||
type="number"
|
||||
step="1"
|
||||
min="0"
|
||||
value={editData.max_score !== undefined ? (editData.max_score?.toString() || '') : (config?.max_score?.toString() || '')}
|
||||
placeholder={(editData.max_score === null || editData.max_score === undefined) && (config?.max_score === null || config?.max_score === undefined) ? '无限制' : ''}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
if (value === '') {
|
||||
onEditDataChange('max_score', null)
|
||||
return
|
||||
}
|
||||
|
||||
const numValue = parseInt(value)
|
||||
if (isNaN(numValue)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (numValue >= 0) {
|
||||
onEditDataChange('max_score', numValue)
|
||||
}
|
||||
}}
|
||||
className="text-xs text-right h-4 w-16 px-0 rounded-none border-none shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 placeholder:text-[12px]"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">LDC</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-3 py-2 flex items-center justify-between border-b border-dashed last:border-b-0">
|
||||
<label className="text-xs font-medium text-muted-foreground">手续费率</label>
|
||||
<div className="flex items-right gap-1">
|
||||
<Input
|
||||
type="number"
|
||||
step="1"
|
||||
min="0"
|
||||
max="100"
|
||||
value={
|
||||
editData.fee_rate !== undefined
|
||||
? (parseFloat(editData.fee_rate.toString()) * 100).toString()
|
||||
: (config?.fee_rate ? (parseFloat(config.fee_rate.toString()) * 100).toString() : '')
|
||||
}
|
||||
placeholder={editData.fee_rate === undefined && !config?.fee_rate ? '必需' : ''}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
if (value === '') {
|
||||
onEditDataChange('fee_rate', '0')
|
||||
return
|
||||
}
|
||||
|
||||
const numValue = parseInt(value)
|
||||
if (isNaN(numValue)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (numValue >= 0 && numValue <= 100) {
|
||||
onEditDataChange('fee_rate', (numValue / 100).toString())
|
||||
}
|
||||
}}
|
||||
className="text-xs text-right h-4 w-16 px-0 mr-3 rounded-none border-none shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 placeholder:text-[12px]"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">%</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-3 py-2 flex items-center justify-between border-b border-dashed last:border-b-0">
|
||||
<label className="text-xs font-medium text-muted-foreground">分数转化率</label>
|
||||
<div className="flex items-right gap-1">
|
||||
<Input
|
||||
type="number"
|
||||
step="1"
|
||||
min="0"
|
||||
max="100"
|
||||
value={
|
||||
editData.score_rate !== undefined
|
||||
? (parseFloat(editData.score_rate.toString()) * 100).toString()
|
||||
: (config?.score_rate ? (parseFloat(config.score_rate.toString()) * 100).toString() : '')
|
||||
}
|
||||
placeholder={editData.score_rate === undefined && !config?.score_rate ? '必需' : ''}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
if (value === '') {
|
||||
onEditDataChange('score_rate', '0')
|
||||
return
|
||||
}
|
||||
|
||||
const numValue = parseInt(value)
|
||||
if (isNaN(numValue)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (numValue >= 0 && numValue <= 100) {
|
||||
onEditDataChange('score_rate', (numValue / 100).toString())
|
||||
}
|
||||
}}
|
||||
className="text-xs text-right h-4 w-16 px-0 mr-3 rounded-none border-none shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 placeholder:text-[12px]"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">%</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-3 py-2 flex items-center justify-between border-b border-dashed last:border-b-0">
|
||||
<label className="text-xs font-medium text-muted-foreground">分发费率</label>
|
||||
<div className="flex items-right gap-1">
|
||||
<Input
|
||||
type="number"
|
||||
step="1"
|
||||
min="0"
|
||||
max="100"
|
||||
value={
|
||||
editData.distribute_rate !== undefined
|
||||
? (parseFloat(editData.distribute_rate.toString()) * 100).toString()
|
||||
: (config?.distribute_rate ? (parseFloat(config.distribute_rate.toString()) * 100).toString() : '')
|
||||
}
|
||||
placeholder={editData.distribute_rate === undefined && !config?.distribute_rate ? '必需' : ''}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
if (value === '') {
|
||||
onEditDataChange('distribute_rate', '0')
|
||||
return
|
||||
}
|
||||
|
||||
const numValue = parseInt(value)
|
||||
if (isNaN(numValue)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (numValue >= 0 && numValue <= 100) {
|
||||
onEditDataChange('distribute_rate', (numValue / 100).toString())
|
||||
}
|
||||
}}
|
||||
className="text-xs text-right h-4 w-16 px-0 mr-3 rounded-none border-none shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 placeholder:text-[12px]"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">%</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-3 py-2 flex items-center justify-between">
|
||||
<label className="text-xs font-medium text-muted-foreground">每日支付上限</label>
|
||||
<div className="flex items-center gap-1">
|
||||
<Input
|
||||
type="number"
|
||||
step="1"
|
||||
min="0"
|
||||
value={editData.daily_limit !== undefined ? (editData.daily_limit?.toString() || '') : (config?.daily_limit?.toString() || '')}
|
||||
placeholder={(editData.daily_limit === null || editData.daily_limit === undefined) && (config?.daily_limit === null || config?.daily_limit === undefined) ? '无限制' : ''}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
if (value === '') {
|
||||
onEditDataChange('daily_limit', null)
|
||||
return
|
||||
}
|
||||
|
||||
const numValue = parseInt(value)
|
||||
if (isNaN(numValue)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (numValue >= 0) {
|
||||
onEditDataChange('daily_limit', numValue)
|
||||
}
|
||||
}}
|
||||
className="text-xs text-right h-4 w-16 px-0 rounded-none border-none shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 placeholder:text-[12px]"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">LDC</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ManageDetailPanel>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 用户积分配置管理组件
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <UserPayConfigs />
|
||||
* ```
|
||||
* @returns {React.ReactNode} 用户积分配置管理组件
|
||||
*/
|
||||
export function UserPayConfigs() {
|
||||
const {
|
||||
userPayConfigs: configs,
|
||||
userPayConfigsLoading: loading,
|
||||
userPayConfigsError: error,
|
||||
refetchUserPayConfigs,
|
||||
updateUserPayConfig,
|
||||
deleteUserPayConfig
|
||||
} = useAdmin()
|
||||
|
||||
const getInitialEditData = (config: UserPayConfig) => ({
|
||||
min_score: config.min_score,
|
||||
max_score: config.max_score,
|
||||
daily_limit: config.daily_limit,
|
||||
fee_rate: config.fee_rate.toString(),
|
||||
score_rate: config.score_rate.toString(),
|
||||
distribute_rate: config.distribute_rate.toString(),
|
||||
})
|
||||
|
||||
const handleSave = async (config: UserPayConfig, editData: Partial<UserPayConfig>) => {
|
||||
if (!config) return
|
||||
|
||||
await updateUserPayConfig(config.id, {
|
||||
min_score: editData.min_score ?? config.min_score,
|
||||
max_score: editData.max_score,
|
||||
daily_limit: editData.daily_limit,
|
||||
fee_rate: editData.fee_rate?.toString() ?? config.fee_rate.toString(),
|
||||
score_rate: editData.score_rate?.toString() ?? config.score_rate.toString(),
|
||||
distribute_rate: editData.distribute_rate?.toString() ?? config.distribute_rate.toString(),
|
||||
})
|
||||
await refetchUserPayConfigs()
|
||||
}
|
||||
|
||||
const handleDelete = async (config: UserPayConfig) => {
|
||||
await deleteUserPayConfig(config.id)
|
||||
}
|
||||
|
||||
return (
|
||||
<ManagePage<UserPayConfig>
|
||||
title="积分配置"
|
||||
data={configs}
|
||||
loading={loading}
|
||||
error={error}
|
||||
onReload={refetchUserPayConfigs}
|
||||
getInitialEditData={getInitialEditData}
|
||||
onSave={handleSave}
|
||||
onDelete={handleDelete}
|
||||
getId={(config) => config.id}
|
||||
emptyDescription="未发现积分配置"
|
||||
loadingDescription="配置加载中"
|
||||
columns={[
|
||||
{ header: "等级", cell: (item) => <span className="font-medium">Level {item.level}</span>, width: "min-w-[80px]", align: "left" },
|
||||
{ header: "最低分", cell: (item) => item.min_score, width: "min-w-[200px]", align: "left" },
|
||||
{ header: "最高分", cell: (item) => item.max_score || "无限制", width: "min-w-[200px]", align: "left" },
|
||||
{ header: "每日限额", cell: (item) => item.daily_limit ? `LDC ${ item.daily_limit.toLocaleString() }` : "无限制", width: "min-w-[200px]", align: "left" },
|
||||
{ header: "费率", cell: (item) => `${ (Number(item.fee_rate) * 100).toFixed(2) }%`, width: "min-w-[200px]", align: "left" },
|
||||
{ header: "分数转化率", cell: (item) => `${ (Number(item.score_rate) * 100).toFixed(2) }%`, width: "min-w-[200px]", align: "left" },
|
||||
{ header: "分发费率", cell: (item) => `${ (Number(item.distribute_rate) * 100).toFixed(2) }%`, width: "min-w-[200px]", align: "left" },
|
||||
{ header: "更新时间", cell: (item) => <span className="text-muted-foreground">{formatDateTime(item.updated_at)}</span>, width: "min-w-[200px]", align: "left" },
|
||||
]}
|
||||
renderDetail={({ selected, hovered, editData, onEditDataChange, onSave, saving }) => (
|
||||
<PayConfigDetailPanel
|
||||
config={selected || hovered}
|
||||
editData={editData}
|
||||
onEditDataChange={onEditDataChange}
|
||||
onSave={onSave}
|
||||
saving={saving}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import * as React from "react"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { ManagePage, ManageDetailPanel } from "@/components/common/general/manage-pannel"
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
|
||||
import { formatDateTime } from "@/lib/utils"
|
||||
import type { SystemConfig } from "@/lib/services"
|
||||
@@ -70,7 +71,22 @@ function SystemConfigDetailPanel({
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
type="number"
|
||||
type={
|
||||
config?.key && (
|
||||
config.key.endsWith('_limit') ||
|
||||
config.key.endsWith('_minutes') ||
|
||||
config.key.endsWith('_days') ||
|
||||
config.key.endsWith('_seconds') ||
|
||||
config.key.endsWith('_hours') ||
|
||||
config.key.endsWith('_ttl') ||
|
||||
config.key.endsWith('_max') ||
|
||||
config.key.endsWith('_min') ||
|
||||
config.key.includes('max_') ||
|
||||
config.key.includes('min_') ||
|
||||
config.key.includes('limit') ||
|
||||
config.key.includes('count')
|
||||
) ? "number" : "text"
|
||||
}
|
||||
step="1"
|
||||
min="0"
|
||||
value={editData.value !== undefined ? editData.value : (config?.value || '')}
|
||||
@@ -108,6 +124,17 @@ function SystemConfigDetailPanel({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-3 py-2 flex items-center justify-between border-b border-dashed last:border-b-0">
|
||||
<label className="text-xs font-medium text-muted-foreground">配置类型</label>
|
||||
<span className={`text-[11px] px-1.5 py-0.5 rounded font-medium ${
|
||||
config?.type === 'system'
|
||||
? 'bg-blue-500/10 text-blue-600 dark:bg-blue-500/20 dark:text-blue-400'
|
||||
: 'bg-indigo-500/10 text-indigo-600 dark:bg-indigo-500/20 dark:text-indigo-400'
|
||||
}`}>
|
||||
{config?.type === 'system' ? '系统配置' : '业务配置'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="px-3 py-2 flex items-center justify-between">
|
||||
<label className="text-xs font-medium text-muted-foreground">创建时间</label>
|
||||
<p className="text-xs text-muted-foreground">{config ? formatDateTime(config.created_at) : ''}</p>
|
||||
@@ -139,6 +166,12 @@ export function SystemConfigs() {
|
||||
deleteSystemConfig
|
||||
} = useAdmin()
|
||||
|
||||
const [activeTab, setActiveTab] = React.useState<'system' | 'business'>('business')
|
||||
|
||||
React.useEffect(() => {
|
||||
refetchSystemConfigs(activeTab)
|
||||
}, [activeTab, refetchSystemConfigs])
|
||||
|
||||
const getInitialEditData = (config: SystemConfig) => ({
|
||||
value: config.value,
|
||||
description: config.description
|
||||
@@ -151,7 +184,6 @@ export function SystemConfigs() {
|
||||
value: editData.value ?? config.value,
|
||||
description: editData.description ?? config.description
|
||||
})
|
||||
await refetchSystemConfigs()
|
||||
}
|
||||
|
||||
const handleDelete = async (config: SystemConfig) => {
|
||||
@@ -164,13 +196,21 @@ export function SystemConfigs() {
|
||||
data={configs}
|
||||
loading={loading}
|
||||
error={error}
|
||||
onReload={refetchSystemConfigs}
|
||||
onReload={() => refetchSystemConfigs(activeTab)}
|
||||
getInitialEditData={getInitialEditData}
|
||||
onSave={handleSave}
|
||||
onDelete={handleDelete}
|
||||
getId={(config) => config.key}
|
||||
emptyDescription="未发现系统配置"
|
||||
loadingDescription="配置加载中"
|
||||
headerExtra={
|
||||
<Tabs value={activeTab} onValueChange={(val) => setActiveTab(val as 'system' | 'business')} className="w-[180px]">
|
||||
<TabsList className="grid w-full grid-cols-2 h-8">
|
||||
<TabsTrigger value="business" className="text-[11px] h-7">业务配置</TabsTrigger>
|
||||
<TabsTrigger value="system" className="text-[11px] h-7">系统配置</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
}
|
||||
columns={[
|
||||
{ header: "配置键", cell: (item) => <span className="font-mono font-medium">{item.key}</span>, width: "200px" },
|
||||
{ header: "配置值", cell: (item) => <span className="truncate max-w-[120px] inline-block" title={item.value}>{item.value}</span>, width: "120px" },
|
||||
|
||||
@@ -8,10 +8,10 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@
|
||||
import { Sheet, SheetTitle, SheetContent } from "@/components/ui/sheet"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
import { Layers, Search, UserX, UserCheck, Eye, Wallet, CreditCard, ShieldCheck, Filter, X, ChevronDown, ChevronLeft, ChevronRight, Users, Loader2 } from "lucide-react"
|
||||
import { Layers, Search, UserX, UserCheck, Eye, ShieldCheck, Filter, X, ChevronDown, ChevronLeft, ChevronRight, Loader2 } from "lucide-react"
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
|
||||
|
||||
import { AdminUser, AdminService, DispatchTaskRequest } from "@/lib/services"
|
||||
import { AdminUser, AdminService } from "@/lib/services"
|
||||
import { toast } from "sonner"
|
||||
import { formatDateTime } from "@/lib/utils"
|
||||
import { EmptyStateWithBorder } from "@/components/layout/empty"
|
||||
@@ -43,7 +43,6 @@ export function UsersManager() {
|
||||
|
||||
const [selectedUser, setSelectedUser] = useState<AdminUser | null>(null)
|
||||
const [detailOpen, setDetailOpen] = useState(false)
|
||||
const [updatingUserId, setUpdatingUserId] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
fetchUsers()
|
||||
@@ -62,25 +61,7 @@ export function UsersManager() {
|
||||
setDetailOpen(true)
|
||||
}
|
||||
|
||||
const handleUpdateCredits = async (user: AdminUser) => {
|
||||
try {
|
||||
setUpdatingUserId(user.id)
|
||||
const params: DispatchTaskRequest = {
|
||||
task_type: 'user_gamification',
|
||||
user_id: user.id
|
||||
}
|
||||
await AdminService.dispatchTask(params)
|
||||
toast.success('积分更新任务已下发', {
|
||||
description: `正在更新用户 ${ user.username } 的积分数据`
|
||||
})
|
||||
} catch (err) {
|
||||
toast.error('任务下发失败', {
|
||||
description: err instanceof Error ? err.message : '未知错误'
|
||||
})
|
||||
} finally {
|
||||
setUpdatingUserId(null)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const totalPages = Math.ceil(total / pageSize)
|
||||
const hasSearchFilter = Boolean(searchUserId || searchUsername)
|
||||
@@ -315,12 +296,7 @@ export function UsersManager() {
|
||||
<TableRow className="border-b border-dashed hover:bg-transparent">
|
||||
<TableHead className="w-[90px] whitespace-nowrap py-2 h-8">ID</TableHead>
|
||||
<TableHead className="w-[120px] whitespace-nowrap py-2 h-8">用户</TableHead>
|
||||
<TableHead className="text-right whitespace-nowrap min-w-[80px] py-2 h-8 font-mono">余额</TableHead>
|
||||
<TableHead className="text-right whitespace-nowrap min-w-[80px] py-2 h-8 font-mono">基准分</TableHead>
|
||||
<TableHead className="text-right whitespace-nowrap min-w-[80px] py-2 h-8 font-mono">支付分</TableHead>
|
||||
<TableHead className="text-right whitespace-nowrap min-w-[80px] py-2 h-8 font-mono">总收益</TableHead>
|
||||
<TableHead className="text-right whitespace-nowrap min-w-[80px] py-2 h-8 font-mono">总消耗</TableHead>
|
||||
<TableHead className="text-right whitespace-nowrap min-w-[80px] py-2 h-8 font-mono is-last-money">总划转</TableHead>
|
||||
|
||||
<TableHead className="whitespace-nowrap min-w-[140px] py-2 h-8 pl-4">上次登陆</TableHead>
|
||||
<TableHead className="whitespace-nowrap min-w-[140px] py-2 h-8">注册时间</TableHead>
|
||||
<TableHead className="whitespace-nowrap min-w-[140px] py-2 h-8">上次更新</TableHead>
|
||||
@@ -358,24 +334,7 @@ export function UsersManager() {
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-right font-mono text-[11px] text-green-600 dark:text-green-500 font-medium py-1">
|
||||
{Number(user.available_balance).toFixed(2)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right font-mono text-[11px] py-1">
|
||||
{Number(user.community_balance).toFixed(2)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right font-mono text-[11px] py-1">
|
||||
{Number(user.pay_score).toFixed(2)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right font-mono text-[11px] text-green-600 py-1">
|
||||
{Number(user.total_receive).toFixed(2)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right font-mono text-[11px] text-red-500 py-1">
|
||||
{Number(user.total_payment).toFixed(2)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right font-mono text-[11px] text-purple-500 py-1 is-last-money">
|
||||
{Number(user.total_community).toFixed(2)}
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="text-[10px] text-muted-foreground font-mono whitespace-nowrap py-1 pl-4">
|
||||
{formatDateTime(user.last_login_at)}
|
||||
</TableCell>
|
||||
@@ -404,24 +363,7 @@ export function UsersManager() {
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 text-muted-foreground hover:text-foreground"
|
||||
onClick={() => handleUpdateCredits(user)}
|
||||
disabled={updatingUserId === user.id}
|
||||
>
|
||||
<Loader2 className={cn("size-3", updatingUserId === user.id && "animate-spin")} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" className="text-xs">
|
||||
更新积分
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
@@ -487,60 +429,7 @@ export function UsersManager() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-4 px-1">核心资产</h4>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<div className="p-3 rounded-xl border bg-card/50 hover:bg-card/80 transition-colors space-y-2">
|
||||
<div className="flex flex-col gap-1 text-muted-foreground">
|
||||
<Wallet className="w-4 h-4" />
|
||||
<span className="text-[10px] font-medium">可用余额</span>
|
||||
</div>
|
||||
<div className="text-lg font-mono font-bold tracking-tight text-foreground truncate">
|
||||
{Number(selectedUser.available_balance).toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 rounded-xl border bg-card/50 hover:bg-card/80 transition-colors space-y-2">
|
||||
<div className="flex flex-col gap-1 text-muted-foreground">
|
||||
<CreditCard className="w-4 h-4" />
|
||||
<span className="text-[10px] font-medium">支付积分</span>
|
||||
</div>
|
||||
<div className="text-lg font-mono font-bold tracking-tight text-foreground truncate">
|
||||
{Number(selectedUser.pay_score).toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 rounded-xl border bg-card/50 hover:bg-card/80 transition-colors space-y-2">
|
||||
<div className="flex flex-col gap-1 text-muted-foreground">
|
||||
<Users className="w-4 h-4" />
|
||||
<span className="text-[10px] font-medium">社区余额</span>
|
||||
</div>
|
||||
<div className="text-lg font-mono font-bold tracking-tight text-foreground truncate">
|
||||
{Number(selectedUser.community_balance).toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator className="mx-6 w-auto opacity-50" />
|
||||
|
||||
<div className="p-6 space-y-6">
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider px-1">财务概况</h4>
|
||||
<div className="rounded-lg border divide-y bg-background/50">
|
||||
<div className="flex items-center justify-between p-3.5">
|
||||
<span className="text-[10px]">累计收益</span>
|
||||
<span className="font-mono text-[10px] font-medium text-purple-600 dark:text-purple-400">+{Number(selectedUser.total_community).toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-3.5">
|
||||
<span className="text-[10px]">累计收入</span>
|
||||
<span className="font-mono text-[10px] font-medium text-green-600">+{Number(selectedUser.total_receive).toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-3.5">
|
||||
<span className="text-[10px]">累计支出</span>
|
||||
<span className="font-mono text-[10px] font-medium text-red-500">{Number(selectedUser.total_payment) === 0 ? '0.00' : `-${ Number(selectedUser.total_payment).toFixed(2) }`}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider px-1">系统记录</h4>
|
||||
<div className="rounded-lg border divide-y bg-background/50">
|
||||
|
||||
Reference in New Issue
Block a user