V13-FE-006: consolidate approved UI and contract hardening
This commit is contained in:
+30
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { KbxNotificationCenter, KbxOperationCenter, KbxRuntimeBanner } from '@kbx/ui'
|
||||
import { ref } from 'vue'
|
||||
import { useKbxRuntime } from './useKbxRuntime'
|
||||
|
||||
const { notice, operations, notifications, unreadCount, refresh, markRead } = useKbxRuntime()
|
||||
const panel = ref<'notifications' | 'operations' | null>(null)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="kbx-runtime-shell">
|
||||
<KbxRuntimeBanner :notice="notice" @retry="refresh" />
|
||||
<header class="tools">
|
||||
<button type="button" @click="panel = panel === 'operations' ? null : 'operations'">작업 {{ operations.filter(x => x.status === 'running' || x.status === 'queued').length }}</button>
|
||||
<button type="button" @click="panel = panel === 'notifications' ? null : 'notifications'">알림 {{ unreadCount }}</button>
|
||||
</header>
|
||||
<aside v-if="panel" class="panel">
|
||||
<KbxOperationCenter v-if="panel === 'operations'" :operations="operations" />
|
||||
<KbxNotificationCenter v-else :notifications="notifications" @read="markRead" />
|
||||
</aside>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.kbx-runtime-shell { position:relative; min-height:100%; }
|
||||
.tools { display:flex; justify-content:flex-end; gap:4px; min-height:32px; align-items:center; }
|
||||
.tools button { min-height:30px; border:1px solid var(--kbx-color-border); border-radius:var(--kbx-radius-sm); background:var(--kbx-color-surface); }
|
||||
.panel { position:absolute; z-index:50; right:8px; top:76px; width:min(420px, calc(100vw - 24px)); max-height:70vh; overflow:auto; padding:8px; border:1px solid var(--kbx-color-border); border-radius:var(--kbx-radius-md); background:var(--kbx-color-surface); box-shadow:0 10px 30px rgba(16,24,40,.16); }
|
||||
</style>
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import type { KbxOperationRun, KbxRuntimeNotice, KbxUserNotification } from '@kbx/contracts'
|
||||
import { kbxApi } from '../http/generated/kbxApiClient'
|
||||
|
||||
export const runtimeApi = {
|
||||
getNotice() {
|
||||
return kbxApi.request<KbxRuntimeNotice | null>('common.runtime.notice')
|
||||
},
|
||||
getOperations() {
|
||||
return kbxApi.request<KbxOperationRun[]>('common.runtime.operations', { query: { limit: 20 } })
|
||||
},
|
||||
getNotifications() {
|
||||
return kbxApi.request<KbxUserNotification[]>('common.runtime.notifications', { query: { limit: 30 } })
|
||||
},
|
||||
markNotificationRead(id: string) {
|
||||
return kbxApi.request<void>('common.runtime.notificationRead', { path: { Id: id } })
|
||||
},
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import type { KbxOperationRun, KbxRuntimeNotice, KbxUserNotification } from '@kbx/contracts'
|
||||
import { runtimeApi } from './runtimeApi'
|
||||
|
||||
export function useKbxRuntime() {
|
||||
const notice = ref<KbxRuntimeNotice | null>(null)
|
||||
const operations = ref<KbxOperationRun[]>([])
|
||||
const notifications = ref<KbxUserNotification[]>([])
|
||||
let timer: ReturnType<typeof setInterval> | undefined
|
||||
|
||||
async function refresh() {
|
||||
const [nextNotice, nextOperations, nextNotifications] = await Promise.all([
|
||||
runtimeApi.getNotice(),
|
||||
runtimeApi.getOperations(),
|
||||
runtimeApi.getNotifications(),
|
||||
])
|
||||
notice.value = nextNotice
|
||||
operations.value = nextOperations
|
||||
notifications.value = nextNotifications
|
||||
}
|
||||
|
||||
async function markRead(id: string) {
|
||||
await runtimeApi.markNotificationRead(id)
|
||||
const item = notifications.value.find(x => x.id === id)
|
||||
if (item && !item.readAt) item.readAt = new Date().toISOString()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void refresh()
|
||||
timer = setInterval(() => void refresh(), 30_000)
|
||||
})
|
||||
onUnmounted(() => { if (timer) clearInterval(timer) })
|
||||
|
||||
return {
|
||||
notice,
|
||||
operations,
|
||||
notifications,
|
||||
unreadCount: computed(() => notifications.value.filter(x => !x.readAt).length),
|
||||
refresh,
|
||||
markRead,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user