feat:按需的在后台顶栏显示需要重启 Vite 热更新服务的警告按钮

This commit is contained in:
妙码生花 2024-10-28 00:14:28 +08:00
parent 8c6e17ac85
commit 978e77660d
8 changed files with 251 additions and 50 deletions

View File

@ -10,6 +10,7 @@ import { useRoute } from 'vue-router'
import { useConfig } from '/@/stores/config' import { useConfig } from '/@/stores/config'
import { setTitleFromRoute } from '/@/utils/common' import { setTitleFromRoute } from '/@/utils/common'
import iconfontInit from '/@/utils/iconfont' import iconfontInit from '/@/utils/iconfont'
import { init as viteInit } from '/@/utils/vite'
// modules import mark, Please do not remove. // modules import mark, Please do not remove.
const route = useRoute() const route = useRoute()
@ -19,6 +20,7 @@ const config = useConfig()
const { getLocaleMessage } = useI18n() const { getLocaleMessage } = useI18n()
const lang = getLocaleMessage(config.lang.defaultLang) as any const lang = getLocaleMessage(config.lang.defaultLang) as any
onMounted(() => { onMounted(() => {
viteInit()
iconfontInit() iconfontInit()
// Modules onMounted mark, Please do not remove. // Modules onMounted mark, Please do not remove.

View File

@ -74,4 +74,18 @@ export default {
'Newly added tasks will never start because they are blocked by failed tasks!(Web terminal)', 'Newly added tasks will never start because they are blocked by failed tasks!(Web terminal)',
'Failed to modify the source command, Please try again manually': 'Failed to modify the source command. Please try again manually.', 'Failed to modify the source command, Please try again manually': 'Failed to modify the source command. Please try again manually.',
}, },
vite: {
Later: '稍后',
'Restart hot update': '重启热更新',
'Close type terminal': 'WEB Terminal server',
'Close type crud': 'CRUD server',
'Close type modules': 'module install server',
'Close type config': 'system configuration server',
'Reload hot server title': 'Need to restart Vite hot update service',
'Reload hot server tips 1': 'To ensure that ',
'Reload hot server tips 2':
" is not interrupted, the system has temporarily suspended Vite's hot update function. During this period, changes to front-end files will not be updated in real-time and web pages will not be automatically reloaded. It has been detected that there are file updates during the service suspension period, and the hot update service needs to be restarted.",
'Reload hot server tips 3':
'The pause of hot updates does not affect the already loaded functions. You can continue to operate and click to restart the hot update service after everything is ready.',
},
} }

View File

@ -156,4 +156,9 @@ export default {
'Wait for installation': 'Wait for installation', 'Wait for installation': 'Wait for installation',
'Conflict pending': 'Conflict pending', 'Conflict pending': 'Conflict pending',
'Dependency to be installed': 'Dependency to be installed', 'Dependency to be installed': 'Dependency to be installed',
'Restart Vite hot server': 'Restart Vite hot server',
'Restart Vite hot server tips':
'Before successfully restarting the service, you can find the button to manually restart the service from the button group on the right side of the top bar.',
'Manual restart': 'Manual restart',
'Restart Now': 'Restart Now',
} }

View File

@ -74,4 +74,17 @@ export default {
'Newly added tasks will never start because they are blocked by failed tasks': '新添加的任务永远不会开始因为被失败的任务阻塞WEB终端', 'Newly added tasks will never start because they are blocked by failed tasks': '新添加的任务永远不会开始因为被失败的任务阻塞WEB终端',
'Failed to modify the source command, Please try again manually': '修改源的命令执行失败,请手动重试。', 'Failed to modify the source command, Please try again manually': '修改源的命令执行失败,请手动重试。',
}, },
vite: {
Later: '稍后',
'Restart hot update': '重启热更新',
'Close type terminal': 'WEB终端执行命令',
'Close type crud': 'CRUD代码生成服务',
'Close type modules': '模块安装服务',
'Close type config': '修改系统配置',
'Reload hot server title': '需要重启 Vite 热更新服务',
'Reload hot server tips 1': '为确保',
'Reload hot server tips 2':
'不被打断,系统暂停了 Vite 的热更新功能,期间前端文件变动将不会实时更新和自动重载网页,现检测到服务暂停期间存在文件更新,需要重启热更新服务。',
'Reload hot server tips 3': '热更新暂停不影响已经加载好的功能,您可以继续操作,并在一切就绪后再点击重新启动热更新服务。',
},
} }

View File

