8 Commits

Author SHA1 Message Date
guanj
04c5eb7185 调整退出问题 2026-07-14 14:49:10 +08:00
guanj
d0d62ab0af 修改菜单树无法展开问题 2026-07-14 14:39:59 +08:00
guanj
b3d671a93e 调整前端两套互不连通的 HTTP 请求层并存、两套布局框架并存、两套 Pinia 实例并存问题 2026-07-10 14:52:29 +08:00
guanj
888b8d1bf4 修改点击列问题 2026-07-10 08:57:37 +08:00
guanj
ee9cd057b8 优化菜单切换导致浏览器刷新问题 2026-07-09 15:40:56 +08:00
guanj
8bca4191f2 调整代码 2026-07-09 15:26:01 +08:00
guanj
69e6a1f93f 调整 2026-07-09 14:41:56 +08:00
guanj
71df1ac4be 修改冲突 2026-07-09 13:59:02 +08:00
100 changed files with 2042 additions and 6816 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -318,7 +318,6 @@ const resetListenersList = () => {
}
// 打开 监听器详情 侧边栏
const openListenerForm = (listener, index?) => {
// debugger
if (listener) {
listenerForm.value = initListenerForm(listener)
editingListenerIndex.value = index
@@ -356,7 +355,6 @@ const openListenerFieldForm = (field, index?) => {
}
// 保存监听器注入字段
const saveListenerFiled = async () => {
// debugger
let validateStatus = await listenerFieldFormRef.value.validate()
if (!validateStatus) return // 验证不通过直接返回
if (editingListenerFieldIndex.value === -1) {
@@ -373,7 +371,6 @@ const saveListenerFiled = async () => {
}
// 移除监听器字段
const removeListenerField = (index) => {
// debugger
ElMessageBox.confirm('确认移除该字段吗?', '提示', {
confirmButtonText: '确 认',
cancelButtonText: '取 消'
@@ -402,7 +399,6 @@ const removeListener = (index) => {
}
// 保存监听器配置
const saveListenerConfig = async () => {
// debugger
let validateStatus = await listenerFormRef.value.validate()
if (!validateStatus) return // 验证不通过直接返回
const listenerObject = createListenerObject(listenerForm.value, false, prefix)

View File

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

View File

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

View File

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

View File

@@ -70,7 +70,15 @@
</el-button>
</template>
<slot name="operation"></slot>
<el-button class="ml10" v-if="showCustomColumn" :icon="Setting" @click="openCustomColumn">列设置</el-button>
<el-button
class="ml10"
v-if="showCustomColumn"
:icon="Setting"
@mousedown="onCustomColumnMousedown"
@click="openCustomColumn"
>
列设置
</el-button>
</div>
<el-form
:style="showSelect && showUnfoldButton ? headerFormSecondStyleOpen : headerFormSecondStyleClose"
@@ -167,8 +175,26 @@ watch(
}, 500)
}
)
let customColumnVisibleOnPress = false
const onCustomColumnMousedown = () => {
const table = tableStore?.table?.ref as any
if (!table) return
customColumnVisibleOnPress = table.getCustomVisible?.() ?? !!table.reactData?.customStore?.visible
if (table.reactData?.customStore) {
table.reactData.customStore.activeBtn = true
}
}
const openCustomColumn = () => {
tableStore?.table?.ref?.openCustom?.()
const table = tableStore?.table?.ref as any
if (!table) return
if (table.reactData?.customStore) {
table.reactData.customStore.activeBtn = false
}
if (customColumnVisibleOnPress) {
table?.closeCustom?.()
} else {
table?.openCustom?.()
}
}
onMounted(() => {
if (props.showCustomColumn && tableStore?.table) {

View File

@@ -1,7 +1,7 @@
<template>
<div :style="{ height: typeof props.height === 'string' ? props.height : tableStore.table.height }">
<div v-if="showCustomColumn && !tableStore?.table?.customColumnInHeader" class="table-custom-toolbar">
<el-button icon="el-icon-Setting" @click="openCustomColumn">列设置</el-button>
<el-button icon="el-icon-Setting" @mousedown="onCustomColumnMousedown" @click="openCustomColumn">列设置</el-button>
</div>
<vxe-table
ref="tableRef"
@@ -75,6 +75,10 @@ import type TableStoreClass from '@/utils/tableStore'
import { useRouter } from 'vue-router'
import { defaultAttribute } from '@/components/table/defaultAttribute'
defineOptions({
inheritAttrs: false
})
const config = useConfig()
const tableRef = ref<VxeTableInstance>()
const tableStore = inject('tableStore') as TableStoreClass
@@ -135,8 +139,26 @@ const selectChangeEvent: VxeTableEvents.CheckboxChange<any> = ({ checked }) => {
const records = (tableRef.value as VxeTableInstance).getCheckboxRecords()
tableStore.onTableAction('selection-change', records)
}
let customColumnVisibleOnPress = false
const onCustomColumnMousedown = () => {
const table = tableRef.value as any
if (!table) return
customColumnVisibleOnPress = table.getCustomVisible?.() ?? !!table.reactData?.customStore?.visible
if (table.reactData?.customStore) {
table.reactData.customStore.activeBtn = true
}
}
const openCustomColumn = () => {
tableRef.value?.openCustom?.()
const table = tableRef.value as any
if (!table) return
if (table.reactData?.customStore) {
table.reactData.customStore.activeBtn = false
}
if (customColumnVisibleOnPress) {
table?.closeCustom?.()
} else {
table?.openCustom?.()
}
}
const getRef = () => {
return tableRef.value

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -104,7 +104,10 @@ const handleLogout = async () => {
redirectToCasLogout()
} else {
// 普通环境直接跳登录页
window.location.reload()
setTimeout(() => {
replace('/login')
}, 1000)
}
}
}
@@ -119,8 +122,6 @@ const clearLocalData = () => {
// 清空标签页
navTabs.closeTabs()
console.log('✅ 本地数据已全部清空')
}
// CAS 登出跳转

View File

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

View File

@@ -1,9 +1,9 @@
<template>
<el-main class="layout-main" :style="mainHeight()">
<router-view v-slot="{ Component }">
<transition :name="config.layout.mainAnimation" mode="out-in">
<transition :name="config.layout.mainAnimation">
<keep-alive :include="state.keepAliveComponentNameList">
<component :is="Component" :key="state.componentKey" />
<component :is="Component" :key="state.componentKey || undefined" />
</keep-alive>
</transition>
</router-view>
@@ -32,7 +32,7 @@ const state: {
componentKey: string
keepAliveComponentNameList: string[]
} = reactive({
componentKey: route.path,
componentKey: '',
keepAliveComponentNameList: []
})
@@ -79,7 +79,6 @@ onMounted(() => {
watch(
() => route.path,
() => {
state.componentKey = route.path
if (typeof navTabs.state.activeRoute?.meta.keepalive == 'string') {
addKeepAliveComponentName(navTabs.state.activeRoute?.meta.keepalive)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
View 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 ?? {}
}

View File

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

View File

@@ -3,6 +3,7 @@ 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>
@@ -10,8 +11,7 @@ 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 ?? {}
const message = await loadLocaleMessages(locale.lang)
setHtmlPageLang(locale.lang)

View File

@@ -346,7 +346,7 @@ const staticRoutes: Array<RouteRecordRaw> = [
{
// 后台找不到页面了-可能是路由未加载上
path: adminBaseRoutePath + ':path(.*)*',
path: adminBaseRoutePath + '/:path(.*)*',
redirect: to => {
return {
name: 'adminMainLoading',

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import { defineStore } from 'pinia'
import { store } from '../indexs'
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'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -81,14 +81,17 @@
.vxe-modal--wrapper {
z-index: 5000 !important;
}
// .vxe-table--body .vxe-body--row:nth-child(even) {
// background-color: #f9f9f9;
// // background-color: var(--el-color-primary-light-9);
// }
// .vxe-table--body .vxe-body--row:nth-child(odd) {
// background-color: #ffffff;
// }
/* 斑马纹:新版 vxe 背景画在 column 上,默认条纹色 #fafafa 几乎看不见 */
:root {
--vxe-ui-table-row-striped-background-color: #fafafa;
--vxe-ui-table-row-hover-striped-background-color: #eef1f6;
}
.vxe-table--render-default .vxe-body--row.row--stripe > .vxe-body--column {
background-color: var(--vxe-ui-table-row-striped-background-color);
}
.vxe-table--render-default .vxe-body--row.row--hover.row--stripe > .vxe-body--column {
background-color: var(--vxe-ui-table-row-hover-striped-background-color);
}
.default-theme.splitpanes--vertical > .splitpanes__splitter,
.default-theme .splitpanes--vertical > .splitpanes__splitter {
width: 10px !important;

View File

@@ -1,16 +1,39 @@
/**
* 数据字典工具类
* 数据字典工具类(统一读取 dictData store
*/
import {useDictStoreWithOut} from '@/stores/modules/dict'
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)

View File

@@ -179,7 +179,6 @@ function createAxios<Data = any, T = ApiPromise<Data>>(
return Axios(response.config)
})
.catch(err => {
window.location.reload()
adminInfo.removeToken()
router.push({ name: 'login' })
return Promise.reject(err)
@@ -243,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
/**

View File

@@ -70,6 +70,7 @@ export const onClickMenu = (menu: RouteRecordRaw) => {
switch (menu.meta?.menu_type) {
case 'iframe':
case 'tab':
if (router.currentRoute.value.path === menu.path) return
routePush({ path: menu.path })
break
case 'link':

View File

@@ -1,10 +1,5 @@
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
@@ -12,242 +7,8 @@ const modules = import.meta.glob('../views/**/*.{vue,tsx}')
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. 生成 dataAppRouteRecordRaw
// 路由地址转首字母大写驼峰作为路由名称适配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
}

View File

@@ -1,6 +1,6 @@
<template>
<div class="default-main">
<TableHeader ref="TableHeaderRef" showExport>
<TableHeader ref="TableHeaderRef" showCustomColumn>
<template #select>
<el-form-item label="地市">
<el-select v-model="tableStore.table.params.city" clearable placeholder="请选择地市">

View File

@@ -24,8 +24,8 @@
</el-form-item>
</template>
<template #operation>
<el-button icon="el-icon-Plus" type="primary" @click="addFormModel">新增</el-button>
<el-button icon="el-icon-Delete" type="primary" @click="deleteEven">删除</el-button>
<el-button icon="el-icon-Plus" type="primary" @click="addFormModel" v-if="information">新增</el-button>
<el-button icon="el-icon-Delete" type="primary" @click="deleteEven" v-if="information">删除</el-button>
<!-- <el-button icon="el-icon-Download" type="primary" @click="exportExcelTemplate" :loading="loading">
模板下载
</el-button>
@@ -59,6 +59,7 @@ import DetailInfo from './components/detail.vue'
import { cancelFormData, getUserReportById } from '@/api/supervision-boot/interfere/index'
import { deleteUserReport } from '@/api/device-boot/sensitiveLoadMange'
import { download } from '@/utils/fileDownLoad'
const addForms = ref()
const dictData = useDictData()
const sensitiveUserPopup = ref()
@@ -73,6 +74,8 @@ defineOptions({
//获取登陆用户姓名和部门
const adminInfo = useAdminInfo()
const information = ref(adminInfo.roleCode.includes('business_manager'))
const tableStore = new TableStore({
url: '/device-boot/userReport/getUserLedgerPage',
// publicHeight: 65,

View File

@@ -1,7 +1,7 @@
<template>
<div class="default-main online">
<div class="online_header">
<TableHeader date-picker area ref="tableHeaderRef">
<TableHeader date-picker area ref="tableHeaderRef" showCustomColumn>
<template #select>
<el-form-item label="统计类型">
<el-select
@@ -106,7 +106,7 @@
ref="tableRef"
:tree-config="{ transform: true, parentField: 'uPid', rowField: 'uId' }"
:scroll-y="{ enabled: true }"
v-if="activeName == 0"
/>
</el-tab-pane>
<el-tab-pane :name="1" :lazy="true" label="稳态指标符合性占比图">

View File

@@ -1,7 +1,7 @@
<template>
<div class="default-main online">
<div class="online_header">
<TableHeader date-picker area ref="tableHeaderRef">
<TableHeader date-picker area ref="tableHeaderRef" showCustomColumn>
<template #select>
<el-form-item label="统计类型">
<el-select
@@ -101,7 +101,7 @@
ref="tableRef"
:tree-config="{ transform: true, parentField: 'uPid', rowField: 'uId' }"
:scroll-y="{ enabled: true }"
v-if="activeName == 0"
/>
</el-tab-pane>
<el-tab-pane :name="1" :lazy="true" label="谐波畸变率统计图">

View File

@@ -1,7 +1,7 @@
<template>
<div class="default-main online">
<div class="online_header">
<TableHeader date-picker area ref="tableHeaderRef">
<TableHeader date-picker area ref="tableHeaderRef" showCustomColumn>
<template #select>
<el-form-item label="统计类型">
<el-select
@@ -101,7 +101,7 @@
ref="tableRef"
:tree-config="{ transform: true, parentField: 'uPid', rowField: 'uId' }"
:scroll-y="{ enabled: true }"
v-if="activeName == 0"
/>
</el-tab-pane>
<el-tab-pane :name="1" :lazy="true" label="稳态合格率统计图">

View File

@@ -748,6 +748,13 @@ const init = () => {
console.error('WebSocket 客户端实例不存在')
return
}
echartsDataV1.value = initEcharts('#DAA520', 0, 'A相')
echartsDataV2.value = initEcharts('#2E8B57', 0, 'B相')
echartsDataV3.value = initEcharts('#A52a2a', 0, 'C相')
echartsDataA1.value = initEcharts('#DAA520', 1, 'A相')
echartsDataA2.value = initEcharts('#2E8B57', 1, 'B相')
echartsDataA3.value = initEcharts('#A52a2a', 1, 'C相')
if (monitoringPoint.state.comFlag == 0) {
loading.value = false
return ElMessage({
@@ -757,13 +764,7 @@ const init = () => {
}
loading.value = true
const url = localStorage.getItem('WebSocketUrl') || 'null' //'ws://192.168.1.68:10407/api/pushMessage/'
echartsDataV1.value = initEcharts('#DAA520', 0, 'A相')
echartsDataV2.value = initEcharts('#2E8B57', 0, 'B相')
echartsDataV3.value = initEcharts('#A52a2a', 0, 'C相')
echartsDataA1.value = initEcharts('#DAA520', 1, 'A相')
echartsDataA2.value = initEcharts('#2E8B57', 1, 'B相')
echartsDataA3.value = initEcharts('#A52a2a', 1, 'C相')
let pids = monitoringPoint.state.pid.split(',')
dataSocket.socketServe.connect(`${url}${adminInfo.id},${monitoringPoint.state.lineId},${pids[pids.length - 2]}`)
@@ -1176,17 +1177,11 @@ onBeforeUnmount(() => {
}
}
:deep(.view_bot) {
.vxe-table--render-default .vxe-body--column:not(.col--ellipsis),
.vxe-table--render-default .vxe-footer--column:not(.col--ellipsis),
.vxe-table--render-default .vxe-header--column:not(.col--ellipsis) {
padding: 5px !important;
}
:deep(.vxe-cell) {
.vxe-table--body-wrapper {
min-height: 32px !important;
}
}
::v-deep .el-collapse-item__header {
font-size: 16px !important;

View File

@@ -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 choose = (files: any) => {
const isJPG = files.raw.type === 'image/jpg'
const isJPEG = files.raw.type === 'image/jpeg'
const isPNG = files.raw.type === 'image/png'
if (!isJPG && !isPNG && !isJPEG) {
ElMessage.warning('上传文件只能是 jpg/png 格式!')
return false
const revokePreviewUrl = () => {
if (previewUrl.value) {
URL.revokeObjectURL(previewUrl.value)
previewUrl.value = ''
}
}
uploadList.value = files
const clearUploadRefs = () => {
headerUploadRef.value?.clearFiles()
formUploadRef.value?.clearFiles()
}
// 上传
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 格式!')
clearUploadRefs()
return
}
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>

View File

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

View File

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

View File

@@ -155,6 +155,20 @@ const tableStore = new TableStore({
/* { field: 'comFlag', title: '通讯状态 ', minWidth: 120 },*/
{ field: 'id', title: '监测点序号', minWidth: 90 },
{
field: 'comFlag',
title: '通讯状态',
render: 'tag',
minWidth: 80,
custom: {
中断: 'danger',
正常: 'success'
},
replaceValue: {
中断: '中断',
正常: '正常'
}
},
{
field: 'runFlag',
title: '运行状态',
@@ -166,6 +180,13 @@ const tableStore = new TableStore({
检修: 'warning',
调试: 'warning',
退运: 'danger'
},
replaceValue: {
投运: '投运',
停运: '停运',
检修: '检修',
调试: '调试',
退运: '退运'
}
},
{ field: 'devName', title: '监测终端编号 ', minWidth: 140 },

View File

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

View File

@@ -1,14 +1,19 @@
<template>
<div class='default-main'>
<div class='custom-table-header'>
<div class="default-main">
<div class="custom-table-header">
<div class="title">字典树列表</div>
<el-button :icon='Plus' type='primary' @click='addMenu'>新增字典类型</el-button>
<el-button :icon="Plus" type="primary" @click="addMenu">新增字典类型</el-button>
</div>
<Table ref='tableRef' />
<PopupForm ref='popupFormRef'></PopupForm>
<Table
ref="tableRef"
:row-config="{ isCurrent: true, isHover: true, keyField: 'id' }"
:tree-config="{ children: 'children', reserve: true }"
:scroll-y="{ enabled: false }"
/>
<PopupForm ref="popupFormRef"></PopupForm>
</div>
</template>
<script setup lang='ts'>
<script setup lang="ts">
import { Plus } from '@element-plus/icons-vue'
import { ref, onMounted, provide } from 'vue'
import TableStore from '@/utils/tableStore'
@@ -49,7 +54,8 @@ const tableStore = new TableStore({
}
},
{
title: '操作',fixed: 'right',
title: '操作',
fixed: 'right',
width: '180',
render: 'buttons',

View File

@@ -13,7 +13,11 @@
show-word-limit
/>
</div>
<Table />
<Table
:row-config="{ isCurrent: true, isHover: true, keyField: 'id' }"
:tree-config="{ children: 'children', reserve: true }"
:scroll-y="{ enabled: false }"
/>
<PopupForm ref="popupFormRef" @init="tableStore.index()"></PopupForm>
<PopupPoint ref="popupPointRef"></PopupPoint>
</div>
@@ -77,7 +81,8 @@ const tableStore = new TableStore({
}
},
{
title: '操作',fixed: 'right',
title: '操作',
fixed: 'right',
align: 'center',
width: '180',
render: 'buttons',

View File

@@ -12,7 +12,12 @@
/>
<el-button :icon="Plus" type="primary" @click="addMenu" class="ml10">新增</el-button>
</div>
<Table @currentChange="currentChange" />
<Table
@currentChange="currentChange"
:row-config="{ isCurrent: true, isHover: true, keyField: 'id' }"
:tree-config="{ children: 'children', reserve: true }"
:scroll-y="{ enabled: false }"
/>
<popupMenu ref="popupRef" @init="emits('init')"></popupMenu>
</div>
</template>

View File

@@ -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,7 +135,7 @@ const submitForm = async () => {
}
/** 关闭按钮 */
const close = () => {
delView(unref(currentRoute))
navTabs.closeTab(unref(currentRoute))
go(-1)
}