This commit is contained in:
oneao committed 2026-04-19 21:30:51 +08:00
1 parent b6415c0d65
commit 89aaef1d6b
29 files changed
+1118 -277

No files matched your search

+25 -6
View File
@@ -6,11 +6,10 @@
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"userInterfaceStyle": "automatic",
"scheme": [
"oneao",
"tencent102826474",
"wxdab3e21a1f7e392f"
],
"scheme": ["oneao", "tencent102826474", "wxdab3e21a1f7e392f"],
"extra": {
"linkingScheme": "oneao"
},
"ios": {
"icon": "./assets/expo.icon"
},
@@ -58,7 +57,27 @@
}
}
],
"expo-wechat"
"expo-wechat",
[
"expo-document-picker",
{
"iCloudContainerEnvironment": "Production"
}
],
[
"expo-image-picker",
{
"photosPermission": "The app accesses your photos to let you share them with your friends.",
"colors": {
"cropToolbarColor": "#000000"
},
"dark": {
"colors": {
"cropToolbarColor": "#000000"
}
}
}
]
],
"experiments": {
"typedRoutes": true,
+2
View File
@@ -27,9 +27,11 @@
"expo-build-properties": "~55.0.13",
"expo-constants": "~55.0.14",
"expo-device": "~55.0.15",
"expo-document-picker": "~55.0.13",
"expo-font": "~55.0.6",
"expo-glass-effect": "~55.0.10",
"expo-image": "~55.0.8",
"expo-image-picker": "~55.0.18",
"expo-linear-gradient": "~55.0.13",
"expo-linking": "~55.0.13",
"expo-qq": "^0.1.8",
@@ -1,95 +1,47 @@
const fs = require("node:fs");
const path = require("node:path");
const { withAppBuildGradle } = require("expo/config-plugins");
// plugins/withAndroidSignature.js
const CREDENTIAL_PATH = path.resolve(
__dirname,
"../credentials/credentials.json"
);
const KEYSTORE_PATH = path.resolve(
__dirname,
"../credentials/oneao-android.keystore"
);
const fs = require('node:fs')
const path = require('node:path')
const { withAppBuildGradle } = require('expo/config-plugins')
module.exports = function withAndroidSignature(config) {
return withAppBuildGradle(config, (config) => {
if (config.modResults.language !== "groovy") {
throw new Error("only groovy build.gradle is supported");
if (config.modResults.language === 'groovy') {
config.modResults.contents = setAndroidSignature(config.modResults.contents)
}
else {
throw new Error('如果不是 groovy,则无法在 app/build.gradle 中设置 signingConfigs')
}
return config
})
}
config.modResults.contents = setAndroidSignature(
config.modResults.contents
);
return config;
});
};
// ======================== 主入口 ========================
function setAndroidSignature(appBuildGradle) {
if (!fs.existsSync(CREDENTIAL_PATH)) {
console.warn("⚠️ credentials.json 不存在,跳过签名配置");
return appBuildGradle;
if (!fs.existsSync(path.resolve(__dirname, '../credentials/credentials.json'))) {
console.warn('警告:没有设置正式版本的 Android Keystore 文件,因为 credentials.json 不存在。')
return appBuildGradle
}
const info = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../credentials/credentials.json'), { encoding: 'utf8' }))
const info = JSON.parse(
fs.readFileSync(CREDENTIAL_PATH, "utf8")
);
let output = appBuildGradle;
// ⭐ 1. 重写 signingConfigs(关键)
output = replaceSigningConfigs(output, info);
// ⭐ 2. 确保所有 debug/release 使用统一 keystore
output = unifySigningUsage(output);
return output;
}
// ======================== ⭐核心:重写 signingConfigs ========================
function replaceSigningConfigs(appBuildGradle, info) {
const block = `
signingConfigs {
// 使用正则表达式插入签名信息
let output = appBuildGradle.replace(
/(signingConfigs\s*\{)/,
`$1
release {
storeFile file(${JSON.stringify(KEYSTORE_PATH)})
storeFile file(${JSON.stringify(path.resolve(__dirname, '../credentials/oneao-android.keystore'))})
storePassword ${JSON.stringify(info.android.keystore.keystorePassword)}
keyAlias ${JSON.stringify(info.android.keystore.keyAlias)}
keyPassword ${JSON.stringify(info.android.keystore.keyPassword)}
}
}`,
)
debug {
storeFile file(${JSON.stringify(KEYSTORE_PATH)})
storePassword ${JSON.stringify(info.android.keystore.keystorePassword)}
keyAlias ${JSON.stringify(info.android.keystore.keyAlias)}
keyPassword ${JSON.stringify(info.android.keystore.keyPassword)}
}
}
`;
const regex = /signingConfigs\s*\{[\s\S]*?\n\s*\}/m;
if (regex.test(appBuildGradle)) {
return appBuildGradle.replace(regex, block.trim());
}
// 没有就插入
return appBuildGradle.replace(
/(android\s*\{)/,
`$1\n${block.trim()}`
);
}
// ======================== ⭐统一使用 signingConfig ========================
function unifySigningUsage(appBuildGradle) {
let output = appBuildGradle;
// debug -> release(你如果想 debug 也用 release,可以改这里)
// 使用正则表达式替换 signingConfig
output = output.replace(
/signingConfig\s+signingConfigs\.debug/g,
"signingConfig signingConfigs.release"
);
/(release\s*\{)[^}]*?signingConfig\s+signingConfigs\.debug/,
`$1
signingConfig signingConfigs.release
`,
)
return output;
}
return output
}
+35 -33
View File
@@ -50,6 +50,9 @@ importers:
expo-device:
specifier: ~55.0.15
version: 55.0.15(expo@55.0.15)
expo-document-picker:
specifier: ~55.0.13
version: 55.0.13(expo@55.0.15)
expo-font:
specifier: ~55.0.6
version: 55.0.6(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)
@@ -59,6 +62,9 @@ importers:
expo-image:
specifier: ~55.0.8
version: 55.0.8(expo@55.0.15)(react-native-web@0.21.2(react-dom@19.2.0(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)
expo-image-picker:
specifier: ~55.0.18
version: 55.0.18(expo@55.0.15)
expo-linear-gradient:
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)
@@ -1010,56 +1016,48 @@ 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==}
@@ -1132,56 +1130,48 @@ 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==}
@@ -1600,28 +1590,24 @@ 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==}
@@ -2256,6 +2242,11 @@ packages:
peerDependencies:
expo: '*'
expo-document-picker@55.0.13:
resolution: {integrity: sha512-IhswJElhdzs3fKDEKW8KXYRoFkWGEsXRMYAZT46Yo56zqqy8yQXrczo33RSwD2hFzNQBdLT97SJL9N311UyS3g==}
peerDependencies:
expo: '*'
expo-file-system@55.0.16:
resolution: {integrity: sha512-EetQ/zVFK07Vmz4Yke0fvoES4xVwScTdd0PMoLekuMX7puE4op75pNnEdh1M0AeWzkqLrBoZIaU2ynSrKN5VZg==}
peerDependencies:
@@ -2276,6 +2267,16 @@ packages:
react: '*'
react-native: '*'
expo-image-loader@55.0.0:
resolution: {integrity: sha512-NOjp56wDrfuA5aiNAybBIjqIn1IxKeGJ8CECWZncQ/GzjZfyTYAHTCyeApYkdKkMBLHINzI4BbTGSlbCa0fXXQ==}
peerDependencies:
expo: '*'
expo-image-picker@55.0.18:
resolution: {integrity: sha512-lGpPGRu+7mE8qN0ma2boRsCmfOGbdHZ2bXTpWVeWly0JCZdogGlTrYFnhTqgS8+lmiRb/UCOs7iTm2P5Rra6kw==}
peerDependencies:
expo: '*'
expo-image@55.0.8:
resolution: {integrity: sha512-fNdvdYVcGn3g1x6o5AXHKzk4xX8U6rg2W9vFdE1pQO80kWCNReh003ypqSrGy4dD+zA8FtZjrNF3oMDGnPpIGQ==}
peerDependencies:
@@ -2539,7 +2540,7 @@ packages:
glob@7.2.3:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
deprecated: Glob versions prior to v9 are no longer supported
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
gopd@1.2.0:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
@@ -2872,84 +2873,72 @@ 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==}
@@ -6497,6 +6486,10 @@ snapshots:
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)
ua-parser-js: 0.7.41
expo-document-picker@55.0.13(expo@55.0.15):
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)
expo-file-system@55.0.16(expo@55.0.15)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(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)
@@ -6515,6 +6508,15 @@ snapshots:
react: 19.2.0
react-native: 0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0)
expo-image-loader@55.0.0(expo@55.0.15):
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)
expo-image-picker@55.0.18(expo@55.0.15):
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)
expo-image-loader: 55.0.0(expo@55.0.15)
expo-image@55.0.8(expo@55.0.15)(react-native-web@0.21.2(react-dom@19.2.0(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):
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)
@@ -0,0 +1,25 @@
import { memo } from "react";
import {
KeyboardAwareScrollView,
KeyboardAwareScrollViewProps,
} from "react-native-keyboard-controller";
import {
SCROLLABLE_TYPE,
createBottomSheetScrollableComponent,
type BottomSheetScrollViewMethods,
} from "@gorhom/bottom-sheet";
import type { BottomSheetScrollViewProps } from "@gorhom/bottom-sheet/src/components/bottomSheetScrollable/types";
import Reanimated from "react-native-reanimated";
const AnimatedScrollView = Reanimated.createAnimatedComponent(KeyboardAwareScrollView);
const BottomSheetScrollViewComponent = createBottomSheetScrollableComponent<
BottomSheetScrollViewMethods,
BottomSheetScrollViewProps
>(SCROLLABLE_TYPE.SCROLLVIEW, AnimatedScrollView);
const BottomSheetKeyboardAwareScrollView = memo(BottomSheetScrollViewComponent);
BottomSheetKeyboardAwareScrollView.displayName = "BottomSheetKeyboardAwareScrollView";
export default BottomSheetKeyboardAwareScrollView as (
props: BottomSheetScrollViewProps & KeyboardAwareScrollViewProps,
) => ReturnType<typeof BottomSheetKeyboardAwareScrollView>;
+47 -36
View File
@@ -1,43 +1,54 @@
import { Button, Toast, useToast, useThemeColor } from "heroui-native";
import { View } from "react-native";
import { BottomSheet, Button, TextField, Input, useBottomSheetAwareHandlers } from "heroui-native";
import { useState, useRef, useEffect } from "react";
import { Keyboard } from "react-native";
export default function ToastExample() {
const { toast } = useToast();
const themeColorForeground = useThemeColor("foreground");
const BottomSheetTextInput = ({ isOpen }: { isOpen: boolean }) => {
const { onFocus, onBlur } = useBottomSheetAwareHandlers();
const inputRef = useRef<any>(null);
useEffect(() => {
if (isOpen) {
inputRef.current?.focus();
} else {
inputRef.current?.blur();
}
}, [isOpen]);
return (
<View className="gap-4 p-4">
<Button
onPress={() =>
toast.show({
variant: "success",
label: "You have upgraded your plan",
description: "You can continue using HeroUI Chat",
actionLabel: "Close",
onActionPress: ({ hide }) => hide(),
})
}
>
Show Success Toast
</Button>
<TextField>
<Input ref={inputRef} placeholder="Type here..." onFocus={onFocus} onBlur={onBlur} />
</TextField>
);
};
<Button
onPress={() =>
toast.show({
duration: 0,
component: (props) => (
<Toast variant="accent" {...props}>
<Toast.Title>Custom Toast</Toast.Title>
<Toast.Description>This uses a custom component</Toast.Description>
<Toast.Action onPress={() => props.hide()}>Undo</Toast.Action>
<Toast.Close className="absolute top-0 right-0" />
</Toast>
),
})
export default function BottomSheetExample() {
const [isOpen, setIsOpen] = useState(false);
return (
<BottomSheet
isOpen={isOpen}
onOpenChange={(open) => {
setIsOpen(open);
if (!open) {
Keyboard.dismiss(); // ✅ 关闭时收键盘
}
>
Show Custom Toast
</Button>
</View>
}}
animation="disable-all"
>
<BottomSheet.Trigger asChild>
<Button variant="secondary" className="mt-30">
Open Bottom Sheet
</Button>
</BottomSheet.Trigger>
<BottomSheet.Portal>
<BottomSheet.Overlay />
<BottomSheet.Content enableBlurKeyboardOnGesture keyboardBlurBehavior="restore">
<BottomSheetTextInput isOpen={isOpen} />
</BottomSheet.Content>
</BottomSheet.Portal>
</BottomSheet>
);
}
@@ -0,0 +1,39 @@
import { BottomSheet, Button, TextField, Input, useBottomSheetAwareHandlers } from "heroui-native";
import { useState } from "react";
const BottomSheetTextInput = () => {
const { onFocus, onBlur } = useBottomSheetAwareHandlers();
return (
<TextField>
<Input placeholder="Type here..." onFocus={onFocus} onBlur={onBlur} />
</TextField>
);
};
export default function BottomSheetExample() {
const [isOpen, setIsOpen] = useState(false);
return (
<BottomSheet
isOpen={isOpen}
onOpenChange={(open) => {
setIsOpen(open);
}}
>
<BottomSheet.Trigger asChild>
<Button variant="secondary" className="mt-30">
Open Bottom Sheet
</Button>
</BottomSheet.Trigger>
<BottomSheet.Portal>
<BottomSheet.Overlay />
<BottomSheet.Content keyboardBlurBehavior="restore">
<BottomSheetTextInput />
</BottomSheet.Content>
</BottomSheet.Portal>
</BottomSheet>
);
}
@@ -1,6 +1,8 @@
import React from "react";
import React, { useEffect } from "react";
import LayoutView from "@/components/layout/view";
import EmptyState from "@/components/EmptyStatus";
import { s3ListBucketsApi } from "@/request/api";
export default function Home() {
// const [dailyData, setDailyData] = useState<any>(null);
@@ -15,6 +17,12 @@ export default function Home() {
// .catch(() => {});
// }, []);
useEffect(() => {
s3ListBucketsApi().then((res) => {
console.log(res);
});
}, []);
return (
<LayoutView>
<LayoutView.ScrollView>
+5 -3
View File
@@ -30,12 +30,14 @@ export default function My() {
<LayoutView.ScrollView>
<View className="flex-row items-center">
<Avatar alt="">
<Avatar.Image source={{ uri: "https://example.com/avatar.jpg" }} />
<Avatar.Fallback className="text-sm">JD</Avatar.Fallback>
<Avatar.Image source={{ uri: userStore.userInfo.avatar }} />
<Avatar.Fallback className="text-sm">{userStore.userInfo.nickname}</Avatar.Fallback>
</Avatar>
<View className="ml-3 mr-1">
<Text className="text-xl font-semibold text-foreground">JD</Text>
<Text className="text-xl font-semibold text-foreground">
{userStore.userInfo.nickname}
</Text>
</View>
<ChevronRightIcon size={18} color="#c7c7cc" />
+75 -35
View File
@@ -26,6 +26,48 @@ 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();
@@ -54,28 +96,38 @@ export default function Space() {
const [createLoading, setCreateLoading] = useState(false);
const createInputRef = useRef<any>(null);
const { onFocus, onBlur } = useBottomSheetAwareHandlers();
useEffect(() => {
listUserSpaces();
}, []);
useEffect(() => {
if (showCreate) {
setTimeout(() => {
requestAnimationFrame(() => {
createInputRef.current?.focus();
}, 0);
});
} else {
setCreateForm({
name: "",
});
setCreateLoading(false);
Keyboard.dismiss();
}
}, [showCreate]);
useEffect(() => {
if (showJoin) {
setTimeout(() => {
requestAnimationFrame(() => {
joinInputRef.current?.focus();
}, 0);
});
} else {
setJoinForm({
invite_code: "",
});
setJoinLoading(false);
Keyboard.dismiss();
}
}, [showJoin]);
useEffect(() => {
listUserSpaces();
}, []);
function listUserSpaces(loading = true) {
if (loading) {
setLoading(true);
@@ -396,7 +448,7 @@ export default function Space() {
className="flex-1"
onPress={() => {
setShowCreate(true);
createInputRef.current?.focus();
// createInputRef.current?.focus();
}}
>
<PlusIcon color="#fff" size={20} />
@@ -463,32 +515,25 @@ export default function Space() {
/>
)}
</LayoutView.ScrollView>
<BottomSheet
isOpen={showCreate}
onOpenChange={(val) => {
if (!val) {
setCreateForm({
name: "",
});
setCreateLoading(false);
}
setShowCreate(val);
}}
>
<BottomSheet.Portal>
<BottomSheet.Overlay />
<BottomSheet.Content>
<BottomSheet.Content
animation="disabled"
enableBlurKeyboardOnGesture
keyboardBlurBehavior="restore"
>
<View>
<TextField>
<Label>空间名称</Label>
<Input
<BottomSheetTextInput
ref={createInputRef}
placeholder="请输入空间名称"
onFocus={onFocus}
onBlur={onBlur}
value={createForm.name}
onChangeText={(val) =>
setCreateForm({
@@ -510,28 +555,23 @@ export default function Space() {
<BottomSheet
isOpen={showJoin}
onOpenChange={(val) => {
if (!val) {
setJoinForm({
invite_code: "",
});
setJoinLoading(false);
}
setShowJoin(val);
}}
>
<BottomSheet.Portal>
<BottomSheet.Overlay />
<BottomSheet.Content>
<BottomSheet.Content
animation={"disabled"}
enableBlurKeyboardOnGesture
keyboardBlurBehavior="restore"
>
<View>
<TextField>
<Label>邀请码</Label>
<InputOTP
<BottomSheetTextInputOTP
ref={joinInputRef}
maxLength={6}
onFocus={onFocus}
onBlur={onBlur}
value={joinForm.invite_code}
onChange={(val) => {
setJoinForm({
@@ -550,7 +590,7 @@ export default function Space() {
<InputOTP.Slot index={4} />
<InputOTP.Slot index={5} />
</InputOTP.Group>
</InputOTP>
</BottomSheetTextInputOTP>
</TextField>
<Button className="mt-3" onPress={handleJoin}>
+12 -11
View File
@@ -11,9 +11,16 @@ import { Toaster } from "sonner-native";
import { KeyboardProvider } from "react-native-keyboard-controller";
import { useUserStore } from "@/store";
import { useRouter } from "expo-router";
import { HeroUINativeConfig } from "heroui-native";
SplashScreen.preventAutoHideAsync();
const config: HeroUINativeConfig = {
devInfo: {
stylingPrinciples: false,
},
};
export default function RootLayout() {
const router = useRouter();
const userStore = useUserStore();
@@ -46,15 +53,9 @@ export default function RootLayout() {
return (
<GestureHandlerRootView className="flex-1">
<KeyboardProvider>
<ThemeProvider value={isDark ? DarkTheme : DefaultTheme}>
<HeroUINativeProvider
config={{
devInfo: {
stylingPrinciples: false,
},
}}
>
<ThemeProvider value={isDark ? DarkTheme : DefaultTheme}>
<KeyboardProvider>
<HeroUINativeProvider config={config}>
<Stack
screenOptions={{
animation: "slide_from_right",
@@ -66,8 +67,8 @@ export default function RootLayout() {
/>
{!isDev && <StatusBar style={isDark ? "light" : "dark"} />}
</HeroUINativeProvider>
</ThemeProvider>
</KeyboardProvider>
</KeyboardProvider>
</ThemeProvider>
<Toaster duration={1000} />
</GestureHandlerRootView>
);
+179 -14
View File
@@ -1,4 +1,4 @@
import React from "react";
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";
@@ -7,19 +7,55 @@ 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 onLoginFinished = useEvent(ExpoQQ, "onLoginFinished");
const onGetUserInfo = useEvent(ExpoQQ, "onGetUserInfo");
const authResult = useEvent(ExpoWeChat, "onAuthResult");
const [loading, setLoading] = useState(false);
const [isAgreed, setIsAgreed] = useState(true);
const [form, setForm] = useState({
type: 0,
openid: "",
code: "",
});
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 登录
@@ -30,23 +66,152 @@ export default function Login() {
setLoading(true);
setTimeout(() => {
requestAnimationFrame(() => {
userStore.setUserInfo({
id: 779895419121733,
token: "123",
nickname: "Zhang",
avatar: "",
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: "ZwmN5_0MGrP9dYpn3quK94HXMG3QYKmAq7wnP5X68dY",
// nickname: "Zhang",
// avatar: "http://thirdqq.qlogo.cn/ek_qqapp/AQHVialeUibE42oV8RvTTVZ4tyRlPYeJrUibWTwtCPfV68E4rSzn9cw00GkXQ3QDTiawwhjoqQb7/100",
// });
// userStore.setAuthExpired(false);
// router.replace("/(tabs)/home");
// });
// 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,
});
userStore.setAuthExpired(false);
console.log(loginRes);
router.replace("/(tabs)/home");
});
if (loginRes.isSuccess) {
requestAnimationFrame(() => {
userStore.setUserInfo({
token: loginRes.data.token,
id: loginRes.data.userId,
nickname: loginRes.data.nickname,
avatar: loginRes.data.avatar,
});
setLoading(false);
}, 2000);
}
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 (
<>
+18
View File
@@ -47,3 +47,21 @@ interface SaveDataParams {
export function saveDataApi(params: SaveDataParams[]) {
return alovaInstance.Post<ResponseData<null>>(`/data/saveData`, params);
}
export function s3ListBucketsApi() {
return alovaInstance.Post<ResponseData<string[]>>(`/s3/listBuckets`);
}
export function s3UploadFileApi(files: { uri: string; name: string; type: string }[]) {
const form = new FormData();
files.forEach((file) => {
form.append("files", file as any);
});
return alovaInstance.Post<ResponseData<string[]>>("/s3/upload", form, {
headers: {
"Content-Type": "multipart/form-data",
},
});
}
+272
View File
@@ -0,0 +1,272 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>家庭系统 Pro Demo</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
.hidden {
display: none;
}
.toast {
position: fixed;
bottom: 80px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 8px 14px;
border-radius: 10px;
font-size: 12px;
}
</style>
</head>
<body class="bg-gray-100">
<div id="app" class="max-w-md mx-auto min-h-screen flex flex-col">
<!-- ================= LOGIN ================= -->
<div id="login" class="flex-1 flex items-center justify-center p-6">
<div class="bg-white p-6 rounded-2xl w-full shadow">
<h1 class="text-xl font-bold mb-1">欢迎回来</h1>
<p class="text-sm text-gray-400 mb-4">家庭系统登录</p>
<input id="username" class="w-full border p-2 rounded mb-3" placeholder="请输入名字">
<button onclick="login()" class="w-full bg-blue-500 text-white py-2 rounded">
登录
</button>
</div>
</div>
<!-- ================= FAMILY ONBOARD ================= -->
<div id="onboard" class="hidden flex-1 p-6">
<div class="bg-white p-6 rounded-2xl shadow">
<h1 class="text-lg font-bold">创建你的家庭</h1>
<p class="text-sm text-gray-400 mb-4">你还没有加入任何家庭</p>
<input id="familyName" class="w-full border p-2 rounded mb-3" placeholder="家庭名称">
<button onclick="createFamily()" class="w-full bg-green-500 text-white py-2 rounded mb-2">
创建家庭
</button>
<button onclick="joinFamilyMock()" class="w-full bg-gray-200 py-2 rounded">
加入示例家庭
</button>
</div>
</div>
<!-- ================= MAIN ================= -->
<div id="main" class="hidden flex-1 flex flex-col">
<!-- CONTENT -->
<div class="flex-1 p-4 space-y-3">
<!-- 家庭卡 -->
<div class="bg-white p-4 rounded-2xl shadow">
<div class="flex justify-between">
<div>
<h2 class="font-bold text-lg">🏠 <span id="familyTitle"></span></h2>
<p class="text-xs text-gray-400">邀请码:ABC123 · 3人</p>
</div>
<button onclick="openSettings()" class="text-xs bg-gray-100 px-3 py-1 rounded-lg">
设置
</button>
</div>
</div>
<!-- 成员 -->
<div class="bg-white p-4 rounded-2xl shadow">
<div class="flex justify-between mb-3">
<div class="font-semibold">家庭成员</div>
<button class="text-blue-500 text-sm">邀请</button>
</div>
<div class="flex gap-4">
<div class="text-center">
<div class="w-10 h-10 bg-blue-200 rounded-full"></div>
<div class="text-xs mt-1">我</div>
</div>
<div class="text-center">
<div class="w-10 h-10 bg-pink-200 rounded-full"></div>
<div class="text-xs mt-1">妈妈</div>
</div>
<div class="text-center">
<div class="w-10 h-10 bg-yellow-200 rounded-full"></div>
<div class="text-xs mt-1">爸爸</div>
</div>
</div>
</div>
<!-- 子应用 -->
<div class="bg-white p-4 rounded-2xl shadow">
<div class="font-semibold mb-3">家庭工具</div>
<div class="grid grid-cols-3 gap-3">
<div class="bg-gray-50 p-3 rounded-xl text-center" onclick="openApp('记账')">
📒<div class="text-xs mt-1">记账</div>
</div>
<div class="bg-gray-50 p-3 rounded-xl text-center" onclick="openApp('清单')">
🛒<div class="text-xs mt-1">清单</div>
</div>
<div class="bg-gray-50 p-3 rounded-xl text-center" onclick="openApp('日程')">
📅<div class="text-xs mt-1">日程</div>
</div>
<div class="bg-gray-50 p-3 rounded-xl text-center" onclick="openApp('物品')">
📦<div class="text-xs mt-1">物品</div>
</div>
<div class="bg-gray-50 p-3 rounded-xl text-center" onclick="openApp('待办')">
📝<div class="text-xs mt-1">待办</div>
</div>
<div class="bg-gray-100 p-3 rounded-xl text-center text-gray-400">
➕<div class="text-xs mt-1">添加</div>
</div>
</div>
</div>
</div>
<!-- TABBAR -->
<div class="flex border-t bg-white">
<button class="flex-1 py-2" onclick="switchTab('home')">🏠</button>
<button class="flex-1 py-2" onclick="switchTab('apps')">🧩</button>
<button class="flex-1 py-2" onclick="switchTab('me')">👤</button>
</div>
</div>
<!-- ================= SETTINGS ================= -->
<div id="settings" class="hidden flex-1 p-4">
<div class="bg-white p-4 rounded-2xl shadow">
<h2 class="font-bold mb-3">家庭设置</h2>
<div class="space-y-2 text-sm">
<div class="p-2 bg-gray-100 rounded">修改家庭名称</div>
<div class="p-2 bg-gray-100 rounded">成员管理</div>
<div class="p-2 bg-gray-100 rounded text-red-500">退出家庭</div>
</div>
<button onclick="backHome()" class="mt-4 w-full bg-blue-500 text-white py-2 rounded">
返回
</button>
</div>
</div>
</div>
<!-- toast -->
<div id="toast" class="toast hidden"></div>
<script>
let state = {
user: null,
family: null
}
function show(id) {
document.querySelectorAll('#app > div').forEach(v => v.classList.add('hidden'));
document.getElementById(id).classList.remove('hidden');
}
function toast(msg) {
const t = document.getElementById('toast');
t.innerText = msg;
t.classList.remove('hidden');
setTimeout(() => {
t.classList.add('hidden');
}, 1500);
}
/* ========== login ========== */
function login() {
const name = document.getElementById('username').value;
if (!name) return toast('请输入用户名');
state.user = name;
show('onboard');
}
/* ========== family ========== */
function createFamily() {
const name = document.getElementById('familyName').value;
if (!name) return toast('请输入家庭名称');
state.family = name;
enterApp();
}
function joinFamilyMock() {
state.family = "幸福家庭";
enterApp();
}
function enterApp() {
document.getElementById('familyTitle').innerText = state.family;
show('main');
switchTab('home');
}
/* ========== tab ========== */
function switchTab(tab) {
document.querySelectorAll('#main > div:nth-child(1) > div').forEach(v => {
v.style.display = 'none';
});
toast("切换:" + tab);
}
function openApp(name) {
toast("进入:" + name);
}
/* ========== settings ========== */
function openSettings() {
show('settings');
}
function backHome() {
show('main');
}
</script>
</body>
</html>