feat(push): 源路径验证、普通文件上传与暂存文件管理
补齐 Round4 H5/H6:验证 Nexus 主机源路径、失败批量重试;新增 upload-files 与普通文件多选上传;上传后可浏览/预览/重命名/删除暂存目录内容。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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 API,SPA 未接入。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `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,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: []
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
@@ -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
|
||||
@@ -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};
|
||||
@@ -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};
|
||||
@@ -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
@@ -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};
|
||||
@@ -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
@@ -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
@@ -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
@@ -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};
|
||||
@@ -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};
|
||||
@@ -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
@@ -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
@@ -0,0 +1 @@
|
||||
import{sn as e}from"./index-AXqGiRqu.js";var t=e(`v-spacer`,`div`,`VSpacer`);export{t};
|
||||
@@ -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
@@ -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};
|
||||
@@ -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,`&`).replace(/</g,`<`).replace(/>/g,`>`)}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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user