feat(extensions): add ZIP installation and unify action dialogs
This commit is contained in:
@@ -5,20 +5,38 @@ import AppDialog from './AppDialog.vue'
|
||||
import AppIcon from './AppIcon.vue'
|
||||
import { t } from '@/i18n'
|
||||
|
||||
const props = defineProps<{ kind: 'Skill' | 'Plugin'; install: (path: string) => Promise<unknown> }>()
|
||||
const props = defineProps<{ kind: 'Skill' | 'Plugin'; install: (source: string | File) => Promise<unknown> }>()
|
||||
const emit = defineEmits<{ close: []; installed: [] }>()
|
||||
const path = ref('')
|
||||
const mode = ref<'path' | 'zip'>('zip')
|
||||
const fileInput = ref<HTMLInputElement>()
|
||||
const file = ref<File | null>(null)
|
||||
const busy = ref(false)
|
||||
const error = ref('')
|
||||
const title = computed(() => t(`安装 ${props.kind}`, `Install ${props.kind}`))
|
||||
const manifest = computed(() => `${props.kind.toLowerCase()}.yaml`)
|
||||
const ready = computed(() => mode.value === 'zip' ? Boolean(file.value) : Boolean(path.value.trim()))
|
||||
|
||||
function chooseFile(event: Event) {
|
||||
const input = event.target as HTMLInputElement
|
||||
file.value = null
|
||||
error.value = ''
|
||||
const selected = input.files?.[0]
|
||||
input.value = ''
|
||||
if (!selected) return
|
||||
if (!selected.name.toLowerCase().endsWith('.zip') || !selected.size || selected.size > 10 * 1024 * 1024) {
|
||||
error.value = t('请选择非空 ZIP 文件,大小不超过 10 MiB。', 'Choose a nonempty ZIP file up to 10 MiB.')
|
||||
return
|
||||
}
|
||||
file.value = selected
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
if (busy.value || !path.value.trim()) return
|
||||
if (busy.value || !ready.value) return
|
||||
error.value = ''
|
||||
busy.value = true
|
||||
try {
|
||||
await props.install(path.value.trim())
|
||||
await props.install(mode.value === 'zip' ? file.value! : path.value.trim())
|
||||
emit('installed')
|
||||
} catch (reason) {
|
||||
error.value = reason instanceof Error ? reason.message : t('安装失败,请检查包目录后重试。', 'Installation failed. Check the package directory and retry.')
|
||||
@@ -33,8 +51,19 @@ async function submit() {
|
||||
<form class="modal extension-install-modal" :aria-busy="busy" @submit.prevent="submit">
|
||||
<span class="badge info">{{ t('扩展安装', 'Extension installation') }}</span>
|
||||
<h2>{{ title }}</h2>
|
||||
<p class="muted">{{ t('从本地包目录安装,安装时会校验清单与依赖。', 'Install from a local package directory. The manifest and dependencies are checked during installation.') }}</p>
|
||||
<div class="package-source">
|
||||
<p class="muted">{{ t('导入 ZIP 或使用本地包目录,安装时会校验清单与依赖。', 'Import a ZIP or use a local directory. The manifest and dependencies are checked during installation.') }}</p>
|
||||
<div class="source-tabs" :aria-label="t('安装来源', 'Installation source')">
|
||||
<button v-for="item in (['zip', 'path'] as const)" :key="item" type="button" class="button-secondary" :aria-pressed="mode === item" :disabled="busy" @click="mode = item; error = ''">{{ item === 'zip' ? t('ZIP 文件', 'ZIP file') : t('本地目录', 'Local directory') }}</button>
|
||||
</div>
|
||||
<div v-if="mode === 'zip'" class="package-source">
|
||||
<AppIcon :icon="FolderOpened" :size="30" />
|
||||
<input ref="fileInput" class="zip-input" type="file" accept=".zip,application/zip" :disabled="busy" :aria-label="t('选择 ZIP 扩展包', 'Choose a ZIP extension package')" @change="chooseFile" />
|
||||
<button type="button" class="button-secondary" :disabled="busy" @click="fileInput?.click()">{{ file ? t('重新选择 ZIP', 'Choose another ZIP') : t('选择 ZIP 文件', 'Choose ZIP file') }}</button>
|
||||
<strong v-if="file" class="package-name">{{ file.name }} · {{ (file.size / 1024).toFixed(1) }} KiB</strong>
|
||||
<p class="muted">{{ t('根目录或唯一顶层文件夹中须包含', 'The root or single top-level folder must contain') }} <code>{{ manifest }}</code></p>
|
||||
<p class="subtle">{{ t('ZIP 最大 10 MiB,解压后最大 50 MiB,最多 2048 个条目。', 'Up to 10 MiB compressed, 50 MiB extracted, and 2048 entries.') }}</p>
|
||||
</div>
|
||||
<div v-else class="package-source">
|
||||
<AppIcon :icon="FolderOpened" :size="30" />
|
||||
<strong>{{ t('本地包目录', 'Local package directory') }}</strong>
|
||||
<p class="muted">{{ t('选择包含以下清单的完整解压目录:', 'Use the extracted directory containing:') }} <code>{{ manifest }}</code></p>
|
||||
@@ -42,13 +71,13 @@ async function submit() {
|
||||
<span>{{ t('目录路径', 'Directory path') }}</span>
|
||||
<input v-model="path" class="input" autofocus required :disabled="busy" :placeholder="t('粘贴本地包目录的完整路径', 'Paste the full package directory path')" aria-describedby="extension-path-help" />
|
||||
</label>
|
||||
<p id="extension-path-help" class="subtle">{{ t('路径须位于 AI Core 所在电脑。ZIP 请先解压,再填写目录路径。', 'The directory must be on the AI Core computer. Extract ZIP packages before entering the directory path.') }}</p>
|
||||
<p id="extension-path-help" class="subtle">{{ t('路径须位于 AI Core 所在电脑。', 'The directory must be on the AI Core computer.') }}</p>
|
||||
</div>
|
||||
<div v-if="error" class="error-banner" role="alert">{{ error }}</div>
|
||||
<p v-if="busy" class="muted" role="status">{{ t('正在校验并安装,请稍候…', 'Validating and installing…') }}</p>
|
||||
<footer class="install-actions">
|
||||
<button type="button" class="button-secondary" :disabled="busy" @click="emit('close')">{{ t('取消', 'Cancel') }}</button>
|
||||
<button type="submit" class="button-primary" :disabled="busy || !path.trim()">{{ busy ? t('安装中…', 'Installing…') : title }}</button>
|
||||
<button type="submit" class="button-primary" :disabled="busy || !ready">{{ busy ? t('安装中…', 'Installing…') : title }}</button>
|
||||
</footer>
|
||||
</form>
|
||||
</AppDialog>
|
||||
@@ -63,4 +92,8 @@ h2 { margin: var(--space-sm) 0 var(--space-md); }
|
||||
.package-field input { min-width: 0; }
|
||||
.install-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: var(--space-sm); margin-top: var(--space-lg); }
|
||||
.error-banner { overflow-wrap: anywhere; }
|
||||
.source-tabs { display: flex; gap: var(--space-sm); margin-top: var(--space-lg); }
|
||||
.source-tabs [aria-pressed="true"] { border-color: var(--color-accent-primary); color: var(--color-accent-primary); background: var(--color-accent-soft); }
|
||||
.zip-input { display: none; }
|
||||
.package-name { overflow-wrap: anywhere; max-width: 100%; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user