TaskPulse/frontend/src/main.js
Steven b04c0ce321 feat: agent avatars, task tags, search, edit/delete agents, cron readability, timezone fix
- Agent avatars: first-letter + deterministic color avatar in all views
- Agent management: edit (name/description) and delete with confirmation
- Execution API: validates agent existence, returns 410 if deleted
- Task tags: JSON array field, editable with tag pills UI
- Task search: by name (?q=) and by tags (?tags=) and by status
- Task edit: simplified to name/tags/description only (not agent params)
- Cron display: human-readable Chinese (e.g. '每5分钟', '每天 09:00')
- Timezone fix: UTC→local via dayjs.utc().local() across all pages
- Content area: full width (removed max-width:1200px constraints)
- Login page: standalone layout without sidebar
2026-06-15 23:33:28 +08:00

137 lines
4.8 KiB
JavaScript
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.

import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc'
import App from './App.vue'
import router from './router'
import './assets/style.css'
// Extend dayjs with UTC plugin for timezone conversion
dayjs.extend(utc)
const app = createApp(App)
// Register all Element Plus icons
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
// Global helper: cron expression to readable Chinese
app.config.globalProperties.$cron = (expr) => {
if (!expr) return ''
try {
const parts = expr.trim().split(/\s+/)
if (parts.length !== 5) return expr
const [min, hour, dom, month, dow] = parts
// Helper: describe a cron field
const descField = (val, unit) => {
if (val === '*') return `${unit}`
if (val.startsWith('*/')) return `${val.slice(2)}${unit}`
if (val.includes(',')) {
const items = val.split(',').map(v => v.padStart(2, '0')).join('分、')
return `${items}`
}
if (val.includes('-')) {
const [from, to] = val.split('-')
return `${from.padStart(2,'0')}分到${to.padStart(2,'0')}`
}
return `${val.padStart(2,'0')}${unit}`
}
const allStar = (...args) => args.every(v => v === '*')
// Every minute
if (min === '*' && hour === '*' && allStar(dom, month, dow)) return '每分钟'
// Every N minutes (always)
if (min.startsWith('*/') && hour === '*' && allStar(dom, month, dow)) {
return `${min.slice(2)}分钟`
}
// Every N hours (at min 0)
if (min === '0' && hour.startsWith('*/') && allStar(dom, month, dow)) {
return `${hour.slice(2)}小时`
}
// Specific minutes every hour
if (min !== '*' && !min.startsWith('*/') && hour === '*' && allStar(dom, month, dow)) {
if (min.includes(',')) {
const items = min.split(',').map(v => v.padStart(2, '0'))
return `每小时 ${items.join('、')}`
}
return `每小时 第${min.padStart(2,'0')}分钟`
}
// Daily at specific time(s)
if (allStar(dom, month, dow)) {
if (min.startsWith('*/')) {
const interval = min.slice(2)
if (hour.includes('-')) {
const [hFrom, hTo] = hour.split('-').map(h => h.padStart(2,'0'))
return `${interval}分钟(${hFrom}:00-${hTo}:00`
}
if (hour !== '*') return `${interval}分钟(${hour.padStart(2,'0')}点)`
return `${interval}分钟`
}
if (hour.includes(',')) {
const times = hour.split(',').map(h => `${h.padStart(2,'0')}:${min.padStart(2,'0')}`)
return `每天 ${times.join('、')}`
}
if (hour.includes('-')) {
return `每天 ${hour.split('-')[0].padStart(2,'0')}:${min.padStart(2,'0')}${hour.split('-')[1].padStart(2,'0')}:${min.padStart(2,'0')}`
}
return `每天 ${hour.padStart(2,'0')}:${min.padStart(2,'0')}`
}
// Weekly
const dowNames = ['日', '一', '二', '三', '四', '五', '六', '日']
if (allStar(dom, month) && dow !== '*') {
if (dow.includes(',')) {
const days = dow.split(',').map(d => `${dowNames[parseInt(d)]}`)
return `${days.join('、')} ${hour.padStart(2,'0')}:${min.padStart(2,'0')}`
}
if (dow.includes('-')) {
const [from, to] = dow.split('-')
return `每周${dowNames[parseInt(from)]}${dowNames[parseInt(to)]} ${hour.padStart(2,'0')}:${min.padStart(2,'0')}`
}
return `每周${dowNames[parseInt(dow)]} ${hour.padStart(2,'0')}:${min.padStart(2,'0')}`
}
// Every N minutes within specific hours (e.g., */15 9-18 * * *)
if (min.startsWith('*/') && !allStar(dom, month, dow)) {
const interval = min.slice(2)
let hourDesc = ''
if (hour !== '*') hourDesc = descField(hour, '点')
return `${interval}分钟${hourDesc ? ` (${hourDesc})` : ''}`
}
return expr
} catch {
return expr
}
}
// Global helper: agent name → avatar { letter, bg color }
const AVATAR_COLORS = [
'#667eea', '#22c55e', '#f59e0b', '#ef4444', '#ec4899',
'#8b5cf6', '#06b6d4', '#f97316', '#14b8a6', '#a855f7',
'#6366f1', '#84cc16', '#e11d48', '#0ea5e9',
]
app.config.globalProperties.$avatar = (name) => {
if (!name) return { letter: '?', bg: AVATAR_COLORS[0] }
const letter = name.trim()[0]
// Deterministic color from name
let hash = 0
for (let i = 0; i < name.length; i++) hash = ((hash << 5) - hash) + name.charCodeAt(i)
const idx = Math.abs(hash) % AVATAR_COLORS.length
return { letter, bg: AVATAR_COLORS[idx] }
}
app.use(ElementPlus, { locale: zhCn })
app.use(router)
app.mount('#app')