- 新增 platform_scheduled_tasks 和 platform_task_logs 数据表 - 实现 APScheduler 调度器服务(支持简单模式和CRON表达式) - 添加定时任务 CRUD API - 支持手动触发执行和查看执行日志 - 前端任务管理页面
This commit is contained in:
@@ -16,6 +16,7 @@ const menuItems = computed(() => {
|
||||
{ path: '/apps', title: '应用管理', icon: 'Grid' },
|
||||
{ path: '/tenant-wechat-apps', title: '企微应用', icon: 'ChatDotRound' },
|
||||
{ path: '/app-config', title: '租户订阅', icon: 'Setting' },
|
||||
{ path: '/scheduled-tasks', title: '定时任务', icon: 'Clock' },
|
||||
{ path: '/stats', title: '统计分析', icon: 'TrendCharts' },
|
||||
{ path: '/logs', title: '日志查看', icon: 'Document' }
|
||||
]
|
||||
|
||||
@@ -53,7 +53,13 @@ const routes = [
|
||||
path: 'app-config',
|
||||
name: 'AppConfig',
|
||||
component: () => import('@/views/app-config/index.vue'),
|
||||
meta: { title: '租户应用配置', icon: 'Setting' }
|
||||
meta: { title: '租户订阅', icon: 'Setting' }
|
||||
},
|
||||
{
|
||||
path: 'scheduled-tasks',
|
||||
name: 'ScheduledTasks',
|
||||
component: () => import('@/views/scheduled-tasks/index.vue'),
|
||||
meta: { title: '定时任务', icon: 'Clock' }
|
||||
},
|
||||
{
|
||||
path: 'stats',
|
||||
|
||||
645
frontend/src/views/scheduled-tasks/index.vue
Normal file
645
frontend/src/views/scheduled-tasks/index.vue
Normal file
@@ -0,0 +1,645 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Delete, Plus, VideoPlay, Clock, Document } from '@element-plus/icons-vue'
|
||||
import api from '@/api'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const loading = ref(false)
|
||||
const tableData = ref([])
|
||||
const total = ref(0)
|
||||
const query = reactive({
|
||||
page: 1,
|
||||
size: 20,
|
||||
tenant_id: ''
|
||||
})
|
||||
|
||||
// 租户列表
|
||||
const tenantList = ref([])
|
||||
const tenantMap = ref({})
|
||||
|
||||
// 对话框
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const editingId = ref(null)
|
||||
const formRef = ref(null)
|
||||
const form = reactive({
|
||||
tenant_id: '',
|
||||
task_name: '',
|
||||
task_desc: '',
|
||||
schedule_type: 'simple',
|
||||
time_points: [],
|
||||
cron_expression: '',
|
||||
webhook_url: '',
|
||||
input_params: '',
|
||||
is_enabled: true
|
||||
})
|
||||
|
||||
// 时间选择器
|
||||
const newTimePoint = ref('')
|
||||
|
||||
const rules = {
|
||||
tenant_id: [{ required: true, message: '请选择租户', trigger: 'change' }],
|
||||
task_name: [{ required: true, message: '请输入任务名称', trigger: 'blur' }],
|
||||
webhook_url: [{ required: true, message: '请输入 Webhook URL', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
// 日志对话框
|
||||
const logsDialogVisible = ref(false)
|
||||
const logsLoading = ref(false)
|
||||
const logsData = ref([])
|
||||
const logsTotal = ref(0)
|
||||
const logsTaskId = ref(null)
|
||||
const logsQuery = reactive({
|
||||
page: 1,
|
||||
size: 10
|
||||
})
|
||||
|
||||
// 获取租户名称
|
||||
function getTenantName(code) {
|
||||
return tenantMap.value[code] || code
|
||||
}
|
||||
|
||||
// 获取调度描述
|
||||
function getScheduleDesc(row) {
|
||||
if (row.schedule_type === 'cron') {
|
||||
return `CRON: ${row.cron_expression}`
|
||||
}
|
||||
const points = row.time_points || []
|
||||
if (points.length === 0) return '-'
|
||||
if (points.length <= 3) return points.join(', ')
|
||||
return `${points.slice(0, 3).join(', ')} 等${points.length}个时间点`
|
||||
}
|
||||
|
||||
// 获取状态标签类型
|
||||
function getStatusType(status) {
|
||||
const map = {
|
||||
success: 'success',
|
||||
failed: 'danger',
|
||||
running: 'warning'
|
||||
}
|
||||
return map[status] || 'info'
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
function formatTime(time) {
|
||||
if (!time) return '-'
|
||||
return new Date(time).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
async function fetchTenants() {
|
||||
try {
|
||||
const res = await api.get('/api/tenants', { params: { size: 1000 } })
|
||||
tenantList.value = res.data.items || []
|
||||
const map = {}
|
||||
tenantList.value.forEach(t => {
|
||||
map[t.code] = t.name
|
||||
})
|
||||
tenantMap.value = map
|
||||
} catch (e) {
|
||||
console.error('获取租户列表失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await api.get('/api/scheduled-tasks', { params: query })
|
||||
tableData.value = res.data.items || []
|
||||
total.value = res.data.total || 0
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
query.page = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
function handlePageChange(page) {
|
||||
query.page = page
|
||||
fetchList()
|
||||
}
|
||||
|
||||
function handleCreate() {
|
||||
editingId.value = null
|
||||
dialogTitle.value = '新建定时任务'
|
||||
Object.assign(form, {
|
||||
tenant_id: '',
|
||||
task_name: '',
|
||||
task_desc: '',
|
||||
schedule_type: 'simple',
|
||||
time_points: [],
|
||||
cron_expression: '',
|
||||
webhook_url: '',
|
||||
input_params: '',
|
||||
is_enabled: true
|
||||
})
|
||||
newTimePoint.value = ''
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function handleEdit(row) {
|
||||
editingId.value = row.id
|
||||
dialogTitle.value = '编辑定时任务'
|
||||
Object.assign(form, {
|
||||
tenant_id: row.tenant_id,
|
||||
task_name: row.task_name,
|
||||
task_desc: row.task_desc || '',
|
||||
schedule_type: row.schedule_type || 'simple',
|
||||
time_points: row.time_points || [],
|
||||
cron_expression: row.cron_expression || '',
|
||||
webhook_url: row.webhook_url,
|
||||
input_params: row.input_params ? JSON.stringify(row.input_params, null, 2) : '',
|
||||
is_enabled: row.is_enabled
|
||||
})
|
||||
newTimePoint.value = ''
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 添加时间点
|
||||
function addTimePoint() {
|
||||
if (!newTimePoint.value) return
|
||||
if (!form.time_points.includes(newTimePoint.value)) {
|
||||
form.time_points.push(newTimePoint.value)
|
||||
form.time_points.sort()
|
||||
}
|
||||
newTimePoint.value = ''
|
||||
}
|
||||
|
||||
// 移除时间点
|
||||
function removeTimePoint(index) {
|
||||
form.time_points.splice(index, 1)
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
await formRef.value.validate()
|
||||
|
||||
// 验证调度配置
|
||||
if (form.schedule_type === 'simple' && form.time_points.length === 0) {
|
||||
ElMessage.error('请至少添加一个执行时间点')
|
||||
return
|
||||
}
|
||||
if (form.schedule_type === 'cron' && !form.cron_expression) {
|
||||
ElMessage.error('请输入 CRON 表达式')
|
||||
return
|
||||
}
|
||||
|
||||
// 解析输入参数
|
||||
let inputParams = null
|
||||
if (form.input_params) {
|
||||
try {
|
||||
inputParams = JSON.parse(form.input_params)
|
||||
} catch (e) {
|
||||
ElMessage.error('输入参数格式错误,请输入有效的 JSON')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const data = {
|
||||
tenant_id: form.tenant_id,
|
||||
task_name: form.task_name,
|
||||
task_desc: form.task_desc,
|
||||
schedule_type: form.schedule_type,
|
||||
time_points: form.schedule_type === 'simple' ? form.time_points : null,
|
||||
cron_expression: form.schedule_type === 'cron' ? form.cron_expression : null,
|
||||
webhook_url: form.webhook_url,
|
||||
input_params: inputParams,
|
||||
is_enabled: form.is_enabled
|
||||
}
|
||||
|
||||
try {
|
||||
if (editingId.value) {
|
||||
await api.put(`/api/scheduled-tasks/${editingId.value}`, data)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await api.post('/api/scheduled-tasks', data)
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
fetchList()
|
||||
} catch (e) {
|
||||
// 错误已在拦截器处理
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row) {
|
||||
await ElMessageBox.confirm(`确定删除任务「${row.task_name}」吗?`, '提示', {
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
try {
|
||||
await api.delete(`/api/scheduled-tasks/${row.id}`)
|
||||
ElMessage.success('删除成功')
|
||||
fetchList()
|
||||
} catch (e) {
|
||||
// 错误已在拦截器处理
|
||||
}
|
||||
}
|
||||
|
||||
async function handleToggle(row) {
|
||||
try {
|
||||
const res = await api.post(`/api/scheduled-tasks/${row.id}/toggle`)
|
||||
ElMessage.success(res.data.message)
|
||||
fetchList()
|
||||
} catch (e) {
|
||||
// 错误已在拦截器处理
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRunNow(row) {
|
||||
await ElMessageBox.confirm(`确定立即执行任务「${row.task_name}」吗?`, '手动执行', {
|
||||
type: 'info'
|
||||
})
|
||||
|
||||
try {
|
||||
await api.post(`/api/scheduled-tasks/${row.id}/run`)
|
||||
ElMessage.success('任务已触发执行')
|
||||
// 延迟刷新以获取最新状态
|
||||
setTimeout(() => fetchList(), 2000)
|
||||
} catch (e) {
|
||||
// 错误已在拦截器处理
|
||||
}
|
||||
}
|
||||
|
||||
async function handleViewLogs(row) {
|
||||
logsTaskId.value = row.id
|
||||
logsQuery.page = 1
|
||||
logsDialogVisible.value = true
|
||||
await fetchLogs()
|
||||
}
|
||||
|
||||
async function fetchLogs() {
|
||||
logsLoading.value = true
|
||||
try {
|
||||
const res = await api.get(`/api/scheduled-tasks/${logsTaskId.value}/logs`, {
|
||||
params: logsQuery
|
||||
})
|
||||
logsData.value = res.data.items || []
|
||||
logsTotal.value = res.data.total || 0
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
logsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleLogsPageChange(page) {
|
||||
logsQuery.page = page
|
||||
fetchLogs()
|
||||
}
|
||||
|
||||
// 快速选择租户
|
||||
function selectTenant(code) {
|
||||
if (query.tenant_id === code) {
|
||||
query.tenant_id = ''
|
||||
} else {
|
||||
query.tenant_id = code
|
||||
}
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchTenants()
|
||||
fetchList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="page-header">
|
||||
<div class="title">定时任务</div>
|
||||
<el-button v-if="authStore.isOperator" type="primary" @click="handleCreate">
|
||||
<el-icon><Plus /></el-icon>
|
||||
新建任务
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="page-tip">
|
||||
<el-alert type="info" :closable="false">
|
||||
管理定时任务,支持简单时间点和 CRON 表达式两种调度方式,可自动调用 n8n 工作流。
|
||||
</el-alert>
|
||||
</div>
|
||||
|
||||
<!-- 租户快速筛选标签 -->
|
||||
<div class="tenant-tags">
|
||||
<span class="tag-label">租户筛选:</span>
|
||||
<el-tag
|
||||
v-for="tenant in tenantList"
|
||||
:key="tenant.code"
|
||||
:type="query.tenant_id === tenant.code ? '' : 'info'"
|
||||
:effect="query.tenant_id === tenant.code ? 'dark' : 'plain'"
|
||||
class="tenant-tag"
|
||||
@click="selectTenant(tenant.code)"
|
||||
>
|
||||
{{ tenant.name }}
|
||||
</el-tag>
|
||||
<el-tag
|
||||
v-if="query.tenant_id"
|
||||
type="danger"
|
||||
effect="plain"
|
||||
class="tenant-tag clear-tag"
|
||||
@click="selectTenant('')"
|
||||
>
|
||||
清除筛选
|
||||
</el-tag>
|
||||
</div>
|
||||
|
||||
<!-- 表格 -->
|
||||
<el-table v-loading="loading" :data="tableData" style="width: 100%">
|
||||
<el-table-column prop="id" label="ID" width="60" />
|
||||
<el-table-column label="租户" width="100">
|
||||
<template #default="{ row }">
|
||||
{{ getTenantName(row.tenant_id) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="task_name" label="任务名称" width="150" />
|
||||
<el-table-column label="调度配置" width="200">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="row.schedule_type === 'cron' ? 'warning' : ''">
|
||||
{{ row.schedule_type === 'cron' ? 'CRON' : '简单' }}
|
||||
</el-tag>
|
||||
<span style="margin-left: 8px; color: #606266; font-size: 12px">
|
||||
{{ getScheduleDesc(row) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.is_enabled ? 'success' : 'info'" size="small">
|
||||
{{ row.is_enabled ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="上次执行" width="180">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.last_run_at" style="font-size: 12px">
|
||||
<div>{{ formatTime(row.last_run_at) }}</div>
|
||||
<el-tag :type="getStatusType(row.last_run_status)" size="small">
|
||||
{{ row.last_run_status || '-' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<span v-else style="color: #909399">未执行</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="280" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link size="small" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button type="success" link size="small" @click="handleRunNow(row)">
|
||||
<el-icon><VideoPlay /></el-icon>
|
||||
执行
|
||||
</el-button>
|
||||
<el-button
|
||||
:type="row.is_enabled ? 'warning' : 'success'"
|
||||
link
|
||||
size="small"
|
||||
@click="handleToggle(row)"
|
||||
>
|
||||
{{ row.is_enabled ? '禁用' : '启用' }}
|
||||
</el-button>
|
||||
<el-button type="info" link size="small" @click="handleViewLogs(row)">
|
||||
<el-icon><Document /></el-icon>
|
||||
日志
|
||||
</el-button>
|
||||
<el-button v-if="authStore.isOperator" type="danger" link size="small" @click="handleDelete(row)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-wrapper">
|
||||
<el-pagination
|
||||
v-model:current-page="query.page"
|
||||
:page-size="query.size"
|
||||
:total="total"
|
||||
layout="total, prev, pager, next"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 编辑对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="700px"
|
||||
:lock-scroll="true"
|
||||
>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||
<el-form-item label="租户" prop="tenant_id">
|
||||
<el-select
|
||||
v-model="form.tenant_id"
|
||||
:disabled="!!editingId"
|
||||
placeholder="请选择租户"
|
||||
filterable
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="tenant in tenantList"
|
||||
:key="tenant.code"
|
||||
:label="`${tenant.name} (${tenant.code})`"
|
||||
:value="tenant.code"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="任务名称" prop="task_name">
|
||||
<el-input v-model="form.task_name" placeholder="如:每日数据同步" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="任务描述">
|
||||
<el-input v-model="form.task_desc" type="textarea" :rows="2" placeholder="可选" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="调度模式">
|
||||
<el-radio-group v-model="form.schedule_type">
|
||||
<el-radio value="simple">简单模式(选择时间点)</el-radio>
|
||||
<el-radio value="cron">CRON 表达式</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 简单模式 -->
|
||||
<el-form-item v-if="form.schedule_type === 'simple'" label="执行时间">
|
||||
<div class="time-points-editor">
|
||||
<div class="time-points-list">
|
||||
<el-tag
|
||||
v-for="(time, index) in form.time_points"
|
||||
:key="time"
|
||||
closable
|
||||
@close="removeTimePoint(index)"
|
||||
style="margin-right: 8px; margin-bottom: 8px"
|
||||
>
|
||||
{{ time }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="time-points-add">
|
||||
<el-time-select
|
||||
v-model="newTimePoint"
|
||||
start="00:00"
|
||||
step="00:30"
|
||||
end="23:30"
|
||||
placeholder="选择时间"
|
||||
style="width: 140px"
|
||||
/>
|
||||
<el-button type="primary" @click="addTimePoint" :disabled="!newTimePoint">
|
||||
添加
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- CRON 模式 -->
|
||||
<el-form-item v-if="form.schedule_type === 'cron'" label="CRON 表达式">
|
||||
<el-input v-model="form.cron_expression" placeholder="如:0 9,18 * * *(每天9点和18点)" />
|
||||
<div class="form-tip">
|
||||
格式:分 时 日 月 周。例如:0 9 * * * 表示每天9点执行
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="Webhook URL" prop="webhook_url">
|
||||
<el-input v-model="form.webhook_url" placeholder="如:https://n8n.ireborn.com.cn/webhook/xxx" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="输入参数">
|
||||
<el-input
|
||||
v-model="form.input_params"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder='可选,JSON 格式,如:{"key": "value"}'
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="启用状态">
|
||||
<el-switch v-model="form.is_enabled" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 日志对话框 -->
|
||||
<el-dialog
|
||||
v-model="logsDialogVisible"
|
||||
title="执行日志"
|
||||
width="800px"
|
||||
:lock-scroll="true"
|
||||
>
|
||||
<el-table v-loading="logsLoading" :data="logsData" style="width: 100%">
|
||||
<el-table-column label="开始时间" width="170">
|
||||
<template #default="{ row }">
|
||||
{{ formatTime(row.started_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="结束时间" width="170">
|
||||
<template #default="{ row }">
|
||||
{{ formatTime(row.finished_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusType(row.status)" size="small">
|
||||
{{ row.status }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="响应码" width="80">
|
||||
<template #default="{ row }">
|
||||
{{ row.response_code || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="错误信息">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.error_message" style="color: #f56c6c">{{ row.error_message }}</span>
|
||||
<span v-else style="color: #67c23a">成功</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination-wrapper">
|
||||
<el-pagination
|
||||
v-model:current-page="logsQuery.page"
|
||||
:page-size="logsQuery.size"
|
||||
:total="logsTotal"
|
||||
layout="total, prev, pager, next"
|
||||
@current-change="handleLogsPageChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="logsDialogVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-tip {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.tenant-tags {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
padding: 12px 16px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.tag-label {
|
||||
color: #606266;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.tenant-tag {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.tenant-tag:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.clear-tag {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.pagination-wrapper {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.time-points-editor {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.time-points-list {
|
||||
min-height: 32px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.time-points-add {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.form-tip {
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user