commit
6710fb92e4
57 changed files with 10382 additions and 0 deletions
@ -0,0 +1,27 @@
@@ -0,0 +1,27 @@
|
||||
.DS_Store |
||||
|
||||
.vscode |
||||
|
||||
# package manager log files |
||||
npm-debug.log* |
||||
yarn-debug.log* |
||||
yarn-error.log* |
||||
.npm |
||||
|
||||
|
||||
|
||||
# eslint cache |
||||
.eslintcache |
||||
# stylelint cache |
||||
.stylelintcache |
||||
|
||||
|
||||
# Log files |
||||
npm-debug.log* |
||||
yarn-debug.log* |
||||
yarn-error.log* |
||||
|
||||
/dist |
||||
/docker_output/ |
||||
/node_modules/ |
||||
/*.tar |
@ -0,0 +1,33 @@
@@ -0,0 +1,33 @@
|
||||
# vue-project |
||||
|
||||
This template should help get you started developing with Vue 3 in Vite. |
||||
|
||||
## Recommended IDE Setup |
||||
|
||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur). |
||||
|
||||
## 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 [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types. |
||||
|
||||
## Customize configuration |
||||
|
||||
See [Vite Configuration Reference](https://vite.dev/config/). |
||||
|
||||
## Project Setup |
||||
|
||||
```sh |
||||
npm install |
||||
``` |
||||
|
||||
### Compile and Hot-Reload for Development |
||||
|
||||
```sh |
||||
npm run dev |
||||
``` |
||||
|
||||
### Type-Check, Compile and Minify for Production |
||||
|
||||
```sh |
||||
npm run build |
||||
``` |
@ -0,0 +1,76 @@
@@ -0,0 +1,76 @@
|
||||
/* eslint-disable */ |
||||
/* prettier-ignore */ |
||||
// @ts-nocheck
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
// Generated by unplugin-auto-import
|
||||
// biome-ignore lint: disable
|
||||
export {} |
||||
declare global { |
||||
const EffectScope: typeof import('vue')['EffectScope'] |
||||
const computed: typeof import('vue')['computed'] |
||||
const createApp: typeof import('vue')['createApp'] |
||||
const customRef: typeof import('vue')['customRef'] |
||||
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] |
||||
const defineComponent: typeof import('vue')['defineComponent'] |
||||
const effectScope: typeof import('vue')['effectScope'] |
||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance'] |
||||
const getCurrentScope: typeof import('vue')['getCurrentScope'] |
||||
const h: typeof import('vue')['h'] |
||||
const inject: typeof import('vue')['inject'] |
||||
const isProxy: typeof import('vue')['isProxy'] |
||||
const isReactive: typeof import('vue')['isReactive'] |
||||
const isReadonly: typeof import('vue')['isReadonly'] |
||||
const isRef: typeof import('vue')['isRef'] |
||||
const markRaw: typeof import('vue')['markRaw'] |
||||
const nextTick: typeof import('vue')['nextTick'] |
||||
const onActivated: typeof import('vue')['onActivated'] |
||||
const onBeforeMount: typeof import('vue')['onBeforeMount'] |
||||
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave'] |
||||
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate'] |
||||
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] |
||||
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] |
||||
const onDeactivated: typeof import('vue')['onDeactivated'] |
||||
const onErrorCaptured: typeof import('vue')['onErrorCaptured'] |
||||
const onMounted: typeof import('vue')['onMounted'] |
||||
const onRenderTracked: typeof import('vue')['onRenderTracked'] |
||||
const onRenderTriggered: typeof import('vue')['onRenderTriggered'] |
||||
const onScopeDispose: typeof import('vue')['onScopeDispose'] |
||||
const onServerPrefetch: typeof import('vue')['onServerPrefetch'] |
||||
const onUnmounted: typeof import('vue')['onUnmounted'] |
||||
const onUpdated: typeof import('vue')['onUpdated'] |
||||
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup'] |
||||
const provide: typeof import('vue')['provide'] |
||||
const reactive: typeof import('vue')['reactive'] |
||||
const readonly: typeof import('vue')['readonly'] |
||||
const ref: typeof import('vue')['ref'] |
||||
const resolveComponent: typeof import('vue')['resolveComponent'] |
||||
const shallowReactive: typeof import('vue')['shallowReactive'] |
||||
const shallowReadonly: typeof import('vue')['shallowReadonly'] |
||||
const shallowRef: typeof import('vue')['shallowRef'] |
||||
const toRaw: typeof import('vue')['toRaw'] |
||||
const toRef: typeof import('vue')['toRef'] |
||||
const toRefs: typeof import('vue')['toRefs'] |
||||
const toValue: typeof import('vue')['toValue'] |
||||
const triggerRef: typeof import('vue')['triggerRef'] |
||||
const unref: typeof import('vue')['unref'] |
||||
const useAttrs: typeof import('vue')['useAttrs'] |
||||
const useCssModule: typeof import('vue')['useCssModule'] |
||||
const useCssVars: typeof import('vue')['useCssVars'] |
||||
const useId: typeof import('vue')['useId'] |
||||
const useLink: typeof import('vue-router')['useLink'] |
||||
const useModel: typeof import('vue')['useModel'] |
||||
const useRoute: typeof import('vue-router')['useRoute'] |
||||
const useRouter: typeof import('vue-router')['useRouter'] |
||||
const useSlots: typeof import('vue')['useSlots'] |
||||
const useTemplateRef: typeof import('vue')['useTemplateRef'] |
||||
const watch: typeof import('vue')['watch'] |
||||
const watchEffect: typeof import('vue')['watchEffect'] |
||||
const watchPostEffect: typeof import('vue')['watchPostEffect'] |
||||
const watchSyncEffect: typeof import('vue')['watchSyncEffect'] |
||||
} |
||||
// for type re-export
|
||||
declare global { |
||||
// @ts-ignore
|
||||
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue' |
||||
import('vue') |
||||
} |
@ -0,0 +1,55 @@
@@ -0,0 +1,55 @@
|
||||
/* eslint-disable */ |
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
// biome-ignore lint: disable
|
||||
export {} |
||||
|
||||
/* prettier-ignore */ |
||||
declare module 'vue' { |
||||
export interface GlobalComponents { |
||||
Components: typeof import('../src/components/Edfs-button.vue')['default'] |
||||
EdfsButton: typeof import('./../src/components/Edfs-button.vue')['default'] |
||||
EdfsDialog: typeof import('./../src/components/Edfs-dialog.vue')['default'] |
||||
EdfsInput: typeof import('./../src/components/Edfs-Input.vue')['default'] |
||||
EdfsNumberInput: typeof import('./../src/components/Edfs-number-input.vue')['default'] |
||||
EdfsTable: typeof import('./../src/components/Edfs-table/index.vue')['default'] |
||||
EdfsWrap: typeof import('./../src/components/Edfs-wrap.vue')['default'] |
||||
ElAlert: typeof import('element-plus/es')['ElAlert'] |
||||
ElAside: typeof import('element-plus/es')['ElAside'] |
||||
ElAutoResizer: typeof import('element-plus/es')['ElAutoResizer'] |
||||
ElAvatar: typeof import('element-plus/es')['ElAvatar'] |
||||
ElButton: typeof import('element-plus/es')['ElButton'] |
||||
ElCard: typeof import('element-plus/es')['ElCard'] |
||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] |
||||
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup'] |
||||
ElCollapse: typeof import('element-plus/es')['ElCollapse'] |
||||
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem'] |
||||
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider'] |
||||
ElContainer: typeof import('element-plus/es')['ElContainer'] |
||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker'] |
||||
ElDialog: typeof import('element-plus/es')['ElDialog'] |
||||
ElDrawer: typeof import('element-plus/es')['ElDrawer'] |
||||
ElEmpty: typeof import('element-plus/es')['ElEmpty'] |
||||
ElHeader: typeof import('element-plus/es')['ElHeader'] |
||||
ElInput: typeof import('element-plus/es')['ElInput'] |
||||
ElMain: typeof import('element-plus/es')['ElMain'] |
||||
ElMenu: typeof import('element-plus/es')['ElMenu'] |
||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] |
||||
ElPagination: typeof import('element-plus/es')['ElPagination'] |
||||
ElProgress: typeof import('element-plus/es')['ElProgress'] |
||||
ElRow: typeof import('element-plus/es')['ElRow'] |
||||
ElScrollbar: typeof import('element-plus/es')['ElScrollbar'] |
||||
ElSubMenu: typeof import('element-plus/es')['ElSubMenu'] |
||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] |
||||
ElTableV2: typeof import('element-plus/es')['ElTableV2'] |
||||
ElTag: typeof import('element-plus/es')['ElTag'] |
||||
ElTooltip: typeof import('element-plus/es')['ElTooltip'] |
||||
FormItemInput: typeof import('./../src/components/FormItemInput.vue')['default'] |
||||
RouterLink: typeof import('vue-router')['RouterLink'] |
||||
RouterView: typeof import('vue-router')['RouterView'] |
||||
} |
||||
export interface ComponentCustomProperties { |
||||
vLoading: typeof import('element-plus/es')['ElLoadingDirective'] |
||||
} |
||||
} |
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
/// <reference types="vite/client" />
|
||||
declare module '*.vue' { |
||||
import { ComponentOptions } from 'vue' |
||||
const componentOptions: ComponentOptions |
||||
export default componentOptions |
||||
} |
||||
declare module "element-plus/dist/locale/zh-cn.mjs"; |
@ -0,0 +1,63 @@
@@ -0,0 +1,63 @@
|
||||
export { } |
||||
declare global { |
||||
interface Fn<T = any> { |
||||
(...arg: T[]): T |
||||
} |
||||
|
||||
type Nullable<T> = T | null |
||||
|
||||
type ElRef<T extends HTMLElement = HTMLDivElement> = Nullable<T> |
||||
type Recordable<T = any> = Record<string, T> |
||||
|
||||
type ComponentRef<T extends abstract new (...args: any) => any> = InstanceType<T> |
||||
|
||||
type LocaleType = 'zh-CN' | 'en' |
||||
|
||||
type AxiosHeaders = |
||||
| 'application/json' |
||||
| 'application/x-www-form-urlencoded' |
||||
| 'multipart/form-data' |
||||
|
||||
type AxiosMethod = 'get' | 'post' | 'delete' | 'put' | 'GET' | 'POST' | 'DELETE' | 'PUT' |
||||
|
||||
type AxiosResponseType = |
||||
| 'arraybuffer' |
||||
| 'blob' |
||||
| 'document' |
||||
| 'json' |
||||
| 'text' |
||||
| 'stream' |
||||
|
||||
interface AxiosConfig { |
||||
params?: any |
||||
data?: any |
||||
url?: string |
||||
method?: AxiosMethod |
||||
headersType?: string |
||||
responseType?: AxiosResponseType |
||||
} |
||||
|
||||
interface IResponse<T = any> { |
||||
code: string |
||||
data: T extends any ? T : T & any |
||||
} |
||||
|
||||
interface PageParam { |
||||
pageSize?: number |
||||
pageNo?: number |
||||
} |
||||
|
||||
interface Tree { |
||||
id: number |
||||
name: string |
||||
children?: Tree[] | any[] |
||||
} |
||||
// 分页数据公共返回
|
||||
interface PageResult<T> { |
||||
list: T // 数据
|
||||
total: number // 总量
|
||||
} |
||||
|
||||
|
||||
|
||||
} |
@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html> |
||||
<html lang=""> |
||||
|
||||
<head> |
||||
<meta charset="UTF-8"> |
||||
<link rel="icon" href="/favicon.ico"> |
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
||||
<title>Vite App</title> |
||||
</head> |
||||
|
||||
<body> |
||||
<div id="app"></div> |
||||
<script type="module" src="/src/main.ts"></script> |
||||
</body> |
||||
|
||||
</html> |
@ -0,0 +1,49 @@
@@ -0,0 +1,49 @@
|
||||
{ |
||||
"name": "vue-project", |
||||
"version": "0.0.0", |
||||
"private": true, |
||||
"type": "module", |
||||
"scripts": { |
||||
"dev": "vite", |
||||
"build": "run-p type-check \"build-only {@}\" --", |
||||
"preview": "vite preview", |
||||
"build-only": "vite build", |
||||
"type-check": "vue-tsc --build" |
||||
}, |
||||
"dependencies": { |
||||
"@types/qs": "^6.9.18", |
||||
"@unocss/reset": "^66.0.0", |
||||
"axios": "^1.8.4", |
||||
"dayjs": "^1.11.13", |
||||
"dexie": "^4.0.11", |
||||
"element-plus": "^2.9.5", |
||||
"jszmq": "^0.1.2", |
||||
"lodash-es": "^4.17.21", |
||||
"pinia": "^3.0.1", |
||||
"qs": "^6.14.0", |
||||
"uuid": "^11.1.0", |
||||
"vue": "^3.5.13", |
||||
"vue-router": "^4.5.0" |
||||
}, |
||||
"devDependencies": { |
||||
"@iconify/json": "^2.2.310", |
||||
"@tsconfig/node22": "^22.0.0", |
||||
"@types/node": "^22.13.5", |
||||
"@unocss/preset-icons": "^66.0.0", |
||||
"@unocss/preset-rem-to-px": "^66.0.0", |
||||
"@vitejs/plugin-vue": "^5.2.1", |
||||
"@vitejs/plugin-vue-jsx": "^4.1.1", |
||||
"@vue/tsconfig": "^0.7.0", |
||||
"npm-run-all2": "^7.0.2", |
||||
"sass": "^1.85.0", |
||||
"typescript": "~5.7.3", |
||||
"unocss": "^66.0.0", |
||||
"unocss-preset-scalpel": "^1.2.7", |
||||
"unplugin-auto-import": "^19.1.0", |
||||
"unplugin-icons": "^22.1.0", |
||||
"unplugin-vue-components": "^28.4.0", |
||||
"vite": "^6.2.2", |
||||
"vite-plugin-vue-devtools": "^7.7.2", |
||||
"vue-tsc": "^2.2.2" |
||||
} |
||||
} |
After Width: | Height: | Size: 4.2 KiB |
@ -0,0 +1,51 @@
@@ -0,0 +1,51 @@
|
||||
<script setup lang="ts"> |
||||
import ZMQWorker from '@/composables/useZMQJsonWorker' |
||||
import type { ZmqStatus } from './utils/zmq' |
||||
import zhCn from 'element-plus/dist/locale/zh-cn.mjs' |
||||
import { ElNotification, type NotificationHandle } from 'element-plus' |
||||
const worker = ZMQWorker.getInstance() |
||||
|
||||
const zmqStatus = ref<ZmqStatus>('connected') |
||||
|
||||
worker.setStatusCallback((status: string) => { |
||||
zmqStatus.value = status as ZmqStatus |
||||
}) |
||||
|
||||
provide('zmqStatus', zmqStatus) |
||||
onMounted(() => { |
||||
worker.start() |
||||
}) |
||||
|
||||
onBeforeUnmount(() => { |
||||
worker.stop() |
||||
}) |
||||
const notification = ref<NotificationHandle>() |
||||
|
||||
watch(zmqStatus, status => { |
||||
if (status === 'disconnected') { |
||||
notification.value?.close() |
||||
notification.value = ElNotification({ |
||||
title: '通讯异常', |
||||
message: '请检查通讯连接', |
||||
type: 'error', |
||||
duration: 0, |
||||
}) |
||||
} else { |
||||
notification.value?.close() |
||||
notification.value = ElNotification({ |
||||
title: '通讯正常', |
||||
message: '通讯正常', |
||||
type: 'success', |
||||
duration: 3000, |
||||
}) |
||||
} |
||||
}) |
||||
</script> |
||||
|
||||
<template> |
||||
<el-config-provider :locale="zhCn"> |
||||
<RouterView /> |
||||
</el-config-provider> |
||||
</template> |
||||
|
||||
<style scoped lang="scss"></style> |
@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
|
||||
export default class Keys { |
||||
public static STORAGE_UUID = 'uuid' |
||||
public static STORAGE_TENANT_ID = 'tenant' |
||||
public static STORAGE_TOKEN = 'token' |
||||
public static STORAGE_REFRESH_TOKEN = 'refreshToken' |
||||
public static STORAGE_USER_INFO = 'userInfo' |
||||
public static STORAGE_ROLE_ROUTERS = 'roleRouters' |
||||
public static STORAGE_DICT_CACHE = 'dict' |
||||
public static STORAGE_LANG = 'lang' |
||||
public static STORAGE_STATIONID = 'stationId' |
||||
public static STORAGE_THEME = 'EDFS-THEME' |
||||
|
||||
public static CODE_SUCCEED = 0 |
||||
} |
@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
|
||||
export const errorCode: any = { |
||||
401: '认证失败,无法访问系统资源', |
||||
403: '当前操作没有权限', |
||||
404: '访问资源不存在', |
||||
} |
@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
|
||||
export interface Result<T> { |
||||
code: number |
||||
msg: string |
||||
status?: number |
||||
data: T |
||||
} |
@ -0,0 +1,27 @@
@@ -0,0 +1,27 @@
|
||||
const authErrMap: any = { |
||||
'401': '认证失败,无法访问系统资源', |
||||
'403': '当前操作没有权限', |
||||
'404': '访问资源不存在', |
||||
default: '系统未知错误,请反馈给管理员', |
||||
} |
||||
|
||||
const networkErrMap: { [key: number]: string } = { |
||||
400: '请求错误(400)', |
||||
401: '未授权,请重新登录(401)', |
||||
403: '拒绝访问(403)', |
||||
404: '请求出错(404)', |
||||
408: '请求超时(408)', |
||||
500: '服务器错误(500)', |
||||
501: '服务未实现(501)', |
||||
502: '网络错误(502)', |
||||
503: '服务不可用(503)', |
||||
504: '网络超时(504)', |
||||
505: 'HTTP版本不受支持(505)', |
||||
} |
||||
|
||||
const ignoreMsgs = [ |
||||
'无效的刷新令牌', // 刷新令牌被删除时,不用提示
|
||||
'刷新令牌已过期', // 使用刷新令牌,刷新获取新的访问令牌时,结果因为过期失败,此时需要忽略。否则,会导致继续 401,无法跳转到登出界面
|
||||
] |
||||
|
||||
export { authErrMap, networkErrMap, ignoreMsgs } |
@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
|
||||
import axiosInstance from '@/api/server/axiosInstance' |
||||
import { API_Config } from '@/api/server/config' |
||||
|
||||
const globalServer = axiosInstance('gloab', API_Config.gloab) |
||||
|
||||
export { globalServer } |
@ -0,0 +1,52 @@
@@ -0,0 +1,52 @@
|
||||
import { globalServer } from '../index' |
||||
|
||||
interface IGetDeviceDataParams { |
||||
columns: string[] |
||||
isLocal: boolean |
||||
limit: number |
||||
offset: number |
||||
} |
||||
|
||||
|
||||
export const getDeviceDetails = (params: IGetDeviceDataParams) => |
||||
globalServer({ |
||||
url: `/api/query`, |
||||
method: 'POST', |
||||
data: params, |
||||
}) |
||||
|
||||
|
||||
|
||||
|
||||
export interface ISiteList { |
||||
id: string |
||||
name: string |
||||
export_time: string |
||||
last_modify_time: string |
||||
export_root_path: string |
||||
} |
||||
|
||||
export const getSiteList = () => |
||||
globalServer<ISiteList[]>({ |
||||
url: `/api/sites`, |
||||
method: 'get', |
||||
}) |
||||
|
||||
export const getDeviceList = (siteId: string) => |
||||
globalServer({ |
||||
url: `/api/devices?siteId=${siteId}`, |
||||
method: 'get', |
||||
}) |
||||
|
||||
export interface IPointsParams { |
||||
site?: string |
||||
sn?: string |
||||
isLocal?: boolean |
||||
host?: string |
||||
} |
||||
export const getPoints = (params: IPointsParams) => |
||||
globalServer({ |
||||
url: `/api/points`, |
||||
method: 'get', |
||||
params, |
||||
}) |
@ -0,0 +1,238 @@
@@ -0,0 +1,238 @@
|
||||
import type { |
||||
AxiosInstance, |
||||
AxiosRequestConfig, |
||||
AxiosRequestHeaders, |
||||
AxiosResponse, |
||||
InternalAxiosRequestConfig, |
||||
} from 'axios' |
||||
|
||||
import qs from 'qs' |
||||
import Keys from '../Keys' |
||||
import axios, { AxiosError, isCancel } from 'axios' |
||||
// import {
|
||||
// getRefreshToken,
|
||||
// getTenantId,
|
||||
// getToken,
|
||||
// removeToken,
|
||||
// setToken,
|
||||
// } from '@/utils/auth'
|
||||
import { authErrMap, ignoreMsgs, networkErrMap } from '../basic/utils' |
||||
import type { Result } from '../basic/httpTypes' |
||||
import type { APIConfigKeys } from './config' |
||||
import { useRouter } from 'vue-router' |
||||
import { useMessage } from '@/composables/useMessage' |
||||
const router = useRouter() |
||||
|
||||
const message = useMessage() |
||||
const basicHeader = { |
||||
tenant() { |
||||
// const tenant = getTenantId()
|
||||
// if (tenant) return tenant
|
||||
return '' |
||||
}, |
||||
token() { |
||||
// const token = getToken()
|
||||
// if (token) return 'Bearer ' + token
|
||||
return '' |
||||
}, |
||||
} |
||||
interface Config { |
||||
baseURL?: string |
||||
baseAPI: string |
||||
} |
||||
const instances: Record<string, AxiosInstance> = {} |
||||
let isRefreshToken = false // 是否正在刷新中
|
||||
let requestList: any[] = [] // 请求队列
|
||||
|
||||
const createAxiosInstance = (module: APIConfigKeys, config: Config) => { |
||||
if (!config || !config.baseAPI) { |
||||
throw new Error(`Invalid configuration for module: ${module}`) |
||||
} |
||||
const { baseAPI } = config |
||||
|
||||
if (!instances[module]) { |
||||
const instance = axios.create({ |
||||
baseURL: `${baseAPI}`, |
||||
timeout: 10000, |
||||
headers: { |
||||
'Content-Type': 'application/json', |
||||
}, |
||||
}) |
||||
instances[module] = instance |
||||
} |
||||
|
||||
const service = instances[module] |
||||
|
||||
service.interceptors.request.use( |
||||
(config: InternalAxiosRequestConfig): InternalAxiosRequestConfig => { |
||||
// config.headers.Authorization = basicHeader.token()
|
||||
// config.headers['tenant-id'] = basicHeader.tenant()
|
||||
const params = config.params || {} |
||||
const data = config.data || false |
||||
|
||||
if ( |
||||
config.method?.toUpperCase() === 'POST' && |
||||
(config.headers as AxiosRequestHeaders)['Content-Type'] === |
||||
'application/x-www-form-urlencoded' |
||||
) { |
||||
config.data = qs.stringify(data) |
||||
} |
||||
|
||||
if (config.method?.toUpperCase() === 'GET' && params) { |
||||
config.params = {} |
||||
const paramsStr = qs.stringify(params, { allowDots: true }) |
||||
if (paramsStr) { |
||||
config.url = config.url + '?' + paramsStr |
||||
} |
||||
} |
||||
return config |
||||
}, |
||||
(error: AxiosError): Promise<AxiosError> => { |
||||
return Promise.reject(error) |
||||
} |
||||
) |
||||
|
||||
service.interceptors.response.use( |
||||
async (res: AxiosResponse) => { |
||||
const config = res.config |
||||
let { data } = res |
||||
if (!res.data) { |
||||
throw new Error('返回“[HTTP]请求没有返回值”') |
||||
} |
||||
if ( |
||||
res.request.responseType === 'blob' || |
||||
res.request.responseType === 'arraybuffer' |
||||
) { |
||||
// 注意:如果导出的响应为 json,说明可能失败了,不直接返回进行下载
|
||||
if (res.data.type !== 'application/json') { |
||||
return Promise.resolve({ code: res.status, data: res.data }) |
||||
} |
||||
data = await new Response(res.data).json() |
||||
} |
||||
const code = data.code || 200 |
||||
const msg = data.msg || authErrMap[code] || authErrMap['default'] |
||||
|
||||
if (ignoreMsgs.indexOf(msg) !== -1) { |
||||
// 如果是忽略的错误码,直接返回 msg 异常
|
||||
return Promise.reject({ code: null, msg }) |
||||
} else |
||||
// if (code === 401) {
|
||||
// if (!isRefreshToken) {
|
||||
// isRefreshToken = true
|
||||
// if (!getRefreshToken()) logout()
|
||||
// try {
|
||||
// const refreshTokenRes = await refreshToken()
|
||||
// if (refreshTokenRes.data.code !== 0) {
|
||||
// logout()
|
||||
// return Promise.reject({ code, msg })
|
||||
// }
|
||||
// setToken(refreshTokenRes.data.data.accessToken)
|
||||
// config.headers!.Authorization = 'Bearer ' + getToken()
|
||||
// requestList.forEach((cb: any) => {
|
||||
// cb()
|
||||
// })
|
||||
// requestList = []
|
||||
// // 重连接socket
|
||||
// // openSocket()
|
||||
// return service(config)
|
||||
// } catch (e) {
|
||||
// logout()
|
||||
// return Promise.reject({ code, msg })
|
||||
// } finally {
|
||||
// requestList = []
|
||||
// isRefreshToken = false
|
||||
// }
|
||||
// } else {
|
||||
// return new Promise(resolve => {
|
||||
// requestList.push(() => {
|
||||
// config.headers!.Authorization = 'Bearer ' + getToken()
|
||||
// resolve(service(config))
|
||||
// })
|
||||
// })
|
||||
// }
|
||||
// } else
|
||||
|
||||
if (code === 500) { |
||||
return Promise.reject({ code, msg }) |
||||
} else if (code !== 200) { |
||||
// if (msg === '无效的刷新令牌') {
|
||||
// // hard coding:忽略这个提示,直接登出
|
||||
// console.log(msg)
|
||||
// } else {
|
||||
// ElMessage.error(msg)
|
||||
// }
|
||||
return Promise.reject({ code, msg }) |
||||
} |
||||
|
||||
return data |
||||
}, |
||||
(error: any): Promise<any> => { |
||||
if (isCancel(error)) { |
||||
return Promise.resolve() |
||||
} |
||||
const message = '请求错误' |
||||
if (error.code === 'ECONNABORTED') { |
||||
return Promise.reject({ code: null, msg: '服务器响应超时' }) |
||||
} |
||||
if (!error.response) { |
||||
return Promise.reject({ code: null, msg: message }) |
||||
} |
||||
|
||||
const status = error.response?.status |
||||
const unKnowError = `连接出错(${error.response.status})!` |
||||
if (status === 401) { |
||||
localStorage.removeItem(Keys.STORAGE_TOKEN) |
||||
router.push('/login') |
||||
} |
||||
|
||||
const msg = networkErrMap[status] ? networkErrMap[status] : unKnowError |
||||
|
||||
return Promise.reject({ code: status || null, msg: msg }) |
||||
} |
||||
) |
||||
|
||||
if (!service) { |
||||
throw new Error(`Failed to create Axios instance for module: ${module}`) |
||||
} |
||||
|
||||
// const refreshToken = async () => {
|
||||
// // axios.defaults.headers.common['tenant-id'] = getTenantId()
|
||||
// return await axios.post(
|
||||
// `${VITE_BASE_API_SYSTEM}/auth/refresh-token?refreshToken=` + getRefreshToken()
|
||||
// )
|
||||
// }
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
async function request<T = any>(config: AxiosRequestConfig): Promise<Result<T>> { |
||||
try { |
||||
const result = (await service(config)) as Result<T> |
||||
return result |
||||
} catch (err: any) { |
||||
console.log(err) |
||||
const result: Result<T> = { |
||||
code: err?.code || -1, |
||||
msg: err.msg || err.message, |
||||
data: err.data || null, |
||||
} |
||||
return result |
||||
} |
||||
} |
||||
|
||||
return request |
||||
} |
||||
|
||||
// let isShowLogout = false
|
||||
// async function logout() {
|
||||
// if (isShowLogout) return
|
||||
// isShowLogout = true
|
||||
|
||||
// await message.forceConfirm('登录状态已失效,请重新登录', '系统提示', '重新登录')
|
||||
// removeToken()
|
||||
// isShowLogout = false
|
||||
// window.location.href = '/login'
|
||||
// }
|
||||
|
||||
export default createAxiosInstance |
@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
|
||||
export interface APIConfig { |
||||
gloab: Config |
||||
} |
||||
|
||||
interface Config { |
||||
baseURL?: string |
||||
baseAPI: string |
||||
} |
||||
|
||||
const API_Config: APIConfig = { |
||||
gloab: { |
||||
baseAPI: import.meta.env.VITE_BASE_API, |
||||
}, |
||||
|
||||
} |
||||
|
||||
export type APIConfigKeys = keyof typeof API_Config |
||||
export { API_Config } |
@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
|
||||
// 导出变量 |
||||
:export { |
||||
namespace: $namespace; |
||||
elNamespace: $elNamespace; |
||||
} |
@ -0,0 +1,15 @@
@@ -0,0 +1,15 @@
|
||||
@import 'element-plus/dist/index.css'; |
||||
@import './theme-variables.css'; |
||||
@import '@unocss/reset/tailwind.css'; |
||||
|
||||
html, |
||||
body { |
||||
@apply w-full h-full; |
||||
} |
||||
|
||||
#app { |
||||
@apply wh-full; |
||||
} |
||||
.el-progress-bar__innerText { |
||||
@apply w-full text-center; |
||||
} |
@ -0,0 +1,77 @@
@@ -0,0 +1,77 @@
|
||||
@use 'sass:math'; |
||||
|
||||
// 命名空间 |
||||
$namespace: v; |
||||
// el命名空间 |
||||
$elNamespace: el; |
||||
$W-scale-ratio-1440: math.div(1440, 1920); |
||||
$H-scale-ratio-900: math.div(900, 1080); |
||||
|
||||
$W-scale-ratio-2560: math.div(2560, 1920); |
||||
$H-scale-ratio-1440: math.div(1440, 1080); |
||||
|
||||
@mixin ResponsiveW($base-width) { |
||||
width: $base-width + px !important; |
||||
|
||||
@media only screen and (max-width: 1440px) { |
||||
width: calc($W-scale-ratio-1440 * #{$base-width}px) !important; |
||||
} |
||||
|
||||
@media only screen and (min-width: 2560px) { |
||||
width: calc($W-scale-ratio-2560 * #{$base-width}px) !important; |
||||
} |
||||
} |
||||
|
||||
@mixin ResponsiveH($base-height) { |
||||
height: $base-height + px; |
||||
|
||||
@media only screen and (max-width: 1440px) { |
||||
height: calc($H-scale-ratio-900 * #{$base-height}px); |
||||
} |
||||
|
||||
@media only screen and (min-width: 2560px) { |
||||
height: calc($H-scale-ratio-1440 * #{$base-height}px); |
||||
} |
||||
} |
||||
|
||||
@mixin ResponsiveFz($base-font-size) { |
||||
font-size: $base-font-size + px; |
||||
|
||||
@media only screen and (max-width: 1440px) { |
||||
font-size: calc($W-scale-ratio-1440 * #{$base-font-size}px); |
||||
} |
||||
|
||||
@media only screen and (min-width: 2560px) { |
||||
font-size: calc($W-scale-ratio-2560 * #{$base-font-size}px); |
||||
} |
||||
} |
||||
|
||||
@mixin ResponsivePadding($top, $right, $bottom, $left) { |
||||
padding: $top + px $right + px $bottom + px $left + px; |
||||
|
||||
@media only screen and (max-width: 1440px) { |
||||
padding: calc($H-scale-ratio-900 * #{$top}px) calc($W-scale-ratio-1440 * #{$right}px) |
||||
calc($H-scale-ratio-900 * #{$bottom}px) calc($W-scale-ratio-1440 * #{$left}px); |
||||
} |
||||
|
||||
@media only screen and (min-width: 2560px) { |
||||
padding: calc($H-scale-ratio-1440 * #{$top}px) calc($W-scale-ratio-2560 * #{$right}px) |
||||
calc($H-scale-ratio-1440 * #{$bottom}px), |
||||
calc($W-scale-ratio-2560 * #{$left}px); |
||||
} |
||||
} |
||||
|
||||
@mixin ResponsiveMargin($top, $right, $bottom, $left) { |
||||
margin: $top + px $right + px $bottom + px $left + px; |
||||
|
||||
@media only screen and (max-width: 1440px) { |
||||
margin: calc($H-scale-ratio-900 * #{$top}px) calc($W-scale-ratio-1440 * #{$right}px) |
||||
calc($H-scale-ratio-900 * #{$bottom}px) calc($W-scale-ratio-1440 * #{$left}px); |
||||
} |
||||
|
||||
@media only screen and (min-width: 2560px) { |
||||
margin: calc($H-scale-ratio-1440 * #{$top}px) calc($W-scale-ratio-2560 * #{$right}px) |
||||
calc($H-scale-ratio-1440 * #{$bottom}px), |
||||
calc($W-scale-ratio-2560 * #{$left}px); |
||||
} |
||||
} |
@ -0,0 +1,74 @@
@@ -0,0 +1,74 @@
|
||||
html[data-theme='dark'] { |
||||
--bg: #0d0d0d; |
||||
--text-color: #ffffff; |
||||
--layout-header-bg: #1b1d23; |
||||
--layout-menu-bg: #1b1d23; |
||||
--menu-active-bg: #2c342c; |
||||
--menu-hover-bg: #272b35; |
||||
--warp-bg: #212327; |
||||
--table-header-bg: #3b3d40; |
||||
--table-header-text-color: #cdcecf; |
||||
--pagination-bg: #101115; |
||||
--pagination-border-color: rgba(255, 255, 255, 0.13); |
||||
--select-header--text-color: #ffffff; |
||||
--station-card-bg: #23272f; |
||||
--station-card-border-color: #4b5361; |
||||
--station-header-bg: #363a40; |
||||
--station-header-text-color: #fff; |
||||
--station-info-val-text: #fff; |
||||
--label-color: #666; |
||||
--text-desc: #8d9095; |
||||
--mask-bg: #333; |
||||
--icon-color: #aaa; |
||||
--icon-hover-color: #606266; |
||||
} |
||||
|
||||
html[data-theme='light'] { |
||||
--bg: #f1f2f6; |
||||
--text-color: #4d4d4d; |
||||
--layout-header-bg: #ffffff; |
||||
--layout-menu-bg: #fff; |
||||
--menu-active-bg: #f5fcee; |
||||
--menu-hover-bg: #f5f5f5; |
||||
--warp-bg: #ffffff; |
||||
--table-header-bg: #e8e9ee; |
||||
--table-header-text-color: #030303; |
||||
--pagination-bg: transparent; |
||||
--pagination-border-color: rgba(217, 217, 217, 1); |
||||
--select-header--text-color: #666; |
||||
--station-card-bg: #ffffff; |
||||
--station-card-border-color: #e9e9e9; |
||||
--station-header-bg: #f1f1f1; |
||||
--station-header-text-color: #030303; |
||||
--station-info-val-text: #4d4d4d; |
||||
--label-color: #666; |
||||
--text-desc: #a8abb2; |
||||
--mask-bg: #f5f5f5; |
||||
--icon-color: #aaa; |
||||
--icon-hover-color: #606266; |
||||
} |
||||
|
||||
html.dark { |
||||
--el-bg-color: #212327; |
||||
--el-border-color: rgba(255, 255, 255, 0.13); |
||||
--el-button-divide-border-color: rgba(255, 255, 255, 0.13); |
||||
--el-bg-color-overlay: #212327; |
||||
--el-text-color-regular: #c7c8cb; |
||||
--el-color-primary: #619925; |
||||
--el-color-primary-light-3: rgb(78.1, 141.8, 46.6); |
||||
--el-color-primary-light-5: rgb(61.5, 107, 39); |
||||
--el-color-primary-light-7: rgb(44.9, 72.2, 31.4); |
||||
--el-color-primary-light-8: rgb(36.6, 54.8, 27.6); |
||||
--el-color-primary-light-9: rgb(28.3, 37.4, 23.8); |
||||
--el-color-primary-dark-2: rgb(133.4, 206.2, 97.4); |
||||
} |
||||
|
||||
html.light { |
||||
--el-color-primary: #619925; |
||||
--el-color-primary-light-3: rgb(148.6, 212.3, 117.1); |
||||
--el-color-primary-light-5: rgb(179, 224.5, 156.5); |
||||
--el-color-primary-light-7: rgb(209.4, 236.7, 195.9); |
||||
--el-color-primary-light-8: rgb(224.6, 242.8, 215.6); |
||||
--el-color-primary-light-9: rgb(239.8, 248.9, 235.3); |
||||
--el-color-primary-dark-2: rgb(82.4, 155.2, 46.4); |
||||
} |
@ -0,0 +1,154 @@
@@ -0,0 +1,154 @@
|
||||
<template> |
||||
<div class="additem-item-input-root"> |
||||
<div class="label" :class="labelWidth"> |
||||
<span v-if="props.require" class="require">*</span> |
||||
{{ label }}: |
||||
</div> |
||||
<el-input |
||||
v-model="localValue" |
||||
:type="type" |
||||
:disabled="!canEdit" |
||||
:show-password="showPassword" |
||||
class="input" |
||||
:placeholder="placeholder" |
||||
@input="inputChange" |
||||
resize="none" |
||||
:rows="rows" |
||||
@blur="handleBlur" |
||||
:formatter="handleFormatter" |
||||
></el-input> |
||||
</div> |
||||
</template> |
||||
|
||||
<script setup lang="ts"> |
||||
export interface Props { |
||||
type?: string |
||||
require?: boolean |
||||
label?: string |
||||
modelValue?: string | number |
||||
canEdit?: boolean |
||||
widthType?: number |
||||
stringNum?: boolean //获取string类型的number,开启时,输入框只能输入数字,且value值为string,适用于id类型的编辑 |
||||
placeholder?: string |
||||
showPassword?: boolean |
||||
rows?: number |
||||
} |
||||
|
||||
const props = withDefaults(defineProps<Props>(), { |
||||
type: 'text', |
||||
require: false, |
||||
label: '', |
||||
modelValue: '', |
||||
canEdit: true, |
||||
widthType: 0, |
||||
stringNum: false, |
||||
cmd: false, |
||||
placeholder: '', |
||||
showPassword: false, |
||||
rows: 5, |
||||
}) |
||||
|
||||
const emit = defineEmits(['update:modelValue']) |
||||
|
||||
const localValue = ref<string | number>('') |
||||
|
||||
const labelWidth = computed(() => { |
||||
switch (props.widthType) { |
||||
case 0: |
||||
return 'width-normal' |
||||
case 1: |
||||
return '' |
||||
default: |
||||
return 'width-normal' |
||||
} |
||||
}) |
||||
|
||||
const inputChange = (val: string | number) => { |
||||
emit('update:modelValue', props.type === 'number' ? Number(val) : val) |
||||
} |
||||
|
||||
const handleBlur = () => { |
||||
if (props.type === 'number') { |
||||
if ( |
||||
String(localValue.value).length === 0 || |
||||
String(localValue.value).match(/^[0]+$/) !== null |
||||
) { |
||||
localValue.value = 0 |
||||
} |
||||
} |
||||
} |
||||
|
||||
const handleFormatter = (value: string | number) => { |
||||
if (props.stringNum && typeof value === 'string') { |
||||
return value.replace(/([^\d])+/g, '') |
||||
} |
||||
return value |
||||
} |
||||
|
||||
watch( |
||||
() => props.modelValue, |
||||
modelValue => { |
||||
localValue.value = modelValue |
||||
}, |
||||
{ immediate: true } |
||||
) |
||||
</script> |
||||
|
||||
<style lang="scss" scoped> |
||||
.additem-item-input-root { |
||||
display: flex; |
||||
align-items: center; |
||||
width: 100%; |
||||
height: 100%; |
||||
box-sizing: border-box; |
||||
column-gap: 8px; |
||||
// font-size: 0.18rem; |
||||
// padding-right: 42px; |
||||
|
||||
.label { |
||||
// font-size: 14px; |
||||
color: var(--label-color); |
||||
line-height: 33px; |
||||
text-align: right; |
||||
// margin-right: 24px; |
||||
|
||||
.require { |
||||
color: red; |
||||
} |
||||
} |
||||
|
||||
.input { |
||||
flex-grow: 1; |
||||
width: 0; |
||||
height: 100%; |
||||
line-height: 100%; |
||||
} |
||||
|
||||
.width-normal { |
||||
width: 110px; |
||||
} |
||||
|
||||
:deep(input::-webkit-outer-spin-button), |
||||
:deep(input::-webkit-inner-spin-button) { |
||||
-webkit-appearance: none; |
||||
} |
||||
|
||||
:deep(input[type='number']) { |
||||
-moz-appearance: textfield; |
||||
} |
||||
|
||||
:deep(.el-input) { |
||||
font-size: inherit; |
||||
} |
||||
|
||||
:deep(.el-input__inner) { |
||||
// background-color: transparent; |
||||
height: 100%; |
||||
line-height: 100%; |
||||
// @include border_color("ws_dialog_input"); |
||||
} |
||||
|
||||
:deep(.el-input__inner::-webkit-input-placeholder) { |
||||
} |
||||
} |
||||
</style> |
@ -0,0 +1,66 @@
@@ -0,0 +1,66 @@
|
||||
<template> |
||||
<el-button |
||||
:type="type" |
||||
:circle="circle" |
||||
:icon="icon" |
||||
:loading="loading" |
||||
:auto-insert-space="autoInsertSpace" |
||||
:round="round" |
||||
:size="size" |
||||
:plain="plain" |
||||
:disabled="disabled" |
||||
:autofocus="autofocus" |
||||
:color="color" |
||||
:class="className" |
||||
@click="onClick" |
||||
:text="text" |
||||
class="edfs-button" |
||||
> |
||||
{{ innerText }} |
||||
</el-button> |
||||
</template> |
||||
|
||||
<script setup lang="ts"> |
||||
import { type ButtonProps } from 'element-plus' |
||||
|
||||
type Props = ButtonProps & { |
||||
innerText: string |
||||
} |
||||
const props = defineProps<Partial<Props>>() |
||||
const emit = defineEmits<{ |
||||
click: [e: MouseEvent] |
||||
}>() |
||||
|
||||
const className = computed(() => { |
||||
let name = 'edfs-button-default' |
||||
switch (props.size) { |
||||
case 'default': |
||||
name = 'edfs-button-default' |
||||
break |
||||
case 'large': |
||||
name = 'edfs-button-large' |
||||
break |
||||
case 'small': |
||||
name = 'edfs-button-small' |
||||
break |
||||
} |
||||
return name |
||||
}) |
||||
|
||||
function onClick(e: MouseEvent) { |
||||
emit('click', e) |
||||
} |
||||
</script> |
||||
|
||||
<style scoped lang="scss"> |
||||
.edfs-button-default { |
||||
height: 32px; |
||||
font-size: 14px; |
||||
padding: 8px 15px; |
||||
} |
||||
.edfs-button-small { |
||||
height: 24px; |
||||
font-size: 14px; |
||||
padding: 6px 11px; |
||||
} |
||||
</style> |
@ -0,0 +1,108 @@
@@ -0,0 +1,108 @@
|
||||
<template> |
||||
<div class="base-dialog"> |
||||
<el-dialog |
||||
:title="title" |
||||
v-model="visible" |
||||
:draggable="draggable" |
||||
:overflow="overflow" |
||||
:width="width" |
||||
:close-on-click-modal="false" |
||||
@close="onClose" |
||||
> |
||||
<slot></slot> |
||||
<template #footer> |
||||
<slot name="footer"></slot> |
||||
</template> |
||||
|
||||
<template v-if="isShowFooter"> |
||||
<div class="dialog-footer"> |
||||
<edfs-button |
||||
type="primary" |
||||
@click="onSave" |
||||
:loading="btnLoading" |
||||
inner-text="确定" |
||||
/> |
||||
<edfs-button v-if="useDelBtn" type="danger" @click="onDel" inner-text="删除" /> |
||||
<edfs-button @click="onClose" inner-text="取消" /> |
||||
</div> |
||||
</template> |
||||
</el-dialog> |
||||
</div> |
||||
</template> |
||||
|
||||
<script setup lang="ts"> |
||||
import edfsButton from './Edfs-button.vue' |
||||
|
||||
interface Props { |
||||
title: string |
||||
isShow: boolean |
||||
isShowFooter?: boolean |
||||
draggable?: boolean |
||||
overflow?: boolean |
||||
width?: string |
||||
btnLoading?: boolean |
||||
useDelBtn?: boolean |
||||
} |
||||
|
||||
const emits = defineEmits(['on-save', 'on-close', 'on-del']) |
||||
|
||||
const visible = ref(false) |
||||
|
||||
const props = withDefaults(defineProps<Props>(), { |
||||
title: '提示', |
||||
isShow: false, |
||||
isShowFooter: true, |
||||
draggable: true, |
||||
overflow: true, |
||||
btnLoading: false, |
||||
useDelBtn: false, |
||||
}) |
||||
|
||||
watch( |
||||
() => props.isShow, |
||||
val => { |
||||
visible.value = val |
||||
} |
||||
) |
||||
|
||||
function onSave() { |
||||
emits('on-save') |
||||
} |
||||
|
||||
function onClose() { |
||||
emits('on-close') |
||||
} |
||||
|
||||
function onDel() { |
||||
emits('on-del') |
||||
} |
||||
</script> |
||||
|
||||
<style lang="scss" scoped> |
||||
.base-dialog { |
||||
:deep(.el-dialog__header) { |
||||
display: flex; |
||||
padding-bottom: 16px; |
||||
padding-right: 20px; |
||||
} |
||||
:deep(.el-dialog__title) { |
||||
font-size: 18px; |
||||
} |
||||
|
||||
:deep(.el-dialog) { |
||||
padding: 20px; |
||||
} |
||||
:deep(.el-dialog__footer) { |
||||
padding: 0; |
||||
} |
||||
|
||||
.dialog-footer { |
||||
display: flex; |
||||
justify-content: center; |
||||
align-items: center; |
||||
column-gap: 4px; |
||||
height: 60px; |
||||
margin-top: 20px; |
||||
} |
||||
} |
||||
</style> |
@ -0,0 +1,151 @@
@@ -0,0 +1,151 @@
|
||||
<template> |
||||
<div class="edfs-number-input"> |
||||
<div class="label" v-if="labelName"> |
||||
<span v-if="props.require" class="require">*</span> |
||||
{{ labelName }} |
||||
</div> |
||||
<el-tooltip |
||||
popper-class="number-tips" |
||||
:content="tipText" |
||||
placement="top" |
||||
:visible="isShowTip" |
||||
:disabled="!isUseTip" |
||||
> |
||||
<el-input |
||||
class="number-input" |
||||
v-model="inputValue" |
||||
type="text" |
||||
@input="handleInput" |
||||
@wheel="onWheel" |
||||
:disabled="disabled" |
||||
:placeholder="placeholder" |
||||
@focus="onFocus" |
||||
@blur="onBlur" |
||||
/> |
||||
</el-tooltip> |
||||
</div> |
||||
</template> |
||||
|
||||
<script setup lang="ts"> |
||||
interface Props { |
||||
tipText?: string |
||||
isUseTip?: boolean |
||||
labelName?: string |
||||
require?: boolean |
||||
placeholder?: string |
||||
modelValue: number | string |
||||
numMax?: number | undefined |
||||
numMin?: number | undefined |
||||
useWheel?: boolean |
||||
disabled?: boolean |
||||
} |
||||
|
||||
const props = withDefaults(defineProps<Props>(), { |
||||
labelName: '', |
||||
modelValue: '', |
||||
require: false, |
||||
placeholder: '请输入', |
||||
numMax: undefined, |
||||
numMin: undefined, |
||||
isUseTip: false, |
||||
tipText: '', |
||||
disabled: false, |
||||
useWheel: false, |
||||
}) |
||||
|
||||
const emit = defineEmits(['update:modelValue', 'change']) |
||||
const inputValue = ref() |
||||
|
||||
watch( |
||||
() => props.modelValue, |
||||
newValue => { |
||||
inputValue.value = newValue |
||||
}, |
||||
{ immediate: true } |
||||
) |
||||
|
||||
const handleInput = (value: string) => { |
||||
let newValue = value.replace(/[^0-9.-]/g, '').replace(/(\..*)\./g, '$1') |
||||
|
||||
if (newValue.indexOf('-') > 0) { |
||||
newValue = newValue.replace(/-/g, '') |
||||
} |
||||
|
||||
if (newValue.length === 0) { |
||||
isShowTip.value = true |
||||
} else { |
||||
isShowTip.value = false |
||||
} |
||||
|
||||
inputValue.value = newValue |
||||
} |
||||
|
||||
const onWheel = (event: WheelEvent) => { |
||||
event.preventDefault() |
||||
if (!props.useWheel) return |
||||
let currentValue = Number(inputValue.value) || 0 |
||||
if (event.deltaY < 0) { |
||||
currentValue++ |
||||
} else { |
||||
currentValue-- |
||||
} |
||||
inputValue.value = currentValue.toString() |
||||
} |
||||
|
||||
watch(inputValue, newValue => { |
||||
if (!newValue) { |
||||
emit('update:modelValue', undefined) |
||||
return |
||||
} |
||||
let numericValue = newValue |
||||
// if (props.numMax !== undefined && numericValue > props.numMax) { |
||||
// inputValue.value = props.numMax.toString() |
||||
// numericValue = props.numMax |
||||
// } |
||||
// if (props.numMin !== undefined && numericValue < props.numMin) { |
||||
// inputValue.value = props.numMin.toString() |
||||
// numericValue = props.numMin |
||||
// } |
||||
emit('update:modelValue', numericValue) |
||||
}) |
||||
|
||||
const isShowTip = ref(false) |
||||
|
||||
function onBlur() { |
||||
onChangeTip(false) |
||||
} |
||||
|
||||
function onFocus() { |
||||
onChangeTip(true) |
||||
} |
||||
|
||||
function onChangeTip(visible: boolean) { |
||||
isShowTip.value = visible |
||||
} |
||||
</script> |
||||
|
||||
<style lang="scss" scoped> |
||||
.edfs-number-input { |
||||
display: flex; |
||||
align-items: center; |
||||
width: 100%; |
||||
height: 100%; |
||||
box-sizing: border-box; |
||||
|
||||
.label { |
||||
color: var(--label-color); |
||||
white-space: nowrap; |
||||
text-align: right; |
||||
line-height: 33px; |
||||
width: 110px; |
||||
|
||||
.require { |
||||
color: red; |
||||
} |
||||
} |
||||
|
||||
.number-input { |
||||
flex: 1; |
||||
} |
||||
} |
||||
</style> |
@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
|
||||
import type { TableProps } from 'element-plus'; |
||||
|
||||
interface IPaging { |
||||
currentPage?: number, |
||||
pageSize?: number, |
||||
pageTotal?: number, |
||||
usePaging?: boolean |
||||
loading?: boolean |
||||
} |
||||
|
||||
export type Props<T> = TableProps<T> & IPaging |
@ -0,0 +1,210 @@
@@ -0,0 +1,210 @@
|
||||
<template> |
||||
<div class="edfs-table-components"> |
||||
<el-table |
||||
:data="data" |
||||
:fit="fit" |
||||
:stripe="stripe" |
||||
:border="border" |
||||
v-loading="loading" |
||||
:show-header="showHeader" |
||||
:max-height="maxHeight" |
||||
:highlight-current-row="highlightCurrentRow" |
||||
:row-class-name="rowClassName" |
||||
@current-change="onCurrentChange" |
||||
ref="ELTableRef" |
||||
@row-click="onRowClick" |
||||
@row-dblclick="onRowDblclick" |
||||
@selection-change="handleSelectionChange" |
||||
class="edfs-table" |
||||
:span-method="spanMethod" |
||||
> |
||||
<slot></slot> |
||||
</el-table> |
||||
<template v-if="usePaging"> |
||||
<div class="pagination-block"> |
||||
<el-pagination |
||||
v-model:current-page="currentPage" |
||||
v-model:page-size="pageSize" |
||||
:width-type="1" |
||||
layout="prev, pager, next, jumper" |
||||
:total="pageTotal" |
||||
background |
||||
@current-change="onPageCurrentChange" |
||||
/> |
||||
|
||||
<!-- @size-change="onPageSizeChange" --> |
||||
</div> |
||||
</template> |
||||
</div> |
||||
</template> |
||||
|
||||
<script setup lang="ts" generic="T"> |
||||
import { ElTable } from 'element-plus' |
||||
import { type Props } from './defaults' |
||||
|
||||
const props = withDefaults(defineProps<Props<T>>(), { |
||||
fit: true, |
||||
showHeader: true, |
||||
usePaging: true, |
||||
currentPage: 1, |
||||
pageTotal: 15, |
||||
loading: false, |
||||
}) |
||||
|
||||
|
||||
const emit = defineEmits<{ |
||||
'current-change': [currentRow: any, oldCurrentRow: any] |
||||
'row-click': [row: any, column: any, event: Event] |
||||
'row-dblclick': [row: any, column: any, event: Event] |
||||
// 'page-size-change': [pageSize: number] |
||||
'page-current-change': [currentPage: number] |
||||
'selection-change': [selection: any[]] |
||||
}>() |
||||
|
||||
function onCurrentChange(currentRow: any, oldCurrentRow: any) { |
||||
emit('current-change', currentRow, oldCurrentRow) |
||||
} |
||||
function onRowClick(row: any, column: any, event: Event) { |
||||
emit('row-click', row, column, event) |
||||
} |
||||
function onRowDblclick(row: any, column: any, event: Event) { |
||||
emit('row-dblclick', row, column, event) |
||||
} |
||||
|
||||
function handleSelectionChange(selection: any[]) { |
||||
emit('selection-change', selection) |
||||
} |
||||
|
||||
// 分页 |
||||
const pageSize = ref() |
||||
|
||||
watch( |
||||
() => props.pageSize, |
||||
val => { |
||||
pageSize.value = val |
||||
} |
||||
) |
||||
|
||||
const currentPage = ref() |
||||
watch( |
||||
() => props.currentPage, |
||||
val => { |
||||
currentPage.value = val |
||||
} |
||||
) |
||||
|
||||
// function onPageSizeChange(pageSize: number) { |
||||
// emit('page-size-change', pageSize) |
||||
// } |
||||
function onPageCurrentChange(currentPage: number) { |
||||
emit('page-current-change', currentPage) |
||||
} |
||||
|
||||
function getSize() { |
||||
const el = document.querySelector('.edfs-table-components') |
||||
if (!el) return 18 |
||||
const height = Math.floor(el.getBoundingClientRect().height) |
||||
return Math.ceil(height / 40) |
||||
} |
||||
|
||||
const ELTableRef = ref<InstanceType<typeof ElTable>>() |
||||
function clearSelection() { |
||||
ELTableRef.value?.clearSelection() |
||||
} |
||||
|
||||
defineExpose({ |
||||
getSize, |
||||
clearSelection, |
||||
}) |
||||
</script> |
||||
|
||||
<style lang="scss" scoped> |
||||
.edfs-table-components { |
||||
width: 100%; |
||||
height: 100%; |
||||
display: flex; |
||||
flex-direction: column; |
||||
box-sizing: border-box; |
||||
// background-color: #fff; |
||||
.edfs-table { |
||||
width: 100%; |
||||
height: 100%; |
||||
font-size: 14px; |
||||
} |
||||
|
||||
:deep(.el-table th.el-table__cell) { |
||||
background-color: var(--table-header-bg); |
||||
font-family: Alibaba-PuHuiTi-M; |
||||
font-size: 14px; |
||||
color: var(--table-header-text-color); |
||||
font-weight: 500; |
||||
} |
||||
|
||||
:deep(.el-table__row .cell) { |
||||
font-family: Alibaba-PuHuiTi-R; |
||||
line-height: 16px; |
||||
min-height: 20px; |
||||
font-weight: 400; |
||||
color: var(--text-color); |
||||
} |
||||
|
||||
.pagination-block { |
||||
user-select: none; |
||||
display: flex; |
||||
// place-content: center; |
||||
align-items: center; |
||||
justify-content: end; |
||||
height: 60px; |
||||
:deep(.el-pagination__editor.el-input) { |
||||
width: 66px; |
||||
} |
||||
:deep(.el-input__wrapper) { |
||||
height: 30px; |
||||
margin-left: 8px; |
||||
padding: 0 10px; |
||||
.el-input__inner { |
||||
font-size: 14px; |
||||
height: 100%; |
||||
} |
||||
} |
||||
:deep(.el-pagination) { |
||||
// width: cvw(200); |
||||
} |
||||
:deep(.el-pagination), |
||||
:deep(.el-pagination .el-icon), |
||||
:deep(.el-pagination li) { |
||||
font-size: 14px; |
||||
} |
||||
|
||||
:deep(.el-pagination .btn-next), |
||||
:deep(.el-pagination .btn-prev), |
||||
:deep(.el-pagination li) { |
||||
background-color: transparent; |
||||
} |
||||
:deep(.el-pager) { |
||||
height: 28px; |
||||
} |
||||
|
||||
:deep(.el-pagination li) { |
||||
min-width: 28px; |
||||
height: 28px; |
||||
border-radius: 2px; |
||||
border: 1px solid var(--pagination-border-color); |
||||
background-color: var(--pagination-bg); |
||||
|
||||
&:hover { |
||||
color: #619925; |
||||
} |
||||
} |
||||
:deep(.is-active) { |
||||
color: #619925 !important; |
||||
background: rgba(97, 153, 37, 0.06) !important; |
||||
border: 1px solid rgba(97, 153, 37, 1) !important; |
||||
border-radius: 2px; |
||||
} |
||||
:deep(.el-input__wrapper) { |
||||
background-color: var(--pagination-bg); |
||||
} |
||||
} |
||||
} |
||||
</style> |
@ -0,0 +1,210 @@
@@ -0,0 +1,210 @@
|
||||
<template> |
||||
<div class="edfs-wrap" :class="{ 'edfs-wrap-cvh': useCvh }"> |
||||
<div class="wrap-title" v-if="title || customLeft"> |
||||
{{}} |
||||
<div class="title-left" v-if="!customLeft"> |
||||
<template v-if="shape === 'rect'"> |
||||
<div |
||||
class="title-rect" |
||||
v-if="!useLeftColorBar" |
||||
:style="{ backgroundColor: shapeColor }" |
||||
></div> |
||||
</template> |
||||
<template v-else-if="shape === 'circle'"> |
||||
<div |
||||
class="title-circle" |
||||
v-if="!useLeftColorBar" |
||||
:style="{ backgroundColor: shapeColor }" |
||||
></div> |
||||
</template> |
||||
<div class="title-text"> |
||||
{{ title }} |
||||
</div> |
||||
</div> |
||||
<div class="title-left" v-else> |
||||
<div class="title-rect" v-if="!useLeftColorBar"></div> |
||||
<slot name="title-left"></slot> |
||||
</div> |
||||
<div class="title-right"> |
||||
<slot name="title-right"></slot> |
||||
</div> |
||||
</div> |
||||
|
||||
<el-scrollbar class="wrap-body" :class="{ 'is-title': title }" v-if="useScrollBar"> |
||||
<slot></slot> |
||||
</el-scrollbar> |
||||
<div class="wrap-body" :class="{ 'is-title': title }" v-else> |
||||
<slot></slot> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<script setup lang="ts"> |
||||
interface Props { |
||||
title?: string |
||||
useLeftColorBar?: boolean |
||||
barColor?: string |
||||
useCvh?: boolean |
||||
customLeft?: boolean |
||||
shape?: 'circle' | 'rect' |
||||
shapeColor?: string |
||||
useScrollBar?: boolean |
||||
} |
||||
const props = withDefaults(defineProps<Props>(), { |
||||
title: '', |
||||
useLeftColorBar: false, |
||||
barColor: '#fff', |
||||
useCvh: false, |
||||
customLeft: false, |
||||
shape: 'rect', |
||||
shapeColor: '#619925', |
||||
useScrollBar: false, |
||||
}) |
||||
</script> |
||||
|
||||
<style lang="scss" scoped> |
||||
.edfs-wrap { |
||||
width: 100%; |
||||
display: flex; |
||||
flex-direction: column; |
||||
overflow: hidden; |
||||
box-sizing: border-box; |
||||
background-color: var(--warp-bg); |
||||
border-radius: 4px; |
||||
.wrap-title { |
||||
height: 46px; |
||||
width: 100%; |
||||
display: flex; |
||||
justify-content: space-between; |
||||
padding: 0 16px; |
||||
box-sizing: border-box; |
||||
background-color: var(--warp-bg); |
||||
.title-left { |
||||
height: 100%; |
||||
display: flex; |
||||
gap: 4px; |
||||
align-items: center; |
||||
.title-rect { |
||||
width: 3px; |
||||
height: 14px; |
||||
} |
||||
.title-circle { |
||||
width: 8px; |
||||
height: 8px; |
||||
border-radius: 50%; |
||||
} |
||||
.title-text { |
||||
font-family: Alibaba-PuHuiTi-B; |
||||
font-size: 16px; |
||||
color: var(--text-color); |
||||
font-weight: 700; |
||||
} |
||||
} |
||||
.title-right { |
||||
// flex: 1; |
||||
height: 100%; |
||||
display: flex; |
||||
align-items: center; |
||||
box-sizing: border-box; |
||||
} |
||||
|
||||
:deep(.el-button) { |
||||
height: 26px; |
||||
font-size: 14px; |
||||
} |
||||
:deep(.el-icon) { |
||||
font-size: 14px; |
||||
} |
||||
:deep(.icon) { |
||||
font-size: 14px; |
||||
} |
||||
} |
||||
|
||||
.wrap-body { |
||||
width: 100%; |
||||
padding: 16px; |
||||
height: 100%; |
||||
box-sizing: border-box; |
||||
background-color: var(--warp-bg); |
||||
} |
||||
.is-title { |
||||
height: calc(100% - 46px); |
||||
} |
||||
} |
||||
|
||||
@media only screen and (max-width: 1440px) { |
||||
.edfs-wrap-cvh { |
||||
border-left: calc($W-scale-ratio-1440 * 4px) solid transparent; |
||||
border-radius: calc($W-scale-ratio-1440 * 4px); |
||||
.wrap-title { |
||||
height: calc($H-scale-ratio-900 * 46px); |
||||
padding: 0 calc($W-scale-ratio-1440 * 16px); |
||||
.title-left { |
||||
.title-rect { |
||||
width: calc($W-scale-ratio-1440 * 3px); |
||||
height: calc($H-scale-ratio-900 * 14px); |
||||
} |
||||
.title-text { |
||||
font-size: calc($W-scale-ratio-1440 * 16px); |
||||
margin-left: calc($W-scale-ratio-1440 * 4px); |
||||
} |
||||
} |
||||
|
||||
:deep(.el-button) { |
||||
height: calc($H-scale-ratio-900 * 26px); |
||||
font-size: calc($W-scale-ratio-1440 * 14px); |
||||
} |
||||
:deep(.el-icon) { |
||||
font-size: calc($W-scale-ratio-1440 * 14px); |
||||
} |
||||
:deep(.icon) { |
||||
font-size: calc($W-scale-ratio-1440 * 14px); |
||||
} |
||||
} |
||||
.wrap-body { |
||||
padding: calc($W-scale-ratio-1440 * 16px); |
||||
} |
||||
.is-title { |
||||
height: calc(100% - calc($H-scale-ratio-900 * 32px)); |
||||
} |
||||
} |
||||
} |
||||
|
||||
@media only screen and (min-width: 2560px) { |
||||
.edfs-wrap-cvh { |
||||
border-left: calc($W-scale-ratio-2560 * 4px) solid transparent; |
||||
border-radius: calc($W-scale-ratio-2560 * 4px); |
||||
.wrap-title { |
||||
height: calc($H-scale-ratio-1440 * 46px); |
||||
padding: 0 calc($W-scale-ratio-2560 * 16px); |
||||
.title-left { |
||||
.title-rect { |
||||
width: calc($W-scale-ratio-2560 * 3px); |
||||
height: calc($H-scale-ratio-1440 * 14px); |
||||
} |
||||
.title-text { |
||||
font-size: calc($W-scale-ratio-2560 * 16px); |
||||
margin-left: calc($W-scale-ratio-2560 * 4px); |
||||
} |
||||
} |
||||
|
||||
:deep(.el-button) { |
||||
height: calc($H-scale-ratio-1440 * 26px); |
||||
font-size: calc($W-scale-ratio-2560 * 14px); |
||||
} |
||||
:deep(.el-icon) { |
||||
font-size: calc($W-scale-ratio-2560 * 14px); |
||||
} |
||||
:deep(.icon) { |
||||
font-size: calc($W-scale-ratio-2560 * 14px); |
||||
} |
||||
} |
||||
.wrap-body { |
||||
padding: calc($W-scale-ratio-2560 * 16px); |
||||
} |
||||
.is-title { |
||||
height: calc(100% - calc($H-scale-ratio-1440 * 32px)); |
||||
} |
||||
} |
||||
} |
||||
</style> |
@ -0,0 +1,140 @@
@@ -0,0 +1,140 @@
|
||||
import { ElMessage, ElMessageBox, ElNotification } from 'element-plus' |
||||
export const useMessage = () => { |
||||
return { |
||||
// 消息提示
|
||||
info(content: string) { |
||||
ElMessage.info(content) |
||||
}, |
||||
// 错误消息
|
||||
error(content: string) { |
||||
ElMessage.error(content) |
||||
}, |
||||
// 成功消息
|
||||
success(content: string) { |
||||
ElMessage.success(content) |
||||
}, |
||||
// 警告消息
|
||||
warning(content: string) { |
||||
ElMessage.warning(content) |
||||
}, |
||||
// 弹出提示
|
||||
alert(content: string) { |
||||
ElMessageBox.alert(content, '系统提示') |
||||
}, |
||||
// 错误提示
|
||||
alertError(content: string) { |
||||
ElMessageBox.alert(content, '系统提示', { type: 'error' }) |
||||
}, |
||||
// 成功提示
|
||||
alertSuccess(content: string) { |
||||
ElMessageBox.alert(content, '系统提示', { type: 'success' }) |
||||
}, |
||||
// 警告提示
|
||||
alertWarning(content: string) { |
||||
ElMessageBox.alert(content, '系统提示', { type: 'warning' }) |
||||
}, |
||||
// 通知提示
|
||||
notify(content: string) { |
||||
ElNotification.info(content) |
||||
}, |
||||
// 错误通知
|
||||
notifyError(content: string, tip?: string) { |
||||
ElNotification.error({ |
||||
title: tip ? tip : '系统提示', |
||||
message: content, |
||||
}) |
||||
}, |
||||
// 成功通知
|
||||
notifySuccess(content: string) { |
||||
ElNotification.success(content) |
||||
}, |
||||
// 警告通知
|
||||
notifyWarning(content: string) { |
||||
ElNotification.warning(content) |
||||
}, |
||||
// 确认窗体
|
||||
confirm(content: string, tip?: string) { |
||||
return ElMessageBox.confirm(content, tip ? tip : '系统提示', { |
||||
confirmButtonText: '确定', |
||||
cancelButtonText: '取消', |
||||
confirmButtonClass: 'el-button--success', |
||||
cancelButtonClass: 'el-button--default', |
||||
type: 'warning', |
||||
}) |
||||
}, |
||||
forceConfirm(content: string, tip?: string, buttonText?: string) { |
||||
return ElMessageBox.confirm(content, tip ? tip : '系统提示', { |
||||
confirmButtonText: buttonText ?? '确定', |
||||
showCancelButton: false, |
||||
closeOnClickModal: false, |
||||
closeOnPressEscape: false, |
||||
confirmButtonClass: 'el-button--success', |
||||
cancelButtonClass: 'el-button--default', |
||||
showClose: false, |
||||
type: 'warning', |
||||
}) |
||||
}, |
||||
// 删除窗体
|
||||
delConfirm(content?: string, tip?: string) { |
||||
return new Promise((resolve, reject) => { |
||||
ElMessageBox.confirm( |
||||
content ? content : '是否确认删除数据项?', |
||||
tip ? tip : '系统提示', |
||||
{ |
||||
confirmButtonText: '确定', |
||||
cancelButtonText: '取消', |
||||
type: 'warning', |
||||
confirmButtonClass: 'el-button--success', |
||||
cancelButtonClass: 'el-button--default', |
||||
} |
||||
) |
||||
.then(() => { |
||||
resolve('') |
||||
}) |
||||
.catch(() => { |
||||
reject('') |
||||
}) |
||||
}) |
||||
}, |
||||
// 导出窗体
|
||||
exportConfirm(content?: string, tip?: string) { |
||||
return ElMessageBox.confirm( |
||||
content ? content : '是否确认导出数据项?', |
||||
tip ? tip : '系统提示', |
||||
{ |
||||
confirmButtonText: '确定', |
||||
cancelButtonText: '取消', |
||||
confirmButtonClass: 'el-button--success', |
||||
cancelButtonClass: 'el-button--default', |
||||
type: 'warning', |
||||
} |
||||
) |
||||
}, |
||||
// 提交内容
|
||||
prompt(content: string, tip: string) { |
||||
return ElMessageBox.prompt(content, tip, { |
||||
confirmButtonText: '确定', |
||||
cancelButtonText: '取消', |
||||
confirmButtonClass: 'el-button--success', |
||||
cancelButtonClass: 'el-button--default', |
||||
type: 'warning', |
||||
}) |
||||
}, |
||||
|
||||
promptVerify(content: string, tip: string, pattern: string, inputErrorMessage = '') { |
||||
const PatternRegExp = new RegExp( |
||||
`^${pattern.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')}$` |
||||
) |
||||
return ElMessageBox.prompt(content, tip, { |
||||
confirmButtonText: '确定', |
||||
cancelButtonText: '取消', |
||||
confirmButtonClass: 'el-button--success', |
||||
cancelButtonClass: 'el-button--default', |
||||
inputPattern: PatternRegExp, |
||||
inputErrorMessage: inputErrorMessage, |
||||
type: 'warning', |
||||
}) |
||||
}, |
||||
} |
||||
} |
||||
|
@ -0,0 +1,41 @@
@@ -0,0 +1,41 @@
|
||||
|
||||
const STORAGE_THEME = 'theme' |
||||
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches |
||||
const localTheme = 'light' |
||||
// localStorage.getItem(STORAGE_THEME) || (prefersDark ? 'dark' : 'light')
|
||||
|
||||
const theme = ref<'light' | 'dark'>(localTheme as 'light' | 'dark') |
||||
|
||||
const chartGraphicTextColor = computed(() => |
||||
theme.value === 'dark' ? '#ccc' : '#4d4d4d' |
||||
) |
||||
|
||||
watchEffect(() => { |
||||
const html = document.querySelector('html') |
||||
if (theme.value === 'dark') { |
||||
html?.classList.remove('light') |
||||
html?.classList.add('dark') |
||||
} else { |
||||
html?.classList.remove('dark') |
||||
html?.classList.add('light') |
||||
} |
||||
document.documentElement.setAttribute('data-theme', theme.value) |
||||
localStorage.setItem(STORAGE_THEME, theme.value) |
||||
}) |
||||
|
||||
function toggle() { |
||||
theme.value = theme.value === 'light' ? 'dark' : 'light' |
||||
} |
||||
function setTheme(value: 'light' | 'dark') { |
||||
theme.value = value |
||||
} |
||||
|
||||
export function useTheme() { |
||||
return { |
||||
theme, |
||||
toggle, |
||||
setTheme, |
||||
chartGraphicTextColor, |
||||
} |
||||
} |
@ -0,0 +1,135 @@
@@ -0,0 +1,135 @@
|
||||
import { WorkerCMD, ZmqCMD, } from '@/utils/zmq' |
||||
import type { ManualAction, PublishMsg, PubMsgData, SubMsgData, TimeoutMsg, ZmqMessage } from '@/utils/zmq' |
||||
import webWorker from '@/utils/zmqJsonWorker?worker' |
||||
|
||||
const defaultHost = '192.168.1.115' |
||||
|
||||
class ZMQJsonWorker { |
||||
private static instance: ZMQJsonWorker | null = null; // ➤ 单例实例
|
||||
private worker: Worker; |
||||
private scribeHandlers: Map<string, (msg: SubMsgData | PubMsgData) => void> = new Map(); |
||||
private pubTimeoutHandlers: Map<string, (msg: TimeoutMsg) => void> = new Map(); |
||||
private host: string; |
||||
private statusCallback: ((status: string) => void) | null = null; |
||||
|
||||
private constructor(host: string = defaultHost) { |
||||
this.host = host; |
||||
this.worker = new webWorker(); |
||||
this.worker.onmessage = this.handleMessage.bind(this); |
||||
} |
||||
|
||||
public static getInstance(host: string = defaultHost): ZMQJsonWorker { |
||||
if (!ZMQJsonWorker.instance) { |
||||
ZMQJsonWorker.instance = new ZMQJsonWorker(host); |
||||
} |
||||
return ZMQJsonWorker.instance; |
||||
} |
||||
|
||||
start() { |
||||
this.worker.postMessage({ cmd: WorkerCMD.START, msg: this.host }); |
||||
} |
||||
|
||||
stop() { |
||||
this.worker.postMessage({ cmd: WorkerCMD.STOP }); |
||||
this.worker.terminate(); |
||||
ZMQJsonWorker.instance = null; // ➤ 释放实例,允许重新创建
|
||||
} |
||||
|
||||
subscribe(topic: string, handler: (msg: any) => void, id?: string) { |
||||
this.scribeHandlers.set(`${topic}${id ? `-${id}` : ''}`, handler); |
||||
this.worker.postMessage({ cmd: WorkerCMD.SUBSCRIBE, topic }); |
||||
} |
||||
|
||||
unsubscribe(topic: string) { |
||||
// 遍历所有订阅消息,删除包含该主题的订阅
|
||||
for (const key in this.scribeHandlers) { |
||||
if (key.startsWith(topic)) { |
||||
this.scribeHandlers.delete(key); |
||||
} |
||||
} |
||||
|
||||
this.worker.postMessage({ cmd: WorkerCMD.UNSUBSCRIBE, topic }); |
||||
} |
||||
|
||||
publish<T extends ManualAction>(topic: string, msg: PublishMsg<T>, isTimeout: boolean = false, handler?: (msg: TimeoutMsg) => void) { |
||||
if (isTimeout) { |
||||
const timeoutId = msg.id |
||||
if (typeof handler !== 'function') { |
||||
console.warn(`发布主题${topic}失败, 回调函数handler为空`) |
||||
return |
||||
} |
||||
this.pubTimeoutHandlers.set(timeoutId, handler) |
||||
} |
||||
this.worker.postMessage({ cmd: WorkerCMD.PUBLISH, topic, msg: JSON.stringify(msg), isTimeout }); |
||||
} |
||||
|
||||
setStatusCallback(callback: (status: string) => void) { |
||||
this.statusCallback = callback; |
||||
} |
||||
|
||||
private handleSubscribeMessage(topic: string, json: PubMsgData & SubMsgData) { |
||||
const handler = this.scribeHandlers.get(topic); |
||||
if (handler) { |
||||
try { |
||||
handler(json); |
||||
} catch (error) { |
||||
console.error(`主题: ${topic} 处理失败:`, error); |
||||
} |
||||
} |
||||
} |
||||
|
||||
private handleTimeoutMessage(timeoutTopic: string, timeoutId: string) { |
||||
const handler = this.pubTimeoutHandlers.get(timeoutId); |
||||
if (handler) { |
||||
handler({ |
||||
timeoutId, |
||||
timeoutTopic |
||||
}); |
||||
} |
||||
} |
||||
|
||||
// 回收发布后订阅的回调
|
||||
private GC_pubReleaseSub(key: string) { |
||||
this.scribeHandlers.delete(key); |
||||
} |
||||
|
||||
// 回收超时消息的回调
|
||||
private GC_pubReleaseTimeout(key: string) { |
||||
this.pubTimeoutHandlers.delete(key); |
||||
} |
||||
|
||||
private handleMessage(e: MessageEvent<ZmqMessage>) { |
||||
const { cmd, msg, topic, community } = e.data; |
||||
|
||||
if (cmd === ZmqCMD.STATUS) { |
||||
const status = community ? 'disconnected' : 'connected'; |
||||
if (this.statusCallback) { |
||||
this.statusCallback(status); |
||||
} |
||||
} else if (cmd === ZmqCMD.JSON_MSG) { |
||||
const json = JSON.parse(msg) as PubMsgData & SubMsgData; |
||||
|
||||
if (json.action) { |
||||
// 处理订阅消息
|
||||
this.handleSubscribeMessage(topic, json); |
||||
} else { |
||||
// 处理需要发布消息并有返回的订阅消息
|
||||
this.handleSubscribeMessage(`${topic}-${json.id}`, json); |
||||
// 删除发布消息的回调
|
||||
if (Object.keys(json).includes('result') && json.result !== 'progress') { |
||||
this.GC_pubReleaseSub(`${topic}-${json.id}`) |
||||
this.GC_pubReleaseTimeout(`${json.id}`) |
||||
} |
||||
} |
||||
|
||||
} else if (cmd === ZmqCMD.TIMEOUT) { |
||||
this.handleTimeoutMessage(topic, msg); |
||||
this.GC_pubReleaseTimeout(msg) |
||||
console.log('pubTimeoutHandlers=>', this.pubTimeoutHandlers) |
||||
|
||||
} |
||||
} |
||||
} |
||||
|
||||
export default ZMQJsonWorker; |
||||
|
File diff suppressed because one or more lines are too long
@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
|
||||
import * as zmq from "jszmq"; |
||||
export default class ZmqClient { |
||||
sock: zmq.Dealer | zmq.Router | zmq.XSub | zmq.XPub | zmq.Pull | zmq.Push | zmq.Pair; |
||||
constructor(type: string); |
||||
zmqReq(host: string, request_type: string, dataStr: string, timeout: number): Promise<string>; |
||||
zmqSub(host: string, callback: (...args: any[]) => void): void; |
||||
zmqPub(host: string): void; |
||||
subscribe(topic: string): void; |
||||
unsubscribe(topic: string): void; |
||||
publishHex(topic: string, dataStr: string): Promise<void>; |
||||
publishStr(topic: string, dataStr: string): Promise<void>; |
||||
close(host: string, callback?: (...args: any[]) => void): void; |
||||
} |
||||
//# sourceMappingURL=zmqClient.d.ts.map
|
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
{ |
||||
"version": 3, |
||||
"file": "zmqClient.d.ts", |
||||
"sourceRoot": "", |
||||
"sources": [ |
||||
"../src/zmqClient.ts" |
||||
], |
||||
"names": [], |
||||
"mappings": "AAAA,OAAO,KAAK,GAAG,MAAM,OAAO,CAAC;AAG7B,MAAM,CAAC,OAAO,OAAO,SAAS;IAC1B,IAAI,EACE,GAAG,CAAC,MAAM,GACV,GAAG,CAAC,MAAM,GACV,GAAG,CAAC,IAAI,GACR,GAAG,CAAC,IAAI,GACR,GAAG,CAAC,IAAI,GACR,GAAG,CAAC,IAAI,GACR,GAAG,CAAC,IAAI,CAAC;gBACH,IAAI,EAAE,MAAM;IAgBlB,MAAM,CACR,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,CAAC;IA6BlB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI;IAKvD,MAAM,CAAC,IAAI,EAAE,MAAM;IAInB,SAAS,CAAC,KAAK,EAAE,MAAM;IAIvB,WAAW,CAAC,KAAK,EAAE,MAAM;IAInB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;IAOzC,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;IAO/C,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI;CAO1D" |
||||
} |
@ -0,0 +1,118 @@
@@ -0,0 +1,118 @@
|
||||
var __awaiter = |
||||
(this && this.__awaiter) || |
||||
function (thisArg, _arguments, P, generator) { |
||||
function adopt(value) { |
||||
return value instanceof P |
||||
? value |
||||
: new P(function (resolve) { |
||||
resolve(value) |
||||
}) |
||||
} |
||||
return new (P || (P = Promise))(function (resolve, reject) { |
||||
function fulfilled(value) { |
||||
try { |
||||
step(generator.next(value)) |
||||
} catch (e) { |
||||
reject(e) |
||||
} |
||||
} |
||||
function rejected(value) { |
||||
try { |
||||
step(generator['throw'](value)) |
||||
} catch (e) { |
||||
reject(e) |
||||
} |
||||
} |
||||
function step(result) { |
||||
result.done |
||||
? resolve(result.value) |
||||
: adopt(result.value).then(fulfilled, rejected) |
||||
} |
||||
step((generator = generator.apply(thisArg, _arguments || [])).next()) |
||||
}) |
||||
} |
||||
import * as zmq from 'jszmq' |
||||
import { Buffer } from 'buffer' |
||||
export default class ZmqClient { |
||||
constructor(type) { |
||||
switch (type) { |
||||
case 'req': |
||||
this.sock = zmq.socket('req') |
||||
break |
||||
case 'sub': |
||||
this.sock = zmq.socket('sub') |
||||
break |
||||
case 'pub': |
||||
this.sock = zmq.socket('pub') |
||||
break |
||||
default: |
||||
throw new Error('unsupported client type') |
||||
} |
||||
} |
||||
zmqReq(host, request_type, dataStr, timeout) { |
||||
return __awaiter(this, void 0, void 0, function* () { |
||||
this.sock.connect(host) |
||||
var request = new Array() |
||||
request[0] = request_type |
||||
request[1] = dataStr |
||||
this.sock.send(request) |
||||
return Promise.race([ |
||||
new Promise((resolve, reject) => { |
||||
this.sock.on('message', function (message) { |
||||
resolve(message.toString()) |
||||
}) |
||||
}), |
||||
new Promise((resolve, reject) => { |
||||
setTimeout(() => { |
||||
reject('timeout error') |
||||
}, timeout) |
||||
}), |
||||
]) |
||||
// var isMessageArrived = false;
|
||||
// const result: string = "";
|
||||
// var msgReceiveTimer = setTimeout(() => {
|
||||
// if (!isMessageArrived) {
|
||||
// console.log("msg receive timed out");
|
||||
// }
|
||||
// }, timeout); // 设置超时时间为5秒
|
||||
}) |
||||
} |
||||
zmqSub(host, callback) { |
||||
this.sock.connect(host) |
||||
this.sock.on('message', callback) |
||||
} |
||||
zmqPub(host) { |
||||
this.sock.connect(host) |
||||
} |
||||
subscribe(topic) { |
||||
this.sock.subscribe(topic) |
||||
} |
||||
unsubscribe(topic) { |
||||
this.sock.unsubscribe(topic) |
||||
} |
||||
publishHex(topic, dataStr) { |
||||
return __awaiter(this, void 0, void 0, function* () { |
||||
var request = new Array() |
||||
request[0] = topic |
||||
const buf = Buffer.from(dataStr, 'hex') |
||||
request[1] = buf |
||||
this.sock.send(request) |
||||
}) |
||||
} |
||||
publishStr(topic, dataStr) { |
||||
return __awaiter(this, void 0, void 0, function* () { |
||||
var request = new Array() |
||||
request[0] = topic |
||||
request[1] = dataStr |
||||
this.sock.send(request) |
||||
}) |
||||
} |
||||
close(host, callback) { |
||||
if (callback) { |
||||
this.sock.removeListener('message', callback) |
||||
} |
||||
this.sock.disconnect(host) |
||||
this.sock.close() |
||||
} |
||||
} |
||||
//# sourceMappingURL=zmqClient.js.map
|
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
{ |
||||
"version": 3, |
||||
"file": "zmqClient.js", |
||||
"sourceRoot": "", |
||||
"sources": [ |
||||
"../src/zmqClient.ts" |
||||
], |
||||
"names": [], |
||||
"mappings": ";;;;;;;;;AAAA,OAAO,KAAK,GAAG,MAAM,OAAO,CAAC;AAC7B,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,MAAM,CAAC,OAAO,OAAO,SAAS;IAS1B,YAAY,IAAY;QACpB,QAAQ,IAAI,EAAE;YACV,KAAK,KAAK;gBACN,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBAC9B,MAAM;YACV,KAAK,KAAK;gBACN,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBAC9B,MAAM;YACV,KAAK,KAAK;gBACN,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBAC9B,MAAM;YACV;gBACI,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;SAClD;IACL,CAAC;IAEK,MAAM,CACR,IAAY,EACZ,YAAoB,EACpB,OAAe,EACf,OAAe;;YAEf,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;YACxB,IAAI,OAAO,GAAG,IAAI,KAAK,EAAE,CAAC;YAC1B,OAAO,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC;YAC1B,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC;YACrB,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAExB,OAAO,OAAO,CAAC,IAAI,CAAC;gBAChB,IAAI,OAAO,CAAS,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;oBACpC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,SAAS,EAAE,UAAU,OAAO;wBACrC,OAAO,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;oBAChC,CAAC,CAAC,CAAC;gBACP,CAAC,CAAC;gBACF,IAAI,OAAO,CAAS,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;oBACpC,UAAU,CAAC,GAAG,EAAE;wBACZ,MAAM,CAAC,eAAe,CAAC,CAAC;oBAC5B,CAAC,EAAE,OAAO,CAAC,CAAC;gBAChB,CAAC,CAAC;aACL,CAAC,CAAC;YAEH,gCAAgC;YAChC,6BAA6B;YAC7B,2CAA2C;YAC3C,+BAA+B;YAC/B,gDAAgD;YAChD,QAAQ;YACR,4BAA4B;QAChC,CAAC;KAAA;IAED,MAAM,CAAC,IAAY,EAAE,QAAkC;QACnD,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACxB,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IACtC,CAAC;IAED,MAAM,CAAC,IAAY;QACf,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC;IAED,SAAS,CAAC,KAAa;QACnB,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAC/B,CAAC;IAED,WAAW,CAAC,KAAa;QACrB,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;IACjC,CAAC;IAEK,UAAU,CAAC,KAAa,EAAE,OAAe;;YAC3C,IAAI,OAAO,GAAG,IAAI,KAAK,EAAE,CAAC;YAC1B,OAAO,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC;YACnB,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;YACxC,OAAO,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC;YACjB,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC5B,CAAC;KAAA;IACK,UAAU,CAAC,KAAa,EAAE,OAAe;;YAC3C,IAAI,OAAO,GAAG,IAAI,KAAK,EAAE,CAAC;YAC1B,OAAO,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC;YACnB,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC;YACrB,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC5B,CAAC;KAAA;IAED,KAAK,CAAC,IAAY,EAAE,QAAmC;QACnD,IAAI,QAAQ,EAAE;YACV,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;SACjD;QACD,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;QAC3B,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;IACtB,CAAC;CACJ" |
||||
} |
@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
|
||||
import './assets/styles/main.css' |
||||
import 'virtual:uno.css' |
||||
import { createApp } from 'vue' |
||||
import { createPinia } from 'pinia' |
||||
|
||||
import App from './App.vue' |
||||
import router from './router' |
||||
|
||||
const app = createApp(App) |
||||
|
||||
app.use(createPinia()) |
||||
app.use(router) |
||||
|
||||
app.mount('#app') |
@ -0,0 +1,44 @@
@@ -0,0 +1,44 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router' |
||||
|
||||
export const defaultRouter = [ |
||||
{ |
||||
path: '/', |
||||
name: 'dashboard', |
||||
redirect: '/station', |
||||
component: () => import('@/views/layout/index.vue'), |
||||
meta: { |
||||
title: '首页', |
||||
isShow: true, |
||||
icon: 'i-mage:file-2', |
||||
}, |
||||
children: [ |
||||
{ |
||||
path: '/station', |
||||
name: 'station', |
||||
component: () => import('@/views/stationData/index.vue'), |
||||
meta: { |
||||
title: '站点数据', |
||||
isShow: true, |
||||
icon: 'i-icon-park-outline:data', |
||||
}, |
||||
}, |
||||
{ |
||||
path: '/data-transfer', |
||||
name: 'data-transfer', |
||||
component: () => import('@/views/stationData/transferData.vue'), |
||||
meta: { |
||||
title: '数据迁移', |
||||
isShow: false, |
||||
icon: 'i-mingcute:transfer-2-line', |
||||
}, |
||||
} |
||||
], |
||||
}, |
||||
] |
||||
|
||||
const router = createRouter({ |
||||
history: createWebHistory(import.meta.env.BASE_URL), |
||||
routes: defaultRouter, |
||||
}) |
||||
|
||||
export default router |
@ -0,0 +1,92 @@
@@ -0,0 +1,92 @@
|
||||
import { ref, computed } from 'vue' |
||||
import { defineStore } from 'pinia' |
||||
import type { IOnlineDevice } from '@/views/stationData/type' |
||||
import ZMQWorker from '@/composables/useZMQJsonWorker' |
||||
import { getSubTopic, type SubMsgData } from '@/utils/zmq' |
||||
import { getDeviceTopic } from '@/views/stationData/utils' |
||||
export const useTransferDataStore = defineStore('transfer', () => { |
||||
const subDevices = getSubTopic('client', 'status', 'transfer') |
||||
const worker = ZMQWorker.getInstance() |
||||
const isConnected = ref(false) |
||||
|
||||
const connectSite = ref<any>(null) |
||||
async function initConnectSite() { |
||||
if (connectSite.value) return |
||||
connectSite.value = { |
||||
title: '未命名站点', |
||||
edit: true, |
||||
editTitle: '未命名站点', |
||||
} |
||||
} |
||||
|
||||
const devicesMap = reactive(new Map<string, IOnlineDevice>()) |
||||
const checkDeviceStatusInterval = ref<NodeJS.Timeout>() |
||||
function checkDeviceStatus() { |
||||
checkDeviceStatusInterval.value = setInterval(() => { |
||||
const now = Date.now(); |
||||
devicesMap.forEach((device: IOnlineDevice, sn) => { |
||||
if (now - device.lastUpdated > 5000) { |
||||
device.status = '离线'; |
||||
} |
||||
}); |
||||
}, 1000); |
||||
} |
||||
|
||||
function getSubDevicesCb(msg: SubMsgData) { |
||||
const { feedback } = msg |
||||
const sn = feedback[1] |
||||
const device: IOnlineDevice = { |
||||
clientIp: feedback[0], |
||||
sn: sn, |
||||
stationName: feedback[2], |
||||
footprint: feedback[3] ?? '--', |
||||
lastUpdated: Date.now(), |
||||
status: '在线', // 初始状态为在线
|
||||
isChecked: false, |
||||
} |
||||
|
||||
|
||||
devicesMap.set(sn, device) |
||||
isConnected.value = devicesMap.size > 0 |
||||
} |
||||
|
||||
|
||||
const onlineCount = computed(() => { |
||||
return Array.from(devicesMap.values()).filter(item => item.status === '在线') |
||||
.length |
||||
}) |
||||
|
||||
const offlineCount = computed(() => { |
||||
return Array.from(devicesMap.values()).filter(item => item.status === '离线') |
||||
.length |
||||
}) |
||||
|
||||
|
||||
|
||||
|
||||
onMounted(() => { |
||||
worker.subscribe(getDeviceTopic, getSubDevicesCb) |
||||
checkDeviceStatus() |
||||
}) |
||||
|
||||
const route = useRoute() |
||||
|
||||
watch(() => route.path, (val) => { |
||||
if (!['/data-transfer', '/station'].includes(val)) { |
||||
clearInterval(checkDeviceStatusInterval.value) |
||||
worker.unsubscribe(subDevices) |
||||
} |
||||
}) |
||||
|
||||
|
||||
return { |
||||
isConnected, |
||||
devicesMap, |
||||
connectSite, |
||||
checkDeviceStatusInterval, |
||||
onlineCount, |
||||
offlineCount, |
||||
checkDeviceStatus, |
||||
initConnectSite, |
||||
} |
||||
}) |
@ -0,0 +1,53 @@
@@ -0,0 +1,53 @@
|
||||
// @unocss-include
|
||||
import type { Preset, PresetUnoTheme } from 'unocss' |
||||
|
||||
export function presetSoybeanAdmin(): Preset<PresetUnoTheme> { |
||||
const preset: Preset<PresetUnoTheme> = { |
||||
name: 'preset-soybean-admin', |
||||
shortcuts: [ |
||||
{ |
||||
'flex-center': 'flex justify-center items-center', |
||||
'flex-x-center': 'flex justify-center', |
||||
'flex-y-center': 'flex items-center', |
||||
'flex-col': 'flex flex-col', |
||||
'flex-col-center': 'flex-center flex-col', |
||||
'flex-col-stretch': 'flex-col items-stretch', |
||||
'i-flex-center': 'inline-flex justify-center items-center', |
||||
'i-flex-x-center': 'inline-flex justify-center', |
||||
'i-flex-y-center': 'inline-flex items-center', |
||||
'i-flex-col': 'flex-col inline-flex', |
||||
'i-flex-col-center': 'flex-col i-flex-center', |
||||
'i-flex-col-stretch': 'i-flex-col items-stretch', |
||||
'flex-1-hidden': 'flex-1 overflow-hidden', |
||||
}, |
||||
{ |
||||
'absolute-lt': 'absolute left-0 top-0', |
||||
'absolute-lb': 'absolute left-0 bottom-0', |
||||
'absolute-rt': 'absolute right-0 top-0', |
||||
'absolute-rb': 'absolute right-0 bottom-0', |
||||
'absolute-tl': 'absolute-lt', |
||||
'absolute-tr': 'absolute-rt', |
||||
'absolute-bl': 'absolute-lb', |
||||
'absolute-br': 'absolute-rb', |
||||
'absolute-center': 'absolute-lt flex-center size-full', |
||||
'fixed-lt': 'fixed left-0 top-0', |
||||
'fixed-lb': 'fixed left-0 bottom-0', |
||||
'fixed-rt': 'fixed right-0 top-0', |
||||
'fixed-rb': 'fixed right-0 bottom-0', |
||||
'fixed-tl': 'fixed-lt', |
||||
'fixed-tr': 'fixed-rt', |
||||
'fixed-bl': 'fixed-lb', |
||||
'fixed-br': 'fixed-rb', |
||||
'fixed-center': 'fixed-lt flex-center size-full', |
||||
}, |
||||
{ |
||||
'nowrap-hidden': 'overflow-hidden whitespace-nowrap', |
||||
'ellipsis-text': 'nowrap-hidden text-ellipsis', |
||||
}, |
||||
], |
||||
} |
||||
|
||||
return preset |
||||
} |
||||
|
||||
export default presetSoybeanAdmin |
@ -0,0 +1,168 @@
@@ -0,0 +1,168 @@
|
||||
import { v4 as uuidv4 } from 'uuid'; |
||||
export type ManualAction = |
||||
'init' | 'release' | 'write' | 'report' | 'lock' | 'unlock' | |
||||
'export' | 'cancel' | 'import' |
||||
|
||||
export type ZmqStatus = 'disconnected' | 'connected' |
||||
|
||||
export enum WorkerCMD { |
||||
START, |
||||
SUBSCRIBE, |
||||
UNSUBSCRIBE, |
||||
PUBLISH, |
||||
STOP, |
||||
SET_TIMEOUT |
||||
} |
||||
|
||||
|
||||
export enum ZmqCMD { |
||||
MSG, |
||||
JSON_MSG, |
||||
STATUS, |
||||
TIMEOUT |
||||
} |
||||
|
||||
export interface TimeoutMsg { |
||||
timeoutId: string |
||||
timeoutTopic: string |
||||
} |
||||
export interface ZmqMessage { |
||||
cmd: ZmqCMD |
||||
msg: string |
||||
community: boolean |
||||
topic: string |
||||
} |
||||
type TopicType = 'event' | 'status' |
||||
|
||||
|
||||
|
||||
// web端发布消息类型
|
||||
export interface PublishMsg<A> { |
||||
id: string // 每条消息的唯一标识
|
||||
action: A // 消息的动作
|
||||
reply: 'yes' | 'no' // 是否需要回复
|
||||
params: any // 消息的参数
|
||||
} |
||||
// web端发布消息服务端返回的数据类型
|
||||
export interface PubMsgData { |
||||
code: number |
||||
feedback: any, |
||||
id: string |
||||
result: 'success' | 'progress' | 'failure' | 'refuse' | 'error' |
||||
} |
||||
|
||||
// 订阅消息服务端返回的数据类型
|
||||
export interface SubMsgData { |
||||
id: string |
||||
action: 'report' |
||||
feedback: any, |
||||
reply: 'yes' | 'no' // 是否需要回复
|
||||
} |
||||
|
||||
|
||||
|
||||
/* |
||||
* @Description: 获取发布的主题 |
||||
* type: 主题类型 |
||||
* module: 模块名称 |
||||
* userid: 用户唯一标识 |
||||
*/ |
||||
|
||||
export function getPubTopic(type: TopicType, module: string,) { |
||||
return `web/${type}/${module}` |
||||
} |
||||
|
||||
export function getSubTopic(server: string, type: TopicType, module: string,) { |
||||
return `${server}/${type}/${module}` |
||||
} |
||||
|
||||
// 获取随机id
|
||||
|
||||
export function getRandomId() { |
||||
const uniqueId = `${Date.now()}-${uuidv4()}`; |
||||
return uniqueId |
||||
} |
||||
|
||||
export function generatePubMsg<A>(massage: Omit<PublishMsg<A>, 'id'>): PublishMsg<A> { |
||||
return { |
||||
id: getRandomId(), |
||||
action: massage.action, |
||||
reply: massage.reply, |
||||
params: massage.params |
||||
} |
||||
} |
||||
|
||||
export function getLockPubMsg(action: 'lock' | 'unlock'): PublishMsg<'lock' | 'unlock'> { |
||||
return { |
||||
id: getRandomId(), |
||||
action: action, |
||||
params: [], |
||||
reply: 'yes' |
||||
} |
||||
} |
||||
|
||||
export function pollingWithCallback(intervalInSeconds: number, callback: Function) { |
||||
let counter = 0 |
||||
const fps = 60 |
||||
|
||||
function poll() { |
||||
counter++ |
||||
if (counter >= intervalInSeconds * fps) { |
||||
callback() |
||||
counter = 0 |
||||
} |
||||
requestAnimationFrame(poll) |
||||
} |
||||
|
||||
poll() |
||||
} |
||||
|
||||
export function getPubInitData<T extends ManualAction>(action: T, params: any, reply: 'yes' | 'no' = 'yes') { |
||||
return generatePubMsg<T>({ |
||||
action, |
||||
reply, |
||||
params |
||||
}) |
||||
} |
||||
export function isHexadecimal(text: string) { |
||||
const hexRegex = /^[0-9A-Fa-f]+$/ |
||||
return hexRegex.test(text) |
||||
} |
||||
|
||||
export function stringToHex(str: string) { |
||||
return Array.from(str) |
||||
.map(char => char.charCodeAt(0).toString(16).toUpperCase()) |
||||
.join('') |
||||
} |
||||
|
||||
export function stringToDecimalismNumbers(str: string): number[] { |
||||
return Array.from(str).map(char => char.charCodeAt(0)) |
||||
} |
||||
|
||||
export function hexToArray(num: string) { |
||||
const numStr = num.toString() |
||||
let paddedNumStr = numStr |
||||
if (numStr.length % 2 !== 0) { |
||||
paddedNumStr = numStr.slice(0, -1) + '0' + numStr.slice(-1) |
||||
} |
||||
|
||||
const result = [] |
||||
|
||||
for (let i = 0; i < paddedNumStr.length; i += 2) { |
||||
result.push(paddedNumStr.slice(i, i + 2)) |
||||
} |
||||
|
||||
return result |
||||
} |
||||
|
||||
export function hexToDecimal(hex: string[]) { |
||||
return hex.map(item => parseInt(item, 16)) |
||||
} |
||||
|
||||
export function decimalToHexArray(decimalArray: number[]) { |
||||
return decimalArray.map(num => num.toString(16).toUpperCase()).join('') |
||||
} |
||||
|
||||
export function decimalToString(arr: number[]) { |
||||
return arr.map(num => String.fromCharCode(num)).join('') |
||||
} |
@ -0,0 +1,164 @@
@@ -0,0 +1,164 @@
|
||||
import ZmqClient from '@/lib/zmq/zmqClient' |
||||
import { WorkerCMD, ZmqCMD, type PublishMsg, type PubMsgData, } from './zmq' |
||||
|
||||
|
||||
const HEARTBEAT_TOPIC = 'HEARTBEAT' |
||||
const HEARTBEAT_INTERVAL = 3000 |
||||
const STATUS_CHECK_INTERVAL = 1000 |
||||
let messageTimeout = 10000 |
||||
|
||||
let heartClient: ZmqClient | null, subClient: ZmqClient | null, pubClient: ZmqClient | null |
||||
let subHost = '', pubHost = '' |
||||
let lastHeartbeatTime = 0 |
||||
let statusTimerId: ReturnType<typeof setInterval> | null |
||||
let isConnectionError = false |
||||
|
||||
function decodeMessage(data: Uint8Array) { |
||||
return new TextDecoder().decode(data) |
||||
} |
||||
|
||||
function updateHeartbeat() { |
||||
lastHeartbeatTime = Date.now() |
||||
} |
||||
|
||||
function changeConnectionStatus(hasError: boolean) { |
||||
if (isConnectionError !== hasError) { |
||||
isConnectionError = hasError |
||||
console.log('ZMQ连接状态更新:', hasError ? '断开' : '连接') |
||||
postMessage({ cmd: ZmqCMD.STATUS, community: hasError }) |
||||
} |
||||
} |
||||
|
||||
function monitorConnection() { |
||||
if (statusTimerId) return |
||||
|
||||
lastHeartbeatTime = Date.now() |
||||
statusTimerId = setInterval(() => { |
||||
const currentTime = Date.now() |
||||
const hasError = currentTime - lastHeartbeatTime > HEARTBEAT_INTERVAL |
||||
changeConnectionStatus(hasError) |
||||
}, STATUS_CHECK_INTERVAL) |
||||
} |
||||
|
||||
function stopMonitoringConnection() { |
||||
if (statusTimerId) { |
||||
clearInterval(statusTimerId) |
||||
statusTimerId = null |
||||
} |
||||
changeConnectionStatus(false) |
||||
} |
||||
|
||||
function disconnect() { |
||||
if (subClient) { |
||||
subClient.close(subHost, handleZmqMessage) |
||||
subClient = null |
||||
} |
||||
|
||||
if (heartClient) { |
||||
heartClient.unsubscribe(HEARTBEAT_TOPIC) |
||||
heartClient.close(subHost, updateHeartbeat) |
||||
heartClient = null |
||||
} |
||||
|
||||
if (pubClient) { |
||||
pubClient.close(pubHost) |
||||
pubClient = null |
||||
} |
||||
|
||||
stopMonitoringConnection() |
||||
} |
||||
|
||||
function handleZmqMessage(topic: Uint8Array, msg: Uint8Array) { |
||||
try { |
||||
if (msg instanceof Uint8Array) { |
||||
const jsonMessage = decodeMessage(msg) as string |
||||
postMessage({ |
||||
topic: decodeMessage(topic), |
||||
cmd: ZmqCMD.JSON_MSG, |
||||
msg: jsonMessage |
||||
}) |
||||
const parsedMessage = JSON.parse(jsonMessage) as PubMsgData |
||||
if (parsedMessage.id && traceMessages.has(parsedMessage.id)) { |
||||
if (parsedMessage.result === 'progress') { |
||||
// 重置消息超时时间
|
||||
const val = traceMessages.get(parsedMessage.id) |
||||
if (val) { |
||||
val.timestamp = Date.now() |
||||
} |
||||
traceMessages.set(parsedMessage.id, val) |
||||
} else { |
||||
traceMessages.delete(parsedMessage.id) |
||||
} |
||||
} |
||||
} |
||||
} catch (e) { |
||||
console.error('handleZmqMessage error:', e) |
||||
} |
||||
} |
||||
|
||||
function connect(host: string) { |
||||
disconnect() |
||||
|
||||
subHost = `ws://${host}:15555` |
||||
subClient = new ZmqClient('sub') |
||||
subClient.zmqSub(subHost, handleZmqMessage) |
||||
|
||||
heartClient = new ZmqClient('sub') |
||||
heartClient.zmqSub(subHost, updateHeartbeat) |
||||
heartClient.subscribe(HEARTBEAT_TOPIC) |
||||
|
||||
monitorConnection() |
||||
|
||||
pubHost = `ws://${host}:15556` |
||||
pubClient = new ZmqClient('pub') |
||||
pubClient.zmqPub(pubHost) |
||||
setInterval(() => { |
||||
const now = Date.now() |
||||
traceMessages.forEach((val, id) => { |
||||
if (now - val.timestamp > messageTimeout) { |
||||
console.warn(`Message ${id} timed out.`) |
||||
postMessage({ |
||||
cmd: ZmqCMD.TIMEOUT, |
||||
topic: val.topic.replace(/^web\//, 'server/'), |
||||
msg: id |
||||
}) |
||||
traceMessages.delete(id) |
||||
} |
||||
}) |
||||
}, 1000) |
||||
} |
||||
|
||||
const traceMessages = new Map<string, any>() |
||||
|
||||
self.onmessage = function (event) { |
||||
const { cmd, topic, msg, isTimeout = false } = event.data |
||||
|
||||
switch (cmd) { |
||||
case WorkerCMD.START: |
||||
connect(msg) |
||||
break |
||||
case WorkerCMD.SET_TIMEOUT: |
||||
messageTimeout = msg |
||||
break |
||||
case WorkerCMD.SUBSCRIBE: |
||||
subClient?.subscribe(topic) |
||||
break |
||||
case WorkerCMD.UNSUBSCRIBE: |
||||
subClient?.unsubscribe(topic) |
||||
break |
||||
case WorkerCMD.PUBLISH: |
||||
if (!msg) throw new Error('msg is required') |
||||
if (isTimeout) { |
||||
const parseMsg = JSON.parse(msg) as PublishMsg<string> |
||||
traceMessages.set(parseMsg.id, { |
||||
timestamp: Date.now(), |
||||
topic: topic |
||||
}) |
||||
} |
||||
pubClient?.publishStr(topic, msg) |
||||
break |
||||
case WorkerCMD.STOP: |
||||
disconnect() |
||||
break |
||||
} |
||||
} |
@ -0,0 +1,197 @@
@@ -0,0 +1,197 @@
|
||||
<template> |
||||
<div class="common-layout"> |
||||
<el-container> |
||||
<el-aside class="aside-wrap"> |
||||
<RouterLink |
||||
to="/" |
||||
class="layout-logo" |
||||
:class="{ 'layout-logo-collapse': isCollapse }" |
||||
> |
||||
<svg |
||||
class="inline-block text-32px" |
||||
width="1em" |
||||
height="1em" |
||||
viewBox="0 0 160 160" |
||||
xmlns="http://www.w3.org/2000/svg" |
||||
> |
||||
<path |
||||
d="M81.28 55.9c-.1-11.67-2.93-22.55-9.37-32.38-1-1.5-2.14-2.86-2.5-4.71a8.1 8.1 0 014-8.61 7.89 7.89 0 019.3 1.23 35.999 35.999 0 015.9 8.83 75.18 75.18 0 018.44 28.58 83.211 83.211 0 01-5.23 36.74 102.983 102.983 0 01-3 7.28 1.2 1.2 0 000 1.41c9.58 13.3 21.76 23 37.85 27.24a54.37 54.37 0 0019.68 1.57 7.72 7.72 0 018.36 6.9 7.903 7.903 0 01-6.7 9 64.744 64.744 0 01-23-1.33 77.68 77.68 0 01-36.93-19.88 93.628 93.628 0 01-11.91-13.71 2.18 2.18 0 00-2.3-1.06 72.744 72.744 0 00-27.38 7.55c-11.6 6-20.67 14.58-26.4 26.45a10.134 10.134 0 01-3.7 4.7 8 8 0 01-9.19-.7 7.86 7.86 0 01-2.36-9.28 60.324 60.324 0 018.72-14.52c12.2-15.43 28.21-24.59 47.32-28.57A85.085 85.085 0 0173.07 87c.524.015 1-.307 1.18-.8a76.06 76.06 0 006.53-22.3c.351-2.652.518-5.325.5-8z" |
||||
fill="currentColor" |
||||
></path> |
||||
<path |
||||
d="M136.26 108.34a44.742 44.742 0 01-11.13-2.87 46.108 46.108 0 01-19.66-13.76 8 8 0 015.72-13.22 7.93 7.93 0 016.54 2.93 33.27 33.27 0 0018.87 10.75c1.546.155 3.058.553 4.48 1.18a8.08 8.08 0 013.84 9.21c-.92 3.52-4.13 5.81-8.66 5.78zm-80.6-75.02a7.61 7.61 0 016.64 5 49.139 49.139 0 013.64 17 46.33 46.33 0 01-2.46 17.28c-2 5.77-8.24 7.79-12.89 4.15a8.1 8.1 0 01-2.39-9 31.679 31.679 0 001.68-12.36 35.77 35.77 0 00-2.43-11c-2.1-5.45 1.75-11.07 8.21-11.07zm22.26 93.25a8 8 0 01-6.68 7.86 32.88 32.88 0 00-19.7 12.19 8.13 8.13 0 01-11.21 1.62 8 8 0 01-1.41-11.58A51.043 51.043 0 0154 123.81a45.842 45.842 0 0114-5.1c5.35-1.04 9.91 2.56 9.92 7.86z" |
||||
fill="currentColor" |
||||
></path> |
||||
</svg> |
||||
<h2 class="pl-8px text-16px font-bold" v-show="!isCollapse"> |
||||
数据迁移管理平台 |
||||
</h2> |
||||
</RouterLink> |
||||
<el-menu |
||||
class="layout-menu" |
||||
:default-active="activeMenu" |
||||
@select="menuSelect" |
||||
router |
||||
:collapse="isCollapse" |
||||
> |
||||
<template v-for="router in menuList"> |
||||
<template v-if="router.meta?.isShow"> |
||||
<el-sub-menu |
||||
v-if="router?.children?.filter((item: any) => item.meta?.isShow).length" |
||||
:index="router.path" |
||||
:key="router.path" |
||||
> |
||||
<template #title> |
||||
<div :class="router.meta.icon" class="menu-icon"></div> |
||||
<span>{{ router.meta.title }}</span> |
||||
</template> |
||||
<template v-for="child in router?.children"> |
||||
<el-menu-item |
||||
v-if="child?.meta?.isShow" |
||||
:key="child.path" |
||||
:index="`${router.path}/${child.path}`" |
||||
> |
||||
<div :class="child.meta.icon" class="menu-icon"></div> |
||||
<span>{{ child.meta.title }}</span> |
||||
</el-menu-item> |
||||
</template> |
||||
</el-sub-menu> |
||||
<el-menu-item v-else :index="router.path" :key="router?.path"> |
||||
<div :class="router.meta.icon" class="menu-icon"></div> |
||||
<span>{{ router.meta.title }}</span> |
||||
</el-menu-item> |
||||
</template> |
||||
</template> |
||||
</el-menu> |
||||
</el-aside> |
||||
<el-container> |
||||
<el-header> |
||||
<div class="flex items-center gap-col-2"> |
||||
<el-button class="collapes-btn" @click="isCollapse = !isCollapse"> |
||||
<div :class="isCollapse ? unfold : fold"></div> |
||||
</el-button> |
||||
<div> |
||||
{{ currentTime }} |
||||
</div> |
||||
</div> |
||||
<!-- <div class="flex items-center gap-col-2 p-r-20"> |
||||
<el-avatar :src="circleUrl" class="avatar" /> |
||||
<span class="username">John Doe</span> |
||||
</div> --> |
||||
</el-header> |
||||
<main class="main-wrap"> |
||||
<RouterView /> |
||||
</main> |
||||
</el-container> |
||||
</el-container> |
||||
</div> |
||||
</template> |
||||
|
||||
<script setup lang="ts"> |
||||
import { useTheme } from '@/composables/useTheme' |
||||
import { defaultRouter } from '@/router' |
||||
import dayjs from 'dayjs' |
||||
|
||||
const unfold = 'i-icon-park-outline:menu-unfold' |
||||
const fold = 'i-icon-park-outline:menu-fold' |
||||
|
||||
const { theme } = useTheme() |
||||
|
||||
const menuList = computed<any[]>(() => defaultRouter[0].children) |
||||
|
||||
const circleUrl = ref( |
||||
'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png' |
||||
) |
||||
const isCollapse = ref(false) |
||||
|
||||
const getIconClass = (icon: string) => { |
||||
return icon |
||||
} |
||||
|
||||
const { push, currentRoute } = useRouter() |
||||
const activeMenu = computed(() => { |
||||
const { meta, path } = unref(currentRoute) |
||||
|
||||
return path |
||||
}) |
||||
|
||||
function menuSelect(path: string) { |
||||
push(path) |
||||
} |
||||
|
||||
const currentTime = ref('123') |
||||
const updateTime = () => { |
||||
currentTime.value = dayjs().format('YYYY-MM-DD HH:mm:ss') |
||||
requestAnimationFrame(updateTime) |
||||
} |
||||
onMounted(() => { |
||||
updateTime() |
||||
}) |
||||
</script> |
||||
<style lang="scss" scoped> |
||||
.common-layout { |
||||
@apply w-full h-full bg-[#F2F3F5]; |
||||
|
||||
:deep(.el-container) { |
||||
@apply w-full h-full; |
||||
} |
||||
:deep(.el-header) { |
||||
@apply h-56 w-full p0 flex items-center justify-between bg-white; |
||||
} |
||||
:deep(.el-main) { |
||||
@apply p0; |
||||
} |
||||
:deep(.el-aside) { |
||||
@apply w-auto; |
||||
} |
||||
:deep(.el-menu) { |
||||
@apply h-full; |
||||
} |
||||
|
||||
.aside-wrap { |
||||
@apply flex-col; |
||||
border-radius: 4px; |
||||
height: calc(100vh - 16px); |
||||
} |
||||
.layout-logo { |
||||
@apply w-full flex-center nowrap-hidden h-56 bg-white; |
||||
} |
||||
|
||||
.layout-menu { |
||||
height: calc(100vh - 56px); |
||||
@apply border-r-none; |
||||
} |
||||
.layout-menu:not(.el-menu--collapse) { |
||||
width: 220px; |
||||
height: calc(100vh - 56px); |
||||
} |
||||
.layout-logo { |
||||
transition: width 0.3s ease-in-out; |
||||
width: 220px; |
||||
} |
||||
.layout-logo-collapse { |
||||
width: 64px; |
||||
} |
||||
:deep(.el-header) { |
||||
@apply p-x-12; |
||||
} |
||||
.collapes-btn { |
||||
@apply border-none; |
||||
&:hover { |
||||
background-color: rgba(46, 51, 56, 0.09); |
||||
color: rgb(51, 54, 57); |
||||
} |
||||
} |
||||
.avatar { |
||||
@apply w-24 h-24; |
||||
} |
||||
.menu-icon { |
||||
@apply m-r-8; |
||||
} |
||||
|
||||
.main-wrap { |
||||
height: calc(100vh - 56px); |
||||
@apply p-16; |
||||
} |
||||
} |
||||
</style> |
@ -0,0 +1,218 @@
@@ -0,0 +1,218 @@
|
||||
<template> |
||||
<div class="fault-rule-drawer"> |
||||
<el-drawer |
||||
v-model="isShowDrawer" |
||||
:title="title" |
||||
direction="rtl" |
||||
size="60%" |
||||
modal-class="model-dev-opn" |
||||
:before-close="handleBeforeClose" |
||||
> |
||||
<main class="drawer-box"> |
||||
<EdfsTable |
||||
class="table" |
||||
v-loading="loading" |
||||
:data="tableData" |
||||
ref="tableRef" |
||||
:highlight-current-row="true" |
||||
:page-total="total" |
||||
:current-page="queryParams.pageNo" |
||||
:page-size="queryParams.pageSize" |
||||
row-class-name="row" |
||||
@pageCurrentChange="handleJump" |
||||
> |
||||
<template v-for="(col, idx) in tableCol" :key="idx"> |
||||
<el-table-column |
||||
v-if="col.prop.endsWith('Time')" |
||||
:label="col.label" |
||||
:min-width="col.minWidth" |
||||
> |
||||
<template #default="scope"> |
||||
{{ dayjs(scope.row.createTime).format('YYYY-MM-DD HH:mm:ss') }} |
||||
</template> |
||||
</el-table-column> |
||||
<el-table-column |
||||
v-else |
||||
:prop="col.prop" |
||||
:label="col.label" |
||||
:min-width="col.minWidth" |
||||
/> |
||||
</template> |
||||
</EdfsTable> |
||||
</main> |
||||
</el-drawer> |
||||
</div> |
||||
</template> |
||||
|
||||
<script setup lang="ts"> |
||||
import dayjs from 'dayjs' |
||||
import { |
||||
getPubInitData, |
||||
type ManualAction, |
||||
type PubMsgData, |
||||
type TimeoutMsg, |
||||
} from '@/utils/zmq' |
||||
import ZMQWorker from '@/composables/useZMQJsonWorker' |
||||
import type { IOfflineDevice, IOnlineDevice } from '../type' |
||||
import { |
||||
getDeviceDetails, |
||||
getPoints, |
||||
type IPointsParams, |
||||
type ISiteList, |
||||
} from '@/api/module/transfer' |
||||
import { useMessage } from '@/composables/useMessage' |
||||
import { getTransferTopic, postTransferTopic } from '../utils' |
||||
|
||||
const worker = ZMQWorker.getInstance() |
||||
const isShowDrawer = defineModel<boolean>() |
||||
const title = computed(() => (props.isTransfer ? '数据详情' : `已迁移数据详情`)) |
||||
const message = useMessage() |
||||
|
||||
const props = defineProps<{ |
||||
siteInfo: ISiteList |
||||
isTransfer: boolean |
||||
}>() |
||||
|
||||
const emit = defineEmits(['on-save']) |
||||
const pubIdWithDevice = new Map< |
||||
string, |
||||
{ device: IOfflineDevice | IOnlineDevice; action: ManualAction } |
||||
>() |
||||
|
||||
const tableCol = ref<any[]>([]) |
||||
|
||||
const curDevice = ref<IOfflineDevice | IOnlineDevice>() |
||||
function open(device: IOfflineDevice | IOnlineDevice) { |
||||
curDevice.value = device |
||||
props.isTransfer ? loadDeviceDetails() : zmqImport(device as IOfflineDevice) |
||||
isShowDrawer.value = true |
||||
} |
||||
|
||||
const tableData = ref<any[]>([]) |
||||
const loading = ref(true) |
||||
const total = ref(0) |
||||
const queryParams = reactive({ |
||||
pageNo: 1, |
||||
pageSize: undefined, |
||||
}) |
||||
const tableRef = ref() |
||||
|
||||
const pointsData = ref<any[]>([]) |
||||
async function loadPoints() { |
||||
const params: IPointsParams = {} |
||||
if (props.isTransfer) { |
||||
const onlineDevice = curDevice.value as IOnlineDevice |
||||
params.isLocal = false |
||||
params.host = onlineDevice.clientIp |
||||
} else { |
||||
const offlineDevice = curDevice.value as IOfflineDevice |
||||
params.sn = offlineDevice.sn |
||||
params.site = props.siteInfo.name |
||||
params.isLocal = true |
||||
} |
||||
|
||||
const res = await getPoints(params) |
||||
|
||||
if (res.code === 0) { |
||||
pointsData.value = res.data |
||||
tableCol.value = res.data.map((i: any) => ({ |
||||
label: i.point_name, |
||||
prop: i.point_id, |
||||
minWidth: '10%', |
||||
})) |
||||
tableCol.value.push({ |
||||
label: '时间', |
||||
prop: 'ts', |
||||
minWidth: '10%', |
||||
}) |
||||
} |
||||
return res |
||||
} |
||||
|
||||
async function loadDeviceDetails() { |
||||
if (!queryParams.pageSize) { |
||||
queryParams.pageSize = tableRef.value?.getSize() ?? 100 |
||||
} |
||||
|
||||
loading.value = true |
||||
const poinsRes = await loadPoints() |
||||
|
||||
if (!poinsRes || poinsRes.code !== 0) { |
||||
message.error('获取点位数据失败') |
||||
loading.value = false |
||||
return |
||||
} |
||||
const res = await getDeviceDetails({ |
||||
columns: ['ts', ...pointsData.value.map(i => i.point_id)], |
||||
isLocal: props.isTransfer ? false : true, |
||||
limit: queryParams?.pageSize ?? 100, |
||||
offset: queryParams.pageNo, |
||||
}) |
||||
if (res.code === 0) { |
||||
tableData.value = res.data.results |
||||
total.value = res.data.total |
||||
} |
||||
loading.value = false |
||||
} |
||||
function handleJump(page: number) { |
||||
queryParams.pageNo = page |
||||
loadDeviceDetails() |
||||
} |
||||
|
||||
function zmqImport(device: IOfflineDevice) { |
||||
if (!device.sn || !props.siteInfo.name) { |
||||
message.error('未找到站点或设备') |
||||
return |
||||
} |
||||
const msg = getPubInitData<'import'>( |
||||
'import', |
||||
['', '', '', '', '', '', `${props.siteInfo.name}/${device.sn}`], |
||||
'yes' |
||||
) |
||||
pubIdWithDevice.set(msg.id, { device, action: 'import' }) |
||||
worker.publish(postTransferTopic, msg, true, zmqTimeoutCb) |
||||
worker.subscribe(getTransferTopic, zmqImportCb, msg.id) |
||||
} |
||||
|
||||
function zmqImportCb(msg: PubMsgData) { |
||||
const { id, result } = msg |
||||
if (result !== 'progress') { |
||||
if (result === 'success') { |
||||
loading.value = true |
||||
loadDeviceDetails() |
||||
} else { |
||||
message.error(`设备数据获取失败`) |
||||
} |
||||
pubIdWithDevice.delete(id) |
||||
} |
||||
} |
||||
|
||||
function zmqTimeoutCb(msg: TimeoutMsg) { |
||||
const { device, action } = pubIdWithDevice.get(msg.timeoutId)! |
||||
if (device && action === 'import') { |
||||
message.error(`设备${device.sn},查询信息超时,请稍后重试`) |
||||
pubIdWithDevice.delete(msg.timeoutId) |
||||
} |
||||
} |
||||
|
||||
function handleBeforeClose(done: () => void) { |
||||
isShowDrawer.value = false |
||||
done() |
||||
} |
||||
defineExpose({ |
||||
open, |
||||
}) |
||||
</script> |
||||
|
||||
<style scoped lang="scss"> |
||||
.fault-rule-drawer { |
||||
font-size: 16px; |
||||
:deep(.el-drawer__header) { |
||||
color: var(--text-color); |
||||
} |
||||
.drawer-box { |
||||
width: 100%; |
||||
height: 100%; |
||||
} |
||||
} |
||||
</style> |
@ -0,0 +1,122 @@
@@ -0,0 +1,122 @@
|
||||
<template> |
||||
<EdfsDialog |
||||
:title="isBatchTransfer ? '批量迁移' : '数据迁移'" |
||||
:is-show="visible" |
||||
width="40%" |
||||
@on-close="close" |
||||
@on-save="onSave" |
||||
> |
||||
<div class="flex-col gap-10 w-80% m-x-30px"> |
||||
<el-row> |
||||
<div class="label"> |
||||
<span class="require">*</span> |
||||
数据时间范围: |
||||
</div> |
||||
<el-date-picker |
||||
v-model="form.timeArr" |
||||
type="datetimerange" |
||||
class="flex-1" |
||||
start-placeholder="开始时间" |
||||
end-placeholder="结束时间" |
||||
/></el-row> |
||||
</div> |
||||
</EdfsDialog> |
||||
</template> |
||||
|
||||
<script setup lang="ts"> |
||||
import dayjs from 'dayjs' |
||||
import { getPubInitData, type PublishMsg } from '@/utils/zmq' |
||||
import { cloneDeep } from 'lodash-es' |
||||
import type { IOnlineDevice } from '../type' |
||||
import { useMessage } from '@/composables/useMessage' |
||||
const message = useMessage() |
||||
|
||||
const emit = defineEmits<{ |
||||
'on-save': [PublishMsg<'export'>, IOnlineDevice] |
||||
}>() |
||||
|
||||
const props = defineProps<{ |
||||
isBatchTransfer: boolean |
||||
}>() |
||||
|
||||
const visible = ref(false) |
||||
const fromData = { |
||||
timeArr: [], |
||||
} |
||||
|
||||
const curDevice = ref<IOnlineDevice>() |
||||
const form = ref(cloneDeep(fromData)) |
||||
|
||||
const batchClientIp = ref('') |
||||
const batchPath = ref('') |
||||
|
||||
function open(item: IOnlineDevice, clientIps: string, paths: string) { |
||||
curDevice.value = item |
||||
visible.value = true |
||||
batchClientIp.value = clientIps |
||||
batchPath.value = paths |
||||
} |
||||
|
||||
function onSave() { |
||||
if (!verifyData()) return |
||||
if (!curDevice.value) { |
||||
message.error('请选择设备') |
||||
return |
||||
} |
||||
const params = [ |
||||
`${props.isBatchTransfer ? batchClientIp.value : curDevice.value.clientIp}`, |
||||
'', |
||||
'', |
||||
'', |
||||
'', |
||||
'', |
||||
`${ |
||||
props.isBatchTransfer |
||||
? batchPath.value |
||||
: `${curDevice.value.stationName}/${curDevice.value.sn}` |
||||
}`, |
||||
`${dayjs(form.value.timeArr[0]).valueOf()},${dayjs(form.value.timeArr[1]).valueOf()}`, |
||||
] |
||||
const msg = getPubInitData<'export'>('export', params) |
||||
emit('on-save', msg, curDevice.value as IOnlineDevice) |
||||
close() |
||||
} |
||||
|
||||
function close() { |
||||
form.value = cloneDeep(fromData) |
||||
visible.value = false |
||||
curDevice.value = undefined |
||||
batchClientIp.value = '' |
||||
batchPath.value = '' |
||||
} |
||||
|
||||
function verifyData() { |
||||
if (!form.value.timeArr.length) { |
||||
message.error('请选择数据时间范围') |
||||
return false |
||||
} |
||||
|
||||
return true |
||||
} |
||||
|
||||
defineExpose({ |
||||
open, |
||||
}) |
||||
</script> |
||||
|
||||
<style lang="scss" scoped> |
||||
.el-row { |
||||
@apply h-32px; |
||||
column-gap: 8px; |
||||
|
||||
.label { |
||||
color: var(--label-color); |
||||
line-height: 33px; |
||||
text-align: right; |
||||
width: 110px; |
||||
.require { |
||||
color: red; |
||||
} |
||||
} |
||||
} |
||||
</style> |
@ -0,0 +1,231 @@
@@ -0,0 +1,231 @@
|
||||
<template> |
||||
<div class="flex-col gap-16 wh-full"> |
||||
<EdfsWrap |
||||
title="当前连接站点" |
||||
v-if="connectSite?.title" |
||||
shape="circle" |
||||
shapeColor="#4B9E5F" |
||||
class="h-auto" |
||||
> |
||||
<div class="station-list-flow"> |
||||
<div class="station-item"> |
||||
<div class="title bg-[#4B9E5F]"> |
||||
<div>{{ connectSite.title }}</div> |
||||
</div> |
||||
<div class="body"> |
||||
<div class="info"> |
||||
<div class="info-item"> |
||||
<div class="info-item-label">在线设备</div> |
||||
<div class="info-item-value"> |
||||
<div class="i-octicon:cloud-16 color-[#4B9E5F] text-16px"></div> |
||||
{{ onlineCount }} |
||||
</div> |
||||
</div> |
||||
<div class="info-item"> |
||||
<div class="info-item-label">离线设备</div> |
||||
<div class="info-item-value"> |
||||
<div class="i-octicon:cloud-offline-16 color-[#F44336] text-16px"></div> |
||||
{{ offlineCount }} |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div class="footer row-end-0"> |
||||
<div class="m-l-auto p-b-8"> |
||||
<el-button |
||||
type="primary" |
||||
style="height: 28px; padding: 0 12px" |
||||
color="#4B9E5F" |
||||
@click="onTransferData" |
||||
>迁移数据</el-button |
||||
> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</EdfsWrap> |
||||
<EdfsWrap |
||||
title="迁移历史" |
||||
shape="circle" |
||||
shapeColor="#F1BF63" |
||||
class="flex-1" |
||||
useScrollBar |
||||
> |
||||
<div class="station-list-flow"> |
||||
<div class="station-item" v-for="item in siteList" :key="item.id"> |
||||
<div class="title bg-[#F1BF63]"> |
||||
{{ item.name }} |
||||
</div> |
||||
<div class="body"> |
||||
<div class="info"> |
||||
<div class="info-item"> |
||||
<div class="info-item-label">导出路径</div> |
||||
<div class="info-item-value">{{ item.export_root_path }}</div> |
||||
</div> |
||||
<!-- <div class="info-item"> |
||||
<div class="info-item-label">数据大小</div> |
||||
<div class="info-item-value">2.5GB</div> |
||||
</div> --> |
||||
</div> |
||||
</div> |
||||
<div class="footer"> |
||||
<div class="item"> |
||||
<div class="label">迁移时间:</div> |
||||
<div class="value"> |
||||
{{ dayjs(item.export_time).format('YYYY-MM-DD HH:mm:ss') }} |
||||
</div> |
||||
</div> |
||||
<div class="info-details" @click="onSiteDetails(item)">详情</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</EdfsWrap> |
||||
</div> |
||||
</template> |
||||
|
||||
<script setup lang="ts"> |
||||
import dayjs from 'dayjs' |
||||
import { useTransferDataStore } from '@/stores/transferData' |
||||
import { storeToRefs } from 'pinia' |
||||
import { getSiteList, type ISiteList } from '@/api/module/transfer' |
||||
|
||||
const router = useRouter() |
||||
const transferDataStore = useTransferDataStore() |
||||
|
||||
const { isConnected, connectSite, onlineCount, offlineCount } = |
||||
storeToRefs(transferDataStore) |
||||
|
||||
const { initConnectSite } = transferDataStore |
||||
|
||||
watch(isConnected, val => { |
||||
val ? initConnectSite() : (connectSite.value = null) |
||||
}) |
||||
|
||||
function onTransferData() { |
||||
router.push({ |
||||
path: '/data-transfer', |
||||
query: { |
||||
type: 'export', |
||||
}, |
||||
}) |
||||
} |
||||
|
||||
function onSiteDetails(site: ISiteList) { |
||||
router.push({ |
||||
path: '/data-transfer', |
||||
query: { |
||||
type: 'details', |
||||
site: JSON.stringify(site), |
||||
}, |
||||
}) |
||||
} |
||||
|
||||
const siteList = ref<ISiteList[]>([]) |
||||
async function loadSiteList() { |
||||
const res = await getSiteList() |
||||
if (res.code === 200 || res.code === 0) { |
||||
siteList.value = res.data |
||||
} |
||||
} |
||||
|
||||
onMounted(() => { |
||||
initConnectSite() |
||||
loadSiteList() |
||||
}) |
||||
</script> |
||||
<style lang="scss" scoped> |
||||
.station-list-flow { |
||||
width: 100%; |
||||
height: 100%; |
||||
display: flex; |
||||
flex-wrap: wrap; |
||||
column-gap: 20px; |
||||
row-gap: 24px; |
||||
|
||||
.station-item { |
||||
width: 280px; |
||||
height: 180px; |
||||
border: 1px solid var(--station-card-border-color); |
||||
border-radius: 6px; |
||||
overflow: hidden; |
||||
display: flex; |
||||
flex-direction: column; |
||||
box-sizing: border-box; |
||||
} |
||||
.title { |
||||
display: flex; |
||||
align-items: center; |
||||
height: 44px; |
||||
width: 100%; |
||||
box-sizing: border-box; |
||||
font-size: 16px; |
||||
color: #fff; |
||||
font-weight: 500; |
||||
justify-content: space-between; |
||||
padding: 0 12px; |
||||
user-select: none; |
||||
.title-edit-btns { |
||||
display: flex; |
||||
margin-left: 12px; |
||||
} |
||||
} |
||||
.body { |
||||
background-color: var(--station-card-bg); |
||||
display: flex; |
||||
flex-direction: column; |
||||
justify-content: center; |
||||
flex: 1; |
||||
.info { |
||||
display: flex; |
||||
justify-content: space-around; |
||||
|
||||
.info-item { |
||||
display: flex; |
||||
flex-direction: column; |
||||
align-items: center; |
||||
.info-item-label { |
||||
font-size: 14px; |
||||
color: var(--label-color); |
||||
} |
||||
.info-item-value { |
||||
font-size: 20px; |
||||
font-weight: 500; |
||||
margin-top: 4px; |
||||
text-align: center; |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
column-gap: 4px; |
||||
color: var(--station-info-val-text); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
.footer { |
||||
display: flex; |
||||
justify-content: space-around; |
||||
align-items: center; |
||||
padding: 0 12px; |
||||
height: 36px; |
||||
|
||||
.item { |
||||
display: flex; |
||||
font-size: 14px; |
||||
color: #999; |
||||
font-weight: 400; |
||||
.value { |
||||
color: #999; |
||||
} |
||||
} |
||||
.info-details { |
||||
font-size: 14px; |
||||
color: #f1bf63; |
||||
cursor: pointer; |
||||
text-decoration: underline; |
||||
&:hover { |
||||
color: #8ace6a; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
</style> |
@ -0,0 +1,445 @@
@@ -0,0 +1,445 @@
|
||||
<template> |
||||
<div class="flex-col gap-16 wh-full"> |
||||
<el-button type="primary" @click="onBack" class="w-150px"> |
||||
<i class="i-line-md:arrow-left"></i>返回站点数据 |
||||
</el-button> |
||||
<EdfsWrap title="设备列表" class="flex-1" useScrollBar> |
||||
<template #title-right v-if="isTransfer"> |
||||
<template v-if="isBatchTransfer"> |
||||
<el-button type="primary" @click="onBatchSave"> 确定迁移 </el-button> |
||||
<el-button type="info" @click="onBatchCancel"> 取消 </el-button> |
||||
</template> |
||||
<template v-else> |
||||
<el-button type="primary" @click="onBatchTransfer"> 批量迁移 </el-button> |
||||
</template> |
||||
</template> |
||||
<div class="device-list-wrap"> |
||||
<el-checkbox-group v-model="onlineDeviceCheckList"> |
||||
<div class="device-item" v-for="item in devices"> |
||||
<div class="device-item-header"> |
||||
<div class="flex items-center"> |
||||
<el-checkbox :value="item.sn" v-if="isBatchTransfer"> |
||||
<div>设备ID: {{ item.sn }}</div> |
||||
</el-checkbox> |
||||
<div v-else> |
||||
<div>设备ID: {{ item.sn }}</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="flex items-center gap-col-2"> |
||||
<el-tooltip |
||||
content="数据迁移" |
||||
v-if="isTransfer && item.status === '在线'" |
||||
> |
||||
<i |
||||
class="i-line-md:cloud-alt-upload-loop :hover:color-[#8ACE6A] color-[#4B9E5F] cursor-pointer text-20px" |
||||
@click="onTransfer(item as IOnlineDevice)" |
||||
></i> |
||||
</el-tooltip> |
||||
<el-tooltip content="详情"> |
||||
<div |
||||
class="i-material-symbols:info-outline :hover:color-[#8ACE6A] color-[#4B9E5F] cursor-pointer text-20px" |
||||
@click="onDeviceDetails(item)" |
||||
></div> |
||||
</el-tooltip> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="device-item-body"> |
||||
<template v-if="isTransfer"> |
||||
<template v-for="key in Object.keys(onlineDeviceMap)"> |
||||
<div class="info-item" v-if="isTransfer && key === 'status'"> |
||||
<div>{{ onlineDeviceMap.status }}:</div> |
||||
<el-tag :type="item.status === '在线' ? 'success' : 'danger'"> |
||||
{{ item.status }} |
||||
</el-tag> |
||||
</div> |
||||
<div class="info-item" v-else> |
||||
<div>{{ onlineDeviceMap[key as keyof typeof onlineDeviceMap] }}:</div> |
||||
<div>{{ item[key] }}</div> |
||||
</div> |
||||
</template> |
||||
</template> |
||||
<template v-else> |
||||
<template v-for="key in Object.keys(offlineDeviceMap)"> |
||||
<div class="info-item" v-if="key === 'create_time'"> |
||||
<div> |
||||
{{ offlineDeviceMap[key as keyof typeof offlineDeviceMap] }}: |
||||
</div> |
||||
<div>{{ dayjs(item[key]).format('YYYY-MM-DD HH:mm:ss') }}</div> |
||||
</div> |
||||
<div class="info-item" v-else> |
||||
<div> |
||||
{{ offlineDeviceMap[key as keyof typeof offlineDeviceMap] }}: |
||||
</div> |
||||
<div>{{ item[key] }}</div> |
||||
</div> |
||||
</template> |
||||
</template> |
||||
</div> |
||||
</div> |
||||
</el-checkbox-group> |
||||
</div> |
||||
</EdfsWrap> |
||||
<EdfsWrap title="迁移进度" class="transfer-wrap h-[42%]" v-if="isShowTransfer"> |
||||
<div class="flex-col gap-col-10 wh-full"> |
||||
<div class="flex items-center gap-col-1"> |
||||
<div class="flex-1 flex items-center"> |
||||
<el-progress |
||||
:percentage="100" |
||||
class="flex-1" |
||||
:stroke-width="18" |
||||
:text-inside="true" |
||||
:status=" |
||||
['progress', 'success', undefined].includes(transferStatus) |
||||
? 'success' |
||||
: 'exception' |
||||
" |
||||
> |
||||
{{ |
||||
transferStatusMap[transferStatus as keyof typeof transferStatusMap] ?? '' |
||||
}} |
||||
</el-progress> |
||||
</div> |
||||
<el-button |
||||
v-if="transferStatus === 'progress'" |
||||
type="primary" |
||||
@click="onStopTransfer" |
||||
>停止迁移</el-button |
||||
> |
||||
</div> |
||||
<div class="transfer-log-wrap"> |
||||
<div class="text-16px font-500">迁移日志</div> |
||||
<el-scrollbar class="h-full"> |
||||
<div |
||||
v-for="i in curTransferLog" |
||||
:class="i.status === 'failed' ? 'text-red-500' : ''" |
||||
> |
||||
{{ i.msg }} |
||||
</div> |
||||
</el-scrollbar> |
||||
</div> |
||||
</div> |
||||
</EdfsWrap> |
||||
</div> |
||||
|
||||
<TransferDlg |
||||
ref="transferDlgRef" |
||||
@on-save="onSave" |
||||
:is-batch-transfer="isBatchTransfer" |
||||
/> |
||||
<DeviceDrawer |
||||
v-model="isShowDetails" |
||||
ref="deviceDrawerRef" |
||||
:siteInfo="siteInfo" |
||||
:is-transfer="isTransfer" |
||||
/> |
||||
</template> |
||||
|
||||
<script setup lang="ts"> |
||||
import dayjs from 'dayjs' |
||||
|
||||
import TransferDlg from './components/transferDlg.vue' |
||||
import ZMQWorker from '@/composables/useZMQJsonWorker' |
||||
import { |
||||
getPubInitData, |
||||
type ManualAction, |
||||
type PublishMsg, |
||||
type PubMsgData, |
||||
type TimeoutMsg, |
||||
type ZmqStatus, |
||||
} from '@/utils/zmq' |
||||
import { useTransferDataStore } from '@/stores/transferData' |
||||
import { storeToRefs } from 'pinia' |
||||
import type { IOfflineDevice, IOnlineDevice } from './type' |
||||
import { useMessage } from '@/composables/useMessage' |
||||
import { getDeviceList, type ISiteList } from '@/api/module/transfer' |
||||
import DeviceDrawer from './components/deviceDrawer.vue' |
||||
import { getTransferTopic, postTransferTopic } from './utils' |
||||
const transferDlgRef = ref<typeof TransferDlg>() |
||||
const router = useRouter() |
||||
const route = useRoute() |
||||
const siteInfo = ref<ISiteList>( |
||||
route.query.site ? JSON.parse(route.query.site as string) : null |
||||
) |
||||
const type = ref<'export' | 'details'>(route.query.type as 'export' | 'details') |
||||
|
||||
const isTransfer = computed(() => type.value === 'export') |
||||
|
||||
const isShowTransfer = computed(() => isTransfer.value && !!curTransferLog.value.length) |
||||
|
||||
const message = useMessage() |
||||
|
||||
const worker = ZMQWorker.getInstance() |
||||
|
||||
const zmqStatus = inject<Ref<ZmqStatus>>('zmqStatus')! |
||||
const transferDataStore = useTransferDataStore() |
||||
const { devicesMap } = storeToRefs(transferDataStore) |
||||
|
||||
const transferStatusMap = { |
||||
progress: '迁移中', |
||||
success: '迁移成功', |
||||
failed: '迁移失败', |
||||
timeout: '迁移超时', |
||||
} |
||||
|
||||
const pubIdWithDevice = new Map<string, { device: IOnlineDevice; action: ManualAction }>() |
||||
|
||||
const curTransferLog = ref< |
||||
{ msg: string; host: string; status: 'success' | 'padding' | 'failed' }[] |
||||
>([]) |
||||
|
||||
const transferStatus = ref<'progress' | 'success' | 'failed' | 'timeout' | undefined>() |
||||
|
||||
const devices = computed(() => { |
||||
return isTransfer.value ? Array.from(devicesMap.value.values()) : deviceList.value |
||||
}) as Ref<any[]> |
||||
|
||||
function onSave(msg: PublishMsg<'export'>, device: IOnlineDevice) { |
||||
curTransferLog.value = [] |
||||
worker.publish(postTransferTopic, msg, true, zmqTimeoutCb) |
||||
pubIdWithDevice.set(msg.id, { device, action: 'export' }) |
||||
worker.subscribe(getTransferTopic, zmqExportCb, msg.id) |
||||
if (isBatchTransfer.value) { |
||||
onBatchCancel() |
||||
} |
||||
} |
||||
|
||||
const statusMap = { |
||||
200: 'success', |
||||
1002: 'padding', |
||||
1003: 'failed', |
||||
} |
||||
|
||||
function zmqExportCb(msg: PubMsgData) { |
||||
const { feedback, result, id } = msg |
||||
if (feedback && feedback[0]) { |
||||
curTransferLog.value.push({ |
||||
msg: `主机【${feedback[0]}】: ${feedback[1]}`, |
||||
host: feedback[0], |
||||
status: statusMap[feedback[1]] ?? 'failed', |
||||
}) |
||||
} |
||||
// 找到 status 为 failed 的 |
||||
transferStatus.value = 'progress' |
||||
if (result !== 'progress') { |
||||
const curMsgInfo = pubIdWithDevice.get(id)! |
||||
if (!curMsgInfo) return |
||||
|
||||
const { device, action } = curMsgInfo |
||||
if (device) { |
||||
if (result === 'success') { |
||||
const res = setTransferStatus() |
||||
if (res === 0) { |
||||
message.success(`迁移成功`) |
||||
transferStatus.value = 'success' |
||||
} else { |
||||
message.error(`迁移失败,请检查迁移日志`) |
||||
transferStatus.value = 'failed' |
||||
} |
||||
} else if (['failed', 'failure'].includes(result)) { |
||||
message.error(`迁移失败`) |
||||
transferStatus.value = 'failed' |
||||
} |
||||
pubIdWithDevice.delete(msg.id) |
||||
} |
||||
} |
||||
} |
||||
|
||||
function setTransferStatus() { |
||||
const failed = curTransferLog.value.filter(i => i.status === 'failed') |
||||
for (const f of failed) { |
||||
curTransferLog.value.forEach(j => { |
||||
if (f.host === j.host) { |
||||
j.status = 'failed' |
||||
} |
||||
}) |
||||
} |
||||
return failed.length |
||||
} |
||||
|
||||
function onStopTransfer() { |
||||
message.confirm('是否确认停止迁移?').then(() => { |
||||
const msg = getPubInitData<'cancel'>('cancel', [], 'no') |
||||
worker.publish(postTransferTopic, msg) |
||||
message.success('迁移已取消') |
||||
clearTransferData() |
||||
}) |
||||
} |
||||
|
||||
function clearTransferData() { |
||||
curTransferLog.value = [] |
||||
transferStatus.value = undefined |
||||
pubIdWithDevice.clear() |
||||
} |
||||
|
||||
function zmqTimeoutCb(msg: TimeoutMsg) { |
||||
const { device, action } = pubIdWithDevice.get(msg.timeoutId)! |
||||
debugger |
||||
if (device && action === 'export') { |
||||
message.error(`迁移超时,请重新稍后尝试`) |
||||
pubIdWithDevice.delete(msg.timeoutId) |
||||
} |
||||
} |
||||
|
||||
const onlineDeviceMap: Record< |
||||
keyof Omit<IOnlineDevice, 'lastUpdated' | 'sn' | 'isChecked'>, |
||||
string |
||||
> = { |
||||
status: '状态', |
||||
stationName: '站点名称', |
||||
clientIp: '客户端IP', |
||||
footprint: '数据占用空间', |
||||
} |
||||
const offlineDeviceMap: Record< |
||||
keyof Pick<IOfflineDevice, 'stationName' | 'db' | 'create_time'>, |
||||
string |
||||
> = { |
||||
stationName: '站点名称', |
||||
db: '数据库', |
||||
create_time: '创建时间', |
||||
} |
||||
|
||||
const onlineDeviceCheckList = ref<string[]>([]) |
||||
|
||||
const isBatchTransfer = ref(false) |
||||
function onBatchTransfer() { |
||||
isBatchTransfer.value = true |
||||
} |
||||
|
||||
function onBatchSave() { |
||||
if (!onlineDeviceCheckList.value.length) { |
||||
message.error('请选择要迁移的设备') |
||||
return |
||||
} |
||||
const checkList = onlineDeviceCheckList.value.map(sn => { |
||||
return devicesMap.value.get(sn) |
||||
}) |
||||
|
||||
const clientIpList = checkList |
||||
.map(item => item?.clientIp) |
||||
.filter(Boolean) |
||||
.join(',') |
||||
const pathList = checkList |
||||
.map(item => `${item?.stationName}/${item?.sn}`) |
||||
.filter(Boolean) |
||||
.join(',') |
||||
|
||||
transferDlgRef.value?.open(checkList[0], clientIpList, pathList) |
||||
} |
||||
|
||||
function onBatchCancel() { |
||||
isBatchTransfer.value = false |
||||
onlineDeviceCheckList.value = [] |
||||
} |
||||
function onTransfer(item: IOnlineDevice) { |
||||
transferDlgRef.value?.open(item) |
||||
} |
||||
|
||||
function onBack() { |
||||
router.push('/station') |
||||
} |
||||
|
||||
// 监听页面刷新 |
||||
window.onbeforeunload = function () { |
||||
stop() |
||||
} |
||||
|
||||
onBeforeRouteLeave(async (to, from, next) => { |
||||
if (transferStatus.value === 'progress') { |
||||
try { |
||||
await message.confirm('当前迁移尚未完成,是否确认离开?') |
||||
window.location.href = to.fullPath |
||||
} catch (error) { |
||||
next(false) |
||||
} |
||||
} else { |
||||
next() |
||||
} |
||||
}) |
||||
|
||||
const deviceList = ref<IOfflineDevice[]>() |
||||
|
||||
async function loadDeviceList() { |
||||
const res = await getDeviceList(siteInfo.value.id) |
||||
if (res.code === 200 || res.code === 0) { |
||||
deviceList.value = res.data |
||||
} |
||||
} |
||||
|
||||
onMounted(() => { |
||||
if (!isTransfer.value) { |
||||
loadDeviceList() |
||||
} |
||||
}) |
||||
|
||||
const isShowDetails = ref(false) |
||||
const deviceDrawerRef = ref<typeof DeviceDrawer>() |
||||
function onDeviceDetails(item: IOfflineDevice) { |
||||
isShowDetails.value = true |
||||
deviceDrawerRef.value?.open(item) |
||||
} |
||||
</script> |
||||
|
||||
<style scoped lang="scss"> |
||||
// .transfer-info-wrap { |
||||
// @apply flex-col gap-col-10 m-t-10 h-80px; |
||||
// .info-item { |
||||
// @apply h-32px flex justify-between items-center; |
||||
// .info-item-label { |
||||
// @apply color-[#6C727F] text-14px font-500; |
||||
// } |
||||
// .info-item-value { |
||||
// @apply color-black text-14px font-500; |
||||
// } |
||||
// } |
||||
// } |
||||
.transfer-log-wrap { |
||||
height: calc(100% - 20px); |
||||
@apply border-radius-8px bg-[#F9FAFB] p-10; |
||||
:deep(.el-scrollbar) { |
||||
height: calc(100% - 20px); |
||||
} |
||||
} |
||||
.device-list-wrap { |
||||
@apply wh-full; |
||||
:deep(.el-checkbox-group) { |
||||
@apply wh-full flex flex-wrap gap-col-6 gap-row-4; |
||||
} |
||||
:deep(.el-checkbox__inner) { |
||||
width: 18px; |
||||
height: 18px; |
||||
&::after { |
||||
left: 6px; |
||||
top: 3px; |
||||
} |
||||
} |
||||
:deep(.el-checkbox__label) { |
||||
@apply text-14px font-500 text-[#313131]; |
||||
} |
||||
:deep(.el-checkbox__input.is-checked + .el-checkbox__label) { |
||||
color: var(--el-color-primary); |
||||
} |
||||
.device-item { |
||||
@apply w-289 h-160 border border-solid border-[#E0E0E0] rounded-8px p-x-14 p-y-10 flex-col; |
||||
.device-item-header { |
||||
@apply w-full text-black text-16px font-500 flex items-center justify-between; |
||||
.info { |
||||
font-size: 14px; |
||||
color: #f1bf63; |
||||
cursor: pointer; |
||||
text-decoration: underline; |
||||
&:hover { |
||||
color: #8ace6a; |
||||
} |
||||
} |
||||
} |
||||
.device-item-body { |
||||
@apply flex-1 flex-col m-t-10 color-[#6C727F] text-14px; |
||||
.info-item { |
||||
@apply flex-1 flex items-center gap-col-2; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
</style> |
@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
|
||||
export interface IOnlineDevice { |
||||
clientIp: string |
||||
sn: string |
||||
stationName: string |
||||
footprint: string |
||||
lastUpdated: number // 新增字段,记录最后更新时间
|
||||
status?: string |
||||
isChecked?: boolean |
||||
} |
||||
|
||||
export interface IOfflineDevice { |
||||
id: string |
||||
sn: string |
||||
stationName: string |
||||
databaseList: string |
||||
last_modify_time?: string |
||||
create_time: string |
||||
db: string |
||||
site_id: string |
||||
status: number |
||||
type: string |
||||
} |
||||
|
||||
export type IDevice = IOnlineDevice | IOfflineDevice |
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
import { getPubTopic, getSubTopic } from "@/utils/zmq"; |
||||
|
||||
// 获取设备状态主题
|
||||
export const getDeviceTopic = getSubTopic('client', 'status', 'transfer') |
||||
|
||||
// 发送迁移请求主题 // action: export=> 开始迁移, cancel=> 迁移取消, import=> 请求历史迁移的设备之前发的消息
|
||||
export const postTransferTopic = getPubTopic('event', 'transfer') |
||||
|
||||
// 获取迁移设备信息主题 // action: export => 迁移进度,
|
||||
export const getTransferTopic = getSubTopic('server', 'event', 'transfer') |
@ -0,0 +1,26 @@
@@ -0,0 +1,26 @@
|
||||
{ |
||||
"extends": "@vue/tsconfig/tsconfig.dom.json", |
||||
"include": [ |
||||
"env.d.ts", |
||||
"src/**/*", |
||||
"src/**/*.ts", |
||||
"src/**/*.vue", |
||||
"src/**/**/*.vue", |
||||
"src/**/**/*.ts", |
||||
"global.types/**/*.d.ts", |
||||
"*/*.d.ts", |
||||
], |
||||
"exclude": [ |
||||
"src/**/__tests__/*" |
||||
], |
||||
"compilerOptions": { |
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", |
||||
"esModuleInterop": true, |
||||
"allowSyntheticDefaultImports": true, |
||||
"paths": { |
||||
"@/*": [ |
||||
"./src/*" |
||||
] |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,17 @@
@@ -0,0 +1,17 @@
|
||||
{ |
||||
"files": [], |
||||
"references": [ |
||||
{ |
||||
"path": "./tsconfig.node.json" |
||||
}, |
||||
{ |
||||
"path": "./tsconfig.app.json" |
||||
} |
||||
], |
||||
"compilerOptions": { |
||||
"types": [ |
||||
"node" |
||||
], |
||||
"moduleResolution": "node" |
||||
} |
||||
} |
@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
|
||||
{ |
||||
"extends": "@tsconfig/node22/tsconfig.json", |
||||
"include": [ |
||||
"vite.config.*", |
||||
"vitest.config.*", |
||||
"cypress.config.*", |
||||
"nightwatch.conf.*", |
||||
"playwright.config.*", |
||||
"eslint.config.*" |
||||
], |
||||
"compilerOptions": { |
||||
"composite": true, // 启用复合项目 |
||||
"noEmit": true, |
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", |
||||
"module": "ESNext", |
||||
"moduleResolution": "Bundler", |
||||
"types": [ |
||||
"node" |
||||
] |
||||
} |
||||
} |
@ -0,0 +1,51 @@
@@ -0,0 +1,51 @@
|
||||
import presetRemToPx from '@unocss/preset-rem-to-px' |
||||
import { presetScalpel } from 'unocss-preset-scalpel' |
||||
import { presetSoybeanAdmin } from './src/uno-preset/src/index' |
||||
import { |
||||
defineConfig, |
||||
presetAttributify, |
||||
presetIcons, |
||||
presetTypography, |
||||
presetWind3, |
||||
presetWebFonts, |
||||
transformerDirectives, |
||||
transformerVariantGroup, |
||||
} from 'unocss' |
||||
|
||||
export default defineConfig({ |
||||
shortcuts: [], |
||||
theme: { |
||||
colors: {}, |
||||
}, |
||||
content: { |
||||
pipeline: { |
||||
include: [ |
||||
//参考:https://unocss.dev/guide/extracting#extracting-from-build-tools-pipeline
|
||||
/\.(vue|svelte|[jt]sx|mdx?|astro|elm|php|phtml|html)($|\?)/, |
||||
'src/**/*.{js,ts}', |
||||
'src/router/index.ts', |
||||
'src/views/home/utils/menuConfig.ts', |
||||
], |
||||
}, |
||||
}, |
||||
rules: [['wh-full', { width: '100%', height: '100%' }]], |
||||
presets: [ |
||||
presetRemToPx(), |
||||
presetScalpel(), |
||||
presetWind3(), |
||||
presetSoybeanAdmin(), |
||||
presetAttributify({ |
||||
prefix: 'uno-', |
||||
prefixedOnly: true, |
||||
}), |
||||
presetIcons({ |
||||
scale: 1.2, |
||||
warn: true, |
||||
}), |
||||
presetTypography(), |
||||
presetWebFonts({ |
||||
fonts: {}, |
||||
}), |
||||
], |
||||
transformers: [transformerDirectives(), transformerVariantGroup()], |
||||
}) |
@ -0,0 +1,78 @@
@@ -0,0 +1,78 @@
|
||||
import { fileURLToPath, URL } from 'node:url' |
||||
import UnoCSS from 'unocss/vite' |
||||
import { defineConfig } from 'vite' |
||||
import vue from '@vitejs/plugin-vue' |
||||
import vueDevTools from 'vite-plugin-vue-devtools' |
||||
import AutoImport from 'unplugin-auto-import/vite' |
||||
import Components from 'unplugin-vue-components/vite' |
||||
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' |
||||
import vueJsx from '@vitejs/plugin-vue-jsx' |
||||
import Icons from 'unplugin-icons/vite' |
||||
// Vite 配置文件
|
||||
export default defineConfig({ |
||||
plugins: [ |
||||
vue(), |
||||
vueJsx(), |
||||
vueDevTools(), |
||||
UnoCSS(), |
||||
Icons({ |
||||
autoInstall: true, |
||||
}), |
||||
AutoImport({ |
||||
imports: ['vue', 'vue-router'], |
||||
resolvers: [ElementPlusResolver()], |
||||
dts: 'global.types/auto-imports.d.ts', |
||||
}), |
||||
Components({ |
||||
dirs: ['src/components'], |
||||
extensions: ['vue'], |
||||
dts: 'global.types/components.d.ts', |
||||
resolvers: [ElementPlusResolver()], |
||||
}), |
||||
], |
||||
resolve: { |
||||
alias: { |
||||
'@': fileURLToPath(new URL('./src', import.meta.url)), |
||||
}, |
||||
}, |
||||
|
||||
build: { |
||||
minify: 'terser', |
||||
terserOptions: { |
||||
compress: { |
||||
drop_console: true, |
||||
drop_debugger: true, |
||||
}, |
||||
}, |
||||
}, |
||||
css: { |
||||
preprocessorOptions: { |
||||
scss: { |
||||
additionalData: '@use "@/assets/styles/mixins.scss" as *;', |
||||
}, |
||||
}, |
||||
}, |
||||
define: { |
||||
'process.env': {} |
||||
}, |
||||
|
||||
// 开发服务器配置
|
||||
server: { |
||||
// 启动时自动打开浏览器
|
||||
// 开发服务器端口
|
||||
port: 3000, |
||||
// 允许局域网访问
|
||||
host: '0.0.0.0', |
||||
proxy: { |
||||
'/remoteServer': { |
||||
target: 'http://192.168.1.115:8080/', |
||||
changeOrigin: true, |
||||
secure: false, |
||||
ws: true, |
||||
rewrite: path => path.replace(/^\/remoteServer/, ''), |
||||
|
||||
}, |
||||
|
||||
} |
||||
}, |
||||
}) |
Loading…
Reference in new issue