feat:关于页面&对接websocket

This commit is contained in:
zy7y 2022-09-17 17:57:09 +08:00
parent 8da0127729
commit 8e37bba724
21 changed files with 5639 additions and 118 deletions

View File

@ -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)}",
},
}

View File

@ -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)

View File

@ -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
tortoise-orm==0.19.2
typing-extensions==4.3.0
urllib3==1.26.12
uvicorn==0.18.3
tortoise-orm==0.19.2
websockets==10.3

File diff suppressed because it is too large Load Diff

View File

@ -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",

View 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>

View 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>

View 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>

View File

@ -88,7 +88,7 @@ const options = computed(() => {
height: 14,
fontSize: 14,
color: '#fff',
backgroundColor: 'auto',
backgroundColor: 'inherit',
borderRadius: 3,
formatter: '{value}%'
}

View File

@ -23,7 +23,6 @@ onMounted(() => {
// props
watchEffect(() => {
console.log('eachart 基础组件')
instance.setOption(props.options)
})
})

View File

@ -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%;

View File

@ -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>

View File

@ -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>

View 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>

View 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>

View File

@ -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()

View 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)
}

View File

@ -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%;
}

View File

@ -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()
ws.onmessage = (e) => {
//
wsData.value = JSON.parse(e.data)
//
const data = reactive({
systemUsage: {
cpu: '0',
momery: '0',
disk: '0'
},
performance: {
rps: [],
time: [],
user: []
}
})
//
ws.onmessage = (e) => {
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>

View File

@ -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] }}

View File

@ -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>