@ -148,4 +148,8 @@ export default {
'Wait for installation': '等待安装', 'Wait for installation': '等待安装',
'Conflict pending': '冲突待处理', 'Conflict pending': '冲突待处理',
'Dependency to be installed': '依赖待安装', 'Dependency to be installed': '依赖待安装',
'Restart Vite hot server': '重启 Vite 热更新服务',
'Restart Vite hot server tips': '在完成服务重启之前,您可以随时从顶栏右侧的按钮组中找到手动重启服务的按钮。',
'Manual restart': '手动重启',
'Restart Now': '立即重启',
} }

View File

@ -1,10 +1,43 @@
<template> <template>
<div class="nav-menus" :class="configStore.layout.layoutMode"> <div class="nav-menus" :class="configStore.layout.layoutMode">
<!-- 需要重启 Vite 热更新服务警告 -->
<el-popover
ref="reloadHotServerPopover"
@show="onCurrentNavMenu(true, 'reloadHotServer')"
@hide="onCurrentNavMenu(false, 'reloadHotServer')"
:width="360"
v-if="hotUpdateState.dirtyFile"
>
<div>
<div class="el-popover__title">{{ t('vite.Reload hot server title') }}</div>
<div class="reload-hot-server-content">
<p>
<span>{{ t('vite.Reload hot server tips 1') }}</span>
<span>{{ t(`vite.Close type ${hotUpdateState.closeType}`) }}</span>
<span>{{ t('vite.Reload hot server tips 2') }}</span>
</p>
<p>{{ t('vite.Reload hot server tips 3') }}</p>
<div class="reload-hot-server-buttons">
<el-button @click="onHotServerOpt('cancel')">{{ t('vite.Later') }}</el-button>
<el-button @click="onHotServerOpt('reload')" type="primary">{{ t('vite.Restart hot update') }}</el-button>
</div>
</div>
</div>
<template #reference>
<div class="nav-menu-item" :class="state.currentNavMenu == 'reloadHotServer' ? 'hover' : ''">
<Icon color="var(--el-color-danger)" class="nav-menu-icon" name="el-icon-Warning" size="18" />
</div>
</template>
</el-popover>
<!-- 站点主页 -->
<router-link class="h100" target="_blank" :title="t('Home')" to="/"> <router-link class="h100" target="_blank" :title="t('Home')" to="/">
<div class="nav-menu-item"> <div class="nav-menu-item">
<Icon :color="configStore.getColorVal('headerBarTabColor')" class="nav-menu-icon" name="el-icon-Monitor" size="18" /> <Icon :color="configStore.getColorVal('headerBarTabColor')" class="nav-menu-icon" name="el-icon-Monitor" size="18" />
</div> </div>
</router-link> </router-link>
<!-- 语言切换 -->
<el-dropdown <el-dropdown
@visible-change="onCurrentNavMenu($event, 'lang')" @visible-change="onCurrentNavMenu($event, 'lang')"
class="h100" class="h100"
@ -25,6 +58,8 @@
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown>
<!-- 全屏切换 -->
<div @click="onFullScreen" class="nav-menu-item" :class="state.isFullScreen ? 'hover' : ''"> <div @click="onFullScreen" class="nav-menu-item" :class="state.isFullScreen ? 'hover' : ''">
<Icon <Icon
:color="configStore.getColorVal('headerBarTabColor')" :color="configStore.getColorVal('headerBarTabColor')"
@ -35,11 +70,15 @@
/> />
<Icon :color="configStore.getColorVal('headerBarTabColor')" class="nav-menu-icon" v-else name="el-icon-FullScreen" size="18" /> <Icon :color="configStore.getColorVal('headerBarTabColor')" class="nav-menu-icon" v-else name="el-icon-FullScreen" size="18" />
</div> </div>
<!-- 终端 - 仅超管 -->
<div v-if="adminInfo.super" @click="terminal.toggle()" class="nav-menu-item pt2"> <div v-if="adminInfo.super" @click="terminal.toggle()" class="nav-menu-item pt2">
<el-badge :is-dot="terminal.state.showDot"> <el-badge :is-dot="terminal.state.showDot">
<Icon :color="configStore.getColorVal('headerBarTabColor')" class="nav-menu-icon" name="local-terminal" size="26" /> <Icon :color="configStore.getColorVal('headerBarTabColor')" class="nav-menu-icon" name="local-terminal" size="26" />
</el-badge> </el-badge>
</div> </div>
<!-- 清理缓存 - 仅超管 -->
<el-dropdown <el-dropdown
v-if="adminInfo.super" v-if="adminInfo.super"
@visible-change="onCurrentNavMenu($event, 'clear')" @visible-change="onCurrentNavMenu($event, 'clear')"
@ -61,6 +100,8 @@
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown>
<!-- 管理员信息 -->
<el-popover <el-popover
v-if="siteConfig.userInitialize" v-if="siteConfig.userInitialize"
@show="onCurrentNavMenu(true, 'adminInfo')" @show="onCurrentNavMenu(true, 'adminInfo')"
@ -92,33 +133,37 @@
</div> </div>
</div> </div>
</el-popover> </el-popover>
<!-- 配置 -->
<div @click="configStore.setLayout('showDrawer', true)" class="nav-menu-item"> <div @click="configStore.setLayout('showDrawer', true)" class="nav-menu-item">
<Icon :color="configStore.getColorVal('headerBarTabColor')" class="nav-menu-icon" name="fa fa-cogs" size="18" /> <Icon :color="configStore.getColorVal('headerBarTabColor')" class="nav-menu-icon" name="fa fa-cogs" size="18" />
</div> </div>
<Config /> <Config />
<TerminalVue /> <TerminalVue />
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { reactive } from 'vue' import { ElMessage, type PopoverInstance } from 'element-plus'
import { editDefaultLang } from '/@/lang'
import screenfull from 'screenfull' import screenfull from 'screenfull'
import { useConfig } from '/@/stores/config' import { reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import Config from './config.vue' import Config from './config.vue'
import { useAdminInfo } from '/@/stores/adminInfo'
import { useTerminal } from '/@/stores/terminal'
import { Local, Session } from '/@/utils/storage'
import { ADMIN_INFO, BA_ACCOUNT } from '/@/stores/constant/cacheKey'
import router from '/@/router'
import { routePush } from '/@/utils/router'
import { logout } from '/@/api/backend/index' import { logout } from '/@/api/backend/index'
import { postClearCache } from '/@/api/common' import { postClearCache } from '/@/api/common'
import TerminalVue from '/@/components/terminal/index.vue' import TerminalVue from '/@/components/terminal/index.vue'
import { fullUrl } from '/@/utils/common' import { editDefaultLang } from '/@/lang'
import router from '/@/router'
import { useAdminInfo } from '/@/stores/adminInfo'
import { useConfig } from '/@/stores/config'
import { ADMIN_INFO, BA_ACCOUNT } from '/@/stores/constant/cacheKey'
import { useSiteConfig } from '/@/stores/siteConfig' import { useSiteConfig } from '/@/stores/siteConfig'
import { useTerminal } from '/@/stores/terminal'
import { fullUrl } from '/@/utils/common'
import { routePush } from '/@/utils/router'
import { Local, Session } from '/@/utils/storage'
import { hotUpdateState, reloadServer } from '/@/utils/vite'
const { t } = useI18n() const { t } = useI18n()
@ -126,6 +171,7 @@ const adminInfo = useAdminInfo()
const configStore = useConfig() const configStore = useConfig()
const terminal = useTerminal() const terminal = useTerminal()
const siteConfig = useSiteConfig() const siteConfig = useSiteConfig()
const reloadHotServerPopover = ref<PopoverInstance>()
const state = reactive({ const state = reactive({
isFullScreen: false, isFullScreen: false,
@ -138,6 +184,14 @@ const onCurrentNavMenu = (status: boolean, name: string) => {
state.currentNavMenu = status ? name : '' state.currentNavMenu = status ? name : ''
} }
const onHotServerOpt = (opt: 'reload' | 'cancel') => {
if (opt == 'cancel') {
reloadHotServerPopover.value?.hide()
} else {
reloadServer('manual')
}
}
const onFullScreen = () => { const onFullScreen = () => {
if (!screenfull.isEnabled) { if (!screenfull.isEnabled) {
ElMessage.warning(t('layouts.Full screen is not supported')) ElMessage.warning(t('layouts.Full screen is not supported'))
@ -180,6 +234,16 @@ const onClearCache = (type: string) => {
border-radius: var(--el-border-radius-base); border-radius: var(--el-border-radius-base);
box-shadow: var(--el-box-shadow-light); box-shadow: var(--el-box-shadow-light);
} }
.reload-hot-server-content {
font-size: var(--el-font-size-small);
p {
margin-bottom: 6px;
}
.reload-hot-server-buttons {
display: flex;
justify-content: flex-end;
}
}
.nav-menus { .nav-menus {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@ -1,5 +1,40 @@
import { ref } from 'vue' import type { Plugin, ViteDevServer } from 'vite'
import { Plugin } from 'vite' import { reactive } from 'vue'
interface HotUpdateState {
// 热更新状态
switch: boolean
// 热更新关闭类型:terminal=WEB终端执行命令,crud=CRUD,modules=模块安装服务,config=修改系统配置
closeType: string
// 是否有脏文件(热更新 switch 为 false又触发了热更新就会产生脏文件
dirtyFile: boolean
}
/**
* Vite Vite
*/
export const hotUpdateState = reactive<HotUpdateState>({
switch: true,
closeType: '',
dirtyFile: false,
})
/**
* Vite
*/
export function init() {
if (import.meta.hot) {
// 监听 Vite 服务器通知热更新相关状态更新
import.meta.hot.on('custom:change-hot-update-state', (state: Partial<HotUpdateState>) => {
hotUpdateState.switch = state.switch ?? hotUpdateState.switch
hotUpdateState.closeType = state.closeType ?? hotUpdateState.closeType
hotUpdateState.dirtyFile = state.dirtyFile ?? hotUpdateState.dirtyFile
})
// 主动从 Vite 服务器获取当前热更新的相关状态
import.meta.hot.send('custom:get-hot-update-state', { type: 'init' })
}
}
/** /**
* *
@ -15,18 +50,10 @@ export function isProd(mode: string | undefined): boolean {
return mode === 'production' return mode === 'production'
} }
/**
*
*/
export const hotUpdateSwitch = ref(true)
/** /**
* *
*/ */
export const closeHotUpdate = (type: string) => { export const closeHotUpdate = (type: string) => {
if (!hotUpdateSwitch.value) return
hotUpdateSwitch.value = false
if (import.meta.hot) { if (import.meta.hot) {
import.meta.hot.send('custom:close-hot', { type }) import.meta.hot.send('custom:close-hot', { type })
} }
@ -36,9 +63,6 @@ export const closeHotUpdate = (type: string) => {
* *
*/ */
export const openHotUpdate = (type: string) => { export const openHotUpdate = (type: string) => {
if (hotUpdateSwitch.value) return
hotUpdateSwitch.value = true
if (import.meta.hot) { if (import.meta.hot) {
import.meta.hot.send('custom:open-hot', { type }) import.meta.hot.send('custom:open-hot', { type })
} }
@ -48,43 +72,92 @@ export const openHotUpdate = (type: string) => {
* *
*/ */
export const reloadServer = (type: string) => { export const reloadServer = (type: string) => {
hotUpdateSwitch.value = true
if (import.meta.hot) { if (import.meta.hot) {
import.meta.hot.send('custom:reload-server', { type }) import.meta.hot.send('custom:reload-server', { type })
} }
} }
/** /**
* * / Vite
*/ */
export const customHotUpdate = (): Plugin => { export const customHotUpdate = (): Plugin => {
let hotUpdateSwitch = true type Listeners = ((...args: any[]) => void)[]
let addFunctionBack: Listeners = []
let unlinkFunctionBack: Listeners = []
// 本服务端的热更新状态数据
const hotUpdateState: HotUpdateState = {
switch: true,
closeType: '',
dirtyFile: false,
}
/**
*
*/
const syncHotUpdateState = (server: ViteDevServer) => {
server.ws.send('custom:change-hot-update-state', hotUpdateState)
}
return { return {
name: 'vite-plugin-custom-hot-update', name: 'vite-plugin-custom-hot-update',
apply: 'serve', apply: 'serve',
configureServer(server) { configureServer(server) {
server.ws.on('custom:close-hot', () => { // 关闭热更新
hotUpdateSwitch = false server.ws.on('custom:close-hot', ({ type }) => {
hotUpdateState.switch = false
hotUpdateState.closeType = type
// 备份文件添加和删除时的函数列表
addFunctionBack = server.watcher.listeners('add') as Listeners
unlinkFunctionBack = server.watcher.listeners('unlink') as Listeners
// 关闭文件添加和删除的监听 // 关闭文件添加和删除的监听
server.watcher.removeAllListeners('add') server.watcher.removeAllListeners('add')
server.watcher.removeAllListeners('unlink') server.watcher.removeAllListeners('unlink')
})
server.ws.on('custom:open-hot', () => { syncHotUpdateState(server)
hotUpdateSwitch = true
// 文件添加时通知客户端新增了脏文件(文件删除无需记录为脏文件)
server.watcher.on('add', () => { server.watcher.on('add', () => {
server.restart() hotUpdateState.dirtyFile = true
}) syncHotUpdateState(server)
server.watcher.on('unlink', () => {
server.restart()
}) })
}) })
// 开启热更新
server.ws.on('custom:open-hot', () => {
hotUpdateState.switch = true
server.watcher.removeAllListeners('add')
server.watcher.removeAllListeners('unlink')
// 恢复备份的函数列表
for (const key in addFunctionBack) {
server.watcher.on('add', addFunctionBack[key])
}
for (const key in unlinkFunctionBack) {
server.watcher.on('unlink', unlinkFunctionBack[key])
}
syncHotUpdateState(server)
})
// 重启热更新
server.ws.on('custom:reload-server', () => { server.ws.on('custom:reload-server', () => {
hotUpdateSwitch = true
server.restart() server.restart()
}) })
// 客户端可从本服务端获取热更新服务状态数据
server.ws.on('custom:get-hot-update-state', () => {
syncHotUpdateState(server)
})
}, },
handleHotUpdate() { handleHotUpdate({ server }) {
if (!hotUpdateSwitch) { if (!hotUpdateState.switch) {
// 通知客户端出现了脏文件
hotUpdateState.dirtyFile = true
syncHotUpdateState(server)
return [] return []
} }
}, },

View File

@ -79,6 +79,26 @@
/> />
</div> </div>
</div> </div>
<div class="install-tis-box max-install-box" v-if="hotUpdateState.dirtyFile">
<div class="install-form">
<FormItem
:label="
(state.common.moduleState == moduleInstallState.DISABLE ? '' : t('module.After installation 2')) +
t('module.Restart Vite hot server')
"
v-model="form.reloadHotServer"
type="radio"
:input-attr="{
border: true,
content: {
0: t('vite.Later') + t('module.Manual restart'),
1: t('module.Restart Now'),
},
}"
:tip="t('module.Restart Vite hot server tips')"
/>
</div>
</div>
<el-button <el-button
v-blur v-blur
class="install-done-button" class="install-done-button"
@ -94,22 +114,23 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ElMessageBox } from 'element-plus'
import { reactive } from 'vue' import { reactive } from 'vue'
import { useI18n } from 'vue-i18n'
import { onRefreshTableData } from '../index'
import { state } from '../store' import { state } from '../store'
import { moduleInstallState } from '../types' import { moduleInstallState } from '../types'
import { onRefreshTableData } from '../index' import { dependentInstallComplete } from '/@/api/backend/module'
import { useTerminal } from '/@/stores/terminal'
import FormItem from '/@/components/formItem/index.vue' import FormItem from '/@/components/formItem/index.vue'
import { taskStatus } from '/@/stores/constant/terminalTaskStatus' import { taskStatus } from '/@/stores/constant/terminalTaskStatus'
import { ElMessageBox } from 'element-plus' import { useTerminal } from '/@/stores/terminal'
import { useI18n } from 'vue-i18n' import { hotUpdateState, reloadServer } from '/@/utils/vite'
import { dependentInstallComplete } from '/@/api/backend/module'
import { reloadServer } from '/@/utils/vite'
const { t } = useI18n() const { t } = useI18n()
const terminal = useTerminal() const terminal = useTerminal()
const form = reactive({ const form = reactive({
rebuild: 0, rebuild: 0,
reloadHotServer: 0,
}) })
const showTerminal = () => { const showTerminal = () => {
@ -121,15 +142,17 @@ const onSubmitInstallDone = () => {
if (form.rebuild == 1) { if (form.rebuild == 1) {
terminal.toggle(true) terminal.toggle(true)
terminal.addTaskPM('web-build', false, '', (res: number) => { terminal.addTaskPM('web-build', false, '', (res: number) => {
if (form.reloadHotServer == 1) {
reloadServer('modules')
}
if (res == taskStatus.Success) { if (res == taskStatus.Success) {
terminal.toggle(false) terminal.toggle(false)
if (state.common.moduleState != moduleInstallState.DISABLE) {
reloadServer('modules')
}
} }
}) })
} else if (state.common.moduleState != moduleInstallState.DISABLE) { } else {
reloadServer('modules') if (form.reloadHotServer == 1) {
reloadServer('modules')
}
} }
} }
@ -228,4 +251,7 @@ const onConfirmDepend = () => {
flex-wrap: wrap; flex-wrap: wrap;
} }
} }
.max-install-box {
width: 86%;
}
</style> </style>