初始化

This commit is contained in:
2026-03-09 17:35:53 +08:00
commit 74f322b7c2
577 changed files with 57404 additions and 0 deletions

View File

@@ -0,0 +1,57 @@
import { defineStore } from 'pinia'
import { ADMIN_INFO } from '/@/stores/constant/cacheKey'
import type { AdminInfo } from '/@/stores/interface'
export const useAdminInfo = defineStore('adminInfo', {
state: (): AdminInfo => {
return {
id: 0,
username: '',
nickname: '',
avatar: '',
last_login_time: '',
token: '',
refresh_token: '',
super: false,
}
},
actions: {
/**
* 状态批量填充
* @param state 新状态数据
* @param [exclude=true] 是否排除某些字段(忽略填充),默认值 true 排除 token 和 refresh_token传递 false 则不排除,还可传递 string[] 指定排除字段列表
*/
dataFill(state: Partial<AdminInfo>, exclude: boolean | string[] = true) {
if (exclude === true) {
exclude = ['token', 'refresh_token']
} else if (exclude === false) {
exclude = []
}
if (Array.isArray(exclude)) {
exclude.forEach((item) => {
delete state[item as keyof AdminInfo]
})
}
this.$patch(state)
},
removeToken() {
this.token = ''
this.refresh_token = ''
},
setToken(token: string, type: 'auth' | 'refresh') {
const field = type == 'auth' ? 'token' : 'refresh_token'
this[field] = token
},
getToken(type: 'auth' | 'refresh' = 'auth') {
return type === 'auth' ? this.token : this.refresh_token
},
setSuper(val: boolean) {
this.super = val
},
},
persist: {
key: ADMIN_INFO,
},
})

View File

@@ -0,0 +1,82 @@
import { defineStore } from 'pinia'
import router from '../router'
import { baAccountLogout } from '/@/api/backend/index'
import { BA_ACCOUNT } from '/@/stores/constant/cacheKey'
import type { UserInfo } from '/@/stores/interface'
import { Local } from '/@/utils/storage'
export const useBaAccount = defineStore('baAccount', {
state: (): Partial<UserInfo> => {
return {
id: 0,
username: '',
nickname: '',
email: '',
mobile: '',
avatar: '',
gender: 0,
birthday: '',
money: 0,
score: 0,
motto: '',
token: '',
refresh_token: '',
}
},
actions: {
/**
* 状态批量填充
* @param state 新状态数据
* @param [exclude=true] 是否排除某些字段(忽略填充),默认值 true 排除 token 和 refresh_token传递 false 则不排除,还可传递 string[] 指定排除字段列表
*/
dataFill(state: Partial<UserInfo>, exclude: boolean | string[] = true) {
if (exclude === true) {
exclude = ['token', 'refresh_token']
} else if (exclude === false) {
exclude = []
}
if (Array.isArray(exclude)) {
exclude.forEach((item) => {
delete state[item as keyof UserInfo]
})
}
this.$patch(state)
},
removeToken() {
this.token = ''
this.refresh_token = ''
},
getGenderIcon() {
let icon = { name: 'fa fa-transgender-alt', color: 'var(--el-text-color-secondary)' }
switch (this.gender) {
case 1:
icon = { name: 'fa fa-mars-stroke-v', color: 'var(--el-color-primary)' }
break
case 2:
icon = { name: 'fa fa-mars-stroke', color: 'var(--el-color-danger)' }
break
}
return icon
},
setToken(token: string, type: 'auth' | 'refresh') {
const field = type == 'auth' ? 'token' : 'refresh_token'
this[field] = token
},
getToken(type: 'auth' | 'refresh' = 'auth') {
return type === 'auth' ? this.token : this.refresh_token
},
logout() {
baAccountLogout().then((res) => {
if (res.code == 1) {
Local.remove(BA_ACCOUNT)
router.go(0)
}
})
},
},
persist: {
key: BA_ACCOUNT,
},
})

111
web/src/stores/config.ts Normal file
View File

