swagger嵌入

This commit is contained in:
kazusa 2025-08-26 15:13:34 +08:00
parent a3c423d5d9
commit d59f99da21
9 changed files with 339 additions and 58 deletions

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"liveServer.settings.port": 5501
}

View File

@ -80,6 +80,12 @@ public class SecurityConfig {
// 其他请求需要认证
.anyRequest().authenticated())
// 允许iframe嵌入
.headers(headers -> headers
.frameOptions(frame -> frame.disable())
.contentTypeOptions(contentType -> contentType.disable())
.httpStrictTransportSecurity(hsts -> hsts.disable()))
// 认证提供者
.authenticationProvider(authenticationProvider())

View File

@ -0,0 +1,42 @@
package com.admin.config;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
@Order(Integer.MIN_VALUE) // 最高优先级
public class SwaggerFrameOptionsFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// 执行过滤器链
filterChain.doFilter(request, response);
// 如果是Swagger相关的请求强制覆盖响应头
String requestURI = request.getRequestURI();
if (requestURI != null && (
requestURI.contains("/swagger-ui") ||
requestURI.contains("/v3/api-docs") ||
requestURI.contains("/swagger-ui.html")
)) {
// 强制移除X-Frame-Options限制
response.setHeader("X-Frame-Options", "SAMEORIGIN");
response.setHeader("Content-Security-Policy", "frame-ancestors 'self' *");
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
response.setHeader("Access-Control-Allow-Headers", "*");
// 确保这些头被正确设置
response.setHeader("X-Frame-Options", "SAMEORIGIN");
}
}
}

View File

@ -1,2 +1,2 @@
# 通用环境变量
VITE_APP_TITLE=Admin Management System
VITE_APP_TITLE=AI后台管理

View File

