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 && (
-
+ <>
+
+
+ >
)}