refactor(ui): implement custom header system and unify screen layouts
Some checks failed
Frontend CI / ota-android (push) Successful in 1m30s
Frontend CI / build-and-push-web (push) Successful in 12m29s
Frontend CI / build-android-apk (push) Failing after 3h10m38s

Refactor the navigation and header strategy by replacing native stack headers with a custom `SimpleHeader` component across most profile and detail screens. This provides a more consistent UI/UX and better control over layout behavior.

- Implement `SimpleHeader` component in `src/components/common`.
- Disable native header rendering in `app/(app)/(tabs)/profile/_layout.tsx` and `app/_layout.tsx`.
- Update profile sub-screens to use `SimpleHeader` and `StatusBar` from `expo-status-bar`.
- Refactor `PostDetailScreen` to use a custom header implementation for better integration with the post author information.
- Update `UserScreen` to wrap content in `SafeAreaView` with the new header.
- Adjust `AppBackButton` to support transparent backgrounds.
This commit is contained in:
2026-05-03 22:01:43 +08:00
parent 490a99ab3c
commit 4213d13b8f
21 changed files with 522 additions and 173 deletions

293
.kilo/package-lock.json generated
View File

@@ -5,21 +5,22 @@
"packages": { "packages": {
"": { "": {
"dependencies": { "dependencies": {
"@kilocode/plugin": "7.2.10" "@kilocode/plugin": "7.2.31"
} }
}, },
"node_modules/@kilocode/plugin": { "node_modules/@kilocode/plugin": {
"version": "7.2.10", "version": "7.2.31",
"resolved": "https://registry.npmjs.org/@kilocode/plugin/-/plugin-7.2.10.tgz", "resolved": "https://registry.npmmirror.com/@kilocode/plugin/-/plugin-7.2.31.tgz",
"integrity": "sha512-VJPhJC+E5WWu7XgEJzrVOxKJlwJ+OATwxEzgjqEPj8KN5N38YxUPBY/rzUTjv90x7nkzyk1rFGfCVqXdA/Koug==", "integrity": "sha512-KmKTTIly7hRlJdXKhqZ/j/brvTPh0z0UTjWSjJWq5fqf4pATgYGn7G0g3ZjILnN7MUkkZXuljgqExTEeQJHGkQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@kilocode/sdk": "7.2.10", "@kilocode/sdk": "7.2.31",
"effect": "4.0.0-beta.48",
"zod": "4.1.8" "zod": "4.1.8"
}, },
"peerDependencies": { "peerDependencies": {
"@opentui/core": ">=0.1.97", "@opentui/core": ">=0.1.100",
"@opentui/solid": ">=0.1.97" "@opentui/solid": ">=0.1.100"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"@opentui/core": { "@opentui/core": {
@@ -31,17 +32,101 @@
} }
}, },
"node_modules/@kilocode/sdk": { "node_modules/@kilocode/sdk": {
"version": "7.2.10", "version": "7.2.31",
"resolved": "https://registry.npmjs.org/@kilocode/sdk/-/sdk-7.2.10.tgz", "resolved": "https://registry.npmmirror.com/@kilocode/sdk/-/sdk-7.2.31.tgz",
"integrity": "sha512-H6jGXYAhN/yjOGX3MRZ0OxyEAuRGY3VOwDbLTh4O6ljpgutFHaLvomDZ82qNVy7gl7AjJgi3SAQAt9UQpeGl/w==", "integrity": "sha512-Sx05yv+3TIlc6M4Ze+YGgKCLoIg8B0WRE15JXDriVncT+wz7M6+e+4mjNWkwfsuywdeOTYPfeFViqX8iO7ckKA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"cross-spawn": "7.0.6" "cross-spawn": "7.0.6"
} }
}, },
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": {
"version": "3.0.3",
"resolved": "https://registry.npmmirror.com/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz",
"integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": {
"version": "3.0.3",
"resolved": "https://registry.npmmirror.com/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz",
"integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": {
"version": "3.0.3",
"resolved": "https://registry.npmmirror.com/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz",
"integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": {
"version": "3.0.3",
"resolved": "https://registry.npmmirror.com/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz",
"integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": {
"version": "3.0.3",
"resolved": "https://registry.npmmirror.com/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz",
"integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": {
"version": "3.0.3",
"resolved": "https://registry.npmmirror.com/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz",
"integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@standard-schema/spec": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/@standard-schema/spec/-/spec-1.1.0.tgz",
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
"license": "MIT"
},
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -53,24 +138,163 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"license": "Apache-2.0",
"optional": true,
"engines": {
"node": ">=8"
}
},
"node_modules/effect": {
"version": "4.0.0-beta.48",
"resolved": "https://registry.npmmirror.com/effect/-/effect-4.0.0-beta.48.tgz",
"integrity": "sha512-MMAM/ZabuNdNmgXiin+BAanQXK7qM8mlt7nfXDoJ/Gn9V8i89JlCq+2N0AiWmqFLXjGLA0u3FjiOjSOYQk5uMw==",
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.1.0",
"fast-check": "^4.6.0",
"find-my-way-ts": "^0.1.6",
"ini": "^6.0.0",
"kubernetes-types": "^1.30.0",
"msgpackr": "^1.11.9",
"multipasta": "^0.2.7",
"toml": "^4.1.1",
"uuid": "^13.0.0",
"yaml": "^2.8.3"
}
},
"node_modules/fast-check": {
"version": "4.7.0",
"resolved": "https://registry.npmmirror.com/fast-check/-/fast-check-4.7.0.tgz",
"integrity": "sha512-NsZRtqvSSoCP0HbNjUD+r1JH8zqZalyp6gLY9e7OYs7NK9b6AHOs2baBFeBG7bVNsuoukh89x2Yg3rPsul8ziQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/dubzzz"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fast-check"
}
],
"license": "MIT",
"dependencies": {
"pure-rand": "^8.0.0"
},
"engines": {
"node": ">=12.17.0"
}
},
"node_modules/find-my-way-ts": {
"version": "0.1.6",
"resolved": "https://registry.npmmirror.com/find-my-way-ts/-/find-my-way-ts-0.1.6.tgz",
"integrity": "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==",
"license": "MIT"
},
"node_modules/ini": {
"version": "6.0.0",
"resolved": "https://registry.npmmirror.com/ini/-/ini-6.0.0.tgz",
"integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==",
"license": "ISC",
"engines": {
"node": "^20.17.0 || >=22.9.0"
}
},
"node_modules/isexe": { "node_modules/isexe": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/kubernetes-types": {
"version": "1.30.0",
"resolved": "https://registry.npmmirror.com/kubernetes-types/-/kubernetes-types-1.30.0.tgz",
"integrity": "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==",
"license": "Apache-2.0"
},
"node_modules/msgpackr": {
"version": "1.11.12",
"resolved": "https://registry.npmmirror.com/msgpackr/-/msgpackr-1.11.12.tgz",
"integrity": "sha512-RBdJ1Un7yGlXWajrkxcSa93nvQ0w4zBf60c0yYv7YtBelP8H2FA7XsfBbMHtXKXUMUxH7zV3Zuozh+kUQWhHvg==",
"license": "MIT",
"optionalDependencies": {
"msgpackr-extract": "^3.0.2"
}
},
"node_modules/msgpackr-extract": {
"version": "3.0.3",
"resolved": "https://registry.npmmirror.com/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz",
"integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"node-gyp-build-optional-packages": "5.2.2"
},
"bin": {
"download-msgpackr-prebuilds": "bin/download-prebuilds.js"
},
"optionalDependencies": {
"@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3",
"@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3",
"@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3",
"@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3",
"@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3",
"@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3"
}
},
"node_modules/multipasta": {
"version": "0.2.7",
"resolved": "https://registry.npmmirror.com/multipasta/-/multipasta-0.2.7.tgz",
"integrity": "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA==",
"license": "MIT"
},
"node_modules/node-gyp-build-optional-packages": {
"version": "5.2.2",
"resolved": "https://registry.npmmirror.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz",
"integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==",
"license": "MIT",
"optional": true,
"dependencies": {
"detect-libc": "^2.0.1"
},
"bin": {
"node-gyp-build-optional-packages": "bin.js",
"node-gyp-build-optional-packages-optional": "optional.js",
"node-gyp-build-optional-packages-test": "build-test.js"
}
},
"node_modules/path-key": { "node_modules/path-key": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/pure-rand": {
"version": "8.4.0",
"resolved": "https://registry.npmmirror.com/pure-rand/-/pure-rand-8.4.0.tgz",
"integrity": "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/dubzzz"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fast-check"
}
],
"license": "MIT"
},
"node_modules/shebang-command": { "node_modules/shebang-command": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -82,16 +306,38 @@
}, },
"node_modules/shebang-regex": { "node_modules/shebang-regex": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/toml": {
"version": "4.1.1",
"resolved": "https://registry.npmmirror.com/toml/-/toml-4.1.1.tgz",
"integrity": "sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw==",
"license": "MIT",
"engines": {
"node": ">=20"
}
},
"node_modules/uuid": {
"version": "13.0.1",
"resolved": "https://registry.npmmirror.com/uuid/-/uuid-13.0.1.tgz",
"integrity": "sha512-9ezox2roIft6ExBVTVqibSd5dc5/47Sw/uY6b4SjQUT2TzQ0tltNquWA46y4xPQmdZYqvnio22SgWd41M86+jw==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist-node/bin/uuid"
}
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
@@ -104,6 +350,21 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/yaml": {
"version": "2.8.4",
"resolved": "https://registry.npmmirror.com/yaml/-/yaml-2.8.4.tgz",
"integrity": "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==",
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
},
"funding": {
"url": "https://github.com/sponsors/eemeli"
}
},
"node_modules/zod": { "node_modules/zod": {
"version": "4.1.8", "version": "4.1.8",
"license": "MIT", "license": "MIT",

View File

@@ -1,49 +1,28 @@
import { useMemo } from 'react';
import { Stack } from 'expo-router'; import { Stack } from 'expo-router';
import { useRouter } from 'expo-router';
import { AppBackButton } from '../../../../src/components/common';
import { useAppColors } from '../../../../src/theme';
export default function ProfileStackLayout() { export default function ProfileStackLayout() {
const router = useRouter();
const colors = useAppColors();
const headerOptions = useMemo(
() => ({
headerStyle: { backgroundColor: colors.background.paper },
headerTintColor: colors.text.primary,
headerTitleStyle: { fontWeight: '600' as const },
headerShadowVisible: false,
headerBackTitle: '',
}),
[colors]
);
return ( return (
<Stack <Stack
screenOptions={{ screenOptions={{
...headerOptions, headerShown: false,
headerBackVisible: false,
headerLeft: () => <AppBackButton onPress={() => router.back()} />,
}} }}
> >
<Stack.Screen name="index" options={{ headerShown: false }} /> <Stack.Screen name="index" />
<Stack.Screen name="settings" options={{ headerShown: true, title: '设置' }} /> <Stack.Screen name="settings" />
<Stack.Screen name="edit-profile" options={{ title: '编辑资料' }} /> <Stack.Screen name="edit-profile" />
<Stack.Screen name="account-security" options={{ title: '账号安全' }} /> <Stack.Screen name="account-security" />
<Stack.Screen name="my-posts" options={{ title: '我的帖子' }} /> <Stack.Screen name="my-posts" />
<Stack.Screen name="bookmarks" options={{ title: '收藏' }} /> <Stack.Screen name="bookmarks" />
<Stack.Screen name="notification-settings" options={{ title: '通知设置' }} /> <Stack.Screen name="notification-settings" />
<Stack.Screen name="blocked-users" options={{ title: '黑名单' }} /> <Stack.Screen name="blocked-users" />
<Stack.Screen name="chat-settings" options={{ title: '聊天设置' }} /> <Stack.Screen name="chat-settings" />
<Stack.Screen name="about" options={{ title: '关于我们' }} /> <Stack.Screen name="about" />
<Stack.Screen name="terms" options={{ title: '用户协议' }} /> <Stack.Screen name="terms" />
<Stack.Screen name="privacy" options={{ title: '隐私政策' }} /> <Stack.Screen name="privacy" />
<Stack.Screen name="verification" options={{ title: '身份认证' }} /> <Stack.Screen name="verification" />
<Stack.Screen name="data-storage" options={{ title: '数据与存储' }} /> <Stack.Screen name="data-storage" />
<Stack.Screen name="privacy-settings" options={{ title: '隐私设置' }} /> <Stack.Screen name="privacy-settings" />
<Stack.Screen name="account-deletion" options={{ title: '注销账号' }} /> <Stack.Screen name="account-deletion" />
</Stack> </Stack>
); );
} }

View File

@@ -19,7 +19,7 @@ import {
usePaperThemeFromStore, usePaperThemeFromStore,
useResolvedColorScheme, useResolvedColorScheme,
} from '../src/theme'; } from '../src/theme';
import { AppBackButton } from '../src/components/common';
import AppPromptBar from '../src/components/common/AppPromptBar'; import AppPromptBar from '../src/components/common/AppPromptBar';
import AppDialogHost from '../src/components/common/AppDialogHost'; import AppDialogHost from '../src/components/common/AppDialogHost';
import { installAlertOverride } from '@/services/ui'; import { installAlertOverride } from '@/services/ui';
@@ -232,7 +232,6 @@ function APKUpdateBootstrap() {
function ThemedStack() { function ThemedStack() {
const router = useRouter(); const router = useRouter();
const colors = useAppColors();
const resolved = useResolvedColorScheme(); const resolved = useResolvedColorScheme();
return ( return (
@@ -250,49 +249,25 @@ function ThemedStack() {
<Stack.Screen <Stack.Screen
name="terms" name="terms"
options={{ options={{
headerShown: true, headerShown: false,
title: '用户协议',
headerStyle: { backgroundColor: colors.background.paper },
headerTintColor: colors.text.primary,
headerShadowVisible: false,
headerBackVisible: false,
headerLeft: () => <AppBackButton onPress={() => router.back()} />,
}} }}
/> />
<Stack.Screen <Stack.Screen
name="privacy" name="privacy"
options={{ options={{
headerShown: true, headerShown: false,
title: '隐私政策',
headerStyle: { backgroundColor: colors.background.paper },
headerTintColor: colors.text.primary,
headerShadowVisible: false,
headerBackVisible: false,
headerLeft: () => <AppBackButton onPress={() => router.back()} />,
}} }}
/> />
<Stack.Screen <Stack.Screen
name="post/[postId]" name="post/[postId]"
options={{ options={{
headerShown: true, headerShown: false,
title: '',
// 与 PostDetailScreen 的 SafeAreaViewbackground.default同色避免出现 header/内容之间的色差线
headerStyle: { backgroundColor: colors.background.default },
headerTintColor: colors.text.primary,
headerShadowVisible: false,
headerBackVisible: false,
headerLeft: () => <AppBackButton onPress={() => router.back()} />,
}} }}
/> />
<Stack.Screen <Stack.Screen
name="user/[userId]" name="user/[userId]"
options={{ options={{
headerShown: true, headerShown: false,
title: '用户主页',
headerStyle: { backgroundColor: colors.background.paper },
headerTintColor: colors.text.primary,
headerBackVisible: false,
headerLeft: () => <AppBackButton onPress={() => router.back()} />,
}} }}
/> />
</Stack> </Stack>

View File

@@ -50,6 +50,7 @@ const styles = StyleSheet.create({
marginLeft: -12, marginLeft: -12,
marginRight: 0, marginRight: 0,
padding: 0, padding: 0,
backgroundColor: 'transparent',
}, },
}); });

View File

@@ -0,0 +1,55 @@
import React from 'react';
import { View, StyleSheet } from 'react-native';
import { useAppColors, spacing } from '../../theme';
import { AppBackButton, Text } from './index';
interface SimpleHeaderProps {
title: string;
onBack: () => void;
rightComponent?: React.ReactNode;
}
export const SimpleHeader: React.FC<SimpleHeaderProps> = ({ title, onBack, rightComponent }) => {
const colors = useAppColors();
return (
<View style={[styles.header, { backgroundColor: colors.background.paper }]}>
<AppBackButton onPress={onBack} style={styles.backButton} />
<Text style={[styles.title, { color: colors.text.primary }]} numberOfLines={1}>
{title}
</Text>
{rightComponent ? (
<View style={styles.rightContainer}>
{rightComponent}
</View>
) : (
<View style={styles.rightPlaceholder} />
)}
</View>
);
};
const styles = StyleSheet.create({
header: {
flexDirection: 'row',
alignItems: 'center',
height: 52,
paddingHorizontal: spacing.xs,
},
title: {
flex: 1,
fontSize: 17,
fontWeight: '600',
marginLeft: spacing.xs,
},
rightContainer: {
flexDirection: 'row',
alignItems: 'center',
},
rightPlaceholder: {
width: 40,
},
backButton: {
marginLeft: 0,
},
});

View File

@@ -18,6 +18,7 @@ export { default as ResponsiveGrid } from './ResponsiveGrid';
export { default as ResponsiveStack, HStack, VStack } from './ResponsiveStack'; export { default as ResponsiveStack, HStack, VStack } from './ResponsiveStack';
export { default as AdaptiveLayout, SidebarLayout } from './AdaptiveLayout'; export { default as AdaptiveLayout, SidebarLayout } from './AdaptiveLayout';
export { default as AppBackButton } from './AppBackButton'; export { default as AppBackButton } from './AppBackButton';
export { SimpleHeader } from './SimpleHeader';
export { default as PagerView } from './PagerView'; export { default as PagerView } from './PagerView';
// 图片相关组件 // 图片相关组件

View File

@@ -23,8 +23,9 @@ import {
Dimensions, Dimensions,
} from 'react-native'; } from 'react-native';
import { FlashList, ListRenderItem, FlashListRef } from '@shopify/flash-list'; import { FlashList, ListRenderItem, FlashListRef } from '@shopify/flash-list';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { useNavigation, useRouter, useLocalSearchParams } from 'expo-router'; import { useNavigation, useRouter, useLocalSearchParams } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import { MaterialCommunityIcons } from '@expo/vector-icons'; import { MaterialCommunityIcons } from '@expo/vector-icons';
import * as ImagePicker from 'expo-image-picker'; import * as ImagePicker from 'expo-image-picker';
import { import {
@@ -131,6 +132,7 @@ export const PostDetailScreen: React.FC = () => {
postId?: string; postId?: string;
scrollToComments?: string; scrollToComments?: string;
}>(); }>();
const postId = postIdParam || ''; const postId = postIdParam || '';
const shouldScrollToComments = scrollParam === '1' || scrollParam === 'true'; const shouldScrollToComments = scrollParam === '1' || scrollParam === 'true';
@@ -367,58 +369,43 @@ export const PostDetailScreen: React.FC = () => {
} }
}, [shouldScrollToComments, isPostInitialLoading, comments?.length]); }, [shouldScrollToComments, isPostInitialLoading, comments?.length]);
// 动态设置导航头部 const handleBackPress = useCallback(() => {
useEffect(() => { if (router.canGoBack()) {
if (!post?.author) return; router.back();
return;
const author = post.author; }
const handleBackPress = () => { router.replace(hrefs.hrefHome());
if (router.canGoBack()) { }, [router]);
router.back();
return; const renderCustomHeader = () => (
} <View style={styles.customHeader}>
router.replace(hrefs.hrefHome()); <AppBackButton onPress={handleBackPress} style={styles.headerBackButton} />
}; {post?.author ? (
navigation.setOptions({
// 与页面容器 background.default 一致,并去掉原生 header 底部分隔阴影,避免「一条线」
headerStyle: { backgroundColor: colors.background.default },
headerShadowVisible: false,
headerBackVisible: false,
headerTitleAlign: 'left',
headerLeft: () => (
<AppBackButton onPress={handleBackPress} style={styles.headerBackButton} />
),
headerTitle: () => (
<View style={styles.headerContainer}> <View style={styles.headerContainer}>
<TouchableOpacity <TouchableOpacity
onPress={() => handleUserPress(author.id)} onPress={() => handleUserPress(post.author!.id)}
style={styles.headerAvatarWrapper} style={styles.headerAvatarWrapper}
> >
<Avatar <Avatar source={post.author.avatar} size={32} name={post.author.nickname} />
source={author.avatar}
size={32}
name={author.nickname}
/>
</TouchableOpacity> </TouchableOpacity>
<View style={styles.headerUserInfo}> <View style={styles.headerUserInfo}>
<TouchableOpacity onPress={() => handleUserPress(author.id)}> <TouchableOpacity onPress={() => handleUserPress(post.author!.id)}>
<Text style={styles.headerNickname} numberOfLines={1}> <Text style={styles.headerNickname} numberOfLines={1}>
{author.nickname && author.nickname.length > 10 {post.author.nickname && post.author.nickname.length > 10
? `${author.nickname.slice(0, 10)}...` ? `${post.author.nickname.slice(0, 10)}...`
: author.nickname} : post.author.nickname}
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</View> </View>
), ) : (
headerRight: () => ( <View style={styles.headerPlaceholder} />
<View style={styles.headerRightContainer}> )}
{renderFollowButton()} <View style={styles.headerRightContainer}>
</View> {renderFollowButton()}
), </View>
}); </View>
}, [post?.author, isFollowing, isFollowingMe, isFollowLoading, navigation, colors.background.default, styles]); );
// 监听键盘事件 // 监听键盘事件
useEffect(() => { useEffect(() => {
@@ -1076,12 +1063,15 @@ export const PostDetailScreen: React.FC = () => {
} }
}; };
// 渲染关注按钮 // 渲染关注按钮(始终返回同尺寸占位,避免 header 右侧宽度跳动)
const renderFollowButton = () => { const renderFollowButton = useCallback(() => {
if (!post?.author?.id || currentUser?.id === post.author.id) return null; const shouldShow = post?.author?.id && currentUser?.id !== post.author.id;
if (!shouldShow) {
return <View style={styles.followButtonPlaceholder} />;
}
const config = getFollowButtonConfig(); const config = getFollowButtonConfig();
return ( return (
<TouchableOpacity <TouchableOpacity
style={[ style={[
@@ -1107,7 +1097,7 @@ export const PostDetailScreen: React.FC = () => {
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
); );
}; }, [post?.author?.id, currentUser?.id, isFollowing, isFollowingMe, isFollowLoading, styles]);
// 使用 useMemo 缓存图片数据,避免输入时重新创建数组导致图片重新加载 // 使用 useMemo 缓存图片数据,避免输入时重新创建数组导致图片重新加载
const postImages = useMemo(() => { const postImages = useMemo(() => {
@@ -1712,7 +1702,9 @@ export const PostDetailScreen: React.FC = () => {
// 桌面端双栏布局 // 桌面端双栏布局
if (useDualColumnLayout) { if (useDualColumnLayout) {
return ( return (
<View style={styles.container}> <SafeAreaView style={styles.container} edges={['top']}>
<StatusBar style="auto" />
{renderCustomHeader()}
<AdaptiveLayout <AdaptiveLayout
sidebar={renderSidebar()} sidebar={renderSidebar()}
sidebarWidth={sidebarWidth} sidebarWidth={sidebarWidth}
@@ -1780,17 +1772,19 @@ export const PostDetailScreen: React.FC = () => {
onClose={() => setShowImageModal(false)} onClose={() => setShowImageModal(false)}
enableSave enableSave
/> />
</View> </SafeAreaView>
); );
} }
// 移动端单栏布局 // 移动端单栏布局
return ( return (
<View style={styles.container}> <SafeAreaView style={styles.container} edges={['top']}>
<StatusBar style="auto" />
{renderCustomHeader()}
<KeyboardAvoidingView <KeyboardAvoidingView
style={styles.flex} style={styles.flex}
behavior={Platform.OS === 'ios' ? 'padding' : undefined} behavior={Platform.OS === 'ios' ? 'padding' : undefined}
keyboardVerticalOffset={88} keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : undefined}
> >
<FlashList <FlashList
ref={flatListRef} ref={flatListRef}
@@ -1896,7 +1890,7 @@ export const PostDetailScreen: React.FC = () => {
} }
}} }}
/> />
</View> </SafeAreaView>
); );
}; };
@@ -1909,6 +1903,16 @@ function createPostDetailStyles(colors: AppColors) {
flex: 1, flex: 1,
backgroundColor: colors.background.default, backgroundColor: colors.background.default,
}, },
customHeader: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.background.default,
height: 52,
paddingRight: spacing.sm,
},
headerPlaceholder: {
flex: 1,
},
// Header 样式 // Header 样式
headerContainer: { headerContainer: {
flexDirection: 'row', flexDirection: 'row',
@@ -1932,10 +1936,12 @@ function createPostDetailStyles(colors: AppColors) {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
marginRight: spacing.sm, marginRight: spacing.sm,
backgroundColor: 'transparent',
}, },
headerBackButton: { headerBackButton: {
marginLeft: 0, marginLeft: 0,
marginRight: 0, marginRight: 0,
backgroundColor: 'transparent',
}, },
// 帖子容器 // 帖子容器
postContainer: { postContainer: {
@@ -1950,6 +1956,7 @@ function createPostDetailStyles(colors: AppColors) {
minWidth: 64, minWidth: 64,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
backgroundColor: 'transparent',
}, },
followButtonPrimary: { followButtonPrimary: {
backgroundColor: colors.primary.main, backgroundColor: colors.primary.main,
@@ -1967,6 +1974,12 @@ function createPostDetailStyles(colors: AppColors) {
followButtonLoading: { followButtonLoading: {
opacity: 0.7, opacity: 0.7,
}, },
followButtonPlaceholder: {
minWidth: 64,
paddingHorizontal: spacing.md,
paddingVertical: 4,
backgroundColor: 'transparent',
},
followButtonText: { followButtonText: {
fontSize: fontSizes.sm, fontSize: fontSizes.sm,
fontWeight: '600', fontWeight: '600',

View File

@@ -18,6 +18,7 @@ import {
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { MaterialCommunityIcons } from '@expo/vector-icons'; import { MaterialCommunityIcons } from '@expo/vector-icons';
import Constants from 'expo-constants'; import Constants from 'expo-constants';
import { StatusBar } from 'expo-status-bar';
import { useRouter } from 'expo-router'; import { useRouter } from 'expo-router';
import { import {
useAppColors, useAppColors,
@@ -26,7 +27,7 @@ import {
borderRadius, borderRadius,
type AppColors, type AppColors,
} from '../../theme'; } from '../../theme';
import { Text } from '../../components/common'; import { Text, SimpleHeader } from '../../components/common';
import { useResponsive, useResponsiveSpacing } from '../../hooks'; import { useResponsive, useResponsiveSpacing } from '../../hooks';
import { checkForAPKUpdate, type VersionCheckResult } from '@/services/platform'; import { checkForAPKUpdate, type VersionCheckResult } from '@/services/platform';
import * as hrefs from '../../navigation/hrefs'; import * as hrefs from '../../navigation/hrefs';
@@ -517,7 +518,9 @@ export const AboutScreen: React.FC = () => {
); );
return ( return (
<SafeAreaView style={styles.container} edges={['bottom']}> <SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<StatusBar style="auto" />
<SimpleHeader title="关于我们" onBack={() => router.back()} />
<ScrollView <ScrollView
contentContainerStyle={[styles.scrollContent, { paddingBottom: scrollBottomInset, paddingHorizontal: responsivePadding }]} contentContainerStyle={[styles.scrollContent, { paddingBottom: scrollBottomInset, paddingHorizontal: responsivePadding }]}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}

View File

@@ -1,3 +1,4 @@
import { StatusBar } from 'expo-status-bar';
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { import {
ActivityIndicator, ActivityIndicator,
@@ -13,7 +14,7 @@ import { MaterialCommunityIcons } from '@expo/vector-icons';
import { useRouter } from 'expo-router'; import { useRouter } from 'expo-router';
import { authService } from '../../services'; import { authService } from '../../services';
import { spacing, borderRadius, fontSizes, useAppColors, type AppColors } from '../../theme'; import { spacing, borderRadius, fontSizes, useAppColors, type AppColors } from '../../theme';
import { Text } from '../../components/common'; import { Text, SimpleHeader } from '../../components/common';
import { useResponsive, useResponsiveSpacing } from '../../hooks'; import { useResponsive, useResponsiveSpacing } from '../../hooks';
import { useAuthStore } from '../../stores'; import { useAuthStore } from '../../stores';
import type { DeletionStatusDTO } from '../../types/dto'; import type { DeletionStatusDTO } from '../../types/dto';
@@ -275,7 +276,7 @@ export const AccountDeletionScreen: React.FC = () => {
if (loading) { if (loading) {
return ( return (
<SafeAreaView style={styles.container} edges={['bottom']}> <SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<View style={styles.loadingWrap}> <View style={styles.loadingWrap}>
<ActivityIndicator size="large" color={colors.primary.main} /> <ActivityIndicator size="large" color={colors.primary.main} />
</View> </View>
@@ -284,7 +285,9 @@ export const AccountDeletionScreen: React.FC = () => {
} }
return ( return (
<SafeAreaView style={styles.container} edges={['bottom']}> <SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<StatusBar style="auto" />
<SimpleHeader title="注销账号" onBack={() => router.back()} />
<ScrollView contentContainerStyle={[styles.scrollContent, { paddingBottom: scrollBottomInset, paddingHorizontal: responsivePadding }]}> <ScrollView contentContainerStyle={[styles.scrollContent, { paddingBottom: scrollBottomInset, paddingHorizontal: responsivePadding }]}>
<View style={styles.content}> <View style={styles.content}>
{status?.is_pending_deletion ? ( {status?.is_pending_deletion ? (

View File

@@ -7,10 +7,12 @@ import {
ActivityIndicator, ActivityIndicator,
ScrollView, ScrollView,
} from 'react-native'; } from 'react-native';
import { StatusBar } from 'expo-status-bar';
import { useRouter } from 'expo-router';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { MaterialCommunityIcons } from '@expo/vector-icons'; import { MaterialCommunityIcons } from '@expo/vector-icons';
import { spacing, fontSizes, borderRadius, useAppColors, type AppColors } from '../../theme'; import { spacing, fontSizes, borderRadius, useAppColors, type AppColors } from '../../theme';
import { Text } from '../../components/common'; import { Text, SimpleHeader } from '../../components/common';
import { useResponsive, useResponsiveSpacing } from '../../hooks'; import { useResponsive, useResponsiveSpacing } from '../../hooks';
// 内容最大宽度 // 内容最大宽度
@@ -21,6 +23,7 @@ import { showPrompt } from '@/services/ui';
import { useAuthStore } from '../../stores'; import { useAuthStore } from '../../stores';
export const AccountSecurityScreen: React.FC = () => { export const AccountSecurityScreen: React.FC = () => {
const router = useRouter();
const colors = useAppColors(); const colors = useAppColors();
const styles = useMemo(() => createAccountSecurityStyles(colors), [colors]); const styles = useMemo(() => createAccountSecurityStyles(colors), [colors]);
const { isWideScreen, isMobile } = useResponsive(); const { isWideScreen, isMobile } = useResponsive();
@@ -202,7 +205,9 @@ export const AccountSecurityScreen: React.FC = () => {
const responsivePadding = useResponsiveSpacing({ xs: 8, sm: 12, md: 16, lg: 24, xl: 32 }); const responsivePadding = useResponsiveSpacing({ xs: 8, sm: 12, md: 16, lg: 24, xl: 32 });
return ( return (
<SafeAreaView style={styles.container} edges={['bottom']}> <SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<StatusBar style="auto" />
<SimpleHeader title="账号安全" onBack={() => router.back()} />
<ScrollView contentContainerStyle={[styles.scrollContent, { paddingBottom: scrollBottomInset, paddingHorizontal: responsivePadding }]}> <ScrollView contentContainerStyle={[styles.scrollContent, { paddingBottom: scrollBottomInset, paddingHorizontal: responsivePadding }]}>
<View style={styles.content}> <View style={styles.content}>
{/* 邮箱验证分组 */} {/* 邮箱验证分组 */}

View File

@@ -10,7 +10,8 @@ import {
} from 'react-native'; } from 'react-native';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { useRouter } from 'expo-router'; import { useRouter } from 'expo-router';
import { Avatar, Button, EmptyState, ResponsiveContainer, Text } from '../../components/common'; import { StatusBar } from 'expo-status-bar';
import { Avatar, Button, EmptyState, ResponsiveContainer, SimpleHeader, Text } from '../../components/common';
import { authService } from '../../services'; import { authService } from '../../services';
import { spacing, borderRadius, shadows, useAppColors, type AppColors } from '../../theme'; import { spacing, borderRadius, shadows, useAppColors, type AppColors } from '../../theme';
import { User } from '../../types'; import { User } from '../../types';
@@ -140,7 +141,8 @@ export const BlockedUsersScreen: React.FC = () => {
if (loading) { if (loading) {
return ( return (
<SafeAreaView style={styles.container} edges={['bottom']}> <SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<StatusBar style="auto" />
<View style={styles.loadingWrap}> <View style={styles.loadingWrap}>
<ActivityIndicator size="large" color={colors.primary.main} /> <ActivityIndicator size="large" color={colors.primary.main} />
</View> </View>
@@ -149,7 +151,9 @@ export const BlockedUsersScreen: React.FC = () => {
} }
return ( return (
<SafeAreaView style={styles.container} edges={['bottom']}> <SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<StatusBar style="auto" />
<SimpleHeader title="黑名单" onBack={() => router.back()} />
<ResponsiveContainer maxWidth={900}> <ResponsiveContainer maxWidth={900}>
<FlatList <FlatList
data={users} data={users}

View File

@@ -12,7 +12,9 @@ import {
Dimensions, Dimensions,
PanResponder, PanResponder,
} from 'react-native'; } from 'react-native';
import { StatusBar } from 'expo-status-bar';
import { SafeAreaView } from 'react-native-safe-area-context'; import { SafeAreaView } from 'react-native-safe-area-context';
import { useRouter } from 'expo-router';
import { import {
useAppColors, useAppColors,
spacing, spacing,
@@ -20,7 +22,7 @@ import {
borderRadius, borderRadius,
type AppColors, type AppColors,
} from '../../theme'; } from '../../theme';
import { Text } from '../../components/common'; import { Text, SimpleHeader } from '../../components/common';
import { useResponsiveSpacing } from '../../hooks'; import { useResponsiveSpacing } from '../../hooks';
import { import {
useChatSettingsStore, useChatSettingsStore,
@@ -265,6 +267,7 @@ function createStyles(colors: AppColors) {
} }
export const ChatSettingsScreen: React.FC = () => { export const ChatSettingsScreen: React.FC = () => {
const router = useRouter();
const colors = useAppColors(); const colors = useAppColors();
const styles = useMemo(() => createStyles(colors), [colors]); const styles = useMemo(() => createStyles(colors), [colors]);
const responsivePadding = useResponsiveSpacing({ xs: 8, sm: 12, md: 16, lg: 24, xl: 32 }); const responsivePadding = useResponsiveSpacing({ xs: 8, sm: 12, md: 16, lg: 24, xl: 32 });
@@ -379,7 +382,9 @@ export const ChatSettingsScreen: React.FC = () => {
); );
return ( return (
<SafeAreaView style={styles.container} edges={['bottom']}> <SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<StatusBar style="auto" />
<SimpleHeader title="聊天设置" onBack={() => router.back()} />
<ScrollView <ScrollView
contentContainerStyle={[ contentContainerStyle={[
styles.scrollContent, styles.scrollContent,

View File

@@ -14,12 +14,14 @@ import {
Platform, Platform,
} from 'react-native'; } from 'react-native';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { StatusBar } from 'expo-status-bar';
import { useRouter } from 'expo-router';
import { MaterialCommunityIcons } from '@expo/vector-icons'; import { MaterialCommunityIcons } from '@expo/vector-icons';
import AsyncStorage from '@react-native-async-storage/async-storage'; import AsyncStorage from '@react-native-async-storage/async-storage';
import { Image } from 'expo-image'; import { Image } from 'expo-image';
import { Directory, File, Paths } from 'expo-file-system'; import { Directory, File, Paths } from 'expo-file-system';
import { spacing, borderRadius, fontSizes, useAppColors, type AppColors } from '../../theme'; import { spacing, borderRadius, fontSizes, useAppColors, type AppColors } from '../../theme';
import { Text } from '../../components/common'; import { Text, SimpleHeader } from '../../components/common';
import { useResponsive, useResponsiveSpacing } from '../../hooks'; import { useResponsive, useResponsiveSpacing } from '../../hooks';
import { mediaCacheManager } from '../../infrastructure/cache'; import { mediaCacheManager } from '../../infrastructure/cache';
import type { CacheStats } from '../../infrastructure/cache'; import type { CacheStats } from '../../infrastructure/cache';
@@ -42,6 +44,7 @@ interface StorageStatItem {
} }
export const DataStorageScreen: React.FC = () => { export const DataStorageScreen: React.FC = () => {
const router = useRouter();
const colors = useAppColors(); const colors = useAppColors();
const styles = useMemo(() => createDataStorageStyles(colors), [colors]); const styles = useMemo(() => createDataStorageStyles(colors), [colors]);
const { isMobile } = useResponsive(); const { isMobile } = useResponsive();
@@ -372,7 +375,9 @@ export const DataStorageScreen: React.FC = () => {
}; };
return ( return (
<SafeAreaView style={styles.container} edges={['bottom']}> <SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<StatusBar style="auto" />
<SimpleHeader title="数据与存储" onBack={() => router.back()} />
<ScrollView <ScrollView
contentContainerStyle={[ contentContainerStyle={[
styles.scrollContent, styles.scrollContent,

View File

@@ -18,6 +18,8 @@ import {
} from 'react-native'; } from 'react-native';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { useRouter } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import { MaterialCommunityIcons } from '@expo/vector-icons'; import { MaterialCommunityIcons } from '@expo/vector-icons';
import * as ImagePicker from 'expo-image-picker'; import * as ImagePicker from 'expo-image-picker';
import { import {
@@ -28,7 +30,7 @@ import {
type AppColors, type AppColors,
} from '../../theme'; } from '../../theme';
import { useAuthStore } from '../../stores'; import { useAuthStore } from '../../stores';
import { Avatar, Text } from '../../components/common'; import { Avatar, Text, SimpleHeader } from '../../components/common';
import { authService, uploadService } from '../../services'; import { authService, uploadService } from '../../services';
import { useResponsive, useResponsiveSpacing } from '../../hooks'; import { useResponsive, useResponsiveSpacing } from '../../hooks';
@@ -276,6 +278,7 @@ export const EditProfileScreen: React.FC = () => {
const colors = useAppColors(); const colors = useAppColors();
const styles = useMemo(() => createEditProfileStyles(colors), [colors]); const styles = useMemo(() => createEditProfileStyles(colors), [colors]);
const navigation = useNavigation(); const navigation = useNavigation();
const router = useRouter();
const { currentUser, updateUser } = useAuthStore(); const { currentUser, updateUser } = useAuthStore();
const { isWideScreen, isMobile, width } = useResponsive(); const { isWideScreen, isMobile, width } = useResponsive();
const responsivePadding = useResponsiveSpacing({ xs: 0, sm: 0, md: 0, lg: 24, xl: 32 }); const responsivePadding = useResponsiveSpacing({ xs: 0, sm: 0, md: 0, lg: 24, xl: 32 });
@@ -596,7 +599,9 @@ export const EditProfileScreen: React.FC = () => {
); );
return ( return (
<SafeAreaView style={styles.container} edges={['bottom']}> <SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<StatusBar style="auto" />
<SimpleHeader title="编辑资料" onBack={() => router.back()} />
<KeyboardAvoidingView <KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'} behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.keyboardView} style={styles.keyboardView}

View File

@@ -4,6 +4,7 @@
* 在宽屏下居中显示 * 在宽屏下居中显示
*/ */
import { useRouter } from 'expo-router';
import React, { useState, useEffect, useMemo } from 'react'; import React, { useState, useEffect, useMemo } from 'react';
import { import {
View, View,
@@ -14,10 +15,11 @@ import {
TouchableOpacity, TouchableOpacity,
Platform, Platform,
} from 'react-native'; } from 'react-native';
import { StatusBar } from 'expo-status-bar';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { MaterialCommunityIcons } from '@expo/vector-icons'; import { MaterialCommunityIcons } from '@expo/vector-icons';
import { spacing, borderRadius, fontSizes, useAppColors, type AppColors } from '../../theme'; import { spacing, borderRadius, fontSizes, useAppColors, type AppColors } from '../../theme';
import { Text } from '../../components/common'; import { Text, SimpleHeader } from '../../components/common';
import { import {
loadNotificationPreferences, loadNotificationPreferences,
setPushNotificationsPreference, setPushNotificationsPreference,
@@ -41,6 +43,7 @@ interface NotificationSettingItem {
} }
export const NotificationSettingsScreen: React.FC = () => { export const NotificationSettingsScreen: React.FC = () => {
const router = useRouter();
const colors = useAppColors(); const colors = useAppColors();
const styles = useMemo(() => createNotificationSettingsStyles(colors), [colors]); const styles = useMemo(() => createNotificationSettingsStyles(colors), [colors]);
const [vibrationEnabled, setVibrationEnabledState] = useState(true); const [vibrationEnabled, setVibrationEnabledState] = useState(true);
@@ -316,7 +319,9 @@ export const NotificationSettingsScreen: React.FC = () => {
const responsivePadding = useResponsiveSpacing({ xs: 8, sm: 12, md: 16, lg: 24, xl: 32 }); const responsivePadding = useResponsiveSpacing({ xs: 8, sm: 12, md: 16, lg: 24, xl: 32 });
return ( return (
<SafeAreaView style={styles.container} edges={['bottom']}> <SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<StatusBar style="auto" />
<SimpleHeader title="通知设置" onBack={() => router.back()} />
<ScrollView contentContainerStyle={[styles.scrollContent, { paddingBottom: scrollBottomInset, paddingHorizontal: responsivePadding }]}> <ScrollView contentContainerStyle={[styles.scrollContent, { paddingBottom: scrollBottomInset, paddingHorizontal: responsivePadding }]}>
{renderContent()} {renderContent()}
</ScrollView> </ScrollView>

View File

@@ -10,13 +10,15 @@ import {
StyleSheet, StyleSheet,
ScrollView, ScrollView,
} from 'react-native'; } from 'react-native';
import { StatusBar } from 'expo-status-bar';
import { useRouter } from 'expo-router';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { import {
useAppColors, useAppColors,
spacing, spacing,
type AppColors, type AppColors,
} from '../../theme'; } from '../../theme';
import { Text } from '../../components/common'; import { Text, SimpleHeader } from '../../components/common';
import { useResponsive, useResponsiveSpacing } from '../../hooks'; import { useResponsive, useResponsiveSpacing } from '../../hooks';
// 内容最大宽度 // 内容最大宽度
@@ -278,6 +280,7 @@ export const PrivacyPolicyScreen: React.FC = () => {
const colors = useAppColors(); const colors = useAppColors();
const styles = useMemo(() => createPrivacyStyles(colors), [colors]); const styles = useMemo(() => createPrivacyStyles(colors), [colors]);
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const router = useRouter();
const { isMobile } = useResponsive(); const { isMobile } = useResponsive();
const responsivePadding = useResponsiveSpacing({ xs: 20, sm: 24, md: 28, lg: 32, xl: 40 }); const responsivePadding = useResponsiveSpacing({ xs: 20, sm: 24, md: 28, lg: 32, xl: 40 });
@@ -285,7 +288,9 @@ export const PrivacyPolicyScreen: React.FC = () => {
const scrollBottomInset = isMobile ? 64 + 24 + insets.bottom + spacing.md : spacing.lg; const scrollBottomInset = isMobile ? 64 + 24 + insets.bottom + spacing.md : spacing.lg;
return ( return (
<SafeAreaView style={styles.container} edges={['bottom']}> <SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<StatusBar style="auto" />
<SimpleHeader title="隐私政策" onBack={() => router.back()} />
<ScrollView <ScrollView
contentContainerStyle={[styles.scrollContent, { paddingHorizontal: responsivePadding, paddingBottom: scrollBottomInset }]} contentContainerStyle={[styles.scrollContent, { paddingHorizontal: responsivePadding, paddingBottom: scrollBottomInset }]}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}

View File

@@ -7,12 +7,13 @@ import {
TouchableOpacity, TouchableOpacity,
View, View,
} from 'react-native'; } from 'react-native';
import { StatusBar } from 'expo-status-bar';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { MaterialCommunityIcons } from '@expo/vector-icons'; import { MaterialCommunityIcons } from '@expo/vector-icons';
import { useRouter } from 'expo-router'; import { useRouter } from 'expo-router';
import { authService } from '../../services'; import { authService } from '../../services';
import { spacing, borderRadius, fontSizes, useAppColors, type AppColors } from '../../theme'; import { spacing, borderRadius, fontSizes, useAppColors, type AppColors } from '../../theme';
import { Text } from '../../components/common'; import { SimpleHeader, Text } from '../../components/common';
import { useResponsive, useResponsiveSpacing } from '../../hooks'; import { useResponsive, useResponsiveSpacing } from '../../hooks';
import type { PrivacySettingsDTO, VisibilityLevel } from '../../types/dto'; import type { PrivacySettingsDTO, VisibilityLevel } from '../../types/dto';
@@ -200,7 +201,8 @@ export const PrivacySettingsScreen: React.FC = () => {
if (loading) { if (loading) {
return ( return (
<SafeAreaView style={styles.container} edges={['bottom']}> <SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<StatusBar style="auto" />
<View style={styles.loadingWrap}> <View style={styles.loadingWrap}>
<ActivityIndicator size="large" color={colors.primary.main} /> <ActivityIndicator size="large" color={colors.primary.main} />
</View> </View>
@@ -209,7 +211,9 @@ export const PrivacySettingsScreen: React.FC = () => {
} }
return ( return (
<SafeAreaView style={styles.container} edges={['bottom']}> <SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<StatusBar style="auto" />
<SimpleHeader title="隐私设置" onBack={() => router.back()} />
<ScrollView contentContainerStyle={[styles.scrollContent, { paddingBottom: scrollBottomInset, paddingHorizontal: responsivePadding }]}> <ScrollView contentContainerStyle={[styles.scrollContent, { paddingBottom: scrollBottomInset, paddingHorizontal: responsivePadding }]}>
<View style={styles.content}> <View style={styles.content}>
<View style={styles.section}> <View style={styles.section}>

View File

@@ -13,6 +13,7 @@ import {
ScrollView, ScrollView,
} from 'react-native'; } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context'; import { SafeAreaView } from 'react-native-safe-area-context';
import { StatusBar } from 'expo-status-bar';
import { useRouter } from 'expo-router'; import { useRouter } from 'expo-router';
import { MaterialCommunityIcons } from '@expo/vector-icons'; import { MaterialCommunityIcons } from '@expo/vector-icons';
import Constants from 'expo-constants'; import Constants from 'expo-constants';
@@ -26,7 +27,7 @@ import {
type AppColors, type AppColors,
} from '../../theme'; } from '../../theme';
import { useAuthStore } from '../../stores'; import { useAuthStore } from '../../stores';
import { Text } from '../../components/common'; import { Text, SimpleHeader } from '../../components/common';
import { useResponsive, useResponsiveSpacing } from '../../hooks'; import { useResponsive, useResponsiveSpacing } from '../../hooks';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import * as hrefs from '../../navigation/hrefs'; import * as hrefs from '../../navigation/hrefs';
@@ -365,7 +366,9 @@ export const SettingsScreen: React.FC = () => {
); );
return ( return (
<SafeAreaView style={styles.container} edges={['bottom']}> <SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<StatusBar style="auto" />
<SimpleHeader title="设置" onBack={() => router.back()} />
<ScrollView contentContainerStyle={[styles.scrollContent, { paddingBottom: scrollBottomInset, paddingHorizontal: responsivePadding }]}> <ScrollView contentContainerStyle={[styles.scrollContent, { paddingBottom: scrollBottomInset, paddingHorizontal: responsivePadding }]}>
{renderContent()} {renderContent()}
</ScrollView> </ScrollView>

View File

@@ -10,13 +10,15 @@ import {
StyleSheet, StyleSheet,
ScrollView, ScrollView,
} from 'react-native'; } from 'react-native';
import { StatusBar } from 'expo-status-bar';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { useRouter } from 'expo-router';
import { import {
useAppColors, useAppColors,
spacing, spacing,
type AppColors, type AppColors,
} from '../../theme'; } from '../../theme';
import { Text } from '../../components/common'; import { Text, SimpleHeader } from '../../components/common';
import { useResponsive, useResponsiveSpacing } from '../../hooks'; import { useResponsive, useResponsiveSpacing } from '../../hooks';
// 内容最大宽度 // 内容最大宽度
@@ -213,12 +215,15 @@ export const TermsOfServiceScreen: React.FC = () => {
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const { isMobile } = useResponsive(); const { isMobile } = useResponsive();
const responsivePadding = useResponsiveSpacing({ xs: 20, sm: 24, md: 28, lg: 32, xl: 40 }); const responsivePadding = useResponsiveSpacing({ xs: 20, sm: 24, md: 28, lg: 32, xl: 40 });
const router = useRouter();
// 底部间距,避免被 TabBar 遮挡 // 底部间距,避免被 TabBar 遮挡
const scrollBottomInset = isMobile ? 64 + 24 + insets.bottom + spacing.md : spacing.lg; const scrollBottomInset = isMobile ? 64 + 24 + insets.bottom + spacing.md : spacing.lg;
return ( return (
<SafeAreaView style={styles.container} edges={['bottom']}> <SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<StatusBar style="auto" />
<SimpleHeader title="用户协议" onBack={() => router.back()} />
<ScrollView <ScrollView
contentContainerStyle={[styles.scrollContent, { paddingHorizontal: responsivePadding, paddingBottom: scrollBottomInset }]} contentContainerStyle={[styles.scrollContent, { paddingHorizontal: responsivePadding, paddingBottom: scrollBottomInset }]}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}

View File

@@ -5,22 +5,28 @@
*/ */
import React from 'react'; import React from 'react';
import { useLocalSearchParams } from 'expo-router'; import { useLocalSearchParams, useRouter } from 'expo-router';
import { SafeAreaView } from 'react-native-safe-area-context';
import UserProfileScreen from './UserProfileScreen'; import UserProfileScreen from './UserProfileScreen';
import { useCurrentUser } from '../../stores/auth'; import { useCurrentUser } from '../../stores/auth';
import { SimpleHeader } from '../../components/common';
export const UserScreen: React.FC = () => { export const UserScreen: React.FC = () => {
const { userId: userIdParam } = useLocalSearchParams<{ userId?: string }>(); const { userId: userIdParam } = useLocalSearchParams<{ userId?: string }>();
const router = useRouter();
const userId = userIdParam || ''; const userId = userIdParam || '';
const currentUser = useCurrentUser(); const currentUser = useCurrentUser();
const isSelfProfile = !!currentUser?.id && currentUser.id === userId; const isSelfProfile = !!currentUser?.id && currentUser.id === userId;
return ( return (
<UserProfileScreen <SafeAreaView style={{ flex: 1 }} edges={['top', 'bottom']}>
mode={isSelfProfile ? 'self' : 'other'} <SimpleHeader title="用户主页" onBack={() => router.back()} />
userId={isSelfProfile ? undefined : userId} <UserProfileScreen
hasHeader mode={isSelfProfile ? 'self' : 'other'}
/> userId={isSelfProfile ? undefined : userId}
hasHeader={false}
/>
</SafeAreaView>
); );
}; };

View File

@@ -14,8 +14,10 @@ import {
Alert, Alert,
ActivityIndicator, ActivityIndicator,
} from 'react-native'; } from 'react-native';
import { StatusBar } from 'expo-status-bar';
import { SafeAreaView } from 'react-native-safe-area-context'; import { SafeAreaView } from 'react-native-safe-area-context';
import { useRouter } from 'expo-router'; import { useRouter } from 'expo-router';
import { SimpleHeader } from '../../components/common';
import { MaterialCommunityIcons } from '@expo/vector-icons'; import { MaterialCommunityIcons } from '@expo/vector-icons';
import { useAppColors, type AppColors } from '../../theme'; import { useAppColors, type AppColors } from '../../theme';
import { import {
@@ -91,7 +93,9 @@ export const VerificationSettingsScreen: React.FC = () => {
if (loading && !status) { if (loading && !status) {
return ( return (
<SafeAreaView style={styles.container}> <SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<StatusBar style="auto" />
<SimpleHeader title="身份认证" onBack={() => router.back()} />
<View style={styles.loadingContainer}> <View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={colors.primary.main} /> <ActivityIndicator size="large" color={colors.primary.main} />
</View> </View>
@@ -104,7 +108,9 @@ export const VerificationSettingsScreen: React.FC = () => {
const identityText = IDENTITY_TEXT[status?.identity || 'general']; const identityText = IDENTITY_TEXT[status?.identity || 'general'];
return ( return (
<SafeAreaView style={styles.container}> <SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<StatusBar style="auto" />
<SimpleHeader title="身份认证" onBack={() => router.back()} />
<ScrollView <ScrollView
contentContainerStyle={styles.scrollContent} contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}