Files
000-platform/frontend/src/views/scheduled-tasks/index.vue
Admin 70fc358d72
All checks were successful
continuous-integration/drone/push Build is passing
fix: 更新脚本示例,说明模块已内置无需 import
2026-01-28 17:27:43 +08:00

982 lines
30 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, reactive, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import api from '@/api'
const loading = ref(false)
const tableData = ref([])
const total = ref(0)
const query = reactive({
page: 1,
size: 20,
tenant_id: '',
status: ''
})
// 租户列表
const tenants = ref([])
// 通知渠道和企微应用
const notifyChannels = ref([])
const wecomApps = ref([])
// 对话框
const dialogVisible = ref(false)
const dialogTitle = ref('')
const editingId = ref(null)
const formRef = ref(null)
const form = reactive({
tenant_id: '',
task_name: '',
task_type: 'script',
schedule_type: 'simple',
time_points: ['09:00'],
cron_expression: '',
webhook_url: '',
webhook_method: 'POST',
webhook_headers: {},
script_content: `# 示例脚本
# 注意:不需要 import以下模块已内置可直接使用
# 内置函数: log, print, ai, dingtalk, wecom, http_get, http_post, db_query
# 内置函数: get_var, set_var, del_var, get_param, get_params
# 内置函数: get_tenants, get_tenant_config, get_all_tenant_configs, get_secret
# 内置变量: task_id, tenant_id, trace_id
# 内置模块: json, re, math, random, hashlib, base64, datetime, date, timedelta
log('任务开始执行')
# 获取当前时间datetime 已内置,无需 import
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
log(f'执行时间: {now}')
# 获取参数
prompt = get_param('prompt', '默认提示词')
log(f'参数: {prompt}')
# 调用 AI 生成内容
content = ai(prompt, system='你是一个助手')
log(f'生成内容: {content[:50]}...')
# 设置返回值(会自动发送到配置的通知渠道)
result = {
'content': content,
'title': '每日推送'
}
log('任务执行完成')
`,
script_timeout: 300,
input_params: {},
retry_count: 0,
retry_interval: 60,
alert_on_failure: false,
alert_webhook: '',
notify_channels: [],
notify_wecom_app_id: null
})
const rules = {
task_name: [{ required: true, message: '请输入任务名称', trigger: 'blur' }]
}
// 日志对话框
const logsDialogVisible = ref(false)
const logsLoading = ref(false)
const logsData = ref([])
const logsTaskId = ref(null)
const logsQuery = reactive({ page: 1, size: 10 })
const logsTotal = ref(0)
// SDK 文档对话框
const sdkDialogVisible = ref(false)
const sdkDocs = ref({ functions: [], variables: [], libraries: [] })
// 测试脚本对话框
const testDialogVisible = ref(false)
const testLoading = ref(false)
const testResult = ref(null)
const testParams = ref('{}')
// 参数编辑
const paramsDialogVisible = ref(false)
const paramsJson = ref('{}')
// 计算属性
const timePointsStr = computed({
get: () => form.time_points.join(', '),
set: (val) => {
form.time_points = val.split(',').map(s => s.trim()).filter(s => s)
}
})
async function fetchList() {
loading.value = true
try {
const params = { ...query }
if (!params.tenant_id) delete params.tenant_id
if (params.status === '') delete params.status
const res = await api.get('/api/scheduled-tasks', { params })
tableData.value = res.data.items || []
total.value = res.data.total || 0
} catch (e) {
console.error(e)
} finally {
loading.value = false
}
}
async function fetchTenants() {
try {
const res = await api.get('/api/tenants', { params: { size: 1000 } })
tenants.value = res.data.items || []
} catch (e) {
console.error(e)
}
}
async function fetchNotifyChannels(tenantId) {
try {
const params = tenantId ? { tenant_id: tenantId } : {}
const res = await api.get('/api/notification-channels', { params })
notifyChannels.value = res.data.items || []
} catch (e) {
console.error(e)
}
}
async function fetchWecomApps(tenantId) {
try {
const params = tenantId ? { tenant_id: tenantId } : {}
const res = await api.get('/api/tenant-wechat-apps', { params })
wecomApps.value = res.data.items || []
} catch (e) {
console.error(e)
}
}
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_type: 'script',
schedule_type: 'simple',
time_points: ['09:00'],
cron_expression: '',
webhook_url: '',
webhook_method: 'POST',
webhook_headers: {},
script_content: `# 示例脚本(无需 import模块已内置
log('任务开始执行')
# 获取当前时间
now = datetime.now().strftime('%H:%M:%S')
log(f'执行时间: {now}')
# 获取参数
prompt = get_param('prompt', '默认提示词')
# 调用 AI 生成内容
content = ai(prompt, system='你是一个助手')
# 设置返回值(自动发送到通知渠道)
result = {'content': content, 'title': '推送通知'}
log('任务执行完成')
`,
script_timeout: 300,
input_params: {},
retry_count: 0,
retry_interval: 60,
alert_on_failure: false,
alert_webhook: '',
notify_channels: [],
notify_wecom_app_id: null
})
notifyChannels.value = []
wecomApps.value = []
dialogVisible.value = true
}
async function handleEdit(row) {
editingId.value = row.id
dialogTitle.value = '编辑任务'
try {
const res = await api.get(`/api/scheduled-tasks/${row.id}`)
const task = res.data
Object.assign(form, {
tenant_id: task.tenant_id || '',
task_name: task.task_name,
task_type: task.task_type,
schedule_type: task.schedule_type,
time_points: task.time_points || ['09:00'],
cron_expression: task.cron_expression || '',
webhook_url: task.webhook_url || '',
webhook_method: task.webhook_method || 'POST',
webhook_headers: task.webhook_headers || {},
script_content: task.script_content || '',
script_timeout: task.script_timeout || 300,
input_params: task.input_params || {},
retry_count: task.retry_count || 0,
retry_interval: task.retry_interval || 60,
alert_on_failure: task.alert_on_failure || false,
alert_webhook: task.alert_webhook || '',
notify_channels: task.notify_channels || [],
notify_wecom_app_id: task.notify_wecom_app_id || null
})
// 加载该租户的通知渠道和企微应用
if (task.tenant_id) {
await fetchNotifyChannels(task.tenant_id)
await fetchWecomApps(task.tenant_id)
}
dialogVisible.value = true
} catch (e) {
console.error(e)
}
}
async function handleSubmit() {
await formRef.value.validate()
const data = {
...form,
time_points: form.schedule_type === 'simple' ? form.time_points : null,
cron_expression: form.schedule_type === 'cron' ? form.cron_expression : null
}
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.status === 1 ? '已启用' : '已禁用')
fetchList()
} catch (e) {
// 错误已在拦截器处理
}
}
async function handleRunNow(row) {
try {
ElMessage.info('任务执行中...')
const res = await api.post(`/api/scheduled-tasks/${row.id}/run`)
if (res.data.success) {
ElMessage.success('执行成功')
} else {
ElMessage.error(`执行失败: ${res.data.error}`)
}
fetchList()
} 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()
}
async function handleShowSdkDocs() {
try {
const res = await api.get('/api/scheduled-tasks/sdk-docs')
sdkDocs.value = res.data
sdkDialogVisible.value = true
} catch (e) {
console.error(e)
}
}
async function handleTestScript() {
testLoading.value = true
testResult.value = null
try {
let params = {}
try {
params = JSON.parse(testParams.value)
} catch (e) {
ElMessage.error('参数 JSON 格式错误')
testLoading.value = false
return
}
const res = await api.post('/api/scheduled-tasks/test-script', {
script_content: form.script_content,
tenant_id: form.tenant_id || null,
params
})
testResult.value = res.data
} catch (e) {
testResult.value = { success: false, error: e.message }
} finally {
testLoading.value = false
}
}
function showTestDialog() {
testParams.value = JSON.stringify(form.input_params || {}, null, 2)
testResult.value = null
testDialogVisible.value = true
}
function showParamsDialog() {
paramsJson.value = JSON.stringify(form.input_params || {}, null, 2)
paramsDialogVisible.value = true
}
function saveParams() {
try {
form.input_params = JSON.parse(paramsJson.value)
paramsDialogVisible.value = false
ElMessage.success('参数已保存')
} catch (e) {
ElMessage.error('JSON 格式错误')
}
}
function getTenantName(tenantId) {
const tenant = tenants.value.find(t => t.code === tenantId)
return tenant ? tenant.name : tenantId
}
function formatSchedule(row) {
if (row.schedule_type === 'cron') {
return `CRON: ${row.cron_expression}`
}
return (row.time_points || []).join(', ')
}
onMounted(() => {
fetchList()
fetchTenants()
})
</script>
<template>
<div class="page-container">
<div class="page-header">
<div class="title">定时任务</div>
<div class="actions">
<el-button @click="handleShowSdkDocs">
<el-icon><Document /></el-icon>
SDK 文档
</el-button>
<el-button type="primary" @click="handleCreate">
<el-icon><Plus /></el-icon>
新建任务
</el-button>
</div>
</div>
<!-- 筛选 -->
<div class="filter-bar">
<el-select v-model="query.tenant_id" placeholder="全部租户" clearable style="width: 180px">
<el-option label="全局任务" value="" />
<el-option v-for="t in tenants" :key="t.code" :label="t.name" :value="t.code" />
</el-select>
<el-select v-model="query.status" placeholder="全部状态" clearable style="width: 120px">
<el-option label="启用" :value="1" />
<el-option label="禁用" :value="0" />
</el-select>
<el-button type="primary" @click="handleSearch">搜索</el-button>
</div>
<!-- 表格 -->
<el-table v-loading="loading" :data="tableData" style="width: 100%">
<el-table-column prop="id" label="ID" width="60" />
<el-table-column prop="task_name" label="任务名称" min-width="150" />
<el-table-column label="租户" width="120">
<template #default="{ row }">
<span v-if="row.tenant_id">{{ getTenantName(row.tenant_id) }}</span>
<el-tag v-else size="small" type="info">全局</el-tag>
</template>
</el-table-column>
<el-table-column label="类型" width="80">
<template #default="{ row }">
<el-tag :type="row.task_type === 'script' ? 'success' : 'warning'" size="small">
{{ row.task_type === 'script' ? '脚本' : 'Webhook' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="执行时间" min-width="150">
<template #default="{ row }">
<span style="font-family: monospace; font-size: 12px">{{ formatSchedule(row) }}</span>
</template>
</el-table-column>
<el-table-column label="状态" width="80">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
{{ row.status === 1 ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="最后执行" width="180">
<template #default="{ row }">
<div v-if="row.last_run_at">
<div style="font-size: 12px">{{ row.last_run_at }}</div>
<el-tag :type="row.last_run_status === 'success' ? 'success' : 'danger'" size="small">
{{ row.last_run_status === 'success' ? '成功' : '失败' }}
</el-tag>
</div>
<span v-else style="color: #999">-</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-button>
<el-button type="info" link size="small" @click="handleViewLogs(row)">日志</el-button>
<el-button :type="row.status === 1 ? 'warning' : 'success'" link size="small" @click="handleToggle(row)">
{{ row.status === 1 ? '禁用' : '启用' }}
</el-button>
<el-button type="danger" link size="small" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div style="margin-top: 20px; display: flex; justify-content: flex-end">
<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="900px" top="5vh">
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="任务名称" prop="task_name">
<el-input v-model="form.task_name" placeholder="如: 每日销售播报" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="所属租户">
<el-select v-model="form.tenant_id" placeholder="全局任务" clearable style="width: 100%">
<el-option label="全局任务" value="" />
<el-option v-for="t in tenants" :key="t.code" :label="t.name" :value="t.code" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="任务类型">
<el-radio-group v-model="form.task_type">
<el-radio value="script">Python 脚本</el-radio>
<el-radio value="webhook">Webhook</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<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-col>
</el-row>
<!-- 调度配置 -->
<el-form-item v-if="form.schedule_type === 'simple'" label="执行时间">
<el-input v-model="timePointsStr" placeholder="09:00, 12:00, 18:00" />
<div class="form-tip">多个时间点用逗号分隔格式: HH:MM</div>
</el-form-item>
<el-form-item v-else label="CRON表达式">
<el-input v-model="form.cron_expression" placeholder="0 9 * * *" />
<div class="form-tip">格式: : 0 9 * * * (每天9点)</div>
</el-form-item>
<!-- Webhook 配置 -->
<template v-if="form.task_type === 'webhook'">
<el-form-item label="Webhook URL">
<el-input v-model="form.webhook_url" placeholder="https://example.com/webhook" />
</el-form-item>
<el-form-item label="请求方法">
<el-select v-model="form.webhook_method" style="width: 120px">
<el-option value="GET" />
<el-option value="POST" />
</el-select>
</el-form-item>
</template>
<!-- 脚本配置 -->
<template v-if="form.task_type === 'script'">
<el-form-item label="脚本内容">
<div class="script-editor-container">
<div class="script-toolbar">
<el-button size="small" @click="handleShowSdkDocs">SDK 文档</el-button>
<el-button size="small" type="primary" @click="showTestDialog">测试运行</el-button>
</div>
<el-input
v-model="form.script_content"
type="textarea"
:rows="15"
placeholder="Python 脚本"
class="script-textarea"
/>
</div>
</el-form-item>
<el-form-item label="超时时间">
<el-input-number v-model="form.script_timeout" :min="10" :max="3600" />
<span style="margin-left: 8px; color: #999"></span>
</el-form-item>
</template>
<!-- 任务参数 -->
<el-form-item label="任务参数">
<div class="params-preview">
<code>{{ JSON.stringify(form.input_params) }}</code>
<el-button size="small" @click="showParamsDialog">编辑参数</el-button>
</div>
<div class="form-tip">脚本中使用 get_param('key') 获取</div>
</el-form-item>
<!-- 高级配置 -->
<el-divider>高级配置</el-divider>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="失败重试">
<el-input-number v-model="form.retry_count" :min="0" :max="10" />
<span style="margin-left: 8px; color: #999"></span>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="重试间隔">
<el-input-number v-model="form.retry_interval" :min="10" :max="3600" />
<span style="margin-left: 8px; color: #999"></span>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="失败告警">
<el-switch v-model="form.alert_on_failure" />
</el-form-item>
<el-form-item v-if="form.alert_on_failure" label="告警地址">
<el-input v-model="form.alert_webhook" placeholder="钉钉/企微机器人 Webhook 地址" />
</el-form-item>
<!-- 通知配置 -->
<el-divider>通知配置脚本设置 result 变量后自动发送</el-divider>
<el-form-item label="通知渠道">
<div v-if="!form.tenant_id" class="form-tip" style="color: #e6a23c">
请先选择租户再配置通知渠道
</div>
<template v-else>
<el-select
v-model="form.notify_channels"
multiple
placeholder="选择通知渠道(可多选)"
style="width: 100%"
@focus="fetchNotifyChannels(form.tenant_id)"
>
<el-option
v-for="ch in notifyChannels"
:key="ch.id"
:label="`${ch.channel_name} (${ch.channel_type === 'dingtalk_bot' ? '钉钉' : '企微'})`"
:value="ch.id"
/>
</el-select>
<div class="form-tip">
脚本中设置 <code>result = {'content': '内容', 'title': '标题'}</code> 变量执行后自动发送
</div>
</template>
</el-form-item>
<el-form-item label="企微应用">
<div v-if="!form.tenant_id" class="form-tip" style="color: #e6a23c">
请先选择租户
</div>
<template v-else>
<el-select
v-model="form.notify_wecom_app_id"
placeholder="选择企微应用(可选)"
clearable
style="width: 100%"
@focus="fetchWecomApps(form.tenant_id)"
>
<el-option
v-for="app in wecomApps"
:key="app.id"
:label="app.app_name"
:value="app.id"
/>
</el-select>
<div class="form-tip">发送到企微应用的全员消息</div>
</template>
</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="900px">
<el-table v-loading="logsLoading" :data="logsData" max-height="400">
<el-table-column label="执行时间" width="180">
<template #default="{ row }">{{ row.started_at }}</template>
</el-table-column>
<el-table-column label="状态" width="80">
<template #default="{ row }">
<el-tag :type="row.status === 'success' ? 'success' : row.status === 'running' ? 'warning' : 'danger'" size="small">
{{ row.status === 'success' ? '成功' : row.status === 'running' ? '运行中' : '失败' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="耗时" width="100">
<template #default="{ row }">
<span v-if="row.duration_ms">{{ row.duration_ms }}ms</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="输出" min-width="200">
<template #default="{ row }">
<el-popover v-if="row.output" trigger="hover" width="400">
<template #reference>
<span class="output-preview">{{ row.output.slice(0, 50) }}...</span>
</template>
<pre class="output-full">{{ row.output }}</pre>
</el-popover>
<span v-else style="color: #999">-</span>
</template>
</el-table-column>
<el-table-column label="错误" min-width="200">
<template #default="{ row }">
<el-popover v-if="row.error" trigger="hover" width="400">
<template #reference>
<span class="error-preview">{{ row.error.slice(0, 50) }}...</span>
</template>
<pre class="error-full">{{ row.error }}</pre>
</el-popover>
<span v-else style="color: #999">-</span>
</template>
</el-table-column>
</el-table>
<div style="margin-top: 16px; display: flex; justify-content: flex-end">
<el-pagination
v-model:current-page="logsQuery.page"
:page-size="logsQuery.size"
:total="logsTotal"
layout="total, prev, pager, next"
@current-change="handleLogsPageChange"
/>
</div>
</el-dialog>
<!-- SDK 文档对话框 -->
<el-dialog v-model="sdkDialogVisible" title="SDK 文档" width="800px">
<el-tabs>
<el-tab-pane label="内置函数">
<div class="sdk-docs">
<div v-for="fn in sdkDocs.functions" :key="fn.name" class="sdk-item">
<div class="sdk-name">{{ fn.name }}</div>
<code class="sdk-signature">{{ fn.signature }}</code>
<div class="sdk-desc">{{ fn.description }}</div>
<code class="sdk-example">{{ fn.example }}</code>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="上下文变量">
<div class="sdk-docs">
<div v-for="v in sdkDocs.variables" :key="v.name" class="sdk-item">
<div class="sdk-name">{{ v.name }}</div>
<div class="sdk-desc">{{ v.description }}</div>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="可用库">
<div class="sdk-docs">
<div v-for="lib in sdkDocs.libraries" :key="lib.name" class="sdk-item">
<div class="sdk-name">{{ lib.name }}</div>
<div class="sdk-desc">{{ lib.description }}</div>
</div>
</div>
</el-tab-pane>
</el-tabs>
</el-dialog>
<!-- 测试脚本对话框 -->
<el-dialog v-model="testDialogVisible" title="测试脚本" width="700px">
<el-form label-width="80px">
<el-form-item label="测试参数">
<el-input v-model="testParams" type="textarea" :rows="5" placeholder='{"key": "value"}' />
</el-form-item>
</el-form>
<div style="margin-bottom: 16px">
<el-button type="primary" :loading="testLoading" @click="handleTestScript">运行测试</el-button>
</div>
<div v-if="testResult" class="test-result">
<el-alert :type="testResult.success ? 'success' : 'error'" :closable="false">
{{ testResult.success ? '执行成功' : '执行失败' }}
<span v-if="testResult.duration_ms"> ({{ testResult.duration_ms }}ms)</span>
</el-alert>
<div v-if="testResult.output" class="result-section">
<div class="result-label">输出:</div>
<pre class="result-content">{{ testResult.output }}</pre>
</div>
<div v-if="testResult.error" class="result-section">
<div class="result-label">错误:</div>
<pre class="result-content error">{{ testResult.error }}</pre>
</div>
</div>
</el-dialog>
<!-- 参数编辑对话框 -->
<el-dialog v-model="paramsDialogVisible" title="编辑任务参数" width="600px">
<el-input v-model="paramsJson" type="textarea" :rows="15" placeholder='{"prompt": "...", "history_days": 30}' />
<div class="form-tip" style="margin-top: 8px">
JSON 格式脚本中使用 get_param('key') 获取
</div>
<template #footer>
<el-button @click="paramsDialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveParams">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<style scoped>
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.page-header .title {
font-size: 20px;
font-weight: 600;
}
.page-header .actions {
display: flex;
gap: 12px;
}
.filter-bar {
display: flex;
gap: 12px;
margin-bottom: 20px;
}
.form-tip {
color: #909399;
font-size: 12px;
margin-top: 4px;
}
.form-tip code {
background: #f5f7fa;
padding: 2px 6px;
border-radius: 4px;
font-size: 11px;
}
.script-editor-container {
width: 100%;
}
.script-toolbar {
display: flex;
gap: 8px;
margin-bottom: 8px;
}
.script-textarea :deep(textarea) {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 13px;
line-height: 1.5;
}
.params-preview {
display: flex;
align-items: center;
gap: 12px;
}
.params-preview code {
flex: 1;
padding: 8px 12px;
background: #f5f7fa;
border-radius: 4px;
font-size: 12px;
max-width: 400px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.output-preview, .error-preview {
cursor: pointer;
font-size: 12px;
}
.error-preview {
color: #f56c6c;
}
.output-full, .error-full {
max-height: 300px;
overflow: auto;
font-size: 12px;
white-space: pre-wrap;
word-break: break-all;
}
.error-full {
color: #f56c6c;
}
.sdk-docs {
max-height: 500px;
overflow-y: auto;
}
.sdk-item {
padding: 12px;
border-bottom: 1px solid #eee;
}
.sdk-item:last-child {
border-bottom: none;
}
.sdk-name {
font-weight: 600;
font-size: 14px;
color: #409eff;
}
.sdk-signature {
display: block;
margin: 4px 0;
padding: 4px 8px;
background: #f5f7fa;
border-radius: 4px;
font-size: 12px;
}
.sdk-desc {
color: #606266;
font-size: 13px;
margin: 4px 0;
}
.sdk-example {
display: block;
padding: 4px 8px;
background: #fef0f0;
border-radius: 4px;
font-size: 12px;
color: #f56c6c;
}
.test-result {
margin-top: 16px;
}
.result-section {
margin-top: 12px;
}
.result-label {
font-weight: 600;
margin-bottom: 4px;
color: #606266;
}
.result-content {
padding: 12px;
background: #f5f7fa;
border-radius: 4px;
max-height: 200px;
overflow: auto;
font-size: 12px;
white-space: pre-wrap;
word-break: break-all;
}
.result-content.error {
background: #fef0f0;
color: #f56c6c;
}
</style>