style: 平台管理端改为浅色主题,与企业端配色一致
This commit is contained in:
@@ -84,19 +84,19 @@ function PlatformRoute({ children }: { children: React.ReactNode }) {
|
||||
function PlatformLayout({ children }: { children: React.ReactNode }) {
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false)
|
||||
return (
|
||||
<div className="flex min-h-screen bg-slate-100">
|
||||
<div className="flex min-h-screen bg-[#f8f9fb]">
|
||||
<PlatformSidebar mobileOpen={sidebarOpen} onClose={() => setSidebarOpen(false)} />
|
||||
<div className="flex-1 flex flex-col min-w-0">
|
||||
<header className="h-14 bg-slate-950 border-b border-slate-800 flex items-center px-4 shrink-0">
|
||||
<header className="h-14 bg-white border-b border-gray-200 flex items-center px-4 shrink-0">
|
||||
<button
|
||||
className="md:hidden mr-3 text-slate-400"
|
||||
className="md:hidden mr-3 text-gray-400"
|
||||
onClick={() => setSidebarOpen(true)}
|
||||
aria-label="打开菜单"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
|
||||
</button>
|
||||
<span className="text-sm text-slate-400">平台管理后台</span>
|
||||
<span className="ml-2 px-2 py-0.5 rounded text-[10px] font-bold bg-amber-400 text-slate-950">ADMIN</span>
|
||||
<span className="text-sm text-gray-500">平台管理后台</span>
|
||||
<span className="ml-2 px-2 py-0.5 rounded text-[10px] font-bold bg-primary text-white">ADMIN</span>
|
||||
</header>
|
||||
<main className="flex-1 py-6 px-4 md:px-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* 平台管理端侧边栏导航
|
||||
* 深色主题,amber 主色,区别于企业端
|
||||
* 浅色主题,与企业端配色一致
|
||||
*/
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom'
|
||||
import { useState } from 'react'
|
||||
@@ -60,20 +60,20 @@ export default function PlatformSidebar({ mobileOpen, onClose }: { mobileOpen: b
|
||||
className={clsx(
|
||||
'fixed md:sticky top-0 left-0 z-50 md:z-auto',
|
||||
'w-56 h-screen flex-shrink-0',
|
||||
'bg-slate-950 border-r border-slate-800',
|
||||
'bg-white border-r border-gray-200',
|
||||
'flex flex-col',
|
||||
'transition-transform duration-200',
|
||||
mobileOpen ? 'translate-x-0' : '-translate-x-full md:translate-x-0',
|
||||
)}
|
||||
>
|
||||
{/* Logo 区 */}
|
||||
<div className="h-14 flex items-center gap-2 px-4 border-b border-slate-800 shrink-0">
|
||||
<div className="w-7 h-7 rounded-md bg-amber-400 flex items-center justify-center">
|
||||
<Shield className="w-4 h-4 text-slate-950" />
|
||||
<div className="h-14 flex items-center gap-2 px-4 border-b border-gray-200 shrink-0">
|
||||
<div className="w-7 h-7 rounded-md bg-primary flex items-center justify-center">
|
||||
<Shield className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-bold text-sm text-white">用工专家平台</div>
|
||||
<div className="text-[10px] text-amber-400 tracking-wider">ADMIN</div>
|
||||
<div className="font-bold text-sm text-gray-900">用工专家平台</div>
|
||||
<div className="text-[10px] text-primary tracking-wider">ADMIN</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -89,7 +89,7 @@ export default function PlatformSidebar({ mobileOpen, onClose }: { mobileOpen: b
|
||||
onClick={() => toggleGroup(group.title)}
|
||||
className={clsx(
|
||||
'flex items-center justify-between w-full px-2 py-1.5 text-xs font-medium rounded-md transition-colors',
|
||||
hasActiveItem ? 'text-slate-200' : 'text-slate-400 hover:text-slate-200',
|
||||
hasActiveItem ? 'text-gray-800' : 'text-gray-500 hover:text-gray-700',
|
||||
)}
|
||||
>
|
||||
<span>{group.title}</span>
|
||||
@@ -109,8 +109,8 @@ export default function PlatformSidebar({ mobileOpen, onClose }: { mobileOpen: b
|
||||
className={clsx(
|
||||
'flex items-center gap-2 px-2 py-1.5 rounded-md text-sm transition-colors',
|
||||
active
|
||||
? 'bg-amber-400/10 text-amber-400 font-medium'
|
||||
: 'text-slate-400 hover:bg-slate-800 hover:text-slate-200',
|
||||
? 'bg-primary/10 text-primary font-medium'
|
||||
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-800',
|
||||
)}
|
||||
>
|
||||
<Icon className="w-4 h-4 flex-shrink-0" />
|
||||
@@ -126,19 +126,19 @@ export default function PlatformSidebar({ mobileOpen, onClose }: { mobileOpen: b
|
||||
</nav>
|
||||
|
||||
{/* 底部用户区 */}
|
||||
<div className="border-t border-slate-800 p-3 shrink-0">
|
||||
<div className="border-t border-gray-200 p-3 shrink-0">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="w-8 h-8 rounded-full bg-slate-800 flex items-center justify-center text-amber-400 text-sm font-medium">
|
||||
<div className="w-8 h-8 rounded-full bg-gray-100 flex items-center justify-center text-primary text-sm font-medium">
|
||||
{user?.name?.charAt(0) || 'A'}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm text-white truncate">{user?.name}</div>
|
||||
<div className="text-[10px] text-amber-400">SUPER_ADMIN</div>
|
||||
<div className="text-sm text-gray-900 truncate">{user?.name}</div>
|
||||
<div className="text-[10px] text-primary">SUPER_ADMIN</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="flex items-center gap-2 w-full px-2 py-1.5 rounded-md text-sm text-slate-400 hover:bg-slate-800 hover:text-slate-200 transition-colors"
|
||||
className="flex items-center gap-2 w-full px-2 py-1.5 rounded-md text-sm text-gray-500 hover:bg-gray-100 hover:text-gray-700 transition-colors"
|
||||
>
|
||||
<LogOut className="w-4 h-4" />
|
||||
<span>退出登录</span>
|
||||
|
||||
@@ -31,22 +31,22 @@ export default function PlatformDashboard() {
|
||||
api.get('/platform/dashboard').then((res: any) => setData(res.data)).finally(() => setLoading(false))
|
||||
}, [])
|
||||
|
||||
if (loading) return <div className="text-center py-12 text-slate-400">加载中...</div>
|
||||
if (loading) return <div className="text-center py-12 text-gray-400">加载中...</div>
|
||||
if (!data) return null
|
||||
|
||||
const stats = [
|
||||
{ label: '企业总数', value: data.totalOrgs, icon: Building2, color: 'text-amber-400' },
|
||||
{ label: '用户总数', value: data.totalUsers, icon: Users, color: 'text-blue-400' },
|
||||
{ label: '员工总数', value: data.totalEmployees, icon: Users, color: 'text-emerald-400' },
|
||||
{ label: '合同总数', value: data.totalContracts, icon: FileText, color: 'text-purple-400' },
|
||||
{ label: '工资条总数', value: data.totalPayslips, icon: Calculator, color: 'text-rose-400' },
|
||||
{ label: '企业总数', value: data.totalOrgs, icon: Building2, color: 'text-primary' },
|
||||
{ label: '用户总数', value: data.totalUsers, icon: Users, color: 'text-blue-600' },
|
||||
{ label: '员工总数', value: data.totalEmployees, icon: Users, color: 'text-emerald-600' },
|
||||
{ label: '合同总数', value: data.totalContracts, icon: FileText, color: 'text-purple-600' },
|
||||
{ label: '工资条总数', value: data.totalPayslips, icon: Calculator, color: 'text-rose-600' },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white">数据总览</h1>
|
||||
<p className="text-sm text-slate-400 mt-1">平台全局数据看板</p>
|
||||
<h1 className="text-2xl font-bold text-gray-900">数据总览</h1>
|
||||
<p className="text-sm text-gray-500 mt-1">平台全局数据看板</p>
|
||||
</div>
|
||||
|
||||
{/* 统计卡片 */}
|
||||
@@ -54,12 +54,12 @@ export default function PlatformDashboard() {
|
||||
{stats.map((s) => {
|
||||
const Icon = s.icon
|
||||
return (
|
||||
<div key={s.label} className="bg-slate-900 border border-slate-800 rounded-lg p-4">
|
||||
<div key={s.label} className="bg-white border border-gray-200 rounded-lg p-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm text-slate-400">{s.label}</span>
|
||||
<span className="text-sm text-gray-500">{s.label}</span>
|
||||
<Icon className={`w-5 h-5 ${s.color}`} />
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-white">{s.value.toLocaleString()}</div>
|
||||
<div className="text-2xl font-bold text-gray-900">{s.value.toLocaleString()}</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
@@ -67,9 +67,9 @@ export default function PlatformDashboard() {
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* 套餐分布 */}
|
||||
<div className="bg-slate-900 border border-slate-800 rounded-lg p-5">
|
||||
<h2 className="text-sm font-semibold text-white mb-4 flex items-center gap-2">
|
||||
<TrendingUp className="w-4 h-4 text-amber-400" />
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-5">
|
||||
<h2 className="text-sm font-semibold text-gray-900 mb-4 flex items-center gap-2">
|
||||
<TrendingUp className="w-4 h-4 text-primary" />
|
||||
套餐分布
|
||||
</h2>
|
||||
<div className="space-y-3">
|
||||
@@ -79,13 +79,13 @@ export default function PlatformDashboard() {
|
||||
{PLAN_LABELS[item.plan] || item.plan}
|
||||
</span>
|
||||
<div className="flex items-center gap-2 flex-1 ml-3">
|
||||
<div className="flex-1 h-2 bg-slate-800 rounded-full overflow-hidden">
|
||||
<div className="flex-1 h-2 bg-gray-100 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-amber-400 rounded-full"
|
||||
className="h-full bg-primary rounded-full"
|
||||
style={{ width: `${data.totalOrgs > 0 ? (item.count / data.totalOrgs) * 100 : 0}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm text-slate-300 w-8 text-right">{item.count}</span>
|
||||
<span className="text-sm text-gray-700 w-8 text-right">{item.count}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -93,21 +93,21 @@ export default function PlatformDashboard() {
|
||||
</div>
|
||||
|
||||
{/* 最近注册企业 */}
|
||||
<div className="bg-slate-900 border border-slate-800 rounded-lg p-5">
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-5">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-sm font-semibold text-white">最近注册企业</h2>
|
||||
<Link to="/platform/orgs" className="text-xs text-amber-400 hover:underline">查看全部 →</Link>
|
||||
<h2 className="text-sm font-semibold text-gray-900">最近注册企业</h2>
|
||||
<Link to="/platform/orgs" className="text-xs text-primary hover:underline">查看全部 →</Link>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{data.recentOrgs.map((org) => (
|
||||
<Link
|
||||
key={org.id}
|
||||
to={`/platform/orgs?id=${org.id}`}
|
||||
className="flex items-center justify-between p-2 rounded-md hover:bg-slate-800 transition-colors"
|
||||
className="flex items-center justify-between p-2 rounded-md hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm text-white truncate">{org.name}</div>
|
||||
<div className="text-xs text-slate-500">{org.city || '未设置'} · {org.employeeCount} 员工</div>
|
||||
<div className="text-sm text-gray-900 truncate">{org.name}</div>
|
||||
<div className="text-xs text-gray-400">{org.city || '未设置'} · {org.employeeCount} 员工</div>
|
||||
</div>
|
||||
<span className={`px-2 py-0.5 rounded text-xs font-medium shrink-0 ${PLAN_COLORS[org.plan] || 'bg-gray-100 text-gray-700'}`}>
|
||||
{PLAN_LABELS[org.plan] || org.plan}
|
||||
|
||||
@@ -75,57 +75,56 @@ export default function PlatformLogin() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-slate-950 px-4">
|
||||
<div className="min-h-screen flex items-center justify-center bg-surface px-4">
|
||||
<div className="w-full max-w-sm">
|
||||
<div className="flex items-center justify-center gap-2 mb-8">
|
||||
<div className="w-10 h-10 rounded-lg bg-amber-400 flex items-center justify-center">
|
||||
<Shield className="w-6 h-6 text-slate-950" />
|
||||
<div className="w-10 h-10 rounded-lg bg-primary flex items-center justify-center">
|
||||
<Shield className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xl font-bold text-white">用工专家平台</div>
|
||||
<div className="text-xs text-amber-400">PLATFORM ADMIN</div>
|
||||
<div className="text-xl font-bold text-gray-900">用工专家平台</div>
|
||||
<div className="text-xs text-primary">PLATFORM ADMIN</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-slate-900 border border-slate-800 rounded-lg p-6">
|
||||
<h1 className="text-lg font-semibold text-white mb-1">平台管理员登录</h1>
|
||||
<p className="text-xs text-slate-400 mb-4">超级管理员后台,管理所有企业租户</p>
|
||||
<div className="card p-6">
|
||||
<h1 className="text-lg font-semibold text-gray-900 mb-1">平台管理员登录</h1>
|
||||
<p className="text-xs text-gray-500 mb-4">超级管理员后台,管理所有企业租户</p>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 px-3 py-2 rounded-md bg-red-950 border border-red-800 text-red-400 text-sm">{error}</div>
|
||||
<div className="mb-4 px-3 py-2 rounded-md bg-red-50 text-red-700 text-sm">{error}</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||
<div>
|
||||
<Label className="text-slate-300">手机号</Label>
|
||||
<Label>手机号</Label>
|
||||
<Input
|
||||
type="tel"
|
||||
placeholder="请输入手机号"
|
||||
className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500"
|
||||
{...register('phone')}
|
||||
maxLength={11}
|
||||
/>
|
||||
{errors.phone && <p className="text-xs text-red-400 mt-1">{errors.phone.message}</p>}
|
||||
{errors.phone && <p className="text-xs text-red-500 mt-1">{errors.phone.message}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-slate-300">密码</Label>
|
||||
<Label>密码</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
placeholder="请输入密码"
|
||||
className="bg-slate-800 border-slate-700 text-white placeholder:text-slate-500 pr-10"
|
||||
className="pr-10"
|
||||
{...register('password')}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-slate-400"
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400"
|
||||
>
|
||||
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||||
</button>
|
||||
</div>
|
||||
{errors.password && <p className="text-xs text-red-400 mt-1">{errors.password.message}</p>}
|
||||
{errors.password && <p className="text-xs text-red-500 mt-1">{errors.password.message}</p>}
|
||||
</div>
|
||||
|
||||
<label className="flex items-center gap-2 cursor-pointer select-none">
|
||||
@@ -133,18 +132,18 @@ export default function PlatformLogin() {
|
||||
type="checkbox"
|
||||
checked={remember}
|
||||
onChange={(e) => setRemember(e.target.checked)}
|
||||
className="w-4 h-4 rounded border-slate-600 bg-slate-800 text-amber-400 focus:ring-amber-400 focus:ring-offset-0"
|
||||
className="w-4 h-4 rounded border-gray-300 text-primary focus:ring-primary focus:ring-offset-0"
|
||||
/>
|
||||
<span className="text-sm text-slate-400">记住账号密码</span>
|
||||
<span className="text-sm text-gray-500">记住账号密码</span>
|
||||
</label>
|
||||
|
||||
<Button type="submit" className="w-full bg-amber-400 text-slate-950 hover:bg-amber-300" disabled={loading}>
|
||||
<Button type="submit" className="w-full" disabled={loading}>
|
||||
{loading ? '登录中...' : '登录'}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<div className="mt-4 text-center">
|
||||
<Link to="/login" className="text-sm text-slate-400 hover:text-slate-200">← 返回企业登录</Link>
|
||||
<Link to="/login" className="text-sm text-gray-500 hover:text-gray-700">← 返回企业登录</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -109,8 +109,8 @@ export default function PlatformOrgs() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white">企业租户管理</h1>
|
||||
<p className="text-sm text-slate-400 mt-1">共 {total} 家企业</p>
|
||||
<h1 className="text-2xl font-bold text-gray-900">企业租户管理</h1>
|
||||
<p className="text-sm text-gray-500 mt-1">共 {total} 家企业</p>
|
||||
</div>
|
||||
|
||||
{/* 搜索栏 */}
|
||||
@@ -119,14 +119,14 @@ export default function PlatformOrgs() {
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
|
||||
<Input
|
||||
placeholder="搜索企业名称、城市、联系人..."
|
||||
className="bg-slate-900 border-slate-700 text-white placeholder:text-slate-500 pl-10"
|
||||
className="pl-10"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && fetchOrgs()}
|
||||
/>
|
||||
</div>
|
||||
<Select
|
||||
className="bg-slate-900 border-slate-700 text-white w-32"
|
||||
className="w-32"
|
||||
value={planFilter}
|
||||
onChange={(e) => setPlanFilter(e.target.value)}
|
||||
>
|
||||
@@ -135,20 +135,20 @@ export default function PlatformOrgs() {
|
||||
<option value="PRO">专业版</option>
|
||||
<option value="ENTERPRISE">企业版</option>
|
||||
</Select>
|
||||
<Button variant="secondary" onClick={fetchOrgs} className="bg-slate-800 text-slate-200 border border-slate-700 hover:bg-slate-700">
|
||||
<Button variant="secondary" onClick={fetchOrgs}>
|
||||
搜索
|
||||
</Button>
|
||||
<Button onClick={() => setCreateOpen(true)} className="bg-amber-400 text-slate-950 hover:bg-amber-300">
|
||||
<Button onClick={() => setCreateOpen(true)}>
|
||||
<Plus className="w-4 h-4 mr-1" />
|
||||
创建企业
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 表格 */}
|
||||
<div className="bg-slate-900 border border-slate-800 rounded-lg overflow-hidden">
|
||||
<div className="bg-white border border-gray-200 rounded-lg overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-800 text-slate-400 text-xs">
|
||||
<tr className="border-b border-gray-200 text-gray-500 text-xs">
|
||||
<th className="text-left px-4 py-3 font-medium">企业名称</th>
|
||||
<th className="text-left px-4 py-3 font-medium">套餐</th>
|
||||
<th className="text-left px-4 py-3 font-medium">城市</th>
|
||||
@@ -161,37 +161,37 @@ export default function PlatformOrgs() {
|
||||
</thead>
|
||||
<tbody>
|
||||
{loading ? (
|
||||
<tr><td colSpan={8} className="text-center py-12 text-slate-400">加载中...</td></tr>
|
||||
<tr><td colSpan={8} className="text-center py-12 text-gray-400">加载中...</td></tr>
|
||||
) : orgs.length === 0 ? (
|
||||
<tr><td colSpan={8} className="text-center py-12 text-slate-400">暂无数据</td></tr>
|
||||
<tr><td colSpan={8} className="text-center py-12 text-gray-400">暂无数据</td></tr>
|
||||
) : orgs.map((org) => (
|
||||
<tr key={org.id} className="border-b border-slate-800/50 hover:bg-slate-800/30">
|
||||
<tr key={org.id} className="border-b border-gray-100 hover:bg-gray-50">
|
||||
<td className="px-4 py-3">
|
||||
<div className="text-white font-medium">{org.name}</div>
|
||||
<div className="text-xs text-slate-500">{org.contactName || '-'} {org.contactPhone || ''}</div>
|
||||
<div className="text-gray-900 font-medium">{org.name}</div>
|
||||
<div className="text-xs text-gray-400">{org.contactName || '-'} {org.contactPhone || ''}</div>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className={`px-2 py-0.5 rounded text-xs font-medium ${PLAN_COLORS[org.plan] || 'bg-gray-100 text-gray-700'}`}>
|
||||
{PLAN_LABELS[org.plan] || org.plan}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-slate-300">{org.city || '-'}</td>
|
||||
<td className="px-4 py-3 text-right text-slate-300">{org.employeeCount}</td>
|
||||
<td className="px-4 py-3 text-right text-slate-300">{org.userCount}</td>
|
||||
<td className="px-4 py-3 text-right text-slate-300">{org.contractCount}</td>
|
||||
<td className="px-4 py-3 text-slate-400 text-xs">{new Date(org.createdAt).toLocaleDateString('zh-CN')}</td>
|
||||
<td className="px-4 py-3 text-gray-700">{org.city || '-'}</td>
|
||||
<td className="px-4 py-3 text-right text-gray-700">{org.employeeCount}</td>
|
||||
<td className="px-4 py-3 text-right text-gray-700">{org.userCount}</td>
|
||||
<td className="px-4 py-3 text-right text-gray-700">{org.contractCount}</td>
|
||||
<td className="px-4 py-3 text-gray-400 text-xs">{new Date(org.createdAt).toLocaleDateString('zh-CN')}</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<button
|
||||
onClick={() => setEditOrg(org)}
|
||||
className="p-1.5 rounded text-slate-400 hover:text-amber-400 hover:bg-slate-800"
|
||||
className="p-1.5 rounded text-gray-400 hover:text-primary hover:bg-gray-100"
|
||||
title="编辑"
|
||||
>
|
||||
<Edit2 className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setDeleteOrg(org)}
|
||||
className="p-1.5 rounded text-slate-400 hover:text-red-400 hover:bg-slate-800"
|
||||
className="p-1.5 rounded text-gray-400 hover:text-red-500 hover:bg-gray-100"
|
||||
title="删除"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
@@ -205,11 +205,11 @@ export default function PlatformOrgs() {
|
||||
|
||||
{/* 分页 */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex items-center justify-between px-4 py-3 border-t border-slate-800">
|
||||
<span className="text-xs text-slate-400">第 {page} / {totalPages} 页,共 {total} 条</span>
|
||||
<div className="flex items-center justify-between px-4 py-3 border-t border-gray-200">
|
||||
<span className="text-xs text-gray-500">第 {page} / {totalPages} 页,共 {total} 条</span>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="secondary" disabled={page <= 1} onClick={() => setPage(page - 1)} className="bg-slate-800 text-slate-200 border border-slate-700">上一页</Button>
|
||||
<Button size="sm" variant="secondary" disabled={page >= totalPages} onClick={() => setPage(page + 1)} className="bg-slate-800 text-slate-200 border border-slate-700">下一页</Button>
|
||||
<Button size="sm" variant="secondary" disabled={page <= 1} onClick={() => setPage(page - 1)}>上一页</Button>
|
||||
<Button size="sm" variant="secondary" disabled={page >= totalPages} onClick={() => setPage(page + 1)}>下一页</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -217,65 +217,65 @@ export default function PlatformOrgs() {
|
||||
|
||||
{/* 创建企业弹窗 */}
|
||||
{createOpen && (
|
||||
<div className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4" onClick={() => setCreateOpen(false)}>
|
||||
<div className="bg-slate-900 border border-slate-700 rounded-lg p-6 w-full max-w-lg max-h-[90vh] overflow-y-auto" onClick={(e) => e.stopPropagation()}>
|
||||
<h2 className="text-lg font-semibold text-white mb-4">创建企业客户</h2>
|
||||
<div className="fixed inset-0 bg-black/40 z-50 flex items-center justify-center p-4" onClick={() => setCreateOpen(false)}>
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-6 w-full max-w-lg max-h-[90vh] overflow-y-auto" onClick={(e) => e.stopPropagation()}>
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">创建企业客户</h2>
|
||||
<div className="space-y-3">
|
||||
<div className="border-b border-slate-700 pb-3">
|
||||
<h3 className="text-xs text-amber-400 font-medium mb-3">企业信息</h3>
|
||||
<div className="border-b border-gray-100 pb-3">
|
||||
<h3 className="text-xs text-primary font-medium mb-3">企业信息</h3>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="col-span-2">
|
||||
<Label className="text-slate-300">企业名称 *</Label>
|
||||
<Input className="bg-slate-800 border-slate-700 text-white" placeholder="如:北京XX科技有限公司" value={createForm.name} onChange={(e) => setCreateForm({ ...createForm, name: e.target.value })} />
|
||||
<Label>企业名称 *</Label>
|
||||
<Input placeholder="如:北京XX科技有限公司" value={createForm.name} onChange={(e) => setCreateForm({ ...createForm, name: e.target.value })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-300">套餐</Label>
|
||||
<Select className="bg-slate-800 border-slate-700 text-white" value={createForm.plan} onChange={(e) => setCreateForm({ ...createForm, plan: e.target.value })}>
|
||||
<Label>套餐</Label>
|
||||
<Select value={createForm.plan} onChange={(e) => setCreateForm({ ...createForm, plan: e.target.value })}>
|
||||
<option value="FREE">免费版</option>
|
||||
<option value="PRO">专业版</option>
|
||||
<option value="ENTERPRISE">企业版</option>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-300">员工上限</Label>
|
||||
<Input type="number" className="bg-slate-800 border-slate-700 text-white" value={createForm.maxEmployees} onChange={(e) => setCreateForm({ ...createForm, maxEmployees: parseInt(e.target.value) || 0 })} />
|
||||
<Label>员工上限</Label>
|
||||
<Input type="number" value={createForm.maxEmployees} onChange={(e) => setCreateForm({ ...createForm, maxEmployees: parseInt(e.target.value) || 0 })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-300">城市</Label>
|
||||
<Input className="bg-slate-800 border-slate-700 text-white" placeholder="如:北京" value={createForm.city} onChange={(e) => setCreateForm({ ...createForm, city: e.target.value })} />
|
||||
<Label>城市</Label>
|
||||
<Input placeholder="如:北京" value={createForm.city} onChange={(e) => setCreateForm({ ...createForm, city: e.target.value })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-300">联系人</Label>
|
||||
<Input className="bg-slate-800 border-slate-700 text-white" value={createForm.contactName} onChange={(e) => setCreateForm({ ...createForm, contactName: e.target.value })} />
|
||||
<Label>联系人</Label>
|
||||
<Input value={createForm.contactName} onChange={(e) => setCreateForm({ ...createForm, contactName: e.target.value })} />
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Label className="text-slate-300">联系电话</Label>
|
||||
<Input className="bg-slate-800 border-slate-700 text-white" value={createForm.contactPhone} onChange={(e) => setCreateForm({ ...createForm, contactPhone: e.target.value })} />
|
||||
<Label>联系电话</Label>
|
||||
<Input value={createForm.contactPhone} onChange={(e) => setCreateForm({ ...createForm, contactPhone: e.target.value })} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-b border-slate-700 pb-3">
|
||||
<h3 className="text-xs text-amber-400 font-medium mb-3">管理员账号</h3>
|
||||
<div className="border-b border-gray-100 pb-3">
|
||||
<h3 className="text-xs text-primary font-medium mb-3">管理员账号</h3>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label className="text-slate-300">管理员姓名</Label>
|
||||
<Input className="bg-slate-800 border-slate-700 text-white" value={createForm.adminName} onChange={(e) => setCreateForm({ ...createForm, adminName: e.target.value })} />
|
||||
<Label>管理员姓名</Label>
|
||||
<Input value={createForm.adminName} onChange={(e) => setCreateForm({ ...createForm, adminName: e.target.value })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-300">手机号 *</Label>
|
||||
<Input className="bg-slate-800 border-slate-700 text-white" placeholder="13800000000" maxLength={11} value={createForm.adminPhone} onChange={(e) => setCreateForm({ ...createForm, adminPhone: e.target.value })} />
|
||||
<Label>手机号 *</Label>
|
||||
<Input placeholder="13800000000" maxLength={11} value={createForm.adminPhone} onChange={(e) => setCreateForm({ ...createForm, adminPhone: e.target.value })} />
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Label className="text-slate-300">密码 *(至少8位)</Label>
|
||||
<Input className="bg-slate-800 border-slate-700 text-white" value={createForm.adminPassword} onChange={(e) => setCreateForm({ ...createForm, adminPassword: e.target.value })} />
|
||||
<Label>密码 *(至少8位)</Label>
|
||||
<Input value={createForm.adminPassword} onChange={(e) => setCreateForm({ ...createForm, adminPassword: e.target.value })} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2 pt-2">
|
||||
<Button className="bg-amber-400 text-slate-950 hover:bg-amber-300 flex-1" onClick={handleCreate} disabled={creating}>
|
||||
<Button className="flex-1" onClick={handleCreate} disabled={creating}>
|
||||
{creating ? '创建中...' : '确认创建'}
|
||||
</Button>
|
||||
<Button variant="secondary" className="bg-slate-800 text-slate-200 border border-slate-700" onClick={() => setCreateOpen(false)}>取消</Button>
|
||||
<Button variant="secondary" onClick={() => setCreateOpen(false)}>取消</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -284,33 +284,33 @@ export default function PlatformOrgs() {
|
||||
|
||||
{/* 编辑弹窗 */}
|
||||
{editOrg && (
|
||||
<div className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4" onClick={() => setEditOrg(null)}>
|
||||
<div className="bg-slate-900 border border-slate-700 rounded-lg p-6 w-full max-w-md" onClick={(e) => e.stopPropagation()}>
|
||||
<h2 className="text-lg font-semibold text-white mb-4">编辑企业</h2>
|
||||
<div className="fixed inset-0 bg-black/40 z-50 flex items-center justify-center p-4" onClick={() => setEditOrg(null)}>
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-6 w-full max-w-md" onClick={(e) => e.stopPropagation()}>
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">编辑企业</h2>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Label className="text-slate-300">企业名称</Label>
|
||||
<Input className="bg-slate-800 border-slate-700 text-white" value={editOrg.name} onChange={(e) => setEditOrg({ ...editOrg, name: e.target.value })} />
|
||||
<Label>企业名称</Label>
|
||||
<Input value={editOrg.name} onChange={(e) => setEditOrg({ ...editOrg, name: e.target.value })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-300">套餐</Label>
|
||||
<Select className="bg-slate-800 border-slate-700 text-white" value={editOrg.plan} onChange={(e) => setEditOrg({ ...editOrg, plan: e.target.value })}>
|
||||
<Label>套餐</Label>
|
||||
<Select value={editOrg.plan} onChange={(e) => setEditOrg({ ...editOrg, plan: e.target.value })}>
|
||||
<option value="FREE">免费版</option>
|
||||
<option value="PRO">专业版</option>
|
||||
<option value="ENTERPRISE">企业版</option>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-300">员工上限</Label>
|
||||
<Input type="number" className="bg-slate-800 border-slate-700 text-white" value={editOrg.maxEmployees} onChange={(e) => setEditOrg({ ...editOrg, maxEmployees: parseInt(e.target.value) || 0 })} />
|
||||
<Label>员工上限</Label>
|
||||
<Input type="number" value={editOrg.maxEmployees} onChange={(e) => setEditOrg({ ...editOrg, maxEmployees: parseInt(e.target.value) || 0 })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-300">城市</Label>
|
||||
<Input className="bg-slate-800 border-slate-700 text-white" value={editOrg.city || ''} onChange={(e) => setEditOrg({ ...editOrg, city: e.target.value })} />
|
||||
<Label>城市</Label>
|
||||
<Input value={editOrg.city || ''} onChange={(e) => setEditOrg({ ...editOrg, city: e.target.value })} />
|
||||
</div>
|
||||
<div className="flex gap-2 pt-2">
|
||||
<Button className="bg-amber-400 text-slate-950 hover:bg-amber-300 flex-1" onClick={handleSaveEdit}>保存</Button>
|
||||
<Button variant="secondary" className="bg-slate-800 text-slate-200 border border-slate-700" onClick={() => setEditOrg(null)}>取消</Button>
|
||||
<Button className="flex-1" onClick={handleSaveEdit}>保存</Button>
|
||||
<Button variant="secondary" onClick={() => setEditOrg(null)}>取消</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -319,24 +319,24 @@ export default function PlatformOrgs() {
|
||||
|
||||
{/* 删除确认 */}
|
||||
{deleteOrg && (
|
||||
<div className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4" onClick={() => setDeleteOrg(null)}>
|
||||
<div className="bg-slate-900 border border-slate-700 rounded-lg p-6 w-full max-w-sm" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="fixed inset-0 bg-black/40 z-50 flex items-center justify-center p-4" onClick={() => setDeleteOrg(null)}>
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-6 w-full max-w-sm" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-10 h-10 rounded-full bg-red-950 flex items-center justify-center">
|
||||
<Trash2 className="w-5 h-5 text-red-400" />
|
||||
<div className="w-10 h-10 rounded-full bg-red-50 flex items-center justify-center">
|
||||
<Trash2 className="w-5 h-5 text-red-500" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-white">确认删除</h2>
|
||||
<p className="text-xs text-slate-400">此操作不可撤销,将级联删除所有数据</p>
|
||||
<h2 className="text-lg font-semibold text-gray-900">确认删除</h2>
|
||||
<p className="text-xs text-gray-500">此操作不可撤销,将级联删除所有数据</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-slate-300 mb-4">
|
||||
确定要删除企业「<span className="text-white font-medium">{deleteOrg.name}</span>」吗?
|
||||
<p className="text-sm text-gray-600 mb-4">
|
||||
确定要删除企业「<span className="text-gray-900 font-medium">{deleteOrg.name}</span>」吗?
|
||||
该企业下有 {deleteOrg.employeeCount} 名员工、{deleteOrg.contractCount} 份合同、{deleteOrg.payslipCount} 条工资条,都将被永久删除。
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="danger" className="flex-1" onClick={handleDelete}>确认删除</Button>
|
||||
<Button variant="secondary" className="bg-slate-800 text-slate-200 border border-slate-700" onClick={() => setDeleteOrg(null)}>取消</Button>
|
||||
<Button variant="secondary" onClick={() => setDeleteOrg(null)}>取消</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -68,8 +68,8 @@ export default function PlatformUsers() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white">用户管理</h1>
|
||||
<p className="text-sm text-slate-400 mt-1">共 {total} 个用户</p>
|
||||
<h1 className="text-2xl font-bold text-gray-900">用户管理</h1>
|
||||
<p className="text-sm text-gray-500 mt-1">共 {total} 个用户</p>
|
||||
</div>
|
||||
|
||||
{/* 搜索栏 */}
|
||||
@@ -78,30 +78,30 @@ export default function PlatformUsers() {
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
|
||||
<Input
|
||||
placeholder="搜索姓名、手机号..."
|
||||
className="bg-slate-900 border-slate-700 text-white placeholder:text-slate-500 pl-10"
|
||||
className="pl-10"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && fetchUsers()}
|
||||
/>
|
||||
</div>
|
||||
<Select
|
||||
className="bg-slate-900 border-slate-700 text-white w-40"
|
||||
className="w-40"
|
||||
value={orgFilter}
|
||||
onChange={(e) => setOrgFilter(e.target.value)}
|
||||
>
|
||||
<option value="">全部企业</option>
|
||||
{orgs.map((o) => <option key={o.id} value={o.id}>{o.name}</option>)}
|
||||
</Select>
|
||||
<Button variant="secondary" onClick={fetchUsers} className="bg-slate-800 text-slate-200 border border-slate-700 hover:bg-slate-700">
|
||||
<Button variant="secondary" onClick={fetchUsers}>
|
||||
搜索
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 表格 */}
|
||||
<div className="bg-slate-900 border border-slate-800 rounded-lg overflow-hidden">
|
||||
<div className="bg-white border border-gray-200 rounded-lg overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-800 text-slate-400 text-xs">
|
||||
<tr className="border-b border-gray-200 text-gray-500 text-xs">
|
||||
<th className="text-left px-4 py-3 font-medium">姓名</th>
|
||||
<th className="text-left px-4 py-3 font-medium">手机号</th>
|
||||
<th className="text-left px-4 py-3 font-medium">角色</th>
|
||||
@@ -113,25 +113,25 @@ export default function PlatformUsers() {
|
||||
</thead>
|
||||
<tbody>
|
||||
{loading ? (
|
||||
<tr><td colSpan={7} className="text-center py-12 text-slate-400">加载中...</td></tr>
|
||||
<tr><td colSpan={7} className="text-center py-12 text-gray-400">加载中...</td></tr>
|
||||
) : users.length === 0 ? (
|
||||
<tr><td colSpan={7} className="text-center py-12 text-slate-400">暂无数据</td></tr>
|
||||
<tr><td colSpan={7} className="text-center py-12 text-gray-400">暂无数据</td></tr>
|
||||
) : users.map((user) => (
|
||||
<tr key={user.id} className="border-b border-slate-800/50 hover:bg-slate-800/30">
|
||||
<td className="px-4 py-3 text-white font-medium">{user.name}</td>
|
||||
<td className="px-4 py-3 text-slate-300">{user.phone}</td>
|
||||
<tr key={user.id} className="border-b border-gray-100 hover:bg-gray-50">
|
||||
<td className="px-4 py-3 text-gray-900 font-medium">{user.name}</td>
|
||||
<td className="px-4 py-3 text-gray-700">{user.phone}</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className={`px-2 py-0.5 rounded text-xs font-medium ${ROLE_COLORS[user.role] || 'bg-gray-100 text-gray-600'}`}>
|
||||
{ROLE_LABELS[user.role] || user.role}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-slate-300">{user.orgName || '-'}</td>
|
||||
<td className="px-4 py-3 text-gray-700">{user.orgName || '-'}</td>
|
||||
<td className="px-4 py-3">
|
||||
{user.disabled
|
||||
? <span className="text-xs text-red-400">已禁用</span>
|
||||
: <span className="text-xs text-emerald-400">正常</span>}
|
||||
? <span className="text-xs text-red-500">已禁用</span>
|
||||
: <span className="text-xs text-emerald-600">正常</span>}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-slate-400 text-xs">
|
||||
<td className="px-4 py-3 text-gray-400 text-xs">
|
||||
{user.lastLoginAt ? new Date(user.lastLoginAt).toLocaleString('zh-CN') : '从未登录'}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-center">
|
||||
@@ -139,8 +139,8 @@ export default function PlatformUsers() {
|
||||
onClick={() => handleToggle(user)}
|
||||
className={`inline-flex items-center gap-1 px-2 py-1 rounded text-xs transition-colors ${
|
||||
user.disabled
|
||||
? 'text-emerald-400 hover:bg-slate-800'
|
||||
: 'text-red-400 hover:bg-slate-800'
|
||||
? 'text-emerald-600 hover:bg-gray-100'
|
||||
: 'text-red-500 hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
{user.disabled ? <><CheckCircle className="w-3 h-3" /> 启用</> : <><Ban className="w-3 h-3" /> 禁用</>}
|
||||
@@ -152,11 +152,11 @@ export default function PlatformUsers() {
|
||||
</table>
|
||||
|
||||
{totalPages > 1 && (
|
||||
<div className="flex items-center justify-between px-4 py-3 border-t border-slate-800">
|
||||
<span className="text-xs text-slate-400">第 {page} / {totalPages} 页,共 {total} 条</span>
|
||||
<div className="flex items-center justify-between px-4 py-3 border-t border-gray-200">
|
||||
<span className="text-xs text-gray-500">第 {page} / {totalPages} 页,共 {total} 条</span>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="secondary" disabled={page <= 1} onClick={() => setPage(page - 1)} className="bg-slate-800 text-slate-200 border border-slate-700">上一页</Button>
|
||||
<Button size="sm" variant="secondary" disabled={page >= totalPages} onClick={() => setPage(page + 1)} className="bg-slate-800 text-slate-200 border border-slate-700">下一页</Button>
|
||||
<Button size="sm" variant="secondary" disabled={page <= 1} onClick={() => setPage(page - 1)}>上一页</Button>
|
||||
<Button size="sm" variant="secondary" disabled={page >= totalPages} onClick={() => setPage(page + 1)}>下一页</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user