mirror of
https://github.com/flipped-aurora/gin-vue-admin.git
synced 2026-09-25 05:50:19 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4fe1dbdc03 | ||
|
|
1c4ef7325e | ||
|
|
b2b68e5237 |
@@ -4,6 +4,7 @@ import (
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@@ -116,10 +117,17 @@ func (s *DictionaryApi) FindSysDictionary(c *gin.Context) {
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data query request.SysDictionarySearch true "字典 name 或者 type"
|
||||
// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "分页获取SysDictionary列表,返回包括列表,总数,页码,每页数量"
|
||||
// @Router /sysDictionary/getSysDictionaryList [get]
|
||||
func (s *DictionaryApi) GetSysDictionaryList(c *gin.Context) {
|
||||
list, err := dictionaryService.GetSysDictionaryInfoList()
|
||||
var dictionary request.SysDictionarySearch
|
||||
err := c.ShouldBindQuery(&dictionary)
|
||||
if err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
list, err := dictionaryService.GetSysDictionaryInfoList(c, dictionary)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("获取失败!", zap.Error(err))
|
||||
response.FailWithMessage("获取失败", c)
|
||||
|
||||
+215
-250
@@ -1,210 +1,3 @@
|
||||
# github.com/flipped-aurora/gin-vue-admin/server Global Configuration
|
||||
|
||||
# jwt configuration
|
||||
jwt:
|
||||
signing-key: qmPlus
|
||||
expires-time: 7d
|
||||
buffer-time: 1d
|
||||
issuer: qmPlus
|
||||
# zap logger configuration
|
||||
zap:
|
||||
level: info
|
||||
format: console
|
||||
prefix: "[github.com/flipped-aurora/gin-vue-admin/server]"
|
||||
director: log
|
||||
show-line: true
|
||||
encode-level: LowercaseColorLevelEncoder
|
||||
stacktrace-key: stacktrace
|
||||
log-in-console: true
|
||||
retention-day: -1
|
||||
|
||||
# redis configuration
|
||||
redis:
|
||||
#是否使用redis集群模式
|
||||
useCluster: false
|
||||
#使用集群模式addr和db默认无效
|
||||
addr: 127.0.0.1:6379
|
||||
password: ""
|
||||
db: 0
|
||||
clusterAddrs:
|
||||
- "172.21.0.3:7000"
|
||||
- "172.21.0.4:7001"
|
||||
- "172.21.0.2:7002"
|
||||
|
||||
# redis-list configuration
|
||||
redis-list:
|
||||
- name: cache # 数据库的名称,注意: name 需要在 redis-list 中唯一
|
||||
useCluster: false # 是否使用redis集群模式
|
||||
addr: 127.0.0.1:6379 # 使用集群模式addr和db默认无效
|
||||
password: ""
|
||||
db: 0
|
||||
clusterAddrs:
|
||||
- "172.21.0.3:7000"
|
||||
- "172.21.0.4:7001"
|
||||
- "172.21.0.2:7002"
|
||||
|
||||
# mongo configuration
|
||||
mongo:
|
||||
coll: ''
|
||||
options: ''
|
||||
database: ''
|
||||
username: ''
|
||||
password: ''
|
||||
auth-source: ''
|
||||
min-pool-size: 0
|
||||
max-pool-size: 100
|
||||
socket-timeout-ms: 0
|
||||
connect-timeout-ms: 0
|
||||
is-zap: false
|
||||
hosts:
|
||||
- host: ''
|
||||
port: ''
|
||||
|
||||
# email configuration
|
||||
email:
|
||||
to: xxx@qq.com
|
||||
port: 465
|
||||
from: xxx@163.com
|
||||
host: smtp.163.com
|
||||
is-ssl: true
|
||||
secret: xxx
|
||||
nickname: test
|
||||
|
||||
# system configuration
|
||||
system:
|
||||
env: local # 修改为public可以关闭路由日志输出
|
||||
addr: 8888
|
||||
db-type: mysql
|
||||
oss-type: local # 控制oss选择走本地还是 七牛等其他仓 自行增加其他oss仓可以在 server/utils/upload/upload.go 中 NewOss函数配置
|
||||
use-redis: false # 使用redis
|
||||
use-mongo: false # 使用mongo
|
||||
use-multipoint: false
|
||||
# IP限制次数 一个小时15000次
|
||||
iplimit-count: 15000
|
||||
# IP限制一个小时
|
||||
iplimit-time: 3600
|
||||
# 路由全局前缀
|
||||
router-prefix: ""
|
||||
# 严格角色模式 打开后权限将会存在上下级关系
|
||||
use-strict-auth: false
|
||||
|
||||
# captcha configuration
|
||||
captcha:
|
||||
key-long: 6
|
||||
img-width: 240
|
||||
img-height: 80
|
||||
open-captcha: 0 # 0代表一直开启,大于0代表限制次数
|
||||
open-captcha-timeout: 3600 # open-captcha大于0时才生效
|
||||
|
||||
# mysql connect configuration
|
||||
# 未初始化之前请勿手动修改数据库信息!!!如果一定要手动初始化请看(https://gin-vue-admin.com/docs/first_master)
|
||||
mysql:
|
||||
path: ""
|
||||
port: ""
|
||||
config: ""
|
||||
db-name: ""
|
||||
username: ""
|
||||
password: ""
|
||||
max-idle-conns: 10
|
||||
max-open-conns: 100
|
||||
log-mode: ""
|
||||
log-zap: false
|
||||
|
||||
# pgsql connect configuration
|
||||
# 未初始化之前请勿手动修改数据库信息!!!如果一定要手动初始化请看(https://gin-vue-admin.com/docs/first_master)
|
||||
pgsql:
|
||||
path: ""
|
||||
port: ""
|
||||
config: ""
|
||||
db-name: ""
|
||||
username: ""
|
||||
password: ""
|
||||
max-idle-conns: 10
|
||||
max-open-conns: 100
|
||||
log-mode: ""
|
||||
log-zap: false
|
||||
oracle:
|
||||
path: ""
|
||||
port: ""
|
||||
config: ""
|
||||
db-name: ""
|
||||
username: ""
|
||||
password: ""
|
||||
max-idle-conns: 10
|
||||
max-open-conns: 100
|
||||
log-mode: ""
|
||||
log-zap: false
|
||||
mssql:
|
||||
path: ""
|
||||
port: ""
|
||||
config: ""
|
||||
db-name: ""
|
||||
username: ""
|
||||
password: ""
|
||||
max-idle-conns: 10
|
||||
max-open-conns: 100
|
||||
log-mode: ""
|
||||
log-zap: false
|
||||
sqlite:
|
||||
path: ""
|
||||
port: ""
|
||||
config: ""
|
||||
db-name: ""
|
||||
username: ""
|
||||
password: ""
|
||||
max-idle-conns: 10
|
||||
max-open-conns: 100
|
||||
log-mode: ""
|
||||
log-zap: false
|
||||
db-list:
|
||||
- disable: true # 是否禁用
|
||||
type: "" # 数据库的类型,目前支持mysql、pgsql、mssql、oracle
|
||||
alias-name: "" # 数据库的名称,注意: alias-name 需要在db-list中唯一
|
||||
path: ""
|
||||
port: ""
|
||||
config: ""
|
||||
db-name: ""
|
||||
username: ""
|
||||
password: ""
|
||||
max-idle-conns: 10
|
||||
max-open-conns: 100
|
||||
log-mode: ""
|
||||
log-zap: false
|
||||
|
||||
# local configuration
|
||||
local:
|
||||
path: uploads/file
|
||||
store-path: uploads/file
|
||||
|
||||
# autocode configuration
|
||||
autocode:
|
||||
web: web/src
|
||||
root: "" # root 自动适配项目根目录, 请不要手动配置,他会在项目加载的时候识别出根路径
|
||||
server: server
|
||||
module: 'github.com/flipped-aurora/gin-vue-admin/server'
|
||||
ai-path: "" # AI服务路径
|
||||
|
||||
# qiniu configuration (请自行七牛申请对应的 公钥 私钥 bucket 和 域名地址)
|
||||
qiniu:
|
||||
zone: ZoneHuaDong
|
||||
bucket: ""
|
||||
img-path: ""
|
||||
use-https: false
|
||||
access-key: ""
|
||||
secret-key: ""
|
||||
use-cdn-domains: false
|
||||
|
||||
# minio oss configuration
|
||||
minio:
|
||||
endpoint: yourEndpoint
|
||||
access-key-id: yourAccessKeyId
|
||||
access-key-secret: yourAccessKeySecret
|
||||
bucket-name: yourBucketName
|
||||
use-ssl: false
|
||||
base-path: ""
|
||||
bucket-url: "http://host:9000/yourBucketName"
|
||||
|
||||
# aliyun oss configuration
|
||||
aliyun-oss:
|
||||
endpoint: yourEndpoint
|
||||
access-key-id: yourAccessKeyId
|
||||
@@ -212,29 +5,28 @@ aliyun-oss:
|
||||
bucket-name: yourBucketName
|
||||
bucket-url: yourBucketUrl
|
||||
base-path: yourBasePath
|
||||
|
||||
# tencent cos configuration
|
||||
tencent-cos:
|
||||
bucket: xxxxx-10005608
|
||||
region: ap-shanghai
|
||||
secret-id: your-secret-id
|
||||
secret-key: your-secret-key
|
||||
base-url: https://gin.vue.admin
|
||||
path-prefix: github.com/flipped-aurora/gin-vue-admin/server
|
||||
|
||||
# aws s3 configuration (minio compatible)
|
||||
autocode:
|
||||
web: web/src
|
||||
root: /Users/panghu/Documents/gva/gin-vue-admin
|
||||
server: server
|
||||
module: github.com/flipped-aurora/gin-vue-admin/server
|
||||
ai-path: ""
|
||||
aws-s3:
|
||||
bucket: xxxxx-10005608
|
||||
region: ap-shanghai
|
||||
endpoint: ""
|
||||
s3-force-path-style: false
|
||||
disable-ssl: false
|
||||
secret-id: your-secret-id
|
||||
secret-key: your-secret-key
|
||||
base-url: https://gin.vue.admin
|
||||
path-prefix: github.com/flipped-aurora/gin-vue-admin/server
|
||||
|
||||
# cloudflare r2 configuration
|
||||
s3-force-path-style: false
|
||||
disable-ssl: false
|
||||
captcha:
|
||||
key-long: 6
|
||||
img-width: 240
|
||||
img-height: 80
|
||||
open-captcha: 0
|
||||
open-captcha-timeout: 3600
|
||||
cloudflare-r2:
|
||||
bucket: xxxx0bucket
|
||||
base-url: https://gin.vue.admin.com
|
||||
@@ -242,42 +34,215 @@ cloudflare-r2:
|
||||
account-id: xxx_account_id
|
||||
access-key-id: xxx_key_id
|
||||
secret-access-key: xxx_secret_key
|
||||
|
||||
# huawei obs configuration
|
||||
cors:
|
||||
mode: strict-whitelist
|
||||
whitelist:
|
||||
- allow-origin: example1.com
|
||||
allow-methods: POST, GET
|
||||
allow-headers: Content-Type,AccessToken,X-CSRF-Token, Authorization, Token,X-Token,X-User-Id
|
||||
expose-headers: Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type
|
||||
allow-credentials: true
|
||||
- allow-origin: example2.com
|
||||
allow-methods: GET, POST
|
||||
allow-headers: content-type
|
||||
expose-headers: Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type
|
||||
allow-credentials: true
|
||||
db-list:
|
||||
- type: ""
|
||||
alias-name: ""
|
||||
prefix: ""
|
||||
port: ""
|
||||
config: ""
|
||||
db-name: ""
|
||||
username: ""
|
||||
password: ""
|
||||
path: ""
|
||||
engine: ""
|
||||
log-mode: ""
|
||||
max-idle-conns: 10
|
||||
max-open-conns: 100
|
||||
singular: false
|
||||
log-zap: false
|
||||
disable: true
|
||||
disk-list:
|
||||
- mount-point: /
|
||||
email:
|
||||
to: xxx@qq.com
|
||||
from: xxx@163.com
|
||||
host: smtp.163.com
|
||||
secret: xxx
|
||||
nickname: test
|
||||
port: 465
|
||||
is-ssl: true
|
||||
is-loginauth: false
|
||||
excel:
|
||||
dir: ./resource/excel/
|
||||
hua-wei-obs:
|
||||
path: you-path
|
||||
bucket: you-bucket
|
||||
endpoint: you-endpoint
|
||||
access-key: you-access-key
|
||||
secret-key: you-secret-key
|
||||
|
||||
# excel configuration
|
||||
excel:
|
||||
dir: ./resource/excel/
|
||||
|
||||
# disk usage configuration
|
||||
disk-list:
|
||||
- mount-point: "/"
|
||||
|
||||
# 跨域配置
|
||||
# 需要配合 server/initialize/router.go -> `Router.Use(middleware.CorsByRules())` 使用
|
||||
cors:
|
||||
mode: strict-whitelist # 放行模式: allow-all, 放行全部; whitelist, 白名单模式, 来自白名单内域名的请求添加 cors 头; strict-whitelist 严格白名单模式, 白名单外的请求一律拒绝
|
||||
whitelist:
|
||||
- allow-origin: example1.com
|
||||
allow-headers: Content-Type,AccessToken,X-CSRF-Token, Authorization, Token,X-Token,X-User-Id
|
||||
allow-methods: POST, GET
|
||||
expose-headers: Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type
|
||||
|
||||
allow-credentials: true # 布尔值
|
||||
- allow-origin: example2.com
|
||||
allow-headers: content-type
|
||||
allow-methods: GET, POST
|
||||
expose-headers: Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type
|
||||
allow-credentials: true # 布尔值
|
||||
jwt:
|
||||
signing-key: 151a53c2-4dfa-42f2-aba3-0c3ad96535d9
|
||||
expires-time: 7d
|
||||
buffer-time: 1d
|
||||
issuer: qmPlus
|
||||
local:
|
||||
path: uploads/file
|
||||
store-path: uploads/file
|
||||
mcp:
|
||||
name: GVA_MCP
|
||||
version: v1.0.0
|
||||
sse_path: /sse
|
||||
message_path: /message
|
||||
url_prefix: ''
|
||||
url_prefix: ""
|
||||
minio:
|
||||
endpoint: yourEndpoint
|
||||
access-key-id: yourAccessKeyId
|
||||
access-key-secret: yourAccessKeySecret
|
||||
bucket-name: yourBucketName
|
||||
use-ssl: false
|
||||
base-path: ""
|
||||
bucket-url: http://host:9000/yourBucketName
|
||||
mongo:
|
||||
coll: ""
|
||||
options: ""
|
||||
database: ""
|
||||
username: ""
|
||||
password: ""
|
||||
auth-source: ""
|
||||
min-pool-size: 0
|
||||
max-pool-size: 100
|
||||
socket-timeout-ms: 0
|
||||
connect-timeout-ms: 0
|
||||
is-zap: false
|
||||
hosts:
|
||||
- host: ""
|
||||
port: ""
|
||||
mssql:
|
||||
prefix: ""
|
||||
port: ""
|
||||
config: ""
|
||||
db-name: ""
|
||||
username: ""
|
||||
password: ""
|
||||
path: ""
|
||||
engine: ""
|
||||
log-mode: ""
|
||||
max-idle-conns: 10
|
||||
max-open-conns: 100
|
||||
singular: false
|
||||
log-zap: false
|
||||
mysql:
|
||||
prefix: ""
|
||||
port: ""
|
||||
config: ""
|
||||
db-name: ""
|
||||
username: ""
|
||||
password: ""
|
||||
path: ""
|
||||
engine: ""
|
||||
log-mode: ""
|
||||
max-idle-conns: 10
|
||||
max-open-conns: 100
|
||||
singular: false
|
||||
log-zap: false
|
||||
oracle:
|
||||
prefix: ""
|
||||
port: ""
|
||||
config: ""
|
||||
db-name: ""
|
||||
username: ""
|
||||
password: ""
|
||||
path: ""
|
||||
engine: ""
|
||||
log-mode: ""
|
||||
max-idle-conns: 10
|
||||
max-open-conns: 100
|
||||
singular: false
|
||||
log-zap: false
|
||||
pgsql:
|
||||
prefix: ""
|
||||
port: "5432"
|
||||
config: sslmode=disable TimeZone=Asia/Shanghai
|
||||
db-name: gva
|
||||
username: jutze
|
||||
password: jutze123
|
||||
path: 127.0.0.1
|
||||
engine: ""
|
||||
log-mode: error
|
||||
max-idle-conns: 10
|
||||
max-open-conns: 100
|
||||
singular: false
|
||||
log-zap: false
|
||||
qiniu:
|
||||
zone: ZoneHuaDong
|
||||
bucket: ""
|
||||
img-path: ""
|
||||
access-key: ""
|
||||
secret-key: ""
|
||||
use-https: false
|
||||
use-cdn-domains: false
|
||||
redis:
|
||||
name: ""
|
||||
addr: 127.0.0.1:6379
|
||||
password: ""
|
||||
db: 0
|
||||
useCluster: false
|
||||
clusterAddrs:
|
||||
- 172.21.0.3:7000
|
||||
- 172.21.0.4:7001
|
||||
- 172.21.0.2:7002
|
||||
redis-list:
|
||||
- name: cache
|
||||
addr: 127.0.0.1:6379
|
||||
password: ""
|
||||
db: 0
|
||||
useCluster: false
|
||||
clusterAddrs:
|
||||
- 172.21.0.3:7000
|
||||
- 172.21.0.4:7001
|
||||
- 172.21.0.2:7002
|
||||
sqlite:
|
||||
prefix: ""
|
||||
port: ""
|
||||
config: ""
|
||||
db-name: ""
|
||||
username: ""
|
||||
password: ""
|
||||
path: ""
|
||||
engine: ""
|
||||
log-mode: ""
|
||||
max-idle-conns: 10
|
||||
max-open-conns: 100
|
||||
singular: false
|
||||
log-zap: false
|
||||
system:
|
||||
db-type: pgsql
|
||||
oss-type: local
|
||||
router-prefix: ""
|
||||
addr: 8888
|
||||
iplimit-count: 15000
|
||||
iplimit-time: 3600
|
||||
use-multipoint: false
|
||||
use-redis: false
|
||||
use-mongo: false
|
||||
use-strict-auth: false
|
||||
tencent-cos:
|
||||
bucket: xxxxx-10005608
|
||||
region: ap-shanghai
|
||||
secret-id: your-secret-id
|
||||
secret-key: your-secret-key
|
||||
base-url: https://gin.vue.admin
|
||||
path-prefix: github.com/flipped-aurora/gin-vue-admin/server
|
||||
zap:
|
||||
level: info
|
||||
prefix: '[github.com/flipped-aurora/gin-vue-admin/server]'
|
||||
format: console
|
||||
director: log
|
||||
encode-level: LowercaseColorLevelEncoder
|
||||
stacktrace-key: stacktrace
|
||||
show-line: true
|
||||
log-in-console: true
|
||||
retention-day: -1
|
||||
|
||||
@@ -73,9 +73,11 @@ require (
|
||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/doquangtan/socketio/v4 v4.1.6 // indirect
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/emirpasic/gods v1.12.0 // indirect
|
||||
github.com/fasthttp/websocket v1.5.3 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/gammazero/toposort v0.1.1 // indirect
|
||||
github.com/gin-contrib/sse v1.0.0 // indirect
|
||||
@@ -89,12 +91,15 @@ require (
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.24.0 // indirect
|
||||
github.com/gofiber/fiber/v2 v2.52.9 // indirect
|
||||
github.com/gofiber/websocket/v2 v2.2.1 // indirect
|
||||
github.com/gofrs/flock v0.12.1 // indirect
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
@@ -115,7 +120,9 @@ require (
|
||||
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect
|
||||
github.com/magiconair/properties v1.8.9 // indirect
|
||||
github.com/mailru/easyjson v0.9.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/microsoft/go-mssqldb v1.8.0 // indirect
|
||||
github.com/minio/md5-simd v1.1.2 // indirect
|
||||
github.com/minio/minlz v1.0.0 // indirect
|
||||
@@ -135,9 +142,11 @@ require (
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/richardlehane/mscfb v1.0.4 // indirect
|
||||
github.com/richardlehane/msoleps v1.0.4 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/rs/xid v1.6.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.7.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
|
||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
||||
github.com/sijms/go-ora/v2 v2.7.17 // indirect
|
||||
github.com/sorairolake/lzip-go v0.3.5 // indirect
|
||||
@@ -153,6 +162,9 @@ require (
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
github.com/ulikunitz/xz v0.5.12 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.51.0 // indirect
|
||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||
github.com/xdg-go/scram v1.1.2 // indirect
|
||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||
|
||||
@@ -102,6 +102,8 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko=
|
||||
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
|
||||
github.com/doquangtan/socketio/v4 v4.1.6 h1:dpcO8IsQxNrvCJ7kNADfXMAmfomO9kTidKExboxtwpM=
|
||||
github.com/doquangtan/socketio/v4 v4.1.6/go.mod h1:p43iXxVgwzOfdFg+TsC0bYXUHycwh6oYKwe+hkuDJu4=
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4=
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
|
||||
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
|
||||
@@ -113,6 +115,8 @@ github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg
|
||||
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fasthttp/websocket v1.5.3 h1:TPpQuLwJYfd4LJPXvHDYPMFWbLjsT91n3GpWtCQtdek=
|
||||
github.com/fasthttp/websocket v1.5.3/go.mod h1:46gg/UBmTU1kUaTcwQXpUxtRwG2PvIZYeA8oL6vF3Fs=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||
@@ -166,6 +170,10 @@ github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpv
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
|
||||
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/gofiber/fiber/v2 v2.52.9 h1:YjKl5DOiyP3j0mO61u3NTmK7or8GzzWzCFzkboyP5cw=
|
||||
github.com/gofiber/fiber/v2 v2.52.9/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
||||
github.com/gofiber/websocket/v2 v2.2.1 h1:C9cjxvloojayOp9AovmpQrk8VqvVnT8Oao3+IUygH7w=
|
||||
github.com/gofiber/websocket/v2 v2.2.1/go.mod h1:Ao/+nyNnX5u/hIFPuHl28a+NIkrqK7PRimyKaj4JxVU=
|
||||
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
|
||||
github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
|
||||
github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
|
||||
@@ -230,6 +238,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
@@ -317,8 +327,13 @@ github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4
|
||||
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||
github.com/mark3labs/mcp-go v0.31.0 h1:4UxSV8aM770OPmTvaVe/b1rA2oZAjBMhGBfUgOGut+4=
|
||||
github.com/mark3labs/mcp-go v0.31.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
@@ -393,6 +408,8 @@ github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7
|
||||
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
||||
github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00=
|
||||
github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
@@ -407,6 +424,8 @@ github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsF
|
||||
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
|
||||
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
|
||||
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
|
||||
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
||||
@@ -473,6 +492,12 @@ github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
|
||||
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/unrolled/secure v1.17.0 h1:Io7ifFgo99Bnh0J7+Q+qcMzWM6kaDPCA5FroFZEdbWU=
|
||||
github.com/unrolled/secure v1.17.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
|
||||
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
|
||||
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
|
||||
@@ -639,6 +664,7 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
||||
@@ -2,6 +2,7 @@ package initialize
|
||||
|
||||
import (
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/plugin/announcement"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/plugin/notice"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/utils/plugin/v2"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -13,4 +14,5 @@ func PluginInitV2(group *gin.Engine, plugins ...plugin.Plugin) {
|
||||
}
|
||||
func bizPluginV2(engine *gin.Engine) {
|
||||
PluginInitV2(engine, announcement.Plugin)
|
||||
PluginInitV2(engine, notice.Plugin)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CasbinHandler 拦截器
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package request
|
||||
|
||||
type SysDictionarySearch struct {
|
||||
Name string `json:"name" form:"name" gorm:"column:name;comment:字典名(中)"` // 字典名(中)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package api
|
||||
|
||||
type ApiGroup struct {
|
||||
NotificationApi
|
||||
OnlineUserApi
|
||||
}
|
||||
|
||||
var ApiGroupApp = new(ApiGroup)
|
||||
@@ -0,0 +1,333 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/plugin/notice/model/request"
|
||||
noticeService "github.com/flipped-aurora/gin-vue-admin/server/plugin/notice/service"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type NotificationApi struct{}
|
||||
|
||||
var notificationService = noticeService.ServiceGroupApp.NotificationService
|
||||
|
||||
// CreateNotification 创建通知
|
||||
// @Tags NoticeCenter
|
||||
// @Summary 创建一个新的通知
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body request.CreateNotificationRequest true "通知的标题、内容、类型等信息"
|
||||
// @Success 200 {object} response.Response{msg=string} "创建成功"
|
||||
// @Router /notice/createNotification [post]
|
||||
func (a *NotificationApi) CreateNotification(c *gin.Context) {
|
||||
var req request.CreateNotificationRequest
|
||||
err := c.ShouldBindJSON(&req)
|
||||
if err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当前用户ID
|
||||
userId := utils.GetUserID(c)
|
||||
|
||||
notification, err := notificationService.CreateNotification(req, userId)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("创建通知失败!" + err.Error())
|
||||
response.FailWithMessage("创建通知失败", c)
|
||||
return
|
||||
}
|
||||
|
||||
response.OkWithDetailed(notification, "创建成功", c)
|
||||
}
|
||||
|
||||
// UpdateNotification 更新通知
|
||||
// @Tags NoticeCenter
|
||||
// @Summary 更新通知信息
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body request.UpdateNotificationRequest true "通知的更新信息"
|
||||
// @Success 200 {object} response.Response{msg=string} "更新成功"
|
||||
// @Router /notice/updateNotification [put]
|
||||
func (a *NotificationApi) UpdateNotification(c *gin.Context) {
|
||||
var req request.UpdateNotificationRequest
|
||||
err := c.ShouldBindJSON(&req)
|
||||
if err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
err = notificationService.UpdateNotification(req)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("更新通知失败!" + err.Error())
|
||||
response.FailWithMessage("更新通知失败", c)
|
||||
return
|
||||
}
|
||||
|
||||
response.OkWithMessage("更新成功", c)
|
||||
}
|
||||
|
||||
// DeleteNotification 删除通知
|
||||
// @Tags NoticeCenter
|
||||
// @Summary 删除通知
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param id path int true "通知ID"
|
||||
// @Success 200 {object} response.Response{msg=string} "删除成功"
|
||||
// @Router /notice/deleteNotification/{id} [delete]
|
||||
func (a *NotificationApi) DeleteNotification(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
response.FailWithMessage("无效的通知ID", c)
|
||||
return
|
||||
}
|
||||
|
||||
err = notificationService.DeleteNotification(uint(id))
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("删除通知失败!" + err.Error())
|
||||
response.FailWithMessage("删除通知失败", c)
|
||||
return
|
||||
}
|
||||
|
||||
response.OkWithMessage("删除成功", c)
|
||||
}
|
||||
|
||||
// GetNotificationList 获取通知列表
|
||||
// @Tags NoticeCenter
|
||||
// @Summary 获取通知列表
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body request.NotificationSearch true "分页和搜索条件"
|
||||
// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功"
|
||||
// @Router /notice/getNotificationList [post]
|
||||
func (a *NotificationApi) GetNotificationList(c *gin.Context) {
|
||||
var req request.NotificationSearch
|
||||
err := c.ShouldBindJSON(&req)
|
||||
if err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
list, total, err := notificationService.GetNotificationList(req)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("获取通知列表失败!" + err.Error())
|
||||
response.FailWithMessage("获取通知列表失败", c)
|
||||
return
|
||||
}
|
||||
|
||||
response.OkWithDetailed(response.PageResult{
|
||||
List: list,
|
||||
Total: total,
|
||||
Page: req.Page,
|
||||
PageSize: req.PageSize,
|
||||
}, "获取成功", c)
|
||||
}
|
||||
|
||||
// GetNotificationById 根据ID获取通知
|
||||
// @Tags NoticeCenter
|
||||
// @Summary 根据ID获取通知详情
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param id path int true "通知ID"
|
||||
// @Success 200 {object} response.Response{data=model.Notification,msg=string} "获取成功"
|
||||
// @Router /notice/getNotification/{id} [get]
|
||||
func (a *NotificationApi) GetNotificationById(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
response.FailWithMessage("无效的通知ID", c)
|
||||
return
|
||||
}
|
||||
|
||||
notification, err := notificationService.GetNotificationById(uint(id))
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("获取通知失败!" + err.Error())
|
||||
response.FailWithMessage("获取通知失败", c)
|
||||
return
|
||||
}
|
||||
|
||||
response.OkWithDetailed(notification, "获取成功", c)
|
||||
}
|
||||
|
||||
// PublishNotification 发布通知
|
||||
// @Tags NoticeCenter
|
||||
// @Summary 发布通知
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param id path int true "通知ID"
|
||||
// @Success 200 {object} response.Response{msg=string} "发布成功"
|
||||
// @Router /notice/publishNotification/{id} [post]
|
||||
func (a *NotificationApi) PublishNotification(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
response.FailWithMessage("无效的通知ID", c)
|
||||
return
|
||||
}
|
||||
|
||||
err = notificationService.PublishNotification(uint(id))
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("发布通知失败!" + err.Error())
|
||||
response.FailWithMessage("发布通知失败", c)
|
||||
return
|
||||
}
|
||||
|
||||
response.OkWithMessage("发布成功", c)
|
||||
}
|
||||
|
||||
// SendNotification 发送通知
|
||||
// @Tags NoticeCenter
|
||||
// @Summary 发送通知给指定用户或角色
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body request.SendNotificationRequest true "发送通知的请求参数"
|
||||
// @Success 200 {object} response.Response{data=response.SendNotificationResponse,msg=string} "发送成功"
|
||||
// @Router /notice/sendNotification [post]
|
||||
func (a *NotificationApi) SendNotification(c *gin.Context) {
|
||||
var req request.SendNotificationRequest
|
||||
err := c.ShouldBindJSON(&req)
|
||||
if err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当前用户ID
|
||||
userId := utils.GetUserID(c)
|
||||
|
||||
result, err := notificationService.SendNotification(req, userId)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("发送通知失败!" + err.Error())
|
||||
response.FailWithMessage("发送通知失败", c)
|
||||
return
|
||||
}
|
||||
|
||||
response.OkWithDetailed(result, "发送成功", c)
|
||||
}
|
||||
|
||||
// GetUserNotifications 获取用户通知列表
|
||||
// @Tags NoticeCenter
|
||||
// @Summary 获取当前用户的通知列表
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body request.UserNotificationSearch true "分页和搜索条件"
|
||||
// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功"
|
||||
// @Router /notice/getUserNotifications [post]
|
||||
func (a *NotificationApi) GetUserNotifications(c *gin.Context) {
|
||||
var req request.UserNotificationSearch
|
||||
err := c.ShouldBindJSON(&req)
|
||||
if err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当前用户ID
|
||||
userId := utils.GetUserID(c)
|
||||
|
||||
list, total, err := notificationService.GetUserNotifications(userId, req)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("获取用户通知列表失败!" + err.Error())
|
||||
response.FailWithMessage("获取用户通知列表失败", c)
|
||||
return
|
||||
}
|
||||
|
||||
response.OkWithDetailed(response.PageResult{
|
||||
List: list,
|
||||
Total: total,
|
||||
Page: req.Page,
|
||||
PageSize: req.PageSize,
|
||||
}, "获取成功", c)
|
||||
}
|
||||
|
||||
// MarkNotificationAsRead 标记通知为已读
|
||||
// @Tags NoticeCenter
|
||||
// @Summary 标记通知为已读
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body request.MarkReadRequest true "标记已读的请求参数"
|
||||
// @Success 200 {object} response.Response{msg=string} "标记成功"
|
||||
// @Router /notice/markAsRead [post]
|
||||
func (a *NotificationApi) MarkNotificationAsRead(c *gin.Context) {
|
||||
var req request.MarkReadRequest
|
||||
err := c.ShouldBindJSON(&req)
|
||||
if err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当前用户ID
|
||||
userId := utils.GetUserID(c)
|
||||
|
||||
err = notificationService.MarkNotificationsAsRead(userId, req.NotificationIds)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("标记通知已读失败!" + err.Error())
|
||||
response.FailWithMessage("标记通知已读失败", c)
|
||||
return
|
||||
}
|
||||
|
||||
response.OkWithMessage("标记成功", c)
|
||||
}
|
||||
|
||||
// GetNotificationStats 获取通知统计信息
|
||||
// @Tags NoticeCenter
|
||||
// @Summary 获取通知统计信息
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Success 200 {object} response.Response{data=response.NotificationStatsResponse,msg=string} "获取成功"
|
||||
// @Router /notice/getNotificationStats [get]
|
||||
func (a *NotificationApi) GetNotificationStats(c *gin.Context) {
|
||||
// 获取当前用户ID
|
||||
userId := utils.GetUserID(c)
|
||||
|
||||
stats, err := notificationService.GetNotificationStats(userId)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("获取通知统计失败!" + err.Error())
|
||||
response.FailWithMessage("获取通知统计失败", c)
|
||||
return
|
||||
}
|
||||
|
||||
response.OkWithDetailed(stats, "获取成功", c)
|
||||
}
|
||||
|
||||
// DeleteUserNotification 删除用户通知记录
|
||||
// @Tags NoticeCenter
|
||||
// @Summary 删除用户通知记录,如果通知只针对单个用户则删除通知本身
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param notificationId path int true "通知ID"
|
||||
// @Success 200 {object} response.Response{msg=string} "删除成功"
|
||||
// @Router /notice/deleteUserNotification/{notificationId} [delete]
|
||||
func (a *NotificationApi) DeleteUserNotification(c *gin.Context) {
|
||||
notificationIdStr := c.Param("notificationId")
|
||||
notificationId, err := strconv.ParseUint(notificationIdStr, 10, 32)
|
||||
if err != nil {
|
||||
response.FailWithMessage("通知ID格式错误", c)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当前用户ID
|
||||
userId := utils.GetUserID(c)
|
||||
|
||||
err = notificationService.DeleteUserNotification(userId, uint(notificationId))
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("删除用户通知失败!" + err.Error())
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
response.OkWithMessage("删除成功", c)
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/plugin/notice/model/request"
|
||||
noticeResponse "github.com/flipped-aurora/gin-vue-admin/server/plugin/notice/model/response"
|
||||
noticeService "github.com/flipped-aurora/gin-vue-admin/server/plugin/notice/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type OnlineUserApi struct{}
|
||||
|
||||
var onlineUserService = noticeService.ServiceGroupApp.OnlineUserService
|
||||
|
||||
// GetOnlineUsers 获取在线用户列表
|
||||
// @Tags NoticeCenter
|
||||
// @Summary 获取在线用户列表
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Success 200 {object} response.Response{data=[]response.OnlineUserResponse,msg=string} "获取成功"
|
||||
// @Router /notice/onlineUser/getOnlineUsers [get]
|
||||
func (a *OnlineUserApi) GetOnlineUsers(c *gin.Context) {
|
||||
onlineUsers, err := onlineUserService.GetOnlineUsers()
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("获取在线用户列表失败!" + err.Error())
|
||||
response.FailWithMessage("获取在线用户列表失败", c)
|
||||
return
|
||||
}
|
||||
|
||||
response.OkWithDetailed(onlineUsers, "获取成功", c)
|
||||
}
|
||||
|
||||
// GetOnlineUsersWithPagination 获取在线用户列表(分页)
|
||||
// @Tags NoticeCenter
|
||||
// @Summary 获取在线用户列表(支持分页和搜索)
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param data body request.OnlineUserSearch true "分页参数"
|
||||
// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功"
|
||||
// @Router /notice/onlineUser/getOnlineUsers [post]
|
||||
func (a *OnlineUserApi) GetOnlineUsersWithPagination(c *gin.Context) {
|
||||
var req request.OnlineUserSearch
|
||||
err := c.ShouldBindJSON(&req)
|
||||
if err != nil {
|
||||
response.FailWithMessage(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
onlineUsers, total, err := onlineUserService.GetOnlineUsersWithPagination(req)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("获取在线用户列表失败!" + err.Error())
|
||||
response.FailWithMessage("获取在线用户列表失败", c)
|
||||
return
|
||||
}
|
||||
|
||||
response.OkWithDetailed(response.PageResult{
|
||||
List: onlineUsers,
|
||||
Total: total,
|
||||
Page: req.Page,
|
||||
PageSize: req.PageSize,
|
||||
}, "获取成功", c)
|
||||
}
|
||||
|
||||
// GetOnlineUsersByRole 根据角色获取在线用户
|
||||
// @Tags NoticeCenter
|
||||
// @Summary 根据角色获取在线用户列表
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param roleId path int true "角色ID"
|
||||
// @Success 200 {object} response.Response{data=[]response.OnlineUserResponse,msg=string} "获取成功"
|
||||
// @Router /notice/getOnlineUsersByRole/{roleId} [get]
|
||||
func (a *OnlineUserApi) GetOnlineUsersByRole(c *gin.Context) {
|
||||
roleIdStr := c.Param("roleId")
|
||||
roleId, err := strconv.ParseUint(roleIdStr, 10, 32)
|
||||
if err != nil {
|
||||
response.FailWithMessage("无效的角色ID", c)
|
||||
return
|
||||
}
|
||||
|
||||
// 根据角色获取用户ID列表
|
||||
userIds, err := onlineUserService.GetUsersByRoleIds([]uint{uint(roleId)})
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("获取角色用户失败!" + err.Error())
|
||||
response.FailWithMessage("获取角色用户失败", c)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取所有在线用户
|
||||
allOnlineUsers, err := onlineUserService.GetOnlineUsers()
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("获取在线用户失败!" + err.Error())
|
||||
response.FailWithMessage("获取在线用户失败", c)
|
||||
return
|
||||
}
|
||||
|
||||
// 过滤出指定角色的在线用户
|
||||
var onlineUsers []noticeResponse.OnlineUserResponse
|
||||
userIdMap := make(map[uint]bool)
|
||||
for _, id := range userIds {
|
||||
userIdMap[id] = true
|
||||
}
|
||||
for _, user := range allOnlineUsers {
|
||||
if userIdMap[user.UserId] {
|
||||
onlineUsers = append(onlineUsers, user)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("根据角色获取在线用户失败!" + err.Error())
|
||||
response.FailWithMessage("根据角色获取在线用户失败", c)
|
||||
return
|
||||
}
|
||||
|
||||
response.OkWithDetailed(onlineUsers, "获取成功", c)
|
||||
}
|
||||
|
||||
// CheckUserOnline 检查用户是否在线
|
||||
// @Tags NoticeCenter
|
||||
// @Summary 检查指定用户是否在线
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param userId path int true "用户ID"
|
||||
// @Success 200 {object} response.Response{data=bool,msg=string} "检查成功"
|
||||
// @Router /notice/checkUserOnline/{userId} [get]
|
||||
func (a *OnlineUserApi) CheckUserOnline(c *gin.Context) {
|
||||
userIdStr := c.Param("userId")
|
||||
userId, err := strconv.ParseUint(userIdStr, 10, 32)
|
||||
if err != nil {
|
||||
response.FailWithMessage("无效的用户ID", c)
|
||||
return
|
||||
}
|
||||
|
||||
isOnline := onlineUserService.IsUserOnline(uint(userId))
|
||||
response.OkWithDetailed(isOnline, "检查成功", c)
|
||||
}
|
||||
|
||||
// GetOnlineUserCount 获取在线用户数量
|
||||
// @Tags NoticeCenter
|
||||
// @Summary 获取当前在线用户数量
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Success 200 {object} response.Response{data=int64,msg=string} "获取成功"
|
||||
// @Router /notice/getOnlineUserCount [get]
|
||||
func (a *OnlineUserApi) GetOnlineUserCount(c *gin.Context) {
|
||||
count, err := onlineUserService.GetOnlineUserCount()
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("获取在线用户数量失败!" + err.Error())
|
||||
response.FailWithMessage("获取在线用户数量失败", c)
|
||||
return
|
||||
}
|
||||
|
||||
response.OkWithDetailed(count, "获取成功", c)
|
||||
}
|
||||
|
||||
// GetOnlineUserStats 获取在线用户统计数据
|
||||
// @Tags NoticeCenter
|
||||
// @Summary 获取在线用户统计数据(总在线数、今日登录、峰值在线、平均在线)
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Success 200 {object} response.Response{data=response.OnlineUserStatsResponse,msg=string} "获取成功"
|
||||
// @Router /notice/onlineUser/getOnlineUserStats [get]
|
||||
func (a *OnlineUserApi) GetOnlineUserStats(c *gin.Context) {
|
||||
stats, err := onlineUserService.GetOnlineUserStats()
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("获取在线用户统计数据失败!" + err.Error())
|
||||
response.FailWithMessage("获取在线用户统计数据失败", c)
|
||||
return
|
||||
}
|
||||
|
||||
response.OkWithDetailed(stats, "获取成功", c)
|
||||
}
|
||||
|
||||
// RemoveOnlineUser 移除在线用户
|
||||
// @Tags NoticeCenter
|
||||
// @Summary 移除指定的在线用户
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Param userId path int true "用户ID"
|
||||
// @Success 200 {object} response.Response{msg=string} "移除成功"
|
||||
// @Router /notice/removeOnlineUser/{userId} [delete]
|
||||
func (a *OnlineUserApi) RemoveOnlineUser(c *gin.Context) {
|
||||
userIdStr := c.Param("userId")
|
||||
userId, err := strconv.ParseUint(userIdStr, 10, 32)
|
||||
if err != nil {
|
||||
response.FailWithMessage("无效的用户ID", c)
|
||||
return
|
||||
}
|
||||
|
||||
//// 检查权限:只有管理员或用户本人可以移除
|
||||
//currentUserId := utils.GetUserID(c)
|
||||
//if currentUserId != uint(userId) {
|
||||
// // 这里可以添加管理员权限检查
|
||||
// // 暂时允许所有用户操作,实际项目中应该添加权限验证
|
||||
//}
|
||||
|
||||
err = onlineUserService.RemoveOnlineUser(uint(userId))
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("移除在线用户失败!" + err.Error())
|
||||
response.FailWithMessage("移除在线用户失败", c)
|
||||
return
|
||||
}
|
||||
|
||||
response.OkWithMessage("移除成功", c)
|
||||
}
|
||||
|
||||
// CleanOfflineUsers 清理离线用户
|
||||
// @Tags NoticeCenter
|
||||
// @Summary 清理长时间未活跃的离线用户
|
||||
// @Security ApiKeyAuth
|
||||
// @accept application/json
|
||||
// @Produce application/json
|
||||
// @Success 200 {object} response.Response{msg=string} "清理成功"
|
||||
// @Router /notice/cleanOfflineUsers [post]
|
||||
func (a *OnlineUserApi) CleanOfflineUsers(c *gin.Context) {
|
||||
// 清理30分钟未活跃的用户
|
||||
err := onlineUserService.CleanOfflineUsers(30 * time.Minute)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("清理离线用户失败!" + err.Error())
|
||||
response.FailWithMessage("清理离线用户失败", c)
|
||||
return
|
||||
}
|
||||
|
||||
response.OkWithMessage("清理成功", c)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package config
|
||||
|
||||
type Config struct {
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"gorm.io/gen"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
//go:generate go mod tidy
|
||||
//go:generate go mod download
|
||||
//go:generate go run gen.go
|
||||
func main() {
|
||||
g := gen.NewGenerator(gen.Config{
|
||||
OutPath: filepath.Join("..", "..", "..", "notice", "blender", "model", "dao"),
|
||||
Mode: gen.WithoutContext | gen.WithDefaultQuery | gen.WithQueryInterface,
|
||||
})
|
||||
g.ApplyBasic()
|
||||
g.Execute()
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package initialize
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
model "github.com/flipped-aurora/gin-vue-admin/server/model/system"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/plugin/plugin-tool/utils"
|
||||
)
|
||||
|
||||
func Api(ctx context.Context) {
|
||||
entities := []model.SysApi{
|
||||
// 通知管理相关API
|
||||
{
|
||||
Path: "/notice/notification/createNotification",
|
||||
Description: "创建通知",
|
||||
ApiGroup: "通知管理",
|
||||
Method: "POST",
|
||||
},
|
||||
{
|
||||
Path: "/notice/notification/updateNotification",
|
||||
Description: "更新通知",
|
||||
ApiGroup: "通知管理",
|
||||
Method: "PUT",
|
||||
},
|
||||
{
|
||||
Path: "/notice/notification/deleteNotification",
|
||||
Description: "删除通知",
|
||||
ApiGroup: "通知管理",
|
||||
Method: "DELETE",
|
||||
},
|
||||
{
|
||||
Path: "/notice/notification/publishNotification/:id",
|
||||
Description: "发布通知",
|
||||
ApiGroup: "通知管理",
|
||||
Method: "POST",
|
||||
},
|
||||
{
|
||||
Path: "/notice/notification/sendNotification",
|
||||
Description: "发送通知",
|
||||
ApiGroup: "通知管理",
|
||||
Method: "POST",
|
||||
},
|
||||
{
|
||||
Path: "/notice/notification/getNotificationList",
|
||||
Description: "获取通知列表",
|
||||
ApiGroup: "通知管理",
|
||||
Method: "POST",
|
||||
},
|
||||
{
|
||||
Path: "/notice/notification/getNotificationById/:id",
|
||||
Description: "根据ID获取通知",
|
||||
ApiGroup: "通知管理",
|
||||
Method: "GET",
|
||||
},
|
||||
{
|
||||
Path: "/notice/notification/getUserNotifications",
|
||||
Description: "获取用户通知列表",
|
||||
ApiGroup: "通知管理",
|
||||
Method: "POST",
|
||||
},
|
||||
{
|
||||
Path: "/notice/notification/markAsRead",
|
||||
Description: "标记通知已读",
|
||||
ApiGroup: "通知管理",
|
||||
Method: "POST",
|
||||
},
|
||||
{
|
||||
Path: "/notice/notification/getNotificationStats",
|
||||
Description: "获取通知统计",
|
||||
ApiGroup: "通知管理",
|
||||
Method: "GET",
|
||||
},
|
||||
// 在线用户管理相关API
|
||||
{
|
||||
Path: "/notice/onlineUser/removeOnlineUser/:userId",
|
||||
Description: "移除在线用户",
|
||||
ApiGroup: "在线用户管理",
|
||||
Method: "DELETE",
|
||||
},
|
||||
{
|
||||
Path: "/notice/onlineUser/cleanOfflineUsers",
|
||||
Description: "清理离线用户",
|
||||
ApiGroup: "在线用户管理",
|
||||
Method: "POST",
|
||||
},
|
||||
{
|
||||
Path: "/notice/onlineUser/getOnlineUsers",
|
||||
Description: "获取在线用户列表",
|
||||
ApiGroup: "在线用户管理",
|
||||
Method: "GET",
|
||||
},
|
||||
{
|
||||
Path: "/notice/onlineUser/getOnlineUsers",
|
||||
Description: "获取在线用户列表(分页)",
|
||||
ApiGroup: "在线用户管理",
|
||||
Method: "POST",
|
||||
},
|
||||
{
|
||||
Path: "/notice/onlineUser/getOnlineUsersByRole/:roleId",
|
||||
Description: "根据角色获取在线用户",
|
||||
ApiGroup: "在线用户管理",
|
||||
Method: "GET",
|
||||
},
|
||||
{
|
||||
Path: "/notice/onlineUser/checkUserOnline/:userId",
|
||||
Description: "检查用户是否在线",
|
||||
ApiGroup: "在线用户管理",
|
||||
Method: "GET",
|
||||
},
|
||||
{
|
||||
Path: "/notice/onlineUser/getOnlineUserCount",
|
||||
Description: "获取在线用户数量",
|
||||
ApiGroup: "在线用户管理",
|
||||
Method: "GET",
|
||||
},
|
||||
{
|
||||
Path: "/notice/onlineUser/getOnlineUserStats",
|
||||
Description: "获取在线用户统计数据",
|
||||
ApiGroup: "在线用户管理",
|
||||
Method: "GET",
|
||||
},
|
||||
// Socket.IO相关API
|
||||
{
|
||||
Path: "/socket.io/*any",
|
||||
Description: "Socket.IO连接",
|
||||
ApiGroup: "实时通信",
|
||||
Method: "GET",
|
||||
},
|
||||
{
|
||||
Path: "/socket.io/*any",
|
||||
Description: "Socket.IO连接",
|
||||
ApiGroup: "实时通信",
|
||||
Method: "POST",
|
||||
},
|
||||
}
|
||||
utils.RegisterApis(entities...)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package initialize
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/plugin/notice/model"
|
||||
"github.com/pkg/errors"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func Gorm(ctx context.Context) {
|
||||
err := global.GVA_DB.WithContext(ctx).AutoMigrate(
|
||||
&model.Notification{},
|
||||
&model.UserRead{},
|
||||
&model.OnlineUser{},
|
||||
)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "注册表失败!")
|
||||
zap.L().Error(fmt.Sprintf("%+v", err))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package initialize
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
model "github.com/flipped-aurora/gin-vue-admin/server/model/system"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/plugin/plugin-tool/utils"
|
||||
)
|
||||
|
||||
func Menu(ctx context.Context) {
|
||||
entities := []model.SysBaseMenu{
|
||||
{
|
||||
MenuLevel: 0,
|
||||
ParentId: 0,
|
||||
Path: "notice",
|
||||
Name: "notice",
|
||||
Hidden: false,
|
||||
Component: "view/routerHolder.vue",
|
||||
Sort: 5,
|
||||
Meta: model.Meta{
|
||||
Title: "通知中心",
|
||||
Icon: "notification",
|
||||
},
|
||||
},
|
||||
{
|
||||
MenuLevel: 0,
|
||||
ParentId: 0, // 这里需要在运行时动态设置为父菜单ID
|
||||
Path: "notificationManage",
|
||||
Name: "notificationManage",
|
||||
Hidden: false,
|
||||
Component: "view/notice/notification/index.vue",
|
||||
Sort: 1,
|
||||
Meta: model.Meta{
|
||||
Title: "通知管理",
|
||||
Icon: "message",
|
||||
},
|
||||
},
|
||||
{
|
||||
MenuLevel: 0,
|
||||
ParentId: 0, // 这里需要在运行时动态设置为父菜单ID
|
||||
Path: "onlineUserManage",
|
||||
Name: "onlineUserManage",
|
||||
Hidden: false,
|
||||
Component: "view/notice/onlineUser/index.vue",
|
||||
Sort: 2,
|
||||
Meta: model.Meta{
|
||||
Title: "在线用户",
|
||||
Icon: "user",
|
||||
},
|
||||
},
|
||||
{
|
||||
MenuLevel: 0,
|
||||
ParentId: 0, // 这里需要在运行时动态设置为父菜单ID
|
||||
Path: "userCenter",
|
||||
Name: "userCenter",
|
||||
Hidden: false,
|
||||
Component: "view/notice/userCenter/index.vue",
|
||||
Sort: 3,
|
||||
Meta: model.Meta{
|
||||
Title: "我的通知",
|
||||
Icon: "bell",
|
||||
},
|
||||
},
|
||||
}
|
||||
utils.RegisterMenus(entities...)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package initialize
|
||||
|
||||
import (
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/middleware"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/plugin/notice/router"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func Router(engine *gin.Engine) {
|
||||
public := engine.Group(global.GVA_CONFIG.System.RouterPrefix).Group("")
|
||||
private := engine.Group(global.GVA_CONFIG.System.RouterPrefix).Group("")
|
||||
private.Use(middleware.JWTAuth()).Use(middleware.CasbinHandler())
|
||||
|
||||
// 初始化通知中心路由
|
||||
noticeGroup := private.Group("notice")
|
||||
{
|
||||
// 初始化通知管理路由
|
||||
router.RouterGroupApp.NotificationRouter.InitNotificationRouter(noticeGroup)
|
||||
|
||||
// 初始化在线用户管理路由
|
||||
router.RouterGroupApp.OnlineUserRouter.InitOnlineUserRouter(noticeGroup)
|
||||
|
||||
// 初始化Socket.IO路由
|
||||
router.RouterGroupApp.SocketRouter.InitSocketRouter(public)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package initialize
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/plugin/notice/plugin"
|
||||
"github.com/pkg/errors"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func Viper() {
|
||||
err := global.GVA_VP.UnmarshalKey("notice", &plugin.Config)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "初始化配置文件失败!")
|
||||
zap.L().Error(fmt.Sprintf("%+v", err))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Notification 通知信息模型
|
||||
type Notification struct {
|
||||
global.GVA_MODEL
|
||||
Title string `json:"title" gorm:"column:title;type:varchar(255);not null;comment:通知标题"`
|
||||
Content string `json:"content" gorm:"column:content;type:text;not null;comment:通知内容"`
|
||||
Type string `json:"type" gorm:"column:type;type:varchar(50);not null;default:system;comment:通知类型:system-系统通知,personal-个人通知,role-角色通知"`
|
||||
Priority string `json:"priority" gorm:"column:priority;type:varchar(20);not null;default:normal;comment:通知优先级:low-低,normal-普通,high-高,urgent-紧急"`
|
||||
SenderId uint `json:"senderId" gorm:"column:sender_id;not null;comment:发送者用户ID"`
|
||||
TargetType string `json:"targetType" gorm:"column:target_type;type:varchar(20);not null;default:all;comment:目标类型:all-全部用户,user-指定用户,role-指定角色"`
|
||||
TargetIds string `json:"targetIds" gorm:"column:target_ids;type:text;comment:目标用户或角色ID列表,JSON格式"`
|
||||
Status string `json:"status" gorm:"column:status;type:varchar(20);not null;default:draft;comment:通知状态:draft-草稿,published-已发布,expired-已过期"`
|
||||
PublishTime *time.Time `json:"publishTime" gorm:"column:publish_time;comment:通知发布时间"`
|
||||
ExpireTime *time.Time `json:"expireTime" gorm:"column:expire_time;comment:通知过期时间"`
|
||||
}
|
||||
|
||||
// TableName 设置表名
|
||||
func (Notification) TableName() string {
|
||||
return "notice_notifications"
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||
"time"
|
||||
)
|
||||
|
||||
// OnlineUser 在线用户模型
|
||||
type OnlineUser struct {
|
||||
global.GVA_MODEL
|
||||
UserId uint `json:"userId" gorm:"column:user_id;not null;uniqueIndex;comment:用户ID"`
|
||||
SocketId string `json:"socketId" gorm:"column:socket_id;type:varchar(255);not null;comment:Socket连接ID"`
|
||||
LastActiveTime time.Time `json:"lastActiveTime" gorm:"column:last_active_time;not null;comment:最后活跃时间"`
|
||||
Status string `json:"status" gorm:"column:status;type:varchar(20);not null;default:online;comment:在线状态:online-在线,offline-离线"`
|
||||
}
|
||||
|
||||
// TableName 设置表名
|
||||
func (OnlineUser) TableName() string {
|
||||
return "notice_online_users"
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package request
|
||||
|
||||
import (
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/common/request"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NotificationSearch 通知搜索请求
|
||||
type NotificationSearch struct {
|
||||
request.PageInfo
|
||||
Title string `json:"title" form:"title"`
|
||||
Type string `json:"type" form:"type"`
|
||||
Priority string `json:"priority" form:"priority"`
|
||||
Status string `json:"status" form:"status"`
|
||||
SenderId uint `json:"senderId" form:"senderId"`
|
||||
TargetType string `json:"targetType" form:"targetType"`
|
||||
StartTime *time.Time `json:"startTime" form:"startTime"`
|
||||
EndTime *time.Time `json:"endTime" form:"endTime"`
|
||||
}
|
||||
|
||||
// CreateNotificationRequest 创建通知请求
|
||||
type CreateNotificationRequest struct {
|
||||
Title string `json:"title" binding:"required" form:"title"`
|
||||
Content string `json:"content" binding:"required" form:"content"`
|
||||
Type string `json:"type" binding:"required" form:"type"`
|
||||
Priority string `json:"priority" form:"priority"`
|
||||
TargetType string `json:"targetType" binding:"required" form:"targetType"`
|
||||
TargetIds []uint `json:"targetIds" form:"targetIds"`
|
||||
PublishTime *time.Time `json:"publishTime" form:"publishTime"`
|
||||
ExpireTime *time.Time `json:"expireTime" form:"expireTime"`
|
||||
}
|
||||
|
||||
// UpdateNotificationRequest 更新通知请求
|
||||
type UpdateNotificationRequest struct {
|
||||
ID uint `json:"id" binding:"required"`
|
||||
Title string `json:"title" binding:"required"`
|
||||
Content string `json:"content" binding:"required"`
|
||||
Type string `json:"type" binding:"required"`
|
||||
Priority string `json:"priority" binding:"required"`
|
||||
TargetType string `json:"targetType" binding:"required"`
|
||||
TargetIds []uint `json:"targetIds"`
|
||||
Status string `json:"status" binding:"required"`
|
||||
PublishTime *time.Time `json:"publishTime"`
|
||||
ExpireTime *time.Time `json:"expireTime"`
|
||||
}
|
||||
|
||||
// SendNotificationRequest 发送通知请求
|
||||
type SendNotificationRequest struct {
|
||||
Title string `json:"title" binding:"required"`
|
||||
Content string `json:"content" binding:"required"`
|
||||
Type string `json:"type" binding:"required"`
|
||||
Priority string `json:"priority" binding:"required"`
|
||||
TargetType string `json:"targetType" binding:"required"`
|
||||
TargetIds []uint `json:"targetIds"`
|
||||
RoleIds []uint `json:"roleIds"`
|
||||
}
|
||||
|
||||
// MarkReadRequest 标记已读请求
|
||||
type MarkReadRequest struct {
|
||||
NotificationIds []uint `json:"notificationIds" binding:"required"`
|
||||
}
|
||||
|
||||
// UserNotificationSearch 用户通知搜索请求
|
||||
type UserNotificationSearch struct {
|
||||
request.PageInfo
|
||||
IsRead *bool `json:"isRead" form:"isRead"`
|
||||
Type string `json:"type" form:"type"`
|
||||
Priority string `json:"priority" form:"priority"`
|
||||
StartTime *time.Time `json:"startTime" form:"startTime"`
|
||||
EndTime *time.Time `json:"endTime" form:"endTime"`
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package request
|
||||
|
||||
import "github.com/flipped-aurora/gin-vue-admin/server/model/common/request"
|
||||
|
||||
// OnlineUserSearch 在线用户搜索请求
|
||||
type OnlineUserSearch struct {
|
||||
request.PageInfo
|
||||
Username string `json:"username" form:"username"` // 用户名搜索
|
||||
NickName string `json:"nickName" form:"nickName"` // 昵称搜索
|
||||
RoleId uint `json:"roleId" form:"roleId"` // 角色ID过滤
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package response
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// NotificationResponse 通知响应
|
||||
type NotificationResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
Type string `json:"type"`
|
||||
Priority string `json:"priority"`
|
||||
SenderId uint `json:"senderId"`
|
||||
SenderName string `json:"senderName"`
|
||||
TargetType string `json:"targetType"`
|
||||
TargetIds string `json:"targetIds"`
|
||||
Status string `json:"status"`
|
||||
PublishTime *time.Time `json:"publishTime"`
|
||||
ExpireTime *time.Time `json:"expireTime"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
// UserNotificationResponse 用户通知响应
|
||||
type UserNotificationResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
Type string `json:"type"`
|
||||
Priority string `json:"priority"`
|
||||
SenderId uint `json:"senderId"`
|
||||
SenderName string `json:"senderName"`
|
||||
IsRead bool `json:"isRead"`
|
||||
ReadTime *time.Time `json:"readTime"`
|
||||
PublishTime *time.Time `json:"publishTime"`
|
||||
ExpireTime *time.Time `json:"expireTime"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
// NotificationStatsResponse 通知统计响应
|
||||
type NotificationStatsResponse struct {
|
||||
TotalCount int64 `json:"totalCount"`
|
||||
UnreadCount int64 `json:"unreadCount"`
|
||||
ReadCount int64 `json:"readCount"`
|
||||
}
|
||||
|
||||
// OnlineUserResponse 在线用户响应
|
||||
type OnlineUserResponse struct {
|
||||
UserId uint `json:"userId"` // 用户ID
|
||||
Username string `json:"username"` // 用户名
|
||||
NickName string `json:"nickName"` // 昵称
|
||||
SocketId string `json:"socketId"` // Socket连接ID
|
||||
LastActiveTime time.Time `json:"lastActiveTime"` // 最后活跃时间
|
||||
Status string `json:"status"` // 状态
|
||||
OnlineDuration int64 `json:"onlineDuration"` // 在线时长(分钟)
|
||||
}
|
||||
|
||||
// OnlineUserStatsResponse 在线用户统计响应结构体
|
||||
type OnlineUserStatsResponse struct {
|
||||
TotalOnline int64 `json:"totalOnline"` // 当前在线总数
|
||||
TodayLogin int64 `json:"todayLogin"` // 今日登录人数
|
||||
PeakOnline int64 `json:"peakOnline"` // 峰值在线人数
|
||||
AverageOnline int64 `json:"averageOnline"` // 平均在线人数
|
||||
}
|
||||
|
||||
// SendNotificationResponse 发送通知响应
|
||||
type SendNotificationResponse struct {
|
||||
NotificationId uint `json:"notificationId"`
|
||||
SuccessCount int `json:"successCount"`
|
||||
FailCount int `json:"failCount"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||
"time"
|
||||
)
|
||||
|
||||
// UserRead 用户通知读取状态模型
|
||||
type UserRead struct {
|
||||
global.GVA_MODEL
|
||||
UserId uint `json:"userId" gorm:"column:user_id;not null;index;comment:用户ID"`
|
||||
NotificationId uint `json:"notificationId" gorm:"column:notification_id;not null;index;comment:通知ID"`
|
||||
IsRead bool `json:"isRead" gorm:"column:is_read;not null;default:false;comment:是否已读"`
|
||||
ReadTime *time.Time `json:"readTime" gorm:"column:read_time;comment:通知读取时间"`
|
||||
}
|
||||
|
||||
// TableName 设置表名
|
||||
func (UserRead) TableName() string {
|
||||
return "notice_user_reads"
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package notice
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/plugin/notice/initialize"
|
||||
interfaces "github.com/flipped-aurora/gin-vue-admin/server/utils/plugin/v2"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
var _ interfaces.Plugin = (*plugin)(nil)
|
||||
|
||||
var Plugin = new(plugin)
|
||||
|
||||
type plugin struct{}
|
||||
|
||||
// Register 注册插件
|
||||
func (p *plugin) Register(group *gin.Engine) {
|
||||
ctx := context.Background()
|
||||
// 初始化数据库
|
||||
initialize.Gorm(ctx)
|
||||
// 初始化路由
|
||||
initialize.Router(group)
|
||||
// 初始化菜单
|
||||
initialize.Menu(ctx)
|
||||
// 初始化 api
|
||||
initialize.Api(ctx)
|
||||
}
|
||||
|
||||
// RouterPath 返回插件路由路径
|
||||
func (p *plugin) RouterPath() string {
|
||||
return "notice"
|
||||
}
|
||||
|
||||
// Name 返回插件名称
|
||||
func (p *plugin) Name() string {
|
||||
return "notice"
|
||||
}
|
||||
|
||||
// Description 返回插件描述
|
||||
func (p *plugin) Description() string {
|
||||
return "通知中心插件,提供实时通知、在线用户管理等功能"
|
||||
}
|
||||
|
||||
// Version 返回插件版本
|
||||
func (p *plugin) Version() string {
|
||||
return "v1.0.0"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package plugin
|
||||
|
||||
import "github.com/flipped-aurora/gin-vue-admin/server/plugin/notice/config"
|
||||
|
||||
var Config config.Config
|
||||
@@ -0,0 +1,9 @@
|
||||
package router
|
||||
|
||||
type RouterGroup struct {
|
||||
NotificationRouter
|
||||
OnlineUserRouter
|
||||
SocketRouter
|
||||
}
|
||||
|
||||
var RouterGroupApp = new(RouterGroup)
|
||||
@@ -0,0 +1,41 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/middleware"
|
||||
noticeApi "github.com/flipped-aurora/gin-vue-admin/server/plugin/notice/api"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type NotificationRouter struct{}
|
||||
|
||||
// InitNotificationRouter 初始化通知路由
|
||||
func (r *NotificationRouter) InitNotificationRouter(Router *gin.RouterGroup) {
|
||||
notificationRouter := Router.Group("notification")
|
||||
notificationRouterWithoutRecord := Router.Group("notification")
|
||||
|
||||
// 需要记录操作日志的路由
|
||||
notificationRouter.Use(middleware.OperationRecord())
|
||||
{
|
||||
// 管理员接口
|
||||
notificationRouter.POST("createNotification", noticeApi.ApiGroupApp.NotificationApi.CreateNotification) // 创建通知
|
||||
notificationRouter.PUT("updateNotification", noticeApi.ApiGroupApp.NotificationApi.UpdateNotification) // 更新通知
|
||||
notificationRouter.DELETE("deleteNotification", noticeApi.ApiGroupApp.NotificationApi.DeleteNotification) // 删除通知
|
||||
notificationRouter.POST("publishNotification/:id", noticeApi.ApiGroupApp.NotificationApi.PublishNotification) // 发布通知
|
||||
notificationRouter.POST("sendNotification", noticeApi.ApiGroupApp.NotificationApi.SendNotification) // 发送通知
|
||||
}
|
||||
|
||||
// 不需要记录操作日志的路由
|
||||
notificationRouterWithoutRecord.Use(middleware.JWTAuth())
|
||||
{
|
||||
// 查询接口
|
||||
notificationRouterWithoutRecord.POST("getNotificationList", noticeApi.ApiGroupApp.NotificationApi.GetNotificationList) // 获取通知列表
|
||||
notificationRouterWithoutRecord.GET("getNotificationById/:id", noticeApi.ApiGroupApp.NotificationApi.GetNotificationById) // 根据ID获取通知
|
||||
|
||||
// 用户接口
|
||||
notificationRouterWithoutRecord.POST("getUserNotifications", noticeApi.ApiGroupApp.NotificationApi.GetUserNotifications) // 获取用户通知列表
|
||||
notificationRouterWithoutRecord.POST("markAsRead", noticeApi.ApiGroupApp.NotificationApi.MarkNotificationAsRead) // 标记已读
|
||||
notificationRouterWithoutRecord.GET("getNotificationStats", noticeApi.ApiGroupApp.NotificationApi.GetNotificationStats) // 获取通知统计
|
||||
notificationRouterWithoutRecord.DELETE("deleteUserNotification/:notificationId", noticeApi.ApiGroupApp.NotificationApi.DeleteUserNotification) // 删除用户通知
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/middleware"
|
||||
noticeApi "github.com/flipped-aurora/gin-vue-admin/server/plugin/notice/api"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type OnlineUserRouter struct{}
|
||||
|
||||
// InitOnlineUserRouter 初始化在线用户路由
|
||||
func (r *OnlineUserRouter) InitOnlineUserRouter(Router *gin.RouterGroup) {
|
||||
onlineUserRouter := Router.Group("onlineUser")
|
||||
onlineUserRouterWithoutRecord := Router.Group("onlineUser")
|
||||
|
||||
// 需要记录操作日志的路由
|
||||
onlineUserRouter.Use(middleware.OperationRecord())
|
||||
{
|
||||
// 管理员接口
|
||||
onlineUserRouter.DELETE("removeOnlineUser/:userId", noticeApi.ApiGroupApp.OnlineUserApi.RemoveOnlineUser) // 移除在线用户
|
||||
onlineUserRouter.POST("cleanOfflineUsers", noticeApi.ApiGroupApp.OnlineUserApi.CleanOfflineUsers) // 清理离线用户
|
||||
}
|
||||
|
||||
// 不需要记录操作日志的路由
|
||||
onlineUserRouterWithoutRecord.Use(middleware.JWTAuth())
|
||||
{
|
||||
// 查询接口
|
||||
onlineUserRouterWithoutRecord.GET("getOnlineUsers", noticeApi.ApiGroupApp.OnlineUserApi.GetOnlineUsers) // 获取在线用户列表
|
||||
onlineUserRouterWithoutRecord.POST("getOnlineUsers", noticeApi.ApiGroupApp.OnlineUserApi.GetOnlineUsersWithPagination) // 获取在线用户列表(分页)
|
||||
onlineUserRouterWithoutRecord.GET("getOnlineUsersByRole/:roleId", noticeApi.ApiGroupApp.OnlineUserApi.GetOnlineUsersByRole) // 根据角色获取在线用户
|
||||
onlineUserRouterWithoutRecord.GET("checkUserOnline/:userId", noticeApi.ApiGroupApp.OnlineUserApi.CheckUserOnline) // 检查用户是否在线
|
||||
onlineUserRouterWithoutRecord.GET("getOnlineUserCount", noticeApi.ApiGroupApp.OnlineUserApi.GetOnlineUserCount) // 获取在线用户数量
|
||||
onlineUserRouterWithoutRecord.GET("getOnlineUserStats", noticeApi.ApiGroupApp.OnlineUserApi.GetOnlineUserStats) // 获取在线用户统计数据
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/utils"
|
||||
"go.uber.org/zap"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/doquangtan/socketio/v4"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||
noticeService "github.com/flipped-aurora/gin-vue-admin/server/plugin/notice/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type SocketRouter struct{}
|
||||
|
||||
var (
|
||||
onlineUserService = noticeService.ServiceGroupApp.OnlineUserService
|
||||
socketService = noticeService.ServiceGroupApp.SocketService
|
||||
)
|
||||
|
||||
// InitSocketRouter 初始化Socket.IO路由
|
||||
func (r *SocketRouter) InitSocketRouter(Router *gin.RouterGroup) {
|
||||
// 创建Socket.IO服务器
|
||||
io := socketio.New()
|
||||
var claims *request.CustomClaims
|
||||
// 认证中间件
|
||||
io.OnAuthentication(func(params map[string]string) bool {
|
||||
// 从参数获取token
|
||||
token, ok := params["token"]
|
||||
if !ok || token == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
j := utils.NewJWT()
|
||||
// parseToken 解析token包含的信息
|
||||
claimsP, err := j.ParseToken(token)
|
||||
if err != nil {
|
||||
zap.L().Error("JWT解析失败: " + err.Error())
|
||||
return false
|
||||
}
|
||||
claims = claimsP
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
// 连接事件
|
||||
io.OnConnection(func(socket *socketio.Socket) {
|
||||
if claims == nil {
|
||||
global.GVA_LOG.Error("用户认证失败,无法建立连接")
|
||||
socket.Disconnect()
|
||||
return
|
||||
}
|
||||
userInfo := claims
|
||||
userID := userInfo.BaseClaims.ID
|
||||
socketID := socket.Id
|
||||
|
||||
// 加入用户房间
|
||||
userRoom := "user_" + strconv.Itoa(int(userID))
|
||||
socket.Join(userRoom)
|
||||
// 添加到在线用户列表
|
||||
err := onlineUserService.AddOnlineUser(userID, socketID)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("添加在线用户失败: " + err.Error())
|
||||
}
|
||||
// 添加到Socket连接管理
|
||||
socketService.AddConnection(userID, socketID, socket)
|
||||
|
||||
// 记录用户连接信息
|
||||
global.GVA_LOG.Info("用户连接详情: UserID=" + strconv.Itoa(int(userID)) +
|
||||
", SocketID=" + socketID)
|
||||
|
||||
// 发送用户上线通知
|
||||
socket.Emit("user_online", map[string]interface{}{
|
||||
"userId": userID,
|
||||
"socketId": socketID,
|
||||
"timestamp": time.Now(),
|
||||
"message": "连接成功",
|
||||
})
|
||||
|
||||
global.GVA_LOG.Info("用户连接成功: UserID=" + strconv.Itoa(int(userID)) + ", SocketID=" + socketID)
|
||||
|
||||
// 监听心跳事件
|
||||
socket.On("heartbeat", func(event *socketio.EventPayload) {
|
||||
// 更新用户活跃时间
|
||||
err := onlineUserService.UpdateUserActivity(userID)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("更新用户活跃时间失败: " + err.Error())
|
||||
}
|
||||
|
||||
// 回复心跳
|
||||
socket.Emit("heartbeat_reply", map[string]interface{}{
|
||||
"timestamp": time.Now(),
|
||||
"status": "ok",
|
||||
})
|
||||
})
|
||||
|
||||
// 监听通知已读事件
|
||||
socket.On("mark_notification_read", func(event *socketio.EventPayload) {
|
||||
if event.Data != nil && len(event.Data) > 0 {
|
||||
if dataMap, ok := event.Data[0].(map[string]interface{}); ok {
|
||||
if notificationIds, exists := dataMap["notificationIds"]; exists {
|
||||
if ids, ok := notificationIds.([]interface{}); ok {
|
||||
var uintIds []uint
|
||||
for _, id := range ids {
|
||||
if idFloat, ok := id.(float64); ok {
|
||||
uintIds = append(uintIds, uint(idFloat))
|
||||
}
|
||||
}
|
||||
|
||||
// 标记通知为已读
|
||||
notificationService := noticeService.ServiceGroupApp.NotificationService
|
||||
err := notificationService.MarkNotificationsAsRead(userID, uintIds)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("标记通知已读失败: " + err.Error())
|
||||
socket.Emit("error", map[string]interface{}{
|
||||
"message": "标记通知已读失败",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 发送成功响应
|
||||
socket.Emit("notification_read_success", map[string]interface{}{
|
||||
"notificationIds": uintIds,
|
||||
"timestamp": time.Now(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 监听获取通知统计事件
|
||||
socket.On("get_notification_stats", func(event *socketio.EventPayload) {
|
||||
notificationService := noticeService.ServiceGroupApp.NotificationService
|
||||
stats, err := notificationService.GetNotificationStats(userID)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("获取通知统计失败: " + err.Error())
|
||||
socket.Emit("error", map[string]interface{}{
|
||||
"message": "获取通知统计失败",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
socket.Emit("notification_stats", stats)
|
||||
})
|
||||
|
||||
// 监听断开连接事件
|
||||
socket.On("disconnect", func(event *socketio.EventPayload) {
|
||||
// 从在线用户列表移除
|
||||
err := onlineUserService.RemoveOnlineUser(userID)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("移除在线用户失败: " + err.Error())
|
||||
}
|
||||
|
||||
// 从Socket连接管理移除
|
||||
socketService.RemoveConnectionByUserId(userID)
|
||||
|
||||
global.GVA_LOG.Info("用户断开连接: UserID=" + strconv.Itoa(int(userID)) + ", SocketID=" + socketID)
|
||||
})
|
||||
})
|
||||
|
||||
// 注册Socket.IO路由
|
||||
Router.GET("/socket.io/*any", gin.WrapH(io.HttpHandler()))
|
||||
Router.POST("/socket.io/*any", gin.WrapH(io.HttpHandler()))
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package service
|
||||
|
||||
import "github.com/doquangtan/socketio/v4"
|
||||
|
||||
type ServiceGroup struct {
|
||||
NotificationService
|
||||
OnlineUserService
|
||||
SocketService
|
||||
}
|
||||
|
||||
var ServiceGroupApp = &ServiceGroup{
|
||||
SocketService: SocketService{
|
||||
connections: make(map[string]*socketio.Socket),
|
||||
userSockets: make(map[uint]string),
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,507 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||
systemModel "github.com/flipped-aurora/gin-vue-admin/server/model/system"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/plugin/notice/model"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/plugin/notice/model/request"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/plugin/notice/model/response"
|
||||
)
|
||||
|
||||
type NotificationService struct{}
|
||||
|
||||
// CreateNotification 创建通知
|
||||
func (s *NotificationService) CreateNotification(req request.CreateNotificationRequest, senderId uint) (notification model.Notification, err error) {
|
||||
// 将 []uint 类型的 TargetIds 转换为 JSON 字符串
|
||||
targetIdsJson := ""
|
||||
if len(req.TargetIds) > 0 {
|
||||
targetIdsBytes, err := json.Marshal(req.TargetIds)
|
||||
if err != nil {
|
||||
return notification, fmt.Errorf("序列化目标用户ID失败: %v", err)
|
||||
}
|
||||
targetIdsJson = string(targetIdsBytes)
|
||||
}
|
||||
|
||||
notification = model.Notification{
|
||||
Title: req.Title,
|
||||
Content: req.Content,
|
||||
Type: req.Type,
|
||||
Priority: req.Priority,
|
||||
SenderId: senderId,
|
||||
TargetType: req.TargetType,
|
||||
TargetIds: targetIdsJson,
|
||||
Status: "draft",
|
||||
PublishTime: req.PublishTime,
|
||||
ExpireTime: req.ExpireTime,
|
||||
}
|
||||
|
||||
err = global.GVA_DB.Create(¬ification).Error
|
||||
return notification, err
|
||||
}
|
||||
|
||||
// UpdateNotification 更新通知
|
||||
func (s *NotificationService) UpdateNotification(req request.UpdateNotificationRequest) error {
|
||||
var notification model.Notification
|
||||
if err := global.GVA_DB.First(¬ification, req.ID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 将 []uint 类型的 TargetIds 转换为 JSON 字符串
|
||||
targetIdsJson := ""
|
||||
if len(req.TargetIds) > 0 {
|
||||
targetIdsBytes, err := json.Marshal(req.TargetIds)
|
||||
if err != nil {
|
||||
return fmt.Errorf("序列化目标用户ID失败: %v", err)
|
||||
}
|
||||
targetIdsJson = string(targetIdsBytes)
|
||||
}
|
||||
|
||||
updates := map[string]interface{}{
|
||||
"title": req.Title,
|
||||
"content": req.Content,
|
||||
"type": req.Type,
|
||||
"priority": req.Priority,
|
||||
"target_type": req.TargetType,
|
||||
"target_ids": targetIdsJson,
|
||||
"status": req.Status,
|
||||
"publish_time": req.PublishTime,
|
||||
"expire_time": req.ExpireTime,
|
||||
}
|
||||
|
||||
return global.GVA_DB.Model(¬ification).Updates(updates).Error
|
||||
}
|
||||
|
||||
// DeleteNotification 删除通知
|
||||
func (s *NotificationService) DeleteNotification(id uint) error {
|
||||
return global.GVA_DB.Delete(&model.Notification{}, id).Error
|
||||
}
|
||||
|
||||
// GetNotificationList 获取通知列表
|
||||
func (s *NotificationService) GetNotificationList(req request.NotificationSearch) (list []response.NotificationResponse, total int64, err error) {
|
||||
limit := req.PageSize
|
||||
offset := req.PageSize * (req.Page - 1)
|
||||
db := global.GVA_DB.Model(&model.Notification{})
|
||||
|
||||
// 构建查询条件
|
||||
if req.Title != "" {
|
||||
db = db.Where("title LIKE ?", "%"+req.Title+"%")
|
||||
}
|
||||
if req.Type != "" {
|
||||
db = db.Where("type = ?", req.Type)
|
||||
}
|
||||
if req.Priority != "" {
|
||||
db = db.Where("priority = ?", req.Priority)
|
||||
}
|
||||
if req.Status != "" {
|
||||
db = db.Where("status = ?", req.Status)
|
||||
}
|
||||
if req.SenderId != 0 {
|
||||
db = db.Where("sender_id = ?", req.SenderId)
|
||||
}
|
||||
if req.TargetType != "" {
|
||||
db = db.Where("target_type = ?", req.TargetType)
|
||||
}
|
||||
if req.StartTime != nil {
|
||||
db = db.Where("created_at >= ?", req.StartTime)
|
||||
}
|
||||
if req.EndTime != nil {
|
||||
db = db.Where("created_at <= ?", req.EndTime)
|
||||
}
|
||||
|
||||
err = db.Count(&total).Error
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var notifications []model.Notification
|
||||
err = db.Limit(limit).Offset(offset).Order("created_at DESC").Find(¬ifications).Error
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取发送者信息
|
||||
var senderIds []uint
|
||||
for _, notification := range notifications {
|
||||
senderIds = append(senderIds, notification.SenderId)
|
||||
}
|
||||
|
||||
var users []systemModel.SysUser
|
||||
userMap := make(map[uint]string)
|
||||
if len(senderIds) > 0 {
|
||||
global.GVA_DB.Where("id IN ?", senderIds).Find(&users)
|
||||
for _, user := range users {
|
||||
userMap[user.ID] = user.Username
|
||||
}
|
||||
}
|
||||
|
||||
// 构建响应数据
|
||||
for _, notification := range notifications {
|
||||
senderName := userMap[notification.SenderId]
|
||||
list = append(list, response.NotificationResponse{
|
||||
ID: notification.ID,
|
||||
Title: notification.Title,
|
||||
Content: notification.Content,
|
||||
Type: notification.Type,
|
||||
Priority: notification.Priority,
|
||||
SenderId: notification.SenderId,
|
||||
SenderName: senderName,
|
||||
TargetType: notification.TargetType,
|
||||
TargetIds: notification.TargetIds,
|
||||
Status: notification.Status,
|
||||
PublishTime: notification.PublishTime,
|
||||
ExpireTime: notification.ExpireTime,
|
||||
CreatedAt: notification.CreatedAt,
|
||||
UpdatedAt: notification.UpdatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
return list, total, err
|
||||
}
|
||||
|
||||
// GetNotificationById 根据ID获取通知
|
||||
func (s *NotificationService) GetNotificationById(id uint) (notification model.Notification, err error) {
|
||||
err = global.GVA_DB.First(¬ification, id).Error
|
||||
return
|
||||
}
|
||||
|
||||
// PublishNotification 发布通知
|
||||
func (s *NotificationService) PublishNotification(id uint) error {
|
||||
// 获取通知详情
|
||||
var notification model.Notification
|
||||
if err := global.GVA_DB.First(¬ification, id).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 检查通知是否已经发布
|
||||
if notification.Status == "published" {
|
||||
return fmt.Errorf("通知已经发布")
|
||||
}
|
||||
|
||||
// 更新通知状态为已发布
|
||||
now := time.Now()
|
||||
if err := global.GVA_DB.Model(&model.Notification{}).Where("id = ?", id).Updates(map[string]interface{}{
|
||||
"status": "published",
|
||||
"publish_time": &now,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 重新获取更新后的通知信息
|
||||
if err := global.GVA_DB.First(¬ification, id).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 根据目标类型创建用户读取记录
|
||||
var targetUserIds []uint
|
||||
var targetIds []uint
|
||||
if notification.TargetIds != "" {
|
||||
json.Unmarshal([]byte(notification.TargetIds), &targetIds)
|
||||
}
|
||||
|
||||
switch notification.TargetType {
|
||||
case "all":
|
||||
// 获取所有用户ID
|
||||
var users []systemModel.SysUser
|
||||
global.GVA_DB.Select("id").Find(&users)
|
||||
for _, user := range users {
|
||||
targetUserIds = append(targetUserIds, user.ID)
|
||||
}
|
||||
case "users":
|
||||
targetUserIds = targetIds
|
||||
case "roles":
|
||||
// 根据角色获取用户ID
|
||||
if len(targetIds) > 0 {
|
||||
var users []systemModel.SysUser
|
||||
global.GVA_DB.Joins("JOIN sys_user_authority ON sys_users.id = sys_user_authority.sys_user_id").
|
||||
Where("sys_user_authority.sys_authority_id IN ?", targetIds).
|
||||
Select("DISTINCT sys_users.id").
|
||||
Find(&users)
|
||||
for _, user := range users {
|
||||
targetUserIds = append(targetUserIds, user.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 批量创建用户读取记录
|
||||
var userReads []model.UserRead
|
||||
for _, userId := range targetUserIds {
|
||||
userReads = append(userReads, model.UserRead{
|
||||
UserId: userId,
|
||||
NotificationId: notification.ID,
|
||||
IsRead: false,
|
||||
})
|
||||
}
|
||||
|
||||
if len(userReads) > 0 {
|
||||
if err := global.GVA_DB.CreateInBatches(userReads, 100).Error; err != nil {
|
||||
return fmt.Errorf("创建用户读取记录失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 通过Socket实时推送通知给在线用户
|
||||
socketService := ServiceGroupApp.SocketService
|
||||
switch notification.TargetType {
|
||||
case "all":
|
||||
// 广播给所有在线用户
|
||||
socketService.BroadcastNotification(¬ification)
|
||||
default:
|
||||
// 发送给指定的在线用户
|
||||
if len(targetUserIds) > 0 {
|
||||
socketService.SendNotificationToUsers(targetUserIds, ¬ification)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendNotification 发送通知
|
||||
func (s *NotificationService) SendNotification(req request.SendNotificationRequest, senderId uint) (response.SendNotificationResponse, error) {
|
||||
var result response.SendNotificationResponse
|
||||
|
||||
// 创建通知记录
|
||||
targetIds, _ := json.Marshal(req.TargetIds)
|
||||
notification := model.Notification{
|
||||
Title: req.Title,
|
||||
Content: req.Content,
|
||||
Type: req.Type,
|
||||
Priority: req.Priority,
|
||||
SenderId: senderId,
|
||||
TargetType: req.TargetType,
|
||||
TargetIds: string(targetIds),
|
||||
Status: "published",
|
||||
PublishTime: &time.Time{},
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
notification.PublishTime = &now
|
||||
|
||||
if err := global.GVA_DB.Create(¬ification).Error; err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
result.NotificationId = notification.ID
|
||||
|
||||
// 根据目标类型创建用户读取记录
|
||||
var targetUserIds []uint
|
||||
switch req.TargetType {
|
||||
case "all":
|
||||
// 获取所有用户ID
|
||||
var users []systemModel.SysUser
|
||||
global.GVA_DB.Select("id").Find(&users)
|
||||
for _, user := range users {
|
||||
targetUserIds = append(targetUserIds, user.ID)
|
||||
}
|
||||
case "user":
|
||||
targetUserIds = req.TargetIds
|
||||
case "role":
|
||||
// 根据角色获取用户ID
|
||||
if len(req.RoleIds) > 0 {
|
||||
var users []systemModel.SysUser
|
||||
global.GVA_DB.Joins("JOIN sys_user_authority ON sys_users.id = sys_user_authority.sys_user_id").
|
||||
Where("sys_user_authority.sys_authority_id IN ?", req.RoleIds).
|
||||
Select("DISTINCT sys_users.id").
|
||||
Find(&users)
|
||||
for _, user := range users {
|
||||
targetUserIds = append(targetUserIds, user.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 批量创建用户读取记录
|
||||
var userReads []model.UserRead
|
||||
for _, userId := range targetUserIds {
|
||||
userReads = append(userReads, model.UserRead{
|
||||
UserId: userId,
|
||||
NotificationId: notification.ID,
|
||||
IsRead: false,
|
||||
})
|
||||
}
|
||||
|
||||
if len(userReads) > 0 {
|
||||
if err := global.GVA_DB.CreateInBatches(userReads, 100).Error; err != nil {
|
||||
result.FailCount = len(userReads)
|
||||
result.Message = "创建用户读取记录失败: " + err.Error()
|
||||
return result, err
|
||||
}
|
||||
result.SuccessCount = len(userReads)
|
||||
}
|
||||
|
||||
result.Message = fmt.Sprintf("通知发送成功,共发送给 %d 个用户", result.SuccessCount)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetUserNotifications 获取用户通知列表
|
||||
func (s *NotificationService) GetUserNotifications(userId uint, req request.UserNotificationSearch) (list []response.UserNotificationResponse, total int64, err error) {
|
||||
limit := req.PageSize
|
||||
offset := req.PageSize * (req.Page - 1)
|
||||
|
||||
db := global.GVA_DB.Table("notice_user_reads ur").
|
||||
Select("n.id, n.title, n.content, n.type, n.priority, n.sender_id, n.publish_time, n.expire_time, n.created_at, ur.is_read, ur.read_time").
|
||||
Joins("JOIN notice_notifications n ON ur.notification_id = n.id").
|
||||
Where("ur.user_id = ? AND n.status = 'published'", userId)
|
||||
|
||||
// 构建查询条件
|
||||
if req.IsRead != nil {
|
||||
db = db.Where("ur.is_read = ?", *req.IsRead)
|
||||
}
|
||||
if req.Type != "" {
|
||||
db = db.Where("n.type = ?", req.Type)
|
||||
}
|
||||
if req.Priority != "" {
|
||||
db = db.Where("n.priority = ?", req.Priority)
|
||||
}
|
||||
if req.StartTime != nil {
|
||||
db = db.Where("n.created_at >= ?", req.StartTime)
|
||||
}
|
||||
if req.EndTime != nil {
|
||||
db = db.Where("n.created_at <= ?", req.EndTime)
|
||||
}
|
||||
|
||||
err = db.Count(&total).Error
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var results []struct {
|
||||
ID uint `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
Type string `json:"type"`
|
||||
Priority string `json:"priority"`
|
||||
SenderId uint `json:"sender_id"`
|
||||
PublishTime *time.Time `json:"publish_time"`
|
||||
ExpireTime *time.Time `json:"expire_time"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
IsRead bool `json:"is_read"`
|
||||
ReadTime *time.Time `json:"read_time"`
|
||||
}
|
||||
|
||||
err = db.Limit(limit).Offset(offset).Order("n.created_at DESC").Scan(&results).Error
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取发送者信息
|
||||
var senderIds []uint
|
||||
for _, result := range results {
|
||||
senderIds = append(senderIds, result.SenderId)
|
||||
}
|
||||
|
||||
var users []systemModel.SysUser
|
||||
userMap := make(map[uint]string)
|
||||
if len(senderIds) > 0 {
|
||||
global.GVA_DB.Where("id IN ?", senderIds).Find(&users)
|
||||
for _, user := range users {
|
||||
userMap[user.ID] = user.Username
|
||||
}
|
||||
}
|
||||
|
||||
// 构建响应数据
|
||||
for _, result := range results {
|
||||
senderName := userMap[result.SenderId]
|
||||
list = append(list, response.UserNotificationResponse{
|
||||
ID: result.ID,
|
||||
Title: result.Title,
|
||||
Content: result.Content,
|
||||
Type: result.Type,
|
||||
Priority: result.Priority,
|
||||
SenderId: result.SenderId,
|
||||
SenderName: senderName,
|
||||
IsRead: result.IsRead,
|
||||
ReadTime: result.ReadTime,
|
||||
PublishTime: result.PublishTime,
|
||||
ExpireTime: result.ExpireTime,
|
||||
CreatedAt: result.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
return list, total, err
|
||||
}
|
||||
|
||||
// MarkNotificationsAsRead 标记通知为已读
|
||||
func (s *NotificationService) MarkNotificationsAsRead(userId uint, notificationIds []uint) error {
|
||||
now := time.Now()
|
||||
return global.GVA_DB.Model(&model.UserRead{}).
|
||||
Where("user_id = ? AND notification_id IN ?", userId, notificationIds).
|
||||
Updates(map[string]interface{}{
|
||||
"is_read": true,
|
||||
"read_time": &now,
|
||||
}).Error
|
||||
}
|
||||
|
||||
// GetNotificationStats 获取用户通知统计
|
||||
func (s *NotificationService) GetNotificationStats(userId uint) (response.NotificationStatsResponse, error) {
|
||||
var stats response.NotificationStatsResponse
|
||||
|
||||
// 总通知数
|
||||
err := global.GVA_DB.Table("notice_user_reads ur").
|
||||
Joins("JOIN notice_notifications n ON ur.notification_id = n.id").
|
||||
Where("ur.user_id = ? AND n.status = 'published'", userId).
|
||||
Count(&stats.TotalCount).Error
|
||||
if err != nil {
|
||||
return stats, err
|
||||
}
|
||||
|
||||
// 未读通知数
|
||||
err = global.GVA_DB.Table("notice_user_reads ur").
|
||||
Joins("JOIN notice_notifications n ON ur.notification_id = n.id").
|
||||
Where("ur.user_id = ? AND ur.is_read = false AND n.status = 'published'", userId).
|
||||
Count(&stats.UnreadCount).Error
|
||||
if err != nil {
|
||||
return stats, err
|
||||
}
|
||||
|
||||
stats.ReadCount = stats.TotalCount - stats.UnreadCount
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// DeleteUserNotification 删除用户通知记录
|
||||
// 如果通知只针对单个用户,则删除通知本身;否则只删除用户读取记录
|
||||
func (s *NotificationService) DeleteUserNotification(userId uint, notificationId uint) error {
|
||||
// 首先检查通知是否存在
|
||||
var notification model.Notification
|
||||
err := global.GVA_DB.First(¬ification, notificationId).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("通知不存在")
|
||||
}
|
||||
|
||||
// 检查通知的接收者数量
|
||||
var receiverCount int64
|
||||
err = global.GVA_DB.Model(&model.UserRead{}).
|
||||
Where("notification_id = ?", notificationId).
|
||||
Count(&receiverCount).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 如果只有一个接收者,删除通知本身
|
||||
if receiverCount == 1 {
|
||||
// 开启事务
|
||||
tx := global.GVA_DB.Begin()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
// 删除用户读取记录
|
||||
if err := tx.Where("notification_id = ? AND user_id = ?", notificationId, userId).Delete(&model.UserRead{}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
// 删除通知本身
|
||||
if err := tx.Delete(¬ification).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit().Error
|
||||
} else {
|
||||
// 如果有多个接收者,只删除当前用户的读取记录
|
||||
return global.GVA_DB.Where("notification_id = ? AND user_id = ?", notificationId, userId).Delete(&model.UserRead{}).Error
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"time"
|
||||
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/plugin/notice/model"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/plugin/notice/model/request"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/plugin/notice/model/response"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type OnlineUserService struct{}
|
||||
|
||||
// AddOnlineUser 添加在线用户
|
||||
func (s *OnlineUserService) AddOnlineUser(userId uint, socketId string) error {
|
||||
// 使用事务确保操作的原子性
|
||||
return global.GVA_DB.Transaction(func(tx *gorm.DB) error {
|
||||
// 先硬删除该用户的所有在线记录(包括软删除的记录)
|
||||
if err := tx.Unscoped().Where("user_id = ?", userId).Delete(&model.OnlineUser{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 创建新的在线记录
|
||||
onlineUser := model.OnlineUser{
|
||||
UserId: userId,
|
||||
SocketId: socketId,
|
||||
LastActiveTime: time.Now(),
|
||||
Status: "online",
|
||||
}
|
||||
|
||||
return tx.Create(&onlineUser).Error
|
||||
})
|
||||
}
|
||||
|
||||
// RemoveOnlineUser 移除在线用户
|
||||
func (s *OnlineUserService) RemoveOnlineUser(userId uint) error {
|
||||
// 使用硬删除确保用户立即从在线列表中移除
|
||||
err := global.GVA_DB.Unscoped().Where("user_id = ?", userId).Delete(&model.OnlineUser{}).Error
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "移除在线用户失败")
|
||||
}
|
||||
// 广播给所有在线用户
|
||||
return ServiceGroupApp.SocketService.SendToUser(userId, "forceLogout", "您已被管理员强制下线,请重新登录")
|
||||
}
|
||||
|
||||
// RemoveOnlineUserBySocketId 根据SocketId移除在线用户
|
||||
func (s *OnlineUserService) RemoveOnlineUserBySocketId(socketId string) error {
|
||||
// 使用硬删除确保用户立即从在线列表中移除
|
||||
return global.GVA_DB.Unscoped().Where("socket_id = ?", socketId).Delete(&model.OnlineUser{}).Error
|
||||
}
|
||||
|
||||
// UpdateUserActivity 更新用户活跃时间
|
||||
func (s *OnlineUserService) UpdateUserActivity(userId uint) error {
|
||||
return global.GVA_DB.Model(&model.OnlineUser{}).
|
||||
Where("user_id = ?", userId).
|
||||
Update("last_active_time", time.Now()).Error
|
||||
}
|
||||
|
||||
// UpdateUserActivityBySocketId 根据SocketId更新用户活跃时间
|
||||
func (s *OnlineUserService) UpdateUserActivityBySocketId(socketId string) error {
|
||||
return global.GVA_DB.Model(&model.OnlineUser{}).
|
||||
Where("socket_id = ?", socketId).
|
||||
Update("last_active_time", time.Now()).Error
|
||||
}
|
||||
|
||||
// GetOnlineUsers 获取在线用户列表
|
||||
func (s *OnlineUserService) GetOnlineUsers() ([]response.OnlineUserResponse, error) {
|
||||
var results []struct {
|
||||
UserId uint `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
NickName string `json:"nick_name"`
|
||||
SocketId string `json:"socket_id"`
|
||||
LastActiveTime time.Time `json:"last_active_time"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
err := global.GVA_DB.Table("notice_online_users ou").
|
||||
Select("ou.user_id, u.username, u.nick_name, ou.socket_id, ou.last_active_time, ou.status").
|
||||
Joins("JOIN sys_users u ON ou.user_id = u.id").
|
||||
Where("ou.status = 'online'").
|
||||
Order("ou.last_active_time DESC").
|
||||
Scan(&results).Error
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var onlineUsers []response.OnlineUserResponse
|
||||
for _, result := range results {
|
||||
onlineUsers = append(onlineUsers, response.OnlineUserResponse{
|
||||
UserId: result.UserId,
|
||||
Username: result.Username,
|
||||
NickName: result.NickName,
|
||||
SocketId: result.SocketId,
|
||||
LastActiveTime: result.LastActiveTime,
|
||||
Status: result.Status,
|
||||
})
|
||||
}
|
||||
|
||||
return onlineUsers, nil
|
||||
}
|
||||
|
||||
// GetOnlineUsersWithPagination 获取在线用户列表(分页)
|
||||
func (s *OnlineUserService) GetOnlineUsersWithPagination(req request.OnlineUserSearch) ([]response.OnlineUserResponse, int64, error) {
|
||||
limit := req.PageSize
|
||||
offset := req.PageSize * (req.Page - 1)
|
||||
|
||||
// 构建查询条件
|
||||
db := global.GVA_DB.Table("notice_online_users ou").
|
||||
Select("ou.user_id, u.username, u.nick_name, ou.socket_id, ou.last_active_time, ou.status").
|
||||
Joins("JOIN sys_users u ON ou.user_id = u.id").
|
||||
Where("ou.status = 'online'")
|
||||
|
||||
// 添加搜索条件
|
||||
if req.Username != "" {
|
||||
db = db.Where("u.username LIKE ?", "%"+req.Username+"%")
|
||||
}
|
||||
if req.NickName != "" {
|
||||
db = db.Where("u.nick_name LIKE ?", "%"+req.NickName+"%")
|
||||
}
|
||||
if req.RoleId != 0 {
|
||||
// 通过角色过滤用户
|
||||
db = db.Where("EXISTS (SELECT 1 FROM sys_user_authority sua WHERE sua.sys_user_id = u.id AND sua.sys_authority_authority_id = ?)", req.RoleId)
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
var total int64
|
||||
err := db.Count(&total).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 获取分页数据
|
||||
var results []struct {
|
||||
UserId uint `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
NickName string `json:"nick_name"`
|
||||
SocketId string `json:"socket_id"`
|
||||
LastActiveTime time.Time `json:"last_active_time"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
err = db.Order("ou.last_active_time DESC").
|
||||
Limit(limit).
|
||||
Offset(offset).
|
||||
Scan(&results).Error
|
||||
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
var onlineUsers []response.OnlineUserResponse
|
||||
for _, result := range results {
|
||||
// 计算在线时长
|
||||
onlineDuration := time.Since(result.LastActiveTime)
|
||||
onlineUsers = append(onlineUsers, response.OnlineUserResponse{
|
||||
UserId: result.UserId,
|
||||
Username: result.Username,
|
||||
NickName: result.NickName,
|
||||
SocketId: result.SocketId,
|
||||
LastActiveTime: result.LastActiveTime,
|
||||
Status: result.Status,
|
||||
OnlineDuration: int64(onlineDuration.Minutes()), // 在线时长(分钟)
|
||||
})
|
||||
}
|
||||
|
||||
return onlineUsers, total, nil
|
||||
}
|
||||
|
||||
// GetOnlineUserByUserId 根据用户ID获取在线用户信息
|
||||
func (s *OnlineUserService) GetOnlineUserByUserId(userId uint) (model.OnlineUser, error) {
|
||||
var onlineUser model.OnlineUser
|
||||
err := global.GVA_DB.Where("user_id = ? AND status = 'online'", userId).First(&onlineUser).Error
|
||||
return onlineUser, err
|
||||
}
|
||||
|
||||
// GetOnlineUserBySocketId 根据SocketId获取在线用户信息
|
||||
func (s *OnlineUserService) GetOnlineUserBySocketId(socketId string) (model.OnlineUser, error) {
|
||||
var onlineUser model.OnlineUser
|
||||
err := global.GVA_DB.Where("socket_id = ? AND status = 'online'", socketId).First(&onlineUser).Error
|
||||
return onlineUser, err
|
||||
}
|
||||
|
||||
// GetUserSocketId 获取用户的SocketId
|
||||
func (s *OnlineUserService) GetUserSocketId(userId uint) (string, error) {
|
||||
var onlineUser model.OnlineUser
|
||||
err := global.GVA_DB.Select("socket_id").Where("user_id = ? AND status = 'online'", userId).First(&onlineUser).Error
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return onlineUser.SocketId, nil
|
||||
}
|
||||
|
||||
// GetUsersByRoleIds 根据角色ID获取在线用户列表
|
||||
func (s *OnlineUserService) GetUsersByRoleIds(roleIds []uint) ([]uint, error) {
|
||||
var userIds []uint
|
||||
|
||||
err := global.GVA_DB.Table("notice_online_users ou").
|
||||
Select("DISTINCT ou.user_id").
|
||||
Joins("JOIN sys_user_authority sua ON ou.user_id = sua.sys_user_id").
|
||||
Where("sua.sys_authority_id IN ? AND ou.status = 'online'", roleIds).
|
||||
Pluck("user_id", &userIds).Error
|
||||
|
||||
return userIds, err
|
||||
}
|
||||
|
||||
// IsUserOnline 检查用户是否在线
|
||||
func (s *OnlineUserService) IsUserOnline(userId uint) bool {
|
||||
var count int64
|
||||
global.GVA_DB.Model(&model.OnlineUser{}).Where("user_id = ? AND status = 'online'", userId).Count(&count)
|
||||
return count > 0
|
||||
}
|
||||
|
||||
// GetOnlineUserCount 获取在线用户数量
|
||||
func (s *OnlineUserService) GetOnlineUserCount() (int64, error) {
|
||||
var count int64
|
||||
err := global.GVA_DB.Model(&model.OnlineUser{}).Where("status = 'online'").Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// CleanOfflineUsers 清理离线用户(超过指定时间未活跃的用户)
|
||||
func (s *OnlineUserService) CleanOfflineUsers(timeout time.Duration) error {
|
||||
cutoffTime := time.Now().Add(-timeout)
|
||||
// 使用硬删除确保用户立即从在线列表中移除
|
||||
return global.GVA_DB.Unscoped().Where("last_active_time < ?", cutoffTime).Delete(&model.OnlineUser{}).Error
|
||||
}
|
||||
|
||||
// GetOnlineUserStats 获取在线用户统计数据
|
||||
func (s *OnlineUserService) GetOnlineUserStats() (response.OnlineUserStatsResponse, error) {
|
||||
var stats response.OnlineUserStatsResponse
|
||||
|
||||
// 获取当前在线总数
|
||||
err := global.GVA_DB.Model(&model.OnlineUser{}).Where("status = 'online'").Count(&stats.TotalOnline).Error
|
||||
if err != nil {
|
||||
return stats, err
|
||||
}
|
||||
|
||||
// 获取今日登录人数(基于最后活跃时间在今天的用户)
|
||||
today := time.Now().Format("2006-01-02")
|
||||
todayStart := today + " 00:00:00"
|
||||
todayEnd := today + " 23:59:59"
|
||||
|
||||
err = global.GVA_DB.Model(&model.OnlineUser{}).
|
||||
Where("last_active_time >= ? AND last_active_time <= ?", todayStart, todayEnd).
|
||||
Count(&stats.TodayLogin).Error
|
||||
if err != nil {
|
||||
return stats, err
|
||||
}
|
||||
|
||||
// 峰值在线人数(这里简化处理,可以根据实际需求存储历史峰值数据)
|
||||
// 暂时使用当前在线数作为峰值
|
||||
stats.PeakOnline = stats.TotalOnline
|
||||
|
||||
// 平均在线人数(这里简化处理,实际应该基于历史数据计算)
|
||||
// 暂时使用当前在线数的80%作为平均值
|
||||
stats.AverageOnline = int64(float64(stats.TotalOnline) * 0.8)
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sync"
|
||||
|
||||
"github.com/doquangtan/socketio/v4"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/plugin/notice/model"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/plugin/notice/model/response"
|
||||
)
|
||||
|
||||
type SocketService struct {
|
||||
server *socketio.Socket
|
||||
connections map[string]*socketio.Socket // socketId -> socket
|
||||
userSockets map[uint]string // userId -> socketId
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
// SetServer 设置Socket.IO服务器实例
|
||||
func (s *SocketService) SetServer(server *socketio.Socket) {
|
||||
s.server = server
|
||||
}
|
||||
|
||||
// AddConnection 添加连接
|
||||
func (s *SocketService) AddConnection(userId uint, socketId string, socket *socketio.Socket) {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
if s.connections == nil {
|
||||
s.connections = make(map[string]*socketio.Socket)
|
||||
}
|
||||
if s.userSockets == nil {
|
||||
s.userSockets = make(map[uint]string)
|
||||
}
|
||||
|
||||
// 如果用户已有连接,先断开旧连接
|
||||
if oldSocketId, exists := s.userSockets[userId]; exists {
|
||||
if oldSocket, ok := s.connections[oldSocketId]; ok {
|
||||
oldSocket.Disconnect()
|
||||
delete(s.connections, oldSocketId)
|
||||
}
|
||||
}
|
||||
|
||||
s.connections[socketId] = socket
|
||||
s.userSockets[userId] = socketId
|
||||
|
||||
log.Printf("用户 %d 连接成功,SocketId: %s", userId, socketId)
|
||||
}
|
||||
|
||||
// RemoveConnection 移除连接
|
||||
func (s *SocketService) RemoveConnection(socketId string) {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
delete(s.connections, socketId)
|
||||
|
||||
// 从用户映射中移除
|
||||
for userId, sid := range s.userSockets {
|
||||
if sid == socketId {
|
||||
delete(s.userSockets, userId)
|
||||
log.Printf("用户 %d 断开连接,SocketId: %s", userId, socketId)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveConnectionByUserId 根据用户ID移除连接
|
||||
func (s *SocketService) RemoveConnectionByUserId(userId uint) {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
if socketId, exists := s.userSockets[userId]; exists {
|
||||
if socket, ok := s.connections[socketId]; ok {
|
||||
socket.Disconnect()
|
||||
delete(s.connections, socketId)
|
||||
}
|
||||
delete(s.userSockets, userId)
|
||||
log.Printf("用户 %d 连接被移除", userId)
|
||||
}
|
||||
}
|
||||
|
||||
// GetConnection 获取连接
|
||||
func (s *SocketService) GetConnection(socketId string) (*socketio.Socket, bool) {
|
||||
s.mutex.RLock()
|
||||
defer s.mutex.RUnlock()
|
||||
|
||||
socket, exists := s.connections[socketId]
|
||||
return socket, exists
|
||||
}
|
||||
|
||||
// GetUserConnection 根据用户ID获取连接
|
||||
func (s *SocketService) GetUserConnection(userId uint) (*socketio.Socket, bool) {
|
||||
s.mutex.RLock()
|
||||
defer s.mutex.RUnlock()
|
||||
|
||||
if socketId, exists := s.userSockets[userId]; exists {
|
||||
if socket, ok := s.connections[socketId]; ok {
|
||||
return socket, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// GetUserSocketId 获取用户的SocketId
|
||||
func (s *SocketService) GetUserSocketId(userId uint) (string, bool) {
|
||||
s.mutex.RLock()
|
||||
defer s.mutex.RUnlock()
|
||||
|
||||
socketId, exists := s.userSockets[userId]
|
||||
return socketId, exists
|
||||
}
|
||||
|
||||
// IsUserOnline 检查用户是否在线
|
||||
func (s *SocketService) IsUserOnline(userId uint) bool {
|
||||
s.mutex.RLock()
|
||||
defer s.mutex.RUnlock()
|
||||
|
||||
_, exists := s.userSockets[userId]
|
||||
return exists
|
||||
}
|
||||
|
||||
// GetOnlineUsers 获取在线用户列表
|
||||
func (s *SocketService) GetOnlineUsers() []uint {
|
||||
s.mutex.RLock()
|
||||
defer s.mutex.RUnlock()
|
||||
|
||||
users := make([]uint, 0, len(s.userSockets))
|
||||
for userId := range s.userSockets {
|
||||
users = append(users, userId)
|
||||
}
|
||||
|
||||
return users
|
||||
}
|
||||
|
||||
// GetOnlineUserCount 获取在线用户数量
|
||||
func (s *SocketService) GetOnlineUserCount() int {
|
||||
s.mutex.RLock()
|
||||
defer s.mutex.RUnlock()
|
||||
|
||||
return len(s.userSockets)
|
||||
}
|
||||
|
||||
// SendToUser 向指定用户发送消息
|
||||
func (s *SocketService) SendToUser(userId uint, event string, data interface{}) error {
|
||||
log.Printf("尝试向用户 %d 发送消息,事件: %s", userId, event)
|
||||
socket, exists := s.GetUserConnection(userId)
|
||||
if !exists {
|
||||
log.Printf("用户 %d 不在线,无法发送消息", userId)
|
||||
return nil // 用户不在线,不发送
|
||||
}
|
||||
|
||||
log.Printf("用户 %d 在线,发送消息,事件: %s", userId, event)
|
||||
socket.Emit(event, data)
|
||||
log.Printf("消息已发送给用户 %d,事件: %s", userId, event)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendToUsers 向多个用户发送消息
|
||||
func (s *SocketService) SendToUsers(userIds []uint, event string, data interface{}) {
|
||||
for _, userId := range userIds {
|
||||
if err := s.SendToUser(userId, event, data); err != nil {
|
||||
log.Printf("向用户 %d 发送消息失败: %v", userId, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BroadcastToAll 向所有在线用户广播消息
|
||||
func (s *SocketService) BroadcastToAll(event string, data interface{}) {
|
||||
s.mutex.RLock()
|
||||
defer s.mutex.RUnlock()
|
||||
|
||||
log.Printf("开始广播消息,事件: %s,在线用户数: %d", event, len(s.userSockets))
|
||||
log.Printf("当前在线用户列表: %v", s.userSockets)
|
||||
|
||||
for userId := range s.userSockets {
|
||||
log.Printf("向用户 %d 发送消息,事件: %s", userId, event)
|
||||
if err := s.SendToUser(userId, event, data); err != nil {
|
||||
log.Printf("向用户 %d 广播消息失败: %v", userId, err)
|
||||
} else {
|
||||
log.Printf("向用户 %d 广播消息成功", userId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SendNotificationToUser 向用户发送通知
|
||||
func (s *SocketService) SendNotificationToUser(userId uint, notification *model.Notification) error {
|
||||
notificationData := response.NotificationResponse{
|
||||
ID: notification.ID,
|
||||
Title: notification.Title,
|
||||
Content: notification.Content,
|
||||
Type: notification.Type,
|
||||
Priority: notification.Priority,
|
||||
SenderId: notification.SenderId,
|
||||
TargetType: notification.TargetType,
|
||||
Status: notification.Status,
|
||||
PublishTime: notification.PublishTime,
|
||||
ExpireTime: notification.ExpireTime,
|
||||
CreatedAt: notification.CreatedAt,
|
||||
UpdatedAt: notification.UpdatedAt,
|
||||
}
|
||||
|
||||
return s.SendToUser(userId, "new_notification", notificationData)
|
||||
}
|
||||
|
||||
// SendNotificationToUsers 向多个用户发送通知
|
||||
func (s *SocketService) SendNotificationToUsers(userIds []uint, notification *model.Notification) {
|
||||
notificationData := response.NotificationResponse{
|
||||
ID: notification.ID,
|
||||
Title: notification.Title,
|
||||
Content: notification.Content,
|
||||
Type: notification.Type,
|
||||
Priority: notification.Priority,
|
||||
SenderId: notification.SenderId,
|
||||
TargetType: notification.TargetType,
|
||||
Status: notification.Status,
|
||||
PublishTime: notification.PublishTime,
|
||||
ExpireTime: notification.ExpireTime,
|
||||
CreatedAt: notification.CreatedAt,
|
||||
UpdatedAt: notification.UpdatedAt,
|
||||
}
|
||||
|
||||
for _, userId := range userIds {
|
||||
if err := s.SendToUser(userId, "new_notification", notificationData); err != nil {
|
||||
log.Printf("向用户 %d 发送通知失败: %v", userId, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BroadcastNotification 广播通知给所有在线用户
|
||||
func (s *SocketService) BroadcastNotification(notification *model.Notification) {
|
||||
notificationData := response.NotificationResponse{
|
||||
ID: notification.ID,
|
||||
Title: notification.Title,
|
||||
Content: notification.Content,
|
||||
Type: notification.Type,
|
||||
Priority: notification.Priority,
|
||||
SenderId: notification.SenderId,
|
||||
TargetType: notification.TargetType,
|
||||
Status: notification.Status,
|
||||
PublishTime: notification.PublishTime,
|
||||
ExpireTime: notification.ExpireTime,
|
||||
CreatedAt: notification.CreatedAt,
|
||||
UpdatedAt: notification.UpdatedAt,
|
||||
}
|
||||
|
||||
s.BroadcastToAll("new_notification", notificationData)
|
||||
}
|
||||
|
||||
// SendReadStatusUpdate 发送已读状态更新
|
||||
func (s *SocketService) SendReadStatusUpdate(userId uint, notificationId uint) error {
|
||||
data := map[string]interface{}{
|
||||
"notification_id": notificationId,
|
||||
"read_status": true,
|
||||
}
|
||||
|
||||
return s.SendToUser(userId, "read_status_update", data)
|
||||
}
|
||||
|
||||
// GetConnectionInfo 获取连接信息
|
||||
func (s *SocketService) GetConnectionInfo() map[string]interface{} {
|
||||
s.mutex.RLock()
|
||||
defer s.mutex.RUnlock()
|
||||
|
||||
return map[string]interface{}{
|
||||
"total_connections": len(s.connections),
|
||||
"online_users": len(s.userSockets),
|
||||
"user_sockets": s.userSockets,
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@ package system
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/global"
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
|
||||
@@ -105,8 +107,12 @@ func (dictionaryService *DictionaryService) GetSysDictionary(Type string, Id uin
|
||||
//@param: info request.SysDictionarySearch
|
||||
//@return: err error, list interface{}, total int64
|
||||
|
||||
func (dictionaryService *DictionaryService) GetSysDictionaryInfoList() (list interface{}, err error) {
|
||||
func (dictionaryService *DictionaryService) GetSysDictionaryInfoList(c *gin.Context, req request.SysDictionarySearch) (list interface{}, err error) {
|
||||
var sysDictionarys []system.SysDictionary
|
||||
err = global.GVA_DB.Find(&sysDictionarys).Error
|
||||
query := global.GVA_DB.WithContext(c)
|
||||
if req.Name != "" {
|
||||
query = query.Where("name LIKE ? OR type LIKE ?", "%"+req.Name+"%", "%"+req.Name+"%")
|
||||
}
|
||||
err = query.Find(&sysDictionarys).Error
|
||||
return sysDictionarys, err
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ VITE_SERVER_PORT = 8888
|
||||
VITE_BASE_API = /api
|
||||
VITE_FILE_API = /api
|
||||
VITE_BASE_PATH = http://127.0.0.1
|
||||
VITE_POSITION = close
|
||||
VITE_POSITION = open
|
||||
VITE_EDITOR = code
|
||||
// VITE_EDITOR = webstorm 如果使用webstorm开发且要使用dom定位到代码行功能 请先自定添加 webstorm到环境变量 再将VITE_EDITOR值修改为webstorm
|
||||
// 如果使用docker-compose开发模式,设置为下面的地址或本机主机IP
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
"pinia": "^2.2.2",
|
||||
"qs": "^6.13.0",
|
||||
"screenfull": "^6.0.2",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"sortablejs": "^1.15.3",
|
||||
"spark-md5": "^3.0.2",
|
||||
"universal-cookie": "^7",
|
||||
|
||||
+29
-26
@@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<div id="app" class="bg-gray-50 text-slate-700 dark:text-slate-500 dark:bg-slate-800">
|
||||
<div
|
||||
id="app"
|
||||
class="bg-gray-50 text-slate-700 !dark:text-slate-500 dark:bg-slate-800"
|
||||
>
|
||||
<el-config-provider :locale="zhCn">
|
||||
<router-view />
|
||||
<Application />
|
||||
@@ -8,36 +11,36 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||
import Application from '@/components/application/index.vue'
|
||||
import { useAppStore } from '@/pinia'
|
||||
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||
import Application from '@/components/application/index.vue'
|
||||
import { useAppStore } from '@/pinia'
|
||||
|
||||
useAppStore()
|
||||
defineOptions({
|
||||
name: 'App'
|
||||
})
|
||||
useAppStore()
|
||||
defineOptions({
|
||||
name: 'App'
|
||||
})
|
||||
</script>
|
||||
<style lang="scss">
|
||||
// 引入初始化样式
|
||||
#app {
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
// 引入初始化样式
|
||||
#app {
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
|
||||
.el-button {
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
.el-button {
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
|
||||
.gva-body-h {
|
||||
min-height: calc(100% - 3rem);
|
||||
}
|
||||
.gva-body-h {
|
||||
min-height: calc(100% - 3rem);
|
||||
}
|
||||
|
||||
.gva-container {
|
||||
height: calc(100% - 2.5rem);
|
||||
}
|
||||
.gva-container {
|
||||
height: calc(100% - 2.5rem);
|
||||
}
|
||||
|
||||
.gva-container2 {
|
||||
height: calc(100% - 4.5rem);
|
||||
}
|
||||
.gva-container2 {
|
||||
height: calc(100% - 4.5rem);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
import service from '@/utils/request'
|
||||
|
||||
// @Summary 创建通知
|
||||
// @Produce application/json
|
||||
// @Param data body {title:"string",content:"string",type:"string",targetType:"string",targetIds:"array"}
|
||||
// @Router /notice/notification/createNotification [post]
|
||||
export const createNotification = (data) => {
|
||||
return service({
|
||||
url: '/notice/notification/createNotification',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary 获取通知列表
|
||||
// @Produce application/json
|
||||
// @Param data body {page:"int",pageSize:"int",title:"string",type:"string",status:"string"}
|
||||
// @Router /notice/notification/getNotificationList [post]
|
||||
export const getNotificationList = (data) => {
|
||||
return service({
|
||||
url: '/notice/notification/getNotificationList',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary 获取通知详情
|
||||
// @Produce application/json
|
||||
// @Param id path int true "通知ID"
|
||||
// @Router /notice/notification/getNotificationById/{id} [get]
|
||||
export const getNotificationById = (id) => {
|
||||
return service({
|
||||
url: `/notice/notification/getNotificationById/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary 更新通知
|
||||
// @Produce application/json
|
||||
// @Param data body {id:"int",title:"string",content:"string",type:"string",targetType:"string",targetIds:"array"}
|
||||
// @Router /notice/notification/updateNotification [put]
|
||||
export const updateNotification = (data) => {
|
||||
return service({
|
||||
url: '/notice/notification/updateNotification',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary 删除通知
|
||||
// @Produce application/json
|
||||
// @Param id path int true "通知ID"
|
||||
// @Router /notice/notification/deleteNotification/{id} [delete]
|
||||
export const deleteNotification = (id) => {
|
||||
return service({
|
||||
url: `/notice/notification/deleteNotification/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary 发送通知
|
||||
// @Produce application/json
|
||||
// @Param data body {id:"int"}
|
||||
// @Router /notice/notification/sendNotification [post]
|
||||
export const sendNotification = (data) => {
|
||||
return service({
|
||||
url: '/notice/notification/sendNotification',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary 发布通知
|
||||
// @Produce application/json
|
||||
// @Param id path int true "通知ID"
|
||||
// @Router /notice/notification/publishNotification/{id} [post]
|
||||
export const publishNotification = (id) => {
|
||||
return service({
|
||||
url: `/notice/notification/publishNotification/${id}`,
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary 获取用户通知列表
|
||||
// @Produce application/json
|
||||
// @Param data body {page:"int",pageSize:"int",isRead:"bool"}
|
||||
// @Router /notice/notification/getUserNotifications [post]
|
||||
export const getUserNotificationList = (data) => {
|
||||
return service({
|
||||
url: '/notice/notification/getUserNotifications',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary 标记通知为已读
|
||||
// @Produce application/json
|
||||
// @Param data body {notificationId:"int"}
|
||||
// @Router /notice/notification/markAsRead [post]
|
||||
export const markNotificationAsRead = (data) => {
|
||||
return service({
|
||||
url: '/notice/notification/markAsRead',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary 获取未读通知统计
|
||||
// @Produce application/json
|
||||
// @Router /notice/notification/getNotificationStats [get]
|
||||
export const getUnreadNotificationCount = () => {
|
||||
return service({
|
||||
url: '/notice/notification/getNotificationStats',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary 获取在线用户列表
|
||||
// @Produce application/json
|
||||
// @Param data body {page:"int",pageSize:"int",username:"string",nickName:"string",roleId:"int"}
|
||||
// @Router /notice/onlineUser/getOnlineUsers [post]
|
||||
export const getOnlineUserList = (data) => {
|
||||
return service({
|
||||
url: '/notice/onlineUser/getOnlineUsers',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary 删除用户通知记录
|
||||
// @Produce application/json
|
||||
// @Param notificationId path int true "通知ID"
|
||||
// @Router /notice/notification/deleteUserNotification/{notificationId} [delete]
|
||||
export const deleteUserNotification = (notificationId) => {
|
||||
return service({
|
||||
url: `/notice/notification/deleteUserNotification/${notificationId}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary 强制用户下线
|
||||
// @Produce application/json
|
||||
// @Param userId path int true "用户ID"
|
||||
// @Router /notice/onlineUser/removeOnlineUser/{userId} [delete]
|
||||
export const kickUser = (userId) => {
|
||||
return service({
|
||||
url: `/notice/onlineUser/removeOnlineUser/${userId}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary 获取在线用户统计
|
||||
// @Produce application/json
|
||||
// @Router /notice/onlineUser/getOnlineUserStats [get]
|
||||
export const getOnlineUserStats = () => {
|
||||
return service({
|
||||
url: '/notice/onlineUser/getOnlineUserStats',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
export default {
|
||||
install: (app) => {
|
||||
app.directive('click-outside', {
|
||||
mounted(el, binding) {
|
||||
const handler = (e) => {
|
||||
// 如果绑定的元素包含事件目标,或元素已经被移除,则不触发
|
||||
if (!el || el.contains(e.target) || e.target === el) return
|
||||
// 支持函数或对象 { handler: fn, exclude: [el1, el2], capture: true }
|
||||
const value = binding.value
|
||||
if (value && typeof value === 'object') {
|
||||
if (
|
||||
value.exclude &&
|
||||
value.exclude.some(
|
||||
(ex) => ex && ex.contains && ex.contains(e.target)
|
||||
)
|
||||
)
|
||||
return
|
||||
if (typeof value.handler === 'function') value.handler(e)
|
||||
} else if (typeof value === 'function') {
|
||||
value(e)
|
||||
}
|
||||
}
|
||||
|
||||
// 存到 el 上,便于解绑
|
||||
el.__clickOutsideHandler__ = handler
|
||||
|
||||
// 延迟注册,避免 mounted 时触发(比如当点击就是触发绑定动作时)
|
||||
setTimeout(() => {
|
||||
document.addEventListener('mousedown', handler)
|
||||
document.addEventListener('touchstart', handler)
|
||||
}, 0)
|
||||
},
|
||||
unmounted(el) {
|
||||
const h = el.__clickOutsideHandler__
|
||||
if (h) {
|
||||
document.removeEventListener('mousedown', h)
|
||||
document.removeEventListener('touchstart', h)
|
||||
delete el.__clickOutsideHandler__
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+10
-2
@@ -1,6 +1,6 @@
|
||||
import './style/element_visiable.scss'
|
||||
import 'element-plus/theme-chalk/dark/css-vars.css'
|
||||
import 'uno.css';
|
||||
import 'uno.css'
|
||||
import { createApp } from 'vue'
|
||||
import ElementPlus from 'element-plus'
|
||||
|
||||
@@ -12,11 +12,19 @@ import router from '@/router/index'
|
||||
import '@/permission'
|
||||
import run from '@/core/gin-vue-admin.js'
|
||||
import auth from '@/directive/auth'
|
||||
import clickOutSide from '@/directive/clickOutSide'
|
||||
import { store } from '@/pinia'
|
||||
import App from './App.vue'
|
||||
|
||||
const app = createApp(App)
|
||||
app.config.productionTip = false
|
||||
|
||||
app.use(run).use(ElementPlus).use(store).use(auth).use(router).mount('#app')
|
||||
app
|
||||
.use(run)
|
||||
.use(ElementPlus)
|
||||
.use(store)
|
||||
.use(auth)
|
||||
.use(clickOutSide)
|
||||
.use(router)
|
||||
.mount('#app')
|
||||
export default app
|
||||
|
||||
@@ -43,6 +43,10 @@
|
||||
"/src/view/layout/setting/modules/layout/index.vue": "LayoutSettings",
|
||||
"/src/view/layout/tabs/index.vue": "HistoryComponent",
|
||||
"/src/view/login/index.vue": "Login",
|
||||
"/src/view/notice/index.vue": "Index",
|
||||
"/src/view/notice/notification/index.vue": "NotificationManagement",
|
||||
"/src/view/notice/onlineUser/index.vue": "OnlineUserManagement",
|
||||
"/src/view/notice/userCenter/index.vue": "UserNotificationCenter",
|
||||
"/src/view/person/person.vue": "Person",
|
||||
"/src/view/routerHolder.vue": "RouterHolder",
|
||||
"/src/view/superAdmin/api/api.vue": "Api",
|
||||
@@ -75,6 +79,7 @@
|
||||
"/src/view/systemTools/pubPlug/pubPlug.vue": "PubPlug",
|
||||
"/src/view/systemTools/system/system.vue": "Config",
|
||||
"/src/view/systemTools/version/version.vue": "SysVersion",
|
||||
"/src/view/userCenter/index.vue": "UserNotificationCenter",
|
||||
"/src/plugin/announcement/form/info.vue": "InfoForm",
|
||||
"/src/plugin/announcement/view/info.vue": "Info",
|
||||
"/src/plugin/email/view/index.vue": "Email"
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
import { io } from 'socket.io-client'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useUserStore } from '@/pinia/modules/user'
|
||||
import { ElNotification } from 'element-plus'
|
||||
import { useRouter } from 'vue-router'
|
||||
const router = useRouter()
|
||||
|
||||
class SocketManager {
|
||||
constructor() {
|
||||
this.socket = null
|
||||
this.connected = false
|
||||
this.reconnectAttempts = 0
|
||||
this.maxReconnectAttempts = 5
|
||||
this.reconnectInterval = 3000
|
||||
this.heartbeatInterval = null
|
||||
this.callbacks = new Map()
|
||||
}
|
||||
|
||||
// 连接Socket.IO服务器
|
||||
connect() {
|
||||
if (this.socket && this.connected) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
const userStore = useUserStore()
|
||||
const token = userStore.token
|
||||
|
||||
if (!token) {
|
||||
console.warn('No token found, cannot connect to socket')
|
||||
return Promise.reject(new Error('No token'))
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.socket = io('http://127.0.0.1:8888/', {
|
||||
auth: {
|
||||
token: token
|
||||
},
|
||||
transports: ['websocket', 'polling'],
|
||||
timeout: 10000,
|
||||
forceNew: true
|
||||
})
|
||||
|
||||
// 连接成功
|
||||
this.socket.on('connect', () => {
|
||||
console.log('Socket connected:', this.socket.id)
|
||||
this.connected = true
|
||||
this.reconnectAttempts = 0
|
||||
this.startHeartbeat()
|
||||
resolve()
|
||||
})
|
||||
|
||||
// 连接失败
|
||||
this.socket.on('connect_error', (error) => {
|
||||
console.error('Socket connection error:', error)
|
||||
this.connected = false
|
||||
reject(error)
|
||||
})
|
||||
|
||||
// 断开连接
|
||||
this.socket.on('disconnect', (reason) => {
|
||||
console.log('Socket disconnected:', reason)
|
||||
this.connected = false
|
||||
this.stopHeartbeat()
|
||||
|
||||
// 如果是服务器主动断开,尝试重连
|
||||
if (reason === 'io server disconnect') {
|
||||
this.reconnect()
|
||||
}
|
||||
})
|
||||
|
||||
// 认证失败
|
||||
this.socket.on('auth_error', (error) => {
|
||||
console.error('Socket auth error:', error)
|
||||
ElMessage.error('Socket认证失败,请重新登录')
|
||||
this.disconnect()
|
||||
})
|
||||
|
||||
// 新通知
|
||||
this.socket.on('new_notification', (data) => {
|
||||
console.log('Received new notification:', data)
|
||||
this.emit('new_notification', data)
|
||||
})
|
||||
|
||||
// 通知统计更新
|
||||
this.socket.on('notification_stats', (data) => {
|
||||
console.log('Notification stats updated:', data)
|
||||
this.emit('notification_stats', data)
|
||||
})
|
||||
|
||||
// 用户被踢下线
|
||||
this.socket.on('forceLogout', (data) => {
|
||||
console.log('forceLogout:', data)
|
||||
this.emit('forceLogout', data)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 断开连接
|
||||
disconnect() {
|
||||
if (this.socket) {
|
||||
this.stopHeartbeat()
|
||||
this.socket.disconnect()
|
||||
this.socket = null
|
||||
this.connected = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重连
|
||||
reconnect() {
|
||||
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
||||
console.error('Max reconnect attempts reached')
|
||||
return
|
||||
}
|
||||
|
||||
this.reconnectAttempts++
|
||||
console.log(
|
||||
`Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})`
|
||||
)
|
||||
|
||||
setTimeout(() => {
|
||||
this.connect().catch(() => {
|
||||
this.reconnect()
|
||||
})
|
||||
}, this.reconnectInterval)
|
||||
}
|
||||
|
||||
// 开始心跳
|
||||
startHeartbeat() {
|
||||
this.heartbeatInterval = setInterval(() => {
|
||||
if (this.socket && this.connected) {
|
||||
this.socket.emit('heartbeat', { timestamp: Date.now() })
|
||||
}
|
||||
}, 30000) // 30秒心跳
|
||||
}
|
||||
|
||||
// 停止心跳
|
||||
stopHeartbeat() {
|
||||
if (this.heartbeatInterval) {
|
||||
clearInterval(this.heartbeatInterval)
|
||||
this.heartbeatInterval = null
|
||||
}
|
||||
}
|
||||
|
||||
// 加入房间
|
||||
joinRoom(roomId) {
|
||||
if (this.socket && this.connected) {
|
||||
this.socket.emit('join_room', { roomId })
|
||||
}
|
||||
}
|
||||
|
||||
// 离开房间
|
||||
leaveRoom(roomId) {
|
||||
if (this.socket && this.connected) {
|
||||
this.socket.emit('leave_room', { roomId })
|
||||
}
|
||||
}
|
||||
|
||||
// 标记通知为已读
|
||||
markNotificationAsRead(notificationId) {
|
||||
if (this.socket && this.connected) {
|
||||
this.socket.emit('notification_read', { notificationId })
|
||||
}
|
||||
}
|
||||
|
||||
// 获取通知统计
|
||||
getNotificationStats() {
|
||||
if (this.socket && this.connected) {
|
||||
this.socket.emit('get_notification_stats')
|
||||
}
|
||||
}
|
||||
|
||||
// 监听事件
|
||||
on(event, callback) {
|
||||
if (!this.callbacks.has(event)) {
|
||||
this.callbacks.set(event, [])
|
||||
}
|
||||
this.callbacks.get(event).push(callback)
|
||||
}
|
||||
|
||||
// 移除事件监听
|
||||
off(event, callback) {
|
||||
if (this.callbacks.has(event)) {
|
||||
const callbacks = this.callbacks.get(event)
|
||||
const index = callbacks.indexOf(callback)
|
||||
if (index > -1) {
|
||||
callbacks.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 触发事件
|
||||
emit(event, data) {
|
||||
if (this.callbacks.has(event)) {
|
||||
this.callbacks.get(event).forEach((callback) => {
|
||||
try {
|
||||
callback(data)
|
||||
} catch (error) {
|
||||
console.error('Error in socket callback:', error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 获取连接状态
|
||||
isConnected() {
|
||||
return this.connected
|
||||
}
|
||||
|
||||
// 获取Socket ID
|
||||
getSocketId() {
|
||||
return this.socket ? this.socket.id : null
|
||||
}
|
||||
}
|
||||
|
||||
// 创建全局Socket管理器实例
|
||||
export const socketManager = new SocketManager()
|
||||
|
||||
export const initSocketConnection = (userStore) => {
|
||||
if (userStore.token && userStore.userInfo.ID) {
|
||||
// 监听新通知事件
|
||||
socketManager.on('new_notification', (notification) => {
|
||||
ElNotification({
|
||||
title: '新通知',
|
||||
message: notification.title,
|
||||
type: 'info',
|
||||
duration: 5000,
|
||||
onClick: () => {
|
||||
// 点击通知跳转到通知详情或通知列表
|
||||
router.push({ name: 'NotificationManagement' })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 监听通知统计更新
|
||||
socketManager.on('notificationStatsUpdate', (stats) => {
|
||||
// 可以在这里更新全局的未读通知数量
|
||||
console.log('通知统计更新:', stats)
|
||||
})
|
||||
|
||||
// 监听被踢下线事件
|
||||
socketManager.on('forceLogout', (data) => {
|
||||
console.log('收到被踢下线通知:', data)
|
||||
ElNotification({
|
||||
title: '系统提示',
|
||||
message: data.message || '您已被管理员强制下线',
|
||||
type: 'warning',
|
||||
duration: 0
|
||||
})
|
||||
|
||||
// 清除用户信息并跳转到登录页
|
||||
setTimeout(() => {
|
||||
userStore.LoginOut()
|
||||
router.push({ name: 'Login' })
|
||||
}, 2000)
|
||||
})
|
||||
|
||||
// 连接到服务器
|
||||
socketManager.connect()
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,46 @@
|
||||
<template>
|
||||
<div v-loading.fullscreen.lock="fullscreenLoading">
|
||||
<div class="flex gap-4 p-2">
|
||||
<div class="flex-none w-64 bg-white text-slate-700 dark:text-slate-400 dark:bg-slate-900 rounded p-4">
|
||||
<div class="flex gap-4 pt-2">
|
||||
<div
|
||||
class="flex-none w-64 bg-white text-slate-700 dark:text-slate-400 dark:bg-slate-900 rounded p-4"
|
||||
>
|
||||
<el-scrollbar style="height: calc(100vh - 300px)">
|
||||
<el-tree
|
||||
:data="categories"
|
||||
node-key="id"
|
||||
:props="defaultProps"
|
||||
@node-click="handleNodeClick"
|
||||
default-expand-all
|
||||
:data="categories"
|
||||
node-key="id"
|
||||
:props="defaultProps"
|
||||
@node-click="handleNodeClick"
|
||||
default-expand-all
|
||||
>
|
||||
<template #default="{ node, data }">
|
||||
<div class="w-36" :class="search.classId === data.ID ? 'text-blue-500 font-bold' : ''">{{ data.name }}
|
||||
<div
|
||||
class="w-36"
|
||||
:class="
|
||||
search.classId === data.ID ? 'text-blue-500 font-bold' : ''
|
||||
"
|
||||
>
|
||||
{{ data.name }}
|
||||
</div>
|
||||
<el-dropdown>
|
||||
<el-icon class="ml-3 text-right" v-if="data.ID > 0"><MoreFilled /></el-icon>
|
||||
<el-icon class="ml-3 text-right" v-if="data.ID > 0"
|
||||
><MoreFilled
|
||||
/></el-icon>
|
||||
<el-icon class="ml-3 text-right mt-1" v-else><Plus /></el-icon>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="addCategoryFun(data)">添加分类</el-dropdown-item>
|
||||
<el-dropdown-item @click="editCategory(data)" v-if="data.ID > 0">编辑分类</el-dropdown-item>
|
||||
<el-dropdown-item @click="deleteCategoryFun(data.ID)" v-if="data.ID > 0">删除分类</el-dropdown-item>
|
||||
<el-dropdown-item @click="addCategoryFun(data)"
|
||||
>添加分类</el-dropdown-item
|
||||
>
|
||||
<el-dropdown-item
|
||||
@click="editCategory(data)"
|
||||
v-if="data.ID > 0"
|
||||
>编辑分类</el-dropdown-item
|
||||
>
|
||||
<el-dropdown-item
|
||||
@click="deleteCategoryFun(data.ID)"
|
||||
v-if="data.ID > 0"
|
||||
>删除分类</el-dropdown-item
|
||||
>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
@@ -28,98 +48,118 @@
|
||||
</el-tree>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
<div class="flex-1 bg-white text-slate-700 dark:text-slate-400 dark:bg-slate-900">
|
||||
<div
|
||||
class="flex-1 bg-white text-slate-700 dark:text-slate-400 dark:bg-slate-900"
|
||||
>
|
||||
<div class="gva-table-box mt-0 mb-0">
|
||||
<warning-bar title="点击“文件名”可以编辑;选择的类别即是上传的类别。" />
|
||||
<warning-bar
|
||||
title="点击“文件名”可以编辑;选择的类别即是上传的类别。"
|
||||
/>
|
||||
<div class="gva-btn-list gap-3">
|
||||
<upload-common :image-common="imageCommon" :classId="search.classId" @on-success="onSuccess" />
|
||||
<upload-common
|
||||
:image-common="imageCommon"
|
||||
:classId="search.classId"
|
||||
@on-success="onSuccess"
|
||||
/>
|
||||
<cropper-image :classId="search.classId" @on-success="onSuccess" />
|
||||
<QRCodeUpload :classId="search.classId" @on-success="onSuccess" />
|
||||
<upload-image
|
||||
:image-url="imageUrl"
|
||||
:file-size="512"
|
||||
:max-w-h="1080"
|
||||
:classId="search.classId"
|
||||
@on-success="onSuccess"
|
||||
:image-url="imageUrl"
|
||||
:file-size="512"
|
||||
:max-w-h="1080"
|
||||
:classId="search.classId"
|
||||
@on-success="onSuccess"
|
||||
/>
|
||||
<el-button type="primary" icon="upload" @click="importUrlFunc">
|
||||
导入URL
|
||||
</el-button>
|
||||
<el-input
|
||||
v-model="search.keyword"
|
||||
class="w-72"
|
||||
placeholder="请输入文件名或备注"
|
||||
v-model="search.keyword"
|
||||
class="w-72"
|
||||
placeholder="请输入文件名或备注"
|
||||
/>
|
||||
<el-button type="primary" icon="search" @click="onSubmit"
|
||||
>查询
|
||||
</el-button
|
||||
>
|
||||
>查询
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="tableData">
|
||||
<el-table-column align="left" label="预览" width="100">
|
||||
<template #default="scope">
|
||||
<CustomPic pic-type="file" :pic-src="scope.row.url" preview/>
|
||||
<CustomPic pic-type="file" :pic-src="scope.row.url" preview />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="日期" prop="UpdatedAt" width="180">
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="日期"
|
||||
prop="UpdatedAt"
|
||||
width="180"
|
||||
>
|
||||
<template #default="scope">
|
||||
<div>{{ formatDate(scope.row.UpdatedAt) }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="文件名/备注"
|
||||
prop="name"
|
||||
width="180"
|
||||
align="left"
|
||||
label="文件名/备注"
|
||||
prop="name"
|
||||
width="180"
|
||||
>
|
||||
<template #default="scope">
|
||||
<div class="cursor-pointer" @click="editFileNameFunc(scope.row)">
|
||||
<div
|
||||
class="cursor-pointer"
|
||||
@click="editFileNameFunc(scope.row)"
|
||||
>
|
||||
{{ scope.row.name }}
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="链接" prop="url" min-width="300"/>
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="链接"
|
||||
prop="url"
|
||||
min-width="300"
|
||||
/>
|
||||
<el-table-column align="left" label="标签" prop="tag" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag
|
||||
:type="scope.row.tag?.toLowerCase() === 'jpg' ? 'info' : 'success'"
|
||||
disable-transitions
|
||||
>{{ scope.row.tag }}
|
||||
:type="
|
||||
scope.row.tag?.toLowerCase() === 'jpg' ? 'info' : 'success'
|
||||
"
|
||||
disable-transitions
|
||||
>{{ scope.row.tag }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="操作" width="160">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
icon="download"
|
||||
type="primary"
|
||||
link
|
||||
@click="downloadFile(scope.row)"
|
||||
>下载
|
||||
</el-button
|
||||
>
|
||||
icon="download"
|
||||
type="primary"
|
||||
link
|
||||
@click="downloadFile(scope.row)"
|
||||
>下载
|
||||
</el-button>
|
||||
<el-button
|
||||
icon="delete"
|
||||
type="primary"
|
||||
link
|
||||
@click="deleteFileFunc(scope.row)"
|
||||
>删除
|
||||
</el-button
|
||||
>
|
||||
icon="delete"
|
||||
type="primary"
|
||||
link
|
||||
@click="deleteFileFunc(scope.row)"
|
||||
>删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="gva-pagination">
|
||||
<el-pagination
|
||||
:current-page="page"
|
||||
:page-size="pageSize"
|
||||
:page-sizes="[10, 30, 50, 100]"
|
||||
:style="{ float: 'right', padding: '20px' }"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@current-change="handleCurrentChange"
|
||||
@size-change="handleSizeChange"
|
||||
:current-page="page"
|
||||
:page-size="pageSize"
|
||||
:page-sizes="[10, 30, 50, 100]"
|
||||
:style="{ float: 'right', padding: '20px' }"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@current-change="handleCurrentChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -127,23 +167,34 @@
|
||||
</div>
|
||||
|
||||
<!-- 添加分类弹窗 -->
|
||||
<el-dialog v-model="categoryDialogVisible" @close="closeAddCategoryDialog" width="520"
|
||||
:title="(categoryFormData.ID === 0 ? '添加' : '编辑') + '分类'"
|
||||
draggable
|
||||
<el-dialog
|
||||
v-model="categoryDialogVisible"
|
||||
@close="closeAddCategoryDialog"
|
||||
width="520"
|
||||
:title="(categoryFormData.ID === 0 ? '添加' : '编辑') + '分类'"
|
||||
draggable
|
||||
>
|
||||
<el-form ref="categoryForm" :rules="rules" :model="categoryFormData" label-width="80px">
|
||||
<el-form
|
||||
ref="categoryForm"
|
||||
:rules="rules"
|
||||
:model="categoryFormData"
|
||||
label-width="80px"
|
||||
>
|
||||
<el-form-item label="上级分类">
|
||||
<el-tree-select
|
||||
v-model="categoryFormData.pid"
|
||||
:data="categories"
|
||||
check-strictly
|
||||
:props="defaultProps"
|
||||
:render-after-expand="false"
|
||||
style="width: 240px"
|
||||
v-model="categoryFormData.pid"
|
||||
:data="categories"
|
||||
check-strictly
|
||||
:props="defaultProps"
|
||||
:render-after-expand="false"
|
||||
style="width: 240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="分类名称" prop="name">
|
||||
<el-input v-model.trim="categoryFormData.name" placeholder="分类名称"></el-input>
|
||||
<el-input
|
||||
v-model.trim="categoryFormData.name"
|
||||
placeholder="分类名称"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
@@ -151,88 +202,91 @@
|
||||
<el-button type="primary" @click="confirmAddCategory">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
getFileList,
|
||||
deleteFile,
|
||||
editFileName,
|
||||
importURL
|
||||
} from '@/api/fileUploadAndDownload'
|
||||
import {downloadImage} from '@/utils/downloadImg'
|
||||
import CustomPic from '@/components/customPic/index.vue'
|
||||
import UploadImage from '@/components/upload/image.vue'
|
||||
import UploadCommon from '@/components/upload/common.vue'
|
||||
import {CreateUUID, formatDate} from '@/utils/format'
|
||||
import WarningBar from '@/components/warningBar/warningBar.vue'
|
||||
import {
|
||||
getFileList,
|
||||
deleteFile,
|
||||
editFileName,
|
||||
importURL
|
||||
} from '@/api/fileUploadAndDownload'
|
||||
import { downloadImage } from '@/utils/downloadImg'
|
||||
import CustomPic from '@/components/customPic/index.vue'
|
||||
import UploadImage from '@/components/upload/image.vue'
|
||||
import UploadCommon from '@/components/upload/common.vue'
|
||||
import { CreateUUID, formatDate } from '@/utils/format'
|
||||
import WarningBar from '@/components/warningBar/warningBar.vue'
|
||||
|
||||
import {ref} from 'vue'
|
||||
import {ElMessage, ElMessageBox} from 'element-plus'
|
||||
import {addCategory, deleteCategory, getCategoryList} from "@/api/attachmentCategory";
|
||||
import CropperImage from "@/components/upload/cropper.vue";
|
||||
import QRCodeUpload from "@/components/upload/QR-code.vue";
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
addCategory,
|
||||
deleteCategory,
|
||||
getCategoryList
|
||||
} from '@/api/attachmentCategory'
|
||||
import CropperImage from '@/components/upload/cropper.vue'
|
||||
import QRCodeUpload from '@/components/upload/QR-code.vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'Upload'
|
||||
})
|
||||
|
||||
const fullscreenLoading = ref(false)
|
||||
const path = ref(import.meta.env.VITE_BASE_API)
|
||||
|
||||
const imageUrl = ref('')
|
||||
const imageCommon = ref('')
|
||||
|
||||
const page = ref(1)
|
||||
const total = ref(0)
|
||||
const pageSize = ref(10)
|
||||
const search = ref({
|
||||
keyword: null,
|
||||
classId: 0
|
||||
})
|
||||
const tableData = ref([])
|
||||
|
||||
// 分页
|
||||
const handleSizeChange = (val) => {
|
||||
pageSize.value = val
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (val) => {
|
||||
page.value = val
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const onSubmit = () => {
|
||||
search.value.classId = 0
|
||||
page.value = 1
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 查询
|
||||
const getTableData = async () => {
|
||||
const table = await getFileList({
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
...search.value
|
||||
defineOptions({
|
||||
name: 'Upload'
|
||||
})
|
||||
if (table.code === 0) {
|
||||
tableData.value = table.data.list
|
||||
total.value = table.data.total
|
||||
page.value = table.data.page
|
||||
pageSize.value = table.data.pageSize
|
||||
|
||||
const fullscreenLoading = ref(false)
|
||||
const path = ref(import.meta.env.VITE_BASE_API)
|
||||
|
||||
const imageUrl = ref('')
|
||||
const imageCommon = ref('')
|
||||
|
||||
const page = ref(1)
|
||||
const total = ref(0)
|
||||
const pageSize = ref(10)
|
||||
const search = ref({
|
||||
keyword: null,
|
||||
classId: 0
|
||||
})
|
||||
const tableData = ref([])
|
||||
|
||||
// 分页
|
||||
const handleSizeChange = (val) => {
|
||||
pageSize.value = val
|
||||
getTableData()
|
||||
}
|
||||
}
|
||||
getTableData()
|
||||
|
||||
const deleteFileFunc = async (row) => {
|
||||
ElMessageBox.confirm('此操作将永久删除文件, 是否继续?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
const handleCurrentChange = (val) => {
|
||||
page.value = val
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const onSubmit = () => {
|
||||
search.value.classId = 0
|
||||
page.value = 1
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 查询
|
||||
const getTableData = async () => {
|
||||
const table = await getFileList({
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
...search.value
|
||||
})
|
||||
if (table.code === 0) {
|
||||
tableData.value = table.data.list
|
||||
total.value = table.data.total
|
||||
page.value = table.data.page
|
||||
pageSize.value = table.data.pageSize
|
||||
}
|
||||
}
|
||||
getTableData()
|
||||
|
||||
const deleteFileFunc = async (row) => {
|
||||
ElMessageBox.confirm('此操作将永久删除文件, 是否继续?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
.then(async () => {
|
||||
const res = await deleteFile(row)
|
||||
if (res.code === 0) {
|
||||
@@ -252,30 +306,30 @@ const deleteFileFunc = async (row) => {
|
||||
message: '已取消删除'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const downloadFile = (row) => {
|
||||
if (row.url.indexOf('http://') > -1 || row.url.indexOf('https://') > -1) {
|
||||
downloadImage(row.url, row.name)
|
||||
} else {
|
||||
downloadImage(path.value + '/' + row.url, row.name)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑文件名或者备注
|
||||
* @param row
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const editFileNameFunc = async (row) => {
|
||||
ElMessageBox.prompt('请输入文件名或者备注', '编辑', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputPattern: /\S/,
|
||||
inputErrorMessage: '不能为空',
|
||||
inputValue: row.name
|
||||
})
|
||||
.then(async ({value}) => {
|
||||
const downloadFile = (row) => {
|
||||
if (row.url.indexOf('http://') > -1 || row.url.indexOf('https://') > -1) {
|
||||
downloadImage(row.url, row.name)
|
||||
} else {
|
||||
downloadImage(path.value + '/' + row.url, row.name)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑文件名或者备注
|
||||
* @param row
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const editFileNameFunc = async (row) => {
|
||||
ElMessageBox.prompt('请输入文件名或者备注', '编辑', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputPattern: /\S/,
|
||||
inputErrorMessage: '不能为空',
|
||||
inputValue: row.name
|
||||
})
|
||||
.then(async ({ value }) => {
|
||||
row.name = value
|
||||
// console.log(row)
|
||||
const res = await editFileName(row)
|
||||
@@ -293,22 +347,22 @@ const editFileNameFunc = async (row) => {
|
||||
message: '取消修改'
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入URL
|
||||
*/
|
||||
const importUrlFunc = () => {
|
||||
ElMessageBox.prompt('格式:文件名|链接或者仅链接。', '导入', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputType: 'textarea',
|
||||
inputPlaceholder:
|
||||
/**
|
||||
* 导入URL
|
||||
*/
|
||||
const importUrlFunc = () => {
|
||||
ElMessageBox.prompt('格式:文件名|链接或者仅链接。', '导入', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputType: 'textarea',
|
||||
inputPlaceholder:
|
||||
'我的图片|https://my-oss.com/my.png\nhttps://my-oss.com/my_1.png',
|
||||
inputPattern: /\S/,
|
||||
inputErrorMessage: '不能为空'
|
||||
})
|
||||
.then(async ({value}) => {
|
||||
inputPattern: /\S/,
|
||||
inputErrorMessage: '不能为空'
|
||||
})
|
||||
.then(async ({ value }) => {
|
||||
let data = value.split('\n')
|
||||
let importData = []
|
||||
data.forEach((item) => {
|
||||
@@ -348,101 +402,101 @@ const importUrlFunc = () => {
|
||||
message: '取消导入'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const onSuccess = () => {
|
||||
search.value.keyword = null
|
||||
page.value = 1
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
children: 'children',
|
||||
label: 'name',
|
||||
value: 'ID'
|
||||
}
|
||||
|
||||
const categories = ref([])
|
||||
const fetchCategories = async () => {
|
||||
const res = await getCategoryList()
|
||||
let data = {
|
||||
name: '全部分类',
|
||||
ID: 0,
|
||||
pid: 0,
|
||||
children:[]
|
||||
}
|
||||
if (res.code === 0) {
|
||||
categories.value = res.data || []
|
||||
categories.value.unshift(data)
|
||||
|
||||
const onSuccess = () => {
|
||||
search.value.keyword = null
|
||||
page.value = 1
|
||||
getTableData()
|
||||
}
|
||||
}
|
||||
|
||||
const handleNodeClick = (node) => {
|
||||
search.value.keyword = null
|
||||
search.value.classId = node.ID
|
||||
page.value = 1
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const categoryDialogVisible = ref(false)
|
||||
const categoryFormData = ref({
|
||||
ID: 0,
|
||||
pid: 0,
|
||||
name: ''
|
||||
})
|
||||
|
||||
const categoryForm = ref(null)
|
||||
const rules = ref({
|
||||
name: [
|
||||
{required: true, message: '请输入分类名称', trigger: 'blur'},
|
||||
{max: 20, message: '最多20位字符', trigger: 'blur'}
|
||||
]
|
||||
})
|
||||
|
||||
const addCategoryFun = (category) => {
|
||||
categoryDialogVisible.value = true
|
||||
categoryFormData.value.ID = 0
|
||||
categoryFormData.value.pid = category.ID
|
||||
}
|
||||
|
||||
const editCategory = (category) => {
|
||||
categoryFormData.value = {
|
||||
ID: category.ID,
|
||||
pid: category.pid,
|
||||
name: category.name
|
||||
const defaultProps = {
|
||||
children: 'children',
|
||||
label: 'name',
|
||||
value: 'ID'
|
||||
}
|
||||
categoryDialogVisible.value = true
|
||||
}
|
||||
|
||||
const deleteCategoryFun = async (id) => {
|
||||
const res = await deleteCategory({id: id})
|
||||
if (res.code === 0) {
|
||||
ElMessage.success({type: 'success', message: '删除成功'})
|
||||
await fetchCategories()
|
||||
}
|
||||
}
|
||||
|
||||
const confirmAddCategory = async () => {
|
||||
categoryForm.value.validate(async valid => {
|
||||
if (valid) {
|
||||
const res = await addCategory(categoryFormData.value)
|
||||
if (res.code === 0) {
|
||||
ElMessage({type: 'success', message: '操作成功'})
|
||||
await fetchCategories()
|
||||
closeAddCategoryDialog()
|
||||
}
|
||||
const categories = ref([])
|
||||
const fetchCategories = async () => {
|
||||
const res = await getCategoryList()
|
||||
let data = {
|
||||
name: '全部分类',
|
||||
ID: 0,
|
||||
pid: 0,
|
||||
children: []
|
||||
}
|
||||
})
|
||||
}
|
||||
if (res.code === 0) {
|
||||
categories.value = res.data || []
|
||||
categories.value.unshift(data)
|
||||
}
|
||||
}
|
||||
|
||||
const closeAddCategoryDialog = () => {
|
||||
categoryDialogVisible.value = false
|
||||
categoryFormData.value = {
|
||||
const handleNodeClick = (node) => {
|
||||
search.value.keyword = null
|
||||
search.value.classId = node.ID
|
||||
page.value = 1
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const categoryDialogVisible = ref(false)
|
||||
const categoryFormData = ref({
|
||||
ID: 0,
|
||||
pid: 0,
|
||||
name: ''
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
fetchCategories()
|
||||
const categoryForm = ref(null)
|
||||
const rules = ref({
|
||||
name: [
|
||||
{ required: true, message: '请输入分类名称', trigger: 'blur' },
|
||||
{ max: 20, message: '最多20位字符', trigger: 'blur' }
|
||||
]
|
||||
})
|
||||
|
||||
const addCategoryFun = (category) => {
|
||||
categoryDialogVisible.value = true
|
||||
categoryFormData.value.ID = 0
|
||||
categoryFormData.value.pid = category.ID
|
||||
}
|
||||
|
||||
const editCategory = (category) => {
|
||||
categoryFormData.value = {
|
||||
ID: category.ID,
|
||||
pid: category.pid,
|
||||
name: category.name
|
||||
}
|
||||
categoryDialogVisible.value = true
|
||||
}
|
||||
|
||||
const deleteCategoryFun = async (id) => {
|
||||
const res = await deleteCategory({ id: id })
|
||||
if (res.code === 0) {
|
||||
ElMessage.success({ type: 'success', message: '删除成功' })
|
||||
await fetchCategories()
|
||||
}
|
||||
}
|
||||
|
||||
const confirmAddCategory = async () => {
|
||||
categoryForm.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
const res = await addCategory(categoryFormData.value)
|
||||
if (res.code === 0) {
|
||||
ElMessage({ type: 'success', message: '操作成功' })
|
||||
await fetchCategories()
|
||||
closeAddCategoryDialog()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const closeAddCategoryDialog = () => {
|
||||
categoryDialogVisible.value = false
|
||||
categoryFormData.value = {
|
||||
ID: 0,
|
||||
pid: 0,
|
||||
name: ''
|
||||
}
|
||||
}
|
||||
|
||||
fetchCategories()
|
||||
</script>
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
<div class="flex flex-row w-full gva-container pt-16 box-border !h-full">
|
||||
<gva-aside
|
||||
v-if="
|
||||
config.side_mode === 'normal' || config.side_mode === 'sidebar' ||
|
||||
config.side_mode === 'normal' ||
|
||||
config.side_mode === 'sidebar' ||
|
||||
(device === 'mobile' && config.side_mode == 'head') ||
|
||||
(device === 'mobile' && config.side_mode == 'combination')
|
||||
"
|
||||
@@ -23,10 +24,10 @@
|
||||
v-if="config.side_mode === 'combination' && device !== 'mobile'"
|
||||
mode="normal"
|
||||
/>
|
||||
<div class="flex-1 px-2 w-0 h-full">
|
||||
<div class="flex-1 w-0 h-full">
|
||||
<gva-tabs v-if="config.showTabs" />
|
||||
<div
|
||||
class="overflow-auto"
|
||||
class="overflow-auto px-2"
|
||||
:class="config.showTabs ? 'gva-container2' : 'gva-container pt-1'"
|
||||
>
|
||||
<router-view v-if="reloadFlag" v-slot="{ Component, route }">
|
||||
@@ -34,7 +35,10 @@
|
||||
id="gva-base-load-dom"
|
||||
class="gva-body-h bg-gray-50 dark:bg-slate-800"
|
||||
>
|
||||
<transition mode="out-in" :name="route.meta.transitionType || config.transition_type">
|
||||
<transition
|
||||
mode="out-in"
|
||||
:name="route.meta.transitionType || config.transition_type"
|
||||
>
|
||||
<keep-alive :include="routerStore.keepAliveRouters">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</keep-alive>
|
||||
@@ -55,12 +59,21 @@
|
||||
import GvaTabs from './tabs/index.vue'
|
||||
import BottomInfo from '@/components/bottomInfo/bottomInfo.vue'
|
||||
import { emitter } from '@/utils/bus.js'
|
||||
import { ref, onMounted, nextTick, reactive, watchEffect } from 'vue'
|
||||
import {
|
||||
ref,
|
||||
onMounted,
|
||||
nextTick,
|
||||
reactive,
|
||||
watchEffect,
|
||||
onUnmounted
|
||||
} from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useRouterStore } from '@/pinia/modules/router'
|
||||
import { useUserStore } from '@/pinia/modules/user'
|
||||
import { useAppStore } from '@/pinia'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { socketManager, initSocketConnection } from '@/utils/socket'
|
||||
|
||||
import '@/style/transition.scss'
|
||||
const appStore = useAppStore()
|
||||
const { config, isDark, device } = storeToRefs(appStore)
|
||||
@@ -88,6 +101,16 @@
|
||||
if (userStore.loadingInstance) {
|
||||
userStore.loadingInstance.close()
|
||||
}
|
||||
|
||||
// 初始化 Socket.IO 连接
|
||||
initSocketConnection(userStore)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// 组件卸载时断开 Socket 连接
|
||||
if (socketManager) {
|
||||
socketManager.disconnect()
|
||||
}
|
||||
})
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div class="notice-center">
|
||||
<el-card class="box-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>通知中心</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="notice-content">
|
||||
<p>欢迎使用通知中心!</p>
|
||||
<p>这里是通知中心的主页面,您可以通过左侧菜单访问具体功能:</p>
|
||||
<ul>
|
||||
<li>通知管理:创建、编辑、发送通知</li>
|
||||
<li>在线用户:查看在线用户,管理用户连接</li>
|
||||
</ul>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted } from 'vue'
|
||||
|
||||
onMounted(() => {
|
||||
console.log('Notice Center mounted')
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.notice-center {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.notice-content {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.notice-content ul {
|
||||
margin-top: 15px;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.notice-content li {
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,590 @@
|
||||
<template>
|
||||
<div class="notification-management">
|
||||
<div class="gva-search-box">
|
||||
<el-form
|
||||
:inline="true"
|
||||
:model="searchInfo"
|
||||
class="demo-form-inline"
|
||||
@keyup.enter="onSubmit"
|
||||
>
|
||||
<el-form-item label="标题">
|
||||
<el-input v-model="searchInfo.title" placeholder="搜索条件" />
|
||||
</el-form-item>
|
||||
<el-form-item label="类型">
|
||||
<el-select v-model="searchInfo.type" placeholder="请选择" clearable>
|
||||
<el-option label="系统通知" value="system" />
|
||||
<el-option label="业务通知" value="business" />
|
||||
<el-option label="警告通知" value="warning" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchInfo.status" placeholder="请选择" clearable>
|
||||
<el-option label="草稿" value="draft" />
|
||||
<el-option label="已发送" value="sent" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="search" @click="onSubmit"
|
||||
>查询</el-button
|
||||
>
|
||||
<el-button icon="refresh" @click="onReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="gva-table-box">
|
||||
<div class="gva-btn-list">
|
||||
<el-button type="primary" icon="plus" @click="openDialog"
|
||||
>新增</el-button
|
||||
>
|
||||
<el-button
|
||||
icon="delete"
|
||||
style="margin-left: 10px"
|
||||
:disabled="!multipleSelection.length"
|
||||
@click="onDelete"
|
||||
>删除</el-button
|
||||
>
|
||||
</div>
|
||||
<el-table
|
||||
ref="multipleTable"
|
||||
style="width: 100%"
|
||||
tooltip-effect="dark"
|
||||
:data="tableData"
|
||||
row-key="ID"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column align="left" label="日期" prop="CreatedAt" width="180">
|
||||
<template #default="scope">
|
||||
<span>{{ formatDate(scope.row.createdAt) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="标题" prop="title" />
|
||||
<el-table-column align="left" label="类型" prop="type" width="120">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getTypeTagType(scope.row.type)">{{
|
||||
getTypeLabel(scope.row.type)
|
||||
}}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="目标类型"
|
||||
prop="targetType"
|
||||
width="120"
|
||||
>
|
||||
<template #default="scope">
|
||||
<span>{{ getTargetTypeLabel(scope.row.targetType) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="状态" prop="status" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getStatusTagType(scope.row.status)">{{
|
||||
getStatusLabel(scope.row.status)
|
||||
}}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="操作" fixed="right" width="300">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
icon="view"
|
||||
size="small"
|
||||
@click="getDetails(scope.row)"
|
||||
>查看</el-button
|
||||
>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
icon="edit"
|
||||
size="small"
|
||||
@click="updateNotificationFunc(scope.row)"
|
||||
>变更</el-button
|
||||
>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
icon="delete"
|
||||
size="small"
|
||||
@click="deleteRow(scope.row)"
|
||||
>删除</el-button
|
||||
>
|
||||
<el-button
|
||||
v-if="scope.row.status === 'draft'"
|
||||
type="success"
|
||||
link
|
||||
icon="promotion"
|
||||
size="small"
|
||||
@click="sendNotificationFunc(scope.row)"
|
||||
>发送</el-button
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="gva-pagination">
|
||||
<el-pagination
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:current-page="page"
|
||||
:page-size="pageSize"
|
||||
:page-sizes="[10, 30, 50, 100]"
|
||||
:total="total"
|
||||
@current-change="handleCurrentChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<el-drawer
|
||||
destroy-on-close
|
||||
size="800"
|
||||
v-model="dialogFormVisible"
|
||||
:show-close="false"
|
||||
:before-close="closeDialog"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-lg">{{
|
||||
type === 'create' ? '添加' : type === 'update' ? '修改' : '查看'
|
||||
}}</span>
|
||||
<div>
|
||||
<el-button
|
||||
v-if="type !== 'create'"
|
||||
type="primary"
|
||||
@click="updateNotificationFunc(formData)"
|
||||
>变更</el-button
|
||||
>
|
||||
<el-button
|
||||
v-if="type === 'create'"
|
||||
type="primary"
|
||||
@click="createNotificationFunc"
|
||||
>添加</el-button
|
||||
>
|
||||
<el-button type="primary" @click="closeDialog">取消</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form
|
||||
:model="formData"
|
||||
label-position="top"
|
||||
ref="elFormRef"
|
||||
:rules="rule"
|
||||
label-width="80px"
|
||||
>
|
||||
<el-form-item label="标题:" prop="title">
|
||||
<el-input
|
||||
v-model="formData.title"
|
||||
:clearable="true"
|
||||
placeholder="请输入标题"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="内容:" prop="content">
|
||||
<el-input
|
||||
v-model="formData.content"
|
||||
:clearable="true"
|
||||
placeholder="请输入内容"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="类型:" prop="type">
|
||||
<el-select
|
||||
v-model="formData.type"
|
||||
placeholder="请选择类型"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option label="系统通知" value="system" />
|
||||
<el-option label="业务通知" value="business" />
|
||||
<el-option label="警告通知" value="warning" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="目标类型:" prop="targetType">
|
||||
<el-select
|
||||
v-model="formData.targetType"
|
||||
placeholder="请选择目标类型"
|
||||
style="width: 100%"
|
||||
@change="onTargetTypeChange"
|
||||
>
|
||||
<el-option label="全部用户" value="all" />
|
||||
<el-option label="指定用户" value="users" />
|
||||
<el-option label="指定角色" value="roles" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="formData.targetType === 'users'"
|
||||
label="目标用户:"
|
||||
prop="targetIds"
|
||||
>
|
||||
<el-select
|
||||
v-model="formData.targetIds"
|
||||
placeholder="请选择用户"
|
||||
multiple
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="user in userList"
|
||||
:key="user.ID"
|
||||
:label="user.nickName"
|
||||
:value="user.ID"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="formData.targetType === 'roles'"
|
||||
label="目标角色:"
|
||||
prop="targetIds"
|
||||
>
|
||||
<el-select
|
||||
v-model="formData.targetIds"
|
||||
placeholder="请选择角色"
|
||||
multiple
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="role in roleList"
|
||||
:key="role.authorityId"
|
||||
:label="role.authorityName"
|
||||
:value="role.authorityId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
createNotification,
|
||||
deleteNotification,
|
||||
updateNotification,
|
||||
getNotificationById,
|
||||
getNotificationList,
|
||||
publishNotification
|
||||
} from '@/api/notice'
|
||||
import { getUserList } from '@/api/user'
|
||||
import { getAuthorityList } from '@/api/authority'
|
||||
import { formatDate } from '@/utils/format'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ref, reactive } from 'vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'NotificationManagement'
|
||||
})
|
||||
|
||||
// 响应式数据
|
||||
const formData = ref({
|
||||
title: '',
|
||||
content: '',
|
||||
type: '',
|
||||
targetType: '',
|
||||
targetIds: []
|
||||
})
|
||||
|
||||
const searchInfo = ref({
|
||||
title: '',
|
||||
type: '',
|
||||
status: ''
|
||||
})
|
||||
|
||||
const type = ref('')
|
||||
const page = ref(1)
|
||||
const total = ref(0)
|
||||
const pageSize = ref(10)
|
||||
const tableData = ref([])
|
||||
const dialogFormVisible = ref(false)
|
||||
const multipleSelection = ref([])
|
||||
const userList = ref([])
|
||||
const roleList = ref([])
|
||||
|
||||
// 表单验证规则
|
||||
const rule = reactive({
|
||||
title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
|
||||
content: [{ required: true, message: '请输入内容', trigger: 'blur' }],
|
||||
type: [{ required: true, message: '请选择类型', trigger: 'change' }],
|
||||
targetType: [
|
||||
{ required: true, message: '请选择目标类型', trigger: 'change' }
|
||||
]
|
||||
})
|
||||
|
||||
const elFormRef = ref()
|
||||
|
||||
// 获取列表数据
|
||||
const getTableData = async () => {
|
||||
const table = await getNotificationList({
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
...searchInfo.value
|
||||
})
|
||||
if (table.code === 0) {
|
||||
tableData.value = table.data.list
|
||||
total.value = table.data.total
|
||||
page.value = table.data.page
|
||||
pageSize.value = table.data.pageSize
|
||||
}
|
||||
}
|
||||
|
||||
getTableData()
|
||||
|
||||
// 获取用户列表
|
||||
const getUserListData = async () => {
|
||||
const res = await getUserList({ page: 1, pageSize: 999 })
|
||||
if (res.code === 0) {
|
||||
userList.value = res.data.list
|
||||
}
|
||||
}
|
||||
|
||||
// 获取角色列表
|
||||
const getRoleListData = async () => {
|
||||
const res = await getAuthorityList({ page: 1, pageSize: 999 })
|
||||
if (res.code === 0) {
|
||||
roleList.value = res.data.list
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化数据
|
||||
getUserListData()
|
||||
getRoleListData()
|
||||
|
||||
// 分页
|
||||
const handleSizeChange = (val) => {
|
||||
pageSize.value = val
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (val) => {
|
||||
page.value = val
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 查询
|
||||
const onSubmit = () => {
|
||||
page.value = 1
|
||||
pageSize.value = 10
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 重置
|
||||
const onReset = () => {
|
||||
searchInfo.value = {
|
||||
title: '',
|
||||
type: '',
|
||||
status: ''
|
||||
}
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 多选
|
||||
const handleSelectionChange = (val) => {
|
||||
multipleSelection.value = val
|
||||
}
|
||||
|
||||
// 删除行
|
||||
const deleteRow = (row) => {
|
||||
ElMessageBox.confirm('确定要删除吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
deleteNotificationFunc(row)
|
||||
})
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
const onDelete = async () => {
|
||||
ElMessageBox.confirm('确定要删除吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
const IDs = []
|
||||
if (multipleSelection.value.length === 0) {
|
||||
ElMessage({
|
||||
type: 'warning',
|
||||
message: '请选择要删除的数据'
|
||||
})
|
||||
return
|
||||
}
|
||||
multipleSelection.value &&
|
||||
multipleSelection.value.map((item) => {
|
||||
IDs.push(item.ID)
|
||||
})
|
||||
const res = await deleteNotification({ IDs })
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '删除成功'
|
||||
})
|
||||
if (tableData.value.length === IDs.length && page.value > 1) {
|
||||
page.value--
|
||||
}
|
||||
getTableData()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 弹窗控制标记
|
||||
const initForm = () => {
|
||||
elFormRef.value?.resetFields()
|
||||
formData.value = {
|
||||
title: '',
|
||||
content: '',
|
||||
type: '',
|
||||
targetType: '',
|
||||
targetIds: []
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
const closeDialog = () => {
|
||||
dialogFormVisible.value = false
|
||||
initForm()
|
||||
}
|
||||
|
||||
// 弹窗打开的标记
|
||||
const openDialog = () => {
|
||||
type.value = 'create'
|
||||
dialogFormVisible.value = true
|
||||
}
|
||||
|
||||
// 创建
|
||||
const createNotificationFunc = async () => {
|
||||
elFormRef.value?.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
const res = await createNotification(formData.value)
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '创建成功'
|
||||
})
|
||||
closeDialog()
|
||||
getTableData()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 更新
|
||||
const updateNotificationFunc = async (row) => {
|
||||
if (type.value === 'update') {
|
||||
elFormRef.value?.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
const res = await updateNotification(formData.value)
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '更新成功'
|
||||
})
|
||||
closeDialog()
|
||||
getTableData()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
const res = await getNotificationById(row.id)
|
||||
type.value = 'update'
|
||||
if (res.code === 0) {
|
||||
formData.value = res.data
|
||||
formData.value.targetIds = res.data.targetIds.split(',') || []
|
||||
dialogFormVisible.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 删除
|
||||
const deleteNotificationFunc = async (row) => {
|
||||
const res = await deleteNotification(row.id)
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '删除成功'
|
||||
})
|
||||
if (tableData.value.length === 1 && page.value > 1) {
|
||||
page.value--
|
||||
}
|
||||
getTableData()
|
||||
}
|
||||
}
|
||||
|
||||
// 发送通知
|
||||
const sendNotificationFunc = async (row) => {
|
||||
ElMessageBox.confirm('确定要发送这条通知吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'info'
|
||||
}).then(async () => {
|
||||
console.log(row)
|
||||
const res = await publishNotification(row.id)
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '发送成功'
|
||||
})
|
||||
getTableData()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const getDetails = async (row) => {
|
||||
const res = await getNotificationById(row.id)
|
||||
if (res.code === 0) {
|
||||
formData.value = res.data
|
||||
formData.value.targetIds = res.data.targetIds.split(',') || []
|
||||
type.value = 'look'
|
||||
dialogFormVisible.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// 目标类型改变
|
||||
const onTargetTypeChange = () => {
|
||||
formData.value.targetIds = []
|
||||
}
|
||||
|
||||
// 获取类型标签类型
|
||||
const getTypeTagType = (type) => {
|
||||
const typeMap = {
|
||||
system: 'primary',
|
||||
business: 'success',
|
||||
warning: 'warning'
|
||||
}
|
||||
return typeMap[type] || ''
|
||||
}
|
||||
|
||||
// 获取类型标签
|
||||
const getTypeLabel = (type) => {
|
||||
const typeMap = {
|
||||
system: '系统通知',
|
||||
business: '业务通知',
|
||||
warning: '警告通知'
|
||||
}
|
||||
return typeMap[type] || type
|
||||
}
|
||||
|
||||
// 获取目标类型标签
|
||||
const getTargetTypeLabel = (targetType) => {
|
||||
const targetTypeMap = {
|
||||
all: '全部用户',
|
||||
users: '指定用户',
|
||||
roles: '指定角色'
|
||||
}
|
||||
return targetTypeMap[targetType] || targetType
|
||||
}
|
||||
|
||||
// 获取状态标签类型
|
||||
const getStatusTagType = (status) => {
|
||||
const statusMap = {
|
||||
draft: 'info',
|
||||
published: 'success'
|
||||
}
|
||||
return statusMap[status] || ''
|
||||
}
|
||||
|
||||
// 获取状态标签
|
||||
const getStatusLabel = (status) => {
|
||||
const statusMap = {
|
||||
draft: '草稿',
|
||||
published: '已发送'
|
||||
}
|
||||
return statusMap[status] || status
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
@@ -0,0 +1,447 @@
|
||||
<template>
|
||||
<div class="online-user-management">
|
||||
<div class="gva-search-box">
|
||||
<el-form
|
||||
:inline="true"
|
||||
:model="searchInfo"
|
||||
class="demo-form-inline"
|
||||
@keyup.enter="onSubmit"
|
||||
>
|
||||
<el-form-item label="用户名" prop="username">
|
||||
<el-input
|
||||
v-model="searchInfo.username"
|
||||
placeholder="请输入用户名"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="昵称" prop="nickName">
|
||||
<el-input
|
||||
v-model="searchInfo.nickName"
|
||||
placeholder="请输入昵称"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色ID" prop="roleId">
|
||||
<el-input-number
|
||||
v-model="searchInfo.roleId"
|
||||
placeholder="请输入角色ID"
|
||||
clearable
|
||||
:min="1"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="onSubmit">查询</el-button>
|
||||
<el-button @click="onReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="m-4">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6">
|
||||
<el-card class="">
|
||||
<div class="flex items-center justify-between">
|
||||
<div
|
||||
class="bg-green-5 rounded-full text-white text-2xl w-12 h-12 p-4 flex items-center justify-center"
|
||||
>
|
||||
<el-icon><User /></el-icon>
|
||||
</div>
|
||||
<div class="flex-1 flex items-center flex-col items-end">
|
||||
<div class="text-3xl font-bold">
|
||||
{{ onlineStats.totalOnline || 0 }}
|
||||
</div>
|
||||
<div class="text-sm mt-2 text-center text-black/60">
|
||||
在线用户
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="">
|
||||
<div class="flex items-center justify-between">
|
||||
<div
|
||||
class="bg-blue-5 rounded-full text-white text-2xl w-12 h-12 p-4 flex items-center justify-center today"
|
||||
>
|
||||
<el-icon><Calendar /></el-icon>
|
||||
</div>
|
||||
<div class="flex-1 flex items-center flex-col">
|
||||
<div class="text-3xl font-bold">
|
||||
{{ onlineStats.todayLogin || 0 }}
|
||||
</div>
|
||||
<div class="text-sm mt-2 text-center text-black/60">
|
||||
今日登录
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="">
|
||||
<div class="flex items-center justify-between">
|
||||
<div
|
||||
class="bg-orange-5 rounded-full text-white text-2xl w-12 h-12 p-4 flex items-center justify-center peak"
|
||||
>
|
||||
<el-icon><TrendCharts /></el-icon>
|
||||
</div>
|
||||
<div class="flex-1 flex items-center flex-col">
|
||||
<div class="text-3xl font-bold">
|
||||
{{ onlineStats.peakOnline || 0 }}
|
||||
</div>
|
||||
<div class="text-sm mt-2 text-center text-black/60">
|
||||
峰值在线
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="">
|
||||
<div class="flex items-center justify-between">
|
||||
<div
|
||||
class="bg-amber rounded-full text-white text-2xl w-12 h-12 p-4 flex items-center justify-center avg"
|
||||
>
|
||||
<el-icon><DataAnalysis /></el-icon>
|
||||
</div>
|
||||
<div class="flex-1 flex items-center flex-col">
|
||||
<div class="text-3xl font-bold">
|
||||
{{ onlineStats.avgOnline || 0 }}
|
||||
</div>
|
||||
<div class="text-sm mt-2 text-center text-black/60">
|
||||
平均在线
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<div class="gva-table-box">
|
||||
<div class="gva-btn-list">
|
||||
<el-button type="primary" icon="refresh" @click="getTableData"
|
||||
>刷新</el-button
|
||||
>
|
||||
<el-button
|
||||
type="danger"
|
||||
icon="close"
|
||||
style="margin-left: 10px"
|
||||
:disabled="!multipleSelection.length"
|
||||
@click="onKickUsers"
|
||||
>批量下线</el-button
|
||||
>
|
||||
</div>
|
||||
<el-table
|
||||
ref="multipleTable"
|
||||
style="width: 100%"
|
||||
tooltip-effect="dark"
|
||||
:data="tableData"
|
||||
row-key="ID"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column align="left" label="用户名" prop="username" />
|
||||
<el-table-column align="left" label="昵称" prop="nickName" />
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="Socket ID"
|
||||
prop="socketId"
|
||||
width="280"
|
||||
>
|
||||
<template #default="scope">
|
||||
<el-tag size="small">{{ scope.row.socketId }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="IP地址"
|
||||
prop="ipAddress"
|
||||
width="150"
|
||||
/>
|
||||
<!-- <el-table-column
|
||||
align="left"
|
||||
label="用户代理"
|
||||
prop="userAgent"
|
||||
width="200"
|
||||
>
|
||||
<template #default="scope">
|
||||
<el-tooltip :content="scope.row.userAgent" placement="top">
|
||||
<span class="user-agent-text">{{ scope.row.userAgent }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column> -->
|
||||
<!-- <el-table-column
|
||||
align="left"
|
||||
label="登录时间"
|
||||
prop="loginTime"
|
||||
width="180"
|
||||
>
|
||||
<template #default="scope">
|
||||
<span>{{ formatDate(scope.row.loginTime) }}</span>
|
||||
</template>
|
||||
</el-table-column> -->
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="最后活跃"
|
||||
prop="lastActiveTime"
|
||||
width="180"
|
||||
>
|
||||
<template #default="scope">
|
||||
<span>{{ formatDate(scope.row.lastActiveTime) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="在线时长"
|
||||
prop="onlineDuration"
|
||||
width="120"
|
||||
>
|
||||
<template #default="scope">
|
||||
<span>{{ formatDuration(scope.row.onlineDuration) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="状态" prop="status" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === 'online' ? 'success' : 'info'">
|
||||
{{ scope.row.status === 'online' ? '在线' : '离线' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="操作"
|
||||
fixed="right"
|
||||
min-width="120"
|
||||
>
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
v-if="scope.row.status === 'online'"
|
||||
type="danger"
|
||||
link
|
||||
icon="close"
|
||||
size="small"
|
||||
@click="kickUser(scope.row)"
|
||||
>强制下线</el-button
|
||||
>
|
||||
<span v-else class="text-gray-400">已离线</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="gva-pagination">
|
||||
<el-pagination
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:current-page="page"
|
||||
:page-size="pageSize"
|
||||
:page-sizes="[10, 30, 50, 100]"
|
||||
:total="total"
|
||||
@current-change="handleCurrentChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
getOnlineUserList,
|
||||
kickUser as kickUserApi,
|
||||
getOnlineUserStats
|
||||
} from '@/api/notice'
|
||||
import { formatDate } from '@/utils/format'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import {
|
||||
User,
|
||||
Calendar,
|
||||
TrendCharts,
|
||||
DataAnalysis
|
||||
} from '@element-plus/icons-vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'OnlineUserManagement'
|
||||
})
|
||||
|
||||
// 响应式数据
|
||||
const searchInfo = ref({
|
||||
username: '',
|
||||
nickName: '',
|
||||
roleId: null
|
||||
})
|
||||
|
||||
const page = ref(1)
|
||||
const total = ref(0)
|
||||
const pageSize = ref(10)
|
||||
const tableData = ref([])
|
||||
const multipleSelection = ref([])
|
||||
const onlineStats = ref({})
|
||||
const refreshTimer = ref(null)
|
||||
|
||||
// 获取列表数据
|
||||
const getTableData = async () => {
|
||||
const table = await getOnlineUserList({
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
...searchInfo.value
|
||||
})
|
||||
if (table.code === 0) {
|
||||
tableData.value = table.data.list
|
||||
total.value = table.data.total
|
||||
page.value = table.data.page
|
||||
pageSize.value = table.data.pageSize
|
||||
}
|
||||
}
|
||||
|
||||
// 获取统计数据
|
||||
const getStatsData = async () => {
|
||||
const res = await getOnlineUserStats()
|
||||
if (res.code === 0) {
|
||||
onlineStats.value = res.data
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化数据
|
||||
const initData = async () => {
|
||||
await getTableData()
|
||||
await getStatsData()
|
||||
}
|
||||
|
||||
initData()
|
||||
|
||||
// 设置定时刷新
|
||||
const startAutoRefresh = () => {
|
||||
refreshTimer.value = setInterval(() => {
|
||||
getTableData()
|
||||
getStatsData()
|
||||
}, 30000) // 30秒刷新一次
|
||||
}
|
||||
|
||||
// 停止定时刷新
|
||||
const stopAutoRefresh = () => {
|
||||
if (refreshTimer.value) {
|
||||
clearInterval(refreshTimer.value)
|
||||
refreshTimer.value = null
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
startAutoRefresh()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopAutoRefresh()
|
||||
})
|
||||
|
||||
// 分页
|
||||
const handleSizeChange = (val) => {
|
||||
pageSize.value = val
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (val) => {
|
||||
page.value = val
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 查询
|
||||
const onSubmit = () => {
|
||||
page.value = 1
|
||||
pageSize.value = 10
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 重置
|
||||
const onReset = () => {
|
||||
searchInfo.value = {
|
||||
username: '',
|
||||
nickName: '',
|
||||
roleId: null
|
||||
}
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 多选
|
||||
const handleSelectionChange = (val) => {
|
||||
multipleSelection.value = val
|
||||
}
|
||||
|
||||
// 强制用户下线
|
||||
const kickUser = (row) => {
|
||||
ElMessageBox.confirm(`确定要强制用户 "${row.username}" 下线吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
const res = await kickUserApi(row.userId)
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '操作成功'
|
||||
})
|
||||
getTableData()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 批量强制下线
|
||||
const onKickUsers = async () => {
|
||||
if (multipleSelection.value.length === 0) {
|
||||
ElMessage({
|
||||
type: 'warning',
|
||||
message: '请选择要下线的用户'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const onlineUsers = multipleSelection.value.filter(
|
||||
(user) => user.status === 'online'
|
||||
)
|
||||
if (onlineUsers.length === 0) {
|
||||
ElMessage({
|
||||
type: 'warning',
|
||||
message: '所选用户均已离线'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ElMessageBox.confirm(
|
||||
`确定要强制 ${onlineUsers.length} 个用户下线吗?`,
|
||||
'提示',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
).then(async () => {
|
||||
const promises = onlineUsers.map((user) => kickUserApi(user.userId))
|
||||
try {
|
||||
await Promise.all(promises)
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '批量下线成功'
|
||||
})
|
||||
getTableData()
|
||||
} catch (error) {
|
||||
ElMessage({
|
||||
type: 'error',
|
||||
message: '部分用户下线失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 格式化在线时长
|
||||
const formatDuration = (seconds) => {
|
||||
if (!seconds) return '0分钟'
|
||||
|
||||
const hours = Math.floor(seconds / 3600)
|
||||
const minutes = Math.floor((seconds % 3600) / 60)
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}小时${minutes}分钟`
|
||||
} else {
|
||||
return `${minutes}分钟`
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,667 @@
|
||||
<template>
|
||||
<div class="user-notification-center">
|
||||
<!-- 页面头部 -->
|
||||
<div class="gva-search-box">
|
||||
<el-form
|
||||
:inline="true"
|
||||
:model="searchInfo"
|
||||
class="demo-form-inline"
|
||||
@keyup.enter="onSubmit"
|
||||
>
|
||||
<el-form-item label="通知类型">
|
||||
<el-select v-model="searchInfo.type" placeholder="请选择" clearable>
|
||||
<el-option label="系统通知" value="system" />
|
||||
<el-option label="业务通知" value="business" />
|
||||
<el-option label="警告通知" value="warning" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="阅读状态">
|
||||
<el-select v-model="searchInfo.isRead" placeholder="请选择" clearable>
|
||||
<el-option label="未读" :value="false" />
|
||||
<el-option label="已读" :value="true" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="优先级">
|
||||
<el-select
|
||||
v-model="searchInfo.priority"
|
||||
placeholder="请选择"
|
||||
clearable
|
||||
>
|
||||
<el-option label="低" value="low" />
|
||||
<el-option label="普通" value="normal" />
|
||||
<el-option label="高" value="high" />
|
||||
<el-option label="紧急" value="urgent" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="search" @click="onSubmit">
|
||||
查询
|
||||
</el-button>
|
||||
<el-button icon="refresh" @click="onReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 通知列表 -->
|
||||
<div class="gva-table-box">
|
||||
<div class="gva-btn-list mb-4">
|
||||
<el-button
|
||||
type="success"
|
||||
icon="check"
|
||||
:disabled="selectedNotifications.length === 0"
|
||||
@click="batchMarkAsRead"
|
||||
>
|
||||
批量已读
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
icon="delete"
|
||||
:disabled="selectedNotifications.length === 0"
|
||||
@click="batchDeleteNotifications"
|
||||
>
|
||||
批量删除
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
ref="multipleTable"
|
||||
:data="tableData"
|
||||
style="width: 100%"
|
||||
tooltip-effect="dark"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-click="handleRowClick"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
|
||||
<el-table-column prop="title" label="标题" min-width="200">
|
||||
<template #default="scope">
|
||||
<div
|
||||
class="notification-title"
|
||||
:class="{ unread: !scope.row.isRead }"
|
||||
>
|
||||
<el-icon v-if="!scope.row.isRead" class="unread-dot">
|
||||
<CircleCheck />
|
||||
</el-icon>
|
||||
{{ scope.row.title }}
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="type" label="类型" width="120">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getTypeTagType(scope.row.type)" size="small">
|
||||
{{ getTypeLabel(scope.row.type) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="priority" label="优先级" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getPriorityTagType(scope.row.priority)" size="small">
|
||||
{{ getPriorityLabel(scope.row.priority) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="isRead" label="状态" width="80">
|
||||
<template #default="scope">
|
||||
<el-tag
|
||||
:type="scope.row.isRead ? 'success' : 'warning'"
|
||||
size="small"
|
||||
>
|
||||
{{ scope.row.isRead ? '已读' : '未读' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="createdAt" label="发送时间" width="180">
|
||||
<template #default="scope">
|
||||
{{ formatDate(scope.row.createdAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="readTime" label="阅读时间" width="180">
|
||||
<template #default="scope">
|
||||
{{ scope.row.readTime ? formatDate(scope.row.readTime) : '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
size="small"
|
||||
type="primary"
|
||||
@click.stop="viewNotification(scope.row)"
|
||||
>
|
||||
查看
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="!scope.row.isRead"
|
||||
size="small"
|
||||
type="success"
|
||||
@click.stop="markAsRead(scope.row)"
|
||||
>
|
||||
标记已读
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="danger"
|
||||
@click.stop="deleteNotificationHandler(scope.row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="gva-pagination">
|
||||
<el-pagination
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:current-page="page"
|
||||
:page-size="pageSize"
|
||||
:page-sizes="[10, 25, 50, 100]"
|
||||
:total="total"
|
||||
@current-change="handleCurrentChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 通知详情弹窗 -->
|
||||
<el-dialog
|
||||
v-model="detailDialogVisible"
|
||||
title="通知详情"
|
||||
width="60%"
|
||||
:before-close="closeDetailDialog"
|
||||
>
|
||||
<div v-if="currentNotification" class="notification-detail">
|
||||
<div class="detail-header">
|
||||
<h3>{{ currentNotification.title }}</h3>
|
||||
<div class="detail-meta">
|
||||
<el-tag
|
||||
:type="getTypeTagType(currentNotification.type)"
|
||||
size="small"
|
||||
>
|
||||
{{ getTypeLabel(currentNotification.type) }}
|
||||
</el-tag>
|
||||
<el-tag
|
||||
:type="getPriorityTagType(currentNotification.priority)"
|
||||
size="small"
|
||||
class="ml-2"
|
||||
>
|
||||
{{ getPriorityLabel(currentNotification.priority) }}
|
||||
</el-tag>
|
||||
<span class="detail-time ml-4">
|
||||
发送时间:{{ formatDate(currentNotification.createdAt) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-content">
|
||||
<div class="content-text" v-html="currentNotification.content"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button
|
||||
v-if="currentNotification && !currentNotification.isRead"
|
||||
type="success"
|
||||
@click="markAsReadAndClose"
|
||||
>
|
||||
标记已读
|
||||
</el-button>
|
||||
<el-button type="danger" @click="deleteAndClose"> 删除 </el-button>
|
||||
<el-button @click="closeDetailDialog">关闭</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, nextTick } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { CircleCheck } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getUserNotificationList,
|
||||
markNotificationAsRead,
|
||||
getUnreadNotificationCount,
|
||||
deleteNotification as deleteNotificationApi
|
||||
} from '@/api/notice.js'
|
||||
import { formatTimeToStr } from '@/utils/date'
|
||||
|
||||
defineOptions({
|
||||
name: 'UserNotificationCenter'
|
||||
})
|
||||
|
||||
// 响应式数据
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const total = ref(0)
|
||||
const tableData = ref([])
|
||||
const loading = ref(false)
|
||||
const selectedNotifications = ref([])
|
||||
const detailDialogVisible = ref(false)
|
||||
const currentNotification = ref(null)
|
||||
|
||||
// 搜索条件
|
||||
const searchInfo = reactive({
|
||||
type: '',
|
||||
isRead: null,
|
||||
priority: ''
|
||||
})
|
||||
|
||||
// 统计信息
|
||||
const stats = reactive({
|
||||
total: 0,
|
||||
unread: 0,
|
||||
read: 0
|
||||
})
|
||||
|
||||
// 获取通知列表
|
||||
const getTableData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
...searchInfo
|
||||
}
|
||||
|
||||
const res = await getUserNotificationList(params)
|
||||
if (res.code === 0) {
|
||||
tableData.value = res.data.list || []
|
||||
total.value = res.data.total || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取通知列表失败:', error)
|
||||
ElMessage.error('获取通知列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取统计信息
|
||||
const getStats = async () => {
|
||||
try {
|
||||
const res = await getUnreadNotificationCount()
|
||||
if (res.code === 0) {
|
||||
stats.total = res.data.total || 0
|
||||
stats.unread = res.data.unread || 0
|
||||
stats.read = res.data.read || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取统计信息失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const onSubmit = () => {
|
||||
page.value = 1
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const onReset = () => {
|
||||
searchInfo.type = ''
|
||||
searchInfo.isRead = null
|
||||
searchInfo.priority = ''
|
||||
page.value = 1
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 分页处理
|
||||
const handleCurrentChange = (val) => {
|
||||
page.value = val
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const handleSizeChange = (val) => {
|
||||
pageSize.value = val
|
||||
page.value = 1
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 选择处理
|
||||
const handleSelectionChange = (val) => {
|
||||
selectedNotifications.value = val
|
||||
}
|
||||
|
||||
// 行点击处理
|
||||
const handleRowClick = (row) => {
|
||||
viewNotification(row)
|
||||
}
|
||||
|
||||
// 查看通知详情
|
||||
const viewNotification = (row) => {
|
||||
currentNotification.value = row
|
||||
detailDialogVisible.value = true
|
||||
|
||||
// 如果是未读通知,自动标记为已读
|
||||
if (!row.isRead) {
|
||||
nextTick(() => {
|
||||
markAsRead(row, false)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭详情弹窗
|
||||
const closeDetailDialog = () => {
|
||||
detailDialogVisible.value = false
|
||||
currentNotification.value = null
|
||||
}
|
||||
|
||||
// 标记单个通知为已读
|
||||
const markAsRead = async (row, showMessage = true) => {
|
||||
try {
|
||||
const res = await markNotificationAsRead({
|
||||
notificationIds: [row.notificationId]
|
||||
})
|
||||
|
||||
if (res.code === 0) {
|
||||
row.isRead = true
|
||||
row.readTime = new Date()
|
||||
|
||||
if (showMessage) {
|
||||
ElMessage.success('标记已读成功')
|
||||
}
|
||||
|
||||
// 更新统计信息
|
||||
getStats()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('标记已读失败:', error)
|
||||
ElMessage.error('标记已读失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 标记已读并关闭弹窗
|
||||
const markAsReadAndClose = async () => {
|
||||
if (currentNotification.value) {
|
||||
await markAsRead(currentNotification.value)
|
||||
closeDetailDialog()
|
||||
}
|
||||
}
|
||||
|
||||
// 批量标记已读
|
||||
const batchMarkAsRead = async () => {
|
||||
if (selectedNotifications.value.length === 0) {
|
||||
ElMessage.warning('请选择要标记的通知')
|
||||
return
|
||||
}
|
||||
|
||||
const unreadNotifications = selectedNotifications.value.filter(
|
||||
(item) => !item.isRead
|
||||
)
|
||||
if (unreadNotifications.length === 0) {
|
||||
ElMessage.warning('所选通知都已是已读状态')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const notificationIds = unreadNotifications.map(
|
||||
(item) => item.notificationId
|
||||
)
|
||||
const res = await markNotificationAsRead({ notificationIds })
|
||||
|
||||
if (res.code === 0) {
|
||||
// 更新本地数据
|
||||
unreadNotifications.forEach((item) => {
|
||||
item.isRead = true
|
||||
item.readTime = new Date()
|
||||
})
|
||||
|
||||
ElMessage.success(`成功标记 ${unreadNotifications.length} 条通知为已读`)
|
||||
getStats()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('批量标记已读失败:', error)
|
||||
ElMessage.error('批量标记已读失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 删除单个通知
|
||||
const deleteNotificationHandler = async (row) => {
|
||||
ElMessageBox.confirm(
|
||||
'确定要删除这条通知吗?如果此通知只针对您一人,删除后将无法恢复。',
|
||||
'删除确认',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
).then(async () => {
|
||||
try {
|
||||
const res = await deleteNotificationApi(row.notificationId)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('删除成功')
|
||||
getTableData()
|
||||
getStats()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除通知失败:', error)
|
||||
ElMessage.error('删除通知失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 删除并关闭弹窗
|
||||
const deleteAndClose = async () => {
|
||||
if (currentNotification.value) {
|
||||
await deleteNotificationHandler(currentNotification.value)
|
||||
closeDetailDialog()
|
||||
}
|
||||
}
|
||||
|
||||
// 批量删除通知
|
||||
const batchDeleteNotifications = async () => {
|
||||
if (selectedNotifications.value.length === 0) {
|
||||
ElMessage.warning('请选择要删除的通知')
|
||||
return
|
||||
}
|
||||
|
||||
ElMessageBox.confirm(
|
||||
`确定要删除选中的 ${selectedNotifications.value.length} 条通知吗?如果通知只针对您一人,删除后将无法恢复。`,
|
||||
'批量删除确认',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
).then(async () => {
|
||||
try {
|
||||
const deletePromises = selectedNotifications.value.map((item) =>
|
||||
deleteNotificationApi(item.notificationId)
|
||||
)
|
||||
|
||||
await Promise.all(deletePromises)
|
||||
ElMessage.success(
|
||||
`成功删除 ${selectedNotifications.value.length} 条通知`
|
||||
)
|
||||
getTableData()
|
||||
getStats()
|
||||
} catch (error) {
|
||||
console.error('批量删除失败:', error)
|
||||
ElMessage.error('批量删除失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 工具函数
|
||||
const formatDate = (date) => {
|
||||
if (!date) return '-'
|
||||
return formatTimeToStr(date, 'yyyy-mm-dd hh:MM:ss')
|
||||
}
|
||||
|
||||
const getTypeLabel = (type) => {
|
||||
const typeMap = {
|
||||
system: '系统通知',
|
||||
business: '业务通知',
|
||||
warning: '警告通知'
|
||||
}
|
||||
return typeMap[type] || type
|
||||
}
|
||||
|
||||
const getTypeTagType = (type) => {
|
||||
const typeMap = {
|
||||
system: 'info',
|
||||
business: 'success',
|
||||
warning: 'warning'
|
||||
}
|
||||
return typeMap[type] || 'info'
|
||||
}
|
||||
|
||||
const getPriorityLabel = (priority) => {
|
||||
const priorityMap = {
|
||||
low: '低',
|
||||
normal: '普通',
|
||||
high: '高',
|
||||
urgent: '紧急'
|
||||
}
|
||||
return priorityMap[priority] || priority
|
||||
}
|
||||
|
||||
const getPriorityTagType = (priority) => {
|
||||
const priorityMap = {
|
||||
low: 'info',
|
||||
normal: 'success',
|
||||
high: 'warning',
|
||||
urgent: 'danger'
|
||||
}
|
||||
return priorityMap[priority] || 'info'
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
getTableData()
|
||||
getStats()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.user-notification-center {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.notification-stats {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.stats-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.stats-card.unread {
|
||||
border-color: #f56c6c;
|
||||
}
|
||||
|
||||
.stats-card.read {
|
||||
border-color: #67c23a;
|
||||
}
|
||||
|
||||
.stats-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.stats-number {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #409eff;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stats-card.unread .stats-number {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.stats-card.read .stats-number {
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
.stats-label {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.notification-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.notification-title.unread {
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.unread-dot {
|
||||
color: #f56c6c;
|
||||
margin-right: 8px;
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.notification-detail {
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.detail-header h3 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 18px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.detail-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.detail-time {
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.content-text {
|
||||
color: #606266;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.gva-pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.ml-2 {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.ml-4 {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -3,13 +3,39 @@
|
||||
<warning-bar
|
||||
title="获取字典且缓存方法已在前端utils/dictionary 已经封装完成 不必自己书写 使用方法查看文件内注释"
|
||||
/>
|
||||
<div class="flex gap-4 p-2">
|
||||
<div class="flex gap-4">
|
||||
<div
|
||||
class="flex-none w-52 bg-white text-slate-700 dark:text-slate-400 dark:bg-slate-900 rounded p-4"
|
||||
class="flex-none w-64 bg-white text-slate-700 dark:text-slate-400 dark:bg-slate-900 rounded p-4"
|
||||
>
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex justify-between items-center relative">
|
||||
<span class="text font-bold">字典列表</span>
|
||||
<el-button type="primary" @click="openDrawer"> 新增 </el-button>
|
||||
<el-input
|
||||
class="!absolute top-0 left-0 z-2 ease-in-out animate-slide-left"
|
||||
placeholder="搜索"
|
||||
v-if="showSearchInput"
|
||||
v-model="searchName"
|
||||
clearable
|
||||
:autofocus="showSearchInput"
|
||||
@clear="clearSearchInput"
|
||||
:prefix-icon="Search"
|
||||
v-click-outside="handleCloseSearchInput"
|
||||
@keydown="handleInputKeyDown"
|
||||
>
|
||||
<template #append>
|
||||
<el-button
|
||||
:type="searchName ? 'primary' : 'info'"
|
||||
@click="getTableData"
|
||||
>搜索</el-button
|
||||
>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
class="ml-auto"
|
||||
:icon="Search"
|
||||
@click="showSearchInputHandler"
|
||||
></el-button>
|
||||
<el-button type="primary" @click="openDrawer" :icon="Plus">
|
||||
</el-button>
|
||||
</div>
|
||||
<el-scrollbar class="mt-4" style="height: calc(100vh - 300px)">
|
||||
<div
|
||||
@@ -23,7 +49,11 @@
|
||||
"
|
||||
@click="toDetail(dictionary)"
|
||||
>
|
||||
<span class="max-w-[160px] truncate">{{ dictionary.name }}</span>
|
||||
<div class="max-w-[160px] truncate">
|
||||
{{ dictionary.name }}
|
||||
<span class="mr-auto text-sm">({{ dictionary.type }})</span>
|
||||
</div>
|
||||
|
||||
<div class="min-w-[40px]">
|
||||
<el-icon
|
||||
class="text-blue-500"
|
||||
@@ -119,8 +149,8 @@
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
import sysDictionaryDetail from './sysDictionaryDetail.vue'
|
||||
import { Edit } from '@element-plus/icons-vue'
|
||||
import { useAppStore } from "@/pinia";
|
||||
import { Edit, Plus, Search } from '@element-plus/icons-vue'
|
||||
import { useAppStore } from '@/pinia'
|
||||
|
||||
defineOptions({
|
||||
name: 'SysDictionary'
|
||||
@@ -136,6 +166,8 @@
|
||||
status: true,
|
||||
desc: null
|
||||
})
|
||||
const searchName = ref('')
|
||||
const showSearchInput = ref(false)
|
||||
const rules = ref({
|
||||
name: [
|
||||
{
|
||||
@@ -164,7 +196,9 @@
|
||||
|
||||
// 查询
|
||||
const getTableData = async () => {
|
||||
const res = await getSysDictionaryList()
|
||||
const res = await getSysDictionaryList({
|
||||
name: searchName.value.trim()
|
||||
})
|
||||
if (res.code === 0) {
|
||||
dictionaryData.value = res.data
|
||||
selectID.value = res.data[0].ID
|
||||
@@ -241,6 +275,27 @@
|
||||
drawerForm.value && drawerForm.value.clearValidate()
|
||||
drawerFormVisible.value = true
|
||||
}
|
||||
|
||||
const clearSearchInput = () => {
|
||||
if (!showSearchInput.value) return
|
||||
searchName.value = ''
|
||||
showSearchInput.value = false
|
||||
getTableData()
|
||||
}
|
||||
const handleCloseSearchInput = () => {
|
||||
if (!showSearchInput.value || searchName.value.trim() != '') return
|
||||
showSearchInput.value = false
|
||||
}
|
||||
|
||||
const showSearchInputHandler = () => {
|
||||
showSearchInput.value = true
|
||||
}
|
||||
|
||||
const handleInputKeyDown = (e) => {
|
||||
if (e.key === 'Enter' && searchName.value.trim() !== '') {
|
||||
getTableData()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -1,8 +1,26 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="gva-table-box">
|
||||
<div class="gva-btn-list justify-between">
|
||||
<div class="gva-btn-list justify-between flex items-center">
|
||||
<span class="text font-bold">字典详细内容</span>
|
||||
<el-input
|
||||
placeholder="搜索展示值"
|
||||
v-model="searchName"
|
||||
clearable
|
||||
class="!w-64 ml-auto"
|
||||
@clear="clearSearchInput"
|
||||
:prefix-icon="Search"
|
||||
v-click-outside="handleCloseSearchInput"
|
||||
@keydown="handleInputKeyDown"
|
||||
>
|
||||
<template #append>
|
||||
<el-button
|
||||
:type="searchName ? 'primary' : 'info'"
|
||||
@click="getTableData"
|
||||
>搜索</el-button
|
||||
>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button type="primary" icon="plus" @click="openDrawer">
|
||||
新增字典项
|
||||
</el-button>
|
||||
@@ -45,7 +63,11 @@
|
||||
width="120"
|
||||
/>
|
||||
|
||||
<el-table-column align="left" label="操作" :min-width="appStore.operateMinWith">
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="操作"
|
||||
:min-width="appStore.operateMinWith"
|
||||
>
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
type="primary"
|
||||
@@ -156,14 +178,14 @@
|
||||
import { ref, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { formatBoolean, formatDate } from '@/utils/format'
|
||||
import { useAppStore } from "@/pinia";
|
||||
import { useAppStore } from '@/pinia'
|
||||
|
||||
defineOptions({
|
||||
name: 'SysDictionaryDetail'
|
||||
})
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const searchName = ref('')
|
||||
const props = defineProps({
|
||||
sysDictionaryID: {
|
||||
type: Number,
|
||||
@@ -223,7 +245,8 @@
|
||||
const table = await getSysDictionaryDetailList({
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
sysDictionaryID: props.sysDictionaryID
|
||||
sysDictionaryID: props.sysDictionaryID,
|
||||
label: searchName.value.trim()
|
||||
})
|
||||
if (table.code === 0) {
|
||||
tableData.value = table.data.list
|
||||
|
||||
@@ -0,0 +1,517 @@
|
||||
<template>
|
||||
<div class="user-notification-center">
|
||||
<!-- 页面标题 -->
|
||||
<div class="gva-table-box">
|
||||
<div class="gva-btn-list">
|
||||
<el-button type="primary" icon="Refresh" @click="getTableData">刷新</el-button>
|
||||
<el-button
|
||||
type="success"
|
||||
icon="Check"
|
||||
:disabled="!multipleSelection.length"
|
||||
@click="batchMarkAsRead"
|
||||
>
|
||||
批量标记已读
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
icon="Delete"
|
||||
:disabled="!multipleSelection.length"
|
||||
@click="batchDelete"
|
||||
>
|
||||
批量删除
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 搜索区域 -->
|
||||
<div class="gva-search-box">
|
||||
<el-form :inline="true" :model="searchInfo" class="demo-form-inline" @keyup.enter="onSubmit">
|
||||
<el-form-item label="通知类型">
|
||||
<el-select v-model="searchInfo.type" placeholder="请选择通知类型" clearable>
|
||||
<el-option label="公告" value="announcement" />
|
||||
<el-option label="通知" value="notification" />
|
||||
<el-option label="警告" value="warning" />
|
||||
<el-option label="违规" value="violation" />
|
||||
<el-option label="信息" value="info" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="阅读状态">
|
||||
<el-select v-model="searchInfo.isRead" placeholder="请选择阅读状态" clearable>
|
||||
<el-option label="未读" :value="false" />
|
||||
<el-option label="已读" :value="true" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="优先级">
|
||||
<el-select v-model="searchInfo.priority" placeholder="请选择优先级" clearable>
|
||||
<el-option label="低" value="low" />
|
||||
<el-option label="中" value="medium" />
|
||||
<el-option label="高" value="high" />
|
||||
<el-option label="紧急" value="urgent" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="search" @click="onSubmit">查询</el-button>
|
||||
<el-button icon="refresh" @click="onReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<div class="notification-stats">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6">
|
||||
<el-card class="stats-card">
|
||||
<div class="stats-item">
|
||||
<div class="stats-number">{{ stats.totalCount }}</div>
|
||||
<div class="stats-label">总通知数</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="stats-card unread">
|
||||
<div class="stats-item">
|
||||
<div class="stats-number">{{ stats.unreadCount }}</div>
|
||||
<div class="stats-label">未读通知</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="stats-card read">
|
||||
<div class="stats-item">
|
||||
<div class="stats-number">{{ stats.readCount }}</div>
|
||||
<div class="stats-label">已读通知</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card class="stats-card">
|
||||
<div class="stats-item">
|
||||
<div class="stats-number">{{ unreadPercentage }}%</div>
|
||||
<div class="stats-label">未读率</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 通知列表 -->
|
||||
<el-table
|
||||
ref="multipleTable"
|
||||
style="width: 100%"
|
||||
tooltip-effect="dark"
|
||||
:data="tableData"
|
||||
row-key="ID"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-click="handleRowClick"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column align="left" label="日期" prop="createdAt" width="180">
|
||||
<template #default="scope">
|
||||
<span>{{ formatDate(scope.row.createdAt) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="标题" prop="title" show-overflow-tooltip />
|
||||
<el-table-column align="left" label="类型" prop="type" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getTypeTagType(scope.row.type)">{{ getTypeLabel(scope.row.type) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="优先级" prop="priority" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getPriorityTagType(scope.row.priority)">{{ getPriorityLabel(scope.row.priority) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="阅读状态" prop="isRead" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.isRead ? 'success' : 'warning'">
|
||||
{{ scope.row.isRead ? '已读' : '未读' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="阅读时间" prop="readTime" width="180">
|
||||
<template #default="scope">
|
||||
<span>{{ scope.row.readTime ? formatDate(scope.row.readTime) : '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="操作" fixed="right" width="240">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" link icon="view" size="small" @click="viewNotification(scope.row)">查看</el-button>
|
||||
<el-button
|
||||
v-if="!scope.row.isRead"
|
||||
type="success"
|
||||
link
|
||||
icon="check"
|
||||
size="small"
|
||||
@click="markAsRead(scope.row)"
|
||||
>
|
||||
标记已读
|
||||
</el-button>
|
||||
<el-button type="danger" link icon="delete" size="small" @click="deleteNotificationHandler(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="gva-pagination">
|
||||
<el-pagination
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:current-page="page"
|
||||
:page-size="pageSize"
|
||||
:page-sizes="[10, 30, 50, 100]"
|
||||
:total="total"
|
||||
@current-change="handleCurrentChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 通知详情弹窗 -->
|
||||
<el-dialog v-model="detailVisible" title="通知详情" width="60%" :before-close="closeDetail">
|
||||
<div v-if="currentNotification" class="notification-detail">
|
||||
<div class="detail-header">
|
||||
<h3>{{ currentNotification.title }}</h3>
|
||||
<div class="detail-meta">
|
||||
<el-tag :type="getTypeTagType(currentNotification.type)">{{ getTypeLabel(currentNotification.type) }}</el-tag>
|
||||
<el-tag :type="getPriorityTagType(currentNotification.priority)">{{ getPriorityLabel(currentNotification.priority) }}</el-tag>
|
||||
<el-tag :type="currentNotification.isRead ? 'success' : 'warning'">
|
||||
{{ currentNotification.isRead ? '已读' : '未读' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-content">
|
||||
<div v-html="currentNotification.content"></div>
|
||||
</div>
|
||||
<div class="detail-footer">
|
||||
<p><strong>发送时间:</strong>{{ formatDate(currentNotification.createdAt) }}</p>
|
||||
<p v-if="currentNotification.readTime"><strong>阅读时间:</strong>{{ formatDate(currentNotification.readTime) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button v-if="!currentNotification?.isRead" type="success" @click="markAsReadInDetail">标记已读</el-button>
|
||||
<el-button type="danger" @click="deleteInDetail">删除</el-button>
|
||||
<el-button @click="closeDetail">关闭</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { formatDate } from '@/utils/format'
|
||||
import { getUserNotificationList, markNotificationAsRead, deleteUserNotification } from '@/api/notice'
|
||||
|
||||
defineOptions({
|
||||
name: 'UserNotificationCenter'
|
||||
})
|
||||
|
||||
// 响应式数据
|
||||
const page = ref(1)
|
||||
const total = ref(0)
|
||||
const pageSize = ref(10)
|
||||
const tableData = ref([])
|
||||
const multipleSelection = ref([])
|
||||
const detailVisible = ref(false)
|
||||
const currentNotification = ref(null)
|
||||
|
||||
// 搜索条件
|
||||
const searchInfo = reactive({
|
||||
type: '',
|
||||
isRead: null,
|
||||
priority: ''
|
||||
})
|
||||
|
||||
// 统计信息
|
||||
const stats = reactive({
|
||||
totalCount: 0,
|
||||
unreadCount: 0,
|
||||
readCount: 0
|
||||
})
|
||||
|
||||
// 计算未读率
|
||||
const unreadPercentage = computed(() => {
|
||||
if (stats.totalCount === 0) return 0
|
||||
return Math.round((stats.unreadCount / stats.totalCount) * 100)
|
||||
})
|
||||
|
||||
// 获取表格数据
|
||||
const getTableData = async () => {
|
||||
const table = await getUserNotificationList({
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
...searchInfo
|
||||
})
|
||||
if (table.code === 0) {
|
||||
tableData.value = table.data.list
|
||||
total.value = table.data.total
|
||||
page.value = table.data.page
|
||||
pageSize.value = table.data.pageSize
|
||||
|
||||
// 更新统计信息
|
||||
stats.totalCount = table.data.total
|
||||
stats.unreadCount = table.data.list.filter(item => !item.isRead).length
|
||||
stats.readCount = table.data.list.filter(item => item.isRead).length
|
||||
}
|
||||
}
|
||||
|
||||
// 分页相关
|
||||
const handleSizeChange = (val) => {
|
||||
pageSize.value = val
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (val) => {
|
||||
page.value = val
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 多选相关
|
||||
const handleSelectionChange = (val) => {
|
||||
multipleSelection.value = val
|
||||
}
|
||||
|
||||
// 搜索相关
|
||||
const onSubmit = () => {
|
||||
page.value = 1
|
||||
pageSize.value = 10
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const onReset = () => {
|
||||
searchInfo.type = ''
|
||||
searchInfo.isRead = null
|
||||
searchInfo.priority = ''
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 查看通知详情
|
||||
const viewNotification = (row) => {
|
||||
currentNotification.value = row
|
||||
detailVisible.value = true
|
||||
|
||||
// 如果是未读通知,自动标记为已读
|
||||
if (!row.isRead) {
|
||||
markAsRead(row, false)
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭详情弹窗
|
||||
const closeDetail = () => {
|
||||
detailVisible.value = false
|
||||
currentNotification.value = null
|
||||
}
|
||||
|
||||
// 行点击事件
|
||||
const handleRowClick = (row) => {
|
||||
viewNotification(row)
|
||||
}
|
||||
|
||||
// 标记已读
|
||||
const markAsRead = async (row, showMessage = true) => {
|
||||
const res = await markNotificationAsRead({
|
||||
notificationIds: [row.notificationId]
|
||||
})
|
||||
if (res.code === 0) {
|
||||
row.isRead = true
|
||||
row.readTime = new Date().toISOString()
|
||||
if (showMessage) {
|
||||
ElMessage.success('标记已读成功')
|
||||
}
|
||||
getTableData()
|
||||
}
|
||||
}
|
||||
|
||||
// 在详情中标记已读
|
||||
const markAsReadInDetail = async () => {
|
||||
await markAsRead(currentNotification.value)
|
||||
currentNotification.value.isRead = true
|
||||
currentNotification.value.readTime = new Date().toISOString()
|
||||
}
|
||||
|
||||
// 批量标记已读
|
||||
const batchMarkAsRead = async () => {
|
||||
const notificationIds = multipleSelection.value.map(item => item.notificationId)
|
||||
const res = await markNotificationAsRead({
|
||||
notificationIds
|
||||
})
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('批量标记已读成功')
|
||||
getTableData()
|
||||
}
|
||||
}
|
||||
|
||||
// 删除通知
|
||||
const deleteNotificationHandler = async (row) => {
|
||||
ElMessageBox.confirm('此操作将永久删除该通知, 是否继续?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
const res = await deleteUserNotification(row.notificationId)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('删除成功')
|
||||
getTableData()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 在详情中删除
|
||||
const deleteInDetail = async () => {
|
||||
ElMessageBox.confirm('此操作将永久删除该通知, 是否继续?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
const res = await deleteUserNotification(currentNotification.value.notificationId)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('删除成功')
|
||||
closeDetail()
|
||||
getTableData()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
const batchDelete = async () => {
|
||||
ElMessageBox.confirm('此操作将永久删除选中的通知, 是否继续?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
const promises = multipleSelection.value.map(item => deleteUserNotification(item.notificationId))
|
||||
await Promise.all(promises)
|
||||
ElMessage.success('批量删除成功')
|
||||
getTableData()
|
||||
})
|
||||
}
|
||||
|
||||
// 获取类型标签样式
|
||||
const getTypeTagType = (type) => {
|
||||
const typeMap = {
|
||||
announcement: '',
|
||||
notification: 'success',
|
||||
warning: 'warning',
|
||||
violation: 'danger',
|
||||
info: 'info'
|
||||
}
|
||||
return typeMap[type] || ''
|
||||
}
|
||||
|
||||
// 获取类型标签文本
|
||||
const getTypeLabel = (type) => {
|
||||
const typeMap = {
|
||||
announcement: '公告',
|
||||
notification: '通知',
|
||||
warning: '警告',
|
||||
violation: '违规',
|
||||
info: '信息'
|
||||
}
|
||||
return typeMap[type] || type
|
||||
}
|
||||
|
||||
// 获取优先级标签样式
|
||||
const getPriorityTagType = (priority) => {
|
||||
const priorityMap = {
|
||||
low: 'info',
|
||||
medium: '',
|
||||
high: 'warning',
|
||||
urgent: 'danger'
|
||||
}
|
||||
return priorityMap[priority] || ''
|
||||
}
|
||||
|
||||
// 获取优先级标签文本
|
||||
const getPriorityLabel = (priority) => {
|
||||
const priorityMap = {
|
||||
low: '低',
|
||||
medium: '中',
|
||||
high: '高',
|
||||
urgent: '紧急'
|
||||
}
|
||||
return priorityMap[priority] || priority
|
||||
}
|
||||
|
||||
// 组件挂载时获取数据
|
||||
onMounted(() => {
|
||||
getTableData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.user-notification-center {
|
||||
.notification-stats {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.stats-card {
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
&.unread {
|
||||
border-left: 4px solid #f56c6c;
|
||||
}
|
||||
|
||||
&.read {
|
||||
border-left: 4px solid #67c23a;
|
||||
}
|
||||
|
||||
.stats-item {
|
||||
.stats-number {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stats-label {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notification-detail {
|
||||
.detail-header {
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
padding-bottom: 16px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
h3 {
|
||||
margin: 0 0 12px 0;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.detail-meta {
|
||||
.el-tag {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
line-height: 1.6;
|
||||
color: #606266;
|
||||
margin-bottom: 20px;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.detail-footer {
|
||||
border-top: 1px solid #ebeef5;
|
||||
padding-top: 16px;
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
|
||||
p {
|
||||
margin: 8px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user