优化账号管理页面:调整表格列宽,美化按钮样式,完善角色显示

This commit is contained in:
xin 2025-05-05 03:08:42 +08:00
parent a6163bfc1c
commit 802c57003c
10 changed files with 380 additions and 56 deletions

View File

@ -202,6 +202,40 @@ class AccountManager:
except Exception as e: except Exception as e:
logger.error(f"更新密码失败: {e}") logger.error(f"更新密码失败: {e}")
raise raise
@staticmethod
def reset_password(account_id: str):
"""重置账号密码"""
try:
# 验证account_id是否为有效UUID
try:
uuid.UUID(account_id)
except ValueError:
logger.warning(f"无效的account_id格式: {account_id}")
return False
# 使用固定密码
new_password = "Welcome123!"
hashed_password, password_salt = AccountManager.hash_password(new_password)
# 更新密码
updated_at = datetime.now(timezone.utc)
update_query = """
UPDATE accounts
SET password = %s, password_salt = %s, updated_at = %s
WHERE id = %s::uuid;
"""
rows_affected = execute_update(update_query, (hashed_password, password_salt, updated_at, account_id))
if rows_affected > 0:
logger.info(f"账号 {account_id} 的密码已重置!")
return True
else:
logger.warning(f"未找到ID为 {account_id} 的账号。")
return False
except Exception as e:
logger.error(f"重置密码失败: {e}")
raise
@staticmethod @staticmethod
def associate_with_tenant(account_id, tenant_id, role="normal", invited_by=None, current=False): def associate_with_tenant(account_id, tenant_id, role="normal", invited_by=None, current=False):
@ -250,3 +284,35 @@ class AccountManager:
except Exception as e: except Exception as e:
logger.error(f"获取租户账户失败: {e}") logger.error(f"获取租户账户失败: {e}")
return [] return []
@staticmethod
def get_account_tenants(account_id):
"""获取账户关联的所有租户"""
try:
# 验证account_id是否为有效UUID
try:
uuid.UUID(account_id)
except ValueError:
logger.error(f"无效的account_id格式: {account_id}")
return []
query = """
SELECT t.id, t.name, j.role, j.current
FROM tenants t
JOIN tenant_account_joins j ON t.id = j.tenant_id
WHERE j.account_id = %s::uuid;
"""
tenants = execute_query(query, (account_id,))
if not tenants:
logger.info(f"账号 {account_id} 未关联任何租户")
return []
return [{
"tenant_id": str(t[0]),
"tenant_name": t[1],
"role": str(t[2]).lower(), # 确保角色值统一为小写
"current": t[3]
} for t in tenants]
except Exception as e:
logger.error(f"获取账户租户失败: {e}", exc_info=True)
raise

View File

