重构ScheduledJobList.vue为根据字段类型动态渲染
This commit is contained in:
parent
7089e04abd
commit
f711090e8d
@ -38,12 +38,15 @@
|
|||||||
<div v-else>
|
<div v-else>
|
||||||
<!-- 列表视图 -->
|
<!-- 列表视图 -->
|
||||||
<div class="job-list">
|
<div class="job-list">
|
||||||
<div class="list-header">
|
<div class="list-header" :style="{ gridTemplateColumns: getGridTemplateColumns() }">
|
||||||
<div class="col-name">{{ t('Method') }}</div>
|
<div
|
||||||
<div class="col-frequency">{{ t('Frequency') }}</div>
|
v-for="field in listViewFields"
|
||||||
<div class="col-cron">{{ t('Cron Format') }}</div>
|
:key="field.fieldname"
|
||||||
<div class="col-status">{{ t('Status') }}</div>
|
class="col-header"
|
||||||
<div class="col-last-execution">{{ t('Last Execution') }}</div>
|
:class="`col-${field.fieldname}`"
|
||||||
|
>
|
||||||
|
{{ t(field.label || field.fieldname) }}
|
||||||
|
</div>
|
||||||
<div class="col-actions">{{ t('Actions') }}</div>
|
<div class="col-actions">{{ t('Actions') }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="list-body">
|
<div class="list-body">
|
||||||
@ -51,30 +54,53 @@
|
|||||||
v-for="job in jobs"
|
v-for="job in jobs"
|
||||||
:key="job.name"
|
:key="job.name"
|
||||||
class="list-item"
|
class="list-item"
|
||||||
|
:style="{ gridTemplateColumns: getGridTemplateColumns() }"
|
||||||
@click="openDetail(job.name)"
|
@click="openDetail(job.name)"
|
||||||
>
|
>
|
||||||
<div class="col-name">
|
<template v-for="field in listViewFields" :key="field.fieldname">
|
||||||
<div class="name">{{ job.method || job.name }}</div>
|
<div class="col-field" :class="`col-${field.fieldname}`">
|
||||||
<div class="description">{{ job.name }}</div>
|
<template v-if="field.fieldtype === 'Check'">
|
||||||
</div>
|
<n-checkbox
|
||||||
<div class="col-frequency">
|
v-model:checked="job[field.fieldname]"
|
||||||
<span class="badge">{{ t(job.frequency || '—') }}</span>
|
@update:checked="field.fieldname === 'stopped' ? () => handleStatusChange(job) : undefined"
|
||||||
</div>
|
@click.stop
|
||||||
<div class="col-cron">
|
/>
|
||||||
<span v-if="job.cron_format" class="cron-text">{{ job.cron_format }}</span>
|
<span
|
||||||
<span v-else class="text-muted">—</span>
|
:class="['status-badge', job[field.fieldname] ? 'stopped' : 'running']"
|
||||||
</div>
|
v-if="field.fieldname === 'stopped'"
|
||||||
<div class="col-status">
|
>
|
||||||
<n-checkbox v-model:checked="job.stopped" @update:checked="() => handleStatusChange(job)">
|
{{ job[field.fieldname] ? t('Stopped') : t('Running') }}
|
||||||
</n-checkbox>
|
</span>
|
||||||
<span :class="['status-badge', job.stopped ? 'stopped' : 'running']">
|
</template>
|
||||||
{{ job.stopped ? t('Stopped') : t('Running') }}
|
<template v-else>
|
||||||
</span>
|
<div v-if="field.fieldname === 'method' || field.fieldname === 'name'" class="col-name-wrapper">
|
||||||
</div>
|
<div class="name">{{ renderFieldValue(field, job).value }}</div>
|
||||||
<div class="col-last-execution">
|
<div v-if="field.fieldname === 'method' && job.name !== job.method" class="description">{{ job.name }}</div>
|
||||||
<span v-if="job.last_execution" class="datetime-text">{{ formatDateTime(job.last_execution) }}</span>
|
</div>
|
||||||
<span v-else class="text-muted">—</span>
|
<span
|
||||||
</div>
|
v-else-if="field.fieldtype === 'Select' && field.fieldname === 'frequency'"
|
||||||
|
class="badge"
|
||||||
|
>
|
||||||
|
{{ t(renderFieldValue(field, job).value) }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else-if="field.fieldname === 'cron_format' && job.cron_format"
|
||||||
|
class="cron-text"
|
||||||
|
>
|
||||||
|
{{ renderFieldValue(field, job).value }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else-if="field.fieldtype === 'Datetime' || field.fieldtype === 'Date'"
|
||||||
|
:class="job[field.fieldname] ? 'datetime-text' : 'text-muted'"
|
||||||
|
>
|
||||||
|
{{ renderFieldValue(field, job).value }}
|
||||||
|
</span>
|
||||||
|
<span v-else :class="job[field.fieldname] ? '' : 'text-muted'">
|
||||||
|
{{ renderFieldValue(field, job).value }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<div class="col-actions">
|
<div class="col-actions">
|
||||||
<button
|
<button
|
||||||
:class="['action-btn', job.stopped ? 'start-btn' : 'stop-btn']"
|
:class="['action-btn', job.stopped ? 'start-btn' : 'stop-btn']"
|
||||||
@ -104,10 +130,11 @@ import { ref, computed, onMounted, watch } from 'vue'
|
|||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { t } from '../../shared/i18n'
|
import { t } from '../../shared/i18n'
|
||||||
import { NInput, NSelect, NPagination, NCheckbox, useMessage } from 'naive-ui'
|
import { NInput, NSelect, NPagination, NCheckbox, useMessage } from 'naive-ui'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { get_session_api_headers } from '../../shared/api/auth'
|
||||||
import {
|
import {
|
||||||
getScheduledJobs,
|
getScheduledJobs,
|
||||||
toggleScheduledJobStatus,
|
toggleScheduledJobStatus
|
||||||
getFrequencyOptions
|
|
||||||
} from '../../shared/api/scheduledJobs'
|
} from '../../shared/api/scheduledJobs'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@ -124,6 +151,10 @@ const statusFilter = ref('all')
|
|||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
const pageSize = ref(parseInt(localStorage.getItem('itemsPerPage') || '10'))
|
const pageSize = ref(parseInt(localStorage.getItem('itemsPerPage') || '10'))
|
||||||
|
|
||||||
|
// 字段元数据
|
||||||
|
const metaFields = ref<any[]>([])
|
||||||
|
const listViewFields = ref<any[]>([]) // 列表中显示的字段
|
||||||
|
|
||||||
// 选项数据
|
// 选项数据
|
||||||
const frequencyOptions = ref<{ label: string; value: string }[]>([])
|
const frequencyOptions = ref<{ label: string; value: string }[]>([])
|
||||||
const statusOptions = computed(() => [
|
const statusOptions = computed(() => [
|
||||||
@ -252,19 +283,139 @@ function formatDateTime(dateTimeStr: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载频率选项
|
// 生成动态网格列模板
|
||||||
async function loadFrequencyOptions() {
|
function getGridTemplateColumns(): string {
|
||||||
|
if (listViewFields.value.length === 0) {
|
||||||
|
return '1fr 120px 150px 100px 180px 120px'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为每个字段生成列宽
|
||||||
|
const columns = listViewFields.value.map((field: any) => {
|
||||||
|
// 根据字段类型和名称设置合适的列宽
|
||||||
|
if (field.fieldname === 'method' || field.fieldname === 'name') {
|
||||||
|
return '1fr' // 主字段自适应宽度
|
||||||
|
} else if (field.fieldname === 'frequency') {
|
||||||
|
return '120px'
|
||||||
|
} else if (field.fieldname === 'cron_format') {
|
||||||
|
return '150px'
|
||||||
|
} else if (field.fieldname === 'stopped') {
|
||||||
|
return '120px'
|
||||||
|
} else if (field.fieldtype === 'Datetime' || field.fieldtype === 'Date') {
|
||||||
|
return '180px'
|
||||||
|
} else if (field.fieldtype === 'Check') {
|
||||||
|
return '100px'
|
||||||
|
} else {
|
||||||
|
return '120px' // 默认宽度
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 添加操作列
|
||||||
|
columns.push('120px')
|
||||||
|
|
||||||
|
return columns.join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载字段元数据
|
||||||
|
async function loadMetaFields() {
|
||||||
try {
|
try {
|
||||||
const options = await getFrequencyOptions()
|
const response = await axios.get(
|
||||||
frequencyOptions.value = [
|
`/api/data/PageType/${encodeURIComponent('Local Scheduled Job')}`,
|
||||||
{ label: t('All'), value: 'all' },
|
{ headers: get_session_api_headers(), withCredentials: true }
|
||||||
...options.map(option => ({
|
)
|
||||||
label: t(option),
|
const data = response.data?.data || {}
|
||||||
value: option
|
metaFields.value = data.fields || []
|
||||||
}))
|
|
||||||
]
|
// 筛选出需要在列表视图中显示的字段
|
||||||
|
listViewFields.value = metaFields.value.filter((field: any) => {
|
||||||
|
// 显示 in_list_view 为 1 的字段,或者特定的字段
|
||||||
|
return field.in_list_view === 1 || ['method', 'frequency', 'cron_format', 'stopped', 'last_execution'].includes(field.fieldname)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 按 field_order 排序
|
||||||
|
const fieldOrder = data.field_order || []
|
||||||
|
listViewFields.value.sort((a: any, b: any) => {
|
||||||
|
const indexA = fieldOrder.indexOf(a.fieldname)
|
||||||
|
const indexB = fieldOrder.indexOf(b.fieldname)
|
||||||
|
if (indexA === -1 && indexB === -1) return 0
|
||||||
|
if (indexA === -1) return 1
|
||||||
|
if (indexB === -1) return -1
|
||||||
|
return indexA - indexB
|
||||||
|
})
|
||||||
|
|
||||||
|
// 从字段元数据中提取 frequency 字段的选项
|
||||||
|
const frequencyField = metaFields.value.find((f: any) => f.fieldname === 'frequency')
|
||||||
|
if (frequencyField && frequencyField.options) {
|
||||||
|
const options = typeof frequencyField.options === 'string'
|
||||||
|
? frequencyField.options.split('\n').filter((opt: string) => opt.trim())
|
||||||
|
: []
|
||||||
|
frequencyOptions.value = [
|
||||||
|
{ label: t('All'), value: 'all' },
|
||||||
|
...options.map((option: string) => ({
|
||||||
|
label: t(option.trim()),
|
||||||
|
value: option.trim()
|
||||||
|
}))
|
||||||
|
]
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load frequency options:', error)
|
console.error('Failed to load meta fields:', error)
|
||||||
|
// 如果加载失败,使用默认字段
|
||||||
|
listViewFields.value = [
|
||||||
|
{ fieldname: 'method', label: 'Method', fieldtype: 'Data' },
|
||||||
|
{ fieldname: 'frequency', label: 'Frequency', fieldtype: 'Select' },
|
||||||
|
{ fieldname: 'cron_format', label: 'Cron Format', fieldtype: 'Data' },
|
||||||
|
{ fieldname: 'stopped', label: 'Status', fieldtype: 'Check' },
|
||||||
|
{ fieldname: 'last_execution', label: 'Last Execution', fieldtype: 'Datetime' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据字段类型渲染字段值
|
||||||
|
function renderFieldValue(field: any, job: any): any {
|
||||||
|
const value = job[field.fieldname]
|
||||||
|
const fieldtype = field.fieldtype || 'Data'
|
||||||
|
|
||||||
|
switch (fieldtype) {
|
||||||
|
case 'Check':
|
||||||
|
return {
|
||||||
|
type: 'checkbox',
|
||||||
|
value: Boolean(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'Datetime':
|
||||||
|
case 'Date':
|
||||||
|
if (!value) return { type: 'text', value: '—' }
|
||||||
|
try {
|
||||||
|
const date = new Date(value)
|
||||||
|
return {
|
||||||
|
type: 'text',
|
||||||
|
value: date.toLocaleString()
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return { type: 'text', value: value }
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'Select':
|
||||||
|
// Select 字段直接显示值
|
||||||
|
return {
|
||||||
|
type: 'text',
|
||||||
|
value: value || '—'
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'Link':
|
||||||
|
// Link 字段显示链接值
|
||||||
|
return {
|
||||||
|
type: 'text',
|
||||||
|
value: value || '—'
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'Data':
|
||||||
|
case 'Small Text':
|
||||||
|
case 'Text':
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
type: 'text',
|
||||||
|
value: value || '—'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -289,7 +440,7 @@ watch(() => localStorage.getItem('itemsPerPage'), (newValue) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadFrequencyOptions()
|
loadMetaFields()
|
||||||
fetchJobs()
|
fetchJobs()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@ -399,7 +550,6 @@ onMounted(() => {
|
|||||||
|
|
||||||
.list-header {
|
.list-header {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 120px 150px 100px 180px 120px;
|
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
padding: 16px 20px;
|
padding: 16px 20px;
|
||||||
background: #f9fafb;
|
background: #f9fafb;
|
||||||
@ -410,7 +560,7 @@ onMounted(() => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-header > div {
|
.col-header {
|
||||||
color: #374151;
|
color: #374151;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -421,7 +571,6 @@ onMounted(() => {
|
|||||||
|
|
||||||
.list-item {
|
.list-item {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 120px 150px 100px 180px 120px;
|
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
padding: 16px 20px;
|
padding: 16px 20px;
|
||||||
border-bottom: 1px solid #f3f4f6;
|
border-bottom: 1px solid #f3f4f6;
|
||||||
@ -438,6 +587,36 @@ onMounted(() => {
|
|||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.col-field {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-name-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-name-wrapper .name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111827;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-name-wrapper .description {
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.3;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 保留向后兼容的类名 */
|
||||||
.col-name {
|
.col-name {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user