@@ -0,0 +1,111 @@
import { defineStore } from 'pinia'
import { reactive } from 'vue'
import { STORE_CONFIG } from '/@/stores/constant/cacheKey'
import type { Crud, Lang, Layout } from '/@/stores/interface'
export const useConfig = defineStore(
'config',
() => {
const layout: Layout = reactive({
// 全局
showDrawer: false,
shrink: false,
layoutMode: 'Default',
mainAnimation: 'slide-right',
isDark: false,
// 侧边栏
menuBackground: ['#ffffff', '#1d1e1f'],
menuColor: ['#303133', '#CFD3DC'],
menuActiveBackground: ['#ffffff', '#1d1e1f'],
menuActiveColor: ['#409eff', '#3375b9'],
menuTopBarBackground: ['#fcfcfc', '#1d1e1f'],
menuWidth: 260,
menuDefaultIcon: 'fa fa-circle-o',
menuCollapse: false,
menuUniqueOpened: false,
menuShowTopBar: true,
// 顶栏
headerBarTabColor: ['#000000', '#CFD3DC'],
headerBarTabActiveBackground: ['#ffffff', '#1d1e1f'],
headerBarTabActiveColor: ['#000000', '#409EFF'],
headerBarBackground: ['#ffffff', '#1d1e1f'],
headerBarHoverBackground: ['#f5f5f5', '#18222c'],
})
const lang: Lang = reactive({
defaultLang: 'zh-cn',
fallbackLang: 'zh-cn',
langArray: [
{ name: 'zh-cn', value: '中文简体' },
{ name: 'en', value: 'English' },
],
})
const crud: Crud = reactive({
syncType: 'manual',
syncedUpdate: 'yes',
syncAutoPublic: 'no',
})
function menuWidth() {
if (layout.shrink) {
return layout.menuCollapse ? '0px' : layout.menuWidth + 'px'
}
// 菜单是否折叠
return layout.menuCollapse ? '64px' : layout.menuWidth + 'px'
}
function setLang(val: string) {
lang.defaultLang = val
}
function onSetLayoutColor(data = layout.layoutMode) {
// 切换布局时,如果是为默认配色方案,对菜单激活背景色重新赋值
const tempValue = layout.isDark ? { idx: 1, color: '#1d1e1f', newColor: '#141414' } : { idx: 0, color: '#ffffff', newColor: '#f5f5f5' }
if (
data == 'Classic' &&
layout.headerBarBackground[tempValue.idx] == tempValue.color &&
layout.headerBarTabActiveBackground[tempValue.idx] == tempValue.color
) {
layout.headerBarTabActiveBackground[tempValue.idx] = tempValue.newColor
} else if (
data == 'Default' &&
layout.headerBarBackground[tempValue.idx] == tempValue.color &&
layout.headerBarTabActiveBackground[tempValue.idx] == tempValue.newColor
) {
layout.headerBarTabActiveBackground[tempValue.idx] = tempValue.color
}
}
function setLayoutMode(data: string) {
layout.layoutMode = data
onSetLayoutColor(data)
}
const setLayout = (name: keyof Layout, value: any) => {
;(layout[name] as any) = value
}
const getColorVal = function (name: keyof Layout): string {
const colors = layout[name] as string[]
if (layout.isDark) {
return colors[1]
} else {
return colors[0]
}
}
const setCrud = (name: keyof Crud, value: any) => {
;(crud[name] as any) = value
}
return { layout, lang, crud, menuWidth, setLang, setLayoutMode, setLayout, getColorVal, onSetLayoutColor, setCrud }
},
{
persist: {
key: STORE_CONFIG,
},
}
)

View File

@@ -0,0 +1,25 @@
/**
* 本地缓存Key
*/
// 管理员资料
export const ADMIN_INFO = 'adminInfo'
// WEB端布局配置
export const STORE_CONFIG = 'storeConfig_v2'
// 后台标签页
export const STORE_TAB_VIEW_CONFIG = 'storeTabViewConfig'
// 终端
export const STORE_TERMINAL = 'storeTerminal'
// 工作时间
export const WORKING_TIME = 'workingTime'
// 切换到手机端前的上次布局方式
export const BEFORE_RESIZE_LAYOUT = 'beforeResizeLayout'
// 会员资料
export const USER_INFO = 'userInfo'
// ba官网用户信息
export const BA_ACCOUNT = 'ba_account'

View File

@@ -0,0 +1,8 @@
/**
* 公共常量定义
*/
/**
* 系统级 z-index 配置,比如全局通知消息的 z-index浏览器支持的最大值通常为 2147483647
*/
export const SYSTEM_ZINDEX = 2147483600

View File

@@ -0,0 +1,8 @@
export const enum taskStatus {
Waiting,
Connecting,
Executing,
Success,
Failed,
Unknown,
}

7
web/src/stores/index.ts Normal file
View File

@@ -0,0 +1,7 @@
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
export default pinia

View File

