swagger嵌入
This commit is contained in:
parent
a3c423d5d9
commit
d59f99da21
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"liveServer.settings.port": 5501
|
||||
}
|
||||
|
|
@ -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())
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +1,2 @@
|
|||
# 通用环境变量
|
||||
VITE_APP_TITLE=Admin Management System
|
||||
VITE_APP_TITLE=AI后台管理
|
||||
|
|
@ -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']
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
// 只有需要认证且不是隐藏页面的路由才添加标签页
|
||||
|
|
|
|||
|
|
@ -5,48 +5,293 @@
|
|||
<span>接口文档</span>
|
||||
</template>
|
||||
|
||||
<div class="content">
|
||||
<p>API接口文档页面</p>
|
||||
<el-alert
|
||||
title="API文档"
|
||||
description="您可以访问后端的Swagger文档查看完整的API接口信息"
|
||||
type="success"
|
||||
show-icon
|
||||
:closable="false"
|
||||
/>
|
||||
<div class="swagger-content">
|
||||
<div v-if="loading" class="loading-container">
|
||||
<el-skeleton :rows="10" animated />
|
||||
</div>
|
||||
|
||||
<div class="swagger-link" style="margin-top: 20px;">
|
||||
<div v-else-if="error" class="error-container">
|
||||
<el-alert
|
||||
title="加载失败"
|
||||
:description="error"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="false"
|
||||
/>
|
||||
<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 UI的HTML内容
|
||||
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()
|
||||
|
||||
// 移除head标签,因为我们不需要完整的HTML结构
|
||||
html = html.replace(/<head>[\s\S]*?<\/head>/gi, '')
|
||||
|
||||
// 移除body标签,只保留内容
|
||||
html = html.replace(/<body[^>]*>/, '').replace(/<\/body>/, '')
|
||||
|
||||
// 移除html标签
|
||||
html = html.replace(/<html[^>]*>/, '').replace(/<\/html>/, '')
|
||||
|
||||
// 修改CSS和JS路径为绝对路径
|
||||
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
|
||||
|
||||
// 等待DOM更新后初始化Swagger 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 UI的CSS
|
||||
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 UI的JS
|
||||
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>
|
||||
|
|
@ -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))
|
||||
},
|
||||
},
|
||||
})
|
||||
}))
|
||||
|
|
|
|||
Loading…
Reference in New Issue