4180f73c53
- 订单列表重构为卡片式布局并新增筛选功能 - 设置管理支持struct/struct_list类型配置 - 新增短信签名和模板独立管理页面 - 通知渠道新增短信渠道配置 - 产品参数管理优化 Co-authored-by: Cursor <cursoragent@cursor.com>
633 lines
27 KiB
Vue
633 lines
27 KiB
Vue
<template>
|
||
<div class="sms-template-page">
|
||
<div class="page-header">
|
||
<div class="header-info">
|
||
<div class="header-icon">
|
||
<svg viewBox="0 0 24 24" width="28" height="28" fill="none" stroke="#409eff" stroke-width="1.8">
|
||
<path d="M4 4h16v16H4z" rx="2"/>
|
||
<path d="M8 8h8M8 12h6M8 16h4" stroke-linecap="round"/>
|
||
</svg>
|
||
</div>
|
||
<div>
|
||
<h2 class="header-title">短信模板管理</h2>
|
||
<p class="header-desc">管理用户短信模板的审核、推荐模板的维护</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 服务选择 -->
|
||
<div class="filter-bar">
|
||
<el-select v-model="serviceId" placeholder="选择主控服务" style="width: 200px" @change="onServiceChange">
|
||
<el-option v-for="s in serviceOptions" :key="s.id" :label="s.name" :value="s.id" />
|
||
</el-select>
|
||
</div>
|
||
|
||
<!-- 主内容 Tabs -->
|
||
<el-tabs v-model="activeTab" type="border-card">
|
||
<!-- 用户模板 -->
|
||
<el-tab-pane label="用户模板" name="user">
|
||
<div class="tab-toolbar">
|
||
<div class="tab-filters">
|
||
<el-select v-model="tplQuery.status" placeholder="全部状态" clearable style="width: 130px" @change="fetchTemplates">
|
||
<el-option label="草稿" :value="0" />
|
||
<el-option label="审核中" :value="1" />
|
||
<el-option label="已通过" :value="2" />
|
||
<el-option label="已驳回" :value="3" />
|
||
</el-select>
|
||
<el-input
|
||
v-if="tplFilterUserInfo"
|
||
:model-value="`${tplFilterUserInfo.user_name} (ID: ${tplQuery.user_id})`"
|
||
readonly
|
||
style="width: 200px"
|
||
>
|
||
<template #suffix>
|
||
<el-icon class="clear-icon" @click="clearTplFilterUser"><Close /></el-icon>
|
||
</template>
|
||
<template #prefix><el-icon><User /></el-icon></template>
|
||
</el-input>
|
||
<el-input v-else placeholder="筛选用户" readonly style="width: 140px" @click="tplFilterUserSelectorVisible = true">
|
||
<template #prefix><el-icon><User /></el-icon></template>
|
||
</el-input>
|
||
<el-button type="primary" @click="fetchTemplates">搜索</el-button>
|
||
</div>
|
||
<el-button type="primary" @click="handleAddTemplate">
|
||
<el-icon><Plus /></el-icon> 新增模板
|
||
</el-button>
|
||
</div>
|
||
|
||
<el-table :data="tplList" v-loading="tplLoading" stripe border style="width: 100%">
|
||
<el-table-column prop="ID" label="ID" width="70" align="center" />
|
||
<el-table-column label="用户ID" width="90">
|
||
<template #default="{ row }">
|
||
<el-link type="primary" :underline="false" @click="$router.push({ path: '/user/detail', query: { user_id: row.user_id } })">{{ row.user_id }}</el-link>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="name" label="模板名称" min-width="140" />
|
||
<el-table-column label="内容" min-width="200">
|
||
<template #default="{ row }">
|
||
<el-tooltip :content="row.content" placement="top" :show-after="300">
|
||
<span class="content-cell">{{ row.content }}</span>
|
||
</el-tooltip>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="参数" width="150">
|
||
<template #default="{ row }">
|
||
<template v-if="row.params && row.params.length">
|
||
<el-tag v-for="p in row.params" :key="p.key" size="small" style="margin: 2px">{{ p.key }}</el-tag>
|
||
</template>
|
||
<span v-else class="text-muted">-</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="状态" width="100">
|
||
<template #default="{ row }">
|
||
<el-tag :type="statusTagType(row.status)" size="small">{{ statusText(row.status) }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="驳回原因" min-width="130">
|
||
<template #default="{ row }">
|
||
<span v-if="row.reject_reason" class="reject-text">{{ row.reject_reason }}</span>
|
||
<span v-else class="text-muted">-</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="280" fixed="right">
|
||
<template #default="{ row }">
|
||
<el-button type="primary" link @click="handleEditTemplate(row)">编辑</el-button>
|
||
<el-button v-if="row.status === 0" type="warning" link @click="handleSubmitTpl(row)">提交审核</el-button>
|
||
<el-button v-if="row.status === 1" type="success" link @click="handleApproveTpl(row)">通过</el-button>
|
||
<el-button v-if="row.status === 1" type="danger" link @click="handleRejectTpl(row)">驳回</el-button>
|
||
<el-button type="danger" link @click="handleDeleteTpl(row)">删除</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<el-pagination
|
||
v-model:current-page="tplQuery.page"
|
||
v-model:page-size="tplQuery.count"
|
||
:page-sizes="[10, 20, 50]"
|
||
layout="total, sizes, prev, pager, next"
|
||
:total="tplTotal"
|
||
@size-change="fetchTemplates"
|
||
@current-change="fetchTemplates"
|
||
background
|
||
class="pagination"
|
||
/>
|
||
</el-tab-pane>
|
||
|
||
<!-- 推荐模板 -->
|
||
<el-tab-pane label="推荐模板" name="recommended">
|
||
<div class="tab-toolbar">
|
||
<div></div>
|
||
<el-button type="primary" @click="handleAddRecommended">
|
||
<el-icon><Plus /></el-icon> 新增推荐模板
|
||
</el-button>
|
||
</div>
|
||
|
||
<el-table :data="recList" v-loading="recLoading" stripe border style="width: 100%">
|
||
<el-table-column prop="id" label="ID" width="70" align="center" />
|
||
<el-table-column prop="name" label="名称" min-width="140" />
|
||
<el-table-column label="分类" width="120">
|
||
<template #default="{ row }">
|
||
<el-tag v-if="row.category" size="small">{{ row.category }}</el-tag>
|
||
<span v-else class="text-muted">-</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="内容" min-width="220">
|
||
<template #default="{ row }">
|
||
<el-tooltip :content="row.content" placement="top" :show-after="300">
|
||
<span class="content-cell">{{ row.content }}</span>
|
||
</el-tooltip>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="参数" width="150">
|
||
<template #default="{ row }">
|
||
<template v-if="row.params && row.params.length">
|
||
<el-tag v-for="p in row.params" :key="p.key" size="small" style="margin: 2px">{{ p.key }}</el-tag>
|
||
</template>
|
||
<span v-else class="text-muted">-</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="150" fixed="right">
|
||
<template #default="{ row }">
|
||
<el-button type="primary" link @click="handleEditRecommended(row)">编辑</el-button>
|
||
<el-button type="danger" link @click="handleDeleteRecommended(row)">删除</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<el-pagination
|
||
v-model:current-page="recQuery.page"
|
||
v-model:page-size="recQuery.count"
|
||
:page-sizes="[10, 20, 50]"
|
||
layout="total, sizes, prev, pager, next"
|
||
:total="recTotal"
|
||
@size-change="fetchRecommended"
|
||
@current-change="fetchRecommended"
|
||
background
|
||
class="pagination"
|
||
/>
|
||
</el-tab-pane>
|
||
</el-tabs>
|
||
|
||
<!-- 用户模板表单对话框 -->
|
||
<el-dialog v-model="tplDialogVisible" :title="tplDialogType === 'add' ? '新增模板' : '编辑模板'" width="640px" append-to-body destroy-on-close>
|
||
<el-form ref="tplFormRef" :model="tplForm" :rules="tplFormRules" label-width="100px">
|
||
<el-form-item label="用户" prop="user_id">
|
||
<el-input
|
||
v-if="tplSelectedUser"
|
||
:model-value="`${tplSelectedUser.user_name} (ID: ${tplForm.user_id})`"
|
||
readonly
|
||
>
|
||
<template #suffix>
|
||
<el-icon v-if="tplDialogType === 'add'" class="clear-icon" @click="clearTplUser"><Close /></el-icon>
|
||
</template>
|
||
<template #append>
|
||
<el-button @click="tplUserSelectorVisible = true" :disabled="tplDialogType === 'edit'"><el-icon><User /></el-icon></el-button>
|
||
</template>
|
||
</el-input>
|
||
<el-input v-else placeholder="请选择用户" readonly @click="openTplUserSelector">
|
||
<template #append>
|
||
<el-button @click="openTplUserSelector" :disabled="tplDialogType === 'edit'"><el-icon><User /></el-icon></el-button>
|
||
</template>
|
||
</el-input>
|
||
</el-form-item>
|
||
<el-form-item label="模板名称" prop="name">
|
||
<el-input v-model="tplForm.name" placeholder="请输入模板名称" maxlength="50" show-word-limit />
|
||
</el-form-item>
|
||
<el-form-item label="推荐模板">
|
||
<el-select v-model="tplForm.recommended_id" placeholder="选择推荐模板(可选)" clearable style="width: 100%" @change="onRecommendedSelect">
|
||
<el-option v-for="r in recOptionsCache" :key="r.id" :label="r.name" :value="r.id" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="模板参数">
|
||
<div class="params-editor">
|
||
<div v-for="(p, idx) in tplForm.paramsList" :key="idx" class="param-row">
|
||
<el-input v-model="p.key" placeholder="参数名" style="width: 100px" />
|
||
<el-select v-model="p.type" placeholder="类型" style="width: 100px">
|
||
<el-option label="字符串" value="string" />
|
||
<el-option label="数字" value="number" />
|
||
<el-option label="时间" value="date" />
|
||
</el-select>
|
||
<el-input-number v-model="p.max_len" :min="1" :max="999" placeholder="长度" style="width: 100px" />
|
||
<el-button type="danger" link @click="tplForm.paramsList.splice(idx, 1)"><el-icon><Delete /></el-icon></el-button>
|
||
</div>
|
||
<el-button type="primary" link @click="tplForm.paramsList.push({ key: '', type: 'string', max_len: 20 })">
|
||
<el-icon><Plus /></el-icon> 添加参数
|
||
</el-button>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="模板内容" prop="content">
|
||
<el-input v-model="tplForm.content" type="textarea" :rows="4" placeholder="如:您的验证码为{code},有效期{time}分钟。" />
|
||
<div v-if="tplForm.paramsList.length" class="insert-btns">
|
||
<span class="insert-label">插入参数:</span>
|
||
<el-button v-for="p in tplForm.paramsList.filter(x => x.key)" :key="p.key" size="small" @click="insertParam(p.key)">{{ '{' + p.key + '}' }}</el-button>
|
||
</div>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="tplDialogVisible = false">取消</el-button>
|
||
<el-button type="primary" :loading="submitting" @click="submitTplForm">确定</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 推荐模板表单对话框 -->
|
||
<el-dialog v-model="recDialogVisible" :title="recDialogType === 'add' ? '新增推荐模板' : '编辑推荐模板'" width="600px" append-to-body destroy-on-close>
|
||
<el-form ref="recFormRef" :model="recForm" :rules="recFormRules" label-width="100px">
|
||
<el-form-item label="模板名称" prop="name">
|
||
<el-input v-model="recForm.name" placeholder="请输入名称" maxlength="50" show-word-limit />
|
||
</el-form-item>
|
||
<el-form-item label="分类">
|
||
<el-input v-model="recForm.category" placeholder="如:验证码、通知、营销" />
|
||
</el-form-item>
|
||
<el-form-item label="模板参数">
|
||
<div class="params-editor">
|
||
<div v-for="(p, idx) in recForm.paramsList" :key="idx" class="param-row">
|
||
<el-input v-model="p.key" placeholder="参数名" style="width: 100px" />
|
||
<el-select v-model="p.type" placeholder="类型" style="width: 100px">
|
||
<el-option label="字符串" value="string" />
|
||
<el-option label="数字" value="number" />
|
||
<el-option label="时间" value="date" />
|
||
</el-select>
|
||
<el-input-number v-model="p.max_len" :min="1" :max="999" placeholder="长度" style="width: 100px" />
|
||
<el-button type="danger" link @click="recForm.paramsList.splice(idx, 1)"><el-icon><Delete /></el-icon></el-button>
|
||
</div>
|
||
<el-button type="primary" link @click="recForm.paramsList.push({ key: '', type: 'string', max_len: 20 })">
|
||
<el-icon><Plus /></el-icon> 添加参数
|
||
</el-button>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="模板内容" prop="content">
|
||
<el-input v-model="recForm.content" type="textarea" :rows="4" placeholder="模板内容,参数使用 {key} 格式" />
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="recDialogVisible = false">取消</el-button>
|
||
<el-button type="primary" :loading="submitting" @click="submitRecForm">确定</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 驳回对话框 -->
|
||
<el-dialog v-model="rejectDialogVisible" title="驳回模板" width="420px" append-to-body>
|
||
<el-form label-width="80px">
|
||
<el-form-item label="驳回原因">
|
||
<el-input v-model="rejectReason" type="textarea" :rows="3" placeholder="请输入驳回原因" />
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="rejectDialogVisible = false">取消</el-button>
|
||
<el-button type="danger" :loading="submitting" @click="confirmRejectTpl">确认驳回</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 表单用户选择器 -->
|
||
<UserListSelector
|
||
v-model="tplUserSelectorVisible"
|
||
:current-user-id="tplForm.user_id"
|
||
@confirm="handleTplUserSelect"
|
||
/>
|
||
<!-- 筛选用户选择器 -->
|
||
<UserListSelector
|
||
v-model="tplFilterUserSelectorVisible"
|
||
:current-user-id="tplQuery.user_id ? Number(tplQuery.user_id) : undefined"
|
||
@confirm="handleTplFilterUserSelect"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, reactive, onMounted } from 'vue'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import { Plus, Delete, User, Close } from '@element-plus/icons-vue'
|
||
import { formatDate } from '@/utils/tool'
|
||
import UserListSelector from '@/components/admin/UserListSelector.vue'
|
||
import {
|
||
getSmsServiceList,
|
||
getSmsTemplateList, createSmsTemplate, updateSmsTemplate, deleteSmsTemplate,
|
||
submitSmsTemplate, approveSmsTemplate, rejectSmsTemplate,
|
||
getSmsRecommendedTemplateList, createSmsRecommendedTemplate, updateSmsRecommendedTemplate, deleteSmsRecommendedTemplate
|
||
} from '@/api/admin/smsService'
|
||
|
||
const serviceOptions = ref([])
|
||
const serviceId = ref(null)
|
||
const activeTab = ref('user')
|
||
const submitting = ref(false)
|
||
|
||
const statusText = (s) => ({ 0: '草稿', 1: '审核中', 2: '已通过', 3: '已驳回' }[s] || '未知')
|
||
const statusTagType = (s) => ({ 0: 'info', 1: 'warning', 2: 'success', 3: 'danger' }[s] || 'info')
|
||
|
||
// ========== 服务选择 ==========
|
||
const loadServices = async () => {
|
||
try {
|
||
const res = await getSmsServiceList({ page: 1, count: 199 })
|
||
if (res.data.code === 200) {
|
||
const list = res.data.data?.data || res.data.data || []
|
||
serviceOptions.value = Array.isArray(list) ? list : []
|
||
if (serviceOptions.value.length && !serviceId.value) {
|
||
serviceId.value = serviceOptions.value[0].id
|
||
}
|
||
}
|
||
} catch (e) { console.error(e) }
|
||
}
|
||
|
||
const onServiceChange = () => {
|
||
fetchTemplates()
|
||
fetchRecommended()
|
||
}
|
||
|
||
// ========== 用户模板 ==========
|
||
const tplLoading = ref(false)
|
||
const tplList = ref([])
|
||
const tplTotal = ref(0)
|
||
const tplQuery = reactive({ page: 1, count: 10, user_id: '', status: null })
|
||
const tplFilterUserSelectorVisible = ref(false)
|
||
const tplFilterUserInfo = ref(null)
|
||
|
||
const handleTplFilterUserSelect = (user) => {
|
||
tplQuery.user_id = String(user.user_id)
|
||
tplFilterUserInfo.value = user
|
||
fetchTemplates()
|
||
}
|
||
const clearTplFilterUser = () => {
|
||
tplQuery.user_id = ''
|
||
tplFilterUserInfo.value = null
|
||
fetchTemplates()
|
||
}
|
||
|
||
const fetchTemplates = async () => {
|
||
if (!serviceId.value) return
|
||
tplLoading.value = true
|
||
try {
|
||
const params = { service_id: serviceId.value, page: tplQuery.page, count: tplQuery.count }
|
||
if (tplQuery.user_id) params.user_id = Number(tplQuery.user_id)
|
||
if (tplQuery.status !== null && tplQuery.status !== '') params.status = tplQuery.status
|
||
const res = await getSmsTemplateList(params)
|
||
if (res.data.code === 200) {
|
||
const d = res.data.data
|
||
tplList.value = d?.data || d || []
|
||
tplTotal.value = d?.all_count || 0
|
||
}
|
||
} catch { ElMessage.error('获取模板列表失败') }
|
||
finally { tplLoading.value = false }
|
||
}
|
||
|
||
// 模板表单
|
||
const tplDialogVisible = ref(false)
|
||
const tplDialogType = ref('add')
|
||
const tplFormRef = ref(null)
|
||
const tplForm = reactive({ user_id: null, name: '', content: '', paramsList: [], recommended_id: null, template_id: null })
|
||
const tplUserSelectorVisible = ref(false)
|
||
const tplSelectedUser = ref(null)
|
||
|
||
const openTplUserSelector = () => {
|
||
if (tplDialogType.value === 'edit') return
|
||
tplUserSelectorVisible.value = true
|
||
}
|
||
const handleTplUserSelect = (user) => {
|
||
tplForm.user_id = user.user_id
|
||
tplSelectedUser.value = user
|
||
}
|
||
const clearTplUser = () => {
|
||
tplForm.user_id = null
|
||
tplSelectedUser.value = null
|
||
}
|
||
const tplFormRules = {
|
||
user_id: [{ required: true, message: '请输入用户ID', trigger: 'blur' }],
|
||
name: [{ required: true, message: '请输入模板名称', trigger: 'blur' }],
|
||
content: [{ required: true, message: '请输入模板内容', trigger: 'blur' }]
|
||
}
|
||
const recOptionsCache = ref([])
|
||
|
||
const handleAddTemplate = () => {
|
||
tplDialogType.value = 'add'
|
||
tplSelectedUser.value = null
|
||
Object.assign(tplForm, { user_id: null, name: '', content: '', paramsList: [], recommended_id: null, template_id: null })
|
||
loadRecOptionsCache()
|
||
tplDialogVisible.value = true
|
||
}
|
||
|
||
const handleEditTemplate = (row) => {
|
||
tplDialogType.value = 'edit'
|
||
tplSelectedUser.value = { user_id: row.user_id, user_name: `用户${row.user_id}` }
|
||
const params = Array.isArray(row.params) ? row.params.map(p => ({ ...p })) : []
|
||
Object.assign(tplForm, { user_id: row.user_id, name: row.name, content: row.content, paramsList: params, recommended_id: row.recommended_id || null, template_id: row.ID })
|
||
loadRecOptionsCache()
|
||
tplDialogVisible.value = true
|
||
}
|
||
|
||
const loadRecOptionsCache = async () => {
|
||
if (!serviceId.value) return
|
||
try {
|
||
const res = await getSmsRecommendedTemplateList({ service_id: serviceId.value, page: 1, count: 100 })
|
||
if (res.data.code === 200) {
|
||
recOptionsCache.value = res.data.data?.data || res.data.data || []
|
||
}
|
||
} catch {}
|
||
}
|
||
|
||
const onRecommendedSelect = (id) => {
|
||
if (!id) return
|
||
const rec = recOptionsCache.value.find(r => r.id === id)
|
||
if (rec) {
|
||
tplForm.content = rec.content
|
||
tplForm.paramsList = Array.isArray(rec.params) ? rec.params.map(p => ({ ...p })) : []
|
||
}
|
||
}
|
||
|
||
const insertParam = (key) => {
|
||
tplForm.content += `{${key}}`
|
||
}
|
||
|
||
const submitTplForm = () => {
|
||
tplFormRef.value?.validate(async (valid) => {
|
||
if (!valid) return
|
||
submitting.value = true
|
||
try {
|
||
const paramsJson = tplForm.paramsList.filter(p => p.key).length > 0
|
||
? JSON.stringify(tplForm.paramsList.filter(p => p.key))
|
||
: ''
|
||
const data = {
|
||
service_id: serviceId.value,
|
||
user_id: tplForm.user_id,
|
||
name: tplForm.name,
|
||
content: tplForm.content
|
||
}
|
||
if (paramsJson) data.params = paramsJson
|
||
if (tplForm.recommended_id) data.recommended_id = tplForm.recommended_id
|
||
|
||
let res
|
||
if (tplDialogType.value === 'add') {
|
||
res = await createSmsTemplate(data)
|
||
} else {
|
||
data.template_id = tplForm.template_id
|
||
res = await updateSmsTemplate(data)
|
||
}
|
||
if (res.data.code === 200) { ElMessage.success('操作成功'); tplDialogVisible.value = false; fetchTemplates() }
|
||
else { ElMessage.error(res.data.message || '操作失败') }
|
||
} catch { ElMessage.error('操作失败') }
|
||
finally { submitting.value = false }
|
||
})
|
||
}
|
||
|
||
const handleSubmitTpl = (row) => {
|
||
ElMessageBox.confirm(`提交模板「${row.name}」审核?`, '提交审核', { type: 'info' }).then(async () => {
|
||
try {
|
||
const res = await submitSmsTemplate({ service_id: serviceId.value, template_id: row.ID })
|
||
if (res.data.code === 200) { ElMessage.success('已提交'); fetchTemplates() }
|
||
else { ElMessage.error(res.data.message || '操作失败') }
|
||
} catch { ElMessage.error('操作失败') }
|
||
}).catch(() => {})
|
||
}
|
||
|
||
const handleApproveTpl = (row) => {
|
||
ElMessageBox.confirm(`通过模板「${row.name}」?`, '审核通过', { type: 'success' }).then(async () => {
|
||
try {
|
||
const res = await approveSmsTemplate({ service_id: serviceId.value, template_id: row.ID })
|
||
if (res.data.code === 200) { ElMessage.success('已通过'); fetchTemplates() }
|
||
else { ElMessage.error(res.data.message || '操作失败') }
|
||
} catch { ElMessage.error('操作失败') }
|
||
}).catch(() => {})
|
||
}
|
||
|
||
const rejectDialogVisible = ref(false)
|
||
const rejectReason = ref('')
|
||
const currentRejectRow = ref(null)
|
||
|
||
const handleRejectTpl = (row) => {
|
||
currentRejectRow.value = row
|
||
rejectReason.value = ''
|
||
rejectDialogVisible.value = true
|
||
}
|
||
|
||
const confirmRejectTpl = async () => {
|
||
if (!rejectReason.value.trim()) { ElMessage.warning('请输入驳回原因'); return }
|
||
submitting.value = true
|
||
try {
|
||
const res = await rejectSmsTemplate({ service_id: serviceId.value, template_id: currentRejectRow.value.ID, reject_reason: rejectReason.value })
|
||
if (res.data.code === 200) { ElMessage.success('已驳回'); rejectDialogVisible.value = false; fetchTemplates() }
|
||
else { ElMessage.error(res.data.message || '操作失败') }
|
||
} catch { ElMessage.error('操作失败') }
|
||
finally { submitting.value = false }
|
||
}
|
||
|
||
const handleDeleteTpl = (row) => {
|
||
ElMessageBox.confirm(`确认删除模板「${row.name}」?`, '删除', { type: 'warning' }).then(async () => {
|
||
try {
|
||
const res = await deleteSmsTemplate({ service_id: serviceId.value, template_id: row.ID })
|
||
if (res.data.code === 200) { ElMessage.success('删除成功'); fetchTemplates() }
|
||
else { ElMessage.error(res.data.message || '删除失败') }
|
||
} catch { ElMessage.error('删除失败') }
|
||
}).catch(() => {})
|
||
}
|
||
|
||
// ========== 推荐模板 ==========
|
||
const recLoading = ref(false)
|
||
const recList = ref([])
|
||
const recTotal = ref(0)
|
||
const recQuery = reactive({ page: 1, count: 10 })
|
||
|
||
const fetchRecommended = async () => {
|
||
if (!serviceId.value) return
|
||
recLoading.value = true
|
||
try {
|
||
const res = await getSmsRecommendedTemplateList({ service_id: serviceId.value, page: recQuery.page, count: recQuery.count })
|
||
if (res.data.code === 200) {
|
||
const d = res.data.data
|
||
recList.value = d?.data || d || []
|
||
recTotal.value = d?.all_count || 0
|
||
}
|
||
} catch { ElMessage.error('获取推荐模板失败') }
|
||
finally { recLoading.value = false }
|
||
}
|
||
|
||
const recDialogVisible = ref(false)
|
||
const recDialogType = ref('add')
|
||
const recFormRef = ref(null)
|
||
const recForm = reactive({ name: '', content: '', paramsList: [], category: '', template_id: null })
|
||
const recFormRules = {
|
||
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
|
||
content: [{ required: true, message: '请输入内容', trigger: 'blur' }]
|
||
}
|
||
|
||
const handleAddRecommended = () => {
|
||
recDialogType.value = 'add'
|
||
Object.assign(recForm, { name: '', content: '', paramsList: [], category: '', template_id: null })
|
||
recDialogVisible.value = true
|
||
}
|
||
|
||
const handleEditRecommended = (row) => {
|
||
recDialogType.value = 'edit'
|
||
const params = Array.isArray(row.params) ? row.params.map(p => ({ ...p })) : []
|
||
Object.assign(recForm, { name: row.name, content: row.content, paramsList: params, category: row.category || '', template_id: row.id })
|
||
recDialogVisible.value = true
|
||
}
|
||
|
||
const submitRecForm = () => {
|
||
recFormRef.value?.validate(async (valid) => {
|
||
if (!valid) return
|
||
submitting.value = true
|
||
try {
|
||
const paramsJson = recForm.paramsList.filter(p => p.key).length > 0
|
||
? JSON.stringify(recForm.paramsList.filter(p => p.key))
|
||
: ''
|
||
const data = {
|
||
service_id: serviceId.value,
|
||
name: recForm.name,
|
||
content: recForm.content
|
||
}
|
||
if (paramsJson) data.params = paramsJson
|
||
if (recForm.category) data.category = recForm.category
|
||
|
||
let res
|
||
if (recDialogType.value === 'add') {
|
||
res = await createSmsRecommendedTemplate(data)
|
||
} else {
|
||
data.template_id = recForm.template_id
|
||
res = await updateSmsRecommendedTemplate(data)
|
||
}
|
||
if (res.data.code === 200) { ElMessage.success('操作成功'); recDialogVisible.value = false; fetchRecommended() }
|
||
else { ElMessage.error(res.data.message || '操作失败') }
|
||
} catch { ElMessage.error('操作失败') }
|
||
finally { submitting.value = false }
|
||
})
|
||
}
|
||
|
||
const handleDeleteRecommended = (row) => {
|
||
ElMessageBox.confirm(`确认删除推荐模板「${row.name}」?`, '删除', { type: 'warning' }).then(async () => {
|
||
try {
|
||
const res = await deleteSmsRecommendedTemplate({ service_id: serviceId.value, template_id: row.id })
|
||
if (res.data.code === 200) { ElMessage.success('删除成功'); fetchRecommended() }
|
||
else { ElMessage.error(res.data.message || '删除失败') }
|
||
} catch { ElMessage.error('删除失败') }
|
||
}).catch(() => {})
|
||
}
|
||
|
||
// ========== 初始化 ==========
|
||
onMounted(async () => {
|
||
await loadServices()
|
||
fetchTemplates()
|
||
fetchRecommended()
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.sms-template-page { padding: 20px; }
|
||
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||
.header-info { display: flex; align-items: center; gap: 14px; }
|
||
.header-icon { width: 48px; height: 48px; border-radius: 12px; background: #ecf5ff; display: flex; align-items: center; justify-content: center; }
|
||
.header-title { margin: 0; font-size: 20px; color: #303133; }
|
||
.header-desc { margin: 4px 0 0; font-size: 13px; color: #909399; }
|
||
.filter-bar { display: flex; gap: 10px; margin-bottom: 16px; align-items: center; }
|
||
.tab-toolbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
|
||
.tab-filters { display: flex; gap: 10px; align-items: center; }
|
||
.pagination { margin-top: 16px; justify-content: flex-end; }
|
||
.content-cell { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; text-overflow: ellipsis; font-size: 13px; cursor: pointer; }
|
||
.reject-text { color: #f56c6c; font-size: 12px; }
|
||
.text-muted { color: #c0c4cc; }
|
||
|
||
.params-editor { width: 100%; }
|
||
.param-row { display: flex; gap: 8px; align-items: center; margin-bottom: 8px; }
|
||
.insert-btns { margin-top: 8px; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
||
.insert-label { font-size: 12px; color: #909399; }
|
||
.clear-icon { cursor: pointer; color: #909399; transition: color 0.2s; }
|
||
.clear-icon:hover { color: #f56c6c; }
|
||
</style>
|