feat: change to old ui
@@ -1,50 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
es2021: true
|
||||
},
|
||||
parser: 'vue-eslint-parser',
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:vue/vue3-recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
// eslint-config-prettier 的缩写
|
||||
'prettier',
|
||||
'vue-global-api'
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 12,
|
||||
parser: '@typescript-eslint/parser',
|
||||
sourceType: 'module',
|
||||
ecmaFeatures: {
|
||||
jsx: true
|
||||
}
|
||||
},
|
||||
// eslint-plugin-vue @typescript-eslint/eslint-plugin eslint-plugin-prettier的缩写
|
||||
plugins: ['vue', '@typescript-eslint', 'prettier'],
|
||||
rules: {
|
||||
'@typescript-eslint/ban-ts-ignore': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'@typescript-eslint/no-use-before-define': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
'@typescript-eslint/ban-types': 'off',
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'vue/multi-word-component-names': 'off',
|
||||
'vue/no-mutating-props': 'off',
|
||||
'no-undef': 'off'
|
||||
},
|
||||
globals: {
|
||||
defineProps: 'readonly',
|
||||
defineEmits: 'readonly',
|
||||
defineExpose: 'readonly',
|
||||
withDefaults: 'readonly'
|
||||
}
|
||||
};
|
||||
5
web/.gitignore
vendored
@@ -6,11 +6,8 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
yarn.lock
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
@@ -22,4 +19,4 @@ dist-ssr
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
*.sw?
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
module.exports = {
|
||||
printWidth: 130, // 超过最大值换行
|
||||
tabWidth: 2, // 缩进字节数
|
||||
useTabs: false, // 缩进使用tab,不使用空格
|
||||
semi: true, // 句尾添加分号
|
||||
singleQuote: true, // 使用单引号代替双引号
|
||||
proseWrap: 'preserve', // 默认值。因为使用了一些折行敏感型的渲染器(如GitHub comment)而按照markdown文本样式进行折行
|
||||
arrowParens: 'avoid', // (x) => {} 箭头函数参数只有一个时是否要有小括号。avoid:省略括号
|
||||
bracketSpacing: true, // 在对象,数组括号与文字之间加空格 "{ foo: bar }"
|
||||
endOfLine: 'auto', // 结尾是 \n \r \n\r auto
|
||||
jsxSingleQuote: false, // 在jsx中使用单引号代替双引号
|
||||
trailingComma: 'none', // 在对象或数组最后一个元素后面是否加逗号(在ES5中加尾逗号)
|
||||
overrides: [
|
||||
{
|
||||
files: '*.html',
|
||||
options: {
|
||||
parser: 'html'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
@@ -1,76 +1,18 @@
|
||||
# 悟空IM后台监控
|
||||
# Vue 3 + TypeScript + Vite
|
||||
|
||||
<a href="https://cn.vuejs.org/" target="_blank" rel="noopener" style="display:inline-block;">
|
||||
<img src="https://img.shields.io/badge/Vue-3.4.21-%236CB52D.svg?logo=Vue.js" alt="Vue">
|
||||
</a>  
|
||||
<a href="https://router.vuejs.org/zh/" target="_blank" rel="noopener" style="display:inline-block;">
|
||||
<img src="https://img.shields.io/badge/Vue Router-4.2.4-%236CB52D.svg?logo=VueRouter" alt="Vue Router">
|
||||
</a>  
|
||||
<a href="https://pinia.vuejs.org/zh/" target="_blank" rel="noopener" style="display:inline-block;">
|
||||
<img src="https://img.shields.io/badge/Pinia-2.1.6-%236CB52D.svg?logo=Pinia" alt="Pinia">
|
||||
</a>  
|
||||
<a href="https://ts.nodejs.cn/" target="_blank" rel="noopener" style="display:inline-block;">
|
||||
<img src="https://img.shields.io/badge/TypeScript-5.0.4-%236CB52D.svg?logo=TypeScript&logoColor=FFF" alt="TypeScript">
|
||||
</a>  
|
||||
<a href="https://cn.vitejs.dev/" target="_blank" rel="noopener" style="display:inline-block;">
|
||||
<img src="https://img.shields.io/badge/Vite-5.2.11-%236CB52D.svg?logo=Vite&logoColor=FFF" alt="Vite">
|
||||
</a>  
|
||||
<a href="https://element-plus.org/" target="_blank" rel="noopener" style="display:inline-block;">
|
||||
<img src="https://img.shields.io/badge/Element Plus-2.7.3-%236CB52D.svg?logo=ElementPlus" alt="Element Plus">
|
||||
</a>  
|
||||
<a href="https://unocss.dev/" target="_blank" rel="noopener" style="display:inline-block;">
|
||||
<img src="https://img.shields.io/badge/UnoCss-0.51.12-%236CB52D.svg?logo=UnoCss" alt="Unocss">
|
||||
</a>  
|
||||
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
- NODE_ENV node环境变量
|
||||
- APP_ENV 应用环境变量
|
||||
## Recommended IDE Setup
|
||||
|
||||
## 安装
|
||||
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
|
||||
|
||||
```sh
|
||||
pnpm install
|
||||
```
|
||||
## Type Support For `.vue` Imports in TS
|
||||
|
||||
## 本地开发
|
||||
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
|
||||
|
||||
```sh
|
||||
pnpm dev
|
||||
```
|
||||
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
|
||||
|
||||
## 编译
|
||||
|
||||
```sh
|
||||
pnpm build
|
||||
```
|
||||
|
||||
## 本地预览
|
||||
|
||||
> 先执行编译再执行该命令
|
||||
|
||||
```sh
|
||||
pnpm serve
|
||||
```
|
||||
|
||||
## eslint检测
|
||||
|
||||
```sh
|
||||
pnpm lint
|
||||
```
|
||||
|
||||
## eslint修复
|
||||
|
||||
```sh
|
||||
pnpm lint:fix
|
||||
```
|
||||
|
||||
## prettier检测
|
||||
|
||||
```sh
|
||||
pnpm format
|
||||
```
|
||||
|
||||
## Git commit
|
||||
|
||||
```sh
|
||||
pnpm cz
|
||||
```
|
||||
1. Disable the built-in TypeScript Extension
|
||||
1. Run `Extensions: Show Built-in Extensions` from VSCode's command palette
|
||||
2. Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
|
||||
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './plugins';
|
||||
export * from './utils';
|
||||
@@ -1,30 +0,0 @@
|
||||
import { createHtmlPlugin } from 'vite-plugin-html';
|
||||
import dayjs from 'dayjs';
|
||||
import { __APP_INFO__ } from '../utils';
|
||||
|
||||
const t = dayjs(__APP_INFO__.PROJECT_BUILD_TIME).format('YYYYMMDDHHmmss');
|
||||
|
||||
const getAppConfigSrc = () => {
|
||||
return `/tsdd-config.js?v=${__APP_INFO__.pkg.version}-${t}`;
|
||||
};
|
||||
|
||||
export default function createHtml() {
|
||||
const tags = [];
|
||||
if (process.env.IS_CONFIG) {
|
||||
tags.push({
|
||||
tag: 'script',
|
||||
attrs: {
|
||||
src: getAppConfigSrc()
|
||||
}
|
||||
});
|
||||
}
|
||||
return createHtmlPlugin({
|
||||
inject: {
|
||||
data: {
|
||||
title: __APP_INFO__.APP_TITLE,
|
||||
__APP_INFO__: JSON.stringify(__APP_INFO__)
|
||||
},
|
||||
tags
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import type { PluginOption } from 'vite';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import vueJsx from '@vitejs/plugin-vue-jsx';
|
||||
import VueDevtools from 'vite-plugin-vue-devtools';
|
||||
import unocss from '@unocss/vite';
|
||||
import unplugin from './unplugin';
|
||||
|
||||
import createHtml from './html';
|
||||
import createLayouts from './layouts';
|
||||
import createPage from './pages';
|
||||
/**
|
||||
* vite插件
|
||||
* @param viteEnv - 环境变量配置
|
||||
*/
|
||||
export function setupVitePlugins(): (PluginOption | PluginOption[])[] {
|
||||
const plugins = [
|
||||
vue(),
|
||||
vueJsx(),
|
||||
VueDevtools(),
|
||||
...unplugin(),
|
||||
unocss()
|
||||
];
|
||||
|
||||
plugins.push(createHtml());
|
||||
plugins.push(createLayouts());
|
||||
plugins.push(createPage());
|
||||
return plugins;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import Layouts from 'vite-plugin-vue-meta-layouts';
|
||||
|
||||
export default function createLayouts() {
|
||||
return Layouts({
|
||||
defaultLayout: 'index'
|
||||
});
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import Pages from 'vite-plugin-pages';
|
||||
|
||||
export default function createPage() {
|
||||
return Pages({
|
||||
dirs: 'src/pages',
|
||||
exclude: ['**/components/*.vue']
|
||||
});
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import AutoImport from 'unplugin-auto-import/vite';
|
||||
import Components from 'unplugin-vue-components/rollup';
|
||||
import setupExtend from 'unplugin-vue-setup-extend-plus/vite';
|
||||
import { VxeResolver, lazyImport } from 'vite-plugin-lazy-import';
|
||||
|
||||
export default function unplugin() {
|
||||
return [
|
||||
AutoImport({
|
||||
include: [/\.[tj]sx?$/, /\.vue\?vue/, /\.md$/],
|
||||
imports: ['vue', 'vue-router', 'pinia'],
|
||||
resolvers: [],
|
||||
dts: 'src/types/auto-imports.d.ts'
|
||||
}),
|
||||
Components({
|
||||
include: [/\.vue$/, /\.vue\?vue/, /\.md$/],
|
||||
resolvers: [],
|
||||
dts: 'src/types/components.d.ts'
|
||||
}),
|
||||
lazyImport({
|
||||
resolvers: [
|
||||
VxeResolver({
|
||||
libraryName: 'vxe-table'
|
||||
}),
|
||||
VxeResolver({
|
||||
libraryName: 'vxe-pc-ui'
|
||||
})
|
||||
]
|
||||
}),
|
||||
setupExtend({})
|
||||
];
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import path from 'path';
|
||||
import dayjs from 'dayjs';
|
||||
import { name, version, engines } from '../../package.json';
|
||||
|
||||
export const __APP_INFO__ = {
|
||||
pkg: { name, version, engines },
|
||||
APP_TITLE: '悟空IM分布式管理系统',
|
||||
APP_TITLE_SHORT: 'WK',
|
||||
APP_DOCS: 'https://githubim.com/',
|
||||
PROJECT_BUILD_TIME: dayjs(new Date()).format('YYYY-MM-DD HH:mm:ss')
|
||||
};
|
||||
/**
|
||||
* 获取项目根路径
|
||||
* @descrition 末尾不带斜杠
|
||||
*/
|
||||
export function getRootPath() {
|
||||
return path.resolve(process.cwd());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取项目src路径
|
||||
* @param srcName - src目录名称(默认: "src")
|
||||
* @descrition 末尾不带斜杠
|
||||
*/
|
||||
export function getSrcPath(srcName = 'src') {
|
||||
const rootPath = getRootPath();
|
||||
|
||||
return `${rootPath}/${srcName}`;
|
||||
}
|
||||
BIN
web/bun.lockb
Executable file
1
web/dist/assets/index-035b81d4.css
vendored
Normal file
345
web/dist/assets/index-6e26e10e.js
vendored
Normal file
BIN
web/dist/assets/logo-9c3a9007.png
vendored
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
15
web/dist/index.html
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/web/logo.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>悟空IM</title>
|
||||
<script type="module" crossorigin src="/web/assets/index-6e26e10e.js"></script>
|
||||
<link rel="stylesheet" href="/web/assets/index-035b81d4.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
BIN
web/dist/logo.png
vendored
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
web/dist/logo_big.png
vendored
Normal file
|
After Width: | Height: | Size: 12 KiB |
1
web/dist/vite.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -4,10 +4,10 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/logo.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title><%- title %></title>
|
||||
<title>悟空IM</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
1785
web/package-lock.json
generated
Normal file
@@ -1,72 +1,34 @@
|
||||
{
|
||||
"name": "wukong-im-console",
|
||||
"version": "2.0.0",
|
||||
"name": "wukongimmanager",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint src --fix --ext .ts,.tsx,.vue,.js,.jsx,",
|
||||
"lint:prettier": "prettier --write src"
|
||||
"build": "vue-tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@antv/x6": "^2.18.1",
|
||||
"@antv/x6-vue-shape": "^2.1.2",
|
||||
"@ctrl/tinycolor": "^4.1.0",
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"@icon-park/vue-next": "^1.4.2",
|
||||
"@vueuse/core": "^11.2.0",
|
||||
"@vxe-ui/plugin-render-element": "^4.0.7",
|
||||
"axios": "^1.7.4",
|
||||
"axios": "^1.6.7",
|
||||
"buffer": "^6.0.3",
|
||||
"dayjs": "^1.11.12",
|
||||
"echarts": "^5.5.1",
|
||||
"element-plus": "^2.8.0",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^2.2.5",
|
||||
"pinia-plugin-persistedstate": "^4.1.2",
|
||||
"unocss": "^0.60.4",
|
||||
"vue": "^3.5.12",
|
||||
"vue-i18n": "^10.0.4",
|
||||
"vue-json-pretty": "^2.4.0",
|
||||
"vue-router": "^4.4.3",
|
||||
"vxe-pc-ui": "^4.2.37",
|
||||
"vxe-table": "^4.7.97",
|
||||
"xe-utils": "^3.5.31"
|
||||
"echarts": "^5.5.0",
|
||||
"vue": "^3.2.47",
|
||||
"vue-json-pretty": "^2.3.0",
|
||||
"vue-router": "^4.3.0",
|
||||
"vue3-cookies": "^1.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/axios": "^0.14.0",
|
||||
"@types/node": "^20.14.10",
|
||||
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||
"@typescript-eslint/parser": "^5.62.0",
|
||||
"@unocss/preset-uno": "^0.60.4",
|
||||
"@unocss/vite": "^0.60.4",
|
||||
"@vitejs/plugin-vue": "^5.1.2",
|
||||
"@vitejs/plugin-vue-jsx": "^4.0.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^8.10.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-vue": "^9.27.0",
|
||||
"sass": "^1.80.6",
|
||||
"typescript": "^5.5.4",
|
||||
"unplugin-auto-import": "^0.18.3",
|
||||
"unplugin-vue-components": "^0.27.4",
|
||||
"unplugin-vue-router": "^0.10.8",
|
||||
"unplugin-vue-setup-extend-plus": "^1.0.0",
|
||||
"vite": "^5.4.10",
|
||||
"vite-plugin-banner": "^0.7.1",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-html": "^3.2.2",
|
||||
"vite-plugin-html-template": "^1.2.2",
|
||||
"vite-plugin-lazy-import": "^1.0.7",
|
||||
"vite-plugin-pages": "^0.32.3",
|
||||
"vite-plugin-vue-devtools": "^7.3.7",
|
||||
"vite-plugin-vue-meta-layouts": "^0.4.3",
|
||||
"vue-global-api": "^0.4.1",
|
||||
"vue-tsc": "^1.8.27"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0",
|
||||
"pnpm": ">=9.0.0"
|
||||
"@vitejs/plugin-vue": "^4.1.0",
|
||||
"autoprefixer": "^10.4.18",
|
||||
"daisyui": "latest",
|
||||
"postcss": "^8.4.35",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5.0.2",
|
||||
"vite": "^4.3.2",
|
||||
"vue-tsc": "^1.4.2"
|
||||
}
|
||||
}
|
||||
|
||||
5424
web/pnpm-lock.yaml
generated
6
web/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
BIN
web/public/logo_big.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
1
web/public/vite.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
393
web/src/App.vue
@@ -1,41 +1,368 @@
|
||||
<script lang="ts" setup>
|
||||
import { getBrowserLang } from '@/utils';
|
||||
import { useGlobalStore } from '@/stores/modules/global';
|
||||
<script setup lang="ts">
|
||||
|
||||
import { LanguageType } from '@/stores/interface';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import zhCn from 'element-plus/es/locale/lang/zh-cn';
|
||||
import en from 'element-plus/es/locale/lang/en';
|
||||
|
||||
// init theme
|
||||
const { initTheme } = useTheme();
|
||||
initTheme();
|
||||
|
||||
const globalStore = useGlobalStore();
|
||||
|
||||
const locale = computed(() => {
|
||||
if (globalStore.language == 'zh') return zhCn;
|
||||
if (globalStore.language == 'en') return en;
|
||||
return getBrowserLang() == 'zh' ? zhCn : en;
|
||||
});
|
||||
|
||||
const assemblySize = computed(() => globalStore.assemblySize);
|
||||
|
||||
const buttonConfig = reactive({ autoInsertSpace: false });
|
||||
const i18n = useI18n();
|
||||
import { onMounted } from 'vue';
|
||||
import Login from './pages/login/Login.vue';
|
||||
import App from './services/App';
|
||||
import APIClient from './services/APIClient';
|
||||
|
||||
onMounted(() => {
|
||||
const language = globalStore.language ?? getBrowserLang();
|
||||
i18n.locale.value = language;
|
||||
globalStore.setGlobalState('language', language as LanguageType);
|
||||
|
||||
|
||||
if(App.shard().loginInfo.isLogin()) {
|
||||
App.shard().loadSystemSettingIfNeed()
|
||||
}
|
||||
|
||||
APIClient.shared.config.tokenCallback = () => {
|
||||
return App.shard().loginInfo.token
|
||||
}
|
||||
|
||||
APIClient.shared.logoutCallback = () => {
|
||||
App.shard().loginInfo.clear();
|
||||
window.location.href = '/web/login'
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
const onLogout = () => {
|
||||
console.log('logout')
|
||||
App.shard().loginInfo.clear();
|
||||
|
||||
window.location.href = '/web/login'
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-config-provider :locale="locale" :size="assemblySize" :button="buttonConfig">
|
||||
<router-view></router-view>
|
||||
</el-config-provider>
|
||||
<div class="flex-col min-h-screen w-screen" v-if="App.shard().loginInfo.isLogin()">
|
||||
<!-- nav -->
|
||||
<div class="h-[4rem] w-full flex items-center fixed justify-end">
|
||||
<div class="h-[4rem] w-full flex items-center">
|
||||
<img src="./assets/logo.png" class="w-10 h-10 ml-[2rem] mt-1">
|
||||
<div class="font-bold text-xl ml-2">悟空IM</div>
|
||||
</div>
|
||||
<div class="mr-20 flex">
|
||||
<label class="mt-[0.4rem] mr-2 text-sm">{{App.shard().loginInfo.username}}</label>
|
||||
<button class="btn btn-sm" v-on:click="()=>onLogout()">
|
||||
<svg width="18" height="18" viewBox="0 0 48 48" fill="none">
|
||||
<path d="M23.9917 6H6V42H24" stroke="#333" stroke-width="4" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
<path d="M33 33L42 24L33 15" stroke="#333" stroke-width="4" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
<path d="M16 23.9917H42" stroke="#333" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<main class="flex pt-[4rem] w-full min-h-screen">
|
||||
<!-- left -->
|
||||
<div class="flex-col w-80 h-full z-50">
|
||||
<!-- menus -->
|
||||
|
||||
<!-- <div class="w-full h-full flex-col justify-start text-left pt-5">
|
||||
<ul class="menu w-56 rounded-box">
|
||||
<li>
|
||||
<RouterLink to="/cluster/nodes">
|
||||
<svg class="h-5 w-5" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9 18V42H39V18L24 6L9 18Z" fill="none" stroke="#333" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
<path d="M19 29V42H29V29H19Z" fill="none" stroke="#333" stroke-width="2" stroke-linejoin="round" />
|
||||
<path d="M9 42H39" stroke="#333" stroke-width="2" stroke-linecap="round" />
|
||||
</svg>
|
||||
<label class="ml-2">首页</label>
|
||||
</RouterLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div> -->
|
||||
|
||||
<!-- cluster -->
|
||||
<div class="w-full h-full flex-col justify-start text-left pt-5">
|
||||
<div class="w-full flex-col">
|
||||
<div class="w-full flex pl-6">
|
||||
<label class="text-[1.1rem] font-semibold">分布式</label>
|
||||
</div>
|
||||
|
||||
<ul className="menu w-56 rounded-box">
|
||||
<li>
|
||||
<RouterLink to="/cluster/nodes">
|
||||
<svg class="h-5 w-5" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="4" y="34" width="8" height="8" fill="none" stroke="#333" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
<rect x="8" y="6" width="32" height="12" fill="none" stroke="#333" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M24 34V18" stroke="#333" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M8 34V26H40V34" stroke="#333" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
<rect x="36" y="34" width="8" height="8" fill="none" stroke="#333" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
<rect x="20" y="34" width="8" height="8" fill="none" stroke="#333" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M14 12H16" stroke="#333" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
<label class="ml-2">节点</label>
|
||||
</RouterLink>
|
||||
</li>
|
||||
<li>
|
||||
<RouterLink to="/cluster/slots">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<label class="ml-2">分区(槽)</label>
|
||||
</RouterLink>
|
||||
</li>
|
||||
<li>
|
||||
<RouterLink to="/cluster/channels">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"
|
||||
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||||
</svg>
|
||||
<label class="ml-2">频道</label>
|
||||
</RouterLink>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<RouterLink to="/cluster/config">
|
||||
<svg class="h-5 w-5" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M41.5 10H35.5" stroke="#333" stroke-width="4" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
<path d="M27.5 6V14" stroke="#333" stroke-width="4" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
<path d="M27.5 10L5.5 10" stroke="#333" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
<path d="M13.5 24H5.5" stroke="#333" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
<path d="M21.5 20V28" stroke="#333" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
<path d="M43.5 24H21.5" stroke="#333" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
<path d="M41.5 38H35.5" stroke="#333" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
<path d="M27.5 34V42" stroke="#333" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
<path d="M27.5 38H5.5" stroke="#333" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
</svg>
|
||||
<label class="ml-2">配置</label>
|
||||
</RouterLink>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div class="w-full bg-gray-100 mt-0" style="height: 1px;"></div> -->
|
||||
<div class="w-full h-full flex-col justify-start text-left pt-5">
|
||||
<div class="w-full flex-col">
|
||||
<div class="w-full flex pl-6">
|
||||
<label class="text-[1.1rem] font-bold">数据</label>
|
||||
</div>
|
||||
|
||||
<ul className="menu w-56 rounded-box">
|
||||
<li>
|
||||
<RouterLink to="/data/connection">
|
||||
<svg class="h-5 w-5" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M37 22.0001L34 25.0001L23 14.0001L26 11.0001C27.5 9.50002 33 7.00005 37 11.0001C41 15.0001 38.5 20.5 37 22.0001Z"
|
||||
fill="none" stroke="#333" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M42 6L37 11" stroke="#333" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
<path
|
||||
d="M11 25.9999L14 22.9999L25 33.9999L22 36.9999C20.5 38.5 15 41 11 36.9999C7 32.9999 9.5 27.5 11 25.9999Z"
|
||||
fill="none" stroke="#333" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M23 32L27 28" stroke="#333" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
<path d="M6 42L11 37" stroke="#333" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
<path d="M16 25L20 21" stroke="#333" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
</svg>
|
||||
<label class="ml-2">连接(Top100)</label>
|
||||
</RouterLink>
|
||||
</li>
|
||||
<li>
|
||||
<RouterLink to="/data/user">
|
||||
<svg class="h-5 w-5" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="24" cy="12" r="8" fill="none" stroke="#333" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
<path d="M42 44C42 34.0589 33.9411 26 24 26C14.0589 26 6 34.0589 6 44" stroke="#333"
|
||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
<label class="ml-2">用户</label>
|
||||
</RouterLink>
|
||||
</li>
|
||||
<li>
|
||||
<RouterLink to="/data/device">
|
||||
<svg class="h-5 w-5" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M23 43H43V5H14V15" stroke="#333" stroke-width="2" stroke-linejoin="round" />
|
||||
<path d="M5 15H23V43H5L5 15Z" fill="none" stroke="#333" stroke-width="2" stroke-linejoin="round" />
|
||||
<path d="M13 37H15" stroke="#333" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M28 37H30" stroke="#333" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
<label class="ml-2">设备</label>
|
||||
</RouterLink>
|
||||
</li>
|
||||
<li>
|
||||
<RouterLink to="/data/message">
|
||||
<svg class="h-5 w-5" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M44 24C44 35.0457 35.0457 44 24 44C18.0265 44 4 44 4 44C4 44 4 29.0722 4 24C4 12.9543 12.9543 4 24 4C35.0457 4 44 12.9543 44 24Z"
|
||||
fill="none" stroke="#333" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M13.9999 26L20 32L33 19" stroke="#333" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
</svg>
|
||||
<label class="ml-2">消息</label>
|
||||
</RouterLink>
|
||||
</li>
|
||||
<li>
|
||||
<RouterLink to="/data/channel">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"
|
||||
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||||
</svg>
|
||||
<label class="ml-2">频道</label>
|
||||
</RouterLink>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<RouterLink to="/data/conversation">
|
||||
<svg width="20" height="20" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M44 6H4V36H13V41L23 36H44V6Z" fill="none" stroke="#333" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M14 21H34" stroke="#333" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
<label class="ml-2">会话</label>
|
||||
</RouterLink>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full h-full flex-col justify-start text-left pt-5">
|
||||
<div class="w-full flex-col">
|
||||
<div class="w-full flex pl-6">
|
||||
<label class="text-[1.1rem] font-bold">监控</label>
|
||||
</div>
|
||||
|
||||
<ul className="menu w-56 rounded-box">
|
||||
<li>
|
||||
<RouterLink to="/monitor/app">
|
||||
<svg class="h-5 w-5" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M44 5H3.99998V17H44V5Z" fill="none" stroke="#333" stroke-width="2"
|
||||
stroke-linejoin="round" />
|
||||
<path d="M3.99998 41.0301L16.1756 28.7293L22.7549 35.0301L30.7982 27L35.2786 31.368" stroke="#333"
|
||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M44 16.1719V42.1719" stroke="#333" stroke-width="2" stroke-linecap="round" />
|
||||
<path d="M3.99998 16.1719V30.1719" stroke="#333" stroke-width="2" stroke-linecap="round" />
|
||||
<path d="M13.0155 43H44" stroke="#333" stroke-width="2" stroke-linecap="round" />
|
||||
<path d="M17 11H38" stroke="#333" stroke-width="2" stroke-linecap="round" />
|
||||
<path d="M9.99998 10.9966H11" stroke="#333" stroke-width="2" stroke-linecap="round" />
|
||||
</svg>
|
||||
<label class="ml-2">应用</label>
|
||||
</RouterLink>
|
||||
</li>
|
||||
<li>
|
||||
<RouterLink to="/monitor/system">
|
||||
<svg class="h-5 w-5" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M24 44C35.0457 44 44 35.0457 44 24C44 12.9543 35.0457 4 24 4C12.9543 4 4 12.9543 4 24C4 35.0457 12.9543 44 24 44Z"
|
||||
fill="none" stroke="#333" stroke-width="2" stroke-linejoin="round" />
|
||||
<path d="M11 28.1321H16.6845L21.2234 13L24.8953 35L29.4483 24.6175L32.9127 28.1321H37" stroke="#333"
|
||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
<label class="ml-2">系统</label>
|
||||
</RouterLink>
|
||||
</li>
|
||||
<li>
|
||||
<RouterLink to="/monitor/cluster">
|
||||
<svg class="h-5 w-5" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M43 4H5C4.44772 4 4 4.48842 4 5.09091V14.9091C4 15.5116 4.44772 16 5 16H43C43.5523 16 44 15.5116 44 14.9091V5.09091C44 4.48842 43.5523 4 43 4Z"
|
||||
fill="none" stroke="#333" stroke-width="2" stroke-linejoin="round" />
|
||||
<path
|
||||
d="M43 32H5C4.44772 32 4 32.4884 4 33.0909V42.9091C4 43.5116 4.44772 44 5 44H43C43.5523 44 44 43.5116 44 42.9091V33.0909C44 32.4884 43.5523 32 43 32Z"
|
||||
fill="none" stroke="#333" stroke-width="2" stroke-linejoin="round" />
|
||||
<path d="M14 16V24.0083L34 24.0172V32" stroke="#333" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
<path d="M18 38H30" stroke="#333" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M18 10H30" stroke="#333" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
<label class="ml-2">分布式</label>
|
||||
</RouterLink>
|
||||
</li>
|
||||
<li>
|
||||
<RouterLink to="/monitor/trace">
|
||||
<svg class="h-5 w-5" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M44.0001 11C44.0001 11 44 36.0623 44 38C44 41.3137 35.0457 44 24 44C12.9543 44 4.00003 41.3137 4.00003 38C4.00003 36.1423 4 11 4 11"
|
||||
stroke="#333" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M44 29C44 32.3137 35.0457 35 24 35C12.9543 35 4 32.3137 4 29" stroke="#333"
|
||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M44 20C44 23.3137 35.0457 26 24 26C12.9543 26 4 23.3137 4 20" stroke="#333"
|
||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<ellipse cx="24" cy="10" rx="20" ry="6" fill="none" stroke="#333" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
<label class="ml-2">消息追踪</label>
|
||||
</RouterLink>
|
||||
</li>
|
||||
|
||||
<!-- <li>
|
||||
<RouterLink to="/monitor/logs">
|
||||
<svg class="h-5 w-5" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M44.0001 11C44.0001 11 44 36.0623 44 38C44 41.3137 35.0457 44 24 44C12.9543 44 4.00003 41.3137 4.00003 38C4.00003 36.1423 4 11 4 11"
|
||||
stroke="#333" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M44 29C44 32.3137 35.0457 35 24 35C12.9543 35 4 32.3137 4 29" stroke="#333"
|
||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M44 20C44 23.3137 35.0457 26 24 26C12.9543 26 4 23.3137 4 20" stroke="#333"
|
||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<ellipse cx="24" cy="10" rx="20" ry="6" fill="none" stroke="#333" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
<label class="ml-2">日志</label>
|
||||
</RouterLink>
|
||||
</li> -->
|
||||
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- right -->
|
||||
<div class="flex w-full pr-20 pt-2 relative">
|
||||
<div class="rounded content min-w-full max-w-[60rem] overflow-hidden h-[calc(100vh-4rem)]">
|
||||
<RouterView class="h-[100%]" />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<div class="flex-col min-h-screen w-screen table" v-else>
|
||||
<div class="table-cell align-middle">
|
||||
<Login class="h-[100%]" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<style scoped>
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
}
|
||||
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
|
||||
.logo.vue:hover {
|
||||
filter: drop-shadow(0 0 2em #42b883aa);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
import { useUserStore } from '@/stores/modules/user';
|
||||
import { LOGIN_URL, WK_CONFIG } from '@/config';
|
||||
import router from '@/router';
|
||||
import { ResultEnum } from '@/enums/http-enum';
|
||||
|
||||
import axios from 'axios';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import type { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
|
||||
|
||||
export interface CustomAxiosRequestConfig extends InternalAxiosRequestConfig {
|
||||
isSaas?: boolean;
|
||||
}
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
const axiosConfig = {
|
||||
baseURL: WK_CONFIG.API_URL, // 默认地址请求地址
|
||||
timeout: ResultEnum.TIMEOUT as number, // 设置超时时间
|
||||
withCredentials: false // 跨域请求时是否需要使用凭证
|
||||
};
|
||||
|
||||
class RequestHttp {
|
||||
service: AxiosInstance;
|
||||
public constructor(config: AxiosRequestConfig) {
|
||||
this.service = axios.create(config);
|
||||
|
||||
/**
|
||||
* @description 请求拦截器
|
||||
* 客户端发送请求 -> [请求拦截器] -> 服务器
|
||||
* token校验(JWT) : 接受服务器返回的 token,存储到 pinia/本地储存当中
|
||||
*/
|
||||
this.service.interceptors.request.use(
|
||||
(config: CustomAxiosRequestConfig) => {
|
||||
// 添加token
|
||||
if (userStore.token) {
|
||||
config.headers['Authorization'] = 'Bearer ' + userStore.token;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error: AxiosError) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* @description 响应拦截器
|
||||
* 服务器换返回信息 -> [拦截统一处理] -> 客户端JS获取到信息
|
||||
*/
|
||||
this.service.interceptors.response.use(
|
||||
(response: AxiosResponse & { config: CustomAxiosRequestConfig }) => {
|
||||
const { data } = response;
|
||||
return Promise.resolve(data);
|
||||
},
|
||||
(error: any) => {
|
||||
const code = error.response.status;
|
||||
if (code == 401) {
|
||||
userStore.setToken('');
|
||||
userStore.setUserInfo({ username: '您好,超管' });
|
||||
ElMessage({
|
||||
message: '登录失效,请重新登录!',
|
||||
grouping: true,
|
||||
plain: true,
|
||||
type: 'error'
|
||||
});
|
||||
return router.replace(LOGIN_URL);
|
||||
}
|
||||
|
||||
if (code == 400) {
|
||||
ElMessage({
|
||||
message: error.response.data.msg,
|
||||
grouping: true,
|
||||
plain: true,
|
||||
type: 'error'
|
||||
});
|
||||
return Promise.reject(error.response.data);
|
||||
}
|
||||
// 响应失败
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
get<T>(url: string, params?: object, _object = {}): Promise<T> {
|
||||
return this.service.get(url, { params, ..._object });
|
||||
}
|
||||
|
||||
post<T>(url: string, params?: object | string, _object = {}): Promise<T> {
|
||||
return this.service.post(url, params, _object);
|
||||
}
|
||||
|
||||
put<T>(url: string, params?: object, _object = {}): Promise<T> {
|
||||
return this.service.put(url, params, _object);
|
||||
}
|
||||
|
||||
delete<T>(url: string, params?: any, _object = {}): Promise<T> {
|
||||
return this.service.delete(url, { params, ..._object });
|
||||
}
|
||||
|
||||
download(url: string, params?: object, _object = {}): Promise<BlobPart> {
|
||||
return this.service.post(url, params, { ..._object, responseType: 'blob' });
|
||||
}
|
||||
}
|
||||
|
||||
export default new RequestHttp(axiosConfig);
|
||||
@@ -1,10 +0,0 @@
|
||||
// 请求响应参数(不包含data)
|
||||
export interface Result {
|
||||
code: number;
|
||||
msg: string;
|
||||
}
|
||||
|
||||
// 请求响应参数(包含data)
|
||||
export interface ResultData<T = any> extends Result {
|
||||
result: T;
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
import http from '@/api';
|
||||
|
||||
export const clusterApi = {
|
||||
/**
|
||||
* 获取集群节点信息
|
||||
*/
|
||||
nodes: () => {
|
||||
return http.get<any>(`/cluster/nodes`);
|
||||
},
|
||||
/**
|
||||
* 获取所有槽
|
||||
*/
|
||||
slots: () => {
|
||||
return http.get<any>(`/cluster/allslot`);
|
||||
},
|
||||
/**
|
||||
* 迁移槽
|
||||
* @param param
|
||||
*/
|
||||
migrateSlot: (param: { slot: number; migrateFrom: number; migrateTo: number }) => {
|
||||
return http.post<any>(`/cluster/slots/${param.slot}/migrate`, {
|
||||
migrate_from: param.migrateFrom,
|
||||
migrate_to: param.migrateTo
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 获取简单节点信息
|
||||
*/
|
||||
simpleNodes: () => {
|
||||
return http.get<any>(`/cluster/simpleNodes`);
|
||||
},
|
||||
/**
|
||||
* 获取节点的频道配置列表
|
||||
*/
|
||||
nodeChannelConfigs: (param: any) => {
|
||||
return http.get<any>(`/cluster/nodes/${param.nodeId}/channels`, param);
|
||||
},
|
||||
/**
|
||||
* 迁移频道
|
||||
* @param param
|
||||
*/
|
||||
migrateChannel: (param: { channelId: string; channelType: number; migrateFrom: number; migrateTo: number }) => {
|
||||
return http.post<any>(`/cluster/channels/${param.channelId}/${param.channelType}/migrate`, {
|
||||
migrate_from: param.migrateFrom,
|
||||
migrate_to: param.migrateTo
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 获取频道分布式配置
|
||||
* @param param
|
||||
*/
|
||||
channelClusterConfig: (param: { channelId: string; channelType: number; nodeId?: number }) => {
|
||||
return http.get(`/cluster/channels/${param.channelId}/${param.channelType}/config?node_id=${param.nodeId || 0}`);
|
||||
},
|
||||
/**
|
||||
* 开启频道
|
||||
* @param param
|
||||
*/
|
||||
channelStart: (param: { channelId: string; channelType: number; nodeId?: number }) => {
|
||||
return http.post<any>(`/cluster/channels/${param.channelId}/${param.channelType}/start`, { node_id: param.nodeId });
|
||||
},
|
||||
/**
|
||||
* 停止频道
|
||||
* @param param
|
||||
*/
|
||||
channelStop: (param: { channelId: string; channelType: number; nodeId?: number }) => {
|
||||
return http.post<any>(`/cluster/channels/${param.channelId}/${param.channelType}/stop`, { node_id: param.nodeId });
|
||||
},
|
||||
/**
|
||||
* 频道副本列表
|
||||
* @param param
|
||||
*/
|
||||
channelReplicas: (param: { channelId: string; channelType: number }) => {
|
||||
return http.get<any>(`/cluster/channels/${param.channelId}/${param.channelType}/replicas`);
|
||||
},
|
||||
/**
|
||||
* 获取集群配置
|
||||
*/
|
||||
clusterConfig: () => {
|
||||
return http.get<any>(`/cluster/info`);
|
||||
},
|
||||
/**
|
||||
* 获取日志
|
||||
* @param param
|
||||
*/
|
||||
clusterLogs: (param: any) => {
|
||||
return http.get<any>(`/cluster/logs`, param);
|
||||
}
|
||||
};
|
||||
@@ -1,71 +0,0 @@
|
||||
import http from '@/api';
|
||||
|
||||
export const dataApi = {
|
||||
/**
|
||||
* 获取连接列表
|
||||
* @param param
|
||||
*/
|
||||
connections: (param: any) => {
|
||||
return http.get<any>(`/connz`, param);
|
||||
},
|
||||
/**
|
||||
* 搜索用户
|
||||
* @param param
|
||||
*/
|
||||
users: (param: any) => {
|
||||
return http.get<any>(`/cluster/users`, param);
|
||||
},
|
||||
/**
|
||||
* 获取用户的设备列表
|
||||
* @param param
|
||||
*/
|
||||
devices: (param: any) => {
|
||||
return http.get<any>(`/cluster/devices`, param);
|
||||
},
|
||||
/**
|
||||
* 搜索消息
|
||||
* @param param
|
||||
*/
|
||||
searchMessages: (param: any) => {
|
||||
return http.get<any>(`/cluster/messages`, param);
|
||||
},
|
||||
/**
|
||||
* 搜索频道
|
||||
* @param param
|
||||
*/
|
||||
searchChannels: (param: any) => {
|
||||
return http.get<any>(`/cluster/channels`, param);
|
||||
},
|
||||
/**
|
||||
* 获取频道订阅者
|
||||
* @param channelId
|
||||
* @param channelType
|
||||
*/
|
||||
subscribers(channelId: string, channelType: number) {
|
||||
return http.get<any>(`/cluster/channels/${channelId}/${channelType}/subscribers`);
|
||||
},
|
||||
/**
|
||||
* 获取频道白名单列表
|
||||
* @param channelId
|
||||
* @param channelType
|
||||
*/
|
||||
allowlist(channelId: string, channelType: number) {
|
||||
return http.get<any>(`/cluster/channels/${channelId}/${channelType}/allowlist`);
|
||||
},
|
||||
/**
|
||||
* 获取频道黑名单列表
|
||||
* @param channelId
|
||||
* @param channelType
|
||||
*/
|
||||
denylist(channelId: string, channelType: number) {
|
||||
return http.get<any>(`/cluster/channels/${channelId}/${channelType}/denylist`);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取用户的会话列表
|
||||
* @param param
|
||||
*/
|
||||
conversations: (param: any) => {
|
||||
return http.get<any>(`/cluster/conversations`, param);
|
||||
}
|
||||
};
|
||||
@@ -1,13 +0,0 @@
|
||||
import http from '@/api';
|
||||
|
||||
import type { ResultData } from '@/api/interface';
|
||||
|
||||
export const loginApi = {
|
||||
/**
|
||||
* 登录
|
||||
* @param param
|
||||
*/
|
||||
login: (param: any) => {
|
||||
return http.post<ResultData>(`/manager/login`, param);
|
||||
}
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
import http from '@/api';
|
||||
|
||||
export const monitorApi = {
|
||||
/**
|
||||
* 获取app监控数据
|
||||
*/
|
||||
apppMetrics: (param: any) => {
|
||||
return http.get<any>('/metrics/app', param);
|
||||
},
|
||||
/**
|
||||
* 获取分布式监控数据
|
||||
*/
|
||||
clusterMetrics: (param: any) => {
|
||||
return http.get<any>('/metrics/cluster', param);
|
||||
},
|
||||
/**
|
||||
* 获取系统监控数据
|
||||
*/
|
||||
systemMetrics: (param: any) => {
|
||||
return http.get<any>('/metrics/system', param);
|
||||
},
|
||||
/**
|
||||
* 获取消息轨迹日志
|
||||
*/
|
||||
messageTraces: (param: any) => {
|
||||
return http.get<any>('/cluster/message/trace', param);
|
||||
},
|
||||
/**
|
||||
* 消息回执轨迹日志
|
||||
*/
|
||||
messageRecvackTraces: (param: any) => {
|
||||
return http.get<any>('/cluster/message/trace/recvack', param);
|
||||
}
|
||||
};
|
||||
@@ -1,13 +0,0 @@
|
||||
import http from '@/api';
|
||||
|
||||
import type { ResultData } from '@/api/interface';
|
||||
|
||||
export const systemApi = {
|
||||
/**
|
||||
* 获取系统设置
|
||||
* @param param
|
||||
*/
|
||||
systemSettings: () => {
|
||||
return http.get<ResultData>(`/varz/setting`);
|
||||
}
|
||||
};
|
||||
|
Before Width: | Height: | Size: 21 KiB |
BIN
web/src/assets/logo.png
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
146
web/src/components/MonitorPanel.vue
Normal file
@@ -0,0 +1,146 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, watchEffect } from 'vue';
|
||||
import * as echarts from "echarts";
|
||||
import { formatDate, formatNumber } from '../services/Utils';
|
||||
import { Series } from '../services/Model';
|
||||
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Array<Series>,
|
||||
required: true
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
}
|
||||
})
|
||||
|
||||
const chartElement = ref<any>(null);
|
||||
|
||||
const option = ref({
|
||||
title: {
|
||||
text: props.title || '',
|
||||
textStyle: {
|
||||
fontWeight: 'normal',
|
||||
fontSize: 12
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
show: true,
|
||||
trigger: 'axis',
|
||||
formatter: function (params: any) {
|
||||
var res = formatDate(new Date(params[0].name * 1000)) + '<br/>'
|
||||
for (var i = 0, length = params.length; i < length; i++) {
|
||||
res += params[i].marker + params[i].seriesName + ':'
|
||||
+ formatNumber(params[i].value) + '<br/>'
|
||||
}
|
||||
return res
|
||||
|
||||
}
|
||||
},
|
||||
legend: new Array<any>(),
|
||||
grid: {
|
||||
left: "1%",
|
||||
top: "15%",
|
||||
bottom: "15%",
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
axisLabel: {
|
||||
formatter: function (value: number) {
|
||||
const date = new Date(value * 1000);
|
||||
const hours = date.getHours();
|
||||
const minutes = date.getMinutes();
|
||||
const seconds = date.getSeconds();
|
||||
return hours + ':' + minutes + ':' + seconds;
|
||||
}
|
||||
},
|
||||
data: new Array<number>(),
|
||||
},
|
||||
yAxis: {
|
||||
boundaryGap: [0, '50%'],
|
||||
type: 'value',
|
||||
minInterval: 1,
|
||||
axisLabel: {
|
||||
formatter: function (value: number) {
|
||||
return formatNumber(value,0);
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: new Array<number>()
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
const updateData = async (data: Series[]) => {
|
||||
const series: any = []
|
||||
const legendNames: any = []
|
||||
option.value.xAxis.data = new Array<number>()
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const value = data[i];
|
||||
series.push({
|
||||
name: value.name,
|
||||
type: value.type || 'line',
|
||||
symbol: 'none',
|
||||
data: new Array<number>(),
|
||||
})
|
||||
legendNames.push(value.name || '')
|
||||
}
|
||||
option.value.series = series
|
||||
option.value.legend = [{
|
||||
data: legendNames,
|
||||
bottom: '0%',
|
||||
textStyle: {
|
||||
fontSize: 12,
|
||||
},
|
||||
}]
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const value = data[i];
|
||||
for (let j = 0; j < value.data.length; j++) {
|
||||
const point = value.data[j]
|
||||
|
||||
if (i == 0) {
|
||||
option.value.xAxis.data.push(point.timestamp || 0);
|
||||
}
|
||||
option.value.series[i].data.push(point.value);
|
||||
}
|
||||
|
||||
}
|
||||
chart?.setOption(option.value);
|
||||
}
|
||||
|
||||
const refreshChart = () => {
|
||||
chart?.setOption(option.value);
|
||||
}
|
||||
|
||||
let chart: echarts.ECharts
|
||||
// 待组件挂载完成后初始化 ECharts
|
||||
onMounted(() => {
|
||||
|
||||
chart = echarts.init(chartElement.value);
|
||||
chart.setOption(option.value);
|
||||
|
||||
// startRealtimeData();
|
||||
updateData(props.data)
|
||||
refreshChart()
|
||||
window.addEventListener('resize', () => chart.resize())
|
||||
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
updateData(props.data)
|
||||
refreshChart()
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<div class="h-full w-full" ref="chartElement"></div>
|
||||
</template>
|
||||
53
web/src/components/RecvackTable.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div class="w-full h-full">
|
||||
<table class="table table-pin-rows">
|
||||
<!-- head -->
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
连接ID
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
用户UID
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
设备ID
|
||||
</div>
|
||||
</th>
|
||||
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
收到时间
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
<tr v-for="item in props.data?.data">
|
||||
<td>{{ item.conn_id }}</td>
|
||||
<td>{{ item.uid }}</td>
|
||||
<td>{{ item.device_id }}</td>
|
||||
<td>{{ item.time }}</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
})
|
||||
|
||||
</script>
|
||||
23
web/src/components/SpanNode.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<div class="w-full h-full border border-blue-500 rounded-md shadow-md grid grid-rows-3 grid-cols-[auto,1fr] bg-white">
|
||||
<div class="row-span-3 p-2" v-html="data.icon"></div>
|
||||
<div class="font-semibold flex items-center text-[0.9rem] justify-between">
|
||||
<div>{{data.name}}</div>
|
||||
</div>
|
||||
<div class="text text-sm text-gray-400 flex items-center">{{data.description}}</div>
|
||||
<div class="text text-sm text-gray-400 flex items-center">{{data.time}}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject, onMounted,ref } from 'vue';
|
||||
const getNode = inject<() => any>('getNode');
|
||||
const data = ref({name: '', time: '',icon:'',duration:0,description:''});
|
||||
onMounted(() => {
|
||||
const node = getNode!();
|
||||
data.value = node.data;
|
||||
});
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
18
web/src/components/TextNode.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<div class="w-full h-full border border-blue-500 rounded-md shadow-md bg-white">
|
||||
<div class="w-full h-full font-semibold flex items-center text-[0.9rem] justify-center">
|
||||
<div>{{data.name}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject, onMounted,ref } from 'vue';
|
||||
const getNode = inject<() => any>('getNode');
|
||||
const data = ref({name: ''});
|
||||
onMounted(() => {
|
||||
const node = getNode!();
|
||||
data.value = node.data;
|
||||
});
|
||||
|
||||
</script>
|
||||
@@ -1,138 +0,0 @@
|
||||
<template>
|
||||
<div class="h-full w-full" ref="chartElement"></div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" name="WkMonitorPanel" setup>
|
||||
import { onMounted, ref, watchEffect } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import { formatDate, formatNumber } from '@/utils';
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Array<any>,
|
||||
required: true
|
||||
},
|
||||
title: {
|
||||
type: String
|
||||
}
|
||||
});
|
||||
|
||||
const chartElement = ref<any>(null);
|
||||
|
||||
const option = ref({
|
||||
title: {
|
||||
text: props.title || '',
|
||||
textStyle: {
|
||||
fontWeight: 'normal',
|
||||
fontSize: 12
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
show: true,
|
||||
trigger: 'axis',
|
||||
formatter: function (params: any) {
|
||||
let res = formatDate(new Date(params[0].name * 1000)) + '<br/>';
|
||||
for (let i = 0, length = params.length; i < length; i++) {
|
||||
res += params[i].marker + params[i].seriesName + ':' + formatNumber(params[i].value) + '<br/>';
|
||||
}
|
||||
return res;
|
||||
}
|
||||
},
|
||||
legend: new Array<any>(),
|
||||
grid: {
|
||||
left: '1%',
|
||||
top: '15%',
|
||||
bottom: '15%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
axisLabel: {
|
||||
formatter: function (value: number) {
|
||||
const date = new Date(value * 1000);
|
||||
const hours = date.getHours();
|
||||
const minutes = date.getMinutes();
|
||||
const seconds = date.getSeconds();
|
||||
return hours + ':' + minutes + ':' + seconds;
|
||||
}
|
||||
},
|
||||
data: new Array<number>()
|
||||
},
|
||||
yAxis: {
|
||||
boundaryGap: [0, '50%'],
|
||||
type: 'value',
|
||||
minInterval: 1,
|
||||
axisLabel: {
|
||||
formatter: function (value: number) {
|
||||
return formatNumber(value, 0);
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: new Array<number>()
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const updateData = async (data: any[]) => {
|
||||
const series: any = [];
|
||||
const legendNames: any = [];
|
||||
option.value.xAxis.data = new Array<number>();
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const value = data[i];
|
||||
series.push({
|
||||
name: value.name,
|
||||
type: value.type || 'line',
|
||||
symbol: 'none',
|
||||
data: new Array<number>()
|
||||
});
|
||||
legendNames.push(value.name || '');
|
||||
}
|
||||
option.value.series = series;
|
||||
option.value.legend = [
|
||||
{
|
||||
data: legendNames,
|
||||
bottom: '0%',
|
||||
textStyle: {
|
||||
fontSize: 12
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const value = data[i];
|
||||
for (let j = 0; j < value.data.length; j++) {
|
||||
const point = value.data[j];
|
||||
|
||||
if (i == 0) {
|
||||
option.value.xAxis.data.push(point.timestamp || 0);
|
||||
}
|
||||
option.value.series[i].data.push(point.value);
|
||||
}
|
||||
}
|
||||
chart?.setOption(option.value);
|
||||
};
|
||||
|
||||
const refreshChart = () => {
|
||||
chart?.setOption(option.value);
|
||||
};
|
||||
|
||||
let chart: echarts.ECharts;
|
||||
// 待组件挂载完成后初始化 ECharts
|
||||
onMounted(() => {
|
||||
chart = echarts.init(chartElement.value);
|
||||
chart.setOption(option.value);
|
||||
|
||||
// startRealtimeData();
|
||||
updateData(props.data);
|
||||
refreshChart();
|
||||
window.addEventListener('resize', () => chart.resize());
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
updateData(props.data);
|
||||
refreshChart();
|
||||
});
|
||||
</script>
|
||||
@@ -1,15 +0,0 @@
|
||||
<template>
|
||||
<div class="wk-page">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" name="WkPage" setup></script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.wk-page {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 12px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
@@ -1,22 +0,0 @@
|
||||
// 首页地址(默认)
|
||||
export const HOME_URL = '/cluster/nodes';
|
||||
|
||||
// 登录页地址(默认)
|
||||
export const LOGIN_URL = '/login';
|
||||
|
||||
// 默认主题颜色
|
||||
export const DEFAULT_PRIMARY = '#DF5D2A';
|
||||
|
||||
// 命名空间
|
||||
export const NAME_SPACE = 'wk';
|
||||
|
||||
// 路由白名单地址(必须是本地存在的路由 static-router.ts 中)
|
||||
export const ROUTER_WHITE_LIST: string[] = ['/login'];
|
||||
|
||||
// 默认应用配置
|
||||
export const WK_CONFIG = {
|
||||
APP_TITLE: '悟空IM分布式',
|
||||
APP_TITLE_SHORT: 'IM',
|
||||
APP_TITLE_LOGIN: 'WuKongIM分布式管理系统',
|
||||
API_URL: 'http://monitor.githubim.com'
|
||||
};
|
||||
@@ -1,30 +0,0 @@
|
||||
interface IItem {
|
||||
value: number;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export const CHANNEL_TYPE: IItem[] = [
|
||||
{ value: 1, label: '个人' },
|
||||
{ value: 2, label: '群聊' },
|
||||
{ value: 3, label: '客服' },
|
||||
{ value: 4, label: '社区' },
|
||||
{ value: 5, label: '话题' },
|
||||
{ value: 6, label: '资讯' },
|
||||
{ value: 7, label: '数据' }
|
||||
];
|
||||
|
||||
export const LATEST_TIME: IItem[] = [
|
||||
{ value: 300, label: '过去5分钟' },
|
||||
{ value: 1800, label: '过去30分钟' },
|
||||
{ value: 3600, label: '过去1小时' },
|
||||
{ value: 21600, label: '过去6小时' },
|
||||
{ value: 86400, label: '过去1天' },
|
||||
{ value: 259200, label: '过去3天' },
|
||||
{ value: 604800, label: '过去7天' }
|
||||
];
|
||||
|
||||
export const APP_DOC = 'https://githubim.com';
|
||||
export const APP_ISSUES = 'https://github.com/WuKongIM/WuKongIM/issues';
|
||||
|
||||
export const ActionWrite = 'w';
|
||||
export const ActionRead = 'r';
|
||||
@@ -1,35 +0,0 @@
|
||||
/**
|
||||
* @description:请求配置
|
||||
*/
|
||||
export enum ResultEnum {
|
||||
SUCCESS = 200,
|
||||
ERROR = 500,
|
||||
OVERDUE = 501,
|
||||
TIMEOUT = 30000,
|
||||
TYPE = 'success'
|
||||
}
|
||||
|
||||
/**
|
||||
* @description:请求方法
|
||||
*/
|
||||
export enum RequestEnum {
|
||||
GET = 'GET',
|
||||
POST = 'POST',
|
||||
PATCH = 'PATCH',
|
||||
PUT = 'PUT',
|
||||
DELETE = 'DELETE'
|
||||
}
|
||||
|
||||
/**
|
||||
* @description:常用的 contentTyp 类型
|
||||
*/
|
||||
export enum ContentTypeEnum {
|
||||
// json
|
||||
JSON = 'application/json;charset=UTF-8',
|
||||
// text
|
||||
TEXT = 'text/plain;charset=UTF-8',
|
||||
// form-data 一般配合qs
|
||||
FORM_URLENCODED = 'application/x-www-form-urlencoded;charset=UTF-8',
|
||||
// form-data 上传
|
||||
FORM_DATA = 'multipart/form-data;charset=UTF-8'
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
export namespace Table {
|
||||
export interface Pageable {
|
||||
pageNum: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
}
|
||||
export interface TableStateProps {
|
||||
tableData: any[];
|
||||
pageable: Pageable;
|
||||
searchParam: {
|
||||
[key: string]: any;
|
||||
};
|
||||
searchInitParam: {
|
||||
[key: string]: any;
|
||||
};
|
||||
totalParam: {
|
||||
[key: string]: any;
|
||||
};
|
||||
icon?: {
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace HandleData {
|
||||
export type MessageType = '' | 'success' | 'warning' | 'info' | 'error';
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace Theme {
|
||||
export type ThemeType = 'light' | 'inverted' | 'dark';
|
||||
export type GreyOrWeakType = 'grey' | 'weak';
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
import { DEFAULT_PRIMARY } from '@/config';
|
||||
import { useGlobalStore } from '@/stores/modules/global';
|
||||
import {getDarkColor, getLightColor, convertToHslCssVar} from '@/utils/color';
|
||||
import { menuTheme } from '@/styles/theme/menu';
|
||||
import { asideTheme } from '@/styles/theme/aside';
|
||||
import { headerTheme } from '@/styles/theme/header';
|
||||
|
||||
import { storeToRefs } from 'pinia';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import type { Theme } from './interface';
|
||||
|
||||
/**
|
||||
* @description 全局主题 hooks
|
||||
* */
|
||||
export const useTheme = () => {
|
||||
const globalStore = useGlobalStore();
|
||||
const { primary, isDark, isGrey, isWeak, asideInverted, layout, headerInverted } = storeToRefs(globalStore);
|
||||
|
||||
// 切换暗黑模式 ==> 并带修改主题颜色、侧边栏颜色
|
||||
const switchDark = () => {
|
||||
const html = document.documentElement as HTMLElement;
|
||||
if (isDark.value) {
|
||||
html.setAttribute('class', 'dark');
|
||||
html.setAttribute('data-vxe-ui-theme', 'dark');
|
||||
html.setAttribute('vxe-docs-theme', 'dark');
|
||||
} else {
|
||||
html.setAttribute('class', '');
|
||||
html.setAttribute('data-vxe-ui-theme', 'light');
|
||||
html.setAttribute('vxe-docs-theme', 'light');
|
||||
}
|
||||
changePrimary(primary.value);
|
||||
setAsideTheme();
|
||||
setHeaderTheme();
|
||||
};
|
||||
|
||||
// 修改主题颜色
|
||||
const changePrimary = (val: any | null) => {
|
||||
if (!val) {
|
||||
val = DEFAULT_PRIMARY;
|
||||
ElMessage({ type: 'success', message: `主题颜色已重置为 ${DEFAULT_PRIMARY}` });
|
||||
}
|
||||
val = val.hex ? val.hex : val;
|
||||
// 计算主题颜色变化
|
||||
document.documentElement.style.setProperty('--el-color-primary', val);
|
||||
document.documentElement.style.setProperty(
|
||||
'--el-color-primary-dark-2',
|
||||
isDark.value ? `${getLightColor(val, 0.2)}` : `${getDarkColor(val, 0.3)}`
|
||||
);
|
||||
for (let i = 1; i <= 9; i++) {
|
||||
const primaryColor = isDark.value ? `${getDarkColor(val, i / 10)}` : `${getLightColor(val, i / 10)}`;
|
||||
document.documentElement.style.setProperty(`--el-color-primary-light-${i}`, primaryColor);
|
||||
}
|
||||
|
||||
document.documentElement.style.setProperty(`--primary`, convertToHslCssVar(val))
|
||||
globalStore.setGlobalState('primary', val);
|
||||
};
|
||||
|
||||
// 灰色和弱色切换
|
||||
const changeGreyOrWeak = (type: Theme.GreyOrWeakType, value: boolean) => {
|
||||
const body = document.body as HTMLElement;
|
||||
if (!value) return body.removeAttribute('style');
|
||||
const styles: Record<Theme.GreyOrWeakType, string> = {
|
||||
grey: 'filter: grayscale(1)',
|
||||
weak: 'filter: invert(80%)'
|
||||
};
|
||||
body.setAttribute('style', styles[type]);
|
||||
const propName = type === 'grey' ? 'isWeak' : 'isGrey';
|
||||
globalStore.setGlobalState(propName, false);
|
||||
};
|
||||
// 设置菜单样式
|
||||
const setMenuTheme = () => {
|
||||
let type: Theme.ThemeType = 'light';
|
||||
if (layout.value === 'transverse' && headerInverted.value) type = 'inverted';
|
||||
if (layout.value !== 'transverse' && asideInverted.value) type = 'inverted';
|
||||
if (isDark.value) type = 'dark';
|
||||
const theme = menuTheme[type!];
|
||||
for (const [key, value] of Object.entries(theme)) {
|
||||
document.documentElement.style.setProperty(key, value);
|
||||
}
|
||||
};
|
||||
// 设置侧边栏样式
|
||||
const setAsideTheme = () => {
|
||||
let type: Theme.ThemeType = 'light';
|
||||
if (asideInverted.value) type = 'inverted';
|
||||
if (isDark.value) type = 'dark';
|
||||
const theme = asideTheme[type!];
|
||||
for (const [key, value] of Object.entries(theme)) {
|
||||
document.documentElement.style.setProperty(key, value);
|
||||
}
|
||||
setMenuTheme();
|
||||
};
|
||||
|
||||
// 设置头部样式
|
||||
const setHeaderTheme = () => {
|
||||
let type: Theme.ThemeType = 'light';
|
||||
if (headerInverted.value) type = 'inverted';
|
||||
if (isDark.value) type = 'dark';
|
||||
const theme = headerTheme[type!];
|
||||
for (const [key, value] of Object.entries(theme)) {
|
||||
document.documentElement.style.setProperty(key, value);
|
||||
}
|
||||
setMenuTheme();
|
||||
};
|
||||
|
||||
// init theme
|
||||
const initTheme = () => {
|
||||
switchDark();
|
||||
if (isGrey.value) changeGreyOrWeak('grey', true);
|
||||
if (isWeak.value) changeGreyOrWeak('weak', true);
|
||||
};
|
||||
|
||||
return {
|
||||
initTheme,
|
||||
switchDark,
|
||||
changePrimary,
|
||||
changeGreyOrWeak,
|
||||
setAsideTheme,
|
||||
setHeaderTheme
|
||||
};
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
import { getBrowserLang } from '@/utils'
|
||||
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import zh from './modules/zh'
|
||||
import en from './modules/en'
|
||||
|
||||
const i18n = createI18n({
|
||||
allowComposition: true,
|
||||
legacy: false,
|
||||
locale: getBrowserLang(),
|
||||
messages: {
|
||||
zh,
|
||||
en
|
||||
}
|
||||
})
|
||||
|
||||
export default i18n
|
||||
@@ -1,28 +0,0 @@
|
||||
export default {
|
||||
home: {
|
||||
welcome: 'Welcome'
|
||||
},
|
||||
tabs: {
|
||||
more: 'More',
|
||||
refresh: 'Refresh',
|
||||
maximize: 'Maximize',
|
||||
closeCurrent: 'Close current',
|
||||
closeOther: 'Close other',
|
||||
closeAll: 'Close All'
|
||||
},
|
||||
header: {
|
||||
componentSize: 'Component size',
|
||||
language: 'Language',
|
||||
theme: 'theme',
|
||||
layoutConfig: 'Layout config',
|
||||
primary: 'primary',
|
||||
darkMode: 'Dark Mode',
|
||||
greyMode: 'Grey mode',
|
||||
weakMode: 'Weak mode',
|
||||
fullScreen: 'Full Screen',
|
||||
exitFullScreen: 'Exit Full Screen',
|
||||
personalData: 'Personal Data',
|
||||
changePassword: 'Change Password',
|
||||
logout: 'Logout'
|
||||
}
|
||||
};
|
||||
@@ -1,28 +0,0 @@
|
||||
export default {
|
||||
home: {
|
||||
welcome: '欢迎使用'
|
||||
},
|
||||
tabs: {
|
||||
more: '更多',
|
||||
refresh: '刷新',
|
||||
maximize: '最大化',
|
||||
closeCurrent: '关闭当前',
|
||||
closeOther: '关闭其它',
|
||||
closeAll: '关闭所有'
|
||||
},
|
||||
header: {
|
||||
componentSize: '组件大小',
|
||||
language: '国际化',
|
||||
theme: '全局主题',
|
||||
layoutConfig: '布局设置',
|
||||
primary: 'primary',
|
||||
darkMode: '暗黑模式',
|
||||
greyMode: '灰色模式',
|
||||
weakMode: '色弱模式',
|
||||
fullScreen: '全屏',
|
||||
exitFullScreen: '退出全屏',
|
||||
personalData: '个人信息',
|
||||
changePassword: '修改密码',
|
||||
logout: '退出登录'
|
||||
}
|
||||
};
|
||||
@@ -1,35 +0,0 @@
|
||||
<script lang="ts" name="Main" setup>
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useGlobalStore } from '@/stores/modules/global';
|
||||
|
||||
const globalStore = useGlobalStore();
|
||||
const { layout } = storeToRefs(globalStore);
|
||||
// 监听布局变化,在 body 上添加相对应的 layout class
|
||||
watch(
|
||||
() => layout.value,
|
||||
() => {
|
||||
const body = document.body as HTMLElement;
|
||||
body.setAttribute('class', layout.value);
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-main>
|
||||
<router-view v-slot="{ Component, route }">
|
||||
<transition appear name="fade-transform" mode="out-in">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</el-main>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-main {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
overflow-x: hidden;
|
||||
background-color: var(--el-bg-color-page);
|
||||
}
|
||||
</style>
|
||||
@@ -1,21 +0,0 @@
|
||||
<script name="ToolBarLeft" lang="ts" setup>
|
||||
import { useGlobalStore } from '@/stores/modules/global';
|
||||
import Breadcrumb from './components/Breadcrumb.vue';
|
||||
const globalStore = useGlobalStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tool-bar-lf">
|
||||
<Breadcrumb v-if="globalStore.breadcrumb" id="breadcrumb" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.tool-bar-lf {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@@ -1,26 +0,0 @@
|
||||
<script name="ToolBarRight" lang="ts" setup>
|
||||
import Avatar from './components/Avatar.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tool-bar-ri">
|
||||
<div class="header-icon"></div>
|
||||
<Avatar id="avatar" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.tool-bar-ri {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.header-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
& > * {
|
||||
margin-left: 20px;
|
||||
color: var(--el-header-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,111 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useUserStore } from '@/stores/modules/user';
|
||||
import { LOGIN_URL } from '@/config';
|
||||
import { APP_DOC, APP_ISSUES } from '@/constants';
|
||||
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
const username = computed(() => {
|
||||
return userStore.userInfo.username;
|
||||
});
|
||||
|
||||
// 文档
|
||||
const onOpenDoc = () => {
|
||||
window.open(APP_DOC, '_blank');
|
||||
};
|
||||
|
||||
// 问题 & 帮助
|
||||
const onOpenIssue = () => {
|
||||
window.open(APP_ISSUES, '_blank');
|
||||
};
|
||||
|
||||
// 退出登录
|
||||
const onLogoutClick = () => {
|
||||
// 1.清除 Token
|
||||
userStore.logout();
|
||||
// 2.重定向到登陆页
|
||||
router.replace(LOGIN_URL);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dropdown trigger="click" placement="bottom-end">
|
||||
<div class="flex items-center cursor-pointer">
|
||||
<div class="avatar relative">
|
||||
<img src="@/assets/images/avatar.webp" alt="avatar" />
|
||||
<span class="wk-badge !size-8px"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu class="w-238px">
|
||||
<div class="pl-15px pr-15px pt-10px pb-10px flex">
|
||||
<div class="inline-flex items-center justify-center w-48px h-48px bg-secondary overflow-hidden rounded-full relative">
|
||||
<img class="h-full w-full object-cover" src="@/assets/images/avatar.webp" alt="avatar" />
|
||||
<span class="wk-badge"></span>
|
||||
</div>
|
||||
<div class="flex-1 ml-10px">
|
||||
<div class="mt-4px flex items-center text-sm font-medium">
|
||||
{{ username }}
|
||||
</div>
|
||||
<div class="mt-8px text-xs font-normal wk-role">角色:管理员</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-dropdown-item divided @click="onOpenDoc">
|
||||
<el-icon class="mt-2px" :size="16">
|
||||
<i-wk-book-open theme="outline" />
|
||||
</el-icon>
|
||||
文档
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="onOpenIssue">
|
||||
<el-icon class="mt-2px" :size="16">
|
||||
<i-wk-help theme="outline" :size="24" />
|
||||
</el-icon>
|
||||
问题 & 帮助
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item divided @click="onLogoutClick">
|
||||
<el-icon class="mt-3px" :size="16">
|
||||
<i-wk-power theme="outline" />
|
||||
</el-icon>
|
||||
退出登录
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.username {
|
||||
margin: 0 16px;
|
||||
font-size: 14px;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
}
|
||||
.avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
overflow: hidden;
|
||||
border-radius: 50%;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.wk-role {
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
|
||||
.wk-badge {
|
||||
position: absolute;
|
||||
background-color: rgb(87, 209, 136);
|
||||
vertical-align: middle;
|
||||
border-radius: 50%;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
display: inline-block;
|
||||
bottom: 6px;
|
||||
right: 6px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,95 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { HOME_URL } from '@/config';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { ArrowRight } from '@element-plus/icons-vue';
|
||||
import { useAuthStore } from '@/stores/modules/auth';
|
||||
import { useGlobalStore } from '@/stores/modules/global';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const authStore = useAuthStore();
|
||||
const globalStore = useGlobalStore();
|
||||
|
||||
const breadcrumbList = computed(() => {
|
||||
let breadcrumbData = authStore.breadcrumbListGet[route.matched[route.matched.length - 1].path] ?? [];
|
||||
// 🙅♀️不需要首页面包屑可删除以下判断
|
||||
if (breadcrumbData.length > 0 && breadcrumbData[0].path !== HOME_URL) {
|
||||
breadcrumbData = [...breadcrumbData];
|
||||
}
|
||||
return breadcrumbData;
|
||||
});
|
||||
|
||||
// Click Breadcrumb
|
||||
const onBreadcrumbClick = (item: Menu.MenuOptions, index: number) => {
|
||||
if (index !== breadcrumbList.value.length - 1) router.push(item.path);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="['breadcrumb-box', !globalStore.breadcrumbIcon && 'no-icon']">
|
||||
<el-breadcrumb :separator-icon="ArrowRight">
|
||||
<transition-group name="breadcrumb">
|
||||
<el-breadcrumb-item v-for="(item, index) in breadcrumbList" :key="item.path">
|
||||
<div class="el-breadcrumb__inner is-link" @click="onBreadcrumbClick(item, index)">
|
||||
<el-icon v-show="item.meta.icon && globalStore.breadcrumbIcon" class="breadcrumb-icon">
|
||||
<component :is="item.meta.icon"></component>
|
||||
</el-icon>
|
||||
<span class="breadcrumb-title">{{ item.meta.title }}</span>
|
||||
</div>
|
||||
</el-breadcrumb-item>
|
||||
</transition-group>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.breadcrumb-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
.el-breadcrumb {
|
||||
white-space: nowrap;
|
||||
.el-breadcrumb__item {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
float: none;
|
||||
.el-breadcrumb__inner {
|
||||
display: inline-flex;
|
||||
&.is-link {
|
||||
color: var(--el-header-text-color);
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
.breadcrumb-icon {
|
||||
margin-top: 2px;
|
||||
margin-right: 6px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.breadcrumb-title {
|
||||
margin-top: 3px;
|
||||
}
|
||||
}
|
||||
&:last-child .el-breadcrumb__inner,
|
||||
&:last-child .el-breadcrumb__inner:hover {
|
||||
color: var(--el-header-text-color-regular);
|
||||
}
|
||||
:deep(.el-breadcrumb__separator) {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.no-icon {
|
||||
.el-breadcrumb {
|
||||
.el-breadcrumb__item {
|
||||
top: -2px;
|
||||
:deep(.el-breadcrumb__separator) {
|
||||
top: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,85 +0,0 @@
|
||||
<script lang="ts" name="SubMenu" setup>
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
defineProps<{ menuList: Menu.MenuOptions[] }>();
|
||||
|
||||
const router = useRouter();
|
||||
const handleClickMenu = (subItem: Menu.MenuOptions) => {
|
||||
if (subItem.meta.isLink) return window.open(subItem.meta.isLink, '_blank');
|
||||
router.push(subItem.path);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-for="subItem in menuList" :key="subItem.path">
|
||||
<el-sub-menu v-if="subItem.children?.length" :index="subItem.path">
|
||||
<template #title>
|
||||
<el-icon v-if="subItem.meta.icon">
|
||||
<component :is="subItem.meta.icon"></component>
|
||||
</el-icon>
|
||||
<span class="sle">{{ subItem.meta.title }}</span>
|
||||
</template>
|
||||
<SubMenu :menu-list="subItem.children" />
|
||||
</el-sub-menu>
|
||||
<el-menu-item v-else :index="subItem.path" @click="handleClickMenu(subItem)">
|
||||
<el-icon v-if="subItem.meta.icon">
|
||||
<component :is="subItem.meta.icon"></component>
|
||||
</el-icon>
|
||||
<template #title>
|
||||
<span class="sle">{{ subItem.meta.title }}</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.el-sub-menu .el-sub-menu__title:hover {
|
||||
color: var(--el-menu-hover-text-color) !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
.el-menu--collapse {
|
||||
.is-active {
|
||||
.el-sub-menu__title {
|
||||
color: #ffffff !important;
|
||||
background-color: var(--el-color-primary) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.el-menu-item {
|
||||
&:hover {
|
||||
color: var(--el-menu-hover-text-color);
|
||||
}
|
||||
&.is-active {
|
||||
color: var(--el-menu-active-color) !important;
|
||||
background-color: var(--el-menu-active-bg-color) !important;
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 4px;
|
||||
content: '';
|
||||
background-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
.vertical,
|
||||
.classic,
|
||||
.transverse {
|
||||
.el-menu-item {
|
||||
&.is-active {
|
||||
&::before {
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.columns {
|
||||
.el-menu-item {
|
||||
&.is-active {
|
||||
&::before {
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,132 +0,0 @@
|
||||
<script lang="ts" name="Main" setup>
|
||||
// API 接口
|
||||
import { systemApi } from '@/api/modules/system-api.ts';
|
||||
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useAuthStore } from '@/stores/modules/auth';
|
||||
import { useGlobalStore } from '@/stores/modules/global';
|
||||
import { useUserStore } from '@/stores/modules/user';
|
||||
|
||||
import { HOME_URL, WK_CONFIG } from '@/config';
|
||||
// 组件
|
||||
import ToolBarLeft from '@/layouts/components/Header/ToolBarLeft.vue';
|
||||
import ToolBarRight from '@/layouts/components/Header/ToolBarRight.vue';
|
||||
import Main from '@/layouts/components/Main.vue';
|
||||
import SubMenu from '@/layouts/components/menu/SubMenu.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const authStore = useAuthStore();
|
||||
const globalStore = useGlobalStore();
|
||||
const userStore = useUserStore();
|
||||
const isCollapse = computed(() => globalStore.isCollapse);
|
||||
const menuList = computed(() => authStore.showMenuListGet);
|
||||
const activeMenu = computed(() => (route.meta.activeMenu ? route.meta.activeMenu : route.path) as string);
|
||||
const APP_TITLE = WK_CONFIG.APP_TITLE;
|
||||
|
||||
/**
|
||||
* 获取系统设置
|
||||
*/
|
||||
const getSystemSettings = async () => {
|
||||
const res = await systemApi.systemSettings();
|
||||
userStore.setSystemSetting(res);
|
||||
};
|
||||
|
||||
const onPageHome = () => {
|
||||
router.push(HOME_URL);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getSystemSettings();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-container class="layout">
|
||||
<el-aside>
|
||||
<div class="aside-box w-224px">
|
||||
<div class="logo flex-center cursor-pointer" @click="onPageHome">
|
||||
<img class="logo-img" src="/logo.png" alt="logo" />
|
||||
<span class="logo-text">{{ APP_TITLE }}</span>
|
||||
</div>
|
||||
|
||||
<el-scrollbar>
|
||||
<el-menu
|
||||
:default-active="activeMenu"
|
||||
:collapse="isCollapse"
|
||||
:router="false"
|
||||
:unique-opened="true"
|
||||
:collapse-transition="false"
|
||||
>
|
||||
<SubMenu :menu-list="menuList" />
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</el-aside>
|
||||
<el-container>
|
||||
<el-header>
|
||||
<ToolBarLeft />
|
||||
<ToolBarRight />
|
||||
</el-header>
|
||||
<Main />
|
||||
</el-container>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
:deep(.el-aside) {
|
||||
width: auto;
|
||||
background-color: var(--el-menu-bg-color);
|
||||
border-right: 1px solid var(--el-aside-border-color);
|
||||
.aside-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
transition: width 0.3s ease;
|
||||
.el-scrollbar {
|
||||
height: calc(100% - 55px);
|
||||
.el-menu {
|
||||
width: 100%;
|
||||
overflow-x: hidden;
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
.logo {
|
||||
box-sizing: border-box;
|
||||
height: 55px;
|
||||
.logo-img {
|
||||
width: 28px;
|
||||
object-fit: contain;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.logo-text {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: var(--el-aside-logo-text-color);
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.el-header {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 50px;
|
||||
padding: 0 12px;
|
||||
background-color: var(--el-header-bg-color);
|
||||
border-bottom: 1px solid var(--el-header-border-color);
|
||||
:deep(.tool-bar-ri) {
|
||||
.toolBar-icon,
|
||||
.username {
|
||||
color: var(--el-header-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,35 +1,15 @@
|
||||
import App from '@/App.vue';
|
||||
// router
|
||||
import router from './router';
|
||||
// pinia store
|
||||
import pinia from '@/stores';
|
||||
// vue i18n
|
||||
import I18n from '@/i18n';
|
||||
import { createApp } from 'vue'
|
||||
import './style.css'
|
||||
import App from './App.vue'
|
||||
|
||||
import 'uno.css';
|
||||
import '@/styles/index.scss';
|
||||
import router from './router'
|
||||
|
||||
// element icons
|
||||
import * as Icons from '@element-plus/icons-vue';
|
||||
// icon-park
|
||||
import { install } from '@icon-park/vue-next/es/all';
|
||||
|
||||
import 'vue-global-api';
|
||||
// import APIClient from './services/APIClient'
|
||||
// APIClient.shared.config.apiURL = "http://localhost:5300" // 本地调试用,正式环境请注释掉
|
||||
|
||||
import { setupUi } from '@/ui';
|
||||
const app = createApp(App)
|
||||
|
||||
import { createApp } from 'vue';
|
||||
app.use(router)
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
install(app, 'i-wk');
|
||||
// register the element Icons component
|
||||
Object.keys(Icons).forEach(key => {
|
||||
app.component(key, Icons[key as keyof typeof Icons]);
|
||||
});
|
||||
|
||||
app.use(router);
|
||||
app.use(setupUi);
|
||||
app.use(I18n);
|
||||
app.use(pinia);
|
||||
app.mount('#app');
|
||||
app.mount('#app')
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
const menuModuleList: any[] = [];
|
||||
const modules = import.meta.glob('./modules/**/*.ts', { import: 'default', eager: true });
|
||||
Object.keys(modules).forEach(key => {
|
||||
const mod = modules[key] || {};
|
||||
const modList = Array.isArray(mod) ? [...mod] : [mod];
|
||||
menuModuleList.push(...modList);
|
||||
});
|
||||
|
||||
// 按照index 升序
|
||||
menuModuleList.sort((a, b) => {
|
||||
return a.meta.index - b.meta.index;
|
||||
});
|
||||
|
||||
export default [...menuModuleList];
|
||||
@@ -1,81 +0,0 @@
|
||||
const cluster: Menu.MenuOptions = {
|
||||
name: 'cluster',
|
||||
path: '/cluster',
|
||||
meta: {
|
||||
icon: 'i-wk-branch-two',
|
||||
isAffix: true,
|
||||
isFull: false,
|
||||
isHide: false,
|
||||
isKeepAlive: true,
|
||||
isLink: '',
|
||||
title: '分布式'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'cluster_nodes',
|
||||
path: '/cluster/nodes',
|
||||
meta: {
|
||||
icon: 'i-wk-connection-point',
|
||||
isAffix: false,
|
||||
isFull: false,
|
||||
isHide: false,
|
||||
isKeepAlive: true,
|
||||
isLink: '',
|
||||
title: '节点'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'cluster_slots',
|
||||
path: '/cluster/slots',
|
||||
meta: {
|
||||
icon: 'i-wk-insert-card',
|
||||
isAffix: false,
|
||||
isFull: false,
|
||||
isHide: false,
|
||||
isKeepAlive: true,
|
||||
isLink: '',
|
||||
title: '分区(槽)'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'cluster_channels',
|
||||
path: '/cluster/channels',
|
||||
meta: {
|
||||
icon: 'i-wk-broadcast',
|
||||
isAffix: false,
|
||||
isFull: false,
|
||||
isHide: false,
|
||||
isKeepAlive: true,
|
||||
isLink: '',
|
||||
title: '频道'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'cluster_config',
|
||||
path: '/cluster/config',
|
||||
meta: {
|
||||
icon: 'i-wk-setting-config',
|
||||
isAffix: false,
|
||||
isFull: false,
|
||||
isHide: false,
|
||||
isKeepAlive: true,
|
||||
isLink: '',
|
||||
title: '配置'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'cluster_log',
|
||||
path: '/cluster/log',
|
||||
meta: {
|
||||
icon: 'i-wk-log',
|
||||
isAffix: false,
|
||||
isFull: false,
|
||||
isHide: true,
|
||||
isKeepAlive: true,
|
||||
isLink: '',
|
||||
title: '日志'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
export default cluster;
|
||||
@@ -1,94 +0,0 @@
|
||||
const data: Menu.MenuOptions = {
|
||||
name: 'data',
|
||||
path: '/data',
|
||||
meta: {
|
||||
icon: 'i-wk-data-one',
|
||||
isAffix: true,
|
||||
isFull: false,
|
||||
isHide: false,
|
||||
isKeepAlive: true,
|
||||
isLink: '',
|
||||
title: '数据'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'data_connection',
|
||||
path: '/data/connection',
|
||||
meta: {
|
||||
icon: 'i-wk-api',
|
||||
isAffix: false,
|
||||
isFull: false,
|
||||
isHide: false,
|
||||
isKeepAlive: true,
|
||||
isLink: '',
|
||||
title: '链接'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'data_user',
|
||||
path: '/data/user',
|
||||
meta: {
|
||||
icon: 'i-wk-user',
|
||||
isAffix: false,
|
||||
isFull: false,
|
||||
isHide: false,
|
||||
isKeepAlive: true,
|
||||
isLink: '',
|
||||
title: '用户'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'data_device',
|
||||
path: '/data/device',
|
||||
meta: {
|
||||
icon: 'i-wk-devices',
|
||||
isAffix: false,
|
||||
isFull: false,
|
||||
isHide: false,
|
||||
isKeepAlive: true,
|
||||
isLink: '',
|
||||
title: '设备'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'data_message',
|
||||
path: '/data/message',
|
||||
meta: {
|
||||
icon: 'i-wk-message',
|
||||
isAffix: false,
|
||||
isFull: false,
|
||||
isHide: false,
|
||||
isKeepAlive: true,
|
||||
isLink: '',
|
||||
title: '消息'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'data_channel',
|
||||
path: '/data/channel',
|
||||
meta: {
|
||||
icon: 'i-wk-broadcast',
|
||||
isAffix: false,
|
||||
isFull: false,
|
||||
isHide: false,
|
||||
isKeepAlive: true,
|
||||
isLink: '',
|
||||
title: '频道'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'data_conversation',
|
||||
path: '/data/conversation',
|
||||
meta: {
|
||||
icon: 'i-wk-communication',
|
||||
isAffix: false,
|
||||
isFull: false,
|
||||
isHide: false,
|
||||
isKeepAlive: true,
|
||||
isLink: '',
|
||||
title: '会话'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
export default data;
|
||||
@@ -1,68 +0,0 @@
|
||||
const monitor: Menu.MenuOptions = {
|
||||
name: 'monitor',
|
||||
path: '/monitor',
|
||||
meta: {
|
||||
icon: 'i-wk-monitor',
|
||||
isAffix: true,
|
||||
isFull: false,
|
||||
isHide: false,
|
||||
isKeepAlive: true,
|
||||
isLink: '',
|
||||
title: '监控'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'monitor_app',
|
||||
path: '/monitor/app',
|
||||
meta: {
|
||||
icon: 'i-wk-application-one',
|
||||
isAffix: false,
|
||||
isFull: false,
|
||||
isHide: false,
|
||||
isKeepAlive: true,
|
||||
isLink: '',
|
||||
title: '应用'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'monitor_system',
|
||||
path: '/monitor/system',
|
||||
meta: {
|
||||
icon: 'i-wk-system',
|
||||
isAffix: false,
|
||||
isFull: false,
|
||||
isHide: false,
|
||||
isKeepAlive: true,
|
||||
isLink: '',
|
||||
title: '系统'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'monitor_cluster',
|
||||
path: '/monitor/cluster',
|
||||
meta: {
|
||||
icon: 'i-wk-connection-box',
|
||||
isAffix: false,
|
||||
isFull: false,
|
||||
isHide: false,
|
||||
isKeepAlive: true,
|
||||
isLink: '',
|
||||
title: '分布式'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'monitor_trace',
|
||||
path: '/monitor/trace',
|
||||
meta: {
|
||||
icon: 'i-wk-trace',
|
||||
isAffix: false,
|
||||
isFull: false,
|
||||
isHide: false,
|
||||
isKeepAlive: true,
|
||||
isLink: '',
|
||||
title: '消息追踪'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
export default monitor;
|
||||
421
web/src/pages/cluster/Channel.vue
Normal file
@@ -0,0 +1,421 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
import API from '../../services/API';
|
||||
import VueJsonPretty from 'vue-json-pretty';
|
||||
import 'vue-json-pretty/lib/styles.css';
|
||||
import { useRouter } from "vue-router";
|
||||
import App, { ActionWrite } from '../../services/App';
|
||||
import { alertNoPermission } from '../../services/Utils';
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const nodeTotal = ref<any>({}); // 节点列表
|
||||
const selectedNodeId = ref<number>(1) // 选中的节点ID
|
||||
const channelTotal = ref<any>({}); // 频道列表
|
||||
const loading = ref<boolean>(false);
|
||||
const selectedChannel = ref<any>({}); // 选中的频道
|
||||
const selectedMigrateFrom = ref<number>() // 选中的源节点ID
|
||||
const selectedMigrateTo = ref<number>() // 选中的目标节点ID
|
||||
const config = ref<any>({}); // 频道配置
|
||||
const channelId = ref<string>() // 接受频道id
|
||||
const channelType = ref<number>() // 频道类型
|
||||
|
||||
const pageSize = ref<number>(20) // 每页数量
|
||||
const currentPage = ref(1); // 当前页
|
||||
|
||||
const replicas = ref<any>({}); // 副本
|
||||
const offsetCreatedAt = ref(0); // 偏移量
|
||||
const pre = ref<boolean>() // 是否向上分页
|
||||
const hasNext = ref<boolean>(true) // 是否有下一页
|
||||
const hasPrev = ref<boolean>(false) // 是否有上一页
|
||||
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
API.shared.simpleNodes().then((res) => {
|
||||
nodeTotal.value = res
|
||||
if (nodeTotal.value.data.length > 0) {
|
||||
selectedNodeId.value = nodeTotal.value.data[0].id
|
||||
}
|
||||
loadNodeChannels()
|
||||
}).catch((err) => {
|
||||
alert(err)
|
||||
})
|
||||
})
|
||||
|
||||
const onNodeChange = (e: any) => {
|
||||
resetFilter()
|
||||
selectedNodeId.value = parseInt(e.target.value)
|
||||
loadNodeChannels()
|
||||
}
|
||||
|
||||
const loadNodeChannels = () => {
|
||||
loading.value = true;
|
||||
API.shared.nodeChannelConfigs({
|
||||
channelId: channelId.value,
|
||||
channelType: channelType.value,
|
||||
nodeId: selectedNodeId.value,
|
||||
limit: pageSize.value,
|
||||
offsetCreatedAt: offsetCreatedAt.value,
|
||||
pre: pre.value
|
||||
}).then((res) => {
|
||||
channelTotal.value = res
|
||||
hasNext.value = channelTotal.value?.more === 1
|
||||
hasPrev.value = currentPage.value > 1
|
||||
if (pre.value) { // 如果是向上翻页,则有下页数据
|
||||
hasNext.value = true
|
||||
}
|
||||
}).catch((err) => {
|
||||
alert(err)
|
||||
}).finally(() => {
|
||||
loading.value = false;
|
||||
})
|
||||
}
|
||||
|
||||
const onShowMigrateModal = (ch: any) => {
|
||||
|
||||
if (!App.shard().loginInfo.hasPermission('channelMigrate', ActionWrite)) {
|
||||
alertNoPermission()
|
||||
return
|
||||
}
|
||||
|
||||
selectedChannel.value = ch;
|
||||
const migrateModal = document.getElementById('migrateModal') as HTMLDialogElement;
|
||||
migrateModal.showModal();
|
||||
}
|
||||
|
||||
const onMigrate = () => {
|
||||
|
||||
const migrateModal = document.getElementById('migrateModal') as HTMLDialogElement;
|
||||
migrateModal.close();
|
||||
|
||||
API.shared.migrateChannel({
|
||||
channelId: selectedChannel.value.channel_id,
|
||||
channelType: selectedChannel.value.channel_type,
|
||||
migrateFrom: selectedMigrateFrom.value || 0,
|
||||
migrateTo: selectedMigrateTo.value || 0,
|
||||
|
||||
}).then((_) => {
|
||||
loadNodeChannels()
|
||||
}).catch((err) => {
|
||||
alert(err.msg)
|
||||
})
|
||||
}
|
||||
|
||||
const onChannelClusterConfig = (ch: any) => {
|
||||
|
||||
resetFilter()
|
||||
|
||||
selectedChannel.value = ch;
|
||||
|
||||
|
||||
API.shared.channelClusterConfig({
|
||||
channelId: selectedChannel.value.channel_id,
|
||||
channelType: selectedChannel.value.channel_type,
|
||||
nodeId: selectedNodeId.value
|
||||
}).then((res) => {
|
||||
config.value = res
|
||||
const modal = document.getElementById('channelClusterModal') as HTMLDialogElement;
|
||||
modal.showModal();
|
||||
}).catch((err) => {
|
||||
alert(err)
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
const onChannelStartOrStop = (ch: any) => {
|
||||
|
||||
if (!App.shard().loginInfo.hasPermission('channelStart', ActionWrite)) {
|
||||
alertNoPermission()
|
||||
return
|
||||
}
|
||||
|
||||
const req = {
|
||||
channelId: ch.channel_id,
|
||||
channelType: ch.channel_type,
|
||||
nodeId: selectedNodeId.value
|
||||
}
|
||||
if (ch.active == 1) {
|
||||
API.shared.channelStop(req).then((_) => {
|
||||
loadNodeChannels()
|
||||
}).catch((err) => {
|
||||
alert(err.msg)
|
||||
})
|
||||
} else {
|
||||
API.shared.channelStart(req).then((_) => {
|
||||
loadNodeChannels()
|
||||
}).catch((err) => {
|
||||
alert(err.msg)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const resetFilter = () => {
|
||||
currentPage.value = 1
|
||||
offsetCreatedAt.value = 0
|
||||
pre.value = false
|
||||
hasNext.value = true
|
||||
hasPrev.value = false
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 上一页
|
||||
const prevPage = () => {
|
||||
if (currentPage.value <= 1) {
|
||||
hasPrev.value = false
|
||||
return
|
||||
}
|
||||
hasPrev.value = true
|
||||
currentPage.value -= 1
|
||||
pre.value = true
|
||||
if (channelTotal.value?.data?.length > 0) {
|
||||
offsetCreatedAt.value = channelTotal.value.data[0].created_at
|
||||
}
|
||||
loadNodeChannels()
|
||||
}
|
||||
|
||||
// 下一页
|
||||
const nextPage = () => {
|
||||
if (channelTotal.value?.more === 0 && !pre.value) {
|
||||
return
|
||||
}
|
||||
currentPage.value += 1
|
||||
pre.value = false
|
||||
if (channelTotal.value?.data?.length > 0) {
|
||||
offsetCreatedAt.value = channelTotal.value.data[channelTotal.value.data.length - 1].created_at
|
||||
}
|
||||
loadNodeChannels()
|
||||
}
|
||||
|
||||
|
||||
const onChannelIdSearch = (e: any) => {
|
||||
channelId.value = e.target.value
|
||||
if (!channelId.value || channelId.value.trim() == '') {
|
||||
channelType.value = 0
|
||||
}
|
||||
currentPage.value = 1
|
||||
loadNodeChannels()
|
||||
}
|
||||
|
||||
const onChannelTypeSearch = (e: any) => {
|
||||
channelType.value = e.target.value
|
||||
if (!channelId.value || channelId.value.trim() == '') {
|
||||
return
|
||||
}
|
||||
currentPage.value = 1
|
||||
loadNodeChannels()
|
||||
}
|
||||
|
||||
// const onRunningSearch = (e: any) => {
|
||||
// running.value = e.target.checked
|
||||
// currentPage.value = 1
|
||||
// loadNodeChannels()
|
||||
// }
|
||||
|
||||
// 查看副本
|
||||
const onReplicas = (ch: any) => {
|
||||
const modal = document.getElementById('replicas') as HTMLDialogElement;
|
||||
modal.showModal();
|
||||
|
||||
API.shared.channelReplicas({
|
||||
channelId: ch.channel_id,
|
||||
channelType: ch.channel_type,
|
||||
}).then((res) => {
|
||||
replicas.value = res
|
||||
const modal = document.getElementById('replicas') as HTMLDialogElement;
|
||||
modal.showModal();
|
||||
}).catch((err) => {
|
||||
alert(err)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// 查看消息
|
||||
const onMessage = (ch: any) => {
|
||||
router.push({
|
||||
path: '/data/message',
|
||||
query: {
|
||||
channelId: ch.channel_id,
|
||||
channelType: ch.channel_type,
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="overflow-x-auto h-5/6">
|
||||
<div class="flex">
|
||||
<div class="text-sm ml-3">
|
||||
<label>节点</label>
|
||||
<select class="select select-bordered max-w-xs select-sm w-40 ml-2" v-on:change="onNodeChange">
|
||||
<option v-for="node in nodeTotal.data" :selected="node.id == selectedNodeId" :value="node.id">{{
|
||||
node.id }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 频道 -->
|
||||
<div class="text-sm ml-10">
|
||||
<label>接受频道</label>
|
||||
<select class="select select-bordered max-w-xs select-sm w-20 ml-2"
|
||||
v-on:change="onChannelTypeSearch" v-model="channelType">
|
||||
<option value="1">个人</option>
|
||||
<option value="2">群聊</option>
|
||||
<option value="3">客服</option>
|
||||
<option value="4">社区</option>
|
||||
<option value="5">话题</option>
|
||||
<option value="6">资讯</option>
|
||||
<option value="7">数据</option>
|
||||
</select>
|
||||
<input type="text" placeholder="输入" class="input input-bordered select-sm ml-2"
|
||||
v-on:change="onChannelIdSearch" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<table class="table mt-10 table-pin-rows">
|
||||
<!-- head -->
|
||||
<thead>
|
||||
<tr>
|
||||
<th>频道ID</th>
|
||||
<th>频道类型</th>
|
||||
<th>所属槽</th>
|
||||
<th>槽领导</th>
|
||||
<th>频道领导</th>
|
||||
|
||||
<th>任期</th>
|
||||
<th>副本节点</th>
|
||||
<th>消息高度</th>
|
||||
<th>最后消息时间</th>
|
||||
<th>是否运行</th>
|
||||
<th>状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- row 1 -->
|
||||
<tr v-for="channel in channelTotal.data">
|
||||
<td class="text-blue-800">{{ channel.channel_id }}</td>
|
||||
<td>{{ channel.channel_type_format }}</td>
|
||||
<td>{{ channel.slot_id }}</td>
|
||||
<td>{{ channel.slot_leader_id }}</td>
|
||||
<td>{{ channel.leader_id }}</td>
|
||||
<td>{{ channel.term }}</td>
|
||||
<td>{{ channel.replicas }} <label class="text-red-500"
|
||||
v-if="channel.migrate_from != 0">[{{ channel.migrate_from }} 迁移至 {{ channel.migrate_to
|
||||
}}
|
||||
]</label></td>
|
||||
<td>{{ channel.last_message_seq }}</td>
|
||||
<td>{{ channel.last_append_time }}</td>
|
||||
<td :class="channel.active == 1 ? 'text-green-500' : 'text-red-500'">{{ channel.active_format }}
|
||||
</td>
|
||||
<td :class="channel.status == 0 ? 'text-green-500' : 'text-red-500'">{{ channel.status_format }}
|
||||
</td>
|
||||
<td class="flex flex-wrap gap-2">
|
||||
|
||||
<button
|
||||
:class="channel.active == 1 ? 'btn btn-primary btn-sm btn-secondary' : 'btn btn-primary btn-sm'"
|
||||
v-on:click="() => onChannelStartOrStop(channel)">{{ channel.active
|
||||
== 1 ? "停止" : "开始" }}</button>
|
||||
<button class="btn btn-primary btn-sm"
|
||||
v-on:click="() => onShowMigrateModal(channel)">迁移</button>
|
||||
<button class="btn btn-primary btn-sm"
|
||||
v-on:click="() => onChannelClusterConfig(channel)">配置</button>
|
||||
<button class="btn btn-primary btn-sm" v-on:click="() => onReplicas(channel)">副本</button>
|
||||
<button class="btn btn-primary btn-sm" v-on:click="() => onMessage(channel)">消息</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- <div class="flex flex-col gap-4 w-full mt-2" v-if="loading">
|
||||
<div class="skeleton h-6 w-full"></div>
|
||||
<div class="skeleton h-6 w-full"></div>
|
||||
<div class="skeleton h-6 w-full"></div>
|
||||
<div class="skeleton h-6 w-full"></div>
|
||||
<div class="skeleton h-6 w-full"></div>
|
||||
<div class="skeleton h-6 w-full"></div>
|
||||
<div class="skeleton h-6 w-full"></div>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end mt-10">
|
||||
|
||||
<div class="flex">
|
||||
<div className="join">
|
||||
<button :class="{ 'join-item btn': true, 'btn-disabled': !hasPrev }"
|
||||
v-on:click="prevPage">«</button>
|
||||
<button className="join-item btn">{{ currentPage }}</button>
|
||||
<button :class="{ 'join-item btn': true, 'btn-disabled': !hasNext }"
|
||||
v-on:click="nextPage">»</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<dialog id="migrateModal" class="modal">
|
||||
<div class="modal-box flex flex-wrap gap-2 justify-center">
|
||||
<select class="select select-bordered" v-model="selectedMigrateFrom">
|
||||
<option v-for="nodeId in selectedChannel.replicas" :value="nodeId">{{ nodeId }}</option>
|
||||
</select>
|
||||
<div class="flex items-center"> 迁移至 </div>
|
||||
<select class="select select-bordered" v-model="selectedMigrateTo">
|
||||
<option v-for="destNode in nodeTotal.data" :value="destNode.id">{{ destNode.id }}</option>
|
||||
</select>
|
||||
|
||||
<button className="btn-primary btn" v-on:click="onMigrate">确认</button>
|
||||
<!-- <a class="link" v-for="uid in currentUids">{{ uid }}</a> -->
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<dialog id="channelClusterModal" class="modal">
|
||||
<div class="modal-box flex flex-wrap gap-2 justify-center">
|
||||
<vue-json-pretty :data="config" class="overflow-auto" />
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<dialog id="replicas" class="modal">
|
||||
<div class="modal-box flex flex-wrap gap-2 justify-center">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-xs">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>节点ID</th>
|
||||
<th>节点角色</th>
|
||||
<th>消息高度</th>
|
||||
<th>最后消息时间</th>
|
||||
<th>是否运行</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="replica in replicas">
|
||||
<td>{{ replica.replica_id }}</td>
|
||||
<td>{{ replica.role_format }}</td>
|
||||
<td>{{ replica.last_msg_seq }}</td>
|
||||
<td>{{ replica.last_msg_time_format }}</td>
|
||||
<td :class="replica.running == 1 ? 'text-green-500' : 'text-red-500'">
|
||||
{{ replica.running == 1 ? '运行中' : '未运行' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,31 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
// API 接口
|
||||
import { clusterApi } from '@/api/modules/cluster-api';
|
||||
<script lang="ts" setup>
|
||||
import { defineComponent, onMounted, ref } from 'vue';
|
||||
import VueJsonPretty from 'vue-json-pretty';
|
||||
import 'vue-json-pretty/lib/styles.css';
|
||||
import API from '../../services/API';
|
||||
|
||||
const config = ref<any>({});
|
||||
|
||||
const getConfig = async () => {
|
||||
config.value = await clusterApi.clusterConfig();
|
||||
};
|
||||
defineComponent({
|
||||
components: {
|
||||
VueJsonPretty,
|
||||
},
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
getConfig();
|
||||
console.log('mounted');
|
||||
API.shared.clusterConfig().then((res) => {
|
||||
config.value = res
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<wk-page class="flex-col overflow-hidden">
|
||||
<div class="flex-1 card">
|
||||
<VueJsonPretty :data="config" />
|
||||
</div>
|
||||
</wk-page>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
title: 配置
|
||||
</route>
|
||||
<vue-json-pretty :data="config" class="overflow-auto"/>
|
||||
</template>
|
||||
@@ -1,157 +1,94 @@
|
||||
<script setup lang="ts">
|
||||
// API 接口
|
||||
import { clusterApi } from '@/api/modules/cluster-api';
|
||||
|
||||
import { useRoute } from 'vue-router';
|
||||
import type { VxeGridInstance, VxeGridProps } from 'vxe-table';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import API from '../../services/API';
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter()
|
||||
const query = router.currentRoute.value.query; //查询参数
|
||||
|
||||
/**
|
||||
* 查询条件
|
||||
*/
|
||||
interface IFormData {
|
||||
node_id?: string;
|
||||
slot?: string;
|
||||
log_type?: string;
|
||||
pre: number;
|
||||
next: number;
|
||||
}
|
||||
const logTotal = ref<any>({});
|
||||
|
||||
const formData = reactive<IFormData>({
|
||||
node_id: '',
|
||||
slot: '',
|
||||
log_type: '',
|
||||
pre: 0,
|
||||
next: 0
|
||||
});
|
||||
|
||||
/**
|
||||
* 表格
|
||||
*/
|
||||
const tableRef = ref<VxeGridInstance<any>>();
|
||||
const applied = ref(0);
|
||||
const currentPage = ref(1); // 当前页
|
||||
const hasPrev = ref<boolean>(false); // 是否有上一页
|
||||
const hasNext = ref<boolean>(true); // 是否有下一页
|
||||
|
||||
const loadList = async (query: any) => {
|
||||
const res = await clusterApi.clusterLogs({ ...query });
|
||||
applied.value = res.applied || 0;
|
||||
if (res.logs) {
|
||||
hasNext.value = res.more !== 1;
|
||||
hasPrev.value = currentPage.value <= 1;
|
||||
return res.logs;
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const gridOptions = reactive<VxeGridProps<any>>({
|
||||
showOverflow: true,
|
||||
height: 'auto',
|
||||
border: true,
|
||||
stripe: true,
|
||||
rowConfig: {
|
||||
isCurrent: true,
|
||||
isHover: true
|
||||
},
|
||||
scrollY: {
|
||||
enabled: true,
|
||||
gt: 0
|
||||
},
|
||||
toolbarConfig: {
|
||||
slots: {
|
||||
buttons: 'tools'
|
||||
},
|
||||
refresh: {
|
||||
icon: 'vxe-icon-refresh',
|
||||
iconLoading: 'vxe-icon-refresh roll'
|
||||
},
|
||||
zoom: {
|
||||
iconIn: 'vxe-icon-fullscreen',
|
||||
iconOut: 'vxe-icon-minimize'
|
||||
},
|
||||
custom: true
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: () => {
|
||||
return loadList(formData);
|
||||
}
|
||||
}
|
||||
},
|
||||
columns: [
|
||||
{ type: 'seq', title: '序号', width: 54 },
|
||||
{ field: 'id', title: '日志ID', width: 180 },
|
||||
{ field: 'index', title: '日志下标', width: 100 },
|
||||
{ field: 'term', title: '日志任期', width: 100 },
|
||||
{ field: 'cmd', title: '命令类型', minWidth: 160 },
|
||||
{ field: 'data', title: '命令内容', minWidth: 420 },
|
||||
{ field: 'time_format', title: '日志时间', width: 160 },
|
||||
{
|
||||
field: 'status',
|
||||
title: '状态',
|
||||
width: 90,
|
||||
formatter({ row }) {
|
||||
return row.index <= applied.value ? '已应用' : '未应用';
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
/**
|
||||
* 分页
|
||||
* @param type
|
||||
*/
|
||||
const onPage = (type: 0 | 1) => {
|
||||
// 下一页
|
||||
const tableData = tableRef.value?.getData();
|
||||
if (type === 0) {
|
||||
formData.pre = 0;
|
||||
currentPage.value = currentPage.value + 1;
|
||||
if (tableData && tableData.length > 0) {
|
||||
formData.next = tableData[tableData.length - 1].index;
|
||||
}
|
||||
}
|
||||
|
||||
// 上一页
|
||||
if (type === 1 && currentPage.value > 1) {
|
||||
formData.next = 0;
|
||||
currentPage.value = currentPage.value - 1;
|
||||
if (tableData && tableData.length > 0) {
|
||||
formData.pre = tableData[0].index;
|
||||
}
|
||||
}
|
||||
|
||||
tableRef.value?.commitProxy('query');
|
||||
};
|
||||
const nextIndex = ref<number>(0);
|
||||
const preIndex = ref<number>(0);
|
||||
|
||||
onMounted(() => {
|
||||
if (route.query) {
|
||||
formData.node_id = (route.query?.node_id as string) || '';
|
||||
formData.log_type = (route.query?.log_type as string) || '';
|
||||
formData.slot = (route.query?.slot as string) || '';
|
||||
}
|
||||
console.log('mounted');
|
||||
loadData();
|
||||
});
|
||||
|
||||
const loadData = () => {
|
||||
const nodeId = Number(query.node_id);
|
||||
const slot = Number(query.slot);
|
||||
const logType = query.log_type as string || "";
|
||||
|
||||
API.shared.clusterLogs({
|
||||
nodeId: nodeId,
|
||||
slot: slot,
|
||||
next: nextIndex.value,
|
||||
pre: preIndex.value,
|
||||
logType: logType
|
||||
}).then((res) => {
|
||||
logTotal.value = res
|
||||
});
|
||||
}
|
||||
|
||||
const prevPage = () => {
|
||||
preIndex.value = logTotal.value.pre || 0;
|
||||
nextIndex.value = 0;
|
||||
loadData();
|
||||
}
|
||||
|
||||
const nextPage = () => {
|
||||
preIndex.value = 0;
|
||||
nextIndex.value = logTotal.value.next || 0;
|
||||
loadData();
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<wk-page class="flex-col">
|
||||
<!-- S 表格 -->
|
||||
<div class="flex-1 card !pt-4px overflow-hidden">
|
||||
<vxe-grid ref="tableRef" v-bind="gridOptions">
|
||||
<template #tools>
|
||||
<el-space>
|
||||
<el-button type="primary" :disabled="hasPrev" @click="onPage(1)">上一页</el-button>
|
||||
<el-button type="primary" :disabled="hasNext" @click="onPage(0)">下一页</el-button>
|
||||
</el-space>
|
||||
</template>
|
||||
</vxe-grid>
|
||||
</div>
|
||||
<!-- E 表格 -->
|
||||
</wk-page>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
title: 日志
|
||||
</route>
|
||||
<template>
|
||||
<div>
|
||||
<div class="overflow-x-auto h-5/6">
|
||||
<table class="table">
|
||||
<!-- head -->
|
||||
<thead>
|
||||
<tr>
|
||||
<th>日志Id</th>
|
||||
<th>日志下标</th>
|
||||
<th>日志任期</th>
|
||||
<th>命令类型</th>
|
||||
<th>命令内容</th>
|
||||
<th>日志时间</th>
|
||||
<th>状态</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- row 1 -->
|
||||
<tr v-for="log in logTotal.logs">
|
||||
<td>{{ log.id }}</td>
|
||||
<td>{{ log.index }}</td>
|
||||
<td>{{ log.term }}</td>
|
||||
<td>{{ log.cmd }}</td>
|
||||
<td>{{ log.data }}</td>
|
||||
<td>{{ log.time_format }}</td>
|
||||
<td v-if="log.index <= logTotal.applied">已应用</td>
|
||||
<td v-if="log.index > logTotal.applied" class="text-red-500">未应用</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="flex items-center mt-10 justify-end">
|
||||
|
||||
<div class="flex">
|
||||
<div className="join">
|
||||
<button :class="{'join-item btn':true,'btn-disabled':logTotal.logs && logTotal.logs.length>0&&logTotal.logs[0].index==logTotal.last}" v-on:click="prevPage">«</button>
|
||||
<!-- <button className="join-item btn">{{ currentPage }}</button> -->
|
||||
<button :class="{'join-item btn':true,'btn-disabled':logTotal.logs && logTotal.logs.length>0&&logTotal.logs[logTotal.logs.length-1].index==1}" v-on:click="nextPage">»</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
106
web/src/pages/cluster/Node.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
import API from '../../services/API';
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const nodeTotal = ref<any>({});
|
||||
const loading = ref<boolean>(false);
|
||||
|
||||
onMounted(() => {
|
||||
console.log('mounted');
|
||||
loading.value = true;
|
||||
API.shared.nodes().then((res) => {
|
||||
nodeTotal.value = res
|
||||
}).catch((err) => {
|
||||
if(err.msg) {
|
||||
alert(err)
|
||||
}
|
||||
|
||||
}).finally(() => {
|
||||
loading.value = false;
|
||||
})
|
||||
});
|
||||
|
||||
const getRole = (node: any) => {
|
||||
if (node.is_leader == 1) {
|
||||
return '领导'
|
||||
}
|
||||
if (node.role == 1) {
|
||||
return '代理'
|
||||
}
|
||||
return '副本'
|
||||
}
|
||||
|
||||
const onLog = (node: any) => {
|
||||
console.log(node)
|
||||
|
||||
router.push({
|
||||
path: '/cluster/log',
|
||||
query: {
|
||||
node_id: node.id
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table">
|
||||
<!-- head -->
|
||||
<thead>
|
||||
<tr>
|
||||
<th>节点ID</th>
|
||||
<th>角色</th>
|
||||
<th>任期</th>
|
||||
<th>槽领导/槽数量</th>
|
||||
<th>投票权</th>
|
||||
<th>在线</th>
|
||||
<th>离线次数</th>
|
||||
<th>运行时间</th>
|
||||
<th>地址</th>
|
||||
<th>程序版本</th>
|
||||
<th>配置版本</th>
|
||||
<th>状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- row 1 -->
|
||||
<tr v-for="node in nodeTotal.data">
|
||||
<td class="text-blue-800">
|
||||
<RouterLink to="/node/detail">{{ node.id }}</RouterLink>
|
||||
</td>
|
||||
<td :class="node.is_leader == 1 ? 'text-blue-400' : ''">{{ getRole(node) }}</td>
|
||||
<td>{{node.term}}</td>
|
||||
<td>{{ node.slot_leader_count }}/{{ node.slot_count }}</td>
|
||||
<td>{{ node.allow_vote == 1 ? '有' : '无' }}</td>
|
||||
<td :class="node.online == 1 ? 'text-green-500' : ''">{{ node.online == 1 ? '在线' : '离线' }}</td>
|
||||
<td>{{node.offline_count>0?`${node.offline_count}(${node.last_offline})`:`0`}}</td>
|
||||
<td>{{ node.uptime }}</td>
|
||||
<td>{{ node.cluster_addr }}</td>
|
||||
<td>{{ node.app_version }}</td>
|
||||
<td>{{ node.config_version }}</td>
|
||||
<td>{{ node.status_format }}</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-primary" v-on:click="()=>onLog(node)">日志</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="flex flex-col gap-4 w-full mt-2" v-if="loading">
|
||||
<div class="skeleton h-6 w-full"></div>
|
||||
<div class="skeleton h-6 w-full"></div>
|
||||
<div class="skeleton h-6 w-full"></div>
|
||||
<div class="skeleton h-6 w-full"></div>
|
||||
<div class="skeleton h-6 w-full"></div>
|
||||
<div class="skeleton h-6 w-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
206
web/src/pages/cluster/Slot.vue
Normal file
@@ -0,0 +1,206 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
import API from '../../services/API';
|
||||
import { useRouter } from "vue-router";
|
||||
import App, { ActionWrite } from '../../services/App';
|
||||
import { alertNoPermission } from '../../services/Utils';
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const nodeTotal = ref<any>({}); // 节点列表
|
||||
|
||||
const slotTotal = ref<any>({});
|
||||
const loading = ref<boolean>(false);
|
||||
|
||||
const orderBy = ref<string>('') // 排序字段
|
||||
const desc = ref<boolean>(false) // 是否降序
|
||||
|
||||
const selectedMigrateFrom = ref<number>() // 选中的源节点ID
|
||||
const selectedMigrateTo = ref<number>() // 选中的目标节点ID
|
||||
const selectedSlot = ref<any>({}); // 选中的槽
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
loadData()
|
||||
|
||||
API.shared.simpleNodes().then((res) => {
|
||||
nodeTotal.value = res
|
||||
}).catch((err) => {
|
||||
alert(err)
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
const loadData = () => {
|
||||
loading.value = true;
|
||||
API.shared.slots().then((res) => {
|
||||
slotTotal.value = res
|
||||
}).catch((err) => {
|
||||
alert(err)
|
||||
}).finally(() => {
|
||||
loading.value = false;
|
||||
})
|
||||
}
|
||||
|
||||
const onSort = (by :string) => {
|
||||
orderBy.value = by
|
||||
sort()
|
||||
}
|
||||
|
||||
const sort = () => {
|
||||
if(orderBy.value === 'channel_count') {
|
||||
if(desc.value) {
|
||||
slotTotal.value.data.sort((a: any, b: any) => {
|
||||
return a.channel_count - b.channel_count
|
||||
})
|
||||
desc.value = false
|
||||
} else {
|
||||
slotTotal.value.data.sort((a: any, b: any) => {
|
||||
return b.channel_count - a.channel_count
|
||||
})
|
||||
desc.value = true
|
||||
}
|
||||
} else if(orderBy.value === 'log_index') {
|
||||
if(desc.value) {
|
||||
slotTotal.value.data.sort((a: any, b: any) => {
|
||||
return a.log_index - b.log_index
|
||||
})
|
||||
desc.value = false
|
||||
} else {
|
||||
slotTotal.value.data.sort((a: any, b: any) => {
|
||||
return b.log_index - a.log_index
|
||||
})
|
||||
desc.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const onLog = (slot: any) => {
|
||||
|
||||
router.push({
|
||||
path: '/cluster/log',
|
||||
query: {
|
||||
slot: slot.id,
|
||||
node_id: slot.leader_id,
|
||||
log_type: 2,
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
const onShowMigrateModal = (slot: any) => {
|
||||
|
||||
if(!App.shard().loginInfo.hasPermission('slotMigrate',ActionWrite)) {
|
||||
alertNoPermission()
|
||||
return
|
||||
}
|
||||
|
||||
selectedSlot.value = slot
|
||||
const migrateModal = document.getElementById('migrateModal') as HTMLDialogElement
|
||||
migrateModal.showModal()
|
||||
}
|
||||
|
||||
const onMigrate = () => {
|
||||
|
||||
|
||||
|
||||
const migrateModal = document.getElementById('migrateModal') as HTMLDialogElement;
|
||||
migrateModal.close();
|
||||
|
||||
API.shared.migrateSlot({
|
||||
slot: selectedSlot.value.id,
|
||||
migrateFrom: selectedMigrateFrom.value||0,
|
||||
migrateTo: selectedMigrateTo.value||0
|
||||
}).then(() => {
|
||||
loadData()
|
||||
}).catch((err) => {
|
||||
alert(err.msg)
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="overflow-x-auto h-5/6">
|
||||
<div class="flex">
|
||||
<div class="text-sm ml-3">
|
||||
<button class="btn" :onclick="() => { onSort('channel_count') }">按频道数量排序</button>
|
||||
</div>
|
||||
|
||||
<div class="text-sm ml-3">
|
||||
<button class="btn" :onclick="() => { onSort('log_index') }">日志高度</button>
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-pin-rows">
|
||||
<!-- head -->
|
||||
<thead>
|
||||
<tr>
|
||||
<th>分区(槽)</th>
|
||||
<th>领导节点</th>
|
||||
<th>任期</th>
|
||||
<th>副本节点</th>
|
||||
<th>频道数量</th>
|
||||
<th>日志高度</th>
|
||||
<th>状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- row 1 -->
|
||||
<tr v-for="slot in slotTotal.data">
|
||||
<td>{{ slot.id }}</td>
|
||||
<td>{{ slot.leader_id }}</td>
|
||||
<td>{{ slot.term }}</td>
|
||||
<td>{{ slot.replicas }}</td>
|
||||
<td>{{ slot.channel_count }}</td>
|
||||
<td>{{slot.log_index}}</td>
|
||||
<td :class="slot.status===0?'text-green-500':'text-red-500'">{{slot.status_format}}</td>
|
||||
<td class="flex flex-wrap gap-2">
|
||||
<button class="btn btn-sm btn-primary" v-on:click="()=>onShowMigrateModal(slot)">迁移</button>
|
||||
<button class="btn btn-sm btn-primary" v-on:click="()=>onLog(slot)">日志</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="flex flex-col gap-4 w-full mt-2" v-if="loading">
|
||||
<div class="skeleton h-6 w-full"></div>
|
||||
<div class="skeleton h-6 w-full"></div>
|
||||
<div class="skeleton h-6 w-full"></div>
|
||||
<div class="skeleton h-6 w-full"></div>
|
||||
<div class="skeleton h-6 w-full"></div>
|
||||
<div class="skeleton h-6 w-full"></div>
|
||||
<div class="skeleton h-6 w-full"></div>
|
||||
<div class="skeleton h-6 w-full"></div>
|
||||
<div class="skeleton h-6 w-full"></div>
|
||||
<div class="skeleton h-6 w-full"></div>
|
||||
<div class="skeleton h-6 w-full"></div>
|
||||
<div class="skeleton h-6 w-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<dialog id="migrateModal" class="modal">
|
||||
<div class="modal-box flex flex-wrap gap-2 justify-center">
|
||||
<select class="select select-bordered" v-model="selectedMigrateFrom">
|
||||
<option v-for="nodeId in selectedSlot.replicas" :value="nodeId">{{ nodeId }}</option>
|
||||
</select>
|
||||
<div class="flex items-center"> 迁移至 </div>
|
||||
<select class="select select-bordered" v-model="selectedMigrateTo">
|
||||
<option v-for="destNode in nodeTotal.data" :value="destNode.id">{{ destNode.id }}</option>
|
||||
</select>
|
||||
|
||||
<button className="btn-primary btn" v-on:click="onMigrate">确认</button>
|
||||
<!-- <a class="link" v-for="uid in currentUids">{{ uid }}</a> -->
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,69 +0,0 @@
|
||||
<script name="ClusterConfig" lang="ts" setup>
|
||||
// API 接口
|
||||
import { clusterApi } from '@/api/modules/cluster-api';
|
||||
import VueJsonPretty from 'vue-json-pretty';
|
||||
import 'vue-json-pretty/lib/styles.css';
|
||||
|
||||
interface IProps {
|
||||
channelId: string;
|
||||
channelType: number;
|
||||
nodeId: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Props
|
||||
*/
|
||||
const props = withDefaults(defineProps<IProps>(), {
|
||||
channelId: '',
|
||||
channelType: 0,
|
||||
nodeId: 0
|
||||
});
|
||||
|
||||
const showModel = defineModel<boolean>();
|
||||
const loadingModel = ref(false);
|
||||
|
||||
watch(
|
||||
() => showModel.value,
|
||||
val => {
|
||||
if (val) {
|
||||
if (props.channelId) {
|
||||
getConfig();
|
||||
} else {
|
||||
config.value = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const config = ref<any>({});
|
||||
|
||||
const getConfig = async () => {
|
||||
const res = await clusterApi.channelClusterConfig({ ...props });
|
||||
config.value = res || {};
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<vxe-modal
|
||||
v-model="showModel"
|
||||
title="配置"
|
||||
:width="560"
|
||||
:height="460"
|
||||
:confirm-closable="false"
|
||||
:padding="false"
|
||||
:loading="loadingModel"
|
||||
show-maximize
|
||||
>
|
||||
<wk-page class="flex-col wk-page-bg">
|
||||
<div class="flex-1 card pt-0px overflow-hidden">
|
||||
<VueJsonPretty :data="config" />
|
||||
</div>
|
||||
</wk-page>
|
||||
</vxe-modal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.wk-page-bg {
|
||||
background-color: var(--el-bg-color-page);
|
||||
}
|
||||
</style>
|
||||
@@ -1,185 +0,0 @@
|
||||
<script name="Migrate" lang="ts" setup>
|
||||
// API 接口
|
||||
import { clusterApi } from '@/api/modules/cluster-api.ts';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import type { VxeFormProps } from 'vxe-pc-ui';
|
||||
|
||||
interface IProps {
|
||||
channelId: string;
|
||||
channelType: number;
|
||||
replicas: number[];
|
||||
}
|
||||
|
||||
interface IReplicasItem {
|
||||
value: number;
|
||||
label: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Props
|
||||
*/
|
||||
const props = withDefaults(defineProps<IProps>(), {
|
||||
channelId: '',
|
||||
channelType: 0,
|
||||
replicas: []
|
||||
});
|
||||
|
||||
const showModel = defineModel<boolean>();
|
||||
const emits = defineEmits(['submit']);
|
||||
const loadingModel = ref(false);
|
||||
const replicasList = ref<IReplicasItem[]>([]);
|
||||
|
||||
watch(
|
||||
() => showModel.value,
|
||||
val => {
|
||||
if (val) {
|
||||
if (props.channelId) {
|
||||
getNodes();
|
||||
|
||||
const getReplicas: IReplicasItem[] = [];
|
||||
props.replicas.map(item => {
|
||||
getReplicas.push({
|
||||
value: item,
|
||||
label: item
|
||||
});
|
||||
});
|
||||
replicasList.value = getReplicas;
|
||||
|
||||
formOptions.data = {
|
||||
...formOptions.data,
|
||||
channelId: props.channelId,
|
||||
channelType: props.channelType
|
||||
} as FormDataVO;
|
||||
}
|
||||
} else {
|
||||
formOptions.data = {
|
||||
channelId: '',
|
||||
channelType: 0,
|
||||
migrateFrom: null,
|
||||
migrateTo: null
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const nodeList = ref();
|
||||
const getNodes = async () => {
|
||||
const nodes: any[] = [];
|
||||
const res = await clusterApi.simpleNodes();
|
||||
if (res.data.length > 0) {
|
||||
res.data.map((item: any) => {
|
||||
nodes.push({
|
||||
value: item.id,
|
||||
label: item.id
|
||||
});
|
||||
});
|
||||
}
|
||||
nodeList.value = nodes;
|
||||
};
|
||||
|
||||
/**
|
||||
* 表单
|
||||
*/
|
||||
|
||||
interface FormDataVO {
|
||||
channelId: string;
|
||||
channelType: number;
|
||||
migrateFrom: number | null;
|
||||
migrateTo: number | null;
|
||||
}
|
||||
|
||||
const formRef = ref();
|
||||
const formOptions = reactive<VxeFormProps<FormDataVO>>({
|
||||
loading: false,
|
||||
data: {
|
||||
channelId: '',
|
||||
channelType: 0,
|
||||
migrateFrom: null,
|
||||
migrateTo: null
|
||||
},
|
||||
rules: {
|
||||
migrateFrom: [{ required: true, message: '请选择副本节点' }],
|
||||
migrateTo: [{ required: true, message: '请选择迁移至节点' }]
|
||||
},
|
||||
titleBold: true,
|
||||
titleWidth: 100,
|
||||
titleColon: true,
|
||||
titleOverflow: true,
|
||||
items: [
|
||||
{
|
||||
field: 'migrateFrom',
|
||||
title: '副本节点',
|
||||
span: 24,
|
||||
itemRender: {
|
||||
name: 'ElSelect',
|
||||
props: {
|
||||
placeholder: '请选择副本节点'
|
||||
},
|
||||
options: replicasList
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'migrateTo',
|
||||
title: '迁移至节点',
|
||||
span: 24,
|
||||
itemRender: {
|
||||
name: 'ElSelect',
|
||||
props: {
|
||||
placeholder: '请选择迁移至节点'
|
||||
},
|
||||
options: nodeList
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// 提交功能
|
||||
const onFuncConfirm = async () => {
|
||||
const validate = await formRef.value.validate();
|
||||
if (validate) return;
|
||||
clusterApi.migrateChannel(formOptions.data as any).then(() => {
|
||||
ElMessage({
|
||||
message: '操作成功!',
|
||||
type: 'success',
|
||||
plain: true
|
||||
});
|
||||
emits('submit')
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getNodes();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<vxe-modal
|
||||
v-model="showModel"
|
||||
title="迁移"
|
||||
:width="460"
|
||||
:height="360"
|
||||
:confirm-closable="false"
|
||||
:padding="false"
|
||||
:loading="loadingModel"
|
||||
show-maximize
|
||||
show-footer
|
||||
show-confirm-button
|
||||
show-cancel-button
|
||||
@confirm="onFuncConfirm"
|
||||
>
|
||||
<wk-page class="flex-col wk-page-bg">
|
||||
<!-- S 表单 -->
|
||||
<div class="flex-1 card overflow-hidden">
|
||||
<vxe-form ref="formRef" v-bind="formOptions"></vxe-form>
|
||||
</div>
|
||||
<!-- E 表单 -->
|
||||
</wk-page>
|
||||
</vxe-modal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.wk-page-bg {
|
||||
background-color: var(--el-bg-color-page);
|
||||
}
|
||||
</style>
|
||||
@@ -1,105 +0,0 @@
|
||||
<script name="Replicas" lang="ts" setup>
|
||||
// API 接口
|
||||
import { clusterApi } from '@/api/modules/cluster-api.ts';
|
||||
|
||||
import type { VxeGridInstance, VxeGridProps } from 'vxe-table';
|
||||
|
||||
interface IProps {
|
||||
channelId: string;
|
||||
channelType: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Props
|
||||
*/
|
||||
const props = withDefaults(defineProps<IProps>(), {
|
||||
channelId: '',
|
||||
channelType: 0
|
||||
});
|
||||
|
||||
const showModel = defineModel<boolean>();
|
||||
const loadingModel = ref(false);
|
||||
|
||||
watch(
|
||||
() => showModel.value,
|
||||
val => {
|
||||
if (val) {
|
||||
if (props.channelId) {
|
||||
tableRef.value?.commitProxy('query');
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 表格
|
||||
*/
|
||||
interface ITableItem {
|
||||
uid: string;
|
||||
}
|
||||
|
||||
const tableRef = ref<VxeGridInstance<ITableItem>>();
|
||||
|
||||
const apiLoadList = async () => {
|
||||
const res = await clusterApi.channelReplicas({ channelId: props.channelId, channelType: props.channelType });
|
||||
return res || [];
|
||||
};
|
||||
|
||||
const gridOptions = reactive<VxeGridProps<ITableItem>>({
|
||||
border: true,
|
||||
showOverflow: true,
|
||||
height: 'auto',
|
||||
stripe: true,
|
||||
rowConfig: { isCurrent: true, isHover: true },
|
||||
scrollY: { enabled: true, gt: 0 },
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: () => {
|
||||
return apiLoadList();
|
||||
}
|
||||
}
|
||||
},
|
||||
columns: [
|
||||
{ type: 'seq', title: '序号', width: 64 },
|
||||
{ field: 'replica_id', title: '节点ID', minWidth: 120 },
|
||||
{ field: 'role_format', title: '节点角色', minWidth: 120 },
|
||||
{ field: 'last_msg_seq', title: '消息高度', minWidth: 120 },
|
||||
{ field: 'last_msg_time_format', title: '最后消息时间', minWidth: 120 },
|
||||
{
|
||||
field: 'running',
|
||||
title: '是否运行',
|
||||
width: 120,
|
||||
formatter({ cellValue }) {
|
||||
return cellValue === 1 ? '运行中' : '未运行';
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<vxe-modal
|
||||
v-model="showModel"
|
||||
title="副本"
|
||||
:width="860"
|
||||
:height="560"
|
||||
:confirm-closable="false"
|
||||
:padding="false"
|
||||
:loading="loadingModel"
|
||||
show-maximize
|
||||
>
|
||||
<wk-page class="flex-col wk-page-bg">
|
||||
<!-- S 表格 -->
|
||||
<div class="flex-1 card pt-0px overflow-hidden">
|
||||
<vxe-grid ref="tableRef" v-bind="gridOptions"></vxe-grid>
|
||||
</div>
|
||||
<!-- E 表格 -->
|
||||
</wk-page>
|
||||
</vxe-modal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.wk-page-bg {
|
||||
background-color: var(--el-bg-color-page);
|
||||
}
|
||||
</style>
|
||||
@@ -1,408 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
// API 接口
|
||||
import { clusterApi } from '@/api/modules/cluster-api';
|
||||
|
||||
// 常量
|
||||
import { ActionWrite, CHANNEL_TYPE } from '@/constants';
|
||||
|
||||
import { VxeFormListeners, VxeFormProps } from 'vxe-pc-ui';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import Migrate from './components/Migrate.vue';
|
||||
import Replicas from './components/Replicas.vue';
|
||||
import ClusterConfig from './components/ClusterConfig.vue';
|
||||
|
||||
import type { VxeGridInstance, VxeGridProps } from 'vxe-table';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useUserStore } from '@/stores/modules/user.ts';
|
||||
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
/**
|
||||
* 查询条件
|
||||
* */
|
||||
const nodeList = ref<any[]>([]);
|
||||
const formOptions = reactive<VxeFormProps<any>>({
|
||||
data: {
|
||||
nodeId: null,
|
||||
channel_type: null,
|
||||
channel_id: null,
|
||||
offset_created_at: null,
|
||||
limit: 20,
|
||||
pre: 0
|
||||
},
|
||||
items: [
|
||||
{
|
||||
field: 'nodeId',
|
||||
title: '节点',
|
||||
itemRender: {
|
||||
name: 'ElSelect',
|
||||
props: {
|
||||
placeholder: '请选择',
|
||||
style: {
|
||||
width: '180px'
|
||||
}
|
||||
},
|
||||
options: nodeList
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'channel_type',
|
||||
title: '频道类型',
|
||||
itemRender: {
|
||||
name: 'ElSelect',
|
||||
props: {
|
||||
placeholder: '请选择频道类型',
|
||||
style: { width: '180px' }
|
||||
},
|
||||
options: CHANNEL_TYPE
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'channel_id',
|
||||
title: '频道ID',
|
||||
itemRender: { name: 'ElInput', props: { placeholder: '请输入频道ID' } }
|
||||
},
|
||||
{
|
||||
align: 'center',
|
||||
slots: { default: 'action' }
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const getNodes = async () => {
|
||||
const nodes: any[] = [];
|
||||
const res = await clusterApi.simpleNodes();
|
||||
if (res.data.length > 0) {
|
||||
res.data.map((item: any) => {
|
||||
nodes.push({
|
||||
value: item.id,
|
||||
label: item.id
|
||||
});
|
||||
});
|
||||
}
|
||||
nodeList.value = nodes;
|
||||
if (nodes.length > 0) {
|
||||
formOptions.data = {
|
||||
...formOptions.data,
|
||||
nodeId: nodes[0].value
|
||||
};
|
||||
tableRef.value?.commitProxy('query');
|
||||
}
|
||||
};
|
||||
/** 搜索事件 **/
|
||||
const formEvents: VxeFormListeners<any> = {
|
||||
/** 查询 **/
|
||||
submit() {
|
||||
tableRef.value?.commitProxy('query');
|
||||
},
|
||||
/** 重置 **/
|
||||
reset() {
|
||||
formOptions.data = {
|
||||
...formOptions.data
|
||||
};
|
||||
if (nodeList.value.length > 0) {
|
||||
formOptions.data = {
|
||||
...formOptions.data,
|
||||
nodeId: nodeList.value[0].value
|
||||
};
|
||||
}
|
||||
|
||||
tableRef.value?.commitProxy('query');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 表格
|
||||
**/
|
||||
const tableRef = ref<VxeGridInstance<any>>();
|
||||
const currentPage = ref(1); // 当前页
|
||||
const hasPrev = ref<boolean>(false); // 是否有上一页
|
||||
const hasNext = ref<boolean>(true); // 是否有下一页
|
||||
|
||||
const loadList = async (query: any) => {
|
||||
const res = await clusterApi.nodeChannelConfigs({ ...query });
|
||||
if (res.data) {
|
||||
hasNext.value = res.more !== 1;
|
||||
hasPrev.value = currentPage.value <= 1;
|
||||
return res.data;
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const gridOptions = reactive<VxeGridProps<any>>({
|
||||
showOverflow: true,
|
||||
height: 'auto',
|
||||
border: true,
|
||||
stripe: true,
|
||||
rowConfig: {
|
||||
isCurrent: true,
|
||||
isHover: true
|
||||
},
|
||||
scrollY: {
|
||||
enabled: true,
|
||||
gt: 0
|
||||
},
|
||||
toolbarConfig: {
|
||||
slots: {
|
||||
buttons: 'tools'
|
||||
},
|
||||
refresh: {
|
||||
icon: 'vxe-icon-refresh',
|
||||
iconLoading: 'vxe-icon-refresh roll'
|
||||
},
|
||||
zoom: {
|
||||
iconIn: 'vxe-icon-fullscreen',
|
||||
iconOut: 'vxe-icon-minimize'
|
||||
},
|
||||
custom: true
|
||||
},
|
||||
proxyConfig: {
|
||||
autoLoad: false,
|
||||
ajax: {
|
||||
query: () => {
|
||||
return loadList(formOptions.data);
|
||||
}
|
||||
}
|
||||
},
|
||||
columns: [
|
||||
{ field: 'channel_id', title: '频道ID', minWidth: 220, fixed: 'left' },
|
||||
{ field: 'channel_type_format', title: '频道类型', minWidth: 120 },
|
||||
{ field: 'slot_id', title: '所属槽', minWidth: 120 },
|
||||
{ field: 'slot_leader_id', title: '槽领导', minWidth: 120 },
|
||||
{ field: 'leader_id', title: '频道领导', minWidth: 120 },
|
||||
{ field: 'term', title: '任期', minWidth: 120 },
|
||||
{ field: 'replicas', title: '副本节点', minWidth: 160 },
|
||||
{ field: 'last_message_seq', title: '消息高度', minWidth: 120 },
|
||||
{ field: 'last_append_time', title: '最后消息时间', minWidth: 120 },
|
||||
{ field: 'active_format', title: '是否运行', minWidth: 100 },
|
||||
{ field: 'status_format', title: '状态', minWidth: 100 },
|
||||
{ field: 'action', title: '操作', width: 224, fixed: 'right', slots: { default: 'action' } }
|
||||
]
|
||||
});
|
||||
|
||||
/**
|
||||
* 分页切换
|
||||
*/
|
||||
const onPage = (type: 0 | 1) => {
|
||||
// 下一页
|
||||
if (type === 0) {
|
||||
currentPage.value = currentPage.value + 1;
|
||||
}
|
||||
|
||||
// 上一页
|
||||
if (type === 1 && currentPage.value > 1) {
|
||||
currentPage.value = currentPage.value - 1;
|
||||
}
|
||||
|
||||
formOptions.data = {
|
||||
...formOptions.data,
|
||||
pre: type
|
||||
};
|
||||
|
||||
tableRef.value?.commitProxy('query');
|
||||
};
|
||||
|
||||
/**
|
||||
* 操作开始或者停止
|
||||
*/
|
||||
const onStartOrStop = (row: any) => {
|
||||
if (!userStore.hasPermission('channelStart', ActionWrite)) {
|
||||
return ElMessage({
|
||||
message: '没有操作权限!',
|
||||
type: 'warning',
|
||||
plain: true
|
||||
});
|
||||
}
|
||||
|
||||
ElMessageBox({
|
||||
title: '系统提示',
|
||||
message: `是否确认${row.active === 1 ? '停止' : '开始'},频道ID为【${row.channel_id}】的数据项?`,
|
||||
type: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
beforeClose: (action, instance, done) => {
|
||||
if (action === 'confirm') {
|
||||
instance.confirmButtonLoading = true;
|
||||
// 停止
|
||||
if (row.active === 1) {
|
||||
clusterApi
|
||||
.channelStop({
|
||||
channelId: row.channel_id,
|
||||
channelType: row.channel_type,
|
||||
nodeId: row.leader_id
|
||||
})
|
||||
.then(() => {
|
||||
done();
|
||||
})
|
||||
.catch(() => {
|
||||
instance.confirmButtonLoading = false;
|
||||
});
|
||||
}
|
||||
// 开始
|
||||
if (row.active === 0) {
|
||||
clusterApi
|
||||
.channelStart({
|
||||
channelId: row.channel_id,
|
||||
channelType: row.channel_type,
|
||||
nodeId: row.leader_id
|
||||
})
|
||||
.then(() => {
|
||||
done();
|
||||
})
|
||||
.catch(() => {
|
||||
instance.confirmButtonLoading = false;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage({
|
||||
message: '操作成功!',
|
||||
type: 'success',
|
||||
plain: true
|
||||
});
|
||||
tableRef.value?.commitProxy('query');
|
||||
})
|
||||
.catch(() => {
|
||||
console.log('点击取消');
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 迁移
|
||||
*/
|
||||
const modelMigrate = ref(false);
|
||||
const channelIdMigrate = ref('');
|
||||
const channelTypeMigrate = ref(0);
|
||||
const replicasMigrate = ref<number[]>([]);
|
||||
const onMigrate = (row: any) => {
|
||||
if (!userStore.hasPermission('channelMigrate', ActionWrite)) {
|
||||
return ElMessage({
|
||||
message: '没有操作权限!',
|
||||
type: 'warning',
|
||||
plain: true
|
||||
});
|
||||
}
|
||||
channelIdMigrate.value = row.channel_id;
|
||||
channelTypeMigrate.value = row.channel_type;
|
||||
replicasMigrate.value = row.replicas;
|
||||
modelMigrate.value = true;
|
||||
};
|
||||
|
||||
const onSubmit = () => {
|
||||
tableRef.value?.commitProxy('query');
|
||||
};
|
||||
|
||||
/**
|
||||
* 配置
|
||||
*/
|
||||
const modelClusterConfig = ref(false);
|
||||
const channelIdClusterConfig = ref('');
|
||||
const channelTypeClusterConfig = ref(0);
|
||||
const nodeIdClusterConfig = ref(0);
|
||||
const onClusterConfig = (row: any) => {
|
||||
channelIdClusterConfig.value = row.channel_id;
|
||||
channelTypeClusterConfig.value = row.channel_type;
|
||||
nodeIdClusterConfig.value = row.leader_id;
|
||||
modelClusterConfig.value = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* 副本
|
||||
*/
|
||||
const modelReplicas = ref(false);
|
||||
const channelIdReplicas = ref('');
|
||||
const channelTypeReplicas = ref(0);
|
||||
|
||||
const onReplicas = (row: any) => {
|
||||
channelIdReplicas.value = row.channel_id;
|
||||
channelTypeReplicas.value = row.channel_type;
|
||||
modelReplicas.value = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* 消息
|
||||
* @param row
|
||||
*/
|
||||
const onMessage = (row: any) => {
|
||||
router.push({
|
||||
path: '/data/message',
|
||||
query: { channel_id: row.channel_id, channel_type: row.channel_type }
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getNodes();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<wk-page class="flex-col">
|
||||
<!-- S 查询条件 -->
|
||||
<div class="mb-12px pt-4px pb-4px card">
|
||||
<vxe-form v-bind="formOptions" v-on="formEvents">
|
||||
<template #action>
|
||||
<el-button native-type="submit" type="primary">查询</el-button>
|
||||
<el-button native-type="reset">重置</el-button>
|
||||
</template>
|
||||
</vxe-form>
|
||||
</div>
|
||||
<!-- E 查询条件 -->
|
||||
|
||||
<!-- S 表格 -->
|
||||
<div class="flex-1 card !pt-4px overflow-hidden">
|
||||
<vxe-grid ref="tableRef" v-bind="gridOptions">
|
||||
<template #tools>
|
||||
<el-space>
|
||||
<el-button type="primary" :disabled="hasPrev" @click="onPage(1)">上一页</el-button>
|
||||
<el-button type="primary" :disabled="hasNext" @click="onPage(0)">下一页</el-button>
|
||||
</el-space>
|
||||
</template>
|
||||
|
||||
<template #action="{ row }">
|
||||
<el-space>
|
||||
<el-button :type="row.active === 1 ? 'danger' : 'primary'" link @click="onStartOrStop(row)">
|
||||
{{ row.active === 1 ? '停止' : '开始' }}
|
||||
</el-button>
|
||||
<el-button type="primary" link @click="onMigrate(row)">迁移</el-button>
|
||||
<el-button type="primary" link @click="onClusterConfig(row)">配置</el-button>
|
||||
<el-button type="primary" link @click="onReplicas(row)">副本</el-button>
|
||||
<el-button type="primary" link @click="onMessage(row)">消息</el-button>
|
||||
</el-space>
|
||||
</template>
|
||||
</vxe-grid>
|
||||
</div>
|
||||
<!-- E 表格 -->
|
||||
|
||||
<!-- 迁移 -->
|
||||
<Migrate
|
||||
v-model="modelMigrate"
|
||||
:channel-id="channelIdMigrate"
|
||||
:channel-type="channelTypeMigrate"
|
||||
:replicas="replicasMigrate"
|
||||
@submit="onSubmit"
|
||||
/>
|
||||
|
||||
<!-- 配置 -->
|
||||
<ClusterConfig
|
||||
v-model="modelClusterConfig"
|
||||
:channel-id="channelIdClusterConfig"
|
||||
:channel-type="channelTypeClusterConfig"
|
||||
:node-id="nodeIdClusterConfig"
|
||||
/>
|
||||
|
||||
<!-- 副本 -->
|
||||
<Replicas v-model="modelReplicas" :channel-id="channelIdReplicas" :channel-type="channelTypeReplicas" />
|
||||
</wk-page>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
title: 频道
|
||||
</route>
|
||||
@@ -1,152 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
// API 接口
|
||||
import { clusterApi } from '@/api/modules/cluster-api';
|
||||
|
||||
import type { VxeGridInstance, VxeGridProps } from 'vxe-table';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
/**
|
||||
* 表格
|
||||
*/
|
||||
const tableRef = ref<VxeGridInstance<any>>();
|
||||
const toolNum = ref(0);
|
||||
const loadList = async () => {
|
||||
const res = await clusterApi.nodes();
|
||||
toolNum.value = res.total;
|
||||
return res.data;
|
||||
};
|
||||
|
||||
const gridOptions = reactive<VxeGridProps<any>>({
|
||||
showOverflow: true,
|
||||
height: 'auto',
|
||||
border: true,
|
||||
stripe: true,
|
||||
rowConfig: {
|
||||
isCurrent: true,
|
||||
isHover: true
|
||||
},
|
||||
scrollY: {
|
||||
enabled: true,
|
||||
gt: 0
|
||||
},
|
||||
toolbarConfig: {
|
||||
slots: {
|
||||
buttons: 'tools'
|
||||
},
|
||||
refresh: {
|
||||
icon: 'vxe-icon-refresh',
|
||||
iconLoading: 'vxe-icon-refresh roll'
|
||||
},
|
||||
zoom: {
|
||||
iconIn: 'vxe-icon-fullscreen',
|
||||
iconOut: 'vxe-icon-minimize'
|
||||
},
|
||||
custom: true
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: () => {
|
||||
return loadList();
|
||||
}
|
||||
}
|
||||
},
|
||||
columns: [
|
||||
{ type: 'seq', title: '序号', width: 54, fixed: 'left' },
|
||||
{ field: 'id', title: '节点ID', minWidth: 80, fixed: 'left' },
|
||||
{
|
||||
field: 'role',
|
||||
title: '角色',
|
||||
minWidth: 120,
|
||||
formatter({ row }) {
|
||||
if (row.is_leader == 1) {
|
||||
return '领导';
|
||||
}
|
||||
if (row.role == 1) {
|
||||
return '代理';
|
||||
}
|
||||
return '副本';
|
||||
}
|
||||
},
|
||||
{ field: 'term', title: '任期', minWidth: 100 },
|
||||
{
|
||||
field: 'slot_leader_count',
|
||||
title: '槽领导/槽数量',
|
||||
minWidth: 120,
|
||||
formatter({ row }) {
|
||||
return `${row.slot_leader_count}/${row.slot_count}`;
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'allow_vote',
|
||||
title: '投票权',
|
||||
minWidth: 100,
|
||||
formatter({ cellValue }) {
|
||||
return cellValue === 1 ? '有' : '无';
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'online',
|
||||
title: '在线',
|
||||
minWidth: 100,
|
||||
formatter({ cellValue }) {
|
||||
return cellValue === 1 ? '在线' : '离线';
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'offline_count',
|
||||
title: '离线次数',
|
||||
minWidth: 100,
|
||||
formatter({ row }) {
|
||||
return row.offline_count > 0 ? `${row.offline_count}(${row.last_offline})` : `0`;
|
||||
}
|
||||
},
|
||||
{ field: 'uptime', title: '运行时间', minWidth: 120 },
|
||||
{ field: 'cluster_addr', title: '地址', minWidth: 160 },
|
||||
{ field: 'app_version', title: '程序版本', minWidth: 120 },
|
||||
{ field: 'config_version', title: '配置版本', minWidth: 120 },
|
||||
{ field: 'status_format', title: '状态', minWidth: 100 },
|
||||
{ field: 'action', title: '操作', width: 60, fixed: 'right', slots: { default: 'action' } }
|
||||
]
|
||||
});
|
||||
|
||||
/**
|
||||
* 日志
|
||||
* @param row
|
||||
*/
|
||||
const onLog = (row: any) => {
|
||||
router.push({
|
||||
path: '/cluster/log',
|
||||
query: {
|
||||
node_id: row.id,
|
||||
}
|
||||
})
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<wk-page class="flex-col">
|
||||
<!-- S 表格 -->
|
||||
<div class="flex-1 card !pt-4px overflow-hidden">
|
||||
<vxe-grid ref="tableRef" v-bind="gridOptions">
|
||||
<template #tools>
|
||||
<el-text type="primary" tag="b">共计节点总数:{{ toolNum }}</el-text>
|
||||
</template>
|
||||
|
||||
<template #action="{ row }">
|
||||
<el-space>
|
||||
<el-button type="primary" link @click="onLog(row)">日志</el-button>
|
||||
</el-space>
|
||||
</template>
|
||||
</vxe-grid>
|
||||
</div>
|
||||
<!-- E 表格 -->
|
||||
</wk-page>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
title: 节点
|
||||
</route>
|
||||
@@ -1,179 +0,0 @@
|
||||
<script name="MigrateSlot" lang="ts" setup>
|
||||
// API 接口
|
||||
import { clusterApi } from '@/api/modules/cluster-api.ts';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import type { VxeFormProps } from 'vxe-pc-ui';
|
||||
|
||||
interface IProps {
|
||||
slot: number;
|
||||
replicas: number[];
|
||||
}
|
||||
|
||||
interface IReplicasItem {
|
||||
value: number;
|
||||
label: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Props
|
||||
*/
|
||||
const props = withDefaults(defineProps<IProps>(), {
|
||||
slot: 0,
|
||||
replicas: []
|
||||
});
|
||||
|
||||
const showModel = defineModel<boolean>();
|
||||
const emits = defineEmits(['submit']);
|
||||
const loadingModel = ref(false);
|
||||
const replicasList = ref<IReplicasItem[]>([]);
|
||||
|
||||
watch(
|
||||
() => showModel.value,
|
||||
val => {
|
||||
if (val) {
|
||||
if (props.slot) {
|
||||
getNodes();
|
||||
|
||||
const getReplicas: IReplicasItem[] = [];
|
||||
props.replicas.map(item => {
|
||||
getReplicas.push({
|
||||
value: item,
|
||||
label: item
|
||||
});
|
||||
});
|
||||
replicasList.value = getReplicas;
|
||||
|
||||
formOptions.data = {
|
||||
...formOptions.data,
|
||||
slot: props.slot
|
||||
} as FormDataVO;
|
||||
}
|
||||
} else {
|
||||
formOptions.data = {
|
||||
slot: 0,
|
||||
migrateFrom: null,
|
||||
migrateTo: null
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const nodeList = ref();
|
||||
const getNodes = async () => {
|
||||
const nodes: any[] = [];
|
||||
const res = await clusterApi.simpleNodes();
|
||||
if (res.data.length > 0) {
|
||||
res.data.map((item: any) => {
|
||||
nodes.push({
|
||||
value: item.id,
|
||||
label: item.id
|
||||
});
|
||||
});
|
||||
}
|
||||
nodeList.value = nodes;
|
||||
};
|
||||
|
||||
/**
|
||||
* 表单
|
||||
*/
|
||||
|
||||
interface FormDataVO {
|
||||
slot: number;
|
||||
migrateFrom: number | null;
|
||||
migrateTo: number | null;
|
||||
}
|
||||
|
||||
const formRef = ref();
|
||||
const formOptions = reactive<VxeFormProps<FormDataVO>>({
|
||||
loading: false,
|
||||
data: {
|
||||
slot: 0,
|
||||
migrateFrom: null,
|
||||
migrateTo: null
|
||||
},
|
||||
rules: {
|
||||
migrateFrom: [{ required: true, message: '请选择副本节点' }],
|
||||
migrateTo: [{ required: true, message: '请选择迁移至节点' }]
|
||||
},
|
||||
titleBold: true,
|
||||
titleWidth: 100,
|
||||
titleColon: true,
|
||||
titleOverflow: true,
|
||||
items: [
|
||||
{
|
||||
field: 'migrateFrom',
|
||||
title: '副本节点',
|
||||
span: 24,
|
||||
itemRender: {
|
||||
name: 'ElSelect',
|
||||
props: {
|
||||
placeholder: '请选择副本节点'
|
||||
},
|
||||
options: replicasList
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'migrateTo',
|
||||
title: '迁移至节点',
|
||||
span: 24,
|
||||
itemRender: {
|
||||
name: 'ElSelect',
|
||||
props: {
|
||||
placeholder: '请选择迁移至节点'
|
||||
},
|
||||
options: nodeList
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// 提交功能
|
||||
const onFuncConfirm = async () => {
|
||||
const validate = await formRef.value.validate();
|
||||
if (validate) return;
|
||||
clusterApi.migrateSlot(formOptions.data as any).then(() => {
|
||||
ElMessage({
|
||||
message: '操作成功!',
|
||||
type: 'success',
|
||||
plain: true
|
||||
});
|
||||
emits('submit');
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getNodes();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<vxe-modal
|
||||
v-model="showModel"
|
||||
title="迁移"
|
||||
:width="460"
|
||||
:height="360"
|
||||
:confirm-closable="false"
|
||||
:padding="false"
|
||||
:loading="loadingModel"
|
||||
show-maximize
|
||||
show-footer
|
||||
show-confirm-button
|
||||
show-cancel-button
|
||||
@confirm="onFuncConfirm"
|
||||
>
|
||||
<wk-page class="flex-col wk-page-bg">
|
||||
<!-- S 表单 -->
|
||||
<div class="flex-1 card overflow-hidden">
|
||||
<vxe-form ref="formRef" v-bind="formOptions"></vxe-form>
|
||||
</div>
|
||||
<!-- E 表单 -->
|
||||
</wk-page>
|
||||
</vxe-modal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.wk-page-bg {
|
||||
background-color: var(--el-bg-color-page);
|
||||
}
|
||||
</style>
|
||||
@@ -1,141 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
// API 接口
|
||||
import { clusterApi } from '@/api/modules/cluster-api';
|
||||
|
||||
import { ElMessage } from 'element-plus';
|
||||
import MigrateSlot from './components/MigrateSlot.vue';
|
||||
|
||||
import type { VxeGridInstance, VxeGridProps } from 'vxe-table';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useUserStore } from '@/stores/modules/user';
|
||||
import { ActionWrite } from '@/constants';
|
||||
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
/**
|
||||
* 表格
|
||||
*/
|
||||
const tableRef = ref<VxeGridInstance<any>>();
|
||||
const toolNum = ref(0);
|
||||
const loadList = async () => {
|
||||
const res = await clusterApi.slots();
|
||||
toolNum.value = res.total;
|
||||
return res.data;
|
||||
};
|
||||
|
||||
const gridOptions = reactive<VxeGridProps<any>>({
|
||||
showOverflow: true,
|
||||
height: 'auto',
|
||||
border: true,
|
||||
stripe: true,
|
||||
rowConfig: {
|
||||
isCurrent: true,
|
||||
isHover: true
|
||||
},
|
||||
scrollY: {
|
||||
enabled: true,
|
||||
gt: 0
|
||||
},
|
||||
toolbarConfig: {
|
||||
slots: {
|
||||
buttons: 'tools'
|
||||
},
|
||||
refresh: {
|
||||
icon: 'vxe-icon-refresh',
|
||||
iconLoading: 'vxe-icon-refresh roll'
|
||||
},
|
||||
zoom: {
|
||||
iconIn: 'vxe-icon-fullscreen',
|
||||
iconOut: 'vxe-icon-minimize'
|
||||
},
|
||||
custom: true
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: () => {
|
||||
return loadList();
|
||||
}
|
||||
}
|
||||
},
|
||||
columns: [
|
||||
{ type: 'seq', title: '序号', width: 54, fixed: 'left' },
|
||||
{ field: 'id', title: '分区(槽)', minWidth: 120, fixed: 'left' },
|
||||
{ field: 'leader_id', title: '领导节点', minWidth: 120 },
|
||||
{ field: 'term', title: '任期', minWidth: 160 },
|
||||
{ field: 'replicas', title: '副本节点', minWidth: 120 },
|
||||
{ field: 'channel_count', title: '频道数量', sortable: true, minWidth: 120 },
|
||||
{ field: 'log_index', title: '日志高度', sortable: true, minWidth: 120 },
|
||||
{ field: 'status_format', title: '状态', minWidth: 100 },
|
||||
{ field: 'action', title: '操作', width: 100, fixed: 'right', slots: { default: 'action' } }
|
||||
]
|
||||
});
|
||||
|
||||
/**
|
||||
* 迁移
|
||||
*/
|
||||
const modelMigrateSlot = ref(false);
|
||||
const slotMigrateSlot = ref(0);
|
||||
const replicasMigrateSlot = ref([]);
|
||||
const onMigrateSlot = (row: any) => {
|
||||
if (!userStore.hasPermission('slotMigrate', ActionWrite)) {
|
||||
return ElMessage({
|
||||
message: '没有操作权限!',
|
||||
type: 'warning',
|
||||
plain: true
|
||||
});
|
||||
}
|
||||
slotMigrateSlot.value = row.id;
|
||||
replicasMigrateSlot.value = row.replicas;
|
||||
modelMigrateSlot.value = true;
|
||||
};
|
||||
|
||||
const onSubmit = () => {
|
||||
tableRef.value?.commitProxy('query');
|
||||
};
|
||||
|
||||
/**
|
||||
* 日志
|
||||
* @param row
|
||||
*/
|
||||
const onLog = (row: any) => {
|
||||
router.push({
|
||||
path: '/cluster/log',
|
||||
query: {
|
||||
slot: row.id,
|
||||
node_id: row.leader_id,
|
||||
log_type: 2
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<wk-page class="flex-col">
|
||||
<!-- S 表格 -->
|
||||
<div class="flex-1 card !pt-4px overflow-hidden">
|
||||
<vxe-grid ref="tableRef" v-bind="gridOptions">
|
||||
<template #tools>
|
||||
<el-text type="primary" tag="b">共计节点总数:{{ toolNum }}</el-text>
|
||||
</template>
|
||||
|
||||
<template #action="{ row }">
|
||||
<el-space>
|
||||
<el-button type="primary" link @click="onMigrateSlot(row)">迁移</el-button>
|
||||
<el-button type="primary" link @click="onLog(row)">日志</el-button>
|
||||
</el-space>
|
||||
</template>
|
||||
</vxe-grid>
|
||||
</div>
|
||||
<!-- E 表格 -->
|
||||
|
||||
<!-- 迁移 -->
|
||||
<MigrateSlot v-model="modelMigrateSlot" :slot="slotMigrateSlot" :replicas="replicasMigrateSlot" @submit="onSubmit" />
|
||||
</wk-page>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
title: 分区(槽)
|
||||
</route>
|
||||
280
web/src/pages/data/Channel.vue
Normal file
@@ -0,0 +1,280 @@
|
||||
<script setup lang="ts">
|
||||
import API from '../../services/API';
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
const channelTotal = ref<any>({}); // 频道列表
|
||||
|
||||
const currentUids = ref<string[]>() // 当前用户列表
|
||||
const loadingOfSubscribers = ref<boolean>(false) // 是否正在加载订阅者
|
||||
const loadingOfDenylist = ref<boolean>(false) // 是否正在加载黑名单
|
||||
const loadingOfAllowlist = ref<boolean>(false) // 是否正在加载白名单
|
||||
const channelType = ref<number>(0) // 频道类型
|
||||
const channelId = ref<string>() // 频道ID
|
||||
const currentPage = ref(1); // 当前页
|
||||
const pageSize = ref(20); // 每页显示数量
|
||||
const offsetCreatedAt = ref("0"); // 偏移量
|
||||
const pre = ref<boolean>() // 是否向上分页
|
||||
const hasNext = ref<boolean>(true) // 是否有下一页
|
||||
const hasPrev = ref<boolean>(false) // 是否有上一页
|
||||
|
||||
onMounted(() => {
|
||||
searchChannels()
|
||||
})
|
||||
|
||||
const searchChannels = () => {
|
||||
API.shared.searchChannels({
|
||||
channelId: channelId.value,
|
||||
channelType: channelType.value,
|
||||
limit: pageSize.value,
|
||||
offsetCreatedAt: offsetCreatedAt.value,
|
||||
pre: pre.value
|
||||
}).then((res) => {
|
||||
channelTotal.value = res
|
||||
hasNext.value = channelTotal.value?.more === 1
|
||||
hasPrev.value = currentPage.value > 1
|
||||
if(pre.value) { // 如果是向上翻页,则有下页数据
|
||||
hasNext.value = true
|
||||
}
|
||||
|
||||
|
||||
}).catch((err) => {
|
||||
alert(err)
|
||||
})
|
||||
}
|
||||
|
||||
const getSubscribers = (channelId: string, channelType: number) => {
|
||||
loadingOfSubscribers.value = true;
|
||||
return API.shared.subscribers(channelId, channelType).then((res) => {
|
||||
currentUids.value = res
|
||||
}).catch((err) => {
|
||||
alert(err)
|
||||
}).finally(() => {
|
||||
loadingOfSubscribers.value = false;
|
||||
})
|
||||
}
|
||||
|
||||
const getDenylist = (channelId: string, channelType: number) => {
|
||||
loadingOfDenylist.value = true;
|
||||
return API.shared.denylist(channelId, channelType).then((res) => {
|
||||
currentUids.value = res
|
||||
}).catch((err) => {
|
||||
alert(err)
|
||||
}).finally(() => {
|
||||
loadingOfDenylist.value = false;
|
||||
})
|
||||
}
|
||||
|
||||
const getAllowlist = (channelId: string, channelType: number) => {
|
||||
loadingOfAllowlist.value = true;
|
||||
return API.shared.allowlist(channelId, channelType).then((res) => {
|
||||
currentUids.value = res
|
||||
}).catch((err) => {
|
||||
alert(err)
|
||||
}).finally(() => {
|
||||
loadingOfAllowlist.value = false;
|
||||
})
|
||||
}
|
||||
|
||||
// 显示订阅者
|
||||
const onShowSubscriber = (channelId: string, channelType: number) => {
|
||||
getSubscribers(channelId, channelType).then(() => {
|
||||
const dialog = document.getElementById('userlist') as HTMLDialogElement;
|
||||
dialog.showModal();
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
const onShowDenylist = (channelId: string, channelType: number) => {
|
||||
getDenylist(channelId, channelType).then(() => {
|
||||
const dialog = document.getElementById('userlist') as HTMLDialogElement;
|
||||
dialog.showModal();
|
||||
})
|
||||
}
|
||||
|
||||
const onShowAllowlist = (channelId: string, channelType: number) => {
|
||||
getAllowlist(channelId, channelType).then(() => {
|
||||
const dialog = document.getElementById('userlist') as HTMLDialogElement;
|
||||
dialog.showModal();
|
||||
})
|
||||
}
|
||||
|
||||
const onChannelTypeSearch = (e: any) => {
|
||||
channelType.value = e.target.value
|
||||
searchChannels()
|
||||
}
|
||||
|
||||
const onChannelIdSearch = (e: any) => {
|
||||
channelId.value = e.target.value
|
||||
searchChannels()
|
||||
}
|
||||
|
||||
const prevPage = () => {
|
||||
if (currentPage.value <= 1) {
|
||||
hasPrev.value = false
|
||||
return
|
||||
}
|
||||
hasPrev.value = true
|
||||
currentPage.value -= 1
|
||||
pre.value = true
|
||||
if (channelTotal.value?.data?.length > 0) {
|
||||
offsetCreatedAt.value = channelTotal.value.data[0].created_at
|
||||
}
|
||||
searchChannels()
|
||||
}
|
||||
|
||||
const nextPage = () => {
|
||||
if (channelTotal.value?.more === 0 && !pre.value) {
|
||||
return
|
||||
}
|
||||
currentPage.value += 1
|
||||
pre.value = false
|
||||
if (channelTotal.value?.data?.length > 0) {
|
||||
offsetCreatedAt.value = channelTotal.value.data[channelTotal.value.data.length - 1].created_at
|
||||
}
|
||||
searchChannels()
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="overflow-x-auto h-5/6">
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<!-- 频道类型 -->
|
||||
<div class="text-sm ml-10">
|
||||
<label>频道</label>
|
||||
<select class="select select-bordered max-w-xs select-sm w-20 ml-2"
|
||||
v-on:change="onChannelTypeSearch" v-model="channelType">
|
||||
<option value="0">所有</option>
|
||||
<option value="1">个人</option>
|
||||
<option value="2">群聊</option>
|
||||
<option value="3">客服</option>
|
||||
<option value="4">社区</option>
|
||||
<option value="5">话题</option>
|
||||
<option value="6">资讯</option>
|
||||
<option value="7">数据</option>
|
||||
</select>
|
||||
<input type="text" placeholder="频道ID" class="input input-bordered select-sm ml-2"
|
||||
v-on:change="onChannelIdSearch" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<table class="table mt-10 table-pin-rows">
|
||||
<!-- head -->
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
频道ID
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
频道类型
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
订阅者数量
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
黑名单数量
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
白名单数量
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
状态
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
最大序号
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
最后消息时间
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
槽位
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
创建时间
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
更新时间
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
操作
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- row 1 -->
|
||||
<tr v-for="channel in channelTotal.data">
|
||||
<td>
|
||||
{{ channel.channel_id }}
|
||||
</td>
|
||||
<td>{{ channel.channel_type }}</td>
|
||||
<td>{{ channel.subscriber_count }}</td>
|
||||
<td>{{ channel.denylist_count }}</td>
|
||||
<td>{{ channel.allowlist_count }}</td>
|
||||
<td>{{ channel.status_format }}</td>
|
||||
<td>{{ channel.last_msg_seq }}</td>
|
||||
<td>{{ channel.last_msg_time_format }}</td>
|
||||
<td>{{ channel.slot }}</td>
|
||||
<td>{{ channel.created_at_format }}</td>
|
||||
<td>{{ channel.updated_at_format }}</td>
|
||||
<td class="flex">
|
||||
<button class="btn btn-link btn-sm"
|
||||
v-on:click="() => { onShowSubscriber(channel.channel_id, channel.channel_type) }">订阅者</button>
|
||||
<button class="btn btn-link btn-sm"
|
||||
v-on:click="() => {
|
||||
onShowAllowlist(channel.channel_id, channel.channel_type)
|
||||
|
||||
}">白名单</button>
|
||||
<button class="btn btn-link btn-sm"
|
||||
v-on:click="() => {
|
||||
onShowDenylist(channel.channel_id, channel.channel_type)
|
||||
}">黑名单</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
<div class="flex justify-end mt-10 mr-10">
|
||||
<div className="join">
|
||||
<button :class="{ 'join-item btn': true, 'btn-disabled': !hasPrev }" v-on:click="prevPage">«</button>
|
||||
<button className="join-item btn">{{ currentPage }}</button>
|
||||
<button :class="{ 'join-item btn': true, 'btn-disabled': !hasNext }" v-on:click="nextPage">»</button>
|
||||
</div>
|
||||
</div>
|
||||
<dialog id="userlist" class="modal">
|
||||
<div class="modal-box flex flex-wrap gap-2">
|
||||
<div v-if="currentUids?.length == 0">无数据</div>
|
||||
<a class="link" v-for="uid in currentUids">{{ uid }}</a>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,197 +1,312 @@
|
||||
<script setup lang="ts">
|
||||
// API 接口
|
||||
import { clusterApi } from '@/api/modules/cluster-api';
|
||||
import { dataApi } from '@/api/modules/data-api';
|
||||
import { onMounted, onBeforeUnmount, ref } from 'vue';
|
||||
const loading = ref<boolean>(false);
|
||||
import API from '../../services/API';
|
||||
import { formatMemory } from '../../services/Utils';
|
||||
|
||||
import { VxeFormListeners, VxeFormProps } from 'vxe-pc-ui';
|
||||
const nodeTotal = ref<any>({}); // 节点列表
|
||||
const selectedNodeId = ref<number>(1) // 选中的节点ID
|
||||
const connectionTotal = ref<any>({}); // 连接列表
|
||||
const sortField = ref<string>('id') // 排序字段
|
||||
|
||||
import type { VxeGridInstance, VxeGridProps } from 'vxe-table';
|
||||
const uidSearch = ref<string>('') // 用户UID搜索
|
||||
|
||||
/**
|
||||
* 查询条件
|
||||
* */
|
||||
const nodeList = ref<any[]>([]);
|
||||
const formOptions = reactive<VxeFormProps<any>>({
|
||||
data: {
|
||||
uid: '',
|
||||
node_id: null,
|
||||
sort: 'id',
|
||||
limit: 100
|
||||
},
|
||||
items: [
|
||||
{
|
||||
field: 'node_id',
|
||||
title: '节点',
|
||||
itemRender: {
|
||||
name: 'ElSelect',
|
||||
props: {
|
||||
placeholder: '请选择',
|
||||
style: {
|
||||
width: '180px'
|
||||
}
|
||||
},
|
||||
options: nodeList
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'uid',
|
||||
title: '用户UID',
|
||||
itemRender: { name: 'ElInput', props: { placeholder: '请输入用户UID' } }
|
||||
},
|
||||
|
||||
{
|
||||
align: 'center',
|
||||
slots: { default: 'action' }
|
||||
}
|
||||
]
|
||||
});
|
||||
/** 搜索事件 **/
|
||||
const formEvents: VxeFormListeners<any> = {
|
||||
/** 查询 **/
|
||||
submit() {
|
||||
tableRef.value?.commitProxy('query');
|
||||
},
|
||||
/** 重置 **/
|
||||
reset() {
|
||||
tableRef.value?.commitProxy('query');
|
||||
}
|
||||
};
|
||||
|
||||
const getNodes = async () => {
|
||||
const nodes: any[] = [];
|
||||
const res = await clusterApi.simpleNodes();
|
||||
if (res.data.length > 0) {
|
||||
res.data.map((item: any) => {
|
||||
nodes.push({
|
||||
value: item.id,
|
||||
label: item.id
|
||||
});
|
||||
});
|
||||
}
|
||||
nodeList.value = nodes;
|
||||
if (nodes.length > 0) {
|
||||
formOptions.data = {
|
||||
...formOptions.data,
|
||||
node_id: nodes[0].value
|
||||
};
|
||||
tableRef.value?.commitProxy('query');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 表格
|
||||
**/
|
||||
const tableRef = ref<VxeGridInstance<any>>();
|
||||
const toolNum = ref(0);
|
||||
|
||||
const loadList = async (query: any) => {
|
||||
const res = await dataApi.connections({ ...query });
|
||||
if (res.connections) {
|
||||
toolNum.value = res.total;
|
||||
return res.connections;
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const gridOptions = reactive<VxeGridProps<any>>({
|
||||
showOverflow: true,
|
||||
height: 'auto',
|
||||
border: true,
|
||||
stripe: true,
|
||||
rowConfig: {
|
||||
isCurrent: true,
|
||||
isHover: true
|
||||
},
|
||||
scrollY: {
|
||||
enabled: true,
|
||||
gt: 0
|
||||
},
|
||||
toolbarConfig: {
|
||||
slots: {
|
||||
buttons: 'tools'
|
||||
},
|
||||
refresh: {
|
||||
icon: 'vxe-icon-refresh',
|
||||
iconLoading: 'vxe-icon-refresh roll'
|
||||
},
|
||||
zoom: {
|
||||
iconIn: 'vxe-icon-fullscreen',
|
||||
iconOut: 'vxe-icon-minimize'
|
||||
},
|
||||
custom: true
|
||||
},
|
||||
proxyConfig: {
|
||||
autoLoad: false,
|
||||
ajax: {
|
||||
query: () => {
|
||||
return loadList(formOptions.data);
|
||||
}
|
||||
}
|
||||
},
|
||||
columns: [
|
||||
{ type: 'seq', title: '序号', width: 54 },
|
||||
{ field: 'id', title: '连接ID', minWidth: 120 },
|
||||
{ field: 'uid', title: '用户UID', minWidth: 120 },
|
||||
{ field: 'in_msgs', title: '发出消息数', minWidth: 100 },
|
||||
{ field: 'out_msgs', title: '收到消息数', minWidth: 100 },
|
||||
{ field: 'in_msg_bytes', title: '发出消息字节数', minWidth: 120, formatter: 'formatMemory' },
|
||||
{ field: 'out_msg_bytes', title: '收到消息字节数', minWidth: 120, formatter: 'formatMemory' },
|
||||
{ field: 'in_packets', title: '发出报文数', minWidth: 100 },
|
||||
{ field: 'out_packets', title: '收到报文数', minWidth: 100 },
|
||||
{ field: 'in_packet_bytes', title: '发出报文字节数', minWidth: 120, formatter: 'formatMemory' },
|
||||
{ field: 'out_packet_bytes', title: '收到报文字节数', minWidth: 120, formatter: 'formatMemory' },
|
||||
{
|
||||
field: 'address',
|
||||
title: '连接地址',
|
||||
minWidth: 180,
|
||||
formatter({ row }) {
|
||||
return `${row.ip}:${row.port}`;
|
||||
}
|
||||
},
|
||||
{ field: 'uptime', title: '存活时间', minWidth: 120 },
|
||||
{ field: 'idle', title: '空闲时间', minWidth: 120 },
|
||||
{ field: 'version', title: '协议版本', minWidth: 100 },
|
||||
{ field: 'device', title: '设备', minWidth: 100 },
|
||||
{ field: 'device_id', title: '设备编号', minWidth: 290 },
|
||||
{ field: 'proxy_type_format', title: '代理类型', minWidth: 100 },
|
||||
{ field: 'leader_id', title: '领导节点', minWidth: 100 }
|
||||
]
|
||||
});
|
||||
let connIntervalId: number;
|
||||
|
||||
onMounted(() => {
|
||||
getNodes();
|
||||
});
|
||||
|
||||
API.shared.simpleNodes().then((res) => {
|
||||
nodeTotal.value = res
|
||||
if (nodeTotal.value.data.length > 0) {
|
||||
selectedNodeId.value = nodeTotal.value.data[0].id
|
||||
}
|
||||
loadConnections()
|
||||
startRequestConns()
|
||||
}).catch((err) => {
|
||||
alert(err)
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.clearInterval(connIntervalId)
|
||||
})
|
||||
|
||||
const onNodeChange = (e: any) => {
|
||||
selectedNodeId.value = e.target.value
|
||||
loadConnections(e.target.value)
|
||||
}
|
||||
|
||||
const loadConnections = (closeLoading?: boolean) => {
|
||||
if (!closeLoading) {
|
||||
loading.value = true;
|
||||
|
||||
}
|
||||
API.shared.connections(selectedNodeId.value, 100,sortField.value,uidSearch.value).then((res) => {
|
||||
connectionTotal.value = res
|
||||
}).catch((err) => {
|
||||
alert(err)
|
||||
}).finally(() => {
|
||||
if (!closeLoading) {
|
||||
loading.value = false;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const startRequestConns = async () => {
|
||||
connIntervalId = window.setInterval(async () => {
|
||||
loadConnections(true)
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const onSort = (s: string) => {
|
||||
if (sortField.value == s) {
|
||||
if (s.endsWith("Desc")) {
|
||||
sortField.value = s.substring(0, s.length - 4)
|
||||
} else {
|
||||
sortField.value = s + "Desc"
|
||||
}
|
||||
} else {
|
||||
sortField.value = s
|
||||
}
|
||||
loadConnections()
|
||||
}
|
||||
|
||||
const onUidSearch = (e: any) => {
|
||||
uidSearch.value = e.target.value
|
||||
loadConnections()
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<wk-page class="flex-col">
|
||||
<!-- S 查询条件 -->
|
||||
<div class="mb-12px pt-4px pb-4px card">
|
||||
<vxe-form v-bind="formOptions" v-on="formEvents">
|
||||
<template #action>
|
||||
<el-button native-type="submit" type="primary">查询</el-button>
|
||||
<el-button native-type="reset">重置</el-button>
|
||||
</template>
|
||||
</vxe-form>
|
||||
</div>
|
||||
<!-- E 查询条件 -->
|
||||
<div>
|
||||
<div class="overflow-x-auto h-5/6">
|
||||
<div class="flex">
|
||||
<div class="text-sm ml-3">
|
||||
<label>节点</label>
|
||||
<select class="select select-bordered max-w-xs select-sm w-40 ml-2" v-on:change="onNodeChange">
|
||||
<option v-for="node in nodeTotal.data" :selected="node.id == selectedNodeId">{{ node.id }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- S 表格 -->
|
||||
<div class="flex-1 card !pt-4px overflow-hidden">
|
||||
<vxe-grid ref="tableRef" v-bind="gridOptions"
|
||||
>total
|
||||
<template #tools>
|
||||
<el-text type="primary" tag="b">共计节点总数:{{ toolNum }}</el-text>
|
||||
</template>
|
||||
</vxe-grid>
|
||||
</div>
|
||||
<!-- E 表格 -->
|
||||
</wk-page>
|
||||
</template>
|
||||
<div class="text-sm ml-10">
|
||||
<label>用户UID</label>
|
||||
<input type="text" placeholder="输入" class="input input-bordered select-sm ml-2" v-on:change="onUidSearch" />
|
||||
</div>
|
||||
</div>
|
||||
<table class="table mt-10 table-pin-rows">
|
||||
<!-- head -->
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
连接ID
|
||||
<a href="#" v-on:click="() => onSort('id')">
|
||||
<svg class="w-3 h-3 ms-1.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M8.574 11.024h6.852a2.075 2.075 0 0 0 1.847-1.086 1.9 1.9 0 0 0-.11-1.986L13.736 2.9a2.122 2.122 0 0 0-3.472 0L6.837 7.952a1.9 1.9 0 0 0-.11 1.986 2.074 2.074 0 0 0 1.847 1.086Zm6.852 1.952H8.574a2.072 2.072 0 0 0-1.847 1.087 1.9 1.9 0 0 0 .11 1.985l3.426 5.05a2.123 2.123 0 0 0 3.472 0l3.427-5.05a1.9 1.9 0 0 0 .11-1.985 2.074 2.074 0 0 0-1.846-1.087Z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
用户UID
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
发出消息数
|
||||
<a href="#" v-on:click="() => onSort('inMsg')">
|
||||
<svg class="w-3 h-3 ms-1.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M8.574 11.024h6.852a2.075 2.075 0 0 0 1.847-1.086 1.9 1.9 0 0 0-.11-1.986L13.736 2.9a2.122 2.122 0 0 0-3.472 0L6.837 7.952a1.9 1.9 0 0 0-.11 1.986 2.074 2.074 0 0 0 1.847 1.086Zm6.852 1.952H8.574a2.072 2.072 0 0 0-1.847 1.087 1.9 1.9 0 0 0 .11 1.985l3.426 5.05a2.123 2.123 0 0 0 3.472 0l3.427-5.05a1.9 1.9 0 0 0 .11-1.985 2.074 2.074 0 0 0-1.846-1.087Z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
收到消息数
|
||||
<a href="#" v-on:click="() => onSort('outMsg')">
|
||||
<svg class="w-3 h-3 ms-1.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M8.574 11.024h6.852a2.075 2.075 0 0 0 1.847-1.086 1.9 1.9 0 0 0-.11-1.986L13.736 2.9a2.122 2.122 0 0 0-3.472 0L6.837 7.952a1.9 1.9 0 0 0-.11 1.986 2.074 2.074 0 0 0 1.847 1.086Zm6.852 1.952H8.574a2.072 2.072 0 0 0-1.847 1.087 1.9 1.9 0 0 0 .11 1.985l3.426 5.05a2.123 2.123 0 0 0 3.472 0l3.427-5.05a1.9 1.9 0 0 0 .11-1.985 2.074 2.074 0 0 0-1.846-1.087Z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
发出消息字节数
|
||||
<a href="#" v-on:click="() => onSort('inMsgBytes')">
|
||||
<svg class="w-3 h-3 ms-1.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M8.574 11.024h6.852a2.075 2.075 0 0 0 1.847-1.086 1.9 1.9 0 0 0-.11-1.986L13.736 2.9a2.122 2.122 0 0 0-3.472 0L6.837 7.952a1.9 1.9 0 0 0-.11 1.986 2.074 2.074 0 0 0 1.847 1.086Zm6.852 1.952H8.574a2.072 2.072 0 0 0-1.847 1.087 1.9 1.9 0 0 0 .11 1.985l3.426 5.05a2.123 2.123 0 0 0 3.472 0l3.427-5.05a1.9 1.9 0 0 0 .11-1.985 2.074 2.074 0 0 0-1.846-1.087Z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
收到消息字节数
|
||||
<a href="#" v-on:click="() => onSort('outMsgBytes')">
|
||||
<svg class="w-3 h-3 ms-1.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M8.574 11.024h6.852a2.075 2.075 0 0 0 1.847-1.086 1.9 1.9 0 0 0-.11-1.986L13.736 2.9a2.122 2.122 0 0 0-3.472 0L6.837 7.952a1.9 1.9 0 0 0-.11 1.986 2.074 2.074 0 0 0 1.847 1.086Zm6.852 1.952H8.574a2.072 2.072 0 0 0-1.847 1.087 1.9 1.9 0 0 0 .11 1.985l3.426 5.05a2.123 2.123 0 0 0 3.472 0l3.427-5.05a1.9 1.9 0 0 0 .11-1.985 2.074 2.074 0 0 0-1.846-1.087Z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
title: 连接
|
||||
</route>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
发出报文数
|
||||
<a href="#" v-on:click="() => onSort('inPacket')">
|
||||
<svg class="w-3 h-3 ms-1.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M8.574 11.024h6.852a2.075 2.075 0 0 0 1.847-1.086 1.9 1.9 0 0 0-.11-1.986L13.736 2.9a2.122 2.122 0 0 0-3.472 0L6.837 7.952a1.9 1.9 0 0 0-.11 1.986 2.074 2.074 0 0 0 1.847 1.086Zm6.852 1.952H8.574a2.072 2.072 0 0 0-1.847 1.087 1.9 1.9 0 0 0 .11 1.985l3.426 5.05a2.123 2.123 0 0 0 3.472 0l3.427-5.05a1.9 1.9 0 0 0 .11-1.985 2.074 2.074 0 0 0-1.846-1.087Z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
收到报文数
|
||||
<a href="#" v-on:click="() => onSort('outPacket')">
|
||||
<svg class="w-3 h-3 ms-1.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M8.574 11.024h6.852a2.075 2.075 0 0 0 1.847-1.086 1.9 1.9 0 0 0-.11-1.986L13.736 2.9a2.122 2.122 0 0 0-3.472 0L6.837 7.952a1.9 1.9 0 0 0-.11 1.986 2.074 2.074 0 0 0 1.847 1.086Zm6.852 1.952H8.574a2.072 2.072 0 0 0-1.847 1.087 1.9 1.9 0 0 0 .11 1.985l3.426 5.05a2.123 2.123 0 0 0 3.472 0l3.427-5.05a1.9 1.9 0 0 0 .11-1.985 2.074 2.074 0 0 0-1.846-1.087Z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
发出报文字节数
|
||||
<a href="#" v-on:click="() => onSort('inPacketBytes')">
|
||||
<svg class="w-3 h-3 ms-1.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M8.574 11.024h6.852a2.075 2.075 0 0 0 1.847-1.086 1.9 1.9 0 0 0-.11-1.986L13.736 2.9a2.122 2.122 0 0 0-3.472 0L6.837 7.952a1.9 1.9 0 0 0-.11 1.986 2.074 2.074 0 0 0 1.847 1.086Zm6.852 1.952H8.574a2.072 2.072 0 0 0-1.847 1.087 1.9 1.9 0 0 0 .11 1.985l3.426 5.05a2.123 2.123 0 0 0 3.472 0l3.427-5.05a1.9 1.9 0 0 0 .11-1.985 2.074 2.074 0 0 0-1.846-1.087Z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
收到报文字节数
|
||||
<a href="#" v-on:click="() => onSort('outPacketBytes')">
|
||||
<svg class="w-3 h-3 ms-1.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M8.574 11.024h6.852a2.075 2.075 0 0 0 1.847-1.086 1.9 1.9 0 0 0-.11-1.986L13.736 2.9a2.122 2.122 0 0 0-3.472 0L6.837 7.952a1.9 1.9 0 0 0-.11 1.986 2.074 2.074 0 0 0 1.847 1.086Zm6.852 1.952H8.574a2.072 2.072 0 0 0-1.847 1.087 1.9 1.9 0 0 0 .11 1.985l3.426 5.05a2.123 2.123 0 0 0 3.472 0l3.427-5.05a1.9 1.9 0 0 0 .11-1.985 2.074 2.074 0 0 0-1.846-1.087Z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
连接地址
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
存活时间
|
||||
<a href="#" v-on:click="() => onSort('uptime')">
|
||||
<svg class="w-3 h-3 ms-1.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M8.574 11.024h6.852a2.075 2.075 0 0 0 1.847-1.086 1.9 1.9 0 0 0-.11-1.986L13.736 2.9a2.122 2.122 0 0 0-3.472 0L6.837 7.952a1.9 1.9 0 0 0-.11 1.986 2.074 2.074 0 0 0 1.847 1.086Zm6.852 1.952H8.574a2.072 2.072 0 0 0-1.847 1.087 1.9 1.9 0 0 0 .11 1.985l3.426 5.05a2.123 2.123 0 0 0 3.472 0l3.427-5.05a1.9 1.9 0 0 0 .11-1.985 2.074 2.074 0 0 0-1.846-1.087Z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
空闲时间
|
||||
<a href="#" v-on:click="() => onSort('idle')">
|
||||
<svg class="w-3 h-3 ms-1.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M8.574 11.024h6.852a2.075 2.075 0 0 0 1.847-1.086 1.9 1.9 0 0 0-.11-1.986L13.736 2.9a2.122 2.122 0 0 0-3.472 0L6.837 7.952a1.9 1.9 0 0 0-.11 1.986 2.074 2.074 0 0 0 1.847 1.086Zm6.852 1.952H8.574a2.072 2.072 0 0 0-1.847 1.087 1.9 1.9 0 0 0 .11 1.985l3.426 5.05a2.123 2.123 0 0 0 3.472 0l3.427-5.05a1.9 1.9 0 0 0 .11-1.985 2.074 2.074 0 0 0-1.846-1.087Z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
协议版本
|
||||
<a href="#" v-on:click="() => onSort('protoVersion')">
|
||||
<svg class="w-3 h-3 ms-1.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M8.574 11.024h6.852a2.075 2.075 0 0 0 1.847-1.086 1.9 1.9 0 0 0-.11-1.986L13.736 2.9a2.122 2.122 0 0 0-3.472 0L6.837 7.952a1.9 1.9 0 0 0-.11 1.986 2.074 2.074 0 0 0 1.847 1.086Zm6.852 1.952H8.574a2.072 2.072 0 0 0-1.847 1.087 1.9 1.9 0 0 0 .11 1.985l3.426 5.05a2.123 2.123 0 0 0 3.472 0l3.427-5.05a1.9 1.9 0 0 0 .11-1.985 2.074 2.074 0 0 0-1.846-1.087Z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</th>
|
||||
<th>设备</th>
|
||||
<th>设备编号</th>
|
||||
<th>代理类型</th>
|
||||
<th>领导节点</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- row 1 -->
|
||||
<tr v-for="conn in connectionTotal.connections">
|
||||
<td class="text-blue-800">{{ conn.id }}</td>
|
||||
<td>{{ conn.uid }}</td>
|
||||
<td>{{ conn.in_msgs }}</td>
|
||||
<td>{{ conn.out_msgs }}</td>
|
||||
<td>{{ formatMemory(conn.in_msg_bytes) }}</td>
|
||||
<td>{{ formatMemory(conn.out_msg_bytes) }}</td>
|
||||
<td>{{ conn.in_packets }}</td>
|
||||
<td>{{ conn.out_packets }}</td>
|
||||
<td>{{ formatMemory(conn.in_packet_bytes) }}</td>
|
||||
<td>{{ formatMemory(conn.out_packet_bytes) }}</td>
|
||||
<td>{{ `${conn.ip}:${conn.port}` }}</td>
|
||||
<td>{{ conn.uptime }}</td>
|
||||
<td>{{ conn.idle }}</td>
|
||||
<td>{{ conn.version }}</td>
|
||||
<td>{{ conn.device }}</td>
|
||||
<td>{{ conn.device_id }}</td>
|
||||
<td>{{conn.proxy_type_format}}</td>
|
||||
<td>{{conn.leader_id}}</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="flex flex-col gap-4 w-full mt-2" v-if="loading">
|
||||
<div class="skeleton h-6 w-full"></div>
|
||||
<div class="skeleton h-6 w-full"></div>
|
||||
<div class="skeleton h-6 w-full"></div>
|
||||
<div class="skeleton h-6 w-full"></div>
|
||||
<div class="skeleton h-6 w-full"></div>
|
||||
<div class="skeleton h-6 w-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end mt-10 mr-10">
|
||||
<div class="flex pr-4">
|
||||
<div class="flex items-center">
|
||||
<ul class="text-sm">
|
||||
<li>
|
||||
总数量: <span class="text-blue-800">{{ connectionTotal.total }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,136 +1,109 @@
|
||||
<script setup lang="ts">
|
||||
// API 接口
|
||||
import { dataApi } from '@/api/modules/data-api';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import API from '../../services/API';
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
import { VxeFormListeners, VxeFormProps } from 'vxe-pc-ui';
|
||||
const conversationTotal = ref<any>({}); // 会话列表
|
||||
const uid = ref<string>(); // 用户UID
|
||||
|
||||
import type { VxeGridInstance, VxeGridProps } from 'vxe-table';
|
||||
const router = useRouter()
|
||||
|
||||
import { useRoute } from 'vue-router';
|
||||
const query = router.currentRoute.value.query; //查询参数
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
/**
|
||||
* 查询条件
|
||||
* */
|
||||
const formOptions = reactive<VxeFormProps<any>>({
|
||||
data: {
|
||||
uid: ''
|
||||
},
|
||||
items: [
|
||||
{
|
||||
field: 'uid',
|
||||
title: '用户UID',
|
||||
itemRender: { name: 'ElInput', props: { placeholder: '请输入用户UID' } }
|
||||
},
|
||||
{
|
||||
align: 'center',
|
||||
slots: { default: 'action' }
|
||||
}
|
||||
]
|
||||
});
|
||||
/** 搜索事件 **/
|
||||
const formEvents: VxeFormListeners<any> = {
|
||||
/** 查询 **/
|
||||
submit() {
|
||||
tableRef.value?.commitProxy('query');
|
||||
},
|
||||
/** 重置 **/
|
||||
reset() {
|
||||
tableRef.value?.commitProxy('query');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 表格
|
||||
**/
|
||||
const tableRef = ref<VxeGridInstance<any>>();
|
||||
|
||||
const loadList = async (query: any) => {
|
||||
const res = await dataApi.conversations({ ...query });
|
||||
if (res.data) {
|
||||
return res.data;
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const gridOptions = reactive<VxeGridProps<any>>({
|
||||
showOverflow: true,
|
||||
height: 'auto',
|
||||
border: true,
|
||||
stripe: true,
|
||||
rowConfig: {
|
||||
isCurrent: true,
|
||||
isHover: true
|
||||
},
|
||||
scrollY: {
|
||||
enabled: true,
|
||||
gt: 0
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: {
|
||||
icon: 'vxe-icon-refresh',
|
||||
iconLoading: 'vxe-icon-refresh roll'
|
||||
},
|
||||
zoom: {
|
||||
iconIn: 'vxe-icon-fullscreen',
|
||||
iconOut: 'vxe-icon-minimize'
|
||||
},
|
||||
custom: true
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: () => {
|
||||
return loadList(formOptions.data);
|
||||
}
|
||||
}
|
||||
},
|
||||
columns: [
|
||||
{ type: 'seq', title: '序号', width: 54 },
|
||||
{ field: 'uid', title: '用户UID', minWidth: 220 },
|
||||
{ field: 'type_format', title: '会话类型', minWidth: 120 },
|
||||
{ field: 'channel_id', title: '频道ID', minWidth: 220 },
|
||||
{ field: 'channel_type_format', title: '频道类型', minWidth: 120 },
|
||||
{ field: 'last_msg_seq', title: '最新消息序号', minWidth: 120 },
|
||||
{ field: 'readed_to_msg_seq', title: '已读消息序号', minWidth: 120 },
|
||||
{ field: 'unread_count', title: '未读数量', minWidth: 120 },
|
||||
{ field: 'updated_at_format', title: '最后会话时间', minWidth: 140 }
|
||||
]
|
||||
});
|
||||
if(query.uid){
|
||||
uid.value = query.uid as string
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (route.query?.uid) {
|
||||
formOptions.data = {
|
||||
uid: route.query.uid
|
||||
};
|
||||
}
|
||||
});
|
||||
searchConversation()
|
||||
})
|
||||
|
||||
const searchConversation = () => {
|
||||
API.shared.conversations({uid:uid.value}).then((res) => {
|
||||
conversationTotal.value = res
|
||||
}).catch((err) => {
|
||||
alert(err)
|
||||
})
|
||||
}
|
||||
|
||||
const onUidSearch = (e: any) => {
|
||||
uid.value = e.target.value
|
||||
searchConversation()
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<wk-page class="flex-col">
|
||||
<!-- S 查询条件 -->
|
||||
<div class="mb-12px pt-4px pb-4px card">
|
||||
<vxe-form v-bind="formOptions" v-on="formEvents">
|
||||
<template #action>
|
||||
<el-button native-type="submit" type="primary">查询</el-button>
|
||||
<el-button native-type="reset">重置</el-button>
|
||||
</template>
|
||||
</vxe-form>
|
||||
<div>
|
||||
<div class="overflow-x-auto h-5/6">
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<div class="text-sm ml-10">
|
||||
<label>用户UID</label>
|
||||
<input type="text" placeholder="输入" class="input input-bordered select-sm ml-2"
|
||||
v-on:change="onUidSearch" v-model="uid"/>
|
||||
</div>
|
||||
</div>
|
||||
<table class="table mt-10 table-pin-rows">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
用户UID
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
会话类型
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
频道ID
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
频道类型
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
最新消息序号
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
已读消息序号
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
未读数量
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
最后会话时间
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="conversation in conversationTotal.data">
|
||||
<td> {{ conversation.uid }}</td>
|
||||
<td>{{conversation.type_format}}</td>
|
||||
<td>{{conversation.channel_id}}</td>
|
||||
<td>{{conversation.channel_type_format}}</td>
|
||||
<td>{{conversation.last_msg_seq}}</td>
|
||||
<td>{{conversation.readed_to_msg_seq}}</td>
|
||||
<td>{{conversation.unread_count}}</td>
|
||||
<td>{{conversation.updated_at_format}}</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!-- E 查询条件 -->
|
||||
|
||||
<!-- S 表格 -->
|
||||
<div class="flex-1 card !pt-4px overflow-hidden">
|
||||
<vxe-grid ref="tableRef" v-bind="gridOptions"></vxe-grid>
|
||||
</div>
|
||||
<!-- E 表格 -->
|
||||
</wk-page>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
title: 会话
|
||||
</route>
|
||||
</template>
|
||||
@@ -1,180 +1,190 @@
|
||||
<script setup lang="ts">
|
||||
// API 接口
|
||||
import { dataApi } from '@/api/modules/data-api';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import API from '../../services/API';
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
import { VxeFormListeners, VxeFormProps } from 'vxe-pc-ui';
|
||||
|
||||
import type { VxeGridInstance, VxeGridProps } from 'vxe-table';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
const route = useRoute();
|
||||
/**
|
||||
* 查询条件
|
||||
* */
|
||||
const formOptions = reactive<VxeFormProps<any>>({
|
||||
data: {
|
||||
uid: '',
|
||||
channel_type: null,
|
||||
channel_id: null,
|
||||
offset_created_at: 0,
|
||||
limit: 20,
|
||||
pre: 0
|
||||
},
|
||||
items: [
|
||||
{
|
||||
field: 'uid',
|
||||
title: '用户UID',
|
||||
itemRender: { name: 'ElInput', props: { placeholder: '请输入用户UID' } }
|
||||
},
|
||||
{
|
||||
align: 'center',
|
||||
slots: { default: 'action' }
|
||||
}
|
||||
]
|
||||
});
|
||||
/** 搜索事件 **/
|
||||
const formEvents: VxeFormListeners<any> = {
|
||||
/** 查询 **/
|
||||
submit() {
|
||||
tableRef.value?.commitProxy('query');
|
||||
},
|
||||
/** 重置 **/
|
||||
reset() {
|
||||
tableRef.value?.commitProxy('query');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 表格
|
||||
**/
|
||||
const tableRef = ref<VxeGridInstance<any>>();
|
||||
const deviceTotal = ref<any>({}); // 设备列表
|
||||
const uid = ref<string>(''); // 用户UID
|
||||
const offsetCreatedAt = ref(0); // 偏移量
|
||||
const pre = ref<boolean>() // 是否向上分页
|
||||
const pageSize = ref(20); // 每页显示数量
|
||||
const currentPage = ref(1); // 当前页
|
||||
const hasPrev = ref<boolean>(false); // 是否有上一页
|
||||
const hasNext = ref<boolean>(true); // 是否有下一页
|
||||
const hasNext = ref<boolean>(true) // 是否有下一页
|
||||
const hasPrev = ref<boolean>(false) // 是否有上一页
|
||||
|
||||
const loadList = async (query: any) => {
|
||||
const res = await dataApi.devices({ ...query });
|
||||
if (res.data) {
|
||||
hasNext.value = res.more !== 1;
|
||||
hasPrev.value = currentPage.value <= 1;
|
||||
return res.data;
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const gridOptions = reactive<VxeGridProps<any>>({
|
||||
showOverflow: true,
|
||||
height: 'auto',
|
||||
border: true,
|
||||
stripe: true,
|
||||
rowConfig: {
|
||||
isCurrent: true,
|
||||
isHover: true
|
||||
},
|
||||
scrollY: {
|
||||
enabled: true,
|
||||
gt: 0
|
||||
},
|
||||
toolbarConfig: {
|
||||
slots: {
|
||||
buttons: 'tools'
|
||||
},
|
||||
refresh: {
|
||||
icon: 'vxe-icon-refresh',
|
||||
iconLoading: 'vxe-icon-refresh roll'
|
||||
},
|
||||
zoom: {
|
||||
iconIn: 'vxe-icon-fullscreen',
|
||||
iconOut: 'vxe-icon-minimize'
|
||||
},
|
||||
custom: true
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: () => {
|
||||
return loadList(formOptions.data);
|
||||
}
|
||||
}
|
||||
},
|
||||
columns: [
|
||||
{ field: 'uid', title: '所属用户', minWidth: 220 },
|
||||
{ field: 'device_flag_format', title: '设备标识', minWidth: 120 },
|
||||
{ field: 'device_level_format', title: '设备等级', minWidth: 120 },
|
||||
{ field: 'token', title: '设备Token', minWidth: 120 },
|
||||
{ field: 'conn_count', title: '连接数', minWidth: 160 },
|
||||
{ field: 'send_msg_count', title: '发出消息数', minWidth: 120 },
|
||||
{ field: 'recv_msg_count', title: '接受消息数', minWidth: 120 },
|
||||
{ field: 'send_msg_bytes', title: '发出消息大小', minWidth: 140 },
|
||||
{ field: 'recv_msg_bytes', title: '接受消息大小', minWidth: 140 },
|
||||
{ field: 'created_at_format', title: '创建时间', minWidth: 140 },
|
||||
{ field: 'updated_at_format', title: '更新时间', minWidth: 140 }
|
||||
]
|
||||
});
|
||||
const router = useRouter()
|
||||
|
||||
/**
|
||||
* 分页切换
|
||||
*/
|
||||
const onPage = (type: 0 | 1) => {
|
||||
// 下一页
|
||||
if (type === 0) {
|
||||
currentPage.value = currentPage.value + 1;
|
||||
}
|
||||
const query = router.currentRoute.value.query; //查询参数
|
||||
|
||||
// 上一页
|
||||
if (type === 1 && currentPage.value > 1) {
|
||||
currentPage.value = currentPage.value - 1;
|
||||
}
|
||||
if (query.uid) {
|
||||
uid.value = query.uid as string
|
||||
}
|
||||
|
||||
console.log(currentPage.value);
|
||||
formOptions.data = {
|
||||
...formOptions.data,
|
||||
pre: type
|
||||
};
|
||||
|
||||
tableRef.value?.commitProxy('query');
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (route.query?.uid) {
|
||||
formOptions.data = {
|
||||
...formOptions.data,
|
||||
uid: route.query.uid
|
||||
};
|
||||
}
|
||||
searchDevice()
|
||||
});
|
||||
|
||||
const searchDevice = () => {
|
||||
API.shared.devices({
|
||||
uid: uid.value,
|
||||
offsetCreatedAt: offsetCreatedAt.value,
|
||||
pre: pre.value,
|
||||
limit: pageSize.value
|
||||
}).then((res) => {
|
||||
deviceTotal.value = res
|
||||
hasNext.value = deviceTotal.value?.more === 1
|
||||
hasPrev.value = currentPage.value > 1
|
||||
if(pre.value) { // 如果是向上翻页,则有下页数据
|
||||
hasNext.value = true
|
||||
}
|
||||
}).catch((err) => {
|
||||
alert(err)
|
||||
})
|
||||
}
|
||||
|
||||
const onUidSearch = (e: any) => {
|
||||
uid.value = e.target.value
|
||||
searchDevice()
|
||||
}
|
||||
|
||||
|
||||
const prevPage = () => {
|
||||
if (currentPage.value <= 1) {
|
||||
hasPrev.value = false
|
||||
return
|
||||
}
|
||||
hasPrev.value = true
|
||||
currentPage.value -= 1
|
||||
pre.value = true
|
||||
if (deviceTotal.value?.data?.length > 0) {
|
||||
offsetCreatedAt.value = deviceTotal.value.data[0].created_at
|
||||
}
|
||||
searchDevice()
|
||||
}
|
||||
|
||||
const nextPage = () => {
|
||||
if (deviceTotal.value?.more === 0 && !pre.value) {
|
||||
return
|
||||
}
|
||||
currentPage.value += 1
|
||||
pre.value = false
|
||||
if (deviceTotal.value?.data?.length > 0) {
|
||||
offsetCreatedAt.value = deviceTotal.value.data[deviceTotal.value.data.length - 1].created_at
|
||||
}
|
||||
searchDevice()
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<wk-page class="flex-col">
|
||||
<!-- S 查询条件 -->
|
||||
<div class="mb-12px pt-4px pb-4px card">
|
||||
<vxe-form v-bind="formOptions" v-on="formEvents">
|
||||
<template #action>
|
||||
<el-button native-type="submit" type="primary">查询</el-button>
|
||||
<el-button native-type="reset">重置</el-button>
|
||||
</template>
|
||||
</vxe-form>
|
||||
|
||||
<div>
|
||||
<div class="overflow-x-auto h-5/6">
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<div class="text-sm ml-4">
|
||||
<label>用户UID</label>
|
||||
<input type="text" placeholder="输入" class="input input-bordered select-sm ml-2"
|
||||
v-on:change="onUidSearch" v-model="uid" />
|
||||
</div>
|
||||
</div>
|
||||
<table class="table mt-5 table-pin-rows">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
所属用户
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
设备标识
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
设备等级
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
设备Token
|
||||
</div>
|
||||
</th>
|
||||
<!-- <th>
|
||||
<div class="flex items-center">
|
||||
连接数
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
发出消息数
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
接受消息数
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
发出消息大小
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
接受消息大小
|
||||
</div>
|
||||
</th> -->
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
创建时间
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
更新时间
|
||||
</div>
|
||||
</th>
|
||||
<!-- <th>
|
||||
<div class="flex items-center">
|
||||
操作
|
||||
</div>
|
||||
</th> -->
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="device in deviceTotal.data">
|
||||
<td>
|
||||
{{ device.uid }}
|
||||
</td>
|
||||
<td>{{ device.device_flag_format }}</td>
|
||||
<td>{{ device.device_level_format }}</td>
|
||||
<td>{{ device.token }}</td>
|
||||
<!-- <td>{{ device.conn_count }}</td>
|
||||
<td>{{ device.send_msg_count }}</td>
|
||||
<td>{{ device.recv_msg_count }}</td>
|
||||
<td>{{ device.send_msg_bytes }}</td>
|
||||
<td>{{ device.recv_msg_bytes }}</td> -->
|
||||
<td>{{ device.created_at_format }}</td>
|
||||
<td>{{ device.updated_at_format }}</td>
|
||||
<!-- <td class="flex">
|
||||
<button class="btn btn-sm btn-warning">踢出</button>
|
||||
</td> -->
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="flex justify-end mt-10 mr-10">
|
||||
<div className="join">
|
||||
<button :class="{ 'join-item btn': true, 'btn-disabled': !hasPrev }" v-on:click="prevPage">«</button>
|
||||
<button className="join-item btn">{{ currentPage }}</button>
|
||||
<button :class="{ 'join-item btn': true, 'btn-disabled': !hasNext }" v-on:click="nextPage">»</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- E 查询条件 -->
|
||||
|
||||
<!-- S 表格 -->
|
||||
<div class="flex-1 card !pt-4px overflow-hidden">
|
||||
<vxe-grid ref="tableRef" v-bind="gridOptions">
|
||||
<template #tools>
|
||||
<el-space>
|
||||
<el-button type="primary" :disabled="hasPrev" @click="onPage(1)">上一页</el-button>
|
||||
<el-button type="primary" :disabled="hasNext" @click="onPage(0)">下一页</el-button>
|
||||
</el-space>
|
||||
</template>
|
||||
</vxe-grid>
|
||||
</div>
|
||||
<!-- E 表格 -->
|
||||
</wk-page>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
title: 设备
|
||||
</route>
|
||||
</template>
|
||||
@@ -1,273 +1,345 @@
|
||||
<script setup lang="ts">
|
||||
// API 接口
|
||||
import { dataApi } from '@/api/modules/data-api';
|
||||
// 常量
|
||||
import { CHANNEL_TYPE } from '@/constants';
|
||||
import { base64Decode } from '@/utils';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import API from '../../services/API';
|
||||
import { ellipsis, base64Decode, base64Encode } from '../../services/Utils';
|
||||
import { useRouter } from "vue-router";
|
||||
import App from '../../services/App';
|
||||
|
||||
import { VxeFormListeners, VxeFormProps } from 'vxe-pc-ui';
|
||||
const router = useRouter()
|
||||
|
||||
import type { VxeGridInstance, VxeGridProps } from 'vxe-table';
|
||||
const nodeTotal = ref<any>({}); // 节点列表
|
||||
const selectedNodeId = ref<number>() // 选中的节点ID
|
||||
const loading = ref<boolean>(false);
|
||||
const messages = ref<any>({}); // 消息列表
|
||||
const fromUid = ref<string>() // 发送者
|
||||
const channelId = ref<string>() // 接受频道id
|
||||
const channelType = ref<number>() // 频道类型
|
||||
const payload = ref<string>() // 消息内容
|
||||
const messageId = ref<number>() // 消息id
|
||||
const clientMsgNo = ref<string>() // 客户端唯一编号
|
||||
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
const currentPage = ref<number>(1) // 当前页码
|
||||
const pageSize = ref<number>(20) // 每页数量
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const offsetMessageId = ref<number>() // 偏移的messageId
|
||||
const offsetMessageSeq = ref<number>() // 偏移的messageSeq
|
||||
const pre = ref<boolean>() // 是否向上分页
|
||||
const content = ref<string>() // 当前要显示的内容
|
||||
|
||||
/**
|
||||
* 查询条件
|
||||
* */
|
||||
const formOptions = reactive<VxeFormProps<any>>({
|
||||
data: {
|
||||
node_id: '',
|
||||
from_uid: '',
|
||||
channel_type: null,
|
||||
channel_id: null,
|
||||
payload: '',
|
||||
message_id: '',
|
||||
offset_message_id: 0,
|
||||
offset_message_seq: 0,
|
||||
client_msg_no: '',
|
||||
limit: 20,
|
||||
pre: 0
|
||||
},
|
||||
items: [
|
||||
{
|
||||
field: 'from_uid',
|
||||
title: '发送者',
|
||||
itemRender: { name: 'ElInput', props: { placeholder: '请输入发送者' } }
|
||||
},
|
||||
{
|
||||
field: 'channel_type',
|
||||
title: '频道类型',
|
||||
itemRender: {
|
||||
name: 'ElSelect',
|
||||
props: {
|
||||
placeholder: '请选择频道类型',
|
||||
style: { width: '180px' }
|
||||
},
|
||||
options: CHANNEL_TYPE
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'channel_id',
|
||||
title: '频道ID',
|
||||
itemRender: { name: 'ElInput', props: { placeholder: '请输入频道ID' } }
|
||||
},
|
||||
{
|
||||
field: 'message_id',
|
||||
title: '消息ID',
|
||||
itemRender: { name: 'ElInput', props: { placeholder: '请输入消息ID' } }
|
||||
},
|
||||
{
|
||||
field: 'payload',
|
||||
title: '消息内容',
|
||||
itemRender: { name: 'ElInput', props: { placeholder: '请输入消息内容' } }
|
||||
},
|
||||
{
|
||||
field: 'client_msg_no',
|
||||
title: '客户端编号',
|
||||
itemRender: { name: 'ElInput', props: { placeholder: '请输入客户端编号' } }
|
||||
},
|
||||
{
|
||||
align: 'center',
|
||||
slots: { default: 'action' }
|
||||
}
|
||||
]
|
||||
});
|
||||
/** 搜索事件 **/
|
||||
const formEvents: VxeFormListeners<any> = {
|
||||
/** 查询 **/
|
||||
submit() {
|
||||
tableRef.value?.commitProxy('query');
|
||||
},
|
||||
/** 重置 **/
|
||||
reset() {
|
||||
tableRef.value?.commitProxy('query');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 表格
|
||||
**/
|
||||
const tableRef = ref<VxeGridInstance<any>>();
|
||||
const currentPage = ref(1); // 当前页
|
||||
const hasPrev = ref<boolean>(false); // 是否有上一页
|
||||
const hasNext = ref<boolean>(true); // 是否有下一页
|
||||
|
||||
const loadList = async (query: any) => {
|
||||
const res = await dataApi.searchMessages({ ...query });
|
||||
if (res.data) {
|
||||
hasNext.value = res.data.length < formOptions.data.limit;
|
||||
hasPrev.value = currentPage.value <= 1;
|
||||
return res.data;
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const gridOptions = reactive<VxeGridProps<any>>({
|
||||
showOverflow: true,
|
||||
height: 'auto',
|
||||
border: true,
|
||||
stripe: true,
|
||||
rowConfig: {
|
||||
isCurrent: true,
|
||||
isHover: true
|
||||
},
|
||||
scrollY: {
|
||||
enabled: true,
|
||||
gt: 0
|
||||
},
|
||||
toolbarConfig: {
|
||||
slots: {
|
||||
buttons: 'tools'
|
||||
},
|
||||
refresh: {
|
||||
icon: 'vxe-icon-refresh',
|
||||
iconLoading: 'vxe-icon-refresh roll'
|
||||
},
|
||||
zoom: {
|
||||
iconIn: 'vxe-icon-fullscreen',
|
||||
iconOut: 'vxe-icon-minimize'
|
||||
},
|
||||
custom: true
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: () => {
|
||||
return loadList(formOptions.data);
|
||||
}
|
||||
}
|
||||
},
|
||||
columns: [
|
||||
{ type: 'seq', title: '序号', width: 54 },
|
||||
{ field: 'message_id', title: '消息ID', minWidth: 140 },
|
||||
{ field: 'message_seq', title: '消息序号', minWidth: 100 },
|
||||
{ field: 'from_uid', title: '发送者', minWidth: 100 },
|
||||
{ field: 'channel_id', title: '接受频道', minWidth: 100 },
|
||||
{
|
||||
field: 'channel_type',
|
||||
title: '接受频道类型',
|
||||
minWidth: 80,
|
||||
formatter({ cellValue }) {
|
||||
const item = CHANNEL_TYPE.find(item => item.value === cellValue);
|
||||
return item ? item.label : '';
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'payload',
|
||||
title: '消息内容',
|
||||
minWidth: 280,
|
||||
formatter({ cellValue }) {
|
||||
return base64Decode(cellValue);
|
||||
}
|
||||
},
|
||||
{ field: 'timestamp_format', title: '发送时间', minWidth: 120 },
|
||||
{ field: 'client_msg_no', title: '客户端唯一编号', minWidth: 240 },
|
||||
{ field: 'action', title: '操作', width: 90, fixed: 'right', slots: { default: 'action' } }
|
||||
]
|
||||
});
|
||||
|
||||
/**
|
||||
* 分页切换
|
||||
*/
|
||||
const onPage = (type: 0 | 1) => {
|
||||
let offset_message_id = 0;
|
||||
let offset_message_seq = 0;
|
||||
const tableData = tableRef.value?.getData();
|
||||
// 下一页
|
||||
if (type === 0) {
|
||||
currentPage.value = currentPage.value + 1;
|
||||
if (tableData && tableData.length > 0) {
|
||||
offset_message_id = tableData[tableData.length - 1].message_id;
|
||||
offset_message_seq = tableData[tableData.length - 1].message_seq;
|
||||
}
|
||||
}
|
||||
|
||||
// 上一页
|
||||
if (type === 1 && currentPage.value > 1) {
|
||||
currentPage.value = currentPage.value - 1;
|
||||
if (tableData && tableData.length > 0) {
|
||||
offset_message_id = tableData[0].message_id;
|
||||
offset_message_seq = tableData[0].message_seq;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(currentPage.value);
|
||||
formOptions.data = {
|
||||
...formOptions.data,
|
||||
offset_message_id,
|
||||
offset_message_seq,
|
||||
pre: type
|
||||
};
|
||||
|
||||
tableRef.value?.commitProxy('query');
|
||||
};
|
||||
|
||||
const onPageTrace = (row: any) => {
|
||||
router.push({
|
||||
path: '/monitor/trace',
|
||||
query: {
|
||||
clientMsgNo: row.client_msg_no
|
||||
}
|
||||
});
|
||||
};
|
||||
const query = router.currentRoute.value.query; //查询参数
|
||||
|
||||
onMounted(() => {
|
||||
// 频道类型
|
||||
if (route.query?.channel_type) {
|
||||
formOptions.data = {
|
||||
...formOptions.data,
|
||||
channel_type: Number(route.query.channel_type)
|
||||
};
|
||||
}
|
||||
// 频道ID
|
||||
if (route.query?.channel_id) {
|
||||
formOptions.data = {
|
||||
...formOptions.data,
|
||||
channel_id: route.query.channel_id
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
App.shard().loadSystemSettingIfNeed()
|
||||
|
||||
if (query.channelId) {
|
||||
channelId.value = query.channelId as string
|
||||
}
|
||||
|
||||
if (query.channelType) {
|
||||
channelType.value = parseInt(query.channelType as string)
|
||||
}
|
||||
|
||||
searchMessages()
|
||||
API.shared.simpleNodes().then((res) => {
|
||||
nodeTotal.value = res
|
||||
}).catch((err) => {
|
||||
alert(err)
|
||||
})
|
||||
})
|
||||
|
||||
const searchMessages = () => {
|
||||
loading.value = true;
|
||||
let base64EncodePayload: string = ''
|
||||
if (payload.value && payload.value.trim() != '') {
|
||||
base64EncodePayload = base64Encode(payload.value)
|
||||
console.log(base64EncodePayload)
|
||||
base64EncodePayload = encodeURIComponent(base64EncodePayload)
|
||||
}
|
||||
API.shared.searchMessages({
|
||||
nodeId: selectedNodeId.value,
|
||||
fromUid: fromUid.value,
|
||||
channelId: channelId.value,
|
||||
channelType: channelType.value,
|
||||
payload: base64EncodePayload,
|
||||
messageId: messageId.value,
|
||||
limit: pageSize.value,
|
||||
offsetMessageId: offsetMessageId.value,
|
||||
offsetMessageSeq: offsetMessageSeq.value,
|
||||
pre: pre.value,
|
||||
clientMsgNo: clientMsgNo.value
|
||||
}).then((res) => {
|
||||
messages.value = res.data
|
||||
}).catch((err) => {
|
||||
alert(err)
|
||||
}).finally(() => {
|
||||
loading.value = false;
|
||||
})
|
||||
}
|
||||
|
||||
// const onNodeChange = (e: any) => {
|
||||
// selectedNodeId.value = e.target.value
|
||||
// searchMessages()
|
||||
// }
|
||||
|
||||
const onFromUidSearch = (e: any) => {
|
||||
fromUid.value = e.target.value
|
||||
resetFilter()
|
||||
searchMessages()
|
||||
}
|
||||
|
||||
const onChannelIdSearch = (e: any) => {
|
||||
channelId.value = e.target.value
|
||||
if (!channelId.value || channelId.value.trim() == '') {
|
||||
channelType.value = 0
|
||||
}
|
||||
resetFilter()
|
||||
searchMessages()
|
||||
}
|
||||
|
||||
const onChannelTypeSearch = (e: any) => {
|
||||
channelType.value = e.target.value
|
||||
if (!channelId.value || channelId.value.trim() == '') {
|
||||
return
|
||||
}
|
||||
resetFilter()
|
||||
searchMessages()
|
||||
}
|
||||
|
||||
const onMessageIdSearch = (e: any) => {
|
||||
messageId.value = e.target.value
|
||||
resetFilter()
|
||||
searchMessages()
|
||||
}
|
||||
|
||||
const onPayloadSearch = (e: any) => {
|
||||
payload.value = e.target.value
|
||||
resetFilter()
|
||||
searchMessages()
|
||||
}
|
||||
|
||||
const onClientMsgNoSearch = (e: any) => {
|
||||
clientMsgNo.value = e.target.value
|
||||
resetFilter()
|
||||
searchMessages()
|
||||
}
|
||||
|
||||
const resetFilter = () => {
|
||||
currentPage.value = 1
|
||||
offsetMessageId.value = 0
|
||||
offsetMessageSeq.value = 0
|
||||
}
|
||||
|
||||
// 下一页
|
||||
const nextPage = () => {
|
||||
if (messages.value.length < pageSize.value) {
|
||||
alert("没有更多数据了")
|
||||
return
|
||||
}
|
||||
currentPage.value += 1
|
||||
offsetMessageId.value = messages.value[messages.value.length - 1].message_id
|
||||
offsetMessageSeq.value = messages.value[messages.value.length - 1].message_seq
|
||||
pre.value = false
|
||||
searchMessages()
|
||||
}
|
||||
|
||||
// 上一页
|
||||
const prevPage = () => {
|
||||
if (currentPage.value <= 1) {
|
||||
return
|
||||
}
|
||||
currentPage.value -= 1
|
||||
offsetMessageId.value = messages.value[0].message_id
|
||||
offsetMessageSeq.value = messages.value[0].message_seq
|
||||
pre.value = true
|
||||
searchMessages()
|
||||
}
|
||||
|
||||
// 显示消息内容
|
||||
const onShowMessageContent = (message: any) => {
|
||||
content.value = base64Decode(message.payload)
|
||||
const dialog = document.getElementById('content') as HTMLDialogElement;
|
||||
dialog.showModal();
|
||||
}
|
||||
|
||||
// 显示消息编号
|
||||
const onShowClientMsgNo = (clientMsgNo: any) => {
|
||||
content.value = clientMsgNo
|
||||
const dialog = document.getElementById('content') as HTMLDialogElement;
|
||||
dialog.showModal();
|
||||
}
|
||||
|
||||
// 消息轨迹惦记
|
||||
const onMessageTrace = (clientMsgNo: string) => {
|
||||
if(!App.shard().systemSetting.messageTraceOn) {
|
||||
alert("消息追踪功能未开启,请查看官网文档: https://githubim.com")
|
||||
return
|
||||
}
|
||||
router.push(`/monitor/trace?clientMsgNo=${clientMsgNo}`)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<wk-page class="flex-col">
|
||||
<!-- S 查询条件 -->
|
||||
<div class="mb-12px pt-4px pb-4px card">
|
||||
<vxe-form v-bind="formOptions" v-on="formEvents">
|
||||
<template #action>
|
||||
<el-button native-type="submit" type="primary">查询</el-button>
|
||||
<el-button native-type="reset">重置</el-button>
|
||||
</template>
|
||||
</vxe-form>
|
||||
<div>
|
||||
<div class="overflow-x-auto h-5/6">
|
||||
<div class="flex flex-wrap gap-4">
|
||||
|
||||
<!-- 发送者 -->
|
||||
<div class="text-sm ml-10">
|
||||
<label>发送者</label>
|
||||
<input type="text" placeholder="输入" class="input input-bordered select-sm ml-2"
|
||||
v-on:change="onFromUidSearch" />
|
||||
</div>
|
||||
|
||||
<!-- 接受频道类型 -->
|
||||
<div class="text-sm ml-10">
|
||||
<label>接受频道</label>
|
||||
<select class="select select-bordered max-w-xs select-sm w-20 ml-2"
|
||||
v-on:change="onChannelTypeSearch" v-model="channelType">
|
||||
<option value="1">个人</option>
|
||||
<option value="2">群聊</option>
|
||||
<option value="3">客服</option>
|
||||
<option value="4">社区</option>
|
||||
<option value="5">话题</option>
|
||||
<option value="6">资讯</option>
|
||||
<option value="7">数据</option>
|
||||
</select>
|
||||
<input type="text" placeholder="输入" v-model="channelId" class="input input-bordered select-sm ml-2"
|
||||
v-on:change="onChannelIdSearch" />
|
||||
</div>
|
||||
|
||||
<!-- 消息id -->
|
||||
<div class="text-sm ml-10">
|
||||
<label>消息ID</label>
|
||||
<input type="text" placeholder="输入" class="input input-bordered select-sm ml-2"
|
||||
v-on:change="onMessageIdSearch" />
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- 节点 -->
|
||||
<!-- <div class="text-sm ml-3">
|
||||
<label>节点</label>
|
||||
<select class="select select-bordered max-w-xs select-sm w-40 ml-2" v-on:change="onNodeChange">
|
||||
<option value="0">所有</option>
|
||||
<option v-for="node in nodeTotal.data" :selected="node.id == selectedNodeId">{{ node.id }}
|
||||
</option>
|
||||
</select>
|
||||
</div> -->
|
||||
<!-- 消息内容 -->
|
||||
<div class="text-sm ml-10">
|
||||
<label>消息内容</label>
|
||||
<input type="text" placeholder="输入" class="input input-bordered select-sm ml-2"
|
||||
v-on:change="onPayloadSearch" />
|
||||
</div>
|
||||
<!-- 客户端唯一编号 -->
|
||||
<div class="text-sm ml-10">
|
||||
<label>客户端唯一编号</label>
|
||||
<input type="text" placeholder="输入" class="input input-bordered select-sm ml-2"
|
||||
v-on:change="onClientMsgNoSearch" />
|
||||
</div>
|
||||
</div>
|
||||
<table class="table mt-10 table-pin-rows">
|
||||
<!-- head -->
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
消息ID
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
消息序号
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
发送者
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
接受频道
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
接受频道类型
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
消息内容
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
发送时间
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
客户端唯一编号
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
操作
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- row 1 -->
|
||||
<tr v-for="message in messages">
|
||||
<td>{{ message.message_id }}</td>
|
||||
<td>{{ message.message_seq }}</td>
|
||||
<td>{{ message.from_uid }}</td>
|
||||
<td>{{ message.channel_id }}</td>
|
||||
<td>{{ message.channel_type }}</td>
|
||||
<td class="text-blue-800" v-on:click="() => onShowMessageContent(message)"><a href="#">{{
|
||||
ellipsis(base64Decode(message.payload), 40) }}</a></td>
|
||||
<td>{{ message.timestamp_format }}</td>
|
||||
<td class="text-blue-800" v-on:click="() => onShowClientMsgNo(message.client_msg_no)"><a
|
||||
href="#">{{ ellipsis(message.client_msg_no, 20) }}</a></td>
|
||||
|
||||
<td class="flex">
|
||||
<button class="btn btn-link btn-sm" v-on:click="()=>onMessageTrace(message.client_msg_no)">消息轨迹</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- <div class="flex flex-col gap-4 w-full mt-2" v-if="loading">
|
||||
<div class="skeleton h-6 w-full"></div>
|
||||
<div class="skeleton h-6 w-full"></div>
|
||||
<div class="skeleton h-6 w-full"></div>
|
||||
<div class="skeleton h-6 w-full"></div>
|
||||
<div class="skeleton h-6 w-full"></div>
|
||||
<div class="skeleton h-6 w-full"></div>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end mt-10 mr-10">
|
||||
<div className="join">
|
||||
<button :class="{ 'join-item btn': true }" v-on:click="prevPage">«</button>
|
||||
<button className="join-item btn">{{ currentPage }}</button>
|
||||
<button :class="{ 'join-item btn': true }" v-on:click="nextPage">»</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<dialog id="content" class="modal">
|
||||
<div class="modal-box flex flex-wrap gap-2">
|
||||
<div>{{ content }}</div>
|
||||
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
</div>
|
||||
<!-- E 查询条件 -->
|
||||
|
||||
<!-- S 表格 -->
|
||||
<div class="flex-1 card !pt-4px overflow-hidden">
|
||||
<vxe-grid ref="tableRef" v-bind="gridOptions">
|
||||
<template #tools>
|
||||
<el-space>
|
||||
<el-button type="primary" :disabled="hasPrev" @click="onPage(1)">上一页</el-button>
|
||||
<el-button type="primary" :disabled="hasNext" @click="onPage(0)">下一页</el-button>
|
||||
</el-space>
|
||||
</template>
|
||||
|
||||
<template #action="{ row }">
|
||||
<el-space>
|
||||
<el-button type="primary" link @click="onPageTrace(row)">消息轨迹</el-button>
|
||||
</el-space>
|
||||
</template>
|
||||
</vxe-grid>
|
||||
</div>
|
||||
<!-- E 表格 -->
|
||||
</wk-page>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
title: 消息
|
||||
</route>
|
||||
</template>
|
||||
@@ -1,247 +1,234 @@
|
||||
<script setup lang="ts">
|
||||
// API 接口
|
||||
import { dataApi } from '@/api/modules/data-api';
|
||||
// 组件
|
||||
import { VxeFormListeners, VxeFormProps } from 'vxe-pc-ui';
|
||||
import Allowlist from './channel/components/Allowlist.vue';
|
||||
import Denylist from './channel/components/Denylist.vue';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import API from '../../services/API';
|
||||
|
||||
import type { VxeGridInstance, VxeGridProps } from 'vxe-table';
|
||||
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
/**
|
||||
* 查询条件
|
||||
* */
|
||||
const formOptions = reactive<VxeFormProps<any>>({
|
||||
data: {
|
||||
uid: '',
|
||||
offset_created_at: null,
|
||||
limit: 20,
|
||||
pre: 0
|
||||
},
|
||||
items: [
|
||||
{
|
||||
field: 'uid',
|
||||
title: '用户UID',
|
||||
itemRender: { name: 'ElInput', props: { placeholder: '请输入用户UID' } }
|
||||
},
|
||||
|
||||
{
|
||||
align: 'center',
|
||||
slots: { default: 'action' }
|
||||
}
|
||||
]
|
||||
});
|
||||
/** 搜索事件 **/
|
||||
const formEvents: VxeFormListeners<any> = {
|
||||
/** 查询 **/
|
||||
submit() {
|
||||
tableRef.value?.commitProxy('query');
|
||||
},
|
||||
/** 重置 **/
|
||||
reset() {
|
||||
tableRef.value?.commitProxy('query');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 表格
|
||||
**/
|
||||
const tableRef = ref<VxeGridInstance<any>>();
|
||||
const userTotal = ref<any>({}); // 用户列表
|
||||
const currentPage = ref(1); // 当前页
|
||||
const hasPrev = ref<boolean>(false); // 是否有上一页
|
||||
const hasNext = ref<boolean>(true); // 是否有下一页
|
||||
const pageSize = ref(20); // 每页显示数量
|
||||
const offsetCreatedAt = ref(0); // 偏移量
|
||||
const pre = ref(false); // 上一页
|
||||
const uid = ref<string>(''); // 用户UID
|
||||
const currentUids = ref<string[]>() // 当前用户列表
|
||||
|
||||
const loadList = async (query: any) => {
|
||||
const res = await dataApi.users({ ...query });
|
||||
if (res.data) {
|
||||
hasNext.value = res.more !== 1;
|
||||
hasPrev.value = currentPage.value <= 1;
|
||||
return res.data;
|
||||
}
|
||||
return [];
|
||||
};
|
||||
const hasNext = ref<boolean>(true) // 是否有下一页
|
||||
const hasPrev = ref<boolean>(false) // 是否有上一页
|
||||
|
||||
const gridOptions = reactive<VxeGridProps<any>>({
|
||||
showOverflow: true,
|
||||
height: 'auto',
|
||||
border: true,
|
||||
stripe: true,
|
||||
rowConfig: {
|
||||
isCurrent: true,
|
||||
isHover: true
|
||||
},
|
||||
scrollY: {
|
||||
enabled: true,
|
||||
gt: 0
|
||||
},
|
||||
toolbarConfig: {
|
||||
slots: {
|
||||
buttons: 'tools'
|
||||
},
|
||||
refresh: {
|
||||
icon: 'vxe-icon-refresh',
|
||||
iconLoading: 'vxe-icon-refresh roll'
|
||||
},
|
||||
zoom: {
|
||||
iconIn: 'vxe-icon-fullscreen',
|
||||
iconOut: 'vxe-icon-minimize'
|
||||
},
|
||||
custom: true
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: () => {
|
||||
return loadList(formOptions.data);
|
||||
}
|
||||
}
|
||||
},
|
||||
columns: [
|
||||
{ type: 'seq', title: '序号', width: 54 },
|
||||
{ field: 'uid', title: '用户UID', minWidth: 220 },
|
||||
{ field: 'device_count', title: '设备数', minWidth: 120 },
|
||||
{ field: 'conn_count', title: '连接数', minWidth: 120 },
|
||||
{ field: 'send_msg_count', title: '发出消息数', minWidth: 120 },
|
||||
{ field: 'recv_msg_count', title: '接受消息数', minWidth: 120 },
|
||||
{ field: 'send_msg_bytes', title: '发出消息大小', minWidth: 120 },
|
||||
{ field: 'recv_msg_bytes', title: '接受消息大小', minWidth: 140 },
|
||||
{ field: 'created_at_format', title: '创建时间', minWidth: 140 },
|
||||
{ field: 'updated_at_format', title: '更新时间', minWidth: 140 },
|
||||
{ field: 'action', title: '操作', width: 240, fixed: 'right', slots: { default: 'action' } }
|
||||
]
|
||||
|
||||
onMounted(() => {
|
||||
searchUser()
|
||||
});
|
||||
|
||||
/**
|
||||
* 分页切换
|
||||
*/
|
||||
const onPage = (type: 0 | 1) => {
|
||||
// 下一页
|
||||
let offset_created_at = 0;
|
||||
const tableData = tableRef.value?.getData();
|
||||
if (type === 0) {
|
||||
currentPage.value = currentPage.value + 1;
|
||||
if (tableData && tableData.length > 0) {
|
||||
offset_created_at = tableData[tableData.length - 1].created_at;
|
||||
const searchUser = () => {
|
||||
|
||||
API.shared.users({
|
||||
uid: uid.value || "",
|
||||
offsetCreatedAt: offsetCreatedAt.value,
|
||||
pre: pre.value,
|
||||
limit: pageSize.value,
|
||||
}).then((res) => {
|
||||
userTotal.value = res
|
||||
hasNext.value = userTotal.value?.more === 1
|
||||
hasPrev.value = currentPage.value > 1
|
||||
if(pre.value) { // 如果是向上翻页,则有下页数据
|
||||
hasNext.value = true
|
||||
}
|
||||
}).catch((err) => {
|
||||
alert(err)
|
||||
})
|
||||
}
|
||||
|
||||
// 通过uid搜索
|
||||
const onUidSearch = (e: any) => {
|
||||
resetFilter()
|
||||
uid.value = e.target.value
|
||||
searchUser()
|
||||
}
|
||||
|
||||
// 显示白名单
|
||||
const onShowAllowlist = (uid:string) => {
|
||||
getAllowlist(uid).then(() => {
|
||||
const dialog = document.getElementById('userlist') as HTMLDialogElement;
|
||||
dialog.showModal();
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// 获取白名单列表
|
||||
const getAllowlist = (uid: string) => {
|
||||
return API.shared.allowlist(uid, 1).then((res) => {
|
||||
currentUids.value = res
|
||||
}).catch((err) => {
|
||||
alert(err)
|
||||
})
|
||||
}
|
||||
|
||||
// 显示黑名单
|
||||
const onShowDenylist = (uid:string) => {
|
||||
getDenylist(uid).then(() => {
|
||||
const dialog = document.getElementById('userlist') as HTMLDialogElement;
|
||||
dialog.showModal();
|
||||
})
|
||||
}
|
||||
|
||||
const getDenylist = (uid: string) => {
|
||||
return API.shared.denylist(uid, 1).then((res) => {
|
||||
currentUids.value = res
|
||||
}).catch((err) => {
|
||||
alert(err)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const resetFilter = () => {
|
||||
currentPage.value = 1
|
||||
pre.value = false
|
||||
offsetCreatedAt.value = 0
|
||||
}
|
||||
|
||||
// 上一页
|
||||
const prevPage = () => {
|
||||
if (currentPage.value <= 1) {
|
||||
hasPrev.value = false
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 上一页
|
||||
if (type === 1 && currentPage.value > 1) {
|
||||
currentPage.value = currentPage.value - 1;
|
||||
if (tableData && tableData.length > 0) {
|
||||
offset_created_at = tableData[0].created_at;
|
||||
hasPrev.value = true
|
||||
currentPage.value -= 1
|
||||
pre.value = true
|
||||
if (userTotal.value?.data?.length > 0) {
|
||||
offsetCreatedAt.value = userTotal.value.data[0].created_at
|
||||
}
|
||||
}
|
||||
searchUser()
|
||||
}
|
||||
|
||||
formOptions.data = {
|
||||
...formOptions.data,
|
||||
offset_created_at,
|
||||
pre: type
|
||||
};
|
||||
|
||||
tableRef.value?.commitProxy('query');
|
||||
};
|
||||
|
||||
/**
|
||||
* 白名单
|
||||
*/
|
||||
const modelAllowlist = ref(false);
|
||||
const channelIdAllowlist = ref('');
|
||||
const channelTypeAllowlist = ref(0);
|
||||
|
||||
const onAllowlist = (row: any) => {
|
||||
channelIdAllowlist.value = row.uid;
|
||||
channelTypeAllowlist.value = 1;
|
||||
modelAllowlist.value = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* 黑名单
|
||||
*/
|
||||
const modelDenylist = ref(false);
|
||||
const channelIdDenylist = ref('');
|
||||
const channelTypeDenylist = ref(0);
|
||||
|
||||
const onDenylist = (row: any) => {
|
||||
channelIdDenylist.value = row.uid;
|
||||
channelTypeDenylist.value = 1;
|
||||
modelDenylist.value = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* 设备
|
||||
* @param row
|
||||
*/
|
||||
const onDevice = (row: any) => {
|
||||
router.push({
|
||||
path: '/data/device',
|
||||
query: {
|
||||
uid: row.uid
|
||||
// 下一页
|
||||
const nextPage = () => {
|
||||
if (userTotal.value?.more === 0 && !pre.value) {
|
||||
return
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 最近会话
|
||||
* @param row
|
||||
*/
|
||||
const onRecentSession = (row: any) => {
|
||||
router.push({
|
||||
path: '/data/conversation',
|
||||
query: {
|
||||
uid: row.uid
|
||||
currentPage.value += 1
|
||||
pre.value = false
|
||||
if (userTotal.value?.data?.length > 0) {
|
||||
offsetCreatedAt.value = userTotal.value.data[userTotal.value.data.length - 1].created_at
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
searchUser()
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<wk-page class="flex-col">
|
||||
<!-- S 查询条件 -->
|
||||
<div class="mb-12px pt-4px pb-4px card">
|
||||
<vxe-form v-bind="formOptions" v-on="formEvents">
|
||||
<template #action>
|
||||
<el-button native-type="submit" type="primary">查询</el-button>
|
||||
<el-button native-type="reset">重置</el-button>
|
||||
</template>
|
||||
</vxe-form>
|
||||
<div>
|
||||
<div class="overflow-x-auto h-5/6">
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<div class="text-sm ml-4">
|
||||
<label>用户UID</label>
|
||||
<input type="text" placeholder="输入" class="input input-bordered select-sm ml-2"
|
||||
v-on:change="onUidSearch" v-model="uid" />
|
||||
</div>
|
||||
</div>
|
||||
<table class="table mt-5 table-pin-rows">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
用户UID
|
||||
</div>
|
||||
</th>
|
||||
<!-- <th>
|
||||
<div class="flex items-center">
|
||||
设备数
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
连接数
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
发出消息数
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
接受消息数
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
发出消息大小
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
接受消息大小
|
||||
</div>
|
||||
</th> -->
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
创建时间
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
更新时间
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="flex items-center">
|
||||
操作
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- row 1 -->
|
||||
<tr v-for="user in userTotal.data">
|
||||
<td>
|
||||
{{ user.uid }}
|
||||
</td>
|
||||
<!-- <td>{{ user.device_count }}</td>
|
||||
<td>{{ user.conn_count }}</td>
|
||||
<td>{{ user.send_msg_count }}</td>
|
||||
<td>{{ user.recv_msg_count }}</td>
|
||||
<td>{{ user.send_msg_bytes }}</td>
|
||||
<td>{{ user.recv_msg_bytes }}</td> -->
|
||||
<td>{{ user.created_at_format }}</td>
|
||||
<td>{{ user.updated_at_format }}</td>
|
||||
<td class="flex">
|
||||
<button class="btn btn-link btn-sm"
|
||||
@click="()=>onShowAllowlist(user.uid)">白名单</button>
|
||||
<button class="btn btn-link btn-sm"
|
||||
@click="()=>onShowDenylist(user.uid)">黑名单</button>
|
||||
<button class="btn btn-link btn-sm"
|
||||
@click="$router.push(`device?uid=${user.uid}`)">设备</button>
|
||||
<button class="btn btn-link btn-sm"
|
||||
@click="$router.push(`conversation?uid=${user.uid}`)">最近会话</button>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
<div class="flex justify-end mt-10 mr-10">
|
||||
<div className="join">
|
||||
<button :class="{ 'join-item btn': true, 'btn-disabled': !hasPrev }" v-on:click="prevPage">«</button>
|
||||
<button className="join-item btn">{{ currentPage }}</button>
|
||||
<button :class="{ 'join-item btn': true, 'btn-disabled': !hasNext }" v-on:click="nextPage">»</button>
|
||||
</div>
|
||||
</div>
|
||||
<dialog id="userlist" class="modal">
|
||||
<div class="modal-box flex flex-wrap gap-2">
|
||||
<div v-if="currentUids?.length == 0">无数据</div>
|
||||
<a class="link" v-for="uid in currentUids">{{ uid }}</a>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
</div>
|
||||
<!-- E 查询条件 -->
|
||||
|
||||
<!-- S 表格 -->
|
||||
<div class="flex-1 card !pt-4px overflow-hidden">
|
||||
<vxe-grid ref="tableRef" v-bind="gridOptions">
|
||||
<template #tools>
|
||||
<el-space>
|
||||
<el-button type="primary" :disabled="hasPrev" @click="onPage(1)">上一页</el-button>
|
||||
<el-button type="primary" :disabled="hasNext" @click="onPage(0)">下一页</el-button>
|
||||
</el-space>
|
||||
</template>
|
||||
|
||||
<template #action="{ row }">
|
||||
<el-space>
|
||||
<el-button type="primary" link @click="onAllowlist(row)">白名单</el-button>
|
||||
<el-button type="primary" link @click="onDenylist(row)">黑名单</el-button>
|
||||
<el-button type="primary" link @click="onDevice(row)">设备</el-button>
|
||||
<el-button type="primary" link @click="onRecentSession(row)">最近会话</el-button>
|
||||
</el-space>
|
||||
</template>
|
||||
</vxe-grid>
|
||||
</div>
|
||||
<!-- E 表格 -->
|
||||
<!-- 白名单 -->
|
||||
<Allowlist v-model="modelAllowlist" :channel-id="channelIdAllowlist" :channel-type="channelTypeAllowlist" />
|
||||
<!-- 黑名单 -->
|
||||
<Denylist v-model="modelDenylist" :channel-id="channelIdDenylist" :channel-type="channelTypeDenylist" />
|
||||
</wk-page>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
title: 用户
|
||||
</route>
|
||||
</template>
|
||||
@@ -1,102 +0,0 @@
|
||||
<script name="Allowlist" lang="ts" setup>
|
||||
// API 接口
|
||||
import { dataApi } from '@/api/modules/data-api.ts';
|
||||
|
||||
import type { VxeGridInstance, VxeGridProps } from 'vxe-table';
|
||||
|
||||
interface IProps {
|
||||
channelId: string;
|
||||
channelType: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Props
|
||||
*/
|
||||
const props = withDefaults(defineProps<IProps>(), {
|
||||
channelId: '',
|
||||
channelType: 0
|
||||
});
|
||||
|
||||
const showModel = defineModel<boolean>();
|
||||
const loadingModel = ref(false);
|
||||
|
||||
watch(
|
||||
() => showModel.value,
|
||||
val => {
|
||||
if (val) {
|
||||
if (props.channelId) {
|
||||
tableRef.value?.commitProxy('query');
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 表格
|
||||
*/
|
||||
interface ITableItem {
|
||||
uid: string;
|
||||
}
|
||||
|
||||
const tableRef = ref<VxeGridInstance<ITableItem>>();
|
||||
|
||||
const apiLoadList = async () => {
|
||||
const res = await dataApi.allowlist(props.channelId, props.channelType);
|
||||
const getData: ITableItem[] = [];
|
||||
if (res.length > 0) {
|
||||
res.map((item: string) => {
|
||||
getData.push({
|
||||
uid: item
|
||||
});
|
||||
});
|
||||
}
|
||||
return getData;
|
||||
};
|
||||
|
||||
const gridOptions = reactive<VxeGridProps<ITableItem>>({
|
||||
border: true,
|
||||
showOverflow: true,
|
||||
height: 'auto',
|
||||
stripe: true,
|
||||
rowConfig: { isCurrent: true, isHover: true },
|
||||
scrollY: { enabled: true, gt: 0 },
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: () => {
|
||||
return apiLoadList();
|
||||
}
|
||||
}
|
||||
},
|
||||
columns: [
|
||||
{ type: 'seq', title: '序号', width: 64 },
|
||||
{ field: 'uid', title: '白名单ID', minWidth: 160 }
|
||||
]
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<vxe-modal
|
||||
v-model="showModel"
|
||||
title="白名单"
|
||||
:width="640"
|
||||
:height="460"
|
||||
:confirm-closable="false"
|
||||
:padding="false"
|
||||
:loading="loadingModel"
|
||||
show-maximize
|
||||
>
|
||||
<wk-page class="flex-col wk-page-bg">
|
||||
<!-- S 表格 -->
|
||||
<div class="flex-1 card pt-0px overflow-hidden">
|
||||
<vxe-grid ref="tableRef" v-bind="gridOptions"></vxe-grid>
|
||||
</div>
|
||||
<!-- E 表格 -->
|
||||
</wk-page>
|
||||
</vxe-modal>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.wk-page-bg {
|
||||
background-color: var(--el-bg-color-page);
|
||||
}
|
||||
</style>
|
||||
@@ -1,102 +0,0 @@
|
||||
<script name="Denylist" lang="ts" setup>
|
||||
// API 接口
|
||||
import { dataApi } from '@/api/modules/data-api.ts';
|
||||
|
||||
import type { VxeGridInstance, VxeGridProps } from 'vxe-table';
|
||||
|
||||
interface IProps {
|
||||
channelId: string;
|
||||
channelType: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Props
|
||||
*/
|
||||
const props = withDefaults(defineProps<IProps>(), {
|
||||
channelId: '',
|
||||
channelType: 0
|
||||
});
|
||||
|
||||
const showModel = defineModel<boolean>();
|
||||
const loadingModel = ref(false);
|
||||
|
||||
watch(
|
||||
() => showModel.value,
|
||||
val => {
|
||||
if (val) {
|
||||
if (props.channelId) {
|
||||
tableRef.value?.commitProxy('query');
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 表格
|
||||
*/
|
||||
interface ITableItem {
|
||||
uid: string;
|
||||
}
|
||||
|
||||
const tableRef = ref<VxeGridInstance<ITableItem>>();
|
||||
|
||||
const apiLoadList = async () => {
|
||||
const res = await dataApi.denylist(props.channelId, props.channelType);
|
||||
const getData: ITableItem[] = [];
|
||||
if (res.length > 0) {
|
||||
res.map((item: string) => {
|
||||
getData.push({
|
||||
uid: item
|
||||
});
|
||||
});
|
||||
}
|
||||
return getData;
|
||||
};
|
||||
|
||||
const gridOptions = reactive<VxeGridProps<ITableItem>>({
|
||||
border: true,
|
||||
showOverflow: true,
|
||||
height: 'auto',
|
||||
stripe: true,
|
||||
rowConfig: { isCurrent: true, isHover: true },
|
||||
scrollY: { enabled: true, gt: 0 },
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: () => {
|
||||
return apiLoadList();
|
||||
}
|
||||
}
|
||||
},
|
||||
columns: [
|
||||
{ type: 'seq', title: '序号', width: 64 },
|
||||
{ field: 'uid', title: '黑名单ID', minWidth: 160 }
|
||||
]
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<vxe-modal
|
||||
v-model="showModel"
|
||||
title="黑名单"
|
||||
:width="640"
|
||||
:height="460"
|
||||
:confirm-closable="false"
|
||||
:padding="false"
|
||||
:loading="loadingModel"
|
||||
show-maximize
|
||||
>
|
||||
<wk-page class="flex-col wk-page-bg">
|
||||
<!-- S 表格 -->
|
||||
<div class="flex-1 card pt-0px overflow-hidden">
|
||||
<vxe-grid ref="tableRef" v-bind="gridOptions"></vxe-grid>
|
||||
</div>
|
||||
<!-- E 表格 -->
|
||||
</wk-page>
|
||||
</vxe-modal>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.wk-page-bg {
|
||||
background-color: var(--el-bg-color-page);
|
||||
}
|
||||
</style>
|
||||
@@ -1,102 +0,0 @@
|
||||
<script name="Subscribers" lang="ts" setup>
|
||||
// API 接口
|
||||
import { dataApi } from '@/api/modules/data-api.ts';
|
||||
|
||||
import type { VxeGridInstance, VxeGridProps } from 'vxe-table';
|
||||
|
||||
interface IProps {
|
||||
channelId: string;
|
||||
channelType: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Props
|
||||
*/
|
||||
const props = withDefaults(defineProps<IProps>(), {
|
||||
channelId: '',
|
||||
channelType: 0
|
||||
});
|
||||
|
||||
const showModel = defineModel<boolean>();
|
||||
const loadingModel = ref(false);
|
||||
|
||||
watch(
|
||||
() => showModel.value,
|
||||
val => {
|
||||
if (val) {
|
||||
if (props.channelId) {
|
||||
tableRef.value?.commitProxy('query');
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 表格
|
||||
*/
|
||||
interface ITableItem {
|
||||
uid: string;
|
||||
}
|
||||
|
||||
const tableRef = ref<VxeGridInstance<ITableItem>>();
|
||||
|
||||
const apiLoadList = async () => {
|
||||
const res = await dataApi.subscribers(props.channelId, props.channelType);
|
||||
const getData: ITableItem[] = [];
|
||||
if (res.length > 0) {
|
||||
res.map((item: string) => {
|
||||
getData.push({
|
||||
uid: item
|
||||
});
|
||||
});
|
||||
}
|
||||
return getData;
|
||||
};
|
||||
|
||||
const gridOptions = reactive<VxeGridProps<ITableItem>>({
|
||||
border: true,
|
||||
showOverflow: true,
|
||||
height: 'auto',
|
||||
stripe: true,
|
||||
rowConfig: { isCurrent: true, isHover: true },
|
||||
scrollY: { enabled: true, gt: 0 },
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: () => {
|
||||
return apiLoadList();
|
||||
}
|
||||
}
|
||||
},
|
||||
columns: [
|
||||
{ type: 'seq', title: '序号', width: 64 },
|
||||
{ field: 'uid', title: '订阅者ID', minWidth: 160 }
|
||||
]
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<vxe-modal
|
||||
v-model="showModel"
|
||||
title="订阅者"
|
||||
:width="640"
|
||||
:height="460"
|
||||
:confirm-closable="false"
|
||||
:padding="false"
|
||||
:loading="loadingModel"
|
||||
show-maximize
|
||||
>
|
||||
<wk-page class="flex-col wk-page-bg">
|
||||
<!-- S 表格 -->
|
||||
<div class="flex-1 card pt-0px overflow-hidden">
|
||||
<vxe-grid ref="tableRef" v-bind="gridOptions"></vxe-grid>
|
||||
</div>
|
||||
<!-- E 表格 -->
|
||||
</wk-page>
|
||||
</vxe-modal>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.wk-page-bg {
|
||||
background-color: var(--el-bg-color-page);
|
||||
}
|
||||
</style>
|
||||
@@ -1,247 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
// API 接口
|
||||
import { dataApi } from '@/api/modules/data-api.ts';
|
||||
// 组件
|
||||
import { VxeFormListeners, VxeFormProps } from 'vxe-pc-ui';
|
||||
import Subscribers from './components/Subscribers.vue';
|
||||
import Allowlist from './components/Allowlist.vue';
|
||||
import Denylist from './components/Denylist.vue';
|
||||
|
||||
import type { VxeGridInstance, VxeGridProps } from 'vxe-table';
|
||||
import { CHANNEL_TYPE } from '@/constants';
|
||||
|
||||
/**
|
||||
* 查询条件
|
||||
* */
|
||||
const formOptions = reactive<VxeFormProps<any>>({
|
||||
data: {
|
||||
channel_type: null,
|
||||
channel_id: null,
|
||||
offset_created_at: null,
|
||||
limit: 20,
|
||||
pre: 0
|
||||
},
|
||||
items: [
|
||||
{
|
||||
field: 'channel_type',
|
||||
title: '频道类型',
|
||||
itemRender: {
|
||||
name: 'ElSelect',
|
||||
props: {
|
||||
placeholder: '请选择频道类型',
|
||||
style: { width: '180px' }
|
||||
},
|
||||
options: CHANNEL_TYPE
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'channel_id',
|
||||
title: '频道ID',
|
||||
itemRender: { name: 'ElInput', props: { placeholder: '请输入频道ID' } }
|
||||
},
|
||||
{
|
||||
align: 'center',
|
||||
slots: { default: 'action' }
|
||||
}
|
||||
]
|
||||
});
|
||||
/** 搜索事件 **/
|
||||
const formEvents: VxeFormListeners<any> = {
|
||||
/** 查询 **/
|
||||
submit() {
|
||||
tableRef.value?.commitProxy('query');
|
||||
},
|
||||
/** 重置 **/
|
||||
reset() {
|
||||
tableRef.value?.commitProxy('query');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 表格
|
||||
**/
|
||||
const tableRef = ref<VxeGridInstance<any>>();
|
||||
const currentPage = ref(1); // 当前页
|
||||
const hasPrev = ref<boolean>(false); // 是否有上一页
|
||||
const hasNext = ref<boolean>(true); // 是否有下一页
|
||||
|
||||
const loadList = async (query: any) => {
|
||||
const res = await dataApi.searchChannels({ ...query });
|
||||
if (res.data) {
|
||||
hasNext.value = res.more !== 1;
|
||||
hasPrev.value = currentPage.value <= 1;
|
||||
return res.data;
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const gridOptions = reactive<VxeGridProps<any>>({
|
||||
showOverflow: true,
|
||||
height: 'auto',
|
||||
border: true,
|
||||
stripe: true,
|
||||
rowConfig: {
|
||||
isCurrent: true,
|
||||
isHover: true
|
||||
},
|
||||
scrollY: {
|
||||
enabled: true,
|
||||
gt: 0
|
||||
},
|
||||
toolbarConfig: {
|
||||
slots: {
|
||||
buttons: 'tools'
|
||||
},
|
||||
refresh: {
|
||||
icon: 'vxe-icon-refresh',
|
||||
iconLoading: 'vxe-icon-refresh roll'
|
||||
},
|
||||
zoom: {
|
||||
iconIn: 'vxe-icon-fullscreen',
|
||||
iconOut: 'vxe-icon-minimize'
|
||||
},
|
||||
custom: true
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: () => {
|
||||
return loadList(formOptions.data);
|
||||
}
|
||||
}
|
||||
},
|
||||
columns: [
|
||||
{ type: 'seq', title: '序号', width: 54 },
|
||||
{ field: 'channel_id', title: '频道ID', minWidth: 220 },
|
||||
{
|
||||
field: 'channel_type',
|
||||
title: '频道类型',
|
||||
minWidth: 120,
|
||||
formatter({ cellValue }) {
|
||||
const item = CHANNEL_TYPE.find(item => item.value === cellValue);
|
||||
return item ? item.label : cellValue;
|
||||
}
|
||||
},
|
||||
{ field: 'subscriber_count', title: '订阅者数量', minWidth: 120 },
|
||||
{ field: 'allowlist_count', title: '黑名单数量', minWidth: 120 },
|
||||
{ field: 'status_format', title: '白名单数量', minWidth: 120 },
|
||||
{ field: 'last_msg_seq', title: '最大序号', minWidth: 120 },
|
||||
{ field: 'last_msg_time_format', title: '最后消息时间', minWidth: 140 },
|
||||
{ field: 'slot', title: '槽位', minWidth: 120 },
|
||||
{ field: 'created_at_format', title: '创建时间', minWidth: 140 },
|
||||
{ field: 'updated_at_format', title: '更新时间', minWidth: 140 },
|
||||
{ field: 'action', title: '操作', width: 184, fixed: 'right', slots: { default: 'action' } }
|
||||
]
|
||||
});
|
||||
|
||||
/**
|
||||
* 分页切换
|
||||
*/
|
||||
const onPage = (type: 0 | 1) => {
|
||||
// 下一页
|
||||
if (type === 0) {
|
||||
currentPage.value = currentPage.value + 1;
|
||||
}
|
||||
|
||||
// 上一页
|
||||
if (type === 1 && currentPage.value > 1) {
|
||||
currentPage.value = currentPage.value - 1;
|
||||
}
|
||||
|
||||
console.log(currentPage.value);
|
||||
formOptions.data = {
|
||||
...formOptions.data,
|
||||
pre: type
|
||||
};
|
||||
|
||||
tableRef.value?.commitProxy('query');
|
||||
};
|
||||
|
||||
/**
|
||||
* 订阅者
|
||||
*/
|
||||
const modelSubscribers = ref(false);
|
||||
const channelIdSubscribers = ref('');
|
||||
const channelTypeSubscribers = ref(0);
|
||||
|
||||
const onSubscribers = (row: any) => {
|
||||
channelIdSubscribers.value = row.channel_id;
|
||||
channelTypeSubscribers.value = row.channel_type;
|
||||
modelSubscribers.value = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* 白名单
|
||||
*/
|
||||
const modelAllowlist = ref(false);
|
||||
const channelIdAllowlist = ref('');
|
||||
const channelTypeAllowlist = ref(0);
|
||||
|
||||
const onAllowlist = (row: any) => {
|
||||
channelIdAllowlist.value = row.channel_id;
|
||||
channelTypeAllowlist.value = row.channel_type;
|
||||
modelAllowlist.value = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* 黑名单
|
||||
*/
|
||||
const modelDenylist = ref(false);
|
||||
const channelIdDenylist = ref('');
|
||||
const channelTypeDenylist = ref(0);
|
||||
|
||||
const onDenylist = (row: any) => {
|
||||
channelIdDenylist.value = row.channel_id;
|
||||
channelTypeDenylist.value = row.channel_type;
|
||||
modelDenylist.value = true;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<wk-page class="flex-col">
|
||||
<!-- S 查询条件 -->
|
||||
<div class="mb-12px pt-4px pb-4px card">
|
||||
<vxe-form v-bind="formOptions" v-on="formEvents">
|
||||
<template #action>
|
||||
<el-button native-type="submit" type="primary">查询</el-button>
|
||||
<el-button native-type="reset">重置</el-button>
|
||||
</template>
|
||||
</vxe-form>
|
||||
</div>
|
||||
<!-- E 查询条件 -->
|
||||
|
||||
<!-- S 表格 -->
|
||||
<div class="flex-1 card !pt-4px overflow-hidden">
|
||||
<vxe-grid ref="tableRef" v-bind="gridOptions">
|
||||
<template #tools>
|
||||
<el-space>
|
||||
<el-button type="primary" :disabled="hasPrev" @click="onPage(1)">上一页</el-button>
|
||||
<el-button type="primary" :disabled="hasNext" @click="onPage(0)">下一页</el-button>
|
||||
</el-space>
|
||||
</template>
|
||||
|
||||
<template #action="{ row }">
|
||||
<el-space>
|
||||
<el-button type="primary" link @click="onSubscribers(row)">订阅者</el-button>
|
||||
<el-button type="primary" link @click="onAllowlist(row)">白名单</el-button>
|
||||
<el-button type="primary" link @click="onDenylist(row)">黑名单</el-button>
|
||||
</el-space>
|
||||
</template>
|
||||
</vxe-grid>
|
||||
</div>
|
||||
<!-- E 表格 -->
|
||||
|
||||
<!-- 订阅者 -->
|
||||
<Subscribers v-model="modelSubscribers" :channel-id="channelIdSubscribers" :channel-type="channelTypeSubscribers" />
|
||||
<!-- 白名单 -->
|
||||
<Allowlist v-model="modelAllowlist" :channel-id="channelIdAllowlist" :channel-type="channelTypeAllowlist" />
|
||||
<!-- 黑名单 -->
|
||||
<Denylist v-model="modelDenylist" :channel-id="channelIdDenylist" :channel-type="channelTypeDenylist" />
|
||||
</wk-page>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
title: 频道
|
||||
</route>
|
||||
@@ -1,10 +0,0 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div>首页</div>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
title: 首页
|
||||
</route>
|
||||
57
web/src/pages/login/Login.vue
Normal file
@@ -0,0 +1,57 @@
|
||||
<script setup lang="ts">
|
||||
import App from '../../services/App.ts';
|
||||
import API from '../../services/API';
|
||||
import { ref } from 'vue';
|
||||
|
||||
const username = ref<string>('guest')
|
||||
const password = ref<string>('guest')
|
||||
|
||||
const onLogin = () => {
|
||||
console.log('login')
|
||||
|
||||
if(!username.value|| username.value.trim()=='') {
|
||||
alert('请输入用户名')
|
||||
return
|
||||
}
|
||||
|
||||
if(!password.value || password.value.trim()=='') {
|
||||
alert('请输入密码')
|
||||
return
|
||||
}
|
||||
|
||||
API.shared.login(username.value, password.value).then((res) => {
|
||||
console.log(res)
|
||||
const loginInfo = App.shard().loginInfo
|
||||
loginInfo.username = res.username
|
||||
loginInfo.token = res.token
|
||||
loginInfo.setPermissionFromStr(res.permissions)
|
||||
loginInfo.save()
|
||||
window.location.href = '/web'
|
||||
}).catch((err) => {
|
||||
console.log(err)
|
||||
alert(err.msg)
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col items-center justify-center h-full mb-20">
|
||||
<div>
|
||||
<img src="/logo_big.png" class="h-[80px] w-[80px]" />
|
||||
</div>
|
||||
<div class="flex flex-col items-center justify-center pt-10">
|
||||
<h1 class="text-[1.5rem] font-semibold">WuKongIM分布式管理系统</h1>
|
||||
<div class="mt-4 text-gray-500">演示账号: guest 密码:guest</div>
|
||||
<div class="mt-10">
|
||||
<input type="text" class="input border-gray-200 w-[20rem]" placeholder="用户名" v-model="username" >
|
||||
</div>
|
||||
<div class="mt-5">
|
||||
<input type="password" class="input border-gray-200 w-[20rem]" placeholder="密码" v-model="password" />
|
||||
</div>
|
||||
<div class="mt-5">
|
||||
<button class="btn btn-primary w-[10rem] mt-10" v-on:click="() => onLogin()">登录</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,123 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { useRouter } from 'vue-router';
|
||||
import type { FormInstance, FormRules } from 'element-plus';
|
||||
|
||||
import { useUserStore } from '@/stores/modules/user';
|
||||
|
||||
import { WK_CONFIG, HOME_URL } from '@/config';
|
||||
|
||||
// API接口
|
||||
import { loginApi } from '@/api/modules/login-api';
|
||||
|
||||
const APP_TITLE = WK_CONFIG.APP_TITLE_LOGIN;
|
||||
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
|
||||
interface RuleForm {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
// 登录表单
|
||||
const formRef = ref<FormInstance>();
|
||||
const loginForm = reactive<RuleForm>({
|
||||
username: '',
|
||||
password: ''
|
||||
});
|
||||
const rules = reactive<FormRules<RuleForm>>({
|
||||
username: [{ required: true, message: '请输入账号', trigger: 'blur' }],
|
||||
password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
|
||||
});
|
||||
const loginButLoading = ref(false);
|
||||
|
||||
// 登录
|
||||
const onLoginClick = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return;
|
||||
loginButLoading.value = true;
|
||||
await formEl.validate(async valid => {
|
||||
loginButLoading.value = false;
|
||||
if (valid) {
|
||||
const res = await loginApi.login(loginForm) as any;
|
||||
userStore.setUserInfo(res);
|
||||
userStore.setToken(res.token);
|
||||
userStore.setPermissions(res.permissions);
|
||||
await router.push(HOME_URL);
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="wh-full lex flex-1 flex-col justify-center items-center overflow-hidden">
|
||||
<div class="flex-center wh-full relative">
|
||||
<div class="login-bg"></div>
|
||||
<div class="flex flex-col items-center justify-center relative">
|
||||
<el-card class="w-460px !border-none" body-style="padding: 48px;">
|
||||
<div class="flex justify-center mb-12px">
|
||||
<img class="h-64px w-64px" src="/logo.png" alt="logo" />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center mb-18px">
|
||||
<h3 class="text-24px app-text-fg-high mt-0 mb-24px" style="align-self: flex-start; font-weight: 500">
|
||||
{{ APP_TITLE }}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<el-form ref="formRef" :model="loginForm" :rules="rules" size="large" autocomplete="on">
|
||||
<el-form-item prop="username">
|
||||
<el-input v-model="loginForm.username" placeholder="请输入账号">
|
||||
<template #prefix>
|
||||
<el-icon class="el-input__icon">
|
||||
<i-wk-people theme="outline" size="24" />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="password">
|
||||
<el-input
|
||||
v-model="loginForm.password"
|
||||
type="password"
|
||||
placeholder="请输入密码"
|
||||
show-password
|
||||
@keyup.enter="onLoginClick(formRef)"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon class="el-input__icon">
|
||||
<i-wk-lock theme="outline" size="24" />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<div class="flex justify-between">
|
||||
<div class="flex-initial">演示账号:guest 密码:guest</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="loginButLoading" class="w-full" @click="onLoginClick(formRef)">
|
||||
登录
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.login-bg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
background: linear-gradient(154deg, #07070915 30%, hsl(var(--primary) / 30%) 48%, #07070915 64%);
|
||||
filter: blur(100px);
|
||||
}
|
||||
</style>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
title: 登录
|
||||
layout: false
|
||||
</route>
|
||||
@@ -1,110 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
// API 接口
|
||||
import { clusterApi } from '@/api/modules/cluster-api';
|
||||
import { monitorApi } from '@/api/modules/monitor-api';
|
||||
|
||||
// 常量
|
||||
import {APP_DOC, LATEST_TIME} from '@/constants';
|
||||
|
||||
import { VxeFormListeners, VxeFormProps } from 'vxe-pc-ui';
|
||||
import { useUserStore } from '@/stores/modules/user';
|
||||
|
||||
const userStore = useUserStore();
|
||||
/**
|
||||
* 查询条件
|
||||
* */
|
||||
const nodeList = ref<any[]>([]);
|
||||
const formOptions = reactive<VxeFormProps<any>>({
|
||||
data: {
|
||||
node_id: 0,
|
||||
latest: 300
|
||||
},
|
||||
items: [
|
||||
{
|
||||
field: 'node_id',
|
||||
title: '节点',
|
||||
itemRender: {
|
||||
name: 'ElSelect',
|
||||
props: {
|
||||
placeholder: '请选择',
|
||||
style: {
|
||||
width: '180px'
|
||||
}
|
||||
},
|
||||
options: nodeList
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'latest',
|
||||
title: '时间',
|
||||
itemRender: {
|
||||
name: 'ElSelect',
|
||||
props: {
|
||||
placeholder: '请选择',
|
||||
style: {
|
||||
width: '180px'
|
||||
}
|
||||
},
|
||||
options: LATEST_TIME
|
||||
}
|
||||
},
|
||||
{
|
||||
align: 'center',
|
||||
slots: { default: 'action' }
|
||||
}
|
||||
]
|
||||
});
|
||||
/** 搜索事件 **/
|
||||
const formEvents: VxeFormListeners<any> = {
|
||||
/** 查询 **/
|
||||
submit() {
|
||||
loadMetrics();
|
||||
},
|
||||
/** 重置 **/
|
||||
reset() {
|
||||
formOptions.data = {
|
||||
node_id: 0,
|
||||
latest: 300
|
||||
};
|
||||
loadMetrics();
|
||||
}
|
||||
};
|
||||
|
||||
const getNodes = async () => {
|
||||
const nodes: any[] = [
|
||||
{
|
||||
value: 0,
|
||||
label: '所有'
|
||||
}
|
||||
];
|
||||
const res = await clusterApi.simpleNodes();
|
||||
if (res.data.length > 0) {
|
||||
res.data.map((item: any) => {
|
||||
nodes.push({
|
||||
value: item.id,
|
||||
label: item.id
|
||||
});
|
||||
});
|
||||
}
|
||||
nodeList.value = nodes;
|
||||
if (nodes.length > 0) {
|
||||
formOptions.data = {
|
||||
...formOptions.data,
|
||||
node_id: nodes[0].value
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
interface Series {
|
||||
name?: string;
|
||||
type?: string;
|
||||
data: Point[];
|
||||
}
|
||||
|
||||
interface Point {
|
||||
timestamp?: number;
|
||||
value?: number;
|
||||
}
|
||||
import MonitorPanel from '../../components/MonitorPanel.vue'
|
||||
import { onMounted, ref } from 'vue';
|
||||
import API from '../../services/API';
|
||||
import { Series } from '../../services/Model';
|
||||
import App from '../../services/App';
|
||||
|
||||
const connectionsRef = ref<Series[]>([]); // 连接数
|
||||
const onlineUserCountRef = ref<Series[]>([]); // 在线用户数
|
||||
@@ -118,232 +17,274 @@ const recvPacketBytesRateRef = ref<Series[]>([]); // 接收数据包字节数
|
||||
const connPacketCountRateRef = ref<Series[]>([]); // 连接数据包数
|
||||
const connPacketBytesRateRef = ref<Series[]>([]); // 连接数据包字节数
|
||||
|
||||
|
||||
const pingPacketCountRateRef = ref<Series[]>([]); // ping数据包数
|
||||
const pingPacketBytesRateRef = ref<Series[]>([]); // ping数据包字节数
|
||||
|
||||
const loadMetrics = () => {
|
||||
monitorApi.apppMetrics(formOptions.data).then(res => {
|
||||
const connections = [];
|
||||
const onlineUserCount = [];
|
||||
const onlineDeviceCount = [];
|
||||
const nodeTotal = ref<any>({}); // 节点列表
|
||||
const selectedNodeId = ref<number>(0) // 选中的节点ID
|
||||
const latest = ref<number>(60 * 5) // 最近时间
|
||||
|
||||
const sendPacketCountRate = [];
|
||||
const sendPacketBytesRate = [];
|
||||
const sendackPacketCountRate = [];
|
||||
const sendackPacketBytesRate = [];
|
||||
|
||||
const recvPacketCountRate = [];
|
||||
const recvPacketBytesRate = [];
|
||||
const recvackPacketCountRate = [];
|
||||
const recvackPacketBytesRate = [];
|
||||
|
||||
const connPacketCountRate = [];
|
||||
const connPacketBytesRate = [];
|
||||
const connackPacketCountRate = [];
|
||||
const connackPacketBytesRate = [];
|
||||
|
||||
const pingPacketCountRate = [];
|
||||
const pingPacketBytesRate = [];
|
||||
const pongPacketCountRate = [];
|
||||
const pongPacketBytesRate = [];
|
||||
for (let index = 0; index < res.length; index++) {
|
||||
const d = res[index];
|
||||
connections.push({ timestamp: d.timestamp, value: d.conn_count });
|
||||
onlineUserCount.push({ timestamp: d.timestamp, value: d.online_user_count });
|
||||
onlineDeviceCount.push({ timestamp: d.timestamp, value: d.online_device_count });
|
||||
|
||||
sendPacketCountRate.push({ timestamp: d.timestamp, value: d.send_packet_count_rate });
|
||||
sendPacketBytesRate.push({ timestamp: d.timestamp, value: d.send_packet_bytes_rate });
|
||||
sendackPacketCountRate.push({ timestamp: d.timestamp, value: d.sendack_packet_count_rate });
|
||||
sendackPacketBytesRate.push({ timestamp: d.timestamp, value: d.sendack_packet_bytes_rate });
|
||||
|
||||
recvPacketCountRate.push({ timestamp: d.timestamp, value: d.recv_packet_count_rate });
|
||||
recvPacketBytesRate.push({ timestamp: d.timestamp, value: d.recv_packet_bytes_rate });
|
||||
recvackPacketCountRate.push({ timestamp: d.timestamp, value: d.recvack_packet_count_rate });
|
||||
recvackPacketBytesRate.push({ timestamp: d.timestamp, value: d.recvack_packet_bytes_rate });
|
||||
|
||||
connPacketCountRate.push({ timestamp: d.timestamp, value: d.conn_packet_count_rate });
|
||||
connPacketBytesRate.push({ timestamp: d.timestamp, value: d.conn_packet_bytes_rate });
|
||||
connackPacketCountRate.push({ timestamp: d.timestamp, value: d.connack_packet_count_rate });
|
||||
connackPacketBytesRate.push({ timestamp: d.timestamp, value: d.connack_packet_bytes_rate });
|
||||
|
||||
pingPacketCountRate.push({ timestamp: d.timestamp, value: d.ping_packet_count_rate });
|
||||
pingPacketBytesRate.push({ timestamp: d.timestamp, value: d.ping_packet_bytes_rate });
|
||||
pongPacketCountRate.push({ timestamp: d.timestamp, value: d.pong_packet_count_rate });
|
||||
pongPacketBytesRate.push({ timestamp: d.timestamp, value: d.pong_packet_bytes_rate });
|
||||
}
|
||||
connectionsRef.value = [
|
||||
{
|
||||
name: '连接数',
|
||||
data: connections
|
||||
}
|
||||
];
|
||||
|
||||
pingPacketCountRateRef.value = [
|
||||
{
|
||||
name: 'ping',
|
||||
data: pingPacketCountRate
|
||||
},
|
||||
{
|
||||
name: 'pong',
|
||||
data: pongPacketCountRate
|
||||
}
|
||||
];
|
||||
pingPacketBytesRateRef.value = [
|
||||
{
|
||||
name: 'ping',
|
||||
data: pingPacketBytesRate
|
||||
},
|
||||
{
|
||||
name: 'pong',
|
||||
data: pongPacketBytesRate
|
||||
}
|
||||
];
|
||||
onlineUserCountRef.value = [
|
||||
{
|
||||
name: '在线用户',
|
||||
data: onlineUserCount
|
||||
}
|
||||
];
|
||||
sendPacketCountRateRef.value = [
|
||||
{
|
||||
name: '发送包',
|
||||
data: sendPacketCountRate
|
||||
},
|
||||
{
|
||||
name: '发送应答包',
|
||||
data: sendackPacketCountRate
|
||||
}
|
||||
];
|
||||
sendPacketBytesRateRef.value = [
|
||||
{
|
||||
name: '发送包',
|
||||
data: sendPacketBytesRate
|
||||
},
|
||||
{
|
||||
name: '发送应答包',
|
||||
data: sendackPacketBytesRate
|
||||
}
|
||||
];
|
||||
|
||||
recvPacketCountRateRef.value = [
|
||||
{
|
||||
name: '接受包',
|
||||
data: recvPacketCountRate
|
||||
},
|
||||
{
|
||||
name: '接受应答包',
|
||||
data: recvackPacketCountRate
|
||||
}
|
||||
];
|
||||
recvPacketBytesRateRef.value = [
|
||||
{
|
||||
name: '接受包',
|
||||
data: recvPacketBytesRate
|
||||
},
|
||||
{
|
||||
name: '接受应答包',
|
||||
data: recvackPacketBytesRate
|
||||
}
|
||||
];
|
||||
|
||||
connPacketCountRateRef.value = [
|
||||
{
|
||||
name: '连接包',
|
||||
data: connPacketCountRate
|
||||
},
|
||||
{
|
||||
name: '连接应答包',
|
||||
data: connackPacketCountRate
|
||||
}
|
||||
];
|
||||
connPacketBytesRateRef.value = [
|
||||
{
|
||||
name: '连接包',
|
||||
data: connPacketBytesRate
|
||||
},
|
||||
{
|
||||
name: '连接应答包',
|
||||
data: connackPacketBytesRate
|
||||
}
|
||||
];
|
||||
});
|
||||
};
|
||||
|
||||
const isShow = computed(() => {
|
||||
return userStore.systemSetting.prometheusOn;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// 是否开启prometheus
|
||||
if (!userStore.systemSetting.prometheusOn) {
|
||||
return;
|
||||
}
|
||||
|
||||
getNodes();
|
||||
loadMetrics();
|
||||
});
|
||||
if(!App.shard().systemSetting.prometheusOn) {
|
||||
return
|
||||
}
|
||||
|
||||
loadMetrics()
|
||||
API.shared.simpleNodes().then((res) => {
|
||||
nodeTotal.value = res
|
||||
}).catch((err) => {
|
||||
alert(err)
|
||||
})
|
||||
})
|
||||
|
||||
const loadMetrics = () => {
|
||||
API.shared.apppMetrics(selectedNodeId.value,latest.value).then((res) => {
|
||||
var connections = []
|
||||
var onlineUserCount = []
|
||||
var onlineDeviceCount = []
|
||||
|
||||
var sendPacketCountRate = []
|
||||
var sendPacketBytesRate = []
|
||||
var sendackPacketCountRate = []
|
||||
var sendackPacketBytesRate = []
|
||||
|
||||
var recvPacketCountRate = []
|
||||
var recvPacketBytesRate = []
|
||||
var recvackPacketCountRate = []
|
||||
var recvackPacketBytesRate = []
|
||||
|
||||
var connPacketCountRate = []
|
||||
var connPacketBytesRate = []
|
||||
var connackPacketCountRate = []
|
||||
var connackPacketBytesRate = []
|
||||
|
||||
var pingPacketCountRate = []
|
||||
var pingPacketBytesRate = []
|
||||
var pongPacketCountRate = []
|
||||
var pongPacketBytesRate = []
|
||||
for (let index = 0; index < res.length; index++) {
|
||||
const d = res[index];
|
||||
connections.push({ timestamp: d.timestamp, value: d.conn_count })
|
||||
onlineUserCount.push({ timestamp: d.timestamp, value: d.online_user_count })
|
||||
onlineDeviceCount.push({ timestamp: d.timestamp, value: d.online_device_count })
|
||||
|
||||
sendPacketCountRate.push({ timestamp: d.timestamp, value: d.send_packet_count_rate })
|
||||
sendPacketBytesRate.push({ timestamp: d.timestamp, value: d.send_packet_bytes_rate })
|
||||
sendackPacketCountRate.push({ timestamp: d.timestamp, value: d.sendack_packet_count_rate })
|
||||
sendackPacketBytesRate.push({ timestamp: d.timestamp, value: d.sendack_packet_bytes_rate })
|
||||
|
||||
recvPacketCountRate.push({ timestamp: d.timestamp, value: d.recv_packet_count_rate })
|
||||
recvPacketBytesRate.push({ timestamp: d.timestamp, value: d.recv_packet_bytes_rate })
|
||||
recvackPacketCountRate.push({ timestamp: d.timestamp, value: d.recvack_packet_count_rate })
|
||||
recvackPacketBytesRate.push({ timestamp: d.timestamp, value: d.recvack_packet_bytes_rate })
|
||||
|
||||
connPacketCountRate.push({ timestamp: d.timestamp, value: d.conn_packet_count_rate })
|
||||
connPacketBytesRate.push({ timestamp: d.timestamp, value: d.conn_packet_bytes_rate })
|
||||
connackPacketCountRate.push({ timestamp: d.timestamp, value: d.connack_packet_count_rate })
|
||||
connackPacketBytesRate.push({ timestamp: d.timestamp, value: d.connack_packet_bytes_rate })
|
||||
|
||||
pingPacketCountRate.push({ timestamp: d.timestamp, value: d.ping_packet_count_rate })
|
||||
pingPacketBytesRate.push({ timestamp: d.timestamp, value: d.ping_packet_bytes_rate })
|
||||
pongPacketCountRate.push({ timestamp: d.timestamp, value: d.pong_packet_count_rate })
|
||||
pongPacketBytesRate.push({ timestamp: d.timestamp, value: d.pong_packet_bytes_rate })
|
||||
}
|
||||
connectionsRef.value = [{
|
||||
name: "连接数",
|
||||
data: connections,
|
||||
|
||||
}]
|
||||
|
||||
pingPacketCountRateRef.value = [
|
||||
{
|
||||
name: "ping",
|
||||
data: pingPacketCountRate,
|
||||
},
|
||||
{
|
||||
name: "pong",
|
||||
data: pongPacketCountRate,
|
||||
},
|
||||
]
|
||||
pingPacketBytesRateRef.value = [
|
||||
{
|
||||
name: "ping",
|
||||
data: pingPacketBytesRate,
|
||||
},
|
||||
{
|
||||
name: "pong",
|
||||
data: pongPacketBytesRate,
|
||||
},
|
||||
]
|
||||
onlineUserCountRef.value = [
|
||||
{
|
||||
name: "在线用户",
|
||||
data: onlineUserCount,
|
||||
},
|
||||
]
|
||||
sendPacketCountRateRef.value = [
|
||||
{
|
||||
name: "发送包",
|
||||
data: sendPacketCountRate,
|
||||
},
|
||||
{
|
||||
name: "发送应答包",
|
||||
data: sendackPacketCountRate
|
||||
},
|
||||
]
|
||||
sendPacketBytesRateRef.value = [
|
||||
{
|
||||
name: "发送包",
|
||||
data: sendPacketBytesRate,
|
||||
},
|
||||
{
|
||||
name: "发送应答包",
|
||||
data: sendackPacketBytesRate
|
||||
},
|
||||
]
|
||||
|
||||
recvPacketCountRateRef.value = [
|
||||
{
|
||||
name: "接受包",
|
||||
data: recvPacketCountRate
|
||||
},
|
||||
{
|
||||
name: "接受应答包",
|
||||
data: recvackPacketCountRate,
|
||||
},
|
||||
]
|
||||
recvPacketBytesRateRef.value = [
|
||||
{
|
||||
name: "接受包",
|
||||
data: recvPacketBytesRate,
|
||||
},
|
||||
{
|
||||
name: "接受应答包",
|
||||
data: recvackPacketBytesRate
|
||||
},
|
||||
]
|
||||
|
||||
connPacketCountRateRef.value = [
|
||||
{
|
||||
name: "连接包",
|
||||
data: connPacketCountRate
|
||||
},
|
||||
{
|
||||
name: "连接应答包",
|
||||
data: connackPacketCountRate
|
||||
},
|
||||
]
|
||||
connPacketBytesRateRef.value = [
|
||||
{
|
||||
name: "连接包",
|
||||
data: connPacketBytesRate,
|
||||
},
|
||||
{
|
||||
name: "连接应答包",
|
||||
data: connackPacketBytesRate
|
||||
},
|
||||
]
|
||||
|
||||
}).catch((err) => {
|
||||
alert(err)
|
||||
})
|
||||
}
|
||||
|
||||
const onNodeChange = (e: any) => {
|
||||
selectedNodeId.value = e.target.value
|
||||
loadMetrics()
|
||||
}
|
||||
|
||||
const onLatestChange = (e: any) => {
|
||||
latest.value = e.target.value
|
||||
loadMetrics()
|
||||
}
|
||||
const onRefresh = () => {
|
||||
loadMetrics()
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<wk-page class="flex-col">
|
||||
<!-- S 查询条件 -->
|
||||
<div class="mb-12px pt-4px pb-4px card">
|
||||
<vxe-form v-bind="formOptions" v-on="formEvents">
|
||||
<template #action>
|
||||
<el-button native-type="submit" type="primary">查询</el-button>
|
||||
<el-button native-type="reset">重置</el-button>
|
||||
</template>
|
||||
</vxe-form>
|
||||
</div>
|
||||
<!-- E 查询条件 -->
|
||||
<div class="overflow-auto h-5/6">
|
||||
<div class="flex">
|
||||
<div class="text-sm ml-3">
|
||||
<label>节点</label>
|
||||
<select class="select select-bordered max-w-xs select-sm w-40 ml-2" v-on:change="onNodeChange">
|
||||
<option value="0" :selected="selectedNodeId==0">所有</option>
|
||||
<option v-for="node in nodeTotal.data" :selected="node.id == selectedNodeId">{{ node.id }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div v-if="isShow" class="flex-1 card overflow-hidden">
|
||||
<el-scrollbar>
|
||||
<div class="flex flex-wrap justify-left">
|
||||
<div class="w-320px h-320px shadow-md p-12px mr-12px mb-12px">
|
||||
<wk-monitor-panel :data="connectionsRef" title="长连接数" />
|
||||
</div>
|
||||
<div class="w-320px h-320px shadow-md p-12px mr-12px mb-12px">
|
||||
<wk-monitor-panel :data="onlineUserCountRef" title="在线用户" />
|
||||
</div>
|
||||
<div class="w-320px h-320px shadow-md p-12px mr-12px mb-12px">
|
||||
<wk-monitor-panel :data="sendPacketCountRateRef" title="发送包(个)" />
|
||||
</div>
|
||||
<div class="w-320px h-320px shadow-md p-12px mr-12px mb-12px">
|
||||
<wk-monitor-panel :data="sendPacketBytesRateRef" title="发送包(字节)" />
|
||||
</div>
|
||||
<div class="w-320px h-320px shadow-md p-12px mr-12px mb-12px">
|
||||
<wk-monitor-panel :data="recvPacketCountRateRef" title="接收包(个)" />
|
||||
</div>
|
||||
<div class="w-320px h-320px shadow-md p-12px mr-12px mb-12px">
|
||||
<wk-monitor-panel :data="recvPacketBytesRateRef" title="接收包字节(字节)" />
|
||||
</div>
|
||||
<div class="w-320px h-320px shadow-md p-12px mr-12px mb-12px">
|
||||
<wk-monitor-panel :data="connPacketCountRateRef" title="连接包(个)" />
|
||||
</div>
|
||||
<div class="w-320px h-320px shadow-md p-12px mr-12px mb-12px">
|
||||
<wk-monitor-panel :data="connPacketBytesRateRef" title="连接包字节(字节" />
|
||||
</div>
|
||||
<div class="w-320px h-320px shadow-md p-12px mr-12px mb-12px">
|
||||
<wk-monitor-panel :data="pingPacketCountRateRef" title="ping包(个)" />
|
||||
</div>
|
||||
<div class="w-320px h-320px shadow-md p-12px mr-12px mb-12px">
|
||||
<wk-monitor-panel :data="pingPacketBytesRateRef" title="ping包字节(字节)" />
|
||||
</div>
|
||||
<div class="text-sm ml-10">
|
||||
<select class="select select-bordered max-w-xs select-sm w-40 ml-2" v-on:change="onLatestChange">
|
||||
<option selected :value="60*5">过去5分钟</option>
|
||||
<option :value="60*30">过去30分钟</option>
|
||||
<option :value="60*60">过去1小时</option>
|
||||
<option :value="60*60*6">过去6小时</option>
|
||||
<option :value="60*60*24">过去1天</option>
|
||||
<option :value="60*60*24*3">过去3天</option>
|
||||
<option :value="60*60*24*7">过去7天</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn btn-sm ml-10" v-on:click="onRefresh">立马刷新</button>
|
||||
</div>
|
||||
<br />
|
||||
<div class="flex flex-wrap justify-left" v-if="App.shard().systemSetting.prometheusOn">
|
||||
<div class="pl-5">
|
||||
<div class="w-[30%] h-[20rem] min-w-[20rem] shadow-md p-5">
|
||||
<MonitorPanel :data="connectionsRef" title="长连接数" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="pl-5">
|
||||
<div class="w-[30%] h-[20rem] min-w-[20rem] shadow-md p-5">
|
||||
<MonitorPanel :data="onlineUserCountRef" title="在线用户" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="pl-5">
|
||||
<div class="w-[30%] h-[20rem] min-w-[20rem] shadow-md p-5">
|
||||
<MonitorPanel :data="sendPacketCountRateRef" title="发送包(个)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="pl-5">
|
||||
<div class="w-[30%] h-[20rem] min-w-[20rem] shadow-md p-5">
|
||||
<MonitorPanel :data="sendPacketBytesRateRef" title="发送包(字节)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="pl-5">
|
||||
<div class="w-[30%] h-[20rem] min-w-[20rem] shadow-md p-5">
|
||||
<MonitorPanel :data="recvPacketCountRateRef" title="接收包(个)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="pl-5">
|
||||
<div class="w-[30%] h-[20rem] min-w-[20rem] shadow-md p-5">
|
||||
<MonitorPanel :data="recvPacketBytesRateRef" title="接收包字节(字节)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="pl-5">
|
||||
<div class="w-[30%] h-[20rem] min-w-[20rem] shadow-md p-5">
|
||||
<MonitorPanel :data="connPacketCountRateRef" title="连接包(个)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="pl-5">
|
||||
<div class="w-[30%] h-[20rem] min-w-[20rem] shadow-md p-5">
|
||||
<MonitorPanel :data="connPacketBytesRateRef" title="连接包字节(字节)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="pl-5">
|
||||
<div class="w-[30%] h-[20rem] min-w-[20rem] shadow-md p-5">
|
||||
<MonitorPanel :data="pingPacketCountRateRef" title="ping包(个)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="pl-5">
|
||||
<div class="w-[30%] h-[20rem] min-w-[20rem] shadow-md p-5">
|
||||
<MonitorPanel :data="pingPacketBytesRateRef" title="ping包字节(字节)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text text-center mt-10 text-red-500" v-else>
|
||||
监控功能未开启,请查看官网文档 https://githubim.com
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
<div v-else class="flex-1 card flex items-center justify-center overflow-hidden">
|
||||
监控功能未开启,请查看官网文档:
|
||||
<el-link type="primary" :href="APP_DOC" target="_blank">{{ APP_DOC }}</el-link>
|
||||
</div>
|
||||
</wk-page>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
title: 应用
|
||||
</route>
|
||||
</template>
|
||||
@@ -1,67 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { monitorApi } from '@/api/modules/monitor-api';
|
||||
|
||||
// 常量
|
||||
import { APP_DOC, LATEST_TIME } from '@/constants';
|
||||
import MonitorPanel from '../../components/MonitorPanel.vue'
|
||||
import { onMounted, ref } from 'vue';
|
||||
import API from '../../services/API';
|
||||
import { Series,setSeries } from '../../services/Model';
|
||||
import App from '../../services/App';
|
||||
|
||||
import { setSeries } from '@/utils';
|
||||
const latest = ref<number>(60 * 5) // 最近时间
|
||||
|
||||
import { VxeFormListeners, VxeFormProps } from 'vxe-pc-ui';
|
||||
|
||||
import { useUserStore } from '@/stores/modules/user';
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
const formOptions = reactive<VxeFormProps<any>>({
|
||||
data: {
|
||||
latest: 300
|
||||
},
|
||||
items: [
|
||||
{
|
||||
field: 'latest',
|
||||
title: '时间',
|
||||
itemRender: {
|
||||
name: 'ElSelect',
|
||||
props: {
|
||||
placeholder: '请选择',
|
||||
style: {
|
||||
width: '180px'
|
||||
}
|
||||
},
|
||||
options: LATEST_TIME
|
||||
}
|
||||
},
|
||||
{
|
||||
align: 'center',
|
||||
slots: { default: 'action' }
|
||||
}
|
||||
]
|
||||
});
|
||||
/** 搜索事件 **/
|
||||
const formEvents: VxeFormListeners<any> = {
|
||||
/** 查询 **/
|
||||
submit() {
|
||||
loadMetrics();
|
||||
},
|
||||
/** 重置 **/
|
||||
reset() {
|
||||
formOptions.data = {
|
||||
latest: 300
|
||||
};
|
||||
loadMetrics();
|
||||
}
|
||||
};
|
||||
|
||||
interface Series {
|
||||
name?: string;
|
||||
type?: string;
|
||||
data: Point[];
|
||||
}
|
||||
|
||||
interface Point {
|
||||
timestamp?: number;
|
||||
value?: number;
|
||||
}
|
||||
const msgIncomingCountRateRef = ref<Series[]>([]);
|
||||
const msgIncomingBytesRateRef = ref<Series[]>([]);
|
||||
const msgOutgoingCountRateRef = ref<Series[]>([]);
|
||||
@@ -82,196 +28,269 @@ const msgSyncIncomingBytesRateRef = ref<Series[]>([]);
|
||||
const msgSyncOutgoingCountRateRef = ref<Series[]>([]);
|
||||
const msgSyncOutgoingBytesRateRef = ref<Series[]>([]);
|
||||
|
||||
const channelActiveCountRef = ref<Series[]>([]);
|
||||
const channelActiveCountRef = ref<Series[]>([])
|
||||
|
||||
const channelProposeCountRateRef = ref<Series[]>([]);
|
||||
const channelProposeFailedCountRate = ref<Series[]>([]);
|
||||
const channelProposeLatencyOver500msRate = ref<Series[]>([]);
|
||||
const channelProposeLatencyUnder500msRate = ref<Series[]>([]);
|
||||
const channelProposeCountRateRef = ref<Series[]>([])
|
||||
const channelProposeFailedCountRate = ref<Series[]>([])
|
||||
const channelProposeLatencyOver500msRate = ref<Series[]>([])
|
||||
const channelProposeLatencyUnder500msRate = ref<Series[]>([])
|
||||
|
||||
const msgPingIncomingCountRateRef = ref<Series[]>([]);
|
||||
const msgPingIncomingBytesRateRef = ref<Series[]>([]);
|
||||
const msgPingOutgoingCountRateRef = ref<Series[]>([]);
|
||||
const msgPingOutgoingBytesRateRef = ref<Series[]>([]);
|
||||
const msgPingIncomingCountRateRef = ref<Series[]>([])
|
||||
const msgPingIncomingBytesRateRef = ref<Series[]>([])
|
||||
const msgPingOutgoingCountRateRef = ref<Series[]>([])
|
||||
const msgPingOutgoingBytesRateRef = ref<Series[]>([])
|
||||
|
||||
const loadMetrics = () => {
|
||||
monitorApi.clusterMetrics(formOptions.data).then(res => {
|
||||
msgIncomingCountRateRef.value = [];
|
||||
msgIncomingBytesRateRef.value = [];
|
||||
msgOutgoingCountRateRef.value = [];
|
||||
msgOutgoingBytesRateRef.value = [];
|
||||
|
||||
sendPacketIncomingCountRateRef.value = [];
|
||||
sendPacketIncomingBytesRateRef.value = [];
|
||||
sendPacketOutgoingBytesRateRef.value = [];
|
||||
sendPacketOutgoingCountRateRef.value = [];
|
||||
|
||||
msgSyncIncomingCountRateRef.value = [];
|
||||
msgSyncIncomingBytesRateRef.value = [];
|
||||
msgSyncOutgoingCountRateRef.value = [];
|
||||
msgSyncOutgoingBytesRateRef.value = [];
|
||||
|
||||
channelActiveCountRef.value = [];
|
||||
|
||||
channelProposeCountRateRef.value = [];
|
||||
channelProposeFailedCountRate.value = [];
|
||||
channelProposeLatencyOver500msRate.value = [];
|
||||
channelProposeLatencyUnder500msRate.value = [];
|
||||
|
||||
msgPingIncomingCountRateRef.value = [];
|
||||
msgPingIncomingBytesRateRef.value = [];
|
||||
msgPingOutgoingCountRateRef.value = [];
|
||||
msgPingOutgoingBytesRateRef.value = [];
|
||||
|
||||
for (let index = 0; index < res.length; index++) {
|
||||
const d = res[index];
|
||||
setSeries('msg_incoming_count_rate', d, msgIncomingCountRateRef.value);
|
||||
setSeries('msg_incoming_bytes_rate', d, msgIncomingBytesRateRef.value);
|
||||
setSeries('msg_outgoing_count_rate', d, msgOutgoingCountRateRef.value);
|
||||
setSeries('msg_outgoing_bytes_rate', d, msgOutgoingBytesRateRef.value);
|
||||
|
||||
setSeries('channel_msg_incoming_count_rate', d, channelMsgIncomingCountRateRef.value);
|
||||
setSeries('channel_msg_incoming_bytes_rate', d, channelMsgIncomingBytesRateRef.value);
|
||||
setSeries('channel_msg_outgoing_count_rate', d, channelMsgOutgoingCountRateRef.value);
|
||||
setSeries('channel_msg_outgoing_bytes_rate', d, channelMsgOutgoingBytesRateRef.value);
|
||||
|
||||
setSeries('sendpacket_incoming_count_rate', d, sendPacketIncomingCountRateRef.value);
|
||||
setSeries('sendpacket_incoming_bytes_rate', d, sendPacketIncomingBytesRateRef.value);
|
||||
setSeries('sendpacket_outgoing_bytes_rate', d, sendPacketOutgoingBytesRateRef.value);
|
||||
setSeries('sendpacket_outgoing_count_rate', d, sendPacketOutgoingCountRateRef.value);
|
||||
|
||||
setSeries('msg_sync_incoming_bytes_rate', d, msgSyncIncomingBytesRateRef.value);
|
||||
setSeries('msg_sync_incoming_count_rate', d, msgSyncIncomingCountRateRef.value);
|
||||
setSeries('msg_sync_outgoing_bytes_rate', d, msgSyncOutgoingBytesRateRef.value);
|
||||
setSeries('msg_sync_outgoing_count_rate', d, msgSyncOutgoingCountRateRef.value);
|
||||
|
||||
setSeries('channel_active_count', d, channelActiveCountRef.value);
|
||||
|
||||
setSeries('channel_propose_count_rate', d, channelProposeCountRateRef.value);
|
||||
setSeries('channel_propose_failed_count_rate', d, channelProposeFailedCountRate.value);
|
||||
setSeries('channel_propose_latency_over_500ms_rate', d, channelProposeLatencyOver500msRate.value);
|
||||
setSeries('channel_propose_latency_under_500ms_rate', d, channelProposeLatencyUnder500msRate.value);
|
||||
|
||||
setSeries('msg_ping_incoming_count_rate', d, msgPingIncomingCountRateRef.value);
|
||||
setSeries('msg_ping_incoming_bytes_rate', d, msgPingIncomingBytesRateRef.value);
|
||||
setSeries('msg_ping_outgoing_count_rate', d, msgPingOutgoingCountRateRef.value);
|
||||
setSeries('msg_ping_outgoing_bytes_rate', d, msgPingOutgoingBytesRateRef.value);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const isShow = computed(() => {
|
||||
return userStore.systemSetting.prometheusOn;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// 是否开启prometheus
|
||||
if (!userStore.systemSetting.prometheusOn) {
|
||||
return;
|
||||
}
|
||||
if(!App.shard().systemSetting.prometheusOn) {
|
||||
return
|
||||
}
|
||||
loadMetrics()
|
||||
})
|
||||
|
||||
|
||||
const loadMetrics = () => {
|
||||
|
||||
API.shared.clusterMetrics(latest.value).then((res) => {
|
||||
msgIncomingCountRateRef.value = []
|
||||
msgIncomingBytesRateRef.value = []
|
||||
msgOutgoingCountRateRef.value = []
|
||||
msgOutgoingBytesRateRef.value = []
|
||||
|
||||
sendPacketIncomingCountRateRef.value = []
|
||||
sendPacketIncomingBytesRateRef.value = []
|
||||
sendPacketOutgoingBytesRateRef.value = []
|
||||
sendPacketOutgoingCountRateRef.value = []
|
||||
|
||||
msgSyncIncomingCountRateRef.value = []
|
||||
msgSyncIncomingBytesRateRef.value = []
|
||||
msgSyncOutgoingCountRateRef.value = []
|
||||
msgSyncOutgoingBytesRateRef.value = []
|
||||
|
||||
channelActiveCountRef.value = []
|
||||
|
||||
channelProposeCountRateRef.value = []
|
||||
channelProposeFailedCountRate.value = []
|
||||
channelProposeLatencyOver500msRate.value = []
|
||||
channelProposeLatencyUnder500msRate.value = []
|
||||
|
||||
msgPingIncomingCountRateRef.value = []
|
||||
msgPingIncomingBytesRateRef.value = []
|
||||
msgPingOutgoingCountRateRef.value = []
|
||||
msgPingOutgoingBytesRateRef.value = []
|
||||
|
||||
for (let index = 0; index < res.length; index++) {
|
||||
const d = res[index];
|
||||
setSeries("msg_incoming_count_rate", d, msgIncomingCountRateRef.value)
|
||||
setSeries("msg_incoming_bytes_rate", d, msgIncomingBytesRateRef.value)
|
||||
setSeries("msg_outgoing_count_rate", d, msgOutgoingCountRateRef.value)
|
||||
setSeries("msg_outgoing_bytes_rate", d, msgOutgoingBytesRateRef.value)
|
||||
|
||||
setSeries("channel_msg_incoming_count_rate", d, channelMsgIncomingCountRateRef.value)
|
||||
setSeries("channel_msg_incoming_bytes_rate", d, channelMsgIncomingBytesRateRef.value)
|
||||
setSeries("channel_msg_outgoing_count_rate", d, channelMsgOutgoingCountRateRef.value)
|
||||
setSeries("channel_msg_outgoing_bytes_rate", d, channelMsgOutgoingBytesRateRef.value)
|
||||
|
||||
setSeries("sendpacket_incoming_count_rate", d, sendPacketIncomingCountRateRef.value)
|
||||
setSeries("sendpacket_incoming_bytes_rate", d, sendPacketIncomingBytesRateRef.value)
|
||||
setSeries("sendpacket_outgoing_bytes_rate", d, sendPacketOutgoingBytesRateRef.value)
|
||||
setSeries("sendpacket_outgoing_count_rate", d, sendPacketOutgoingCountRateRef.value)
|
||||
|
||||
setSeries("msg_sync_incoming_bytes_rate", d, msgSyncIncomingBytesRateRef.value)
|
||||
setSeries("msg_sync_incoming_count_rate", d, msgSyncIncomingCountRateRef.value)
|
||||
setSeries("msg_sync_outgoing_bytes_rate", d, msgSyncOutgoingBytesRateRef.value)
|
||||
setSeries("msg_sync_outgoing_count_rate", d, msgSyncOutgoingCountRateRef.value)
|
||||
|
||||
setSeries("channel_active_count", d, channelActiveCountRef.value)
|
||||
|
||||
setSeries("channel_propose_count_rate", d, channelProposeCountRateRef.value)
|
||||
setSeries("channel_propose_failed_count_rate", d, channelProposeFailedCountRate.value)
|
||||
setSeries("channel_propose_latency_over_500ms_rate", d, channelProposeLatencyOver500msRate.value)
|
||||
setSeries("channel_propose_latency_under_500ms_rate", d, channelProposeLatencyUnder500msRate.value)
|
||||
|
||||
setSeries("msg_ping_incoming_count_rate", d, msgPingIncomingCountRateRef.value)
|
||||
setSeries("msg_ping_incoming_bytes_rate", d, msgPingIncomingBytesRateRef.value)
|
||||
setSeries("msg_ping_outgoing_count_rate", d, msgPingOutgoingCountRateRef.value)
|
||||
setSeries("msg_ping_outgoing_bytes_rate", d, msgPingOutgoingBytesRateRef.value)
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
const onLatestChange = (e: any) => {
|
||||
latest.value = e.target.value
|
||||
loadMetrics()
|
||||
}
|
||||
const onRefresh = () => {
|
||||
loadMetrics()
|
||||
}
|
||||
|
||||
loadMetrics();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<wk-page class="flex-col">
|
||||
<!-- S 查询条件 -->
|
||||
<div class="mb-12px pt-4px pb-4px card">
|
||||
<vxe-form v-bind="formOptions" v-on="formEvents">
|
||||
<template #action>
|
||||
<el-button native-type="submit" type="primary">查询</el-button>
|
||||
<el-button native-type="reset">重置</el-button>
|
||||
</template>
|
||||
</vxe-form>
|
||||
</div>
|
||||
<!-- E 查询条件 -->
|
||||
|
||||
<div v-if="isShow" class="flex-1 card overflow-hidden">
|
||||
<el-scrollbar>
|
||||
<div class="flex flex-wrap justify-left">
|
||||
<div class="w-320px h-320px shadow-md p-12px mr-12px mb-12px">
|
||||
<wk-monitor-panel :data="channelProposeCountRateRef" title="频道提案(次)" />
|
||||
</div>
|
||||
<div class="w-320px h-320px shadow-md p-12px mr-12px mb-12px">
|
||||
<wk-monitor-panel :data="channelProposeLatencyUnder500msRate" title="频道提案小于500ms(次)" />
|
||||
</div>
|
||||
<div class="w-320px h-320px shadow-md p-12px mr-12px mb-12px">
|
||||
<wk-monitor-panel :data="channelProposeLatencyOver500msRate" title="频道提案大于500ms(次)" />
|
||||
</div>
|
||||
<div class="w-320px h-320px shadow-md p-12px mr-12px mb-12px">
|
||||
<wk-monitor-panel :data="channelProposeFailedCountRate" title="频道失败提案(次)" />
|
||||
</div>
|
||||
<div class="w-320px h-320px shadow-md p-12px mr-12px mb-12px">
|
||||
<wk-monitor-panel :data="msgIncomingCountRateRef" title="流入总消息(个)" />
|
||||
</div>
|
||||
<div class="w-320px h-320px shadow-md p-12px mr-12px mb-12px">
|
||||
<wk-monitor-panel :data="msgIncomingBytesRateRef" title="流入总消息(字节)" />
|
||||
</div>
|
||||
<div class="w-320px h-320px shadow-md p-12px mr-12px mb-12px">
|
||||
<wk-monitor-panel :data="msgOutgoingCountRateRef" title="流出总消息(个)" />
|
||||
</div>
|
||||
<div class="w-320px h-320px shadow-md p-12px mr-12px mb-12px">
|
||||
<wk-monitor-panel :data="msgOutgoingBytesRateRef" title="流出总消息(字节)" />
|
||||
</div>
|
||||
<div class="w-320px h-320px shadow-md p-12px mr-12px mb-12px">
|
||||
<wk-monitor-panel :data="channelMsgIncomingBytesRateRef" title="流入频道消息(字节)" />
|
||||
</div>
|
||||
<div class="w-320px h-320px shadow-md p-12px mr-12px mb-12px">
|
||||
<wk-monitor-panel :data="channelMsgOutgoingCountRateRef" title="流出频道消息(个)" />
|
||||
</div>
|
||||
<div class="w-320px h-320px shadow-md p-12px mr-12px mb-12px">
|
||||
<wk-monitor-panel :data="channelMsgOutgoingBytesRateRef" title="流出频道消息(字节)" />
|
||||
</div>
|
||||
<div class="w-320px h-320px shadow-md p-12px mr-12px mb-12px">
|
||||
<wk-monitor-panel :data="msgSyncIncomingCountRateRef" title="流入同步消息(个)" />
|
||||
</div>
|
||||
<div class="w-320px h-320px shadow-md p-12px mr-12px mb-12px">
|
||||
<wk-monitor-panel :data="msgSyncIncomingBytesRateRef" title="流入同步消息(字节)" />
|
||||
</div>
|
||||
<div class="w-320px h-320px shadow-md p-12px mr-12px mb-12px">
|
||||
<wk-monitor-panel :data="msgSyncOutgoingCountRateRef" title="流出同步消息(个)" />
|
||||
</div>
|
||||
<div class="w-320px h-320px shadow-md p-12px mr-12px mb-12px">
|
||||
<wk-monitor-panel :data="msgOutgoingBytesRateRef" title="流出同步消息(字节)" />
|
||||
</div>
|
||||
<div class="w-320px h-320px shadow-md p-12px mr-12px mb-12px">
|
||||
<wk-monitor-panel :data="sendPacketIncomingCountRateRef" title="转入的发送包(个)" />
|
||||
</div>
|
||||
<div class="w-320px h-320px shadow-md p-12px mr-12px mb-12px">
|
||||
<wk-monitor-panel :data="sendPacketIncomingBytesRateRef" title="转入的发送包(字节)" />
|
||||
</div>
|
||||
<div class="w-320px h-320px shadow-md p-12px mr-12px mb-12px">
|
||||
<wk-monitor-panel :data="sendPacketOutgoingCountRateRef" title="转出的发送包(个)" />
|
||||
</div>
|
||||
<div class="w-320px h-320px shadow-md p-12px mr-12px mb-12px">
|
||||
<wk-monitor-panel :data="sendPacketOutgoingBytesRateRef" title="转出的发送包(字节)" />
|
||||
</div>
|
||||
<div class="w-320px h-320px shadow-md p-12px mr-12px mb-12px">
|
||||
<wk-monitor-panel :data="msgPingIncomingCountRateRef" title="流入的Ping(个)" />
|
||||
</div>
|
||||
<div class="w-320px h-320px shadow-md p-12px mr-12px mb-12px">
|
||||
<wk-monitor-panel :data="msgPingIncomingBytesRateRef" title="流入的Ping(字节)" />
|
||||
</div>
|
||||
<div class="w-320px h-320px shadow-md p-12px mr-12px mb-12px">
|
||||
<wk-monitor-panel :data="msgPingOutgoingCountRateRef" title="流出的Ping(个)" />
|
||||
</div>
|
||||
<div class="w-320px h-320px shadow-md p-12px mr-12px mb-12px">
|
||||
<wk-monitor-panel :data="msgPingOutgoingBytesRateRef" title="流出的Ping(字节)" />
|
||||
</div>
|
||||
<div class="overflow-auto h-5/6">
|
||||
<div class="flex">
|
||||
<div class="text-sm ml-10">
|
||||
<select class="select select-bordered max-w-xs select-sm w-40 ml-2" v-on:change="onLatestChange">
|
||||
<option selected :value="60 * 5">过去5分钟</option>
|
||||
<option :value="60 * 30">过去30分钟</option>
|
||||
<option :value="60 * 60">过去1小时</option>
|
||||
<option :value="60 * 60 * 6">过去6小时</option>
|
||||
<option :value="60 * 60 * 24">过去1天</option>
|
||||
<option :value="60 * 60 * 24 * 3">过去3天</option>
|
||||
<option :value="60 * 60 * 24 * 7">过去7天</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn btn-sm ml-10" v-on:click="onRefresh">立马刷新</button>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
<div v-else class="flex-1 card flex items-center justify-center overflow-hidden">
|
||||
监控功能未开启,请查看官网文档:
|
||||
<el-link type="primary" :href="APP_DOC" target="_blank">{{ APP_DOC }}</el-link>
|
||||
</div>
|
||||
</wk-page>
|
||||
</template>
|
||||
<br />
|
||||
<div class="flex flex-wrap justify-left" v-if="App.shard().systemSetting.prometheusOn">
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
<!-- propose -->
|
||||
<div class="pl-5">
|
||||
<div class="w-[30%] h-[20rem] min-w-[20rem] shadow-md p-5">
|
||||
<MonitorPanel :data="channelProposeCountRateRef" title="频道提案(次)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="pl-5">
|
||||
<div class="w-[30%] h-[20rem] min-w-[20rem] shadow-md p-5">
|
||||
<MonitorPanel :data="channelProposeLatencyUnder500msRate" title="频道提案小于500ms(次)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="pl-5">
|
||||
<div class="w-[30%] h-[20rem] min-w-[20rem] shadow-md p-5">
|
||||
<MonitorPanel :data="channelProposeLatencyOver500msRate" title="频道提案大于500ms(次)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="pl-5">
|
||||
<div class="w-[30%] h-[20rem] min-w-[20rem] shadow-md p-5">
|
||||
<MonitorPanel :data="channelProposeFailedCountRate" title="频道失败提案(次)" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- message -->
|
||||
<div class="pl-5">
|
||||
<div class="w-[30%] h-[20rem] min-w-[20rem] shadow-md p-5">
|
||||
<MonitorPanel :data="msgIncomingCountRateRef" title="流入总消息(个)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="pl-5">
|
||||
<div class="w-[30%] h-[20rem] min-w-[20rem] shadow-md p-5">
|
||||
<MonitorPanel :data="msgIncomingBytesRateRef" title="流入总消息(字节)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="pl-5">
|
||||
<div class="w-[30%] h-[20rem] min-w-[20rem] shadow-md p-5">
|
||||
<MonitorPanel :data="msgOutgoingCountRateRef" title="流出总消息(个)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="pl-5">
|
||||
<div class="w-[30%] h-[20rem] min-w-[20rem] shadow-md p-5">
|
||||
<MonitorPanel :data="msgOutgoingBytesRateRef" title="流出总消息(字节)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
title: 分布式
|
||||
</route>
|
||||
<!-- message -->
|
||||
<div class="pl-5">
|
||||
<div class="w-[30%] h-[20rem] min-w-[20rem] shadow-md p-5">
|
||||
<MonitorPanel :data="channelMsgIncomingCountRateRef" title="流入频道消息(个)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="pl-5">
|
||||
<div class="w-[30%] h-[20rem] min-w-[20rem] shadow-md p-5">
|
||||
<MonitorPanel :data="channelMsgIncomingBytesRateRef" title="流入频道消息(字节)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="pl-5">
|
||||
<div class="w-[30%] h-[20rem] min-w-[20rem] shadow-md p-5">
|
||||
<MonitorPanel :data="channelMsgOutgoingCountRateRef" title="流出频道消息(个)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="pl-5">
|
||||
<div class="w-[30%] h-[20rem] min-w-[20rem] shadow-md p-5">
|
||||
<MonitorPanel :data="channelMsgOutgoingBytesRateRef" title="流出频道消息(字节)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- channel active -->
|
||||
<div class="pl-5">
|
||||
<div class="w-[30%] h-[20rem] min-w-[20rem] shadow-md p-5">
|
||||
<MonitorPanel :data="channelActiveCountRef" title="激活的频道(个)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- sync -->
|
||||
<div class="pl-5">
|
||||
<div class="w-[30%] h-[20rem] min-w-[20rem] shadow-md p-5">
|
||||
<MonitorPanel :data="msgSyncIncomingCountRateRef" title="流入同步消息(个)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="pl-5">
|
||||
<div class="w-[30%] h-[20rem] min-w-[20rem] shadow-md p-5">
|
||||
<MonitorPanel :data="msgSyncIncomingBytesRateRef" title="流入同步消息(字节)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="pl-5">
|
||||
<div class="w-[30%] h-[20rem] min-w-[20rem] shadow-md p-5">
|
||||
<MonitorPanel :data="msgSyncOutgoingCountRateRef" title="流出同步消息(个)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="pl-5">
|
||||
<div class="w-[30%] h-[20rem] min-w-[20rem] shadow-md p-5">
|
||||
<MonitorPanel :data="msgOutgoingBytesRateRef" title="流出同步消息(字节)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- sendpacket -->
|
||||
<div class="pl-5">
|
||||
<div class="w-[30%] h-[20rem] min-w-[20rem] shadow-md p-5">
|
||||
<MonitorPanel :data="sendPacketIncomingCountRateRef" title="转入的发送包(个)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="pl-5">
|
||||
<div class="w-[30%] h-[20rem] min-w-[20rem] shadow-md p-5">
|
||||
<MonitorPanel :data="sendPacketIncomingBytesRateRef" title="转入的发送包(字节)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="pl-5">
|
||||
<div class="w-[30%] h-[20rem] min-w-[20rem] shadow-md p-5">
|
||||
<MonitorPanel :data="sendPacketOutgoingCountRateRef" title="转出的发送包(个)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="pl-5">
|
||||
<div class="w-[30%] h-[20rem] min-w-[20rem] shadow-md p-5">
|
||||
<MonitorPanel :data="sendPacketOutgoingBytesRateRef" title="转出的发送包(字节)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ping -->
|
||||
<div class="pl-5">
|
||||
<div class="w-[30%] h-[20rem] min-w-[20rem] shadow-md p-5">
|
||||
<MonitorPanel :data="msgPingIncomingCountRateRef" title="流入的Ping(个)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="pl-5">
|
||||
<div class="w-[30%] h-[20rem] min-w-[20rem] shadow-md p-5">
|
||||
<MonitorPanel :data="msgPingIncomingBytesRateRef" title="流入的Ping(字节)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="pl-5">
|
||||
<div class="w-[30%] h-[20rem] min-w-[20rem] shadow-md p-5">
|
||||
<MonitorPanel :data="msgPingOutgoingCountRateRef" title="流出的Ping(个)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="pl-5">
|
||||
<div class="w-[30%] h-[20rem] min-w-[20rem] shadow-md p-5">
|
||||
<MonitorPanel :data="msgPingOutgoingBytesRateRef" title="流出的Ping(字节)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text text-center mt-10 text-red-500" v-else>
|
||||
监控功能未开启,请查看官网文档 https://githubim.com
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
3
web/src/pages/monitor/DB.vue
Normal file
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
开发中,敬请期待
|
||||
</template>
|
||||
252
web/src/pages/monitor/Logs.vue
Normal file
@@ -0,0 +1,252 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, nextTick } from 'vue';
|
||||
import APIClient from '../../services/APIClient';
|
||||
import App from '../../services/App';
|
||||
import API from '../../services/API';
|
||||
|
||||
class LabelInput {
|
||||
name: string = ""
|
||||
operator: string = ""
|
||||
value: string = ""
|
||||
}
|
||||
|
||||
|
||||
const logs = ref(new Array<string>()) // 日志行
|
||||
const labelInputs = ref(new Array<LabelInput>()) // 属性输入
|
||||
const nodes = ref(new Array()) // 节点列表
|
||||
const nodeId = ref("") // 节点ID
|
||||
const logLevel = ref("") // 日志等级
|
||||
const time = ref("1h") // 时间
|
||||
const searchValue = ref("") // 搜索输入
|
||||
const tail = ref(false) // 是否实时刷新
|
||||
const ws = ref<WebSocket>()
|
||||
const preUrl = ref("")
|
||||
|
||||
// 创建WebSocket实例
|
||||
|
||||
let wsUrl = ""
|
||||
|
||||
if (APIClient.shared.config.apiURL.startsWith('http')) {
|
||||
wsUrl = APIClient.shared.config.apiURL.replace('http', 'ws')
|
||||
wsUrl = `${wsUrl}/cluster/logs/tail`
|
||||
} else {
|
||||
wsUrl = `ws://${window.location.host}/cluster/logs/tail`
|
||||
}
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
API.shared.simpleNodes().then((res) => {
|
||||
nodes.value = res.data
|
||||
|
||||
}).catch((err) => {
|
||||
alert(err)
|
||||
})
|
||||
|
||||
search()
|
||||
})
|
||||
|
||||
const search = () => {
|
||||
|
||||
if (ws.value) {
|
||||
ws.value.close()
|
||||
ws.value = undefined
|
||||
}
|
||||
|
||||
const u = new URL(wsUrl)
|
||||
u.searchParams.append("Authorization", `Bearer ${App.shard().loginInfo.token}`)
|
||||
|
||||
u.searchParams.append("level", logLevel.value)
|
||||
u.searchParams.append("time", time.value)
|
||||
u.searchParams.append("search", searchValue.value)
|
||||
u.searchParams.append("tail", tail.value ? "1" : "0")
|
||||
u.searchParams.append("node_id", nodeId.value.toString())
|
||||
u.searchParams.append("labels", JSON.stringify(labelInputs.value.filter((labelInput) => {
|
||||
return labelInput.name.length > 0 && labelInput.value.length > 0 && labelInput.operator.length > 0
|
||||
}).map((labelInput) => {
|
||||
return {
|
||||
name: labelInput.name,
|
||||
operator: labelInput.operator,
|
||||
value: labelInput.value
|
||||
}
|
||||
})))
|
||||
|
||||
const reqUrl = u.toString()
|
||||
if (reqUrl === preUrl.value) {
|
||||
return
|
||||
}
|
||||
preUrl.value = reqUrl
|
||||
|
||||
let loading = true // 加载日志中
|
||||
|
||||
|
||||
logs.value = new Array<string>() // 清空日志
|
||||
|
||||
ws.value = new WebSocket(reqUrl)
|
||||
ws.value.onopen = function () {
|
||||
console.log('WebSocket is open now.');
|
||||
};
|
||||
|
||||
ws.value.onmessage = function (event) {
|
||||
console.log('WebSocket message received:', event.data);
|
||||
|
||||
if (loading) {
|
||||
const results = JSON.parse(event.data)
|
||||
if (!tail.value) {
|
||||
logs.value.push(...results.map((log: string) => log.replace(/\n/g, '')));
|
||||
}
|
||||
loading = false
|
||||
scrollToBottom()
|
||||
}
|
||||
|
||||
if (tail.value) {
|
||||
const results = JSON.parse(event.data)
|
||||
logs.value.push(...results.map((log: string) => log.replace(/\n/g, '')));
|
||||
scrollToBottom()
|
||||
}
|
||||
};
|
||||
|
||||
ws.value.onclose = function (event) {
|
||||
console.log('WebSocket is closed now.', event);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
const scrollToBottom = () => {
|
||||
nextTick(() => {
|
||||
const el = document.querySelector("#logBox")
|
||||
if (el) {
|
||||
el.scrollTop = el.scrollHeight
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
const onAddLabelInput = () => {
|
||||
const labelInput = new LabelInput()
|
||||
labelInput.operator = "="
|
||||
labelInputs.value.push(labelInput)
|
||||
}
|
||||
|
||||
const onRemoveLabelInput = (index: number) => {
|
||||
labelInputs.value.splice(index, 1)
|
||||
search()
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div class="grid grid-rows-[auto,1fr,auto]">
|
||||
<div class="flex flex-wrap gap-4 mb-4">
|
||||
<div class="flex text-sm ml-4 items-center">
|
||||
<label>节点</label>
|
||||
<select class="select select-bordered select-sm ml-2 max-w-xs " v-model="nodeId" v-on:change="search">
|
||||
<option selected value="">All</option>
|
||||
<option v-for="node in nodes" :value="node.id">{{ node.id }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex text-sm ml-4 items-center">
|
||||
<label>日志等级</label>
|
||||
<select class="select select-bordered select-sm ml-2 max-w-xs " v-model="logLevel" v-on:change="search">
|
||||
<option selected value="">All</option>
|
||||
<option value="info">Info</option>
|
||||
<option value="warn">Warn</option>
|
||||
<option value="error">Error</option>
|
||||
<option value="trace">Trace</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center text-sm ml-4">
|
||||
<label>时间</label>
|
||||
<select class="select select-bordered select-sm ml-2 max-w-xs" v-model="time" v-on:change="search">
|
||||
<option selected value="5m">5分钟</option>
|
||||
<option value="30m">30分钟</option>
|
||||
<option value="1h">1小时</option>
|
||||
<option value="6h">6小时</option>
|
||||
<option value="12h">12小时</option>
|
||||
<option value="24h">24小时</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center text-sm ml-4">
|
||||
<label>搜索</label>
|
||||
<input type="text" placeholder="输入" class="input input-bordered select-sm ml-2" v-model="searchValue"
|
||||
v-on:change="search" />
|
||||
</div>
|
||||
<div class="text-sm ml-4 flex items-center">
|
||||
<label>实时刷新</label>
|
||||
<input type="checkbox" class="checkbox ml-2" v-model="tail" />
|
||||
</div>
|
||||
<div class="text-sm ml-4 flex items-center">
|
||||
<label>属性</label>
|
||||
<div class="flex flex-wrap">
|
||||
<div class="text-sm ml-2 border grid-rows-[auto,1fr] w-60 rounded"
|
||||
v-for="(labelInput, index) in labelInputs">
|
||||
<div class="flex justify-between border-b-[0.02rem] h-8 items-center">
|
||||
<div class="ml-2">属性筛选</div>
|
||||
<div class="cursor-pointer mr-2" v-on:click="() => onRemoveLabelInput(index)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24"
|
||||
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round" class="lucide lucide-x">
|
||||
<path d="M18 6 6 18" />
|
||||
<path d="m6 6 12 12" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-rows-3 p-2 space-y-1">
|
||||
<div class="flex items-center">
|
||||
<div class="w-20">属性名</div>
|
||||
<input type="text" class="input input-bordered w-28 h-8 text-sm"
|
||||
v-model="labelInput.name" v-on:change="search" />
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="w-20">操作</div>
|
||||
<select class="select select-bordered select-sm w-28 h-8" v-model="labelInput.operator">
|
||||
<option selected value="=">等于</option>
|
||||
<option value="~~">包含</option>
|
||||
<option value="!=">不等于</option>
|
||||
<option value="!~">不包含</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="w-20">属性值</div>
|
||||
<input type="text" class="input input-bordered w-28 h-8 text-sm"
|
||||
v-model="labelInput.value" v-on:change="search" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-square btn-md ml-2" v-on:click="onAddLabelInput">添加</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="mockup-code overflow-scroll text-sm w-full h-full">
|
||||
<pre v-for="log in logs">
|
||||
<code >{{ log }}</code>
|
||||
</pre>
|
||||
</div> -->
|
||||
<div class="overflow-hidden">
|
||||
<div class="coding inverse-toggle px-5 pt-4 shadow-lg text-gray-100 text-sm font-mono subpixel-antialiased
|
||||
bg-gray-800 pb-6 pt-4 rounded-lg leading-normal overflow-hidden h-full">
|
||||
<div class="top mb-2 flex">
|
||||
<div class="h-3 w-3 bg-red-500 rounded-full"></div>
|
||||
<div class="ml-2 h-3 w-3 bg-orange-300 rounded-full"></div>
|
||||
<div class="ml-2 h-3 w-3 bg-green-500 rounded-full"></div>
|
||||
</div>
|
||||
<div class="mt-4 overflow-scroll h-full" id="logBox">
|
||||
<p class="text whitespace-nowrap leading-6" v-for="log in logs">
|
||||
{{ log }}
|
||||
</p>
|
||||
<br /> <br />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
@@ -1,187 +1,149 @@
|
||||
<script setup lang="ts">
|
||||
import { monitorApi } from '@/api/modules/monitor-api';
|
||||
|
||||
// 常量
|
||||
import {APP_DOC, LATEST_TIME} from '@/constants';
|
||||
import MonitorPanel from '../../components/MonitorPanel.vue'
|
||||
import { onMounted, ref } from 'vue';
|
||||
import API from '../../services/API';
|
||||
import { Series, setSeries } from '../../services/Model';
|
||||
import App from '../../services/App';
|
||||
|
||||
import { setSeries } from '@/utils';
|
||||
const latest = ref<number>(60 * 5) // 最近时间
|
||||
|
||||
import { VxeFormListeners, VxeFormProps } from 'vxe-pc-ui';
|
||||
import { useUserStore } from '@/stores/modules/user';
|
||||
const intranetIncomingBytesRateRef = ref<Series[]>([])
|
||||
const intranetOutgoingBytesRateRef = ref<Series[]>([])
|
||||
|
||||
const userStore = useUserStore();
|
||||
const extranetIncomingBytesRateRef = ref<Series[]>([])
|
||||
const extranetOutgoingBytesRateRef = ref<Series[]>([])
|
||||
const memstatsAllocBytes = ref<Series[]>([])
|
||||
const goroutines = ref<Series[]>([])
|
||||
|
||||
const formOptions = reactive<VxeFormProps<any>>({
|
||||
data: {
|
||||
latest: 300
|
||||
},
|
||||
items: [
|
||||
{
|
||||
field: 'latest',
|
||||
title: '时间',
|
||||
itemRender: {
|
||||
name: 'ElSelect',
|
||||
props: {
|
||||
placeholder: '请选择',
|
||||
style: {
|
||||
width: '180px'
|
||||
}
|
||||
},
|
||||
options: LATEST_TIME
|
||||
}
|
||||
},
|
||||
{
|
||||
align: 'center',
|
||||
slots: { default: 'action' }
|
||||
}
|
||||
]
|
||||
});
|
||||
/** 搜索事件 **/
|
||||
const formEvents: VxeFormListeners<any> = {
|
||||
/** 查询 **/
|
||||
submit() {
|
||||
loadMetrics();
|
||||
},
|
||||
/** 重置 **/
|
||||
reset() {
|
||||
formOptions.data = {
|
||||
latest: 300
|
||||
};
|
||||
loadMetrics();
|
||||
}
|
||||
};
|
||||
const gcDurationSecondsCount = ref<Series[]>([])
|
||||
const cpuPercent = ref<Series[]>([])
|
||||
|
||||
interface Series {
|
||||
name?: string;
|
||||
type?: string;
|
||||
data: Point[];
|
||||
}
|
||||
|
||||
interface Point {
|
||||
timestamp?: number;
|
||||
value?: number;
|
||||
}
|
||||
|
||||
const intranetIncomingBytesRateRef = ref<Series[]>([]);
|
||||
const intranetOutgoingBytesRateRef = ref<Series[]>([]);
|
||||
|
||||
const extranetIncomingBytesRateRef = ref<Series[]>([]);
|
||||
const extranetOutgoingBytesRateRef = ref<Series[]>([]);
|
||||
const memstatsAllocBytes = ref<Series[]>([]);
|
||||
const goroutines = ref<Series[]>([]);
|
||||
|
||||
const gcDurationSecondsCount = ref<Series[]>([]);
|
||||
const cpuPercent = ref<Series[]>([]);
|
||||
|
||||
const filefdAllocated = ref<Series[]>([]); // 文件打开数
|
||||
|
||||
const loadMetrics = () => {
|
||||
monitorApi.systemMetrics(formOptions.data).then(res => {
|
||||
intranetIncomingBytesRateRef.value = [];
|
||||
intranetOutgoingBytesRateRef.value = [];
|
||||
extranetIncomingBytesRateRef.value = [];
|
||||
extranetOutgoingBytesRateRef.value = [];
|
||||
|
||||
memstatsAllocBytes.value = [];
|
||||
goroutines.value = [];
|
||||
gcDurationSecondsCount.value = [];
|
||||
cpuPercent.value = [];
|
||||
filefdAllocated.value = [];
|
||||
|
||||
var filefdAllocatedData = [];
|
||||
|
||||
for (let index = 0; index < res.length; index++) {
|
||||
const d = res[index];
|
||||
setSeries('intranet_incoming_bytes_rate', d, intranetIncomingBytesRateRef.value);
|
||||
setSeries('intranet_outgoing_bytes_rate', d, intranetOutgoingBytesRateRef.value);
|
||||
|
||||
setSeries('extranet_incoming_bytes_rate', d, extranetIncomingBytesRateRef.value);
|
||||
setSeries('extranet_outgoing_bytes_rate', d, extranetOutgoingBytesRateRef.value);
|
||||
setSeries('memstats_alloc_bytes', d, memstatsAllocBytes.value);
|
||||
setSeries('goroutines', d, goroutines.value);
|
||||
setSeries('gc_duration_seconds_count', d, gcDurationSecondsCount.value);
|
||||
setSeries('cpu_percent', d, cpuPercent.value);
|
||||
filefdAllocatedData.push({ timestamp: d.timestamp, value: d.filefd_allocated });
|
||||
}
|
||||
filefdAllocated.value = [
|
||||
{
|
||||
name: '文件打开数',
|
||||
data: filefdAllocatedData
|
||||
}
|
||||
];
|
||||
});
|
||||
};
|
||||
|
||||
const isShow = computed(() => {
|
||||
return userStore.systemSetting.prometheusOn;
|
||||
});
|
||||
const filefdAllocated = ref<Series[]>([]) // 文件打开数
|
||||
|
||||
onMounted(() => {
|
||||
// 是否开启prometheus
|
||||
if (!userStore.systemSetting.prometheusOn) {
|
||||
return;
|
||||
}
|
||||
if (!App.shard().systemSetting.prometheusOn) {
|
||||
return
|
||||
}
|
||||
loadMetrics()
|
||||
})
|
||||
|
||||
const loadMetrics = () => {
|
||||
API.shared.systemMetrics(latest.value).then((res) => {
|
||||
intranetIncomingBytesRateRef.value = []
|
||||
intranetOutgoingBytesRateRef.value = []
|
||||
extranetIncomingBytesRateRef.value = []
|
||||
extranetOutgoingBytesRateRef.value = []
|
||||
|
||||
memstatsAllocBytes.value = []
|
||||
goroutines.value = []
|
||||
gcDurationSecondsCount.value = []
|
||||
cpuPercent.value = []
|
||||
filefdAllocated.value = []
|
||||
|
||||
var filefdAllocatedData = []
|
||||
|
||||
for (let index = 0; index < res.length; index++) {
|
||||
const d = res[index];
|
||||
setSeries("intranet_incoming_bytes_rate", d, intranetIncomingBytesRateRef.value)
|
||||
setSeries("intranet_outgoing_bytes_rate", d, intranetOutgoingBytesRateRef.value)
|
||||
|
||||
setSeries("extranet_incoming_bytes_rate", d, extranetIncomingBytesRateRef.value)
|
||||
setSeries("extranet_outgoing_bytes_rate", d, extranetOutgoingBytesRateRef.value)
|
||||
setSeries("memstats_alloc_bytes", d, memstatsAllocBytes.value)
|
||||
setSeries("goroutines", d, goroutines.value)
|
||||
setSeries("gc_duration_seconds_count", d, gcDurationSecondsCount.value)
|
||||
setSeries("cpu_percent", d, cpuPercent.value)
|
||||
filefdAllocatedData.push({ timestamp: d.timestamp, value: d.filefd_allocated })
|
||||
|
||||
}
|
||||
filefdAllocated.value = [{
|
||||
name: "文件打开数",
|
||||
data: filefdAllocatedData
|
||||
}]
|
||||
|
||||
}).catch((err) => {
|
||||
alert(err)
|
||||
|
||||
})
|
||||
}
|
||||
const onLatestChange = (e: any) => {
|
||||
latest.value = e.target.value
|
||||
loadMetrics()
|
||||
}
|
||||
const onRefresh = () => {
|
||||
loadMetrics()
|
||||
}
|
||||
|
||||
|
||||
loadMetrics();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<wk-page class="flex-col">
|
||||
<!-- S 查询条件 -->
|
||||
<div class="mb-12px pt-4px pb-4px card">
|
||||
<vxe-form v-bind="formOptions" v-on="formEvents">
|
||||
<template #action>
|
||||
<el-button native-type="submit" type="primary">查询</el-button>
|
||||
<el-button native-type="reset">重置</el-button>
|
||||
</template>
|
||||
</vxe-form>
|
||||
</div>
|
||||
<!-- E 查询条件 -->
|
||||
|
||||
<div v-if="isShow" class="flex-1 card overflow-hidden">
|
||||
<el-scrollbar>
|
||||
<div class="flex flex-wrap justify-left">
|
||||
<div class="w-320px h-320px shadow-md p-12px mr-12px mb-12px">
|
||||
<wk-monitor-panel :data="intranetIncomingBytesRateRef" title="内网流入流量" />
|
||||
</div>
|
||||
<div class="w-320px h-320px shadow-md p-12px mr-12px mb-12px">
|
||||
<wk-monitor-panel :data="intranetOutgoingBytesRateRef" title="内网流出流量" />
|
||||
</div>
|
||||
<div class="w-320px h-320px shadow-md p-12px mr-12px mb-12px">
|
||||
<wk-monitor-panel :data="extranetIncomingBytesRateRef" title="外网流入流量" />
|
||||
</div>
|
||||
<div class="w-320px h-320px shadow-md p-12px mr-12px mb-12px">
|
||||
<wk-monitor-panel :data="extranetOutgoingBytesRateRef" title="外网流出流量" />
|
||||
</div>
|
||||
<div class="w-320px h-320px shadow-md p-12px mr-12px mb-12px">
|
||||
<wk-monitor-panel :data="memstatsAllocBytes" title="内存" />
|
||||
</div>
|
||||
<div class="w-320px h-320px shadow-md p-12px mr-12px mb-12px">
|
||||
<wk-monitor-panel :data="goroutines" title="协程" />
|
||||
</div>
|
||||
<div class="w-320px h-320px shadow-md p-12px mr-12px mb-12px">
|
||||
<wk-monitor-panel :data="gcDurationSecondsCount" title="GC" />
|
||||
</div>
|
||||
<div class="w-320px h-320px shadow-md p-12px mr-12px mb-12px">
|
||||
<wk-monitor-panel :data="cpuPercent" title="CPU" />
|
||||
</div>
|
||||
<div class="w-320px h-320px shadow-md p-12px mr-12px mb-12px">
|
||||
<wk-monitor-panel :data="filefdAllocated" title="文件打开数" />
|
||||
</div>
|
||||
<div class="overflow-auto h-5/6">
|
||||
<div class="flex">
|
||||
<div class="text-sm ml-10">
|
||||
<select class="select select-bordered max-w-xs select-sm w-40 ml-2" v-on:change="onLatestChange">
|
||||
<option selected :value="60 * 5">过去5分钟</option>
|
||||
<option :value="60 * 30">过去30分钟</option>
|
||||
<option :value="60 * 60">过去1小时</option>
|
||||
<option :value="60 * 60 * 6">过去6小时</option>
|
||||
<option :value="60 * 60 * 24">过去1天</option>
|
||||
<option :value="60 * 60 * 24 * 3">过去3天</option>
|
||||
<option :value="60 * 60 * 24 * 7">过去7天</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn btn-sm ml-10" v-on:click="onRefresh">立马刷新</button>
|
||||
</div>
|
||||
<br />
|
||||
<div class="flex flex-wrap justify-left" v-if="App.shard().systemSetting.prometheusOn">
|
||||
<div class="pl-5">
|
||||
<div class="w-[30%] h-[20rem] min-w-[20rem] shadow-md p-5">
|
||||
<MonitorPanel :data="intranetIncomingBytesRateRef" title="内网流入流量" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="pl-5">
|
||||
<div class="w-[30%] h-[20rem] min-w-[20rem] shadow-md p-5">
|
||||
<MonitorPanel :data="intranetOutgoingBytesRateRef" title="内网流出流量" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="pl-5">
|
||||
<div class="w-[30%] h-[20rem] min-w-[20rem] shadow-md p-5">
|
||||
<MonitorPanel :data="extranetIncomingBytesRateRef" title="外网流入流量" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="pl-5">
|
||||
<div class="w-[30%] h-[20rem] min-w-[20rem] shadow-md p-5">
|
||||
<MonitorPanel :data="extranetOutgoingBytesRateRef" title="外网流出流量" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="pl-5">
|
||||
<div class="w-[30%] h-[20rem] min-w-[20rem] shadow-md p-5">
|
||||
<MonitorPanel :data="memstatsAllocBytes" title="内存" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="pl-5">
|
||||
<div class="w-[30%] h-[20rem] min-w-[20rem] shadow-md p-5">
|
||||
<MonitorPanel :data="goroutines" title="协程" />
|
||||
</div>
|
||||
</div class="pl-5">
|
||||
<div class="pl-5">
|
||||
<div class="w-[30%] h-[20rem] min-w-[20rem] shadow-md p-5">
|
||||
<MonitorPanel :data="gcDurationSecondsCount" title="GC" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="pl-5">
|
||||
<div class="w-[30%] h-[20rem] min-w-[20rem] shadow-md p-5">
|
||||
<MonitorPanel :data="cpuPercent" title="CPU" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="pl-5">
|
||||
<div class="w-[30%] h-[20rem] min-w-[20rem] shadow-md p-5">
|
||||
<MonitorPanel :data="filefdAllocated" title="文件打开数" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text text-center mt-10 text-red-500" v-else>
|
||||
监控功能未开启,请查看官网文档 https://githubim.com
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
<div v-else class="flex-1 card flex items-center justify-center overflow-hidden">
|
||||
监控功能未开启,请查看官网文档:
|
||||
<el-link type="primary" :href="APP_DOC" target="_blank">{{ APP_DOC }}</el-link>
|
||||
</div>
|
||||
</wk-page>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
title: 系统
|
||||
</route>
|
||||
</template>
|
||||
255
web/src/pages/monitor/Trace.vue
Normal file
@@ -0,0 +1,255 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from "vue";
|
||||
import { Graph } from '@antv/x6'
|
||||
import { register } from '@antv/x6-vue-shape'
|
||||
import SpanNode from '../../components/SpanNode.vue'
|
||||
import TextNode from '../../components/TextNode.vue'
|
||||
import RecvackTable from '../../components/RecvackTable.vue'
|
||||
import API from "../../services/API";
|
||||
import { useRouter } from "vue-router";
|
||||
import App from "../../services/App";
|
||||
|
||||
const clientMsgNo = ref('')
|
||||
const messageId = ref(0)
|
||||
|
||||
const nodeWidth = 180
|
||||
const nodeHeight = 70
|
||||
const error = ref('')
|
||||
const recvackResult = ref()
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const query = router.currentRoute.value.query; //查询参数
|
||||
|
||||
if (query.clientMsgNo) {
|
||||
clientMsgNo.value = query.clientMsgNo as string
|
||||
}
|
||||
|
||||
const modalContent = ref('')
|
||||
|
||||
register({
|
||||
shape: 'spanNode',
|
||||
width: nodeWidth,
|
||||
height: nodeHeight,
|
||||
component: SpanNode,
|
||||
})
|
||||
|
||||
register({
|
||||
shape: 'textNode',
|
||||
width: nodeWidth,
|
||||
height: nodeHeight,
|
||||
component: TextNode,
|
||||
})
|
||||
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
App.shard().loadSystemSettingIfNeed()
|
||||
if (clientMsgNo.value && clientMsgNo.value.length > 0) {
|
||||
drawFlow()
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const drawFlow = async () => {
|
||||
|
||||
const container = document.getElementById('container')
|
||||
const containerWidth = container!.offsetWidth
|
||||
const containerHeight = container!.offsetHeight
|
||||
|
||||
error.value = ''
|
||||
const result = await requestMessageTrace({
|
||||
width: containerWidth,
|
||||
height: containerHeight
|
||||
}).catch((e) => {
|
||||
error.value = e.msg
|
||||
})
|
||||
|
||||
|
||||
const nodes = []
|
||||
const edges = []
|
||||
if (result.nodes) {
|
||||
for (let i = 0; i < result.nodes.length; i++) {
|
||||
const node = result.nodes[i]
|
||||
nodes.push({
|
||||
id: node.id,
|
||||
shape: node.shape,
|
||||
x: node.x,
|
||||
y: node.y,
|
||||
data: {
|
||||
name: node.name,
|
||||
time: node.time,
|
||||
icon: node.icon,
|
||||
duration: node.duration,
|
||||
description: node.description,
|
||||
data: node.data,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (result.edges) {
|
||||
for (let i = 0; i < result.edges.length; i++) {
|
||||
const edge = result.edges[i]
|
||||
|
||||
const edgeObj = {
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
connector: { name: 'smooth' },
|
||||
attrs: {
|
||||
line: {
|
||||
stroke: '#1890ff',
|
||||
targetMarker: 'classic',
|
||||
strokeDasharray: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if (edge.shape == "dashed") {
|
||||
edgeObj.attrs.line.strokeDasharray = 5
|
||||
}
|
||||
edges.push(edgeObj)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const graph = new Graph({
|
||||
container: container!,
|
||||
width: containerWidth,
|
||||
height: containerHeight,
|
||||
panning: true,
|
||||
background: {
|
||||
color: '#F2F7FA',
|
||||
},
|
||||
grid: true,
|
||||
mousewheel: true,
|
||||
})
|
||||
|
||||
graph.addNodes(nodes)
|
||||
graph.addEdges(edges)
|
||||
|
||||
graph.on('node:click', async ({ node }) => {
|
||||
const nodeId = node.id as string
|
||||
if (nodeId === "processMessage") {
|
||||
const data = node.data.data
|
||||
modalContent.value = `
|
||||
发送者: ${data.uid} <br>
|
||||
发送设备: ${data.deviceId} <br>
|
||||
设备类型: ${data.deviceFlag} <br>
|
||||
设备等级: ${data.deviceLevel} <br>
|
||||
接受频道: ${data.channelId} <br>
|
||||
频道类型: ${data.channelType} <br>
|
||||
`
|
||||
}else if (nodeId.startsWith("deliverOnline") || nodeId.startsWith("deliverOffline")) {
|
||||
const data = node.data.data
|
||||
var uids: string[] = []
|
||||
if( data.uids) {
|
||||
data.uids.split(',').forEach((uid: string) => {
|
||||
uids.push(uid)
|
||||
})
|
||||
}
|
||||
modalContent.value = '<div class="flex flex-wrap space-x-2">'
|
||||
for (let i = 0; i < uids.length; i++) {
|
||||
modalContent.value += `
|
||||
<a href="#" class="inline-block"> ${uids[i]}</a>
|
||||
`
|
||||
}
|
||||
modalContent.value += '</div>'
|
||||
} else if (nodeId.startsWith('processRecvack')) {
|
||||
const data = node.data.data
|
||||
const nodeId = data.nodeId
|
||||
const messageId = data.messageId
|
||||
recvackResult.value = await requestMessageRecvackTraces({
|
||||
nodeId: nodeId,
|
||||
messageId: messageId,
|
||||
})
|
||||
if(recvackResult.value) {
|
||||
const dialog = document.getElementById('recvackTable') as HTMLDialogElement;
|
||||
dialog.showModal();
|
||||
}
|
||||
return
|
||||
} else {
|
||||
return
|
||||
}
|
||||
const dialog = document.getElementById('content') as HTMLDialogElement;
|
||||
dialog.showModal();
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
|
||||
const requestMessageTrace = ({
|
||||
width,
|
||||
height,
|
||||
}: any) => {
|
||||
return API.shared.messageTraces({
|
||||
clientMsgNo: clientMsgNo.value,
|
||||
messageId: messageId.value,
|
||||
width: width,
|
||||
height: height,
|
||||
since: 60 * 60 * 24,
|
||||
})
|
||||
}
|
||||
|
||||
const requestMessageRecvackTraces = ({
|
||||
nodeId,
|
||||
messageId,
|
||||
}:any) => {
|
||||
return API.shared.messageRecvackTraces({
|
||||
nodeId: nodeId,
|
||||
messageId: messageId,
|
||||
since: 60 * 60 * 24,
|
||||
})
|
||||
}
|
||||
|
||||
const onSearch = (e: any) => {
|
||||
if (!App.shard().systemSetting.messageTraceOn) {
|
||||
error.value = '消息追踪功能未开启,请查看官网文档 https://githubim.com'
|
||||
return
|
||||
}
|
||||
|
||||
clientMsgNo.value = e.target.value
|
||||
drawFlow()
|
||||
}
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div class="overflow-x-auto h-5/6">
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<div class="text-sm ml-4">
|
||||
<label>消息编号</label>
|
||||
<input type="text" placeholder="输入" class="input input-bordered select-sm ml-2" v-model="clientMsgNo"
|
||||
v-on:change="onSearch" />
|
||||
</div>
|
||||
<div class="text text-red-500">{{ error }}</div>
|
||||
</div>
|
||||
<br />
|
||||
<div id="container" class="w-full h-full"></div>
|
||||
|
||||
<dialog id="content" class="modal">
|
||||
<div class="modal-box flex flex-wrap gap-2">
|
||||
<div v-html="modalContent" class="break-words"></div>
|
||||
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<dialog id="recvackTable" class="modal">
|
||||
<div class="modal-box">
|
||||
<div class="min-w-[40rem]">
|
||||
<RecvackTable :data="recvackResult" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,39 +0,0 @@
|
||||
<script name="Content" lang="ts" setup>
|
||||
interface IProps {
|
||||
contentData: string;
|
||||
}
|
||||
/**
|
||||
* Props
|
||||
*/
|
||||
withDefaults(defineProps<IProps>(), {
|
||||
contentData: ''
|
||||
});
|
||||
|
||||
const showModel = defineModel<boolean>();
|
||||
const loadingModel = ref(false);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<vxe-modal
|
||||
v-model="showModel"
|
||||
title="内容"
|
||||
:width="560"
|
||||
:height="460"
|
||||
:confirm-closable="false"
|
||||
:padding="false"
|
||||
:loading="loadingModel"
|
||||
show-maximize
|
||||
>
|
||||
<wk-page class="flex-col wk-page-bg">
|
||||
<div class="flex-1 card pt-0px overflow-hidden">
|
||||
<div v-html="contentData" class="break-words"></div>
|
||||
</div>
|
||||
</wk-page>
|
||||
</vxe-modal>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.wk-page-bg {
|
||||
background-color: var(--el-bg-color-page);
|
||||
}
|
||||
</style>
|
||||
@@ -1,99 +0,0 @@
|
||||
<script name="RecvackTraces" lang="ts" setup>
|
||||
// API 接口
|
||||
import { monitorApi } from '@/api/modules/monitor-api.ts';
|
||||
|
||||
import type { VxeGridInstance, VxeGridProps } from 'vxe-table';
|
||||
|
||||
interface IProps {
|
||||
nodeId: string;
|
||||
messageId: string;
|
||||
}
|
||||
/**
|
||||
* Props
|
||||
*/
|
||||
const props = withDefaults(defineProps<IProps>(), {
|
||||
nodeId: '',
|
||||
messageId: ''
|
||||
});
|
||||
|
||||
const showModel = defineModel<boolean>();
|
||||
const loadingModel = ref(false);
|
||||
watch(
|
||||
() => showModel.value,
|
||||
val => {
|
||||
if (val) {
|
||||
if (props.nodeId) {
|
||||
tableRef.value?.commitProxy('query');
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
/**
|
||||
* 表格
|
||||
*/
|
||||
interface ITableItem {
|
||||
conn_id: string;
|
||||
uid: string;
|
||||
device_id: string;
|
||||
time: string;
|
||||
}
|
||||
|
||||
const tableRef = ref<VxeGridInstance<ITableItem>>();
|
||||
|
||||
const apiLoadList = async () => {
|
||||
const res = await monitorApi.messageRecvackTraces({ ...props, since: 60 * 60 * 24 });
|
||||
return res.data || [];
|
||||
};
|
||||
|
||||
const gridOptions = reactive<VxeGridProps<ITableItem>>({
|
||||
border: true,
|
||||
showOverflow: true,
|
||||
height: 'auto',
|
||||
stripe: true,
|
||||
rowConfig: { isCurrent: true, isHover: true },
|
||||
scrollY: { enabled: true, gt: 0 },
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: () => {
|
||||
return apiLoadList();
|
||||
}
|
||||
}
|
||||
},
|
||||
columns: [
|
||||
{ type: 'seq', title: '序号', width: 64 },
|
||||
{ field: 'replica_id', title: '连接ID', minWidth: 120 },
|
||||
{ field: 'role_format', title: '用户UID', minWidth: 120 },
|
||||
{ field: 'last_msg_seq', title: '设备ID', minWidth: 120 },
|
||||
{ field: 'last_msg_time_format', title: '收到时间', minWidth: 120 }
|
||||
]
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<vxe-modal
|
||||
v-model="showModel"
|
||||
title="内容"
|
||||
:width="860"
|
||||
:height="560"
|
||||
:confirm-closable="false"
|
||||
:padding="false"
|
||||
:loading="loadingModel"
|
||||
show-maximize
|
||||
>
|
||||
<wk-page class="flex-col wk-page-bg">
|
||||
<div class="flex-1 card pt-0px overflow-hidden">
|
||||
<!-- S 表格 -->
|
||||
<div class="flex-1 card pt-0px overflow-hidden">
|
||||
<vxe-grid ref="tableRef" v-bind="gridOptions"></vxe-grid>
|
||||
</div>
|
||||
<!-- E 表格 -->
|
||||
</div>
|
||||
</wk-page>
|
||||
</vxe-modal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.wk-page-bg {
|
||||
background-color: var(--el-bg-color-page);
|
||||
}
|
||||
</style>
|
||||
@@ -1,22 +0,0 @@
|
||||
<script name="SpanNode" lang="ts" setup>
|
||||
import { inject, onMounted, ref } from 'vue';
|
||||
const getNode = inject<() => any>('getNode');
|
||||
const data = ref({ name: '', time: '', icon: '', duration: 0, description: '' });
|
||||
onMounted(() => {
|
||||
const node = getNode!();
|
||||
data.value = node.data;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full h-full flex bg-white p-4px">
|
||||
<div v-if="data.icon" v-html="data.icon" class="pr-4px pt-8px"></div>
|
||||
<div class="flex-1">
|
||||
<div class="font-semibold flex items-center text-16px justify-between">
|
||||
<div>{{ data.name }}</div>
|
||||
</div>
|
||||
<div class="text text-sm text-gray-400 flex items-center">{{ data.description }}</div>
|
||||
<div class="text text-sm text-gray-400 flex items-center">{{ data.time }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,17 +0,0 @@
|
||||
<script name="TextNode" setup lang="ts">
|
||||
import { inject, onMounted, ref } from 'vue';
|
||||
const getNode = inject<() => any>('getNode');
|
||||
const data = ref({ name: '' });
|
||||
onMounted(() => {
|
||||
const node = getNode!();
|
||||
data.value = node.data;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full h-full border border-blue-500 rounded-md shadow-md bg-white">
|
||||
<div class="w-full h-full font-semibold flex items-center text-[0.9rem] justify-center">
|
||||
<div>{{ data.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,252 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
// API接口
|
||||
import { monitorApi } from '@/api/modules/monitor-api';
|
||||
import { VxeFormListeners, VxeFormProps } from 'vxe-pc-ui';
|
||||
|
||||
import SpanNode from './components/SpanNode.vue';
|
||||
import TextNode from './components/TextNode.vue';
|
||||
import Content from './components/Content.vue';
|
||||
import RecvackTraces from './components/RecvackTraces.vue';
|
||||
|
||||
import { Graph } from '@antv/x6';
|
||||
import { register } from '@antv/x6-vue-shape';
|
||||
|
||||
import { useRoute } from 'vue-router';
|
||||
const route = useRoute();
|
||||
|
||||
/**
|
||||
* 查询条件
|
||||
* */
|
||||
const formOptions = reactive<VxeFormProps<any>>({
|
||||
data: {
|
||||
message_id: 0,
|
||||
client_msg_no: null
|
||||
},
|
||||
items: [
|
||||
{
|
||||
field: 'client_msg_no',
|
||||
title: '消息编号',
|
||||
itemRender: {
|
||||
name: 'ElInput',
|
||||
props: { placeholder: '请输入消息编号' }
|
||||
}
|
||||
},
|
||||
{
|
||||
align: 'center',
|
||||
slots: { default: 'action' }
|
||||
}
|
||||
]
|
||||
});
|
||||
/** 搜索事件 **/
|
||||
const formEvents: VxeFormListeners<any> = {
|
||||
/** 查询 **/
|
||||
submit() {
|
||||
drawFlow();
|
||||
}
|
||||
};
|
||||
|
||||
const nodeWidth = 180;
|
||||
const nodeHeight = 70;
|
||||
const loading = ref(false);
|
||||
const error = ref('');
|
||||
|
||||
/**
|
||||
* 查看节点内容
|
||||
*/
|
||||
const contentModal = ref(false);
|
||||
const contentHtml = ref('');
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
const recvackModal = ref(false);
|
||||
const recvackNodeId = ref('');
|
||||
const recvackMessageId = ref('');
|
||||
|
||||
register({
|
||||
shape: 'spanNode',
|
||||
width: nodeWidth,
|
||||
height: nodeHeight,
|
||||
component: SpanNode
|
||||
});
|
||||
|
||||
register({
|
||||
shape: 'textNode',
|
||||
width: nodeWidth,
|
||||
height: nodeHeight,
|
||||
component: TextNode
|
||||
});
|
||||
|
||||
const requestMessageTrace = ({ width, height }: any) => {
|
||||
return monitorApi.messageTraces({
|
||||
...formOptions.data,
|
||||
width: width,
|
||||
height: height,
|
||||
since: 60 * 60 * 24
|
||||
});
|
||||
};
|
||||
|
||||
const drawFlow = async () => {
|
||||
const container = document.getElementById('container');
|
||||
const containerWidth = container!.offsetWidth;
|
||||
const containerHeight = container!.offsetHeight;
|
||||
|
||||
error.value = '';
|
||||
loading.value = true;
|
||||
const result = await requestMessageTrace({
|
||||
width: containerWidth,
|
||||
height: containerHeight
|
||||
}).catch(e => {
|
||||
loading.value = false;
|
||||
error.value = e.msg;
|
||||
});
|
||||
loading.value = false;
|
||||
|
||||
const nodes = [];
|
||||
const edges = [];
|
||||
if (result.nodes) {
|
||||
for (let i = 0; i < result.nodes.length; i++) {
|
||||
const node = result.nodes[i];
|
||||
nodes.push({
|
||||
id: node.id,
|
||||
shape: node.shape,
|
||||
x: node.x,
|
||||
y: node.y,
|
||||
data: {
|
||||
name: node.name,
|
||||
time: node.time,
|
||||
icon: node.icon,
|
||||
duration: node.duration,
|
||||
description: node.description,
|
||||
data: node.data
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (result.edges) {
|
||||
for (let i = 0; i < result.edges.length; i++) {
|
||||
const edge = result.edges[i];
|
||||
|
||||
const edgeObj = {
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
connector: { name: 'smooth' },
|
||||
attrs: {
|
||||
line: {
|
||||
stroke: '#1890ff',
|
||||
targetMarker: 'classic',
|
||||
strokeDasharray: 0
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (edge.shape == 'dashed') {
|
||||
edgeObj.attrs.line.strokeDasharray = 5;
|
||||
}
|
||||
edges.push(edgeObj);
|
||||
}
|
||||
}
|
||||
|
||||
const graph = new Graph({
|
||||
container: container!,
|
||||
width: containerWidth,
|
||||
height: containerHeight,
|
||||
panning: true,
|
||||
background: {
|
||||
color: '#F2F7FA'
|
||||
},
|
||||
grid: true,
|
||||
mousewheel: true
|
||||
});
|
||||
|
||||
graph.addNodes(nodes);
|
||||
graph.addEdges(edges);
|
||||
|
||||
graph.on('node:click', async ({ node }) => {
|
||||
const nodeId = node.id as string;
|
||||
if (nodeId === 'processMessage') {
|
||||
const data = node.data.data;
|
||||
contentHtml.value = `
|
||||
发送者: ${data.uid} <br>
|
||||
发送设备: ${data.deviceId} <br>
|
||||
设备类型: ${data.deviceFlag} <br>
|
||||
设备等级: ${data.deviceLevel} <br>
|
||||
接受频道: ${data.channelId} <br>
|
||||
频道类型: ${data.channelType} <br>
|
||||
`;
|
||||
} else if (nodeId.startsWith('deliverOnline') || nodeId.startsWith('deliverOffline')) {
|
||||
const data = node.data.data;
|
||||
let uids: string[] = [];
|
||||
if (data.uids) {
|
||||
data.uids.split(',').forEach((uid: string) => {
|
||||
uids.push(uid);
|
||||
});
|
||||
}
|
||||
contentHtml.value = '<div class="flex flex-wrap space-x-2">';
|
||||
for (let i = 0; i < uids.length; i++) {
|
||||
contentHtml.value += `
|
||||
<a href="#" class="inline-block"> ${uids[i]}</a>
|
||||
`;
|
||||
}
|
||||
contentHtml.value += '</div>';
|
||||
} else if (nodeId.startsWith('processRecvack')) {
|
||||
const data = node.data.data;
|
||||
|
||||
recvackNodeId.value = data.nodeId;
|
||||
recvackMessageId.value = data.messageId;
|
||||
recvackModal.value = true;
|
||||
return;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
contentModal.value = true;
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (route.query?.clientMsgNo) {
|
||||
formOptions.data = {
|
||||
...formOptions.data,
|
||||
client_msg_no: route.query.clientMsgNo
|
||||
};
|
||||
drawFlow();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<wk-page class="flex-col">
|
||||
<!-- S 查询条件 -->
|
||||
<div class="mb-12px pt-4px pb-4px card">
|
||||
<vxe-form v-bind="formOptions" v-on="formEvents">
|
||||
<template #action>
|
||||
<el-button native-type="submit" type="primary">查询</el-button>
|
||||
</template>
|
||||
</vxe-form>
|
||||
</div>
|
||||
<!-- E 查询条件 -->
|
||||
|
||||
<!-- S 消息轨迹 -->
|
||||
<div v-loading="loading" class="flex-1 card overflow-hidden">
|
||||
<div id="container" class="w-full h-full"></div>
|
||||
</div>
|
||||
<!-- E 消息轨迹 -->
|
||||
|
||||
<!-- 内容 -->
|
||||
<Content v-model="contentModal" :content-data="contentHtml" />
|
||||
<!-- 回执轨迹 -->
|
||||
<RecvackTraces v-model="recvackModal" :node-id="recvackNodeId" :message-id="recvackMessageId" />
|
||||
</wk-page>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.wk-page-bg {
|
||||
background-color: var(--el-bg-color-page);
|
||||
}
|
||||
</style>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
title: 消息追踪
|
||||
</route>
|
||||
@@ -1,84 +1,135 @@
|
||||
import { useUserStore } from '@/stores/modules/user';
|
||||
import { useAuthStore } from '@/stores/modules/auth';
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
import { LOGIN_URL, ROUTER_WHITE_LIST } from '@/config';
|
||||
import Login from '../pages/login/Login.vue'
|
||||
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
// ==================== cluster ====================
|
||||
import Node from '../pages/cluster/Node.vue'
|
||||
import Slot from '../pages/cluster/Slot.vue'
|
||||
import Channel from '../pages/cluster/Channel.vue'
|
||||
import Config from '../pages/cluster/Config.vue'
|
||||
import Log from '../pages/cluster/Log.vue'
|
||||
// import NodeDetail from '../pages/cluster/NodeDetail.vue'
|
||||
|
||||
import NProgress from '@/utils/nprogress';
|
||||
// ==================== data ====================
|
||||
import User from '../pages/data/User.vue'
|
||||
import Device from '../pages/data/Device.vue'
|
||||
import Connection from '../pages/data/Connection.vue'
|
||||
import Message from '../pages/data/Message.vue'
|
||||
import ChannelForData from '../pages/data/Channel.vue'
|
||||
import Conversation from '../pages/data/Conversation.vue'
|
||||
|
||||
// ==================== monitor ====================
|
||||
|
||||
import MonitorApp from '../pages/monitor/App.vue'
|
||||
import MonitorCluster from '../pages/monitor/Cluster.vue'
|
||||
import MonitorSystem from '../pages/monitor/System.vue'
|
||||
import TraceDB from '../pages/monitor/Trace.vue'
|
||||
import Logs from '../pages/monitor/Logs.vue'
|
||||
|
||||
import routes from './routers';
|
||||
|
||||
/**
|
||||
* @description 📚 路由参数配置简介
|
||||
* @param path ==> 菜单路径
|
||||
* @param name ==> 菜单别名
|
||||
* @param redirect ==> 重定向地址
|
||||
* @param component ==> 视图文件路径
|
||||
* @param meta ==> 菜单信息
|
||||
* @param meta.icon ==> 菜单图标
|
||||
* @param meta.title ==> 菜单标题
|
||||
* @param meta.activeMenu ==> 当前路由为详情页时,需要高亮的菜单
|
||||
* @param meta.isLink ==> 是否外链
|
||||
* @param meta.isHide ==> 是否隐藏
|
||||
* @param meta.isFull ==> 是否全屏(示例:数据大屏页面)
|
||||
* @param meta.isAffix ==> 是否固定在 tabs nav
|
||||
* @param meta.isKeepAlive ==> 是否缓存
|
||||
* */
|
||||
const router = createRouter({
|
||||
history: createWebHistory('/web'),
|
||||
routes,
|
||||
strict: false,
|
||||
scrollBehavior: () => ({ left: 0, top: 0 })
|
||||
});
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
// ==================== cluster ====================
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: Node
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
component: Login
|
||||
},
|
||||
{
|
||||
path: '/cluster/nodes',
|
||||
name: 'node',
|
||||
component: Node
|
||||
},
|
||||
// {
|
||||
// path: '/cluster/node/detail',
|
||||
// name: 'nodeDetail',
|
||||
// component: NodeDetail
|
||||
// },
|
||||
{
|
||||
path: '/cluster/slots',
|
||||
name: 'slot',
|
||||
component: Slot
|
||||
},
|
||||
{
|
||||
path: '/cluster/channels',
|
||||
name: 'channel',
|
||||
component: Channel
|
||||
},
|
||||
{
|
||||
path: '/cluster/config',
|
||||
name: 'config',
|
||||
component: Config
|
||||
},
|
||||
{
|
||||
path: '/cluster/log',
|
||||
name: 'clusterlog',
|
||||
component: Log
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 路由拦截 beforeEach
|
||||
* */
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
const authStore = useAuthStore();
|
||||
const userStore = useUserStore();
|
||||
// ==================== data ====================
|
||||
{
|
||||
path: '/data/connection',
|
||||
name: 'dataConnection',
|
||||
component: Connection
|
||||
},
|
||||
{
|
||||
path: '/data/user',
|
||||
name: 'dataUser',
|
||||
component: User
|
||||
},
|
||||
{
|
||||
path: '/data/device',
|
||||
name: 'dataDevice',
|
||||
component: Device
|
||||
},
|
||||
{
|
||||
path: '/data/message',
|
||||
name: 'dataMessage',
|
||||
component: Message
|
||||
},
|
||||
{
|
||||
path: '/data/channel',
|
||||
name: 'dataChannel',
|
||||
component: ChannelForData
|
||||
},
|
||||
{
|
||||
path: '/data/conversation',
|
||||
name: 'dataConversation',
|
||||
component: Conversation
|
||||
},
|
||||
// ==================== monitor ====================
|
||||
{
|
||||
path: '/monitor/app',
|
||||
name: 'monitorApp',
|
||||
component: MonitorApp
|
||||
},
|
||||
{
|
||||
path: '/monitor/cluster',
|
||||
name: 'monitorCluster',
|
||||
component: MonitorCluster
|
||||
},
|
||||
{
|
||||
path: '/monitor/system',
|
||||
name: 'monitorSystem',
|
||||
component: MonitorSystem
|
||||
},
|
||||
{
|
||||
path: '/monitor/trace',
|
||||
name: 'traceDB',
|
||||
component: TraceDB
|
||||
},
|
||||
{
|
||||
path: '/monitor/logs',
|
||||
name: 'logs',
|
||||
component: Logs
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
// NProgress 开始
|
||||
NProgress.start();
|
||||
|
||||
/** 如果已经登录并存在登录信息后不能跳转到路由白名单,而是继续保持在当前页面 */
|
||||
function toCorrectRoute() {
|
||||
ROUTER_WHITE_LIST.includes(to.fullPath) ? next(from.fullPath) : next();
|
||||
}
|
||||
|
||||
if (userStore.token) {
|
||||
// 正常访问页面
|
||||
if (!authStore.authMenuListGet.length) {
|
||||
await authStore.getAuthMenuList();
|
||||
}
|
||||
toCorrectRoute();
|
||||
} else {
|
||||
if (to.path !== LOGIN_URL) {
|
||||
if (ROUTER_WHITE_LIST.indexOf(to.path) !== -1) {
|
||||
next();
|
||||
} else {
|
||||
next({ path: LOGIN_URL, replace: true });
|
||||
}
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @description 路由跳转错误
|
||||
* */
|
||||
router.onError(error => {
|
||||
NProgress.done();
|
||||
console.warn('路由错误', error.message);
|
||||
});
|
||||
|
||||
/**
|
||||
* @description 路由跳转结束
|
||||
* */
|
||||
router.afterEach(() => {
|
||||
NProgress.done();
|
||||
});
|
||||
|
||||
export default router;
|
||||
export default router
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { staticRouter } from '@/router/static-router';
|
||||
|
||||
import { setupLayouts } from 'virtual:meta-layouts';
|
||||
|
||||
import generatedRoutes from 'virtual:generated-pages';
|
||||
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
const routes: RouteRecordRaw[] = setupLayouts(
|
||||
generatedRoutes.filter(item => {
|
||||
return item.meta?.enabled !== false && item.meta?.constant !== true && item.meta?.layout !== false;
|
||||
})
|
||||
);
|
||||
|
||||
export default [...staticRouter, ...routes];
|
||||