feat(admission): 园区入驻申请 —— 小程序首页按钮+表单 + 网站详细流程
- 小程序: 首页首屏Hero(预约/测评后)加「申请入驻」; pages/park-apply 对照《入园申请表》全字段分区表单+资料上传; utils/parks - 网站: #/park-apply 7步入驻wizard(选园区→申请人→企业→项目→需求→上传→承诺提交); services/park; 首页入口 - 修复循环请求(getParks入useEffect一次); 输入框样式对齐event-detail
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
@@ -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,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 },
|
||||
];
|
||||
Reference in New Issue
Block a user