@ -1,3 +1,4 @@
import uuid
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from fastapi import FastAPI, Depends, HTTPException, status, Request, Body, Response from fastapi import FastAPI, Depends, HTTPException, status, Request, Body, Response
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
@ -25,7 +26,7 @@ logger = logging.getLogger(__name__)
# JWT配置 # JWT配置
SECRET_KEY = "your-secret-key-here" # 生产环境应该从环境变量获取 SECRET_KEY = "your-secret-key-here" # 生产环境应该从环境变量获取
ALGORITHM = "HS256" ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30 ACCESS_TOKEN_EXPIRE_MINUTES = 1440 # 延长至24小时
# 密码哈希 # 密码哈希
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
@ -114,17 +115,20 @@ async def get_current_user(token: str = Depends(oauth2_scheme)):
username: str = payload.get("sub") username: str = payload.get("sub")
if username is None: if username is None:
raise credentials_exception raise credentials_exception
# 验证用户(兼容前后端用户)
user = backend_account_manager.get_user_by_username(username) or \
AccountManager.get_user_by_username(username)
if not user:
raise credentials_exception
return {
"id": str(user.get("id")),
"username": user.get("username"),
"email": user.get("email", "")
}
except JWTError: except JWTError:
raise credentials_exception raise credentials_exception
user = AccountManager.get_user_by_username(username)
if user is None:
raise credentials_exception
return {
"id": user["id"],
"username": user["username"],
"email": user["email"]
}
# 认证路由组 # 认证路由组
@app.options("/api/auth/register", include_in_schema=False) @app.options("/api/auth/register", include_in_schema=False)
@ -260,29 +264,46 @@ async def search_accounts(
@app.get("/api/dify_accounts/{username}") @app.get("/api/dify_accounts/{username}")
async def get_dify_account(username: str, current_user: dict = Depends(get_current_user)): async def get_dify_account(username: str, current_user: dict = Depends(get_current_user)):
"""查询Dify账户信息""" """查询Dify账户信息及关联租户"""
try: try:
account = AccountManager.get_user_by_username(username) account = AccountManager.get_user_by_username(username)
if not account: if not account:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Dify账户不存在") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Dify账户不存在")
# 获取关联租户信息
tenant_info = AccountManager.get_account_tenants(account["id"])
return { return {
"user_id": str(account["id"]), "user_id": str(account["id"]),
"username": account["username"], "username": account["username"],
"email": account["email"], "email": account["email"],
"created_at": account["created_at"] "created_at": account["created_at"],
"tenants": tenant_info # 直接使用已格式化的租户信息
} }
except ValueError as e:
logger.error(f"参数格式错误: {e}")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="参数格式错误"
)
except Exception as e: except Exception as e:
logger.error(f"查询Dify账户失败: {e}") logger.error(f"查询Dify账户失败: {e}", exc_info=True)
if "404" in str(e): if "404" in str(e):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Dify账户不存在") raise HTTPException(
raise HTTPException(status_code=400, detail="查询Dify账户失败") status_code=status.HTTP_404_NOT_FOUND,
detail="Dify账户不存在"
)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="服务器内部错误"
)
@app.put("/api/accounts/password") @app.put("/api/accounts/password")
async def change_password( async def change_password(
password_change: PasswordChange, password_change: PasswordChange,
current_user: dict = Depends(get_current_user) current_user: dict = Depends(get_current_user)
): ):
"""修改密码""" """修改当前用户密码"""
try: try:
user = AccountManager.get_user_by_username(current_user["username"]) user = AccountManager.get_user_by_username(current_user["username"])
if not AccountManager.verify_password( if not AccountManager.verify_password(
@ -305,6 +326,55 @@ async def change_password(
logger.error(f"修改密码失败: {e}") logger.error(f"修改密码失败: {e}")
raise HTTPException(status_code=400, detail="修改密码失败") raise HTTPException(status_code=400, detail="修改密码失败")
@app.post("/api/accounts/{account_id}/reset-password")
async def reset_password(
account_id: str,
current_user: dict = Depends(get_current_user)
):
"""管理员重置用户密码"""
try:
# 检查当前用户是否是admin
admin_user = backend_account_manager.get_user_by_username(current_user["username"])
if not admin_user or admin_user.get("username") != "admin":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="需要管理员权限"
)
# 增强account_id格式验证
logger.info(f"完整请求参数: account_id={account_id}")
# 去除可能的空格和引号
clean_account_id = account_id.strip().strip('"').strip("'")
logger.info(f"清理后的account_id: {clean_account_id}")
try:
# 严格验证UUID格式
if len(clean_account_id) != 36 or clean_account_id.count("-") != 4:
raise ValueError("UUID格式不正确")
parsed_uuid = uuid.UUID(clean_account_id)
logger.info(f"成功解析为UUID: {parsed_uuid}")
except ValueError as e:
logger.error(f"无效的account_id格式: {clean_account_id}, 原始值: {account_id}, 错误: {str(e)}")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"账号ID格式无效: {clean_account_id} (必须是标准的UUID格式)"
)
# 重置密码
success = AccountManager.reset_password(account_id)
if not success:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="账号不存在"
)
return {"message": "密码重置成功"}
except Exception as e:
logger.error(f"重置密码失败: {e}")
raise HTTPException(status_code=400, detail="重置密码失败")
# 租户管理路由组 # 租户管理路由组
@app.post("/api/tenants/") @app.post("/api/tenants/")
async def create_tenant(tenant: TenantCreate, current_user: dict = Depends(get_current_user)): async def create_tenant(tenant: TenantCreate, current_user: dict = Depends(get_current_user)):

