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.
 
 
 
 
 
 

304 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()">&larr; 返回列表</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('zh') // 默认只显示中文模板
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)
}
// 语言基码:zh-CN/zh-TW/zh-HK → zh,让"默认中文"能涵盖所有中文变体,与后端 baseLang 语义一致
function baseLang(s?: string) {
return (s || '').split('-')[0].trim().toLowerCase()
}
// 筛选下拉选项(基于现有数据)
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 && baseLang(t.language) !== baseLang(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>