Files
Jnote-nodeJs/src/views/CronView.vue
dcr_xuxgc d759a9e740 init
2026-06-12 17:49:54 +08:00

591 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup>
import { ref, onMounted, onUnmounted, computed } from 'vue'
import { useCronStore } from '@/stores/cron'
import { useI18n } from '@/composables/useI18n'
const { t, locale } = useI18n()
const cronStore = useCronStore()
const showModal = ref(false)
const editingTask = ref(null)
const form = ref({
name: '',
cron: '',
url: '',
method: 'GET',
headers: '',
body: '',
enabled: true
})
let schedulerTimer = null
onMounted(() => {
cronStore.loadTasks()
startScheduler()
})
onUnmounted(() => {
stopScheduler()
})
function openModal(task = null) {
if (task) {
editingTask.value = task.id
form.value = { ...task }
} else {
editingTask.value = null
form.value = {
name: '',
cron: '',
url: '',
method: 'GET',
headers: '',
body: '',
enabled: true
}
}
showModal.value = true
}
function closeModal() {
showModal.value = false
editingTask.value = null
}
async function saveTask() {
if (!form.value.name || !form.value.cron || !form.value.url) {
alert('请填写必填项任务名称、Cron表达式、请求URL')
return
}
if (!validateCron(form.value.cron)) {
alert('Cron表达式格式不正确')
return
}
if (editingTask.value) {
await cronStore.updateTask(editingTask.value, form.value)
} else {
await cronStore.createTask(form.value)
}
closeModal()
}
async function deleteTask(id) {
if (confirm('确定要删除这个任务吗?')) {
await cronStore.deleteTask(id)
}
}
async function toggleTask(id) {
await cronStore.toggleTask(id)
}
function validateCron(cron) {
const parts = cron.trim().split(/\s+/)
if (parts.length < 5 || parts.length > 6) return false
return parts.every(part => /^[^\s]+$/.test(part) && part !== '')
}
function formatTime(date) {
if (!date) return '-'
return new Date(date).toLocaleString(locale.value === 'en' ? 'en-US' : 'zh-CN')
}
async function executeTask(task) {
await cronStore.executeTask(task.id)
}
function startScheduler() {
schedulerTimer = setInterval(() => {
const now = new Date()
cronStore.tasks.forEach(task => {
if (!task.enabled || !task.nextRun) return
const nextRun = new Date(task.nextRun)
if (now >= nextRun) {
cronStore.executeTask(task.id)
}
})
}, 10000)
}
function stopScheduler() {
if (schedulerTimer) {
clearInterval(schedulerTimer)
schedulerTimer = null
}
}
</script>
<template>
<div class="cron-page">
<div class="cron-header">
<h1>{{ t('cron.title') }}</h1>
<button class="btn-primary" @click="openModal()">+ {{ t('cron.newTask') }}</button>
</div>
<div class="cron-tips">
<h3>{{ t('cron.usage') }}</h3>
<ul>
<li>{{ t('cron.cronFormat') }}</li>
<li>{{ t('cron.supportedSymbols') }}</li>
<li>{{ t('cron.headersFormat') }}</li>
</ul>
</div>
<div class="task-list">
<div v-if="cronStore.tasks.length === 0" class="empty-state">
<p>{{ t('cron.empty') }}</p>
</div>
<div v-for="task in cronStore.tasks" :key="task.id" class="task-card">
<div class="task-main">
<div class="task-info">
<div class="task-name">
{{ task.name }}
<span v-if="!task.enabled" class="badge-disabled">{{ t('cron.disabled') }}</span>
<span v-if="cronStore.taskStatus[task.id] === 'running'" class="badge-running">{{ t('cron.running') }}</span>
<span v-else-if="cronStore.taskStatus[task.id] === 'success'" class="badge-success">{{ t('cron.success') }}</span>
<span v-else-if="cronStore.taskStatus[task.id] === 'error'" class="badge-error">{{ t('cron.error') }}</span>
</div>
<div class="task-cron">{{ task.cron }}</div>
<div class="task-request">
<span class="method-badge" :class="'method-' + task.method.toLowerCase()">{{ task.method }}</span>
{{ task.url }}
</div>
</div>
<div class="task-stats">
<div class="stat-item">
<span class="stat-label">{{ t('cron.nextRun') }}</span>
<span class="stat-value">{{ task.nextRun || '-' }}</span>
</div>
<div class="stat-item">
<span class="stat-label">{{ t('cron.lastRun') }}</span>
<span class="stat-value">{{ task.lastRun || '-' }}</span>
</div>
<div class="stat-item">
<span class="stat-label">{{ t('cron.runCount') }}</span>
<span class="stat-value">{{ task.runCount || 0 }}</span>
</div>
</div>
</div>
<div class="task-actions">
<button class="btn-action" @click="executeTask(task)" :disabled="cronStore.taskStatus[task.id] === 'running'">
{{ cronStore.taskStatus[task.id] === 'running' ? t('cron.executing') : t('cron.runNow') }}
</button>
<button class="btn-action" @click="toggleTask(task.id)">
{{ task.enabled ? t('cron.disable') : t('cron.enable') }}
</button>
<button class="btn-action" @click="openModal(task)">{{ t('cron.edit') }}</button>
<button class="btn-action btn-danger" @click="deleteTask(task.id)">{{ t('cron.delete') }}</button>
</div>
</div>
</div>
<div v-if="showModal" class="modal-overlay" @click.self="closeModal">
<div class="modal">
<div class="modal-header">
<h2>{{ editingTask ? t('cron.editTask') : t('cron.newTask') }}</h2>
<button class="btn-close" @click="closeModal">&times;</button>
</div>
<div class="modal-body">
<div class="form-group">
<label>{{ t('cron.taskName') }} *</label>
<input v-model="form.name" type="text" :placeholder="t('cron.taskNamePlaceholder')" />
</div>
<div class="form-group">
<label>{{ t('cron.cronExpression') }} *</label>
<input v-model="form.cron" type="text" placeholder="*/5 * * * *" />
<span class="form-hint">{{ t('cron.cronHint') }}</span>
</div>
<div class="form-row">
<div class="form-group">
<label>{{ t('cron.requestMethod') }}</label>
<select v-model="form.method">
<option value="GET">GET</option>
<option value="POST">POST</option>
<option value="PUT">PUT</option>
<option value="PATCH">PATCH</option>
<option value="DELETE">DELETE</option>
</select>
</div>
<div class="form-group flex-1">
<label>{{ t('cron.requestURL') }} *</label>
<input v-model="form.url" type="text" placeholder="https://api.example.com/webhook" />
</div>
</div>
<div class="form-group">
<label>{{ t('cron.requestHeaders') }} (JSON)</label>
<input v-model="form.headers" type="text" placeholder='{"Content-Type": "application/json"}' />
</div>
<div class="form-group">
<label>{{ t('cron.requestBody') }}</label>
<textarea v-model="form.body" rows="3" placeholder='{"key": "value"}'></textarea>
</div>
<div class="form-group">
<label class="checkbox-label">
<input v-model="form.enabled" type="checkbox" />
{{ t('cron.enableTask') }}
</label>
</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" @click="closeModal">{{ t('cron.cancel') }}</button>
<button class="btn-primary" @click="saveTask">{{ t('cron.save') }}</button>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.cron-page {
max-width: 1000px;
margin: 0 auto;
}
.cron-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--color-border);
}
.cron-header h1 {
font-size: 1.8rem;
}
.btn-primary {
background: var(--color-primary);
color: white;
border: none;
padding: 0.6rem 1.2rem;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
}
.btn-primary:hover {
opacity: 0.9;
}
.cron-tips {
background: var(--color-background);
border: 1px solid var(--color-border);
border-radius: 8px;
padding: 1rem 1.5rem;
margin-bottom: 2rem;
}
.cron-tips h3 {
margin-bottom: 0.5rem;
font-size: 1rem;
}
.cron-tips ul {
list-style: none;
padding: 0;
margin: 0;
}
.cron-tips li {
line-height: 1.8;
color: var(--color-text-secondary);
}
.cron-tips code {
background: var(--color-border);
padding: 0.1rem 0.4rem;
border-radius: 3px;
font-size: 0.9em;
}
.task-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.empty-state {
text-align: center;
padding: 3rem;
color: var(--color-text-secondary);
background: var(--color-background);
border-radius: 8px;
}
.task-card {
background: var(--color-background);
border: 1px solid var(--color-border);
border-radius: 8px;
padding: 1.5rem;
}
.task-main {
display: flex;
justify-content: space-between;
gap: 2rem;
margin-bottom: 1rem;
}
.task-info {
flex: 1;
}
.task-name {
font-size: 1.2rem;
font-weight: bold;
margin-bottom: 0.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.badge-disabled {
background: #999;
color: white;
padding: 0.1rem 0.5rem;
border-radius: 3px;
font-size: 0.75rem;
font-weight: normal;
}
.badge-running {
background: #f59e0b;
color: white;
padding: 0.1rem 0.5rem;
border-radius: 3px;
font-size: 0.75rem;
font-weight: normal;
}
.badge-success {
background: #22c55e;
color: white;
padding: 0.1rem 0.5rem;
border-radius: 3px;
font-size: 0.75rem;
font-weight: normal;
}
.badge-error {
background: #ef4444;
color: white;
padding: 0.1rem 0.5rem;
border-radius: 3px;
font-size: 0.75rem;
font-weight: normal;
}
.task-cron {
font-family: monospace;
color: var(--color-primary);
margin-bottom: 0.5rem;
}
.task-request {
color: var(--color-text-secondary);
font-size: 0.9rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.method-badge {
padding: 0.1rem 0.4rem;
border-radius: 3px;
font-size: 0.75rem;
font-weight: bold;
}
.method-get { background: #22c55e; color: white; }
.method-post { background: #3b82f6; color: white; }
.method-put { background: #f59e0b; color: white; }
.method-patch { background: #8b5cf6; color: white; }
.method-delete { background: #ef4444; color: white; }
.task-stats {
display: flex;
gap: 2rem;
text-align: center;
}
.stat-item {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.stat-label {
font-size: 0.75rem;
color: var(--color-text-secondary);
}
.stat-value {
font-size: 0.9rem;
font-weight: 500;
}
.task-actions {
display: flex;
gap: 0.5rem;
padding-top: 1rem;
border-top: 1px solid var(--color-border);
}
.btn-action {
background: var(--color-border);
border: none;
padding: 0.4rem 0.8rem;
border-radius: 4px;
cursor: pointer;
font-size: 0.85rem;
}
.btn-action:hover {
background: #ccc;
}
.btn-action:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-danger {
background: #fee2e2;
color: #dc2626;
}
.btn-danger:hover {
background: #fecaca;
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal {
background: var(--color-background);
border-radius: 8px;
width: 90%;
max-width: 500px;
max-height: 90vh;
overflow-y: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--color-border);
}
.modal-header h2 {
font-size: 1.2rem;
margin: 0;
}
.btn-close {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: var(--color-text-secondary);
}
.modal-body {
padding: 1.5rem;
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.3rem;
font-weight: 500;
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
padding: 0.5rem;
border: 1px solid var(--color-border);
border-radius: 4px;
background: var(--color-background);
color: var(--color-text);
font-size: 1rem;
box-sizing: border-box;
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: var(--color-primary);
}
.form-hint {
font-size: 0.8rem;
color: var(--color-text-secondary);
margin-top: 0.25rem;
display: block;
}
.form-row {
display: flex;
gap: 1rem;
}
.flex-1 {
flex: 1;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
}
.checkbox-label input {
width: auto;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
padding: 1rem 1.5rem;
border-top: 1px solid var(--color-border);
}
.btn-secondary {
background: var(--color-border);
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
}
.btn-secondary:hover {
background: #ccc;
}
</style>