mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
Merge branch 'main' into opencode/sunny-orchid
# Conflicts: # README.md
This commit is contained in:
@@ -117,7 +117,7 @@ async function syncExternalAuthState(nextToken: string | null): Promise<void> {
|
||||
return
|
||||
}
|
||||
|
||||
if (router.currentRoute.value.path.startsWith('/admin') && user.role !== 'admin') {
|
||||
if (router.currentRoute.value.path.startsWith('/admin') && !authStore.canAccessAdmin) {
|
||||
await router.replace('/dashboard')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,7 +340,7 @@ async function handleLogin() {
|
||||
// 延迟一下让用户看到成功消息
|
||||
setTimeout(() => {
|
||||
// 根据用户角色跳转到不同的仪表盘
|
||||
const targetPath = authStore.user?.role === 'admin' ? '/admin/dashboard' : '/dashboard'
|
||||
const targetPath = authStore.canAccessAdmin ? '/admin/dashboard' : '/dashboard'
|
||||
router.push(targetPath)
|
||||
}, 1000)
|
||||
} else {
|
||||
|
||||
@@ -135,12 +135,15 @@
|
||||
<SelectItem value="admin">
|
||||
管理员
|
||||
</SelectItem>
|
||||
<SelectItem value="audit_admin">
|
||||
审计管理员
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-amber-200/70 bg-amber-50/70 px-3 py-2.5 text-xs leading-relaxed text-amber-800 dark:border-amber-900/50 dark:bg-amber-950/30 dark:text-amber-200">
|
||||
{{ targetRole === 'admin' ? '提示:设置为管理员会授予用户后台管理能力。' : '提示:设置为普通用户会移除目标用户的管理员权限。' }}
|
||||
{{ targetRole === 'admin' ? '提示:设置为管理员会授予用户完整后台管理能力。' : targetRole === 'audit_admin' ? '提示:设置为审计管理员会授予后台只读查看能力。' : '提示:设置为普通用户会移除目标用户的管理员权限。' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -80,6 +80,9 @@
|
||||
<SelectItem value="admin">
|
||||
管理员
|
||||
</SelectItem>
|
||||
<SelectItem value="audit_admin">
|
||||
审计管理员
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
</div>
|
||||
<div class="flex flex-col min-w-0">
|
||||
<span class="text-xs font-semibold leading-none truncate opacity-90 text-foreground">{{ authStore.user?.username }}</span>
|
||||
<span class="text-[10px] opacity-50 leading-none mt-1.5 text-muted-foreground">{{ authStore.user?.role === 'admin' ? '管理员' : '用户' }}</span>
|
||||
<span class="text-[10px] opacity-50 leading-none mt-1.5 text-muted-foreground">{{ currentRoleLabel }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -227,7 +227,7 @@
|
||||
</div>
|
||||
<div class="flex flex-col min-w-0">
|
||||
<span class="text-sm font-semibold leading-none truncate text-[#191919] dark:text-white">{{ authStore.user?.username }}</span>
|
||||
<span class="text-[10px] text-[#91918d] dark:text-muted-foreground leading-none mt-1">{{ authStore.user?.role === 'admin' ? '管理员' : '用户' }}</span>
|
||||
<span class="text-[10px] text-[#91918d] dark:text-muted-foreground leading-none mt-1">{{ currentRoleLabel }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
@@ -520,7 +520,7 @@ function showDebugVersionStatus(hasUpdate = true) {
|
||||
// 检查更新
|
||||
async function checkForUpdate() {
|
||||
// 只有管理员才检查更新
|
||||
if (!isAdmin.value) return
|
||||
if (!authStore.canOperateAdmin) return
|
||||
|
||||
// 同一会话内只检查一次
|
||||
const sessionKey = 'aether_update_checked'
|
||||
@@ -567,7 +567,7 @@ onMounted(() => {
|
||||
syncAuthNotice()
|
||||
|
||||
// 管理员预加载模块状态(路由守卫会按需加载,这里提前加载以避免菜单闪烁)
|
||||
if (isAdmin.value && !moduleStore.loaded && !moduleStore.loading) {
|
||||
if (authStore.canAccessAdmin && !moduleStore.loaded && !moduleStore.loading) {
|
||||
moduleStore.fetchModules()
|
||||
}
|
||||
void loadVersionStatus()
|
||||
@@ -707,7 +707,13 @@ const navigation = computed(() => {
|
||||
}
|
||||
]
|
||||
|
||||
return authStore.user?.role === 'admin' ? adminNavigation : baseNavigation
|
||||
return authStore.canAccessAdmin ? adminNavigation : baseNavigation
|
||||
})
|
||||
|
||||
const currentRoleLabel = computed(() => {
|
||||
if (authStore.isAdmin) return '管理员'
|
||||
if (authStore.isAuditAdmin) return '审计管理员'
|
||||
return '用户'
|
||||
})
|
||||
|
||||
// Breadcrumbs
|
||||
|
||||
@@ -12,8 +12,7 @@ export async function checkAdminAccess(
|
||||
authStore: ReturnType<typeof useAuthStore>,
|
||||
moduleStore: ReturnType<typeof useModuleStore>
|
||||
): Promise<string | null> {
|
||||
const isAdmin = authStore.user?.role === 'admin'
|
||||
if (!isAdmin) {
|
||||
if (!authStore.canAccessAdmin) {
|
||||
log.warn('Non-admin user attempted to access admin page, redirecting to user dashboard')
|
||||
return '/dashboard'
|
||||
}
|
||||
|
||||
@@ -26,12 +26,11 @@ export function resolveHomeRedirect(
|
||||
}
|
||||
|
||||
// 已登录用户首次访问首页(非返回/刷新场景),根据角色跳转到对应仪表盘
|
||||
const isAdmin = authStore.user?.role === 'admin'
|
||||
const redirectPath = sessionStorage.getItem('redirectPath')
|
||||
if (redirectPath && redirectPath !== '/') {
|
||||
sessionStorage.removeItem('redirectPath')
|
||||
return redirectPath
|
||||
}
|
||||
|
||||
return isAdmin ? '/admin/dashboard' : '/dashboard'
|
||||
return authStore.canAccessAdmin ? '/admin/dashboard' : '/dashboard'
|
||||
}
|
||||
|
||||
@@ -105,4 +105,21 @@ describe('auth store logout', () => {
|
||||
expect(store.user).toBeNull()
|
||||
expect(store.token).toBeNull()
|
||||
})
|
||||
|
||||
it('separates admin access from admin operations for audit administrators', () => {
|
||||
const store = useAuthStore()
|
||||
|
||||
store.user = {
|
||||
id: 'audit-1',
|
||||
username: 'auditor',
|
||||
role: 'audit_admin',
|
||||
is_active: true,
|
||||
created_at: '2026-03-16T00:00:00Z',
|
||||
}
|
||||
|
||||
expect(store.isAdmin).toBe(false)
|
||||
expect(store.isAuditAdmin).toBe(true)
|
||||
expect(store.canAccessAdmin).toBe(true)
|
||||
expect(store.canOperateAdmin).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -32,6 +32,9 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
}
|
||||
}
|
||||
const isAdmin = computed(() => user.value?.role === 'admin')
|
||||
const isAuditAdmin = computed(() => user.value?.role === 'audit_admin')
|
||||
const canAccessAdmin = computed(() => isAdmin.value || isAuditAdmin.value)
|
||||
const canOperateAdmin = computed(() => isAdmin.value)
|
||||
|
||||
async function login(email: string, password: string, authType: 'local' | 'ldap' = 'local') {
|
||||
loading.value = true
|
||||
@@ -114,6 +117,9 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
error,
|
||||
isAuthenticated,
|
||||
isAdmin,
|
||||
isAuditAdmin,
|
||||
canAccessAdmin,
|
||||
canOperateAdmin,
|
||||
login,
|
||||
logout,
|
||||
applyExternalLogout,
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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 })
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
Server,
|
||||
Key,
|
||||
Container,
|
||||
Code,
|
||||
Shield,
|
||||
Monitor,
|
||||
Check,
|
||||
@@ -44,38 +43,11 @@ const productionSteps = [
|
||||
}
|
||||
]
|
||||
|
||||
const localBuildSteps = [
|
||||
{
|
||||
title: '克隆代码',
|
||||
code: 'git clone https://github.com/fawney19/Aether.git\ncd Aether',
|
||||
icon: Code
|
||||
},
|
||||
{
|
||||
title: '配置环境变量',
|
||||
note: '生成密钥并填入 .env',
|
||||
code: 'cp .env.example .env\npython generate_keys.py',
|
||||
icon: Key
|
||||
},
|
||||
{
|
||||
title: '构建',
|
||||
note: '自动构建、启动、迁移',
|
||||
code: './deploy.sh',
|
||||
icon: Container
|
||||
},
|
||||
{
|
||||
title: '更新',
|
||||
note: '需要拉取最新代码',
|
||||
code: 'git pull origin main',
|
||||
icon: Code,
|
||||
optional: true
|
||||
}
|
||||
]
|
||||
|
||||
const developmentSteps = [
|
||||
{
|
||||
title: '启动依赖',
|
||||
note: 'PostgreSQL + Redis',
|
||||
code: 'docker compose -f docker-compose.build.yml up -d postgres redis',
|
||||
code: 'docker compose up -d postgres redis',
|
||||
icon: Container
|
||||
},
|
||||
{
|
||||
@@ -133,7 +105,6 @@ function copyStep(stepId: string, code: string) {
|
||||
<button
|
||||
v-for="(tab, idx) in [
|
||||
{ icon: Container, label: 'Docker 预构建镜像' },
|
||||
{ icon: Code, label: '本地代码构建' },
|
||||
{ icon: Monitor, label: '本地开发' }
|
||||
]"
|
||||
:key="idx"
|
||||
@@ -200,58 +171,9 @@ function copyStep(stepId: string, code: string) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 本地构建步骤 -->
|
||||
<div
|
||||
v-show="activeDeployTab === 1"
|
||||
class="p-5 space-y-3"
|
||||
>
|
||||
<div
|
||||
v-for="(step, idx) in localBuildSteps"
|
||||
:key="idx"
|
||||
class="group rounded-xl border border-[#e5e4df] dark:border-[rgba(227,224,211,0.12)] overflow-hidden transition-colors"
|
||||
:class="step.optional ? 'border-dashed opacity-80' : ''"
|
||||
>
|
||||
<div class="flex items-center gap-3 px-4 py-3">
|
||||
<span class="w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold flex-shrink-0 bg-[#cc785c] text-white">
|
||||
{{ idx + 1 }}
|
||||
</span>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm font-medium text-[#262624] dark:text-[#f1ead8]">{{ step.title }}</span>
|
||||
<span
|
||||
v-if="step.optional"
|
||||
class="text-[10px] px-1.5 py-0.5 rounded-full bg-[#e5e4df] dark:bg-[rgba(227,224,211,0.12)] text-[#666663] dark:text-[#a3a094]"
|
||||
>
|
||||
更新时
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
v-if="step.note"
|
||||
class="text-xs text-[#91918d] dark:text-[#a3a094]/80"
|
||||
>{{ step.note }}</span>
|
||||
</div>
|
||||
<button
|
||||
class="flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs text-[#666663] dark:text-[#a3a094] hover:bg-[#f0f0eb] dark:hover:bg-[#3a3731] transition-colors shrink-0"
|
||||
@click="copyStep(`build-${idx}`, step.code)"
|
||||
>
|
||||
<Check
|
||||
v-if="copiedStep === `build-${idx}`"
|
||||
class="h-3.5 w-3.5 text-green-500"
|
||||
/>
|
||||
<Copy
|
||||
v-else
|
||||
class="h-3.5 w-3.5"
|
||||
/>
|
||||
{{ copiedStep === `build-${idx}` ? '已复制' : '复制' }}
|
||||
</button>
|
||||
</div>
|
||||
<pre class="px-4 pb-3 text-[13px] font-mono text-[#262624] dark:text-[#f1ead8] overflow-x-auto leading-relaxed border-t border-[#e5e4df]/50 dark:border-[rgba(227,224,211,0.06)] pt-3 mx-4 mb-1"><code>{{ step.code }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 开发环境步骤 -->
|
||||
<div
|
||||
v-show="activeDeployTab === 2"
|
||||
v-show="activeDeployTab === 1"
|
||||
class="p-5 space-y-3"
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -20,28 +20,11 @@ docker compose pull && docker compose up -d
|
||||
docker compose exec postgres pg_dump -U postgres aether | gzip > backup_$(date +%Y%m%d_%H%M%S).sql.gz
|
||||
```
|
||||
|
||||
### 2. 本地代码构建镜像 (Docker Compose)
|
||||
```markdown
|
||||
# 1. 克隆代码
|
||||
git clone https://github.com/fawney19/Aether.git
|
||||
cd Aether
|
||||
|
||||
# 2. 配置环境变量
|
||||
cp .env.example .env
|
||||
./generate_keys.sh # 生成密钥, 并将生成的密钥填入 .env
|
||||
|
||||
# 3. 构建(自动构建、启动、迁移)
|
||||
./deploy.sh
|
||||
|
||||
# 4. 更新需要拉取最新代码
|
||||
git pull origin main
|
||||
```
|
||||
|
||||
### 3. 本地开发
|
||||
### 2. 本地开发
|
||||
依赖 Docker、uv、nodejs
|
||||
```markdown
|
||||
# 启动数据库
|
||||
docker compose -f docker-compose.build.yml up -d postgres redis
|
||||
docker compose up -d postgres redis
|
||||
|
||||
# 后端
|
||||
uv sync
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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: '',
|
||||
|
||||
Reference in New Issue
Block a user