@@ -0,0 +1,203 @@
import type { RouteLocationNormalized, RouteRecordRaw } from 'vue-router'
export interface Layout {
/* 全局 - s */
// 是否显示布局配置抽屉
showDrawer: boolean
// 是否收缩布局(小屏设备)
shrink: boolean
// 后台布局方式,可选值<Default|Classic|Streamline|Double>
layoutMode: string
// 后台主页面切换动画,可选值<slide-right|slide-left|el-fade-in-linear|el-fade-in|el-zoom-in-center|el-zoom-in-top|el-zoom-in-bottom>
mainAnimation: string
// 是否暗黑模式
isDark: boolean
/* 全局 - e */
/* 侧边栏 - s */
// 侧边菜单宽度展开时单位px
menuWidth: number
// 侧边菜单项默认图标
menuDefaultIcon: string
// 是否水平折叠收起菜单
menuCollapse: boolean
// 是否只保持一个子菜单的展开(手风琴)
menuUniqueOpened: boolean
// 显示菜单栏顶栏LOGO
menuShowTopBar: boolean
// 侧边菜单背景色
menuBackground: string[]
// 侧边菜单文字颜色
menuColor: string[]
// 侧边菜单激活项背景色
menuActiveBackground: string[]
// 侧边菜单激活项文字色
menuActiveColor: string[]
// 侧边菜单顶栏背景色
menuTopBarBackground: string[]
/* 侧边栏 - e */
/* 顶栏 - s */
// 顶栏文字色
headerBarTabColor: string[]
// 顶栏背景色
headerBarBackground: string[]
// 顶栏悬停时背景色
headerBarHoverBackground: string[]
// 顶栏激活项背景色
headerBarTabActiveBackground: string[]
// 顶栏激活项文字色
headerBarTabActiveColor: string[]
/* 顶栏 - e */
}
export interface Lang {
// 默认语言,可选值<zh-cn|en>
defaultLang: string
// 当在默认语言包找不到翻译时,继续在 fallbackLang 语言包内查找翻译
fallbackLang: string
// 支持的语言列表
langArray: { name: string; value: string }[]
}
export interface Crud {
// 日志同步方式
syncType: 'manual' | 'automatic'
// 已同步记录被更新时,是否自动重新同步
syncedUpdate: 'no' | 'yes'
// 自动同步时是否分享至开源社区
syncAutoPublic: 'no' | 'yes'
}
export interface NavTabs {
// 激活 tab 的 index
activeIndex: number
// 激活的 tab
activeRoute: RouteLocationNormalized | null
// tab 列表
tabsView: RouteLocationNormalized[]
// 当前 tab 是否全屏
tabFullScreen: boolean
// 从后台加载到的菜单路由列表
tabsViewRoutes: RouteRecordRaw[]
// 权限节点
authNode: Map<string, string[]>
}
export interface MemberCenter {
// 是否开启会员中心
open: boolean
// 布局模式
layoutMode: string
// 从后台加载到的菜单
viewRoutes: RouteRecordRaw[]
// 是否显示一级菜单标题(当有多个一级菜单分组时显示)
showHeadline: boolean
// 权限节点
authNode: Map<string, string[]>
// 收缩布局(小屏设备)
shrink: boolean
// 菜单展开状态(小屏设备)
menuExpand: boolean
// 顶栏会员菜单下拉项
navUserMenus: RouteRecordRaw[]
}
export interface AdminInfo {
id: number
username: string
nickname: string
avatar: string
last_login_time: string
token: string
refresh_token: string
// 是否是 superAdmin用于判定是否显示终端按钮等不做任何权限判断
super: boolean
}
export interface UserInfo {
id: number
username: string
nickname: string
email: string
mobile: string
gender: number
birthday: string
money: number
score: number
avatar: string
last_login_time: string
last_login_ip: string
join_time: string
motto: string
token: string
refresh_token: string
}
export interface TaskItem {
// 任务唯一标识
uuid: string
// 创建时间
createTime: string
// 状态
status: number
// 命令
command: string
// 命令执行日志
message: string[]
// 显示命令执行日志
showMessage: boolean
// 失败阻断后续命令执行
blockOnFailure: boolean
// 扩展信息,自动发送到后台
extend: string
// 执行结果回调
callback: Function
}
export interface Terminal {
// 显示终端窗口
show: boolean
// 在后台终端按钮上显示一个红点
showDot: boolean
// 任务列表
taskList: TaskItem[]
// 包管理器
packageManager: string
// 显示终端设置窗口
showConfig: boolean
// 开始任务时自动清理已完成任务
automaticCleanupTask: string
// PHP 开发服务环境
phpDevelopmentServer: boolean
// NPM 源
npmRegistry: string
// composer 源
composerRegistry: string
}
export interface SiteConfig {
// 站点名称
siteName: string
// 系统版本号
version: string
// 内容分发网络URL
cdnUrl: string
// 中心接口地址(用于请求模块市场的数据等用途)
apiUrl: string
// 上传配置
upload: {
mode: string
[key: string]: any
}
// 顶部导航菜单数据
headNav: RouteRecordRaw[]
// 备案号
recordNumber?: string
// 内容分发网络URL的参数格式如 imageMogr2/format/heif
cdnUrlParams: string
// 初始化状态
initialize: boolean
userInitialize: boolean
}

