46 Commits

Author SHA1 Message Date
guanj
1abd7dbc45 Merge branch 'master' of http://192.168.1.22:3000/Web/admin-sjzx 2026-07-21 15:10:58 +08:00
guanj
472babe0ce 修改全局悬浮框添加最大高度 2026-07-21 15:10:48 +08:00
8054b44e88 适配辽宁现场调整 2026-07-21 10:45:50 +08:00
guanj
5eb5a01b50 调整电网一张图污染水平评估 2026-07-20 10:04:02 +08:00
guanj
e77c57f8d0 修改辽宁测试问题 2026-07-20 09:30:48 +08:00
guanj
22011188a1 修改辽宁全景展示 2026-07-17 16:13:49 +08:00
guanj
c72ba3e5db 添加一类监测点字段 2026-07-17 11:27:40 +08:00
guanj
7f1c3e31c0 监测点评估添加字段 2026-07-17 11:17:38 +08:00
guanj
6c70a776a0 列设置添加缓存
调整监测点台账导出id重复问题
2026-07-17 08:50:25 +08:00
guanj
dc3b3cfc74 全局下拉框添加filterable属性 2026-07-15 09:21:35 +08:00
guanj
04c5eb7185 调整退出问题 2026-07-14 14:49:10 +08:00
guanj
d0d62ab0af 修改菜单树无法展开问题 2026-07-14 14:39:59 +08:00
guanj
b3d671a93e 调整前端两套互不连通的 HTTP 请求层并存、两套布局框架并存、两套 Pinia 实例并存问题 2026-07-10 14:52:29 +08:00
guanj
888b8d1bf4 修改点击列问题 2026-07-10 08:57:37 +08:00
guanj
ee9cd057b8 优化菜单切换导致浏览器刷新问题 2026-07-09 15:40:56 +08:00
guanj
8bca4191f2 调整代码 2026-07-09 15:26:01 +08:00
guanj
69e6a1f93f 调整 2026-07-09 14:41:56 +08:00
guanj
71df1ac4be 修改冲突 2026-07-09 13:59:02 +08:00
guanj
cd2be0f4aa Merge branch 'master' of http://192.168.1.22:3000/Web/admin-sjzx
# Conflicts:
#	src/views/pqs/business/terminal/terminaCkeck/index.vue
2026-07-09 13:57:58 +08:00
guanj
855c569535 全局表格添加列配置功能 2026-07-09 13:54:18 +08:00
6a84d28101 添加辽宁周期检测功能 2026-07-09 11:41:01 +08:00
guanj
1a69be62a0 修改监测点信息 2026-07-09 11:08:56 +08:00
guanj
02caca969e 修改用户性质 2026-07-09 10:33:22 +08:00
guanj
3f1af5e4cc 监测点信息添加终端型号 2026-07-08 16:08:02 +08:00
guanj
88e9812d5f 联调周期监测 2026-07-08 14:37:19 +08:00
guanj
83a827ba5b 修改文件上传问题 2026-07-08 11:14:32 +08:00
40b1092718 添加周期 2026-07-08 09:21:59 +08:00
guanj
83a1128884 用户管理添加监测报告,评估报告 2026-07-07 13:52:53 +08:00
1a44a2f9e0 添加周期 2026-07-07 10:18:22 +08:00
guanj
5044c88946 优化全景展示页面 2026-07-07 10:15:33 +08:00
guanj
51a862ed00 修改进程显示 2026-07-06 10:58:02 +08:00
guanj
1ca8e536dd 修改限值显示问题 2026-07-06 10:41:33 +08:00
guanj
2445cfbff3 修改辽宁全景展示问题 2026-07-06 10:12:51 +08:00
guanj
21ac0611cd 修改地图api 2026-06-29 14:41:12 +08:00
guanj
ff841cfa93 修改Socket路径问题 2026-06-26 10:17:43 +08:00
guanj
01cc29c8b2 修改电网一张图 2026-06-24 09:57:48 +08:00
guanj
a324f7afcf 修改算法库拖动问题 2026-06-23 21:36:43 +08:00
guanj
4272f7756e 修改配置 2026-06-23 21:28:35 +08:00
guanj
070e404bbb 修改辽宁反馈问题 2026-06-23 21:27:15 +08:00
ed60e2b7f3 台账新增修改添加objName 2026-06-23 16:09:41 +08:00
7785d4f708 台账新增修改添加objName 2026-06-23 15:12:58 +08:00
0c64711dd8 辽宁页面调整 2026-06-22 15:50:31 +08:00
guanj
d85dba8bf8 修改补召 2026-06-16 09:56:38 +08:00
guanj
56b8158f4a 优化波形 2026-06-16 08:36:22 +08:00
cdf
765507398e 辽宁单点登录 2026-06-15 17:57:14 +08:00
cdf
9224ea17b8 区域报告调整 2026-06-07 15:56:31 +08:00
373 changed files with 24532 additions and 34769 deletions

6
.env.LN Normal file
View File

@@ -0,0 +1,6 @@
# 辽宁嵌入
NODE_ENV = LN
VITE_NAME="LN"
# 电网一张图 地图图层
VITE_NARIMAP=null
VITE_NRGISCOMMON=null

8813
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,12 +8,14 @@
"dev:jibei": "vite --mode jibei", "dev:jibei": "vite --mode jibei",
"dev:hainan": "vite --mode hainan", "dev:hainan": "vite --mode hainan",
"dev:qujing": "vite --mode qujing", "dev:qujing": "vite --mode qujing",
"dev:LN": "vite --mode LN",
"dev:LNqr": "vite --mode LNqr", "dev:LNqr": "vite --mode LNqr",
"dev:removeMode": "vite --mode removeMode", "dev:removeMode": "vite --mode removeMode",
"build": "vite build --mode dev", "build": "vite build --mode dev",
"build:jibei": "vite build --mode jibei", "build:jibei": "vite build --mode jibei",
"build:hainan": "vite build --mode hainan", "build:hainan": "vite build --mode hainan",
"build:qujing": "vite build --mode qujing", "build:qujing": "vite build --mode qujing",
"build:LN": "vite build --mode LN",
"build:LNqr": "vite build --mode LNqr", "build:LNqr": "vite build --mode LNqr",
"build:removeMode": "vite build --mode removeMode", "build:removeMode": "vite build --mode removeMode",
"preview": "vite preview" "preview": "vite preview"
@@ -82,8 +84,9 @@
"vue-katex": "^0.5.0", "vue-katex": "^0.5.0",
"vue-router": "4", "vue-router": "4",
"vue-types": "^5.1.1", "vue-types": "^5.1.1",
"vxe-table": "^4.5.17", "vxe-pc-ui": "~4.15.19",
"vxe-table-plugin-export-xlsx": "^4.0.1", "vxe-table": "^4.19.22",
"vxe-table-plugin-export-xlsx": "^4.0.7",
"web-storage-cache": "^1.1.1", "web-storage-cache": "^1.1.1",
"xe-utils": "^3.5.14", "xe-utils": "^3.5.14",
"yarn": "^1.22.22" "yarn": "^1.22.22"

View File

