refactor(i18n): U23 Task 模块 i18n 治理 — 4 文件 phantom key + hash key + 硬编码中文清零
- CronJobManager/CronJobFormDialog/ScriptManager/RemoteExecPage
从 import i18n 全局调用改为 useTranslation() hook
- 42 处 phantom key (i18n.t('Chinese text') → 语义化键)
- 28 个孤儿 hash key 清理 (cronjobformdial.s*/remoteexecpaget.s*)
- 50+ 处硬编码 JSX 中文 → t() 调用
- 新增 task.cronJob.*/task.scriptList.*/task.remoteExec.* 子命名空间
- zh/en 6825→6886 keys 对齐
- tsc -b=0, npm build✓ (17.46s), ESLint=0/45/0, go build=0
- 17/17 集成回归测试通过
This commit is contained in:
1 parent
a8f6fd190b
commit
de6f9454fc
7 files changed
+361
-229
No files matched your search
@@ -514,6 +514,7 @@ services/api.ts (1324L)
|
||||
| U17 | K8s 残余组件 i18n 补全(NodeDetailPage 16 处硬编码中文清零 + 8 个 hash 键替换为语义化 k8s.nodeDetail.* 键 + DeploymentWorkload 12 处 toast/confirm 硬编码中文清零,zh/en 7564→7597 +33 键对齐) | P3 | **done** |
|
||||
| U19 | i18n hash 键清理(删除 866 个死 hash 键 + 重命名 21 个高频 live hash 键为语义化键名,zh/en 7597→6731 keys 对齐,文件 328KB→276KB (-16%),tsc -b=0,npm build✓,ESLint=0/66/0,17/17 测试通过) | P3 | **done** |
|
||||
| U20 | ESLint static-components 清零 + CredDetailDialog 内联组件修复 + i18n 批量迁移损坏修复(CredDetailDialog.tsx `Field` 内联组件提取为模块级 `CredField` — 23 处 react-hooks/static-components 警告清零;修复 8 处被批量正则损坏的 `i18n.t()` 调用(`i18n.t('>名称:</span>')` 等 → 语义化键);6 处硬编码中文清零;新增 `deploy.cred.sensitiveWarn` 键;ESLint warnings 66→43 (-35%),tsc -b=0,npm build✓ (17.29s),17/17 测试通过) | P3 | **done** |
|
||||
| U23 | Task 模块 i18n 治理(CronJobManager + CronJobFormDialog + ScriptManager + RemoteExecPage 4 文件 — `import i18n` 全局调用 → `useTranslation()` hook;42 处 phantom key (`i18n.t('Chinese text')` → 语义化 `task.cronJob.*`/`task.scriptList.*`/`task.remoteExec.*` 键);28 个孤儿 hash 键清理 (cronjobformdial.s*/remoteexecpaget.s*/systemtaskspanel.s*);50+ 处硬编码 JSX 中文 → t() 调用;zh/en 6825→6886 +61 键对齐,tsc -b=0,npm build✓ (17.46s),ESLint=0/45/0,go build=0,17/17 测试通过) | P3 | **done** |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
@@ -22,15 +23,6 @@ import { Badge } from '@/components/ui/badge';
|
||||
import { toast } from 'sonner';
|
||||
import { cronJobApi, scriptApi } from '@/api';
|
||||
import type { JsonData } from '@/types/common';
|
||||
import i18n from '@/i18n';
|
||||
|
||||
// 定时任务类型定义 // Logan.Li
|
||||
const jobTypes = [
|
||||
{ value: 'shell', label: 'Shell', desc: i18n.t('关联脚本库中的 Shell 脚本') },
|
||||
{ value: 'python', label: 'Python', desc: i18n.t('关联脚本库中的 Python 脚本') },
|
||||
{ value: 'powershell', label: 'PowerShell', desc: i18n.t('关联脚本库中的 PowerShell 脚本') },
|
||||
{ value: 'command', label: i18n.t('自定义命令'), desc: i18n.t('手动输入命令或脚本路径') },
|
||||
];
|
||||
|
||||
const scriptJobTypes = ['shell', 'python', 'powershell'];
|
||||
|
||||
@@ -51,6 +43,7 @@ export function CronJobFormDialog({
|
||||
editJob: JsonData | null;
|
||||
onSuccess: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [form, setForm] = useState({
|
||||
name: '',
|
||||
description: '',
|
||||
@@ -63,10 +56,15 @@ export function CronJobFormDialog({
|
||||
const [selectedScript, setSelectedScript] = useState<JsonData>(null);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
// 弹窗打开时:加载脚本库 + 初始化表单 // Logan.Li
|
||||
const jobTypes = useMemo(() => [
|
||||
{ value: 'shell', label: 'Shell', desc: t('task.cronJob.typeShell') },
|
||||
{ value: 'python', label: 'Python', desc: t('task.cronJob.typePython') },
|
||||
{ value: 'powershell', label: 'PowerShell', desc: t('task.cronJob.typePowershell') },
|
||||
{ value: 'command', label: t('task.cronJob.typeCommandLabel'), desc: t('task.cronJob.typeCommandDesc') },
|
||||
], [t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
// 拉取脚本库
|
||||
scriptApi.getScripts({ page_size: 200 }).then((res: JsonData) => {
|
||||
setScripts(res?.list || []);
|
||||
}).catch(() => setScripts([]));
|
||||
@@ -80,7 +78,6 @@ export function CronJobFormDialog({
|
||||
content: editJob.content || '',
|
||||
script_id: editJob.script_id || null,
|
||||
});
|
||||
// 如果关联了脚本,拉取脚本详情
|
||||
if (editJob.script_id) {
|
||||
scriptApi.getScript(editJob.script_id).then((res: JsonData) => {
|
||||
setSelectedScript(res);
|
||||
@@ -94,12 +91,10 @@ export function CronJobFormDialog({
|
||||
}
|
||||
}, [open, editJob]);
|
||||
|
||||
// 按类型过滤脚本库 // Logan.Li
|
||||
const filteredScripts = scriptJobTypes.includes(form.type)
|
||||
? scripts.filter(s => s.type === form.type)
|
||||
: [];
|
||||
|
||||
// 选中脚本时更新关联
|
||||
const handleSelectScript = (scriptId: string) => {
|
||||
const sc = scripts.find(s => String(s.id) === scriptId);
|
||||
if (sc) {
|
||||
@@ -108,7 +103,6 @@ export function CronJobFormDialog({
|
||||
}
|
||||
};
|
||||
|
||||
// 切换类型时重置脚本关联
|
||||
const handleTypeChange = (type: string) => {
|
||||
setForm(prev => ({
|
||||
...prev,
|
||||
@@ -121,12 +115,11 @@ export function CronJobFormDialog({
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!form.name || !form.cron_expr) {
|
||||
toast.error(i18n.t('请填写任务名称和 Cron 表达式'));
|
||||
toast.error(t('task.cronJob.form.toastNameRequired'));
|
||||
return;
|
||||
}
|
||||
// 脚本类型必须选关联脚本 // Logan.Li
|
||||
if (scriptJobTypes.includes(form.type) && !form.script_id) {
|
||||
toast.error(i18n.t('请填写任务名称和 Cron 表达式'));
|
||||
toast.error(t('task.cronJob.form.toastNameRequired'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -141,16 +134,16 @@ export function CronJobFormDialog({
|
||||
};
|
||||
if (editJob) {
|
||||
await cronJobApi.updateJob(editJob.id, payload);
|
||||
toast.success(i18n.t('任务更新成功'));
|
||||
toast.success(t('task.cronJob.form.toastSaved'));
|
||||
} else {
|
||||
await cronJobApi.createJob(payload);
|
||||
toast.success(i18n.t('任务更新成功'));
|
||||
toast.success(t('task.cronJob.form.toastSaved'));
|
||||
}
|
||||
onOpenChange(false);
|
||||
onSuccess();
|
||||
} catch (error) {
|
||||
console.error('Failed to save cron job:', error);
|
||||
toast.error(editJob ? i18n.t('kubernetes.deployment.imageUpdateFailed') : i18n.t('monitor.dashboard.custom.toastCreateFailed'));
|
||||
toast.error(editJob ? t('kubernetes.deployment.imageUpdateFailed') : t('monitor.dashboard.custom.toastCreateFailed'));
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
@@ -160,36 +153,33 @@ export function CronJobFormDialog({
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl bg-[var(--dg-bg-primary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editJob ? i18n.t('automation.editCronJob') : i18n.t('编辑定时任务')}</DialogTitle>
|
||||
<DialogDescription className="text-[var(--dg-text-secondary)]"> {i18n.t('task.cronjobformdial.s2e10ee70')} </DialogDescription>
|
||||
<DialogTitle>{editJob ? t('task.cronJob.form.titleEdit') : t('task.cronJob.form.titleCreate')}</DialogTitle>
|
||||
<DialogDescription className="text-[var(--dg-text-secondary)]"> {t('task.cronJob.form.desc')} </DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4 max-h-[60vh] overflow-y-auto pr-1">
|
||||
{/* 任务名称 */}
|
||||
<div>
|
||||
<Label className="text-[var(--dg-text-primary)]"> {i18n.t('automation.cronJobName')} </Label>
|
||||
<Label className="text-[var(--dg-text-primary)]"> {t('task.cronJob.form.labelName')} </Label>
|
||||
<Input
|
||||
value={form.name}
|
||||
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
||||
placeholder={i18n.t('例如: 数据库备份')}
|
||||
placeholder={t('task.cronJob.form.placeholderName')}
|
||||
className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] mt-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 描述 */}
|
||||
<div>
|
||||
<Label className="text-[var(--dg-text-primary)]"> {i18n.t('aiops.llmModelDesc')} </Label>
|
||||
<Label className="text-[var(--dg-text-primary)]"> {t('task.cronJob.form.labelDesc')} </Label>
|
||||
<Textarea
|
||||
value={form.description}
|
||||
onChange={(e) => setForm({ ...form, description: e.target.value })}
|
||||
placeholder={i18n.t('例如: 数据库备份')}
|
||||
placeholder={t('task.cronJob.form.placeholderName')}
|
||||
rows={2}
|
||||
className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] mt-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Cron 表达式 */}
|
||||
<div>
|
||||
<Label className="text-[var(--dg-text-primary)]">Cron 表达式</Label>
|
||||
<Label className="text-[var(--dg-text-primary)]">{t('task.cronJob.form.labelCronExpr')}</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={form.cron_expr}
|
||||
@@ -199,58 +189,56 @@ export function CronJobFormDialog({
|
||||
/>
|
||||
<Select onValueChange={(v) => setForm({ ...form, cron_expr: v })}>
|
||||
<SelectTrigger className="w-48 bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] mt-1">
|
||||
<SelectValue placeholder={i18n.t('快速选择')} />
|
||||
<SelectValue placeholder={t('task.cronJob.form.quickSelect')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<SelectItem value="*/5 * * * *" className="text-[var(--dg-text-primary)]"> {i18n.t('task.cronJob.cron.every5min')} </SelectItem>
|
||||
<SelectItem value="0 * * * *" className="text-[var(--dg-text-primary)]"> {i18n.t('task.cronJob.cron.everyHour')} </SelectItem>
|
||||
<SelectItem value="0 2 * * *" className="text-[var(--dg-text-primary)]"> {i18n.t('task.cronJob.cron.daily2')} </SelectItem>
|
||||
<SelectItem value="0 3 * * 0" className="text-[var(--dg-text-primary)]"> {i18n.t('task.cronJob.cron.weeklySun3')} </SelectItem>
|
||||
<SelectItem value="0 9 * * 1" className="text-[var(--dg-text-primary)]"> {i18n.t('task.cronJob.cron.weeklyMon9')} </SelectItem>
|
||||
<SelectItem value="*/5 * * * *" className="text-[var(--dg-text-primary)]"> {t('task.cronJob.cron.every5min')} </SelectItem>
|
||||
<SelectItem value="0 * * * *" className="text-[var(--dg-text-primary)]"> {t('task.cronJob.cron.everyHour')} </SelectItem>
|
||||
<SelectItem value="0 2 * * *" className="text-[var(--dg-text-primary)]"> {t('task.cronJob.cron.daily2')} </SelectItem>
|
||||
<SelectItem value="0 3 * * 0" className="text-[var(--dg-text-primary)]"> {t('task.cronJob.cron.weeklySun3')} </SelectItem>
|
||||
<SelectItem value="0 9 * * 1" className="text-[var(--dg-text-primary)]"> {t('task.cronJob.cron.weeklyMon9')} </SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<p className="text-xs text-[var(--dg-text-muted)] mt-1"> {i18n.t('task.cronjobformdial.s3b56b1f9')} </p>
|
||||
<p className="text-xs text-[var(--dg-text-muted)] mt-1"> {t('task.cronJob.form.cronHint')} </p>
|
||||
</div>
|
||||
|
||||
{/* 执行类型 */}
|
||||
<div>
|
||||
<Label className="text-[var(--dg-text-primary)]"> {i18n.t('task.cronjobformdial.s15d864c1')} </Label>
|
||||
<Label className="text-[var(--dg-text-primary)]"> {t('task.cronJob.form.labelJobType')} </Label>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2 mt-1">
|
||||
{jobTypes.map(t => (
|
||||
{jobTypes.map(jt => (
|
||||
<button
|
||||
key={t.value}
|
||||
key={jt.value}
|
||||
type="button"
|
||||
onClick={() => handleTypeChange(t.value)}
|
||||
onClick={() => handleTypeChange(jt.value)}
|
||||
className={`px-3 py-2 rounded-md border text-sm transition-colors text-left ${
|
||||
form.type === t.value
|
||||
form.type === jt.value
|
||||
? 'border-[var(--dg-blue)] bg-[var(--dg-blue)]/10 text-[var(--dg-blue)]'
|
||||
: 'border-[var(--dg-border)] text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-hover)]'
|
||||
}`}
|
||||
>
|
||||
<div className="font-medium">{t.label}</div>
|
||||
<div className="font-medium">{jt.label}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-xs text-[var(--dg-text-muted)] mt-1">
|
||||
{jobTypes.find(t => t.value === form.type)?.desc}
|
||||
{jobTypes.find(jt => jt.value === form.type)?.desc}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 脚本选择 (shell/python/powershell) 或 命令输入 (command) */}
|
||||
{scriptJobTypes.includes(form.type) ? (
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Label className="text-[var(--dg-text-primary)]">
|
||||
选择脚本 <span className="dg-text-red">*</span>
|
||||
{t('task.cronJob.form.labelScript')} <span className="dg-text-red">*</span>
|
||||
</Label>
|
||||
{filteredScripts.length === 0 ? (
|
||||
<div className="mt-1 px-3 py-4 rounded-md border border-dashed border-[var(--dg-border)] text-center">
|
||||
<p className="text-sm text-[var(--dg-text-muted)]">
|
||||
脚本库中没有 {scriptTypeLabel(form.type)} 脚本
|
||||
{t('task.cronJob.form.noScriptType', { type: scriptTypeLabel(form.type) })}
|
||||
</p>
|
||||
<p className="text-xs text-[var(--dg-text-muted)] mt-1">
|
||||
请先到「脚本库」创建 {scriptTypeLabel(form.type)} 脚本
|
||||
{t('task.cronJob.form.noScriptHint', { type: scriptTypeLabel(form.type) })}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
@@ -259,7 +247,7 @@ export function CronJobFormDialog({
|
||||
onValueChange={handleSelectScript}
|
||||
>
|
||||
<SelectTrigger className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] mt-1">
|
||||
<SelectValue placeholder={i18n.t('选择脚本库中的脚本...')} />
|
||||
<SelectValue placeholder={t('task.cronJob.form.selectScriptPlaceholder')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
{filteredScripts.map(sc => (
|
||||
@@ -272,7 +260,6 @@ export function CronJobFormDialog({
|
||||
</Select>
|
||||
)}
|
||||
</div>
|
||||
{/* 脚本预览 */}
|
||||
{selectedScript && (
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
@@ -280,22 +267,22 @@ export function CronJobFormDialog({
|
||||
{scriptTypeLabel(selectedScript.type || form.type)}
|
||||
</Badge>
|
||||
<span className="text-xs text-[var(--dg-text-muted)]">
|
||||
{(selectedScript.content || '').split('\n').length} 行
|
||||
{t('task.cronJob.form.scriptLines', { count: (selectedScript.content || '').split('\n').length })}
|
||||
</span>
|
||||
</div>
|
||||
<pre className="max-h-48 overflow-auto rounded-md bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] p-3 text-xs font-mono text-[var(--dg-text-secondary)] whitespace-pre-wrap break-all">
|
||||
{selectedScript.content || i18n.t('(空脚本)')}
|
||||
{selectedScript.content || t('task.cronJob.form.emptyScript')}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<Label className="text-[var(--dg-text-primary)]"> {i18n.t('aiops.assistantCommand')} </Label>
|
||||
<Label className="text-[var(--dg-text-primary)]"> {t('task.cronJob.form.labelCommand')} </Label>
|
||||
<Textarea
|
||||
value={form.content}
|
||||
onChange={(e) => setForm({ ...form, content: e.target.value })}
|
||||
placeholder="/scripts/backup.sh arg1 arg2"
|
||||
placeholder={t('task.cronJob.form.placeholderCommand')}
|
||||
rows={4}
|
||||
className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] mt-1 font-mono"
|
||||
/>
|
||||
@@ -304,14 +291,14 @@ export function CronJobFormDialog({
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)} className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]">
|
||||
取消
|
||||
{t('task.cronJob.form.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
className="bg-blue-600 hover:bg-blue-700"
|
||||
onClick={handleSave}
|
||||
disabled={submitting}
|
||||
>
|
||||
{submitting ? i18n.t('system.savingDot') : editJob ? i18n.t('cloud.saveChanges') : i18n.t('保存中...')}
|
||||
{submitting ? t('system.savingDot') : editJob ? t('cloud.saveChanges') : t('task.script.save')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CronJobStatsPanel } from './CronJobStatsPanel';
|
||||
import { CronJobTable } from './CronJobTable';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -24,13 +25,13 @@ import { PageShell } from '@/components/layout/PageShell';
|
||||
import { ContentPanel } from '@/components/layout/ContentPanel';
|
||||
import { PageEmpty } from '@/components/layout/PageEmpty';
|
||||
import type { CronJobRecord } from '@/types';
|
||||
import i18n from '@/i18n';
|
||||
|
||||
type StatusFilter = 'all' | 'enabled' | 'disabled';
|
||||
type HealthFilter = 'all' | 'healthy' | 'failed';
|
||||
type TypeFilter = 'all' | 'shell' | 'python' | 'powershell' | 'command';
|
||||
|
||||
export function CronJobManager() {
|
||||
const { t } = useTranslation();
|
||||
const { hasAnyPermission, isAdmin } = usePermission();
|
||||
const canWrite = isAdmin || hasAnyPermission(['auto:script', 'auto:cron', 'auto:ansible']);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
@@ -55,9 +56,10 @@ export function CronJobManager() {
|
||||
|
||||
const _confirm = useConfirm();
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const pageTabs = useMemo(() => [
|
||||
{ id: 'user', label: i18n.t('task.cronJobStats.userJobs'), badge: jobs.length || undefined },
|
||||
{ id: 'system', label: i18n.t('automation.systemTask'), badge: systemTaskCount || undefined },
|
||||
{ id: 'user', label: t('task.cronJobStats.userJobs'), badge: jobs.length || undefined },
|
||||
{ id: 'system', label: t('automation.systemTask'), badge: systemTaskCount || undefined },
|
||||
], [jobs.length, systemTaskCount]);
|
||||
|
||||
const openCreate = () => {
|
||||
@@ -81,11 +83,12 @@ export function CronJobManager() {
|
||||
setSystemTaskCount(sysRes.list?.length ?? 0);
|
||||
} catch (error) {
|
||||
console.error('Failed to load cron jobs:', error);
|
||||
toast.error(i18n.t('kubernetes.msg.loadFailed'));
|
||||
toast.error(t('kubernetes.msg.loadFailed'));
|
||||
} finally {
|
||||
setRefreshing(false);
|
||||
setInitialLoading(false);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -121,11 +124,11 @@ export function CronJobManager() {
|
||||
const handleToggleJob = async (id: number) => {
|
||||
try {
|
||||
await cronJobApi.toggleJob(id);
|
||||
toast.success(i18n.t('状态已切换'));
|
||||
toast.success(t('task.cronJob.list.toastToggled'));
|
||||
refreshData();
|
||||
} catch (error) {
|
||||
console.error('Failed to toggle job:', error);
|
||||
toast.error(i18n.t('cloud.operationFailed'));
|
||||
toast.error(t('cloud.operationFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -135,14 +138,14 @@ export function CronJobManager() {
|
||||
const result = await cronJobApi.executeJob(id);
|
||||
const status = result?.status || 'success';
|
||||
if (status === 'success') {
|
||||
toast.success(i18n.t('automation.systemTaskSuccess'), { description: `耗时 ${result?.duration || 0}ms` });
|
||||
toast.success(t('automation.systemTaskSuccess'), { description: t('task.cronJob.list.execDuration', { ms: result?.duration || 0 }) });
|
||||
} else {
|
||||
toast.error(i18n.t('automation.systemTaskFailed'), { description: (result?.output || '').slice(0, 100) });
|
||||
toast.error(t('automation.systemTaskFailed'), { description: (result?.output || '').slice(0, 100) });
|
||||
}
|
||||
refreshData();
|
||||
} catch (error) {
|
||||
console.error('Failed to execute job:', error);
|
||||
toast.error(i18n.t('automation.systemTaskFailed'));
|
||||
toast.error(t('automation.systemTaskFailed'));
|
||||
} finally {
|
||||
setExecutingId(null);
|
||||
}
|
||||
@@ -150,18 +153,18 @@ export function CronJobManager() {
|
||||
|
||||
const handleDeleteJob = async (job: CronJobRecord) => {
|
||||
const ok = await _confirm({
|
||||
title: i18n.t('automation.deleteCronJob'),
|
||||
description: `确定删除「${job.name}」吗?此操作不可恢复。`,
|
||||
title: t('task.cronJob.list.delete'),
|
||||
description: t('task.cronJob.list.confirmDelete', { name: job.name }),
|
||||
variant: 'destructive',
|
||||
});
|
||||
if (!ok) return;
|
||||
try {
|
||||
await cronJobApi.deleteJob(job.id);
|
||||
toast.success(i18n.t('状态已切换'));
|
||||
toast.success(t('task.cronJob.list.toastToggled'));
|
||||
refreshData();
|
||||
} catch (error) {
|
||||
console.error('Failed to delete job:', error);
|
||||
toast.error(i18n.t('kubernetes.msg.deleteFailed2'));
|
||||
toast.error(t('kubernetes.msg.deleteFailed2'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -178,20 +181,20 @@ export function CronJobManager() {
|
||||
const handleBatchDelete = async () => {
|
||||
if (selectedIds.length === 0) return;
|
||||
const ok = await _confirm({
|
||||
title: i18n.t('删除定时任务'),
|
||||
description: `确定删除选中的 ${selectedIds.length} 个任务吗?此操作不可恢复。`,
|
||||
confirmText: i18n.t('system.terminalBatchDelete'),
|
||||
title: t('task.cronJob.list.delete'),
|
||||
description: t('task.cronJob.list.batchDeleteConfirm', { count: selectedIds.length }),
|
||||
confirmText: t('system.terminalBatchDelete'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
if (!ok) return;
|
||||
try {
|
||||
await cronJobApi.batchDelete(selectedIds);
|
||||
toast.success(`已删除 ${selectedIds.length} 个任务`);
|
||||
toast.success(t('task.cronJob.list.toastDeleted', { count: selectedIds.length }));
|
||||
setSelectedIds([]);
|
||||
refreshData();
|
||||
} catch (error) {
|
||||
console.error('Failed to batch delete jobs:', error);
|
||||
toast.error(i18n.t('system.batchDeleteFailed'));
|
||||
toast.error(t('system.batchDeleteFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -203,7 +206,7 @@ export function CronJobManager() {
|
||||
<div className="relative flex-1 min-w-[180px] max-w-md">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 dg-text-muted" />
|
||||
<Input
|
||||
placeholder={i18n.t('搜索名称、描述、脚本…')}
|
||||
placeholder={t('task.cronJob.list.searchPlaceholder')}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10 dg-input h-9"
|
||||
@@ -211,34 +214,34 @@ export function CronJobManager() {
|
||||
</div>
|
||||
<Select value={statusFilter} onValueChange={(v) => setStatusFilter(v as StatusFilter)}>
|
||||
<SelectTrigger className="w-[108px] dg-input h-9 text-xs">
|
||||
<SelectValue placeholder={i18n.t('system.terminalStatus')} />
|
||||
<SelectValue placeholder={t('system.terminalStatus')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all"> {i18n.t('cloud.allStatus')} </SelectItem>
|
||||
<SelectItem value="enabled"> {i18n.t('aiops.llmEnabled')} </SelectItem>
|
||||
<SelectItem value="disabled"> {i18n.t('aiops.llmDisabled')} </SelectItem>
|
||||
<SelectItem value="all"> {t('cloud.allStatus')} </SelectItem>
|
||||
<SelectItem value="enabled"> {t('aiops.llmEnabled')} </SelectItem>
|
||||
<SelectItem value="disabled"> {t('aiops.llmDisabled')} </SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={healthFilter} onValueChange={(v) => setHealthFilter(v as HealthFilter)}>
|
||||
<SelectTrigger className="w-[108px] dg-input h-9 text-xs">
|
||||
<SelectValue placeholder={i18n.t('k8s.clusters.statusHealthy')} />
|
||||
<SelectValue placeholder={t('k8s.clusters.statusHealthy')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all"> {i18n.t('状态')} </SelectItem>
|
||||
<SelectItem value="healthy"> {i18n.t('agent.heartbeatNormal')} </SelectItem>
|
||||
<SelectItem value="failed"> {i18n.t('monitor.siteProtocol.tcp.lastFail')} </SelectItem>
|
||||
<SelectItem value="all"> {t('task.cronJob.list.filterStatus')} </SelectItem>
|
||||
<SelectItem value="healthy"> {t('agent.heartbeatNormal')} </SelectItem>
|
||||
<SelectItem value="failed"> {t('monitor.siteProtocol.tcp.lastFail')} </SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={typeFilter} onValueChange={(v) => setTypeFilter(v as TypeFilter)}>
|
||||
<SelectTrigger className="w-[108px] dg-input h-9 text-xs">
|
||||
<SelectValue placeholder={i18n.t('system.credentialType')} />
|
||||
<SelectValue placeholder={t('system.credentialType')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all"> {i18n.t('k8s.topology.allTypes')} </SelectItem>
|
||||
<SelectItem value="all"> {t('k8s.topology.allTypes')} </SelectItem>
|
||||
<SelectItem value="shell">Shell</SelectItem>
|
||||
<SelectItem value="python">Python</SelectItem>
|
||||
<SelectItem value="powershell">PowerShell</SelectItem>
|
||||
<SelectItem value="command"> {i18n.t('system.terminalCommand')} </SelectItem>
|
||||
<SelectItem value="command"> {t('system.terminalCommand')} </SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{filteredJobs.length > 0 && (
|
||||
@@ -248,22 +251,22 @@ export function CronJobManager() {
|
||||
onCheckedChange={toggleSelectAll}
|
||||
className="border-[var(--dg-border)]"
|
||||
/>
|
||||
全选
|
||||
{t('task.cronJob.list.selectAll')}
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
{hasActiveFilters && (
|
||||
<span className="text-xs dg-text-muted">
|
||||
筛选结果 {filteredJobs.length}/{jobs.length}
|
||||
{t('task.cronJob.list.filterResult', { shown: filteredJobs.length, total: jobs.length })}
|
||||
</span>
|
||||
)}
|
||||
{selectedIds.length > 0 && (
|
||||
<>
|
||||
<span className="text-xs dg-text-secondary">已选 {selectedIds.length}</span>
|
||||
<span className="text-xs dg-text-secondary">{t('task.cronJob.list.selectedCount', { count: selectedIds.length })}</span>
|
||||
<Button variant="outline" size="sm" className="dg-btn-danger h-8" onClick={handleBatchDelete}>
|
||||
<Trash2 className="h-3.5 w-3.5 mr-1" />
|
||||
删除
|
||||
{t('task.cronJob.list.batchDelete')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
@@ -275,8 +278,8 @@ export function CronJobManager() {
|
||||
<>
|
||||
<PageShell
|
||||
variant="list"
|
||||
title={i18n.t('automation.cronJobs')}
|
||||
subtitle={i18n.t('Cron 调度 · 脚本执行 · 内置系统任务')}
|
||||
title={t('automation.cronJobs')}
|
||||
subtitle={t('task.cronJob.list.subtitle')}
|
||||
icon={<Clock className="h-5 w-5" />}
|
||||
tabs={pageTabs}
|
||||
activeTab={activeTab}
|
||||
@@ -293,7 +296,7 @@ export function CronJobManager() {
|
||||
onClick={() => { setHistoryJob(null); setIsLogDialogOpen(true); }}
|
||||
>
|
||||
<History className="h-4 w-4 mr-2" />
|
||||
全部历史
|
||||
{t('task.cronJob.list.allHistory')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
@@ -303,12 +306,12 @@ export function CronJobManager() {
|
||||
disabled={refreshing}
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 mr-2 ${refreshing ? 'animate-spin' : ''}`} />
|
||||
刷新
|
||||
{t('task.cronJob.list.refresh')}
|
||||
</Button>
|
||||
{activeTab === 'user' && canWrite && (
|
||||
<Button className="dg-btn-primary" onClick={openCreate}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
新建
|
||||
{t('task.cronJob.list.create')}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
@@ -324,17 +327,17 @@ export function CronJobManager() {
|
||||
{jobs.length === 0 ? (
|
||||
<PageEmpty
|
||||
icon={<PackageOpen className="h-12 w-12" />}
|
||||
title={i18n.t('automation.noCronJob')}
|
||||
title={t('automation.noCronJob')}
|
||||
action={canWrite ? (
|
||||
<Button className="dg-btn-primary" onClick={openCreate}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
新建任务
|
||||
{t('task.cronJob.list.createTask')}
|
||||
</Button>
|
||||
) : undefined}
|
||||
/>
|
||||
) : filteredJobs.length === 0 ? (
|
||||
<PageEmpty
|
||||
title={i18n.t('rbac.jobRole.patternHint.noMatch')}
|
||||
title={t('rbac.jobRole.patternHint.noMatch')}
|
||||
action={(
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -346,7 +349,7 @@ export function CronJobManager() {
|
||||
setTypeFilter('all');
|
||||
}}
|
||||
>
|
||||
清除筛选
|
||||
{t('task.cronJob.list.clearFilter')}
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// 远程执行页 — 关联主机后批量执行命令 / 上传文件(Go-native SSH) // Logan.Li
|
||||
// 对接后端 /remote-exec/*(controller.RemoteExecHandler + pkg/remoteexec),替代已移除的 Ansible。
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Terminal, Upload, RefreshCw, Loader2, Server as ServerIcon, CheckCircle2, XCircle, FileUp,
|
||||
} from 'lucide-react';
|
||||
@@ -15,7 +16,6 @@ import { serverApi, remoteExecApi, type RemoteExecResult, type RemoteUploadResul
|
||||
import { PageShell } from '@/components/layout/PageShell';
|
||||
import { PageEmpty } from '@/components/layout/PageEmpty';
|
||||
import type { JsonData } from '@/types/common';
|
||||
import i18n from '@/i18n';
|
||||
|
||||
interface RemoteServer {
|
||||
id: number; hostname: string; ip: string; username: string; port: number;
|
||||
@@ -23,6 +23,7 @@ interface RemoteServer {
|
||||
}
|
||||
|
||||
export function RemoteExecPage() {
|
||||
const { t } = useTranslation();
|
||||
const _confirm = useConfirm();
|
||||
const [servers, setServers] = useState<RemoteServer[]>([]);
|
||||
const [selected, setSelected] = useState<Set<number>>(new Set());
|
||||
@@ -42,7 +43,7 @@ export function RemoteExecPage() {
|
||||
try {
|
||||
const res = await serverApi.getServers({ page: 1, page_size: 500, keyword });
|
||||
setServers(((res as JsonData)?.list as RemoteServer[]) || []);
|
||||
} catch { toast.error(i18n.t('加载服务器列表失败')); }
|
||||
} catch { toast.error(t('task.remoteExec.toastFetchFailed')); }
|
||||
finally { setLoading(false); }
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -64,12 +65,12 @@ export function RemoteExecPage() {
|
||||
};
|
||||
|
||||
const doExec = async () => {
|
||||
if (!command.trim()) { toast.error(i18n.t('请输入要执行的命令')); return; }
|
||||
if (selCount === 0) { toast.error(i18n.t('请至少选择一台主机')); return; }
|
||||
if (!command.trim()) { toast.error(t('task.remoteExec.toastCommandRequired')); return; }
|
||||
if (selCount === 0) { toast.error(t('task.remoteExec.toastHostRequired')); return; }
|
||||
const ok = await _confirm({
|
||||
title: i18n.t('确认批量执行命令'),
|
||||
description: `将在 ${selCount} 台主机上执行该命令,请确认命令安全:\n\n${command}`,
|
||||
confirmText: i18n.t('deploy.label.execute'), variant: 'destructive',
|
||||
title: t('task.remoteExec.toastExecConfirm'),
|
||||
description: t('task.remoteExec.toastExecConfirmDesc', { count: selCount, command }),
|
||||
confirmText: t('deploy.label.execute'), variant: 'destructive',
|
||||
});
|
||||
if (!ok) return;
|
||||
setRunning(true); setExecResults([]);
|
||||
@@ -77,38 +78,38 @@ export function RemoteExecPage() {
|
||||
const res = await remoteExecApi.execute({ server_ids: selIds, command, timeout: timeoutSec });
|
||||
setExecResults(res || []);
|
||||
const succ = (res || []).filter(r => r.success).length;
|
||||
toast.success(`执行完成:${succ}/${selCount} 成功`);
|
||||
} catch { toast.error(i18n.t('automation.systemTaskFailed')); }
|
||||
toast.success(t('task.remoteExec.toastExecResult', { success: succ, total: selCount }));
|
||||
} catch { toast.error(t('automation.systemTaskFailed')); }
|
||||
finally { setRunning(false); }
|
||||
};
|
||||
|
||||
const doUpload = async () => {
|
||||
if (!file) { toast.error(i18n.t('请选择要上传的文件')); return; }
|
||||
if (!remotePath.trim()) { toast.error(i18n.t('请填写远程目标路径')); return; }
|
||||
if (selCount === 0) { toast.error(i18n.t('请至少选择一台主机')); return; }
|
||||
if (!file) { toast.error(t('task.remoteExec.toastFileRequired')); return; }
|
||||
if (!remotePath.trim()) { toast.error(t('task.remoteExec.toastPathRequired')); return; }
|
||||
if (selCount === 0) { toast.error(t('task.remoteExec.toastHostRequired')); return; }
|
||||
const dst = remotePath.trim().endsWith('/') ? remotePath.trim() + file.name : remotePath.trim();
|
||||
setRunning(true); setUploadResults([]);
|
||||
try {
|
||||
const res = await remoteExecApi.uploadFile({ server_ids: selIds, remote_path: dst, file });
|
||||
setUploadResults(res || []);
|
||||
const succ = (res || []).filter(r => r.success).length;
|
||||
toast.success(`上传完成:${succ}/${selCount} 成功`);
|
||||
} catch { toast.error(i18n.t('common.uploadFailed')); }
|
||||
toast.success(t('task.remoteExec.toastUploadResult', { success: succ, total: selCount }));
|
||||
} catch { toast.error(t('common.uploadFailed')); }
|
||||
finally { setRunning(false); }
|
||||
};
|
||||
|
||||
return (
|
||||
<PageShell
|
||||
variant="workspace"
|
||||
title={i18n.t('automation.remoteExec')}
|
||||
subtitle={i18n.t('关联主机后批量执行命令 / 上传文件(Go-native SSH)')}
|
||||
title={t('automation.remoteExec')}
|
||||
subtitle={t('task.remoteExec.subtitle')}
|
||||
icon={<Terminal className="h-5 w-5" />}
|
||||
contentClassName="flex-1 min-h-0"
|
||||
loading={loading}
|
||||
actions={(
|
||||
<Button variant="outline" size="sm" className="dg-btn-secondary" onClick={load}>
|
||||
<RefreshCw className="w-3.5 h-3.5 mr-1" />
|
||||
刷新
|
||||
{t('task.remoteExec.refresh')}
|
||||
</Button>
|
||||
)}
|
||||
>
|
||||
@@ -117,9 +118,9 @@ export function RemoteExecPage() {
|
||||
<Card className="col-span-5 dg-card">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm flex items-center justify-between text-[var(--dg-text-primary)]">
|
||||
<span className="flex items-center gap-2"><ServerIcon className="h-4 w-4 dg-text-blue" /> {i18n.t('automation.remoteSelectHost')} </span>
|
||||
<span className="flex items-center gap-2"><ServerIcon className="h-4 w-4 dg-text-blue" /> {t('automation.remoteSelectHost')} </span>
|
||||
<Input
|
||||
placeholder={i18n.t('搜索 主机名/IP')}
|
||||
placeholder={t('task.remoteExec.searchPlaceholder')}
|
||||
value={keyword}
|
||||
onChange={(e) => setKeyword(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && load()}
|
||||
@@ -130,16 +131,16 @@ export function RemoteExecPage() {
|
||||
<CardContent className="max-h-[520px] overflow-y-auto space-y-1">
|
||||
{servers.length === 0 ? (
|
||||
<PageEmpty
|
||||
title={i18n.t('dashboard.serversStatus.empty')}
|
||||
description={i18n.t('请先在「服务器管理」添加')}
|
||||
title={t('dashboard.serversStatus.empty')}
|
||||
description={t('task.remoteExec.emptyHint')}
|
||||
className="py-10"
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex items-center gap-2 px-2 py-1.5 text-xs dg-text-secondary border-b border-[var(--dg-border)]">
|
||||
<Checkbox checked={selected.size === servers.length && servers.length > 0} onCheckedChange={toggleAll} />
|
||||
全选({servers.length} 台)
|
||||
{selCount > 0 && <Badge className="ml-auto bg-blue-500/20 dg-text-blue">已选 {selCount}</Badge>}
|
||||
{t('task.remoteExec.selectAllCount', { count: servers.length })}
|
||||
{selCount > 0 && <Badge className="ml-auto bg-blue-500/20 dg-text-blue">{t('task.remoteExec.selectedCount', { count: selCount })}</Badge>}
|
||||
</div>
|
||||
{servers.map(s => (
|
||||
<label key={s.id} className="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-[var(--dg-bg-tertiary)] cursor-pointer">
|
||||
@@ -149,8 +150,8 @@ export function RemoteExecPage() {
|
||||
<div className="text-[10px] text-[var(--dg-text-muted)] font-mono">{s.ip} · {s.username || 'root'}</div>
|
||||
</div>
|
||||
{s.has_auth === false
|
||||
? <Badge className="bg-amber-500/20 dg-text-yellow text-[10px]"> {i18n.t('密钥')} </Badge>
|
||||
: <Badge className="bg-emerald-500/20 dg-text-green text-[10px]">{s.auth_type === 'key' ? i18n.t('system.credentialKey') : i18n.t('system.passwordLabel')}</Badge>}
|
||||
? <Badge className="bg-amber-500/20 dg-text-yellow text-[10px]"> {t('system.credentialKey')} </Badge>
|
||||
: <Badge className="bg-emerald-500/20 dg-text-green text-[10px]">{s.auth_type === 'key' ? t('system.credentialKey') : t('system.passwordLabel')}</Badge>}
|
||||
</label>
|
||||
))}
|
||||
</>
|
||||
@@ -167,7 +168,7 @@ export function RemoteExecPage() {
|
||||
onClick={() => setTab('exec')}
|
||||
className={tab === 'exec' ? 'dg-btn-primary h-8 text-xs gap-1' : 'dg-btn-secondary h-8 text-xs gap-1'}
|
||||
>
|
||||
<Terminal className="w-3.5 h-3.5" />执行命令
|
||||
<Terminal className="w-3.5 h-3.5" />{t('task.remoteExec.tabExec')}
|
||||
</Button>
|
||||
<Button
|
||||
variant={tab === 'upload' ? 'default' : 'outline'}
|
||||
@@ -175,50 +176,50 @@ export function RemoteExecPage() {
|
||||
onClick={() => setTab('upload')}
|
||||
className={tab === 'upload' ? 'dg-btn-primary h-8 text-xs gap-1' : 'dg-btn-secondary h-8 text-xs gap-1'}
|
||||
>
|
||||
<Upload className="w-3.5 h-3.5" />上传文件
|
||||
<Upload className="w-3.5 h-3.5" />{t('task.remoteExec.tabUpload')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{tab === 'exec' ? (
|
||||
<Card className="dg-card">
|
||||
<CardHeader className="pb-3"><CardTitle className="text-sm text-[var(--dg-text-primary)]"> {i18n.t('task.remoteexecpaget.seb6e67f1')} </CardTitle></CardHeader>
|
||||
<CardHeader className="pb-3"><CardTitle className="text-sm text-[var(--dg-text-primary)]"> {t('task.remoteExec.execTitle')} </CardTitle></CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<textarea
|
||||
value={command}
|
||||
onChange={(e) => setCommand(e.target.value)}
|
||||
rows={5}
|
||||
placeholder={i18n.t('输入要在选中主机上执行的 shell 命令,例如:df -h | head -5')}
|
||||
placeholder={t('task.remoteExec.execPlaceholder')}
|
||||
className="w-full bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-md px-3 py-2 text-xs font-mono text-[var(--dg-text-primary)] resize-none"
|
||||
/>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs dg-text-secondary"> {i18n.t('automation.remoteTimeout')} </span>
|
||||
<span className="text-xs dg-text-secondary"> {t('automation.remoteTimeout')} </span>
|
||||
<Input type="number" min={1} value={timeoutSec} onChange={(e) => setTimeoutSec(Number(e.target.value) || 60)}
|
||||
className="h-8 w-24 text-xs dg-input" />
|
||||
<Button onClick={doExec} disabled={running} className="ml-auto dg-btn-primary h-8 text-xs">
|
||||
{running ? <Loader2 className="w-3.5 h-3.5 mr-1 animate-spin" /> : <Terminal className="w-3.5 h-3.5 mr-1" />}
|
||||
执行({selCount} 台)
|
||||
{t('task.remoteExec.execButton', { count: selCount })}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<Card className="dg-card">
|
||||
<CardHeader className="pb-3"><CardTitle className="text-sm text-[var(--dg-text-primary)]"> {i18n.t('task.remoteexecpaget.s30048caa')} </CardTitle></CardHeader>
|
||||
<CardHeader className="pb-3"><CardTitle className="text-sm text-[var(--dg-text-primary)]"> {t('task.remoteExec.uploadTitle')} </CardTitle></CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="space-y-1.5">
|
||||
<span className="text-xs dg-text-secondary"> {i18n.t('task.remoteexecpaget.sb7e21a1e')} </span>
|
||||
<span className="text-xs dg-text-secondary"> {t('task.remoteExec.uploadFileLabel')} </span>
|
||||
<Input type="file" onChange={(e) => setFile(e.target.files?.[0] || null)}
|
||||
className="h-8 text-xs dg-input" />
|
||||
{file && <span className="text-[10px] text-[var(--dg-text-muted)]">{file.name} ({(file.size / 1024).toFixed(1)} KB)</span>}
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<span className="text-xs dg-text-secondary"> {i18n.t('task.remoteexecpaget.sb47f0323')} </span>
|
||||
<Input value={remotePath} onChange={(e) => setRemotePath(e.target.value)} placeholder={i18n.t('/tmp/ 或 /opt/app/config.yaml')}
|
||||
<span className="text-xs dg-text-secondary"> {t('task.remoteExec.uploadPathLabel')} </span>
|
||||
<Input value={remotePath} onChange={(e) => setRemotePath(e.target.value)} placeholder={t('task.remoteExec.uploadPathPlaceholder')}
|
||||
className="h-8 text-xs font-mono dg-input" />
|
||||
</div>
|
||||
<Button onClick={doUpload} disabled={running} className="dg-btn-primary h-8 text-xs">
|
||||
{running ? <Loader2 className="w-3.5 h-3.5 mr-1 animate-spin" /> : <FileUp className="w-3.5 h-3.5 mr-1" />}
|
||||
上传({selCount} 台)
|
||||
{t('task.remoteExec.uploadButton', { count: selCount })}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -226,7 +227,7 @@ export function RemoteExecPage() {
|
||||
|
||||
{(execResults.length > 0 || uploadResults.length > 0) && (
|
||||
<Card className="dg-card">
|
||||
<CardHeader className="pb-3"><CardTitle className="text-sm text-[var(--dg-text-primary)]"> {i18n.t('automation.remoteExecResult')} </CardTitle></CardHeader>
|
||||
<CardHeader className="pb-3"><CardTitle className="text-sm text-[var(--dg-text-primary)]"> {t('automation.remoteExecResult')} </CardTitle></CardHeader>
|
||||
<CardContent className="space-y-2 max-h-[300px] overflow-y-auto">
|
||||
{execResults.map((r, i) => (
|
||||
<div key={i} className="p-2 rounded border border-[var(--dg-border)] bg-[var(--dg-bg-tertiary)]">
|
||||
@@ -238,7 +239,7 @@ export function RemoteExecPage() {
|
||||
</Badge>
|
||||
</div>
|
||||
<pre className="text-[10px] font-mono dg-text-secondary whitespace-pre-wrap break-all max-h-32 overflow-y-auto">
|
||||
{r.error || r.output || i18n.t('(无输出)')}
|
||||
{r.error || r.output || t('task.remoteExec.noOutput')}
|
||||
</pre>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// ScriptManager — 脚本库主组件 // Logan.Li
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Plus, Search, RotateCcw, Trash2, FileCode } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
@@ -23,9 +24,9 @@ import { PageShell } from '@/components/layout/PageShell';
|
||||
import { ContentPanel } from '@/components/layout/ContentPanel';
|
||||
import { PageEmpty } from '@/components/layout/PageEmpty';
|
||||
import type { JsonData } from '@/types/common';
|
||||
import i18n from '@/i18n';
|
||||
|
||||
export function ScriptManager() {
|
||||
const { t } = useTranslation();
|
||||
const _confirm = useConfirm();
|
||||
const { hasAnyPermission, isAdmin } = usePermission();
|
||||
const canWrite = isAdmin || hasAnyPermission(['auto:script', 'auto:cron', 'auto:ansible']);
|
||||
@@ -52,7 +53,7 @@ export function ScriptManager() {
|
||||
const items = (res as JsonData)?.list || [];
|
||||
setScripts(items.map(mapScript));
|
||||
} catch {
|
||||
toast.error(i18n.t('获取脚本列表失败'));
|
||||
toast.error(t('task.scriptList.toastFetchFailed'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -73,6 +74,7 @@ export function ScriptManager() {
|
||||
useEffect(() => {
|
||||
fetchScripts();
|
||||
fetchRuntime();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const openCreate = () => {
|
||||
@@ -113,7 +115,7 @@ export function ScriptManager() {
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!formData.name.trim() || !formData.content.trim()) {
|
||||
toast.error(i18n.t('获取脚本列表失败'));
|
||||
toast.error(t('task.scriptList.toastNameRequired'));
|
||||
return;
|
||||
}
|
||||
const payload = {
|
||||
@@ -127,49 +129,49 @@ export function ScriptManager() {
|
||||
try {
|
||||
if (editingId !== null) {
|
||||
await scriptApi.updateScript(editingId, payload);
|
||||
toast.success(i18n.t('脚本更新成功'));
|
||||
toast.success(t('task.scriptList.toastSaved'));
|
||||
} else {
|
||||
await scriptApi.createScript(payload);
|
||||
toast.success(i18n.t('脚本更新成功'));
|
||||
toast.success(t('task.scriptList.toastSaved'));
|
||||
}
|
||||
setEditorOpen(false);
|
||||
fetchScripts();
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : (editingId !== null ? i18n.t('kubernetes.deployment.imageUpdateFailed') : i18n.t('monitor.dashboard.custom.toastCreateFailed')));
|
||||
toast.error(e instanceof Error ? e.message : (editingId !== null ? t('kubernetes.deployment.imageUpdateFailed') : t('monitor.dashboard.custom.toastCreateFailed')));
|
||||
}
|
||||
};
|
||||
|
||||
const handleClone = async (sc: Script) => {
|
||||
try {
|
||||
await scriptApi.createScript({
|
||||
name: `${sc.name} (副本)`,
|
||||
name: `${sc.name} ${t('task.scriptList.copySuffix')}`,
|
||||
code: genCode(),
|
||||
description: sc.description,
|
||||
type: sc.type,
|
||||
content: sc.content,
|
||||
timeout: sc.timeout,
|
||||
});
|
||||
toast.success(i18n.t('common.copySuccess'));
|
||||
toast.success(t('common.copySuccess'));
|
||||
fetchScripts();
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : i18n.t('monitor.logPanel.copyFailed'));
|
||||
toast.error(e instanceof Error ? e.message : t('monitor.logPanel.copyFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (sc: Script) => {
|
||||
const ok = await _confirm({
|
||||
title: i18n.t('automation.deleteScript'),
|
||||
description: `确定删除「${sc.name}」吗?此操作不可恢复。`,
|
||||
confirmText: i18n.t('kubernetes.storage.delete2'),
|
||||
title: t('task.scriptList.delete'),
|
||||
description: t('task.scriptList.confirmDelete', { name: sc.name }),
|
||||
confirmText: t('kubernetes.storage.delete2'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
if (!ok) return;
|
||||
try {
|
||||
await scriptApi.deleteScript(sc.id);
|
||||
toast.success(i18n.t('kubernetes.msg.deleteSuccess'));
|
||||
toast.success(t('kubernetes.msg.deleteSuccess'));
|
||||
fetchScripts();
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : i18n.t('kubernetes.msg.deleteFailed2'));
|
||||
toast.error(e instanceof Error ? e.message : t('kubernetes.msg.deleteFailed2'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -194,19 +196,19 @@ export function ScriptManager() {
|
||||
const handleBatchDelete = async () => {
|
||||
if (selectedIds.length === 0) return;
|
||||
const ok = await _confirm({
|
||||
title: i18n.t('删除脚本'),
|
||||
description: `确定删除选中的 ${selectedIds.length} 个脚本吗?此操作不可恢复。`,
|
||||
confirmText: i18n.t('system.terminalBatchDelete'),
|
||||
title: t('task.scriptList.delete'),
|
||||
description: t('task.scriptList.batchDeleteConfirm', { count: selectedIds.length }),
|
||||
confirmText: t('system.terminalBatchDelete'),
|
||||
variant: 'destructive',
|
||||
});
|
||||
if (!ok) return;
|
||||
try {
|
||||
await scriptApi.batchDelete(selectedIds);
|
||||
toast.success(`已删除 ${selectedIds.length} 个脚本`);
|
||||
toast.success(t('task.scriptList.toastDeleted', { count: selectedIds.length }));
|
||||
setSelectedIds([]);
|
||||
fetchScripts();
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : i18n.t('system.batchDeleteFailed'));
|
||||
toast.error(e instanceof Error ? e.message : t('system.batchDeleteFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -216,7 +218,7 @@ export function ScriptManager() {
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 dg-text-muted" />
|
||||
<Input
|
||||
placeholder={i18n.t('deploy.label.searchPlaceholder')}
|
||||
placeholder={t('deploy.label.searchPlaceholder')}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10 dg-input"
|
||||
@@ -224,10 +226,10 @@ export function ScriptManager() {
|
||||
</div>
|
||||
<Select value={typeFilter} onValueChange={setTypeFilter}>
|
||||
<SelectTrigger className="w-[140px] dg-input">
|
||||
<SelectValue placeholder={i18n.t('system.credentialType')} />
|
||||
<SelectValue placeholder={t('system.credentialType')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all"> {i18n.t('k8s.topology.allTypes')} </SelectItem>
|
||||
<SelectItem value="all"> {t('k8s.topology.allTypes')} </SelectItem>
|
||||
{scriptTypes.map(t => (
|
||||
<SelectItem key={t.value} value={t.value}>{t.label}</SelectItem>
|
||||
))}
|
||||
@@ -241,8 +243,8 @@ export function ScriptManager() {
|
||||
<>
|
||||
<PageShell
|
||||
variant="list"
|
||||
title={i18n.t('automation.scripts')}
|
||||
subtitle={i18n.t('脚本编辑与管理')}
|
||||
title={t('automation.scripts')}
|
||||
subtitle={t('task.scriptList.subtitle')}
|
||||
icon={<FileCode className="h-5 w-5" />}
|
||||
toolbar={toolbar}
|
||||
loading={loading}
|
||||
@@ -250,12 +252,12 @@ export function ScriptManager() {
|
||||
<>
|
||||
<Button variant="outline" className="dg-btn-secondary" onClick={fetchScripts}>
|
||||
<RotateCcw className="w-4 h-4 mr-2" />
|
||||
刷新
|
||||
{t('task.scriptList.refresh')}
|
||||
</Button>
|
||||
{canWrite && (
|
||||
<Button className="dg-btn-primary" onClick={openCreate}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
新建脚本
|
||||
{t('task.scriptList.create')}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
@@ -273,12 +275,12 @@ export function ScriptManager() {
|
||||
className="border-[var(--dg-border)]"
|
||||
/>
|
||||
<span className="text-sm dg-text-secondary">
|
||||
已选择 {selectedIds.length} 项 / 共 {filteredScripts.length} 项
|
||||
{t('task.scriptList.selectedCount', { selected: selectedIds.length, total: filteredScripts.length })}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" className="dg-btn-secondary" onClick={() => setSelectedIds([])}>
|
||||
取消选择
|
||||
{t('task.scriptList.cancelSelect')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -287,7 +289,7 @@ export function ScriptManager() {
|
||||
onClick={handleBatchDelete}
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
批量删除
|
||||
{t('task.scriptList.batchDelete')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -295,12 +297,12 @@ export function ScriptManager() {
|
||||
|
||||
{filteredScripts.length === 0 ? (
|
||||
<PageEmpty
|
||||
title={scripts.length === 0 ? i18n.t('automation.noScript') : i18n.t('暂无脚本')}
|
||||
description={scripts.length === 0 ? i18n.t('点击「新建脚本」创建第一个') : undefined}
|
||||
title={scripts.length === 0 ? t('automation.noScript') : t('task.scriptList.empty')}
|
||||
description={scripts.length === 0 ? t('task.scriptList.emptyHint') : undefined}
|
||||
action={scripts.length === 0 && canWrite ? (
|
||||
<Button className="dg-btn-primary" onClick={openCreate}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
新建脚本
|
||||
{t('task.scriptList.create')}
|
||||
</Button>
|
||||
) : undefined}
|
||||
/>
|
||||
|
||||
@@ -4520,6 +4520,56 @@
|
||||
"minutesAgo": "{{count}}m ago",
|
||||
"hoursAgo": "{{count}}h ago",
|
||||
"daysAgo": "{{count}}d ago"
|
||||
},
|
||||
"form": {
|
||||
"titleCreate": "Create Cron Job",
|
||||
"titleEdit": "Edit Cron Job",
|
||||
"desc": "Configure scheduled tasks with script association or custom commands",
|
||||
"labelName": "Job Name",
|
||||
"labelDesc": "Description",
|
||||
"labelCronExpr": "Cron Expression",
|
||||
"labelJobType": "Execution Type",
|
||||
"labelScript": "Select Script",
|
||||
"labelCommand": "Command",
|
||||
"placeholderName": "e.g., Database backup",
|
||||
"placeholderCommand": "/scripts/backup.sh arg1 arg2",
|
||||
"quickSelect": "Quick Select",
|
||||
"cronHint": "Format: minute hour day month weekday",
|
||||
"noScriptType": "No {{type}} scripts found in script library",
|
||||
"noScriptHint": "Please create a {{type}} script in Script Library first",
|
||||
"selectScriptPlaceholder": "Select a script from library...",
|
||||
"emptyScript": "(empty script)",
|
||||
"scriptLines": "{{count}} lines",
|
||||
"cancel": "Cancel",
|
||||
"toastNameRequired": "Please fill in job name and Cron expression",
|
||||
"toastSaved": "Job saved successfully"
|
||||
},
|
||||
"typeShell": "Link to Shell scripts from the script library",
|
||||
"typePython": "Link to Python scripts from the script library",
|
||||
"typePowershell": "Link to PowerShell scripts from the script library",
|
||||
"typeCommandDesc": "Manually enter command or script path",
|
||||
"typeCommandLabel": "Custom Command",
|
||||
"list": {
|
||||
"subtitle": "Cron Scheduler · Script Execution · Built-in System Tasks",
|
||||
"searchPlaceholder": "Search name, description, script...",
|
||||
"filterStatus": "Status",
|
||||
"filterHealth": "Health",
|
||||
"filterType": "Type",
|
||||
"selectAll": "Select All",
|
||||
"batchDelete": "Delete",
|
||||
"clearFilter": "Clear Filters",
|
||||
"filterResult": "Filtered {{shown}}/{{total}}",
|
||||
"selectedCount": "{{count}} selected",
|
||||
"allHistory": "All History",
|
||||
"refresh": "Refresh",
|
||||
"create": "Create",
|
||||
"createTask": "New Task",
|
||||
"delete": "Delete Cron Job",
|
||||
"toastToggled": "Status toggled",
|
||||
"toastDeleted": "{{count}} jobs deleted",
|
||||
"batchDeleteConfirm": "Delete {{count}} selected jobs? This cannot be undone.",
|
||||
"confirmDelete": "Delete \"{{name}}\"? This cannot be undone.",
|
||||
"execDuration": "Duration: {{ms}}ms"
|
||||
}
|
||||
},
|
||||
"script": {
|
||||
@@ -4566,34 +4616,53 @@
|
||||
"noPython": "Python is not installed. Python scripts will not be executable.",
|
||||
"partialUnavailable": "Some interpreters are unavailable; scripts of those types cannot run after creation."
|
||||
},
|
||||
"remoteexecpaget.seb6e67f1": "Batch Execute Commands",
|
||||
"remoteexecpaget.s30048caa": "Batch Upload Files",
|
||||
"remoteexecpaget.sb7e21a1e": "Select Local File",
|
||||
"remoteexecpaget.sb47f0323": "Remote target path (filename auto-appended for directories)",
|
||||
"cronjobformdial.s2e10ee70": "Configure Cron Job",
|
||||
"cronjobformdial.s3b56b1f9": "Format: min hour day month weekday",
|
||||
"cronjobformdial.s15d864c1": "Execution Type",
|
||||
"systemtaskspanel.s4d5e6f7g": "Built-in",
|
||||
"systemtaskspanel.s5e6f7g8h": "Process-level built-in scheduling, not editable; cleanup and cost tasks support manual trigger",
|
||||
"systemtaskspanel.s6f7g8h9i": "{{count}} tasks",
|
||||
"systemtaskspanel.s7g8h9i0j": "{{count}} failed",
|
||||
"systemtaskspanel.s8h9i0j1k": "Task",
|
||||
"systemtaskspanel.s9i0j1k2l": "Schedule / Description",
|
||||
"systemtaskspanel.s0j1k2l3m": "Last Run",
|
||||
"systemtaskspanel.s1k2l3m4n": "Actions",
|
||||
"systemtaskspanel.s2l3m4n5o": "Retain {{days}} days",
|
||||
"systemtaskspanel.s3m4n5o6p": "{{count}} runs",
|
||||
"systemtaskspanel.s4n5o6p7q": "Auto",
|
||||
"systemtaskspanel.s5o6p7q8r": "Run Now",
|
||||
"systemtaskspanel.s6p7q8r9s": "Manually run system task '{{name}}'?",
|
||||
"systemtaskspanel.s7q8r9s0t": "Execute",
|
||||
"systemtaskspanel.s8r9s0t1u": "Execution completed",
|
||||
"systemtaskspanel.s9s0t1u2v": "Execution failed",
|
||||
"systemtaskspanel.s0t1u2v3w": "Failed to load system tasks",
|
||||
"systemtaskspanel.s1u2v3w4x": "No system tasks",
|
||||
"systemtaskspanel.monitor": "Monitor",
|
||||
"systemtaskspanel.cleanup": "Cleanup",
|
||||
"systemtaskspanel.finops": "Cost"
|
||||
"scriptList": {
|
||||
"subtitle": "Script editing and management",
|
||||
"refresh": "Refresh",
|
||||
"create": "New Script",
|
||||
"batchDelete": "Batch Delete",
|
||||
"cancelSelect": "Cancel Selection",
|
||||
"selectedCount": "{{selected}} selected / {{total}} total",
|
||||
"empty": "No scripts",
|
||||
"emptyHint": "Click \"New Script\" to create your first one",
|
||||
"delete": "Delete Script",
|
||||
"confirmDelete": "Delete \"{{name}}\"? This cannot be undone.",
|
||||
"batchDeleteConfirm": "Delete {{count}} selected scripts? This cannot be undone.",
|
||||
"toastDeleted": "{{count}} scripts deleted",
|
||||
"toastFetchFailed": "Failed to fetch script list",
|
||||
"toastSaved": "Script saved successfully",
|
||||
"toastNameRequired": "Please fill in script name and content",
|
||||
"copySuffix": "(copy)"
|
||||
},
|
||||
"remoteExec": {
|
||||
"subtitle": "Batch execute commands / upload files via native SSH",
|
||||
"refresh": "Refresh",
|
||||
"searchPlaceholder": "Search hostname/IP",
|
||||
"emptyHint": "Add servers in Server Management first",
|
||||
"selectAllCount": "All ({{count}})",
|
||||
"selectedCount": "{{count}} selected",
|
||||
"tabExec": "Execute Command",
|
||||
"tabUpload": "Upload File",
|
||||
"execTitle": "Batch Execute Commands",
|
||||
"execPlaceholder": "Enter shell command to run on selected hosts, e.g.: df -h | head -5",
|
||||
"execButton": "Execute ({{count}} hosts)",
|
||||
"execResultTitle": "Results",
|
||||
"uploadTitle": "Batch Upload Files",
|
||||
"uploadFileLabel": "Select Local File",
|
||||
"uploadPathLabel": "Remote target path (directories auto-append filename)",
|
||||
"uploadPathPlaceholder": "/tmp/ or /opt/app/config.yaml",
|
||||
"uploadButton": "Upload ({{count}} hosts)",
|
||||
"toastFetchFailed": "Failed to load server list",
|
||||
"toastCommandRequired": "Please enter a command to execute",
|
||||
"toastHostRequired": "Please select at least one host",
|
||||
"toastFileRequired": "Please select a file to upload",
|
||||
"toastPathRequired": "Please enter a remote target path",
|
||||
"toastExecResult": "Execution complete: {{success}}/{{total}} succeeded",
|
||||
"toastUploadResult": "Upload complete: {{success}}/{{total}} succeeded",
|
||||
"toastExecConfirm": "Confirm batch execution",
|
||||
"toastExecConfirmDesc": "This will execute the command on {{count}} hosts. Please verify the command is safe:\n\n{{command}}",
|
||||
"noOutput": "(no output)"
|
||||
}
|
||||
},
|
||||
"rbac": {
|
||||
"logRole": {
|
||||
|
||||
@@ -4520,6 +4520,56 @@
|
||||
"minutesAgo": "{{count}} 分钟前",
|
||||
"hoursAgo": "{{count}} 小时前",
|
||||
"daysAgo": "{{count}} 天前"
|
||||
},
|
||||
"form": {
|
||||
"titleCreate": "新建定时任务",
|
||||
"titleEdit": "编辑定时任务",
|
||||
"desc": "配置 Cron 定时任务,支持脚本关联和自定义命令",
|
||||
"labelName": "任务名称",
|
||||
"labelDesc": "描述",
|
||||
"labelCronExpr": "Cron 表达式",
|
||||
"labelJobType": "执行类型",
|
||||
"labelScript": "选择脚本",
|
||||
"labelCommand": "命令",
|
||||
"placeholderName": "例如: 数据库备份",
|
||||
"placeholderCommand": "/scripts/backup.sh arg1 arg2",
|
||||
"quickSelect": "快速选择",
|
||||
"cronHint": "格式: 分 时 日 月 周",
|
||||
"noScriptType": "脚本库中没有 {{type}} 脚本",
|
||||
"noScriptHint": "请先到「脚本库」创建 {{type}} 脚本",
|
||||
"selectScriptPlaceholder": "选择脚本库中的脚本...",
|
||||
"emptyScript": "(空脚本)",
|
||||
"scriptLines": "{{count}} 行",
|
||||
"cancel": "取消",
|
||||
"toastNameRequired": "请填写任务名称和 Cron 表达式",
|
||||
"toastSaved": "任务更新成功"
|
||||
},
|
||||
"typeShell": "关联脚本库中的 Shell 脚本",
|
||||
"typePython": "关联脚本库中的 Python 脚本",
|
||||
"typePowershell": "关联脚本库中的 PowerShell 脚本",
|
||||
"typeCommandDesc": "手动输入命令或脚本路径",
|
||||
"typeCommandLabel": "自定义命令",
|
||||
"list": {
|
||||
"subtitle": "Cron 调度 · 脚本执行 · 内置系统任务",
|
||||
"searchPlaceholder": "搜索名称、描述、脚本…",
|
||||
"filterStatus": "状态",
|
||||
"filterHealth": "健康",
|
||||
"filterType": "类型",
|
||||
"selectAll": "全选",
|
||||
"batchDelete": "删除",
|
||||
"clearFilter": "清除筛选",
|
||||
"filterResult": "筛选结果 {{shown}}/{{total}}",
|
||||
"selectedCount": "已选 {{count}}",
|
||||
"allHistory": "全部历史",
|
||||
"refresh": "刷新",
|
||||
"create": "新建",
|
||||
"createTask": "新建任务",
|
||||
"delete": "删除定时任务",
|
||||
"toastToggled": "状态已切换",
|
||||
"toastDeleted": "已删除 {{count}} 个任务",
|
||||
"batchDeleteConfirm": "确定删除选中的 {{count}} 个任务吗?此操作不可恢复。",
|
||||
"confirmDelete": "确定删除「{{name}}」吗?此操作不可恢复。",
|
||||
"execDuration": "耗时 {{ms}}ms"
|
||||
}
|
||||
},
|
||||
"script": {
|
||||
@@ -4566,34 +4616,53 @@
|
||||
"noPython": "Python 未安装,Python 类型脚本将无法执行。",
|
||||
"partialUnavailable": "部分解释器不可用,对应类型的脚本创建后无法执行。"
|
||||
},
|
||||
"remoteexecpaget.seb6e67f1": "批量执行命令",
|
||||
"remoteexecpaget.s30048caa": "批量上传文件",
|
||||
"remoteexecpaget.sb7e21a1e": "选择本地文件",
|
||||
"remoteexecpaget.sb47f0323": "远程目标路径(目录则自动追加文件名)",
|
||||
"cronjobformdial.s2e10ee70": "配置 Cron 定时任务",
|
||||
"cronjobformdial.s3b56b1f9": "格式: 分 时 日 月 周",
|
||||
"cronjobformdial.s15d864c1": "执行类型",
|
||||
"systemtaskspanel.s4d5e6f7g": "内置",
|
||||
"systemtaskspanel.s5e6f7g8h": "进程内置调度,不可编辑;清理与成本类任务支持手动触发",
|
||||
"systemtaskspanel.s6f7g8h9i": "{{count}} 个任务",
|
||||
"systemtaskspanel.s7g8h9i0j": "{{count}} 个失败",
|
||||
"systemtaskspanel.s8h9i0j1k": "任务",
|
||||
"systemtaskspanel.s9i0j1k2l": "调度 / 说明",
|
||||
"systemtaskspanel.s0j1k2l3m": "最近执行",
|
||||
"systemtaskspanel.s1k2l3m4n": "操作",
|
||||
"systemtaskspanel.s2l3m4n5o": "保留 {{days}} 天",
|
||||
"systemtaskspanel.s3m4n5o6p": "{{count}} 次",
|
||||
"systemtaskspanel.s4n5o6p7q": "自动",
|
||||
"systemtaskspanel.s5o6p7q8r": "立即执行",
|
||||
"systemtaskspanel.s6p7q8r9s": "确定手动执行系统任务「{{name}}」吗?",
|
||||
"systemtaskspanel.s7q8r9s0t": "执行",
|
||||
"systemtaskspanel.s8r9s0t1u": "执行完成",
|
||||
"systemtaskspanel.s9s0t1u2v": "执行失败",
|
||||
"systemtaskspanel.s0t1u2v3w": "加载系统任务失败",
|
||||
"systemtaskspanel.s1u2v3w4x": "暂无系统任务",
|
||||
"systemtaskspanel.monitor": "监控",
|
||||
"systemtaskspanel.cleanup": "清理",
|
||||
"systemtaskspanel.finops": "成本"
|
||||
"scriptList": {
|
||||
"subtitle": "脚本编辑与管理",
|
||||
"refresh": "刷新",
|
||||
"create": "新建脚本",
|
||||
"batchDelete": "批量删除",
|
||||
"cancelSelect": "取消选择",
|
||||
"selectedCount": "已选择 {{selected}} 项 / 共 {{total}} 项",
|
||||
"empty": "暂无脚本",
|
||||
"emptyHint": "点击「新建脚本」创建第一个",
|
||||
"delete": "删除脚本",
|
||||
"confirmDelete": "确定删除「{{name}}」吗?此操作不可恢复。",
|
||||
"batchDeleteConfirm": "确定删除选中的 {{count}} 个脚本吗?此操作不可恢复。",
|
||||
"toastDeleted": "已删除 {{count}} 个脚本",
|
||||
"toastFetchFailed": "获取脚本列表失败",
|
||||
"toastSaved": "脚本更新成功",
|
||||
"toastNameRequired": "请填写脚本名称和内容",
|
||||
"copySuffix": "(副本)"
|
||||
},
|
||||
"remoteExec": {
|
||||
"subtitle": "关联主机后批量执行命令 / 上传文件(Go-native SSH)",
|
||||
"refresh": "刷新",
|
||||
"searchPlaceholder": "搜索 主机名/IP",
|
||||
"emptyHint": "请先在「服务器管理」添加",
|
||||
"selectAllCount": "全选({{count}} 台)",
|
||||
"selectedCount": "已选 {{count}}",
|
||||
"tabExec": "执行命令",
|
||||
"tabUpload": "上传文件",
|
||||
"execTitle": "批量执行命令",
|
||||
"execPlaceholder": "输入要在选中主机上执行的 shell 命令,例如:df -h | head -5",
|
||||
"execButton": "执行({{count}} 台)",
|
||||
"execResultTitle": "执行结果",
|
||||
"uploadTitle": "批量上传文件",
|
||||
"uploadFileLabel": "选择本地文件",
|
||||
"uploadPathLabel": "远程目标路径(目录则自动追加文件名)",
|
||||
"uploadPathPlaceholder": "/tmp/ 或 /opt/app/config.yaml",
|
||||
"uploadButton": "上传({{count}} 台)",
|
||||
"toastFetchFailed": "加载服务器列表失败",
|
||||
"toastCommandRequired": "请输入要执行的命令",
|
||||
"toastHostRequired": "请至少选择一台主机",
|
||||
"toastFileRequired": "请选择要上传的文件",
|
||||
"toastPathRequired": "请填写远程目标路径",
|
||||
"toastExecResult": "执行完成:{{success}}/{{total}} 成功",
|
||||
"toastUploadResult": "上传完成:{{success}}/{{total}} 成功",
|
||||
"toastExecConfirm": "确认批量执行命令",
|
||||
"toastExecConfirmDesc": "将在 {{count}} 台主机上执行该命令,请确认命令安全:\n\n{{command}}",
|
||||
"noOutput": "(无输出)"
|
||||
}
|
||||
},
|
||||
"rbac": {
|
||||
"logRole": {
|
||||
|
||||
Reference in new issue
Block a user