feat(admin): 新增短信平台管理功能
- 新增短信主控服务和额度商品的API接口 - 添加短信平台管理菜单项,包含主控服务管理和额度商品管理子菜单 - 实现短信平台管理相关路由配置 - 创建短信额度商品管理页面,支持额度类型配置、商品管理等功
This commit is contained in:
@@ -0,0 +1,384 @@
|
||||
<template>
|
||||
<div class="sms-service-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="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
|
||||
<path d="M8 9h8M8 13h4" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="header-title">短信主控服务管理</h2>
|
||||
<p class="header-desc">管理短信平台的主控服务实例,每个服务对应一个 sms-server 节点</p>
|
||||
</div>
|
||||
</div>
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon> 新增服务
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<div class="filter-bar">
|
||||
<el-input
|
||||
v-model="queryParams.key"
|
||||
placeholder="搜索名称 / 说明 / 地址"
|
||||
clearable
|
||||
style="width: 300px"
|
||||
@keyup.enter="handleSearch"
|
||||
@clear="handleSearch"
|
||||
>
|
||||
<template #prefix><el-icon><Search /></el-icon></template>
|
||||
</el-input>
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<el-table :data="tableData" v-loading="loading" stripe border style="width: 100%">
|
||||
<el-table-column prop="id" label="ID" width="70" align="center" />
|
||||
<el-table-column prop="name" label="服务名称" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<div class="service-name-cell">
|
||||
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="#67c23a" stroke-width="2">
|
||||
<rect x="2" y="3" width="20" height="14" rx="2"/>
|
||||
<path d="M8 21h8M12 17v4"/>
|
||||
</svg>
|
||||
<span class="name-text">{{ row.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="note" label="说明" min-width="180" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span class="note-text">{{ row.note || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="host" label="服务地址" min-width="250">
|
||||
<template #default="{ row }">
|
||||
<el-tag type="info" effect="plain" class="host-tag">{{ row.host }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="serviceToken" label="Service Token" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<div class="token-cell">
|
||||
<span v-if="!row._showToken" class="token-mask">{{ maskToken(row.serviceToken) }}</span>
|
||||
<span v-else class="token-full">{{ row.serviceToken }}</span>
|
||||
<el-button link size="small" @click="row._showToken = !row._showToken">
|
||||
{{ row._showToken ? '隐藏' : '显示' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="CreatedAt" label="创建时间" width="170" align="center">
|
||||
<template #default="{ row }">{{ formatTime(row.CreatedAt) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button link type="primary" size="small" @click="goToGoods(row)">额度商品</el-button>
|
||||
<el-popconfirm title="确认删除该服务?" @confirm="handleDelete(row)">
|
||||
<template #reference>
|
||||
<el-button link type="danger" size="small">删除</el-button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-wrap">
|
||||
<el-pagination
|
||||
v-model:current-page="queryParams.page"
|
||||
v-model:page-size="queryParams.count"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="fetchList"
|
||||
@current-change="fetchList"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 新增/编辑弹窗 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="isEdit ? '编辑短信主控服务' : '新增短信主控服务'"
|
||||
width="560px"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="110px">
|
||||
<el-form-item label="服务名称" prop="name">
|
||||
<el-input v-model="form.name" placeholder="请输入服务名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="说明" prop="note">
|
||||
<el-input v-model="form.note" type="textarea" :rows="2" placeholder="服务说明(可选)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="服务地址" prop="host">
|
||||
<el-input v-model="form.host" placeholder="https://sms.example.com" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Service Token" prop="service_token">
|
||||
<el-input v-model="form.service_token" placeholder="sms-server 的 SERVICE_TOKEN" show-password />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="handleSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Plus, Search } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getSmsServiceList,
|
||||
createSmsService,
|
||||
updateSmsService,
|
||||
deleteSmsService
|
||||
} from '@/api/admin/smsService.js'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
const tableData = ref([])
|
||||
const total = ref(0)
|
||||
const dialogVisible = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const formRef = ref(null)
|
||||
|
||||
const queryParams = reactive({
|
||||
page: 1,
|
||||
count: 10,
|
||||
key: ''
|
||||
})
|
||||
|
||||
const form = ref({
|
||||
id: null,
|
||||
name: '',
|
||||
note: '',
|
||||
host: '',
|
||||
service_token: ''
|
||||
})
|
||||
|
||||
const rules = {
|
||||
name: [{ required: true, message: '请输入服务名称', trigger: 'blur' }],
|
||||
host: [{ required: true, message: '请输入服务地址', trigger: 'blur' }],
|
||||
service_token: [{ required: true, message: '请输入 Service Token', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const formatTime = (t) => {
|
||||
if (!t) return '-'
|
||||
return new Date(t).toLocaleString('zh-CN', { hour12: false })
|
||||
}
|
||||
|
||||
const maskToken = (token) => {
|
||||
if (!token) return '-'
|
||||
if (token.length <= 8) return '****'
|
||||
return token.slice(0, 4) + '****' + token.slice(-4)
|
||||
}
|
||||
|
||||
const fetchList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getSmsServiceList(queryParams)
|
||||
const body = res.data
|
||||
if (body.code === 200) {
|
||||
const d = body.data?.data || body.data || []
|
||||
tableData.value = (Array.isArray(d) ? d : []).map(item => ({ ...item, _showToken: false }))
|
||||
total.value = body.data?.all_count || 0
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
queryParams.page = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
queryParams.key = ''
|
||||
queryParams.page = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
isEdit.value = false
|
||||
form.value = { id: null, name: '', note: '', host: '', service_token: '' }
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (row) => {
|
||||
isEdit.value = true
|
||||
form.value = {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
note: row.note || '',
|
||||
host: row.host,
|
||||
service_token: row.serviceToken || ''
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
await formRef.value?.validate()
|
||||
submitting.value = true
|
||||
try {
|
||||
const params = new URLSearchParams()
|
||||
if (isEdit.value) params.append('id', form.value.id)
|
||||
params.append('name', form.value.name)
|
||||
params.append('note', form.value.note)
|
||||
params.append('host', form.value.host)
|
||||
params.append('service_token', form.value.service_token)
|
||||
|
||||
const fn = isEdit.value ? updateSmsService : createSmsService
|
||||
const res = await fn(params)
|
||||
if (res.data.code === 200) {
|
||||
ElMessage.success(isEdit.value ? '更新成功' : '创建成功')
|
||||
dialogVisible.value = false
|
||||
fetchList()
|
||||
} else {
|
||||
ElMessage.error(res.data.message || '操作失败')
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error('操作失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (row) => {
|
||||
try {
|
||||
const params = new URLSearchParams()
|
||||
params.append('id', row.id)
|
||||
const res = await deleteSmsService(params)
|
||||
if (res.data.code === 200) {
|
||||
ElMessage.success('删除成功')
|
||||
fetchList()
|
||||
} else {
|
||||
ElMessage.error(res.data.message || '删除失败')
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
const goToGoods = (row) => {
|
||||
router.push({ path: '/sms/goods', query: { service_id: row.id, service_name: row.name } })
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.sms-service-page {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
padding: 20px 24px;
|
||||
background: linear-gradient(135deg, #f0f7ff 0%, #e8f4f8 100%);
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e0ecf5;
|
||||
}
|
||||
|
||||
.header-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.15);
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin: 0 0 4px;
|
||||
}
|
||||
|
||||
.header-desc {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
padding: 14px 16px;
|
||||
background: #fafbfc;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.service-name-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.name-text {
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.note-text {
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.host-tag {
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.token-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.token-mask {
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
color: #c0c4cc;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.token-full {
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
color: #606266;
|
||||
font-size: 12px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.pagination-wrap {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user