调整前端两套互不连通的 HTTP 请求层并存、两套布局框架并存、两套 Pinia 实例并存问题
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import request from '@/config/axios'
|
||||
import { requestData, requestDownload } from '@/utils/request'
|
||||
|
||||
// BPM 流程表达式 VO
|
||||
export interface ProcessExpressionVO {
|
||||
@@ -12,31 +12,36 @@ export interface ProcessExpressionVO {
|
||||
export const ProcessExpressionApi = {
|
||||
// 查询BPM 流程表达式分页
|
||||
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 流程表达式详情
|
||||
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 流程表达式
|
||||
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 流程表达式
|
||||
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 流程表达式
|
||||
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
|
||||
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'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import request from '@/config/axios'
|
||||
import { requestData } from '@/utils/request'
|
||||
|
||||
// BPM 流程监听器 VO
|
||||
export interface ProcessListenerVO {
|
||||
@@ -15,26 +15,26 @@ export interface ProcessListenerVO {
|
||||
export const ProcessListenerApi = {
|
||||
// 查询流程监听器分页
|
||||
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) => {
|
||||
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) => {
|
||||
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) => {
|
||||
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) => {
|
||||
return await request.delete({ url: `/bpm/process-listener/delete?id=` + id })
|
||||
return await requestData({ url: '/bpm/process-listener/delete?id=' + id, method: 'DELETE' })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,47 +1,41 @@
|
||||
import request from '@/config/axios'
|
||||
|
||||
export type UserGroupVO = {
|
||||
id: number
|
||||
name: string
|
||||
description: string
|
||||
userIds: number[]
|
||||
status: number
|
||||
remark: string
|
||||
createTime: string
|
||||
}
|
||||
|
||||
// 创建用户组
|
||||
export const createUserGroup = async (data: UserGroupVO) => {
|
||||
return await request.post({
|
||||
url: '/bpm/user-group/create',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 更新用户组
|
||||
export const updateUserGroup = async (data: UserGroupVO) => {
|
||||
return await request.put({
|
||||
url: '/bpm/user-group/update',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除用户组
|
||||
export const deleteUserGroup = async (id: number) => {
|
||||
return await request.delete({ url: '/bpm/user-group/delete?id=' + id })
|
||||
}
|
||||
|
||||
// 获得用户组
|
||||
export const getUserGroup = async (id: number) => {
|
||||
return await request.get({ url: '/bpm/user-group/get?id=' + id })
|
||||
}
|
||||
|
||||
// 获得用户组分页
|
||||
export const getUserGroupPage = async (params) => {
|
||||
return await request.get({ url: '/bpm/user-group/page', params })
|
||||
}
|
||||
|
||||
// 获取用户组精简信息列表
|
||||
export const getUserGroupSimpleList = async (): Promise<UserGroupVO[]> => {
|
||||
return await request.get({ url: '/bpm/user-group/simple-list' })
|
||||
}
|
||||
import { requestData } from '@/utils/request'
|
||||
|
||||
export type UserGroupVO = {
|
||||
id: number
|
||||
name: string
|
||||
description: string
|
||||
userIds: number[]
|
||||
status: number
|
||||
remark: string
|
||||
createTime: string
|
||||
}
|
||||
|
||||
// 创建用户组
|
||||
export const createUserGroup = async (data: UserGroupVO) => {
|
||||
return await requestData({ url: '/bpm/user-group/create', method: 'POST', data })
|
||||
}
|
||||
|
||||
// 更新用户组
|
||||
export const updateUserGroup = async (data: UserGroupVO) => {
|
||||
return await requestData({ url: '/bpm/user-group/update', method: 'PUT', data })
|
||||
}
|
||||
|
||||
// 删除用户组
|
||||
export const deleteUserGroup = async (id: number) => {
|
||||
return await requestData({ url: '/bpm/user-group/delete?id=' + id, method: 'DELETE' })
|
||||
}
|
||||
|
||||
// 获得用户组
|
||||
export const getUserGroup = async (id: number) => {
|
||||
return await requestData({ url: '/bpm/user-group/get?id=' + id, method: 'GET' })
|
||||
}
|
||||
|
||||
// 获得用户组分页
|
||||
export const getUserGroupPage = async (params) => {
|
||||
return await requestData({ url: '/bpm/user-group/page', method: 'GET', params })
|
||||
}
|
||||
|
||||
// 获取用户组精简信息列表
|
||||
export const getUserGroupSimpleList = async (): Promise<UserGroupVO[]> => {
|
||||
return await requestData({ url: '/bpm/user-group/simple-list', method: 'GET' })
|
||||
}
|
||||
|
||||
@@ -1,49 +1,49 @@
|
||||
import request from '@/config/axios'
|
||||
|
||||
export type DictDataVO = {
|
||||
id: number | undefined
|
||||
sort: number | undefined
|
||||
label: string
|
||||
value: string
|
||||
dictType: string
|
||||
status: number
|
||||
colorType: string
|
||||
cssClass: string
|
||||
remark: string
|
||||
createTime: Date
|
||||
}
|
||||
|
||||
// 查询字典数据(精简)列表
|
||||
export const getSimpleDictDataList = () => {
|
||||
return request.get({ url: '/system/dict-data/simple-list' })
|
||||
}
|
||||
|
||||
// 查询字典数据列表
|
||||
export const getDictDataPage = (params: PageParam) => {
|
||||
return request.get({ url: '/system/dict-data/page', params })
|
||||
}
|
||||
|
||||
// 查询字典数据详情
|
||||
export const getDictData = (id: number) => {
|
||||
return request.get({ url: '/system/dict-data/get?id=' + id })
|
||||
}
|
||||
|
||||
// 新增字典数据
|
||||
export const createDictData = (data: DictDataVO) => {
|
||||
return request.post({ url: '/system/dict-data/create', data })
|
||||
}
|
||||
|
||||
// 修改字典数据
|
||||
export const updateDictData = (data: DictDataVO) => {
|
||||
return request.put({ url: '/system/dict-data/update', data })
|
||||
}
|
||||
|
||||
// 删除字典数据
|
||||
export const deleteDictData = (id: number) => {
|
||||
return request.delete({ url: '/system/dict-data/delete?id=' + id })
|
||||
}
|
||||
|
||||
// 导出字典类型数据
|
||||
export const exportDictData = (params) => {
|
||||
return request.download({ url: '/system/dict-data/export', params })
|
||||
}
|
||||
import { requestData, requestDownload } from '@/utils/request'
|
||||
|
||||
export type DictDataVO = {
|
||||
id: number | undefined
|
||||
sort: number | undefined
|
||||
label: string
|
||||
value: string
|
||||
dictType: string
|
||||
status: number
|
||||
colorType: string
|
||||
cssClass: string
|
||||
remark: string
|
||||
createTime: Date
|
||||
}
|
||||
|
||||
// 查询字典数据(精简)列表
|
||||
export const getSimpleDictDataList = () => {
|
||||
return requestData({ url: '/system/dict-data/simple-list', method: 'GET' })
|
||||
}
|
||||
|
||||
// 查询字典数据列表
|
||||
export const getDictDataPage = (params: PageParam) => {
|
||||
return requestData({ url: '/system/dict-data/page', method: 'GET', params })
|
||||
}
|
||||
|
||||
// 查询字典数据详情
|
||||
export const getDictData = (id: number) => {
|
||||
return requestData({ url: '/system/dict-data/get?id=' + id, method: 'GET' })
|
||||
}
|
||||
|
||||
// 新增字典数据
|
||||
export const createDictData = (data: DictDataVO) => {
|
||||
return requestData({ url: '/system/dict-data/create', method: 'POST', data })
|
||||
}
|
||||
|
||||
// 修改字典数据
|
||||
export const updateDictData = (data: DictDataVO) => {
|
||||
return requestData({ url: '/system/dict-data/update', method: 'PUT', data })
|
||||
}
|
||||
|
||||
// 删除字典数据
|
||||
export const deleteDictData = (id: number) => {
|
||||
return requestData({ url: '/system/dict-data/delete?id=' + id, method: 'DELETE' })
|
||||
}
|
||||
|
||||
// 导出字典类型数据
|
||||
export const exportDictData = (params) => {
|
||||
return requestDownload({ url: '/system/dict-data/export', method: 'GET', params, responseType: 'blob' })
|
||||
}
|
||||
|
||||
@@ -1,44 +1,45 @@
|
||||
import request from '@/config/axios'
|
||||
|
||||
export type DictTypeVO = {
|
||||
id: number | undefined
|
||||
name: string
|
||||
type: string
|
||||
status: number
|
||||
remark: string
|
||||
createTime: Date
|
||||
}
|
||||
|
||||
// 查询字典(精简)列表
|
||||
export const getSimpleDictTypeList = () => {
|
||||
return request.get({ url: '/system/dict-type/list-all-simple' })
|
||||
}
|
||||
|
||||
// 查询字典列表
|
||||
export const getDictTypePage = (params: PageParam) => {
|
||||
return request.get({ url: '/system/dict-type/page', params })
|
||||
}
|
||||
|
||||
// 查询字典详情
|
||||
export const getDictType = (id: number) => {
|
||||
return request.get({ url: '/system/dict-type/get?id=' + id })
|
||||
}
|
||||
|
||||
// 新增字典
|
||||
export const createDictType = (data: DictTypeVO) => {
|
||||
return request.post({ url: '/system/dict-type/create', data })
|
||||
}
|
||||
|
||||
// 修改字典
|
||||
export const updateDictType = (data: DictTypeVO) => {
|
||||
return request.put({ url: '/system/dict-type/update', data })
|
||||
}
|
||||
|
||||
// 删除字典
|
||||
export const deleteDictType = (id: number) => {
|
||||
return request.delete({ url: '/system/dict-type/delete?id=' + id })
|
||||
}
|
||||
// 导出字典类型
|
||||
export const exportDictType = (params) => {
|
||||
return request.download({ url: '/system/dict-type/export', params })
|
||||
}
|
||||
import { requestData, requestDownload } from '@/utils/request'
|
||||
|
||||
export type DictTypeVO = {
|
||||
id: number | undefined
|
||||
name: string
|
||||
type: string
|
||||
status: number
|
||||
remark: string
|
||||
createTime: Date
|
||||
}
|
||||
|
||||
// 查询字典(精简)列表
|
||||
export const getSimpleDictTypeList = () => {
|
||||
return requestData({ url: '/system/dict-type/list-all-simple', method: 'GET' })
|
||||
}
|
||||
|
||||
// 查询字典列表
|
||||
export const getDictTypePage = (params: PageParam) => {
|
||||
return requestData({ url: '/system/dict-type/page', method: 'GET', params })
|
||||
}
|
||||
|
||||
// 查询字典详情
|
||||
export const getDictType = (id: number) => {
|
||||
return requestData({ url: '/system/dict-type/get?id=' + id, method: 'GET' })
|
||||
}
|
||||
|
||||
// 新增字典
|
||||
export const createDictType = (data: DictTypeVO) => {
|
||||
return requestData({ url: '/system/dict-type/create', method: 'POST', data })
|
||||
}
|
||||
|
||||
// 修改字典
|
||||
export const updateDictType = (data: DictTypeVO) => {
|
||||
return requestData({ url: '/system/dict-type/update', method: 'PUT', data })
|
||||
}
|
||||
|
||||
// 删除字典
|
||||
export const deleteDictType = (id: number) => {
|
||||
return requestData({ url: '/system/dict-type/delete?id=' + id, method: 'DELETE' })
|
||||
}
|
||||
|
||||
// 导出字典类型
|
||||
export const exportDictType = (params) => {
|
||||
return requestDownload({ url: '/system/dict-type/export', method: 'GET', params, responseType: 'blob' })
|
||||
}
|
||||
|
||||
@@ -1,49 +1,50 @@
|
||||
import request from '@/config/axios'
|
||||
import qs from 'qs'
|
||||
|
||||
export interface NotifyMessageVO {
|
||||
id: number
|
||||
userId: number
|
||||
userType: number
|
||||
templateId: number
|
||||
templateCode: string
|
||||
templateNickname: string
|
||||
templateContent: string
|
||||
templateType: number
|
||||
templateParams: string
|
||||
readStatus: boolean
|
||||
readTime: Date
|
||||
createTime: Date
|
||||
}
|
||||
|
||||
// 查询站内信消息列表
|
||||
export const getNotifyMessagePage = async (params: PageParam) => {
|
||||
return await request.get({ url: '/system/notify-message/page', params })
|
||||
}
|
||||
|
||||
// 获得我的站内信分页
|
||||
export const getMyNotifyMessagePage = async (params: PageParam) => {
|
||||
return await request.get({ url: '/system/notify-message/my-page', params })
|
||||
}
|
||||
|
||||
// 批量标记已读
|
||||
export const updateNotifyMessageRead = async (ids) => {
|
||||
return await request.put({
|
||||
url: '/system/notify-message/update-read?' + qs.stringify({ ids: ids }, { indices: false })
|
||||
})
|
||||
}
|
||||
|
||||
// 标记所有站内信为已读
|
||||
export const updateAllNotifyMessageRead = async () => {
|
||||
return await request.put({ url: '/system/notify-message/update-all-read' })
|
||||
}
|
||||
|
||||
// 获取当前用户的最新站内信列表
|
||||
export const getUnreadNotifyMessageList = async () => {
|
||||
return await request.get({ url: '/system/notify-message/get-unread-list' })
|
||||
}
|
||||
|
||||
// 获得当前用户的未读站内信数量
|
||||
export const getUnreadNotifyMessageCount = async () => {
|
||||
return await request.get({ url: '/system/notify-message/get-unread-count' })
|
||||
}
|
||||
import { requestData } from '@/utils/request'
|
||||
import qs from 'qs'
|
||||
|
||||
export interface NotifyMessageVO {
|
||||
id: number
|
||||
userId: number
|
||||
userType: number
|
||||
templateId: number
|
||||
templateCode: string
|
||||
templateNickname: string
|
||||
templateContent: string
|
||||
templateType: number
|
||||
templateParams: string
|
||||
readStatus: boolean
|
||||
readTime: Date
|
||||
createTime: Date
|
||||
}
|
||||
|
||||
// 查询站内信消息列表
|
||||
export const getNotifyMessagePage = async (params: any) => {
|
||||
return await requestData({ url: '/system/notify-message/page', method: 'GET', params })
|
||||
}
|
||||
|
||||
// 获得我的站内信分页
|
||||
export const getMyNotifyMessagePage = async (params: any) => {
|
||||
return await requestData({ url: '/system/notify-message/my-page', method: 'GET', params })
|
||||
}
|
||||
|
||||
// 批量标记已读
|
||||
export const updateNotifyMessageRead = async (ids: any) => {
|
||||
return await requestData({
|
||||
url: '/system/notify-message/update-read?' + qs.stringify({ ids: ids }, { indices: false }),
|
||||
method: 'PUT'
|
||||
})
|
||||
}
|
||||
|
||||
// 标记所有站内信为已读
|
||||
export const updateAllNotifyMessageRead = async () => {
|
||||
return await requestData({ url: '/system/notify-message/update-all-read', method: 'PUT' })
|
||||
}
|
||||
|
||||
// 获取当前用户的最新站内信列表
|
||||
export const getUnreadNotifyMessageList = async () => {
|
||||
return await requestData({ url: '/system/notify-message/get-unread-list', method: 'GET' })
|
||||
}
|
||||
|
||||
// 获得当前用户的未读站内信数量
|
||||
export const getUnreadNotifyMessageCount = async () => {
|
||||
return await requestData({ url: '/system/notify-message/get-unread-count', method: 'GET' })
|
||||
}
|
||||
|
||||
@@ -1,49 +1,49 @@
|
||||
import request from '@/config/axios'
|
||||
|
||||
export interface NotifyTemplateVO {
|
||||
id?: number
|
||||
name: string
|
||||
nickname: string
|
||||
code: string
|
||||
content: string
|
||||
type?: number
|
||||
params: string
|
||||
status: number
|
||||
remark: string
|
||||
}
|
||||
|
||||
export interface NotifySendReqVO {
|
||||
userId: number | null
|
||||
templateCode: string
|
||||
templateParams: Map<String, Object>
|
||||
}
|
||||
|
||||
// 查询站内信模板列表
|
||||
export const getNotifyTemplatePage = async (params: PageParam) => {
|
||||
return await request.get({ url: '/system/notify-template/page', params })
|
||||
}
|
||||
|
||||
// 查询站内信模板详情
|
||||
export const getNotifyTemplate = async (id: number) => {
|
||||
return await request.get({ url: '/system/notify-template/get?id=' + id })
|
||||
}
|
||||
|
||||
// 新增站内信模板
|
||||
export const createNotifyTemplate = async (data: NotifyTemplateVO) => {
|
||||
return await request.post({ url: '/system/notify-template/create', data })
|
||||
}
|
||||
|
||||
// 修改站内信模板
|
||||
export const updateNotifyTemplate = async (data: NotifyTemplateVO) => {
|
||||
return await request.put({ url: '/system/notify-template/update', data })
|
||||
}
|
||||
|
||||
// 删除站内信模板
|
||||
export const deleteNotifyTemplate = async (id: number) => {
|
||||
return await request.delete({ url: '/system/notify-template/delete?id=' + id })
|
||||
}
|
||||
|
||||
// 发送站内信
|
||||
export const sendNotify = (data: NotifySendReqVO) => {
|
||||
return request.post({ url: '/system/notify-template/send-notify', data })
|
||||
}
|
||||
import { requestData } from '@/utils/request'
|
||||
|
||||
export interface NotifyTemplateVO {
|
||||
id?: number
|
||||
name: string
|
||||
nickname: string
|
||||
code: string
|
||||
content: string
|
||||
type?: number
|
||||
params: string
|
||||
status: number
|
||||
remark: string
|
||||
}
|
||||
|
||||
export interface NotifySendReqVO {
|
||||
userId: number | null
|
||||
templateCode: string
|
||||
templateParams: Map<String, Object>
|
||||
}
|
||||
|
||||
// 查询站内信模板列表
|
||||
export const getNotifyTemplatePage = async (params: PageParam) => {
|
||||
return await requestData({ url: '/system/notify-template/page', method: 'GET', params })
|
||||
}
|
||||
|
||||
// 查询站内信模板详情
|
||||
export const getNotifyTemplate = async (id: number) => {
|
||||
return await requestData({ url: '/system/notify-template/get?id=' + id, method: 'GET' })
|
||||
}
|
||||
|
||||
// 新增站内信模板
|
||||
export const createNotifyTemplate = async (data: NotifyTemplateVO) => {
|
||||
return await requestData({ url: '/system/notify-template/create', method: 'POST', data })
|
||||
}
|
||||
|
||||
// 修改站内信模板
|
||||
export const updateNotifyTemplate = async (data: NotifyTemplateVO) => {
|
||||
return await requestData({ url: '/system/notify-template/update', method: 'PUT', data })
|
||||
}
|
||||
|
||||
// 删除站内信模板
|
||||
export const deleteNotifyTemplate = async (id: number) => {
|
||||
return await requestData({ url: '/system/notify-template/delete?id=' + id, method: 'DELETE' })
|
||||
}
|
||||
|
||||
// 发送站内信
|
||||
export const sendNotify = (data: NotifySendReqVO) => {
|
||||
return requestData({ url: '/system/notify-template/send-notify', method: 'POST', data })
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ export const defaultAttribute: VxeTableProps = {
|
||||
border: true,
|
||||
stripe: true,
|
||||
size: 'small',
|
||||
columnConfig: { resizable: true },
|
||||
columnConfig: { resizable: true, useKey: true },
|
||||
rowConfig: { isCurrent: true, isHover: true },
|
||||
scrollX: { scrollToLeftOnChange: true },
|
||||
scrollY: { scrollToTopOnChange: true, enabled: true, gt: 100 },
|
||||
|
||||
@@ -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 }
|
||||
@@ -1,6 +0,0 @@
|
||||
export default {
|
||||
'401': '认证失败,无法访问系统资源',
|
||||
'403': '当前操作没有权限',
|
||||
'404': '访问资源不存在',
|
||||
default: '系统未知错误,请反馈给管理员'
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
@@ -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> </div>' +
|
||||
'<div>参考 https://doc.iocoder.cn/ 教程</div>' +
|
||||
'<div> </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 }
|
||||
@@ -1,35 +1,36 @@
|
||||
import { i18n } from '@/plugins/vueI18n'
|
||||
import { useLocaleStoreWithOut } from '@/stores/modules/locale'
|
||||
import { setHtmlPageLang } from '@/plugins/vueI18n/helper'
|
||||
|
||||
const setI18nLanguage = (locale: LocaleType) => {
|
||||
const localeStore = useLocaleStoreWithOut()
|
||||
|
||||
if (i18n.mode === 'legacy') {
|
||||
i18n.global.locale = locale
|
||||
} else {
|
||||
;(i18n.global.locale as any).value = locale
|
||||
}
|
||||
localeStore.setCurrentLocale({
|
||||
lang: locale
|
||||
})
|
||||
setHtmlPageLang(locale)
|
||||
}
|
||||
|
||||
export const useLocale = () => {
|
||||
// Switching the language will change the locale of useI18n
|
||||
// And submit to configuration modification
|
||||
const changeLocale = async (locale: LocaleType) => {
|
||||
const globalI18n = i18n.global
|
||||
|
||||
const langModule = await import(`../../locales/${locale}.ts`)
|
||||
|
||||
globalI18n.setLocaleMessage(locale, langModule.default)
|
||||
|
||||
setI18nLanguage(locale)
|
||||
}
|
||||
|
||||
return {
|
||||
changeLocale
|
||||
}
|
||||
}
|
||||
import { i18n } from '@/plugins/vueI18n'
|
||||
import { useLocaleStoreWithOut } from '@/stores/modules/locale'
|
||||
import { setHtmlPageLang } from '@/plugins/vueI18n/helper'
|
||||
import { loadLocaleMessages } from '@/locales'
|
||||
|
||||
const setI18nLanguage = (locale: LocaleType) => {
|
||||
const localeStore = useLocaleStoreWithOut()
|
||||
|
||||
if (i18n.mode === 'legacy') {
|
||||
i18n.global.locale = locale
|
||||
} else {
|
||||
;(i18n.global.locale as any).value = locale
|
||||
}
|
||||
localeStore.setCurrentLocale({
|
||||
lang: locale
|
||||
})
|
||||
setHtmlPageLang(locale)
|
||||
}
|
||||
|
||||
export const useLocale = () => {
|
||||
// Switching the language will change the locale of useI18n
|
||||
// And submit to configuration modification
|
||||
const changeLocale = async (locale: LocaleType) => {
|
||||
const globalI18n = i18n.global
|
||||
|
||||
const message = await loadLocaleMessages(locale)
|
||||
|
||||
globalI18n.setLocaleMessage(locale, message)
|
||||
|
||||
setI18nLanguage(locale)
|
||||
}
|
||||
|
||||
return {
|
||||
changeLocale
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -1,3 +0,0 @@
|
||||
import Breadcrumb from './src/Breadcrumb.vue'
|
||||
|
||||
export { Breadcrumb }
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import Collapse from './src/Collapse.vue'
|
||||
|
||||
export { Collapse }
|
||||
@@ -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>
|
||||
@@ -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 }
|
||||
@@ -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>
|
||||
@@ -1,3 +0,0 @@
|
||||
import Footer from './src/Footer.vue'
|
||||
|
||||
export { Footer }
|
||||
@@ -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>
|
||||
@@ -1,3 +0,0 @@
|
||||
import LocaleDropdown from './src/LocaleDropdown.vue'
|
||||
|
||||
export { LocaleDropdown }
|
||||
@@ -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>
|
||||
@@ -1,3 +0,0 @@
|
||||
import Logo from './src/Logo.vue'
|
||||
|
||||
export { Logo }
|
||||
@@ -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>
|
||||
@@ -1,3 +0,0 @@
|
||||
import Menu from './src/Menu.vue'
|
||||
|
||||
export { Menu }
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import Screenfull from './src/Screenfull.vue'
|
||||
|
||||
export { Screenfull }
|
||||
@@ -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>
|
||||
@@ -1,3 +0,0 @@
|
||||
import Setting from './src/Setting.vue'
|
||||
|
||||
export { Setting }
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -1,3 +0,0 @@
|
||||
import SizeDropdown from './src/SizeDropdown.vue'
|
||||
|
||||
export { SizeDropdown }
|
||||
@@ -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>
|
||||
@@ -1,3 +0,0 @@
|
||||
import TabMenu from './src/TabMenu.vue'
|
||||
|
||||
export { TabMenu }
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import TagsView from './src/TagsView.vue'
|
||||
|
||||
export { TagsView }
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import ThemeSwitch from './src/ThemeSwitch.vue'
|
||||
|
||||
export { ThemeSwitch }
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -1,3 +0,0 @@
|
||||
import UserInfo from './src/UserInfo.vue'
|
||||
|
||||
export { UserInfo }
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -1,306 +0,0 @@
|
||||
import { computed } from 'vue'
|
||||
import { useAppStore } from '@/stores/modules/app'
|
||||
import { Menu } from '@/layouts/components/Menu'
|
||||
import { TabMenu } from '@/layouts/components/TabMenu'
|
||||
import { TagsView } from '@/layouts/components/TagsView'
|
||||
import { Logo } from '@/layouts/components/Logo'
|
||||
import AppView from './AppView.vue'
|
||||
import ToolHeader from './ToolHeader.vue'
|
||||
import { ElScrollbar } from 'element-plus'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
const prefixCls = getPrefixCls('layout')
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const pageLoading = computed(() => appStore.getPageLoading)
|
||||
|
||||
// 标签页
|
||||
const tagsView = computed(() => appStore.getTagsView)
|
||||
|
||||
// 菜单折叠
|
||||
const collapse = computed(() => appStore.getCollapse)
|
||||
|
||||
// logo
|
||||
const logo = computed(() => appStore.logo)
|
||||
|
||||
// 固定头部
|
||||
const fixedHeader = computed(() => appStore.getFixedHeader)
|
||||
|
||||
// 是否是移动端
|
||||
const mobile = computed(() => appStore.getMobile)
|
||||
|
||||
// 固定菜单
|
||||
const fixedMenu = computed(() => appStore.getFixedMenu)
|
||||
|
||||
export const useRenderLayout = () => {
|
||||
const renderClassic = () => {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
class={[
|
||||
'absolute top-0 left-0 h-full layout-border__right',
|
||||
{ '!fixed z-3000': mobile.value }
|
||||
]}
|
||||
>
|
||||
{logo.value ? (
|
||||
<Logo
|
||||
class={[
|
||||
'bg-[var(--left-menu-bg-color)] relative',
|
||||
{
|
||||
'!pl-0': mobile.value && collapse.value,
|
||||
'w-[var(--left-menu-min-width)]': appStore.getCollapse,
|
||||
'w-[var(--left-menu-max-width)]': !appStore.getCollapse
|
||||
}
|
||||
]}
|
||||
style="transition: all var(--transition-time-02);"
|
||||
></Logo>
|
||||
) : undefined}
|
||||
<Menu class={[{ '!h-[calc(100%-var(--logo-height))]': logo.value }]}></Menu>
|
||||
</div>
|
||||
<div
|
||||
class={[
|
||||
`${prefixCls}-content`,
|
||||
'absolute top-0 h-[100%]',
|
||||
{
|
||||
'w-[calc(100%-var(--left-menu-min-width))] left-[var(--left-menu-min-width)]':
|
||||
collapse.value && !mobile.value && !mobile.value,
|
||||
'w-[calc(100%-var(--left-menu-max-width))] left-[var(--left-menu-max-width)]':
|
||||
!collapse.value && !mobile.value && !mobile.value,
|
||||
'fixed !w-full !left-0': mobile.value
|
||||
}
|
||||
]}
|
||||
style="transition: all var(--transition-time-02);"
|
||||
>
|
||||
<ElScrollbar
|
||||
v-loading={pageLoading.value}
|
||||
class={[
|
||||
`${prefixCls}-content-scrollbar`,
|
||||
{
|
||||
'!h-[calc(100%-var(--top-tool-height)-var(--tags-view-height))] mt-[calc(var(--top-tool-height)+var(--tags-view-height))]':
|
||||
fixedHeader.value
|
||||
}
|
||||
]}
|
||||
>
|
||||
<div
|
||||
class={[
|
||||
{
|
||||
'fixed top-0 left-0 z-10': fixedHeader.value,
|
||||
'w-[calc(100%-var(--left-menu-min-width))] !left-[var(--left-menu-min-width)]':
|
||||
collapse.value && fixedHeader.value && !mobile.value,
|
||||
'w-[calc(100%-var(--left-menu-max-width))] !left-[var(--left-menu-max-width)]':
|
||||
!collapse.value && fixedHeader.value && !mobile.value,
|
||||
'!w-full !left-0': mobile.value
|
||||
}
|
||||
]}
|
||||
style="transition: all var(--transition-time-02);"
|
||||
>
|
||||
<ToolHeader
|
||||
class={[
|
||||
'bg-[var(--top-header-bg-color)]',
|
||||
{
|
||||
'layout-border__bottom': !tagsView.value
|
||||
}
|
||||
]}
|
||||
></ToolHeader>
|
||||
|
||||
{tagsView.value ? (
|
||||
<TagsView class="layout-border__top layout-border__bottom"></TagsView>
|
||||
) : undefined}
|
||||
</div>
|
||||
|
||||
<AppView></AppView>
|
||||
</ElScrollbar>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const renderTopLeft = () => {
|
||||
return (
|
||||
<>
|
||||
<div class="relative flex items-center bg-[var(--top-header-bg-color)] layout-border__bottom dark:bg-[var(--el-bg-color)]">
|
||||
{logo.value ? <Logo class="custom-hover"></Logo> : undefined}
|
||||
|
||||
<ToolHeader class="flex-1"></ToolHeader>
|
||||
</div>
|
||||
<div class="absolute left-0 top-[var(--logo-height)+1px] h-[calc(100%-1px-var(--logo-height))] w-full flex">
|
||||
<Menu class="relative layout-border__right !h-full"></Menu>
|
||||
<div
|
||||
class={[
|
||||
`${prefixCls}-content`,
|
||||
'h-[100%]',
|
||||
{
|
||||
'w-[calc(100%-var(--left-menu-min-width))] left-[var(--left-menu-min-width)]':
|
||||
collapse.value,
|
||||
'w-[calc(100%-var(--left-menu-max-width))] left-[var(--left-menu-max-width)]':
|
||||
!collapse.value
|
||||
}
|
||||
]}
|
||||
style="transition: all var(--transition-time-02);"
|
||||
>
|
||||
<ElScrollbar
|
||||
v-loading={pageLoading.value}
|
||||
class={[
|
||||
`${prefixCls}-content-scrollbar`,
|
||||
{
|
||||
'!h-[calc(100%-var(--tags-view-height))] mt-[calc(var(--tags-view-height))]':
|
||||
fixedHeader.value && tagsView.value
|
||||
}
|
||||
]}
|
||||
>
|
||||
{tagsView.value ? (
|
||||
<TagsView
|
||||
class={[
|
||||
'layout-border__bottom absolute',
|
||||
{
|
||||
'!fixed top-0 left-0 z-10': fixedHeader.value,
|
||||
'w-[calc(100%-var(--left-menu-min-width))] !left-[var(--left-menu-min-width)] mt-[calc(var(--logo-height)+1px)]':
|
||||
collapse.value && fixedHeader.value,
|
||||
'w-[calc(100%-var(--left-menu-max-width))] !left-[var(--left-menu-max-width)] mt-[calc(var(--logo-height)+1px)]':
|
||||
!collapse.value && fixedHeader.value
|
||||
}
|
||||
]}
|
||||
style="transition: width var(--transition-time-02), left var(--transition-time-02);"
|
||||
></TagsView>
|
||||
) : undefined}
|
||||
|
||||
<AppView></AppView>
|
||||
</ElScrollbar>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const renderTop = () => {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
class={[
|
||||
'flex items-center justify-between bg-[var(--top-header-bg-color)] relative',
|
||||
{
|
||||
'layout-border__bottom': !tagsView.value
|
||||
}
|
||||
]}
|
||||
>
|
||||
{logo.value ? <Logo class="custom-hover"></Logo> : undefined}
|
||||
<Menu class="h-[var(--top-tool-height)] flex-1 px-10px"></Menu>
|
||||
<ToolHeader></ToolHeader>
|
||||
</div>
|
||||
<div
|
||||
class={[
|
||||
`${prefixCls}-content`,
|
||||
'w-full',
|
||||
{
|
||||
'h-[calc(100%-var(--app-footer-height))]': !fixedHeader.value,
|
||||
'h-[calc(100%-var(--tags-view-height)-var(--app-footer-height))]': fixedHeader.value
|
||||
}
|
||||
]}
|
||||
>
|
||||
<ElScrollbar
|
||||
v-loading={pageLoading.value}
|
||||
class={[
|
||||
`${prefixCls}-content-scrollbar`,
|
||||
{
|
||||
'mt-[var(--tags-view-height)] !pb-[calc(var(--tags-view-height)+var(--app-footer-height))]':
|
||||
fixedHeader.value,
|
||||
'pb-[var(--app-footer-height)]': !fixedHeader.value
|
||||
}
|
||||
]}
|
||||
>
|
||||
{tagsView.value ? (
|
||||
<TagsView
|
||||
class={[
|
||||
'layout-border__bottom layout-border__top relative',
|
||||
{
|
||||
'!fixed w-full top-[calc(var(--top-tool-height)+1px)] left-0': fixedHeader.value
|
||||
}
|
||||
]}
|
||||
style="transition: width var(--transition-time-02), left var(--transition-time-02);"
|
||||
></TagsView>
|
||||
) : undefined}
|
||||
|
||||
<AppView></AppView>
|
||||
</ElScrollbar>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const renderCutMenu = () => {
|
||||
return (
|
||||
<>
|
||||
<div class="relative flex items-center bg-[var(--top-header-bg-color)] layout-border__bottom">
|
||||
{logo.value ? <Logo class="custom-hover !pr-15px"></Logo> : undefined}
|
||||
|
||||
<ToolHeader class="flex-1"></ToolHeader>
|
||||
</div>
|
||||
<div class="absolute left-0 top-[var(--logo-height)] h-[calc(100%-var(--logo-height))] w-[calc(100%-2px)] flex">
|
||||
<TabMenu></TabMenu>
|
||||
<div
|
||||
class={[
|
||||
`${prefixCls}-content`,
|
||||
'h-[100%]',
|
||||
{
|
||||
'w-[calc(100%-var(--tab-menu-min-width))] left-[var(--tab-menu-min-width)]':
|
||||
collapse.value && !fixedMenu.value,
|
||||
'w-[calc(100%-var(--tab-menu-max-width))] left-[var(--tab-menu-max-width)]':
|
||||
!collapse.value && !fixedMenu.value,
|
||||
'w-[calc(100%-var(--tab-menu-min-width)-var(--left-menu-max-width))] ml-[var(--left-menu-max-width)]':
|
||||
collapse.value && fixedMenu.value,
|
||||
'w-[calc(100%-var(--tab-menu-max-width)-var(--left-menu-max-width))] ml-[var(--left-menu-max-width)]':
|
||||
!collapse.value && fixedMenu.value
|
||||
}
|
||||
]}
|
||||
style="transition: all var(--transition-time-02);"
|
||||
>
|
||||
<ElScrollbar
|
||||
v-loading={pageLoading.value}
|
||||
class={[
|
||||
`${prefixCls}-content-scrollbar`,
|
||||
{
|
||||
'!h-[calc(100%-var(--tags-view-height))] mt-[calc(var(--tags-view-height))]':
|
||||
fixedHeader.value && tagsView.value
|
||||
}
|
||||
]}
|
||||
>
|
||||
{tagsView.value ? (
|
||||
<TagsView
|
||||
class={[
|
||||
'relative layout-border__bottom layout-border__top',
|
||||
{
|
||||
'!fixed top-0 left-0 z-10': fixedHeader.value,
|
||||
'w-[calc(100%-var(--tab-menu-min-width))] !left-[var(--tab-menu-min-width)] mt-[var(--logo-height)]':
|
||||
collapse.value && fixedHeader.value,
|
||||
'w-[calc(100%-var(--tab-menu-max-width))] !left-[var(--tab-menu-max-width)] mt-[var(--logo-height)]':
|
||||
!collapse.value && fixedHeader.value,
|
||||
'!fixed top-0 !left-[var(--tab-menu-min-width)+var(--left-menu-max-width)] z-10':
|
||||
fixedHeader.value && fixedMenu.value,
|
||||
'w-[calc(100%-var(--tab-menu-min-width)-var(--left-menu-max-width))] !left-[var(--tab-menu-min-width)+var(--left-menu-max-width)] mt-[var(--logo-height)]':
|
||||
collapse.value && fixedHeader.value && fixedMenu.value,
|
||||
'w-[calc(100%-var(--tab-menu-max-width)-var(--left-menu-max-width))] !left-[var(--tab-menu-max-width)+var(--left-menu-max-width)] mt-[var(--logo-height)]':
|
||||
!collapse.value && fixedHeader.value && fixedMenu.value
|
||||
}
|
||||
]}
|
||||
style="transition: width var(--transition-time-02), left var(--transition-time-02);"
|
||||
></TagsView>
|
||||
) : undefined}
|
||||
|
||||
<AppView></AppView>
|
||||
</ElScrollbar>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
renderClassic,
|
||||
renderTopLeft,
|
||||
renderTop,
|
||||
renderCutMenu
|
||||
}
|
||||
}
|
||||
11
src/locales/index.ts
Normal file
11
src/locales/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
const localeModules = import.meta.glob<{ default: Record<string, unknown> }>('./*.ts')
|
||||
|
||||
export async function loadLocaleMessages(locale: string) {
|
||||
const path = `./${locale}.ts`
|
||||
const loader = localeModules[path]
|
||||
if (!loader) {
|
||||
throw new Error(`Locale not found: ${locale}`)
|
||||
}
|
||||
const mod = await loader()
|
||||
return mod.default ?? {}
|
||||
}
|
||||
11
src/main.ts
11
src/main.ts
@@ -1,7 +1,7 @@
|
||||
import { createApp, reactive } from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import pinia from '@/stores/index'
|
||||
import { setupStore } from '@/stores/index'
|
||||
import { registerIcons } from '@/utils/common'
|
||||
import mitt from 'mitt'
|
||||
import VxeUI from 'vxe-pc-ui'
|
||||
@@ -25,6 +25,13 @@ import VXETablePluginExportXLSX from 'vxe-table-plugin-export-xlsx'
|
||||
VXETable.use(VXETablePluginExportXLSX, {
|
||||
ExcelJS
|
||||
})
|
||||
VxeUI.setConfig({
|
||||
table: {
|
||||
columnConfig: {
|
||||
useKey: true
|
||||
}
|
||||
}
|
||||
})
|
||||
// 全局注册 tooltip 组件
|
||||
window.XEUtils = XEUtils
|
||||
|
||||
@@ -62,7 +69,7 @@ const setupAll = async () => {
|
||||
|
||||
await setupI18n(app)
|
||||
app.use(router)
|
||||
app.use(pinia)
|
||||
setupStore(app)
|
||||
app.use(VxeUI)
|
||||
app.use(ElementPlus)
|
||||
app.use(VXETable)
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
import type { App } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import { useLocaleStoreWithOut } from '@/stores/modules/locale'
|
||||
import type { I18n, I18nOptions } from 'vue-i18n'
|
||||
import { setHtmlPageLang } from './helper'
|
||||
|
||||
export let i18n: ReturnType<typeof createI18n>
|
||||
|
||||
const createI18nOptions = async (): Promise<I18nOptions> => {
|
||||
const localeStore = useLocaleStoreWithOut()
|
||||
const locale = localeStore.getCurrentLocale
|
||||
const localeMap = localeStore.getLocaleMap
|
||||
const defaultLocal = await import(`../../locales/${locale.lang}.ts`)
|
||||
const message = defaultLocal.default ?? {}
|
||||
|
||||
setHtmlPageLang(locale.lang)
|
||||
|
||||
localeStore.setCurrentLocale({
|
||||
lang: locale.lang
|
||||
// elLocale: elLocal
|
||||
})
|
||||
|
||||
return {
|
||||
legacy: false,
|
||||
locale: locale.lang,
|
||||
fallbackLocale: locale.lang,
|
||||
messages: {
|
||||
[locale.lang]: message
|
||||
},
|
||||
availableLocales: localeMap.map((v) => v.lang),
|
||||
sync: true,
|
||||
silentTranslationWarn: true,
|
||||
missingWarn: false,
|
||||
silentFallbackWarn: true
|
||||
}
|
||||
}
|
||||
|
||||
export const setupI18n = async (app: App<Element>) => {
|
||||
const options = await createI18nOptions()
|
||||
i18n = createI18n(options) as I18n
|
||||
app.use(i18n)
|
||||
}
|
||||
import type { App } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import { useLocaleStoreWithOut } from '@/stores/modules/locale'
|
||||
import type { I18n, I18nOptions } from 'vue-i18n'
|
||||
import { setHtmlPageLang } from './helper'
|
||||
import { loadLocaleMessages } from '@/locales'
|
||||
|
||||
export let i18n: ReturnType<typeof createI18n>
|
||||
|
||||
const createI18nOptions = async (): Promise<I18nOptions> => {
|
||||
const localeStore = useLocaleStoreWithOut()
|
||||
const locale = localeStore.getCurrentLocale
|
||||
const localeMap = localeStore.getLocaleMap
|
||||
const message = await loadLocaleMessages(locale.lang)
|
||||
|
||||
setHtmlPageLang(locale.lang)
|
||||
|
||||
localeStore.setCurrentLocale({
|
||||
lang: locale.lang
|
||||
// elLocale: elLocal
|
||||
})
|
||||
|
||||
return {
|
||||
legacy: false,
|
||||
locale: locale.lang,
|
||||
fallbackLocale: locale.lang,
|
||||
messages: {
|
||||
[locale.lang]: message
|
||||
},
|
||||
availableLocales: localeMap.map((v) => v.lang),
|
||||
sync: true,
|
||||
silentTranslationWarn: true,
|
||||
missingWarn: false,
|
||||
silentFallbackWarn: true
|
||||
}
|
||||
}
|
||||
|
||||
export const setupI18n = async (app: App<Element>) => {
|
||||
const options = await createI18nOptions()
|
||||
i18n = createI18n(options) as I18n
|
||||
app.use(i18n)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import type { App } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
|
||||
|
||||
const pinia = createPinia()
|
||||
pinia.use(piniaPluginPersistedstate)
|
||||
|
||||
/** 供 setup 外(WithOut)使用的同一 Pinia 实例 */
|
||||
export const store = pinia
|
||||
|
||||
export const setupStore = (app: App<Element>) => {
|
||||
app.use(pinia)
|
||||
}
|
||||
|
||||
export default pinia
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import type { App } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
|
||||
|
||||
const store = createPinia()
|
||||
store.use(piniaPluginPersistedstate)
|
||||
|
||||
export const setupStore = (app: App<Element>) => {
|
||||
app.use(store)
|
||||
}
|
||||
|
||||
export { store }
|
||||
@@ -1,277 +0,0 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { store } from '../indexs'
|
||||
import { setCssVar, humpToUnderline } from '@/utils'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
|
||||
import { ElementPlusSize } from '@/types/elementPlus'
|
||||
import { LayoutType } from '@/types/layout'
|
||||
import { ThemeTypes } from '@/types/theme'
|
||||
|
||||
const { wsCache } = useCache()
|
||||
|
||||
interface AppState {
|
||||
breadcrumb: boolean
|
||||
breadcrumbIcon: boolean
|
||||
collapse: boolean
|
||||
uniqueOpened: boolean
|
||||
hamburger: boolean
|
||||
screenfull: boolean
|
||||
search: boolean
|
||||
size: boolean
|
||||
locale: boolean
|
||||
message: boolean
|
||||
tagsView: boolean
|
||||
tagsViewIcon: boolean
|
||||
logo: boolean
|
||||
fixedHeader: boolean
|
||||
greyMode: boolean
|
||||
pageLoading: boolean
|
||||
layout: LayoutType
|
||||
title: string
|
||||
userInfo: string
|
||||
isDark: boolean
|
||||
currentSize: ElementPlusSize
|
||||
sizeMap: ElementPlusSize[]
|
||||
mobile: boolean
|
||||
footer: boolean
|
||||
theme: ThemeTypes
|
||||
fixedMenu: boolean
|
||||
}
|
||||
|
||||
export const useAppStore = defineStore('app', {
|
||||
state: (): AppState => {
|
||||
return {
|
||||
userInfo: 'userInfo', // 登录信息存储字段-建议每个项目换一个字段,避免与其他项目冲突
|
||||
sizeMap: ['default', 'large', 'small'],
|
||||
mobile: false, // 是否是移动端
|
||||
title: import.meta.env.VITE_APP_TITLE, // 标题
|
||||
pageLoading: false, // 路由跳转loading
|
||||
|
||||
breadcrumb: true, // 面包屑
|
||||
breadcrumbIcon: true, // 面包屑图标
|
||||
collapse: false, // 折叠菜单
|
||||
uniqueOpened: true, // 是否只保持一个子菜单的展开
|
||||
hamburger: true, // 折叠图标
|
||||
screenfull: true, // 全屏图标
|
||||
search: true, // 搜索图标
|
||||
size: true, // 尺寸图标
|
||||
locale: true, // 多语言图标
|
||||
message: true, // 消息图标
|
||||
tagsView: true, // 标签页
|
||||
tagsViewIcon: true, // 是否显示标签图标
|
||||
logo: true, // logo
|
||||
fixedHeader: true, // 固定toolheader
|
||||
footer: true, // 显示页脚
|
||||
greyMode: false, // 是否开始灰色模式,用于特殊悼念日
|
||||
fixedMenu: wsCache.get('fixedMenu') || false, // 是否固定菜单
|
||||
|
||||
layout: wsCache.get(CACHE_KEY.LAYOUT) || 'classic', // layout布局
|
||||
isDark: wsCache.get(CACHE_KEY.IS_DARK) || false, // 是否是暗黑模式
|
||||
currentSize: wsCache.get('default') || 'default', // 组件尺寸
|
||||
theme: wsCache.get(CACHE_KEY.THEME) || {
|
||||
// 主题色
|
||||
elColorPrimary: '#409eff',
|
||||
// 左侧菜单边框颜色
|
||||
leftMenuBorderColor: 'inherit',
|
||||
// 左侧菜单背景颜色
|
||||
leftMenuBgColor: '#001529',
|
||||
// 左侧菜单浅色背景颜色
|
||||
leftMenuBgLightColor: '#0f2438',
|
||||
// 左侧菜单选中背景颜色
|
||||
leftMenuBgActiveColor: 'var(--el-color-primary)',
|
||||
// 左侧菜单收起选中背景颜色
|
||||
leftMenuCollapseBgActiveColor: 'var(--el-color-primary)',
|
||||
// 左侧菜单字体颜色
|
||||
leftMenuTextColor: '#bfcbd9',
|
||||
// 左侧菜单选中字体颜色
|
||||
leftMenuTextActiveColor: '#fff',
|
||||
// logo字体颜色
|
||||
logoTitleTextColor: '#fff',
|
||||
// logo边框颜色
|
||||
logoBorderColor: 'inherit',
|
||||
// 头部背景颜色
|
||||
topHeaderBgColor: '#fff',
|
||||
// 头部字体颜色
|
||||
topHeaderTextColor: 'inherit',
|
||||
// 头部悬停颜色
|
||||
topHeaderHoverColor: '#f6f6f6',
|
||||
// 头部边框颜色
|
||||
topToolBorderColor: '#eee'
|
||||
}
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
getBreadcrumb(): boolean {
|
||||
return this.breadcrumb
|
||||
},
|
||||
getBreadcrumbIcon(): boolean {
|
||||
return this.breadcrumbIcon
|
||||
},
|
||||
getCollapse(): boolean {
|
||||
return this.collapse
|
||||
},
|
||||
getUniqueOpened(): boolean {
|
||||
return this.uniqueOpened
|
||||
},
|
||||
getHamburger(): boolean {
|
||||
return this.hamburger
|
||||
},
|
||||
getScreenfull(): boolean {
|
||||
return this.screenfull
|
||||
},
|
||||
getSize(): boolean {
|
||||
return this.size
|
||||
},
|
||||
getLocale(): boolean {
|
||||
return this.locale
|
||||
},
|
||||
getMessage(): boolean {
|
||||
return this.message
|
||||
},
|
||||
getTagsView(): boolean {
|
||||
return this.tagsView
|
||||
},
|
||||
getTagsViewIcon(): boolean {
|
||||
return this.tagsViewIcon
|
||||
},
|
||||
getLogo(): boolean {
|
||||
return this.logo
|
||||
},
|
||||
getFixedHeader(): boolean {
|
||||
return this.fixedHeader
|
||||
},
|
||||
getGreyMode(): boolean {
|
||||
return this.greyMode
|
||||
},
|
||||
getFixedMenu(): boolean {
|
||||
return this.fixedMenu
|
||||
},
|
||||
getPageLoading(): boolean {
|
||||
return this.pageLoading
|
||||
},
|
||||
getLayout(): LayoutType {
|
||||
return this.layout
|
||||
},
|
||||
getTitle(): string {
|
||||
return this.title
|
||||
},
|
||||
getUserInfo(): string {
|
||||
return this.userInfo
|
||||
},
|
||||
getIsDark(): boolean {
|
||||
return this.isDark
|
||||
},
|
||||
getCurrentSize(): ElementPlusSize {
|
||||
return this.currentSize
|
||||
},
|
||||
getSizeMap(): ElementPlusSize[] {
|
||||
return this.sizeMap
|
||||
},
|
||||
getMobile(): boolean {
|
||||
return this.mobile
|
||||
},
|
||||
getTheme(): ThemeTypes {
|
||||
return this.theme
|
||||
},
|
||||
getFooter(): boolean {
|
||||
return this.footer
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
setBreadcrumb(breadcrumb: boolean) {
|
||||
this.breadcrumb = breadcrumb
|
||||
},
|
||||
setBreadcrumbIcon(breadcrumbIcon: boolean) {
|
||||
this.breadcrumbIcon = breadcrumbIcon
|
||||
},
|
||||
setCollapse(collapse: boolean) {
|
||||
this.collapse = collapse
|
||||
},
|
||||
setUniqueOpened(uniqueOpened: boolean) {
|
||||
this.uniqueOpened = uniqueOpened
|
||||
},
|
||||
setHamburger(hamburger: boolean) {
|
||||
this.hamburger = hamburger
|
||||
},
|
||||
setScreenfull(screenfull: boolean) {
|
||||
this.screenfull = screenfull
|
||||
},
|
||||
setSize(size: boolean) {
|
||||
this.size = size
|
||||
},
|
||||
setLocale(locale: boolean) {
|
||||
this.locale = locale
|
||||
},
|
||||
setMessage(message: boolean) {
|
||||
this.message = message
|
||||
},
|
||||
setTagsView(tagsView: boolean) {
|
||||
this.tagsView = tagsView
|
||||
},
|
||||
setTagsViewIcon(tagsViewIcon: boolean) {
|
||||
this.tagsViewIcon = tagsViewIcon
|
||||
},
|
||||
setLogo(logo: boolean) {
|
||||
this.logo = logo
|
||||
},
|
||||
setFixedHeader(fixedHeader: boolean) {
|
||||
this.fixedHeader = fixedHeader
|
||||
},
|
||||
setGreyMode(greyMode: boolean) {
|
||||
this.greyMode = greyMode
|
||||
},
|
||||
setFixedMenu(fixedMenu: boolean) {
|
||||
wsCache.set('fixedMenu', fixedMenu)
|
||||
this.fixedMenu = fixedMenu
|
||||
},
|
||||
setPageLoading(pageLoading: boolean) {
|
||||
this.pageLoading = pageLoading
|
||||
},
|
||||
setLayout(layout: LayoutType) {
|
||||
if (this.mobile && layout !== 'classic') {
|
||||
ElMessage.warning('移动端模式下不支持切换其他布局')
|
||||
return
|
||||
}
|
||||
this.layout = layout
|
||||
wsCache.set(CACHE_KEY.LAYOUT, this.layout)
|
||||
},
|
||||
setTitle(title: string) {
|
||||
this.title = title
|
||||
},
|
||||
setIsDark(isDark: boolean) {
|
||||
this.isDark = isDark
|
||||
if (this.isDark) {
|
||||
document.documentElement.classList.add('dark')
|
||||
document.documentElement.classList.remove('light')
|
||||
} else {
|
||||
document.documentElement.classList.add('light')
|
||||
document.documentElement.classList.remove('dark')
|
||||
}
|
||||
wsCache.set(CACHE_KEY.IS_DARK, this.isDark)
|
||||
},
|
||||
setCurrentSize(currentSize: ElementPlusSize) {
|
||||
this.currentSize = currentSize
|
||||
wsCache.set('currentSize', this.currentSize)
|
||||
},
|
||||
setMobile(mobile: boolean) {
|
||||
this.mobile = mobile
|
||||
},
|
||||
setTheme(theme: ThemeTypes) {
|
||||
this.theme = Object.assign(this.theme, theme)
|
||||
wsCache.set(CACHE_KEY.THEME, this.theme)
|
||||
},
|
||||
setCssVarTheme() {
|
||||
for (const key in this.theme) {
|
||||
setCssVar(`--${humpToUnderline(key)}`, this.theme[key])
|
||||
}
|
||||
},
|
||||
setFooter(footer: boolean) {
|
||||
this.footer = footer
|
||||
}
|
||||
},
|
||||
persist: false
|
||||
})
|
||||
|
||||
export const useAppStoreWithOut = () => {
|
||||
return useAppStore(store)
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { store } from '../indexs'
|
||||
// @ts-ignore
|
||||
import { DictDataVO } from '@/api/system/dict/types'
|
||||
import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
|
||||
const { wsCache } = useCache('sessionStorage')
|
||||
// import { getSimpleDictDataList } from '@/api/system/dict/dict.data'
|
||||
|
||||
export interface DictValueType {
|
||||
value: any
|
||||
label: string
|
||||
clorType?: string
|
||||
cssClass?: string
|
||||
}
|
||||
export interface DictTypeType {
|
||||
dictType: string
|
||||
dictValue: DictValueType[]
|
||||
}
|
||||
export interface DictState {
|
||||
dictMap: Map<string, any>
|
||||
isSetDict: boolean
|
||||
}
|
||||
|
||||
export const useDictStore = defineStore('dict', {
|
||||
state: (): DictState => ({
|
||||
dictMap: new Map<string, any>(),
|
||||
isSetDict: false
|
||||
}),
|
||||
getters: {
|
||||
getDictMap(): Recordable {
|
||||
const dictMap = wsCache.get(CACHE_KEY.DICT_CACHE)
|
||||
if (dictMap) {
|
||||
this.dictMap = dictMap
|
||||
}
|
||||
return this.dictMap
|
||||
},
|
||||
getIsSetDict(): boolean {
|
||||
return this.isSetDict
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
async setDictMap() {
|
||||
const dictMap = wsCache.get(CACHE_KEY.DICT_CACHE)
|
||||
if (dictMap) {
|
||||
this.dictMap = dictMap
|
||||
this.isSetDict = true
|
||||
} else {
|
||||
// const res = await getSimpleDictDataList()
|
||||
// 设置数据
|
||||
const dictDataMap = new Map<string, any>()
|
||||
// res.forEach((dictData: DictDataVO) => {
|
||||
// // 获得 dictType 层级
|
||||
// const enumValueObj = dictDataMap[dictData.dictType]
|
||||
// if (!enumValueObj) {
|
||||
// dictDataMap[dictData.dictType] = []
|
||||
// }
|
||||
// // 处理 dictValue 层级
|
||||
// dictDataMap[dictData.dictType].push({
|
||||
// value: dictData.value,
|
||||
// label: dictData.label,
|
||||
// colorType: dictData.colorType,
|
||||
// cssClass: dictData.cssClass
|
||||
// })
|
||||
// })
|
||||
this.dictMap = dictDataMap
|
||||
this.isSetDict = true
|
||||
wsCache.set(CACHE_KEY.DICT_CACHE, dictDataMap, { exp: 60 }) // 60 秒 过期
|
||||
}
|
||||
},
|
||||
getDictByType(type: string) {
|
||||
console.log("🚀 ~ type:", type)
|
||||
if (!this.isSetDict) {
|
||||
this.setDictMap()
|
||||
}
|
||||
return this.dictMap[type]
|
||||
},
|
||||
async resetDict() {
|
||||
wsCache.delete(CACHE_KEY.DICT_CACHE)
|
||||
// const res = await getSimpleDictDataList()
|
||||
// 设置数据
|
||||
const dictDataMap = new Map<string, any>()
|
||||
// res.forEach((dictData: DictDataVO) => {
|
||||
// // 获得 dictType 层级
|
||||
// const enumValueObj = dictDataMap[dictData.dictType]
|
||||
// if (!enumValueObj) {
|
||||
// dictDataMap[dictData.dictType] = []
|
||||
// }
|
||||
// // 处理 dictValue 层级
|
||||
// dictDataMap[dictData.dictType].push({
|
||||
// value: dictData.value,
|
||||
// label: dictData.label,
|
||||
// colorType: dictData.colorType,
|
||||
// cssClass: dictData.cssClass
|
||||
// })
|
||||
// })
|
||||
this.dictMap = dictDataMap
|
||||
this.isSetDict = true
|
||||
wsCache.set(CACHE_KEY.DICT_CACHE, dictDataMap, { exp: 60 }) // 60 秒 过期
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const useDictStoreWithOut = () => {
|
||||
return useDictStore(store)
|
||||
}
|
||||
@@ -1,59 +1,59 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { store } from '../indexs'
|
||||
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||
import en from 'element-plus/es/locale/lang/en'
|
||||
import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
|
||||
import { LocaleDropdownType } from '@/types/localeDropdown'
|
||||
|
||||
const { wsCache } = useCache()
|
||||
|
||||
const elLocaleMap = {
|
||||
'zh-CN': zhCn,
|
||||
en: en
|
||||
}
|
||||
interface LocaleState {
|
||||
currentLocale: LocaleDropdownType
|
||||
localeMap: LocaleDropdownType[]
|
||||
}
|
||||
|
||||
export const useLocaleStore = defineStore('locales', {
|
||||
state: (): LocaleState => {
|
||||
return {
|
||||
currentLocale: {
|
||||
lang: wsCache.get(CACHE_KEY.LANG) || 'zh-CN',
|
||||
elLocale: elLocaleMap[wsCache.get(CACHE_KEY.LANG) || 'zh-CN']
|
||||
},
|
||||
// 多语言
|
||||
localeMap: [
|
||||
{
|
||||
lang: 'zh-CN',
|
||||
name: '简体中文'
|
||||
},
|
||||
{
|
||||
lang: 'en',
|
||||
name: 'English'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
getCurrentLocale(): LocaleDropdownType {
|
||||
return this.currentLocale
|
||||
},
|
||||
getLocaleMap(): LocaleDropdownType[] {
|
||||
return this.localeMap
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
setCurrentLocale(localeMap: LocaleDropdownType) {
|
||||
// this.locale = Object.assign(this.locale, localeMap)
|
||||
this.currentLocale.lang = localeMap?.lang
|
||||
this.currentLocale.elLocale = elLocaleMap[localeMap?.lang]
|
||||
wsCache.set(CACHE_KEY.LANG, localeMap?.lang)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const useLocaleStoreWithOut = () => {
|
||||
return useLocaleStore(store)
|
||||
}
|
||||
import { defineStore } from 'pinia'
|
||||
import { store } from '@/stores/index'
|
||||
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||
import en from 'element-plus/es/locale/lang/en'
|
||||
import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
|
||||
import { LocaleDropdownType } from '@/types/localeDropdown'
|
||||
|
||||
const { wsCache } = useCache()
|
||||
|
||||
const elLocaleMap = {
|
||||
'zh-CN': zhCn,
|
||||
en: en
|
||||
}
|
||||
interface LocaleState {
|
||||
currentLocale: LocaleDropdownType
|
||||
localeMap: LocaleDropdownType[]
|
||||
}
|
||||
|
||||
export const useLocaleStore = defineStore('locales', {
|
||||
state: (): LocaleState => {
|
||||
return {
|
||||
currentLocale: {
|
||||
lang: wsCache.get(CACHE_KEY.LANG) || 'zh-CN',
|
||||
elLocale: elLocaleMap[wsCache.get(CACHE_KEY.LANG) || 'zh-CN']
|
||||
},
|
||||
// 多语言
|
||||
localeMap: [
|
||||
{
|
||||
lang: 'zh-CN',
|
||||
name: '简体中文'
|
||||
},
|
||||
{
|
||||
lang: 'en',
|
||||
name: 'English'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
getCurrentLocale(): LocaleDropdownType {
|
||||
return this.currentLocale
|
||||
},
|
||||
getLocaleMap(): LocaleDropdownType[] {
|
||||
return this.localeMap
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
setCurrentLocale(localeMap: LocaleDropdownType) {
|
||||
// this.locale = Object.assign(this.locale, localeMap)
|
||||
this.currentLocale.lang = localeMap?.lang
|
||||
this.currentLocale.elLocale = elLocaleMap[localeMap?.lang]
|
||||
wsCache.set(CACHE_KEY.LANG, localeMap?.lang)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const useLocaleStoreWithOut = () => {
|
||||
return useLocaleStore(store)
|
||||
}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { store } from '@/stores/indexs'
|
||||
|
||||
interface lockInfo {
|
||||
isLock?: boolean
|
||||
password?: string
|
||||
}
|
||||
|
||||
interface LockState {
|
||||
lockInfo: lockInfo
|
||||
}
|
||||
|
||||
export const useLockStore = defineStore('lock', {
|
||||
state: (): LockState => {
|
||||
return {
|
||||
lockInfo: {
|
||||
// isLock: false, // 是否锁定屏幕
|
||||
// password: '' // 锁屏密码
|
||||
}
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
getLockInfo(): lockInfo {
|
||||
return this.lockInfo
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
setLockInfo(lockInfo: lockInfo) {
|
||||
this.lockInfo = lockInfo
|
||||
},
|
||||
resetLockInfo() {
|
||||
this.lockInfo = {}
|
||||
},
|
||||
unLock(password: string) {
|
||||
if (this.lockInfo?.password === password) {
|
||||
this.resetLockInfo()
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
},
|
||||
persist: true
|
||||
})
|
||||
|
||||
export const useLockStoreWithOut = () => {
|
||||
return useLockStore(store)
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { store } from '@/stores/indexs'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import { flatMultiLevelRoutes, generateRoute } from '@/utils/routerHelper'
|
||||
import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
|
||||
|
||||
const { wsCache } = useCache()
|
||||
|
||||
export interface PermissionState {
|
||||
routers: AppRouteRecordRaw[]
|
||||
addRouters: AppRouteRecordRaw[]
|
||||
menuTabRouters: AppRouteRecordRaw[]
|
||||
}
|
||||
|
||||
export const usePermissionStore = defineStore('permission', {
|
||||
state: (): PermissionState => ({
|
||||
routers: [],
|
||||
addRouters: [],
|
||||
menuTabRouters: []
|
||||
}),
|
||||
getters: {
|
||||
getRouters(): AppRouteRecordRaw[] {
|
||||
return this.routers
|
||||
},
|
||||
getAddRouters(): AppRouteRecordRaw[] {
|
||||
return flatMultiLevelRoutes(cloneDeep(this.addRouters))
|
||||
},
|
||||
getMenuTabRouters(): AppRouteRecordRaw[] {
|
||||
return this.menuTabRouters
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
async generateRoutes(): Promise<unknown> {
|
||||
return new Promise<void>(async (resolve) => {
|
||||
// 获得菜单列表,它在登录的时候,setUserInfoAction 方法中已经进行获取
|
||||
let res: AppCustomRouteRecordRaw[] = []
|
||||
if (wsCache.get(CACHE_KEY.ROLE_ROUTERS)) {
|
||||
res = wsCache.get(CACHE_KEY.ROLE_ROUTERS) as AppCustomRouteRecordRaw[]
|
||||
}
|
||||
const routerMap: AppRouteRecordRaw[] = generateRoute(res)
|
||||
// 动态路由,404一定要放到最后面
|
||||
this.addRouters = routerMap.concat([
|
||||
{
|
||||
path: '/:path(.*)*',
|
||||
redirect: '/404',
|
||||
name: '404Page',
|
||||
meta: {
|
||||
hidden: true,
|
||||
breadcrumb: false
|
||||
}
|
||||
}
|
||||
])
|
||||
// 渲染菜单的所有路由
|
||||
resolve()
|
||||
})
|
||||
},
|
||||
setMenuTabRouters(routers: AppRouteRecordRaw[]): void {
|
||||
this.menuTabRouters = routers
|
||||
}
|
||||
},
|
||||
persist: false
|
||||
})
|
||||
|
||||
export const usePermissionStoreWithOut = () => {
|
||||
return usePermissionStore(store)
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import { store } from '../index'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useWorkFlowStore = defineStore('simpleWorkflow', {
|
||||
state: () => ({
|
||||
tableId: '',
|
||||
isTried: false,
|
||||
promoterDrawer: false,
|
||||
flowPermission1: {},
|
||||
approverDrawer: false,
|
||||
approverConfig1: {},
|
||||
copyerDrawer: false,
|
||||
copyerConfig1: {},
|
||||
conditionDrawer: false,
|
||||
conditionsConfig1: {
|
||||
conditionNodes: []
|
||||
}
|
||||
}),
|
||||
actions: {
|
||||
setTableId(payload) {
|
||||
this.tableId = payload
|
||||
},
|
||||
setIsTried(payload) {
|
||||
this.isTried = payload
|
||||
},
|
||||
setPromoter(payload) {
|
||||
this.promoterDrawer = payload
|
||||
},
|
||||
setFlowPermission(payload) {
|
||||
this.flowPermission1 = payload
|
||||
},
|
||||
setApprover(payload) {
|
||||
this.approverDrawer = payload
|
||||
},
|
||||
setApproverConfig(payload) {
|
||||
this.approverConfig1 = payload
|
||||
},
|
||||
setCopyer(payload) {
|
||||
this.copyerDrawer = payload
|
||||
},
|
||||
setCopyerConfig(payload) {
|
||||
this.copyerConfig1 = payload
|
||||
},
|
||||
setCondition(payload) {
|
||||
this.conditionDrawer = payload
|
||||
},
|
||||
setConditionsConfig(payload) {
|
||||
this.conditionsConfig1 = payload
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const useWorkFlowStoreWithOut = () => {
|
||||
return useWorkFlowStore(store)
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
import router from '@/router'
|
||||
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
||||
import { getRawRoute } from '@/utils/routerHelper'
|
||||
import { defineStore } from 'pinia'
|
||||
import { store } from '../indexs'
|
||||
import { findIndex } from '@/utils'
|
||||
|
||||
export interface TagsViewState {
|
||||
visitedViews: RouteLocationNormalizedLoaded[]
|
||||
cachedViews: Set<string>
|
||||
}
|
||||
|
||||
export const useTagsViewStore = defineStore('tagsView', {
|
||||
state: (): TagsViewState => ({
|
||||
visitedViews: [],
|
||||
cachedViews: new Set()
|
||||
}),
|
||||
getters: {
|
||||
getVisitedViews(): RouteLocationNormalizedLoaded[] {
|
||||
return this.visitedViews
|
||||
},
|
||||
getCachedViews(): string[] {
|
||||
return Array.from(this.cachedViews)
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
// 新增缓存和tag
|
||||
addView(view: RouteLocationNormalizedLoaded): void {
|
||||
this.addVisitedView(view)
|
||||
this.addCachedView()
|
||||
},
|
||||
// 新增tag
|
||||
addVisitedView(view: RouteLocationNormalizedLoaded) {
|
||||
if (this.visitedViews.some((v) => v.path === view.path)) return
|
||||
if (view.meta?.noTagsView) return
|
||||
this.visitedViews.push(
|
||||
Object.assign({}, view, {
|
||||
title: view.meta?.title || 'no-name'
|
||||
})
|
||||
)
|
||||
},
|
||||
// 新增缓存
|
||||
addCachedView() {
|
||||
const cacheMap: Set<string> = new Set()
|
||||
for (const v of this.visitedViews) {
|
||||
const item = getRawRoute(v)
|
||||
const needCache = !item.meta?.noCache
|
||||
if (!needCache) {
|
||||
continue
|
||||
}
|
||||
const name = item.name as string
|
||||
cacheMap.add(name)
|
||||
}
|
||||
if (Array.from(this.cachedViews).sort().toString() === Array.from(cacheMap).sort().toString())
|
||||
return
|
||||
this.cachedViews = cacheMap
|
||||
},
|
||||
// 删除某个
|
||||
delView(view: RouteLocationNormalizedLoaded) {
|
||||
this.delVisitedView(view)
|
||||
this.delCachedView()
|
||||
},
|
||||
// 删除tag
|
||||
delVisitedView(view: RouteLocationNormalizedLoaded) {
|
||||
for (const [i, v] of this.visitedViews.entries()) {
|
||||
if (v.path === view.path) {
|
||||
this.visitedViews.splice(i, 1)
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
// 删除缓存
|
||||
delCachedView() {
|
||||
const route = router.currentRoute.value
|
||||
const index = findIndex<string>(this.getCachedViews, (v) => v === route.name)
|
||||
if (index > -1) {
|
||||
this.cachedViews.delete(this.getCachedViews[index])
|
||||
}
|
||||
},
|
||||
// 删除所有缓存和tag
|
||||
delAllViews() {
|
||||
this.delAllVisitedViews()
|
||||
this.delCachedView()
|
||||
},
|
||||
// 删除所有tag
|
||||
delAllVisitedViews() {
|
||||
// const affixTags = this.visitedViews.filter((tag) => tag.meta.affix)
|
||||
this.visitedViews = []
|
||||
},
|
||||
// 删除其他
|
||||
delOthersViews(view: RouteLocationNormalizedLoaded) {
|
||||
this.delOthersVisitedViews(view)
|
||||
this.addCachedView()
|
||||
},
|
||||
// 删除其他tag
|
||||
delOthersVisitedViews(view: RouteLocationNormalizedLoaded) {
|
||||
this.visitedViews = this.visitedViews.filter((v) => {
|
||||
return v?.meta?.affix || v.path === view.path
|
||||
})
|
||||
},
|
||||
// 删除左侧
|
||||
delLeftViews(view: RouteLocationNormalizedLoaded) {
|
||||
const index = findIndex<RouteLocationNormalizedLoaded>(
|
||||
this.visitedViews,
|
||||
(v) => v.path === view.path
|
||||
)
|
||||
if (index > -1) {
|
||||
this.visitedViews = this.visitedViews.filter((v, i) => {
|
||||
return v?.meta?.affix || v.path === view.path || i > index
|
||||
})
|
||||
this.addCachedView()
|
||||
}
|
||||
},
|
||||
// 删除右侧
|
||||
delRightViews(view: RouteLocationNormalizedLoaded) {
|
||||
const index = findIndex<RouteLocationNormalizedLoaded>(
|
||||
this.visitedViews,
|
||||
(v) => v.path === view.path
|
||||
)
|
||||
if (index > -1) {
|
||||
this.visitedViews = this.visitedViews.filter((v, i) => {
|
||||
return v?.meta?.affix || v.path === view.path || i < index
|
||||
})
|
||||
this.addCachedView()
|
||||
}
|
||||
},
|
||||
updateVisitedView(view: RouteLocationNormalizedLoaded) {
|
||||
for (let v of this.visitedViews) {
|
||||
if (v.path === view.path) {
|
||||
v = Object.assign(v, view)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
persist: false
|
||||
})
|
||||
|
||||
export const useTagsViewStoreWithOut = () => {
|
||||
return useTagsViewStore(store)
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
import { store } from '@/stores/indexs'
|
||||
import { defineStore } from 'pinia'
|
||||
import { getAccessToken, removeToken } from '@/utils/auth'
|
||||
import { CACHE_KEY, useCache, deleteUserCache } from '@/hooks/web/useCache'
|
||||
// import { getInfo, loginOut } from '@/api/login'
|
||||
|
||||
const { wsCache } = useCache()
|
||||
|
||||
interface UserVO {
|
||||
id: number
|
||||
avatar: string
|
||||
nickname: string
|
||||
deptId: number
|
||||
}
|
||||
|
||||
interface UserInfoVO {
|
||||
// USER 缓存
|
||||
permissions: string[]
|
||||
roles: string[]
|
||||
isSetUser: boolean
|
||||
user: UserVO
|
||||
}
|
||||
|
||||
export const useUserStore = defineStore('admin-user', {
|
||||
state: (): UserInfoVO => ({
|
||||
permissions: [],
|
||||
roles: [],
|
||||
isSetUser: false,
|
||||
user: {
|
||||
id: 0,
|
||||
avatar: '',
|
||||
nickname: '',
|
||||
deptId: 0
|
||||
}
|
||||
}),
|
||||
getters: {
|
||||
getPermissions(): string[] {
|
||||
return this.permissions
|
||||
},
|
||||
getRoles(): string[] {
|
||||
return this.roles
|
||||
},
|
||||
getIsSetUser(): boolean {
|
||||
return this.isSetUser
|
||||
},
|
||||
getUser(): UserVO {
|
||||
return this.user
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
async setUserInfoAction() {
|
||||
if (!getAccessToken()) {
|
||||
this.resetState()
|
||||
return null
|
||||
}
|
||||
let userInfo = wsCache.get(CACHE_KEY.USER)
|
||||
if (!userInfo) {
|
||||
// userInfo = await getInfo()
|
||||
}
|
||||
this.permissions = userInfo.permissions
|
||||
this.roles = userInfo.roles
|
||||
this.user = userInfo.user
|
||||
this.isSetUser = true
|
||||
wsCache.set(CACHE_KEY.USER, userInfo)
|
||||
wsCache.set(CACHE_KEY.ROLE_ROUTERS, userInfo.menus)
|
||||
},
|
||||
async setUserAvatarAction(avatar: string) {
|
||||
const userInfo = wsCache.get(CACHE_KEY.USER)
|
||||
// NOTE: 是否需要像`setUserInfoAction`一样判断`userInfo != null`
|
||||
this.user.avatar = avatar
|
||||
userInfo.user.avatar = avatar
|
||||
wsCache.set(CACHE_KEY.USER, userInfo)
|
||||
},
|
||||
async setUserNicknameAction(nickname: string) {
|
||||
const userInfo = wsCache.get(CACHE_KEY.USER)
|
||||
// NOTE: 是否需要像`setUserInfoAction`一样判断`userInfo != null`
|
||||
this.user.nickname = nickname
|
||||
userInfo.user.nickname = nickname
|
||||
wsCache.set(CACHE_KEY.USER, userInfo)
|
||||
},
|
||||
async loginOut() {
|
||||
// await loginOut()
|
||||
removeToken()
|
||||
deleteUserCache() // 删除用户缓存
|
||||
this.resetState()
|
||||
},
|
||||
resetState() {
|
||||
this.permissions = []
|
||||
this.roles = []
|
||||
this.isSetUser = false
|
||||
this.user = {
|
||||
id: 0,
|
||||
avatar: '',
|
||||
nickname: '',
|
||||
deptId: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const useUserStoreWithOut = () => {
|
||||
return useUserStore(store)
|
||||
}
|
||||
@@ -1,16 +1,39 @@
|
||||
/**
|
||||
* 数据字典工具类
|
||||
* 数据字典工具类(统一读取 dictData store)
|
||||
*/
|
||||
import {useDictStoreWithOut} from '@/stores/modules/dict'
|
||||
import {ElementPlusInfoType} from '@/types/elementPlus'
|
||||
import { useDictData } from '@/stores/dictData'
|
||||
import { store } from '@/stores/index'
|
||||
import type { BasicDictData } from '@/stores/interface'
|
||||
import { ElementPlusInfoType } from '@/types/elementPlus'
|
||||
|
||||
const dictStore = useDictStoreWithOut()
|
||||
const getDictDataStore = () => useDictData(store)
|
||||
|
||||
function toDictCodeVariants(dictType: string): string[] {
|
||||
const variants = new Set<string>([dictType])
|
||||
if (dictType.includes('_')) {
|
||||
variants.add(
|
||||
dictType
|
||||
.split('_')
|
||||
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
||||
.join('_')
|
||||
)
|
||||
}
|
||||
return [...variants]
|
||||
}
|
||||
|
||||
function findBasicDict(dictType: string): BasicDictData | undefined {
|
||||
const basic = getDictDataStore().state.basic
|
||||
for (const variant of toDictCodeVariants(dictType)) {
|
||||
const found = basic.find(
|
||||
(item) => item.code === variant || item.code.toLowerCase() === variant.toLowerCase()
|
||||
)
|
||||
if (found) return found
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 dictType 对应的数据字典数组
|
||||
*
|
||||
* @param dictType 数据类型
|
||||
* @returns {*|Array} 数据字典数组
|
||||
*/
|
||||
export interface DictDataType {
|
||||
dictType: string
|
||||
@@ -24,15 +47,20 @@ export interface NumberDictDataType extends DictDataType {
|
||||
value: number
|
||||
}
|
||||
|
||||
export const getDictOptions = (dictType: string) => {
|
||||
return dictStore.getDictByType(dictType) || []
|
||||
export const getDictOptions = (dictType: string): DictDataType[] => {
|
||||
const basic = findBasicDict(dictType)
|
||||
if (!basic?.children?.length) return []
|
||||
return basic.children.map((item) => ({
|
||||
dictType,
|
||||
label: item.name,
|
||||
value: item.code || item.id,
|
||||
colorType: '' as ElementPlusInfoType | '',
|
||||
cssClass: ''
|
||||
}))
|
||||
}
|
||||
|
||||
export const getIntDictOptions = (dictType: string): NumberDictDataType[] => {
|
||||
// 获得通用的 DictDataType 列表
|
||||
const dictOptions: DictDataType[] = getDictOptions(dictType)
|
||||
// 转换成 number 类型的 NumberDictDataType 类型
|
||||
// why 需要特殊转换:避免 IDEA 在 v-for="dict in getIntDictOptions(...)" 时,el-option 的 key 会告警
|
||||
const dictOption: NumberDictDataType[] = []
|
||||
dictOptions.forEach((dict: DictDataType) => {
|
||||
dictOption.push({
|
||||
@@ -40,7 +68,6 @@ export const getIntDictOptions = (dictType: string): NumberDictDataType[] => {
|
||||
value: parseInt(dict.value + '')
|
||||
})
|
||||
})
|
||||
console.log("🚀 ~ getIntDictOptions ~ dictOption:", dictOption)
|
||||
return dictOption
|
||||
}
|
||||
|
||||
@@ -70,9 +97,6 @@ export const getBoolDictOptions = (dictType: string) => {
|
||||
|
||||
/**
|
||||
* 获取指定字典类型的指定值对应的字典对象
|
||||
* @param dictType 字典类型
|
||||
* @param value 字典值
|
||||
* @return DictDataType 字典对象
|
||||
*/
|
||||
export const getDictObj = (dictType: string, value: any): DictDataType | undefined => {
|
||||
const dictOptions: DictDataType[] = getDictOptions(dictType)
|
||||
@@ -85,10 +109,6 @@ export const getDictObj = (dictType: string, value: any): DictDataType | undefin
|
||||
|
||||
/**
|
||||
* 获得字典数据的文本展示
|
||||
*
|
||||
* @param dictType 字典类型
|
||||
* @param value 字典数据的值
|
||||
* @return 字典名称
|
||||
*/
|
||||
export const getDictLabel = (dictType: string, value: any): string => {
|
||||
const dictOptions: DictDataType[] = getDictOptions(dictType)
|
||||
|
||||
@@ -242,6 +242,25 @@ function createAxios<Data = any, T = ApiPromise<Data>>(
|
||||
return Axios(axiosConfig) as T
|
||||
}
|
||||
|
||||
/** 发起请求并直接返回业务 data 字段 */
|
||||
export async function requestData<T = any>(
|
||||
axiosConfig: AxiosRequestConfig,
|
||||
options?: Options,
|
||||
loading?: LoadingOptions
|
||||
): Promise<T> {
|
||||
const res = await createAxios<any, ApiPromise<T>>(axiosConfig, options, loading)
|
||||
return (res as ApiResponse<T>)?.data as T
|
||||
}
|
||||
|
||||
/** 下载文件,返回原始 axios 响应 */
|
||||
export function requestDownload(
|
||||
axiosConfig: AxiosRequestConfig,
|
||||
options?: Options,
|
||||
loading?: LoadingOptions
|
||||
) {
|
||||
return createAxios(axiosConfig, { ...options, reductDataFormat: false }, loading)
|
||||
}
|
||||
|
||||
export default createAxios
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,253 +1,14 @@
|
||||
import type { RouteLocationNormalized, Router, RouteRecordNormalized } from 'vue-router'
|
||||
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
|
||||
import { isUrl } from '@/utils/is'
|
||||
import { cloneDeep, omit } from 'lodash-es'
|
||||
import qs from 'qs'
|
||||
|
||||
const modules = import.meta.glob('../views/**/*.{vue,tsx}')
|
||||
/**
|
||||
* 注册一个异步组件
|
||||
* @param componentPath 例:/bpm/oa/leave/detail
|
||||
*/
|
||||
export const registerComponent = (componentPath: string) => {
|
||||
for (const item in modules) {
|
||||
if (item.includes(componentPath)) {
|
||||
// 使用异步组件的方式来动态加载组件
|
||||
// @ts-ignore
|
||||
return defineAsyncComponent(modules[item])
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Layout */
|
||||
export const Layout = () => import('@/layouts/Layout.vue')
|
||||
|
||||
export const getParentLayout = () => {
|
||||
return () =>
|
||||
new Promise((resolve) => {
|
||||
resolve({
|
||||
name: 'ParentLayout'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 按照路由中meta下的rank等级升序来排序路由
|
||||
export const ascending = (arr: any[]) => {
|
||||
arr.forEach((v) => {
|
||||
if (v?.meta?.rank === null) v.meta.rank = undefined
|
||||
if (v?.meta?.rank === 0) {
|
||||
if (v.name !== 'home' && v.path !== '/') {
|
||||
console.warn('rank only the home page can be 0')
|
||||
}
|
||||
}
|
||||
})
|
||||
return arr.sort((a: { meta: { rank: number } }, b: { meta: { rank: number } }) => {
|
||||
return a?.meta?.rank - b?.meta?.rank
|
||||
})
|
||||
}
|
||||
|
||||
export const getRawRoute = (route: RouteLocationNormalized): RouteLocationNormalized => {
|
||||
if (!route) return route
|
||||
const { matched, ...opt } = route
|
||||
return {
|
||||
...opt,
|
||||
matched: (matched
|
||||
? matched.map((item) => ({
|
||||
meta: item.meta,
|
||||
name: item.name,
|
||||
path: item.path
|
||||
}))
|
||||
: undefined) as RouteRecordNormalized[]
|
||||
}
|
||||
}
|
||||
|
||||
// 后端控制路由生成
|
||||
export const generateRoute = (routes: AppCustomRouteRecordRaw[]): AppRouteRecordRaw[] => {
|
||||
const res: AppRouteRecordRaw[] = []
|
||||
const modulesRoutesKeys = Object.keys(modules)
|
||||
for (const route of routes) {
|
||||
// 1. 生成 meta 菜单元数据
|
||||
const meta = {
|
||||
title: route.name,
|
||||
icon: route.icon,
|
||||
hidden: !route.visible,
|
||||
noCache: !route.keepAlive,
|
||||
alwaysShow:
|
||||
route.children &&
|
||||
route.children.length === 1 &&
|
||||
(route.alwaysShow !== undefined ? route.alwaysShow : true)
|
||||
} as any
|
||||
// 特殊逻辑:如果后端配置的 MenuDO.component 包含 ?,则表示需要传递参数
|
||||
// 此时,我们需要解析参数,并且将参数放到 meta.query 中
|
||||
// 这样,后续在 Vue 文件中,可以通过 const { currentRoute } = useRouter() 中,通过 meta.query 获取到参数
|
||||
if (route.component && route.component.indexOf('?') > -1) {
|
||||
const query = route.component.split('?')[1]
|
||||
route.component = route.component.split('?')[0]
|
||||
meta.query = qs.parse(query)
|
||||
}
|
||||
|
||||
// 2. 生成 data(AppRouteRecordRaw)
|
||||
// 路由地址转首字母大写驼峰,作为路由名称,适配keepAlive
|
||||
let data: AppRouteRecordRaw = {
|
||||
path: route.path.indexOf('?') > -1 ? route.path.split('?')[0] : route.path,
|
||||
name:
|
||||
route.componentName && route.componentName.length > 0
|
||||
? route.componentName
|
||||
: toCamelCase(route.path, true),
|
||||
redirect: route.redirect,
|
||||
meta: meta
|
||||
}
|
||||
//处理顶级非目录路由
|
||||
if (!route.children && route.parentId == 0 && route.component) {
|
||||
data.component = Layout
|
||||
data.meta = {}
|
||||
data.name = toCamelCase(route.path, true) + 'Parent'
|
||||
data.redirect = ''
|
||||
meta.alwaysShow = true
|
||||
const childrenData: AppRouteRecordRaw = {
|
||||
path: '',
|
||||
name:
|
||||
route.componentName && route.componentName.length > 0
|
||||
? route.componentName
|
||||
: toCamelCase(route.path, true),
|
||||
redirect: route.redirect,
|
||||
meta: meta
|
||||
}
|
||||
const index = route?.component
|
||||
? modulesRoutesKeys.findIndex((ev) => ev.includes(route.component))
|
||||
: modulesRoutesKeys.findIndex((ev) => ev.includes(route.path))
|
||||
childrenData.component = modules[modulesRoutesKeys[index]]
|
||||
data.children = [childrenData]
|
||||
} else {
|
||||
// 目录
|
||||
if (route.children) {
|
||||
data.component = Layout
|
||||
data.redirect = getRedirect(route.path, route.children)
|
||||
// 外链
|
||||
} else if (isUrl(route.path)) {
|
||||
data = {
|
||||
path: '/external-link',
|
||||
component: Layout,
|
||||
meta: {
|
||||
name: route.name
|
||||
},
|
||||
children: [data]
|
||||
} as AppRouteRecordRaw
|
||||
// 菜单
|
||||
} else {
|
||||
// 对后端传component组件路径和不传做兼容(如果后端传component组件路径,那么path可以随便写,如果不传,component组件路径会根path保持一致)
|
||||
const index = route?.component
|
||||
? modulesRoutesKeys.findIndex((ev) => ev.includes(route.component))
|
||||
: modulesRoutesKeys.findIndex((ev) => ev.includes(route.path))
|
||||
data.component = modules[modulesRoutesKeys[index]]
|
||||
}
|
||||
if (route.children) {
|
||||
data.children = generateRoute(route.children)
|
||||
}
|
||||
}
|
||||
res.push(data as AppRouteRecordRaw)
|
||||
}
|
||||
return res
|
||||
}
|
||||
export const getRedirect = (parentPath: string, children: AppCustomRouteRecordRaw[]) => {
|
||||
if (!children || children.length == 0) {
|
||||
return parentPath
|
||||
}
|
||||
const path = generateRoutePath(parentPath, children[0].path)
|
||||
// 递归子节点
|
||||
if (children[0].children) return getRedirect(path, children[0].children)
|
||||
}
|
||||
const generateRoutePath = (parentPath: string, path: string) => {
|
||||
if (parentPath.endsWith('/')) {
|
||||
parentPath = parentPath.slice(0, -1) // 移除默认的 /
|
||||
}
|
||||
if (!path.startsWith('/')) {
|
||||
path = '/' + path
|
||||
}
|
||||
return parentPath + path
|
||||
}
|
||||
export const pathResolve = (parentPath: string, path: string) => {
|
||||
if (isUrl(path)) return path
|
||||
const childPath = path.startsWith('/') || !path ? path : `/${path}`
|
||||
return `${parentPath}${childPath}`.replace(/\/\//g, '/')
|
||||
}
|
||||
|
||||
// 路由降级
|
||||
export const flatMultiLevelRoutes = (routes: AppRouteRecordRaw[]) => {
|
||||
const modules: AppRouteRecordRaw[] = cloneDeep(routes)
|
||||
for (let index = 0; index < modules.length; index++) {
|
||||
const route = modules[index]
|
||||
if (!isMultipleRoute(route)) {
|
||||
continue
|
||||
}
|
||||
promoteRouteLevel(route)
|
||||
}
|
||||
return modules
|
||||
}
|
||||
|
||||
// 层级是否大于2
|
||||
const isMultipleRoute = (route: AppRouteRecordRaw) => {
|
||||
if (!route || !Reflect.has(route, 'children') || !route.children?.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
const children = route.children
|
||||
|
||||
let flag = false
|
||||
for (let index = 0; index < children.length; index++) {
|
||||
const child = children[index]
|
||||
if (child.children?.length) {
|
||||
flag = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return flag
|
||||
}
|
||||
|
||||
// 生成二级路由
|
||||
const promoteRouteLevel = (route: AppRouteRecordRaw) => {
|
||||
let router: Router | null = createRouter({
|
||||
routes: [route as RouteRecordRaw],
|
||||
history: createWebHashHistory()
|
||||
})
|
||||
|
||||
const routes = router.getRoutes()
|
||||
addToChildren(routes, route.children || [], route)
|
||||
router = null
|
||||
|
||||
route.children = route.children?.map((item) => omit(item, 'children'))
|
||||
}
|
||||
|
||||
// 添加所有子菜单
|
||||
const addToChildren = (
|
||||
routes: RouteRecordNormalized[],
|
||||
children: AppRouteRecordRaw[],
|
||||
routeModule: AppRouteRecordRaw
|
||||
) => {
|
||||
for (let index = 0; index < children.length; index++) {
|
||||
const child = children[index]
|
||||
const route = routes.find((item) => item.name === child.name)
|
||||
if (!route) {
|
||||
continue
|
||||
}
|
||||
routeModule.children = routeModule.children || []
|
||||
if (!routeModule.children.find((item) => item.name === route.name)) {
|
||||
routeModule.children?.push(route as unknown as AppRouteRecordRaw)
|
||||
}
|
||||
if (child.children?.length) {
|
||||
addToChildren(routes, child.children, routeModule)
|
||||
}
|
||||
}
|
||||
}
|
||||
const toCamelCase = (str: string, upperCaseFirst: boolean) => {
|
||||
str = (str || '')
|
||||
.replace(/-(.)/g, function (group1: string) {
|
||||
return group1.toUpperCase()
|
||||
})
|
||||
.replaceAll('-', '')
|
||||
|
||||
if (upperCaseFirst && str) {
|
||||
str = str.charAt(0).toUpperCase() + str.slice(1)
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
const modules = import.meta.glob('../views/**/*.{vue,tsx}')
|
||||
|
||||
/**
|
||||
* 注册一个异步组件
|
||||
* @param componentPath 例:/bpm/oa/leave/detail
|
||||
*/
|
||||
export const registerComponent = (componentPath: string) => {
|
||||
for (const item in modules) {
|
||||
if (item.includes(componentPath)) {
|
||||
// @ts-ignore
|
||||
return defineAsyncComponent(modules[item])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,51 +10,63 @@
|
||||
</pane>
|
||||
<pane style="background: #fff" :style="height">
|
||||
<TableHeader ref="TableHeaderRef" datePicker :show-search="false">
|
||||
<template v-slot:select>
|
||||
<el-form-item label="客户名称">
|
||||
<el-input
|
||||
v-model="tableStore.table.params.crmName"
|
||||
maxlength="32"
|
||||
show-word-limit
|
||||
clearable
|
||||
placeholder="请输入客户名称"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="报表编号">
|
||||
<el-input
|
||||
v-model="tableStore.table.params.reportNumber"
|
||||
clearable
|
||||
placeholder="请输入报表编号"
|
||||
maxlength="12"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
</template>
|
||||
<template #operation>
|
||||
<el-upload
|
||||
:show-file-list="false"
|
||||
ref="uploadRef"
|
||||
action=""
|
||||
accept=".png,.jpg"
|
||||
:on-change="choose"
|
||||
:auto-upload="false"
|
||||
>
|
||||
<template #trigger>
|
||||
<el-button icon="el-icon-Upload" type="primary" class="mr10 ml10">上传接线图</el-button>
|
||||
</template>
|
||||
</el-upload>
|
||||
<el-button icon="el-icon-Download" type="primary" @click="exportEvent" :loading="loading">
|
||||
生成
|
||||
</el-button>
|
||||
</template>
|
||||
</TableHeader>
|
||||
<div class="box">
|
||||
<div id="luckysheet">
|
||||
<!-- <div id="luckysheet">
|
||||
<img
|
||||
width="100%"
|
||||
:style="`height: calc(${tableStore.table.height} + 40px)`"
|
||||
src="@/assets/img/jss.png"
|
||||
/>
|
||||
</div> -->
|
||||
<el-form label-width="auto" class="form-one">
|
||||
<el-form-item label="报表编号">
|
||||
<el-input
|
||||
v-model="tableStore.table.params.reportNumber"
|
||||
style="width: 240px"
|
||||
maxlength="32"
|
||||
show-word-limit
|
||||
clearable
|
||||
placeholder="请输入报表编号"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="客户名称">
|
||||
<el-input
|
||||
v-model="tableStore.table.params.crmName"
|
||||
style="width: 240px"
|
||||
maxlength="32"
|
||||
show-word-limit
|
||||
clearable
|
||||
placeholder="请输入客户名称"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="上传接线图">
|
||||
<el-upload
|
||||
:show-file-list="false"
|
||||
ref="formUploadRef"
|
||||
action=""
|
||||
accept=".png,.jpg,.jpeg"
|
||||
:on-change="choose"
|
||||
:auto-upload="false"
|
||||
>
|
||||
<template #trigger>
|
||||
<el-button icon="el-icon-Upload" type="primary">上传接线图</el-button>
|
||||
</template>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div v-if="previewUrl" class="upload-preview">
|
||||
<div class="upload-preview-header">
|
||||
<span class="upload-preview-title">接线图预览</span>
|
||||
<el-button type="danger" link @click="removeImage">删除</el-button>
|
||||
</div>
|
||||
<img :src="previewUrl" alt="接线图" class="upload-preview-image" />
|
||||
</div>
|
||||
</div>
|
||||
</pane>
|
||||
@@ -63,7 +75,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, provide } from 'vue'
|
||||
import { onMounted, onBeforeUnmount, ref, provide } from 'vue'
|
||||
import 'splitpanes/dist/splitpanes.css'
|
||||
import { Splitpanes, Pane } from 'splitpanes'
|
||||
import TableStore from '@/utils/tableStore'
|
||||
@@ -72,8 +84,8 @@ import TableHeader from '@/components/table/header/index.vue'
|
||||
import { useDictData } from '@/stores/dictData'
|
||||
import { mainHeight } from '@/utils/layout'
|
||||
import { exportModel } from '@/api/process-boot/reportForms'
|
||||
import { genFileId, ElMessage, ElNotification } from 'element-plus'
|
||||
import type { UploadProps, UploadUserFile } from 'element-plus'
|
||||
import { ElMessage, ElNotification } from 'element-plus'
|
||||
import type { UploadFile, UploadInstance, UploadProps } from 'element-plus'
|
||||
import dayjs from 'dayjs'
|
||||
defineOptions({
|
||||
// name: 'harmonic-boot/report/word'
|
||||
@@ -83,10 +95,11 @@ const height = mainHeight(20)
|
||||
const size = ref(19)
|
||||
const dictData = useDictData()
|
||||
const TableHeaderRef = ref()
|
||||
const headerUploadRef = ref<UploadInstance>()
|
||||
const formUploadRef = ref<UploadInstance>()
|
||||
const dotList: any = ref({})
|
||||
const Template: any = ref({})
|
||||
const uploadList: any = ref([])
|
||||
|
||||
const uploadList = ref<UploadFile>()
|
||||
const previewUrl = ref('')
|
||||
const tableStore = new TableStore({
|
||||
url: '',
|
||||
method: 'POST',
|
||||
@@ -107,19 +120,50 @@ onMounted(() => {
|
||||
const handleNodeClick = (data: any, node: any) => {
|
||||
dotList.value = data
|
||||
}
|
||||
|
||||
const revokePreviewUrl = () => {
|
||||
if (previewUrl.value) {
|
||||
URL.revokeObjectURL(previewUrl.value)
|
||||
previewUrl.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const clearUploadRefs = () => {
|
||||
headerUploadRef.value?.clearFiles()
|
||||
formUploadRef.value?.clearFiles()
|
||||
}
|
||||
|
||||
// 上传
|
||||
const choose = (files: any) => {
|
||||
const isJPG = files.raw.type === 'image/jpg'
|
||||
const isJPEG = files.raw.type === 'image/jpeg'
|
||||
const isPNG = files.raw.type === 'image/png'
|
||||
const choose: UploadProps['onChange'] = file => {
|
||||
if (!file.raw) return
|
||||
|
||||
const isJPG = file.raw.type === 'image/jpg'
|
||||
const isJPEG = file.raw.type === 'image/jpeg'
|
||||
const isPNG = file.raw.type === 'image/png'
|
||||
if (!isJPG && !isPNG && !isJPEG) {
|
||||
ElMessage.warning('上传文件只能是 jpg/png 格式!')
|
||||
return false
|
||||
clearUploadRefs()
|
||||
return
|
||||
}
|
||||
|
||||
uploadList.value = files
|
||||
const isLt5M = file.raw.size / 1024 / 1024 < 5
|
||||
if (!isLt5M) {
|
||||
ElMessage.warning('上传图片大小不能超过 5MB!')
|
||||
clearUploadRefs()
|
||||
return
|
||||
}
|
||||
|
||||
revokePreviewUrl()
|
||||
uploadList.value = file
|
||||
previewUrl.value = URL.createObjectURL(file.raw)
|
||||
ElMessage.success('上传成功')
|
||||
}
|
||||
|
||||
const removeImage = () => {
|
||||
revokePreviewUrl()
|
||||
uploadList.value = undefined
|
||||
clearUploadRefs()
|
||||
}
|
||||
// 生成
|
||||
const exportEvent = () => {
|
||||
if (dotList.value.level != 6) {
|
||||
@@ -173,6 +217,10 @@ const exportEvent = () => {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
revokePreviewUrl()
|
||||
})
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.splitpanes.default-theme .splitpanes__pane {
|
||||
@@ -182,4 +230,32 @@ const exportEvent = () => {
|
||||
.box {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.upload-preview {
|
||||
margin-top: 16px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: 4px;
|
||||
background: #fafafa;
|
||||
|
||||
&-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
&-title {
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
&-image {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
max-height: 400px;
|
||||
object-fit: contain;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
showOverflow
|
||||
v-loading="loading"
|
||||
:row-config="{ isCurrent: true, isHover: true }"
|
||||
:columnConfig="{ resizable: true }"
|
||||
:columnConfig="{ resizable: true, useKey: true }"
|
||||
@cell-click="currentChangeEvent"
|
||||
style="z-index: 0"
|
||||
>
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
border
|
||||
align="center"
|
||||
showOverflow
|
||||
:columnConfig="{ resizable: true }"
|
||||
:columnConfig="{ resizable: true, useKey: true }"
|
||||
>
|
||||
<vxe-column field="targetName" title="供电公司"></vxe-column>
|
||||
<vxe-column field="avg" title="幅值"></vxe-column>
|
||||
|
||||
@@ -52,7 +52,7 @@ import { onMounted, ref, reactive } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import FcDesigner from '@form-create/designer'
|
||||
import { encodeConf, encodeFields, setConfAndFields } from '@/utils/formCreate'
|
||||
import { useTagsViewStore } from '@/stores/modules/tagsView'
|
||||
import { useNavTabs } from '@/stores/navTabs'
|
||||
import { useFormCreateDesigner } from '@/components/FormCreate'
|
||||
import { Plus, Back } from '@element-plus/icons-vue'
|
||||
import ContentWrap from '@/components/ContentWrap/src/ContentWrap.vue'
|
||||
@@ -75,7 +75,7 @@ import { addForm, getById, updateForm } from '@/api/bpm-boot/form'
|
||||
|
||||
const { push, currentRoute, go } = useRouter() // 路由
|
||||
const { query } = useRoute() // 路由信息
|
||||
const { delView } = useTagsViewStore() // 视图操作
|
||||
const navTabs = useNavTabs()
|
||||
|
||||
const designer = ref() // 表单设计器
|
||||
useFormCreateDesigner(designer) // 表单设计器增强
|
||||
@@ -135,8 +135,8 @@ const submitForm = async () => {
|
||||
}
|
||||
/** 关闭按钮 */
|
||||
const close = () => {
|
||||
delView(unref(currentRoute))
|
||||
go(-1)
|
||||
navTabs.closeTab(unref(currentRoute))
|
||||
go(-1)
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
|
||||
Reference in New Issue
Block a user