优化:列表分类请求

This commit is contained in:
ivan 2026-02-27 10:04:04 +08:00
parent da9de8c772
commit 38d70e3e56
12 changed files with 660 additions and 44 deletions

View File

@ -34,7 +34,7 @@ import {
} from '@/api/event' } 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) const cards = res.data.list.map(mapEventItemToCard)
// 获取详情(需鉴权) // 获取详情(需鉴权)

358
sdk/approve.ts Normal file
View 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...')

View File

@ -113,42 +113,39 @@ export interface PmEventListResponse {
msg: string msg: string
} }
/**
* GET /PmEvent/getPmEventPublic doc.json
*/
export interface GetPmEventListParams { export interface GetPmEventListParams {
page?: number page?: number
pageSize?: number pageSize?: number
keyword?: string keyword?: string
/** 创建时间范围,如 ['2025-01-01', '2025-12-31'] */ /** 创建时间范围,如 ['2025-01-01', '2025-12-31']collectionFormat: csv */
createdAtRange?: string[] createdAtRange?: string[]
/** clobTokenIds 对应的值,用于按市场 token 筛选;可从 market.clobTokenIds 获取 */ /** 标签 ID 列表按分类筛选collectionFormat: csv */
tokenid?: string | string[] tagIds?: number[]
/** 标签 ID按分类筛选 */
tagId?: number
/** 标签 slug按分类筛选 */
tagSlug?: string
} }
/** /**
* Event * Event
* GET /PmEvent/getPmEventPublic * 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( export async function getPmEventPublic(
params: GetPmEventListParams = {}, params: GetPmEventListParams = {},
): Promise<PmEventListResponse> { ): 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> = { const query: Record<string, string | number | string[] | undefined> = {
page, page,
pageSize, pageSize,
} }
if (keyword != null && keyword !== '') query.keyword = keyword if (keyword != null && keyword !== '') query.keyword = keyword
if (createdAtRange != null && createdAtRange.length) query.createdAtRange = createdAtRange if (createdAtRange != null && createdAtRange.length) query.createdAtRange = createdAtRange
if (tokenid != null) { if (tagIds != null && tagIds.length > 0) {
query.tokenid = Array.isArray(tokenid) ? tokenid : [tokenid] 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) return get<PmEventListResponse>('/PmEvent/getPmEventPublic', query)
} }

138
src/api/order.ts Normal file
View 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-tokenx-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 10000sizeMatched
*/
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),
}
}

View File

@ -108,6 +108,8 @@
"today": "Today", "today": "Today",
"deposit": "Deposit", "deposit": "Deposit",
"withdraw": "Withdraw", "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", "profitLoss": "Profit/Loss",
"allTime": "All-Time", "allTime": "All-Time",
"pl1D": "1D", "pl1D": "1D",

View File

@ -108,6 +108,8 @@
"today": "今日", "today": "今日",
"deposit": "入金", "deposit": "入金",
"withdraw": "出金", "withdraw": "出金",
"authorize": "承認",
"authorizeDesc": "取引のため USDC の使用を承認します。注文時に取引所が残高を使用できるようになります。",
"profitLoss": "損益", "profitLoss": "損益",
"allTime": "全期間", "allTime": "全期間",
"pl1D": "1日", "pl1D": "1日",

View File

@ -108,6 +108,8 @@
"today": "오늘", "today": "오늘",
"deposit": "입금", "deposit": "입금",
"withdraw": "출금", "withdraw": "출금",
"authorize": "승인",
"authorizeDesc": "거래를 위해 USDC 사용을 승인합니다. 주문 시 거래소가 잔액을 사용할 수 있습니다.",
"profitLoss": "손익", "profitLoss": "손익",
"allTime": "전체", "allTime": "전체",
"pl1D": "1일", "pl1D": "1일",

View File

@ -108,6 +108,8 @@
"today": "今日", "today": "今日",
"deposit": "入金", "deposit": "入金",
"withdraw": "提现", "withdraw": "提现",
"authorize": "授权",
"authorizeDesc": "授权 USDC 用于交易。允许交易合约在下单时使用您的余额。",
"profitLoss": "盈亏", "profitLoss": "盈亏",
"allTime": "全部", "allTime": "全部",
"pl1D": "1天", "pl1D": "1天",

View File

@ -108,6 +108,8 @@
"today": "今日", "today": "今日",
"deposit": "入金", "deposit": "入金",
"withdraw": "提現", "withdraw": "提現",
"authorize": "授權",
"authorizeDesc": "授權 USDC 用於交易。允許交易合約在下單時使用您的餘額。",
"profitLoss": "盈虧", "profitLoss": "盈虧",
"allTime": "全部", "allTime": "全部",
"pl1D": "1天", "pl1D": "1天",

View File

