收口前端平台组件库能力

新增 PlatformUiKit 通用弹窗、按钮、状态、空态、媒体、表单和标签等公共组件
迁移结果页、创作工作台、认证入口、RPG 暗色面板和运行态弹窗的重复 UI chrome
补充组件测试、页面回归测试、技术文档和 Hermes 共享决策记录
This commit is contained in:
2026-06-10 10:24:18 +08:00
parent a4ee6ff698
commit 1ad25e30f8
226 changed files with 23364 additions and 7825 deletions

View File

@@ -1,7 +1,12 @@
import { ArrowLeft, CheckCircle2, ImagePlus, Send, X } from 'lucide-react';
import { ArrowLeft, CheckCircle2, Send } from 'lucide-react';
import { useRef, useState } from 'react';
import type { ProfileFeedbackEvidenceItemInput } from '../../../packages/shared/src/contracts/runtime';
import { PlatformActionButton } from '../common/PlatformActionButton';
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
import { PlatformSubpanel } from '../common/PlatformSubpanel';
import { PlatformUploadPreviewCard } from '../common/PlatformUploadPreviewCard';
import { PlatformUploadTile } from '../common/PlatformUploadTile';
const MIN_FEEDBACK_DESCRIPTION_LENGTH = 10;
const MAX_FEEDBACK_DESCRIPTION_LENGTH = 200;
@@ -55,7 +60,9 @@ export function PlatformFeedbackView({
const evidenceInputRef = useRef<HTMLInputElement | null>(null);
const [description, setDescription] = useState('');
const [contactPhone, setContactPhone] = useState('');
const [evidencePreviews, setEvidencePreviews] = useState<EvidencePreview[]>([]);
const [evidencePreviews, setEvidencePreviews] = useState<EvidencePreview[]>(
[],
);
const [error, setError] = useState<string | null>(null);
const [notice, setNotice] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
@@ -97,7 +104,8 @@ export function PlatformFeedbackView({
setError('反馈凭证只支持图片类型');
return;
}
const remainingCount = MAX_FEEDBACK_EVIDENCE_COUNT - evidencePreviews.length;
const remainingCount =
MAX_FEEDBACK_EVIDENCE_COUNT - evidencePreviews.length;
if (remainingCount <= 0 || selectedFiles.length > remainingCount) {
setError('最多上传四张凭证');
}
@@ -142,7 +150,9 @@ export function PlatformFeedbackView({
setSubmitted(false);
})
.catch((readError: unknown) => {
setError(readError instanceof Error ? readError.message : '图片读取失败');
setError(
readError instanceof Error ? readError.message : '图片读取失败',
);
});
};
@@ -199,7 +209,9 @@ export function PlatformFeedbackView({
.then(() => setSubmitted(true))
.catch((submitError: unknown) => {
setSubmitted(false);
setError(submitError instanceof Error ? submitError.message : '提交失败');
setError(
submitError instanceof Error ? submitError.message : '提交失败',
);
})
.finally(() => setIsSubmitting(false));
};
@@ -208,14 +220,16 @@ export function PlatformFeedbackView({
<div className="platform-page-stage platform-remap-surface min-h-0 flex-1 overflow-y-auto text-[var(--platform-text-strong)]">
<div className="mx-auto flex min-h-full w-full max-w-[30rem] flex-col px-4 pb-6 pt-4">
<header className="flex shrink-0 items-center gap-3 pb-3">
<button
type="button"
<PlatformActionButton
onClick={onBack}
aria-label="返回我的页签"
className="platform-button platform-button--ghost h-10 w-10 shrink-0 justify-center rounded-full p-0"
tone="ghost"
shape="pill"
size="xs"
className="h-10 w-10 shrink-0 p-0"
>
<ArrowLeft className="h-[1.125rem] w-[1.125rem]" />
</button>
</PlatformActionButton>
<div className="min-w-0 text-base font-black text-[var(--platform-text-strong)]">
</div>
@@ -226,7 +240,7 @@ export function PlatformFeedbackView({
</div>
<section className="platform-subpanel rounded-[1.2rem] px-4 py-4">
<PlatformSubpanel radius="md">
<label
htmlFor="profile-feedback-description"
className="block text-base font-semibold text-[var(--platform-text-strong)]"
@@ -244,43 +258,28 @@ export function PlatformFeedbackView({
<div className="text-right text-xs text-[var(--platform-text-soft)]">
{descriptionLength}/{MAX_FEEDBACK_DESCRIPTION_LENGTH}
</div>
</section>
</PlatformSubpanel>
<section className="platform-subpanel rounded-[1.2rem] px-4 py-4">
<PlatformSubpanel radius="md">
<div className="text-base font-semibold text-[var(--platform-text-strong)]">
()
</div>
<div className="mt-4 flex flex-wrap gap-3">
{evidencePreviews.map((preview) => (
<div
<PlatformUploadPreviewCard
key={preview.id}
className="relative h-[5.75rem] w-[5.75rem] overflow-hidden rounded-xl border border-[var(--platform-subpanel-border)] bg-[var(--platform-input-fill)]"
>
<img
src={preview.dataUrl}
alt="反馈凭证预览"
className="h-full w-full object-cover"
/>
<button
type="button"
onClick={() => removeEvidencePreview(preview.id)}
aria-label="移除上传凭证"
className="absolute right-1 top-1 flex h-5 w-5 items-center justify-center rounded-full bg-black/55 text-white"
>
<X className="h-3 w-3" />
</button>
</div>
imageSrc={preview.dataUrl}
imageAlt="反馈凭证预览"
removeLabel="移除上传凭证"
onRemove={() => removeEvidencePreview(preview.id)}
/>
))}
{evidencePreviews.length < MAX_FEEDBACK_EVIDENCE_COUNT ? (
<button
type="button"
<PlatformUploadTile
onClick={openEvidencePicker}
className="flex h-[5.75rem] w-[5.75rem] flex-col items-center justify-center rounded-xl border border-dashed border-[var(--platform-subpanel-border)] bg-[var(--platform-input-fill)] text-[var(--platform-text-soft)] transition hover:border-[var(--platform-surface-hover-border)] hover:text-[var(--platform-text-strong)]"
>
<ImagePlus className="h-6 w-6" />
<span className="mt-2 text-xs font-medium"></span>
<span className="mt-0.5 text-[11px]">()</span>
</button>
label="上传凭证"
hint="(最多四张)"
/>
) : null}
</div>
<input
@@ -291,9 +290,9 @@ export function PlatformFeedbackView({
className="hidden"
onChange={(event) => addEvidenceFiles(event.target.files)}
/>
</section>
</PlatformSubpanel>
<section className="platform-subpanel rounded-[1.2rem] px-4 py-4">
<PlatformSubpanel radius="md">
<label
htmlFor="profile-feedback-phone"
className="block text-base font-semibold text-[var(--platform-text-strong)]"
@@ -309,34 +308,35 @@ export function PlatformFeedbackView({
placeholder="选填,如您填写则将会同步开发者与您联系"
className="mt-3 w-full border-0 bg-transparent text-sm leading-6 text-[var(--platform-text-strong)] outline-none placeholder:text-[var(--platform-text-soft)]"
/>
</section>
</PlatformSubpanel>
{error ? (
<div className="rounded-[1.2rem] border border-[var(--platform-button-danger-border)] bg-[var(--platform-button-danger-fill)] px-4 py-3 text-sm font-medium text-[var(--platform-button-danger-text)]">
<PlatformStatusMessage tone="error" surface="profile">
{error}
</div>
</PlatformStatusMessage>
) : null}
{submitted ? (
<div className="flex items-center gap-2 rounded-[1.2rem] border border-[var(--platform-success-border)] bg-[var(--platform-success-bg)] px-4 py-3 text-sm font-medium text-[var(--platform-success-text)]">
<PlatformStatusMessage tone="success" surface="profile">
<CheckCircle2 className="h-4 w-4" />
</div>
</PlatformStatusMessage>
) : null}
{notice ? (
<div className="rounded-[1.2rem] border border-[var(--platform-cool-border)] bg-[var(--platform-cool-bg)] px-4 py-3 text-sm font-medium text-[var(--platform-cool-text)]">
<PlatformStatusMessage tone="info" surface="profile">
{notice}
</div>
</PlatformStatusMessage>
) : null}
<button
type="button"
<PlatformActionButton
fullWidth
size="md"
className="mt-2 h-12 text-base"
onClick={submitFeedback}
disabled={isSubmitting}
className="platform-button platform-button--primary mt-2 h-12 w-full justify-center text-base disabled:cursor-not-allowed disabled:opacity-60"
>
<Send className="h-4 w-4" />
{isSubmitting ? '提交中' : '提交'}
</button>
</PlatformActionButton>
<button
type="button"