初始提交:OCI 面板前端(含 GenAI 网关一期)
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user