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 @@
+
+
+
+
+
+
+
+
+
用户列表
+
+
+
+
+
+
+
+
+
+
+ 未找到用户。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
![]()
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ formatDate(data.created_at) }}
+
+
+
+
+
+ {{ formatDate(data.updated_at) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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