diff --git a/app.config.js b/app.config.js index 95e8dd9..86697c1 100644 --- a/app.config.js +++ b/app.config.js @@ -10,9 +10,9 @@ */ export default { expo: { - name: '有职', - slug: 'youzhi', - scheme: 'youzhi', + name: '轻松聘', + slug: 'qingsongpin', + scheme: 'qingsongpin', version: '1.0.0', orientation: 'portrait', icon: './assets/icon.png', @@ -34,11 +34,11 @@ export default { ios: { supportsTablet: true, // iOS Bundle Identifier:上架 App Store 的唯一标识,按你的开发者账号命名。 - bundleIdentifier: 'com.youzhi.app', + bundleIdentifier: 'com.qingsongpin.app', }, android: { // Android Package:上架 Google Play 的唯一标识,通常与 iOS 保持一致。 - package: 'com.youzhi.app', + package: 'com.qingsongpin.app', adaptiveIcon: { backgroundColor: '#E6F4FE', foregroundImage: './assets/android-icon-foreground.png', @@ -57,8 +57,8 @@ export default { // 本项目只选静态图、不录视频:不申请麦克风权限(最小必要原则), // 插件默认会加 RECORD_AUDIO,这里显式关掉 microphonePermission: false, - photosPermission: '有职需要访问相册,用于上传营业执照与设置头像', - cameraPermission: '有职需要使用相机,用于拍摄营业执照与设置头像', + photosPermission: '轻松聘需要访问相册,用于上传营业执照与设置头像', + cameraPermission: '轻松聘需要使用相机,用于拍摄营业执照与设置头像', }, ], // 启动屏:冷启动期间展示品牌 logo,消除白屏。 diff --git a/app/(tabs)/chat.tsx b/app/(tabs)/chat.tsx index 92b34cb..295f40f 100644 --- a/app/(tabs)/chat.tsx +++ b/app/(tabs)/chat.tsx @@ -93,7 +93,7 @@ function interviewActionTarget(interview: Interview): { url: string; label: stri return interview.location ? // 高德 URI API:callnative=1 优先唤起高德 App,未安装则落网页版 { - url: `https://uri.amap.com/search?keyword=${encodeURIComponent(interview.location)}&src=youzhi&callnative=1`, + url: `https://uri.amap.com/search?keyword=${encodeURIComponent(interview.location)}&src=qingsongpin&callnative=1`, label: '导航前往', } : null; diff --git a/app/guide.tsx b/app/guide.tsx index 2f5c4eb..05df129 100644 --- a/app/guide.tsx +++ b/app/guide.tsx @@ -1,6 +1,7 @@ import { Ionicons } from '@expo/vector-icons'; import { LinearGradient } from 'expo-linear-gradient'; import { useRouter } from 'expo-router'; +import { StatusBar } from 'expo-status-bar'; import { useRef, useState } from 'react'; import { Pressable, @@ -11,7 +12,7 @@ import { View, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; -import { colors, headerGradientShort } from '../src/theme'; +import { colors, onboardingGradient } from '../src/theme'; import { useAuth } from '../src/context/AuthContext'; import type { AssetName } from '../components/onboarding/assets'; import { ImageSlot } from '../components/onboarding/ui/ImageSlot'; @@ -24,29 +25,30 @@ interface GuideSlide { } /** - * 引导页内容:聚焦有职四大差异化能力。 - * 每屏的配图走 ImageSlot —— 素材未到位时页面上会显示「配图待补 + 该放什么图」的标注。 + * 引导页三屏。标题是产品定的文案,副标题只写**已上线**的模块 + * (AI 简历优化 / AI 模拟面试 / AI 合同咨询、霍兰德测评、成长页文章与闯关), + * 不写还没做的能力。 + * + * 整页用品牌主色渐变打底(见 src/theme/colors.ts 的 onboardingGradient), + * 所以文字统一用 textInverse 反白,按钮反过来用白底 + primaryText 蓝字。 + * + * 每屏配图走 ImageSlot —— 素材未到位时页面上会显示「配图待补 + 该放什么图」的标注。 */ const SLIDES: GuideSlide[] = [ { - art: 'guideEvaluate', - title: '先看评价,再投简历', - desc: '公司评分、在职员工评价、行业声誉都摆在明面上,信息差不该由求职者买单。', + art: 'guideAiCopilot', + title: '求职开挂 AI护驾', + desc: 'AI 简历优化、AI 模拟面试、AI 合同咨询,从投递到签约都有人接话。', }, { - art: 'guideWeightDim', - title: '按你的标准打分', - desc: '关注什么就调什么权重,实时算出综合得分,找到真正合适你的那家公司。', + art: 'guideAbilityTest', + title: '真实能力 一测便知', + desc: '霍兰德兴趣测评 + 能力自评,先看清自己的底牌,再决定投什么岗。', }, { - art: 'guideGameMap', - title: '用 7 关走完求职全流程', - desc: '认识自己 → 明确方向 → 准备简历 → 面试 → 签约,边闯关边把该准备的都准备好。', - }, - { - art: 'guideLegalAI', - title: '签字之前,先看清权益', - desc: '免费的 AI 咨询与合同模板,付费的律师与合同审核,入职前后都有人帮你把关。', + art: 'guideGrowthUnlock', + title: '一起解锁 职场秘籍', + desc: '成长页的职场文章与闯关任务,把该准备的提前准备好。', }, ]; @@ -54,8 +56,11 @@ const SLIDES: GuideSlide[] = [ * 引导页(/guide)。 * * 两种进入方式,出口不同 —— 出口错了会把已登录用户甩回注册流程: - * - 首次启动:`/` 判定没看过引导时跳来,看完落 /register(跳过等同看完); + * - 首次启动:`/` 判定没看过引导时跳来,看完落 /register; * - 已登录后回顾:设置页「查看引导页」推入,出口是返回上一页,不能再去 /register。 + * + * 底部按钮前两屏是「下一步」(翻页),最后一屏才是「开启体验」(离开引导页)。 + * 三屏都直接离开的话,后两屏等于摆设。 */ export default function GuideScreen() { const router = useRouter(); @@ -92,67 +97,67 @@ export default function GuideScreen() { const slotHeight = Math.round(slotWidth * 0.72); return ( - - - {reviewing ? ( - - - - ) : ( - - )} - - {reviewing ? '关闭' : '跳过'} - - - - { - const index = Math.round(e.nativeEvent.contentOffset.x / width); - setPage(index); - }} - style={styles.pager} - > - {SLIDES.map((slide) => ( - - - {slide.title} - {slide.desc} - - ))} - + + {/* 整页深蓝底,状态栏要走浅色图标,否则黑字压蓝底看不清 */} + + + + {reviewing ? ( + + + + ) : ( + + )} + - - - {SLIDES.map((slide, i) => ( - + { + const index = Math.round(e.nativeEvent.contentOffset.x / width); + setPage(index); + }} + style={styles.pager} + > + {SLIDES.map((slide) => ( + + + {slide.title} + {slide.desc} + ))} - - - + + + + + {SLIDES.map((slide, i) => ( + + ))} + + - {isLast ? (reviewing ? '完成' : '立即体验') : '下一步'} + {isLast ? (reviewing ? '完成' : '开启体验') : '下一步'} - - - - {page + 1} / {SLIDES.length} - - - + + + + ); } const styles = StyleSheet.create({ - container: { flex: 1, backgroundColor: colors.white }, + container: { flex: 1 }, + safe: { flex: 1 }, topBar: { flexDirection: 'row', alignItems: 'center', - justifyContent: 'space-between', paddingHorizontal: 16, paddingVertical: 10, }, @@ -162,31 +167,31 @@ const styles = StyleSheet.create({ alignItems: 'center', justifyContent: 'center', }, - skipText: { fontSize: 14, color: colors.textSecondary, paddingHorizontal: 4 }, pager: { flex: 1 }, slide: { flex: 1, alignItems: 'center', justifyContent: 'center', - paddingHorizontal: 36, + paddingHorizontal: 32, }, slideTitle: { - fontSize: 22, + fontSize: 26, fontWeight: '800', - color: colors.textPrimary, + color: colors.textInverse, textAlign: 'center', - marginTop: 36, - marginBottom: 14, + letterSpacing: 1, + marginTop: 32, + marginBottom: 12, }, slideDesc: { fontSize: 15, - color: colors.textSecondary, - textAlign: 'center', lineHeight: 24, + color: colors.textInverse, + textAlign: 'center', }, bottom: { alignItems: 'center', - paddingHorizontal: 36, + paddingHorizontal: 16, paddingBottom: 28, }, dots: { flexDirection: 'row', gap: 8, marginBottom: 24 }, @@ -194,24 +199,16 @@ const styles = StyleSheet.create({ width: 8, height: 8, borderRadius: 4, - backgroundColor: colors.border, + backgroundColor: colors.onBrandTrack, }, - dotActive: { width: 22, backgroundColor: colors.primary }, + dotActive: { width: 22, backgroundColor: colors.textInverse }, nextBtn: { width: '100%', height: 50, borderRadius: 25, - overflow: 'hidden', - }, - nextGradient: { - flex: 1, + backgroundColor: colors.white, alignItems: 'center', justifyContent: 'center', }, - nextText: { fontSize: 16, fontWeight: '700', color: colors.textInverse }, - pageHint: { - marginTop: 16, - fontSize: 13, - color: colors.textHint, - }, + nextText: { fontSize: 16, fontWeight: '700', color: colors.primaryText }, }); diff --git a/components/onboarding/assets.ts b/components/onboarding/assets.ts index 89928d3..4fa4e26 100644 --- a/components/onboarding/assets.ts +++ b/components/onboarding/assets.ts @@ -26,11 +26,10 @@ export type AssetName = | 'avatarPreset3' | 'avatarPreset4' | 'avatarPreset5' - // 引导页(/guide)四屏插图:每屏一张,补图只改这几行的 source - | 'guideEvaluate' - | 'guideWeightDim' - | 'guideGameMap' - | 'guideLegalAI'; + // 引导页(/guide)三屏插图:每屏一张,补图只改这几行的 source + | 'guideAiCopilot' + | 'guideAbilityTest' + | 'guideGrowthUnlock'; type IconName = ComponentProps['name']; @@ -46,7 +45,7 @@ export interface AssetDef { export const ASSET_REGISTRY: Record = { // RN 静态图片仅支持 require 引用,豁免 no-require-imports 规则 // eslint-disable-next-line @typescript-eslint/no-require-imports - logo: { source: require('../../assets/brand-logo.jpg'), tint: colors.primary, icon: 'briefcase', hint: '有职 Logo' }, + logo: { source: require('../../assets/brand-logo.jpg'), tint: colors.primary, icon: 'briefcase', hint: '轻松聘 Logo' }, illustCandidate: { source: null, tint: colors.primaryLight, icon: 'document-text', hint: '手持 OFFER 职场男性插画' }, illustRecruiter: { source: null, tint: colors.primaryLight, icon: 'person', hint: '手持 RESUME 职场男性插画' }, illustCelebrate: { source: null, tint: colors.successLight, icon: 'ribbon', hint: '庆祝插画' }, @@ -61,33 +60,28 @@ export const ASSET_REGISTRY: Record = { avatarPreset4: { source: null, tint: colors.actionLight, icon: 'man', hint: '预设头像 4' }, avatarPreset5: { source: null, tint: colors.dangerLight, icon: 'person-circle', hint: '预设头像 5' }, /* - * 引导页四屏插图。hint 会**显示在页面上**(ImageSlot 的占位标注), + * 引导页三屏插图。hint 会**显示在页面上**(ImageSlot 的占位标注), * 所以这里写的是「给谁看的、画什么」,补图后标注自动消失。 - * 建议尺寸 640×480(4:3),深色底以外的浅色背景,与页面白底衔接。 + * 建议尺寸 640×480(4:3);引导页底色是深蓝渐变,插画本身请用浅色底, + * 否则浅色占位块换成深色插画后会和背景糊在一起。 */ - guideEvaluate: { + guideAiCopilot: { source: null, tint: colors.primaryLight, - icon: 'star', - hint: '手机屏幕里展示公司评分与员工评价列表', + icon: 'sparkles', + hint: 'AI 助手对话界面:简历优化建议与模拟面试提问', }, - guideWeightDim: { + guideAbilityTest: { source: null, tint: colors.infoLight, - icon: 'options', - hint: '拖动滑块调节评价维度权重,实时算出综合得分', + icon: 'analytics', + hint: '能力测评雷达图与测评得分报告', }, - guideGameMap: { - source: null, - tint: colors.successLight, - icon: 'map', - hint: '闯关地图:7 个关卡节点,从「认识自己」走到「签约」', - }, - guideLegalAI: { + guideGrowthUnlock: { source: null, tint: colors.actionLight, - icon: 'document-text', - hint: 'AI 法律助手与劳动合同文书', + icon: 'map', + hint: '职场成长地图:文章与闯关任务节点', }, }; diff --git a/components/onboarding/screens/B1Wechat.tsx b/components/onboarding/screens/B1Wechat.tsx index b6a44cf..18d529b 100644 --- a/components/onboarding/screens/B1Wechat.tsx +++ b/components/onboarding/screens/B1Wechat.tsx @@ -17,7 +17,7 @@ export function B1Wechat() { 微信用户_8f3a - 将使用以下信息登录有职 + 将使用以下信息登录轻松聘 · 获得你的昵称、头像 diff --git a/components/onboarding/screens/B2Agreement.tsx b/components/onboarding/screens/B2Agreement.tsx index 04b938f..8b30072 100644 --- a/components/onboarding/screens/B2Agreement.tsx +++ b/components/onboarding/screens/B2Agreement.tsx @@ -4,13 +4,13 @@ import { useOnboarding } from '../../../src/hooks'; import { ScreenShell } from '../ui/ScreenShell'; const PARAGRAPHS = [ - '一、总则。欢迎使用有职求职招聘平台。在使用本服务前,请您务必仔细阅读并透彻理解本《用户协议》与《隐私政策》的全部内容。', - '二、账号注册。您确认,在您开始注册程序使用有职服务前,您应当具备中华人民共和国法律规定的与您行为相适应的民事行为能力。', + '一、总则。欢迎使用轻松聘求职招聘平台。在使用本服务前,请您务必仔细阅读并透彻理解本《用户协议》与《隐私政策》的全部内容。', + '二、账号注册。您确认,在您开始注册程序使用轻松聘服务前,您应当具备中华人民共和国法律规定的与您行为相适应的民事行为能力。', '三、信息收集。为向您提供职位推荐、企业认证等服务,我们可能收集您的手机号、简历信息、位置信息等。我们承诺严格依照《隐私政策》使用并保护上述信息。', '四、未成年人保护。本平台服务主要面向年满 18 周岁的用户。如您未满 18 周岁,请在监护人陪同下使用,并确保已取得监护人同意。', - '五、用户行为规范。您承诺不利用有职平台制作、发布、传播任何违反法律法规或公序良俗的信息,不从事任何干扰平台正常运营的行为。', - '六、责任限制。在法律允许的最大范围内,有职对您因使用或无法使用本服务而导致的任何间接、偶然或后果性损害不承担责任。', - '七、协议变更。有职保留在必要时修改本条款的权利,修改后的条款将在平台公示后生效。', + '五、用户行为规范。您承诺不利用轻松聘平台制作、发布、传播任何违反法律法规或公序良俗的信息,不从事任何干扰平台正常运营的行为。', + '六、责任限制。在法律允许的最大范围内,轻松聘对您因使用或无法使用本服务而导致的任何间接、偶然或后果性损害不承担责任。', + '七、协议变更。轻松聘保留在必要时修改本条款的权利,修改后的条款将在平台公示后生效。', ]; export function B2Agreement() { @@ -19,7 +19,7 @@ export function B2Agreement() { return ( - 有职用户协议与隐私政策 + 轻松聘用户协议与隐私政策 {PARAGRAPHS.map((p, i) => ( {p} diff --git a/components/onboarding/screens/P1Login.tsx b/components/onboarding/screens/P1Login.tsx index faae574..cf0f05b 100644 --- a/components/onboarding/screens/P1Login.tsx +++ b/components/onboarding/screens/P1Login.tsx @@ -52,10 +52,10 @@ export function P1Login() { text={ 已阅读并同意 - navigate('B2')}>《有职用户协议》 + navigate('B2')}>《轻松聘用户协议》 和 navigate('B2')}>《隐私政策》 - ,允许有职统一登录本账号信息 + ,允许轻松聘统一登录本账号信息 } /> diff --git a/components/onboarding/screens/P2PasswordLogin.tsx b/components/onboarding/screens/P2PasswordLogin.tsx index d4ca13d..829f465 100644 --- a/components/onboarding/screens/P2PasswordLogin.tsx +++ b/components/onboarding/screens/P2PasswordLogin.tsx @@ -56,7 +56,7 @@ export function P2PasswordLogin() { return ( 密码登录 - 使用手机号与登录密码进入有职 + 使用手机号与登录密码进入轻松聘 {/* 手机号 */} @@ -102,10 +102,10 @@ export function P2PasswordLogin() { text={ 已阅读并同意 - navigate('B2')}>《有职用户协议》 + navigate('B2')}>《轻松聘用户协议》 和 navigate('B2')}>《隐私政策》 - ,允许有职统一管理本人账号信息 + ,允许轻松聘统一管理本人账号信息 } /> diff --git a/components/onboarding/screens/P2PhoneCode.tsx b/components/onboarding/screens/P2PhoneCode.tsx index 37574c4..6ad3b8c 100644 --- a/components/onboarding/screens/P2PhoneCode.tsx +++ b/components/onboarding/screens/P2PhoneCode.tsx @@ -99,7 +99,7 @@ export function P2PhoneCode() { /* ---------- 阶段1:手机号 ---------- */ 登录/注册 - 首次验证通过即注册有职账号 + 首次验证通过即注册轻松聘账号 +86 @@ -139,10 +139,10 @@ export function P2PhoneCode() { text={ 已阅读并同意 - navigate('B2')}>《有职用户协议》 + navigate('B2')}>《轻松聘用户协议》 和 navigate('B2')}>《隐私政策》 - ,允许有职统一管理本人账号信息 + ,允许轻松聘统一管理本人账号信息 } /> @@ -198,10 +198,10 @@ export function P2PhoneCode() { text={ 已阅读并同意 - navigate('B2')}>《有职用户协议》 + navigate('B2')}>《轻松聘用户协议》 和 navigate('B2')}>《隐私政策》 - ,允许有职统一管理本人账号信息 + ,允许轻松聘统一管理本人账号信息 } /> diff --git a/components/onboarding/screens/P2QuickLogin.tsx b/components/onboarding/screens/P2QuickLogin.tsx index 92e2dbc..6b77559 100644 --- a/components/onboarding/screens/P2QuickLogin.tsx +++ b/components/onboarding/screens/P2QuickLogin.tsx @@ -73,7 +73,7 @@ export function P2QuickLogin() { )} - {failed ? '可重新获取,或改用验证码登录' : '首次验证通过即注册有职账号'} + {failed ? '可重新获取,或改用验证码登录' : '首次验证通过即注册轻松聘账号'} @@ -116,10 +116,10 @@ export function P2QuickLogin() { 《{carrierName}认证服务条款》 、 - navigate('B2')}>《有职用户协议》 + navigate('B2')}>《轻松聘用户协议》 和 navigate('B2')}>《隐私政策》 - ,允许有职统一管理本人账号信息 + ,允许轻松聘统一管理本人账号信息 } /> diff --git a/components/onboarding/ui/AgreementModal.tsx b/components/onboarding/ui/AgreementModal.tsx index 5c6916f..d7f9aa3 100644 --- a/components/onboarding/ui/AgreementModal.tsx +++ b/components/onboarding/ui/AgreementModal.tsx @@ -18,10 +18,10 @@ export function AgreementModal({ visible, onAccept, onDecline, onOpenAgreement } 请阅读并同意 - 《有职用户协议》 + 《轻松聘用户协议》 和 《隐私政策》 - ,允许有职统一管理本人账号信息 + ,允许轻松聘统一管理本人账号信息 diff --git a/components/onboarding/ui/BrandHeader.tsx b/components/onboarding/ui/BrandHeader.tsx index 6a73087..caba9d0 100644 --- a/components/onboarding/ui/BrandHeader.tsx +++ b/components/onboarding/ui/BrandHeader.tsx @@ -7,7 +7,7 @@ export function BrandHeader({ slogan }: { slogan?: string }) { return ( - 有职 + 轻松聘 {slogan ? {slogan} : null} ); diff --git a/screenshots/README.md b/screenshots/README.md index 64036e1..61ad876 100644 --- a/screenshots/README.md +++ b/screenshots/README.md @@ -1,6 +1,6 @@ -# 有职 · App Store / Google Play 截图流水线 +# 轻松聘 · App Store / Google Play 截图流水线 -本目录为「有职」App 的商店素材(App Store Connect + Google Play Console)准备区,遵循 +本目录为「轻松聘」App 的商店素材(App Store Connect + Google Play Console)准备区,遵循 [Expo App Store Screenshots](https://www.npmjs.com/package/expo-app-store-screenshots) 技能的目录与命名约定。 ## 目录结构(约定) diff --git a/screenshots/capture-manifest.json b/screenshots/capture-manifest.json index 4e27178..49677ad 100644 --- a/screenshots/capture-manifest.json +++ b/screenshots/capture-manifest.json @@ -1,6 +1,6 @@ { - "project": "有职 (youzhi)", - "scheme": "youzhi", + "project": "轻松聘 (qingsongpin)", + "scheme": "qingsongpin", "locale": "zh-CN", "devices": ["iphone", "ipad", "android-phone"], "targetSizes": { diff --git a/src/__tests__/job-match.test.ts b/src/__tests__/job-match.test.ts index c110199..9bcd3a7 100644 --- a/src/__tests__/job-match.test.ts +++ b/src/__tests__/job-match.test.ts @@ -9,7 +9,7 @@ import { storageRemove, storageSet } from '../lib/storage'; import type { Job, JobPreference, OnboardingProfile } from '../types'; // 内存版 storage mock(与 salary.test.ts / onboarding-profile.test.ts 同)。 -// 键是 api 层传入的原始键(无 youzhi: 前缀,前缀由真实 storage 封装补上)。 +// 键是 api 层传入的原始键(无 qingsongpin: 前缀,前缀由真实 storage 封装补上)。 jest.mock('../lib/storage', () => { const store: Record = {}; return { diff --git a/src/__tests__/share-text.test.ts b/src/__tests__/share-text.test.ts index 8b3f145..6c3b2ac 100644 --- a/src/__tests__/share-text.test.ts +++ b/src/__tests__/share-text.test.ts @@ -51,7 +51,7 @@ import type { } from '../types'; /** 所有分享文本都以这句收尾 —— 对外可见,单独锁一条 */ -const SIGNATURE = '—— 来自「有职」'; +const SIGNATURE = '—— 来自「轻松聘」'; const textOf = (text: string) => text.split('\n'); const hasLineStartingWith = (text: string, prefix: string) => diff --git a/src/api/api.ts b/src/api/api.ts index 09ac869..43ebd78 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -674,7 +674,7 @@ const MOCK_ARTICLES: Article[] = [ views: 23000, thumbnailColor: 'violet', icon: 'document-text-outline', - content: '简历往往是你与 HR 之间唯一的第一印象,而对方平均只花 6-10 秒扫读一份简历。核心原则只有三条:一页纸、结果导向、量化成就。\n\n先用「岗位关键词」反向定制:把 JD 里的高频能力词自然融入项目描述,让 ATS 筛选系统更容易命中。再套用 STAR 法则(情境-任务-行动-结果)拆解每段经历,例如「主导 X 项目,通过 Y 动作,带来 Z% 的留存提升」明显比「负责 X 项目」更有说服力。\n\n最后做减法:删掉与岗位无关的兴趣社团,把空间留给能证明你胜任的硬证据。有职「简历诊断」会基于行业评分模型给出逐项改进建议,投递前先自检一遍,能少走很多弯路。', + content: '简历往往是你与 HR 之间唯一的第一印象,而对方平均只花 6-10 秒扫读一份简历。核心原则只有三条:一页纸、结果导向、量化成就。\n\n先用「岗位关键词」反向定制:把 JD 里的高频能力词自然融入项目描述,让 ATS 筛选系统更容易命中。再套用 STAR 法则(情境-任务-行动-结果)拆解每段经历,例如「主导 X 项目,通过 Y 动作,带来 Z% 的留存提升」明显比「负责 X 项目」更有说服力。\n\n最后做减法:删掉与岗位无关的兴趣社团,把空间留给能证明你胜任的硬证据。轻松聘「简历诊断」会基于行业评分模型给出逐项改进建议,投递前先自检一遍,能少走很多弯路。', }, { id: 'art-2', @@ -683,7 +683,7 @@ const MOCK_ARTICLES: Article[] = [ views: 18000, thumbnailColor: 'orange', icon: 'help-circle-outline', - content: '面试不是考试,而是一次双向匹配。最容易翻车的 10 个问题集中在:自我介绍、离职原因、期望薪资、职业规划、自身缺点、压力情境、对公司的了解、对前东家的评价、到岗时间,以及最后的「你还有什么想问的」。\n\n高频雷区有三个:把离职原因说成吐槽前公司、在薪资上先亮底牌、用「我没什么缺点」来回避成长话题。正确姿势是用「事实加反思」替代情绪,把每个问题都变成展示你方法论的机会。\n\n最后一题「你有什么想问的」最容易被忽略,其实它是你反向评估公司是否靠谱的关键窗口——问团队节奏、问岗位成功标准、问业务下一步,比问福利更有价值。有职 AI 模拟面试可针对你的目标岗位做专项陪练。', + content: '面试不是考试,而是一次双向匹配。最容易翻车的 10 个问题集中在:自我介绍、离职原因、期望薪资、职业规划、自身缺点、压力情境、对公司的了解、对前东家的评价、到岗时间,以及最后的「你还有什么想问的」。\n\n高频雷区有三个:把离职原因说成吐槽前公司、在薪资上先亮底牌、用「我没什么缺点」来回避成长话题。正确姿势是用「事实加反思」替代情绪,把每个问题都变成展示你方法论的机会。\n\n最后一题「你有什么想问的」最容易被忽略,其实它是你反向评估公司是否靠谱的关键窗口——问团队节奏、问岗位成功标准、问业务下一步,比问福利更有价值。轻松聘 AI 模拟面试可针对你的目标岗位做专项陪练。', }, { id: 'art-3', @@ -692,7 +692,7 @@ const MOCK_ARTICLES: Article[] = [ views: 35000, thumbnailColor: 'green', icon: 'stats-chart-outline', - content: '薪资报告的价值不在数字本身,而在帮你建立谈薪的「锚点」与「区间」。2024 年互联网行业整体回归理性:中厂与垂直领域岗位增长稳定,纯补贴型高薪收缩,复合能力(业务加技术加沟通)溢价明显。\n\n看报告要学会三层拆解:一是按城市与年限看基线,二是按岗位序列看带宽(P25、P50、P75),三是评估自己的「可替代性」——越稀缺的能力,越能突破带宽上限。\n\n谈薪时不要只报一个数,给一个区间并说明依据(市场分位加你的独特价值),更容易落到上限。有职薪资计算器已内置城市与序列系数,可一键估算你的合理区间。', + content: '薪资报告的价值不在数字本身,而在帮你建立谈薪的「锚点」与「区间」。2024 年互联网行业整体回归理性:中厂与垂直领域岗位增长稳定,纯补贴型高薪收缩,复合能力(业务加技术加沟通)溢价明显。\n\n看报告要学会三层拆解:一是按城市与年限看基线,二是按岗位序列看带宽(P25、P50、P75),三是评估自己的「可替代性」——越稀缺的能力,越能突破带宽上限。\n\n谈薪时不要只报一个数,给一个区间并说明依据(市场分位加你的独特价值),更容易落到上限。轻松聘薪资计算器已内置城市与序列系数,可一键估算你的合理区间。', }, ]; @@ -1898,7 +1898,7 @@ export function buildJobShareText( lines.push(reviewCount > 0 ? `员工评价 ${reviewCount} 条` : '暂无员工评价'); lines.push(''); - lines.push('—— 来自「有职」'); + lines.push('—— 来自「轻松聘」'); return lines.join('\n'); } @@ -2068,7 +2068,7 @@ export function buildOfferCompareText(data: OfferCompareData): string { lines.push(aiSuggestion.detail); lines.push(''); - lines.push('本报告由「有职」App 根据当前对比页数据生成,仅供参考。'); + lines.push('本报告由「轻松聘」App 根据当前对比页数据生成,仅供参考。'); return lines.join('\n'); } @@ -2296,7 +2296,7 @@ export function buildRankReportText(overview: RankOverview): string { lines.push(''); } - lines.push('—— 来自「有职」'); + lines.push('—— 来自「轻松聘」'); return lines.join('\n'); } @@ -2620,7 +2620,7 @@ export function computeSalaryResult(input: SalaryCalcInput): SalaryCalcResult { } /* ===================== 薪资计算记录(本地持久化) ===================== - * mock 阶段以 storage(youzhi: 前缀)为唯一真相源读写,不另建模块级内存缓存; + * mock 阶段以 storage(qingsongpin: 前缀)为唯一真相源读写,不另建模块级内存缓存; * 真实后端就绪后把三个函数体替换为 fetch 即可,签名不变。 */ @@ -3143,8 +3143,8 @@ function buildResumeFromProfile(profile: OnboardingProfile, preference: JobPrefe * @method GET * @url /api/resume * @returns Promise 简历数据 - * @remark 内容 = 引导采集资料派生(`youzhi:onboarding:profile` + 有效求职偏好)叠加用户编辑 - * 覆盖层(`youzhi:resume:override`):某板块被编辑过就以覆盖层为准,未编辑过用派生值。 + * @remark 内容 = 引导采集资料派生(`qingsongpin:onboarding:profile` + 有效求职偏好)叠加用户编辑 + * 覆盖层(`qingsongpin:resume:override`):某板块被编辑过就以覆盖层为准,未编辑过用派生值。 * 只有完全没走过注册流程(无资料)时才回落到演示简历 */ export async function fetchResume(): Promise { @@ -3365,7 +3365,7 @@ export async function optimizeResumeWithJD(_jd: string): Promise 导出设置 - * @remark 以 storage(`youzhi:resume:export-settings`)为唯一真相源;老数据缺后加的字段时 + * @remark 以 storage(`qingsongpin:resume:export-settings`)为唯一真相源;老数据缺后加的字段时 * 用默认值补齐(不能整份替换,否则新字段会变成 undefined),存储损坏回落默认值 */ export async function fetchResumeExportSettings(): Promise { @@ -3767,7 +3767,7 @@ export function buildInterviewConfigText( if (typeLabels.length > 0) lines.push(`题型:${typeLabels.join('、')}`); if (specialLabels.length > 0) lines.push(`专项突破:${specialLabels.join('、')}`); lines.push(''); - lines.push('—— 来自「有职」'); + lines.push('—— 来自「轻松聘」'); return lines.join('\n'); } @@ -4710,7 +4710,7 @@ export function buildHollandReportText( lines.push(''); } - lines.push('—— 来自「有职」'); + lines.push('—— 来自「轻松聘」'); return lines.join('\n'); } @@ -6995,7 +6995,7 @@ export function buildPrivacySummaryText(center: PrivacyCenter): string { lines.push(''); }); - lines.push('—— 来自「有职」'); + lines.push('—— 来自「轻松聘」'); return lines.join('\n'); } @@ -7346,7 +7346,7 @@ const MOCK_PRIVACY_POLICY: LegalDocument = { updatedAt: '2026-08-31', effectiveAt: '2026-08-31', preamble: - '有职(以下简称「我们」)深知个人信息对你的重要性,并会尽全力保护你的个人信息安全可靠。我们致力于维持你对我们的信任,恪守以下原则保护你的个人信息:权责一致原则、目的明确原则、选择同意原则、最少够用原则、确保安全原则、主体参与原则、公开透明原则。本政策适用于你通过有职平台(含移动客户端、网站及关联服务)所进行的全部活动。', + '轻松聘(以下简称「我们」)深知个人信息对你的重要性,并会尽全力保护你的个人信息安全可靠。我们致力于维持你对我们的信任,恪守以下原则保护你的个人信息:权责一致原则、目的明确原则、选择同意原则、最少够用原则、确保安全原则、主体参与原则、公开透明原则。本政策适用于你通过轻松聘平台(含移动客户端、网站及关联服务)所进行的全部活动。', sections: [ { heading: '一、我们收集的信息', @@ -7431,12 +7431,12 @@ const MOCK_USER_AGREEMENT: LegalDocument = { updatedAt: '2026-08-31', effectiveAt: '2026-08-31', preamble: - '本协议是你与有职平台运营方【待确认:公司全称】(以下简称「平台」)之间就使用有职平台服务所订立的协议。在使用平台各项服务前,请你务必审慎阅读、充分理解本协议各条款内容,特别是以粗体或下划线标注的免责条款、责任限制条款与争议解决条款。如你未满 18 周岁,请在法定监护人的陪同下阅读并决定是否同意本协议。', + '本协议是你与轻松聘平台运营方【待确认:公司全称】(以下简称「平台」)之间就使用轻松聘平台服务所订立的协议。在使用平台各项服务前,请你务必审慎阅读、充分理解本协议各条款内容,特别是以粗体或下划线标注的免责条款、责任限制条款与争议解决条款。如你未满 18 周岁,请在法定监护人的陪同下阅读并决定是否同意本协议。', sections: [ { heading: '一、协议的范围与接受', paragraphs: [ - '本协议适用于你使用有职平台提供的全部服务,包括但不限于职位浏览与搜索、简历创建与投递、在线沟通、能力测评、竞争力排名、AI 模拟面试、求职进度管理以及企业端的职位发布、人才搜索与招聘管理功能。', + '本协议适用于你使用轻松聘平台提供的全部服务,包括但不限于职位浏览与搜索、简历创建与投递、在线沟通、能力测评、竞争力排名、AI 模拟面试、求职进度管理以及企业端的职位发布、人才搜索与招聘管理功能。', '你通过点击「同意」「注册」或以其他任何方式实际使用平台服务,即视为你已阅读并同意受本协议约束。若你不同意本协议,请立即停止使用平台服务。', '平台有权在必要时修订本协议并在平台上公布。修订后的协议自公布之日起生效。若你继续使用平台服务,即视为你已接受修订后的协议。', ], @@ -7777,11 +7777,11 @@ export async function fetchAccountSecurity(): Promise { /** 关于我们信息 */ const MOCK_ABOUT_INFO: AboutInfo = { - appName: '有职', + appName: '轻松聘', version: 'v1.0.0', company: '阿布阿诺人工智能科技有限公司', description: - '有职是面向全社会青年求职者的双向透明求职生态平台,提供公司调研、真实评价、行业声誉与岗位评价分级,帮助用户找到更适合自己的工作。', + '轻松聘是面向全社会青年求职者的双向透明求职生态平台,提供公司调研、真实评价、行业声誉与岗位评价分级,帮助用户找到更适合自己的工作。', links: [ // 前两条原先没有 route,导致在「关于我们」页点下去毫无反应(静默失效)。 // 但这两个页面**早已存在**(app/settings/privacy-policy.tsx / user-agreement.tsx), diff --git a/src/hooks/useResume.ts b/src/hooks/useResume.ts index 83b3073..317a1c1 100644 --- a/src/hooks/useResume.ts +++ b/src/hooks/useResume.ts @@ -1,7 +1,7 @@ /** * 简历数据 hook:取简历 + 按板块保存 + AI 动作。 * - * 写操作一律「落库 → 重新取数」,不做本地乐观更新:`youzhi:resume:override` 是唯一真相源, + * 写操作一律「落库 → 重新取数」,不做本地乐观更新:`qingsongpin:resume:override` 是唯一真相源, * 页面拿到的永远是落库后的结果(与 `useSalaryRecords` 同一套约定)。 */ import { useCallback, useState } from 'react'; diff --git a/src/lib/storage.ts b/src/lib/storage.ts index 0c5dcc7..13a387a 100644 --- a/src/lib/storage.ts +++ b/src/lib/storage.ts @@ -4,10 +4,10 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; * 统一封装 @react-native-async-storage/async-storage, * 业务侧只调用 get/set/remove,不感知底层实现; * 所有读写包 try/catch,存储不可用(如 web 未链接原生模块)时静默降级, - * 不影响内存态功能。键统一加 'youzhi:' 前缀避免与其他 App 冲突。 + * 不影响内存态功能。键统一加 'qingsongpin:' 前缀避免与其他 App 冲突。 */ -const KEY_PREFIX = 'youzhi:'; +const KEY_PREFIX = 'qingsongpin:'; /** 读取字符串值,缺失/异常时返回 null */ export async function storageGet(key: string): Promise { diff --git a/src/theme/colors.ts b/src/theme/colors.ts index cf68323..6481f43 100644 --- a/src/theme/colors.ts +++ b/src/theme/colors.ts @@ -306,3 +306,18 @@ export const headerGradientShort = ['#2C83FD', '#3FA7E8', '#4FD1C5'] as const; * 头部含个人资料卡片(头像/昵称/简介),白字需始终落在深色区,末端不加入白色 */ export const headerGradientSolid = ['#2C83FD', '#3FA7E8', '#4FD1C5'] as const; + +/** + * 引导页满屏底色 · 主色家族由浅到深(primary → primaryBtn → primaryDark)。 + * + * 为什么不复用上面三支:它们都收在「蓝 → 青」的青端(#4FD1C5), + * 白字落在青端只有约 1.9:1,连大字号的 3:1 都不到 —— 那三支能用, + * 是因为白字只压在**顶部**深色区(头部标题、我的页资料卡)。 + * 引导页是**整页底色**,正文压在中下部,所以整条渐变必须始终深于白字阈值: + * · primary #2C83FD 白字 3.6:1 —— 只放标题(26pt/800,大字只需 3:1) + * · primaryBtn#1F6FDB 白字 4.8:1 —— 正文起点(普通字号需 4.5:1,刚过 AA) + * · primaryDark#175AB0 白字 6.4:1 —— 正文落点,留出余量 + * 配合页面里的 locations=[0, 0.34, 1],让 4.8:1 那一档提前到 34% 处, + * 正文(约 55%~70% 高度)实际落在 5.2:1 以上。 + */ +export const onboardingGradient = [colors.primary, colors.primaryBtn, colors.primaryDark] as const; diff --git a/src/theme/index.ts b/src/theme/index.ts index 118e6c8..ff44c78 100644 --- a/src/theme/index.ts +++ b/src/theme/index.ts @@ -7,6 +7,7 @@ export { headerGradient, headerGradientShort, headerGradientSolid, + onboardingGradient, templateAccentColors, semanticColorTokens, jobLevelTextColors, diff --git a/src/types/tools.ts b/src/types/tools.ts index a3167c0..4e7aaed 100644 --- a/src/types/tools.ts +++ b/src/types/tools.ts @@ -1,6 +1,6 @@ /** * 工具类页面领域模型(行业技能词云 / 薪资计算器 / 求职进度 / 选择身份 / 入职准备清单)。 - * 与有职设计稿中「行业技能词云」「薪资计算器」「求职进度」「选择身份」「入职准备清单」五屏对应。 + * 与轻松聘设计稿中「行业技能词云」「薪资计算器」「求职进度」「选择身份」「入职准备清单」五屏对应。 */ /** 入职准备清单分类 */ diff --git a/微信登录接入方案.md b/微信登录接入方案.md index 0534f04..a981f6d 100644 --- a/微信登录接入方案.md +++ b/微信登录接入方案.md @@ -7,7 +7,7 @@ > 本文以下内容描述的是**代码现状与未来接入路线**,不代表当前有可用入口。 > 状态:方案文档 · 不涉及代码改写 · 真实后端就绪后按路线图替换 mock 即可 -> 适用范围:有职 App 微信授权登录(三分支状态机) +> 适用范围:轻松聘 App 微信授权登录(三分支状态机) > 关联文件:`src/lib/wechat-sdk.ts`、`src/hooks/useWechatLogin.ts`、`src/api/api.ts`、`src/types/wechat-auth.ts`、`components/onboarding/screens/P1Login.tsx`、`components/onboarding/screens/P2PhoneCode.tsx` --- @@ -74,7 +74,7 @@ ### Step 1 · 微信开放平台申请移动应用(前置,阻塞项) - 登录微信开放平台 → 创建**移动应用**,拿到 `AppID` / `AppSecret`。 - **iOS**:登记 **Universal Links**(微信授权回调硬性要求)。需准备一个 HTTPS 域名 + 后端托管 `apple-app-site-association`。 -- **Android**:登记 **应用包名**(`com.youzhi.app`,见 `app.config.js`)与**应用签名**(发布/调试签名 SHA256)。 +- **Android**:登记 **应用包名**(`com.qingsongpin.app`,见 `app.config.js`)与**应用签名**(发布/调试签名 SHA256)。 - ⚠️ 此步产生的 `AppID`、`Universal Links` 域名需回填到代码与 EAS 配置(见 Step 3/4)。 ### Step 2 · 安装微信 SDK(Expo 插件优先) diff --git a/运营商一键登录接入方案.md b/运营商一键登录接入方案.md index 723588d..b066327 100644 --- a/运营商一键登录接入方案.md +++ b/运营商一键登录接入方案.md @@ -1,7 +1,7 @@ # 运营商一键登录接入方案(P2Q) > 状态:方案文档 · 不涉及代码改写 · 真实取号 SDK 就绪后按路线图替换 mock 即可 -> 适用范围:有职 App 运营商一键登录(本机号码认证) +> 适用范围:轻松聘 App 运营商一键登录(本机号码认证) > 关联文件:`src/types/carrier-auth.ts`、`src/hooks/useCarrierPhone.ts`、`src/hooks/useLogin.ts`、`src/api/api.ts`、`components/onboarding/screens/P2QuickLogin.tsx`、`src/__tests__/carrier-login.test.ts` > 本文回答的问题:「一键登录时应当获取到手机号,为什么获取不到」—— 见第 1 节。 @@ -66,7 +66,7 @@ ### Step 1 · 开通运营商号码认证服务(前置,阻塞项) - 任选其一:中国移动 / 联通 / 电信官方号码认证能力,或**聚合方案**(阿里云号码认证服务、极光、闪验等,一次接多家运营商)。 -- 需要准备:企业资质、`android.package`(`com.youzhi.app`,见 `app.config.js`)与**签名 SHA256**、iOS `bundleIdentifier`。 +- 需要准备:企业资质、`android.package`(`com.qingsongpin.app`,见 `app.config.js`)与**签名 SHA256**、iOS `bundleIdentifier`。 - 拿到的 **AppKey / AppSecret** 需同时配置在**端上(SDK 初始化)**与**服务器(换取号码)**两侧。 ### Step 2 · 安装取号 SDK(Expo 插件优先) diff --git a/项目开发状态.md b/项目开发状态.md index bc7988f..c056d26 100644 --- a/项目开发状态.md +++ b/项目开发状态.md @@ -1,4 +1,4 @@ -# 有职 App 开发进度与规划文档 +# 轻松聘 App 开发进度与规划文档 > 生成日期:2026-09-10 > 最后更新:2026-09-20 · 第三批(**把同一类缺陷的另一半「同一张映射表 / 同一段业务规则在多个页面各写一份」收敛掉**,见 4.16。共 **14 处**本地重复声明压成 **6 张单一来源的表**:评级→文字色 **5 份 → 1**(growth / jobs / ability / assessment 四处同名同值,hr-evaluation 因数据层在那里下发的是**中文评级**而非 `JobLevel`、键的域不同而另立一份,故第六处是**派生**而非并列复写)、霍兰德六型色 3 → 1、语义色名→色值 3 → 1、文章缩略图 2 种写法(对象表 vs 两份平行 `Record`)→ 1、社保基数 clamp 3 → 1(**下游那份在接口层,才是业务规则该在的层**;页面内联的那份是因为它要用**新城市**的上下限而 helper 读的是旧城市,故提取时把 `city` 变成显式参数,特殊情况随之消失)、`formatRecordTime` 与登录页内联脱敏 → 复用 `formatDateTime` / `maskPhone`;另把企业端发布页自持的学历 / 工作类型选项表接回 `src/constants`。**取值一律等于现状、一像素未改**,属纯重构。新增 **26 例回归锁**并做**6 处注入验证**(6 例如期变红、逐字还原、grep 确认无残留)。**两条如实记录**:① 子代理报来的 `DIMENSION_LEVEL`「重复声明」经查**不成立**(全仓只有一处),其 `THUMBNAIL_*` 与 clamp 的行号描述亦不精确 —— 与「代理报告不是证据」这一贯口径一致,**每条都独立核对过**;② 查出 `profile.tsx` 的「良好」用 `textSecondary`、而另外五处用 `primary` 的**配色分歧,未擅自统一**,留给产品定夺) @@ -26,7 +26,7 @@ | 图标名审计 | `node scripts/check-icons.mjs` | ✅ **0 处无效图标名**(2026-09-18 新增门槛,修掉 `gavel-outline` 后建立,见 4.13) | | 单元测试 | `npm test` | ✅ **25 个文件 339 例全绿**(2026-09-20) | | 入口接线 | 读 `app/_layout.tsx` / `app/index.tsx` | ✅ 根栈 → 入口围栏 → 双端 Tab 完整 | -| 应用配置 | `app.config.js` | ✅ name「有职」、scheme `youzhi`、plugins 正确 | +| 应用配置 | `app.config.js` | ✅ name「轻松聘」、scheme `qingsongpin`、plugins 正确 | **判定**:项目处于可启动状态(`expo start` 即可运行),未发现阻塞运行的错误。本次未进入"修正"环节。 @@ -101,7 +101,7 @@ **问题**:整条引导链在空转 —— `components/onboarding/screens/` 下 20+ 个引导屏**没有一处调用 API**,用户从 P2 登录到 P22 完成填的姓名 / 城市 / 期望职位 / 期望薪资 / 技能 / 学历 / 公司**全部只写进 `useOnboardingFlow` 的内存 state**(该 Provider 只包在 `/register` 路由内),进程一结束全部蒸发。主界面因此读不到用户填的任何东西:「我的」页偏好**恒为** mock 的「前端开发工程师 / 北京 / 25-35K」。 -- **落库闭环**:新增持久化模型 `src/types/onboarding.ts`(`OnboardingProfile` 与含登录瞬时字段的 `OnboardingData` 拆开,`phone` / `code` / `agreed` / `wechatTicket` 不落库)+ API `saveOnboardingProfile` / `fetchOnboardingProfile` / `clearOnboardingProfile`(键 `youzhi:onboarding:profile`)。写入为**步骤粒度**(`navigate` / `back` / `finish` 各提交一次,不逐键写,避免每次按键都打存储);Provider 挂载时水合,流程仍从 P1 开始(按决定不做断点续填)。 +- **落库闭环**:新增持久化模型 `src/types/onboarding.ts`(`OnboardingProfile` 与含登录瞬时字段的 `OnboardingData` 拆开,`phone` / `code` / `agreed` / `wechatTicket` 不落库)+ API `saveOnboardingProfile` / `fetchOnboardingProfile` / `clearOnboardingProfile`(键 `qingsongpin:onboarding:profile`)。写入为**步骤粒度**(`navigate` / `back` / `finish` 各提交一次,不逐键写,避免每次按键都打存储);Provider 挂载时水合,流程仍从 P1 开始(按决定不做断点续填)。 - **主界面读取(让填的生效)**:`fetchJobPreference` 改为读持久化资料并映射(工作类型默认「全职」;未采集项显示 `UNSET_LABEL`「未设置」,不再返回 `MOCK_JOB_PREFERENCE` 常量);`fetchJobList` 在**默认推荐视图**(`sortBy==='comprehensive'` 且未显式筛选城市 / 关键词)叠加偏好加分(城市 +15 / 职位 +20 / 薪资区间重叠 +10,权重在 `src/constants`),**显式筛选一律不叠加偏好加分**;`fetchUserProfile` 的姓名 / 头像字 / 简介改用采集值(有资料就只用资料,**不再用 mock 的「李明 / 3年工作经验·本科·北京」补空**,空值由页面渲染「未设置」或隐藏该行)。`app/(tabs)/jobs.tsx` 默认城市由写死的「北京」改为「全部城市」—— 不改这一处,偏好加分永远不生效。 - **简历页接采集资料**:`fetchResume` 原先只覆盖姓名与头像,其余(学历「北京大学」、工作经历「字节跳动」、技能 Vue/React)仍是演示人设「李明」的 —— 用户会在自己的简历里看到别人的学历和公司。现改为 `buildResumeFromProfile` 逐项映射采集值:教育背景(P20 学校 + P18 学历/学习形式)、工作经历(P12 公司 + P11 职位 + P13 入职年月 + P16 工作内容,在职才写「至今」,离职时结束时间留空而不编)、技能(P14)、求职意向;**未采集项一律留空**(专业、入学毕业年份、项目经历、自我评价、手机号、邮箱),页面按空值隐藏对应行,不再拼出「· 本科」「2022.07 - 」这类半截内容,空板块渲染「未填写」。P9 的求职状态选项提为常量 `JOB_STATUS_OPTIONS`(`JOB_STATUS_INACTIVE` / `JOB_STATUS_EMPLOYED_PREFIX` 同处),简历「积极 / 不积极」与「是否至今」的判定与页面读同一份,避免两处文案分叉。 - **求职意向只认一个答案**:新增 `resolveJobPreference()`(显式编辑 > 引导资料推导 > 空值),「我的」页偏好卡片与简历「求职意向」都读它 —— 原先简历的求职意向只读引导资料,用户在求职意向编辑页改过之后,两个页面会给出不同答案。顺带把「基本信息的所在城市」留空:引导采集的是 P4「**期望**城市」,不能冒充「现居城市」(期望城市仍正确显示在求职意向里)。 @@ -124,11 +124,11 @@ **问题**:引导链闭环之后,简历成了新的瓶颈 —— `app/resume.tsx` 与 `app/resume/export.tsx` 共 **26 处** `Pressable` 点下去没有任何反应(顶部「保存」「导出」、「编辑基本信息」、四个「添加*」、「AI 优化 / AI 润色 / 一键全部优化」、模板 / 纸型 / 方向 / 预览 / AI 智能优化 / 分享简历),且**简历根本没有可写通道** —— `fetchResume()` 每次从引导资料现算派生,storage 里没有任何简历键。根因不是「忘了绑回调」,而是无处可写,所以先建数据层。处理口径:**实现功能,不是删按钮**。 -- **覆盖层数据层**(新增类型 `ResumeOverride` / `ResumeItemInput` / `ResumeAIActionResult` / `ResumeExportOutcome`):简历按「**用户显式编辑过的板块 > 引导资料派生**」取值,键 `youzhi:resume:override`;**键出现(含 `[]`、`''`)即归用户,键不存在才用引导派生** —— 删光教育经历不会再冒出引导那条(`undefined` 被 `JSON.stringify` 丢弃,正好用 `??` 而非 `||` 区分「未编辑」与「清空」)。整表保存而非逐条写:逐条写会以空数组为基底,把引导派生的整块改丢。登出时一并清除该键(`clearResumeOverride`),避免换账号后看到上一个人的简历。 +- **覆盖层数据层**(新增类型 `ResumeOverride` / `ResumeItemInput` / `ResumeAIActionResult` / `ResumeExportOutcome`):简历按「**用户显式编辑过的板块 > 引导资料派生**」取值,键 `qingsongpin:resume:override`;**键出现(含 `[]`、`''`)即归用户,键不存在才用引导派生** —— 删光教育经历不会再冒出引导那条(`undefined` 被 `JSON.stringify` 丢弃,正好用 `??` 而非 `||` 区分「未编辑」与「清空」)。整表保存而非逐条写:逐条写会以空数组为基底,把引导派生的整块改丢。登出时一并清除该键(`clearResumeOverride`),避免换账号后看到上一个人的简历。 - **7 个可写接口 + 4 个新页面**:`saveResumeBasicInfo / saveResumeSelfEvaluation / saveResumeSkills / saveResumeEducation / saveResumeExperience / saveResumeProjects / saveResumeSnapshot`;页面 `/resume/basic`、`/resume/education`、`/resume/experience`、`/resume/project`(编辑)、`/resume/preview`(只读预览)。**多字段板块走独立编辑页,技能与自我评价走页面内弹窗**(按字段量分开处理:弹窗装不下教育/工作/项目的字段量,编辑页对单字段又太重)。新增条目**不带 `id` 字段**(不是给空串 —— 空串不是 nullish,会被当成「已有 id」写成空),id 由 API 层补,同毫秒连加两条也不会撞号。 -- **头部「保存」补充功能**:把当前简历 6 个可编辑板块整体写入覆盖层 + 保存时间戳(`youzhi:resume:saved-at`),页面显示「上次保存:MM-DD HH:mm」(从未保存则不显示该行)。 +- **头部「保存」补充功能**:把当前简历 6 个可编辑板块整体写入覆盖层 + 保存时间戳(`qingsongpin:resume:saved-at`),页面显示「上次保存:MM-DD HH:mm」(从未保存则不显示该行)。 - **AI 类按钮:保留入口 + 坦诚提示**(`optimizeResumeExperience` / `polishResumeSelfEvaluation` / `optimizeResumeWithJD`):mock 恒返回 `available:false` + 一句「AI 改写需要后端模型支持,当前版本暂未开放;你的简历内容不会被改动」,页面只提示、不改简历。**绝不在正文未改写时把 `aiOptimized` 置 true** —— 那会让列表显示「已优化」徽标,用户等于被自己的界面骗了。参数加 `_` 前缀,去掉下划线即表示能力已接入。 -- **导出设置真正保存**:`saveResumeExportSettings` 原先是 `Object.assign(DEFAULT_EXPORT_SETTINGS, …)` —— 只改模块内常量,杀进程即丢,「保存」按钮实则保存不了,还会污染其它调用方读到的默认值。现改落 `youzhi:resume:export-settings`(读时用默认值补齐后加字段,存储损坏回落默认)。默认文件名原为演示人设「李明_前端工程师简历」,现为空串,由 `resolveExportFileName` 兜底成「姓名_期望职位简历」。模板 / 纸型 / 方向的选择弹层**就地在页内改设置、随「保存」落盘**(跳新页会出现「模板已存、格式没存」)。 +- **导出设置真正保存**:`saveResumeExportSettings` 原先是 `Object.assign(DEFAULT_EXPORT_SETTINGS, …)` —— 只改模块内常量,杀进程即丢,「保存」按钮实则保存不了,还会污染其它调用方读到的默认值。现改落 `qingsongpin:resume:export-settings`(读时用默认值补齐后加字段,存储损坏回落默认)。默认文件名原为演示人设「李明_前端工程师简历」,现为空串,由 `resolveExportFileName` 兜底成「姓名_期望职位简历」。模板 / 纸型 / 方向的选择弹层**就地在页内改设置、随「保存」落盘**(跳新页会出现「模板已存、格式没存」)。 - **导出与分享真做**(新增依赖 `expo-print` / `expo-sharing` / `expo-file-system`):「立即导出」原先返回写死的 `https://cdn.example.com/resume/xxx.pdf` —— 不产出文件、页面还丢弃返回值、不给任何提示,是纯假成功。现改为**本机真出 PDF**:`buildResumeExportHtml`(纯函数,空板块整段略去、用户文本全转义、模板 id 白名单校验)→ `printToFileAsync` 按纸型 / 方向的真实页宽页高打印 → 重命名成用户设的文件名(打印引擎生成的是随机临时名,不改的话分享面板里是一串乱码);「分享简历」拉起系统面板分享这份文件。**封面页、自荐信、隐藏联系方式、纸型 / 方向四个开关由此第一次真正生效**(此前全是装饰):自荐信以自我评价为正文,**自我评价为空时不生成**(没有素材就编一封自荐信属于伪造内容)。Word / 图片格式与导出密码需后端导出服务,在**选中的那一刻**与导出成功后都写明。 - **未做的验证(诚实口径)**:真机 smoke test(生成 PDF 并拉起分享面板)**尚未执行**;Expo Go 是否内置 `expo-print` / `expo-sharing` 本轮无法联网核实(Expo 文档被网络策略拦截)。已完成的替代验证:`expo-doctor` 20/21 通过(依赖版本与 SDK 57 要求一致)、`expo export --platform android` 打包通过(新模块 import 可解析)、两个原生封装失败时一律返回 `{ ok: false, message }` 并弹出原因,**不返回假链接**。若 Expo Go 确缺模块,症状是**启动即崩**(`requireNativeModule` 在 import 期执行,非静默失败),届时改用 development build 出包即可,代码无需改。 - **审计工具常驻**:新增 `scripts/check-affordances.mjs`(扫「无处理函数 / 空处理函数」的 `Pressable`,支持 `affordance-ok` 豁免标记),与 `check-routes.mjs` 配对,加 `npm run check:affordances` / `check:routes` / `typecheck` 三个 script(暂不进 CI 硬门槛:其余 22 处未修完,`--error` 会一直红)。全仓死控件由 **37 → 22**,**简历域 0 处**(**22 → 0 于 2026-09-17 完成,见 4.8**)。**「不进硬门槛」的理由已于 2026-09-18 消失**(假交互同批清零,脚本并补上了弹窗式假交互的检测口径),`check:affordances` 已改为带 `--error` 的硬门槛,见 4.9。 @@ -137,13 +137,13 @@ ### 4.5 工程与上线 -- **应用图标**:✅ 已完成(提交 `878710c`)。`assets/icon.png` 已替换为「有职」logo,实测 1024×1024,符合商店要求。 +- **应用图标**:✅ 已完成(提交 `878710c`)。`assets/icon.png` 已替换为「轻松聘」logo,实测 1024×1024,符合商店要求。 - **启动屏**:✅ 已接线。安装 `expo-splash-screen@57.0.9`,在 `app.config.js` 以 config plugin 形式配置 `assets/splash-icon.png`(1024×1024、`imageWidth: 200`、`resizeMode: 'contain'`、白底)。此前该资源为死资源且冷启动白屏。因应用为纯亮色(`userInterfaceStyle: 'light'`),未配置 `dark` 分支。**注意**:启动屏属原生配置,Expo Go 内不生效,需 `eas build` 出包后可见。 - **EAS / OTA**:`eas.json` 配置骨架(development / preview / production + submit)已建;`app.config.js` 的 `runtimeVersion` / `ios.bundleIdentifier` / `android.package` / `extra.eas.projectId` 字段已就位。**以下两项占位符会直接阻塞 `eas build` / `eas submit`,上架前必做**: 1. `owner: 'REPLACE_WITH_EAS_OWNER'` —— 需替换为你的 Expo 账号名; 2. `extra.eas.projectId: 'REPLACE_WITH_PROJECT_ID'` —— 需替换为真实 EAS 项目 ID。 - 两者均可由 `eas login` 后执行 `eas init` 自动注入,**不要手工填**。`ios.bundleIdentifier` / `android.package` 当前为 `com.youzhi.app`,可按开发者账号调整。 + 两者均可由 `eas login` 后执行 `eas init` 自动注入,**不要手工填**。`ios.bundleIdentifier` / `android.package` 当前为 `com.qingsongpin.app`,可按开发者账号调整。 - **`updates`(OTA)块当前为注释状态,属有意为之**:见提交 `aa7b171`。占位的 `REPLACE_WITH_PROJECT_ID` 会让 Expo Go(开发期)启动时去拉取不存在的远程更新,报 "Failed to download remote update" 并蓝屏。待 `eas init` 注入真实 projectId 后再取消注释启用 OTA。 - **真实后端**:全量 mock(`src/api/api.ts` 7385 行),未切换真实 `fetch`。 - **技术债**:8 处 `any` 软警告已全部清理(图标断言改为 `keyof typeof Ionicons.glyphMap`、路由断言改为 `as string`),`eslint .` 现 0 error 0 warning,`tsc --noEmit` 保持 0 错误。 @@ -184,7 +184,7 @@ - **内容与配图重做**:四屏文案全部重写(聚焦差异化能力),配图改走新增组件 `ImageSlot`。 - **配图位带文字标注**(按 2026-09-16 要求「要配图的地方页面留有文字标注放什么图」):素材未到位时页面上画虚线占位框 + 「配图待补」+ **该放什么图**的说明(取自 `assets.ts` 的 `hint`),补图后标注自动消失。新增 4 个引导页素材位 `guideEvaluate` / `guideWeightDim` / `guideGameMap` / `guideLegalAI`,**换真图只改 `assets.ts` 里对应一行的 `source`**。与 `Illustration` 的分工:那个是小图标位(头像 / Logo),占位只画色块;`ImageSlot` 是内容大图位,占位必须自解释。 - **待补素材**:`assets.ts` 注册 17 个素材位,其中 **16 个 `source` 仍为 `null`**(含引导页 4 张,另一张 `logo` 已是真图)。 -- **「首次启动没出现」的核查结论**:`app/index.tsx`(本轮未改动)在 hydration 完成后按 `onboardingSeen` 放行 —— 没看过即 `Redirect /guide`,默认值 `false`(`AuthContext` 的 `memoryAuth`),存储里没有该键时不会误判为已看过。因此**跳过引导只可能来自「本机已看过一次」**(看完即 `completeOnboarding()` 写入 `youzhi:auth`),属设计行为;要复现真·首次启动需清应用数据或卸载重装。**该结论为代码走查所得,本机未复现(无设备可测)。** +- **「首次启动没出现」的核查结论**:`app/index.tsx`(本轮未改动)在 hydration 完成后按 `onboardingSeen` 放行 —— 没看过即 `Redirect /guide`,默认值 `false`(`AuthContext` 的 `memoryAuth`),存储里没有该键时不会误判为已看过。因此**跳过引导只可能来自「本机已看过一次」**(看完即 `completeOnboarding()` 写入 `qingsongpin:auth`),属设计行为;要复现真·首次启动需清应用数据或卸载重装。**该结论为代码走查所得,本机未复现(无设备可测)。** - **顺带**:设置页「清除缓存」的体积改为运行时读取(`useAppCache` + `src/lib/cache.ts`),不再写死数字;页脚服务热线抽成 `SupportFooter`(号码为空则整块不渲染)。 ### 4.8 全仓死控件清零(2026-09-17) @@ -195,7 +195,7 @@ 2. **人才推荐 `app/(enterprise)/talent-recommendation.tsx`**:顶部「搜索 + 漏斗」两枚图标 —— 漏斗无对应页面 → **摘掉**;搜索改成真关键词过滤(姓名 / 职位 / 技能标签 / 学校)。「调整筛选条件」原为弹「功能开发中」→ 改为真复位(切回全部 + 清关键词)。Tab 计数同样由候选人列表推导(`matchTalentTab`:匹配度 ≥90 / 985-211 院校名单 / 经验 ≥5 年 / 硕士及以上)。「查看人才市场报告」仍无对应页面 → 保留入口 + 明确提示,不假装跳转。 3. **申诉举证 `app/appeal.tsx`**:原为空 `onPress` → 接真实选图(新增 `src/lib/imagePicker.ts` + `useImagePick`),可拍照 / 相册 / 移除缩略图,证据随 `submitAppeal` 一并提交。**PDF 类材料后端上传接口不存在,在页面上写明**,而不是给个点不动的按钮。 4. **候选人详情 `app/candidate-detail.tsx`**:「发起沟通」→ `/chat/[id]`(此前点不动)。 -5. **面试评分 `app/interview-score.tsx`**:「保存草稿」接真实本地暂存(键 `youzhi:interview:score-draft:`,**按面试 id 分开不串台**),再次进入显示「检测到未提交草稿」+ 恢复 / 放弃;提交成功后才清草稿。 +5. **面试评分 `app/interview-score.tsx`**:「保存草稿」接真实本地暂存(键 `qingsongpin:interview:score-draft:`,**按面试 id 分开不串台**),再次进入显示「检测到未提交草稿」+ 恢复 / 放弃;提交成功后才清草稿。 6. **入职准备清单 `app/onboarding.tsx`**:勾选状态**落库**(此前退出再进入勾选全丢,`completed` 恒为 3/18);新增自定义待办分组(**只有这一组可删**,模板条目删了下次进页面还会出现,接口层直接 reject);「导出 / 分享」由 `buildChecklistText` 生成纯文本经系统分享面板发出(`shareText`)。 7. **微信验证码页 `components/onboarding/screens/P2WechatCode.tsx`**:点验证码输入区未聚焦 → 加 `inputRef` 聚焦。 @@ -448,7 +448,7 @@ if (link.route) { router.push(link.route); return; } - **资质证照的真实内容**:三类证照的编号 / 发证机关 / 有效期 / 证照图**全部待法务与资质方提供**。页面与数据契约已就绪,材料到位后只改 `src/api/api.ts` 里 `MOCK_QUALIFICATIONS` 的字段(`number` / `issuer` / `validPeriod` / `imageUrl`),并把 `qualifications.test.ts` 中锁 `null` 的断言一并更新。 - **合规页脚**:见第 7 条,需产品定位置与形态。 -- 4.9 第 8 条列出的「仍未验证」项:~~企业端「重新上架」点击路径~~ **已于 4.11 在模拟器实测通过**;~~其余 5 处导出分享~~ **已于 4.12 全部实测通过**(另加 `jobs/[id]` 岗位分享,共 6 处);~~其余 6 处空态~~ **已于 4.13 用注入空数据实测通过**。~~`settings` 缓存读取失败态~~ **已于 4.13 结案 —— 结论是「不可达」而非「未验证」**。**至此 4.9 第 8 条全部收口**,仅剩一项本机无法验证的外部依赖项(生产包 `youzhi://` 深链,见 4.12 第 6 条)。 +- 4.9 第 8 条列出的「仍未验证」项:~~企业端「重新上架」点击路径~~ **已于 4.11 在模拟器实测通过**;~~其余 5 处导出分享~~ **已于 4.12 全部实测通过**(另加 `jobs/[id]` 岗位分享,共 6 处);~~其余 6 处空态~~ **已于 4.13 用注入空数据实测通过**。~~`settings` 缓存读取失败态~~ **已于 4.13 结案 —— 结论是「不可达」而非「未验证」**。**至此 4.9 第 8 条全部收口**,仅剩一项本机无法验证的外部依赖项(生产包 `qingsongpin://` 深链,见 4.12 第 6 条)。 --- @@ -507,7 +507,7 @@ const isActive = job.status === 'active'; #### 4. 两处如实记录 -- **模拟器上的草稿样本被我用掉了**:验证「上架」写入时把那张草稿变成了在招,**已落库**。UI 只能在 `active`↔`offline` 之间切,**回不到 `draft`**;且「设置 → 清除缓存」只删文件系统缓存目录(`src/lib/cache.ts`),**不动 AsyncStorage**,清不掉 `youzhi:enterprise:jobs-status`。要把草稿样本变回来只能清应用数据(`adb shell pm clear`,会一并清掉登录态)。属本机开发数据,不影响仓库。 +- **模拟器上的草稿样本被我用掉了**:验证「上架」写入时把那张草稿变成了在招,**已落库**。UI 只能在 `active`↔`offline` 之间切,**回不到 `draft`**;且「设置 → 清除缓存」只删文件系统缓存目录(`src/lib/cache.ts`),**不动 AsyncStorage**,清不掉 `qingsongpin:enterprise:jobs-status`。要把草稿样本变回来只能清应用数据(`adb shell pm clear`,会一并清掉登录态)。属本机开发数据,不影响仓库。 - **这不是又一个「文档说已清零」**:本缺陷是**代码走查没发现、复看截图才发现**的(与 4.10 第 6 条的标签折行同一类)。4.9 第 4 条当时只跑了 tsc/eslint/单测,这三道门槛都看不见「文案对不对」。 #### 5. 门槛复跑(2026-09-18 实测) @@ -581,7 +581,7 @@ const isActive = job.status === 'active'; #### 6. 深链不路由:原因已查明(**2026-09-18 更正,原解释是错的**) -验证过程中试过 `youzhi://jobs/1`、`youzhi://settings/privacy` 等深链,**均未跳转**,app 只是恢复了上次的路由。 +验证过程中试过 `qingsongpin://jobs/1`、`qingsongpin://settings/privacy` 等深链,**均未跳转**,app 只是恢复了上次的路由。 这件事本节最初写的是「当前跑的是 dev build,scheme 很可能由 **expo-dev-client** 的 launcher 接管,生产包行为未必相同」。 **这个解释是错的**,必须更正 —— 一个错的归因比「未知」更危险:它会让人去查 dev-client 配置,而那里根本没有问题。 @@ -597,18 +597,18 @@ const isActive = job.status === 'active'; **根因(一条命令直接证死)**: ```bash -$ adb shell am start -a android.intent.action.VIEW -d "youzhi://jobs/1" -Error: Activity not started, unable to resolve Intent { ... dat=youzhi://jobs/... } +$ adb shell am start -a android.intent.action.VIEW -d "qingsongpin://jobs/1" +Error: Activity not started, unable to resolve Intent { ... dat=qingsongpin://jobs/... } $ adb shell am start -a android.intent.action.VIEW -d "exp://127.0.0.1:8081/--/jobs/1" Starting: Intent { ... dat=exp://127.0.0.1:8081/... } # 正常启动 ``` -`unable to resolve Intent` 的含义是 **Android 在整台设备上找不到任何能处理 `youzhi://` 的应用** —— 不是「应用收到了但忽略了」。因为**没有任何已安装的原生包注册过这个 scheme**:`app.config.js` 里那句 `scheme: 'youzhi'` 要出**原生包**才会写进 AndroidManifest;Expo Go 只注册 `exp://`。 +`unable to resolve Intent` 的含义是 **Android 在整台设备上找不到任何能处理 `qingsongpin://` 的应用** —— 不是「应用收到了但忽略了」。因为**没有任何已安装的原生包注册过这个 scheme**:`app.config.js` 里那句 `scheme: 'qingsongpin'` 要出**原生包**才会写进 AndroidManifest;Expo Go 只注册 `exp://`。 **结论**:这是 **Expo Go 的已知限制,不是本项目的缺陷**。所以 —— -- 「`youzhi://` 在 Expo Go 里点不动」**永远测不出别的结果**,不必再试; +- 「`qingsongpin://` 在 Expo Go 里点不动」**永远测不出别的结果**,不必再试; - 生产包是否路由,**仍未验证**(本机没有原生包可测),但**成因已不再是谜**:只要出包后 AndroidManifest 里的 scheme 生效即可。**建议首次出包后专门验一次**,不要等上线才发现; - 潜在影响面如前述:微信登录回调、分享出去的内容被点开等,都依赖这个 scheme。 @@ -725,7 +725,7 @@ name: '劳动仲裁申请书', icon: 'gavel-outline' ← 原值 #### 6. 更正 4.12 第 6 条的错误归因 -详见 4.12 第 6 条(已就地更正):深链 `youzhi://` 不路由**不是** expo-dev-client 造成的 —— 本项目**根本没装 dev-client**,当前跑在 **Expo Go** 里(本机第三方包只有 `host.exp.exponent`),Expo Go 只注册 `exp://`,故 Android 报 `unable to resolve Intent`(**整机没有任何应用能处理该 scheme**)。属 Expo Go 已知限制,非项目缺陷。 +详见 4.12 第 6 条(已就地更正):深链 `qingsongpin://` 不路由**不是** expo-dev-client 造成的 —— 本项目**根本没装 dev-client**,当前跑在 **Expo Go** 里(本机第三方包只有 `host.exp.exponent`),Expo Go 只注册 `exp://`,故 Android 报 `unable to resolve Intent`(**整机没有任何应用能处理该 scheme**)。属 Expo Go 已知限制,非项目缺陷。 #### 7. 本轮本机副作用(如实记录) @@ -764,7 +764,7 @@ app/settings.tsx ← hook(useAppCache) ← api(fetchCacheSize) ← lib(readCa #### 9. 仍未验证(不要当成验过) -- **生产包的 `youzhi://` 深链**:本机无原生包,测不了(成因已查明,见 4.12 第 6 条)。**这是唯一一项真正的「未验证」。** +- **生产包的 `qingsongpin://` 深链**:本机无原生包,测不了(成因已查明,见 4.12 第 6 条)。**这是唯一一项真正的「未验证」。** - 4.9 第 8 条其余项目已全部关闭:企业端「重新上架」见 4.11,6 处导出分享见 4.12,6 处空态见本节第 3 条,`settings` 缓存失败态见本节第 8 条(结论是「不可达」而非「未验证」)。 #### 10. 第七处分享路径实测 + 5 个拼装函数补上测试锁 @@ -794,7 +794,7 @@ app/settings.tsx ← hook(useAppCache) ← api(fetchCacheSize) ← lib(readCa |---|---|---| | **空值守卫** | 无评分数据不输出该行(不输出 `null`/`0 分`);薪资月数为 0 不写「0薪」;真实薪资样本数 0 不展示;福利为空不留「福利:」空壳;评价数 0 说「暂无员工评价」而非「员工评价 0 条」 | 约 14 例 | | **取的是本地选择态** | 面试配置分享**只输出被选中的题型**,未选中的不得出现 | 6 例 | -| **对外可见的格式** | 首行格式、`—— 来自「有职」` 签名、不留悬空分隔符、空分组不产生连续空行 | 约 8 例 | +| **对外可见的格式** | 首行格式、`—— 来自「轻松聘」` 签名、不留悬空分隔符、空分组不产生连续空行 | 约 8 例 | 其中「只输出被选中的题型」这条,是把 **4.12 那个真机证伪实验(改选后再分享、面板预览随之变)固化成不依赖设备的断言** —— 今后谁把它改回「输出接口默认值」,不必再上模拟器,跑测试就会红。 @@ -807,7 +807,7 @@ app/settings.tsx ← hook(useAppCache) ← api(fetchCacheSize) ← lib(readCa 两次注入均从备份完整还原(**md5 逐字节一致**),复跑 28 例全绿。 -> 写测试时自己写错过一条断言,如实记一笔:本想断言「建议没有描述时不留悬空的 `——`」,写成对**整段文本** `not.toContain('——')` —— 而签名行 `—— 来自「有职」` 本身就含 `——`,必然失败。**是断言写错了,不是代码错了**;改为断言**那一行**等于 `· 补足项目数据`。留着这段是因为它说明一个常见坑:断言的**粒度**要落在被测行为上,对整段文本做否定断言很容易被无关内容打脸。 +> 写测试时自己写错过一条断言,如实记一笔:本想断言「建议没有描述时不留悬空的 `——`」,写成对**整段文本** `not.toContain('——')` —— 而签名行 `—— 来自「轻松聘」` 本身就含 `——`,必然失败。**是断言写错了,不是代码错了**;改为断言**那一行**等于 `· 补足项目数据`。留着这段是因为它说明一个常见坑:断言的**粒度**要落在被测行为上,对整段文本做否定断言很容易被无关内容打脸。 #### 11. 「数据层引用外部词表」的系统性排查(本轮顺带做完) @@ -1161,7 +1161,7 @@ EXIT=1,规则确实生效;随后还原,`grep -c __probe` = 0,`eslint .` - 颜色映射进 `src/theme/colors.ts` —— 该文件自己就写着「**色值是本文件独占的职责**」, 且已有 `templateAccentColors` 这个先例; - clamp 进 `src/api/api.ts` —— AGENTS.md 规定业务规则在接口层,而**权威的那一份本来就在那里**; -- 时间格式化进 `src/utils` —— 日期格式化是 utils 的既有职责(该文件已有 `formatDateTime`); +- 时间格式化进 `src/utils` —— 日期格式化是 utils 的既轻松聘责(该文件已有 `formatDateTime`); - 选项词表进 `src/constants` —— 4.14 定的单一来源。 #### 3. 一处必须**派生**而不是并列复写:`levelLabelTextColors` @@ -1684,9 +1684,98 @@ deep-link 只能到 P1。**临时**把初始步改成目标页截图,**看完 --- +### 4.20 改名「轻松聘」 + 引导页改三屏满屏主题色(2026-09-20 批次,接 4.19) + +**起因**:产品确定软件名从「有职」改为「轻松聘」;引导页从四屏改为三屏,标题由产品给定, +整页背景用页面主题色,底部一个「开启体验」按钮。 + +**1. 改名范围(`grep -o | wc -l` 实测,不是估计)** + +| 面 | 文件数 | 出现次数 | +|---|---|---| +| 代码 + 配置里的显示名「有职」→「轻松聘」 | 13 | 50 | +| 标识符 `youzhi` → `qingsongpin` | 17 | 17 | +| 文档(`项目开发状态.md` / 两份接入方案 / `screenshots/`) | 5 | 有职 12 / youzhi 19 | +| **合计** | **27 个文件**(含重叠) | 有职 62 / youzhi 36 | + +改法是**带断言的白名单脚本**(`/tmp/mdj/rename.js`):每个文件先写死「改之前应该出现几次」, +对不上就抛错、**一个文件都不写**。跑第一步就有用 —— `项目开发状态.md` 我数的是 18 处, +脚本报实际 19 处(有 2 处挤在同一行),拦住了一次目测不准的替换。 + +**2. 三处**不是纯文本、是**行为**变化,单独列出** +- **`app.config.js` 的 `slug` / `scheme` / `ios.bundleIdentifier` / `android.package`** + `youzhi` → `qingsongpin`(`com.youzhi.app` → `com.qingsongpin.app`)。 + **现在改无代价**(从未上架、`owner` / `projectId` 还是占位符);**一旦上架就改不动了**。 +- **`src/lib/storage.ts` 的 `KEY_PREFIX`:`'youzhi:'` → `'qingsongpin:'`**。 + 前缀变了 = 本机**已存的登录态 / 简历覆盖层 / 引导采集答案 / 草稿全部读不到**(不是清空,是再也找不到)。 + 本机只有开发数据、无真实用户,可接受;**但若线上已有用户数据,这一步必须写迁移,不能直接换前缀**。 + 副作用正好可用:改完前缀本机回到「首次启动」,`/` 直接落到引导页 —— 顺手完成了端到端验证(见第 5 条)。 +- **`screenshots/capture-manifest.json` 的 `scheme` 必须与 `app.config.js` 同步**,否则截图流水线的深链失效。已同步。 +- (另有 `app/(tabs)/chat.tsx` 高德 URL 的 `src=youzhi` → `src=qingsongpin`,仅来源标识,无功能影响。) + +**3. 引导页三屏重做(`app/guide.tsx`)** + +| 屏 | 标题(产品给定) | 副标题(我写的) | +|---|---|---| +| 1 | 求职开挂 AI护驾 | AI 简历优化、AI 模拟面试、AI 合同咨询,从投递到签约都有人接话。 | +| 2 | 真实能力 一测便知 | 霍兰德兴趣测评 + 能力自评,先看清自己的底牌,再决定投什么岗。 | +| 3 | 一起解锁 职场秘籍 | 成长页的职场文章与闯关任务,把该准备的提前准备好。 | + +- **副标题只写仓库里查得到的已上线模块**(`'AI简历优化'` / `'AI面试官真实场景演练'` / 法务页 + `'免费的 AI 咨询与合同模板'` / `app/holland` / `app/(tabs)/growth.tsx` 的 article 与 `app/game`), + 没有编造未上线能力 —— 现在写进去的每一句都能在代码里指到出处。 +- **底色没有复用现成的 `headerGradient*` 三支**,新增 token `onboardingGradient` + = `[primary, primaryBtn, primaryDark]`(#2C83FD → #1F6FDB → #175AB0)。理由写进了 `colors.ts`: + 现成三支末端是青色 `#4FD1C5`,白字压上去只有约 **1.9:1**,它们能用是因为白字只压在**顶部**深色区; + 引导页是**整页**底色、正文在中下部,整条渐变必须始终深于白字阈值。配 `locations={[0, 0.34, 1]}` + 把 4.8:1 那一档提前到 34% 处,正文实际落在 5.2:1 以上。 +- **按钮反过来做**:白底 + `primaryText`(#104686 白底 9.4:1 AAA)。没有用品牌蓝底白字 —— + `primary` 白字只有 3.6:1,`primaryBtn` 白字 4.8:1 也只为大字准备。 +- **状态栏加了 ``**:根布局是 `style="dark"`,整页深蓝底会让图标变成黑压黑。 + 旧引导页是白底所以没暴露;**任何深色头部页面都会中招**,属既有缺陷,本页先行修掉。 +- **按钮文案是我的判断,不是用户明说的**:用户只说「页面底部写上开启体验按钮」。 + 三屏都用「开启体验」的话后两屏等于摆设,故按**前两屏「下一步」、第三屏「开启体验」**处理; + 若要求三屏一致,改 `isLast` 那一个三元表达式即可。 +- 配图仍是三个素材位(`guideAiCopilot` / `guideAbilityTest` / `guideGrowthUnlock`), + `hint` 直接画在页面上(ImageSlot 的「配图待补 + 该放什么图」)。 + +**4. 需要补的图片(我生成不了,按「没有图片请标明」逐项列出)** +改名后**所有带字标的位图都过期了**,已逐张打开确认,不是推测: + +| 文件 | 现状(已打开看过) | 要换成 | +|---|---|---| +| `assets/icon.png` | 1024×1024,蓝底白字**「有职」**+上箭头 | 「轻松聘」同规格 1024×1024 应用图标 | +| `assets/brand-logo.jpg` | 同上字标,登录页 `BrandHeader` 与 `ASSET_REGISTRY.logo` 在用 | 「轻松聘」字标 | +| `assets/splash-icon.png` | 启动屏,冷启动第一眼 | 「轻松聘」字标(同规格) | +| `assets/android-icon-foreground.png` / `-background.png` / `-monochrome.png` | 自适应图标三分层 | 由新图标重新导出 | +| 引导页三张插画 | 仍是占位框 | 见 `components/onboarding/assets.ts` 里三条 `hint` | + +(注:`项目开发状态.md` 第五章 P0 里「应用图标已完成」那条指的是 `878710c` 做的**「有职」**图标, +本批改名后该结论已过期,待新图到位再更新。) + +**5. 验证(模拟器 `emulator-5554`,像素测量,非目测)** +- 端到端:`exp://127.0.0.1:8081/--/` 进入 → 因 `KEY_PREFIX` 改名本机回到首次启动 → **自动落到引导页** ✓ +- 底色渐变实测(`x=540`):y=70 `#2c82fc`(= primary)· y=1300 `#1c68cd` · y=2300 `#175ab1`(= primaryDark), + 与 `locations=[0,0.34,1]` 的理论值逐点吻合。 +- CTA 按钮:左边界 **x=42px = 16.0 dp**、距右 **16.0 dp**、纵向 **y=2132~2263 = 50.0 dp**, + 文字色实测 **#104686**(= `primaryText`)✓ +- 滑到第 3 屏:标题「一起解锁 职场秘籍」、指示点第 3 个点亮、按钮变「开启体验」✓ +- 门槛:`tsc` 0 · `eslint` 0 · `npm test` 26 套件 / 365 用例全过 · `check:spacing` 卡片 196 全 8px · + `check:routes` · `check:affordances` · `check:icons` · `check:backend-stubs` 全绿。 +- **上下各 24dp 的黑/白条是系统栏**(状态栏黑、手势导航栏白),在 Expo Go 里由 Expo Go 自己的主题决定, + 不是本页画的;桌面上截同一位置可看到系统栏是透明的,可佐证。非本批引入。 + +**6. 仍未做 / 待定夺** +- **改名只改了「能改的」**:字标位图(第 4 条)必须由设计出图;两份接入方案里 + 「微信开放平台 / 运营商那边登记的包名」若已提交过,也需要同步改(文档已改,**外部登记状态我无从核实**)。 +- 引导页三屏按钮文案是否统一为「开启体验」,见第 3 条末 —— 一行改动,等一句话。 +- 4.19 第 9 条的三项(主应用 footer 16 vs 卡片 8、非卡片间距、设计令牌层)**均未动**。 + +--- + ## 五、后续增量开发建议(优先级) -- **P0(上架前必做)**:~~图标替换~~ **应用图标【已完成,`878710c`,1024×1024「有职」logo】** · ~~隐私 / 协议内容填充~~ **隐私/协议草案【已存在,含 18 岁门槛,待填【待确认】占位】** · ~~EAS 初始化~~ **EAS 配置骨架【已完成,`owner` / `projectId` 两处占位符阻塞 `eas build`,需 `eas init` 注入,详见 4.5】** · ~~18 岁门槛核对~~ **18 岁门槛【已在隐私政策第六章 + 用户协议前言落地】** · ~~微信登录接入评估~~ **微信登录接入评估【已完成 → 见 `微信登录接入方案.md`,SDK 接入阻塞于微信开放平台 AppID】** · ~~启动屏接线~~ **启动屏【已完成,`expo-splash-screen` + `splash-icon.png`,需出包验证】**。 +- **P0(上架前必做)**:~~图标替换~~ **应用图标【`878710c` 做的 1024×1024 是「有职」字标,2026-09-20 改名后已过期,须重出图,见 4.20 第 4 条】** · ~~隐私 / 协议内容填充~~ **隐私/协议草案【已存在,含 18 岁门槛,待填【待确认】占位】** · ~~EAS 初始化~~ **EAS 配置骨架【已完成,`owner` / `projectId` 两处占位符阻塞 `eas build`,需 `eas init` 注入,详见 4.5】** · ~~18 岁门槛核对~~ **18 岁门槛【已在隐私政策第六章 + 用户协议前言落地】** · ~~微信登录接入评估~~ **微信登录接入评估【已完成 → 见 `微信登录接入方案.md`,SDK 接入阻塞于微信开放平台 AppID】** · ~~启动屏接线~~ **启动屏【已完成,`expo-splash-screen` + `splash-icon.png`,需出包验证】**。 - **P0(补充 · 2026-09-15 新发现)**:① ~~页脚服务热线 / 举报监督电话待填~~ **已填 `15154608969`**,且 **`SupportFooter` 已挂在 P1Login / P2QuickLogin 两页上,页脚实际渲染**(2026-09-20 复核;4.10 第 7 条那句「无消费者 / 缺页脚组件」已撤回,见 4.14 第 7 条);**隐私政策里【待确认】的客服电话应与之对齐**(同一个号,两处别填成两个)。② ~~缺「资质证照」页~~ **页面【已完成,见 4.10】—— 四层链路齐备(`src/types/qualification.ts` / `fetchQualifications` / `useQualifications` / `app/settings/qualifications.tsx`),设置页与「关于我们」页两处入口已恢复,三类证照的补图占位已逐张标明;**但证照的真实编号 / 发证机关 / 有效期 / 证照图仍全部待法务与资质方提供**(材料到位后只改 `MOCK_QUALIFICATIONS` 字段 + 更新测试断言),**未编造任何证件编号**。③ **注册引导链 + 简历导出的真机手工验证均未执行**(见 4.3 / 4.4 末条)。 - **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)** · ~~死控件清零~~ **死控件已清零(22 → 0,2026-09-17,见 4.8)** · ~~假交互清零~~ **假交互已清零(37 → 0,2026-09-18,**但 4.8 当时的「已完成」结论是错的,见 4.9 首条**)** · ~~卡片圆角统一 8px + 去阴影~~ **已完成(147 处圆角 + 54 行阴影,覆盖边界见 4.17;新增卡片须从 `src/theme` 出圆角常量这一层尚未建立)** · 真实后端对接 · OTA 通道验证。