新增风险项撤回操作并优化队列布局

This commit is contained in:
kx_guo 2026-09-18 17:31:14 +08:00
parent e172d82eb5
commit 9359f11cd0
3 changed files with 134 additions and 24 deletions

View File

@ -28,6 +28,10 @@ export function updateRiskItem(itemId: number, payload: RiskItemUpdate) {
return http.patch<ApiResponse<RiskItem>>(`/v1/risk-items/${itemId}`, payload);
}
export function withdrawRiskItem(itemId: number) {
return http.post<ApiResponse<null>>(`/v1/risk-items/${itemId}/withdraw`);
}
export function reviewRiskItem(itemId: number, payload: ReviewAction) {
return http.post<ApiResponse<ReviewRecord>>(
`/v1/risk-items/${itemId}/review`,

View File

@ -15,6 +15,7 @@ import {
RefreshCw,
Save,
SlidersHorizontal,
Undo2,
Upload,
Users,
X,
@ -40,6 +41,7 @@ const statusFilter = ref("待审核");
const expandedId = ref<number | null>(null);
const editingId = ref<number | null>(null);
const reviewingId = ref<number | null>(null);
const withdrawingId = ref<number | null>(null);
const ocrOpen = ref(false);
const attachmentError = ref("");
const modelConfig = ref<ModelWeightConfig | null>(null);
@ -245,6 +247,20 @@ async function review(item: RiskItem, action: "通过" | "驳回") {
reviewingId.value = null;
}
}
async function withdraw(item: RiskItem) {
if (withdrawingId.value !== null) return;
if (!window.confirm("确认撤回该风险项吗?撤回后将不再显示在审核队列中。"))
return;
withdrawingId.value = item.id;
try {
await store.withdraw(item.id);
if (expandedId.value === item.id) expandedId.value = null;
} catch {
// The exact API error is shown above the queue.
} finally {
withdrawingId.value = null;
}
}
function formatDate(value: string | null) {
if (!value) return "未记录时间";
return new Intl.DateTimeFormat("zh-CN", {
@ -374,8 +390,9 @@ onMounted(() => {
class="review-item"
:class="{ expanded: expandedId === item.id }"
>
<div class="review-summary">
<button
class="review-summary"
class="review-summary__content"
type="button"
@click="toggleItem(item.id)"
>
@ -389,7 +406,21 @@ onMounted(() => {
>{{ item.productCategory || "未分类" }} ·
{{ formatDate(item.submittedAt || item.createdAt) }}</small
></span
><span class="review-meta"
>
</button>
<button
v-if="item.status === '待审核' || item.status === '待预审'"
class="review-withdraw"
type="button"
:disabled="withdrawingId !== null"
title="撤回风险项"
@click="withdraw(item)"
>
<Undo2 :size="12" />{{
withdrawingId === item.id ? "撤回中" : "撤回"
}}
</button>
<span class="review-meta"
><span
class="level-tag"
:class="levelClass(item.defectLevel)"
@ -401,11 +432,17 @@ onMounted(() => {
:class="item.status === '待审核' ? 'is-pending' : 'is-pre'"
>{{ item.status }}</span
></span
><ChevronUp
v-if="expandedId === item.id"
:size="17"
/><ChevronDown v-else :size="17" />
>
<button
class="review-toggle"
type="button"
:aria-label="expandedId === item.id ? '收起风险项' : '展开风险项'"
@click="toggleItem(item.id)"
>
<ChevronUp v-if="expandedId === item.id" :size="17" />
<ChevronDown v-else :size="17" />
</button>
</div>
<div v-if="expandedId === item.id" class="review-detail">
<template v-if="editingId === item.id"
><div class="edit-grid">
@ -1068,16 +1105,68 @@ onMounted(() => {
.review-summary {
width: 100%;
gap: 11px;
border: 0;
padding: 14px 5px;
padding: 8px 5px;
background: transparent;
color: var(--color-text);
text-align: left;
}
.review-summary:hover,
.review-item.expanded .review-summary {
background: var(--color-bg-warm);
}
.review-summary__content {
display: flex;
min-width: 0;
flex: 1;
align-items: center;
gap: 11px;
border: 0;
padding: 6px 0;
background: transparent;
color: var(--color-text);
font: inherit;
text-align: left;
cursor: pointer;
}
.review-withdraw {
display: inline-flex;
flex: 0 0 auto;
min-height: 26px;
align-items: center;
gap: 4px;
border: 1px solid var(--color-border);
border-radius: 5px;
padding: 0 8px;
background: var(--color-surface);
color: var(--color-text-muted);
font: inherit;
font-size: 10px;
cursor: pointer;
}
.review-withdraw:hover:not(:disabled) {
border-color: #fecaca;
background: #fef2f2;
color: var(--color-danger);
}
.review-withdraw:disabled {
cursor: wait;
opacity: 0.55;
}
.review-toggle {
display: grid;
width: 28px;
height: 28px;
flex: 0 0 28px;
place-items: center;
border: 0;
border-radius: 5px;
padding: 0;
background: transparent;
color: var(--color-text-secondary);
cursor: pointer;
}
.review-toggle:hover {
background: var(--color-surface);
}
.source-badge {
display: inline-flex;
min-width: 52px;

View File

@ -7,6 +7,7 @@ import {
fetchPendingRiskItems,
reviewRiskItem,
updateRiskItem,
withdrawRiskItem,
} from "@/api/riskItems";
import type {
RiskItem,
@ -76,6 +77,21 @@ export const useReviewStore = defineStore("review", () => {
}
}
async function withdraw(itemId: number) {
error.value = null;
try {
await withdrawRiskItem(itemId);
items.value = items.value.filter((item) => item.id !== itemId);
} catch (reason) {
error.value = axios.isAxiosError(reason)
? String(reason.response?.data?.message || "撤回失败")
: reason instanceof Error
? reason.message
: "撤回失败";
throw reason;
}
}
return {
items,
loading,
@ -85,5 +101,6 @@ export const useReviewStore = defineStore("review", () => {
submit,
update,
review,
withdraw,
};
});