feat(push): 源路径验证、普通文件上传与暂存文件管理

补齐 Round4 H5/H6:验证 Nexus 主机源路径、失败批量重试;新增 upload-files 与普通文件多选上传;上传后可浏览/预览/重命名/删除暂存目录内容。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Nexus Agent
2026-06-08 01:14:07 +08:00
parent a225b655d9
commit 217c5d9935
52 changed files with 1231 additions and 83 deletions
@@ -0,0 +1,52 @@
# Changelog — 推送页 Round4 H5/H6 SPA 补齐
**日期**2026-06-07
## 摘要
Vue 推送页按 `docs/design/plans/2026-05-29-push-round4.md` 补齐 **H5 源路径验证****H6 批量重试失败**;禁止未验证路径直接推送,修复「填了路径但容器内不存在 → 全部失败」的常见误用。
## 动机
Round4 后端 `POST /api/sync/validate-source-path` 与重试队列早已存在,SPA 重构后前端缺失:
- 用户可在「推送源路径」随意输入未验证路径并触发 rsync,导致 Telegram「文件推送全部失败」
- 失败后无法一键提交重试任务
## 涉及文件
- `frontend/src/composables/push/usePushForm.ts``validateSourcePath()``validatedSource`、表单校验
- `frontend/src/composables/push/usePushProgress.ts` — 捕获 `retry_job_id``retryAllFailed()`
- `frontend/src/composables/push/types.ts``ValidatedSource``ServerProgress.retryJobId`
- `frontend/src/components/push/PushZipUpload.vue` — ZIP / 普通文件 / 主机路径三选一
- `server/api/sync_v2.py``POST /api/sync/upload-files` 普通文件暂存
- `tests/test_sync_upload_staging.py` — 暂存上传单测
- `frontend/src/components/push/PushToolbar.vue` — 移除源路径(迁至源文件卡片)
- `frontend/src/components/push/PushProgressList.vue` — 「重试全部失败」按钮
- `frontend/src/pages/PushPage.vue` — 接线
## 迁移 / 重启
- 仅前端静态资源;`vite build` 后部署 `web/app/` 即可
- 无 DB / API 变更
## 验证
```bash
cd frontend && npm run build
# type-check + vite build 通过
# 手动(推送页 #/push):
# 1. 仅输入源路径不点「验证路径」→ 推送应被拦截
# 2. 验证通过后显示文件数/大小,可推送
# 3. 推送部分失败后进度区显示「重试全部失败 (N)」
```
## 行为说明
| 源 | 规则 |
|----|------|
| ZIP 上传 | 解压至 `/tmp/nexus_upload_*`,与主机路径/普通文件互斥 |
| 普通文件 | `POST /api/sync/upload-files`,可多选,暂存同目录后 rsync |
| 主机源路径 | 须点击「验证路径」调用 `/api/sync/validate-source-path` |
| 目标路径 | 留空用各机 `servers.target_path`(与 a225b65 一致) |
| 失败重试 | WS/HTTP 返回的 `retry_job_id``POST /api/retries/{id}/retry` |
@@ -0,0 +1,37 @@
# Changelog — 推送页暂存文件管理
**日期**2026-06-07
## 摘要
推送页在上传 ZIP/普通文件后展示**暂存文件列表**,支持浏览子目录、预览、重命名、删除;复用已有 `/api/sync/browse-local``local-file-ops``local-file-preview`
## 动机
上传后仅显示摘要,无法查看/调整暂存目录内容;后端 Round2 已实现 staging APISPA 未接入。
## 涉及文件
- `frontend/src/composables/push/usePushStaging.ts` — 浏览/预览/重命名/删除
- `frontend/src/components/push/PushStagingPanel.vue` — 文件列表与操作对话框
- `frontend/src/components/push/PushStagingPreviewDialog.vue` — 文本/二进制预览
- `frontend/src/composables/push/usePushPage.ts` — 编排
- `frontend/src/pages/PushPage.vue` — 上传后展示管理面板
- `frontend/src/components/push/PushZipUpload.vue` — 提示文案
## 迁移 / 重启
- 仅前端;`vite build` 后部署 `web/app/`
## 验证
```bash
cd frontend && npm run build
# 推送页上传文件 → 出现「暂存文件」表格 → 预览/重命名/删除/进入子目录
```
## 行为
-**上传暂存**`/tmp/nexus_upload_*`)显示管理面板;「验证路径」的主机目录不在此管理(安全沙箱限制)
- 预览:≤1MB 文件,最多 4KB 内容
- 删除/重命名后刷新列表并更新顶部文件数/大小
@@ -3,6 +3,18 @@
<v-card-title class="d-flex align-center text-subtitle-1">
推送进度
<v-spacer />
<v-btn
v-if="failedRetryCount > 0 && !pushing"
size="small"
variant="tonal"
color="warning"
class="mr-2"
prepend-icon="mdi-refresh"
:loading="retryingAll"
@click="$emit('retry-all')"
>
重试全部失败 ({{ failedRetryCount }})
</v-btn>
<v-btn
v-if="batchId && items.some(s => s.status === 'running' || s.status === 'pending') && pushing"
size="small"
@@ -30,6 +42,9 @@
/>
<div v-if="items.length" class="text-caption text-medium-emphasis mb-2">
{{ doneCount }} / {{ items.length }} 台已结束
<span v-if="items.some(s => s.status === 'failed')">
· {{ items.filter(s => s.status === 'failed').length }} 失败
</span>
</div>
<v-list density="compact">
<v-list-item v-for="s in items" :key="s.id" :title="s.name">
@@ -59,11 +74,14 @@ defineProps<{
batchId: string
pushing: boolean
cancelling: boolean
retryingAll: boolean
failedRetryCount: number
doneCount: number
totalProgress: number
}>()
defineEmits<{
cancel: []
'retry-all': []
}>()
</script>
@@ -0,0 +1,144 @@
<template>
<v-card elevation="0" border rounded="lg" class="mb-4">
<v-card-title class="d-flex align-center text-subtitle-1">
暂存文件
<v-spacer />
<v-btn size="small" variant="text" prepend-icon="mdi-refresh" :loading="loading" @click="$emit('refresh')">
刷新
</v-btn>
</v-card-title>
<v-card-subtitle class="text-caption pb-2">
{{ rootPath }}
<span v-if="relativePath"> / {{ relativePath }}</span>
</v-card-subtitle>
<v-divider />
<v-card-text class="pa-0">
<v-progress-linear v-if="loading" indeterminate />
<v-table v-else density="compact" hover>
<thead>
<tr>
<th>名称</th>
<th class="text-end" style="width: 100px">大小</th>
<th class="text-end" style="width: 160px">操作</th>
</tr>
</thead>
<tbody>
<tr v-if="relativePath">
<td colspan="3">
<v-btn size="small" variant="text" prepend-icon="mdi-arrow-up" @click="$emit('go-root')">
返回根目录
</v-btn>
</td>
</tr>
<tr v-if="!entries.length">
<td colspan="3" class="text-medium-emphasis text-center py-4">目录为空</td>
</tr>
<tr v-for="entry in entries" :key="entry.name">
<td>
<div
class="d-flex align-center"
:class="{ 'cursor-pointer': entry.is_dir }"
@click="entry.is_dir ? $emit('open-dir', entry) : undefined"
>
<v-icon size="small" class="mr-2" :color="entry.is_dir ? 'primary' : undefined">
{{ entry.is_dir ? 'mdi-folder' : 'mdi-file-outline' }}
</v-icon>
<span>{{ entry.name }}</span>
</div>
</td>
<td class="text-end text-caption text-medium-emphasis">
{{ entry.is_dir ? '—' : formatSize(entry.size) }}
</td>
<td class="text-end">
<v-btn
v-if="!entry.is_dir"
size="x-small"
variant="text"
@click="$emit('preview', entry)"
>
预览
</v-btn>
<v-btn size="x-small" variant="text" @click="$emit('rename', entry)">重命名</v-btn>
<v-btn size="x-small" variant="text" color="error" @click="$emit('delete', entry)">删除</v-btn>
</td>
</tr>
</tbody>
</v-table>
</v-card-text>
</v-card>
<v-dialog :model-value="renameOpen" max-width="420" @update:model-value="$emit('update:renameOpen', $event)">
<v-card border>
<v-card-title>重命名</v-card-title>
<v-card-text>
<v-text-field
:model-value="renameValue"
label="新名称"
variant="outlined"
density="compact"
hide-details
autofocus
@update:model-value="$emit('update:renameValue', $event)"
@keydown.enter.prevent="$emit('confirm-rename')"
/>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn variant="text" @click="$emit('update:renameOpen', false)">取消</v-btn>
<v-btn color="primary" variant="flat" :loading="renameLoading" @click="$emit('confirm-rename')">确定</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog :model-value="deleteOpen" max-width="420" @update:model-value="$emit('update:deleteOpen', $event)">
<v-card border>
<v-card-title>确认删除</v-card-title>
<v-card-text>
确定删除{{ deleteTarget?.name }}{{ deleteTarget?.is_dir ? '将删除整个目录及其内容。' : '' }}
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn variant="text" @click="$emit('update:deleteOpen', false)">取消</v-btn>
<v-btn color="error" variant="flat" :loading="deleteLoading" @click="$emit('confirm-delete')">删除</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script setup lang="ts">
import { formatSize } from '@/composables/push/labels'
import type { StagingEntry } from '@/composables/push/types'
defineProps<{
rootPath: string
relativePath: string
entries: StagingEntry[]
loading: boolean
renameOpen: boolean
renameValue: string
renameLoading: boolean
deleteOpen: boolean
deleteTarget: StagingEntry | null
deleteLoading: boolean
}>()
defineEmits<{
refresh: []
'go-root': []
'open-dir': [entry: StagingEntry]
preview: [entry: StagingEntry]
rename: [entry: StagingEntry]
delete: [entry: StagingEntry]
'confirm-rename': []
'confirm-delete': []
'update:renameOpen': [value: boolean]
'update:renameValue': [value: string]
'update:deleteOpen': [value: boolean]
}>()
</script>
<style scoped>
.cursor-pointer {
cursor: pointer;
}
</style>
@@ -0,0 +1,83 @@
<template>
<v-dialog :model-value="modelValue" max-width="720" @update:model-value="$emit('update:modelValue', $event)">
<v-card border>
<v-card-title class="text-subtitle-1">
{{ result?.name ?? '文件预览' }}
<span v-if="result" class="text-caption text-medium-emphasis ml-2">
({{ formatSize(result.size) }})
</span>
</v-card-title>
<v-card-text>
<v-progress-linear v-if="loading" indeterminate class="mb-3" />
<template v-else-if="result">
<v-alert
v-if="result.truncated"
type="info"
density="compact"
variant="tonal"
class="mb-2"
>
仅显示前 4KB 内容
</v-alert>
<v-alert
v-if="result.encoding_hint !== 'text'"
type="warning"
density="compact"
variant="tonal"
class="mb-2"
>
二进制文件以下为 Base64 片段
</v-alert>
<pre class="staging-preview-body">{{ previewText }}</pre>
</template>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn variant="text" @click="$emit('update:modelValue', false)">关闭</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { formatSize } from '@/composables/push/labels'
import type { StagingPreviewResult } from '@/composables/push/types'
const props = defineProps<{
modelValue: boolean
loading: boolean
result: StagingPreviewResult | null
}>()
defineEmits<{
'update:modelValue': [value: boolean]
}>()
const previewText = computed(() => {
const r = props.result
if (!r?.content_base64) return ''
try {
const raw = atob(r.content_base64)
if (r.encoding_hint === 'text') return raw
return r.content_base64
} catch {
return r.content_base64
}
})
</script>
<style scoped>
.staging-preview-body {
max-height: 420px;
overflow: auto;
margin: 0;
padding: 12px;
border-radius: 8px;
background: rgba(var(--v-theme-on-surface), 0.04);
font-size: 12px;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-all;
}
</style>
+1 -17
View File
@@ -1,20 +1,7 @@
<template>
<v-card elevation="0" border rounded="lg" class="mb-4 pa-4">
<v-row align="center" dense>
<v-col cols="12" sm="4">
<v-text-field
:model-value="sourcePath"
label="推送源路径"
variant="outlined"
density="compact"
prepend-inner-icon="mdi-folder"
:disabled="zipLocked"
:hint="zipLocked ? '已使用 ZIP 解压目录作为源路径' : undefined"
persistent-hint
@update:model-value="$emit('update:sourcePath', $event)"
/>
</v-col>
<v-col cols="12" sm="4">
<v-col cols="12" sm="8">
<v-text-field
:model-value="targetPath"
label="目标路径"
@@ -47,14 +34,11 @@
<script setup lang="ts">
defineProps<{
sourcePath: string
targetPath: string
zipLocked: boolean
pushing: boolean
}>()
defineEmits<{
'update:sourcePath': [value: string]
'update:targetPath': [value: string]
preview: []
push: []
+87 -23
View File
@@ -2,28 +2,86 @@
<v-card elevation="0" border rounded="lg" class="mb-4">
<v-card-title class="text-subtitle-1">源文件</v-card-title>
<v-card-text>
<div v-if="!uploadedZip">
<div
class="border-dashed border rounded pa-6 text-center cursor-pointer"
:class="{ 'border-primary bg-primary-lighten-5': isDragging }"
@dragover.prevent="$emit('dragover')"
@dragleave="$emit('dragleave')"
@drop.prevent="$emit('drop', $event)"
@click="zipInputRef?.click()"
>
<v-icon size="48" color="grey">mdi-cloud-upload-outline</v-icon>
<div class="text-body-2 mt-2">拖拽 ZIP 文件到此处或点击选择</div>
<div class="text-caption text-medium-emphasis">支持 .zip 格式</div>
<template v-if="!validatedSource">
<div v-if="!uploadedSource">
<div
class="border-dashed border rounded pa-6 text-center cursor-pointer"
:class="{ 'border-primary bg-primary-lighten-5': isDragging }"
@dragover.prevent="$emit('dragover')"
@dragleave="$emit('dragleave')"
@drop.prevent="$emit('drop', $event)"
@click="fileInputRef?.click()"
>
<v-icon size="48" color="grey">mdi-cloud-upload-outline</v-icon>
<div class="text-body-2 mt-2">拖拽文件到此处或点击选择</div>
<div class="text-caption text-medium-emphasis">
支持普通文件可多选 .zip 压缩包
</div>
</div>
<input
ref="fileInputRef"
type="file"
multiple
hidden
@change="$emit('file-select', $event)"
/>
</div>
<div v-else class="d-flex align-center flex-wrap ga-2">
<v-icon class="mr-1">
{{ uploadedSource.kind === 'zip' ? 'mdi-folder-zip' : 'mdi-file-multiple' }}
</v-icon>
<span>
{{ uploadedSource.name }}
({{ uploadedSource.fileCount }} 个文件, {{ formatSize(uploadedSource.size) }})
</span>
<v-spacer />
<v-btn size="small" variant="text" color="error" @click="$emit('clear')">清除</v-btn>
</div>
<v-progress-linear v-if="sourceUploading" indeterminate class="mt-2" />
</template>
<v-alert v-else type="success" variant="tonal" density="compact" class="mb-0">
<div class="d-flex align-center flex-wrap ga-2">
<v-icon>mdi-folder-check</v-icon>
<span class="text-body-2">
{{ validatedSource.path }}
({{ validatedSource.fileCount }} 个文件, {{ formatSize(validatedSource.sizeBytes) }})
</span>
<v-spacer />
<v-btn size="small" variant="text" color="error" @click="$emit('clear-validated')">清除</v-btn>
</div>
</v-alert>
<v-divider v-if="!validatedSource" class="my-4" />
<div v-if="!validatedSource && !uploadedSource">
<div class="text-caption text-medium-emphasis mb-2">或输入 Nexus 主机源路径容器内绝对路径</div>
<div class="d-flex ga-2 align-start flex-wrap">
<v-text-field
:model-value="sourcePath"
placeholder="/path/to/files"
variant="outlined"
density="compact"
hide-details
prepend-inner-icon="mdi-folder"
class="flex-grow-1"
style="min-width: 240px"
@update:model-value="$emit('update:sourcePath', $event)"
@keydown.enter.prevent="$emit('validate')"
/>
<v-btn
variant="tonal"
color="primary"
:loading="sourcePathValidating"
:disabled="!sourcePath.trim()"
@click="$emit('validate')"
>
验证路径
</v-btn>
</div>
<input ref="zipInputRef" type="file" accept=".zip" hidden @change="$emit('file-select', $event)" />
</div>
<div v-else class="d-flex align-center">
<v-icon class="mr-2">mdi-folder-zip</v-icon>
<span>{{ uploadedZip.name }} ({{ uploadedZip.fileCount }} 个文件, {{ formatSize(uploadedZip.size) }})</span>
<v-spacer />
<v-btn size="small" variant="text" color="error" @click="$emit('clear')">清除</v-btn>
<div v-else-if="uploadedSource" class="text-caption text-medium-emphasis mt-2">
已使用上传暂存目录作为源可在下方管理文件或清除后改用主机源路径
</div>
<v-progress-linear v-if="zipUploading" indeterminate class="mt-2" />
</v-card-text>
</v-card>
</template>
@@ -31,12 +89,15 @@
<script setup lang="ts">
import { ref } from 'vue'
import { formatSize } from '@/composables/push/labels'
import type { UploadedZip } from '@/composables/push/types'
import type { UploadedSource, ValidatedSource } from '@/composables/push/types'
defineProps<{
uploadedZip: UploadedZip | null
uploadedSource: UploadedSource | null
validatedSource: ValidatedSource | null
sourcePath: string
sourcePathValidating: boolean
isDragging: boolean
zipUploading: boolean
sourceUploading: boolean
}>()
defineEmits<{
@@ -45,7 +106,10 @@ defineEmits<{
drop: [e: DragEvent]
'file-select': [e: Event]
clear: []
'clear-validated': []
'update:sourcePath': [value: string]
validate: []
}>()
const zipInputRef = ref<HTMLInputElement | null>(null)
const fileInputRef = ref<HTMLInputElement | null>(null)
</script>
+29 -1
View File
@@ -18,6 +18,7 @@ export interface ServerProgress {
name: string
status: ServerProgressStatus
detail?: string
retryJobId?: number
}
export interface SyncProgressMsg {
@@ -27,15 +28,42 @@ export interface SyncProgressMsg {
status?: string
error_message?: string | null
duration_seconds?: number | null
retry_job_id?: number | null
}
export interface UploadedZip {
export interface ValidatedSource {
path: string
fileCount: number
sizeBytes: number
}
export interface StagingEntry {
name: string
is_dir: boolean
size: number
}
export interface StagingPreviewResult {
name: string
size: number
content_base64: string
truncated: boolean
encoding_hint: string
}
export type UploadedSourceKind = 'zip' | 'files'
export interface UploadedSource {
kind: UploadedSourceKind
name: string
sourcePath: string
fileCount: number
size: number
}
/** @deprecated use UploadedSource */
export type UploadedZip = UploadedSource
export interface PreviewStats {
files_total?: number
files_created?: number
+160 -27
View File
@@ -1,19 +1,56 @@
import { ref, computed } from 'vue'
import { ref, computed, watch } from 'vue'
import { http } from '@/api'
import { useSnackbar } from '@/composables/useSnackbar'
import type { PushServerItem, SyncMode, UploadedZip } from './types'
import type { PushServerItem, SyncMode, UploadedSource, ValidatedSource } from './types'
type StagingUploadResponse = {
source_path: string
file_count: number
size: number
files?: string[]
}
type ValidateSourcePathResponse = {
valid: boolean
path: string
file_count: number
size_bytes: number
}
function partitionUploadFiles(files: File[]): { zips: File[]; others: File[] } {
const zips: File[] = []
const others: File[] = []
for (const f of files) {
if (f.name.toLowerCase().endsWith('.zip')) {
zips.push(f)
} else {
others.push(f)
}
}
return { zips, others }
}
export function usePushForm() {
const snackbar = useSnackbar()
const sourcePath = ref('')
const validatedSource = ref<ValidatedSource | null>(null)
const lastValidatedPath = ref('')
const sourcePathValidating = ref(false)
/** 留空 = 使用各服务器在「服务器管理」中配置的 target_path */
const targetPath = ref('')
const syncMode = ref<SyncMode>('incremental')
const isDragging = ref(false)
const uploadedZip = ref<UploadedZip | null>(null)
const zipUploading = ref(false)
const uploadedSource = ref<UploadedSource | null>(null)
const sourceUploading = ref(false)
watch(sourcePath, (v) => {
if (validatedSource.value && v.trim() !== lastValidatedPath.value) {
validatedSource.value = null
}
})
const syncModeDesc = computed(() => {
switch (syncMode.value) {
case 'incremental':
@@ -28,7 +65,7 @@ export function usePushForm() {
})
function effectiveSourcePath(): string {
return uploadedZip.value?.sourcePath || sourcePath.value
return uploadedSource.value?.sourcePath || validatedSource.value?.path || ''
}
/** 留空时后端按各服务器 target_path 推送 */
@@ -39,7 +76,11 @@ export function usePushForm() {
function validateForm(selectedCount: number, selectedServers: PushServerItem[] = []): boolean {
if (!effectiveSourcePath()) {
snackbar('请选择源路径或上传 ZIP 文件', 'warning')
if (sourcePath.value.trim() && !validatedSource.value) {
snackbar('请先点击「验证路径」确认 Nexus 主机上的源目录', 'warning')
} else {
snackbar('请上传文件/ZIP 或验证 Nexus 主机源路径', 'warning')
}
return false
}
if (!effectiveTargetPath()) {
@@ -59,27 +100,90 @@ export function usePushForm() {
return true
}
async function handleZipDrop(e: DragEvent) {
isDragging.value = false
const file = e.dataTransfer?.files[0]
if (file?.name.endsWith('.zip')) await uploadZip(file)
async function validateSourcePath() {
const p = sourcePath.value.trim()
if (!p) {
snackbar('请输入 Nexus 主机上的源目录路径', 'warning')
return
}
if (uploadedSource.value) {
snackbar('请先清除已上传的源文件', 'warning')
return
}
sourcePathValidating.value = true
try {
const res = await http.post<ValidateSourcePathResponse>('/sync/validate-source-path', { path: p })
validatedSource.value = {
path: res.path,
fileCount: res.file_count,
sizeBytes: res.size_bytes,
}
lastValidatedPath.value = res.path
sourcePath.value = res.path
snackbar(`路径验证通过:${res.file_count} 个文件`)
} catch (e: unknown) {
validatedSource.value = null
snackbar(e instanceof Error ? e.message : '路径验证失败', 'error')
} finally {
sourcePathValidating.value = false
}
}
function handleZipSelect(e: Event) {
const file = (e.target as HTMLInputElement).files?.[0]
if (file) uploadZip(file)
function clearValidatedSource() {
validatedSource.value = null
lastValidatedPath.value = ''
sourcePath.value = ''
}
function clearUploadedSource() {
uploadedSource.value = null
}
async function ingestSelectedFiles(files: File[]) {
if (!files.length) return
if (validatedSource.value) {
snackbar('请先清除已验证的源路径', 'warning')
return
}
const { zips, others } = partitionUploadFiles(files)
if (zips.length && others.length) {
snackbar('请勿同时上传 ZIP 与普通文件', 'warning')
return
}
if (zips.length > 1) {
snackbar('一次只能上传一个 ZIP 文件', 'warning')
return
}
if (zips.length === 1) {
await uploadZip(zips[0])
return
}
await uploadPlainFiles(others)
}
async function handleFileDrop(e: DragEvent) {
isDragging.value = false
const files = Array.from(e.dataTransfer?.files ?? [])
await ingestSelectedFiles(files)
}
function handleFileSelect(e: Event) {
const input = e.target as HTMLInputElement
const files = Array.from(input.files ?? [])
input.value = ''
void ingestSelectedFiles(files)
}
async function uploadZip(file: File) {
zipUploading.value = true
sourceUploading.value = true
try {
const form = new FormData()
form.append('file', file)
const res = await http.upload<{ source_path: string; file_count: number; size: number }>(
'/sync/upload-zip',
form,
)
uploadedZip.value = {
const res = await http.upload<StagingUploadResponse>('/sync/upload-zip', form)
uploadedSource.value = {
kind: 'zip',
name: file.name,
sourcePath: res.source_path,
fileCount: res.file_count,
@@ -89,27 +193,56 @@ export function usePushForm() {
} catch (e: unknown) {
snackbar(e instanceof Error ? e.message : '上传失败', 'error')
} finally {
zipUploading.value = false
sourceUploading.value = false
}
}
function clearZipUpload() {
uploadedZip.value = null
async function uploadPlainFiles(files: File[]) {
if (!files.length) return
sourceUploading.value = true
try {
const form = new FormData()
for (const f of files) {
form.append('files', f)
}
const res = await http.upload<StagingUploadResponse>('/sync/upload-files', form)
const displayName =
files.length === 1
? files[0].name
: `${files.length} 个文件`
uploadedSource.value = {
kind: 'files',
name: displayName,
sourcePath: res.source_path,
fileCount: res.file_count,
size: res.size,
}
snackbar(`已上传 ${res.file_count} 个文件`)
} catch (e: unknown) {
snackbar(e instanceof Error ? e.message : '上传失败', 'error')
} finally {
sourceUploading.value = false
}
}
return {
sourcePath,
validatedSource,
sourcePathValidating,
targetPath,
syncMode,
syncModeDesc,
isDragging,
uploadedZip,
zipUploading,
uploadedSource,
sourceUploading,
effectiveSourcePath,
effectiveTargetPath,
validateForm,
handleZipDrop,
handleZipSelect,
clearZipUpload,
validateSourcePath,
clearValidatedSource,
clearUploadedSource,
handleFileDrop,
handleFileSelect,
}
}
@@ -4,10 +4,12 @@ import { usePushServers } from './usePushServers'
import { usePushProgress } from './usePushProgress'
import { usePushPreview } from './usePushPreview'
import { usePushLogs } from './usePushLogs'
import { usePushStaging } from './usePushStaging'
/** Orchestrates push page composables and lifecycle */
export function usePushPage() {
const form = usePushForm()
const staging = usePushStaging(form.uploadedSource)
const pushStatus = ref<Record<number, string>>({})
const servers = usePushServers(pushStatus)
const logs = usePushLogs(form, servers)
@@ -31,6 +33,7 @@ export function usePushPage() {
return {
form: reactive(form),
staging: reactive(staging),
servers: reactive(servers),
progress: reactive(progress),
preview: reactive(preview),
@@ -20,7 +20,12 @@ type PushFilesResponse = {
batch_id?: string
results?: Record<
string,
{ status?: string; error_message?: string | null; duration_seconds?: number | null }
{
status?: string
error_message?: string | null
duration_seconds?: number | null
retry_job_id?: number | null
}
>
}
@@ -38,6 +43,7 @@ export function usePushProgress(
const pushing = ref(false)
const cancelling = ref(false)
const retryingAll = ref(false)
const wsProgressItems = ref<ServerProgress[]>([])
const pushBatchId = ref('')
const wsSocket = ref<WebSocket | null>(null)
@@ -96,6 +102,9 @@ export function usePushProgress(
} else if (st === 'failed') {
item.status = 'failed'
item.detail = formatPushErrorMessage(msg.error_message) || '失败'
if (msg.retry_job_id != null) {
item.retryJobId = msg.retry_job_id
}
servers.pushStatus.value[msg.server_id] = '失败'
} else if (st === 'cancelled') {
item.status = 'cancelled'
@@ -124,6 +133,9 @@ export function usePushProgress(
} else if (st === 'failed') {
item.status = 'failed'
item.detail = formatPushErrorMessage(log.error_message) || '失败'
if (log.retry_job_id != null) {
item.retryJobId = log.retry_job_id
}
servers.pushStatus.value[serverId] = '失败'
} else if (st === 'cancelled') {
item.status = 'cancelled'
@@ -278,6 +290,41 @@ export function usePushProgress(
_pushTimeouts.push(safetyTimer)
}
const failedRetryCount = computed(() =>
wsProgressItems.value.filter(p => p.status === 'failed' && p.retryJobId != null).length,
)
async function retryAllFailed() {
const jobIds = wsProgressItems.value
.filter(p => p.status === 'failed' && p.retryJobId != null)
.map(p => p.retryJobId!)
if (!jobIds.length) {
snackbar('没有可重试的失败任务(重试任务可能尚未创建)', 'warning')
return
}
retryingAll.value = true
let ok = 0
let fail = 0
try {
for (const id of jobIds) {
try {
await http.post(`/retries/${id}/retry`)
ok++
} catch {
fail++
}
}
snackbar(
fail > 0
? `已提交 ${ok} 个重试,${fail} 个提交失败`
: `已提交 ${ok} 个失败服务器的重试任务`,
)
} finally {
retryingAll.value = false
}
}
function dispose() {
disconnectProgressWs()
clearPushTimers()
@@ -286,12 +333,15 @@ export function usePushProgress(
return {
pushing,
cancelling,
retryingAll,
wsProgressItems,
pushBatchId,
doneCount,
totalProgress,
failedRetryCount,
doPush,
cancelPush,
retryAllFailed,
dispose,
}
}
@@ -0,0 +1,208 @@
import { ref, watch, computed, type Ref } from 'vue'
import { http } from '@/api'
import { useSnackbar } from '@/composables/useSnackbar'
import type { StagingEntry, StagingPreviewResult, UploadedSource } from './types'
type BrowseLocalResponse = {
path: string
entries: StagingEntry[]
}
type ValidateSourcePathResponse = {
file_count: number
size_bytes: number
}
function joinHostPath(dir: string, name: string): string {
const base = dir.replace(/\/+$/, '')
return `${base}/${name}`
}
export function usePushStaging(uploadedSource: Ref<UploadedSource | null>) {
const snackbar = useSnackbar()
const loading = ref(false)
const currentPath = ref('')
const entries = ref<StagingEntry[]>([])
const previewOpen = ref(false)
const previewLoading = ref(false)
const previewResult = ref<StagingPreviewResult | null>(null)
const renameOpen = ref(false)
const renameTarget = ref<StagingEntry | null>(null)
const renameValue = ref('')
const renameLoading = ref(false)
const deleteOpen = ref(false)
const deleteTarget = ref<StagingEntry | null>(null)
const deleteLoading = ref(false)
const stagingRoot = () => uploadedSource.value?.sourcePath ?? ''
const relativePath = computed(() => {
const root = stagingRoot()
const cur = currentPath.value
if (!root || !cur || cur === root) return ''
return cur.startsWith(`${root}/`) ? cur.slice(root.length + 1) : cur
})
async function refreshStagingStats() {
const root = stagingRoot()
if (!root || !uploadedSource.value) return
try {
const res = await http.post<ValidateSourcePathResponse>('/sync/validate-source-path', { path: root })
uploadedSource.value = {
...uploadedSource.value,
fileCount: res.file_count,
size: res.size_bytes,
}
} catch {
// Stats refresh is best-effort after file ops
}
}
async function loadDirectory(path: string) {
if (!path) {
entries.value = []
currentPath.value = ''
return
}
loading.value = true
try {
const res = await http.post<BrowseLocalResponse>('/sync/browse-local', { path })
currentPath.value = res.path
entries.value = res.entries ?? []
} catch (e: unknown) {
entries.value = []
snackbar(e instanceof Error ? e.message : '加载暂存目录失败', 'error')
} finally {
loading.value = false
}
}
function goRoot() {
void loadDirectory(stagingRoot())
}
function refreshCurrent() {
void loadDirectory(currentPath.value || stagingRoot())
}
function openDirectory(entry: StagingEntry) {
if (!entry.is_dir || !currentPath.value) return
void loadDirectory(joinHostPath(currentPath.value, entry.name))
}
function openRename(entry: StagingEntry) {
renameTarget.value = entry
renameValue.value = entry.name
renameOpen.value = true
}
async function confirmRename() {
const entry = renameTarget.value
const newName = renameValue.value.trim()
if (!entry || !currentPath.value || !newName || newName === entry.name) {
renameOpen.value = false
return
}
renameLoading.value = true
try {
await http.post('/sync/local-file-ops', {
operation: 'rename',
path: joinHostPath(currentPath.value, entry.name),
new_name: newName,
})
snackbar('已重命名')
renameOpen.value = false
await loadDirectory(currentPath.value)
await refreshStagingStats()
} catch (e: unknown) {
snackbar(e instanceof Error ? e.message : '重命名失败', 'error')
} finally {
renameLoading.value = false
}
}
function openDelete(entry: StagingEntry) {
deleteTarget.value = entry
deleteOpen.value = true
}
async function confirmDelete() {
const entry = deleteTarget.value
if (!entry || !currentPath.value) {
deleteOpen.value = false
return
}
deleteLoading.value = true
try {
await http.post('/sync/local-file-ops', {
operation: 'delete',
path: joinHostPath(currentPath.value, entry.name),
})
snackbar('已删除')
deleteOpen.value = false
await loadDirectory(currentPath.value)
await refreshStagingStats()
} catch (e: unknown) {
snackbar(e instanceof Error ? e.message : '删除失败', 'error')
} finally {
deleteLoading.value = false
}
}
async function previewFile(entry: StagingEntry) {
if (entry.is_dir || !currentPath.value) return
previewOpen.value = true
previewLoading.value = true
previewResult.value = null
try {
previewResult.value = await http.post<StagingPreviewResult>('/sync/local-file-preview', {
path: joinHostPath(currentPath.value, entry.name),
})
} catch (e: unknown) {
previewOpen.value = false
snackbar(e instanceof Error ? e.message : '预览失败', 'error')
} finally {
previewLoading.value = false
}
}
watch(
() => uploadedSource.value?.sourcePath,
(path) => {
if (path) {
void loadDirectory(path)
} else {
entries.value = []
currentPath.value = ''
}
},
{ immediate: true },
)
return {
loading,
currentPath,
entries,
relativePath,
goRoot,
refreshCurrent,
openDirectory,
loadDirectory,
openRename,
confirmRename,
renameOpen,
renameValue,
renameLoading,
renameTarget,
openDelete,
confirmDelete,
deleteOpen,
deleteLoading,
deleteTarget,
previewFile,
previewOpen,
previewLoading,
previewResult,
}
}
+44 -8
View File
@@ -1,20 +1,51 @@
<template>
<v-container fluid class="pa-6">
<PushZipUpload
:uploaded-zip="form.uploadedZip"
:uploaded-source="form.uploadedSource"
:validated-source="form.validatedSource"
v-model:source-path="form.sourcePath"
:source-path-validating="form.sourcePathValidating"
:is-dragging="form.isDragging"
:zip-uploading="form.zipUploading"
:source-uploading="form.sourceUploading"
@dragover="form.isDragging = true"
@dragleave="form.isDragging = false"
@drop="form.handleZipDrop"
@file-select="form.handleZipSelect"
@clear="form.clearZipUpload"
@drop="form.handleFileDrop"
@file-select="form.handleFileSelect"
@clear="form.clearUploadedSource"
@clear-validated="form.clearValidatedSource"
@validate="form.validateSourcePath"
/>
<PushStagingPanel
v-if="form.uploadedSource"
:root-path="form.uploadedSource.sourcePath"
:relative-path="staging.relativePath"
:entries="staging.entries"
:loading="staging.loading"
v-model:rename-open="staging.renameOpen"
v-model:rename-value="staging.renameValue"
:rename-loading="staging.renameLoading"
v-model:delete-open="staging.deleteOpen"
:delete-target="staging.deleteTarget"
:delete-loading="staging.deleteLoading"
@refresh="staging.refreshCurrent()"
@go-root="staging.goRoot"
@open-dir="staging.openDirectory"
@preview="staging.previewFile"
@rename="staging.openRename"
@delete="staging.openDelete"
@confirm-rename="staging.confirmRename"
@confirm-delete="staging.confirmDelete"
/>
<PushStagingPreviewDialog
v-model="staging.previewOpen"
:loading="staging.previewLoading"
:result="staging.previewResult"
/>
<PushToolbar
v-model:source-path="form.sourcePath"
v-model:target-path="form.targetPath"
:zip-locked="!!form.uploadedZip"
:pushing="progress.pushing"
@preview="preview.doPreview"
@push="progress.doPush"
@@ -44,9 +75,12 @@
:batch-id="progress.pushBatchId"
:pushing="progress.pushing"
:cancelling="progress.cancelling"
:retrying-all="progress.retryingAll"
:failed-retry-count="progress.failedRetryCount"
:done-count="progress.doneCount"
:total-progress="progress.totalProgress"
@cancel="progress.cancelPush"
@retry-all="progress.retryAllFailed"
/>
<PushPreviewDialog
@@ -92,6 +126,8 @@
</template>
<script setup lang="ts">
import PushStagingPanel from '@/components/push/PushStagingPanel.vue'
import PushStagingPreviewDialog from '@/components/push/PushStagingPreviewDialog.vue'
import PushDiagnoseDialog from '@/components/push/PushDiagnoseDialog.vue'
import PushHistoryTable from '@/components/push/PushHistoryTable.vue'
import PushPreviewDialog from '@/components/push/PushPreviewDialog.vue'
@@ -102,5 +138,5 @@ import PushToolbar from '@/components/push/PushToolbar.vue'
import PushZipUpload from '@/components/push/PushZipUpload.vue'
import { usePushPage } from '@/composables/push'
const { form, servers, progress, preview, logs, onConfirmAndPush } = usePushPage()
const { form, staging, servers, progress, preview, logs, onConfirmAndPush } = usePushPage()
</script>
+1
View File
@@ -120,6 +120,7 @@ const ACTION_LABELS: Record<string, string> = {
local_file_rename: '重命名本地文件',
local_file_preview: '预览本地文件',
upload_zip: '上传并解压 ZIP',
upload_staging_files: '上传推送源文件',
diagnose: '服务器诊断',
chmod: '修改权限',
chown: '修改属主',
+136 -5
View File
@@ -871,10 +871,7 @@ async def _sftp_upload_one(
detail=f"文件大小超过限制 ({MAX_UPLOAD_FILE_SIZE // (1024 * 1024)}MB)",
)
filename = upload_file.filename or ""
safe_filename = filename.replace("/", "_").replace("\\", "_").strip()
if not safe_filename:
raise HTTPException(status_code=400, detail="文件名无效")
safe_filename = _safe_upload_basename(upload_file.filename or "")
try:
full_remote_path = remote_join(remote_dir, safe_filename)
@@ -993,9 +990,101 @@ async def upload_file(
}
# ── ZIP Upload & Extract (for push workflow) ──
# ── Push source upload (ZIP extract + plain files staging) ──
MAX_ZIP_FILE_SIZE = 524_288_000 # 500 MB
MAX_STAGING_TOTAL_SIZE = 524_288_000 # 500 MB per batch
def _safe_upload_basename(filename: str) -> str:
"""Strip path separators from an upload filename (Nexus host or remote SFTP)."""
safe = (filename or "").replace("/", "_").replace("\\", "_").strip()
if not safe or safe in (".", ".."):
raise HTTPException(status_code=400, detail="文件名无效")
return safe
def _unique_staging_name(name: str, used: set[str]) -> str:
if name not in used:
return name
base, dot, ext = name.rpartition(".")
if not dot:
base, ext = name, ""
n = 2
while True:
candidate = f"{base}_{n}.{ext}" if ext else f"{base}_{n}"
if candidate not in used:
return candidate
n += 1
async def _persist_staging_files(staging_dir: str, upload_items: list) -> dict:
"""Write multipart uploads into *staging_dir*; returns staging metadata."""
import os
import shutil
os.makedirs(staging_dir, exist_ok=True)
staging_base = resolve_nexus_host_path(staging_dir)
total_size = 0
file_count = 0
files_list: list[str] = []
used_names: set[str] = set()
files_truncated = False
try:
for upload_file in upload_items:
filename = upload_file.filename or ""
if filename.lower().endswith(".zip"):
raise HTTPException(status_code=400, detail="ZIP 请使用「上传 ZIP」或 /sync/upload-zip")
content = await upload_file.read()
if len(content) == 0:
raise HTTPException(status_code=400, detail=f"文件为空: {filename or '(未命名)'}")
if len(content) > MAX_UPLOAD_FILE_SIZE:
raise HTTPException(
status_code=400,
detail=f"文件大小超过限制 ({MAX_UPLOAD_FILE_SIZE // (1024 * 1024)}MB): {filename}",
)
total_size += len(content)
if total_size > MAX_STAGING_TOTAL_SIZE:
raise HTTPException(
status_code=400,
detail=f"本次上传总大小超过限制 ({MAX_STAGING_TOTAL_SIZE // (1024 * 1024)}MB)",
)
safe_name = _unique_staging_name(_safe_upload_basename(filename), used_names)
used_names.add(safe_name)
dest_path = posix_join(staging_dir, safe_name)
real_dest = resolve_nexus_host_path(dest_path)
if not is_path_under_prefix(real_dest, staging_base):
raise HTTPException(status_code=400, detail=f"非法文件名: {filename}")
with open(real_dest, "wb") as fh:
fh.write(content)
file_count += 1
if len(files_list) < 500:
files_list.append(safe_name)
else:
files_truncated = True
if file_count == 0:
raise HTTPException(status_code=400, detail="请选择至少一个文件")
return {
"source_path": staging_dir,
"file_count": file_count,
"files": files_list,
"files_truncated": files_truncated,
"size": total_size,
}
except HTTPException:
shutil.rmtree(staging_dir, ignore_errors=True)
raise
except Exception as exc:
shutil.rmtree(staging_dir, ignore_errors=True)
raise HTTPException(status_code=500, detail=f"上传失败: {exc}") from exc
@router.post("/upload-zip")
@@ -1122,6 +1211,48 @@ async def upload_zip(
raise HTTPException(status_code=500, detail=f"解压失败: {e}") from e
@router.post("/upload-files")
async def upload_staging_files(
request: Request,
admin: Admin = Depends(get_current_admin),
):
"""Upload one or more plain files to Nexus staging for rsync push.
Multipart form fields:
- file / files: UploadFile(s), non-.zip only (use upload-zip for archives)
Returns {"source_path": "/tmp/nexus_upload_xxx/", "file_count": N, ...}
"""
import uuid
form = await request.form()
upload_items = _collect_multipart_upload_files(form)
if not upload_items:
raise HTTPException(status_code=400, detail="请选择文件")
upload_id = uuid.uuid4().hex[:12]
staging_dir = f"/tmp/nexus_upload_{upload_id}"
meta = await _persist_staging_files(staging_dir, upload_items)
names = ", ".join(meta["files"][:3])
if meta["file_count"] > 3:
names += f"{meta['file_count']}"
await _audit_sync(
"upload_staging_files",
"sync",
0,
f"上传推送源文件: {names}{staging_dir} ({meta['size']} bytes)",
admin.username,
request,
)
return {
"success": True,
**meta,
}
# ── Post-Push Verification (sha256sum comparison) ──
@router.post("/verify")
+72
View File
@@ -0,0 +1,72 @@
"""Tests for push staging plain-file upload helpers."""
from __future__ import annotations
import pytest
from fastapi import HTTPException
from server.api.sync_v2 import (
_persist_staging_files,
_safe_upload_basename,
_unique_staging_name,
)
from server.utils.posix_paths import UPLOAD_STAGING_PREFIX
def test_safe_upload_basename_strips_path():
assert _safe_upload_basename("foo/bar.txt") == "foo_bar.txt"
assert _safe_upload_basename(" ok.php ") == "ok.php"
def test_safe_upload_basename_rejects_invalid():
with pytest.raises(HTTPException) as exc:
_safe_upload_basename("")
assert exc.value.status_code == 400
with pytest.raises(HTTPException):
_safe_upload_basename(".")
def test_unique_staging_name():
used = {"a.txt"}
assert _unique_staging_name("a.txt", used) == "a_2.txt"
used.add("a_2.txt")
assert _unique_staging_name("a.txt", used) == "a_3.txt"
@pytest.mark.asyncio
async def test_persist_staging_files_writes_to_staging(tmp_path):
staging = str(tmp_path / "nexus_upload_abc")
class FakeUpload:
def __init__(self, name: str, data: bytes):
self.filename = name
self._data = data
async def read(self):
return self._data
items = [
FakeUpload("a.txt", b"hello"),
FakeUpload("b.txt", b"world"),
]
meta = await _persist_staging_files(staging, items)
assert meta["file_count"] == 2
assert meta["size"] == 10
assert (tmp_path / "nexus_upload_abc" / "a.txt").read_bytes() == b"hello"
assert meta["source_path"] == staging
@pytest.mark.asyncio
async def test_persist_staging_files_rejects_zip():
staging = f"{UPLOAD_STAGING_PREFIX}test"
class FakeZip:
filename = "pkg.zip"
async def read(self):
return b"PK"
with pytest.raises(HTTPException) as exc:
await _persist_staging_files(staging, [FakeZip()])
assert "upload-zip" in exc.value.detail
+1
View File
@@ -0,0 +1 @@
import{A as e,F as t,N as n,Vr as r,Z as i,Zn as a,ar as o,er as s,gr as c,jr as l,k as u,mr as d,or as f,rr as p,sr as m,tr as h,vr as g,w as _,wr as v,zt as y}from"./index-AXqGiRqu.js";import{o as b,t as x}from"./VContainer-CXLvz-1q.js";import{t as S}from"./VChip-C9gQpplu.js";import{t as C}from"./VSelect-BzkPFBem.js";import{t as w}from"./VDataTableServer-Bbieiuh4.js";import{n as T,t as E}from"./VRow-D7EirQ9k.js";import{t as D}from"./VSpacer-B0VjZ5QA.js";var O={class:`font-weight-medium`},k={class:`text-body-2`},A={class:`text-caption text-medium-emphasis`},j=m({__name:`AlertsPage`,setup(m){let j=b(),M=l(!1),N=l([]),P=l(0),F=l(1),I=l(null),L=l(null),R=l([{label:`今日告警`,value:`0`,color:`blue`,icon:`mdi-alert`},{label:`活跃告警`,value:`0`,color:`orange`,icon:`mdi-bell-ring`},{label:`已恢复`,value:`0`,color:`green`,icon:`mdi-check-circle`},{label:`Top 服务器`,value:``,color:`red`,icon:`mdi-server`}]),z=[{label:`CPU`,value:`cpu`},{label:`内存`,value:`memory`},{label:`磁盘`,value:`disk`},{label:`连接`,value:`connection`}],B=[{label:`活跃`,value:`active`},{label:`已恢复`,value:`recovered`}],V=[{title:`类型`,key:`alert_type`,width:100},{title:`服务器`,key:`server_name`},{title:`详情`,key:`value`},{title:`状态`,key:`is_recovery`,width:100},{title:`时间`,key:`created_at`,width:160}];async function H(){try{let e=await y.get(`/alert-history/stats`);R.value[0].value=String(e.today||0),R.value[1].value=String(e.active||0),R.value[2].value=String(e.recovered||0),R.value[3].value=e.top_server||``}catch{j(`加载统计失败`,`error`)}}async function U(){M.value=!0;try{let e=await y.get(`/alert-history/`,{page:F.value,per_page:20,type:I.value||void 0,status:L.value||void 0});N.value=e.items||[],P.value=e.total||0}catch{N.value=[]}finally{M.value=!1}}function W(e){return e===`cpu`||e===`CPU`?`error`:e===`memory`||e===`内存`?`warning`:e===`disk`||e===`磁盘`?`info`:`primary`}function G(e){return e===`cpu`||e===`CPU`?`mdi-chip`:e===`memory`||e===`内存`?`mdi-memory`:e===`disk`||e===`磁盘`?`mdi-harddisk`:`mdi-lan-disconnect`}return d(()=>{H(),U()}),(l,d)=>(c(),h(x,{fluid:``,class:`pa-6`},{default:v(()=>[f(E,{class:`mb-4`},{default:v(()=>[(c(!0),p(a,null,g(R.value,t=>(c(),h(T,{cols:`12`,sm:`6`,lg:`3`,key:t.label},{default:v(()=>[f(_,{elevation:`0`,lines:`two`,rounded:`lg`,border:``},{default:v(()=>[f(u,null,{append:v(()=>[f(i,{color:t.color,size:`30`},{default:v(()=>[o(r(t.icon),1)]),_:2},1032,[`color`])]),default:v(()=>[f(e,{class:`text-body-small`},{default:v(()=>[o(r(t.label),1)]),_:2},1024),f(e,null,{default:v(()=>[o(r(t.value),1)]),_:2},1024)]),_:2},1024)]),_:2},1024)]),_:2},1024))),128))]),_:1}),f(n,{elevation:`0`,border:``,rounded:`lg`},{default:v(()=>[f(t,{class:`d-flex align-center`},{default:v(()=>[d[5]||=o(` 告警历史 `,-1),f(D),f(C,{modelValue:I.value,"onUpdate:modelValue":[d[0]||=e=>I.value=e,d[1]||=e=>{F.value=1,U()}],items:z,"item-title":`label`,"item-value":`value`,label:`类型`,variant:`outlined`,density:`compact`,"hide-details":``,style:{"max-width":`140px`},clearable:``},null,8,[`modelValue`]),f(C,{modelValue:L.value,"onUpdate:modelValue":[d[2]||=e=>L.value=e,d[3]||=e=>{F.value=1,U()}],items:B,"item-title":`label`,"item-value":`value`,label:`状态`,variant:`outlined`,density:`compact`,"hide-details":``,style:{"max-width":`140px`},class:`ml-3`,clearable:``},null,8,[`modelValue`])]),_:1}),f(w,{items:N.value,headers:V,"items-length":P.value,loading:M.value,page:F.value,"items-per-page":20,hover:``,density:`comfortable`,"onUpdate:page":d[4]||=e=>{F.value=e,U()}},{"item.alert_type":v(({item:e})=>[f(S,{color:W(e.alert_type),size:`small`,variant:`tonal`,label:``},{prepend:v(()=>[f(i,{size:`12`},{default:v(()=>[o(r(G(e.alert_type)),1)]),_:2},1024)]),default:v(()=>[o(` `+r(e.alert_type),1)]),_:2},1032,[`color`])]),"item.server_name":v(({item:e})=>[s(`span`,O,r(e.server_name),1)]),"item.value":v(({item:e})=>[s(`span`,k,r(e.value),1)]),"item.is_recovery":v(({item:e})=>[e.is_recovery?(c(),h(S,{key:0,color:`success`,size:`small`,variant:`tonal`,label:``},{default:v(()=>[...d[6]||=[o(`已恢复`,-1)]]),_:1})):(c(),h(S,{key:1,color:`error`,size:`small`,variant:`tonal`,label:``},{default:v(()=>[...d[7]||=[o(`告警中`,-1)]]),_:1}))]),"item.created_at":v(({item:e})=>[s(`span`,A,r(e.created_at),1)]),"no-data":v(()=>[...d[8]||=[s(`div`,{class:`text-center text-medium-emphasis py-6`},`暂无数据`,-1)]]),_:1},8,[`items`,`items-length`,`loading`,`page`])]),_:1})]),_:1}))}});export{j as default};
+1
View File
@@ -0,0 +1 @@
import{F as e,Lr as t,N as n,Vr as r,ar as i,er as a,gr as o,jr as s,mr as c,or as l,sr as u,t as d,tr as f,wr as p,zt as m}from"./index-AXqGiRqu.js";import{o as h,t as g}from"./VContainer-CXLvz-1q.js";import{t as _}from"./VChip-C9gQpplu.js";import{t as v}from"./VSelect-BzkPFBem.js";import{t as y}from"./VDataTableServer-Bbieiuh4.js";import{t as b}from"./VSpacer-B0VjZ5QA.js";import{a as x,n as S,r as C,t as w}from"./auditNormalize-bmSA0RyN.js";var T={class:`font-weight-medium`},E={class:`text-body-2`},D={class:`text-body-2 text-medium-emphasis text-truncate`,style:{"max-width":`300px`,display:`inline-block`}},O={class:`text-caption text-medium-emphasis`},k={class:`text-caption text-medium-emphasis`},A=u({__name:`AuditPage`,setup(u){let A=h(),j=s(!1),M=s([]),N=s(0),P=s(1),F=s(null),I=s(``),L=s(``),R=S,z=[{title:`操作`,key:`action`,width:100},{title:`用户`,key:`admin_username`,width:100},{title:`目标`,key:`target`,width:140},{title:`详情`,key:`detail`},{title:`IP`,key:`ip_address`,width:120},{title:`时间`,key:`created_at`,width:160}];async function B(){j.value=!0;try{let e=(P.value-1)*50,t=await m.get(`/audit/`,{limit:50,offset:e,action:F.value||void 0,admin_username:I.value||void 0,date_from:L.value||void 0});M.value=w(t.items||[]),N.value=t.total||0}catch{M.value=[],A(`加载审计日志失败`,`error`)}finally{j.value=!1}}function V(e){return e.startsWith(`create`)||e===`login`?`success`:e.startsWith(`update`)?`info`:e.startsWith(`delete`)||e===`logout`?`error`:e.includes(`execute`)||e.includes(`push`)||e.includes(`retry`)?`warning`:`primary`}return c(()=>B()),(s,c)=>(o(),f(g,{fluid:``,class:`pa-6`},{default:p(()=>[l(n,{elevation:`0`,border:``,rounded:`lg`},{default:p(()=>[l(e,{class:`d-flex align-center`},{default:p(()=>[c[7]||=i(` 审计日志 `,-1),l(b),l(v,{modelValue:F.value,"onUpdate:modelValue":[c[0]||=e=>F.value=e,c[1]||=e=>{P.value=1,B()}],items:t(R),"item-title":`label`,"item-value":`value`,label:`操作类型`,variant:`outlined`,density:`compact`,"hide-details":``,style:{"max-width":`200px`},clearable:``},null,8,[`modelValue`,`items`]),l(d,{modelValue:I.value,"onUpdate:modelValue":[c[2]||=e=>I.value=e,c[3]||=e=>{P.value=1,B()}],"prepend-inner-icon":`mdi-account`,label:`用户`,variant:`outlined`,density:`compact`,"hide-details":``,style:{"max-width":`140px`},class:`ml-3`,clearable:``},null,8,[`modelValue`]),l(d,{modelValue:L.value,"onUpdate:modelValue":[c[4]||=e=>L.value=e,c[5]||=e=>{P.value=1,B()}],"prepend-inner-icon":`mdi-calendar`,label:`日期`,variant:`outlined`,density:`compact`,"hide-details":``,style:{"max-width":`160px`},class:`ml-3`,type:`date`,clearable:``},null,8,[`modelValue`])]),_:1}),l(y,{items:M.value,headers:z,"items-length":N.value,loading:j.value,page:P.value,"items-per-page":50,hover:``,density:`compact`,"onUpdate:page":c[6]||=e=>{P.value=e,B()}},{"item.action":p(({item:e})=>[l(_,{color:V(e.action),size:`small`,variant:`tonal`,label:``},{default:p(()=>[i(r(t(C)(e.action)),1)]),_:2},1032,[`color`])]),"item.admin_username":p(({item:e})=>[a(`span`,T,r(e.admin_username),1)]),"item.target":p(({item:e})=>[a(`span`,E,r(t(x)(e.resource_type))+r(e.resource_id?` #${e.resource_id}`:``),1)]),"item.detail":p(({item:e})=>[a(`span`,D,r(e.detail||``),1)]),"item.ip_address":p(({item:e})=>[a(`span`,O,r(e.ip_address||``),1)]),"item.created_at":p(({item:e})=>[a(`span`,k,r(e.created_at),1)]),"no-data":p(()=>[...c[8]||=[a(`div`,{class:`text-center text-medium-emphasis py-6`},`暂无数据`,-1)]]),_:1},8,[`items`,`items-length`,`loading`,`page`])]),_:1})]),_:1}))}});export{A as default};
+1
View File
@@ -0,0 +1 @@
import{F as e,Lr as t,M as n,N as r,Vr as i,ar as a,er as o,gr as s,jr as c,mr as l,or as u,sr as d,tr as f,wr as p,zt as m}from"./index-AXqGiRqu.js";import{o as h,t as g}from"./VContainer-CXLvz-1q.js";import{t as _}from"./VChip-C9gQpplu.js";import{t as v}from"./VSelect-BzkPFBem.js";import{t as y}from"./VDataTableServer-Bbieiuh4.js";import{t as b}from"./VSpacer-B0VjZ5QA.js";import{t as x}from"./apiError-Cfu1CDNq.js";import{t as S}from"./useServerList-COeZgQOK.js";var C={class:`text-body-2`},w={class:`text-medium-emphasis`},T={class:`text-medium-emphasis`},E={class:`text-caption text-medium-emphasis`},D={class:`font-weight-medium`},O={class:`text-caption text-medium-emphasis`},k=d({__name:`CommandsPage`,setup(d){let k=h(),{servers:A,loadServers:j}=S(),M=c(!1),N=c(`commands`),P=c(1),F=c(0),I=c(30),L=c(null),R=c([]),z=c([]),B=[{title:`命令`,key:`command`},{title:`服务器`,key:`server_name`},{title:`用户`,key:`admin_username`,width:100},{title:`时间`,key:`created_at`,width:160}],V=[{title:`服务器`,key:`server_name`},{title:`状态`,key:`status`,width:80},{title:`来源`,key:`remote_addr`,width:120},{title:`开始时间`,key:`started_at`,width:160}];async function H(){M.value=!0;try{if(N.value===`commands`){let e=await m.getList(`/assets/command-logs`,{server_id:L.value||void 0,page:P.value,per_page:I.value});R.value=e.items,F.value=e.total}else{let e=await m.getList(`/assets/ssh-sessions`,{server_id:L.value||void 0,page:P.value,per_page:I.value});z.value=e.items,F.value=e.total}}catch(e){R.value=[],z.value=[],k(x(e,`加载命令日志失败`),`error`)}finally{M.value=!1}}return l(()=>{j(),H()}),(c,l)=>(s(),f(g,{fluid:``,class:`pa-6`},{default:p(()=>[u(r,{elevation:`0`,border:``,rounded:`lg`},{default:p(()=>[u(e,{class:`d-flex align-center`},{default:p(()=>[l[4]||=a(` 命令日志 `,-1),u(b),u(v,{modelValue:N.value,"onUpdate:modelValue":[l[0]||=e=>N.value=e,H],items:[{title:`命令视图`,value:`commands`},{title:`会话视图`,value:`sessions`}],"item-title":`title`,"item-value":`value`,variant:`outlined`,density:`compact`,"hide-details":``,style:{"max-width":`160px`}},null,8,[`modelValue`]),u(v,{modelValue:L.value,"onUpdate:modelValue":[l[1]||=e=>L.value=e,H],items:t(A),"item-title":`name`,"item-value":`id`,label:`服务器`,variant:`outlined`,density:`compact`,"hide-details":``,style:{"max-width":`180px`},class:`ml-3`,clearable:``},null,8,[`modelValue`,`items`])]),_:1}),u(n),N.value===`commands`?(s(),f(y,{key:0,items:R.value,headers:B,"items-length":F.value,loading:M.value,page:P.value,"items-per-page":30,hover:``,density:`compact`,"onUpdate:page":l[2]||=e=>{P.value=e,H()}},{"item.command":p(({item:e})=>[o(`code`,C,i(e.command),1)]),"item.server_name":p(({item:e})=>[o(`span`,w,i(e.server_name||``),1)]),"item.admin_username":p(({item:e})=>[o(`span`,T,i(e.admin_username||``),1)]),"item.created_at":p(({item:e})=>[o(`span`,E,i(e.created_at),1)]),"no-data":p(()=>[...l[5]||=[o(`div`,{class:`text-center text-medium-emphasis py-6`},`暂无数据`,-1)]]),_:1},8,[`items`,`items-length`,`loading`,`page`])):(s(),f(y,{key:1,items:z.value,headers:V,"items-length":F.value,loading:M.value,page:P.value,"items-per-page":30,hover:``,density:`compact`,"onUpdate:page":l[3]||=e=>{P.value=e,H()}},{"item.server_name":p(({item:e})=>[o(`span`,D,i(e.server_name||``),1)]),"item.status":p(({item:e})=>[u(_,{color:e.status===`closed`?`grey`:`success`,size:`small`,variant:`tonal`,label:``},{default:p(()=>[a(i(e.status),1)]),_:2},1032,[`color`])]),"item.started_at":p(({item:e})=>[o(`span`,O,i(e.started_at),1)]),"no-data":p(()=>[...l[6]||=[o(`div`,{class:`text-center text-medium-emphasis py-6`},`暂无数据`,-1)]]),_:1},8,[`items`,`items-length`,`loading`,`page`]))]),_:1})]),_:1}))}});export{k as default};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
import{$n as e,Br as t,Ft as n,Ht as r,It as i,Lt as a,N as o,Sr as s,Ut as c,Vr as l,Xn as u,ar as d,c as f,er as p,fn as m,gr as h,hr as g,jr as _,l as v,ln as y,mr as b,nr as x,or as S,pn as C,rn as w,rr as T,sr as E,t as D,tr as O,v as k,wr as A,z as j,zr as M,zt as N}from"./index-AXqGiRqu.js";import{t as P}from"./VChip-C9gQpplu.js";import{t as F}from"./_plugin-vue_export-helper-BDNMzG2s.js";import{t as I}from"./VAlert-DCVKTXpe.js";var L=C({...m(),...v()},`VForm`),R=y()({name:`VForm`,props:L(),emits:{"update:modelValue":e=>!0,submit:e=>!0},setup(e,{slots:n,emit:r}){let i=f(e),a=_();function o(e){e.preventDefault(),i.reset()}function s(e){let t=e,n=i.validate();t.then=n.then.bind(n),t.catch=n.catch.bind(n),t.finally=n.finally.bind(n),r(`submit`,t),t.defaultPrevented||n.then(({valid:e})=>{e&&a.value?.submit()}),t.preventDefault()}return w(()=>p(`form`,{ref:a,class:M([`v-form`,e.class]),style:t(e.style),novalidate:!0,onReset:o,onSubmit:s},[n.default?.({get errors(){return i.errors.value},get isDisabled(){return i.isDisabled.value},get isReadonly(){return i.isReadonly.value},get isValidating(){return i.isValidating.value},get isValid(){return i.isValid.value},get items(){return i.items.value},validate:i.validate,reset:i.reset,resetValidation:i.resetValidation})])),k(i,a)}}),z={class:`login-page`},B={class:`login-center`},V={key:2,class:`text-center mt-4`},H=F(E({__name:`LoginPage`,setup(f){let m=r(),v=c(),y=n(),C=_(``),w=_(``),E=_(``),k=_(!1),M=_(!1),F=_(``),L=_(!1),H=_(0),U=_(null),W=_([]),G=_(0),K=null,q=e(()=>W.value.length===0?``:W.value[G.value]);async function J(){try{let e=await N.get(`/settings/bing-wallpapers`);e?.urls?.length&&(W.value=e.urls,G.value=Math.floor(Math.random()*e.urls.length))}catch{}}function Y(){K&&clearInterval(K),!(W.value.length<=1)&&(K=setInterval(()=>{G.value=(G.value+1)%W.value.length},36e5))}b(async()=>{await J(),Y()}),g(()=>{K&&clearInterval(K),Z&&clearInterval(Z)});let X=_(Date.now()),Z=null,Q=e(()=>{if(!U.value)return 0;let e=new Date(U.value).getTime()-X.value;return Math.max(0,Math.ceil(e/6e4))});s(U,e=>{Z&&=(clearInterval(Z),null),e&&(Z=setInterval(()=>{X.value=Date.now()},3e4))});async function $(){if(!C.value||!w.value){F.value=`请输入用户名和密码`;return}M.value=!0,F.value=``;try{await y.login(C.value,w.value,L.value?E.value:void 0);let e=m.query.redirect||`/`,t=e.startsWith(`/`)&&!e.startsWith(`//`)?e:`/`;v.push(t)}catch(e){e instanceof a?(L.value=!0,F.value=e.message):e instanceof i?e.status===429?(U.value=new Date(Date.now()+900*1e3).toISOString(),F.value=`登录尝试过多,账户已锁定 15 分钟`):(H.value++,F.value=e.message):F.value=`网络错误,请重试`}finally{M.value=!1}}return(e,n)=>(h(),T(`div`,z,[p(`div`,{class:`wallpaper-bg`,style:t({backgroundImage:`url(${q.value})`})},null,4),n[6]||=p(`div`,{class:`wallpaper-overlay`},null,-1),p(`div`,B,[S(o,{elevation:`0`,rounded:`xl`,class:`login-card pa-10`,border:``},{default:A(()=>[F.value?(h(),O(I,{key:0,type:`error`,density:`compact`,class:`mb-4`,closable:``,"onClick:close":n[0]||=e=>F.value=``,rounded:`lg`,variant:`tonal`},{default:A(()=>[d(l(F.value),1)]),_:1})):x(``,!0),U.value?(h(),O(I,{key:1,type:`warning`,density:`compact`,class:`mb-4`,rounded:`lg`,variant:`tonal`},{default:A(()=>[d(` 账户已锁定,请 `+l(Q.value)+` 分钟后重试 `,1)]),_:1})):x(``,!0),S(R,{onSubmit:u($,[`prevent`])},{default:A(()=>[S(D,{modelValue:C.value,"onUpdate:modelValue":n[1]||=e=>C.value=e,label:`用户名`,variant:`outlined`,density:`comfortable`,"prepend-inner-icon":`mdi-account-outline`,disabled:M.value,class:`mb-4`,autocomplete:`username`,rounded:`lg`,"hide-details":`auto`},null,8,[`modelValue`,`disabled`]),S(D,{modelValue:w.value,"onUpdate:modelValue":n[2]||=e=>w.value=e,label:`密码`,variant:`outlined`,density:`comfortable`,"prepend-inner-icon":`mdi-lock-outline`,"append-inner-icon":k.value?`mdi-eye`:`mdi-eye-off`,type:k.value?`text`:`password`,"onClick:appendInner":n[3]||=e=>k.value=!k.value,disabled:M.value,class:`mb-4`,autocomplete:`current-password`,rounded:`lg`,"hide-details":`auto`},null,8,[`modelValue`,`append-inner-icon`,`type`,`disabled`]),L.value?(h(),O(D,{key:0,modelValue:E.value,"onUpdate:modelValue":n[4]||=e=>E.value=e,label:`TOTP 验证码`,variant:`outlined`,density:`comfortable`,"prepend-inner-icon":`mdi-shield-key-outline`,disabled:M.value,class:`mb-4`,autocomplete:`one-time-code`,rounded:`lg`,"hide-details":`auto`},null,8,[`modelValue`,`disabled`])):x(``,!0),S(j,{type:`submit`,color:`primary`,variant:`flat`,block:``,size:`large`,loading:M.value,disabled:!!U.value,rounded:`lg`,class:`mt-2`,height:`48`},{default:A(()=>[...n[5]||=[d(` 登录 `,-1)]]),_:1},8,[`loading`,`disabled`])]),_:1}),H.value>0&&!U.value?(h(),T(`div`,V,[S(P,{size:`small`,variant:`tonal`,color:`warning`,label:``},{default:A(()=>[d(` 已失败 `+l(H.value)+``,1)]),_:1})])):x(``,!0)]),_:1})])]))}}),[[`__scopeId`,`data-v-c0c6156f`]]);export{H as default};
+1
View File
@@ -0,0 +1 @@
.cursor-pointer[data-v-453c83f9]{cursor:pointer}.staging-preview-body[data-v-adb4800a]{background:rgba(var(--v-theme-on-surface), .04);white-space:pre-wrap;word-break:break-all;border-radius:8px;max-height:420px;margin:0;padding:12px;font-size:12px;line-height:1.5;overflow:auto}@layer vuetify-components{.v-radio-group>.v-input__control{flex-direction:column}.v-radio-group>.v-input__control>.v-label{margin-inline-start:16px}.v-radio-group>.v-input__control>.v-label+.v-selection-control-group{margin-top:8px;padding-inline-start:6px}}
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
import{F as e,L as t,N as n,P as r,Vr as i,ar as a,er as o,gr as s,jr as c,mr as l,nr as u,or as d,sr as f,tr as p,wr as m,z as h,zt as g}from"./index-AXqGiRqu.js";import{o as _,t as v}from"./VContainer-CXLvz-1q.js";import{t as y}from"./VChip-C9gQpplu.js";import{t as b}from"./VSelect-BzkPFBem.js";import{t as x}from"./VDataTableServer-Bbieiuh4.js";import{t as S}from"./VSpacer-B0VjZ5QA.js";import{t as C}from"./VDialog-BdMt4zWB.js";var w={class:`font-weight-medium text-truncate d-inline-block`,style:{"max-width":`240px`}},T={class:`text-medium-emphasis`},E={class:`text-medium-emphasis`},D={class:`text-caption text-medium-emphasis`},O={class:`text-caption text-medium-emphasis`},k=f({__name:`RetriesPage`,setup(f){let k=_(),A=c(!1),j=c([]),M=c(0),N=c(1),P=c(null),F=c(!1),I=c(null),L=[{label:`待重试`,value:`pending`},{label:`重试中`,value:`retrying`},{label:`成功`,value:`success`},{label:`失败`,value:`failed`}],R=[{title:`状态`,key:`status`,width:100},{title:`任务`,key:`operation`},{title:`服务器`,key:`server_name`},{title:`次数`,key:`retry_count`,width:100},{title:`下次重试`,key:`next_retry_at`,width:140},{title:`创建时间`,key:`created_at`,width:160},{title:``,key:`actions`,width:120,align:`end`,sortable:!1}];async function z(){A.value=!0;try{let e=(await g.getList(`/retries/`,{limit:200})).items||[];P.value&&(e=e.filter(e=>e.status===P.value)),M.value=e.length;let t=(N.value-1)*20;j.value=e.slice(t,t+20)}catch(e){j.value=[],M.value=0,k(e instanceof Error?e.message:`加载重试队列失败`,`error`)}finally{A.value=!1}}async function B(e){try{await g.post(`/retries/${e.id}/retry`),z(),k(`重试已触发`)}catch(e){k(e.message||`操作失败`,`error`)}}function V(e){I.value=e.id,F.value=!0}async function H(){if(I.value)try{await g.delete(`/retries/${I.value}`),F.value=!1,z(),k(`已删除`)}catch(e){k(e.message||`删除失败`,`error`)}}function U(e){return e===`success`?`success`:e===`failed`?`error`:e===`retrying`?`warning`:`info`}function W(e){return e===`pending`?`待重试`:e===`retrying`?`重试中`:e===`success`?`成功`:e===`failed`?`失败`:e}function G(e){return e.source_path&&e.target_path?`${e.source_path}${e.target_path}`:e.source_path||e.target_path||`文件推送`}return l(()=>z()),(c,l)=>(s(),p(v,{fluid:``,class:`pa-6`},{default:m(()=>[d(n,{elevation:`0`,border:``,rounded:`lg`},{default:m(()=>[d(e,{class:`d-flex align-center text-h6`},{default:m(()=>[l[5]||=a(` 重试队列 `,-1),d(S),d(b,{modelValue:P.value,"onUpdate:modelValue":[l[0]||=e=>P.value=e,l[1]||=e=>{N.value=1,z()}],items:L,"item-title":`label`,"item-value":`value`,label:`状态筛选`,variant:`outlined`,density:`compact`,"hide-details":``,style:{"max-width":`160px`},clearable:``},null,8,[`modelValue`])]),_:1}),d(x,{items:j.value,headers:R,"items-length":M.value,loading:A.value,page:N.value,"items-per-page":20,hover:``,density:`comfortable`,"onUpdate:page":l[2]||=e=>{N.value=e,z()}},{"item.status":m(({item:e})=>[d(y,{color:U(e.status),size:`x-small`,variant:`tonal`,label:``,border:`sm`},{default:m(()=>[a(i(W(e.status)),1)]),_:2},1032,[`color`])]),"item.operation":m(({item:e})=>[o(`span`,w,i(G(e)),1)]),"item.server_name":m(({item:e})=>[o(`span`,T,i(e.server_name||``),1)]),"item.retry_count":m(({item:e})=>[o(`span`,E,i(e.retry_count||0)+` / `+i(e.max_retries||3),1)]),"item.next_retry_at":m(({item:e})=>[o(`span`,D,i(e.next_retry_at||``),1)]),"item.created_at":m(({item:e})=>[o(`span`,O,i(e.created_at),1)]),"item.actions":m(({item:e})=>[e.status===`pending`||e.status===`failed`?(s(),p(h,{key:0,variant:`text`,size:`x-small`,color:`primary`,density:`compact`,onClick:t=>B(e)},{default:m(()=>[...l[6]||=[a(` 重试 `,-1)]]),_:1},8,[`onClick`])):u(``,!0),d(h,{variant:`text`,size:`x-small`,color:`error`,density:`compact`,onClick:t=>V(e)},{default:m(()=>[...l[7]||=[a(` 删除 `,-1)]]),_:1},8,[`onClick`])]),"no-data":m(()=>[...l[8]||=[o(`div`,{class:`text-center text-medium-emphasis py-6`},`暂无数据`,-1)]]),_:1},8,[`items`,`items-length`,`loading`,`page`])]),_:1}),d(C,{modelValue:F.value,"onUpdate:modelValue":l[4]||=e=>F.value=e,"max-width":`400`},{default:m(()=>[d(n,{border:``},{default:m(()=>[d(e,null,{default:m(()=>[...l[9]||=[a(`确认删除`,-1)]]),_:1}),d(r,null,{default:m(()=>[...l[10]||=[a(`确定要删除此重试任务吗?`,-1)]]),_:1}),d(t,null,{default:m(()=>[d(S),d(h,{variant:`text`,onClick:l[3]||=e=>F.value=!1},{default:m(()=>[...l[11]||=[a(`取消`,-1)]]),_:1}),d(h,{color:`error`,variant:`flat`,onClick:H},{default:m(()=>[...l[12]||=[a(`删除`,-1)]]),_:1})]),_:1})]),_:1})]),_:1},8,[`modelValue`])]),_:1}))}});export{k as default};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
import{N as e,P as t,R as n,Vr as r,Z as i,Zn as a,er as o,gr as s,hr as c,jr as l,or as u,rr as d,sr as f,tr as p,vr as m,wr as h,zr as g,zt as _}from"./index-AXqGiRqu.js";import{n as v,t as y}from"./VRow-D7EirQ9k.js";import{t as b}from"./_plugin-vue_export-helper-BDNMzG2s.js";function x(){let e=l([]),t=l(!1),n=l(1),r=l(20),i=l(0),a=l(``);async function o(){t.value=!0;try{let t=await _.get(`/servers/`,{page:n.value,per_page:r.value,search:a.value||void 0});e.value=t.items||[],i.value=t.total||0}catch(t){throw e.value=[],i.value=0,t}finally{t.value=!1}}let s;c(()=>clearTimeout(s));function u(){clearTimeout(s),s=setTimeout(()=>{n.value=1,o()},300)}function d(e){n.value=e,o()}function f(e){r.value=e,n.value=1,o()}return{servers:e,loading:t,page:n,itemsPerPage:r,total:i,search:a,loadServers:o,onSearch:u,onPageChange:d,onItemsPerPageChange:f}}function S(e){switch(e){case`online`:return`success`;case`offline`:return`error`;default:return`warning`}}function C(e){switch(e){case`online`:return`在线`;case`offline`:return`离线`;default:return`未知`}}function w(e){if(!e)return``;let t=new Date(e);if(isNaN(t.getTime()))return``;let n=Date.now()-t.getTime();return n<6e4?`刚刚`:n<36e5?`${Math.floor(n/6e4)} 分钟前`:n<864e5?`${Math.floor(n/36e5)} 小时前`:`${Math.floor(n/864e5)} 天前`}var T={class:`text-caption text-medium-emphasis mb-1`,style:{"font-size":`14px`,"letter-spacing":`0.5px`,"text-transform":`uppercase`}},E=b(f({__name:`StatCardsRow`,props:{items:{}},setup(c){return(l,f)=>(s(),p(y,null,{default:h(()=>[(s(!0),d(a,null,m(c.items,(a,c)=>(s(),p(v,{key:c,cols:`6`,sm:`3`},{default:h(()=>[u(e,{elevation:`0`,rounded:`xl`,border:``,class:`stat-card`},{default:h(()=>[u(t,{class:`pa-4 d-flex align-center justify-space-between`},{default:h(()=>[o(`div`,null,[o(`div`,T,r(a.subtitle),1),o(`div`,{class:g([`font-weight-bold`,`text-`+a.color]),style:{"font-size":`36px`,"line-height":`1.1`}},r(a.title),3)]),u(n,{color:a.color,size:`48`,rounded:`lg`,class:`stat-icon`},{default:h(()=>[u(i,{icon:a.icon,size:`24`,color:`white`},null,8,[`icon`])]),_:2},1032,[`color`])]),_:2},1024)]),_:2},1024)]),_:2},1024))),128))]),_:1}))}}),[[`__scopeId`,`data-v-68b984da`]]);export{x as a,C as i,w as n,S as r,E as t};
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
import{$n as e,Br as t,Ct as n,Et as r,G as i,Gt as a,Kt as o,Mt as s,Pr as c,Qt as l,Tt as u,W as d,Z as f,at as p,bt as m,ct as h,dt as g,en as _,er as v,fn as y,ft as b,ln as x,lt as S,mt as C,nn as w,or as T,ot as E,pn as D,pt as O,sn as k,st as A,ur as j,wt as M,xt as N,z as P,zr as F}from"./index-AXqGiRqu.js";var I=k(`v-alert-title`),L=D({iconSize:[Number,String],iconSizes:{type:Array,default:()=>[[`x-small`,10],[`small`,16],[`default`,24],[`large`,28],[`x-large`,32]]}},`iconSize`);function R(t,n){return{iconSize:e(()=>{let e=new Map(t.iconSizes),r=t.iconSize??n()??`default`;return e.has(r)?e.get(r):r})}}var z=[`success`,`info`,`warning`,`error`],B=D({border:{type:[Boolean,String],validator:e=>typeof e==`boolean`||[`top`,`end`,`bottom`,`start`].includes(e)},borderColor:String,closable:Boolean,closeIcon:{type:w,default:`$close`},closeLabel:{type:String,default:`$vuetify.close`},icon:{type:[Boolean,String,Function,Object],default:null},modelValue:{type:Boolean,default:!0},prominent:Boolean,title:String,text:String,type:{type:String,validator:e=>z.includes(e)},...y(),...h(),...M(),...O(),...L(),...g(),...d(),...m(),...s(),...a(),...E({variant:`flat`})},`VAlert`),V=x()({name:`VAlert`,props:B(),emits:{"click:close":e=>!0,"update:modelValue":e=>!0},setup(e,{emit:a,slots:s}){let d=_(e,`modelValue`),m=c(()=>{if(e.icon!==!1)return e.type?e.icon??`$${e.type}`:e.icon}),{iconSize:h}=R(e,()=>e.prominent?44:void 0),{themeClasses:g}=o(e),{colorClasses:y,colorStyles:x,variantClasses:w}=A(()=>({color:e.color??e.type,variant:e.variant})),{densityClasses:E}=S(e),{dimensionStyles:D}=u(e),{elevationClasses:O}=C(e),{locationStyles:k}=b(e),{positionClasses:M}=i(e),{roundedClasses:L}=N(e),{textColorClasses:z,textColorStyles:B}=n(()=>e.borderColor),{t:V}=l(),H=c(()=>({"aria-label":V(e.closeLabel),onClick(e){d.value=!1,a(`click:close`,e)}}));return()=>{let n=!!(s.prepend||m.value),i=!!(s.title||e.title),a=!!(s.close||e.closable),o={density:e.density,icon:m.value,size:e.iconSize||e.prominent?h.value:void 0};return d.value&&T(e.tag,{class:F([`v-alert`,e.border&&{"v-alert--border":!!e.border,[`v-alert--border-${e.border===!0?`start`:e.border}`]:!0},{"v-alert--prominent":e.prominent},g.value,y.value,E.value,O.value,M.value,L.value,w.value,e.class]),style:t([x.value,D.value,k.value,e.style]),role:`alert`},{default:()=>[p(!1,`v-alert`),e.border&&v(`div`,{key:`border`,class:F([`v-alert__border`,z.value]),style:t(B.value)},null),n&&v(`div`,{key:`prepend`,class:`v-alert__prepend`},[s.prepend?T(r,{key:`prepend-defaults`,disabled:!m.value,defaults:{VIcon:{...o}}},s.prepend):T(f,j({key:`prepend-icon`},o),null)]),v(`div`,{class:`v-alert__content`},[i&&T(I,{key:`title`},{default:()=>[s.title?.()??e.title]}),s.text?.()??e.text,s.default?.()]),s.append&&v(`div`,{key:`append`,class:`v-alert__append`},[s.append()]),a&&v(`div`,{key:`close`,class:`v-alert__close`},[s.close?T(r,{key:`close-defaults`,defaults:{VBtn:{icon:e.closeIcon,size:`x-small`,variant:`text`}}},{default:()=>[s.close?.({props:H.value})]}):T(P,j({key:`close-btn`,icon:e.closeIcon,size:`x-small`,variant:`text`},H.value),null)])]})}}});export{V as t};
+1
View File
@@ -0,0 +1 @@
import{Tn as e,en as t,jr as n,ln as r,o as i,or as a,p as o,pn as s,rn as c,s as l,ur as u,v as d,xr as f,zn as p}from"./index-AXqGiRqu.js";import{f as m,p as h}from"./VSelect-BzkPFBem.js";var g=s({...p(l(),[`direction`]),...p(h(),[`inline`])},`VCheckbox`),_=r()({name:`VCheckbox`,inheritAttrs:!1,props:g(),emits:{"update:modelValue":e=>!0,"update:focused":e=>!0},setup(r,{attrs:s,slots:l}){let p=t(r,`modelValue`),{isFocused:h,focus:g,blur:_}=o(r),v=n(),y=f();return c(()=>{let[t,n]=e(s),o=i.filterProps(r),c=m.filterProps(r);return a(i,u({ref:v,class:[`v-checkbox`,r.class]},t,o,{modelValue:p.value,"onUpdate:modelValue":e=>p.value=e,id:r.id||`checkbox-${y}`,focused:h.value,style:r.style}),{...l,default:({id:e,messagesId:t,isDisabled:r,isReadonly:i,isValid:o})=>a(m,u(c,{id:e.value,"aria-describedby":t.value,disabled:r.value,readonly:i.value},n,{error:o.value===!1,modelValue:p.value,"onUpdate:modelValue":e=>p.value=e,onFocus:g,onBlur:_}),l)})}),d({},v)}});export{_ as t};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
import{_ as e,a as t,b as n,c as r,d as i,f as a,g as o,h as s,i as c,l,m as u,n as d,o as f,p,r as m,s as h,u as ee,v as te,y as g}from"./VDataTable-D5mmIg9O.js";import{$n as _,Fr as v,M as y,Pr as b,Zn as x,_r as S,er as C,ln as w,or as T,pn as E,rn as ne,un as D,ur as O,zn as k}from"./index-AXqGiRqu.js";var A=E({itemsLength:{type:[Number,String],required:!0},...g(),...d(),...e()},`VDataTableServer`),j=w()({name:`VDataTableServer`,props:A(),emits:{"update:modelValue":e=>!0,"update:page":e=>!0,"update:itemsPerPage":e=>!0,"update:sortBy":e=>!0,"update:options":e=>!0,"update:expanded":e=>!0,"update:groupBy":e=>!0},setup(e,{attrs:d,slots:g}){let{groupBy:w}=r(e),{initialSortOrder:E,sortBy:A,multiSort:j,mustSort:M}=a(e),{page:N,itemsPerPage:P}=te(e),{disableSort:F}=v(e),I=_(()=>parseInt(e.itemsLength,10)),{columns:L,headers:R}=s(e,{groupBy:w,showSelect:b(()=>e.showSelect),showExpand:b(()=>e.showExpand)}),{items:z}=c(e,L),{toggleSort:B}=p({initialSortOrder:E,sortBy:A,multiSort:j,mustSort:M,page:N}),{opened:V,isGroupOpen:H,toggleGroup:re,extractRows:U}=l({groupBy:w,sortBy:A,disableSort:F}),{pageCount:W,setItemsPerPage:G,prevPage:K,nextPage:q,setPage:J}=n({page:N,itemsPerPage:P,itemsLength:I}),{flatItems:Y}=ee(z,w,V,()=>!!g[`group-summary`]),{isSelected:X,select:ie,selectAll:ae,toggleSelect:oe,someSelected:se,allSelected:ce}=u(e,{allItems:z,currentPage:z}),{isExpanded:Z,toggleExpand:le}=h(e),Q=_(()=>U(z.value));m({page:N,itemsPerPage:P,sortBy:A,groupBy:w,search:b(()=>e.search)}),S(`v-data-table`,{toggleSort:B,sortBy:A}),D({VDataTableRows:{hideNoData:b(()=>e.hideNoData),noDataText:b(()=>e.noDataText),loading:b(()=>e.loading),loadingText:b(()=>e.loadingText)}});let $=_(()=>({page:N.value,itemsPerPage:P.value,sortBy:A.value,pageCount:W.value,toggleSort:B,setItemsPerPage:G,prevPage:K,nextPage:q,setPage:J,someSelected:se.value,allSelected:ce.value,isSelected:X,select:ie,selectAll:ae,toggleSelect:oe,isExpanded:Z,toggleExpand:le,isGroupOpen:H,toggleGroup:re,items:Q.value.map(e=>e.raw),internalItems:Q.value,groupedItems:Y.value,columns:L.value,headers:R.value}));ne(()=>{let n=o.filterProps(e),r=i.filterProps(k(e,[`multiSort`])),a=f.filterProps(e),s=t.filterProps(e);return T(t,O({class:[`v-data-table`,{"v-data-table--loading":e.loading},e.class],style:e.style},s,{fixedHeader:e.fixedHeader||e.sticky}),{top:()=>g.top?.($.value),default:()=>g.default?g.default($.value):C(x,null,[g.colgroup?.($.value),!e.hideDefaultHeader&&C(`thead`,{key:`thead`,class:`v-data-table__thead`,role:`rowgroup`},[T(i,O(r,{multiSort:!!e.multiSort}),g)]),g.thead?.($.value),!e.hideDefaultBody&&C(`tbody`,{class:`v-data-table__tbody`,role:`rowgroup`},[g[`body.prepend`]?.($.value),g.body?g.body($.value):T(f,O(d,a,{items:Y.value}),g),g[`body.append`]?.($.value)]),g.tbody?.($.value),g.tfoot?.($.value)]),bottom:()=>g.bottom?g.bottom($.value):!e.hideDefaultFooter&&C(x,null,[T(y,null,null),T(o,n,{prepend:g[`footer.prepend`]})])})})}});export{j as t};
+1
View File
@@ -0,0 +1 @@
import{Et as e,Sr as t,b as n,dr as r,en as i,jr as a,jt as o,ln as s,or as c,pn as l,rn as u,ur as d,v as f,x as p,y as m,zn as h}from"./index-AXqGiRqu.js";var g=l({fullscreen:Boolean,scrollable:Boolean,...h(n({captureFocus:!0,origin:`center center`,scrollStrategy:`block`,transition:{component:o},zIndex:2400,retainFocus:!0}),[`disableInitialFocus`])},`VDialog`),_=s()({name:`VDialog`,props:g(),emits:{"update:modelValue":e=>!0,afterEnter:()=>!0,afterLeave:()=>!0},setup(n,{emit:o,slots:s}){let l=i(n,`modelValue`),{scopeId:h}=p(),g=a();function _(){o(`afterEnter`),(n.scrim||n.retainFocus)&&g.value?.contentEl&&!g.value.contentEl.contains(document.activeElement)&&g.value.contentEl.focus({preventScroll:!0})}function v(){o(`afterLeave`)}return t(l,async e=>{e||(await r(),g.value.activatorEl?.focus({preventScroll:!0}))}),u(()=>{let t=m.filterProps(n),r=d({"aria-haspopup":`dialog`},n.activatorProps),i=d({tabindex:-1},n.contentProps);return c(m,d({ref:g,class:[`v-dialog`,{"v-dialog--fullscreen":n.fullscreen,"v-dialog--scrollable":n.scrollable},n.class],style:n.style},t,{modelValue:l.value,"onUpdate:modelValue":e=>l.value=e,"aria-modal":`true`,activatorProps:r,contentProps:i,height:n.fullscreen?void 0:n.height,width:n.fullscreen?void 0:n.width,maxHeight:n.fullscreen?void 0:n.maxHeight,maxWidth:n.fullscreen?void 0:n.maxWidth,role:`dialog`,onAfterEnter:_,onAfterLeave:v},h),{activator:s.activator,default:(...t)=>c(e,{root:`VDialog`},{default:()=>[s.default?.(...t)]})})}),f({},g)}});export{_ as t};
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
import{$n as e,In as t,Kn as n,Mt as r,Rr as i,Yt as a,bn as o,cr as s,ct as c,fn as l,ln as u,pn as d}from"./index-AXqGiRqu.js";var f=a.reduce((e,t)=>(e[t]={type:[Boolean,String,Number],default:!1},e),{}),p=a.reduce((e,t)=>{let n=`offset`+i(t);return e[n]={type:[String,Number],default:null},e},{}),m={col:t(f),offset:t(p),order:[`order`,`orderSm`,`orderMd`,`orderLg`,`orderXl`,`orderXxl`]};function h(e){if(typeof e==`string`&&e.includes(`/`)){let[t,n]=e.split(`/`);return{cols:Number(t),size:Number(n)}}return{cols:e}}function g(e,t,n){if(n==null||n===!1)return{};let{cols:r,size:i}=h(n),a=t.replace(e,``).toLowerCase();return e===`offset`?{className:`v-col--offset-${a}-${r}`,variables:[{[`--v-col-offset-base-${a}`]:i}]}:e===`order`?{className:`order-${a}-${r}`}:{className:r===``||r===!0?`v-col--${a}`:`v-col--cols-${a}-${r}`,variables:[{[`--v-col-size-base-${a}`]:i}]}}var _=[`auto`,`start`,`end`,`center`,`baseline`,`stretch`],v=e=>_.includes(e),y=d({cols:{type:[Boolean,String,Number],default:!1},...f,offset:{type:[String,Number],default:null},...p,order:{type:[String,Number],default:null},orderSm:{type:[String,Number],default:null},orderMd:{type:[String,Number],default:null},orderLg:{type:[String,Number],default:null},orderXl:{type:[String,Number],default:null},orderXxl:{type:[String,Number],default:null},alignSelf:{type:String,default:null,validator:v},...l(),...r()},`VCol`),b=u()({name:`VCol`,props:y(),setup(t,{slots:n}){let r=e(()=>h(t.cols).size),i=e(()=>h(t.offset).size),a=e(()=>{let e=[`v-col`],n=[],r;for(r in m)m[r].forEach(i=>{let a=t[i],{className:o,variables:s}=g(r,i,a);o&&e.push(o),s&&n.push(...s)});let{cols:i}=h(t.cols),{cols:a}=h(t.offset);return e.push({[`v-col--cols-${i}`]:i,[`v-col--offset-${a}`]:a,[`order-${t.order}`]:t.order,[`align-self-${t.alignSelf}`]:t.alignSelf}),{classes:e,variables:n}});return()=>s(t.tag,{class:[a.value.classes,t.class],style:[{"--v-col-size-base":r.value},{"--v-col-offset-base":i.value},a.value.variables,t.style]},n.default?.())}}),x=[`start`,`end`,`center`],S=[`space-between`,`space-around`,`space-evenly`],C=[...x,`baseline`,`stretch`],w=e=>C.includes(e),T=[...x,...S],E=e=>T.includes(e),D=[...x,...S,`stretch`],O=e=>D.includes(e),k={align:[`align`,`alignSm`,`alignMd`,`alignLg`,`alignXl`,`alignXxl`],justify:[`justify`,`justifySm`,`justifyMd`,`justifyLg`,`justifyXl`,`justifyXxl`],alignContent:[`alignContent`,`alignContentSm`,`alignContentMd`,`alignContentLg`,`alignContentXl`,`alignContentXxl`]},A={align:`align`,justify:`justify`,alignContent:`align-content`};function j(e,t,n){let r=A[e];if(n!=null){if(t){let n=t.replace(e,``);r+=`-${n}`}return r+=`-${n}`,r.toLowerCase()}}var M=d({dense:Boolean,align:{type:String,default:null,validator:w},alignSm:{type:String,default:null,validator:w},alignMd:{type:String,default:null,validator:w},alignLg:{type:String,default:null,validator:w},alignXl:{type:String,default:null,validator:w},alignXxl:{type:String,default:null,validator:w},justify:{type:String,default:null,validator:E},justifySm:{type:String,default:null,validator:E},justifyMd:{type:String,default:null,validator:E},justifyLg:{type:String,default:null,validator:E},justifyXl:{type:String,default:null,validator:E},justifyXxl:{type:String,default:null,validator:E},alignContent:{type:String,default:null,validator:O},alignContentSm:{type:String,default:null,validator:O},alignContentMd:{type:String,default:null,validator:O},alignContentLg:{type:String,default:null,validator:O},alignContentXl:{type:String,default:null,validator:O},alignContentXxl:{type:String,default:null,validator:O},noGutters:Boolean,gap:[Number,String,Array],size:[Number,String],...l(),...c(),...r()},`VRow`),N=u()({name:`VRow`,props:M(),setup(t,{slots:r}){t.dense&&n(`dense`,`density="comfortable"`);let i=e(()=>{let e=[],n;for(n in k)k[n].forEach(r=>{let i=t[r],a=j(n,r,i);a&&e.push(a)});return e.push({"v-row--no-gutters":t.noGutters,"v-row--density-default":t.density===`default`&&!t.noGutters&&!t.dense,"v-row--density-compact":t.density===`compact`,"v-row--density-comfortable":t.density===`comfortable`||t.dense,[`align-${t.align}`]:t.align,[`justify-${t.justify}`]:t.justify,[`align-content-${t.alignContent}`]:t.alignContent}),e}),a=e(()=>Array.isArray(t.gap)?o(t.gap[0]||0):o(t.gap)),c=e(()=>Array.isArray(t.gap)?o(t.gap[1]||0):a.value);return()=>s(t.tag,{class:[`v-row`,i.value,t.class],style:[{"--v-col-gap-x":a.value,"--v-col-gap-y":c.value,"--v-row-columns":t.size},t.style]},r.default?.())}});export{b as n,N as t};
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
import{sn as e}from"./index-AXqGiRqu.js";var t=e(`v-spacer`,`div`,`VSpacer`);export{t};
+1
View File
@@ -0,0 +1 @@
import{At as e,Br as t,Et as n,J as r,K as i,Pr as a,Tn as o,Wn as s,X as c,Z as l,Zn as u,en as d,er as f,jr as p,ln as m,o as h,or as g,p as _,pn as v,rn as y,s as b,ur as x,v as S,xr as C,zr as w}from"./index-AXqGiRqu.js";import{n as T,r as E}from"./VContainer-CXLvz-1q.js";var D=v({indeterminate:Boolean,inset:Boolean,flat:Boolean,loading:{type:[Boolean,String],default:!1},...b(),...E()},`VSwitch`),O=m()({name:`VSwitch`,inheritAttrs:!1,props:D(),emits:{"update:focused":e=>!0,"update:modelValue":e=>!0,"update:indeterminate":e=>!0},setup(m,{attrs:v,slots:b}){let E=d(m,`indeterminate`),D=d(m,`modelValue`),{loaderClasses:O}=r(m),{isFocused:k,focus:A,blur:j}=_(m),M=p(),N=p(),P=s&&window.matchMedia(`(forced-colors: active)`).matches,F=a(()=>typeof m.loading==`string`&&m.loading!==``?m.loading:m.color),I=C(),L=a(()=>m.id||`switch-${I}`);function R(){E.value&&=!1}function z(e){e.stopPropagation(),e.preventDefault(),M.value?.input?.click()}return y(()=>{let[r,a]=o(v),s=h.filterProps(m),d=T.filterProps(m);return g(h,x({ref:N,class:[`v-switch`,{"v-switch--flat":m.flat},{"v-switch--inset":m.inset},{"v-switch--indeterminate":E.value},O.value,m.class]},r,s,{modelValue:D.value,"onUpdate:modelValue":e=>D.value=e,id:L.value,focused:k.value,style:m.style}),{...b,default:({id:r,messagesId:o,isDisabled:s,isReadonly:p,isValid:h})=>{let _={model:D,isValid:h};return g(T,x({ref:M},d,{modelValue:D.value,"onUpdate:modelValue":[e=>D.value=e,R],id:r.value,"aria-describedby":o.value,type:`checkbox`,"aria-checked":E.value?`mixed`:void 0,disabled:s.value,readonly:p.value,onFocus:A,onBlur:j},a),{...b,default:({backgroundColorClasses:e,backgroundColorStyles:n})=>f(`div`,{class:w([`v-switch__track`,P?void 0:e.value]),style:t(n.value),onClick:z},[b[`track-true`]&&f(`div`,{key:`prepend`,class:`v-switch__track-true`},[b[`track-true`](_)]),b[`track-false`]&&f(`div`,{key:`append`,class:`v-switch__track-false`},[b[`track-false`](_)])]),input:({inputNode:r,icon:a,backgroundColorClasses:o,backgroundColorStyles:s})=>f(u,null,[r,f(`div`,{class:w([`v-switch__thumb`,{"v-switch__thumb--filled":a||m.loading},m.inset||P?void 0:o.value]),style:t(m.inset?void 0:s.value)},[b.thumb?g(n,{defaults:{VIcon:{icon:a,size:`x-small`}}},{default:()=>[b.thumb({..._,icon:a})]}):g(e,null,{default:()=>[m.loading?g(i,{name:`v-switch`,active:!0,color:h.value===!1?void 0:F.value},{default:e=>b.loader?b.loader(e):g(c,{active:e.isActive,color:e.color,indeterminate:!0,size:`16`,width:`2`},null)}):a&&g(l,{key:String(a),icon:a,size:`x-small`},null)]})])])})}})}),S({},N)}});export{O as t};
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
import{It as e}from"./index-AXqGiRqu.js";function t(t,n){if(t instanceof e){let e=t.detail??t.message;return typeof e==`string`?e:Array.isArray(e)?e.map(e=>e?.msg||JSON.stringify(e)).join(`; `):e&&typeof e==`object`?JSON.stringify(e):t.message||n}return t instanceof Error&&t.message||n}export{t};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/tsMode-DcSzuJt_.js","assets/editor.api2-CLbMfXfG.js","assets/editor-DX2BVZF3.css","assets/workers-DM14Vrt9.js"])))=>i.map(i=>d[i]);
import{g as e,h as t,n}from"./editor.api2-CLbMfXfG.js";import{Pt as r}from"./index-AXqGiRqu.js";var i=`5.9.3`,a=e({JsxEmit:()=>s,ModuleKind:()=>o,ModuleResolutionKind:()=>u,NewLineKind:()=>c,ScriptTarget:()=>l,getJavaScriptWorker:()=>_,getTypeScriptWorker:()=>g,javascriptDefaults:()=>h,typescriptDefaults:()=>m,typescriptVersion:()=>f}),o=(e=>(e[e.None=0]=`None`,e[e.CommonJS=1]=`CommonJS`,e[e.AMD=2]=`AMD`,e[e.UMD=3]=`UMD`,e[e.System=4]=`System`,e[e.ES2015=5]=`ES2015`,e[e.ESNext=99]=`ESNext`,e))(o||{}),s=(e=>(e[e.None=0]=`None`,e[e.Preserve=1]=`Preserve`,e[e.React=2]=`React`,e[e.ReactNative=3]=`ReactNative`,e[e.ReactJSX=4]=`ReactJSX`,e[e.ReactJSXDev=5]=`ReactJSXDev`,e))(s||{}),c=(e=>(e[e.CarriageReturnLineFeed=0]=`CarriageReturnLineFeed`,e[e.LineFeed=1]=`LineFeed`,e))(c||{}),l=(e=>(e[e.ES3=0]=`ES3`,e[e.ES5=1]=`ES5`,e[e.ES2015=2]=`ES2015`,e[e.ES2016=3]=`ES2016`,e[e.ES2017=4]=`ES2017`,e[e.ES2018=5]=`ES2018`,e[e.ES2019=6]=`ES2019`,e[e.ES2020=7]=`ES2020`,e[e.ESNext=99]=`ESNext`,e[e.JSON=100]=`JSON`,e[e.Latest=99]=`Latest`,e))(l||{}),u=(e=>(e[e.Classic=1]=`Classic`,e[e.NodeJs=2]=`NodeJs`,e))(u||{}),d=class{constructor(e,t,r,i,a){this._onDidChange=new n,this._onDidExtraLibsChange=new n,this._extraLibs=Object.create(null),this._removedExtraLibs=Object.create(null),this._eagerModelSync=!1,this.setCompilerOptions(e),this.setDiagnosticsOptions(t),this.setWorkerOptions(r),this.setInlayHintsOptions(i),this.setModeConfiguration(a),this._onDidExtraLibsChangeTimeout=-1}get onDidChange(){return this._onDidChange.event}get onDidExtraLibsChange(){return this._onDidExtraLibsChange.event}get modeConfiguration(){return this._modeConfiguration}get workerOptions(){return this._workerOptions}get inlayHintsOptions(){return this._inlayHintsOptions}getExtraLibs(){return this._extraLibs}addExtraLib(e,t){let n;if(n=t===void 0?`ts:extralib-${Math.random().toString(36).substring(2,15)}`:t,this._extraLibs[n]&&this._extraLibs[n].content===e)return{dispose:()=>{}};let r=1;return this._removedExtraLibs[n]&&(r=this._removedExtraLibs[n]+1),this._extraLibs[n]&&(r=this._extraLibs[n].version+1),this._extraLibs[n]={content:e,version:r},this._fireOnDidExtraLibsChangeSoon(),{dispose:()=>{let e=this._extraLibs[n];e&&e.version===r&&(delete this._extraLibs[n],this._removedExtraLibs[n]=r,this._fireOnDidExtraLibsChangeSoon())}}}setExtraLibs(e){for(let e in this._extraLibs)this._removedExtraLibs[e]=this._extraLibs[e].version;if(this._extraLibs=Object.create(null),e&&e.length>0)for(let t of e){let e=t.filePath||`ts:extralib-${Math.random().toString(36).substring(2,15)}`,n=t.content,r=1;this._removedExtraLibs[e]&&(r=this._removedExtraLibs[e]+1),this._extraLibs[e]={content:n,version:r}}this._fireOnDidExtraLibsChangeSoon()}_fireOnDidExtraLibsChangeSoon(){this._onDidExtraLibsChangeTimeout===-1&&(this._onDidExtraLibsChangeTimeout=window.setTimeout(()=>{this._onDidExtraLibsChangeTimeout=-1,this._onDidExtraLibsChange.fire(void 0)},0))}getCompilerOptions(){return this._compilerOptions}setCompilerOptions(e){this._compilerOptions=e||Object.create(null),this._onDidChange.fire(void 0)}getDiagnosticsOptions(){return this._diagnosticsOptions}setDiagnosticsOptions(e){this._diagnosticsOptions=e||Object.create(null),this._onDidChange.fire(void 0)}setWorkerOptions(e){this._workerOptions=e||Object.create(null),this._onDidChange.fire(void 0)}setInlayHintsOptions(e){this._inlayHintsOptions=e||Object.create(null),this._onDidChange.fire(void 0)}setMaximumWorkerIdleTime(e){}setEagerModelSync(e){this._eagerModelSync=e}getEagerModelSync(){return this._eagerModelSync}setModeConfiguration(e){this._modeConfiguration=e||Object.create(null),this._onDidChange.fire(void 0)}},f=i,p={completionItems:!0,hovers:!0,documentSymbols:!0,definitions:!0,references:!0,documentHighlights:!0,rename:!0,diagnostics:!0,documentRangeFormattingEdits:!0,signatureHelp:!0,onTypeFormattingEdits:!0,codeActions:!0,inlayHints:!0},m=new d({allowNonTsExtensions:!0,target:99},{noSemanticValidation:!1,noSyntaxValidation:!1,onlyVisible:!1},{},{},p),h=new d({allowNonTsExtensions:!0,allowJs:!0,target:99},{noSemanticValidation:!0,noSyntaxValidation:!1,onlyVisible:!1},{},{},p),g=()=>v().then(e=>e.getTypeScriptWorker()),_=()=>v().then(e=>e.getJavaScriptWorker());function v(){return r(()=>import(`./tsMode-DcSzuJt_.js`),__vite__mapDeps([0,1,2,3]))}t.onLanguage(`typescript`,()=>v().then(e=>e.setupTypeScript(m))),t.onLanguage(`javascript`,()=>v().then(e=>e.setupJavaScript(h)));export{m as n,a as t};
+1
View File
@@ -0,0 +1 @@
import{jr as e,zt as t}from"./index-AXqGiRqu.js";import{o as n}from"./VContainer-CXLvz-1q.js";import{t as r}from"./apiError-Cfu1CDNq.js";function i(){let i=e([]),a=e(!1),o=n();async function s(e){a.value=!0;try{i.value=(await t.getList(`/servers/`,{per_page:e?.perPage??200})).items}catch(e){i.value=[],o(r(e,`加载服务器列表失败`),`error`)}finally{a.value=!1}}return{servers:i,loading:a,loadServers:s}}export{i as t};
@@ -0,0 +1,2 @@
import{$n as e,br as t,dr as n,gr as r,jr as i,nr as a,rr as o,sr as s,tr as c,zr as l,zt as u}from"./index-AXqGiRqu.js";import{t as d}from"./_plugin-vue_export-helper-BDNMzG2s.js";var f=new Set(`if.then.else.elif.fi.for.while.do.done.case.esac.in.function.return.break.continue.select.until.cd.ls.grep.egrep.fgrep.find.cat.chmod.chown.docker.podman.kubectl.apt.apt-get.yum.dnf.apk.pip.pip3.npm.npx.ssh.scp.rsync.curl.wget.tar.gzip.gunzip.kill.ps.top.htop.nano.vim.vi.journalctl.systemctl.service.supervisorctl.crontab.nginx.df.du.free.mount.uname.hostname.whoami.id.pwd.head.tail.wc.sort.uniq.awk.sed.cut.tr.xargs.tee.netstat.ss.ip.ping.dig.git.make.nohup.timeout.watch`.split(`.`)),p=new Set([`echo`,`printf`,`test`,`read`,`pushd`,`popd`,`exit`,`set`,`unset`,`export`,`source`,`alias`,`type`,`command`,`hash`,`help`,`history`]);function m(e){return e===`sudo`?`sudo`:f.has(e)?`keyword`:p.has(e)?`builtin`:/^[/~]/.test(e)||e.startsWith(`./`)||e.startsWith(`../`)?`path`:/^\d+(?:\.\d+)?$/.test(e)?`number`:`text`}function h(e){let t=[],n=0;for(;n<e.length;){let r=e[n];if(r===`#`){let r=n+1;for(;r<e.length&&e[r]!==`
`;)r++;t.push({type:`comment`,value:e.slice(n,r)}),n=r;continue}if(r===`'`||r===`"`){let i=r,a=n+1;for(;a<e.length;){if(e[a]===`\\`&&i===`"`){a+=2;continue}if(e[a]===i){a++;break}a++}t.push({type:`string`,value:e.slice(n,a)}),n=a;continue}if(r==="`"){let r=n+1;for(;r<e.length&&e[r]!=="`";)r++;r<e.length&&r++,t.push({type:`backtick`,value:e.slice(n,r)}),n=r;continue}if(r===`$`){let r=n+1;if(e[r]===`{`){for(;r<e.length&&e[r]!==`}`;)r++;r<e.length&&r++}else if(e[r]===`(`)r++;else for(;r<e.length&&/[\w]/.test(e[r]);)r++;t.push({type:`variable`,value:e.slice(n,r)}),n=r;continue}if(r===`|`||r===`&`||r===`;`){let r=n;e[r]===`|`&&e[r+1]===`|`||e[r]===`&`&&e[r+1]===`&`?r+=2:r+=1;let i=e.slice(n,r);t.push({type:i===`|`||i===`||`?`pipe`:`operator`,value:i}),n=r;continue}if(r===`>`||r===`<`){let r=n;e[r]===`>`&&e[r+1]===`>`||e[r]===`<`&&e[r+1]===`<`?r+=2:r+=1,e[r]===`&`&&r++,t.push({type:`redirect`,value:e.slice(n,r)}),n=r;continue}if(/\s/.test(r)){let r=n;for(;r<e.length&&/\s/.test(e[r]);)r++;t.push({type:`text`,value:e.slice(n,r)}),n=r;continue}if(r===`-`&&e[n+1]===`-`){let r=n+2;for(;r<e.length&&/[\w-]/.test(e[r]);)r++;t.push({type:`flag`,value:e.slice(n,r)}),n=r;continue}if(r===`-`&&/[a-zA-Z]/.test(e[n+1]||``)){let r=n+1;for(;r<e.length&&/[a-zA-Z]/.test(e[r]);)r++;t.push({type:`flag`,value:e.slice(n,r)}),n=r;continue}let i=n;for(;i<e.length&&!/[\s#"'`$|;&<>]/.test(e[i]);)i++;let a=e.slice(n,i);t.push({type:m(a),value:a}),n=i}return t}function g(e){return e.replace(/&/g,`&amp;`).replace(/</g,`&lt;`).replace(/>/g,`&gt;`)}var _={keyword:`sh-keyword`,builtin:`sh-builtin`,sudo:`sh-sudo`,pipe:`sh-pipe`,operator:`sh-operator`,redirect:`sh-redirect`,string:`sh-string`,flag:`sh-flag`,variable:`sh-variable`,backtick:`sh-backtick`,comment:`sh-comment`,number:`sh-number`,path:`sh-path`,text:`sh-text`};function v(e){return e?h(e).map(e=>`<span class="${_[e.type]}">${g(e.value)}</span>`).join(``):``}var y=[`innerHTML`],b=d(s({__name:`ShellHighlightField`,props:{modelValue:{},placeholder:{default:``},disabled:{type:Boolean,default:!1},multiline:{type:Boolean,default:!1},rows:{default:3},size:{default:`default`}},emits:[`update:modelValue`,`keydown`],setup(s,{expose:u,emit:d}){let f=i(null),p=s,m=d,h=e(()=>v(p.modelValue));function g(e){let t=e.target;m(`update:modelValue`,t.value)}function _(e){m(`keydown`,e)}function b(){return f.value}function x(){f.value?.focus()}function S(){let e=f.value;return e?e.value.substring(e.selectionStart??0,e.selectionEnd??0):``}function C(){let e=f.value;e&&(e.focus(),e.select())}async function w(e){let t=f.value;if(!t)return;let r=t.selectionStart??t.value.length,i=t.selectionEnd??r;m(`update:modelValue`,t.value.slice(0,r)+e+t.value.slice(i)),await n(),t.focus();let a=r+e.length;t.setSelectionRange(a,a)}return u({getInputEl:b,focusInput:x,getSelectionText:S,selectAllInput:C,insertAtSelection:w}),(e,n)=>(r(),o(`div`,{class:l([`sh-field position-relative`,{"sh-field--disabled":s.disabled,"sh-field--empty":!s.modelValue,"sh-field--large":s.size===`large`}])},[s.modelValue?(r(),o(`div`,{key:0,class:l([`sh-field__highlight font-mono`,s.multiline?`sh-field__highlight--multi`:`sh-field__highlight--single`]),"aria-hidden":`true`,innerHTML:h.value},null,10,y)):a(``,!0),(r(),c(t(s.multiline?`textarea`:`input`),{ref_key:`inputRef`,ref:f,value:s.modelValue,placeholder:s.placeholder,disabled:s.disabled,rows:s.multiline?s.rows:void 0,class:l([`sh-field__input font-mono`,s.multiline?`sh-field__input--multi`:`sh-field__input--single`]),spellcheck:`false`,autocomplete:`off`,onInput:g,onKeydown:_},null,40,[`value`,`placeholder`,`disabled`,`rows`,`class`]))],2))}}),[[`__scopeId`,`data-v-b73e3452`]]);function x(e){return[...e].sort((e,t)=>e.is_builtin===t.is_builtin?e.sort_order===t.sort_order?e.id-t.id:e.sort_order-t.sort_order:e.is_builtin?1:-1)}function S(){let e=i([]),t=i(!1);async function n(){t.value=!0;try{e.value=x(await u.get(`/terminal/quick-commands`)||[])}catch{throw e.value=[],Error(``)}finally{t.value=!1}}async function r(e,t){await u.post(`/terminal/quick-commands`,{name:e.trim(),cmd:t.trim()}),await n()}async function a(e,t,r){await u.put(`/terminal/quick-commands/${e}`,{name:t.trim(),cmd:r.trim()}),await n()}async function o(e){await u.delete(`/terminal/quick-commands/${e}`),await n()}async function s(t){e.value=x((await u.put(`/terminal/quick-commands/reorder`,{ids:t})).items||[])}return{commands:e,loading:t,load:n,create:r,update:a,remove:o,reorder:s,customCommands:()=>e.value.filter(e=>!e.is_builtin),builtinCommands:()=>e.value.filter(e=>e.is_builtin)}}export{S as n,b as r,x as t};
+1 -1
View File
@@ -5,7 +5,7 @@
<link rel="icon" href="/app/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nexus</title>
<script type="module" crossorigin src="/app/assets/index-oHtif8No.js"></script>
<script type="module" crossorigin src="/app/assets/index-AXqGiRqu.js"></script>
<link rel="stylesheet" crossorigin href="/app/assets/index-BGzIPEZO.css">
</head>
<body>