Commit 7079edc2 authored by shaw's avatar shaw
Browse files

feat: announcement支持强制弹窗通知

parent a42a1f08
...@@ -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(", ")
......
...@@ -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()
......
...@@ -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))
......
...@@ -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) {
......
...@@ -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)
} }
......
...@@ -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]},
}, },
}, },
} }
......
...@@ -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, 10) fields := make([]string, 0, 11)
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, 31) fields := make([]string, 0, 30)
if m.created_at != nil { if m.created_at != nil {
fields = append(fields, group.FieldCreatedAt) fields = append(fields, group.FieldCreatedAt)
} }
......
...@@ -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.
......
...@@ -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"}).
......
...@@ -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=
......
...@@ -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 {
......
...@@ -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 {
......
...@@ -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,
} }
} }
...@@ -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,
} }
} }
......
...@@ -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
......
...@@ -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
}
}
<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>
...@@ -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>
......
<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>
...@@ -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',
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment