循环发送页面完善 接口调试

This commit is contained in:
BianLzhaoMin 2026-01-27 14:46:33 +08:00
parent d8cccea4c5
commit 5ca1288587
8 changed files with 1517 additions and 0 deletions

View File

@ -36,3 +36,11 @@ export function delGroupAPI(data) {
})
}
// 获取所有分组
export function getAllGroupAPI() {
return request({
url: '/group/getGroupSelect',
method: 'GET',
})
}

View File

@ -0,0 +1,58 @@
import request from '@/utils/request'
// 循环发送短信 - 查询列表
export function listLoopSendAPI(query) {
return request({
url: '/msgJob/list',
method: 'GET',
params: query,
})
}
// 循环发送短信 - 新增
export function addLoopSendAPI(data) {
return request({
url: '/msgJob/add',
method: 'POST',
data,
})
}
// 循环发送短信 - 修改
export function updateLoopSendAPI(data) {
return request({
url: '/msgJob/update',
method: 'POST',
data,
})
}
// 循环发送短信 - 删除
export function delLoopSendAPI(data) {
return request({
url: '/msgJob/delete',
method: 'POST',
data,
})
}
// 循环发送短信 - 详情
export function getLoopSendDetailAPI(id) {
return request({
url: `/msgJob/getById`,
method: 'POST',
data: {
id,
},
})
}
// 循环发送短信 - 更新任务状态
export function updateLoopSendStatusAPI(data) {
return request({
url: '/msgJob/updateLoopTaskStatus',
method: 'POST',
data,
})
}

View File

