feat: add run scripts configure language and version

This commit is contained in:
engigu
2026-03-06 09:16:37 +08:00
parent 36d1569f83
commit e7324729c4
4 changed files with 623 additions and 376 deletions
@@ -0,0 +1,120 @@
<script setup lang="ts">
import { ref } from 'vue'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog'
import { Label } from '@/components/ui/label'
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
// Create Dialog State
const showCreate = ref(false)
const newItemName = ref('')
const newItemType = ref<'file' | 'dir'>('file')
const createInDir = ref('')
// Delete Dialog State
const showDelete = ref(false)
const deletePath = ref('')
// Rename Dialog State
const showRename = ref(false)
const renamePath = ref('')
const newName = ref('')
const emit = defineEmits<{
create: [name: string, type: 'file' | 'dir', parent: string]
delete: [path: string]
rename: [oldPath: string, newName: string]
}>()
function openCreate(parent = '') {
newItemName.value = ''
newItemType.value = 'file'
createInDir.value = parent
showCreate.value = true
}
function openDelete(path: string) {
deletePath.value = path
showDelete.value = true
}
function openRename(path: string) {
renamePath.value = path
newName.value = path.split('/').pop() || ''
showRename.value = true
}
defineExpose({ openCreate, openDelete, openRename, closeCreate: () => showCreate.value = false, closeRename: () => showRename.value = false, closeDelete: () => showDelete.value = false })
</script>
<template>
<!-- 新建对话框 -->
<Dialog v-model:open="showCreate">
<DialogContent class="max-w-xs" @openAutoFocus.prevent>
<DialogHeader>
<DialogTitle class="text-sm">新建</DialogTitle>
</DialogHeader>
<div class="space-y-3 py-2">
<div class="text-xs text-muted-foreground">
位置: {{ createInDir || '根目录' }}
</div>
<RadioGroup v-model="newItemType" class="flex gap-4">
<div class="flex items-center gap-2">
<RadioGroupItem value="file" id="file" />
<Label for="file" class="text-xs">文件</Label>
</div>
<div class="flex items-center gap-2">
<RadioGroupItem value="dir" id="dir" />
<Label for="dir" class="text-xs">文件夹</Label>
</div>
</RadioGroup>
<div class="space-y-1">
<Label class="text-xs">名称</Label>
<Input v-model="newItemName" class="h-8 text-xs" placeholder="script.sh" @keyup.enter="emit('create', newItemName, newItemType, createInDir)" />
</div>
</div>
<DialogFooter>
<Button variant="outline" size="sm" class="h-7 text-xs" @click="showCreate = false">取消</Button>
<Button size="sm" class="h-7 text-xs" @click="emit('create', newItemName, newItemType, createInDir)">创建</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<!-- 删除确认 -->
<AlertDialog v-model:open="showDelete">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle class="text-sm">确认删除</AlertDialogTitle>
<AlertDialogDescription class="text-xs">确定要删除 {{ deletePath }} </AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel class="h-7 text-xs" @click="showDelete = false">取消</AlertDialogCancel>
<AlertDialogAction class="h-7 text-xs bg-destructive text-white hover:bg-destructive/90" @click="emit('delete', deletePath)">
删除
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<!-- 重命名对话框 -->
<Dialog v-model:open="showRename">
<DialogContent class="max-w-xs" @openAutoFocus.prevent>
<DialogHeader>
<DialogTitle class="text-sm">重命名</DialogTitle>
</DialogHeader>
<div class="space-y-3 py-2">
<div class="space-y-1">
<Label class="text-xs">新名称</Label>
<Input v-model="newName" class="h-8 text-xs" placeholder="new_name.sh" @keyup.enter="emit('rename', renamePath, newName)" />
<p class="text-[10px] text-muted-foreground">仅支持修改名称不可包含路径分隔符 /</p>
</div>
</div>
<DialogFooter>
<Button variant="outline" size="sm" class="h-7 text-xs" @click="showRename = false">取消</Button>
<Button size="sm" class="h-7 text-xs" @click="emit('rename', renamePath, newName)">确定</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>
@@ -0,0 +1,101 @@
<script setup lang="ts">
import { ref } from 'vue'
import { Button } from '@/components/ui/button'
import { RefreshCw, FolderUp, Upload, Plus } from 'lucide-vue-next'
import FileTreeNode from '@/components/FileTreeNode.vue'
import { type FileNode } from '@/api'
defineProps<{
fileTree: FileNode[]
expandedDirs: Set<string>
selectedPath: string | null
}>()
const emit = defineEmits<{
refresh: []
create: [path: string]
select: [node: FileNode]
delete: [path: string]
download: [path: string]
move: [oldPath: string, newPath: string]
rename: [path: string]
duplicate: [path: string]
uploadArchive: [file: File, target: string]
uploadFiles: [files: FileList, paths: string[], target: string]
}>()
const archiveInputRef = ref<HTMLInputElement | null>(null)
const filesInputRef = ref<HTMLInputElement | null>(null)
const uploadTargetDir = ref('')
function triggerArchiveUpload(targetDir = '') {
uploadTargetDir.value = targetDir
if (archiveInputRef.value) archiveInputRef.value.click()
}
function triggerFilesUpload(targetDir = '') {
uploadTargetDir.value = targetDir
if (filesInputRef.value) filesInputRef.value.click()
}
function handleArchiveUpload(e: Event) {
const input = e.target as HTMLInputElement
const file = input.files?.[0]
if (file) emit('uploadArchive', file, uploadTargetDir.value)
input.value = ''
}
function handleFilesUpload(e: Event) {
const input = e.target as HTMLInputElement
const files = input.files
if (files && files.length > 0) {
const paths: string[] = []
for (let i = 0; i < files.length; i++) {
const f = files.item(i)
if (f) {
paths.push((f as any).webkitRelativePath || f.name)
}
}
emit('uploadFiles', files, paths, uploadTargetDir.value)
}
input.value = ''
}
</script>
<template>
<div class="w-full lg:w-56 h-48 lg:h-auto flex-shrink-0 border rounded-md flex flex-col">
<div class="flex items-center justify-between p-2 border-b">
<span class="text-xs font-medium">脚本文件</span>
<div class="flex gap-1">
<Button variant="ghost" size="icon" class="h-6 w-6" @click="emit('refresh')" title="刷新">
<RefreshCw class="h-3 w-3" />
</Button>
<Button variant="ghost" size="icon" class="h-6 w-6" @click="triggerFilesUpload('')" title="上传文件/文件夹(放在根目录)">
<FolderUp class="h-3 w-3" />
</Button>
<Button variant="ghost" size="icon" class="h-6 w-6" @click="triggerArchiveUpload('')" title="导入压缩包(放在根目录)">
<Upload class="h-3 w-3" />
</Button>
<Button variant="ghost" size="icon" class="h-6 w-6" @click="emit('create', '')" title="新建">
<Plus class="h-3 w-3" />
</Button>
</div>
<input ref="archiveInputRef" type="file" accept=".zip,.tar,.gz,.tgz" class="hidden" @change="handleArchiveUpload" />
<input ref="filesInputRef" type="file" multiple class="hidden" @change="handleFilesUpload" />
</div>
<div class="flex-1 overflow-auto p-1 text-[13px]">
<div v-if="fileTree.length === 0" class="text-xs text-muted-foreground text-center py-4">
暂无文件
</div>
<FileTreeNode v-for="node in fileTree" :key="node.path" :node="node" :expanded-dirs="expandedDirs"
:selected-path="selectedPath"
@select="n => emit('select', n)"
@delete="p => emit('delete', p)"
@create="p => emit('create', p)"
@download-file="p => emit('download', p)"
@move="(o, n) => emit('move', o, n)"
@rename="p => emit('rename', p)"
@duplicate="p => emit('duplicate', p)" />
</div>
</div>
</template>
@@ -0,0 +1,144 @@
<script setup lang="ts">
import { Button } from '@/components/ui/button'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
import { Label } from '@/components/ui/label'
import { Plus, X, AlertCircle } from 'lucide-vue-next'
interface Env {
plugin: string
version: string
}
const props = defineProps<{
open: boolean
selectedEnvs: Env[]
langGroups: Record<string, string[]>
getLangIcon: (plugin: string) => string
}>()
const emit = defineEmits<{
'update:open': [value: boolean]
'update:selectedEnvs': [value: Env[]]
'confirm': []
}>()
function addEnv() {
const newEnvs = [...props.selectedEnvs, { plugin: '', version: '' }]
emit('update:selectedEnvs', newEnvs)
}
function removeEnv(index: number) {
const newEnvs = [...props.selectedEnvs]
newEnvs.splice(index, 1)
emit('update:selectedEnvs', newEnvs)
}
function updateEnvPlugin(index: number, plugin: string) {
const newEnvs = [...props.selectedEnvs]
const group = props.langGroups ? props.langGroups[plugin] : null
newEnvs[index] = {
plugin,
version: (group && group.length > 0) ? (group[0] as string) : ''
}
emit('update:selectedEnvs', newEnvs)
}
function updateEnvVersion(index: number, version: string) {
const newEnvs = [...props.selectedEnvs]
if (newEnvs[index]) {
newEnvs[index].version = version
emit('update:selectedEnvs', newEnvs)
}
}
</script>
<template>
<Dialog :open="open" @update:open="val => emit('update:open', val)">
<DialogContent class="max-w-md" @openAutoFocus.prevent>
<DialogHeader>
<DialogTitle class="text-sm">运行配置</DialogTitle>
</DialogHeader>
<div class="space-y-4 py-2">
<div class="space-y-2">
<Label class="text-xs font-medium">运行环境 (Mise)</Label>
<div class="space-y-3">
<div v-for="(env, index) in selectedEnvs" :key="index" class="flex gap-1.5 items-center group bg-muted/20 hover:bg-muted/40 p-1 rounded-lg border border-transparent hover:border-border/50 transition-all duration-200">
<!-- 语言选择 -->
<div class="flex-1 min-w-0">
<Select :modelValue="env.plugin" @update:model-value="newVal => updateEnvPlugin(index, newVal as string)">
<SelectTrigger class="w-full bg-background/50 hover:bg-background border-border/50 h-9 transition-colors">
<div class="flex items-center gap-2 overflow-hidden">
<div class="h-5 w-5 rounded-full overflow-hidden shrink-0 bg-white flex items-center justify-center p-0.5" v-if="getLangIcon(env.plugin)">
<img :src="getLangIcon(env.plugin)" :alt="env.plugin" class="w-full h-full object-contain" />
</div>
<SelectValue placeholder="语言" class="text-xs" />
</div>
</SelectTrigger>
<SelectContent>
<template v-for="(_, lang) in langGroups" :key="lang">
<SelectItem v-if="lang !== 'none'" :value="lang as string">
<div class="flex items-center gap-2">
<img v-if="getLangIcon(lang as string)" :src="getLangIcon(lang as string)" class="h-3 w-3" />
<span class="text-xs">{{ lang }}</span>
</div>
</SelectItem>
</template>
</SelectContent>
</Select>
</div>
<!-- 版本选择 -->
<div class="w-28 shrink-0">
<Select :modelValue="env.version" @update:model-value="newVal => updateEnvVersion(index, newVal as string)" :disabled="!env.plugin">
<SelectTrigger class="w-full bg-background/50 hover:bg-background border-border/50 h-9 text-xs">
<SelectValue placeholder="版本" />
</SelectTrigger>
<SelectContent v-if="env.plugin && langGroups && langGroups[env.plugin]">
<SelectItem v-for="v in langGroups[env.plugin]" :key="v" :value="v" class="text-xs">
{{ v }}
</SelectItem>
</SelectContent>
</Select>
</div>
<!-- 删除按钮 -->
<Button variant="ghost" size="icon" class="h-8 w-8 text-muted-foreground hover:text-destructive shrink-0 opacity-0 group-hover:opacity-100 transition-opacity" @click="removeEnv(index)">
<X class="h-3.5 w-3.5" />
</Button>
</div>
<!-- 添加按钮 -->
<Button variant="outline" class="w-full border-dashed h-9 text-xs font-normal text-muted-foreground hover:text-foreground" @click="addEnv">
<Plus class="h-3 w-3 mr-2" /> 添加语言环境
</Button>
</div>
</div>
<div class="text-[10px] text-amber-500 bg-amber-500/10 p-3 rounded-lg border border-amber-500/20 flex gap-2">
<AlertCircle class="h-3 w-3 shrink-0 mt-0.5" />
<p>请先在「语言依赖」中安装所需的运行时。任务执行时将使用该环境,确保所有依赖已正确配置(如果是执行 <span class="font-bold">bash</span> 脚本,可随便选择一个环境即可)。</p>
</div>
<div class="text-[10px] text-muted-foreground bg-muted/40 p-3 rounded-lg border border-border/50">
<p class="font-medium mb-1">提示:</p>
<template v-if="selectedEnvs.length === 0">
<p>使用服务器默认环境运行脚本。</p>
</template>
<template v-else>
<p>将在此 Mise 隔离环境中运行脚本:</p>
<div class="mt-1 flex flex-wrap gap-1">
<span v-for="env in selectedEnvs" :key="env.plugin" class="bg-primary/10 text-primary px-1.5 py-0.5 rounded-sm" v-show="env.plugin && env.version">
{{ env.plugin }}@{{ env.version }}
</span>
</div>
</template>
</div>
</div>
<DialogFooter class="gap-2 sm:gap-0">
<Button variant="outline" size="sm" class="h-8 text-xs px-4" @click="emit('update:open', false)">取消</Button>
<Button size="sm" class="h-8 text-xs px-4" @click="emit('confirm')">确认运行</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>