Compare commits
3 Commits
169e880d37
...
35e87889db
| Author | SHA1 | Date | |
|---|---|---|---|
| 35e87889db | |||
| 600f4fc5a9 | |||
| 55c6a026df |
@@ -10,6 +10,8 @@ export default {
|
||||
'pages/event-detail/index',
|
||||
'pages/scan/index',
|
||||
'pages/park-apply/index',
|
||||
'pages/cert-apply/index',
|
||||
'pages/park-transfer/index',
|
||||
'pages/scan-login/index'
|
||||
],
|
||||
window: {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export default { navigationBarTitleText: 'OPC 认证' };
|
||||
@@ -0,0 +1,184 @@
|
||||
import { View, Text, Input, Textarea, Button, Radio, RadioGroup } from '@tarojs/components';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { phoneLogin, bindPhone, isAuthed } from '@/utils/auth';
|
||||
import { submitOpCert, getOpCertMine } from '@/utils/profile';
|
||||
import { uploadParkDoc } from '@/utils/parks';
|
||||
import './index.scss';
|
||||
|
||||
const EMPTY = () => ({ real_name: '', gender: '', address: '', industry: '', ability: '', phone: '' });
|
||||
const GENDER_OPTS = ['男', '女'];
|
||||
|
||||
/** OPC 认证申请(C端):提交认证资料 → 平台运营方审核。 */
|
||||
export default function CertApply() {
|
||||
const [authed, setAuthed] = useState(isAuthed());
|
||||
const [gatePhone, setGatePhone] = useState('');
|
||||
const [gateCode, setGateCode] = useState('');
|
||||
const [gateErr, setGateErr] = useState('');
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const [form, setForm] = useState(EMPTY());
|
||||
const [docs, setDocs] = useState({});
|
||||
const [mine, setMine] = useState(null); // {certification_status, certification}
|
||||
const [err, setErr] = useState('');
|
||||
const [done, setDone] = useState(false);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
|
||||
const up = (k, v) => setForm((f) => ({ ...f, [k]: v }));
|
||||
|
||||
const reloadMine = async () => {
|
||||
try {
|
||||
const m = await getOpCertMine();
|
||||
setMine(m);
|
||||
} catch { /* ignore */ }
|
||||
finally { setLoaded(true); }
|
||||
};
|
||||
useEffect(() => { if (isAuthed()) reloadMine(); else setLoaded(true); }, []);
|
||||
|
||||
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);
|
||||
reloadMine();
|
||||
} catch (e) { setGateErr((e && e.message) || '登录失败'); }
|
||||
finally { setBusy(false); }
|
||||
};
|
||||
|
||||
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 statusText = (s) => ({ uncertified: '未认证', pending: '审核中', reviewing: '审核中', certified: '已认证', rejected: '未通过' }[s] || s || '未认证');
|
||||
|
||||
const submit = async () => {
|
||||
setErr('');
|
||||
if (!form.real_name || !form.phone) { setErr('请填写真实姓名与联系电话'); return; }
|
||||
const payload = { ...form, docs_json: JSON.stringify(docs) };
|
||||
setBusy(true);
|
||||
try {
|
||||
await submitOpCert(payload);
|
||||
setDone(true);
|
||||
await reloadMine();
|
||||
} catch (e) { setErr((e && e.message) || '提交失败,请稍后重试'); }
|
||||
finally { setBusy(false); }
|
||||
};
|
||||
|
||||
if (done) {
|
||||
return (
|
||||
<View className="ca 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 (!loaded) return <View className="ca page-in"><Text className="err-text">加载中…</Text></View>;
|
||||
|
||||
// 未登录/未绑手机 → 快捷门
|
||||
if (!authed) {
|
||||
return (
|
||||
<View className="ca page-in">
|
||||
<View className="ca-card">
|
||||
<Text className="ca-title">OPC 认证申请</Text>
|
||||
<Text className="ca-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>
|
||||
);
|
||||
}
|
||||
|
||||
const st = mine?.certification_status || 'uncertified';
|
||||
const disabled = st === 'certified' || st === 'pending' || st === 'reviewing';
|
||||
|
||||
return (
|
||||
<View className="ca page-in">
|
||||
<View className="ca-head">
|
||||
<Text className="ca-title">OPC 认证申请</Text>
|
||||
<Text className="ca-sub">当前认证状态:<Text className="ca-status">{statusText(st)}</Text></Text>
|
||||
</View>
|
||||
|
||||
<View className="ca-card">
|
||||
<Text className="ca-sec">一 · 基本信息</Text>
|
||||
<Field label="真实姓名 *" value={form.real_name} ph="" onChange={(v) => up('real_name', v)} />
|
||||
<View className="field"><Text className="ca-label">性别</Text>
|
||||
<RadioGroup onChange={(e) => up('gender', e.detail.value)}><View className="ca-radios">
|
||||
{GENDER_OPTS.map((g) => <Radio key={g} value={g} checked={form.gender === g}>{g}</Radio>)}
|
||||
</View></RadioGroup>
|
||||
</View>
|
||||
<Field label="联系地址" value={form.address} ph="常住地/通讯地址" onChange={(v) => up('address', v)} />
|
||||
<Field label="所属行业" value={form.industry} ph="如 人工智能、文化创意、电商" onChange={(v) => up('industry', v)} />
|
||||
<Field label="联系电话 *" value={form.phone} ph="" onChange={(v) => up('phone', v)} />
|
||||
</View>
|
||||
|
||||
<View className="ca-card">
|
||||
<Text className="ca-sec">二 · 能力与成果</Text>
|
||||
<Text className="ca-label">专业技能 / 代表作品 / 成果</Text>
|
||||
<Textarea className="ca-textarea" placeholder="描述你的核心能力、代表作品或已取得的成果" value={form.ability}
|
||||
onChange={(e) => up('ability', e.detail.value)} />
|
||||
</View>
|
||||
|
||||
<View className="ca-card">
|
||||
<Text className="ca-sec">三 · 佐证材料</Text>
|
||||
{DOC_ITEMS.map((d) => (
|
||||
<View className="ca-doc" key={d.key}>
|
||||
<Text className="ca-label">{d.label}</Text>
|
||||
<Button className="btn btn-ghost" disabled={busy || disabled} onClick={() => pickAndUpload(d.key)}>
|
||||
{docs[d.key] ? '已上传 ✓' : '选择文件'}
|
||||
</Button>
|
||||
{docs[d.key] && <Text className="ca-uploaded">{docs[d.key].split('/').pop()}</Text>}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{disabled && <Text className="err-text">{st === 'certified' ? '您已是认证 OPC,无需重复申请' : '已有认证申请审核中,请勿重复提交'}</Text>}
|
||||
{err && <Text className="err-text">{err}</Text>}
|
||||
<Button className="btn btn-cta" loading={busy || disabled} disabled={disabled} onClick={submit}>提交认证申请</Button>
|
||||
<View className="ca-foot">提交即授权平台审核材料,结果可在「我的」查看</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const DOC_ITEMS = [
|
||||
{ key: 'id_card_doc', label: '身份证(正面)' },
|
||||
{ key: 'achievement', label: '成果 / 资质证明材料' },
|
||||
{ key: 'other', label: '其他佐证材料' },
|
||||
];
|
||||
|
||||
function Field({ label, value, ph, onChange }) {
|
||||
return (
|
||||
<View className="field">
|
||||
<Text className="ca-label">{label}</Text>
|
||||
<Input className="input" placeholder={ph} value={value} onInput={(e) => onChange(e.detail.value)} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/* OPC 认证申请 · 小程序样式(暗色令牌一致) */
|
||||
.ca { position: relative; z-index: 2; padding: 0 0 60rpx; }
|
||||
.ca-head { padding: 24rpx 16rpx 8rpx; }
|
||||
.ca-title { font-size: 40rpx; font-weight: 700; color: #fff; }
|
||||
.ca-sub { display: block; margin-top: 8rpx; font-size: 25rpx; color: #8e8e8e; }
|
||||
.ca-status { color: #ffd28a; }
|
||||
|
||||
.ca-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;
|
||||
}
|
||||
.ca-sec { display: block; font-size: 26rpx; color: #ffd28a; margin-bottom: 16rpx; }
|
||||
.ca-label { display: block; font-size: 25rpx; color: #c4c2c3; margin: 14rpx 0 8rpx; }
|
||||
.ca-radios { display: flex; gap: 24rpx; font-size: 26rpx; color: #eaeaea; }
|
||||
.field { margin-top: 12rpx; }
|
||||
.input {
|
||||
background: #17171b; border: 1rpx solid #2a2a2d; border-radius: 10rpx;
|
||||
color: #eaeaea; padding: 14rpx 16rpx; font-size: 27rpx; width: 100%;
|
||||
}
|
||||
.ca-textarea {
|
||||
width: 100%; min-height: 140rpx; background: #17171b; border: 1rpx solid #2a2a2d;
|
||||
border-radius: 10rpx; color: #eaeaea; padding: 14rpx 16rpx; font-size: 27rpx;
|
||||
}
|
||||
.ca-doc { display: flex; align-items: center; gap: 16rpx; margin-top: 12rpx; }
|
||||
.ca-uploaded { font-size: 23rpx; color: #8e8e8e; flex: 1; }
|
||||
.ca-foot { margin-top: 20rpx; text-align: center; font-size: 23rpx; color: #8e8e8e; }
|
||||
|
||||
.ok-icon { width: 96rpx; height: 96rpx; margin: 120rpx auto 24rpx; border-radius: 50%; background: #26a69a; }
|
||||
.ok-title { display: block; text-align: center; font-size: 36rpx; color: #fff; }
|
||||
.ok-desc { display: block; text-align: center; margin-top: 12rpx; font-size: 26rpx; color: #8e8e8e; }
|
||||
.btn { margin-top: 32rpx; }
|
||||
.err-text { display: block; margin-top: 16rpx; text-align: center; font-size: 25rpx; color: #f0b8b8; }
|
||||
@@ -26,6 +26,8 @@ export default function Index() {
|
||||
<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>
|
||||
<Button className="btn btn-ghost" onClick={() => Taro.navigateTo({ url: '/pages/cert-apply/index' })}>OPC认证</Button>
|
||||
<Button className="btn btn-ghost" onClick={() => Taro.navigateTo({ url: '/pages/park-transfer/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, mpQrConfirm } from '@/utils/auth';
|
||||
import { isAuthed, getUser, logout, wxLogin, phoneLogin, bindPhone, sendCode, wxPhoneBind, updateProfile, getMe } 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,8 +200,6 @@ 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) || '');
|
||||
@@ -264,23 +262,6 @@ 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 (
|
||||
@@ -354,12 +335,10 @@ 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>}
|
||||
<Text className="next-lbl">登录其它端</Text>
|
||||
<Text className="bind-tip">在电脑/网页登录页点「小程序扫码」,用微信扫一扫,进入小程序授权页确认即可登录</Text>
|
||||
</View>
|
||||
|
||||
{/* 下一场活动倒计时(个人信息卡之后显示;取最早的待参加且未开始的报名场次) */}
|
||||
@@ -422,6 +401,24 @@ export default function Mine() {
|
||||
<Text className="scan-entry-arrow">›</Text>
|
||||
</View>
|
||||
|
||||
{/* 申请/认证入口:入驻 / OPC认证 / 转园 */}
|
||||
<View className="mine-section-head">
|
||||
<Text className="mine-no">APPLY</Text>
|
||||
<Text className="mine-section-title">申请与认证</Text>
|
||||
</View>
|
||||
<View className="card scan-entry-card" onClick={() => Taro.navigateTo({ url: '/pages/park-apply/index' })}>
|
||||
<View className="scan-entry-body"><Text className="scan-entry-title">园区入驻申请</Text><Text className="scan-entry-sub">填写入园申请表,入驻创业园</Text></View>
|
||||
<Text className="scan-entry-arrow">›</Text>
|
||||
</View>
|
||||
<View className="card scan-entry-card" onClick={() => Taro.navigateTo({ url: '/pages/cert-apply/index' })}>
|
||||
<View className="scan-entry-body"><Text className="scan-entry-title">OPC 认证</Text><Text className="scan-entry-sub">提交认证资料,认证 OPC 超级个体</Text></View>
|
||||
<Text className="scan-entry-arrow">›</Text>
|
||||
</View>
|
||||
<View className="card scan-entry-card" onClick={() => Taro.navigateTo({ url: '/pages/park-transfer/index' })}>
|
||||
<View className="scan-entry-body"><Text className="scan-entry-title">转园申请</Text><Text className="scan-entry-sub">园区 OPC 申请转入其它园区</Text></View>
|
||||
<Text className="scan-entry-arrow">›</Text>
|
||||
</View>
|
||||
|
||||
{/* 我的报名 */}
|
||||
<View className="mine-section-head">
|
||||
<Text className="mine-no">BOOK</Text>
|
||||
|
||||
@@ -153,7 +153,7 @@ export default function ParkApply() {
|
||||
|
||||
<View className="pa-card">
|
||||
<Text className="pa-sec">一 · 申请人信息</Text>
|
||||
<Field label="姓名 *" value={form.name} ph="王思松" onChange={(v) => up('name', v)} />
|
||||
<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">
|
||||
@@ -161,17 +161,17 @@ export default function ParkApply() {
|
||||
</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)} />
|
||||
<Field label="毕业院校及专业 *" value={form.grad_school_major} ph="" onChange={(v) => up('grad_school_major', v)} />
|
||||
<Field label="毕业时间" value={form.grad_time} ph="" onChange={(v) => up('grad_time', v)} />
|
||||
<Field label="身份证号 *" value={form.id_card} ph="(选填照片可后传)" onChange={(v) => up('id_card', v)} />
|
||||
<Field label="联系电话 *" value={form.contact_phone} ph="" 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.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="" 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">
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export default { navigationBarTitleText: '转园申请' };
|
||||
@@ -0,0 +1,133 @@
|
||||
import { View, Text, Input, Textarea, Button, Picker } from '@tarojs/components';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { phoneLogin, bindPhone, isAuthed } from '@/utils/auth';
|
||||
import { submitParkTransfer, getParkTransferMine } from '@/utils/profile';
|
||||
import { getParks } from '@/utils/parks';
|
||||
import './index.scss';
|
||||
|
||||
/** 转园申请(C端):园区 OPC 申请转入目标园区 → 园区/平台审核。 */
|
||||
export default function ParkTransfer() {
|
||||
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 [toParkId, setToParkId] = useState('');
|
||||
const [reason, setReason] = useState('');
|
||||
const [mine, setMine] = useState(null); // {transfer}
|
||||
const [err, setErr] = useState('');
|
||||
const [done, setDone] = useState(false);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
|
||||
const reload = async () => {
|
||||
try {
|
||||
const [t, m] = await Promise.all([getParks(), getParkTransferMine()]);
|
||||
setParks(t);
|
||||
setMine(m);
|
||||
} catch { /* ignore */ }
|
||||
finally { setLoaded(true); }
|
||||
};
|
||||
useEffect(() => { if (isAuthed()) reload(); else setLoaded(true); }, []);
|
||||
|
||||
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);
|
||||
reload();
|
||||
} catch (e) { setGateErr((e && e.message) || '登录失败'); }
|
||||
finally { setBusy(false); }
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
setErr('');
|
||||
if (!toParkId) { setErr('请选择目标园区'); return; }
|
||||
setBusy(true);
|
||||
try {
|
||||
const park = parks.find((p) => p.id === toParkId);
|
||||
await submitParkTransfer({ to_park_id: toParkId, to_park_name: park?.name || '', reason });
|
||||
setDone(true);
|
||||
} catch (e) { setErr((e && e.message) || '提交失败,请稍后重试'); }
|
||||
finally { setBusy(false); }
|
||||
};
|
||||
|
||||
const cur = mine?.transfer;
|
||||
const curSt = cur?.status;
|
||||
|
||||
if (done) {
|
||||
return (
|
||||
<View className="pt 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 (!loaded) return <View className="pt page-in"><Text className="err-text">加载中…</Text></View>;
|
||||
|
||||
if (!authed) {
|
||||
return (
|
||||
<View className="pt page-in">
|
||||
<View className="pt-card">
|
||||
<Text className="pt-title">转园申请</Text>
|
||||
<Text className="pt-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>
|
||||
);
|
||||
}
|
||||
|
||||
const disabled = curSt === 'pending' || curSt === 'reviewing';
|
||||
|
||||
return (
|
||||
<View className="pt page-in">
|
||||
<View className="pt-head">
|
||||
<Text className="pt-title">转园申请</Text>
|
||||
<Text className="pt-sub">当前园区 OPC 可申请转入其它园区,需园区/平台审核。</Text>
|
||||
</View>
|
||||
|
||||
{cur && (
|
||||
<View className="pt-card">
|
||||
<Text className="pt-sec">当前申请</Text>
|
||||
<Text className="pt-line">
|
||||
目标:{cur.to_park_name || cur.to_park_id || '-'} · 状态:
|
||||
<Text className="pt-status">{{ pending: '审核中', reviewing: '审核中', approved: '已通过', rejected: '未通过' }[curSt] || curSt}</Text>
|
||||
</Text>
|
||||
{cur.review_comment && <Text className="pt-line">审核意见:{cur.review_comment}</Text>}
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View className="pt-card">
|
||||
<Text className="pt-sec">选择目标园区</Text>
|
||||
<Picker range={parks.map((p) => p.name)} value={Math.max(0, parks.findIndex((p) => p.id === toParkId))}
|
||||
onChange={(e) => setToParkId(parks[Number(e.detail.value)]?.id || '')}>
|
||||
<View className="pt-picker">{parks.find((p) => p.id === toParkId)?.name || '请选择目标园区'} ▾</View>
|
||||
</Picker>
|
||||
<Text className="pt-label">转园理由</Text>
|
||||
<Textarea className="pt-textarea" placeholder="请说明转园原因" value={reason}
|
||||
onChange={(e) => setReason(e.detail.value)} />
|
||||
</View>
|
||||
|
||||
{disabled && <Text className="err-text">已有转园申请审核中,请勿重复提交</Text>}
|
||||
{err && <Text className="err-text">{err}</Text>}
|
||||
<Button className="btn btn-cta" loading={busy || disabled} disabled={disabled} onClick={submit}>提交转园申请</Button>
|
||||
<View className="pt-foot">仅园区 OPC 可发起转园;审核结果可在「我的」查看</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/* 转园申请 · 小程序样式(暗色令牌一致) */
|
||||
.pt { position: relative; z-index: 2; padding: 0 0 60rpx; }
|
||||
.pt-head { padding: 24rpx 16rpx 8rpx; }
|
||||
.pt-title { font-size: 40rpx; font-weight: 700; color: #fff; }
|
||||
.pt-sub { display: block; margin-top: 8rpx; font-size: 25rpx; color: #8e8e8e; }
|
||||
|
||||
.pt-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;
|
||||
}
|
||||
.pt-sec { display: block; font-size: 26rpx; color: #ffd28a; margin-bottom: 16rpx; }
|
||||
.pt-label { display: block; font-size: 25rpx; color: #c4c2c3; margin: 14rpx 0 8rpx; }
|
||||
.pt-line { display: block; font-size: 25rpx; color: #eaeaea; margin-top: 8rpx; }
|
||||
.pt-status { color: #ffd28a; }
|
||||
.pt-picker {
|
||||
background: #17171b; border: 1rpx solid #2a2a2d; border-radius: 10rpx;
|
||||
color: #eaeaea; padding: 14rpx 16rpx; font-size: 27rpx;
|
||||
}
|
||||
.pt-textarea {
|
||||
width: 100%; min-height: 120rpx; background: #17171b; border: 1rpx solid #2a2a2d;
|
||||
border-radius: 10rpx; color: #eaeaea; padding: 14rpx 16rpx; font-size: 27rpx;
|
||||
}
|
||||
.field { margin-top: 12rpx; }
|
||||
.input {
|
||||
background: #17171b; border: 1rpx solid #2a2a2d; border-radius: 10rpx;
|
||||
color: #eaeaea; padding: 14rpx 16rpx; font-size: 27rpx; width: 100%;
|
||||
}
|
||||
.pt-foot { margin-top: 20rpx; text-align: center; font-size: 23rpx; color: #8e8e8e; }
|
||||
|
||||
.ok-icon { width: 96rpx; height: 96rpx; margin: 120rpx auto 24rpx; border-radius: 50%; background: #26a69a; }
|
||||
.ok-title { display: block; text-align: center; font-size: 36rpx; color: #fff; }
|
||||
.ok-desc { display: block; text-align: center; margin-top: 12rpx; font-size: 26rpx; color: #8e8e8e; }
|
||||
.btn { margin-top: 32rpx; }
|
||||
.err-text { display: block; margin-top: 16rpx; text-align: center; font-size: 25rpx; color: #f0b8b8; }
|
||||
@@ -1,6 +1,6 @@
|
||||
import { View, Text, Button, Input } from '@tarojs/components';
|
||||
import Taro, { useDidShow } from '@tarojs/taro';
|
||||
import { useState } from 'react';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { isAuthed, wxLogin, phoneLogin, sendCode, mpQrConfirm } from '@/utils/auth';
|
||||
import './index.scss';
|
||||
|
||||
@@ -20,13 +20,18 @@ export default function ScanLogin() {
|
||||
const [gateCode, setGateCode] = useState('');
|
||||
const [cd, setCd] = useState(0);
|
||||
|
||||
useDidShow(() => {
|
||||
const opts = Taro.getEnterOptionsSync();
|
||||
const q = (opts && opts.query) || {};
|
||||
const s = String(q.scene || opts.scene || '');
|
||||
setScene(s);
|
||||
setDone(false); setMsg('');
|
||||
});
|
||||
// 冷启动/扫码进入:纯 React useEffect 读当前实例参数(wxacode 的 scene 在 router.params.scene)。
|
||||
// 不再用 useLoad/useDidShow 等 Taro 元钩子(其内部在原生计时器里调度 React hook,会触发
|
||||
// "Invalid attempt to destruct non-iterable" → 页面黑屏)。
|
||||
useEffect(() => {
|
||||
try {
|
||||
const inst = Taro.getCurrentInstance();
|
||||
const router = (inst && inst.router) || {};
|
||||
const params = (router && (router.params || router.query)) || {};
|
||||
const s = String(params.scene || router.scene || '');
|
||||
if (s) setScene(s);
|
||||
} catch { /* 忽略取参异常 */ }
|
||||
}, []);
|
||||
|
||||
const wxLoginNow = async () => {
|
||||
setBusy(true); setMsg('');
|
||||
@@ -47,7 +52,8 @@ export default function ScanLogin() {
|
||||
const r = await sendCode(gatePhone);
|
||||
setMsg(`验证码已发送${r.debugCode ? `(演示 ${r.debugCode})` : ''}`);
|
||||
setCd(60);
|
||||
const t = window.setInterval(() => setCd((c) => (c <= 1 ? (window.clearInterval(t), 0) : c - 1)), 1000);
|
||||
// 小程序无 window,用全局 setInterval/clearInterval(源码里 window.* 会导致运行时 ReferenceError)
|
||||
const t = setInterval(() => setCd((c) => (c <= 1 ? (clearInterval(t), 0) : c - 1)), 1000);
|
||||
} catch (e) { setMsg((e && e.message) || '发送失败'); }
|
||||
finally { setBusy(false); }
|
||||
};
|
||||
@@ -106,7 +112,7 @@ export default function ScanLogin() {
|
||||
<View className="sl-icon">{done ? '✓' : '✅'}</View>
|
||||
<Text className="sl-title">{done ? '已完成' : '确认扫码操作'}</Text>
|
||||
<Text className="sl-desc">
|
||||
{done ? msg : `确认使用当前微信账号${scene.includes('bind') || true ? '' : ''}在你操作的端口${scene ? '' : ' '}完成登录 / 绑定?`}
|
||||
{done ? msg : `确认使用当前微信账号在你操作的端口完成登录 / 绑定?`}
|
||||
</Text>
|
||||
{!done && scene && <Text className="sl-scene">请求 #{scene.slice(0, 12)}</Text>}
|
||||
{!done && (
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { request } from './api';
|
||||
|
||||
/** OPC 认证:提交认证申请(需登录,opc_member) */
|
||||
export async function submitOpCert(payload) {
|
||||
const r = await request('POST', '/opc/certification/apply', payload, true);
|
||||
if (!r?.ok) throw new Error(r?.detail || r?.error || '提交失败');
|
||||
return r;
|
||||
}
|
||||
|
||||
/** OPC 认证:我的认证状态 {certification_status, certification} */
|
||||
export async function getOpCertMine() {
|
||||
const r = await request('GET', '/opc/certification/mine', {}, true);
|
||||
return r || {};
|
||||
}
|
||||
|
||||
/** 转园:提交转园申请(需登录,园区 OPC) */
|
||||
export async function submitParkTransfer(payload) {
|
||||
const r = await request('POST', '/opc/park-transfer/apply', payload, true);
|
||||
if (!r?.ok) throw new Error(r?.detail || r?.error || '提交失败');
|
||||
return r;
|
||||
}
|
||||
|
||||
/** 转园:我的转园申请 {transfer} */
|
||||
export async function getParkTransferMine() {
|
||||
const r = await request('GET', '/opc/park-transfer/mine', {}, true);
|
||||
return r || {};
|
||||
}
|
||||
@@ -26,6 +26,8 @@ import Login from './platform/Login';
|
||||
import { ParticleField } from './components/organisms';
|
||||
import { isAuthed } from './services/auth';
|
||||
import ParkApply from './user/ParkApply';
|
||||
import CertApply from './user/CertApply';
|
||||
import ParkTransfer from './user/ParkTransfer';
|
||||
|
||||
/* 公开路由(无需登录) */
|
||||
const PUBLIC_ROUTES = [
|
||||
@@ -37,6 +39,8 @@ const PUBLIC_ROUTES = [
|
||||
{ path: 'start-plan', Page: StartPlan },
|
||||
{ path: 'survey', Page: OPCSurvey },
|
||||
{ path: 'park-apply', Page: ParkApply },
|
||||
{ path: 'cert-apply', Page: CertApply },
|
||||
{ path: 'park-transfer', Page: ParkTransfer },
|
||||
{ path: 'profile', Page: Profile }
|
||||
];
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* OPC 认证 / 转园申请(C端,需登录)
|
||||
* 对接后端:POST|GET /opc/certification/apply|mine、POST|GET /opc/park-transfer/apply|mine。
|
||||
*/
|
||||
import { getToken } from '@/services/auth';
|
||||
const API_BASE = 'https://opc.pinesound.cn';
|
||||
|
||||
async function j(path, method, payload) {
|
||||
const token = getToken();
|
||||
const opts = {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) },
|
||||
};
|
||||
if (payload) opts.body = JSON.stringify(payload);
|
||||
const res = await fetch(`${API_BASE}${path}`, opts);
|
||||
const r = await res.json().catch(() => ({}));
|
||||
if (!res.ok || r.ok === false) throw new Error(r.detail || r.error || '请求失败,请重试');
|
||||
return r;
|
||||
}
|
||||
|
||||
/** 提交 OPC 认证申请 */
|
||||
export async function submitOpCert(payload) {
|
||||
return j('/opc/certification/apply', 'POST', payload);
|
||||
}
|
||||
|
||||
/** 我的 OPC 认证状态 {certification_status, certification} */
|
||||
export async function getOpCertMine() {
|
||||
try {
|
||||
return await j('/opc/certification/mine', 'GET');
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/** 提交转园申请 */
|
||||
export async function submitParkTransfer(payload) {
|
||||
return j('/opc/park-transfer/apply', 'POST', payload);
|
||||
}
|
||||
|
||||
/** 我的转园申请 {transfer} */
|
||||
export async function getParkTransferMine() {
|
||||
try {
|
||||
return await j('/opc/park-transfer/mine', 'GET');
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import React from 'react';
|
||||
import { ContentTemplate } from '@/components/templates';
|
||||
import { SectionHead, FieldText, FieldTextarea, FieldSelect, Button } from '@/components/atoms';
|
||||
import AuthGate from '@/components/AuthGate';
|
||||
import { isAuthed, getUser } from '@/services/auth';
|
||||
import { submitOpCert, getOpCertMine } from '@/services/opcProfile';
|
||||
import { uploadParkDoc } from '@/services/park';
|
||||
import '@/styles/park.css';
|
||||
|
||||
const EMPTY = { real_name: '', gender: '', address: '', industry: '', ability: '', phone: '' };
|
||||
const GENDER_OPTS = ['男', '女'];
|
||||
|
||||
const ST_TEXT = { uncertified: '未认证', pending: '审核中', reviewing: '审核中', certified: '已认证', rejected: '未通过' };
|
||||
const stText = (s) => ST_TEXT[s] || s || '未认证';
|
||||
|
||||
/** OPC 认证申请(C端)→ 平台运营方审核 */
|
||||
export default function CertApply() {
|
||||
const [form, setForm] = React.useState(EMPTY);
|
||||
const [docs, setDocs] = React.useState({});
|
||||
const [mine, setMine] = React.useState({});
|
||||
const [busy, setBusy] = React.useState(false);
|
||||
const [err, setErr] = React.useState('');
|
||||
const [done, setDone] = React.useState(false);
|
||||
const [authed, setAuthed] = React.useState(isAuthed());
|
||||
|
||||
React.useEffect(() => { if (isAuthed()) getOpCertMine().then(setMine); }, []);
|
||||
const set = (key) => (v) => setForm((f) => ({ ...f, [key]: v }));
|
||||
const user = getUser();
|
||||
|
||||
const pickDoc = async (key, file) => {
|
||||
if (!file) return;
|
||||
setBusy(true); setErr('');
|
||||
try {
|
||||
const url = await uploadParkDoc(file);
|
||||
setDocs((d) => ({ ...d, [key]: url }));
|
||||
} catch (e) { setErr((e && e.message) || '上传失败'); }
|
||||
finally { setBusy(false); }
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
setErr('');
|
||||
if (!form.real_name || !form.phone) { setErr('请填写真实姓名与联系电话'); return; }
|
||||
setBusy(true);
|
||||
try {
|
||||
await submitOpCert({ ...form, docs_json: JSON.stringify(docs) });
|
||||
setDone(true);
|
||||
getOpCertMine().then(setMine);
|
||||
} catch (e) { setErr((e && e.message) || '提交失败,请稍后重试'); }
|
||||
finally { setBusy(false); }
|
||||
};
|
||||
|
||||
const st = mine.certification_status || 'uncertified';
|
||||
const disabled = st === 'certified' || st === 'pending' || st === 'reviewing';
|
||||
|
||||
if (done) {
|
||||
return (
|
||||
<ContentTemplate active="cert-apply" kicker="Done" title="认证申请已提交" desc="">
|
||||
<section className="section pf-success">
|
||||
<div className="pf-success-icon">✓</div>
|
||||
<h2>OPC 认证申请已提交</h2>
|
||||
<p>平台将对您的资料进行审核,结果可在个人中心留意。</p>
|
||||
<div className="pf-success-actions">
|
||||
<Button variant="white" href="#/user">返回个人中心</Button>
|
||||
</div>
|
||||
</section>
|
||||
</ContentTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
if (!authed) {
|
||||
return (
|
||||
<ContentTemplate active="cert-apply" kicker="Apply" title="OPC 认证" desc="登录后提交认证资料,认证 OPC 超级个体">
|
||||
<section className="section"><AuthGate title="登录后提交认证" subtitle="手机号即账号,未注册自动注册" /></section>
|
||||
</ContentTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ContentTemplate active="cert-apply" kicker="Apply" title="OPC 认证" desc="当前认证状态">
|
||||
<section className="section pf-sub" style={{ margin: 0 }}>
|
||||
<b style={{ color: '#7fd087' }}>{stText(st)}</b>
|
||||
{user?.username && <span className="pf-muted"> · {user.username}</span>}
|
||||
</section>
|
||||
|
||||
<section className="section">
|
||||
<SectionHead no="01" title="基本信息" en="Basic" />
|
||||
<div className="pf-grid">
|
||||
<FieldText label="真实姓名 *" value={form.real_name} placeholder="" onChange={set('real_name')} />
|
||||
<FieldSelect label="性别" options={GENDER_OPTS} value={form.gender} onChange={set('gender')} />
|
||||
<FieldText label="联系电话 *" value={form.phone} placeholder="" onChange={set('phone')} />
|
||||
<FieldText label="所属行业" value={form.industry} placeholder="如 人工智能、文化创意、电商" onChange={set('industry')} />
|
||||
</div>
|
||||
<FieldText label="联系地址" value={form.address} placeholder="常住地 / 通讯地址" onChange={set('address')} />
|
||||
</section>
|
||||
|
||||
<section className="section">
|
||||
<SectionHead no="02" title="能力与成果" en="Ability" />
|
||||
<FieldTextarea label="专业技能 / 代表作品 / 成果" placeholder="描述你的核心能力、代表作品或已取得的成果"
|
||||
value={form.ability} onChange={set('ability')} rows={4} />
|
||||
</section>
|
||||
|
||||
<section className="section">
|
||||
<SectionHead no="03" title="佐证材料" en="Docs" />
|
||||
{[{ k: 'id_card_doc', l: '身份证(正面)' }, { k: 'achievement', l: '成果 / 资质证明材料' }, { k: 'other', l: '其他佐证材料' }].map((d) => (
|
||||
<label className="pf-upl" key={d.k}>
|
||||
{docs[d.k] ? <span className="pf-fname">已上传:{docs[d.k].split('/').pop()}</span> : `选择文件 · ${d.l}`}
|
||||
<input type="file" accept="image/*,.pdf,.doc,.docx" disabled={busy || disabled}
|
||||
onChange={(e) => pickDoc(d.k, e.target.files && e.target.files[0])} />
|
||||
</label>
|
||||
))}
|
||||
</section>
|
||||
|
||||
{disabled && <p className="pf-muted" style={{ color: '#f0b8b8' }}>{st === 'certified' ? '您已是认证 OPC,无需重复申请' : '已有认证申请审核中,请勿重复提交'}</p>}
|
||||
{err && <p className="pf-muted" style={{ color: '#f0b8b8' }}>{err}</p>}
|
||||
<section className="section">
|
||||
<Button variant="cta" disabled={busy || disabled} loading={busy} onClick={submit}>提交认证申请</Button>
|
||||
</section>
|
||||
</ContentTemplate>
|
||||
);
|
||||
}
|
||||
@@ -158,6 +158,36 @@ export default function Home() {
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
{/* 2.6 · OPC认证 */}
|
||||
<Reveal as="section" className="section mk-section">
|
||||
<SectionHead no="00" title="OPC 认证" en="Cert" />
|
||||
<div className="mk-assess">
|
||||
<div className="assess-txt">
|
||||
<span className="assess-badge">认证</span>
|
||||
<span className="assess-title">提交资料认证 OPC 超级个体</span>
|
||||
<span className="assess-desc">填写能力与成果、上传佐证材料,经平台审核即成为认证 OPC。</span>
|
||||
</div>
|
||||
<div className="assess-cta">
|
||||
<Button variant="cta" href="#/cert-apply">去认证 →</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
{/* 2.7 · 转园申请 */}
|
||||
<Reveal as="section" className="section mk-section">
|
||||
<SectionHead no="00" title="转园申请" en="Transfer" />
|
||||
<div className="mk-assess">
|
||||
<div className="assess-txt">
|
||||
<span className="assess-badge">转园</span>
|
||||
<span className="assess-title">园区 OPC 申请转入其它园区</span>
|
||||
<span className="assess-desc">选择目标园区并说明理由,经园区 / 平台审核通过后更新归属。</span>
|
||||
</div>
|
||||
<div className="assess-cta">
|
||||
<Button variant="cta" href="#/park-transfer">申请转园 →</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
{/* 3 · 痛点理念 */}
|
||||
<Reveal as="section" className="section mk-section">
|
||||
<SectionHead no="01" title={MK_PRINCIPLE.title} en={MK_PRINCIPLE.en} />
|
||||
|
||||
@@ -134,22 +134,22 @@ export default function ParkApply() {
|
||||
|
||||
{step === 1 && (
|
||||
<div className="form-card">
|
||||
<FieldText label="姓名 *" placeholder="王思松" value={form.name} onChange={set('name')} />
|
||||
<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')} />
|
||||
<FieldText label="毕业院校及专业 *" placeholder="" value={form.grad_school_major} onChange={set('grad_school_major')} />
|
||||
<FieldText label="毕业时间" placeholder="" value={form.grad_time} onChange={set('grad_time')} />
|
||||
<FieldText label="身份证号 *" placeholder="" value={form.id_card} onChange={set('id_card')} />
|
||||
<FieldText label="联系电话 *" placeholder="" 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="" value={form.company_name} onChange={set('company_name')} />
|
||||
<FieldText label="法定代表人" placeholder="" value={form.legal_person} onChange={set('legal_person')} />
|
||||
<FieldText label="法人联系电话" placeholder="" 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')} />
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import React from 'react';
|
||||
import { ContentTemplate } from '@/components/templates';
|
||||
import { SectionHead, FieldText, FieldTextarea, FieldSelect, Button } from '@/components/atoms';
|
||||
import AuthGate from '@/components/AuthGate';
|
||||
import { isAuthed, getUser } from '@/services/auth';
|
||||
import { submitParkTransfer, getParkTransferMine } from '@/services/opcProfile';
|
||||
import { getParks } from '@/services/park';
|
||||
import '@/styles/park.css';
|
||||
|
||||
const ST_TEXT = { pending: '审核中', reviewing: '审核中', approved: '已通过', rejected: '未通过' };
|
||||
|
||||
/** 转园申请(C端)→ 园区 / 平台审核 */
|
||||
export default function ParkTransfer() {
|
||||
const [parks, setParks] = React.useState([]);
|
||||
const [toParkId, setToParkId] = React.useState('');
|
||||
const [reason, setReason] = React.useState('');
|
||||
const [mine, setMine] = React.useState({});
|
||||
const [busy, setBusy] = React.useState(false);
|
||||
const [err, setErr] = React.useState('');
|
||||
const [done, setDone] = React.useState(false);
|
||||
const [authed, setAuthed] = React.useState(isAuthed());
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isAuthed()) return;
|
||||
getParks().then(setParks);
|
||||
getParkTransferMine().then(setMine);
|
||||
}, []);
|
||||
const user = getUser();
|
||||
|
||||
const submit = async () => {
|
||||
setErr('');
|
||||
if (!toParkId) { setErr('请选择目标园区'); return; }
|
||||
setBusy(true);
|
||||
try {
|
||||
const p = parks.find((x) => x.id === toParkId);
|
||||
await submitParkTransfer({ to_park_id: toParkId, to_park_name: p?.name || '', reason });
|
||||
setDone(true);
|
||||
getParkTransferMine().then(setMine);
|
||||
} catch (e) { setErr((e && e.message) || '提交失败,请稍后重试'); }
|
||||
finally { setBusy(false); }
|
||||
};
|
||||
|
||||
const cur = mine.transfer;
|
||||
const curSt = cur?.status;
|
||||
const disabled = curSt === 'pending' || curSt === 'reviewing';
|
||||
|
||||
if (done) {
|
||||
return (
|
||||
<ContentTemplate active="park-transfer" kicker="Done" title="转园申请已提交" desc="">
|
||||
<section className="section pf-success">
|
||||
<div className="pf-success-icon">✓</div>
|
||||
<h2>转园申请已提交</h2>
|
||||
<p>园区与平台将对您的转园申请进行审核,结果可在个人中心留意。</p>
|
||||
<div className="pf-success-actions">
|
||||
<Button variant="white" href="#/user">返回个人中心</Button>
|
||||
</div>
|
||||
</section>
|
||||
</ContentTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
if (!authed) {
|
||||
return (
|
||||
<ContentTemplate active="park-transfer" kicker="Apply" title="转园申请" desc="登录后申请转入其它园区">
|
||||
<section className="section"><AuthGate title="登录后申请转园" subtitle="手机号即账号,未注册自动注册" /></section>
|
||||
</ContentTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ContentTemplate active="park-transfer" kicker="Apply" title="转园申请" desc="园区 OPC 可申请转入其它园区,需园区/平台审核">
|
||||
{cur && (
|
||||
<section className="section pf-sub" style={{ margin: 0 }}>
|
||||
<span className="pf-muted">目标:{cur.to_park_name || cur.to_park_id || '-'} · 状态:</span>
|
||||
<b style={{ color: '#7fd087' }}>{ST_TEXT[curSt] || curSt}</b>
|
||||
{cur.review_comment && <span className="pf-muted"> · 意见:{cur.review_comment}</span>}
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section className="section">
|
||||
<SectionHead no="01" title="选择目标园区" en="Target" />
|
||||
<FieldSelect label="目标园区 *" options={parks.map((p) => p.name)} value={parks.find((p) => p.id === toParkId)?.name || ''}
|
||||
onChange={(name) => setToParkId(parks.find((p) => p.name === name)?.id || '')} />
|
||||
<FieldTextarea label="转园理由" placeholder="请说明转园原因" value={reason} onChange={(v) => setReason(v)} rows={4} />
|
||||
</section>
|
||||
|
||||
{disabled && <p className="pf-muted" style={{ color: '#f0b8b8' }}>已有转园申请审核中,请勿重复提交</p>}
|
||||
{err && <p className="pf-muted" style={{ color: '#f0b8b8' }}>{err}</p>}
|
||||
<section className="section">
|
||||
<Button variant="cta" disabled={busy || disabled} loading={busy} onClick={submit}>提交转园申请</Button>
|
||||
</section>
|
||||
</ContentTemplate>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user