@ -0,0 +1,610 @@
<template>
<div class="person-picker">
<el-row :gutter="20" style="padding-bottom: 30px">
<el-col :span="16">
<!-- Tabs 切换 -->
<el-tabs v-model="activeTab" @tab-change="handleTabChange">
<el-tab-pane label="人员" name="person">
<template #label>
<span>人员</span>
<el-badge
v-if="selectedPersonCount > 0"
:value="selectedPersonCount"
class="tab-badge"
/>
</template>
</el-tab-pane>
<el-tab-pane label="分组" name="group">
<template #label>
<span>分组</span>
<el-badge
v-if="selectedGroupCount > 0"
:value="selectedGroupCount"
class="tab-badge"
/>
</template>
</el-tab-pane>
</el-tabs>
<!-- 搜索栏 -->
<div class="search-bar">
<el-input
v-if="activeTab === 'person'"
v-model.trim="searchKeyword"
placeholder="输入关键字"
clearable
prefix-icon="Search"
@input="handleSearch"
/>
<el-input
v-else
v-model.trim="groupSearchKeyword"
placeholder="输入关键字"
clearable
prefix-icon="Search"
@input="handleGroupSearch"
/>
</div>
<!-- 人员表格 -->
<el-table
v-if="activeTab === 'person'"
border
ref="personTableRef"
:data="filteredPersonList"
height="400"
@selection-change="onPersonSelectionChange"
>
<el-table-column type="selection" width="50" align="center" />
<el-table-column
show-overflow-tooltip
prop="orgName"
label="人员所属"
align="center"
/>
<el-table-column
show-overflow-tooltip
prop="workerName"
label="姓名"
width="120"
align="center"
/>
<el-table-column show-overflow-tooltip prop="sex" label="性别" align="center">
<template #default="{ row }">
{{ row.sex == 1 ? '男' : '女' }}
</template>
</el-table-column>
<el-table-column
show-overflow-tooltip
prop="phone"
label="电话"
align="center"
/>
</el-table>
<!-- 分组表格 -->
<el-table
v-else
border
ref="groupTableRef"
:data="filteredGroupList"
height="400"
@selection-change="onGroupSelectionChange"
>
<el-table-column type="selection" width="50" align="center" />
<el-table-column
show-overflow-tooltip
prop="groupName"
label="分组名称"
align="center"
/>
</el-table>
</el-col>
<!-- 右侧已选人员 -->
<el-col :span="8">
<div class="selected-persons">
<div class="selected-header">
<span>已选人员</span>
<el-button
type="text"
size="small"
@click="handleClearAll"
:disabled="allSelectedPersons.length === 0"
>
清空
</el-button>
</div>
<div class="selected-list">
<div
v-for="person in allSelectedPersons"
:key="person.id"
class="selected-item"
>
<span>{{ person.workerName }}</span>
<el-icon class="remove-icon" @click="handleRemovePerson(person.id)">
<Close />
</el-icon>
</div>
<div v-if="allSelectedPersons.length === 0" class="empty-tip">
暂未选择人员
</div>
</div>
</div>
</el-col>
</el-row>
</div>
</template>
<script setup name="PersonPicker">
import { ref, computed, watch, nextTick, onMounted } from 'vue'
import { Close } from '@element-plus/icons-vue'
import { getAllPersonAPI } from '@/api/personManage/person.js'
import { getAllGroupAPI } from '@/api/basicManage/groupManage.js'
const props = defineProps({
modelValue: {
type: Array,
default: () => [],
},
})
const emit = defineEmits(['update:modelValue', 'confirm'])
const activeTab = ref('person')
const searchKeyword = ref('')
const groupSearchKeyword = ref('')
const personTableRef = ref(null)
const groupTableRef = ref(null)
//
const personList = ref([])
const groupList = ref([])
// IDtab
const directSelectedPersonIds = ref([])
// ID
const selectedGroupIds = ref([])
//
const isSyncing = ref(false)
//
const filteredPersonList = computed(() => {
if (!searchKeyword.value) {
return personList.value
}
const keyword = searchKeyword.value.toLowerCase()
return personList.value.filter((item) => {
const orgName = (item.orgName || '').toLowerCase()
const workerName = (item.workerName || '').toLowerCase()
return orgName.includes(keyword) || workerName.includes(keyword)
})
})
//
const filteredGroupList = computed(() => {
if (!groupSearchKeyword.value) {
return groupList.value
}
const keyword = groupSearchKeyword.value.toLowerCase()
return groupList.value.filter((item) => {
const groupName = (item.groupName || '').toLowerCase()
return groupName.includes(keyword)
})
})
//
const selectedGroupCount = computed(() => selectedGroupIds.value.length)
//
const allSelectedPersons = computed(() => {
const personMap = new Map()
//
directSelectedPersonIds.value.forEach((personId) => {
const person = personList.value.find((p) => p.id === personId)
if (person) {
personMap.set(person.id, {
id: person.id,
workerName: person.workerName,
orgName: person.orgName,
sex: person.sex,
phone: person.phone,
})
}
})
//
selectedGroupIds.value.forEach((groupId) => {
const group = groupList.value.find((g) => g.id === groupId)
if (group && group.workerList && Array.isArray(group.workerList)) {
group.workerList.forEach((worker) => {
if (worker && worker.id) {
//
if (!personMap.has(worker.id)) {
personMap.set(worker.id, {
id: worker.id,
workerName: worker.workerName || worker.name || '',
orgName: worker.orgName || '',
sex: worker.sex,
phone: worker.phone || '',
})
}
}
})
}
})
return Array.from(personMap.values())
})
//
const selectedPersonCount = computed(() => {
return directSelectedPersonIds.value.length
})
// Tab
const handleTabChange = (tabName) => {
searchKeyword.value = ''
groupSearchKeyword.value = ''
nextTick(() => {
if (tabName === 'person') {
syncPersonTableSelection()
} else {
syncGroupTableSelection()
}
})
}
//
const handleSearch = () => {
nextTick(() => {
syncPersonTableSelection()
})
}
const handleGroupSearch = () => {
nextTick(() => {
syncGroupTableSelection()
})
}
//
const onPersonSelectionChange = (selection) => {
if (isSyncing.value) {
return
}
// ID
directSelectedPersonIds.value = selection.map((item) => item.id)
//
updateSelectedPersons()
}
//
const updateSelectedPersons = () => {
const personMap = new Map()
//
directSelectedPersonIds.value.forEach((personId) => {
const person = personList.value.find((p) => p.id === personId)
if (person) {
personMap.set(person.id, {
id: person.id,
workerName: person.workerName,
orgName: person.orgName,
sex: person.sex,
phone: person.phone,
})
}
})
//
selectedGroupIds.value.forEach((groupId) => {
const group = groupList.value.find((g) => g.id === groupId)
if (group && group.workerList && Array.isArray(group.workerList)) {
group.workerList.forEach((worker) => {
if (worker && worker.id) {
//
if (!personMap.has(worker.id)) {
personMap.set(worker.id, {
id: worker.id,
workerName: worker.workerName || worker.name || '',
orgName: worker.orgName || '',
sex: worker.sex,
phone: worker.phone || '',
})
}
}
})
}
})
//
emit('update:modelValue', Array.from(personMap.values()))
}
//
const onGroupSelectionChange = (selection) => {
if (isSyncing.value) {
return
}
selectedGroupIds.value = selection.map((group) => group.id)
//
updateSelectedPersons()
}
//
const syncPersonTableSelection = () => {
if (!personTableRef.value || directSelectedPersonIds.value.length === 0) {
if (personTableRef.value) {
isSyncing.value = true
personTableRef.value.clearSelection()
nextTick(() => {
isSyncing.value = false
})
}
return
}
isSyncing.value = true
const selectedIds = new Set(directSelectedPersonIds.value.map((id) => Number(id)))
nextTick(() => {
personTableRef.value.clearSelection()
filteredPersonList.value.forEach((row) => {
const rowId = Number(row.id)
if (selectedIds.has(rowId)) {
personTableRef.value.toggleRowSelection(row, true)
}
})
nextTick(() => {
isSyncing.value = false
})
})
}
//
const syncGroupTableSelection = () => {
if (!groupTableRef.value || selectedGroupIds.value.length === 0) {
if (groupTableRef.value) {
isSyncing.value = true
groupTableRef.value.clearSelection()
nextTick(() => {
isSyncing.value = false
})
}
return
}
isSyncing.value = true
const selectedIds = new Set(selectedGroupIds.value.map((id) => Number(id)))
nextTick(() => {
groupTableRef.value.clearSelection()
filteredGroupList.value.forEach((row) => {
const rowId = Number(row.id)
if (selectedIds.has(rowId)) {
groupTableRef.value.toggleRowSelection(row, true)
}
})
nextTick(() => {
isSyncing.value = false
})
})
}
//
const handleRemovePerson = (personId) => {
//
const groupsToRemove = []
selectedGroupIds.value.forEach((groupId) => {
const group = groupList.value.find((g) => g.id === groupId)
if (group && group.workerList && Array.isArray(group.workerList)) {
const hasPerson = group.workerList.some((w) => w.id === personId)
if (hasPerson) {
groupsToRemove.push(groupId)
}
}
})
if (groupsToRemove.length > 0) {
//
selectedGroupIds.value = selectedGroupIds.value.filter((id) => !groupsToRemove.includes(id))
updateSelectedPersons()
nextTick(() => {
syncGroupTableSelection()
})
} else {
//
directSelectedPersonIds.value = directSelectedPersonIds.value.filter(
(id) => id !== personId,
)
updateSelectedPersons()
}
//
nextTick(() => {
syncPersonTableSelection()
})
}
//
const handleClearAll = () => {
isSyncing.value = true
directSelectedPersonIds.value = []
selectedGroupIds.value = []
emit('update:modelValue', [])
if (personTableRef.value) {
personTableRef.value.clearSelection()
}
if (groupTableRef.value) {
groupTableRef.value.clearSelection()
}
nextTick(() => {
isSyncing.value = false
})
}
//
const getPersonList = async () => {
try {
const result = await getAllPersonAPI()
if (result.code === 200 && result.rows) {
personList.value = result.rows || []
nextTick(() => {
syncPersonTableSelection()
})
}
} catch (error) {
console.error('获取人员列表失败:', error)
}
}
//
const getGroupList = async () => {
try {
const result = await getAllGroupAPI()
if (result.code === 200 && result.rows) {
groupList.value = result.rows || []
nextTick(() => {
syncGroupTableSelection()
})
}
} catch (error) {
console.error('获取分组列表失败:', error)
}
}
// modelValue
watch(
() => props.modelValue,
(newVal) => {
if (!newVal || newVal.length === 0) {
directSelectedPersonIds.value = []
return
}
//
const directSelectedIds = []
const groupPersonIds = new Set()
// ID
selectedGroupIds.value.forEach((groupId) => {
const group = groupList.value.find((g) => g.id === groupId)
if (group && group.workerList && Array.isArray(group.workerList)) {
group.workerList.forEach((worker) => {
if (worker && worker.id) {
groupPersonIds.add(worker.id)
}
})
}
})
//
newVal.forEach((person) => {
if (person && person.id && !groupPersonIds.has(person.id)) {
directSelectedIds.push(person.id)
}
})
directSelectedPersonIds.value = directSelectedIds
if (activeTab.value === 'person') {
syncPersonTableSelection()
}
},
{ deep: true, immediate: true },
)
onMounted(() => {
getPersonList()
getGroupList()
})
</script>
<style lang="scss" scoped>
.person-picker {
.search-bar {
margin-bottom: 10px;
}
.selected-persons {
border: 1px solid #dcdfe6;
border-radius: 4px;
background: #fff;
height: 100%;
display: flex;
flex-direction: column;
.selected-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 15px;
border-bottom: 1px solid #dcdfe6;
font-weight: 500;
}
.selected-list {
flex: 1;
padding: 10px;
overflow-y: auto;
max-height: 400px;
.selected-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 2px 10px;
margin-bottom: 5px;
background: #f0f9ff;
border: 1px solid #b3d8ff;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s;
&:hover {
background: #e1f3ff;
border-color: #409eff;
}
.remove-icon {
color: #909399;
cursor: pointer;
font-size: 16px;
&:hover {
color: #f56c6c;
}
}
}
.empty-tip {
text-align: center;
color: #909399;
padding: 20px;
}
}
}
:deep(.el-tabs) {
margin-bottom: 10px;
.el-tabs__header {
margin-bottom: 10px;
}
.tab-badge {
margin-left: 8px;
}
}
}
</style>

