feat(admin): 新增短信平台管理功能 #21
@@ -39,3 +39,10 @@ export const updateNoticeTemplate = (data) => {
|
|||||||
export const deleteNoticeTemplate = (params) => {
|
export const deleteNoticeTemplate = (params) => {
|
||||||
return http2.delete('/api/v1/admin/notice_message/template/delete', { params })
|
return http2.delete('/api/v1/admin/notice_message/template/delete', { params })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 使用默认参数预览渲染模板 */
|
||||||
|
export const previewNoticeTemplate = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/notice_message/template/default_msg', data, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
import { http2 } from '@/utils/request.js'
|
||||||
|
|
||||||
|
const formHeaders = { headers: { 'Content-Type': 'multipart/form-data' } }
|
||||||
|
|
||||||
|
// ========== 短信主控服务 ==========
|
||||||
|
|
||||||
|
export const getSmsServiceList = (params) => {
|
||||||
|
return http2.get('/api/v1/admin/server/sms_service/list', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createSmsService = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/server/sms_service/create', data, {
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateSmsService = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/server/sms_service/update', data, {
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteSmsService = (data) => {
|
||||||
|
return http2.delete('/api/v1/admin/server/sms_service/delete', {
|
||||||
|
data,
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setDefaultSmsService = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/server/sms_service/set_default', data, {
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 短信额度商品 ==========
|
||||||
|
|
||||||
|
export const getSmsGoodsList = (params) => {
|
||||||
|
return http2.get('/api/v1/admin/server/sms_service/goods/list', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createSmsGoods = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/server/sms_service/goods/create', data, {
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateSmsGoods = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/server/sms_service/goods/update', data, {
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteSmsGoods = (data) => {
|
||||||
|
return http2.delete('/api/v1/admin/server/sms_service/goods/delete', {
|
||||||
|
data,
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 短信签名管理 ==========
|
||||||
|
|
||||||
|
export const getSmsSignatureList = (params) => {
|
||||||
|
return http2.get('/api/v1/admin/server/sms_service/signature/list', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getSmsSignatureDetail = (params) => {
|
||||||
|
return http2.get('/api/v1/admin/server/sms_service/signature/detail', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createSmsSignature = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/server/sms_service/signature/create', data, formHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateSmsSignature = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/server/sms_service/signature/update', data, formHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteSmsSignature = (data) => {
|
||||||
|
return http2.delete('/api/v1/admin/server/sms_service/signature/delete', { data, ...formHeaders })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const submitSmsSignature = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/server/sms_service/signature/submit', data, formHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const approveSmsSignature = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/server/sms_service/signature/approve', data, formHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const rejectSmsSignature = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/server/sms_service/signature/reject', data, formHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 短信模板管理 ==========
|
||||||
|
|
||||||
|
export const getSmsTemplateList = (params) => {
|
||||||
|
return http2.get('/api/v1/admin/server/sms_service/template/list', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getSmsTemplateDetail = (params) => {
|
||||||
|
return http2.get('/api/v1/admin/server/sms_service/template/detail', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createSmsTemplate = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/server/sms_service/template/create', data, formHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateSmsTemplate = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/server/sms_service/template/update', data, formHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteSmsTemplate = (data) => {
|
||||||
|
return http2.delete('/api/v1/admin/server/sms_service/template/delete', { data, ...formHeaders })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const submitSmsTemplate = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/server/sms_service/template/submit', data, formHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const approveSmsTemplate = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/server/sms_service/template/approve', data, formHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const rejectSmsTemplate = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/server/sms_service/template/reject', data, formHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 推荐模板管理 ==========
|
||||||
|
|
||||||
|
export const getSmsRecommendedTemplateList = (params) => {
|
||||||
|
return http2.get('/api/v1/admin/server/sms_service/template/recommended/list', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createSmsRecommendedTemplate = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/server/sms_service/template/recommended/create', data, formHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateSmsRecommendedTemplate = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/server/sms_service/template/recommended/update', data, formHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteSmsRecommendedTemplate = (data) => {
|
||||||
|
return http2.delete('/api/v1/admin/server/sms_service/template/recommended/delete', { data, ...formHeaders })
|
||||||
|
}
|
||||||
@@ -149,6 +149,29 @@ export const menus = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/sms',
|
||||||
|
title: '短信平台管理',
|
||||||
|
icon: 'ChatDotRound',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/sms/service',
|
||||||
|
title: '主控服务管理'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/sms/goods',
|
||||||
|
title: '额度商品管理'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/sms/signature',
|
||||||
|
title: '签名管理'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/sms/template',
|
||||||
|
title: '模板管理'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '虚拟化平台管理',
|
title: '虚拟化平台管理',
|
||||||
icon: 'Platform',
|
icon: 'Platform',
|
||||||
|
|||||||
@@ -609,6 +609,42 @@ const routes = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
// 短信平台管理路由
|
||||||
|
{
|
||||||
|
path: 'sms',
|
||||||
|
name: 'Sms',
|
||||||
|
meta: {
|
||||||
|
title: '短信平台管理',
|
||||||
|
icon: 'ChatDotRound'
|
||||||
|
},
|
||||||
|
redirect: '/sms/service',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'service',
|
||||||
|
name: 'SmsService',
|
||||||
|
component: () => import('../views/sms/SmsService.vue'),
|
||||||
|
meta: { title: '主控服务管理' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'goods',
|
||||||
|
name: 'SmsGoods',
|
||||||
|
component: () => import('../views/sms/SmsGoods.vue'),
|
||||||
|
meta: { title: '额度商品管理' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'signature',
|
||||||
|
name: 'SmsSignature',
|
||||||
|
component: () => import('../views/sms/SmsSignature.vue'),
|
||||||
|
meta: { title: '签名管理' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'template',
|
||||||
|
name: 'SmsTemplateMgr',
|
||||||
|
component: () => import('../views/sms/SmsTemplate.vue'),
|
||||||
|
meta: { title: '模板管理' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
// 站点审计路由
|
// 站点审计路由
|
||||||
{
|
{
|
||||||
path: 'audit',
|
path: 'audit',
|
||||||
|
|||||||
@@ -359,7 +359,7 @@ const editForm = reactive({
|
|||||||
|
|
||||||
const editRules = {
|
const editRules = {
|
||||||
discount_id: [
|
discount_id: [
|
||||||
{ required: true, message: '请输入代金券ID', trigger: 'blur' }
|
{ required: false, message: '请输入代金券ID', trigger: 'blur' }
|
||||||
],
|
],
|
||||||
use_times: [
|
use_times: [
|
||||||
{ required: true, message: '请输入已使用次数', trigger: 'blur' }
|
{ required: true, message: '请输入已使用次数', trigger: 'blur' }
|
||||||
|
|||||||
+477
-211
@@ -73,6 +73,11 @@
|
|||||||
<el-table-column type="selection" width="55" />
|
<el-table-column type="selection" width="55" />
|
||||||
<el-table-column prop="id" label="订单ID" width="100" />
|
<el-table-column prop="id" label="订单ID" width="100" />
|
||||||
<el-table-column prop="name" label="订单名称" min-width="180" />
|
<el-table-column prop="name" label="订单名称" min-width="180" />
|
||||||
|
<el-table-column label="类型" width="90">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag size="small" :type="row.type === 'create' ? 'success' : row.type === 'renew' ? 'warning' : 'info'">{{ getTypeText(row.type) }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column label="用户ID" width="100">
|
<el-table-column label="用户ID" width="100">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-link v-if="row.userId" type="primary" :underline="false" @click.stop="router.push({ path: '/user/detail', query: { user_id: row.userId } })">{{ row.userId }}</el-link>
|
<el-link v-if="row.userId" type="primary" :underline="false" @click.stop="router.push({ path: '/user/detail', query: { user_id: row.userId } })">{{ row.userId }}</el-link>
|
||||||
@@ -143,7 +148,7 @@
|
|||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
<el-button type="primary" link @click="handleView(row)">查看</el-button>
|
<el-button type="primary" link @click="handleView(row)">查看</el-button>
|
||||||
<el-button type="warning" link @click="handleEdit(row)">编辑</el-button>
|
<el-button type="warning" link @click="handleEdit(row)">编辑</el-button>
|
||||||
<el-button v-if="row.error" type="danger" link @click="handleRetryOrder(row)">重试流程</el-button>
|
<el-button type="danger" link @click="handleRetryOrder(row)">重试流程</el-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -174,8 +179,12 @@
|
|||||||
<el-descriptions :column="2" border v-if="orderDetail">
|
<el-descriptions :column="2" border v-if="orderDetail">
|
||||||
<el-descriptions-item label="订单ID">{{ orderDetail.id }}</el-descriptions-item>
|
<el-descriptions-item label="订单ID">{{ orderDetail.id }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="订单名称">{{ orderDetail.name }}</el-descriptions-item>
|
<el-descriptions-item label="订单名称">{{ orderDetail.name }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="订单类型">
|
||||||
|
<el-tag size="small">{{ getTypeText(orderDetail.type) }}</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="用户ID">{{ orderDetail.userId }}</el-descriptions-item>
|
<el-descriptions-item label="用户ID">{{ orderDetail.userId }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="商品ID">{{ orderDetail.commodityId }}</el-descriptions-item>
|
<el-descriptions-item label="商品ID">{{ orderDetail.commodityId }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="套餐ID">{{ orderDetail.planId || '-' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="表名">{{ orderDetail.table }}</el-descriptions-item>
|
<el-descriptions-item label="表名">{{ orderDetail.table }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="数量">{{ orderDetail.payNum }}</el-descriptions-item>
|
<el-descriptions-item label="数量">{{ orderDetail.payNum }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="订单金额">¥{{ (orderDetail.price / 100).toFixed(2) }}</el-descriptions-item>
|
<el-descriptions-item label="订单金额">¥{{ (orderDetail.price / 100).toFixed(2) }}</el-descriptions-item>
|
||||||
@@ -198,191 +207,231 @@
|
|||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<!-- 订单表单对话框 -->
|
<!-- 订单表单对话框(分步向导) -->
|
||||||
<el-dialog
|
<el-dialog
|
||||||
v-model="dialogVisible"
|
v-model="dialogVisible"
|
||||||
:title="dialogType === 'add' ? '新增订单' : '编辑订单'"
|
:title="dialogType === 'add' ? '新增订单' : '编辑订单'"
|
||||||
width="700px"
|
width="820px"
|
||||||
append-to-body
|
append-to-body
|
||||||
|
destroy-on-close
|
||||||
>
|
>
|
||||||
<el-form
|
<div class="wizard-container">
|
||||||
ref="orderFormRef"
|
<el-steps :active="currentStep" finish-status="success" align-center class="wizard-steps">
|
||||||
:model="orderForm"
|
<el-step title="选择用户" description="指定订单用户与类型" />
|
||||||
:rules="orderRules"
|
<el-step title="商品配置" description="选择商品与套餐" />
|
||||||
label-width="120px"
|
<el-step title="订单详情" description="价格、支付与优惠" />
|
||||||
>
|
</el-steps>
|
||||||
<el-form-item label="订单名称" prop="name">
|
|
||||||
<el-input v-model="orderForm.name" placeholder="请输入订单名称" />
|
<div class="wizard-body">
|
||||||
</el-form-item>
|
<!-- Step 1: 用户与订单类型 -->
|
||||||
<el-form-item label="所属表" prop="table">
|
<el-form v-show="currentStep === 0" ref="step1FormRef" :model="orderForm" :rules="step1Rules" label-width="110px">
|
||||||
<el-input v-model="orderForm.table" placeholder="请输入所属表" />
|
<el-form-item label="用户" prop="user_id">
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="用户ID" prop="user_id">
|
|
||||||
<el-input
|
<el-input
|
||||||
v-if="selectedUserInfo"
|
v-if="selectedUserInfo"
|
||||||
:model-value="`${selectedUserInfo.user_name} (ID: ${orderForm.user_id})`"
|
:model-value="`${selectedUserInfo.user_name} (ID: ${orderForm.user_id})`"
|
||||||
readonly
|
readonly
|
||||||
style="width: 100%"
|
|
||||||
>
|
>
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
<el-icon class="clear-icon" @click="clearUser"><Close /></el-icon>
|
<el-icon class="clear-icon" @click="clearUser"><Close /></el-icon>
|
||||||
</template>
|
</template>
|
||||||
<template #append>
|
<template #append>
|
||||||
<el-button @click="userSelectorVisible = true">
|
<el-button @click="userSelectorVisible = true"><el-icon><User /></el-icon></el-button>
|
||||||
<el-icon><User /></el-icon>
|
|
||||||
</el-button>
|
|
||||||
</template>
|
</template>
|
||||||
</el-input>
|
</el-input>
|
||||||
<el-input
|
<el-input v-else placeholder="请选择用户" readonly @click="userSelectorVisible = true">
|
||||||
v-else
|
|
||||||
placeholder="请选择用户"
|
|
||||||
readonly
|
|
||||||
style="width: 100%"
|
|
||||||
@click="userSelectorVisible = true"
|
|
||||||
>
|
|
||||||
<template #append>
|
<template #append>
|
||||||
<el-button @click="userSelectorVisible = true">
|
<el-button @click="userSelectorVisible = true"><el-icon><User /></el-icon></el-button>
|
||||||
<el-icon><User /></el-icon>
|
|
||||||
</el-button>
|
|
||||||
</template>
|
</template>
|
||||||
</el-input>
|
</el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="商品ID" prop="commodity_id">
|
<el-form-item label="订单类型" prop="type">
|
||||||
|
<el-select v-model="orderForm.type" placeholder="请选择订单类型" style="width: 100%" @change="handleTypeChange">
|
||||||
|
<el-option label="新购" value="create" />
|
||||||
|
<el-option label="续费" value="renew" />
|
||||||
|
<el-option label="升级" value="update" />
|
||||||
|
<el-option label="快照" value="snapshot" />
|
||||||
|
<el-option label="备份" value="backup" />
|
||||||
|
<el-option label="数据盘" value="data_volume" />
|
||||||
|
<el-option label="IPv4" value="ipv4" />
|
||||||
|
<el-option label="IPv6" value="ipv6" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="订单名称" prop="name">
|
||||||
|
<el-input v-model="orderForm.name" placeholder="请输入订单名称" maxlength="200" show-word-limit />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<!-- Step 2: 商品与套餐 -->
|
||||||
|
<el-form v-show="currentStep === 1" ref="step2FormRef" :model="orderForm" :rules="step2Rules" label-width="110px">
|
||||||
|
<el-form-item label="商品" prop="commodity_id">
|
||||||
<el-input
|
<el-input
|
||||||
v-if="selectedProductInfo"
|
v-if="selectedProductInfo"
|
||||||
:model-value="`${selectedProductInfo.name} (ID: ${orderForm.commodity_id})`"
|
:model-value="`${selectedProductInfo.name} (ID: ${orderForm.commodity_id})`"
|
||||||
readonly
|
readonly
|
||||||
style="width: 100%"
|
|
||||||
>
|
>
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
<el-icon class="clear-icon" @click="clearProduct"><Close /></el-icon>
|
<el-icon class="clear-icon" @click="clearProduct"><Close /></el-icon>
|
||||||
</template>
|
</template>
|
||||||
<template #append>
|
<template #append>
|
||||||
<el-button @click="productSelectorVisible = true">
|
<el-button @click="productSelectorVisible = true"><el-icon><ShoppingCart /></el-icon></el-button>
|
||||||
<el-icon><ShoppingCart /></el-icon>
|
|
||||||
</el-button>
|
|
||||||
</template>
|
</template>
|
||||||
</el-input>
|
</el-input>
|
||||||
|
<el-input v-else placeholder="请选择商品" readonly @click="productSelectorVisible = true">
|
||||||
|
<template #append>
|
||||||
|
<el-button @click="productSelectorVisible = true"><el-icon><ShoppingCart /></el-icon></el-button>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="套餐" prop="plan_id">
|
||||||
|
<el-select v-model="orderForm.plan_id" placeholder="请选择套餐(可选)" clearable style="width: 100%" :loading="planLoading" @change="handlePlanChange">
|
||||||
|
<el-option v-for="p in planList" :key="p.id" :label="`${p.name} - ¥${(p.price / 100).toFixed(2)}`" :value="p.id" />
|
||||||
|
</el-select>
|
||||||
|
<div v-if="!planList.length && orderForm.commodity_id && !planLoading" class="form-tip">该商品暂无套餐,可直接进入下一步手动设置价格</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="所属表">
|
||||||
|
<el-input v-model="orderForm.table" placeholder="选择商品后自动填充,或手动输入" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- 动态商品参数 -->
|
||||||
|
<div v-if="productParams.length" class="args-section">
|
||||||
|
<div class="args-section-title">
|
||||||
|
<el-icon><Setting /></el-icon>商品参数配置
|
||||||
|
</div>
|
||||||
|
<el-form-item
|
||||||
|
v-for="param in productParams"
|
||||||
|
:key="param.id"
|
||||||
|
:label="param.name"
|
||||||
|
:required="param.must"
|
||||||
|
>
|
||||||
|
<!-- select 类型 -->
|
||||||
|
<el-select
|
||||||
|
v-if="param.type === 'select'"
|
||||||
|
v-model="argValues[param.id]"
|
||||||
|
:placeholder="`请选择${param.name}`"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="attr in param.attrs"
|
||||||
|
:key="attr.id"
|
||||||
|
:label="`${attr.name}${attr.price ? ' (¥' + (attr.price / 100).toFixed(2) + ')' : ''}`"
|
||||||
|
:value="attr.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
<!-- number 类型 -->
|
||||||
|
<div v-else-if="param.type === 'number'" class="number-arg-row">
|
||||||
|
<el-input-number
|
||||||
|
v-model="argValues[param.id]"
|
||||||
|
:min="param.min || 0"
|
||||||
|
:max="param.max || 9999"
|
||||||
|
:step="param.step || 1"
|
||||||
|
style="flex: 1"
|
||||||
|
/>
|
||||||
|
<span v-if="param.arg_key" class="unit-text">{{ param.arg_key }}</span>
|
||||||
|
</div>
|
||||||
|
<!-- string 类型 -->
|
||||||
<el-input
|
<el-input
|
||||||
v-else
|
v-else
|
||||||
placeholder="请选择商品"
|
v-model="argValues[param.id]"
|
||||||
readonly
|
:placeholder="`请输入${param.name}`"
|
||||||
style="width: 100%"
|
|
||||||
@click="productSelectorVisible = true"
|
|
||||||
>
|
|
||||||
<template #append>
|
|
||||||
<el-button @click="productSelectorVisible = true">
|
|
||||||
<el-icon><ShoppingCart /></el-icon>
|
|
||||||
</el-button>
|
|
||||||
</template>
|
|
||||||
</el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="购买数量" prop="pay_num">
|
|
||||||
<el-input-number v-model="orderForm.pay_num" :min="1" placeholder="请输入数量" style="width: 100%" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="价格" prop="price">
|
|
||||||
<div class="unit-input-row">
|
|
||||||
<el-input-number v-model="orderForm.price" :min="0" placeholder="请输入价格(分)" style="flex:1" />
|
|
||||||
<span class="unit-text">分</span>
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="续费价格" prop="renew_price">
|
|
||||||
<div class="unit-input-row">
|
|
||||||
<el-input-number v-model="orderForm.renew_price" :min="0" placeholder="请输入续费价格(分)" style="flex:1" />
|
|
||||||
<span class="unit-text">分</span>
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="过期时间" prop="expire_time">
|
|
||||||
<el-date-picker
|
|
||||||
v-model="orderForm.expire_time"
|
|
||||||
type="datetime"
|
|
||||||
placeholder="请选择过期时间"
|
|
||||||
format="YYYY-MM-DD HH:mm:ss"
|
|
||||||
value-format="x"
|
|
||||||
style="width: 100%"
|
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="优惠码ID" prop="discount_code_id">
|
</div>
|
||||||
|
<div v-else-if="paramsLoading" class="form-tip" style="text-align: center; padding: 12px 0;">
|
||||||
|
<el-icon class="is-loading"><Loading /></el-icon> 加载商品参数中...
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<!-- Step 3: 价格与支付 -->
|
||||||
|
<el-form v-show="currentStep === 2" ref="step3FormRef" :model="orderForm" :rules="step3Rules" label-width="110px">
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="价格" prop="price">
|
||||||
|
<div class="unit-input-row">
|
||||||
|
<el-input-number v-model="orderForm.price" :min="0" :precision="0" style="flex:1" />
|
||||||
|
<span class="unit-text">分</span>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="续费价格">
|
||||||
|
<div class="unit-input-row">
|
||||||
|
<el-input-number v-model="orderForm.renew_price" :min="0" :precision="0" style="flex:1" />
|
||||||
|
<span class="unit-text">分</span>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="购买数量" prop="pay_num">
|
||||||
|
<el-input-number v-model="orderForm.pay_num" :min="1" style="width: 100%" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="过期时间">
|
||||||
|
<el-date-picker v-model="orderForm.expire_time" type="datetime" placeholder="请选择" format="YYYY-MM-DD HH:mm:ss" value-format="x" style="width: 100%" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-form-item label="订单状态">
|
||||||
|
<el-radio-group v-model="orderForm.state">
|
||||||
|
<el-radio :value="0">待支付</el-radio>
|
||||||
|
<el-radio :value="1">已支付</el-radio>
|
||||||
|
<el-radio :value="2">已失效</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="支付方式">
|
||||||
|
<el-select v-model="orderForm.pay_type" placeholder="请选择" clearable style="width: 100%">
|
||||||
|
<el-option label="默认(余额)" value="default" />
|
||||||
|
<el-option label="支付宝" value="ali" />
|
||||||
|
<el-option label="微信" value="wx" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="优惠码">
|
||||||
<el-input
|
<el-input
|
||||||
v-if="selectedDiscountCodeInfo"
|
v-if="selectedDiscountCodeInfo"
|
||||||
:model-value="`${selectedDiscountCodeInfo.name || selectedDiscountCodeInfo.code} (ID: ${orderForm.discount_code_id})`"
|
:model-value="`${selectedDiscountCodeInfo.name || selectedDiscountCodeInfo.code} (ID: ${orderForm.discount_code_id})`"
|
||||||
readonly
|
readonly
|
||||||
style="width: 100%"
|
|
||||||
>
|
>
|
||||||
<template #suffix>
|
<template #suffix><el-icon class="clear-icon" @click="clearDiscountCode"><Close /></el-icon></template>
|
||||||
<el-icon class="clear-icon" @click="clearDiscountCode"><Close /></el-icon>
|
<template #append><el-button @click="discountCodeSelectorVisible = true"><el-icon><Ticket /></el-icon></el-button></template>
|
||||||
</template>
|
|
||||||
<template #append>
|
|
||||||
<el-button @click="discountCodeSelectorVisible = true">
|
|
||||||
<el-icon><Ticket /></el-icon>
|
|
||||||
</el-button>
|
|
||||||
</template>
|
|
||||||
</el-input>
|
</el-input>
|
||||||
<el-input
|
<el-input v-else placeholder="选择优惠码(可选)" readonly @click="discountCodeSelectorVisible = true">
|
||||||
v-else
|
<template #append><el-button @click="discountCodeSelectorVisible = true"><el-icon><Ticket /></el-icon></el-button></template>
|
||||||
placeholder="请选择优惠码(可选)"
|
|
||||||
readonly
|
|
||||||
style="width: 100%"
|
|
||||||
@click="discountCodeSelectorVisible = true"
|
|
||||||
>
|
|
||||||
<template #append>
|
|
||||||
<el-button @click="discountCodeSelectorVisible = true">
|
|
||||||
<el-icon><Ticket /></el-icon>
|
|
||||||
</el-button>
|
|
||||||
</template>
|
|
||||||
</el-input>
|
</el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="代金券ID" prop="coupon_id">
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="代金券">
|
||||||
<el-input
|
<el-input
|
||||||
v-if="selectedVoucherInfo"
|
v-if="selectedVoucherInfo"
|
||||||
:model-value="`${selectedVoucherInfo.name || selectedVoucherInfo.code} (ID: ${orderForm.coupon_id})`"
|
:model-value="`${selectedVoucherInfo.name || selectedVoucherInfo.code} (ID: ${orderForm.user_coupon_id})`"
|
||||||
readonly
|
readonly
|
||||||
style="width: 100%"
|
|
||||||
>
|
>
|
||||||
<template #suffix>
|
<template #suffix><el-icon class="clear-icon" @click="clearVoucher"><Close /></el-icon></template>
|
||||||
<el-icon class="clear-icon" @click="clearVoucher"><Close /></el-icon>
|
<template #append><el-button @click="voucherSelectorVisible = true"><el-icon><Money /></el-icon></el-button></template>
|
||||||
</template>
|
|
||||||
<template #append>
|
|
||||||
<el-button @click="voucherSelectorVisible = true">
|
|
||||||
<el-icon><Money /></el-icon>
|
|
||||||
</el-button>
|
|
||||||
</template>
|
|
||||||
</el-input>
|
</el-input>
|
||||||
<el-input
|
<el-input v-else placeholder="选择代金券(可选)" readonly @click="voucherSelectorVisible = true">
|
||||||
v-else
|
<template #append><el-button @click="voucherSelectorVisible = true"><el-icon><Money /></el-icon></el-button></template>
|
||||||
placeholder="请选择代金券(可选)"
|
|
||||||
readonly
|
|
||||||
style="width: 100%"
|
|
||||||
@click="voucherSelectorVisible = true"
|
|
||||||
>
|
|
||||||
<template #append>
|
|
||||||
<el-button @click="voucherSelectorVisible = true">
|
|
||||||
<el-icon><Money /></el-icon>
|
|
||||||
</el-button>
|
|
||||||
</template>
|
|
||||||
</el-input>
|
</el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="订单状态" prop="state">
|
</el-col>
|
||||||
<el-radio-group v-model="orderForm.state">
|
</el-row>
|
||||||
<el-radio :label="0">待支付</el-radio>
|
<el-form-item label="备注">
|
||||||
<el-radio :label="1">已支付</el-radio>
|
<el-input v-model="orderForm.note" type="textarea" :rows="2" placeholder="请输入备注(可选)" />
|
||||||
<el-radio :label="2">已失效</el-radio>
|
|
||||||
</el-radio-group>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="支付方式" prop="pay_type">
|
|
||||||
<el-input v-model="orderForm.pay_type" placeholder="请输入支付类型" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="订单参数" prop="args">
|
|
||||||
<el-input v-model="orderForm.args" placeholder="请输入订单参数" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="备注" prop="note">
|
|
||||||
<el-input v-model="orderForm.note" type="textarea" :rows="3" placeholder="请输入备注" />
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="dialog-footer">
|
<div class="wizard-footer">
|
||||||
|
<el-button v-if="currentStep > 0" @click="currentStep--">上一步</el-button>
|
||||||
|
<el-button v-if="currentStep < 2" type="primary" @click="handleNextStep">下一步</el-button>
|
||||||
|
<el-button v-if="currentStep === 2" type="primary" :loading="submitLoading" @click="submitForm">提交订单</el-button>
|
||||||
<el-button @click="dialogVisible = false">取消</el-button>
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
<el-button type="primary" @click="submitForm">确定</el-button>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
@@ -411,7 +460,7 @@
|
|||||||
<!-- 代金券选择器 -->
|
<!-- 代金券选择器 -->
|
||||||
<VoucherSelector
|
<VoucherSelector
|
||||||
v-model="voucherSelectorVisible"
|
v-model="voucherSelectorVisible"
|
||||||
:current-voucher-id="orderForm.coupon_id"
|
:current-voucher-id="orderForm.user_coupon_id"
|
||||||
@confirm="handleVoucherSelect"
|
@confirm="handleVoucherSelect"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -421,8 +470,9 @@
|
|||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { Plus, Delete, Search, Download, Refresh, User, ShoppingCart, Ticket, Money, Close } from '@element-plus/icons-vue'
|
import { Plus, Delete, Search, Download, Refresh, User, ShoppingCart, Ticket, Money, Close, Setting, Loading } from '@element-plus/icons-vue'
|
||||||
import { getOrderList, getOrderDetail, createOrder, updateOrder, deleteOrder, retryOrderHook } from '@/api/admin/order'
|
import { getOrderList, getOrderDetail, createOrder, updateOrder, deleteOrder, retryOrderHook } from '@/api/admin/order'
|
||||||
|
import { getProductPlanList, getProductParameterList, getProductParameterDetail } from '@/api/admin/product'
|
||||||
import UserListSelector from '@/components/admin/UserListSelector.vue'
|
import UserListSelector from '@/components/admin/UserListSelector.vue'
|
||||||
import ProductSelector from '@/components/admin/ProductSelector.vue'
|
import ProductSelector from '@/components/admin/ProductSelector.vue'
|
||||||
import DiscountCodeSelector from '@/components/admin/DiscountCodeSelector.vue'
|
import DiscountCodeSelector from '@/components/admin/DiscountCodeSelector.vue'
|
||||||
@@ -448,42 +498,52 @@ const orderForm = reactive({
|
|||||||
order_id: undefined,
|
order_id: undefined,
|
||||||
name: '',
|
name: '',
|
||||||
table: '',
|
table: '',
|
||||||
|
type: 'create',
|
||||||
user_id: undefined,
|
user_id: undefined,
|
||||||
commodity_id: 0,
|
commodity_id: 0,
|
||||||
|
plan_id: null,
|
||||||
pay_num: 1,
|
pay_num: 1,
|
||||||
price: 0,
|
price: 0,
|
||||||
renew_price: 0,
|
renew_price: 0,
|
||||||
expire_time: 0,
|
expire_time: 0,
|
||||||
discount_code_id: 0,
|
discount_code_id: 0,
|
||||||
coupon_id: 0,
|
user_coupon_id: 0,
|
||||||
state: 0,
|
state: 0,
|
||||||
pay_type: '',
|
pay_type: 'default',
|
||||||
|
payment_order_id: '',
|
||||||
args: '',
|
args: '',
|
||||||
note: ''
|
note: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
const orderRules = {
|
// 分步验证规则
|
||||||
name: [
|
const step1Rules = {
|
||||||
{ required: true, message: '请输入订单名称', trigger: 'blur' }
|
user_id: [{ required: true, message: '请选择用户', trigger: 'change' }],
|
||||||
],
|
type: [{ required: true, message: '请选择订单类型', trigger: 'change' }],
|
||||||
table: [
|
name: [{ required: true, message: '请输入订单名称', trigger: 'blur' }]
|
||||||
{ required: true, message: '请输入所属表', trigger: 'blur' }
|
|
||||||
],
|
|
||||||
user_id: [
|
|
||||||
{ required: true, message: '请输入用户ID', trigger: 'blur' },
|
|
||||||
{ type: 'number', message: '用户ID必须是数字', trigger: 'blur' }
|
|
||||||
],
|
|
||||||
coupon_id: [
|
|
||||||
{ message: '请输入代金券ID', trigger: 'blur' },
|
|
||||||
{ type: 'number', message: '代金券ID必须是数字', trigger: 'blur' }
|
|
||||||
],
|
|
||||||
pay_num: [
|
|
||||||
{ required: true, message: '请输入购买数量', trigger: 'blur' }
|
|
||||||
],
|
|
||||||
price: [
|
|
||||||
{ required: true, message: '请输入价格', trigger: 'blur' }
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
const step2Rules = {
|
||||||
|
commodity_id: [{ required: true, message: '请选择商品', trigger: 'change' }]
|
||||||
|
}
|
||||||
|
const step3Rules = {
|
||||||
|
price: [{ required: true, message: '请输入价格', trigger: 'blur' }],
|
||||||
|
pay_num: [{ required: true, message: '请输入数量', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分步向导
|
||||||
|
const currentStep = ref(0)
|
||||||
|
const submitLoading = ref(false)
|
||||||
|
const step1FormRef = ref(null)
|
||||||
|
const step2FormRef = ref(null)
|
||||||
|
const step3FormRef = ref(null)
|
||||||
|
|
||||||
|
// 套餐相关
|
||||||
|
const planList = ref([])
|
||||||
|
const planLoading = ref(false)
|
||||||
|
|
||||||
|
// 商品参数相关
|
||||||
|
const productParams = ref([])
|
||||||
|
const paramsLoading = ref(false)
|
||||||
|
const argValues = reactive({})
|
||||||
|
|
||||||
// 状态数据
|
// 状态数据
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -494,7 +554,6 @@ const selectedRows = ref([])
|
|||||||
const dialogVisible = ref(false)
|
const dialogVisible = ref(false)
|
||||||
const detailDialogVisible = ref(false)
|
const detailDialogVisible = ref(false)
|
||||||
const dialogType = ref('add')
|
const dialogType = ref('add')
|
||||||
const orderFormRef = ref(null)
|
|
||||||
|
|
||||||
// 选择器弹窗状态
|
// 选择器弹窗状态
|
||||||
const userSelectorVisible = ref(false)
|
const userSelectorVisible = ref(false)
|
||||||
@@ -559,16 +618,17 @@ const getStatusType = (status) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取订单状态文本
|
// 获取订单状态文本
|
||||||
// state 0:未支付 1:已支付 2:已失效
|
|
||||||
const getStatusText = (status) => {
|
const getStatusText = (status) => {
|
||||||
const statusMap = {
|
const statusMap = { 0: '待支付', 1: '已支付', 2: '已失效' }
|
||||||
0: '待支付',
|
|
||||||
1: '已支付',
|
|
||||||
2: '已失效'
|
|
||||||
}
|
|
||||||
return statusMap[status] || '未知'
|
return statusMap[status] || '未知'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取订单类型文本
|
||||||
|
const getTypeText = (type) => {
|
||||||
|
const typeMap = { create: '新购', renew: '续费', update: '升级', snapshot: '快照', backup: '备份', data_volume: '数据盘', ipv4: 'IPv4', ipv6: 'IPv6' }
|
||||||
|
return typeMap[type] || type || '-'
|
||||||
|
}
|
||||||
|
|
||||||
// 查询
|
// 查询
|
||||||
const handleQuery = () => {
|
const handleQuery = () => {
|
||||||
queryParams.page = 1
|
queryParams.page = 1
|
||||||
@@ -605,26 +665,32 @@ const handleCurrentChange = (page) => {
|
|||||||
// 新增订单
|
// 新增订单
|
||||||
const handleAdd = () => {
|
const handleAdd = () => {
|
||||||
dialogType.value = 'add'
|
dialogType.value = 'add'
|
||||||
dialogVisible.value = true
|
currentStep.value = 0
|
||||||
clearAllSelections()
|
clearAllSelections()
|
||||||
|
planList.value = []
|
||||||
|
productParams.value = []
|
||||||
|
Object.keys(argValues).forEach(k => delete argValues[k])
|
||||||
Object.assign(orderForm, {
|
Object.assign(orderForm, {
|
||||||
order_id: undefined,
|
order_id: undefined,
|
||||||
name: '',
|
name: '',
|
||||||
table: '',
|
table: 'good',
|
||||||
|
type: 'create',
|
||||||
user_id: undefined,
|
user_id: undefined,
|
||||||
commodity_id: 0,
|
commodity_id: 0,
|
||||||
|
plan_id: null,
|
||||||
pay_num: 1,
|
pay_num: 1,
|
||||||
price: 0,
|
price: 0,
|
||||||
renew_price: 0,
|
renew_price: 0,
|
||||||
expire_time: 0,
|
expire_time: 0,
|
||||||
discount_code_id: 0,
|
discount_code_id: 0,
|
||||||
coupon_id: 0,
|
user_coupon_id: 0,
|
||||||
state: 0,
|
state: 0,
|
||||||
pay_type: '',
|
pay_type: 'default',
|
||||||
|
payment_order_id: '',
|
||||||
args: '',
|
args: '',
|
||||||
note: ''
|
note: ''
|
||||||
})
|
})
|
||||||
orderFormRef.value?.resetFields()
|
dialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查看订单详情
|
// 查看订单详情
|
||||||
@@ -644,10 +710,12 @@ const handleView = async (row) => {
|
|||||||
// 编辑订单
|
// 编辑订单
|
||||||
const handleEdit = (row) => {
|
const handleEdit = (row) => {
|
||||||
dialogType.value = 'edit'
|
dialogType.value = 'edit'
|
||||||
dialogVisible.value = true
|
currentStep.value = 0
|
||||||
clearAllSelections()
|
clearAllSelections()
|
||||||
|
planList.value = []
|
||||||
|
productParams.value = []
|
||||||
|
Object.keys(argValues).forEach(k => delete argValues[k])
|
||||||
|
|
||||||
// 处理过期时间:优先使用已转换的时间戳,否则转换ISO格式
|
|
||||||
let expireTimeMs = null
|
let expireTimeMs = null
|
||||||
if (row._expireTimeMs !== undefined) {
|
if (row._expireTimeMs !== undefined) {
|
||||||
expireTimeMs = row._expireTimeMs
|
expireTimeMs = row._expireTimeMs
|
||||||
@@ -658,29 +726,54 @@ const handleEdit = (row) => {
|
|||||||
Object.assign(orderForm, {
|
Object.assign(orderForm, {
|
||||||
order_id: row.id,
|
order_id: row.id,
|
||||||
name: row.name,
|
name: row.name,
|
||||||
table: row.table,
|
table: row.table || '',
|
||||||
|
type: row.type || 'create',
|
||||||
user_id: row.userId,
|
user_id: row.userId,
|
||||||
commodity_id: row.commodityId,
|
commodity_id: row.commodityId || 0,
|
||||||
|
plan_id: row.planId || null,
|
||||||
pay_num: row.payNum,
|
pay_num: row.payNum,
|
||||||
price: row.price,
|
price: row.price,
|
||||||
renew_price: row.renewPrice,
|
renew_price: row.renewPrice,
|
||||||
expire_time: expireTimeMs,
|
expire_time: expireTimeMs,
|
||||||
discount_code_id: 0, // 从详情接口获取
|
discount_code_id: 0,
|
||||||
coupon_id: 0, // 从详情接口获取
|
user_coupon_id: 0,
|
||||||
state: row.state,
|
state: row.state,
|
||||||
pay_type: row.payType || '',
|
pay_type: row.payType || 'default',
|
||||||
|
payment_order_id: row.paymentOrderId || '',
|
||||||
args: row.args || '',
|
args: row.args || '',
|
||||||
note: row.note || ''
|
note: row.note || ''
|
||||||
})
|
})
|
||||||
|
|
||||||
// 设置显示信息(只显示ID,名称需要从选择器中获取)
|
|
||||||
if (row.userId) {
|
if (row.userId) {
|
||||||
selectedUserInfo.value = { user_id: row.userId, user_name: `用户${row.userId}` }
|
selectedUserInfo.value = { user_id: row.userId, user_name: `用户${row.userId}` }
|
||||||
}
|
}
|
||||||
if (row.commodityId) {
|
if (row.commodityId) {
|
||||||
selectedProductInfo.value = { id: row.commodityId, name: `商品${row.commodityId}` }
|
selectedProductInfo.value = { id: row.commodityId, name: `商品${row.commodityId}` }
|
||||||
|
fetchPlanList(row.commodityId)
|
||||||
|
fetchProductParams(row.commodityId).then(() => {
|
||||||
|
// 从已有 args 中恢复参数值
|
||||||
|
if (row.args) {
|
||||||
|
try {
|
||||||
|
const existingArgs = JSON.parse(row.args)
|
||||||
|
if (Array.isArray(existingArgs)) {
|
||||||
|
for (const a of existingArgs) {
|
||||||
|
const param = productParams.value.find(p => p.id === a.arg_id)
|
||||||
|
if (!param) continue
|
||||||
|
if (param.type === 'select') {
|
||||||
|
argValues[param.id] = a.attr_id
|
||||||
|
} else if (param.type === 'number') {
|
||||||
|
argValues[param.id] = a.number
|
||||||
|
} else {
|
||||||
|
argValues[param.id] = a.value || ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
// 重试订单流程
|
// 重试订单流程
|
||||||
const handleRetryOrder = (row) => {
|
const handleRetryOrder = (row) => {
|
||||||
@@ -744,46 +837,166 @@ const handleBatchDelete = () => {
|
|||||||
}).catch(() => {})
|
}).catch(() => {})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提交表单
|
// 订单类型变更
|
||||||
const submitForm = () => {
|
const handleTypeChange = (type) => {
|
||||||
orderFormRef.value?.validate(async (valid) => {
|
if (type === 'create') {
|
||||||
if (valid) {
|
orderForm.table = 'good'
|
||||||
try {
|
}
|
||||||
// 处理过期时间:将毫秒级时间戳转换为秒级时间戳
|
|
||||||
let expireTimeSeconds = 0
|
|
||||||
if (orderForm.expire_time) {
|
|
||||||
const timestamp = timeToTimestamp(new Date(orderForm.expire_time))
|
|
||||||
expireTimeSeconds = timestamp || 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2026-05-12: /api/v1/admin/order/create 与 /update 入参 price / renew_price 单位由
|
// 分步导航
|
||||||
// "分"改为"元"(后端接口变更,用户确认两端都已生效);列表与表单内部仍按"分"持有,
|
const handleNextStep = async () => {
|
||||||
// 提交时统一除以 100 做"分→元"换算,避免再次入库时被当成元导致额外放大 100 倍。
|
if (currentStep.value === 0) {
|
||||||
// 输入框旁的"分"单位文案暂不改动(用户明确仅要求 /100),UI 一致性问题待后续单独处理。
|
const formEl = step1FormRef.value
|
||||||
|
if (!formEl) return
|
||||||
|
const valid = await formEl.validate().catch(() => false)
|
||||||
|
if (!valid) return
|
||||||
|
} else if (currentStep.value === 1) {
|
||||||
|
const formEl = step2FormRef.value
|
||||||
|
if (!formEl) return
|
||||||
|
const valid = await formEl.validate().catch(() => false)
|
||||||
|
if (!valid) return
|
||||||
|
}
|
||||||
|
currentStep.value++
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载套餐列表
|
||||||
|
const fetchPlanList = async (goodId) => {
|
||||||
|
if (!goodId) { planList.value = []; return }
|
||||||
|
planLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await getProductPlanList({ good_id: goodId, page: 1, count: 100 })
|
||||||
|
if (res.data.code === 200) {
|
||||||
|
planList.value = res.data.data?.list || res.data.data || []
|
||||||
|
} else {
|
||||||
|
planList.value = []
|
||||||
|
}
|
||||||
|
} catch { planList.value = [] }
|
||||||
|
finally { planLoading.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载商品参数列表
|
||||||
|
const fetchProductParams = async (goodId) => {
|
||||||
|
if (!goodId) { productParams.value = []; return }
|
||||||
|
paramsLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await getProductParameterList({ good_id: goodId })
|
||||||
|
if (res.data.code === 200) {
|
||||||
|
const list = res.data.data || []
|
||||||
|
await Promise.all(list.map(async (param) => {
|
||||||
|
try {
|
||||||
|
const detail = await getProductParameterDetail({ good_id: goodId, arg_id: param.id })
|
||||||
|
if (detail.data.code === 200) {
|
||||||
|
const attrs = detail.data.data.attrs || []
|
||||||
|
attrs.sort((a, b) => (Number(a.index) || 0) - (Number(b.index) || 0))
|
||||||
|
param.attrs = attrs
|
||||||
|
} else { param.attrs = [] }
|
||||||
|
} catch { param.attrs = [] }
|
||||||
|
}))
|
||||||
|
productParams.value = list
|
||||||
|
// 初始化默认值
|
||||||
|
for (const param of list) {
|
||||||
|
if (argValues[param.id] !== undefined) continue
|
||||||
|
if (param.type === 'select' && param.attrs?.length) {
|
||||||
|
argValues[param.id] = param.attrs[0].id
|
||||||
|
} else if (param.type === 'number') {
|
||||||
|
argValues[param.id] = param.min || 0
|
||||||
|
} else {
|
||||||
|
argValues[param.id] = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else { productParams.value = [] }
|
||||||
|
} catch { productParams.value = [] }
|
||||||
|
finally { paramsLoading.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据参数表单值构建 args JSON
|
||||||
|
const buildArgsJson = () => {
|
||||||
|
if (!productParams.value.length) return ''
|
||||||
|
const args = []
|
||||||
|
for (const param of productParams.value) {
|
||||||
|
const val = argValues[param.id]
|
||||||
|
if (val === undefined || val === null || val === '') continue
|
||||||
|
if (param.type === 'select') {
|
||||||
|
const attr = param.attrs?.find(a => a.id === val)
|
||||||
|
if (attr) {
|
||||||
|
args.push({ arg_id: param.id, name: param.name, attr_id: attr.id, value: attr.value || attr.name })
|
||||||
|
}
|
||||||
|
} else if (param.type === 'number') {
|
||||||
|
const matchedAttr = findMatchedAttr(param, val)
|
||||||
|
args.push({ arg_id: param.id, name: param.name, attr_id: matchedAttr?.id || (param.attrs?.[0]?.id || 0), number: val })
|
||||||
|
} else {
|
||||||
|
const attr = param.attrs?.[0]
|
||||||
|
args.push({ arg_id: param.id, name: param.name, attr_id: attr?.id || 0, value: val })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return args.length > 0 ? JSON.stringify(args) : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const findMatchedAttr = (param, numValue) => {
|
||||||
|
if (!param.attrs || !param.attrs.length) return null
|
||||||
|
const sorted = [...param.attrs].sort((a, b) => (a.phase || 0) - (b.phase || 0))
|
||||||
|
for (const attr of sorted) {
|
||||||
|
const phase = attr.phase || 0
|
||||||
|
if (attr.rangeType === 'before' && numValue <= phase) return attr
|
||||||
|
if (attr.rangeType === 'after' && numValue >= phase) return attr
|
||||||
|
if (attr.rangeType === 'equal' && numValue === phase) return attr
|
||||||
|
}
|
||||||
|
return sorted[sorted.length - 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 套餐选择变更
|
||||||
|
const handlePlanChange = (planId) => {
|
||||||
|
if (!planId) return
|
||||||
|
const plan = planList.value.find(p => p.id === planId)
|
||||||
|
if (plan) {
|
||||||
|
if (plan.price) orderForm.price = plan.price
|
||||||
|
if (plan.renew_price) orderForm.renew_price = plan.renew_price
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交表单
|
||||||
|
const submitForm = async () => {
|
||||||
|
const formEl = step3FormRef.value
|
||||||
|
if (formEl) {
|
||||||
|
const valid = await formEl.validate().catch(() => false)
|
||||||
|
if (!valid) return
|
||||||
|
}
|
||||||
|
|
||||||
|
submitLoading.value = true
|
||||||
|
try {
|
||||||
|
let expireTimeStr = ''
|
||||||
|
if (orderForm.expire_time) {
|
||||||
|
expireTimeStr = new Date(Number(orderForm.expire_time)).toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
|
const argsStr = productParams.value.length ? buildArgsJson() : orderForm.args || ''
|
||||||
|
|
||||||
const submitData = {
|
const submitData = {
|
||||||
name: orderForm.name,
|
name: orderForm.name,
|
||||||
table: orderForm.table,
|
table: orderForm.table || '',
|
||||||
|
type: orderForm.type,
|
||||||
user_id: Number(orderForm.user_id),
|
user_id: Number(orderForm.user_id),
|
||||||
commodity_id: Number(orderForm.commodity_id),
|
commodity_id: Number(orderForm.commodity_id),
|
||||||
pay_num: Number(orderForm.pay_num),
|
pay_num: Number(orderForm.pay_num),
|
||||||
price: Number(orderForm.price) / 100,
|
price: Number(orderForm.price) / 100,
|
||||||
renew_price: Number(orderForm.renew_price) / 100,
|
renew_price: Number(orderForm.renew_price) / 100,
|
||||||
expire_time: expireTimeSeconds,
|
expire_time: expireTimeStr,
|
||||||
discount_code_id: Number(orderForm.discount_code_id),
|
|
||||||
coupon_id: Number(orderForm.coupon_id),
|
|
||||||
state: Number(orderForm.state),
|
state: Number(orderForm.state),
|
||||||
pay_type: orderForm.pay_type || '',
|
pay_type: orderForm.pay_type || 'default',
|
||||||
args: orderForm.args || '',
|
args: argsStr,
|
||||||
note: orderForm.note || ''
|
note: orderForm.note || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果是编辑,添加order_id
|
if (orderForm.plan_id) submitData.plan_id = Number(orderForm.plan_id)
|
||||||
|
if (orderForm.discount_code_id) submitData.discount_code_id = Number(orderForm.discount_code_id)
|
||||||
|
if (orderForm.user_coupon_id) submitData.user_coupon_id = Number(orderForm.user_coupon_id)
|
||||||
|
if (orderForm.payment_order_id) submitData.payment_order_id = orderForm.payment_order_id
|
||||||
|
|
||||||
if (dialogType.value === 'edit') {
|
if (dialogType.value === 'edit') {
|
||||||
submitData.order_id = Number(orderForm.order_id)
|
submitData.order_id = Number(orderForm.order_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('提交订单数据:', submitData)
|
|
||||||
|
|
||||||
let res
|
let res
|
||||||
if (dialogType.value === 'add') {
|
if (dialogType.value === 'add') {
|
||||||
res = await createOrder(submitData)
|
res = await createOrder(submitData)
|
||||||
@@ -795,14 +1008,15 @@ const submitForm = () => {
|
|||||||
ElMessage.success(dialogType.value === 'add' ? '新增成功' : '修改成功')
|
ElMessage.success(dialogType.value === 'add' ? '新增成功' : '修改成功')
|
||||||
dialogVisible.value = false
|
dialogVisible.value = false
|
||||||
fetchOrderList()
|
fetchOrderList()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.data.message || '操作失败')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('操作失败:', error)
|
|
||||||
ElMessage.error(error.response?.data?.message || '操作失败')
|
ElMessage.error(error.response?.data?.message || '操作失败')
|
||||||
|
} finally {
|
||||||
|
submitLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 用户选择处理
|
// 用户选择处理
|
||||||
const handleUserSelect = (user) => {
|
const handleUserSelect = (user) => {
|
||||||
@@ -819,15 +1033,21 @@ const clearUser = () => {
|
|||||||
const handleProductSelect = (product) => {
|
const handleProductSelect = (product) => {
|
||||||
orderForm.commodity_id = product.id
|
orderForm.commodity_id = product.id
|
||||||
selectedProductInfo.value = product
|
selectedProductInfo.value = product
|
||||||
// 自动填充表名
|
if (product.table) orderForm.table = product.table
|
||||||
if (product.table) {
|
orderForm.plan_id = null
|
||||||
orderForm.table = product.table
|
// 清空旧参数值
|
||||||
}
|
Object.keys(argValues).forEach(k => delete argValues[k])
|
||||||
|
fetchPlanList(product.id)
|
||||||
|
fetchProductParams(product.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearProduct = () => {
|
const clearProduct = () => {
|
||||||
orderForm.commodity_id = 0
|
orderForm.commodity_id = 0
|
||||||
|
orderForm.plan_id = null
|
||||||
selectedProductInfo.value = null
|
selectedProductInfo.value = null
|
||||||
|
planList.value = []
|
||||||
|
productParams.value = []
|
||||||
|
Object.keys(argValues).forEach(k => delete argValues[k])
|
||||||
}
|
}
|
||||||
|
|
||||||
// 优惠码选择处理
|
// 优惠码选择处理
|
||||||
@@ -843,12 +1063,12 @@ const clearDiscountCode = () => {
|
|||||||
|
|
||||||
// 代金券选择处理
|
// 代金券选择处理
|
||||||
const handleVoucherSelect = (voucher) => {
|
const handleVoucherSelect = (voucher) => {
|
||||||
orderForm.coupon_id = voucher.id
|
orderForm.user_coupon_id = voucher.id
|
||||||
selectedVoucherInfo.value = voucher
|
selectedVoucherInfo.value = voucher
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearVoucher = () => {
|
const clearVoucher = () => {
|
||||||
orderForm.coupon_id = 0
|
orderForm.user_coupon_id = 0
|
||||||
selectedVoucherInfo.value = null
|
selectedVoucherInfo.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -946,11 +1166,57 @@ onMounted(() => {
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog-footer {
|
/* 分步向导样式 */
|
||||||
|
.wizard-container {
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wizard-steps {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wizard-body {
|
||||||
|
min-height: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wizard-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 0;
|
}
|
||||||
|
|
||||||
|
.form-tip {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
margin-top: 4px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.args-section {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
background: #f9fafb;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.args-section-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px solid #ebeef5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.number-arg-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 表格样式优化 */
|
/* 表格样式优化 */
|
||||||
|
|||||||
@@ -488,11 +488,11 @@
|
|||||||
<div class="tk-section-title">数值范围配置</div>
|
<div class="tk-section-title">数值范围配置</div>
|
||||||
<el-form-item label="范围类型" prop="range_type">
|
<el-form-item label="范围类型" prop="range_type">
|
||||||
<el-select v-model="paramValueForm.range_type" placeholder="请选择范围类型" style="width: 100%">
|
<el-select v-model="paramValueForm.range_type" placeholder="请选择范围类型" style="width: 100%">
|
||||||
<el-option label="小于 (before)" value="before" />
|
<el-option label="小于等于 (before)" value="before" />
|
||||||
<el-option label="大于 (after)" value="after" />
|
<el-option label="大于等于 (after)" value="after" />
|
||||||
<el-option label="等于 (equal)" value="equal" />
|
<el-option label="等于 (equal)" value="equal" />
|
||||||
</el-select>
|
</el-select>
|
||||||
<div class="form-tip">before: 数值 < phase 时匹配 | after: 数值 > phase 时匹配</div>
|
<div class="form-tip">before: 数值 ≤ phase 时匹配 | after: 数值 ≥ phase 时匹配</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="阈值" prop="attr_range">
|
<el-form-item label="阈值" prop="attr_range">
|
||||||
<el-input-number v-model="paramValueForm.attr_range" :min="0" placeholder="范围阈值" style="width: 100%" />
|
<el-input-number v-model="paramValueForm.attr_range" :min="0" placeholder="范围阈值" style="width: 100%" />
|
||||||
@@ -1532,7 +1532,7 @@ const getArgTypeTag = (type) => {
|
|||||||
|
|
||||||
// 范围类型显示
|
// 范围类型显示
|
||||||
const getRangeTypeText = (type) => {
|
const getRangeTypeText = (type) => {
|
||||||
const typeMap = { 'after': '大于 >', 'before': '小于 <', 'equal': '等于 =' }
|
const typeMap = { 'after': '大于等于 ≥', 'before': '小于等于 ≤', 'equal': '等于 =' }
|
||||||
return typeMap[type] || type || '-'
|
return typeMap[type] || type || '-'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1960,9 +1960,9 @@ const findMatchingNumberAttr = (spec, numValue) => {
|
|||||||
const phase = attr.phase || 0
|
const phase = attr.phase || 0
|
||||||
const rangeType = attr.rangeType || 'before'
|
const rangeType = attr.rangeType || 'before'
|
||||||
|
|
||||||
if (rangeType === 'before' && numValue < phase) {
|
if (rangeType === 'before' && numValue <= phase) {
|
||||||
return attr
|
return attr
|
||||||
} else if (rangeType === 'after' && numValue > phase) {
|
} else if (rangeType === 'after' && numValue >= phase) {
|
||||||
return attr
|
return attr
|
||||||
} else if (rangeType === 'equal' && numValue === phase) {
|
} else if (rangeType === 'equal' && numValue === phase) {
|
||||||
return attr
|
return attr
|
||||||
|
|||||||
@@ -275,13 +275,13 @@
|
|||||||
<el-form-item v-if="currentParam?.type === 'number'" label="数值范围" prop="attr_range">
|
<el-form-item v-if="currentParam?.type === 'number'" label="数值范围" prop="attr_range">
|
||||||
<div class="range-config-row">
|
<div class="range-config-row">
|
||||||
<el-select v-model="paramValueForm.range_type" class="range-type-select">
|
<el-select v-model="paramValueForm.range_type" class="range-type-select">
|
||||||
<el-option label="<" value="before">
|
<el-option label="≤" value="before">
|
||||||
<span class="range-opt-symbol"><</span>
|
<span class="range-opt-symbol">≤</span>
|
||||||
<span class="range-opt-desc">小于</span>
|
<span class="range-opt-desc">小于等于</span>
|
||||||
</el-option>
|
</el-option>
|
||||||
<el-option label=">" value="after">
|
<el-option label="≥" value="after">
|
||||||
<span class="range-opt-symbol">></span>
|
<span class="range-opt-symbol">≥</span>
|
||||||
<span class="range-opt-desc">大于</span>
|
<span class="range-opt-desc">大于等于</span>
|
||||||
</el-option>
|
</el-option>
|
||||||
<el-option label="=" value="equal">
|
<el-option label="=" value="equal">
|
||||||
<span class="range-opt-symbol">=</span>
|
<span class="range-opt-symbol">=</span>
|
||||||
@@ -635,7 +635,7 @@ const getArgTypeTag = (type) => {
|
|||||||
return tagMap[type] || 'info'
|
return tagMap[type] || 'info'
|
||||||
}
|
}
|
||||||
const getRangeTypeText = (type) => {
|
const getRangeTypeText = (type) => {
|
||||||
const typeMap = { 'after': '>', 'before': '<', 'equal': '=' }
|
const typeMap = { 'after': '≥', 'before': '≤', 'equal': '=' }
|
||||||
return typeMap[type] || type || '-'
|
return typeMap[type] || type || '-'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -548,8 +548,8 @@ const findMatchingNumberAttr = (spec, numValue) => {
|
|||||||
for (const attr of sortedAttrs) {
|
for (const attr of sortedAttrs) {
|
||||||
const phase = attr.phase || 0
|
const phase = attr.phase || 0
|
||||||
const rangeType = attr.rangeType || 'before'
|
const rangeType = attr.rangeType || 'before'
|
||||||
if (rangeType === 'before' && numValue < phase) return attr
|
if (rangeType === 'before' && numValue <= phase) return attr
|
||||||
else if (rangeType === 'after' && numValue > phase) return attr
|
else if (rangeType === 'after' && numValue >= phase) return attr
|
||||||
else if (rangeType === 'equal' && numValue === phase) return attr
|
else if (rangeType === 'equal' && numValue === phase) return attr
|
||||||
}
|
}
|
||||||
return sortedAttrs[sortedAttrs.length - 1]
|
return sortedAttrs[sortedAttrs.length - 1]
|
||||||
|
|||||||
@@ -0,0 +1,892 @@
|
|||||||
|
<template>
|
||||||
|
<div class="sms-goods-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="#e6a23c" stroke-width="1.8">
|
||||||
|
<path d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 class="header-title">短信额度商品管理</h2>
|
||||||
|
<p class="header-desc">
|
||||||
|
管理短信平台的额度商品配置
|
||||||
|
<template v-if="filterServiceName">
|
||||||
|
— 当前筛选:<el-tag size="small" type="warning">{{ filterServiceName }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="header-actions">
|
||||||
|
<el-button @click="router.push('/sms/service')">
|
||||||
|
<el-icon><Back /></el-icon> 返回服务列表
|
||||||
|
</el-button>
|
||||||
|
<el-button type="primary" @click="handleAdd">
|
||||||
|
<el-icon><Plus /></el-icon> 新增商品
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 筛选栏 -->
|
||||||
|
<div class="filter-bar">
|
||||||
|
<el-select
|
||||||
|
v-model="queryParams.service_id"
|
||||||
|
placeholder="筛选主控服务"
|
||||||
|
clearable
|
||||||
|
style="width: 220px"
|
||||||
|
@change="handleSearch"
|
||||||
|
>
|
||||||
|
<el-option v-for="s in serviceOptions" :key="s.id" :label="s.name" :value="s.id" />
|
||||||
|
</el-select>
|
||||||
|
<el-input
|
||||||
|
v-model="queryParams.key"
|
||||||
|
placeholder="搜索商品名称"
|
||||||
|
clearable
|
||||||
|
style="width: 240px"
|
||||||
|
@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 label="关联服务" min-width="140">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button link type="primary" size="small" @click="goToService(row.serviceId)">
|
||||||
|
{{ resolveServiceName(row.serviceId) }}
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="关联商品" min-width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span class="goods-name">{{ row.good?.name || `商品#${row.goodId}` }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="额度类型" width="120" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="quotaTypeTag(row.quotaType).type" effect="dark" size="small">
|
||||||
|
{{ quotaTypeTag(row.quotaType).label }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="有效期/周期配置" min-width="200">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<template v-if="row.quotaType === 2">
|
||||||
|
<span class="config-label">有效期模式:</span>
|
||||||
|
<el-tag size="small" type="info">{{ row.expireMode === 'fixed' ? '固定' : '用户选择' }}</el-tag>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="row.quotaType === 3">
|
||||||
|
<span class="config-label">周期模式:</span>
|
||||||
|
<el-tag size="small" type="info">{{ row.cycleMode === 'fixed' ? '固定' : '用户选择' }}</el-tag>
|
||||||
|
<template v-if="row.cycleMode === 'fixed'">
|
||||||
|
<span class="config-detail">每{{ row.cycleValue || 1 }}{{ cycleUnitLabel(row.cycleUnit) }}</span>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<span v-else class="config-detail">永久有效</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="note" label="备注" min-width="150" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span class="note-text">{{ row.note || '-' }}</span>
|
||||||
|
</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="160" align="center" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button link type="primary" size="small" @click="handleEdit(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="addDialogVisible"
|
||||||
|
title="新增短信额度商品"
|
||||||
|
width="680px"
|
||||||
|
destroy-on-close
|
||||||
|
>
|
||||||
|
<el-form ref="addFormRef" :model="addForm" :rules="addRules" label-width="130px">
|
||||||
|
<el-divider content-position="left">基本信息</el-divider>
|
||||||
|
<el-form-item label="关联主控服务" prop="service_id">
|
||||||
|
<el-select v-model="addForm.service_id" placeholder="选择短信主控服务" style="width: 100%">
|
||||||
|
<el-option v-for="s in serviceOptions" :key="s.id" :label="s.name" :value="s.id" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="商品分组" prop="good_group_id">
|
||||||
|
<el-tree-select
|
||||||
|
v-model="addForm.good_group_id"
|
||||||
|
:data="groupTreeData"
|
||||||
|
:props="groupTreeProps"
|
||||||
|
lazy
|
||||||
|
:load="loadGroupChildren"
|
||||||
|
node-key="id"
|
||||||
|
placeholder="选择商品分组"
|
||||||
|
clearable
|
||||||
|
check-strictly
|
||||||
|
:render-after-expand="false"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="商品名称" prop="name">
|
||||||
|
<el-input v-model="addForm.name" placeholder="如:短信1000条包" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="商品介绍">
|
||||||
|
<el-input v-model="addForm.content" type="textarea" :rows="2" placeholder="商品介绍(可选,系统自动追加配额说明)" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="管理备注">
|
||||||
|
<el-input v-model="addForm.note" placeholder="管理备注(可选)" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-divider content-position="left">额度配置</el-divider>
|
||||||
|
<el-form-item label="额度类型" prop="quota_type">
|
||||||
|
<div class="quota-type-cards">
|
||||||
|
<div
|
||||||
|
v-for="qt in quotaTypes"
|
||||||
|
:key="qt.value"
|
||||||
|
class="qt-card"
|
||||||
|
:class="{ active: addForm.quota_type === qt.value }"
|
||||||
|
@click="addForm.quota_type = qt.value"
|
||||||
|
>
|
||||||
|
<div class="qt-icon" v-html="qt.icon"></div>
|
||||||
|
<div class="qt-label">{{ qt.label }}</div>
|
||||||
|
<div class="qt-desc">{{ qt.desc }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="额度数量模式" prop="quota_value_type">
|
||||||
|
<el-radio-group v-model="addForm.quota_value_type">
|
||||||
|
<el-radio value="number">数值范围(用户输入数值)</el-radio>
|
||||||
|
<el-radio value="select">固定选项(用户选择档位)</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<template v-if="addForm.quota_value_type === 'number'">
|
||||||
|
<el-form-item label="最小值">
|
||||||
|
<el-input-number v-model="addForm.quota_min" :min="1" :step="100" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="最大值">
|
||||||
|
<el-input-number v-model="addForm.quota_max" :min="1" :step="100" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="步长">
|
||||||
|
<el-input-number v-model="addForm.quota_step" :min="1" :step="10" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="单条价格" prop="quota_unit_price">
|
||||||
|
<el-input-number v-model="addForm.quota_unit_price" :min="0" :step="0.01" :precision="2" controls-position="right" />
|
||||||
|
<span class="opt-unit">元/条</span>
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<el-form-item label="额度选项">
|
||||||
|
<div class="dynamic-options">
|
||||||
|
<div v-for="(item, idx) in addForm.quota_options_list" :key="idx" class="option-row">
|
||||||
|
<el-input-number v-model="item.value" :min="1" placeholder="数量" controls-position="right" class="opt-value" />
|
||||||
|
<el-input v-model="item.label" placeholder="显示名称" class="opt-label" />
|
||||||
|
<el-input-number v-model="item.price" :min="0" :step="0.01" :precision="2" placeholder="价格" controls-position="right" class="opt-price" />
|
||||||
|
<span class="opt-unit">元</span>
|
||||||
|
<el-button link type="danger" @click="addForm.quota_options_list.splice(idx, 1)" :disabled="addForm.quota_options_list.length <= 1">
|
||||||
|
<el-icon><Delete /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<el-button size="small" @click="addForm.quota_options_list.push({ value: null, label: '', price: null })">
|
||||||
|
<el-icon><Plus /></el-icon> 添加选项
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 短期额度配置 -->
|
||||||
|
<template v-if="addForm.quota_type === 2">
|
||||||
|
<el-divider content-position="left">有效期配置</el-divider>
|
||||||
|
<el-form-item label="有效期模式" prop="expire_mode">
|
||||||
|
<el-radio-group v-model="addForm.expire_mode">
|
||||||
|
<el-radio value="fixed">固定天数(管理员指定)</el-radio>
|
||||||
|
<el-radio value="select">用户选择</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="addForm.expire_mode === 'fixed'" label="有效天数">
|
||||||
|
<el-input-number v-model="addForm.expire_fixed" :min="1" />
|
||||||
|
<span style="margin-left: 8px; color: #909399">天</span>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="addForm.expire_mode === 'select'" label="有效期选项">
|
||||||
|
<div class="dynamic-options">
|
||||||
|
<div v-for="(item, idx) in addForm.expire_options_list" :key="idx" class="option-row">
|
||||||
|
<el-input-number v-model="item.value" :min="1" placeholder="天数" controls-position="right" class="opt-value" />
|
||||||
|
<span class="opt-unit">天</span>
|
||||||
|
<el-input v-model="item.label" placeholder="显示名称" class="opt-label" />
|
||||||
|
<el-button link type="danger" @click="addForm.expire_options_list.splice(idx, 1)" :disabled="addForm.expire_options_list.length <= 1">
|
||||||
|
<el-icon><Delete /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<el-button size="small" @click="addForm.expire_options_list.push({ value: null, label: '' })">
|
||||||
|
<el-icon><Plus /></el-icon> 添加选项
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 周期额度配置 -->
|
||||||
|
<template v-if="addForm.quota_type === 3">
|
||||||
|
<el-divider content-position="left">周期配置</el-divider>
|
||||||
|
<el-form-item label="周期单位模式" prop="cycle_mode">
|
||||||
|
<el-radio-group v-model="addForm.cycle_mode">
|
||||||
|
<el-radio value="fixed">固定单位(管理员指定)</el-radio>
|
||||||
|
<el-radio value="select">用户选择</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="addForm.cycle_mode === 'fixed'" label="周期单位">
|
||||||
|
<el-select v-model="addForm.cycle_unit" style="width: 160px">
|
||||||
|
<el-option label="天" value="day" />
|
||||||
|
<el-option label="周" value="week" />
|
||||||
|
<el-option label="月" value="month" />
|
||||||
|
<el-option label="年" value="year" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="addForm.cycle_mode === 'select'" label="周期选项">
|
||||||
|
<div class="dynamic-options">
|
||||||
|
<div v-for="(item, idx) in addForm.cycle_options_list" :key="idx" class="option-row">
|
||||||
|
<el-select v-model="item.value" placeholder="周期单位" class="opt-cycle-unit">
|
||||||
|
<el-option label="天" value="day" />
|
||||||
|
<el-option label="周" value="week" />
|
||||||
|
<el-option label="月" value="month" />
|
||||||
|
<el-option label="年" value="year" />
|
||||||
|
</el-select>
|
||||||
|
<el-input v-model="item.label" placeholder="显示名称" class="opt-label" />
|
||||||
|
<el-button link type="danger" @click="addForm.cycle_options_list.splice(idx, 1)" :disabled="addForm.cycle_options_list.length <= 1">
|
||||||
|
<el-icon><Delete /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<el-button size="small" @click="addForm.cycle_options_list.push({ value: '', label: '' })">
|
||||||
|
<el-icon><Plus /></el-icon> 添加选项
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="周期数值" prop="cycle_value">
|
||||||
|
<el-input-number v-model="addForm.cycle_value" :min="1" />
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="addDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="submitting" @click="handleSubmitAdd">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 编辑弹窗(仅可修改 SmsGoods 自身字段) -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="editDialogVisible"
|
||||||
|
title="编辑短信额度商品"
|
||||||
|
width="580px"
|
||||||
|
destroy-on-close
|
||||||
|
>
|
||||||
|
<el-form ref="editFormRef" :model="editForm" label-width="130px">
|
||||||
|
<el-form-item label="管理备注">
|
||||||
|
<el-input v-model="editForm.note" placeholder="管理备注" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="额度类型">
|
||||||
|
<div class="quota-type-cards small">
|
||||||
|
<div
|
||||||
|
v-for="qt in quotaTypes"
|
||||||
|
:key="qt.value"
|
||||||
|
class="qt-card"
|
||||||
|
:class="{ active: editForm.quota_type === qt.value }"
|
||||||
|
@click="editForm.quota_type = qt.value"
|
||||||
|
>
|
||||||
|
<div class="qt-icon" v-html="qt.icon"></div>
|
||||||
|
<div class="qt-label">{{ qt.label }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<template v-if="editForm.quota_type === 2">
|
||||||
|
<el-form-item label="有效期模式">
|
||||||
|
<el-radio-group v-model="editForm.expire_mode">
|
||||||
|
<el-radio value="fixed">固定天数</el-radio>
|
||||||
|
<el-radio value="select">用户选择</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
<template v-if="editForm.quota_type === 3">
|
||||||
|
<el-form-item label="周期单位模式">
|
||||||
|
<el-radio-group v-model="editForm.cycle_mode">
|
||||||
|
<el-radio value="fixed">固定单位</el-radio>
|
||||||
|
<el-radio value="select">用户选择</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="editForm.cycle_mode === 'fixed'" label="周期单位">
|
||||||
|
<el-select v-model="editForm.cycle_unit" style="width: 160px">
|
||||||
|
<el-option label="天" value="day" />
|
||||||
|
<el-option label="周" value="week" />
|
||||||
|
<el-option label="月" value="month" />
|
||||||
|
<el-option label="年" value="year" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="周期数值">
|
||||||
|
<el-input-number v-model="editForm.cycle_value" :min="1" />
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="editDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="submitting" @click="handleSubmitEdit">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted, computed } from 'vue'
|
||||||
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { Plus, Search, Back, Delete } from '@element-plus/icons-vue'
|
||||||
|
import {
|
||||||
|
getSmsServiceList,
|
||||||
|
getSmsGoodsList,
|
||||||
|
createSmsGoods,
|
||||||
|
updateSmsGoods,
|
||||||
|
deleteSmsGoods
|
||||||
|
} from '@/api/admin/smsService.js'
|
||||||
|
import { getProductGroupList } from '@/api/admin/product'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const submitting = ref(false)
|
||||||
|
const tableData = ref([])
|
||||||
|
const total = ref(0)
|
||||||
|
const serviceOptions = ref([])
|
||||||
|
const serviceMap = ref({})
|
||||||
|
const addDialogVisible = ref(false)
|
||||||
|
const editDialogVisible = ref(false)
|
||||||
|
const addFormRef = ref(null)
|
||||||
|
const editFormRef = ref(null)
|
||||||
|
|
||||||
|
const groupTreeData = ref([])
|
||||||
|
const groupTreeProps = { label: 'name', children: 'children', isLeaf: (data) => !data.existSub }
|
||||||
|
const filterServiceName = computed(() => route.query.service_name || '')
|
||||||
|
|
||||||
|
const queryParams = reactive({
|
||||||
|
page: 1,
|
||||||
|
count: 10,
|
||||||
|
service_id: route.query.service_id ? parseInt(route.query.service_id) : undefined,
|
||||||
|
key: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const quotaTypes = [
|
||||||
|
{
|
||||||
|
value: 1, label: '长期',
|
||||||
|
desc: '永久有效,一次购买持续可用',
|
||||||
|
icon: '<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 2, label: '短期',
|
||||||
|
desc: '限时有效,到期后额度失效',
|
||||||
|
icon: '<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 3, label: '周期',
|
||||||
|
desc: '按周期自动重置额度',
|
||||||
|
icon: '<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const defaultAddForm = () => ({
|
||||||
|
service_id: queryParams.service_id || null,
|
||||||
|
good_group_id: null,
|
||||||
|
name: '',
|
||||||
|
content: '',
|
||||||
|
note: '',
|
||||||
|
quota_type: 1,
|
||||||
|
quota_value_type: 'number',
|
||||||
|
quota_options_list: [{ value: null, label: '', price: null }],
|
||||||
|
quota_min: 100,
|
||||||
|
quota_max: 100000,
|
||||||
|
quota_step: 100,
|
||||||
|
quota_unit_price: 0.05,
|
||||||
|
expire_mode: 'fixed',
|
||||||
|
expire_fixed: 30,
|
||||||
|
expire_options_list: [{ value: null, label: '' }],
|
||||||
|
cycle_mode: 'fixed',
|
||||||
|
cycle_unit: 'month',
|
||||||
|
cycle_value: 1,
|
||||||
|
cycle_options_list: [{ value: '', label: '' }]
|
||||||
|
})
|
||||||
|
|
||||||
|
const serializeOptions = (list, withPrice = false) => {
|
||||||
|
return list
|
||||||
|
.filter(item => item.value !== null && item.value !== '' && item.label)
|
||||||
|
.map(item => withPrice ? `${item.value}:${item.label}:${item.price ?? 0}` : `${item.value}:${item.label}`)
|
||||||
|
.join(',')
|
||||||
|
}
|
||||||
|
|
||||||
|
const addForm = ref(defaultAddForm())
|
||||||
|
|
||||||
|
const editForm = ref({
|
||||||
|
id: null,
|
||||||
|
note: '',
|
||||||
|
quota_type: 1,
|
||||||
|
expire_mode: '',
|
||||||
|
cycle_mode: '',
|
||||||
|
cycle_unit: '',
|
||||||
|
cycle_value: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
const addRules = {
|
||||||
|
service_id: [{ required: true, message: '请选择主控服务', trigger: 'change' }],
|
||||||
|
name: [{ required: true, message: '请输入商品名称', trigger: 'blur' }],
|
||||||
|
quota_type: [{ required: true, message: '请选择额度类型', trigger: 'change' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatTime = (t) => {
|
||||||
|
if (!t) return '-'
|
||||||
|
return new Date(t).toLocaleString('zh-CN', { hour12: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
const quotaTypeTag = (type) => {
|
||||||
|
const map = { 1: { label: '长期', type: 'success' }, 2: { label: '短期', type: 'warning' }, 3: { label: '周期', type: '' } }
|
||||||
|
return map[type] || { label: '未知', type: 'info' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const cycleUnitLabel = (unit) => {
|
||||||
|
const map = { day: '天', week: '周', month: '月', year: '年' }
|
||||||
|
return map[unit] || unit
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveServiceName = (id) => {
|
||||||
|
return serviceMap.value[id] || `服务#${id}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToService = (serviceId) => {
|
||||||
|
router.push({ path: '/sms/service' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadGroupOptions = async () => {
|
||||||
|
try {
|
||||||
|
const res = await getProductGroupList({ level: 1, count: 199 })
|
||||||
|
if (res.data.code === 200) {
|
||||||
|
groupTreeData.value = (res.data.data?.data || res.data.data || []).map(item => ({
|
||||||
|
...item,
|
||||||
|
existSub: !!item.existSub
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadGroupChildren = async (node, resolve) => {
|
||||||
|
if (node.level === 0) return resolve(groupTreeData.value)
|
||||||
|
try {
|
||||||
|
const res = await getProductGroupList({ parent_id: node.data.id, level: node.data.level + 1, count: 199 })
|
||||||
|
if (res.data.code === 200) {
|
||||||
|
const children = (res.data.data?.data || res.data.data || []).map(item => ({
|
||||||
|
...item,
|
||||||
|
existSub: !!item.existSub
|
||||||
|
}))
|
||||||
|
resolve(children)
|
||||||
|
} else {
|
||||||
|
resolve([])
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
resolve([])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadServiceOptions = async () => {
|
||||||
|
try {
|
||||||
|
const res = await getSmsServiceList({ page: 1, count: 199 })
|
||||||
|
const body = res.data
|
||||||
|
if (body.code === 200) {
|
||||||
|
const list = body.data?.data || body.data || []
|
||||||
|
serviceOptions.value = Array.isArray(list) ? list : []
|
||||||
|
const map = {}
|
||||||
|
serviceOptions.value.forEach(s => { map[s.id] = s.name })
|
||||||
|
serviceMap.value = map
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchList = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const params = { ...queryParams }
|
||||||
|
if (!params.service_id) delete params.service_id
|
||||||
|
const res = await getSmsGoodsList(params)
|
||||||
|
const body = res.data
|
||||||
|
if (body.code === 200) {
|
||||||
|
const d = body.data?.data || body.data || []
|
||||||
|
tableData.value = Array.isArray(d) ? d : []
|
||||||
|
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.service_id = undefined
|
||||||
|
queryParams.page = 1
|
||||||
|
fetchList()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAdd = () => {
|
||||||
|
addForm.value = defaultAddForm()
|
||||||
|
addDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmitAdd = async () => {
|
||||||
|
await addFormRef.value?.validate()
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.append('service_id', addForm.value.service_id)
|
||||||
|
if (addForm.value.good_group_id) params.append('good_group_id', addForm.value.good_group_id)
|
||||||
|
params.append('name', addForm.value.name)
|
||||||
|
if (addForm.value.content) params.append('content', addForm.value.content)
|
||||||
|
if (addForm.value.note) params.append('note', addForm.value.note)
|
||||||
|
params.append('quota_type', addForm.value.quota_type)
|
||||||
|
params.append('quota_value_type', addForm.value.quota_value_type)
|
||||||
|
|
||||||
|
if (addForm.value.quota_value_type === 'number') {
|
||||||
|
params.append('quota_min', addForm.value.quota_min)
|
||||||
|
params.append('quota_max', addForm.value.quota_max)
|
||||||
|
params.append('quota_step', addForm.value.quota_step)
|
||||||
|
params.append('quota_unit_price', addForm.value.quota_unit_price)
|
||||||
|
} else {
|
||||||
|
params.append('quota_options', serializeOptions(addForm.value.quota_options_list, true))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addForm.value.quota_type === 2) {
|
||||||
|
params.append('expire_mode', addForm.value.expire_mode)
|
||||||
|
if (addForm.value.expire_mode === 'fixed') {
|
||||||
|
params.append('expire_fixed', addForm.value.expire_fixed)
|
||||||
|
} else {
|
||||||
|
params.append('expire_options', serializeOptions(addForm.value.expire_options_list))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addForm.value.quota_type === 3) {
|
||||||
|
params.append('cycle_mode', addForm.value.cycle_mode)
|
||||||
|
params.append('cycle_value', addForm.value.cycle_value)
|
||||||
|
if (addForm.value.cycle_mode === 'fixed') {
|
||||||
|
params.append('cycle_unit', addForm.value.cycle_unit)
|
||||||
|
} else {
|
||||||
|
params.append('cycle_options', serializeOptions(addForm.value.cycle_options_list))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await createSmsGoods(params)
|
||||||
|
if (res.data.code === 200) {
|
||||||
|
ElMessage.success('创建成功')
|
||||||
|
addDialogVisible.value = false
|
||||||
|
fetchList()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.data.message || '创建失败')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error('创建失败')
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEdit = (row) => {
|
||||||
|
editForm.value = {
|
||||||
|
id: row.id,
|
||||||
|
note: row.note || '',
|
||||||
|
quota_type: row.quotaType,
|
||||||
|
expire_mode: row.expireMode || 'fixed',
|
||||||
|
cycle_mode: row.cycleMode || 'fixed',
|
||||||
|
cycle_unit: row.cycleUnit || 'month',
|
||||||
|
cycle_value: row.cycleValue || 1
|
||||||
|
}
|
||||||
|
editDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmitEdit = async () => {
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.append('id', editForm.value.id)
|
||||||
|
if (editForm.value.note) params.append('note', editForm.value.note)
|
||||||
|
params.append('quota_type', editForm.value.quota_type)
|
||||||
|
|
||||||
|
if (editForm.value.quota_type === 2) {
|
||||||
|
params.append('expire_mode', editForm.value.expire_mode)
|
||||||
|
}
|
||||||
|
if (editForm.value.quota_type === 3) {
|
||||||
|
params.append('cycle_mode', editForm.value.cycle_mode)
|
||||||
|
params.append('cycle_value', editForm.value.cycle_value)
|
||||||
|
if (editForm.value.cycle_mode === 'fixed') {
|
||||||
|
params.append('cycle_unit', editForm.value.cycle_unit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await updateSmsGoods(params)
|
||||||
|
if (res.data.code === 200) {
|
||||||
|
ElMessage.success('更新成功')
|
||||||
|
editDialogVisible.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 deleteSmsGoods(params)
|
||||||
|
if (res.data.code === 200) {
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
fetchList()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.data.message || '删除失败')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error('删除失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await Promise.all([loadServiceOptions(), loadGroupOptions()])
|
||||||
|
fetchList()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.sms-goods-page {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 20px 24px;
|
||||||
|
background: linear-gradient(135deg, #fef9f0 0%, #fdf2e4 100%);
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #f0dfc8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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(230, 162, 60, 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
background: #fafbfc;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-name {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-label {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-detail {
|
||||||
|
color: #606266;
|
||||||
|
font-size: 13px;
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-text {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-wrap {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 额度类型卡片 */
|
||||||
|
.quota-type-cards {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quota-type-cards.small .qt-card {
|
||||||
|
padding: 10px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qt-card {
|
||||||
|
flex: 1;
|
||||||
|
padding: 16px;
|
||||||
|
border: 2px solid #ebeef5;
|
||||||
|
border-radius: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
text-align: center;
|
||||||
|
background: #fafbfc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qt-card:hover {
|
||||||
|
border-color: #c0c4cc;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qt-card.active {
|
||||||
|
border-color: #409eff;
|
||||||
|
background: #f0f7ff;
|
||||||
|
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.qt-icon {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qt-card.active .qt-icon {
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qt-label {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qt-desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #c0c4cc;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 动态选项列表 */
|
||||||
|
.dynamic-options {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-row:last-of-type {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opt-value {
|
||||||
|
width: 140px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opt-unit {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #909399;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opt-label {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opt-price {
|
||||||
|
width: 120px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opt-cycle-unit {
|
||||||
|
width: 120px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,410 @@
|
|||||||
|
<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>
|
||||||
|
<el-tag v-if="row.default" type="success" size="small" effect="dark" style="margin-left: 6px">默认</el-tag>
|
||||||
|
</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="320" align="center" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button link type="success" size="small" @click="openConsole(row)">控制台</el-button>
|
||||||
|
<el-button v-if="!row.default" link type="warning" size="small" @click="handleSetDefault(row)">设为默认</el-button>
|
||||||
|
<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="row.default ? '该服务为默认服务,删除前请先设置其他服务为默认' : '确认删除该服务?'" :disabled="row.default" @confirm="handleDelete(row)">
|
||||||
|
<template #reference>
|
||||||
|
<el-button link type="danger" size="small" :disabled="row.default">删除</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,
|
||||||
|
setDefaultSmsService
|
||||||
|
} 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 handleSetDefault = async (row) => {
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.append('id', row.id)
|
||||||
|
const res = await setDefaultSmsService(params)
|
||||||
|
if (res.data.code === 200) {
|
||||||
|
ElMessage.success(`已将「${row.name}」设为默认短信服务`)
|
||||||
|
fetchList()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.data.message || '设置失败')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error('设置默认服务失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openConsole = (row) => {
|
||||||
|
const base = (row.host || '').replace(/\/+$/, '')
|
||||||
|
if (!base) return ElMessage.warning('该服务未配置地址')
|
||||||
|
window.open(`${base}/login?serverToken=${encodeURIComponent(row.serviceToken || '')}`, '_blank')
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
@@ -0,0 +1,549 @@
|
|||||||
|
<template>
|
||||||
|
<div class="sms-signature-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="#67c23a" stroke-width="1.8">
|
||||||
|
<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/>
|
||||||
|
<polyline points="14 2 14 8 20 8"/>
|
||||||
|
<path d="M9 15l2 2 4-4"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 class="header-title">短信签名管理</h2>
|
||||||
|
<p class="header-desc">管理用户提交的短信签名,审核通过后方可使用</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="header-actions">
|
||||||
|
<el-button type="primary" @click="handleAdd">
|
||||||
|
<el-icon><Plus /></el-icon> 新增签名
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 筛选栏 -->
|
||||||
|
<div class="filter-bar">
|
||||||
|
<el-select v-model="queryParams.service_id" placeholder="选择主控服务" style="width: 200px" @change="handleSearch">
|
||||||
|
<el-option v-for="s in serviceOptions" :key="s.id" :label="s.name" :value="s.id" />
|
||||||
|
</el-select>
|
||||||
|
<el-select v-model="queryParams.status" placeholder="全部状态" clearable style="width: 140px" @change="handleSearch">
|
||||||
|
<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="filterUserInfo"
|
||||||
|
:model-value="`${filterUserInfo.user_name} (ID: ${queryParams.user_id})`"
|
||||||
|
readonly
|
||||||
|
style="width: 200px"
|
||||||
|
>
|
||||||
|
<template #suffix>
|
||||||
|
<el-icon class="clear-icon" @click="clearFilterUser"><Close /></el-icon>
|
||||||
|
</template>
|
||||||
|
<template #prefix><el-icon><User /></el-icon></template>
|
||||||
|
</el-input>
|
||||||
|
<el-input v-else placeholder="筛选用户" readonly style="width: 140px" @click="filterUserSelectorVisible = true">
|
||||||
|
<template #prefix><el-icon><User /></el-icon></template>
|
||||||
|
</el-input>
|
||||||
|
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||||
|
<el-button @click="handleReset">重置</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 卡片列表 -->
|
||||||
|
<div v-loading="loading" class="signature-cards">
|
||||||
|
<div v-if="!tableData.length && !loading" class="empty-state">
|
||||||
|
<el-empty description="暂无签名数据" :image-size="80" />
|
||||||
|
</div>
|
||||||
|
<div v-for="item in tableData" :key="item.ID" class="sig-card" :class="`status-${item.status}`">
|
||||||
|
<div class="sig-card-header">
|
||||||
|
<div class="sig-card-title">
|
||||||
|
<span class="sig-title-text">{{ item.title }}</span>
|
||||||
|
<el-tag :type="statusTagType(item.status)" size="small" effect="dark">{{ statusText(item.status) }}</el-tag>
|
||||||
|
</div>
|
||||||
|
<span class="sig-card-id">#{{ item.ID }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sig-card-body">
|
||||||
|
<div class="sig-card-info">
|
||||||
|
<div class="sig-info-row">
|
||||||
|
<span class="sig-label">申请人</span>
|
||||||
|
<span class="sig-value">{{ item.applicant_name || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="sig-info-row">
|
||||||
|
<span class="sig-label">用户</span>
|
||||||
|
<el-link type="primary" :underline="false" @click="$router.push({ path: '/user/detail', query: { user_id: item.user_id } })">ID: {{ item.user_id }}</el-link>
|
||||||
|
</div>
|
||||||
|
<div class="sig-info-row">
|
||||||
|
<span class="sig-label">公司</span>
|
||||||
|
<span class="sig-value">{{ item.applicant_company || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="sig-info-row">
|
||||||
|
<span class="sig-label">身份证</span>
|
||||||
|
<span class="sig-value sig-id-card">{{ item.applicant_id_card || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="sig-info-row">
|
||||||
|
<span class="sig-label">创建时间</span>
|
||||||
|
<span class="sig-value">{{ formatDate(item.CreatedAt) }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="item.reject_reason" class="sig-info-row reject-row">
|
||||||
|
<span class="sig-label">驳回原因</span>
|
||||||
|
<span class="sig-value reject-text">{{ item.reject_reason }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="sig-card-license">
|
||||||
|
<el-image
|
||||||
|
v-if="item.license_url"
|
||||||
|
:src="item.license_url"
|
||||||
|
:preview-src-list="[item.license_url]"
|
||||||
|
fit="cover"
|
||||||
|
class="license-img"
|
||||||
|
preview-teleported
|
||||||
|
/>
|
||||||
|
<div v-else class="license-placeholder">
|
||||||
|
<el-icon :size="24"><Picture /></el-icon>
|
||||||
|
<span>无资质证明</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sig-card-footer">
|
||||||
|
<el-button type="primary" size="small" @click="handleEdit(item)">编辑</el-button>
|
||||||
|
<el-button v-if="item.status === 0" type="warning" size="small" @click="handleSubmit(item)">提交审核</el-button>
|
||||||
|
<el-button v-if="item.status === 1" type="success" size="small" @click="handleApprove(item)">通过</el-button>
|
||||||
|
<el-button v-if="item.status === 1" type="danger" size="small" plain @click="handleReject(item)">驳回</el-button>
|
||||||
|
<el-button type="danger" size="small" plain @click="handleDelete(item)">删除</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="queryParams.page"
|
||||||
|
v-model:page-size="queryParams.count"
|
||||||
|
:page-sizes="[10, 20, 50]"
|
||||||
|
layout="total, sizes, prev, pager, next"
|
||||||
|
:total="total"
|
||||||
|
@size-change="fetchList"
|
||||||
|
@current-change="fetchList"
|
||||||
|
background
|
||||||
|
class="pagination"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 新增/编辑对话框 -->
|
||||||
|
<el-dialog v-model="dialogVisible" :title="dialogType === 'add' ? '新增签名' : '编辑签名'" width="560px" append-to-body destroy-on-close>
|
||||||
|
<el-form ref="formRef" :model="form" :rules="formRules" label-width="110px">
|
||||||
|
<el-form-item label="主控服务" prop="service_id">
|
||||||
|
<el-select v-model="form.service_id" placeholder="选择服务" style="width: 100%" :disabled="dialogType === 'edit'">
|
||||||
|
<el-option v-for="s in serviceOptions" :key="s.id" :label="s.name" :value="s.id" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="用户" prop="user_id">
|
||||||
|
<el-input
|
||||||
|
v-if="selectedUserInfo"
|
||||||
|
:model-value="`${selectedUserInfo.user_name} (ID: ${form.user_id})`"
|
||||||
|
readonly
|
||||||
|
>
|
||||||
|
<template #suffix>
|
||||||
|
<el-icon v-if="dialogType === 'add'" class="clear-icon" @click="clearUser"><Close /></el-icon>
|
||||||
|
</template>
|
||||||
|
<template #append>
|
||||||
|
<el-button @click="userSelectorVisible = true" :disabled="dialogType === 'edit'"><el-icon><User /></el-icon></el-button>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
<el-input v-else placeholder="请选择用户" readonly @click="openUserSelector">
|
||||||
|
<template #append>
|
||||||
|
<el-button @click="openUserSelector" :disabled="dialogType === 'edit'"><el-icon><User /></el-icon></el-button>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="签名标题" prop="title">
|
||||||
|
<el-input v-model="form.title" placeholder="如:某某公司" maxlength="30" show-word-limit />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="申请人姓名">
|
||||||
|
<el-input v-model="form.applicant_name" placeholder="申请人真实姓名" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="身份证号">
|
||||||
|
<el-input v-model="form.applicant_id_card" placeholder="申请人身份证号" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="公司名称">
|
||||||
|
<el-input v-model="form.applicant_company" placeholder="申请人公司(可选)" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="营业执照URL">
|
||||||
|
<el-input v-model="form.license_url" placeholder="营业执照图片地址(可选)" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="submitting" @click="handleFormSubmit">确定</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="confirmReject">确认驳回</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 表单用户选择器 -->
|
||||||
|
<UserListSelector
|
||||||
|
v-model="userSelectorVisible"
|
||||||
|
:current-user-id="form.user_id"
|
||||||
|
@confirm="handleUserSelect"
|
||||||
|
/>
|
||||||
|
<!-- 筛选用户选择器 -->
|
||||||
|
<UserListSelector
|
||||||
|
v-model="filterUserSelectorVisible"
|
||||||
|
:current-user-id="queryParams.user_id ? Number(queryParams.user_id) : undefined"
|
||||||
|
@confirm="handleFilterUserSelect"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { Plus, User, Close, Picture } from '@element-plus/icons-vue'
|
||||||
|
import { formatDate } from '@/utils/tool'
|
||||||
|
import {
|
||||||
|
getSmsServiceList,
|
||||||
|
getSmsSignatureList, createSmsSignature, updateSmsSignature, deleteSmsSignature,
|
||||||
|
submitSmsSignature, approveSmsSignature, rejectSmsSignature
|
||||||
|
} from '@/api/admin/smsService'
|
||||||
|
import UserListSelector from '@/components/admin/UserListSelector.vue'
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const submitting = ref(false)
|
||||||
|
const tableData = ref([])
|
||||||
|
const total = ref(0)
|
||||||
|
const serviceOptions = ref([])
|
||||||
|
|
||||||
|
const queryParams = reactive({ service_id: null, page: 1, count: 10, user_id: '', status: null })
|
||||||
|
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const dialogType = ref('add')
|
||||||
|
const formRef = ref(null)
|
||||||
|
const form = reactive({ service_id: null, user_id: null, title: '', applicant_name: '', applicant_id_card: '', applicant_company: '', license_url: '' })
|
||||||
|
const formRules = {
|
||||||
|
service_id: [{ required: true, message: '请选择服务', trigger: 'change' }],
|
||||||
|
user_id: [{ required: true, message: '请输入用户ID', trigger: 'blur' }],
|
||||||
|
title: [{ required: true, message: '请输入签名标题', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
const rejectDialogVisible = ref(false)
|
||||||
|
const rejectReason = ref('')
|
||||||
|
const currentRow = ref(null)
|
||||||
|
|
||||||
|
const userSelectorVisible = ref(false)
|
||||||
|
const selectedUserInfo = ref(null)
|
||||||
|
const filterUserSelectorVisible = ref(false)
|
||||||
|
const filterUserInfo = ref(null)
|
||||||
|
|
||||||
|
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 })
|
||||||
|
const body = res.data
|
||||||
|
if (body.code === 200) {
|
||||||
|
const list = body.data?.data || body.data || []
|
||||||
|
serviceOptions.value = Array.isArray(list) ? list : []
|
||||||
|
if (serviceOptions.value.length && !queryParams.service_id) {
|
||||||
|
queryParams.service_id = serviceOptions.value[0].id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) { console.error(e) }
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchList = async () => {
|
||||||
|
if (!queryParams.service_id) return
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const params = { service_id: queryParams.service_id, page: queryParams.page, count: queryParams.count }
|
||||||
|
if (queryParams.user_id) params.user_id = Number(queryParams.user_id)
|
||||||
|
if (queryParams.status !== null && queryParams.status !== '') params.status = queryParams.status
|
||||||
|
const res = await getSmsSignatureList(params)
|
||||||
|
if (res.data.code === 200) {
|
||||||
|
const d = res.data.data
|
||||||
|
tableData.value = d?.data || d || []
|
||||||
|
total.value = d?.all_count || 0
|
||||||
|
}
|
||||||
|
} catch (e) { ElMessage.error('获取签名列表失败') }
|
||||||
|
finally { loading.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFilterUserSelect = (user) => {
|
||||||
|
queryParams.user_id = String(user.user_id)
|
||||||
|
filterUserInfo.value = user
|
||||||
|
handleSearch()
|
||||||
|
}
|
||||||
|
const clearFilterUser = () => {
|
||||||
|
queryParams.user_id = ''
|
||||||
|
filterUserInfo.value = null
|
||||||
|
handleSearch()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSearch = () => { queryParams.page = 1; fetchList() }
|
||||||
|
const handleReset = () => { queryParams.user_id = ''; queryParams.status = null; filterUserInfo.value = null; queryParams.page = 1; fetchList() }
|
||||||
|
|
||||||
|
const openUserSelector = () => {
|
||||||
|
if (dialogType.value === 'edit') return
|
||||||
|
userSelectorVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUserSelect = (user) => {
|
||||||
|
form.user_id = user.user_id
|
||||||
|
selectedUserInfo.value = user
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearUser = () => {
|
||||||
|
form.user_id = null
|
||||||
|
selectedUserInfo.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAdd = () => {
|
||||||
|
dialogType.value = 'add'
|
||||||
|
selectedUserInfo.value = null
|
||||||
|
Object.assign(form, { service_id: queryParams.service_id, user_id: null, title: '', applicant_name: '', applicant_id_card: '', applicant_company: '', license_url: '' })
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEdit = (row) => {
|
||||||
|
dialogType.value = 'edit'
|
||||||
|
selectedUserInfo.value = { user_id: row.user_id, user_name: `用户${row.user_id}` }
|
||||||
|
Object.assign(form, {
|
||||||
|
service_id: queryParams.service_id,
|
||||||
|
signature_id: row.ID,
|
||||||
|
user_id: row.user_id,
|
||||||
|
title: row.title,
|
||||||
|
applicant_name: row.applicant_name || '',
|
||||||
|
applicant_id_card: row.applicant_id_card || '',
|
||||||
|
applicant_company: row.applicant_company || '',
|
||||||
|
license_url: row.license_url || ''
|
||||||
|
})
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFormSubmit = () => {
|
||||||
|
formRef.value?.validate(async (valid) => {
|
||||||
|
if (!valid) return
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
const data = { ...form }
|
||||||
|
let res
|
||||||
|
if (dialogType.value === 'add') {
|
||||||
|
res = await createSmsSignature(data)
|
||||||
|
} else {
|
||||||
|
res = await updateSmsSignature(data)
|
||||||
|
}
|
||||||
|
if (res.data.code === 200) {
|
||||||
|
ElMessage.success(dialogType.value === 'add' ? '创建成功' : '修改成功')
|
||||||
|
dialogVisible.value = false
|
||||||
|
fetchList()
|
||||||
|
} else { ElMessage.error(res.data.message || '操作失败') }
|
||||||
|
} catch (e) { ElMessage.error('操作失败') }
|
||||||
|
finally { submitting.value = false }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = (row) => {
|
||||||
|
ElMessageBox.confirm(`确认提交签名「${row.title}」进行审核?`, '提交审核', { type: 'info' }).then(async () => {
|
||||||
|
try {
|
||||||
|
const res = await submitSmsSignature({ service_id: queryParams.service_id, signature_id: row.ID })
|
||||||
|
if (res.data.code === 200) { ElMessage.success('已提交审核'); fetchList() }
|
||||||
|
else { ElMessage.error(res.data.message || '提交失败') }
|
||||||
|
} catch { ElMessage.error('提交失败') }
|
||||||
|
}).catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleApprove = (row) => {
|
||||||
|
ElMessageBox.confirm(`确认通过签名「${row.title}」?`, '审核通过', { type: 'success' }).then(async () => {
|
||||||
|
try {
|
||||||
|
const res = await approveSmsSignature({ service_id: queryParams.service_id, signature_id: row.ID })
|
||||||
|
if (res.data.code === 200) { ElMessage.success('已通过'); fetchList() }
|
||||||
|
else { ElMessage.error(res.data.message || '操作失败') }
|
||||||
|
} catch { ElMessage.error('操作失败') }
|
||||||
|
}).catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleReject = (row) => {
|
||||||
|
currentRow.value = row
|
||||||
|
rejectReason.value = ''
|
||||||
|
rejectDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmReject = async () => {
|
||||||
|
if (!rejectReason.value.trim()) { ElMessage.warning('请输入驳回原因'); return }
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
const res = await rejectSmsSignature({ service_id: queryParams.service_id, signature_id: currentRow.value.ID, reject_reason: rejectReason.value })
|
||||||
|
if (res.data.code === 200) { ElMessage.success('已驳回'); rejectDialogVisible.value = false; fetchList() }
|
||||||
|
else { ElMessage.error(res.data.message || '驳回失败') }
|
||||||
|
} catch { ElMessage.error('驳回失败') }
|
||||||
|
finally { submitting.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = (row) => {
|
||||||
|
ElMessageBox.confirm(`确认删除签名「${row.title}」?`, '删除', { type: 'warning' }).then(async () => {
|
||||||
|
try {
|
||||||
|
const res = await deleteSmsSignature({ service_id: queryParams.service_id, signature_id: row.ID })
|
||||||
|
if (res.data.code === 200) { ElMessage.success('删除成功'); fetchList() }
|
||||||
|
else { ElMessage.error(res.data.message || '删除失败') }
|
||||||
|
} catch { ElMessage.error('删除失败') }
|
||||||
|
}).catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await loadServices()
|
||||||
|
fetchList()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.sms-signature-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: #f0f9eb; 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; }
|
||||||
|
.header-actions { display: flex; gap: 10px; }
|
||||||
|
.filter-bar { display: flex; gap: 10px; margin-bottom: 16px; align-items: center; flex-wrap: wrap; }
|
||||||
|
.pagination { margin-top: 16px; justify-content: flex-end; }
|
||||||
|
.clear-icon { cursor: pointer; color: #909399; transition: color 0.2s; }
|
||||||
|
.clear-icon:hover { color: #f56c6c; }
|
||||||
|
|
||||||
|
/* 卡片列表 */
|
||||||
|
.signature-cards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(420px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 40px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sig-card {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e8e8e8;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 18px;
|
||||||
|
transition: all 0.25s;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.sig-card:hover {
|
||||||
|
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.08);
|
||||||
|
border-color: #d9d9d9;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
.sig-card.status-1 { border-left: 3px solid #e6a23c; }
|
||||||
|
.sig-card.status-2 { border-left: 3px solid #67c23a; }
|
||||||
|
.sig-card.status-3 { border-left: 3px solid #f56c6c; }
|
||||||
|
|
||||||
|
.sig-card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
.sig-card-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.sig-title-text {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
.sig-card-id {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #c0c4cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sig-card-body {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
flex: 1;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
.sig-card-info {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.sig-info-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.sig-label {
|
||||||
|
color: #909399;
|
||||||
|
min-width: 56px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.sig-value {
|
||||||
|
color: #303133;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.sig-id-card {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
.reject-row .sig-value {
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
.reject-text { color: #f56c6c; }
|
||||||
|
|
||||||
|
.sig-card-license {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 90px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.license-img {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.license-placeholder {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #f5f7fa;
|
||||||
|
border: 1px dashed #dcdfe6;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 4px;
|
||||||
|
color: #c0c4cc;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sig-card-footer {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px solid #f0f2f5;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,632 @@
|
|||||||
|
<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>
|
||||||
@@ -193,7 +193,10 @@
|
|||||||
</el-tabs>
|
</el-tabs>
|
||||||
|
|
||||||
<!-- ==================== 模板编辑弹窗 ==================== -->
|
<!-- ==================== 模板编辑弹窗 ==================== -->
|
||||||
<el-dialog v-model="tplDialogVisible" :title="tplForm.id ? '编辑模板' : '新增模板'" width="640px" append-to-body destroy-on-close>
|
<el-dialog v-model="tplDialogVisible" :title="tplForm.id ? '编辑模板' : '新增模板'" width="1060px" append-to-body destroy-on-close @opened="onTplDialogOpened">
|
||||||
|
<div class="tpl-dialog-body">
|
||||||
|
<!-- 左侧:表单 -->
|
||||||
|
<div class="tpl-dialog-left">
|
||||||
<el-form :model="tplForm" :rules="tplRules" ref="tplFormRef" label-width="90px" label-position="right">
|
<el-form :model="tplForm" :rules="tplRules" ref="tplFormRef" label-width="90px" label-position="right">
|
||||||
<el-form-item label="模板名称" prop="name">
|
<el-form-item label="模板名称" prop="name">
|
||||||
<el-input v-model="tplForm.name" placeholder="例:用户注册通知" maxlength="100" show-word-limit />
|
<el-input v-model="tplForm.name" placeholder="例:用户注册通知" maxlength="100" show-word-limit />
|
||||||
@@ -227,9 +230,38 @@
|
|||||||
<div class="form-hint">点击参数按钮可将其插入到模板内容光标处</div>
|
<div class="form-hint">点击参数按钮可将其插入到模板内容光标处</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="模板内容" prop="content">
|
<el-form-item label="模板内容" prop="content">
|
||||||
<el-input ref="contentInputRef" v-model="tplForm.content" type="textarea" :rows="6" placeholder="模板内容,点击上方参数按钮插入变量" />
|
<el-input ref="contentInputRef" v-model="tplForm.content" type="textarea" :rows="8" placeholder="模板内容,点击上方参数按钮插入变量" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧:渲染预览 -->
|
||||||
|
<div class="tpl-dialog-right">
|
||||||
|
<div class="preview-header">
|
||||||
|
<span class="preview-title">
|
||||||
|
<svg viewBox="0 0 24 24" width="16" height="16"><path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z" fill="currentColor"/></svg>
|
||||||
|
渲染预览
|
||||||
|
</span>
|
||||||
|
<el-button size="small" link type="primary" @click="fetchPreview" :loading="previewLoading">
|
||||||
|
<el-icon><Refresh /></el-icon> 刷新
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="preview-body" v-loading="previewLoading">
|
||||||
|
<template v-if="previewData.rendered">
|
||||||
|
<div class="preview-rendered" :class="{ 'html-mode': tplForm.type === 'email' }" v-html="previewRenderedHtml"></div>
|
||||||
|
</template>
|
||||||
|
<div v-else-if="previewError" class="preview-empty">
|
||||||
|
<el-icon :size="32" color="#f56c6c"><WarningFilled /></el-icon>
|
||||||
|
<p>{{ previewError }}</p>
|
||||||
|
</div>
|
||||||
|
<div v-else class="preview-empty">
|
||||||
|
<el-icon :size="32" color="#c0c4cc"><View /></el-icon>
|
||||||
|
<p>填写模板标识和类型后自动加载预览</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="tplDialogVisible = false">取消</el-button>
|
<el-button @click="tplDialogVisible = false">取消</el-button>
|
||||||
<el-button type="primary" :loading="tplSubmitting" @click="handleSubmitTpl">确定</el-button>
|
<el-button type="primary" :loading="tplSubmitting" @click="handleSubmitTpl">确定</el-button>
|
||||||
@@ -239,12 +271,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, nextTick } from 'vue'
|
import { ref, computed, onMounted, nextTick, watch } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { Plus } from '@element-plus/icons-vue'
|
import { Plus, Refresh, View, WarningFilled } from '@element-plus/icons-vue'
|
||||||
import {
|
import {
|
||||||
getNoticeChannelList, updateNoticeChannel,
|
getNoticeChannelList, updateNoticeChannel,
|
||||||
getNoticeTemplateList, addNoticeTemplate, updateNoticeTemplate, deleteNoticeTemplate
|
getNoticeTemplateList, addNoticeTemplate, updateNoticeTemplate, deleteNoticeTemplate,
|
||||||
|
previewNoticeTemplate
|
||||||
} from '@/api/admin/noticeChannel'
|
} from '@/api/admin/noticeChannel'
|
||||||
|
|
||||||
// ==================== 渠道配置 ====================
|
// ==================== 渠道配置 ====================
|
||||||
@@ -410,6 +443,70 @@ const handleDeleteTpl = async (row) => {
|
|||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 渲染预览 ====================
|
||||||
|
const previewLoading = ref(false)
|
||||||
|
const previewError = ref('')
|
||||||
|
const previewData = ref({ rendered: '', default_args: null })
|
||||||
|
|
||||||
|
const previewRenderedHtml = computed(() => {
|
||||||
|
const text = previewData.value.rendered || ''
|
||||||
|
if (tplForm.value.type === 'email') return text
|
||||||
|
return text.replace(/\n/g, '<br>')
|
||||||
|
})
|
||||||
|
|
||||||
|
let previewTimer = null
|
||||||
|
const debouncedFetchPreview = () => {
|
||||||
|
clearTimeout(previewTimer)
|
||||||
|
previewTimer = setTimeout(() => fetchPreview(), 600)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchPreview = async () => {
|
||||||
|
const tag = tplForm.value.tag?.trim()
|
||||||
|
const type = tplForm.value.type
|
||||||
|
const content = tplForm.value.content?.trim()
|
||||||
|
|
||||||
|
if (!content && (!tag || !type)) {
|
||||||
|
previewData.value = { rendered: '', default_args: null }
|
||||||
|
previewError.value = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
previewLoading.value = true
|
||||||
|
previewError.value = ''
|
||||||
|
try {
|
||||||
|
const params = {}
|
||||||
|
if (content) {
|
||||||
|
params.content = content
|
||||||
|
} else {
|
||||||
|
params.tag = tag
|
||||||
|
params.type = type
|
||||||
|
}
|
||||||
|
const res = await previewNoticeTemplate(params)
|
||||||
|
const body = res?.data
|
||||||
|
if (body?.code === 200 && body.data) {
|
||||||
|
previewData.value = body.data
|
||||||
|
} else {
|
||||||
|
previewData.value = { rendered: '', default_args: null }
|
||||||
|
previewError.value = body?.message || '渲染失败'
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
previewData.value = { rendered: '', default_args: null }
|
||||||
|
previewError.value = '请求失败'
|
||||||
|
} finally {
|
||||||
|
previewLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onTplDialogOpened = () => {
|
||||||
|
if (tplForm.value.tag && tplForm.value.type) {
|
||||||
|
fetchPreview()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => tplForm.value.tag, debouncedFetchPreview)
|
||||||
|
watch(() => tplForm.value.type, debouncedFetchPreview)
|
||||||
|
watch(() => tplForm.value.content, debouncedFetchPreview)
|
||||||
|
|
||||||
// ==================== 工具函数 ====================
|
// ==================== 工具函数 ====================
|
||||||
const formatTime = (t) => {
|
const formatTime = (t) => {
|
||||||
if (!t) return '-'
|
if (!t) return '-'
|
||||||
@@ -495,6 +592,129 @@ onMounted(() => {
|
|||||||
.form-hint { margin-top: 6px; font-size: 12px; color: #a8abb2; line-height: 1.4; }
|
.form-hint { margin-top: 6px; font-size: 12px; color: #a8abb2; line-height: 1.4; }
|
||||||
.form-hint code { background: #f5f7fa; padding: 1px 5px; border-radius: 3px; font-size: 11px; color: #606266; }
|
.form-hint code { background: #f5f7fa; padding: 1px 5px; border-radius: 3px; font-size: 11px; color: #606266; }
|
||||||
|
|
||||||
/* ===== 弹窗 ===== */
|
/* ===== 弹窗双栏布局 ===== */
|
||||||
.radio-with-icon { display: inline-flex; align-items: center; gap: 4px; }
|
.radio-with-icon { display: inline-flex; align-items: center; gap: 4px; }
|
||||||
|
|
||||||
|
.tpl-dialog-body {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
min-height: 380px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tpl-dialog-left {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tpl-dialog-right {
|
||||||
|
width: 380px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #fafbfc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 14px;
|
||||||
|
background: #f5f7fa;
|
||||||
|
border-bottom: 1px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-title {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-body {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-rendered {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: #303133;
|
||||||
|
word-break: break-all;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 12px;
|
||||||
|
max-height: 320px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-rendered.html-mode {
|
||||||
|
white-space: normal;
|
||||||
|
transform: scale(0.75);
|
||||||
|
transform-origin: top left;
|
||||||
|
width: 133.33%;
|
||||||
|
max-height: 426px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-args {
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-args-title {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #606266;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-args-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-arg-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-arg-key {
|
||||||
|
background: #ecf5ff;
|
||||||
|
color: #409eff;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-family: 'Consolas','Monaco',monospace;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-arg-val {
|
||||||
|
color: #606266;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-empty {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 200px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-empty p {
|
||||||
|
margin: 10px 0 0;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+1649
-15
File diff suppressed because it is too large
Load Diff
@@ -36,6 +36,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="overview-actions">
|
<div class="overview-actions">
|
||||||
<el-button v-if="isVmGoods" size="small" type="primary" @click="handleVnc">VNC</el-button>
|
<el-button v-if="isVmGoods" size="small" type="primary" @click="handleVnc">VNC</el-button>
|
||||||
|
<el-button v-if="isVmGoods" size="small" type="success" @click="handleSshConnect" :disabled="!vmPublicIpList.length || isWindows">SSH连接</el-button>
|
||||||
<el-button v-if="isVmGoods" size="small" type="success" @click="handlePower('start')" :disabled="vm?.status === 'running'">启动</el-button>
|
<el-button v-if="isVmGoods" size="small" type="success" @click="handlePower('start')" :disabled="vm?.status === 'running'">启动</el-button>
|
||||||
<el-button v-if="isVmGoods" size="small" type="warning" @click="handlePower('reboot')">重启</el-button>
|
<el-button v-if="isVmGoods" size="small" type="warning" @click="handlePower('reboot')">重启</el-button>
|
||||||
<el-button v-if="isVmGoods" size="small" type="danger" @click="handlePower('stop')" :disabled="vm?.status === 'stopped' || vm?.status === 'stop'">关机</el-button>
|
<el-button v-if="isVmGoods" size="small" type="danger" @click="handlePower('stop')" :disabled="vm?.status === 'stopped' || vm?.status === 'stop'">关机</el-button>
|
||||||
@@ -1407,6 +1408,15 @@ const loadSgLockInfo = async () => {
|
|||||||
const vncVisible = ref(false)
|
const vncVisible = ref(false)
|
||||||
const vncLoading = ref(false)
|
const vncLoading = ref(false)
|
||||||
const vncResult = ref(null)
|
const vncResult = ref(null)
|
||||||
|
const handleSshConnect = () => {
|
||||||
|
const host = vmPublicIpList.value[0]
|
||||||
|
if (!host) return ElMessage.warning('无可用公网 IP')
|
||||||
|
const username = isWindows.value ? 'Administrator' : 'root'
|
||||||
|
const password = vm.value?.root_password || ''
|
||||||
|
const encodedPwd = btoa(password)
|
||||||
|
window.open(`https://webssh1.007yjs.com/?hostname=${encodeURIComponent(host)}&username=${encodeURIComponent(username)}&password=${encodedPwd}`, '_blank')
|
||||||
|
}
|
||||||
|
|
||||||
const handleVnc = async () => {
|
const handleVnc = async () => {
|
||||||
vncVisible.value = true; vncLoading.value = true; vncResult.value = null
|
vncVisible.value = true; vncLoading.value = true; vncResult.value = null
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="overview-actions">
|
<div class="overview-actions">
|
||||||
<el-button type="primary" @click="handleGetVnc">远程连接</el-button>
|
<el-button type="primary" @click="handleGetVnc">远程连接</el-button>
|
||||||
|
<el-button type="success" @click="handleSshConnect" :disabled="!publicIpList.length || isWindows">SSH连接</el-button>
|
||||||
<el-button type="danger" @click="handlePower('stop')" :disabled="detail.status === 'stopped' || detail.status === 'stop' || isMigrating">关机</el-button>
|
<el-button type="danger" @click="handlePower('stop')" :disabled="detail.status === 'stopped' || detail.status === 'stop' || isMigrating">关机</el-button>
|
||||||
<el-dropdown trigger="click" @command="handleMoreCommand">
|
<el-dropdown trigger="click" @command="handleMoreCommand">
|
||||||
<el-button>更多 <el-icon class="el-icon--right"><ArrowDown /></el-icon></el-button>
|
<el-button>更多 <el-icon class="el-icon--right"><ArrowDown /></el-icon></el-button>
|
||||||
@@ -2783,6 +2784,15 @@ const loadVncNodes = async () => {
|
|||||||
} catch { /* */ }
|
} catch { /* */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleSshConnect = () => {
|
||||||
|
const host = publicIpList.value[0]
|
||||||
|
if (!host) return ElMessage.warning('无可用公网 IP')
|
||||||
|
const username = isWindows.value ? 'Administrator' : 'root'
|
||||||
|
const password = detail.value?.root_password || ''
|
||||||
|
const encodedPwd = btoa(password)
|
||||||
|
window.open(`https://webssh1.007yjs.com/?hostname=${encodeURIComponent(host)}&username=${encodeURIComponent(username)}&password=${encodedPwd}`, '_blank')
|
||||||
|
}
|
||||||
|
|
||||||
const handleGetVnc = async () => {
|
const handleGetVnc = async () => {
|
||||||
vncNodeId.value = null
|
vncNodeId.value = null
|
||||||
vncResult.value = null
|
vncResult.value = null
|
||||||
|
|||||||
Reference in New Issue
Block a user