This commit is contained in:
2026-05-13 00:28:07 +08:00
parent ef4f91a75e
commit 01c5ab985a
101 changed files with 10635 additions and 2292 deletions

View 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>
);
}

View 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');
}