增加通知模块
This commit is contained in:
parent
4e1b30b3cc
commit
d3886e2475
3
platform/components.d.ts
vendored
3
platform/components.d.ts
vendored
@ -18,7 +18,6 @@ declare module 'vue' {
|
|||||||
ElAvatar: typeof import('element-plus/es')['ElAvatar']
|
ElAvatar: typeof import('element-plus/es')['ElAvatar']
|
||||||
ElBacktop: typeof import('element-plus/es')['ElBacktop']
|
ElBacktop: typeof import('element-plus/es')['ElBacktop']
|
||||||
ElButton: typeof import('element-plus/es')['ElButton']
|
ElButton: typeof import('element-plus/es')['ElButton']
|
||||||
ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
|
|
||||||
ElCard: typeof import('element-plus/es')['ElCard']
|
ElCard: typeof import('element-plus/es')['ElCard']
|
||||||
ElCascader: typeof import('element-plus/es')['ElCascader']
|
ElCascader: typeof import('element-plus/es')['ElCascader']
|
||||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||||
@ -40,7 +39,6 @@ declare module 'vue' {
|
|||||||
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||||
ElHeader: typeof import('element-plus/es')['ElHeader']
|
ElHeader: typeof import('element-plus/es')['ElHeader']
|
||||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||||
ElImage: typeof import('element-plus/es')['ElImage']
|
|
||||||
ElInput: typeof import('element-plus/es')['ElInput']
|
ElInput: typeof import('element-plus/es')['ElInput']
|
||||||
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
||||||
ElLink: typeof import('element-plus/es')['ElLink']
|
ElLink: typeof import('element-plus/es')['ElLink']
|
||||||
@ -56,6 +54,7 @@ declare module 'vue' {
|
|||||||
ElRow: typeof import('element-plus/es')['ElRow']
|
ElRow: typeof import('element-plus/es')['ElRow']
|
||||||
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||||
|
ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
|
||||||
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||||
ElTable: typeof import('element-plus/es')['ElTable']
|
ElTable: typeof import('element-plus/es')['ElTable']
|
||||||
|
|||||||
53
platform/src/api/reminder.js
Normal file
53
platform/src/api/reminder.js
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import request from "@/utils/request";
|
||||||
|
|
||||||
|
/** 提醒列表 */
|
||||||
|
export function getReminderList(params) {
|
||||||
|
return request({
|
||||||
|
url: "/platform/reminder/list",
|
||||||
|
method: "get",
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 提醒详情 */
|
||||||
|
export function getReminderDetail(id) {
|
||||||
|
return request({
|
||||||
|
url: `/platform/reminder/${id}`,
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 新增提醒 */
|
||||||
|
export function createReminder(data) {
|
||||||
|
return request({
|
||||||
|
url: "/platform/reminder",
|
||||||
|
method: "post",
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 更新提醒 */
|
||||||
|
export function updateReminder(id, data) {
|
||||||
|
return request({
|
||||||
|
url: `/platform/reminder/${id}`,
|
||||||
|
method: "post",
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除提醒 */
|
||||||
|
export function deleteReminder(id) {
|
||||||
|
return request({
|
||||||
|
url: `/platform/reminder/${id}`,
|
||||||
|
method: "delete",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量删除提醒 */
|
||||||
|
export function batchDeleteReminder(ids) {
|
||||||
|
return request({
|
||||||
|
url: "/platform/reminder/batchDelete",
|
||||||
|
method: "post",
|
||||||
|
data: { ids },
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -0,0 +1,765 @@
|
|||||||
|
<template>
|
||||||
|
<div class="notification-settings">
|
||||||
|
<div class="settings-content">
|
||||||
|
<el-tabs v-model="activeSubTab" tab-position="left" class="sub-tabs-container">
|
||||||
|
|
||||||
|
<!-- 邮箱 -->
|
||||||
|
<el-tab-pane name="email">
|
||||||
|
<template #label>
|
||||||
|
<span class="tab-label-item">
|
||||||
|
<el-icon><Message /></el-icon>
|
||||||
|
<span>邮箱配置</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<div class="tab-pane-content">
|
||||||
|
<el-card shadow="never" class="settings-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>邮箱通知配置</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-form :model="formData.email" label-width="140px" label-position="right">
|
||||||
|
<el-form-item label="启用状态">
|
||||||
|
<el-switch v-model="formData.email.enabled" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="发件人邮箱" required>
|
||||||
|
<el-input v-model="formData.email.fromAddress" placeholder="例如:noreply@example.com" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="发件人名称">
|
||||||
|
<el-input v-model="formData.email.fromName" placeholder="例如:官方网站" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="SMTP 服务器" required>
|
||||||
|
<el-input v-model="formData.email.host" placeholder="例如:smtp.qq.com" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="SMTP 端口" required>
|
||||||
|
<el-input v-model="formData.email.port" placeholder="例如:465 或 587" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="授权码/密码" required>
|
||||||
|
<el-input v-model="formData.email.password" type="password" show-password placeholder="邮箱授权码或登录密码" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="加密方式">
|
||||||
|
<el-radio-group v-model="formData.email.encryption">
|
||||||
|
<el-radio label="ssl">SSL</el-radio>
|
||||||
|
<el-radio label="tls">TLS</el-radio>
|
||||||
|
<el-radio label="none">无加密</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="超时时间(秒)">
|
||||||
|
<el-input-number v-model="formData.email.timeout" :min="1" :max="300" controls-position="right" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="测试收件邮箱">
|
||||||
|
<el-input v-model="formData.email.testEmail" placeholder="输入邮箱地址用于测试发信" clearable />
|
||||||
|
<div style="margin-top: 8px;">
|
||||||
|
<el-button type="success" plain size="small" :loading="emailTestLoading" @click="handleSendEmailTest">
|
||||||
|
发送测试邮件
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<!-- 短信 -->
|
||||||
|
<el-tab-pane name="sms">
|
||||||
|
<template #label>
|
||||||
|
<span class="tab-label-item">
|
||||||
|
<el-icon><Cellphone /></el-icon>
|
||||||
|
<span>短信配置</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<div class="tab-pane-content">
|
||||||
|
<el-card shadow="never" class="settings-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>短信配置</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-form :model="formData.sms" label-width="140px" label-position="right">
|
||||||
|
<el-form-item label="启用状态">
|
||||||
|
<el-switch v-model="formData.sms.enabled" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="服务商">
|
||||||
|
<el-radio-group v-model="formData.sms.provider">
|
||||||
|
<el-radio label="aliyun">阿里云短信</el-radio>
|
||||||
|
<el-radio label="tencent">腾讯云短信</el-radio>
|
||||||
|
<el-radio label="custom">自定义</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- 阿里云/腾讯云服务商配置 -->
|
||||||
|
<template v-if="formData.sms.provider !== 'custom'">
|
||||||
|
<el-form-item label="AccessKey ID">
|
||||||
|
<el-input v-model="formData.sms.access_key" placeholder="请输入 AccessKey ID" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="AccessKey Secret">
|
||||||
|
<el-input v-model="formData.sms.secret_key" type="password" show-password placeholder="请输入 AccessKey Secret" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="短信签名">
|
||||||
|
<el-input v-model="formData.sms.sign_name" placeholder="例如:云泽科技" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="短信模板 Code">
|
||||||
|
<el-input v-model="formData.sms.template_code" placeholder="例如:SMS_200300400" />
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 自定义网关配置 -->
|
||||||
|
<template v-else>
|
||||||
|
<el-divider content-position="left">自定义短信网关</el-divider>
|
||||||
|
<el-form-item label="短信网关地址" required>
|
||||||
|
<el-input
|
||||||
|
v-model="formData.sms.backendUrl"
|
||||||
|
placeholder="例如:https://yzsms.yunzer.cn"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="短信API KEY" required>
|
||||||
|
<el-input
|
||||||
|
v-model="formData.sms.apiKey"
|
||||||
|
placeholder="请输入 API_KEY"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="测试手机号">
|
||||||
|
<el-input
|
||||||
|
v-model="formData.sms.testPhone"
|
||||||
|
placeholder="国际格式号码,例如 +8613712345678"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
<div style="margin-top: 8px;">
|
||||||
|
<el-button type="success" plain size="small" :loading="testLoading" @click="handleSendTest">
|
||||||
|
发送测试短信
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- 站内信配置 -->
|
||||||
|
<el-tab-pane name="sitemsg">
|
||||||
|
<template #label>
|
||||||
|
<span class="tab-label-item">
|
||||||
|
<el-icon><Bell /></el-icon>
|
||||||
|
<span>站内信配置</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<div class="tab-pane-content">
|
||||||
|
<el-card shadow="never" class="settings-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>站内信通知配置</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-form :model="formData.sitemsg" label-width="140px" label-position="right">
|
||||||
|
<el-form-item label="启用状态">
|
||||||
|
<el-switch v-model="formData.sitemsg.enabled" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="保留天数">
|
||||||
|
<el-input-number v-model="formData.sitemsg.retention_days" :min="1" :max="365" controls-position="right" />
|
||||||
|
<span class="form-tip-inline">天(过期消息将自动清理)</span>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="自动标记已读">
|
||||||
|
<el-switch v-model="formData.sitemsg.auto_read" />
|
||||||
|
<div class="form-tip">用户打开消息中心列表时是否自动将所有消息标记为已读</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<!-- 钉钉配置 -->
|
||||||
|
<el-tab-pane name="dingtalk">
|
||||||
|
<template #label>
|
||||||
|
<span class="tab-label-item">
|
||||||
|
<el-icon><ChatDotSquare /></el-icon>
|
||||||
|
<span>钉钉配置</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<div class="tab-pane-content">
|
||||||
|
<el-card shadow="never" class="settings-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>钉钉群机器人配置</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-form :model="formData.dingtalk" label-width="140px" label-position="right">
|
||||||
|
<el-form-item label="启用状态">
|
||||||
|
<el-switch v-model="formData.dingtalk.enabled" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="Webhook 地址">
|
||||||
|
<el-input v-model="formData.dingtalk.webhook_url" type="textarea" :rows="3" placeholder="请输入钉钉机器人的 Webhook 地址 (https://oapi.dingtalk.com/robot/send?access_token=...)" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="加签密钥">
|
||||||
|
<el-input v-model="formData.dingtalk.secret" type="password" show-password placeholder="请输入安全配置中加签的密钥(可选)" />
|
||||||
|
<div class="form-tip">如果机器人的安全配置开启了“加签”,请在此处填写对应的秘钥</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<!-- Webhook配置 -->
|
||||||
|
<el-tab-pane name="WebHook配置">
|
||||||
|
<template #label>
|
||||||
|
<span class="tab-label-item">
|
||||||
|
<el-icon><Connection /></el-icon>
|
||||||
|
<span>WebHook配置</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<div class="tab-pane-content">
|
||||||
|
<el-card shadow="never" class="settings-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>HTTP Webhook 配置</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-form :model="formData.webhook" label-width="140px" label-position="right">
|
||||||
|
<el-form-item label="启用状态">
|
||||||
|
<el-switch v-model="formData.webhook.enabled" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="推送 URL">
|
||||||
|
<el-input v-model="formData.webhook.url" type="textarea" :rows="2" placeholder="请输入通知推送的 Webhook 目标 URL" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="请求方式">
|
||||||
|
<el-select v-model="formData.webhook.method" style="width: 120px">
|
||||||
|
<el-option label="POST" value="POST" />
|
||||||
|
<el-option label="GET" value="GET" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="签名密钥 (Token)">
|
||||||
|
<el-input v-model="formData.webhook.token" type="password" show-password placeholder="请输入用于验证的 Token 密钥" />
|
||||||
|
<div class="form-tip">该 Token 将被包含在请求 Header 的 X-Webhook-Token 中以供接收方校验</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<!-- 飞书配置 -->
|
||||||
|
<el-tab-pane name="feishu">
|
||||||
|
<template #label>
|
||||||
|
<span class="tab-label-item">
|
||||||
|
<el-icon><Coordinate /></el-icon>
|
||||||
|
<span>飞书配置</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<div class="tab-pane-content">
|
||||||
|
<el-card shadow="never" class="settings-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>飞书机器人配置</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-form :model="formData.feishu" label-width="140px" label-position="right">
|
||||||
|
<el-form-item label="启用状态">
|
||||||
|
<el-switch v-model="formData.feishu.enabled" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="Webhook 地址">
|
||||||
|
<el-input v-model="formData.feishu.webhook_url" type="textarea" :rows="3" placeholder="请输入飞书机器人的 Webhook 地址 (https://open.feishu.cn/open-apis/bot/v2/hook/...)" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="加签密钥">
|
||||||
|
<el-input v-model="formData.feishu.secret" type="password" show-password placeholder="请输入安全配置中加签的密钥(可选)" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<!-- Bark配置 -->
|
||||||
|
<el-tab-pane name="Bark配置">
|
||||||
|
<template #label>
|
||||||
|
<span class="tab-label-item">
|
||||||
|
<el-icon><ChatRound /></el-icon>
|
||||||
|
<span>Bark配置</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<div class="tab-pane-content">
|
||||||
|
<el-card shadow="never" class="settings-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>Bark 推送配置</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-form :model="formData.bark" label-width="140px" label-position="right">
|
||||||
|
<el-form-item label="启用状态">
|
||||||
|
<el-switch v-model="formData.bark.enabled" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="推送服务器">
|
||||||
|
<el-input v-model="formData.bark.server_url" placeholder="默认官方服务:https://api.day.app" />
|
||||||
|
<div class="form-tip">支持自建 Bark 服务,如:https://bark.yourdomain.com</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="设备 Key">
|
||||||
|
<el-input v-model="formData.bark.device_key" placeholder="请输入您的 Bark Device Key" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<!-- 微信配置 -->
|
||||||
|
<el-tab-pane name="wechat">
|
||||||
|
<template #label>
|
||||||
|
<span class="tab-label-item">
|
||||||
|
<el-icon><ChatDotRound /></el-icon>
|
||||||
|
<span>微信配置</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<div class="tab-pane-content">
|
||||||
|
<el-card shadow="never" class="settings-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>微信公众号/企业微信推送</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-form :model="formData.wechat" label-width="140px" label-position="right">
|
||||||
|
<el-form-item label="启用状态">
|
||||||
|
<el-switch v-model="formData.wechat.enabled" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="AppID / CorpID">
|
||||||
|
<el-input v-model="formData.wechat.app_id" placeholder="请输入公众号 AppID 或企业微信 CorpID" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="AppSecret / Secret">
|
||||||
|
<el-input v-model="formData.wechat.app_secret" type="password" show-password placeholder="请输入 AppSecret" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="模版ID / AgentID">
|
||||||
|
<el-input v-model="formData.wechat.template_id" placeholder="请输入模版消息 ID 或企业微信应用 AgentID" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<!-- Telegram配置 -->
|
||||||
|
<el-tab-pane name="Telegram配置">
|
||||||
|
<template #label>
|
||||||
|
<span class="tab-label-item">
|
||||||
|
<el-icon><Promotion /></el-icon>
|
||||||
|
<span>Telegram配置</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<div class="tab-pane-content">
|
||||||
|
<el-card shadow="never" class="settings-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>Telegram Bot 推送</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-form :model="formData.telegram" label-width="140px" label-position="right">
|
||||||
|
<el-form-item label="启用状态">
|
||||||
|
<el-switch v-model="formData.telegram.enabled" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="Bot Token">
|
||||||
|
<el-input v-model="formData.telegram.bot_token" type="password" show-password placeholder="例如:123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="Chat ID">
|
||||||
|
<el-input v-model="formData.telegram.chat_id" placeholder="例如:123456789 或 @my_channel" />
|
||||||
|
<div class="form-tip">支持个人 Chat ID、群组 ID (通常为负数) 或公开频道 Username</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
</el-tabs>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 底部操作按钮 -->
|
||||||
|
<div class="footer-actions">
|
||||||
|
<el-button @click="handleReset">重置</el-button>
|
||||||
|
<el-button type="primary" :loading="submitting" @click="handleSubmit">
|
||||||
|
保存配置
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { reactive, ref, onMounted } from 'vue';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import {
|
||||||
|
Message,
|
||||||
|
Cellphone,
|
||||||
|
Bell,
|
||||||
|
ChatDotSquare,
|
||||||
|
Connection,
|
||||||
|
Coordinate,
|
||||||
|
ChatRound,
|
||||||
|
ChatDotRound,
|
||||||
|
Promotion
|
||||||
|
} from "@element-plus/icons-vue";
|
||||||
|
import { getSmsInfo, editSmsInfo, sendTestSms } from "@/api/sms";
|
||||||
|
import { getEmailInfo, editEmailInfo, sendTestEmail } from "@/api/email";
|
||||||
|
|
||||||
|
const STORAGE_KEY = "notification_settings_draft";
|
||||||
|
const activeSubTab = ref("email");
|
||||||
|
const submitting = ref(false);
|
||||||
|
|
||||||
|
const formData = reactive({
|
||||||
|
email: {
|
||||||
|
enabled: false,
|
||||||
|
fromAddress: "",
|
||||||
|
fromName: "",
|
||||||
|
host: "",
|
||||||
|
port: "",
|
||||||
|
password: "",
|
||||||
|
encryption: "ssl",
|
||||||
|
timeout: 30,
|
||||||
|
testEmail: "",
|
||||||
|
},
|
||||||
|
sms: {
|
||||||
|
enabled: false,
|
||||||
|
provider: "aliyun",
|
||||||
|
access_key: "",
|
||||||
|
secret_key: "",
|
||||||
|
sign_name: "",
|
||||||
|
template_code: "",
|
||||||
|
backendUrl: "",
|
||||||
|
apiKey: "",
|
||||||
|
testPhone: "",
|
||||||
|
},
|
||||||
|
sitemsg: {
|
||||||
|
enabled: false,
|
||||||
|
retention_days: 30,
|
||||||
|
auto_read: false,
|
||||||
|
},
|
||||||
|
dingtalk: {
|
||||||
|
enabled: false,
|
||||||
|
webhook_url: "",
|
||||||
|
secret: "",
|
||||||
|
},
|
||||||
|
webhook: {
|
||||||
|
enabled: false,
|
||||||
|
url: "",
|
||||||
|
method: "POST",
|
||||||
|
token: "",
|
||||||
|
},
|
||||||
|
feishu: {
|
||||||
|
enabled: false,
|
||||||
|
webhook_url: "",
|
||||||
|
secret: "",
|
||||||
|
},
|
||||||
|
bark: {
|
||||||
|
enabled: false,
|
||||||
|
server_url: "https://api.day.app",
|
||||||
|
device_key: "",
|
||||||
|
},
|
||||||
|
wechat: {
|
||||||
|
enabled: false,
|
||||||
|
app_id: "",
|
||||||
|
app_secret: "",
|
||||||
|
template_id: "",
|
||||||
|
},
|
||||||
|
telegram: {
|
||||||
|
enabled: false,
|
||||||
|
bot_token: "",
|
||||||
|
chat_id: "",
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const testLoading = ref(false);
|
||||||
|
|
||||||
|
const emailTestLoading = ref(false);
|
||||||
|
|
||||||
|
const loadDraft = () => {
|
||||||
|
const raw = localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (!raw) return;
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(raw);
|
||||||
|
for (const key in data) {
|
||||||
|
if (formData[key] && typeof data[key] === 'object') {
|
||||||
|
Object.assign(formData[key], data[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("加载本地通知配置失败", e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadEmailConfig = async () => {
|
||||||
|
try {
|
||||||
|
const res = await getEmailInfo();
|
||||||
|
if (res.code === 200 && Array.isArray(res.data) && res.data.length > 0) {
|
||||||
|
const item = res.data[0];
|
||||||
|
formData.email.fromAddress = item.from_address || "";
|
||||||
|
formData.email.fromName = item.from_name || "";
|
||||||
|
formData.email.host = item.host || "";
|
||||||
|
formData.email.port = item.port != null ? String(item.port) : "";
|
||||||
|
formData.email.password = item.password || "";
|
||||||
|
formData.email.encryption = item.encryption || "ssl";
|
||||||
|
formData.email.timeout = item.timeout != null ? item.timeout : 30;
|
||||||
|
formData.email.enabled = Number(item.enabled ?? 0) === 1;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("加载邮箱配置失败:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadSmsConfig = async () => {
|
||||||
|
try {
|
||||||
|
const res = await getSmsInfo();
|
||||||
|
if (res.code === 200 && Array.isArray(res.data) && res.data.length > 0) {
|
||||||
|
const item = res.data[0];
|
||||||
|
formData.sms.backendUrl = item.backend_url || item.backendUrl || "";
|
||||||
|
formData.sms.apiKey = item.api_key || item.apiKey || "";
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("加载自定义短信配置失败:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveDraft = () => {
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(formData));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
const defaults = {
|
||||||
|
email: { enabled: false, fromAddress: "", fromName: "", host: "", port: "", password: "", encryption: "ssl", timeout: 30, testEmail: "" },
|
||||||
|
sms: { enabled: false, provider: "aliyun", access_key: "", secret_key: "", sign_name: "", template_code: "", backendUrl: "", apiKey: "", testPhone: "" },
|
||||||
|
sitemsg: { enabled: false, retention_days: 30, auto_read: false },
|
||||||
|
dingtalk: { enabled: false, webhook_url: "", secret: "" },
|
||||||
|
webhook: { enabled: false, url: "", method: "POST", token: "" },
|
||||||
|
feishu: { enabled: false, webhook_url: "", secret: "" },
|
||||||
|
bark: { enabled: false, server_url: "https://api.day.app", device_key: "" },
|
||||||
|
wechat: { enabled: false, app_id: "", app_secret: "", template_id: "" },
|
||||||
|
telegram: { enabled: false, bot_token: "", chat_id: "" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentTab = activeSubTab.value;
|
||||||
|
Object.assign(formData[currentTab], defaults[currentTab]);
|
||||||
|
saveDraft();
|
||||||
|
ElMessage.success("已重置当前配置");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSendEmailTest = async () => {
|
||||||
|
if (!formData.email.testEmail) {
|
||||||
|
ElMessage.warning("请先输入测试收件邮箱");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
emailTestLoading.value = true;
|
||||||
|
const payload = {
|
||||||
|
fromAddress: formData.email.fromAddress,
|
||||||
|
fromName: formData.email.fromName,
|
||||||
|
host: formData.email.host,
|
||||||
|
port: formData.email.port,
|
||||||
|
password: formData.email.password,
|
||||||
|
encryption: formData.email.encryption,
|
||||||
|
timeout: formData.email.timeout,
|
||||||
|
testEmail: formData.email.testEmail
|
||||||
|
};
|
||||||
|
const res = await sendTestEmail(payload);
|
||||||
|
if (res.code === 200) {
|
||||||
|
ElMessage.success("测试邮件发送成功");
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.msg || "测试邮件发送失败,请稍后重试");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error?.message || "测试邮件发送失败,请稍后重试");
|
||||||
|
} finally {
|
||||||
|
emailTestLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSendTest = async () => {
|
||||||
|
const phone = (formData.sms.testPhone || "").trim();
|
||||||
|
if (!phone) {
|
||||||
|
ElMessage.warning("请先输入测试收件手机号");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!/^\+\d{6,15}$/.test(phone)) {
|
||||||
|
ElMessage.warning("请使用国际格式号码(以 + 开头,后面为数字)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
testLoading.value = true;
|
||||||
|
const res = await sendTestSms({
|
||||||
|
backendUrl: formData.sms.backendUrl,
|
||||||
|
apiKey: formData.sms.apiKey,
|
||||||
|
phone
|
||||||
|
});
|
||||||
|
if (res.code === 200) {
|
||||||
|
ElMessage.success(res.msg || "短信测试任务入队成功");
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.msg || "短信测试失败,请稍后重试");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(e?.message || "短信测试失败,请稍后重试");
|
||||||
|
} finally {
|
||||||
|
testLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
submitting.value = true;
|
||||||
|
try {
|
||||||
|
if (activeSubTab.value === 'email') {
|
||||||
|
if (!formData.email.fromAddress || !formData.email.host || !formData.email.port) {
|
||||||
|
ElMessage.error("发件人邮箱、SMTP 服务器和 SMTP 端口不能为空");
|
||||||
|
submitting.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const res = await editEmailInfo({
|
||||||
|
fromAddress: formData.email.fromAddress,
|
||||||
|
fromName: formData.email.fromName,
|
||||||
|
host: formData.email.host,
|
||||||
|
port: formData.email.port,
|
||||||
|
password: formData.email.password,
|
||||||
|
encryption: formData.email.encryption,
|
||||||
|
timeout: formData.email.timeout,
|
||||||
|
enabled: formData.email.enabled ? 1 : 0
|
||||||
|
});
|
||||||
|
if (res.code === 200) {
|
||||||
|
ElMessage.success("邮箱配置保存成功");
|
||||||
|
} else {
|
||||||
|
throw new Error(res.msg || "保存邮箱配置失败");
|
||||||
|
}
|
||||||
|
} else if (activeSubTab.value === 'sms' && formData.sms.provider === 'custom') {
|
||||||
|
if (!formData.sms.backendUrl || !formData.sms.apiKey) {
|
||||||
|
ElMessage.error("自定义短信网关地址和API KEY不能为空");
|
||||||
|
submitting.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const res = await editSmsInfo({
|
||||||
|
backendUrl: formData.sms.backendUrl,
|
||||||
|
apiKey: formData.sms.apiKey
|
||||||
|
});
|
||||||
|
if (res.code === 200) {
|
||||||
|
ElMessage.success("自定义短信网关配置保存成功");
|
||||||
|
} else {
|
||||||
|
throw new Error(res.msg || "保存自定义短信配置失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveDraft();
|
||||||
|
ElMessage.success("保存配置成功");
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error(e?.message || "保存配置失败");
|
||||||
|
} finally {
|
||||||
|
submitting.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadDraft();
|
||||||
|
loadSmsConfig();
|
||||||
|
loadEmailConfig();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.notification-settings {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-content {
|
||||||
|
background-color: var(--el-bg-color-overlay);
|
||||||
|
border: 1px solid var(--el-border-color-light);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-tabs-container {
|
||||||
|
/* height: 520px; */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make tabs look extra sleek */
|
||||||
|
:deep(.el-tabs--left .el-tabs__header.is-left) {
|
||||||
|
margin-right: 0;
|
||||||
|
border-right: 1px solid var(--el-border-color-light);
|
||||||
|
background-color: var(--el-fill-color-blank);
|
||||||
|
width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-tabs--left .el-tabs__nav-wrap.is-left) {
|
||||||
|
padding-top: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-tabs__item) {
|
||||||
|
height: 44px;
|
||||||
|
line-height: 44px;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 0 20px !important;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-tabs__item:hover) {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
background-color: var(--el-fill-color-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-tabs__item.is-active) {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
background-color: var(--el-color-primary-light-9);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-tabs__active-bar) {
|
||||||
|
width: 3px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-label-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-label-item .el-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-pane-content {
|
||||||
|
padding: 24px;
|
||||||
|
height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-card {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-card__header) {
|
||||||
|
padding: 0 0 16px 0;
|
||||||
|
border-bottom: 1px dashed var(--el-border-color-light);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-tip {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
margin-top: 4px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-tip-inline {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -23,6 +23,14 @@
|
|||||||
v-if="activeTab === 'storage'"
|
v-if="activeTab === 'storage'"
|
||||||
/>
|
/>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<!-- 通知配置 -->
|
||||||
|
<el-tab-pane label="通知配置" name="notification">
|
||||||
|
<notificationSettings
|
||||||
|
ref="notificationSettingsRef"
|
||||||
|
v-if="activeTab === 'notification'"
|
||||||
|
/>
|
||||||
|
</el-tab-pane>
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -32,6 +40,7 @@
|
|||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import platformSettings from "./components/platformSettings.vue";
|
import platformSettings from "./components/platformSettings.vue";
|
||||||
import storageSettings from "./components/storageSettings.vue";
|
import storageSettings from "./components/storageSettings.vue";
|
||||||
|
import notificationSettings from "./components/notificationSettings.vue";
|
||||||
|
|
||||||
const activeTab = ref("platform");
|
const activeTab = ref("platform");
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -0,0 +1,3 @@
|
|||||||
|
<template>
|
||||||
|
<div>Notebook Component</div>
|
||||||
|
</template>
|
||||||
@ -1,148 +0,0 @@
|
|||||||
/**
|
|
||||||
* 记事本模块 API 测试脚本
|
|
||||||
* 在浏览器控制台中运行此脚本测试 API
|
|
||||||
*/
|
|
||||||
|
|
||||||
// 测试配置
|
|
||||||
const API_BASE = '/platform/notebook';
|
|
||||||
|
|
||||||
// 辅助函数:发送请求
|
|
||||||
async function request(url, options = {}) {
|
|
||||||
const token = localStorage.getItem('token');
|
|
||||||
const headers = {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': token ? `Bearer ${token}` : '',
|
|
||||||
...options.headers,
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(url, { ...options, headers });
|
|
||||||
const data = await response.json();
|
|
||||||
console.log(`✅ ${options.method || 'GET'} ${url}`, data);
|
|
||||||
return data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`❌ ${options.method || 'GET'} ${url}`, error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 测试函数
|
|
||||||
const NotebookTest = {
|
|
||||||
// 1. 创建笔记
|
|
||||||
async create() {
|
|
||||||
return await request(`${API_BASE}/create`, {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({
|
|
||||||
title: '测试笔记 - ' + new Date().toLocaleString(),
|
|
||||||
content: '<p>这是一条测试笔记</p><p>内容包含<strong>富文本</strong>格式</p>',
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// 2. 获取笔记列表
|
|
||||||
async list(params = {}) {
|
|
||||||
const query = new URLSearchParams({
|
|
||||||
page: params.page || 1,
|
|
||||||
pageSize: params.pageSize || 20,
|
|
||||||
keyword: params.keyword || '',
|
|
||||||
}).toString();
|
|
||||||
return await request(`${API_BASE}/list?${query}`);
|
|
||||||
},
|
|
||||||
|
|
||||||
// 3. 获取笔记详情
|
|
||||||
async detail(id) {
|
|
||||||
return await request(`${API_BASE}/detail/${id}`);
|
|
||||||
},
|
|
||||||
|
|
||||||
// 4. 更新笔记
|
|
||||||
async update(id, data) {
|
|
||||||
return await request(`${API_BASE}/update/${id}`, {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// 5. 删除笔记
|
|
||||||
async delete(id) {
|
|
||||||
return await request(`${API_BASE}/delete/${id}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// 完整测试流程
|
|
||||||
async runFullTest() {
|
|
||||||
console.log('🚀 开始测试记事本模块 API...\n');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1. 创建笔记
|
|
||||||
console.log('📝 测试1: 创建笔记');
|
|
||||||
const createResult = await this.create();
|
|
||||||
if (createResult.code !== 200) {
|
|
||||||
throw new Error('创建笔记失败');
|
|
||||||
}
|
|
||||||
const noteId = createResult.data.id;
|
|
||||||
console.log(`✅ 创建成功,笔记ID: ${noteId}\n`);
|
|
||||||
|
|
||||||
// 2. 获取笔记列表
|
|
||||||
console.log('📋 测试2: 获取笔记列表');
|
|
||||||
const listResult = await this.list();
|
|
||||||
if (listResult.code !== 200) {
|
|
||||||
throw new Error('获取列表失败');
|
|
||||||
}
|
|
||||||
console.log(`✅ 获取成功,共 ${listResult.data.total} 条笔记\n`);
|
|
||||||
|
|
||||||
// 3. 获取笔记详情
|
|
||||||
console.log('🔍 测试3: 获取笔记详情');
|
|
||||||
const detailResult = await this.detail(noteId);
|
|
||||||
if (detailResult.code !== 200) {
|
|
||||||
throw new Error('获取详情失败');
|
|
||||||
}
|
|
||||||
console.log(`✅ 获取成功,标题: ${detailResult.data.title}\n`);
|
|
||||||
|
|
||||||
// 4. 更新笔记
|
|
||||||
console.log('✏️ 测试4: 更新笔记');
|
|
||||||
const updateResult = await this.update(noteId, {
|
|
||||||
title: '更新后的标题 - ' + new Date().toLocaleString(),
|
|
||||||
content: '<p>这是更新后的内容</p>',
|
|
||||||
});
|
|
||||||
if (updateResult.code !== 200) {
|
|
||||||
throw new Error('更新笔记失败');
|
|
||||||
}
|
|
||||||
console.log(`✅ 更新成功\n`);
|
|
||||||
|
|
||||||
// 5. 搜索笔记
|
|
||||||
console.log('🔎 测试5: 搜索笔记');
|
|
||||||
const searchResult = await this.list({ keyword: '更新' });
|
|
||||||
if (searchResult.code !== 200) {
|
|
||||||
throw new Error('搜索失败');
|
|
||||||
}
|
|
||||||
console.log(`✅ 搜索成功,找到 ${searchResult.data.total} 条匹配笔记\n`);
|
|
||||||
|
|
||||||
// 6. 删除笔记
|
|
||||||
console.log('🗑️ 测试6: 删除笔记');
|
|
||||||
const deleteResult = await this.delete(noteId);
|
|
||||||
if (deleteResult.code !== 200) {
|
|
||||||
throw new Error('删除笔记失败');
|
|
||||||
}
|
|
||||||
console.log(`✅ 删除成功\n`);
|
|
||||||
|
|
||||||
console.log('🎉 所有测试通过!');
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ 测试失败:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// 导出到全局
|
|
||||||
window.NotebookTest = NotebookTest;
|
|
||||||
|
|
||||||
console.log('📚 记事本模块测试工具已加载');
|
|
||||||
console.log('使用方法:');
|
|
||||||
console.log(' NotebookTest.create() - 创建笔记');
|
|
||||||
console.log(' NotebookTest.list() - 获取列表');
|
|
||||||
console.log(' NotebookTest.detail(id) - 获取详情');
|
|
||||||
console.log(' NotebookTest.update(id, data) - 更新笔记');
|
|
||||||
console.log(' NotebookTest.delete(id) - 删除笔记');
|
|
||||||
console.log(' NotebookTest.runFullTest() - 运行完整测试');
|
|
||||||
148
platform/src/views/tools/reminder/components/detail.vue
Normal file
148
platform/src/views/tools/reminder/components/detail.vue
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
<template>
|
||||||
|
<el-drawer v-model="visible" title="提醒详情" size="520px" destroy-on-close>
|
||||||
|
<div v-if="detail" class="detail-wrap" v-loading="loading">
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="ID" label-width="110px">
|
||||||
|
{{ detail.id }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="状态" label-width="110px">
|
||||||
|
<el-tag :type="detail.status === 1 ? 'success' : 'info'" size="small">
|
||||||
|
{{ detail.status === 1 ? '启用' : '禁用' }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item label="标题" :span="2" label-width="110px">
|
||||||
|
{{ detail.title }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item label="类型" label-width="110px">
|
||||||
|
<el-tag :type="typeTagType(detail.type)" size="small">
|
||||||
|
{{ typeText(detail.type) }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="接收范围" label-width="110px">
|
||||||
|
<el-tag type="info" size="small">{{ targetTypeText(detail.targetType) }}</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item
|
||||||
|
v-if="[2, 3].includes(detail.targetType)"
|
||||||
|
label="接收 ID"
|
||||||
|
:span="2"
|
||||||
|
label-width="110px"
|
||||||
|
>
|
||||||
|
{{ detail.targetIds || '—' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item label="提醒时间" label-width="110px">
|
||||||
|
{{ detail.remindTime || '—' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="是否重复" label-width="110px">
|
||||||
|
<el-tag :type="detail.isRepeat === 1 ? 'warning' : 'info'" size="small">
|
||||||
|
{{ detail.isRepeat === 1 ? '是' : '否' }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item
|
||||||
|
v-if="detail.isRepeat === 1"
|
||||||
|
label="重复周期"
|
||||||
|
label-width="110px"
|
||||||
|
>
|
||||||
|
{{ repeatCycleText(detail.repeatCycle) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item label="内容" :span="2" label-width="110px">
|
||||||
|
<div class="content-text">{{ detail.content || '—' }}</div>
|
||||||
|
</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item v-if="detail.remark" label="备注" :span="2" label-width="110px">
|
||||||
|
<div class="content-text">{{ detail.remark }}</div>
|
||||||
|
</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item label="创建时间" label-width="110px">
|
||||||
|
{{ detail.createTime || '—' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="更新时间" label-width="110px">
|
||||||
|
{{ detail.updateTime || '—' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="loading" class="empty-wrap">
|
||||||
|
<el-skeleton :rows="8" animated />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="empty-wrap">
|
||||||
|
<el-empty description="暂无数据" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="visible = false">关闭</el-button>
|
||||||
|
</template>
|
||||||
|
</el-drawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { getReminderDetail } from '@/api/reminder'
|
||||||
|
|
||||||
|
const visible = ref(false)
|
||||||
|
const loading = ref(false)
|
||||||
|
const detail = ref(null)
|
||||||
|
|
||||||
|
function typeText(type) {
|
||||||
|
const map = { 1: '系统通知', 2: '待办提醒', 3: '活动提醒', 4: '自定义' }
|
||||||
|
return map[type] ?? '未知'
|
||||||
|
}
|
||||||
|
|
||||||
|
function typeTagType(type) {
|
||||||
|
const map = { 1: 'primary', 2: 'warning', 3: 'success', 4: '' }
|
||||||
|
return map[type] ?? 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
function targetTypeText(targetType) {
|
||||||
|
const map = { 1: '全体用户', 2: '指定用户', 3: '指定角色', 4: '仅管理员' }
|
||||||
|
return map[targetType] ?? '—'
|
||||||
|
}
|
||||||
|
|
||||||
|
function repeatCycleText(cycle) {
|
||||||
|
const map = { daily: '每天', weekly: '每周', monthly: '每月' }
|
||||||
|
return map[cycle] ?? cycle ?? '—'
|
||||||
|
}
|
||||||
|
|
||||||
|
async function open(id) {
|
||||||
|
detail.value = null
|
||||||
|
visible.value = true
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await getReminderDetail(id)
|
||||||
|
if (res?.code === 200 && res.data) {
|
||||||
|
detail.value = res.data
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res?.msg || '加载失败')
|
||||||
|
visible.value = false
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ open })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.detail-wrap {
|
||||||
|
padding: 4px 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-text {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
color: #606266;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-wrap {
|
||||||
|
padding: 40px 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
272
platform/src/views/tools/reminder/components/edit.vue
Normal file
272
platform/src/views/tools/reminder/components/edit.vue
Normal file
@ -0,0 +1,272 @@
|
|||||||
|
<template>
|
||||||
|
<el-drawer
|
||||||
|
v-model="visible"
|
||||||
|
:title="isAdd ? '新增提醒' : '编辑提醒'"
|
||||||
|
size="520px"
|
||||||
|
destroy-on-close
|
||||||
|
@closed="onClosed"
|
||||||
|
>
|
||||||
|
<el-form
|
||||||
|
ref="formRef"
|
||||||
|
:model="form"
|
||||||
|
:rules="rules"
|
||||||
|
label-width="100px"
|
||||||
|
v-loading="loading"
|
||||||
|
>
|
||||||
|
<el-form-item label="标题" prop="title">
|
||||||
|
<el-input v-model="form.title" placeholder="请输入提醒标题" maxlength="100" show-word-limit />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="类型" prop="type">
|
||||||
|
<el-select v-model="form.type" placeholder="请选择类型" style="width: 100%">
|
||||||
|
<el-option label="系统通知" :value="1" />
|
||||||
|
<el-option label="待办提醒" :value="2" />
|
||||||
|
<el-option label="活动提醒" :value="3" />
|
||||||
|
<el-option label="自定义" :value="4" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="内容" prop="content">
|
||||||
|
<el-input
|
||||||
|
v-model="form.content"
|
||||||
|
type="textarea"
|
||||||
|
:rows="5"
|
||||||
|
placeholder="请输入提醒内容"
|
||||||
|
maxlength="500"
|
||||||
|
show-word-limit
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="提醒时间" prop="remindTime">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="form.remindTime"
|
||||||
|
type="datetime"
|
||||||
|
placeholder="请选择提醒时间"
|
||||||
|
format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
value-format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="接收范围" prop="targetType">
|
||||||
|
<el-select v-model="form.targetType" placeholder="请选择接收范围" style="width: 100%">
|
||||||
|
<el-option label="全体用户" :value="1" />
|
||||||
|
<el-option label="指定用户" :value="2" />
|
||||||
|
<el-option label="指定角色" :value="3" />
|
||||||
|
<el-option label="仅管理员" :value="4" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- 指定用户时展示 -->
|
||||||
|
<el-form-item
|
||||||
|
v-if="form.targetType === 2"
|
||||||
|
label="指定用户"
|
||||||
|
prop="targetIds"
|
||||||
|
>
|
||||||
|
<el-input
|
||||||
|
v-model="form.targetIds"
|
||||||
|
placeholder="多个用户 ID,用英文逗号分隔,如:1,2,3"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- 指定角色时展示 -->
|
||||||
|
<el-form-item
|
||||||
|
v-if="form.targetType === 3"
|
||||||
|
label="指定角色"
|
||||||
|
prop="targetIds"
|
||||||
|
>
|
||||||
|
<el-input
|
||||||
|
v-model="form.targetIds"
|
||||||
|
placeholder="多个角色 ID,用英文逗号分隔,如:1,2,3"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="是否重复">
|
||||||
|
<el-switch v-model="form.isRepeat" :active-value="1" :inactive-value="0" />
|
||||||
|
<span class="form-tip">开启后按下方周期重复发送</span>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item v-if="form.isRepeat === 1" label="重复周期" prop="repeatCycle">
|
||||||
|
<el-select v-model="form.repeatCycle" placeholder="请选择" style="width: 100%">
|
||||||
|
<el-option label="每天" value="daily" />
|
||||||
|
<el-option label="每周" value="weekly" />
|
||||||
|
<el-option label="每月" value="monthly" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="备注">
|
||||||
|
<el-input
|
||||||
|
v-model="form.remark"
|
||||||
|
type="textarea"
|
||||||
|
:rows="2"
|
||||||
|
placeholder="选填"
|
||||||
|
maxlength="200"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="状态">
|
||||||
|
<el-radio-group v-model="form.status">
|
||||||
|
<el-radio-button :value="1">启用</el-radio-button>
|
||||||
|
<el-radio-button :value="0">禁用</el-radio-button>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="visible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="saving" @click="submit">保存</el-button>
|
||||||
|
</template>
|
||||||
|
</el-drawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, nextTick } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { getReminderDetail, createReminder, updateReminder } from '@/api/reminder'
|
||||||
|
|
||||||
|
const emit = defineEmits(['saved'])
|
||||||
|
|
||||||
|
const visible = ref(false)
|
||||||
|
const loading = ref(false)
|
||||||
|
const saving = ref(false)
|
||||||
|
const isAdd = ref(true)
|
||||||
|
const formRef = ref(null)
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
id: 0,
|
||||||
|
title: '',
|
||||||
|
type: 1,
|
||||||
|
content: '',
|
||||||
|
remindTime: '',
|
||||||
|
targetType: 1,
|
||||||
|
targetIds: '',
|
||||||
|
isRepeat: 0,
|
||||||
|
repeatCycle: '',
|
||||||
|
remark: '',
|
||||||
|
status: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
|
||||||
|
type: [{ required: true, message: '请选择类型', trigger: 'change' }],
|
||||||
|
content: [{ required: true, message: '请输入内容', trigger: 'blur' }],
|
||||||
|
remindTime: [{ required: true, message: '请选择提醒时间', trigger: 'change' }],
|
||||||
|
targetType: [{ required: true, message: '请选择接收范围', trigger: 'change' }],
|
||||||
|
repeatCycle: [
|
||||||
|
{
|
||||||
|
validator: (rule, value, callback) => {
|
||||||
|
if (form.isRepeat === 1 && !value) {
|
||||||
|
callback(new Error('请选择重复周期'))
|
||||||
|
} else {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trigger: 'change',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetForm() {
|
||||||
|
form.id = 0
|
||||||
|
form.title = ''
|
||||||
|
form.type = 1
|
||||||
|
form.content = ''
|
||||||
|
form.remindTime = ''
|
||||||
|
form.targetType = 1
|
||||||
|
form.targetIds = ''
|
||||||
|
form.isRepeat = 0
|
||||||
|
form.repeatCycle = ''
|
||||||
|
form.remark = ''
|
||||||
|
form.status = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
async function open(id) {
|
||||||
|
resetForm()
|
||||||
|
isAdd.value = !id
|
||||||
|
visible.value = true
|
||||||
|
await nextTick()
|
||||||
|
formRef.value?.clearValidate?.()
|
||||||
|
|
||||||
|
if (id) {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await getReminderDetail(id)
|
||||||
|
if (res?.code !== 200 || !res.data) {
|
||||||
|
ElMessage.error(res?.msg || '加载失败')
|
||||||
|
visible.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const d = res.data
|
||||||
|
form.id = d.id
|
||||||
|
form.title = d.title || ''
|
||||||
|
form.type = d.type ?? 1
|
||||||
|
form.content = d.content || ''
|
||||||
|
form.remindTime = d.remindTime || ''
|
||||||
|
form.targetType = d.targetType ?? 1
|
||||||
|
form.targetIds = d.targetIds || ''
|
||||||
|
form.isRepeat = d.isRepeat ?? 0
|
||||||
|
form.repeatCycle = d.repeatCycle || ''
|
||||||
|
form.remark = d.remark || ''
|
||||||
|
form.status = d.status ?? 1
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClosed() {
|
||||||
|
resetForm()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submit() {
|
||||||
|
if (!formRef.value) return
|
||||||
|
try {
|
||||||
|
await formRef.value.validate()
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
title: form.title,
|
||||||
|
type: form.type,
|
||||||
|
content: form.content,
|
||||||
|
remindTime: form.remindTime,
|
||||||
|
targetType: form.targetType,
|
||||||
|
targetIds: [2, 3].includes(form.targetType) ? form.targetIds : undefined,
|
||||||
|
isRepeat: form.isRepeat,
|
||||||
|
repeatCycle: form.isRepeat === 1 ? form.repeatCycle : undefined,
|
||||||
|
remark: form.remark || undefined,
|
||||||
|
status: form.status,
|
||||||
|
}
|
||||||
|
|
||||||
|
let res
|
||||||
|
if (isAdd.value) {
|
||||||
|
res = await createReminder(payload)
|
||||||
|
} else {
|
||||||
|
res = await updateReminder(form.id, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res?.code === 200) {
|
||||||
|
ElMessage.success(isAdd.value ? '新增成功' : '保存成功')
|
||||||
|
visible.value = false
|
||||||
|
emit('saved')
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res?.msg || '操作失败')
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ open })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.form-tip {
|
||||||
|
margin-left: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
318
platform/src/views/tools/reminder/index.vue
Normal file
318
platform/src/views/tools/reminder/index.vue
Normal file
@ -0,0 +1,318 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container-box">
|
||||||
|
<div class="header-bar">
|
||||||
|
<h2>提醒管理</h2>
|
||||||
|
<div class="header-actions">
|
||||||
|
<el-button type="primary" @click="editRef.open()">
|
||||||
|
<el-icon><Plus /></el-icon>
|
||||||
|
新增提醒
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="fetchList" :loading="loading">
|
||||||
|
<el-icon><Refresh /></el-icon>
|
||||||
|
刷新
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-divider />
|
||||||
|
|
||||||
|
<!-- 搜索筛选 -->
|
||||||
|
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||||
|
<el-form-item label="关键词">
|
||||||
|
<el-input
|
||||||
|
v-model="searchForm.keyword"
|
||||||
|
placeholder="标题/内容"
|
||||||
|
clearable
|
||||||
|
style="width: 200px"
|
||||||
|
@keyup.enter="handleSearch"
|
||||||
|
@clear="handleSearch"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="类型">
|
||||||
|
<el-select
|
||||||
|
v-model="searchForm.type"
|
||||||
|
placeholder="全部"
|
||||||
|
clearable
|
||||||
|
style="width: 130px"
|
||||||
|
>
|
||||||
|
<el-option label="系统通知" :value="1" />
|
||||||
|
<el-option label="待办提醒" :value="2" />
|
||||||
|
<el-option label="活动提醒" :value="3" />
|
||||||
|
<el-option label="自定义" :value="4" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态">
|
||||||
|
<el-select
|
||||||
|
v-model="searchForm.status"
|
||||||
|
placeholder="全部"
|
||||||
|
clearable
|
||||||
|
style="width: 110px"
|
||||||
|
>
|
||||||
|
<el-option label="启用" :value="1" />
|
||||||
|
<el-option label="禁用" :value="0" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="提醒时间">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="dateRange"
|
||||||
|
type="daterange"
|
||||||
|
range-separator="至"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
style="width: 240px"
|
||||||
|
@change="handleSearch"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleSearch">
|
||||||
|
<el-icon><Search /></el-icon>
|
||||||
|
查询
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="resetSearch">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<!-- 批量操作 -->
|
||||||
|
<div v-if="selectedIds.length > 0" class="batch-bar">
|
||||||
|
<span class="selected-tip">已选 {{ selectedIds.length }} 条</span>
|
||||||
|
<el-button type="danger" size="small" @click="handleBatchDelete">批量删除</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 列表表格 -->
|
||||||
|
<el-table
|
||||||
|
:data="list"
|
||||||
|
v-loading="loading"
|
||||||
|
border
|
||||||
|
style="width: 100%"
|
||||||
|
@selection-change="handleSelectionChange"
|
||||||
|
>
|
||||||
|
<el-table-column type="selection" width="50" align="center" />
|
||||||
|
<el-table-column prop="id" label="ID" width="70" align="center" />
|
||||||
|
<el-table-column prop="title" label="标题" min-width="160" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="type" label="类型" width="110" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="typeTagType(row.type)" size="small">{{ typeText(row.type) }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="content" label="内容摘要" min-width="200" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="remindTime" label="提醒时间" width="170" align="center" />
|
||||||
|
<el-table-column prop="targetType" label="接收范围" width="110" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag type="info" size="small">{{ targetTypeText(row.targetType) }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="status" label="状态" width="90" align="center">
|
||||||
|
<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 prop="createTime" label="创建时间" width="170" align="center" />
|
||||||
|
<el-table-column label="操作" width="150" align="center" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button text type="primary" size="small" @click="handleViewDetail(row)">详情</el-button>
|
||||||
|
<el-button text type="primary" size="small" @click="editRef.open(row.id)">编辑</el-button>
|
||||||
|
<el-button text type="danger" size="small" @click="handleDelete(row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pager">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pagination.page"
|
||||||
|
v-model:page-size="pagination.pageSize"
|
||||||
|
:total="pagination.total"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@current-change="fetchList"
|
||||||
|
@size-change="fetchList"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 新增/编辑抽屉 -->
|
||||||
|
<ReminderEdit ref="editRef" @saved="fetchList" />
|
||||||
|
|
||||||
|
<!-- 详情抽屉 -->
|
||||||
|
<ReminderDetail ref="detailRef" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { Plus, Refresh, Search } from '@element-plus/icons-vue'
|
||||||
|
import { getReminderList, deleteReminder, batchDeleteReminder } from '@/api/reminder'
|
||||||
|
import ReminderEdit from './components/edit.vue'
|
||||||
|
import ReminderDetail from './components/detail.vue'
|
||||||
|
|
||||||
|
const editRef = ref(null)
|
||||||
|
const detailRef = ref(null)
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const list = ref([])
|
||||||
|
const selectedIds = ref([])
|
||||||
|
const dateRange = ref(null)
|
||||||
|
|
||||||
|
const searchForm = reactive({
|
||||||
|
keyword: '',
|
||||||
|
type: null,
|
||||||
|
status: null,
|
||||||
|
startDate: '',
|
||||||
|
endDate: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const pagination = reactive({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
total: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 类型文本
|
||||||
|
function typeText(type) {
|
||||||
|
const map = { 1: '系统通知', 2: '待办提醒', 3: '活动提醒', 4: '自定义' }
|
||||||
|
return map[type] ?? '未知'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 类型标签颜色
|
||||||
|
function typeTagType(type) {
|
||||||
|
const map = { 1: 'primary', 2: 'warning', 3: 'success', 4: '' }
|
||||||
|
return map[type] ?? 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 接收范围文本
|
||||||
|
function targetTypeText(targetType) {
|
||||||
|
const map = { 1: '全体用户', 2: '指定用户', 3: '指定角色', 4: '仅管理员' }
|
||||||
|
return map[targetType] ?? '—'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取列表
|
||||||
|
async function fetchList() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
page: pagination.page,
|
||||||
|
pageSize: pagination.pageSize,
|
||||||
|
keyword: searchForm.keyword || undefined,
|
||||||
|
type: searchForm.type != null ? searchForm.type : undefined,
|
||||||
|
status: searchForm.status != null ? searchForm.status : undefined,
|
||||||
|
startDate: searchForm.startDate || undefined,
|
||||||
|
endDate: searchForm.endDate || undefined,
|
||||||
|
}
|
||||||
|
const res = await getReminderList(params)
|
||||||
|
if (res?.code === 200 && res.data) {
|
||||||
|
list.value = res.data.list || []
|
||||||
|
pagination.total = res.data.total ?? 0
|
||||||
|
} else {
|
||||||
|
list.value = []
|
||||||
|
ElMessage.error(res?.msg || '加载失败')
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
function handleSearch() {
|
||||||
|
if (dateRange.value && dateRange.value.length === 2) {
|
||||||
|
searchForm.startDate = dateRange.value[0]
|
||||||
|
searchForm.endDate = dateRange.value[1]
|
||||||
|
} else {
|
||||||
|
searchForm.startDate = ''
|
||||||
|
searchForm.endDate = ''
|
||||||
|
}
|
||||||
|
pagination.page = 1
|
||||||
|
fetchList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置搜索
|
||||||
|
function resetSearch() {
|
||||||
|
searchForm.keyword = ''
|
||||||
|
searchForm.type = null
|
||||||
|
searchForm.status = null
|
||||||
|
searchForm.startDate = ''
|
||||||
|
searchForm.endDate = ''
|
||||||
|
dateRange.value = null
|
||||||
|
pagination.page = 1
|
||||||
|
fetchList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 多选变化
|
||||||
|
function handleSelectionChange(rows) {
|
||||||
|
selectedIds.value = rows.map((r) => r.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看详情
|
||||||
|
function handleViewDetail(row) {
|
||||||
|
detailRef.value?.open(row.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除单条
|
||||||
|
async function handleDelete(row) {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确定删除提醒「${row.title}」吗?`, '提示', { type: 'warning' })
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const res = await deleteReminder(row.id)
|
||||||
|
if (res?.code === 200) {
|
||||||
|
ElMessage.success('已删除')
|
||||||
|
fetchList()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res?.msg || '删除失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量删除
|
||||||
|
async function handleBatchDelete() {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确定批量删除选中的 ${selectedIds.value.length} 条提醒吗?`, '提示', {
|
||||||
|
type: 'warning',
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const res = await batchDeleteReminder(selectedIds.value)
|
||||||
|
if (res?.code === 200) {
|
||||||
|
ElMessage.success('已批量删除')
|
||||||
|
selectedIds.value = []
|
||||||
|
fetchList()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res?.msg || '批量删除失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchList()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.batch-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: #ecf5ff;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #b3d8ff;
|
||||||
|
|
||||||
|
.selected-tip {
|
||||||
|
color: #409eff;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pager {
|
||||||
|
margin-top: 16px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Loading…
Reference in New Issue
Block a user