refactor: 枚举移入Models目录,命名空间更新为Rainbow.Entity.Models
大石头 authored at 2026-07-02 12:54:58
6.57 KiB
RainbowBridge
import { useEffect, useState } from 'react'
import { Button, Badge, Select } from '@/components/atoms'
import { Modal } from '@/components/common/Modal'
import { DataTable, type DataColumn } from '@/components/common/DataTable'
import { showToast } from '@/stores/toastStore'
import { getMembers, addMember, updateMember, deleteMember, type Member } from '@/lib/api'

const ROLE_LABELS: Record<string, string> = { Parent: '家长', Child: '孩子', Guest: '访客' }
const ROLE_OPTIONS = [
  { value: 'Parent', label: '家长' },
  { value: 'Child', label: '孩子' },
  { value: 'Guest', label: '访客' },
]

export function MembersPage() {
  const [members, setMembers] = useState<Member[]>([])
  const [loading, setLoading] = useState(true)
  const [showAdd, setShowAdd] = useState(false)
  const [editing, setEditing] = useState<Member | null>(null)
  const [form, setForm] = useState({ name: '', displayName: '', mobile: '', role: 'Child', monthlyQuota: 0 })

  const fetchData = async () => {
    try { setMembers(await getMembers()) } catch { /* 静默 */ }
    setLoading(false)
  }

  useEffect(() => { fetchData() }, [])

  const resetForm = () => setForm({ name: '', displayName: '', mobile: '', role: 'Child', monthlyQuota: 0 })

  const handleAdd = async () => {
    if (!form.name) { showToast('warning', '姓名为必填'); return }
    try { await addMember(form); showToast('success', '成员已添加'); setShowAdd(false); resetForm(); await fetchData() }
    catch { showToast('error', '添加失败') }
  }

  const handleEdit = (m: Member) => {
    setEditing(m)
    setForm({
      name: m.name, displayName: m.displayName || '', mobile: m.mobile || '',
      role: m.role || 'Child', monthlyQuota: m.monthlyQuota || 0,
    })
  }

  const handleSaveEdit = async () => {
    if (!editing) return
    try { await updateMember(editing.id, form); showToast('success', '已更新'); setEditing(null); resetForm(); await fetchData() }
    catch { showToast('error', '更新失败') }
  }

  const handleDelete = async (id: number) => {
    try { await deleteMember(id); showToast('success', '已删除'); await fetchData() }
    catch { showToast('error', '删除失败') }
  }

  const quotaText = (bytes: number) => {
    if (bytes === 0) return '不限'
    if (bytes >= 1e9) return `${(bytes / 1e9).toFixed(0)} GB`
    return `${(bytes / 1e6).toFixed(0)} MB`
  }

  const columns: DataColumn<Member>[] = [
    { key: 'name', label: '姓名', sortable: true, render: (m) => <span className="font-medium">{m.name}</span> },
    { key: 'displayName', label: '显示名', sortable: true, render: (m) => m.displayName || '-' },
    { key: 'mobile', label: '手机号', render: (m) => m.mobile || '-' },
    {
      key: 'role', label: '角色', sortable: true,
      render: (m) => {
        const v = m.role === 'Parent' ? 'primary' as const : m.role === 'Child' ? 'info' as const : 'default' as const
        return <Badge variant={v}>{ROLE_LABELS[m.role] || m.role}</Badge>
      },
    },
    { key: 'monthlyQuota', label: '流量配额', render: (m) => quotaText(m.monthlyQuota) },
    {
      key: 'enable', label: '状态', sortable: true,
      render: (m) => <Badge variant={m.enable ? 'success' : 'default'}>{m.enable ? '启用' : '禁用'}</Badge>,
    },
    {
      key: 'actions', label: '操作',
      render: (m) => (
        <div className="flex gap-1">
          <Button variant="ghost" size="sm" onClick={() => handleEdit(m)}>编辑</Button>
          <Button variant="ghost" size="sm" onClick={() => handleDelete(m.id)}>删除</Button>
        </div>
      ),
    },
  ]

  return (
    <div className="p-6">
      <div className="flex items-center justify-between mb-4">
        <h1 className="text-lg font-bold text-[var(--color-text-primary)]">用户管理</h1>
        <Button variant="primary" size="sm" onClick={() => { resetForm(); setShowAdd(true) }}>
          添加成员
        </Button>
      </div>

      <DataTable
        data={members}
        columns={columns}
        rowKey={(m) => m.id}
        searchFields={['name', 'displayName', 'mobile']}
        searchPlaceholder="搜索姓名 / 显示名 / 手机号…"
        loading={loading}
        emptyText="暂无家庭成员"
      />

      {/* 添加/编辑弹窗 */}
      <Modal open={showAdd || !!editing} onClose={() => { setShowAdd(false); setEditing(null) }} maxWidth="max-w-sm">
        <div className="p-6">
          <h3 className="text-sm font-semibold mb-4 text-[var(--color-text-primary)]">
            {editing ? '编辑成员' : '添加成员'}
          </h3>
          <div className="space-y-3">
            <InputField label="姓名 *" value={form.name} onChange={(v) => setForm((f) => ({ ...f, name: v }))} placeholder="张三" />
            <InputField label="显示名" value={form.displayName} onChange={(v) => setForm((f) => ({ ...f, displayName: v }))} placeholder="爸爸" />
            <InputField label="手机号" value={form.mobile} onChange={(v) => setForm((f) => ({ ...f, mobile: v }))} placeholder="可选" />
            <div>
              <label className="text-xs text-[var(--color-text-secondary)]">角色</label>
              <Select
                options={ROLE_OPTIONS}
                value={form.role}
                onChange={(v) => setForm((f) => ({ ...f, role: v }))}
                className="mt-1"
              />
            </div>
            <InputField label="月度流量配额(GB,0=不限)" value={String(form.monthlyQuota)} onChange={(v) => setForm((f) => ({ ...f, monthlyQuota: Number(v) || 0 }))} type="number" />
          </div>
          <div className="flex justify-end gap-2 mt-4">
            <Button variant="secondary" size="sm" onClick={() => { setShowAdd(false); setEditing(null) }}>取消</Button>
            <Button variant="primary" size="sm" onClick={editing ? handleSaveEdit : handleAdd}>确定</Button>
          </div>
        </div>
      </Modal>
    </div>
  )
}

function InputField({ label, value, onChange, placeholder, type = 'text' }: {
  label: string; value: string; onChange: (v: string) => void; placeholder?: string; type?: string
}) {
  return (
    <label className="block">
      <span className="text-xs text-[var(--color-text-secondary)]">{label}</span>
      <input
        type={type}
        value={value}
        onChange={(e) => onChange(e.target.value)}
        placeholder={placeholder}
        className="mt-1 w-full px-3 py-2 text-sm rounded-lg border border-[var(--color-border-default)] bg-[var(--color-surface-0)] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] focus:outline-none focus:ring-2 focus:ring-[color:var(--color-brand-500)]/40"
      />
    </label>
  )
}