View File

@@ -0,0 +1,84 @@
import { defineStore } from 'pinia'
import { reactive } from 'vue'
import type { RouteRecordRaw } from 'vue-router'
import type { MemberCenter } from '/@/stores/interface/index'
export const useMemberCenter = defineStore('memberCenter', () => {
const state: MemberCenter = reactive({
open: true,
layoutMode: 'Default',
viewRoutes: [],
showHeadline: false,
authNode: new Map(),
shrink: false,
menuExpand: false,
navUserMenus: [],
})
const setNavUserMenus = (menus: RouteRecordRaw[]) => {
state.navUserMenus = menus
}
const mergeNavUserMenus = (menus: RouteRecordRaw[]) => {
state.navUserMenus = [...state.navUserMenus, ...menus]
}
const setAuthNode = (key: string, data: string[]) => {
state.authNode.set(key, data)
}
const mergeAuthNode = (authNode: Map<string, string[]>) => {
state.authNode = new Map([...state.authNode, ...authNode])
}
const setViewRoutes = (data: RouteRecordRaw[]): void => {
state.viewRoutes = encodeRoutesURI(data)
}
const setShowHeadline = (show: boolean): void => {
state.showHeadline = show
}
const setShrink = (shrink: boolean) => {
state.shrink = shrink
}
const setStatus = (status: boolean) => {
state.open = status
}
const setLayoutMode = (mode: string) => {
state.layoutMode = mode
}
const toggleMenuExpand = (expand = !state.menuExpand) => {
state.menuExpand = expand
}
return {
state,
setNavUserMenus,
mergeNavUserMenus,
setAuthNode,
mergeAuthNode,
setViewRoutes,
setShowHeadline,
setShrink,
setStatus,
setLayoutMode,
toggleMenuExpand,
}
})
function encodeRoutesURI(data: RouteRecordRaw[]) {
data.forEach((item) => {
if (item.meta?.menu_type == 'iframe') {
item.path = '/user/iframe/' + encodeURIComponent(item.path)
}
if (item.children && item.children.length) {
item.children = encodeRoutesURI(item.children)
}
})
return data
}

245
web/src/stores/navTabs.ts Normal file
View File

