重构ScheduledJobList.vue为根据字段类型动态渲染
This commit is contained in:
parent
7089e04abd
commit
f711090e8d
@ -38,12 +38,15 @@
|
||||
<div v-else>
|
||||
<!-- 列表视图 -->
|
||||
<div class="job-list">
|
||||
<div class="list-header">
|
||||
<div class="col-name">{{ t('Method') }}</div>
|
||||
<div class="col-frequency">{{ t('Frequency') }}</div>
|
||||
<div class="col-cron">{{ t('Cron Format') }}</div>
|
||||
<div class="col-status">{{ t('Status') }}</div>
|
||||
<div class="col-last-execution">{{ t('Last Execution') }}</div>
|
||||
<div class="list-header" :style="{ gridTemplateColumns: getGridTemplateColumns() }">
|
||||
<div
|
||||
v-for="field in listViewFields"
|
||||
:key="field.fieldname"
|
||||
class="col-header"
|
||||
:class="`col-${field.fieldname}`"
|
||||
>
|
||||
{{ t(field.label || field.fieldname) }}
|
||||
</div>
|
||||
<div class="col-actions">{{ t('Actions') }}</div>
|
||||
</div>
|
||||
<div class="list-body">
|
||||
@ -51,30 +54,53 @@
|
||||
v-for="job in jobs"
|
||||
:key="job.name"
|
||||
class="list-item"
|
||||
:style="{ gridTemplateColumns: getGridTemplateColumns() }"
|
||||
@click="openDetail(job.name)"
|
||||
>
|
||||
<div class="col-name">
|
||||
<div class="name">{{ job.method || job.name }}</div>
|
||||
<div class="description">{{ job.name }}</div>
|
||||
</div>
|
||||
<div class="col-frequency">
|
||||
<span class="badge">{{ t(job.frequency || '—') }}</span>
|
||||
</div>
|
||||
<div class="col-cron">
|
||||
<span v-if="job.cron_format" class="cron-text">{{ job.cron_format }}</span>
|
||||
<span v-else class="text-muted">—</span>
|
||||
</div>
|
||||
<div class="col-status">
|
||||
<n-checkbox v-model:checked="job.stopped" @update:checked="() => handleStatusChange(job)">
|
||||
</n-checkbox>
|
||||
<span :class="['status-badge', job.stopped ? 'stopped' : 'running']">
|
||||
{{ job.stopped ? t('Stopped') : t('Running') }}
|
||||
<template v-for="field in listViewFields" :key="field.fieldname">
|
||||
<div class="col-field" :class="`col-${field.fieldname}`">
|
||||
<template v-if="field.fieldtype === 'Check'">
|
||||
<n-checkbox
|
||||
v-model:checked="job[field.fieldname]"
|
||||
@update:checked="field.fieldname === 'stopped' ? () => handleStatusChange(job) : undefined"
|
||||
@click.stop
|
||||
/>
|
||||
<span
|
||||
:class="['status-badge', job[field.fieldname] ? 'stopped' : 'running']"
|
||||
v-if="field.fieldname === 'stopped'"
|
||||
>
|
||||
{{ job[field.fieldname] ? t('Stopped') : t('Running') }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div v-if="field.fieldname === 'method' || field.fieldname === 'name'" class="col-name-wrapper">
|
||||
<div class="name">{{ renderFieldValue(field, job).value }}</div>
|
||||
<div v-if="field.fieldname === 'method' && job.name !== job.method" class="description">{{ job.name }}</div>
|
||||
</div>
|
||||
<div class="col-last-execution">
|
||||
<span v-if="job.last_execution" class="datetime-text">{{ formatDateTime(job.last_execution) }}</span>
|
||||
<span v-else class="text-muted">—</span>
|
||||
<span
|
||||
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">
|
||||
<button
|
||||
: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 { t } from '../../shared/i18n'
|
||||
import { NInput, NSelect, NPagination, NCheckbox, useMessage } from 'naive-ui'
|
||||
import axios from 'axios'
|
||||
import { get_session_api_headers } from '../../shared/api/auth'
|
||||
import {
|
||||
getScheduledJobs,
|
||||
toggleScheduledJobStatus,
|
||||
getFrequencyOptions
|
||||
toggleScheduledJobStatus
|
||||
} from '../../shared/api/scheduledJobs'
|
||||
|
||||
const router = useRouter()
|
||||
@ -124,6 +151,10 @@ const statusFilter = ref('all')
|
||||
const page = ref(1)
|
||||
const pageSize = ref(parseInt(localStorage.getItem('itemsPerPage') || '10'))
|
||||
|
||||
// 字段元数据
|
||||
const metaFields = ref<any[]>([])
|
||||
const listViewFields = ref<any[]>([]) // 列表中显示的字段
|
||||
|
||||
// 选项数据
|
||||
const frequencyOptions = ref<{ label: string; value: string }[]>([])
|
||||
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 {
|
||||
const options = await getFrequencyOptions()
|
||||
const response = await axios.get(
|
||||
`/api/data/PageType/${encodeURIComponent('Local Scheduled Job')}`,
|
||||
{ headers: get_session_api_headers(), withCredentials: true }
|
||||
)
|
||||
const data = response.data?.data || {}
|
||||
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 => ({
|
||||
label: t(option),
|
||||
value: option
|
||||
...options.map((option: string) => ({
|
||||
label: t(option.trim()),
|
||||
value: option.trim()
|
||||
}))
|
||||
]
|
||||
}
|
||||
} 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(() => {
|
||||
loadFrequencyOptions()
|
||||
loadMetaFields()
|
||||
fetchJobs()
|
||||
})
|
||||
</script>
|
||||
@ -399,7 +550,6 @@ onMounted(() => {
|
||||
|
||||
.list-header {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 120px 150px 100px 180px 120px;
|
||||
gap: 16px;
|
||||
padding: 16px 20px;
|
||||
background: #f9fafb;
|
||||
@ -410,7 +560,7 @@ onMounted(() => {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.list-header > div {
|
||||
.col-header {
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
@ -421,7 +571,6 @@ onMounted(() => {
|
||||
|
||||
.list-item {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 120px 150px 100px 180px 120px;
|
||||
gap: 16px;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
@ -438,6 +587,36 @@ onMounted(() => {
|
||||
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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user