diff --git a/miniprogram/src/app.config.js b/miniprogram/src/app.config.js
index b0791e6..56f9478 100644
--- a/miniprogram/src/app.config.js
+++ b/miniprogram/src/app.config.js
@@ -38,6 +38,15 @@ export default {
'pages/pay-qr/index',
'pages/parks/index',
'pages/park-detail/index',
+ 'pages/hall/index',
+ 'pages/hall-talents/index',
+ 'pages/hall-talent-detail/index',
+ 'pages/hall-jobs/index',
+ 'pages/hall-job-detail/index',
+ 'pages/hall-services/index',
+ 'pages/hall-service-detail/index',
+ 'pages/community/index',
+ 'pages/community-post/index',
'pages/park-screens/index',
'pages/park-tasks/index',
'pages/park-screen-ctl/index',
diff --git a/miniprogram/src/pages/community-post/index.jsx b/miniprogram/src/pages/community-post/index.jsx
new file mode 100644
index 0000000..f941556
--- /dev/null
+++ b/miniprogram/src/pages/community-post/index.jsx
@@ -0,0 +1,43 @@
+import { View, Text, Input, Textarea, Button } from '@tarojs/components';
+import Taro from '@tarojs/taro';
+import { useState } from 'react';
+import { communityCreatePost } from '@/utils/hall';
+import { isAuthed } from '@/utils/auth';
+import { PageHeader } from '@/components/TopInset';
+import '@/styles/hall.scss';
+
+const TOPICS = ['交流', '求助', '资源', '经验', '公告'];
+
+export default function CommunityPost() {
+ const [f, setF] = useState({ topic: '交流', title: '', content: '' });
+ const [msg, setMsg] = useState('');
+ const [busy, setBusy] = useState(false);
+ const submit = async () => {
+ if (!isAuthed()) { Taro.showToast({ title: '请先登录', icon: 'none' }); return; }
+ setBusy(true); setMsg('');
+ try {
+ await communityCreatePost(f);
+ Taro.showToast({ title: '发布成功', icon: 'success' });
+ setTimeout(() => Taro.navigateBack(), 800);
+ } catch (e) { setMsg((e && e.message) || '发布失败'); }
+ finally { setBusy(false); }
+ };
+ return (
+
+
+
+
+ {TOPICS.map((t) => (
+ setF((s) => ({ ...s, topic: t }))}>{t}
+ ))}
+
+ setF((s) => ({ ...s, title: e.detail.value }))} />
+
+
+ );
+}
diff --git a/miniprogram/src/pages/community-post/index.scss b/miniprogram/src/pages/community-post/index.scss
new file mode 100644
index 0000000..32b8902
--- /dev/null
+++ b/miniprogram/src/pages/community-post/index.scss
@@ -0,0 +1 @@
+@import '@/styles/hall.scss';
diff --git a/miniprogram/src/pages/community/index.jsx b/miniprogram/src/pages/community/index.jsx
new file mode 100644
index 0000000..6636f95
--- /dev/null
+++ b/miniprogram/src/pages/community/index.jsx
@@ -0,0 +1,70 @@
+import { View, Text, Button } from '@tarojs/components';
+import Taro from '@tarojs/taro';
+import { useState, useEffect } from 'react';
+import { communityPosts, communityLikePost } from '@/utils/hall';
+import { getParksPublic } from '@/utils/park';
+import { isAuthed } from '@/utils/auth';
+import { PageHeader } from '@/components/TopInset';
+import Skeleton from '@/components/skeleton';
+import '@/styles/hall.scss';
+
+const TOPICS = ['全部', '交流', '求助', '资源', '经验', '公告'];
+const shortCity = (n) => String(n || '').replace(/(市|自治州|自治县)$/, '');
+
+export default function Community() {
+ const [topic, setTopic] = useState('全部');
+ const [posts, setPosts] = useState(null);
+ const [carriers, setCarriers] = useState([]);
+ const [err, setErr] = useState('');
+ useEffect(() => {
+ const t = topic === '全部' ? '' : topic;
+ communityPosts({ topic: t }).then((d) => setPosts(d.items || [])).catch((e) => setErr(e.message));
+ }, [topic]);
+ useEffect(() => { getParksPublic().then((d) => setCarriers((d.items || []).slice(0, 4))).catch(() => {}); }, []);
+
+ const like = async (id) => {
+ if (!isAuthed()) { Taro.showToast({ title: '请先登录', icon: 'none' }); return; }
+ try {
+ const r = await communityLikePost(id);
+ setPosts((ls) => ls.map((p) => (p.id === id ? { ...p, likeCount: p.likeCount + (r.liked ? 1 : -1) } : p)));
+ } catch { /* 忽略点赞失败 */ }
+ };
+
+ return (
+
+
+
+ {TOPICS.map((t) => (
+ setTopic(t)}>{t}
+ ))}
+
+ {err && {err}}
+ {!posts && !err && }
+ {posts && posts.length === 0 && 还没有帖子,来发第一帖}
+ {(posts || []).map((p) => (
+
+
+
+ {(p.authorName || '·').slice(0, 1)}
+
+
+ {p.authorName}
+ {p.topic}{p.pinned ? ' · 置顶' : ''} · {(p.createdAt || '').slice(0, 10)}
+
+
+ {p.title}
+ {p.content}
+
+ like(p.id)}>赞 {p.likeCount || 0}
+ 评论 {p.commentCount || 0}
+
+
+ ))}
+
+
+
+
+
+
+ );
+}
diff --git a/miniprogram/src/pages/community/index.scss b/miniprogram/src/pages/community/index.scss
new file mode 100644
index 0000000..32b8902
--- /dev/null
+++ b/miniprogram/src/pages/community/index.scss
@@ -0,0 +1 @@
+@import '@/styles/hall.scss';
diff --git a/miniprogram/src/pages/hall-job-detail/index.jsx b/miniprogram/src/pages/hall-job-detail/index.jsx
new file mode 100644
index 0000000..0b6ba85
--- /dev/null
+++ b/miniprogram/src/pages/hall-job-detail/index.jsx
@@ -0,0 +1,64 @@
+import { View, Text, Input, Textarea, Button } from '@tarojs/components';
+import Taro from '@tarojs/taro';
+import { useState, useEffect } from 'react';
+import { getCurrentInstance } from '@tarojs/taro';
+import { hallJobDetail, hallApplyJob } from '@/utils/hall';
+import { isAuthed } from '@/utils/auth';
+import { PageHeader } from '@/components/TopInset';
+import Skeleton from '@/components/skeleton';
+import '@/styles/hall.scss';
+
+const salary = (a, b) => (!a && !b ? '面议' : a && b && a !== b ? `${a}–${b} 元/月` : `${a || b} 元/月`);
+
+export default function JobDetail() {
+ const [job, setJob] = useState(null);
+ const [err, setErr] = useState('');
+ const [contact, setContact] = useState('');
+ const [note, setNote] = useState('');
+ const [msg, setMsg] = useState('');
+ const [busy, setBusy] = useState(false);
+ useEffect(() => {
+ const id = getCurrentInstance().router && getCurrentInstance().router.params.id;
+ if (!id) { setErr('缺少参数'); return; }
+ hallJobDetail(id).then(setJob).catch((e) => setErr(e.message));
+ }, []);
+ if (err) return {err};
+ if (!job) return ;
+
+ const apply = async () => {
+ if (!isAuthed()) { Taro.showToast({ title: '请先登录', icon: 'none' }); return; }
+ setBusy(true); setMsg('');
+ try {
+ await hallApplyJob(job.id, { contact, note });
+ setMsg('投递成功,企业可在投递列表看到你。');
+ } catch (e) { setMsg((e && e.message) || '投递失败'); }
+ finally { setBusy(false); }
+ };
+
+ return (
+
+
+
+
+ {job.category || '综合'}
+ {salary(job.salaryMin, job.salaryMax)}
+
+ {job.title}
+
+ {job.companyName || '企业'}
+ {job.location || '云南'}
+ {job.headcount > 0 && 招 {job.headcount} 人}
+
+ {job.description ? <>岗位职责{job.description}> : null}
+ {job.requirement ? <>任职要求{job.requirement}> : null}
+ 投递简历
+ setContact(e.detail.value)} />
+
+
+ );
+}
diff --git a/miniprogram/src/pages/hall-job-detail/index.scss b/miniprogram/src/pages/hall-job-detail/index.scss
new file mode 100644
index 0000000..32b8902
--- /dev/null
+++ b/miniprogram/src/pages/hall-job-detail/index.scss
@@ -0,0 +1 @@
+@import '@/styles/hall.scss';
diff --git a/miniprogram/src/pages/hall-jobs/index.jsx b/miniprogram/src/pages/hall-jobs/index.jsx
new file mode 100644
index 0000000..32cfc61
--- /dev/null
+++ b/miniprogram/src/pages/hall-jobs/index.jsx
@@ -0,0 +1,47 @@
+import { View, Text } from '@tarojs/components';
+import Taro from '@tarojs/taro';
+import { useState, useEffect } from 'react';
+import { hallJobs } from '@/utils/hall';
+import { PageHeader } from '@/components/TopInset';
+import Skeleton from '@/components/skeleton';
+import '@/styles/hall.scss';
+
+const TYPES = [{ k: '', l: '全部' }, { k: 'fulltime', l: '全职' }, { k: 'parttime', l: '兼职' }, { k: 'intern', l: '实习' }, { k: 'remote', l: '远程' }];
+const WT = { fulltime: '全职', parttime: '兼职', intern: '实习', remote: '远程' };
+const salary = (a, b) => (!a && !b ? '面议' : a && b && a !== b ? `${a}–${b}元/月` : `${a || b}元/月`);
+
+export default function HallJobs() {
+ const [wt, setWt] = useState('');
+ const [items, setItems] = useState(null);
+ const [err, setErr] = useState('');
+ useEffect(() => {
+ hallJobs({ work_type: wt }).then((d) => setItems(d.items || [])).catch((e) => setErr(e.message));
+ }, [wt]);
+ return (
+
+
+
+ {TYPES.map((t) => (
+ setWt(t.k)}>{t.l}
+ ))}
+
+ {err && {err}}
+ {!items && !err && }
+ {items && items.length === 0 && 暂无招聘岗位}
+
+ {(items || []).map((j) => (
+ Taro.navigateTo({ url: `/pages/hall-job-detail/index?id=${j.id}` })}>
+
+ {j.category || '综合'}
+ {WT[j.workType] || j.workType}
+ {salary(j.salaryMin, j.salaryMax)}
+
+ {j.title}
+ {j.description}
+ {j.companyName || '企业'}{j.location || '云南'}
+
+ ))}
+
+
+ );
+}
diff --git a/miniprogram/src/pages/hall-jobs/index.scss b/miniprogram/src/pages/hall-jobs/index.scss
new file mode 100644
index 0000000..32b8902
--- /dev/null
+++ b/miniprogram/src/pages/hall-jobs/index.scss
@@ -0,0 +1 @@
+@import '@/styles/hall.scss';
diff --git a/miniprogram/src/pages/hall-service-detail/index.jsx b/miniprogram/src/pages/hall-service-detail/index.jsx
new file mode 100644
index 0000000..40963ec
--- /dev/null
+++ b/miniprogram/src/pages/hall-service-detail/index.jsx
@@ -0,0 +1,72 @@
+import { View, Text, Input, Textarea, Button } from '@tarojs/components';
+import Taro from '@tarojs/taro';
+import { useState, useEffect } from 'react';
+import { getCurrentInstance } from '@tarojs/taro';
+import { hallServiceDetail, hallServiceInquiry } from '@/utils/hall';
+import { isAuthed } from '@/utils/auth';
+import { PageHeader } from '@/components/TopInset';
+import Skeleton from '@/components/skeleton';
+import '@/styles/hall.scss';
+
+export default function ServiceDetail() {
+ const [s, setS] = useState(null);
+ const [err, setErr] = useState('');
+ const [contact, setContact] = useState('');
+ const [note, setNote] = useState('');
+ const [msg, setMsg] = useState('');
+ const [busy, setBusy] = useState(false);
+ useEffect(() => {
+ const id = getCurrentInstance().router && getCurrentInstance().router.params.id;
+ if (!id) { setErr('缺少参数'); return; }
+ hallServiceDetail(id).then(setS).catch((e) => setErr(e.message));
+ }, []);
+ if (err) return {err};
+ if (!s) return ;
+
+ const inquire = async () => {
+ if (!isAuthed()) { Taro.showToast({ title: '请先登录', icon: 'none' }); return; }
+ setBusy(true); setMsg('');
+ try {
+ await hallServiceInquiry(s.id, { contact, note });
+ setMsg('咨询已提交,服务方会尽快联系你。');
+ } catch (e) { setMsg((e && e.message) || '提交失败'); }
+ finally { setBusy(false); }
+ };
+
+ return (
+
+
+
+
+ {s.category || '综合'}
+ {s.price ? `¥${s.price}` : '面议'}
+
+ {s.title}
+
+ 提供者 {s.opcName}
+ {s.deliveryDays > 0 && {s.deliveryDays} 天交付}
+
+ {s.description ? <>服务介绍{s.description}> : null}
+ {s.talent && s.talent.displayName ? (
+ <>
+ 服务方
+
+ {(s.talent.displayName || '·').slice(0, 1)}
+
+ {s.talent.displayName}
+ {s.talent.headline}
+
+
+ >
+ ) : null}
+ 立即咨询
+ setContact(e.detail.value)} />
+
+
+ );
+}
diff --git a/miniprogram/src/pages/hall-service-detail/index.scss b/miniprogram/src/pages/hall-service-detail/index.scss
new file mode 100644
index 0000000..32b8902
--- /dev/null
+++ b/miniprogram/src/pages/hall-service-detail/index.scss
@@ -0,0 +1 @@
+@import '@/styles/hall.scss';
diff --git a/miniprogram/src/pages/hall-services/index.jsx b/miniprogram/src/pages/hall-services/index.jsx
new file mode 100644
index 0000000..59d13c7
--- /dev/null
+++ b/miniprogram/src/pages/hall-services/index.jsx
@@ -0,0 +1,37 @@
+import { View, Text } from '@tarojs/components';
+import Taro from '@tarojs/taro';
+import { useState, useEffect } from 'react';
+import { hallServices } from '@/utils/hall';
+import { PageHeader } from '@/components/TopInset';
+import Skeleton from '@/components/skeleton';
+import '@/styles/hall.scss';
+
+export default function HallServices() {
+ const [items, setItems] = useState(null);
+ const [err, setErr] = useState('');
+ useEffect(() => {
+ hallServices().then((d) => setItems(d.items || [])).catch((e) => setErr(e.message));
+ }, []);
+ return (
+
+
+ {err && {err}}
+ {!items && !err && }
+ {items && items.length === 0 && 暂无服务}
+
+ {(items || []).map((s) => (
+ Taro.navigateTo({ url: `/pages/hall-service-detail/index?id=${s.id}` })}>
+
+ {s.category || '综合'}
+ {s.price ? `¥${s.price}` : '面议'}
+ {s.deliveryDays > 0 && {s.deliveryDays} 天交付}
+
+ {s.title}
+ {s.description}
+ {s.opcName}{s.orderCount ? `${s.orderCount} 单` : `评分 ${s.rating || 5}`}
+
+ ))}
+
+
+ );
+}
diff --git a/miniprogram/src/pages/hall-services/index.scss b/miniprogram/src/pages/hall-services/index.scss
new file mode 100644
index 0000000..32b8902
--- /dev/null
+++ b/miniprogram/src/pages/hall-services/index.scss
@@ -0,0 +1 @@
+@import '@/styles/hall.scss';
diff --git a/miniprogram/src/pages/hall-talent-detail/index.jsx b/miniprogram/src/pages/hall-talent-detail/index.jsx
new file mode 100644
index 0000000..9418918
--- /dev/null
+++ b/miniprogram/src/pages/hall-talent-detail/index.jsx
@@ -0,0 +1,54 @@
+import { View, Text } from '@tarojs/components';
+import Taro from '@tarojs/taro';
+import { useState, useEffect } from 'react';
+import { getCurrentInstance } from '@tarojs/taro';
+import { hallTalentDetail } from '@/utils/hall';
+import { PageHeader } from '@/components/TopInset';
+import Skeleton from '@/components/skeleton';
+import '@/styles/hall.scss';
+
+export default function TalentDetail() {
+ const [t, setT] = useState(null);
+ const [err, setErr] = useState('');
+ useEffect(() => {
+ const id = getCurrentInstance().router && getCurrentInstance().router.params.id;
+ if (!id) { setErr('缺少参数'); return; }
+ hallTalentDetail(id).then(setT).catch((e) => setErr(e.message));
+ }, []);
+ if (err) return {err};
+ if (!t) return ;
+ return (
+
+
+
+
+ {(t.displayName || '·').slice(0, 1)}
+
+ {t.displayName}
+ {t.headline}
+
+
+
+ {t.region ? {t.region}常驻地区 : null}
+ {t.workYears || 0} 年工作年限
+ {t.education ? {t.education}学历 : null}
+ {t.casesCount || 0} 例服务案例
+
+ {t.bio ? <>自我介绍{t.bio}> : null}
+ {(t.fields || []).length > 0 && <>领域{t.fields.map((x) => {x})}>}
+ {(t.skills || []).length > 0 && <>技能{t.skills.map((x) => {x})}>}
+ {(t.services || []).length > 0 && (
+ <>
+ TA 的服务
+ {t.services.map((s) => (
+ Taro.navigateTo({ url: `/pages/hall-service-detail/index?id=${s.id}` })}>
+ {s.title}
+ {s.price ? `¥${s.price}` : '面议'}{s.deliveryDays ? ` · ${s.deliveryDays} 天交付` : ''}
+
+ ))}
+ >
+ )}
+
+
+ );
+}
diff --git a/miniprogram/src/pages/hall-talent-detail/index.scss b/miniprogram/src/pages/hall-talent-detail/index.scss
new file mode 100644
index 0000000..32b8902
--- /dev/null
+++ b/miniprogram/src/pages/hall-talent-detail/index.scss
@@ -0,0 +1 @@
+@import '@/styles/hall.scss';
diff --git a/miniprogram/src/pages/hall-talents/index.jsx b/miniprogram/src/pages/hall-talents/index.jsx
new file mode 100644
index 0000000..69b5219
--- /dev/null
+++ b/miniprogram/src/pages/hall-talents/index.jsx
@@ -0,0 +1,48 @@
+import { View, Text } from '@tarojs/components';
+import Taro from '@tarojs/taro';
+import { useState, useEffect } from 'react';
+import { hallTalents } from '@/utils/hall';
+import { PageHeader } from '@/components/TopInset';
+import Skeleton from '@/components/skeleton';
+import '@/styles/hall.scss';
+
+const TYPES = [{ k: '', l: '全部' }, { k: 'opc', l: 'OPC' }, { k: 'seeker', l: '求职者' }];
+
+export default function HallTalents() {
+ const [type, setType] = useState('');
+ const [items, setItems] = useState(null);
+ const [err, setErr] = useState('');
+ useEffect(() => {
+ hallTalents({ type }).then((d) => setItems(d.items || [])).catch((e) => setErr(e.message));
+ }, [type]);
+ return (
+
+
+
+ {TYPES.map((t) => (
+ setType(t.k)}>{t.l}
+ ))}
+
+ {err && {err}}
+ {!items && !err && }
+ {items && items.length === 0 && 暂无人才档案}
+
+ {(items || []).map((t) => (
+ Taro.navigateTo({ url: `/pages/hall-talent-detail/index?id=${t.userId}` })}>
+
+ {(t.displayName || '·').slice(0, 1)}
+
+ {t.displayName} · {t.talentType === 'seeker' ? '求职者' : 'OPC'}
+ {t.headline}
+
+ {(t.fields || []).map((x) => {x})}
+ {(t.skills || []).slice(0, 4).map((x) => {x})}
+
+
+
+
+ ))}
+
+
+ );
+}
diff --git a/miniprogram/src/pages/hall-talents/index.scss b/miniprogram/src/pages/hall-talents/index.scss
new file mode 100644
index 0000000..32b8902
--- /dev/null
+++ b/miniprogram/src/pages/hall-talents/index.scss
@@ -0,0 +1 @@
+@import '@/styles/hall.scss';
diff --git a/miniprogram/src/pages/hall/index.jsx b/miniprogram/src/pages/hall/index.jsx
new file mode 100644
index 0000000..037bda3
--- /dev/null
+++ b/miniprogram/src/pages/hall/index.jsx
@@ -0,0 +1,35 @@
+import { View, Text } from '@tarojs/components';
+import Taro from '@tarojs/taro';
+import { useState, useEffect } from 'react';
+import { hallOverview } from '@/utils/hall';
+import { PageHeader } from '@/components/TopInset';
+import '@/styles/hall.scss';
+
+/* 大厅 Hub:任务 / 人才 / 岗位 / 服务 四厅总入口 */
+const HALLS = [
+ { key: 'tasks', name: '任务大厅', desc: '抢单 · 投标 · 报名', stat: 'tasks', url: '/pages/tasks/index', hot: true },
+ { key: 'talents', name: '人才大厅', desc: 'OPC 与求职者档案', stat: 'talents', url: '/pages/hall-talents/index' },
+ { key: 'jobs', name: '岗位大厅', desc: '企业招聘 · 在线投递', stat: 'jobs', url: '/pages/hall-jobs/index' },
+ { key: 'services', name: '服务大厅', desc: 'OPC 服务与产品', stat: 'services', url: '/pages/hall-services/index' }
+];
+
+export default function Hall() {
+ const [stat, setStat] = useState(null);
+ useEffect(() => { hallOverview().then(setStat).catch(() => {}); }, []);
+ const num = (k) => (stat && Number.isFinite(stat[k]) ? stat[k] : '—');
+ return (
+
+
+
+ {HALLS.map((h) => (
+ Taro.navigateTo({ url: h.url })}>
+ {num(h.stat)}
+ {h.name}
+ {h.desc}
+
+ ))}
+
+
+ );
+}
diff --git a/miniprogram/src/pages/hall/index.scss b/miniprogram/src/pages/hall/index.scss
new file mode 100644
index 0000000..32b8902
--- /dev/null
+++ b/miniprogram/src/pages/hall/index.scss
@@ -0,0 +1 @@
+@import '@/styles/hall.scss';
diff --git a/miniprogram/src/pages/task-detail/index.jsx b/miniprogram/src/pages/task-detail/index.jsx
index 016ff96..ec25e87 100644
--- a/miniprogram/src/pages/task-detail/index.jsx
+++ b/miniprogram/src/pages/task-detail/index.jsx
@@ -4,9 +4,10 @@ import { useState, useEffect } from 'react';
import { getCurrentInstance } from '@tarojs/taro';
import { isAuthed } from '@/utils/auth';
import { getTask, grabById } from '@/utils/tasks';
+import { hallRegisterTask } from '@/utils/hall';
import './index.scss';
-const MODE = { grab: '抢单', bid: '竞标', assign: '指派', recommend: '推荐' };
+const MODE = { grab: '抢单', bid: '竞标', register: '报名', assign: '指派', recommend: '推荐' };
const money = (a, b) => (a || b) ? (a && b && a !== b ? `¥${a}–${b}` : `¥${a || b}`) : '面议';
const getId = () => (getCurrentInstance().router && getCurrentInstance().router.params) || {};
@@ -27,10 +28,16 @@ export default function TaskDetail() {
if (!isAuthed()) { setErr('抢单需先登录(我的 → 登录绑定)。'); return; }
setBusy(true); setErr('');
try {
- await grabById(task.id);
- Taro.showToast({ title: '接单成功', icon: 'success' });
- setErr('接单成功,可在「我的」查看。');
- } catch (e) { setErr((e && e.message) || '抢单失败'); }
+ if (task.mode === 'register') {
+ await hallRegisterTask(task.id);
+ Taro.showToast({ title: '报名成功', icon: 'success' });
+ setErr('报名成功,等待发包方选定。');
+ } else {
+ await grabById(task.id);
+ Taro.showToast({ title: '接单成功', icon: 'success' });
+ setErr('接单成功,可在「我的」查看。');
+ }
+ } catch (e) { setErr((e && e.message) || '操作失败'); }
finally { setBusy(false); }
};
@@ -76,7 +83,7 @@ export default function TaskDetail() {
{can === false ? (
) : (
-
+
)}
{!isAuthed() && 抢单/报名需登录,去「我的」绑定手机号。}
diff --git a/miniprogram/src/pages/tasks/index.jsx b/miniprogram/src/pages/tasks/index.jsx
index afbcb5d..6f0d817 100644
--- a/miniprogram/src/pages/tasks/index.jsx
+++ b/miniprogram/src/pages/tasks/index.jsx
@@ -5,7 +5,7 @@ import { isAuthed } from '@/utils/auth';
import { listSquare, grabById } from '@/utils/tasks';
import './index.scss';
-const MODE = { grab: '抢单', bid: '竞标', assign: '指派', recommend: '推荐' };
+const MODE = { grab: '抢单', bid: '竞标', register: '报名', assign: '指派', recommend: '推荐' };
const money = (a, b) => (a || b) ? (a && b && a !== b ? `¥${a}–${b}` : `¥${a || b}`) : '面议';
export default function Tasks() {
diff --git a/miniprogram/src/styles/hall.scss b/miniprogram/src/styles/hall.scss
new file mode 100644
index 0000000..ab201c3
--- /dev/null
+++ b/miniprogram/src/styles/hall.scss
@@ -0,0 +1,77 @@
+/* 大厅与社区共享样式(Soft UI 柔彩卡片语言) */
+
+.hl-pad { padding-bottom: 60rpx; }
+.hl-head {
+ margin: 8rpx 24rpx 20rpx;
+ display: flex;
+ align-items: flex-end;
+ justify-content: space-between;
+}
+.hl-title { font-size: 38rpx; font-weight: 800; color: #1c2b4a; }
+.hl-sub { display: block; margin-top: 6rpx; font-size: 22rpx; color: #9aa7bd; }
+.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;
+ &::-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;
+ &.on { color: #3a5fe0; border-color: #b9c8f5; background: #eef2ff; font-weight: 600; } }
+
+/* Hub 四宫格 */
+.hl-hub { margin: 0 24rpx; display: grid; grid-template-columns: 1fr 1fr; gap: 20rpx; }
+.hl-hub-card { background: #fff; border-radius: 24rpx; padding: 30rpx 28rpx; box-shadow: 0 8rpx 30rpx rgba(60, 90, 180, .06);
+ &.hot { background: linear-gradient(135deg, #eef2ff, #f4efff); } }
+.hl-hub-num { font-size: 44rpx; font-weight: 800; color: #3a5fe0; }
+.hl-hub-name { font-size: 30rpx; font-weight: 700; color: #1c2b4a; margin-top: 6rpx; }
+.hl-hub-desc { font-size: 21rpx; color: #9aa7bd; margin-top: 8rpx; line-height: 1.5; }
+
+/* 通用卡片列表 */
+.hl-cards { margin: 0 24rpx; }
+.hl-card { background: #fff; border-radius: 24rpx; padding: 26rpx 28rpx; margin-bottom: 20rpx;
+ box-shadow: 0 8rpx 30rpx rgba(60, 90, 180, .06); }
+.hl-tags { display: flex; flex-wrap: wrap; gap: 10rpx; }
+.hl-tag { padding: 4rpx 18rpx; border-radius: 999rpx; background: #f1f4fb; color: #6b7a99; font-size: 20rpx;
+ white-space: nowrap;
+ &.grab { background: #e8f6ee; color: #188a4c; }
+ &.bid { background: #fff3e4; color: #c46a10; }
+ &.register { background: #ece9ff; color: #6a4fd8; }
+ &.price { background: #fdeeee; color: #d0453e; font-weight: 600; } }
+.hl-name { display: block; font-size: 30rpx; font-weight: 700; color: #1c2b4a; margin-top: 12rpx; }
+.hl-desc { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
+ margin-top: 8rpx; font-size: 24rpx; line-height: 1.6; color: #7a879e; }
+.hl-foot { display: flex; justify-content: space-between; margin-top: 14rpx; font-size: 21rpx; color: #9aa7bd; }
+
+/* 详情 */
+.hl-detail { margin: 0 24rpx 20rpx; background: #fff; border-radius: 24rpx; padding: 30rpx 30rpx 34rpx;
+ box-shadow: 0 8rpx 30rpx rgba(60, 90, 180, .06); }
+.hl-dtitle { display: block; font-size: 36rpx; font-weight: 800; color: #1c2b4a; margin-top: 14rpx; }
+.hl-meta { display: flex; flex-wrap: wrap; gap: 20rpx; margin-top: 10rpx; font-size: 22rpx; color: #9aa7bd; }
+.hl-sec { display: block; font-size: 28rpx; font-weight: 700; color: #1c2b4a; margin: 28rpx 0 10rpx; }
+.hl-para { display: block; font-size: 26rpx; line-height: 1.8; color: #3c4a63; white-space: pre-wrap; }
+.hl-kv { display: flex; flex-wrap: wrap; gap: 30rpx; margin-top: 20rpx; }
+.hl-kv-v { display: block; font-size: 30rpx; font-weight: 700; color: #1c2b4a; }
+.hl-kv-l { display: block; font-size: 20rpx; color: #9aa7bd; margin-top: 4rpx; }
+.hl-actions { display: flex; gap: 18rpx; margin-top: 30rpx; }
+.hl-btn { flex: 1; margin: 0; height: 80rpx; line-height: 80rpx; border-radius: 999rpx;
+ background: linear-gradient(135deg, #4d7cfe, #7a5ff0); color: #fff; font-size: 27rpx; font-weight: 600; padding: 0;
+ &.ghost { background: #fff; color: #3a5fe0; border: 1rpx solid #c9d6f7; font-weight: 400; } }
+.hl-input { width: 100%; box-sizing: border-box; padding: 18rpx 22rpx; border-radius: 16rpx;
+ background: #f5f7fc; font-size: 25rpx; margin-bottom: 16rpx; }
+.hl-textarea { width: 100%; box-sizing: border-box; min-height: 150rpx; padding: 18rpx 22rpx;
+ border-radius: 16rpx; background: #f5f7fc; font-size: 25rpx; }
+
+/* 人才 */
+.hl-talent { display: flex; gap: 20rpx; align-items: center; }
+.hl-avatar { width: 88rpx; height: 88rpx; border-radius: 50%; background: linear-gradient(135deg, #4d7cfe, #7a5ff0);
+ color: #fff; display: flex; align-items: center; justify-content: center; font-size: 34rpx; font-weight: 700; flex: none; }
+
+/* 帖子 */
+.hl-post { background: #fff; border-radius: 24rpx; padding: 26rpx 28rpx; margin: 0 24rpx 20rpx;
+ box-shadow: 0 8rpx 30rpx rgba(60, 90, 180, .06); }
+.hl-post-head { display: flex; align-items: center; gap: 16rpx; }
+.hl-pauthor { font-size: 25rpx; font-weight: 600; color: #1c2b4a; }
+.hl-pmeta { font-size: 20rpx; color: #9aa7bd; margin-top: 2rpx; }
+.hl-ptitle { display: block; font-size: 29rpx; font-weight: 700; color: #1c2b4a; margin-top: 16rpx; }
+.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; }
diff --git a/miniprogram/src/utils/hall.js b/miniprogram/src/utils/hall.js
new file mode 100644
index 0000000..1c005dc
--- /dev/null
+++ b/miniprogram/src/utils/hall.js
@@ -0,0 +1,47 @@
+import { cachedRequest, request } from './api';
+
+/* 大厅与社区(/hall/* + /community/*,0035)。公开读缓存;写需登录。 */
+
+export const hallOverview = () => cachedRequest('GET', '/hall/overview', 30000);
+
+/* 任务大厅(复用 /hall/tasks;含 can_accept 与 register 报名数) */
+export const hallTasks = (params = {}) => {
+ const qs = Object.entries(params).filter(([, v]) => v).map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&');
+ return cachedRequest('GET', `/hall/tasks${qs ? `?${qs}` : ''}`, 20000);
+};
+export const hallTaskDetail = (id, withAuth = false) => request('GET', `/hall/tasks/${encodeURIComponent(id)}`, {}, withAuth);
+export const hallRegisterTask = (id) => request('POST', `/hall/tasks/${encodeURIComponent(id)}/register`, {}, true);
+export const hallPublishTask = (body) => request('POST', '/hall/tasks', body, true);
+
+/* 人才大厅 */
+export const hallTalents = (params = {}) => {
+ const qs = Object.entries(params).filter(([, v]) => v).map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&');
+ return cachedRequest('GET', `/hall/talents${qs ? `?${qs}` : ''}`, 30000);
+};
+export const hallTalentDetail = (uid) => cachedRequest('GET', `/hall/talents/${encodeURIComponent(uid)}`, 30000);
+
+/* 岗位大厅 */
+export const hallJobs = (params = {}) => {
+ const qs = Object.entries(params).filter(([, v]) => v).map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&');
+ return cachedRequest('GET', `/hall/jobs${qs ? `?${qs}` : ''}`, 30000);
+};
+export const hallJobDetail = (id) => cachedRequest('GET', `/hall/jobs/${encodeURIComponent(id)}`, 30000);
+export const hallApplyJob = (id, body) => request('POST', `/hall/jobs/${encodeURIComponent(id)}/apply`, body, true);
+
+/* 服务大厅 */
+export const hallServices = (params = {}) => {
+ const qs = Object.entries(params).filter(([, v]) => v).map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&');
+ return cachedRequest('GET', `/hall/services${qs ? `?${qs}` : ''}`, 30000);
+};
+export const hallServiceDetail = (id) => cachedRequest('GET', `/hall/services/${encodeURIComponent(id)}`, 30000);
+export const hallServiceInquiry = (id, body) => request('POST', `/hall/services/${encodeURIComponent(id)}/inquiry`, body, true);
+export const hallPublishService = (body) => request('POST', '/hall/services', body, true);
+
+/* 社区 */
+export const communityPosts = (params = {}) => {
+ const qs = Object.entries(params).filter(([, v]) => v).map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&');
+ return cachedRequest('GET', `/community/posts${qs ? `?${qs}` : ''}`, 15000);
+};
+export const communityCreatePost = (body) => request('POST', '/community/posts', body, true);
+export const communityLikePost = (id) => request('POST', `/community/posts/${encodeURIComponent(id)}/like`, {}, true);
+export const communityCommentPost = (id, body) => request('POST', `/community/posts/${encodeURIComponent(id)}/comments`, body, true);