Compare commits
2 Commits
b05a606df8
...
a39fbc7955
| Author | SHA1 | Date | |
|---|---|---|---|
| a39fbc7955 | |||
| 0b264ee9b3 |
@@ -8,7 +8,9 @@ export default {
|
||||
'pages/survey/index',
|
||||
'pages/mine/index',
|
||||
'pages/event-detail/index',
|
||||
'pages/scan/index'
|
||||
'pages/scan/index',
|
||||
'pages/park-apply/index',
|
||||
'pages/scan-login/index'
|
||||
],
|
||||
window: {
|
||||
backgroundTextStyle: 'light',
|
||||
|
||||
@@ -25,6 +25,7 @@ export default function Index() {
|
||||
<View className="mk-cta">
|
||||
<Button className="btn btn-cta" onClick={goTab(MK_HERO.ctaPrimary.tab)}>{MK_HERO.ctaPrimary.label} →</Button>
|
||||
<Button className="btn btn-ghost" onClick={goTab(MK_HERO.ctaSecondary.tab)}>{MK_HERO.ctaSecondary.label}</Button>
|
||||
<Button className="btn btn-ghost" onClick={() => Taro.navigateTo({ url: '/pages/park-apply/index' })}>申请入驻</Button>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { View, Text, Button, Input, Image } from '@tarojs/components';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTabIndex } from '@/utils/tab';
|
||||
import { isAuthed, getUser, logout, wxLogin, phoneLogin, bindPhone, sendCode, wxPhoneBind, updateProfile, getMe } from '@/utils/auth';
|
||||
import { isAuthed, getUser, logout, wxLogin, phoneLogin, bindPhone, sendCode, wxPhoneBind, updateProfile, getMe, mpQrConfirm } from '@/utils/auth';
|
||||
import { getMyBookings, checkIn, parseIso, BK_STATUS_OPTIONS, BK_TOPIC_OPTIONS, BK_SOURCE_OPTIONS } from '@/utils/booking';
|
||||
import Skeleton from '@/components/skeleton';
|
||||
import CountdownBox from '@/components/CountdownBox';
|
||||
@@ -200,6 +200,8 @@ export default function Mine() {
|
||||
const [pTopics, setPTopics] = useState((user && user.topics) || []);
|
||||
const [pSource, setPSource] = useState((user && user.source) || '');
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [scanBusy, setScanBusy] = useState(false);
|
||||
const [scanErr, setScanErr] = useState('');
|
||||
useEffect(() => {
|
||||
setPName((user && user.name) || '');
|
||||
setPStatus((user && user.status) || '');
|
||||
@@ -262,6 +264,23 @@ export default function Mine() {
|
||||
Taro.showToast({ title: '已退出登录', icon: 'none' });
|
||||
};
|
||||
|
||||
/* ============ 跨端口扫码登录:扫电脑/网页登录页二维码 → 小程序确认 ============ */
|
||||
const onScanLogin = async () => {
|
||||
setScanErr(''); setScanBusy(true);
|
||||
try {
|
||||
const res = await Taro.scanCode(); // 扫其它端口登录页的「小程序扫码」二维码
|
||||
const str = res.result || '';
|
||||
const m = str.match(/[?&]scene=([^&#]+)/); // 二维码内容=跨端口 login url,含 scene
|
||||
if (!m) throw new Error('无效的扫码登录二维码');
|
||||
const scene = decodeURIComponent(m[1]);
|
||||
const { code } = await Taro.login(); // 微信登录 code
|
||||
await mpQrConfirm(scene, code); // 确认 → 目标端口轮询取到登录令牌
|
||||
Taro.showToast({ title: '已登录目标端口,请回电脑/网页查看', icon: 'none' });
|
||||
} catch (e) {
|
||||
setScanErr((e && e.message) || '扫码登录失败');
|
||||
} finally { setScanBusy(false); }
|
||||
};
|
||||
|
||||
/* ============ 未登录:登录入口 ============ */
|
||||
if (!user || !isAuthed()) {
|
||||
return (
|
||||
@@ -335,6 +354,14 @@ export default function Mine() {
|
||||
<Button className="btn-ghost logout-btn" onClick={onLogout}>退出</Button>
|
||||
</View>
|
||||
|
||||
{/* 跨端口扫码登录:扫电脑/网页登录页的「小程序扫码」二维码 → 用本小程序确认登录该端口 */}
|
||||
<View className="card next-card">
|
||||
<Text className="next-lbl">跨端口登录</Text>
|
||||
<Text className="bind-tip">在电脑/网页登录页点「小程序扫码」,用本小程序「扫一扫」即可登录该端口</Text>
|
||||
<Button className="btn-main" loading={scanBusy} onClick={onScanLogin}>扫码登录其它端口</Button>
|
||||
{scanErr && <Text className="scan-err">{scanErr}</Text>}
|
||||
</View>
|
||||
|
||||
{/* 下一场活动倒计时(个人信息卡之后显示;取最早的待参加且未开始的报名场次) */}
|
||||
{(() => {
|
||||
const now = Date.now();
|
||||
|
||||
@@ -376,3 +376,5 @@
|
||||
font-size: 34rpx; font-weight: 700; color: #fff; padding: 0; margin: 0;
|
||||
}
|
||||
.save-nick { margin-top: 12rpx; align-self: flex-start; line-height: 2.2; padding: 0 28rpx; font-size: 24rpx; }
|
||||
|
||||
.scan-err { display:block; margin-top:12rpx; font-size:24rpx; color:#f0b8b8; text-align:center; }
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export default { navigationBarTitleText: '园区入驻申请' };
|
||||
@@ -0,0 +1,248 @@
|
||||
import { View, Text, Input, Textarea, Button, Radio, RadioGroup, Checkbox, CheckboxGroup, Picker } from '@tarojs/components';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { phoneLogin, bindPhone, isAuthed, getUser } from '@/utils/auth';
|
||||
import {
|
||||
getParks, submitParkApplication,
|
||||
GENDER_OPTS, COMPANY_TYPE_OPTS, REGION_OPTS, SERVICE_OPTS, DOC_ITEMS, uploadParkDoc,
|
||||
} from '@/utils/parks';
|
||||
import './index.scss';
|
||||
|
||||
const EMPTY = () => ({
|
||||
name: '', birth: '', gender: '', grad_school_major: '', ethnicity: '',
|
||||
grad_time: '', id_card: '', contact_phone: '',
|
||||
company_name: '', legal_person: '', legal_phone: '', registered_capital: '',
|
||||
company_type: '', industry: '', honors: '',
|
||||
emp_total: '', emp_grad: '', emp_layoff: '', emp_veteran: '', emp_migrant: '',
|
||||
project_overview: '',
|
||||
region: '', lease_period: '', services: [], agree: false,
|
||||
});
|
||||
|
||||
export default function ParkApply() {
|
||||
const [authed, setAuthed] = useState(isAuthed());
|
||||
const [gatePhone, setGatePhone] = useState('');
|
||||
const [gateCode, setGateCode] = useState('');
|
||||
const [gateErr, setGateErr] = useState('');
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const [parks, setParks] = useState([]);
|
||||
const [tenantId, setTenantId] = useState('');
|
||||
const [form, setForm] = useState(EMPTY());
|
||||
const [docs, setDocs] = useState({});
|
||||
const [err, setErr] = useState('');
|
||||
const [done, setDone] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const up = (k, v) => setForm((f) => ({ ...f, [k]: v }));
|
||||
|
||||
// 加载园区列表(仅挂载时一次;每次渲染都请求会无限循环)
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
(async () => {
|
||||
try {
|
||||
const t = await getParks();
|
||||
if (!alive) return;
|
||||
setParks(t);
|
||||
if (t[0]) setTenantId(t[0].id);
|
||||
} catch (e) {}
|
||||
finally { if (alive) setLoading(false); }
|
||||
})();
|
||||
return () => { alive = false; };
|
||||
}, []);
|
||||
|
||||
const gateSmsLogin = async () => {
|
||||
setGateErr('');
|
||||
if (!/^1\d{10}$/.test(gatePhone)) { setGateErr('请输入正确的 11 位手机号'); return; }
|
||||
if (!gateCode) { setGateErr('请输入验证码'); return; }
|
||||
setBusy(true);
|
||||
try {
|
||||
if (authed) {
|
||||
const b = await bindPhone(gatePhone, gateCode);
|
||||
if (!b.ok) { setGateErr(b.error || '绑定失败'); return; }
|
||||
} else {
|
||||
await phoneLogin(gatePhone, gateCode);
|
||||
}
|
||||
setAuthed(true);
|
||||
} catch (e) { setGateErr((e && e.message) || '登录失败'); }
|
||||
finally { setBusy(false); }
|
||||
};
|
||||
|
||||
// 选择并上传一个资料文件(图片/pdf/doc),存 url
|
||||
const pickAndUpload = async (key) => {
|
||||
let filePath = null;
|
||||
try {
|
||||
const res = await Taro.chooseMessageFile({ count: 1, type: 'file' });
|
||||
filePath = res.tempFiles && res.tempFiles[0] && res.tempFiles[0].path;
|
||||
} catch (e) {
|
||||
try {
|
||||
const img = await Taro.chooseImage({ count: 1, sizeType: ['compressed'] });
|
||||
filePath = img.tempFilePaths && img.tempFilePaths[0];
|
||||
} catch (_) { return; }
|
||||
}
|
||||
if (!filePath) return;
|
||||
setBusy(true); setErr('');
|
||||
try {
|
||||
const url = await uploadParkDoc(filePath);
|
||||
setDocs((d) => ({ ...d, [key]: url }));
|
||||
} catch (e) { setErr((e && e.message) || '文件上传失败'); }
|
||||
finally { setBusy(false); }
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
setErr('');
|
||||
if (!tenantId) { setErr('请选择意向园区'); return; }
|
||||
if (!form.name || !form.contact_phone || !form.id_card) { setErr('请完整填写申请人信息(姓名/电话/身份证)'); return; }
|
||||
if (!form.agree) { setErr('请勾选企业承诺'); return; }
|
||||
const req = DOC_ITEMS.filter((d) => d.required);
|
||||
if (req.some((d) => !docs[d.key])) { setErr('请上传必填资料(创业计划书 / 身份证 / 学历证明)'); return; }
|
||||
setBusy(true);
|
||||
try {
|
||||
const r = await submitParkApplication({ tenant_id: tenantId, form, docs });
|
||||
setDone(!!r?.id || r?.ok);
|
||||
setErr('');
|
||||
} catch (e) { setErr((e && e.message) || '提交失败,请稍后重试'); }
|
||||
finally { setBusy(false); }
|
||||
};
|
||||
|
||||
if (done) {
|
||||
return (
|
||||
<View className="pa page-in">
|
||||
<View className="ok-icon" />
|
||||
<Text className="ok-title">申请已提交</Text>
|
||||
<Text className="ok-desc">园区将在收到材料后尽快审核。</Text>
|
||||
<Button className="btn btn-cta" onClick={() => Taro.navigateBack()}>返回</Button>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (loading) return <View className="pa page-in"><Text className="err-text">加载中…</Text></View>;
|
||||
|
||||
// 未登录/未绑手机 → 快捷门
|
||||
if (!authed) {
|
||||
return (
|
||||
<View className="pa page-in">
|
||||
<View className="pa-card">
|
||||
<Text className="pa-title">园区入驻申请</Text>
|
||||
<Text className="pa-sub">请先用手机号登录(验证码 123456)</Text>
|
||||
<View className="field"><Input className="input" placeholder="手机号" value={gatePhone} onChange={(e) => setGatePhone(e.detail.value)} /></View>
|
||||
<View className="field"><Input className="input" placeholder="验证码" value={gateCode} onChange={(e) => setGateCode(e.detail.value)} /></View>
|
||||
{gateErr && <Text className="err-text">{gateErr}</Text>}
|
||||
<Button className="btn btn-cta" loading={busy} onClick={gateSmsLogin}>登录 / 绑定</Button>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="pa page-in">
|
||||
<View className="pa-head">
|
||||
<Text className="pa-title">园区入驻申请表</Text>
|
||||
<Text className="pa-sub">对照《入园申请表》逐项填写,附带的各项资料请上传</Text>
|
||||
</View>
|
||||
|
||||
<View className="pa-card">
|
||||
<Text className="pa-sec">一 · 选择意向园区</Text>
|
||||
<Picker range={parks.map((p) => p.name)} value={Math.max(0, parks.findIndex((p) => p.id === tenantId))}
|
||||
onChange={(e) => setTenantId(parks[Number(e.detail.value)]?.id || '')}>
|
||||
<View className="pa-picker">{parks.find((p) => p.id === tenantId)?.name || '请选择园区'} ▾</View>
|
||||
</Picker>
|
||||
<Picker range={GENDER_OPTS} onChange={(e) => up('gender', GENDER_OPTS[Number(e.detail.value)])}>
|
||||
<View className="pa-picker">性别:{form.gender || '请选择'} ▾</View>
|
||||
</Picker>
|
||||
</View>
|
||||
|
||||
<View className="pa-card">
|
||||
<Text className="pa-sec">一 · 申请人信息</Text>
|
||||
<Field label="姓名 *" value={form.name} ph="王思松" onChange={(v) => up('name', v)} />
|
||||
<Field label="出生年月" value={form.birth} ph="如 1998年8月" onChange={(v) => up('birth', v)} />
|
||||
<View className="field"><Text className="pa-label">性别</Text>
|
||||
<RadioGroup onChange={(e) => up('gender', e.detail.value)}><View className="pa-radios">
|
||||
{GENDER_OPTS.map((g) => <Radio key={g} value={g} checked={form.gender === g}>{g}</Radio>)}
|
||||
</View></RadioGroup>
|
||||
</View>
|
||||
<Field label="民族" value={form.ethnicity} ph="汉族" onChange={(v) => up('ethnicity', v)} />
|
||||
<Field label="毕业院校及专业 *" value={form.grad_school_major} ph="华北科技学院 新闻学专业" onChange={(v) => up('grad_school_major', v)} />
|
||||
<Field label="毕业时间" value={form.grad_time} ph="2020年6月" onChange={(v) => up('grad_time', v)} />
|
||||
<Field label="身份证号 *" value={form.id_card} ph="5303…(选填照片可后传)" onChange={(v) => up('id_card', v)} />
|
||||
<Field label="联系电话 *" value={form.contact_phone} ph="176…" onChange={(v) => up('contact_phone', v)} />
|
||||
</View>
|
||||
|
||||
<View className="pa-card">
|
||||
<Text className="pa-sec">二 · 企业信息</Text>
|
||||
<Field label="企业名称 *" value={form.company_name} ph="云南派因人工智能有限公司(拟注册)" onChange={(v) => up('company_name', v)} />
|
||||
<Field label="法定代表人" value={form.legal_person} ph="王思松" onChange={(v) => up('legal_person', v)} />
|
||||
<Field label="法人联系电话" value={form.legal_phone} ph="176…" onChange={(v) => up('legal_phone', v)} />
|
||||
<Field label="注册资本(万元)" value={form.registered_capital} ph="100" type="number" onChange={(v) => up('registered_capital', v)} />
|
||||
<View className="field"><Text className="pa-label">企业类型</Text>
|
||||
<CheckboxGroup onChange={(e) => up('company_type', e.detail.value[0] || '')}><View className="pa-checks">
|
||||
{COMPANY_TYPE_OPTS.map((t) => <Checkbox key={t} value={t} checked={form.company_type === t}>{t}</Checkbox>)}
|
||||
</View></CheckboxGroup>
|
||||
</View>
|
||||
<Field label="所属行业" value={form.industry} ph="人工智能技术(按国民经济行业分类)" onChange={(v) => up('industry', v)} />
|
||||
<Field label="企业所获政策/荣誉" value={form.honors} ph="无" onChange={(v) => up('honors', v)} />
|
||||
<Text className="pa-label">企业人员情况</Text>
|
||||
<View className="pa-grid">
|
||||
<Field label="职工总数" value={form.emp_total} type="number" onChange={(v) => up('emp_total', v)} />
|
||||
<Field label="高校毕业生" value={form.emp_grad} type="number" onChange={(v) => up('emp_grad', v)} />
|
||||
<Field label="下岗失业" value={form.emp_layoff} type="number" onChange={(v) => up('emp_layoff', v)} />
|
||||
<Field label="退伍军人" value={form.emp_veteran} type="number" onChange={(v) => up('emp_veteran', v)} />
|
||||
<Field label="农民工" value={form.emp_migrant} type="number" onChange={(v) => up('emp_migrant', v)} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="pa-card">
|
||||
<Text className="pa-sec">三 · 项目概况</Text>
|
||||
<Text className="pa-label">项目概况 / 商业模式 *</Text>
|
||||
<Textarea className="pa-textarea" placeholder="核心业务、商业模式、目标用户等" value={form.project_overview}
|
||||
onChange={(e) => up('project_overview', e.detail.value)} />
|
||||
</View>
|
||||
|
||||
<View className="pa-card">
|
||||
<Text className="pa-sec">四 · 入驻需求</Text>
|
||||
<View className="field"><Text className="pa-label">拟入驻区域</Text>
|
||||
<CheckboxGroup onChange={(e) => up('region', e.detail.value[0] || '')}><View className="pa-checks">
|
||||
{REGION_OPTS.map((r) => <Checkbox key={r} value={r} checked={form.region === r}>{r}</Checkbox>)}
|
||||
</View></CheckboxGroup>
|
||||
</View>
|
||||
<Field label="租期需求" value={form.lease_period} ph="自入驻之日起 N 年" onChange={(v) => up('lease_period', v)} />
|
||||
<Text className="pa-label">配套服务需求(可多选)</Text>
|
||||
<CheckboxGroup onChange={(e) => up('services', e.detail.value)}><View className="pa-checks">
|
||||
{SERVICE_OPTS.map((s) => <Checkbox key={s} value={s} checked={form.services.includes(s)}>{s}</Checkbox>)}
|
||||
</View></CheckboxGroup>
|
||||
</View>
|
||||
|
||||
<View className="pa-card">
|
||||
<Text className="pa-sec">五 · 企业承诺</Text>
|
||||
<CheckboxGroup onChange={(e) => up('agree', e.detail.value.includes('agree'))}>
|
||||
<View className="pa-checks"><Checkbox value="agree" checked={form.agree}>本人/本企业承诺所填信息及提交材料真实、准确、完整;</Checkbox></View>
|
||||
</CheckboxGroup>
|
||||
</View>
|
||||
|
||||
<View className="pa-card">
|
||||
<Text className="pa-sec">六 · 上传资料</Text>
|
||||
{DOC_ITEMS.map((d) => (
|
||||
<View className="pa-doc" key={d.key}>
|
||||
<Text className="pa-label">{d.label}{d.required ? ' *' : ''}</Text>
|
||||
<Button className="btn btn-ghost" disabled={busy} onClick={() => pickAndUpload(d.key)}>
|
||||
{docs[d.key] ? '已上传 ✓' : '选择文件'}
|
||||
</Button>
|
||||
{docs[d.key] && <Text className="pa-uploaded">{docs[d.key].split('/').pop()}</Text>}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{err && <Text className="err-text">{err}</Text>}
|
||||
<Button className="btn btn-cta" loading={busy} onClick={submit}>提交入驻申请</Button>
|
||||
<View className="pa-foot">提交即授权园区管理方审核材料,进度可在「我的」查看</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function Field({ label, value, ph, type, onChange }) {
|
||||
return (
|
||||
<View className="field">
|
||||
<Text className="pa-label">{label}</Text>
|
||||
<Input className="input" placeholder={ph} value={value} type={type || 'text'} onInput={(e) => onChange(e.detail.value)} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/* 园区入驻申请 · 小程序样式(暗色令牌一致) */
|
||||
.pa { position: relative; z-index: 2; padding: 0 0 60rpx; }
|
||||
.pa-head { padding: 24rpx 16rpx 8rpx; }
|
||||
.pa-title { font-size: 40rpx; font-weight: 700; color: #fff; }
|
||||
.pa-sub { display: block; margin-top: 8rpx; font-size: 25rpx; color: #8e8e8e; }
|
||||
|
||||
.pa-card {
|
||||
margin: 24rpx 16rpx 0;
|
||||
padding: 28rpx 24rpx;
|
||||
background: linear-gradient(180deg, #0e0e12, #0a0a0d);
|
||||
border: 1px solid rgba(255,255,255,.12);
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
.pa-sec { display: block; font-size: 30rpx; font-weight: 700; color: #fff; margin-bottom: 20rpx; }
|
||||
.pa-label { display: block; font-size: 24rpx; color: #8e8e8e; margin: 0 0 10rpx; }
|
||||
|
||||
.field { margin-top: 22rpx; }
|
||||
.input {
|
||||
width: 100%; box-sizing: border-box;
|
||||
height: 88rpx; line-height: 88rpx;
|
||||
background: #141417; border: 1rpx solid rgba(255, 255, 255, 0.14); border-radius: 14px;
|
||||
color: #fff; font-size: 28rpx; padding: 0 22rpx;
|
||||
}
|
||||
.input::placeholder { color: #5b5b5b; }
|
||||
|
||||
.pa-picker {
|
||||
width: 100%; box-sizing: border-box;
|
||||
height: 88rpx; line-height: 88rpx;
|
||||
background: #141417; border: 1rpx solid rgba(255, 255, 255, 0.14); border-radius: 14px;
|
||||
color: #fff; font-size: 28rpx; padding: 0 22rpx;
|
||||
}
|
||||
.pa-radios, .pa-checks { display: flex; flex-wrap: wrap; gap: 12rpx; align-items: center; }
|
||||
.pa-radios radio, .pa-checks checkbox { color: #e8e8e8; font-size: 26rpx; margin: 4rpx 0; }
|
||||
.pa-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0 20rpx; }
|
||||
.pa-textarea {
|
||||
width: 100%; box-sizing: border-box; min-height: 200rpx;
|
||||
background: #141417; border: 1rpx solid rgba(255, 255, 255, 0.14); border-radius: 14px;
|
||||
color: #fff; font-size: 28rpx; line-height: 1.5; padding: 18rpx 22rpx;
|
||||
}
|
||||
.pa-doc { margin: 24rpx 0 0; }
|
||||
.pa-uploaded { display: block; margin-top: 8rpx; font-size: 22rpx; color: #7fd087; }
|
||||
|
||||
.btn-cta {
|
||||
background: linear-gradient(135deg, #5b8cff, #9d6bff);
|
||||
color: #fff; border-radius: 999rpx; font-weight: 600; font-size: 28rpx;
|
||||
text-align: center; line-height: 2.6; padding: 0 40rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(120,110,255,.4); margin-top: 12rpx;
|
||||
}
|
||||
.btn-cta::after { border: none; }
|
||||
.pa button::after { border: none; }
|
||||
|
||||
.err-text { display: block; margin: 20rpx 32rpx 0; font-size: 26rpx; color: #f0b8b8; text-align: center; }
|
||||
.pa-foot { margin: 24rpx 40rpx 0; text-align: center; font-size: 22rpx; color: #6f6f6f; }
|
||||
|
||||
.ok-icon { width: 120rpx; height: 120rpx; margin: 80rpx auto 24rpx; border-radius: 50%; background: #fff; position: relative; }
|
||||
.ok-icon::after { content: ''; position: absolute; left: 50%; top: 50%; width: 46rpx; height: 26rpx; border-left: 8rpx solid #000; border-bottom: 8rpx solid #000; transform: translate(-50%, -62%) rotate(-45deg); }
|
||||
.ok-title { display: block; text-align: center; font-size: 40rpx; font-weight: 700; }
|
||||
.ok-desc { display: block; text-align: center; font-size: 28rpx; color: #c4c2c3; margin-top: 16rpx; line-height: 1.6; }
|
||||
@@ -0,0 +1 @@
|
||||
export default { navigationBarTitleText: '扫码登录' };
|
||||
@@ -0,0 +1,67 @@
|
||||
import { View, Text, Button } from '@tarojs/components';
|
||||
import Taro, { useDidShow } from '@tarojs/taro';
|
||||
import { useState } from 'react';
|
||||
import { mpQrConfirm } from '@/utils/auth';
|
||||
import './index.scss';
|
||||
|
||||
/**
|
||||
* 小程序扫码登录确认页:由「小程序码(wxacode)」自动打开(微信扫一扫→自动跳到本页),
|
||||
* 或用户在小程序内「我的→扫码登录」扫二维码后进入。读取携带的 scene,
|
||||
* 复用当前微信登录资格(Taro.login→code→平台归户)完成目标端口登录。
|
||||
*/
|
||||
export default function ScanLogin() {
|
||||
const [scene, setScene] = useState('');
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [msg, setMsg] = useState('');
|
||||
const [done, setDone] = useState(false);
|
||||
|
||||
useDidShow(() => {
|
||||
const opts = Taro.getEnterOptionsSync();
|
||||
const q = (opts && opts.query) || {};
|
||||
// wxacode 的 scene 会放进 launch options.query.scene;普通 navigateTo 带 ?scene= 同理
|
||||
const s = String(q.scene || opts.scene || '');
|
||||
setScene(s);
|
||||
setDone(false);
|
||||
setMsg('');
|
||||
});
|
||||
|
||||
const confirm = async () => {
|
||||
if (!scene) { setMsg('无效的登录请求(缺少场景)'); return; }
|
||||
setBusy(true); setMsg('');
|
||||
try {
|
||||
const { code } = await Taro.login(); // 复用微信登录资格
|
||||
await mpQrConfirm(scene, code); // 目标端口轮询取到令牌
|
||||
setDone(true);
|
||||
setMsg('登录成功,请回到电脑 / 网页查看');
|
||||
} catch (e) {
|
||||
setMsg((e && e.message) || '登录失败,请重试');
|
||||
} finally { setBusy(false); }
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
// 取消:返回上一页;无上一页则回首页
|
||||
Taro.navigateBack().catch(() => Taro.switchTab({ url: '/pages/index/index' }));
|
||||
};
|
||||
|
||||
return (
|
||||
<View className="sl page-in">
|
||||
<View className="sl-card">
|
||||
<View className="sl-icon">{done ? '✓' : '🔐'}</View>
|
||||
<Text className="sl-title">{done ? '已登录' : '扫码登录确认'}</Text>
|
||||
<Text className="sl-desc">
|
||||
{done ? msg : '确认使用当前微信账号登录你正在操作的端口?'}
|
||||
</Text>
|
||||
{!done && scene && <Text className="sl-scene">登录请求 #{scene.slice(0, 12)}</Text>}
|
||||
{msg && !done && <Text className="sl-err">{msg}</Text>}
|
||||
{!done ? (
|
||||
<View className="sl-actions">
|
||||
<Button className="btn btn-cta" loading={busy} onClick={confirm}>确认登录</Button>
|
||||
<Button className="btn btn-ghost" disabled={busy} onClick={cancel}>取消</Button>
|
||||
</View>
|
||||
) : (
|
||||
<Button className="btn btn-cta" onClick={() => Taro.switchTab({ url: '/pages/index/index' })}>回首页</Button>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/* 小程序扫码登录确认页 */
|
||||
.sl { position: relative; z-index: 2; }
|
||||
.sl-card {
|
||||
margin: 40rpx 32rpx 0; padding: 60rpx 40rpx;
|
||||
background: linear-gradient(180deg, #0e0e12, #0a0a0d);
|
||||
border: 1px solid rgba(255,255,255,.12); border-radius: 20rpx; text-align: center;
|
||||
}
|
||||
.sl-icon { font-size: 72rpx; margin-bottom: 20rpx; }
|
||||
.sl-title { display: block; font-size: 40rpx; font-weight: 700; color: #fff; }
|
||||
.sl-desc { display: block; margin: 16rpx 0 0; font-size: 27rpx; color: #c4c2c3; line-height: 1.6; }
|
||||
.sl-scene { display: block; margin-top: 12rpx; font-size: 23rpx; color: #8e8e8e; }
|
||||
.sl-err { display: block; margin-top: 16rpx; font-size: 25rpx; color: #f0b8b8; }
|
||||
.sl-actions { display: flex; flex-direction: column; gap: 16rpx; margin-top: 40rpx; }
|
||||
@@ -57,10 +57,17 @@ export async function wxLogin({ code, nickName, avatarUrl }) {
|
||||
if (data.token) {
|
||||
Taro.setStorageSync('pine_token', data.token);
|
||||
saveUser(data);
|
||||
return data;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/** 小程序扫码登录其它端口:扫跨端登录页二维码 → 用 wx.login code 确认 → 目标端口轮询取令牌 */
|
||||
export async function mpQrConfirm(scene, code) {
|
||||
const data = await request('POST', '/auth/mp-qr/confirm', { scene, code });
|
||||
return data;
|
||||
}
|
||||
|
||||
/** 拉取当前账号资料(刷新昵称/头像/手机绑定状态)——平台 /auth/me 返回扁平 profile */
|
||||
export async function getMe() {
|
||||
const data = await request('GET', '/auth/me', {}, true);
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import Taro from '@tarojs/taro';
|
||||
import { request } from './api';
|
||||
|
||||
const API_BASE = 'https://opc.pinesound.cn';
|
||||
|
||||
/** 平台可见园区列表(选择园区用,公开) */
|
||||
export async function getParks() {
|
||||
const r = await request('GET', '/api/park/tenants', {}, false);
|
||||
return Array.isArray(r?.tenants) ? r.tenants : [];
|
||||
}
|
||||
|
||||
/** 提交园区入驻申请(需登录) */
|
||||
export async function submitParkApplication(payload) {
|
||||
const r = await request('POST', '/api/park-admission', payload, true);
|
||||
if (!r?.ok) throw new Error(r?.message || '提交失败');
|
||||
return r;
|
||||
}
|
||||
|
||||
/** 我的入驻申请 */
|
||||
export async function getMyAdmission() {
|
||||
const r = await request('GET', '/api/park-admission/mine', {}, true);
|
||||
return Array.isArray(r?.items) ? r.items : [];
|
||||
}
|
||||
|
||||
/** 上传入驻资料(图片/pdf/word),返回 url;失败抛错 */
|
||||
export async function uploadParkDoc(filePath, name = 'file') {
|
||||
const token = Taro.getStorageSync('pine_token') || '';
|
||||
const up = await Taro.uploadFile({
|
||||
url: `${API_BASE}/api/upload`,
|
||||
filePath,
|
||||
name,
|
||||
header: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
const data = JSON.parse(up.data || '{}');
|
||||
if (!data.ok) throw new Error(data.error || '上传失败');
|
||||
return data.url;
|
||||
}
|
||||
|
||||
// ---- 表单选项(对照《入园申请表》) ----
|
||||
export const GENDER_OPTS = ['男', '女'];
|
||||
export const COMPANY_TYPE_OPTS = ['有限责任公司', '股份有限公司', '合伙企业', '个体工商户', '其他'];
|
||||
export const REGION_OPTS = ['直播孵化区', '综合展示区', '创业苗圃区', '孵化加速区', '国际创客区'];
|
||||
export const SERVICE_OPTS = [
|
||||
'办公设备租赁', '政策申报指导', '投融资对接', '人才招聘服务',
|
||||
'技术研发支持', '市场推广服务', '法律咨询', '财务代理', '其他',
|
||||
];
|
||||
|
||||
// 上传资料项(key → label/placeholder)
|
||||
export const DOC_ITEMS = [
|
||||
{ key: 'plan', label: '创业计划书', required: true },
|
||||
{ key: 'id_card_doc', label: '创业者身份证(外籍提供护照/签证)', required: true },
|
||||
{ key: 'diploma', label: '学历证明', required: true },
|
||||
{ key: 'license', label: '企业营业执照(已注册企业)', required: false },
|
||||
{ key: 'cert', label: '相关资质专利证书', required: false },
|
||||
];
|
||||
@@ -25,6 +25,7 @@ import PineLogs from './platform/PineLogs';
|
||||
import Login from './platform/Login';
|
||||
import { ParticleField } from './components/organisms';
|
||||
import { isAuthed } from './services/auth';
|
||||
import ParkApply from './user/ParkApply';
|
||||
|
||||
/* 公开路由(无需登录) */
|
||||
const PUBLIC_ROUTES = [
|
||||
@@ -35,6 +36,7 @@ const PUBLIC_ROUTES = [
|
||||
{ path: 'policy-test', Page: PolicyTest },
|
||||
{ path: 'start-plan', Page: StartPlan },
|
||||
{ path: 'survey', Page: OPCSurvey },
|
||||
{ path: 'park-apply', Page: ParkApply },
|
||||
{ path: 'profile', Page: Profile }
|
||||
];
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import '@/styles/auth.css';
|
||||
/** 二维码轮询登录(微信扫码 / 小程序扫码共用) */
|
||||
function PollingQr({ start, poll, tip, onResult }) {
|
||||
const [qrUrl, setQrUrl] = React.useState('');
|
||||
const [qrImage, setQrImage] = React.useState('');
|
||||
const [status, setStatus] = React.useState('loading');
|
||||
const [error, setError] = React.useState('');
|
||||
|
||||
@@ -21,8 +22,10 @@ function PollingQr({ start, poll, tip, onResult }) {
|
||||
(async () => {
|
||||
setStatus('loading');
|
||||
try {
|
||||
const { scene, qr_url } = await start();
|
||||
setQrUrl(qr_url);
|
||||
const r = await start();
|
||||
const { scene, qr_url, qr_image } = r || {};
|
||||
setQrUrl(qr_url || '');
|
||||
setQrImage(qr_image || '');
|
||||
setStatus('pending');
|
||||
timer = window.setInterval(async () => {
|
||||
const res = await poll(scene);
|
||||
@@ -55,7 +58,7 @@ function PollingQr({ start, poll, tip, onResult }) {
|
||||
return (
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div style={{ display: 'inline-block', padding: 8, background: '#fff', borderRadius: 8 }}>
|
||||
<QRCodeSVG value={qrUrl} size={180} />
|
||||
{qrImage ? <img src={qrImage} alt="小程序码" style={{ width: 180, height: 180 }} /> : <QRCodeSVG value={qrUrl} size={180} />}
|
||||
</div>
|
||||
<p className="auth-sub">{tip}</p>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* 园区入驻申请服务
|
||||
* -------------------------------------------------------------
|
||||
* 对接后端:GET /api/park/tenants(园区列表,公开)、POST /api/park-admission(提交,需登录)、
|
||||
* POST /api/upload(资料上传,需登录)。
|
||||
*/
|
||||
import { getToken } from '@/services/auth';
|
||||
const API_BASE = 'https://opc.pinesound.cn';
|
||||
|
||||
/** 平台可见园区列表(选择园区用) */
|
||||
export async function getParks() {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/park/tenants`);
|
||||
const d = await res.json().catch(() => ({}));
|
||||
return Array.isArray(d.tenants) ? d.tenants : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/** 提交园区入驻申请(表单 + 已上传资料 URL) */
|
||||
export async function submitParkApplication(payload) {
|
||||
const token = getToken();
|
||||
const res = await fetch(`${API_BASE}/api/park-admission`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const r = await res.json().catch(() => ({}));
|
||||
if (!res.ok || !r.ok) throw new Error(r.detail || r.error || '提交失败,请重试');
|
||||
return r;
|
||||
}
|
||||
|
||||
/** 上传一份资料(图片/pdf/doc),返回 url */
|
||||
export async function uploadParkDoc(file) {
|
||||
const token = getToken();
|
||||
const fd = new FormData();
|
||||
fd.append('file', file);
|
||||
const res = await fetch(`${API_BASE}/api/upload`, {
|
||||
method: 'POST',
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
body: fd,
|
||||
});
|
||||
const d = await res.json().catch(() => ({}));
|
||||
if (!res.ok || !d.ok) throw new Error(d.error || '上传失败');
|
||||
return d.url;
|
||||
}
|
||||
|
||||
// ---- 表单选项(对照《入园申请表》) ----
|
||||
export const GENDER_OPTS = ['男', '女'];
|
||||
export const COMPANY_TYPE_OPTS = ['有限责任公司', '股份有限公司', '合伙企业', '个体工商户', '其他'];
|
||||
export const REGION_OPTS = ['直播孵化区', '综合展示区', '创业苗圃区', '孵化加速区', '国际创客区'];
|
||||
export const SERVICE_OPTS = [
|
||||
'办公设备租赁', '政策申报指导', '投融资对接', '人才招聘服务',
|
||||
'技术研发支持', '市场推广服务', '法律咨询', '财务代理', '其他',
|
||||
];
|
||||
export const DOC_ITEMS = [
|
||||
{ key: 'plan', label: '创业计划书', required: true },
|
||||
{ key: 'id_card_doc', label: '创业者身份证(外籍提供护照/签证)', required: true },
|
||||
{ key: 'diploma', label: '学历证明', required: true },
|
||||
{ key: 'license', label: '企业营业执照(已注册企业)', required: false },
|
||||
{ key: 'cert', label: '相关资质专利证书', required: false },
|
||||
];
|
||||
@@ -0,0 +1,30 @@
|
||||
/* 园区入驻申请 · 样式(暗色令牌一致) */
|
||||
|
||||
/* 步骤指示器 */
|
||||
.pf-steps { display: flex; flex-wrap: wrap; gap: 6px 8px; margin: 6px 0 0; }
|
||||
.pf-step { display: flex; align-items: center; gap: 6px; padding: 6px 10px; border-radius: 999px; background: #161618; border: 1px solid #2a2a2d; color: #9b9b9b; font-size: 12.5px; }
|
||||
.pf-step.on { border-color: #fff; color: #fff; }
|
||||
.pf-step.done { border-color: rgba(127,208,135,.5); color: #7fd087; }
|
||||
.pf-step-dot { display: grid; place-items: center; width: 18px; height: 18px; border-radius: 50%; background: #2a2a2d; font-size: 11px; }
|
||||
.pf-step.on .pf-step-dot { background: #fff; color: #111; }
|
||||
|
||||
.pf-sub { margin: 18px 0 4px; font-size: 13.5px; color: #c4c2c3; font-weight: 600; }
|
||||
.pf-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0 14px; }
|
||||
.pf-muted { margin-top: 8px; font-size: 13px; color: #8e8e8e; }
|
||||
|
||||
/* 资料上传 */
|
||||
.pf-doc { padding: 6px 0; }
|
||||
.pf-upl { display: block; margin-top: 8px; cursor: pointer; padding: 10px 12px; border-radius: 10px; background: #161618; border: 1px dashed #2a2a2d; color: #e8e8e8; font-size: 13.5px; }
|
||||
.pf-upl input { display: none; }
|
||||
.pf-fname { display: block; margin-top: 6px; font-size: 12px; color: #7fd087; }
|
||||
|
||||
/* 提交前摘要 */
|
||||
.pf-summary { margin-top: 16px; padding: 12px 14px; border-radius: 12px; background: #161618; border: 1px solid #2a2a2d; font-size: 13.5px; color: #c4c2c3; line-height: 1.8; }
|
||||
.pf-summary b { color: #fff; }
|
||||
|
||||
/* 成功 */
|
||||
.pf-success { text-align: center; padding: 40px 10px; }
|
||||
.pf-success-icon { display: flex; justify-content: center; margin-bottom: 16px; }
|
||||
.pf-success h2 { font-size: 24px; }
|
||||
.pf-success p { color: #c4c2c3; margin-top: 8px; line-height: 1.7; }
|
||||
.pf-success-actions { display: flex; gap: 12px; justify-content: center; margin-top: 24px; }
|
||||
@@ -143,6 +143,21 @@ export default function Home() {
|
||||
{MK_TRUST.map((s, i) => <StatItem key={i} {...s} />)}
|
||||
</section>
|
||||
|
||||
{/* 2.5 · 申请入驻创业园 */}
|
||||
<Reveal as="section" className="section mk-section">
|
||||
<SectionHead no="00" title="申请入驻创业园" en="Park" />
|
||||
<div className="mk-assess">
|
||||
<div className="assess-txt">
|
||||
<span className="assess-badge">入驻</span>
|
||||
<span className="assess-title">填写《入园申请表》入驻园区</span>
|
||||
<span className="assess-desc">选择意向园区,逐项填表并上传创业计划书、身份/学历/营业执照等资料,审核通过即可入驻。</span>
|
||||
</div>
|
||||
<div className="assess-cta">
|
||||
<Button variant="cta" href="#/park-apply">开始入驻 →</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
{/* 3 · 痛点理念 */}
|
||||
<Reveal as="section" className="section mk-section">
|
||||
<SectionHead no="01" title={MK_PRINCIPLE.title} en={MK_PRINCIPLE.en} />
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
import React from 'react';
|
||||
import { ContentTemplate } from '@/components/templates';
|
||||
import { SectionHead, FieldText, FieldTextarea, FieldSelect, FieldCheck, Button, Icon } from '@/components/atoms';
|
||||
import AuthGate from '@/components/AuthGate';
|
||||
import { getUser, isAuthed } from '@/services/auth';
|
||||
import {
|
||||
getParks, submitParkApplication, uploadParkDoc,
|
||||
GENDER_OPTS, COMPANY_TYPE_OPTS, REGION_OPTS, SERVICE_OPTS, DOC_ITEMS,
|
||||
} from '@/services/park';
|
||||
import '@/styles/park.css';
|
||||
|
||||
const EMPTY = {
|
||||
name: '', birth: '', gender: '', grad_school_major: '', ethnicity: '',
|
||||
grad_time: '', id_card: '', contact_phone: '',
|
||||
company_name: '', legal_person: '', legal_phone: '', registered_capital: '',
|
||||
company_type: '', industry: '', honors: '',
|
||||
emp_total: '', emp_grad: '', emp_layoff: '', emp_veteran: '', emp_migrant: '',
|
||||
project_overview: '',
|
||||
region: '', lease_period: '', services: [], agree: false,
|
||||
};
|
||||
|
||||
const STEPS = ['选择园区', '申请人信息', '企业信息', '项目概况', '入驻需求', '上传资料', '确认提交'];
|
||||
|
||||
export default function ParkApply() {
|
||||
const [parks, setParks] = React.useState([]);
|
||||
const [tenantId, setTenantId] = React.useState('');
|
||||
const [form, setForm] = React.useState(EMPTY);
|
||||
const [docs, setDocs] = React.useState({});
|
||||
const [docNames, setDocNames] = React.useState({});
|
||||
const [step, setStep] = React.useState(0);
|
||||
const [busy, setBusy] = React.useState(false);
|
||||
const [err, setErr] = React.useState('');
|
||||
const [done, setDone] = React.useState(null);
|
||||
const [authed, setAuthed] = React.useState(isAuthed());
|
||||
|
||||
React.useEffect(() => { getParks().then(setParks); }, []);
|
||||
const set = (key) => (v) => setForm((f) => ({ ...f, [key]: v }));
|
||||
const user = getUser();
|
||||
|
||||
if (done) {
|
||||
return (
|
||||
<ContentTemplate active="park-apply" kicker="Done" title="申请已提交" desc="">
|
||||
<section className="section pf-success">
|
||||
<div className="pf-success-icon"><Icon name="check" size="lg" /></div>
|
||||
<h2>园区入驻申请已提交</h2>
|
||||
<p>意向园区:{parks.find((p) => p.id === tenantId)?.name || ''}</p>
|
||||
<p>园区将在收到材料后尽快审核,进度可留意通知。</p>
|
||||
<div className="pf-success-actions">
|
||||
<Button variant="cta" href="#/park-apply">再填一份</Button>
|
||||
<Button variant="white" href="#/">返回首页</Button>
|
||||
</div>
|
||||
</section>
|
||||
</ContentTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
const validate = (s) => {
|
||||
if (s === 0) return !!tenantId;
|
||||
if (s === 1) return !!form.name && !!form.contact_phone && !!form.id_card;
|
||||
if (s === 2) return !!form.company_name;
|
||||
if (s === 3) return !!form.project_overview;
|
||||
if (s === 4) return !!form.region;
|
||||
if (s === 5) return DOC_ITEMS.filter((d) => d.required).every((d) => docs[d.key]);
|
||||
if (s === 6) return form.agree;
|
||||
return true;
|
||||
};
|
||||
|
||||
const next = () => {
|
||||
setErr('');
|
||||
if (!validate(step)) {
|
||||
setErr('请完整填写本步必填项');
|
||||
return;
|
||||
}
|
||||
setStep((s) => Math.min(s + 1, STEPS.length - 1));
|
||||
};
|
||||
const prev = () => { setErr(''); setStep((s) => Math.max(s - 1, 0)); };
|
||||
|
||||
const pickDoc = async (key, file) => {
|
||||
if (!file) return;
|
||||
setBusy(true); setErr('');
|
||||
try {
|
||||
const url = await uploadParkDoc(file);
|
||||
setDocs((d) => ({ ...d, [key]: url }));
|
||||
setDocNames((n) => ({ ...n, [key]: file.name }));
|
||||
} catch (e) { setErr(e.message || '文件上传失败'); }
|
||||
finally { setBusy(false); }
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
setErr('');
|
||||
if (!validate(6)) { setErr('请勾选企业承诺'); return; }
|
||||
setBusy(true);
|
||||
try {
|
||||
const r = await submitParkApplication({ tenant_id: tenantId, form, docs });
|
||||
setDone(r);
|
||||
setErr('');
|
||||
} catch (e) { setErr(e.message || '提交失败,请重试'); }
|
||||
finally { setBusy(false); }
|
||||
};
|
||||
|
||||
if (!authed) {
|
||||
return (
|
||||
<ContentTemplate active="park-apply" kicker="Park" title="园区入驻申请" desc="">
|
||||
<section className="section">
|
||||
<AuthGate subtitle="提交入驻申请需先登录(登录即绑定手机号)。" onAuthed={() => setAuthed(true)} />
|
||||
</section>
|
||||
</ContentTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ContentTemplate active="park-apply" kicker="Park" title="园区入驻申请" desc="对照《入园申请表》逐项填写,随附资料请上传">
|
||||
<section className="section">
|
||||
<div className="pf-steps">
|
||||
{STEPS.map((s, i) => (
|
||||
<div key={s} className={`pf-step${i === step ? ' on' : ''}${i < step ? ' done' : ''}`}>
|
||||
<span className="pf-step-dot">{i < step ? '✓' : i + 1}</span>
|
||||
<span className="pf-step-label">{s}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="section">
|
||||
<SectionHead no={String(step + 1).padStart(2, '0')} title={STEPS[step]} en="Step" />
|
||||
|
||||
{step === 0 && (
|
||||
<div className="form-card">
|
||||
<FieldSelect label="意向园区 *" options={parks.map((p) => p.name)} value={parks.find((p) => p.id === tenantId)?.name || ''}
|
||||
onChange={(v) => setTenantId(parks.find((p) => p.name === v)?.id || '')} />
|
||||
{parks.length === 0 && <p className="pf-muted">暂无开放入驻的园区</p>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 1 && (
|
||||
<div className="form-card">
|
||||
<FieldText label="姓名 *" placeholder="王思松" value={form.name} onChange={set('name')} />
|
||||
<FieldText label="出生年月" placeholder="如 1998年8月" value={form.birth} onChange={set('birth')} />
|
||||
<FieldSelect label="性别" options={GENDER_OPTS} value={form.gender} onChange={set('gender')} />
|
||||
<FieldText label="民族" placeholder="汉族" value={form.ethnicity} onChange={set('ethnicity')} />
|
||||
<FieldText label="毕业院校及专业 *" placeholder="华北科技学院 新闻学专业" value={form.grad_school_major} onChange={set('grad_school_major')} />
|
||||
<FieldText label="毕业时间" placeholder="2020年6月" value={form.grad_time} onChange={set('grad_time')} />
|
||||
<FieldText label="身份证号 *" placeholder="53032619…" value={form.id_card} onChange={set('id_card')} />
|
||||
<FieldText label="联系电话 *" placeholder="17637177199" value={form.contact_phone} onChange={set('contact_phone')} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 2 && (
|
||||
<div className="form-card">
|
||||
<FieldText label="企业名称 *" placeholder="云南派因人工智能有限公司(拟注册)" value={form.company_name} onChange={set('company_name')} />
|
||||
<FieldText label="法定代表人" placeholder="王思松" value={form.legal_person} onChange={set('legal_person')} />
|
||||
<FieldText label="法人联系电话" placeholder="17637177199" value={form.legal_phone} onChange={set('legal_phone')} />
|
||||
<FieldText label="注册资本(万元)" placeholder="100" type="number" value={form.registered_capital} onChange={set('registered_capital')} />
|
||||
<FieldSelect label="企业类型" options={COMPANY_TYPE_OPTS} value={form.company_type} onChange={set('company_type')} />
|
||||
<FieldText label="所属行业" placeholder="人工智能技术(按国民经济行业分类)" value={form.industry} onChange={set('industry')} />
|
||||
<FieldText label="企业所获政策/荣誉" placeholder="无" value={form.honors} onChange={set('honors')} />
|
||||
<div className="pf-sub">企业人员情况</div>
|
||||
<div className="pf-grid">
|
||||
<FieldText label="职工总数" type="number" value={form.emp_total} onChange={set('emp_total')} />
|
||||
<FieldText label="高校毕业生" type="number" value={form.emp_grad} onChange={set('emp_grad')} />
|
||||
<FieldText label="下岗失业" type="number" value={form.emp_layoff} onChange={set('emp_layoff')} />
|
||||
<FieldText label="退伍军人" type="number" value={form.emp_veteran} onChange={set('emp_veteran')} />
|
||||
<FieldText label="农民工" type="number" value={form.emp_migrant} onChange={set('emp_migrant')} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 3 && (
|
||||
<div className="form-card">
|
||||
<FieldTextarea rows={8} label="项目概况 / 商业模式 *" placeholder="核心业务、商业模式、目标用户等" value={form.project_overview} onChange={set('project_overview')} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 4 && (
|
||||
<div className="form-card">
|
||||
<FieldSelect label="拟入驻区域 *" options={REGION_OPTS} value={form.region} onChange={set('region')} />
|
||||
<FieldText label="租期需求" placeholder="自入驻之日起 N 年" value={form.lease_period} onChange={set('lease_period')} />
|
||||
<FieldCheck label="配套服务需求(可多选)" options={SERVICE_OPTS} value={form.services} onChange={set('services')} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 5 && (
|
||||
<div className="form-card">
|
||||
{DOC_ITEMS.map((d) => (
|
||||
<div className="pf-doc" key={d.key}>
|
||||
<div className="field">
|
||||
<label>{d.label}{d.required ? ' *' : ''}</label>
|
||||
<label className="pf-upl">
|
||||
<span>{docs[d.key] ? '已上传 ✓' : '选择文件'}</span>
|
||||
<input type="file" accept="image/*,.pdf,.doc,.docx" disabled={busy}
|
||||
onChange={(e) => pickDoc(d.key, e.target.files && e.target.files[0])} />
|
||||
</label>
|
||||
{docNames[d.key] && <span className="pf-fname">{docNames[d.key]}</span>}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 6 && (
|
||||
<div className="form-card">
|
||||
<FieldCheck label="企业承诺"
|
||||
options={['本人/本企业承诺所填信息及提交材料真实、准确、完整;']}
|
||||
value={form.agree ? ['本人/本企业承诺所填信息及提交材料真实、准确、完整;'] : []}
|
||||
onChange={(v) => setForm((f) => ({ ...f, agree: v.length > 0 }))} />
|
||||
<div className="pf-summary">
|
||||
<p><b>意向园区:</b>{parks.find((p) => p.id === tenantId)?.name || '-'}</p>
|
||||
<p><b>申请人:</b>{form.name || '-'}({form.contact_phone || '未填电话'})</p>
|
||||
<p><b>企业:</b>{form.company_name || '未填'} · 平台账号 {user && user.name ? user.name : ''}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{err && <div className="bk-err">{err}</div>}
|
||||
<div className="form-actions">
|
||||
{step > 0 && <Button variant="dark" onClick={prev} disabled={busy}>上一步</Button>}
|
||||
{step < STEPS.length - 1
|
||||
? <Button variant="cta" onClick={next}>下一步</Button>
|
||||
: <button className="btn btn-cta" disabled={busy} onClick={submit}>{busy ? '提交中…' : '提交入驻申请'}</button>}
|
||||
</div>
|
||||
</section>
|
||||
</ContentTemplate>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user