This commit is contained in:
oneao committed 2026-04-21 17:29:49 +08:00
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()
+14 -4
View File
@@ -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
}
+2
View File
@@ -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",
+57 -1
View File
@@ -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)
+58 -90
View File
@@ -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>
+33 -3
View File
@@ -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>
);
}
+37 -179
View File
@@ -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>
-280
View File
@@ -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>
</>
);
}
+262
View File
@@ -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>
);
}
+15
View File
@@ -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>
);
}
+29 -12
View File
@@ -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>
);
}
+9 -5
View File
@@ -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;
},
}),
{
+150
View File
@@ -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");
}
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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
View File
@@ -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']