mirror of
https://gh-proxy.org/https://github.com/klinsmaya/portal-template-default.git
synced 2026-11-08 00:57:57 +08:00
首次进项目弹操作指引:四步一页一步,本机记住,总览可重开
Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_017Dhew22GiDVT9oKNm3U7gH
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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="把本企业已有项目里的表带进来作为草稿,再按今年改">
|
||||
|
||||
Reference in New Issue
Block a user