View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEA5j3p4ndQP1Qc8v2Jl8CVHu13phNaJ/gBEtt9Rrxmx149n/7X
Wgs3GY7/MMV+AvrZuDDy8tY1J7B1yQySG10Z/I25MHrgSvuNfdzxxcN64wb2cgKz
hsfjqGYOtd5dRRTjLAP1H3+lN7hd9Y0izPN4rM8I9Nz+GUpcks3nxcw5BEHolWIo
LM5L2Or9S4ZIcVtI65rkwoucSzfCDfVbda4rXEFCZjkDQyI/pwRfta6EZQrez/Lf
M6b3sJjBP7Ub9EaKxwAbc2YgcFHIvq8HCtEkSCsjvq+75inADUet0hXlaL1xl0Uf
PYEKk6CftEX/PKEdsWrDdJEth9+R1/xJEYavOQIDAQABAoIBAApagNMMLliOHvf/
w7lGUc5b4BRObOeF7iS3KDA86MhHoPnw0dy8yxYopM5lAf4jT8uA0/9sidHPlWIT
TtpjcXekbeCKuW/DekDfwNoaTVWVxhG7tKZJ4B9HIC3KxyMWWCduDUWW+6Y3cbdb
SMHXEUmkqFCFaGbrdMYbPLRJRYLRWBQQIfHWJKoWfngUr5iRqStmSoUhyY1PwgAO
rD4DhQZOqOYE8DotIA7q5raawKcX5qIf6sEjm+QtmVNQ3g53QPNSTTwlXVQ7vTYz
znkQZBgHIc/6Wb0iLDGUyMT7abHVcr8/EuRCLYSn5Xlcp9nESYE0y4agOx4MPHxw
/x5Kn1MCgYEA7Gab2umVqcS+e+OP8vVPpVvC+G2XeALg7g0M5xnHRYLmlm1GoycO
tG8XcEpI6IsZFBqKtyrpmE+qE2F1mf5oxab6pI0IGvH6IVVibpKdi60CV3lF+d5T
ayZ/7nvMBrXSdhOXacuk+o3qqhjTsqKoy85CNQqwNcYAZu+4+UT0k58CgYEA+VSW
HYnlya0AKWXza8SxU8n3Y1UYMZbbjpUt5Xq6n7nfxQlMRcqwii21u6WeXza2binH
KTOzWW2fLyT4n67bhM6IGYLcJY6hSjvvXGAzftt+tbOGu9PJlWVe4ELLz+kHdhV3
rbjZDEISRi8VGTVZRQghjLU8yerkMMLxcQ5ejicCgYEAyzjuVMOnMFl88x3Oeqtd
+6YlttDnfHjlCl/Xrrefcec0+S4ZoloKLxytRo/lm1swhPLIOuw+AfzCFYUbxvVI
9lk0cM74n8lTIOK5CpspqpBhSfdsK4Bvr9ZZ9hcgbshRk8YFzSIOwoHLsMxE+PUS
LJo0mkqE7sU3RUZhepBHvLsCgYEA20KglK9tDXL+/mjyrSYXD2lADfGKSimxQO09
pF3OeqJ5/4uSsJlzsMBL3g3ifTbfLXe99iTKJu25HDt2DO83is4Zb93dfYW1n1Of
xmuvPXMHNgD/jnPMBX5U9gCnvVnfPt/YFETHUvlTmrbS5g09SPDCmDvVjnfrXlpA
+zw4uOcCgYEAthxwyIrBFJfSR1YWsuhFIXcjXb68UODOL0uJJsnQB61A1+mVLlO9
SO/zQ9ItZYGu4/OFB1fYb7r9n0P0TMICJ0d/uK8AfAcgV6Y1ruTvzgVkhyoPb/p4
eE/C8xdf99RLUfd30D3UeQXYIi9EU57sHCfoupjNGk7P+g7XwrWojQ8=
-----END RSA PRIVATE KEY-----

View File

@ -23,3 +23,6 @@ python-jose>=3.3.0
passlib>=1.7.4 passlib>=1.7.4
python-multipart>=0.0.6 python-multipart>=0.0.6
werkzeug>=2.3.7 werkzeug>=2.3.7
# 邮箱验证
email_validator>=2.2.0

