fix: 完善服务重启交互
This commit is contained in:
@@ -63,7 +63,13 @@
|
||||
<button class="theme-toggle" type="button" :title="themeToggleTitle" @click="toggleTheme">
|
||||
<n-icon><component :is="themeToggleIcon" /></n-icon>
|
||||
</button>
|
||||
<button class="restart-btn" type="button" title="重启服务" @click="restartService">
|
||||
<button
|
||||
class="restart-btn"
|
||||
type="button"
|
||||
title="重启服务"
|
||||
:disabled="restarting"
|
||||
@click="restartService"
|
||||
>
|
||||
<n-icon><PowerOutline /></n-icon>
|
||||
</button>
|
||||
<n-button tertiary circle title="退出登录" class="logout-button" @click="logout">
|
||||
@@ -79,12 +85,19 @@
|
||||
</n-layout-content>
|
||||
</n-layout>
|
||||
</n-layout>
|
||||
|
||||
<div v-if="restarting" class="restart-overlay" role="status" aria-live="assertive">
|
||||
<div class="restart-overlay-content">
|
||||
<div class="restart-overlay-spinner"></div>
|
||||
<div class="restart-overlay-text">{{ restartOverlayText }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, h, onBeforeUnmount, onMounted, ref, watch, type Component } from 'vue';
|
||||
import { RouterView, useRoute, useRouter } from 'vue-router';
|
||||
import { useMessage, type MenuOption, NIcon } from 'naive-ui';
|
||||
import { useDialog, useMessage, type MenuOption, NIcon } from 'naive-ui';
|
||||
import {
|
||||
CloudUploadOutline,
|
||||
ConstructOutline,
|
||||
@@ -97,20 +110,24 @@ import {
|
||||
SunnyOutline
|
||||
} from '@vicons/ionicons5';
|
||||
|
||||
import { apiPost, clearToken, getToken, setToken, setUnauthorizedHandler } from './api/client';
|
||||
import type { LoginResponse } from './types';
|
||||
import { apiGet, apiPost, clearToken, getToken, setToken, setUnauthorizedHandler } from './api/client';
|
||||
import type { ApiMessage, LoginResponse, ServiceStatus } from './types';
|
||||
import LoginView from './components/LoginView.vue';
|
||||
import { pageHeader, resetPageHeader, resolveHeaderValue, type PageHeaderAction } from './composables/pageHeader';
|
||||
import { themeName, toggleAppTheme } from './composables/theme';
|
||||
|
||||
const message = useMessage();
|
||||
const dialog = useDialog();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const token = ref(getToken());
|
||||
const loginLoading = ref(false);
|
||||
const restarting = ref(false);
|
||||
const restartOverlayText = ref('正在重启服务...');
|
||||
const sidebarCollapsed = ref(localStorage.getItem('sidebarCollapsed') === 'true');
|
||||
const menuKeys = ['workflow', 'history', 'database', 'script', 'settings'] as const;
|
||||
type MenuKey = (typeof menuKeys)[number];
|
||||
let restartPollTimer: number | undefined;
|
||||
|
||||
const menuOptions: MenuOption[] = [
|
||||
{ label: '数据上传', key: 'workflow', icon: renderIcon(CloudUploadOutline) },
|
||||
@@ -139,6 +156,9 @@ onMounted(() => {
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('dragover', preventWindowFileDrop, { capture: true });
|
||||
window.removeEventListener('drop', preventWindowFileDrop, { capture: true });
|
||||
if (restartPollTimer !== undefined) {
|
||||
window.clearTimeout(restartPollTimer);
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
@@ -180,12 +200,63 @@ function toggleTheme() {
|
||||
}
|
||||
|
||||
async function restartService() {
|
||||
if (restarting.value) return;
|
||||
|
||||
dialog.warning({
|
||||
title: '重启服务',
|
||||
content: '确定要重启服务吗?这将中断当前所有操作。',
|
||||
positiveText: '重启',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: () => {
|
||||
void executeRestartService();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function executeRestartService() {
|
||||
restarting.value = true;
|
||||
restartOverlayText.value = '正在重启服务...';
|
||||
|
||||
try {
|
||||
await apiPost('/api/service/restart');
|
||||
message.success('服务正在重启');
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '重启服务失败');
|
||||
const result = await apiPost<ApiMessage>('/api/service/restart');
|
||||
restartOverlayText.value = result.message || '正在等待服务恢复...';
|
||||
} catch {
|
||||
restartOverlayText.value = '正在等待服务恢复...';
|
||||
}
|
||||
|
||||
pollServiceStatus();
|
||||
}
|
||||
|
||||
function pollServiceStatus() {
|
||||
let attempts = 0;
|
||||
const maxAttempts = 60;
|
||||
const pollInterval = 5000;
|
||||
|
||||
const checkService = async () => {
|
||||
attempts += 1;
|
||||
|
||||
try {
|
||||
await apiGet<ServiceStatus>('/api/service/status');
|
||||
restartOverlayText.value = '服务已恢复,正在刷新页面...';
|
||||
restartPollTimer = window.setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 500);
|
||||
return;
|
||||
} catch {
|
||||
restartOverlayText.value = `正在等待服务恢复... (${attempts}/${maxAttempts})`;
|
||||
}
|
||||
|
||||
if (attempts < maxAttempts) {
|
||||
restartPollTimer = window.setTimeout(checkService, pollInterval);
|
||||
return;
|
||||
}
|
||||
|
||||
restarting.value = false;
|
||||
restartOverlayText.value = '正在重启服务...';
|
||||
message.warning('服务重启超时,请手动刷新页面');
|
||||
};
|
||||
|
||||
restartPollTimer = window.setTimeout(checkService, 3000);
|
||||
}
|
||||
|
||||
function handleMenuChange(key: string | number) {
|
||||
|
||||
@@ -26,8 +26,10 @@
|
||||
--td-bg-color-component-disabled: #eeeeee;
|
||||
--td-border-color: #dcdcdc;
|
||||
--td-border-color-light: #e7e7e7;
|
||||
--td-mask-color: rgba(0, 0, 0, 0.6);
|
||||
--td-shadow-1: 0 1px 10px rgba(0, 0, 0, 0.05), 0 4px 5px rgba(0, 0, 0, 0.08), 0 2px 4px -1px rgba(0, 0, 0, 0.12);
|
||||
--td-shadow-2: 0 3px 14px 2px rgba(0, 0, 0, 0.05), 0 8px 10px 1px rgba(0, 0, 0, 0.06), 0 5px 5px -3px rgba(0, 0, 0, 0.1);
|
||||
--td-shadow-3: 0 6px 30px 5px rgba(0, 0, 0, 0.05), 0 16px 24px 2px rgba(0, 0, 0, 0.04), 0 8px 10px -5px rgba(0, 0, 0, 0.08);
|
||||
--td-radius-small: 3px;
|
||||
--td-radius-default: 6px;
|
||||
--td-radius-large: 12px;
|
||||
@@ -61,6 +63,7 @@
|
||||
--td-border-color-light: #4c4c4c;
|
||||
--td-shadow-1: 0 1px 10px rgba(0, 0, 0, 0.15), 0 4px 5px rgba(0, 0, 0, 0.22), 0 2px 4px -1px rgba(0, 0, 0, 0.32);
|
||||
--td-shadow-2: 0 3px 14px 2px rgba(0, 0, 0, 0.15), 0 8px 10px 1px rgba(0, 0, 0, 0.18), 0 5px 5px -3px rgba(0, 0, 0, 0.28);
|
||||
--td-shadow-3: 0 6px 30px 5px rgba(0, 0, 0, 0.15), 0 16px 24px 2px rgba(0, 0, 0, 0.12), 0 8px 10px -5px rgba(0, 0, 0, 0.24);
|
||||
}
|
||||
|
||||
/* ==================== Base ==================== */
|
||||
@@ -373,6 +376,11 @@ select {
|
||||
background: var(--td-error-color-light);
|
||||
}
|
||||
|
||||
.restart-btn:disabled {
|
||||
cursor: wait;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.theme-toggle svg,
|
||||
.restart-btn svg {
|
||||
display: block;
|
||||
@@ -380,6 +388,58 @@ select {
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.restart-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 5000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--td-mask-color);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.restart-overlay-content {
|
||||
display: flex;
|
||||
min-width: 260px;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
padding: 32px 44px;
|
||||
color: var(--td-text-color-primary);
|
||||
background: var(--td-bg-color-container);
|
||||
border: 1px solid var(--td-border-color-light);
|
||||
border-radius: var(--td-radius-large);
|
||||
box-shadow: var(--td-shadow-3);
|
||||
}
|
||||
|
||||
.restart-overlay-spinner {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border: 4px solid var(--td-border-color-light);
|
||||
border-top-color: var(--td-brand-color);
|
||||
border-radius: 50%;
|
||||
animation: spinner-rotate 1s linear infinite;
|
||||
}
|
||||
|
||||
.restart-overlay-text {
|
||||
color: var(--td-text-color-primary);
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
line-height: 22px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@keyframes spinner-rotate {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.logout-button {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -104,6 +104,7 @@ export interface ServiceStatus {
|
||||
version: string;
|
||||
platform: string;
|
||||
supervisor: boolean;
|
||||
container: boolean;
|
||||
pid: number;
|
||||
python_version: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user