You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
299 lines
12 KiB
299 lines
12 KiB
<template>
|
|
<div>
|
|
<!-- 列表视图 -->
|
|
<div v-show="view === 'list'">
|
|
<div class="flex items-center justify-between mb-5">
|
|
<div>
|
|
<h2 class="text-lg font-semibold text-slate-800">会议模板</h2>
|
|
<p class="text-xs text-slate-400 mt-1">会议纪要公共模板(source=public),所有应用共享。修改后各服务约 10 分钟内自动同步。</p>
|
|
</div>
|
|
<button class="btn btn-primary btn-sm text-white" @click="showForm()">+ 新增模板</button>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-3 mb-4 flex-wrap">
|
|
<span class="text-sm text-slate-500">语言</span>
|
|
<select v-model="langFilter" class="select select-bordered select-sm">
|
|
<option value="">全部语言</option>
|
|
<option v-for="l in langOptions" :key="l" :value="l">{{ l }}</option>
|
|
</select>
|
|
<span class="text-sm text-slate-500">分组(tid)</span>
|
|
<select v-model="tidFilter" class="select select-bordered select-sm">
|
|
<option value="">全部分组</option>
|
|
<option v-for="t in tidOptions" :key="t" :value="t">{{ t }}</option>
|
|
</select>
|
|
<input v-model="kwFilter" class="input input-bordered input-sm w-48" placeholder="按标题搜索" />
|
|
</div>
|
|
|
|
<div class="bg-white rounded-xl shadow-sm border border-slate-100">
|
|
<div class="overflow-x-auto">
|
|
<table class="table table-zebra">
|
|
<thead>
|
|
<tr>
|
|
<th>ID</th><th>标题</th><th>模板标识(tid)</th><th>语言</th><th>类型</th><th>标签</th><th class="text-right">操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-if="loading">
|
|
<td colspan="7" class="text-center py-8"><span class="loading loading-spinner loading-sm"></span></td>
|
|
</tr>
|
|
<tr v-else-if="!filteredRows.length">
|
|
<td colspan="7" class="text-center text-slate-400 py-8">暂无模板</td>
|
|
</tr>
|
|
<template v-for="g in groupedRows" v-else :key="g.key">
|
|
<tr class="bg-slate-100/80">
|
|
<td colspan="7" class="py-2">
|
|
<span class="font-semibold text-slate-600 text-sm">▸ {{ g.label }}</span>
|
|
<span class="text-xs text-slate-400 ml-2">{{ g.items.length }} 个版本</span>
|
|
</td>
|
|
</tr>
|
|
<tr v-for="t in g.items" :key="t.id" class="hover">
|
|
<td class="font-mono text-sm text-slate-400">{{ t.id || '' }}</td>
|
|
<td class="font-medium">{{ t.title || '' }}</td>
|
|
<td class="font-mono text-sm">{{ t.tid || '' }}</td>
|
|
<td><span class="badge badge-outline badge-sm">{{ t.language || '-' }}</span></td>
|
|
<td class="text-sm">{{ t.ttype || '' }}</td>
|
|
<td class="text-sm">
|
|
<template v-if="tagList(t.tags).length">
|
|
<span v-for="(tag, i) in tagList(t.tags)" :key="i" class="badge badge-ghost badge-sm mr-1">{{ tag }}</span>
|
|
</template>
|
|
<span v-else class="text-slate-300">-</span>
|
|
</td>
|
|
<td class="text-right">
|
|
<div class="inline-flex gap-1">
|
|
<button class="btn btn-ghost btn-xs" @click="showForm(t)">编辑</button>
|
|
<button class="btn btn-ghost btn-xs text-red-500" @click="delTpl(t)">删除</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 表单视图 -->
|
|
<div v-show="view === 'form'">
|
|
<div class="flex items-center gap-3 mb-5">
|
|
<button class="btn btn-ghost btn-sm" @click="showList()">← 返回列表</button>
|
|
<h2 class="text-lg font-semibold text-slate-800">{{ isEditing ? '编辑模板' : '新增模板' }}</h2>
|
|
</div>
|
|
<div class="bg-white rounded-xl shadow-sm border border-slate-100 p-6 max-w-3xl">
|
|
<form @submit.prevent="saveTpl">
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div class="form-control mb-4">
|
|
<label class="label"><span class="label-text font-medium">标题 *</span></label>
|
|
<input v-model="form.title" type="text" class="input input-bordered w-full" required />
|
|
</div>
|
|
<div class="form-control mb-4">
|
|
<label class="label"><span class="label-text font-medium">模板标识 (tid)</span></label>
|
|
<input v-model="form.tid" type="text" class="input input-bordered w-full" placeholder="同一 tid 可有多语言版本" />
|
|
</div>
|
|
<div class="form-control mb-4">
|
|
<label class="label"><span class="label-text font-medium">语言</span></label>
|
|
<input v-model="form.language" type="text" class="input input-bordered w-full" placeholder="如 zh / en" list="langlist" />
|
|
<datalist id="langlist">
|
|
<option value="zh" />
|
|
<option value="en" />
|
|
<option value="ja" />
|
|
<option value="ko" />
|
|
</datalist>
|
|
</div>
|
|
<div class="form-control mb-4">
|
|
<label class="label"><span class="label-text font-medium">类型 (ttype)</span></label>
|
|
<input v-model="form.ttype" type="text" class="input input-bordered w-full" placeholder="如 meeting / interview" />
|
|
</div>
|
|
<div class="form-control mb-4">
|
|
<label class="label"><span class="label-text font-medium">标签 (tags)</span></label>
|
|
<input v-model="form.tags" type="text" class="input input-bordered w-full" placeholder="逗号分隔" />
|
|
</div>
|
|
<div class="form-control mb-4">
|
|
<label class="label"><span class="label-text font-medium">图标 (icon)</span></label>
|
|
<input v-model="form.icon" type="text" class="input input-bordered w-full" placeholder="图标 URL 或名称" />
|
|
</div>
|
|
</div>
|
|
<div class="form-control mb-4">
|
|
<label class="label"><span class="label-text font-medium">描述</span></label>
|
|
<input v-model="form.description" type="text" class="input input-bordered w-full" />
|
|
</div>
|
|
<div class="form-control mb-4">
|
|
<label class="label"><span class="label-text font-medium">大纲 (outline)</span></label>
|
|
<textarea v-model="form.outline" class="textarea textarea-bordered w-full font-mono text-sm" rows="4"></textarea>
|
|
</div>
|
|
<div class="form-control mb-6">
|
|
<label class="label"><span class="label-text font-medium">模板内容 (template)</span></label>
|
|
<textarea v-model="form.template" class="textarea textarea-bordered w-full font-mono text-sm" rows="10"></textarea>
|
|
</div>
|
|
<div class="flex gap-3">
|
|
<button type="button" class="btn btn-ghost" @click="showList()">取消</button>
|
|
<button type="submit" class="btn btn-primary text-white" :disabled="submitting">保存</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, reactive, computed, onMounted } from 'vue'
|
|
import { useApi } from '~/composables/useApi'
|
|
import { useToast } from '~/composables/useToast'
|
|
|
|
definePageMeta({ title: '会议模板' })
|
|
|
|
interface MeetTemplate {
|
|
id: number
|
|
title: string
|
|
tid: string
|
|
language: string
|
|
ttype: string
|
|
tags: string
|
|
icon: string
|
|
description: string
|
|
outline: string
|
|
template: string
|
|
}
|
|
|
|
const { webApi } = useApi()
|
|
const { success, error } = useToast()
|
|
|
|
const view = ref<'list' | 'form'>('list')
|
|
const rows = ref<MeetTemplate[]>([])
|
|
const loading = ref(false)
|
|
const submitting = ref(false)
|
|
const isEditing = ref(false)
|
|
|
|
const langFilter = ref('')
|
|
const tidFilter = ref('')
|
|
const kwFilter = ref('')
|
|
|
|
const form = reactive<MeetTemplate>({
|
|
id: 0, title: '', tid: '', language: '', ttype: '', tags: '',
|
|
icon: '', description: '', outline: '', template: '',
|
|
})
|
|
|
|
function tagList(s?: string) {
|
|
return (s || '').split(',').map((x) => x.trim()).filter(Boolean)
|
|
}
|
|
|
|
// 筛选下拉选项(基于现有数据)
|
|
const langOptions = computed(() => {
|
|
const set = new Set<string>()
|
|
rows.value.forEach((t) => { if (t.language) set.add(t.language) })
|
|
return Array.from(set).sort()
|
|
})
|
|
const tidOptions = computed(() => {
|
|
const set = new Set<string>()
|
|
rows.value.forEach((t) => { if (t.tid) set.add(t.tid) })
|
|
return Array.from(set).sort()
|
|
})
|
|
|
|
// 过滤后的行
|
|
const filteredRows = computed(() => {
|
|
const kw = kwFilter.value.trim().toLowerCase()
|
|
return rows.value.filter((t) => {
|
|
if (langFilter.value && (t.language || '') !== langFilter.value) return false
|
|
if (tidFilter.value && (t.tid || '') !== tidFilter.value) return false
|
|
if (kw && (t.title || '').toLowerCase().indexOf(kw) < 0) return false
|
|
return true
|
|
})
|
|
})
|
|
|
|
// 按 tid 分组(无 tid 排最前)
|
|
const groupedRows = computed(() => {
|
|
const groups: Record<string, MeetTemplate[]> = {}
|
|
filteredRows.value.forEach((t) => {
|
|
const g = t.tid || ''
|
|
;(groups[g] = groups[g] || []).push(t)
|
|
})
|
|
const names = Object.keys(groups).sort((a, b) => {
|
|
if (a === '') return -1
|
|
if (b === '') return 1
|
|
return a < b ? -1 : 1
|
|
})
|
|
return names.map((g) => ({ key: g, label: g || '无 tid', items: groups[g]! }))
|
|
})
|
|
|
|
async function loadData() {
|
|
loading.value = true
|
|
try {
|
|
const d = await webApi<{ templates: MeetTemplate[] }>('getmeettemplates', {})
|
|
rows.value = d?.templates ?? []
|
|
} catch (e: any) {
|
|
error(e?.message ?? '加载失败')
|
|
rows.value = []
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function showList() {
|
|
view.value = 'list'
|
|
loadData()
|
|
}
|
|
|
|
function fillForm(t: Partial<MeetTemplate>) {
|
|
form.id = t.id || 0
|
|
form.title = t.title || ''
|
|
form.tid = t.tid || ''
|
|
form.language = t.language || ''
|
|
form.ttype = t.ttype || ''
|
|
form.tags = t.tags || ''
|
|
form.icon = t.icon || ''
|
|
form.description = t.description || ''
|
|
form.outline = t.outline || ''
|
|
form.template = t.template || ''
|
|
}
|
|
|
|
// 新增:不传;编辑:先按 id 拉详情(列表不含 outline/template 大字段)
|
|
async function showForm(t?: MeetTemplate) {
|
|
view.value = 'form'
|
|
if (t && t.id) {
|
|
isEditing.value = true
|
|
fillForm(t)
|
|
try {
|
|
const detail = await webApi<MeetTemplate>('getmeettemplate', { id: t.id })
|
|
if (detail) fillForm(detail)
|
|
} catch (e: any) {
|
|
error(e?.message ?? '加载详情失败')
|
|
}
|
|
} else {
|
|
isEditing.value = false
|
|
fillForm({})
|
|
}
|
|
}
|
|
|
|
async function saveTpl() {
|
|
submitting.value = true
|
|
try {
|
|
const data: Record<string, unknown> = {
|
|
title: form.title.trim(),
|
|
tid: form.tid.trim(),
|
|
language: form.language.trim(),
|
|
ttype: form.ttype.trim(),
|
|
tags: form.tags.trim(),
|
|
icon: form.icon.trim(),
|
|
description: form.description.trim(),
|
|
outline: form.outline,
|
|
template: form.template,
|
|
}
|
|
if (isEditing.value) data.id = Number(form.id)
|
|
await webApi(isEditing.value ? 'updatemeettemplate' : 'addmeettemplate', data)
|
|
success('保存成功')
|
|
showList()
|
|
} catch (e: any) {
|
|
error(e?.message ?? '保存失败')
|
|
} finally {
|
|
submitting.value = false
|
|
}
|
|
}
|
|
|
|
function delTpl(t: MeetTemplate) {
|
|
if (!window.confirm(`确定删除模板 "${t.title || ''}" 吗?`)) return
|
|
webApi('delmeettemplate', { ids: [t.id] })
|
|
.then(() => { success('删除成功'); loadData() })
|
|
.catch((e: any) => error(e?.message ?? '删除失败'))
|
|
}
|
|
|
|
onMounted(loadData)
|
|
</script>
|
|
|