View File

@ -12,8 +12,12 @@ export const fetchAccounts = (params: AccountListParams) =>
total: number total: number
}>({ }>({
method: 'GET', method: 'GET',
url: '/accounts/search', url: '/api/accounts/search',
params params: {
page: params.page,
page_size: params.page_size,
search: params.search
}
}) })
export const updateAccount = (id: string, data: UpdateAccountParams) => export const updateAccount = (id: string, data: UpdateAccountParams) =>
@ -21,7 +25,7 @@ export const updateAccount = (id: string, data: UpdateAccountParams) =>
message: string message: string
}>({ }>({
method: 'PATCH', method: 'PATCH',
url: `/accounts/${id}`, url: `/api/accounts/${id}`,
data data
}) })
@ -30,7 +34,7 @@ export const resetPassword = (id: string, data: ResetPasswordParams) =>
message: string message: string
}>({ }>({
method: 'POST', method: 'POST',
url: `/accounts/${id}/reset-password`, url: `/api/accounts/${id}/reset-password`,
data data
}) })
@ -39,7 +43,7 @@ export const toggleAccountStatus = (id: string) =>
message: string message: string
}>({ }>({
method: 'POST', method: 'POST',
url: `/accounts/${id}/toggle-status` url: `/api/accounts/${id}/toggle-status`
}) })
export const createAccount = (data: { username: string }) => export const createAccount = (data: { username: string }) =>
@ -48,6 +52,56 @@ export const createAccount = (data: { username: string }) =>
account: AccountItem account: AccountItem
}>({ }>({
method: 'POST', method: 'POST',
url: '/accounts', url: '/api/accounts',
data data
}) })
// Dify账号相关API
export const getDifyAccount = (username: string) =>
request<{
user_id: string
username: string
email: string
created_at: string
tenants: Array<{
tenant_id: string
tenant_name: string
role: string
current: boolean
}>
}>({
method: 'GET',
url: `/api/dify_accounts/${username}`
})
export const getDifyAccounts = (params: { page: number; page_size: number }) =>
request<{
accounts: AccountItem[]
total: number
}>({
method: 'GET',
url: '/api/dify_accounts',
params
})
export const createDifyAccount = (data: {
username: string
email: string
password: string
}) =>
request<{
message: string
account: AccountItem
}>({
method: 'POST',
url: '/api/dify_accounts',
data
})
export const resetDifyPassword = (id: string) =>
request<{
message: string
}>({
method: 'POST',
url: `/api/accounts/${id}/reset-password`
})

View File

