短信统计页面

This commit is contained in:
qiuyuan 2025-12-18 15:42:09 +08:00
parent 32693979d5
commit aa8e0f0dd4
7 changed files with 436 additions and 3 deletions

View File

@ -0,0 +1,14 @@
/**
* 短信模块接口
*/
import request from '@/utils/request'
// 获取项目列表
export const getSmsLogsList = (params) => request.basic.get('/api/v1/sms_logs', params)
// 获取单挑数据
export const getItem = (id) => request.basic.get(`/api/v1/balances/${id}`)
// 添加条目
export const createProject = (params) => request.basic.post('/api/v1/balances', params)
// 更新role
export const updateItem = (id, params) => request.basic.put(`/api/v1/balances/${id}`, params)
// 删除数据
export const delItem = (id) => request.basic.delete(`/api/v1/balances/${id}`)

View File

@ -110,4 +110,7 @@ export default {
earnPointLog:'赚海贝记录', earnPointLog:'赚海贝记录',
earnPointRule:'赚海贝规则', earnPointRule:'赚海贝规则',
houseList:'购房记录', houseList:'购房记录',
message:'短信管理',
messageLog:'短信记录',
} }

View File

@ -21,6 +21,7 @@ import signIn from './signIn'
import lottery from './lottery' import lottery from './lottery'
import equiteMgt from './equiteMgt' import equiteMgt from './equiteMgt'
import earnPoint from './earnPoint' import earnPoint from './earnPoint'
import message from './message'
export default [ export default [
...home, ...home,
@ -46,4 +47,5 @@ export default [
...equiteMgt, ...equiteMgt,
...signIn, ...signIn,
...earnPoint, ...earnPoint,
...message,
] ]

View File

@ -0,0 +1,40 @@
import { DollarOutlined } from '@ant-design/icons-vue'
export default [
{
path: 'message',
name: 'message',
component: 'RouteViewLayout',
meta: {
icon: DollarOutlined,
title: '短信管理',
isMenu: true,
keepAlive: true,
permission: '*',
},
children: [
{
path: 'messageLog',
name: 'messageLog',
component: 'message/messageLog/index.vue',
meta: {
title: '短信统计',
isMenu: true,
keepAlive: true,
permission: '*',
},
},
{
path: 'signInLog',
name: 'signInLog',
component: 'signInModule/signInLog/index.vue',
meta: {
title: '核销记录',
isMenu: true,
keepAlive: true,
permission: '*',
},
},
],
},
]

View File

@ -0,0 +1,151 @@
<template>
<a-modal :open="modal.open" :title="modal.title" :width="640" :confirm-loading="modal.confirmLoading"
:after-close="onAfterClose" :cancel-text="cancelText" :ok-text="okText" @ok="handleOk" @cancel="handleCancel">
<a-spin :spinning="spining">
<a-form ref="formRef" :model="formData" :rules="formRules">
<a-card class="mb-8-2">
<a-col :span="24">
<a-form-item label="所属区域" name="areaId">
<a-select v-model:value="formData.areaId" allowClear>
<a-select-option :value="1">南通</a-select-option>
<a-select-option :value="2">盐城</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item :label="'导入文件'" name="fileList">
<gx-upload v-model="formData.fileList" accept=".xlsx,.xls"
:customUploadHandler="uploadExcelFile" @uploadSuccess="uploadSuccess" />
</a-form-item>
</a-col>
</a-card>
</a-form>
</a-spin>
</a-modal>
</template>
<script setup>
import { ref, onBeforeMount } from 'vue'
import { config } from '@/config'
import apis from '@/apis'
import { useForm, useModal, useSpining } from '@/hooks'
import { message } from 'ant-design-vue'
import { useI18n } from 'vue-i18n'
import dayjs from 'dayjs'
import GxUpload from '@/components/GxUpload/index.vue'
const emit = defineEmits(['ok'])
const { t } = useI18n() // t
const { modal, showModal, hideModal, showLoading, hideLoading } = useModal()
const { formData, formRef, formRules, resetForm } = useForm()
const { spining, showSpining, hideSpining } = useSpining()
const cancelText = ref(t('button.cancel'))
const okText = ref(t('button.confirm'))
const fileList = ref([])
onBeforeMount(() => {
formData.value.areaId = ''
})
/**
* 新建
*/
function handleCreate() {
showModal({
type: 'create',
title: '导入文件',
})
// initData()
// formData.value.status = 'enabled'
}
/**
* 编辑
*/
async function handleEdit(record = {}) {
showModal({
type: 'edit',
title: '编辑客户',
})
try {
showSpining()
const { data, success } = await apis.customer.getItem(record.id).catch()
if (!success) {
hideModal()
return
}
hideSpining()
formData.value = { ...data }
formData.value.birthday = dayjs(data.birthday)
if (data.avatar) {
formData.value.fileList = [config('http.apiBasic') + data.avatar]
}
} catch (error) {
message.error({ content: error.message })
hideSpining()
}
}
const uploadSuccess = (data) => {
fileList.value.push(data)
}
/**
* 确定
*/
function handleOk() {
hideModal()
formData.value.areaId = null
fileList.value = []
}
// Excel
const uploadExcelFile = async (file) => {
const areaId = formData.value.areaId
if (!areaId) {
message.error('请先选择所属区域')
throw new Error('缺少 areaId')
}
const formDataUpload = new FormData()
formDataUpload.append('file', file)
//
const res = await apis.customer.pushFile(areaId, formDataUpload)
if (!res.success) {
// message.error('Excel ')
throw new Error(res.message)
}
message.success('Excel 上传成功')
emit('ok')
hideModal()
formData.value.areaId = null
fileList.value = []
return res.data
}
/**
* 取消
*/
function handleCancel() {
formData.value.areaId = null
hideModal()
}
/**
* 关闭后
*/
function onAfterClose() {
resetForm()
hideLoading()
}
defineExpose({
handleCreate,
handleEdit,
})
</script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,225 @@
<template>
<x-search-bar class="mb-8-2">
<template #default="{ gutter, colSpan }">
<!-- 统一设置 label 宽度 -->
<a-form :model="searchFormData" layout="inline" :label-col="{ style: { width: '80px' } }"
:wrapper-col="{ style: { flex: 1 } }">
<a-row :gutter="gutter">
<a-col v-bind="colSpan">
<a-form-item label="用户姓名" name="customerName">
<a-input placeholder="请输入客户姓名" v-model:value="searchFormData.customerName" />
</a-form-item>
</a-col>
<a-col v-bind="colSpan">
<a-form-item label="联系方式" name="phone">
<a-input placeholder="请输入" v-model:value="searchFormData.phone" />
</a-form-item>
</a-col>
<a-col v-bind="colSpan">
<a-form-item label="短信类型" name="smsType">
<a-select v-model:value="searchFormData.smsType" allowClear>
<a-select-option value="ACTIVITY">活动推广</a-select-option>
<a-select-option value="LV_UP">等级通知</a-select-option>
<a-select-option value="FIRST_REGISTER">首次注册</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col v-bind="colSpan">
<a-form-item label="发送时间" name="sendAtRange">
<a-range-picker v-model:value="searchFormData.sendAtRange" style="width: 100%;"
:show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:placeholder="['开始时间', '结束时间']" @change="handleDateRangeChange" />
</a-form-item>
</a-col>
<a-col class="align-right" v-bind="colSpan">
<a-space>
<a-button @click="handleResetSearch">{{ $t('button.reset') }}</a-button>
<a-button ghost type="primary" @click="handleSearch">
{{ $t('button.search') }}
</a-button>
</a-space>
</a-col>
</a-row>
</a-form>
</template>
</x-search-bar>
<a-row :gutter="8" :wrap="false">
<a-col flex="auto">
<a-card type="flex">
<a-table :columns="columns" :data-source="listData" bordered="true" :loading="loading"
:pagination="paginationState" :scroll="{ x: 'max-content' }" @change="onTableChange">
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'smsType'">
<span v-if="record.smsType == 'ACTIVITY'">{{ '活动推广' }}</span>
<span v-else-if="record.smsType == 'LV_UP'">{{ '等级通知' }}</span>
<span v-else-if="record.smsType == 'FIRST_REGISTER'">{{ '首次注册' }}</span>
</template>
<template v-if="column.dataIndex === 'sendAt'">
<span>{{ dayjs(record.sendAt).format('YYYY-MM-DD HH:mm:ss') }}</span>
</template>
<template v-if="column.dataIndex === 'status'">
<span>{{ record.status == 'Success' ? '成功' : '失败' }}</span>
</template>
<template v-if="'action' === column.key">
<!-- <x-action-button @click="$refs.editDialogRef.handleEdit(record)">
<a-tooltip>
<template #title> {{ $t('pages.system.user.edit') }}</template>
<edit-outlined /> </a-tooltip></x-action-button> -->
<x-action-button @click="handleDelete(record)">
<a-tooltip>
<template #title>{{ $t('pages.system.delete') }}</template>
<delete-outlined style="color: #ff4d4f" /> </a-tooltip></x-action-button>
</template>
</template>
</a-table>
</a-card>
</a-col>
</a-row>
<!-- <edit-dialog ref="editDialogRef" @ok="onOk"></edit-dialog> -->
</template>
<script setup>
import { message, Modal } from 'ant-design-vue'
import { reactive, ref } from 'vue'
import apis from '@/apis'
import { config } from '@/config'
import dayjs from 'dayjs'
import { usePagination } from '@/hooks'
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons-vue'
import { useI18n } from 'vue-i18n'
defineOptions({
name: 'owner',
})
const { t } = useI18n()
//
const columns = [
{ title: '用户姓名', dataIndex: 'customerName', width: 120, align: 'center' },
{ title: '用户联系方式', dataIndex: 'phone', width: 120, align: 'center' },
{ title: '信息内容', dataIndex: 'SMSContent', width: 300, align: 'center' },
{ title: '信息类型', dataIndex: 'smsType', width: 120, align: 'center' },
{ title: '发送状态', dataIndex: 'status', width: 120 },
{ title: '发送时间', dataIndex: 'sendAt', width: 120 },
]
// 使 usePagination 使 searchFormData
const { listData, loading, showLoading, hideLoading, paginationState, resetPagination } = usePagination()
//
const searchFormData = reactive({
customerName: '',
phone: '',
smsType: undefined,
sendAtRange: [], // [dayjs, dayjs] []
})
/**
* 获取表格数据
*/
async function getPageList() {
try {
showLoading()
const { pageSize, current } = paginationState
const params = {
pageSize,
current,
customerName: searchFormData.customerName || undefined,
phone: searchFormData.phone || undefined,
smsType: searchFormData.smsType || undefined,
}
// created_at
if (searchFormData.sendAtRange?.length === 2) {
const [start, end] = searchFormData.sendAtRange
params.send_start = start.format('YYYY-MM-DD HH:mm:ss')
params.send_at = end.format('YYYY-MM-DD HH:mm:ss') // send_at
} else {
params.send_start = undefined
params.send_at = undefined
}
const { success, data, total } = await apis.message.getSmsLogsList(params)
hideLoading()
if (config('http.code.success') === success) {
listData.value = data
paginationState.total = total
}
} catch (error) {
hideLoading()
console.error('获取短信日志失败:', error)
}
}
/**
* 删除记录
*/
function handleDelete({ id }) {
Modal.confirm({
title: t('pages.system.user.delTip'),
content: t('button.confirm'),
okText: t('button.confirm'),
onOk: async () => {
try {
const { success } = await apis.customer.delHouseOwner(id)
if (success === config('http.code.success')) {
message.success('删除成功')
await getPageList()
} else {
message.error('删除失败')
}
} catch (error) {
message.error('删除失败')
throw error
}
},
})
}
/**
* 分页变化
*/
function onTableChange(pagination) {
paginationState.current = pagination.current
paginationState.pageSize = pagination.pageSize
getPageList()
}
/**
* 搜索
*/
function handleSearch() {
resetPagination()
getPageList()
}
/**
* 重置搜索
*/
function handleResetSearch() {
//
Object.assign(searchFormData, {
customerName: '',
phone: '',
smsType: undefined,
sendAtRange: [],
})
resetPagination()
getPageList()
}
/**
* 日期范围变化
*/
const handleDateRangeChange = (dates) => {
// dates [dayjs, dayjs] null
searchFormData.sendAtRange = dates || []
}
//
getPageList()
</script>

View File

@ -54,13 +54,11 @@
<a-table :columns="columns" :data-source="listData" bordered="true" :loading="loading" <a-table :columns="columns" :data-source="listData" bordered="true" :loading="loading"
:pagination="paginationState" :scroll="{ x: 'max-content' }" @change="onTableChange"> :pagination="paginationState" :scroll="{ x: 'max-content' }" @change="onTableChange">
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'areaId'"> <template v-if="column.dataIndex === 'areaId'">
<span>{{ record.areaId === 1 ? '南通' : '盐城' }}</span> <span>{{ record.areaId === 1 ? '南通' : '盐城' }}</span>
</template> </template>
<template v-if="column.dataIndex === 'status'"> <template v-if="column.dataIndex === 'status'">
<span>{{ record.status == "uncheck" ? '未核' : '已核' }}</span> <span>{{ record.status == "uncheck" ? '未' : '已' }}</span>
</template> </template>
<template v-if="'action' === column.key"> <template v-if="'action' === column.key">
<!-- <x-action-button @click="$refs.editDialogRef.handleEdit(record)"> <!-- <x-action-button @click="$refs.editDialogRef.handleEdit(record)">