View File

@ -117,6 +117,22 @@ export const constantRoutes = [
},
],
},
{
path: '/sms/loopSendEdit',
component: Layout,
hidden: true,
children: [
{
path: 'index',
component: () => import('@/views/sMsSendManage/loopSend/edit.vue'),
name: 'LoopSendEdit',
meta: {
title: '循环发送短信',
activeMenu: '/sms/loopSend', // 保持左侧高亮在列表菜单
},
},
],
},
]
// 动态路由,基于用户权限动态去加载

View File

@ -0,0 +1,69 @@
import { reactive } from 'vue'
// 搜索表单配置
export const buildFormColumns = () => [
{
type: 'input',
prop: 'taskName',
placeholder: '请输入任务名称',
},
{
type: 'date',
prop: 'dateRange',
dateType: 'daterange',
placeholder: '请选择日期范围',
paramsList: ['startTime', 'endTime'], // 将日期范围拆分为开始时间和结束时间
},
{
type: 'select',
prop: 'smsType',
placeholder: '请选择短信类型',
options: [
{ label: '通知', value: '1' },
{ label: '计划', value: '2' },
],
},
]
// 表格列配置
export const tableColumns = [
{
prop: 'taskName',
label: '任务名称',
},
{
prop: 'createTime',
label: '创建时间',
},
{
prop: 'msgType',
label: '短信类型',
formatter: (row) => {
const typeMap = {
1: '通知',
2: '计划',
}
return typeMap[row.msgType] || row.smsType
},
},
{
prop: 'sendContent',
label: '短信内容',
showOverflowTooltip: true,
},
{
prop: 'workerCount',
label: '短信接收人数',
},
{
prop: 'taskStatus',
label: '任务状态',
slot: 'taskStatus',
},
]
export default {
tableColumns,
buildFormColumns,
}

