diff --git a/app/jobs/[id].tsx b/app/jobs/[id].tsx index 1400a07..6571257 100644 --- a/app/jobs/[id].tsx +++ b/app/jobs/[id].tsx @@ -148,8 +148,28 @@ function SectionTitle({ export default function JobDetailScreen() { const { id } = useLocalSearchParams<{ id: string }>(); const router = useRouter(); - const { detail, overallScore, salaryData, reviews, loading, error, refresh } = useJobDetail(id); + const { detail, overallScore, salaryData, reviews, loading, error, refresh, publishReview } = + useJobDetail(id); const [comment, setComment] = useState(''); + const [publishing, setPublishing] = useState(false); + + // 发布评价:空内容拦截,成功后清空输入框(列表由 hook 自动刷新) + const handlePublish = async () => { + const text = comment.trim(); + if (!text) { + Alert.alert('提示', '请先输入你的评价内容'); + return; + } + setPublishing(true); + try { + await publishReview(text); + setComment(''); + } catch (e) { + Alert.alert('发布失败', (e as Error).message || '请稍后重试'); + } finally { + setPublishing(false); + } + }; // 错误态 if (error) { @@ -285,8 +305,16 @@ export default function JobDetailScreen() { value={comment} onChangeText={setComment} /> - Alert.alert('发布评论', '评论发布功能开发中,敬请期待')}> - 发布 + + {publishing ? ( + + ) : ( + 发布 + )} @@ -604,6 +632,9 @@ const styles = StyleSheet.create({ justifyContent: 'center', backgroundColor: colors.primaryBtn, }, + publishBtnDisabled: { + opacity: 0.6, + }, publishText: { fontSize: 14, fontWeight: '600', diff --git a/app/legal.tsx b/app/legal.tsx index 6044da0..81098b8 100644 --- a/app/legal.tsx +++ b/app/legal.tsx @@ -1,6 +1,6 @@ import { Ionicons } from '@expo/vector-icons'; -import { useRouter } from 'expo-router'; -import { useState } from 'react'; +import { useFocusEffect, useRouter } from 'expo-router'; +import { useCallback, useState } from 'react'; import { ActivityIndicator, Pressable, @@ -10,7 +10,7 @@ import { View, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; -import { useLegalData } from '../src/hooks'; +import { useLegalData, useLegalMembership } from '../src/hooks'; import { colors } from '../src/theme'; import type { LegalFAQ, LegalPlan, LegalService, LegalTemplate } from '../src/types'; @@ -47,7 +47,16 @@ function BrandCard({ name, company, slogan, logo }: { name: string; company: str /* ===================== 子组件:服务入口 ===================== */ -function ServiceCard({ service, onPress }: { service: LegalService; onPress: () => void }) { +function ServiceCard({ + service, + isUnlocked, + onPress, +}: { + service: LegalService; + /** 会员服务是否已解锁(已开通会员) */ + isUnlocked: boolean; + onPress: () => void; +}) { const isMember = service.type === 'member'; return ( {service.title} {isMember && ( - 会员 + {isUnlocked ? '已开通' : '会员'} )} @@ -107,9 +116,17 @@ function TemplateCard({ template }: { template: LegalTemplate }) { /* ===================== 子组件:会员套餐 ===================== */ -function PlanCard({ plan }: { plan: LegalPlan }) { +function PlanCard({ plan, onPress }: { plan: LegalPlan; onPress?: () => void }) { return ( - + [ + styles.planCard, + plan.recommended && styles.planCardRecommended, + pressed && styles.planPressed, + ]} + onPress={onPress} + disabled={!onPress} + > {plan.recommended && ( 推荐 @@ -129,7 +146,11 @@ function PlanCard({ plan }: { plan: LegalPlan }) { ))} - + + 立即开通 + + + ); } @@ -138,6 +159,14 @@ function PlanCard({ plan }: { plan: LegalPlan }) { export default function LegalScreen() { const router = useRouter(); const { brand, services, faqs, templates, plans, loading, error, refresh } = useLegalData(); + const { membership, refresh: refreshMembership } = useLegalMembership(); + + // 返回本页时刷新会员态(支付成功后需即时反映) + useFocusEffect( + useCallback(() => { + refreshMembership(); + }, [refreshMembership]), + ); const freeServices = services.filter((s) => s.type === 'free'); const memberServices = services.filter((s) => s.type === 'member'); @@ -150,7 +179,12 @@ export default function LegalScreen() { } else if (service.action === 'faq') { router.push('/legal/faq'); } else if (service.action === 'upgrade') { - router.push('/legal/upgrade'); + // 已开通会员 → 进入服务使用页(权益兑现);未开通 → 引导开通 + if (membership) { + router.push(`/legal/service?id=${service.id}`); + } else { + router.push('/legal/upgrade'); + } } }; @@ -190,6 +224,7 @@ export default function LegalScreen() { handleServicePress(service)} /> ))} @@ -201,6 +236,7 @@ export default function LegalScreen() { handleServicePress(service)} /> ))} @@ -231,9 +267,21 @@ export default function LegalScreen() { {plans.length > 0 && ( <> + {membership && ( + + + + 您已开通{membership.planName},可畅享会员专属服务 + + + )} {plans.map((plan) => ( - + router.push(`/legal/checkout?planId=${plan.id}`)} + /> ))} @@ -474,6 +522,39 @@ const styles = StyleSheet.create({ borderWidth: 1, borderColor: colors.border, }, + planPressed: { + opacity: 0.85, + }, + planCtaRow: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'flex-end', + marginTop: 12, + gap: 2, + }, + planCtaText: { + fontSize: 13, + fontWeight: '700', + color: colors.action, + }, + memberBanner: { + flexDirection: 'row', + alignItems: 'center', + gap: 6, + paddingHorizontal: 12, + paddingVertical: 10, + borderRadius: 8, + backgroundColor: colors.actionLight, + borderWidth: 1, + borderColor: colors.action, + marginBottom: 12, + }, + memberBannerText: { + flex: 1, + fontSize: 13, + fontWeight: '600', + color: colors.actionText, + }, planCardRecommended: { borderColor: colors.action, backgroundColor: colors.actionLight, diff --git a/app/legal/checkout.tsx b/app/legal/checkout.tsx new file mode 100644 index 0000000..2258e81 --- /dev/null +++ b/app/legal/checkout.tsx @@ -0,0 +1,395 @@ +import { Ionicons } from '@expo/vector-icons'; +import { useLocalSearchParams, useRouter } from 'expo-router'; +import { useCallback, useState } from 'react'; +import { ActivityIndicator, Alert, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { useLegalData, useLegalMembership } from '../../src/hooks'; +import { LoadingState, EmptyState } from '../../src/components/StateView'; +import { colors } from '../../src/theme'; +import type { LegalPaymentChannel } from '../../src/types'; + +/* ===================== 付款方式 ===================== */ + +const CHANNELS: { id: LegalPaymentChannel; label: string; icon: keyof typeof Ionicons.glyphMap; color: string }[] = [ + { id: 'wechat', label: '微信支付', icon: 'logo-wechat', color: colors.success }, + { id: 'alipay', label: '支付宝', icon: 'wallet-outline', color: colors.primary }, +]; + +/* ===================== 顶部导航 ===================== */ + +function Header({ onBack }: { onBack: () => void }) { + return ( + + + + + 确认支付 + + + ); +} + +/* ===================== 页面 ===================== */ + +export default function LegalCheckoutScreen() { + const router = useRouter(); + const { planId } = useLocalSearchParams(); + const id = Array.isArray(planId) ? planId[0] : planId; + const { plans, loading } = useLegalData(); + const { createOrder, payOrder, processing } = useLegalMembership(); + + const [channel, setChannel] = useState('wechat'); + const [paid, setPaid] = useState(false); + + const plan = plans.find((p) => p.id === id); + + const handlePay = useCallback(async () => { + if (!plan) return; + try { + const order = await createOrder(plan.id, channel); + const result = await payOrder(order.id); + if (result.status === 'paid') { + setPaid(true); + } else { + Alert.alert('支付失败', '请稍后重试'); + } + } catch (e) { + Alert.alert('支付失败', (e as Error).message || '请稍后重试'); + } + }, [plan, channel, createOrder, payOrder]); + + // 加载套餐中 + if (loading) { + return ( + +
router.back()} /> + + + ); + } + + // 套餐不存在(异常入口) + if (!plan) { + return ( + +
router.back()} /> + + + ); + } + + // 支付成功 + if (paid) { + return ( + +
router.replace('/legal')} /> + + + + + 支付成功 + + 已开通{plan.name},可畅享会员专属法律服务 + + router.replace('/legal')}> + 返回法律服务 + + + + ); + } + + return ( + +
router.back()} /> + + {/* 套餐摘要 */} + + {plan.name} + + ¥ + {plan.price} + {plan.period} + + + {plan.benefits.map((benefit, idx) => ( + + + {benefit} + + ))} + + + + {/* 支付方式 */} + 选择支付方式 + + {CHANNELS.map((c) => { + const active = c.id === channel; + return ( + [ + styles.channelRow, + active && styles.channelRowActive, + pressed && styles.channelPressed, + ]} + onPress={() => setChannel(c.id)} + > + + {c.label} + + {active && } + + + ); + })} + + + {/* 实付金额 */} + + 实付金额 + + ¥ + {plan.price} + + + + + {/* 确认支付(底部固定) */} + + [styles.payBtn, pressed && styles.payBtnPressed, processing && styles.payBtnDisabled]} + onPress={handlePay} + disabled={processing} + > + {processing ? ( + + ) : ( + 确认支付 ¥{plan.price} + )} + + + + ); +} + +const styles = StyleSheet.create({ + safeArea: { + flex: 1, + backgroundColor: colors.pageBg, + }, + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 8, + height: 48, + backgroundColor: colors.white, + borderBottomWidth: 1, + borderBottomColor: colors.borderLight, + }, + headerBtn: { + width: 40, + height: 40, + alignItems: 'center', + justifyContent: 'center', + }, + headerTitle: { + fontSize: 17, + fontWeight: '700', + color: colors.textPrimary, + }, + scroll: { + flex: 1, + }, + content: { + padding: 12, + paddingBottom: 24, + }, + summaryCard: { + padding: 16, + borderRadius: 8, + backgroundColor: colors.white, + borderWidth: 1, + borderColor: colors.border, + }, + summaryName: { + fontSize: 16, + fontWeight: '700', + color: colors.textPrimary, + }, + summaryPriceRow: { + flexDirection: 'row', + alignItems: 'baseline', + marginTop: 8, + marginBottom: 12, + }, + summaryCurrency: { + fontSize: 15, + fontWeight: '700', + color: colors.actionText, + }, + summaryPrice: { + fontSize: 30, + fontWeight: '700', + color: colors.actionText, + }, + summaryPeriod: { + fontSize: 13, + color: colors.textSecondary, + marginLeft: 2, + }, + benefits: { + gap: 6, + }, + benefitRow: { + flexDirection: 'row', + alignItems: 'center', + gap: 6, + }, + benefitText: { + fontSize: 12, + color: colors.textSecondary, + }, + sectionTitle: { + marginTop: 20, + marginBottom: 10, + fontSize: 15, + fontWeight: '700', + color: colors.textPrimary, + }, + channelGroup: { + gap: 8, + }, + channelRow: { + flexDirection: 'row', + alignItems: 'center', + padding: 14, + borderRadius: 8, + backgroundColor: colors.white, + borderWidth: 1, + borderColor: colors.border, + gap: 12, + }, + channelRowActive: { + borderColor: colors.action, + backgroundColor: colors.actionLight, + }, + channelPressed: { + opacity: 0.85, + }, + channelLabel: { + flex: 1, + fontSize: 15, + fontWeight: '600', + color: colors.textPrimary, + }, + radio: { + width: 20, + height: 20, + borderRadius: 10, + borderWidth: 2, + borderColor: colors.border, + alignItems: 'center', + justifyContent: 'center', + }, + radioActive: { + borderColor: colors.action, + }, + radioDot: { + width: 10, + height: 10, + borderRadius: 5, + backgroundColor: colors.action, + }, + amountRow: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + marginTop: 20, + paddingTop: 16, + borderTopWidth: 1, + borderTopColor: colors.borderLight, + }, + amountLabel: { + fontSize: 14, + color: colors.textSecondary, + }, + amountValue: { + flexDirection: 'row', + alignItems: 'baseline', + }, + amountCurrency: { + fontSize: 15, + fontWeight: '700', + color: colors.actionText, + }, + amountPrice: { + fontSize: 22, + fontWeight: '700', + color: colors.actionText, + }, + footer: { + padding: 12, + backgroundColor: colors.white, + borderTopWidth: 1, + borderTopColor: colors.borderLight, + }, + payBtn: { + height: 48, + borderRadius: 8, + backgroundColor: colors.primaryBtn, + alignItems: 'center', + justifyContent: 'center', + }, + payBtnPressed: { + opacity: 0.9, + }, + payBtnDisabled: { + opacity: 0.6, + }, + payBtnText: { + fontSize: 16, + fontWeight: '700', + color: colors.white, + }, + successBox: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + padding: 24, + }, + successIconWrap: { + marginBottom: 16, + }, + successTitle: { + fontSize: 20, + fontWeight: '700', + color: colors.textPrimary, + }, + successDesc: { + marginTop: 8, + fontSize: 14, + color: colors.textSecondary, + textAlign: 'center', + }, + successBtn: { + marginTop: 28, + width: 200, + height: 46, + borderRadius: 23, + backgroundColor: colors.primaryBtn, + alignItems: 'center', + justifyContent: 'center', + }, + successBtnText: { + fontSize: 15, + fontWeight: '700', + color: colors.white, + }, +}); diff --git a/app/legal/service.tsx b/app/legal/service.tsx new file mode 100644 index 0000000..580747c --- /dev/null +++ b/app/legal/service.tsx @@ -0,0 +1,430 @@ +import { Ionicons } from '@expo/vector-icons'; +import { useFocusEffect, useLocalSearchParams, useRouter } from 'expo-router'; +import { useCallback, useState } from 'react'; +import { + ActivityIndicator, + Alert, + Pressable, + ScrollView, + StyleSheet, + Text, + TextInput, + View, +} from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { useLegalData, useLegalMembership, useLegalServiceRequests } from '../../src/hooks'; +import { LoadingState, EmptyState } from '../../src/components/StateView'; +import { colors } from '../../src/theme'; + +/* ===================== 顶部导航 ===================== */ + +function Header({ onBack }: { onBack: () => void }) { + return ( + + + + + 会员服务 + + + ); +} + +/* ===================== 页面 ===================== */ + +export default function LegalServiceScreen() { + const router = useRouter(); + const { id } = useLocalSearchParams(); + const serviceId = Array.isArray(id) ? id[0] : id; + const { services, loading } = useLegalData(); + const { membership, refresh: refreshMembership } = useLegalMembership(); + const { + requests, + loading: requestsLoading, + refresh: refreshRequests, + submit, + submitting, + } = useLegalServiceRequests(); + + const [content, setContent] = useState(''); + + // 返回本页时刷新会员态与申请记录 + useFocusEffect( + useCallback(() => { + refreshMembership(); + refreshRequests(); + }, [refreshMembership, refreshRequests]), + ); + + const service = services.find((s) => s.id === serviceId); + + const handleSubmit = useCallback(async () => { + if (!service) return; + const text = content.trim(); + if (!text) { + Alert.alert('提示', '请先填写你的需求描述'); + return; + } + try { + await submit(service.id, text); + setContent(''); + Alert.alert('提交成功', '我们已收到你的申请,将尽快安排专人跟进。'); + } catch (e) { + Alert.alert('提交失败', (e as Error).message || '请稍后重试'); + } + }, [service, content, submit]); + + if (loading) { + return ( + +
router.back()} /> + + + ); + } + + if (!service) { + return ( + +
router.back()} /> + + + ); + } + + // 未开通会员:拦截并引导开通(付费前不给权益) + if (!membership) { + return ( + +
router.back()} /> + + + + + {service.title} + {service.description} + 该服务为会员专享,开通后即可使用 + router.push('/legal/upgrade')}> + 开通会员 + + + + ); + } + + const myRequests = requests.filter((r) => r.serviceId === service.id); + const canSubmit = content.trim().length > 0 && !submitting; + + return ( + +
router.back()} /> + + {/* 服务说明 */} + + + + + + + {service.title} + {service.description} + + + {service.requestHint && {service.requestHint}} + + + {/* 需求填写 */} + 填写你的需求 + + {content.length}/500 + + [ + styles.submitBtn, + !canSubmit && styles.submitBtnDisabled, + pressed && canSubmit && styles.submitBtnPressed, + ]} + onPress={handleSubmit} + disabled={!canSubmit} + > + {submitting ? ( + + ) : ( + 提交申请 + )} + + + {/* 我的申请记录 */} + 我的申请记录 + {requestsLoading ? ( + + ) : myRequests.length === 0 ? ( + + 暂无申请记录 + + ) : ( + + {myRequests.map((r) => ( + + + {r.serviceName} + + + {r.status === 'submitted' ? '已提交' : r.status === 'processing' ? '处理中' : '已完成'} + + + + {r.content} + + {new Date(r.createdAt).toLocaleString('zh-CN')} + + + ))} + + )} + + + ); +} + +const styles = StyleSheet.create({ + safeArea: { + flex: 1, + backgroundColor: colors.pageBg, + }, + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 8, + height: 48, + backgroundColor: colors.white, + borderBottomWidth: 1, + borderBottomColor: colors.borderLight, + }, + headerBtn: { + width: 40, + height: 40, + alignItems: 'center', + justifyContent: 'center', + }, + headerTitle: { + fontSize: 17, + fontWeight: '700', + color: colors.textPrimary, + }, + scroll: { + flex: 1, + }, + content: { + padding: 12, + paddingBottom: 24, + }, + card: { + padding: 14, + borderRadius: 8, + backgroundColor: colors.white, + borderWidth: 1, + borderColor: colors.border, + }, + serviceHead: { + flexDirection: 'row', + alignItems: 'center', + }, + serviceIconWrap: { + width: 44, + height: 44, + borderRadius: 22, + backgroundColor: colors.actionLight, + alignItems: 'center', + justifyContent: 'center', + }, + serviceHeadBody: { + flex: 1, + marginLeft: 12, + }, + serviceName: { + fontSize: 16, + fontWeight: '700', + color: colors.textPrimary, + }, + serviceDesc: { + marginTop: 2, + fontSize: 12, + color: colors.textSecondary, + lineHeight: 17, + }, + hint: { + marginTop: 12, + paddingTop: 12, + borderTopWidth: 1, + borderTopColor: colors.borderLight, + fontSize: 13, + lineHeight: 19, + color: colors.textSecondary, + }, + sectionTitle: { + marginTop: 20, + marginBottom: 10, + fontSize: 15, + fontWeight: '700', + color: colors.textPrimary, + }, + input: { + minHeight: 120, + padding: 12, + borderRadius: 8, + backgroundColor: colors.white, + borderWidth: 1, + borderColor: colors.border, + fontSize: 14, + lineHeight: 20, + color: colors.textPrimary, + }, + counter: { + marginTop: 6, + textAlign: 'right', + fontSize: 12, + color: colors.textHint, + }, + submitBtn: { + marginTop: 16, + height: 48, + borderRadius: 8, + backgroundColor: colors.primaryBtn, + alignItems: 'center', + justifyContent: 'center', + }, + submitBtnPressed: { + opacity: 0.9, + }, + submitBtnDisabled: { + opacity: 0.5, + }, + submitBtnText: { + fontSize: 16, + fontWeight: '700', + color: colors.white, + }, + requestsLoading: { + marginTop: 16, + }, + emptyCard: { + padding: 20, + borderRadius: 8, + backgroundColor: colors.white, + alignItems: 'center', + }, + emptyText: { + fontSize: 13, + color: colors.textSecondary, + }, + requestList: { + gap: 8, + }, + requestCard: { + padding: 12, + borderRadius: 8, + backgroundColor: colors.white, + borderWidth: 1, + borderColor: colors.border, + }, + requestHead: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, + requestService: { + flex: 1, + fontSize: 14, + fontWeight: '700', + color: colors.textPrimary, + marginRight: 8, + }, + statusTag: { + paddingHorizontal: 8, + paddingVertical: 2, + borderRadius: 4, + backgroundColor: colors.primaryLight, + }, + statusText: { + fontSize: 11, + fontWeight: '700', + color: colors.primaryText, + }, + requestContent: { + marginTop: 8, + fontSize: 13, + lineHeight: 19, + color: colors.textSecondary, + }, + requestTime: { + marginTop: 8, + fontSize: 11, + color: colors.textHint, + }, + lockedBox: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + padding: 24, + }, + lockedIconWrap: { + width: 80, + height: 80, + borderRadius: 40, + backgroundColor: colors.actionLight, + alignItems: 'center', + justifyContent: 'center', + marginBottom: 16, + }, + lockedTitle: { + fontSize: 18, + fontWeight: '700', + color: colors.textPrimary, + }, + lockedDesc: { + marginTop: 8, + fontSize: 14, + lineHeight: 20, + color: colors.textSecondary, + textAlign: 'center', + }, + lockedTip: { + marginTop: 12, + fontSize: 13, + fontWeight: '600', + color: colors.actionText, + }, + lockedBtn: { + marginTop: 28, + width: 200, + height: 46, + borderRadius: 23, + backgroundColor: colors.primaryBtn, + alignItems: 'center', + justifyContent: 'center', + }, + lockedBtnText: { + fontSize: 15, + fontWeight: '700', + color: colors.white, + }, +}); diff --git a/app/legal/upgrade.tsx b/app/legal/upgrade.tsx index 7c94cd5..bc8802d 100644 --- a/app/legal/upgrade.tsx +++ b/app/legal/upgrade.tsx @@ -1,8 +1,9 @@ import { Ionicons } from '@expo/vector-icons'; -import { useRouter } from 'expo-router'; +import { useFocusEffect, useRouter } from 'expo-router'; +import { useCallback } from 'react'; import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; -import { useLegalData } from '../../src/hooks'; +import { useLegalData, useLegalMembership } from '../../src/hooks'; import { LoadingState, ErrorState, EmptyState } from '../../src/components/StateView'; import { colors } from '../../src/theme'; import type { LegalPlan } from '../../src/types'; @@ -23,9 +24,17 @@ function Header({ onBack }: { onBack: () => void }) { /* ===================== 套餐卡片 ===================== */ -function PlanCard({ plan }: { plan: LegalPlan }) { +function PlanCard({ plan, onPress }: { plan: LegalPlan; onPress?: () => void }) { return ( - + [ + styles.planCard, + plan.recommended && styles.planCardRecommended, + pressed && styles.planPressed, + ]} + onPress={onPress} + disabled={!onPress} + > {plan.recommended && ( 推荐 @@ -45,7 +54,11 @@ function PlanCard({ plan }: { plan: LegalPlan }) { ))} - + + 立即开通 + + + ); } @@ -54,6 +67,15 @@ function PlanCard({ plan }: { plan: LegalPlan }) { export default function LegalUpgradeScreen() { const router = useRouter(); const { plans, loading, error, refresh } = useLegalData(); + const { membership, refresh: refreshMembership } = useLegalMembership(); + + useFocusEffect( + useCallback(() => { + refreshMembership(); + }, [refreshMembership]), + ); + + const formatDate = (iso: string) => new Date(iso).toLocaleDateString('zh-CN'); return ( @@ -70,10 +92,23 @@ export default function LegalUpgradeScreen() { contentContainerStyle={styles.content} showsVerticalScrollIndicator={false} > - 开通会员后可享受律师咨询、合同审核等专属法律服务 + {membership ? ( + + + + 您已开通{membership.planName},有效期至 {formatDate(membership.expiresAt)} + + + ) : ( + 开通会员后可享受律师咨询、合同审核等专属法律服务 + )} {plans.map((plan) => ( - + router.push(`/legal/checkout?planId=${plan.id}`)} + /> ))} @@ -130,6 +165,39 @@ const styles = StyleSheet.create({ borderWidth: 1, borderColor: colors.border, }, + planPressed: { + opacity: 0.85, + }, + planCtaRow: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'flex-end', + marginTop: 12, + gap: 2, + }, + planCtaText: { + fontSize: 13, + fontWeight: '700', + color: colors.action, + }, + memberBanner: { + flexDirection: 'row', + alignItems: 'center', + gap: 6, + paddingHorizontal: 12, + paddingVertical: 10, + borderRadius: 8, + backgroundColor: colors.actionLight, + borderWidth: 1, + borderColor: colors.action, + marginBottom: 12, + }, + memberBannerText: { + flex: 1, + fontSize: 13, + fontWeight: '600', + color: colors.actionText, + }, planCardRecommended: { borderColor: colors.action, backgroundColor: colors.actionLight, diff --git a/src/api/api.ts b/src/api/api.ts index 15bd0d6..59e9527 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -74,8 +74,12 @@ import type { LegalConversation, LegalFAQ, LegalMessage, + LegalMembership, + LegalOrder, + LegalPaymentChannel, LegalPlan, LegalService, + LegalServiceRequest, LegalTemplate, ToggleSetting, ActionSetting, @@ -122,6 +126,7 @@ import type { } from '../types'; import { MOCK_DELAY, SALARY_RANGES, TOKEN_TTL_MS } from '../constants'; import { colors } from '../theme'; +import { storageGet, storageSet, storageRemove } from '../lib/storage'; /** 接口地址(通过 expo-constants 从 app.config.js 的 extra 读取) */ export const API_BASE_URL: string = @@ -1240,8 +1245,50 @@ export async function fetchSalaryData(id: string): Promise { */ export async function fetchEmployeeReviews(id: string): Promise { // TODO: const res = await fetch(`${API_BASE_URL}/api/jobs/${id}/reviews`, { method: 'GET' }); return res.json(); - void id; - return mockRequest(MOCK_EMPLOYEE_REVIEWS); + // mock 阶段:内置评价 + 该岗位下本人已发布的评价(本人发布的最新在前) + const mine = (await readJobReviews()).filter((r) => r.jobId === id); + return mockRequest([...mine, ...MOCK_EMPLOYEE_REVIEWS]); +} + +/* ===================== 岗位评价发布(mock 阶段持久化到 AsyncStorage) ===================== */ + +const JOB_REVIEWS_KEY = 'job:reviews'; + +/** 本人发布评价的存储结构:在 EmployeeReview 上补充归属岗位 */ +interface StoredJobReview extends EmployeeReview { + jobId: string; +} + +/** 读取本人发布的全部评价 */ +async function readJobReviews(): Promise { + const raw = await storageGet(JOB_REVIEWS_KEY); + return raw ? (JSON.parse(raw) as StoredJobReview[]) : []; +} + +/** + * 发布岗位评价 + * @method POST + * @url /api/jobs/:id/reviews + * @param id 岗位 id + * @param content 评价内容 + * @returns Promise 新建的评价 + * @remark 真实后端下需做敏感词校验与重复提交限制,此处不做 + */ +export async function createJobReview(id: string, content: string): Promise { + // TODO: const res = await fetch(`${API_BASE_URL}/api/jobs/${id}/reviews`, { method: 'POST', body: JSON.stringify({ content }) }); return res.json(); + const review: StoredJobReview = { + id: `my-review-${Date.now()}`, + jobId: id, + userName: '我', + userRole: '我的评价', + time: '刚刚', + content, + tags: [], + }; + const all = await readJobReviews(); + all.unshift(review); + await storageSet(JOB_REVIEWS_KEY, JSON.stringify(all)); + return mockRequest(review); } /** @@ -3813,6 +3860,7 @@ const MOCK_LEGAL_SERVICES: LegalService[] = [ icon: 'people-outline', type: 'member', action: 'upgrade', + requestHint: '请简要描述你遇到的劳动法律问题,例如试用期被辞退、拖欠工资、未签劳动合同等,律师将在 24 小时内响应。', }, { id: 'member-review', @@ -3821,6 +3869,7 @@ const MOCK_LEGAL_SERVICES: LegalService[] = [ icon: 'reader-outline', type: 'member', action: 'upgrade', + requestHint: '请说明需要审核的文书类型与你重点关注的风险点,例如劳动合同中的违约金条款、竞业限制范围等。', }, { id: 'member-advisor', @@ -3829,6 +3878,7 @@ const MOCK_LEGAL_SERVICES: LegalService[] = [ icon: 'shield-checkmark-outline', type: 'member', action: 'upgrade', + requestHint: '请描述你希望顾问长期跟进的事项,例如在职期间的权益保障、跳槽风险把控等。', }, ]; @@ -3973,6 +4023,198 @@ export async function fetchLegalPlans(): Promise { return mockRequest(MOCK_LEGAL_PLANS); } +/* ===================== 付费 / 订单 / 会员态(mock 阶段持久化到 AsyncStorage) ===================== */ + +const LEGAL_MEMBERSHIP_KEY = 'legal:membership'; +const LEGAL_ORDERS_KEY = 'legal:orders'; + +/** 读取会员态(storage 为唯一真相源,避免内存态过期) */ +async function readLegalMembership(): Promise { + const raw = await storageGet(LEGAL_MEMBERSHIP_KEY); + return raw ? (JSON.parse(raw) as LegalMembership) : null; +} + +/** 写入会员态(null 表示清除) */ +async function writeLegalMembership(m: LegalMembership | null): Promise { + if (m) { + await storageSet(LEGAL_MEMBERSHIP_KEY, JSON.stringify(m)); + } else { + await storageRemove(LEGAL_MEMBERSHIP_KEY); + } +} + +/** 读取订单列表 */ +async function readLegalOrders(): Promise { + const raw = await storageGet(LEGAL_ORDERS_KEY); + return raw ? (JSON.parse(raw) as LegalOrder[]) : []; +} + +/** 写入订单列表 */ +async function writeLegalOrders(orders: LegalOrder[]): Promise { + await storageSet(LEGAL_ORDERS_KEY, JSON.stringify(orders)); +} + +/** + * 获取当前会员态 + * @method GET + * @url /api/legal/membership + * @returns Promise 未开通则返回 null + */ +export async function fetchLegalMembership(): Promise { + // TODO: const res = await fetch(`${API_BASE_URL}/api/legal/membership`, { method: 'GET' }); return res.json(); + return mockRequest(await readLegalMembership()); +} + +/** + * 获取付费订单列表(按创建时间倒序) + * @method GET + * @url /api/legal/orders + * @returns Promise 订单列表 + */ +export async function fetchLegalOrders(): Promise { + // TODO: const res = await fetch(`${API_BASE_URL}/api/legal/orders`, { method: 'GET' }); return res.json(); + return mockRequest(await readLegalOrders()); +} + +/** + * 创建会员订单(mock 阶段仅本地落库,不接真实支付) + * @method POST + * @url /api/legal/orders + * @param planId 套餐 id + * @param channel 支付方式 + * @returns Promise 新建的待支付订单 + */ +export async function createLegalOrder( + planId: string, + channel: LegalPaymentChannel, +): Promise { + // TODO: const res = await fetch(`${API_BASE_URL}/api/legal/orders`, { method: 'POST', body: JSON.stringify({ planId, channel }) }); return res.json(); + const plans = await fetchLegalPlans(); + const plan = plans.find((p) => p.id === planId); + if (!plan) { + throw new Error('套餐不存在'); + } + const order: LegalOrder = { + id: `order-${Date.now()}`, + planId: plan.id, + planName: plan.name, + price: plan.price, + channel, + status: 'pending', + createdAt: new Date().toISOString(), + }; + const orders = await readLegalOrders(); + orders.unshift(order); + await writeLegalOrders(orders); + return mockRequest(order); +} + +/** + * 支付订单(mock 阶段模拟支付处理并激活会员态) + * @method POST + * @url /api/legal/orders/:id/pay + * @param orderId 订单 id + * @returns Promise 支付后的订单(status=paid) + * @remark 真实接入时,应由后端支付回调确认支付结果后再激活会员态,前端不自行判定成功 + */ +export async function payLegalOrder(orderId: string): Promise { + // TODO: 调用真实支付 SDK → 后端回调确认 → 返回订单 + const orders = await readLegalOrders(); + const idx = orders.findIndex((o) => o.id === orderId); + if (idx === -1) { + throw new Error('订单不存在'); + } + const paid: LegalOrder = { + ...orders[idx], + status: 'paid', + paidAt: new Date().toISOString(), + }; + orders[idx] = paid; + await writeLegalOrders(orders); + + // 根据套餐周期计算会员有效期并激活会员态 + const plans = await fetchLegalPlans(); + const plan = plans.find((p) => p.id === paid.planId); + if (plan) { + const now = new Date(); + const expires = new Date(now.getTime()); + // period: '/年' 或 '/月' + if (plan.period.includes('年')) { + expires.setFullYear(now.getFullYear() + 1); + } else { + expires.setMonth(now.getMonth() + 1); + } + await writeLegalMembership({ + planId: plan.id, + planName: plan.name, + startedAt: now.toISOString(), + expiresAt: expires.toISOString(), + }); + } + + // 模拟支付处理延时 + await new Promise((resolve) => setTimeout(resolve, 800)); + return mockRequest(paid); +} + +/* ===================== 会员服务使用申请(付费后权益兑现) ===================== */ + +const LEGAL_REQUESTS_KEY = 'legal:requests'; + +/** 读取服务申请列表 */ +async function readLegalRequests(): Promise { + const raw = await storageGet(LEGAL_REQUESTS_KEY); + return raw ? (JSON.parse(raw) as LegalServiceRequest[]) : []; +} + +/** 写入服务申请列表 */ +async function writeLegalRequests(requests: LegalServiceRequest[]): Promise { + await storageSet(LEGAL_REQUESTS_KEY, JSON.stringify(requests)); +} + +/** + * 获取会员服务使用申请列表(按提交时间倒序) + * @method GET + * @url /api/legal/service-requests + * @returns Promise 申请列表 + */ +export async function fetchLegalServiceRequests(): Promise { + // TODO: const res = await fetch(`${API_BASE_URL}/api/legal/service-requests`, { method: 'GET' }); return res.json(); + return mockRequest(await readLegalRequests()); +} + +/** + * 提交会员服务使用申请(仅已开通会员可提交,权限由后端校验) + * @method POST + * @url /api/legal/service-requests + * @param serviceId 服务 id + * @param content 用户填写的需求描述 + * @returns Promise 新建的申请 + */ +export async function createLegalServiceRequest( + serviceId: string, + content: string, +): Promise { + // TODO: const res = await fetch(`${API_BASE_URL}/api/legal/service-requests`, { method: 'POST', body: JSON.stringify({ serviceId, content }) }); return res.json(); + const services = await fetchLegalServices(); + const service = services.find((s) => s.id === serviceId); + if (!service) { + throw new Error('服务不存在'); + } + const request: LegalServiceRequest = { + id: `request-${Date.now()}`, + serviceId: service.id, + serviceName: service.title, + content, + status: 'submitted', + createdAt: new Date().toISOString(), + }; + const requests = await readLegalRequests(); + requests.unshift(request); + await writeLegalRequests(requests); + return mockRequest(request); +} + /** * 获取默认法律咨询服务会话 * @method GET diff --git a/src/api/index.ts b/src/api/index.ts index 1ce157e..14458e4 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -38,6 +38,7 @@ export { fetchLegalServices, fetchLegalTemplates, fetchEmployeeReviews, + createJobReview, fetchEnterpriseEntry, fetchHollandAssessment, fetchHollandResult, @@ -75,6 +76,12 @@ export { fetchDeleteAllData, submitDeleteData, fetchLegalDocument, + fetchLegalMembership, + fetchLegalOrders, + createLegalOrder, + payLegalOrder, + fetchLegalServiceRequests, + createLegalServiceRequest, loginByPhone, fetchWechatLoginResult, bindWechatAndRegister, diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 504c7ea..da14531 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -37,6 +37,8 @@ export { useJobPublish } from './useJobPublish'; export { useKeywordBlock } from './useKeywordBlock'; export { useLegalData } from './useLegalData'; export { useLegalDocument } from './useLegalDocument'; +export { useLegalMembership } from './useLegalMembership'; +export { useLegalServiceRequests } from './useLegalServiceRequests'; export { useMessageData } from './useMessageData'; export { useNotificationSettings } from './useNotificationSettings'; export { useOfferCompareData } from './useOfferCompareData'; diff --git a/src/hooks/useJobDetail.ts b/src/hooks/useJobDetail.ts index b73ef71..71676f2 100644 --- a/src/hooks/useJobDetail.ts +++ b/src/hooks/useJobDetail.ts @@ -4,6 +4,7 @@ */ import { useCallback } from 'react'; import { + createJobReview, fetchEmployeeReviews, fetchJobDetail, fetchJobOverallScore, @@ -38,6 +39,16 @@ export function useJobDetail(id: string) { const { data, loading, error, refresh } = useAsyncData(fetcher); + /** 发布岗位评价,成功后自动刷新评价列表 */ + const publishReview = useCallback( + async (content: string) => { + const created = await createJobReview(id, content); + refresh(); + return created; + }, + [id, refresh], + ); + return { detail: data?.detail ?? null, overallScore: data?.overallScore ?? null, @@ -46,5 +57,6 @@ export function useJobDetail(id: string) { loading, error, refresh, + publishReview, }; } diff --git a/src/hooks/useLegalMembership.ts b/src/hooks/useLegalMembership.ts new file mode 100644 index 0000000..daf5dda --- /dev/null +++ b/src/hooks/useLegalMembership.ts @@ -0,0 +1,92 @@ +/** + * 法律服务会员态 hook:拉取当前会员态与付费订单,并提供 + * 创建订单 / 模拟支付 的能力。mock 阶段数据持久化在 AsyncStorage。 + */ +import { useCallback, useEffect, useState } from 'react'; +import { + createLegalOrder, + fetchLegalMembership, + fetchLegalOrders, + payLegalOrder, +} from '../api'; +import type { LegalMembership, LegalOrder, LegalPaymentChannel } from '../types'; + +export interface LegalMembershipState { + /** 当前会员态,未开通为 null */ + membership: LegalMembership | null; + /** 付费订单列表(倒序) */ + orders: LegalOrder[]; + /** 首次加载中 */ + loading: boolean; + /** 错误对象 */ + error: Error | null; + /** 支付处理中(创建订单 + 模拟支付) */ + processing: boolean; + /** 刷新会员态与订单 */ + refresh: () => Promise; + /** 创建订单,返回新建的待支付订单 */ + createOrder: (planId: string, channel: LegalPaymentChannel) => Promise; + /** 支付订单,成功后同步会员态 */ + payOrder: (orderId: string) => Promise; +} + +export function useLegalMembership(): LegalMembershipState { + const [membership, setMembership] = useState(null); + const [orders, setOrders] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [processing, setProcessing] = useState(false); + + const refresh = useCallback(async () => { + try { + setLoading(true); + setError(null); + const [m, o] = await Promise.all([fetchLegalMembership(), fetchLegalOrders()]); + setMembership(m); + setOrders(o); + } catch (e) { + setError(e as Error); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + refresh(); + }, [refresh]); + + const createOrder = useCallback( + async (planId: string, channel: LegalPaymentChannel) => { + const order = await createLegalOrder(planId, channel); + setOrders((prev) => [order, ...prev]); + return order; + }, + [], + ); + + const payOrder = useCallback(async (orderId: string) => { + setProcessing(true); + try { + const order = await payLegalOrder(orderId); + setOrders((prev) => prev.map((o) => (o.id === orderId ? order : o))); + if (order.status === 'paid') { + const m = await fetchLegalMembership(); + setMembership(m); + } + return order; + } finally { + setProcessing(false); + } + }, []); + + return { + membership, + orders, + loading, + error, + processing, + refresh, + createOrder, + payOrder, + }; +} diff --git a/src/hooks/useLegalServiceRequests.ts b/src/hooks/useLegalServiceRequests.ts new file mode 100644 index 0000000..e6aae79 --- /dev/null +++ b/src/hooks/useLegalServiceRequests.ts @@ -0,0 +1,58 @@ +/** + * 会员服务使用申请 hook:拉取本人的服务申请记录,并提供提交申请的能力。 + * 权益兑现载体——已开通会员提交后由律师/顾问跟进。 + */ +import { useCallback, useEffect, useState } from 'react'; +import { createLegalServiceRequest, fetchLegalServiceRequests } from '../api'; +import type { LegalServiceRequest } from '../types'; + +export interface LegalServiceRequestsState { + /** 申请记录(倒序) */ + requests: LegalServiceRequest[]; + /** 首次加载中 */ + loading: boolean; + /** 错误对象 */ + error: Error | null; + /** 提交中 */ + submitting: boolean; + /** 刷新申请记录 */ + refresh: () => Promise; + /** 提交申请 */ + submit: (serviceId: string, content: string) => Promise; +} + +export function useLegalServiceRequests(): LegalServiceRequestsState { + const [requests, setRequests] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [submitting, setSubmitting] = useState(false); + + const refresh = useCallback(async () => { + try { + setLoading(true); + setError(null); + setRequests(await fetchLegalServiceRequests()); + } catch (e) { + setError(e as Error); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + refresh(); + }, [refresh]); + + const submit = useCallback(async (serviceId: string, content: string) => { + setSubmitting(true); + try { + const created = await createLegalServiceRequest(serviceId, content); + setRequests((prev) => [created, ...prev]); + return created; + } finally { + setSubmitting(false); + } + }, []); + + return { requests, loading, error, submitting, refresh, submit }; +} diff --git a/src/types/index.ts b/src/types/index.ts index d1fa7f1..494e4ec 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -192,6 +192,12 @@ export type { LegalService, LegalServiceType, LegalTemplate, + LegalPaymentChannel, + LegalOrderStatus, + LegalOrder, + LegalMembership, + LegalServiceRequest, + LegalServiceRequestStatus, } from './legal'; export type { AboutInfo, diff --git a/src/types/legal.ts b/src/types/legal.ts index 509cef0..4373d64 100644 --- a/src/types/legal.ts +++ b/src/types/legal.ts @@ -20,6 +20,8 @@ export interface LegalService { type: LegalServiceType; /** 跳转动作:chat=法律咨询对话 / templates=文书模板 / faq=常见问题 / upgrade=开通会员 */ action: 'chat' | 'templates' | 'faq' | 'upgrade'; + /** 会员服务使用页的引导提示语(仅 type=member 时配置) */ + requestHint?: string; } /** 法律常见问题 */ @@ -101,3 +103,62 @@ export interface LegalBrand { /** 品牌 Logo(mock 阶段用 emoji/图标占位) */ logo: string; } + +/* ===================== 付费 / 订单 / 会员态 ===================== */ + +/** 支付方式(mock 阶段仅作选择,真实支付由后端凭证决定) */ +export type LegalPaymentChannel = 'wechat' | 'alipay'; + +/** 订单状态:待支付 / 已支付 / 失败 */ +export type LegalOrderStatus = 'pending' | 'paid' | 'failed'; + +/** 会员订单 */ +export interface LegalOrder { + /** 订单 id */ + id: string; + /** 套餐 id */ + planId: string; + /** 套餐名称(下单时快照) */ + planName: string; + /** 实付金额(元) */ + price: number; + /** 支付方式 */ + channel: LegalPaymentChannel; + /** 订单状态 */ + status: LegalOrderStatus; + /** 创建时间 ISO */ + createdAt: string; + /** 支付时间 ISO(status=paid 时存在) */ + paidAt?: string; +} + +/** 会员态 */ +export interface LegalMembership { + /** 套餐 id */ + planId: string; + /** 套餐名称 */ + planName: string; + /** 开通时间 ISO */ + startedAt: string; + /** 到期时间 ISO */ + expiresAt: string; +} + +/** 会员服务申请状态:已提交 / 处理中 / 已完成 */ +export type LegalServiceRequestStatus = 'submitted' | 'processing' | 'done'; + +/** 会员服务使用申请(付费后权益兑现的载体) */ +export interface LegalServiceRequest { + /** 申请 id */ + id: string; + /** 服务 id */ + serviceId: string; + /** 服务名称(提交时快照) */ + serviceName: string; + /** 用户填写的需求描述 */ + content: string; + /** 申请状态 */ + status: LegalServiceRequestStatus; + /** 提交时间 ISO */ + createdAt: string; +} diff --git a/项目开发状态.md b/项目开发状态.md index 6de4824..ef8f9c9 100644 --- a/项目开发状态.md +++ b/项目开发状态.md @@ -75,7 +75,8 @@ ### 4.2 功能完整性缺口(标注【待确认】为推断项,需与产品对齐) - **通关小游戏(7 关)**:框架页 `game.tsx` + `game/level.tsx` 已建,7 关(认识自己 → 能力 → 岗位 → 企业 → 简历 → 面试 → 签约)**内容与数据已完整**:`MOCK_GAME_LEVELS` 共 22 题(self 4 / 其余各 3),题型含单选/多选/输入,配套解析与 `tags` 用于岗位推荐。评分(≥60 通关)、解锁、进度聚合逻辑均在 `submitGameLevel` 实现。✅ 2026-09-10 完成内容增厚(每关由 1–2 题补至 3 题左右)。 -- **阿布阿诺 AI 法律服务**:免费层(咨询 / 模板 / FAQ)页已建;付费层 `legal/upgrade.tsx` 已建,但付费 / 支付链路 **【待确认】是否接通**。 +- **阿布阿诺 AI 法律服务**:免费层(咨询 / 模板 / FAQ)页已建;付费层 `legal/upgrade.tsx` 已建,**付费 / 支付链路已接通(mock 支付闭环)**——`legal/checkout.tsx` 选套餐→选支付方式→模拟支付→会员态激活并 AsyncStorage 持久化;`legal.tsx` / `upgrade.tsx` 套餐卡可点击进入结算,会员态在返回时即时刷新。真实支付 SDK 后续仅换 `api.ts` 的 `createLegalOrder` / `payLegalOrder` 函数体。**付费后权益已兑现**:新增 `legal/service.tsx` 会员服务使用页——未开通点会员服务仍引导开通,已开通则进入使用页(服务说明 + 引导语 + 需求输入框 + 提交申请 + 申请记录持久化),`legal.tsx` 会员卡标签按态显示「会员 / 已开通」。 +- **岗位评价发布(双向透明评价生态核心)**:✅ 2026-09-10 接通。此前 `jobs/[id].tsx` 底部发布按钮为「功能开发中」占位——产品核心定位即评价体系却无法发布。现已新增 `createJobReview`(AsyncStorage 键 `job:reviews`,按 jobId 归属),`fetchEmployeeReviews` 合并「本人发布(置顶)+ 内置评价」,`useJobDetail` 暴露 `publishReview`,页面接线含空输入拦截、发布中 loading、成功后清空输入框并自动刷新列表。 - **隐私政策 / 用户协议内容**:`src/api/api.ts` 中 `MOCK_PRIVACY_POLICY` / `MOCK_USER_AGREEMENT` 已含完整合规草案(各 9–10 章),**18 岁门槛已落地**(隐私政策第六章 + 用户协议前言)。仅余【待确认】占位待法务/产品替换:公司全称、注册地址、客服邮箱/电话、个人信息保护负责人邮箱、管辖法院。属**上架硬性审查项**,占位填妥即可提交审核。 ### 4.3 工程与上线 @@ -90,7 +91,7 @@ ## 五、后续增量开发建议(优先级) - **P0(上架前必做)**:图标替换【待你提供 1024×1024 logo】 · ~~隐私 / 协议内容填充~~ **隐私/协议草案【已存在,含 18 岁门槛,待填【待确认】占位】** · ~~EAS 初始化~~ **EAS 配置骨架【已完成,待账号注入占位】** · ~~18 岁门槛核对~~ **18 岁门槛【已在隐私政策第六章 + 用户协议前言落地】** · ~~微信登录接入评估~~ **微信登录接入评估【已完成 → 见 `微信登录接入方案.md`,SDK 接入阻塞于微信开放平台 AppID】**。 -- **P1(核心差异化)**:~~微信登录接入方案~~ **微信登录接入方案【已落文档,真实 SDK 接入阻塞于微信开放平台移动应用申请】** · ~~企业端快捷入口话术统一~~ **企业端快捷入口话术统一【已完成,6 处 label 对齐规范词;含沟通页快捷操作 onPress 接线,tsc/eslint 0 问题】** · 付费法律服务支付 · ~~通关小游戏 7 关内容~~ **通关小游戏 7 关内容【已完成:结构原已建成,本次增厚至 7 关 22 题,tsc/eslint 0 问题】**。 +- **P1(核心差异化)**:~~微信登录接入方案~~ **微信登录接入方案【已落文档,真实 SDK 接入阻塞于微信开放平台移动应用申请】** · ~~企业端快捷入口话术统一~~ **企业端快捷入口话术统一【已完成,6 处 label 对齐规范词;含沟通页快捷操作 onPress 接线,tsc/eslint 0 问题】** · ~~付费法律服务支付~~ **付费法律服务支付【已完成:mock 支付闭环,选套餐→结算→模拟支付→会员激活持久化,tsc/eslint 0 问题】** · ~~通关小游戏 7 关内容~~ **通关小游戏 7 关内容【已完成:结构原已建成,本次增厚至 7 关 22 题,tsc/eslint 0 问题】**。 - **P2(打磨)**:~~技术债清理~~ **技术债已清理(8 处 `any` → 明确类型,`eslint` 0 warning)** · 真实后端对接 · OTA 通道验证。 ---