@@ -0,0 +1,245 @@
import { isEmpty } from 'lodash-es'
import { defineStore } from 'pinia'
import { reactive } from 'vue'
import type { RouteLocationNormalized, RouteRecordRaw } from 'vue-router'
import { i18n } from '../lang'
import { adminBaseRoutePath } from '/@/router/static/adminBase'
import { STORE_TAB_VIEW_CONFIG } from '/@/stores/constant/cacheKey'
import type { NavTabs } from '/@/stores/interface/index'
import { layoutNavTabsRef } from '/@/stores/refs'
export const useNavTabs = defineStore(
'navTabs',
() => {
const state: NavTabs = reactive({
activeIndex: 0,
activeRoute: null,
tabsView: [],
tabFullScreen: false,
tabsViewRoutes: [],
authNode: new Map(),
})
/**
* 通过路由路径关闭tab
* @param fullPath 需要关闭的 tab 的路径
*/
const closeTabByPath = (fullPath: string) => {
layoutNavTabsRef.value?.closeTabByPath(fullPath)
}
/**
* 关闭所有tab
* @param menu 需要保留的标签,否则关闭全部标签并打开第一个路由
*/
const closeAllTab = (menu?: RouteLocationNormalized) => {
layoutNavTabsRef.value?.closeAllTab(menu)
}
/**
* 修改 tab 标题
* @param fullPath 需要修改标题的 tab 的路径
* @param title 新的标题
*/
const updateTabTitle = (fullPath: string, title: string) => {
layoutNavTabsRef.value?.updateTabTitle(fullPath, title)
}
/**
* 添加 tab内部
* ps: router.push 时可自动完成 tab 添加,无需调用此方法
*/
function _addTab(route: RouteLocationNormalized) {
const tabView = { ...route, matched: [], meta: { ...route.meta } }
if (!tabView.meta.addtab) return
// 通过路由寻找菜单的原始数据
const tabViewRoute = getTabsViewDataByRoute(tabView)
if (tabViewRoute && tabViewRoute.meta) {
tabView.name = tabViewRoute.name
tabView.meta.id = tabViewRoute.meta.id
tabView.meta.title = tabViewRoute.meta.title
}
for (const key in state.tabsView) {
// 菜单已在 tabs 存在,更新 params 和 query
if (state.tabsView[key].meta.id === tabView.meta.id || state.tabsView[key].fullPath == tabView.fullPath) {
state.tabsView[key].fullPath = tabView.fullPath
state.tabsView[key].params = !isEmpty(tabView.params) ? tabView.params : state.tabsView[key].params
state.tabsView[key].query = !isEmpty(tabView.query) ? tabView.query : state.tabsView[key].query
return
}
}
if (typeof tabView.meta.title == 'string') {
tabView.meta.title = i18n.global.te(tabView.meta.title) ? i18n.global.t(tabView.meta.title) : tabView.meta.title
}
state.tabsView.push(tabView)
}
/**
* 设置激活 tab内部
* ps: router.push 时可自动完成 tab 激活,无需调用此方法
*/
const _setActiveRoute = (route: RouteLocationNormalized): void => {
const currentRouteIndex: number = state.tabsView.findIndex((item: RouteLocationNormalized) => {
return item.fullPath === route.fullPath
})
if (currentRouteIndex === -1) return
state.activeRoute = route
state.activeIndex = currentRouteIndex
}
/**
* 关闭 tab内部
* ps: 使用 closeTabByPath 代替
*/
function _closeTab(route: RouteLocationNormalized) {
state.tabsView.map((v, k) => {
if (v.fullPath == route.fullPath) {
state.tabsView.splice(k, 1)
return
}
})
}
/**
* 关闭多个标签(内部)
* ps使用 closeAllTab 代替
*/
const _closeTabs = (retainMenu: RouteLocationNormalized | false = false) => {
if (retainMenu) {
state.tabsView = [retainMenu]
} else {
state.tabsView = []
}
}
/**
* 更新标签标题(内部)
* ps: 使用 updateTabTitle 代替
*/
const _updateTabTitle = (fullPath: string, title: string) => {
for (const key in state.tabsView) {
if (state.tabsView[key].fullPath == fullPath) {
state.tabsView[key].meta.title = title
break
}
}
}
/**
* 设置从后台加载到的菜单路由列表
*/
const setTabsViewRoutes = (data: RouteRecordRaw[]): void => {
state.tabsViewRoutes = encodeRoutesURI(data)
}
/**
* 以key设置权限节点
*/
const setAuthNode = (key: string, data: string[]) => {
state.authNode.set(key, data)
}
/**
* 覆盖设置权限节点
*/
const fillAuthNode = (data: Map<string, string[]>) => {
state.authNode = data
}
/**
* 设置当前 tab 是否全屏
* @param status 全屏状态
*/
const setFullScreen = (status: boolean): void => {
state.tabFullScreen = status
}
/**
* 寻找路由在菜单中的数据
* @param route 路由
* @param returnType 返回值要求:normal=返回被搜索的路径对应的菜单数据,above=返回被搜索的路径对应的上一级菜单数组
*/
const getTabsViewDataByRoute = (route: RouteLocationNormalized, returnType: 'normal' | 'above' = 'normal'): RouteRecordRaw | false => {
// 以完整路径寻找
let found = getTabsViewDataByPath(route.fullPath, state.tabsViewRoutes, returnType)
if (found) {
found.meta!.matched = route.fullPath
return found
}
// 以路径寻找
found = getTabsViewDataByPath(route.path, state.tabsViewRoutes, returnType)
if (found) {
found.meta!.matched = route.path
return found
}
return false
}
/**
* 递归的寻找路由路径在菜单中的数据
* @param path 路由路径
* @param menus 菜单数据(只有 path 代表完整 url没有 fullPath
* @param returnType 返回值要求:normal=返回被搜索的路径对应的菜单数据,above=返回被搜索的路径对应的上一级菜单数组
*/
const getTabsViewDataByPath = (path: string, menus: RouteRecordRaw[], returnType: 'normal' | 'above'): RouteRecordRaw | false => {
for (const key in menus) {
// 找到目标
if (menus[key].path === path) {
return menus[key]
}
// 从子级继续寻找
if (menus[key].children && menus[key].children.length) {
const find = getTabsViewDataByPath(path, menus[key].children, returnType)
if (find) {
return returnType == 'above' ? menus[key] : find
}
}
}
return false
}
return {
state,
closeAllTab,
closeTabByPath,
updateTabTitle,
setTabsViewRoutes,
setAuthNode,
fillAuthNode,
setFullScreen,
getTabsViewDataByPath,
getTabsViewDataByRoute,
_addTab,
_closeTab,
_closeTabs,
_setActiveRoute,
_updateTabTitle,
}
},
{
persist: {
key: STORE_TAB_VIEW_CONFIG,
pick: ['state.tabFullScreen'],
},
}
)
/**
* 对iframe的url进行编码
*/
function encodeRoutesURI(data: RouteRecordRaw[]) {
data.forEach((item) => {
if (item.meta?.menu_type == 'iframe') {
item.path = adminBaseRoutePath + '/iframe/' + encodeURIComponent(item.path)
}
if (item.children && item.children.length) {
item.children = encodeRoutesURI(item.children)
}
})
return data
}

