1
This commit is contained in:
124
src/components/common/LegalDocumentModal.tsx
Normal file
124
src/components/common/LegalDocumentModal.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import { FileText } from 'lucide-react';
|
||||
|
||||
import type { PlatformTheme } from '../../../packages/shared/src/contracts/runtime';
|
||||
import type {
|
||||
LegalDocument,
|
||||
LegalDocumentBlock,
|
||||
} from './legalDocuments';
|
||||
import { UnifiedModal } from './UnifiedModal';
|
||||
|
||||
type LegalDocumentModalProps = {
|
||||
document: LegalDocument | null;
|
||||
open: boolean;
|
||||
platformTheme?: PlatformTheme;
|
||||
zIndexClassName?: string;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
function LegalRichText({ text }: { text: string }) {
|
||||
const parts = text.split(/(\*\*[^*]+\*\*)/gu);
|
||||
|
||||
return (
|
||||
<>
|
||||
{parts.map((part, index) => {
|
||||
const boldMatch = /^\*\*([^*]+)\*\*$/u.exec(part);
|
||||
if (boldMatch) {
|
||||
return (
|
||||
<strong
|
||||
key={`${index}:${part}`}
|
||||
className="font-black text-[var(--platform-text-strong)]"
|
||||
>
|
||||
{boldMatch[1]}
|
||||
</strong>
|
||||
);
|
||||
}
|
||||
|
||||
return part;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function LegalDocumentBodyBlock({
|
||||
block,
|
||||
}: {
|
||||
block: LegalDocumentBlock;
|
||||
}) {
|
||||
if (block.type === 'heading') {
|
||||
const className =
|
||||
block.level === 2
|
||||
? 'mt-5 text-base font-black leading-7 text-[var(--platform-text-strong)] first:mt-0'
|
||||
: 'mt-4 text-sm font-black leading-6 text-[var(--platform-text-strong)] first:mt-0';
|
||||
|
||||
return <div className={className}>{block.text}</div>;
|
||||
}
|
||||
|
||||
if (block.type === 'list') {
|
||||
return (
|
||||
<ul className="mt-3 list-disc space-y-2 pl-5 text-sm leading-7 text-[var(--platform-text-base)]">
|
||||
{block.items.map((item, index) => (
|
||||
<li key={`${index}:${item}`}>
|
||||
<LegalRichText text={item} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<p className="mt-3 whitespace-pre-line text-sm leading-7 text-[var(--platform-text-base)]">
|
||||
<LegalRichText text={block.text} />
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
export function LegalDocumentModal({
|
||||
document,
|
||||
open,
|
||||
platformTheme,
|
||||
zIndexClassName,
|
||||
onClose,
|
||||
}: LegalDocumentModalProps) {
|
||||
return (
|
||||
<UnifiedModal
|
||||
open={open && Boolean(document)}
|
||||
title={document?.title ?? '法律信息'}
|
||||
onClose={onClose}
|
||||
size="md"
|
||||
closeLabel="关闭法律信息"
|
||||
zIndexClassName={zIndexClassName ?? 'z-[150]'}
|
||||
overlayClassName={`platform-theme ${
|
||||
platformTheme ? `platform-theme--${platformTheme}` : ''
|
||||
}`}
|
||||
panelClassName="platform-remap-surface rounded-t-[1.4rem] sm:rounded-[1.4rem]"
|
||||
headerClassName="items-center"
|
||||
bodyClassName="px-4 py-0 sm:px-5"
|
||||
footerClassName="justify-stretch sm:justify-end"
|
||||
footer={
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="platform-button platform-button--secondary min-h-0 w-full rounded-[0.9rem] px-4 py-2.5 text-sm sm:w-auto"
|
||||
>
|
||||
我知道了
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<div className="max-h-[min(64vh,34rem)] overflow-y-auto py-4 pr-1">
|
||||
<div className="mb-4 flex items-center gap-2 text-[var(--platform-cool-text)]">
|
||||
<FileText className="h-4 w-4" />
|
||||
<span className="text-xs font-black tracking-[0.2em]">
|
||||
LEGAL
|
||||
</span>
|
||||
</div>
|
||||
{document?.blocks.map((block, index) => (
|
||||
<LegalDocumentBodyBlock
|
||||
// 中文注释:法律文本没有稳定段落 id,用序号只限定在静态文档渲染列表内。
|
||||
key={`${block.type}:${index}`}
|
||||
block={block}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</UnifiedModal>
|
||||
);
|
||||
}
|
||||
157
src/components/common/legalDocuments.ts
Normal file
157
src/components/common/legalDocuments.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import disclaimerMarkdown from '../../../media/files/disclaimer.md?raw';
|
||||
import privacyPolicyMarkdown from '../../../media/files/privacy_policy.md?raw';
|
||||
import userAgreementMarkdown from '../../../media/files/user_agreement.md?raw';
|
||||
|
||||
export type LegalDocumentId =
|
||||
| 'user-agreement'
|
||||
| 'privacy-policy'
|
||||
| 'disclaimer';
|
||||
|
||||
export type LegalDocumentBlock =
|
||||
| {
|
||||
type: 'heading';
|
||||
level: 2 | 3;
|
||||
text: string;
|
||||
}
|
||||
| {
|
||||
type: 'paragraph';
|
||||
text: string;
|
||||
}
|
||||
| {
|
||||
type: 'list';
|
||||
items: string[];
|
||||
};
|
||||
|
||||
export type LegalDocument = {
|
||||
id: LegalDocumentId;
|
||||
title: string;
|
||||
markdown: string;
|
||||
blocks: LegalDocumentBlock[];
|
||||
};
|
||||
|
||||
export const LEGAL_CONSENT_STORAGE_KEY =
|
||||
'genarrative.auth.legal-consent.v1';
|
||||
export const ICP_RECORD_NUMBER = '京ICP备2026025677号';
|
||||
export const ICP_RECORD_URL = 'https://beian.miit.gov.cn/';
|
||||
|
||||
function normalizeMarkdownInlineText(value: string) {
|
||||
return value.replace(/`([^`]+)`/gu, '$1').trim();
|
||||
}
|
||||
|
||||
function pushParagraph(
|
||||
blocks: LegalDocumentBlock[],
|
||||
lines: string[],
|
||||
) {
|
||||
if (lines.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const text = normalizeMarkdownInlineText(lines.join('\n'));
|
||||
if (text) {
|
||||
blocks.push({ type: 'paragraph', text });
|
||||
}
|
||||
lines.length = 0;
|
||||
}
|
||||
|
||||
function pushList(blocks: LegalDocumentBlock[], items: string[]) {
|
||||
if (items.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
blocks.push({
|
||||
type: 'list',
|
||||
items: items.map(normalizeMarkdownInlineText).filter(Boolean),
|
||||
});
|
||||
items.length = 0;
|
||||
}
|
||||
|
||||
function parseLegalMarkdown(markdown: string): LegalDocumentBlock[] {
|
||||
const blocks: LegalDocumentBlock[] = [];
|
||||
const paragraphLines: string[] = [];
|
||||
const listItems: string[] = [];
|
||||
|
||||
markdown.split(/\r?\n/u).forEach((rawLine) => {
|
||||
const line = rawLine.trim();
|
||||
|
||||
if (!line) {
|
||||
pushParagraph(blocks, paragraphLines);
|
||||
pushList(blocks, listItems);
|
||||
return;
|
||||
}
|
||||
|
||||
const headingMatch = /^(#{2,3})\s+(.+)$/u.exec(line);
|
||||
if (headingMatch) {
|
||||
pushParagraph(blocks, paragraphLines);
|
||||
pushList(blocks, listItems);
|
||||
blocks.push({
|
||||
type: 'heading',
|
||||
level: headingMatch[1]?.length === 2 ? 2 : 3,
|
||||
text: normalizeMarkdownInlineText(headingMatch[2] ?? ''),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const listMatch = /^(?:[-*]|\d+[.)、])\s+(.+)$/u.exec(line);
|
||||
if (listMatch) {
|
||||
pushParagraph(blocks, paragraphLines);
|
||||
listItems.push(listMatch[1] ?? '');
|
||||
return;
|
||||
}
|
||||
|
||||
pushList(blocks, listItems);
|
||||
paragraphLines.push(line);
|
||||
});
|
||||
|
||||
pushParagraph(blocks, paragraphLines);
|
||||
pushList(blocks, listItems);
|
||||
return blocks;
|
||||
}
|
||||
|
||||
const legalDocumentDefinitions = [
|
||||
{
|
||||
id: 'user-agreement',
|
||||
title: '用户协议',
|
||||
markdown: userAgreementMarkdown,
|
||||
},
|
||||
{
|
||||
id: 'privacy-policy',
|
||||
title: '隐私政策',
|
||||
markdown: privacyPolicyMarkdown,
|
||||
},
|
||||
{
|
||||
id: 'disclaimer',
|
||||
title: '免责声明',
|
||||
markdown: disclaimerMarkdown,
|
||||
},
|
||||
] satisfies Array<{
|
||||
id: LegalDocumentId;
|
||||
title: string;
|
||||
markdown: string;
|
||||
}>;
|
||||
|
||||
export const LEGAL_DOCUMENTS: LegalDocument[] = legalDocumentDefinitions.map(
|
||||
(document) => ({
|
||||
...document,
|
||||
blocks: parseLegalMarkdown(document.markdown),
|
||||
}),
|
||||
);
|
||||
|
||||
export function getLegalDocument(id: LegalDocumentId) {
|
||||
return LEGAL_DOCUMENTS.find((document) => document.id === id) ?? null;
|
||||
}
|
||||
|
||||
export function readStoredLegalConsent() {
|
||||
if (typeof window === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return window.localStorage.getItem(LEGAL_CONSENT_STORAGE_KEY) === 'true';
|
||||
}
|
||||
|
||||
export function persistLegalConsent() {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
window.localStorage.setItem(LEGAL_CONSENT_STORAGE_KEY, 'true');
|
||||
}
|
||||
Reference in New Issue
Block a user