@ -44,9 +44,14 @@ export const useToastStore = defineStore('toast', () => {
if (now - last >= DEDUP_MS) return false if (now - last >= DEDUP_MS) return false
const msg = normalizeMsg(item.message) const msg = normalizeMsg(item.message)
const inDisplayingIdx = displaying.value.findLastIndex( let inDisplayingIdx = -1
(d) => normalizeMsg(d.message) === msg && d.type === item.type 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) { if (inDisplayingIdx >= 0) {
displaying.value = displaying.value.map((x, i) => displaying.value = displaying.value.map((x, i) =>
i === inDisplayingIdx i === inDisplayingIdx
@ -95,7 +100,7 @@ export const useToastStore = defineStore('toast', () => {
function remove(id: string) { function remove(id: string) {
displaying.value = displaying.value.filter((d) => d.id !== id) displaying.value = displaying.value.filter((d) => d.id !== id)
if (queue.value.length > 0) { if (queue.value.length > 0) {
const next = queue.value[0] const next = queue.value[0]!
queue.value = queue.value.slice(1) queue.value = queue.value.slice(1)
displaying.value = [...displaying.value, next] displaying.value = [...displaying.value, next]
} }

View File

@ -415,16 +415,14 @@ function findNodeById(nodes: CategoryTreeNode[], id: string): CategoryTreeNode |
return undefined return undefined
} }
/** 当前选中分类的 tag 筛选(取最后选中的层级,用于 API tagId/tagSlug */ /** 当前选中分类的 tagIds[第一层, 第二层, 第三层],仅包含数字 ID */
const activeTagFilter = computed(() => { const activeTagIds = computed(() => {
const ids = layerActiveValues.value const ids = layerActiveValues.value
if (ids.length === 0) return { tagId: undefined as number | undefined, tagSlug: undefined as string | undefined } const tagIds: number[] = []
const lastId = ids[ids.length - 1] for (const id of ids) {
const root = filterVisible(categoryTree.value) if (id && /^\d+$/.test(id)) tagIds.push(parseInt(id, 10))
const node = lastId ? findNodeById(root, lastId) : undefined }
if (!node) return { tagId: undefined, tagSlug: undefined } return tagIds
const tagId = /^\d+$/.test(node.id) ? parseInt(node.id, 10) : undefined
return { tagId, tagSlug: node.slug || undefined }
}) })
/** 当前展示的层级数据:[[layer0], [layer1]?, [layer2]?] */ /** 当前展示的层级数据:[[layer0], [layer1]?, [layer2]?] */
@ -596,14 +594,13 @@ const activeSearchKeyword = ref('')
/** 请求事件列表并追加或覆盖到 eventList公开接口无需鉴权成功后会更新内存缓存 */ /** 请求事件列表并追加或覆盖到 eventList公开接口无需鉴权成功后会更新内存缓存 */
async function loadEvents(page: number, append: boolean, keyword?: string) { async function loadEvents(page: number, append: boolean, keyword?: string) {
const kw = keyword !== undefined ? keyword : activeSearchKeyword.value const kw = keyword !== undefined ? keyword : activeSearchKeyword.value
const { tagId, tagSlug } = activeTagFilter.value const tagIds = activeTagIds.value
try { try {
const res = await getPmEventPublic({ const res = await getPmEventPublic({
page, page,
pageSize: PAGE_SIZE, pageSize: PAGE_SIZE,
keyword: kw || undefined, keyword: kw || undefined,
tagId, tagIds: tagIds.length > 0 ? tagIds : undefined,
tagSlug,
}) })
if (res.code !== 0 && res.code !== 200) { if (res.code !== 0 && res.code !== 200) {
throw new Error(res.msg || '请求失败') throw new Error(res.msg || '请求失败')
@ -656,7 +653,7 @@ function checkScrollLoad() {
} }
onMounted(() => { onMounted(() => {
/** 分类树就绪后加载列表(确保 activeTagFilter 已计算,与下拉刷新参数一致) */ /** 分类树就绪后加载列表(确保 activeTagIds 已计算,与下拉刷新参数一致) */
function loadEventListAfterCategoryReady() { function loadEventListAfterCategoryReady() {
const cached = getEventListCache() const cached = getEventListCache()
if (cached && cached.list.length > 0) { if (cached && cached.list.length > 0) {

View File

@ -35,6 +35,15 @@
> >
{{ t('wallet.withdraw') }} {{ t('wallet.withdraw') }}
</v-btn> </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> </div>
</v-card> </v-card>
</v-col> </v-col>
@ -396,7 +405,10 @@
<template v-else-if="activeTab === 'history'"> <template v-else-if="activeTab === 'history'">
<!-- 移动端历史卡片列表 --> <!-- 移动端历史卡片列表 -->
<div v-if="mobile" class="history-mobile-list"> <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> <div class="empty-cell">{{ t('wallet.noHistoryFound') }}</div>
</template> </template>
<div <div
@ -462,7 +474,10 @@
</tr> </tr>
</thead> </thead>
<tbody> <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> <td colspan="3" class="empty-cell">{{ t('wallet.noHistoryFound') }}</td>
</tr> </tr>
<tr v-for="h in paginatedHistory" :key="h.id"> <tr v-for="h in paginatedHistory" :key="h.id">
@ -510,6 +525,33 @@
@success="onWithdrawSuccess" @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 --> <!-- Sell position dialog -->
<v-dialog <v-dialog
v-model="sellDialogOpen" v-model="sellDialogOpen"
@ -573,6 +615,7 @@ import DepositDialog from '../components/DepositDialog.vue'
import WithdrawDialog from '../components/WithdrawDialog.vue' import WithdrawDialog from '../components/WithdrawDialog.vue'
import { useUserStore } from '../stores/user' import { useUserStore } from '../stores/user'
import { pmCancelOrder } from '../api/market' import { pmCancelOrder } from '../api/market'
import { getOrderList, mapOrderToHistoryItem } from '../api/order'
import { import {
MOCK_TOKEN_ID, MOCK_TOKEN_ID,
MOCK_WALLET_POSITIONS, MOCK_WALLET_POSITIONS,
@ -580,6 +623,7 @@ import {
MOCK_WALLET_HISTORY, MOCK_WALLET_HISTORY,
} from '../api/mockData' } from '../api/mockData'
import { USE_MOCK_WALLET } from '../config/mock' import { USE_MOCK_WALLET } from '../config/mock'
import { CrossChainUSDTAuth } from '../../sdk/approve'
const { mobile } = useDisplay() const { mobile } = useDisplay()
const userStore = useUserStore() const userStore = useUserStore()
@ -596,6 +640,7 @@ const activeTab = ref<'positions' | 'orders' | 'history'>('positions')
const search = ref('') const search = ref('')
const depositDialogOpen = ref(false) const depositDialogOpen = ref(false)
const withdrawDialogOpen = ref(false) const withdrawDialogOpen = ref(false)
const authorizeDialogOpen = ref(false)
const sellDialogOpen = ref(false) const sellDialogOpen = ref(false)
const sellPositionItem = ref<Position | null>(null) const sellPositionItem = ref<Position | null>(null)
/** 移动端展开的持仓 idnull 表示全部折叠 */ /** 移动端展开的持仓 idnull 表示全部折叠 */
@ -685,6 +730,51 @@ const openOrders = ref<OpenOrder[]>(
const history = ref<HistoryItem[]>( const history = ref<HistoryItem[]>(
USE_MOCK_WALLET ? [...MOCK_WALLET_HISTORY] : [], 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 { function matchSearch(text: string): boolean {
const q = search.value.trim().toLowerCase() 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 filteredPositions = computed(() => positions.value.filter((p) => matchSearch(p.market)))
const filteredOpenOrders = computed(() => openOrders.value.filter((o) => matchSearch(o.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 page = ref(1)
const itemsPerPage = ref(10) const itemsPerPage = ref(10)
@ -704,7 +797,10 @@ function paginate<T>(list: T[]) {
} }
const paginatedPositions = computed(() => paginate(filteredPositions.value)) const paginatedPositions = computed(() => paginate(filteredPositions.value))
const paginatedOpenOrders = computed(() => paginate(filteredOpenOrders.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(() => const totalPagesPositions = computed(() =>
Math.max(1, Math.ceil(filteredPositions.value.length / itemsPerPage.value)), Math.max(1, Math.ceil(filteredPositions.value.length / itemsPerPage.value)),
@ -712,14 +808,15 @@ const totalPagesPositions = computed(() =>
const totalPagesOrders = computed(() => const totalPagesOrders = computed(() =>
Math.max(1, Math.ceil(filteredOpenOrders.value.length / itemsPerPage.value)), Math.max(1, Math.ceil(filteredOpenOrders.value.length / itemsPerPage.value)),
) )
const totalPagesHistory = computed(() => const totalPagesHistory = computed(() => {
Math.max(1, Math.ceil(filteredHistory.value.length / itemsPerPage.value)), const total = USE_MOCK_WALLET ? filteredHistory.value.length : historyTotal.value
) return Math.max(1, Math.ceil(total / itemsPerPage.value))
})
const currentListTotal = computed(() => { const currentListTotal = computed(() => {
if (activeTab.value === 'positions') return filteredPositions.value.length if (activeTab.value === 'positions') return filteredPositions.value.length
if (activeTab.value === 'orders') return filteredOpenOrders.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(() => { const currentTotalPages = computed(() => {
if (activeTab.value === 'positions') return totalPagesPositions.value if (activeTab.value === 'positions') return totalPagesPositions.value
@ -733,8 +830,12 @@ const currentPageEnd = computed(() =>
Math.min(page.value * itemsPerPage.value, currentListTotal.value), Math.min(page.value * itemsPerPage.value, currentListTotal.value),
) )
watch(activeTab, () => { watch(activeTab, (tab) => {
page.value = 1 page.value = 1
if (tab === 'history' && !USE_MOCK_WALLET) loadHistoryOrders()
})
watch([page, itemsPerPage], () => {
if (activeTab.value === 'history' && !USE_MOCK_WALLET) loadHistoryOrders()
}) })
watch([currentListTotal, itemsPerPage], () => { watch([currentListTotal, itemsPerPage], () => {
const maxPage = currentTotalPages.value const maxPage = currentTotalPages.value
@ -966,6 +1067,16 @@ onUnmounted(() => {
function onWithdrawSuccess() { function onWithdrawSuccess() {
withdrawDialogOpen.value = false 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> </script>
<style scoped> <style scoped>