@ -1,3 +1,10 @@
export interface TenantInfo {
tenant_id: string
tenant_name: string
role: string
current: boolean
}
export interface AccountItem { export interface AccountItem {
id: string id: string
username: string username: string
@ -5,11 +12,12 @@ export interface AccountItem {
status: 'active' | 'disabled' status: 'active' | 'disabled'
createdAt: string createdAt: string
updatedAt: string updatedAt: string
tenants?: TenantInfo[]
} }
export interface AccountListParams { export interface AccountListParams {
page?: number page?: number
pageSize?: number page_size?: number
username?: string username?: string
email?: string email?: string
status?: 'active' | 'disabled' status?: 'active' | 'disabled'

View File

@ -1,7 +1,7 @@
import { request } from '../../axios/service' import { request } from '../../axios/service'
import type { LoginParams, RegisterParams } from '@/api/auth/types' import type { LoginParams, RegisterParams, LoginForm } from '@/api/auth/types'
export const login = (formData: FormData) => export const login = (formData: FormData | LoginForm) =>
request<{ access_token: string }>({ request<{ access_token: string }>({
method: 'POST', method: 'POST',
url: '/api/auth/login', url: '/api/auth/login',

View File

@ -1,9 +1,14 @@
export type LoginParams = { export interface LoginParams {
username: string username: string
password: string password: string
tenantId?: string tenantId?: string
} }
export interface LoginForm {
username: string
password: string
}
export type LoginFormData = FormData export type LoginFormData = FormData
export type RegisterParams = { export type RegisterParams = {

View File

@ -1,7 +1,7 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { ref } from 'vue' import { ref } from 'vue'
import { loginApi } from '../../api/login' import { login } from '../../api/auth'
import type { LoginForm } from '../../api/login/types' import type { LoginForm } from '../../api/auth/types'
import { setToken as setStorageToken, removeToken } from '../../utils/storage' import { setToken as setStorageToken, removeToken } from '../../utils/storage'
export const useUserStore = defineStore('user', () => { export const useUserStore = defineStore('user', () => {
@ -17,10 +17,13 @@ export const useUserStore = defineStore('user', () => {
userInfo.value = info userInfo.value = info
} }
const login = async (form: LoginForm) => { const login = async (params: LoginForm): Promise<{ access_token: string }> => {
const res = await loginApi(form) const formData = new FormData()
setToken(res.token) formData.append('username', params.username)
setUserInfo(res.userInfo) formData.append('password', params.password)
const res = await login(formData as FormData)
setToken(res.access_token)
return res return res
} }

View File

@ -21,9 +21,10 @@
</div> </div>
<el-table :data="filteredAccountList" border style="width: 100%"> <el-table :data="filteredAccountList" border style="width: 100%">
<el-table-column prop="id" label="ID" width="240" />
<el-table-column prop="username" label="用户名" width="180" /> <el-table-column prop="username" label="用户名" width="180" />
<el-table-column prop="email" label="邮箱" width="220" /> <el-table-column prop="email" label="邮箱" width="220" />
<el-table-column prop="status" label="状态" width="120"> <el-table-column prop="status" label="状态" width="100">
<template #default="{ row }"> <template #default="{ row }">
<el-tag :type="row.status === 'active' ? 'success' : 'danger'"> <el-tag :type="row.status === 'active' ? 'success' : 'danger'">
{{ row.status === 'active' ? '启用' : '禁用' }} {{ row.status === 'active' ? '启用' : '禁用' }}
@ -31,9 +32,24 @@
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="createdAt" label="创建时间" width="180" /> <el-table-column prop="createdAt" label="创建时间" width="180" />
<el-table-column label="操作" width="220"> <el-table-column label="操作" width="280" align="center">
<template #default="{ row }"> <template #default="{ row }">
<el-button size="small" @click="handleEdit(row)">编辑</el-button> <el-button
plain
size="small"
@click="handleResetPassword(row)"
style="color: #409EFF; border-color: #c6e2ff; background-color: #ecf5ff;"
>
重置密码
</el-button>
<el-button
plain
size="small"
@click="handleShowTenants(row.username)"
style="color: #67C23A; border-color: #e1f3d8; background-color: #f0f9eb; margin-left: 10px;"
>
关联租户
</el-button>
<el-button <el-button
size="small" size="small"
type="danger" type="danger"
@ -49,10 +65,32 @@
v-model:current-page="pagination.current" v-model:current-page="pagination.current"
v-model:page-size="pagination.size" v-model:page-size="pagination.size"
:total="pagination.total" :total="pagination.total"
@current-change="fetchAccounts" @current-change="fetchAccountData"
layout="total, sizes, prev, pager, next, jumper" layout="total, sizes, prev, pager, next, jumper"
/> />
</el-card> </el-card>
<!-- 租户信息弹窗 -->
<el-dialog v-model="tenantDialogVisible" title="关联租户信息" width="60%">
<el-table :data="tenantList" border v-loading="loading">
<el-table-column prop="tenant_id" label="租户ID" width="220" />
<el-table-column prop="tenant_name" label="租户名称" width="180" />
<el-table-column prop="role" label="角色" width="120">
<template #default="{ row }">
<el-tag :type="getRoleTagType(row.role)">
{{ getRoleDisplayName(row.role) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="current" label="当前租户" width="120">
<template #default="{ row }">
<el-tag :type="row.current ? 'success' : 'info'">
{{ row.current ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
</el-table>
</el-dialog>
</div> </div>
</template> </template>
@ -62,7 +100,8 @@ import {
fetchAccounts, fetchAccounts,
toggleAccountStatus, toggleAccountStatus,
createAccount, createAccount,
updateAccount resetPassword,
getDifyAccount
} from '@/api/account' } from '@/api/account'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
@ -72,12 +111,15 @@ interface Account {
email: string email: string
status: 'active' | 'disabled' status: 'active' | 'disabled'
createdAt: string createdAt: string
isDify?: boolean
} }
const accountList = ref<Account[]>([]) const accountList = ref<Account[]>([])
const filteredAccountList = ref<Account[]>([]) const filteredAccountList = ref<Account[]>([])
const tenantList = ref<any[]>([])
const searchQuery = ref('') const searchQuery = ref('')
const loading = ref(false) const loading = ref(false)
const tenantDialogVisible = ref(false)
const pagination = ref({ const pagination = ref({
current: 1, current: 1,
@ -90,10 +132,15 @@ const fetchAccountData = async () => {
loading.value = true loading.value = true
const res = await fetchAccounts({ const res = await fetchAccounts({
page: pagination.value.current, page: pagination.value.current,
pageSize: pagination.value.size page_size: pagination.value.size,
search: searchQuery.value || undefined
}) })
accountList.value = res.accounts accountList.value = res.accounts.map(account => ({
...account,
isDify: account.email?.endsWith('@dify.com')
}))
pagination.value.total = res.total pagination.value.total = res.total
filteredAccountList.value = res.accounts
} finally { } finally {
loading.value = false loading.value = false
} }
@ -125,38 +172,79 @@ const handleCreate = () => {
}) })
} }
const handleEdit = (account: Account) => { const handleResetPassword = async (account: Account) => {
ElMessageBox.prompt('请输入新用户名', '编辑账号', { try {
confirmButtonText: '确定', await ElMessageBox.confirm('确定要重置该账号的密码吗?', '重置密码', {
cancelButtonText: '取消', confirmButtonText: '确定',
inputValue: account.username, cancelButtonText: '取消',
inputPattern: /^[a-zA-Z0-9_]{4,20}$/, type: 'warning'
inputErrorMessage: '用户名必须为4-20位字母数字或下划线' })
}).then(async ({ value }) => { await resetPassword(account.id)
try { ElMessage.success('密码重置成功')
await updateAccount(account.id, { username: value }) } catch (error) {
ElMessage.success('账号更新成功') if (error !== 'cancel') {
await fetchAccountData() ElMessage.error('密码重置失败')
} catch (error) {
ElMessage.error('账号更新失败')
} }
}) }
} }
const handleSearchClear = () => { const handleShowTenants = async (username: string) => {
try {
loading.value = true
const res = await getDifyAccount(username)
console.log('租户信息:', res) //
tenantList.value = res.tenants || []
if (tenantList.value.length === 0) {
ElMessage.warning('该账号未关联任何租户')
} else {
tenantDialogVisible.value = true
}
} catch (error) {
console.error('获取租户信息失败:', error)
ElMessage.error('获取租户信息失败: ' + error.message)
} finally {
loading.value = false
}
}
const handleSearchClear = async () => {
searchQuery.value = ''
await fetchAccountData()
filteredAccountList.value = accountList.value filteredAccountList.value = accountList.value
} }
const getRoleTagType = (role) => {
const roleMap = {
'owner': 'danger',
'administrator': 'warning',
'editor': 'primary',
'member': 'info'
}
return roleMap[role.toLowerCase()] || 'primary'
}
const getRoleDisplayName = (role) => {
const roleMap = {
'owner': '所有者',
'administrator': '管理员',
'editor': '编辑者',
'member': '成员'
}
return roleMap[role.toLowerCase()] || role
}
const handleSearch = async () => { const handleSearch = async () => {
try { try {
loading.value = true loading.value = true
pagination.value.current = 1
const res = await fetchAccounts({ const res = await fetchAccounts({
page: 1, page: 1,
pageSize: pagination.value.size, page_size: pagination.value.size,
search: searchQuery.value search: searchQuery.value
}) })
filteredAccountList.value = res.accounts filteredAccountList.value = res.accounts
pagination.value.total = res.total pagination.value.total = res.total
accountList.value = res.accounts
} finally { } finally {
loading.value = false loading.value = false
} }