34
web/src/stores/refs.ts Normal file
View File

@@ -0,0 +1,34 @@
/**
* references
* 全局提供:引用(指向)一些对象(组件)的句柄
*/
import type { ScrollbarInstance } from 'element-plus'
import type { CSSProperties } from 'vue'
import { computed, ref } from 'vue'
import NavTabs from '/@/layouts/backend/components/navBar/tabs.vue'
import { mainHeight } from '/@/utils/layout'
/**
* 后台顶栏(tabs)组件ref仅默认和经典布局
*/
export const layoutNavTabsRef = ref<InstanceType<typeof NavTabs>>()
/**
* 前后台布局的主体的滚动条组件ref
*/
export const layoutMainScrollbarRef = ref<ScrollbarInstance>()
/**
* 前后台布局的主体滚动条的额外样式,包括高度
*/
export const layoutMainScrollbarStyle = computed<CSSProperties>(() => mainHeight())
/**
* 前后台布局的菜单组件ref
*/
export const layoutMenuRef = ref<ScrollbarInstance>()
/**
* 前后台布局的菜单栏滚动条组件ref
*/
export const layoutMenuScrollbarRef = ref<ScrollbarInstance>()

View File

@@ -0,0 +1,37 @@
import { defineStore } from 'pinia'
import type { RouteRecordRaw } from 'vue-router'
import type { SiteConfig } from '/@/stores/interface'
export const useSiteConfig = defineStore('siteConfig', {
state: (): SiteConfig => {
return {
siteName: '',
version: '',
cdnUrl: '',
apiUrl: '',
upload: {
mode: 'local',
},
headNav: [],
recordNumber: '',
cdnUrlParams: '',
initialize: false,
userInitialize: false,
}
},
actions: {
dataFill(state: SiteConfig) {
// 使用 this.$patch(state) 时 headNav 的类型异常,直接赋值
this.$state = state
},
setHeadNav(headNav: RouteRecordRaw[]) {
this.headNav = headNav
},
setInitialize(initialize: boolean) {
this.initialize = initialize
},
setUserInitialize(userInitialize: boolean) {
this.userInitialize = userInitialize
},
},
})

292
web/src/stores/terminal.ts Normal file
View File

