feat(mini): 大厅与社区页面(同步大厅升级体系)
- utils/hall.js(/hall/* /community/* 请求封装)+ styles/hall.scss 共享样式
- 大厅 Hub(四宫格统计入口)、人才大厅列表/详情、岗位大厅列表/详情+投递、
服务大厅列表/详情+咨询
- 社区页(话题筛选帖子流+点赞+发帖)与发帖页;实体社区复用载体列表(parks)
- task-detail/tasks 扩展 register 报名模式(报名走 /hall/tasks/{id}/register)
- 首页新增「大厅」「社区」入口卡;app.config.js 注册 9 个新页面
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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 (
|
||||
<View className="hl-pad page-in">
|
||||
<PageHeader no="COMM" title="发帖" en="POST" />
|
||||
<View className="hl-detail">
|
||||
<View className="hl-chips" style={{ padding: 0 }}>
|
||||
{TOPICS.map((t) => (
|
||||
<Text key={t} className={`hl-chip${f.topic === t ? ' on' : ''}`} onClick={() => setF((s) => ({ ...s, topic: t }))}>{t}</Text>
|
||||
))}
|
||||
</View>
|
||||
<Input className="hl-input" placeholder="标题" value={f.title} onInput={(e) => setF((s) => ({ ...s, title: e.detail.value }))} />
|
||||
<Textarea className="hl-textarea" style={{ minHeight: 240 }} placeholder="正文…" value={f.content} onInput={(e) => setF((s) => ({ ...s, content: e.detail.value }))} />
|
||||
{msg && <Text className="hl-err">{msg}</Text>}
|
||||
<View className="hl-actions">
|
||||
<Button className="hl-btn" loading={busy} onClick={submit}>发布</Button>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
@import '@/styles/hall.scss';
|
||||
@@ -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 (
|
||||
<View className="hl-pad page-in">
|
||||
<PageHeader no="COMM" title="社区" en="COMMUNITY" />
|
||||
<View className="hl-chips">
|
||||
{TOPICS.map((t) => (
|
||||
<Text key={t} className={`hl-chip${topic === t ? ' on' : ''}`} onClick={() => setTopic(t)}>{t}</Text>
|
||||
))}
|
||||
</View>
|
||||
{err && <Text className="hl-err">{err}</Text>}
|
||||
{!posts && !err && <Skeleton />}
|
||||
{posts && posts.length === 0 && <Text className="hl-empty">还没有帖子,来发第一帖</Text>}
|
||||
{(posts || []).map((p) => (
|
||||
<View key={p.id} className="hl-post">
|
||||
<View className="hl-post-head">
|
||||
<View className="hl-avatar" style={{ width: 64, height: 64, fontSize: 26 }}>
|
||||
<Text>{(p.authorName || '·').slice(0, 1)}</Text>
|
||||
</View>
|
||||
<View>
|
||||
<Text className="hl-pauthor">{p.authorName}</Text>
|
||||
<Text className="hl-pmeta">{p.topic}{p.pinned ? ' · 置顶' : ''} · {(p.createdAt || '').slice(0, 10)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Text className="hl-ptitle">{p.title}</Text>
|
||||
<Text className="hl-pcontent">{p.content}</Text>
|
||||
<View className="hl-pfoot">
|
||||
<Text onClick={() => like(p.id)}>赞 {p.likeCount || 0}</Text>
|
||||
<Text>评论 {p.commentCount || 0}</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
|
||||
<View style={{ display: 'flex', gap: 18, margin: '10rpx 24rpx 30rpx' }}>
|
||||
<Button className="hl-btn ghost" onClick={() => Taro.navigateTo({ url: '/pages/community-post/index' })}>发帖</Button>
|
||||
<Button className="hl-btn ghost" onClick={() => Taro.navigateTo({ url: '/pages/parks/index' })}>实体社区 · 创业载体</Button>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
@import '@/styles/hall.scss';
|
||||
@@ -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 <View className="hl-pad page-in"><PageHeader no="JOBS" title="岗位详情" en="JOB" /><Text className="hl-err">{err}</Text></View>;
|
||||
if (!job) return <View className="hl-pad page-in"><PageHeader no="JOBS" title="岗位详情" en="JOB" /><Skeleton /></View>;
|
||||
|
||||
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 (
|
||||
<View className="hl-pad page-in">
|
||||
<PageHeader no="JOBS" title={job.title} en="JOB" />
|
||||
<View className="hl-detail">
|
||||
<View className="hl-tags">
|
||||
<Text className="hl-tag">{job.category || '综合'}</Text>
|
||||
<Text className="hl-tag price">{salary(job.salaryMin, job.salaryMax)}</Text>
|
||||
</View>
|
||||
<Text className="hl-dtitle">{job.title}</Text>
|
||||
<View className="hl-meta">
|
||||
<Text>{job.companyName || '企业'}</Text>
|
||||
<Text>{job.location || '云南'}</Text>
|
||||
{job.headcount > 0 && <Text>招 {job.headcount} 人</Text>}
|
||||
</View>
|
||||
{job.description ? <><Text className="hl-sec">岗位职责</Text><Text className="hl-para">{job.description}</Text></> : null}
|
||||
{job.requirement ? <><Text className="hl-sec">任职要求</Text><Text className="hl-para">{job.requirement}</Text></> : null}
|
||||
<Text className="hl-sec">投递简历</Text>
|
||||
<Input className="hl-input" placeholder="联系方式(手机号 / 微信)" value={contact} onInput={(e) => setContact(e.detail.value)} />
|
||||
<Textarea className="hl-textarea" placeholder="意向说明(可选)" value={note} onInput={(e) => setNote(e.detail.value)} />
|
||||
{msg && <Text className="hl-pmeta" style={{ color: msg.includes('成功') ? '#188a4c' : '#e5484d' }}>{msg}</Text>}
|
||||
<View className="hl-actions">
|
||||
<Button className="hl-btn" loading={busy} onClick={apply}>投递简历</Button>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
@import '@/styles/hall.scss';
|
||||
@@ -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 (
|
||||
<View className="hl-pad page-in">
|
||||
<PageHeader no="JOBS" title="岗位大厅" en="JOBS" />
|
||||
<View className="hl-chips">
|
||||
{TYPES.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>}
|
||||
<View className="hl-cards">
|
||||
{(items || []).map((j) => (
|
||||
<View key={j.id} className="hl-card" onClick={() => Taro.navigateTo({ url: `/pages/hall-job-detail/index?id=${j.id}` })}>
|
||||
<View className="hl-tags">
|
||||
<Text className="hl-tag">{j.category || '综合'}</Text>
|
||||
<Text className="hl-tag">{WT[j.workType] || j.workType}</Text>
|
||||
<Text className="hl-tag price">{salary(j.salaryMin, j.salaryMax)}</Text>
|
||||
</View>
|
||||
<Text className="hl-name">{j.title}</Text>
|
||||
<Text className="hl-desc">{j.description}</Text>
|
||||
<View className="hl-foot"><Text>{j.companyName || '企业'}</Text><Text>{j.location || '云南'}</Text></View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
@import '@/styles/hall.scss';
|
||||
@@ -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 <View className="hl-pad page-in"><PageHeader no="SVC" title="服务详情" en="SERVICE" /><Text className="hl-err">{err}</Text></View>;
|
||||
if (!s) return <View className="hl-pad page-in"><PageHeader no="SVC" title="服务详情" en="SERVICE" /><Skeleton /></View>;
|
||||
|
||||
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 (
|
||||
<View className="hl-pad page-in">
|
||||
<PageHeader no="SVC" title={s.title} en="SERVICE" />
|
||||
<View className="hl-detail">
|
||||
<View className="hl-tags">
|
||||
<Text className="hl-tag">{s.category || '综合'}</Text>
|
||||
<Text className="hl-tag price">{s.price ? `¥${s.price}` : '面议'}</Text>
|
||||
</View>
|
||||
<Text className="hl-dtitle">{s.title}</Text>
|
||||
<View className="hl-meta">
|
||||
<Text>提供者 {s.opcName}</Text>
|
||||
{s.deliveryDays > 0 && <Text>{s.deliveryDays} 天交付</Text>}
|
||||
</View>
|
||||
{s.description ? <><Text className="hl-sec">服务介绍</Text><Text className="hl-para">{s.description}</Text></> : null}
|
||||
{s.talent && s.talent.displayName ? (
|
||||
<>
|
||||
<Text className="hl-sec">服务方</Text>
|
||||
<View className="hl-talent">
|
||||
<View className="hl-avatar"><Text>{(s.talent.displayName || '·').slice(0, 1)}</Text></View>
|
||||
<View>
|
||||
<Text className="hl-name" style={{ marginTop: 0 }}>{s.talent.displayName}</Text>
|
||||
<Text className="hl-pmeta">{s.talent.headline}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
) : null}
|
||||
<Text className="hl-sec">立即咨询</Text>
|
||||
<Input className="hl-input" placeholder="联系方式(手机号 / 微信)" value={contact} onInput={(e) => setContact(e.detail.value)} />
|
||||
<Textarea className="hl-textarea" placeholder="需求说明(可选)" value={note} onInput={(e) => setNote(e.detail.value)} />
|
||||
{msg && <Text className="hl-pmeta" style={{ color: msg.includes('成功') ? '#188a4c' : '#e5484d' }}>{msg}</Text>}
|
||||
<View className="hl-actions">
|
||||
<Button className="hl-btn" loading={busy} onClick={inquire}>提交咨询</Button>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
@import '@/styles/hall.scss';
|
||||
@@ -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 (
|
||||
<View className="hl-pad page-in">
|
||||
<PageHeader no="SVC" title="服务大厅" en="SERVICES" />
|
||||
{err && <Text className="hl-err">{err}</Text>}
|
||||
{!items && !err && <Skeleton />}
|
||||
{items && items.length === 0 && <Text className="hl-empty">暂无服务</Text>}
|
||||
<View className="hl-cards">
|
||||
{(items || []).map((s) => (
|
||||
<View key={s.id} className="hl-card" onClick={() => Taro.navigateTo({ url: `/pages/hall-service-detail/index?id=${s.id}` })}>
|
||||
<View className="hl-tags">
|
||||
<Text className="hl-tag">{s.category || '综合'}</Text>
|
||||
<Text className="hl-tag price">{s.price ? `¥${s.price}` : '面议'}</Text>
|
||||
{s.deliveryDays > 0 && <Text className="hl-tag">{s.deliveryDays} 天交付</Text>}
|
||||
</View>
|
||||
<Text className="hl-name">{s.title}</Text>
|
||||
<Text className="hl-desc">{s.description}</Text>
|
||||
<View className="hl-foot"><Text>{s.opcName}</Text><Text>{s.orderCount ? `${s.orderCount} 单` : `评分 ${s.rating || 5}`}</Text></View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
@import '@/styles/hall.scss';
|
||||
@@ -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 <View className="hl-pad page-in"><PageHeader no="TALENT" title="人才详情" en="TALENT" /><Text className="hl-err">{err}</Text></View>;
|
||||
if (!t) return <View className="hl-pad page-in"><PageHeader no="TALENT" title="人才详情" en="TALENT" /><Skeleton /></View>;
|
||||
return (
|
||||
<View className="hl-pad page-in">
|
||||
<PageHeader no="TALENT" title={t.displayName} en={t.talentType === 'seeker' ? 'SEEKER' : 'OPC'} />
|
||||
<View className="hl-detail">
|
||||
<View className="hl-talent">
|
||||
<View className="hl-avatar"><Text>{(t.displayName || '·').slice(0, 1)}</Text></View>
|
||||
<View>
|
||||
<Text className="hl-name" style={{ marginTop: 0 }}>{t.displayName}</Text>
|
||||
<Text className="hl-pmeta">{t.headline}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className="hl-kv">
|
||||
{t.region ? <View><Text className="hl-kv-v">{t.region}</Text><Text className="hl-kv-l">常驻地区</Text></View> : null}
|
||||
<View><Text className="hl-kv-v">{t.workYears || 0} 年</Text><Text className="hl-kv-l">工作年限</Text></View>
|
||||
{t.education ? <View><Text className="hl-kv-v">{t.education}</Text><Text className="hl-kv-l">学历</Text></View> : null}
|
||||
<View><Text className="hl-kv-v">{t.casesCount || 0} 例</Text><Text className="hl-kv-l">服务案例</Text></View>
|
||||
</View>
|
||||
{t.bio ? <><Text className="hl-sec">自我介绍</Text><Text className="hl-para">{t.bio}</Text></> : null}
|
||||
{(t.fields || []).length > 0 && <><Text className="hl-sec">领域</Text><View className="hl-tags">{t.fields.map((x) => <Text className="hl-tag" key={x}>{x}</Text>)}</View></>}
|
||||
{(t.skills || []).length > 0 && <><Text className="hl-sec">技能</Text><View className="hl-tags">{t.skills.map((x) => <Text className="hl-tag" key={x}>{x}</Text>)}</View></>}
|
||||
{(t.services || []).length > 0 && (
|
||||
<>
|
||||
<Text className="hl-sec">TA 的服务</Text>
|
||||
{t.services.map((s) => (
|
||||
<View key={s.id} className="hl-card" style={{ marginBottom: 14 }} onClick={() => Taro.navigateTo({ url: `/pages/hall-service-detail/index?id=${s.id}` })}>
|
||||
<Text className="hl-name" style={{ marginTop: 0 }}>{s.title}</Text>
|
||||
<Text className="hl-pmeta">{s.price ? `¥${s.price}` : '面议'}{s.deliveryDays ? ` · ${s.deliveryDays} 天交付` : ''}</Text>
|
||||
</View>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
@import '@/styles/hall.scss';
|
||||
@@ -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 (
|
||||
<View className="hl-pad page-in">
|
||||
<PageHeader no="TALENT" title="人才大厅" en="TALENTS" />
|
||||
<View className="hl-chips">
|
||||
{TYPES.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>}
|
||||
<View className="hl-cards">
|
||||
{(items || []).map((t) => (
|
||||
<View key={t.userId} className="hl-card" onClick={() => Taro.navigateTo({ url: `/pages/hall-talent-detail/index?id=${t.userId}` })}>
|
||||
<View className="hl-talent">
|
||||
<View className="hl-avatar"><Text>{(t.displayName || '·').slice(0, 1)}</Text></View>
|
||||
<View style={{ flex: 1, minWidth: 0 }}>
|
||||
<Text className="hl-name" style={{ marginTop: 0 }}>{t.displayName} · {t.talentType === 'seeker' ? '求职者' : 'OPC'}</Text>
|
||||
<Text className="hl-desc">{t.headline}</Text>
|
||||
<View className="hl-tags" style={{ marginTop: 10 }}>
|
||||
{(t.fields || []).map((x) => <Text className="hl-tag" key={x}>{x}</Text>)}
|
||||
{(t.skills || []).slice(0, 4).map((x) => <Text className="hl-tag" key={x}>{x}</Text>)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
@import '@/styles/hall.scss';
|
||||
@@ -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 (
|
||||
<View className="hl-pad page-in">
|
||||
<PageHeader no="HALL" title="大厅" en="HUB" />
|
||||
<View className="hl-hub">
|
||||
{HALLS.map((h) => (
|
||||
<View key={h.key} className={`hl-hub-card${h.hot ? ' hot' : ''}`}
|
||||
onClick={() => Taro.navigateTo({ url: h.url })}>
|
||||
<Text className="hl-hub-num">{num(h.stat)}</Text>
|
||||
<Text className="hl-hub-name">{h.name}</Text>
|
||||
<Text className="hl-hub-desc">{h.desc}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
@import '@/styles/hall.scss';
|
||||
@@ -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 ? (
|
||||
<Button className="btn btn-ghost" disabled>暂不可抢</Button>
|
||||
) : (
|
||||
<Button className="btn btn-cta" loading={busy} onClick={grab}>{task.mode === 'bid' ? '报名' : '抢单'}</Button>
|
||||
<Button className="btn btn-cta" loading={busy} onClick={grab}>{task.mode === 'bid' ? '投标(请到 Web 端报价)' : task.mode === 'register' ? '立即报名' : '抢单'}</Button>
|
||||
)}
|
||||
{!isAuthed() && <Text className="tkd-note">抢单/报名需登录,去「我的」绑定手机号。</Text>}
|
||||
</View>
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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; }
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user