View File

@ -0,0 +1,596 @@
<template>
<div class="app-container loop-send-edit">
<!-- 顶部返回 + 标题 -->
<el-page-header @back="onBack" :content="pageTitle" class="page-header" />
<!-- 表单内容 -->
<el-card shadow="never" class="card-section">
<template #header>
<div class="card-header">任务信息</div>
</template>
<el-form
ref="formRef"
:model="formData"
:rules="!isDetail ? rules : {}"
size="large"
label-width="120px"
:disabled="isDetail"
>
<el-row :gutter="24">
<el-col :span="12">
<el-form-item label="任务名称" prop="taskName">
<el-input
v-model.trim="formData.taskName"
placeholder="请输入任务名称"
maxlength="50"
show-word-limit
clearable
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="短信类型" prop="msgType">
<el-select
v-model="formData.msgType"
placeholder="请选择短信类型"
clearable
style="width: 100%"
>
<el-option label="通知" value="1" />
<el-option label="计划" value="2" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="24">
<el-form-item label="cron表达式" prop="cronExpression">
<el-input
v-model="formData.cronExpression"
placeholder="请输入cron执行表达式"
>
<template #append>
<el-button
type="primary"
v-if="!isDetail"
@click="handleShowCron"
>
生成表达式
<el-icon class="el-icon--right"><Clock /></el-icon>
</el-button>
</template>
</el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="12">
<el-form-item label="执行策略" prop="misfirePolicy">
<el-radio-group v-model="formData.misfirePolicy">
<el-radio-button value="1">立即执行</el-radio-button>
<el-radio-button value="2">执行一次</el-radio-button>
<el-radio-button value="3">放弃执行</el-radio-button>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="是否并发" prop="concurrent">
<el-radio-group v-model="formData.concurrent">
<el-radio-button value="0">允许</el-radio-button>
<el-radio-button value="1">禁止</el-radio-button>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="24">
<el-form-item label="发送内容" prop="sendContent">
<el-input
v-model.trim="formData.sendContent"
type="textarea"
:rows="5"
placeholder="请输入发送内容"
maxlength="500"
show-word-limit
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="24">
<el-form-item label="备注" prop="remark">
<el-input
v-model.trim="formData.remark"
type="textarea"
:rows="5"
placeholder="请输入备注"
maxlength="500"
show-word-limit
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="24">
<el-form-item label="接收人员" prop="recipientList">
<div class="recipient-select">
<el-button
type="primary"
link
icon="Plus"
@click="onOpenPersonPicker"
:disabled="isDetail"
v-if="!isDetail"
>
选择
</el-button>
<span class="selected-names">{{ selectedRecipientNames }}</span>
</div>
<el-table
v-if="formData.recipientList && formData.recipientList.length > 0"
:data="formData.recipientList"
border
style="margin-top: 10px"
max-height="200"
>
<el-table-column align="center" prop="workerName" label="姓名" />
<el-table-column align="center" prop="phone" label="电话" />
<el-table-column v-if="!isDetail" label="操作" align="center">
<template #default="{ $index }">
<el-button
type="danger"
link
icon="Delete"
@click="handleRemoveRecipient($index)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
<!-- 底部操作按钮 -->
<div class="page-footer">
<ComButton plain type="info" @click="onBack">
{{ isDetail ? '返回' : '取消' }}
</ComButton>
<ComButton v-if="!isDetail" type="primary" @click="onSubmit">确认执行</ComButton>
</div>
<!-- Cron表达式生成器弹窗 -->
<ComDialog :dialog-config="cronDialogConfig" @closeDialogOuter="onCloseCronDialog">
<template #outerContent>
<crontab
ref="crontabRef"
@hide="handleCronHide"
@fill="crontabFill"
:expression="cronExpression"
/>
</template>
</ComDialog>
<!-- 人员选择弹窗 -->
<ComDialog :dialog-config="managerDialogConfig" @closeDialogOuter="onCloseDialogOuter">
<template #outerContent>
<PersonPicker v-model="formData.recipientList" />
<el-row class="common-btn-row fixed-bottom">
<ComButton plain type="info" @click="managerDialogConfig.outerVisible = false">
取消
</ComButton>
<ComButton type="primary" @click="onConfirmRecipients">确定</ComButton>
</el-row>
</template>
</ComDialog>
</div>
</template>
<script setup name="LoopSendEdit">
import { ref, reactive, computed, getCurrentInstance, nextTick, onMounted, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { Clock } from '@element-plus/icons-vue'
import Crontab from '@/components/Crontab'
import {
addLoopSendAPI,
updateLoopSendAPI,
getLoopSendDetailAPI,
} from '@/api/sMsSendManage/loopSend.js'
import ComButton from '@/components/ComButton/index.vue'
import ComDialog from '@/components/ComDialog/index.vue'
import PersonPicker from '@/components/PersonPicker/index.vue'
const route = useRoute()
const router = useRouter()
const { proxy } = getCurrentInstance()
const mode = computed(() => route.query.mode || 'add')
const isDetail = computed(() => mode.value === 'detail')
const pageTitle = computed(() => {
if (mode.value === 'edit') return '编辑循环发送任务'
if (mode.value === 'detail') return '循环发送任务详情'
return '新增循环发送任务'
})
const formRef = ref(null)
const crontabRef = ref(null)
const personTableRef = ref(null)
const cronExpression = ref('')
// Cron
const cronDialogConfig = reactive({
outerVisible: false,
outerTitle: 'Cron表达式生成器',
outerWidth: '800px',
minHeight: '500px',
maxHeight: '80vh',
})
//
const getInitFormData = () => ({
id: null,
taskName: '', //
msgType: '', // 1-2-
cronExpression: '', // cron
misfirePolicy: '1', // 1-2-3-
concurrent: '1', // 0-1-
sendContent: '', //
recipientList: [], //
})
const formData = ref(getInitFormData())
//
const rules = {
taskName: [{ required: true, message: '请输入任务名称', trigger: 'blur' }],
msgType: [{ required: true, message: '请选择短信类型', trigger: 'change' }],
cronExpression: [{ required: true, message: '请输入cron表达式', trigger: 'change' }],
sendContent: [{ required: true, message: '请输入发送内容', trigger: 'blur' }],
recipientList: [
{
required: true,
message: '请至少选择一个接收人员',
trigger: 'change',
validator: (rule, value, callback) => {
if (!value || value.length === 0) {
callback(new Error('请至少选择一个接收人员'))
} else {
callback()
}
},
},
],
}
//
const managerDialogConfig = reactive({
outerVisible: false,
outerTitle: '选择接收人员',
outerWidth: '80%',
minHeight: '60vh',
maxHeight: '80vh',
})
//
const selectedRecipientNames = computed(() => {
if (!formData.value.recipientList || formData.value.recipientList.length === 0) {
return '暂未选择人员'
}
return formData.value.recipientList.map((item) => item.workerName).join('、')
})
//
const onOpenPersonPicker = () => {
managerDialogConfig.outerVisible = true
}
//
const onConfirmRecipients = () => {
managerDialogConfig.outerVisible = false
}
//
const handleRemoveRecipient = (index) => {
formData.value.recipientList.splice(index, 1)
}
//
const onCloseDialogOuter = (visible) => {
managerDialogConfig.outerVisible = visible
}
// Cron
const handleShowCron = () => {
cronExpression.value = formData.value.cronExpression || ''
cronDialogConfig.outerVisible = true
}
// Cron
const crontabFill = (value) => {
formData.value.cronExpression = value
cronDialogConfig.outerVisible = false
}
// Cronhide
const handleCronHide = () => {
cronDialogConfig.outerVisible = false
}
// Cron
const onCloseCronDialog = (visible) => {
cronDialogConfig.outerVisible = visible
}
//
const onBack = () => {
router.back()
}
//
const onSubmit = async () => {
try {
await formRef.value.validate()
const API = mode.value === 'edit' ? updateLoopSendAPI : addLoopSendAPI
//
const params = {
remark: formData.value.remark,
taskName: formData.value.taskName,
msgType: formData.value.msgType,
cronExpression: formData.value.cronExpression,
misfirePolicy: formData.value.misfirePolicy,
concurrent: formData.value.concurrent,
sendContent: formData.value.sendContent,
workerList: formData.value.recipientList.map((item) => {
return {
id: item.id,
}
}),
}
// id
if (mode.value === 'edit' && route.query.id) {
params.id = Number(route.query.id)
}
const result = await API(params)
if (result.code === 200) {
proxy.$modal.msgSuccess(mode.value === 'edit' ? '编辑成功' : '新增成功')
onBack()
}
} catch (error) {
return Promise.reject(error)
}
}
//
const getDetail = async () => {
if (!route.query.id) return
try {
const result = await getLoopSendDetailAPI(route.query.id)
if (result.code === 200 && result.data) {
const data = result.data
formData.value = {
id: data.id,
taskName: data.taskName || '',
msgType: String(data.msgType || ''),
cronExpression: data.cronExpression || '',
misfirePolicy: String(data.misfirePolicy || '1'),
concurrent: String(data.concurrent || '1'),
sendContent: data.sendContent || '',
recipientList: data.workerList || [],
remark: data.remark || '',
}
cronExpression.value = data.cronExpression || ''
}
} catch (error) {
console.error('获取详情失败:', error)
}
}
onMounted(() => {
if (mode.value === 'edit' || mode.value === 'detail') {
getDetail()
}
})
</script>
<style lang="scss" scoped>
.loop-send-edit {
padding: 20px;
.page-header {
margin-bottom: 20px;
padding: 16px 20px;
background: linear-gradient(135deg, #f5f7fa 0%, #ffffff 100%);
border-radius: 8px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
:deep(.el-page-header__left) {
.el-page-header__back {
padding: 8px;
border-radius: 6px;
transition: all 0.3s ease;
background-color: transparent;
&:hover {
background-color: #f0f2f5;
color: #1677ff;
transform: translateX(-2px);
}
&::before {
font-size: 16px;
font-weight: 600;
}
}
}
:deep(.el-page-header__content) {
font-size: 18px;
font-weight: 600;
color: #1f2937;
margin-left: 12px;
}
}
.card-section {
margin-bottom: 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
overflow: hidden;
transition: box-shadow 0.3s ease;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
:deep(.el-card__header) {
padding: 16px 20px;
background-color: #fafbfc;
border-bottom: 1px solid #f0f2f5;
}
:deep(.el-card__body) {
padding: 24px 20px;
}
}
.recipient-select {
display: flex;
align-items: center;
gap: 10px;
.selected-names {
color: #606266;
font-size: 14px;
flex: 1;
}
}
.person-search-bar {
margin-bottom: 8px;
}
.selected-persons {
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 12px;
height: 100%;
display: flex;
flex-direction: column;
background: #fff;
.selected-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 15px;
border-bottom: 1px solid #e5e7eb;
font-weight: 500;
margin-bottom: 8px;
}
.selected-list {
flex: 1;
padding: 10px;
overflow-y: auto;
max-height: 400px;
display: flex;
flex-direction: column;
gap: 8px;
.selected-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background: #f0f9ff;
border: 1px solid #b3d8ff;
border-radius: 6px;
transition: all 0.3s ease;
&:hover {
background: #e1f3ff;
border-color: #409eff;
}
.remove-icon {
color: #909399;
cursor: pointer;
font-size: 16px;
transition: color 0.3s ease;
&:hover {
color: #f56c6c;
}
}
}
.empty-tip {
text-align: center;
color: #9ca3af;
padding: 20px;
}
}
}
.page-footer {
margin-top: 12px;
display: flex;
justify-content: flex-end;
gap: 12px;
}
}
.common-btn-row {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 20px;
padding: 0 20px 20px;
&.fixed-bottom {
position: sticky;
bottom: 0;
background: #fff;
z-index: 10;
border-top: 1px solid #e4e7ed;
padding-top: 15px;
}
}
.card-header {
font-weight: 600;
font-size: 15px;
color: #1f2937;
position: relative;
padding-left: 12px;
display: flex;
align-items: center;
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 16px;
background: linear-gradient(180deg, #1677ff 0%, #4096ff 100%);
border-radius: 2px;
}
}
</style>

