u
This commit is contained in:
1 parent
ab3eaed7ce
commit
cf23acdabc
28 files changed
+1292
-486
No files matched your search
@@ -2,7 +2,6 @@
|
||||
<project version="4">
|
||||
<component name="DataSourcePerFileMappings">
|
||||
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/4db2dbd4-857e-4234-ba5e-f8cbf0ba6e5d/console_1.sql" value="4db2dbd4-857e-4234-ba5e-f8cbf0ba6e5d" />
|
||||
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/4db2dbd4-857e-4234-ba5e-f8cbf0ba6e5d/console_2.sql" value="4db2dbd4-857e-4234-ba5e-f8cbf0ba6e5d" />
|
||||
<file url="file://$PROJECT_DIR$/db/queries/finance_category_default.sql" value="4db2dbd4-857e-4234-ba5e-f8cbf0ba6e5d" />
|
||||
<file url="file://$PROJECT_DIR$/db/queries/finance_category_space.sql" value="4db2dbd4-857e-4234-ba5e-f8cbf0ba6e5d" />
|
||||
<file url="file://$PROJECT_DIR$/db/queries/space.sql" value="4db2dbd4-857e-4234-ba5e-f8cbf0ba6e5d" />
|
||||
|
||||
@@ -220,11 +220,36 @@ func UpdateSpaceDefault(c fiber.Ctx) error {
|
||||
return err
|
||||
}
|
||||
|
||||
err = db.Queries.UpdateSpaceIsDefault(c.Context(), repo.UpdateSpaceIsDefaultParams{
|
||||
SpaceID: dto.SpaceId,
|
||||
UserID: userId,
|
||||
IsDefault: dto.IsDefault,
|
||||
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
|
||||
}
|
||||
@@ -270,6 +295,85 @@ func UpdateSpaceName(c fiber.Ctx) error {
|
||||
return responsex.OK(c, "更新成功")
|
||||
}
|
||||
|
||||
func GetSpaceSettings(c fiber.Ctx) error {
|
||||
dto := new(types.GetSpaceSettingDTO)
|
||||
if err := requestx.BindAndValidate(c, dto); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userId, err := middleware.GetUserID(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
spaceId := dto.SpaceId
|
||||
|
||||
row, err := db.Queries.GetSpaceByUserIdAndSpaceId(c.Context(), repo.GetSpaceByUserIdAndSpaceIdParams{
|
||||
UserID: userId,
|
||||
ID: spaceId,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
if errorx.IsNotFound(err) {
|
||||
return responsex.SpaceError(c, "不在该空间或该空间已解散")
|
||||
}
|
||||
return err
|
||||
}
|
||||
vo := new(types.UserSpace)
|
||||
|
||||
vo.SpaceId = row.SpaceID
|
||||
vo.Name = row.Name
|
||||
vo.IsDefault = row.IsDefault
|
||||
vo.MemberName = row.MemberName
|
||||
vo.Role = row.Role
|
||||
vo.MemberCount = row.MemberCount
|
||||
vo.OwnerId = row.OwnerID
|
||||
vo.SortNumber = row.SortNumber
|
||||
|
||||
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 initFinanceCategory(ctx context.Context, q *repo.Queries, spaceId int64, userId int64) error {
|
||||
categoryDefaults, err := q.ListFinanceCategoryDefault(ctx)
|
||||
|
||||
@@ -25,9 +25,11 @@ func SetupRouters(app *fiber.App) {
|
||||
spaceRouter.Post("/list", space.ListUserSpaces)
|
||||
spaceRouter.Post("/dissolve", space.DissolveSpace)
|
||||
spaceRouter.Post("/name", space.UpdateSpaceName)
|
||||
spaceRouter.Post("/settings", space.GetSpaceSettings)
|
||||
|
||||
spaceRouter.Post("/member/default", space.UpdateSpaceDefault)
|
||||
spaceRouter.Post("/member/name", space.UpdateMemberName)
|
||||
spaceRouter.Post("/member/sortNumber", space.UpdateSpaceSortNumber)
|
||||
|
||||
// authGroup.Get("/user/profile", user.GetProfile)
|
||||
// spaceGroup.Get("/space/info", space.GetInfo)
|
||||
|
||||
@@ -45,3 +45,16 @@ type UpdateSpaceNameDTO struct {
|
||||
SpaceId int64 `json:"space_id" validate:"required"`
|
||||
SpaceName string `json:"space_name" validate:"required"`
|
||||
}
|
||||
|
||||
type GetSpaceSettingDTO struct {
|
||||
SpaceId int64 `json:"space_id" validate:"required"`
|
||||
}
|
||||
|
||||
type SpaceSortNumber struct {
|
||||
SpaceId int64 `json:"space_id" validate:"required"`
|
||||
SortNumber int32 `json:"sort_number" validate:"required"`
|
||||
}
|
||||
|
||||
type UpdateSpaceSortNumberDTO struct {
|
||||
Spaces []SpaceSortNumber `json:"spaces"`
|
||||
}
|
||||
@@ -42,6 +42,28 @@ FROM space s
|
||||
WHERE sm.user_id = $1
|
||||
ORDER BY sm.is_default DESC, sm.sort_number ASC;
|
||||
|
||||
-- name: GetSpaceByUserIdAndSpaceId :one
|
||||
SELECT
|
||||
s.id AS space_id,
|
||||
s.owner_id,
|
||||
s.name,
|
||||
sm.role,
|
||||
COALESCE(mc.member_count, 0) AS member_count,
|
||||
sm.is_default,
|
||||
sm.member_name,
|
||||
sm.sort_number
|
||||
FROM space s
|
||||
JOIN space_member sm
|
||||
ON s.id = sm.space_id
|
||||
LEFT JOIN (
|
||||
SELECT space_id, COUNT(*) AS member_count
|
||||
FROM space_member
|
||||
GROUP BY space_id
|
||||
) mc
|
||||
ON s.id = mc.space_id
|
||||
WHERE sm.user_id = $1 AND s.id = $2
|
||||
ORDER BY sm.is_default DESC, sm.sort_number ASC;
|
||||
|
||||
-- name: UpdateSpaceName :exec
|
||||
UPDATE space SET name = $1
|
||||
WHERE id = $2;
|
||||
@@ -25,3 +25,17 @@ WHERE space_id = $2 AND user_id = $3;
|
||||
-- name: UpdateSpaceIsDefault :exec
|
||||
UPDATE space_member SET is_default = $1
|
||||
WHERE space_id = $2 AND user_id = $3;
|
||||
|
||||
-- 将用户的其它空间更新为非默认
|
||||
-- name: UpdateOtherSpacesIsDefault :exec
|
||||
UPDATE space_member
|
||||
SET is_default = $1
|
||||
WHERE user_id = $2
|
||||
AND space_id != $3;
|
||||
|
||||
-- 更新空间顺序
|
||||
-- name: BatchUpdateSpaceSortNumber :batchexec
|
||||
UPDATE space_member
|
||||
SET sort_number = $1
|
||||
WHERE user_id = $2
|
||||
AND space_id = $3;
|
||||
@@ -0,0 +1,72 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// source: batch.go
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrBatchAlreadyClosed = errors.New("batch already closed")
|
||||
)
|
||||
|
||||
const batchUpdateSpaceSortNumber = `-- name: BatchUpdateSpaceSortNumber :batchexec
|
||||
UPDATE space_member
|
||||
SET sort_number = $1
|
||||
WHERE user_id = $2
|
||||
AND space_id = $3
|
||||
`
|
||||
|
||||
type BatchUpdateSpaceSortNumberBatchResults struct {
|
||||
br pgx.BatchResults
|
||||
tot int
|
||||
closed bool
|
||||
}
|
||||
|
||||
type BatchUpdateSpaceSortNumberParams struct {
|
||||
SortNumber int32
|
||||
UserID int64
|
||||
SpaceID int64
|
||||
}
|
||||
|
||||
// 更新空间顺序
|
||||
func (q *Queries) BatchUpdateSpaceSortNumber(ctx context.Context, arg []BatchUpdateSpaceSortNumberParams) *BatchUpdateSpaceSortNumberBatchResults {
|
||||
batch := &pgx.Batch{}
|
||||
for _, a := range arg {
|
||||
vals := []interface{}{
|
||||
a.SortNumber,
|
||||
a.UserID,
|
||||
a.SpaceID,
|
||||
}
|
||||
batch.Queue(batchUpdateSpaceSortNumber, vals...)
|
||||
}
|
||||
br := q.db.SendBatch(ctx, batch)
|
||||
return &BatchUpdateSpaceSortNumberBatchResults{br, len(arg), false}
|
||||
}
|
||||
|
||||
func (b *BatchUpdateSpaceSortNumberBatchResults) 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 *BatchUpdateSpaceSortNumberBatchResults) Close() error {
|
||||
b.closed = true
|
||||
return b.br.Close()
|
||||
}
|
||||
@@ -16,6 +16,7 @@ type DBTX interface {
|
||||
Query(context.Context, string, ...interface{}) (pgx.Rows, error)
|
||||
QueryRow(context.Context, string, ...interface{}) pgx.Row
|
||||
CopyFrom(ctx context.Context, tableName pgx.Identifier, columnNames []string, rowSrc pgx.CopyFromSource) (int64, error)
|
||||
SendBatch(context.Context, *pgx.Batch) pgx.BatchResults
|
||||
}
|
||||
|
||||
func New(db DBTX) *Queries {
|
||||
|
||||
@@ -66,6 +66,61 @@ func (q *Queries) GetSpaceByInviteCode(ctx context.Context, inviteCode string) (
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getSpaceByUserIdAndSpaceId = `-- name: GetSpaceByUserIdAndSpaceId :one
|
||||
SELECT
|
||||
s.id AS space_id,
|
||||
s.owner_id,
|
||||
s.name,
|
||||
sm.role,
|
||||
COALESCE(mc.member_count, 0) AS member_count,
|
||||
sm.is_default,
|
||||
sm.member_name,
|
||||
sm.sort_number
|
||||
FROM space s
|
||||
JOIN space_member sm
|
||||
ON s.id = sm.space_id
|
||||
LEFT JOIN (
|
||||
SELECT space_id, COUNT(*) AS member_count
|
||||
FROM space_member
|
||||
GROUP BY space_id
|
||||
) mc
|
||||
ON s.id = mc.space_id
|
||||
WHERE sm.user_id = $1 AND s.id = $2
|
||||
ORDER BY sm.is_default DESC, sm.sort_number ASC
|
||||
`
|
||||
|
||||
type GetSpaceByUserIdAndSpaceIdParams struct {
|
||||
UserID int64
|
||||
ID int64
|
||||
}
|
||||
|
||||
type GetSpaceByUserIdAndSpaceIdRow struct {
|
||||
SpaceID int64
|
||||
OwnerID int64
|
||||
Name string
|
||||
Role int16
|
||||
MemberCount int64
|
||||
IsDefault int16
|
||||
MemberName string
|
||||
SortNumber int32
|
||||
}
|
||||
|
||||
func (q *Queries) GetSpaceByUserIdAndSpaceId(ctx context.Context, arg GetSpaceByUserIdAndSpaceIdParams) (GetSpaceByUserIdAndSpaceIdRow, error) {
|
||||
row := q.db.QueryRow(ctx, getSpaceByUserIdAndSpaceId, arg.UserID, arg.ID)
|
||||
var i GetSpaceByUserIdAndSpaceIdRow
|
||||
err := row.Scan(
|
||||
&i.SpaceID,
|
||||
&i.OwnerID,
|
||||
&i.Name,
|
||||
&i.Role,
|
||||
&i.MemberCount,
|
||||
&i.IsDefault,
|
||||
&i.MemberName,
|
||||
&i.SortNumber,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const insertSpace = `-- name: InsertSpace :exec
|
||||
INSERT INTO space (
|
||||
id,name,invite_code,owner_id,create_time,update_time
|
||||
|
||||
@@ -15,6 +15,7 @@ FROM space_member
|
||||
WHERE user_id = $1
|
||||
`
|
||||
|
||||
// 合计某个用户的空间数量
|
||||
func (q *Queries) CountUserSpaces(ctx context.Context, userID int64) (int64, error) {
|
||||
row := q.db.QueryRow(ctx, countUserSpaces, userID)
|
||||
var count int64
|
||||
@@ -57,6 +58,7 @@ type InsertSpaceMemberParams struct {
|
||||
SortNumber int32
|
||||
}
|
||||
|
||||
// 插入空间用户
|
||||
func (q *Queries) InsertSpaceMember(ctx context.Context, arg InsertSpaceMemberParams) error {
|
||||
_, err := q.db.Exec(ctx, insertSpaceMember,
|
||||
arg.ID,
|
||||
@@ -70,6 +72,25 @@ func (q *Queries) InsertSpaceMember(ctx context.Context, arg InsertSpaceMemberPa
|
||||
return err
|
||||
}
|
||||
|
||||
const updateOtherSpacesIsDefault = `-- name: UpdateOtherSpacesIsDefault :exec
|
||||
UPDATE space_member
|
||||
SET is_default = $1
|
||||
WHERE user_id = $2
|
||||
AND space_id != $3
|
||||
`
|
||||
|
||||
type UpdateOtherSpacesIsDefaultParams struct {
|
||||
IsDefault int16
|
||||
UserID int64
|
||||
SpaceID int64
|
||||
}
|
||||
|
||||
// 将用户的其它空间更新为非默认
|
||||
func (q *Queries) UpdateOtherSpacesIsDefault(ctx context.Context, arg UpdateOtherSpacesIsDefaultParams) error {
|
||||
_, err := q.db.Exec(ctx, updateOtherSpacesIsDefault, arg.IsDefault, arg.UserID, arg.SpaceID)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateSpaceIsDefault = `-- name: UpdateSpaceIsDefault :exec
|
||||
UPDATE space_member SET is_default = $1
|
||||
WHERE space_id = $2 AND user_id = $3
|
||||
@@ -81,6 +102,7 @@ type UpdateSpaceIsDefaultParams struct {
|
||||
UserID int64
|
||||
}
|
||||
|
||||
// 更新空间是否为默认
|
||||
func (q *Queries) UpdateSpaceIsDefault(ctx context.Context, arg UpdateSpaceIsDefaultParams) error {
|
||||
_, err := q.db.Exec(ctx, updateSpaceIsDefault, arg.IsDefault, arg.SpaceID, arg.UserID)
|
||||
return err
|
||||
@@ -97,6 +119,7 @@ type UpdateSpaceMemberNameParams struct {
|
||||
UserID int64
|
||||
}
|
||||
|
||||
// 更新用户在空间内的名称
|
||||
func (q *Queries) UpdateSpaceMemberName(ctx context.Context, arg UpdateSpaceMemberNameParams) error {
|
||||
_, err := q.db.Exec(ctx, updateSpaceMemberName, arg.MemberName, arg.SpaceID, arg.UserID)
|
||||
return err
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
-- name: BatchInsertMoneyUserCategories :copyfrom
|
||||
INSERT INTO money_user_category (id, user_id, name, icon, type, sort_number)
|
||||
INSERT INTO money_user_category (id, user_id, name, icon, type, sort_number)
|
||||
VALUES ($1, $2, $3, $4, $5, $6);
|
||||
|
||||
-- name: FindUserMoneyCategoriesByUserID :many
|
||||
|
||||
@@ -4,6 +4,6 @@ module.exports = function (api) {
|
||||
presets: [
|
||||
['babel-preset-expo', { jsxImportSource: 'nativewind' }],
|
||||
'nativewind/babel',
|
||||
]
|
||||
],
|
||||
}
|
||||
}
|
||||
@@ -1,68 +1,33 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import { KeyboardAvoidingView } from 'react-native-keyboard-controller'
|
||||
import React, { useState } from 'react'
|
||||
import { Text, View } from 'react-native'
|
||||
import { AButton, AModal } from '@/components/common/ao'
|
||||
import AnimatedSwitch from '@/components/common/ao/Switch'
|
||||
|
||||
export default function TestScreen() {
|
||||
const [showDissolve, setShowDissolve] = useState(false)
|
||||
|
||||
export default function KeyboardAvoidingViewExample() {
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
behavior="padding"
|
||||
keyboardVerticalOffset={100}
|
||||
style={styles.content}
|
||||
>
|
||||
<View style={styles.inner}>
|
||||
<Text style={styles.heading}>Header</Text>
|
||||
<View>
|
||||
<TextInput placeholder="Username" style={styles.textInput} />
|
||||
<TextInput placeholder="Password" style={styles.textInput} />
|
||||
</View>
|
||||
<TouchableOpacity style={styles.button}>
|
||||
<Text style={styles.text}>Submit</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
||||
<AButton onPress={() => {
|
||||
setShowDissolve(true)
|
||||
}}
|
||||
>
|
||||
测试
|
||||
</AButton>
|
||||
<AModal
|
||||
title="提示"
|
||||
body="确认解散该空间么?"
|
||||
isOpen={showDissolve}
|
||||
onClose={() => {
|
||||
setShowDissolve(false)
|
||||
}}
|
||||
onCancel={() => {
|
||||
setShowDissolve(false)
|
||||
}}
|
||||
onConfirm={() => {
|
||||
setShowDissolve(false)
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
content: {
|
||||
flex: 1,
|
||||
maxHeight: 600,
|
||||
},
|
||||
heading: {
|
||||
fontSize: 36,
|
||||
marginBottom: 48,
|
||||
fontWeight: '600',
|
||||
},
|
||||
inner: {
|
||||
padding: 24,
|
||||
flex: 1,
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
textInput: {
|
||||
height: 45,
|
||||
borderColor: '#000000',
|
||||
borderWidth: 1,
|
||||
borderRadius: 10,
|
||||
marginBottom: 36,
|
||||
paddingLeft: 10,
|
||||
},
|
||||
button: {
|
||||
marginTop: 12,
|
||||
height: 45,
|
||||
borderRadius: 10,
|
||||
backgroundColor: 'rgb(40, 64, 147)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
text: {
|
||||
fontWeight: '500',
|
||||
fontSize: 16,
|
||||
color: 'white',
|
||||
},
|
||||
})
|
||||
@@ -87,7 +87,7 @@ export default function RootLayout() {
|
||||
</GluestackUIProvider>
|
||||
</KeyboardProvider>
|
||||
</MenuProvider>
|
||||
<Toaster />
|
||||
<Toaster duration={1000} />
|
||||
</GestureHandlerRootView>
|
||||
)
|
||||
}
|
||||
@@ -1,175 +0,0 @@
|
||||
import type { UserSpace } from '@/types/space'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { Text, View } from 'react-native'
|
||||
import { KeyboardAvoidingView } from 'react-native-keyboard-controller'
|
||||
import Animated, { useAnimatedStyle } from 'react-native-reanimated'
|
||||
import { toast } from 'sonner-native'
|
||||
import { AButton, LucideIcon } from '@/components/common/ao'
|
||||
import { AInput, AModal } from '@/components/common/ao/index'
|
||||
import {
|
||||
Actionsheet,
|
||||
ActionsheetBackdrop,
|
||||
ActionsheetContent,
|
||||
ActionsheetDragIndicator,
|
||||
ActionsheetDragIndicatorWrapper,
|
||||
ActionsheetIcon,
|
||||
ActionsheetItem,
|
||||
ActionsheetItemText,
|
||||
} from '@/components/ui/actionsheet'
|
||||
import { dissolveSpaceApi, updateSpaceNameApi } from '@/request/api/space'
|
||||
import { useUserStore } from '@/store/user'
|
||||
|
||||
interface SettingActionsProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
|
||||
space: UserSpace | null
|
||||
|
||||
onActionSuccess?: () => void
|
||||
}
|
||||
|
||||
export default function SettingActions(
|
||||
{
|
||||
isOpen,
|
||||
onClose,
|
||||
space,
|
||||
onActionSuccess,
|
||||
}: SettingActionsProps,
|
||||
) {
|
||||
const userStore = useUserStore()
|
||||
const inputRef = useRef<any>(null)
|
||||
const [localSpace, setLocalSpace] = useState(space)
|
||||
|
||||
const [showNameModal, setShowNameModal] = useState(false)
|
||||
const [showMemberNameModal, setShowMemberNameModal] = useState(false)
|
||||
useEffect(() => {
|
||||
setLocalSpace(space)
|
||||
}, [space])
|
||||
|
||||
useEffect(() => {
|
||||
if (showNameModal) {
|
||||
inputRef.current?.focus()
|
||||
}
|
||||
}, [showNameModal])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Actionsheet
|
||||
isOpen={isOpen}
|
||||
onClose={() => {
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
<ActionsheetBackdrop />
|
||||
<ActionsheetContent
|
||||
style={{
|
||||
paddingBottom: 6,
|
||||
}}
|
||||
>
|
||||
<ActionsheetDragIndicatorWrapper>
|
||||
<ActionsheetDragIndicator />
|
||||
</ActionsheetDragIndicatorWrapper>
|
||||
{
|
||||
space?.role === 0 && (
|
||||
<ActionsheetItem>
|
||||
<ActionsheetIcon
|
||||
as={() => <LucideIcon icon="UserCog" />}
|
||||
/>
|
||||
<ActionsheetItemText>成员管理</ActionsheetItemText>
|
||||
</ActionsheetItem>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
space?.owner_id === userStore.userInfo.id && (
|
||||
<ActionsheetItem onPress={() => {
|
||||
setShowNameModal(true)
|
||||
}}
|
||||
>
|
||||
<ActionsheetIcon
|
||||
as={() => <LucideIcon icon="Pencil" />}
|
||||
/>
|
||||
<ActionsheetItemText>修改空间名称</ActionsheetItemText>
|
||||
</ActionsheetItem>
|
||||
)
|
||||
}
|
||||
|
||||
<ActionsheetItem>
|
||||
<ActionsheetIcon
|
||||
as={() => <LucideIcon icon="Pencil" />}
|
||||
/>
|
||||
<ActionsheetItemText>修改我的空间昵称</ActionsheetItemText>
|
||||
</ActionsheetItem>
|
||||
|
||||
<ActionsheetItem>
|
||||
<ActionsheetIcon
|
||||
as={() => <LucideIcon icon="Star" />}
|
||||
/>
|
||||
<ActionsheetItemText>
|
||||
{space?.is_default === 1 ? '取消' : '设置'}
|
||||
默认
|
||||
</ActionsheetItemText>
|
||||
</ActionsheetItem>
|
||||
|
||||
<ActionsheetItem onPress={() => {
|
||||
if (space?.owner_id === userStore.userInfo.id) {
|
||||
// 解散空间
|
||||
dissolveSpaceApi({
|
||||
space_id: space.space_id,
|
||||
}).then((res) => {
|
||||
if (res.isSuccess) {
|
||||
toast.success('空间已解散')
|
||||
onActionSuccess?.()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
else {
|
||||
// 退出空间
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ActionsheetIcon
|
||||
as={() => <LucideIcon icon="LogOut" />}
|
||||
/>
|
||||
<ActionsheetItemText>
|
||||
{space?.owner_id === userStore.userInfo.id ? '解散' : '退出'}
|
||||
空间
|
||||
</ActionsheetItemText>
|
||||
</ActionsheetItem>
|
||||
|
||||
</ActionsheetContent>
|
||||
</Actionsheet>
|
||||
<Actionsheet
|
||||
isOpen={showNameModal}
|
||||
onClose={() => {
|
||||
setShowNameModal(false)
|
||||
}}
|
||||
>
|
||||
<KeyboardAvoidingView
|
||||
behavior="position"
|
||||
>
|
||||
<ActionsheetBackdrop />
|
||||
|
||||
<ActionsheetContent>
|
||||
<ActionsheetDragIndicatorWrapper>
|
||||
<ActionsheetDragIndicator />
|
||||
</ActionsheetDragIndicatorWrapper>
|
||||
|
||||
<View className="pb-6">
|
||||
<View className="items-center mb-6">
|
||||
<Text className="text-lg font-semibold">空间名称</Text>
|
||||
</View>
|
||||
|
||||
<View className="w-full mb-4">
|
||||
<AInput ref={inputRef} className="w-full" placeholder="请输入空间名称" />
|
||||
</View>
|
||||
|
||||
<AButton className="rounded-full">保存</AButton>
|
||||
</View>
|
||||
</ActionsheetContent>
|
||||
</KeyboardAvoidingView>
|
||||
</Actionsheet>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,20 +1,28 @@
|
||||
import type { UserSpace } from '@/types/space'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useFocusEffect, useRouter } from 'expo-router'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Pressable, Text, 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 { toast } from 'sonner-native'
|
||||
import { AButton, ADivider, AInput, LucideIcon } from '@/components/common/ao'
|
||||
import { AModal } from '@/components/common/ao/index'
|
||||
import EmptyState from '@/components/common/EmptyStatus'
|
||||
import CustomHeader from '@/components/custom/Header'
|
||||
import {
|
||||
Actionsheet,
|
||||
ActionsheetBackdrop,
|
||||
ActionsheetContent,
|
||||
ActionsheetDragIndicator,
|
||||
ActionsheetDragIndicatorWrapper,
|
||||
} from '@/components/ui/actionsheet'
|
||||
import { Badge, BadgeText } from '@/components/ui/badge'
|
||||
import { createSpaceApi, listUserSpacesApi } from '@/request/api/space/index'
|
||||
import { createSpaceApi, listUserSpacesApi, updateSpaceSortNumberApi } from '@/request/api/space/index'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import SettingActions from './components/SettingActions'
|
||||
|
||||
export default function AuthSpace() {
|
||||
const userStore = useUserStore()
|
||||
const router = useRouter()
|
||||
|
||||
// scrollView 相关
|
||||
const scrollableRef = useAnimatedRef<Animated.ScrollView>()
|
||||
@@ -23,23 +31,30 @@ export default function AuthSpace() {
|
||||
|
||||
// 空间列表
|
||||
const [spaces, setSpaces] = useState<UserSpace[]>([])
|
||||
const [selectedId, setSelectedId] = useState<number | null>(null)
|
||||
|
||||
// 新建
|
||||
const [createOpen, setCreateOpen] = useState(false)
|
||||
const [createForm, setCreateForm] = useState({
|
||||
name: '',
|
||||
})
|
||||
const [createLoading, setCreateLoading] = useState(false)
|
||||
|
||||
// 设置
|
||||
const [showSettings, setShowSettings] = useState(false)
|
||||
const [settingCurrentSpace, setSettingCurrentSpace] = useState<UserSpace | null>(null)
|
||||
const createInputRef = useRef<any>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (createOpen) {
|
||||
const timer = setTimeout(() => {
|
||||
createInputRef.current?.focus()
|
||||
}, 300)
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [createOpen])
|
||||
|
||||
// 获取空间列表
|
||||
function listUserSpaces() {
|
||||
listUserSpacesApi().then((res) => {
|
||||
if (res.isSuccess) {
|
||||
console.log(res.data)
|
||||
setSpaces(res.data.spaces)
|
||||
}
|
||||
})
|
||||
@@ -50,23 +65,22 @@ export default function AuthSpace() {
|
||||
<Pressable
|
||||
key={item.space_id}
|
||||
onPress={() => {
|
||||
setSelectedId(item.space_id)
|
||||
scrollToCenter(index)
|
||||
router.push({
|
||||
pathname: '/space/manage/sub-page/settings',
|
||||
params: {
|
||||
spaceId: item.space_id,
|
||||
},
|
||||
})
|
||||
}}
|
||||
style={{
|
||||
height: ITEM_HEIGHT,
|
||||
}}
|
||||
className={`bg-[#f9f9f9] rounded-md flex-row items-center justify-between px-4 py-3 border
|
||||
${item.space_id === selectedId ? 'border-gray-700' : 'border-gray-200'}`}
|
||||
className={`rounded-xl flex-row items-center px-4 py-3 bg-[#fff] ${
|
||||
index === spaces.length - 1 ? 'mb-4' : ''
|
||||
}`}
|
||||
>
|
||||
<View className="flex-row items-center">
|
||||
<Sortable.Handle>
|
||||
<LucideIcon
|
||||
icon="Grip"
|
||||
size={20}
|
||||
className="mr-4"
|
||||
/>
|
||||
</Sortable.Handle>
|
||||
<View className="w-full flex-row items-center justify-between">
|
||||
|
||||
<View>
|
||||
<View className="flex-row items-center gap-2">
|
||||
<Text
|
||||
@@ -77,16 +91,16 @@ export default function AuthSpace() {
|
||||
|
||||
{
|
||||
item.owner_id === userStore.userInfo.id && (
|
||||
<Badge size="sm" variant="solid" action="muted">
|
||||
<BadgeText>创建者</BadgeText>
|
||||
<Badge size="sm" className="bg-[#333] rounded-full">
|
||||
<BadgeText className="text-white">创建者</BadgeText>
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
item.is_default === 1 && (
|
||||
<Badge size="sm" variant="solid" action="info">
|
||||
<BadgeText>默认</BadgeText>
|
||||
<Badge size="sm" className="bg-[#333] rounded-full">
|
||||
<BadgeText className="text-white">默认</BadgeText>
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
@@ -112,33 +126,15 @@ export default function AuthSpace() {
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Sortable.Handle>
|
||||
<LucideIcon
|
||||
icon="Grip"
|
||||
size={20}
|
||||
/>
|
||||
</Sortable.Handle>
|
||||
</View>
|
||||
|
||||
<View className="flex flex-row items-center gap-3">
|
||||
<View className="w-8 h-8 rounded-full items-center justify-center bg-[#333333]">
|
||||
<LucideIcon
|
||||
icon="UserPlus"
|
||||
size={18}
|
||||
color="#fff"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Pressable
|
||||
className="w-8 h-8 rounded-full items-center justify-center bg-[#333333]"
|
||||
onPress={(e) => {
|
||||
e.stopPropagation()
|
||||
setSettingCurrentSpace(item)
|
||||
setShowSettings(true)
|
||||
}}
|
||||
>
|
||||
<LucideIcon
|
||||
icon="Bolt"
|
||||
size={18}
|
||||
color="#fff"
|
||||
clickable={false}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
</Pressable>
|
||||
)
|
||||
|
||||
@@ -157,16 +153,17 @@ export default function AuthSpace() {
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
listUserSpaces()
|
||||
}, [])
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
listUserSpaces()
|
||||
}, []),
|
||||
)
|
||||
|
||||
return (
|
||||
<View className="flex-1 bg-white">
|
||||
<View className="flex-1 bg-[#f5f6f8]">
|
||||
<CustomHeader title="空间列表" />
|
||||
|
||||
<View className="flex-1 px-4">
|
||||
|
||||
<View className="flex-1 p-4">
|
||||
<View className="">
|
||||
<Text className="text-sm text-gray-500 mb-2">
|
||||
选择或创建空间
|
||||
@@ -224,84 +221,108 @@ export default function AuthSpace() {
|
||||
highlight-next-line
|
||||
customHandle
|
||||
autoScrollActivationOffset={200}
|
||||
onDragEnd={(dragData) => {
|
||||
const newData = dragData.data
|
||||
if (JSON.stringify(spaces) !== JSON.stringify(newData)) {
|
||||
const updateData = newData.map((item, index) => {
|
||||
return {
|
||||
space_id: item.space_id,
|
||||
sort_number: index,
|
||||
}
|
||||
})
|
||||
|
||||
updateSpaceSortNumberApi({
|
||||
spaces: updateData,
|
||||
}).then((res) => {
|
||||
if (res.isSuccess) {
|
||||
// 更新成功
|
||||
}
|
||||
})
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Animated.ScrollView>
|
||||
|
||||
<View className="py-2">
|
||||
<AButton size="sm" leftIcon={<LucideIcon icon="CircleArrowRight" color="#fff" />}>进入空间</AButton>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<AModal
|
||||
title="创建新空间"
|
||||
showCloseIcon
|
||||
body={
|
||||
(
|
||||
<View className="gap-2">
|
||||
<Text>空间名称</Text>
|
||||
<AInput
|
||||
className="flex-1"
|
||||
value={createForm.name}
|
||||
onChangeText={(val) => {
|
||||
setCreateForm(prev => ({
|
||||
...prev,
|
||||
name: val,
|
||||
}))
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
<Actionsheet
|
||||
isOpen={createOpen}
|
||||
onCancel={() => {
|
||||
setCreateOpen(false)
|
||||
setCreateForm({
|
||||
name: '',
|
||||
})
|
||||
}}
|
||||
onClose={() => {
|
||||
setCreateOpen(false)
|
||||
setCreateForm({
|
||||
name: '',
|
||||
})
|
||||
}}
|
||||
onConfirm={async () => {
|
||||
if (!createForm.name) {
|
||||
toast.warning('请输入空间名称')
|
||||
return
|
||||
}
|
||||
|
||||
await createSpaceApi({
|
||||
name: createForm.name,
|
||||
member_name: userStore.userInfo.nickname,
|
||||
sort_number: 0,
|
||||
}).then((res) => {
|
||||
if (res.isSuccess) {
|
||||
toast.success('创建成功')
|
||||
setCreateOpen(false)
|
||||
setCreateForm({
|
||||
name: '',
|
||||
})
|
||||
|
||||
listUserSpaces()
|
||||
}
|
||||
})
|
||||
}}
|
||||
>
|
||||
</AModal>
|
||||
<KeyboardAvoidingView
|
||||
behavior="position"
|
||||
>
|
||||
<ActionsheetBackdrop />
|
||||
|
||||
<SettingActions
|
||||
isOpen={showSettings}
|
||||
onClose={() => {
|
||||
setShowSettings(false)
|
||||
<ActionsheetContent>
|
||||
<ActionsheetDragIndicatorWrapper>
|
||||
<ActionsheetDragIndicator />
|
||||
</ActionsheetDragIndicatorWrapper>
|
||||
|
||||
setTimeout(() => {
|
||||
setSettingCurrentSpace(null)
|
||||
}, 300)
|
||||
}}
|
||||
space={settingCurrentSpace}
|
||||
/>
|
||||
<View className="pb-6">
|
||||
<View className="items-center mb-6">
|
||||
<Text className="text-lg font-semibold">创建新空间</Text>
|
||||
</View>
|
||||
|
||||
<View className="w-full mb-4">
|
||||
<AInput
|
||||
ref={createInputRef}
|
||||
value={createForm.name}
|
||||
onChangeText={(val) => {
|
||||
setCreateForm(prev => ({
|
||||
...prev,
|
||||
name: val,
|
||||
}))
|
||||
}}
|
||||
className="w-full"
|
||||
placeholder="请输入空间名称"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<AButton
|
||||
className="rounded-full"
|
||||
loading={createLoading}
|
||||
onPress={() => {
|
||||
if (!createForm.name) {
|
||||
toast.warning('请输入空间名称')
|
||||
return
|
||||
}
|
||||
|
||||
setCreateLoading(true)
|
||||
|
||||
try {
|
||||
createSpaceApi({
|
||||
name: createForm.name,
|
||||
member_name: userStore.userInfo.nickname,
|
||||
sort_number: spaces.length,
|
||||
}).then((res) => {
|
||||
if (res.isSuccess) {
|
||||
toast.success('创建成功')
|
||||
setCreateOpen(false)
|
||||
setCreateForm({
|
||||
name: '',
|
||||
})
|
||||
|
||||
listUserSpaces()
|
||||
}
|
||||
})
|
||||
}
|
||||
finally {
|
||||
setCreateLoading(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
创建
|
||||
</AButton>
|
||||
</View>
|
||||
</ActionsheetContent>
|
||||
</KeyboardAvoidingView>
|
||||
</Actionsheet>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { View } from 'react-native'
|
||||
|
||||
import CustomHeader from '@/components/custom/Header'
|
||||
|
||||
export default function Settings() {
|
||||
return (
|
||||
<View className="flex-1">
|
||||
<CustomHeader title="空间设置" />
|
||||
|
||||
<View></View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,430 @@
|
||||
import type { UserSpace } from '@/types/space'
|
||||
import { router, useLocalSearchParams } from 'expo-router'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { Text, TouchableOpacity, View } from 'react-native'
|
||||
import { KeyboardAvoidingView } from 'react-native-keyboard-controller'
|
||||
import { toast } from 'sonner-native'
|
||||
import { AButton, AInput, AModal, ASwitch, LucideIcon } from '@/components/common/ao'
|
||||
import EmptyState from '@/components/common/EmptyStatus'
|
||||
import CustomHeader from '@/components/custom/Header'
|
||||
import {
|
||||
Actionsheet,
|
||||
ActionsheetBackdrop,
|
||||
ActionsheetContent,
|
||||
ActionsheetDragIndicator,
|
||||
ActionsheetDragIndicatorWrapper,
|
||||
} from '@/components/ui/actionsheet'
|
||||
import { dissolveSpaceApi, getSpaceSettingsApi, updateSpaceDefaultApi, updateSpaceMemberNameApi, updateSpaceNameApi } from '@/request/api/space'
|
||||
import { useUserStore } from '@/store/user'
|
||||
|
||||
interface ItemProps {
|
||||
icon: any
|
||||
title: string
|
||||
right?: React.ReactNode
|
||||
isLast?: boolean
|
||||
showRightIcon?: boolean
|
||||
onPress?: () => void
|
||||
}
|
||||
|
||||
function Item({
|
||||
icon,
|
||||
title,
|
||||
right,
|
||||
isLast,
|
||||
showRightIcon = true,
|
||||
onPress,
|
||||
}: ItemProps) {
|
||||
return (
|
||||
<>
|
||||
<TouchableOpacity
|
||||
className="flex-row items-center justify-between px-4 h-12"
|
||||
onPress={onPress}
|
||||
>
|
||||
<View className="flex-row items-center gap-3">
|
||||
<LucideIcon icon={icon} size={18} strokeWidth={1.5} />
|
||||
<Text className="text-sm text-gray-900">{title}</Text>
|
||||
</View>
|
||||
|
||||
<View className="flex-row items-center gap-1">
|
||||
{right}
|
||||
{showRightIcon && (
|
||||
<LucideIcon icon="ChevronRight" size={16} color="#9ca3af" />
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
{!isLast && <View className="h-px bg-gray-100 ml-14" />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Settings() {
|
||||
const userStore = useUserStore()
|
||||
|
||||
const params = useLocalSearchParams<{ spaceId: string }>()
|
||||
const { spaceId } = params
|
||||
|
||||
const [space, setSpace] = useState<UserSpace | null>(null)
|
||||
|
||||
// 空间名称
|
||||
const [showName, setShowName] = useState(false)
|
||||
const [nameForm, setNameForm] = useState({
|
||||
name: '',
|
||||
})
|
||||
const [nameLoading, setNameLoading] = useState(false)
|
||||
const nameInputRef = useRef<any>(null)
|
||||
|
||||
// 空间昵称
|
||||
const [showMemberName, setShowMemberName] = useState(false)
|
||||
const [memberNameForm, setMemberNameForm] = useState({
|
||||
name: '',
|
||||
})
|
||||
const [memberNameLoading, setMemberNameLoading] = useState(false)
|
||||
const memberNameInputRef = useRef<any>(null)
|
||||
|
||||
// 默认
|
||||
const [defaultLoading, setDefaultLoading] = useState(false)
|
||||
|
||||
// 解散
|
||||
const [showDissolve, setShowDissolve] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (showName) {
|
||||
const timer = setTimeout(() => {
|
||||
nameInputRef.current?.focus()
|
||||
}, 300)
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [showName])
|
||||
|
||||
useEffect(() => {
|
||||
if (!spaceId) {
|
||||
toast.warning('空间无效')
|
||||
router.back()
|
||||
}
|
||||
else {
|
||||
getSpaceSettingsApi({
|
||||
space_id: Number(spaceId),
|
||||
}).then((res) => {
|
||||
if (res.isSuccess) {
|
||||
setSpace(res.data)
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [spaceId])
|
||||
|
||||
return (
|
||||
<View className="flex-1 bg-[#f5f6f8]">
|
||||
<CustomHeader title="空间设置" />
|
||||
{
|
||||
!space
|
||||
? (
|
||||
<EmptyState title="正在加载空间中 . . ." />
|
||||
)
|
||||
: (
|
||||
<View className="flex-1 px-4 pt-4">
|
||||
<Text className="text-sm font-semibold text-gray-400 mb-2 tracking-wider">
|
||||
常用设置
|
||||
</Text>
|
||||
|
||||
<View className="bg-white rounded-xl overflow-hidden mb-6">
|
||||
{
|
||||
space?.owner_id === userStore.userInfo.id && (
|
||||
<Item
|
||||
icon="Earth"
|
||||
title="空间名称"
|
||||
right={(
|
||||
<Text className="text-sm text-gray-400">{space.name}</Text>
|
||||
)}
|
||||
onPress={() => {
|
||||
setNameForm({
|
||||
name: space.name,
|
||||
})
|
||||
setShowName(true)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
<Item
|
||||
icon="User"
|
||||
title="我的空间昵称"
|
||||
right={(
|
||||
<Text className="text-sm text-gray-400">{space.member_name}</Text>
|
||||
)}
|
||||
onPress={() => {
|
||||
setMemberNameForm({
|
||||
name: space.member_name,
|
||||
})
|
||||
setShowMemberName(true)
|
||||
}}
|
||||
/>
|
||||
|
||||
<Item
|
||||
icon="Star"
|
||||
title="设为默认空间"
|
||||
isLast
|
||||
showRightIcon={false}
|
||||
right={(
|
||||
<ASwitch
|
||||
value={space.is_default}
|
||||
trueValue={1}
|
||||
falseValue={0}
|
||||
disabled={defaultLoading}
|
||||
onChange={(val) => {
|
||||
if (!space) {
|
||||
toast.error('空间信息异常')
|
||||
return
|
||||
}
|
||||
try {
|
||||
setDefaultLoading(true)
|
||||
|
||||
updateSpaceDefaultApi({
|
||||
space_id: space.space_id,
|
||||
is_default: val,
|
||||
}).then((res) => {
|
||||
if (res.isSuccess) {
|
||||
toast.success('更新默认空间成功')
|
||||
|
||||
setSpace(prev => prev
|
||||
? ({
|
||||
...prev,
|
||||
is_default: val,
|
||||
})
|
||||
: null)
|
||||
}
|
||||
})
|
||||
}
|
||||
finally {
|
||||
setDefaultLoading(false)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Text className="text-sm font-semibold text-gray-400 mb-2 tracking-wider">
|
||||
其它操作
|
||||
</Text>
|
||||
|
||||
<View className="bg-white rounded-xl overflow-hidden">
|
||||
<Item
|
||||
icon="Users"
|
||||
title="成员管理"
|
||||
/>
|
||||
|
||||
<Item
|
||||
icon="LogOut"
|
||||
title={userStore.userInfo.id === space.owner_id ? '解散空间' : '退出空间'}
|
||||
isLast
|
||||
onPress={() => {
|
||||
setShowDissolve(true)
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
{/* 空间名称 */}
|
||||
<Actionsheet
|
||||
isOpen={showName}
|
||||
onClose={() => {
|
||||
setShowName(false)
|
||||
setNameForm(prev => ({
|
||||
...prev,
|
||||
name: '',
|
||||
}))
|
||||
}}
|
||||
>
|
||||
<KeyboardAvoidingView
|
||||
behavior="position"
|
||||
>
|
||||
<ActionsheetBackdrop />
|
||||
|
||||
<ActionsheetContent>
|
||||
<ActionsheetDragIndicatorWrapper>
|
||||
<ActionsheetDragIndicator />
|
||||
</ActionsheetDragIndicatorWrapper>
|
||||
|
||||
<View className="pb-6">
|
||||
<View className="items-center mb-6">
|
||||
<Text className="text-lg font-semibold">空间名称</Text>
|
||||
</View>
|
||||
|
||||
<View className="w-full mb-4">
|
||||
<AInput
|
||||
ref={nameInputRef}
|
||||
value={nameForm.name}
|
||||
onChangeText={text => setNameForm(prev => prev
|
||||
? {
|
||||
...prev,
|
||||
name: text,
|
||||
}
|
||||
: null)}
|
||||
className="w-full"
|
||||
placeholder="请输入空间名称"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<AButton
|
||||
className="rounded-full"
|
||||
loading={nameLoading}
|
||||
onPress={() => {
|
||||
if (!space) {
|
||||
toast.error('空间信息异常')
|
||||
return
|
||||
}
|
||||
try {
|
||||
setNameLoading(true)
|
||||
|
||||
updateSpaceNameApi({
|
||||
space_id: space.space_id,
|
||||
space_name: nameForm.name,
|
||||
}).then((res) => {
|
||||
if (res.isSuccess) {
|
||||
toast.success('空间名称已更新')
|
||||
|
||||
setShowName(false)
|
||||
|
||||
setSpace(prev => prev
|
||||
? ({
|
||||
...prev,
|
||||
name: nameForm.name,
|
||||
})
|
||||
: null)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
finally {
|
||||
setNameLoading(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
保存
|
||||
</AButton>
|
||||
</View>
|
||||
</ActionsheetContent>
|
||||
</KeyboardAvoidingView>
|
||||
</Actionsheet>
|
||||
|
||||
{/* 空间昵称 */}
|
||||
<Actionsheet
|
||||
isOpen={showMemberName}
|
||||
onClose={() => {
|
||||
setShowMemberName(false)
|
||||
setMemberNameForm(prev => ({
|
||||
...prev,
|
||||
name: '',
|
||||
}))
|
||||
}}
|
||||
>
|
||||
<KeyboardAvoidingView
|
||||
behavior="position"
|
||||
>
|
||||
<ActionsheetBackdrop />
|
||||
|
||||
<ActionsheetContent>
|
||||
<ActionsheetDragIndicatorWrapper>
|
||||
<ActionsheetDragIndicator />
|
||||
</ActionsheetDragIndicatorWrapper>
|
||||
|
||||
<View className="pb-6">
|
||||
<View className="items-center mb-6">
|
||||
<Text className="text-lg font-semibold">我的空间昵称</Text>
|
||||
</View>
|
||||
|
||||
<View className="w-full mb-4">
|
||||
<AInput
|
||||
ref={memberNameInputRef}
|
||||
value={memberNameForm.name}
|
||||
onChangeText={text => setMemberNameForm(prev => prev
|
||||
? {
|
||||
...prev,
|
||||
name: text,
|
||||
}
|
||||
: null)}
|
||||
className="w-full"
|
||||
placeholder="请输入空间昵称"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<AButton
|
||||
className="rounded-full"
|
||||
loading={memberNameLoading}
|
||||
onPress={() => {
|
||||
if (!space) {
|
||||
toast.error('空间信息异常')
|
||||
return
|
||||
}
|
||||
try {
|
||||
setMemberNameLoading(true)
|
||||
|
||||
updateSpaceMemberNameApi({
|
||||
space_id: space.space_id,
|
||||
member_name: memberNameForm.name,
|
||||
}).then((res) => {
|
||||
if (res.isSuccess) {
|
||||
toast.success('空间昵称已更新')
|
||||
|
||||
setShowMemberName(false)
|
||||
|
||||
setSpace(prev => prev
|
||||
? ({
|
||||
...prev,
|
||||
member_name: memberNameForm.name,
|
||||
})
|
||||
: null)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
finally {
|
||||
setMemberNameLoading(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
保存
|
||||
</AButton>
|
||||
</View>
|
||||
</ActionsheetContent>
|
||||
</KeyboardAvoidingView>
|
||||
</Actionsheet>
|
||||
|
||||
{/* 解散 */}
|
||||
<AModal
|
||||
title="提示"
|
||||
body="确认解散该空间么?"
|
||||
isOpen={showDissolve}
|
||||
onClose={() => {
|
||||
setShowDissolve(false)
|
||||
}}
|
||||
onCancel={() => {
|
||||
setShowDissolve(false)
|
||||
}}
|
||||
onConfirm={async () => {
|
||||
if (!space) {
|
||||
toast.error('空间信息异常')
|
||||
return
|
||||
}
|
||||
|
||||
await dissolveSpaceApi({
|
||||
space_id: space.space_id,
|
||||
}).then((res) => {
|
||||
if (res.isSuccess) {
|
||||
setShowDissolve(false)
|
||||
toast.success('解散成功')
|
||||
|
||||
setTimeout(() => {
|
||||
router.back()
|
||||
}, 300)
|
||||
}
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -30,7 +30,6 @@ export default function EmptyState({
|
||||
flexGrow: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'white',
|
||||
padding: 16,
|
||||
}}
|
||||
showsVerticalScrollIndicator={true}
|
||||
|
||||
@@ -47,6 +47,7 @@ const AInput = forwardRef<React.ComponentRef<typeof InputField>, AInputProps>(
|
||||
isFocused={isFocused}
|
||||
className={className}
|
||||
style={style}
|
||||
|
||||
>
|
||||
{leftIcon && (
|
||||
<InputSlot className="pl-3">
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import type { ViewStyle } from 'react-native'
|
||||
import { useState } from 'react'
|
||||
import { Text } from 'react-native'
|
||||
import { Text, TouchableOpacity, View } from 'react-native'
|
||||
import {
|
||||
ButtonSpinner,
|
||||
} from '@/components/ui/button'
|
||||
import { Heading } from '@/components/ui/heading'
|
||||
import { CloseIcon, Icon } from '@/components/ui/icon'
|
||||
|
||||
import {
|
||||
Modal,
|
||||
ModalBackdrop,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
} from '@/components/ui/modal'
|
||||
import AButton from '../ao/Button'
|
||||
|
||||
interface AModalProps {
|
||||
isOpen?: boolean
|
||||
@@ -23,7 +24,6 @@ interface AModalProps {
|
||||
onConfirm?: () => Promise<void> | void
|
||||
onCancel?: () => void
|
||||
showBackdrop?: boolean
|
||||
showCloseIcon?: boolean
|
||||
|
||||
className?: string
|
||||
style?: ViewStyle
|
||||
@@ -37,7 +37,6 @@ export default function AModal({
|
||||
onClose,
|
||||
onConfirm,
|
||||
showBackdrop = true,
|
||||
showCloseIcon = false,
|
||||
|
||||
className,
|
||||
style,
|
||||
@@ -76,42 +75,58 @@ export default function AModal({
|
||||
style={{
|
||||
...style,
|
||||
width: '80%',
|
||||
padding: 0,
|
||||
}}
|
||||
>
|
||||
<ModalHeader>
|
||||
{title && (typeof title === 'string' ? <Heading>{title}</Heading> : title)}
|
||||
{
|
||||
showCloseIcon && (
|
||||
<ModalCloseButton>
|
||||
<Icon as={CloseIcon} />
|
||||
</ModalCloseButton>
|
||||
)
|
||||
}
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
{body && (typeof body === 'string' ? <Text>{body}</Text> : body)}
|
||||
</ModalBody>
|
||||
{title && (
|
||||
<ModalHeader>
|
||||
{typeof body === 'string'
|
||||
? (
|
||||
<View className="items-center justify-center w-full py-2">
|
||||
<Heading>{title}</Heading>
|
||||
</View>
|
||||
)
|
||||
: (
|
||||
title
|
||||
)}
|
||||
</ModalHeader>
|
||||
)}
|
||||
|
||||
{body && (
|
||||
<ModalBody>
|
||||
{typeof body === 'string'
|
||||
? (
|
||||
<View className="items-center justify-center w-full">
|
||||
<Text>{body}</Text>
|
||||
</View>
|
||||
)
|
||||
: (
|
||||
body
|
||||
)}
|
||||
</ModalBody>
|
||||
)}
|
||||
<ModalFooter>
|
||||
{
|
||||
footer || (
|
||||
<>
|
||||
<AButton
|
||||
variant="outline"
|
||||
action="secondary"
|
||||
className="mr-3"
|
||||
<View className="flex-row border-t border-gray-300">
|
||||
<TouchableOpacity
|
||||
className="flex-1 items-center justify-center py-3 border-r border-gray-300"
|
||||
onPress={onClose}
|
||||
size="sm"
|
||||
>
|
||||
取消
|
||||
</AButton>
|
||||
<AButton
|
||||
<Text className="text-gray-500">取消</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
className="flex-1 flex-row gap-2 items-center justify-center py-3"
|
||||
onPress={handleConfirm}
|
||||
size="sm"
|
||||
loading={innerLoading}
|
||||
>
|
||||
确认
|
||||
</AButton>
|
||||
</>
|
||||
{
|
||||
innerLoading && (
|
||||
<ButtonSpinner color="#333" />
|
||||
)
|
||||
}
|
||||
<Text className="text-[#333]">确认</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
</ModalFooter>
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import { useEffect } from 'react'
|
||||
import { Pressable, StyleSheet } from 'react-native'
|
||||
import Animated, {
|
||||
interpolateColor,
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
withTiming,
|
||||
} from 'react-native-reanimated'
|
||||
|
||||
interface SwitchProps {
|
||||
value: any
|
||||
onChange: (val: any) => void
|
||||
trueValue?: any
|
||||
falseValue?: any
|
||||
width?: number
|
||||
height?: number
|
||||
activeColor?: string
|
||||
inactiveColor?: string
|
||||
duration?: number
|
||||
disabled?: boolean // 新增 disabled
|
||||
}
|
||||
|
||||
export default function Switch({
|
||||
value,
|
||||
onChange,
|
||||
trueValue,
|
||||
falseValue,
|
||||
width = 52,
|
||||
height = 24,
|
||||
activeColor = '#333',
|
||||
inactiveColor = '#e5e7eb',
|
||||
duration = 200,
|
||||
disabled = false, // 默认可用
|
||||
}: SwitchProps) {
|
||||
// 默认值 fallback
|
||||
const trueVal = trueValue ?? true
|
||||
const falseVal = falseValue ?? false
|
||||
|
||||
// 当前状态是否为激活
|
||||
const isActive = Object.is(value, trueVal)
|
||||
|
||||
// 控制动画的 sharedValue
|
||||
const progress = useSharedValue(isActive ? 1 : 0)
|
||||
|
||||
useEffect(() => {
|
||||
progress.value = withTiming(isActive ? 1 : 0, { duration })
|
||||
}, [isActive, duration])
|
||||
|
||||
const knobSize = height - 4
|
||||
const translateX = width - knobSize - 4
|
||||
|
||||
// 动画轨道样式
|
||||
const trackStyle = useAnimatedStyle(() => {
|
||||
const baseColor = interpolateColor(
|
||||
progress.value,
|
||||
[0, 1],
|
||||
[inactiveColor, activeColor],
|
||||
)
|
||||
return {
|
||||
backgroundColor: baseColor,
|
||||
opacity: disabled ? 0.7 : 1, // disabled 半透明
|
||||
}
|
||||
})
|
||||
|
||||
// 动画滑块样式
|
||||
const knobStyle = useAnimatedStyle(() => {
|
||||
return {
|
||||
transform: [
|
||||
{
|
||||
translateX: progress.value * translateX,
|
||||
},
|
||||
],
|
||||
opacity: disabled ? 0.8 : 1, // knob 半透明
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
if (disabled)
|
||||
return
|
||||
const nextValue = isActive ? falseVal : trueVal
|
||||
onChange(nextValue)
|
||||
}}
|
||||
style={{ width, height }}
|
||||
>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.track,
|
||||
{
|
||||
width,
|
||||
height,
|
||||
borderRadius: height / 2,
|
||||
},
|
||||
trackStyle,
|
||||
]}
|
||||
>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.knob,
|
||||
{
|
||||
width: knobSize,
|
||||
height: knobSize,
|
||||
borderRadius: knobSize / 2,
|
||||
},
|
||||
knobStyle,
|
||||
]}
|
||||
/>
|
||||
</Animated.View>
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
track: {
|
||||
justifyContent: 'center',
|
||||
padding: 2,
|
||||
},
|
||||
knob: {
|
||||
backgroundColor: '#fff',
|
||||
elevation: 2,
|
||||
shadowColor: '#000',
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 2,
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
},
|
||||
})
|
||||
@@ -15,6 +15,7 @@ import ListItem from './ListItem'
|
||||
import LucideIcon from './LucideIcon'
|
||||
import Modal from './Modal'
|
||||
import SegmentedTabs from './SegmentedTabs'
|
||||
import Switch from './Switch'
|
||||
import Tab from './Tab'
|
||||
|
||||
export {
|
||||
@@ -35,6 +36,7 @@ export {
|
||||
ListItem as AListItem,
|
||||
Modal as AModal,
|
||||
SegmentedTabs as ASegmentedTabs,
|
||||
Switch as ASwitch,
|
||||
Tab as ATab,
|
||||
LucideIcon,
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import { Switch as RNSwitch } from 'react-native';
|
||||
import { createSwitch } from '@gluestack-ui/core/switch/creator';
|
||||
import { tva } from '@gluestack-ui/utils/nativewind-utils';
|
||||
import { withStyleContext } from '@gluestack-ui/utils/nativewind-utils';
|
||||
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils';
|
||||
|
||||
const UISwitch = createSwitch({
|
||||
Root: withStyleContext(RNSwitch),
|
||||
});
|
||||
|
||||
const switchStyle = tva({
|
||||
base: 'data-[focus=true]:outline-0 data-[focus=true]:ring-2 data-[focus=true]:ring-indicator-primary web:cursor-pointer disabled:cursor-not-allowed data-[disabled=true]:opacity-40 data-[invalid=true]:border-error-700 data-[invalid=true]:rounded-xl data-[invalid=true]:border-2',
|
||||
|
||||
variants: {
|
||||
size: {
|
||||
sm: 'scale-75',
|
||||
md: '',
|
||||
lg: 'scale-125',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
type ISwitchProps = React.ComponentProps<typeof UISwitch> &
|
||||
VariantProps<typeof switchStyle>;
|
||||
const Switch = React.forwardRef<
|
||||
React.ComponentRef<typeof UISwitch>,
|
||||
ISwitchProps
|
||||
>(function Switch({ className, size = 'md', ...props }, ref) {
|
||||
return (
|
||||
<UISwitch
|
||||
ref={ref}
|
||||
{...props}
|
||||
className={switchStyle({ size, class: className })}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
Switch.displayName = 'Switch';
|
||||
export { Switch };
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
DissolveSpaceResult,
|
||||
ExitSpaceParams,
|
||||
ExitSpaceResult,
|
||||
GetSpaceSettingsParams,
|
||||
JoinSpaceParams,
|
||||
JoinSpaceResult,
|
||||
ListSpaceMembersParams,
|
||||
@@ -12,9 +13,13 @@ import type {
|
||||
ListUserSpacesResult,
|
||||
RemoveSpaceMemberParams,
|
||||
RemoveSpaceMemberResult,
|
||||
UpdateSpaceDefaultParams,
|
||||
UpdateSpaceMemberNameParams,
|
||||
UpdateSpaceMemberRoleParams,
|
||||
UpdateSpaceMemberRoleResult,
|
||||
UpdateSpaceNameParams,
|
||||
UpdateSpaceSortNumberParams,
|
||||
UserSpace,
|
||||
} from '@/types/space'
|
||||
import alovaInstance from '../../index'
|
||||
|
||||
@@ -35,6 +40,26 @@ export function updateSpaceNameApi(params: UpdateSpaceNameParams) {
|
||||
return alovaInstance.Post<ResponseData<void>>(`${API_PREFIX}/name`, params)
|
||||
}
|
||||
|
||||
// 修改空间昵称
|
||||
export function updateSpaceMemberNameApi(params: UpdateSpaceMemberNameParams) {
|
||||
return alovaInstance.Post<ResponseData<void>>(`${API_PREFIX}/member/name`, params)
|
||||
}
|
||||
|
||||
// 修改空间是否默认
|
||||
export function updateSpaceDefaultApi(params: UpdateSpaceDefaultParams) {
|
||||
return alovaInstance.Post<ResponseData<void>>(`${API_PREFIX}/member/default`, params)
|
||||
}
|
||||
|
||||
// 修改空间排列顺序
|
||||
export function updateSpaceSortNumberApi(params: UpdateSpaceSortNumberParams) {
|
||||
return alovaInstance.Post<ResponseData<void>>(`${API_PREFIX}/member/sortNumber`, params)
|
||||
}
|
||||
|
||||
// 获取空间设置信息
|
||||
export function getSpaceSettingsApi(params: GetSpaceSettingsParams) {
|
||||
return alovaInstance.Post<ResponseData<UserSpace>>(`${API_PREFIX}/settings`, params)
|
||||
}
|
||||
|
||||
// Join Space API
|
||||
export function joinSpaceApi(params: JoinSpaceParams) {
|
||||
return alovaInstance.Post<ResponseData<JoinSpaceResult>>(`${API_PREFIX}/join`, params)
|
||||
|
||||
@@ -28,8 +28,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,
|
||||
|
||||
@@ -34,11 +34,35 @@ export interface DissolveSpaceParams {
|
||||
export interface DissolveSpaceResult {
|
||||
}
|
||||
|
||||
// 更新名称
|
||||
// 更新空间名称
|
||||
export interface UpdateSpaceNameParams {
|
||||
space_id: number
|
||||
space_name: string
|
||||
}
|
||||
}
|
||||
|
||||
// 更新空间昵称
|
||||
export interface UpdateSpaceMemberNameParams {
|
||||
space_id: number
|
||||
member_name: string
|
||||
}
|
||||
|
||||
// 更新空间是否默认
|
||||
export interface UpdateSpaceDefaultParams {
|
||||
space_id: number
|
||||
is_default: number
|
||||
}
|
||||
|
||||
// 更新空间排列顺序
|
||||
export interface UpdateSpaceSortNumberParams {
|
||||
spaces: {
|
||||
space_id: number
|
||||
sort_number: number
|
||||
}[]
|
||||
}
|
||||
// 获取空间设置信息
|
||||
export interface GetSpaceSettingsParams {
|
||||
space_id: number
|
||||
}
|
||||
|
||||
// 加入空间
|
||||
export interface JoinSpaceParams {
|
||||
|
||||
+108
-69
@@ -1,82 +1,121 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>空间状态演示</title>
|
||||
<style>
|
||||
body{margin:0;font-family:-apple-system;background:#fff;padding:20px;}
|
||||
.space{
|
||||
background:#f8f8f8;
|
||||
border-radius:12px;
|
||||
padding:16px;
|
||||
margin-bottom:12px;
|
||||
display:flex;
|
||||
justify-content:space-between;
|
||||
align-items:center;
|
||||
border:2px solid transparent;
|
||||
}
|
||||
.space.selected{
|
||||
border:2px solid #111;
|
||||
}
|
||||
.name{
|
||||
font-weight:600;
|
||||
}
|
||||
.meta{font-size:12px;color:#666;margin-top:4px;}
|
||||
.tag{
|
||||
font-size:10px;
|
||||
padding:2px 6px;
|
||||
border-radius:6px;
|
||||
margin-left:6px;
|
||||
}
|
||||
.tag-default{
|
||||
background:#e5e7eb;
|
||||
color:#555;
|
||||
}
|
||||
.tag-current{
|
||||
background:#111;
|
||||
color:#fff;
|
||||
}
|
||||
.more{font-size:18px;color:#999;}
|
||||
</style>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Space Card</title>
|
||||
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #f4f6f8;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.space-card {
|
||||
width: 380px;
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
padding: 18px;
|
||||
border: 1px solid #eee;
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.04);
|
||||
transition: all 0.25s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.space-card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 14px 28px rgba(0, 0, 0, 0.08);
|
||||
border-color: #ddd;
|
||||
}
|
||||
|
||||
.space-card.selected {
|
||||
border: 1px solid #111;
|
||||
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.space-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.space-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.space-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #111;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: 11px;
|
||||
padding: 3px 8px;
|
||||
border-radius: 999px;
|
||||
background: #f0f2f5;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.badge.primary {
|
||||
background: #e8f0ff;
|
||||
color: #3b6cff;
|
||||
}
|
||||
|
||||
.space-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<!-- 当前正在使用 -->
|
||||
<div class="space">
|
||||
<div>
|
||||
<div class="name">
|
||||
工作空间
|
||||
<span class="tag tag-current">当前</span>
|
||||
<span class="tag tag-default">默认</span>
|
||||
</div>
|
||||
<div class="meta">张三 · 5人</div>
|
||||
</div>
|
||||
<div class="more">⋯</div>
|
||||
</div>
|
||||
<div class="space-card selected">
|
||||
<div class="space-header">
|
||||
<div class="space-avatar">工</div>
|
||||
|
||||
<!-- 只是默认 -->
|
||||
<div class="space">
|
||||
<div>
|
||||
<div class="name">
|
||||
家庭账本
|
||||
<span class="tag tag-default">默认</span>
|
||||
<div>
|
||||
<div style="display:flex; align-items:center; gap:6px;">
|
||||
<div class="space-name">
|
||||
我的超级长长长长长长工作空间
|
||||
</div>
|
||||
<span class="badge">创建者</span>
|
||||
<span class="badge primary">默认</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="meta">张三 · 3人</div>
|
||||
</div>
|
||||
<div class="more">⋯</div>
|
||||
</div>
|
||||
|
||||
<!-- 被选中但未进入 -->
|
||||
<div class="space selected">
|
||||
<div>
|
||||
<div class="name">
|
||||
副业空间
|
||||
<div class="space-info">
|
||||
<div>昵称:张三</div>
|
||||
<div>成员:8人</div>
|
||||
</div>
|
||||
<div class="meta">张三 · 2人</div>
|
||||
</div>
|
||||
<div class="more">⋯</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in new issue
Block a user