Compare commits
4 Commits
8789019db2
...
156a952b47
| Author | SHA1 | Date | |
|---|---|---|---|
| 156a952b47 | |||
|
|
4173b53b1b | ||
|
|
ab9e87f948 | ||
|
|
5a040366da |
@@ -26,6 +26,15 @@ npm run dev
|
|||||||
|
|
||||||
后端 API 默认通过 Vite 代理转发到 `http://localhost:7861`(见 `vite.config.ts`)。
|
后端 API 默认通过 Vite 代理转发到 `http://localhost:7861`(见 `vite.config.ts`)。
|
||||||
|
|
||||||
|
开发环境默认启用前端 Mock。如需联调真实后端,使用:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
VITE_ENABLE_MOCK=false npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
生产构建默认不包含 Mock;仅在演示构建中可显式设置
|
||||||
|
`VITE_ENABLE_MOCK=true`。
|
||||||
|
|
||||||
## 构建
|
## 构建
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -117,7 +117,8 @@ assert.match(
|
|||||||
assert.match(source, /import \{ getSystemInfo \} from '@\/api\/modules\/system'/, '训练概览必须复用系统 GPU 监控数据源')
|
assert.match(source, /import \{ getSystemInfo \} from '@\/api\/modules\/system'/, '训练概览必须复用系统 GPU 监控数据源')
|
||||||
assert.match(source, /Promise\.all\(\[datasetPromise, loadLog\(currentTask\), loadGpuStatus\(\)\]\)/, 'GPU 状态必须和训练日志一起刷新')
|
assert.match(source, /Promise\.all\(\[datasetPromise, loadLog\(currentTask\), loadGpuStatus\(\)\]\)/, 'GPU 状态必须和训练日志一起刷新')
|
||||||
assert.match(source, /if \(refreshInFlight\) return/, '轮询刷新必须阻止并发重叠')
|
assert.match(source, /if \(refreshInFlight\) return/, '轮询刷新必须阻止并发重叠')
|
||||||
assert.match(source, /onUnmounted\([\s\S]*?clearInterval\(timer\)/, '组件卸载时必须清理轮询定时器')
|
assert.match(source, /usePolling/, '训练日志必须使用统一轮询机制')
|
||||||
|
assert.match(source, /stopPolling\(\)/, '训练结束后必须停止轮询')
|
||||||
|
|
||||||
const overview = findElements(
|
const overview = findElements(
|
||||||
templateAst,
|
templateAst,
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// 根组件,仅承载路由出口
|
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<el-config-provider :locale="zhCn">
|
||||||
<router-view />
|
<router-view />
|
||||||
|
</el-config-provider>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -17,10 +17,6 @@ const service: AxiosInstance = axios.create({
|
|||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 安装 mock adapter(拦截所有 axios 请求返回 mock 数据,方便前端独立开发调试)
|
|
||||||
import { installMockAdapter } from '@/mock/adapter'
|
|
||||||
installMockAdapter(service)
|
|
||||||
|
|
||||||
// 请求拦截器
|
// 请求拦截器
|
||||||
service.interceptors.request.use(
|
service.interceptors.request.use(
|
||||||
(config) => config,
|
(config) => config,
|
||||||
|
|||||||
@@ -1,20 +1,82 @@
|
|||||||
import { useIntervalFn } from '@vueuse/core'
|
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
|
interface PollingOptions {
|
||||||
|
/** 启动后是否立即执行一次,默认立即执行。 */
|
||||||
|
immediate?: boolean
|
||||||
|
/** 页面不可见时是否暂停,默认暂停。 */
|
||||||
|
pauseWhenHidden?: boolean
|
||||||
|
/** 轮询函数抛错时的可选回调。 */
|
||||||
|
onError?: (error: unknown) => void
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 轮询 composable
|
* 轮询 composable
|
||||||
* 封装 useIntervalFn,自动在组件卸载时清理
|
* 串行轮询:上一轮完成后才安排下一轮,避免慢请求重叠和旧响应覆盖。
|
||||||
|
* 页面进入后台时自动暂停,恢复可见后立即刷新;组件卸载时自动清理。
|
||||||
*/
|
*/
|
||||||
export function usePolling(fn: () => void | Promise<void>, interval = 5000, immediate = true) {
|
export function usePolling(
|
||||||
const { pause, resume } = useIntervalFn(fn, interval, { immediate })
|
fn: () => void | Promise<void>,
|
||||||
|
interval: number | (() => number) = 5000,
|
||||||
|
options: PollingOptions = {},
|
||||||
|
) {
|
||||||
|
const { immediate = true, pauseWhenHidden = true, onError } = options
|
||||||
|
const isActive = ref(false)
|
||||||
|
const isRunning = ref(false)
|
||||||
|
let timer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
|
function clearTimer() {
|
||||||
|
if (!timer) return
|
||||||
|
clearTimeout(timer)
|
||||||
|
timer = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function schedule() {
|
||||||
|
clearTimer()
|
||||||
|
if (!isActive.value) return
|
||||||
|
if (pauseWhenHidden && document.hidden) return
|
||||||
|
const delay = typeof interval === 'function' ? interval() : interval
|
||||||
|
timer = setTimeout(() => void run(), Math.max(0, delay))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
clearTimer()
|
||||||
|
if (!isActive.value || isRunning.value) return
|
||||||
|
if (pauseWhenHidden && document.hidden) return
|
||||||
|
|
||||||
|
isRunning.value = true
|
||||||
|
try {
|
||||||
|
await fn()
|
||||||
|
} catch (error) {
|
||||||
|
onError?.(error)
|
||||||
|
} finally {
|
||||||
|
isRunning.value = false
|
||||||
|
schedule()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function stop() {
|
function stop() {
|
||||||
pause()
|
isActive.value = false
|
||||||
|
clearTimer()
|
||||||
}
|
}
|
||||||
|
|
||||||
function start() {
|
function start() {
|
||||||
if (immediate) fn()
|
if (isActive.value) return
|
||||||
resume()
|
isActive.value = true
|
||||||
|
if (immediate) void run()
|
||||||
|
else schedule()
|
||||||
}
|
}
|
||||||
|
|
||||||
return { start, stop, pause, resume }
|
function handleVisibilityChange() {
|
||||||
|
if (!pauseWhenHidden || !isActive.value) return
|
||||||
|
if (document.hidden) clearTimer()
|
||||||
|
else void run()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => document.addEventListener('visibilitychange', handleVisibilityChange))
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
stop()
|
||||||
|
document.removeEventListener('visibilitychange', handleVisibilityChange)
|
||||||
|
})
|
||||||
|
|
||||||
|
return { start, stop, run, isActive, isRunning }
|
||||||
}
|
}
|
||||||
|
|||||||
9
frontend/src/env.d.ts
vendored
9
frontend/src/env.d.ts
vendored
@@ -1,5 +1,14 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
/** 是否启用前端 Mock;开发环境默认开启,生产环境默认关闭。 */
|
||||||
|
readonly VITE_ENABLE_MOCK?: 'true' | 'false'
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv
|
||||||
|
}
|
||||||
|
|
||||||
declare module '*.vue' {
|
declare module '*.vue' {
|
||||||
import type { DefineComponent } from 'vue'
|
import type { DefineComponent } from 'vue'
|
||||||
const component: DefineComponent<{}, {}, any>
|
const component: DefineComponent<{}, {}, any>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, onUnmounted } from 'vue'
|
import { onMounted, onUnmounted } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
|
import '@/assets/font-awesome/css/font-awesome.min.css'
|
||||||
import AppSidebar from '@/components/AppSidebar.vue'
|
import AppSidebar from '@/components/AppSidebar.vue'
|
||||||
import AppHeader from '@/components/AppHeader.vue'
|
import AppHeader from '@/components/AppHeader.vue'
|
||||||
import { useSystemStore } from '@/stores/system'
|
import { useSystemStore } from '@/stores/system'
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import { createPinia } from 'pinia'
|
import { createPinia } from 'pinia'
|
||||||
import ElementPlus from 'element-plus'
|
|
||||||
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
|
||||||
import 'element-plus/dist/index.css'
|
|
||||||
|
|
||||||
// Font Awesome 图标
|
// 这三个能力通过函数或指令使用,无法由模板组件扫描自动补充样式。
|
||||||
import '@/assets/font-awesome/css/font-awesome.min.css'
|
import 'element-plus/es/components/message/style/css'
|
||||||
|
import 'element-plus/es/components/message-box/style/css'
|
||||||
// ECharts(按需引入)+ vue-echarts 组件
|
import 'element-plus/es/components/loading/style/css'
|
||||||
import '@/plugins/echarts'
|
|
||||||
import VChart from 'vue-echarts'
|
|
||||||
|
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
|
import service from '@/api/request'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
import './styles/index.scss'
|
import './styles/index.scss'
|
||||||
|
|
||||||
@@ -19,7 +15,19 @@ const app = createApp(App)
|
|||||||
|
|
||||||
app.use(createPinia())
|
app.use(createPinia())
|
||||||
app.use(router)
|
app.use(router)
|
||||||
app.use(ElementPlus, { locale: zhCn })
|
|
||||||
app.component('VChart', VChart)
|
async function bootstrap() {
|
||||||
|
// 开发环境默认使用 Mock;生产环境只有显式开启时才加载整套 Mock 数据。
|
||||||
|
// 这样真实部署不会被前端适配器截断请求,也不会把 Mock 数据打进首屏包。
|
||||||
|
const shouldEnableMock = import.meta.env.VITE_ENABLE_MOCK === 'true'
|
||||||
|
|| (import.meta.env.DEV && import.meta.env.VITE_ENABLE_MOCK !== 'false')
|
||||||
|
|
||||||
|
if (shouldEnableMock) {
|
||||||
|
const { installMockAdapter } = await import('@/mock/adapter')
|
||||||
|
installMockAdapter(service)
|
||||||
|
}
|
||||||
|
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
|
}
|
||||||
|
|
||||||
|
void bootstrap()
|
||||||
|
|||||||
@@ -1,26 +1,20 @@
|
|||||||
/**
|
/**
|
||||||
* ECharts 按需引入
|
* 服务看板图表按需注册。
|
||||||
* 仅注册训练曲线所需的模块,避免引入全量包以减小体积
|
|
||||||
*/
|
*/
|
||||||
import { use } from 'echarts/core'
|
import { use } from 'echarts/core'
|
||||||
import { CanvasRenderer } from 'echarts/renderers'
|
import { CanvasRenderer } from 'echarts/renderers'
|
||||||
import { BarChart, LineChart, PieChart } from 'echarts/charts'
|
import { BarChart, PieChart } from 'echarts/charts'
|
||||||
import {
|
import {
|
||||||
GridComponent,
|
GridComponent,
|
||||||
TooltipComponent,
|
TooltipComponent,
|
||||||
LegendComponent,
|
LegendComponent,
|
||||||
DataZoomComponent,
|
|
||||||
MarkLineComponent,
|
|
||||||
} from 'echarts/components'
|
} from 'echarts/components'
|
||||||
|
|
||||||
use([
|
use([
|
||||||
CanvasRenderer,
|
CanvasRenderer,
|
||||||
BarChart,
|
BarChart,
|
||||||
LineChart,
|
|
||||||
PieChart,
|
PieChart,
|
||||||
GridComponent,
|
GridComponent,
|
||||||
TooltipComponent,
|
TooltipComponent,
|
||||||
LegendComponent,
|
LegendComponent,
|
||||||
DataZoomComponent,
|
|
||||||
MarkLineComponent,
|
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -10,15 +10,24 @@ import type { ModelItem } from '@/types'
|
|||||||
export const useModelsStore = defineStore('models', () => {
|
export const useModelsStore = defineStore('models', () => {
|
||||||
const list = ref<ModelItem[]>([])
|
const list = ref<ModelItem[]>([])
|
||||||
const loaded = ref(false)
|
const loaded = ref(false)
|
||||||
|
let pendingLoad: Promise<void> | null = null
|
||||||
|
|
||||||
async function load(force = false) {
|
async function load(force = false) {
|
||||||
if (loaded.value && !force) return
|
if (loaded.value && !force) return
|
||||||
|
if (pendingLoad && !force) return pendingLoad
|
||||||
|
|
||||||
|
pendingLoad = (async () => {
|
||||||
try {
|
try {
|
||||||
list.value = (await getModelList()) || []
|
list.value = (await getModelList()) || []
|
||||||
loaded.value = true
|
loaded.value = true
|
||||||
} catch {
|
} catch {
|
||||||
list.value = []
|
list.value = []
|
||||||
|
} finally {
|
||||||
|
pendingLoad = null
|
||||||
}
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
return pendingLoad
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 根据 id 获取模型名 */
|
/** 根据 id 获取模型名 */
|
||||||
|
|||||||
@@ -10,19 +10,29 @@ import type { HealthMetrics } from '@/types'
|
|||||||
export const useSystemStore = defineStore('system', () => {
|
export const useSystemStore = defineStore('system', () => {
|
||||||
const metrics = ref<HealthMetrics>({})
|
const metrics = ref<HealthMetrics>({})
|
||||||
let timer: ReturnType<typeof setInterval> | null = null
|
let timer: ReturnType<typeof setInterval> | null = null
|
||||||
|
let fetching = false
|
||||||
|
|
||||||
async function fetchMetrics() {
|
async function fetchMetrics() {
|
||||||
|
if (fetching || document.hidden) return
|
||||||
|
fetching = true
|
||||||
try {
|
try {
|
||||||
metrics.value = await getHealth()
|
metrics.value = await getHealth()
|
||||||
} catch {
|
} catch {
|
||||||
// 静默失败,顶部栏非关键
|
// 静默失败,顶部栏非关键
|
||||||
|
} finally {
|
||||||
|
fetching = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleVisibilityChange() {
|
||||||
|
if (!document.hidden) void fetchMetrics()
|
||||||
|
}
|
||||||
|
|
||||||
function start() {
|
function start() {
|
||||||
if (timer) return
|
if (timer) return
|
||||||
fetchMetrics()
|
void fetchMetrics()
|
||||||
timer = setInterval(fetchMetrics, 30000)
|
timer = setInterval(fetchMetrics, 30000)
|
||||||
|
document.addEventListener('visibilitychange', handleVisibilityChange)
|
||||||
}
|
}
|
||||||
|
|
||||||
function stop() {
|
function stop() {
|
||||||
@@ -30,6 +40,7 @@ export const useSystemStore = defineStore('system', () => {
|
|||||||
clearInterval(timer)
|
clearInterval(timer)
|
||||||
timer = null
|
timer = null
|
||||||
}
|
}
|
||||||
|
document.removeEventListener('visibilitychange', handleVisibilityChange)
|
||||||
}
|
}
|
||||||
|
|
||||||
return { metrics, fetchMetrics, start, stop }
|
return { metrics, fetchMetrics, start, stop }
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
|
import { ref, reactive, computed, onMounted } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import PageCard from '@/components/PageCard.vue'
|
import PageCard from '@/components/PageCard.vue'
|
||||||
|
import { usePolling } from '@/composables/usePolling'
|
||||||
import { getCompare } from '@/api/modules/compare'
|
import { getCompare } from '@/api/modules/compare'
|
||||||
import type { CompareTask, LoadedModel } from '@/types'
|
import type { CompareTask, LoadedModel } from '@/types'
|
||||||
|
|
||||||
@@ -11,7 +12,6 @@ const router = useRouter()
|
|||||||
const taskId = route.params.id as string
|
const taskId = route.params.id as string
|
||||||
|
|
||||||
const task = ref<CompareTask | null>(null)
|
const task = ref<CompareTask | null>(null)
|
||||||
let pollTimer: ReturnType<typeof setInterval> | null = null
|
|
||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
systemPrompt: '',
|
systemPrompt: '',
|
||||||
@@ -73,13 +73,11 @@ function handleSubmit() {
|
|||||||
window.open(url, '_blank')
|
window.open(url, '_blank')
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
const { start: startPolling } = usePolling(loadTask, 5000, { immediate: false })
|
||||||
loadTask()
|
|
||||||
pollTimer = setInterval(loadTask, 5000)
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
onMounted(async () => {
|
||||||
if (pollTimer) clearInterval(pollTimer)
|
await loadTask()
|
||||||
|
startPolling()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, computed, onMounted } from 'vue'
|
import { ref, computed, onBeforeUnmount, onMounted } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import MarkdownView from '@/components/MarkdownView.vue'
|
import MarkdownView from '@/components/MarkdownView.vue'
|
||||||
import {
|
import { getCompare, chatWithPort } from '@/api/modules/compare'
|
||||||
getCompare,
|
import type { LoadedModel } from '@/types'
|
||||||
chatWithPort,
|
|
||||||
batchChat,
|
|
||||||
} from '@/api/modules/compare'
|
|
||||||
import { getModelByName } from '@/api/modules/model'
|
|
||||||
import type { CompareTask, LoadedModel } from '@/types'
|
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const taskId = route.query.taskId as string
|
const taskId = route.query.taskId as string
|
||||||
@@ -25,6 +20,7 @@ interface ModelResult {
|
|||||||
name: string
|
name: string
|
||||||
content: string
|
content: string
|
||||||
displayContent: string
|
displayContent: string
|
||||||
|
isTyping: boolean
|
||||||
status: 'loading' | 'done' | 'error'
|
status: 'loading' | 'done' | 'error'
|
||||||
stats?: { charsPerSec?: number; totalTime?: number }
|
stats?: { charsPerSec?: number; totalTime?: number }
|
||||||
}
|
}
|
||||||
@@ -33,6 +29,7 @@ const results = ref<ModelResult[]>([])
|
|||||||
const started = ref(false)
|
const started = ref(false)
|
||||||
|
|
||||||
const loadedModels = ref<LoadedModel[]>([])
|
const loadedModels = ref<LoadedModel[]>([])
|
||||||
|
const typewriterTimers = new Set<ReturnType<typeof setInterval>>()
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
if (started.value) return
|
if (started.value) return
|
||||||
@@ -50,6 +47,7 @@ async function init() {
|
|||||||
name: m.model_name || '模型',
|
name: m.model_name || '模型',
|
||||||
content: '',
|
content: '',
|
||||||
displayContent: '',
|
displayContent: '',
|
||||||
|
isTyping: false,
|
||||||
status: 'loading',
|
status: 'loading',
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -65,7 +63,7 @@ async function inferOne(model: LoadedModel, idx: number) {
|
|||||||
const startTime = Date.now()
|
const startTime = Date.now()
|
||||||
try {
|
try {
|
||||||
// 尝试通过端口代理调用
|
// 尝试通过端口代理调用
|
||||||
const res: any = await Promise.race([
|
const res: any = await withTimeout(
|
||||||
chatWithPort({
|
chatWithPort({
|
||||||
port: model.port,
|
port: model.port,
|
||||||
model_name: model.model_name,
|
model_name: model.model_name,
|
||||||
@@ -78,8 +76,8 @@ async function inferOne(model: LoadedModel, idx: number) {
|
|||||||
top_k: topK,
|
top_k: topK,
|
||||||
max_tokens: maxTokens,
|
max_tokens: maxTokens,
|
||||||
}),
|
}),
|
||||||
new Promise((_, reject) => setTimeout(() => reject(new Error('推理超时')), 300000)),
|
300000,
|
||||||
])
|
)
|
||||||
|
|
||||||
const content = res?.response || res?.content || res?.data || JSON.stringify(res)
|
const content = res?.response || res?.content || res?.data || JSON.stringify(res)
|
||||||
const totalTime = (Date.now() - startTime) / 1000
|
const totalTime = (Date.now() - startTime) / 1000
|
||||||
@@ -87,7 +85,7 @@ async function inferOne(model: LoadedModel, idx: number) {
|
|||||||
results.value[idx].status = 'done'
|
results.value[idx].status = 'done'
|
||||||
results.value[idx].stats = {
|
results.value[idx].stats = {
|
||||||
totalTime,
|
totalTime,
|
||||||
charsPerSec: totalTime > 0 ? (content.length / totalTime).toFixed(1) as unknown as number : 0,
|
charsPerSec: totalTime > 0 ? Number((content.length / totalTime).toFixed(1)) : 0,
|
||||||
}
|
}
|
||||||
// 模拟打字机效果
|
// 模拟打字机效果
|
||||||
typewriterDisplay(idx, content)
|
typewriterDisplay(idx, content)
|
||||||
@@ -97,22 +95,46 @@ async function inferOne(model: LoadedModel, idx: number) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
|
||||||
|
let timeoutId: ReturnType<typeof setTimeout> | null = null
|
||||||
|
try {
|
||||||
|
return await Promise.race([
|
||||||
|
promise,
|
||||||
|
new Promise<T>((_, reject) => {
|
||||||
|
timeoutId = setTimeout(() => reject(new Error('推理超时')), timeoutMs)
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
} finally {
|
||||||
|
if (timeoutId) clearTimeout(timeoutId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** 打字机效果逐字展示 */
|
/** 打字机效果逐字展示 */
|
||||||
function typewriterDisplay(idx: number, content: string) {
|
function typewriterDisplay(idx: number, content: string) {
|
||||||
let pos = 0
|
let pos = 0
|
||||||
|
results.value[idx].isTyping = true
|
||||||
|
// 将更新次数控制在约 30 次,避免长回答逐字触发 Markdown 全文解析。
|
||||||
|
const step = Math.max(2, Math.ceil(content.length / 30))
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
pos += 2
|
pos += step
|
||||||
results.value[idx].displayContent = content.slice(0, pos)
|
results.value[idx].displayContent = content.slice(0, pos)
|
||||||
if (pos >= content.length) {
|
if (pos >= content.length) {
|
||||||
clearInterval(interval)
|
clearInterval(interval)
|
||||||
|
typewriterTimers.delete(interval)
|
||||||
results.value[idx].displayContent = content
|
results.value[idx].displayContent = content
|
||||||
|
results.value[idx].isTyping = false
|
||||||
}
|
}
|
||||||
}, 20)
|
}, 50)
|
||||||
|
typewriterTimers.add(interval)
|
||||||
}
|
}
|
||||||
|
|
||||||
const allDone = computed(() => results.value.length > 0 && results.value.every((r) => r.status === 'done' || r.status === 'error'))
|
const allDone = computed(() => results.value.length > 0 && results.value.every((r) => r.status === 'done' || r.status === 'error'))
|
||||||
|
|
||||||
onMounted(init)
|
onMounted(init)
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
typewriterTimers.forEach(clearInterval)
|
||||||
|
typewriterTimers.clear()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -144,6 +166,7 @@ onMounted(init)
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div v-if="r.status === 'error'" class="error-text">{{ r.content }}</div>
|
<div v-if="r.status === 'error'" class="error-text">{{ r.content }}</div>
|
||||||
|
<div v-else-if="r.isTyping" class="streaming-text">{{ r.displayContent }}</div>
|
||||||
<MarkdownView v-else-if="r.displayContent" :content="r.displayContent" />
|
<MarkdownView v-else-if="r.displayContent" :content="r.displayContent" />
|
||||||
<div v-else class="loading-text">
|
<div v-else class="loading-text">
|
||||||
<i class="fa fa-spinner fa-spin" /> 正在生成回答...
|
<i class="fa fa-spinner fa-spin" /> 正在生成回答...
|
||||||
@@ -212,6 +235,13 @@ onMounted(init)
|
|||||||
color: #f56c6c;
|
color: #f56c6c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.streaming-text {
|
||||||
|
min-height: 80px;
|
||||||
|
line-height: 1.7;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
.result-stats {
|
.result-stats {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
import VChart from 'vue-echarts'
|
||||||
|
import '@/plugins/echarts'
|
||||||
import type { EChartsOption } from 'echarts'
|
import type { EChartsOption } from 'echarts'
|
||||||
|
|
||||||
type ServiceState = 'normal' | 'busy' | 'error'
|
type ServiceState = 'normal' | 'busy' | 'error'
|
||||||
@@ -253,7 +255,7 @@ const loginDurationStats: LoginDurationStat[] = [
|
|||||||
|
|
||||||
const loginDurationChartOption = computed<EChartsOption>(() => ({
|
const loginDurationChartOption = computed<EChartsOption>(() => ({
|
||||||
animationDuration: 500,
|
animationDuration: 500,
|
||||||
grid: { top: 8, right: 60, bottom: 6, left: 8, containLabel: true },
|
grid: { top: 8, right: 12, bottom: 6, left: 8, containLabel: true },
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: 'axis',
|
trigger: 'axis',
|
||||||
axisPointer: { type: 'shadow' },
|
axisPointer: { type: 'shadow' },
|
||||||
@@ -284,7 +286,7 @@ const loginDurationChartOption = computed<EChartsOption>(() => ({
|
|||||||
barMaxWidth: 18,
|
barMaxWidth: 18,
|
||||||
barCategoryGap: '34%',
|
barCategoryGap: '34%',
|
||||||
itemStyle: { color: '#4f46e5', borderRadius: [0, 4, 4, 0] },
|
itemStyle: { color: '#4f46e5', borderRadius: [0, 4, 4, 0] },
|
||||||
label: { show: true, position: 'right', distance: 6, color: '#64748b', fontSize: 11, formatter: '{c} 小时' },
|
label: { show: true, position: 'insideRight', distance: 6, color: '#ffffff', fontSize: 11, formatter: '{c} 小时' },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, watch } from 'vue'
|
import { computed, defineAsyncComponent, watch } from 'vue'
|
||||||
import { MdEditor } from 'md-editor-v3'
|
|
||||||
import 'md-editor-v3/lib/style.css'
|
|
||||||
import { EVAL_METHODS, EVAL_METHOD_PROMPTS } from '@/constants/dimension'
|
import { EVAL_METHODS, EVAL_METHOD_PROMPTS } from '@/constants/dimension'
|
||||||
import { DIMENSION_TYPE_MAP } from '@/constants'
|
import { DIMENSION_TYPE_MAP } from '@/constants'
|
||||||
import type { DimensionType, ModelItem } from '@/types'
|
import type { DimensionType, ModelItem } from '@/types'
|
||||||
|
|
||||||
|
// 编辑器体积较大,仅在用户进入包含 Prompt 的指标配置时加载。
|
||||||
|
const MdEditor = defineAsyncComponent(async () => {
|
||||||
|
await import('md-editor-v3/lib/style.css')
|
||||||
|
const module = await import('md-editor-v3')
|
||||||
|
return module.MdEditor
|
||||||
|
})
|
||||||
|
|
||||||
export interface DimensionFormDraft {
|
export interface DimensionFormDraft {
|
||||||
type: DimensionType | ''
|
type: DimensionType | ''
|
||||||
description: string
|
description: string
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import DataTablePage from '@/components/DataTablePage.vue'
|
import DataTablePage from '@/components/DataTablePage.vue'
|
||||||
import ModelStatusTag from '@/components/ModelStatusTag.vue'
|
import ModelStatusTag from '@/components/ModelStatusTag.vue'
|
||||||
|
import { usePolling } from '@/composables/usePolling'
|
||||||
import { useModelsStore } from '@/stores/models'
|
import { useModelsStore } from '@/stores/models'
|
||||||
import {
|
import {
|
||||||
getFineTuneList,
|
getFineTuneList,
|
||||||
@@ -44,14 +45,10 @@ const filteredList = computed(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
let progressTimer: ReturnType<typeof setInterval> | null = null
|
|
||||||
|
|
||||||
async function loadData() {
|
async function loadData() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
dataList.value = (await getFineTuneList()) || []
|
dataList.value = (await getFineTuneList()) || []
|
||||||
// 列表加载完成后,立即获取一次运行中任务的进度
|
|
||||||
refreshProgress()
|
|
||||||
} catch {
|
} catch {
|
||||||
// 拦截器已提示
|
// 拦截器已提示
|
||||||
} finally {
|
} finally {
|
||||||
@@ -101,14 +98,13 @@ function formatDateTime(value?: string) {
|
|||||||
return new Date(value).toLocaleString('zh-CN', { hour12: false })
|
return new Date(value).toLocaleString('zh-CN', { hour12: false })
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
const { start: startProgressPolling } = usePolling(refreshProgress, 5000, { immediate: false })
|
||||||
modelsStore.load()
|
|
||||||
loadData()
|
|
||||||
progressTimer = setInterval(refreshProgress, 5000)
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
onMounted(async () => {
|
||||||
if (progressTimer) clearInterval(progressTimer)
|
void modelsStore.load()
|
||||||
|
await loadData()
|
||||||
|
await refreshProgress()
|
||||||
|
startProgressPolling()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, nextTick, onMounted } from 'vue'
|
import { ref, reactive, nextTick, onMounted, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import MarkdownView from '@/components/MarkdownView.vue'
|
import MarkdownView from '@/components/MarkdownView.vue'
|
||||||
@@ -34,6 +34,7 @@ const temperature = ref(0.7)
|
|||||||
const top_p = ref(0.95)
|
const top_p = ref(0.95)
|
||||||
const maxTokens = ref(2048)
|
const maxTokens = ref(2048)
|
||||||
const contentRef = ref<HTMLElement>()
|
const contentRef = ref<HTMLElement>()
|
||||||
|
let activeAssistant: ChatMessage | null = null
|
||||||
/** 设置面板抽屉 */
|
/** 设置面板抽屉 */
|
||||||
const showSettings = ref(false)
|
const showSettings = ref(false)
|
||||||
|
|
||||||
@@ -98,8 +99,8 @@ async function handleSend() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听流式 message 变化,同步到 assistantMsg
|
// 流式状态变化时只同步当前回复,避免固定定时器空转。
|
||||||
const watchStop = watchMessage(assistantMsg)
|
activeAssistant = assistantMsg
|
||||||
|
|
||||||
await send({
|
await send({
|
||||||
port: target.port,
|
port: target.port,
|
||||||
@@ -118,7 +119,7 @@ async function handleSend() {
|
|||||||
assistantMsg.isThinking = false
|
assistantMsg.isThinking = false
|
||||||
assistantMsg.isStreaming = false
|
assistantMsg.isStreaming = false
|
||||||
assistantMsg.done = true
|
assistantMsg.done = true
|
||||||
watchStop()
|
activeAssistant = null
|
||||||
reset()
|
reset()
|
||||||
await nextTick()
|
await nextTick()
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
@@ -149,17 +150,24 @@ async function mockReply(assistantMsg: ChatMessage, question: string) {
|
|||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 轮询同步流式状态到展示消息 */
|
watch(
|
||||||
function watchMessage(assistantMsg: ChatMessage) {
|
() => [
|
||||||
const timer = setInterval(() => {
|
message.value.displayContent,
|
||||||
assistantMsg.content = message.value.displayContent
|
message.value.thinkContent,
|
||||||
assistantMsg.think = message.value.thinkContent
|
message.value.isThinking,
|
||||||
assistantMsg.isThinking = message.value.isThinking
|
message.value.done,
|
||||||
if (message.value.done) clearInterval(timer)
|
] as const,
|
||||||
|
async ([content, think, isThinking, done]) => {
|
||||||
|
if (!activeAssistant) return
|
||||||
|
activeAssistant.content = content
|
||||||
|
activeAssistant.think = think
|
||||||
|
activeAssistant.isThinking = isThinking
|
||||||
|
activeAssistant.isStreaming = !done
|
||||||
|
await nextTick()
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
}, 80)
|
},
|
||||||
return () => clearInterval(timer)
|
{ flush: 'post' },
|
||||||
}
|
)
|
||||||
|
|
||||||
function scrollToBottom() {
|
function scrollToBottom() {
|
||||||
if (contentRef.value) {
|
if (contentRef.value) {
|
||||||
@@ -168,6 +176,7 @@ function scrollToBottom() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleNewChat() {
|
function handleNewChat() {
|
||||||
|
activeAssistant = null
|
||||||
messages.value = []
|
messages.value = []
|
||||||
reset()
|
reset()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { ref, onMounted, onUnmounted } from 'vue'
|
|||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import DataTablePage from '@/components/DataTablePage.vue'
|
import DataTablePage from '@/components/DataTablePage.vue'
|
||||||
|
import { usePolling } from '@/composables/usePolling'
|
||||||
import {
|
import {
|
||||||
getCompareList,
|
getCompareList,
|
||||||
deleteCompare,
|
deleteCompare,
|
||||||
@@ -17,7 +18,7 @@ const router = useRouter()
|
|||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const dataList = ref<CompareTask[]>([])
|
const dataList = ref<CompareTask[]>([])
|
||||||
let refreshTimer: ReturnType<typeof setInterval> | null = null
|
let delayedRefreshTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
async function loadData(silent = false) {
|
async function loadData(silent = false) {
|
||||||
if (!silent) {
|
if (!silent) {
|
||||||
@@ -80,7 +81,8 @@ function parseModelNames(row: any): string[] {
|
|||||||
async function handleLoad(row: any) {
|
async function handleLoad(row: any) {
|
||||||
await loadCompare(row.id)
|
await loadCompare(row.id)
|
||||||
ElMessage.info('正在加载模型,请稍候...')
|
ElMessage.info('正在加载模型,请稍候...')
|
||||||
setTimeout(loadData, 1000)
|
if (delayedRefreshTimer) clearTimeout(delayedRefreshTimer)
|
||||||
|
delayedRefreshTimer = setTimeout(loadData, 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 卸载推理任务 */
|
/** 卸载推理任务 */
|
||||||
@@ -112,13 +114,15 @@ function startChat(row: any) {
|
|||||||
router.push(`/model-inference/chat/${row.id}`)
|
router.push(`/model-inference/chat/${row.id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
const { start: startPolling } = usePolling(() => loadData(true), 3000, { immediate: false })
|
||||||
loadData()
|
|
||||||
refreshTimer = setInterval(() => loadData(true), 3000)
|
onMounted(async () => {
|
||||||
|
await loadData()
|
||||||
|
startPolling()
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (refreshTimer) clearInterval(refreshTimer)
|
if (delayedRefreshTimer) clearTimeout(delayedRefreshTimer)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ async function handleLogin() {
|
|||||||
position: relative;
|
position: relative;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: #121127;
|
background-color: #121127;
|
||||||
background-image: url('@/assets/login-hero-flow.png');
|
background-image: url('@/assets/login-hero-flow.jpg');
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
|
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
|
||||||
|
import VChart from 'vue-echarts'
|
||||||
import PageCard from '@/components/PageCard.vue'
|
import PageCard from '@/components/PageCard.vue'
|
||||||
|
import '@/plugins/echarts-hardware'
|
||||||
import { getSystemInfo } from '@/api/modules/system'
|
import { getSystemInfo } from '@/api/modules/system'
|
||||||
import type { GpuInfo, SystemInfo } from '@/types'
|
import type { GpuInfo, SystemInfo } from '@/types'
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
import PageCard from '@/components/PageCard.vue'
|
import PageCard from '@/components/PageCard.vue'
|
||||||
import { useCountdown } from '@/composables/useCountdown'
|
import { useCountdown } from '@/composables/useCountdown'
|
||||||
|
import { usePolling } from '@/composables/usePolling'
|
||||||
import {
|
import {
|
||||||
getLogFiles,
|
getLogFiles,
|
||||||
getLogContent,
|
getLogContent,
|
||||||
@@ -30,29 +31,26 @@ const fullContent = ref('')
|
|||||||
// 自动刷新
|
// 自动刷新
|
||||||
const refreshInterval = ref(10)
|
const refreshInterval = ref(10)
|
||||||
const { remaining, start: startCountdown, stop: stopCountdown } = useCountdown(10)
|
const { remaining, start: startCountdown, stop: stopCountdown } = useCountdown(10)
|
||||||
let refreshTimer: ReturnType<typeof setInterval> | null = null
|
|
||||||
|
|
||||||
const filteredContent = computed(() => {
|
const filteredLog = computed(() => {
|
||||||
if (!keyword.value.trim()) return fullContent.value
|
if (!keyword.value.trim()) return { content: fullContent.value, count: 0 }
|
||||||
const kw = keyword.value.toLowerCase().trim()
|
const kw = keyword.value.toLowerCase().trim()
|
||||||
return fullContent.value
|
const lines = fullContent.value
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.filter((line) => line.toLowerCase().includes(kw))
|
.filter((line) => line.toLowerCase().includes(kw))
|
||||||
.join('\n')
|
return { content: lines.join('\n'), count: lines.length }
|
||||||
})
|
})
|
||||||
|
|
||||||
const matchCount = computed(() => {
|
const filteredContent = computed(() => filteredLog.value.content)
|
||||||
if (!keyword.value.trim()) return 0
|
const matchCount = computed(() => filteredLog.value.count)
|
||||||
const kw = keyword.value.toLowerCase().trim()
|
|
||||||
return fullContent.value.split('\n').filter((line) => line.toLowerCase().includes(kw)).length
|
|
||||||
})
|
|
||||||
|
|
||||||
async function loadSysFiles() {
|
async function loadSysFiles() {
|
||||||
try {
|
try {
|
||||||
sysFiles.value = (await getLogFiles(sysDate.value)) || []
|
sysFiles.value = (await getLogFiles(sysDate.value)) || []
|
||||||
if (sysFiles.value.length > 0) {
|
if (sysFiles.value.length > 0) {
|
||||||
sysSelected.value = sysFiles.value[0].file
|
const firstFile = sysFiles.value[0].file
|
||||||
loadSysContent()
|
if (sysSelected.value === firstFile) void loadSysContent()
|
||||||
|
else sysSelected.value = firstFile
|
||||||
} else {
|
} else {
|
||||||
sysContent.value = '该日期暂无日志文件'
|
sysContent.value = '该日期暂无日志文件'
|
||||||
}
|
}
|
||||||
@@ -76,8 +74,9 @@ async function loadTrainFiles() {
|
|||||||
try {
|
try {
|
||||||
trainFiles.value = (await getTrainingLogFiles()) || []
|
trainFiles.value = (await getTrainingLogFiles()) || []
|
||||||
if (trainFiles.value.length > 0) {
|
if (trainFiles.value.length > 0) {
|
||||||
trainSelected.value = trainFiles.value[0].file
|
const firstFile = trainFiles.value[0].file
|
||||||
loadTrainContent()
|
if (trainSelected.value === firstFile) void loadTrainContent()
|
||||||
|
else trainSelected.value = firstFile
|
||||||
} else {
|
} else {
|
||||||
trainContent.value = '暂无训练日志'
|
trainContent.value = '暂无训练日志'
|
||||||
}
|
}
|
||||||
@@ -97,29 +96,28 @@ async function loadTrainContent() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function refresh() {
|
async function refresh() {
|
||||||
if (activeTab.value === 'system') {
|
if (activeTab.value === 'system') {
|
||||||
loadSysContent()
|
await loadSysContent()
|
||||||
} else {
|
} else {
|
||||||
loadTrainContent()
|
await loadTrainContent()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { start: startPolling, stop: stopPolling } = usePolling(
|
||||||
|
refresh,
|
||||||
|
() => refreshInterval.value * 1000,
|
||||||
|
{ immediate: false },
|
||||||
|
)
|
||||||
|
|
||||||
function startAutoRefresh() {
|
function startAutoRefresh() {
|
||||||
stopAutoRefresh()
|
stopPolling()
|
||||||
if (refreshInterval.value === 0) {
|
if (refreshInterval.value === 0) {
|
||||||
stopCountdown()
|
stopCountdown()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
startCountdown()
|
startCountdown()
|
||||||
refreshTimer = setInterval(refresh, refreshInterval.value * 1000)
|
startPolling()
|
||||||
}
|
|
||||||
|
|
||||||
function stopAutoRefresh() {
|
|
||||||
if (refreshTimer) {
|
|
||||||
clearInterval(refreshTimer)
|
|
||||||
refreshTimer = null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(refreshInterval, startAutoRefresh)
|
watch(refreshInterval, startAutoRefresh)
|
||||||
@@ -135,8 +133,6 @@ onMounted(() => {
|
|||||||
loadSysFiles()
|
loadSysFiles()
|
||||||
startAutoRefresh()
|
startAutoRefresh()
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(stopAutoRefresh)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
|
import { ref, reactive, computed, onMounted } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
|
import VChart from 'vue-echarts'
|
||||||
import PageCard from '@/components/PageCard.vue'
|
import PageCard from '@/components/PageCard.vue'
|
||||||
import ModelStatusTag from '@/components/ModelStatusTag.vue'
|
import ModelStatusTag from '@/components/ModelStatusTag.vue'
|
||||||
import TrainingTaskOverview from './training-log/TrainingTaskOverview.vue'
|
import TrainingTaskOverview from './training-log/TrainingTaskOverview.vue'
|
||||||
|
import { usePolling } from '@/composables/usePolling'
|
||||||
|
import '@/plugins/echarts-training-log'
|
||||||
import { useModelsStore } from '@/stores/models'
|
import { useModelsStore } from '@/stores/models'
|
||||||
import { getFineTune } from '@/api/modules/fineTune'
|
import { getFineTune } from '@/api/modules/fineTune'
|
||||||
import { getTrainingLogFiles, getTrainingLogContent } from '@/api/modules/log'
|
import { getTrainingLogFiles, getTrainingLogContent } from '@/api/modules/log'
|
||||||
@@ -55,7 +58,6 @@ const paramsExpanded = ref(false)
|
|||||||
const GPU_PREVIEW_LIMIT = 4
|
const GPU_PREVIEW_LIMIT = 4
|
||||||
const gpuExpanded = ref(false)
|
const gpuExpanded = ref(false)
|
||||||
|
|
||||||
let timer: ReturnType<typeof setInterval> | null = null
|
|
||||||
let refreshInFlight = false
|
let refreshInFlight = false
|
||||||
|
|
||||||
/** 三个曲线的 ECharts 配置(响应式,数据变化自动重绘) */
|
/** 三个曲线的 ECharts 配置(响应式,数据变化自动重绘) */
|
||||||
@@ -257,14 +259,16 @@ async function refreshAll() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
const isTerminalTask = () => ['completed', 'failed', 'stopped', 'cancelled'].includes(task.value?.status || '')
|
||||||
modelsStore.load()
|
const { start: startPolling, stop: stopPolling } = usePolling(async () => {
|
||||||
refreshAll()
|
await refreshAll()
|
||||||
timer = setInterval(refreshAll, 5000)
|
if (isTerminalTask()) stopPolling()
|
||||||
})
|
}, 5000, { immediate: false })
|
||||||
|
|
||||||
onUnmounted(() => {
|
onMounted(async () => {
|
||||||
if (timer) clearInterval(timer)
|
void modelsStore.load()
|
||||||
|
await refreshAll()
|
||||||
|
if (!isTerminalTask()) startPolling()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ export default defineConfig({
|
|||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
AutoImport({
|
AutoImport({
|
||||||
resolvers: [ElementPlusResolver({ importStyle: false })],
|
resolvers: [ElementPlusResolver({ importStyle: 'css' })],
|
||||||
}),
|
}),
|
||||||
Components({
|
Components({
|
||||||
resolvers: [ElementPlusResolver({ importStyle: false })],
|
resolvers: [ElementPlusResolver({ importStyle: 'css' })],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
|
|||||||
Reference in New Issue
Block a user