View File

@ -0,0 +1,151 @@
<template>
<div class="app-container">
<!-- 短信余额提示 -->
<el-alert
v-if="smsBalance !== null"
:title="`短信余额剩余${smsBalance}条,请及时充值!`"
type="warning"
:closable="false"
show-icon
style="margin-bottom: 20px"
/>
<!-- 循环发送短信列表 -->
<ComTable
ref="comTableRef"
:form-columns="formColumns"
:table-columns="tableColumns"
:load-data="listLoopSendAPI"
:show-toolbar="true"
:show-action="true"
:action-columns="actionColumns"
>
<template #toolbar>
<ComButton
type="primary"
icon="Plus"
@click="onHandleAdd"
v-hasPermi="['sms:loopTask:add']"
>
新建
</ComButton>
</template>
<template #taskStatus="{ row }">
<!-- <el-switch
v-model="row.taskStatus"
active-value="1"
inactive-value="0"
@change="onHandleStatusChange($event, row)"
/> -->
</template>
</ComTable>
</div>
</template>
<script setup name="LoopSend">
import { ref, computed, getCurrentInstance } from 'vue'
import { useRouter } from 'vue-router'
import {
listLoopSendAPI,
delLoopSendAPI,
updateLoopSendStatusAPI,
} from '@/api/sMsSendManage/loopSend.js'
import config from './config'
import ComTable from '@/components/ComTable/index.vue'
import ComButton from '@/components/ComButton/index.vue'
const router = useRouter()
const { proxy } = getCurrentInstance()
const { tableColumns, buildFormColumns } = config
const comTableRef = ref(null)
const smsBalance = ref(null) //
//
const formColumns = computed(() => buildFormColumns())
//
const actionColumns = [
{
label: '详情',
type: 'primary',
link: true,
permission: ['sms:loopTask:detail'],
handler: (row) => {
router.push({
path: '/sms/loopSendEdit/index',
query: {
id: row.id,
mode: 'detail',
},
})
},
},
// {
// label: '',
// type: 'primary',
// link: true,
// permission: ['sms:loopTask:edit'],
// handler: (row) => {
// router.push({
// path: '/sms/loopSendEdit/index',
// query: {
// id: row.id,
// mode: 'edit',
// },
// })
// },
// },
{
label: '删除',
type: 'danger',
link: true,
permission: ['sms:loopTask:remove'],
handler: (row) => {
proxy.$modal.confirm('是否确认删除该循环发送任务?').then(async () => {
const result = await delLoopSendAPI({ id: row.id })
if (result.code === 200) {
proxy.$modal.msgSuccess('删除成功')
comTableRef.value?.refresh()
}
})
},
},
]
//
const onHandleAdd = () => {
router.push({
path: '/sms/loopSendEdit/index',
query: {
mode: 'add',
},
})
}
//
const onHandleStatusChange = async (value, row) => {
try {
const result = await updateLoopSendStatusAPI({
id: row.id,
taskStatus: value,
})
if (result.code === 200) {
proxy.$modal.msgSuccess(value === '1' ? '启用成功' : '停用成功')
comTableRef.value?.refresh()
} else {
//
row.taskStatus = value === '1' ? '0' : '1'
}
} catch (error) {
//
row.taskStatus = value === '1' ? '0' : '1'
}
}
</script>
<style lang="scss" scoped>
.app-container {
padding: 20px;
}
</style>

View File

@ -0,0 +1,9 @@
<template>
<div>单次发送</div>
</template>
<script setup>
</script>
<style>
</style>