用户协议默认不勾选

This commit is contained in:
2026-07-15 19:50:09 +08:00
parent 5a2e8dfcf8
commit 68aa7d3ce2
34 changed files with 821 additions and 18 deletions
+3
View File
@@ -1,6 +1,7 @@
import { Routes, Route } from 'react-router-dom'; import { Routes, Route } from 'react-router-dom';
import AuthGate from './components/AuthGate'; import AuthGate from './components/AuthGate';
import LoginPage from './pages/LoginPage'; import LoginPage from './pages/LoginPage';
import LegalPage from './pages/LegalPage';
import PartnerAppRoutes from './PartnerAppRoutes'; import PartnerAppRoutes from './PartnerAppRoutes';
export default function App() { export default function App() {
@@ -8,6 +9,8 @@ export default function App() {
<AuthGate> <AuthGate>
<Routes> <Routes>
<Route path="/login" element={<LoginPage />} /> <Route path="/login" element={<LoginPage />} />
<Route path="/legal/user-agreement" element={<LegalPage docId="user-agreement" />} />
<Route path="/legal/privacy-policy" element={<LegalPage docId="privacy-policy" />} />
<Route path="/*" element={<PartnerAppRoutes />} /> <Route path="/*" element={<PartnerAppRoutes />} />
</Routes> </Routes>
</AuthGate> </AuthGate>
+1 -1
View File
@@ -3,7 +3,7 @@ import { getPartnerProfile, hasPartnerWxSession } from '../lib/api';
import { usePartnerSession } from '../contexts/PartnerSessionContext'; import { usePartnerSession } from '../contexts/PartnerSessionContext';
import { partnerHomePath } from '../lib/partnerAccess'; import { partnerHomePath } from '../lib/partnerAccess';
const PUBLIC_PATHS = new Set(['/login']); const PUBLIC_PATHS = new Set(['/login', '/legal/user-agreement', '/legal/privacy-policy']);
export default function AuthGate({ children }: { children: React.ReactNode }) { export default function AuthGate({ children }: { children: React.ReactNode }) {
const { ready, authenticated, account } = usePartnerSession(); const { ready, authenticated, account } = usePartnerSession();
+1
View File
@@ -6,6 +6,7 @@ import App from './App';
import { PartnerSessionProvider } from './contexts/PartnerSessionContext'; import { PartnerSessionProvider } from './contexts/PartnerSessionContext';
import { PartnerToastProvider } from './contexts/PartnerToastContext'; import { PartnerToastProvider } from './contexts/PartnerToastContext';
import './styles.css'; import './styles.css';
import './styles/legal.css';
ReactDOM.createRoot(document.getElementById('root')!).render( ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode> <React.StrictMode>
+36
View File
@@ -0,0 +1,36 @@
import { Link } from 'react-router-dom';
import { getLegalDocument, type LegalDocument } from '@dukang/shared-types';
type LegalPageProps = {
docId: LegalDocument['id'];
/** 返回登录页的路径,如 /login */
backTo?: string;
};
/** H5 各端共用的协议/隐私正文页 */
export default function LegalPage({ docId, backTo = '/login' }: LegalPageProps) {
const doc = getLegalDocument(docId);
return (
<div className="legal-h5-page">
<header className="legal-h5-header">
<Link to={backTo} className="legal-h5-back" aria-label="返回">
</Link>
<h1 className="legal-h5-title">{doc.title}</h1>
</header>
<main className="legal-h5-body">
<p className="legal-h5-updated">{doc.updatedAt}</p>
<p className="legal-h5-intro">{doc.intro}</p>
{doc.sections.map((section) => (
<section key={section.heading} className="legal-h5-section">
<h2>{section.heading}</h2>
{section.paragraphs.map((p, i) => (
<p key={`${section.heading}-${i}`}>{p}</p>
))}
</section>
))}
</main>
</div>
);
}
+9 -2
View File
@@ -68,7 +68,7 @@ export default function LoginPage() {
const [phone, setPhone] = useState(remembered.phone || getLastPhone()); const [phone, setPhone] = useState(remembered.phone || getLastPhone());
const [code, setCode] = useState(''); const [code, setCode] = useState('');
const [rememberAccount, setRememberAccount] = useState(remembered.remember); const [rememberAccount, setRememberAccount] = useState(remembered.remember);
const [agreed, setAgreed] = useState(true); const [agreed, setAgreed] = useState(false);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [wxLoading, setWxLoading] = useState(false); const [wxLoading, setWxLoading] = useState(false);
const [msg, setMsg] = useState(''); const [msg, setMsg] = useState('');
@@ -343,7 +343,14 @@ export default function LoginPage() {
<label className="partner-checkbox-row"> <label className="partner-checkbox-row">
<input type="checkbox" checked={agreed} onChange={(e) => setAgreed(e.target.checked)} /> <input type="checkbox" checked={agreed} onChange={(e) => setAgreed(e.target.checked)} />
<span> <span>
<span className="text-primary" style={{ fontWeight: 600 }}></span> <span className="text-primary" style={{ fontWeight: 600 }}></span> {' '}
<Link to="/legal/user-agreement" className="text-primary" style={{ fontWeight: 600 }} onClick={(e) => e.stopPropagation()}>
</Link>{' '}
{' '}
<Link to="/legal/privacy-policy" className="text-primary" style={{ fontWeight: 600 }} onClick={(e) => e.stopPropagation()}>
</Link>
</span> </span>
</label> </label>
</div> </div>
+75
View File
@@ -0,0 +1,75 @@
.legal-h5-page {
min-height: 100vh;
background: #faf9f7;
color: #1f1a17;
}
.legal-h5-header {
position: sticky;
top: 0;
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
height: 52px;
padding: 0 16px;
background: #faf9f7;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
}
.legal-h5-back {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
line-height: 1;
color: #1f1a17;
text-decoration: none;
}
.legal-h5-title {
margin: 0;
font-size: 17px;
font-weight: 600;
}
.legal-h5-body {
padding: 16px 20px 40px;
max-width: 720px;
margin: 0 auto;
}
.legal-h5-updated {
margin: 0 0 12px;
font-size: 12px;
color: #8d706e;
}
.legal-h5-intro {
margin: 0 0 20px;
font-size: 14px;
line-height: 1.7;
}
.legal-h5-section {
margin-bottom: 18px;
}
.legal-h5-section h2 {
margin: 0 0 8px;
font-size: 15px;
font-weight: 600;
}
.legal-h5-section p {
margin: 0 0 8px;
font-size: 13px;
line-height: 1.7;
color: #5c504c;
}
+3
View File
@@ -2,6 +2,7 @@ import { Routes, Route, Navigate } from 'react-router-dom';
import AuthGate from './components/AuthGate'; import AuthGate from './components/AuthGate';
import TabLayout from './layouts/TabLayout'; import TabLayout from './layouts/TabLayout';
import LoginPage from './pages/LoginPage'; import LoginPage from './pages/LoginPage';
import LegalPage from './pages/LegalPage';
import SelectStorePage from './pages/SelectStorePage'; import SelectStorePage from './pages/SelectStorePage';
import HomePage from './pages/HomePage'; import HomePage from './pages/HomePage';
import RedeemConfirmPage from './pages/RedeemConfirmPage'; import RedeemConfirmPage from './pages/RedeemConfirmPage';
@@ -17,6 +18,8 @@ export default function App() {
<AuthGate> <AuthGate>
<Routes> <Routes>
<Route path="/login" element={<LoginPage />} /> <Route path="/login" element={<LoginPage />} />
<Route path="/legal/user-agreement" element={<LegalPage docId="user-agreement" />} />
<Route path="/legal/privacy-policy" element={<LegalPage docId="privacy-policy" />} />
<Route path="/select-store" element={<SelectStorePage />} /> <Route path="/select-store" element={<SelectStorePage />} />
<Route path="/staff" element={<StaffPage />} /> <Route path="/staff" element={<StaffPage />} />
<Route path="/redeem" element={<RedeemConfirmPage />} /> <Route path="/redeem" element={<RedeemConfirmPage />} />
+1 -1
View File
@@ -2,7 +2,7 @@ import { Navigate, useLocation } from 'react-router-dom';
import { getStoreProfile, hasShopWxSession } from '../lib/api'; import { getStoreProfile, hasShopWxSession } from '../lib/api';
import { useStoreSession } from '../contexts/StoreSessionContext'; import { useStoreSession } from '../contexts/StoreSessionContext';
const PUBLIC_PATHS = new Set(['/login']); const PUBLIC_PATHS = new Set(['/login', '/legal/user-agreement', '/legal/privacy-policy']);
const SELECT_STORE_PATH = '/select-store'; const SELECT_STORE_PATH = '/select-store';
export default function AuthGate({ children }: { children: React.ReactNode }) { export default function AuthGate({ children }: { children: React.ReactNode }) {
+1
View File
@@ -5,6 +5,7 @@ import { getRouterBasename } from '@dukang/weixin-sdk';
import { StoreSessionProvider } from './contexts/StoreSessionContext'; import { StoreSessionProvider } from './contexts/StoreSessionContext';
import App from './App'; import App from './App';
import './styles.css'; import './styles.css';
import './styles/legal.css';
ReactDOM.createRoot(document.getElementById('root')!).render( ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode> <React.StrictMode>
+36
View File
@@ -0,0 +1,36 @@
import { Link } from 'react-router-dom';
import { getLegalDocument, type LegalDocument } from '@dukang/shared-types';
type LegalPageProps = {
docId: LegalDocument['id'];
/** 返回登录页的路径,如 /login */
backTo?: string;
};
/** H5 各端共用的协议/隐私正文页 */
export default function LegalPage({ docId, backTo = '/login' }: LegalPageProps) {
const doc = getLegalDocument(docId);
return (
<div className="legal-h5-page">
<header className="legal-h5-header">
<Link to={backTo} className="legal-h5-back" aria-label="返回">
</Link>
<h1 className="legal-h5-title">{doc.title}</h1>
</header>
<main className="legal-h5-body">
<p className="legal-h5-updated">{doc.updatedAt}</p>
<p className="legal-h5-intro">{doc.intro}</p>
{doc.sections.map((section) => (
<section key={section.heading} className="legal-h5-section">
<h2>{section.heading}</h2>
{section.paragraphs.map((p, i) => (
<p key={`${section.heading}-${i}`}>{p}</p>
))}
</section>
))}
</main>
</div>
);
}
+7 -3
View File
@@ -36,7 +36,7 @@ export default function LoginPage() {
const savedProfile = getStoreProfile(); const savedProfile = getStoreProfile();
const [phone, setPhone] = useState(getLastPhone()); const [phone, setPhone] = useState(getLastPhone());
const [code, setCode] = useState(''); const [code, setCode] = useState('');
const [agreed, setAgreed] = useState(true); const [agreed, setAgreed] = useState(false);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [wxLoading, setWxLoading] = useState(false); const [wxLoading, setWxLoading] = useState(false);
const [msg, setMsg] = useState(''); const [msg, setMsg] = useState('');
@@ -309,9 +309,13 @@ export default function LoginPage() {
/> />
<span> <span>
<a href="#user-agreement"></a> <Link to="/legal/user-agreement" onClick={(e) => e.stopPropagation()}>
</Link>
<a href="#privacy"></a> <Link to="/legal/privacy-policy" onClick={(e) => e.stopPropagation()}>
</Link>
</span> </span>
</label> </label>
</main> </main>
+75
View File
@@ -0,0 +1,75 @@
.legal-h5-page {
min-height: 100vh;
background: #faf9f7;
color: #1f1a17;
}
.legal-h5-header {
position: sticky;
top: 0;
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
height: 52px;
padding: 0 16px;
background: #faf9f7;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
}
.legal-h5-back {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
line-height: 1;
color: #1f1a17;
text-decoration: none;
}
.legal-h5-title {
margin: 0;
font-size: 17px;
font-weight: 600;
}
.legal-h5-body {
padding: 16px 20px 40px;
max-width: 720px;
margin: 0 auto;
}
.legal-h5-updated {
margin: 0 0 12px;
font-size: 12px;
color: #8d706e;
}
.legal-h5-intro {
margin: 0 0 20px;
font-size: 14px;
line-height: 1.7;
}
.legal-h5-section {
margin-bottom: 18px;
}
.legal-h5-section h2 {
margin: 0 0 8px;
font-size: 15px;
font-weight: 600;
}
.legal-h5-section p {
margin: 0 0 8px;
font-size: 13px;
line-height: 1.7;
color: #5c504c;
}
+3
View File
@@ -2,6 +2,7 @@ import { Routes, Route, Navigate } from 'react-router-dom';
import { useEffect } from 'react'; import { useEffect } from 'react';
import TabLayout from './layouts/TabLayout'; import TabLayout from './layouts/TabLayout';
import LoginPage from './pages/LoginPage'; import LoginPage from './pages/LoginPage';
import LegalPage from './pages/LegalPage';
import HomePage from './pages/HomePage'; import HomePage from './pages/HomePage';
import ProductDetailPage from './pages/ProductDetailPage'; import ProductDetailPage from './pages/ProductDetailPage';
import OrderConfirmPage from './pages/OrderConfirmPage'; import OrderConfirmPage from './pages/OrderConfirmPage';
@@ -37,6 +38,8 @@ export default function App() {
<WechatShareBootstrap /> <WechatShareBootstrap />
<Routes> <Routes>
<Route path="/login" element={<LoginPage />} /> <Route path="/login" element={<LoginPage />} />
<Route path="/legal/user-agreement" element={<LegalPage docId="user-agreement" />} />
<Route path="/legal/privacy-policy" element={<LegalPage docId="privacy-policy" />} />
<Route element={<TabLayout />}> <Route element={<TabLayout />}>
<Route path="/" element={<HomePage />} /> <Route path="/" element={<HomePage />} />
<Route path="/stores" element={<StoreListPage />} /> <Route path="/stores" element={<StoreListPage />} />
+1
View File
@@ -4,6 +4,7 @@ import { BrowserRouter } from 'react-router-dom';
import { getRouterBasename } from '@dukang/weixin-sdk'; import { getRouterBasename } from '@dukang/weixin-sdk';
import App from './App'; import App from './App';
import './styles.css'; import './styles.css';
import './styles/legal.css';
ReactDOM.createRoot(document.getElementById('root')!).render( ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode> <React.StrictMode>
+36
View File
@@ -0,0 +1,36 @@
import { Link } from 'react-router-dom';
import { getLegalDocument, type LegalDocument } from '@dukang/shared-types';
type LegalPageProps = {
docId: LegalDocument['id'];
/** 返回登录页的路径,如 /login */
backTo?: string;
};
/** H5 各端共用的协议/隐私正文页 */
export default function LegalPage({ docId, backTo = '/login' }: LegalPageProps) {
const doc = getLegalDocument(docId);
return (
<div className="legal-h5-page">
<header className="legal-h5-header">
<Link to={backTo} className="legal-h5-back" aria-label="返回">
</Link>
<h1 className="legal-h5-title">{doc.title}</h1>
</header>
<main className="legal-h5-body">
<p className="legal-h5-updated">{doc.updatedAt}</p>
<p className="legal-h5-intro">{doc.intro}</p>
{doc.sections.map((section) => (
<section key={section.heading} className="legal-h5-section">
<h2>{section.heading}</h2>
{section.paragraphs.map((p, i) => (
<p key={`${section.heading}-${i}`}>{p}</p>
))}
</section>
))}
</main>
</div>
);
}
+8 -4
View File
@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom'; import { useNavigate, useSearchParams, Link } from 'react-router-dom';
import AppImage from '@dukang/shared-ui/AppImage'; import AppImage from '@dukang/shared-ui/AppImage';
import type { WechatLoginResult } from '@dukang/shared-types'; import type { WechatLoginResult } from '@dukang/shared-types';
import { SmsScene } from '@dukang/shared-types'; import { SmsScene } from '@dukang/shared-types';
@@ -26,7 +26,7 @@ export default function LoginPage() {
const [phone, setPhone] = useState(''); const [phone, setPhone] = useState('');
const [code, setCode] = useState(''); const [code, setCode] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [agreed, setAgreed] = useState(true); const [agreed, setAgreed] = useState(false);
const [msg, setMsg] = useState(''); const [msg, setMsg] = useState('');
const [wxSessionKey, setWxSessionKey] = useState<string | null>(null); const [wxSessionKey, setWxSessionKey] = useState<string | null>(null);
const [bindMode, setBindMode] = useState(false); const [bindMode, setBindMode] = useState(false);
@@ -229,9 +229,13 @@ export default function LoginPage() {
/> />
<span> <span>
<a href="#user-agreement"></a> <Link to="/legal/user-agreement" onClick={(e) => e.stopPropagation()}>
</Link>
<a href="#privacy"></a> <Link to="/legal/privacy-policy" onClick={(e) => e.stopPropagation()}>
</Link>
</span> </span>
</label> </label>
</footer> </footer>
+75
View File
@@ -0,0 +1,75 @@
.legal-h5-page {
min-height: 100vh;
background: #faf9f7;
color: #1f1a17;
}
.legal-h5-header {
position: sticky;
top: 0;
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
height: 52px;
padding: 0 16px;
background: #faf9f7;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
}
.legal-h5-back {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
line-height: 1;
color: #1f1a17;
text-decoration: none;
}
.legal-h5-title {
margin: 0;
font-size: 17px;
font-weight: 600;
}
.legal-h5-body {
padding: 16px 20px 40px;
max-width: 720px;
margin: 0 auto;
}
.legal-h5-updated {
margin: 0 0 12px;
font-size: 12px;
color: #8d706e;
}
.legal-h5-intro {
margin: 0 0 20px;
font-size: 14px;
line-height: 1.7;
}
.legal-h5-section {
margin-bottom: 18px;
}
.legal-h5-section h2 {
margin: 0 0 8px;
font-size: 15px;
font-weight: 600;
}
.legal-h5-section p {
margin: 0 0 8px;
font-size: 13px;
line-height: 1.7;
color: #5c504c;
}
+2
View File
@@ -5,6 +5,8 @@ export default defineAppConfig({
'pages/settlement/index', 'pages/settlement/index',
'pages/tickets/index', 'pages/tickets/index',
'pages/login/index', 'pages/login/index',
'pages/user-agreement/index',
'pages/privacy-policy/index',
'pages/stores/detail', 'pages/stores/detail',
'pages/orders/index', 'pages/orders/index',
'pages/orders/detail', 'pages/orders/detail',
+19 -3
View File
@@ -43,7 +43,7 @@ export default function LoginPage() {
const [phone, setPhone] = useState(remembered.phone || DEMO_PHONE); const [phone, setPhone] = useState(remembered.phone || DEMO_PHONE);
const [code, setCode] = useState('123456'); const [code, setCode] = useState('123456');
const [rememberAccount, setRememberAccount] = useState(remembered.remember); const [rememberAccount, setRememberAccount] = useState(remembered.remember);
const [agreed, setAgreed] = useState(true); const [agreed, setAgreed] = useState(false);
const [cooldown, setCooldown] = useState(0); const [cooldown, setCooldown] = useState(0);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [wxLoading, setWxLoading] = useState(false); const [wxLoading, setWxLoading] = useState(false);
@@ -234,9 +234,25 @@ export default function LoginPage() {
<View className={`login-checkbox${agreed ? ' is-checked' : ''}`} /> <View className={`login-checkbox${agreed ? ' is-checked' : ''}`} />
<Text className="login-agreement-text"> <Text className="login-agreement-text">
<Text className="login-agreement-link"></Text> <Text
className="login-agreement-link"
onClick={(e) => {
e.stopPropagation();
Taro.navigateTo({ url: '/pages/user-agreement/index' });
}}
>
</Text>
<Text className="login-agreement-link"></Text> <Text
className="login-agreement-link"
onClick={(e) => {
e.stopPropagation();
Taro.navigateTo({ url: '/pages/privacy-policy/index' });
}}
>
</Text>
</Text> </Text>
</View> </View>
</View> </View>
@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '隐私政策',
});
@@ -0,0 +1,32 @@
import { View, Text, ScrollView } from '@tarojs/components';
import Taro from '@tarojs/taro';
import { getLegalDocument } from '@dukang/shared-types';
import '../user-agreement/legal.css';
export default function PrivacyPolicyPage() {
const doc = getLegalDocument('privacy-policy');
return (
<View className="hq-legal-page">
<View className="hq-legal-header">
<Text className="hq-legal-back" onClick={() => Taro.navigateBack()}>
</Text>
<Text className="hq-legal-title">{doc.title}</Text>
</View>
<ScrollView scrollY className="hq-legal-body">
<Text className="hq-legal-updated">{doc.updatedAt}</Text>
<Text className="hq-legal-intro">{doc.intro}</Text>
{doc.sections.map((section) => (
<View key={section.heading} className="hq-legal-section">
<Text className="hq-legal-heading">{section.heading}</Text>
{section.paragraphs.map((p, i) => (
<Text key={`${section.heading}-${i}`} className="hq-legal-paragraph">
{p}
</Text>
))}
</View>
))}
</ScrollView>
</View>
);
}
@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '用户协议',
});
@@ -0,0 +1,32 @@
import { View, Text, ScrollView } from '@tarojs/components';
import Taro from '@tarojs/taro';
import { getLegalDocument } from '@dukang/shared-types';
import './legal.css';
export default function UserAgreementPage() {
const doc = getLegalDocument('user-agreement');
return (
<View className="hq-legal-page">
<View className="hq-legal-header">
<Text className="hq-legal-back" onClick={() => Taro.navigateBack()}>
</Text>
<Text className="hq-legal-title">{doc.title}</Text>
</View>
<ScrollView scrollY className="hq-legal-body">
<Text className="hq-legal-updated">{doc.updatedAt}</Text>
<Text className="hq-legal-intro">{doc.intro}</Text>
{doc.sections.map((section) => (
<View key={section.heading} className="hq-legal-section">
<Text className="hq-legal-heading">{section.heading}</Text>
{section.paragraphs.map((p, i) => (
<Text key={`${section.heading}-${i}`} className="hq-legal-paragraph">
{p}
</Text>
))}
</View>
))}
</ScrollView>
</View>
);
}
@@ -0,0 +1,68 @@
.hq-legal-page {
min-height: 100vh;
background: #faf9f7;
}
.hq-legal-header {
position: sticky;
top: 0;
z-index: 10;
height: 52px;
display: flex;
align-items: center;
justify-content: center;
background: #faf9f7;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
}
.hq-legal-back {
position: absolute;
left: 12px;
font-size: 28px;
line-height: 1;
padding: 4px 8px;
}
.hq-legal-title {
font-size: 17px;
font-weight: 600;
}
.hq-legal-body {
height: calc(100vh - 52px);
padding: 16px 20px 40px;
box-sizing: border-box;
}
.hq-legal-updated {
display: block;
font-size: 12px;
color: #8d706e;
margin-bottom: 12px;
}
.hq-legal-intro {
display: block;
font-size: 14px;
line-height: 1.7;
margin-bottom: 20px;
}
.hq-legal-section {
margin-bottom: 18px;
}
.hq-legal-heading {
display: block;
font-size: 15px;
font-weight: 600;
margin-bottom: 8px;
}
.hq-legal-paragraph {
display: block;
font-size: 13px;
line-height: 1.7;
color: #5c504c;
margin-bottom: 8px;
}
+2
View File
@@ -18,6 +18,8 @@ export default defineAppConfig({
'pages/redeem-code/index', 'pages/redeem-code/index',
'pages/redeem-success/index', 'pages/redeem-success/index',
'pages/login/index', 'pages/login/index',
'pages/user-agreement/index',
'pages/privacy-policy/index',
], ],
window: { window: {
backgroundTextStyle: 'light', backgroundTextStyle: 'light',
+1
View File
@@ -8,6 +8,7 @@
@import './styles/product-detail.css'; @import './styles/product-detail.css';
@import './styles/store-detail.css'; @import './styles/store-detail.css';
@import './styles/login.css'; @import './styles/login.css';
@import './styles/legal.css';
@import './styles/order.css'; @import './styles/order.css';
@import './styles/address.css'; @import './styles/address.css';
@import './styles/redeem.css'; @import './styles/redeem.css';
+20 -4
View File
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { View, Text, Input, Button, Image } from '@tarojs/components'; import { View, Text, Input, Button, Image } from '@tarojs/components';
import { useRouter } from '@tarojs/taro'; import Taro, { useRouter } from '@tarojs/taro';
import { import {
SmsScene, SmsScene,
isWxAuthorizeEnabled, isWxAuthorizeEnabled,
@@ -47,7 +47,7 @@ export default function LoginPage() {
const [wxLoading, setWxLoading] = useState(false); const [wxLoading, setWxLoading] = useState(false);
const [sending, setSending] = useState(false); const [sending, setSending] = useState(false);
const [cooldown, setCooldown] = useState(0); const [cooldown, setCooldown] = useState(0);
const [agreed, setAgreed] = useState(true); const [agreed, setAgreed] = useState(false);
const [msg, setMsg] = useState(''); const [msg, setMsg] = useState('');
const [sentHint, setSentHint] = useState(''); const [sentHint, setSentHint] = useState('');
const [bindMode, setBindMode] = useState(initialBindMode); const [bindMode, setBindMode] = useState(initialBindMode);
@@ -387,9 +387,25 @@ export default function LoginPage() {
</View> </View>
<Text className="login-agreement-text"> <Text className="login-agreement-text">
<Text className="login-agreement-link"></Text> <Text
className="login-agreement-link"
onClick={(e) => {
e.stopPropagation();
Taro.navigateTo({ url: '/pages/user-agreement/index' });
}}
>
</Text>
<Text className="login-agreement-link"></Text> <Text
className="login-agreement-link"
onClick={(e) => {
e.stopPropagation();
Taro.navigateTo({ url: '/pages/privacy-policy/index' });
}}
>
</Text>
</Text> </Text>
</View> </View>
</View> </View>
@@ -0,0 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '隐私政策',
navigationStyle: 'custom',
});
@@ -0,0 +1,29 @@
import { View, Text, ScrollView } from '@tarojs/components';
import { getLegalDocument } from '@dukang/shared-types';
import PageShell from '../../components/PageShell';
import SubPageHeader from '../../components/SubPageHeader';
export default function PrivacyPolicyPage() {
const doc = getLegalDocument('privacy-policy');
return (
<PageShell variant="sub" className="legal-page">
<SubPageHeader title={doc.title} />
<View className="sub-page-body inset-page">
<ScrollView scrollY className="legal-scroll">
<Text className="legal-updated">{doc.updatedAt}</Text>
<Text className="legal-intro">{doc.intro}</Text>
{doc.sections.map((section) => (
<View key={section.heading} className="legal-section">
<Text className="legal-heading">{section.heading}</Text>
{section.paragraphs.map((p, i) => (
<Text key={`${section.heading}-${i}`} className="legal-paragraph">
{p}
</Text>
))}
</View>
))}
</ScrollView>
</View>
</PageShell>
);
}
@@ -0,0 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '用户协议',
navigationStyle: 'custom',
});
@@ -0,0 +1,29 @@
import { View, Text, ScrollView } from '@tarojs/components';
import { getLegalDocument } from '@dukang/shared-types';
import PageShell from '../../components/PageShell';
import SubPageHeader from '../../components/SubPageHeader';
export default function UserAgreementPage() {
const doc = getLegalDocument('user-agreement');
return (
<PageShell variant="sub" className="legal-page">
<SubPageHeader title={doc.title} />
<View className="sub-page-body inset-page">
<ScrollView scrollY className="legal-scroll">
<Text className="legal-updated">{doc.updatedAt}</Text>
<Text className="legal-intro">{doc.intro}</Text>
{doc.sections.map((section) => (
<View key={section.heading} className="legal-section">
<Text className="legal-heading">{section.heading}</Text>
{section.paragraphs.map((p, i) => (
<Text key={`${section.heading}-${i}`} className="legal-paragraph">
{p}
</Text>
))}
</View>
))}
</ScrollView>
</View>
</PageShell>
);
}
+40
View File
@@ -0,0 +1,40 @@
.legal-page .legal-scroll {
height: 100%;
box-sizing: border-box;
padding-bottom: 32px;
}
.legal-updated {
display: block;
font-size: 12px;
color: var(--color-on-surface-variant, #8d706e);
margin-bottom: 12px;
}
.legal-intro {
display: block;
font-size: 14px;
line-height: 1.7;
color: var(--color-on-surface, #1f1a17);
margin-bottom: 20px;
}
.legal-section {
margin-bottom: 18px;
}
.legal-heading {
display: block;
font-size: 15px;
font-weight: 600;
color: var(--color-on-surface, #1f1a17);
margin-bottom: 8px;
}
.legal-paragraph {
display: block;
font-size: 13px;
line-height: 1.7;
color: var(--color-on-surface-variant, #5c504c);
margin-bottom: 8px;
}
+1
View File
@@ -19,3 +19,4 @@ export * from './shop';
export * from './city-partner'; export * from './city-partner';
export * from './city-warehouse'; export * from './city-warehouse';
export * from './system-config'; export * from './system-config';
export * from './legal';
+161
View File
@@ -0,0 +1,161 @@
import { CUSTOMER_SERVICE_PHONE } from './config';
export type LegalSection = {
heading: string;
paragraphs: string[];
};
export type LegalDocument = {
id: 'user-agreement' | 'privacy-policy';
title: string;
updatedAt: string;
intro: string;
sections: LegalSection[];
};
/**
* 杜康好客 · 用户协议 / 隐私政策(多端共用)。
* 结构对齐《个人信息保护法》及 App 合规常见披露要点。
* 参考:小米开发者「隐私政策不合规修改指引」、FreeBuf APP 隐私合规规范(禁止默认勾选等)。
* 正式上线前请法务审定主体名称与联系方式。
*/
export const USER_AGREEMENT: LegalDocument = {
id: 'user-agreement',
title: '用户协议',
updatedAt: '2026-07-15',
intro:
'欢迎使用「杜康好客」平台(含微信小程序、微信内置浏览器 H5 及门店端/合伙人端相关服务,以下统称「本平台」)。请您在注册、登录或以其他方式使用本服务前仔细阅读并充分理解本协议。您勾选同意并继续使用,即视为已阅读并接受本协议全部内容。',
sections: [
{
heading: '一、服务说明',
paragraphs: [
'杜康好客是杜康酒业 O2O 消费服务平台:用户可在线浏览与购买酒类商品,获得对应「好客权益」并在合作门店核销;门店与城市合伙人可使用管理端完成核销、门店与订单相关履约操作。',
'我们有权根据业务需要调整服务内容、功能或规则,并在合理范围内通过平台公告、页面提示等方式通知您。',
],
},
{
heading: '二、账号注册与安全',
paragraphs: [
'您可通过手机号验证码、微信授权等方式注册或登录。您应保证提供的信息真实、准确、完整,并及时更新。',
'您应妥善保管账号、验证码及设备。因您自身原因导致的账号被盗用、信息泄露等风险,由您自行承担;如发现异常请立即联系客服。',
'您不得利用本平台从事违法违规、侵害他人权益或扰乱平台秩序的行为,否则我们有权限制或终止服务。',
],
},
{
heading: '三、订单、支付与权益',
paragraphs: [
'下单、支付、配送及「好客权益」发放/核销规则以平台页面展示及相关业务规则为准。支付成功后产生的权益额度按产品说明计入您的账户。',
'核销须在合作门店按规则完成;超出可用余额、已失效或违反使用规则的核销请求将被拒绝。',
'如发生退款、补发等售后事宜,将按平台售后规则及客服处理结果执行。',
],
},
{
heading: '四、用户行为规范',
paragraphs: [
'您承诺依法使用本平台,不得利用技术手段恶意刷单、伪造核销、攻击系统或传播违法信息。',
'您理解并同意:酒类商品及相关服务可能仅面向符合法律法规要求的主体;若您不具备相应资格,请勿使用购买或核销功能。',
],
},
{
heading: '五、知识产权',
paragraphs: [
'本平台中的文字、图片、标识、界面设计、软件等知识产权归平台运营方或合法权利人所有。未经许可,您不得擅自复制、传播或用于商业目的。',
],
},
{
heading: '六、免责与责任限制',
paragraphs: [
'因不可抗力、网络故障、第三方支付或配送服务异常等非我们可控因素导致的服务中断或延误,我们将在合理范围内协助处理,但不承担因此产生的间接损失。',
'在法律允许的范围内,我们对因使用或无法使用本服务所产生的损害责任以您就相关服务实际支付的费用为限(免费服务除外另有约定)。',
],
},
{
heading: '七、协议变更与终止',
paragraphs: [
'我们可能适时修订本协议,修订后的协议将在平台公布并自公布之日起生效(法律法规另有要求的除外)。若您继续使用服务,视为接受修订后的协议。',
'您可停止使用并申请注销账号;我们亦可在您严重违反本协议时中止或终止向您提供服务。',
],
},
{
heading: '八、联系我们',
paragraphs: [
`如对本协议有任何疑问,请通过客服热线 ${CUSTOMER_SERVICE_PHONE}(工作时间 9:00–21:00)与我们联系。`,
],
},
],
};
export const PRIVACY_POLICY: LegalDocument = {
id: 'privacy-policy',
title: '隐私政策',
updatedAt: '2026-07-15',
intro:
'杜康好客平台运营方(以下简称「我们」)深知个人信息对您的重要性,将按《中华人民共和国个人信息保护法》等相关法律法规保护您的个人信息。请您在使用服务前仔细阅读本政策。您勾选同意,即表示您已充分理解并同意我们按本政策处理相关个人信息。',
sections: [
{
heading: '一、我们如何收集与使用个人信息',
paragraphs: [
'为向您提供注册登录、下单支付、配送履约、门店核销、客服支持等核心功能,我们可能收集并使用下列信息:',
'1)账号信息:手机号码、验证码、微信 OpenID/UnionID、昵称与头像(若您授权微信);用于注册登录、账号绑定与安全保障。',
'2)交易信息:订单内容、收货地址、支付状态、配送状态、权益与核销记录;用于履约、售后与对账。',
'3)位置信息:在您授权后获取大致位置或精确位置,用于展示所在城市商品与附近门店;您可拒绝授权,我们将使用默认开城城市兜底。',
'4)设备与日志信息:设备型号、操作系统、网络类型、崩溃日志、操作日志等;用于安全风控、故障排查与服务优化。',
'5)您主动提供的其他信息:如客服沟通内容、反馈建议等。',
'我们不会以默认勾选等方式强制您同意本政策;未征得同意前,我们不会超范围收集与实现业务功能无关的个人信息。',
],
},
{
heading: '二、我们如何共享、转让、公开披露',
paragraphs: [
'我们不会向第三方出售您的个人信息。仅在以下情形共享:',
'1)获得您的明确同意;',
'2)为实现支付、短信、配送、地图/定位、微信登录与支付等功能,与必要的服务提供商共享履行服务所必需的信息,并要求其依法保护;',
'3)根据法律法规、行政或司法机关要求;',
'4)在合并、分立、资产转让等情形下,如涉及个人信息转移,我们将要求新的持有方继续受本政策约束,或重新征得您的同意。',
],
},
{
heading: '三、我们如何存储与保护',
paragraphs: [
'您的个人信息存储于中华人民共和国境内。我们仅在实现本政策所述目的所必需的期限内保存;超出期限后将删除或匿名化处理(法律法规另有规定的除外)。',
'我们采取合理的技术与管理措施保护信息安全,防止未经授权的访问、披露、篡改或丢失。如发生安全事件,我们将按法规要求及时告知并采取补救措施。',
],
},
{
heading: '四、第三方 SDK / 服务说明',
paragraphs: [
'为实现登录、支付、分享、定位等功能,本平台可能接入微信开放平台、支付、短信、地图定位等第三方服务。该类服务会按其自身隐私政策处理相关信息。我们仅在实现功能所必需的范围内启用,并尽量采用最小化授权。',
],
},
{
heading: '五、您的权利',
paragraphs: [
'您有权查阅、复制、更正、补充、删除您的个人信息,有权撤回同意、注销账号,以及在符合条件时限制或拒绝我们处理您的个人信息。',
'您可通过「我的」相关功能或联系客服行使上述权利。为保障安全,我们可能需要先验证您的身份。我们将在合理期限内答复。',
'您撤回同意后,我们将停止基于相应同意的处理活动,但不影响此前基于同意已进行的处理。',
],
},
{
heading: '六、未成年人保护',
paragraphs: [
'本平台主要面向成年人。若您为未成年人,请在监护人陪同下阅读本政策,并在监护人同意后使用服务。我们不会主动收集未成年人的个人信息;如发现误收集,将尽快删除。',
],
},
{
heading: '七、本政策的更新',
paragraphs: [
'我们可能适时更新本政策,并通过平台公告、弹窗或页面提示等方式告知。重大变更时,我们会提供更显著的通知,并在必要时重新征得您的同意。',
],
},
{
heading: '八、联系我们',
paragraphs: [
`个人信息保护相关事宜,请拨打客服热线 ${CUSTOMER_SERVICE_PHONE}(工作时间 9:00–21:00)。我们将尽快处理您的请求。`,
],
},
],
};
export function getLegalDocument(id: LegalDocument['id']): LegalDocument {
return id === 'privacy-policy' ? PRIVACY_POLICY : USER_AGREEMENT;
}