feat:关于页面&对接websocket
This commit is contained in:
parent
8da0127729
commit
8e37bba724
@ -38,5 +38,10 @@ def get_system_info():
|
||||
"cpu": f"{random.random() * 100: .2}",
|
||||
"memory": f"{random.random() * 100: .2}",
|
||||
"disk": f"{random.random() * 100: .2}",
|
||||
}
|
||||
},
|
||||
"performance": {
|
||||
"rps": f"{random.random() * random.randint(1, 50): .2}",
|
||||
"time": f"{random.random() * random.randint(1, 50): .2}",
|
||||
"user": f"{random.randint(1, 50)}",
|
||||
},
|
||||
}
|
||||
|
@ -19,8 +19,12 @@ app.mount("/", ws_app)
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
from fastapi.routing import Mount
|
||||
|
||||
for i in app.routes:
|
||||
logger.info(f"{i.path}, {i.methods}, {i.__dict__.get('summary')}, {i.endpoint}")
|
||||
if not isinstance(i, Mount):
|
||||
logger.info(
|
||||
f"{i.path}, {i.methods}, {i.__dict__.get('summary')}, {i.endpoint}"
|
||||
)
|
||||
|
||||
uvicorn.run("main:app", reload=True)
|
||||
|
@ -1,35 +1,13 @@
|
||||
aiosqlite==0.17.0
|
||||
anyio==3.6.1
|
||||
attrs==22.1.0
|
||||
bcrypt==4.0.0
|
||||
certifi==2022.6.15.1
|
||||
charset-normalizer==2.1.1
|
||||
click==8.1.3
|
||||
colorama==0.4.5
|
||||
ecdsa==0.18.0
|
||||
fastapi==0.82.0
|
||||
h11==0.13.0
|
||||
idna==3.3
|
||||
iniconfig==1.1.1
|
||||
iso8601==1.0.2
|
||||
packaging==21.3
|
||||
passlib==1.7.4
|
||||
pluggy==1.0.0
|
||||
py==1.11.0
|
||||
pyasn1==0.4.8
|
||||
pydantic==1.10.2
|
||||
pyparsing==3.0.9
|
||||
pypika-tortoise==0.1.6
|
||||
pytest==7.1.3
|
||||
python-jose==3.3.0
|
||||
pytz==2022.2.1
|
||||
requests==2.28.1
|
||||
rsa==4.9
|
||||
six==1.16.0
|
||||
sniffio==1.3.0
|
||||
starlette==0.19.1
|
||||
tomli==2.0.1
|
||||
uvicorn==0.18.3
|
||||
tortoise-orm==0.19.2
|
||||
typing-extensions==4.3.0
|
||||
urllib3==1.26.12
|
||||
uvicorn==0.18.3
|
||||
websockets==10.3
|
||||
|
||||
|
||||
|
||||
|
||||
|
5368
frontend/package-lock.json
generated
5368
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -9,6 +9,7 @@
|
||||
"prettier": "prettier --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@kangc/v-md-editor": "^2.3.15",
|
||||
"ant-design-vue": "^3.2.12",
|
||||
"axios": "^0.27.2",
|
||||
"echarts": "^5.3.3",
|
||||
|
25
frontend/src/components/card/card.vue
Normal file
25
frontend/src/components/card/card.vue
Normal file
@ -0,0 +1,25 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
title: { type: String },
|
||||
style: {
|
||||
default: () => ({
|
||||
padding: '0'
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card">
|
||||
<a-card :bodyStyle="style" :title="title">
|
||||
<slot></slot>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.card {
|
||||
width: 100%;
|
||||
margin: 10px;
|
||||
}
|
||||
</style>
|
68
frontend/src/components/echart/eachart-per-result.vue
Normal file
68
frontend/src/components/echart/eachart-per-result.vue
Normal file
@ -0,0 +1,68 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import Echart from './echart.vue'
|
||||
|
||||
const props = defineProps({
|
||||
rps: {
|
||||
type: Object
|
||||
},
|
||||
time: {
|
||||
type: Object
|
||||
},
|
||||
user: {
|
||||
type: Object
|
||||
}
|
||||
})
|
||||
|
||||
const options = computed(() => {
|
||||
return {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
animation: false
|
||||
}
|
||||
},
|
||||
xAxis: {
|
||||
type: 'time',
|
||||
splitLine: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
boundaryGap: [0, '100%'],
|
||||
splitLine: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: 'RPS',
|
||||
type: 'line',
|
||||
showSymbol: false,
|
||||
data: props.rps
|
||||
},
|
||||
{
|
||||
name: 'RT (ms)',
|
||||
type: 'line',
|
||||
showSymbol: false,
|
||||
data: props.time
|
||||
},
|
||||
{
|
||||
name: 'User',
|
||||
type: 'line',
|
||||
showSymbol: false,
|
||||
data: props.user
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="result">
|
||||
<Echart :options="options"></Echart>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
43
frontend/src/components/echart/eachart-view.vue
Normal file
43
frontend/src/components/echart/eachart-view.vue
Normal file
@ -0,0 +1,43 @@
|
||||
<script setup>
|
||||
import Card from '@/components/card/card.vue'
|
||||
import EchartSystemInfo from './echart-system-info.vue'
|
||||
import EachartPerResult from './eachart-per-result.vue'
|
||||
|
||||
defineProps({
|
||||
performance: {
|
||||
type: Object
|
||||
},
|
||||
systemUsage: {
|
||||
type: Object
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="echart-data">
|
||||
<Card title="资源使用率">
|
||||
<EchartSystemInfo
|
||||
:cpu-value="systemUsage.cpu"
|
||||
:disk-value="systemUsage.disk"
|
||||
:memory-value="systemUsage.memory"
|
||||
:style="{ width: '100%', height: '300px' }"
|
||||
/>
|
||||
</Card>
|
||||
<Card title="压测结果">
|
||||
<EachartPerResult
|
||||
:rps="performance.rps"
|
||||
:time="performance.time"
|
||||
:user="performance.user"
|
||||
:style="{ width: '100%', height: '300px' }"
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.echart-data {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-around;
|
||||
}
|
||||
</style>
|
@ -88,7 +88,7 @@ const options = computed(() => {
|
||||
height: 14,
|
||||
fontSize: 14,
|
||||
color: '#fff',
|
||||
backgroundColor: 'auto',
|
||||
backgroundColor: 'inherit',
|
||||
borderRadius: 3,
|
||||
formatter: '{value}%'
|
||||
}
|
||||
|
@ -23,7 +23,6 @@ onMounted(() => {
|
||||
|
||||
// props 变化就重新设置值
|
||||
watchEffect(() => {
|
||||
console.log('eachart 基础组件')
|
||||
instance.setOption(props.options)
|
||||
})
|
||||
})
|
||||
|
@ -2,7 +2,6 @@
|
||||
import { ref } from 'vue'
|
||||
import UserInfo from '@/components/layout/layout-info/layout-info.vue'
|
||||
import HeaderCrumb from './header-crumb.vue'
|
||||
import { loadIconCpn } from '@/utils/loadCpn'
|
||||
|
||||
// 记录图标状态
|
||||
const collapsed = ref(false)
|
||||
@ -22,7 +21,7 @@ const clickMenuFold = () => {
|
||||
<!-- 左侧菜单收缩控制 -->
|
||||
<component
|
||||
class="menu-fold"
|
||||
:is="loadIconCpn(collapsed ? 'MenuUnfoldOutlined' : 'MenuFoldOutlined')"
|
||||
:is="$loadIconCpn(collapsed ? 'MenuUnfoldOutlined' : 'MenuFoldOutlined')"
|
||||
@click="clickMenuFold"
|
||||
>
|
||||
</component>
|
||||
@ -35,6 +34,9 @@ const clickMenuFold = () => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
div {
|
||||
font-size: 20px;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
|
@ -2,6 +2,7 @@
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { userStore } from '@/stores/user'
|
||||
import { GithubOutlined } from '@ant-design/icons-vue'
|
||||
|
||||
import SelectRole from './select-role.vue'
|
||||
|
||||
@ -19,10 +20,15 @@ const onClick = ({ key }) => {
|
||||
router.push('/login')
|
||||
}
|
||||
}
|
||||
|
||||
const visitGithub = () => {
|
||||
window.open('https://github.com/zy7y/mini-rbac', '_blank')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="inline">
|
||||
<github-outlined @click="visitGithub" />
|
||||
<a-dropdown>
|
||||
<a class="ant-dropdown-link" @click.prevent>
|
||||
{{ store.userInfo.nickname }} - {{ store.userInfo.roles[0].name }}
|
||||
@ -39,7 +45,7 @@ const onClick = ({ key }) => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
div {
|
||||
font-size: 16px;
|
||||
span {
|
||||
padding: 0 16px;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,7 +1,6 @@
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router'
|
||||
import { userStore } from '@/stores/user'
|
||||
import { loadIconCpn } from '@/utils/loadCpn'
|
||||
|
||||
const store = userStore()
|
||||
const router = useRouter()
|
||||
@ -21,14 +20,14 @@ const menuClick = (menu) => {
|
||||
<template v-if="menu.type === 0">
|
||||
<a-sub-menu :key="menu.id">
|
||||
<template #icon>
|
||||
<component :is="loadIconCpn(menu.meta.icon)"></component>
|
||||
<component :is="$loadIconCpn(menu.meta.icon)"></component>
|
||||
</template>
|
||||
<template #title>{{ menu.name }}</template>
|
||||
<!-- 1 组件 子菜单项 -->
|
||||
<template v-for="sub in menu.children" :key="sub.id">
|
||||
<a-menu-item @click="menuClick(sub)">
|
||||
<template #icon>
|
||||
<component :is="loadIconCpn(sub.meta.icon)"></component>
|
||||
<component :is="$loadIconCpn(sub.meta.icon)"></component>
|
||||
</template>
|
||||
<span>{{ sub.name }}</span>
|
||||
</a-menu-item>
|
||||
|
52
frontend/src/components/markdown/preview-view.vue
Normal file
52
frontend/src/components/markdown/preview-view.vue
Normal file
@ -0,0 +1,52 @@
|
||||
<script setup>
|
||||
import Markdown from '@/components/markdown/preview.vue'
|
||||
import Card from '@/components/card/card.vue'
|
||||
|
||||
/**https://emoji6.com/emojiall/#symbols
|
||||
*
|
||||
* https://emojixd.com/
|
||||
*/
|
||||
const backend = `
|
||||
💬 编程语言: [Python 3.9](https://docs.python.org/zh-cn/3.9/)
|
||||
🌐 Web服务: [FastAPI](https://fastapi.tiangolo.com/) + [Uvicorn](https://www.uvicorn.org/)
|
||||
📄 数据库: SQLite + [Tortoise ORM](https://tortoise.github.io/)
|
||||
📱 即时通信: [Websockets](https://fastapi.tiangolo.com/advanced/websockets/)
|
||||
🔐 JWT认证:[python-jose](https://python-jose.readthedocs.io/en/latest/)
|
||||
`
|
||||
const info = `
|
||||
🎉 [MiniRBAC](https://github.com/zy7y/mini-rbac)
|
||||
✅ 前端菜单权限
|
||||
✅ 前端路由权限
|
||||
✅ 前端按钮权限
|
||||
✅ 后端接口权限
|
||||
`
|
||||
|
||||
const forntend = `
|
||||
💬 编程语言:Javascript
|
||||
⚛️ 框架:[Vue](https://cn.vuejs.org/)
|
||||
🚵🏽 路由:[Vue Router](https://router.vuejs.org/zh/)
|
||||
♻️ 缓存:[Pinia](https://pinia.vuejs.org/)
|
||||
🛠️ 构建工具:[Vite](https://cn.vitejs.dev/)
|
||||
`
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="content">
|
||||
<Card title="🖥️前端">
|
||||
<Markdown :text="forntend" />
|
||||
</Card>
|
||||
<Card title="🎯控制">
|
||||
<Markdown :text="info" />
|
||||
</Card>
|
||||
<Card title="🌐后端">
|
||||
<Markdown :text="backend" />
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
33
frontend/src/components/markdown/preview.vue
Normal file
33
frontend/src/components/markdown/preview.vue
Normal file
@ -0,0 +1,33 @@
|
||||
<script setup>
|
||||
import VMdPreview from '@kangc/v-md-editor/lib/preview'
|
||||
import '@kangc/v-md-editor/lib/style/preview.css'
|
||||
import githubTheme from '@kangc/v-md-editor/lib/theme/github.js'
|
||||
import '@kangc/v-md-editor/lib/theme/style/github.css'
|
||||
import createEmojiPlugin from '@kangc/v-md-editor/lib/plugins/emoji/index'
|
||||
import '@kangc/v-md-editor/lib/plugins/emoji/emoji.css'
|
||||
|
||||
// highlightjs
|
||||
import hljs from 'highlight.js'
|
||||
|
||||
VMdPreview.use(githubTheme, {
|
||||
Hljs: hljs
|
||||
})
|
||||
VMdPreview.use(createEmojiPlugin())
|
||||
|
||||
defineProps({
|
||||
text: {
|
||||
type: String
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-md-preview :text="text"></v-md-preview>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.v-md-editor-preview /deep/ .github-markdown-body {
|
||||
padding: 5px 20px !important;
|
||||
margin: 0px !important;
|
||||
}
|
||||
</style>
|
@ -11,11 +11,14 @@ import '@/assets/css/base.css'
|
||||
|
||||
import 'ant-design-vue/dist/antd.css'
|
||||
import hasPermisson from '@/utils/directive'
|
||||
import { formatTime } from './utils/format'
|
||||
import { registerFilter } from './utils'
|
||||
|
||||
const app = createApp(App)
|
||||
// 权限校验自定义指令
|
||||
hasPermisson(app)
|
||||
app.config.globalProperties.$formatTime = (value) => formatTime(value)
|
||||
// 注册全局filter函数
|
||||
registerFilter(app)
|
||||
|
||||
app.use(store)
|
||||
|
||||
userStore().loadRoleRouter()
|
||||
|
7
frontend/src/utils/index.js
Normal file
7
frontend/src/utils/index.js
Normal file
@ -0,0 +1,7 @@
|
||||
import { formatTime } from './format'
|
||||
import { loadIconCpn } from './loadCpn'
|
||||
|
||||
export const registerFilter = (app) => {
|
||||
app.config.globalProperties.$formatTime = (value) => formatTime(value)
|
||||
app.config.globalProperties.$loadIconCpn = (value) => loadIconCpn(value)
|
||||
}
|
@ -25,8 +25,8 @@ const changeSiderFold = (subValue) => {
|
||||
<!-- 页头 -->
|
||||
<LayoutHeader @changeFold="changeSiderFold" />
|
||||
</a-layout-header>
|
||||
<!-- 面包屑 -->
|
||||
<a-layout-content
|
||||
class="content"
|
||||
:style="{
|
||||
margin: '24px 16px',
|
||||
background: '#F0F2F5',
|
||||
@ -41,8 +41,16 @@ const changeSiderFold = (subValue) => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.main,
|
||||
.main {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ant-layout {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
@ -1,46 +1,58 @@
|
||||
<script setup>
|
||||
import { onUnmounted, ref } from 'vue'
|
||||
import { onUnmounted, reactive, toRefs } from 'vue'
|
||||
|
||||
import EchartSystemInfo from '@/components/echart/echart-system-info.vue'
|
||||
import EachartView from '@/components/echart/eachart-view.vue'
|
||||
import MarkdownView from '@/components/markdown/preview-view.vue'
|
||||
|
||||
/** websocket */
|
||||
let ws = new WebSocket('ws://localhost:8000/ws')
|
||||
|
||||
const wsData = ref()
|
||||
// 响应式数据
|
||||
const data = reactive({
|
||||
systemUsage: {
|
||||
cpu: '0',
|
||||
momery: '0',
|
||||
disk: '0'
|
||||
},
|
||||
performance: {
|
||||
rps: [],
|
||||
time: [],
|
||||
user: []
|
||||
}
|
||||
})
|
||||
|
||||
// 接收消息
|
||||
ws.onmessage = (e) => {
|
||||
// 接收消息
|
||||
wsData.value = JSON.parse(e.data)
|
||||
const wsData = JSON.parse(e.data)
|
||||
data.systemUsage = wsData.usage
|
||||
data.performance.rps.push({
|
||||
value: [Date.now(), wsData.performance.rps]
|
||||
})
|
||||
data.performance.time.push({
|
||||
value: [Date.now(), wsData.performance.time]
|
||||
})
|
||||
data.performance.user.push({
|
||||
value: [Date.now(), wsData.performance.user]
|
||||
})
|
||||
}
|
||||
|
||||
const { systemUsage, performance } = toRefs(data)
|
||||
|
||||
onUnmounted(() => {
|
||||
ws.close()
|
||||
console.log('关闭socket 连接')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="about">
|
||||
<a-card class="system">
|
||||
<template #title> 资源使用率(虚拟数据) </template>
|
||||
<EchartSystemInfo
|
||||
:cpu-value="wsData?.usage.cpu"
|
||||
:disk-value="wsData?.usage.disk"
|
||||
:memory-value="wsData?.usage.memory"
|
||||
:style="{ width: '100%', height: '300px' }"
|
||||
/>
|
||||
</a-card>
|
||||
<EachartView :performance="performance" :system-usage="systemUsage" />
|
||||
<MarkdownView class="footer" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.about {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.system {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.header .ant-card {
|
||||
width: 40%;
|
||||
.footer {
|
||||
margin: 20px 0px;
|
||||
}
|
||||
</style>
|
||||
|
@ -3,7 +3,6 @@ import { ref } from 'vue'
|
||||
import { PlusOutlined } from '@ant-design/icons-vue'
|
||||
|
||||
import { columns, menuType, methodColor } from './conf'
|
||||
import { loadIconCpn } from '@/utils/loadCpn'
|
||||
import { getMenus } from '@/service/menu'
|
||||
|
||||
// 列表数据
|
||||
@ -67,11 +66,10 @@ const delClick = () => {
|
||||
}"
|
||||
:row-key="(record) => record.id"
|
||||
@expand="zi"
|
||||
:expandedRowKeys="expandRowKeys"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'meta'">
|
||||
<component :is="loadIconCpn(record.meta?.icon)"></component>
|
||||
<component :is="$loadIconCpn(record.meta?.icon)"></component>
|
||||
</template>
|
||||
<template v-if="column.key === 'type'">
|
||||
{{ menuType[record.type] }}
|
||||
|
@ -238,10 +238,10 @@ watch(
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'created'">
|
||||
{{ $$formatTime(record.created) }}
|
||||
{{ $formatTime(record.created) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'modified'">
|
||||
{{ $$formatTime(record.modified) }}
|
||||
{{ $formatTime(record.modified) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<span>
|
||||
|
Loading…
Reference in New Issue
Block a user