@ -19,11 +19,15 @@ declare module 'vue' {
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
ElCol: typeof import('element-plus/es')['ElCol']
ElCollapse: typeof import('element-plus/es')['ElCollapse']
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
ElContainer: typeof import('element-plus/es')['ElContainer']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElDivider: typeof import('element-plus/es')['ElDivider']
ElDropdown: typeof import('element-plus/es')['ElDropdown']
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
ElEmpty: typeof import('element-plus/es')['ElEmpty']
ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElHeader: typeof import('element-plus/es')['ElHeader']
@ -41,6 +45,7 @@ declare module 'vue' {
ElRow: typeof import('element-plus/es')['ElRow']
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']

View File

@ -115,27 +115,22 @@
</template>
<script setup lang="ts">
import TabsView from '@/components/TabsView.vue'
import DynamicMenu from '@/components/DynamicMenu.vue'
import TabsView from '@/components/TabsView.vue'
import { useMenuStore } from '@/stores/menu'
import { useTabsStore } from '@/stores/tabs'
import { useUserStore } from '@/stores/user'
import { useMenuStore } from '@/stores/menu'
import {
ArrowDown,
House,
Loading,
Lock,
Menu,
Setting,
SwitchButton,
User,
UserFilled,
Expand,
Fold,
Loading
User
} from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { storeToRefs } from 'pinia'
import { computed, ref, watch, onMounted } from 'vue'
import { computed, onMounted, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()

View File

@ -1,3 +1,4 @@
import { useTabsStore } from '@/stores/tabs'
import { useUserStore } from '@/stores/user'
import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'
@ -94,7 +95,6 @@ router.beforeEach(async (to, from, next) => {
// 路由后置守卫 - 添加标签页
router.afterEach((to) => {
const { useTabsStore } = require('@/stores/tabs')
const tabsStore = useTabsStore()
// 只有需要认证且不是隐藏页面的路由才添加标签页

View File

@ -5,48 +5,293 @@
<span>接口文档</span>
</template>
<div class="content">
<p>API接口文档页面</p>
<div class="swagger-content">
<div v-if="loading" class="loading-container">
<el-skeleton :rows="10" animated />
</div>
<div v-else-if="error" class="error-container">
<el-alert
title="API文档"
description="您可以访问后端的Swagger文档查看完整的API接口信息"
type="success"
title="加载失败"
:description="error"
type="error"
show-icon
:closable="false"
/>
<div class="swagger-link" style="margin-top: 20px;">
<el-button
type="primary"
size="large"
@click="openSwagger"
style="margin-top: 20px;"
@click="loadSwagger"
>
打开Swagger文档
重新加载
</el-button>
</div>
<div v-else class="swagger-embedded-container">
<div class="swagger-header">
<h3>API接口文档</h3>
<div class="swagger-controls">
<el-button
type="primary"
size="small"
@click="refreshSwagger"
>
刷新
</el-button>
<el-button
type="success"
size="small"
@click="openInNewTab"
>
新窗口打开
</el-button>
</div>
</div>
<div class="swagger-body">
<div
ref="swaggerContainer"
class="swagger-content-container"
v-html="swaggerContent"
></div>
</div>
</div>
</div>
</el-card>
</div>
</template>
<script setup lang="ts">
// Swagger
const openSwagger = () => {
window.open('http://localhost:8080/swagger-ui.html', '_blank')
import { nextTick, onMounted, ref } from 'vue'
//
interface SwaggerUIBundle {
(config: SwaggerConfig): unknown
presets: {
apis: unknown
}
plugins: {
DownloadUrl: unknown
}
}
interface SwaggerConfig {
url: string
dom_id: string
deepLinking: boolean
presets: unknown[]
plugins: unknown[]
layout: string
}
//
const loading = ref(true)
const error = ref('')
const swaggerContent = ref('')
const swaggerContainer = ref<HTMLElement>()
// Swagger UI
const loadSwagger = async () => {
loading.value = true
error.value = ''
try {
// Swagger UIHTML
const response = await fetch('http://localhost:8080/api/swagger-ui/index.html')
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
let html = await response.text()
// headHTML
html = html.replace(/<head>[\s\S]*?<\/head>/gi, '')
// body
html = html.replace(/<body[^>]*>/, '').replace(/<\/body>/, '')
// html
html = html.replace(/<html[^>]*>/, '').replace(/<\/html>/, '')
// CSSJS
html = html.replace(/href="\.\//g, 'href="http://localhost:8080/api/swagger-ui/')
html = html.replace(/src="\.\//g, 'src="http://localhost:8080/api/swagger-ui/')
swaggerContent.value = html
loading.value = false
// DOMSwagger UI
await nextTick()
initializeSwaggerUI()
} catch (err) {
console.error('加载Swagger UI失败:', err)
error.value = '无法连接到后端服务,请确保后端服务正在运行在 http://localhost:8080'
loading.value = false
}
}
// Swagger UI
const initializeSwaggerUI = () => {
if (!swaggerContainer.value) return
// Swagger UICSS
const link = document.createElement('link')
link.rel = 'stylesheet'
link.type = 'text/css'
link.href = 'http://localhost:8080/api/swagger-ui/swagger-ui.css'
document.head.appendChild(link)
// Swagger UIJS
const script = document.createElement('script')
script.src = 'http://localhost:8080/api/swagger-ui/swagger-ui-bundle.js'
script.onload = () => {
const standaloneScript = document.createElement('script')
standaloneScript.src = 'http://localhost:8080/api/swagger-ui/swagger-ui-standalone-preset.js'
standaloneScript.onload = () => {
// Swagger UI
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const SwaggerUIBundle = (window as any).SwaggerUIBundle as SwaggerUIBundle
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const SwaggerUIStandalonePreset = (window as any).SwaggerUIStandalonePreset
if (SwaggerUIBundle) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as any).ui = SwaggerUIBundle({
url: 'http://localhost:8080/api/v3/api-docs',
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
})
}
}
document.head.appendChild(standaloneScript)
}
document.head.appendChild(script)
}
// Swagger UI
const refreshSwagger = () => {
loadSwagger()
}
// Swagger UI
const openInNewTab = () => {
window.open('http://localhost:8080/api/swagger-ui/index.html', '_blank')
}
//
onMounted(() => {
loadSwagger()
})
</script>
<style scoped>
.swagger-container {
height: 100%;
display: flex;
flex-direction: column;
}
.swagger-container :deep(.el-card) {
height: 100%;
display: flex;
flex-direction: column;
}
.swagger-container :deep(.el-card__body) {
flex: 1;
padding: 0;
overflow: hidden;
}
.swagger-content {
height: 100%;
display: flex;
flex-direction: column;
}
.loading-container {
padding: 40px;
flex: 1;
}
.error-container {
padding: 40px;
text-align: center;
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.swagger-embedded-container {
height: 100%;
display: flex;
flex-direction: column;
}
.swagger-header {
padding: 15px 20px;
border-bottom: 1px solid #e4e7ed;
display: flex;
justify-content: space-between;
align-items: center;
background-color: #fafafa;
flex-shrink: 0;
}
.swagger-header h3 {
margin: 0;
color: #303133;
font-size: 16px;
}
.swagger-controls {
display: flex;
gap: 10px;
}
.swagger-body {
flex: 1;
overflow: hidden;
position: relative;
}
.swagger-content-container {
height: 100%;
overflow: auto;
padding: 20px;
}
.content {
text-align: center;
padding: 50px 0;
.swagger-content-container :deep(#swagger-ui) {
height: 100%;
min-height: 600px;
}
.swagger-link {
margin-top: 30px;
/* 自定义滚动条 */
.swagger-content-container::-webkit-scrollbar {
width: 6px;
}
.swagger-content-container::-webkit-scrollbar-track {
background: #f1f1f1;
}
.swagger-content-container::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
.swagger-content-container::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
</style>

View File

@ -7,17 +7,18 @@ import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
// https://vite.dev/config/
export default defineConfig({
export default defineConfig(({ mode }) => ({
plugins: [
vue(),
vueDevTools(),
// 仅在开发环境启用 DevTools避免对生产构建的注入影响打包产物
mode === 'development' && vueDevTools(),
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
].filter(Boolean),
server: {
port: 3000,
proxy: {
@ -28,28 +29,12 @@ export default defineConfig({
}
},
build: {
// 确保构建时使用相对路径
// 使用相对路径,便于在静态服务器(如 Live Server直接打开 dist
assetsDir: 'assets',
rollupOptions: {
output: {
// 修复循环引用问题的分包策略
manualChunks(id) {
if (id.includes('node_modules')) {
if (id.includes('element-plus')) {
return 'element-plus'
}
if (id.includes('vue')) {
return 'vue'
}
if (id.includes('@element-plus/icons-vue')) {
return 'element-icons'
}
// 其他第三方库
return 'vendor'
}
}
}
}
// 交给 Vite/Rollup 自己做最佳分包,避免对 Vue 这类存在循环依赖的包强制分块
// 若线上仍遇到压缩器相关问题,可将 minify 切为 'terser'
// minify: 'terser',
sourcemap: false,
},
// 设置基础路径为相对路径,适用于静态文件服务器
base: './',
@ -58,4 +43,4 @@ export default defineConfig({
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
})
}))