This commit is contained in:
薇薇安 2026-02-03 13:51:49 +08:00
parent 8ece78a3dc
commit d34e3cc998
2 changed files with 111 additions and 54 deletions

View File

@ -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)

View File

@ -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 = () => {
重置
</button>
{trades.length > 0 && (
<button className="btn-export" onClick={handleExport} title="导出Excel/CSV含入场/离场原因、入场思路等),便于后续分析">
导出 Excel ({trades.length})
</button>
<>
<button className="btn-export" onClick={() => handleExport('csv')} title="导出Excel/CSV含入场/离场原因、入场思路等),便于后续分析">
导出 Excel ({trades.length})
</button>
<button className="btn-export" onClick={() => handleExport('json')} style={{ backgroundColor: '#607D8B' }} title="导出JSON数据方便程序处理">
导出 JSON ({trades.length})
</button>
</>
)}
</div>
</div>