|
|
@@ -0,0 +1,376 @@
|
|
|
+<script setup lang="ts">
|
|
|
+import { reactive, onMounted, ref, nextTick } from "vue";
|
|
|
+import { getTemplateList, createTemplate, updateTemplate, deleteTemplate, sendByTemplate } from "@/api/message-template";
|
|
|
+import { searchAdminUser } from "@/api/message";
|
|
|
+import { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
|
|
+import { ElMessage, ElMessageBox } from "element-plus";
|
|
|
+
|
|
|
+defineOptions({ name: "AdminMessageTemplate" });
|
|
|
+
|
|
|
+const queryRef = ref();
|
|
|
+
|
|
|
+const state = reactive({
|
|
|
+ formQuery: {
|
|
|
+ name: "",
|
|
|
+ code: "",
|
|
|
+ type: "" as number | string,
|
|
|
+ status: "" as number | string
|
|
|
+ },
|
|
|
+ pageQuery: {
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 10,
|
|
|
+ total: 0
|
|
|
+ },
|
|
|
+ tableData: {
|
|
|
+ height: 500,
|
|
|
+ data: [] as Array<any>,
|
|
|
+ loading: false
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+// 新增/编辑模板弹窗
|
|
|
+const formVisible = ref(false);
|
|
|
+const formLoading = ref(false);
|
|
|
+const formType = ref<"add" | "edit">("add");
|
|
|
+const formData = reactive({
|
|
|
+ id: null as number | null,
|
|
|
+ code: "",
|
|
|
+ name: "",
|
|
|
+ type: 1,
|
|
|
+ priority: 0,
|
|
|
+ titleTemplate: "",
|
|
|
+ contentTemplate: "",
|
|
|
+ status: 1,
|
|
|
+ remark: ""
|
|
|
+});
|
|
|
+
|
|
|
+// 使用模板发送弹窗
|
|
|
+const sendVisible = ref(false);
|
|
|
+const sendLoading = ref(false);
|
|
|
+const sendForm = reactive({
|
|
|
+ templateId: null as number | null,
|
|
|
+ title: "",
|
|
|
+ content: "",
|
|
|
+ type: 1,
|
|
|
+ priority: 0,
|
|
|
+ sendType: "all" as "all" | "selected",
|
|
|
+ receiverIds: [] as number[]
|
|
|
+});
|
|
|
+const userOptions = ref<any[]>([]);
|
|
|
+
|
|
|
+const typeOptions = [
|
|
|
+ { label: "系统通知", value: 1 },
|
|
|
+ { label: "站内信", value: 2 },
|
|
|
+ { label: "待办事项", value: 3 },
|
|
|
+ { label: "公告通知", value: 4 }
|
|
|
+];
|
|
|
+
|
|
|
+const priorityOptions = [
|
|
|
+ { label: "普通", value: 0 },
|
|
|
+ { label: "重要", value: 1 },
|
|
|
+ { label: "紧急", value: 2 }
|
|
|
+];
|
|
|
+
|
|
|
+const getTypeTag = (type: number) => {
|
|
|
+ const map: Record<number, string> = { 1: "", 2: "success", 3: "warning", 4: "info" };
|
|
|
+ return map[type] || "";
|
|
|
+};
|
|
|
+
|
|
|
+const getTypeName = (type: number) => {
|
|
|
+ return typeOptions.find(k => k.value === type)?.label || "";
|
|
|
+};
|
|
|
+
|
|
|
+const getPriorityTag = (priority: number) => {
|
|
|
+ const map: Record<number, string> = { 0: "info", 1: "warning", 2: "danger" };
|
|
|
+ return map[priority] || "";
|
|
|
+};
|
|
|
+
|
|
|
+const getPriorityName = (priority: number) => {
|
|
|
+ return priorityOptions.find(k => k.value === priority)?.label || "";
|
|
|
+};
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ loadData();
|
|
|
+ nextTick(() => {
|
|
|
+ const bodyHeight = document.body.clientHeight;
|
|
|
+ const queryHeight = queryRef.value?.$el?.clientHeight || 0;
|
|
|
+ state.tableData.height = bodyHeight - queryHeight - 280;
|
|
|
+ });
|
|
|
+});
|
|
|
+
|
|
|
+const loadData = (refresh: boolean = false) => {
|
|
|
+ if (refresh) state.pageQuery.pageNum = 1;
|
|
|
+ state.tableData.loading = true;
|
|
|
+ getTemplateList({ ...state.formQuery, ...state.pageQuery })
|
|
|
+ .then((res: any) => {
|
|
|
+ const { list, total } = res || {};
|
|
|
+ state.tableData.data = list || [];
|
|
|
+ state.pageQuery.total = total || 0;
|
|
|
+ })
|
|
|
+ .catch(() => { state.tableData.data = []; })
|
|
|
+ .finally(() => { state.tableData.loading = false; });
|
|
|
+};
|
|
|
+
|
|
|
+const handleSizeChange = (size: number) => { state.pageQuery.pageSize = size; loadData(true); };
|
|
|
+const handleCurrentChange = (page: number) => { state.pageQuery.pageNum = page; loadData(); };
|
|
|
+const handleSearch = () => loadData(true);
|
|
|
+const handleReset = () => {
|
|
|
+ state.formQuery = { name: "", code: "", type: "", status: "" };
|
|
|
+ loadData(true);
|
|
|
+};
|
|
|
+
|
|
|
+// 新增/编辑模板
|
|
|
+const handleAdd = () => {
|
|
|
+ formType.value = "add";
|
|
|
+ Object.assign(formData, {
|
|
|
+ id: null, code: "", name: "", type: 1, priority: 0,
|
|
|
+ titleTemplate: "", contentTemplate: "", status: 1, remark: ""
|
|
|
+ });
|
|
|
+ formVisible.value = true;
|
|
|
+};
|
|
|
+
|
|
|
+const handleEdit = (row: any) => {
|
|
|
+ formType.value = "edit";
|
|
|
+ Object.assign(formData, row);
|
|
|
+ formVisible.value = true;
|
|
|
+};
|
|
|
+
|
|
|
+const handleFormSubmit = async () => {
|
|
|
+ if (!formData.code || !formData.name || !formData.titleTemplate || !formData.contentTemplate) {
|
|
|
+ ElMessage.warning("请完整填写必填项");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ formLoading.value = true;
|
|
|
+ try {
|
|
|
+ if (formType.value === "add") {
|
|
|
+ await createTemplate(formData);
|
|
|
+ } else {
|
|
|
+ await updateTemplate(formData);
|
|
|
+ }
|
|
|
+ ElMessage.success("操作成功");
|
|
|
+ formVisible.value = false;
|
|
|
+ loadData(true);
|
|
|
+ } catch (e) {
|
|
|
+ ElMessage.error("操作失败");
|
|
|
+ } finally {
|
|
|
+ formLoading.value = false;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 删除模板
|
|
|
+const handleDelete = (row: any) => {
|
|
|
+ ElMessageBox.confirm(`确定要删除模板『${row.name}』吗?`, "提示", {
|
|
|
+ confirmButtonText: "确定", cancelButtonText: "取消", type: "warning"
|
|
|
+ }).then(() => {
|
|
|
+ deleteTemplate(row.id).then(() => {
|
|
|
+ ElMessage.success("删除成功");
|
|
|
+ loadData(true);
|
|
|
+ });
|
|
|
+ }).catch(() => {});
|
|
|
+};
|
|
|
+
|
|
|
+// 使用模板发送
|
|
|
+const handleOpenSend = (row: any) => {
|
|
|
+ Object.assign(sendForm, {
|
|
|
+ templateId: row.id,
|
|
|
+ title: row.titleTemplate || "",
|
|
|
+ content: row.contentTemplate || "",
|
|
|
+ type: row.type,
|
|
|
+ priority: row.priority,
|
|
|
+ sendType: "all",
|
|
|
+ receiverIds: []
|
|
|
+ });
|
|
|
+ userOptions.value = [];
|
|
|
+ sendVisible.value = true;
|
|
|
+};
|
|
|
+
|
|
|
+const searchUsers = (query: string) => {
|
|
|
+ if (!query) { userOptions.value = []; return; }
|
|
|
+ searchAdminUser({ keyword: query, pageSize: 20 }).then((res: any) => {
|
|
|
+ userOptions.value = (res || []).map((k: any) => ({
|
|
|
+ label: `${k.nickname || k.username} (${k.mobilePhone || ""})`,
|
|
|
+ value: k.id || k.userId
|
|
|
+ }));
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+const handleSendSubmit = async () => {
|
|
|
+ if (!sendForm.title || !sendForm.content) {
|
|
|
+ ElMessage.warning("请填写标题和内容");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ sendLoading.value = true;
|
|
|
+ try {
|
|
|
+ await sendByTemplate({
|
|
|
+ title: sendForm.title,
|
|
|
+ content: sendForm.content,
|
|
|
+ type: sendForm.type,
|
|
|
+ priority: sendForm.priority,
|
|
|
+ sendAll: sendForm.sendType === "all",
|
|
|
+ receiverIds: sendForm.sendType === "selected" ? sendForm.receiverIds : [],
|
|
|
+ templateId: sendForm.templateId
|
|
|
+ });
|
|
|
+ ElMessage.success("发送成功");
|
|
|
+ sendVisible.value = false;
|
|
|
+ } catch (e) {
|
|
|
+ ElMessage.error("发送失败");
|
|
|
+ } finally {
|
|
|
+ sendLoading.value = false;
|
|
|
+ }
|
|
|
+};
|
|
|
+</script>
|
|
|
+
|
|
|
+<template>
|
|
|
+ <div class="page-container">
|
|
|
+ <el-card shadow="hover">
|
|
|
+ <el-form ref="queryRef" :model="state.formQuery" inline class="search-form">
|
|
|
+ <el-form-item label="模板名称">
|
|
|
+ <el-input v-model="state.formQuery.name" placeholder="请输入" clearable style="width: 160px" @keyup.enter="handleSearch" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="模板编码">
|
|
|
+ <el-input v-model="state.formQuery.code" placeholder="请输入" clearable style="width: 140px" @keyup.enter="handleSearch" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="消息类型">
|
|
|
+ <el-select v-model="state.formQuery.type" placeholder="请选择" clearable style="width: 130px">
|
|
|
+ <el-option v-for="opt in typeOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="状态">
|
|
|
+ <el-select v-model="state.formQuery.status" placeholder="请选择" clearable style="width: 100px">
|
|
|
+ <el-option label="启用" :value="1" />
|
|
|
+ <el-option label="禁用" :value="0" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item>
|
|
|
+ <el-button type="primary" :icon="useRenderIcon('ri/search-line')" @click="handleSearch">查询</el-button>
|
|
|
+ <el-button :icon="useRenderIcon('ri/refresh-line')" @click="handleReset">重置</el-button>
|
|
|
+ <el-button type="success" :icon="useRenderIcon('ri/add-line')" @click="handleAdd">新增模板</el-button>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+
|
|
|
+ <el-table v-loading="state.tableData.loading" :data="state.tableData.data" :height="state.tableData.height" border stripe>
|
|
|
+ <template #empty><el-empty description="暂无数据" /></template>
|
|
|
+ <el-table-column label="ID" prop="id" width="70" />
|
|
|
+ <el-table-column label="模板编码" prop="code" width="140" />
|
|
|
+ <el-table-column label="模板名称" prop="name" width="140" />
|
|
|
+ <el-table-column label="消息类型" prop="type" width="110">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tag :type="getTypeTag(row.type)" size="small">{{ getTypeName(row.type) }}</el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="标题模板" prop="titleTemplate" min-width="180" show-overflow-tooltip />
|
|
|
+ <el-table-column label="内容模板" prop="contentTemplate" min-width="250" show-overflow-tooltip />
|
|
|
+ <el-table-column label="优先级" prop="priority" width="90">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tag :type="getPriorityTag(row.priority)" size="small">{{ getPriorityName(row.priority) }}</el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="状态" prop="status" width="80">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">{{ row.status === 1 ? "启用" : "禁用" }}</el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="创建时间" prop="createTime" width="170" />
|
|
|
+ <el-table-column label="操作" width="200" fixed="right">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-button type="success" link size="small" @click="handleOpenSend(row)">发送</el-button>
|
|
|
+ <el-button type="primary" link size="small" @click="handleEdit(row)">编辑</el-button>
|
|
|
+ <el-button type="danger" link size="small" @click="handleDelete(row)">删除</el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+
|
|
|
+ <div class="pagination-container">
|
|
|
+ <el-pagination
|
|
|
+ v-model:current-page="state.pageQuery.pageNum"
|
|
|
+ v-model:page-size="state.pageQuery.pageSize"
|
|
|
+ :total="state.pageQuery.total"
|
|
|
+ :page-sizes="[10, 20, 50, 100]"
|
|
|
+ layout="total, sizes, prev, pager, next, jumper"
|
|
|
+ @size-change="handleSizeChange"
|
|
|
+ @current-change="handleCurrentChange"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <!-- 新增/编辑模板弹窗 -->
|
|
|
+ <el-dialog v-model="formVisible" :title="formType === 'add' ? '新增模板' : '编辑模板'" width="700px" destroy-on-close>
|
|
|
+ <el-form :model="formData" label-width="120px">
|
|
|
+ <el-form-item label="模板编码" required>
|
|
|
+ <el-input v-model="formData.code" placeholder="请输入编码(大写)" :disabled="formType === 'edit'" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="模板名称" required>
|
|
|
+ <el-input v-model="formData.name" placeholder="请输入名称" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="消息类型">
|
|
|
+ <el-select v-model="formData.type" style="width: 100%">
|
|
|
+ <el-option v-for="opt in typeOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="优先级">
|
|
|
+ <el-select v-model="formData.priority" style="width: 100%">
|
|
|
+ <el-option v-for="opt in priorityOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="标题模板" required>
|
|
|
+ <el-input v-model="formData.titleTemplate" placeholder="支持 ${变量名} 语法" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="内容模板" required>
|
|
|
+ <el-input v-model="formData.contentTemplate" type="textarea" :rows="4" placeholder="支持 ${变量名} 语法" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="状态">
|
|
|
+ <el-radio-group v-model="formData.status">
|
|
|
+ <el-radio :value="1">启用</el-radio>
|
|
|
+ <el-radio :value="0">禁用</el-radio>
|
|
|
+ </el-radio-group>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="备注">
|
|
|
+ <el-input v-model="formData.remark" type="textarea" :rows="2" placeholder="备注信息" />
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <template #footer>
|
|
|
+ <el-button @click="formVisible = false">取消</el-button>
|
|
|
+ <el-button :loading="formLoading" type="primary" @click="handleFormSubmit">确定</el-button>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <!-- 使用模板发送弹窗 -->
|
|
|
+ <el-dialog v-model="sendVisible" title="使用模板发送消息" width="650px" destroy-on-close>
|
|
|
+ <el-form :model="sendForm" label-width="100px">
|
|
|
+ <el-form-item label="标题" required>
|
|
|
+ <el-input v-model="sendForm.title" placeholder="消息标题" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="内容" required>
|
|
|
+ <el-input v-model="sendForm.content" type="textarea" :rows="5" placeholder="消息内容" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="发送方式">
|
|
|
+ <el-radio-group v-model="sendForm.sendType">
|
|
|
+ <el-radio value="all">全部用户</el-radio>
|
|
|
+ <el-radio value="selected">指定用户</el-radio>
|
|
|
+ </el-radio-group>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="接收者" v-if="sendForm.sendType === 'selected'">
|
|
|
+ <el-select
|
|
|
+ v-model="sendForm.receiverIds"
|
|
|
+ multiple filterable remote reserve-keyword
|
|
|
+ placeholder="搜索并选择用户"
|
|
|
+ :remote-method="searchUsers"
|
|
|
+ style="width: 100%"
|
|
|
+ >
|
|
|
+ <el-option v-for="user in userOptions" :key="user.value" :label="user.label" :value="user.value" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <template #footer>
|
|
|
+ <el-button @click="sendVisible = false">取消</el-button>
|
|
|
+ <el-button :loading="sendLoading" type="primary" @click="handleSendSubmit">发送</el-button>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+.page-container { padding: 15px; }
|
|
|
+.search-form { margin-bottom: 15px; }
|
|
|
+.pagination-container { display: flex; justify-content: flex-end; margin-top: 15px; }
|
|
|
+</style>
|