From 2890248afa814ff44a6dbbc0112b827ab3c8c7df Mon Sep 17 00:00:00 2001 From: zhangjianjun Date: Mon, 31 Aug 2026 15:04:26 +0800 Subject: [PATCH] ud --- src/app.config.ts | 1 + src/components/privacy-consent/index.scss | 29 ++++ src/components/privacy-consent/index.tsx | 52 +++++++ src/lib/consent.ts | 53 +++++++ src/lib/status.test.ts | 16 ++- src/lib/status.ts | 3 +- src/pages/agreement/index.config.ts | 6 + src/pages/agreement/index.scss | 55 ++++++++ src/pages/agreement/index.tsx | 165 ++++++++++++++++++++++ src/pages/index/index.tsx | 3 +- src/pages/resubmit/index.scss | 13 ++ src/pages/resubmit/index.tsx | 24 +++- 12 files changed, 409 insertions(+), 11 deletions(-) create mode 100644 src/components/privacy-consent/index.scss create mode 100644 src/components/privacy-consent/index.tsx create mode 100644 src/lib/consent.ts create mode 100644 src/pages/agreement/index.config.ts create mode 100644 src/pages/agreement/index.scss create mode 100644 src/pages/agreement/index.tsx diff --git a/src/app.config.ts b/src/app.config.ts index bc66352..95b6a0c 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -4,6 +4,7 @@ export default defineAppConfig({ 'pages/resubmit/index', 'pages/complaint/index', 'pages/map/index', + 'pages/agreement/index', ], window: { backgroundTextStyle: 'light', diff --git a/src/components/privacy-consent/index.scss b/src/components/privacy-consent/index.scss new file mode 100644 index 0000000..94e4746 --- /dev/null +++ b/src/components/privacy-consent/index.scss @@ -0,0 +1,29 @@ +.privacy-consent { + display: flex; + flex-wrap: wrap; + align-items: center; + color: #4e5969; + font-size: 24px; + line-height: 36px; +} + +.privacy-consent__group { + display: flex; + align-items: center; +} + +.privacy-consent__checkbox { + margin-right: 6px; + transform: scale(0.8); + transform-origin: center; +} + +.privacy-consent__text, +.privacy-consent__link { + font-size: 24px; + line-height: 36px; +} + +.privacy-consent__link { + color: #165dff; +} diff --git a/src/components/privacy-consent/index.tsx b/src/components/privacy-consent/index.tsx new file mode 100644 index 0000000..d6af448 --- /dev/null +++ b/src/components/privacy-consent/index.tsx @@ -0,0 +1,52 @@ +import { Checkbox, CheckboxGroup, Text, View } from '@tarojs/components' +import Taro from '@tarojs/taro' +import { requestPlatformPrivacyAuthorization } from '@/lib/consent' +import './index.scss' + +type Props = { + agreed: boolean + className?: string + onChange: (agreed: boolean) => void +} + +function openAgreement (type: 'service' | 'privacy') { + Taro.navigateTo({ url: `/pages/agreement/index?type=${type}` }) +} + +export default function PrivacyConsent ({ agreed, className = '', onChange }: Props) { + return ( + + { + const nextAgreed = event.detail.value.includes('agreed') + if (!nextAgreed) { + onChange(false) + return + } + void requestPlatformPrivacyAuthorization().then((authorized) => { + onChange(authorized) + if (!authorized) { + Taro.showToast({ title: '需同意隐私保护指引后才能继续', icon: 'none' }) + } + }) + }} + > + + 我已阅读并同意 + + openAgreement('service')}> + 《用户服务协议》 + + + openAgreement('privacy')}> + 《隐私政策》 + + + ) +} diff --git a/src/lib/consent.ts b/src/lib/consent.ts new file mode 100644 index 0000000..a6eb4af --- /dev/null +++ b/src/lib/consent.ts @@ -0,0 +1,53 @@ +import Taro from '@tarojs/taro' + +export const PRIVACY_CONSENT_VERSION = '2026-08-31' + +const PRIVACY_CONSENT_KEY = 'corpMpPrivacyConsent' + +type StoredConsent = { + agreed: boolean + agreedAt: number + version: string +} + +export function hasPrivacyConsent () { + try { + const consent = Taro.getStorageSync(PRIVACY_CONSENT_KEY) as StoredConsent | undefined + return Boolean(consent?.agreed && consent.version === PRIVACY_CONSENT_VERSION) + } catch { + return false + } +} + +export function requestPlatformPrivacyAuthorization () { + if ( + Taro.getEnv() !== Taro.ENV_TYPE.WEAPP || + typeof Taro.requirePrivacyAuthorize !== 'function' + ) { + return Promise.resolve(true) + } + + return new Promise((resolve) => { + Taro.requirePrivacyAuthorize({ + success: () => resolve(true), + fail: () => resolve(false), + }) + }) +} + +export function savePrivacyConsent (agreed: boolean) { + try { + if (!agreed) { + Taro.removeStorageSync(PRIVACY_CONSENT_KEY) + return + } + const consent: StoredConsent = { + agreed: true, + agreedAt: Date.now(), + version: PRIVACY_CONSENT_VERSION, + } + Taro.setStorageSync(PRIVACY_CONSENT_KEY, consent) + } catch { + // Consent is still valid for the current page when storage is unavailable. + } +} diff --git a/src/lib/status.test.ts b/src/lib/status.test.ts index 622f134..562aa30 100644 --- a/src/lib/status.test.ts +++ b/src/lib/status.test.ts @@ -58,18 +58,18 @@ describe('getOrderCardActions', () => { }) }) - it('shows only complaint on 待办理', () => { + it('keeps submit available on 待办理', () => { expect(getOrderCardActions({ process_status: '1', goods_type: 'map_label' })).toEqual({ complaint: true, - submit: false, + submit: true, detail: false, }) }) - it('shows view-detail on completed orders and hides complaint', () => { + it('keeps submit available on completed orders and hides complaint', () => { expect(getOrderCardActions({ process_status: '2', goods_type: 'map_label' })).toEqual({ complaint: false, - submit: false, + submit: true, detail: true, }) }) @@ -83,9 +83,15 @@ describe('getOrderCardActions', () => { }) expect(getOrderCardActions({ process_status: '4', goods_type: goodsType })).toEqual({ complaint: false, - submit: false, + submit: true, detail: true, }) } }) + + it('keeps submit available for every known and unknown status', () => { + for (const status of ['', '1', '2', '3', '4', '5', '6', '7', '8', 'unknown']) { + expect(getOrderCardActions({ process_status: status, goods_type: 'map_label' }).submit).toBe(true) + } + }) }) diff --git a/src/lib/status.ts b/src/lib/status.ts index 9ac5027..6d36d67 100644 --- a/src/lib/status.ts +++ b/src/lib/status.ts @@ -27,7 +27,6 @@ export const ORDER_TABS = [ export type OrderTabKey = (typeof ORDER_TABS)[number]['key'] const DONE_STATUSES = new Set(['2', '5', '7', '8']) -const SUBMIT_STATUSES = new Set(['4', '6']) export function getStatusMeta (status?: string, name?: string) { const config = STATUS_CONFIG[String(status || '')] @@ -52,7 +51,7 @@ export function getOrderCardActions (order: { process_status?: string, goods_typ const detail = corpGoods ? status !== '6' : DONE_STATUSES.has(status) return { complaint: !detail, - submit: corpGoods ? status === '6' : SUBMIT_STATUSES.has(status), + submit: true, detail, } } diff --git a/src/pages/agreement/index.config.ts b/src/pages/agreement/index.config.ts new file mode 100644 index 0000000..90f7087 --- /dev/null +++ b/src/pages/agreement/index.config.ts @@ -0,0 +1,6 @@ +export default definePageConfig({ + navigationBarTitleText: '用户服务协议', + navigationBarBackgroundColor: '#ffffff', + navigationBarTextStyle: 'black', + backgroundColor: '#ffffff', +}) diff --git a/src/pages/agreement/index.scss b/src/pages/agreement/index.scss new file mode 100644 index 0000000..952279a --- /dev/null +++ b/src/pages/agreement/index.scss @@ -0,0 +1,55 @@ +.agreement-page { + min-height: 100vh; + box-sizing: border-box; + padding: 32px 32px calc(56px + env(safe-area-inset-bottom)); + background: #ffffff; +} + +.agreement-page__title { + display: block; + color: #1d2129; + font-size: 40px; + font-weight: 700; + line-height: 56px; + text-align: center; +} + +.agreement-page__date { + display: block; + margin-top: 16px; + color: #86909c; + font-size: 24px; + line-height: 36px; + text-align: center; +} + +.agreement-page__intro { + display: block; + margin-top: 36px; + color: #4e5969; + font-size: 28px; + line-height: 48px; + text-align: justify; +} + +.agreement-page__section { + margin-top: 40px; +} + +.agreement-page__heading { + display: block; + color: #1d2129; + font-size: 30px; + font-weight: 700; + line-height: 44px; +} + +.agreement-page__paragraph { + display: block; + margin-top: 16px; + color: #4e5969; + font-size: 28px; + line-height: 48px; + text-align: justify; + word-break: break-all; +} diff --git a/src/pages/agreement/index.tsx b/src/pages/agreement/index.tsx new file mode 100644 index 0000000..4e3b0fa --- /dev/null +++ b/src/pages/agreement/index.tsx @@ -0,0 +1,165 @@ +import { Text, View } from '@tarojs/components' +import Taro, { useLoad } from '@tarojs/taro' +import { useState } from 'react' +import './index.scss' + +type Agreement = { + title: string + date: string + intro: string + sections: Array<{ + title: string + paragraphs: string[] + }> +} + +const SERVICE_AGREEMENT: Agreement = { + title: '用户服务协议', + date: '生效日期:2026年8月31日', + intro: '欢迎使用“敌无双企业服务”小程序。本协议是您与本小程序运营者(以下简称“我们”)之间关于使用本小程序服务所订立的协议。请您在使用服务前认真阅读并充分理解全部条款,尤其是涉及个人信息、责任限制及争议解决的内容。您主动勾选同意并继续使用服务,即表示您已阅读、理解并接受本协议。', + sections: [ + { + title: '一、服务内容', + paragraphs: [ + '本小程序向用户提供企业服务订单查询、订单详情查看、资料补充或重新提交、门店地图位置选择、企业信息申报、投诉反馈及客户服务等功能。具体服务内容以页面实际展示为准。', + '我们有权根据业务需要和法律法规要求调整服务内容。涉及用户权益的重要变更,将通过页面提示、公告或其他合理方式告知。', + ], + }, + { + title: '二、用户资格与使用规范', + paragraphs: [ + '您应具备与使用本服务相适应的民事行为能力。代表企业或其他组织提交信息的,您应已取得相应授权,并有权提供相关资料。', + '您应提供真实、准确、完整、有效的信息,不得冒用他人身份,不得提交违法、虚假、侵权或与申请事项无关的内容,不得利用本服务干扰系统运行或损害他人合法权益。因信息不实、无权提交或违法使用造成的责任,由您依法承担。', + ], + }, + { + title: '三、信息提交与业务办理', + paragraphs: [ + '订单查询需要您提供联系电话,用于匹配并展示与该号码关联的订单。资料补充、企业申报或投诉处理可能需要您提供门店信息、企业信息、联系人信息、身份证号、图片、位置及问题描述。具体收集目的、方式和使用范围请查阅《隐私政策》。', + '您提交资料后,我们将按照业务流程进行审核、联系确认或转交相关服务人员处理。审核结果和办理时间受资料完整性、第三方处理进度及客观情况影响,页面展示的预计时间不构成对特定完成时间的保证。', + ], + }, + { + title: '四、知识产权与内容授权', + paragraphs: [ + '本小程序的软件、界面、文字、图片及其他内容的知识产权归我们或合法权利人所有。未经许可,您不得复制、修改、传播、反向工程或用于其他商业目的。', + '您保留对所提交内容依法享有的权利。为提供申请审核、订单处理、地图标注、投诉处理和客户服务,您授权我们在实现相关服务所必要的范围和期限内处理您提交的内容。', + ], + }, + { + title: '五、服务变更、中断与终止', + paragraphs: [ + '因系统维护、网络故障、第三方服务异常、不可抗力或法律政策变化,服务可能暂时中断。我们将采取合理措施减少影响。您违反本协议、法律法规或损害他人权益时,我们有权限制或终止向您提供服务,并保留依法追究责任的权利。', + ], + }, + { + title: '六、责任说明', + paragraphs: [ + '我们将在法律允许的范围内保障服务稳定和信息安全,但不对因用户自身原因、第三方原因、不可抗力或现有技术无法防范的事件造成的损失承担超出法定范围的责任。本条不排除或限制法律规定不得排除或限制的责任。', + ], + }, + { + title: '七、法律适用与争议解决', + paragraphs: [ + '本协议的订立、履行和解释适用中华人民共和国大陆地区法律。因本协议产生争议时,双方应先友好协商;协商不成的,可依法向有管辖权的人民法院提起诉讼。', + ], + }, + { + title: '八、联系我们', + paragraphs: [ + '如您对本协议或服务有疑问,可通过本小程序首页“客服”入口联系我们。我们会在核实相关情况后尽快处理。', + ], + }, + ], +} + +const PRIVACY_POLICY: Agreement = { + title: '隐私政策', + date: '生效日期:2026年8月31日', + intro: '“敌无双企业服务”小程序运营者(以下简称“我们”)重视您的个人信息和隐私安全。本政策说明我们在提供订单查询、资料提交、地图选点、企业申报、投诉及客户服务时,如何收集、使用、存储、共享和保护信息,以及您如何行使相关权利。请您在勾选同意前完整阅读。', + sections: [ + { + title: '一、我们收集的信息及目的', + paragraphs: [ + '1. 联系电话:由您在查询框或表单中主动输入,或由业务跳转链接携带。我们使用联系电话匹配和展示关联订单、核验申请、联系办理人、反馈处理进度及提供客户服务。未经您勾选同意,我们不会以该号码发起订单查询或提交表单。', + '2. 门店及业务资料:由您主动填写或上传,包括门店名称、地址、地图坐标、门店电话、办理人电话、门头照片和营业执照图片。我们仅用于审核申请资料、确认门店真实性、完成地图标注、生成相关业务展示页面及与您联系。', + '3. 企业申报资料:由您主动填写、查询并选择,包括企业名称、统一社会信用代码、法定代表人姓名、法定代表人身份证号和手机号。身份证号属于敏感个人信息,仅在核验申报主体身份和办理相关业务确有必要时使用。处理敏感个人信息可能对个人权益产生较大影响,请在确认必要后再提交。', + '4. 位置信息:当您使用定位、搜索地点或地图选点功能时,经您授权后通过微信定位能力获取当前位置或由您主动选择地点,并可能将坐标发送至地图服务进行地址解析。该信息仅用于帮助您确定门店位置和填写门店地址。您可以拒绝定位并手动在地图上选点,但部分便捷定位功能将无法使用。', + '5. 投诉与反馈信息:由您主动提交,包括投诉类型、问题描述、图片以及选填的手机号、微信号或邮箱。我们用于受理、核实、联系和处理投诉。', + '6. 设备、网络与日志信息:为建立临时登录状态、保障接口安全和排查故障,在您同意本政策并使用联网服务后,我们可能自动收集设备标识符、IP地址、平台类型、应用版本、访问时间、请求记录和异常日志。上述信息用于身份校验、反作弊、网络与数据安全,不用于与本服务无关的营销。', + ], + }, + { + title: '二、信息的收集方式和使用范围', + paragraphs: [ + '我们通过您主动输入、选择、上传、业务跳转链接传递,以及在获得系统权限后调用定位能力等方式收集信息。我们仅在实现本政策所述功能所必需的范围内使用,不会将信息用于未向您说明的其他目的。若使用目的发生实质变化,我们将再次告知并依法重新取得您的同意。', + '您可以不提供非必需信息。必需信息缺失时,相应的订单查询、资料审核、企业申报或投诉联系功能可能无法完成,但不影响您浏览协议等无需该信息的功能。', + ], + }, + { + title: '三、信息存储与保护', + paragraphs: [ + '我们将在中华人民共和国境内存储收集的信息,并仅在实现业务目的所需的最短期限或法律法规要求的期限内保留。保存期限届满后,我们将依法删除或匿名化处理;法律法规另有规定的除外。', + '我们采取访问控制、身份鉴别、传输保护、权限管理和安全审计等合理措施保护信息。请理解,互联网环境不存在绝对安全,我们会在发生个人信息安全事件时依法采取补救和告知措施。', + ], + }, + { + title: '四、委托处理、共享与公开披露', + paragraphs: [ + '为实现图片存储、地图定位与地址解析、网络基础设施及业务办理,我们可能委托云存储、地图或相关服务供应商在必要范围内处理信息,并要求其按照我们的指示和适用法律采取保护措施。', + '除取得您的单独同意、为履行您请求的服务所必需或法律法规另有规定外,我们不会向其他第三方出售、交换或共享您的个人信息。我们原则上不公开披露个人信息;确需公开披露时,将依法告知并取得相应授权。', + ], + }, + { + title: '五、您的权利', + paragraphs: [ + '您依法享有查阅、复制、更正、补充、删除个人信息,以及撤回同意、限制或拒绝处理等权利。您可以取消协议勾选以撤回后续处理授权,也可以通过首页“客服”入口提交请求。撤回同意不影响撤回前基于同意已进行处理的效力。', + '收到请求后,我们可能先核验您的身份,并在法律规定的期限内处理。对于法律法规要求保留的信息,我们将在法定保留期内停止除存储和必要安全保护之外的处理。', + ], + }, + { + title: '六、未成年人保护', + paragraphs: [ + '本服务主要面向具备相应民事行为能力的企业经营者或经办人员。未满十四周岁的未成年人不得自行提交个人信息;确有必要使用时,应由监护人阅读并同意本政策。', + ], + }, + { + title: '七、政策更新', + paragraphs: [ + '我们可能根据服务或法律要求更新本政策。发生对您权益有重大影响的变更时,我们会通过页面提示等显著方式告知,并在依法需要时重新取得您的同意。', + ], + }, + { + title: '八、联系我们', + paragraphs: [ + '如您对本政策、个人信息处理或权利请求有疑问,可通过本小程序首页“客服”入口联系我们。我们会在核实您的身份和请求后尽快答复。', + ], + }, + ], +} + +export default function AgreementPage () { + const [agreement, setAgreement] = useState(SERVICE_AGREEMENT) + + useLoad((options) => { + const next = options?.type === 'privacy' ? PRIVACY_POLICY : SERVICE_AGREEMENT + setAgreement(next) + Taro.setNavigationBarTitle({ title: next.title }) + }) + + return ( + + {agreement.title} + {agreement.date} + {agreement.intro} + {agreement.sections.map((section) => ( + + {section.title} + {section.paragraphs.map((paragraph) => ( + {paragraph} + ))} + + ))} + + ) +} diff --git a/src/pages/index/index.tsx b/src/pages/index/index.tsx index bba880d..92b1bfb 100644 --- a/src/pages/index/index.tsx +++ b/src/pages/index/index.tsx @@ -5,7 +5,6 @@ import { getOrdersByPhone } from '@/api/pay' import { LOCAL_IMAGES } from '@/lib/assets' import { bootstrapSession } from '@/lib/bootstrap' import { COPY_ICON, PAGE_TITLE } from '@/lib/cdn' -import { isCorpGoodsType } from '@/lib/detail' import { getNavMetrics } from '@/lib/nav' import { pickServiceContact, type ServiceContact } from '@/lib/contact' import { setOrderDraft } from '@/lib/orderDraft' @@ -240,7 +239,7 @@ function OrderCard ({ ) : null} {actions.submit ? ( - {isCorpGoodsType(order.goods_type) ? '重新提交' : '补充资料'} + 补充资料 ) : null} diff --git a/src/pages/resubmit/index.scss b/src/pages/resubmit/index.scss index 0510608..3228037 100644 --- a/src/pages/resubmit/index.scss +++ b/src/pages/resubmit/index.scss @@ -17,6 +17,10 @@ padding: 24px 24px calc(172px + env(safe-area-inset-bottom)); } +.resubmit-page__body--consent { + padding-bottom: calc(208px + env(safe-area-inset-bottom)); +} + .resubmit-card { box-sizing: border-box; width: 702px; @@ -211,6 +215,11 @@ color: #333333; } +.resubmit__privacy { + justify-content: center; + margin-bottom: 16px; +} + .resubmit__error { display: block; margin-top: 16px; @@ -228,6 +237,10 @@ background: #f7f8fa; } +.resubmit-page__footer--consent { + padding-top: 20px; +} + .resubmit__submit { display: flex; align-items: center; diff --git a/src/pages/resubmit/index.tsx b/src/pages/resubmit/index.tsx index 39e62ad..486d224 100644 --- a/src/pages/resubmit/index.tsx +++ b/src/pages/resubmit/index.tsx @@ -1,10 +1,12 @@ import { Button, Image, Input, Text, View } from '@tarojs/components' import Taro, { useDidShow, useLoad } from '@tarojs/taro' import { useState } from 'react' +import PrivacyConsent from '@/components/privacy-consent' import { updateOrderExtra } from '@/api/pay' import { uploadImage } from '@/api/user' import { LOCAL_IMAGES } from '@/lib/assets' import { ENTITY_LABELS, HOME_ICON_BASE, INPUT_ARROW_ICON, MAP_ASSET_BASE } from '@/lib/cdn' +import { hasPrivacyConsent, savePrivacyConsent } from '@/lib/consent' import { isCorpGoodsType } from '@/lib/detail' import { getOrderDraft } from '@/lib/orderDraft' import { isValidPhone } from '@/lib/phone' @@ -138,6 +140,7 @@ export default function ResubmitPage () { const [uploadProgress, setUploadProgress] = useState(0) const [formError, setFormError] = useState('') const [submitting, setSubmitting] = useState(false) + const [agreed, setAgreed] = useState(hasPrivacyConsent) useLoad(() => { const draft = getOrderDraft() @@ -174,6 +177,17 @@ export default function ResubmitPage () { setValues((current) => ({ ...current, [field]: value })) } + const requireConsent = () => { + if (agreed) return true + Taro.showToast({ title: '请先阅读并同意服务协议和隐私政策', icon: 'none' }) + return false + } + + const handleConsentChange = (nextAgreed: boolean) => { + setAgreed(nextAgreed) + savePrivacyConsent(nextAgreed) + } + const handleUpload = async (field: 'storefront0' | 'storefront1' | 'storefront2' | 'license', filePath: string) => { setUploadingField(field) setUploadProgress(0) @@ -213,6 +227,7 @@ export default function ResubmitPage () { const handleSubmit = async () => { if (!order || submitting) return + if (!requireConsent()) return if (uploadingField) { setFormError('图片正在上传,请稍候') return @@ -269,7 +284,7 @@ export default function ResubmitPage () { 驳回理由:{order.reject_reason} ) : null} - + @@ -381,7 +396,12 @@ export default function ResubmitPage () { {formError ? {formError} : null} - + +