feat(protocol): add dedicated full-text legal protocol page with smooth scroll view and link from login and user center

这个提交包含在:
xcbyy
2026-10-13 08:35:47 +08:00
父节点 ecde03f02c
当前提交 8d85ed54b2
共有 5 个文件被更改,包括 492 次插入 和 23 次删除

查看文件

@@ -28,6 +28,15 @@
"backgroundColor": "#0A0F1D"
}
},
{
"path": "pages/common/protocol",
"style": {
"navigationBarTitleText": "服务协议与隐私政策",
"navigationBarBackgroundColor": "#0A0F1D",
"navigationBarTextStyle": "white",
"backgroundColor": "#0A0F1D"
}
},
{
"path": "pages/activity/list",
"style": {

查看文件

@@ -249,20 +249,30 @@ const loginWithDemo = (acc) => {
};
const openUserAgreement = () => {
uni.showModal({
title: platformProtocol.value.userAgreementTitle || '用户服务协议',
content: platformProtocol.value.userAgreementContent,
showCancel: false,
confirmText: '我知道了'
uni.navigateTo({
url: '/pages/common/protocol?type=service',
fail: () => {
uni.showModal({
title: platformProtocol.value.userAgreementTitle || '用户服务协议',
content: platformProtocol.value.userAgreementContent,
showCancel: false,
confirmText: '我知道了'
});
}
});
};
const openPrivacyAgreement = () => {
uni.showModal({
title: platformProtocol.value.privacyPolicyTitle || '隐私保护指引',
content: platformProtocol.value.privacyPolicyContent,
showCancel: false,
confirmText: '我知道了'
uni.navigateTo({
url: '/pages/common/protocol?type=privacy',
fail: () => {
uni.showModal({
title: platformProtocol.value.privacyPolicyTitle || '隐私保护指引',
content: platformProtocol.value.privacyPolicyContent,
showCancel: false,
confirmText: '我知道了'
});
}
});
};
</script>

查看文件

@@ -0,0 +1,403 @@
<template>
<view class="protocol-page" :class="{ 'theme-warm': currentThemeKey === 'warm' }">
<!-- 顶部状态栏与标题区 -->
<view class="protocol-header">
<view class="protocol-badge">合规法律条款</view>
<text class="protocol-title">{{ pageTitle }}</text>
<text class="protocol-meta">生效版本:v2.4.0 · 适用全平台所有注册与履约用户</text>
</view>
<!-- 协议条款滚动主体区域 -->
<scroll-view class="protocol-scroll-body" scroll-y enable-flex>
<view class="protocol-card">
<!-- 协议概要导言卡片 -->
<view class="intro-box">
<text class="intro-label">重要提示与导言:</text>
<text class="intro-text">{{ pageSummary }}</text>
</view>
<!-- 条款章节列表 -->
<view class="sections-list">
<view
v-for="(sec, idx) in formattedSections"
:key="idx"
class="section-item"
>
<view class="section-head" v-if="sec.title">
<view class="section-indicator"></view>
<text class="section-title">{{ sec.title }}</text>
</view>
<text class="section-body">{{ sec.body }}</text>
</view>
</view>
<!-- 法律声明与签署提示 -->
<view class="legal-notice">
<text class="notice-title">法律合规声明:</text>
<text class="notice-desc">
平台全面遵守国家网信办相关规定及微信小程序平台运营规范。平台依法对同城履约全链路提供合规安全守护、技术加密保障与7x24小时资金争议仲裁。如您对本协议内容有任何异议,请停止使用平台相关服务。
</text>
</view>
</view>
</scroll-view>
<!-- 底部操作与确认栏 -->
<view class="protocol-bottom-bar">
<button class="btn-confirm-read btn-active" @click="handleBack">
我已阅读并知悉
</button>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
import { getCurrentThemeKey, applyThemeToNativeUI } from '@/common/theme.js';
import { getPlatformConfigs } from '@/common/api.js';
const currentThemeKey = ref(getCurrentThemeKey());
const protocolType = ref('service'); // 'service' | 'privacy'
const protocolData = ref({
userAgreementTitle: '用户服务协议',
userAgreementContent: '1. 账号使用规范:用户注册与使用同城生活陪伴服务须符合国家法律法规,严禁利用平台从事任何违法违规行为。\n2. 资金与交易保障:平台提供全额担保交易,订单完成前款项由官方账户托管,碰面出示4位核销码确认后自动结算至大神钱包。\n3. 履约安全守则:所有同城活动须在合规公开商业街区进行,严禁私下导流与脱离平台交易。\n4. 争议与售后仲裁:若遇服务不符或搭子违约,可随时在订单中发起申诉,由平台运营仲裁专员裁决。',
privacyPolicyTitle: '隐私保护指引',
privacyPolicyContent: '1. 信息收集范围:为保障同城陪玩交易安全与现场4位核销码核验,平台在获得您授权后会依法收集手机号与必要位置信息。\n2. 数据加密保护:平台全面接入微信小程序官方安全风控规范,所有用户数据均实施高强度加密脱敏存储。\n3. 权限撤回与管理:您可随时在小程序系统设置中撤回位置或通讯授权,平台未经您的明确许可绝不会向任何第三方泄露您的隐私数据。',
summary: '严禁发布任何违规言论及不良欺诈行为,平台全面遵循微信内容安全风控与用户隐私保护标准。'
});
const pageTitle = computed(() => {
if (protocolType.value === 'privacy') {
return protocolData.value.privacyPolicyTitle || '隐私保护指引';
}
return protocolData.value.userAgreementTitle || '用户服务协议';
});
const pageSummary = computed(() => {
return protocolData.value.summary || '平台全面遵循微信官方内容安全风控与用户隐私保护标准。';
});
const rawContent = computed(() => {
if (protocolType.value === 'privacy') {
return protocolData.value.privacyPolicyContent || '';
}
return protocolData.value.userAgreementContent || '';
});
// 将纯文本条款拆分为结构化章节
const formattedSections = computed(() => {
const content = rawContent.value;
if (!content) return [];
const lines = content.split('\n').map(l => l.trim()).filter(Boolean);
const sections = [];
lines.forEach((line) => {
// 匹配如 1. 2. 或者 【】 格式的标题
const match = line.match(/^(\d+[\.\、]|【.+?】)(.+)/);
if (match) {
sections.push({
title: line.substring(0, line.indexOf(':') > -1 ? line.indexOf(':') + 1 : 12),
body: line.substring(line.indexOf(':') > -1 ? line.indexOf(':') + 1 : 12)
});
} else {
sections.push({
title: '',
body: line
});
}
});
return sections.length > 0 ? sections : [{ title: '条款内容', body: content }];
});
const loadConfig = async () => {
try {
const res = await getPlatformConfigs();
if (res && res.platform_protocol) {
const p = res.platform_protocol;
if (typeof p === 'object') {
if (p.userAgreementTitle) protocolData.value.userAgreementTitle = p.userAgreementTitle;
if (p.userAgreementContent) protocolData.value.userAgreementContent = p.userAgreementContent;
if (p.privacyPolicyTitle) protocolData.value.privacyPolicyTitle = p.privacyPolicyTitle;
if (p.privacyPolicyContent) protocolData.value.privacyPolicyContent = p.privacyPolicyContent;
if (p.summary) protocolData.value.summary = p.summary;
}
}
} catch (e) {
console.warn('[Protocol] 加载系统配置失败,降级展示默认条款:', e);
}
};
onLoad((options) => {
if (options && options.type) {
protocolType.value = options.type;
}
uni.setNavigationBarTitle({
title: protocolType.value === 'privacy' ? '隐私保护指引' : '用户服务协议'
});
});
onMounted(() => {
currentThemeKey.value = getCurrentThemeKey();
applyThemeToNativeUI(currentThemeKey.value);
loadConfig();
});
onShow(() => {
currentThemeKey.value = getCurrentThemeKey();
applyThemeToNativeUI(currentThemeKey.value);
});
const handleBack = () => {
uni.navigateBack({
fail: () => {
uni.switchTab({ url: '/pages/index/index' });
}
});
};
</script>
<style lang="scss" scoped>
.protocol-page {
min-height: 100vh;
background-color: #0A0F1D;
display: flex;
flex-direction: column;
box-sizing: border-box;
}
/* 顶部标题区 */
.protocol-header {
padding: 40rpx 36rpx 24rpx;
background: #10172A;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
.protocol-badge {
display: inline-block;
font-size: 20rpx;
font-weight: 600;
color: #10B981;
background: rgba(16, 185, 129, 0.15);
padding: 4rpx 16rpx;
border-radius: 8rpx;
margin-bottom: 12rpx;
}
.protocol-title {
display: block;
font-size: 38rpx;
font-weight: 800;
color: #F8FAFC;
letter-spacing: 1rpx;
margin-bottom: 10rpx;
}
.protocol-meta {
display: block;
font-size: 22rpx;
color: #64748B;
}
}
/* 协议主体滚动区 */
.protocol-scroll-body {
flex: 1;
padding: 28rpx 32rpx calc(140rpx + env(safe-area-inset-bottom));
box-sizing: border-box;
.protocol-card {
background: #151D30;
border-radius: 24rpx;
border: 1px solid rgba(255, 255, 255, 0.06);
padding: 32rpx;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.3);
.intro-box {
background: rgba(99, 102, 241, 0.08);
border-left: 6rpx solid #6366F1;
padding: 20rpx 24rpx;
border-radius: 12rpx;
margin-bottom: 36rpx;
.intro-label {
font-size: 24rpx;
font-weight: 700;
color: #818CF8;
display: block;
margin-bottom: 6rpx;
}
.intro-text {
font-size: 24rpx;
color: #CBD5E1;
line-height: 1.6;
}
}
.sections-list {
display: flex;
flex-direction: column;
gap: 32rpx;
margin-bottom: 36rpx;
.section-item {
padding-bottom: 24rpx;
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
&:last-child {
border-bottom: none;
padding-bottom: 0;
}
.section-head {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 12rpx;
.section-indicator {
width: 8rpx;
height: 24rpx;
border-radius: 4rpx;
background: #10B981;
}
.section-title {
font-size: 28rpx;
font-weight: 700;
color: #F8FAFC;
}
}
.section-body {
font-size: 26rpx;
color: #94A3B8;
line-height: 1.7;
display: block;
}
}
}
.legal-notice {
background: rgba(255, 255, 255, 0.02);
border-radius: 16rpx;
padding: 24rpx;
border: 1px solid rgba(255, 255, 255, 0.05);
.notice-title {
font-size: 22rpx;
font-weight: 700;
color: #64748B;
display: block;
margin-bottom: 8rpx;
}
.notice-desc {
font-size: 22rpx;
color: #64748B;
line-height: 1.6;
}
}
}
}
/* 底部操作栏 */
.protocol-bottom-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 20rpx 36rpx calc(24rpx + env(safe-area-inset-bottom));
background: rgba(10, 15, 29, 0.95);
backdrop-filter: blur(20px);
border-top: 1px solid rgba(255, 255, 255, 0.08);
box-sizing: border-box;
z-index: 99;
.btn-confirm-read {
width: 100%;
height: 84rpx;
border-radius: 42rpx;
background: #07C160;
color: #FFFFFF;
font-size: 28rpx;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
border: none;
&::after {
border: none;
}
}
}
/* 浅色暖阳模式适配 */
.theme-warm {
background-color: #FAF8F5;
.protocol-header {
background: #FFFFFF;
border-bottom-color: #E7E5E4;
.protocol-title {
color: #1C1917;
}
.protocol-meta {
color: #78716C;
}
}
.protocol-scroll-body .protocol-card {
background: #FFFFFF;
border-color: #E7E5E4;
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.04);
.intro-box {
background: #FFF7ED;
border-left-color: #EA580C;
.intro-label {
color: #EA580C;
}
.intro-text {
color: #78716C;
}
}
.sections-list .section-item {
border-bottom-color: #F5F5F4;
.section-head {
.section-indicator {
background: #EA580C;
}
.section-title {
color: #1C1917;
}
}
.section-body {
color: #57534E;
}
}
.legal-notice {
background: #FDFBF7;
border-color: #E7E5E4;
.notice-title {
color: #A8A29E;
}
.notice-desc {
color: #A8A29E;
}
}
}
.protocol-bottom-bar {
background: rgba(250, 248, 245, 0.95);
border-top-color: #E7E5E4;
}
}
</style>

查看文件

@@ -816,18 +816,26 @@ const openFaq = () => {
};
const openAbout = () => {
const proto = platformConfigs.value.platform_protocol || {};
if (privacyModalRef.value && privacyModalRef.value.show) {
privacyModalRef.value.show();
} else {
const fullText = (proto.userAgreementContent ? `【用户服务协议】\n${proto.userAgreementContent}\n\n` : '') +
(proto.privacyPolicyContent ? `【隐私保护指引】\n${proto.privacyPolicyContent}` : (proto.content || proto.summary || '严禁发布任何违规言论及低俗诈骗行为,平台全面遵循微信内容安全风控与用户隐私保护标准。'));
uni.showModal({
title: proto.title || '用户服务协议与隐私规范',
content: fullText,
showCancel: false
});
}
uni.showActionSheet({
itemList: ['用户服务协议', '隐私保护指引'],
success: (res) => {
const type = res.tapIndex === 1 ? 'privacy' : 'service';
uni.navigateTo({
url: `/pages/common/protocol?type=${type}`,
fail: () => {
const proto = platformConfigs.value.platform_protocol || {};
const fullText = type === 'privacy'
? (proto.privacyPolicyContent || '平台全面遵守微信小程序开发者隐私规范要求,对用户数据实施高强度加密保护。')
: (proto.userAgreementContent || '平台遵循合法合规与安全原则,所有同城活动履约均须在公开透明街区展开。');
uni.showModal({
title: type === 'privacy' ? (proto.privacyPolicyTitle || '隐私保护指引') : (proto.userAgreementTitle || '用户服务协议'),
content: fullText,
showCancel: false
});
}
});
}
});
};
const navTo = (url) => {

查看文件

@@ -130,7 +130,7 @@ describe('ContentSecurityService & Compliance Architecture (TDD 微信内容安
const raw = fs.readFileSync(pagesJsonPath, 'utf-8');
const config = JSON.parse(raw);
expect(Array.isArray(config.pages)).toBe(true);
expect(config.pages.length).toBe(15);
expect(config.pages.length).toBe(16);
for (const pageItem of config.pages) {
const vueFilePath = path.join(rootDir, 'pwuniapp', `${pageItem.path}.vue`);
@@ -483,6 +483,45 @@ describe('ContentSecurityService & Compliance Architecture (TDD 微信内容安
expect(content).toContain('white-space: nowrap');
});
});
describe('13. 专属独立法律协议与隐私政策长文阅读页面验证 (pages/common/protocol.vue)', () => {
it('pages.json 必须成功注册 pages/common/protocol 且物理文件必须存在', () => {
const protoPath = path.join(rootDir, 'pwuniapp', 'pages', 'common', 'protocol.vue');
expect(fs.existsSync(protoPath)).toBe(true);
const pagesJsonPath = path.join(rootDir, 'pwuniapp', 'pages.json');
const pagesJson = fs.readFileSync(pagesJsonPath, 'utf-8');
expect(pagesJson).toContain('pages/common/protocol');
expect(pagesJson).toContain('服务协议与隐私政策');
});
it('协议页面必须支持动态章节结构化拆分、长文上下滚动与双主题适配', () => {
const protoPath = path.join(rootDir, 'pwuniapp', 'pages', 'common', 'protocol.vue');
const content = fs.readFileSync(protoPath, 'utf-8');
expect(content).toContain('scroll-view');
expect(content).toContain('formattedSections');
expect(content).toContain('getPlatformConfigs');
expect(content).toContain('applyThemeToNativeUI');
expect(content).toContain('theme-warm');
});
it('登录页与个人中心必须实现对专属协议页面的原生导航跳转', () => {
const loginPath = path.join(rootDir, 'pwuniapp', 'pages', 'auth', 'login.vue');
const loginContent = fs.readFileSync(loginPath, 'utf-8');
expect(loginContent).toContain('/pages/common/protocol?type=service');
expect(loginContent).toContain('/pages/common/protocol?type=privacy');
const userPath = path.join(rootDir, 'pwuniapp', 'pages', 'user', 'index.vue');
const userContent = fs.readFileSync(userPath, 'utf-8');
expect(userContent).toContain('/pages/common/protocol?type=');
});
it('协议阅读页面代码严禁包含任何系统 Emoji', () => {
const protoPath = path.join(rootDir, 'pwuniapp', 'pages', 'common', 'protocol.vue');
const content = fs.readFileSync(protoPath, 'utf-8');
const emojiRegex = /(?:\p{Extended_Pictographic}|\p{Emoji_Presentation}|[\u2600-\u27BF])/u;
expect(emojiRegex.test(content)).toBe(false);
});
});
});