diff --git a/code/allapp/allapp-go/.idea/data_source_mapping.xml b/code/allapp/allapp-go/.idea/data_source_mapping.xml
index 1c9b86ee..e007eda8 100644
--- a/code/allapp/allapp-go/.idea/data_source_mapping.xml
+++ b/code/allapp/allapp-go/.idea/data_source_mapping.xml
@@ -4,6 +4,7 @@
+
\ No newline at end of file
diff --git a/code/allapp/allapp-go/.idea/sqldialects.xml b/code/allapp/allapp-go/.idea/sqldialects.xml
index e19bb375..79e0bd3d 100644
--- a/code/allapp/allapp-go/.idea/sqldialects.xml
+++ b/code/allapp/allapp-go/.idea/sqldialects.xml
@@ -3,6 +3,7 @@
+
diff --git a/code/allapp/allapp-go/api/handler/finance/category.go b/code/allapp/allapp-go/api/handler/finance/category.go
new file mode 100644
index 00000000..df3d1270
--- /dev/null
+++ b/code/allapp/allapp-go/api/handler/finance/category.go
@@ -0,0 +1,200 @@
+package finance
+
+import (
+ "allapp/api/router/middleware"
+ "allapp/api/types"
+ "allapp/db"
+ "allapp/db/repo"
+ "allapp/utils/errorx"
+ "allapp/utils/requestx"
+ "allapp/utils/responsex"
+ "allapp/utils/uniqueid"
+
+ "github.com/gofiber/fiber/v3"
+)
+
+// ListFinanceCategories 查看记账分类
+func ListFinanceCategories(c fiber.Ctx) error {
+ spaceId, err := middleware.GetSpaceID(c)
+
+ if err != nil {
+ return err
+ }
+
+ categories, err := db.Queries.ListFinanceCategories(c.Context(), spaceId)
+
+ if err != nil {
+ return err
+ }
+
+ financeCategories := make([]types.FinanceCategory, 0, len(categories))
+
+ for _, category := range categories {
+ financeCategories = append(financeCategories, types.FinanceCategory{
+ Id: category.ID,
+ Name: category.Name,
+ Icon: category.Icon,
+ Type: category.Type,
+ SortNumber: category.SortNumber,
+ })
+ }
+
+ vo := new(types.ListFinanceCategoriesVO)
+ vo.Categories = financeCategories
+
+ return responsex.OK(c, vo)
+}
+
+// CreateFinanceCategory 新建分类
+func CreateFinanceCategory(c fiber.Ctx) error {
+ dto := new(types.CreateFinanceCategoryDTO)
+ if err := requestx.BindAndValidate(c, dto); err != nil {
+ return err
+ }
+
+ userId, err := middleware.GetUserID(c)
+ if err != nil {
+ return err
+ }
+
+ spaceID, err2 := middleware.GetSpaceID(c)
+ if err2 != nil {
+ return err2
+ }
+
+ _, err = db.Queries.GetFinanceCategoryByName(c.Context(), repo.GetFinanceCategoryByNameParams{
+ SpaceID: spaceID,
+ Name: dto.Name,
+ Type: dto.Type,
+ })
+
+ isFound := true
+
+ if err != nil {
+ if errorx.IsNotFound(err) {
+ isFound = false
+ } else {
+ return err
+ }
+ }
+
+ if isFound {
+ return responsex.Error(c, "分类名称不能重复")
+ }
+
+ nextId := uniqueid.NextId()
+
+ err = db.Queries.InsertFinanceCategory(c.Context(), repo.InsertFinanceCategoryParams{
+ ID: nextId,
+ SpaceID: spaceID,
+ Name: dto.Name,
+ Icon: dto.Icon,
+ Type: dto.Type,
+ SortNumber: dto.SortNumber,
+ CreateBy: userId,
+ UpdateBy: userId,
+ })
+
+ if err != nil {
+ return err
+ }
+
+ vo := new(types.CreateFinanceCategoryVO)
+ vo.Id = nextId
+
+ return responsex.OK(c, vo)
+}
+
+// UpdateFinanceCategorySortNumber 更新记账分类顺序
+func UpdateFinanceCategorySortNumber(c fiber.Ctx) error {
+ dto := new(types.UpdateFinanceCategorySortDTO)
+ if err := requestx.BindAndValidate(c, dto); err != nil {
+ return err
+ }
+
+ userId, err := middleware.GetUserID(c)
+ if err != nil {
+ return err
+ }
+
+ // 组装批量更新参数
+ params := make([]repo.BatchUpdateFinanceCategorySortNumberParams, 0, len(dto.Categories))
+ for _, datum := range dto.Categories {
+ params = append(params, repo.BatchUpdateFinanceCategorySortNumberParams{
+ ID: datum.Id,
+ SortNumber: datum.SortNumber,
+ UpdateBy: userId,
+ })
+ }
+
+ // 使用统一事务封装
+ err = db.WithTx(c.Context(), func(q *repo.Queries) error {
+ batch := q.BatchUpdateFinanceCategorySortNumber(c.Context(), params)
+
+ if err := db.RunBatch(c.Context(), batch); err != nil {
+ return err
+ }
+
+ return nil
+ })
+
+ if err != nil {
+ return err
+ }
+
+ return responsex.OK(c, nil)
+}
+
+// DeleteFinanceCategory 删除记账分类
+func DeleteFinanceCategory(c fiber.Ctx) error {
+ dto := new(types.DeleteFinanceCategoryDTO)
+ if err := requestx.BindAndValidate(c, dto); err != nil {
+ return err
+ }
+
+ err := db.WithTx(c.Context(), func(q *repo.Queries) error {
+ err := q.DeleteFinanceCategory(c.Context(), dto.Id)
+ if err != nil {
+ return err
+ }
+
+ err = q.DeleteFinanceRecordByCategoryId(c.Context(), dto.Id)
+ if err != nil {
+ return err
+ }
+
+ return nil
+ })
+
+ if err != nil {
+ return err
+ }
+
+ return responsex.OK(c, nil)
+}
+
+// UpdateFinanceCategory 更新记账分类
+func UpdateFinanceCategory(c fiber.Ctx) error {
+ dto := new(types.UpdateFinanceCategoryDTO)
+ if err := requestx.BindAndValidate(c, dto); err != nil {
+ return err
+ }
+
+ userId, err := middleware.GetUserID(c)
+ if err != nil {
+ return err
+ }
+
+ err = db.Queries.UpdateFinanceCategory(c.Context(), repo.UpdateFinanceCategoryParams{
+ ID: dto.Id,
+ Name: dto.Name,
+ Icon: dto.Icon,
+ UpdateBy: userId,
+ })
+
+ if err != nil {
+ return err
+ }
+
+ return responsex.OK(c, nil)
+}
diff --git a/code/allapp/allapp-go/api/handler/finance/record.go b/code/allapp/allapp-go/api/handler/finance/record.go
new file mode 100644
index 00000000..d811bfc5
--- /dev/null
+++ b/code/allapp/allapp-go/api/handler/finance/record.go
@@ -0,0 +1,163 @@
+package finance
+
+import (
+ "allapp/api/router/middleware"
+ "allapp/api/types"
+ "allapp/db"
+ "allapp/db/repo"
+ "allapp/utils/pgtypex"
+ "allapp/utils/requestx"
+ "allapp/utils/responsex"
+ "allapp/utils/timex"
+ "allapp/utils/uniqueid"
+ "time"
+
+ "github.com/gofiber/fiber/v3"
+)
+
+// ListFinanceRecords 记账记录
+func ListFinanceRecords(c fiber.Ctx) error {
+ dto := new(types.ListFinanceRecordsDTO)
+ if err := requestx.BindAndValidate(c, dto); err != nil {
+ return err
+ }
+
+ spaceID, err := middleware.GetSpaceID(c)
+ if err != nil {
+ return err
+ }
+
+ // 解析年月(yyyy-MM)
+ startTime, err := timex.ParseTimeFlexible(dto.StartTime)
+ if err != nil {
+ return err
+ }
+
+ endTime, err := timex.ParseTimeFlexible(dto.EndTime)
+ if err != nil {
+ return err
+ }
+
+ // 开始时间兜底
+ if startTime == nil {
+ t := time.Unix(0, 0)
+ startTime = &t
+ }
+
+ // 结束时间兜底
+ if endTime == nil {
+ t := time.Now().AddDate(10, 0, 0)
+ endTime = &t
+ }
+
+ categoryId := dto.CategoryId
+
+ records, err := db.Queries.ListFinanceRecords(c.Context(), repo.ListFinanceRecordsParams{
+ SpaceID: spaceID,
+ RecordTime: pgtypex.TimePtrToPgDate(startTime),
+ RecordTime_2: pgtypex.TimePtrToPgDate(endTime),
+ Column4: categoryId,
+ })
+
+ if err != nil {
+ return err
+ }
+
+ financeRecords := make([]types.FinanceRecord, 0, len(records))
+
+ for _, record := range records {
+ financeRecords = append(financeRecords, types.FinanceRecord{
+ Id: record.ID,
+ CategoryId: record.CategoryID,
+ Amount: pgtypex.NumericToString(record.Amount),
+ Remark: pgtypex.TextToString(record.Remark),
+ RecordTime: pgtypex.DateToString(record.RecordTime),
+ MemberId: record.UpdateBy,
+ MemberName: pgtypex.TextToString(record.MemberName),
+ })
+ }
+
+ vo := new(types.ListFinanceRecordsVO)
+ vo.Records = financeRecords
+
+ return responsex.OK(c, vo)
+}
+
+// CreateFinanceRecord 新增记账记录
+func CreateFinanceRecord(c fiber.Ctx) error {
+ dto := new(types.CreateFinanceRecordDTO)
+ if err := requestx.BindAndValidate(c, dto); err != nil {
+ return err
+ }
+
+ spaceID, err := middleware.GetSpaceID(c)
+ if err != nil {
+ return err
+ }
+
+ userID, err := middleware.GetUserID(c)
+ if err != nil {
+ return err
+ }
+
+ err = db.Queries.InsertFinanceRecord(c.Context(), repo.InsertFinanceRecordParams{
+ ID: uniqueid.NextId(),
+ SpaceID: spaceID,
+ CategoryID: dto.CategoryId,
+ Amount: pgtypex.StringToNumeric(dto.Amount),
+ RecordTime: pgtypex.StringToDate(dto.RecordTime),
+ Remark: pgtypex.StringToText(dto.Remark),
+ CreateBy: userID,
+ UpdateBy: userID,
+ })
+
+ if err != nil {
+ return err
+ }
+
+ return responsex.OK(c, nil)
+}
+
+// UpdateFinanceRecord 更新记账记录
+func UpdateFinanceRecord(c fiber.Ctx) error {
+ dto := new(types.UpdateFinanceRecordDTO)
+ if err := requestx.BindAndValidate(c, dto); err != nil {
+ return err
+ }
+
+ userID, err := middleware.GetUserID(c)
+ if err != nil {
+ return err
+ }
+
+ err = db.Queries.UpdateFinanceRecord(c.Context(), repo.UpdateFinanceRecordParams{
+ ID: uniqueid.NextId(),
+ CategoryID: dto.CategoryId,
+ Amount: pgtypex.StringToNumeric(dto.Amount),
+ Remark: pgtypex.StringToText(dto.Remark),
+ RecordTime: pgtypex.StringToDate(dto.RecordTime),
+ UpdateBy: userID,
+ })
+
+ if err != nil {
+ return err
+ }
+
+ return responsex.OK(c, nil)
+}
+
+// DeleteFinanceRecord 删除记账记录
+func DeleteFinanceRecord(c fiber.Ctx) error {
+ dto := new(types.DeleteFinanceRecordDTO)
+ if err := requestx.BindAndValidate(c, dto); err != nil {
+ return err
+ }
+
+ err := db.Queries.DeleteFinanceRecord(c.Context(), dto.Id)
+
+ if err != nil {
+ return err
+ }
+
+ return responsex.OK(c, nil)
+}
diff --git a/code/allapp/allapp-go/api/handler/space/member.go b/code/allapp/allapp-go/api/handler/space/member.go
new file mode 100644
index 00000000..2567b9b3
--- /dev/null
+++ b/code/allapp/allapp-go/api/handler/space/member.go
@@ -0,0 +1,127 @@
+package space
+
+import (
+ "allapp/api/router/middleware"
+ "allapp/api/types"
+ "allapp/db"
+ "allapp/db/repo"
+ "allapp/utils/requestx"
+ "allapp/utils/responsex"
+
+ "github.com/gofiber/fiber/v3"
+)
+
+// UpdateSpaceDefault 更新空间默认情况
+func UpdateSpaceDefault(c fiber.Ctx) error {
+ dto := new(types.UpdateSpaceIsDefaultDTO)
+ if err := requestx.BindAndValidate(c, dto); err != nil {
+ return err
+ }
+
+ userId, err := middleware.GetUserID(c)
+ if err != nil {
+ return err
+ }
+
+ err = db.WithTx(c.Context(), func(q *repo.Queries) error {
+ err = q.UpdateSpaceIsDefault(c.Context(), repo.UpdateSpaceIsDefaultParams{
+ SpaceID: dto.SpaceId,
+ UserID: userId,
+ IsDefault: dto.IsDefault,
+ })
+
+ if err != nil {
+ return err
+ }
+
+ var otherDefault int16
+
+ if dto.IsDefault == 1 {
+ otherDefault = 0
+ }
+
+ err = q.UpdateOtherSpacesIsDefault(c.Context(), repo.UpdateOtherSpacesIsDefaultParams{
+ IsDefault: otherDefault,
+ UserID: userId,
+ SpaceID: dto.SpaceId,
+ })
+
+ if err != nil {
+ return err
+ }
+
+ return nil
+ })
+
+ if err != nil {
+ return err
+ }
+
+ return responsex.OK(c, "更新成功")
+}
+
+// UpdateMemberName 更新群成员名称
+func UpdateMemberName(c fiber.Ctx) error {
+ dto := new(types.UpdateMemberNameDTO)
+ if err := requestx.BindAndValidate(c, dto); err != nil {
+ return err
+ }
+
+ userId, err := middleware.GetUserID(c)
+ if err != nil {
+ return err
+ }
+
+ err = db.Queries.UpdateSpaceMemberName(c.Context(), repo.UpdateSpaceMemberNameParams{
+ SpaceID: dto.SpaceId,
+ UserID: userId,
+ MemberName: dto.MemberName,
+ })
+
+ if err != nil {
+ return err
+ }
+
+ return responsex.OK(c, "更新成功")
+}
+
+// UpdateSpaceSortNumber 更新空间排序
+func UpdateSpaceSortNumber(c fiber.Ctx) error {
+ dto := new(types.UpdateSpaceSortNumberDTO)
+ if err := requestx.BindAndValidate(c, dto); err != nil {
+ return err
+ }
+
+ userId, err := middleware.GetUserID(c)
+ if err != nil {
+ return err
+ }
+
+ // 组装批量更新参数
+ params := make([]repo.BatchUpdateSpaceSortNumberParams, 0, len(dto.Spaces))
+
+ for _, datum := range dto.Spaces {
+ params = append(params, repo.BatchUpdateSpaceSortNumberParams{
+ SortNumber: datum.SortNumber,
+ SpaceID: datum.SpaceId,
+ UserID: userId,
+ })
+ }
+
+ // 使用统一事务封装
+ err = db.WithTx(c.Context(), func(q *repo.Queries) error {
+ batch := q.BatchUpdateSpaceSortNumber(c.Context(), params)
+
+ if err := db.RunBatch(c.Context(), batch); err != nil {
+ return err
+ }
+
+ return nil
+ })
+
+ if err != nil {
+ return err
+ }
+
+ return responsex.OK(c, "更新成功")
+}
diff --git a/code/allapp/allapp-go/api/handler/space/index.go b/code/allapp/allapp-go/api/handler/space/space.go
similarity index 86%
rename from code/allapp/allapp-go/api/handler/space/index.go
rename to code/allapp/allapp-go/api/handler/space/space.go
index b4effdf4..b949dbed 100644
--- a/code/allapp/allapp-go/api/handler/space/index.go
+++ b/code/allapp/allapp-go/api/handler/space/space.go
@@ -228,80 +228,6 @@ func ExitSpace(c fiber.Ctx) error {
return responsex.OK(c, "空间已成功退出")
}
-// UpdateMemberName 更新群成员名称
-func UpdateMemberName(c fiber.Ctx) error {
- dto := new(types.UpdateMemberNameDTO)
- if err := requestx.BindAndValidate(c, dto); err != nil {
- return err
- }
-
- userId, err := middleware.GetUserID(c)
- if err != nil {
- return err
- }
-
- err = db.Queries.UpdateSpaceMemberName(c.Context(), repo.UpdateSpaceMemberNameParams{
- SpaceID: dto.SpaceId,
- UserID: userId,
- MemberName: dto.MemberName,
- })
-
- if err != nil {
- return err
- }
-
- return responsex.OK(c, "更新成功")
-}
-
-// UpdateSpaceDefault 更新空间默认情况
-func UpdateSpaceDefault(c fiber.Ctx) error {
- dto := new(types.UpdateSpaceIsDefaultDTO)
- if err := requestx.BindAndValidate(c, dto); err != nil {
- return err
- }
-
- userId, err := middleware.GetUserID(c)
- if err != nil {
- return err
- }
-
- err = db.WithTx(c.Context(), func(q *repo.Queries) error {
- err = q.UpdateSpaceIsDefault(c.Context(), repo.UpdateSpaceIsDefaultParams{
- SpaceID: dto.SpaceId,
- UserID: userId,
- IsDefault: dto.IsDefault,
- })
-
- if err != nil {
- return err
- }
-
- var otherDefault int16
-
- if dto.IsDefault == 1 {
- otherDefault = 0
- }
-
- err = q.UpdateOtherSpacesIsDefault(c.Context(), repo.UpdateOtherSpacesIsDefaultParams{
- IsDefault: otherDefault,
- UserID: userId,
- SpaceID: dto.SpaceId,
- })
-
- if err != nil {
- return err
- }
-
- return nil
- })
-
- if err != nil {
- return err
- }
-
- return responsex.OK(c, "更新成功")
-}
-
// UpdateSpaceName 更新空间名称
func UpdateSpaceName(c fiber.Ctx) error {
dto := new(types.UpdateSpaceNameDTO)
@@ -381,47 +307,6 @@ func GetSpaceSettings(c fiber.Ctx) error {
return responsex.OK(c, vo)
}
-// UpdateSpaceSortNumber 更新空间排序
-func UpdateSpaceSortNumber(c fiber.Ctx) error {
- dto := new(types.UpdateSpaceSortNumberDTO)
- if err := requestx.BindAndValidate(c, dto); err != nil {
- return err
- }
-
- userId, err := middleware.GetUserID(c)
- if err != nil {
- return err
- }
-
- // 组装批量更新参数
- params := make([]repo.BatchUpdateSpaceSortNumberParams, 0, len(dto.Spaces))
-
- for _, datum := range dto.Spaces {
- params = append(params, repo.BatchUpdateSpaceSortNumberParams{
- SortNumber: datum.SortNumber,
- SpaceID: datum.SpaceId,
- UserID: userId,
- })
- }
-
- // 使用统一事务封装
- err = db.WithTx(c.Context(), func(q *repo.Queries) error {
- batch := q.BatchUpdateSpaceSortNumber(c.Context(), params)
-
- if err := db.RunBatch(c.Context(), batch); err != nil {
- return err
- }
-
- return nil
- })
-
- if err != nil {
- return err
- }
-
- return responsex.OK(c, "更新成功")
-}
-
func ListSpaceMembers(c fiber.Ctx) error {
dto := new(types.ListSpaceMembersDTO)
if err := requestx.BindAndValidate(c, dto); err != nil {
diff --git a/code/allapp/allapp-go/api/router/router.go b/code/allapp/allapp-go/api/router/router.go
index 40d068d1..04a4aaa4 100644
--- a/code/allapp/allapp-go/api/router/router.go
+++ b/code/allapp/allapp-go/api/router/router.go
@@ -2,6 +2,7 @@ package router
import (
"allapp/api/handler/auth"
+ "allapp/api/handler/finance"
"allapp/api/handler/space"
"allapp/api/router/middleware"
"allapp/config"
@@ -39,6 +40,19 @@ func SetupRouters(app *fiber.App) {
spaceRouter.Post("/member/name", space.UpdateMemberName)
spaceRouter.Post("/member/sortNumber", space.UpdateSpaceSortNumber)
+ // 记账操作
+ financeRouter := api.Group("/finance").Use(middleware.Auth()).Use(middleware.SpaceGuard())
+ financeRouter.Post("/category/list", finance.ListFinanceCategories)
+ financeRouter.Post("/category/create", finance.CreateFinanceCategory)
+ financeRouter.Post("/category/sortNumber", finance.UpdateFinanceCategorySortNumber)
+ financeRouter.Post("/category/delete", finance.DeleteFinanceCategory)
+ financeRouter.Post("/category/update", finance.UpdateFinanceCategory)
+
+ financeRouter.Post("/record/list", finance.ListFinanceRecords)
+ financeRouter.Post("/record/create", finance.CreateFinanceRecord)
+ financeRouter.Post("/record/update", finance.UpdateFinanceRecord)
+ financeRouter.Post("/record/delete", finance.DeleteFinanceRecord)
+
// authGroup.Get("/user/profile", user.GetProfile)
// spaceGroup.Get("/space/info", space.GetInfo)
}
diff --git a/code/allapp/allapp-go/api/types/finance.go b/code/allapp/allapp-go/api/types/finance.go
new file mode 100644
index 00000000..b2be4fa4
--- /dev/null
+++ b/code/allapp/allapp-go/api/types/finance.go
@@ -0,0 +1,95 @@
+package types
+
+// =================================== 记账分类 ===================================
+
+// FinanceCategory 记账分类
+type FinanceCategory struct {
+ Id int64 `json:"id"`
+ Name string `json:"name"`
+ Icon string `json:"icon"`
+ Type int16 `json:"type"`
+ SortNumber int32 `json:"sort_number"`
+}
+
+type ListFinanceCategoriesVO struct {
+ Categories []FinanceCategory `json:"categories"`
+}
+
+// CreateFinanceCategoryDTO 创建记账分类
+type CreateFinanceCategoryDTO struct {
+ Name string `json:"name" validate:"required"`
+ Icon string `json:"icon" validate:"required"`
+ Type int16 `json:"type" validate:"required"`
+ SortNumber int32 `json:"sort_number" validate:"required"`
+}
+
+type CreateFinanceCategoryVO struct {
+ Id int64 `json:"id"`
+}
+
+// UpdateFinanceCategorySortItem 更新记账分类排序
+type UpdateFinanceCategorySortItem struct {
+ Id int64 `json:"id" validate:"required"`
+ SortNumber int32 `json:"sort_number" validate:"required"`
+}
+
+type UpdateFinanceCategorySortDTO struct {
+ Categories []UpdateFinanceCategorySortItem `json:"categories" validate:"required,dive"`
+}
+
+// DeleteFinanceCategoryDTO 删除记账分类
+type DeleteFinanceCategoryDTO struct {
+ Id int64 `json:"id" validate:"required"`
+}
+
+type UpdateFinanceCategoryDTO struct {
+ Id int64 `json:"id" validate:"required"`
+ Name string `json:"name" validate:"required"`
+ Icon string `json:"icon" validate:"required"`
+}
+
+// =================================== 记账分类 ===================================
+
+// =================================== 记账 ===================================
+
+type FinanceRecord struct {
+ Id int64 `json:"id"`
+ CategoryId int64 `json:"category_id"`
+ Amount string `json:"amount"`
+ Remark string `json:"remark"`
+ RecordTime string `json:"record_time"`
+ MemberId int64 `json:"member_id"`
+ MemberName string `json:"member_name"`
+}
+
+type ListFinanceRecordsDTO struct {
+ StartTime string `json:"start_time"`
+ EndTime string `json:"end_time"`
+ CategoryId int64 `json:"category_id"`
+}
+
+type ListFinanceRecordsVO struct {
+ Records []FinanceRecord `json:"records"`
+}
+
+// CreateFinanceRecordDTO 新建记账
+type CreateFinanceRecordDTO struct {
+ CategoryId int64 `json:"category_id" validate:"required"`
+ Amount string `json:"amount" validate:"required"`
+ RecordTime string `json:"record_time" validate:"required"`
+ Remark string `json:"remark"`
+}
+
+// UpdateFinanceRecordDTO 更新记账
+type UpdateFinanceRecordDTO struct {
+ Id int64 `json:"id" validate:"required"`
+ CategoryId int64 `json:"category_id" validate:"required"`
+ Amount string `json:"amount" validate:"required"`
+ RecordTime string `json:"record_time" validate:"required"`
+ Remark string `json:"remark"`
+}
+
+// DeleteFinanceRecordDTO 删除记账
+type DeleteFinanceRecordDTO struct {
+ Id int64 `json:"id" validate:"required"`
+}
diff --git a/code/allapp/allapp-go/db/queries/finance_category_space.sql b/code/allapp/allapp-go/db/queries/finance_category_space.sql
index a46c93a6..f0c88aaa 100644
--- a/code/allapp/allapp-go/db/queries/finance_category_space.sql
+++ b/code/allapp/allapp-go/db/queries/finance_category_space.sql
@@ -1,3 +1,26 @@
-- name: BatchInsertFinanceCategorySpace :copyfrom
INSERT INTO finance_category_space (id, space_id, name, icon, type, sort_number,create_by,update_by)
-VALUES ($1, $2, $3, $4, $5, $6,$7,$8);
\ No newline at end of file
+VALUES ($1, $2, $3, $4, $5, $6,$7,$8);
+
+-- name: ListFinanceCategories :many
+SELECT * FROM finance_category_space WHERE space_id = $1;
+
+-- name: GetFinanceCategoryByName :one
+SELECT * FROM finance_category_space WHERE space_id = $1 AND name = $2 AND type = $3;
+
+-- name: InsertFinanceCategory :exec
+INSERT INTO finance_category_space(id,space_id,name,icon,type,sort_number,create_by,update_by,create_time,update_time)
+values ($1,$2,$3,$4,$5,$6,$7,$8,NOW(),NOW());
+
+-- name: BatchUpdateFinanceCategorySortNumber :batchexec
+UPDATE finance_category_space
+SET sort_number = $1,update_by = $2,update_time = NOW()
+WHERE id = $3;
+
+-- name: DeleteFinanceCategory :exec
+DELETE FROM finance_category_space WHERE id = $1;
+
+-- name: UpdateFinanceCategory :exec
+UPDATE finance_category_space
+SET name = $1,icon = $2,update_by = $3,update_time = NOW()
+WHERE id = $4;
\ No newline at end of file
diff --git a/code/allapp/allapp-go/db/queries/finance_record.sql b/code/allapp/allapp-go/db/queries/finance_record.sql
new file mode 100644
index 00000000..0d996237
--- /dev/null
+++ b/code/allapp/allapp-go/db/queries/finance_record.sql
@@ -0,0 +1,40 @@
+-- name: ListFinanceRecords :many
+SELECT
+ fr.*,
+ su.member_name
+FROM finance_record fr
+ LEFT JOIN space_member su
+ ON fr.space_id = su.space_id
+ AND fr.update_by = su.user_id
+WHERE
+ fr.space_id = $1
+ AND fr.record_time >= $2
+ AND fr.record_time < $3
+ AND (
+ $4 = 0
+ OR fr.category_id = $4
+ )
+ORDER BY
+ fr.record_time DESC,
+ fr.create_time DESC;
+
+-- name: InsertFinanceRecord :exec
+INSERT INTO finance_record(id,category_id,space_id,amount,record_time,remark,create_by,update_by,create_time,update_time)
+VALUES ($1,$2,$3,$4,$5,$6,$7,$8,NOW(),NOW());
+
+-- name: UpdateFinanceRecord :exec
+UPDATE finance_record
+SET
+ category_id = $2,
+ amount = $3,
+ remark = $4,
+ record_time = $5,
+ update_by = $6,
+ update_time = NOW()
+WHERE id = $1;
+
+-- name: DeleteFinanceRecord :exec
+DELETE FROM finance_record WHERE id = $1;
+
+-- name: DeleteFinanceRecordByCategoryId :exec
+DELETE FROM finance_record WHERE category_id = $1;
\ No newline at end of file
diff --git a/code/allapp/allapp-go/db/repo/batch.go b/code/allapp/allapp-go/db/repo/batch.go
index 702371db..3ea9b5a8 100644
--- a/code/allapp/allapp-go/db/repo/batch.go
+++ b/code/allapp/allapp-go/db/repo/batch.go
@@ -16,6 +16,59 @@ var (
ErrBatchAlreadyClosed = errors.New("batch already closed")
)
+const batchUpdateFinanceCategorySortNumber = `-- name: BatchUpdateFinanceCategorySortNumber :batchexec
+UPDATE finance_category_space
+SET sort_number = $1,update_by = $2,update_time = NOW()
+WHERE id = $3
+`
+
+type BatchUpdateFinanceCategorySortNumberBatchResults struct {
+ br pgx.BatchResults
+ tot int
+ closed bool
+}
+
+type BatchUpdateFinanceCategorySortNumberParams struct {
+ SortNumber int32
+ UpdateBy int64
+ ID int64
+}
+
+func (q *Queries) BatchUpdateFinanceCategorySortNumber(ctx context.Context, arg []BatchUpdateFinanceCategorySortNumberParams) *BatchUpdateFinanceCategorySortNumberBatchResults {
+ batch := &pgx.Batch{}
+ for _, a := range arg {
+ vals := []interface{}{
+ a.SortNumber,
+ a.UpdateBy,
+ a.ID,
+ }
+ batch.Queue(batchUpdateFinanceCategorySortNumber, vals...)
+ }
+ br := q.db.SendBatch(ctx, batch)
+ return &BatchUpdateFinanceCategorySortNumberBatchResults{br, len(arg), false}
+}
+
+func (b *BatchUpdateFinanceCategorySortNumberBatchResults) Exec(f func(int, error)) {
+ defer b.br.Close()
+ for t := 0; t < b.tot; t++ {
+ if b.closed {
+ if f != nil {
+ f(t, ErrBatchAlreadyClosed)
+ }
+ continue
+ }
+ _, err := b.br.Exec()
+ if f != nil {
+ f(t, err)
+ }
+ }
+}
+
+func (b *BatchUpdateFinanceCategorySortNumberBatchResults) Close() error {
+ b.closed = true
+ return b.br.Close()
+}
+
const batchUpdateSpaceSortNumber = `-- name: BatchUpdateSpaceSortNumber :batchexec
UPDATE space_member
SET sort_number = $1
diff --git a/code/allapp/allapp-go/db/repo/finance_category_space.sql.go b/code/allapp/allapp-go/db/repo/finance_category_space.sql.go
index 5d9735b4..c4f2a32e 100644
--- a/code/allapp/allapp-go/db/repo/finance_category_space.sql.go
+++ b/code/allapp/allapp-go/db/repo/finance_category_space.sql.go
@@ -5,6 +5,10 @@
package repo
+import (
+ "context"
+)
+
type BatchInsertFinanceCategorySpaceParams struct {
ID int64
SpaceID int64
@@ -15,3 +19,128 @@ type BatchInsertFinanceCategorySpaceParams struct {
CreateBy int64
UpdateBy int64
}
+
+const deleteFinanceCategory = `-- name: DeleteFinanceCategory :exec
+DELETE FROM finance_category_space WHERE id = $1
+`
+
+func (q *Queries) DeleteFinanceCategory(ctx context.Context, id int64) error {
+ _, err := q.db.Exec(ctx, deleteFinanceCategory, id)
+ return err
+}
+
+const getFinanceCategoryByName = `-- name: GetFinanceCategoryByName :one
+SELECT id, space_id, name, icon, type, sort_number, create_by, create_time, update_by, update_time FROM finance_category_space WHERE space_id = $1 AND name = $2 AND type = $3
+`
+
+type GetFinanceCategoryByNameParams struct {
+ SpaceID int64
+ Name string
+ Type int16
+}
+
+func (q *Queries) GetFinanceCategoryByName(ctx context.Context, arg GetFinanceCategoryByNameParams) (FinanceCategorySpace, error) {
+ row := q.db.QueryRow(ctx, getFinanceCategoryByName, arg.SpaceID, arg.Name, arg.Type)
+ var i FinanceCategorySpace
+ err := row.Scan(
+ &i.ID,
+ &i.SpaceID,
+ &i.Name,
+ &i.Icon,
+ &i.Type,
+ &i.SortNumber,
+ &i.CreateBy,
+ &i.CreateTime,
+ &i.UpdateBy,
+ &i.UpdateTime,
+ )
+ return i, err
+}
+
+const insertFinanceCategory = `-- name: InsertFinanceCategory :exec
+INSERT INTO finance_category_space(id,space_id,name,icon,type,sort_number,create_by,update_by,create_time,update_time)
+values ($1,$2,$3,$4,$5,$6,$7,$8,NOW(),NOW())
+`
+
+type InsertFinanceCategoryParams struct {
+ ID int64
+ SpaceID int64
+ Name string
+ Icon string
+ Type int16
+ SortNumber int32
+ CreateBy int64
+ UpdateBy int64
+}
+
+func (q *Queries) InsertFinanceCategory(ctx context.Context, arg InsertFinanceCategoryParams) error {
+ _, err := q.db.Exec(ctx, insertFinanceCategory,
+ arg.ID,
+ arg.SpaceID,
+ arg.Name,
+ arg.Icon,
+ arg.Type,
+ arg.SortNumber,
+ arg.CreateBy,
+ arg.UpdateBy,
+ )
+ return err
+}
+
+const listFinanceCategories = `-- name: ListFinanceCategories :many
+SELECT id, space_id, name, icon, type, sort_number, create_by, create_time, update_by, update_time FROM finance_category_space WHERE space_id = $1
+`
+
+func (q *Queries) ListFinanceCategories(ctx context.Context, spaceID int64) ([]FinanceCategorySpace, error) {
+ rows, err := q.db.Query(ctx, listFinanceCategories, spaceID)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ var items []FinanceCategorySpace
+ for rows.Next() {
+ var i FinanceCategorySpace
+ if err := rows.Scan(
+ &i.ID,
+ &i.SpaceID,
+ &i.Name,
+ &i.Icon,
+ &i.Type,
+ &i.SortNumber,
+ &i.CreateBy,
+ &i.CreateTime,
+ &i.UpdateBy,
+ &i.UpdateTime,
+ ); err != nil {
+ return nil, err
+ }
+ items = append(items, i)
+ }
+ if err := rows.Err(); err != nil {
+ return nil, err
+ }
+ return items, nil
+}
+
+const updateFinanceCategory = `-- name: UpdateFinanceCategory :exec
+UPDATE finance_category_space
+SET name = $1,icon = $2,update_by = $3,update_time = NOW()
+WHERE id = $4
+`
+
+type UpdateFinanceCategoryParams struct {
+ Name string
+ Icon string
+ UpdateBy int64
+ ID int64
+}
+
+func (q *Queries) UpdateFinanceCategory(ctx context.Context, arg UpdateFinanceCategoryParams) error {
+ _, err := q.db.Exec(ctx, updateFinanceCategory,
+ arg.Name,
+ arg.Icon,
+ arg.UpdateBy,
+ arg.ID,
+ )
+ return err
+}
diff --git a/code/allapp/allapp-go/db/repo/finance_record.sql.go b/code/allapp/allapp-go/db/repo/finance_record.sql.go
new file mode 100644
index 00000000..fae9f4d7
--- /dev/null
+++ b/code/allapp/allapp-go/db/repo/finance_record.sql.go
@@ -0,0 +1,172 @@
+// Code generated by sqlc. DO NOT EDIT.
+// versions:
+// sqlc v1.30.0
+// source: finance_record.sql
+
+package repo
+
+import (
+ "context"
+
+ "github.com/jackc/pgx/v5/pgtype"
+)
+
+const deleteFinanceRecord = `-- name: DeleteFinanceRecord :exec
+DELETE FROM finance_record WHERE id = $1
+`
+
+func (q *Queries) DeleteFinanceRecord(ctx context.Context, id int64) error {
+ _, err := q.db.Exec(ctx, deleteFinanceRecord, id)
+ return err
+}
+
+const deleteFinanceRecordByCategoryId = `-- name: DeleteFinanceRecordByCategoryId :exec
+DELETE FROM finance_record WHERE category_id = $1
+`
+
+func (q *Queries) DeleteFinanceRecordByCategoryId(ctx context.Context, categoryID int64) error {
+ _, err := q.db.Exec(ctx, deleteFinanceRecordByCategoryId, categoryID)
+ return err
+}
+
+const insertFinanceRecord = `-- name: InsertFinanceRecord :exec
+INSERT INTO finance_record(id,category_id,space_id,amount,record_time,remark,create_by,update_by,create_time,update_time)
+VALUES ($1,$2,$3,$4,$5,$6,$7,$8,NOW(),NOW())
+`
+
+type InsertFinanceRecordParams struct {
+ ID int64
+ CategoryID int64
+ SpaceID int64
+ Amount pgtype.Numeric
+ RecordTime pgtype.Date
+ Remark pgtype.Text
+ CreateBy int64
+ UpdateBy int64
+}
+
+func (q *Queries) InsertFinanceRecord(ctx context.Context, arg InsertFinanceRecordParams) error {
+ _, err := q.db.Exec(ctx, insertFinanceRecord,
+ arg.ID,
+ arg.CategoryID,
+ arg.SpaceID,
+ arg.Amount,
+ arg.RecordTime,
+ arg.Remark,
+ arg.CreateBy,
+ arg.UpdateBy,
+ )
+ return err
+}
+
+const listFinanceRecords = `-- name: ListFinanceRecords :many
+SELECT
+ fr.id, fr.category_id, fr.space_id, fr.amount, fr.remark, fr.record_time, fr.create_by, fr.create_time, fr.update_by, fr.update_time,
+ su.member_name
+FROM finance_record fr
+ LEFT JOIN space_member su
+ ON fr.space_id = su.space_id
+ AND fr.update_by = su.user_id
+WHERE
+ fr.space_id = $1
+ AND fr.record_time >= $2
+ AND fr.record_time < $3
+ AND (
+ $4 = 0
+ OR fr.category_id = $4
+ )
+ORDER BY
+ fr.record_time DESC,
+ fr.create_time DESC
+`
+
+type ListFinanceRecordsParams struct {
+ SpaceID int64
+ RecordTime pgtype.Date
+ RecordTime_2 pgtype.Date
+ Column4 interface{}
+}
+
+type ListFinanceRecordsRow struct {
+ ID int64
+ CategoryID int64
+ SpaceID int64
+ Amount pgtype.Numeric
+ Remark pgtype.Text
+ RecordTime pgtype.Date
+ CreateBy int64
+ CreateTime pgtype.Timestamptz
+ UpdateBy int64
+ UpdateTime pgtype.Timestamptz
+ MemberName pgtype.Text
+}
+
+func (q *Queries) ListFinanceRecords(ctx context.Context, arg ListFinanceRecordsParams) ([]ListFinanceRecordsRow, error) {
+ rows, err := q.db.Query(ctx, listFinanceRecords,
+ arg.SpaceID,
+ arg.RecordTime,
+ arg.RecordTime_2,
+ arg.Column4,
+ )
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ var items []ListFinanceRecordsRow
+ for rows.Next() {
+ var i ListFinanceRecordsRow
+ if err := rows.Scan(
+ &i.ID,
+ &i.CategoryID,
+ &i.SpaceID,
+ &i.Amount,
+ &i.Remark,
+ &i.RecordTime,
+ &i.CreateBy,
+ &i.CreateTime,
+ &i.UpdateBy,
+ &i.UpdateTime,
+ &i.MemberName,
+ ); err != nil {
+ return nil, err
+ }
+ items = append(items, i)
+ }
+ if err := rows.Err(); err != nil {
+ return nil, err
+ }
+ return items, nil
+}
+
+const updateFinanceRecord = `-- name: UpdateFinanceRecord :exec
+UPDATE finance_record
+SET
+ category_id = $2,
+ amount = $3,
+ remark = $4,
+ record_time = $5,
+ update_by = $6,
+ update_time = NOW()
+WHERE id = $1
+`
+
+type UpdateFinanceRecordParams struct {
+ ID int64
+ CategoryID int64
+ Amount pgtype.Numeric
+ Remark pgtype.Text
+ RecordTime pgtype.Date
+ UpdateBy int64
+}
+
+func (q *Queries) UpdateFinanceRecord(ctx context.Context, arg UpdateFinanceRecordParams) error {
+ _, err := q.db.Exec(ctx, updateFinanceRecord,
+ arg.ID,
+ arg.CategoryID,
+ arg.Amount,
+ arg.Remark,
+ arg.RecordTime,
+ arg.UpdateBy,
+ )
+ return err
+}
diff --git a/code/allapp/allapp-go/db/repo/models.go b/code/allapp/allapp-go/db/repo/models.go
index 8c602775..d1916094 100644
--- a/code/allapp/allapp-go/db/repo/models.go
+++ b/code/allapp/allapp-go/db/repo/models.go
@@ -31,6 +31,19 @@ type FinanceCategorySpace struct {
UpdateTime pgtype.Timestamptz
}
+type FinanceRecord struct {
+ ID int64
+ CategoryID int64
+ SpaceID int64
+ Amount pgtype.Numeric
+ Remark pgtype.Text
+ RecordTime pgtype.Date
+ CreateBy int64
+ CreateTime pgtype.Timestamptz
+ UpdateBy int64
+ UpdateTime pgtype.Timestamptz
+}
+
type Space struct {
ID int64
Name string
diff --git a/code/allapp/allapp-go/db/schema.sql b/code/allapp/allapp-go/db/schema.sql
index f9b48b4e..42277ddf 100644
--- a/code/allapp/allapp-go/db/schema.sql
+++ b/code/allapp/allapp-go/db/schema.sql
@@ -66,6 +66,7 @@ create table space_member
unique (space_id, user_id)
);
+--记账分类默认表
create table finance_category_default
(
id bigint not null
@@ -78,6 +79,7 @@ create table finance_category_default
update_time timestamp with time zone default now()
);
+--记账分类空间表
create table finance_category_space
(
id bigint not null
@@ -93,4 +95,19 @@ create table finance_category_space
update_time timestamp with time zone default now()
);
+-- 记账记录表
+create table finance_record
+(
+ id bigint not null
+ primary key,
+ category_id bigint not null,
+ space_id bigint not null,
+ amount numeric(12, 2) not null,
+ remark varchar(255),
+ record_time date,
+ create_by bigint not null,
+ create_time timestamp with time zone default now(),
+ update_by bigint not null,
+ update_time timestamp with time zone default now()
+);
diff --git a/code/allapp/allapp-go/utils/timex/index.go b/code/allapp/allapp-go/utils/timex/index.go
new file mode 100644
index 00000000..a1a6fe66
--- /dev/null
+++ b/code/allapp/allapp-go/utils/timex/index.go
@@ -0,0 +1,29 @@
+package timex
+
+import (
+ "fmt"
+ "time"
+)
+
+func ParseTimeFlexible(value string) (*time.Time, error) {
+ if value == "" {
+ return nil, nil // ⭐ 空字符串 → 不限时间
+ }
+
+ layouts := []string{
+ "2006-01-02 15:04:05",
+ "2006-01-02 15:04",
+ "2006-01-02",
+ "2006-01",
+ time.RFC3339,
+ }
+
+ for _, layout := range layouts {
+ t, err := time.Parse(layout, value)
+ if err == nil {
+ return &t, nil
+ }
+ }
+
+ return nil, fmt.Errorf("unsupported time format: %s", value)
+}
diff --git a/code/allapp/allapp-rn/src/app/(tabs)/_layout.tsx b/code/allapp/allapp-rn/src/app/(tabs)/_layout.tsx
index c853301e..b6414efc 100644
--- a/code/allapp/allapp-rn/src/app/(tabs)/_layout.tsx
+++ b/code/allapp/allapp-rn/src/app/(tabs)/_layout.tsx
@@ -3,7 +3,6 @@ import type { NavItem } from '@/store/navigation'
import { Slot, usePathname, useRouter } from 'expo-router'
import { useCallback, useEffect, useState } from 'react'
import { Text, TouchableOpacity, View } from 'react-native'
-import { KeyboardAvoidingView } from 'react-native-keyboard-controller'
import Animated, { useAnimatedRef } from 'react-native-reanimated'
import Sortable from 'react-native-sortables'
import { ADrawer, LucideIcon } from '@/components/common/ao'
@@ -72,14 +71,13 @@ export default function TabsLayout() {
return (
-
-
+
diff --git a/code/allapp/allapp-rn/src/app/(tabs)/money/components/CategorySheet.tsx b/code/allapp/allapp-rn/src/app/(tabs)/finance/components/CategorySheet.tsx
similarity index 94%
rename from code/allapp/allapp-rn/src/app/(tabs)/money/components/CategorySheet.tsx
rename to code/allapp/allapp-rn/src/app/(tabs)/finance/components/CategorySheet.tsx
index 83368594..3521c251 100644
--- a/code/allapp/allapp-rn/src/app/(tabs)/money/components/CategorySheet.tsx
+++ b/code/allapp/allapp-rn/src/app/(tabs)/finance/components/CategorySheet.tsx
@@ -1,4 +1,4 @@
-import type { MoneyCategoryItem } from '@/types/money/category'
+import type { FinanceCategory } from '@/types/finance/category'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { AInput, LucideIcon } from '@/components/common/ao'
import EmptyState from '@/components/common/EmptyStatus'
@@ -16,15 +16,15 @@ import {
interface CategorySection {
title: string
- data: MoneyCategoryItem[]
+ data: FinanceCategory[]
}
interface CategorySheetProps {
open?: boolean
- moneyCategories?: MoneyCategoryItem[]
+ moneyCategories?: FinanceCategory[]
value?: number | null
onClose?: () => void
- onSelect?: (category: MoneyCategoryItem) => void
+ onSelect?: (category: FinanceCategory) => void
}
export default function CategorySheet({
@@ -45,7 +45,7 @@ export default function CategorySheet({
} | null>(null)
const handleSelect = useCallback(
- (item: MoneyCategoryItem) => {
+ (item: FinanceCategory) => {
onSelect?.(item)
onClose?.()
},
@@ -56,7 +56,7 @@ export default function CategorySheet({
id: number,
name: string,
type: number,
- ): MoneyCategoryItem {
+ ): FinanceCategory {
return {
id,
name,
@@ -188,9 +188,9 @@ export default function CategorySheet({
}, 0)
}}
keyExtractor={(item, index) =>
- String((item as MoneyCategoryItem).id ?? index)}
+ String((item as FinanceCategory).id ?? index)}
renderItem={({ item }) => {
- const category = item as MoneyCategoryItem
+ const category = item as FinanceCategory
const selected = value === category.id
return (
diff --git a/code/allapp/allapp-rn/src/app/(tabs)/money/components/TransactionsList.tsx b/code/allapp/allapp-rn/src/app/(tabs)/finance/components/TransactionsList.tsx
similarity index 96%
rename from code/allapp/allapp-rn/src/app/(tabs)/money/components/TransactionsList.tsx
rename to code/allapp/allapp-rn/src/app/(tabs)/finance/components/TransactionsList.tsx
index 9788068f..42323ad1 100644
--- a/code/allapp/allapp-rn/src/app/(tabs)/money/components/TransactionsList.tsx
+++ b/code/allapp/allapp-rn/src/app/(tabs)/finance/components/TransactionsList.tsx
@@ -1,7 +1,6 @@
-import type { MoneyRecord, MoneyRecordGroup } from '@/types/money/record'
+import type { FinanceRecord, FinanceRecordGroup } from '@/types/finance/record'
import React, { useCallback, useState } from 'react'
import { Pressable, RefreshControl, SectionList, Text, Vibration, View } from 'react-native'
-
import { LucideIcon } from '@/components/common/ao'
import EmptyState from '@/components/common/EmptyStatus'
import MoneyCategoryIcon from '@/components/common/MoneyCategoryIcon'
@@ -11,8 +10,8 @@ import { useUserStore } from '@/store/user'
import { MoneyCategoryEnum } from '@/types/money/category'
interface StaticTransactionsProps {
- records: MoneyRecordGroup[]
- onUpdate?: (data: MoneyRecord) => void
+ records: FinanceRecordGroup[]
+ onUpdate?: (data: FinanceRecord) => void
onDelete?: (id: number) => void
onDetail?: (id: number) => void
onRefreshData?: () => void
@@ -163,9 +162,9 @@ export default function StaticTransactions({
{item.remark ? item.remark : category?.name}
{
- item.user_id !== userID && (
+ item.member_id !== userID && (
- {item.username}
+ {item.member_name}
)
}
@@ -186,7 +185,7 @@ export default function StaticTransactions({
}}
>
{
- userID === item.user_id && (
+ userID === item.member_id && (
<>
@@ -158,7 +160,7 @@ export default function Money() {
numberOfLines={1}
ellipsizeMode="tail"
>
- {moneyRecordStore.getTotalExpense()}
+ {financeStore.getTotalExpense()}
@@ -169,7 +171,7 @@ export default function Money() {
numberOfLines={1}
ellipsizeMode="tail"
>
- {moneyRecordStore.getTotalBalance()}
+ {financeStore.getTotalBalance()}
@@ -178,16 +180,16 @@ export default function Money() {
{
- router.navigate({
- pathname: '/money/record/editor',
+ router.push({
+ pathname: '/finance/record/editor',
params: { id: data.id },
})
}}
onDetail={(id) => {
router.navigate({
- pathname: '/money/record/detail',
+ pathname: '/finance/record/detail',
params: { id },
})
}}
@@ -195,7 +197,7 @@ export default function Money() {
handleDeleteRecord(id)
}}
onRefreshData={() => {
- fetchMoneyRecords()
+ fetchFinanceRecords()
}}
/>
@@ -203,7 +205,7 @@ export default function Money() {
{
- router.navigate('/money/record/editor')
+ router.navigate('/finance/record/editor')
}}
>
@@ -247,7 +249,7 @@ export default function Money() {
{
setShowCategorySheet(false)
}}
@@ -283,7 +285,8 @@ export default function Money() {
icon="LayoutGrid"
text="类别设置"
onPress={() => {
- router.navigate('/money/category/setting')
+ // router.navigate('/money/category/setting')
+ router.push('/finance/category')
setShowActionsMenu(false)
}}
/>
diff --git a/code/allapp/allapp-rn/src/app/(tabs)/index.tsx b/code/allapp/allapp-rn/src/app/(tabs)/index.tsx
index ffb25217..4fe80fcc 100644
--- a/code/allapp/allapp-rn/src/app/(tabs)/index.tsx
+++ b/code/allapp/allapp-rn/src/app/(tabs)/index.tsx
@@ -26,21 +26,21 @@ export default function Index() {
{/* ('/money')}
+ onPress={() => ('/finance')}
>
开始使用
*/}
('/money')}
+ onPress={() => ('/finance')}
>
创建空间
('/money')}
+ onPress={() => ('/finance')}
>
加入空间
diff --git a/code/allapp/allapp-rn/src/app/finance/category/components/EditCategory.tsx b/code/allapp/allapp-rn/src/app/finance/category/components/EditCategory.tsx
new file mode 100644
index 00000000..a06e5f7c
--- /dev/null
+++ b/code/allapp/allapp-rn/src/app/finance/category/components/EditCategory.tsx
@@ -0,0 +1,197 @@
+import type { LegendListRef } from '@legendapp/list'
+import type { FinanceCategory } from '@/types/finance/category'
+import { LegendList } from '@legendapp/list'
+import { useEffect, useRef, useState } from 'react'
+import { Pressable, Text, View } from 'react-native'
+import { AButton, AInput } from '@/components/common/ao'
+import MoneyCategoryIcon from '@/components/common/MoneyCategoryIcon'
+import {
+ Actionsheet,
+ ActionsheetBackdrop,
+ ActionsheetContent,
+ ActionsheetDragIndicator,
+ ActionsheetDragIndicatorWrapper,
+} from '@/components/ui/actionsheet'
+import { useFinanceStore } from '@/store/finance'
+import { FinanceCategoryEnum } from '@/types/finance/category'
+
+export interface EditCategoryForm {
+ id?: number
+ name: string
+ icon: string
+}
+
+interface EditCategoryProps {
+ open: boolean
+ onClose?: () => void
+ type: FinanceCategoryEnum
+ category: FinanceCategory | null
+ onConfirm?: (form: EditCategoryForm) => Promise
+}
+
+export default function EditCategory({
+ open,
+ onClose,
+ type,
+ onConfirm,
+ category,
+}: EditCategoryProps) {
+ const financeStore = useFinanceStore()
+
+ const [isOpen, setIsOpen] = useState(open)
+ const [form, setForm] = useState({ icon: '', name: '' })
+ const [confirmDisabled, setConfirmDisabled] = useState(true)
+ const [confirmLoading, setConfirmLoading] = useState(false)
+ const listRef = useRef(null)
+
+ useEffect(() => {
+ setIsOpen(open)
+ if (!open)
+ setForm({ icon: '', name: '' })
+ }, [open])
+
+ // 确认按钮状态
+ useEffect(() => {
+ setConfirmDisabled(!(form.name && form.name.length <= 5 && form.icon))
+ }, [form.name, form.icon])
+
+ useEffect(() => {
+ if (!form.icon || !listRef.current) {
+ return
+ }
+
+ const index = financeStore.getDefaultGroupRow(form.icon)
+
+ if (index !== -1) {
+ listRef.current.scrollToIndex({
+ index,
+ viewPosition: 0.5,
+ animated: true,
+ })
+ }
+ }, [form.icon])
+
+ // 编辑已有分类时填充数据
+ useEffect(() => {
+ if (category)
+ setForm(category)
+ }, [category])
+
+ function handleClose() {
+ setForm({ icon: '', name: '' })
+ setIsOpen(false)
+ onClose?.()
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+ 取消
+
+
+ {type === FinanceCategoryEnum.Expense ? '添加支出分类' : '添加收入分类'}
+
+ {
+ if (!onConfirm)
+ return
+ try {
+ setConfirmLoading(true)
+ await onConfirm({ ...form, name: form.name.trim() })
+ }
+ finally {
+ setConfirmLoading(false)
+ }
+ }}
+ >
+ 确认
+
+
+
+ setForm(prevForm => ({ ...prevForm, name: text }))}
+ />
+ String(item.index)}
+ recycleItems
+ initialContainerPoolRatio={20}
+ style={{
+ maxHeight: 400,
+ }}
+ initialScrollIndex={financeStore.getDefaultGroupRow(category?.icon || '')}
+ renderItem={({ item }) => (
+
+ {
+ item.type === 'title'
+ ? (
+ {item.value}
+ )
+ : (
+
+ {item.value.map((icon) => {
+ const selected = form.icon === icon
+
+ return (
+ {
+ setForm(prevForm => ({
+ ...prevForm,
+ icon,
+ }))
+ }}
+ >
+
+
+
+
+ )
+ })}
+
+ )
+ }
+
+ )}
+ />
+
+
+
+
+ )
+}
diff --git a/code/allapp/allapp-rn/src/app/finance/category/index.tsx b/code/allapp/allapp-rn/src/app/finance/category/index.tsx
new file mode 100644
index 00000000..0f7ea71c
--- /dev/null
+++ b/code/allapp/allapp-rn/src/app/finance/category/index.tsx
@@ -0,0 +1,303 @@
+import type { SortableGridRenderItem } from 'react-native-sortables'
+import type { EditCategoryForm } from './components/EditCategory'
+import type { FinanceCategory } from '@/types/finance/category'
+import { useCallback, useEffect, useState } from 'react'
+import { Pressable, Text, Vibration, View } from 'react-native'
+import Animated, { useAnimatedRef } from 'react-native-reanimated'
+import Sortable from 'react-native-sortables'
+import { toast } from 'sonner-native'
+import { AButton, ATab, LucideIcon } from '@/components/common/ao'
+import MoneyCategoryIcon from '@/components/common/MoneyCategoryIcon'
+import CustomHeader from '@/components/custom/Header'
+import { Spinner } from '@/components/ui/spinner'
+import { createFinanceCategoryApi, deleteFinanceCategoryApi, updateFinanceCategoryApi } from '@/request/api/finance/category'
+import { useFinanceStore } from '@/store/finance'
+import { useMoneyCategoryStore } from '@/store/moneyCategory'
+import { useSpaceStore } from '@/store/space'
+import { FinanceCategoryEnum } from '@/types/finance/category'
+import EditCategory from './components/EditCategory'
+
+export default function MoneyCategorySetting() {
+ const spaceStore = useSpaceStore()
+ const financeStore = useFinanceStore()
+ const moneyCategoryStore = useMoneyCategoryStore()
+
+ const [selectedTab, setSelectedTab] = useState(FinanceCategoryEnum.Expense)
+ const scrollableRef = useAnimatedRef()
+
+ const [categories, setCategories] = useState([])
+
+ const [draggingId, setDraggingId] = useState('')
+ const [draggingDeleteZone, setDraggingDeleteZone] = useState(false)
+ const [deleteTimer, setDeleteTimer] = useState(null)
+ const [deleteLoading, setDeleteLoading] = useState(false)
+
+ const [showEditCategory, setshowEditCategory] = useState(false)
+
+ const [selectedCategory, setSelectedCategory] = useState(null)
+
+ useEffect(() => {
+ setCategories(selectedTab === FinanceCategoryEnum.Expense ? financeStore.getExpenseCategories() : financeStore.getIncomeCategories())
+ }, [selectedTab, financeStore.categories])
+
+ const renderItem = useCallback>(
+ ({ item }) => (
+ {
+ setSelectedCategory(item)
+ setshowEditCategory(true)
+ }}
+ className="flex flex-col items-center justify-center"
+ >
+
+
+
+
+ {item.name}
+
+
+ ),
+ [],
+ )
+
+ async function handleConfirmEdit(form: EditCategoryForm) {
+ if (form.name === '') {
+ toast.warning('分类名称不能为空')
+ return
+ }
+ if (form.icon === '') {
+ toast.warning('分类图标不能为空')
+ return
+ }
+
+ const isDuplicate = moneyCategoryStore.isDuplicateCategory(
+ form.name,
+ selectedTab,
+ form.id ?? null,
+ )
+
+ if (isDuplicate) {
+ toast.warning('类别不能重复')
+ return
+ }
+
+ if (form.id != null) {
+ const currentCategory = financeStore.getCategoryById(form.id)
+
+ if (currentCategory === null) {
+ toast.warning('分类无效')
+ return
+ }
+
+ await updateFinanceCategoryApi({
+ id: form.id,
+ name: form.name,
+ icon: form.icon,
+ }).then((res) => {
+ if (res.isSuccess) {
+ currentCategory.name = form.name
+ currentCategory.icon = form.icon
+
+ financeStore.updateCategory(currentCategory)
+ }
+ })
+ }
+ else {
+ const nextSortNumber = financeStore.getCategoryNextSortNumber(selectedTab)
+
+ await createFinanceCategoryApi({
+ name: form.name,
+ icon: form.icon,
+ sort_number: nextSortNumber,
+ type: selectedTab,
+ }).then((res) => {
+ if (res.isSuccess) {
+ const newCategory = {
+ id: res.data.id,
+ name: form.name,
+ icon: form.icon,
+ sort_number: nextSortNumber,
+ type: selectedTab,
+ space_id: spaceStore.currentSpace?.space_id || 0,
+ }
+
+ financeStore.createCategory(newCategory)
+ }
+ })
+
+ scrollableRef.current?.scrollToEnd({ animated: true })
+ }
+
+ setshowEditCategory(false)
+ }
+
+ return (
+
+
+
+
+
+ {
+ setSelectedTab(key as FinanceCategoryEnum)
+ }}
+ data={[
+ {
+ key: FinanceCategoryEnum.Expense,
+ label: '支出',
+ },
+ {
+ key: FinanceCategoryEnum.Income,
+ label: '收入',
+ },
+ ]}
+ />
+
+ }
+ onPress={(() =>
+ setshowEditCategory(true)
+ )}
+ >
+ 新增
+
+
+
+
+
+
+
+ {
+ Vibration.vibrate(50)
+ setDraggingId(data.key)
+ }}
+ onDragEnd={(dragData) => {
+ setDraggingId('')
+ if (draggingDeleteZone)
+ return
+
+ const newData = dragData.data
+
+ if (JSON.stringify(categories) !== JSON.stringify(newData)) {
+ moneyCategoryStore.updateSortNumbers(newData, selectedTab)
+ }
+ }}
+ />
+
+
+ {
+ if (!draggingId)
+ return
+
+ setDraggingDeleteZone(true)
+ Vibration.vibrate(50)
+
+ const timer = setTimeout(async () => {
+ const deleteId = Number(draggingId)
+
+ setDeleteLoading(true)
+
+ try {
+ const res = await deleteFinanceCategoryApi({ id: deleteId })
+
+ if (!res.isSuccess) {
+ throw new Error(res.message)
+ }
+
+ financeStore.deleteCategory(deleteId)
+
+ setDraggingId('')
+ setDraggingDeleteZone(false)
+ }
+ finally {
+ setDeleteLoading(false)
+ setDeleteTimer(null)
+ }
+ }, 1000)
+
+ setDeleteTimer(timer)
+ }}
+ onItemLeave={() => {
+ setDraggingDeleteZone(false)
+ if (deleteTimer) {
+ clearTimeout(deleteTimer)
+ setDeleteTimer(null)
+ }
+ }}
+ onItemDrop={() => {
+ setDraggingDeleteZone(false)
+ if (deleteTimer) {
+ clearTimeout(deleteTimer)
+ setDeleteTimer(null)
+ }
+ }}
+ minActivationDistance={40}
+ >
+
+ {
+ deleteLoading
+ ? (
+
+ )
+ : (
+
+ )
+ }
+
+ {draggingDeleteZone ? '松手删除(将删除该类下所有记录)' : '拖到这里删除'}
+
+
+
+
+
+
+
+
+ {
+ setshowEditCategory(false)
+ setSelectedCategory(null)
+ }}
+ category={selectedCategory}
+ onConfirm={handleConfirmEdit}
+ />
+
+ )
+}
diff --git a/code/allapp/allapp-rn/src/app/finance/record/editor/components/AmountKeyboard.tsx b/code/allapp/allapp-rn/src/app/finance/record/editor/components/AmountKeyboard.tsx
new file mode 100644
index 00000000..20e03858
--- /dev/null
+++ b/code/allapp/allapp-rn/src/app/finance/record/editor/components/AmountKeyboard.tsx
@@ -0,0 +1,367 @@
+import Big from 'big.js'
+import { useEffect, useState } from 'react'
+import { Keyboard, Pressable, StyleSheet, Text, TouchableWithoutFeedback, View } from 'react-native'
+import { ADatePicker, AInput, LucideIcon } from '@/components/common/ao/index'
+import { useKeyboardHeight } from '@/hooks/useKeyboardHeight'
+import { getCurrentDate } from '@/utils/time'
+
+interface KeydownItem {
+ key: string | number
+ content: React.ReactNode
+}
+
+const keys: KeydownItem[] = [
+ { key: '1', content: 1 },
+ { key: '2', content: 2 },
+ { key: '3', content: 3 },
+ { key: 'del', content: },
+ { key: '4', content: 4 },
+ { key: '5', content: 5 },
+ { key: '6', content: 6 },
+ { key: '+', content: },
+ { key: '7', content: 7 },
+ { key: '8', content: 8 },
+ { key: '9', content: 9 },
+ { key: '-', content: },
+ { key: 'clear', content: (
+
+ 重置
+
+ ) },
+ { key: '0', content: 0 },
+ { key: '.', content: },
+ { key: '/', content: (
+
+ 完成
+
+ ) },
+]
+
+interface AmountKeyboardProps {
+ show?: boolean
+ record_time?: string
+ amount?: string
+ remark?: string
+
+ // 回调:当键盘输入或修改数据时触发
+ onChange?: (data: { record_time: string, amount: string, remark: string }) => void
+
+ // 可选:点击完成按钮触发
+ onDone?: () => void
+}
+
+export default function AmountKeyboard({
+ show = false,
+ record_time = '',
+ amount = '',
+ remark = '',
+ onChange,
+ onDone,
+}: AmountKeyboardProps) {
+ // 自定义键盘计算过程
+ const [keydownInput, setKeydownInput] = useState('')
+ // 日期选择
+ const [showDatePicker, setShowDatePicker] = useState(false)
+ // 备注聚焦
+ const [remarkIsFocused, setRemarkIsFocused] = useState(false)
+ // 键盘高度
+ const keyboardHeight = useKeyboardHeight()
+
+ // 使用一个对象管理三个字段
+ const [internalData, setInternalData] = useState({
+ record_time: record_time || getCurrentDate(),
+ amount: amount || '0',
+ remark: remark || '',
+ })
+
+ // 如果父组件 props 变化,需要同步
+ useEffect(() => {
+ setInternalData({
+ record_time: record_time || getCurrentDate(),
+ amount: amount || '0',
+ remark: remark || '',
+ })
+ }, [record_time, amount, remark])
+
+ useEffect(() => {
+ if (!keydownInput) {
+ handleChange({ amount: '0' })
+ return
+ }
+
+ try {
+ const computed = calculateWithBig(keydownInput)
+ handleChange({ amount: computed })
+ }
+ catch {
+ handleChange({ amount: '0' })
+ }
+ }, [keydownInput])
+
+ const handleChange = (newData: Partial) => {
+ const updated = { ...internalData, ...newData }
+ setInternalData(updated)
+ onChange?.(updated)
+ }
+
+ // 计算过程
+ const handlePress = (key: string | number) => {
+ setKeydownInput((prev) => {
+ let newInput = prev
+
+ if (key === 'del') {
+ newInput = prev.slice(0, -1)
+ }
+ else if (key === 'clear') {
+ setInternalData({
+ record_time: getCurrentDate(),
+ amount: '0',
+ remark: '',
+ })
+ return ''
+ }
+ else if (key === '/') {
+ onDone?.()
+ return prev
+ }
+ else {
+ const keyStr = key.toString()
+
+ // 检查是否要输入运算符(+ 或 -)
+ if (keyStr === '+' || keyStr === '-') {
+ // 如果当前输入为空,不允许以运算符开头
+ if (prev === '') {
+ return prev
+ }
+
+ // 获取最后一个字符
+ const lastChar = prev.slice(-1)
+
+ // 如果最后一个字符已经是运算符,则替换而不是追加
+ if (lastChar === '+' || lastChar === '-') {
+ newInput = prev.slice(0, -1) + keyStr
+ }
+ // 如果最后一个字符是小数点,不允许在小数点后直接加运算符
+ else if (lastChar === '.') {
+ return prev
+ }
+ else {
+ newInput = prev + keyStr
+ }
+ }
+ // 检查是否要输入小数点(.)
+ else if (keyStr === '.') {
+ // 如果当前输入为空,不允许以小数点开头,改为输入"0."
+ if (prev === '') {
+ newInput = '0.'
+ }
+ else {
+ // 获取最后一个字符
+ const lastChar = prev.slice(-1)
+
+ // 如果最后一个字符是运算符,则需要在运算符后添加"0."而不是单独的"."
+ if (lastChar === '+' || lastChar === '-') {
+ newInput = `${prev}0.`
+ }
+ else {
+ // 检查当前数字是否已经包含小数点
+ const parts = prev.split(/[+-]/)
+ const currentNumber = parts[parts.length - 1]
+
+ // 如果当前数字中还没有小数点,可以添加
+ if (!currentNumber.includes('.')) {
+ newInput = `${prev}.`
+ }
+ else {
+ // 已经有小数点了,不重复添加
+ newInput = prev
+ }
+ }
+ }
+ }
+ // 处理数字输入
+ else if (/^\d$/.test(keyStr)) {
+ // 检查当前数字的小数位数
+ const parts = prev.split(/[+-]/)
+ const currentNumber = parts[parts.length - 1]
+
+ // 如果当前数字包含小数点,检查小数位数
+ if (currentNumber.includes('.')) {
+ const decimalParts = currentNumber.split('.')
+ // 如果小数部分已经达到2位,不允许继续输入
+ if (decimalParts.length > 1 && decimalParts[1].length >= 2) {
+ newInput = prev // 不改变输入
+ }
+ else {
+ newInput = prev + keyStr
+ }
+ }
+ else {
+ // 没有小数点,正常输入数字
+ newInput = prev + keyStr
+ }
+ }
+ else {
+ // 对于其他字符,正常追加
+ newInput = prev + keyStr
+ }
+ }
+
+ return newInput
+ })
+ }
+
+ function calculateWithBig(expression: string): string {
+ if (!expression.trim())
+ return '0'
+
+ // 移除表达式末尾的运算符(+或-),避免不完整的表达式
+ let cleanExpression = expression.trim()
+ const lastChar = cleanExpression.slice(-1)
+ if (lastChar === '+' || lastChar === '-') {
+ cleanExpression = cleanExpression.slice(0, -1)
+ }
+
+ if (!cleanExpression)
+ return '0'
+
+ try {
+ // 使用更智能的解析方式,支持基本的加减运算
+ const numbers = cleanExpression.split(/[+-]/).filter(Boolean)
+ const operators = cleanExpression.split(/[^+-]/).filter(Boolean)
+
+ if (numbers.length === 0)
+ return '0'
+
+ let result = new Big(numbers[0])
+
+ for (let i = 0; i < operators.length; i++) {
+ const operator = operators[i]
+ const nextNumber = new Big(numbers[i + 1])
+
+ if (operator === '+') {
+ result = result.plus(nextNumber)
+ }
+ else if (operator === '-') {
+ result = result.minus(nextNumber)
+ }
+ }
+
+ // 限制结果为最多两位小数
+ return result.toFixed(2)
+ }
+ catch (error) {
+ // 如果解析失败,尝试只计算数字部分
+ const tokens = expression.match(/[+-]?\d+(\.\d+)?/g)
+ if (!tokens || tokens.length === 0)
+ return '0'
+
+ let total = new Big(0)
+ tokens.forEach((token) => {
+ total = total.plus(new Big(token))
+ })
+
+ // 限制结果为最多两位小数
+ return total.toFixed(2)
+ }
+ }
+
+ if (!show) {
+ return null
+ }
+
+ return (
+
+
+
+
+ setShowDatePicker(true)}
+ >
+
+ {internalData.record_time}
+
+
+ {internalData.amount}
+
+
+ {keydownInput}
+ handleChange({ remark: val })}
+ onFocus={() => setRemarkIsFocused(true)}
+ onBlur={() => setRemarkIsFocused(false)}
+ />
+
+
+
+ {keys.map((item, idx) => {
+ const isFirstCol = idx % 4 === 0
+ const isFirstRow = idx < 4
+ const isLastCol = (idx + 1) % 4 === 0
+ const isLastRow = idx >= 12
+
+ return (
+ handlePress(item.key)}
+ style={{
+ width: '25%',
+ height: '25%',
+ justifyContent: 'center',
+ alignItems: 'center',
+ borderTopWidth: isFirstRow ? 0.3 : 0,
+ borderLeftWidth: isFirstCol ? 0.3 : 0,
+ borderRightWidth: !isLastCol ? 0.3 : 0,
+ borderBottomWidth: !isLastRow ? 0.3 : 0,
+ borderColor: '#ddd',
+ }}
+ >
+ {({ pressed }) => {
+ const defaultBg = item.key === '/' ? '#e6d7b8' : '#fff'
+ const pressedBg = item.key === '/' ? '#f5e9d4' : '#f2f2f2'
+
+ return (
+ <>
+
+ {item.content}
+ >
+ )
+ }}
+
+ )
+ })}
+
+ {remarkIsFocused && keyboardHeight > 220 && (
+
+ )}
+
+ {
+ setShowDatePicker(false)
+ }}
+ onConfirm={(date) => {
+ setShowDatePicker(false)
+ handleChange({ record_time: date })
+ }}
+ />
+
+ )
+}
diff --git a/code/allapp/allapp-rn/src/app/finance/record/editor/components/CategoryGrid.tsx b/code/allapp/allapp-rn/src/app/finance/record/editor/components/CategoryGrid.tsx
new file mode 100644
index 00000000..423f3f4b
--- /dev/null
+++ b/code/allapp/allapp-rn/src/app/finance/record/editor/components/CategoryGrid.tsx
@@ -0,0 +1,96 @@
+import type { ViewStyle } from 'react-native'
+import type { FinanceCategory } from '@/types/finance/category'
+import React, { useEffect, useRef } from 'react'
+import { FlatList, Pressable, Text, View } from 'react-native'
+import MoneyCategoryIcon from '@/components/common/MoneyCategoryIcon'
+
+interface CategoryGridProps {
+ data: FinanceCategory[]
+ selectedCategory?: FinanceCategory | null
+ onSelectCategory?: (item: FinanceCategory | null) => void
+
+ className?: string
+ style?: ViewStyle
+}
+
+export default function CategoryGrid({
+ data,
+ selectedCategory,
+ onSelectCategory,
+ className,
+ style,
+}: CategoryGridProps) {
+ const NUM_COLUMNS = 4
+
+ const flatListRef = useRef>(null)
+
+ // 自动滚动到选中行
+ useEffect(() => {
+ if (!selectedCategory || !flatListRef.current)
+ return
+
+ const itemIndex = data.findIndex(item => item.id === selectedCategory.id)
+ if (itemIndex === -1 || !flatListRef.current)
+ return
+
+ setTimeout(() => {
+ if (flatListRef.current) {
+ flatListRef.current.scrollToIndex({
+ index: Math.floor(itemIndex / 4),
+ animated: true,
+ viewPosition: 0.5,
+ })
+ }
+ }, 0)
+ }, [selectedCategory])
+
+ return (
+ item.id.toString()}
+ showsVerticalScrollIndicator={false}
+ className={`${className}`}
+ style={[style]}
+ getItemLayout={(data, index) => {
+ return { length: 80, offset: 80 * index, index }
+ }}
+ renderItem={({ item }) => {
+ const isSelected = selectedCategory?.id === item.id
+
+ return (
+ onSelectCategory?.(item)}
+ >
+
+
+
+
+ {item.name}
+
+
+ )
+ }}
+ />
+ )
+}
diff --git a/code/allapp/allapp-rn/src/app/finance/record/editor/index.tsx b/code/allapp/allapp-rn/src/app/finance/record/editor/index.tsx
new file mode 100644
index 00000000..2871d104
--- /dev/null
+++ b/code/allapp/allapp-rn/src/app/finance/record/editor/index.tsx
@@ -0,0 +1,188 @@
+import type { FinanceCategory } from '@/types/finance/category'
+import type { FinanceRecord } from '@/types/finance/record'
+import { useLocalSearchParams, useRouter } from 'expo-router'
+import { useEffect, useState } from 'react'
+import { Text, TouchableOpacity, View } from 'react-native'
+import { toast } from 'sonner-native'
+import { LucideIcon } from '@/components/common/ao'
+import CustomHeader from '@/components/custom/Header'
+import PagerTabs from '@/components/custom/PagerTabs'
+import { createFinanceRecordApi, updateFinanceRecordApi } from '@/request/api/finance/record'
+import { useFinanceStore } from '@/store/finance'
+import { getCurrentDate, toDateString } from '@/utils/time'
+import AmountKeyboard from './components/AmountKeyboard'
+import CategoryGrid from './components/CategoryGrid'
+
+export default function MoenyRecordEditor() {
+ const financeStore = useFinanceStore()
+
+ const router = useRouter()
+ const expense = financeStore.getExpenseCategories()
+ const income = financeStore.getIncomeCategories()
+
+ const { id: updateId } = useLocalSearchParams<{ id: string }>()
+ // 自定义键盘
+ const [showKeydown, setShowKeydown] = useState(false)
+ // 选中分类
+ const [selectedCategory, setSelectedCategory] = useState(null)
+ // 记账记录
+ const defaultFinanceRecord: FinanceRecord = {
+ id: -1,
+ category_id: -1,
+ amount: '0.00',
+ remark: '',
+ record_time: getCurrentDate(),
+ member_id: -1,
+ member_name: '',
+ }
+
+ const [financeRecord, setFinanceRecord] = useState(defaultFinanceRecord)
+
+ useEffect(() => {
+ if (updateId) {
+ const updateData = financeStore.getRecordById(Number(updateId))
+ if (updateData !== null) {
+ setFinanceRecord({
+ ...updateData,
+ record_time: toDateString(updateData.record_time),
+ })
+ setShowKeydown(true)
+
+ const moneyCategory = financeStore.getCategoryById(updateData.category_id)
+
+ if (moneyCategory != null) {
+ setSelectedCategory(moneyCategory)
+ }
+ }
+ }
+ }, [])
+
+ // 保存记录
+ function handleSaveRecord() {
+ if (selectedCategory === null) {
+ toast.warning('请选择分类')
+ return
+ }
+
+ if (financeRecord.amount === '0.00') {
+ toast.warning('请输入金额')
+ return
+ }
+
+ const amount = financeRecord.amount.replace(/^-+/, '')
+
+ if (updateId) {
+ updateFinanceRecordApi({
+ id: financeRecord.id,
+ category_id: selectedCategory.id,
+ amount,
+ remark: financeRecord.remark,
+ record_time: financeRecord.record_time,
+ }).then((res) => {
+ if (res.isSuccess) {
+ router.back()
+ }
+ })
+ }
+ else {
+ createFinanceRecordApi({
+ amount,
+ category_id: selectedCategory.id,
+ remark: financeRecord.remark,
+ record_time: financeRecord.record_time,
+ })
+ .then((res) => {
+ if (res.isSuccess) {
+ router.back()
+ }
+ })
+ }
+ }
+
+ return (
+
+
+
+ {
+ router.navigate('/finance/category/setting')
+ }}
+ >
+
+ 管理
+
+ )}
+ right={(
+ {
+ router.back()
+ }}
+ >
+ 取消
+
+ )}
+ routes={[
+ {
+ key: 'expense',
+ title: '支出',
+ element: (
+ {
+ setShowKeydown(true)
+ setSelectedCategory(data)
+ }}
+ className="mt-4"
+ />
+ ),
+ },
+ {
+ key: 'income',
+ title: '收入',
+ element: (
+ {
+ setShowKeydown(true)
+ setSelectedCategory(data)
+ }}
+ className="mt-4"
+ />
+ ),
+ },
+ ]}
+ />
+
+ {
+ setFinanceRecord({
+ ...financeRecord,
+ record_time: data.record_time,
+ amount: data.amount,
+ remark: data.remark,
+ })
+ }}
+ onDone={handleSaveRecord}
+ />
+
+
+ )
+}
diff --git a/code/allapp/allapp-rn/src/app/space/sub-page/settings/index.tsx b/code/allapp/allapp-rn/src/app/space/sub-page/settings/index.tsx
index db2e6267..1e978962 100644
--- a/code/allapp/allapp-rn/src/app/space/sub-page/settings/index.tsx
+++ b/code/allapp/allapp-rn/src/app/space/sub-page/settings/index.tsx
@@ -1,5 +1,5 @@
import type { UserSpace } from '@/types/space'
-import { router, useFocusEffect, useLocalSearchParams, useNavigation } from 'expo-router'
+import { router, useFocusEffect, useLocalSearchParams } from 'expo-router'
import { useCallback, useEffect, useRef, useState } from 'react'
import { ActivityIndicator, Text, TouchableOpacity, View } from 'react-native'
import { KeyboardAvoidingView } from 'react-native-keyboard-controller'
diff --git a/code/allapp/allapp-rn/src/components/ui/spinner/index.tsx b/code/allapp/allapp-rn/src/components/ui/spinner/index.tsx
new file mode 100644
index 00000000..f5e88ed9
--- /dev/null
+++ b/code/allapp/allapp-rn/src/components/ui/spinner/index.tsx
@@ -0,0 +1,40 @@
+'use client';
+import { ActivityIndicator } from 'react-native';
+import React from 'react';
+import { tva } from '@gluestack-ui/utils/nativewind-utils';
+import { cssInterop } from 'nativewind';
+
+cssInterop(ActivityIndicator, {
+ className: { target: 'style', nativeStyleToProp: { color: true } },
+});
+
+const spinnerStyle = tva({});
+
+const Spinner = React.forwardRef<
+ React.ComponentRef,
+ React.ComponentProps
+>(function Spinner(
+ {
+ className,
+ color,
+ focusable = false,
+ 'aria-label': ariaLabel = 'loading',
+ ...props
+ },
+ ref
+) {
+ return (
+
+ );
+});
+
+Spinner.displayName = 'Spinner';
+
+export { Spinner };
diff --git a/code/allapp/allapp-rn/src/request/api/finance/category.ts b/code/allapp/allapp-rn/src/request/api/finance/category.ts
new file mode 100644
index 00000000..76ce67be
--- /dev/null
+++ b/code/allapp/allapp-rn/src/request/api/finance/category.ts
@@ -0,0 +1,24 @@
+import type { CreateFinanceCategoryParams, CreateFinanceCategoryResult, DeleteFinanceCategoryParams, ListFinanceCategoriesResult, UpdateFinanceCategoryParams, UpdateFinanceCategorySortParams } from '@/types/finance/category'
+import alovaInstance from '../../index'
+
+const API_PREFIX = '/finance/category'
+
+export function listFinanceCategoriesApi() {
+ return alovaInstance.Post>(`${API_PREFIX}/list`)
+}
+
+export function createFinanceCategoryApi(params: CreateFinanceCategoryParams) {
+ return alovaInstance.Post>(`${API_PREFIX}/create`, params)
+}
+
+export function updateFinanceCategoryApi(params: UpdateFinanceCategoryParams) {
+ return alovaInstance.Post>(`${API_PREFIX}/update`, params)
+}
+
+export function updateFinanceCategorySortNumberApi(params: UpdateFinanceCategorySortParams) {
+ return alovaInstance.Post>(`${API_PREFIX}/sortNumber`, params)
+}
+
+export function deleteFinanceCategoryApi(params: DeleteFinanceCategoryParams) {
+ return alovaInstance.Post>(`${API_PREFIX}/delete`, params)
+}
diff --git a/code/allapp/allapp-rn/src/request/api/finance/record.ts b/code/allapp/allapp-rn/src/request/api/finance/record.ts
new file mode 100644
index 00000000..878663db
--- /dev/null
+++ b/code/allapp/allapp-rn/src/request/api/finance/record.ts
@@ -0,0 +1,20 @@
+import type { CreateFinanceRecordParams, DeleteFinanceRecordParams, ListFinanceRecordsParams, ListFinanceRecordsResult, UpdateFinanceRecordParams } from '@/types/finance/record'
+import alovaInstance from '../../index'
+
+const API_PREFIX = '/finance/record'
+
+export function listFinanceRecordsApi(params: ListFinanceRecordsParams) {
+ return alovaInstance.Post>(`${API_PREFIX}/list`, params)
+}
+
+export function createFinanceRecordApi(params: CreateFinanceRecordParams) {
+ return alovaInstance.Post>(`${API_PREFIX}/create`, params)
+}
+
+export function updateFinanceRecordApi(params: UpdateFinanceRecordParams) {
+ return alovaInstance.Post>(`${API_PREFIX}/update`, params)
+}
+
+export function deleteFinanceRecordApi(params: DeleteFinanceRecordParams) {
+ return alovaInstance.Post>(`${API_PREFIX}/delete`, params)
+}
diff --git a/code/allapp/allapp-rn/src/request/index.ts b/code/allapp/allapp-rn/src/request/index.ts
index e1b1517e..960c014f 100644
--- a/code/allapp/allapp-rn/src/request/index.ts
+++ b/code/allapp/allapp-rn/src/request/index.ts
@@ -31,8 +31,8 @@ function isWhiteListed(url: string) {
}
const alovaInstance = createAlova({
- baseURL: 'http://192.168.5.6:8086/app',
- // baseURL: 'http://192.168.1.137:8086/app',
+ // baseURL: 'http://192.168.5.6:8086/app',
+ baseURL: 'http://192.168.1.137:8086/app',
requestAdapter: axiosRequestAdapter(),
timeout: 5000,
cacheFor: null,
diff --git a/code/allapp/allapp-rn/src/store/finance.ts b/code/allapp/allapp-rn/src/store/finance.ts
new file mode 100644
index 00000000..c4dc0ce1
--- /dev/null
+++ b/code/allapp/allapp-rn/src/store/finance.ts
@@ -0,0 +1,373 @@
+import type { FinanceCategory, IconGroupType } from '@/types/finance/category'
+import type { FinanceRecord, FinanceRecordGroup } from '@/types/finance/record'
+import { create } from 'zustand'
+import { listFinanceCategoriesApi } from '@/request/api/finance/category'
+import { FinanceCategoryEnum } from '@/types/finance/category'
+import { add, sub } from '@/utils/math'
+
+interface FinanceState {
+ // 分类
+ DefaultGroupCategories: IconGroupType[]
+ categories: FinanceCategory[]
+ setCategories: (categories: FinanceCategory[]) => void
+ ensureCategoriesLoaded: () => Promise
+ getExpenseCategories: () => FinanceCategory[]
+ getIncomeCategories: () => FinanceCategory[]
+ createCategory: (category: FinanceCategory) => void
+ updateCategory: (category: FinanceCategory) => void
+ deleteCategory: (id: number) => void
+ updateCategorySortNumber: (data: FinanceCategory[], type: number) => void
+ isDuplicateCategory: (name: string, type: number, excludeId: number | null) => boolean
+ getDefaultGroupRow: (iconName: string) => number
+ getCategoryById: (id: number) => FinanceCategory | null
+ getCategoryNextSortNumber: (type: number) => number
+
+ // 记录
+ records: FinanceRecordGroup[]
+ getTotalIncome: () => string
+ getTotalExpense: () => string
+ getTotalBalance: () => string
+ setRecords: (records: FinanceRecord[]) => void
+ getRecordById: (id: number) => FinanceRecord | null
+}
+
+function recomputeGroups(records: FinanceRecord[]) {
+ const { getCategoryById } = useFinanceStore.getState()
+
+ const groupsMap: Record = {}
+
+ records.forEach((r) => {
+ const category = getCategoryById(r.category_id)
+ if (!category)
+ return
+
+ const group = groupsMap[r.record_time] ?? {
+ record_time: r.record_time,
+ records: [],
+ income: '0.00',
+ expense: '0.00',
+ }
+
+ group.records.push(r)
+
+ if (category.type === FinanceCategoryEnum.Expense)
+ group.expense = add(group.expense, r.amount)
+
+ if (category.type === FinanceCategoryEnum.Income)
+ group.income = add(group.income, r.amount)
+
+ groupsMap[r.record_time] = group
+ })
+
+ return Object.values(groupsMap).sort((a, b) =>
+ b.record_time.localeCompare(a.record_time),
+ )
+}
+
+export const useFinanceStore = create()(
+ (set, get) => ({
+ // ================= 分类 =================
+ DefaultGroupCategories: [
+ {
+ index: 0,
+ type: 'title',
+ value: '餐饮',
+ },
+ {
+ index: 1,
+ type: 'icons',
+ value: [
+ 'money_canyin',
+ 'money_yinpin',
+ 'money_lingshi',
+ 'money_xiaoye',
+ ],
+ },
+ {
+ index: 2,
+ type: 'title',
+ value: '娱乐',
+ },
+ {
+ index: 3,
+ type: 'icons',
+ value: [
+ 'money_yule',
+ 'money_youxi',
+ 'money_dianying',
+ ],
+ },
+ {
+ index: 4,
+ type: 'title',
+ value: '交通',
+ },
+ {
+ index: 5,
+ type: 'icons',
+ value: [
+ 'money_jiaotong',
+ 'money_tingchefei',
+ 'money_jiayou',
+ ],
+ },
+ {
+ index: 6,
+ type: 'title',
+ value: '生活消费',
+ },
+ {
+ index: 7,
+ type: 'icons',
+ value: [
+ 'money_lijin',
+ 'money_jiadian',
+ 'money_riyongpin',
+ 'money_shucai',
+ 'money_meifa',
+ ],
+ },
+ {
+ index: 8,
+ type: 'icons',
+ value: [
+ 'money_shuidian',
+ 'money_wenju',
+ 'money_zhufang',
+ 'money_baoxian',
+ 'money_baojianpin',
+ ],
+ },
+ {
+ index: 9,
+ type: 'icons',
+ value: [
+ 'money_huafei',
+ 'money_lvyou',
+ 'money_gouwu',
+ 'money_shuben',
+ 'money_chongwu',
+ ],
+ },
+ {
+ index: 10,
+ type: 'icons',
+ value: [
+ 'money_ruanjian',
+ ],
+ },
+ {
+ index: 11,
+ type: 'title',
+ value: '健康',
+ },
+ {
+ index: 12,
+ type: 'icons',
+ value: [
+ 'money_tijian',
+ 'money_yao',
+ 'money_jiuyi',
+ ],
+ },
+ {
+ index: 13,
+ type: 'title',
+ value: '公益',
+ },
+ {
+ index: 14,
+ type: 'icons',
+ value: [
+ 'money_gongyi',
+ ],
+ },
+ {
+ index: 15,
+ type: 'title',
+ value: '兼职与收入',
+ },
+ {
+ index: 16,
+ type: 'icons',
+ value: [
+ 'money_gongzi',
+ 'money_jianzhi',
+ ],
+ },
+ {
+ index: 17,
+ type: 'title',
+ value: '鲜花与水果',
+ },
+ {
+ index: 18,
+ type: 'icons',
+ value: [
+ 'money_shuiguo',
+ 'money_xianhua',
+ ],
+ },
+ {
+ index: 19,
+ type: 'title',
+ value: '转账与转移',
+ },
+ {
+ index: 20,
+ type: 'icons',
+ value: [
+ 'money_zhuanzhang',
+ ],
+ },
+ {
+ index: 21,
+ type: 'title',
+ value: '家庭与家电',
+ },
+ {
+ index: 22,
+ type: 'icons',
+ value: [
+ 'money_kuaidi',
+ 'money_weixiu',
+ 'money_jiaju',
+ 'money_muying',
+ 'money_liwu',
+ ],
+ },
+ {
+ index: 23,
+ type: 'title',
+ value: '运动与娱乐',
+ },
+ {
+ index: 24,
+ type: 'icons',
+ value: [
+ 'money_yundong',
+ 'money_peixun',
+ ],
+ },
+ ],
+
+ categories: [],
+
+ setCategories: categories =>
+ set({
+ categories: [...categories].sort(
+ (a, b) => (a.sort_number ?? 0) - (b.sort_number ?? 0),
+ ),
+ }),
+
+ ensureCategoriesLoaded: async () => {
+ if (get().categories.length > 0)
+ return
+ const res = await listFinanceCategoriesApi()
+ if (res.isSuccess) {
+ set({ categories: res.data.categories })
+ }
+ },
+
+ getExpenseCategories: () =>
+ get().categories.filter(c => c.type === FinanceCategoryEnum.Expense),
+
+ getIncomeCategories: () =>
+ get().categories.filter(c => c.type === FinanceCategoryEnum.Income),
+
+ getCategoryById: (id: number) => {
+ return get().categories.find(c => c.id === id) || null
+ },
+
+ createCategory: (category) => {
+ set({
+ categories: [...get().categories, category],
+ })
+ },
+ updateCategory: (category) => {
+ set(state => ({
+ categories: state.categories.map(c =>
+ c.id !== category.id
+ ? c
+ : {
+ ...c,
+ ...category,
+ },
+ ),
+ }))
+ },
+ deleteCategory: (id) => {
+ const allCategories = get().categories
+ const finalCategories = allCategories.filter(c => c.id !== id)
+ set({ categories: finalCategories })
+ },
+ updateCategorySortNumber: (data, type) => {
+ const allCategories = get().categories
+ const otherCategories = allCategories.filter(c => c.type !== type)
+ const updatedData = data.map((item, index) => ({
+ ...item,
+ sort_number: index,
+ }))
+
+ set({ categories: [...otherCategories, ...updatedData] })
+ },
+ isDuplicateCategory: (name, type, excludeId) => {
+ const categories = get().categories || []
+ return categories.some(c =>
+ c.name === name
+ && c.type === type
+ && c.id !== excludeId,
+ )
+ },
+ getDefaultGroupRow: (iconName) => {
+ const iconGroups = get().DefaultGroupCategories
+
+ for (const group of iconGroups) {
+ if (group.type === 'icons' && group.value.includes(iconName)) {
+ return group.index
+ }
+ }
+
+ return 0
+ },
+ getCategoryNextSortNumber: (type: number): number => {
+ const categories = get().categories.filter(c => c.type === type)
+
+ if (categories.length === 0) {
+ return 0
+ }
+
+ const maxSortNumber = Math.max(...categories.map(c => c.sort_number))
+
+ return maxSortNumber + 1
+ },
+ // ================= 分类 =================
+
+ // ================= 记录 =================
+ records: [],
+ getTotalIncome: () => {
+ return get().records.reduce((sum, g) => add(sum, g.income), '0.00')
+ },
+
+ getTotalExpense: () => {
+ return get().records.reduce((sum, g) => add(sum, g.expense), '0.00')
+ },
+
+ getTotalBalance: () => {
+ return sub(get().getTotalIncome(), get().getTotalExpense())
+ },
+ setRecords: (records) => {
+ const newGroups = recomputeGroups(records)
+ set({ records: newGroups })
+ },
+ getRecordById: (id) => {
+ for (const group of get().records) {
+ const record = group.records.find(r => r.id === id)
+ if (record)
+ return record
+ }
+ return null
+ },
+ // ================= 记录 =================
+ }),
+)
diff --git a/code/allapp/allapp-rn/src/store/navigation.ts b/code/allapp/allapp-rn/src/store/navigation.ts
index 97486647..eda481fc 100644
--- a/code/allapp/allapp-rn/src/store/navigation.ts
+++ b/code/allapp/allapp-rn/src/store/navigation.ts
@@ -19,7 +19,7 @@ interface NavState {
}
const DEFAULT_NAV_MENUS: NavItem[] = [
- { label: '记账', path: '/money', icon: 'CircleDollarSign' },
+ { label: '记账', path: '/finance', icon: 'CircleDollarSign' },
{ label: '恋爱记', path: '/love', icon: 'Heart' },
{ label: '聊天', path: '/chat', icon: 'MessageCircle' },
{ label: '个人中心', path: '/user', icon: 'CircleUserRound' },
diff --git a/code/allapp/allapp-rn/src/types/finance/category.ts b/code/allapp/allapp-rn/src/types/finance/category.ts
new file mode 100644
index 00000000..a8f1be08
--- /dev/null
+++ b/code/allapp/allapp-rn/src/types/finance/category.ts
@@ -0,0 +1,69 @@
+export interface FinanceCategory {
+ id: number
+ name: string
+ icon: string
+ type: number
+ sort_number: number
+}
+
+export enum FinanceCategoryEnum {
+ Expense = 0, // 支出
+ Income = 1, // 收入
+}
+
+// 记账分类
+export interface ListFinanceCategoriesResult {
+ categories: FinanceCategory[]
+}
+
+// 创建记账分类
+export interface CreateFinanceCategoryParams {
+ name: string
+ icon: string
+ type: number
+ sort_number: number
+}
+export interface CreateFinanceCategoryResult {
+ id: number
+}
+
+// 更新记账分类排序
+export interface UpdateFinanceCategorySortItem {
+ id: number
+ sort_number: number
+}
+export interface UpdateFinanceCategorySortParams {
+ categories: UpdateFinanceCategorySortItem[]
+}
+
+// 删除记账分类
+export interface DeleteFinanceCategoryParams {
+ id: number
+}
+
+// 更新记账分类
+export interface UpdateFinanceCategoryParams {
+ id: number
+ name: string
+ icon: string
+}
+
+// 图标分组基础接口
+interface BaseIconGroupType {
+ index: number
+}
+
+// 当 type 为 'title' 时,value 必须是 string
+interface TitleIconGroupType extends BaseIconGroupType {
+ type: 'title'
+ value: string
+}
+
+// 当 type 为 'icons' 时,value 必须是 string[]
+interface IconsIconGroupType extends BaseIconGroupType {
+ type: 'icons'
+ value: string[]
+}
+
+// 联合类型
+export type IconGroupType = TitleIconGroupType | IconsIconGroupType
diff --git a/code/allapp/allapp-rn/src/types/finance/record.ts b/code/allapp/allapp-rn/src/types/finance/record.ts
new file mode 100644
index 00000000..2465c38f
--- /dev/null
+++ b/code/allapp/allapp-rn/src/types/finance/record.ts
@@ -0,0 +1,45 @@
+export interface FinanceRecordGroup {
+ record_time: string
+ expense: string
+ income: string
+ records: FinanceRecord[]
+}
+
+export interface FinanceRecord {
+ id: number
+ category_id: number
+ amount: string
+ remark: string
+ record_time: string
+ member_id: number
+ member_name: string
+}
+
+export interface ListFinanceRecordsParams {
+ start_time: string
+ end_time: string
+ category_id: number
+}
+
+export interface ListFinanceRecordsResult {
+ records: FinanceRecord[]
+}
+
+export interface CreateFinanceRecordParams {
+ category_id: number
+ amount: string
+ record_time: string
+ remark: string
+}
+
+export interface UpdateFinanceRecordParams {
+ id: number
+ category_id: number
+ amount: string
+ record_time: string
+ remark: string
+}
+
+export interface DeleteFinanceRecordParams {
+ id: number
+}