xianyu-freedom-front/src/pages/admin/DataManagement.tsx
2026-01-21 00:37:59 +08:00

193 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect } from 'react'
import { Database, RefreshCw, Trash2, Table } from 'lucide-react'
import { getTableData, clearTableData } from '@/api/admin'
import { useUIStore } from '@/store/uiStore'
import { useAuthStore } from '@/store/authStore'
import { PageLoading, ButtonLoading } from '@/components/common/Loading'
import { Select } from '@/components/common/Select'
// 可选择的数据表
const tableOptions = [
{ value: 'default_replies', label: '默认回复表' },
{ value: 'keywords', label: '关键词表' },
{ value: 'cookies', label: '账号表' },
{ value: 'cards', label: '卡券表' },
{ value: 'orders', label: '订单表' },
{ value: 'item_info', label: '商品信息表' },
{ value: 'notification_channels', label: '通知渠道表' },
{ value: 'delivery_rules', label: '发货规则表' },
{ value: 'risk_control_logs', label: '风控日志表' },
]
export function DataManagement() {
const { addToast } = useUIStore()
const { isAuthenticated, token, _hasHydrated } = useAuthStore()
const [loading, setLoading] = useState(false)
const [selectedTable, setSelectedTable] = useState('default_replies')
const [tableData, setTableData] = useState<Record<string, unknown>[]>([])
const [columns, setColumns] = useState<string[]>([])
const [count, setCount] = useState(0)
const [clearing, setClearing] = useState(false)
const loadTableData = async () => {
if (!_hasHydrated || !isAuthenticated || !token) return
try {
setLoading(true)
const result = await getTableData(selectedTable)
if (result.success) {
setTableData(result.data || [])
setColumns(result.columns || [])
setCount(result.count || 0)
} else {
addToast({ type: 'error', message: '加载数据失败' })
}
} catch {
addToast({ type: 'error', message: '加载数据失败' })
} finally {
setLoading(false)
}
}
useEffect(() => {
if (_hasHydrated && isAuthenticated && token) {
loadTableData()
}
}, [_hasHydrated, isAuthenticated, token, selectedTable])
const handleClearTable = async () => {
if (!confirm(`确定要清空 ${tableOptions.find(t => t.value === selectedTable)?.label} 吗?此操作不可恢复!`)) return
if (!confirm('再次确认:是否真的要清空该表的所有数据?')) return
try {
setClearing(true)
const result = await clearTableData(selectedTable)
if (result.success) {
addToast({ type: 'success', message: '清空成功' })
loadTableData()
} else {
addToast({ type: 'error', message: result.message || '清空失败' })
}
} catch {
addToast({ type: 'error', message: '清空失败' })
} finally {
setClearing(false)
}
}
if (!_hasHydrated) {
return <PageLoading />
}
return (
<div className="space-y-4">
{/* 数据表选择 */}
<div className="vben-card">
<div className="vben-card-header">
<h2 className="vben-card-title">
<Table className="w-4 h-4" />
</h2>
</div>
<div className="vben-card-body">
<div className="grid grid-cols-1 sm:grid-cols-12 gap-4 items-end">
<div className="sm:col-span-6">
<label className="input-label mb-1"></label>
<Select
value={selectedTable}
onChange={setSelectedTable}
options={tableOptions}
placeholder="选择数据表"
/>
</div>
<div className="sm:col-span-2 text-center py-2 px-4 bg-slate-50 dark:bg-slate-800/50 rounded-lg">
<p className="text-xs text-slate-500 dark:text-slate-400"></p>
<p className="text-xl font-bold text-slate-900 dark:text-slate-100">{count}</p>
<p className="text-xs text-slate-400"></p>
</div>
<div className="sm:col-span-4 flex justify-end">
<button
onClick={loadTableData}
disabled={loading}
className="btn-ios-primary w-full sm:w-auto"
>
{loading ? <ButtonLoading /> : <RefreshCw className="w-4 h-4" />}
</button>
</div>
</div>
</div>
</div>
{/* 数据表展示 */}
<div className="vben-card">
<div className="vben-card-header flex items-center justify-between">
<h2 className="vben-card-title">
<Database className="w-4 h-4" />
{tableOptions.find(t => t.value === selectedTable)?.label || selectedTable}
</h2>
<button
onClick={handleClearTable}
disabled={clearing || count === 0}
className="btn-ios-danger text-sm"
>
{clearing ? <ButtonLoading /> : <Trash2 className="w-4 h-4" />}
</button>
</div>
<div className="vben-card-body p-0">
{loading ? (
<div className="p-8 text-center">
<ButtonLoading />
<p className="text-slate-500 mt-2">...</p>
</div>
) : tableData.length === 0 ? (
<div className="p-8 text-center">
<Database className="w-12 h-12 text-slate-300 dark:text-slate-600 mx-auto mb-3" />
<p className="text-slate-500 dark:text-slate-400"></p>
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full text-sm table-fixed">
<thead className="bg-slate-50 dark:bg-slate-800/50 border-b border-slate-200 dark:border-slate-700">
<tr>
{columns.map((col, index) => (
<th
key={col}
className={`px-4 py-3 text-left font-medium text-slate-700 dark:text-slate-300 ${
index === 0 ? 'w-32' : 'min-w-[120px]'
}`}
>
{col}
</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-slate-100 dark:divide-slate-700">
{tableData.slice(0, 100).map((row, idx) => (
<tr key={idx} className="hover:bg-slate-50 dark:hover:bg-slate-800/30">
{columns.map((col) => (
<td
key={col}
className="px-4 py-3 text-slate-600 dark:text-slate-400 truncate"
title={String(row[col] ?? '')}
>
{String(row[col] ?? '-')}
</td>
))}
</tr>
))}
</tbody>
</table>
{tableData.length > 100 && (
<div className="p-3 text-center text-sm text-slate-500 bg-slate-50 dark:bg-slate-800/30 border-t border-slate-200 dark:border-slate-700">
100 {tableData.length}
</div>
)}
</div>
)}
</div>
</div>
</div>
)
}