merge space p4 feedback attention
This commit is contained in:
commit
675485ad0f
|
|
@ -2,6 +2,7 @@ import { getPMFlowExecutionSummary, getPMFlowTruthState, type PMFlowExecutionSum
|
||||||
import { projectsApi, type ProjectSummary } from "./projects";
|
import { projectsApi, type ProjectSummary } from "./projects";
|
||||||
import type {
|
import type {
|
||||||
ChangeItem,
|
ChangeItem,
|
||||||
|
MetricSignal,
|
||||||
PMFlowExecutionStatus,
|
PMFlowExecutionStatus,
|
||||||
PMFlowObjectStatus,
|
PMFlowObjectStatus,
|
||||||
Release,
|
Release,
|
||||||
|
|
@ -64,6 +65,20 @@ export interface SpaceReleaseAttentionItem {
|
||||||
href: string;
|
href: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SpaceFeedbackAttentionItem {
|
||||||
|
id: string;
|
||||||
|
projectId: string;
|
||||||
|
projectTitle: string;
|
||||||
|
source: "feedback" | "revenue" | "roi" | "compliance" | "iteration";
|
||||||
|
sourceLabel: string;
|
||||||
|
title: string;
|
||||||
|
summary: string;
|
||||||
|
value?: string | null;
|
||||||
|
severity: "low" | "medium" | "high";
|
||||||
|
updatedAt: string;
|
||||||
|
href: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SpaceDraftItem {
|
export interface SpaceDraftItem {
|
||||||
id: string;
|
id: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
|
@ -89,6 +104,7 @@ export interface SpaceWorkbench {
|
||||||
pendingConfirmations: SpacePendingConfirmation[];
|
pendingConfirmations: SpacePendingConfirmation[];
|
||||||
aiProgress: SpaceAIProgressItem[];
|
aiProgress: SpaceAIProgressItem[];
|
||||||
releaseAttention: SpaceReleaseAttentionItem[];
|
releaseAttention: SpaceReleaseAttentionItem[];
|
||||||
|
feedbackAttention: SpaceFeedbackAttentionItem[];
|
||||||
savedIdeas: SpaceSavedIdea[];
|
savedIdeas: SpaceSavedIdea[];
|
||||||
drafts: SpaceDraftItem[];
|
drafts: SpaceDraftItem[];
|
||||||
recentProjects: SpaceWorkbenchProject[];
|
recentProjects: SpaceWorkbenchProject[];
|
||||||
|
|
@ -481,6 +497,109 @@ function deriveReleaseAttention(snapshot: ProjectSnapshot): SpaceReleaseAttentio
|
||||||
return deriveGateAttention(snapshot);
|
return deriveGateAttention(snapshot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function feedbackSignalSource(signal: MetricSignal): SpaceFeedbackAttentionItem["source"] | null {
|
||||||
|
const text = `${signal.id} ${signal.title} ${signal.summary}`.toLowerCase();
|
||||||
|
if (text.includes("compliance") || text.includes("合规")) return "compliance";
|
||||||
|
if (signal.id.startsWith("revenue-") || text.includes("收入") || text.includes("revenue")) return "revenue";
|
||||||
|
if (signal.id.startsWith("roi-") || text.includes("roi")) return "roi";
|
||||||
|
if (text.includes("iteration") || text.includes("recommendation") || text.includes("下一轮") || text.includes("建议")) return "iteration";
|
||||||
|
if (signal.id.startsWith("feedback-") || text.includes("feedback") || text.includes("反馈")) return "feedback";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function feedbackSourceLabel(source: SpaceFeedbackAttentionItem["source"]): string {
|
||||||
|
const labels: Record<SpaceFeedbackAttentionItem["source"], string> = {
|
||||||
|
feedback: "用户反馈",
|
||||||
|
revenue: "收入信号",
|
||||||
|
roi: "ROI 信号",
|
||||||
|
compliance: "合规检查",
|
||||||
|
iteration: "下一轮建议",
|
||||||
|
};
|
||||||
|
return labels[source];
|
||||||
|
}
|
||||||
|
|
||||||
|
function feedbackAttentionTitle(source: SpaceFeedbackAttentionItem["source"], signal?: MetricSignal): string {
|
||||||
|
if (source === "feedback") return "新反馈信号";
|
||||||
|
if (source === "revenue") return "收入信号更新";
|
||||||
|
if (source === "roi") return "ROI 信号更新";
|
||||||
|
if (source === "compliance") return "合规阻塞关注";
|
||||||
|
return signal?.title || "下一轮建议待处理";
|
||||||
|
}
|
||||||
|
|
||||||
|
function feedbackAttentionSeverity(source: SpaceFeedbackAttentionItem["source"], signal?: MetricSignal): SpaceFeedbackAttentionItem["severity"] {
|
||||||
|
if (source === "compliance") return "high";
|
||||||
|
if (source === "iteration" || signal?.direction === "down") return "medium";
|
||||||
|
return "low";
|
||||||
|
}
|
||||||
|
|
||||||
|
function deriveFeedbackSignalAttention(snapshot: ProjectSnapshot): SpaceFeedbackAttentionItem[] {
|
||||||
|
const { project, truth } = snapshot;
|
||||||
|
if (!truth) return [];
|
||||||
|
const projectId = String(project.id);
|
||||||
|
const projectTitle = project.title || truth.project.name || `项目 ${projectId}`;
|
||||||
|
|
||||||
|
return truth.metricSignals
|
||||||
|
.map((signal): SpaceFeedbackAttentionItem | null => {
|
||||||
|
const source = feedbackSignalSource(signal);
|
||||||
|
if (!source) return null;
|
||||||
|
return {
|
||||||
|
id: `feedback-signal-${projectId}-${signal.id}`,
|
||||||
|
projectId,
|
||||||
|
projectTitle,
|
||||||
|
source,
|
||||||
|
sourceLabel: feedbackSourceLabel(source),
|
||||||
|
title: feedbackAttentionTitle(source, signal),
|
||||||
|
summary: signal.summary || signal.title || "发布后信号已同步,等待在 Release Growth 查看。",
|
||||||
|
value: signal.value,
|
||||||
|
severity: feedbackAttentionSeverity(source, signal),
|
||||||
|
updatedAt: project.updated_at,
|
||||||
|
href: releaseGrowthHref(projectId),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((item): item is SpaceFeedbackAttentionItem => Boolean(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
function isIterationChangeItem(item: ChangeItem): boolean {
|
||||||
|
const text = `${item.objectType} ${item.title} ${item.summary} ${item.diff}`.toLowerCase();
|
||||||
|
return (
|
||||||
|
item.status === "pending_review" &&
|
||||||
|
(text.includes("feedback") ||
|
||||||
|
text.includes("反馈") ||
|
||||||
|
text.includes("iteration") ||
|
||||||
|
text.includes("recommendation") ||
|
||||||
|
text.includes("下一轮") ||
|
||||||
|
text.includes("增长") ||
|
||||||
|
text.includes("release"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deriveIterationAttention(snapshot: ProjectSnapshot): SpaceFeedbackAttentionItem[] {
|
||||||
|
const { project, truth } = snapshot;
|
||||||
|
if (!truth) return [];
|
||||||
|
const projectId = String(project.id);
|
||||||
|
const projectTitle = project.title || truth.project.name || `项目 ${projectId}`;
|
||||||
|
|
||||||
|
return pendingChangeItems(truth)
|
||||||
|
.filter(isIterationChangeItem)
|
||||||
|
.map((item) => ({
|
||||||
|
id: `feedback-iteration-${projectId}-${item.id}`,
|
||||||
|
projectId,
|
||||||
|
projectTitle,
|
||||||
|
source: "iteration" as const,
|
||||||
|
sourceLabel: "下一轮建议",
|
||||||
|
title: "下一轮建议待处理",
|
||||||
|
summary: item.summary || item.diff || "发布后反馈已形成待确认建议,需要回到 Change Review 处理。",
|
||||||
|
value: null,
|
||||||
|
severity: item.riskLevel === "high" ? "high" : "medium",
|
||||||
|
updatedAt: truth.lastWritebackAt || project.updated_at,
|
||||||
|
href: reviewHref(projectId, item.changeSetId),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function deriveFeedbackAttention(snapshot: ProjectSnapshot): SpaceFeedbackAttentionItem[] {
|
||||||
|
return [...deriveFeedbackSignalAttention(snapshot), ...deriveIterationAttention(snapshot)];
|
||||||
|
}
|
||||||
|
|
||||||
function deriveDrafts(snapshot: ProjectSnapshot): SpaceDraftItem[] {
|
function deriveDrafts(snapshot: ProjectSnapshot): SpaceDraftItem[] {
|
||||||
const { project, truth } = snapshot;
|
const { project, truth } = snapshot;
|
||||||
if (!truth) return [];
|
if (!truth) return [];
|
||||||
|
|
@ -548,6 +667,7 @@ export const spaceApi = {
|
||||||
pendingConfirmations: sortByDate(snapshots.flatMap(derivePending)).slice(0, 8),
|
pendingConfirmations: sortByDate(snapshots.flatMap(derivePending)).slice(0, 8),
|
||||||
aiProgress: sortByDate(snapshots.flatMap(deriveAIProgress)).slice(0, 8),
|
aiProgress: sortByDate(snapshots.flatMap(deriveAIProgress)).slice(0, 8),
|
||||||
releaseAttention: sortByDate(snapshots.flatMap(deriveReleaseAttention)).slice(0, 8),
|
releaseAttention: sortByDate(snapshots.flatMap(deriveReleaseAttention)).slice(0, 8),
|
||||||
|
feedbackAttention: sortByDate(snapshots.flatMap(deriveFeedbackAttention)).slice(0, 8),
|
||||||
savedIdeas: [],
|
savedIdeas: [],
|
||||||
drafts: sortByDate(snapshots.flatMap(deriveDrafts)).slice(0, 8),
|
drafts: sortByDate(snapshots.flatMap(deriveDrafts)).slice(0, 8),
|
||||||
recentProjects: sortByDate(workbenchProjects),
|
recentProjects: sortByDate(workbenchProjects),
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ const emptyWorkbench: SpaceWorkbench = {
|
||||||
pendingConfirmations: [],
|
pendingConfirmations: [],
|
||||||
aiProgress: [],
|
aiProgress: [],
|
||||||
releaseAttention: [],
|
releaseAttention: [],
|
||||||
|
feedbackAttention: [],
|
||||||
savedIdeas: [],
|
savedIdeas: [],
|
||||||
drafts: [],
|
drafts: [],
|
||||||
recentProjects: [],
|
recentProjects: [],
|
||||||
|
|
@ -42,6 +43,8 @@ describe("PersonalSpacePage", () => {
|
||||||
expect(screen.getAllByText("待我确认")).not.toHaveLength(0);
|
expect(screen.getAllByText("待我确认")).not.toHaveLength(0);
|
||||||
expect(screen.getAllByText("发布关注")).not.toHaveLength(0);
|
expect(screen.getAllByText("发布关注")).not.toHaveLength(0);
|
||||||
expect(screen.getAllByText("暂无发布验证记录")).not.toHaveLength(0);
|
expect(screen.getAllByText("暂无发布验证记录")).not.toHaveLength(0);
|
||||||
|
expect(screen.getAllByText("发布后反馈关注")).not.toHaveLength(0);
|
||||||
|
expect(screen.getAllByText("暂无发布后反馈")).not.toHaveLength(0);
|
||||||
expect(screen.getAllByText("AI 正在推进")).not.toHaveLength(0);
|
expect(screen.getAllByText("AI 正在推进")).not.toHaveLength(0);
|
||||||
expect(screen.getAllByText("暂存想法")).not.toHaveLength(0);
|
expect(screen.getAllByText("暂存想法")).not.toHaveLength(0);
|
||||||
expect(screen.getAllByText("草案箱")).not.toHaveLength(0);
|
expect(screen.getAllByText("草案箱")).not.toHaveLength(0);
|
||||||
|
|
@ -144,6 +147,60 @@ describe("PersonalSpacePage", () => {
|
||||||
href: "/release-growth?projectId=19",
|
href: "/release-growth?projectId=19",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
feedbackAttention: [
|
||||||
|
{
|
||||||
|
id: "feedback-signal-17-feedback-1",
|
||||||
|
projectId: "17",
|
||||||
|
projectTitle: "真实项目",
|
||||||
|
source: "feedback",
|
||||||
|
sourceLabel: "用户反馈",
|
||||||
|
title: "新反馈信号",
|
||||||
|
summary: "用户反馈集中在 onboarding 卡顿。",
|
||||||
|
value: "12",
|
||||||
|
severity: "medium",
|
||||||
|
updatedAt: "2026-04-26T08:00:00.000Z",
|
||||||
|
href: "/release-growth?projectId=17",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "feedback-signal-17-revenue-1",
|
||||||
|
projectId: "17",
|
||||||
|
projectTitle: "真实项目",
|
||||||
|
source: "revenue",
|
||||||
|
sourceLabel: "收入信号",
|
||||||
|
title: "收入信号更新",
|
||||||
|
summary: "付费转化新增。",
|
||||||
|
value: "USD 1200",
|
||||||
|
severity: "low",
|
||||||
|
updatedAt: "2026-04-26T08:00:00.000Z",
|
||||||
|
href: "/release-growth?projectId=17",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "feedback-signal-17-compliance-1",
|
||||||
|
projectId: "17",
|
||||||
|
projectTitle: "真实项目",
|
||||||
|
source: "compliance",
|
||||||
|
sourceLabel: "合规检查",
|
||||||
|
title: "合规阻塞关注",
|
||||||
|
summary: "隐私文案未通过。",
|
||||||
|
value: "blocked",
|
||||||
|
severity: "high",
|
||||||
|
updatedAt: "2026-04-26T08:00:00.000Z",
|
||||||
|
href: "/release-growth?projectId=17",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "feedback-iteration-17-change-92",
|
||||||
|
projectId: "17",
|
||||||
|
projectTitle: "真实项目",
|
||||||
|
source: "iteration",
|
||||||
|
sourceLabel: "下一轮建议",
|
||||||
|
title: "下一轮建议待处理",
|
||||||
|
summary: "基于发布后反馈生成下一轮优化建议。",
|
||||||
|
value: null,
|
||||||
|
severity: "medium",
|
||||||
|
updatedAt: "2026-04-26T08:00:00.000Z",
|
||||||
|
href: "/change-review?projectId=17&changeSetId=92",
|
||||||
|
},
|
||||||
|
],
|
||||||
drafts: [
|
drafts: [
|
||||||
{
|
{
|
||||||
id: "17-需求-draft-1",
|
id: "17-需求-draft-1",
|
||||||
|
|
@ -201,7 +258,24 @@ describe("PersonalSpacePage", () => {
|
||||||
"href",
|
"href",
|
||||||
"/release-growth?projectId=19"
|
"/release-growth?projectId=19"
|
||||||
);
|
);
|
||||||
|
expect(screen.getAllByRole("link", { name: /新反馈信号/ })[0]).toHaveAttribute(
|
||||||
|
"href",
|
||||||
|
"/release-growth?projectId=17"
|
||||||
|
);
|
||||||
|
expect(screen.getAllByRole("link", { name: /收入信号更新/ })[0]).toHaveAttribute(
|
||||||
|
"href",
|
||||||
|
"/release-growth?projectId=17"
|
||||||
|
);
|
||||||
|
expect(screen.getAllByRole("link", { name: /合规阻塞关注/ })[0]).toHaveAttribute(
|
||||||
|
"href",
|
||||||
|
"/release-growth?projectId=17"
|
||||||
|
);
|
||||||
|
expect(screen.getAllByRole("link", { name: /下一轮建议待处理/ })[0]).toHaveAttribute(
|
||||||
|
"href",
|
||||||
|
"/change-review?projectId=17&changeSetId=92"
|
||||||
|
);
|
||||||
expect(screen.getAllByText("测试失败: typecheck 未通过")).not.toHaveLength(0);
|
expect(screen.getAllByText("测试失败: typecheck 未通过")).not.toHaveLength(0);
|
||||||
expect(screen.queryByRole("button", { name: /创建发布验证|运行发布验证|重新验证/ })).not.toBeInTheDocument();
|
expect(screen.queryByRole("button", { name: /创建发布验证|运行发布验证|重新验证/ })).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByRole("button", { name: /创建反馈|写入反馈|记录收入|运行合规/ })).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import {
|
||||||
spaceApi,
|
spaceApi,
|
||||||
type SpaceAIProgressItem,
|
type SpaceAIProgressItem,
|
||||||
type SpaceDraftItem,
|
type SpaceDraftItem,
|
||||||
|
type SpaceFeedbackAttentionItem,
|
||||||
type SpacePendingConfirmation,
|
type SpacePendingConfirmation,
|
||||||
type SpaceReleaseAttentionItem,
|
type SpaceReleaseAttentionItem,
|
||||||
type SpaceSavedIdea,
|
type SpaceSavedIdea,
|
||||||
|
|
@ -107,6 +108,7 @@ function DesktopSpace({
|
||||||
pendingCount={data?.pendingConfirmations.length ?? 0}
|
pendingCount={data?.pendingConfirmations.length ?? 0}
|
||||||
aiCount={data?.aiProgress.length ?? 0}
|
aiCount={data?.aiProgress.length ?? 0}
|
||||||
releaseCount={data?.releaseAttention.length ?? 0}
|
releaseCount={data?.releaseAttention.length ?? 0}
|
||||||
|
feedbackCount={data?.feedbackAttention.length ?? 0}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isLoading ? <WorkbenchLoading /> : error ? <WorkbenchError error={error} onRetry={onRetry} /> : null}
|
{isLoading ? <WorkbenchLoading /> : error ? <WorkbenchError error={error} onRetry={onRetry} /> : null}
|
||||||
|
|
@ -141,6 +143,8 @@ function DesktopSpace({
|
||||||
|
|
||||||
<ReleaseAttentionPanel items={data.releaseAttention} />
|
<ReleaseAttentionPanel items={data.releaseAttention} />
|
||||||
|
|
||||||
|
<FeedbackAttentionPanel items={data.feedbackAttention} />
|
||||||
|
|
||||||
<div className="grid gap-5 xl:grid-cols-3">
|
<div className="grid gap-5 xl:grid-cols-3">
|
||||||
<SavedIdeasBox ideas={data.savedIdeas} />
|
<SavedIdeasBox ideas={data.savedIdeas} />
|
||||||
<DraftBox drafts={data.drafts} />
|
<DraftBox drafts={data.drafts} />
|
||||||
|
|
@ -200,7 +204,7 @@ function MobileSpace({
|
||||||
<div className="mt-5 grid grid-cols-4 gap-2 text-center">
|
<div className="mt-5 grid grid-cols-4 gap-2 text-center">
|
||||||
<MobileMetric label="待确认" value={data?.pendingConfirmations.length ?? 0} />
|
<MobileMetric label="待确认" value={data?.pendingConfirmations.length ?? 0} />
|
||||||
<MobileMetric label="发布" value={data?.releaseAttention.length ?? 0} />
|
<MobileMetric label="发布" value={data?.releaseAttention.length ?? 0} />
|
||||||
<MobileMetric label="AI 推进" value={data?.aiProgress.length ?? 0} />
|
<MobileMetric label="反馈" value={data?.feedbackAttention.length ?? 0} />
|
||||||
<MobileMetric label="草案" value={data?.drafts.length ?? 0} />
|
<MobileMetric label="草案" value={data?.drafts.length ?? 0} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
@ -242,6 +246,16 @@ function MobileSpace({
|
||||||
</div>
|
</div>
|
||||||
</MobileSection>
|
</MobileSection>
|
||||||
|
|
||||||
|
<MobileSection title="发布后反馈关注" badge={data.feedbackAttention.length}>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{data.feedbackAttention.length ? (
|
||||||
|
data.feedbackAttention.slice(0, 4).map((item) => <MobileFeedbackAttention key={item.id} item={item} />)
|
||||||
|
) : (
|
||||||
|
<InlineEmpty title="暂无发布后反馈" description="有真实反馈、收入信号、ROI、合规阻塞或下一轮建议时会显示;不会虚构结果。" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</MobileSection>
|
||||||
|
|
||||||
<MobileSection title="AI 正在推进" badge={data.aiProgress.length}>
|
<MobileSection title="AI 正在推进" badge={data.aiProgress.length}>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{data.aiProgress.length ? (
|
{data.aiProgress.length ? (
|
||||||
|
|
@ -297,6 +311,7 @@ function WorkbenchHeader({
|
||||||
pendingCount,
|
pendingCount,
|
||||||
aiCount,
|
aiCount,
|
||||||
releaseCount,
|
releaseCount,
|
||||||
|
feedbackCount,
|
||||||
}: {
|
}: {
|
||||||
generatedAt?: string;
|
generatedAt?: string;
|
||||||
isFetching: boolean;
|
isFetching: boolean;
|
||||||
|
|
@ -304,6 +319,7 @@ function WorkbenchHeader({
|
||||||
pendingCount: number;
|
pendingCount: number;
|
||||||
aiCount: number;
|
aiCount: number;
|
||||||
releaseCount: number;
|
releaseCount: number;
|
||||||
|
feedbackCount: number;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<section className="rounded-[24px] border border-[#dbe5f2] bg-white px-6 py-5 shadow-[0_14px_40px_rgba(15,23,42,0.06)]">
|
<section className="rounded-[24px] border border-[#dbe5f2] bg-white px-6 py-5 shadow-[0_14px_40px_rgba(15,23,42,0.06)]">
|
||||||
|
|
@ -318,10 +334,11 @@ function WorkbenchHeader({
|
||||||
聚合继续推进、待确认、执行中、暂存想法、草案箱和最近项目;点击项目后回到主流程处理。
|
聚合继续推进、待确认、执行中、暂存想法、草案箱和最近项目;点击项目后回到主流程处理。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid min-w-[520px] grid-cols-4 gap-3">
|
<div className="grid min-w-[640px] grid-cols-5 gap-3">
|
||||||
<MetricTile label="最近项目" value={projectCount} />
|
<MetricTile label="最近项目" value={projectCount} />
|
||||||
<MetricTile label="待确认" value={pendingCount} tone="orange" />
|
<MetricTile label="待确认" value={pendingCount} tone="orange" />
|
||||||
<MetricTile label="发布关注" value={releaseCount} tone="rose" />
|
<MetricTile label="发布关注" value={releaseCount} tone="rose" />
|
||||||
|
<MetricTile label="反馈关注" value={feedbackCount} tone="emerald" />
|
||||||
<MetricTile label="执行中" value={aiCount} tone="blue" />
|
<MetricTile label="执行中" value={aiCount} tone="blue" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -473,6 +490,50 @@ function ReleaseAttentionRow({ item }: { item: SpaceReleaseAttentionItem }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function FeedbackAttentionPanel({ items }: { items: SpaceFeedbackAttentionItem[] }) {
|
||||||
|
return (
|
||||||
|
<section className="rounded-[20px] border border-[#dde6f2] bg-white">
|
||||||
|
<PanelHeader
|
||||||
|
title="发布后反馈关注"
|
||||||
|
description="只读聚合发布后反馈、收入、ROI、合规和下一轮建议;处理动作回到 Release Growth 或 Change Review。"
|
||||||
|
badge={items.length}
|
||||||
|
className="px-5 pt-5"
|
||||||
|
/>
|
||||||
|
<div className="mt-4 divide-y divide-[#eef3f8]">
|
||||||
|
{items.length ? (
|
||||||
|
items.slice(0, 5).map((item) => <FeedbackAttentionRow key={item.id} item={item} />)
|
||||||
|
) : (
|
||||||
|
<InlineEmpty title="暂无发布后反馈" description="有真实反馈、收入信号、ROI、合规阻塞或下一轮建议时会显示;不会虚构结果。" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FeedbackAttentionRow({ item }: { item: SpaceFeedbackAttentionItem }) {
|
||||||
|
const tone = feedbackAttentionTone(item);
|
||||||
|
return (
|
||||||
|
<Link to={item.href} className="grid grid-cols-[44px_minmax(0,1fr)_120px] items-center gap-4 px-5 py-4 transition hover:bg-[#f8fbff]">
|
||||||
|
<span className={cn("flex h-10 w-10 items-center justify-center rounded-xl", tone.iconClass)}>
|
||||||
|
<MessageCircle className="h-5 w-5" />
|
||||||
|
</span>
|
||||||
|
<span className="min-w-0">
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<span className="truncate text-sm font-semibold text-slate-950">{item.title}</span>
|
||||||
|
<span className={cn("shrink-0 rounded-md px-2 py-0.5 text-xs font-semibold", tone.badgeClass)}>{item.sourceLabel}</span>
|
||||||
|
</span>
|
||||||
|
<span className="mt-1 block line-clamp-2 text-xs leading-5 text-slate-500">{item.summary}</span>
|
||||||
|
<span className="mt-1 block truncate text-xs text-slate-500">
|
||||||
|
{item.projectTitle} · {item.value || "无数值"} · {formatRelativeTime(item.updatedAt)}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span className="justify-self-end rounded-lg border border-[#cfe0ff] px-3 py-1.5 text-xs font-semibold text-[#2463EB]">
|
||||||
|
{item.href.startsWith("/change-review") ? "去确认" : "看反馈"}
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function SavedIdeasBox({ ideas }: { ideas: SpaceSavedIdea[] }) {
|
function SavedIdeasBox({ ideas }: { ideas: SpaceSavedIdea[] }) {
|
||||||
return (
|
return (
|
||||||
<section className="rounded-[20px] border border-[#dbe5f2] bg-white p-5">
|
<section className="rounded-[20px] border border-[#dbe5f2] bg-white p-5">
|
||||||
|
|
@ -618,8 +679,9 @@ function PanelHeader({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MetricTile({ label, value, tone = "slate" }: { label: string; value: number; tone?: "slate" | "orange" | "blue" | "rose" }) {
|
function MetricTile({ label, value, tone = "slate" }: { label: string; value: number; tone?: "slate" | "orange" | "blue" | "rose" | "emerald" }) {
|
||||||
const toneClass = tone === "orange" ? "text-orange-500" : tone === "blue" ? "text-[#2463EB]" : tone === "rose" ? "text-rose-600" : "text-slate-950";
|
const toneClass =
|
||||||
|
tone === "orange" ? "text-orange-500" : tone === "blue" ? "text-[#2463EB]" : tone === "rose" ? "text-rose-600" : tone === "emerald" ? "text-emerald-600" : "text-slate-950";
|
||||||
return (
|
return (
|
||||||
<div className="rounded-2xl border border-[#e3eaf5] bg-[#fbfcff] px-4 py-3">
|
<div className="rounded-2xl border border-[#e3eaf5] bg-[#fbfcff] px-4 py-3">
|
||||||
<div className="text-xs text-slate-500">{label}</div>
|
<div className="text-xs text-slate-500">{label}</div>
|
||||||
|
|
@ -723,6 +785,25 @@ function MobileReleaseAttention({ item }: { item: SpaceReleaseAttentionItem }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function MobileFeedbackAttention({ item }: { item: SpaceFeedbackAttentionItem }) {
|
||||||
|
const tone = feedbackAttentionTone(item);
|
||||||
|
return (
|
||||||
|
<Link to={item.href} className="block rounded-[18px] border border-[#dbe5f2] bg-white p-4">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<span className={cn("flex h-10 w-10 shrink-0 items-center justify-center rounded-xl", tone.iconClass)}>
|
||||||
|
<MessageCircle className="h-5 w-5" />
|
||||||
|
</span>
|
||||||
|
<span className="min-w-0 flex-1">
|
||||||
|
<span className="block truncate text-sm font-semibold text-slate-950">{item.title}</span>
|
||||||
|
<span className="mt-1 block line-clamp-2 text-xs leading-5 text-slate-500">{item.summary}</span>
|
||||||
|
<span className="mt-2 block truncate text-xs text-slate-500">{item.projectTitle} · {item.value || "无数值"}</span>
|
||||||
|
</span>
|
||||||
|
<span className={cn("shrink-0 rounded-md px-2 py-1 text-xs font-semibold", tone.badgeClass)}>{item.sourceLabel}</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function MobileRecentProjectRow({ project }: { project: SpaceWorkbenchProject }) {
|
function MobileRecentProjectRow({ project }: { project: SpaceWorkbenchProject }) {
|
||||||
return (
|
return (
|
||||||
<Link to={project.href} className="flex items-center justify-between gap-3 py-3 first:pt-3 last:pb-3">
|
<Link to={project.href} className="flex items-center justify-between gap-3 py-3 first:pt-3 last:pb-3">
|
||||||
|
|
@ -862,6 +943,25 @@ function releaseAttentionTone(item: SpaceReleaseAttentionItem): { iconClass: str
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function feedbackAttentionTone(item: SpaceFeedbackAttentionItem): { iconClass: string; badgeClass: string } {
|
||||||
|
if (item.severity === "high") {
|
||||||
|
return {
|
||||||
|
iconClass: "bg-rose-50 text-rose-600",
|
||||||
|
badgeClass: "bg-rose-50 text-rose-600",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (item.severity === "medium") {
|
||||||
|
return {
|
||||||
|
iconClass: "bg-orange-50 text-orange-600",
|
||||||
|
badgeClass: "bg-orange-50 text-orange-600",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
iconClass: "bg-emerald-50 text-emerald-600",
|
||||||
|
badgeClass: "bg-emerald-50 text-emerald-600",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function formatRelativeTime(value?: string): string {
|
function formatRelativeTime(value?: string): string {
|
||||||
if (!value) return "未记录时间";
|
if (!value) return "未记录时间";
|
||||||
const timestamp = new Date(value).getTime();
|
const timestamp = new Date(value).getTime();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue