129 lines
4.0 KiB
TypeScript
129 lines
4.0 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { fetchAuthenticated } from "../../../auth/api-client";
|
|
import {
|
|
GAP_REASONS,
|
|
GAP_STATUSES,
|
|
type DemandGap,
|
|
type GapStatus,
|
|
} from "../../../shared/assets/assistant";
|
|
|
|
export function DemandGaps({ close }: { close: () => void }) {
|
|
const [items, setItems] = useState<DemandGap[]>([]),
|
|
[all, setAll] = useState(false),
|
|
[canManage, setCanManage] = useState(false);
|
|
const [error, setError] = useState(""),
|
|
[loading, setLoading] = useState(true),
|
|
[revision, setRevision] = useState(0);
|
|
useEffect(() => {
|
|
const controller = new AbortController();
|
|
setLoading(true);
|
|
setError("");
|
|
void fetchAuthenticated(
|
|
`/api/asset-assistant/gaps?scope=${all ? "all" : "mine"}`,
|
|
{ signal: controller.signal },
|
|
)
|
|
.then(async (r) => {
|
|
const data = await r.json();
|
|
if (!r.ok) throw new Error(data.error);
|
|
return data;
|
|
})
|
|
.then((data) => {
|
|
setItems(data.items);
|
|
setCanManage(data.canManage);
|
|
})
|
|
.catch((e) => {
|
|
if (!controller.signal.aborted) setError(e.message || "读取失败。");
|
|
})
|
|
.finally(() => {
|
|
if (!controller.signal.aborted) setLoading(false);
|
|
});
|
|
return () => controller.abort();
|
|
}, [all, revision]);
|
|
const update = async (id: number, status: GapStatus) => {
|
|
setError("");
|
|
try {
|
|
const r = await fetchAuthenticated(`/api/asset-assistant/gaps/${id}`, {
|
|
method: "PATCH",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ status }),
|
|
});
|
|
if (!r.ok) throw new Error("状态保存失败。");
|
|
setRevision((x) => x + 1);
|
|
} catch {
|
|
setError("状态保存失败,请重试。");
|
|
}
|
|
};
|
|
return (
|
|
<section className="aq-gap-panel" aria-label="待完善需求">
|
|
<div className="aq-card-top">
|
|
<div>
|
|
<h3>待完善需求</h3>
|
|
<p>保留90天 · 记录未完成的问题与反馈,用于完善分析能力</p>
|
|
</div>
|
|
<button onClick={close}>返回问数</button>
|
|
</div>
|
|
<div className="aq-result-toolbar">
|
|
{canManage && (
|
|
<label>
|
|
<input
|
|
type="checkbox"
|
|
checked={all}
|
|
onChange={(e) => setAll(e.target.checked)}
|
|
/>{" "}
|
|
查看全部用户需求
|
|
</label>
|
|
)}
|
|
<button onClick={() => setRevision((x) => x + 1)} disabled={loading}>
|
|
刷新
|
|
</button>
|
|
</div>
|
|
{error && (
|
|
<p role="alert" className="aq-error">
|
|
{error}
|
|
</p>
|
|
)}
|
|
{loading ? (
|
|
<p role="status">正在读取需求记录…</p>
|
|
) : (
|
|
<>
|
|
<p>
|
|
{items.length}项需求 · 累计
|
|
{items.reduce((n, v) => n + v.occurrences, 0)}次(最近200项)
|
|
</p>
|
|
{items.length === 0 && !error && (
|
|
<p className="aq-no-data">暂无未满足需求</p>
|
|
)}
|
|
{items.map((item) => (
|
|
<article key={item.id} className="aq-gap-item">
|
|
<strong>{item.demand}</strong>
|
|
<p>
|
|
{GAP_REASONS[item.reason]} · {item.occurrences}次 · 最近{" "}
|
|
{new Date(item.lastSeen).toLocaleString("zh-CN")}
|
|
</p>
|
|
{canManage ? (
|
|
<label>
|
|
处理状态{" "}
|
|
<select
|
|
value={item.status}
|
|
onChange={(e) =>
|
|
void update(item.id, e.target.value as GapStatus)
|
|
}
|
|
>
|
|
{Object.entries(GAP_STATUSES).map(([id, label]) => (
|
|
<option key={id} value={id}>
|
|
{label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
) : (
|
|
<span>{GAP_STATUSES[item.status]}</span>
|
|
)}
|
|
</article>
|
|
))}
|
|
</>
|
|
)}
|
|
</section>
|
|
);
|
|
}
|