diff --git a/backend/app/http/admin/provider.gen.go b/backend/app/http/admin/provider.gen.go index 1c41cc1..b967710 100755 --- a/backend/app/http/admin/provider.gen.go +++ b/backend/app/http/admin/provider.gen.go @@ -29,11 +29,13 @@ func Provide(opts ...opt.Option) error { medias *medias, posts *posts, uploads *uploads, + users *users, ) (contracts.HttpRoute, error) { obj := &Routes{ medias: medias, posts: posts, uploads: uploads, + users: users, } if err := obj.Prepare(); err != nil { return nil, err @@ -56,5 +58,12 @@ func Provide(opts ...opt.Option) error { }); err != nil { return err } + if err := container.Container.Provide(func() (*users, error) { + obj := &users{} + + return obj, nil + }); err != nil { + return err + } return nil } diff --git a/backend/app/http/admin/routes.gen.go b/backend/app/http/admin/routes.gen.go index 5a3da4e..b983a64 100644 --- a/backend/app/http/admin/routes.gen.go +++ b/backend/app/http/admin/routes.gen.go @@ -18,6 +18,7 @@ type Routes struct { medias *medias posts *posts uploads *uploads + users *users } func (r *Routes) Prepare() error { @@ -83,4 +84,11 @@ func (r *Routes) Register(router fiber.Router) { r.uploads.Token, )) + // 注册路由组: users + router.Get("/v1/admin/users", DataFunc2( + r.users.List, + Query[requests.Pagination]("pagination"), + Query[UserListQuery]("query"), + )) + } diff --git a/backend/app/http/admin/users.go b/backend/app/http/admin/users.go new file mode 100644 index 0000000..c6469b8 --- /dev/null +++ b/backend/app/http/admin/users.go @@ -0,0 +1,24 @@ +package admin + +import ( + "quyun/app/models" + "quyun/app/requests" + + "github.com/gofiber/fiber/v3" +) + +type UserListQuery struct { + Keyword *string `query:"keyword"` +} + +// @provider +type users struct{} + +// List users +// @Router /v1/admin/users [get] +// @Bind pagination query +// @Bind query query +func (ctl *users) List(ctx fiber.Ctx, pagination *requests.Pagination, query *UserListQuery) (*requests.Pager, error) { + cond := models.Users.BuildConditionWithKey(query.Keyword) + return models.Users.List(ctx.Context(), pagination, cond) +} diff --git a/backend/app/models/medias_test.go b/backend/app/models/medias_test.go index 1ea0a6b..36746bf 100644 --- a/backend/app/models/medias_test.go +++ b/backend/app/models/medias_test.go @@ -11,6 +11,7 @@ import ( "quyun/app/requests" "quyun/app/service/testx" "quyun/database" + "quyun/database/fields" "quyun/database/schemas/public/model" "quyun/database/schemas/public/table" @@ -195,19 +196,19 @@ func (s *MediasTestSuite) Test_ConvertFileTypeByMimeType() { Convey("image", func() { mimeType := "image/jpeg" fileType := Medias.ConvertFileTypeByMimeType(mimeType) - So(fileType, ShouldEqual, MediaTypeImage) + So(fileType, ShouldEqual, fields.MediaAssetTypeImage) }) Convey("video", func() { mimeType := "video/mp4" fileType := Medias.ConvertFileTypeByMimeType(mimeType) - So(fileType, ShouldEqual, MediaTypeVideo) + So(fileType, ShouldEqual, fields.MediaAssetTypeVideo) }) Convey("invalid mime type", func() { mimeType := "invalid/type" fileType := Medias.ConvertFileTypeByMimeType(mimeType) - So(fileType, ShouldEqual, MediaTypeUnknown) + So(fileType, ShouldEqual, fields.MediaAssetTypeUnknown) }) }) } diff --git a/backend/app/models/posts.go b/backend/app/models/posts.go index d7ead86..240b5b5 100644 --- a/backend/app/models/posts.go +++ b/backend/app/models/posts.go @@ -90,7 +90,7 @@ func (m *postsModel) Update(ctx context.Context, id int64, model *model.Posts) e model.UpdatedAt = time.Now() tbl := table.Posts - stmt := tbl.UPDATE(tbl.MutableColumns).MODEL(model).WHERE(tbl.ID.EQ(Int64(id))) + stmt := tbl.UPDATE(tbl.MutableColumns.Except(tbl.CreatedAt, tbl.DeletedAt)).MODEL(model).WHERE(tbl.ID.EQ(Int64(id))) m.log.Infof("sql: %s", stmt.DebugSql()) _, err := stmt.ExecContext(ctx, db) diff --git a/backend/app/models/users.go b/backend/app/models/users.go index c5fe6c4..c59c5d7 100644 --- a/backend/app/models/users.go +++ b/backend/app/models/users.go @@ -2,7 +2,9 @@ package models import ( "context" + "time" + "quyun/app/requests" "quyun/database/schemas/public/model" "quyun/database/schemas/public/table" @@ -75,3 +77,123 @@ func (m *usersModel) Posts(ctx context.Context, userID int64) ([]*model.Posts, e return posts, nil } + +// BuildConditionWithKey builds the WHERE clause for user queries +func (m *usersModel) BuildConditionWithKey(key *string) BoolExpression { + tbl := table.Users + + cond := tbl.DeletedAt.IS_NULL() + + if key == nil || *key == "" { + return cond + } + + cond = cond.AND( + tbl.Username.LIKE(String("%" + *key + "%")), + ) + + return cond +} + +// countByCondition counts users matching the given condition +func (m *usersModel) countByCondition(ctx context.Context, expr BoolExpression) (int64, error) { + var cnt struct { + Cnt int64 + } + + tbl := table.Users + stmt := SELECT(COUNT(tbl.ID).AS("cnt")).FROM(tbl).WHERE(expr) + m.log.Infof("sql: %s", stmt.DebugSql()) + + err := stmt.QueryContext(ctx, db, &cnt) + if err != nil { + m.log.Errorf("error counting users: %v", err) + return 0, err + } + + return cnt.Cnt, nil +} + +// List returns a paginated list of users +func (m *usersModel) List(ctx context.Context, pagination *requests.Pagination, cond BoolExpression) (*requests.Pager, error) { + pagination.Format() + + tbl := table.Users + stmt := tbl. + SELECT(tbl.AllColumns). + WHERE(cond). + ORDER_BY(tbl.ID.DESC()). + LIMIT(pagination.Limit). + OFFSET(pagination.Offset) + m.log.Infof("sql: %s", stmt.DebugSql()) + + var users []model.Users = make([]model.Users, 0) + err := stmt.QueryContext(ctx, db, &users) + if err != nil { + m.log.Errorf("error querying users: %v", err) + return nil, err + } + + count, err := m.countByCondition(ctx, cond) + if err != nil { + m.log.Errorf("error getting user count: %v", err) + return nil, err + } + + return &requests.Pager{ + Items: users, + Total: count, + Pagination: *pagination, + }, nil +} + +// Create creates a new user +func (m *usersModel) Create(ctx context.Context, model *model.Users) error { + model.CreatedAt = time.Now() + model.UpdatedAt = time.Now() + + tbl := table.Users + stmt := tbl.INSERT(tbl.MutableColumns).MODEL(model) + m.log.Infof("sql: %s", stmt.DebugSql()) + + _, err := stmt.ExecContext(ctx, db) + if err != nil { + m.log.Errorf("error creating user: %v", err) + return err + } + return nil +} + +// Update updates an existing user +func (m *usersModel) Update(ctx context.Context, id int64, model *model.Users) error { + model.UpdatedAt = time.Now() + + tbl := table.Users + stmt := tbl.UPDATE(tbl.MutableColumns.Except(tbl.CreatedAt, tbl.DeletedAt)).MODEL(model).WHERE(tbl.ID.EQ(Int64(id))) + m.log.Infof("sql: %s", stmt.DebugSql()) + + _, err := stmt.ExecContext(ctx, db) + if err != nil { + m.log.Errorf("error updating user: %v", err) + return err + } + return nil +} + +// DeleteByID soft deletes a user by ID +func (m *usersModel) DeleteByID(ctx context.Context, id int64) error { + tbl := table.Users + stmt := tbl. + UPDATE(tbl.DeletedAt). + SET(TimestampT(time.Now())). + WHERE( + tbl.ID.EQ(Int64(id)), + ) + m.log.Infof("sql: %s", stmt.DebugSql()) + + if _, err := stmt.ExecContext(ctx, db); err != nil { + m.log.Errorf("error deleting user: %v", err) + return err + } + return nil +} diff --git a/backend/app/models/users_test.go b/backend/app/models/users_test.go index 5dcf488..86e503a 100644 --- a/backend/app/models/users_test.go +++ b/backend/app/models/users_test.go @@ -6,12 +6,16 @@ import ( "quyun/app/service/testx" "quyun/database" + "quyun/database/fields" + "quyun/database/schemas/public/model" "quyun/database/schemas/public/table" + "github.com/samber/lo" . "github.com/smartystreets/goconvey/convey" "go.ipao.vip/atom/contracts" // . "github.com/go-jet/jet/v2/postgres" + gonanoid "github.com/matoous/go-nanoid/v2" "github.com/stretchr/testify/suite" "go.uber.org/dig" ) @@ -36,8 +40,25 @@ func Test_Users(t *testing.T) { }) } -func (s *UsersTestSuite) Test_Demo() { - Convey("Test_Demo", s.T(), func() { +func (s *UsersTestSuite) Test_BatchInsert() { + Convey("Test_BatchInsert", s.T(), func() { database.Truncate(context.Background(), db, table.Users.TableName()) + + count := 100 + for i := 0; i < count; i++ { + // generate openid use nanoid + openID, err := gonanoid.New(10) + So(err, ShouldBeNil) + + user := &model.Users{ + Status: fields.UserStatusOk, + OpenID: openID, + Username: "Username_" + openID, + Avatar: lo.ToPtr("Avatar_" + openID), + } + err = Users.Create(context.Background(), user) + So(err, ShouldBeNil) + + } }) } diff --git a/backend/database/fields/users.gen.go b/backend/database/fields/users.gen.go new file mode 100644 index 0000000..abfb8e9 --- /dev/null +++ b/backend/database/fields/users.gen.go @@ -0,0 +1,241 @@ +// Code generated by go-enum DO NOT EDIT. +// Version: - +// Revision: - +// Build Date: - +// Built By: - + +package fields + +import ( + "database/sql/driver" + "errors" + "fmt" + "strconv" + "strings" +) + +const ( + // UserStatusOk is a UserStatus of type Ok. + UserStatusOk UserStatus = iota + // UserStatusBanned is a UserStatus of type Banned. + UserStatusBanned + // UserStatusBlocked is a UserStatus of type Blocked. + UserStatusBlocked +) + +var ErrInvalidUserStatus = fmt.Errorf("not a valid UserStatus, try [%s]", strings.Join(_UserStatusNames, ", ")) + +const _UserStatusName = "okbannedblocked" + +var _UserStatusNames = []string{ + _UserStatusName[0:2], + _UserStatusName[2:8], + _UserStatusName[8:15], +} + +// UserStatusNames returns a list of possible string values of UserStatus. +func UserStatusNames() []string { + tmp := make([]string, len(_UserStatusNames)) + copy(tmp, _UserStatusNames) + return tmp +} + +// UserStatusValues returns a list of the values for UserStatus +func UserStatusValues() []UserStatus { + return []UserStatus{ + UserStatusOk, + UserStatusBanned, + UserStatusBlocked, + } +} + +var _UserStatusMap = map[UserStatus]string{ + UserStatusOk: _UserStatusName[0:2], + UserStatusBanned: _UserStatusName[2:8], + UserStatusBlocked: _UserStatusName[8:15], +} + +// String implements the Stringer interface. +func (x UserStatus) String() string { + if str, ok := _UserStatusMap[x]; ok { + return str + } + return fmt.Sprintf("UserStatus(%d)", x) +} + +// IsValid provides a quick way to determine if the typed value is +// part of the allowed enumerated values +func (x UserStatus) IsValid() bool { + _, ok := _UserStatusMap[x] + return ok +} + +var _UserStatusValue = map[string]UserStatus{ + _UserStatusName[0:2]: UserStatusOk, + _UserStatusName[2:8]: UserStatusBanned, + _UserStatusName[8:15]: UserStatusBlocked, +} + +// ParseUserStatus attempts to convert a string to a UserStatus. +func ParseUserStatus(name string) (UserStatus, error) { + if x, ok := _UserStatusValue[name]; ok { + return x, nil + } + return UserStatus(0), fmt.Errorf("%s is %w", name, ErrInvalidUserStatus) +} + +var errUserStatusNilPtr = errors.New("value pointer is nil") // one per type for package clashes + +// Scan implements the Scanner interface. +func (x *UserStatus) Scan(value interface{}) (err error) { + if value == nil { + *x = UserStatus(0) + return + } + + // A wider range of scannable types. + // driver.Value values at the top of the list for expediency + switch v := value.(type) { + case int64: + *x = UserStatus(v) + case string: + *x, err = ParseUserStatus(v) + if err != nil { + // try parsing the integer value as a string + if val, verr := strconv.Atoi(v); verr == nil { + *x, err = UserStatus(val), nil + } + } + case []byte: + *x, err = ParseUserStatus(string(v)) + if err != nil { + // try parsing the integer value as a string + if val, verr := strconv.Atoi(string(v)); verr == nil { + *x, err = UserStatus(val), nil + } + } + case UserStatus: + *x = v + case int: + *x = UserStatus(v) + case *UserStatus: + if v == nil { + return errUserStatusNilPtr + } + *x = *v + case uint: + *x = UserStatus(v) + case uint64: + *x = UserStatus(v) + case *int: + if v == nil { + return errUserStatusNilPtr + } + *x = UserStatus(*v) + case *int64: + if v == nil { + return errUserStatusNilPtr + } + *x = UserStatus(*v) + case float64: // json marshals everything as a float64 if it's a number + *x = UserStatus(v) + case *float64: // json marshals everything as a float64 if it's a number + if v == nil { + return errUserStatusNilPtr + } + *x = UserStatus(*v) + case *uint: + if v == nil { + return errUserStatusNilPtr + } + *x = UserStatus(*v) + case *uint64: + if v == nil { + return errUserStatusNilPtr + } + *x = UserStatus(*v) + case *string: + if v == nil { + return errUserStatusNilPtr + } + *x, err = ParseUserStatus(*v) + if err != nil { + // try parsing the integer value as a string + if val, verr := strconv.Atoi(*v); verr == nil { + *x, err = UserStatus(val), nil + } + } + } + + return +} + +// Value implements the driver Valuer interface. +func (x UserStatus) Value() (driver.Value, error) { + return int64(x), nil +} + +// Set implements the Golang flag.Value interface func. +func (x *UserStatus) Set(val string) error { + v, err := ParseUserStatus(val) + *x = v + return err +} + +// Get implements the Golang flag.Getter interface func. +func (x *UserStatus) Get() interface{} { + return *x +} + +// Type implements the github.com/spf13/pFlag Value interface. +func (x *UserStatus) Type() string { + return "UserStatus" +} + +type NullUserStatus struct { + UserStatus UserStatus + Valid bool +} + +func NewNullUserStatus(val interface{}) (x NullUserStatus) { + x.Scan(val) // yes, we ignore this error, it will just be an invalid value. + return +} + +// Scan implements the Scanner interface. +func (x *NullUserStatus) Scan(value interface{}) (err error) { + if value == nil { + x.UserStatus, x.Valid = UserStatus(0), false + return + } + + err = x.UserStatus.Scan(value) + x.Valid = (err == nil) + return +} + +// Value implements the driver Valuer interface. +func (x NullUserStatus) Value() (driver.Value, error) { + if !x.Valid { + return nil, nil + } + // driver.Value accepts int64 for int values. + return int64(x.UserStatus), nil +} + +type NullUserStatusStr struct { + NullUserStatus +} + +func NewNullUserStatusStr(val interface{}) (x NullUserStatusStr) { + x.Scan(val) // yes, we ignore this error, it will just be an invalid value. + return +} + +// Value implements the driver Valuer interface. +func (x NullUserStatusStr) Value() (driver.Value, error) { + if !x.Valid { + return nil, nil + } + return x.UserStatus.String(), nil +} diff --git a/backend/database/fields/users.go b/backend/database/fields/users.go new file mode 100644 index 0000000..331f71c --- /dev/null +++ b/backend/database/fields/users.go @@ -0,0 +1,5 @@ +package fields + +// swagger:enum PostStatus +// ENUM( ok, banned, blocked) +type UserStatus int16 diff --git a/backend/database/schemas/public/model/users.go b/backend/database/schemas/public/model/users.go index ef4f2c0..c1f732f 100644 --- a/backend/database/schemas/public/model/users.go +++ b/backend/database/schemas/public/model/users.go @@ -8,16 +8,17 @@ package model import ( + "quyun/database/fields" "time" ) type Users struct { - ID int64 `sql:"primary_key" json:"id"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - DeletedAt *time.Time `json:"deleted_at"` - Status int16 `json:"status"` - OpenID string `json:"open_id"` - Username string `json:"username"` - Avatar *string `json:"avatar"` + ID int64 `sql:"primary_key" json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt *time.Time `json:"deleted_at"` + Status fields.UserStatus `json:"status"` + OpenID string `json:"open_id"` + Username string `json:"username"` + Avatar *string `json:"avatar"` } diff --git a/backend/database/transform.yaml b/backend/database/transform.yaml index 3b93dbd..810a7b2 100644 --- a/backend/database/transform.yaml +++ b/backend/database/transform.yaml @@ -17,3 +17,6 @@ types: assets: Json[[]MediaAsset] tags: Json[[]string] meta: PostMeta + + users: + status: UserStatus diff --git a/backend/go.mod b/backend/go.mod index 6a28d40..2cdae7b 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -18,6 +18,7 @@ require ( github.com/jackc/pgx/v5 v5.7.2 github.com/juju/go4 v0.0.0-20160222163258-40d72ab9641a github.com/lib/pq v1.10.9 + github.com/matoous/go-nanoid/v2 v2.1.0 github.com/opentracing/opentracing-go v1.2.0 github.com/pkg/errors v0.9.1 github.com/pressly/goose/v3 v3.24.1 @@ -102,7 +103,6 @@ require ( github.com/jcmturner/gofork v1.7.6 // indirect github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect github.com/jcmturner/rpc/v2 v2.0.3 // indirect - github.com/jinzhu/copier v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/jtolds/gls v4.20.0+incompatible // indirect diff --git a/backend/go.sum b/backend/go.sum index 67c13d8..24d1815 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -168,8 +168,6 @@ github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh6 github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= -github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= -github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -194,6 +192,8 @@ github.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJV github.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/matoous/go-nanoid/v2 v2.1.0 h1:P64+dmq21hhWdtvZfEAofnvJULaRR1Yib0+PnU669bE= +github.com/matoous/go-nanoid/v2 v2.1.0/go.mod h1:KlbGNQ+FhrUNIHUxZdL63t7tl4LaPkZNpUULS8H4uVM= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= diff --git a/backend/test.http b/backend/test.http index a02f82c..ae3a6db 100644 --- a/backend/test.http +++ b/backend/test.http @@ -46,4 +46,8 @@ GET {{host}}/v1/admin/posts?page=1&limit=10&keyword=99123 HTTP/1.1 Content-Type: application/json ### delete posts -DELETE {{host}}/v1/admin/posts/103 HTTP/1.1 \ No newline at end of file +DELETE {{host}}/v1/admin/posts/103 HTTP/1.1 + +### get users +GET {{host}}/v1/admin/users HTTP/1.1 +Content-Type: application/json \ No newline at end of file diff --git a/frontend/admin/bun.lock b/frontend/admin/bun.lock index 2b26690..e5857d7 100644 --- a/frontend/admin/bun.lock +++ b/frontend/admin/bun.lock @@ -8,6 +8,9 @@ "@tailwindcss/vite": "^4.0.17", "axios": "^1.8.4", "daisyui": "^5.0.9", + "date-fns": "^4.1.0", + "dayjs": "^1.11.13", + "install": "^0.13.0", "primeicons": "^7.0.0", "primevue": "^4.3.3", "tailwindcss": "^4.0.17", @@ -198,6 +201,10 @@ "daisyui": ["daisyui@5.0.9", "https://registry.npmmirror.com/daisyui/-/daisyui-5.0.9.tgz", {}, "sha512-RsaehHh45f+0shWgZZaOY09/8eOae2voRsqJCD71j9yrnYgcke0Nj5ys0ZxrW4SPcc4+q96kWyJu0Z8P1zZdoA=="], + "date-fns": ["date-fns@4.1.0", "https://registry.npmmirror.com/date-fns/-/date-fns-4.1.0.tgz", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="], + + "dayjs": ["dayjs@1.11.13", "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.13.tgz", {}, "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg=="], + "delayed-stream": ["delayed-stream@1.0.0", "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], "detect-libc": ["detect-libc@2.0.3", "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.0.3.tgz", {}, "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw=="], @@ -242,6 +249,8 @@ "hasown": ["hasown@2.0.2", "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + "install": ["install@0.13.0", "https://registry.npmmirror.com/install/-/install-0.13.0.tgz", {}, "sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA=="], + "jiti": ["jiti@2.4.2", "https://registry.npmmirror.com/jiti/-/jiti-2.4.2.tgz", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="], "lightningcss": ["lightningcss@1.29.2", "https://registry.npmmirror.com/lightningcss/-/lightningcss-1.29.2.tgz", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.29.2", "lightningcss-darwin-x64": "1.29.2", "lightningcss-freebsd-x64": "1.29.2", "lightningcss-linux-arm-gnueabihf": "1.29.2", "lightningcss-linux-arm64-gnu": "1.29.2", "lightningcss-linux-arm64-musl": "1.29.2", "lightningcss-linux-x64-gnu": "1.29.2", "lightningcss-linux-x64-musl": "1.29.2", "lightningcss-win32-arm64-msvc": "1.29.2", "lightningcss-win32-x64-msvc": "1.29.2" } }, "sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA=="], diff --git a/frontend/admin/package.json b/frontend/admin/package.json index 489033a..24820a9 100644 --- a/frontend/admin/package.json +++ b/frontend/admin/package.json @@ -13,6 +13,9 @@ "@tailwindcss/vite": "^4.0.17", "axios": "^1.8.4", "daisyui": "^5.0.9", + "date-fns": "^4.1.0", + "dayjs": "^1.11.13", + "install": "^0.13.0", "primeicons": "^7.0.0", "primevue": "^4.3.3", "tailwindcss": "^4.0.17", diff --git a/frontend/admin/src/App.vue b/frontend/admin/src/App.vue index 38ee0db..6de9ba3 100644 --- a/frontend/admin/src/App.vue +++ b/frontend/admin/src/App.vue @@ -22,6 +22,11 @@ const navItems = ref([ icon: 'pi pi-file', command: () => router.push('/posts') }, + { + label: 'Usesrs', + icon: 'pi pi-users', + command: () => router.push('/users') + }, { label: 'Settings', icon: 'pi pi-cog', diff --git a/frontend/admin/src/api/userService.js b/frontend/admin/src/api/userService.js new file mode 100644 index 0000000..346bef7 --- /dev/null +++ b/frontend/admin/src/api/userService.js @@ -0,0 +1,19 @@ +import httpClient from './httpClient'; + +export const userService = { + getUsers({ page = 1, limit = 10, keyword = '' } = {}) { + return httpClient.get('/admin/users', { + params: { + page, + limit, + keyword: keyword.trim() + } + }); + }, + getUser(id) { + return httpClient.get(`/admin/users/${id}`); + }, + deleteUser(id) { + return httpClient.delete(`/admin/users/${id}`); + }, +} \ No newline at end of file diff --git a/frontend/admin/src/api/users_list.json b/frontend/admin/src/api/users_list.json new file mode 100644 index 0000000..8b4c946 --- /dev/null +++ b/frontend/admin/src/api/users_list.json @@ -0,0 +1,107 @@ +{ + "page": 1, + "limit": 10, + "total": 100, + "items": [ + { + "id": 100, + "created_at": "2025-04-10T20:38:25.224535Z", + "updated_at": "2025-04-10T20:38:25.224535Z", + "deleted_at": null, + "status": 0, + "open_id": "ovxaRhVa-t", + "username": "Username_ovxaRhVa-t", + "avatar": "Avatar_ovxaRhVa-t" + }, + { + "id": 99, + "created_at": "2025-04-10T20:38:25.220304Z", + "updated_at": "2025-04-10T20:38:25.220304Z", + "deleted_at": null, + "status": 0, + "open_id": "pnQlEwOawi", + "username": "Username_pnQlEwOawi", + "avatar": "Avatar_pnQlEwOawi" + }, + { + "id": 98, + "created_at": "2025-04-10T20:38:25.215726Z", + "updated_at": "2025-04-10T20:38:25.215726Z", + "deleted_at": null, + "status": 0, + "open_id": "NnjpNALmUE", + "username": "Username_NnjpNALmUE", + "avatar": "Avatar_NnjpNALmUE" + }, + { + "id": 97, + "created_at": "2025-04-10T20:38:25.211126Z", + "updated_at": "2025-04-10T20:38:25.211126Z", + "deleted_at": null, + "status": 0, + "open_id": "m3xb_ShqB8", + "username": "Username_m3xb_ShqB8", + "avatar": "Avatar_m3xb_ShqB8" + }, + { + "id": 96, + "created_at": "2025-04-10T20:38:25.204553Z", + "updated_at": "2025-04-10T20:38:25.204553Z", + "deleted_at": null, + "status": 0, + "open_id": "TDdXtAZQsQ", + "username": "Username_TDdXtAZQsQ", + "avatar": "Avatar_TDdXtAZQsQ" + }, + { + "id": 95, + "created_at": "2025-04-10T20:38:25.199782Z", + "updated_at": "2025-04-10T20:38:25.199783Z", + "deleted_at": null, + "status": 0, + "open_id": "BW9oaHl7at", + "username": "Username_BW9oaHl7at", + "avatar": "Avatar_BW9oaHl7at" + }, + { + "id": 94, + "created_at": "2025-04-10T20:38:25.194655Z", + "updated_at": "2025-04-10T20:38:25.194655Z", + "deleted_at": null, + "status": 0, + "open_id": "qL9iCPjSVh", + "username": "Username_qL9iCPjSVh", + "avatar": "Avatar_qL9iCPjSVh" + }, + { + "id": 93, + "created_at": "2025-04-10T20:38:25.190243Z", + "updated_at": "2025-04-10T20:38:25.190243Z", + "deleted_at": null, + "status": 0, + "open_id": "ctQtyed6Gt", + "username": "Username_ctQtyed6Gt", + "avatar": "Avatar_ctQtyed6Gt" + }, + { + "id": 92, + "created_at": "2025-04-10T20:38:25.186196Z", + "updated_at": "2025-04-10T20:38:25.186196Z", + "deleted_at": null, + "status": 0, + "open_id": "3Z4V0x6l_B", + "username": "Username_3Z4V0x6l_B", + "avatar": "Avatar_3Z4V0x6l_B" + }, + { + "id": 91, + "created_at": "2025-04-10T20:38:25.181205Z", + "updated_at": "2025-04-10T20:38:25.181205Z", + "deleted_at": null, + "status": 0, + "open_id": "kQwboErzWc", + "username": "Username_kQwboErzWc", + "avatar": "Avatar_kQwboErzWc" + } + ] +} \ No newline at end of file diff --git a/frontend/admin/src/pages/PostEditPage.vue b/frontend/admin/src/pages/PostEditPage.vue index 78273f3..014906a 100644 --- a/frontend/admin/src/pages/PostEditPage.vue +++ b/frontend/admin/src/pages/PostEditPage.vue @@ -66,48 +66,7 @@ const mediaTotalPages = computed(() => { }); // Sample media data - in a real app, this would come from an API -const mediaItems = ref([ - { - id: 1, - fileName: 'sunset-beach.jpg', - fileType: 'Image', - thumbnailUrl: 'https://via.placeholder.com/300x225', - fileSize: '2.4 MB', - uploadTime: 'Today, 10:30 AM' - }, - { - id: 2, - fileName: 'presentation.pdf', - fileType: 'PDF', - thumbnailUrl: null, - fileSize: '4.8 MB', - uploadTime: 'Yesterday, 3:45 PM' - }, - { - id: 3, - fileName: 'promo_video.mp4', - fileType: 'Video', - thumbnailUrl: null, - fileSize: '24.8 MB', - uploadTime: 'Aug 28, 2023' - }, - { - id: 4, - fileName: 'report_q3.docx', - fileType: 'Document', - thumbnailUrl: null, - fileSize: '1.2 MB', - uploadTime: 'Aug 25, 2023' - }, - { - id: 5, - fileName: 'podcast_interview.mp3', - fileType: 'Audio', - thumbnailUrl: null, - fileSize: '18.5 MB', - uploadTime: 'Aug 20, 2023' - } -]); +const mediaItems = ref([]); // Fetch post data by ID const fetchPost = async (id) => { diff --git a/frontend/admin/src/pages/UserPage.vue b/frontend/admin/src/pages/UserPage.vue new file mode 100644 index 0000000..3752992 --- /dev/null +++ b/frontend/admin/src/pages/UserPage.vue @@ -0,0 +1,191 @@ + + + diff --git a/frontend/admin/src/router.js b/frontend/admin/src/router.js index c341d6b..348cc24 100644 --- a/frontend/admin/src/router.js +++ b/frontend/admin/src/router.js @@ -35,6 +35,11 @@ const routes = [ component: () => import('./pages/PostEditPage.vue'), props: true }, + { + path: '/users', + name: 'Users', + component: () => import('./pages/UserPage.vue'), + } ]; // Create the router instance