diff --git a/backend/ent/group.go b/backend/ent/group.go index 1eb05e0e..3c8d68b5 100644 --- a/backend/ent/group.go +++ b/backend/ent/group.go @@ -66,6 +66,8 @@ type Group struct { McpXMLInject bool `json:"mcp_xml_inject,omitempty"` // 支持的模型系列:claude, gemini_text, gemini_image SupportedModelScopes []string `json:"supported_model_scopes,omitempty"` + // 分组显示排序,数值越小越靠前 + SortOrder int `json:"sort_order,omitempty"` // Edges holds the relations/edges for other nodes in the graph. // The values are being populated by the GroupQuery when eager-loading is set. Edges GroupEdges `json:"edges"` @@ -178,7 +180,7 @@ func (*Group) scanValues(columns []string) ([]any, error) { values[i] = new(sql.NullBool) case group.FieldRateMultiplier, group.FieldDailyLimitUsd, group.FieldWeeklyLimitUsd, group.FieldMonthlyLimitUsd, group.FieldImagePrice1k, group.FieldImagePrice2k, group.FieldImagePrice4k: values[i] = new(sql.NullFloat64) - case group.FieldID, group.FieldDefaultValidityDays, group.FieldFallbackGroupID, group.FieldFallbackGroupIDOnInvalidRequest: + case group.FieldID, group.FieldDefaultValidityDays, group.FieldFallbackGroupID, group.FieldFallbackGroupIDOnInvalidRequest, group.FieldSortOrder: values[i] = new(sql.NullInt64) case group.FieldName, group.FieldDescription, group.FieldStatus, group.FieldPlatform, group.FieldSubscriptionType: values[i] = new(sql.NullString) @@ -363,6 +365,12 @@ func (_m *Group) assignValues(columns []string, values []any) error { return fmt.Errorf("unmarshal field supported_model_scopes: %w", err) } } + case group.FieldSortOrder: + if value, ok := values[i].(*sql.NullInt64); !ok { + return fmt.Errorf("unexpected type %T for field sort_order", values[i]) + } else if value.Valid { + _m.SortOrder = int(value.Int64) + } default: _m.selectValues.Set(columns[i], values[i]) } @@ -530,6 +538,9 @@ func (_m *Group) String() string { builder.WriteString(", ") builder.WriteString("supported_model_scopes=") builder.WriteString(fmt.Sprintf("%v", _m.SupportedModelScopes)) + builder.WriteString(", ") + builder.WriteString("sort_order=") + builder.WriteString(fmt.Sprintf("%v", _m.SortOrder)) builder.WriteByte(')') return builder.String() } diff --git a/backend/ent/group/group.go b/backend/ent/group/group.go index 278b2daf..31c67756 100644 --- a/backend/ent/group/group.go +++ b/backend/ent/group/group.go @@ -63,6 +63,8 @@ const ( FieldMcpXMLInject = "mcp_xml_inject" // FieldSupportedModelScopes holds the string denoting the supported_model_scopes field in the database. FieldSupportedModelScopes = "supported_model_scopes" + // FieldSortOrder holds the string denoting the sort_order field in the database. + FieldSortOrder = "sort_order" // EdgeAPIKeys holds the string denoting the api_keys edge name in mutations. EdgeAPIKeys = "api_keys" // EdgeRedeemCodes holds the string denoting the redeem_codes edge name in mutations. @@ -162,6 +164,7 @@ var Columns = []string{ FieldModelRoutingEnabled, FieldMcpXMLInject, FieldSupportedModelScopes, + FieldSortOrder, } var ( @@ -225,6 +228,8 @@ var ( DefaultMcpXMLInject bool // DefaultSupportedModelScopes holds the default value on creation for the "supported_model_scopes" field. DefaultSupportedModelScopes []string + // DefaultSortOrder holds the default value on creation for the "sort_order" field. + DefaultSortOrder int ) // OrderOption defines the ordering options for the Group queries. @@ -345,6 +350,11 @@ func ByMcpXMLInject(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldMcpXMLInject, opts...).ToFunc() } +// BySortOrder orders the results by the sort_order field. +func BySortOrder(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldSortOrder, opts...).ToFunc() +} + // ByAPIKeysCount orders the results by api_keys count. func ByAPIKeysCount(opts ...sql.OrderTermOption) OrderOption { return func(s *sql.Selector) { diff --git a/backend/ent/group/where.go b/backend/ent/group/where.go index b6fa2c33..cd5197c9 100644 --- a/backend/ent/group/where.go +++ b/backend/ent/group/where.go @@ -165,6 +165,11 @@ func McpXMLInject(v bool) predicate.Group { return predicate.Group(sql.FieldEQ(FieldMcpXMLInject, v)) } +// SortOrder applies equality check predicate on the "sort_order" field. It's identical to SortOrderEQ. +func SortOrder(v int) predicate.Group { + return predicate.Group(sql.FieldEQ(FieldSortOrder, v)) +} + // CreatedAtEQ applies the EQ predicate on the "created_at" field. func CreatedAtEQ(v time.Time) predicate.Group { return predicate.Group(sql.FieldEQ(FieldCreatedAt, v)) @@ -1160,6 +1165,46 @@ func McpXMLInjectNEQ(v bool) predicate.Group { return predicate.Group(sql.FieldNEQ(FieldMcpXMLInject, v)) } +// SortOrderEQ applies the EQ predicate on the "sort_order" field. +func SortOrderEQ(v int) predicate.Group { + return predicate.Group(sql.FieldEQ(FieldSortOrder, v)) +} + +// SortOrderNEQ applies the NEQ predicate on the "sort_order" field. +func SortOrderNEQ(v int) predicate.Group { + return predicate.Group(sql.FieldNEQ(FieldSortOrder, v)) +} + +// SortOrderIn applies the In predicate on the "sort_order" field. +func SortOrderIn(vs ...int) predicate.Group { + return predicate.Group(sql.FieldIn(FieldSortOrder, vs...)) +} + +// SortOrderNotIn applies the NotIn predicate on the "sort_order" field. +func SortOrderNotIn(vs ...int) predicate.Group { + return predicate.Group(sql.FieldNotIn(FieldSortOrder, vs...)) +} + +// SortOrderGT applies the GT predicate on the "sort_order" field. +func SortOrderGT(v int) predicate.Group { + return predicate.Group(sql.FieldGT(FieldSortOrder, v)) +} + +// SortOrderGTE applies the GTE predicate on the "sort_order" field. +func SortOrderGTE(v int) predicate.Group { + return predicate.Group(sql.FieldGTE(FieldSortOrder, v)) +} + +// SortOrderLT applies the LT predicate on the "sort_order" field. +func SortOrderLT(v int) predicate.Group { + return predicate.Group(sql.FieldLT(FieldSortOrder, v)) +} + +// SortOrderLTE applies the LTE predicate on the "sort_order" field. +func SortOrderLTE(v int) predicate.Group { + return predicate.Group(sql.FieldLTE(FieldSortOrder, v)) +} + // HasAPIKeys applies the HasEdge predicate on the "api_keys" edge. func HasAPIKeys() predicate.Group { return predicate.Group(func(s *sql.Selector) { diff --git a/backend/ent/group_create.go b/backend/ent/group_create.go index 9d845b61..707600a7 100644 --- a/backend/ent/group_create.go +++ b/backend/ent/group_create.go @@ -340,6 +340,20 @@ func (_c *GroupCreate) SetSupportedModelScopes(v []string) *GroupCreate { return _c } +// SetSortOrder sets the "sort_order" field. +func (_c *GroupCreate) SetSortOrder(v int) *GroupCreate { + _c.mutation.SetSortOrder(v) + return _c +} + +// SetNillableSortOrder sets the "sort_order" field if the given value is not nil. +func (_c *GroupCreate) SetNillableSortOrder(v *int) *GroupCreate { + if v != nil { + _c.SetSortOrder(*v) + } + return _c +} + // AddAPIKeyIDs adds the "api_keys" edge to the APIKey entity by IDs. func (_c *GroupCreate) AddAPIKeyIDs(ids ...int64) *GroupCreate { _c.mutation.AddAPIKeyIDs(ids...) @@ -521,6 +535,10 @@ func (_c *GroupCreate) defaults() error { v := group.DefaultSupportedModelScopes _c.mutation.SetSupportedModelScopes(v) } + if _, ok := _c.mutation.SortOrder(); !ok { + v := group.DefaultSortOrder + _c.mutation.SetSortOrder(v) + } return nil } @@ -585,6 +603,9 @@ func (_c *GroupCreate) check() error { if _, ok := _c.mutation.SupportedModelScopes(); !ok { return &ValidationError{Name: "supported_model_scopes", err: errors.New(`ent: missing required field "Group.supported_model_scopes"`)} } + if _, ok := _c.mutation.SortOrder(); !ok { + return &ValidationError{Name: "sort_order", err: errors.New(`ent: missing required field "Group.sort_order"`)} + } return nil } @@ -708,6 +729,10 @@ func (_c *GroupCreate) createSpec() (*Group, *sqlgraph.CreateSpec) { _spec.SetField(group.FieldSupportedModelScopes, field.TypeJSON, value) _node.SupportedModelScopes = value } + if value, ok := _c.mutation.SortOrder(); ok { + _spec.SetField(group.FieldSortOrder, field.TypeInt, value) + _node.SortOrder = value + } if nodes := _c.mutation.APIKeysIDs(); len(nodes) > 0 { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.O2M, @@ -1266,6 +1291,24 @@ func (u *GroupUpsert) UpdateSupportedModelScopes() *GroupUpsert { return u } +// SetSortOrder sets the "sort_order" field. +func (u *GroupUpsert) SetSortOrder(v int) *GroupUpsert { + u.Set(group.FieldSortOrder, v) + return u +} + +// UpdateSortOrder sets the "sort_order" field to the value that was provided on create. +func (u *GroupUpsert) UpdateSortOrder() *GroupUpsert { + u.SetExcluded(group.FieldSortOrder) + return u +} + +// AddSortOrder adds v to the "sort_order" field. +func (u *GroupUpsert) AddSortOrder(v int) *GroupUpsert { + u.Add(group.FieldSortOrder, v) + return u +} + // UpdateNewValues updates the mutable fields using the new values that were set on create. // Using this option is equivalent to using: // @@ -1780,6 +1823,27 @@ func (u *GroupUpsertOne) UpdateSupportedModelScopes() *GroupUpsertOne { }) } +// SetSortOrder sets the "sort_order" field. +func (u *GroupUpsertOne) SetSortOrder(v int) *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.SetSortOrder(v) + }) +} + +// AddSortOrder adds v to the "sort_order" field. +func (u *GroupUpsertOne) AddSortOrder(v int) *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.AddSortOrder(v) + }) +} + +// UpdateSortOrder sets the "sort_order" field to the value that was provided on create. +func (u *GroupUpsertOne) UpdateSortOrder() *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.UpdateSortOrder() + }) +} + // Exec executes the query. func (u *GroupUpsertOne) Exec(ctx context.Context) error { if len(u.create.conflict) == 0 { @@ -2460,6 +2524,27 @@ func (u *GroupUpsertBulk) UpdateSupportedModelScopes() *GroupUpsertBulk { }) } +// SetSortOrder sets the "sort_order" field. +func (u *GroupUpsertBulk) SetSortOrder(v int) *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.SetSortOrder(v) + }) +} + +// AddSortOrder adds v to the "sort_order" field. +func (u *GroupUpsertBulk) AddSortOrder(v int) *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.AddSortOrder(v) + }) +} + +// UpdateSortOrder sets the "sort_order" field to the value that was provided on create. +func (u *GroupUpsertBulk) UpdateSortOrder() *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.UpdateSortOrder() + }) +} + // Exec executes the query. func (u *GroupUpsertBulk) Exec(ctx context.Context) error { if u.create.err != nil { diff --git a/backend/ent/group_update.go b/backend/ent/group_update.go index 9e7246ea..393fd304 100644 --- a/backend/ent/group_update.go +++ b/backend/ent/group_update.go @@ -475,6 +475,27 @@ func (_u *GroupUpdate) AppendSupportedModelScopes(v []string) *GroupUpdate { return _u } +// SetSortOrder sets the "sort_order" field. +func (_u *GroupUpdate) SetSortOrder(v int) *GroupUpdate { + _u.mutation.ResetSortOrder() + _u.mutation.SetSortOrder(v) + return _u +} + +// SetNillableSortOrder sets the "sort_order" field if the given value is not nil. +func (_u *GroupUpdate) SetNillableSortOrder(v *int) *GroupUpdate { + if v != nil { + _u.SetSortOrder(*v) + } + return _u +} + +// AddSortOrder adds value to the "sort_order" field. +func (_u *GroupUpdate) AddSortOrder(v int) *GroupUpdate { + _u.mutation.AddSortOrder(v) + return _u +} + // AddAPIKeyIDs adds the "api_keys" edge to the APIKey entity by IDs. func (_u *GroupUpdate) AddAPIKeyIDs(ids ...int64) *GroupUpdate { _u.mutation.AddAPIKeyIDs(ids...) @@ -912,6 +933,12 @@ func (_u *GroupUpdate) sqlSave(ctx context.Context) (_node int, err error) { sqljson.Append(u, group.FieldSupportedModelScopes, value) }) } + if value, ok := _u.mutation.SortOrder(); ok { + _spec.SetField(group.FieldSortOrder, field.TypeInt, value) + } + if value, ok := _u.mutation.AddedSortOrder(); ok { + _spec.AddField(group.FieldSortOrder, field.TypeInt, value) + } if _u.mutation.APIKeysCleared() { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.O2M, @@ -1666,6 +1693,27 @@ func (_u *GroupUpdateOne) AppendSupportedModelScopes(v []string) *GroupUpdateOne return _u } +// SetSortOrder sets the "sort_order" field. +func (_u *GroupUpdateOne) SetSortOrder(v int) *GroupUpdateOne { + _u.mutation.ResetSortOrder() + _u.mutation.SetSortOrder(v) + return _u +} + +// SetNillableSortOrder sets the "sort_order" field if the given value is not nil. +func (_u *GroupUpdateOne) SetNillableSortOrder(v *int) *GroupUpdateOne { + if v != nil { + _u.SetSortOrder(*v) + } + return _u +} + +// AddSortOrder adds value to the "sort_order" field. +func (_u *GroupUpdateOne) AddSortOrder(v int) *GroupUpdateOne { + _u.mutation.AddSortOrder(v) + return _u +} + // AddAPIKeyIDs adds the "api_keys" edge to the APIKey entity by IDs. func (_u *GroupUpdateOne) AddAPIKeyIDs(ids ...int64) *GroupUpdateOne { _u.mutation.AddAPIKeyIDs(ids...) @@ -2133,6 +2181,12 @@ func (_u *GroupUpdateOne) sqlSave(ctx context.Context) (_node *Group, err error) sqljson.Append(u, group.FieldSupportedModelScopes, value) }) } + if value, ok := _u.mutation.SortOrder(); ok { + _spec.SetField(group.FieldSortOrder, field.TypeInt, value) + } + if value, ok := _u.mutation.AddedSortOrder(); ok { + _spec.AddField(group.FieldSortOrder, field.TypeInt, value) + } if _u.mutation.APIKeysCleared() { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.O2M, diff --git a/backend/ent/migrate/schema.go b/backend/ent/migrate/schema.go index f9e90d73..cfd4a72b 100644 --- a/backend/ent/migrate/schema.go +++ b/backend/ent/migrate/schema.go @@ -372,6 +372,7 @@ var ( {Name: "model_routing_enabled", Type: field.TypeBool, Default: false}, {Name: "mcp_xml_inject", Type: field.TypeBool, Default: true}, {Name: "supported_model_scopes", Type: field.TypeJSON, SchemaType: map[string]string{"postgres": "jsonb"}}, + {Name: "sort_order", Type: field.TypeInt, Default: 0}, } // GroupsTable holds the schema information for the "groups" table. GroupsTable = &schema.Table{ @@ -404,6 +405,11 @@ var ( Unique: false, Columns: []*schema.Column{GroupsColumns[3]}, }, + { + Name: "group_sort_order", + Unique: false, + Columns: []*schema.Column{GroupsColumns[25]}, + }, }, } // PromoCodesColumns holds the columns for the "promo_codes" table. diff --git a/backend/ent/mutation.go b/backend/ent/mutation.go index 5c182dea..969d9357 100644 --- a/backend/ent/mutation.go +++ b/backend/ent/mutation.go @@ -7059,6 +7059,8 @@ type GroupMutation struct { mcp_xml_inject *bool supported_model_scopes *[]string appendsupported_model_scopes []string + sort_order *int + addsort_order *int clearedFields map[string]struct{} api_keys map[int64]struct{} removedapi_keys map[int64]struct{} @@ -8411,6 +8413,62 @@ func (m *GroupMutation) ResetSupportedModelScopes() { m.appendsupported_model_scopes = nil } +// SetSortOrder sets the "sort_order" field. +func (m *GroupMutation) SetSortOrder(i int) { + m.sort_order = &i + m.addsort_order = nil +} + +// SortOrder returns the value of the "sort_order" field in the mutation. +func (m *GroupMutation) SortOrder() (r int, exists bool) { + v := m.sort_order + if v == nil { + return + } + return *v, true +} + +// OldSortOrder returns the old "sort_order" field's value of the Group entity. +// If the Group 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 *GroupMutation) OldSortOrder(ctx context.Context) (v int, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldSortOrder is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldSortOrder requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldSortOrder: %w", err) + } + return oldValue.SortOrder, nil +} + +// AddSortOrder adds i to the "sort_order" field. +func (m *GroupMutation) AddSortOrder(i int) { + if m.addsort_order != nil { + *m.addsort_order += i + } else { + m.addsort_order = &i + } +} + +// AddedSortOrder returns the value that was added to the "sort_order" field in this mutation. +func (m *GroupMutation) AddedSortOrder() (r int, exists bool) { + v := m.addsort_order + if v == nil { + return + } + return *v, true +} + +// ResetSortOrder resets all changes to the "sort_order" field. +func (m *GroupMutation) ResetSortOrder() { + m.sort_order = nil + m.addsort_order = nil +} + // AddAPIKeyIDs adds the "api_keys" edge to the APIKey entity by ids. func (m *GroupMutation) AddAPIKeyIDs(ids ...int64) { if m.api_keys == nil { @@ -8769,7 +8827,7 @@ func (m *GroupMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *GroupMutation) Fields() []string { - fields := make([]string, 0, 24) + fields := make([]string, 0, 25) if m.created_at != nil { fields = append(fields, group.FieldCreatedAt) } @@ -8842,6 +8900,9 @@ func (m *GroupMutation) Fields() []string { if m.supported_model_scopes != nil { fields = append(fields, group.FieldSupportedModelScopes) } + if m.sort_order != nil { + fields = append(fields, group.FieldSortOrder) + } return fields } @@ -8898,6 +8959,8 @@ func (m *GroupMutation) Field(name string) (ent.Value, bool) { return m.McpXMLInject() case group.FieldSupportedModelScopes: return m.SupportedModelScopes() + case group.FieldSortOrder: + return m.SortOrder() } return nil, false } @@ -8955,6 +9018,8 @@ func (m *GroupMutation) OldField(ctx context.Context, name string) (ent.Value, e return m.OldMcpXMLInject(ctx) case group.FieldSupportedModelScopes: return m.OldSupportedModelScopes(ctx) + case group.FieldSortOrder: + return m.OldSortOrder(ctx) } return nil, fmt.Errorf("unknown Group field %s", name) } @@ -9132,6 +9197,13 @@ func (m *GroupMutation) SetField(name string, value ent.Value) error { } m.SetSupportedModelScopes(v) return nil + case group.FieldSortOrder: + v, ok := value.(int) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetSortOrder(v) + return nil } return fmt.Errorf("unknown Group field %s", name) } @@ -9170,6 +9242,9 @@ func (m *GroupMutation) AddedFields() []string { if m.addfallback_group_id_on_invalid_request != nil { fields = append(fields, group.FieldFallbackGroupIDOnInvalidRequest) } + if m.addsort_order != nil { + fields = append(fields, group.FieldSortOrder) + } return fields } @@ -9198,6 +9273,8 @@ func (m *GroupMutation) AddedField(name string) (ent.Value, bool) { return m.AddedFallbackGroupID() case group.FieldFallbackGroupIDOnInvalidRequest: return m.AddedFallbackGroupIDOnInvalidRequest() + case group.FieldSortOrder: + return m.AddedSortOrder() } return nil, false } @@ -9277,6 +9354,13 @@ func (m *GroupMutation) AddField(name string, value ent.Value) error { } m.AddFallbackGroupIDOnInvalidRequest(v) return nil + case group.FieldSortOrder: + v, ok := value.(int) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.AddSortOrder(v) + return nil } return fmt.Errorf("unknown Group numeric field %s", name) } @@ -9445,6 +9529,9 @@ func (m *GroupMutation) ResetField(name string) error { case group.FieldSupportedModelScopes: m.ResetSupportedModelScopes() return nil + case group.FieldSortOrder: + m.ResetSortOrder() + return nil } return fmt.Errorf("unknown Group field %s", name) } diff --git a/backend/ent/runtime/runtime.go b/backend/ent/runtime/runtime.go index 4b3c1a4f..e5c34929 100644 --- a/backend/ent/runtime/runtime.go +++ b/backend/ent/runtime/runtime.go @@ -409,6 +409,10 @@ func init() { groupDescSupportedModelScopes := groupFields[20].Descriptor() // group.DefaultSupportedModelScopes holds the default value on creation for the supported_model_scopes field. group.DefaultSupportedModelScopes = groupDescSupportedModelScopes.Default.([]string) + // groupDescSortOrder is the schema descriptor for sort_order field. + groupDescSortOrder := groupFields[21].Descriptor() + // group.DefaultSortOrder holds the default value on creation for the sort_order field. + group.DefaultSortOrder = groupDescSortOrder.Default.(int) promocodeFields := schema.PromoCode{}.Fields() _ = promocodeFields // promocodeDescCode is the schema descriptor for code field. diff --git a/backend/ent/schema/group.go b/backend/ent/schema/group.go index 8a3c1a90..c36ca770 100644 --- a/backend/ent/schema/group.go +++ b/backend/ent/schema/group.go @@ -121,6 +121,11 @@ func (Group) Fields() []ent.Field { Default([]string{"claude", "gemini_text", "gemini_image"}). SchemaType(map[string]string{dialect.Postgres: "jsonb"}). Comment("支持的模型系列:claude, gemini_text, gemini_image"), + + // 分组排序 (added by migration 052) + field.Int("sort_order"). + Default(0). + Comment("分组显示排序,数值越小越靠前"), } } @@ -149,5 +154,6 @@ func (Group) Indexes() []ent.Index { index.Fields("subscription_type"), index.Fields("is_exclusive"), index.Fields("deleted_at"), + index.Fields("sort_order"), } } diff --git a/backend/go.sum b/backend/go.sum index 171995c7..90470fbc 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -135,6 +135,8 @@ github.com/icholy/digest v1.1.0 h1:HfGg9Irj7i+IX1o1QAmPfIBNu/Q5A5Tu3n/MED9k9H4= github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y= github.com/imroc/req/v3 v3.57.0 h1:LMTUjNRUybUkTPn8oJDq8Kg3JRBOBTcnDhKu7mzupKI= github.com/imroc/req/v3 v3.57.0/go.mod h1:JL62ey1nvSLq81HORNcosvlf7SxZStONNqOprg0Pz00= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= @@ -170,6 +172,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.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 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/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI= @@ -203,6 +207,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/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= 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/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= @@ -230,6 +236,8 @@ github.com/refraction-networking/utls v1.8.1 h1:yNY1kapmQU8JeM1sSw2H2asfTIwWxIkr github.com/refraction-networking/utls v1.8.1/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= @@ -252,6 +260,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/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= 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/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= diff --git a/backend/internal/handler/admin/admin_service_stub_test.go b/backend/internal/handler/admin/admin_service_stub_test.go index 77d288f9..cbbfe942 100644 --- a/backend/internal/handler/admin/admin_service_stub_test.go +++ b/backend/internal/handler/admin/admin_service_stub_test.go @@ -357,5 +357,9 @@ func (s *stubAdminService) GetUserBalanceHistory(ctx context.Context, userID int return s.redeems, int64(len(s.redeems)), 100.0, nil } +func (s *stubAdminService) UpdateGroupSortOrders(ctx context.Context, updates []service.GroupSortOrderUpdate) error { + return nil +} + // Ensure stub implements interface. var _ service.AdminService = (*stubAdminService)(nil) diff --git a/backend/internal/handler/admin/group_handler.go b/backend/internal/handler/admin/group_handler.go index d10d678b..7daaf281 100644 --- a/backend/internal/handler/admin/group_handler.go +++ b/backend/internal/handler/admin/group_handler.go @@ -302,3 +302,36 @@ func (h *GroupHandler) GetGroupAPIKeys(c *gin.Context) { } response.Paginated(c, outKeys, total, page, pageSize) } + +// UpdateSortOrderRequest represents the request to update group sort orders +type UpdateSortOrderRequest struct { + Updates []struct { + ID int64 `json:"id" binding:"required"` + SortOrder int `json:"sort_order"` + } `json:"updates" binding:"required,min=1"` +} + +// UpdateSortOrder handles updating group sort orders +// PUT /api/v1/admin/groups/sort-order +func (h *GroupHandler) UpdateSortOrder(c *gin.Context) { + var req UpdateSortOrderRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "Invalid request: "+err.Error()) + return + } + + updates := make([]service.GroupSortOrderUpdate, 0, len(req.Updates)) + for _, u := range req.Updates { + updates = append(updates, service.GroupSortOrderUpdate{ + ID: u.ID, + SortOrder: u.SortOrder, + }) + } + + if err := h.adminService.UpdateGroupSortOrders(c.Request.Context(), updates); err != nil { + response.ErrorFrom(c, err) + return + } + + response.Success(c, gin.H{"message": "Sort order updated successfully"}) +} diff --git a/backend/internal/handler/dto/mappers.go b/backend/internal/handler/dto/mappers.go index d14ab1d1..2caf6847 100644 --- a/backend/internal/handler/dto/mappers.go +++ b/backend/internal/handler/dto/mappers.go @@ -115,6 +115,7 @@ func GroupFromServiceAdmin(g *service.Group) *AdminGroup { MCPXMLInject: g.MCPXMLInject, SupportedModelScopes: g.SupportedModelScopes, AccountCount: g.AccountCount, + SortOrder: g.SortOrder, } if len(g.AccountGroups) > 0 { out.AccountGroups = make([]AccountGroup, 0, len(g.AccountGroups)) diff --git a/backend/internal/handler/dto/types.go b/backend/internal/handler/dto/types.go index 71bb1ed4..2338eb78 100644 --- a/backend/internal/handler/dto/types.go +++ b/backend/internal/handler/dto/types.go @@ -98,6 +98,9 @@ type AdminGroup struct { SupportedModelScopes []string `json:"supported_model_scopes"` AccountGroups []AccountGroup `json:"account_groups,omitempty"` AccountCount int64 `json:"account_count,omitempty"` + + // 分组排序 + SortOrder int `json:"sort_order"` } type Account struct { diff --git a/backend/internal/repository/api_key_repo.go b/backend/internal/repository/api_key_repo.go index c0cfd256..22dfa700 100644 --- a/backend/internal/repository/api_key_repo.go +++ b/backend/internal/repository/api_key_repo.go @@ -485,6 +485,7 @@ func groupEntityToService(g *dbent.Group) *service.Group { ModelRoutingEnabled: g.ModelRoutingEnabled, MCPXMLInject: g.McpXMLInject, SupportedModelScopes: g.SupportedModelScopes, + SortOrder: g.SortOrder, CreatedAt: g.CreatedAt, UpdatedAt: g.UpdatedAt, } diff --git a/backend/internal/repository/group_repo.go b/backend/internal/repository/group_repo.go index d8cec491..4e7a836f 100644 --- a/backend/internal/repository/group_repo.go +++ b/backend/internal/repository/group_repo.go @@ -191,7 +191,7 @@ func (r *groupRepository) ListWithFilters(ctx context.Context, params pagination groups, err := q. Offset(params.Offset()). Limit(params.Limit()). - Order(dbent.Asc(group.FieldID)). + Order(dbent.Asc(group.FieldSortOrder), dbent.Asc(group.FieldID)). All(ctx) if err != nil { return nil, nil, err @@ -218,7 +218,7 @@ func (r *groupRepository) ListWithFilters(ctx context.Context, params pagination func (r *groupRepository) ListActive(ctx context.Context) ([]service.Group, error) { groups, err := r.client.Group.Query(). Where(group.StatusEQ(service.StatusActive)). - Order(dbent.Asc(group.FieldID)). + Order(dbent.Asc(group.FieldSortOrder), dbent.Asc(group.FieldID)). All(ctx) if err != nil { return nil, err @@ -245,7 +245,7 @@ func (r *groupRepository) ListActive(ctx context.Context) ([]service.Group, erro func (r *groupRepository) ListActiveByPlatform(ctx context.Context, platform string) ([]service.Group, error) { groups, err := r.client.Group.Query(). Where(group.StatusEQ(service.StatusActive), group.PlatformEQ(platform)). - Order(dbent.Asc(group.FieldID)). + Order(dbent.Asc(group.FieldSortOrder), dbent.Asc(group.FieldID)). All(ctx) if err != nil { return nil, err @@ -497,3 +497,29 @@ func (r *groupRepository) BindAccountsToGroup(ctx context.Context, groupID int64 return nil } + +// UpdateSortOrders 批量更新分组排序 +func (r *groupRepository) UpdateSortOrders(ctx context.Context, updates []service.GroupSortOrderUpdate) error { + if len(updates) == 0 { + return nil + } + + // 使用事务批量更新 + tx, err := r.client.Tx(ctx) + if err != nil { + return err + } + defer func() { _ = tx.Rollback() }() + + for _, u := range updates { + if _, err := tx.Group.UpdateOneID(u.ID).SetSortOrder(u.SortOrder).Save(ctx); err != nil { + return translatePersistenceError(err, service.ErrGroupNotFound, nil) + } + } + + if err := tx.Commit(); err != nil { + return err + } + + return nil +} diff --git a/backend/internal/server/api_contract_test.go b/backend/internal/server/api_contract_test.go index efef0452..4ee8a6ee 100644 --- a/backend/internal/server/api_contract_test.go +++ b/backend/internal/server/api_contract_test.go @@ -896,6 +896,10 @@ func (stubGroupRepo) GetAccountIDsByGroupIDs(ctx context.Context, groupIDs []int return nil, errors.New("not implemented") } +func (stubGroupRepo) UpdateSortOrders(ctx context.Context, updates []service.GroupSortOrderUpdate) error { + return nil +} + type stubAccountRepo struct { bulkUpdateIDs []int64 } diff --git a/backend/internal/server/routes/admin.go b/backend/internal/server/routes/admin.go index 14815262..bd6788b2 100644 --- a/backend/internal/server/routes/admin.go +++ b/backend/internal/server/routes/admin.go @@ -192,6 +192,7 @@ func registerGroupRoutes(admin *gin.RouterGroup, h *handler.Handlers) { { groups.GET("", h.Admin.Group.List) groups.GET("/all", h.Admin.Group.GetAll) + groups.PUT("/sort-order", h.Admin.Group.UpdateSortOrder) groups.GET("/:id", h.Admin.Group.GetByID) groups.POST("", h.Admin.Group.Create) groups.PUT("/:id", h.Admin.Group.Update) diff --git a/backend/internal/service/admin_service.go b/backend/internal/service/admin_service.go index 59d7062b..06354e1e 100644 --- a/backend/internal/service/admin_service.go +++ b/backend/internal/service/admin_service.go @@ -36,6 +36,7 @@ type AdminService interface { UpdateGroup(ctx context.Context, id int64, input *UpdateGroupInput) (*Group, error) DeleteGroup(ctx context.Context, id int64) error GetGroupAPIKeys(ctx context.Context, groupID int64, page, pageSize int) ([]APIKey, int64, error) + UpdateGroupSortOrders(ctx context.Context, updates []GroupSortOrderUpdate) error // Account management ListAccounts(ctx context.Context, page, pageSize int, platform, accountType, status, search string) ([]Account, int64, error) @@ -1015,6 +1016,10 @@ func (s *adminServiceImpl) GetGroupAPIKeys(ctx context.Context, groupID int64, p return keys, result.Total, nil } +func (s *adminServiceImpl) UpdateGroupSortOrders(ctx context.Context, updates []GroupSortOrderUpdate) error { + return s.groupRepo.UpdateSortOrders(ctx, updates) +} + // Account management implementations func (s *adminServiceImpl) ListAccounts(ctx context.Context, page, pageSize int, platform, accountType, status, search string) ([]Account, int64, error) { params := pagination.PaginationParams{Page: page, PageSize: pageSize} diff --git a/backend/internal/service/admin_service_delete_test.go b/backend/internal/service/admin_service_delete_test.go index c775749d..60fa3d77 100644 --- a/backend/internal/service/admin_service_delete_test.go +++ b/backend/internal/service/admin_service_delete_test.go @@ -172,6 +172,10 @@ func (s *groupRepoStub) GetAccountIDsByGroupIDs(ctx context.Context, groupIDs [] panic("unexpected GetAccountIDsByGroupIDs call") } +func (s *groupRepoStub) UpdateSortOrders(ctx context.Context, updates []GroupSortOrderUpdate) error { + return nil +} + type proxyRepoStub struct { deleteErr error countErr error diff --git a/backend/internal/service/admin_service_group_test.go b/backend/internal/service/admin_service_group_test.go index d921a086..ef77a980 100644 --- a/backend/internal/service/admin_service_group_test.go +++ b/backend/internal/service/admin_service_group_test.go @@ -116,6 +116,10 @@ func (s *groupRepoStubForAdmin) GetAccountIDsByGroupIDs(_ context.Context, _ []i panic("unexpected GetAccountIDsByGroupIDs call") } +func (s *groupRepoStubForAdmin) UpdateSortOrders(_ context.Context, _ []GroupSortOrderUpdate) error { + return nil +} + // TestAdminService_CreateGroup_WithImagePricing 测试创建分组时 ImagePrice 字段正确传递 func TestAdminService_CreateGroup_WithImagePricing(t *testing.T) { repo := &groupRepoStubForAdmin{} @@ -395,6 +399,10 @@ func (s *groupRepoStubForFallbackCycle) GetAccountIDsByGroupIDs(_ context.Contex panic("unexpected GetAccountIDsByGroupIDs call") } +func (s *groupRepoStubForFallbackCycle) UpdateSortOrders(_ context.Context, _ []GroupSortOrderUpdate) error { + return nil +} + type groupRepoStubForInvalidRequestFallback struct { groups map[int64]*Group created *Group @@ -466,6 +474,10 @@ func (s *groupRepoStubForInvalidRequestFallback) BindAccountsToGroup(_ context.C panic("unexpected BindAccountsToGroup call") } +func (s *groupRepoStubForInvalidRequestFallback) UpdateSortOrders(_ context.Context, _ []GroupSortOrderUpdate) error { + return nil +} + func TestAdminService_CreateGroup_InvalidRequestFallbackRejectsUnsupportedPlatform(t *testing.T) { fallbackID := int64(10) repo := &groupRepoStubForInvalidRequestFallback{ diff --git a/backend/internal/service/gateway_multiplatform_test.go b/backend/internal/service/gateway_multiplatform_test.go index b3e60c21..67750a34 100644 --- a/backend/internal/service/gateway_multiplatform_test.go +++ b/backend/internal/service/gateway_multiplatform_test.go @@ -290,6 +290,10 @@ func (m *mockGroupRepoForGateway) GetAccountIDsByGroupIDs(ctx context.Context, g return nil, nil } +func (m *mockGroupRepoForGateway) UpdateSortOrders(ctx context.Context, updates []GroupSortOrderUpdate) error { + return nil +} + func ptr[T any](v T) *T { return &v } diff --git a/backend/internal/service/gemini_multiplatform_test.go b/backend/internal/service/gemini_multiplatform_test.go index 601e7e2c..e20e9d9f 100644 --- a/backend/internal/service/gemini_multiplatform_test.go +++ b/backend/internal/service/gemini_multiplatform_test.go @@ -226,6 +226,10 @@ func (m *mockGroupRepoForGemini) GetAccountIDsByGroupIDs(ctx context.Context, gr return nil, nil } +func (m *mockGroupRepoForGemini) UpdateSortOrders(ctx context.Context, updates []GroupSortOrderUpdate) error { + return nil +} + var _ GroupRepository = (*mockGroupRepoForGemini)(nil) // mockGatewayCacheForGemini Gemini 测试用的 cache mock diff --git a/backend/internal/service/group.go b/backend/internal/service/group.go index 1302047a..e9423ddb 100644 --- a/backend/internal/service/group.go +++ b/backend/internal/service/group.go @@ -45,6 +45,9 @@ type Group struct { // 可选值: claude, gemini_text, gemini_image SupportedModelScopes []string + // 分组排序 + SortOrder int + CreatedAt time.Time UpdatedAt time.Time diff --git a/backend/internal/service/group_service.go b/backend/internal/service/group_service.go index a2bf2073..22a67eda 100644 --- a/backend/internal/service/group_service.go +++ b/backend/internal/service/group_service.go @@ -33,6 +33,14 @@ type GroupRepository interface { GetAccountIDsByGroupIDs(ctx context.Context, groupIDs []int64) ([]int64, error) // BindAccountsToGroup 将多个账号绑定到指定分组 BindAccountsToGroup(ctx context.Context, groupID int64, accountIDs []int64) error + // UpdateSortOrders 批量更新分组排序 + UpdateSortOrders(ctx context.Context, updates []GroupSortOrderUpdate) error +} + +// GroupSortOrderUpdate 分组排序更新 +type GroupSortOrderUpdate struct { + ID int64 `json:"id"` + SortOrder int `json:"sort_order"` } // CreateGroupRequest 创建分组请求 diff --git a/backend/migrations/052_add_group_sort_order.sql b/backend/migrations/052_add_group_sort_order.sql new file mode 100644 index 00000000..ee687608 --- /dev/null +++ b/backend/migrations/052_add_group_sort_order.sql @@ -0,0 +1,8 @@ +-- Add sort_order field to groups table for custom ordering +ALTER TABLE groups ADD COLUMN IF NOT EXISTS sort_order INT NOT NULL DEFAULT 0; + +-- Initialize existing groups with sort_order based on their ID +UPDATE groups SET sort_order = id WHERE sort_order = 0; + +-- Create index for efficient sorting +CREATE INDEX IF NOT EXISTS idx_groups_sort_order ON groups(sort_order); diff --git a/frontend/package.json b/frontend/package.json index 38b92708..325eba60 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -27,6 +27,7 @@ "qrcode": "^1.5.4", "vue": "^3.4.0", "vue-chartjs": "^5.3.0", + "vue-draggable-plus": "^0.6.1", "vue-i18n": "^9.14.5", "vue-router": "^4.2.5", "xlsx": "^0.18.5" diff --git a/frontend/src/api/admin/groups.ts b/frontend/src/api/admin/groups.ts index 4d2b10ef..3d18ba87 100644 --- a/frontend/src/api/admin/groups.ts +++ b/frontend/src/api/admin/groups.ts @@ -153,6 +153,20 @@ export async function getGroupApiKeys( return data } +/** + * Update group sort orders + * @param updates - Array of { id, sort_order } objects + * @returns Success confirmation + */ +export async function updateSortOrder( + updates: Array<{ id: number; sort_order: number }> +): Promise<{ message: string }> { + const { data } = await apiClient.put<{ message: string }>('/admin/groups/sort-order', { + updates + }) + return data +} + export const groupsAPI = { list, getAll, @@ -163,7 +177,8 @@ export const groupsAPI = { delete: deleteGroup, toggleStatus, getStats, - getGroupApiKeys + getGroupApiKeys, + updateSortOrder } export default groupsAPI diff --git a/frontend/src/components/icons/Icon.vue b/frontend/src/components/icons/Icon.vue index 1f055111..382a35af 100644 --- a/frontend/src/components/icons/Icon.vue +++ b/frontend/src/components/icons/Icon.vue @@ -58,6 +58,7 @@ const icons = { arrowLeft: 'M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18', arrowUp: 'M5 10l7-7m0 0l7 7m-7-7v18', arrowDown: 'M19 14l-7 7m0 0l-7-7m7 7V3', + arrowsUpDown: 'M3 7.5L7.5 3m0 0L12 7.5M7.5 3v13.5m13.5 0L16.5 21m0 0L12 16.5m4.5 4.5V7.5', chevronUp: 'M5 15l7-7 7 7', externalLink: 'M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14', diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 1c71006d..7daaca73 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -1042,6 +1042,10 @@ export default { createGroup: 'Create Group', editGroup: 'Edit Group', deleteGroup: 'Delete Group', + sortOrder: 'Sort', + sortOrderHint: 'Drag groups to adjust display order, groups at the top will be displayed first', + sortOrderUpdated: 'Sort order updated', + failedToUpdateSortOrder: 'Failed to update sort order', allPlatforms: 'All Platforms', allStatus: 'All Status', allGroups: 'All Groups', diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index 5eef6243..5221bd8f 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -1099,6 +1099,10 @@ export default { createGroup: '创建分组', editGroup: '编辑分组', deleteGroup: '删除分组', + sortOrder: '排序', + sortOrderHint: '拖拽分组调整显示顺序,排在前面的分组会优先显示', + sortOrderUpdated: '排序已更新', + failedToUpdateSortOrder: '更新排序失败', deleteConfirm: "确定要删除分组 '{name}' 吗?所有关联的 API 密钥将不再属于任何分组。", deleteConfirmSubscription: "确定要删除订阅分组 '{name}' 吗?此操作会让所有绑定此订阅的用户的 API Key 失效,并删除所有相关的订阅记录。此操作无法撤销。", diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 84e3a380..115af34c 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -377,6 +377,9 @@ export interface AdminGroup extends Group { // 分组下账号数量(仅管理员可见) account_count?: number + + // 分组排序 + sort_order: number } export interface ApiKey { diff --git a/frontend/src/views/admin/GroupsView.vue b/frontend/src/views/admin/GroupsView.vue index 57cde914..00515ab7 100644 --- a/frontend/src/views/admin/GroupsView.vue +++ b/frontend/src/views/admin/GroupsView.vue @@ -52,6 +52,14 @@ > +