首次进项目弹操作指引:四步一页一步,本机记住,总览可重开

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_017Dhew22GiDVT9oKNm3U7gH
This commit is contained in:
Claude
2026-11-02 11:59:22 +00:00
parent 7956a2890c
commit 9a3a3853ef
3 changed files with 113 additions and 2 deletions
+1
View File
@@ -20,6 +20,7 @@
- **批量进人**(第二轮反馈):邀请码加「免审核」(默认开),凭码注册马上开通并进项目,注册页直接登录;训练营批次页「一键发团队码」给每家企业一个免审核码(项目成员 · 20 人 · 60 天),可复制全部转给企业;新接口 `dingze:campInvites`,`dz_invites.autoApprove`。
- **上传资料放到最前**(第二轮反馈):资料页页签改为 ① 上传资料 · ② 企业画像 · ③ 组织与职责,默认打开上传;没有资料时整块是一个大的上传按钮。
- **空表起草**(第二轮反馈):新表打开时顶部提示「按资料起草这张表」,一键让数字咨询师出整张表的修改卡;旁边是「看示例」。
- **首次指引**:企业侧第一次进新项目弹四步操作指引(上传资料 → 战略解码 → 目标分解 → 计划制定,一步一页),关掉后本机不再弹;总览“看操作指引”随时重开。
- **手册**:新增客户版[三分钟上手](quick-start.md)(门户左侧「使用指南」同一份)和我方版[运营与咨询师速查](ops-quick-ref.md);原操作手册改名“详细参考”,只作查阅。
- 注意:
- 开放公开注册前要在 dev 上把注册 → 上传资料 → 第一张表走通一遍(多空间插件在未登录请求下建空间和账号的行为需实测)。
@@ -0,0 +1,90 @@
import { ArrowRight } from "lucide-react";
import { useState } from "react";
import { Link } from "react-router";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { GUIDE_STEPS } from "@/lib/dingze/guide";
import { samplePath } from "@/lib/dingze/sample";
import { usePersistentFlag } from "@/lib/dingze/view-prefs";
const KEY = "dingze:first-run-guide";
/** 本机是否已看过首次指引。 */
export function useFirstRunGuide(): [boolean, (seen: boolean) => void] {
const [seen, setSeen] = usePersistentFlag(KEY);
return [seen === true, setSeen];
}
/**
* 首次进项目的操作指引:五步各一句话,一页看完。关掉后本机不再弹;
* 左侧「使用指南」随时能再看完整说明。
*/
export function FirstRunGuide({ open, onClose, startHref }: { open: boolean; onClose: () => void; startHref: string }) {
// 注册那一步已经做完了,从第 2 步“上传资料”开始讲。
const steps = GUIDE_STEPS.filter((s) => s.no !== 1);
const [page, setPage] = useState(0);
const step = steps[page];
const last = page === steps.length - 1;
return (
<Dialog open={open} onOpenChange={(o) => (!o ? onClose() : undefined)}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>欢迎,先看一眼怎么走</DialogTitle>
<DialogDescription>一共 {steps.length} 步,做完就有年度经营计划书。每一步右边都有数字咨询师,不会填就问它。</DialogDescription>
</DialogHeader>
<ol className="flex gap-1" aria-label="进度">
{steps.map((s, i) => (
<li key={s.title} className="flex-1">
<button
type="button"
onClick={() => setPage(i)}
aria-current={i === page ? "step" : undefined}
className={`flex w-full flex-col items-center gap-1 rounded-md px-1 py-1.5 text-xs ${i === page ? "bg-accent font-semibold" : "text-muted-foreground hover:bg-muted"}`}
>
<span className={`flex size-6 items-center justify-center rounded-full font-bold ${i === page ? "bg-primary text-primary-foreground" : "bg-muted"}`}>{s.no! - 1}</span>
{s.title}
</button>
</li>
))}
</ol>
<div className="min-h-32 rounded-xl border bg-card p-4">
<h3 className="font-heading text-lg font-bold">
第 {step.no! - 1} 步 · {step.title}
</h3>
<ul className="mt-2 flex list-disc flex-col gap-1 pl-4 text-sm">
{step.items.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</div>
<DialogFooter className="flex-wrap gap-2 sm:justify-between">
<span className="flex gap-2">
<Button variant="ghost" size="sm" nativeButton={false} render={<Link to={samplePath()} target="_blank" rel="noopener" />}>
先看一套填好的示例
</Button>
<Button variant="ghost" size="sm" nativeButton={false} render={<Link to="/help" />} onClick={onClose}>
完整指南
</Button>
</span>
<span className="flex gap-2">
{page > 0 ? (
<Button variant="outline" onClick={() => setPage(page - 1)}>
上一步
</Button>
) : null}
{last ? (
<Button nativeButton={false} render={<Link to={startHref} />} onClick={onClose}>
开始:上传资料 <ArrowRight />
</Button>
) : (
<Button onClick={() => setPage(page + 1)}>
下一步 <ArrowRight />
</Button>
)}
</span>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
+22 -2
View File
@@ -1,5 +1,5 @@
import { ArrowRight, Check, CircleCheck, CircleDashed, CircleDot, Import, Lock } from "lucide-react";
import { useState } from "react";
import { useEffect, useState } from "react";
import { Link, useSearchParams } from "react-router";
import { STAGES, collaborationStatus, getArtifactDef, nextAction } from "@dingze/shared";
@@ -15,6 +15,7 @@ import { STEP_ARTIFACTS, nextActionPath, progressOf, stageEntryCode, stageProgre
import { type StartupItem, startupChecklist } from "@/lib/dingze/project-startup";
import { cn } from "@/lib/utils";
import { FirstRunGuide, useFirstRunGuide } from "./components/first-run-guide";
import { ImportDialog } from "./components/import-dialog";
import { useProjectContext } from "./project-context";
@@ -47,6 +48,19 @@ export default function ProjectOverviewPage() {
started: artifacts.some((a) => a.status !== "not_started"),
});
const materialsFirst = !materials.isLoading && !profile.isLoading && flow[0].state === "current" && role !== "readonly";
// 首次进项目弹操作指引(本机记住)。只给企业侧的新项目:还没开始填表、不是咨询方。
const [guideSeen, setGuideSeen] = useFirstRunGuide();
const [guideOpen, setGuideOpen] = useState(false);
const freshProject = !artifacts.some((a) => a.status !== "not_started");
useEffect(() => {
if (!guideSeen && !consulting && freshProject && !materials.isLoading && !profile.isLoading) setGuideOpen(true);
// 只在首次加载完判断一次
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [materials.isLoading, profile.isLoading]);
const closeGuide = () => {
setGuideOpen(false);
setGuideSeen(true);
};
// 训练营和企业内训对企业是同一套界面;场景只给咨询侧看。
const consulting = overview.isConsultAdmin || role === "lead_consultant" || role === "co_consultant";
// Everything else that is moving: returned, in review, waiting for 定版, upstream changed.
@@ -67,6 +81,7 @@ export default function ProjectOverviewPage() {
return (
<div className="mx-auto flex w-full max-w-[1360px] flex-col gap-5 px-4 py-6 md:px-7">
{canImport ? <ImportDialog project={project} open={importOpen} onOpenChange={closeImport} /> : null}
<FirstRunGuide open={guideOpen} onClose={closeGuide} startHref={`/projects/${project.id}/materials`} />
<div className="flex flex-wrap items-end gap-3">
<div className="min-w-0 flex-1">
<p className="text-sm text-muted-foreground">
@@ -74,7 +89,12 @@ export default function ProjectOverviewPage() {
{service === "self" ? " · 自助(数字咨询师陪你做,复核由企业项目负责人对照书中门禁自查)" : ""} · 往哪打 · 怎么走 · 谁负责
</p>
<h1 className="font-heading text-3xl font-bold text-brand">总览</h1>
<p className="mt-1 text-sm text-muted-foreground">按 1 → 5 往下走。先做蓝色按钮上的那件事。</p>
<p className="mt-1 text-sm text-muted-foreground">
按 1 → 5 往下走。先做蓝色按钮上的那件事。
<button type="button" onClick={() => setGuideOpen(true)} className="ml-2 text-primary hover:underline">
看操作指引
</button>
</p>
</div>
{canImport ? (
<Button variant="outline" onClick={() => setImportOpen(true)} title="把本企业已有项目里的表带进来作为草稿,再按今年改">