优化:列表分类请求
This commit is contained in:
parent
da9de8c772
commit
38d70e3e56
@ -34,7 +34,7 @@ import {
|
||||
} from '@/api/event'
|
||||
|
||||
// 获取列表
|
||||
const res = await getPmEventPublic({ page: 1, pageSize: 10, tagSlug: 'crypto' })
|
||||
const res = await getPmEventPublic({ page: 1, pageSize: 10, tagIds: [1, 2] })
|
||||
const cards = res.data.list.map(mapEventItemToCard)
|
||||
|
||||
// 获取详情(需鉴权)
|
||||
|
||||
358
sdk/approve.ts
Normal file
358
sdk/approve.ts
Normal file
@ -0,0 +1,358 @@
|
||||
// 跨链USDT授权核心方法 - 修复版(无需外部ethers库)
|
||||
// 适用于支持EIP-1193标准的钱包(如MetaMask)
|
||||
|
||||
interface ChainConfig {
|
||||
chainId: string;
|
||||
name: string;
|
||||
usdtAddress: string;
|
||||
rpcUrl: string;
|
||||
explorer: string;
|
||||
}
|
||||
|
||||
// 链配置
|
||||
const chains: Record<string, ChainConfig> = {
|
||||
eth: {
|
||||
chainId: '0x1', // Ethereum Mainnet
|
||||
name: 'Ethereum',
|
||||
usdtAddress: '0xdAC17F958D2ee523a2206206994597C13D831ec7',
|
||||
rpcUrl: 'https://mainnet.infura.io/v3/',
|
||||
explorer: 'https://etherscan.io'
|
||||
},
|
||||
bnb: {
|
||||
chainId: '0x38', // Binance Smart Chain Mainnet
|
||||
name: 'Binance Smart Chain',
|
||||
usdtAddress: '0x55d398326f99059fF775485246999027B3197955',
|
||||
rpcUrl: 'https://bsc-dataseed.binance.org/',
|
||||
explorer: 'https://bscscan.com'
|
||||
},
|
||||
polygon: {
|
||||
chainId: '0x89', // Polygon Mainnet
|
||||
name: 'Polygon',
|
||||
usdtAddress: '0xc2132D05D31c914a87C6611C10748AEb04B58e8F',
|
||||
rpcUrl: 'https://polygon-rpc.com/',
|
||||
explorer: 'https://polygonscan.com'
|
||||
}
|
||||
};
|
||||
|
||||
// USDT ABI
|
||||
const USDT_ABI = [
|
||||
"function approve(address spender, uint256 value) public returns (bool)",
|
||||
"function allowance(address owner, address spender) view returns (uint256)",
|
||||
"function balanceOf(address owner) view returns (uint256)"
|
||||
];
|
||||
|
||||
// 将数字转换为十六进制字符串
|
||||
function numberToHex(num: number | string): string {
|
||||
return '0x' + BigInt(num).toString(16);
|
||||
}
|
||||
|
||||
// 将十进制数字转换为带指定小数位的整数形式
|
||||
function parseUnits(value: string, decimals: number): string {
|
||||
const [integerPart = '0', decimalPart = ''] = value.split('.');
|
||||
let result = integerPart.replace(/^0+/, '') || '0'; // 移除前导零
|
||||
|
||||
if (decimalPart.length > decimals) {
|
||||
throw new Error(`小数位数超过限制: ${decimals}`);
|
||||
}
|
||||
|
||||
const paddedDecimal = decimalPart.padEnd(decimals, '0');
|
||||
result += paddedDecimal;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// 验证钱包连接
|
||||
function validateWallet(): boolean {
|
||||
if (typeof window === 'undefined' || typeof (window as any).ethereum === 'undefined') {
|
||||
throw new Error('请在支持以太坊的钱包浏览器中运行此代码');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 获取当前钱包账户
|
||||
async function getAccount(): Promise<string> {
|
||||
try {
|
||||
const accounts = await (window as any).ethereum.request({
|
||||
method: 'eth_requestAccounts'
|
||||
});
|
||||
return accounts[0];
|
||||
} catch (error) {
|
||||
console.error('用户拒绝授权:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前网络ID
|
||||
async function getCurrentNetworkId(): Promise<string> {
|
||||
try {
|
||||
const chainId = await (window as any).ethereum.request({
|
||||
method: 'eth_chainId'
|
||||
});
|
||||
return chainId;
|
||||
} catch (error) {
|
||||
console.error('获取网络ID失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 切换网络
|
||||
async function switchNetwork(chainId: string): Promise<void> {
|
||||
const chainName = Object.values(chains).find(c => c.chainId === chainId)?.name || 'Unknown Network';
|
||||
|
||||
try {
|
||||
await (window as any).ethereum.request({
|
||||
method: 'wallet_switchEthereumChain',
|
||||
params: [{ chainId: chainId }],
|
||||
});
|
||||
console.log(`已切换到${chainName}`);
|
||||
} catch (switchError: any) {
|
||||
if (switchError.code === 4902) {
|
||||
// 网络不存在,尝试添加网络
|
||||
const config = Object.values(chains).find(c => c.chainId === chainId);
|
||||
if (!config) {
|
||||
throw new Error(`不支持的网络: ${chainId}`);
|
||||
}
|
||||
|
||||
try {
|
||||
await (window as any).ethereum.request({
|
||||
method: 'wallet_addEthereumChain',
|
||||
params: [{
|
||||
chainId: chainId,
|
||||
chainName: config.name,
|
||||
nativeCurrency: {
|
||||
name: chainId === '0x38' ? 'BNB' : chainId === '0x89' ? 'MATIC' : 'ETH',
|
||||
symbol: chainId === '0x38' ? 'BNB' : chainId === '0x89' ? 'MATIC' : 'ETH',
|
||||
decimals: 18,
|
||||
},
|
||||
rpcUrls: [config.rpcUrl],
|
||||
blockExplorerUrls: [config.explorer]
|
||||
}],
|
||||
});
|
||||
console.log(`已添加${chainName}`);
|
||||
} catch (addError) {
|
||||
console.error('添加网络失败:', addError);
|
||||
throw addError;
|
||||
}
|
||||
} else {
|
||||
console.error('切换网络失败:', switchError);
|
||||
throw switchError;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 编码函数调用数据
|
||||
function encodeFunctionCall(abiFragment: string, args: any[]): string {
|
||||
// 简化的函数签名计算
|
||||
const funcSignature = abiFragment.substring(0, abiFragment.indexOf('('));
|
||||
const params = abiFragment.substring(abiFragment.indexOf('(') + 1, abiFragment.lastIndexOf(')'));
|
||||
|
||||
// 为 approve(address,uint256) 函数构建调用数据
|
||||
if (funcSignature === 'approve' && params === 'address,uint256') {
|
||||
// 函数选择器: keccak256("approve(address,uint256)") 的前4字节
|
||||
// 已知为 0x095ea7b3
|
||||
let data = '0x095ea7b3';
|
||||
|
||||
// 地址参数(补零到32字节)
|
||||
let addr = args[0].toLowerCase();
|
||||
if (addr.startsWith('0x')) addr = addr.substring(2);
|
||||
data += addr.padStart(64, '0');
|
||||
|
||||
// 数量参数(补零到32字节)
|
||||
let amount = BigInt(args[1]).toString(16);
|
||||
data += amount.padStart(64, '0');
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// 为 allowance(address,address) 函数构建调用数据
|
||||
if (funcSignature === 'allowance' && params === 'address,address') {
|
||||
// 函数选择器: keccak256("allowance(address,address)") 的前4字节
|
||||
// 已知为 0xdd62ed3e
|
||||
let data = '0xdd62ed3e';
|
||||
|
||||
// 第一个地址参数
|
||||
let owner = args[0].toLowerCase();
|
||||
if (owner.startsWith('0x')) owner = owner.substring(2);
|
||||
data += owner.padStart(64, '0');
|
||||
|
||||
// 第二个地址参数
|
||||
let spender = args[1].toLowerCase();
|
||||
if (spender.startsWith('0x')) spender = spender.substring(2);
|
||||
data += spender.padStart(64, '0');
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// 为 balanceOf(address) 函数构建调用数据
|
||||
if (funcSignature === 'balanceOf' && params === 'address') {
|
||||
// 函数选择器: keccak256("balanceOf(address)") 的前4字节
|
||||
// 已知为 0x70a08231
|
||||
let data = '0x70a08231';
|
||||
|
||||
// 地址参数(补零到32字节)
|
||||
let addr = args[0].toLowerCase();
|
||||
if (addr.startsWith('0x')) addr = addr.substring(2);
|
||||
data += addr.padStart(64, '0');
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
throw new Error('不支持的ABI片段: ' + abiFragment);
|
||||
}
|
||||
|
||||
// 授权USDT
|
||||
async function authorizeUSDT(
|
||||
chain: 'eth' | 'bnb' | 'polygon',
|
||||
spenderAddress: string,
|
||||
amount: string
|
||||
): Promise<{ success: boolean; transactionHash?: string; error?: any }> {
|
||||
try {
|
||||
// 验证参数
|
||||
if (!isValidAddress(spenderAddress)) {
|
||||
throw new Error('无效的被授权地址');
|
||||
}
|
||||
|
||||
if (isNaN(parseFloat(amount)) || parseFloat(amount) <= 0) {
|
||||
throw new Error('授权金额必须大于0');
|
||||
}
|
||||
|
||||
// 获取钱包账户
|
||||
const account = await getAccount();
|
||||
|
||||
// 获取当前网络
|
||||
const currentNetworkId = await getCurrentNetworkId();
|
||||
const targetChainConfig = chains[chain];
|
||||
if (!targetChainConfig) throw new Error(`不支持的链: ${chain}`);
|
||||
|
||||
// 检查是否需要切换网络
|
||||
if (currentNetworkId !== targetChainConfig.chainId) {
|
||||
await switchNetwork(targetChainConfig.chainId);
|
||||
}
|
||||
|
||||
// 将金额转换为带6位小数的单位(USDT有6位小数)
|
||||
const parsedAmount = parseUnits(amount, 6);
|
||||
|
||||
// 构建交易数据
|
||||
const data = encodeFunctionCall('approve(address,uint256)', [spenderAddress, parsedAmount]);
|
||||
|
||||
// 构建交易对象
|
||||
const tx = {
|
||||
from: account,
|
||||
to: targetChainConfig.usdtAddress,
|
||||
data: data
|
||||
};
|
||||
|
||||
// 发送交易
|
||||
const transactionHash = await (window as any).ethereum.request({
|
||||
method: 'eth_sendTransaction',
|
||||
params: [tx]
|
||||
});
|
||||
|
||||
console.log(`正在${targetChainConfig.name}上发送授权交易...`);
|
||||
console.log('交易哈希:', transactionHash);
|
||||
|
||||
// 等待交易确认
|
||||
// 注意:这里我们只返回交易哈希,实际等待确认需要轮询或监听
|
||||
return {
|
||||
success: true,
|
||||
transactionHash: transactionHash
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('授权失败:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 查询USDT余额
|
||||
async function getUSDTBalance(chain: 'eth' | 'bnb' | 'polygon', account: string): Promise<string> {
|
||||
try {
|
||||
const targetChainConfig = chains[chain];
|
||||
if (!targetChainConfig) throw new Error(`不支持的链: ${chain}`);
|
||||
|
||||
// 构建调用数据
|
||||
const data = encodeFunctionCall('balanceOf(address)', [account]);
|
||||
|
||||
// 执行调用
|
||||
const result = await (window as any).ethereum.request({
|
||||
method: 'eth_call',
|
||||
params: [{
|
||||
to: targetChainConfig.usdtAddress,
|
||||
data: data
|
||||
}, 'latest']
|
||||
});
|
||||
|
||||
// 解析结果(十六进制转十进制)
|
||||
const balance = BigInt(result).toString();
|
||||
|
||||
// 转换为带6位小数的格式
|
||||
if (balance.length <= 6) {
|
||||
return '0.' + balance.padStart(6, '0');
|
||||
} else {
|
||||
const integerPart = balance.slice(0, -6);
|
||||
const decimalPart = balance.slice(-6).replace(/0+$/, ''); // 移除末尾零
|
||||
return decimalPart ? `${integerPart}.${decimalPart}` : integerPart;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('查询余额失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 查询当前授权额度
|
||||
async function getAllowance(
|
||||
chain: 'eth' | 'bnb' | 'polygon',
|
||||
owner: string,
|
||||
spender: string
|
||||
): Promise<string> {
|
||||
try {
|
||||
const targetChainConfig = chains[chain];
|
||||
if (!targetChainConfig) throw new Error(`不支持的链: ${chain}`);
|
||||
|
||||
// 构建调用数据
|
||||
const data = encodeFunctionCall('allowance(address,address)', [owner, spender]);
|
||||
|
||||
// 执行调用
|
||||
const result = await (window as any).ethereum.request({
|
||||
method: 'eth_call',
|
||||
params: [{
|
||||
to: targetChainConfig.usdtAddress,
|
||||
data: data
|
||||
}, 'latest']
|
||||
});
|
||||
|
||||
// 解析结果(十六进制转十进制)
|
||||
const allowance = BigInt(result).toString();
|
||||
|
||||
// 转换为带6位小数的格式
|
||||
if (allowance.length <= 6) {
|
||||
return '0.' + allowance.padStart(6, '0');
|
||||
} else {
|
||||
const integerPart = allowance.slice(0, -6);
|
||||
const decimalPart = allowance.slice(-6).replace(/0+$/, ''); // 移除末尾零
|
||||
return decimalPart ? `${integerPart}.${decimalPart}` : integerPart;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('查询授权额度失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 验证地址格式
|
||||
function isValidAddress(address: string): boolean {
|
||||
return /^0x[a-fA-F0-9]{40}$/.test(address);
|
||||
}
|
||||
|
||||
// 导出方法供UI调用
|
||||
export const CrossChainUSDTAuth = {
|
||||
authorizeUSDT,
|
||||
getUSDTBalance,
|
||||
getAllowance
|
||||
};
|
||||
|
||||
// 使用示例:
|
||||
// await CrossChainUSDTAuth.authorizeUSDT('eth', '0x1234...', '100')
|
||||
// await CrossChainUSDTAuth.getUSDTBalance('eth', '0x1234...')
|
||||
// await CrossChainUSDTAuth.getAllowance('eth', '0x1234...', '0x5678...')
|
||||
@ -113,42 +113,39 @@ export interface PmEventListResponse {
|
||||
msg: string
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /PmEvent/getPmEventPublic 请求参数(与 doc.json 对齐)
|
||||
*/
|
||||
export interface GetPmEventListParams {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
keyword?: string
|
||||
/** 创建时间范围,如 ['2025-01-01', '2025-12-31'] */
|
||||
/** 创建时间范围,如 ['2025-01-01', '2025-12-31'],collectionFormat: csv */
|
||||
createdAtRange?: string[]
|
||||
/** clobTokenIds 对应的值,用于按市场 token 筛选;可从 market.clobTokenIds 获取 */
|
||||
tokenid?: string | string[]
|
||||
/** 标签 ID,按分类筛选 */
|
||||
tagId?: number
|
||||
/** 标签 slug,按分类筛选 */
|
||||
tagSlug?: string
|
||||
/** 标签 ID 列表,按分类筛选,collectionFormat: csv */
|
||||
tagIds?: number[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页获取 Event 列表(公开接口,不需要鉴权)
|
||||
* GET /PmEvent/getPmEventPublic
|
||||
*
|
||||
* Query: page, pageSize, keyword, createdAtRange, tokenid, tagId, tagSlug
|
||||
* Query: page, pageSize, keyword, createdAtRange, tagIds
|
||||
* doc.json: paths["/PmEvent/getPmEventPublic"].get.parameters
|
||||
*/
|
||||
export async function getPmEventPublic(
|
||||
params: GetPmEventListParams = {},
|
||||
): Promise<PmEventListResponse> {
|
||||
const { page = 1, pageSize = 10, keyword, createdAtRange, tokenid, tagId, tagSlug } = params
|
||||
const { page = 1, pageSize = 10, keyword, createdAtRange, tagIds } = params
|
||||
const query: Record<string, string | number | string[] | undefined> = {
|
||||
page,
|
||||
pageSize,
|
||||
}
|
||||
if (keyword != null && keyword !== '') query.keyword = keyword
|
||||
if (createdAtRange != null && createdAtRange.length) query.createdAtRange = createdAtRange
|
||||
if (tokenid != null) {
|
||||
query.tokenid = Array.isArray(tokenid) ? tokenid : [tokenid]
|
||||
if (tagIds != null && tagIds.length > 0) {
|
||||
query.tagIds = tagIds.join(',')
|
||||
}
|
||||
// if (tagId != null && Number.isFinite(tagId)) query.tagId = tagId
|
||||
// if (tagSlug != null && tagSlug !== '') query.tagSlug = tagSlug
|
||||
|
||||
return get<PmEventListResponse>('/PmEvent/getPmEventPublic', query)
|
||||
}
|
||||
|
||||
|
||||
138
src/api/order.ts
Normal file
138
src/api/order.ts
Normal file
@ -0,0 +1,138 @@
|
||||
import { get } from './request'
|
||||
|
||||
/** 分页结果 */
|
||||
export interface PageResult<T> {
|
||||
list: T[]
|
||||
page: number
|
||||
pageSize: number
|
||||
total: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单项(与 doc.json definitions["model.ClobOrder"] 对齐)
|
||||
* GET /clob/order/getOrderList 列表项
|
||||
*/
|
||||
export interface ClobOrderItem {
|
||||
ID: number
|
||||
assetID?: string
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
expiration?: number
|
||||
feeRateBps?: number
|
||||
market?: string
|
||||
orderType?: number
|
||||
originalSize?: number
|
||||
outcome?: string
|
||||
price?: number
|
||||
side?: number
|
||||
sizeMatched?: number
|
||||
status?: number
|
||||
userID?: number
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
/** 订单列表响应 */
|
||||
export interface OrderListResponse {
|
||||
code: number
|
||||
data: PageResult<ClobOrderItem>
|
||||
msg: string
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /clob/order/getOrderList 请求参数
|
||||
*/
|
||||
export interface GetOrderListParams {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
startCreatedAt?: string
|
||||
endCreatedAt?: string
|
||||
marketID?: string
|
||||
tokenID?: string
|
||||
userID?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页获取订单列表
|
||||
* GET /clob/order/getOrderList
|
||||
* 需鉴权:x-token、x-user-id
|
||||
*/
|
||||
export async function getOrderList(
|
||||
params: GetOrderListParams = {},
|
||||
config?: { headers?: Record<string, string> },
|
||||
): Promise<OrderListResponse> {
|
||||
const { page = 1, pageSize = 10, startCreatedAt, endCreatedAt, marketID, tokenID, userID } = params
|
||||
const query: Record<string, string | number | undefined> = { page, pageSize }
|
||||
if (startCreatedAt != null && startCreatedAt !== '') query.startCreatedAt = startCreatedAt
|
||||
if (endCreatedAt != null && endCreatedAt !== '') query.endCreatedAt = endCreatedAt
|
||||
if (marketID != null && marketID !== '') query.marketID = marketID
|
||||
if (tokenID != null && tokenID !== '') query.tokenID = tokenID
|
||||
if (userID != null && Number.isFinite(userID)) query.userID = userID
|
||||
return get<OrderListResponse>('/clob/order/getOrderList', query, config)
|
||||
}
|
||||
|
||||
/** 钱包 History 展示项(与 Wallet.vue HistoryItem 一致) */
|
||||
export interface HistoryDisplayItem {
|
||||
id: string
|
||||
market: string
|
||||
side: 'Yes' | 'No'
|
||||
activity: string
|
||||
value: string
|
||||
activityDetail?: string
|
||||
profitLoss?: string
|
||||
profitLossNegative?: boolean
|
||||
timeAgo?: string
|
||||
avgPrice?: string
|
||||
shares?: string
|
||||
iconChar?: string
|
||||
iconClass?: string
|
||||
}
|
||||
|
||||
/** Side: Buy=1, Sell=2 */
|
||||
const Side = { Buy: 1, Sell: 2 } as const
|
||||
|
||||
function formatTimeAgo(createdAt: string | undefined): string {
|
||||
if (!createdAt) return ''
|
||||
const d = new Date(createdAt)
|
||||
const now = Date.now()
|
||||
const diff = now - d.getTime()
|
||||
if (diff < 60000) return 'Just now'
|
||||
if (diff < 3600000) return `${Math.floor(diff / 60000)} minutes ago`
|
||||
if (diff < 86400000) return `${Math.floor(diff / 3600000)} hours ago`
|
||||
if (diff < 604800000) return `${Math.floor(diff / 86400000)} days ago`
|
||||
return d.toLocaleDateString()
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 ClobOrderItem 映射为钱包 History 展示项
|
||||
* price 为整数(已乘 10000),sizeMatched 为已成交份额
|
||||
*/
|
||||
export function mapOrderToHistoryItem(order: ClobOrderItem): HistoryDisplayItem {
|
||||
const id = String(order.ID ?? '')
|
||||
const market = order.market ?? ''
|
||||
const outcome = order.outcome ?? 'Yes'
|
||||
const sideNum = order.side ?? Side.Buy
|
||||
const sideLabel = sideNum === Side.Sell ? 'Sell' : 'Buy'
|
||||
const activity = `${sideLabel} ${outcome}`
|
||||
const priceBps = order.price ?? 0
|
||||
const priceCents = Math.round(priceBps / 100)
|
||||
const size = order.sizeMatched ?? order.originalSize ?? 0
|
||||
const valueUsd = (priceBps / 10000) * size
|
||||
const value = `$${valueUsd.toFixed(2)}`
|
||||
const verb = sideNum === Side.Sell ? 'Sold' : 'Bought'
|
||||
const activityDetail = `${verb} ${size} ${outcome} at ${priceCents}¢`
|
||||
const avgPrice = `${priceCents}¢`
|
||||
const timeAgo = formatTimeAgo(order.createdAt)
|
||||
return {
|
||||
id,
|
||||
market,
|
||||
side: outcome === 'No' ? 'No' : 'Yes',
|
||||
activity,
|
||||
value,
|
||||
activityDetail,
|
||||
profitLoss: value,
|
||||
profitLossNegative: false,
|
||||
timeAgo,
|
||||
avgPrice,
|
||||
shares: String(size),
|
||||
}
|
||||
}
|
||||
@ -108,6 +108,8 @@
|
||||
"today": "Today",
|
||||
"deposit": "Deposit",
|
||||
"withdraw": "Withdraw",
|
||||
"authorize": "Authorize",
|
||||
"authorizeDesc": "Authorize USDC spending for trading. This allows the exchange contract to use your balance when placing orders.",
|
||||
"profitLoss": "Profit/Loss",
|
||||
"allTime": "All-Time",
|
||||
"pl1D": "1D",
|
||||
|
||||
@ -108,6 +108,8 @@
|
||||
"today": "今日",
|
||||
"deposit": "入金",
|
||||
"withdraw": "出金",
|
||||
"authorize": "承認",
|
||||
"authorizeDesc": "取引のため USDC の使用を承認します。注文時に取引所が残高を使用できるようになります。",
|
||||
"profitLoss": "損益",
|
||||
"allTime": "全期間",
|
||||
"pl1D": "1日",
|
||||
|
||||
@ -108,6 +108,8 @@
|
||||
"today": "오늘",
|
||||
"deposit": "입금",
|
||||
"withdraw": "출금",
|
||||
"authorize": "승인",
|
||||
"authorizeDesc": "거래를 위해 USDC 사용을 승인합니다. 주문 시 거래소가 잔액을 사용할 수 있습니다.",
|
||||
"profitLoss": "손익",
|
||||
"allTime": "전체",
|
||||
"pl1D": "1일",
|
||||
|
||||
@ -108,6 +108,8 @@
|
||||
"today": "今日",
|
||||
"deposit": "入金",
|
||||
"withdraw": "提现",
|
||||
"authorize": "授权",
|
||||
"authorizeDesc": "授权 USDC 用于交易。允许交易合约在下单时使用您的余额。",
|
||||
"profitLoss": "盈亏",
|
||||
"allTime": "全部",
|
||||
"pl1D": "1天",
|
||||
|
||||
@ -108,6 +108,8 @@
|
||||
"today": "今日",
|
||||
"deposit": "入金",
|
||||
"withdraw": "提現",
|
||||
"authorize": "授權",
|
||||
"authorizeDesc": "授權 USDC 用於交易。允許交易合約在下單時使用您的餘額。",
|
||||
"profitLoss": "盈虧",
|
||||
"allTime": "全部",
|
||||
"pl1D": "1天",
|
||||
|
||||
@ -44,9 +44,14 @@ export const useToastStore = defineStore('toast', () => {
|
||||
if (now - last >= DEDUP_MS) return false
|
||||
|
||||
const msg = normalizeMsg(item.message)
|
||||
const inDisplayingIdx = displaying.value.findLastIndex(
|
||||
(d) => normalizeMsg(d.message) === msg && d.type === item.type
|
||||
)
|
||||
let inDisplayingIdx = -1
|
||||
for (let i = displaying.value.length - 1; i >= 0; i--) {
|
||||
const d = displaying.value[i]
|
||||
if (d && normalizeMsg(d.message) === msg && d.type === item.type) {
|
||||
inDisplayingIdx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if (inDisplayingIdx >= 0) {
|
||||
displaying.value = displaying.value.map((x, i) =>
|
||||
i === inDisplayingIdx
|
||||
@ -95,7 +100,7 @@ export const useToastStore = defineStore('toast', () => {
|
||||
function remove(id: string) {
|
||||
displaying.value = displaying.value.filter((d) => d.id !== id)
|
||||
if (queue.value.length > 0) {
|
||||
const next = queue.value[0]
|
||||
const next = queue.value[0]!
|
||||
queue.value = queue.value.slice(1)
|
||||
displaying.value = [...displaying.value, next]
|
||||
}
|
||||
|
||||
@ -415,16 +415,14 @@ function findNodeById(nodes: CategoryTreeNode[], id: string): CategoryTreeNode |
|
||||
return undefined
|
||||
}
|
||||
|
||||
/** 当前选中分类的 tag 筛选(取最后选中的层级,用于 API tagId/tagSlug) */
|
||||
const activeTagFilter = computed(() => {
|
||||
/** 当前选中分类的 tagIds:[第一层, 第二层, 第三层],仅包含数字 ID */
|
||||
const activeTagIds = computed(() => {
|
||||
const ids = layerActiveValues.value
|
||||
if (ids.length === 0) return { tagId: undefined as number | undefined, tagSlug: undefined as string | undefined }
|
||||
const lastId = ids[ids.length - 1]
|
||||
const root = filterVisible(categoryTree.value)
|
||||
const node = lastId ? findNodeById(root, lastId) : undefined
|
||||
if (!node) return { tagId: undefined, tagSlug: undefined }
|
||||
const tagId = /^\d+$/.test(node.id) ? parseInt(node.id, 10) : undefined
|
||||
return { tagId, tagSlug: node.slug || undefined }
|
||||
const tagIds: number[] = []
|
||||
for (const id of ids) {
|
||||
if (id && /^\d+$/.test(id)) tagIds.push(parseInt(id, 10))
|
||||
}
|
||||
return tagIds
|
||||
})
|
||||
|
||||
/** 当前展示的层级数据:[[layer0], [layer1]?, [layer2]?] */
|
||||
@ -596,14 +594,13 @@ const activeSearchKeyword = ref('')
|
||||
/** 请求事件列表并追加或覆盖到 eventList(公开接口,无需鉴权);成功后会更新内存缓存 */
|
||||
async function loadEvents(page: number, append: boolean, keyword?: string) {
|
||||
const kw = keyword !== undefined ? keyword : activeSearchKeyword.value
|
||||
const { tagId, tagSlug } = activeTagFilter.value
|
||||
const tagIds = activeTagIds.value
|
||||
try {
|
||||
const res = await getPmEventPublic({
|
||||
page,
|
||||
pageSize: PAGE_SIZE,
|
||||
keyword: kw || undefined,
|
||||
tagId,
|
||||
tagSlug,
|
||||
tagIds: tagIds.length > 0 ? tagIds : undefined,
|
||||
})
|
||||
if (res.code !== 0 && res.code !== 200) {
|
||||
throw new Error(res.msg || '请求失败')
|
||||
@ -656,7 +653,7 @@ function checkScrollLoad() {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
/** 分类树就绪后加载列表(确保 activeTagFilter 已计算,与下拉刷新参数一致) */
|
||||
/** 分类树就绪后加载列表(确保 activeTagIds 已计算,与下拉刷新参数一致) */
|
||||
function loadEventListAfterCategoryReady() {
|
||||
const cached = getEventListCache()
|
||||
if (cached && cached.list.length > 0) {
|
||||
|
||||
@ -35,6 +35,15 @@
|
||||
>
|
||||
{{ t('wallet.withdraw') }}
|
||||
</v-btn>
|
||||
<!-- <v-btn
|
||||
variant="outlined"
|
||||
color="grey"
|
||||
class="action-btn"
|
||||
prepend-icon="mdi-shield-check-outline"
|
||||
@click="onAuthorizeClick"
|
||||
>
|
||||
{{ t('wallet.authorize') }}
|
||||
</v-btn> -->
|
||||
</div>
|
||||
</v-card>
|
||||
</v-col>
|
||||
@ -396,7 +405,10 @@
|
||||
<template v-else-if="activeTab === 'history'">
|
||||
<!-- 移动端:历史卡片列表 -->
|
||||
<div v-if="mobile" class="history-mobile-list">
|
||||
<template v-if="filteredHistory.length === 0">
|
||||
<template v-if="historyLoading">
|
||||
<div class="empty-cell">{{ t('common.loading') }}</div>
|
||||
</template>
|
||||
<template v-else-if="filteredHistory.length === 0">
|
||||
<div class="empty-cell">{{ t('wallet.noHistoryFound') }}</div>
|
||||
</template>
|
||||
<div
|
||||
@ -462,7 +474,10 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-if="filteredHistory.length === 0">
|
||||
<tr v-if="historyLoading">
|
||||
<td colspan="3" class="empty-cell">{{ t('common.loading') }}</td>
|
||||
</tr>
|
||||
<tr v-else-if="filteredHistory.length === 0">
|
||||
<td colspan="3" class="empty-cell">{{ t('wallet.noHistoryFound') }}</td>
|
||||
</tr>
|
||||
<tr v-for="h in paginatedHistory" :key="h.id">
|
||||
@ -510,6 +525,33 @@
|
||||
@success="onWithdrawSuccess"
|
||||
/>
|
||||
|
||||
<!-- 授权弹窗 -->
|
||||
<v-dialog
|
||||
v-model="authorizeDialogOpen"
|
||||
max-width="420"
|
||||
persistent
|
||||
transition="dialog-transition"
|
||||
>
|
||||
<v-card rounded="lg">
|
||||
<v-card-title class="d-flex align-center">
|
||||
<v-icon class="mr-2">mdi-shield-check-outline</v-icon>
|
||||
{{ t('wallet.authorize') }}
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
{{ t('wallet.authorizeDesc') }}
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn variant="text" @click="authorizeDialogOpen = false">
|
||||
{{ t('deposit.close') }}
|
||||
</v-btn>
|
||||
<v-btn color="primary" variant="flat" @click="submitAuthorize">
|
||||
{{ t('wallet.authorize') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- Sell position dialog -->
|
||||
<v-dialog
|
||||
v-model="sellDialogOpen"
|
||||
@ -573,6 +615,7 @@ import DepositDialog from '../components/DepositDialog.vue'
|
||||
import WithdrawDialog from '../components/WithdrawDialog.vue'
|
||||
import { useUserStore } from '../stores/user'
|
||||
import { pmCancelOrder } from '../api/market'
|
||||
import { getOrderList, mapOrderToHistoryItem } from '../api/order'
|
||||
import {
|
||||
MOCK_TOKEN_ID,
|
||||
MOCK_WALLET_POSITIONS,
|
||||
@ -580,6 +623,7 @@ import {
|
||||
MOCK_WALLET_HISTORY,
|
||||
} from '../api/mockData'
|
||||
import { USE_MOCK_WALLET } from '../config/mock'
|
||||
import { CrossChainUSDTAuth } from '../../sdk/approve'
|
||||
|
||||
const { mobile } = useDisplay()
|
||||
const userStore = useUserStore()
|
||||
@ -596,6 +640,7 @@ const activeTab = ref<'positions' | 'orders' | 'history'>('positions')
|
||||
const search = ref('')
|
||||
const depositDialogOpen = ref(false)
|
||||
const withdrawDialogOpen = ref(false)
|
||||
const authorizeDialogOpen = ref(false)
|
||||
const sellDialogOpen = ref(false)
|
||||
const sellPositionItem = ref<Position | null>(null)
|
||||
/** 移动端展开的持仓 id,null 表示全部折叠 */
|
||||
@ -685,6 +730,51 @@ const openOrders = ref<OpenOrder[]>(
|
||||
const history = ref<HistoryItem[]>(
|
||||
USE_MOCK_WALLET ? [...MOCK_WALLET_HISTORY] : [],
|
||||
)
|
||||
/** 订单历史(API 数据,非 mock 时使用) */
|
||||
const historyList = ref<HistoryItem[]>([])
|
||||
const historyTotal = ref(0)
|
||||
const historyLoading = ref(false)
|
||||
|
||||
async function loadHistoryOrders() {
|
||||
if (USE_MOCK_WALLET) return
|
||||
const headers = userStore.getAuthHeaders()
|
||||
if (!headers) {
|
||||
historyList.value = []
|
||||
historyTotal.value = 0
|
||||
return
|
||||
}
|
||||
const uid = userStore.user?.id ?? userStore.user?.ID
|
||||
const userID = uid != null ? Number(uid) : undefined
|
||||
if (!userID || !Number.isFinite(userID)) {
|
||||
historyList.value = []
|
||||
historyTotal.value = 0
|
||||
return
|
||||
}
|
||||
historyLoading.value = true
|
||||
try {
|
||||
const res = await getOrderList(
|
||||
{
|
||||
page: page.value,
|
||||
pageSize: itemsPerPage.value,
|
||||
userID,
|
||||
},
|
||||
{ headers },
|
||||
)
|
||||
if (res.code === 0 || res.code === 200) {
|
||||
const list = res.data?.list ?? []
|
||||
historyList.value = list.map(mapOrderToHistoryItem)
|
||||
historyTotal.value = res.data?.total ?? 0
|
||||
} else {
|
||||
historyList.value = []
|
||||
historyTotal.value = 0
|
||||
}
|
||||
} catch {
|
||||
historyList.value = []
|
||||
historyTotal.value = 0
|
||||
} finally {
|
||||
historyLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function matchSearch(text: string): boolean {
|
||||
const q = search.value.trim().toLowerCase()
|
||||
@ -692,7 +782,10 @@ function matchSearch(text: string): boolean {
|
||||
}
|
||||
const filteredPositions = computed(() => positions.value.filter((p) => matchSearch(p.market)))
|
||||
const filteredOpenOrders = computed(() => openOrders.value.filter((o) => matchSearch(o.market)))
|
||||
const filteredHistory = computed(() => history.value.filter((h) => matchSearch(h.market)))
|
||||
const filteredHistory = computed(() => {
|
||||
const list = USE_MOCK_WALLET ? history.value : historyList.value
|
||||
return list.filter((h) => matchSearch(h.market))
|
||||
})
|
||||
|
||||
const page = ref(1)
|
||||
const itemsPerPage = ref(10)
|
||||
@ -704,7 +797,10 @@ function paginate<T>(list: T[]) {
|
||||
}
|
||||
const paginatedPositions = computed(() => paginate(filteredPositions.value))
|
||||
const paginatedOpenOrders = computed(() => paginate(filteredOpenOrders.value))
|
||||
const paginatedHistory = computed(() => paginate(filteredHistory.value))
|
||||
const paginatedHistory = computed(() => {
|
||||
if (USE_MOCK_WALLET) return paginate(filteredHistory.value)
|
||||
return filteredHistory.value
|
||||
})
|
||||
|
||||
const totalPagesPositions = computed(() =>
|
||||
Math.max(1, Math.ceil(filteredPositions.value.length / itemsPerPage.value)),
|
||||
@ -712,14 +808,15 @@ const totalPagesPositions = computed(() =>
|
||||
const totalPagesOrders = computed(() =>
|
||||
Math.max(1, Math.ceil(filteredOpenOrders.value.length / itemsPerPage.value)),
|
||||
)
|
||||
const totalPagesHistory = computed(() =>
|
||||
Math.max(1, Math.ceil(filteredHistory.value.length / itemsPerPage.value)),
|
||||
)
|
||||
const totalPagesHistory = computed(() => {
|
||||
const total = USE_MOCK_WALLET ? filteredHistory.value.length : historyTotal.value
|
||||
return Math.max(1, Math.ceil(total / itemsPerPage.value))
|
||||
})
|
||||
|
||||
const currentListTotal = computed(() => {
|
||||
if (activeTab.value === 'positions') return filteredPositions.value.length
|
||||
if (activeTab.value === 'orders') return filteredOpenOrders.value.length
|
||||
return filteredHistory.value.length
|
||||
return USE_MOCK_WALLET ? filteredHistory.value.length : historyTotal.value
|
||||
})
|
||||
const currentTotalPages = computed(() => {
|
||||
if (activeTab.value === 'positions') return totalPagesPositions.value
|
||||
@ -733,8 +830,12 @@ const currentPageEnd = computed(() =>
|
||||
Math.min(page.value * itemsPerPage.value, currentListTotal.value),
|
||||
)
|
||||
|
||||
watch(activeTab, () => {
|
||||
watch(activeTab, (tab) => {
|
||||
page.value = 1
|
||||
if (tab === 'history' && !USE_MOCK_WALLET) loadHistoryOrders()
|
||||
})
|
||||
watch([page, itemsPerPage], () => {
|
||||
if (activeTab.value === 'history' && !USE_MOCK_WALLET) loadHistoryOrders()
|
||||
})
|
||||
watch([currentListTotal, itemsPerPage], () => {
|
||||
const maxPage = currentTotalPages.value
|
||||
@ -966,6 +1067,16 @@ onUnmounted(() => {
|
||||
function onWithdrawSuccess() {
|
||||
withdrawDialogOpen.value = false
|
||||
}
|
||||
|
||||
function onAuthorizeClick() {
|
||||
authorizeDialogOpen.value = true
|
||||
}
|
||||
|
||||
async function submitAuthorize() {
|
||||
// TODO: 对接 USDC 授权接口(approve CLOB 合约)
|
||||
// authorizeDialogOpen.value = false
|
||||
await CrossChainUSDTAuth.authorizeUSDT('eth', '0x024b7270Ee9c0Fc0de2E00a979d146255E0e9C00', '100')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user