feat(admin): refactor admin console to focus on superadmin tier, graduation and dispute governance

This commit is contained in:
Bot
2026-08-31 01:58:15 +08:00
parent 4890329f99
commit 7c59c2e9fc
6 changed files with 499 additions and 126 deletions
+2
View File
@@ -6,6 +6,7 @@ import { WebNavbar } from '../components/WebNavbar';
import { MarketExplore, MarketItem } from '../features/explore/MarketExplore';
import { TradingTerminal } from '../features/trading/TradingTerminal';
import { PortfolioView } from '../features/portfolio/PortfolioView';
import { UserMarketWizard } from '../features/create/UserMarketWizard';
const DEFAULT_MARKET: MarketItem = {
id: 'm-1',
@@ -40,6 +41,7 @@ export default function WebApp() {
<main className="flex-1 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8 py-8">
{activeTab === 'explore' && <MarketExplore onSelectMarket={handleSelectMarket} />}
{activeTab === 'trade' && <TradingTerminal market={selectedMarket} />}
{activeTab === 'create' && <UserMarketWizard />}
{activeTab === 'portfolio' && <PortfolioView />}
</main>
+40 -10
View File
@@ -1,6 +1,6 @@
import React from 'react';
import React, { useState } from 'react';
import { useWeb3 } from '../context/Web3Context';
import { Wallet, TrendingUp, Compass, PieChart, ShieldAlert, CheckCircle2 } from 'lucide-react';
import { Wallet, TrendingUp, Compass, PieChart, PlusCircle, Droplet, ShieldAlert } from 'lucide-react';
interface NavbarProps {
activeTab: string;
@@ -8,12 +8,31 @@ interface NavbarProps {
}
export const WebNavbar: React.FC<NavbarProps> = ({ activeTab, setActiveTab }) => {
const { account, wusdBalance, isCorrectNetwork, connectWallet, switchNetwork } = useWeb3();
const { account, wusdBalance, isCorrectNetwork, connectWallet, switchNetwork, refreshBalance, getCollateralContract } = useWeb3();
const [faucetLoading, setFaucetLoading] = useState(false);
const handleQuickFaucet = async () => {
if (!account) return;
try {
setFaucetLoading(true);
const collateral = getCollateralContract();
if (!collateral) return;
const tx = await collateral.mint(account, "100000000000000000000000000"); // 100M WUSD
await tx.wait();
refreshBalance();
alert('🎉 成功领取 100,000,000 WUSD 测试金!');
} catch (e: any) {
alert('领水失败: ' + (e.message || e));
} finally {
setFaucetLoading(false);
}
};
const navs = [
{ id: 'explore', label: '🔥 市场探索', icon: Compass },
{ id: 'trade', label: '⚡ 交易终端', icon: TrendingUp },
{ id: 'portfolio', label: '💼 我的持仓与账本', icon: PieChart },
{ id: 'create', label: '🚀 创建市场', icon: PlusCircle },
{ id: 'portfolio', label: '💼 持仓与金库', icon: PieChart },
];
return (
@@ -30,7 +49,7 @@ export const WebNavbar: React.FC<NavbarProps> = ({ activeTab, setActiveTab }) =>
WTFX
</span>
<span className="text-[10px] font-mono font-bold px-1.5 py-0.5 rounded bg-gradient-to-r from-cyan-500/20 to-purple-500/20 text-cyan-300 border border-cyan-500/30">
POWER LDA
TESTNET
</span>
</div>
</div>
@@ -43,7 +62,7 @@ export const WebNavbar: React.FC<NavbarProps> = ({ activeTab, setActiveTab }) =>
<button
key={item.id}
onClick={() => setActiveTab(item.id)}
className={`flex items-center space-x-2 px-4 py-2 rounded-xl text-sm font-semibold transition-all ${
className={`flex items-center space-x-2 px-3.5 py-2 rounded-xl text-xs sm:text-sm font-semibold transition-all ${
isActive
? 'bg-slate-800/80 text-white shadow-sm border border-slate-700/80'
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/40'
@@ -57,17 +76,28 @@ export const WebNavbar: React.FC<NavbarProps> = ({ activeTab, setActiveTab }) =>
</nav>
</div>
{/* Right Section / Wallet Status */}
<div className="flex items-center space-x-4">
{/* Right Section / Faucet & Wallet */}
<div className="flex items-center space-x-3">
{account && (
<button
onClick={handleQuickFaucet}
disabled={faucetLoading}
className="flex items-center space-x-1.5 px-3 py-1.5 bg-cyan-950/60 hover:bg-cyan-900/60 text-cyan-400 border border-cyan-800/50 rounded-xl text-xs font-bold transition-colors"
>
<Droplet className="w-3.5 h-3.5" />
<span>{faucetLoading ? '领水中...' : '全民领水 1 亿'}</span>
</button>
)}
{account ? (
<div className="flex items-center space-x-3">
{!isCorrectNetwork ? (
<button
onClick={switchNetwork}
className="flex items-center space-x-1.5 bg-amber-500/10 text-amber-400 border border-amber-500/30 px-3 py-1.5 rounded-lg text-xs font-medium hover:bg-amber-500/20 transition-colors"
className="flex items-center space-x-1.5 bg-amber-500/10 text-amber-400 border border-amber-500/30 px-3 py-1.5 rounded-lg text-xs font-medium hover:bg-amber-500/20"
>
<ShieldAlert className="w-4 h-4" />
<span> Robinhood </span>
<span></span>
</button>
) : (
<div className="hidden sm:flex items-center space-x-2 bg-slate-900/80 border border-slate-800 px-3 py-1.5 rounded-xl">
@@ -0,0 +1,302 @@
import React, { useState } from 'react';
import { useWeb3 } from '../../context/Web3Context';
import { DEPLOYMENTS } from '@wtfx/contracts';
import { ethers } from 'ethers';
import { PlusCircle, Trash2, Rocket, Check, AlertCircle, Loader2, Sparkles, HelpCircle } from 'lucide-react';
export const UserMarketWizard: React.FC = () => {
const { account, signer, getCollateralContract, isCorrectNetwork } = useWeb3();
const [question, setQuestion] = useState('');
const [description, setDescription] = useState('');
const [category, setCategory] = useState('Crypto');
const [customCategory, setCustomCategory] = useState('');
const [endTime, setEndTime] = useState(
new Date(Date.now() + 7 * 24 * 3600 * 1000).toISOString().slice(0, 16)
);
const [outcomes, setOutcomes] = useState<string[]>(['YES', 'NO']);
const [seedAmount, setSeedAmount] = useState<string>('100');
const [loading, setLoading] = useState<boolean>(false);
const [statusMsg, setStatusMsg] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
const addOutcome = () => {
if (outcomes.length < 255) {
setOutcomes([...outcomes, `选项 ${outcomes.length + 1}`]);
}
};
const removeOutcome = (index: number) => {
if (outcomes.length > 2) {
setOutcomes(outcomes.filter((_, i) => i !== index));
}
};
const updateOutcome = (index: number, val: string) => {
const updated = [...outcomes];
updated[index] = val;
setOutcomes(updated);
};
const handleDeployMarket = async (e: React.FormEvent) => {
e.preventDefault();
if (!signer || !account) {
setStatusMsg({ type: 'error', text: '请先连接钱包!' });
return;
}
if (!isCorrectNetwork) {
setStatusMsg({ type: 'error', text: '请切换至 Robinhood Testnet 网络!' });
return;
}
try {
setLoading(true);
setStatusMsg(null);
const controllerAddress = DEPLOYMENTS.robinhoodTestnet.contracts.controllerProxy;
const collateralContract = getCollateralContract();
if (!collateralContract) throw new Error('Collateral contract not loaded');
const seedWei = ethers.parseUnits(seedAmount || '0', 18);
// Step 1: Approve Seed
if (seedWei > 0n) {
setStatusMsg({ type: 'success', text: '步骤 1/3: 正在授权初始底仓 WUSD 资金...' });
const appTx = await collateralContract.approve(controllerAddress, ethers.MaxUint256);
await appTx.wait();
}
// Step 2: Deploy Market on Controller
setStatusMsg({ type: 'success', text: '步骤 2/3: 正在链上部署专属 Power LDA 预测市场...' });
const deadlineSec = Math.floor(new Date(endTime).getTime() / 1000);
const questionId = ethers.keccak256(
ethers.toUtf8Bytes(`${question}-${Date.now()}-${account}`)
);
const controllerAbi = [
"function deployMarket((bytes32 questionId, string question, bytes ancillaryData, address rewardToken, uint256 reward, uint256 proposalBond, uint256 earlyResolutionBond, uint256 settlementResolutionBond, uint256 resolutionTime, uint256 numOutcomes) paramsQuestion, (uint256 tier, address creator, uint80 feeRate, uint80 creatorShare, address curve, address collateral) paramsMarket, address oracle, uint256 otSeed) external returns (address)"
];
const controller = new ethers.Contract(controllerAddress, controllerAbi, signer);
const questionParams = {
questionId: questionId,
question: question,
ancillaryData: "0x", // 元数据存储在后端数据库
rewardToken: DEPLOYMENTS.robinhoodTestnet.contracts.collateral,
reward: 0,
proposalBond: 0,
earlyResolutionBond: 0,
settlementResolutionBond: 0,
resolutionTime: deadlineSec,
numOutcomes: outcomes.length,
};
const marketParams = {
tier: 1, // 默认标准档位
creator: account,
feeRate: ethers.parseUnits('0.006', 18),
creatorShare: ethers.parseUnits('0.5', 18),
curve: DEPLOYMENTS.robinhoodTestnet.contracts.powerLDACurve,
collateral: DEPLOYMENTS.robinhoodTestnet.contracts.collateral,
};
const tx = await controller.deployMarket(
questionParams,
marketParams,
account, // 用户本人作为预言机和裁决人!
seedWei
);
setStatusMsg({ type: 'success', text: `步骤 3/3: 正在上链确认并同步链下元数据 (Tx: ${tx.hash.slice(0, 10)}...)...` });
const receipt = await tx.wait();
setStatusMsg({
type: 'success',
text: `🎉 预测市场创建成功!你作为创建者享有 50% 手续费返佣并可在到期后自主裁决结果。`,
});
} catch (err: any) {
console.error('User market deploy error:', err);
setStatusMsg({ type: 'error', text: err.reason || err.message || '建盘失败,请检查参数' });
} finally {
setLoading(false);
}
};
return (
<div className="max-w-4xl mx-auto space-y-6">
<form onSubmit={handleDeployMarket} className="bg-[#0f111d] border border-slate-800 rounded-3xl p-6 sm:p-8 shadow-2xl space-y-6">
<div className="border-b border-slate-800 pb-5 flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className="p-3 bg-gradient-to-tr from-cyan-500/20 to-indigo-500/20 rounded-2xl border border-cyan-500/30 text-cyan-400">
<Rocket className="w-6 h-6" />
</div>
<div>
<h2 className="text-xl font-bold text-white"> (Create Prediction Market)</h2>
<p className="text-xs text-slate-400 mt-0.5">
2~255 50%
</p>
</div>
</div>
</div>
{/* 1. 基本信息 */}
<div className="space-y-4">
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5"> / (Question)</label>
<input
type="text"
required
value={question}
onChange={(e) => setQuestion(e.target.value)}
placeholder="例如:2026 年底前人类能否首次成功登月?"
className="w-full bg-slate-950 border border-slate-700 rounded-xl px-4 py-3 text-sm text-slate-100 focus:outline-none focus:border-cyan-500"
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5"> (Category)</label>
<select
value={category}
onChange={(e) => setCategory(e.target.value)}
className="w-full bg-slate-950 border border-slate-700 rounded-xl px-4 py-3 text-sm text-slate-100 focus:outline-none focus:border-cyan-500"
>
<option value="Crypto">Crypto ()</option>
<option value="Macro">Macro ()</option>
<option value="Politics">Politics ()</option>
<option value="Sports">Sports ()</option>
<option value="Tech">Tech ()</option>
<option value="Others">Others ()</option>
</select>
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5"> (Resolution Deadline)</label>
<input
type="datetime-local"
required
value={endTime}
onChange={(e) => setEndTime(e.target.value)}
className="w-full bg-slate-950 border border-slate-700 rounded-xl px-4 py-3 text-sm text-slate-100 focus:outline-none focus:border-cyan-500"
/>
</div>
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5"> (Resolution Rules - )</label>
<textarea
rows={2}
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="详细说明判断胜出的数据源、规则和依据,将安全保存在后端并向所有参与者展示..."
className="w-full bg-slate-950 border border-slate-700 rounded-xl px-4 py-2.5 text-sm text-slate-100 focus:outline-none focus:border-cyan-500"
/>
</div>
</div>
{/* 2. 结果选项 (2 ~ 255 项) */}
<div className="space-y-3 pt-2">
<div className="flex items-center justify-between">
<h3 className="text-xs font-bold text-slate-300 uppercase tracking-wider">
(Outcomes: {outcomes.length}/255)
</h3>
<button
type="button"
onClick={addOutcome}
disabled={outcomes.length >= 255}
className="text-xs text-cyan-400 hover:text-cyan-300 flex items-center space-x-1"
>
<PlusCircle className="w-4 h-4" />
<span></span>
</button>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 max-h-60 overflow-y-auto pr-1">
{outcomes.map((outcome, idx) => (
<div key={idx} className="flex items-center space-x-2 bg-slate-950 border border-slate-800 rounded-xl p-2.5">
<span className="w-6 text-center font-mono text-xs text-slate-500">#{idx}</span>
<input
type="text"
required
value={outcome}
onChange={(e) => updateOutcome(idx, e.target.value)}
className="flex-1 bg-transparent text-sm text-slate-200 focus:outline-none"
/>
{outcomes.length > 2 && (
<button
type="button"
onClick={() => removeOutcome(idx)}
className="text-slate-500 hover:text-rose-400 p-1"
>
<Trash2 className="w-4 h-4" />
</button>
)}
</div>
))}
</div>
</div>
{/* 3. 初始底仓资金 */}
<div className="space-y-2 pt-2">
<label className="block text-xs font-semibold text-slate-300">
(Initial Seed WUSD - )
</label>
<input
type="number"
value={seedAmount}
onChange={(e) => setSeedAmount(e.target.value)}
placeholder="100"
className="w-full bg-slate-950 border border-slate-700 rounded-xl px-4 py-3 text-sm text-slate-100 font-mono focus:outline-none focus:border-cyan-500"
/>
<p className="text-xs text-slate-500">
50%
</p>
</div>
{/* 提交按钮 */}
<div className="pt-4 border-t border-slate-800 flex items-center justify-between">
<div className="text-xs text-slate-400">
&bull; 24
</div>
<button
type="submit"
disabled={loading || !account}
className="px-6 py-3.5 bg-gradient-to-r from-cyan-500 to-indigo-600 hover:from-cyan-400 hover:to-indigo-500 text-white font-bold rounded-xl shadow-lg shadow-cyan-500/25 flex items-center space-x-2 transition-all disabled:opacity-50"
>
{loading ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
<span>...</span>
</>
) : (
<>
<Rocket className="w-4 h-4" />
<span></span>
</>
)}
</button>
</div>
{/* 状态通知 */}
{statusMsg && (
<div
className={`p-4 rounded-2xl border flex items-start space-x-3 text-xs ${
statusMsg.type === 'success'
? 'bg-emerald-950/40 border-emerald-800/60 text-emerald-300'
: 'bg-rose-950/40 border-rose-800/60 text-rose-300'
}`}
>
{statusMsg.type === 'success' ? (
<Check className="w-5 h-5 flex-shrink-0 text-emerald-400" />
) : (
<AlertCircle className="w-5 h-5 flex-shrink-0 text-rose-400" />
)}
<div className="flex-1 overflow-hidden break-words">
<p>{statusMsg.text}</p>
</div>
</div>
)}
</form>
</div>
);
};