u
This commit is contained in:
1 parent
34240d2e16
commit
bf350d8d35
21 files changed
+670
-304
No files matched your search
@@ -5,6 +5,7 @@ import (
|
|||||||
"allapp/api/types"
|
"allapp/api/types"
|
||||||
"allapp/db"
|
"allapp/db"
|
||||||
"allapp/db/repo"
|
"allapp/db/repo"
|
||||||
|
"allapp/utils/errorx"
|
||||||
"allapp/utils/pgtypex"
|
"allapp/utils/pgtypex"
|
||||||
"allapp/utils/requestx"
|
"allapp/utils/requestx"
|
||||||
"allapp/utils/responsex"
|
"allapp/utils/responsex"
|
||||||
@@ -57,6 +58,7 @@ func ListFinanceRecords(c fiber.Ctx) error {
|
|||||||
RecordTime: pgtypex.TimePtrToPgDate(startTime),
|
RecordTime: pgtypex.TimePtrToPgDate(startTime),
|
||||||
RecordTime_2: pgtypex.TimePtrToPgDate(endTime),
|
RecordTime_2: pgtypex.TimePtrToPgDate(endTime),
|
||||||
Column4: categoryId,
|
Column4: categoryId,
|
||||||
|
Column5: dto.CategoryType,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -131,7 +133,7 @@ func UpdateFinanceRecord(c fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
err = db.Queries.UpdateFinanceRecord(c.Context(), repo.UpdateFinanceRecordParams{
|
err = db.Queries.UpdateFinanceRecord(c.Context(), repo.UpdateFinanceRecordParams{
|
||||||
ID: uniqueid.NextId(),
|
ID: dto.Id,
|
||||||
CategoryID: dto.CategoryId,
|
CategoryID: dto.CategoryId,
|
||||||
Amount: pgtypex.StringToNumeric(dto.Amount),
|
Amount: pgtypex.StringToNumeric(dto.Amount),
|
||||||
Remark: pgtypex.StringToText(dto.Remark),
|
Remark: pgtypex.StringToText(dto.Remark),
|
||||||
@@ -161,3 +163,38 @@ func DeleteFinanceRecord(c fiber.Ctx) error {
|
|||||||
|
|
||||||
return responsex.OK(c, nil)
|
return responsex.OK(c, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetFinanceRecordDetail 删除记账记录
|
||||||
|
func GetFinanceRecordDetail(c fiber.Ctx) error {
|
||||||
|
dto := new(types.FinanceRecordDetailDTO)
|
||||||
|
if err := requestx.BindAndValidate(c, dto); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
recordDetail, err := db.Queries.GetFinanceRecordDetail(c.Context(), dto.Id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if errorx.IsNotFound(err) {
|
||||||
|
return responsex.Error(c, "该条记录不存在")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
vo := new(types.FinanceRecordDetailVO)
|
||||||
|
|
||||||
|
vo.Id = recordDetail.ID
|
||||||
|
vo.CategoryId = recordDetail.CategoryID
|
||||||
|
vo.Amount = pgtypex.NumericToString(recordDetail.Amount)
|
||||||
|
vo.RecordTime = pgtypex.DateToString(recordDetail.RecordTime)
|
||||||
|
vo.Remark = pgtypex.TextToString(recordDetail.Remark)
|
||||||
|
|
||||||
|
vo.CreateTime = pgtypex.TimestamptzToMillis(recordDetail.CreateTime)
|
||||||
|
vo.CreateMemberId = recordDetail.CreateBy
|
||||||
|
vo.CreateMemberName = pgtypex.TextToString(recordDetail.CreateMemberName)
|
||||||
|
|
||||||
|
vo.UpdateTime = pgtypex.TimestamptzToMillis(recordDetail.UpdateTime)
|
||||||
|
vo.UpdateMemberId = recordDetail.UpdateBy
|
||||||
|
vo.UpdateMemberName = pgtypex.TextToString(recordDetail.UpdateMemberName)
|
||||||
|
|
||||||
|
return responsex.OK(c, vo)
|
||||||
|
}
|
||||||
@@ -52,6 +52,7 @@ func SetupRouters(app *fiber.App) {
|
|||||||
financeRouter.Post("/record/create", finance.CreateFinanceRecord)
|
financeRouter.Post("/record/create", finance.CreateFinanceRecord)
|
||||||
financeRouter.Post("/record/update", finance.UpdateFinanceRecord)
|
financeRouter.Post("/record/update", finance.UpdateFinanceRecord)
|
||||||
financeRouter.Post("/record/delete", finance.DeleteFinanceRecord)
|
financeRouter.Post("/record/delete", finance.DeleteFinanceRecord)
|
||||||
|
financeRouter.Post("/record/detail", finance.GetFinanceRecordDetail)
|
||||||
|
|
||||||
// authGroup.Get("/user/profile", user.GetProfile)
|
// authGroup.Get("/user/profile", user.GetProfile)
|
||||||
// spaceGroup.Get("/space/info", space.GetInfo)
|
// spaceGroup.Get("/space/info", space.GetInfo)
|
||||||
|
|||||||
@@ -63,9 +63,10 @@ type FinanceRecord struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ListFinanceRecordsDTO struct {
|
type ListFinanceRecordsDTO struct {
|
||||||
StartTime string `json:"start_time"`
|
StartTime string `json:"start_time"`
|
||||||
EndTime string `json:"end_time"`
|
EndTime string `json:"end_time"`
|
||||||
CategoryId int64 `json:"category_id"`
|
CategoryId int64 `json:"category_id"`
|
||||||
|
CategoryType int16 `json:"category_type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListFinanceRecordsVO struct {
|
type ListFinanceRecordsVO struct {
|
||||||
@@ -93,3 +94,21 @@ type UpdateFinanceRecordDTO struct {
|
|||||||
type DeleteFinanceRecordDTO struct {
|
type DeleteFinanceRecordDTO struct {
|
||||||
Id int64 `json:"id" validate:"required"`
|
Id int64 `json:"id" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FinanceRecordDetailDTO struct {
|
||||||
|
Id int64 `json:"id" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FinanceRecordDetailVO struct {
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
CategoryId int64 `json:"category_id"`
|
||||||
|
Amount string `json:"amount"`
|
||||||
|
RecordTime string `json:"record_time"`
|
||||||
|
Remark string `json:"remark"`
|
||||||
|
CreateTime int64 `json:"create_time"`
|
||||||
|
CreateMemberId int64 `json:"create_member_id"`
|
||||||
|
CreateMemberName string `json:"create_member_name"`
|
||||||
|
UpdateTime int64 `json:"update_time"`
|
||||||
|
UpdateMemberId int64 `json:"update_member_id"`
|
||||||
|
UpdateMemberName string `json:"update_member_name"`
|
||||||
|
}
|
||||||
@@ -4,16 +4,14 @@ SELECT
|
|||||||
su.member_name
|
su.member_name
|
||||||
FROM finance_record fr
|
FROM finance_record fr
|
||||||
LEFT JOIN space_member su
|
LEFT JOIN space_member su
|
||||||
ON fr.space_id = su.space_id
|
ON fr.space_id = su.space_id AND fr.update_by = su.user_id
|
||||||
AND fr.update_by = su.user_id
|
LEFT JOIN finance_category_space fcs ON fr.category_id = fcs.id
|
||||||
WHERE
|
WHERE
|
||||||
fr.space_id = $1
|
fr.space_id = $1
|
||||||
AND fr.record_time >= $2
|
AND fr.record_time >= $2
|
||||||
AND fr.record_time < $3
|
AND fr.record_time < $3
|
||||||
AND (
|
AND fr.category_id = COALESCE(NULLIF($4::bigint, 0), fr.category_id)
|
||||||
$4 = 0
|
AND fcs.type = COALESCE(NULLIF($5::smallint, 0), fcs.type)
|
||||||
OR fr.category_id = $4
|
|
||||||
)
|
|
||||||
ORDER BY
|
ORDER BY
|
||||||
fr.record_time DESC,
|
fr.record_time DESC,
|
||||||
fr.create_time DESC;
|
fr.create_time DESC;
|
||||||
@@ -37,4 +35,23 @@ WHERE id = $1;
|
|||||||
DELETE FROM finance_record WHERE id = $1;
|
DELETE FROM finance_record WHERE id = $1;
|
||||||
|
|
||||||
-- name: DeleteFinanceRecordByCategoryId :exec
|
-- name: DeleteFinanceRecordByCategoryId :exec
|
||||||
DELETE FROM finance_record WHERE category_id = $1;
|
DELETE FROM finance_record WHERE category_id = $1;
|
||||||
|
|
||||||
|
-- name: GetFinanceRecordDetail :one
|
||||||
|
SELECT
|
||||||
|
fr.id,
|
||||||
|
fr.category_id,
|
||||||
|
fr.amount,
|
||||||
|
fr.record_time,
|
||||||
|
fr.remark,
|
||||||
|
fr.create_by,
|
||||||
|
fr.create_time,
|
||||||
|
cm.nickname AS create_member_name,
|
||||||
|
fr.update_by,
|
||||||
|
fr.update_time,
|
||||||
|
um.nickname AS update_member_name
|
||||||
|
FROM finance_record fr
|
||||||
|
LEFT JOIN "user" cm ON cm.id = fr.create_by
|
||||||
|
LEFT JOIN "user" um ON um.id = fr.update_by
|
||||||
|
WHERE fr.id = $1
|
||||||
|
LIMIT 1;
|
||||||
@@ -29,6 +29,59 @@ func (q *Queries) DeleteFinanceRecordByCategoryId(ctx context.Context, categoryI
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getFinanceRecordDetail = `-- name: GetFinanceRecordDetail :one
|
||||||
|
SELECT
|
||||||
|
fr.id,
|
||||||
|
fr.category_id,
|
||||||
|
fr.amount,
|
||||||
|
fr.record_time,
|
||||||
|
fr.remark,
|
||||||
|
fr.create_by,
|
||||||
|
fr.create_time,
|
||||||
|
cm.nickname AS create_member_name,
|
||||||
|
fr.update_by,
|
||||||
|
fr.update_time,
|
||||||
|
um.nickname AS update_member_name
|
||||||
|
FROM finance_record fr
|
||||||
|
LEFT JOIN "user" cm ON cm.id = fr.create_by
|
||||||
|
LEFT JOIN "user" um ON um.id = fr.update_by
|
||||||
|
WHERE fr.id = $1
|
||||||
|
LIMIT 1
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetFinanceRecordDetailRow struct {
|
||||||
|
ID int64
|
||||||
|
CategoryID int64
|
||||||
|
Amount pgtype.Numeric
|
||||||
|
RecordTime pgtype.Date
|
||||||
|
Remark pgtype.Text
|
||||||
|
CreateBy int64
|
||||||
|
CreateTime pgtype.Timestamptz
|
||||||
|
CreateMemberName pgtype.Text
|
||||||
|
UpdateBy int64
|
||||||
|
UpdateTime pgtype.Timestamptz
|
||||||
|
UpdateMemberName pgtype.Text
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetFinanceRecordDetail(ctx context.Context, id int64) (GetFinanceRecordDetailRow, error) {
|
||||||
|
row := q.db.QueryRow(ctx, getFinanceRecordDetail, id)
|
||||||
|
var i GetFinanceRecordDetailRow
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.CategoryID,
|
||||||
|
&i.Amount,
|
||||||
|
&i.RecordTime,
|
||||||
|
&i.Remark,
|
||||||
|
&i.CreateBy,
|
||||||
|
&i.CreateTime,
|
||||||
|
&i.CreateMemberName,
|
||||||
|
&i.UpdateBy,
|
||||||
|
&i.UpdateTime,
|
||||||
|
&i.UpdateMemberName,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
const insertFinanceRecord = `-- name: InsertFinanceRecord :exec
|
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)
|
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())
|
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,NOW(),NOW())
|
||||||
@@ -65,16 +118,14 @@ SELECT
|
|||||||
su.member_name
|
su.member_name
|
||||||
FROM finance_record fr
|
FROM finance_record fr
|
||||||
LEFT JOIN space_member su
|
LEFT JOIN space_member su
|
||||||
ON fr.space_id = su.space_id
|
ON fr.space_id = su.space_id AND fr.update_by = su.user_id
|
||||||
AND fr.update_by = su.user_id
|
LEFT JOIN finance_category_space fcs ON fr.category_id = fcs.id
|
||||||
WHERE
|
WHERE
|
||||||
fr.space_id = $1
|
fr.space_id = $1
|
||||||
AND fr.record_time >= $2
|
AND fr.record_time >= $2
|
||||||
AND fr.record_time < $3
|
AND fr.record_time < $3
|
||||||
AND (
|
AND fr.category_id = COALESCE(NULLIF($4::bigint, 0), fr.category_id)
|
||||||
$4 = 0
|
AND fcs.type = COALESCE(NULLIF($5::smallint, 0), fcs.type)
|
||||||
OR fr.category_id = $4
|
|
||||||
)
|
|
||||||
ORDER BY
|
ORDER BY
|
||||||
fr.record_time DESC,
|
fr.record_time DESC,
|
||||||
fr.create_time DESC
|
fr.create_time DESC
|
||||||
@@ -84,7 +135,8 @@ type ListFinanceRecordsParams struct {
|
|||||||
SpaceID int64
|
SpaceID int64
|
||||||
RecordTime pgtype.Date
|
RecordTime pgtype.Date
|
||||||
RecordTime_2 pgtype.Date
|
RecordTime_2 pgtype.Date
|
||||||
Column4 interface{}
|
Column4 int64
|
||||||
|
Column5 int16
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListFinanceRecordsRow struct {
|
type ListFinanceRecordsRow struct {
|
||||||
@@ -107,6 +159,7 @@ func (q *Queries) ListFinanceRecords(ctx context.Context, arg ListFinanceRecords
|
|||||||
arg.RecordTime,
|
arg.RecordTime,
|
||||||
arg.RecordTime_2,
|
arg.RecordTime_2,
|
||||||
arg.Column4,
|
arg.Column4,
|
||||||
|
arg.Column5,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -110,4 +110,3 @@ create table finance_record
|
|||||||
update_by bigint not null,
|
update_by bigint not null,
|
||||||
update_time timestamp with time zone default now()
|
update_time timestamp with time zone default now()
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ export default function TestScreen() {
|
|||||||
const spaceStore = useSpaceStore()
|
const spaceStore = useSpaceStore()
|
||||||
const navigation = useNavigation()
|
const navigation = useNavigation()
|
||||||
|
|
||||||
console.log(navigation.getState()?.routes)
|
console.log(navigation.getState()?.routeNames)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export default function CategorySheet({
|
|||||||
|
|
||||||
const handleSelect = useCallback(
|
const handleSelect = useCallback(
|
||||||
(item: FinanceCategory) => {
|
(item: FinanceCategory) => {
|
||||||
|
console.log(item)
|
||||||
onSelect?.(item)
|
onSelect?.(item)
|
||||||
onClose?.()
|
onClose?.()
|
||||||
},
|
},
|
||||||
@@ -74,9 +75,9 @@ export default function CategorySheet({
|
|||||||
{
|
{
|
||||||
title: '筛选',
|
title: '筛选',
|
||||||
data: [
|
data: [
|
||||||
createVirtualCategory(-1, '全部', -1),
|
createVirtualCategory(0, '全部', -1),
|
||||||
createVirtualCategory(-2, '支出', 0),
|
createVirtualCategory(-1, '支出', 0),
|
||||||
createVirtualCategory(-3, '收入', 1),
|
createVirtualCategory(-2, '收入', 1),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import type { FinanceRecord, FinanceRecordGroup } from '@/types/finance/record'
|
import type { FinanceRecord } from '@/types/finance/record'
|
||||||
import React, { useCallback, useState } from 'react'
|
import React, { useCallback, useEffect, useState } from 'react'
|
||||||
import { Pressable, RefreshControl, SectionList, Text, Vibration, View } from 'react-native'
|
import { Pressable, RefreshControl, SectionList, Text, Vibration, View } from 'react-native'
|
||||||
import { LucideIcon } from '@/components/common/ao'
|
import { LucideIcon } from '@/components/common/ao'
|
||||||
import EmptyState from '@/components/common/EmptyStatus'
|
import EmptyState from '@/components/common/EmptyStatus'
|
||||||
import MoneyCategoryIcon from '@/components/common/MoneyCategoryIcon'
|
import MoneyCategoryIcon from '@/components/common/MoneyCategoryIcon'
|
||||||
import { Menu, MenuItem, MenuItemLabel, MenuSeparator } from '@/components/ui/menu'
|
import { Menu, MenuItem, MenuItemLabel, MenuSeparator } from '@/components/ui/menu'
|
||||||
import { useMoneyCategoryStore } from '@/store/moneyCategory'
|
import { useFinanceStore } from '@/store/finance'
|
||||||
import { useUserStore } from '@/store/user'
|
import { useUserStore } from '@/store/user'
|
||||||
import { MoneyCategoryEnum } from '@/types/money/category'
|
import { FinanceCategoryEnum } from '@/types/finance/category'
|
||||||
|
|
||||||
interface StaticTransactionsProps {
|
interface StaticTransactionsProps {
|
||||||
records: FinanceRecordGroup[]
|
|
||||||
onUpdate?: (data: FinanceRecord) => void
|
onUpdate?: (data: FinanceRecord) => void
|
||||||
onDelete?: (id: number) => void
|
onDelete?: (id: number) => void
|
||||||
onDetail?: (id: number) => void
|
onDetail?: (id: number) => void
|
||||||
@@ -19,18 +18,18 @@ interface StaticTransactionsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function StaticTransactions({
|
export default function StaticTransactions({
|
||||||
records,
|
|
||||||
onUpdate,
|
onUpdate,
|
||||||
onDelete,
|
onDelete,
|
||||||
onDetail,
|
onDetail,
|
||||||
onLoadMore,
|
onLoadMore,
|
||||||
onRefreshData,
|
onRefreshData,
|
||||||
}: StaticTransactionsProps) {
|
}: StaticTransactionsProps) {
|
||||||
const moneyCategoryStore = useMoneyCategoryStore()
|
const financeStore = useFinanceStore()
|
||||||
const userID = useUserStore().userInfo?.id
|
const userID = useUserStore().userInfo?.id
|
||||||
const [refreshing, setRefreshing] = useState(false)
|
const [refreshing, setRefreshing] = useState(false)
|
||||||
const [loadingMore, setLoadingMore] = useState(false)
|
const [loadingMore, setLoadingMore] = useState(false)
|
||||||
const [selectedId, setSelectedId] = useState<number | null>(null)
|
const [selectedId, setSelectedId] = useState<number | null>(null)
|
||||||
|
const [sections, setSections] = useState<any[]>([])
|
||||||
|
|
||||||
const handleRefresh = useCallback(async () => {
|
const handleRefresh = useCallback(async () => {
|
||||||
if (!onRefreshData)
|
if (!onRefreshData)
|
||||||
@@ -56,12 +55,14 @@ export default function StaticTransactions({
|
|||||||
}
|
}
|
||||||
}, [onLoadMore, loadingMore])
|
}, [onLoadMore, loadingMore])
|
||||||
|
|
||||||
const sections = records.map(group => ({
|
useEffect(() => {
|
||||||
title: group.record_time,
|
setSections(financeStore.records.map(group => ({
|
||||||
totalIncome: group.income,
|
title: group.record_time,
|
||||||
totalExpense: group.expense,
|
totalIncome: group.income,
|
||||||
data: group.records,
|
totalExpense: group.expense,
|
||||||
}))
|
data: group.records,
|
||||||
|
})))
|
||||||
|
}, [financeStore.records])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionList
|
<SectionList
|
||||||
@@ -115,7 +116,7 @@ export default function StaticTransactions({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
renderItem={({ item }) => {
|
renderItem={({ item }) => {
|
||||||
const category = moneyCategoryStore.getCategoryById(item.category_id)
|
const category = financeStore.getCategoryById(item.category_id)
|
||||||
return (
|
return (
|
||||||
<Menu
|
<Menu
|
||||||
style={{
|
style={{
|
||||||
@@ -174,7 +175,7 @@ export default function StaticTransactions({
|
|||||||
<Text
|
<Text
|
||||||
className="text-sm text-gray-600 text-right"
|
className="text-sm text-gray-600 text-right"
|
||||||
>
|
>
|
||||||
{category?.type === MoneyCategoryEnum.Income ? '+' : '-'}
|
{category?.type === FinanceCategoryEnum.Income ? '+' : '-'}
|
||||||
{' '}
|
{' '}
|
||||||
{item.amount}
|
{item.amount}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -184,32 +185,27 @@ export default function StaticTransactions({
|
|||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{
|
|
||||||
userID === item.member_id && (
|
<MenuItem
|
||||||
<>
|
key="update"
|
||||||
<MenuItem
|
textValue="update"
|
||||||
key="update"
|
className="flex gap-3"
|
||||||
textValue="update"
|
style={{
|
||||||
className="flex gap-3"
|
minWidth: 150,
|
||||||
style={{
|
height: 42,
|
||||||
minWidth: 150,
|
paddingTop: 0,
|
||||||
height: 42,
|
paddingBottom: 0,
|
||||||
paddingTop: 0,
|
alignItems: 'center',
|
||||||
paddingBottom: 0,
|
borderRadius: 0,
|
||||||
alignItems: 'center',
|
}}
|
||||||
borderRadius: 0,
|
onPress={() => {
|
||||||
}}
|
onUpdate?.(item)
|
||||||
onPress={() => {
|
}}
|
||||||
onUpdate?.(item)
|
>
|
||||||
}}
|
<LucideIcon icon="Pencil" size={18} />
|
||||||
>
|
<MenuItemLabel className="text-lg">更新</MenuItemLabel>
|
||||||
<LucideIcon icon="Pencil" size={18} />
|
</MenuItem>
|
||||||
<MenuItemLabel className="text-lg">更新</MenuItemLabel>
|
<MenuSeparator />
|
||||||
</MenuItem>
|
|
||||||
<MenuSeparator />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
key="delete"
|
key="delete"
|
||||||
@@ -231,6 +227,7 @@ export default function StaticTransactions({
|
|||||||
<MenuItemLabel className="text-lg">删除</MenuItemLabel>
|
<MenuItemLabel className="text-lg">删除</MenuItemLabel>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuSeparator />
|
<MenuSeparator />
|
||||||
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
key="detail"
|
key="detail"
|
||||||
textValue="detail"
|
textValue="detail"
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import type { DateMorePickerMode } from '@/components/common/ao/DateMorePicker'
|
import type { DateMorePickerMode } from '@/components/common/ao/DateMorePicker'
|
||||||
|
import type { FinanceCategory } from '@/types/finance/category'
|
||||||
import { useFocusEffect, useRouter } from 'expo-router'
|
import { useFocusEffect, useRouter } from 'expo-router'
|
||||||
import React, { useCallback, useEffect, useState } from 'react'
|
import React, { useCallback, useEffect, useState } from 'react'
|
||||||
import { Image, Text, TouchableOpacity, View } from 'react-native'
|
import { Image, Text, TouchableOpacity, View } from 'react-native'
|
||||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'
|
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||||
|
import { toast } from 'sonner-native'
|
||||||
import { ADateMorePicker, ADatePicker, ADrawer, AListItem, LucideIcon } from '@/components/common/ao'
|
import { ADateMorePicker, ADatePicker, ADrawer, AListItem, LucideIcon } from '@/components/common/ao'
|
||||||
import { resolveDateRange } from '@/components/common/ao/DateMorePicker'
|
import { resolveDateRange } from '@/components/common/ao/DateMorePicker'
|
||||||
import { Fab, FabIcon } from '@/components/ui/fab'
|
import { Fab, FabIcon } from '@/components/ui/fab'
|
||||||
import { AddIcon } from '@/components/ui/icon'
|
import { AddIcon } from '@/components/ui/icon'
|
||||||
import { listFinanceRecordsApi } from '@/request/api/finance/record'
|
import { deleteFinanceRecordApi, listFinanceRecordsApi } from '@/request/api/finance/record'
|
||||||
import { deleteMoneyRecordApi, listMoneyRecordsApi } from '@/request/api/money/record'
|
|
||||||
import { useFinanceStore } from '@/store/finance'
|
import { useFinanceStore } from '@/store/finance'
|
||||||
import { getCurrentYearMonth, normalizeDateTime } from '@/utils/time'
|
import { getCurrentYearMonth } from '@/utils/time'
|
||||||
import { toast } from '@/utils/toast'
|
|
||||||
import CategorySheet from './components/CategorySheet'
|
import CategorySheet from './components/CategorySheet'
|
||||||
import TransactionsList from './components/TransactionsList'
|
import TransactionsList from './components/TransactionsList'
|
||||||
|
|
||||||
@@ -27,10 +27,14 @@ export default function Money() {
|
|||||||
const [showDatePicker, setShowDatePicker] = useState(false)
|
const [showDatePicker, setShowDatePicker] = useState(false)
|
||||||
const [dateMode, setDateMode] = useState<DateMorePickerMode>('month')
|
const [dateMode, setDateMode] = useState<DateMorePickerMode>('month')
|
||||||
|
|
||||||
const [searchParams, setSearchParams] = useState({
|
const [searchParams, setSearchParams] = useState<{
|
||||||
|
startTime: string
|
||||||
|
endTime: string
|
||||||
|
category: FinanceCategory | null
|
||||||
|
}>({
|
||||||
startTime: getCurrentYearMonth(),
|
startTime: getCurrentYearMonth(),
|
||||||
endTime: '',
|
endTime: '',
|
||||||
category: -1,
|
category: null,
|
||||||
})
|
})
|
||||||
|
|
||||||
const [showCategorySheet, setShowCategorySheet] = useState(false)
|
const [showCategorySheet, setShowCategorySheet] = useState(false)
|
||||||
@@ -41,21 +45,23 @@ export default function Money() {
|
|||||||
function fetchFinanceRecords() {
|
function fetchFinanceRecords() {
|
||||||
const resolveDate = resolveDateRange(dateMode, searchParams.startTime)
|
const resolveDate = resolveDateRange(dateMode, searchParams.startTime)
|
||||||
|
|
||||||
|
let searchType = 0
|
||||||
|
|
||||||
|
if (searchParams.category) {
|
||||||
|
if (searchParams.category.id === -1 || searchParams.category.id === -2) {
|
||||||
|
searchType = searchParams.category.type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
listFinanceRecordsApi({
|
listFinanceRecordsApi({
|
||||||
start_time: resolveDate.startTime,
|
start_time: resolveDate.startTime,
|
||||||
end_time: resolveDate.endTime,
|
end_time: resolveDate.endTime,
|
||||||
category_id: 0,
|
category_id: searchParams.category ? searchParams.category.id : 0,
|
||||||
|
category_type: searchType,
|
||||||
}).then((res) => {
|
}).then((res) => {
|
||||||
if (res.isSuccess) {
|
if (res.isSuccess) {
|
||||||
financeStore.setRecords(res.data.records || [])
|
financeStore.setRecords(res.data.records || [])
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
toast.show({
|
|
||||||
title: res.message || '查询错误,请稍后再试',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}).catch((err) => {
|
|
||||||
console.log(err)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,15 +69,25 @@ export default function Money() {
|
|||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
financeStore.ensureCategoriesLoaded()
|
financeStore.ensureCategoriesLoaded()
|
||||||
fetchFinanceRecords()
|
fetchFinanceRecords()
|
||||||
}, [searchParams.startTime, searchParams.endTime]),
|
}, [searchParams.startTime, searchParams.endTime, searchParams.category]),
|
||||||
)
|
)
|
||||||
|
|
||||||
async function handleDeleteRecord(id: number) {
|
async function handleDeleteRecord(id: number) {
|
||||||
await deleteMoneyRecordApi({
|
const toastId = toast.loading('正在删除...', {
|
||||||
id,
|
duration: Infinity,
|
||||||
})
|
})
|
||||||
|
|
||||||
fetchFinanceRecords()
|
await deleteFinanceRecordApi({
|
||||||
|
id,
|
||||||
|
}).then((res) => {
|
||||||
|
if (res.isSuccess) {
|
||||||
|
toast.success('删除成功')
|
||||||
|
|
||||||
|
fetchFinanceRecords()
|
||||||
|
}
|
||||||
|
}).finally(() => {
|
||||||
|
toast.dismiss(toastId)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function timeModeTip() {
|
function timeModeTip() {
|
||||||
@@ -117,7 +133,11 @@ export default function Money() {
|
|||||||
onPress={() => setShowCategorySheet(true)}
|
onPress={() => setShowCategorySheet(true)}
|
||||||
className="flex flex-row items-center gap-1"
|
className="flex flex-row items-center gap-1"
|
||||||
>
|
>
|
||||||
<Text className="font-semibold text-lg">分类</Text>
|
<Text className="font-semibold text-lg">
|
||||||
|
{
|
||||||
|
searchParams.category ? searchParams.category.name : '分类'
|
||||||
|
}
|
||||||
|
</Text>
|
||||||
<LucideIcon icon="ChevronDown" size={16} color="#666" clickable={false} />
|
<LucideIcon icon="ChevronDown" size={16} color="#666" clickable={false} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
@@ -180,7 +200,6 @@ export default function Money() {
|
|||||||
|
|
||||||
<View className="flex-1 mt-2">
|
<View className="flex-1 mt-2">
|
||||||
<TransactionsList
|
<TransactionsList
|
||||||
records={financeStore.records}
|
|
||||||
onUpdate={(data) => {
|
onUpdate={(data) => {
|
||||||
router.push({
|
router.push({
|
||||||
pathname: '/finance/record/editor',
|
pathname: '/finance/record/editor',
|
||||||
@@ -188,7 +207,7 @@ export default function Money() {
|
|||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
onDetail={(id) => {
|
onDetail={(id) => {
|
||||||
router.navigate({
|
router.push({
|
||||||
pathname: '/finance/record/detail',
|
pathname: '/finance/record/detail',
|
||||||
params: { id },
|
params: { id },
|
||||||
})
|
})
|
||||||
@@ -248,15 +267,16 @@ export default function Money() {
|
|||||||
|
|
||||||
<CategorySheet
|
<CategorySheet
|
||||||
open={showCategorySheet}
|
open={showCategorySheet}
|
||||||
value={searchParams.category}
|
value={searchParams.category?.id}
|
||||||
moneyCategories={financeStore.categories}
|
moneyCategories={financeStore.categories}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setShowCategorySheet(false)
|
setShowCategorySheet(false)
|
||||||
}}
|
}}
|
||||||
onSelect={(category) => {
|
onSelect={(category) => {
|
||||||
|
console.log(category)
|
||||||
setSearchParams(prev => ({
|
setSearchParams(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
category: category.id,
|
category,
|
||||||
}))
|
}))
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ export default function Auth() {
|
|||||||
|
|
||||||
userStore.setUserInfo({
|
userStore.setUserInfo({
|
||||||
token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjo3Nzk4OTU0MTkxMjE3MzMsImlhdCI6MTc3MjU0MDU1OH0.M5KqfPWdpOAlCCtZrJRxwKc8bRpGBjEt9BNCfgBZgnc',
|
token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjo3Nzk4OTU0MTkxMjE3MzMsImlhdCI6MTc3MjU0MDU1OH0.M5KqfPWdpOAlCCtZrJRxwKc8bRpGBjEt9BNCfgBZgnc',
|
||||||
id: 778115032100933,
|
id: 779895419121733,
|
||||||
nickname: 'Zhang',
|
nickname: 'Zhang',
|
||||||
avatar: 'http://thirdqq.qlogo.cn/ek_qqapp/AQHVialeUibE42oV8RvTTVZ4tyRlPYeJrUibWTwtCPfV68E4rSzn9cw00GkXQ3QDTiawwhjoqQb7/100',
|
avatar: 'http://thirdqq.qlogo.cn/ek_qqapp/AQHVialeUibE42oV8RvTTVZ4tyRlPYeJrUibWTwtCPfV68E4rSzn9cw00GkXQ3QDTiawwhjoqQb7/100',
|
||||||
})
|
})
|
||||||
@@ -194,7 +194,7 @@ export default function Auth() {
|
|||||||
// name: '(tabs)' as never,
|
// name: '(tabs)' as never,
|
||||||
// }],
|
// }],
|
||||||
// })
|
// })
|
||||||
// console.log(navigation.getState()?.routeNames)
|
console.log(navigation.getState()?.routeNames)
|
||||||
|
|
||||||
// ('/')
|
// ('/')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { AButton, ATab, LucideIcon } from '@/components/common/ao'
|
|||||||
import MoneyCategoryIcon from '@/components/common/MoneyCategoryIcon'
|
import MoneyCategoryIcon from '@/components/common/MoneyCategoryIcon'
|
||||||
import CustomHeader from '@/components/custom/Header'
|
import CustomHeader from '@/components/custom/Header'
|
||||||
import { Spinner } from '@/components/ui/spinner'
|
import { Spinner } from '@/components/ui/spinner'
|
||||||
import { createFinanceCategoryApi, deleteFinanceCategoryApi, updateFinanceCategoryApi } from '@/request/api/finance/category'
|
import { createFinanceCategoryApi, deleteFinanceCategoryApi, updateFinanceCategoryApi, updateFinanceCategorySortNumberApi } from '@/request/api/finance/category'
|
||||||
import { useFinanceStore } from '@/store/finance'
|
import { useFinanceStore } from '@/store/finance'
|
||||||
import { useMoneyCategoryStore } from '@/store/moneyCategory'
|
import { useMoneyCategoryStore } from '@/store/moneyCategory'
|
||||||
import { useSpaceStore } from '@/store/space'
|
import { useSpaceStore } from '@/store/space'
|
||||||
@@ -209,7 +209,23 @@ export default function MoneyCategorySetting() {
|
|||||||
const newData = dragData.data
|
const newData = dragData.data
|
||||||
|
|
||||||
if (JSON.stringify(categories) !== JSON.stringify(newData)) {
|
if (JSON.stringify(categories) !== JSON.stringify(newData)) {
|
||||||
moneyCategoryStore.updateSortNumbers(newData, selectedTab)
|
const updateData: any[] = []
|
||||||
|
newData.forEach((item, index) => {
|
||||||
|
item.sort_number = index
|
||||||
|
|
||||||
|
updateData.push({
|
||||||
|
id: item.id,
|
||||||
|
sort_number: index,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
financeStore.setCategories(newData)
|
||||||
|
|
||||||
|
updateFinanceCategorySortNumberApi({
|
||||||
|
categories: updateData,
|
||||||
|
}).then((res) => {
|
||||||
|
console.log(res)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
import type { FinanceCategory } from '@/types/finance/category'
|
||||||
|
import type { FinanceRecordDetail } from '@/types/finance/record'
|
||||||
|
import { useFocusEffect, useLocalSearchParams, useRouter } from 'expo-router'
|
||||||
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
|
import { ActivityIndicator, Text, View } from 'react-native'
|
||||||
|
import { toast } from 'sonner-native'
|
||||||
|
import { AButton, ACell, ACellGroup, AModal } from '@/components/common/ao'
|
||||||
|
import MoneyCategoryIcon from '@/components/common/MoneyCategoryIcon'
|
||||||
|
import CustomHeader from '@/components/custom/Header'
|
||||||
|
import { deleteFinanceRecordApi, getFinanceRecordDetailApi } from '@/request/api/finance/record'
|
||||||
|
import { useFinanceStore } from '@/store/finance'
|
||||||
|
import { useNavStore } from '@/store/navigation'
|
||||||
|
import { MoneyCategoryEnum } from '@/types/money/category'
|
||||||
|
import { toDateTimeString } from '@/utils/time'
|
||||||
|
|
||||||
|
export default function MoneyRecordDetail() {
|
||||||
|
const router = useRouter()
|
||||||
|
const financeStore = useFinanceStore()
|
||||||
|
const navStore = useNavStore()
|
||||||
|
|
||||||
|
const { id: detailId } = useLocalSearchParams<{ id: string }>()
|
||||||
|
|
||||||
|
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
||||||
|
const [detail, setDetail] = useState<FinanceRecordDetail | null>(null)
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
|
const [category, setCategory] = useState<FinanceCategory | null>(null)
|
||||||
|
|
||||||
|
useFocusEffect(
|
||||||
|
useCallback(() => {
|
||||||
|
if (detailId) {
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
getFinanceRecordDetailApi({
|
||||||
|
id: Number(detailId),
|
||||||
|
}).then((res) => {
|
||||||
|
if (res.isSuccess) {
|
||||||
|
setDetail(res.data)
|
||||||
|
setLoading(false)
|
||||||
|
|
||||||
|
const category = financeStore.getCategoryById(res.data.category_id)
|
||||||
|
if (category) {
|
||||||
|
setCategory(category)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, []),
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<View className="flex-1">
|
||||||
|
<CustomHeader title="账单详情" />
|
||||||
|
<View className="flex-1">
|
||||||
|
{
|
||||||
|
loading
|
||||||
|
? (
|
||||||
|
<View className="flex-1 items-center justify-center">
|
||||||
|
<ActivityIndicator size="large" color="#333" />
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<>
|
||||||
|
<View className="flex flex-col items-center">
|
||||||
|
<MoneyCategoryIcon name={category?.icon} size={56} />
|
||||||
|
<Text className="mt-1">{category?.name}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<ACellGroup showDivider={false} className="mt-4">
|
||||||
|
<ACell title="类别" rightText={category?.type === MoneyCategoryEnum.Expense ? '支出' : '收入'} />
|
||||||
|
<ACell title="金额" rightText={detail?.amount} />
|
||||||
|
<ACell title="备注" rightText={detail?.remark} />
|
||||||
|
<ACell title="日期" rightText={detail?.record_time} />
|
||||||
|
<ACell title="创建人" rightText={detail?.create_member_name} />
|
||||||
|
<ACell title="创建时间" rightText={toDateTimeString(detail?.create_time || '')} />
|
||||||
|
{
|
||||||
|
detail?.create_member_id !== detail?.update_member_id && (
|
||||||
|
<>
|
||||||
|
<ACell title="更新人" rightText={detail?.update_member_name} />
|
||||||
|
<ACell title="更新时间" rightText={toDateTimeString(detail?.update_time || '')} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
</ACellGroup>
|
||||||
|
|
||||||
|
<View className="flex flex-row px-4 mt-4 gap-4">
|
||||||
|
|
||||||
|
<AButton
|
||||||
|
className="flex-1"
|
||||||
|
action="negative"
|
||||||
|
variant="outline"
|
||||||
|
onPress={() => {
|
||||||
|
setShowDeleteModal(true)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</AButton>
|
||||||
|
<AButton
|
||||||
|
className="flex-1"
|
||||||
|
onPress={() => {
|
||||||
|
router.navigate({
|
||||||
|
pathname: '/finance/record/editor',
|
||||||
|
params: { id: detailId },
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</AButton>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<AModal
|
||||||
|
isOpen={showDeleteModal}
|
||||||
|
title="提示"
|
||||||
|
body="确认删除该条账单?"
|
||||||
|
onConfirm={async () => {
|
||||||
|
setShowDeleteModal(false)
|
||||||
|
if (!detail) {
|
||||||
|
toast.warning('当前记录无效')
|
||||||
|
router.back()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const toastId = toast.loading('正在删除...', {
|
||||||
|
duration: Infinity,
|
||||||
|
})
|
||||||
|
|
||||||
|
await deleteFinanceRecordApi({
|
||||||
|
id: detail?.id,
|
||||||
|
}).then((res) => {
|
||||||
|
if (res.isSuccess) {
|
||||||
|
toast.success('删除成功')
|
||||||
|
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
}).finally(() => {
|
||||||
|
toast.dismiss(toastId)
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
onCancel={() => {
|
||||||
|
setShowDeleteModal(false)
|
||||||
|
}}
|
||||||
|
onClose={() => {
|
||||||
|
setShowDeleteModal(false)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { FinanceRecord } from '@/types/finance/record'
|
||||||
import Big from 'big.js'
|
import Big from 'big.js'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Keyboard, Pressable, StyleSheet, Text, TouchableWithoutFeedback, View } from 'react-native'
|
import { Keyboard, Pressable, StyleSheet, Text, TouchableWithoutFeedback, View } from 'react-native'
|
||||||
@@ -39,23 +40,15 @@ const keys: KeydownItem[] = [
|
|||||||
|
|
||||||
interface AmountKeyboardProps {
|
interface AmountKeyboardProps {
|
||||||
show?: boolean
|
show?: boolean
|
||||||
record_time?: string
|
record: FinanceRecord
|
||||||
amount?: string
|
|
||||||
remark?: string
|
|
||||||
|
|
||||||
// 回调:当键盘输入或修改数据时触发
|
|
||||||
onChange?: (data: { record_time: string, amount: string, remark: string }) => void
|
|
||||||
|
|
||||||
// 可选:点击完成按钮触发
|
// 可选:点击完成按钮触发
|
||||||
onDone?: () => void
|
onDone?: (record: FinanceRecord) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AmountKeyboard({
|
export default function AmountKeyboard({
|
||||||
show = false,
|
show = false,
|
||||||
record_time = '',
|
record,
|
||||||
amount = '',
|
|
||||||
remark = '',
|
|
||||||
onChange,
|
|
||||||
onDone,
|
onDone,
|
||||||
}: AmountKeyboardProps) {
|
}: AmountKeyboardProps) {
|
||||||
// 自定义键盘计算过程
|
// 自定义键盘计算过程
|
||||||
@@ -67,43 +60,36 @@ export default function AmountKeyboard({
|
|||||||
// 键盘高度
|
// 键盘高度
|
||||||
const keyboardHeight = useKeyboardHeight()
|
const keyboardHeight = useKeyboardHeight()
|
||||||
|
|
||||||
// 使用一个对象管理三个字段
|
const [innerRecord, setInnerRecord] = useState<FinanceRecord>({
|
||||||
const [internalData, setInternalData] = useState({
|
id: 0,
|
||||||
record_time: record_time || getCurrentDate(),
|
category_id: 0,
|
||||||
amount: amount || '0',
|
amount: '',
|
||||||
remark: remark || '',
|
remark: '',
|
||||||
|
record_time: getCurrentDate(),
|
||||||
|
member_id: 0,
|
||||||
|
member_name: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
// 如果父组件 props 变化,需要同步
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setInternalData({
|
setInnerRecord(record)
|
||||||
record_time: record_time || getCurrentDate(),
|
setKeydownInput(record.amount || '')
|
||||||
amount: amount || '0',
|
}, [record])
|
||||||
remark: remark || '',
|
|
||||||
|
useEffect(() => {
|
||||||
|
setInnerRecord((prev) => {
|
||||||
|
if (!keydownInput) {
|
||||||
|
return { ...prev, amount: '0' }
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = calculateWithBig(keydownInput)
|
||||||
|
return { ...prev, amount: result }
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return { ...prev, amount: '0' }
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}, [record_time, amount, remark])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!keydownInput) {
|
|
||||||
handleChange({ amount: '0' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const computed = calculateWithBig(keydownInput)
|
|
||||||
handleChange({ amount: computed })
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
handleChange({ amount: '0' })
|
|
||||||
}
|
|
||||||
}, [keydownInput])
|
}, [keydownInput])
|
||||||
|
|
||||||
const handleChange = (newData: Partial<typeof internalData>) => {
|
|
||||||
const updated = { ...internalData, ...newData }
|
|
||||||
setInternalData(updated)
|
|
||||||
onChange?.(updated)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算过程
|
// 计算过程
|
||||||
const handlePress = (key: string | number) => {
|
const handlePress = (key: string | number) => {
|
||||||
setKeydownInput((prev) => {
|
setKeydownInput((prev) => {
|
||||||
@@ -113,7 +99,8 @@ export default function AmountKeyboard({
|
|||||||
newInput = prev.slice(0, -1)
|
newInput = prev.slice(0, -1)
|
||||||
}
|
}
|
||||||
else if (key === 'clear') {
|
else if (key === 'clear') {
|
||||||
setInternalData({
|
setInnerRecord({
|
||||||
|
...innerRecord,
|
||||||
record_time: getCurrentDate(),
|
record_time: getCurrentDate(),
|
||||||
amount: '0',
|
amount: '0',
|
||||||
remark: '',
|
remark: '',
|
||||||
@@ -121,7 +108,7 @@ export default function AmountKeyboard({
|
|||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
else if (key === '/') {
|
else if (key === '/') {
|
||||||
onDone?.()
|
onDone?.(innerRecord)
|
||||||
return prev
|
return prev
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -206,7 +193,7 @@ export default function AmountKeyboard({
|
|||||||
newInput = prev + keyStr
|
newInput = prev + keyStr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return newInput
|
return newInput
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -271,10 +258,11 @@ export default function AmountKeyboard({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{
|
<View
|
||||||
backgroundColor: '#fff',
|
style={{
|
||||||
elevation: 20,
|
backgroundColor: '#fff',
|
||||||
}}
|
elevation: 20,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
|
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
|
||||||
<View className="h-28 flex pl-4 pr-4 justify-center">
|
<View className="h-28 flex pl-4 pr-4 justify-center">
|
||||||
@@ -284,19 +272,24 @@ export default function AmountKeyboard({
|
|||||||
onPress={() => setShowDatePicker(true)}
|
onPress={() => setShowDatePicker(true)}
|
||||||
>
|
>
|
||||||
<LucideIcon icon="Calendar" size={16} color="#60a5fa" clickable={false} />
|
<LucideIcon icon="Calendar" size={16} color="#60a5fa" clickable={false} />
|
||||||
<Text className="text-gray-600 text-sm">{internalData.record_time}</Text>
|
<Text className="text-gray-600 text-sm">{innerRecord.record_time}</Text>
|
||||||
<LucideIcon icon="ChevronDown" size={12} color="#aaa" clickable={false} />
|
<LucideIcon icon="ChevronDown" size={12} color="#aaa" clickable={false} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<Text className="text-3xl text-blue-700 text-right">{internalData.amount}</Text>
|
<Text className="text-3xl text-blue-700 text-right">{innerRecord.amount}</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Text className="text-gray-400 text-right">{keydownInput}</Text>
|
<Text className="text-gray-400 text-right">{keydownInput}</Text>
|
||||||
<AInput
|
<AInput
|
||||||
value={internalData.remark}
|
value={innerRecord.remark}
|
||||||
placeholder="备注"
|
placeholder="备注"
|
||||||
className="bg-[#f2f3f5] border-0"
|
className="bg-[#f2f3f5] border-0"
|
||||||
size="sm"
|
size="sm"
|
||||||
onChangeText={val => handleChange({ remark: val })}
|
onChangeText={(val) => {
|
||||||
|
setInnerRecord({
|
||||||
|
...innerRecord,
|
||||||
|
remark: val,
|
||||||
|
})
|
||||||
|
}}
|
||||||
onFocus={() => setRemarkIsFocused(true)}
|
onFocus={() => setRemarkIsFocused(true)}
|
||||||
onBlur={() => setRemarkIsFocused(false)}
|
onBlur={() => setRemarkIsFocused(false)}
|
||||||
/>
|
/>
|
||||||
@@ -352,14 +345,18 @@ export default function AmountKeyboard({
|
|||||||
<ADatePicker
|
<ADatePicker
|
||||||
pickerTitle="记账时间"
|
pickerTitle="记账时间"
|
||||||
mode="date"
|
mode="date"
|
||||||
date={internalData.record_time}
|
date={innerRecord.record_time}
|
||||||
show={showDatePicker}
|
show={showDatePicker}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setShowDatePicker(false)
|
setShowDatePicker(false)
|
||||||
}}
|
}}
|
||||||
onConfirm={(date) => {
|
onConfirm={(date) => {
|
||||||
setShowDatePicker(false)
|
setShowDatePicker(false)
|
||||||
handleChange({ record_time: date })
|
|
||||||
|
setInnerRecord({
|
||||||
|
...innerRecord,
|
||||||
|
record_time: date,
|
||||||
|
})
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export default function MoenyRecordEditor() {
|
|||||||
const defaultFinanceRecord: FinanceRecord = {
|
const defaultFinanceRecord: FinanceRecord = {
|
||||||
id: -1,
|
id: -1,
|
||||||
category_id: -1,
|
category_id: -1,
|
||||||
amount: '0.00',
|
amount: '',
|
||||||
remark: '',
|
remark: '',
|
||||||
record_time: getCurrentDate(),
|
record_time: getCurrentDate(),
|
||||||
member_id: -1,
|
member_id: -1,
|
||||||
@@ -58,26 +58,26 @@ export default function MoenyRecordEditor() {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// 保存记录
|
// 保存记录
|
||||||
function handleSaveRecord() {
|
async function handleSaveRecord(record: FinanceRecord) {
|
||||||
if (selectedCategory === null) {
|
if (selectedCategory === null) {
|
||||||
toast.warning('请选择分类')
|
toast.warning('请选择分类')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (financeRecord.amount === '0.00') {
|
if (record.amount === '0.00') {
|
||||||
toast.warning('请输入金额')
|
toast.warning('请输入金额')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const amount = financeRecord.amount.replace(/^-+/, '')
|
const innerAmount = record.amount.replace(/^-+/, '')
|
||||||
|
|
||||||
if (updateId) {
|
if (updateId) {
|
||||||
updateFinanceRecordApi({
|
await updateFinanceRecordApi({
|
||||||
id: financeRecord.id,
|
id: financeRecord.id,
|
||||||
category_id: selectedCategory.id,
|
category_id: selectedCategory.id,
|
||||||
amount,
|
amount: innerAmount,
|
||||||
remark: financeRecord.remark,
|
remark: record.remark,
|
||||||
record_time: financeRecord.record_time,
|
record_time: record.record_time,
|
||||||
}).then((res) => {
|
}).then((res) => {
|
||||||
if (res.isSuccess) {
|
if (res.isSuccess) {
|
||||||
router.back()
|
router.back()
|
||||||
@@ -85,11 +85,11 @@ export default function MoenyRecordEditor() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
createFinanceRecordApi({
|
await createFinanceRecordApi({
|
||||||
amount,
|
amount: innerAmount,
|
||||||
category_id: selectedCategory.id,
|
category_id: selectedCategory.id,
|
||||||
remark: financeRecord.remark,
|
remark: record.remark,
|
||||||
record_time: financeRecord.record_time,
|
record_time: record.record_time,
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.isSuccess) {
|
if (res.isSuccess) {
|
||||||
@@ -116,7 +116,7 @@ export default function MoenyRecordEditor() {
|
|||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
className="w-[30%] flex items-center px-4 flex-row"
|
className="w-[30%] flex items-center px-4 flex-row"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
router.navigate('/finance/category/setting')
|
router.push('/finance/category')
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<LucideIcon icon="LayoutList" />
|
<LucideIcon icon="LayoutList" />
|
||||||
@@ -169,17 +169,7 @@ export default function MoenyRecordEditor() {
|
|||||||
|
|
||||||
<AmountKeyboard
|
<AmountKeyboard
|
||||||
show={showKeydown}
|
show={showKeydown}
|
||||||
record_time={financeRecord.record_time}
|
record={financeRecord}
|
||||||
amount={financeRecord.amount}
|
|
||||||
remark={financeRecord.remark}
|
|
||||||
onChange={(data) => {
|
|
||||||
setFinanceRecord({
|
|
||||||
...financeRecord,
|
|
||||||
record_time: data.record_time,
|
|
||||||
amount: data.amount,
|
|
||||||
remark: data.remark,
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
onDone={handleSaveRecord}
|
onDone={handleSaveRecord}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ const AInput = forwardRef<React.ComponentRef<typeof InputField>, AInputProps>(
|
|||||||
isFocused={isFocused}
|
isFocused={isFocused}
|
||||||
className={className}
|
className={className}
|
||||||
style={style}
|
style={style}
|
||||||
|
|
||||||
>
|
>
|
||||||
{leftIcon && (
|
{leftIcon && (
|
||||||
<InputSlot className="pl-3">
|
<InputSlot className="pl-3">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { CreateFinanceRecordParams, DeleteFinanceRecordParams, ListFinanceRecordsParams, ListFinanceRecordsResult, UpdateFinanceRecordParams } from '@/types/finance/record'
|
import type { CreateFinanceRecordParams, DeleteFinanceRecordParams, GetFinanceRecordDetailParams, GetFinanceRecordDetailResult, ListFinanceRecordsParams, ListFinanceRecordsResult, UpdateFinanceRecordParams } from '@/types/finance/record'
|
||||||
import alovaInstance from '../../index'
|
import alovaInstance from '../../index'
|
||||||
|
|
||||||
const API_PREFIX = '/finance/record'
|
const API_PREFIX = '/finance/record'
|
||||||
@@ -18,3 +18,7 @@ export function updateFinanceRecordApi(params: UpdateFinanceRecordParams) {
|
|||||||
export function deleteFinanceRecordApi(params: DeleteFinanceRecordParams) {
|
export function deleteFinanceRecordApi(params: DeleteFinanceRecordParams) {
|
||||||
return alovaInstance.Post<ResponseData<void>>(`${API_PREFIX}/delete`, params)
|
return alovaInstance.Post<ResponseData<void>>(`${API_PREFIX}/delete`, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getFinanceRecordDetailApi(params: GetFinanceRecordDetailParams) {
|
||||||
|
return alovaInstance.Post<ResponseData<GetFinanceRecordDetailResult>>(`${API_PREFIX}/detail`, params)
|
||||||
|
}
|
||||||
@@ -31,8 +31,8 @@ function isWhiteListed(url: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const alovaInstance = createAlova({
|
const alovaInstance = createAlova({
|
||||||
// baseURL: 'http://192.168.5.6:8086/app',
|
baseURL: 'http://192.168.5.6:8086/app',
|
||||||
baseURL: 'http://192.168.1.137:8086/app',
|
// baseURL: 'http://192.168.1.137:8086/app',
|
||||||
requestAdapter: axiosRequestAdapter(),
|
requestAdapter: axiosRequestAdapter(),
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
cacheFor: null,
|
cacheFor: null,
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ interface FinanceState {
|
|||||||
createCategory: (category: FinanceCategory) => void
|
createCategory: (category: FinanceCategory) => void
|
||||||
updateCategory: (category: FinanceCategory) => void
|
updateCategory: (category: FinanceCategory) => void
|
||||||
deleteCategory: (id: number) => void
|
deleteCategory: (id: number) => void
|
||||||
updateCategorySortNumber: (data: FinanceCategory[], type: number) => void
|
|
||||||
isDuplicateCategory: (name: string, type: number, excludeId: number | null) => boolean
|
isDuplicateCategory: (name: string, type: number, excludeId: number | null) => boolean
|
||||||
getDefaultGroupRow: (iconName: string) => number
|
getDefaultGroupRow: (iconName: string) => number
|
||||||
getCategoryById: (id: number) => FinanceCategory | null
|
getCategoryById: (id: number) => FinanceCategory | null
|
||||||
@@ -301,16 +300,6 @@ export const useFinanceStore = create<FinanceState>()(
|
|||||||
const finalCategories = allCategories.filter(c => c.id !== id)
|
const finalCategories = allCategories.filter(c => c.id !== id)
|
||||||
set({ categories: finalCategories })
|
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) => {
|
isDuplicateCategory: (name, type, excludeId) => {
|
||||||
const categories = get().categories || []
|
const categories = get().categories || []
|
||||||
return categories.some(c =>
|
return categories.some(c =>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export interface ListFinanceRecordsParams {
|
|||||||
start_time: string
|
start_time: string
|
||||||
end_time: string
|
end_time: string
|
||||||
category_id: number
|
category_id: number
|
||||||
|
category_type: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ListFinanceRecordsResult {
|
export interface ListFinanceRecordsResult {
|
||||||
@@ -43,3 +44,27 @@ export interface UpdateFinanceRecordParams {
|
|||||||
export interface DeleteFinanceRecordParams {
|
export interface DeleteFinanceRecordParams {
|
||||||
id: number
|
id: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FinanceRecordDetail {
|
||||||
|
id: number
|
||||||
|
category_id: number
|
||||||
|
amount: string
|
||||||
|
remark: string
|
||||||
|
record_time: string
|
||||||
|
|
||||||
|
create_time: number
|
||||||
|
create_member_id: number
|
||||||
|
create_member_name: string
|
||||||
|
|
||||||
|
update_time: number
|
||||||
|
update_member_id: number
|
||||||
|
update_member_name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetFinanceRecordDetailParams {
|
||||||
|
id: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetFinanceRecordDetailResult extends FinanceRecordDetail {
|
||||||
|
|
||||||
|
}
|
||||||
+166
-119
@@ -1,130 +1,177 @@
|
|||||||
<!DOCTYPE html>
|
import type { FinanceRecord } from '@/types/finance/record'
|
||||||
<html lang="en">
|
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'
|
||||||
|
|
||||||
<head>
|
interface KeydownItem {
|
||||||
<meta charset="UTF-8">
|
key: string | number
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
content: React.ReactNode
|
||||||
<title>加入其它空间 Demo</title>
|
}
|
||||||
<style>
|
|
||||||
body {
|
interface AmountKeyboardProps {
|
||||||
font-family: sans-serif;
|
show?: boolean
|
||||||
background: #f5f6f8;
|
record: FinanceRecord
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
// 可选:点击完成按钮触发
|
||||||
|
onDone?: (record: FinanceRecord) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AmountKeyboard({
|
||||||
|
show = false,
|
||||||
|
record,
|
||||||
|
onDone,
|
||||||
|
}: AmountKeyboardProps) {
|
||||||
|
// 自定义键盘计算过程
|
||||||
|
const [keydownInput, setKeydownInput] = useState('')
|
||||||
|
// 日期选择
|
||||||
|
const [showDatePicker, setShowDatePicker] = useState(false)
|
||||||
|
// 备注聚焦
|
||||||
|
const [remarkIsFocused, setRemarkIsFocused] = useState(false)
|
||||||
|
// 键盘高度
|
||||||
|
const keyboardHeight = useKeyboardHeight()
|
||||||
|
|
||||||
|
const [innerRecord, setInnerRecord] = useState<FinanceRecord>({
|
||||||
|
id: 0,
|
||||||
|
category_id: 0,
|
||||||
|
amount: '',
|
||||||
|
remark: '',
|
||||||
|
record_time: '',
|
||||||
|
member_id: 0,
|
||||||
|
member_name: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setInnerRecord(record)
|
||||||
|
setKeydownInput(record.amount || '0.00')
|
||||||
|
}, [record])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!keydownInput) {
|
||||||
|
setInnerRecord({
|
||||||
|
...innerRecord,
|
||||||
|
amount: '0',
|
||||||
|
})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 页面列表项 */
|
try {
|
||||||
.space-option {
|
setInnerRecord({
|
||||||
padding: 16px;
|
...innerRecord,
|
||||||
background: #fff;
|
amount: calculateWithBig(keydownInput),
|
||||||
margin: 16px;
|
})
|
||||||
border-radius: 12px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
}
|
||||||
|
catch {
|
||||||
.space-option:hover {
|
setInnerRecord({
|
||||||
background: #f0f0f0;
|
...innerRecord,
|
||||||
|
amount: '0',
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
}, [keydownInput])
|
||||||
|
|
||||||
/* modal 背景 */
|
return (
|
||||||
.modal-backdrop {
|
<View style={{
|
||||||
position: fixed;
|
backgroundColor: '#fff',
|
||||||
inset: 0;
|
elevation: 20,
|
||||||
background: rgba(0, 0, 0, 0.4);
|
}}
|
||||||
display: none;
|
>
|
||||||
justify-content: center;
|
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
|
||||||
align-items: center;
|
<View className="h-28 flex pl-4 pr-4 justify-center">
|
||||||
}
|
<View className="flex flex-row items-center justify-between">
|
||||||
|
<Pressable
|
||||||
|
className="flex flex-row items-center gap-1.5 px-2 py-1.5 rounded-md bg-gray-50 active:opacity-70"
|
||||||
|
onPress={() => setShowDatePicker(true)}
|
||||||
|
>
|
||||||
|
<LucideIcon icon="Calendar" size={16} color="#60a5fa" clickable={false} />
|
||||||
|
<Text className="text-gray-600 text-sm">{innerRecord.record_time}</Text>
|
||||||
|
<LucideIcon icon="ChevronDown" size={12} color="#aaa" clickable={false} />
|
||||||
|
</Pressable>
|
||||||
|
<Text className="text-3xl text-blue-700 text-right">{innerRecord.amount}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
/* modal 内容 */
|
<Text className="text-gray-400 text-right">{keydownInput}</Text>
|
||||||
.modal {
|
<AInput
|
||||||
background: #fff;
|
value={innerRecord.remark}
|
||||||
border-radius: 16px;
|
placeholder="备注"
|
||||||
padding: 24px;
|
className="bg-[#f2f3f5] border-0"
|
||||||
width: 90%;
|
size="sm"
|
||||||
max-width: 400px;
|
onChangeText={(val) => {
|
||||||
}
|
setInnerRecord({
|
||||||
|
...innerRecord,
|
||||||
|
remark: val,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
onFocus={() => setRemarkIsFocused(true)}
|
||||||
|
onBlur={() => setRemarkIsFocused(false)}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</TouchableWithoutFeedback>
|
||||||
|
<View className="flex flex-row flex-wrap" style={{ height: 220 }}>
|
||||||
|
{keys.map((item, idx) => {
|
||||||
|
const isFirstCol = idx % 4 === 0
|
||||||
|
const isFirstRow = idx < 4
|
||||||
|
const isLastCol = (idx + 1) % 4 === 0
|
||||||
|
const isLastRow = idx >= 12
|
||||||
|
|
||||||
.modal h2 {
|
return (
|
||||||
margin: 0 0 16px 0;
|
<Pressable
|
||||||
font-size: 20px;
|
key={item.key}
|
||||||
color: #333;
|
onPress={() => 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'
|
||||||
|
|
||||||
.modal input {
|
return (
|
||||||
width: 100%;
|
<>
|
||||||
padding: 12px;
|
<View
|
||||||
margin-bottom: 16px;
|
style={{
|
||||||
border-radius: 8px;
|
...StyleSheet.absoluteFillObject,
|
||||||
border: 1px solid #ccc;
|
backgroundColor: pressed ? pressedBg : defaultBg,
|
||||||
font-size: 16px;
|
}}
|
||||||
}
|
/>
|
||||||
|
{item.content}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</Pressable>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
{remarkIsFocused && keyboardHeight > 220 && (
|
||||||
|
<View style={{ height: keyboardHeight + 10 - 220 }} />
|
||||||
|
)}
|
||||||
|
|
||||||
.modal button {
|
<ADatePicker
|
||||||
width: 100%;
|
pickerTitle="记账时间"
|
||||||
padding: 12px;
|
mode="date"
|
||||||
border-radius: 8px;
|
date={innerRecord.record_time}
|
||||||
border: none;
|
show={showDatePicker}
|
||||||
background: #333;
|
onClose={() => {
|
||||||
color: #fff;
|
setShowDatePicker(false)
|
||||||
font-size: 16px;
|
}}
|
||||||
cursor: pointer;
|
onConfirm={(date) => {
|
||||||
margin-bottom: 8px;
|
setShowDatePicker(false)
|
||||||
}
|
|
||||||
|
|
||||||
.modal .scan-button {
|
setInnerRecord({
|
||||||
background: #f0f0f0;
|
...innerRecord,
|
||||||
color: #333;
|
record_time: date,
|
||||||
}
|
})
|
||||||
</style>
|
}}
|
||||||
</head>
|
/>
|
||||||
|
</View>
|
||||||
<body>
|
)
|
||||||
|
}
|
||||||
<!-- 加入其它空间选项 -->
|
|
||||||
<div class="space-option" id="joinSpaceBtn">
|
|
||||||
<span>加入其它空间</span>
|
|
||||||
<span>→</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Modal -->
|
|
||||||
<div class="modal-backdrop" id="modalBackdrop">
|
|
||||||
<div class="modal">
|
|
||||||
<h2>加入空间</h2>
|
|
||||||
<input type="text" placeholder="请输入邀请码" id="inviteInput" />
|
|
||||||
<button id="joinBtn">加入</button>
|
|
||||||
<button class="scan-button" id="scanBtn">扫码加入</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const joinBtnEl = document.getElementById('joinSpaceBtn');
|
|
||||||
const modalEl = document.getElementById('modalBackdrop');
|
|
||||||
|
|
||||||
joinBtnEl.addEventListener('click', () => {
|
|
||||||
modalEl.style.display = 'flex';
|
|
||||||
});
|
|
||||||
|
|
||||||
modalEl.addEventListener('click', (e) => {
|
|
||||||
if (e.target === modalEl) {
|
|
||||||
modalEl.style.display = 'none';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('joinBtn').addEventListener('click', () => {
|
|
||||||
const code = document.getElementById('inviteInput').value;
|
|
||||||
alert(`尝试加入空间:${code}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('scanBtn').addEventListener('click', () => {
|
|
||||||
alert('扫码功能触发(可接入扫码库)');
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
Reference in new issue
Block a user