Files
geo/apps/ops-web/src/layouts/AppShell.vue
T
root 162abdc97c
Backend CI / Backend (push) Has been cancelled
Frontend CI / Frontend (push) Failing after 1m39s
chore(frontend): introduce prettier + eslint and prune unused code
- Add Prettier 3 with prettier-plugin-organize-imports (sorts/removes unused imports)
- Add ESLint 10 flat config with typescript-eslint + eslint-plugin-vue + eslint-config-prettier
- Add root scripts: format, format:check, lint, lint:fix
- Reformat 257 files across admin-web, ops-web, desktop-client, packages
- Remove unused locals/exports flagged by --noUnusedLocals/--noUnusedParameters
- Fix duplicate localTabLabel key, surrogate-pair regex u-flag, NBSP literal in regex
- Skip server/ (Go) and apps/browser-extension/ (deprecated per ADR)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 20:39:09 +08:00

224 lines
5.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<a-layout style="min-height: 100vh">
<a-layout-sider v-model:collapsed="collapsed" collapsible breakpoint="lg" :trigger="null">
<div class="ops-brand">
<span v-if="!collapsed">省心推 · 运营</span>
<span v-else>SXT</span>
</div>
<a-menu
theme="light"
mode="inline"
:selected-keys="selectedKeys"
:open-keys="openKeys"
:items="menuItems"
@update:open-keys="onOpenKeysChange"
@click="onMenuClick"
/>
</a-layout-sider>
<a-layout>
<a-layout-header class="ops-header">
<a-button type="text" class="header-trigger" @click="collapsed = !collapsed">
{{ collapsed ? '' : '' }}
</a-button>
<span style="flex: 1" />
<a-dropdown placement="bottomRight">
<div class="user-dropdown-btn" style="cursor: pointer">
<div class="user-avatar">
{{ auth.operator?.display_name?.charAt(0) ?? 'U' }}
</div>
<span>{{ auth.operator?.display_name ?? '未登录' }}</span>
</div>
<template #overlay>
<a-menu @click="onUserMenuClick">
<a-menu-item key="profile">个人资料</a-menu-item>
<a-menu-item key="logout">退出登录</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-layout-header>
<a-layout-content class="ops-shell-content">
<router-view />
</a-layout-content>
</a-layout>
</a-layout>
</template>
<script setup lang="ts">
import {
AuditOutlined,
CrownOutlined,
GlobalOutlined,
SafetyCertificateOutlined,
SettingOutlined,
TeamOutlined,
} from '@ant-design/icons-vue'
import type { ItemType } from 'ant-design-vue'
import { computed, h, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
interface MenuLeaf {
key: string
label: string
path: string
}
const router = useRouter()
const route = useRoute()
const auth = useAuthStore()
const collapsed = ref(false)
const menuLeaves: MenuLeaf[] = [
{ key: '/admin-users', label: '用户管理', path: '/admin-users' },
{ key: '/kol-subscriptions', label: '订阅包审批', path: '/kol-subscriptions' },
{ key: '/site-domain-mappings', label: '站点映射', path: '/site-domain-mappings' },
{ key: '/accounts', label: '操作员管理', path: '/accounts' },
{ key: '/audits', label: '审计日志', path: '/audits' },
]
const menuItems = computed<ItemType[]>(() => [
{
key: '/admin-users',
label: '用户管理',
icon: () => h(TeamOutlined),
},
{
key: '/kol-subscriptions',
label: '订阅包审批',
icon: () => h(CrownOutlined),
},
{
key: '/site-domain-mappings',
label: '站点映射',
icon: () => h(GlobalOutlined),
},
{
key: 'system',
label: '系统设置',
icon: () => h(SettingOutlined),
children: [
{
key: '/accounts',
label: '操作员管理',
icon: () => h(SafetyCertificateOutlined),
},
{
key: '/audits',
label: '审计日志',
icon: () => h(AuditOutlined),
},
],
},
])
const openKeys = ref<string[]>(['system'])
function onOpenKeysChange(keys: string[]) {
openKeys.value = keys
}
const selectedKeys = computed(() => [route.path])
function onMenuClick(info: { key: string | number }) {
const item = menuLeaves.find((m) => m.key === info.key)
if (item) void router.push(item.path)
}
function onUserMenuClick(info: { key: string | number }) {
if (info.key === 'logout') {
auth.logout()
void router.replace({ name: 'login' })
} else if (info.key === 'profile') {
void router.push({ name: 'profile' })
}
}
onMounted(() => {
if (auth.isAuthenticated) {
void auth.refreshSelf()
}
})
</script>
<style scoped>
:deep(.ant-layout-sider) {
background: #fff !important;
border-right: 1px solid #f0f0f0;
}
.ops-brand {
height: 40px;
background: #eef2ff;
border: 1px solid #c7d2fe;
margin: 16px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
color: #4f46e5;
font-weight: 800;
font-size: 16px;
letter-spacing: 0.05em;
}
.ops-header {
background: #fff;
padding: 0 24px;
display: flex;
align-items: center;
height: 64px;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
z-index: 1;
position: relative;
}
.header-trigger {
font-size: 18px;
color: #8c8c8c;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
transition: all 0.2s;
}
.header-trigger:hover {
background-color: #f5f5f5;
color: #141414;
}
.user-dropdown-btn {
display: flex;
align-items: center;
gap: 8px;
color: #141414;
font-weight: 500;
border-radius: 6px;
padding: 4px 8px;
transition: all 0.2s;
}
.user-dropdown-btn:hover {
background-color: #f5f5f5;
}
.user-avatar {
background-color: #4f46e5;
color: #fff;
width: 28px;
height: 28px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
font-weight: 600;
}
</style>