Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
7079edc2
Commit
7079edc2
authored
Mar 07, 2026
by
shaw
Browse files
feat: announcement支持强制弹窗通知
parent
a42a1f08
Changes
25
Hide whitespace changes
Inline
Side-by-side
backend/ent/announcement.go
View file @
7079edc2
...
@@ -25,6 +25,8 @@ type Announcement struct {
...
@@ -25,6 +25,8 @@ type Announcement struct {
Content
string
`json:"content,omitempty"`
Content
string
`json:"content,omitempty"`
// 状态: draft, active, archived
// 状态: draft, active, archived
Status
string
`json:"status,omitempty"`
Status
string
`json:"status,omitempty"`
// 通知模式: silent(仅铃铛), popup(弹窗提醒)
NotifyMode
string
`json:"notify_mode,omitempty"`
// 展示条件(JSON 规则)
// 展示条件(JSON 规则)
Targeting
domain
.
AnnouncementTargeting
`json:"targeting,omitempty"`
Targeting
domain
.
AnnouncementTargeting
`json:"targeting,omitempty"`
// 开始展示时间(为空表示立即生效)
// 开始展示时间(为空表示立即生效)
...
@@ -72,7 +74,7 @@ func (*Announcement) scanValues(columns []string) ([]any, error) {
...
@@ -72,7 +74,7 @@ func (*Announcement) scanValues(columns []string) ([]any, error) {
values
[
i
]
=
new
([]
byte
)
values
[
i
]
=
new
([]
byte
)
case
announcement
.
FieldID
,
announcement
.
FieldCreatedBy
,
announcement
.
FieldUpdatedBy
:
case
announcement
.
FieldID
,
announcement
.
FieldCreatedBy
,
announcement
.
FieldUpdatedBy
:
values
[
i
]
=
new
(
sql
.
NullInt64
)
values
[
i
]
=
new
(
sql
.
NullInt64
)
case
announcement
.
FieldTitle
,
announcement
.
FieldContent
,
announcement
.
FieldStatus
:
case
announcement
.
FieldTitle
,
announcement
.
FieldContent
,
announcement
.
FieldStatus
,
announcement
.
FieldNotifyMode
:
values
[
i
]
=
new
(
sql
.
NullString
)
values
[
i
]
=
new
(
sql
.
NullString
)
case
announcement
.
FieldStartsAt
,
announcement
.
FieldEndsAt
,
announcement
.
FieldCreatedAt
,
announcement
.
FieldUpdatedAt
:
case
announcement
.
FieldStartsAt
,
announcement
.
FieldEndsAt
,
announcement
.
FieldCreatedAt
,
announcement
.
FieldUpdatedAt
:
values
[
i
]
=
new
(
sql
.
NullTime
)
values
[
i
]
=
new
(
sql
.
NullTime
)
...
@@ -115,6 +117,12 @@ func (_m *Announcement) assignValues(columns []string, values []any) error {
...
@@ -115,6 +117,12 @@ func (_m *Announcement) assignValues(columns []string, values []any) error {
}
else
if
value
.
Valid
{
}
else
if
value
.
Valid
{
_m
.
Status
=
value
.
String
_m
.
Status
=
value
.
String
}
}
case
announcement
.
FieldNotifyMode
:
if
value
,
ok
:=
values
[
i
]
.
(
*
sql
.
NullString
);
!
ok
{
return
fmt
.
Errorf
(
"unexpected type %T for field notify_mode"
,
values
[
i
])
}
else
if
value
.
Valid
{
_m
.
NotifyMode
=
value
.
String
}
case
announcement
.
FieldTargeting
:
case
announcement
.
FieldTargeting
:
if
value
,
ok
:=
values
[
i
]
.
(
*
[]
byte
);
!
ok
{
if
value
,
ok
:=
values
[
i
]
.
(
*
[]
byte
);
!
ok
{
return
fmt
.
Errorf
(
"unexpected type %T for field targeting"
,
values
[
i
])
return
fmt
.
Errorf
(
"unexpected type %T for field targeting"
,
values
[
i
])
...
@@ -213,6 +221,9 @@ func (_m *Announcement) String() string {
...
@@ -213,6 +221,9 @@ func (_m *Announcement) String() string {
builder
.
WriteString
(
"status="
)
builder
.
WriteString
(
"status="
)
builder
.
WriteString
(
_m
.
Status
)
builder
.
WriteString
(
_m
.
Status
)
builder
.
WriteString
(
", "
)
builder
.
WriteString
(
", "
)
builder
.
WriteString
(
"notify_mode="
)
builder
.
WriteString
(
_m
.
NotifyMode
)
builder
.
WriteString
(
", "
)
builder
.
WriteString
(
"targeting="
)
builder
.
WriteString
(
"targeting="
)
builder
.
WriteString
(
fmt
.
Sprintf
(
"%v"
,
_m
.
Targeting
))
builder
.
WriteString
(
fmt
.
Sprintf
(
"%v"
,
_m
.
Targeting
))
builder
.
WriteString
(
", "
)
builder
.
WriteString
(
", "
)
...
...
backend/ent/announcement/announcement.go
View file @
7079edc2
...
@@ -20,6 +20,8 @@ const (
...
@@ -20,6 +20,8 @@ const (
FieldContent
=
"content"
FieldContent
=
"content"
// FieldStatus holds the string denoting the status field in the database.
// FieldStatus holds the string denoting the status field in the database.
FieldStatus
=
"status"
FieldStatus
=
"status"
// FieldNotifyMode holds the string denoting the notify_mode field in the database.
FieldNotifyMode
=
"notify_mode"
// FieldTargeting holds the string denoting the targeting field in the database.
// FieldTargeting holds the string denoting the targeting field in the database.
FieldTargeting
=
"targeting"
FieldTargeting
=
"targeting"
// FieldStartsAt holds the string denoting the starts_at field in the database.
// FieldStartsAt holds the string denoting the starts_at field in the database.
...
@@ -53,6 +55,7 @@ var Columns = []string{
...
@@ -53,6 +55,7 @@ var Columns = []string{
FieldTitle
,
FieldTitle
,
FieldContent
,
FieldContent
,
FieldStatus
,
FieldStatus
,
FieldNotifyMode
,
FieldTargeting
,
FieldTargeting
,
FieldStartsAt
,
FieldStartsAt
,
FieldEndsAt
,
FieldEndsAt
,
...
@@ -81,6 +84,10 @@ var (
...
@@ -81,6 +84,10 @@ var (
DefaultStatus
string
DefaultStatus
string
// StatusValidator is a validator for the "status" field. It is called by the builders before save.
// StatusValidator is a validator for the "status" field. It is called by the builders before save.
StatusValidator
func
(
string
)
error
StatusValidator
func
(
string
)
error
// DefaultNotifyMode holds the default value on creation for the "notify_mode" field.
DefaultNotifyMode
string
// NotifyModeValidator is a validator for the "notify_mode" field. It is called by the builders before save.
NotifyModeValidator
func
(
string
)
error
// DefaultCreatedAt holds the default value on creation for the "created_at" field.
// DefaultCreatedAt holds the default value on creation for the "created_at" field.
DefaultCreatedAt
func
()
time
.
Time
DefaultCreatedAt
func
()
time
.
Time
// DefaultUpdatedAt holds the default value on creation for the "updated_at" field.
// DefaultUpdatedAt holds the default value on creation for the "updated_at" field.
...
@@ -112,6 +119,11 @@ func ByStatus(opts ...sql.OrderTermOption) OrderOption {
...
@@ -112,6 +119,11 @@ func ByStatus(opts ...sql.OrderTermOption) OrderOption {
return
sql
.
OrderByField
(
FieldStatus
,
opts
...
)
.
ToFunc
()
return
sql
.
OrderByField
(
FieldStatus
,
opts
...
)
.
ToFunc
()
}
}
// ByNotifyMode orders the results by the notify_mode field.
func
ByNotifyMode
(
opts
...
sql
.
OrderTermOption
)
OrderOption
{
return
sql
.
OrderByField
(
FieldNotifyMode
,
opts
...
)
.
ToFunc
()
}
// ByStartsAt orders the results by the starts_at field.
// ByStartsAt orders the results by the starts_at field.
func
ByStartsAt
(
opts
...
sql
.
OrderTermOption
)
OrderOption
{
func
ByStartsAt
(
opts
...
sql
.
OrderTermOption
)
OrderOption
{
return
sql
.
OrderByField
(
FieldStartsAt
,
opts
...
)
.
ToFunc
()
return
sql
.
OrderByField
(
FieldStartsAt
,
opts
...
)
.
ToFunc
()
...
...
backend/ent/announcement/where.go
View file @
7079edc2
...
@@ -70,6 +70,11 @@ func Status(v string) predicate.Announcement {
...
@@ -70,6 +70,11 @@ func Status(v string) predicate.Announcement {
return
predicate
.
Announcement
(
sql
.
FieldEQ
(
FieldStatus
,
v
))
return
predicate
.
Announcement
(
sql
.
FieldEQ
(
FieldStatus
,
v
))
}
}
// NotifyMode applies equality check predicate on the "notify_mode" field. It's identical to NotifyModeEQ.
func
NotifyMode
(
v
string
)
predicate
.
Announcement
{
return
predicate
.
Announcement
(
sql
.
FieldEQ
(
FieldNotifyMode
,
v
))
}
// StartsAt applies equality check predicate on the "starts_at" field. It's identical to StartsAtEQ.
// StartsAt applies equality check predicate on the "starts_at" field. It's identical to StartsAtEQ.
func
StartsAt
(
v
time
.
Time
)
predicate
.
Announcement
{
func
StartsAt
(
v
time
.
Time
)
predicate
.
Announcement
{
return
predicate
.
Announcement
(
sql
.
FieldEQ
(
FieldStartsAt
,
v
))
return
predicate
.
Announcement
(
sql
.
FieldEQ
(
FieldStartsAt
,
v
))
...
@@ -295,6 +300,71 @@ func StatusContainsFold(v string) predicate.Announcement {
...
@@ -295,6 +300,71 @@ func StatusContainsFold(v string) predicate.Announcement {
return
predicate
.
Announcement
(
sql
.
FieldContainsFold
(
FieldStatus
,
v
))
return
predicate
.
Announcement
(
sql
.
FieldContainsFold
(
FieldStatus
,
v
))
}
}
// NotifyModeEQ applies the EQ predicate on the "notify_mode" field.
func
NotifyModeEQ
(
v
string
)
predicate
.
Announcement
{
return
predicate
.
Announcement
(
sql
.
FieldEQ
(
FieldNotifyMode
,
v
))
}
// NotifyModeNEQ applies the NEQ predicate on the "notify_mode" field.
func
NotifyModeNEQ
(
v
string
)
predicate
.
Announcement
{
return
predicate
.
Announcement
(
sql
.
FieldNEQ
(
FieldNotifyMode
,
v
))
}
// NotifyModeIn applies the In predicate on the "notify_mode" field.
func
NotifyModeIn
(
vs
...
string
)
predicate
.
Announcement
{
return
predicate
.
Announcement
(
sql
.
FieldIn
(
FieldNotifyMode
,
vs
...
))
}
// NotifyModeNotIn applies the NotIn predicate on the "notify_mode" field.
func
NotifyModeNotIn
(
vs
...
string
)
predicate
.
Announcement
{
return
predicate
.
Announcement
(
sql
.
FieldNotIn
(
FieldNotifyMode
,
vs
...
))
}
// NotifyModeGT applies the GT predicate on the "notify_mode" field.
func
NotifyModeGT
(
v
string
)
predicate
.
Announcement
{
return
predicate
.
Announcement
(
sql
.
FieldGT
(
FieldNotifyMode
,
v
))
}
// NotifyModeGTE applies the GTE predicate on the "notify_mode" field.
func
NotifyModeGTE
(
v
string
)
predicate
.
Announcement
{
return
predicate
.
Announcement
(
sql
.
FieldGTE
(
FieldNotifyMode
,
v
))
}
// NotifyModeLT applies the LT predicate on the "notify_mode" field.
func
NotifyModeLT
(
v
string
)
predicate
.
Announcement
{
return
predicate
.
Announcement
(
sql
.
FieldLT
(
FieldNotifyMode
,
v
))
}
// NotifyModeLTE applies the LTE predicate on the "notify_mode" field.
func
NotifyModeLTE
(
v
string
)
predicate
.
Announcement
{
return
predicate
.
Announcement
(
sql
.
FieldLTE
(
FieldNotifyMode
,
v
))
}
// NotifyModeContains applies the Contains predicate on the "notify_mode" field.
func
NotifyModeContains
(
v
string
)
predicate
.
Announcement
{
return
predicate
.
Announcement
(
sql
.
FieldContains
(
FieldNotifyMode
,
v
))
}
// NotifyModeHasPrefix applies the HasPrefix predicate on the "notify_mode" field.
func
NotifyModeHasPrefix
(
v
string
)
predicate
.
Announcement
{
return
predicate
.
Announcement
(
sql
.
FieldHasPrefix
(
FieldNotifyMode
,
v
))
}
// NotifyModeHasSuffix applies the HasSuffix predicate on the "notify_mode" field.
func
NotifyModeHasSuffix
(
v
string
)
predicate
.
Announcement
{
return
predicate
.
Announcement
(
sql
.
FieldHasSuffix
(
FieldNotifyMode
,
v
))
}
// NotifyModeEqualFold applies the EqualFold predicate on the "notify_mode" field.
func
NotifyModeEqualFold
(
v
string
)
predicate
.
Announcement
{
return
predicate
.
Announcement
(
sql
.
FieldEqualFold
(
FieldNotifyMode
,
v
))
}
// NotifyModeContainsFold applies the ContainsFold predicate on the "notify_mode" field.
func
NotifyModeContainsFold
(
v
string
)
predicate
.
Announcement
{
return
predicate
.
Announcement
(
sql
.
FieldContainsFold
(
FieldNotifyMode
,
v
))
}
// TargetingIsNil applies the IsNil predicate on the "targeting" field.
// TargetingIsNil applies the IsNil predicate on the "targeting" field.
func
TargetingIsNil
()
predicate
.
Announcement
{
func
TargetingIsNil
()
predicate
.
Announcement
{
return
predicate
.
Announcement
(
sql
.
FieldIsNull
(
FieldTargeting
))
return
predicate
.
Announcement
(
sql
.
FieldIsNull
(
FieldTargeting
))
...
...
backend/ent/announcement_create.go
View file @
7079edc2
...
@@ -50,6 +50,20 @@ func (_c *AnnouncementCreate) SetNillableStatus(v *string) *AnnouncementCreate {
...
@@ -50,6 +50,20 @@ func (_c *AnnouncementCreate) SetNillableStatus(v *string) *AnnouncementCreate {
return
_c
return
_c
}
}
// SetNotifyMode sets the "notify_mode" field.
func
(
_c
*
AnnouncementCreate
)
SetNotifyMode
(
v
string
)
*
AnnouncementCreate
{
_c
.
mutation
.
SetNotifyMode
(
v
)
return
_c
}
// SetNillableNotifyMode sets the "notify_mode" field if the given value is not nil.
func
(
_c
*
AnnouncementCreate
)
SetNillableNotifyMode
(
v
*
string
)
*
AnnouncementCreate
{
if
v
!=
nil
{
_c
.
SetNotifyMode
(
*
v
)
}
return
_c
}
// SetTargeting sets the "targeting" field.
// SetTargeting sets the "targeting" field.
func
(
_c
*
AnnouncementCreate
)
SetTargeting
(
v
domain
.
AnnouncementTargeting
)
*
AnnouncementCreate
{
func
(
_c
*
AnnouncementCreate
)
SetTargeting
(
v
domain
.
AnnouncementTargeting
)
*
AnnouncementCreate
{
_c
.
mutation
.
SetTargeting
(
v
)
_c
.
mutation
.
SetTargeting
(
v
)
...
@@ -202,6 +216,10 @@ func (_c *AnnouncementCreate) defaults() {
...
@@ -202,6 +216,10 @@ func (_c *AnnouncementCreate) defaults() {
v
:=
announcement
.
DefaultStatus
v
:=
announcement
.
DefaultStatus
_c
.
mutation
.
SetStatus
(
v
)
_c
.
mutation
.
SetStatus
(
v
)
}
}
if
_
,
ok
:=
_c
.
mutation
.
NotifyMode
();
!
ok
{
v
:=
announcement
.
DefaultNotifyMode
_c
.
mutation
.
SetNotifyMode
(
v
)
}
if
_
,
ok
:=
_c
.
mutation
.
CreatedAt
();
!
ok
{
if
_
,
ok
:=
_c
.
mutation
.
CreatedAt
();
!
ok
{
v
:=
announcement
.
DefaultCreatedAt
()
v
:=
announcement
.
DefaultCreatedAt
()
_c
.
mutation
.
SetCreatedAt
(
v
)
_c
.
mutation
.
SetCreatedAt
(
v
)
...
@@ -238,6 +256,14 @@ func (_c *AnnouncementCreate) check() error {
...
@@ -238,6 +256,14 @@ func (_c *AnnouncementCreate) check() error {
return
&
ValidationError
{
Name
:
"status"
,
err
:
fmt
.
Errorf
(
`ent: validator failed for field "Announcement.status": %w`
,
err
)}
return
&
ValidationError
{
Name
:
"status"
,
err
:
fmt
.
Errorf
(
`ent: validator failed for field "Announcement.status": %w`
,
err
)}
}
}
}
}
if
_
,
ok
:=
_c
.
mutation
.
NotifyMode
();
!
ok
{
return
&
ValidationError
{
Name
:
"notify_mode"
,
err
:
errors
.
New
(
`ent: missing required field "Announcement.notify_mode"`
)}
}
if
v
,
ok
:=
_c
.
mutation
.
NotifyMode
();
ok
{
if
err
:=
announcement
.
NotifyModeValidator
(
v
);
err
!=
nil
{
return
&
ValidationError
{
Name
:
"notify_mode"
,
err
:
fmt
.
Errorf
(
`ent: validator failed for field "Announcement.notify_mode": %w`
,
err
)}
}
}
if
_
,
ok
:=
_c
.
mutation
.
CreatedAt
();
!
ok
{
if
_
,
ok
:=
_c
.
mutation
.
CreatedAt
();
!
ok
{
return
&
ValidationError
{
Name
:
"created_at"
,
err
:
errors
.
New
(
`ent: missing required field "Announcement.created_at"`
)}
return
&
ValidationError
{
Name
:
"created_at"
,
err
:
errors
.
New
(
`ent: missing required field "Announcement.created_at"`
)}
}
}
...
@@ -283,6 +309,10 @@ func (_c *AnnouncementCreate) createSpec() (*Announcement, *sqlgraph.CreateSpec)
...
@@ -283,6 +309,10 @@ func (_c *AnnouncementCreate) createSpec() (*Announcement, *sqlgraph.CreateSpec)
_spec
.
SetField
(
announcement
.
FieldStatus
,
field
.
TypeString
,
value
)
_spec
.
SetField
(
announcement
.
FieldStatus
,
field
.
TypeString
,
value
)
_node
.
Status
=
value
_node
.
Status
=
value
}
}
if
value
,
ok
:=
_c
.
mutation
.
NotifyMode
();
ok
{
_spec
.
SetField
(
announcement
.
FieldNotifyMode
,
field
.
TypeString
,
value
)
_node
.
NotifyMode
=
value
}
if
value
,
ok
:=
_c
.
mutation
.
Targeting
();
ok
{
if
value
,
ok
:=
_c
.
mutation
.
Targeting
();
ok
{
_spec
.
SetField
(
announcement
.
FieldTargeting
,
field
.
TypeJSON
,
value
)
_spec
.
SetField
(
announcement
.
FieldTargeting
,
field
.
TypeJSON
,
value
)
_node
.
Targeting
=
value
_node
.
Targeting
=
value
...
@@ -415,6 +445,18 @@ func (u *AnnouncementUpsert) UpdateStatus() *AnnouncementUpsert {
...
@@ -415,6 +445,18 @@ func (u *AnnouncementUpsert) UpdateStatus() *AnnouncementUpsert {
return
u
return
u
}
}
// SetNotifyMode sets the "notify_mode" field.
func
(
u
*
AnnouncementUpsert
)
SetNotifyMode
(
v
string
)
*
AnnouncementUpsert
{
u
.
Set
(
announcement
.
FieldNotifyMode
,
v
)
return
u
}
// UpdateNotifyMode sets the "notify_mode" field to the value that was provided on create.
func
(
u
*
AnnouncementUpsert
)
UpdateNotifyMode
()
*
AnnouncementUpsert
{
u
.
SetExcluded
(
announcement
.
FieldNotifyMode
)
return
u
}
// SetTargeting sets the "targeting" field.
// SetTargeting sets the "targeting" field.
func
(
u
*
AnnouncementUpsert
)
SetTargeting
(
v
domain
.
AnnouncementTargeting
)
*
AnnouncementUpsert
{
func
(
u
*
AnnouncementUpsert
)
SetTargeting
(
v
domain
.
AnnouncementTargeting
)
*
AnnouncementUpsert
{
u
.
Set
(
announcement
.
FieldTargeting
,
v
)
u
.
Set
(
announcement
.
FieldTargeting
,
v
)
...
@@ -616,6 +658,20 @@ func (u *AnnouncementUpsertOne) UpdateStatus() *AnnouncementUpsertOne {
...
@@ -616,6 +658,20 @@ func (u *AnnouncementUpsertOne) UpdateStatus() *AnnouncementUpsertOne {
})
})
}
}
// SetNotifyMode sets the "notify_mode" field.
func
(
u
*
AnnouncementUpsertOne
)
SetNotifyMode
(
v
string
)
*
AnnouncementUpsertOne
{
return
u
.
Update
(
func
(
s
*
AnnouncementUpsert
)
{
s
.
SetNotifyMode
(
v
)
})
}
// UpdateNotifyMode sets the "notify_mode" field to the value that was provided on create.
func
(
u
*
AnnouncementUpsertOne
)
UpdateNotifyMode
()
*
AnnouncementUpsertOne
{
return
u
.
Update
(
func
(
s
*
AnnouncementUpsert
)
{
s
.
UpdateNotifyMode
()
})
}
// SetTargeting sets the "targeting" field.
// SetTargeting sets the "targeting" field.
func
(
u
*
AnnouncementUpsertOne
)
SetTargeting
(
v
domain
.
AnnouncementTargeting
)
*
AnnouncementUpsertOne
{
func
(
u
*
AnnouncementUpsertOne
)
SetTargeting
(
v
domain
.
AnnouncementTargeting
)
*
AnnouncementUpsertOne
{
return
u
.
Update
(
func
(
s
*
AnnouncementUpsert
)
{
return
u
.
Update
(
func
(
s
*
AnnouncementUpsert
)
{
...
@@ -1002,6 +1058,20 @@ func (u *AnnouncementUpsertBulk) UpdateStatus() *AnnouncementUpsertBulk {
...
@@ -1002,6 +1058,20 @@ func (u *AnnouncementUpsertBulk) UpdateStatus() *AnnouncementUpsertBulk {
})
})
}
}
// SetNotifyMode sets the "notify_mode" field.
func
(
u
*
AnnouncementUpsertBulk
)
SetNotifyMode
(
v
string
)
*
AnnouncementUpsertBulk
{
return
u
.
Update
(
func
(
s
*
AnnouncementUpsert
)
{
s
.
SetNotifyMode
(
v
)
})
}
// UpdateNotifyMode sets the "notify_mode" field to the value that was provided on create.
func
(
u
*
AnnouncementUpsertBulk
)
UpdateNotifyMode
()
*
AnnouncementUpsertBulk
{
return
u
.
Update
(
func
(
s
*
AnnouncementUpsert
)
{
s
.
UpdateNotifyMode
()
})
}
// SetTargeting sets the "targeting" field.
// SetTargeting sets the "targeting" field.
func
(
u
*
AnnouncementUpsertBulk
)
SetTargeting
(
v
domain
.
AnnouncementTargeting
)
*
AnnouncementUpsertBulk
{
func
(
u
*
AnnouncementUpsertBulk
)
SetTargeting
(
v
domain
.
AnnouncementTargeting
)
*
AnnouncementUpsertBulk
{
return
u
.
Update
(
func
(
s
*
AnnouncementUpsert
)
{
return
u
.
Update
(
func
(
s
*
AnnouncementUpsert
)
{
...
...
backend/ent/announcement_update.go
View file @
7079edc2
...
@@ -72,6 +72,20 @@ func (_u *AnnouncementUpdate) SetNillableStatus(v *string) *AnnouncementUpdate {
...
@@ -72,6 +72,20 @@ func (_u *AnnouncementUpdate) SetNillableStatus(v *string) *AnnouncementUpdate {
return
_u
return
_u
}
}
// SetNotifyMode sets the "notify_mode" field.
func
(
_u
*
AnnouncementUpdate
)
SetNotifyMode
(
v
string
)
*
AnnouncementUpdate
{
_u
.
mutation
.
SetNotifyMode
(
v
)
return
_u
}
// SetNillableNotifyMode sets the "notify_mode" field if the given value is not nil.
func
(
_u
*
AnnouncementUpdate
)
SetNillableNotifyMode
(
v
*
string
)
*
AnnouncementUpdate
{
if
v
!=
nil
{
_u
.
SetNotifyMode
(
*
v
)
}
return
_u
}
// SetTargeting sets the "targeting" field.
// SetTargeting sets the "targeting" field.
func
(
_u
*
AnnouncementUpdate
)
SetTargeting
(
v
domain
.
AnnouncementTargeting
)
*
AnnouncementUpdate
{
func
(
_u
*
AnnouncementUpdate
)
SetTargeting
(
v
domain
.
AnnouncementTargeting
)
*
AnnouncementUpdate
{
_u
.
mutation
.
SetTargeting
(
v
)
_u
.
mutation
.
SetTargeting
(
v
)
...
@@ -286,6 +300,11 @@ func (_u *AnnouncementUpdate) check() error {
...
@@ -286,6 +300,11 @@ func (_u *AnnouncementUpdate) check() error {
return
&
ValidationError
{
Name
:
"status"
,
err
:
fmt
.
Errorf
(
`ent: validator failed for field "Announcement.status": %w`
,
err
)}
return
&
ValidationError
{
Name
:
"status"
,
err
:
fmt
.
Errorf
(
`ent: validator failed for field "Announcement.status": %w`
,
err
)}
}
}
}
}
if
v
,
ok
:=
_u
.
mutation
.
NotifyMode
();
ok
{
if
err
:=
announcement
.
NotifyModeValidator
(
v
);
err
!=
nil
{
return
&
ValidationError
{
Name
:
"notify_mode"
,
err
:
fmt
.
Errorf
(
`ent: validator failed for field "Announcement.notify_mode": %w`
,
err
)}
}
}
return
nil
return
nil
}
}
...
@@ -310,6 +329,9 @@ func (_u *AnnouncementUpdate) sqlSave(ctx context.Context) (_node int, err error
...
@@ -310,6 +329,9 @@ func (_u *AnnouncementUpdate) sqlSave(ctx context.Context) (_node int, err error
if
value
,
ok
:=
_u
.
mutation
.
Status
();
ok
{
if
value
,
ok
:=
_u
.
mutation
.
Status
();
ok
{
_spec
.
SetField
(
announcement
.
FieldStatus
,
field
.
TypeString
,
value
)
_spec
.
SetField
(
announcement
.
FieldStatus
,
field
.
TypeString
,
value
)
}
}
if
value
,
ok
:=
_u
.
mutation
.
NotifyMode
();
ok
{
_spec
.
SetField
(
announcement
.
FieldNotifyMode
,
field
.
TypeString
,
value
)
}
if
value
,
ok
:=
_u
.
mutation
.
Targeting
();
ok
{
if
value
,
ok
:=
_u
.
mutation
.
Targeting
();
ok
{
_spec
.
SetField
(
announcement
.
FieldTargeting
,
field
.
TypeJSON
,
value
)
_spec
.
SetField
(
announcement
.
FieldTargeting
,
field
.
TypeJSON
,
value
)
}
}
...
@@ -456,6 +478,20 @@ func (_u *AnnouncementUpdateOne) SetNillableStatus(v *string) *AnnouncementUpdat
...
@@ -456,6 +478,20 @@ func (_u *AnnouncementUpdateOne) SetNillableStatus(v *string) *AnnouncementUpdat
return
_u
return
_u
}
}
// SetNotifyMode sets the "notify_mode" field.
func
(
_u
*
AnnouncementUpdateOne
)
SetNotifyMode
(
v
string
)
*
AnnouncementUpdateOne
{
_u
.
mutation
.
SetNotifyMode
(
v
)
return
_u
}
// SetNillableNotifyMode sets the "notify_mode" field if the given value is not nil.
func
(
_u
*
AnnouncementUpdateOne
)
SetNillableNotifyMode
(
v
*
string
)
*
AnnouncementUpdateOne
{
if
v
!=
nil
{
_u
.
SetNotifyMode
(
*
v
)
}
return
_u
}
// SetTargeting sets the "targeting" field.
// SetTargeting sets the "targeting" field.
func
(
_u
*
AnnouncementUpdateOne
)
SetTargeting
(
v
domain
.
AnnouncementTargeting
)
*
AnnouncementUpdateOne
{
func
(
_u
*
AnnouncementUpdateOne
)
SetTargeting
(
v
domain
.
AnnouncementTargeting
)
*
AnnouncementUpdateOne
{
_u
.
mutation
.
SetTargeting
(
v
)
_u
.
mutation
.
SetTargeting
(
v
)
...
@@ -683,6 +719,11 @@ func (_u *AnnouncementUpdateOne) check() error {
...
@@ -683,6 +719,11 @@ func (_u *AnnouncementUpdateOne) check() error {
return
&
ValidationError
{
Name
:
"status"
,
err
:
fmt
.
Errorf
(
`ent: validator failed for field "Announcement.status": %w`
,
err
)}
return
&
ValidationError
{
Name
:
"status"
,
err
:
fmt
.
Errorf
(
`ent: validator failed for field "Announcement.status": %w`
,
err
)}
}
}
}
}
if
v
,
ok
:=
_u
.
mutation
.
NotifyMode
();
ok
{
if
err
:=
announcement
.
NotifyModeValidator
(
v
);
err
!=
nil
{
return
&
ValidationError
{
Name
:
"notify_mode"
,
err
:
fmt
.
Errorf
(
`ent: validator failed for field "Announcement.notify_mode": %w`
,
err
)}
}
}
return
nil
return
nil
}
}
...
@@ -724,6 +765,9 @@ func (_u *AnnouncementUpdateOne) sqlSave(ctx context.Context) (_node *Announceme
...
@@ -724,6 +765,9 @@ func (_u *AnnouncementUpdateOne) sqlSave(ctx context.Context) (_node *Announceme
if
value
,
ok
:=
_u
.
mutation
.
Status
();
ok
{
if
value
,
ok
:=
_u
.
mutation
.
Status
();
ok
{
_spec
.
SetField
(
announcement
.
FieldStatus
,
field
.
TypeString
,
value
)
_spec
.
SetField
(
announcement
.
FieldStatus
,
field
.
TypeString
,
value
)
}
}
if
value
,
ok
:=
_u
.
mutation
.
NotifyMode
();
ok
{
_spec
.
SetField
(
announcement
.
FieldNotifyMode
,
field
.
TypeString
,
value
)
}
if
value
,
ok
:=
_u
.
mutation
.
Targeting
();
ok
{
if
value
,
ok
:=
_u
.
mutation
.
Targeting
();
ok
{
_spec
.
SetField
(
announcement
.
FieldTargeting
,
field
.
TypeJSON
,
value
)
_spec
.
SetField
(
announcement
.
FieldTargeting
,
field
.
TypeJSON
,
value
)
}
}
...
...
backend/ent/migrate/schema.go
View file @
7079edc2
...
@@ -251,6 +251,7 @@ var (
...
@@ -251,6 +251,7 @@ var (
{
Name
:
"title"
,
Type
:
field
.
TypeString
,
Size
:
200
},
{
Name
:
"title"
,
Type
:
field
.
TypeString
,
Size
:
200
},
{
Name
:
"content"
,
Type
:
field
.
TypeString
,
SchemaType
:
map
[
string
]
string
{
"postgres"
:
"text"
}},
{
Name
:
"content"
,
Type
:
field
.
TypeString
,
SchemaType
:
map
[
string
]
string
{
"postgres"
:
"text"
}},
{
Name
:
"status"
,
Type
:
field
.
TypeString
,
Size
:
20
,
Default
:
"draft"
},
{
Name
:
"status"
,
Type
:
field
.
TypeString
,
Size
:
20
,
Default
:
"draft"
},
{
Name
:
"notify_mode"
,
Type
:
field
.
TypeString
,
Size
:
20
,
Default
:
"silent"
},
{
Name
:
"targeting"
,
Type
:
field
.
TypeJSON
,
Nullable
:
true
,
SchemaType
:
map
[
string
]
string
{
"postgres"
:
"jsonb"
}},
{
Name
:
"targeting"
,
Type
:
field
.
TypeJSON
,
Nullable
:
true
,
SchemaType
:
map
[
string
]
string
{
"postgres"
:
"jsonb"
}},
{
Name
:
"starts_at"
,
Type
:
field
.
TypeTime
,
Nullable
:
true
,
SchemaType
:
map
[
string
]
string
{
"postgres"
:
"timestamptz"
}},
{
Name
:
"starts_at"
,
Type
:
field
.
TypeTime
,
Nullable
:
true
,
SchemaType
:
map
[
string
]
string
{
"postgres"
:
"timestamptz"
}},
{
Name
:
"ends_at"
,
Type
:
field
.
TypeTime
,
Nullable
:
true
,
SchemaType
:
map
[
string
]
string
{
"postgres"
:
"timestamptz"
}},
{
Name
:
"ends_at"
,
Type
:
field
.
TypeTime
,
Nullable
:
true
,
SchemaType
:
map
[
string
]
string
{
"postgres"
:
"timestamptz"
}},
...
@@ -273,17 +274,17 @@ var (
...
@@ -273,17 +274,17 @@ var (
{
{
Name
:
"announcement_created_at"
,
Name
:
"announcement_created_at"
,
Unique
:
false
,
Unique
:
false
,
Columns
:
[]
*
schema
.
Column
{
AnnouncementsColumns
[
9
]},
Columns
:
[]
*
schema
.
Column
{
AnnouncementsColumns
[
10
]},
},
},
{
{
Name
:
"announcement_starts_at"
,
Name
:
"announcement_starts_at"
,
Unique
:
false
,
Unique
:
false
,
Columns
:
[]
*
schema
.
Column
{
AnnouncementsColumns
[
5
]},
Columns
:
[]
*
schema
.
Column
{
AnnouncementsColumns
[
6
]},
},
},
{
{
Name
:
"announcement_ends_at"
,
Name
:
"announcement_ends_at"
,
Unique
:
false
,
Unique
:
false
,
Columns
:
[]
*
schema
.
Column
{
AnnouncementsColumns
[
6
]},
Columns
:
[]
*
schema
.
Column
{
AnnouncementsColumns
[
7
]},
},
},
},
},
}
}
...
...
backend/ent/mutation.go
View file @
7079edc2
...
@@ -5167,6 +5167,7 @@ type AnnouncementMutation struct {
...
@@ -5167,6 +5167,7 @@ type AnnouncementMutation struct {
title *string
title *string
content *string
content *string
status *string
status *string
notify_mode *string
targeting *domain.AnnouncementTargeting
targeting *domain.AnnouncementTargeting
starts_at *time.Time
starts_at *time.Time
ends_at *time.Time
ends_at *time.Time
...
@@ -5391,6 +5392,42 @@ func (m *AnnouncementMutation) ResetStatus() {
...
@@ -5391,6 +5392,42 @@ func (m *AnnouncementMutation) ResetStatus() {
m.status = nil
m.status = nil
}
}
// SetNotifyMode sets the "notify_mode" field.
func (m *AnnouncementMutation) SetNotifyMode(s string) {
m.notify_mode = &s
}
// NotifyMode returns the value of the "notify_mode" field in the mutation.
func (m *AnnouncementMutation) NotifyMode() (r string, exists bool) {
v := m.notify_mode
if v == nil {
return
}
return *v, true
}
// OldNotifyMode returns the old "notify_mode" field's value of the Announcement entity.
// If the Announcement object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *AnnouncementMutation) OldNotifyMode(ctx context.Context) (v string, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldNotifyMode is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldNotifyMode requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldNotifyMode: %w", err)
}
return oldValue.NotifyMode, nil
}
// ResetNotifyMode resets all changes to the "notify_mode" field.
func (m *AnnouncementMutation) ResetNotifyMode() {
m.notify_mode = nil
}
// SetTargeting sets the "targeting" field.
// SetTargeting sets the "targeting" field.
func (m *AnnouncementMutation) SetTargeting(dt domain.AnnouncementTargeting) {
func (m *AnnouncementMutation) SetTargeting(dt domain.AnnouncementTargeting) {
m.targeting = &dt
m.targeting = &dt
...
@@ -5838,7 +5875,7 @@ func (m *AnnouncementMutation) Type() string {
...
@@ -5838,7 +5875,7 @@ func (m *AnnouncementMutation) Type() string {
// order to get all numeric fields that were incremented/decremented, call
// order to get all numeric fields that were incremented/decremented, call
// AddedFields().
// AddedFields().
func (m *AnnouncementMutation) Fields() []string {
func (m *AnnouncementMutation) Fields() []string {
fields := make([]string, 0, 1
0
)
fields := make([]string, 0, 1
1
)
if m.title != nil {
if m.title != nil {
fields = append(fields, announcement.FieldTitle)
fields = append(fields, announcement.FieldTitle)
}
}
...
@@ -5848,6 +5885,9 @@ func (m *AnnouncementMutation) Fields() []string {
...
@@ -5848,6 +5885,9 @@ func (m *AnnouncementMutation) Fields() []string {
if m.status != nil {
if m.status != nil {
fields = append(fields, announcement.FieldStatus)
fields = append(fields, announcement.FieldStatus)
}
}
if m.notify_mode != nil {
fields = append(fields, announcement.FieldNotifyMode)
}
if m.targeting != nil {
if m.targeting != nil {
fields = append(fields, announcement.FieldTargeting)
fields = append(fields, announcement.FieldTargeting)
}
}
...
@@ -5883,6 +5923,8 @@ func (m *AnnouncementMutation) Field(name string) (ent.Value, bool) {
...
@@ -5883,6 +5923,8 @@ func (m *AnnouncementMutation) Field(name string) (ent.Value, bool) {
return m.Content()
return m.Content()
case announcement.FieldStatus:
case announcement.FieldStatus:
return m.Status()
return m.Status()
case announcement.FieldNotifyMode:
return m.NotifyMode()
case announcement.FieldTargeting:
case announcement.FieldTargeting:
return m.Targeting()
return m.Targeting()
case announcement.FieldStartsAt:
case announcement.FieldStartsAt:
...
@@ -5912,6 +5954,8 @@ func (m *AnnouncementMutation) OldField(ctx context.Context, name string) (ent.V
...
@@ -5912,6 +5954,8 @@ func (m *AnnouncementMutation) OldField(ctx context.Context, name string) (ent.V
return m.OldContent(ctx)
return m.OldContent(ctx)
case announcement.FieldStatus:
case announcement.FieldStatus:
return m.OldStatus(ctx)
return m.OldStatus(ctx)
case announcement.FieldNotifyMode:
return m.OldNotifyMode(ctx)
case announcement.FieldTargeting:
case announcement.FieldTargeting:
return m.OldTargeting(ctx)
return m.OldTargeting(ctx)
case announcement.FieldStartsAt:
case announcement.FieldStartsAt:
...
@@ -5956,6 +6000,13 @@ func (m *AnnouncementMutation) SetField(name string, value ent.Value) error {
...
@@ -5956,6 +6000,13 @@ func (m *AnnouncementMutation) SetField(name string, value ent.Value) error {
}
}
m.SetStatus(v)
m.SetStatus(v)
return nil
return nil
case announcement.FieldNotifyMode:
v, ok := value.(string)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetNotifyMode(v)
return nil
case announcement.FieldTargeting:
case announcement.FieldTargeting:
v, ok := value.(domain.AnnouncementTargeting)
v, ok := value.(domain.AnnouncementTargeting)
if !ok {
if !ok {
...
@@ -6123,6 +6174,9 @@ func (m *AnnouncementMutation) ResetField(name string) error {
...
@@ -6123,6 +6174,9 @@ func (m *AnnouncementMutation) ResetField(name string) error {
case announcement.FieldStatus:
case announcement.FieldStatus:
m.ResetStatus()
m.ResetStatus()
return nil
return nil
case announcement.FieldNotifyMode:
m.ResetNotifyMode()
return nil
case announcement.FieldTargeting:
case announcement.FieldTargeting:
m.ResetTargeting()
m.ResetTargeting()
return nil
return nil
...
@@ -10298,7 +10352,7 @@ func (m *GroupMutation) Type() string {
...
@@ -10298,7 +10352,7 @@ func (m *GroupMutation) Type() string {
// order to get all numeric fields that were incremented/decremented, call
// order to get all numeric fields that were incremented/decremented, call
// AddedFields().
// AddedFields().
func (m *GroupMutation) Fields() []string {
func (m *GroupMutation) Fields() []string {
fields := make([]string, 0, 3
1
)
fields := make([]string, 0, 3
0
)
if m.created_at != nil {
if m.created_at != nil {
fields = append(fields, group.FieldCreatedAt)
fields = append(fields, group.FieldCreatedAt)
}
}
...
...
backend/ent/runtime/runtime.go
View file @
7079edc2
...
@@ -277,12 +277,18 @@ func init() {
...
@@ -277,12 +277,18 @@ func init() {
announcement
.
DefaultStatus
=
announcementDescStatus
.
Default
.
(
string
)
announcement
.
DefaultStatus
=
announcementDescStatus
.
Default
.
(
string
)
// announcement.StatusValidator is a validator for the "status" field. It is called by the builders before save.
// announcement.StatusValidator is a validator for the "status" field. It is called by the builders before save.
announcement
.
StatusValidator
=
announcementDescStatus
.
Validators
[
0
]
.
(
func
(
string
)
error
)
announcement
.
StatusValidator
=
announcementDescStatus
.
Validators
[
0
]
.
(
func
(
string
)
error
)
// announcementDescNotifyMode is the schema descriptor for notify_mode field.
announcementDescNotifyMode
:=
announcementFields
[
3
]
.
Descriptor
()
// announcement.DefaultNotifyMode holds the default value on creation for the notify_mode field.
announcement
.
DefaultNotifyMode
=
announcementDescNotifyMode
.
Default
.
(
string
)
// announcement.NotifyModeValidator is a validator for the "notify_mode" field. It is called by the builders before save.
announcement
.
NotifyModeValidator
=
announcementDescNotifyMode
.
Validators
[
0
]
.
(
func
(
string
)
error
)
// announcementDescCreatedAt is the schema descriptor for created_at field.
// announcementDescCreatedAt is the schema descriptor for created_at field.
announcementDescCreatedAt
:=
announcementFields
[
8
]
.
Descriptor
()
announcementDescCreatedAt
:=
announcementFields
[
9
]
.
Descriptor
()
// announcement.DefaultCreatedAt holds the default value on creation for the created_at field.
// announcement.DefaultCreatedAt holds the default value on creation for the created_at field.
announcement
.
DefaultCreatedAt
=
announcementDescCreatedAt
.
Default
.
(
func
()
time
.
Time
)
announcement
.
DefaultCreatedAt
=
announcementDescCreatedAt
.
Default
.
(
func
()
time
.
Time
)
// announcementDescUpdatedAt is the schema descriptor for updated_at field.
// announcementDescUpdatedAt is the schema descriptor for updated_at field.
announcementDescUpdatedAt
:=
announcementFields
[
9
]
.
Descriptor
()
announcementDescUpdatedAt
:=
announcementFields
[
10
]
.
Descriptor
()
// announcement.DefaultUpdatedAt holds the default value on creation for the updated_at field.
// announcement.DefaultUpdatedAt holds the default value on creation for the updated_at field.
announcement
.
DefaultUpdatedAt
=
announcementDescUpdatedAt
.
Default
.
(
func
()
time
.
Time
)
announcement
.
DefaultUpdatedAt
=
announcementDescUpdatedAt
.
Default
.
(
func
()
time
.
Time
)
// announcement.UpdateDefaultUpdatedAt holds the default value on update for the updated_at field.
// announcement.UpdateDefaultUpdatedAt holds the default value on update for the updated_at field.
...
...
backend/ent/schema/announcement.go
View file @
7079edc2
...
@@ -41,6 +41,10 @@ func (Announcement) Fields() []ent.Field {
...
@@ -41,6 +41,10 @@ func (Announcement) Fields() []ent.Field {
MaxLen
(
20
)
.
MaxLen
(
20
)
.
Default
(
domain
.
AnnouncementStatusDraft
)
.
Default
(
domain
.
AnnouncementStatusDraft
)
.
Comment
(
"状态: draft, active, archived"
),
Comment
(
"状态: draft, active, archived"
),
field
.
String
(
"notify_mode"
)
.
MaxLen
(
20
)
.
Default
(
domain
.
AnnouncementNotifyModeSilent
)
.
Comment
(
"通知模式: silent(仅铃铛), popup(弹窗提醒)"
),
field
.
JSON
(
"targeting"
,
domain
.
AnnouncementTargeting
{})
.
field
.
JSON
(
"targeting"
,
domain
.
AnnouncementTargeting
{})
.
Optional
()
.
Optional
()
.
SchemaType
(
map
[
string
]
string
{
dialect
.
Postgres
:
"jsonb"
})
.
SchemaType
(
map
[
string
]
string
{
dialect
.
Postgres
:
"jsonb"
})
.
...
...
backend/go.sum
View file @
7079edc2
...
@@ -94,6 +94,10 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
...
@@ -94,6 +94,10 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
...
@@ -230,6 +234,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
...
@@ -230,6 +234,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI=
github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI=
...
@@ -263,6 +269,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
...
@@ -263,6 +269,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
...
@@ -314,6 +322,8 @@ github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
...
@@ -314,6 +322,8 @@ github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
...
...
backend/internal/domain/announcement.go
View file @
7079edc2
...
@@ -13,6 +13,11 @@ const (
...
@@ -13,6 +13,11 @@ const (
AnnouncementStatusArchived
=
"archived"
AnnouncementStatusArchived
=
"archived"
)
)
const
(
AnnouncementNotifyModeSilent
=
"silent"
AnnouncementNotifyModePopup
=
"popup"
)
const
(
const
(
AnnouncementConditionTypeSubscription
=
"subscription"
AnnouncementConditionTypeSubscription
=
"subscription"
AnnouncementConditionTypeBalance
=
"balance"
AnnouncementConditionTypeBalance
=
"balance"
...
@@ -195,17 +200,18 @@ func (c AnnouncementCondition) validate() error {
...
@@ -195,17 +200,18 @@ func (c AnnouncementCondition) validate() error {
}
}
type
Announcement
struct
{
type
Announcement
struct
{
ID
int64
ID
int64
Title
string
Title
string
Content
string
Content
string
Status
string
Status
string
Targeting
AnnouncementTargeting
NotifyMode
string
StartsAt
*
time
.
Time
Targeting
AnnouncementTargeting
EndsAt
*
time
.
Time
StartsAt
*
time
.
Time
CreatedBy
*
int64
EndsAt
*
time
.
Time
UpdatedBy
*
int64
CreatedBy
*
int64
CreatedAt
time
.
Time
UpdatedBy
*
int64
UpdatedAt
time
.
Time
CreatedAt
time
.
Time
UpdatedAt
time
.
Time
}
}
func
(
a
*
Announcement
)
IsActiveAt
(
now
time
.
Time
)
bool
{
func
(
a
*
Announcement
)
IsActiveAt
(
now
time
.
Time
)
bool
{
...
...
backend/internal/handler/admin/announcement_handler.go
View file @
7079edc2
...
@@ -27,21 +27,23 @@ func NewAnnouncementHandler(announcementService *service.AnnouncementService) *A
...
@@ -27,21 +27,23 @@ func NewAnnouncementHandler(announcementService *service.AnnouncementService) *A
}
}
type
CreateAnnouncementRequest
struct
{
type
CreateAnnouncementRequest
struct
{
Title
string
`json:"title" binding:"required"`
Title
string
`json:"title" binding:"required"`
Content
string
`json:"content" binding:"required"`
Content
string
`json:"content" binding:"required"`
Status
string
`json:"status" binding:"omitempty,oneof=draft active archived"`
Status
string
`json:"status" binding:"omitempty,oneof=draft active archived"`
Targeting
service
.
AnnouncementTargeting
`json:"targeting"`
NotifyMode
string
`json:"notify_mode" binding:"omitempty,oneof=silent popup"`
StartsAt
*
int64
`json:"starts_at"`
// Unix seconds, 0/empty = immediate
Targeting
service
.
AnnouncementTargeting
`json:"targeting"`
EndsAt
*
int64
`json:"ends_at"`
// Unix seconds, 0/empty = never
StartsAt
*
int64
`json:"starts_at"`
// Unix seconds, 0/empty = immediate
EndsAt
*
int64
`json:"ends_at"`
// Unix seconds, 0/empty = never
}
}
type
UpdateAnnouncementRequest
struct
{
type
UpdateAnnouncementRequest
struct
{
Title
*
string
`json:"title"`
Title
*
string
`json:"title"`
Content
*
string
`json:"content"`
Content
*
string
`json:"content"`
Status
*
string
`json:"status" binding:"omitempty,oneof=draft active archived"`
Status
*
string
`json:"status" binding:"omitempty,oneof=draft active archived"`
Targeting
*
service
.
AnnouncementTargeting
`json:"targeting"`
NotifyMode
*
string
`json:"notify_mode" binding:"omitempty,oneof=silent popup"`
StartsAt
*
int64
`json:"starts_at"`
// Unix seconds, 0 = clear
Targeting
*
service
.
AnnouncementTargeting
`json:"targeting"`
EndsAt
*
int64
`json:"ends_at"`
// Unix seconds, 0 = clear
StartsAt
*
int64
`json:"starts_at"`
// Unix seconds, 0 = clear
EndsAt
*
int64
`json:"ends_at"`
// Unix seconds, 0 = clear
}
}
// List handles listing announcements with filters
// List handles listing announcements with filters
...
@@ -110,11 +112,12 @@ func (h *AnnouncementHandler) Create(c *gin.Context) {
...
@@ -110,11 +112,12 @@ func (h *AnnouncementHandler) Create(c *gin.Context) {
}
}
input
:=
&
service
.
CreateAnnouncementInput
{
input
:=
&
service
.
CreateAnnouncementInput
{
Title
:
req
.
Title
,
Title
:
req
.
Title
,
Content
:
req
.
Content
,
Content
:
req
.
Content
,
Status
:
req
.
Status
,
Status
:
req
.
Status
,
Targeting
:
req
.
Targeting
,
NotifyMode
:
req
.
NotifyMode
,
ActorID
:
&
subject
.
UserID
,
Targeting
:
req
.
Targeting
,
ActorID
:
&
subject
.
UserID
,
}
}
if
req
.
StartsAt
!=
nil
&&
*
req
.
StartsAt
>
0
{
if
req
.
StartsAt
!=
nil
&&
*
req
.
StartsAt
>
0
{
...
@@ -157,11 +160,12 @@ func (h *AnnouncementHandler) Update(c *gin.Context) {
...
@@ -157,11 +160,12 @@ func (h *AnnouncementHandler) Update(c *gin.Context) {
}
}
input
:=
&
service
.
UpdateAnnouncementInput
{
input
:=
&
service
.
UpdateAnnouncementInput
{
Title
:
req
.
Title
,
Title
:
req
.
Title
,
Content
:
req
.
Content
,
Content
:
req
.
Content
,
Status
:
req
.
Status
,
Status
:
req
.
Status
,
Targeting
:
req
.
Targeting
,
NotifyMode
:
req
.
NotifyMode
,
ActorID
:
&
subject
.
UserID
,
Targeting
:
req
.
Targeting
,
ActorID
:
&
subject
.
UserID
,
}
}
if
req
.
StartsAt
!=
nil
{
if
req
.
StartsAt
!=
nil
{
...
...
backend/internal/handler/dto/announcement.go
View file @
7079edc2
...
@@ -7,10 +7,11 @@ import (
...
@@ -7,10 +7,11 @@ import (
)
)
type
Announcement
struct
{
type
Announcement
struct
{
ID
int64
`json:"id"`
ID
int64
`json:"id"`
Title
string
`json:"title"`
Title
string
`json:"title"`
Content
string
`json:"content"`
Content
string
`json:"content"`
Status
string
`json:"status"`
Status
string
`json:"status"`
NotifyMode
string
`json:"notify_mode"`
Targeting
service
.
AnnouncementTargeting
`json:"targeting"`
Targeting
service
.
AnnouncementTargeting
`json:"targeting"`
...
@@ -25,9 +26,10 @@ type Announcement struct {
...
@@ -25,9 +26,10 @@ type Announcement struct {
}
}
type
UserAnnouncement
struct
{
type
UserAnnouncement
struct
{
ID
int64
`json:"id"`
ID
int64
`json:"id"`
Title
string
`json:"title"`
Title
string
`json:"title"`
Content
string
`json:"content"`
Content
string
`json:"content"`
NotifyMode
string
`json:"notify_mode"`
StartsAt
*
time
.
Time
`json:"starts_at,omitempty"`
StartsAt
*
time
.
Time
`json:"starts_at,omitempty"`
EndsAt
*
time
.
Time
`json:"ends_at,omitempty"`
EndsAt
*
time
.
Time
`json:"ends_at,omitempty"`
...
@@ -43,17 +45,18 @@ func AnnouncementFromService(a *service.Announcement) *Announcement {
...
@@ -43,17 +45,18 @@ func AnnouncementFromService(a *service.Announcement) *Announcement {
return
nil
return
nil
}
}
return
&
Announcement
{
return
&
Announcement
{
ID
:
a
.
ID
,
ID
:
a
.
ID
,
Title
:
a
.
Title
,
Title
:
a
.
Title
,
Content
:
a
.
Content
,
Content
:
a
.
Content
,
Status
:
a
.
Status
,
Status
:
a
.
Status
,
Targeting
:
a
.
Targeting
,
NotifyMode
:
a
.
NotifyMode
,
StartsAt
:
a
.
StartsAt
,
Targeting
:
a
.
Targeting
,
EndsAt
:
a
.
EndsAt
,
StartsAt
:
a
.
StartsAt
,
CreatedBy
:
a
.
CreatedBy
,
EndsAt
:
a
.
EndsAt
,
UpdatedBy
:
a
.
UpdatedBy
,
CreatedBy
:
a
.
CreatedBy
,
CreatedAt
:
a
.
CreatedAt
,
UpdatedBy
:
a
.
UpdatedBy
,
UpdatedAt
:
a
.
UpdatedAt
,
CreatedAt
:
a
.
CreatedAt
,
UpdatedAt
:
a
.
UpdatedAt
,
}
}
}
}
...
@@ -62,13 +65,14 @@ func UserAnnouncementFromService(a *service.UserAnnouncement) *UserAnnouncement
...
@@ -62,13 +65,14 @@ func UserAnnouncementFromService(a *service.UserAnnouncement) *UserAnnouncement
return
nil
return
nil
}
}
return
&
UserAnnouncement
{
return
&
UserAnnouncement
{
ID
:
a
.
Announcement
.
ID
,
ID
:
a
.
Announcement
.
ID
,
Title
:
a
.
Announcement
.
Title
,
Title
:
a
.
Announcement
.
Title
,
Content
:
a
.
Announcement
.
Content
,
Content
:
a
.
Announcement
.
Content
,
StartsAt
:
a
.
Announcement
.
StartsAt
,
NotifyMode
:
a
.
Announcement
.
NotifyMode
,
EndsAt
:
a
.
Announcement
.
EndsAt
,
StartsAt
:
a
.
Announcement
.
StartsAt
,
ReadAt
:
a
.
ReadAt
,
EndsAt
:
a
.
Announcement
.
EndsAt
,
CreatedAt
:
a
.
Announcement
.
CreatedAt
,
ReadAt
:
a
.
ReadAt
,
UpdatedAt
:
a
.
Announcement
.
UpdatedAt
,
CreatedAt
:
a
.
Announcement
.
CreatedAt
,
UpdatedAt
:
a
.
Announcement
.
UpdatedAt
,
}
}
}
}
backend/internal/repository/announcement_repo.go
View file @
7079edc2
...
@@ -24,6 +24,7 @@ func (r *announcementRepository) Create(ctx context.Context, a *service.Announce
...
@@ -24,6 +24,7 @@ func (r *announcementRepository) Create(ctx context.Context, a *service.Announce
SetTitle
(
a
.
Title
)
.
SetTitle
(
a
.
Title
)
.
SetContent
(
a
.
Content
)
.
SetContent
(
a
.
Content
)
.
SetStatus
(
a
.
Status
)
.
SetStatus
(
a
.
Status
)
.
SetNotifyMode
(
a
.
NotifyMode
)
.
SetTargeting
(
a
.
Targeting
)
SetTargeting
(
a
.
Targeting
)
if
a
.
StartsAt
!=
nil
{
if
a
.
StartsAt
!=
nil
{
...
@@ -64,6 +65,7 @@ func (r *announcementRepository) Update(ctx context.Context, a *service.Announce
...
@@ -64,6 +65,7 @@ func (r *announcementRepository) Update(ctx context.Context, a *service.Announce
SetTitle
(
a
.
Title
)
.
SetTitle
(
a
.
Title
)
.
SetContent
(
a
.
Content
)
.
SetContent
(
a
.
Content
)
.
SetStatus
(
a
.
Status
)
.
SetStatus
(
a
.
Status
)
.
SetNotifyMode
(
a
.
NotifyMode
)
.
SetTargeting
(
a
.
Targeting
)
SetTargeting
(
a
.
Targeting
)
if
a
.
StartsAt
!=
nil
{
if
a
.
StartsAt
!=
nil
{
...
@@ -169,17 +171,18 @@ func announcementEntityToService(m *dbent.Announcement) *service.Announcement {
...
@@ -169,17 +171,18 @@ func announcementEntityToService(m *dbent.Announcement) *service.Announcement {
return
nil
return
nil
}
}
return
&
service
.
Announcement
{
return
&
service
.
Announcement
{
ID
:
m
.
ID
,
ID
:
m
.
ID
,
Title
:
m
.
Title
,
Title
:
m
.
Title
,
Content
:
m
.
Content
,
Content
:
m
.
Content
,
Status
:
m
.
Status
,
Status
:
m
.
Status
,
Targeting
:
m
.
Targeting
,
NotifyMode
:
m
.
NotifyMode
,
StartsAt
:
m
.
StartsAt
,
Targeting
:
m
.
Targeting
,
EndsAt
:
m
.
EndsAt
,
StartsAt
:
m
.
StartsAt
,
CreatedBy
:
m
.
CreatedBy
,
EndsAt
:
m
.
EndsAt
,
UpdatedBy
:
m
.
UpdatedBy
,
CreatedBy
:
m
.
CreatedBy
,
CreatedAt
:
m
.
CreatedAt
,
UpdatedBy
:
m
.
UpdatedBy
,
UpdatedAt
:
m
.
UpdatedAt
,
CreatedAt
:
m
.
CreatedAt
,
UpdatedAt
:
m
.
UpdatedAt
,
}
}
}
}
...
...
backend/internal/service/announcement.go
View file @
7079edc2
...
@@ -14,6 +14,11 @@ const (
...
@@ -14,6 +14,11 @@ const (
AnnouncementStatusArchived
=
domain
.
AnnouncementStatusArchived
AnnouncementStatusArchived
=
domain
.
AnnouncementStatusArchived
)
)
const
(
AnnouncementNotifyModeSilent
=
domain
.
AnnouncementNotifyModeSilent
AnnouncementNotifyModePopup
=
domain
.
AnnouncementNotifyModePopup
)
const
(
const
(
AnnouncementConditionTypeSubscription
=
domain
.
AnnouncementConditionTypeSubscription
AnnouncementConditionTypeSubscription
=
domain
.
AnnouncementConditionTypeSubscription
AnnouncementConditionTypeBalance
=
domain
.
AnnouncementConditionTypeBalance
AnnouncementConditionTypeBalance
=
domain
.
AnnouncementConditionTypeBalance
...
...
backend/internal/service/announcement_service.go
View file @
7079edc2
...
@@ -33,23 +33,25 @@ func NewAnnouncementService(
...
@@ -33,23 +33,25 @@ func NewAnnouncementService(
}
}
type
CreateAnnouncementInput
struct
{
type
CreateAnnouncementInput
struct
{
Title
string
Title
string
Content
string
Content
string
Status
string
Status
string
Targeting
AnnouncementTargeting
NotifyMode
string
StartsAt
*
time
.
Time
Targeting
AnnouncementTargeting
EndsAt
*
time
.
Time
StartsAt
*
time
.
Time
ActorID
*
int64
// 管理员用户ID
EndsAt
*
time
.
Time
ActorID
*
int64
// 管理员用户ID
}
}
type
UpdateAnnouncementInput
struct
{
type
UpdateAnnouncementInput
struct
{
Title
*
string
Title
*
string
Content
*
string
Content
*
string
Status
*
string
Status
*
string
Targeting
*
AnnouncementTargeting
NotifyMode
*
string
StartsAt
**
time
.
Time
Targeting
*
AnnouncementTargeting
EndsAt
**
time
.
Time
StartsAt
**
time
.
Time
ActorID
*
int64
// 管理员用户ID
EndsAt
**
time
.
Time
ActorID
*
int64
// 管理员用户ID
}
}
type
UserAnnouncement
struct
{
type
UserAnnouncement
struct
{
...
@@ -93,6 +95,14 @@ func (s *AnnouncementService) Create(ctx context.Context, input *CreateAnnouncem
...
@@ -93,6 +95,14 @@ func (s *AnnouncementService) Create(ctx context.Context, input *CreateAnnouncem
return
nil
,
err
return
nil
,
err
}
}
notifyMode
:=
strings
.
TrimSpace
(
input
.
NotifyMode
)
if
notifyMode
==
""
{
notifyMode
=
AnnouncementNotifyModeSilent
}
if
!
isValidAnnouncementNotifyMode
(
notifyMode
)
{
return
nil
,
fmt
.
Errorf
(
"create announcement: invalid notify_mode"
)
}
if
input
.
StartsAt
!=
nil
&&
input
.
EndsAt
!=
nil
{
if
input
.
StartsAt
!=
nil
&&
input
.
EndsAt
!=
nil
{
if
!
input
.
StartsAt
.
Before
(
*
input
.
EndsAt
)
{
if
!
input
.
StartsAt
.
Before
(
*
input
.
EndsAt
)
{
return
nil
,
fmt
.
Errorf
(
"create announcement: starts_at must be before ends_at"
)
return
nil
,
fmt
.
Errorf
(
"create announcement: starts_at must be before ends_at"
)
...
@@ -100,12 +110,13 @@ func (s *AnnouncementService) Create(ctx context.Context, input *CreateAnnouncem
...
@@ -100,12 +110,13 @@ func (s *AnnouncementService) Create(ctx context.Context, input *CreateAnnouncem
}
}
a
:=
&
Announcement
{
a
:=
&
Announcement
{
Title
:
title
,
Title
:
title
,
Content
:
content
,
Content
:
content
,
Status
:
status
,
Status
:
status
,
Targeting
:
targeting
,
NotifyMode
:
notifyMode
,
StartsAt
:
input
.
StartsAt
,
Targeting
:
targeting
,
EndsAt
:
input
.
EndsAt
,
StartsAt
:
input
.
StartsAt
,
EndsAt
:
input
.
EndsAt
,
}
}
if
input
.
ActorID
!=
nil
&&
*
input
.
ActorID
>
0
{
if
input
.
ActorID
!=
nil
&&
*
input
.
ActorID
>
0
{
a
.
CreatedBy
=
input
.
ActorID
a
.
CreatedBy
=
input
.
ActorID
...
@@ -150,6 +161,14 @@ func (s *AnnouncementService) Update(ctx context.Context, id int64, input *Updat
...
@@ -150,6 +161,14 @@ func (s *AnnouncementService) Update(ctx context.Context, id int64, input *Updat
a
.
Status
=
status
a
.
Status
=
status
}
}
if
input
.
NotifyMode
!=
nil
{
notifyMode
:=
strings
.
TrimSpace
(
*
input
.
NotifyMode
)
if
!
isValidAnnouncementNotifyMode
(
notifyMode
)
{
return
nil
,
fmt
.
Errorf
(
"update announcement: invalid notify_mode"
)
}
a
.
NotifyMode
=
notifyMode
}
if
input
.
Targeting
!=
nil
{
if
input
.
Targeting
!=
nil
{
targeting
,
err
:=
domain
.
AnnouncementTargeting
(
*
input
.
Targeting
)
.
NormalizeAndValidate
()
targeting
,
err
:=
domain
.
AnnouncementTargeting
(
*
input
.
Targeting
)
.
NormalizeAndValidate
()
if
err
!=
nil
{
if
err
!=
nil
{
...
@@ -376,3 +395,12 @@ func isValidAnnouncementStatus(status string) bool {
...
@@ -376,3 +395,12 @@ func isValidAnnouncementStatus(status string) bool {
return
false
return
false
}
}
}
}
func
isValidAnnouncementNotifyMode
(
mode
string
)
bool
{
switch
mode
{
case
AnnouncementNotifyModeSilent
,
AnnouncementNotifyModePopup
:
return
true
default
:
return
false
}
}
frontend/src/App.vue
View file @
7079edc2
<
script
setup
lang=
"ts"
>
<
script
setup
lang=
"ts"
>
import
{
RouterView
,
useRouter
,
useRoute
}
from
'
vue-router
'
import
{
RouterView
,
useRouter
,
useRoute
}
from
'
vue-router
'
import
{
onMounted
,
watch
}
from
'
vue
'
import
{
onMounted
,
onBeforeUnmount
,
watch
}
from
'
vue
'
import
Toast
from
'
@/components/common/Toast.vue
'
import
Toast
from
'
@/components/common/Toast.vue
'
import
NavigationProgress
from
'
@/components/common/NavigationProgress.vue
'
import
NavigationProgress
from
'
@/components/common/NavigationProgress.vue
'
import
{
useAppStore
,
useAuthStore
,
useSubscriptionStore
}
from
'
@/stores
'
import
AnnouncementPopup
from
'
@/components/common/AnnouncementPopup.vue
'
import
{
useAppStore
,
useAuthStore
,
useSubscriptionStore
,
useAnnouncementStore
}
from
'
@/stores
'
import
{
getSetupStatus
}
from
'
@/api/setup
'
import
{
getSetupStatus
}
from
'
@/api/setup
'
const
router
=
useRouter
()
const
router
=
useRouter
()
...
@@ -11,6 +12,7 @@ const route = useRoute()
...
@@ -11,6 +12,7 @@ const route = useRoute()
const
appStore
=
useAppStore
()
const
appStore
=
useAppStore
()
const
authStore
=
useAuthStore
()
const
authStore
=
useAuthStore
()
const
subscriptionStore
=
useSubscriptionStore
()
const
subscriptionStore
=
useSubscriptionStore
()
const
announcementStore
=
useAnnouncementStore
()
/**
/**
* Update favicon dynamically
* Update favicon dynamically
...
@@ -39,24 +41,55 @@ watch(
...
@@ -39,24 +41,55 @@ watch(
{
immediate
:
true
}
{
immediate
:
true
}
)
)
// Watch for authentication state and manage subscription data
// Watch for authentication state and manage subscription data + announcements
function
onVisibilityChange
()
{
if
(
document
.
visibilityState
===
'
visible
'
&&
authStore
.
isAuthenticated
)
{
announcementStore
.
fetchAnnouncements
()
}
}
watch
(
watch
(
()
=>
authStore
.
isAuthenticated
,
()
=>
authStore
.
isAuthenticated
,
(
isAuthenticated
)
=>
{
(
isAuthenticated
,
oldValue
)
=>
{
if
(
isAuthenticated
)
{
if
(
isAuthenticated
)
{
// User logged in: preload subscriptions and start polling
// User logged in: preload subscriptions and start polling
subscriptionStore
.
fetchActiveSubscriptions
().
catch
((
error
)
=>
{
subscriptionStore
.
fetchActiveSubscriptions
().
catch
((
error
)
=>
{
console
.
error
(
'
Failed to preload subscriptions:
'
,
error
)
console
.
error
(
'
Failed to preload subscriptions:
'
,
error
)
})
})
subscriptionStore
.
startPolling
()
subscriptionStore
.
startPolling
()
// Announcements: new login vs page refresh restore
if
(
oldValue
===
false
)
{
// New login: delay 3s then force fetch
setTimeout
(()
=>
announcementStore
.
fetchAnnouncements
(
true
),
3000
)
}
else
{
// Page refresh restore (oldValue was undefined)
announcementStore
.
fetchAnnouncements
()
}
// Register visibility change listener
document
.
addEventListener
(
'
visibilitychange
'
,
onVisibilityChange
)
}
else
{
}
else
{
// User logged out: clear data and stop polling
// User logged out: clear data and stop polling
subscriptionStore
.
clear
()
subscriptionStore
.
clear
()
announcementStore
.
reset
()
document
.
removeEventListener
(
'
visibilitychange
'
,
onVisibilityChange
)
}
}
},
},
{
immediate
:
true
}
{
immediate
:
true
}
)
)
// Route change trigger (throttled by store)
router
.
afterEach
(()
=>
{
if
(
authStore
.
isAuthenticated
)
{
announcementStore
.
fetchAnnouncements
()
}
})
onBeforeUnmount
(()
=>
{
document
.
removeEventListener
(
'
visibilitychange
'
,
onVisibilityChange
)
})
onMounted
(
async
()
=>
{
onMounted
(
async
()
=>
{
// Check if setup is needed
// Check if setup is needed
try
{
try
{
...
@@ -78,4 +111,5 @@ onMounted(async () => {
...
@@ -78,4 +111,5 @@ onMounted(async () => {
<NavigationProgress
/>
<NavigationProgress
/>
<RouterView
/>
<RouterView
/>
<Toast
/>
<Toast
/>
<AnnouncementPopup
/>
</
template
>
</
template
>
frontend/src/components/common/AnnouncementBell.vue
View file @
7079edc2
...
@@ -314,16 +314,18 @@
...
@@ -314,16 +314,18 @@
<
script
setup
lang=
"ts"
>
<
script
setup
lang=
"ts"
>
import
{
ref
,
computed
,
onMounted
,
onBeforeUnmount
,
watch
}
from
'
vue
'
import
{
ref
,
computed
,
onMounted
,
onBeforeUnmount
,
watch
}
from
'
vue
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
storeToRefs
}
from
'
pinia
'
import
{
marked
}
from
'
marked
'
import
{
marked
}
from
'
marked
'
import
DOMPurify
from
'
dompurify
'
import
DOMPurify
from
'
dompurify
'
import
{
announcementsAPI
}
from
'
@/api
'
import
{
useAppStore
}
from
'
@/stores/app
'
import
{
useAppStore
}
from
'
@/stores/app
'
import
{
useAnnouncementStore
}
from
'
@/stores/announcements
'
import
{
formatRelativeTime
,
formatRelativeWithDateTime
}
from
'
@/utils/format
'
import
{
formatRelativeTime
,
formatRelativeWithDateTime
}
from
'
@/utils/format
'
import
type
{
UserAnnouncement
}
from
'
@/types
'
import
type
{
UserAnnouncement
}
from
'
@/types
'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
Icon
from
'
@/components/icons/Icon.vue
'
const
{
t
}
=
useI18n
()
const
{
t
}
=
useI18n
()
const
appStore
=
useAppStore
()
const
appStore
=
useAppStore
()
const
announcementStore
=
useAnnouncementStore
()
// Configure marked
// Configure marked
marked
.
setOptions
({
marked
.
setOptions
({
...
@@ -331,17 +333,14 @@ marked.setOptions({
...
@@ -331,17 +333,14 @@ marked.setOptions({
gfm
:
true
,
gfm
:
true
,
})
})
// State
// Use store state (storeToRefs for reactivity)
const
announcements
=
ref
<
UserAnnouncement
[]
>
([])
const
{
announcements
,
loading
}
=
storeToRefs
(
announcementStore
)
const
unreadCount
=
computed
(()
=>
announcementStore
.
unreadCount
)
// Local modal state
const
isModalOpen
=
ref
(
false
)
const
isModalOpen
=
ref
(
false
)
const
detailModalOpen
=
ref
(
false
)
const
detailModalOpen
=
ref
(
false
)
const
selectedAnnouncement
=
ref
<
UserAnnouncement
|
null
>
(
null
)
const
selectedAnnouncement
=
ref
<
UserAnnouncement
|
null
>
(
null
)
const
loading
=
ref
(
false
)
// Computed
const
unreadCount
=
computed
(()
=>
announcements
.
value
.
filter
((
a
)
=>
!
a
.
read_at
).
length
)
// Methods
// Methods
function
renderMarkdown
(
content
:
string
):
string
{
function
renderMarkdown
(
content
:
string
):
string
{
...
@@ -350,24 +349,8 @@ function renderMarkdown(content: string): string {
...
@@ -350,24 +349,8 @@ function renderMarkdown(content: string): string {
return
DOMPurify
.
sanitize
(
html
)
return
DOMPurify
.
sanitize
(
html
)
}
}
async
function
loadAnnouncements
()
{
try
{
loading
.
value
=
true
const
allAnnouncements
=
await
announcementsAPI
.
list
(
false
)
announcements
.
value
=
allAnnouncements
.
slice
(
0
,
20
)
}
catch
(
err
:
any
)
{
console
.
error
(
'
Failed to load announcements:
'
,
err
)
appStore
.
showError
(
err
?.
message
||
t
(
'
common.unknownError
'
))
}
finally
{
loading
.
value
=
false
}
}
function
openModal
()
{
function
openModal
()
{
isModalOpen
.
value
=
true
isModalOpen
.
value
=
true
if
(
announcements
.
value
.
length
===
0
)
{
loadAnnouncements
()
}
}
}
function
closeModal
()
{
function
closeModal
()
{
...
@@ -389,14 +372,7 @@ function closeDetail() {
...
@@ -389,14 +372,7 @@ function closeDetail() {
async
function
markAsRead
(
id
:
number
)
{
async
function
markAsRead
(
id
:
number
)
{
try
{
try
{
await
announcementsAPI
.
markRead
(
id
)
await
announcementStore
.
markAsRead
(
id
)
const
announcement
=
announcements
.
value
.
find
((
a
)
=>
a
.
id
===
id
)
if
(
announcement
)
{
announcement
.
read_at
=
new
Date
().
toISOString
()
}
if
(
selectedAnnouncement
.
value
?.
id
===
id
)
{
selectedAnnouncement
.
value
.
read_at
=
new
Date
().
toISOString
()
}
}
catch
(
err
:
any
)
{
}
catch
(
err
:
any
)
{
appStore
.
showError
(
err
?.
message
||
t
(
'
common.unknownError
'
))
appStore
.
showError
(
err
?.
message
||
t
(
'
common.unknownError
'
))
}
}
...
@@ -410,19 +386,10 @@ async function markAsReadAndClose(id: number) {
...
@@ -410,19 +386,10 @@ async function markAsReadAndClose(id: number) {
async
function
markAllAsRead
()
{
async
function
markAllAsRead
()
{
try
{
try
{
loading
.
value
=
true
await
announcementStore
.
markAllAsRead
()
const
unreadAnnouncements
=
announcements
.
value
.
filter
((
a
)
=>
!
a
.
read_at
)
await
Promise
.
all
(
unreadAnnouncements
.
map
((
a
)
=>
announcementsAPI
.
markRead
(
a
.
id
)))
announcements
.
value
.
forEach
((
a
)
=>
{
if
(
!
a
.
read_at
)
{
a
.
read_at
=
new
Date
().
toISOString
()
}
})
appStore
.
showSuccess
(
t
(
'
announcements.allMarkedAsRead
'
))
appStore
.
showSuccess
(
t
(
'
announcements.allMarkedAsRead
'
))
}
catch
(
err
:
any
)
{
}
catch
(
err
:
any
)
{
appStore
.
showError
(
err
?.
message
||
t
(
'
common.unknownError
'
))
appStore
.
showError
(
err
?.
message
||
t
(
'
common.unknownError
'
))
}
finally
{
loading
.
value
=
false
}
}
}
}
...
@@ -438,22 +405,19 @@ function handleEscape(e: KeyboardEvent) {
...
@@ -438,22 +405,19 @@ function handleEscape(e: KeyboardEvent) {
onMounted
(()
=>
{
onMounted
(()
=>
{
document
.
addEventListener
(
'
keydown
'
,
handleEscape
)
document
.
addEventListener
(
'
keydown
'
,
handleEscape
)
loadAnnouncements
()
})
})
onBeforeUnmount
(()
=>
{
onBeforeUnmount
(()
=>
{
document
.
removeEventListener
(
'
keydown
'
,
handleEscape
)
document
.
removeEventListener
(
'
keydown
'
,
handleEscape
)
// Restore body overflow in case component is unmounted while modals are open
document
.
body
.
style
.
overflow
=
''
document
.
body
.
style
.
overflow
=
''
})
})
watch
([
isModalOpen
,
detailModalOpen
],
([
modal
,
detail
])
=>
{
watch
(
if
(
modal
||
detail
)
{
[
isModalOpen
,
detailModalOpen
,
()
=>
announcementStore
.
currentPopup
],
document
.
body
.
style
.
overflow
=
'
hidden
'
([
modal
,
detail
,
popup
])
=>
{
}
else
{
document
.
body
.
style
.
overflow
=
(
modal
||
detail
||
popup
)
?
'
hidden
'
:
''
document
.
body
.
style
.
overflow
=
''
}
}
}
)
)
</
script
>
</
script
>
<
style
scoped
>
<
style
scoped
>
...
...
frontend/src/components/common/AnnouncementPopup.vue
0 → 100644
View file @
7079edc2
<
template
>
<Teleport
to=
"body"
>
<Transition
name=
"popup-fade"
>
<div
v-if=
"announcementStore.currentPopup"
class=
"fixed inset-0 z-[120] flex items-start justify-center overflow-y-auto bg-gradient-to-br from-black/70 via-black/60 to-black/70 p-4 pt-[8vh] backdrop-blur-md"
>
<div
class=
"w-full max-w-[680px] overflow-hidden rounded-3xl bg-white shadow-2xl ring-1 ring-black/5 dark:bg-dark-800 dark:ring-white/10"
@
click.stop
>
<!-- Header with warm gradient -->
<div
class=
"relative overflow-hidden border-b border-amber-100/80 bg-gradient-to-br from-amber-50/80 via-orange-50/50 to-yellow-50/30 px-8 py-6 dark:border-dark-700/50 dark:from-amber-900/20 dark:via-orange-900/10 dark:to-yellow-900/5"
>
<!-- Decorative background -->
<div
class=
"absolute right-0 top-0 h-full w-64 bg-gradient-to-l from-orange-100/30 to-transparent dark:from-orange-900/20"
></div>
<div
class=
"absolute -right-8 -top-8 h-32 w-32 rounded-full bg-gradient-to-br from-amber-400/20 to-orange-500/20 blur-3xl"
></div>
<div
class=
"absolute -left-4 -bottom-4 h-24 w-24 rounded-full bg-gradient-to-tr from-yellow-400/20 to-amber-500/20 blur-2xl"
></div>
<div
class=
"relative z-10"
>
<!-- Icon and badge -->
<div
class=
"mb-3 flex items-center gap-2"
>
<div
class=
"flex h-10 w-10 items-center justify-center rounded-xl bg-gradient-to-br from-amber-500 to-orange-600 text-white shadow-lg shadow-amber-500/30"
>
<svg
class=
"h-5 w-5"
fill=
"none"
viewBox=
"0 0 24 24"
stroke=
"currentColor"
stroke-width=
"2"
>
<path
stroke-linecap=
"round"
stroke-linejoin=
"round"
d=
"M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"
/>
</svg>
</div>
<span
class=
"inline-flex items-center gap-1.5 rounded-lg bg-gradient-to-r from-amber-500 to-orange-600 px-2.5 py-1 text-xs font-medium text-white shadow-lg shadow-amber-500/30"
>
<span
class=
"relative flex h-2 w-2"
>
<span
class=
"absolute inline-flex h-full w-full animate-ping rounded-full bg-white opacity-75"
></span>
<span
class=
"relative inline-flex h-2 w-2 rounded-full bg-white"
></span>
</span>
{{
t
(
'
announcements.unread
'
)
}}
</span>
</div>
<!-- Title -->
<h2
class=
"mb-2 text-2xl font-bold leading-tight text-gray-900 dark:text-white"
>
{{
announcementStore
.
currentPopup
.
title
}}
</h2>
<!-- Time -->
<div
class=
"flex items-center gap-1.5 text-sm text-gray-600 dark:text-gray-400"
>
<svg
class=
"h-4 w-4"
fill=
"none"
viewBox=
"0 0 24 24"
stroke=
"currentColor"
stroke-width=
"2"
>
<path
stroke-linecap=
"round"
stroke-linejoin=
"round"
d=
"M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<time>
{{
formatRelativeWithDateTime
(
announcementStore
.
currentPopup
.
created_at
)
}}
</time>
</div>
</div>
</div>
<!-- Body -->
<div
class=
"max-h-[50vh] overflow-y-auto bg-white px-8 py-8 dark:bg-dark-800"
>
<div
class=
"relative"
>
<div
class=
"absolute left-0 top-0 bottom-0 w-1 rounded-full bg-gradient-to-b from-amber-500 via-orange-500 to-yellow-500"
></div>
<div
class=
"pl-6"
>
<div
class=
"markdown-body prose prose-sm max-w-none dark:prose-invert"
v-html=
"renderedContent"
></div>
</div>
</div>
</div>
<!-- Footer -->
<div
class=
"border-t border-gray-100 bg-gray-50/50 px-8 py-5 dark:border-dark-700 dark:bg-dark-900/30"
>
<div
class=
"flex items-center justify-end"
>
<button
@
click=
"handleDismiss"
class=
"rounded-xl bg-gradient-to-r from-amber-500 to-orange-600 px-6 py-2.5 text-sm font-medium text-white shadow-lg shadow-amber-500/30 transition-all hover:shadow-xl hover:scale-105"
>
<span
class=
"flex items-center gap-2"
>
<svg
class=
"h-4 w-4"
fill=
"none"
viewBox=
"0 0 24 24"
stroke=
"currentColor"
stroke-width=
"2"
>
<path
stroke-linecap=
"round"
stroke-linejoin=
"round"
d=
"M5 13l4 4L19 7"
/>
</svg>
{{
t
(
'
announcements.markRead
'
)
}}
</span>
</button>
</div>
</div>
</div>
</div>
</Transition>
</Teleport>
</
template
>
<
script
setup
lang=
"ts"
>
import
{
computed
,
watch
}
from
'
vue
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
marked
}
from
'
marked
'
import
DOMPurify
from
'
dompurify
'
import
{
useAnnouncementStore
}
from
'
@/stores/announcements
'
import
{
formatRelativeWithDateTime
}
from
'
@/utils/format
'
const
{
t
}
=
useI18n
()
const
announcementStore
=
useAnnouncementStore
()
marked
.
setOptions
({
breaks
:
true
,
gfm
:
true
,
})
const
renderedContent
=
computed
(()
=>
{
const
content
=
announcementStore
.
currentPopup
?.
content
if
(
!
content
)
return
''
const
html
=
marked
.
parse
(
content
)
as
string
return
DOMPurify
.
sanitize
(
html
)
})
function
handleDismiss
()
{
announcementStore
.
dismissPopup
()
}
// Manage body overflow — only set, never unset (bell component handles restore)
watch
(
()
=>
announcementStore
.
currentPopup
,
(
popup
)
=>
{
if
(
popup
)
{
document
.
body
.
style
.
overflow
=
'
hidden
'
}
}
)
</
script
>
<
style
scoped
>
.popup-fade-enter-active
{
transition
:
all
0.3s
cubic-bezier
(
0.16
,
1
,
0.3
,
1
);
}
.popup-fade-leave-active
{
transition
:
all
0.2s
cubic-bezier
(
0.4
,
0
,
1
,
1
);
}
.popup-fade-enter-from
,
.popup-fade-leave-to
{
opacity
:
0
;
}
.popup-fade-enter-from
>
div
{
transform
:
scale
(
0.94
)
translateY
(
-12px
);
opacity
:
0
;
}
.popup-fade-leave-to
>
div
{
transform
:
scale
(
0.96
)
translateY
(
-8px
);
opacity
:
0
;
}
/* Scrollbar Styling */
.overflow-y-auto
::-webkit-scrollbar
{
width
:
8px
;
}
.overflow-y-auto
::-webkit-scrollbar-track
{
background
:
transparent
;
}
.overflow-y-auto
::-webkit-scrollbar-thumb
{
background
:
linear-gradient
(
to
bottom
,
#cbd5e1
,
#94a3b8
);
border-radius
:
4px
;
}
.dark
.overflow-y-auto
::-webkit-scrollbar-thumb
{
background
:
linear-gradient
(
to
bottom
,
#4b5563
,
#374151
);
}
</
style
>
frontend/src/i18n/locales/en.ts
View file @
7079edc2
...
@@ -2704,6 +2704,7 @@ export default {
...
@@ -2704,6 +2704,7 @@ export default {
columns
:
{
columns
:
{
title
:
'
Title
'
,
title
:
'
Title
'
,
status
:
'
Status
'
,
status
:
'
Status
'
,
notifyMode
:
'
Notify Mode
'
,
targeting
:
'
Targeting
'
,
targeting
:
'
Targeting
'
,
timeRange
:
'
Schedule
'
,
timeRange
:
'
Schedule
'
,
createdAt
:
'
Created At
'
,
createdAt
:
'
Created At
'
,
...
@@ -2714,10 +2715,16 @@ export default {
...
@@ -2714,10 +2715,16 @@ export default {
active
:
'
Active
'
,
active
:
'
Active
'
,
archived
:
'
Archived
'
archived
:
'
Archived
'
},
},
notifyModeLabels
:
{
silent
:
'
Silent
'
,
popup
:
'
Popup
'
},
form
:
{
form
:
{
title
:
'
Title
'
,
title
:
'
Title
'
,
content
:
'
Content (Markdown supported)
'
,
content
:
'
Content (Markdown supported)
'
,
status
:
'
Status
'
,
status
:
'
Status
'
,
notifyMode
:
'
Notify Mode
'
,
notifyModeHint
:
'
Popup mode will show a popup notification to users
'
,
startsAt
:
'
Starts At
'
,
startsAt
:
'
Starts At
'
,
endsAt
:
'
Ends At
'
,
endsAt
:
'
Ends At
'
,
startsAtHint
:
'
Leave empty to start immediately
'
,
startsAtHint
:
'
Leave empty to start immediately
'
,
...
...
Prev
1
2
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment