diff --git a/backend/api/routes/admin.py b/backend/api/routes/admin.py index 4ff4469..18ea835 100644 --- a/backend/api/routes/admin.py +++ b/backend/api/routes/admin.py @@ -26,6 +26,42 @@ async def list_users(_admin: Dict[str, Any] = Depends(get_admin_user)): return User.list_all() +@router.get("/users/detailed") +async def list_users_with_accounts(_admin: Dict[str, Any] = Depends(get_admin_user)): + """获取所有用户及其关联账号列表""" + users = User.list_all() + out = [] + + # 获取所有授权关系 + # 优化:一次性查询所有 memberships 并在内存中分组,避免 N+1 查询 + # 但由于 UserAccountMembership 没有 list_all 方法,暂时循环查询或添加 list_all + # 考虑到用户量不大,循环查询尚可接受。 + + for u in users: + uid = u['id'] + memberships = UserAccountMembership.list_for_user(uid) + user_accounts = [] + for m in memberships or []: + aid = int(m.get("account_id")) + a = Account.get(aid) + if a: + user_accounts.append({ + "id": aid, + "name": a.get("name"), + "status": a.get("status"), + "role": m.get("role") + }) + + out.append({ + "id": uid, + "username": u['username'], + "role": u['role'], + "status": u['status'], + "accounts": user_accounts + }) + return out + + @router.post("/users") async def create_user(payload: UserCreateReq, _admin: Dict[str, Any] = Depends(get_admin_user)): exists = User.get_by_username(payload.username) diff --git a/frontend/src/components/TradeList.jsx b/frontend/src/components/TradeList.jsx index 9e0b458..539c2e6 100644 --- a/frontend/src/components/TradeList.jsx +++ b/frontend/src/components/TradeList.jsx @@ -83,7 +83,8 @@ const TradeList = () => { } // 导出当前订单数据(含入场/离场原因、入场思路等完整字段,便于后续分析) - const handleExport = () => { + // type: 'csv' | 'json' + const handleExport = (type = 'csv') => { if (trades.length === 0) { alert('暂无数据可导出') return @@ -137,58 +138,73 @@ const TradeList = () => { return row }) - // 转换为CSV格式 helper - const convertToCSV = (objArray) => { - const array = typeof objArray !== 'object' ? JSON.parse(objArray) : objArray; - let str = ''; - - if (array.length === 0) return ''; - - // Header - const headers = Object.keys(array[0]); - str += headers.join(',') + '\r\n'; - - // Rows - for (let i = 0; i < array.length; i++) { - let line = ''; - for (const index in array[i]) { - if (line !== '') line += ','; - - let value = array[i][index]; - if (value === null || value === undefined) { - value = ''; - } else { - value = String(value); - } - - // Escape quotes and wrap in quotes if necessary - // Excel needs double quotes to be escaped as "" - if (value.search(/("|,|\n|\r)/g) >= 0) { - value = '"' + value.replace(/"/g, '""') + '"'; - } - line += value; - } - str += line + '\r\n'; - } - return str; - } - - // 生成文件名 const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-') - const filename = `交易记录_${timestamp}.csv` + + if (type === 'json') { + const filename = `交易记录_${timestamp}.json` + const dataStr = JSON.stringify(exportData, null, 2) + const dataBlob = new Blob([dataStr], { type: 'application/json' }) + const url = URL.createObjectURL(dataBlob) + const link = document.createElement('a') + link.href = url + link.download = filename + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + URL.revokeObjectURL(url) + } else { + // 转换为CSV格式 helper + const convertToCSV = (objArray) => { + const array = typeof objArray !== 'object' ? JSON.parse(objArray) : objArray; + let str = ''; + + if (array.length === 0) return ''; - // 创建并下载文件 (CSV with BOM for Excel) - const csvStr = convertToCSV(exportData) - const bom = '\uFEFF' - const dataBlob = new Blob([bom + csvStr], { type: 'text/csv;charset=utf-8;' }) - const url = URL.createObjectURL(dataBlob) - const link = document.createElement('a') - link.href = url - link.download = filename - document.body.appendChild(link) - link.click() - document.body.removeChild(link) - URL.revokeObjectURL(url) + // Header + const headers = Object.keys(array[0]); + str += headers.join(',') + '\r\n'; + + // Rows + for (let i = 0; i < array.length; i++) { + let line = ''; + for (const index in array[i]) { + if (line !== '') line += ','; + + let value = array[i][index]; + if (value === null || value === undefined) { + value = ''; + } else if (typeof value === 'object') { + value = JSON.stringify(value); + } else { + value = String(value); + } + + // Escape quotes and wrap in quotes if necessary + // Excel needs double quotes to be escaped as "" + if (value.search(/("|,|\n|\r)/g) >= 0) { + value = '"' + value.replace(/"/g, '""') + '"'; + } + line += value; + } + str += line + '\r\n'; + } + return str; + } + + const filename = `交易记录_${timestamp}.csv` + // 创建并下载文件 (CSV with BOM for Excel) + const csvStr = convertToCSV(exportData) + const bom = '\uFEFF' + const dataBlob = new Blob([bom + csvStr], { type: 'text/csv;charset=utf-8;' }) + const url = URL.createObjectURL(dataBlob) + const link = document.createElement('a') + link.href = url + link.download = filename + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + URL.revokeObjectURL(url) + } } // 复制统计数据到剪贴板 @@ -373,9 +389,14 @@ const TradeList = () => { 重置 {trades.length > 0 && ( - + <> + + + )}