refactor: 枚举移入Models目录,命名空间更新为Rainbow.Entity.Models
大石头 authored at 2026-07-02 12:54:58
6.80 KiB
RainbowBridge
import { useEffect, useState } from 'react'
import { Button, Badge } from '@/components/atoms'
import { Modal } from '@/components/common/Modal'
import { DataTable, type DataColumn } from '@/components/common/DataTable'
import { showToast } from '@/stores/toastStore'
import {
  getPppoeStatus, connectPppoe, disconnectPppoe,
  getPppoeAccounts, addPppoeAccount, deletePppoeAccount,
  type PppoeStatus, type PppoeAccount,
} from '@/lib/api'

export function PppoePage() {
  const [status, setStatus] = useState<PppoeStatus | null>(null)
  const [accounts, setAccounts] = useState<PppoeAccount[]>([])
  const [loading, setLoading] = useState(true)
  const [connecting, setConnecting] = useState(false)
  const [showAdd, setShowAdd] = useState(false)
  const [form, setForm] = useState({ interfaceName: '', username: '', password: '' })

  const fetchData = async () => {
    try {
      const [s, a] = await Promise.all([getPppoeStatus(), getPppoeAccounts()])
      setStatus(s); setAccounts(a)
    } catch { /* 静默 */ }
    setLoading(false)
  }

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

  const handleConnect = async () => {
    setConnecting(true)
    try { await connectPppoe(); showToast('success', 'PPPoE 拨号已发起'); await fetchData() }
    catch { showToast('error', '拨号失败') }
    setConnecting(false)
  }

  const handleDisconnect = async () => {
    setConnecting(true)
    try { await disconnectPppoe(); showToast('success', 'PPPoE 已断开'); await fetchData() }
    catch { showToast('error', '断开失败') }
    setConnecting(false)
  }

  const handleAddAccount = async () => {
    if (!form.interfaceName || !form.username || !form.password) { showToast('warning', '请填写完整信息'); return }
    try { await addPppoeAccount(form); showToast('success', '账号已添加'); setShowAdd(false); setForm({ interfaceName: '', username: '', password: '' }); await fetchData() }
    catch { showToast('error', '添加失败') }
  }

  const accColumns: DataColumn<PppoeAccount>[] = [
    { key: 'interfaceName', label: '接口', sortable: true },
    { key: 'username', label: '用户名', sortable: true },
    {
      key: 'enable', label: '状态', sortable: true,
      render: (a) => <Badge variant={a.enable ? 'success' : 'default'}>{a.enable ? '启用' : '禁用'}</Badge>,
    },
    {
      key: 'actions', label: '操作',
      render: (a) => (
        <Button variant="ghost" size="sm" onClick={async () => {
          try { await deletePppoeAccount(a.id); showToast('success', '账号已删除'); await fetchData() }
          catch { showToast('error', '删除失败') }
        }}>删除</Button>
      ),
    },
  ]

  return (
    <div className="p-6">
      <h1 className="text-lg font-bold text-[var(--color-text-primary)] mb-4">PPPoE 拨号管理</h1>

      {/* 拨号状态卡片 */}
      <div className="glass-panel p-5 mb-6">
        <div className="flex items-center justify-between mb-4">
          <h2 className="text-sm font-semibold text-[var(--color-text-primary)]">拨号状态</h2>
          <Badge variant={status?.connected ? 'success' : 'default'}>
            {status?.connected ? '已连接' : '未连接'}
          </Badge>
        </div>
        {status?.connected ? (
          <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
            <StatItem label="接口" value={status.interfaceName || '-'} />
            <StatItem label="IP 地址" value={status.ip || '-'} />
            <StatItem label="在线时长" value={fmtUptime(status.uptime)} />
            <StatItem label="状态" value="● 在线" />
          </div>
        ) : (
          <p className="text-sm text-[var(--color-text-tertiary)] mb-4">当前未拨号</p>
        )}
        <div className="flex gap-3">
          <Button variant="primary" size="sm" disabled={connecting || status?.connected} onClick={handleConnect}>连接</Button>
          <Button variant="secondary" size="sm" disabled={connecting || !status?.connected} onClick={handleDisconnect}>断开</Button>
        </div>
      </div>

      {/* 账号列表 */}
      <div>
        <div className="flex items-center justify-between mb-3">
          <h2 className="text-sm font-semibold text-[var(--color-text-primary)]">宽带账号</h2>
          <Button size="sm" variant="soft" onClick={() => setShowAdd(true)}>添加账号</Button>
        </div>
        <DataTable
          data={accounts} columns={accColumns} rowKey={(a) => a.id}
          searchFields={['interfaceName', 'username']} searchPlaceholder="搜索接口 / 用户名…"
          loading={loading} emptyText="暂无宽带账号"
        />
      </div>

      {/* 添加账号弹窗 */}
      <Modal open={showAdd} onClose={() => setShowAdd(false)} maxWidth="max-w-sm">
        <div className="p-6">
          <h3 className="text-sm font-semibold mb-4 text-[var(--color-text-primary)]">添加宽带账号</h3>
          <div className="space-y-3">
            <InputField label="接口名" value={form.interfaceName} onChange={(v) => setForm((f) => ({ ...f, interfaceName: v }))} placeholder="例如 ppp0" />
            <InputField label="用户名" value={form.username} onChange={(v) => setForm((f) => ({ ...f, username: v }))} />
            <InputField label="密码" value={form.password} onChange={(v) => setForm((f) => ({ ...f, password: v }))} type="password" />
          </div>
          <div className="flex justify-end gap-2 mt-4">
            <Button variant="secondary" size="sm" onClick={() => setShowAdd(false)}>取消</Button>
            <Button variant="primary" size="sm" onClick={handleAddAccount}>确定</Button>
          </div>
        </div>
      </Modal>
    </div>
  )
}

function StatItem({ label, value }: { label: string; value: string }) {
  return (
    <div>
      <p className="text-xs text-[var(--color-text-tertiary)]">{label}</p>
      <p className="text-sm font-medium text-[var(--color-text-primary)]">{value}</p>
    </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>
  )
}

function fmtUptime(seconds: number): string {
  if (seconds < 60) return `${seconds}秒`
  if (seconds < 3600) return `${Math.floor(seconds / 60)}分钟`
  const h = Math.floor(seconds / 3600)
  const m = Math.floor((seconds % 3600) / 60)
  return `${h}小时${m}分`
}