优化构架

This commit is contained in:
2026-08-03 18:49:18 +08:00
parent 1241d11c01
commit 44c33a6280
133 changed files with 651 additions and 603 deletions
+19
View File
@@ -0,0 +1,19 @@
import { createPinia, setActivePinia } from 'pinia'
import { mount } from '@vue/test-utils'
import { createMemoryHistory, createRouter } from 'vue-router'
import { describe, expect, it } from 'vitest'
import ForbiddenView from './ForbiddenView.vue'
describe('ForbiddenView', () => {
it('shows server-account guidance and sanitizes the source path', async () => {
const pinia = createPinia()
setActivePinia(pinia)
const router = createRouter({ history: createMemoryHistory(), routes: [{ path: '/403', component: ForbiddenView }] })
await router.push({ path: '/403', query: { from: '//example.com' } })
await router.isReady()
const wrapper = mount(ForbiddenView, { global: { plugins: [pinia, router] } })
expect(wrapper.text()).toContain('当前登录账号无法访问此页面')
expect(wrapper.text()).toContain('来源页面:/')
expect(wrapper.text()).not.toContain('权限预览')
})
})
+28
View File
@@ -0,0 +1,28 @@
<script setup lang="ts">
import { computed } from 'vue'
import { ShieldX } from '@lucide/vue'
import { useRoute, useRouter } from 'vue-router'
import { sanitizeInternalPath } from '@/core/routing/accessDecision'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
const route = useRoute()
const router = useRouter()
const access = useAccessPreviewStore()
const sourcePath = computed(() => sanitizeInternalPath(route.query.from))
async function returnToAccessiblePage() {
if (access.firstAccessiblePath) await router.replace(access.firstAccessiblePath)
}
</script>
<template>
<section class="forbidden-page" aria-labelledby="forbidden-title">
<div class="forbidden-icon" aria-hidden="true"><ShieldX :size="32" /></div>
<p class="section-kicker">403 · 权限不足</p>
<h2 id="forbidden-title">当前登录账号无法访问此页面</h2>
<p>请联系平台管理员核对账号角色和权限配置。</p>
<p class="forbidden-source">来源页面:{{ sourcePath }}</p>
<el-button v-if="access.firstAccessiblePath" type="primary" @click="returnToAccessiblePage">返回可访问页面</el-button>
<p v-else>当前账号暂无可访问的业务菜单。</p>
</section>
</template>
@@ -0,0 +1,33 @@
<script setup lang="ts">
import { Cable } from '@lucide/vue'
defineProps<{
moduleName: string
description: string
}>()
</script>
<template>
<section class="surface-panel unavailable-panel" role="status" aria-live="polite">
<Cable :size="30" aria-hidden="true" />
<p class="section-kicker">接口尚未开放</p>
<h2>{{ moduleName }}暂不可用</h2>
<p>{{ description }}</p>
<p class="secondary-text">为避免产生误导,本页面不会展示演示数据,也不会模拟新增、编辑或导入成功。</p>
</section>
</template>
<style scoped>
.unavailable-panel {
display: grid;
justify-items: start;
gap: 10px;
max-width: 720px;
padding: 32px;
}
.unavailable-panel h2,
.unavailable-panel p {
margin: 0;
}
</style>
+29
View File
@@ -0,0 +1,29 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import { Construction, Plus, Search } from '@lucide/vue'
const route = useRoute()
const title = computed(() => route.meta.title || '功能模块')
const description = computed(() => route.meta.description || '')
</script>
<template>
<section class="module-page">
<div class="module-toolbar">
<el-input placeholder="输入关键词搜索" clearable>
<template #prefix><Search :size="17" /></template>
</el-input>
<el-button type="primary"><Plus :size="17" /> 新增</el-button>
</div>
<div class="module-card">
<div class="empty-state">
<div class="empty-icon"><Construction :size="28" /></div>
<h2>{{ title }}</h2>
<p>{{ description }}</p>
<el-tag effect="plain">页面骨架已完成,业务接口待接入</el-tag>
</div>
</div>
</section>
</template>