diff --git a/miniprogram/src/app.config.js b/miniprogram/src/app.config.js index 1d6c4b1..9af9480 100644 --- a/miniprogram/src/app.config.js +++ b/miniprogram/src/app.config.js @@ -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: { diff --git a/miniprogram/src/pages/cert-apply/index.config.js b/miniprogram/src/pages/cert-apply/index.config.js new file mode 100644 index 0000000..7590d70 --- /dev/null +++ b/miniprogram/src/pages/cert-apply/index.config.js @@ -0,0 +1 @@ +export default { navigationBarTitleText: 'OPC 认证' }; diff --git a/miniprogram/src/pages/cert-apply/index.jsx b/miniprogram/src/pages/cert-apply/index.jsx new file mode 100644 index 0000000..a3f9880 --- /dev/null +++ b/miniprogram/src/pages/cert-apply/index.jsx @@ -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 ( + + + 认证申请已提交 + 平台将对您的资料进行审核,请留意认证状态。 + Taro.navigateBack()}>返回 + + ); + } + + if (!loaded) return 加载中…; + + // 未登录/未绑手机 → 快捷门 + if (!authed) { + return ( + + + OPC 认证申请 + 请先用手机号登录(验证码 123456) + setGatePhone(e.detail.value)} /> + setGateCode(e.detail.value)} /> + {gateErr && {gateErr}} + 登录 / 绑定 + + + ); + } + + const st = mine?.certification_status || 'uncertified'; + const disabled = st === 'certified' || st === 'pending' || st === 'reviewing'; + + return ( + + + OPC 认证申请 + 当前认证状态:{statusText(st)} + + + + 一 · 基本信息 + up('real_name', v)} /> + 性别 + up('gender', e.detail.value)}> + {GENDER_OPTS.map((g) => {g})} + + + up('address', v)} /> + up('industry', v)} /> + up('phone', v)} /> + + + + 二 · 能力与成果 + 专业技能 / 代表作品 / 成果 + up('ability', e.detail.value)} /> + + + + 三 · 佐证材料 + {DOC_ITEMS.map((d) => ( + + {d.label} + pickAndUpload(d.key)}> + {docs[d.key] ? '已上传 ✓' : '选择文件'} + + {docs[d.key] && {docs[d.key].split('/').pop()}} + + ))} + + + {disabled && {st === 'certified' ? '您已是认证 OPC,无需重复申请' : '已有认证申请审核中,请勿重复提交'}} + {err && {err}} + 提交认证申请 + 提交即授权平台审核材料,结果可在「我的」查看 + + ); +} + +const DOC_ITEMS = [ + { key: 'id_card_doc', label: '身份证(正面)' }, + { key: 'achievement', label: '成果 / 资质证明材料' }, + { key: 'other', label: '其他佐证材料' }, +]; + +function Field({ label, value, ph, onChange }) { + return ( + + {label} + onChange(e.detail.value)} /> + + ); +} diff --git a/miniprogram/src/pages/cert-apply/index.scss b/miniprogram/src/pages/cert-apply/index.scss new file mode 100644 index 0000000..7331d67 --- /dev/null +++ b/miniprogram/src/pages/cert-apply/index.scss @@ -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; } diff --git a/miniprogram/src/pages/index/index.jsx b/miniprogram/src/pages/index/index.jsx index 3698d0d..de936d9 100644 --- a/miniprogram/src/pages/index/index.jsx +++ b/miniprogram/src/pages/index/index.jsx @@ -26,6 +26,8 @@ export default function Index() { {MK_HERO.ctaPrimary.label} → {MK_HERO.ctaSecondary.label} Taro.navigateTo({ url: '/pages/park-apply/index' })}>申请入驻 + Taro.navigateTo({ url: '/pages/cert-apply/index' })}>OPC认证 + Taro.navigateTo({ url: '/pages/park-transfer/index' })}>转园申请 diff --git a/miniprogram/src/pages/mine/index.jsx b/miniprogram/src/pages/mine/index.jsx index 454bed0..3196a81 100644 --- a/miniprogram/src/pages/mine/index.jsx +++ b/miniprogram/src/pages/mine/index.jsx @@ -401,6 +401,24 @@ export default function Mine() { › + {/* 申请/认证入口:入驻 / OPC认证 / 转园 */} + + APPLY + 申请与认证 + + Taro.navigateTo({ url: '/pages/park-apply/index' })}> + 园区入驻申请填写入园申请表,入驻创业园 + › + + Taro.navigateTo({ url: '/pages/cert-apply/index' })}> + OPC 认证提交认证资料,认证 OPC 超级个体 + › + + Taro.navigateTo({ url: '/pages/park-transfer/index' })}> + 转园申请园区 OPC 申请转入其它园区 + › + + {/* 我的报名 */} BOOK diff --git a/miniprogram/src/pages/park-transfer/index.config.js b/miniprogram/src/pages/park-transfer/index.config.js new file mode 100644 index 0000000..c37c1aa --- /dev/null +++ b/miniprogram/src/pages/park-transfer/index.config.js @@ -0,0 +1 @@ +export default { navigationBarTitleText: '转园申请' }; diff --git a/miniprogram/src/pages/park-transfer/index.jsx b/miniprogram/src/pages/park-transfer/index.jsx new file mode 100644 index 0000000..ae692a5 --- /dev/null +++ b/miniprogram/src/pages/park-transfer/index.jsx @@ -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 ( + + + 转园申请已提交 + 园区与平台将对您的转园申请进行审核。 + Taro.navigateBack()}>返回 + + ); + } + + if (!loaded) return 加载中…; + + if (!authed) { + return ( + + + 转园申请 + 请先用手机号登录(验证码 123456) + setGatePhone(e.detail.value)} /> + setGateCode(e.detail.value)} /> + {gateErr && {gateErr}} + 登录 / 绑定 + + + ); + } + + const disabled = curSt === 'pending' || curSt === 'reviewing'; + + return ( + + + 转园申请 + 当前园区 OPC 可申请转入其它园区,需园区/平台审核。 + + + {cur && ( + + 当前申请 + + 目标:{cur.to_park_name || cur.to_park_id || '-'} · 状态: + {{ pending: '审核中', reviewing: '审核中', approved: '已通过', rejected: '未通过' }[curSt] || curSt} + + {cur.review_comment && 审核意见:{cur.review_comment}} + + )} + + + 选择目标园区 + p.name)} value={Math.max(0, parks.findIndex((p) => p.id === toParkId))} + onChange={(e) => setToParkId(parks[Number(e.detail.value)]?.id || '')}> + {parks.find((p) => p.id === toParkId)?.name || '请选择目标园区'} ▾ + + 转园理由 + setReason(e.detail.value)} /> + + + {disabled && 已有转园申请审核中,请勿重复提交} + {err && {err}} + 提交转园申请 + 仅园区 OPC 可发起转园;审核结果可在「我的」查看 + + ); +} diff --git a/miniprogram/src/pages/park-transfer/index.scss b/miniprogram/src/pages/park-transfer/index.scss new file mode 100644 index 0000000..e7e5fcd --- /dev/null +++ b/miniprogram/src/pages/park-transfer/index.scss @@ -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; } diff --git a/miniprogram/src/utils/profile.js b/miniprogram/src/utils/profile.js new file mode 100644 index 0000000..6dbb46a --- /dev/null +++ b/miniprogram/src/utils/profile.js @@ -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 || {}; +} diff --git a/website/src/App.jsx b/website/src/App.jsx index ce61533..75e05ef 100644 --- a/website/src/App.jsx +++ b/website/src/App.jsx @@ -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 } ]; diff --git a/website/src/services/opcProfile.js b/website/src/services/opcProfile.js new file mode 100644 index 0000000..6e4d1ed --- /dev/null +++ b/website/src/services/opcProfile.js @@ -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 {}; + } +} diff --git a/website/src/user/CertApply.jsx b/website/src/user/CertApply.jsx new file mode 100644 index 0000000..163ae7b --- /dev/null +++ b/website/src/user/CertApply.jsx @@ -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 ( + + + ✓ + OPC 认证申请已提交 + 平台将对您的资料进行审核,结果可在个人中心留意。 + + 返回个人中心 + + + + ); + } + + if (!authed) { + return ( + + + + ); + } + + return ( + + + {stText(st)} + {user?.username && · {user.username}} + + + + + + + + + + + + + + + + + + + + + {[{ k: 'id_card_doc', l: '身份证(正面)' }, { k: 'achievement', l: '成果 / 资质证明材料' }, { k: 'other', l: '其他佐证材料' }].map((d) => ( + + {docs[d.k] ? 已上传:{docs[d.k].split('/').pop()} : `选择文件 · ${d.l}`} + pickDoc(d.k, e.target.files && e.target.files[0])} /> + + ))} + + + {disabled && {st === 'certified' ? '您已是认证 OPC,无需重复申请' : '已有认证申请审核中,请勿重复提交'}} + {err && {err}} + + 提交认证申请 + + + ); +} diff --git a/website/src/user/Home.jsx b/website/src/user/Home.jsx index ff1d6d3..b186759 100644 --- a/website/src/user/Home.jsx +++ b/website/src/user/Home.jsx @@ -158,6 +158,36 @@ export default function Home() { + {/* 2.6 · OPC认证 */} + + + + + 认证 + 提交资料认证 OPC 超级个体 + 填写能力与成果、上传佐证材料,经平台审核即成为认证 OPC。 + + + 去认证 → + + + + + {/* 2.7 · 转园申请 */} + + + + + 转园 + 园区 OPC 申请转入其它园区 + 选择目标园区并说明理由,经园区 / 平台审核通过后更新归属。 + + + 申请转园 → + + + + {/* 3 · 痛点理念 */} diff --git a/website/src/user/ParkTransfer.jsx b/website/src/user/ParkTransfer.jsx new file mode 100644 index 0000000..663dc51 --- /dev/null +++ b/website/src/user/ParkTransfer.jsx @@ -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 ( + + + ✓ + 转园申请已提交 + 园区与平台将对您的转园申请进行审核,结果可在个人中心留意。 + + 返回个人中心 + + + + ); + } + + if (!authed) { + return ( + + + + ); + } + + return ( + + {cur && ( + + 目标:{cur.to_park_name || cur.to_park_id || '-'} · 状态: + {ST_TEXT[curSt] || curSt} + {cur.review_comment && · 意见:{cur.review_comment}} + + )} + + + + p.name)} value={parks.find((p) => p.id === toParkId)?.name || ''} + onChange={(name) => setToParkId(parks.find((p) => p.name === name)?.id || '')} /> + setReason(v)} rows={4} /> + + + {disabled && 已有转园申请审核中,请勿重复提交} + {err && {err}} + + 提交转园申请 + + + ); +}
平台将对您的资料进行审核,结果可在个人中心留意。
{st === 'certified' ? '您已是认证 OPC,无需重复申请' : '已有认证申请审核中,请勿重复提交'}
{err}
园区与平台将对您的转园申请进行审核,结果可在个人中心留意。
已有转园申请审核中,请勿重复提交