mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
feat(web): schedule page
This commit is contained in:
@@ -354,6 +354,7 @@
|
||||
"mcp": "MCP",
|
||||
"subagents": "Subagents",
|
||||
"heartbeat": "Heartbeat",
|
||||
"schedule": "Schedule",
|
||||
"history": "History",
|
||||
"skills": "Skills",
|
||||
"email": "Email",
|
||||
@@ -594,6 +595,20 @@
|
||||
"filterAll": "All",
|
||||
"noResult": "—"
|
||||
},
|
||||
"schedule": {
|
||||
"title": "Scheduled Tasks",
|
||||
"loadFailed": "Failed to load schedules",
|
||||
"empty": "No scheduled tasks",
|
||||
"name": "Name",
|
||||
"pattern": "Pattern",
|
||||
"enabled": "Enabled",
|
||||
"calls": "Calls",
|
||||
"createdAt": "Created",
|
||||
"updatedAt": "Updated",
|
||||
"statusEnabled": "Enabled",
|
||||
"statusDisabled": "Disabled",
|
||||
"unlimited": "∞"
|
||||
},
|
||||
"history": {
|
||||
"title": "History",
|
||||
"loadFailed": "Failed to load history",
|
||||
|
||||
@@ -350,6 +350,7 @@
|
||||
"mcp": "MCP",
|
||||
"subagents": "子智能体",
|
||||
"heartbeat": "心跳",
|
||||
"schedule": "定时任务",
|
||||
"history": "对话历史",
|
||||
"skills": "技能",
|
||||
"email": "邮件",
|
||||
@@ -590,6 +591,20 @@
|
||||
"filterAll": "全部",
|
||||
"noResult": "—"
|
||||
},
|
||||
"schedule": {
|
||||
"title": "定时任务",
|
||||
"loadFailed": "加载定时任务失败",
|
||||
"empty": "暂无定时任务",
|
||||
"name": "名称",
|
||||
"pattern": "表达式",
|
||||
"enabled": "启用",
|
||||
"calls": "调用次数",
|
||||
"createdAt": "创建时间",
|
||||
"updatedAt": "更新时间",
|
||||
"statusEnabled": "已启用",
|
||||
"statusDisabled": "已禁用",
|
||||
"unlimited": "无限制"
|
||||
},
|
||||
"history": {
|
||||
"title": "对话历史",
|
||||
"loadFailed": "加载历史消息失败",
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<h3 class="text-lg font-medium">
|
||||
{{ $t('bots.schedule.title') }}
|
||||
</h3>
|
||||
<Badge
|
||||
v-if="schedules.length"
|
||||
variant="secondary"
|
||||
class="text-xs"
|
||||
>
|
||||
{{ schedules.length }}
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:disabled="isLoading"
|
||||
@click="handleRefresh"
|
||||
>
|
||||
<Spinner
|
||||
v-if="isLoading"
|
||||
class="mr-2 size-4"
|
||||
/>
|
||||
{{ $t('common.refresh') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading -->
|
||||
<div
|
||||
v-if="isLoading && schedules.length === 0"
|
||||
class="flex items-center justify-center py-8 text-sm text-muted-foreground"
|
||||
>
|
||||
<Spinner class="mr-2" />
|
||||
{{ $t('common.loading') }}
|
||||
</div>
|
||||
|
||||
<!-- Empty -->
|
||||
<div
|
||||
v-else-if="!isLoading && schedules.length === 0"
|
||||
class="flex flex-col items-center justify-center py-12 text-center"
|
||||
>
|
||||
<div class="rounded-full bg-muted p-3 mb-4">
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', 'calendar-alt']"
|
||||
class="size-6 text-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{{ $t('bots.schedule.empty') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<template v-else>
|
||||
<div class="rounded-md border overflow-hidden">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="border-b bg-muted/50">
|
||||
<th class="px-4 py-2 text-left font-medium">
|
||||
{{ $t('bots.schedule.name') }}
|
||||
</th>
|
||||
<th class="px-4 py-2 text-left font-medium">
|
||||
{{ $t('bots.schedule.pattern') }}
|
||||
</th>
|
||||
<th class="px-4 py-2 text-left font-medium">
|
||||
{{ $t('bots.schedule.enabled') }}
|
||||
</th>
|
||||
<th class="px-4 py-2 text-left font-medium">
|
||||
{{ $t('bots.schedule.calls') }}
|
||||
</th>
|
||||
<th class="px-4 py-2 text-left font-medium">
|
||||
{{ $t('bots.schedule.createdAt') }}
|
||||
</th>
|
||||
<th class="px-4 py-2 text-left font-medium">
|
||||
{{ $t('bots.schedule.updatedAt') }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="item in pagedSchedules"
|
||||
:key="item.id"
|
||||
class="border-b last:border-0 hover:bg-muted/30"
|
||||
>
|
||||
<td class="px-4 py-2 font-medium">
|
||||
<div>{{ item.name }}</div>
|
||||
<div class="text-xs text-muted-foreground line-clamp-1">
|
||||
{{ item.description }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-2">
|
||||
<code class="text-xs bg-muted px-1.5 py-0.5 rounded">
|
||||
{{ item.pattern }}
|
||||
</code>
|
||||
</td>
|
||||
<td class="px-4 py-2">
|
||||
<Badge :variant="item.enabled ? 'secondary' : 'outline'">
|
||||
{{ item.enabled ? $t('bots.schedule.statusEnabled') : $t('bots.schedule.statusDisabled') }}
|
||||
</Badge>
|
||||
</td>
|
||||
<td class="px-4 py-2 text-muted-foreground">
|
||||
{{ item.current_calls ?? 0 }} / {{ item.max_calls || $t('bots.schedule.unlimited') }}
|
||||
</td>
|
||||
<td class="px-4 py-2 text-muted-foreground">
|
||||
{{ formatDateTime(item.created_at) }}
|
||||
</td>
|
||||
<td class="px-4 py-2 text-muted-foreground">
|
||||
{{ formatDateTime(item.updated_at) }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div
|
||||
v-if="totalPages > 1"
|
||||
class="flex items-center justify-between pt-4"
|
||||
>
|
||||
<span class="text-sm text-muted-foreground">
|
||||
{{ paginationSummary }}
|
||||
</span>
|
||||
<Pagination
|
||||
:total="schedules.length"
|
||||
:items-per-page="PAGE_SIZE"
|
||||
:sibling-count="1"
|
||||
:page="currentPage"
|
||||
show-edges
|
||||
@update:page="currentPage = $event"
|
||||
>
|
||||
<PaginationContent v-slot="{ items }">
|
||||
<PaginationFirst />
|
||||
<PaginationPrevious />
|
||||
<template
|
||||
v-for="(item, index) in items"
|
||||
:key="index"
|
||||
>
|
||||
<PaginationEllipsis
|
||||
v-if="item.type === 'ellipsis'"
|
||||
:index="index"
|
||||
/>
|
||||
<PaginationItem
|
||||
v-else
|
||||
:value="item.value"
|
||||
/>
|
||||
</template>
|
||||
<PaginationNext />
|
||||
<PaginationLast />
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { toast } from 'vue-sonner'
|
||||
import {
|
||||
Button, Badge, Spinner,
|
||||
Pagination, PaginationContent, PaginationEllipsis,
|
||||
PaginationFirst, PaginationItem, PaginationLast,
|
||||
PaginationNext, PaginationPrevious,
|
||||
} from '@memoh/ui'
|
||||
import { getBotsByBotIdSchedule } from '@memoh/sdk'
|
||||
import type { ScheduleSchedule } from '@memoh/sdk'
|
||||
import { resolveApiErrorMessage } from '@/utils/api-error'
|
||||
import { formatDateTime } from '@/utils/date-time'
|
||||
|
||||
const props = defineProps<{
|
||||
botId: string
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const isLoading = ref(false)
|
||||
const schedules = ref<ScheduleSchedule[]>([])
|
||||
const currentPage = ref(1)
|
||||
const PAGE_SIZE = 10
|
||||
|
||||
const totalPages = computed(() => Math.ceil(schedules.value.length / PAGE_SIZE))
|
||||
|
||||
const pagedSchedules = computed(() => {
|
||||
const start = (currentPage.value - 1) * PAGE_SIZE
|
||||
return schedules.value.slice(start, start + PAGE_SIZE)
|
||||
})
|
||||
|
||||
const paginationSummary = computed(() => {
|
||||
const total = schedules.value.length
|
||||
if (total === 0) return ''
|
||||
const start = (currentPage.value - 1) * PAGE_SIZE + 1
|
||||
const end = Math.min(currentPage.value * PAGE_SIZE, total)
|
||||
return `${start}-${end} / ${total}`
|
||||
})
|
||||
|
||||
async function fetchSchedules() {
|
||||
if (!props.botId) return
|
||||
isLoading.value = true
|
||||
try {
|
||||
const { data } = await getBotsByBotIdSchedule({
|
||||
path: { bot_id: props.botId },
|
||||
throwOnError: true,
|
||||
})
|
||||
schedules.value = data?.items || []
|
||||
} catch (error) {
|
||||
toast.error(resolveApiErrorMessage(error, t('bots.schedule.loadFailed')))
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRefresh() {
|
||||
currentPage.value = 1
|
||||
await fetchSchedules()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchSchedules()
|
||||
})
|
||||
</script>
|
||||
@@ -268,6 +268,7 @@ import BotHeartbeat from './components/bot-heartbeat.vue'
|
||||
import BotEmail from './components/bot-email.vue'
|
||||
import BotSubagents from './components/bot-subagents.vue'
|
||||
import BotOverview from './components/bot-overview.vue'
|
||||
import BotSchedule from './components/bot-schedule.vue'
|
||||
import BotContainer from './components/bot-container.vue'
|
||||
import BotFiles from './components/bot-files.vue'
|
||||
import { resolveApiErrorMessage } from '@/utils/api-error'
|
||||
@@ -294,6 +295,7 @@ const tabList = [
|
||||
{ value: 'mcp', label: 'bots.tabs.mcp' ,component: BotMcp },
|
||||
{ value: 'subagents', label: 'bots.tabs.subagents',component: BotSubagents },
|
||||
{ value: 'heartbeat', label: 'bots.tabs.heartbeat',component: BotHeartbeat },
|
||||
{ value: 'schedule', label: 'bots.tabs.schedule', component: BotSchedule },
|
||||
{ value: 'history', label: 'bots.tabs.history',component: BotHistory },
|
||||
{ value: 'skills', label: 'bots.tabs.skills',component: BotSkills },
|
||||
{ value: 'settings', label: 'bots.tabs.settings',component: BotSettings }
|
||||
|
||||
Reference in New Issue
Block a user