feat(mini): 大厅沉浸式——去除原生导航栏,两级 Tab(一级TABS+二级筛选)sticky 吸顶悬浮不随内容滚动

- hall 页新增 index.config.js:navigationStyle=custom(与其他 tabBar 页一致,去掉原生状态栏/双标题)
- 二级筛选 chips 从各 pane 提升到 Hall 顶层,与一级 TABS 连续吸顶
- hl-tabs/hl-chips 加 position:sticky + 毛玻璃背景,top 由 getTopInset() navH + 实测 tab 高度动态赋值
- 服务 tab 无筛选不渲染 chips
This commit is contained in:
Pine
2026-09-01 20:32:49 +08:00
parent dd6df37395
commit eeaf9526e5
3 changed files with 72 additions and 42 deletions
@@ -0,0 +1 @@
export default { navigationBarTitleText: '大厅', navigationStyle: 'custom' };
+65 -39
View File
@@ -2,12 +2,14 @@ import { View, Text, Button } from '@tarojs/components';
import Taro from '@tarojs/taro';
import { useState, useEffect, useCallback } from 'react';
import { hallOverview, hallTasks, hallTalents, hallJobs, hallServices } from '@/utils/hall';
import { PageHeader } from '@/components/TopInset';
import { PageHeader, getTopInset } from '@/components/TopInset';
import Skeleton from '@/components/skeleton';
import { isAuthed } from '@/utils/auth';
import '@/styles/hall.scss';
/* 大厅(Tab 容器):任务 / 人才 / 岗位 / 服务 / 社区 页内切换;详情跳子页 */
/* 大厅(Tab 容器):任务 / 人才 / 岗位 / 服务 / 社区 页内切换;详情跳子页
* 原生导航栏关闭(navigationStyle=custom),两级 Tab(一级 TABS + 二级筛选 chips
* 吸顶悬浮在顶部,内容滚动时不被带走。 */
const TABS = [
{ k: 'tasks', l: '任务' },
{ k: 'talents', l: '人才' },
@@ -15,6 +17,23 @@ const TABS = [
{ k: 'services', l: '服务' },
{ k: 'community', l: '社区' },
];
const CHIPS = {
tasks: [
{ k: '', l: '全部' }, { k: 'grab', l: '抢单' }, { k: 'bid', l: '投标' },
{ k: 'register', l: '报名' }, { k: 'assign', l: '指派' },
],
talents: [
{ k: '', l: '全部' }, { k: 'opc', l: 'OPC' }, { k: 'seeker', l: '求职者' },
],
jobs: [
{ k: '', l: '全部' }, { k: 'fulltime', l: '全职' }, { k: 'parttime', l: '兼职' },
{ k: 'intern', l: '实习' }, { k: 'remote', l: '远程' },
],
community: [
{ k: '', l: '全部' }, { k: '交流', l: '交流' }, { k: '求助', l: '求助' },
{ k: '资源', l: '资源' }, { k: '经验', l: '经验' }, { k: '公告', l: '公告' },
],
};
const MODE_LABEL = { grab: '抢单', bid: '投标', register: '报名', assign: '指派', recommend: '推荐' };
const WT = { fulltime: '全职', parttime: '兼职', intern: '实习', remote: '远程' };
const money = (a, b) => (!a && !b ? '面议' : a && b && a !== b ? `¥${a}${b}` : `¥${a || b}`);
@@ -29,33 +48,63 @@ const PUBLISH_URL = {
export default function Hall() {
const [tab, setTab] = useState('tasks');
const [stat, setStat] = useState(null);
useEffect(() => { hallOverview().then(setStat).catch(() => {}); }, []);
const [mode, setMode] = useState(''); // 任务筛选
const [ttype, setTtype] = useState(''); // 人才筛选
const [wt, setWt] = useState(''); // 岗位筛选
const [topic, setTopic] = useState(''); // 社区筛选
const [tabBarH, setTabBarH] = useState(0); // 一级 tab 实测高度(吸顶基准)
const { navH } = getTopInset();
useEffect(() => {
hallOverview().then(setStat).catch(() => {});
Taro.nextTick(() => {
Taro.createSelectorQuery()
.select('.hl-tabs')
.boundingClientRect((r) => { if (r && r.height) setTabBarH(r.height); })
.exec();
});
}, []);
const goPublish = () => {
if (!isAuthed()) { Taro.showToast({ title: '请先登录', icon: 'none' }); return; }
Taro.navigateTo({ url: PUBLISH_URL[tab] || PUBLISH_URL.tasks });
};
// 当前二级筛选
const cur = CHIPS[tab] || null;
const chipVal = tab === 'tasks' ? mode : tab === 'talents' ? ttype : tab === 'jobs' ? wt : topic;
const chipSet = tab === 'tasks' ? setMode : tab === 'talents' ? setTtype : tab === 'jobs' ? setWt : setTopic;
return (
<View className="hl-pad page-in">
<PageHeader no="HALL" title="大厅" en="HUB" />
<View className="hl-tabs">
{/* 一级 Tab:吸顶在自定义标题下沿 */}
<View className="hl-tabs" style={{ top: `${navH}px`, zIndex: 60 }}>
{TABS.map((t) => (
<Text key={t.k} className={`hl-tab${tab === t.k ? ' on' : ''}`} onClick={() => setTab(t.k)}>{t.l}</Text>
))}
<View className="hl-tab-spacer" />
<Text className="hl-pub" onClick={goPublish}>+ 发布</Text>
</View>
{tab === 'tasks' && <TasksPane />}
{tab === 'talents' && <TalentsPane />}
{tab === 'jobs' && <JobsPane />}
{/* 二级筛选:吸顶在一级 Tab 下沿(无筛选的 tab 不渲染) */}
{cur && (
<View className="hl-chips" style={{ top: `${navH + tabBarH}px`, zIndex: 59 }}>
{cur.map((c) => (
<Text key={c.k} className={`hl-chip${chipVal === c.k ? ' on' : ''}`} onClick={() => chipSet(c.k)}>{c.l}</Text>
))}
</View>
)}
{tab === 'tasks' && <TasksPane mode={mode} onMode={setMode} />}
{tab === 'talents' && <TalentsPane ttype={ttype} onTtype={setTtype} />}
{tab === 'jobs' && <JobsPane wt={wt} onWt={setWt} />}
{tab === 'services' && <ServicesPane />}
{tab === 'community' && <CommunityPane />}
{tab === 'community' && <CommunityPane topic={topic} onTopic={setTopic} />}
</View>
);
}
/* ---------- 任务 ---------- */
function TasksPane() {
const [mode, setMode] = useState('');
function TasksPane({ mode, onMode }) {
const [items, setItems] = useState(null);
const [err, setErr] = useState('');
const load = useCallback(() => {
@@ -64,11 +113,6 @@ function TasksPane() {
useEffect(load, [load]);
return (
<>
<View className="hl-chips">
{[{ k: '', l: '全部' }, { k: 'grab', l: '抢单' }, { k: 'bid', l: '投标' }, { k: 'register', l: '报名' }, { k: 'assign', l: '指派' }].map((m) => (
<Text key={m.k} className={`hl-chip${mode === m.k ? ' on' : ''}`} onClick={() => setMode(m.k)}>{m.l}</Text>
))}
</View>
{err && <Text className="hl-err">{err}</Text>}
{!items && !err && <Skeleton />}
{items && items.length === 0 && <Text className="hl-empty">暂无任务</Text>}
@@ -95,20 +139,14 @@ function TasksPane() {
}
/* ---------- 人才 ---------- */
function TalentsPane() {
const [type, setType] = useState('');
function TalentsPane({ ttype, onTtype }) {
const [items, setItems] = useState(null);
const [err, setErr] = useState('');
useEffect(() => {
hallTalents({ type }).then((d) => setItems(d.items || [])).catch((e) => setErr(e.message));
}, [type]);
hallTalents({ type: ttype }).then((d) => setItems(d.items || [])).catch((e) => setErr(e.message));
}, [ttype]);
return (
<>
<View className="hl-chips">
{[{ k: '', l: '全部' }, { k: 'opc', l: 'OPC' }, { k: 'seeker', l: '求职者' }].map((t) => (
<Text key={t.k} className={`hl-chip${type === t.k ? ' on' : ''}`} onClick={() => setType(t.k)}>{t.l}</Text>
))}
</View>
{err && <Text className="hl-err">{err}</Text>}
{!items && !err && <Skeleton />}
{items && items.length === 0 && <Text className="hl-empty">暂无人才档案</Text>}
@@ -134,8 +172,7 @@ function TalentsPane() {
}
/* ---------- 岗位 ---------- */
function JobsPane() {
const [wt, setWt] = useState('');
function JobsPane({ wt, onWt }) {
const [items, setItems] = useState(null);
const [err, setErr] = useState('');
useEffect(() => {
@@ -143,11 +180,6 @@ function JobsPane() {
}, [wt]);
return (
<>
<View className="hl-chips">
{[{ k: '', l: '全部' }, { k: 'fulltime', l: '全职' }, { k: 'parttime', l: '兼职' }, { k: 'intern', l: '实习' }, { k: 'remote', l: '远程' }].map((t) => (
<Text key={t.k} className={`hl-chip${wt === t.k ? ' on' : ''}`} onClick={() => setWt(t.k)}>{t.l}</Text>
))}
</View>
{err && <Text className="hl-err">{err}</Text>}
{!items && !err && <Skeleton />}
{items && items.length === 0 && <Text className="hl-empty">暂无招聘岗位</Text>}
@@ -200,21 +232,15 @@ function ServicesPane() {
}
/* ---------- 社区(列表页内切换;帖子详情跳子页) ---------- */
function CommunityPane() {
const [topic, setTopic] = useState('全部');
function CommunityPane({ topic, onTopic }) {
const [posts, setPosts] = useState(null);
const load = useCallback(() => {
import('@/utils/hall').then(({ communityPosts }) =>
communityPosts({ topic: topic === '全部' ? '' : topic }).then((d) => setPosts(d.items || [])).catch(() => {}));
communityPosts({ topic: topic === '' ? '' : topic }).then((d) => setPosts(d.items || [])).catch(() => {}));
}, [topic]);
useEffect(load, [load]);
return (
<>
<View className="hl-chips">
{['全部', '交流', '求助', '资源', '经验', '公告'].map((t) => (
<Text key={t} className={`hl-chip${topic === t ? ' on' : ''}`} onClick={() => setTopic(t)}>{t}</Text>
))}
</View>
{!posts && <Skeleton />}
{posts && posts.length === 0 && <Text className="hl-empty">还没有帖子来发第一帖</Text>}
{(posts || []).map((p) => (
+6 -3
View File
@@ -12,7 +12,8 @@
.hl-err { display: block; padding: 40rpx 24rpx; text-align: center; color: #e5484d; font-size: 26rpx; }
.hl-empty { display: block; padding: 100rpx 0; text-align: center; color: #9aa7bd; font-size: 26rpx; }
.hl-chips { display: flex; gap: 14rpx; overflow-x: auto; padding: 8rpx 24rpx 20rpx; white-space: nowrap;
.hl-chips { position: sticky; display: flex; gap: 14rpx; overflow-x: auto; padding: 8rpx 24rpx 20rpx; white-space: nowrap;
background: rgba(247, 249, 255, 0.92); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
&::-webkit-scrollbar { display: none; } }
.hl-chip { flex: none; padding: 10rpx 26rpx; border-radius: 999rpx; background: #fff; color: #6b7a99;
font-size: 24rpx; border: 1rpx solid #e6ebf5;
@@ -76,8 +77,10 @@
.hl-pcontent { display: block; font-size: 25rpx; line-height: 1.7; color: #7a879e; margin-top: 8rpx; }
.hl-pfoot { display: flex; gap: 36rpx; margin-top: 16rpx; font-size: 22rpx; color: #9aa7bd; }
/* 大厅 Tab(页内切换) */
.hl-tabs { display: flex; align-items: center; gap: 8rpx; margin: 0 24rpx 20rpx; }
/* 大厅 Tab(页内切换,吸顶悬浮不随内容滚动;top 由页面动态赋值 */
.hl-tabs { position: sticky; display: flex; align-items: center; gap: 8rpx; margin: 0 24rpx 12rpx;
padding: 14rpx 0 10rpx; background: rgba(247, 249, 255, 0.92);
backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); }
.hl-tab { padding: 10rpx 26rpx; font-size: 26rpx; color: #6b7a99;
&.on { color: #3a5fe0; font-weight: 700; border-bottom: 4rpx solid #3a5fe0; } }
.hl-tab-spacer { flex: 1; }