chore: opt template code
This commit is contained in:
@@ -3,10 +3,7 @@ import { ref, computed, reactive, onMounted } from 'vue'
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import EmptyTableState from '@/components/ui/EmptyTableState.vue'
|
||||
import Pagination from '@/components/ui/Pagination.vue'
|
||||
@@ -34,12 +31,6 @@ interface MessageTemplate {
|
||||
modified_on: string
|
||||
}
|
||||
|
||||
interface Placeholder {
|
||||
key: string
|
||||
label: string
|
||||
default: string
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
let state = reactive({
|
||||
@@ -51,9 +42,6 @@ let state = reactive({
|
||||
status: 'all'
|
||||
})
|
||||
|
||||
const isPreviewOpen = ref(false)
|
||||
const currentTemplate = ref<MessageTemplate | null>(null)
|
||||
|
||||
// API代码查看器状态
|
||||
const isApiViewerOpen = ref(false)
|
||||
const selectedTemplateForApi = ref<MessageTemplate | null>(null)
|
||||
@@ -67,13 +55,6 @@ const isEditorOpen = ref(false)
|
||||
const isEditing = ref(false)
|
||||
const selectedTemplateForEdit = ref<MessageTemplate | null>(null)
|
||||
|
||||
const previewData = reactive({
|
||||
text: '',
|
||||
html: '',
|
||||
markdown: '',
|
||||
params: {} as Record<string, string>
|
||||
})
|
||||
|
||||
const totalPages = computed(() => Math.ceil(state.total / state.pageSize))
|
||||
|
||||
const queryListData = async (page: number, size: number, text = '', status = '') => {
|
||||
@@ -143,41 +124,6 @@ const handleViewLogs = (template: MessageTemplate) => {
|
||||
router.push(`/sendlogs?taskid=${template.id}`)
|
||||
}
|
||||
|
||||
const openPreview = async (template: MessageTemplate) => {
|
||||
currentTemplate.value = template
|
||||
|
||||
// 解析占位符
|
||||
let placeholders: Placeholder[] = []
|
||||
try {
|
||||
placeholders = JSON.parse(template.placeholders || '[]')
|
||||
} catch {}
|
||||
|
||||
// 初始化预览参数
|
||||
previewData.params = {}
|
||||
placeholders.forEach(p => {
|
||||
previewData.params[p.key] = p.default || ''
|
||||
})
|
||||
|
||||
await refreshPreview()
|
||||
isPreviewOpen.value = true
|
||||
}
|
||||
|
||||
const refreshPreview = async () => {
|
||||
if (!currentTemplate.value) return
|
||||
|
||||
try {
|
||||
const rsp = await request.post('/templates/preview', {
|
||||
id: currentTemplate.value.id,
|
||||
params: previewData.params
|
||||
})
|
||||
previewData.text = rsp.data.data.text || ''
|
||||
previewData.html = rsp.data.data.html || ''
|
||||
previewData.markdown = rsp.data.data.markdown || ''
|
||||
} catch (error: any) {
|
||||
toast.error(error.response?.data?.message || '预览失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await queryListData(1, state.pageSize)
|
||||
})
|
||||
@@ -273,13 +219,10 @@ onMounted(async () => {
|
||||
</TableCell>
|
||||
<TableCell class="whitespace-nowrap w-[160px]">{{ item.created_on }}</TableCell>
|
||||
<TableCell class="text-center space-x-2">
|
||||
<Button size="sm" variant="outline" @click="handleViewLogs(item)">日志</Button>
|
||||
<Button size="sm" variant="outline" @click="handleViewApi(item)">接口</Button>
|
||||
<Button size="sm" variant="outline" @click="openPreview(item)">预览</Button>
|
||||
<Button size="sm" variant="outline" @click="handleViewLogs(item)">日志</Button>
|
||||
<Button size="sm" variant="outline" @click="handleViewApi(item)">接口</Button>
|
||||
<Button size="sm" variant="outline" @click="openEditDialog(item)">编辑</Button>
|
||||
<Button size="sm" variant="outline" @click="handleConfigInstance(item)">实例</Button>
|
||||
|
||||
|
||||
<Button size="sm" variant="destructive" @click="deleteTemplate(item.id)">删除</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -303,68 +246,6 @@ onMounted(async () => {
|
||||
@saved="handleEditorSaved"
|
||||
/>
|
||||
|
||||
<!-- 预览对话框 -->
|
||||
<Dialog v-model:open="isPreviewOpen">
|
||||
<DialogContent class="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>模板预览 - {{ currentTemplate?.name }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div class="space-y-4 py-4">
|
||||
<!-- 参数输入 -->
|
||||
<div v-if="currentTemplate" class="space-y-2">
|
||||
<Label>填写占位符参数</Label>
|
||||
<div
|
||||
v-for="(_, key, index) in previewData.params"
|
||||
:key="key"
|
||||
class="flex gap-2 items-center"
|
||||
>
|
||||
<Label class="w-32">{{ key }}</Label>
|
||||
<Input
|
||||
v-model="previewData.params[key]"
|
||||
:placeholder="`请输入 ${key}`"
|
||||
@input="refreshPreview"
|
||||
:autofocus="false"
|
||||
:tabindex="index + 1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 预览结果 -->
|
||||
<Tabs default-value="text" class="w-full">
|
||||
<TabsList class="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="text">Text</TabsTrigger>
|
||||
<TabsTrigger value="html">HTML</TabsTrigger>
|
||||
<TabsTrigger value="markdown">Markdown</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="text">
|
||||
<div class="p-4 border rounded-md bg-muted/50">
|
||||
<pre class="whitespace-pre-wrap">{{ previewData.text || '无Text模板' }}</pre>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="html">
|
||||
<div class="space-y-2">
|
||||
<div class="p-4 border rounded-md bg-muted/50">
|
||||
<div v-html="previewData.html || '无HTML模板'"></div>
|
||||
</div>
|
||||
<details class="text-xs">
|
||||
<summary class="cursor-pointer">查看HTML源码</summary>
|
||||
<pre class="mt-2 p-2 bg-muted rounded">{{ previewData.html }}</pre>
|
||||
</details>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="markdown">
|
||||
<div class="p-4 border rounded-md bg-muted/50">
|
||||
<pre class="whitespace-pre-wrap">{{ previewData.markdown || '无Markdown模板' }}</pre>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button @click="isPreviewOpen = false">关闭</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<!-- API代码查看器 -->
|
||||
<TemplateApiViewer
|
||||
:open="isApiViewerOpen"
|
||||
|
||||
@@ -75,6 +75,71 @@ const validPlaceholders = computed(() => {
|
||||
return placeholdersList.value.filter(ph => ph.key && ph.key.trim())
|
||||
})
|
||||
|
||||
// 预览数据
|
||||
const previewData = ref({
|
||||
text: '',
|
||||
html: '',
|
||||
markdown: '',
|
||||
params: {} as Record<string, string>
|
||||
})
|
||||
|
||||
// 是否显示预览
|
||||
const showPreview = ref(false)
|
||||
|
||||
// 预览防抖定时器
|
||||
let previewDebounceTimer: number | null = null
|
||||
|
||||
// 刷新预览
|
||||
const refreshPreview = async () => {
|
||||
if (!props.isEditing || !formData.value.id) {
|
||||
// 新建模板时,直接使用当前输入的内容作为预览
|
||||
previewData.value.text = replacePreviewPlaceholders(formData.value.text_template)
|
||||
previewData.value.html = replacePreviewPlaceholders(formData.value.html_template)
|
||||
previewData.value.markdown = replacePreviewPlaceholders(formData.value.markdown_template)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const rsp = await request.post('/templates/preview', {
|
||||
id: formData.value.id,
|
||||
params: previewData.value.params
|
||||
})
|
||||
previewData.value.text = rsp.data.data.text || ''
|
||||
previewData.value.html = rsp.data.data.html || ''
|
||||
previewData.value.markdown = rsp.data.data.markdown || ''
|
||||
} catch (error: any) {
|
||||
console.error('预览失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 替换预览占位符(用于新建模板)
|
||||
const replacePreviewPlaceholders = (template: string) => {
|
||||
if (!template) return ''
|
||||
let result = template
|
||||
Object.keys(previewData.value.params).forEach(key => {
|
||||
const value = previewData.value.params[key] || `{{${key}}}`
|
||||
result = result.replace(new RegExp(`{{${key}}}`, 'g'), value)
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
// 监听模板内容变化,自动刷新预览(防抖)
|
||||
watch([
|
||||
() => formData.value.text_template,
|
||||
() => formData.value.html_template,
|
||||
() => formData.value.markdown_template,
|
||||
() => previewData.value.params
|
||||
], () => {
|
||||
if (!showPreview.value) return
|
||||
|
||||
if (previewDebounceTimer) {
|
||||
clearTimeout(previewDebounceTimer)
|
||||
}
|
||||
previewDebounceTimer = window.setTimeout(() => {
|
||||
refreshPreview()
|
||||
}, 500)
|
||||
}, { deep: true })
|
||||
|
||||
// 监听占位符列表变化,同步到 formData(使用防抖)
|
||||
let placeholderDebounceTimer: number | null = null
|
||||
watch(placeholdersList, () => {
|
||||
@@ -164,6 +229,12 @@ const loadTemplateData = (template: TemplateData) => {
|
||||
} catch {
|
||||
placeholdersList.value = []
|
||||
}
|
||||
|
||||
// 初始化预览参数
|
||||
previewData.value.params = {}
|
||||
placeholdersList.value.forEach(p => {
|
||||
previewData.value.params[p.key] = p.default || ''
|
||||
})
|
||||
}
|
||||
|
||||
// 保存模板
|
||||
@@ -263,7 +334,7 @@ watch(() => props.open, (newVal) => {
|
||||
|
||||
<!-- @提醒配置 -->
|
||||
<div class="space-y-2">
|
||||
<Label>@提醒配置 <span class="text-xs text-muted-foreground font-normal">(适用于钉钉、企业微信)</span></Label>
|
||||
<Label>@提醒配置 <span class="text-xs text-muted-foreground font-normal">(适用于钉钉、企业微信等)</span></Label>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-2">
|
||||
<div class="flex items-center gap-2 px-3 py-2 border rounded-md">
|
||||
<input
|
||||
@@ -288,6 +359,35 @@ watch(() => props.open, (newVal) => {
|
||||
</div>
|
||||
|
||||
<!-- 模板内容 -->
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<Label class="text-base font-semibold">模板内容</Label>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
@click="showPreview = !showPreview; if (showPreview) refreshPreview()"
|
||||
>
|
||||
{{ showPreview ? '隐藏预览' : '显示预览' }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- 占位符参数输入(仅在显示预览时) -->
|
||||
<div v-if="showPreview && validPlaceholders.length > 0" class="p-3 bg-muted rounded-lg space-y-2">
|
||||
<Label class="text-sm font-medium">填写占位符参数(用于预览)</Label>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||
<div v-for="ph in validPlaceholders" :key="ph.key" class="flex gap-2 items-center">
|
||||
<Label class="text-xs w-24 flex-shrink-0">{{ ph.key }}</Label>
|
||||
<Input
|
||||
v-model="previewData.params[ph.key]"
|
||||
:placeholder="ph.default || `请输入 ${ph.key}`"
|
||||
class="text-sm h-8"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs default-value="text" class="w-full">
|
||||
<TabsList class="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="text">Text</TabsTrigger>
|
||||
@@ -317,8 +417,16 @@ watch(() => props.open, (newVal) => {
|
||||
ref="textTemplateRef"
|
||||
v-model="formData.text_template"
|
||||
placeholder="请输入纯文本模板内容,可使用 {{key}} 作为占位符"
|
||||
rows="15"
|
||||
:rows="showPreview ? 10 : 15"
|
||||
/>
|
||||
|
||||
<!-- 预览区 -->
|
||||
<div v-if="showPreview" class="space-y-2">
|
||||
<Label>预览效果</Label>
|
||||
<div class="p-4 border rounded-md bg-muted/50">
|
||||
<pre class="whitespace-pre-wrap text-sm">{{ previewData.text || '无内容' }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="html" class="space-y-2">
|
||||
<div class="flex flex-col gap-1">
|
||||
@@ -343,8 +451,19 @@ watch(() => props.open, (newVal) => {
|
||||
ref="htmlTemplateRef"
|
||||
v-model="formData.html_template"
|
||||
placeholder="请输入HTML模板内容,可使用 {{key}} 作为占位符"
|
||||
rows="15"
|
||||
:rows="showPreview ? 10 : 15"
|
||||
/>
|
||||
|
||||
<!-- 预览区 -->
|
||||
<div v-if="showPreview" class="space-y-2">
|
||||
<Label>预览效果(基础渲染)</Label>
|
||||
<div class="p-4 border rounded-md bg-muted/50">
|
||||
<div v-html="previewData.html || '无内容'"></div>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
💡 HTML 预览仅显示基础结构,实际发送时可能包含邮件样式等
|
||||
</p>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="markdown" class="space-y-2">
|
||||
<div class="flex flex-col gap-1">
|
||||
@@ -369,8 +488,19 @@ watch(() => props.open, (newVal) => {
|
||||
ref="markdownTemplateRef"
|
||||
v-model="formData.markdown_template"
|
||||
placeholder="请输入Markdown模板内容,可使用 {{key}} 作为占位符"
|
||||
rows="15"
|
||||
:rows="showPreview ? 10 : 15"
|
||||
/>
|
||||
|
||||
<!-- 预览区 -->
|
||||
<div v-if="showPreview" class="space-y-2">
|
||||
<Label>预览效果(原始格式)</Label>
|
||||
<div class="p-4 border rounded-md bg-muted/50">
|
||||
<pre class="whitespace-pre-wrap text-sm">{{ previewData.markdown || '无内容' }}</pre>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
💡 Markdown 在发送时会被渲染为对应格式(钉钉、企业微信等平台支持)
|
||||
</p>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user