u
This commit is contained in:
1 parent
f59d5c4f59
commit
b1348343c9
24 files changed
+1691
-1243
No files matched your search
@@ -20,12 +20,15 @@ func Auth() fiber.Handler {
|
||||
return httpx.Unauthorized(c, "未登录")
|
||||
}
|
||||
|
||||
verifyToken, ok := jwtx.VerifyToken(c.Context(), token)
|
||||
_, ok := jwtx.VerifyToken(c.Context(), token)
|
||||
if !ok {
|
||||
return httpx.Unauthorized(c, "登录已过期")
|
||||
}
|
||||
|
||||
ctx := context.WithValue(c.Context(), CtxUserIDKey, verifyToken.Data.UserID)
|
||||
//ctx := context.WithValue(c.Context(), CtxUserIDKey, verifyToken.Data.UserID)
|
||||
|
||||
ctx := context.WithValue(c.Context(), CtxUserIDKey, int64(796541901389893))
|
||||
|
||||
c.SetContext(ctx)
|
||||
|
||||
return c.Next()
|
||||
|
||||
@@ -7,18 +7,28 @@ import (
|
||||
)
|
||||
|
||||
func (c *Client) Delete(ctx context.Context, table, keyField string, data map[string]any) error {
|
||||
keyVal, err := validateData(table, keyField, data)
|
||||
_, err := validateData(table, keyField, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
where := make([]string, 0, len(data))
|
||||
args := make([]any, 0, len(data))
|
||||
i := 1
|
||||
|
||||
for k, v := range data {
|
||||
where = append(where, fmt.Sprintf("%s = $%d", quoteCol(k), i))
|
||||
args = append(args, v)
|
||||
i++
|
||||
}
|
||||
|
||||
sql := fmt.Sprintf(
|
||||
"DELETE FROM %s WHERE %s=$1",
|
||||
"DELETE FROM %s WHERE %s",
|
||||
quoteTable(table),
|
||||
quoteCol(keyField),
|
||||
strings.Join(where, " AND "),
|
||||
)
|
||||
|
||||
_, err = c.exec().Exec(ctx, sql, keyVal)
|
||||
_, err = c.exec().Exec(ctx, sql, args...)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -84,6 +84,7 @@ func VerifyToken(ctx context.Context, tokenString string) (*CustomClaims, bool)
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(*CustomClaims)
|
||||
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
@@ -22,9 +22,11 @@
|
||||
"@react-navigation/native": "^7.1.33",
|
||||
"alova": "^3.5.1",
|
||||
"axios": "^1.15.0",
|
||||
"dayjs": "^1.11.20",
|
||||
"expo": "~55.0.15",
|
||||
"expo-blur": "~55.0.14",
|
||||
"expo-build-properties": "~55.0.13",
|
||||
"expo-clipboard": "~55.0.13",
|
||||
"expo-constants": "~55.0.14",
|
||||
"expo-device": "~55.0.15",
|
||||
"expo-document-picker": "~55.0.13",
|
||||
|
||||
Generated
+57
-1
@@ -35,6 +35,9 @@ importers:
|
||||
axios:
|
||||
specifier: ^1.15.0
|
||||
version: 1.15.0
|
||||
dayjs:
|
||||
specifier: ^1.11.20
|
||||
version: 1.11.20
|
||||
expo:
|
||||
specifier: ~55.0.15
|
||||
version: 55.0.15(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.12)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.2(@babel/core@7.29.0)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
@@ -44,6 +47,9 @@ importers:
|
||||
expo-build-properties:
|
||||
specifier: ~55.0.13
|
||||
version: 55.0.13(expo@55.0.15)
|
||||
expo-clipboard:
|
||||
specifier: ~55.0.13
|
||||
version: 55.0.13(expo@55.0.15)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
|
||||
expo-constants:
|
||||
specifier: ~55.0.14
|
||||
version: 55.0.14(expo@55.0.15)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(typescript@5.9.3)
|
||||
@@ -1016,48 +1022,56 @@ packages:
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-arm64-musl@0.44.0':
|
||||
resolution: {integrity: sha512-AlObQIXyVRZ96LbtVljtFq0JqH5B92NU+BQeDFrXWBUWlCKAM0wF5GLfIhCLT5kQ3Sl+U0YjRJ7Alqj5hGQaCg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxfmt/binding-linux-ppc64-gnu@0.44.0':
|
||||
resolution: {integrity: sha512-YcFE8/q/BbrCiIiM5piwbkA6GwJc5QqhMQp2yDrqQ2fuVkZ7CInb1aIijZ/k8EXc72qXMSwKpVlBv1w/MsGO/A==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-riscv64-gnu@0.44.0':
|
||||
resolution: {integrity: sha512-eOdzs6RqkRzuqNHUX5C8ISN5xfGh4xDww8OEd9YAmc3OWN8oAe5bmlIqQ+rrHLpv58/0BuU48bxkhnIGjA/ATQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-riscv64-musl@0.44.0':
|
||||
resolution: {integrity: sha512-YBgNTxntD/QvlFUfgvh8bEdwOhXiquX8gaofZJAwYa/Xp1S1DQrFVZEeck7GFktr24DztsSp8N8WtWCBwxs0Hw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxfmt/binding-linux-s390x-gnu@0.44.0':
|
||||
resolution: {integrity: sha512-GLIh1R6WHWshl/i4QQDNgj0WtT25aRO4HNUWEoitxiywyRdhTFmFEYT2rXlcl9U6/26vhmOqG5cRlMLG3ocaIA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-x64-gnu@0.44.0':
|
||||
resolution: {integrity: sha512-gZOpgTlOsLcLfAF9qgpTr7FIIFSKnQN3hDf/0JvQ4CIwMY7h+eilNjxq/CorqvYcEOu+LRt1W4ZS7KccEHLOdA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-x64-musl@0.44.0':
|
||||
resolution: {integrity: sha512-1CyS9JTB+pCUFYFI6pkQGGZaT/AY5gnhHVrQQLhFba6idP9AzVYm1xbdWfywoldTYvjxQJV6x4SuduCIfP3W+A==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxfmt/binding-openharmony-arm64@0.44.0':
|
||||
resolution: {integrity: sha512-bmEv70Ak6jLr1xotCbF5TxIKjsmQaiX+jFRtnGtfA03tJPf6VG3cKh96S21boAt3JZc+Vjx8PYcDuLj39vM2Pw==}
|
||||
@@ -1130,48 +1144,56 @@ packages:
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-arm64-musl@1.60.0':
|
||||
resolution: {integrity: sha512-eDYDXZGhQAXyn6GwtwiX/qcLS0HlOLPJ/+iiIY8RYr+3P8oKBmgKxADLlniL6FtWfE7pPk7IGN9/xvDEvDvFeg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxlint/binding-linux-ppc64-gnu@1.60.0':
|
||||
resolution: {integrity: sha512-nxehly5XYBHUWI9VJX1bqCf9j/B43DaK/aS/T1fcxCpX3PA4Rm9BB54nPD1CKayT8xg6REN1ao+01hSRNgy8OA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-riscv64-gnu@1.60.0':
|
||||
resolution: {integrity: sha512-j1qf/NaUfOWQutjeoooNG1Q0zsK0XGmSu1uDLq3cctquRF3j7t9Hxqf/76ehCc5GEUAanth2W4Fa+XT1RFg/nw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-riscv64-musl@1.60.0':
|
||||
resolution: {integrity: sha512-YELKPRefQ/q/h3RUmeRfPCUhh2wBvgV1RyZ/F9M9u8cDyXsQW2ojv1DeWQTt466yczDITjZnIOg/s05pk7Ve2A==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxlint/binding-linux-s390x-gnu@1.60.0':
|
||||
resolution: {integrity: sha512-JkO3C6Gki7Y6h/MiIkFKvHFOz98/YWvQ4WYbK9DLXACMP2rjULzkeGyAzorJE5S1dzLQGFgeqvN779kSFwoV1g==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-x64-gnu@1.60.0':
|
||||
resolution: {integrity: sha512-XjKHdFVCpZZZSWBCKyyqCq65s2AKXykMXkjLoKYODrD+f5toLhlwsMESscu8FbgnJQ4Y/dpR/zdazsahmgBJIA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-x64-musl@1.60.0':
|
||||
resolution: {integrity: sha512-js29ZWIuPhNWzY8NC7KoffEMEeWG105vbmm+8EOJsC+T/jHBiKIJEUF78+F/IrgEWMMP9N0kRND4Pp75+xAhKg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxlint/binding-openharmony-arm64@1.60.0':
|
||||
resolution: {integrity: sha512-H+PUITKHk04stFpWj3x3Kg08Afp/bcXSBi0EhasR5a0Vw7StXHTzdl655PUI0fB4qdh2Wsu6Dsi+3ACxPoyQnA==}
|
||||
@@ -1590,24 +1612,28 @@ packages:
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-musl@4.2.1':
|
||||
resolution: {integrity: sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==}
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-gnu@4.2.1':
|
||||
resolution: {integrity: sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==}
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-musl@4.2.1':
|
||||
resolution: {integrity: sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==}
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tailwindcss/oxide-wasm32-wasi@4.2.1':
|
||||
resolution: {integrity: sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==}
|
||||
@@ -2049,6 +2075,9 @@ packages:
|
||||
resolution: {integrity: sha512-1+BhOB8ahCn4O0cep0Sh2l9KCOfOdY+BXJnKMHFFzDEouSr/el18QwXEMRlOj9UY5nCeA8UN3a/82rUWRBeyBw==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
dayjs@1.11.20:
|
||||
resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==}
|
||||
|
||||
debug@2.6.9:
|
||||
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
|
||||
peerDependencies:
|
||||
@@ -2231,6 +2260,13 @@ packages:
|
||||
peerDependencies:
|
||||
expo: '*'
|
||||
|
||||
expo-clipboard@55.0.13:
|
||||
resolution: {integrity: sha512-PrOmmuVsGW4bAkNQmGKtxMXj3invsfN+jfIKmQxHwE/dn7ODqwFWviUTa+PMUjP3XZmYCDLyu/i0GLeu7HF9Ew==}
|
||||
peerDependencies:
|
||||
expo: '*'
|
||||
react: '*'
|
||||
react-native: '*'
|
||||
|
||||
expo-constants@55.0.14:
|
||||
resolution: {integrity: sha512-l23QVQCYBPKT5zbxxZdJeuhiunadvWdjcQ9+GC8h+02jCoLmWRk20064nCINnQTP3Hf+uLPteUiwYrJd0e446w==}
|
||||
peerDependencies:
|
||||
@@ -2540,7 +2576,7 @@ packages:
|
||||
|
||||
glob@7.2.3:
|
||||
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
|
||||
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||
deprecated: Glob versions prior to v9 are no longer supported
|
||||
|
||||
gopd@1.2.0:
|
||||
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
|
||||
@@ -2873,72 +2909,84 @@ packages:
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-arm64-gnu@1.31.1:
|
||||
resolution: {integrity: sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-arm64-gnu@1.32.0:
|
||||
resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-arm64-musl@1.30.1:
|
||||
resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-linux-arm64-musl@1.31.1:
|
||||
resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-linux-arm64-musl@1.32.0:
|
||||
resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-linux-x64-gnu@1.30.1:
|
||||
resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-x64-gnu@1.31.1:
|
||||
resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-x64-gnu@1.32.0:
|
||||
resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-x64-musl@1.30.1:
|
||||
resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-linux-x64-musl@1.31.1:
|
||||
resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-linux-x64-musl@1.32.0:
|
||||
resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-win32-arm64-msvc@1.30.1:
|
||||
resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==}
|
||||
@@ -6333,6 +6381,8 @@ snapshots:
|
||||
|
||||
culori@4.0.2: {}
|
||||
|
||||
dayjs@1.11.20: {}
|
||||
|
||||
debug@2.6.9:
|
||||
dependencies:
|
||||
ms: 2.0.0
|
||||
@@ -6471,6 +6521,12 @@ snapshots:
|
||||
resolve-from: 5.0.0
|
||||
semver: 7.7.4
|
||||
|
||||
expo-clipboard@55.0.13(expo@55.0.15)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
|
||||
dependencies:
|
||||
expo: 55.0.15(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.12)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.2(@babel/core@7.29.0)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
react: 19.2.0
|
||||
react-native: 0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0)
|
||||
|
||||
expo-constants@55.0.14(expo@55.0.15)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@expo/config': 55.0.15(typescript@5.9.3)
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import LayoutView from "@/components/layout/view";
|
||||
import { loadDataApi } from "@/request/api";
|
||||
import { View, Text, TouchableOpacity } from "react-native";
|
||||
import { View, Text } from "react-native";
|
||||
import { useFamilyStore } from "@/store";
|
||||
import {
|
||||
HouseHeartIcon,
|
||||
LayoutGridIcon,
|
||||
LayersIcon,
|
||||
} from "lucide-react-native";
|
||||
import { IconAppFinance, IconAppClothes } from "@/components/iconfont";
|
||||
import { PressableFeedback } from "heroui-native";
|
||||
|
||||
export default function Home() {
|
||||
const familyStore = useFamilyStore();
|
||||
const [family, setFamily] = useState<any>({});
|
||||
|
||||
const [duration, setDuration] = useState({
|
||||
days: 0,
|
||||
hours: 0,
|
||||
@@ -14,16 +22,10 @@ export default function Home() {
|
||||
seconds: 0,
|
||||
});
|
||||
|
||||
const mockMembers = [
|
||||
{ name: "张三", online: true },
|
||||
{ name: "李四", online: false },
|
||||
{ name: "王五", online: true },
|
||||
];
|
||||
|
||||
async function loadFamilyData() {
|
||||
const res = await loadDataApi({
|
||||
view_name: "b_family",
|
||||
search_condition: "id = ?",
|
||||
view_name: `b_family`,
|
||||
search_condition: `id = ?`,
|
||||
search_columns: [],
|
||||
order_by: "",
|
||||
args: [familyStore.currentFamily?.family_id],
|
||||
@@ -68,97 +70,63 @@ export default function Home() {
|
||||
return (
|
||||
<LayoutView>
|
||||
<LayoutView.ScrollView>
|
||||
{/* 🌸 顶部情绪背景区 */}
|
||||
<View
|
||||
style={{
|
||||
paddingTop: 60,
|
||||
paddingBottom: 40,
|
||||
alignItems: "center",
|
||||
backgroundColor: "#FFF3F6",
|
||||
borderBottomLeftRadius: 30,
|
||||
borderBottomRightRadius: 30,
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontSize: 22, fontWeight: "600", color: "#333" }}>
|
||||
{family.name || "我们的家庭"}
|
||||
</Text>
|
||||
<View className="items-center justify-center">
|
||||
<Text className="text-4xl font-bold tracking-tight text-gray-900">{family.name}</Text>
|
||||
|
||||
<Text style={{ fontSize: 12, color: "#999", marginTop: 6 }}>在一起的第</Text>
|
||||
<View className="items-center mt-3">
|
||||
<View className="flex-row items-center gap-2 mb-1">
|
||||
<HouseHeartIcon size={18} color="#6a7282" />
|
||||
|
||||
{/* 💖 超大核心数字(恋爱记风格) */}
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 56,
|
||||
fontWeight: "700",
|
||||
color: "#FF5C8A",
|
||||
marginTop: 8,
|
||||
}}
|
||||
>
|
||||
{duration.days}
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-500">家庭已陪伴你</Text>
|
||||
</View>
|
||||
|
||||
<Text style={{ fontSize: 14, color: "#999" }}>天</Text>
|
||||
</View>
|
||||
<View className="flex-row items-center gap-2">
|
||||
<Text className="text-3xl font-bold text-accent">{duration.days}</Text>
|
||||
<Text className="text-gray-400">天</Text>
|
||||
|
||||
{/* ⏱ 小时间补充 */}
|
||||
<View style={{ flexDirection: "row", justifyContent: "center", marginTop: 20 }}>
|
||||
<Text style={{ fontSize: 14, color: "#666" }}>
|
||||
{duration.hours}时 {duration.minutes}分 {duration.seconds}秒
|
||||
</Text>
|
||||
</View>
|
||||
<Text className="text-3xl font-bold text-accent">{duration.hours}</Text>
|
||||
<Text className="text-gray-400">时</Text>
|
||||
|
||||
{/* 💕 情侣/家庭状态区 */}
|
||||
<View
|
||||
style={{
|
||||
marginTop: 30,
|
||||
marginHorizontal: 20,
|
||||
backgroundColor: "#fff",
|
||||
borderRadius: 20,
|
||||
padding: 16,
|
||||
shadowColor: "#000",
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 10,
|
||||
elevation: 2,
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontSize: 12, color: "#999", marginBottom: 10 }}>我们的状态</Text>
|
||||
<Text className="text-3xl font-bold text-accent">{duration.minutes}</Text>
|
||||
<Text className="text-gray-400">分</Text>
|
||||
|
||||
<View style={{ flexDirection: "row", justifyContent: "space-between" }}>
|
||||
<Text>家庭成员</Text>
|
||||
<Text style={{ fontWeight: "600" }}>{mockMembers.length} 人</Text>
|
||||
</View>
|
||||
|
||||
<View style={{ flexDirection: "row", justifyContent: "space-between", marginTop: 8 }}>
|
||||
<Text>在线人数</Text>
|
||||
<Text style={{ fontWeight: "600", color: "#FF5C8A" }}>
|
||||
{mockMembers.filter((m) => m.online).length} 人
|
||||
</Text>
|
||||
<Text className="text-3xl font-bold text-accent">{duration.seconds}</Text>
|
||||
<Text className="text-gray-400">秒</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 💌 邀请区(恋爱记风格按钮) */}
|
||||
<View style={{ paddingHorizontal: 20, marginTop: 20 }}>
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
backgroundColor: "#FF5C8A",
|
||||
padding: 14,
|
||||
borderRadius: 30,
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: "#fff", fontWeight: "600" }}>邀请家人加入</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View className="mt-6">
|
||||
<View className="flex-row items-center mb-2 gap-2">
|
||||
<View className="text-gray-500">
|
||||
<LayoutGridIcon size={18} color="#6B7280" />
|
||||
</View>
|
||||
<Text className="text-sm text-muted">快捷应用</Text>
|
||||
</View>
|
||||
|
||||
{/* 🔑 邀请码(弱化但温柔) */}
|
||||
<View style={{ alignItems: "center", marginTop: 20 }}>
|
||||
<Text style={{ fontSize: 12, color: "#999" }}>邀请码</Text>
|
||||
<Text style={{ fontSize: 18, fontFamily: "monospace", marginTop: 4 }}>AB12CD</Text>
|
||||
</View>
|
||||
<View className="flex-row flex-wrap gap-3">
|
||||
<PressableFeedback className="flex-1 items-center justify-center h-20 bg-white rounded-xl shadow-lg">
|
||||
<IconAppFinance size={26} />
|
||||
|
||||
{/* 🕊 底部情绪提示 */}
|
||||
<View style={{ alignItems: "center", marginTop: 30, marginBottom: 40 }}>
|
||||
<Text style={{ fontSize: 12, color: "#ccc" }}>💖 一起走过的每一天都值得记录</Text>
|
||||
<Text className="text-xs text-gray-700 mt-2 font-medium">记账</Text>
|
||||
|
||||
<PressableFeedback.Ripple />
|
||||
</PressableFeedback>
|
||||
|
||||
<PressableFeedback className="flex-1 items-center justify-center h-20 bg-white rounded-xl shadow-lg">
|
||||
<IconAppClothes size={26} />
|
||||
|
||||
<Text className="text-xs text-gray-700 mt-2 font-medium">衣服搭配</Text>
|
||||
<PressableFeedback.Ripple />
|
||||
</PressableFeedback>
|
||||
|
||||
<PressableFeedback className="flex-1 items-center justify-center h-20 bg-white rounded-xl shadow-lg">
|
||||
<LayersIcon size={26} />
|
||||
|
||||
<Text className="text-xs text-gray-700 mt-2 font-medium">更多</Text>
|
||||
<PressableFeedback.Ripple />
|
||||
</PressableFeedback>
|
||||
</View>
|
||||
</View>
|
||||
</LayoutView.ScrollView>
|
||||
</LayoutView>
|
||||
|
||||
@@ -46,9 +46,13 @@ export default function My() {
|
||||
</View>
|
||||
<View className="justify-center mt-4">
|
||||
<Text className="text-sm text-muted mb-2 ml-2">账户</Text>
|
||||
<ListGroup className="mb-6">
|
||||
<PressableFeedback>
|
||||
<ListGroup.Item onPress={() => {}}>
|
||||
<ListGroup className="mb-4">
|
||||
<PressableFeedback
|
||||
onPress={() => {
|
||||
router.navigate("/family/user");
|
||||
}}
|
||||
>
|
||||
<ListGroup.Item>
|
||||
<ListGroup.ItemPrefix>
|
||||
<StyledIonicons name="person-outline" size={22} className="text-foreground" />
|
||||
</ListGroup.ItemPrefix>
|
||||
@@ -63,6 +67,31 @@ export default function My() {
|
||||
<PressableFeedback.Ripple />
|
||||
</PressableFeedback>
|
||||
</ListGroup>
|
||||
|
||||
<Text className="text-sm text-muted mb-2 ml-2">家庭</Text>
|
||||
|
||||
<ListGroup className="mb-4">
|
||||
<PressableFeedback
|
||||
onPress={() => {
|
||||
router.navigate("/family/settings");
|
||||
}}
|
||||
>
|
||||
<ListGroup.Item>
|
||||
<ListGroup.ItemPrefix>
|
||||
<StyledIonicons name="home-outline" size={22} className="text-foreground" />
|
||||
</ListGroup.ItemPrefix>
|
||||
|
||||
<ListGroup.ItemContent>
|
||||
<ListGroup.ItemTitle>家庭设置</ListGroup.ItemTitle>
|
||||
</ListGroup.ItemContent>
|
||||
|
||||
<ListGroup.ItemSuffix />
|
||||
</ListGroup.Item>
|
||||
|
||||
<PressableFeedback.Ripple />
|
||||
</PressableFeedback>
|
||||
</ListGroup>
|
||||
|
||||
<ListGroup>
|
||||
<PressableFeedback
|
||||
onPress={() => {
|
||||
@@ -82,6 +111,7 @@ export default function My() {
|
||||
</PressableFeedback>
|
||||
</ListGroup>
|
||||
</View>
|
||||
|
||||
<Dialog isOpen={openLogout} onOpenChange={setOpenLogout}>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay />
|
||||
|
||||
@@ -1,606 +0,0 @@
|
||||
import React, { useEffect, useState, useRef } from "react";
|
||||
import LayoutView from "@/components/layout/view";
|
||||
import { View, Text } from "react-native";
|
||||
import {
|
||||
Button,
|
||||
Separator,
|
||||
Spinner,
|
||||
Chip,
|
||||
BottomSheet,
|
||||
Label,
|
||||
TextField,
|
||||
Input,
|
||||
InputOTP,
|
||||
type InputOTPRef,
|
||||
PressableFeedback,
|
||||
useBottomSheetAwareHandlers,
|
||||
} from "heroui-native";
|
||||
import { PlusIcon, HousePlusIcon, GripIcon } from "lucide-react-native";
|
||||
import { useUserStore } from "@/store";
|
||||
import { getUniqueIdApi, loadDataApi, loadDataBySqlApi, saveDataApi } from "@/request/api";
|
||||
import EmptyState from "@/components/EmptyStatus";
|
||||
import Sortable from "react-native-sortables";
|
||||
import { useSpaceStore } from "@/store";
|
||||
import { useRouter } from "expo-router";
|
||||
import { UserRoundIcon, UsersRoundIcon } from "lucide-react-native";
|
||||
import { toast } from "sonner-native";
|
||||
import { FadeIn } from "react-native-reanimated";
|
||||
import { generateCode } from "@/utils/common";
|
||||
import { Keyboard } from "react-native";
|
||||
import { forwardRef } from "react";
|
||||
|
||||
const BottomSheetTextInput = forwardRef<any, React.ComponentProps<typeof Input>>((props, ref) => {
|
||||
const { onFocus, onBlur } = useBottomSheetAwareHandlers();
|
||||
|
||||
return (
|
||||
<Input
|
||||
ref={ref}
|
||||
{...props}
|
||||
onFocus={(e) => {
|
||||
onFocus(e);
|
||||
props.onFocus?.(e);
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
onBlur(e);
|
||||
props.onBlur?.(e);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const BottomSheetTextInputOTP = forwardRef<any, React.ComponentProps<typeof InputOTP>>(
|
||||
(props, ref) => {
|
||||
const { onFocus, onBlur } = useBottomSheetAwareHandlers();
|
||||
|
||||
return (
|
||||
<InputOTP
|
||||
ref={ref}
|
||||
{...props}
|
||||
onFocus={(e) => {
|
||||
onFocus(e);
|
||||
props.onFocus?.(e);
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
onBlur(e);
|
||||
props.onBlur?.(e);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default function Space() {
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
const spaceStore = useSpaceStore();
|
||||
|
||||
const ITEM_HEIGHT = 64;
|
||||
|
||||
const [spaces, setSpaces] = useState<any[]>([]);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// 加入
|
||||
const [showJoin, setShowJoin] = useState(false);
|
||||
const [joinLoading, setJoinLoading] = useState(false);
|
||||
const [joinForm, setJoinForm] = useState({
|
||||
invite_code: "",
|
||||
});
|
||||
const joinInputRef = useRef<InputOTPRef>(null);
|
||||
|
||||
// 新建
|
||||
const [showCreate, setShowCreate] = useState(false);
|
||||
const [createForm, setCreateForm] = useState({
|
||||
name: "",
|
||||
});
|
||||
const [createLoading, setCreateLoading] = useState(false);
|
||||
const createInputRef = useRef<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
listUserSpaces();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (showCreate) {
|
||||
requestAnimationFrame(() => {
|
||||
createInputRef.current?.focus();
|
||||
});
|
||||
} else {
|
||||
setCreateForm({
|
||||
name: "",
|
||||
});
|
||||
setCreateLoading(false);
|
||||
Keyboard.dismiss();
|
||||
}
|
||||
}, [showCreate]);
|
||||
|
||||
useEffect(() => {
|
||||
if (showJoin) {
|
||||
requestAnimationFrame(() => {
|
||||
joinInputRef.current?.focus();
|
||||
});
|
||||
} else {
|
||||
setJoinForm({
|
||||
invite_code: "",
|
||||
});
|
||||
setJoinLoading(false);
|
||||
Keyboard.dismiss();
|
||||
}
|
||||
}, [showJoin]);
|
||||
|
||||
function listUserSpaces(loading = true) {
|
||||
if (loading) {
|
||||
setLoading(true);
|
||||
}
|
||||
|
||||
loadDataApi({
|
||||
view_name: "v_user_spaces",
|
||||
search_condition: `v_user_id = ?`,
|
||||
search_columns: [],
|
||||
order_by: "",
|
||||
args: [userStore.userInfo.id],
|
||||
}).then((res) => {
|
||||
if (res.isSuccess) {
|
||||
setSpaces(res.data);
|
||||
if (loading) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function handleCreate() {
|
||||
if (createLoading) return;
|
||||
|
||||
if (createForm.name === "") {
|
||||
toast.warning("空间名称不能为空");
|
||||
return;
|
||||
}
|
||||
|
||||
setCreateLoading(true);
|
||||
|
||||
try {
|
||||
let inviteCode = "";
|
||||
let isExist = true;
|
||||
|
||||
// 循环生成直到不存在
|
||||
while (isExist) {
|
||||
inviteCode = generateCode(6);
|
||||
|
||||
const res = await loadDataBySqlApi({
|
||||
sql: `SELECT 1 FROM space WHERE invite_code = ? LIMIT 1`,
|
||||
args: [inviteCode],
|
||||
});
|
||||
|
||||
if (!res.isSuccess) {
|
||||
throw new Error("校验邀请码失败");
|
||||
}
|
||||
|
||||
isExist = res.data.length > 0;
|
||||
}
|
||||
|
||||
// 设置是否是默认空间
|
||||
let isDefault = false;
|
||||
|
||||
const defaultRes = await loadDataApi({
|
||||
view_name: "v_user_spaces",
|
||||
search_condition: `v_user_id = ?`,
|
||||
search_columns: ["v_is_default"],
|
||||
order_by: "",
|
||||
args: [userStore.userInfo.id],
|
||||
});
|
||||
|
||||
if (!defaultRes.isSuccess) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (defaultRes.data.length == 0) {
|
||||
isDefault = true;
|
||||
}
|
||||
|
||||
// 创建
|
||||
const autoRes = await getUniqueIdApi(2);
|
||||
if (!autoRes.isSuccess) {
|
||||
return;
|
||||
}
|
||||
|
||||
const spaceId = autoRes.data[0];
|
||||
const spaceMemberId = autoRes.data[1];
|
||||
|
||||
// 插入空间
|
||||
const requestData = [];
|
||||
|
||||
requestData.push({
|
||||
table_name: "space",
|
||||
key_field: "id",
|
||||
inserts: [
|
||||
{
|
||||
id: spaceId,
|
||||
name: createForm.name,
|
||||
invite_code: inviteCode,
|
||||
owner_id: userStore.userInfo.id,
|
||||
status: 0,
|
||||
},
|
||||
],
|
||||
updates: [],
|
||||
deletes: [],
|
||||
});
|
||||
|
||||
// 插入空间用户
|
||||
requestData.push({
|
||||
table_name: "space_member",
|
||||
key_field: "id",
|
||||
inserts: [
|
||||
{
|
||||
id: spaceMemberId,
|
||||
space_id: spaceId,
|
||||
user_id: userStore.userInfo.id,
|
||||
role: 0,
|
||||
is_default: isDefault ? 1 : 0,
|
||||
sort_number: defaultRes.data.length,
|
||||
member_name: userStore.userInfo.nickname,
|
||||
},
|
||||
],
|
||||
updates: [],
|
||||
deletes: [],
|
||||
});
|
||||
|
||||
const saveRes = await saveDataApi(requestData);
|
||||
|
||||
if (saveRes.isSuccess) {
|
||||
toast.success("创建成功");
|
||||
setShowCreate(false);
|
||||
|
||||
setCreateForm({
|
||||
name: "",
|
||||
});
|
||||
|
||||
listUserSpaces();
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
setCreateLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleJoin() {
|
||||
if (joinLoading) return;
|
||||
|
||||
const code = joinForm.invite_code.trim();
|
||||
|
||||
if (!code) {
|
||||
toast.warning("邀请码不能为空");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!/^[A-Za-z0-9]{6}$/.test(code)) {
|
||||
toast.warning("邀请码必须为6位字母或数字");
|
||||
return;
|
||||
}
|
||||
|
||||
setJoinLoading(true);
|
||||
|
||||
try {
|
||||
const res = await loadDataBySqlApi({
|
||||
sql: ` SELECT
|
||||
s.id,
|
||||
s.name,
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM space_member sm
|
||||
WHERE sm.space_id = s.id
|
||||
AND sm.user_id = ?
|
||||
) AS is_joined
|
||||
FROM space s
|
||||
WHERE s.invite_code = ?
|
||||
LIMIT 1;
|
||||
`,
|
||||
args: [userStore.userInfo.id, code],
|
||||
});
|
||||
|
||||
if (!res.isSuccess) return;
|
||||
|
||||
const row = res.data?.[0];
|
||||
|
||||
if (!row) {
|
||||
toast.warning("邀请码无效");
|
||||
return;
|
||||
}
|
||||
|
||||
if (row.is_joined) {
|
||||
toast.warning("你已经在该空间了");
|
||||
return;
|
||||
}
|
||||
|
||||
const autoRes = await getUniqueIdApi(1);
|
||||
if (!autoRes.isSuccess) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置是否是默认空间
|
||||
let isDefault = false;
|
||||
|
||||
const defaultRes = await loadDataApi({
|
||||
view_name: "v_user_spaces",
|
||||
search_condition: `v_user_id = ?`,
|
||||
search_columns: ["v_is_default"],
|
||||
order_by: "",
|
||||
args: [userStore.userInfo.id],
|
||||
});
|
||||
|
||||
if (!defaultRes.isSuccess) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (defaultRes.data.length == 0) {
|
||||
isDefault = true;
|
||||
}
|
||||
|
||||
// 加入
|
||||
const requestData = [];
|
||||
|
||||
requestData.push({
|
||||
table_name: "space_member",
|
||||
key_field: "id",
|
||||
inserts: [
|
||||
{
|
||||
id: autoRes.data[0],
|
||||
space_id: row.id,
|
||||
user_id: userStore.userInfo.id,
|
||||
role: 1,
|
||||
is_default: isDefault ? 1 : 0,
|
||||
sort_number: defaultRes.data.length,
|
||||
member_name: userStore.userInfo.nickname,
|
||||
},
|
||||
],
|
||||
updates: [],
|
||||
deletes: [],
|
||||
});
|
||||
|
||||
const saveRes = await saveDataApi(requestData);
|
||||
|
||||
if (saveRes.isSuccess) {
|
||||
toast.success("加入成功");
|
||||
setShowJoin(false);
|
||||
|
||||
setJoinForm({
|
||||
invite_code: "",
|
||||
});
|
||||
|
||||
listUserSpaces();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toast.error("加入失败,请稍后重试");
|
||||
} finally {
|
||||
setJoinLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
const renderItem = ({ item, index }: { item: any; index: number }) => (
|
||||
<PressableFeedback
|
||||
key={item.space_id}
|
||||
onPress={() => {
|
||||
router.navigate({
|
||||
pathname: "/tab-space" as any,
|
||||
params: {
|
||||
spaceId: item.space_id,
|
||||
},
|
||||
});
|
||||
}}
|
||||
style={{
|
||||
height: ITEM_HEIGHT,
|
||||
}}
|
||||
className={`rounded-xl flex-row items-center px-4 py-3 bg-white ${
|
||||
index === spaces.length - 1 ? "mb-4" : ""
|
||||
}`}
|
||||
>
|
||||
<View className="w-full flex-row items-center justify-between">
|
||||
<View>
|
||||
<View className="flex-row items-center gap-2">
|
||||
<Text className="font-semibold text-gray-900">{item.name}</Text>
|
||||
|
||||
{item.owner_id === userStore.userInfo.id && (
|
||||
<Chip size="sm" variant="soft" color="success">
|
||||
创建者
|
||||
</Chip>
|
||||
)}
|
||||
|
||||
{item.v_is_default === 1 && (
|
||||
<Chip size="sm" variant="secondary" color="default">
|
||||
默认
|
||||
</Chip>
|
||||
)}
|
||||
|
||||
{item.space_id === spaceStore?.currentSpace?.space_id && (
|
||||
<Chip size="sm" variant="primary" color="accent">
|
||||
当前空间
|
||||
</Chip>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View className="flex-row items-center gap-3 mt-3">
|
||||
<View className="flex-row gap-1 items-center">
|
||||
<UserRoundIcon size={12} />
|
||||
<Text className="text-xs">{item.v_member_name}</Text>
|
||||
</View>
|
||||
<View className="flex-row gap-1 items-center">
|
||||
<UsersRoundIcon size={12} />
|
||||
<Text className={`text-xs `}>{item.v_member_count}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Sortable.Handle>
|
||||
<GripIcon size={20} />
|
||||
</Sortable.Handle>
|
||||
</View>
|
||||
<PressableFeedback.Ripple />
|
||||
</PressableFeedback>
|
||||
);
|
||||
|
||||
return (
|
||||
<LayoutView>
|
||||
<LayoutView.ScrollView>
|
||||
<View className="flex-row gap-4">
|
||||
<Button
|
||||
className="flex-1"
|
||||
onPress={() => {
|
||||
setShowCreate(true);
|
||||
// createInputRef.current?.focus();
|
||||
}}
|
||||
>
|
||||
<PlusIcon color="#fff" size={20} />
|
||||
<Button.Label>创建新空间</Button.Label>
|
||||
</Button>
|
||||
<Button
|
||||
className="flex-1"
|
||||
variant="outline"
|
||||
onPress={() => {
|
||||
setShowJoin(true);
|
||||
}}
|
||||
>
|
||||
<HousePlusIcon size={20} />
|
||||
<Button.Label>加入其它空间</Button.Label>
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
<View className="flex-row my-3 items-center gap-3">
|
||||
<Separator className="flex-1" />
|
||||
<Text className="text-xs text-gray-400">我的空间</Text>
|
||||
<Separator className="flex-1" />
|
||||
</View>
|
||||
|
||||
{loading ? (
|
||||
<View className="flex-1 items-center justify-center">
|
||||
<Spinner size="lg" />
|
||||
</View>
|
||||
) : spaces.length === 0 ? (
|
||||
<EmptyState />
|
||||
) : (
|
||||
<Sortable.Grid
|
||||
keyExtractor={(item: any) => {
|
||||
return String(item.space_id);
|
||||
}}
|
||||
activeItemScale={1}
|
||||
columns={1}
|
||||
data={spaces}
|
||||
overDrag="vertical"
|
||||
renderItem={renderItem}
|
||||
rowGap={10}
|
||||
highlight-next-line
|
||||
customHandle
|
||||
autoScrollActivationOffset={200}
|
||||
onDragEnd={(dragData: any) => {
|
||||
const list = dragData.data;
|
||||
|
||||
const updates = list.map((item: any, index: number) => ({
|
||||
id: item.v_space_member_id,
|
||||
sort_number: index,
|
||||
}));
|
||||
|
||||
saveDataApi([
|
||||
{
|
||||
table_name: "space_member",
|
||||
key_field: "id",
|
||||
inserts: [],
|
||||
updates,
|
||||
deletes: [],
|
||||
},
|
||||
]).then((_res) => {
|
||||
// console.log(res);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</LayoutView.ScrollView>
|
||||
<BottomSheet
|
||||
isOpen={showCreate}
|
||||
onOpenChange={(val) => {
|
||||
setShowCreate(val);
|
||||
}}
|
||||
>
|
||||
<BottomSheet.Portal>
|
||||
<BottomSheet.Overlay />
|
||||
<BottomSheet.Content
|
||||
animation="disabled"
|
||||
enableBlurKeyboardOnGesture
|
||||
keyboardBlurBehavior="restore"
|
||||
>
|
||||
<View>
|
||||
<TextField>
|
||||
<Label>空间名称</Label>
|
||||
<BottomSheetTextInput
|
||||
ref={createInputRef}
|
||||
placeholder="请输入空间名称"
|
||||
value={createForm.name}
|
||||
onChangeText={(val) =>
|
||||
setCreateForm({
|
||||
name: val,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</TextField>
|
||||
|
||||
<Button className="mt-3" onPress={handleCreate}>
|
||||
{createLoading && <Spinner entering={FadeIn.delay(50)} color="white" />}
|
||||
<Button.Label>创建</Button.Label>
|
||||
</Button>
|
||||
</View>
|
||||
</BottomSheet.Content>
|
||||
</BottomSheet.Portal>
|
||||
</BottomSheet>
|
||||
|
||||
<BottomSheet
|
||||
isOpen={showJoin}
|
||||
onOpenChange={(val) => {
|
||||
setShowJoin(val);
|
||||
}}
|
||||
>
|
||||
<BottomSheet.Portal>
|
||||
<BottomSheet.Overlay />
|
||||
|
||||
<BottomSheet.Content
|
||||
animation={"disabled"}
|
||||
enableBlurKeyboardOnGesture
|
||||
keyboardBlurBehavior="restore"
|
||||
>
|
||||
<View>
|
||||
<TextField>
|
||||
<Label>邀请码</Label>
|
||||
<BottomSheetTextInputOTP
|
||||
ref={joinInputRef}
|
||||
maxLength={6}
|
||||
value={joinForm.invite_code}
|
||||
onChange={(val) => {
|
||||
setJoinForm({
|
||||
invite_code: val,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<InputOTP.Group>
|
||||
<InputOTP.Slot index={0} />
|
||||
<InputOTP.Slot index={1} />
|
||||
<InputOTP.Slot index={2} />
|
||||
</InputOTP.Group>
|
||||
<InputOTP.Separator />
|
||||
<InputOTP.Group>
|
||||
<InputOTP.Slot index={3} />
|
||||
<InputOTP.Slot index={4} />
|
||||
<InputOTP.Slot index={5} />
|
||||
</InputOTP.Group>
|
||||
</BottomSheetTextInputOTP>
|
||||
</TextField>
|
||||
|
||||
<Button className="mt-3" onPress={handleJoin}>
|
||||
{joinLoading && <Spinner entering={FadeIn.delay(50)} color="white" />}
|
||||
<Button.Label>加入</Button.Label>
|
||||
</Button>
|
||||
</View>
|
||||
</BottomSheet.Content>
|
||||
</BottomSheet.Portal>
|
||||
</BottomSheet>
|
||||
</LayoutView>
|
||||
);
|
||||
}
|
||||
@@ -1,17 +1,11 @@
|
||||
import { useEffect } from "react";
|
||||
import { Stack } from "expo-router";
|
||||
import { Image, Text, View } from "react-native";
|
||||
import { Button, LinkButton, ControlField, Checkbox, Spinner } from "heroui-native";
|
||||
import { IconQq, IconWeixin } from "@/components/iconfont";
|
||||
import { IconQq } from "@/components/iconfont";
|
||||
import { useState } from "react";
|
||||
import { FadeIn } from "react-native-reanimated";
|
||||
import { useUserStore, useFamilyStore } from "@/store";
|
||||
import { useRouter } from "expo-router";
|
||||
import ExpoQQ from "expo-qq";
|
||||
import ExpoWeChat from "expo-wechat";
|
||||
import { useEvent } from "expo";
|
||||
import { toast } from "sonner-native";
|
||||
import { loginQqApi, loginWxApi } from "@/request/api";
|
||||
|
||||
export default function Login() {
|
||||
const router = useRouter();
|
||||
@@ -20,189 +14,48 @@ export default function Login() {
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isAgreed, setIsAgreed] = useState(true);
|
||||
const [form, setForm] = useState({
|
||||
type: 0,
|
||||
openid: "",
|
||||
code: "",
|
||||
});
|
||||
|
||||
// 登录
|
||||
const onLoginFinished = useEvent(ExpoQQ, "onLoginFinished");
|
||||
const onGetUserInfo = useEvent(ExpoQQ, "onGetUserInfo");
|
||||
const authResult = useEvent(ExpoWeChat, "onAuthResult");
|
||||
/**
|
||||
* {"code": 1000, "data": {"avatar": "http://thirdqq.qlogo.cn/ek_qqapp/AQHVialeUibE42oV8RvTTVZ4tyRlPYeJrUibWTwtCPfV68E4rSzn9cw00GkXQ3QDTiawwhjoqQb7/100",
|
||||
* "nickname": "Zhang", "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7ImlkIjo3OTY0NTY3MTI3ODU5ODl9LCJpYXQiOjE3NzY1ODU2MDR9.ZwmN5_0MGrP9dYpn3quK94HXMG3QYKmAq7wnP5X68dY", "userId": 796456712785989}, "isSuccess": true, "message": "success", "track_id": "djIRo23SZlA160zBTH2DX-kxeuLApKTNyZdxqqauSMk"}
|
||||
*/
|
||||
|
||||
useEffect(() => {
|
||||
ExpoQQ.init("102826474", "");
|
||||
ExpoWeChat.registerApp("wxdab3e21a1f7e392f", "");
|
||||
}, []);
|
||||
|
||||
// 微信登录
|
||||
async function handleLoginWechat() {
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
|
||||
// 检查微信是否安装
|
||||
const isInstalled = await ExpoWeChat.isWXAppInstalled();
|
||||
if (!isInstalled) {
|
||||
toast.warning(onLoginFinished?.errorMessage || "请先安装微信");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setForm((val) => ({
|
||||
...val,
|
||||
type: 0,
|
||||
}));
|
||||
|
||||
ExpoWeChat.sendAuthRequest("snsapi_userinfo", "state123");
|
||||
}
|
||||
|
||||
// QQ 登录
|
||||
async function handleLoginQQ() {
|
||||
function handleTest() {
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
setForm((val) => ({
|
||||
...val,
|
||||
type: 1,
|
||||
}));
|
||||
await ExpoQQ.login(["get_user_info"]);
|
||||
}
|
||||
setTimeout(() => {
|
||||
requestAnimationFrame(() => {
|
||||
userStore.setUserInfo({
|
||||
id: 796541901389893,
|
||||
token:
|
||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7ImlkIjo3OTY0NTY3MTI3ODU5ODl9LCJpYXQiOjE3NzY1ODU2MDR9.ZwmN5_0MGrP9dYpn3quK94HXMG3QYKmAq7wnP5X68dY",
|
||||
nickname: "Zhang",
|
||||
avatar:
|
||||
"http://thirdqq.qlogo.cn/ek_qqapp/AQHVialeUibE42oV8RvTTVZ4tyRlPYeJrUibWTwtCPfV68E4rSzn9cw00GkXQ3QDTiawwhjoqQb7/100",
|
||||
});
|
||||
userStore.setAuthExpired(false);
|
||||
console.log(familyStore.inFamily());
|
||||
|
||||
useEffect(() => {
|
||||
if (!onLoginFinished) {
|
||||
return;
|
||||
}
|
||||
// if (familyStore.inFamily()) {
|
||||
// router.replace("/(tabs)/home");
|
||||
// } else {
|
||||
// router.replace("/family/entry");
|
||||
// return;
|
||||
// }
|
||||
|
||||
if (!onLoginFinished.success) {
|
||||
toast.warning(onLoginFinished?.errorMessage || "授权失败,请检查手机是否安装 QQ 或确认授权");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchUserInfo = async () => {
|
||||
try {
|
||||
const tokenInfo = await ExpoQQ.getLoginTokenInfo();
|
||||
if (!tokenInfo.openId) {
|
||||
toast.warning(tokenInfo?.errorMessage || "授权失败,请检查手机是否安装 QQ 或确认授权");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setForm((val) => ({
|
||||
...val,
|
||||
openid: String(tokenInfo.openId),
|
||||
}));
|
||||
|
||||
await ExpoQQ.sendGetUserInfoReq();
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchUserInfo();
|
||||
}, [onLoginFinished]);
|
||||
|
||||
// QQ登录
|
||||
useEffect(() => {
|
||||
const handleLogin = async () => {
|
||||
if (!onGetUserInfo) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const loginRes = await loginQqApi({
|
||||
openid: form.openid,
|
||||
nickname: onGetUserInfo.nickname || "",
|
||||
avatar: onGetUserInfo.figureurlQq_2 || onGetUserInfo.figureurl_2,
|
||||
familyStore.setCurrentFamily({
|
||||
family_id: 797139042881605,
|
||||
});
|
||||
|
||||
if (loginRes.isSuccess) {
|
||||
requestAnimationFrame(() => {
|
||||
userStore.setUserInfo({
|
||||
token: loginRes.data.token,
|
||||
id: loginRes.data.userId,
|
||||
nickname: loginRes.data.nickname,
|
||||
avatar: loginRes.data.avatar,
|
||||
});
|
||||
|
||||
userStore.setAuthExpired(false);
|
||||
|
||||
if (loginRes.data.family_id === 0) {
|
||||
router.replace("/family/entry");
|
||||
} else {
|
||||
familyStore.setCurrentFamily({
|
||||
family_id: loginRes.data.family_id,
|
||||
});
|
||||
|
||||
router.replace("/(tabs)/home");
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("登录接口出错", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
handleLogin();
|
||||
}, [onGetUserInfo]);
|
||||
|
||||
// 微信登录
|
||||
useEffect(() => {
|
||||
if (!authResult) return;
|
||||
|
||||
const handleLogin = async () => {
|
||||
if (authResult.errorCode === 0) {
|
||||
try {
|
||||
const loginRes = await loginWxApi({
|
||||
code: authResult.code,
|
||||
});
|
||||
|
||||
if (loginRes.isSuccess) {
|
||||
requestAnimationFrame(() => {
|
||||
userStore.setUserInfo({
|
||||
token: loginRes.data.token,
|
||||
id: loginRes.data.userId,
|
||||
nickname: loginRes.data.nickname,
|
||||
avatar: loginRes.data.avatar,
|
||||
});
|
||||
|
||||
userStore.setAuthExpired(false);
|
||||
|
||||
if (loginRes.data.family_id === 0) {
|
||||
router.replace("/family/entry");
|
||||
} else {
|
||||
familyStore.setCurrentFamily({
|
||||
family_id: loginRes.data.family_id,
|
||||
});
|
||||
|
||||
router.replace("/(tabs)/home");
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("登录接口出错", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
} else {
|
||||
console.log("授权失败:", authResult.errorMessage);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
handleLogin();
|
||||
}, [authResult]);
|
||||
router.replace("/(tabs)/home");
|
||||
});
|
||||
setLoading(false);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -222,7 +75,7 @@ export default function Login() {
|
||||
</View>
|
||||
|
||||
<View className="flex flex-col gap-4">
|
||||
<Button onPress={handleLoginWechat}>
|
||||
{/* <Button onPress={handleLoginWechat}>
|
||||
{loading ? (
|
||||
<Spinner entering={FadeIn.delay(50)} color="white" />
|
||||
) : (
|
||||
@@ -234,6 +87,11 @@ export default function Login() {
|
||||
<Button variant="outline" onPress={handleLoginQQ}>
|
||||
{loading ? <Spinner entering={FadeIn.delay(50)} /> : <IconQq />}
|
||||
<Button.Label>QQ</Button.Label>
|
||||
</Button> */}
|
||||
|
||||
<Button variant="outline" onPress={handleTest}>
|
||||
{loading ? <Spinner entering={FadeIn.delay(50)} /> : <IconQq />}
|
||||
<Button.Label>Test</Button.Label>
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
|
||||
@@ -1,280 +0,0 @@
|
||||
import { useEffect } from "react";
|
||||
import { Stack } from "expo-router";
|
||||
import { Image, Text, View } from "react-native";
|
||||
import { Button, LinkButton, ControlField, Checkbox, Spinner } from "heroui-native";
|
||||
import { IconQq, IconWeixin } from "@/components/iconfont";
|
||||
import { useState } from "react";
|
||||
import { FadeIn } from "react-native-reanimated";
|
||||
import { useUserStore } from "@/store";
|
||||
import { useRouter } from "expo-router";
|
||||
import ExpoQQ from "expo-qq";
|
||||
import ExpoWeChat from "expo-wechat";
|
||||
import { useEvent } from "expo";
|
||||
import { toast } from "sonner-native";
|
||||
import { loginQqApi, loginWxApi } from "@/request/api";
|
||||
|
||||
export default function Login() {
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isAgreed, setIsAgreed] = useState(true);
|
||||
const [form, setForm] = useState({
|
||||
type: 0,
|
||||
openid: "",
|
||||
code: "",
|
||||
});
|
||||
|
||||
// 登录
|
||||
// const onLoginFinished = useEvent(ExpoQQ, "onLoginFinished");
|
||||
// const onGetUserInfo = useEvent(ExpoQQ, "onGetUserInfo");
|
||||
// const authResult = useEvent(ExpoWeChat, "onAuthResult");
|
||||
|
||||
// useEffect(() => {
|
||||
// ExpoQQ.init("102826474", "");
|
||||
// ExpoWeChat.registerApp("wxdab3e21a1f7e392f", "");
|
||||
// }, []);
|
||||
|
||||
// // 微信登录
|
||||
// async function handleLoginWechat() {
|
||||
// if (loading) {
|
||||
// return;
|
||||
// }
|
||||
// setLoading(true);
|
||||
|
||||
// // 检查微信是否安装
|
||||
// const isInstalled = await ExpoWeChat.isWXAppInstalled();
|
||||
// if (!isInstalled) {
|
||||
// toast.warning(onLoginFinished?.errorMessage || "请先安装微信");
|
||||
// setLoading(false);
|
||||
// return;
|
||||
// }
|
||||
|
||||
// setForm((val) => ({
|
||||
// ...val,
|
||||
// type: 0,
|
||||
// }));
|
||||
|
||||
// ExpoWeChat.sendAuthRequest("snsapi_userinfo", "state123");
|
||||
// }
|
||||
|
||||
// // QQ 登录
|
||||
// async function handleLoginQQ() {
|
||||
// if (loading) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// setLoading(true);
|
||||
|
||||
// setForm((val) => ({
|
||||
// ...val,
|
||||
// type: 1,
|
||||
// }));
|
||||
// await ExpoQQ.login(["get_user_info"]);
|
||||
// }
|
||||
|
||||
/**
|
||||
* {"code": 1000, "data": {"avatar": "http://thirdqq.qlogo.cn/ek_qqapp/AQHVialeUibE42oV8RvTTVZ4tyRlPYeJrUibWTwtCPfV68E4rSzn9cw00GkXQ3QDTiawwhjoqQb7/100",
|
||||
* "nickname": "Zhang", "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7ImlkIjo3OTY0NTY3MTI3ODU5ODl9LCJpYXQiOjE3NzY1ODU2MDR9.ZwmN5_0MGrP9dYpn3quK94HXMG3QYKmAq7wnP5X68dY", "userId": 796456712785989}, "isSuccess": true, "message": "success", "track_id": "djIRo23SZlA160zBTH2DX-kxeuLApKTNyZdxqqauSMk"}
|
||||
*/
|
||||
|
||||
function handleTest() {
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
setTimeout(() => {
|
||||
requestAnimationFrame(() => {
|
||||
userStore.setUserInfo({
|
||||
id: 779895419121733,
|
||||
token:
|
||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7ImlkIjo3OTY0NTY3MTI3ODU5ODl9LCJpYXQiOjE3NzY1ODU2MDR9.ZwmN5_0MGrP9dYpn3quK94HXMG3QYKmAq7wnP5X68dY",
|
||||
nickname: "Zhang",
|
||||
avatar:
|
||||
"http://thirdqq.qlogo.cn/ek_qqapp/AQHVialeUibE42oV8RvTTVZ4tyRlPYeJrUibWTwtCPfV68E4rSzn9cw00GkXQ3QDTiawwhjoqQb7/100",
|
||||
});
|
||||
userStore.setAuthExpired(false);
|
||||
// router.replace("/(tabs)/home");
|
||||
router.replace("/family/entry");
|
||||
});
|
||||
setLoading(false);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// useEffect(() => {
|
||||
// if (!onLoginFinished) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// if (!onLoginFinished.success) {
|
||||
// toast.warning(onLoginFinished?.errorMessage || "授权失败,请检查手机是否安装 QQ 或确认授权");
|
||||
// setLoading(false);
|
||||
// return;
|
||||
// }
|
||||
|
||||
// const fetchUserInfo = async () => {
|
||||
// try {
|
||||
// const tokenInfo = await ExpoQQ.getLoginTokenInfo();
|
||||
// if (!tokenInfo.openId) {
|
||||
// toast.warning(tokenInfo?.errorMessage || "授权失败,请检查手机是否安装 QQ 或确认授权");
|
||||
// setLoading(false);
|
||||
// return;
|
||||
// }
|
||||
|
||||
// setForm((val) => ({
|
||||
// ...val,
|
||||
// openid: String(tokenInfo.openId),
|
||||
// }));
|
||||
|
||||
// await ExpoQQ.sendGetUserInfoReq();
|
||||
// } catch (e) {
|
||||
// console.log(e);
|
||||
|
||||
// setLoading(false);
|
||||
// }
|
||||
// };
|
||||
|
||||
// fetchUserInfo();
|
||||
// }, [onLoginFinished]);
|
||||
|
||||
// // QQ登录
|
||||
// useEffect(() => {
|
||||
// const handleLogin = async () => {
|
||||
// if (!onGetUserInfo) {
|
||||
// setLoading(false);
|
||||
// return;
|
||||
// }
|
||||
|
||||
// try {
|
||||
// const loginRes = await loginQqApi({
|
||||
// openid: form.openid,
|
||||
// nickname: onGetUserInfo.nickname || "",
|
||||
// avatar: onGetUserInfo.figureurlQq_2 || onGetUserInfo.figureurl_2,
|
||||
// });
|
||||
|
||||
// console.log(loginRes);
|
||||
|
||||
// if (loginRes.isSuccess) {
|
||||
// requestAnimationFrame(() => {
|
||||
// userStore.setUserInfo({
|
||||
// token: loginRes.data.token,
|
||||
// id: loginRes.data.userId,
|
||||
// nickname: loginRes.data.nickname,
|
||||
// avatar: loginRes.data.avatar,
|
||||
// });
|
||||
|
||||
// userStore.setAuthExpired(false);
|
||||
// router.replace("/(tabs)/home");
|
||||
// });
|
||||
// }
|
||||
// } catch (err) {
|
||||
// console.error("登录接口出错", err);
|
||||
// } finally {
|
||||
// setLoading(false);
|
||||
// }
|
||||
// };
|
||||
|
||||
// handleLogin();
|
||||
// }, [onGetUserInfo]);
|
||||
|
||||
// // 微信登录
|
||||
// useEffect(() => {
|
||||
// if (!authResult) return;
|
||||
|
||||
// const handleLogin = async () => {
|
||||
// if (authResult.errorCode === 0) {
|
||||
// try {
|
||||
// const loginRes = await loginWxApi({
|
||||
// code: authResult.code,
|
||||
// });
|
||||
|
||||
// if (loginRes.isSuccess) {
|
||||
// requestAnimationFrame(() => {
|
||||
// userStore.setUserInfo({
|
||||
// token: loginRes.data.token,
|
||||
// id: loginRes.data.userId,
|
||||
// nickname: loginRes.data.nickname,
|
||||
// avatar: loginRes.data.avatar,
|
||||
// });
|
||||
|
||||
// userStore.setAuthExpired(false);
|
||||
// router.replace("/(tabs)/home");
|
||||
// });
|
||||
// }
|
||||
// } catch (err) {
|
||||
// console.error("登录接口出错", err);
|
||||
// } finally {
|
||||
// setLoading(false);
|
||||
// }
|
||||
// } else {
|
||||
// console.log("授权失败:", authResult.errorMessage);
|
||||
// setLoading(false);
|
||||
// }
|
||||
// };
|
||||
|
||||
// handleLogin();
|
||||
// }, [authResult]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
animation: "fade",
|
||||
}}
|
||||
/>
|
||||
<View className="flex-1 bg-white px-6 justify-center">
|
||||
<View className="items-center mb-12">
|
||||
<Image
|
||||
source={require("@/static/images/logo.png")}
|
||||
className="w-20 h-20 mb-4"
|
||||
resizeMode="contain"
|
||||
/>
|
||||
<Text className="text-xl font-semibold text-gray-900">Welcome to Allapp</Text>
|
||||
</View>
|
||||
|
||||
<View className="flex flex-col gap-4">
|
||||
{/* <Button onPress={handleLoginWechat}>
|
||||
{loading ? (
|
||||
<Spinner entering={FadeIn.delay(50)} color="white" />
|
||||
) : (
|
||||
<IconWeixin color="white" />
|
||||
)}
|
||||
<Button.Label>WeChat</Button.Label>
|
||||
</Button>
|
||||
|
||||
<Button variant="outline" onPress={handleLoginQQ}>
|
||||
{loading ? <Spinner entering={FadeIn.delay(50)} /> : <IconQq />}
|
||||
<Button.Label>QQ</Button.Label>
|
||||
</Button> */}
|
||||
|
||||
<Button variant="outline" onPress={handleTest}>
|
||||
{loading ? <Spinner entering={FadeIn.delay(50)} /> : <IconQq />}
|
||||
<Button.Label>Test</Button.Label>
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
<ControlField
|
||||
isSelected={isAgreed}
|
||||
onSelectedChange={setIsAgreed}
|
||||
className="items-start mt-10"
|
||||
>
|
||||
<ControlField.Indicator>
|
||||
<Checkbox className="w-5 h-5 rounded-sm" />
|
||||
</ControlField.Indicator>
|
||||
<View className="flex-row flex-wrap flex-1 gap-1">
|
||||
<Text className="text-sm text-muted">登录即表示你同意我们的</Text>
|
||||
<LinkButton size="sm">
|
||||
<LinkButton.Label className="text-accent">用户协议</LinkButton.Label>
|
||||
</LinkButton>
|
||||
<Text className="text-sm text-muted">和</Text>
|
||||
<LinkButton size="sm">
|
||||
<LinkButton.Label className="text-accent">隐私政策</LinkButton.Label>
|
||||
</LinkButton>
|
||||
</View>
|
||||
</ControlField>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
import { useEffect } from "react";
|
||||
import { Stack } from "expo-router";
|
||||
import { Image, Text, View } from "react-native";
|
||||
import { Button, LinkButton, ControlField, Checkbox, Spinner } from "heroui-native";
|
||||
import { IconQq, IconWeixin } from "@/components/iconfont";
|
||||
import { useState } from "react";
|
||||
import { FadeIn } from "react-native-reanimated";
|
||||
import { useUserStore, useFamilyStore } from "@/store";
|
||||
import { useRouter } from "expo-router";
|
||||
import ExpoQQ from "expo-qq";
|
||||
import ExpoWeChat from "expo-wechat";
|
||||
import { useEvent } from "expo";
|
||||
import { toast } from "sonner-native";
|
||||
import { loginQqApi, loginWxApi } from "@/request/api";
|
||||
|
||||
export default function Login() {
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
const familyStore = useFamilyStore();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isAgreed, setIsAgreed] = useState(true);
|
||||
const [form, setForm] = useState({
|
||||
type: 0,
|
||||
openid: "",
|
||||
code: "",
|
||||
});
|
||||
|
||||
// 登录
|
||||
const onLoginFinished = useEvent(ExpoQQ, "onLoginFinished");
|
||||
const onGetUserInfo = useEvent(ExpoQQ, "onGetUserInfo");
|
||||
const authResult = useEvent(ExpoWeChat, "onAuthResult");
|
||||
|
||||
useEffect(() => {
|
||||
ExpoQQ.init("102826474", "");
|
||||
ExpoWeChat.registerApp("wxdab3e21a1f7e392f", "");
|
||||
}, []);
|
||||
|
||||
// 微信登录
|
||||
async function handleLoginWechat() {
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
|
||||
// 检查微信是否安装
|
||||
const isInstalled = await ExpoWeChat.isWXAppInstalled();
|
||||
if (!isInstalled) {
|
||||
toast.warning(onLoginFinished?.errorMessage || "请先安装微信");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setForm((val) => ({
|
||||
...val,
|
||||
type: 0,
|
||||
}));
|
||||
|
||||
ExpoWeChat.sendAuthRequest("snsapi_userinfo", "state123");
|
||||
}
|
||||
|
||||
// QQ 登录
|
||||
async function handleLoginQQ() {
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
setForm((val) => ({
|
||||
...val,
|
||||
type: 1,
|
||||
}));
|
||||
await ExpoQQ.login(["get_user_info"]);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!onLoginFinished) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!onLoginFinished.success) {
|
||||
toast.warning(onLoginFinished?.errorMessage || "授权失败,请检查手机是否安装 QQ 或确认授权");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchUserInfo = async () => {
|
||||
try {
|
||||
const tokenInfo = await ExpoQQ.getLoginTokenInfo();
|
||||
if (!tokenInfo.openId) {
|
||||
toast.warning(tokenInfo?.errorMessage || "授权失败,请检查手机是否安装 QQ 或确认授权");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setForm((val) => ({
|
||||
...val,
|
||||
openid: String(tokenInfo.openId),
|
||||
}));
|
||||
|
||||
await ExpoQQ.sendGetUserInfoReq();
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchUserInfo();
|
||||
}, [onLoginFinished]);
|
||||
|
||||
// QQ登录
|
||||
useEffect(() => {
|
||||
const handleLogin = async () => {
|
||||
if (!onGetUserInfo) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const loginRes = await loginQqApi({
|
||||
openid: form.openid,
|
||||
nickname: onGetUserInfo.nickname || "",
|
||||
avatar: onGetUserInfo.figureurlQq_2 || onGetUserInfo.figureurl_2,
|
||||
});
|
||||
|
||||
if (loginRes.isSuccess) {
|
||||
requestAnimationFrame(() => {
|
||||
userStore.setUserInfo({
|
||||
token: loginRes.data.token,
|
||||
id: loginRes.data.userId,
|
||||
nickname: loginRes.data.nickname,
|
||||
avatar: loginRes.data.avatar,
|
||||
});
|
||||
|
||||
userStore.setAuthExpired(false);
|
||||
|
||||
if (loginRes.data.family_id === 0) {
|
||||
router.replace("/family/entry");
|
||||
} else {
|
||||
familyStore.setCurrentFamily({
|
||||
family_id: loginRes.data.family_id,
|
||||
});
|
||||
|
||||
router.replace("/(tabs)/home");
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("登录接口出错", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
handleLogin();
|
||||
}, [onGetUserInfo]);
|
||||
|
||||
// 微信登录
|
||||
useEffect(() => {
|
||||
if (!authResult) return;
|
||||
|
||||
const handleLogin = async () => {
|
||||
if (authResult.errorCode === 0) {
|
||||
try {
|
||||
const loginRes = await loginWxApi({
|
||||
code: authResult.code,
|
||||
});
|
||||
|
||||
if (loginRes.isSuccess) {
|
||||
requestAnimationFrame(() => {
|
||||
userStore.setUserInfo({
|
||||
token: loginRes.data.token,
|
||||
id: loginRes.data.userId,
|
||||
nickname: loginRes.data.nickname,
|
||||
avatar: loginRes.data.avatar,
|
||||
});
|
||||
|
||||
userStore.setAuthExpired(false);
|
||||
|
||||
if (loginRes.data.family_id === 0) {
|
||||
router.replace("/family/entry");
|
||||
} else {
|
||||
familyStore.setCurrentFamily({
|
||||
family_id: loginRes.data.family_id,
|
||||
});
|
||||
|
||||
router.replace("/(tabs)/home");
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("登录接口出错", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
} else {
|
||||
console.log("授权失败:", authResult.errorMessage);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
handleLogin();
|
||||
}, [authResult]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
animation: "fade",
|
||||
}}
|
||||
/>
|
||||
<View className="flex-1 bg-white px-6 justify-center">
|
||||
<View className="items-center mb-12">
|
||||
<Image
|
||||
source={require("@/static/images/logo.png")}
|
||||
className="w-20 h-20 mb-4"
|
||||
resizeMode="contain"
|
||||
/>
|
||||
<Text className="text-xl font-semibold text-gray-900">Welcome to Allapp</Text>
|
||||
</View>
|
||||
|
||||
<View className="flex flex-col gap-4">
|
||||
<Button onPress={handleLoginWechat}>
|
||||
{loading ? (
|
||||
<Spinner entering={FadeIn.delay(50)} color="white" />
|
||||
) : (
|
||||
<IconWeixin color="white" />
|
||||
)}
|
||||
<Button.Label>WeChat</Button.Label>
|
||||
</Button>
|
||||
|
||||
<Button variant="outline" onPress={handleLoginQQ}>
|
||||
{loading ? <Spinner entering={FadeIn.delay(50)} /> : <IconQq />}
|
||||
<Button.Label>QQ</Button.Label>
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
<ControlField
|
||||
isSelected={isAgreed}
|
||||
onSelectedChange={setIsAgreed}
|
||||
className="items-start mt-10"
|
||||
>
|
||||
<ControlField.Indicator>
|
||||
<Checkbox className="w-5 h-5 rounded-sm" />
|
||||
</ControlField.Indicator>
|
||||
<View className="flex-row flex-wrap flex-1 gap-1">
|
||||
<Text className="text-sm text-muted">登录即表示你同意我们的</Text>
|
||||
<LinkButton size="sm">
|
||||
<LinkButton.Label className="text-accent">用户协议</LinkButton.Label>
|
||||
</LinkButton>
|
||||
<Text className="text-sm text-muted">和</Text>
|
||||
<LinkButton size="sm">
|
||||
<LinkButton.Label className="text-accent">隐私政策</LinkButton.Label>
|
||||
</LinkButton>
|
||||
</View>
|
||||
</ControlField>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import LayoutView from "@/components/layout/view";
|
||||
import { Text, View } from "react-native";
|
||||
import PageLayout from "@/layouts/PageLayout";
|
||||
import { useLocalSearchParams } from "expo-router";
|
||||
import { useUserStore, useFamilyStore } from "@/store";
|
||||
import { loadDataApi, saveDataApi } from "@/request/api";
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
ListGroup,
|
||||
Separator,
|
||||
PressableFeedback,
|
||||
Dialog,
|
||||
Spinner,
|
||||
} from "heroui-native";
|
||||
import {
|
||||
MessageCircleIcon,
|
||||
VideoIcon,
|
||||
PhoneIcon,
|
||||
CakeIcon,
|
||||
CalendarDaysIcon,
|
||||
Trash2Icon,
|
||||
} from "lucide-react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { withUniwind } from "uniwind";
|
||||
import { FadeIn } from "react-native-reanimated";
|
||||
import { toast } from "sonner-native";
|
||||
import { useRouter } from "expo-router";
|
||||
|
||||
const StyledIonicons = withUniwind(Ionicons);
|
||||
|
||||
export default function FamilyMember() {
|
||||
const { id } = useLocalSearchParams();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const userStore = useUserStore();
|
||||
const familyStore = useFamilyStore();
|
||||
|
||||
const [memberInfo, setMemberInfo] = useState<any>({});
|
||||
const [currentUserRole, setCurrentUserRole] = useState(1);
|
||||
|
||||
// 移除家庭
|
||||
const [showRemove, setShowRemove] = useState(false);
|
||||
const [removeLoading, setRemoveLoading] = useState(false);
|
||||
|
||||
async function loadMemberInfo() {
|
||||
const familyId = familyStore.currentFamily.family_id;
|
||||
const currentUserId = userStore.userInfo.id;
|
||||
|
||||
try {
|
||||
const [memberRes, roleRes] = await Promise.all([
|
||||
loadDataApi({
|
||||
view_name: "v_family_member",
|
||||
search_condition: "user_id = ? and family_id = ?",
|
||||
search_columns: [],
|
||||
order_by: "",
|
||||
args: [id, familyId],
|
||||
}),
|
||||
loadDataApi({
|
||||
view_name: "v_family_member",
|
||||
search_condition: "user_id = ? and family_id = ?",
|
||||
search_columns: ["role"],
|
||||
order_by: "",
|
||||
args: [currentUserId, familyId],
|
||||
}),
|
||||
]);
|
||||
|
||||
if (memberRes.isSuccess && memberRes.data.length > 0) {
|
||||
setMemberInfo(memberRes.data[0]);
|
||||
}
|
||||
|
||||
if (roleRes.isSuccess && roleRes.data.length > 0) {
|
||||
setCurrentUserRole(roleRes.data[0].role);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("loadMemberInfo error:", err);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
|
||||
loadMemberInfo();
|
||||
}, [id]);
|
||||
|
||||
async function handleRemove() {
|
||||
const familyId = familyStore.currentFamily.family_id;
|
||||
const currentUserId = userStore.userInfo.id;
|
||||
|
||||
// 前端快速拦截
|
||||
if (currentUserRole !== 0) return;
|
||||
|
||||
if (Number(id) === currentUserId) {
|
||||
toast.error("无法移除自己");
|
||||
return;
|
||||
}
|
||||
|
||||
if (removeLoading) return;
|
||||
|
||||
setRemoveLoading(true);
|
||||
|
||||
try {
|
||||
const roleRes = await loadDataApi({
|
||||
view_name: "v_family_member",
|
||||
search_condition: "user_id = ? and family_id = ?",
|
||||
search_columns: ["role"],
|
||||
order_by: "",
|
||||
args: [currentUserId, familyId],
|
||||
});
|
||||
|
||||
if (!roleRes.isSuccess) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (roleRes.data.length === 0) {
|
||||
toast.error("无权限操作");
|
||||
return;
|
||||
}
|
||||
|
||||
const latestRole = roleRes.data[0].role;
|
||||
|
||||
if (latestRole !== 0) {
|
||||
toast.error("无权限操作");
|
||||
return;
|
||||
}
|
||||
|
||||
const requestData = [
|
||||
{
|
||||
table_name: "b_family_member",
|
||||
key_field: "id",
|
||||
inserts: [],
|
||||
updates: [],
|
||||
deletes: [
|
||||
{
|
||||
id: memberInfo.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const res = await saveDataApi(requestData);
|
||||
|
||||
if (res.isSuccess) {
|
||||
toast.success("移除成功");
|
||||
setShowRemove(false);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
router.back();
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("handleRemove error:", e);
|
||||
toast.error("操作失败");
|
||||
} finally {
|
||||
setRemoveLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<PageLayout title="用户资料">
|
||||
<LayoutView>
|
||||
<LayoutView.ScrollView>
|
||||
<View className="items-center justify-center">
|
||||
<Avatar alt="" className="w-28 h-28">
|
||||
<Avatar.Image source={{ uri: memberInfo.avatar }} />
|
||||
<Avatar.Fallback className="text-sm">{memberInfo.nickname}</Avatar.Fallback>
|
||||
</Avatar>
|
||||
|
||||
<Text className="text-2xl font-medium mt-4">{memberInfo.nickname}</Text>
|
||||
|
||||
<View className="mt-3 flex-row gap-3">
|
||||
<Button className="flex-1" variant="primary">
|
||||
<MessageCircleIcon size={18} color="#fff" />
|
||||
<Button.Label>聊天</Button.Label>
|
||||
</Button>
|
||||
|
||||
<Button className="flex-1" variant="outline">
|
||||
<PhoneIcon size={18} />
|
||||
<Button.Label>语音</Button.Label>
|
||||
</Button>
|
||||
|
||||
<Button className="flex-1" variant="outline">
|
||||
<VideoIcon size={18} />
|
||||
<Button.Label>视频</Button.Label>
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Text className="text-sm text-muted mt-4 mb-2 ml-2">家庭成员信息</Text>
|
||||
|
||||
<ListGroup className="mb-6">
|
||||
<PressableFeedback>
|
||||
<ListGroup.Item>
|
||||
<ListGroup.ItemPrefix>
|
||||
<StyledIonicons name="home-outline" size={22} className="text-foreground" />
|
||||
</ListGroup.ItemPrefix>
|
||||
<ListGroup.ItemContent>
|
||||
<ListGroup.ItemTitle>姓名</ListGroup.ItemTitle>
|
||||
</ListGroup.ItemContent>
|
||||
<ListGroup.ItemSuffix>
|
||||
<Text className="text-sm text-muted">张三</Text>
|
||||
</ListGroup.ItemSuffix>
|
||||
</ListGroup.Item>
|
||||
<PressableFeedback.Ripple />
|
||||
</PressableFeedback>
|
||||
<Separator className="mx-4" />
|
||||
<PressableFeedback>
|
||||
<ListGroup.Item>
|
||||
<ListGroup.ItemPrefix>
|
||||
<PhoneIcon size="22" strokeWidth={1.5} />
|
||||
</ListGroup.ItemPrefix>
|
||||
<ListGroup.ItemContent>
|
||||
<ListGroup.ItemTitle>手机号</ListGroup.ItemTitle>
|
||||
</ListGroup.ItemContent>
|
||||
<ListGroup.ItemSuffix>
|
||||
<Text className="text-sm text-muted">18233334444</Text>
|
||||
</ListGroup.ItemSuffix>
|
||||
</ListGroup.Item>
|
||||
<PressableFeedback.Ripple />
|
||||
</PressableFeedback>
|
||||
<Separator className="mx-4" />
|
||||
<PressableFeedback>
|
||||
<ListGroup.Item>
|
||||
<ListGroup.ItemPrefix>
|
||||
<CakeIcon size="22" strokeWidth={1.5} />
|
||||
</ListGroup.ItemPrefix>
|
||||
<ListGroup.ItemContent>
|
||||
<ListGroup.ItemTitle>生日</ListGroup.ItemTitle>
|
||||
</ListGroup.ItemContent>
|
||||
<ListGroup.ItemSuffix>
|
||||
<Text className="text-sm text-muted">18233334444</Text>
|
||||
</ListGroup.ItemSuffix>
|
||||
</ListGroup.Item>
|
||||
<PressableFeedback.Ripple />
|
||||
</PressableFeedback>
|
||||
<Separator className="mx-4" />
|
||||
<PressableFeedback>
|
||||
<ListGroup.Item>
|
||||
<ListGroup.ItemPrefix>
|
||||
<CalendarDaysIcon size="22" strokeWidth={1.5} />
|
||||
</ListGroup.ItemPrefix>
|
||||
<ListGroup.ItemContent>
|
||||
<ListGroup.ItemTitle>年龄</ListGroup.ItemTitle>
|
||||
</ListGroup.ItemContent>
|
||||
<ListGroup.ItemSuffix>
|
||||
<Text className="text-sm text-muted">26</Text>
|
||||
</ListGroup.ItemSuffix>
|
||||
</ListGroup.Item>
|
||||
<PressableFeedback.Ripple />
|
||||
</PressableFeedback>
|
||||
</ListGroup>
|
||||
|
||||
{currentUserRole === 0 && (
|
||||
<View className="mt-4">
|
||||
<Button variant="danger" onPress={() => setShowRemove(true)}>
|
||||
<Trash2Icon color="#fff" size={22} strokeWidth={1.5} />
|
||||
<Button.Label>移除家庭</Button.Label>
|
||||
</Button>
|
||||
</View>
|
||||
)}
|
||||
</LayoutView.ScrollView>
|
||||
</LayoutView>
|
||||
|
||||
<Dialog isOpen={showRemove} onOpenChange={setShowRemove}>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay />
|
||||
|
||||
<Dialog.Content>
|
||||
<View className="flex-row items-center justify-between">
|
||||
<Dialog.Title>提示</Dialog.Title>
|
||||
<Dialog.Close variant="ghost" />
|
||||
</View>
|
||||
|
||||
<View className="my-2 gap-1.5">
|
||||
<Dialog.Description>
|
||||
确定将 {memberInfo.nickname}{" "}
|
||||
从家庭中移除吗?被移除的成员将无法再访问家庭相关的任何信息。
|
||||
</Dialog.Description>
|
||||
</View>
|
||||
|
||||
<View className="flex-row justify-end gap-3">
|
||||
<Button variant="ghost" size="sm" onPress={() => setShowRemove(false)}>
|
||||
取消
|
||||
</Button>
|
||||
|
||||
<Button size="sm" variant="danger" onPress={handleRemove}>
|
||||
{removeLoading && <Spinner entering={FadeIn.delay(50)} color="white" />}
|
||||
|
||||
<Button.Label>确认移除</Button.Label>
|
||||
</Button>
|
||||
</View>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,659 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import LayoutView from "@/components/layout/view";
|
||||
import { View, Text, Keyboard, TouchableOpacity } from "react-native";
|
||||
import PageLayout from "@/layouts/PageLayout";
|
||||
import {
|
||||
Button,
|
||||
Separator,
|
||||
Spinner,
|
||||
BottomSheet,
|
||||
Label,
|
||||
TextField,
|
||||
Input,
|
||||
PressableFeedback,
|
||||
useBottomSheetAwareHandlers,
|
||||
ListGroup,
|
||||
Dialog,
|
||||
Avatar,
|
||||
Chip,
|
||||
} from "heroui-native";
|
||||
import { withUniwind } from "uniwind";
|
||||
import { forwardRef, useEffect, useRef, useState } from "react";
|
||||
import { loadDataApi, saveDataApi } from "@/request/api";
|
||||
import { useFamilyStore, useUserStore } from "@/store";
|
||||
import { FadeIn } from "react-native-reanimated";
|
||||
import { toast } from "sonner-native";
|
||||
import { router } from "expo-router";
|
||||
import { CopyIcon } from "lucide-react-native";
|
||||
import * as Clipboard from "expo-clipboard";
|
||||
import { formatRelativeTime } from "@/utils/time";
|
||||
import { UserRoundIcon } from "lucide-react-native";
|
||||
|
||||
const StyledIonicons = withUniwind(Ionicons);
|
||||
|
||||
const BottomSheetTextInput = forwardRef<any, React.ComponentProps<typeof Input>>((props, ref) => {
|
||||
const { onFocus, onBlur } = useBottomSheetAwareHandlers();
|
||||
|
||||
return (
|
||||
<Input
|
||||
ref={ref}
|
||||
{...props}
|
||||
onFocus={(e) => {
|
||||
onFocus(e);
|
||||
props.onFocus?.(e);
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
onBlur(e);
|
||||
props.onBlur?.(e);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default function HomeSettings() {
|
||||
const familyStore = useFamilyStore();
|
||||
const userStore = useUserStore();
|
||||
|
||||
const [family, setFamily] = useState<any>({});
|
||||
const [familyMemeber, setFamilyMember] = useState<any>({});
|
||||
|
||||
// 家庭名称
|
||||
const [showRenameFamily, setShowRenameFamily] = useState(false);
|
||||
const [renameLoading, setRenameLoading] = useState(false);
|
||||
|
||||
const [renameForm, setRenameForm] = useState({
|
||||
name: "",
|
||||
});
|
||||
|
||||
const renameInputRef = useRef<any>(null);
|
||||
|
||||
// 我的昵称
|
||||
const [showRenameNickname, setShowRenameNickname] = useState(false);
|
||||
const [nicknameLoading, setNicknameLoading] = useState(false);
|
||||
|
||||
const [nicknameForm, setNicknameForm] = useState({
|
||||
nickname: "",
|
||||
});
|
||||
|
||||
const nicknameInputRef = useRef<any>(null);
|
||||
|
||||
// 退出/解散 家庭
|
||||
const [showExit, setShowExit] = useState(false);
|
||||
const [exitLoading, setExitLoading] = useState(false);
|
||||
|
||||
// 邀请
|
||||
const [showInvite, setShowInvite] = useState(false);
|
||||
|
||||
// 成员
|
||||
const [familyMembers, setFamilyMembers] = useState<any>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (showRenameFamily) {
|
||||
requestAnimationFrame(() => {
|
||||
renameInputRef.current?.focus();
|
||||
});
|
||||
} else {
|
||||
setRenameForm({
|
||||
name: "",
|
||||
});
|
||||
setRenameLoading(false);
|
||||
Keyboard.dismiss();
|
||||
}
|
||||
}, [showRenameFamily]);
|
||||
|
||||
useEffect(() => {
|
||||
if (showRenameNickname) {
|
||||
requestAnimationFrame(() => {
|
||||
nicknameInputRef.current?.focus();
|
||||
});
|
||||
} else {
|
||||
setNicknameForm({
|
||||
nickname: "",
|
||||
});
|
||||
setNicknameLoading(false);
|
||||
Keyboard.dismiss();
|
||||
}
|
||||
}, [showRenameNickname]);
|
||||
|
||||
// 加载
|
||||
async function loadFamilyData() {
|
||||
try {
|
||||
const familyRes = await loadDataApi({
|
||||
view_name: `b_family`,
|
||||
search_condition: `id = ?`,
|
||||
search_columns: [],
|
||||
order_by: "",
|
||||
args: [familyStore.currentFamily?.family_id],
|
||||
});
|
||||
|
||||
if (familyRes.isSuccess && familyRes.data.length > 0) {
|
||||
setFamily(familyRes.data[0]);
|
||||
}
|
||||
|
||||
const memberRes = await loadDataApi({
|
||||
view_name: `b_family_member`,
|
||||
search_condition: `family_id = ? AND user_id = ?`,
|
||||
search_columns: [],
|
||||
order_by: "",
|
||||
args: [familyStore.currentFamily.family_id, userStore.userInfo.id],
|
||||
});
|
||||
|
||||
if (memberRes.isSuccess && memberRes.data.length > 0) {
|
||||
setFamilyMember(memberRes.data[0]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadFamilyData();
|
||||
loadMembers();
|
||||
}, []);
|
||||
|
||||
async function handleRename() {
|
||||
if (renameLoading) return;
|
||||
|
||||
if (renameForm.name === "") {
|
||||
toast.warning("家庭名称不能为空");
|
||||
return;
|
||||
}
|
||||
|
||||
if (renameForm.name === family.name) {
|
||||
setShowRenameFamily(false);
|
||||
setRenameForm({
|
||||
name: "",
|
||||
});
|
||||
toast.success("家庭名称修改成功");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const requestData = [];
|
||||
|
||||
requestData.push({
|
||||
table_name: "b_family",
|
||||
key_field: "id",
|
||||
inserts: [],
|
||||
updates: [
|
||||
{
|
||||
id: family.id,
|
||||
name: renameForm.name,
|
||||
},
|
||||
],
|
||||
deletes: [],
|
||||
});
|
||||
|
||||
const saveRes = await saveDataApi(requestData);
|
||||
|
||||
if (saveRes.isSuccess) {
|
||||
setFamily((prev: any) => ({
|
||||
...prev,
|
||||
name: renameForm.name,
|
||||
}));
|
||||
|
||||
setShowRenameFamily(false);
|
||||
setRenameForm({
|
||||
name: "",
|
||||
});
|
||||
|
||||
toast.success("家庭名称修改成功");
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
setRenameLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleNickname() {
|
||||
if (nicknameLoading) return;
|
||||
|
||||
if (nicknameForm.nickname === "") {
|
||||
toast.warning("昵称不能为空");
|
||||
return;
|
||||
}
|
||||
|
||||
if (nicknameForm.nickname === familyMemeber.nickname) {
|
||||
setShowRenameNickname(false);
|
||||
setNicknameForm({
|
||||
nickname: "",
|
||||
});
|
||||
toast.success("昵称修改成功");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const requestData = [];
|
||||
|
||||
requestData.push({
|
||||
table_name: "b_family_member",
|
||||
key_field: "id",
|
||||
inserts: [],
|
||||
updates: [
|
||||
{
|
||||
id: familyMemeber.id,
|
||||
nickname: nicknameForm.nickname,
|
||||
},
|
||||
],
|
||||
deletes: [],
|
||||
});
|
||||
|
||||
const saveRes = await saveDataApi(requestData);
|
||||
|
||||
if (saveRes.isSuccess) {
|
||||
setFamilyMember((prev: any) => ({
|
||||
...prev,
|
||||
nickname: nicknameForm.nickname,
|
||||
}));
|
||||
|
||||
setShowRenameNickname(false);
|
||||
setNicknameForm({
|
||||
nickname: "",
|
||||
});
|
||||
|
||||
toast.success("昵称修改成功");
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
setRenameLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleExitFamily() {
|
||||
if (exitLoading) return;
|
||||
|
||||
try {
|
||||
let isExit = true;
|
||||
|
||||
const memberRes = await loadDataApi({
|
||||
view_name: `b_family_member`,
|
||||
search_condition: `family_id = ? AND user_id = ?`,
|
||||
search_columns: [],
|
||||
order_by: "",
|
||||
args: [familyStore.currentFamily.family_id, userStore.userInfo.id],
|
||||
});
|
||||
|
||||
if (!memberRes.isSuccess || memberRes.data.length === 0) {
|
||||
toast.success("退出家庭成功");
|
||||
familyStore.clear();
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
router.replace("/family/entry");
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const member = memberRes.data[0];
|
||||
|
||||
isExit = memberRes.data[0].role !== 0;
|
||||
|
||||
const requestData = [];
|
||||
|
||||
if (isExit) {
|
||||
requestData.push({
|
||||
table_name: "b_family_member",
|
||||
key_field: "id",
|
||||
inserts: [],
|
||||
updates: [],
|
||||
deletes: [
|
||||
{
|
||||
id: member.id,
|
||||
},
|
||||
],
|
||||
});
|
||||
} else {
|
||||
requestData.push({
|
||||
table_name: "b_family",
|
||||
key_field: "id",
|
||||
inserts: [],
|
||||
updates: [],
|
||||
deletes: [
|
||||
{
|
||||
id: member.family_id,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
requestData.push({
|
||||
table_name: "b_family_member",
|
||||
key_field: "family_id",
|
||||
inserts: [],
|
||||
updates: [],
|
||||
deletes: [
|
||||
{
|
||||
family_id: member.family_id,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
const saveRes = await saveDataApi(requestData);
|
||||
|
||||
if (saveRes.isSuccess) {
|
||||
toast.success(isExit ? "退出家庭成功" : "解散家庭成功");
|
||||
familyStore.clear();
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
router.replace("/family/entry");
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
setExitLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function copyInviteCode() {
|
||||
if (family.invite_code) {
|
||||
await Clipboard.setStringAsync(family.invite_code);
|
||||
toast.success("邀请码已复制");
|
||||
} else {
|
||||
toast.error("邀请码无效");
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMembers() {
|
||||
let searchCondition = `family_id = ?`;
|
||||
let args = [familyStore.currentFamily.family_id];
|
||||
|
||||
const loadRes = await loadDataApi({
|
||||
view_name: `v_family_member`,
|
||||
search_condition: searchCondition,
|
||||
search_columns: [],
|
||||
order_by: "role ASC, create_time ASC",
|
||||
args,
|
||||
});
|
||||
|
||||
if (loadRes.isSuccess) {
|
||||
setFamilyMembers(loadRes.data);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<PageLayout title="家庭设置">
|
||||
<LayoutView>
|
||||
<LayoutView.ScrollView>
|
||||
<Text className="text-sm text-muted mb-2 ml-2">
|
||||
共 {familyMembers.length || 0} 位成员
|
||||
</Text>
|
||||
|
||||
<View className="mb-4 gap-3">
|
||||
{familyMembers.map((item: any) => (
|
||||
<TouchableOpacity
|
||||
key={item.id}
|
||||
onPress={() => {
|
||||
requestAnimationFrame(() => {
|
||||
router.navigate(`/family/member/${item.user_id}` as any);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<View className="flex-row items-center py-3">
|
||||
<Avatar alt="">
|
||||
<Avatar.Image source={{ uri: item.avatar }} />
|
||||
<Avatar.Fallback className="text-sm">{item.nickname}</Avatar.Fallback>
|
||||
</Avatar>
|
||||
|
||||
<View className="ml-3">
|
||||
<View className="flex-row items-center gap-1">
|
||||
<Text className="text-sm">{item.nickname}</Text>
|
||||
|
||||
{item.role === 0 && (
|
||||
<Chip size="sm" variant="soft" color="success">
|
||||
创建者
|
||||
</Chip>
|
||||
)}
|
||||
</View>
|
||||
<View className="mt-2 gap-2 flex-row items-center">
|
||||
<View className="flex-row gap-1 items-center">
|
||||
<Text className="text-xs text-gray-500">
|
||||
{formatRelativeTime(item.last_login_time)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<Text className="text-sm text-muted mb-2 ml-2">常用设置</Text>
|
||||
<ListGroup className="mb-6">
|
||||
<PressableFeedback
|
||||
onPress={() => {
|
||||
setShowRenameFamily(true);
|
||||
setRenameForm({
|
||||
name: family.name,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ListGroup.Item>
|
||||
<ListGroup.ItemPrefix>
|
||||
<StyledIonicons name="home-outline" size={22} className="text-foreground" />
|
||||
</ListGroup.ItemPrefix>
|
||||
<ListGroup.ItemContent>
|
||||
<ListGroup.ItemTitle>家庭名称</ListGroup.ItemTitle>
|
||||
</ListGroup.ItemContent>
|
||||
<ListGroup.ItemSuffix>
|
||||
<Text className="text-sm text-muted">{family.name}</Text>
|
||||
</ListGroup.ItemSuffix>
|
||||
</ListGroup.Item>
|
||||
<PressableFeedback.Ripple />
|
||||
</PressableFeedback>
|
||||
<Separator className="mx-4" />
|
||||
|
||||
<PressableFeedback
|
||||
onPress={() => {
|
||||
setShowRenameNickname(true);
|
||||
setNicknameForm({
|
||||
nickname: familyMemeber.nickname,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ListGroup.Item>
|
||||
<ListGroup.ItemPrefix>
|
||||
<UserRoundIcon size={22} strokeWidth={1.5} className="text-foreground" />
|
||||
</ListGroup.ItemPrefix>
|
||||
<ListGroup.ItemContent>
|
||||
<ListGroup.ItemTitle>我的昵称</ListGroup.ItemTitle>
|
||||
</ListGroup.ItemContent>
|
||||
<ListGroup.ItemSuffix>
|
||||
<Text className="text-sm text-muted">{familyMemeber.nickname}</Text>
|
||||
</ListGroup.ItemSuffix>
|
||||
</ListGroup.Item>
|
||||
<PressableFeedback.Ripple />
|
||||
</PressableFeedback>
|
||||
|
||||
<Separator className="mx-4" />
|
||||
|
||||
<PressableFeedback onPress={() => setShowInvite(true)}>
|
||||
<ListGroup.Item>
|
||||
<ListGroup.ItemPrefix>
|
||||
<StyledIonicons name="qr-code-outline" size={22} className="text-foreground" />
|
||||
</ListGroup.ItemPrefix>
|
||||
<ListGroup.ItemContent>
|
||||
<ListGroup.ItemTitle>邀请其他人</ListGroup.ItemTitle>
|
||||
</ListGroup.ItemContent>
|
||||
<ListGroup.ItemSuffix />
|
||||
</ListGroup.Item>
|
||||
<PressableFeedback.Ripple />
|
||||
</PressableFeedback>
|
||||
</ListGroup>
|
||||
|
||||
<Text className="text-sm text-muted mb-2 ml-2">其他操作</Text>
|
||||
<ListGroup>
|
||||
<PressableFeedback
|
||||
onPress={() => {
|
||||
setShowExit(true);
|
||||
}}
|
||||
>
|
||||
<ListGroup.Item>
|
||||
<ListGroup.ItemPrefix>
|
||||
<StyledIonicons
|
||||
name="notifications-outline"
|
||||
size={22}
|
||||
className="text-foreground"
|
||||
/>
|
||||
</ListGroup.ItemPrefix>
|
||||
<ListGroup.ItemContent>
|
||||
<ListGroup.ItemTitle>退出家庭</ListGroup.ItemTitle>
|
||||
</ListGroup.ItemContent>
|
||||
<ListGroup.ItemSuffix iconProps={{ size: 18 }} />
|
||||
</ListGroup.Item>
|
||||
<PressableFeedback.Ripple />
|
||||
</PressableFeedback>
|
||||
</ListGroup>
|
||||
</LayoutView.ScrollView>
|
||||
</LayoutView>
|
||||
|
||||
<BottomSheet
|
||||
isOpen={showRenameFamily}
|
||||
onOpenChange={(val) => {
|
||||
setShowRenameFamily(val);
|
||||
}}
|
||||
>
|
||||
<BottomSheet.Portal>
|
||||
<BottomSheet.Overlay />
|
||||
<BottomSheet.Content
|
||||
animation="disabled"
|
||||
enableBlurKeyboardOnGesture
|
||||
keyboardBlurBehavior="restore"
|
||||
>
|
||||
<View>
|
||||
<TextField>
|
||||
<Label>家庭名称</Label>
|
||||
<BottomSheetTextInput
|
||||
ref={renameInputRef}
|
||||
placeholder="请输入家庭名称"
|
||||
value={renameForm.name}
|
||||
onChangeText={(val) =>
|
||||
setRenameForm({
|
||||
name: val,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</TextField>
|
||||
|
||||
<Button className="mt-3" onPress={handleRename}>
|
||||
{renameLoading && <Spinner entering={FadeIn.delay(50)} color="white" />}
|
||||
<Button.Label>保存</Button.Label>
|
||||
</Button>
|
||||
</View>
|
||||
</BottomSheet.Content>
|
||||
</BottomSheet.Portal>
|
||||
</BottomSheet>
|
||||
|
||||
<BottomSheet
|
||||
isOpen={showRenameNickname}
|
||||
onOpenChange={(val) => {
|
||||
setShowRenameNickname(val);
|
||||
}}
|
||||
>
|
||||
<BottomSheet.Portal>
|
||||
<BottomSheet.Overlay />
|
||||
<BottomSheet.Content
|
||||
animation="disabled"
|
||||
enableBlurKeyboardOnGesture
|
||||
keyboardBlurBehavior="restore"
|
||||
>
|
||||
<View>
|
||||
<TextField>
|
||||
<Label>我的昵称</Label>
|
||||
<BottomSheetTextInput
|
||||
ref={nicknameInputRef}
|
||||
placeholder="请输入昵称"
|
||||
value={nicknameForm.nickname}
|
||||
onChangeText={(val) =>
|
||||
setNicknameForm({
|
||||
nickname: val,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</TextField>
|
||||
|
||||
<Button className="mt-3" onPress={handleNickname}>
|
||||
{renameLoading && <Spinner entering={FadeIn.delay(50)} color="white" />}
|
||||
<Button.Label>保存</Button.Label>
|
||||
</Button>
|
||||
</View>
|
||||
</BottomSheet.Content>
|
||||
</BottomSheet.Portal>
|
||||
</BottomSheet>
|
||||
|
||||
<Dialog isOpen={showExit} onOpenChange={setShowExit}>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay />
|
||||
|
||||
<Dialog.Content>
|
||||
<View className="flex-row items-center justify-between">
|
||||
<Dialog.Title>提示</Dialog.Title>
|
||||
<Dialog.Close variant="ghost" />
|
||||
</View>
|
||||
|
||||
<View className="my-2 gap-1.5">
|
||||
<Dialog.Description>
|
||||
{familyMemeber.role === 0
|
||||
? "您是家庭管理员,确认要解散家庭吗?"
|
||||
: "确认要退出家庭吗?"}
|
||||
</Dialog.Description>
|
||||
|
||||
{familyMemeber.role === 0 && (
|
||||
<Text className="text-xs text-red-500 mt-1">
|
||||
你是家庭管理员,退出将导致家庭解散,不可恢复
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View className="flex-row justify-end gap-3">
|
||||
<Button variant="ghost" size="sm" onPress={() => setShowExit(false)}>
|
||||
取消
|
||||
</Button>
|
||||
|
||||
<Button size="sm" variant="danger" onPress={handleExitFamily}>
|
||||
{exitLoading && <Spinner entering={FadeIn.delay(50)} color="white" />}
|
||||
<Button.Label>确认退出</Button.Label>
|
||||
</Button>
|
||||
</View>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog>
|
||||
|
||||
<BottomSheet
|
||||
isOpen={showInvite}
|
||||
onOpenChange={(val) => {
|
||||
setShowInvite(val);
|
||||
}}
|
||||
>
|
||||
<BottomSheet.Portal>
|
||||
<BottomSheet.Overlay />
|
||||
<BottomSheet.Content
|
||||
animation="disabled"
|
||||
enableBlurKeyboardOnGesture
|
||||
keyboardBlurBehavior="restore"
|
||||
>
|
||||
<View>
|
||||
<TextField>
|
||||
<Label>邀请码</Label>
|
||||
<PressableFeedback
|
||||
className="flex-row gap-2 items-center justify-center h-20 bg-white rounded-xl shadow-2xl"
|
||||
onPress={copyInviteCode}
|
||||
>
|
||||
<Text className="text-3xl font-bold tracking-[8px] text-gray-900 ">
|
||||
{family.invite_code}
|
||||
</Text>
|
||||
|
||||
<CopyIcon />
|
||||
|
||||
<PressableFeedback.Ripple />
|
||||
</PressableFeedback>
|
||||
</TextField>
|
||||
</View>
|
||||
</BottomSheet.Content>
|
||||
</BottomSheet.Portal>
|
||||
</BottomSheet>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import LayoutView from "@/components/layout/view";
|
||||
import { Text } from "react-native";
|
||||
import PageLayout from "@/layouts/PageLayout";
|
||||
|
||||
export default function UserSetting() {
|
||||
return (
|
||||
<PageLayout title="个人设置">
|
||||
<LayoutView>
|
||||
<LayoutView.ScrollView>
|
||||
<Text className="text-base text-black">UserSetting</Text>
|
||||
</LayoutView.ScrollView>
|
||||
</LayoutView>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
@@ -1,30 +1,47 @@
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "expo-router";
|
||||
import { useUserStore, useFamilyStore } from "@/store";
|
||||
import { loadDataApi } from "@/request/api";
|
||||
import * as SplashScreen from "expo-splash-screen";
|
||||
import { toast } from "sonner-native";
|
||||
|
||||
export default function Index() {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
const run = async () => {
|
||||
const { isLogged } = useUserStore.getState();
|
||||
const { inFamily } = useFamilyStore.getState();
|
||||
|
||||
console.log(useFamilyStore.getState().currentFamily);
|
||||
const { isLogged, userInfo } = useUserStore.getState();
|
||||
const familyStore = useFamilyStore.getState();
|
||||
|
||||
if (!isLogged()) {
|
||||
requestAnimationFrame(() => {
|
||||
router.replace("/auth/login");
|
||||
router.replace("/auth/login");
|
||||
await SplashScreen.hideAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await loadDataApi({
|
||||
view_name: "b_family_member",
|
||||
search_condition: "user_id = ?",
|
||||
search_columns: ["family_id"],
|
||||
order_by: "",
|
||||
args: [userInfo.id],
|
||||
});
|
||||
} else if (!inFamily()) {
|
||||
requestAnimationFrame(() => {
|
||||
|
||||
if (res.isSuccess && res.data.length === 0) {
|
||||
familyStore.clear();
|
||||
toast.warning("你还没有加入任何家庭,请先创建或加入一个家庭");
|
||||
|
||||
router.replace("/family/entry");
|
||||
});
|
||||
} else {
|
||||
requestAnimationFrame(() => {
|
||||
} else {
|
||||
familyStore.setCurrentFamily({
|
||||
family_id: res.data[0].family_id,
|
||||
});
|
||||
|
||||
router.replace("/(tabs)/home");
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
await SplashScreen.hideAsync();
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import React from "react";
|
||||
import LayoutView from "@/components/layout/view";
|
||||
import { View, Text } from "react-native";
|
||||
import Navbar from "@/components/layout/navbar";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { EarthIcon } from "lucide-react-native";
|
||||
import { useRouter } from "expo-router";
|
||||
|
||||
export default function Space() {
|
||||
const insets = useSafeAreaInsets();
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<View style={{ flex: 1, zIndex: 0 }}>
|
||||
<LayoutView>
|
||||
<LayoutView.ScrollView>
|
||||
<Text className="text-base text-black">Space</Text>
|
||||
</LayoutView.ScrollView>
|
||||
</LayoutView>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 10,
|
||||
}}
|
||||
>
|
||||
<View style={{ paddingTop: insets.top }}>
|
||||
<Navbar
|
||||
title={"空间"}
|
||||
titleIcon={EarthIcon}
|
||||
onBack={() => {
|
||||
router.back();
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -128,6 +128,7 @@ export default function Navbar({
|
||||
safeRender
|
||||
style={styles.groupShadow}
|
||||
>
|
||||
|
||||
<Pressable
|
||||
onPressIn={() => {
|
||||
leftScale.value = withSpring(0.8);
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import { Slot, useRouter } from "expo-router";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import Navbar from "@/components/layout/navbar";
|
||||
|
||||
interface ModuleLayoutProps {
|
||||
title?: string;
|
||||
titleIcon?: any;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function PageLayout({ title, titleIcon, children }: ModuleLayoutProps) {
|
||||
const router = useRouter();
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
function handleBack() {
|
||||
router.back();
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
router.navigate("/(tabs)/home");
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<View style={{ flex: 1 }}>{children ?? <Slot />}</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 10,
|
||||
}}
|
||||
>
|
||||
<View style={{ paddingTop: insets.top }}>
|
||||
<Navbar
|
||||
title={title}
|
||||
titleIcon={titleIcon}
|
||||
showBack={true}
|
||||
onBack={handleBack}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -7,19 +7,23 @@ interface Family {
|
||||
}
|
||||
|
||||
interface FamilyState {
|
||||
currentFamily: Family | null;
|
||||
currentFamily: Family;
|
||||
|
||||
setCurrentFamily: (family: Family | null) => void;
|
||||
setCurrentFamily: (family: Family) => void;
|
||||
|
||||
clear: () => void;
|
||||
|
||||
inFamily: () => boolean;
|
||||
}
|
||||
|
||||
const defaultFamily: Family = {
|
||||
family_id: 0,
|
||||
};
|
||||
|
||||
export const useFamilyStore = create<FamilyState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
currentFamily: null,
|
||||
currentFamily: defaultFamily,
|
||||
|
||||
setCurrentFamily: (family) =>
|
||||
set({
|
||||
@@ -28,13 +32,13 @@ export const useFamilyStore = create<FamilyState>()(
|
||||
|
||||
clear: () =>
|
||||
set({
|
||||
currentFamily: null,
|
||||
currentFamily: defaultFamily,
|
||||
}),
|
||||
|
||||
inFamily: () => {
|
||||
const currentFamily = get().currentFamily;
|
||||
|
||||
return currentFamily !== null && currentFamily.family_id !== 0;
|
||||
return currentFamily.family_id !== 0;
|
||||
},
|
||||
}),
|
||||
{
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
import dayjs from "dayjs";
|
||||
|
||||
/**
|
||||
* 获取当前日期,默认格式: YYYY-MM-DD
|
||||
*/
|
||||
export function getCurrentDate(format = "YYYY-MM-DD"): string {
|
||||
return dayjs().format(format);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前日期时间,默认格式: YYYY-MM-DD HH:mm:ss
|
||||
*/
|
||||
export function getCurrentDateTime(format = "YYYY-MM-DD HH:mm:ss"): string {
|
||||
return dayjs().format(format);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前时间,默认格式: HH:mm:ss
|
||||
*/
|
||||
export function getCurrentTime(format = "HH:mm:ss"): string {
|
||||
return dayjs().format(format);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前年月,默认格式: YYYY-MM
|
||||
*/
|
||||
export function getCurrentYearMonth(format = "YYYY-MM"): string {
|
||||
return dayjs().format(format);
|
||||
}
|
||||
|
||||
export function getCurrentYear(): string {
|
||||
return dayjs().format("YYYY");
|
||||
}
|
||||
|
||||
/** 字符串 / 数字 / Date → Date(无效返回 null) */
|
||||
export function toDate(input: string | number | Date | undefined | null): Date {
|
||||
if (!input) return new Date();
|
||||
if (input instanceof Date) return input;
|
||||
|
||||
const d = dayjs(input);
|
||||
return d.isValid() ? d.toDate() : new Date();
|
||||
}
|
||||
|
||||
/** Date → YYYY-MM-DD */
|
||||
export function toDateString(date: string | number | Date): string {
|
||||
const d = dayjs(toDate(date));
|
||||
return d.isValid() ? d.format("YYYY-MM-DD") : "";
|
||||
}
|
||||
|
||||
/** Date → YYYY-MM */
|
||||
export function toYearMonthString(date: string | number | Date): string {
|
||||
const d = dayjs(toDate(date));
|
||||
return d.isValid() ? d.format("YYYY-MM") : "";
|
||||
}
|
||||
|
||||
/** Date → HH:mm:ss */
|
||||
export function toTimeString(date: string | number | Date): string {
|
||||
const d = dayjs(toDate(date));
|
||||
return d.isValid() ? d.format("HH:mm:ss") : "";
|
||||
}
|
||||
|
||||
/** Date → YYYY-MM-DD HH:mm:ss */
|
||||
export function toDateTimeString(date: string | number | Date): string {
|
||||
const d = dayjs(toDate(date));
|
||||
return d.isValid() ? d.format("YYYY-MM-DD HH:mm:ss") : "";
|
||||
}
|
||||
|
||||
export function isInMonth(date: string, month: string): boolean {
|
||||
// date 形如 "2025-01-12"
|
||||
// month 形如 "2025-01"
|
||||
return dayjs(date).format("YYYY-MM") === month;
|
||||
}
|
||||
|
||||
// 获取本月的第一天
|
||||
export function getStartOfMonth() {
|
||||
return dayjs().startOf("month").format("YYYY-MM-DD");
|
||||
}
|
||||
|
||||
// 获取本月的最后一天
|
||||
export function getEndOfMonth() {
|
||||
return dayjs().endOf("month").format("YYYY-MM-DD");
|
||||
}
|
||||
|
||||
export function normalizeDateTime(input?: string | Date | dayjs.Dayjs): string {
|
||||
if (!input) return "";
|
||||
|
||||
const raw = typeof input === "string" ? input.trim() : input;
|
||||
let d = dayjs(raw);
|
||||
|
||||
if (!d.isValid()) return "";
|
||||
|
||||
// string 才需要“猜精度”,Date / dayjs 默认是完整的
|
||||
if (typeof raw === "string") {
|
||||
if (/^\d{4}$/.test(raw)) {
|
||||
d = d.startOf("year");
|
||||
} else if (/^\d{4}-\d{2}$/.test(raw)) {
|
||||
d = d.startOf("month");
|
||||
} else if (/^\d{4}-\d{2}-\d{2}$/.test(raw)) {
|
||||
d = d.startOf("day");
|
||||
} else if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/.test(raw)) {
|
||||
d = d.second(0);
|
||||
}
|
||||
}
|
||||
|
||||
return d.format("YYYY-MM-DD HH:mm:ss");
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化时间为相对时间
|
||||
* @param timestamp 秒级或毫秒级时间戳
|
||||
*/
|
||||
export function formatRelativeTime(timestamp: number): string {
|
||||
if (!timestamp) return "未活跃";
|
||||
|
||||
// 兼容秒级时间戳
|
||||
const time = timestamp.toString().length === 10 ? dayjs(timestamp * 1000) : dayjs(timestamp);
|
||||
|
||||
const now = dayjs();
|
||||
|
||||
const diffSeconds = now.diff(time, "second");
|
||||
const diffMinutes = now.diff(time, "minute");
|
||||
const diffHours = now.diff(time, "hour");
|
||||
const diffDays = now.diff(time, "day");
|
||||
|
||||
if (diffSeconds < 30) {
|
||||
return "刚刚";
|
||||
}
|
||||
|
||||
if (diffMinutes < 60) {
|
||||
return `${diffMinutes}分钟前`;
|
||||
}
|
||||
|
||||
if (diffHours < 24) {
|
||||
return `${diffHours}小时前`;
|
||||
}
|
||||
|
||||
if (diffDays === 1) {
|
||||
return "昨天";
|
||||
}
|
||||
|
||||
if (diffDays < 7) {
|
||||
return `${diffDays}天前`;
|
||||
}
|
||||
|
||||
if (time.year() === now.year()) {
|
||||
return time.format("MM-DD HH:mm");
|
||||
}
|
||||
|
||||
return time.format("YYYY-MM-DD");
|
||||
}
|
||||
@@ -18,5 +18,5 @@
|
||||
"env.js",
|
||||
".expo/types/**/*.ts",
|
||||
"expo-env.d.ts"
|
||||
]
|
||||
, "src/app/auth/login_2" ]
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
#url=jdbc:sqlserver://118.89.70.199:1433;databaseName=G3BoxOCR
|
||||
#username=sa
|
||||
#password=zhang520..
|
||||
#driver=com.microsoft.sqlserver.jdbc.SQLServerDriver
|
||||
|
||||
url=jdbc:sqlserver://127.0.0.1:1433;databaseName=G3ContainerOCR
|
||||
url=jdbc:sqlserver://118.89.70.199:1433;databaseName=G3BoxOCR
|
||||
username=sa
|
||||
password=123456@aaa
|
||||
password=zhang520..
|
||||
driver=com.microsoft.sqlserver.jdbc.SQLServerDriver
|
||||
|
||||
#url=jdbc:sqlserver://127.0.0.1:1433;databaseName=G3ContainerOCR
|
||||
#username=sa
|
||||
#password=123456@aaa
|
||||
#driver=com.microsoft.sqlserver.jdbc.SQLServerDriver
|
||||
|
||||
@@ -10,7 +10,7 @@ PUBLIC_SERVER_PORT=8080
|
||||
|
||||
# API
|
||||
# 后端 API 基础地址
|
||||
PUBLIC_API_BASE_URL=http://ser369.g3soft.cn:80/api
|
||||
# PUBLIC_API_BASE_URL=http://192.168.1.137:80/api
|
||||
# PUBLIC_API_BASE_URL=http://ser369.g3soft.cn:80/api
|
||||
PUBLIC_API_BASE_URL=http://192.168.1.137:80/api
|
||||
|
||||
PUBLIC_ALIYUN_BUCKET = g3hd
|
||||
-8
@@ -18,7 +18,6 @@ declare module 'vue' {
|
||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||
ElDivider: typeof import('element-plus/es')['ElDivider']
|
||||
ElDrawer: typeof import('element-plus/es')['ElDrawer']
|
||||
ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
||||
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
||||
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
|
||||
@@ -28,23 +27,16 @@ declare module 'vue' {
|
||||
ElInput: typeof import('element-plus/es')['ElInput']
|
||||
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||
ElOption: typeof import('element-plus/es')['ElOption']
|
||||
ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||
ElPopover: typeof import('element-plus/es')['ElPopover']
|
||||
ElRadio: typeof import('element-plus/es')['ElRadio']
|
||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||
ElResult: typeof import('element-plus/es')['ElResult']
|
||||
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
ElSplitter: typeof import('element-plus/es')['ElSplitter']
|
||||
ElSplitterPanel: typeof import('element-plus/es')['ElSplitterPanel']
|
||||
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||
ElTable: typeof import('element-plus/es')['ElTable']
|
||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
||||
ElTabs: typeof import('element-plus/es')['ElTabs']
|
||||
ElText: typeof import('element-plus/es')['ElText']
|
||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||
ElTree: typeof import('element-plus/es')['ElTree']
|
||||
G3Column: typeof import('./src/components/g3-table/g3-column.vue')['default']
|
||||
|
||||
Reference in new issue
Block a user