From d909d376f998c574a164d5f600d3c24bfc09282d Mon Sep 17 00:00:00 2001
From: liu <2302563948@qq.com>
Date: Wed, 1 Jul 2026 21:54:00 +0800
Subject: [PATCH] =?UTF-8?q?=E5=BA=95=E9=83=A8=E5=AF=BC=E8=88=AA?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
CLAUDE.md | 26 ++++++
src/app.config.ts | 32 ++++++-
src/components/PublishPopup/index.less | 82 +++++++++++++++++
src/components/PublishPopup/index.tsx | 64 ++++++++++++++
src/custom-tab-bar/index.config.ts | 3 +
src/custom-tab-bar/index.less | 71 +++++++++++++++
src/custom-tab-bar/index.tsx | 118 +++++++++++++++++++++++++
src/pages/index/index.tsx | 3 +
src/pages/messages/index.config.ts | 4 +
src/pages/messages/index.less | 29 ++++++
src/pages/messages/index.tsx | 16 ++++
src/pages/profile/index.config.ts | 4 +
src/pages/profile/index.less | 29 ++++++
src/pages/profile/index.tsx | 16 ++++
src/pages/tasks/index.config.ts | 5 ++
src/pages/tasks/index.less | 29 ++++++
src/pages/tasks/index.tsx | 16 ++++
17 files changed, 544 insertions(+), 3 deletions(-)
create mode 100644 src/components/PublishPopup/index.less
create mode 100644 src/components/PublishPopup/index.tsx
create mode 100644 src/custom-tab-bar/index.config.ts
create mode 100644 src/custom-tab-bar/index.less
create mode 100644 src/custom-tab-bar/index.tsx
create mode 100644 src/pages/messages/index.config.ts
create mode 100644 src/pages/messages/index.less
create mode 100644 src/pages/messages/index.tsx
create mode 100644 src/pages/profile/index.config.ts
create mode 100644 src/pages/profile/index.less
create mode 100644 src/pages/profile/index.tsx
create mode 100644 src/pages/tasks/index.config.ts
create mode 100644 src/pages/tasks/index.less
create mode 100644 src/pages/tasks/index.tsx
diff --git a/CLAUDE.md b/CLAUDE.md
index 89bbac6..745c640 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -127,3 +127,29 @@ This project uses `@antmjs/vantui` v3.7. The official docs: https://antmjs.githu
- Use `@tarojs/components` for: `View`, `Text`, `ScrollView`, `Image` (native, simpler)
- Use `@antmjs/vantui` for: styled UI components (Search, Swiper, Tabs, Grid, Button, etc.)
- VantUI component props extend Taro's `ViewProps`, so `onClick`/`onTap` and style props are always available
+
+## Custom TabBar (`src/custom-tab-bar/`)
+
+This project uses Taro's **custom tabBar** mechanism (`tabBar.custom: true` in `app.config.ts`). Key rules:
+
+### Component location
+- The custom tabBar component MUST be at `src/custom-tab-bar/index.tsx` — this is a Taro convention, not configurable
+- Taro automatically renders this component on every tab page; do NOT import `` in individual pages
+
+### Do NOT use CoverView
+- `CoverView` / `CoverImage` have known bugs on real devices: invisible, click events don't bubble
+- Use regular `` with `position: fixed; bottom: 0; z-index: 999` instead
+
+### Active tab state
+- Taro creates a **new instance** of the tabBar component on each page, so `useState` resets on every tab switch
+- Derive the active tab from `Taro.getCurrentPages()` at render time — NOT from component state
+- Pattern: `const route = Taro.getCurrentPages()[last]?.route; const activeKey = TAB_LIST.find(t => route.includes(t.key))`
+
+### Tab switching
+- Use `Taro.switchTab({ url: pagePath })` — this matches native tabBar behavior
+- The `pagePath` format must match the `tabBar.list[].pagePath` in `app.config.ts`
+
+### Center "publish" button
+- The publish button is a UI-only element not in `tabBar.list`
+- It triggers a VantUI `` rendered as a sibling of the tabBar, NOT nested inside it
+- The popup state can use `useState` since it's ephemeral and not shared across tabs
diff --git a/src/app.config.ts b/src/app.config.ts
index 38c6228..ef57de9 100644
--- a/src/app.config.ts
+++ b/src/app.config.ts
@@ -1,13 +1,39 @@
export default defineAppConfig({
pages: [
'pages/index/index',
+ 'pages/tasks/index',
+ 'pages/messages/index',
+ 'pages/profile/index',
],
window: {
backgroundTextStyle: 'light',
navigationBarBackgroundColor: '#fff',
navigationBarTitleText: 'WeChat',
- navigationBarTextStyle: 'black'
+ navigationBarTextStyle: 'black',
+ },
+ tabBar: {
+ custom: true,
+ color: '#969799',
+ selectedColor: '#1989fa',
+ backgroundColor: '#ffffff',
+ list: [
+ {
+ pagePath: 'pages/index/index',
+ text: '首页',
+ },
+ {
+ pagePath: 'pages/tasks/index',
+ text: '校园任务',
+ },
+ {
+ pagePath: 'pages/messages/index',
+ text: '消息',
+ },
+ {
+ pagePath: 'pages/profile/index',
+ text: '我的',
+ },
+ ],
},
animation: false,
-}
-)
\ No newline at end of file
+})
diff --git a/src/components/PublishPopup/index.less b/src/components/PublishPopup/index.less
new file mode 100644
index 0000000..2ea567e
--- /dev/null
+++ b/src/components/PublishPopup/index.less
@@ -0,0 +1,82 @@
+.publish-popup {
+ padding: 32px 24px 0;
+ background: #f7f8fa;
+ border-radius: 24px 24px 0 0;
+
+ .popup-header {
+ text-align: center;
+ margin-bottom: 32px;
+
+ .popup-title {
+ display: block;
+ font-size: 36px;
+ font-weight: 600;
+ color: #1a1a1a;
+ margin-bottom: 8px;
+ }
+
+ .popup-subtitle {
+ font-size: 26px;
+ color: #969799;
+ }
+ }
+
+ .popup-grid {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ margin-bottom: 24px;
+ }
+
+ .publish-card {
+ display: flex;
+ align-items: center;
+ background: #fff;
+ border-radius: 16px;
+ padding: 28px 24px;
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
+ transition: transform 0.2s;
+
+ &:active {
+ transform: scale(0.98);
+ background: #f7f8fa;
+ }
+
+ .card-icon {
+ font-size: 48px;
+ margin-right: 20px;
+ flex-shrink: 0;
+ }
+
+ .card-info {
+ flex: 1;
+
+ .card-label {
+ display: block;
+ font-size: 32px;
+ font-weight: 500;
+ color: #1a1a1a;
+ margin-bottom: 4px;
+ }
+
+ .card-desc {
+ font-size: 24px;
+ color: #969799;
+ }
+ }
+ }
+
+ .popup-cancel {
+ padding: 20px 0 40px;
+ text-align: center;
+
+ .cancel-text {
+ display: inline-block;
+ font-size: 32px;
+ color: #969799;
+ padding: 12px 48px;
+ background: #fff;
+ border-radius: 48px;
+ }
+ }
+}
diff --git a/src/components/PublishPopup/index.tsx b/src/components/PublishPopup/index.tsx
new file mode 100644
index 0000000..a014ac6
--- /dev/null
+++ b/src/components/PublishPopup/index.tsx
@@ -0,0 +1,64 @@
+import { View, Text } from '@tarojs/components'
+import { Popup } from '@antmjs/vantui'
+import './index.less'
+
+interface PublishOption {
+ id: string
+ label: string
+ icon: string
+ desc: string
+}
+
+const PUBLISH_OPTIONS: PublishOption[] = [
+ { id: 'article', label: '发布文章', icon: '📝', desc: '分享知识与见解' },
+ { id: 'goods', label: '发布二手', icon: '🛍️', desc: '闲置物品交易' },
+ { id: 'express', label: '快递代取', icon: '📦', desc: '发布代取任务' },
+ { id: 'activity', label: '校园活动', icon: '🎉', desc: '组织校园活动' },
+ { id: 'help', label: '求助信息', icon: '🆘', desc: '发布求助需求' },
+]
+
+interface PublishPopupProps {
+ visible: boolean
+ onClose: () => void
+ onSelect: (option: PublishOption) => void
+}
+
+export default function PublishPopup({ visible, onClose, onSelect }: PublishPopupProps) {
+ return (
+
+
+
+ 发布
+ 选择你要发布的内容类型
+
+
+ {PUBLISH_OPTIONS.map((option) => (
+ onSelect(option)}
+ >
+ {option.icon}
+
+ {option.label}
+ {option.desc}
+
+
+ ))}
+
+
+ 取消
+
+
+
+ )
+}
+
+export type { PublishOption }
diff --git a/src/custom-tab-bar/index.config.ts b/src/custom-tab-bar/index.config.ts
new file mode 100644
index 0000000..29210de
--- /dev/null
+++ b/src/custom-tab-bar/index.config.ts
@@ -0,0 +1,3 @@
+export default {
+ "component": true
+}
diff --git a/src/custom-tab-bar/index.less b/src/custom-tab-bar/index.less
new file mode 100644
index 0000000..061c45b
--- /dev/null
+++ b/src/custom-tab-bar/index.less
@@ -0,0 +1,71 @@
+.custom-tab-bar {
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-around;
+ height: 100px;
+ background: rgba(255, 255, 255, 0.85);
+ backdrop-filter: blur(20px);
+ -webkit-backdrop-filter: blur(20px);
+ border-top: 1px solid rgba(0, 0, 0, 0.06);
+ z-index: 999;
+ box-sizing: border-box;
+
+ .tab-item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding-top: 10px;
+ flex: 1;
+ position: relative;
+
+ .tab-icon {
+ font-size: 40px;
+ line-height: 1.2;
+ margin-bottom: 2px;
+ }
+
+ .tab-label {
+ font-size: 20px;
+ color: #969799;
+ line-height: 1.4;
+ }
+
+ &.active .tab-label {
+ color: #1989fa;
+ }
+ }
+
+ /* 中间发布按钮 */
+ .tab-publish {
+ justify-content: flex-start;
+ padding-top: 0;
+
+ .publish-btn {
+ width: 88px;
+ height: 88px;
+ border-radius: 50%;
+ background: linear-gradient(135deg, #1989fa 0%, #07c160 100%);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: 0 8px 24px rgba(25, 137, 250, 0.4);
+ margin-top: -30px;
+
+ .publish-icon {
+ font-size: 44px;
+ color: #fff;
+ font-weight: 300;
+ line-height: 1;
+ }
+ }
+
+ .publish-label {
+ margin-top: 4px;
+ }
+ }
+}
diff --git a/src/custom-tab-bar/index.tsx b/src/custom-tab-bar/index.tsx
new file mode 100644
index 0000000..4d118b5
--- /dev/null
+++ b/src/custom-tab-bar/index.tsx
@@ -0,0 +1,118 @@
+import { useState, useCallback } from 'react'
+import Taro from '@tarojs/taro'
+import { View, Text } from '@tarojs/components'
+import PublishPopup from '@/components/PublishPopup'
+import type { PublishOption } from '@/components/PublishPopup'
+import './index.less'
+
+/** Tab 项配置(顺序与 app.config.ts tabBar.list 一致) */
+interface TabItem {
+ key: string
+ label: string
+ icon: string
+ pagePath: string
+}
+
+const TAB_LIST: TabItem[] = [
+ { key: 'index', label: '首页', icon: '🏠', pagePath: '/pages/index/index' },
+ { key: 'tasks', label: '校园任务', icon: '📋', pagePath: '/pages/tasks/index' },
+ { key: 'messages', label: '消息', icon: '💬', pagePath: '/pages/messages/index' },
+ { key: 'profile', label: '我的', icon: '👤', pagePath: '/pages/profile/index' },
+]
+
+/**
+ * 根据当前页面路由推导激活的 tab。
+ * 不使用 useState —— Taro 在每次切换 tab 时都会重建 custom-tab-bar 实例,
+ * 因此直接从 getCurrentPages() 实时计算即可保证正确性。
+ */
+function useActiveKey(): string {
+ const pages = Taro.getCurrentPages()
+ if (pages.length > 0) {
+ const route = pages[pages.length - 1].route || ''
+ for (const tab of TAB_LIST) {
+ if (route.includes(tab.key)) return tab.key
+ }
+ }
+ return 'index'
+}
+
+/** 获取底部安全距离 */
+function useSafeBottom(): number {
+ try {
+ const info = Taro.getSystemInfoSync()
+ if (info.safeArea) {
+ return info.safeArea.bottom
+ }
+ } catch { /* ignore */ }
+ return 0
+}
+
+export default function CustomTabBar() {
+ const activeKey = useActiveKey()
+ const safeBottom = useSafeBottom()
+ const [publishVisible, setPublishVisible] = useState(false)
+
+ /** 切换 Tab —— 使用 Taro.switchTab 与原生 tabBar 行为一致 */
+ const handleTabClick = useCallback((tab: TabItem) => {
+ if (tab.key === activeKey) return
+ Taro.switchTab({ url: tab.pagePath })
+ }, [activeKey])
+
+ /** 发布弹窗 */
+ const handlePublish = () => setPublishVisible(true)
+ const handlePublishClose = () => setPublishVisible(false)
+ const handlePublishSelect = (option: PublishOption) => {
+ setPublishVisible(false)
+ Taro.showToast({ title: `${option.label}即将上线`, icon: 'none' })
+ }
+
+ // 将 4 个 tab 拆分为 [0,1] + 发布按钮 + [2,3]
+ const leftTabs = TAB_LIST.slice(0, 2)
+ const rightTabs = TAB_LIST.slice(2)
+
+ return (
+ <>
+
+ {leftTabs.map((tab) => (
+ handleTabClick(tab)}
+ >
+ {tab.icon}
+ {tab.label}
+
+ ))}
+
+ {/* 中间发布按钮 —— 不属于 tabBar list,纯 UI 元素 */}
+
+
+ +
+
+ 发布
+
+
+ {rightTabs.map((tab) => (
+ handleTabClick(tab)}
+ >
+ {tab.icon}
+ {tab.label}
+
+ ))}
+
+
+ {/* 发布弹窗 —— 使用 VantUI Popup,独立于 tabBar 渲染 */}
+
+ >
+ )
+}
diff --git a/src/pages/index/index.tsx b/src/pages/index/index.tsx
index 42d5f84..6e58767 100644
--- a/src/pages/index/index.tsx
+++ b/src/pages/index/index.tsx
@@ -5,6 +5,7 @@ import { Search, Swiper, SwiperItem, Grid, GridItem, Tabs, Tab, Image as VantIma
import ArticleCard from '@/components/ArticleCard'
import { banners, navItems, articles } from '@/mock/articles'
import type { ArticleCategory } from '@/types/article'
+import CustomTabBar from "@/custom-tab-bar";
import './index.less'
/** 获取状态栏和导航栏高度 */
@@ -163,6 +164,8 @@ export default function Index() {
{/* 底部安全距离 */}
+
+
)
}
diff --git a/src/pages/messages/index.config.ts b/src/pages/messages/index.config.ts
new file mode 100644
index 0000000..4bbd8f2
--- /dev/null
+++ b/src/pages/messages/index.config.ts
@@ -0,0 +1,4 @@
+export default definePageConfig({
+ navigationBarTitleText: '消息',
+ navigationStyle: 'custom',
+})
diff --git a/src/pages/messages/index.less b/src/pages/messages/index.less
new file mode 100644
index 0000000..64f76aa
--- /dev/null
+++ b/src/pages/messages/index.less
@@ -0,0 +1,29 @@
+.messages-page {
+ min-height: 100vh;
+ background: #f7f8fa;
+
+ .page-placeholder {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding-top: 300px;
+
+ .placeholder-icon {
+ font-size: 96px;
+ margin-bottom: 24px;
+ }
+
+ .placeholder-text {
+ font-size: 36px;
+ font-weight: 500;
+ color: #323233;
+ margin-bottom: 12px;
+ }
+
+ .placeholder-desc {
+ font-size: 28px;
+ color: #969799;
+ }
+ }
+}
diff --git a/src/pages/messages/index.tsx b/src/pages/messages/index.tsx
new file mode 100644
index 0000000..ec84474
--- /dev/null
+++ b/src/pages/messages/index.tsx
@@ -0,0 +1,16 @@
+import { View, Text } from '@tarojs/components'
+import CustomTabBar from "@/custom-tab-bar"
+import './index.less'
+
+export default function Messages() {
+ return (
+
+
+ 💬
+ 消息
+ 即将上线,敬请期待
+
+
+
+ )
+}
diff --git a/src/pages/profile/index.config.ts b/src/pages/profile/index.config.ts
new file mode 100644
index 0000000..977ab5f
--- /dev/null
+++ b/src/pages/profile/index.config.ts
@@ -0,0 +1,4 @@
+export default definePageConfig({
+ navigationBarTitleText: '我的',
+ navigationStyle: 'custom',
+})
diff --git a/src/pages/profile/index.less b/src/pages/profile/index.less
new file mode 100644
index 0000000..c66ca7b
--- /dev/null
+++ b/src/pages/profile/index.less
@@ -0,0 +1,29 @@
+.profile-page {
+ min-height: 100vh;
+ background: #f7f8fa;
+
+ .page-placeholder {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding-top: 300px;
+
+ .placeholder-icon {
+ font-size: 96px;
+ margin-bottom: 24px;
+ }
+
+ .placeholder-text {
+ font-size: 36px;
+ font-weight: 500;
+ color: #323233;
+ margin-bottom: 12px;
+ }
+
+ .placeholder-desc {
+ font-size: 28px;
+ color: #969799;
+ }
+ }
+}
diff --git a/src/pages/profile/index.tsx b/src/pages/profile/index.tsx
new file mode 100644
index 0000000..59d0af2
--- /dev/null
+++ b/src/pages/profile/index.tsx
@@ -0,0 +1,16 @@
+import { View, Text } from '@tarojs/components'
+import CustomTabBar from "@/custom-tab-bar"
+import './index.less'
+
+export default function Profile() {
+ return (
+
+
+ 👤
+ 我的
+ 即将上线,敬请期待
+
+
+
+ )
+}
diff --git a/src/pages/tasks/index.config.ts b/src/pages/tasks/index.config.ts
new file mode 100644
index 0000000..1c4a702
--- /dev/null
+++ b/src/pages/tasks/index.config.ts
@@ -0,0 +1,5 @@
+export default definePageConfig({
+ navigationBarTitleText: '校园任务',
+ navigationStyle: 'custom',
+ "usingComponents": {}
+})
diff --git a/src/pages/tasks/index.less b/src/pages/tasks/index.less
new file mode 100644
index 0000000..c26d7f5
--- /dev/null
+++ b/src/pages/tasks/index.less
@@ -0,0 +1,29 @@
+.tasks-page {
+ min-height: 100vh;
+ background: #f7f8fa;
+
+ .page-placeholder {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding-top: 300px;
+
+ .placeholder-icon {
+ font-size: 96px;
+ margin-bottom: 24px;
+ }
+
+ .placeholder-text {
+ font-size: 36px;
+ font-weight: 500;
+ color: #323233;
+ margin-bottom: 12px;
+ }
+
+ .placeholder-desc {
+ font-size: 28px;
+ color: #969799;
+ }
+ }
+}
diff --git a/src/pages/tasks/index.tsx b/src/pages/tasks/index.tsx
new file mode 100644
index 0000000..3a1a8f3
--- /dev/null
+++ b/src/pages/tasks/index.tsx
@@ -0,0 +1,16 @@
+import { View, Text } from '@tarojs/components'
+import CustomTabBar from "@/custom-tab-bar"
+import './index.less'
+
+export default function Tasks() {
+ return (
+
+
+ 📋
+ 校园任务
+ 即将上线,敬请期待
+
+
+
+ )
+}