Files
gaokao-volunteer-system/apps/web/src/components/ShareDialog.tsx

183 lines
6.6 KiB
TypeScript
Raw Normal View History

feat(sprint-3): 5 个新模块(Share/Query/Review/LLM/Poster)+ G2 闸门通过 ## 5 大新模块(30+ hooks + 5 页面 + 1 LLM 适配器) ### Share Link (T-B-22/28) - useShareLinkCreate/Delete/LatestQuery/StatsQuery - ShareDialog 弹窗(QR + 链接 + 复制) - StatsCard 3 卡(访问数/独立访客/最近访问) - AccessTrendChart(recharts 折线图) ### Data Query (T-B-23) - useScoreLineQuery / useRankEstimatorQuery / useMajorsQuery / useSchoolsQuery - DataQueryPage:4 过滤器 + 等效分数 + 分数线表 ### Review Flow (T-B-24) - useReviewStartMutation / useReviewStatusQuery / useReviewActionMutation - Zod discriminated union 处理 3 种 action - 5 状态轮询(pending/in_progress/approved/rejected/changes_requested) - ReviewPage:状态机 UI + 3 操作按钮 ### Portal (T-B-25) - usePortalCWBQuery / usePortalFullPlanQuery - PortalPage:token 路由 + 等效分数 + 完整方案 ### Poster (T-B-27) - usePosterGenerateMutation(3 模板 classic/modern/minimal) - PosterPreviewPage:渐变预览 + 下载 + 复制 QR ### LLM (T-B-17/26) - 4 模适配器(claude/gpt/gemini/deepseek) - enhanceWithFallback 链式降级 - useLLMConfig / useAuditEnhanceMutation ## G2 闸门(全部通过) - typecheck: 0 error - lint: 0 error 0 warning - test (Vitest): 37/37 passed (10 文件) - test:e2e (Playwright): 24/24 passed (4 浏览器 × 6 spec) - build (Vite): 312 KB gzip (recharts 拆分后主 chunk 83KB) - codegen:check: 0 any ## 关键技术决策 1. 4 模 LLM fallback 链:claude → gpt → gemini → deepseek 2. 所有 API 强类型 + Zod 校验(apiClient 强制 schema) 3. recharts 拆 manualChunk:chart-vendor 104KB gzip 4. Portal token 路由:/portal/:token 共享 token 自动级联 ## Sprint 3 收口报告 SPRINT_3_CLOSEOUT_2026-07-03.md 下一步:Sprint 4 · 性能 + 监控(Sentry / Web Vitals)
2026-07-03 16:42:03 +08:00
/**
* V10 · Sprint 3 · ShareDialog
*
* QR Code + + +
* V10 48px
*/
import { useState } from 'react';
import { QRCodeSVG } from 'qrcode.react';
import { X, Copy, Check, Share2 } from 'lucide-react';
import { useShareLinkCreate, useShareLinkDelete, useShareLinkStatsQuery } from '@/hooks/useShareLink';
import { usePosterGenerateMutation } from '@/hooks/usePosterGenerate';
interface Props {
planId: string;
planTitle: string;
open: boolean;
onClose: () => void;
}
export function ShareDialog({ planId, planTitle, open, onClose }: Props) {
const [copied, setCopied] = useState(false);
const create = useShareLinkCreate();
const del = useShareLinkDelete();
const posterGen = usePosterGenerateMutation();
const code = create.data?.code ?? null;
const stats = useShareLinkStatsQuery(code);
if (!open) return null;
const handleCreate = (): void => {
create.mutate({ planId, expiresIn: 30 });
};
const handleCopy = (): void => {
if (!create.data?.url) return;
void navigator.clipboard
.writeText(create.data.url)
.then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
})
.catch(() => {
// 剪贴板不可用
});
};
const handleDelete = (): void => {
if (!create.data?.id) return;
del.mutate(create.data.id, {
onSuccess: () => create.reset(),
});
};
const handleGeneratePoster = (): void => {
posterGen.mutate({ planId, template: 'classic' });
};
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm p-4"
role="dialog"
aria-modal="true"
aria-label="分享方案"
tabIndex={-1}
onClick={onClose}
onKeyDown={(e) => {
if (e.key === 'Escape') onClose();
}}
>
<div
className="w-full max-w-md rounded-2xl bg-white shadow-2xl p-6"
role="document"
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
>
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-gray-800 flex items-center gap-2">
<Share2 className="w-5 h-5" aria-hidden="true" />
</h2>
<button
type="button"
onClick={onClose}
className="w-10 h-10 min-w-[48px] min-h-[48px] flex items-center justify-center rounded-lg hover:bg-gray-100"
aria-label="关闭"
>
<X className="w-5 h-5" />
</button>
</div>
<p className="text-sm text-gray-500 mb-4">{planTitle}</p>
{!create.data ? (
<div className="flex flex-col gap-3">
{create.isError && (
<div className="rounded-lg border border-red-100 bg-red-50 px-3 py-2 text-sm text-red-700" role="alert">
</div>
)}
feat(sprint-3): 5 个新模块(Share/Query/Review/LLM/Poster)+ G2 闸门通过 ## 5 大新模块(30+ hooks + 5 页面 + 1 LLM 适配器) ### Share Link (T-B-22/28) - useShareLinkCreate/Delete/LatestQuery/StatsQuery - ShareDialog 弹窗(QR + 链接 + 复制) - StatsCard 3 卡(访问数/独立访客/最近访问) - AccessTrendChart(recharts 折线图) ### Data Query (T-B-23) - useScoreLineQuery / useRankEstimatorQuery / useMajorsQuery / useSchoolsQuery - DataQueryPage:4 过滤器 + 等效分数 + 分数线表 ### Review Flow (T-B-24) - useReviewStartMutation / useReviewStatusQuery / useReviewActionMutation - Zod discriminated union 处理 3 种 action - 5 状态轮询(pending/in_progress/approved/rejected/changes_requested) - ReviewPage:状态机 UI + 3 操作按钮 ### Portal (T-B-25) - usePortalCWBQuery / usePortalFullPlanQuery - PortalPage:token 路由 + 等效分数 + 完整方案 ### Poster (T-B-27) - usePosterGenerateMutation(3 模板 classic/modern/minimal) - PosterPreviewPage:渐变预览 + 下载 + 复制 QR ### LLM (T-B-17/26) - 4 模适配器(claude/gpt/gemini/deepseek) - enhanceWithFallback 链式降级 - useLLMConfig / useAuditEnhanceMutation ## G2 闸门(全部通过) - typecheck: 0 error - lint: 0 error 0 warning - test (Vitest): 37/37 passed (10 文件) - test:e2e (Playwright): 24/24 passed (4 浏览器 × 6 spec) - build (Vite): 312 KB gzip (recharts 拆分后主 chunk 83KB) - codegen:check: 0 any ## 关键技术决策 1. 4 模 LLM fallback 链:claude → gpt → gemini → deepseek 2. 所有 API 强类型 + Zod 校验(apiClient 强制 schema) 3. recharts 拆 manualChunk:chart-vendor 104KB gzip 4. Portal token 路由:/portal/:token 共享 token 自动级联 ## Sprint 3 收口报告 SPRINT_3_CLOSEOUT_2026-07-03.md 下一步:Sprint 4 · 性能 + 监控(Sentry / Web Vitals)
2026-07-03 16:42:03 +08:00
<button
type="button"
onClick={handleCreate}
disabled={create.isPending}
className="w-full min-h-[48px] py-3 rounded-xl bg-blue-600 text-white font-medium hover:bg-blue-700 disabled:opacity-50 transition-colors"
>
{create.isPending ? '创建中…' : create.isError ? '重试创建分享链接' : '创建分享链接30天有效'}
feat(sprint-3): 5 个新模块(Share/Query/Review/LLM/Poster)+ G2 闸门通过 ## 5 大新模块(30+ hooks + 5 页面 + 1 LLM 适配器) ### Share Link (T-B-22/28) - useShareLinkCreate/Delete/LatestQuery/StatsQuery - ShareDialog 弹窗(QR + 链接 + 复制) - StatsCard 3 卡(访问数/独立访客/最近访问) - AccessTrendChart(recharts 折线图) ### Data Query (T-B-23) - useScoreLineQuery / useRankEstimatorQuery / useMajorsQuery / useSchoolsQuery - DataQueryPage:4 过滤器 + 等效分数 + 分数线表 ### Review Flow (T-B-24) - useReviewStartMutation / useReviewStatusQuery / useReviewActionMutation - Zod discriminated union 处理 3 种 action - 5 状态轮询(pending/in_progress/approved/rejected/changes_requested) - ReviewPage:状态机 UI + 3 操作按钮 ### Portal (T-B-25) - usePortalCWBQuery / usePortalFullPlanQuery - PortalPage:token 路由 + 等效分数 + 完整方案 ### Poster (T-B-27) - usePosterGenerateMutation(3 模板 classic/modern/minimal) - PosterPreviewPage:渐变预览 + 下载 + 复制 QR ### LLM (T-B-17/26) - 4 模适配器(claude/gpt/gemini/deepseek) - enhanceWithFallback 链式降级 - useLLMConfig / useAuditEnhanceMutation ## G2 闸门(全部通过) - typecheck: 0 error - lint: 0 error 0 warning - test (Vitest): 37/37 passed (10 文件) - test:e2e (Playwright): 24/24 passed (4 浏览器 × 6 spec) - build (Vite): 312 KB gzip (recharts 拆分后主 chunk 83KB) - codegen:check: 0 any ## 关键技术决策 1. 4 模 LLM fallback 链:claude → gpt → gemini → deepseek 2. 所有 API 强类型 + Zod 校验(apiClient 强制 schema) 3. recharts 拆 manualChunk:chart-vendor 104KB gzip 4. Portal token 路由:/portal/:token 共享 token 自动级联 ## Sprint 3 收口报告 SPRINT_3_CLOSEOUT_2026-07-03.md 下一步:Sprint 4 · 性能 + 监控(Sentry / Web Vitals)
2026-07-03 16:42:03 +08:00
</button>
<button
type="button"
onClick={handleGeneratePoster}
disabled={posterGen.isPending}
className="w-full min-h-[48px] py-3 rounded-xl bg-white text-gray-700 border border-gray-200 font-medium hover:bg-gray-50 disabled:opacity-50 transition-colors"
>
{posterGen.isPending ? '生成中…' : posterGen.data ? '查看海报' : '生成分享海报'}
</button>
</div>
) : (
<div className="flex flex-col items-center gap-4">
<div className="rounded-xl bg-white p-3 border border-gray-100">
<QRCodeSVG value={create.data.url} size={180} level="M" />
</div>
<div className="w-full flex items-center gap-2">
<input
type="text"
readOnly
value={create.data.url}
className="flex-1 px-3 py-2 text-xs bg-gray-50 border border-gray-200 rounded-lg truncate"
aria-label="分享链接"
/>
<button
type="button"
onClick={handleCopy}
className="min-w-[48px] min-h-[48px] px-3 rounded-lg bg-blue-50 text-blue-700 flex items-center justify-center hover:bg-blue-100"
aria-label="复制链接"
>
{copied ? <Check className="w-4 h-4" /> : <Copy className="w-4 h-4" />}
</button>
</div>
{stats.isError && (
<div className="w-full rounded-lg border border-amber-100 bg-amber-50 px-3 py-2 text-sm text-amber-700" role="status">
访
</div>
)}
feat(sprint-3): 5 个新模块(Share/Query/Review/LLM/Poster)+ G2 闸门通过 ## 5 大新模块(30+ hooks + 5 页面 + 1 LLM 适配器) ### Share Link (T-B-22/28) - useShareLinkCreate/Delete/LatestQuery/StatsQuery - ShareDialog 弹窗(QR + 链接 + 复制) - StatsCard 3 卡(访问数/独立访客/最近访问) - AccessTrendChart(recharts 折线图) ### Data Query (T-B-23) - useScoreLineQuery / useRankEstimatorQuery / useMajorsQuery / useSchoolsQuery - DataQueryPage:4 过滤器 + 等效分数 + 分数线表 ### Review Flow (T-B-24) - useReviewStartMutation / useReviewStatusQuery / useReviewActionMutation - Zod discriminated union 处理 3 种 action - 5 状态轮询(pending/in_progress/approved/rejected/changes_requested) - ReviewPage:状态机 UI + 3 操作按钮 ### Portal (T-B-25) - usePortalCWBQuery / usePortalFullPlanQuery - PortalPage:token 路由 + 等效分数 + 完整方案 ### Poster (T-B-27) - usePosterGenerateMutation(3 模板 classic/modern/minimal) - PosterPreviewPage:渐变预览 + 下载 + 复制 QR ### LLM (T-B-17/26) - 4 模适配器(claude/gpt/gemini/deepseek) - enhanceWithFallback 链式降级 - useLLMConfig / useAuditEnhanceMutation ## G2 闸门(全部通过) - typecheck: 0 error - lint: 0 error 0 warning - test (Vitest): 37/37 passed (10 文件) - test:e2e (Playwright): 24/24 passed (4 浏览器 × 6 spec) - build (Vite): 312 KB gzip (recharts 拆分后主 chunk 83KB) - codegen:check: 0 any ## 关键技术决策 1. 4 模 LLM fallback 链:claude → gpt → gemini → deepseek 2. 所有 API 强类型 + Zod 校验(apiClient 强制 schema) 3. recharts 拆 manualChunk:chart-vendor 104KB gzip 4. Portal token 路由:/portal/:token 共享 token 自动级联 ## Sprint 3 收口报告 SPRINT_3_CLOSEOUT_2026-07-03.md 下一步:Sprint 4 · 性能 + 监控(Sentry / Web Vitals)
2026-07-03 16:42:03 +08:00
{stats.data && (
<div className="w-full grid grid-cols-3 gap-2 text-center text-xs">
<div className="rounded-lg bg-gray-50 px-2 py-2">
<p className="text-gray-400">访</p>
<p className="font-semibold text-gray-800">{stats.data.views}</p>
</div>
<div className="rounded-lg bg-gray-50 px-2 py-2">
<p className="text-gray-400">访</p>
<p className="font-semibold text-gray-800">{stats.data.uniqueVisitors}</p>
</div>
<div className="rounded-lg bg-gray-50 px-2 py-2">
<p className="text-gray-400">访</p>
<p className="font-semibold text-gray-800 truncate">
{stats.data.lastAccessedAt ? new Date(stats.data.lastAccessedAt).toLocaleDateString() : '—'}
</p>
</div>
</div>
)}
<button
type="button"
onClick={handleDelete}
className="text-xs text-gray-400 hover:text-red-500 transition-colors min-h-[48px] py-2"
>
</button>
{del.isError && (
<p className="text-xs text-red-500" role="alert">
</p>
)}
feat(sprint-3): 5 个新模块(Share/Query/Review/LLM/Poster)+ G2 闸门通过 ## 5 大新模块(30+ hooks + 5 页面 + 1 LLM 适配器) ### Share Link (T-B-22/28) - useShareLinkCreate/Delete/LatestQuery/StatsQuery - ShareDialog 弹窗(QR + 链接 + 复制) - StatsCard 3 卡(访问数/独立访客/最近访问) - AccessTrendChart(recharts 折线图) ### Data Query (T-B-23) - useScoreLineQuery / useRankEstimatorQuery / useMajorsQuery / useSchoolsQuery - DataQueryPage:4 过滤器 + 等效分数 + 分数线表 ### Review Flow (T-B-24) - useReviewStartMutation / useReviewStatusQuery / useReviewActionMutation - Zod discriminated union 处理 3 种 action - 5 状态轮询(pending/in_progress/approved/rejected/changes_requested) - ReviewPage:状态机 UI + 3 操作按钮 ### Portal (T-B-25) - usePortalCWBQuery / usePortalFullPlanQuery - PortalPage:token 路由 + 等效分数 + 完整方案 ### Poster (T-B-27) - usePosterGenerateMutation(3 模板 classic/modern/minimal) - PosterPreviewPage:渐变预览 + 下载 + 复制 QR ### LLM (T-B-17/26) - 4 模适配器(claude/gpt/gemini/deepseek) - enhanceWithFallback 链式降级 - useLLMConfig / useAuditEnhanceMutation ## G2 闸门(全部通过) - typecheck: 0 error - lint: 0 error 0 warning - test (Vitest): 37/37 passed (10 文件) - test:e2e (Playwright): 24/24 passed (4 浏览器 × 6 spec) - build (Vite): 312 KB gzip (recharts 拆分后主 chunk 83KB) - codegen:check: 0 any ## 关键技术决策 1. 4 模 LLM fallback 链:claude → gpt → gemini → deepseek 2. 所有 API 强类型 + Zod 校验(apiClient 强制 schema) 3. recharts 拆 manualChunk:chart-vendor 104KB gzip 4. Portal token 路由:/portal/:token 共享 token 自动级联 ## Sprint 3 收口报告 SPRINT_3_CLOSEOUT_2026-07-03.md 下一步:Sprint 4 · 性能 + 监控(Sentry / Web Vitals)
2026-07-03 16:42:03 +08:00
</div>
)}
</div>
</div>
);
}