@@ -4747,10 +4747,11 @@ window.BMAP_AUTHENTIC_KEY = ''
c.Nm c.Nm
? c.Nm.sa(b) ? c.Nm.sa(b)
: ((c.Nm = new U(b, { : ((c.Nm = new U(b, {
icon: new qc(G.qa + 'blank.gif', { // icon: new qc(G.qa + 'blank.gif', {
width: 1, // width: 1,
height: 1 // height: 1
}), // }),
icon: '',
offset: new O(0, 0), offset: new O(0, 0),
clickable: t clickable: t
})), })),
@@ -5886,7 +5887,7 @@ window.BMAP_AUTHENTIC_KEY = ''
Qc.prototype.co = function () { Qc.prototype.co = function () {
this.Gb.src = this.Gb.src =
0 < z.ca.ia && 6 >= z.ca.ia && this.GJ 0 < z.ca.ia && 6 >= z.ca.ia && this.GJ
? G.qa + 'blank.gif' ? '' //G.qa + 'blank.gif'
: '' !== this.src && this.Gb.src == this.src : '' !== this.src && this.Gb.src == this.src
? this.src + '&t = ' + Date.now() ? this.src + '&t = ' + Date.now()
: this.src : this.src

View File

@@ -9,7 +9,7 @@ import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
import useSetTheme from '@/utils/setTheme' import useSetTheme from '@/utils/setTheme'
import { onMounted } from 'vue' import { onMounted } from 'vue'
useSetTheme() useSetTheme()
onMounted(async () => { const fetchMqttUrl = async () => {
const response = await fetch('/') const response = await fetch('/')
const WebSocketUrl: any = response.headers.get('X-WebSocket-Url') const WebSocketUrl: any = response.headers.get('X-WebSocket-Url')
const WebSocketUrl2: any = response.headers.get('X-WebSocket-Url2') const WebSocketUrl2: any = response.headers.get('X-WebSocket-Url2')
@@ -19,6 +19,10 @@ onMounted(async () => {
localStorage.setItem('WebSocketUrl2', WebSocketUrl2) localStorage.setItem('WebSocketUrl2', WebSocketUrl2)
localStorage.setItem('WebSocketUrl3', WebSocketUrl3) localStorage.setItem('WebSocketUrl3', WebSocketUrl3)
localStorage.setItem('MqttUrl', MqttUrl) localStorage.setItem('MqttUrl', MqttUrl)
}
onMounted(() => {
fetchMqttUrl()
}) })
</script> </script>

View File

@@ -1,4 +1,4 @@
import request from '@/config/axios' import { requestData, requestDownload } from '@/utils/request'
// BPM 流程表达式 VO // BPM 流程表达式 VO
export interface ProcessExpressionVO { export interface ProcessExpressionVO {
@@ -12,31 +12,36 @@ export interface ProcessExpressionVO {
export const ProcessExpressionApi = { export const ProcessExpressionApi = {
// 查询BPM 流程表达式分页 // 查询BPM 流程表达式分页
getProcessExpressionPage: async (params: any) => { getProcessExpressionPage: async (params: any) => {
return await request.get({ url: `/bpm/process-expression/page`, params }) return await requestData({ url: '/bpm/process-expression/page', method: 'GET', params })
}, },
// 查询BPM 流程表达式详情 // 查询BPM 流程表达式详情
getProcessExpression: async (id: number) => { getProcessExpression: async (id: number) => {
return await request.get({ url: `/bpm/process-expression/get?id=` + id }) return await requestData({ url: '/bpm/process-expression/get?id=' + id, method: 'GET' })
}, },
// 新增BPM 流程表达式 // 新增BPM 流程表达式
createProcessExpression: async (data: ProcessExpressionVO) => { createProcessExpression: async (data: ProcessExpressionVO) => {
return await request.post({ url: `/bpm/process-expression/create`, data }) return await requestData({ url: '/bpm/process-expression/create', method: 'POST', data })
}, },
// 修改BPM 流程表达式 // 修改BPM 流程表达式
updateProcessExpression: async (data: ProcessExpressionVO) => { updateProcessExpression: async (data: ProcessExpressionVO) => {
return await request.put({ url: `/bpm/process-expression/update`, data }) return await requestData({ url: '/bpm/process-expression/update', method: 'PUT', data })
}, },
// 删除BPM 流程表达式 // 删除BPM 流程表达式
deleteProcessExpression: async (id: number) => { deleteProcessExpression: async (id: number) => {
return await request.delete({ url: `/bpm/process-expression/delete?id=` + id }) return await requestData({ url: '/bpm/process-expression/delete?id=' + id, method: 'DELETE' })
}, },
// 导出BPM 流程表达式 Excel // 导出BPM 流程表达式 Excel
exportProcessExpression: async (params) => { exportProcessExpression: async (params) => {
return await request.download({ url: `/bpm/process-expression/export-excel`, params }) return await requestDownload({
url: '/bpm/process-expression/export-excel',
method: 'GET',
params,
responseType: 'blob'
})
} }
} }

View File

@@ -1,4 +1,4 @@
import request from '@/config/axios' import { requestData } from '@/utils/request'
// BPM 流程监听器 VO // BPM 流程监听器 VO
export interface ProcessListenerVO { export interface ProcessListenerVO {
@@ -15,26 +15,26 @@ export interface ProcessListenerVO {
export const ProcessListenerApi = { export const ProcessListenerApi = {
// 查询流程监听器分页 // 查询流程监听器分页
getProcessListenerPage: async (params: any) => { getProcessListenerPage: async (params: any) => {
return await request.get({ url: `/bpm/process-listener/page`, params }) return await requestData({ url: '/bpm/process-listener/page', method: 'GET', params })
}, },
// 查询流程监听器详情 // 查询流程监听器详情
getProcessListener: async (id: number) => { getProcessListener: async (id: number) => {
return await request.get({ url: `/bpm/process-listener/get?id=` + id }) return await requestData({ url: '/bpm/process-listener/get?id=' + id, method: 'GET' })
}, },
// 新增流程监听器 // 新增流程监听器
createProcessListener: async (data: ProcessListenerVO) => { createProcessListener: async (data: ProcessListenerVO) => {
return await request.post({ url: `/bpm/process-listener/create`, data }) return await requestData({ url: '/bpm/process-listener/create', method: 'POST', data })
}, },
// 修改流程监听器 // 修改流程监听器
updateProcessListener: async (data: ProcessListenerVO) => { updateProcessListener: async (data: ProcessListenerVO) => {
return await request.put({ url: `/bpm/process-listener/update`, data }) return await requestData({ url: '/bpm/process-listener/update', method: 'PUT', data })
}, },
// 删除流程监听器 // 删除流程监听器
deleteProcessListener: async (id: number) => { deleteProcessListener: async (id: number) => {
return await request.delete({ url: `/bpm/process-listener/delete?id=` + id }) return await requestData({ url: '/bpm/process-listener/delete?id=' + id, method: 'DELETE' })
} }
} }

View File

@@ -1,4 +1,4 @@
import request from '@/config/axios' import { requestData } from '@/utils/request'
export type UserGroupVO = { export type UserGroupVO = {
id: number id: number
@@ -12,36 +12,30 @@ export type UserGroupVO = {
// 创建用户组 // 创建用户组
export const createUserGroup = async (data: UserGroupVO) => { export const createUserGroup = async (data: UserGroupVO) => {
return await request.post({ return await requestData({ url: '/bpm/user-group/create', method: 'POST', data })
url: '/bpm/user-group/create',
data: data
})
} }
// 更新用户组 // 更新用户组
export const updateUserGroup = async (data: UserGroupVO) => { export const updateUserGroup = async (data: UserGroupVO) => {
return await request.put({ return await requestData({ url: '/bpm/user-group/update', method: 'PUT', data })
url: '/bpm/user-group/update',
data: data
})
} }
// 删除用户组 // 删除用户组
export const deleteUserGroup = async (id: number) => { export const deleteUserGroup = async (id: number) => {
return await request.delete({ url: '/bpm/user-group/delete?id=' + id }) return await requestData({ url: '/bpm/user-group/delete?id=' + id, method: 'DELETE' })
} }
// 获得用户组 // 获得用户组
export const getUserGroup = async (id: number) => { export const getUserGroup = async (id: number) => {
return await request.get({ url: '/bpm/user-group/get?id=' + id }) return await requestData({ url: '/bpm/user-group/get?id=' + id, method: 'GET' })
} }
// 获得用户组分页 // 获得用户组分页
export const getUserGroupPage = async (params) => { export const getUserGroupPage = async (params) => {
return await request.get({ url: '/bpm/user-group/page', params }) return await requestData({ url: '/bpm/user-group/page', method: 'GET', params })
} }
// 获取用户组精简信息列表 // 获取用户组精简信息列表
export const getUserGroupSimpleList = async (): Promise<UserGroupVO[]> => { export const getUserGroupSimpleList = async (): Promise<UserGroupVO[]> => {
return await request.get({ url: '/bpm/user-group/simple-list' }) return await requestData({ url: '/bpm/user-group/simple-list', method: 'GET' })
} }

View File

@@ -1,7 +1,7 @@
import request from '@/utils/request' import request from '@/utils/request'
// 获取区域中断终端 // 获取区域中断终端
export function getAreaOffDev(data) { export function getAreaOffDev(data:any) {
return request({ return request({
url: '/device-boot/device/getAreaOffDev', url: '/device-boot/device/getAreaOffDev',
method: 'post', method: 'post',
@@ -9,10 +9,28 @@ export function getAreaOffDev(data) {
}) })
} }
// 获取区域中断终端 // 获取区域中断终端
export function getYwZtSubstation(data) { export function getYwZtSubstation(data:any) {
return request({ return request({
url: '/device-boot/ywZt/getYwZtSubstation', url: '/device-boot/ywZt/getYwZtSubstation',
method: 'GET', method: 'GET',
params: data params: data
}) })
} }
//终端周期检测
export function terminalCheckPage(data:any) {
return request({
url: '/device-boot/alarm/terminalCheckPage',
method: 'post',
data
})
}
export function userCheckUpload(data:any) {
return request({
url: '/device-boot/userReport/userCheckUpload',
method: 'post',
data
})
}

View File

@@ -4,6 +4,10 @@ import request from '@/utils/request'
export function getSubLineGiveAnAlarm(data: any) { export function getSubLineGiveAnAlarm(data: any) {
return request({ url: '/harmonic-boot/gridDiagram/getSubLineGiveAnAlarm', method: 'post', data }) return request({ url: '/harmonic-boot/gridDiagram/getSubLineGiveAnAlarm', method: 'post', data })
} }
export function getAreaObjAlarm(data: any) {
return request({ url: '/harmonic-boot/gridDiagram/getAreaObjAlarm', method: 'post', data })
}
// 变电站详细列表 // 变电站详细列表
export function getPollutionAlarmPageData(data: any) { export function getPollutionAlarmPageData(data: any) {
return request({ url: '/harmonic-boot/gridDiagram/getPollutionAlarmPageData', method: 'post', data }) return request({ url: '/harmonic-boot/gridDiagram/getPollutionAlarmPageData', method: 'post', data })
@@ -182,3 +186,7 @@ export function limitTableDetail(data: any) {
export function downPollutionSensitiveUser(data: any) { export function downPollutionSensitiveUser(data: any) {
return request({ url: '/harmonic-boot/PollutionSubstation/downPollutionSensitiveUser', method: 'post', data }) return request({ url: '/harmonic-boot/PollutionSubstation/downPollutionSensitiveUser', method: 'post', data })
} }
//干扰源类型统计
export function loadTypeVOList(data: any) {
return request({ url: '/device-boot/gridDiagram/loadTypeVOList', method: 'post', data })
}

View File

@@ -1,4 +1,4 @@
import request from '@/config/axios' import { requestData, requestDownload } from '@/utils/request'
export type DictDataVO = { export type DictDataVO = {
id: number | undefined id: number | undefined
@@ -15,35 +15,35 @@ export type DictDataVO = {
// 查询字典数据(精简)列表 // 查询字典数据(精简)列表
export const getSimpleDictDataList = () => { export const getSimpleDictDataList = () => {
return request.get({ url: '/system/dict-data/simple-list' }) return requestData({ url: '/system/dict-data/simple-list', method: 'GET' })
} }
// 查询字典数据列表 // 查询字典数据列表
export const getDictDataPage = (params: PageParam) => { export const getDictDataPage = (params: PageParam) => {
return request.get({ url: '/system/dict-data/page', params }) return requestData({ url: '/system/dict-data/page', method: 'GET', params })
} }
// 查询字典数据详情 // 查询字典数据详情
export const getDictData = (id: number) => { export const getDictData = (id: number) => {
return request.get({ url: '/system/dict-data/get?id=' + id }) return requestData({ url: '/system/dict-data/get?id=' + id, method: 'GET' })
} }
// 新增字典数据 // 新增字典数据
export const createDictData = (data: DictDataVO) => { export const createDictData = (data: DictDataVO) => {
return request.post({ url: '/system/dict-data/create', data }) return requestData({ url: '/system/dict-data/create', method: 'POST', data })
} }
// 修改字典数据 // 修改字典数据
export const updateDictData = (data: DictDataVO) => { export const updateDictData = (data: DictDataVO) => {
return request.put({ url: '/system/dict-data/update', data }) return requestData({ url: '/system/dict-data/update', method: 'PUT', data })
} }
// 删除字典数据 // 删除字典数据
export const deleteDictData = (id: number) => { export const deleteDictData = (id: number) => {
return request.delete({ url: '/system/dict-data/delete?id=' + id }) return requestData({ url: '/system/dict-data/delete?id=' + id, method: 'DELETE' })
} }
// 导出字典类型数据 // 导出字典类型数据
export const exportDictData = (params) => { export const exportDictData = (params) => {
return request.download({ url: '/system/dict-data/export', params }) return requestDownload({ url: '/system/dict-data/export', method: 'GET', params, responseType: 'blob' })
} }

View File

@@ -1,4 +1,4 @@
import request from '@/config/axios' import { requestData, requestDownload } from '@/utils/request'
export type DictTypeVO = { export type DictTypeVO = {
id: number | undefined id: number | undefined
@@ -11,34 +11,35 @@ export type DictTypeVO = {
// 查询字典(精简)列表 // 查询字典(精简)列表
export const getSimpleDictTypeList = () => { export const getSimpleDictTypeList = () => {
return request.get({ url: '/system/dict-type/list-all-simple' }) return requestData({ url: '/system/dict-type/list-all-simple', method: 'GET' })
} }
// 查询字典列表 // 查询字典列表
export const getDictTypePage = (params: PageParam) => { export const getDictTypePage = (params: PageParam) => {
return request.get({ url: '/system/dict-type/page', params }) return requestData({ url: '/system/dict-type/page', method: 'GET', params })
} }
// 查询字典详情 // 查询字典详情
export const getDictType = (id: number) => { export const getDictType = (id: number) => {
return request.get({ url: '/system/dict-type/get?id=' + id }) return requestData({ url: '/system/dict-type/get?id=' + id, method: 'GET' })
} }
// 新增字典 // 新增字典
export const createDictType = (data: DictTypeVO) => { export const createDictType = (data: DictTypeVO) => {
return request.post({ url: '/system/dict-type/create', data }) return requestData({ url: '/system/dict-type/create', method: 'POST', data })
} }
// 修改字典 // 修改字典
export const updateDictType = (data: DictTypeVO) => { export const updateDictType = (data: DictTypeVO) => {
return request.put({ url: '/system/dict-type/update', data }) return requestData({ url: '/system/dict-type/update', method: 'PUT', data })
} }
// 删除字典 // 删除字典
export const deleteDictType = (id: number) => { export const deleteDictType = (id: number) => {
return request.delete({ url: '/system/dict-type/delete?id=' + id }) return requestData({ url: '/system/dict-type/delete?id=' + id, method: 'DELETE' })
} }
// 导出字典类型 // 导出字典类型
export const exportDictType = (params) => { export const exportDictType = (params) => {
return request.download({ url: '/system/dict-type/export', params }) return requestDownload({ url: '/system/dict-type/export', method: 'GET', params, responseType: 'blob' })
} }

View File

@@ -1,4 +1,4 @@
import request from '@/config/axios' import { requestData } from '@/utils/request'
import qs from 'qs' import qs from 'qs'
export interface NotifyMessageVO { export interface NotifyMessageVO {
@@ -17,33 +17,34 @@ export interface NotifyMessageVO {
} }
// 查询站内信消息列表 // 查询站内信消息列表
export const getNotifyMessagePage = async (params: PageParam) => { export const getNotifyMessagePage = async (params: any) => {
return await request.get({ url: '/system/notify-message/page', params }) return await requestData({ url: '/system/notify-message/page', method: 'GET', params })
} }
// 获得我的站内信分页 // 获得我的站内信分页
export const getMyNotifyMessagePage = async (params: PageParam) => { export const getMyNotifyMessagePage = async (params: any) => {
return await request.get({ url: '/system/notify-message/my-page', params }) return await requestData({ url: '/system/notify-message/my-page', method: 'GET', params })
} }
// 批量标记已读 // 批量标记已读
export const updateNotifyMessageRead = async (ids) => { export const updateNotifyMessageRead = async (ids: any) => {
return await request.put({ return await requestData({
url: '/system/notify-message/update-read?' + qs.stringify({ ids: ids }, { indices: false }) url: '/system/notify-message/update-read?' + qs.stringify({ ids: ids }, { indices: false }),
method: 'PUT'
}) })
} }
// 标记所有站内信为已读 // 标记所有站内信为已读
export const updateAllNotifyMessageRead = async () => { export const updateAllNotifyMessageRead = async () => {
return await request.put({ url: '/system/notify-message/update-all-read' }) return await requestData({ url: '/system/notify-message/update-all-read', method: 'PUT' })
} }
// 获取当前用户的最新站内信列表 // 获取当前用户的最新站内信列表
export const getUnreadNotifyMessageList = async () => { export const getUnreadNotifyMessageList = async () => {
return await request.get({ url: '/system/notify-message/get-unread-list' }) return await requestData({ url: '/system/notify-message/get-unread-list', method: 'GET' })
} }
// 获得当前用户的未读站内信数量 // 获得当前用户的未读站内信数量
export const getUnreadNotifyMessageCount = async () => { export const getUnreadNotifyMessageCount = async () => {
return await request.get({ url: '/system/notify-message/get-unread-count' }) return await requestData({ url: '/system/notify-message/get-unread-count', method: 'GET' })
} }

View File

@@ -1,4 +1,4 @@
import request from '@/config/axios' import { requestData } from '@/utils/request'
export interface NotifyTemplateVO { export interface NotifyTemplateVO {
id?: number id?: number
@@ -20,30 +20,30 @@ export interface NotifySendReqVO {
// 查询站内信模板列表 // 查询站内信模板列表
export const getNotifyTemplatePage = async (params: PageParam) => { export const getNotifyTemplatePage = async (params: PageParam) => {
return await request.get({ url: '/system/notify-template/page', params }) return await requestData({ url: '/system/notify-template/page', method: 'GET', params })
} }
// 查询站内信模板详情 // 查询站内信模板详情
export const getNotifyTemplate = async (id: number) => { export const getNotifyTemplate = async (id: number) => {
return await request.get({ url: '/system/notify-template/get?id=' + id }) return await requestData({ url: '/system/notify-template/get?id=' + id, method: 'GET' })
} }
// 新增站内信模板 // 新增站内信模板
export const createNotifyTemplate = async (data: NotifyTemplateVO) => { export const createNotifyTemplate = async (data: NotifyTemplateVO) => {
return await request.post({ url: '/system/notify-template/create', data }) return await requestData({ url: '/system/notify-template/create', method: 'POST', data })
} }
// 修改站内信模板 // 修改站内信模板
export const updateNotifyTemplate = async (data: NotifyTemplateVO) => { export const updateNotifyTemplate = async (data: NotifyTemplateVO) => {
return await request.put({ url: '/system/notify-template/update', data }) return await requestData({ url: '/system/notify-template/update', method: 'PUT', data })
} }
// 删除站内信模板 // 删除站内信模板
export const deleteNotifyTemplate = async (id: number) => { export const deleteNotifyTemplate = async (id: number) => {
return await request.delete({ url: '/system/notify-template/delete?id=' + id }) return await requestData({ url: '/system/notify-template/delete?id=' + id, method: 'DELETE' })
} }
// 发送站内信 // 发送站内信
export const sendNotify = (data: NotifySendReqVO) => { export const sendNotify = (data: NotifySendReqVO) => {
return request.post({ url: '/system/notify-template/send-notify', data }) return requestData({ url: '/system/notify-template/send-notify', method: 'POST', data })
} }

View File

@@ -44,13 +44,30 @@ export async function login(params: any) {
}) })
} }
//辽宁嵌入登录获取token //辽宁嵌入登录获取token
export async function loginLNqr() { export async function loginLNqr(data: any) {
return request({ return request({
url: '/pqs-auth/oauth/lnLogin', url: '/pqs-auth/oauth/lnLogin',
method: 'get', method: 'get',
params: data
}) })
} }
export async function lnRefreshToken(data: any) {
return request({
url: '/pqs-auth/oauth/lnRefreshToken',
method: 'get',
params: data
})
}
export async function logout() {
return request({
url: '/pqs-auth/oauth/logout',
method: 'DELETE',
})
}
//获取用户信息 //获取用户信息
export function getUserById() { export function getUserById() {
const adminInfo = useAdminInfo() const adminInfo = useAdminInfo()
@@ -77,6 +94,17 @@ export function refreshToken(): Promise<any> {
}) })
} }
export function lnRefreshTokenTo(): Promise<any> {
// 调用useAdminInfo函数获取管理员信息并将其赋值给adminInfo变量
const adminInfo = useAdminInfo()
return lnRefreshToken({
refreshToken: adminInfo.refresh_token,
clientId: "njcn",
clientSecret:"njcnpqs"
})
}
/** /**
* 获取营销用户列表 * 获取营销用户列表
* @returns {AxiosPromise} * @returns {AxiosPromise}

View File

@@ -1,6 +1,6 @@
<!-- 数据字典 Select 选择器 --> <!-- 数据字典 Select 选择器 -->
<template> <template>
<el-select class="w-1/1" v-bind="attrs"> <el-select class="w-1/1" v-bind="attrs" filterable>
<template v-if="valueType === 'int'"> <template v-if="valueType === 'int'">
<el-option <el-option
v-for="(dict, index) in getIntDictOptions(dictType)" v-for="(dict, index) in getIntDictOptions(dictType)"

View File

@@ -30,7 +30,7 @@ import { downloadFile } from '@/api/system-boot/file'
import { previewFile } from '@/utils/fileDownLoad' import { previewFile } from '@/utils/fileDownLoad'
const { push, options, currentRoute } = useRouter() const { push, options, currentRoute } = useRouter()
const VITE_FLAG = import.meta.env.VITE_NAME == 'jibei' const VITE_FLAG = false//import.meta.env.VITE_NAME == 'jibei'
// const url = 'http://192.168.1.22:9009/excelreport' + currentRoute.value.href?.split('?')[1] // const url = 'http://192.168.1.22:9009/excelreport' + currentRoute.value.href?.split('?')[1]
const url = ref('') const url = ref('')
const excelOptions = ref({}) const excelOptions = ref({})

View File

@@ -103,7 +103,7 @@ const highlightDiagram = async () => {
let findProcessTask = false //是否已经高亮了进行中的任务 let findProcessTask = false //是否已经高亮了进行中的任务
//进行中高亮之后的任务 key 集合,用于过滤掉 taskList 进行中后面的任务,避免进行中后面的数据 Hover 还有数据 //进行中高亮之后的任务 key 集合,用于过滤掉 taskList 进行中后面的任务,避免进行中后面的数据 Hover 还有数据
let removeTaskDefinitionKeyList = [] let removeTaskDefinitionKeyList = []
// debugger //
//芋道这里取值是rootElements[0].flowElements //芋道这里取值是rootElements[0].flowElements
bpmnModeler.getDefinitions().rootElements[1].flowElements?.forEach((n: any) => { bpmnModeler.getDefinitions().rootElements[1].flowElements?.forEach((n: any) => {
let activity: any = activityList.find((m: any) => m.key === n.id) // 找到对应的活动 let activity: any = activityList.find((m: any) => m.key === n.id) // 找到对应的活动
@@ -135,7 +135,6 @@ const highlightDiagram = async () => {
// 处理 outgoing 出线 // 处理 outgoing 出线
const outgoing = getActivityOutgoing(activity) const outgoing = getActivityOutgoing(activity)
outgoing?.forEach((nn: any) => { outgoing?.forEach((nn: any) => {
// debugger
let targetActivity: any = activityList.find((m: any) => m.key === nn.targetRef.id) let targetActivity: any = activityList.find((m: any) => m.key === nn.targetRef.id)
// 如果目标活动存在则根据该活动是否结束进行【bpmn:SequenceFlow】连线的高亮设置 // 如果目标活动存在则根据该活动是否结束进行【bpmn:SequenceFlow】连线的高亮设置
if (targetActivity) { if (targetActivity) {

View File

@@ -2,14 +2,14 @@
<div class="panel-tab__content"> <div class="panel-tab__content">
<el-form :model="flowConditionForm" label-width="90px" size="small"> <el-form :model="flowConditionForm" label-width="90px" size="small">
<el-form-item label="流转类型"> <el-form-item label="流转类型">
<el-select v-model="flowConditionForm.type" @change="updateFlowType"> <el-select v-model="flowConditionForm.type" @change="updateFlowType" filterable>
<el-option label="普通流转路径" value="normal" /> <el-option label="普通流转路径" value="normal" />
<el-option label="默认流转路径" value="default" /> <el-option label="默认流转路径" value="default" />
<el-option label="条件流转路径" value="condition" /> <el-option label="条件流转路径" value="condition" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="条件格式" v-if="flowConditionForm.type === 'condition'" key="condition"> <el-form-item label="条件格式" v-if="flowConditionForm.type === 'condition'" key="condition">
<el-select v-model="flowConditionForm.conditionType"> <el-select v-model="flowConditionForm.conditionType" filterable>
<el-option label="表达式" value="expression" /> <el-option label="表达式" value="expression" />
<el-option label="脚本" value="script" /> <el-option label="脚本" value="script" />
</el-select> </el-select>
@@ -33,7 +33,7 @@
<el-input v-model="flowConditionForm.language" clearable @change="updateFlowCondition" /> <el-input v-model="flowConditionForm.language" clearable @change="updateFlowCondition" />
</el-form-item> </el-form-item>
<el-form-item label="脚本类型" key="scriptType"> <el-form-item label="脚本类型" key="scriptType">
<el-select v-model="flowConditionForm.scriptType"> <el-select v-model="flowConditionForm.scriptType" filterable>
<el-option label="内联脚本" value="inlineScript" /> <el-option label="内联脚本" value="inlineScript" />
<el-option label="外部脚本" value="externalScript" /> <el-option label="外部脚本" value="externalScript" />
</el-select> </el-select>

View File

@@ -3,7 +3,7 @@
<el-form label-width="80px"> <el-form label-width="80px">
<el-form-item label="流程表单"> <el-form-item label="流程表单">
<!-- <el-input v-model="formKey" clearable @change="updateElementFormKey" />--> <!-- <el-input v-model="formKey" clearable @change="updateElementFormKey" />-->
<el-select v-model="formKey" clearable @change="updateElementFormKey"> <el-select v-model="formKey" clearable @change="updateElementFormKey" filterable>
<el-option v-for="form in formList" :key="form.id" :label="form.name" :value="form.id" /> <el-option v-for="form in formList" :key="form.id" :label="form.name" :value="form.id" />
</el-select> </el-select>
</el-form-item> </el-form-item>

View File

@@ -64,7 +64,7 @@
prop='event' prop='event'
:rules="{ required: true, trigger: ['blur', 'change'] }" :rules="{ required: true, trigger: ['blur', 'change'] }"
> >
<el-select v-model='listenerForm.event'> <el-select v-model='listenerForm.event' filterable>
<el-option label='start' value='start' /> <el-option label='start' value='start' />
<el-option label='end' value='end' /> <el-option label='end' value='end' />
</el-select> </el-select>
@@ -74,7 +74,7 @@
prop='listenerType' prop='listenerType'
:rules="{ required: true, trigger: ['blur', 'change'] }" :rules="{ required: true, trigger: ['blur', 'change'] }"
> >
<el-select v-model='listenerForm.listenerType'> <el-select v-model='listenerForm.listenerType' filterable>
<el-option <el-option
v-for='i in Object.keys(listenerTypeObject)' v-for='i in Object.keys(listenerTypeObject)'
:key='i' :key='i'
@@ -125,7 +125,7 @@
key='listener-script-type' key='listener-script-type'
:rules="{ required: true, trigger: ['blur', 'change'], message: '请选择脚本类型' }" :rules="{ required: true, trigger: ['blur', 'change'], message: '请选择脚本类型' }"
> >
<el-select v-model='listenerForm.scriptType'> <el-select v-model='listenerForm.scriptType' filterable>
<el-option label='内联脚本' value='inlineScript' /> <el-option label='内联脚本' value='inlineScript' />
<el-option label='外部脚本' value='externalScript' /> <el-option label='外部脚本' value='externalScript' />
</el-select> </el-select>
@@ -228,7 +228,7 @@
prop='fieldType' prop='fieldType'
:rules="{ required: true, trigger: ['blur', 'change'] }" :rules="{ required: true, trigger: ['blur', 'change'] }"
> >
<el-select v-model='listenerFieldForm.fieldType'> <el-select v-model='listenerFieldForm.fieldType' filterable>
<el-option <el-option
v-for='i in Object.keys(fieldTypeObject)' v-for='i in Object.keys(fieldTypeObject)'
:key='i' :key='i'
@@ -318,7 +318,6 @@ const resetListenersList = () => {
} }
// 打开 监听器详情 侧边栏 // 打开 监听器详情 侧边栏
const openListenerForm = (listener, index?) => { const openListenerForm = (listener, index?) => {
// debugger
if (listener) { if (listener) {
listenerForm.value = initListenerForm(listener) listenerForm.value = initListenerForm(listener)
editingListenerIndex.value = index editingListenerIndex.value = index
@@ -356,7 +355,6 @@ const openListenerFieldForm = (field, index?) => {
} }
// 保存监听器注入字段 // 保存监听器注入字段
const saveListenerFiled = async () => { const saveListenerFiled = async () => {
// debugger
let validateStatus = await listenerFieldFormRef.value.validate() let validateStatus = await listenerFieldFormRef.value.validate()
if (!validateStatus) return // 验证不通过直接返回 if (!validateStatus) return // 验证不通过直接返回
if (editingListenerFieldIndex.value === -1) { if (editingListenerFieldIndex.value === -1) {
@@ -373,7 +371,6 @@ const saveListenerFiled = async () => {
} }
// 移除监听器字段 // 移除监听器字段
const removeListenerField = (index) => { const removeListenerField = (index) => {
// debugger
ElMessageBox.confirm('确认移除该字段吗?', '提示', { ElMessageBox.confirm('确认移除该字段吗?', '提示', {
confirmButtonText: '确 认', confirmButtonText: '确 认',
cancelButtonText: '取 消' cancelButtonText: '取 消'
@@ -402,7 +399,6 @@ const removeListener = (index) => {
} }
// 保存监听器配置 // 保存监听器配置
const saveListenerConfig = async () => { const saveListenerConfig = async () => {
// debugger
let validateStatus = await listenerFormRef.value.validate() let validateStatus = await listenerFormRef.value.validate()
if (!validateStatus) return // 验证不通过直接返回 if (!validateStatus) return // 验证不通过直接返回
const listenerObject = createListenerObject(listenerForm.value, false, prefix) const listenerObject = createListenerObject(listenerForm.value, false, prefix)

View File

@@ -62,7 +62,7 @@
prop="event" prop="event"
:rules="{ required: true, trigger: ['blur', 'change'] }" :rules="{ required: true, trigger: ['blur', 'change'] }"
> >
<el-select v-model="listenerForm.event"> <el-select v-model="listenerForm.event" filterable>
<el-option <el-option
v-for="i in Object.keys(listenerEventTypeObject)" v-for="i in Object.keys(listenerEventTypeObject)"
:key="i" :key="i"
@@ -83,7 +83,7 @@
prop="listenerType" prop="listenerType"
:rules="{ required: true, trigger: ['blur', 'change'] }" :rules="{ required: true, trigger: ['blur', 'change'] }"
> >
<el-select v-model="listenerForm.listenerType"> <el-select v-model="listenerForm.listenerType" filterable>
<el-option <el-option
v-for="i in Object.keys(listenerTypeObject)" v-for="i in Object.keys(listenerTypeObject)"
:key="i" :key="i"
@@ -134,7 +134,7 @@
key="listener-script-type" key="listener-script-type"
:rules="{ required: true, trigger: ['blur', 'change'], message: '请选择脚本类型' }" :rules="{ required: true, trigger: ['blur', 'change'], message: '请选择脚本类型' }"
> >
<el-select v-model="listenerForm.scriptType"> <el-select v-model="listenerForm.scriptType" filterable>
<el-option label="内联脚本" value="inlineScript" /> <el-option label="内联脚本" value="inlineScript" />
<el-option label="外部脚本" value="externalScript" /> <el-option label="外部脚本" value="externalScript" />
</el-select> </el-select>
@@ -161,7 +161,7 @@
<template v-if="listenerForm.event === 'timeout'"> <template v-if="listenerForm.event === 'timeout'">
<el-form-item label="定时器类型" prop="eventDefinitionType" key="eventDefinitionType"> <el-form-item label="定时器类型" prop="eventDefinitionType" key="eventDefinitionType">
<el-select v-model="listenerForm.eventDefinitionType"> <el-select v-model="listenerForm.eventDefinitionType" filterable>
<el-option label="统计时间" value="date" /> <el-option label="统计时间" value="date" />
<el-option label="持续时长" value="duration" /> <el-option label="持续时长" value="duration" />
<el-option label="循环" value="cycle" /> <el-option label="循环" value="cycle" />
@@ -259,7 +259,7 @@
prop="fieldType" prop="fieldType"
:rules="{ required: true, trigger: ['blur', 'change'] }" :rules="{ required: true, trigger: ['blur', 'change'] }"
> >
<el-select v-model="listenerFieldForm.fieldType"> <el-select v-model="listenerFieldForm.fieldType" filterable>
<el-option <el-option
v-for="i in Object.keys(fieldTypeObject)" v-for="i in Object.keys(fieldTypeObject)"
:key="i" :key="i"

View File

@@ -7,7 +7,7 @@
<el-button size="small" @click="changeConfig('或签')">或签</el-button> <el-button size="small" @click="changeConfig('或签')">或签</el-button>
</el-form-item> </el-form-item>
<el-form-item label="会签类型"> <el-form-item label="会签类型">
<el-select v-model="loopCharacteristics" @change="changeLoopCharacteristicsType"> <el-select v-model="loopCharacteristics" @change="changeLoopCharacteristicsType" filterable>
<el-option label="并行多重事件" value="ParallelMultiInstance" /> <el-option label="并行多重事件" value="ParallelMultiInstance" />
<el-option label="时序多重事件" value="SequentialMultiInstance" /> <el-option label="时序多重事件" value="SequentialMultiInstance" />
<el-option label="无" value="Null" /> <el-option label="无" value="Null" />

View File

@@ -9,7 +9,7 @@
flex-wrap: nowrap; flex-wrap: nowrap;
" "
> >
<el-select v-model="bindMessageId" @change="updateTaskMessage"> <el-select v-model="bindMessageId" @change="updateTaskMessage" filterable>
<el-option <el-option
v-for="key in Object.keys(messageMap)" v-for="key in Object.keys(messageMap)"
:value="key" :value="key"

View File

@@ -9,7 +9,7 @@
/> />
</el-form-item> </el-form-item>
<el-form-item label="脚本类型"> <el-form-item label="脚本类型">
<el-select v-model="scriptTaskForm.scriptType"> <el-select v-model="scriptTaskForm.scriptType" filterable>
<el-option label="内联脚本" value="inline" /> <el-option label="内联脚本" value="inline" />
<el-option label="外部资源" value="external" /> <el-option label="外部资源" value="external" />
</el-select> </el-select>

View File

@@ -6,6 +6,7 @@
clearable clearable
style='width: 100%' style='width: 100%'
@change='changeCandidateStrategy' @change='changeCandidateStrategy'
filterable
> >
<el-option <el-option
v-for='dict in RuleTypeList' v-for='dict in RuleTypeList'
@@ -26,6 +27,7 @@
multiple multiple
style='width: 100%' style='width: 100%'
@change='updateElementTask' @change='updateElementTask'
filterable
> >
<el-option v-for='item in roleOptions' :key='item.id' :label='item.name' :value='item.id' /> <el-option v-for='item in roleOptions' :key='item.id' :label='item.name' :value='item.id' />
</el-select> </el-select>
@@ -61,6 +63,7 @@
multiple multiple
style='width: 100%' style='width: 100%'
@change='updateElementTask' @change='updateElementTask'
filterable
> >
<el-option <el-option
v-for='item in userOptions' v-for='item in userOptions'

View File

@@ -126,7 +126,8 @@ const prop = defineProps({
width: { type: [String, Number] }, width: { type: [String, Number] },
height: { type: [String, Number] }, height: { type: [String, Number] },
timeKey: { type: [String, Number] }, timeKey: { type: [String, Number] },
timeValue: { type: Object } timeValue: { type: Object },
searchKey: { type: [String, Number] }
}) })
const headerHeight = ref(57) const headerHeight = ref(57)
@@ -347,22 +348,22 @@ onMounted(() => {
tableStore.index() tableStore.index()
}) })
watch( watch(
() => prop.timeKey, () => prop.searchKey,
val => { val => {
tableStore.index() tableStore.index()
} }
) )
watch( // watch(
() => prop.timeValue, // () => prop.timeValue,
val => { // val => {
tableStore.index() // tableStore.index()
}, // },
{ // {
deep: true // deep: true
} // }
) // )
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '@/assets/font/iconfont.css'; @import '@/assets/font/iconfont.css';

View File

@@ -122,7 +122,8 @@ const prop = defineProps({
width: { type: [String, Number] }, width: { type: [String, Number] },
height: { type: [String, Number] }, height: { type: [String, Number] },
timeKey: { type: [String, Number] }, timeKey: { type: [String, Number] },
timeValue: { type: Object } timeValue: { type: Object },
searchKey: { type: [String, Number] }
}) })
const headerHeight = ref(57) const headerHeight = ref(57)
@@ -329,22 +330,22 @@ onMounted(() => {
tableStore.index() tableStore.index()
}) })
watch( watch(
() => prop.timeKey, () => prop.searchKey,
val => { val => {
tableStore.index() tableStore.index()
} }
) )
watch( // watch(
() => prop.timeValue, // () => prop.timeValue,
val => { // val => {
tableStore.index() // tableStore.index()
}, // },
{ // {
deep: true // deep: true
} // }
) // )
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '@/assets/font/iconfont.css'; @import '@/assets/font/iconfont.css';

View File

@@ -7,7 +7,8 @@
@selectChange="selectChange" @selectChange="selectChange"
datePicker datePicker
v-if="fullscreen" v-if="fullscreen"
></TableHeader> >
</TableHeader>
<div <div
class="monitoringPoints" class="monitoringPoints"
:style="{ :style="{
@@ -120,7 +121,8 @@ const prop = defineProps({
width: { type: [String, Number] }, width: { type: [String, Number] },
height: { type: [String, Number] }, height: { type: [String, Number] },
timeKey: { type: [String, Number] }, timeKey: { type: [String, Number] },
timeValue: { type: Object } timeValue: { type: Object },
searchKey: { type: [String, Number] }
}) })
const headerHeight = ref(57) const headerHeight = ref(57)
@@ -328,22 +330,22 @@ onMounted(() => {
tableStore.index() tableStore.index()
}) })
watch( watch(
() => prop.timeKey, () => prop.searchKey,
val => { val => {
tableStore.index() tableStore.index()
} }
) )
watch( // watch(
() => prop.timeValue, // () => prop.timeValue,
val => { // val => {
tableStore.index() // tableStore.index()
}, // },
{ // {
deep: true // deep: true
} // }
) // )
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '@/assets/font/iconfont.css'; @import '@/assets/font/iconfont.css';

View File

@@ -150,7 +150,8 @@ const prop = defineProps({
width: { type: [String, Number] }, width: { type: [String, Number] },
height: { type: [String, Number] }, height: { type: [String, Number] },
timeKey: { type: [String, Number] }, timeKey: { type: [String, Number] },
timeValue: { type: Object } timeValue: { type: Object },
searchKey: { type: [String, Number] }
}) })
const headerHeight = ref(57) const headerHeight = ref(57)
@@ -233,7 +234,7 @@ const tableStore: any = new TableStore({
if (tableStore.table.params.statisticalType.name == '终端厂家') { if (tableStore.table.params.statisticalType.name == '终端厂家') {
return k.count != 0 return k.count != 0
} else { } else {
return !k.citName.includes('上送') return !k.name.includes('上送')
} }
}) })
} }
@@ -246,22 +247,28 @@ onMounted(() => {
tableStore.index() tableStore.index()
}) })
watch( watch(
() => prop.timeKey, () => prop.searchKey,
val => { val => {
tableStore.index() tableStore.index()
} }
) )
watch( // watch(
() => prop.timeValue, // () => prop.timeKey,
// val => {
// tableStore.index()
// }
// )
// watch(
// () => prop.timeValue,
val => { // val => {
tableStore.index() // tableStore.index()
}, // },
{ // {
deep: true // deep: true
} // }
) // )
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '@/assets/font/iconfont.css'; @import '@/assets/font/iconfont.css';

View File

@@ -169,7 +169,7 @@ const handlerBar = (options: any) => {
if (Array.isArray(options.series)) { if (Array.isArray(options.series)) {
options.series.forEach((item: any) => { options.series.forEach((item: any) => {
if (item.type === 'bar') { if (item.type === 'bar') {
item.barMinHeight = 10 item.barMinHeight = 0
item.barMaxWidth = 20 item.barMaxWidth = 20
item.itemStyle = Object.assign( item.itemStyle = Object.assign(
{ {

View File

@@ -0,0 +1,322 @@
// 辅助函数
const getMax = (temp, tempA, tempB, tempC) => {
temp = temp > tempA ? temp : tempA
temp = temp > tempB ? temp : tempB
if (tempC !== undefined) {
temp = temp > tempC ? temp : tempC
}
return temp
}
const getMaxTwo = (temp, tempA, tempB) => {
temp = temp > tempA ? temp : tempA
temp = temp > tempB ? temp : tempB
return temp
}
const getMin = (temp, tempA, tempB, tempC) => {
temp = temp < tempA ? temp : tempA
temp = temp < tempB ? temp : tempB
if (tempC !== undefined) {
temp = temp < tempC ? temp : tempC
}
return temp
}
const getMinOpen = (temp, tempA, tempB) => {
temp = temp < tempA ? temp : tempA
temp = temp < tempB ? temp : tempB
return temp
}
// 数据处理函数
const fliteWaveData = (wp, step, iphasicValue, isOpen) => {
const rmsData = wp.listRmsData
const pt = Number(wp.pt) / 1000
const ct = Number(wp.ct)
const titleList = wp.waveTitle
let xishu = pt
let aTitle = '',
bTitle = '',
cTitle = '',
unit = '电压'
let rmsvFirstX = 0,
rmsvFirstY = 0,
rmsvSecondX = 0,
rmsvSecondY = 0,
firstZhou = 'a',
secondeZhou = 'a'
let ifmax = 0,
ifmin = 0,
ismax = 0,
ismin = 0,
rfmax = 0,
rfmin = 0,
rsmax = 0,
rsmin = 0
const shunshiFA = []
const shunshiFB = []
const shunshiFC = []
const shunshiSA = []
const shunshiSB = []
const shunshiSC = []
const rmsFA = []
const rmsFB = []
const rmsFC = []
const rmsSA = []
const rmsSB = []
const rmsSC = []
if (titleList[iphasicValue * step + 1]?.substring(0, 1) !== 'U') {
xishu = ct
unit = '电流'
}
for (let i = 1; i <= iphasicValue; i++) {
switch (i) {
case 1:
aTitle = titleList[iphasicValue * step + i]?.substring(1) || ''
break
case 2:
bTitle = titleList[iphasicValue * step + i]?.substring(1) || ''
break
case 3:
cTitle = titleList[iphasicValue * step + i]?.substring(1) || ''
break
}
}
if (rmsData[0] && rmsData[0][iphasicValue * step + 1] !== undefined) {
rfmax = rmsData[0][iphasicValue * step + 1] * xishu
rfmin = rmsData[0][iphasicValue * step + 1] * xishu
rmsvFirstY = rmsData[0][iphasicValue * step + 1] * xishu
rmsvFirstX = rmsData[0][0]
rsmax = rmsData[0][iphasicValue * step + 1]
rsmin = rmsData[0][iphasicValue * step + 1]
rmsvSecondY = rmsData[0][iphasicValue * step + 1]
rmsvSecondX = rmsData[0][0]
}
for (let rms = 0; rms < rmsData.length; rms++) {
if (!rmsData[rms] || rmsData[rms][iphasicValue * step + 1] === undefined) {
break
}
switch (iphasicValue) {
case 1:
const rmsFirstA = rmsData[rms][iphasicValue * step + 1] * xishu
rmsFA.push([rmsData[rms][0], rmsFirstA])
rfmax = rfmax > rmsFirstA ? rfmax : rmsFirstA
rfmin = rfmin < rmsFirstA ? rfmin : rmsFirstA
if (rfmin < rmsvFirstY) {
rmsvFirstY = rfmin
firstZhou = 'a'
rmsvFirstX = rmsData[rms][0]
}
const rmsSecondA = rmsData[rms][iphasicValue * step + 1]
rmsSA.push([rmsData[rms][0], rmsSecondA])
rsmax = rsmax > rmsSecondA ? rsmax : rmsSecondA
rsmin = rsmin < rmsSecondA ? rsmin : rmsSecondA
if (rsmin < rmsvSecondY) {
rmsvSecondY = rsmin
secondeZhou = 'a'
rmsvSecondX = rmsData[rms][0]
}
break
case 2:
const rmsFirstA2 = rmsData[rms][iphasicValue * step + 1] * xishu
const rmsFirstB2 = rmsData[rms][iphasicValue * step + 2] * xishu
rmsFA.push([rmsData[rms][0], rmsFirstA2])
rmsFB.push([rmsData[rms][0], rmsFirstB2])
rfmax = getMaxTwo(rfmax, rmsFirstA2, rmsFirstB2)
rfmin = getMinOpen(rfmin, rmsFirstA2, rmsFirstB2)
if (rfmin < rmsvFirstY) {
rmsvFirstY = rfmin
if (rfmin === rmsFirstA2) {
firstZhou = 'a'
} else if (rfmin === rmsFirstB2) {
firstZhou = 'b'
}
rmsvFirstX = rmsData[rms][0]
}
const rmsSecondA2 = rmsData[rms][iphasicValue * step + 1]
const rmsSecondB2 = rmsData[rms][iphasicValue * step + 2]
rmsSA.push([rmsData[rms][0], rmsSecondA2])
rmsSB.push([rmsData[rms][0], rmsSecondB2])
rsmax = getMaxTwo(rsmax, rmsSecondA2, rmsSecondB2)
rsmin = getMinOpen(rsmin, rmsSecondA2, rmsSecondB2)
if (rsmin < rmsvSecondY) {
rmsvSecondY = rsmin
if (rsmin === rmsSecondA2) {
secondeZhou = 'a'
} else if (rsmin === rmsSecondB2) {
secondeZhou = 'b'
}
rmsvSecondX = rmsData[rms][0]
}
break
case 3:
const rmsFirstA3 = rmsData[rms][iphasicValue * step + 1] * xishu
const rmsFirstB3 = rmsData[rms][iphasicValue * step + 2] * xishu
const rmsFirstC3 = rmsData[rms][iphasicValue * step + 3] * xishu
rmsFA.push([rmsData[rms][0], rmsFirstA3])
rmsFB.push([rmsData[rms][0], rmsFirstB3])
rmsFC.push([rmsData[rms][0], rmsFirstC3])
rfmax = getMax(rfmax, rmsFirstA3, rmsFirstB3, rmsFirstC3)
rfmin = isOpen
? getMinOpen(rfmin, rmsFirstA3, rmsFirstC3)
: getMin(rfmin, rmsFirstA3, rmsFirstB3, rmsFirstC3)
if (rfmin < rmsvFirstY) {
rmsvFirstY = rfmin
if (rfmin === rmsFirstA3) {
firstZhou = 'a'
} else if (rfmin === rmsFirstB3) {
firstZhou = 'b'
} else {
firstZhou = 'c'
}
rmsvFirstX = rmsData[rms][0]
}
const rmsSecondA3 = rmsData[rms][iphasicValue * step + 1]
const rmsSecondB3 = rmsData[rms][iphasicValue * step + 2]
const rmsSecondC3 = rmsData[rms][iphasicValue * step + 3]
rmsSA.push([rmsData[rms][0], rmsSecondA3])
rmsSB.push([rmsData[rms][0], rmsSecondB3])
rmsSC.push([rmsData[rms][0], rmsSecondC3])
rsmax = getMax(rsmax, rmsSecondA3, rmsSecondB3, rmsSecondC3)
rsmin = isOpen
? getMinOpen(rsmin, rmsSecondA3, rmsSecondC3)
: getMin(rsmin, rmsSecondA3, rmsSecondB3, rmsSecondC3)
if (rsmin < rmsvSecondY) {
rmsvSecondY = rsmin
if (rsmin === rmsSecondA3) {
secondeZhou = 'a'
} else if (rsmin === rmsSecondB3) {
secondeZhou = 'b'
} else {
secondeZhou = 'c'
}
rmsvSecondX = rmsData[rms][0]
}
break
}
}
const instantF = { max: ifmax, min: ifmin }
const instantS = { max: ismax, min: ismin }
const RMSF = { max: rfmax, min: rfmin }
const RMSS = { max: rsmax, min: rsmin }
const RMSFMinDetail = { rmsvFirstX, rmsvFirstY, firstZhou }
const RMSSMinDetail = { rmsvSecondX, rmsvSecondY, secondeZhou }
const shunshiF = { shunshiFA, shunshiFB, shunshiFC }
const shunshiS = { shunshiSA, shunshiSB, shunshiSC }
const RMSFWave = { rmsFA, rmsFB, rmsFC }
const RMSSWave = { rmsSA, rmsSB, rmsSC }
const title = { aTitle, bTitle, cTitle, unit }
return {
instantF,
instantS,
RMSF,
RMSS,
RMSFMinDetail,
RMSSMinDetail,
shunshiF,
shunshiS,
RMSFWave,
RMSSWave,
title,
unit
}
}
// 监听消息
self.onmessage = function (e) {
const { wp, isOpen, value, boxoList, requestId } = e.data
try {
const iphasicValue = wp.iphasic || 1
const picCounts = (wp.waveTitle.length - 1) / iphasicValue
const waveDatas = []
for (let i = 0; i < picCounts; i++) {
const data = fliteWaveData(wp, i, iphasicValue, isOpen, boxoList)
waveDatas.push(data)
}
// 处理标题
let titles = ''
if (boxoList.systemType == 'pms') {
titles =
'变电站名称:' +
boxoList.powerStationName +
' 监测点名称:' +
boxoList.measurementPointName +
' 发生时刻:' +
boxoList.startTime +
' 暂降(骤升)幅值:' +
(boxoList.featureAmplitude * 100).toFixed(2) +
'%  持续时间:' +
boxoList.duration +
's'
} else if (boxoList.systemType == 'ZL') {
titles =
(boxoList.engineeringName == undefined ? '' : ' 项目名称:' + boxoList.engineeringName) +
' 监测点名称:' +
boxoList.equipmentName +
' 发生时刻:' +
boxoList.startTime +
' 暂降(骤升)幅值:' +
boxoList.evtParamVVaDepth +
'% 持续时间:' +
boxoList.evtParamTm +
's'
} else if (boxoList.systemType == 'YPT') {
titles =
(boxoList.engineeringName == undefined ? '' : ' 项目名称:' + boxoList.engineeringName) +
' 监测点名称:' +
boxoList.lineName +
' 发生时刻:' +
boxoList.startTime +
' 暂降(骤升)幅值:' +
(boxoList.featureAmplitude * 100).toFixed(2) +
'% 持续时间:' +
boxoList.persistTime +
's'
} else {
titles =
' 变电站名称:' +
boxoList.subName +
' 监测点名称:' +
boxoList.lineName +
' 发生时刻:' +
boxoList.startTime +
' 暂降(骤升)幅值:' +
(boxoList.featureAmplitude * 100).toFixed(2) +
'% 持续时间:' +
boxoList.duration +
's'
}
// 发送处理结果回主线程
self.postMessage({
requestId,
titles: titles,
success: true,
waveDatas,
time: wp.time,
type: wp.waveType,
severity: wp.yzd,
iphasic: iphasicValue
})
} catch (error) {
self.postMessage({
requestId,
success: false,
error: error.message
})
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,207 @@
// waveData.worker.js
self.addEventListener('message', function (e) {
const { wp, value, iphasic, isOpen, boxoList, requestId } = e.data
// 处理波形数据的函数
const fliteWaveData = (wp, step) => {
// 将原有的fliteWaveData函数实现复制到这里
const shunData = wp.listWaveData
const pt = Number(wp.pt) / 1000
const ct = Number(wp.ct)
const titleList = wp.waveTitle
let xishu = pt
let aTitle = '',
bTitle = '',
cTitle = '',
unit = '电压'
let ifmax = 0,
ifmin = 0,
ismax = 0,
ismin = 0
const shunshiFA = []
const shunshiFB = []
const shunshiFC = []
const shunshiSA = []
const shunshiSB = []
const shunshiSC = []
if (shunData.length > 0) {
if (titleList[iphasic * step + 1]?.substring(0, 1) !== 'U') {
xishu = ct
unit = '电流'
}
for (let i = 1; i <= iphasic; i++) {
switch (i) {
case 1:
aTitle = titleList[iphasic * step + i]?.substring(1) || ''
break
case 2:
bTitle = titleList[iphasic * step + i]?.substring(1) || ''
break
case 3:
cTitle = titleList[iphasic * step + i]?.substring(1) || ''
break
}
}
if (shunData[0][iphasic * step + 1] !== undefined) {
ifmax = shunData[0][iphasic * step + 1] * xishu
ifmin = shunData[0][iphasic * step + 1] * xishu
ismax = shunData[0][iphasic * step + 1]
ismin = shunData[0][iphasic * step + 1]
}
for (let shun = 0; shun < shunData.length; shun++) {
if (shunData[shun][iphasic * step + 1] === undefined) {
break
}
switch (iphasic) {
case 1:
const shunFirstA = shunData[shun][iphasic * step + 1] * xishu
shunshiFA.push([shunData[shun][0], shunFirstA])
ifmax = Math.max(ifmax, shunFirstA)
ifmin = Math.min(ifmin, shunFirstA)
const shunSecondA = shunData[shun][iphasic * step + 1]
shunshiSA.push([shunData[shun][0], shunSecondA])
ismax = Math.max(ismax, shunSecondA)
ismin = Math.min(ismin, shunSecondA)
break
case 2:
const shunFirstA2 = shunData[shun][iphasic * step + 1] * xishu
const shunFirstB2 = shunData[shun][iphasic * step + 2] * xishu
shunshiFA.push([shunData[shun][0], shunFirstA2])
shunshiFB.push([shunData[shun][0], shunFirstB2])
ifmax = Math.max(ifmax, shunFirstA2, shunFirstB2)
ifmin = Math.min(ifmin, shunFirstA2, shunFirstB2)
const shunSecondA2 = shunData[shun][iphasic * step + 1]
const shunSecondB2 = shunData[shun][iphasic * step + 2]
shunshiSA.push([shunData[shun][0], shunSecondA2])
shunshiSB.push([shunData[shun][0], shunSecondB2])
ismax = Math.max(ismax, shunSecondA2, shunSecondB2)
ismin = Math.min(ismin, shunSecondA2, shunSecondB2)
break
case 3:
const shunFirstA3 = shunData[shun][iphasic * step + 1] * xishu
const shunFirstB3 = shunData[shun][iphasic * step + 2] * xishu
const shunFirstC3 = shunData[shun][iphasic * step + 3] * xishu
shunshiFA.push([shunData[shun][0], shunFirstA3])
shunshiFB.push([shunData[shun][0], shunFirstB3])
shunshiFC.push([shunData[shun][0], shunFirstC3])
ifmax = Math.max(ifmax, shunFirstA3, shunFirstB3, shunFirstC3)
ifmin = isOpen
? Math.min(ifmin, shunFirstA3, shunFirstC3)
: Math.min(ifmin, shunFirstA3, shunFirstB3, shunFirstC3)
const shunSecondA3 = shunData[shun][iphasic * step + 1]
const shunSecondB3 = shunData[shun][iphasic * step + 2]
const shunSecondC3 = shunData[shun][iphasic * step + 3]
shunshiSA.push([shunData[shun][0], shunSecondA3])
shunshiSB.push([shunData[shun][0], shunSecondB3])
shunshiSC.push([shunData[shun][0], shunSecondC3])
ismax = Math.max(ismax, shunSecondA3, shunSecondB3, shunSecondC3)
ismin = isOpen
? Math.min(ismin, shunSecondA3, shunSecondC3)
: Math.min(ismin, shunSecondA3, shunSecondB3, shunSecondC3)
break
}
}
}
const instantF = { max: ifmax, min: ifmin }
const instantS = { max: ismax, min: ismin }
const shunshiF = { shunshiFA, shunshiFB, shunshiFC }
const shunshiS = { shunshiSA, shunshiSB, shunshiSC }
const title = { aTitle, bTitle, cTitle, unit }
return { instantF, instantS, shunshiF, shunshiS, title, unit }
}
// 处理标题
let titles = ''
if (boxoList.systemType == 'pms') {
titles =
'变电站名称:' +
boxoList.powerStationName +
' 监测点名称:' +
boxoList.measurementPointName +
' 发生时刻:' +
boxoList.startTime +
' 暂降(骤升)幅值:' +
(boxoList.featureAmplitude * 100).toFixed(2) +
'% 持续时间:' +
boxoList.duration +
's'
} else if (boxoList.systemType == 'ZL') {
titles =
(boxoList.engineeringName == undefined ? '' : ' 项目名称:' + boxoList.engineeringName) +
' 监测点名称:' +
boxoList.equipmentName +
' 发生时刻:' +
boxoList.startTime +
' 暂降(骤升)幅值:' +
boxoList.evtParamVVaDepth +
'% 持续时间:' +
boxoList.evtParamTm +
's'
} else if (boxoList.systemType == 'YPT') {
titles =
(boxoList.engineeringName == undefined ? '' : ' 项目名称:' + boxoList.engineeringName) +
' 监测点名称:' +
boxoList.lineName +
' 发生时刻:' +
boxoList.startTime +
' 暂降(骤升)幅值:' +
(boxoList.featureAmplitude * 100).toFixed(2) +
'% 持续时间:' +
boxoList.persistTime +
's'
} else {
titles =
'变电站名称:' +
boxoList.subName +
' 监测点名称:' +
boxoList.lineName +
' 发生时刻:' +
boxoList.startTime +
' 暂降(骤升)幅值:' +
(boxoList.featureAmplitude * 100).toFixed(2) +
'% 持续时间:' +
boxoList.duration +
's'
}
const iphasicValue = wp.iphasic || 1
const picCounts = (wp.waveTitle.length - 1) / iphasicValue
const waveDatas = []
for (let i = 0; i < picCounts; i++) {
const data = fliteWaveData(wp, i)
waveDatas.push(data)
}
const time = wp.time
const type = wp.waveType
let severity = wp.yzd
if (severity < 0) {
severity = '/'
type = '/'
}
// 将处理结果发送回主线程
self.postMessage({
requestId,
success: true,
waveDatas,
time,
type,
severity,
titles,
iphasic: iphasicValue
})
})

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
<div v-if="view2"> <div v-if="view2">
<el-row> <el-row>
<el-col :span="12"> <el-col :span="12">
<span style="font-size: 14px; line-height: 30px">值类型选择:</span> <!-- <span style="font-size: 14px; line-height: 30px">值类型选择:</span>
<el-select <el-select
style="min-width: 200px; width: 200px" style="min-width: 200px; width: 200px"
@change="changeView" @change="changeView"
@@ -15,7 +15,11 @@
:label="item.label" :label="item.label"
:value="item.value" :value="item.value"
></el-option> ></el-option>
</el-select> </el-select> -->
<el-radio-group v-model.trim="value" @change="changeView">
<el-radio-button label="一次值" :value="1" />
<el-radio-button label="二次值" :value="2" />
</el-radio-group>
<!-- <el-button v-if="view2 && senior" class="ml10" type="primary" @click="AdvancedAnalytics"> <!-- <el-button v-if="view2 && senior" class="ml10" type="primary" @click="AdvancedAnalytics">
高级分析 高级分析
</el-button> --> </el-button> -->
@@ -24,37 +28,18 @@
<el-button @click="backbxlb" icon="el-icon-Back" style="float: right">返回</el-button> <el-button @click="backbxlb" icon="el-icon-Back" style="float: right">返回</el-button>
</el-col> </el-col>
</el-row> </el-row>
<div v-loading="loading" style="height: calc(100vh - 190px)"> <div v-loading="loading" style="height: calc(100vh - 190px)" class="mt10">
<el-tabs v-if="view4" class="default-main" v-model="bxactiveName" @tab-click="bxhandleClick"> <el-tabs v-if="view4" type="border-card" v-model="bxactiveName"
<el-tab-pane @tab-click="bxhandleClick">
label="瞬时波形" <el-tab-pane label="瞬时波形" name="ssbx" class="boxbx pt10 pb10"
name="ssbx" :style="'height:' + bxecharts + ';overflow-y: scroll;'">
class="boxbx pt10 pb10" <shushiboxi ref="shushiboxiRef" v-if="bxactiveName == 'ssbx' && showBoxi" :value="value"
:style="'height:' + bxecharts + ';overflow-y: scroll;'" :parentHeight="parentHeight" :boxoList="boxoList" :wp="wp"></shushiboxi>
>
<shushiboxi
ref="shushiboxiRef"
v-if="bxactiveName == 'ssbx' && showBoxi"
:value="value"
:parentHeight="parentHeight"
:boxoList="boxoList"
:wp="wp"
></shushiboxi>
</el-tab-pane> </el-tab-pane>
<el-tab-pane <el-tab-pane label="RMS波形" class="boxbx pt10 pb10" name="rmsbx"
label="RMS波形" :style="'height:' + bxecharts + ';overflow-y: scroll;'">
class="boxbx pt10 pb10" <rmsboxi ref="rmsboxiRef" v-if="bxactiveName == 'rmsbx' && showBoxi" :value="value"
name="rmsbx" :parentHeight="parentHeight" :boxoList="boxoList" :wp="wp"></rmsboxi>
:style="'height:' + bxecharts + ';overflow-y: scroll;'"
>
<rmsboxi
ref="rmsboxiRef"
v-if="bxactiveName == 'rmsbx' && showBoxi"
:value="value"
:parentHeight="parentHeight"
:boxoList="boxoList"
:wp="wp"
></rmsboxi>
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
<el-empty v-else description="暂无数据" style="height: calc(100vh - 190px)" /> <el-empty v-else description="暂无数据" style="height: calc(100vh - 190px)" />
@@ -70,10 +55,12 @@
import shushiboxi from '@/components/echarts/shushiboxi.vue' import shushiboxi from '@/components/echarts/shushiboxi.vue'
import rmsboxi from '@/components/echarts/rmsboxi.vue' import rmsboxi from '@/components/echarts/rmsboxi.vue'
import analytics from '@/components/echarts/analytics.vue' import analytics from '@/components/echarts/analytics.vue'
import { ref, reactive } from 'vue' import { ref, shallowRef } from 'vue'
import { analysis } from '@/api/advance-boot/analyse' import { analysis } from '@/api/advance-boot/analyse'
import { mainHeight } from '@/utils/layout' import { mainHeight } from '@/utils/layout'
import { getMonitorEventAnalyseWave, downloadWaveFile } from '@/api/event-boot/transient' import { getMonitorEventAnalyseWave, downloadWaveFile } from '@/api/event-boot/transient'
import { buildWaveStoreKey, registerWaveRaw, buildSlimWp, releaseWaveRaw } from '@/utils/waveRawStore'
import { clearWaveCache } from '@/utils/waveCache'
const emit = defineEmits(['backbxlb']) const emit = defineEmits(['backbxlb'])
interface Props { interface Props {
// boxoList: any // boxoList: any
@@ -100,10 +87,10 @@ const options = ref([
} }
]) ])
const shushiboxiRef = ref() const shushiboxiRef = ref()
const bxecharts = ref(mainHeight(95).height as any) const bxecharts = ref(mainHeight(145).height as any)
const view2 = ref(true) const view2 = ref(true)
const boxoList: any = ref(null) const boxoList: any = ref(null)
const wp = ref(null) const wp = shallowRef<any>(null)
const showBoxi = ref(true) const showBoxi = ref(true)
const view3 = ref(false) const view3 = ref(false)
const view4 = ref(false) const view4 = ref(false)
@@ -117,7 +104,9 @@ const open = async (row: any) => {
if (res != undefined) { if (res != undefined) {
boxoList.value = row boxoList.value = row
boxoList.value.pt = res.data.pt boxoList.value.pt = res.data.pt
wp.value = res.data const storeKey = buildWaveStoreKey(row)
registerWaveRaw(storeKey, res.data)
wp.value = buildSlimWp(res.data, storeKey)
loading.value = false loading.value = false
view4.value = true view4.value = true
} }
@@ -128,9 +117,6 @@ const open = async (row: any) => {
}) })
} }
const bxhandleClick = (tab: any) => { const bxhandleClick = (tab: any) => {
if (shushiboxiRef.value) shushiboxiRef.value.backbxlb()
if (rmsboxiRef.value) rmsboxiRef.value.backbxlb()
loading.value = true loading.value = true
if (tab.name == 'ssbx') { if (tab.name == 'ssbx') {
bxactiveName.value = 'ssbx' bxactiveName.value = 'ssbx'
@@ -143,10 +129,12 @@ const bxhandleClick = (tab: any) => {
// console.log(tab, event); // console.log(tab, event);
} }
const backbxlb = () => { const backbxlb = () => {
const storeKey = wp.value?.waveStoreKey
releaseWaveRaw(storeKey, 'all')
clearWaveCache()
boxoList.value = null boxoList.value = null
wp.value = null wp.value = null
if (shushiboxiRef.value) shushiboxiRef.value.backbxlb()
if (rmsboxiRef.value) rmsboxiRef.value.backbxlb()
emit('backbxlb') emit('backbxlb')
} }
@@ -173,8 +161,8 @@ const AdvancedAnalytics = () => {
view2.value = false view2.value = false
} }
const changeView = () => { const changeView = () => {
if (shushiboxiRef.value) shushiboxiRef.value.backbxlb() shushiboxiRef.value?.backbxlb?.()
if (rmsboxiRef.value) rmsboxiRef.value.backbxlb() rmsboxiRef.value?.backbxlb?.()
showBoxi.value = false showBoxi.value = false
setTimeout(() => { setTimeout(() => {
showBoxi.value = true showBoxi.value = true

View File

@@ -1,22 +1,22 @@
<template> <template>
<div style="width: 540px"> <div :style="{ width: isMinuteData ? '750px' : '620px' }">
<el-select v-model="interval" style="min-width: 90px; width: 90px; margin-right: 10px" @change="timeChange"> <el-select v-model="interval" style="min-width: 90px; width: 90px; margin-right: 10px" @change="timeChange" filterable>
<el-option v-for="item in timeOptions" :key="item.value" :label="item.label" :value="item.value" /> <el-option v-for="item in timeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select> </el-select>
<el-date-picker <el-date-picker
v-model="timeValue" v-model="timeValue"
type="daterange" :type="pickerType"
:disabled="disabledPicker" :disabled="disabledPicker"
:disabled-date="isFutureDate" :disabled-date="isFutureDate"
style="width: 220px; margin-right: 10px" :style="{ width: isMinuteData ? '360px' : '220px', marginRight: '10px' }"
unlink-panels unlink-panels
:clearable="false" :clearable="false"
range-separator="" range-separator=""
start-placeholder="开始日期" :start-placeholder="isMinuteData ? '开始时间' : '开始日期'"
end-placeholder="结束日期" :end-placeholder="isMinuteData ? '结束时间' : '结束日期'"
value-format="YYYY-MM-DD" :value-format="pickerValueFormat"
:shortcuts="shortcuts" :shortcuts="isMinuteData ? undefined : shortcuts"
/> />
<el-button :disabled="backDisabled" type="primary" :icon="DArrowLeft" @click="preClick"></el-button> <el-button :disabled="backDisabled" type="primary" :icon="DArrowLeft" @click="preClick"></el-button>
<el-button type="primary" :icon="VideoPause" @click="nowTime">当前</el-button> <el-button type="primary" :icon="VideoPause" @click="nowTime">当前</el-button>
@@ -26,18 +26,41 @@
<script lang="ts" setup> <script lang="ts" setup>
import { DArrowLeft, VideoPause, DArrowRight } from '@element-plus/icons-vue' import { DArrowLeft, VideoPause, DArrowRight } from '@element-plus/icons-vue'
import { ref, onMounted, nextTick, watch } from 'vue' import { ref, onMounted, computed, watch } from 'vue'
interface Props { interface Props {
nextFlag?: boolean nextFlag?: boolean
theCurrentTime?: boolean theCurrentTime?: boolean
isStatisticData?: number | string
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
nextFlag: false, nextFlag: false,
theCurrentTime: false theCurrentTime: false,
isStatisticData: 0
}) })
const isMinuteData = computed(() => Number(props.isStatisticData) === 1)
const pickerType = computed(() => (isMinuteData.value ? 'datetimerange' : 'daterange'))
const pickerValueFormat = computed(() => (isMinuteData.value ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD'))
const toDatePart = (value: string) => (value?.includes(' ') ? value.split(' ')[0] : value)
const toMinuteStart = (date: string) => `${toDatePart(date)} 00:00:00`
const toMinuteEnd = (date: string) => `${toDatePart(date)} 23:59:59`
const normalizeRange = (start: string, end: string) => {
if (!isMinuteData.value) {
return [toDatePart(start), toDatePart(end)]
}
return [toMinuteStart(start), toMinuteEnd(end)]
}
const setRangeValue = (start: string, end: string) => {
timeValue.value = normalizeRange(start, end)
}
const interval = ref(3) const interval = ref(3)
const timeFlag = ref(1) const timeFlag = ref(1)
const count = ref(0) const count = ref(0)
@@ -95,13 +118,13 @@ const timeChange = (e: number) => {
if (e == 1) { if (e == 1) {
disabledPicker.value = true disabledPicker.value = true
timeValue.value = [setTime(1), setTime()] setRangeValue(setTime(1), setTime())
} else if (e == 2) { } else if (e == 2) {
disabledPicker.value = true disabledPicker.value = true
timeValue.value = [setTime(2), setTime()] setRangeValue(setTime(2), setTime())
} else if (e == 3) { } else if (e == 3) {
disabledPicker.value = true disabledPicker.value = true
timeValue.value = [setTime(3), setTime()] setRangeValue(setTime(3), setTime())
} else if (e == 4) { } else if (e == 4) {
let year = parseInt(setTime().substring(0, 4)) let year = parseInt(setTime().substring(0, 4))
let month = parseInt(setTime().substring(5, 7)) let month = parseInt(setTime().substring(5, 7))
@@ -111,12 +134,12 @@ const timeChange = (e: number) => {
var dayOfWeek = start.getDay() == 0 ? 7 : start.getDay() - 1 // 如果为周日则置为7天 var dayOfWeek = start.getDay() == 0 ? 7 : start.getDay() - 1 // 如果为周日则置为7天
disabledPicker.value = true disabledPicker.value = true
timeValue.value = [setTime(0, dayOfWeek), setTime(0, -6 + dayOfWeek)] setRangeValue(setTime(0, dayOfWeek), setTime(0, -6 + dayOfWeek))
} else if (e == 5) { } else if (e == 5) {
disabledPicker.value = false disabledPicker.value = false
backDisabled.value = true backDisabled.value = true
preDisabled.value = props.nextFlag ? false : true preDisabled.value = props.nextFlag ? false : true
timeValue.value = [setTime(), setTime()] setRangeValue(setTime(), setTime())
} }
if (e == 1 || e == 2) { if (e == 1 || e == 2) {
timeFlag.value = 0 timeFlag.value = 0
@@ -186,7 +209,7 @@ const preClick = () => {
startTime = year + '-01-01' startTime = year + '-01-01'
endTime = year + '-12-31' endTime = year + '-12-31'
} }
timeValue.value = [startTime, endTime] setRangeValue(startTime, endTime)
// 判断向后键的状态 // 判断向后键的状态
// var temp = NowgetEndTime() // var temp = NowgetEndTime()
@@ -415,16 +438,27 @@ const next = () => {
} }
if (!props.nextFlag) { if (!props.nextFlag) {
if ( if (
new Date(endTime + ' 00:00:00').getTime() >= new Date(toDatePart(endTime) + ' 00:00:00').getTime() >=
new Date(window.XEUtils.toDateString(new Date(), 'yyyy-MM-dd ') + ' 00:00:00').getTime() new Date(window.XEUtils.toDateString(new Date(), 'yyyy-MM-dd ') + ' 00:00:00').getTime()
) { ) {
preDisabled.value = props.nextFlag ? false : true preDisabled.value = props.nextFlag ? false : true
} }
} }
timeValue.value = [startTime, endTime] setRangeValue(startTime, endTime)
} }
watch(
() => props.isStatisticData,
() => {
if (timeValue.value?.length === 2) {
setRangeValue(timeValue.value[0], timeValue.value[1])
} else {
timeChange(interval.value)
}
}
)
const setTime = (flag = 0, e = 0) => { const setTime = (flag = 0, e = 0) => {
let dd = window.XEUtils.toDateString(new Date().getTime() - e * 3600 * 1000 * 24, 'dd') let dd = window.XEUtils.toDateString(new Date().getTime() - e * 3600 * 1000 * 24, 'dd')

View File

@@ -10,7 +10,7 @@ export default defineComponent({
required: true required: true
}, },
size: { size: {
type: String, type: [String, Number],
default: '18px' default: '18px'
}, },
color: { color: {
@@ -21,7 +21,7 @@ export default defineComponent({
setup(props) { setup(props) {
const iconStyle = computed((): CSSProperties => { const iconStyle = computed((): CSSProperties => {
const { size, color } = props const { size, color } = props
let s = `${size.replace('px', '')}px` const s = `${String(size).replace('px', '')}px`
return { return {
fontSize: s, fontSize: s,
color: color color: color

View File

@@ -10,7 +10,7 @@ import { computed, type CSSProperties } from 'vue'
import { isExternal } from '@/utils/common' import { isExternal } from '@/utils/common'
interface Props { interface Props {
name: string name: string
size: string size: string | number
color: string color: string
} }
@@ -20,7 +20,7 @@ const props = withDefaults(defineProps<Props>(), {
color: '#000000', color: '#000000',
}) })
const s = `${props.size.replace('px', '')}px` const s = `${String(props.size).replace('px', '')}px`
const iconName = computed(() => `#${props.name}`) const iconName = computed(() => `#${props.name}`)
const iconStyle = computed((): CSSProperties => { const iconStyle = computed((): CSSProperties => {
return { return {

View File

@@ -1,8 +1,22 @@
<script lang='ts'> <script lang="ts">
import { defineComponent, createVNode, reactive } from 'vue' import { defineComponent, createVNode, reactive } from 'vue'
import { Column } from 'vxe-table' import { Column } from 'vxe-table'
import { uuid } from '@/utils/random' import { uuid } from '@/utils/random'
/** storage 开启时列必须有稳定 field序号/操作等无 field 列按 title 生成 */
function resolveColumnField(attr: Record<string, any>): string | undefined {
if (attr.field) return attr.field
if (attr.prop) return attr.prop
// checkbox / radio / expand / seq 等类型列由 vxe 内部处理,可不强制 field
if (attr.type && ['checkbox', 'radio', 'expand', 'html'].includes(attr.type)) {
return undefined
}
if (attr.title != null && attr.title !== '') {
return `__col_${attr.title}`
}
return `col_${uuid()}`
}
export default defineComponent({ export default defineComponent({
name: 'Column', name: 'Column',
props: { props: {
@@ -12,9 +26,13 @@ export default defineComponent({
} }
}, },
setup(props, { slots }) { setup(props, { slots }) {
const attr = reactive(props.attr) const attr = reactive({ ...props.attr })
attr['align'] = attr['align'] ? attr['align'] : 'center' attr['align'] = attr['align'] ? attr['align'] : 'center'
attr['column-key'] = attr['column-key'] ? attr['column-key'] : attr.prop || uuid() const field = resolveColumnField(attr)
if (field) {
attr.field = field
}
attr['column-key'] = attr['column-key'] ? attr['column-key'] : attr.field || attr.prop || uuid()
return () => { return () => {
return createVNode(Column, attr, slots.default) return createVNode(Column, attr, slots.default)
} }

View File

@@ -6,14 +6,20 @@ export const defaultAttribute: VxeTableProps = {
border: true, border: true,
stripe: true, stripe: true,
size: 'small', size: 'small',
columnConfig: { resizable: true }, columnConfig: { resizable: true, useKey: true },
rowConfig: { isCurrent: true, isHover: true }, rowConfig: { isCurrent: true, isHover: true },
scrollX: { scrollToLeftOnChange: true }, scrollX: { scrollToLeftOnChange: true },
scrollY: { scrollToTopOnChange: true, enabled: true }, scrollY: { enabled: false },
treeConfig: { tooltipConfig: { maxHeight: 150 },
reserve: true // 注意:全局不要默认开启 treeConfig会与 stripe 冲突;树表在页面自行配置
customConfig: {
enabled: true,
allowFixed: true,
storage: false,
showFooter: false,
immediate: true,
mode: 'default'
}, },
showOverflow: true, showOverflow: 'tooltip',
showFooter:true showHeaderOverflow: false
} }

View File

@@ -1,20 +1,39 @@
<template> <template>
<div ref="tableHeader" class="cn-table-header"> <div ref="tableHeader" class="cn-table-header">
<div class="table-header ba-scroll-style"> <div class="table-header ba-scroll-style">
<el-form style="flex: 1; height: 32px; display: flex; flex-wrap: wrap" ref="headerForm" @submit.prevent="" <el-form
@keyup.enter="onComSearch" label-position="left" :inline="true"> style="flex: 1; height: 32px; display: flex; flex-wrap: wrap"
<el-form-item v-if="datePicker" style="grid-column: span 2; max-width: 630px"> ref="headerForm"
@submit.prevent=""
@keyup.enter="onComSearch"
label-position="left"
:inline="true"
>
<el-form-item
v-if="datePicker"
style="grid-column: span 2"
:style="{ maxWidth: isStatisticData == 1 ? '750px' : '620px' }"
>
<template #label> <template #label>
<el-checkbox v-if="showTimeAll" v-model="timeAll" label="统计时间" /> <el-checkbox v-if="showTimeAll" v-model="timeAll" label="统计时间" />
<span v-else>{{ dateLabel }}</span> <span v-else>{{ dateLabel }}</span>
</template> </template>
<DatePicker ref="datePickerRef" v-if="timeAll" :nextFlag="nextFlag" <DatePicker
:theCurrentTime="theCurrentTime"></DatePicker> ref="datePickerRef"
v-if="timeAll"
:nextFlag="nextFlag"
:theCurrentTime="theCurrentTime"
:isStatisticData="isStatisticData"
></DatePicker>
</el-form-item> </el-form-item>
<el-form-item label="区域" v-if="area"> <el-form-item label="区域" v-if="area">
<Area ref="areaRef" v-model="tableStore.table.params.deptIndex" @change-value="onAreaChange" <Area
style="width: 200px;" /> ref="areaRef"
v-model="tableStore.table.params.deptIndex"
@change-value="onAreaChange"
style="width: 200px"
/>
</el-form-item> </el-form-item>
<slot name="select"></slot> <slot name="select"></slot>
</el-form> </el-form>
@@ -23,24 +42,52 @@
<Icon size="14" name="el-icon-ArrowUp" style="color: #fff" v-if="showSelect" /> <Icon size="14" name="el-icon-ArrowUp" style="color: #fff" v-if="showSelect" />
<Icon size="14" name="el-icon-ArrowDown" style="color: #fff" v-else /> <Icon size="14" name="el-icon-ArrowDown" style="color: #fff" v-else />
</el-button> </el-button>
<el-button @click="onComSearch" v-if="showSearch" :loading="tableStore.table.loading" type="primary" <el-button
:icon="Search"> @click="onComSearch"
v-if="showSearch"
:loading="tableStore.table.loading"
type="primary"
:icon="Search"
>
查询 查询
</el-button> </el-button>
<el-button @click="onResetForm" v-if="showSearch && showReset" :loading="tableStore.table.loading" <el-button
:icon="RefreshLeft"> @click="onResetForm"
v-if="showSearch && showReset"
:loading="tableStore.table.loading"
:icon="RefreshLeft"
>
重置 重置
</el-button> </el-button>
<el-button @click="onExport" v-if="showExport" :loading="tableStore.table.exportLoading" type="primary" <el-button
icon="el-icon-Download"> @click="onExport"
v-if="showExport"
:loading="tableStore.table.exportLoading"
type="primary"
icon="el-icon-Download"
>
导出 导出
</el-button> </el-button>
</template> </template>
<slot name="operation"></slot> <slot name="operation"></slot>
<el-button
class="ml10"
v-if="showCustomColumn"
:icon="Setting"
@mousedown="onCustomColumnMousedown"
@click="openCustomColumn"
>
列设置
</el-button>
</div> </div>
<el-form :style="showSelect && showUnfoldButton ? headerFormSecondStyleOpen : headerFormSecondStyleClose" <el-form
ref="headerFormSecond" @submit.prevent="" @keyup.enter="onComSearch" label-position="left" :style="showSelect && showUnfoldButton ? headerFormSecondStyleOpen : headerFormSecondStyleClose"
:inline="true"></el-form> ref="headerFormSecond"
@submit.prevent=""
@keyup.enter="onComSearch"
label-position="left"
:inline="true"
></el-form>
</div> </div>
</template> </template>
@@ -51,8 +98,7 @@ import DatePicker from '@/components/form/datePicker/index.vue'
import Area from '@/components/form/area/index.vue' import Area from '@/components/form/area/index.vue'
import { mainHeight } from '@/utils/layout' import { mainHeight } from '@/utils/layout'
import { useDictData } from '@/stores/dictData' import { useDictData } from '@/stores/dictData'
import { Search, RefreshLeft } from '@element-plus/icons-vue' import { Search, RefreshLeft, Setting } from '@element-plus/icons-vue'
import { defineProps } from 'vue'
const emit = defineEmits(['selectChange', 'areaChange']) const emit = defineEmits(['selectChange', 'areaChange'])
const tableStore = inject('tableStore') as TableStore const tableStore = inject('tableStore') as TableStore
const tableHeader = ref() const tableHeader = ref()
@@ -72,6 +118,8 @@ interface Props {
showExport?: boolean //导出控制 showExport?: boolean //导出控制
showTimeAll?: boolean //控制时间是否显示 showTimeAll?: boolean //控制时间是否显示
dateLabel?: string //设置时间名称 dateLabel?: string //设置时间名称
isStatisticData?: number | string // 0统计数据 1分钟数据
showCustomColumn?: boolean // 列设置
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
@@ -83,7 +131,9 @@ const props = withDefaults(defineProps<Props>(), {
showReset: true, showReset: true,
showExport: false, showExport: false,
showTimeAll: false, showTimeAll: false,
dateLabel: '统计日期' dateLabel: '统计日期',
isStatisticData: 0,
showCustomColumn: false
}) })
// 动态计算table高度 // 动态计算table高度
const resizeObserver = new ResizeObserver(entries => { const resizeObserver = new ResizeObserver(entries => {
@@ -125,7 +175,31 @@ watch(
}, 500) }, 500)
} }
) )
let customColumnVisibleOnPress = false
const onCustomColumnMousedown = () => {
const table = tableStore?.table?.ref as any
if (!table) return
customColumnVisibleOnPress = table.getCustomVisible?.() ?? !!table.reactData?.customStore?.visible
if (table.reactData?.customStore) {
table.reactData.customStore.activeBtn = true
}
}
const openCustomColumn = () => {
const table = tableStore?.table?.ref as any
if (!table) return
if (table.reactData?.customStore) {
table.reactData.customStore.activeBtn = false
}
if (customColumnVisibleOnPress) {
table?.closeCustom?.()
} else {
table?.openCustom?.()
}
}
onMounted(() => { onMounted(() => {
if (props.showCustomColumn && tableStore?.table) {
tableStore.table.customColumnInHeader = true
}
timeAll.value = props.showTimeAll ? false : true timeAll.value = props.showTimeAll ? false : true
if (props.datePicker && timeAll.value) { if (props.datePicker && timeAll.value) {
tableStore.table.params.searchBeginTime = datePickerRef.value.timeValue[0] tableStore.table.params.searchBeginTime = datePickerRef.value.timeValue[0]
@@ -343,7 +417,6 @@ defineExpose({
html.dark { html.dark {
.table-search-button-group { .table-search-button-group {
button:focus, button:focus,
button:active { button:active {
background-color: var(--el-color-info-dark-2); background-color: var(--el-color-info-dark-2);

View File

@@ -1,24 +1,49 @@
<template> <template>
<div :style="{ height: typeof props.height === 'string' ? props.height : tableStore.table.height }"> <div :style="{ height: typeof props.height === 'string' ? props.height : tableStore.table.height }">
<vxe-table ref="tableRef" height="auto" :key="key" :data="tableStore.table.data" <div v-if="showCustomColumn && !tableStore?.table?.customColumnInHeader" class="table-custom-toolbar">
v-loading="tableStore.table.loading" v-bind="Object.assign({}, defaultAttribute, $attrs)" <el-button icon="el-icon-Setting" @mousedown="onCustomColumnMousedown" @click="openCustomColumn">
@checkbox-all="selectChangeEvent" @checkbox-change="selectChangeEvent" :showOverflow="showOverflow" 列设置
@sort-change="handleSortChange"> </el-button>
</div>
<vxe-table
ref="tableRef"
height="auto"
:key="key"
:data="tableStore.table.data"
v-loading="tableStore.table.loading"
v-bind="tableBindProps"
:id="getTableId()"
@checkbox-all="selectChangeEvent"
@checkbox-change="selectChangeEvent"
@cell-click="onCellClick"
>
<!-- @sort-change="handleSortChange" -->
<!-- Column 组件内部是 el-table-column --> <!-- Column 组件内部是 el-table-column -->
<template v-if="isGroup"> <template v-if="isGroup">
<GroupColumn :column="tableStore.table.column" /> <GroupColumn :column="tableStore.table.column" />
</template> </template>
<template v-else> <template v-else>
<Column :attr="item" :key="key + '-column'" v-for="(item, key) in tableStore.table.column" <Column
:tree-node="item.treeNode"> :attr="item"
:key="key + '-column'"
v-for="(item, key) in tableStore.table.column"
:tree-node="item.treeNode"
>
<!-- tableStore 预设的列 render 方案 --> <!-- tableStore 预设的列 render 方案 -->
<template v-if="item.render" #default="scope"> <template v-if="item.render" #default="scope">
<FieldRender :field="item" :row="scope.row" :column="scope.column" :index="scope.rowIndex" :key="key + <FieldRender
:field="item"
:row="scope.row"
:column="scope.column"
:index="scope.rowIndex"
:key="
key +
'-' + '-' +
item.render + item.render +
'-' + '-' +
(item.field ? '-' + item.field + '-' + scope.row[item.field] : '') (item.field ? '-' + item.field + '-' + scope.row[item.field] : '')
" /> "
/>
</template> </template>
</Column> </Column>
</template> </template>
@@ -27,17 +52,22 @@
</div> </div>
<div v-if="tableStore.showPage" class="table-pagination"> <div v-if="tableStore.showPage" class="table-pagination">
<el-pagination :currentPage="tableStore.table.params!.pageNum" :page-size="tableStore.table.params!.pageSize" <el-pagination
:page-sizes="pageSizes" background :currentPage="tableStore.table.params!.pageNum"
:page-size="tableStore.table.params!.pageSize"
:page-sizes="pageSizes"
background
:layout="config.layout.shrink ? 'prev, next, jumper' : 'sizes,total, ->, prev, pager, next, jumper'" :layout="config.layout.shrink ? 'prev, next, jumper' : 'sizes,total, ->, prev, pager, next, jumper'"
:total="tableStore.table.total" @size-change="onTableSizeChange" :total="tableStore.table.total"
@current-change="onTableCurrentChange"></el-pagination> @size-change="onTableSizeChange"
@current-change="onTableCurrentChange"
></el-pagination>
</div> </div>
<slot name="footer"></slot> <slot name="footer"></slot>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, nextTick, inject, computed, onMounted, watch } from 'vue' import { ref, nextTick, inject, computed, onMounted, watch, useAttrs } from 'vue'
import type { ElTable } from 'element-plus' import type { ElTable } from 'element-plus'
import { VxeTableEvents, VxeTableInstance } from 'vxe-table' import { VxeTableEvents, VxeTableInstance } from 'vxe-table'
import FieldRender from '@/components/table/fieldRender/index.vue' import FieldRender from '@/components/table/fieldRender/index.vue'
@@ -48,22 +78,49 @@ import type TableStoreClass from '@/utils/tableStore'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { defaultAttribute } from '@/components/table/defaultAttribute' import { defaultAttribute } from '@/components/table/defaultAttribute'
defineOptions({
inheritAttrs: false
})
const config = useConfig() const config = useConfig()
const tableRef = ref<VxeTableInstance>() const tableRef = ref<VxeTableInstance>()
const tableStore = inject('tableStore') as TableStoreClass const tableStore = inject('tableStore') as TableStoreClass
const router = useRouter() const router = useRouter()
const key = ref(0) const key = ref(0)
const emit = defineEmits<{
(e: 'cell-click', params: any): void
}>()
interface Props extends /* @vue-ignore */ Partial<InstanceType<typeof ElTable>> { interface Props extends /* @vue-ignore */ Partial<InstanceType<typeof ElTable>> {
isGroup?: boolean isGroup?: boolean
showOverflow?: boolean showOverflow?: boolean | 'ellipsis' | 'title' | 'tooltip'
height?: string | number height?: string | number
showCustomColumn?: boolean
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
isGroup: false, isGroup: false,
showOverflow: true, showOverflow: 'tooltip',
height: undefined height: undefined,
showCustomColumn: false
}) })
const attrs = useAttrs()
const tableBindProps = computed(() => {
const enableStorage = props.showCustomColumn || !!tableStore?.table?.customColumnInHeader
const baseCustomConfig = (defaultAttribute.customConfig || {}) as Record<string, any>
const attrsCustomConfig = ((attrs.customConfig as Record<string, any>) || {}) as Record<string, any>
return Object.assign({}, defaultAttribute, attrs, {
showOverflow: props.showOverflow,
showHeaderOverflow: false,
customConfig: {
...baseCustomConfig,
...attrsCustomConfig,
storage: enableStorage ? true : !!attrsCustomConfig.storage
}
})
})
const onCellClick: VxeTableEvents.CellClick<any> = params => {
emit('cell-click', params)
}
onMounted(() => { onMounted(() => {
tableStore.table.ref = tableRef.value as VxeTableInstance tableStore.table.ref = tableRef.value as VxeTableInstance
}) })
@@ -93,10 +150,33 @@ const selectChangeEvent: VxeTableEvents.CheckboxChange<any> = ({ checked }) => {
const records = (tableRef.value as VxeTableInstance).getCheckboxRecords() const records = (tableRef.value as VxeTableInstance).getCheckboxRecords()
tableStore.onTableAction('selection-change', records) tableStore.onTableAction('selection-change', records)
} }
let customColumnVisibleOnPress = false
const onCustomColumnMousedown = () => {
const table = tableRef.value as any
if (!table) return
customColumnVisibleOnPress = table.getCustomVisible?.() ?? !!table.reactData?.customStore?.visible
if (table.reactData?.customStore) {
table.reactData.customStore.activeBtn = true
}
}
const openCustomColumn = () => {
const table = tableRef.value as any
if (!table) return
if (table.reactData?.customStore) {
table.reactData.customStore.activeBtn = false
}
if (customColumnVisibleOnPress) {
table?.closeCustom?.()
} else {
table?.openCustom?.()
}
}
const getRef = () => { const getRef = () => {
return tableRef.value return tableRef.value
} }
const getTableId = () => {
return tableStore.table.filename || document.querySelectorAll('.ba-nav-tab.active')[0].textContent || ''
}
// 排序 // 排序
const handleSortChange = ({ column, order }: { column: TableColumn; order: 'asc' | 'desc' | null }) => { const handleSortChange = ({ column, order }: { column: TableColumn; order: 'asc' | 'desc' | null }) => {
// console.log('排序列:', column?.property); // console.log('排序列:', column?.property);

View File

@@ -12,36 +12,46 @@
<el-descriptions-item label="所属变电站"> <el-descriptions-item label="所属变电站">
{{ details.bdName }} {{ details.bdName }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="电压等级"> <el-descriptions-item label="监测点名称">
{{ details.scale }} {{ details.lineName }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="监测点对象名称">
{{ details.objName }}
</el-descriptions-item>
<el-descriptions-item label="监测点序号">
{{ details.id }}
</el-descriptions-item>
<el-descriptions-item label="终端厂家"> <el-descriptions-item label="终端厂家">
{{ details.manufacturer }} {{ details.manufacturer }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="终端名称"> <el-descriptions-item label="终端名称">
{{ details.devName }} {{ details.devName }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="终端型号">
{{ typeArr.find(item => item.id == details.devType)?.name }}
</el-descriptions-item>
<el-descriptions-item label="电压等级">
{{ details.scale }}
</el-descriptions-item>
<el-descriptions-item label="网络参数"> <el-descriptions-item label="网络参数">
{{ details.ip }} {{ details.ip }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="监测点名称">
{{ details.lineName }}
</el-descriptions-item>
<el-descriptions-item label="监测点序号">
{{ details.id }}
</el-descriptions-item>
<el-descriptions-item label="通讯状态"> <el-descriptions-item label="通讯状态">
{{ details.comFlag }} {{ details.comFlag }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="干扰源类型"> <el-descriptions-item label="干扰源类型">
{{ details.loadType }} {{ details.loadType }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="监测点对象名称">
{{ details.objName }}
</el-descriptions-item>
<el-descriptions-item label="接线方式"> <el-descriptions-item label="接线方式">
{{ details.ptType }} {{ details.ptType }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="统计间隔(分钟)">
{{ details.timeInterval }}
</el-descriptions-item>
<el-descriptions-item label="PT变比"> <el-descriptions-item label="PT变比">
{{ details.pt }} {{ details.pt }}
</el-descriptions-item> </el-descriptions-item>
@@ -51,13 +61,13 @@
<el-descriptions-item label="基准容量(MVA)"> <el-descriptions-item label="基准容量(MVA)">
{{ details.standardCapacity }} {{ details.standardCapacity }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="最小短路容量(MVA)">
{{ details.shortCapacity }}
</el-descriptions-item>
<el-descriptions-item label="供电设备容量(MVA)"> <el-descriptions-item label="供电设备容量(MVA)">
{{ details.devCapacity }} {{ details.devCapacity }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="用户协议容量(MVA)"> <el-descriptions-item label="最小短路容量(MW)">
{{ details.shortCapacity }}
</el-descriptions-item>
<el-descriptions-item label="用户协议容量(MW)">
{{ details.dealCapacity }} {{ details.dealCapacity }}
</el-descriptions-item> </el-descriptions-item>
</el-descriptions> </el-descriptions>
@@ -174,11 +184,14 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref, inject } from 'vue' import { ref, inject } from 'vue'
import { getLineOverLimitData, getLineDetailData } from '@/api/device-boot/line' import { getLineOverLimitData, getLineDetailData } from '@/api/device-boot/line'
import { useDictData } from '@/stores/dictData'
const dialogVisible = ref(false) const dialogVisible = ref(false)
const title = ref('') const title = ref('')
const activeName = ref('tab1') const activeName = ref('tab1')
const details: any = ref([]) const details: any = ref([])
const dictData = useDictData()
const limitValue: any = ref([]) const limitValue: any = ref([])
const typeArr = dictData.getBasicData('Dev_Type')
const open = (data: any) => { const open = (data: any) => {
details.value = [] details.value = []
limitValue.value = [] limitValue.value = []
@@ -187,7 +200,7 @@ const open = (data: any) => {
getLineDetailData(data.id).then(res => { getLineDetailData(data.id).then(res => {
details.value = res.data details.value = res.data
res.data.objName ? (title.value = res.data.objName + '_' + title.value) : ''
}) })
getLineOverLimitData(data.id).then(res => { getLineOverLimitData(data.id).then(res => {
limitValue.value = res.data limitValue.value = res.data

View File

@@ -1,5 +1,4 @@
<template> <template>
<Tree ref="treRef" :data="tree" style="height: 100%" :width="'100%'" :expanded="expanded" /> <Tree ref="treRef" :data="tree" style="height: 100%" :width="'100%'" :expanded="expanded" />
</template> </template>
<!-- <div class="mb10"> <!-- <div class="mb10">
@@ -90,35 +89,44 @@ const info = (id: any) => {
// } else { // } else {
res.data.forEach((item: any) => { res.data.forEach((item: any) => {
item.icon = 'el-icon-Menu' item.icon = 'el-icon-Menu'
item.color = 'var(--el-color-primary)'
item.plevel = item.level item.plevel = item.level
item.level = (item.level + 1) * 100 item.level = (item.level + 1) * 100
item.children.forEach((item2: any) => { item.children.forEach((item2: any) => {
item2.icon = 'el-icon-HomeFilled' item2.icon = 'el-icon-HomeFilled'
item2.color = 'var(--el-color-primary)'
item2.plevel = item2.level item2.plevel = item2.level
item2.level = (item2.level + 1) * 100 item2.level = (item2.level + 1) * 100
expanded.value.push(item2.id) expanded.value.push(item2.id)
item2.children.forEach((item3: any) => { item2.children.forEach((item3: any) => {
item3.icon = 'el-icon-CollectionTag' item3.icon = 'el-icon-CollectionTag'
item3.color = 'var(--el-color-primary)'
item3.plevel = item3.level item3.plevel = item3.level
item3.level = (item3.level + 1) * 100 item3.level = (item3.level + 1) * 100
item3.children.forEach((item4: any) => { item3.children.forEach((item4: any) => {
item4.icon = 'el-icon-Flag' item4.icon = 'el-icon-Flag'
item4.color = 'var(--el-color-primary)'
item4.plevel = item4.level item4.plevel = item4.level
item4.level = (item4.level + 1) * 100 item4.level = (item4.level + 1) * 100
item4.children.forEach((item5: any) => { item4.children.forEach((item5: any) => {
item5.icon = 'el-icon-OfficeBuilding' item5.icon = 'el-icon-OfficeBuilding'
item5.color = 'var(--el-color-primary)'
item5.plevel = item5.level item5.plevel = item5.level
item5.level = (item5.level == 7 ? 4 : item5.level + 1) * 100 item5.level = (item5.level == 7 ? 4 : item5.level + 1) * 100
item5.children.forEach((item6: any) => { item5.children.forEach((item6: any) => {
item6.icon = 'el-icon-Film' item6.icon =
item6.children?.[0]?.children?.length > 0 ? 'el-icon-Film' : 'el-icon-Share'
item6.color = 'var(--el-color-primary)'
item6.plevel = item6.level item6.plevel = item6.level
item6.level = (item6.level + 1) * 100 item6.level = (item6.level + 1) * 100
item6.children.forEach((item7: any) => { item6.children.forEach((item7: any) => {
item7.icon = 'el-icon-Share' item7.icon = item7.children.length > 0 ? 'el-icon-Share' : 'el-icon-Location'
item7.color = 'var(--el-color-primary)'
item7.plevel = item7.level item7.plevel = item7.level
item7.level = (item7.level + 1) * 100 item7.level = (item7.level + 1) * 100
item7.children.forEach((item8: any) => { item7.children.forEach((item8: any) => {
item8.icon = 'el-icon-Location' item8.icon = 'el-icon-Location'
item8.color = 'var(--el-color-primary)'
item8.plevel = item8.level item8.plevel = item8.level
item8.level = (item8.level + 1) * 100 item8.level = (item8.level + 1) * 100
}) })

View File

@@ -6,6 +6,7 @@
style="min-width: unset; padding: 10px 10px 0" style="min-width: unset; padding: 10px 10px 0"
@change="loadData" @change="loadData"
v-if="props.showSelect" v-if="props.showSelect"
filterable
> >
<el-option <el-option
v-for="item in classificationData" v-for="item in classificationData"

View File

@@ -1,28 +0,0 @@
const config: {
base_url: string
result_code: number | string
default_headers: AxiosHeaders
request_timeout: number
} = {
/**
* api请求基础路径
*/
base_url: import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL,
/**
* 接口成功返回状态码
*/
result_code: 200,
/**
* 接口请求超时时间
*/
request_timeout: 30000,
/**
* 默认接口请求类型
* 可选值application/x-www-form-urlencoded multipart/form-data
*/
default_headers: 'application/json'
}
export { config }

View File

@@ -1,6 +0,0 @@
export default {
'401': '认证失败,无法访问系统资源',
'403': '当前操作没有权限',
'404': '访问资源不存在',
default: '系统未知错误,请反馈给管理员'
}

View File

@@ -1,51 +0,0 @@
import { service } from './service'
import { config } from './config'
const { default_headers } = config
const request = (option: any) => {
const { url, method, params, data, headersType, responseType, ...config } = option
return service({
url: url,
method,
params,
data,
...config,
responseType: responseType,
headers: {
'Content-Type': headersType || default_headers
}
})
}
export default {
get: async <T = any>(option: any) => {
const res = await request({ method: 'GET', ...option })
return res.data as unknown as T
},
post: async <T = any>(option: any) => {
const res = await request({ method: 'POST', ...option })
return res.data as unknown as T
},
postOriginal: async (option: any) => {
const res = await request({ method: 'POST', ...option })
return res
},
delete: async <T = any>(option: any) => {
const res = await request({ method: 'DELETE', ...option })
return res.data as unknown as T
},
put: async <T = any>(option: any) => {
const res = await request({ method: 'PUT', ...option })
return res.data as unknown as T
},
download: async <T = any>(option: any) => {
const res = await request({ method: 'GET', responseType: 'blob', ...option })
return res as unknown as Promise<T>
},
upload: async <T = any>(option: any) => {
option.headersType = 'multipart/form-data'
const res = await request({ method: 'POST', ...option })
return res as unknown as Promise<T>
}
}

View File

@@ -1,228 +0,0 @@
import axios, {
AxiosError,
AxiosInstance,
AxiosRequestHeaders,
AxiosResponse,
InternalAxiosRequestConfig
} from 'axios'
import { ElMessage, ElMessageBox, ElNotification } from 'element-plus'
import qs from 'qs'
import { config } from '@/config/axios/config'
import { getAccessToken, getRefreshToken, getTenantId, removeToken, setToken } from '@/utils/auth'
import errorCode from './errorCode'
import { deleteUserCache } from '@/hooks/web/useCache'
const tenantEnable = import.meta.env.VITE_APP_TENANT_ENABLE
const { result_code, base_url, request_timeout } = config
// 需要忽略的提示。忽略后,自动 Promise.reject('error')
const ignoreMsgs = [
'无效的刷新令牌', // 刷新令牌被删除时,不用提示
'刷新令牌已过期' // 使用刷新令牌,刷新获取新的访问令牌时,结果因为过期失败,此时需要忽略。否则,会导致继续 401无法跳转到登出界面
]
// 是否显示重新登录
export const isRelogin = { show: false }
// Axios 无感知刷新令牌,参考 https://www.dashingdog.cn/article/11 与 https://segmentfault.com/a/1190000020210980 实现
// 请求队列
let requestList: any[] = []
// 是否正在刷新中
let isRefreshToken = false
// 请求白名单无须token的接口
const whiteList: string[] = ['/login', '/refresh-token']
// 创建axios实例
const service: AxiosInstance = axios.create({
baseURL: base_url, // api 的 base_url
timeout: request_timeout, // 请求超时时间
withCredentials: false // 禁用 Cookie 等信息
})
// request拦截器
service.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
// 是否需要设置 token
let isToken = (config!.headers || {}).isToken === false
whiteList.some((v) => {
if (config.url) {
config.url.indexOf(v) > -1
return (isToken = false)
}
})
if (getAccessToken() && !isToken) {
;(config as Recordable).headers.Authorization = 'Bearer ' + getAccessToken() // 让每个请求携带自定义token
}
// 设置租户
if (tenantEnable && tenantEnable === 'true') {
const tenantId = getTenantId()
if (tenantId) (config as Recordable).headers['tenant-id'] = tenantId
}
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)
}
// get参数编码
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) => {
// Do something with request error
console.log(error) // for debug
Promise.reject(error)
}
)
// response 拦截器
service.interceptors.response.use(
async (response: AxiosResponse<any>) => {
let { data } = response
const config = response.config
if (!data) {
// 返回“[HTTP]请求没有返回值”;
throw new Error()
}
const { t } = useI18n()
// 未设置状态码则默认成功状态
// 二进制数据则直接返回,例如说 Excel 导出
if (
response.request.responseType === 'blob' ||
response.request.responseType === 'arraybuffer'
) {
// 注意:如果导出的响应为 json说明可能失败了不直接返回进行下载
if (response.data.type !== 'application/json') {
return response.data
}
data = await new Response(response.data).json()
}
const code = data.code || result_code
// 获取错误信息
const msg = data.msg || errorCode[code] || errorCode['default']
if (ignoreMsgs.indexOf(msg) !== -1) {
// 如果是忽略的错误码,直接返回 msg 异常
return Promise.reject(msg)
} else if (code === 401) {
// 如果未认证,并且未进行刷新令牌,说明可能是访问令牌过期了
if (!isRefreshToken) {
isRefreshToken = true
// 1. 如果获取不到刷新令牌,则只能执行登出操作
if (!getRefreshToken()) {
return handleAuthorized()
}
// 2. 进行刷新访问令牌
try {
const refreshTokenRes = await refreshToken()
// 2.1 刷新成功,则回放队列的请求 + 当前请求
setToken((await refreshTokenRes).data.data)
config.headers!.Authorization = 'Bearer ' + getAccessToken()
requestList.forEach((cb: any) => {
cb()
})
requestList = []
return service(config)
} catch (e) {
// 为什么需要 catch 异常呢?刷新失败时,请求因为 Promise.reject 触发异常。
// 2.2 刷新失败,只回放队列的请求
requestList.forEach((cb: any) => {
cb()
})
// 提示是否要登出。即不回放当前请求!不然会形成递归
return handleAuthorized()
} finally {
requestList = []
isRefreshToken = false
}
} else {
// 添加到队列,等待刷新获取到新的令牌
return new Promise((resolve) => {
requestList.push(() => {
config.headers!.Authorization = 'Bearer ' + getAccessToken() // 让每个请求携带自定义token 请根据实际情况自行修改
resolve(service(config))
})
})
}
} else if (code === 500) {
ElMessage.error(t('sys.api.errMsg500'))
return Promise.reject(new Error(msg))
} else if (code === 901) {
ElMessage.error({
offset: 300,
dangerouslyUseHTMLString: true,
message:
'<div>' +
t('sys.api.errMsg901') +
'</div>' +
'<div> &nbsp; </div>' +
'<div>参考 https://doc.iocoder.cn/ 教程</div>' +
'<div> &nbsp; </div>' +
'<div>5 分钟搭建本地环境</div>'
})
return Promise.reject(new Error(msg))
} else if (code !== 200) {
if (msg === '无效的刷新令牌') {
// hard coding忽略这个提示直接登出
console.log(msg)
} else {
ElNotification.error({ title: msg })
}
return Promise.reject('error')
} else {
return data
}
},
(error: AxiosError) => {
console.log('err' + error) // for debug
let { message } = error
const { t } = useI18n()
if (message === 'Network Error') {
message = t('sys.api.errorMessage')
} else if (message.includes('timeout')) {
message = t('sys.api.apiTimeoutMessage')
} else if (message.includes('Request failed with status code')) {
message = t('sys.api.apiRequestFailed') + message.substr(message.length - 3)
}
ElMessage.error(message)
return Promise.reject(error)
}
)
const refreshToken = async () => {
axios.defaults.headers.common['tenant-id'] = getTenantId()
return await axios.post(base_url + '/system/auth/refresh-token?refreshToken=' + getRefreshToken())
}
const handleAuthorized = () => {
const { t } = useI18n()
if (!isRelogin.show) {
// 如果已经到重新登录页面则不进行弹窗提示
if (window.location.href.includes('login?redirect=')) {
return
}
isRelogin.show = true
ElMessageBox.confirm(t('sys.api.timeoutMessage'), t('common.confirmTitle'), {
showCancelButton: false,
closeOnClickModal: false,
showClose: false,
confirmButtonText: t('login.relogin'),
type: 'warning'
}).then(() => {
deleteUserCache() // 删除用户缓存
removeToken()
isRelogin.show = false
// 干掉token后再走一次路由让它过router.beforeEach的校验
window.location.href = window.location.href
})
}
return Promise.reject(t('sys.api.timeoutMessage'))
}
export { service }

View File

@@ -1,6 +1,7 @@
import { i18n } from '@/plugins/vueI18n' import { i18n } from '@/plugins/vueI18n'
import { useLocaleStoreWithOut } from '@/stores/modules/locale' import { useLocaleStoreWithOut } from '@/stores/modules/locale'
import { setHtmlPageLang } from '@/plugins/vueI18n/helper' import { setHtmlPageLang } from '@/plugins/vueI18n/helper'
import { loadLocaleMessages } from '@/locales'
const setI18nLanguage = (locale: LocaleType) => { const setI18nLanguage = (locale: LocaleType) => {
const localeStore = useLocaleStoreWithOut() const localeStore = useLocaleStoreWithOut()
@@ -22,9 +23,9 @@ export const useLocale = () => {
const changeLocale = async (locale: LocaleType) => { const changeLocale = async (locale: LocaleType) => {
const globalI18n = i18n.global const globalI18n = i18n.global
const langModule = await import(`../../locales/${locale}.ts`) const message = await loadLocaleMessages(locale)
globalI18n.setLocaleMessage(locale, langModule.default) globalI18n.setLocaleMessage(locale, message)
setI18nLanguage(locale) setI18nLanguage(locale)
} }

View File

@@ -1,78 +0,0 @@
<script lang="tsx">
import { computed, defineComponent, unref } from 'vue'
import { useAppStore } from '@/stores/modules/app'
import { Backtop } from '@/components/Backtop'
import { Setting } from '@/layouts/components/Setting'
import { useRenderLayout } from './components/useRenderLayout'
import { useDesign } from '@/hooks/web/useDesign'
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('layout')
const appStore = useAppStore()
// 是否是移动端
const mobile = computed(() => appStore.getMobile)
// 菜单折叠
const collapse = computed(() => appStore.getCollapse)
const layout = computed(() => appStore.getLayout)
const handleClickOutside = () => {
appStore.setCollapse(true)
}
const renderLayout = () => {
switch (unref(layout)) {
case 'classic':
const { renderClassic } = useRenderLayout()
return renderClassic()
case 'topLeft':
const { renderTopLeft } = useRenderLayout()
return renderTopLeft()
case 'top':
const { renderTop } = useRenderLayout()
return renderTop()
case 'cutMenu':
const { renderCutMenu } = useRenderLayout()
return renderCutMenu()
default:
break
}
}
export default defineComponent({
name: 'Layout',
setup() {
return () => (
<section class={[prefixCls, `${prefixCls}__${layout.value}`, 'w-[100%] h-[100%] relative']}>
{mobile.value && !collapse.value ? (
<div
class="absolute left-0 top-0 z-99 h-full w-full bg-[var(--el-color-black)] opacity-30"
onClick={handleClickOutside}
></div>
) : undefined}
{renderLayout()}
<Backtop></Backtop>
<Setting></Setting>
</section>
)
}
})
</script>
<style lang="scss" scoped>
$prefix-cls: v-layout;
.#{$prefix-cls} {
background-color: var(--app-content-bg-color);
:deep(.el-scrollbar__view) {
height: 100% !important;
}
}
</style>

View File

@@ -72,6 +72,7 @@
@change="onCommitState($event, 'mainAnimation')" @change="onCommitState($event, 'mainAnimation')"
:model-value="configStore.layout.mainAnimation" :model-value="configStore.layout.mainAnimation"
:placeholder="'layouts.Please select an animation name'" :placeholder="'layouts.Please select an animation name'"
filterable
> >
<el-option label="slide-right" value="slide-right"></el-option> <el-option label="slide-right" value="slide-right"></el-option>
<el-option label="slide-left" value="slide-left"></el-option> <el-option label="slide-left" value="slide-left"></el-option>

View File

@@ -11,25 +11,25 @@
:data="eventList.slice((pageNum - 1) * pageSize, pageNum * pageSize)" :data="eventList.slice((pageNum - 1) * pageSize, pageNum * pageSize)"
> >
<!-- <vxe-column type="seq" width="70px" title="序号"></vxe-column> --> <!-- <vxe-column type="seq" width="70px" title="序号"></vxe-column> -->
<vxe-column field="time" width="180px" sortable title="发生时刻"></vxe-column> <vxe-column field="time" width="180px" sortable title="暂降发生时刻"></vxe-column>
<vxe-column field="lineName" title="监测点"></vxe-column>
<vxe-column field="powerCompany" title="变电站" width="100px"></vxe-column>
<vxe-column field="powerCompany" title="供电公司" width="100px"></vxe-column>
<vxe-column field="persistTime" width="120px" sortable title="持续时间(s)">
<template #default="{ row }">
{{ Math.floor(row.persistTime * 1000) / 1000 }}
</template>
</vxe-column>
<vxe-column field="eventValue" width="160px" sortable title="暂降(骤升)幅值(%)"> <vxe-column field="eventValue" width="160px" sortable title="暂降(骤升)幅值(%)">
<template #default="{ row }"> <template #default="{ row }">
{{ Math.floor(row.eventValue * 10000) / 100 }} {{ Math.floor(row.eventValue * 10000) / 100 }}
</template> </template>
</vxe-column> </vxe-column>
<vxe-column field="persistTime" width="120px" sortable title="持续时间(s)">
<template #default="{ row }">
{{ Math.floor(row.persistTime * 1000) / 1000 }}
</template>
</vxe-column>
<vxe-column field="eventType" width="100px" title="触发类型"> <vxe-column field="eventType" width="100px" title="触发类型">
<template #default="{ row }"> <template #default="{ row }">
{{ event.filter(item => item.id == row.eventType)[0]?.name || '/' }} {{ event.filter(item => item.id == row.eventType)[0]?.name || '/' }}
</template> </template>
</vxe-column> </vxe-column>
<vxe-column field="powerCompany" title="变电站" width="100px"></vxe-column>
<vxe-column field="powerCompany" title="供电公司" width="100px"></vxe-column>
<vxe-column field="lineName" title="监测点"></vxe-column>
</vxe-table> </vxe-table>
<div class="table-pagination mt10"> <div class="table-pagination mt10">
<el-pagination <el-pagination
@@ -100,7 +100,6 @@ const dataSocket = reactive({
const socket = async () => { const socket = async () => {
const url = localStorage.getItem('WebSocketUrl3') || 'null' //'ws://192.168.2.130:10203/event/' const url = localStorage.getItem('WebSocketUrl3') || 'null' //'ws://192.168.2.130:10203/event/'
// const url = 'ws://192.168.1.68:10203/event/' // const url = 'ws://192.168.1.68:10203/event/'
await dataSocket.socketServe.connect(`${url}${adminInfo.id}`) await dataSocket.socketServe.connect(`${url}${adminInfo.id}`)

View File

@@ -8,41 +8,14 @@
name="el-icon-BellFilled" name="el-icon-BellFilled"
size="18" size="18"
/> />
<span class="nav-menu-text" v-if="globalPopUpRef?.eventList.length != 0"> <span class="nav-menu-text" v-if="globalPopUpRef?.eventList?.length > 0">
{{ (globalPopUpRef?.eventList.length>99? '99+':globalPopUpRef?.eventList.length) || 0 }} {{ globalPopUpRef.eventList.length > 99 ? '99+' : globalPopUpRef.eventList.length }}
</span> </span>
</div> </div>
</el-tooltip> </el-tooltip>
<!-- <el-tooltip effect="dark" content="截图" placement="bottom">
<div @click="savePng" class="nav-menu-item">
<Icon
:color="configStore.getColorVal('headerBarTabColor')"
class="nav-menu-icon"
name="el-icon-Camera"
size="18"
/>
</div>
</el-tooltip>
<el-tooltip effect="dark" :content="state.isFullScreen ? '缩小' : '放大'" placement="bottom">
<div @click="onFullScreen" class="nav-menu-item" :class="state.isFullScreen ? 'hover' : ''">
<Icon
:color="configStore.getColorVal('headerBarTabColor')"
class="nav-menu-icon"
v-if="state.isFullScreen"
name="fa-solid fa-compress"
size="18"
/>
<Icon
:color="configStore.getColorVal('headerBarTabColor')"
class="nav-menu-icon"
v-else
name="fa-solid fa-expand"
size="18"
/>
</div>
</el-tooltip> -->
<el-dropdown style="height: 100%" @command="handleCommand"> <el-dropdown style="height: 100%" @command="handleCommand">
<div class="admin-info" :class="state.currentNavMenu == 'adminInfo' ? 'hover' : ''"> <div class="admin-info" :class="state.currentNavMenu === 'adminInfo' ? 'hover' : ''">
<el-avatar :size="25" fit="fill"> <el-avatar :size="25" fit="fill">
<img src="@/assets/avatar.png" alt="" /> <img src="@/assets/avatar.png" alt="" />
</el-avatar> </el-avatar>
@@ -51,105 +24,117 @@
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item command="adminInfo">个人资料</el-dropdown-item> <el-dropdown-item command="adminInfo">个人资料</el-dropdown-item>
<el-dropdown-item command="changePwd">修改密码</el-dropdown-item> <el-dropdown-item command="changePwd" v-if="!IS_LNQR">修改密码</el-dropdown-item>
<el-dropdown-item command="layout">退出登录</el-dropdown-item> <el-dropdown-item command="logout">退出登录</el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown>
<!-- <div @click="configStore.setLayout('showDrawer', true)" class="nav-menu-item">
<Icon
:color="configStore.getColorVal('headerBarTabColor')"
class="nav-menu-icon"
name="fa fa-cogs"
size="18"
/>
</div> -->
<Config /> <Config />
<PopupPwd ref="popupPwd" /> <PopupPwd ref="popupPwd" />
<AdminInfo ref="popupAdminInfo" /> <AdminInfo ref="popupAdminInfo" />
<!-- <TerminalVue /> -->
<!-- 全局暂降事件 -->
<globalPopUp ref="globalPopUpRef" /> <globalPopUp ref="globalPopUpRef" />
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { reactive, ref } from 'vue' import { reactive, ref } from 'vue'
import screenfull from 'screenfull'
import { useConfig } from '@/stores/config' import { useConfig } from '@/stores/config'
import { ElMessage } from 'element-plus'
import Config from './config.vue'
import { useAdminInfo } from '@/stores/adminInfo' import { useAdminInfo } from '@/stores/adminInfo'
import router from '@/router' import { useNavTabs } from '@/stores/navTabs'
import globalPopUp from './globalPopUp.vue' import { logout } from '@/api/user-boot/user'
import { routePush } from '@/utils/router' import { Local } from '@/utils/storage'
import html2canvas from 'html2canvas' import Config from './config.vue'
import PopupPwd from './popup/password.vue' import PopupPwd from './popup/password.vue'
import AdminInfo from './popup/adminInfo.vue' import AdminInfo from './popup/adminInfo.vue'
import { useNavTabs } from '@/stores/navTabs' import globalPopUp from './globalPopUp.vue'
const { replace } = useRouter()
// 环境标识
const IS_LNQR = import.meta.env.VITE_NAME === 'LNqr'
const adminInfo = useAdminInfo() const adminInfo = useAdminInfo()
const navTabs = useNavTabs() const navTabs = useNavTabs()
const configStore = useConfig() const configStore = useConfig()
// 引用
const popupPwd = ref() const popupPwd = ref()
const popupAdminInfo = ref() const popupAdminInfo = ref()
const state = reactive({
isFullScreen: false,
currentNavMenu: '',
showLayoutDrawer: false,
showAdminInfoPopover: false
})
const globalPopUpRef = ref() const globalPopUpRef = ref()
const savePng = () => {
html2canvas(document.body, { // 状态
scale: 1, const state = reactive({
useCORS: true currentNavMenu: ''
}).then(function (canvas) {
var link = document.createElement('a')
link.href = canvas.toDataURL('image/png')
link.download = 'screenshot.png'
link.click()
}) })
// 打开暂降事件
const temporaryLandingEvent = () => {
if (globalPopUpRef.value) {
globalPopUpRef.value.open()
} }
const onFullScreen = () => {
if (!screenfull.isEnabled) {
ElMessage.warning('layouts.Full screen is not supported')
return false
}
screenfull.toggle()
screenfull.onchange(() => {
state.isFullScreen = screenfull.isFullscreen
})
} }
// 下拉菜单命令
const handleCommand = async (key: string) => { const handleCommand = async (key: string) => {
console.log(key)
switch (key) { switch (key) {
case 'adminInfo': case 'adminInfo':
popupAdminInfo.value.open() popupAdminInfo.value?.open()
break break
case 'changePwd': case 'changePwd':
popupPwd.value.open() popupPwd.value?.open()
break break
case 'layout': case 'logout':
await window.location.reload() await handleLogout()
break
}
}
// 统一登出逻辑
const handleLogout = async () => {
try {
// 1. 调用后端登出接口
await logout()
// console.log('✅ 后端登出成功')
} catch (err) {
// console.error('❌ 登出接口异常', err)
} finally {
// 2. 清理本地所有数据
clearLocalData()
// 3. LNqr 环境跳 CAS 登出
if (IS_LNQR) {
redirectToCasLogout()
} else {
// 普通环境直接跳登录页
window.location.reload()
setTimeout(() => { setTimeout(() => {
navTabs.closeTabs() replace('/login')
window.localStorage.clear() }, 1000)
}
}
}
// 清理本地数据
const clearLocalData = () => {
// 清空 localStorage保留 VXE 自定义列、DevTools 配置)
Local.clear()
// 清空用户状态
adminInfo.reset() adminInfo.reset()
router.push({ name: 'login' })
}, 0) // 清空标签页
// navTabs.closeTabs() navTabs.closeTabs()
// window.localStorage.clear()
// adminInfo.reset()
// router.push({ name: 'login' })
break
default:
break
} }
}
const temporaryLandingEvent = () => { // CAS 登出跳转
globalPopUpRef.value.open() const redirectToCasLogout = () => {
const casLogoutUrl =
'http://privilege-epri.dcloud.ln.dc.sgcc.com.cn/cas/logout?service=http://privilege-epri.dcloud.ln.dc.sgcc.com.cn/cas/login?service=http://PQMonitoring.dcloud.ln.dc.sgcc.com.cn'
window.location.href = casLogoutUrl
// 清 Cookie
document.cookie.split(';').forEach(cookie => {
const name = cookie.split('=')[0].trim()
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`
})
} }
</script> </script>
@@ -163,7 +148,6 @@ const temporaryLandingEvent = () => {
height: 60px; height: 60px;
display: flex; display: flex;
align-items: center; align-items: center;
// height: 100%;
margin-left: auto; margin-left: auto;
background-color: v-bind('configStore.getColorVal("headerBarBackground")'); background-color: v-bind('configStore.getColorVal("headerBarBackground")');
@@ -219,37 +203,15 @@ const temporaryLandingEvent = () => {
} }
} }
.dropdown-menu-box :deep(.el-dropdown-menu__item) { .nav-menu-text {
justify-content: center; position: absolute;
} top: 13px;
left: 20px;
.admin-info-base { font-size: 12px;
display: flex; background: #ff0000;
justify-content: center; color: #fff;
flex-wrap: wrap; border-radius: 5px;
padding-top: 10px; padding: 0 3px;
.admin-info-other {
display: block;
width: 100%;
text-align: center;
padding: 10px 0;
.admin-info-name {
font-size: var(--el-font-size-large);
}
}
}
.admin-info-footer {
padding: 10px 0;
margin: 0 -12px -12px -12px;
display: flex;
justify-content: space-around;
}
.pt2 {
padding-top: 2px;
} }
@keyframes twinkle { @keyframes twinkle {
@@ -263,15 +225,4 @@ const temporaryLandingEvent = () => {
transform: scale(1); transform: scale(1);
} }
} }
.nav-menu-text {
position: absolute;
top: 13px;
left: 20px;
font-size: 12px;
display: inline-block;
background-color: #ff0000;
color: #fff;
border-radius: 5px;
padding: 0 3px;
}
</style> </style>

View File

@@ -30,6 +30,7 @@ import { validatePwd } from '@/utils/common'
import { useAdminInfo } from '@/stores/adminInfo' import { useAdminInfo } from '@/stores/adminInfo'
import router from '@/router' import router from '@/router'
import { useNavTabs } from '@/stores/navTabs' import { useNavTabs } from '@/stores/navTabs'
import { Local } from '@/utils/storage'
const navTabs = useNavTabs() const navTabs = useNavTabs()
const adminInfo = useAdminInfo() const adminInfo = useAdminInfo()
const dialogVisible = ref(false) const dialogVisible = ref(false)
@@ -103,7 +104,7 @@ const submit = () => {
dialogVisible.value = false dialogVisible.value = false
setTimeout(() => { setTimeout(() => {
navTabs.closeTabs() navTabs.closeTabs()
window.localStorage.clear() Local.clear()
adminInfo.reset() adminInfo.reset()
router.push({ name: 'login' }) router.push({ name: 'login' })
}, 0) }, 0)

View File

@@ -94,9 +94,11 @@ const init = async () => {
return return
} }
} }
// 跳转到第一个菜单 // 仅在 loading 页时跳转到第一个菜单,避免覆盖用户当前页面
let firstRoute = getFirstRoute(navTabs.state.tabsViewRoutes) if (route.name === 'adminMainLoading') {
const firstRoute = getFirstRoute(navTabs.state.tabsViewRoutes)
if (firstRoute) routePush(firstRoute.path) if (firstRoute) routePush(firstRoute.path)
}
}) })
} }

View File

@@ -1,9 +1,13 @@
<template> <template>
<el-main class="layout-main" :style="mainHeight()"> <el-main class="layout-main" :style="mainHeight()">
<router-view v-slot="{ Component }"> <router-view v-slot="{ Component, route: viewRoute }">
<transition :name="config.layout.mainAnimation" mode="out-in"> <transition :name="config.layout.mainAnimation" mode="out-in">
<keep-alive :include="state.keepAliveComponentNameList"> <keep-alive :include="state.keepAliveComponentNameList">
<component :is="Component" :key="state.componentKey" /> <component
:is="Component"
v-if="Component"
:key="state.componentKey || viewRoute.fullPath"
/>
</keep-alive> </keep-alive>
</transition> </transition>
</router-view> </router-view>
@@ -32,7 +36,7 @@ const state: {
componentKey: string componentKey: string
keepAliveComponentNameList: string[] keepAliveComponentNameList: string[]
} = reactive({ } = reactive({
componentKey: route.path, componentKey: '',
keepAliveComponentNameList: [] keepAliveComponentNameList: []
}) })
@@ -51,9 +55,10 @@ onBeforeMount(() => {
state.keepAliveComponentNameList = state.keepAliveComponentNameList.filter( state.keepAliveComponentNameList = state.keepAliveComponentNameList.filter(
(name: string) => menu.meta.keepalive !== name (name: string) => menu.meta.keepalive !== name
) )
state.componentKey = '' // 临时换 key 强制卸载,再在 nextTick 恢复为路由 fullPath避免 keep-alive 缓存错乱
state.componentKey = `${menu.fullPath || menu.path}__refresh__${Date.now()}`
nextTick(() => { nextTick(() => {
state.componentKey = menu.path state.componentKey = ''
addKeepAliveComponentName(menu.meta.keepalive as string) addKeepAliveComponentName(menu.meta.keepalive as string)
}) })
}) })
@@ -79,7 +84,6 @@ onMounted(() => {
watch( watch(
() => route.path, () => route.path,
() => { () => {
state.componentKey = route.path
if (typeof navTabs.state.activeRoute?.meta.keepalive == 'string') { if (typeof navTabs.state.activeRoute?.meta.keepalive == 'string') {
addKeepAliveComponentName(navTabs.state.activeRoute?.meta.keepalive) addKeepAliveComponentName(navTabs.state.activeRoute?.meta.keepalive)
} }

View File

@@ -14,26 +14,65 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onUnmounted, reactive } from 'vue' import { onMounted, onUnmounted, reactive, watch } from 'vue'
import { useRoute } from 'vue-router'
import router from '@/router/index' import router from '@/router/index'
import { useNavTabs } from '@/stores/navTabs' import { useNavTabs } from '@/stores/navTabs'
import { isAdminApp } from '@/utils/common' import { isAdminApp } from '@/utils/common'
import { getFirstRoute, routePush } from '@/utils/router' import { getFirstRoute, routePush } from '@/utils/router'
import { isEmpty } from 'lodash-es'
import { adminBaseRoutePath } from '@/router/static'
let timer: number let timer: number
const route = useRoute()
const navTabs = useNavTabs() const navTabs = useNavTabs()
const state = reactive({ const state = reactive({
maximumWait: 1000 * 6, maximumWait: 1000 * 6,
showReload: false, showReload: false
}) })
const refresh = () => { const tryRedirect = () => {
router.go(0) if (route.params.to) {
try {
const lastRoute = JSON.parse(route.params.to as string)
if (lastRoute.path && lastRoute.path !== adminBaseRoutePath) {
const query = !isEmpty(lastRoute.query) ? lastRoute.query : {}
routePush({ path: lastRoute.path, query })
return true
} }
// if (isAdminApp() && navTabs.state.tabsViewRoutes.length > 0) { } catch (error) {
// let firstRoute = getFirstRoute(navTabs.state.tabsViewRoutes) console.error(error)
// if (firstRoute) routePush(firstRoute.path) }
// } }
if (isAdminApp() && navTabs.state.tabsViewRoutes.length > 0) {
const firstRoute = getFirstRoute(navTabs.state.tabsViewRoutes)
if (firstRoute) {
routePush(firstRoute.path)
return true
}
}
return false
}
const refresh = () => {
if (!tryRedirect()) {
router.replace(route.fullPath)
}
}
onMounted(() => {
if (tryRedirect()) return
const stop = watch(
() => navTabs.state.tabsViewRoutes.length,
len => {
if (len > 0 && tryRedirect()) {
stop()
}
}
)
})
timer = window.setTimeout(() => { timer = window.setTimeout(() => {
state.showReload = true state.showReload = true

View File

@@ -1,72 +0,0 @@
<script lang="ts" setup>
import { useTagsViewStore } from '@/stores/modules/tagsView'
import { useAppStore } from '@/stores/modules/app'
import { Footer } from '@/layouts/components/Footer'
defineOptions({ name: 'AppView' })
const appStore = useAppStore()
const layout = computed(() => appStore.getLayout)
const fixedHeader = computed(() => appStore.getFixedHeader)
const footer = computed(() => appStore.getFooter)
const tagsViewStore = useTagsViewStore()
const getCaches = computed((): string[] => {
return tagsViewStore.getCachedViews
})
const tagsView = computed(() => appStore.getTagsView)
//region 无感刷新
const routerAlive = ref(true)
// 无感刷新,防止出现页面闪烁白屏
const reload = () => {
routerAlive.value = false
nextTick(() => (routerAlive.value = true))
}
// 为组件后代提供刷新方法
provide('reload', reload)
//endregion
</script>
<template>
<section
:class="[
'p-[var(--app-content-padding)] w-[calc(100%-var(--app-content-padding)-var(--app-content-padding))] bg-[var(--app-content-bg-color)] dark:bg-[var(--el-bg-color)]',
{
'!min-h-[calc(100%-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height))]':
(fixedHeader &&
(layout === 'classic' || layout === 'topLeft' || layout === 'top') &&
footer) ||
(!tagsView && layout === 'top' && footer),
'!min-h-[calc(100%-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height)-var(--tags-view-height))]':
tagsView && layout === 'top' && footer,
'!min-h-[calc(100%-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-var(--top-tool-height)-var(--app-footer-height))]':
!fixedHeader && layout === 'classic' && footer,
'!min-h-[calc(100%-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height))]':
!fixedHeader && layout === 'topLeft' && footer,
'!min-h-[calc(100%-var(--top-tool-height)-var(--app-content-padding)-var(--app-content-padding))]':
fixedHeader && layout === 'cutMenu' && footer,
'!min-h-[calc(100%-var(--top-tool-height)-var(--app-content-padding)-var(--app-content-padding)-var(--tags-view-height))]':
!fixedHeader && layout === 'cutMenu' && footer
}
]"
>
<router-view v-if="routerAlive">
<template #default="{ Component, route }">
<keep-alive :include="getCaches">
<component :is="Component" :key="route.fullPath" />
</keep-alive>
</template>
</router-view>
</section>
<Footer v-if="footer" />
</template>

View File

@@ -1,3 +0,0 @@
import Breadcrumb from './src/Breadcrumb.vue'
export { Breadcrumb }

View File

@@ -1,130 +0,0 @@
<script lang="tsx">
import { ElBreadcrumb, ElBreadcrumbItem } from 'element-plus'
import { ref, watch, computed, unref, defineComponent, TransitionGroup } from 'vue'
import { useRouter } from 'vue-router'
import { usePermissionStore } from '@/stores/modules/permission'
import { filterBreadcrumb } from './helper'
import { filter, treeToList } from '@/utils/tree'
import type { RouteLocationNormalizedLoaded, RouteMeta } from 'vue-router'
import { Icon } from '@/components/Icon'
import { useAppStore } from '@/stores/modules/app'
import { useDesign } from '@/hooks/web/useDesign'
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('breadcrumb')
const appStore = useAppStore()
// 面包屑图标
const breadcrumbIcon = computed(() => appStore.getBreadcrumbIcon)
export default defineComponent({
name: 'Breadcrumb',
setup() {
const { currentRoute } = useRouter()
const { t } = useI18n()
const levelList = ref<AppRouteRecordRaw[]>([])
const permissionStore = usePermissionStore()
const menuRouters = computed(() => {
const routers = permissionStore.getRouters
return filterBreadcrumb(routers)
})
const getBreadcrumb = () => {
const currentPath = currentRoute.value.matched.slice(-1)[0].path
levelList.value = filter<AppRouteRecordRaw>(unref(menuRouters), (node: AppRouteRecordRaw) => {
return node.path === currentPath
})
}
const renderBreadcrumb = () => {
const breadcrumbList = treeToList<AppRouteRecordRaw[]>(unref(levelList))
return breadcrumbList.map((v) => {
const disabled = !v.redirect || v.redirect === 'noredirect'
const meta = v.meta as RouteMeta
return (
<ElBreadcrumbItem to={{ path: disabled ? '' : v.path }} key={v.name}>
{meta?.icon && breadcrumbIcon.value ? (
<div class="flex items-center">
<Icon icon={meta.icon} class="mr-[2px]" svgClass="inline-block"></Icon>
{t(v?.meta?.title)}
</div>
) : (
t(v?.meta?.title)
)}
</ElBreadcrumbItem>
)
})
}
watch(
() => currentRoute.value,
(route: RouteLocationNormalizedLoaded) => {
if (route.path.startsWith('/redirect/')) {
return
}
getBreadcrumb()
},
{
immediate: true
}
)
return () => (
<ElBreadcrumb separator="/" class={`${prefixCls} flex items-center h-full ml-[10px]`}>
<TransitionGroup appear enter-active-class="animate__animated animate__fadeInRight">
{renderBreadcrumb()}
</TransitionGroup>
</ElBreadcrumb>
)
}
})
</script>
<style lang="scss" scoped>
$prefix-cls: el-breadcrumb;
.#{$prefix-cls} {
:deep(&__item) {
display: flex;
.#{$prefix-cls}__inner {
display: flex;
align-items: center;
color: var(--top-header-text-color);
&:hover {
color: var(--el-color-primary);
}
}
}
:deep(&__item):not(:last-child) {
.#{$prefix-cls}__inner {
color: var(--top-header-text-color);
&:hover {
color: var(--el-color-primary);
}
}
}
:deep(&__item):last-child {
.#{$prefix-cls}__inner {
display: flex;
align-items: center;
color: var(--el-text-color-placeholder);
&:hover {
color: var(--el-text-color-placeholder);
}
}
}
}
</style>

View File

@@ -1,31 +0,0 @@
import { pathResolve } from '@/utils/routerHelper'
import type { RouteMeta } from 'vue-router'
export const filterBreadcrumb = (
routes: AppRouteRecordRaw[],
parentPath = ''
): AppRouteRecordRaw[] => {
const res: AppRouteRecordRaw[] = []
for (const route of routes) {
const meta = route?.meta as RouteMeta
if (meta.hidden && !meta.canTo) {
continue
}
const data: AppRouteRecordRaw =
!meta.alwaysShow && route.children?.length === 1
? { ...route.children[0], path: pathResolve(route.path, route.children[0].path) }
: { ...route }
data.path = pathResolve(parentPath, data.path)
if (data.children) {
data.children = filterBreadcrumb(data.children, data.path)
}
if (data) {
res.push(data)
}
}
return res
}

View File

@@ -1,3 +0,0 @@
import Collapse from './src/Collapse.vue'
export { Collapse }

View File

@@ -1,36 +0,0 @@
<script lang="ts" setup>
import { useAppStore } from '@/stores/modules/app'
import { propTypes } from '@/utils/propTypes'
import { useDesign } from '@/hooks/web/useDesign'
import { Expand,Fold,ZoomIn,Delete} from '@element-plus/icons-vue'
defineOptions({ name: 'Collapse' })
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('collapse')
defineProps({
color: propTypes.string.def('')
})
const appStore = useAppStore()
const collapse = computed(() => appStore.getCollapse)
const toggleCollapse = () => {
const collapsed = unref(collapse)
appStore.setCollapse(!collapsed)
}
</script>
<template>
<div :class="prefixCls" @click="toggleCollapse">
<Icon
:color="color"
:icon="collapse ? Expand: Fold"
:size="18"
class="cursor-pointer"
/>
</div>
</template>

View File

@@ -1,10 +0,0 @@
import ContextMenu from './src/ContextMenu.vue'
import { ElDropdown } from 'element-plus'
import type { RouteLocationNormalizedLoaded } from 'vue-router'
export interface ContextMenuExpose {
elDropdownMenuRef: ComponentRef<typeof ElDropdown>
tagItem: RouteLocationNormalizedLoaded
}
export { ContextMenu }

View File

@@ -1,76 +0,0 @@
<script lang="ts" setup>
import { PropType } from 'vue'
import { useDesign } from '@/hooks/web/useDesign'
import type { RouteLocationNormalizedLoaded } from 'vue-router'
import { contextMenuSchema } from '@/types/contextMenu'
import type { ElDropdown } from 'element-plus'
defineOptions({ name: 'ContextMenu' })
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('context-menu')
const { t } = useI18n()
const emit = defineEmits(['visibleChange'])
const props = defineProps({
schema: {
type: Array as PropType<contextMenuSchema[]>,
default: () => []
},
trigger: {
type: String as PropType<'click' | 'hover' | 'focus' | 'contextmenu'>,
default: 'contextmenu'
},
tagItem: {
type: Object as PropType<RouteLocationNormalizedLoaded>,
default: () => ({})
}
})
const command = (item: contextMenuSchema) => {
item.command && item.command(item)
}
const visibleChange = (visible: boolean) => {
emit('visibleChange', visible, props.tagItem)
}
const elDropdownMenuRef = ref<ComponentRef<typeof ElDropdown>>()
defineExpose({
elDropdownMenuRef,
tagItem: props.tagItem
})
</script>
<template>
<ElDropdown
ref="elDropdownMenuRef"
:class="prefixCls"
:trigger="trigger"
placement="bottom-start"
popper-class="v-context-menu-popper"
@command="command"
@visible-change="visibleChange"
>
<slot></slot>
<template #dropdown>
<ElDropdownMenu>
<ElDropdownItem
v-for="(item, index) in schema"
:key="`dropdown${index}`"
:command="item"
:disabled="item.disabled"
:divided="item.divided"
>
<Icon :icon="item.icon" />
{{ t(item.label) }}
</ElDropdownItem>
</ElDropdownMenu>
</template>
</ElDropdown>
</template>

View File

@@ -1,3 +0,0 @@
import Footer from './src/Footer.vue'
export { Footer }

View File

@@ -1,24 +0,0 @@
<script lang="ts" setup>
import { useAppStore } from '@/stores/modules/app'
import { useDesign } from '@/hooks/web/useDesign'
// eslint-disable-next-line vue/no-reserved-component-names
defineOptions({ name: 'Footer' })
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('footer')
const appStore = useAppStore()
const title = computed(() => appStore.getTitle)
</script>
<template>
<div
:class="prefixCls"
class="h-[var(--app-footer-height)] bg-[var(--app-content-bg-color)] text-center leading-[var(--app-footer-height)] text-[var(--el-text-color-placeholder)] dark:bg-[var(--el-bg-color)]"
>
<span class="text-14px">Copyright ©2022-{{ title }}</span>
</div>
</template>

View File

@@ -1,3 +0,0 @@
import LocaleDropdown from './src/LocaleDropdown.vue'
export { LocaleDropdown }

View File

@@ -1,52 +0,0 @@
<script lang="ts" setup>
import { useLocaleStore } from '@/stores/modules/locale'
import { useLocale } from '@/hooks/web/useLocale'
import { propTypes } from '@/utils/propTypes'
import { useDesign } from '@/hooks/web/useDesign'
defineOptions({ name: 'LocaleDropdown' })
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('locale-dropdown')
defineProps({
color: propTypes.string.def('')
})
const localeStore = useLocaleStore()
const langMap = computed(() => localeStore.getLocaleMap)
const currentLang = computed(() => localeStore.getCurrentLocale)
const setLang = (lang: LocaleType) => {
if (lang === unref(currentLang).lang) return
// 需要重新加载页面让整个语言多初始化
window.location.reload()
localeStore.setCurrentLocale({
lang
})
const { changeLocale } = useLocale()
changeLocale(lang)
}
</script>
<template>
<ElDropdown :class="prefixCls" trigger="click" @command="setLang">
<Icon
:class="$attrs.class"
:color="color"
:size="18"
class="cursor-pointer !p-0"
icon="ion:language-sharp"
/>
<template #dropdown>
<ElDropdownMenu>
<ElDropdownItem v-for="item in langMap" :key="item.lang" :command="item.lang">
{{ item.name }}
</ElDropdownItem>
</ElDropdownMenu>
</template>
</ElDropdown>
</template>

View File

@@ -1,3 +0,0 @@
import Logo from './src/Logo.vue'
export { Logo }

View File

@@ -1,88 +0,0 @@
<script lang="ts" setup>
import { computed, onMounted, ref, unref, watch } from 'vue'
import { useAppStore } from '@/stores/modules/app'
import { useDesign } from '@/hooks/web/useDesign'
defineOptions({ name: 'Logo' })
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('logo')
const appStore = useAppStore()
const show = ref(true)
const title = computed(() => appStore.getTitle)
const layout = computed(() => appStore.getLayout)
const collapse = computed(() => appStore.getCollapse)
onMounted(() => {
if (unref(collapse)) show.value = false
})
watch(
() => collapse.value,
(collapse: boolean) => {
if (unref(layout) === 'topLeft' || unref(layout) === 'cutMenu') {
show.value = true
return
}
if (!collapse) {
setTimeout(() => {
show.value = !collapse
}, 400)
} else {
show.value = !collapse
}
}
)
watch(
() => layout.value,
(layout) => {
if (layout === 'top' || layout === 'cutMenu') {
show.value = true
} else {
if (unref(collapse)) {
show.value = false
} else {
show.value = true
}
}
}
)
</script>
<template>
<div>
<router-link
:class="[
prefixCls,
layout !== 'classic' ? `${prefixCls}__Top` : '',
'flex !h-[var(--logo-height)] items-center cursor-pointer pl-8px relative decoration-none overflow-hidden'
]"
to="/"
>
<img
class="h-[calc(var(--logo-height)-10px)] w-[calc(var(--logo-height)-10px)]"
src="@/assets/imgs/logo.png"
/>
<div
v-if="show"
:class="[
'ml-10px text-16px font-700',
{
'text-[var(--logo-title-text-color)]': layout === 'classic',
'text-[var(--top-header-text-color)]':
layout === 'topLeft' || layout === 'top' || layout === 'cutMenu'
}
]"
>
{{ title }}
</div>
</router-link>
</div>
</template>

View File

@@ -1,3 +0,0 @@
import Menu from './src/Menu.vue'
export { Menu }

View File

@@ -1,257 +0,0 @@
<script lang="tsx">
import { PropType } from 'vue'
import { ElMenu, ElScrollbar } from 'element-plus'
import { useAppStore } from '@/stores/modules/app'
import { usePermissionStore } from '@/stores/modules/permission'
import { useRenderMenuItem } from './components/useRenderMenuItem'
import { isUrl } from '@/utils/is'
import { useDesign } from '@/hooks/web/useDesign'
import { LayoutType } from '@/types/layout'
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('menu')
export default defineComponent({
// eslint-disable-next-line vue/no-reserved-component-names
name: 'Menu',
props: {
menuSelect: {
type: Function as PropType<(index: string) => void>,
default: undefined
}
},
setup(props) {
const appStore = useAppStore()
const layout = computed(() => appStore.getLayout)
const { push, currentRoute } = useRouter()
const permissionStore = usePermissionStore()
const menuMode = computed((): 'vertical' | 'horizontal' => {
// 竖
const vertical: LayoutType[] = ['classic', 'topLeft', 'cutMenu']
if (vertical.includes(unref(layout))) {
return 'vertical'
} else {
return 'horizontal'
}
})
const routers = computed(() =>
unref(layout) === 'cutMenu' ? permissionStore.getMenuTabRouters : permissionStore.getRouters
)
const collapse = computed(() => appStore.getCollapse)
const uniqueOpened = computed(() => appStore.getUniqueOpened)
const activeMenu = computed(() => {
const { meta, path } = unref(currentRoute)
// if set path, the sidebar will highlight the path you set
if (meta.activeMenu) {
return meta.activeMenu as string
}
return path
})
const menuSelect = (index: string) => {
if (props.menuSelect) {
props.menuSelect(index)
}
// 自定义事件
if (isUrl(index)) {
window.open(index)
} else {
push(index)
}
}
const renderMenuWrap = () => {
if (unref(layout) === 'top') {
return renderMenu()
} else {
return <ElScrollbar>{renderMenu()}</ElScrollbar>
}
}
const renderMenu = () => {
return (
<ElMenu
defaultActive={unref(activeMenu)}
mode={unref(menuMode)}
collapse={
unref(layout) === 'top' || unref(layout) === 'cutMenu' ? false : unref(collapse)
}
uniqueOpened={unref(layout) === 'top' ? false : unref(uniqueOpened)}
backgroundColor="var(--left-menu-bg-color)"
textColor="var(--left-menu-text-color)"
activeTextColor="var(--left-menu-text-active-color)"
onSelect={menuSelect}
>
{{
default: () => {
const { renderMenuItem } = useRenderMenuItem(unref(menuMode))
return renderMenuItem(unref(routers))
}
}}
</ElMenu>
)
}
return () => (
<div
id={prefixCls}
class={[
`${prefixCls} ${prefixCls}__${unref(menuMode)}`,
'h-[100%] overflow-hidden flex-col bg-[var(--left-menu-bg-color)]',
{
'w-[var(--left-menu-min-width)]': unref(collapse) && unref(layout) !== 'cutMenu',
'w-[var(--left-menu-max-width)]': !unref(collapse) && unref(layout) !== 'cutMenu'
}
]}
>
{renderMenuWrap()}
</div>
)
}
})
</script>
<style lang="scss" scoped>
$prefix-cls: v-menu;
.#{$prefix-cls} {
position: relative;
transition: width var(--transition-time-02);
:deep(.el-menu) {
width: 100% !important;
border-right: none;
// 设置选中时子标题的颜色
.is-active {
& > .el-sub-menu__title {
color: var(--left-menu-text-active-color) !important;
}
}
// 设置子菜单悬停的高亮和背景色
.el-sub-menu__title,
.el-menu-item {
&:hover {
color: var(--left-menu-text-active-color) !important;
background-color: var(--left-menu-bg-color) !important;
}
}
// 设置选中时的高亮背景和高亮颜色
.el-menu-item.is-active {
color: var(--left-menu-text-active-color) !important;
background-color: var(--left-menu-bg-active-color) !important;
&:hover {
background-color: var(--left-menu-bg-active-color) !important;
}
}
.el-menu-item.is-active {
position: relative;
}
// 设置子菜单的背景颜色
.el-menu {
.el-sub-menu__title,
.el-menu-item:not(.is-active) {
background-color: var(--left-menu-bg-light-color) !important;
}
}
}
// 折叠时的最小宽度
:deep(.el-menu--collapse) {
width: var(--left-menu-min-width);
& > .is-active,
& > .is-active > .el-sub-menu__title {
position: relative;
background-color: var(--left-menu-collapse-bg-active-color) !important;
}
}
// 折叠动画的时候,就需要把文字给隐藏掉
:deep(.horizontal-collapse-transition) {
// transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out !important;
.#{$prefix-cls}__title {
display: none;
}
}
// 水平菜单
&__horizontal {
height: calc(var(--top-tool-height)) !important;
:deep(.el-menu--horizontal) {
height: calc(var(--top-tool-height));
border-bottom: none;
// 重新设置底部高亮颜色
& > .el-sub-menu.is-active {
.el-sub-menu__title {
border-bottom-color: var(--el-color-primary) !important;
}
}
.el-menu-item.is-active {
position: relative;
&::after {
display: none !important;
}
}
.#{$prefix-cls}__title {
/* stylelint-disable-next-line */
max-height: calc(var(--top-tool-height) - 2px) !important;
/* stylelint-disable-next-line */
line-height: calc(var(--top-tool-height) - 2px);
}
}
}
}
</style>
<style lang="scss">
$prefix-cls: v-menu-popper;
.#{$prefix-cls}--vertical,
.#{$prefix-cls}--horizontal {
// 设置选中时子标题的颜色
.is-active {
& > .el-sub-menu__title {
color: var(--left-menu-text-active-color) !important;
}
}
// 设置子菜单悬停的高亮和背景色
.el-sub-menu__title,
.el-menu-item {
&:hover {
color: var(--left-menu-text-active-color) !important;
background-color: var(--left-menu-bg-color) !important;
}
}
// 设置选中时的高亮背景
.el-menu-item.is-active {
position: relative;
background-color: var(--left-menu-bg-active-color) !important;
&:hover {
background-color: var(--left-menu-bg-active-color) !important;
}
}
}
</style>

View File

@@ -1,50 +0,0 @@
import { ElSubMenu, ElMenuItem } from 'element-plus'
import { hasOneShowingChild } from '../helper'
import { isUrl } from '@/utils/is'
import { useRenderMenuTitle } from './useRenderMenuTitle'
import { pathResolve } from '@/utils/routerHelper'
const { renderMenuTitle } = useRenderMenuTitle()
export const useRenderMenuItem = () =>
// allRouters: AppRouteRecordRaw[] = [],
{
const renderMenuItem = (routers: AppRouteRecordRaw[], parentPath = '/') => {
return routers
.filter((v) => !v.meta?.hidden)
.map((v) => {
const meta = v.meta ?? {}
const { oneShowingChild, onlyOneChild } = hasOneShowingChild(v.children, v)
const fullPath = isUrl(v.path) ? v.path : pathResolve(parentPath, v.path) // getAllParentPath<AppRouteRecordRaw>(allRouters, v.path).join('/')
if (
oneShowingChild &&
(!onlyOneChild?.children || onlyOneChild?.noShowingChildren) &&
!meta?.alwaysShow
) {
return (
<ElMenuItem
index={onlyOneChild ? pathResolve(fullPath, onlyOneChild.path) : fullPath}
>
{{
default: () => renderMenuTitle(onlyOneChild ? onlyOneChild?.meta : meta)
}}
</ElMenuItem>
)
} else {
return (
<ElSubMenu index={fullPath}>
{{
title: () => renderMenuTitle(meta),
default: () => renderMenuItem(v.children!, fullPath)
}}
</ElSubMenu>
)
}
})
}
return {
renderMenuItem
}
}

View File

@@ -1,27 +0,0 @@
import type { RouteMeta } from 'vue-router'
import { Icon } from '@/components/Icon'
import { useI18n } from '@/hooks/web/useI18n'
export const useRenderMenuTitle = () => {
const renderMenuTitle = (meta: RouteMeta) => {
const { t } = useI18n()
const { title = 'Please set title', icon } = meta
return icon ? (
<>
<Icon icon={meta.icon}></Icon>
<span class="v-menu__title overflow-hidden overflow-ellipsis whitespace-nowrap">
{t(title as string)}
</span>
</>
) : (
<span class="v-menu__title overflow-hidden overflow-ellipsis whitespace-nowrap">
{t(title as string)}
</span>
)
}
return {
renderMenuTitle
}
}

View File

@@ -1,54 +0,0 @@
import type { RouteMeta } from 'vue-router'
import { findPath } from '@/utils/tree'
type OnlyOneChildType = AppRouteRecordRaw & { noShowingChildren?: boolean }
interface HasOneShowingChild {
oneShowingChild?: boolean
onlyOneChild?: OnlyOneChildType
}
export const getAllParentPath = <T = Recordable>(treeData: T[], path: string) => {
const menuList = findPath(treeData, (n) => n.path === path) as AppRouteRecordRaw[]
return (menuList || []).map((item) => item.path)
}
export const hasOneShowingChild = (
children: AppRouteRecordRaw[] = [],
parent: AppRouteRecordRaw
): HasOneShowingChild => {
const onlyOneChild = ref<OnlyOneChildType>()
const showingChildren = children.filter((v) => {
const meta = (v.meta ?? {}) as RouteMeta
if (meta.hidden) {
return false
} else {
// Temp set(will be used if only has one showing child)
onlyOneChild.value = v
return true
}
})
// When there is only one child router, the child router is displayed by default
if (showingChildren.length === 1) {
return {
oneShowingChild: true,
onlyOneChild: unref(onlyOneChild)
}
}
// Show parent if there are no child router to display
if (!showingChildren.length) {
onlyOneChild.value = { ...parent, path: '', noShowingChildren: true }
return {
oneShowingChild: true,
onlyOneChild: unref(onlyOneChild)
}
}
return {
oneShowingChild: false,
onlyOneChild: unref(onlyOneChild)
}
}

View File

@@ -1,3 +0,0 @@
import Screenfull from './src/Screenfull.vue'
export { Screenfull }

View File

@@ -1,32 +0,0 @@
<script lang="ts" setup>
import { Icon } from '@/components/Icon'
import { useFullscreen } from '@vueuse/core'
import { propTypes } from '@/utils/propTypes'
import { useDesign } from '@/hooks/web/useDesign'
defineOptions({ name: 'ScreenFull' })
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('screenfull')
defineProps({
color: propTypes.string.def('')
})
const { toggle, isFullscreen } = useFullscreen()
const toggleFullscreen = () => {
toggle()
}
</script>
<template>
<div :class="prefixCls" @click="toggleFullscreen">
<Icon
:color="color"
:icon="isFullscreen ? 'zmdi:fullscreen-exit' : 'zmdi:fullscreen'"
:size="18"
/>
</div>
</template>

View File

@@ -1,3 +0,0 @@
import Setting from './src/Setting.vue'
export { Setting }

View File

@@ -1,299 +0,0 @@
<script lang="ts" setup>
import { ElMessage } from 'element-plus'
import { useClipboard, useCssVar } from '@vueuse/core'
import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
import { useDesign } from '@/hooks/web/useDesign'
import { setCssVar, trim } from '@/utils'
import { colorIsDark, hexToRGB, lighten } from '@/utils/color'
import { useAppStore } from '@/stores/modules/app'
import { ThemeSwitch } from '@/layouts/components/ThemeSwitch'
import ColorRadioPicker from './components/ColorRadioPicker.vue'
import InterfaceDisplay from './components/InterfaceDisplay.vue'
import LayoutRadioPicker from './components/LayoutRadioPicker.vue'
defineOptions({ name: 'Setting' })
const { t } = useI18n()
const appStore = useAppStore()
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('setting')
const layout = computed(() => appStore.getLayout)
const drawer = ref(false)
// 主题色相关
const systemTheme = ref(appStore.getTheme.elColorPrimary)
const setSystemTheme = (color: string) => {
setCssVar('--el-color-primary', color)
appStore.setTheme({ elColorPrimary: color })
const leftMenuBgColor = useCssVar('--left-menu-bg-color', document.documentElement)
setMenuTheme(trim(unref(leftMenuBgColor)))
}
// 头部主题相关
const headerTheme = ref(appStore.getTheme.topHeaderBgColor || '')
const setHeaderTheme = (color: string) => {
const isDarkColor = colorIsDark(color)
const textColor = isDarkColor ? '#fff' : 'inherit'
const textHoverColor = isDarkColor ? lighten(color!, 6) : '#f6f6f6'
const topToolBorderColor = isDarkColor ? color : '#eee'
setCssVar('--top-header-bg-color', color)
setCssVar('--top-header-text-color', textColor)
setCssVar('--top-header-hover-color', textHoverColor)
appStore.setTheme({
topHeaderBgColor: color,
topHeaderTextColor: textColor,
topHeaderHoverColor: textHoverColor,
topToolBorderColor
})
if (unref(layout) === 'top') {
setMenuTheme(color)
}
}
// 菜单主题相关
const menuTheme = ref(appStore.getTheme.leftMenuBgColor || '')
const setMenuTheme = (color: string) => {
const primaryColor = useCssVar('--el-color-primary', document.documentElement)
const isDarkColor = colorIsDark(color)
const theme: Recordable = {
// 左侧菜单边框颜色
leftMenuBorderColor: isDarkColor ? 'inherit' : '#eee',
// 左侧菜单背景颜色
leftMenuBgColor: color,
// 左侧菜单浅色背景颜色
leftMenuBgLightColor: isDarkColor ? lighten(color!, 6) : color,
// 左侧菜单选中背景颜色
leftMenuBgActiveColor: isDarkColor
? 'var(--el-color-primary)'
: hexToRGB(unref(primaryColor), 0.1),
// 左侧菜单收起选中背景颜色
leftMenuCollapseBgActiveColor: isDarkColor
? 'var(--el-color-primary)'
: hexToRGB(unref(primaryColor), 0.1),
// 左侧菜单字体颜色
leftMenuTextColor: isDarkColor ? '#bfcbd9' : '#333',
// 左侧菜单选中字体颜色
leftMenuTextActiveColor: isDarkColor ? '#fff' : 'var(--el-color-primary)',
// logo字体颜色
logoTitleTextColor: isDarkColor ? '#fff' : 'inherit',
// logo边框颜色
logoBorderColor: isDarkColor ? color : '#eee'
}
appStore.setTheme(theme)
appStore.setCssVarTheme()
}
if (layout.value === 'top' && !appStore.getIsDark) {
headerTheme.value = '#fff'
setHeaderTheme('#fff')
}
// 监听layout变化重置一些主题色
watch(
() => layout.value,
(n) => {
if (n === 'top' && !appStore.getIsDark) {
headerTheme.value = '#fff'
setHeaderTheme('#fff')
} else {
setMenuTheme(unref(menuTheme))
}
}
)
// 拷贝
const copyConfig = async () => {
const { copy, copied, isSupported } = useClipboard({
source: `
// 面包屑
breadcrumb: ${appStore.getBreadcrumb},
// 面包屑图标
breadcrumbIcon: ${appStore.getBreadcrumbIcon},
// 折叠图标
hamburger: ${appStore.getHamburger},
// 全屏图标
screenfull: ${appStore.getScreenfull},
// 尺寸图标
size: ${appStore.getSize},
// 多语言图标
locale: ${appStore.getLocale},
// 消息图标
message: ${appStore.getMessage},
// 标签页
tagsView: ${appStore.getTagsView},
// 标签页图标
getTagsViewIcon: ${appStore.getTagsViewIcon},
// logo
logo: ${appStore.getLogo},
// 菜单手风琴
uniqueOpened: ${appStore.getUniqueOpened},
// 固定header
fixedHeader: ${appStore.getFixedHeader},
// 页脚
footer: ${appStore.getFooter},
// 灰色模式
greyMode: ${appStore.getGreyMode},
// layout布局
layout: '${appStore.getLayout}',
// 暗黑模式
isDark: ${appStore.getIsDark},
// 组件尺寸
currentSize: '${appStore.getCurrentSize}',
// 主题相关
theme: {
// 主题色
elColorPrimary: '${appStore.getTheme.elColorPrimary}',
// 左侧菜单边框颜色
leftMenuBorderColor: '${appStore.getTheme.leftMenuBorderColor}',
// 左侧菜单背景颜色
leftMenuBgColor: '${appStore.getTheme.leftMenuBgColor}',
// 左侧菜单浅色背景颜色
leftMenuBgLightColor: '${appStore.getTheme.leftMenuBgLightColor}',
// 左侧菜单选中背景颜色
leftMenuBgActiveColor: '${appStore.getTheme.leftMenuBgActiveColor}',
// 左侧菜单收起选中背景颜色
leftMenuCollapseBgActiveColor: '${appStore.getTheme.leftMenuCollapseBgActiveColor}',
// 左侧菜单字体颜色
leftMenuTextColor: '${appStore.getTheme.leftMenuTextColor}',
// 左侧菜单选中字体颜色
leftMenuTextActiveColor: '${appStore.getTheme.leftMenuTextActiveColor}',
// logo字体颜色
logoTitleTextColor: '${appStore.getTheme.logoTitleTextColor}',
// logo边框颜色
logoBorderColor: '${appStore.getTheme.logoBorderColor}',
// 头部背景颜色
topHeaderBgColor: '${appStore.getTheme.topHeaderBgColor}',
// 头部字体颜色
topHeaderTextColor: '${appStore.getTheme.topHeaderTextColor}',
// 头部悬停颜色
topHeaderHoverColor: '${appStore.getTheme.topHeaderHoverColor}',
// 头部边框颜色
topToolBorderColor: '${appStore.getTheme.topToolBorderColor}'
}
`
})
if (!isSupported) {
ElMessage.error(t('setting.copyFailed'))
} else {
await copy()
if (unref(copied)) {
ElMessage.success(t('setting.copySuccess'))
}
}
}
// 清空缓存
const clear = () => {
const { wsCache } = useCache()
wsCache.delete(CACHE_KEY.LAYOUT)
wsCache.delete(CACHE_KEY.THEME)
wsCache.delete(CACHE_KEY.IS_DARK)
window.location.reload()
}
</script>
<template>
<div
:class="prefixCls"
class="fixed right-0 top-[45%] h-40px w-40px cursor-pointer bg-[var(--el-color-primary)] text-center leading-40px"
@click="drawer = true"
>
<Icon color="#fff" icon="ep:setting" />
</div>
<ElDrawer v-model="drawer" :z-index="4000" direction="rtl" size="350px">
<template #header>
<span class="text-16px font-700">{{ t('setting.projectSetting') }}</span>
</template>
<div class="text-center">
<!-- 主题 -->
<ElDivider>{{ t('setting.theme') }}</ElDivider>
<ThemeSwitch />
<!-- 布局 -->
<ElDivider>{{ t('setting.layout') }}</ElDivider>
<LayoutRadioPicker />
<!-- 系统主题 -->
<ElDivider>{{ t('setting.systemTheme') }}</ElDivider>
<ColorRadioPicker
v-model="systemTheme"
:schema="[
'#409eff',
'#009688',
'#536dfe',
'#ff5c93',
'#ee4f12',
'#0096c7',
'#9c27b0',
'#ff9800'
]"
@change="setSystemTheme"
/>
<!-- 头部主题 -->
<ElDivider>{{ t('setting.headerTheme') }}</ElDivider>
<ColorRadioPicker
v-model="headerTheme"
:schema="[
'#fff',
'#151515',
'#5172dc',
'#e74c3c',
'#24292e',
'#394664',
'#009688',
'#383f45'
]"
@change="setHeaderTheme"
/>
<!-- 菜单主题 -->
<template v-if="layout !== 'top'">
<ElDivider>{{ t('setting.menuTheme') }}</ElDivider>
<ColorRadioPicker
v-model="menuTheme"
:schema="[
'#fff',
'#001529',
'#212121',
'#273352',
'#191b24',
'#383f45',
'#001628',
'#344058'
]"
@change="setMenuTheme"
/>
</template>
</div>
<!-- 界面显示 -->
<ElDivider>{{ t('setting.interfaceDisplay') }}</ElDivider>
<InterfaceDisplay />
<ElDivider />
<div>
<ElButton class="w-full" type="primary" @click="copyConfig">{{ t('setting.copy') }}</ElButton>
</div>
<div class="mt-5px">
<ElButton class="w-full" type="danger" @click="clear">
{{ t('setting.clearAndReset') }}
</ElButton>
</div>
</ElDrawer>
</template>
<style lang="scss" scoped>
$prefix-cls: v-setting;
.#{$prefix-cls} {
border-radius: 6px 0 0 6px;
}
</style>

View File

@@ -1,67 +0,0 @@
<script lang="ts" setup>
import { PropType } from 'vue'
import { propTypes } from '@/utils/propTypes'
import { useDesign } from '@/hooks/web/useDesign'
defineOptions({ name: 'ColorRadioPicker' })
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('color-radio-picker')
const props = defineProps({
schema: {
type: Array as PropType<string[]>,
default: () => []
},
modelValue: propTypes.string.def('')
})
const emit = defineEmits(['update:modelValue', 'change'])
const colorVal = ref(props.modelValue)
watch(
() => props.modelValue,
(val: string) => {
if (val === unref(colorVal)) return
colorVal.value = val
}
)
// 监听
watch(
() => colorVal.value,
(val: string) => {
emit('update:modelValue', val)
emit('change', val)
}
)
</script>
<template>
<div :class="prefixCls" class="flex flex-wrap space-x-14px">
<span
v-for="(item, i) in schema"
:key="`radio-${i}`"
:class="{ 'is-active': colorVal === item }"
:style="{
background: item
}"
class="mb-5px h-20px w-20px cursor-pointer border-2px border-gray-300 rounded-2px border-solid text-center leading-20px"
@click="colorVal = item"
>
<Icon v-if="colorVal === item" :size="16" color="#fff" icon="ep:check" />
</span>
</div>
</template>
<style lang="scss" scoped>
$prefix-cls: v-color-radio-picker;
.#{$prefix-cls} {
.is-active {
border-color: var(--el-color-primary);
}
}
</style>

View File

@@ -1,224 +0,0 @@
<script lang="ts" setup>
import { setCssVar } from '@/utils'
import { useDesign } from '@/hooks/web/useDesign'
import { useWatermark } from '@/hooks/web/useWatermark'
import { useAppStore } from '@/stores/modules/app'
defineOptions({ name: 'InterfaceDisplay' })
const { t } = useI18n()
const { getPrefixCls } = useDesign()
const { setWatermark } = useWatermark()
const prefixCls = getPrefixCls('interface-display')
const appStore = useAppStore()
const water = ref()
// 面包屑
const breadcrumb = ref(appStore.getBreadcrumb)
const breadcrumbChange = (show: boolean) => {
appStore.setBreadcrumb(show)
}
// 面包屑图标
const breadcrumbIcon = ref(appStore.getBreadcrumbIcon)
const breadcrumbIconChange = (show: boolean) => {
appStore.setBreadcrumbIcon(show)
}
// 折叠图标
const hamburger = ref(appStore.getHamburger)
const hamburgerChange = (show: boolean) => {
appStore.setHamburger(show)
}
// 全屏图标
const screenfull = ref(appStore.getScreenfull)
const screenfullChange = (show: boolean) => {
appStore.setScreenfull(show)
}
// 尺寸图标
const size = ref(appStore.getSize)
const sizeChange = (show: boolean) => {
appStore.setSize(show)
}
// 多语言图标
const locale = ref(appStore.getLocale)
const localeChange = (show: boolean) => {
appStore.setLocale(show)
}
// 消息图标
const message = ref(appStore.getMessage)
const messageChange = (show: boolean) => {
appStore.setMessage(show)
}
// 标签页
const tagsView = ref(appStore.getTagsView)
const tagsViewChange = (show: boolean) => {
// 切换标签栏显示时,同步切换标签栏的高度
setCssVar('--tags-view-height', show ? '35px' : '0px')
appStore.setTagsView(show)
}
// 标签页图标
const tagsViewIcon = ref(appStore.getTagsViewIcon)
const tagsViewIconChange = (show: boolean) => {
appStore.setTagsViewIcon(show)
}
// logo
const logo = ref(appStore.getLogo)
const logoChange = (show: boolean) => {
appStore.setLogo(show)
}
// 菜单手风琴
const uniqueOpened = ref(appStore.getUniqueOpened)
const uniqueOpenedChange = (uniqueOpened: boolean) => {
appStore.setUniqueOpened(uniqueOpened)
}
// 固定头部
const fixedHeader = ref(appStore.getFixedHeader)
const fixedHeaderChange = (show: boolean) => {
appStore.setFixedHeader(show)
}
// 页脚
const footer = ref(appStore.getFooter)
const footerChange = (show: boolean) => {
appStore.setFooter(show)
}
// 灰色模式
const greyMode = ref(appStore.getGreyMode)
const greyModeChange = (show: boolean) => {
appStore.setGreyMode(show)
}
// 固定菜单
const fixedMenu = ref(appStore.getFixedMenu)
const fixedMenuChange = (show: boolean) => {
appStore.setFixedMenu(show)
}
// 设置水印
const setWater = () => {
setWatermark(water.value)
}
const layout = computed(() => appStore.getLayout)
watch(
() => layout.value,
(n) => {
if (n === 'top') {
appStore.setCollapse(false)
}
}
)
</script>
<template>
<div :class="prefixCls">
<div class="flex items-center justify-between">
<span class="text-14px">{{ t('setting.breadcrumb') }}</span>
<ElSwitch v-model="breadcrumb" @change="breadcrumbChange" />
</div>
<div class="flex items-center justify-between">
<span class="text-14px">{{ t('setting.breadcrumbIcon') }}</span>
<ElSwitch v-model="breadcrumbIcon" @change="breadcrumbIconChange" />
</div>
<div class="flex items-center justify-between">
<span class="text-14px">{{ t('setting.hamburgerIcon') }}</span>
<ElSwitch v-model="hamburger" @change="hamburgerChange" />
</div>
<div class="flex items-center justify-between">
<span class="text-14px">{{ t('setting.screenfullIcon') }}</span>
<ElSwitch v-model="screenfull" @change="screenfullChange" />
</div>
<div class="flex items-center justify-between">
<span class="text-14px">{{ t('setting.sizeIcon') }}</span>
<ElSwitch v-model="size" @change="sizeChange" />
</div>
<div class="flex items-center justify-between">
<span class="text-14px">{{ t('setting.localeIcon') }}</span>
<ElSwitch v-model="locale" @change="localeChange" />
</div>
<div class="flex items-center justify-between">
<span class="text-14px">{{ t('setting.messageIcon') }}</span>
<ElSwitch v-model="message" @change="messageChange" />
</div>
<div class="flex items-center justify-between">
<span class="text-14px">{{ t('setting.tagsView') }}</span>
<ElSwitch v-model="tagsView" @change="tagsViewChange" />
</div>
<div class="flex items-center justify-between">
<span class="text-14px">{{ t('setting.tagsViewIcon') }}</span>
<ElSwitch v-model="tagsViewIcon" @change="tagsViewIconChange" />
</div>
<div class="flex items-center justify-between">
<span class="text-14px">{{ t('setting.logo') }}</span>
<ElSwitch v-model="logo" @change="logoChange" />
</div>
<div class="flex items-center justify-between">
<span class="text-14px">{{ t('setting.uniqueOpened') }}</span>
<ElSwitch v-model="uniqueOpened" @change="uniqueOpenedChange" />
</div>
<div class="flex items-center justify-between">
<span class="text-14px">{{ t('setting.fixedHeader') }}</span>
<ElSwitch v-model="fixedHeader" @change="fixedHeaderChange" />
</div>
<div class="flex items-center justify-between">
<span class="text-14px">{{ t('setting.footer') }}</span>
<ElSwitch v-model="footer" @change="footerChange" />
</div>
<div class="flex items-center justify-between">
<span class="text-14px">{{ t('setting.greyMode') }}</span>
<ElSwitch v-model="greyMode" @change="greyModeChange" />
</div>
<div class="flex items-center justify-between">
<span class="text-14px">{{ t('setting.fixedMenu') }}</span>
<ElSwitch v-model="fixedMenu" @change="fixedMenuChange" />
</div>
<div class="flex items-center justify-between">
<span class="text-14px">{{ t('watermark.watermark') }}</span>
<ElInput v-model="water" class="right-1 w-20" @change="setWater()" />
</div>
</div>
</template>

View File

@@ -1,172 +0,0 @@
<script lang="ts" setup>
import { useAppStore } from '@/stores/modules/app'
import { useDesign } from '@/hooks/web/useDesign'
defineOptions({ name: 'LayoutRadioPicker' })
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('layout-radio-picker')
const appStore = useAppStore()
const layout = computed(() => appStore.getLayout)
</script>
<template>
<div :class="prefixCls" class="flex flex-wrap space-x-14px">
<div
:class="[
`${prefixCls}__classic`,
'relative w-56px h-48px cursor-pointer bg-gray-300',
{
'is-acitve': layout === 'classic'
}
]"
@click="appStore.setLayout('classic')"
></div>
<div
:class="[
`${prefixCls}__top-left`,
'relative w-56px h-48px cursor-pointer bg-gray-300',
{
'is-acitve': layout === 'topLeft'
}
]"
@click="appStore.setLayout('topLeft')"
></div>
<div
:class="[
`${prefixCls}__top`,
'relative w-56px h-48px cursor-pointer bg-gray-300',
{
'is-acitve': layout === 'top'
}
]"
@click="appStore.setLayout('top')"
></div>
<div
:class="[
`${prefixCls}__cut-menu`,
'relative w-56px h-48px cursor-pointer bg-gray-300',
{
'is-acitve': layout === 'cutMenu'
}
]"
@click="appStore.setLayout('cutMenu')"
>
<div class="absolute left-[10%] top-0 h-full w-[33%] bg-gray-200"></div>
</div>
</div>
</template>
<style lang="scss" scoped>
$prefix-cls: v-layout-radio-picker;
.#{$prefix-cls} {
&__classic {
border: 2px solid #e5e7eb;
border-radius: 4px;
&::before {
position: absolute;
top: 0;
left: 0;
z-index: 1;
width: 33%;
height: 100%;
background-color: #273352;
border-radius: 4px 0 0 4px;
content: '';
}
&::after {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 25%;
background-color: #fff;
border-radius: 4px 4px 0;
content: '';
}
}
&__top-left {
border: 2px solid #e5e7eb;
border-radius: 4px;
&::before {
position: absolute;
top: 0;
left: 0;
z-index: 1;
width: 100%;
height: 33%;
background-color: #273352;
border-radius: 4px 4px 0 0;
content: '';
}
&::after {
position: absolute;
top: 0;
left: 0;
width: 33%;
height: 100%;
background-color: #fff;
border-radius: 4px 0 0 4px;
content: '';
}
}
&__top {
border: 2px solid #e5e7eb;
border-radius: 4px;
&::before {
position: absolute;
top: 0;
left: 0;
z-index: 1;
width: 100%;
height: 33%;
background-color: #273352;
border-radius: 4px 4px 0 0;
content: '';
}
}
&__cut-menu {
border: 2px solid #e5e7eb;
border-radius: 4px;
&::before {
position: absolute;
top: 0;
left: 0;
z-index: 1;
width: 100%;
height: 33%;
background-color: #273352;
border-radius: 4px 4px 0 0;
content: '';
}
&::after {
position: absolute;
top: 0;
left: 0;
width: 10%;
height: 100%;
background-color: #fff;
border-radius: 4px 0 0 4px;
content: '';
}
}
.is-acitve {
border-color: var(--el-color-primary);
}
}
</style>

View File

@@ -1,3 +0,0 @@
import SizeDropdown from './src/SizeDropdown.vue'
export { SizeDropdown }

View File

@@ -1,40 +0,0 @@
<script lang="ts" setup>
import { useAppStore } from '@/stores/modules/app'
import { propTypes } from '@/utils/propTypes'
import { useDesign } from '@/hooks/web/useDesign'
import { ElementPlusSize } from '@/types/elementPlus'
defineOptions({ name: 'SizeDropdown' })
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('size-dropdown')
defineProps({
color: propTypes.string.def('')
})
const { t } = useI18n()
const appStore = useAppStore()
const sizeMap = computed(() => appStore.sizeMap)
const setCurrentSize = (size: ElementPlusSize) => {
appStore.setCurrentSize(size)
}
</script>
<template>
<ElDropdown :class="prefixCls" trigger="click" @command="setCurrentSize">
<Icon :color="color" :size="18" class="cursor-pointer" icon="mdi:format-size" />
<template #dropdown>
<ElDropdownMenu>
<ElDropdownItem v-for="item in sizeMap" :key="item" :command="item">
{{ t(`size.${item}`) }}
</ElDropdownItem>
</ElDropdownMenu>
</template>
</ElDropdown>
</template>

View File

@@ -1,3 +0,0 @@
import TabMenu from './src/TabMenu.vue'
export { TabMenu }

View File

@@ -1,237 +0,0 @@
<script lang="tsx">
import { usePermissionStore } from '@/stores/modules/permission'
import { useAppStore } from '@/stores/modules/app'
import { ElScrollbar } from 'element-plus'
import { Menu } from '@/layouts/components/Menu'
import { pathResolve } from '@/utils/routerHelper'
import { cloneDeep } from 'lodash-es'
import { filterMenusPath, initTabMap, tabPathMap } from './helper'
import { useDesign } from '@/hooks/web/useDesign'
import { isUrl } from '@/utils/is'
const { getPrefixCls, variables } = useDesign()
const prefixCls = getPrefixCls('tab-menu')
export default defineComponent({
name: 'TabMenu',
setup() {
const { push, currentRoute } = useRouter()
const { t } = useI18n()
const appStore = useAppStore()
const collapse = computed(() => appStore.getCollapse)
const fixedMenu = computed(() => appStore.getFixedMenu)
const permissionStore = usePermissionStore()
const routers = computed(() => permissionStore.getRouters)
const tabRouters = computed(() => unref(routers).filter((v) => !v?.meta?.hidden))
const setCollapse = () => {
appStore.setCollapse(!unref(collapse))
}
onMounted(() => {
if (unref(fixedMenu)) {
const path = `/${unref(currentRoute).path.split('/')[1]}`
const children = unref(tabRouters).find(
(v) =>
(v.meta?.alwaysShow || (v?.children?.length && v?.children?.length > 1)) &&
v.path === path
)?.children
tabActive.value = path
if (children) {
permissionStore.setMenuTabRouters(
cloneDeep(children).map((v) => {
v.path = pathResolve(unref(tabActive), v.path)
return v
})
)
}
}
})
watch(
() => routers.value,
(routers: AppRouteRecordRaw[]) => {
initTabMap(routers)
filterMenusPath(routers, routers)
},
{
immediate: true,
deep: true
}
)
const showTitle = ref(true)
watch(
() => collapse.value,
(collapse: boolean) => {
if (!collapse) {
setTimeout(() => {
showTitle.value = !collapse
}, 200)
} else {
showTitle.value = !collapse
}
}
)
// 是否显示菜单
const showMenu = ref(unref(fixedMenu) ? true : false)
// tab高亮
const tabActive = ref('')
// tab点击事件
const tabClick = (item: AppRouteRecordRaw) => {
if (isUrl(item.path)) {
window.open(item.path)
return
}
const newPath = item.children ? item.path : item.path.split('/')[0]
const oldPath = unref(tabActive)
tabActive.value = item.children ? item.path : item.path.split('/')[0]
if (item.children) {
if (newPath === oldPath || !unref(showMenu)) {
showMenu.value = unref(fixedMenu) ? true : !unref(showMenu)
}
if (unref(showMenu)) {
permissionStore.setMenuTabRouters(
cloneDeep(item.children).map((v) => {
v.path = pathResolve(unref(tabActive), v.path)
return v
})
)
}
} else {
push(item.path)
permissionStore.setMenuTabRouters([])
showMenu.value = false
}
}
// 设置高亮
const isActive = (currentPath: string) => {
const { path } = unref(currentRoute)
if (tabPathMap[currentPath].includes(path)) {
return true
}
return false
}
const mouseleave = () => {
if (!unref(showMenu) || unref(fixedMenu)) return
showMenu.value = false
}
return () => (
<div
id={`${variables.namespace}-menu`}
class={[
prefixCls,
'relative bg-[var(--left-menu-bg-color)] top-1px layout-border__right',
{
'w-[var(--tab-menu-max-width)]': !unref(collapse),
'w-[var(--tab-menu-min-width)]': unref(collapse)
}
]}
onMouseleave={mouseleave}
>
<ElScrollbar class="!h-[calc(100%-var(--tab-menu-collapse-height)-1px)]">
<div>
{() => {
return unref(tabRouters).map((v) => {
const item = (
v.meta?.alwaysShow || (v?.children?.length && v?.children?.length > 1)
? v
: {
...(v?.children && v?.children[0]),
path: pathResolve(v.path, (v?.children && v?.children[0])?.path as string)
}
) as AppRouteRecordRaw
return (
<div
class={[
`${prefixCls}__item`,
'text-center text-12px relative py-12px cursor-pointer',
{
'is-active': isActive(v.path)
}
]}
onClick={() => {
tabClick(item)
}}
>
<div>
</div>
{!unref(showTitle) ? undefined : (
<p class="mt-5px break-words px-2px">{t(item.meta?.title)}</p>
)}
</div>
)
})
}}
</div>
</ElScrollbar>
<div
class={[
`${prefixCls}--collapse`,
'text-center h-[var(--tab-menu-collapse-height)] leading-[var(--tab-menu-collapse-height)] cursor-pointer'
]}
onClick={setCollapse}
>
</div>
<Menu
class={[
'!absolute top-0 z-11',
{
'!left-[var(--tab-menu-min-width)]': unref(collapse),
'!left-[var(--tab-menu-max-width)]': !unref(collapse),
'!w-[calc(var(--left-menu-max-width)+1px)]': unref(showMenu) || unref(fixedMenu),
'!w-0': !unref(showMenu) && !unref(fixedMenu)
}
]}
style="transition: width var(--transition-time-02), left var(--transition-time-02);"
></Menu>
</div>
)
}
})
</script>
<style lang="scss" scoped>
$prefix-cls: v-tab-menu;
.#{$prefix-cls} {
transition: all var(--transition-time-02);
&__item {
color: var(--left-menu-text-color);
transition: all var(--transition-time-02);
&:hover {
color: var(--left-menu-text-active-color);
// background-color: var(--left-menu-bg-active-color);
}
}
&--collapse {
color: var(--left-menu-text-color);
background-color: var(--left-menu-bg-light-color);
}
.is-active {
color: var(--left-menu-text-active-color);
background-color: var(--left-menu-bg-active-color);
}
}
</style>

View File

@@ -1,51 +0,0 @@
import { getAllParentPath } from '@/layouts/components/Menu/src/helper'
import type { RouteMeta } from 'vue-router'
import { isUrl } from '@/utils/is'
import { cloneDeep } from 'lodash-es'
export type TabMapTypes = {
[key: string]: string[]
}
export const tabPathMap = reactive<TabMapTypes>({})
export const initTabMap = (routes: AppRouteRecordRaw[]) => {
for (const v of routes) {
const meta = (v.meta ?? {}) as RouteMeta
if (!meta?.hidden) {
tabPathMap[v.path] = []
}
}
}
export const filterMenusPath = (
routes: AppRouteRecordRaw[],
allRoutes: AppRouteRecordRaw[]
): AppRouteRecordRaw[] => {
const res: AppRouteRecordRaw[] = []
for (const v of routes) {
let data: Nullable<AppRouteRecordRaw> = null
const meta = (v.meta ?? {}) as RouteMeta
if (!meta.hidden || meta.canTo) {
const allParentPath = getAllParentPath<AppRouteRecordRaw>(allRoutes, v.path)
const fullPath = isUrl(v.path) ? v.path : allParentPath.join('/')
data = cloneDeep(v)
data.path = fullPath
if (v.children && data) {
data.children = filterMenusPath(v.children, allRoutes)
}
if (data) {
res.push(data)
}
if (allParentPath.length && Reflect.has(tabPathMap, allParentPath[0])) {
tabPathMap[allParentPath[0]].push(fullPath)
}
}
}
return res
}

View File

@@ -1,3 +0,0 @@
import TagsView from './src/TagsView.vue'
export { TagsView }

View File

@@ -1,585 +0,0 @@
<script lang="ts" setup>
import { onMounted, watch, computed, unref, ref, nextTick } from 'vue'
import { useRouter } from 'vue-router'
import type { RouteLocationNormalizedLoaded, RouterLinkProps } from 'vue-router'
import { usePermissionStore } from '@/stores/modules/permission'
import { useTagsViewStore } from '@/stores/modules/tagsView'
import { useAppStore } from '@/stores/modules/app'
import { useI18n } from '@/hooks/web/useI18n'
import { filterAffixTags } from './helper'
import { ContextMenu, ContextMenuExpose } from '@/layouts/components/ContextMenu'
import { useDesign } from '@/hooks/web/useDesign'
import { useTemplateRefsList } from '@vueuse/core'
import { ElScrollbar } from 'element-plus'
import { useScrollTo } from '@/hooks/event/useScrollTo'
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('tags-view')
const { t } = useI18n()
const { currentRoute, push, replace } = useRouter()
const permissionStore = usePermissionStore()
const routers = computed(() => permissionStore.getRouters)
const tagsViewStore = useTagsViewStore()
const visitedViews = computed(() => tagsViewStore.getVisitedViews)
const affixTagArr = ref<RouteLocationNormalizedLoaded[]>([])
const appStore = useAppStore()
const tagsViewIcon = computed(() => appStore.getTagsViewIcon)
const isDark = computed(() => appStore.getIsDark)
// 初始化tag
const initTags = () => {
affixTagArr.value = filterAffixTags(unref(routers))
for (const tag of unref(affixTagArr)) {
// Must have tag name
if (tag.name) {
tagsViewStore.addVisitedView(tag)
}
}
}
const selectedTag = ref<RouteLocationNormalizedLoaded>()
// 新增tag
const addTags = () => {
const { name } = unref(currentRoute)
if (name) {
selectedTag.value = unref(currentRoute)
tagsViewStore.addView(unref(currentRoute))
}
return false
}
// 关闭选中的tag
const closeSelectedTag = (view: RouteLocationNormalizedLoaded) => {
if (view?.meta?.affix) return
tagsViewStore.delView(view)
if (isActive(view)) {
toLastView()
}
}
// 关闭全部
const closeAllTags = () => {
tagsViewStore.delAllViews()
toLastView()
}
// 关闭其它
const closeOthersTags = () => {
tagsViewStore.delOthersViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
}
// 重新加载
const refreshSelectedTag = async (view?: RouteLocationNormalizedLoaded) => {
if (!view) return
tagsViewStore.delCachedView()
const { path, query } = view
await nextTick()
replace({
path: '/redirect' + path,
query: query
})
}
// 关闭左侧
const closeLeftTags = () => {
tagsViewStore.delLeftViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
}
// 关闭右侧
const closeRightTags = () => {
tagsViewStore.delRightViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
}
// 跳转到最后一个
const toLastView = () => {
const visitedViews = tagsViewStore.getVisitedViews
const latestView = visitedViews.slice(-1)[0]
if (latestView) {
push(latestView)
} else {
if (
unref(currentRoute).path === permissionStore.getAddRouters[0].path ||
unref(currentRoute).path === permissionStore.getAddRouters[0].redirect
) {
addTags()
return
}
// TODO: You can set another route
push('/')
}
}
// 滚动到选中的tag
const moveToCurrentTag = async () => {
await nextTick()
for (const v of unref(visitedViews)) {
if (v.fullPath === unref(currentRoute).path) {
moveToTarget(v)
if (v.fullPath !== unref(currentRoute).fullPath) {
tagsViewStore.updateVisitedView(unref(currentRoute))
}
break
}
}
}
const tagLinksRefs = useTemplateRefsList<RouterLinkProps>()
const moveToTarget = (currentTag: RouteLocationNormalizedLoaded) => {
const wrap$ = unref(scrollbarRef)?.wrapRef
let firstTag: Nullable<RouterLinkProps> = null
let lastTag: Nullable<RouterLinkProps> = null
const tagList = unref(tagLinksRefs)
// find first tag and last tag
if (tagList.length > 0) {
firstTag = tagList[0]
lastTag = tagList[tagList.length - 1]
}
if ((firstTag?.to as RouteLocationNormalizedLoaded).fullPath === currentTag.fullPath) {
// 直接滚动到0的位置
const { start } = useScrollTo({
el: wrap$!,
position: 'scrollLeft',
to: 0,
duration: 500
})
start()
} else if ((lastTag?.to as RouteLocationNormalizedLoaded).fullPath === currentTag.fullPath) {
// 滚动到最后的位置
const { start } = useScrollTo({
el: wrap$!,
position: 'scrollLeft',
to: wrap$!.scrollWidth - wrap$!.offsetWidth,
duration: 500
})
start()
} else {
// find preTag and nextTag
const currentIndex: number = tagList.findIndex(
(item) => (item?.to as RouteLocationNormalizedLoaded).fullPath === currentTag.fullPath
)
const tgsRefs = document.getElementsByClassName(`${prefixCls}__item`)
const prevTag = tgsRefs[currentIndex - 1] as HTMLElement
const nextTag = tgsRefs[currentIndex + 1] as HTMLElement
// the tag's offsetLeft after of nextTag
const afterNextTagOffsetLeft = nextTag.offsetLeft + nextTag.offsetWidth + 4
// the tag's offsetLeft before of prevTag
const beforePrevTagOffsetLeft = prevTag.offsetLeft - 4
if (afterNextTagOffsetLeft > unref(scrollLeftNumber) + wrap$!.offsetWidth) {
const { start } = useScrollTo({
el: wrap$!,
position: 'scrollLeft',
to: afterNextTagOffsetLeft - wrap$!.offsetWidth,
duration: 500
})
start()
} else if (beforePrevTagOffsetLeft < unref(scrollLeftNumber)) {
const { start } = useScrollTo({
el: wrap$!,
position: 'scrollLeft',
to: beforePrevTagOffsetLeft,
duration: 500
})
start()
}
}
}
// 是否是当前tag
const isActive = (route: RouteLocationNormalizedLoaded): boolean => {
return route.path === unref(currentRoute).path
}
// 所有右键菜单组件的元素
const itemRefs = useTemplateRefsList<ComponentRef<typeof ContextMenu & ContextMenuExpose>>()
// 右键菜单装填改变的时候
const visibleChange = (visible: boolean, tagItem: RouteLocationNormalizedLoaded) => {
if (visible) {
for (const v of unref(itemRefs)) {
const elDropdownMenuRef = v.elDropdownMenuRef
if (tagItem.fullPath !== v.tagItem.fullPath) {
elDropdownMenuRef?.handleClose()
}
}
}
}
// elscroll 实例
const scrollbarRef = ref<ComponentRef<typeof ElScrollbar>>()
// 保存滚动位置
const scrollLeftNumber = ref(0)
const scroll = ({ scrollLeft }) => {
scrollLeftNumber.value = scrollLeft as number
}
// 移动到某个位置
const move = (to: number) => {
const wrap$ = unref(scrollbarRef)?.wrapRef
const { start } = useScrollTo({
el: wrap$!,
position: 'scrollLeft',
to: unref(scrollLeftNumber) + to,
duration: 500
})
start()
}
onMounted(() => {
initTags()
addTags()
})
watch(
() => currentRoute.value,
() => {
addTags()
moveToCurrentTag()
}
)
</script>
<template>
<div
:id="prefixCls"
:class="prefixCls"
class="relative w-full flex bg-[#fff] dark:bg-[var(--el-bg-color)]"
>
<span
:class="`${prefixCls}__tool ${prefixCls}__tool--first`"
class="h-[var(--tags-view-height)] w-[var(--tags-view-height)] flex cursor-pointer items-center justify-center"
@click="move(-200)"
>
<Icon
icon="ep:d-arrow-left"
color="var(--el-text-color-placeholder)"
:hover-color="isDark ? '#fff' : 'var(--el-color-black)'"
/>
</span>
<div class="flex-1 overflow-hidden">
<ElScrollbar ref="scrollbarRef" class="h-full" @scroll="scroll">
<div class="h-full flex">
<ContextMenu
:ref="itemRefs.set"
:schema="[
{
icon: 'ep:refresh',
label: t('common.reload'),
disabled: selectedTag?.fullPath !== item.fullPath,
command: () => {
refreshSelectedTag(item)
}
},
{
icon: 'ep:close',
label: t('common.closeTab'),
disabled: !!visitedViews?.length && selectedTag?.meta.affix,
command: () => {
closeSelectedTag(item)
}
},
{
divided: true,
icon: 'ep:d-arrow-left',
label: t('common.closeTheLeftTab'),
disabled:
!!visitedViews?.length &&
(item.fullPath === visitedViews[0].fullPath ||
selectedTag?.fullPath !== item.fullPath),
command: () => {
closeLeftTags()
}
},
{
icon: 'ep:d-arrow-right',
label: t('common.closeTheRightTab'),
disabled:
!!visitedViews?.length &&
(item.fullPath === visitedViews[visitedViews.length - 1].fullPath ||
selectedTag?.fullPath !== item.fullPath),
command: () => {
closeRightTags()
}
},
{
divided: true,
icon: 'ep:discount',
label: t('common.closeOther'),
disabled: selectedTag?.fullPath !== item.fullPath,
command: () => {
closeOthersTags()
}
},
{
icon: 'ep:minus',
label: t('common.closeAll'),
command: () => {
closeAllTags()
}
}
]"
v-for="item in visitedViews"
:key="item.fullPath"
:tag-item="item"
:class="[
`${prefixCls}__item`,
item?.meta?.affix ? `${prefixCls}__item--affix` : '',
{
'is-active': isActive(item)
}
]"
@visible-change="visibleChange"
>
<div>
<router-link :ref="tagLinksRefs.set" :to="{ ...item }" custom v-slot="{ navigate }">
<div
@click="navigate"
class="h-full flex items-center justify-center whitespace-nowrap pl-15px"
>
<Icon
v-if="
item?.matched &&
item?.matched[1] &&
item?.matched[1]?.meta?.icon &&
tagsViewIcon
"
:icon="item?.matched[1]?.meta?.icon"
:size="12"
class="mr-5px"
/>
{{ t(item?.meta?.title as string) }}
<Icon
:class="`${prefixCls}__item--close`"
color="#333"
icon="ep:close"
:size="12"
@click.prevent.stop="closeSelectedTag(item)"
/>
</div>
</router-link>
</div>
</ContextMenu>
</div>
</ElScrollbar>
</div>
<span
:class="`${prefixCls}__tool`"
class="h-[var(--tags-view-height)] w-[var(--tags-view-height)] flex cursor-pointer items-center justify-center"
@click="move(200)"
>
<Icon
icon="ep:d-arrow-right"
color="var(--el-text-color-placeholder)"
:hover-color="isDark ? '#fff' : 'var(--el-color-black)'"
/>
</span>
<span
:class="`${prefixCls}__tool`"
class="h-[var(--tags-view-height)] w-[var(--tags-view-height)] flex cursor-pointer items-center justify-center"
@click="refreshSelectedTag(selectedTag)"
>
<Icon
icon="ep:refresh-right"
color="var(--el-text-color-placeholder)"
:hover-color="isDark ? '#fff' : 'var(--el-color-black)'"
/>
</span>
<ContextMenu
trigger="click"
:schema="[
{
icon: 'ep:refresh',
label: t('common.reload'),
command: () => {
refreshSelectedTag(selectedTag)
}
},
{
icon: 'ep:close',
label: t('common.closeTab'),
disabled: !!visitedViews?.length && selectedTag?.meta.affix,
command: () => {
closeSelectedTag(selectedTag!)
}
},
{
divided: true,
icon: 'ep:d-arrow-left',
label: t('common.closeTheLeftTab'),
disabled: !!visitedViews?.length && selectedTag?.fullPath === visitedViews[0].fullPath,
command: () => {
closeLeftTags()
}
},
{
icon: 'ep:d-arrow-right',
label: t('common.closeTheRightTab'),
disabled:
!!visitedViews?.length &&
selectedTag?.fullPath === visitedViews[visitedViews.length - 1].fullPath,
command: () => {
closeRightTags()
}
},
{
divided: true,
icon: 'ep:discount',
label: t('common.closeOther'),
command: () => {
closeOthersTags()
}
},
{
icon: 'ep:minus',
label: t('common.closeAll'),
command: () => {
closeAllTags()
}
}
]"
>
<span
:class="`${prefixCls}__tool`"
class="block h-[var(--tags-view-height)] w-[var(--tags-view-height)] flex cursor-pointer items-center justify-center"
>
<Icon
icon="ep:menu"
color="var(--el-text-color-placeholder)"
:hover-color="isDark ? '#fff' : 'var(--el-color-black)'"
/>
</span>
</ContextMenu>
</div>
</template>
<style lang="scss" scoped>
$prefix-cls: v-tags-view;
.#{$prefix-cls} {
:deep(.el-scrollbar__view) {
height: 100%;
}
&__tool {
position: relative;
&::before {
position: absolute;
top: 1px;
left: 0;
width: 100%;
height: calc(100% - 1px);
border-left: 1px solid var(--el-border-color);
content: '';
}
&--first {
&::before {
position: absolute;
top: 1px;
left: 0;
width: 100%;
height: calc(100% - 1px);
border-right: 1px solid var(--el-border-color);
border-left: none;
content: '';
}
}
}
&__item {
position: relative;
top: 2px;
height: calc(100% - 6px);
padding-right: 25px;
margin-left: 4px;
font-size: 12px;
cursor: pointer;
border: 1px solid #d9d9d9;
border-radius: 2px;
&--close {
position: absolute;
top: 50%;
right: 5px;
display: none;
transform: translate(0, -50%);
}
&:not(.#{$prefix-cls}__item--affix):hover {
.#{$prefix-cls}__item--close {
display: block;
}
}
}
&__item:not(.is-active) {
&:hover {
color: var(--el-color-primary);
}
}
&__item.is-active {
color: var(--el-color-white);
background-color: var(--el-color-primary);
border: 1px solid var(--el-color-primary);
.#{$prefix-cls}__item--close {
:deep(span) {
color: var(--el-color-white) !important;
}
}
}
}
.dark {
.#{$prefix-cls} {
&__tool {
&--first {
&::after {
display: none;
}
}
}
&__item {
border: 1px solid var(--el-border-color);
}
&__item:not(.is-active) {
&:hover {
color: var(--el-color-primary);
}
}
&__item.is-active {
color: var(--el-color-white);
background-color: var(--el-color-primary);
border: 1px solid var(--el-color-primary);
.#{$prefix-cls}__item--close {
:deep(span) {
color: var(--el-color-white) !important;
}
}
}
}
}
</style>

View File

@@ -1,21 +0,0 @@
import type { RouteMeta, RouteLocationNormalizedLoaded } from 'vue-router'
import { pathResolve } from '@/utils/routerHelper'
export const filterAffixTags = (routes: AppRouteRecordRaw[], parentPath = '') => {
let tags: RouteLocationNormalizedLoaded[] = []
routes.forEach((route) => {
const meta = route.meta as RouteMeta
const tagPath = pathResolve(parentPath, route.path)
if (meta?.affix) {
tags.push({ ...route, path: tagPath, fullPath: tagPath } as RouteLocationNormalizedLoaded)
}
if (route.children) {
const tempTags: RouteLocationNormalizedLoaded[] = filterAffixTags(route.children, tagPath)
if (tempTags.length >= 1) {
tags = [...tags, ...tempTags]
}
}
})
return tags
}

View File

@@ -1,3 +0,0 @@
import ThemeSwitch from './src/ThemeSwitch.vue'
export { ThemeSwitch }

View File

@@ -1,46 +0,0 @@
<script lang="ts" setup>
import { useAppStore } from '@/stores/modules/app'
import { useIcon } from '@/hooks/web/useIcon'
import { useDesign } from '@/hooks/web/useDesign'
defineOptions({ name: 'ThemeSwitch' })
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('theme-switch')
const Sun = useIcon({ icon: 'emojione-monotone:sun', color: '#fde047' })
const CrescentMoon = useIcon({ icon: 'emojione-monotone:crescent-moon', color: '#fde047' })
const appStore = useAppStore()
// 初始化获取是否是暗黑主题
const isDark = ref(appStore.getIsDark)
// 设置switch的背景颜色
const blackColor = 'var(--el-color-black)'
const themeChange = (val: boolean) => {
appStore.setIsDark(val)
}
</script>
<template>
<ElSwitch
v-model="isDark"
:active-color="blackColor"
:active-icon="Sun"
:border-color="blackColor"
:class="prefixCls"
:inactive-color="blackColor"
:inactive-icon="CrescentMoon"
inline-prompt
@change="themeChange"
/>
</template>
<style lang="scss" scoped>
:deep(.el-switch__core .el-switch__inner .is-icon) {
overflow: visible;
}
</style>

View File

@@ -1,95 +0,0 @@
<script lang="tsx">
import { defineComponent, computed } from 'vue'
import { Collapse } from '@/layouts/components/Collapse'
import { UserInfo } from '@/layouts/components/UserInfo'
import { Screenfull } from '@/layouts/components/Screenfull'
import { Breadcrumb } from '@/layouts/components/Breadcrumb'
import { SizeDropdown } from '@/layouts/components/SizeDropdown'
import { LocaleDropdown } from '@/layouts/components/LocaleDropdown'
import RouterSearch from '@/components/RouterSearch/index.vue'
import { useAppStore } from '@/stores/modules/app'
import { useDesign } from '@/hooks/web/useDesign'
const { getPrefixCls, variables } = useDesign()
const prefixCls = getPrefixCls('tool-header')
const appStore = useAppStore()
// 面包屑
const breadcrumb = computed(() => appStore.getBreadcrumb)
// 折叠图标
const hamburger = computed(() => appStore.getHamburger)
// 全屏图标
const screenfull = computed(() => appStore.getScreenfull)
// 搜索图片
const search = computed(() => appStore.search)
// 尺寸图标
const size = computed(() => appStore.getSize)
// 布局
const layout = computed(() => appStore.getLayout)
// 多语言图标
const locale = computed(() => appStore.getLocale)
// 消息图标
const message = computed(() => appStore.getMessage)
export default defineComponent({
name: 'ToolHeader',
setup() {
return () => (
<div
id={`${variables.namespace}-tool-header`}
class={[
prefixCls,
'h-[var(--top-tool-height)] relative px-[var(--top-tool-p-x)] flex items-center justify-between',
'dark:bg-[var(--el-bg-color)]'
]}
>
{layout.value !== 'top' ? (
<div class="h-full flex items-center">
{hamburger.value && layout.value !== 'cutMenu' ? (
<Collapse class="custom-hover" color="var(--top-header-text-color)"></Collapse>
) : undefined}
{breadcrumb.value ? <Breadcrumb class="lt-md:hidden"></Breadcrumb> : undefined}
</div>
) : undefined}
<div class="h-full flex items-center">
{screenfull.value ? (
<Screenfull class="custom-hover" color="var(--top-header-text-color)"></Screenfull>
) : undefined}
{search.value ? <RouterSearch isModal={false} /> : undefined}
{size.value ? (
<SizeDropdown class="custom-hover" color="var(--top-header-text-color)"></SizeDropdown>
) : undefined}
{locale.value ? (
<LocaleDropdown
class="custom-hover"
color="var(--top-header-text-color)"
></LocaleDropdown>
) : undefined}
{message.value ? (
"undefined"
// <Message class="custom-hover" color="var(--top-header-text-color)"></Message>
) : undefined}
<UserInfo></UserInfo>
</div>
</div>
)
}
})
</script>
<style lang="scss" scoped>
$prefix-cls: v-tool-header;
.#{$prefix-cls} {
transition: left var(--transition-time-02);
}
</style>

View File

@@ -1,3 +0,0 @@
import UserInfo from './src/UserInfo.vue'
export { UserInfo }

View File

@@ -1,113 +0,0 @@
<script lang='ts' setup>
import { ElMessageBox } from 'element-plus'
import { Tools, Menu, Lock, SwitchButton } from '@element-plus/icons-vue'
import avatarImg from '@/assets/imgs/avatar.gif'
import { useDesign } from '@/hooks/web/useDesign'
import { useTagsViewStore } from '@/stores/modules/tagsView'
import { useUserStore } from '@/stores/modules/user'
import LockDialog from './components/LockDialog.vue'
import LockPage from './components/LockPage.vue'
import { useLockStore } from '@/stores/modules/lock'
defineOptions({ name: 'UserInfo' })
const { t } = useI18n()
const { push, replace } = useRouter()
const userStore = useUserStore()
const tagsViewStore = useTagsViewStore()
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('user-info')
const avatar = computed(() => userStore.user.avatar ?? avatarImg)
const userName = computed(() => userStore.user.nickname ?? 'Admin')
// 锁定屏幕
const lockStore = useLockStore()
const getIsLock = computed(() => lockStore.getLockInfo?.isLock ?? false)
const dialogVisible = ref<boolean>(false)
const lockScreen = () => {
dialogVisible.value = true
}
const loginOut = async () => {
try {
await ElMessageBox.confirm(t('common.loginOutMessage'), t('common.reminder'), {
confirmButtonText: t('common.ok'),
cancelButtonText: t('common.cancel'),
type: 'warning'
})
await userStore.loginOut()
tagsViewStore.delAllViews()
replace('/login?redirect=/index')
} catch {
}
}
const toProfile = async () => {
push('/user/profile')
}
const toDocument = () => {
window.open('https://doc.iocoder.cn/')
}
</script>
<template>
<ElDropdown class='custom-hover' :class='prefixCls' trigger='click'>
<div class='flex items-center'>
<ElAvatar :src='avatar' alt='' class='w-[calc(var(--logo-height)-25px)] rounded-[50%]' />
<span class='pl-[5px] text-14px text-[var(--top-header-text-color)] <lg:hidden'>
{{ userName }}
</span>
</div>
<template #dropdown>
<ElDropdownMenu>
<ElDropdownItem>
<Tools />
<div @click='toProfile'>{{ t('common.profile') }}</div>
</ElDropdownItem>
<ElDropdownItem>
<Menu />
<div @click='toDocument'>{{ t('common.document') }}</div>
</ElDropdownItem>
<ElDropdownItem divided>
<Lock />
<div @click='lockScreen'>{{ t('lock.lockScreen') }}</div>
</ElDropdownItem>
<ElDropdownItem divided @click='loginOut'>
<SwitchButton />
<div>{{ t('common.loginOut') }}</div>
</ElDropdownItem>
</ElDropdownMenu>
</template>
</ElDropdown>
<LockDialog v-if='dialogVisible' v-model='dialogVisible' />
<teleport to='body'>
<transition name='fade-bottom' mode='out-in'>
<LockPage v-if='getIsLock' />
</transition>
</teleport>
</template>
<style scoped lang='scss'>
.fade-bottom-enter-active,
.fade-bottom-leave-active {
transition: opacity 0.25s,
transform 0.3s;
}
.fade-bottom-enter-from {
opacity: 0;
transform: translateY(-10%);
}
.fade-bottom-leave-to {
opacity: 0;
transform: translateY(10%);
}
</style>

View File

@@ -1,98 +0,0 @@
<script setup lang="ts">
import { useValidator } from '@/hooks/web/useValidator'
import { useDesign } from '@/hooks/web/useDesign'
import { useLockStore } from '@/stores/modules/lock'
import avatarImg from '@/assets/imgs/avatar.gif'
import { useUserStore } from '@/stores/modules/user'
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('lock-dialog')
const { required } = useValidator()
const { t } = useI18n()
const lockStore = useLockStore()
const props = defineProps({
modelValue: {
type: Boolean
}
})
const userStore = useUserStore()
const avatar = computed(() => userStore.user.avatar ?? avatarImg)
const userName = computed(() => userStore.user.nickname ?? 'Admin')
const emit = defineEmits(['update:modelValue'])
const dialogVisible = computed({
get: () => props.modelValue,
set: (val) => {
emit('update:modelValue', val)
}
})
const dialogTitle = ref(t('lock.lockScreen'))
const formData = ref({
password: undefined
})
const formRules = reactive({
password: [required()]
})
const formRef = ref() // 表单 Ref
const handleLock = async () => {
// 校验表单
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
// 提交请求
dialogVisible.value = false
lockStore.setLockInfo({
...formData.value,
isLock: true
})
}
</script>
<template>
<Dialog
v-model="dialogVisible"
width="500px"
max-height="170px"
:class="prefixCls"
:title="dialogTitle"
>
<div class="flex flex-col items-center">
<img :src="avatar" alt="" class="w-70px h-70px rounded-[50%]" />
<span class="text-14px my-10px text-[var(--top-header-text-color)]">
{{ userName }}
</span>
</div>
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="80px">
<el-form-item :label="t('lock.lockPassword')" prop="password">
<el-input
type="password"
v-model="formData.password"
:placeholder="'请输入' + t('lock.lockPassword')"
clearable
show-password
/>
</el-form-item>
</el-form>
<template #footer>
<ElButton type="primary" @click="handleLock">{{ t('lock.lock') }}</ElButton>
</template>
</Dialog>
</template>
<style lang="scss" scoped>
:global(.v-lock-dialog) {
@media (max-width: 767px) {
max-width: calc(100vw - 16px);
}
}
</style>

View File

@@ -1,269 +0,0 @@
<script lang="ts" setup>
import { deleteUserCache } from '@/hooks/web/useCache'
import { useLockStore } from '@/stores/modules/lock'
import { useNow } from '@/hooks/web/useNow'
import { useDesign } from '@/hooks/web/useDesign'
import { useTagsViewStore } from '@/stores/modules/tagsView'
import { useUserStore } from '@/stores/modules/user'
import avatarImg from '@/assets/imgs/avatar.gif'
const tagsViewStore = useTagsViewStore()
const { replace } = useRouter()
const userStore = useUserStore()
const password = ref('')
const loading = ref(false)
const errMsg = ref(false)
const showDate = ref(true)
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('lock-page')
const avatar = computed(() => userStore.user.avatar ?? avatarImg)
const userName = computed(() => userStore.user.nickname ?? 'Admin')
const lockStore = useLockStore()
const { hour, month, minute, meridiem, year, day, week } = useNow(true)
const { t } = useI18n()
// 解锁
async function unLock() {
if (!password.value) {
return
}
let pwd = password.value
try {
loading.value = true
const res = await lockStore.unLock(pwd)
errMsg.value = !res
} finally {
loading.value = false
}
}
// 返回登录
async function goLogin() {
await userStore.loginOut().catch(() => {})
// 登出后清理
deleteUserCache() // 清空用户缓存
tagsViewStore.delAllViews()
// resetRouter() // 重置静态路由表
lockStore.resetLockInfo()
replace('/login')
}
function handleShowForm(show = false) {
showDate.value = show
}
</script>
<template>
<div
:class="prefixCls"
class="fixed inset-0 flex h-screen w-screen bg-black items-center justify-center"
>
<div
:class="`${prefixCls}__unlock`"
class="absolute top-0 left-1/2 flex pt-5 h-16 items-center justify-center sm:text-md xl:text-xl text-white flex-col cursor-pointer transform translate-x-1/2"
@click="handleShowForm(false)"
v-show="showDate"
>
<Icon icon="ep:lock" />
<span>{{ t('lock.unlock') }}</span>
</div>
<div class="flex w-screen h-screen justify-center items-center">
<div :class="`${prefixCls}__hour`" class="relative mr-5 md:mr-20 w-2/5 h-2/5 md:h-4/5">
<span>{{ hour }}</span>
<span class="meridiem absolute left-5 top-5 text-md xl:text-xl" v-show="showDate">
{{ meridiem }}
</span>
</div>
<div :class="`${prefixCls}__minute w-2/5 h-2/5 md:h-4/5 `">
<span> {{ minute }}</span>
</div>
</div>
<transition name="fade-slide">
<div :class="`${prefixCls}-entry`" v-show="!showDate">
<div :class="`${prefixCls}-entry-content`">
<div class="flex flex-col items-center">
<img :src="avatar" alt="" class="w-70px h-70px rounded-[50%]" />
<span class="text-14px my-10px text-[var(--logo-title-text-color)]">
{{ userName }}
</span>
</div>
<ElInput
type="password"
:placeholder="t('lock.placeholder')"
class="enter-x"
v-model="password"
/>
<span :class="`text-14px ${prefixCls}-entry__err-msg enter-x`" v-if="errMsg">
{{ t('lock.message') }}
</span>
<div :class="`${prefixCls}-entry__footer enter-x`">
<ElButton
type="primary"
size="small"
class="mt-2 mr-2 enter-x"
link
:disabled="loading"
@click="handleShowForm(true)"
>
{{ t('common.back') }}
</ElButton>
<ElButton
type="primary"
size="small"
class="mt-2 mr-2 enter-x"
link
:disabled="loading"
@click="goLogin"
>
{{ t('lock.backToLogin') }}
</ElButton>
<ElButton
type="primary"
class="mt-2"
size="small"
link
@click="unLock()"
:disabled="loading"
>
{{ t('lock.entrySystem') }}
</ElButton>
</div>
</div>
</div>
</transition>
<div class="absolute bottom-5 w-full text-gray-300 xl:text-xl 2xl:text-3xl text-center enter-y">
<div class="text-5xl mb-4 enter-x" v-show="!showDate">
{{ hour }}:{{ minute }} <span class="text-3xl">{{ meridiem }}</span>
</div>
<div class="text-2xl">{{ year }}/{{ month }}/{{ day }} {{ week }}</div>
</div>
</div>
</template>
<style lang="scss" scoped>
$prefix-cls: 'v-lock-page';
// Small screen / tablet
$screen-sm: 576px;
// Medium screen / desktop
$screen-md: 768px;
// Large screen / wide desktop
$screen-lg: 992px;
// Extra large screen / full hd
$screen-xl: 1200px;
// Extra extra large screen / large desktop
$screen-2xl: 1600px;
$error-color: #ed6f6f;
.#{$prefix-cls} {
z-index: 3000;
&__unlock {
transform: translate(-50%, 0);
}
&__hour,
&__minute {
display: flex;
font-weight: 700;
color: #bababa;
background-color: #141313;
border-radius: 30px;
justify-content: center;
align-items: center;
@media screen and (max-width: $screen-md) {
span:not(.meridiem) {
font-size: 160px;
}
}
@media screen and (min-width: $screen-md) {
span:not(.meridiem) {
font-size: 160px;
}
}
@media screen and (max-width: $screen-sm) {
span:not(.meridiem) {
font-size: 90px;
}
}
@media screen and (min-width: $screen-lg) {
span:not(.meridiem) {
font-size: 220px;
}
}
@media screen and (min-width: $screen-xl) {
span:not(.meridiem) {
font-size: 260px;
}
}
@media screen and (min-width: $screen-2xl) {
span:not(.meridiem) {
font-size: 320px;
}
}
}
&-entry {
position: absolute;
top: 0;
left: 0;
display: flex;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(8px);
justify-content: center;
align-items: center;
&-content {
width: 260px;
}
&__header {
text-align: center;
&-img {
width: 70px;
margin: 0 auto;
border-radius: 50%;
}
&-name {
margin-top: 5px;
font-weight: 500;
color: #bababa;
}
}
&__err-msg {
display: inline-block;
margin-top: 10px;
color: $error-color;
}
&__footer {
display: flex;
justify-content: space-between;
}
}
}
</style>

Some files were not shown because too many files have changed in this diff Show More