From f6d42023beacef0d0aa8537940f38eba725fbd89 Mon Sep 17 00:00:00 2001
From: doublex001 <16157008+doublex001@user.noreply.gitee.com>
Date: Thu, 10 Sep 2026 20:47:28 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E8=96=AA=E8=B5=84=E8=AE=A1=E7=AE=97?=
=?UTF-8?q?=E5=99=A8=E5=9F=8E=E5=B8=82=E9=80=89=E6=8B=A9=20+=20=E6=96=87?=
=?UTF-8?q?=E4=B9=A6=E6=A8=A1=E6=9D=BF=E9=A2=84=E8=A7=88=EF=BC=88=E5=8D=A0?=
=?UTF-8?q?=E4=BD=8D=E5=AE=A1=E8=AE=A1=E6=96=AD=E7=82=B9=E6=94=B6=E5=8F=A3?=
=?UTF-8?q?=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 薪资计算器:新增城市社保/公积金基数上下限表,选城市后 clamp 社保基数并计入计算(断点②)
- 文书模板:新增模板正文与预览页 /legal/template,正文 selectable 长按可复制(断点③)
- 分层:types/constants/api/页面;tsc --noEmit 0 错误、eslint 0 警告、路由审计 0 死链
---
app/legal/template.tsx | 145 ++++++++++++++++++++++++++++++++
app/legal/templates.tsx | 6 +-
app/tools/salary-calculator.tsx | 131 ++++++++++++++++++++++++++++-
src/api/api.ts | 68 +++++++++++++--
src/constants/index.ts | 26 +++++-
src/types/index.ts | 1 +
src/types/legal.ts | 4 +-
src/types/tools.ts | 16 +++-
8 files changed, 383 insertions(+), 14 deletions(-)
create mode 100644 app/legal/template.tsx
diff --git a/app/legal/template.tsx b/app/legal/template.tsx
new file mode 100644
index 0000000..b7f263f
--- /dev/null
+++ b/app/legal/template.tsx
@@ -0,0 +1,145 @@
+import { Ionicons } from '@expo/vector-icons';
+import { useRouter, useLocalSearchParams } from 'expo-router';
+import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
+import { SafeAreaView } from 'react-native-safe-area-context';
+import { useLegalData } from '../../src/hooks';
+import { LoadingState, ErrorState, EmptyState } from '../../src/components/StateView';
+import { colors } from '../../src/theme';
+
+/* ===================== 顶部导航 ===================== */
+
+function Header({ onBack }: { onBack: () => void }) {
+ return (
+
+
+
+
+ 文书预览
+
+
+ );
+}
+
+/* ===================== 页面 ===================== */
+
+export default function LegalTemplateScreen() {
+ const router = useRouter();
+ const { id } = useLocalSearchParams();
+ const { templates, loading, error, refresh } = useLegalData();
+
+ const templateId = Array.isArray(id) ? id[0] : id;
+ const template = templateId ? templates.find((t) => t.id === templateId) : undefined;
+
+ return (
+
+ router.back()} />
+ {loading ? (
+
+ ) : error ? (
+
+ ) : !template ? (
+
+ ) : (
+
+
+
+
+ {template.name}
+ {template.description}
+
+
+
+
+ {/* selectable:长按正文即可复制,无需额外原生模块(mock 阶段) */}
+
+ {template.content}
+
+
+
+
+ 长按正文即可复制;正式下载 / 导出为 Word·PDF 需后端文件支持,当前为内容预览。
+
+
+ )}
+
+ );
+}
+
+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,
+ },
+ headCard: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ padding: 16,
+ borderRadius: 12,
+ backgroundColor: colors.white,
+ marginBottom: 12,
+ },
+ headBody: {
+ flex: 1,
+ marginLeft: 12,
+ },
+ name: {
+ fontSize: 16,
+ fontWeight: '700',
+ color: colors.textPrimary,
+ },
+ desc: {
+ marginTop: 2,
+ fontSize: 12,
+ color: colors.textSecondary,
+ },
+ contentCard: {
+ padding: 16,
+ borderRadius: 12,
+ backgroundColor: colors.white,
+ borderWidth: 1,
+ borderColor: colors.borderLight,
+ },
+ contentText: {
+ fontSize: 14,
+ lineHeight: 22,
+ color: colors.textPrimary,
+ },
+ tip: {
+ marginTop: 12,
+ fontSize: 12,
+ color: colors.textHint,
+ lineHeight: 18,
+ },
+});
diff --git a/app/legal/templates.tsx b/app/legal/templates.tsx
index 2d50346..c32ca49 100644
--- a/app/legal/templates.tsx
+++ b/app/legal/templates.tsx
@@ -1,6 +1,6 @@
import { Ionicons } from '@expo/vector-icons';
import { useRouter } from 'expo-router';
-import { Alert, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
+import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useLegalData } from '../../src/hooks';
import { LoadingState, ErrorState, EmptyState } from '../../src/components/StateView';
@@ -62,9 +62,7 @@ export default function LegalTemplatesScreen() {
- Alert.alert('文书预览', `「${template.name}」预览与下载功能即将上线`)
- }
+ onPress={() => router.push(`/legal/template?id=${template.id}`)}
/>
))}
diff --git a/app/tools/salary-calculator.tsx b/app/tools/salary-calculator.tsx
index bc884fe..de696ba 100644
--- a/app/tools/salary-calculator.tsx
+++ b/app/tools/salary-calculator.tsx
@@ -4,6 +4,7 @@ import { useRouter } from 'expo-router';
import { useState } from 'react';
import {
Alert,
+ Modal,
Pressable,
ScrollView,
StyleSheet,
@@ -14,6 +15,11 @@ import {
import { SafeAreaView } from 'react-native-safe-area-context';
import { useSalaryCalculator } from '../../src/hooks';
import { colors, headerGradientShort } from '../../src/theme';
+import {
+ CITIES,
+ DEFAULT_SOCIAL_PROFILE,
+ SOCIAL_SECURITY_CITY_PROFILES,
+} from '../../src/constants';
import type { SalaryCalcInput, SalaryToolLink } from '../../src/types';
import { formatCurrency } from '../../src/utils';
@@ -226,11 +232,32 @@ function ToolLink({ tool }: { tool: SalaryToolLink }) {
export default function SalaryCalculatorScreen() {
const router = useRouter();
const { inputs, setInputs, result } = useSalaryCalculator();
+ const [cityPickerVisible, setCityPickerVisible] = useState(false);
+
+ const cityProfile =
+ SOCIAL_SECURITY_CITY_PROFILES[inputs.city] ?? DEFAULT_SOCIAL_PROFILE;
const update = (key: K, value: SalaryCalcInput[K]) => {
setInputs({ ...inputs, [key]: value });
};
+ /** 将社保基数 clamp 到当前城市上下限内 */
+ const clampSocialBase = (v: number) => {
+ const profile = SOCIAL_SECURITY_CITY_PROFILES[inputs.city] ?? DEFAULT_SOCIAL_PROFILE;
+ return Math.min(profile.socialBaseUpper, Math.max(profile.socialBaseLower, v));
+ };
+
+ /** 选城市:切换城市并把社保基数 clamp 到新城市上下限 */
+ const handleSelectCity = (city: string) => {
+ const profile = SOCIAL_SECURITY_CITY_PROFILES[city] ?? DEFAULT_SOCIAL_PROFILE;
+ const clamped = Math.min(
+ profile.socialBaseUpper,
+ Math.max(profile.socialBaseLower, inputs.socialBase),
+ );
+ setInputs({ ...inputs, city, socialBase: clamped });
+ setCityPickerVisible(false);
+ };
+
return (
router.back()} onHistory={() => Alert.alert('历史记录', '计算历史功能开发中,敬请期待')} />
@@ -244,13 +271,17 @@ export default function SalaryCalculatorScreen() {
suffix="元"
onChange={(v) => update('monthlyGross', Math.max(0, v))}
/>
- Alert.alert('选择城市', '城市选择功能开发中,敬请期待')} />
+ setCityPickerVisible(true)} />
update('socialBase', Math.max(0, v))}
+ onChange={(v) => update('socialBase', clampSocialBase(Math.max(0, v)))}
/>
+
+ 基数范围 ¥{formatCurrency(cityProfile.socialBaseLower, 0)} – ¥
+ {formatCurrency(cityProfile.socialBaseUpper, 0)}({inputs.city})
+
保存计算记录
+
+ setCityPickerVisible(false)}
+ onSelect={handleSelectCity}
+ />
);
}
+/* ===================== 子组件:城市选择底部弹层 ===================== */
+
+function CityPicker({
+ visible,
+ current,
+ onClose,
+ onSelect,
+}: {
+ visible: boolean;
+ current: string;
+ onClose: () => void;
+ onSelect: (city: string) => void;
+}) {
+ return (
+
+
+
+
+ 选择所在城市
+
+ {CITIES.map((city) => {
+ const active = city === current;
+ return (
+ onSelect(city)}
+ >
+
+ {city}
+
+ {active && }
+
+ );
+ })}
+
+
+
+
+ );
+}
+
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: colors.pageBg },
header: {
@@ -462,4 +542,51 @@ const styles = StyleSheet.create({
marginTop: 8,
},
saveBtnText: { fontSize: 16, fontWeight: '700', color: colors.white },
+ baseHint: {
+ fontSize: 12,
+ color: colors.textHint,
+ marginTop: 6,
+ paddingHorizontal: 2,
+ },
+ sheetBackdrop: {
+ flex: 1,
+ backgroundColor: colors.scrim,
+ justifyContent: 'flex-end',
+ },
+ sheet: {
+ backgroundColor: colors.white,
+ borderTopLeftRadius: 16,
+ borderTopRightRadius: 16,
+ paddingTop: 10,
+ paddingBottom: 24,
+ maxHeight: '72%',
+ },
+ sheetHandle: {
+ width: 36,
+ height: 4,
+ borderRadius: 2,
+ backgroundColor: colors.borderLight,
+ alignSelf: 'center',
+ marginBottom: 10,
+ },
+ sheetTitle: {
+ fontSize: 16,
+ fontWeight: '700',
+ color: colors.textPrimary,
+ textAlign: 'center',
+ marginBottom: 12,
+ },
+ sheetList: { maxHeight: 360 },
+ sheetItem: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ paddingVertical: 14,
+ paddingHorizontal: 20,
+ borderTopWidth: 1,
+ borderTopColor: colors.borderLight,
+ },
+ sheetItemActive: { backgroundColor: colors.primaryLight },
+ sheetItemText: { fontSize: 15, color: colors.textPrimary },
+ sheetItemTextActive: { color: colors.primary, fontWeight: '700' },
});
diff --git a/src/api/api.ts b/src/api/api.ts
index 59e9527..6f4ae30 100644
--- a/src/api/api.ts
+++ b/src/api/api.ts
@@ -124,7 +124,7 @@ import type {
WechatBindPhonePayload,
WechatSession,
} from '../types';
-import { MOCK_DELAY, SALARY_RANGES, TOKEN_TTL_MS } from '../constants';
+import { DEFAULT_SOCIAL_PROFILE, MOCK_DELAY, SALARY_RANGES, SOCIAL_SECURITY_CITY_PROFILES, TOKEN_TTL_MS } from '../constants';
import { colors } from '../theme';
import { storageGet, storageSet, storageRemove } from '../lib/storage';
@@ -1747,11 +1747,18 @@ export function computeSalaryResult(input: SalaryCalcInput): SalaryCalcResult {
// 年终奖摊入月度,得到月度税前合计
const monthlyTotal = monthlyGross * (1 + input.bonusMonths / 12);
+ // 按所在城市 clamp 社保基数到该城市上下限(养老/医疗/失业比例全国一致,城市差异在基数上下限)
+ const socialProfile = SOCIAL_SECURITY_CITY_PROFILES[input.city] ?? DEFAULT_SOCIAL_PROFILE;
+ const socialBase = Math.min(
+ socialProfile.socialBaseUpper,
+ Math.max(socialProfile.socialBaseLower, input.socialBase),
+ );
+
// 社保个人缴纳比例:养老 8% + 医疗 2% + 失业 0.5%
- const pension = input.socialBase * 0.08;
- const medical = input.socialBase * 0.02;
- const unemployment = input.socialBase * 0.005;
- const providentFund = input.socialBase * input.providentFundRate;
+ const pension = socialBase * 0.08;
+ const medical = socialBase * 0.02;
+ const unemployment = socialBase * 0.005;
+ const providentFund = socialBase * input.providentFundRate;
const monthlySocial = pension + medical + unemployment + providentFund;
// 个税:月度应纳税所得额 = 税前 - 五险一金 - 5000 起征点 - 专项附加扣除
@@ -3918,6 +3925,20 @@ const MOCK_LEGAL_TEMPLATES: LegalTemplate[] = [
description: '入职前必查 12 项关键条款',
icon: 'checkbox-outline',
url: 'https://dev.api.com/legal/templates/contract-checklist',
+ content: `劳动合同审查清单(入职前必查 12 项)
+
+1. 用人单位名称、法定代表人、注册地址是否与营业执照一致
+2. 合同期限(固定/无固定/以完成一定工作任务为期限)与试用期约定是否合法(试用期不超合同期 1/12,且最长 6 个月)
+3. 工作地点、岗位名称、工作内容是否明确,有无「公司可随意调岗」等霸王条款
+4. 工时制度(标准/综合计算/不定时)及加班工资计算基数与倍数
+5. 劳动报酬:基本工资、绩效、补贴构成,发放时间与方式
+6. 社会保险与住房公积金的缴纳基数、比例、起始时间
+7. 试用期工资是否不低于转正工资 80% 且不低于当地最低工资
+8. 竞业限制范围、期限(不超 2 年)、补偿标准是否明确
+9. 培训服务期约定及违约金上限(不超培训费用)
+10. 解除劳动合同的条件与经济补偿(N / N+1 / 2N)是否合法
+11. 违约金条款是否超出法定范围(仅培训、竞业可约定违约金)
+12. 争议解决方式(仲裁/诉讼)与管辖地是否合理`,
},
{
id: 'tmpl-2',
@@ -3925,6 +3946,22 @@ const MOCK_LEGAL_TEMPLATES: LegalTemplate[] = [
description: '规范离职通知,保留证据',
icon: 'exit-outline',
url: 'https://dev.api.com/legal/templates/resignation-letter',
+ content: `离职申请书
+
+致:____(公司名称)____ 人力资源部
+
+本人 ____(姓名)____,身份证号 ____,于 ____(入职日期)____ 入职,现任 ____(部门 / 岗位)____。
+
+现因 ____(离职原因,如:个人职业发展)____,根据《劳动合同法》第三十七条,本人提前三十日以书面形式通知公司,拟于 ____(最后工作日期)____ 正式解除劳动关系。
+
+请公司按约定办理工作交接、出具离职证明,并结清工资、加班费及未休年假工资;社会保险与住房公积金请缴存至 ____(年 / 月)____。
+
+此致
+敬礼
+
+申请人:____(签名)____
+联系电话:____
+日期:____ 年 ____ 月 ____ 日`,
},
{
id: 'tmpl-3',
@@ -3932,6 +3969,27 @@ const MOCK_LEGAL_TEMPLATES: LegalTemplate[] = [
description: '劳动争议维权起步文书',
icon: 'gavel-outline',
url: 'https://dev.api.com/legal/templates/arbitration-application',
+ content: `劳动仲裁申请书
+
+申请人:____(姓名)____,性别 ____,身份证号 ____,联系电话 ____,住址 ____
+
+被申请人:____(公司全称)____,统一社会信用代码 ____,住所地 ____,法定代表人 ____
+
+仲裁请求:
+1. 请求裁决被申请人支付违法解除劳动合同赔偿金 ____ 元;
+2. 请求裁决被申请人支付拖欠工资 ____ 元及加班费 ____ 元;
+3. 请求裁决被申请人补缴 ____ 年 ____ 月至 ____ 年 ____ 月的社会保险与住房公积金。
+
+事实与理由:
+____(简述入职时间、岗位、工资标准、争议发生经过及证据,如劳动合同、工资条、考勤记录、聊天记录等)____
+
+此致
+____(用人单位所在地或合同履行地)____ 劳动人事争议仲裁委员会
+
+申请人:____(签名)____
+日期:____ 年 ____ 月 ____ 日
+
+附:1. 申请人身份证复印件;2. 证据清单及材料 ____ 份`,
},
];
diff --git a/src/constants/index.ts b/src/constants/index.ts
index 75101fe..3a15791 100644
--- a/src/constants/index.ts
+++ b/src/constants/index.ts
@@ -1,7 +1,7 @@
/**
* 常量统一导出(枚举、配置项等)
*/
-import type { JobLevel } from '../types';
+import type { CitySocialProfile, JobLevel } from '../types';
/** 请求失败自动重试次数 */
export const MAX_RETRY_COUNT = 3;
@@ -35,6 +35,30 @@ export const EDUCATION_OPTIONS: string[] = ['不限', '大专', '本科', '硕
/** 热门城市(定位/城市选择用) */
export const CITIES: string[] = ['北京', '上海', '深圳', '杭州', '广州', '成都', '南京', '武汉'];
+/**
+ * 城市社保/公积金缴费基数上下限(mock 数值,真实接入后端按城市下发)。
+ * 养老 8% / 医疗 2% / 失业 0.5% 全国一致,城市差异体现在基数上下限,故按城市 clamp 基数即可反映差异。
+ */
+export const SOCIAL_SECURITY_CITY_PROFILES: Record = {
+ 北京: { city: '北京', socialBaseUpper: 33891, socialBaseLower: 6821, providentBaseUpper: 33891, providentBaseLower: 2420 },
+ 上海: { city: '上海', socialBaseUpper: 36549, socialBaseLower: 7384, providentBaseUpper: 36549, providentBaseLower: 2690 },
+ 深圳: { city: '深圳', socialBaseUpper: 43659, socialBaseLower: 4492, providentBaseUpper: 43659, providentBaseLower: 2360 },
+ 广州: { city: '广州', socialBaseUpper: 27501, socialBaseLower: 5500, providentBaseUpper: 39579, providentBaseLower: 2300 },
+ 杭州: { city: '杭州', socialBaseUpper: 24060, socialBaseLower: 4812, providentBaseUpper: 36675, providentBaseLower: 2490 },
+ 成都: { city: '成都', socialBaseUpper: 22555, socialBaseLower: 4511, providentBaseUpper: 29331, providentBaseLower: 2100 },
+ 南京: { city: '南京', socialBaseUpper: 24396, socialBaseLower: 4878, providentBaseUpper: 39900, providentBaseLower: 2280 },
+ 武汉: { city: '武汉', socialBaseUpper: 22467, socialBaseLower: 4494, providentBaseUpper: 29330, providentBaseLower: 2210 },
+};
+
+/** 未匹配城市时的兜底上下限(参考一线城市,mock 阶段使用) */
+export const DEFAULT_SOCIAL_PROFILE: CitySocialProfile = {
+ city: '全国',
+ socialBaseUpper: 33891,
+ socialBaseLower: 6821,
+ providentBaseUpper: 33891,
+ providentBaseLower: 2420,
+};
+
/** 一天的毫秒数,用于令牌有效期等时间计算 */
export const ONE_DAY_MS = 24 * 60 * 60 * 1000;
diff --git a/src/types/index.ts b/src/types/index.ts
index 494e4ec..47e3ed6 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -160,6 +160,7 @@ export type {
ProgressStatus,
ProgressStep,
SalaryBracket,
+ CitySocialProfile,
SalaryCalcInput,
SalaryCalcResult,
SalaryInsuranceItem,
diff --git a/src/types/legal.ts b/src/types/legal.ts
index 4373d64..2c6a7be 100644
--- a/src/types/legal.ts
+++ b/src/types/legal.ts
@@ -46,8 +46,10 @@ export interface LegalTemplate {
description: string;
/** 图标名(Ionicons) */
icon: string;
- /** 下载/预览地址(mock 阶段为占位) */
+ /** 下载/预览地址(mock 阶段为占位,真实下载由后端文件提供) */
url: string;
+ /** 模板正文(mock 阶段提供可预览/复制的内容;____ 表示需填写项) */
+ content: string;
}
/** 会员套餐 */
diff --git a/src/types/tools.ts b/src/types/tools.ts
index be5e78f..b197aeb 100644
--- a/src/types/tools.ts
+++ b/src/types/tools.ts
@@ -139,7 +139,7 @@ export interface SkillCloudData {
export interface SalaryCalcInput {
/** 税前月薪(元) */
monthlyGross: number;
- /** 所在城市(决定社保公积金上下限,mock 阶段仅展示) */
+ /** 所在城市(决定社保/公积金缴费基数上下限,选择后实际参与计算) */
city: string;
/** 社保缴费基数(元) */
socialBase: number;
@@ -151,6 +151,20 @@ export interface SalaryCalcInput {
bonusMonths: number;
}
+/** 城市社保/公积金缴费基数上下限(mock 阶段内置,真实后端按城市下发) */
+export interface CitySocialProfile {
+ /** 城市名(与 CITIES 常量对应) */
+ city: string;
+ /** 社保缴费基数上限(元/月) */
+ socialBaseUpper: number;
+ /** 社保缴费基数下限(元/月) */
+ socialBaseLower: number;
+ /** 公积金缴存基数上限(元/月) */
+ providentBaseUpper: number;
+ /** 公积金缴存基数下限(元/月) */
+ providentBaseLower: number;
+}
+
/** 五险一金单项明细 */
export interface SalaryInsuranceItem {
/** 险种名称 */