Merge pull request #449 from RWDai/feat/audit-admin-readonly

Add read-only audit administrator role
This commit is contained in:
fawney19
2026-05-14 15:51:27 +08:00
committed by GitHub
28 changed files with 305 additions and 61 deletions

View File

@@ -414,7 +414,7 @@
使用记录
</Button>
<Button
v-if="canCancel(task.status)"
v-if="authStore.canOperateAdmin && canCancel(task.status)"
variant="outline"
size="sm"
class="h-7 text-xs text-red-500 border-red-200 hover:bg-red-50"
@@ -830,7 +830,7 @@
<!-- 操作按钮 -->
<div
v-if="canCancel(selectedTask.status)"
v-if="authStore.canOperateAdmin && canCancel(selectedTask.status)"
class="pt-4 border-t border-border/60"
>
<Button
@@ -902,7 +902,7 @@ import { useAuthStore } from '@/stores/auth'
import { log } from '@/utils/logger'
const authStore = useAuthStore()
const isAdmin = computed(() => authStore.user?.role === 'admin')
const isAdmin = computed(() => authStore.canAccessAdmin)
const { toast } = useToast()
const { copyToClipboard } = useClipboard()

View File

@@ -65,6 +65,9 @@
<SelectItem value="admin">
管理员
</SelectItem>
<SelectItem value="audit_admin">
审计管理员
</SelectItem>
<SelectItem value="user">
用户
</SelectItem>
@@ -148,6 +151,9 @@
<SelectItem value="admin">
管理员
</SelectItem>
<SelectItem value="audit_admin">
审计管理员
</SelectItem>
<SelectItem value="user">
普通用户
</SelectItem>
@@ -200,6 +206,7 @@
<!-- 新增用户按钮 -->
<Button
v-if="authStore.canOperateAdmin"
variant="ghost"
size="icon"
class="h-8 w-8"
@@ -211,6 +218,7 @@
<!-- 新增用户按钮 -->
<Button
v-if="authStore.canOperateAdmin"
variant="ghost"
size="icon"
class="h-8 w-8"
@@ -262,6 +270,7 @@
清空选择
</Button>
<Button
v-if="authStore.canOperateAdmin"
size="sm"
class="h-7 px-3 text-[11px]"
:disabled="(selectedCount === 0 && userGroups.length === 0) || usersStore.loading"
@@ -362,10 +371,10 @@
{{ user.username }}
</div>
<Badge
:variant="user.role === 'admin' ? 'default' : 'secondary'"
:variant="userRoleBadgeVariant(user.role)"
class="h-5 px-1.5 py-0 text-[10px] font-medium flex-shrink-0"
>
{{ user.role === 'admin' ? '管理员' : '普通用户' }}
{{ formatUserRole(user.role) }}
</Badge>
</div>
<div
@@ -468,6 +477,7 @@
<TableCell class="py-4">
<div class="flex justify-center gap-1">
<Button
v-if="authStore.canOperateAdmin"
variant="ghost"
size="icon"
class="h-8 w-8"
@@ -477,6 +487,7 @@
<SquarePen class="h-4 w-4" />
</Button>
<Button
v-if="authStore.canOperateAdmin"
variant="ghost"
size="icon"
class="h-8 w-8"
@@ -486,6 +497,7 @@
<DollarSign class="h-4 w-4" />
</Button>
<Button
v-if="authStore.canOperateAdmin"
variant="ghost"
size="icon"
class="h-8 w-8"
@@ -495,6 +507,7 @@
<Key class="h-4 w-4" />
</Button>
<Button
v-if="authStore.canOperateAdmin"
variant="ghost"
size="icon"
class="h-8 w-8"
@@ -588,10 +601,10 @@
{{ user.username }}
</div>
<Badge
:variant="user.role === 'admin' ? 'default' : 'secondary'"
:variant="userRoleBadgeVariant(user.role)"
class="h-5 px-1.5 py-0 text-[10px] font-medium flex-shrink-0"
>
{{ user.role === 'admin' ? '管理员' : '普通用户' }}
{{ formatUserRole(user.role) }}
</Badge>
</div>
<div
@@ -694,6 +707,7 @@
<div class="grid grid-cols-2 gap-2 pt-0.5">
<Button
v-if="authStore.canOperateAdmin"
variant="outline"
size="sm"
class="h-8 text-xs"
@@ -703,6 +717,7 @@
编辑
</Button>
<Button
v-if="authStore.canOperateAdmin"
variant="outline"
size="sm"
class="h-8 text-xs"
@@ -712,6 +727,7 @@
资金
</Button>
<Button
v-if="authStore.canOperateAdmin"
variant="outline"
size="sm"
class="h-8 text-xs"
@@ -721,6 +737,7 @@
API Keys
</Button>
<Button
v-if="authStore.canOperateAdmin"
variant="outline"
size="sm"
class="h-8 text-xs"
@@ -1236,6 +1253,7 @@
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue'
import { useUsersStore } from '@/stores/users'
import { useAuthStore } from '@/stores/auth'
import type { User, ApiKey, UserSession, UserBatchActionResponse, UserBatchSelectionFilters, UserGroup } from '@/api/users'
import { formatSessionMeta } from '@/types/session'
import { adminWalletApi, type AdminWallet } from '@/api/admin-wallets'
@@ -1310,6 +1328,7 @@ const { success, error } = useToast()
const { confirmDanger } = useConfirm()
const { copyToClipboard } = useClipboard()
const usersStore = useUsersStore()
const authStore = useAuthStore()
// 用户表单对话框状态
const showUserFormDialog = ref(false)
@@ -1354,6 +1373,7 @@ const userGroups = ref<UserGroup[]>([])
const userRoleFilterOptions = [
{ value: 'all', label: '全部角色' },
{ value: 'admin', label: '管理员' },
{ value: 'audit_admin', label: '审计管理员' },
{ value: 'user', label: '普通用户' },
]
const userStatusFilterOptions = [
@@ -1373,9 +1393,9 @@ const filteredUsers = computed(() => {
// 先排序:管理员优先,然后按创建时间倒序
filtered.sort((a, b) => {
// 管理员优先
if (a.role === 'admin' && b.role !== 'admin') return -1
if (a.role !== 'admin' && b.role === 'admin') return 1
const roleRank = (role: string) => role === 'admin' ? 0 : role === 'audit_admin' ? 1 : 2
const roleDiff = roleRank(a.role) - roleRank(b.role)
if (roleDiff !== 0) return roleDiff
// 同角色按创建时间倒序(新用户在前)
return new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
})
@@ -1437,7 +1457,7 @@ const batchSelectionFilters = computed<UserBatchSelectionFilters>(() => {
const filters: UserBatchSelectionFilters = {}
const search = searchQuery.value.trim()
if (search) filters.search = search
if (filterRole.value === 'admin' || filterRole.value === 'user') filters.role = filterRole.value
if (filterRole.value === 'admin' || filterRole.value === 'audit_admin' || filterRole.value === 'user') filters.role = filterRole.value
if (filterStatus.value === 'active') filters.is_active = true
if (filterStatus.value === 'inactive') filters.is_active = false
if (filterGroup.value !== 'all') filters.group_id = filterGroup.value
@@ -1452,6 +1472,16 @@ watch([searchQuery, filterRole, filterStatus, filterGroup], () => {
watch(paginatedUsers, (users) => rememberBatchPageUsers(users), { immediate: true })
function formatUserRole(role: string) {
if (role === 'admin') return '管理员'
if (role === 'audit_admin') return '审计管理员'
return '普通用户'
}
function userRoleBadgeVariant(role: string) {
return role === 'admin' ? 'default' : 'secondary'
}
onMounted(() => {
void refreshUsers({ preferCache: true })
})

View File

@@ -109,7 +109,7 @@ onMounted(async () => {
success('登录成功')
const redirectPath = consumeRedirectPath()
const target = redirectPath || (authStore.user?.role === 'admin' ? '/admin/dashboard' : '/dashboard')
const target = redirectPath || (authStore.canAccessAdmin ? '/admin/dashboard' : '/dashboard')
await router.replace(target)
})
</script>

View File

@@ -498,7 +498,7 @@ const { copyToClipboard } = useClipboard()
const { siteName, siteSubtitle } = useSiteInfo()
const dashboardPath = computed(() =>
authStore.user?.role === 'admin' ? '/admin/dashboard' : '/dashboard'
authStore.canAccessAdmin ? '/admin/dashboard' : '/dashboard'
)
const baseUrl = computed(() => window.location.origin)

View File

@@ -8,10 +8,10 @@
class="flex-1 min-w-0 flex flex-col"
>
<Badge
:variant="authStore.user?.role === 'admin' ? 'default' : 'secondary'"
:variant="authStore.isAdmin ? 'default' : 'secondary'"
class="uppercase tracking-[0.45em] mb-4 self-start"
>
{{ authStore.user?.role === 'admin' ? 'ADMIN MODE' : 'PERSONAL MODE' }}
{{ dashboardModeLabel }}
</Badge>
<!-- 主要统计卡片 -->
@@ -915,7 +915,12 @@ function setupTimelineResizeObserver() {
announcementsTimelineObserver.observe(container)
}
const isAdmin = computed(() => authStore.user?.role === 'admin')
const isAdmin = computed(() => authStore.canAccessAdmin)
const dashboardModeLabel = computed(() => {
if (authStore.isAdmin) return 'ADMIN MODE'
if (authStore.isAuditAdmin) return 'AUDIT MODE'
return 'PERSONAL MODE'
})
const statCardBorders = [
'border-book-cloth/30 dark:border-book-cloth/25',

View File

@@ -50,15 +50,15 @@
>
<UsageModelTable
:data="enhancedModelStats"
:is-admin="authStore.isAdmin"
:is-admin="authStore.canAccessAdmin"
/>
<UsageProviderTable
:data="providerStats"
:is-admin="authStore.isAdmin"
:is-admin="authStore.canAccessAdmin"
/>
<UsageApiFormatTable
:data="apiFormatStats"
:is-admin="authStore.isAdmin"
:is-admin="authStore.canAccessAdmin"
/>
</div>
<!-- 用户模型 + API格式2 -->
@@ -68,7 +68,7 @@
>
<UsageModelTable
:data="enhancedModelStats"
:is-admin="authStore.isAdmin"
:is-admin="authStore.canAccessAdmin"
/>
<UsageApiFormatTable
:data="apiFormatStats"
@@ -81,7 +81,7 @@
<UsageRecordsTable
:records="displayRecords"
:is-admin="isAdminPage"
:show-actual-cost="authStore.isAdmin"
:show-actual-cost="authStore.canAccessAdmin"
:loading="isLoadingRecords"
:time-range="timeRange"
:filter-search="filterSearch"

View File

@@ -623,7 +623,7 @@ async function loadAnnouncements(page = 1) {
currentPage.value = page
try {
const response = await announcementApi.getAnnouncements({
active_only: !isAdmin.value, // 管理员可以看到所有公告
active_only: !authStore.canAccessAdmin, // 管理员和审计管理员可以看到所有公告
limit: pageSize.value,
offset: (page - 1) * pageSize.value
})

View File

@@ -561,7 +561,7 @@
<div class="flex justify-between">
<span class="text-muted-foreground">角色</span>
<Badge :variant="profile?.role === 'admin' ? 'default' : 'secondary'">
{{ profile?.role === 'admin' ? '管理员' : '普通用户' }}
{{ profileRoleLabel }}
</Badge>
</div>
<div class="flex justify-between">
@@ -682,6 +682,11 @@ const { setThemeMode } = useDarkMode()
const profile = ref<Profile | null>(null)
const userSessions = ref<UserSession[]>([])
const profileRoleLabel = computed(() => {
if (profile.value?.role === 'admin') return '管理员'
if (profile.value?.role === 'audit_admin') return '审计管理员'
return '普通用户'
})
const profileForm = ref({
email: '',