初始提交:OCI 面板前端(含 GenAI 网关一期)

This commit is contained in:
Wang Defa
2026-07-09 15:31:29 +08:00
commit db45a669e3
135 changed files with 25220 additions and 0 deletions
@@ -0,0 +1,162 @@
<script setup lang="ts">
import { NButton, NInput, NModal, useMessage } from 'naive-ui'
import { computed, ref, watch } from 'vue'
import { testNotifyTemplate, updateNotifyTemplate } from '@/api/settings'
import type { NotifyTemplateItem } from '@/types/api'
const props = defineProps<{
show: boolean
item: NotifyTemplateItem | null
/** 事件行的说明文案,展示在弹窗副标题 */
hint?: string
}>()
const emit = defineEmits<{
'update:show': [value: boolean]
/** 保存成功后携带最新自定义模板(空串=已恢复默认) */
saved: [kind: string, template: string]
}>()
const message = useMessage()
const draft = ref('')
const saving = ref(false)
const testing = ref(false)
const inputRef = ref<InstanceType<typeof NInput> | null>(null)
watch(
() => props.show,
(show) => {
if (show && props.item) draft.value = props.item.template || props.item.defaultTemplate
},
)
const visible = computed({
get: () => props.show,
set: (v: boolean) => emit('update:show', v),
})
/** 变量 chips:事件专属变量 + 公共变量 time */
const varChips = computed(() => [...(props.item?.vars ?? []), 'time'])
/** 包装为占位符文本;直接写在模板插值里会被 Vue 编译器误判提前闭合 */
function varToken(name: string) {
return '{{' + name + '}}'
}
/** 在光标处插入变量占位符;拿不到底层 textarea 时退化为追加 */
function insertVar(name: string) {
const token = `{{${name}}}`
const el = inputRef.value?.textareaElRef
if (!el) {
draft.value += token
return
}
const start = el.selectionStart ?? draft.value.length
const end = el.selectionEnd ?? start
draft.value = draft.value.slice(0, start) + token + draft.value.slice(end)
requestAnimationFrame(() => {
el.focus()
el.selectionStart = el.selectionEnd = start + token.length
})
}
/** 与默认模板一致或留空 → 按空串保存(恢复默认) */
function normalized() {
const text = draft.value.trim()
if (!props.item || text === props.item.defaultTemplate.trim()) return ''
return text
}
async function save() {
if (!props.item) return
saving.value = true
try {
const tpl = normalized()
await updateNotifyTemplate(props.item.kind, tpl)
message.success(tpl ? '模板已保存' : '已恢复默认模板')
emit('saved', props.item.kind, tpl)
visible.value = false
} catch (e) {
message.error(e instanceof Error ? e.message : '保存失败')
} finally {
saving.value = false
}
}
async function sendTest() {
if (!props.item) return
testing.value = true
try {
await testNotifyTemplate(props.item.kind, draft.value.trim())
message.success('测试消息已发送,请在 Telegram 查收')
} catch (e) {
message.error(e instanceof Error ? e.message : '测试发送失败')
} finally {
testing.value = false
}
}
</script>
<template>
<NModal
v-model:show="visible"
preset="card"
closable
:style="{ width: '600px', maxWidth: 'calc(100vw - 24px)' }"
>
<template #header>
<div class="flex flex-col gap-1">
<div class="text-[11px] tracking-wider text-ink-3 uppercase">
通知模板 · {{ item?.label }}
</div>
<div class="text-[17px] font-semibold">编辑推送模板</div>
<div class="text-[12.5px] font-normal text-ink-2">
{{ hint ? hint + ' · ' : '' }}Markdown 语法(加粗 / 斜体 / 代码块) Telegram 语义渲染
</div>
</div>
</template>
<div v-if="item" class="flex flex-col gap-3.5">
<div>
<div class="mb-1.5 flex items-baseline gap-2 text-[12.5px] font-medium text-ink-2">
模板内容
<span class="text-[11px] font-normal text-ink-3">留空保存 = 恢复默认模板</span>
</div>
<NInput
ref="inputRef"
v-model:value="draft"
type="textarea"
:rows="6"
class="mono"
:placeholder="item.defaultTemplate"
/>
</div>
<div>
<div class="mb-1.5 flex items-baseline gap-2 text-[12.5px] font-medium text-ink-2">
可用变量
<span class="text-[11px] font-normal text-ink-3">点击插入光标处</span>
</div>
<div class="flex flex-wrap gap-1.5">
<button
v-for="v in varChips"
:key="v"
type="button"
class="mono cursor-pointer rounded-md border border-line bg-white px-2 py-0.5 text-[11.5px] text-ink-2 hover:border-accent hover:text-accent dark:bg-transparent"
@click="insertVar(v)"
>
{{ varToken(v) }}
</button>
</div>
</div>
<div class="text-[11.5px] leading-relaxed text-ink-3">
变量在发送时替换为实际值,未知变量原样保留 · 正文超长自动截断
</div>
<div class="flex items-center gap-2 pt-1">
<NButton size="small" :loading="testing" @click="sendTest">发送测试消息</NButton>
<div class="flex-1" />
<NButton size="small" @click="visible = false">取消</NButton>
<NButton size="small" type="primary" :loading="saving" @click="save">保存</NButton>
</div>
</div>
</NModal>
</template>