feat(protocol): add dedicated full-text legal protocol page with smooth scroll view and link from login and user center
这个提交包含在:
@@ -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>
|
||||
|
||||
403
pwuniapp/pages/common/protocol.vue
普通文件
403
pwuniapp/pages/common/protocol.vue
普通文件
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户