feat: initial commit for HookPilot

This commit is contained in:
吴璨
2026-09-27 20:09:10 +08:00
commit e998d4defc
73 changed files with 10782 additions and 0 deletions
+148
View File
@@ -0,0 +1,148 @@
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue';
import { api, errorMessage } from '../../api/client';
import type { HookDetail, HookForm, ScriptVersion, ScriptVersionSummary } from '../../api/types';
import ModalDialog from '../../components/ModalDialog.vue';
import ScriptEditor from '../../components/ScriptEditor.vue';
import { confirmDialog } from '../../composables/dialog';
import { toast } from '../../composables/toast';
import { formatTime } from '../../utils/format';
import { SCRIPT_TEMPLATES } from '../../utils/templates';
const props = defineProps<{ form: HookForm; hook: HookDetail | null; versionsKey: number }>();
const versions = ref<ScriptVersionSummary[]>([]);
const viewing = ref<ScriptVersion | null>(null);
const templateIndex = ref('');
async function loadVersions() {
if (!props.hook) return;
try {
versions.value = (await api.listVersions(props.hook.id)).items;
} catch (err) {
toast.error(errorMessage(err));
}
}
async function applyTemplate() {
const template = SCRIPT_TEMPLATES[Number(templateIndex.value)];
templateIndex.value = '';
if (!template) return;
if (props.form.script.trim()) {
const ok = await confirmDialog({
title: `使用模板「${template.name}」`,
message: '当前编辑器中的内容将被替换(已保存的版本不受影响)。',
confirmText: '替换',
});
if (!ok) return;
}
props.form.script = template.content;
}
async function view(version: ScriptVersionSummary) {
if (!props.hook) return;
try {
viewing.value = await api.getVersion(props.hook.id, version.id);
} catch (err) {
toast.error(errorMessage(err));
}
}
function loadIntoEditor() {
if (!viewing.value) return;
props.form.script = viewing.value.content;
toast.info(`已载入 v${viewing.value.version} 的内容,保存后将生成新版本`);
viewing.value = null;
}
watch(() => props.versionsKey, loadVersions);
onMounted(loadVersions);
</script>
<template>
<div class="script-layout">
<div class="stack" style="min-width: 0">
<div class="row wrap">
<span class="muted small">
Bash 脚本。Tab 键缩进,参数以环境变量传入,请用 <code>"$HOOK_XXX"</code> 引用。
</span>
<span class="spacer" />
<select v-model="templateIndex" style="width: auto" @change="applyTemplate">
<option value="" disabled>插入示例模板…</option>
<option v-for="(t, i) in SCRIPT_TEMPLATES" :key="t.name" :value="String(i)">{{ t.name }} — {{ t.description }}</option>
</select>
</div>
<ScriptEditor v-model="form.script" height="520px" />
<div class="small faint">
保存时若脚本内容有变化会生成新版本;已入队的任务仍使用入队时的版本执行。
</div>
</div>
<div class="versions card">
<div class="card-header"><h3>版本记录</h3></div>
<div v-if="!hook" class="empty small">创建后生成 v1</div>
<ul v-else class="version-list">
<li v-for="v in versions" :key="v.id">
<button class="version-item" @click="view(v)">
<span class="row">
<strong>v{{ v.version }}</strong>
<span v-if="hook.currentVersion?.id === v.id" class="badge badge-info">当前</span>
</span>
<span class="small faint">{{ formatTime(v.createdAt) }}</span>
</button>
</li>
</ul>
</div>
</div>
<ModalDialog :open="!!viewing" :title="`脚本版本 v${viewing?.version ?? ''}`" wide @close="viewing = null">
<div class="small muted">保存于 {{ formatTime(viewing?.createdAt) }}</div>
<ScriptEditor v-if="viewing" :model-value="viewing.content" readonly height="420px" />
<template #footer>
<button class="btn" @click="viewing = null">关闭</button>
<button class="btn btn-primary" @click="loadIntoEditor">载入到编辑器</button>
</template>
</ModalDialog>
</template>
<style scoped>
.script-layout {
display: grid;
grid-template-columns: minmax(0, 1fr) 220px;
gap: 16px;
align-items: start;
}
.versions {
box-shadow: none;
}
.version-list {
list-style: none;
margin: 0;
padding: 6px;
max-height: 520px;
overflow-y: auto;
}
.version-item {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 2px;
width: 100%;
padding: 8px 10px;
border: none;
border-radius: 6px;
background: none;
font: inherit;
color: inherit;
cursor: pointer;
text-align: left;
}
.version-item:hover {
background: var(--surface-2);
}
@media (max-width: 1000px) {
.script-layout {
grid-template-columns: minmax(0, 1fr);
}
}
</style>