149 lines
4.6 KiB
Vue
149 lines
4.6 KiB
Vue
<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>
|