@@ -0,0 +1,292 @@
import { ElNotification } from 'element-plus'
import { defineStore } from 'pinia'
import { nextTick, reactive } from 'vue'
import { buildTerminalUrl } from '/@/api/common'
import { i18n } from '/@/lang/index'
import { STORE_TERMINAL } from '/@/stores/constant/cacheKey'
import { SYSTEM_ZINDEX } from '/@/stores/constant/common'
import { taskStatus } from '/@/stores/constant/terminalTaskStatus'
import type { Terminal } from '/@/stores/interface/index'
import { timeFormat } from '/@/utils/common'
import { uuid } from '/@/utils/random'
import { closeHotUpdate, openHotUpdate } from '/@/utils/vite'
export const useTerminal = defineStore(
'terminal',
() => {
const state: Terminal = reactive({
show: false,
showDot: false,
taskList: [],
packageManager: 'pnpm',
showConfig: false,
automaticCleanupTask: '1',
phpDevelopmentServer: false,
npmRegistry: 'unknown',
composerRegistry: 'unknown',
})
function init() {
for (const key in state.taskList) {
if (state.taskList[key].status == taskStatus.Connecting || state.taskList[key].status == taskStatus.Executing) {
state.taskList[key].status = taskStatus.Unknown
}
}
}
function toggle(val = !state.show) {
state.show = val
if (val) {
toggleDot(false)
}
}
function toggleDot(val = !state.showDot) {
state.showDot = val
}
function toggleConfigDialog(val = !state.showConfig) {
toggle(!val)
state.showConfig = val
}
function changeRegistry(val: string, type: 'npm' | 'composer') {
state[type == 'npm' ? 'npmRegistry' : 'composerRegistry'] = val
}
function changePackageManager(val: string) {
state.packageManager = val
}
function changePHPDevelopmentServer(val: boolean) {
state.phpDevelopmentServer = val
}
function changeAutomaticCleanupTask(val: '0' | '1') {
state.automaticCleanupTask = val
}
function setTaskStatus(idx: number, status: number) {
state.taskList[idx].status = status
if ((status == taskStatus.Failed || status == taskStatus.Unknown) && state.taskList[idx].blockOnFailure) {
setTaskShowMessage(idx, true)
}
}
function taskCompleted(idx: number) {
// 命令执行完毕,重新打开热更新
openHotUpdate('terminal')
if (typeof state.taskList[idx].callback != 'function') return
const status = state.taskList[idx].status
if (status == taskStatus.Failed || status == taskStatus.Unknown) {
state.taskList[idx].callback(taskStatus.Failed)
} else if (status == taskStatus.Success) {
state.taskList[idx].callback(taskStatus.Success)
}
}
function setTaskShowMessage(idx: number, val = !state.taskList[idx].showMessage) {
state.taskList[idx].showMessage = val
}
function addTaskMessage(idx: number, message: string) {
if (!state.show) toggleDot(true)
state.taskList[idx].message = state.taskList[idx].message.concat(message)
nextTick(() => {
execMessageScrollbarKeep(state.taskList[idx].uuid)
})
}
function addTask(command: string, blockOnFailure = true, extend = '', callback: Function = () => {}) {
if (!state.show) toggleDot(true)
state.taskList = state.taskList.concat({
uuid: uuid(),
createTime: timeFormat(),
status: taskStatus.Waiting,
command: command,
message: [],
showMessage: false,
blockOnFailure: blockOnFailure,
extend: extend,
callback: callback,
})
// 清理任务列表
if (parseInt(state.automaticCleanupTask) === 1) {
clearSuccessTask()
}
// 检查是否有已经失败的任务
if (state.show === false) {
for (const key in state.taskList) {
if (state.taskList[key].status == taskStatus.Failed || state.taskList[key].status == taskStatus.Unknown) {
ElNotification({
type: 'error',
message: i18n.global.t('terminal.Newly added tasks will never start because they are blocked by failed tasks'),
zIndex: SYSTEM_ZINDEX,
})
break
}
}
}
startTask()
}
function addTaskPM(command: string, blockOnFailure = true, extend = '', callback: Function = () => {}) {
addTask(command + '.' + state.packageManager, blockOnFailure, extend, callback)
}
function delTask(idx: number) {
if (state.taskList[idx].status != taskStatus.Connecting && state.taskList[idx].status != taskStatus.Executing) {
state.taskList.splice(idx, 1)
}
startTask()
}
function startTask() {
let taskKey = null
// 寻找可以开始执行的命令
for (const key in state.taskList) {
if (state.taskList[key].status == taskStatus.Waiting) {
taskKey = parseInt(key)
break
}
if (state.taskList[key].status == taskStatus.Connecting || state.taskList[key].status == taskStatus.Executing) {
break
}
if (state.taskList[key].status == taskStatus.Success) {
continue
}
if (state.taskList[key].status == taskStatus.Failed || state.taskList[key].status == taskStatus.Unknown) {
if (state.taskList[key].blockOnFailure) {
break
} else {
continue
}
}
}
if (taskKey !== null) {
setTaskStatus(taskKey, taskStatus.Connecting)
startEventSource(taskKey)
}
}
function startEventSource(taskKey: number) {
// 命令执行期间禁用热更新
closeHotUpdate('terminal')
window.eventSource = new EventSource(
buildTerminalUrl(state.taskList[taskKey].command, state.taskList[taskKey].uuid, state.taskList[taskKey].extend)
)
window.eventSource.onmessage = function (e) {
const data = JSON.parse(e.data)
if (!data || !data.data) {
return
}
const taskIdx = findTaskIdxFromUuid(data.uuid)
if (taskIdx === false) {
return
}
if (data.data == 'command-exec-error') {
setTaskStatus(taskIdx, taskStatus.Failed)
window.eventSource.close()
taskCompleted(taskIdx)
startTask()
} else if (data.data == 'command-exec-completed') {
window.eventSource.close()
if (state.taskList[taskIdx].status != taskStatus.Success) {
setTaskStatus(taskIdx, taskStatus.Failed)
}
taskCompleted(taskIdx)
startTask()
} else if (data.data == 'command-link-success') {
setTaskStatus(taskIdx, taskStatus.Executing)
} else if (data.data == 'command-exec-success') {
setTaskStatus(taskIdx, taskStatus.Success)
} else {
addTaskMessage(taskIdx, data.data)
}
}
window.eventSource.onerror = function () {
window.eventSource.close()
const taskIdx = findTaskIdxFromGuess(taskKey)
if (taskIdx === false) return
setTaskStatus(taskIdx, taskStatus.Failed)
taskCompleted(taskIdx)
}
}
function retryTask(idx: number) {
state.taskList[idx].message = []
setTaskStatus(idx, taskStatus.Waiting)
startTask()
}
function clearSuccessTask() {
state.taskList = state.taskList.filter((item) => item.status != taskStatus.Success)
}
function findTaskIdxFromUuid(uuid: string) {
for (const key in state.taskList) {
if (state.taskList[key].uuid == uuid) {
return parseInt(key)
}
}
return false
}
function findTaskIdxFromGuess(idx: number) {
if (!state.taskList[idx]) {
let taskKey = -1
for (const key in state.taskList) {
if (state.taskList[key].status == taskStatus.Connecting || state.taskList[key].status == taskStatus.Executing) {
taskKey = parseInt(key)
}
}
return taskKey === -1 ? false : taskKey
} else {
return idx
}
}
function execMessageScrollbarKeep(uuid: string) {
const execMessageEl = document.querySelector('.exec-message-' + uuid) as Element
if (execMessageEl && execMessageEl.scrollHeight) {
execMessageEl.scrollTop = execMessageEl.scrollHeight
}
}
return {
state,
init,
toggle,
toggleDot,
setTaskStatus,
setTaskShowMessage,
addTaskMessage,
addTask,
addTaskPM,
delTask,
startTask,
retryTask,
clearSuccessTask,
toggleConfigDialog,
changeRegistry,
changePackageManager,
changePHPDevelopmentServer,
changeAutomaticCleanupTask,
}
},
{
persist: {
key: STORE_TERMINAL,
pick: ['state.showDot', 'state.taskList', 'state.automaticCleanupTask', 'state.npmRegistry', 'state.composerRegistry'],
},
}
)

