|
|
|
|
@ -0,0 +1,668 @@
|
|
|
|
|
<!-- 能源报告审核任务列表页 -->
|
|
|
|
|
<template>
|
|
|
|
|
<div class="review-list-page">
|
|
|
|
|
<!-- 统计卡片 -->
|
|
|
|
|
<div class="stats-bar">
|
|
|
|
|
<div class="stat-card" @click="filterByStatus(undefined)" :class="{ active: filterStatus === undefined }">
|
|
|
|
|
<div class="stat-icon total">
|
|
|
|
|
<ElIcon size="28"><Document /></ElIcon>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="stat-info">
|
|
|
|
|
<div class="stat-value">{{ stats.total }}</div>
|
|
|
|
|
<div class="stat-label">提交任务数</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="stat-card" @click="filterByStatus(2)" :class="{ active: filterStatus === 2 }">
|
|
|
|
|
<div class="stat-icon success">
|
|
|
|
|
<ElIcon size="28"><CircleCheck /></ElIcon>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="stat-info">
|
|
|
|
|
<div class="stat-value">{{ stats.reviewed }}</div>
|
|
|
|
|
<div class="stat-label">已审查任务</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="stat-card" @click="filterByStatus(0)" :class="{ active: filterStatus === 0 }">
|
|
|
|
|
<div class="stat-icon pending">
|
|
|
|
|
<ElIcon size="28"><Clock /></ElIcon>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="stat-info">
|
|
|
|
|
<div class="stat-value">{{ stats.pending }}</div>
|
|
|
|
|
<div class="stat-label">待审查任务</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="stat-card" @click="filterByStatus(3)" :class="{ active: filterStatus === 3 }">
|
|
|
|
|
<div class="stat-icon failed">
|
|
|
|
|
<ElIcon size="28"><CircleClose /></ElIcon>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="stat-info">
|
|
|
|
|
<div class="stat-value">{{ stats.failed }}</div>
|
|
|
|
|
<div class="stat-label">审查失败</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="time-range-wrap">
|
|
|
|
|
<span class="time-label">时间范围:</span>
|
|
|
|
|
<ElSelect v-model="timeRange" size="default" class="time-select" @change="loadList">
|
|
|
|
|
<ElOption label="今日" value="today" />
|
|
|
|
|
<ElOption label="近7天" value="7days" />
|
|
|
|
|
<ElOption label="近30天" value="30days" />
|
|
|
|
|
<ElOption label="全部" value="all" />
|
|
|
|
|
</ElSelect>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 查询条件 -->
|
|
|
|
|
<div class="search-bar">
|
|
|
|
|
<div class="search-row">
|
|
|
|
|
<div class="search-item">
|
|
|
|
|
<span class="search-label">任务名称</span>
|
|
|
|
|
<ElInput
|
|
|
|
|
v-model="queryParams.task_name"
|
|
|
|
|
placeholder="请输入任务名称"
|
|
|
|
|
clearable
|
|
|
|
|
class="search-input"
|
|
|
|
|
@keyup.enter="handleSearch"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="search-item">
|
|
|
|
|
<span class="search-label">状态</span>
|
|
|
|
|
<ElSelect v-model="queryParams.status" placeholder="请选择状态" clearable class="search-input">
|
|
|
|
|
<ElOption label="未审查" :value="0" />
|
|
|
|
|
<ElOption label="审查中" :value="1" />
|
|
|
|
|
<ElOption label="审查完成" :value="2" />
|
|
|
|
|
<ElOption label="审查失败" :value="3" />
|
|
|
|
|
</ElSelect>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="search-item">
|
|
|
|
|
<span class="search-label">创建时间</span>
|
|
|
|
|
<ElDatePicker
|
|
|
|
|
v-model="queryParams.created_time"
|
|
|
|
|
type="daterange"
|
|
|
|
|
range-separator="-"
|
|
|
|
|
start-placeholder="开始日期"
|
|
|
|
|
end-placeholder="结束日期"
|
|
|
|
|
value-format="YYYY-MM-DD"
|
|
|
|
|
class="search-input-date"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="search-item">
|
|
|
|
|
<span class="search-label">审查人</span>
|
|
|
|
|
<ElInput
|
|
|
|
|
v-model="queryParams.reviewer_name"
|
|
|
|
|
placeholder="请输入审查人"
|
|
|
|
|
clearable
|
|
|
|
|
class="search-input"
|
|
|
|
|
@keyup.enter="handleSearch"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="search-actions">
|
|
|
|
|
<ElButton type="primary" :icon="Search" @click="handleSearch">查询</ElButton>
|
|
|
|
|
<ElButton :icon="Refresh" @click="handleReset">重置</ElButton>
|
|
|
|
|
<ElButton type="primary" :icon="Plus" @click="handleCreate">新增任务</ElButton>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 任务表格 -->
|
|
|
|
|
<div class="table-card">
|
|
|
|
|
<ElTable
|
|
|
|
|
v-loading="loading"
|
|
|
|
|
:data="taskList"
|
|
|
|
|
stripe
|
|
|
|
|
border
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
:header-cell-style="{ background: '#f9fafb', color: '#374151', fontWeight: 600 }"
|
|
|
|
|
>
|
|
|
|
|
<ElTableColumn prop="task_name" label="任务名称" min-width="200" show-overflow-tooltip>
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<ElLink type="primary" @click="handleDetail(row)">
|
|
|
|
|
{{ row.task_name }}
|
|
|
|
|
</ElLink>
|
|
|
|
|
</template>
|
|
|
|
|
</ElTableColumn>
|
|
|
|
|
<ElTableColumn prop="status" label="状态" width="110" align="center">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<ElTag :type="statusTagType(row.status)" effect="light" size="small">
|
|
|
|
|
{{ statusText(row.status) }}
|
|
|
|
|
</ElTag>
|
|
|
|
|
</template>
|
|
|
|
|
</ElTableColumn>
|
|
|
|
|
<ElTableColumn label="审查进度" width="180" align="center">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<div class="progress-cell">
|
|
|
|
|
<ElProgress
|
|
|
|
|
:percentage="row.progress ?? (row.status === 2 ? 100 : row.status === 0 ? 0 : row.progress ?? 0)"
|
|
|
|
|
:status="row.status === 3 ? 'exception' : row.status === 2 ? 'success' : undefined"
|
|
|
|
|
:stroke-width="10"
|
|
|
|
|
:show-text="true"
|
|
|
|
|
:format="(p: number) => row.status === 1 ? `${p}%` : (row.status === 2 ? '100%' : row.status === 3 ? '失败' : `${p}%`)"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
</ElTableColumn>
|
|
|
|
|
<ElTableColumn prop="updated_time" label="更新时间" width="170" align="center">
|
|
|
|
|
<template #default="{ row }">{{ formatDateTime(row.updated_time) }}</template>
|
|
|
|
|
</ElTableColumn>
|
|
|
|
|
<ElTableColumn prop="created_time" label="创建时间" width="170" align="center">
|
|
|
|
|
<template #default="{ row }">{{ formatDateTime(row.created_time) }}</template>
|
|
|
|
|
</ElTableColumn>
|
|
|
|
|
<ElTableColumn prop="reviewer_name" label="审查人" width="100" align="center">
|
|
|
|
|
<template #default="{ row }">{{ row.reviewer_name || "-" }}</template>
|
|
|
|
|
</ElTableColumn>
|
|
|
|
|
<ElTableColumn label="审核文档" align="center">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<ElLink
|
|
|
|
|
v-if="row.file_url"
|
|
|
|
|
type="primary"
|
|
|
|
|
:icon="View"
|
|
|
|
|
@click="handlePreviewDoc(row)"
|
|
|
|
|
>
|
|
|
|
|
{{ row.original_name ? row.original_name : "未上传" }}
|
|
|
|
|
</ElLink>
|
|
|
|
|
<span v-else class="text-muted">未上传</span>
|
|
|
|
|
</template>
|
|
|
|
|
</ElTableColumn>
|
|
|
|
|
<ElTableColumn label="操作" width="260" align="center" fixed="right">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<ElButton type="primary" link size="small" @click="handleViewResult(row)">查看</ElButton>
|
|
|
|
|
<ElButton
|
|
|
|
|
type="primary"
|
|
|
|
|
link
|
|
|
|
|
size="small"
|
|
|
|
|
:disabled="row.status !== 0 && row.status !== 3"
|
|
|
|
|
@click="handleReview(row)"
|
|
|
|
|
>
|
|
|
|
|
审查
|
|
|
|
|
</ElButton>
|
|
|
|
|
<ElButton
|
|
|
|
|
type="primary"
|
|
|
|
|
link
|
|
|
|
|
size="small"
|
|
|
|
|
:disabled="row.status === 1 || row.status === 2"
|
|
|
|
|
@click="handleEdit(row)"
|
|
|
|
|
>
|
|
|
|
|
编辑
|
|
|
|
|
</ElButton>
|
|
|
|
|
<ElButton type="primary" link size="small" @click="handleLog(row)">日志</ElButton>
|
|
|
|
|
<ElButton
|
|
|
|
|
type="danger"
|
|
|
|
|
link
|
|
|
|
|
size="small"
|
|
|
|
|
:disabled="row.status === 1"
|
|
|
|
|
@click="handleDelete(row)"
|
|
|
|
|
>
|
|
|
|
|
删除
|
|
|
|
|
</ElButton>
|
|
|
|
|
</template>
|
|
|
|
|
</ElTableColumn>
|
|
|
|
|
</ElTable>
|
|
|
|
|
|
|
|
|
|
<!-- 分页 -->
|
|
|
|
|
<div v-if="total > 0" class="pagination">
|
|
|
|
|
<span class="total-text">共 {{ total }} 条</span>
|
|
|
|
|
<ElPagination
|
|
|
|
|
:current-page="pageNo"
|
|
|
|
|
:page-size="pageSize"
|
|
|
|
|
:total="total"
|
|
|
|
|
:page-sizes="[10, 20, 50, 100]"
|
|
|
|
|
layout="sizes, prev, pager, next, jumper"
|
|
|
|
|
@current-change="onPageChange"
|
|
|
|
|
@size-change="onSizeChange"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 新增/编辑任务弹窗 -->
|
|
|
|
|
<ReviewFormDialog
|
|
|
|
|
v-model:visible="formVisible"
|
|
|
|
|
:edit-data="currentEditItem"
|
|
|
|
|
@success="onFormSuccess"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<!-- 任务详情弹窗 -->
|
|
|
|
|
<TaskDetailDialog
|
|
|
|
|
v-model:visible="detailVisible"
|
|
|
|
|
:task="currentDetailItem"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<!-- 文档预览弹窗 -->
|
|
|
|
|
<DocPreviewDialog
|
|
|
|
|
v-model:visible="docPreviewVisible"
|
|
|
|
|
:title="currentPreviewFile?.name || '审核文档'"
|
|
|
|
|
:file-url="currentPreviewFile?.url"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<!-- 操作日志抽屉 -->
|
|
|
|
|
<LogDrawer
|
|
|
|
|
v-model:visible="logVisible"
|
|
|
|
|
:task-id="currentLogTask?.id"
|
|
|
|
|
:task="currentLogTask"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { ref, reactive, onMounted, computed } from "vue";
|
|
|
|
|
import { useRoute, useRouter } from "vue-router";
|
|
|
|
|
import { ElMessage, ElMessageBox } from "element-plus";
|
|
|
|
|
import {
|
|
|
|
|
Search,
|
|
|
|
|
Refresh,
|
|
|
|
|
Plus,
|
|
|
|
|
View,
|
|
|
|
|
Document,
|
|
|
|
|
CircleCheck,
|
|
|
|
|
CircleClose,
|
|
|
|
|
Clock,
|
|
|
|
|
} from "@element-plus/icons-vue";
|
|
|
|
|
import { ReviewTaskAPI } from "@/api/module_report/review";
|
|
|
|
|
import type { ReviewTaskItem, ReviewTaskPageQuery } from "@/api/module_report/review";
|
|
|
|
|
import type { ReportType } from "@/api/module_report/report";
|
|
|
|
|
import ReviewFormDialog from "./components/ReviewFormDialog.vue";
|
|
|
|
|
import TaskDetailDialog from "./components/TaskDetailDialog.vue";
|
|
|
|
|
import DocPreviewDialog from "./components/DocPreviewDialog.vue";
|
|
|
|
|
import LogDrawer from "./components/LogDrawer.vue";
|
|
|
|
|
|
|
|
|
|
defineOptions({ name: "EnergyReportReviewList" });
|
|
|
|
|
|
|
|
|
|
const route = useRoute();
|
|
|
|
|
const router = useRouter();
|
|
|
|
|
|
|
|
|
|
/** 报告类型:1 能源报告 / 2 光伏报告(由路由参数传入) */
|
|
|
|
|
const reportType = computed<ReportType>(() => {
|
|
|
|
|
const t = route.query.report_type;
|
|
|
|
|
return t === "2" ? 2 : 1;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const loading = ref(false);
|
|
|
|
|
const taskList = ref<ReviewTaskItem[]>([]);
|
|
|
|
|
const total = ref(0);
|
|
|
|
|
const pageNo = ref(1);
|
|
|
|
|
const pageSize = ref(10);
|
|
|
|
|
const timeRange = ref("all");
|
|
|
|
|
const filterStatus = ref<number | undefined>(undefined);
|
|
|
|
|
|
|
|
|
|
const stats = reactive({
|
|
|
|
|
total: 0,
|
|
|
|
|
reviewed: 0,
|
|
|
|
|
pending: 0,
|
|
|
|
|
failed: 0,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const queryParams = reactive<{
|
|
|
|
|
task_name: string;
|
|
|
|
|
status: number | "";
|
|
|
|
|
created_time: string[] | null;
|
|
|
|
|
reviewer_name: string;
|
|
|
|
|
}>({
|
|
|
|
|
task_name: "",
|
|
|
|
|
status: "",
|
|
|
|
|
created_time: null,
|
|
|
|
|
reviewer_name: "",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 弹窗状态
|
|
|
|
|
const formVisible = ref(false);
|
|
|
|
|
const detailVisible = ref(false);
|
|
|
|
|
const docPreviewVisible = ref(false);
|
|
|
|
|
const logVisible = ref(false);
|
|
|
|
|
const currentEditItem = ref<ReviewTaskItem | null>(null);
|
|
|
|
|
const currentDetailItem = ref<ReviewTaskItem | null>(null);
|
|
|
|
|
const currentLogTask = ref<ReviewTaskItem | null>(null);
|
|
|
|
|
const currentPreviewFile = ref<{ url: string; name: string } | null>(null);
|
|
|
|
|
|
|
|
|
|
function getTimeRangeParam(): string[] | undefined {
|
|
|
|
|
const now = new Date();
|
|
|
|
|
const pad = (n: number) => String(n).padStart(2, "0");
|
|
|
|
|
const fmt = (d: Date) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
|
|
|
if (timeRange.value === "today") {
|
|
|
|
|
const s = fmt(now);
|
|
|
|
|
return [s + " 00:00:00", s + " 23:59:59"];
|
|
|
|
|
}
|
|
|
|
|
if (timeRange.value === "7days") {
|
|
|
|
|
const s = new Date(now.getTime() - 6 * 86400000);
|
|
|
|
|
return [fmt(s) + " 00:00:00", fmt(now) + " 23:59:59"];
|
|
|
|
|
}
|
|
|
|
|
if (timeRange.value === "30days") {
|
|
|
|
|
const s = new Date(now.getTime() - 29 * 86400000);
|
|
|
|
|
return [fmt(s) + " 00:00:00", fmt(now) + " 23:59:59"];
|
|
|
|
|
}
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadList() {
|
|
|
|
|
loading.value = true;
|
|
|
|
|
try {
|
|
|
|
|
const params: ReviewTaskPageQuery = {
|
|
|
|
|
page_no: pageNo.value,
|
|
|
|
|
page_size: pageSize.value,
|
|
|
|
|
task_name: queryParams.task_name?.trim() || undefined,
|
|
|
|
|
status: queryParams.status === "" ? undefined : (queryParams.status as any),
|
|
|
|
|
report_type: reportType.value,
|
|
|
|
|
reviewer_name: queryParams.reviewer_name?.trim() || undefined,
|
|
|
|
|
};
|
|
|
|
|
if (queryParams.created_time && queryParams.created_time.length === 2) {
|
|
|
|
|
params.created_time = [
|
|
|
|
|
queryParams.created_time[0] + " 00:00:00",
|
|
|
|
|
queryParams.created_time[1] + " 23:59:59",
|
|
|
|
|
];
|
|
|
|
|
} else {
|
|
|
|
|
params.created_time = getTimeRangeParam();
|
|
|
|
|
}
|
|
|
|
|
if (filterStatus.value !== undefined) {
|
|
|
|
|
params.status = filterStatus.value as any;
|
|
|
|
|
}
|
|
|
|
|
const resp = await ReviewTaskAPI.page(params);
|
|
|
|
|
const data = resp?.data?.data;
|
|
|
|
|
taskList.value = (data?.items ?? []).map((item) => ({
|
|
|
|
|
...item,
|
|
|
|
|
progress: item.progress ?? (item.status === 2 ? 100 : item.status === 0 ? 0 : 0),
|
|
|
|
|
}));
|
|
|
|
|
total.value = data?.total ?? 0;
|
|
|
|
|
|
|
|
|
|
// 统计数据随列表接口一起返回
|
|
|
|
|
const s = data?.stats;
|
|
|
|
|
stats.total = s?.submitted_count ?? 0;
|
|
|
|
|
stats.reviewed = s?.reviewed_count ?? 0;
|
|
|
|
|
stats.pending = s?.pending_count ?? 0;
|
|
|
|
|
stats.failed = s?.failed_count ?? 0;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
taskList.value = [];
|
|
|
|
|
total.value = 0;
|
|
|
|
|
} finally {
|
|
|
|
|
loading.value = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleSearch() {
|
|
|
|
|
pageNo.value = 1;
|
|
|
|
|
loadList();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleReset() {
|
|
|
|
|
queryParams.task_name = "";
|
|
|
|
|
queryParams.status = "";
|
|
|
|
|
queryParams.created_time = null;
|
|
|
|
|
queryParams.reviewer_name = "";
|
|
|
|
|
filterStatus.value = undefined;
|
|
|
|
|
timeRange.value = "all";
|
|
|
|
|
pageNo.value = 1;
|
|
|
|
|
loadList();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function filterByStatus(status?: number) {
|
|
|
|
|
filterStatus.value = status;
|
|
|
|
|
pageNo.value = 1;
|
|
|
|
|
loadList();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onPageChange(p: number) {
|
|
|
|
|
pageNo.value = p;
|
|
|
|
|
loadList();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onSizeChange(s: number) {
|
|
|
|
|
pageSize.value = s;
|
|
|
|
|
pageNo.value = 1;
|
|
|
|
|
loadList();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ========== 操作 ==========
|
|
|
|
|
|
|
|
|
|
function handleCreate() {
|
|
|
|
|
currentEditItem.value = null;
|
|
|
|
|
formVisible.value = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleEdit(row: ReviewTaskItem) {
|
|
|
|
|
currentEditItem.value = row;
|
|
|
|
|
formVisible.value = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onFormSuccess() {
|
|
|
|
|
loadList();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleDetail(row: ReviewTaskItem) {
|
|
|
|
|
currentDetailItem.value = row;
|
|
|
|
|
detailVisible.value = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handlePreviewDoc(row: ReviewTaskItem) {
|
|
|
|
|
if (!row.file_url) return;
|
|
|
|
|
currentPreviewFile.value = {
|
|
|
|
|
url: row.file_url,
|
|
|
|
|
name: row.original_name || "审核文档",
|
|
|
|
|
};
|
|
|
|
|
docPreviewVisible.value = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleLog(row: ReviewTaskItem) {
|
|
|
|
|
currentLogTask.value = row;
|
|
|
|
|
logVisible.value = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleViewResult(row: ReviewTaskItem) {
|
|
|
|
|
router.push({ name: "ReportReviewResult", query: { id: row.id, report_type: reportType.value } });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleReview(row: ReviewTaskItem) {
|
|
|
|
|
// TODO: 审核接口暂未提供,预留入口
|
|
|
|
|
ElMessageBox.confirm(`确定开始审查任务「${row.task_name}」吗?`, "开始审查", {
|
|
|
|
|
type: "info",
|
|
|
|
|
confirmButtonText: "开始审查",
|
|
|
|
|
cancelButtonText: "取消",
|
|
|
|
|
})
|
|
|
|
|
.then(async () => {
|
|
|
|
|
try {
|
|
|
|
|
await ReviewTaskAPI.startReview(row.id, reportType.value);
|
|
|
|
|
// 调用审查接口后,新增 start_review 操作记录
|
|
|
|
|
await ReviewTaskAPI.createLog({
|
|
|
|
|
report_review_id: row.id,
|
|
|
|
|
action_type: "start_review",
|
|
|
|
|
description: `开始审查:${row.task_name}`,
|
|
|
|
|
});
|
|
|
|
|
ElMessage.success("审查任务已提交");
|
|
|
|
|
loadList();
|
|
|
|
|
} catch (e) {}
|
|
|
|
|
})
|
|
|
|
|
.catch(() => {});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleDelete(row: ReviewTaskItem) {
|
|
|
|
|
ElMessageBox.confirm(`确定要删除任务「${row.task_name}」吗?`, "删除确认", {
|
|
|
|
|
type: "warning",
|
|
|
|
|
confirmButtonText: "删除",
|
|
|
|
|
cancelButtonText: "取消",
|
|
|
|
|
})
|
|
|
|
|
.then(async () => {
|
|
|
|
|
try {
|
|
|
|
|
await ReviewTaskAPI.remove([row.id], reportType.value);
|
|
|
|
|
loadList();
|
|
|
|
|
} catch (e) {}
|
|
|
|
|
})
|
|
|
|
|
.catch(() => {});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ========== 工具函数 ==========
|
|
|
|
|
|
|
|
|
|
function statusText(s: number): string {
|
|
|
|
|
return ["未审查", "审查中", "审查完成", "审查失败"][s] ?? "未知";
|
|
|
|
|
}
|
|
|
|
|
function statusTagType(s: number): "info" | "warning" | "success" | "danger" {
|
|
|
|
|
return ["info", "warning", "success", "danger"][s] as any;
|
|
|
|
|
}
|
|
|
|
|
function formatDateTime(value?: string): string {
|
|
|
|
|
if (!value) return "-";
|
|
|
|
|
const d = new Date(value);
|
|
|
|
|
if (Number.isNaN(d.getTime())) return value;
|
|
|
|
|
const pad = (n: number) => String(n).padStart(2, "0");
|
|
|
|
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
loadList();
|
|
|
|
|
});
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
|
.review-list-page {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
height: 100%;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 统计卡片 */
|
|
|
|
|
.stats-bar {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 16px;
|
|
|
|
|
padding: 16px 20px;
|
|
|
|
|
background: #fff;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
border: 1px solid #e5e7eb;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-card {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
padding: 12px 20px;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
border: 1px solid transparent;
|
|
|
|
|
min-width: 160px;
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
background: #f9fafb;
|
|
|
|
|
border-color: #e5e7eb;
|
|
|
|
|
}
|
|
|
|
|
&.active {
|
|
|
|
|
background: #eff6ff;
|
|
|
|
|
border-color: #bfdbfe;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-icon {
|
|
|
|
|
width: 48px;
|
|
|
|
|
height: 48px;
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
|
|
|
|
&.total { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
|
|
|
|
|
&.success { background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); }
|
|
|
|
|
&.pending { background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); }
|
|
|
|
|
&.failed { background: linear-gradient(135deg, #eb3349 0%, #f45c43 100%); }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-info {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
}
|
|
|
|
|
.stat-value {
|
|
|
|
|
font-size: 22px;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
color: #111827;
|
|
|
|
|
line-height: 1.2;
|
|
|
|
|
}
|
|
|
|
|
.stat-label {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: #6b7280;
|
|
|
|
|
margin-top: 2px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.time-range-wrap {
|
|
|
|
|
margin-left: auto;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
.time-label {
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
color: #606266;
|
|
|
|
|
}
|
|
|
|
|
.time-select {
|
|
|
|
|
width: 120px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 搜索栏 */
|
|
|
|
|
.search-bar {
|
|
|
|
|
background: #fff;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
border: 1px solid #e5e7eb;
|
|
|
|
|
padding: 16px 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.search-row {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
gap: 16px 24px;
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.search-item {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.search-label {
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
color: #374151;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
min-width: 56px;
|
|
|
|
|
text-align: right;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.search-input {
|
|
|
|
|
width: 200px;
|
|
|
|
|
}
|
|
|
|
|
.search-input-date {
|
|
|
|
|
width: 280px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.search-actions {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 表格 */
|
|
|
|
|
.table-card {
|
|
|
|
|
flex: 1;
|
|
|
|
|
background: #fff;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
border: 1px solid #e5e7eb;
|
|
|
|
|
padding: 0;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.progress-cell {
|
|
|
|
|
padding: 0 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.text-muted {
|
|
|
|
|
color: #c0c4cc;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.pagination {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
border-top: 1px solid #f0f0f0;
|
|
|
|
|
}
|
|
|
|
|
.total-text {
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
color: #6b7280;
|
|
|
|
|
margin-right: auto;
|
|
|
|
|
}
|
|
|
|
|
</style>
|