优化卡片
This commit is contained in:
1 parent
e34543465a
commit
8acbe732ff
8 files changed
+181
-21
No files matched your search
@@ -99,8 +99,19 @@ export const useTabsStore = defineTabsStore('tabs', () => {
|
|||||||
function setDefaultTab(tab) {
|
function setDefaultTab(tab) {
|
||||||
const path = tab?.fullPath || '';
|
const path = tab?.fullPath || '';
|
||||||
if (!path) return;
|
if (!path) return;
|
||||||
|
const prevDefault = defaultDashboardPath.value;
|
||||||
defaultDashboardPath.value = path;
|
defaultDashboardPath.value = path;
|
||||||
|
|
||||||
|
// 默认工作台路径变化 = 切换了模块:关闭旧模块的全部 tab,
|
||||||
|
// 只保留新模块的默认工作台。
|
||||||
|
// prevDefault 为空(首次设置,如刷新后恢复)时不清空,保留恢复出来的 tabs。
|
||||||
|
if (prevDefault && prevDefault !== path) {
|
||||||
|
tabList.value = [tab];
|
||||||
|
activeTab.value = path;
|
||||||
|
saveTabsToStorage(tabList.value, activeTab.value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const idx = tabList.value.findIndex(t => t.fullPath === path);
|
const idx = tabList.value.findIndex(t => t.fullPath === path);
|
||||||
if (idx === -1) {
|
if (idx === -1) {
|
||||||
tabList.value.unshift(tab);
|
tabList.value.unshift(tab);
|
||||||
|
|||||||
@@ -1253,29 +1253,77 @@ onUnmounted(() => {
|
|||||||
border-color: #3d3d3d;
|
border-color: #3d3d3d;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 暗色主题下未开通卡片
|
// 暗色主题 hover:用深灰浮起替代亮色主题的「蓝底白字」效果
|
||||||
&.disabled {
|
&:hover {
|
||||||
background: #222;
|
transform: translateY(-2px);
|
||||||
border-color: #333;
|
background: #222222;
|
||||||
|
border-color: #4d4d4d;
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
|
||||||
|
|
||||||
.card-title {
|
.card-title {
|
||||||
color: #666 !important;
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-desc {
|
||||||
|
color: #a8abb2 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-icon-wrapper {
|
.card-icon-wrapper {
|
||||||
color: #555;
|
color: var(--yz-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-divider {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暗色主题下未开通卡片
|
||||||
|
&.disabled {
|
||||||
|
background: #222222;
|
||||||
|
border-color: #333333;
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
color: #666666 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-icon-wrapper {
|
||||||
|
color: #555555;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-divider {
|
.card-divider {
|
||||||
border-color: #2e2e2e;
|
border-color: #2e2e2e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 未开通卡片 hover:保持置灰,不闪浅色底也不浮起
|
||||||
|
&:hover {
|
||||||
|
transform: none;
|
||||||
|
background: #222222;
|
||||||
|
border-color: #333333;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
color: #666666 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-desc {
|
||||||
|
color: #6b6b6b !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-icon-wrapper {
|
||||||
|
color: #555555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-divider {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.card-locked .lock-icon {
|
.card-locked .lock-icon {
|
||||||
color: #555;
|
color: #555555;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-locked .lock-tip {
|
.card-locked .lock-tip {
|
||||||
color: #666;
|
color: #666666;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1304,6 +1352,32 @@ onUnmounted(() => {
|
|||||||
color: #8c8c8c !important;
|
color: #8c8c8c !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 暗色主题 hover:深灰浮起,替代亮蓝底
|
||||||
|
.modules-section .module-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
background: #222222;
|
||||||
|
border-color: #4d4d4d;
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modules-section .module-card:hover .card-desc {
|
||||||
|
color: #a8abb2 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 未开通卡片 hover:保持置灰不浮起
|
||||||
|
.modules-section .module-card.disabled:hover,
|
||||||
|
.modules-section .module-card.is-locked:hover {
|
||||||
|
transform: none;
|
||||||
|
background: #1f1f1f;
|
||||||
|
border-color: #333333;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modules-section .module-card.disabled:hover .card-desc,
|
||||||
|
.modules-section .module-card.is-locked:hover .card-desc {
|
||||||
|
color: #6b6b6b !important;
|
||||||
|
}
|
||||||
|
|
||||||
.modules-section .module-card.is-locked {
|
.modules-section .module-card.is-locked {
|
||||||
background: #1f1f1f;
|
background: #1f1f1f;
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,8 @@ const channelTextMap = {
|
|||||||
EMAIL: '邮件',
|
EMAIL: '邮件',
|
||||||
BARK: 'Bark 推送',
|
BARK: 'Bark 推送',
|
||||||
SITE_MSG: '站内信',
|
SITE_MSG: '站内信',
|
||||||
|
WECHAT_MP: '微信服务号',
|
||||||
|
WECHAT_MINI: '小程序订阅',
|
||||||
}
|
}
|
||||||
|
|
||||||
const channelColorMap = {
|
const channelColorMap = {
|
||||||
@@ -82,6 +84,8 @@ const channelColorMap = {
|
|||||||
EMAIL: 'warning',
|
EMAIL: 'warning',
|
||||||
BARK: 'danger',
|
BARK: 'danger',
|
||||||
SITE_MSG: 'primary',
|
SITE_MSG: 'primary',
|
||||||
|
WECHAT_MP: 'success',
|
||||||
|
WECHAT_MINI: 'success',
|
||||||
}
|
}
|
||||||
|
|
||||||
function channelText(ch) {
|
function channelText(ch) {
|
||||||
|
|||||||
@@ -43,7 +43,12 @@
|
|||||||
<el-checkbox value="EMAIL">邮件 (EMAIL)</el-checkbox>
|
<el-checkbox value="EMAIL">邮件 (EMAIL)</el-checkbox>
|
||||||
<el-checkbox value="BARK">Bark 推送</el-checkbox>
|
<el-checkbox value="BARK">Bark 推送</el-checkbox>
|
||||||
<el-checkbox value="SITE_MSG">站内信 (SITE_MSG)</el-checkbox>
|
<el-checkbox value="SITE_MSG">站内信 (SITE_MSG)</el-checkbox>
|
||||||
|
<el-checkbox value="WECHAT_MP">微信服务号</el-checkbox>
|
||||||
|
<el-checkbox value="WECHAT_MINI">小程序订阅</el-checkbox>
|
||||||
</el-checkbox-group>
|
</el-checkbox-group>
|
||||||
|
<span class="form-tip">
|
||||||
|
微信服务号:需认证服务号且在个人中心完成账号绑定并关注;小程序订阅:需在小程序内授权过订阅消息(一次授权仅可发送一条)。两者均为一次性发送。
|
||||||
|
</span>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="提前提醒分钟" prop="advance_minutes">
|
<el-form-item label="提前提醒分钟" prop="advance_minutes">
|
||||||
@@ -200,7 +205,7 @@ async function handleTest() {
|
|||||||
|
|
||||||
if (res?.code === 200 && Array.isArray(res.data)) {
|
if (res?.code === 200 && Array.isArray(res.data)) {
|
||||||
const lines = res.data.map(item => {
|
const lines = res.data.map(item => {
|
||||||
const name = { SMS: '短信', EMAIL: '邮件', BARK: 'Bark推送', SITE_MSG: '站内信' }[item.channel] || item.channel
|
const name = { SMS: '短信', EMAIL: '邮件', BARK: 'Bark推送', SITE_MSG: '站内信', WECHAT_MP: '微信服务号', WECHAT_MINI: '小程序订阅' }[item.channel] || item.channel
|
||||||
const status = item.success
|
const status = item.success
|
||||||
? '<span style="color: #67C23A; font-weight: bold;">发送成功</span>'
|
? '<span style="color: #67C23A; font-weight: bold;">发送成功</span>'
|
||||||
: `<span style="color: #F56C6C; font-weight: bold;">发送失败 (${item.msg})</span>`
|
: `<span style="color: #F56C6C; font-weight: bold;">发送失败 (${item.msg})</span>`
|
||||||
|
|||||||
@@ -168,6 +168,8 @@ const channelTextMap = {
|
|||||||
EMAIL: '邮件',
|
EMAIL: '邮件',
|
||||||
BARK: 'Bark',
|
BARK: 'Bark',
|
||||||
SITE_MSG: '站内信',
|
SITE_MSG: '站内信',
|
||||||
|
WECHAT_MP: '微信服务号',
|
||||||
|
WECHAT_MINI: '小程序订阅',
|
||||||
}
|
}
|
||||||
|
|
||||||
const channelColorMap = {
|
const channelColorMap = {
|
||||||
@@ -175,6 +177,8 @@ const channelColorMap = {
|
|||||||
EMAIL: 'warning',
|
EMAIL: 'warning',
|
||||||
BARK: 'danger',
|
BARK: 'danger',
|
||||||
SITE_MSG: 'primary',
|
SITE_MSG: 'primary',
|
||||||
|
WECHAT_MP: 'success',
|
||||||
|
WECHAT_MINI: 'success',
|
||||||
}
|
}
|
||||||
|
|
||||||
function channelText(ch) {
|
function channelText(ch) {
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
appname = server
|
appname = server
|
||||||
httpport = 9000
|
httpport = 9000
|
||||||
runmode = dev
|
runmode = dev
|
||||||
|
|
||||||
|
|||||||
@@ -287,7 +287,7 @@ func (c *BackendReminderController) CreateReminder() {
|
|||||||
|
|
||||||
for _, ch := range p.RemindChannels {
|
for _, ch := range p.RemindChannels {
|
||||||
ch = strings.ToUpper(strings.TrimSpace(ch))
|
ch = strings.ToUpper(strings.TrimSpace(ch))
|
||||||
if ch != "SMS" && ch != "EMAIL" && ch != "BARK" && ch != "SITE_MSG" {
|
if ch != "SMS" && ch != "EMAIL" && ch != "BARK" && ch != "SITE_MSG" && ch != "WECHAT_MP" && ch != "WECHAT_MINI" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,7 +426,7 @@ func (c *BackendReminderController) UpdateReminder() {
|
|||||||
|
|
||||||
for _, ch := range p.RemindChannels {
|
for _, ch := range p.RemindChannels {
|
||||||
ch = strings.ToUpper(strings.TrimSpace(ch))
|
ch = strings.ToUpper(strings.TrimSpace(ch))
|
||||||
if ch != "SMS" && ch != "EMAIL" && ch != "BARK" && ch != "SITE_MSG" {
|
if ch != "SMS" && ch != "EMAIL" && ch != "BARK" && ch != "SITE_MSG" && ch != "WECHAT_MP" && ch != "WECHAT_MINI" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -691,10 +691,12 @@ func (c *BackendReminderController) TestReminder() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
senders := map[string]services.ReminderSender{
|
senders := map[string]services.ReminderSender{
|
||||||
"SMS": &services.SMSSender{},
|
"SMS": &services.SMSSender{},
|
||||||
"EMAIL": &services.EmailSender{},
|
"EMAIL": &services.EmailSender{},
|
||||||
"BARK": &services.BarkSender{},
|
"BARK": &services.BarkSender{},
|
||||||
"SITE_MSG": &services.SiteMsgSender{},
|
"SITE_MSG": &services.SiteMsgSender{},
|
||||||
|
"WECHAT_MP": &services.WechatMpSender{},
|
||||||
|
"WECHAT_MINI": &services.WechatMiniSender{},
|
||||||
}
|
}
|
||||||
|
|
||||||
type TestResult struct {
|
type TestResult struct {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
|
|
||||||
"server/models"
|
"server/models"
|
||||||
"server/services/wechatmini"
|
"server/services/wechatmini"
|
||||||
|
"server/services/wechatmp"
|
||||||
|
|
||||||
"github.com/beego/beego/v2/core/logs"
|
"github.com/beego/beego/v2/core/logs"
|
||||||
)
|
)
|
||||||
@@ -312,6 +313,62 @@ func (s *SiteMsgSender) Send(ctx context.Context, reminder ReminderData, title,
|
|||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WechatMpSender 微信服务号模板消息发送实现
|
||||||
|
// 要求:服务号已认证且在平台设置中启用;用户已在服务号绑定账号并关注。
|
||||||
|
type WechatMpSender struct{}
|
||||||
|
|
||||||
|
func (s *WechatMpSender) Send(ctx context.Context, reminder ReminderData, title, content string) (bool, error) {
|
||||||
|
cfg, err := wechatmp.LoadEnabledConfig()
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("服务号未配置或未启用: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 接收目标优先级:手动指定的 openid → 服务号粉丝绑定表反查
|
||||||
|
openid := ""
|
||||||
|
if reminder.ReceiverTarget != nil && *reminder.ReceiverTarget != "" {
|
||||||
|
openid = *reminder.ReceiverTarget
|
||||||
|
}
|
||||||
|
if openid == "" && reminder.Tid > 0 {
|
||||||
|
if f, ferr := wechatmp.GetFollowerByUser("tenant_user", reminder.ReceiverUserID); ferr == nil && f != nil {
|
||||||
|
if f.Subscribe != 1 {
|
||||||
|
return false, fmt.Errorf("用户已取消关注服务号,无法推送模板消息")
|
||||||
|
}
|
||||||
|
openid = f.OpenID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if openid == "" {
|
||||||
|
return false, fmt.Errorf("用户未绑定服务号,无法推送(请在服务号内完成账号绑定)")
|
||||||
|
}
|
||||||
|
|
||||||
|
perr := wechatmp.PushToOpenID(cfg, openid, wechatmp.PushMessage{
|
||||||
|
Title: title,
|
||||||
|
Content: content,
|
||||||
|
Remark: "来自云泽管理平台日程提醒",
|
||||||
|
})
|
||||||
|
if perr != nil {
|
||||||
|
return false, perr
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WechatMiniSender 微信小程序订阅消息发送实现
|
||||||
|
// 要求:用户在小程序内授权过订阅消息(一次性授权,发一条少一条)。
|
||||||
|
type WechatMiniSender struct{}
|
||||||
|
|
||||||
|
func (s *WechatMiniSender) Send(ctx context.Context, reminder ReminderData, title, content string) (bool, error) {
|
||||||
|
if reminder.ReceiverUserID == 0 {
|
||||||
|
return false, fmt.Errorf("无效的接收用户")
|
||||||
|
}
|
||||||
|
res := wechatmini.PushTenantNotice(reminder.ReceiverUserID, title, content)
|
||||||
|
if res.Sent > 0 {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
if len(res.Errors) > 0 {
|
||||||
|
return false, res.Errors[0]
|
||||||
|
}
|
||||||
|
return false, fmt.Errorf("用户未授权小程序订阅消息(授权一次仅可发送一条)")
|
||||||
|
}
|
||||||
|
|
||||||
// generateUUID 生成一个安全的随机 UUID 字符
|
// generateUUID 生成一个安全的随机 UUID 字符
|
||||||
func generateUUID() string {
|
func generateUUID() string {
|
||||||
b := make([]byte, 16)
|
b := make([]byte, 16)
|
||||||
@@ -368,10 +425,12 @@ func scanAndSendReminders() {
|
|||||||
|
|
||||||
// 实例分发发送
|
// 实例分发发送
|
||||||
senders := map[string]ReminderSender{
|
senders := map[string]ReminderSender{
|
||||||
"SMS": &SMSSender{},
|
"SMS": &SMSSender{},
|
||||||
"EMAIL": &EmailSender{},
|
"EMAIL": &EmailSender{},
|
||||||
"BARK": &BarkSender{},
|
"BARK": &BarkSender{},
|
||||||
"SITE_MSG": &SiteMsgSender{},
|
"SITE_MSG": &SiteMsgSender{},
|
||||||
|
"WECHAT_MP": &WechatMpSender{},
|
||||||
|
"WECHAT_MINI": &WechatMiniSender{},
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range list {
|
for i := range list {
|
||||||
@@ -437,7 +496,8 @@ func scanAndSendReminders() {
|
|||||||
newSendCount := reminder.SendCount + 1
|
newSendCount := reminder.SendCount + 1
|
||||||
newStatus := reminder.RemindStatus
|
newStatus := reminder.RemindStatus
|
||||||
|
|
||||||
if reminder.RemindChannel == "SMS" || reminder.RemindChannel == "SITE_MSG" {
|
if reminder.RemindChannel == "SMS" || reminder.RemindChannel == "SITE_MSG" ||
|
||||||
|
reminder.RemindChannel == "WECHAT_MP" || reminder.RemindChannel == "WECHAT_MINI" {
|
||||||
// 一次性发送:发送后直接置为结束
|
// 一次性发送:发送后直接置为结束
|
||||||
newStatus = 2
|
newStatus = 2
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in new issue
Block a user