View File

@@ -0,0 +1,88 @@
import { defineStore } from 'pinia'
import router from '../router'
import { postLogout } from '/@/api/frontend/user/index'
import { USER_INFO } from '/@/stores/constant/cacheKey'
import type { UserInfo } from '/@/stores/interface'
import { Local } from '/@/utils/storage'
export const useUserInfo = defineStore('userInfo', {
state: (): UserInfo => {
return {
id: 0,
username: '',
nickname: '',
email: '',
mobile: '',
avatar: '',
gender: 0,
birthday: '',
money: 0,
score: 0,
last_login_time: '',
last_login_ip: '',
join_time: '',
motto: '',
token: '',
refresh_token: '',
}
},
actions: {
/**
* 状态批量填充
* @param state 新状态数据
* @param [exclude=true] 是否排除某些字段(忽略填充),默认值 true 排除 token 和 refresh_token传递 false 则不排除,还可传递 string[] 指定排除字段列表
*/
dataFill(state: Partial<UserInfo>, exclude: boolean | string[] = true) {
if (exclude === true) {
exclude = ['token', 'refresh_token']
} else if (exclude === false) {
exclude = []
}
if (Array.isArray(exclude)) {
exclude.forEach((item) => {
delete state[item as keyof UserInfo]
})
}
this.$patch(state)
},
removeToken() {
this.token = ''
this.refresh_token = ''
},
setToken(token: string, type: 'auth' | 'refresh') {
const field = type == 'auth' ? 'token' : 'refresh_token'
this[field] = token
},
getToken(type: 'auth' | 'refresh' = 'auth') {
return type === 'auth' ? this.token : this.refresh_token
},
getGenderIcon() {
let icon = { name: 'fa fa-transgender-alt', color: 'var(--el-text-color-secondary)' }
switch (this.gender) {
case 1:
icon = { name: 'fa fa-mars-stroke-v', color: 'var(--el-color-primary)' }
break
case 2:
icon = { name: 'fa fa-mars-stroke', color: 'var(--el-color-danger)' }
break
}
return icon
},
logout() {
postLogout().then((res) => {
if (res.code == 1) {
Local.remove(USER_INFO)
router.go(0)
}
})
},
isLogin() {
return this.id && this.token
},
},
persist: {
key: USER_INFO,
},
})