feat(tplus): 新增 T+ 同步管理页面与菜单入口
This commit is contained in:
parent
4fe5916c2c
commit
ea989ce9ad
2
frontend/.env.example
Normal file
2
frontend/.env.example
Normal file
@ -0,0 +1,2 @@
|
||||
# Vite 开发代理目标,仅开发服务器读取;前端请求仍使用 /api 相对地址。
|
||||
DEV_API_TARGET=http://127.0.0.1:8000
|
||||
@ -6,6 +6,9 @@
|
||||
|
||||
- 登录页与图形验证码交互
|
||||
- 会话状态管理与路由守卫
|
||||
- 管理员 T+ 数据同步页:`/system/tplus`,支持读取排队、关联检查、批次/异常详情和失败重试;只读配置未就绪时禁用创建。
|
||||
- T+ 页面位于 `src/views/system/tplus/`,合同在 `src/api/tplus.ts`;创建或重试失败后保留请求键,范围改变或提交成功后才换键。任务由独立读取服务执行,页面刷新查看状态,运行命令见 T+ 本地恢复手册。
|
||||
- Vite 开发代理可通过 `DEV_API_TARGET` 指定后端,默认本地 8000;浏览器请求继续使用 `/api` 相对地址。
|
||||
- 岗位角色管理页(模板绑定)、计提参数配置页(分组目录/批次提交/版本对比/导出/计算预览)
|
||||
- 首页 Bento 仪表盘(M1 空态设计;业务上线后接入真实统计,图表用 ECharts)
|
||||
- 后续:基础数据、业务数据、绩效核算、报表等业务页面
|
||||
|
||||
84
frontend/src/api/tplus.ts
Normal file
84
frontend/src/api/tplus.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import { request } from './request'
|
||||
import type { PageData } from './types'
|
||||
|
||||
export type TaskType = 'READ' | 'AUDIT'
|
||||
export type TaskStatus = 'QUEUED' | 'RUNNING' | 'SUCCEEDED' | 'PARTIAL' | 'FAILED'
|
||||
export type EntityType = 'personnel' | 'products' | 'customers' | 'businesses' | 'shipments'
|
||||
| 'receipts' | 'business_items' | 'shipment_items' | 'receipt_items'
|
||||
|
||||
/** 仅包含系统字典和汇总,不包含客户记录或源配置。 */
|
||||
export interface AuditReport {
|
||||
captured_at: string
|
||||
source_version: string
|
||||
relationship_status: string
|
||||
compatibility_status: string
|
||||
sales_links: Record<string, number> | null
|
||||
order_coverage: Record<string, number> | null
|
||||
state_enums: { id: number; code: string; name: string; enum_name: string }[]
|
||||
receipt_kinds: { voucher_type_name: string; row_count: number }[]
|
||||
receipt_line_kinds: { voucher_type_name: string | null; receive_flag: number; row_count: number }[]
|
||||
pending_confirmations: { key: string; title: string; reason: string }[]
|
||||
}
|
||||
|
||||
export interface TPlusBatch {
|
||||
batch_no: string
|
||||
task_type: TaskType
|
||||
status: TaskStatus
|
||||
entity_types: EntityType[] | null
|
||||
source_record_count: number
|
||||
staged_record_count: number
|
||||
skipped_record_count: number
|
||||
failed_record_count: number
|
||||
parent_batch_no: string | null
|
||||
create_time: string
|
||||
started_at: string | null
|
||||
finished_at: string | null
|
||||
can_retry: boolean
|
||||
error_summary: string | null
|
||||
}
|
||||
export interface TPlusDetail extends TPlusBatch { audit_result: AuditReport | null }
|
||||
export interface TPlusError {
|
||||
id: number
|
||||
entity_type: string
|
||||
source_id: string | null
|
||||
error_code: string
|
||||
error_message: string
|
||||
retry_count: number
|
||||
create_time: string
|
||||
}
|
||||
export interface TPlusOverview {
|
||||
is_configured: boolean
|
||||
configuration_message: string
|
||||
status_counts: Partial<Record<TaskStatus, number>>
|
||||
total_staged_records: number
|
||||
compatibility_status: string
|
||||
business_import_enabled: boolean
|
||||
latest_audit: AuditReport | null
|
||||
}
|
||||
export interface BatchQuery { status?: TaskStatus; task_type?: TaskType; pageNum?: number; pageSize?: number }
|
||||
export interface ErrorQuery { entity_type?: EntityType; pageNum?: number; pageSize?: number }
|
||||
export interface CreateTask { task_type: TaskType; entity_types: EntityType[]; request_key: string }
|
||||
|
||||
const base = '/v1/system/tplus'
|
||||
/** 查询配置和任务状态;不会触发源库连接。 */
|
||||
export function getOverview(): Promise<TPlusOverview> { return request({ url: `${base}/overview` }) }
|
||||
/** 分页查询任务。 */
|
||||
export function listBatches(params: BatchQuery): Promise<PageData<TPlusBatch>> {
|
||||
return request({ url: `${base}/batches`, params })
|
||||
}
|
||||
/** 按需查询报告。 */
|
||||
export function getBatch(number: string): Promise<TPlusDetail> {
|
||||
return request({ url: `${base}/batches/${encodeURIComponent(number)}` })
|
||||
}
|
||||
/** 分页查询固定错误说明。 */
|
||||
export function listErrors(number: string, params: ErrorQuery): Promise<PageData<TPlusError>> {
|
||||
return request({ url: `${base}/batches/${encodeURIComponent(number)}/errors`, params })
|
||||
}
|
||||
/** 以调用方保留的请求键持久化排队。 */
|
||||
export function createTask(data: CreateTask): Promise<TPlusDetail> {
|
||||
return request({ url: `${base}/batches`, method: 'post', data })
|
||||
}
|
||||
/** 重试会创建新批次,原异常记录保留。 */
|
||||
export function retryTask(number: string, request_key: string): Promise<TPlusDetail> {
|
||||
return request({ url: `${base}/batches/${encodeURIComponent(number)}/retry`, method: 'post', data: { request_key } })
|
||||
}
|
||||
@ -15,6 +15,7 @@ import {
|
||||
PieChart,
|
||||
Postcard,
|
||||
SetUp,
|
||||
Refresh,
|
||||
Shop,
|
||||
Stamp,
|
||||
Tickets,
|
||||
@ -47,6 +48,7 @@ const ICONS = {
|
||||
settings: OfficeBuilding,
|
||||
approvals: Stamp,
|
||||
logs: Tickets,
|
||||
tplus: Refresh,
|
||||
personnel: Postcard,
|
||||
products: Box,
|
||||
customers: Shop,
|
||||
|
||||
@ -63,6 +63,12 @@ const routes: RouteRecordRaw[] = [
|
||||
component: () => import('@/views/system/users/index.vue'),
|
||||
meta: { title: '用户管理', icon: 'users', group: '系统管理', requiresAdmin: true, order: 2 },
|
||||
},
|
||||
{
|
||||
path: '/system/tplus',
|
||||
name: 'system-tplus',
|
||||
component: () => import('@/views/system/tplus/index.vue'),
|
||||
meta: { title: 'T+ 数据同步', icon: 'tplus', group: '系统管理', requiresAdmin: true, order: 10 },
|
||||
},
|
||||
{
|
||||
path: '/system/roles',
|
||||
name: 'system-roles',
|
||||
|
||||
@ -19,8 +19,9 @@ export const MODULE_OPTIONS = [
|
||||
'发货管理',
|
||||
'回款管理',
|
||||
'客户评级',
|
||||
'T+数据同步',
|
||||
] as const
|
||||
export const ACTION_OPTIONS = ['新增', '修改', '删除', '重置密码', '启用', '禁用'] as const
|
||||
export const ACTION_OPTIONS = ['新增', '修改', '删除', '重置密码', '启用', '禁用', '创建读取任务', '重试读取任务'] as const
|
||||
|
||||
/** 启用/禁用筛选选项(文案与 StatusTag 的展示一致) */
|
||||
export const STATUS_OPTIONS = [
|
||||
|
||||
42
frontend/src/views/system/tplus/AuditResult.vue
Normal file
42
frontend/src/views/system/tplus/AuditResult.vue
Normal file
@ -0,0 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { AuditReport } from '@/api/tplus'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
|
||||
const props = defineProps<{ report: AuditReport }>()
|
||||
const linkLabels: Record<string, string> = {
|
||||
row_count: '销货类收款明细', delivery_matches: '销货单匹配', detail_matches: '销货明细匹配',
|
||||
direct_order_matches: '收款明细直接关联订单', detail_order_matches: '经销货明细关联订单',
|
||||
unambiguous_order_matches: '订单关联无冲突', invalid_order_ids: '悬空直接订单 ID', conflicting_order_ids: '两路径订单冲突',
|
||||
}
|
||||
const rows = computed(() => Object.entries(props.report.sales_links ?? {}).map(([key, value]) => ({
|
||||
label: linkLabels[key] ?? key, value,
|
||||
})))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="audit-result" aria-label="关联核验结果">
|
||||
<p class="audit-caption">核验于 {{ formatDateTime(report.captured_at) }} · 源引擎 {{ report.source_version }}</p>
|
||||
<el-alert type="warning" :closable="false" title="SQL Server 2008 R2 同版本验收待完成;关联存在不代表业务口径已确认。" />
|
||||
<p v-if="report.relationship_status !== 'OBSERVED'" role="alert">单据类型未能唯一确认,关联统计未执行。</p>
|
||||
<el-table v-if="rows.length" :data="rows" size="small" class="audit-table">
|
||||
<el-table-column prop="label" label="核验项目" min-width="190" />
|
||||
<el-table-column label="明细行数" width="120" align="right"><template #default="{ row }"><span class="cell-num">{{ row.value.toLocaleString() }}</span></template></el-table-column>
|
||||
</el-table>
|
||||
<p v-if="report.order_coverage" class="coverage-note">按关联销货单统计:无订单 {{ report.order_coverage.without_order?.toLocaleString() }} 行,单订单 {{ report.order_coverage.single_order?.toLocaleString() }} 行,多订单 {{ report.order_coverage.multiple_orders?.toLocaleString() }} 行。统计单位为收款明细行,同一销货单可能出现多次。</p>
|
||||
<h3>收付款与系统状态</h3>
|
||||
<p>{{ report.receipt_kinds?.map((item) => `${item.voucher_type_name} ${item.row_count.toLocaleString()} 条`).join(';') || '暂无统计' }}</p>
|
||||
<p>{{ report.state_enums?.map((item) => `${item.id}:${item.name}`).join(';') }}</p>
|
||||
<h3>待确认口径</h3>
|
||||
<dl><template v-for="item in report.pending_confirmations" :key="item.key"><dt>{{ item.title }}</dt><dd>{{ item.reason }}</dd></template></dl>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.audit-result { line-height: 1.7; }
|
||||
.audit-caption, .coverage-note, dd { color: var(--ys-ink-2); }
|
||||
.audit-table { margin-top: 16px; }
|
||||
h3 { font-size: 14px; margin: 20px 0 8px; }
|
||||
dt { font-weight: 600; }
|
||||
dd { margin: 0 0 12px; }
|
||||
</style>
|
||||
78
frontend/src/views/system/tplus/BatchDetailDialog.vue
Normal file
78
frontend/src/views/system/tplus/BatchDetailDialog.vue
Normal file
@ -0,0 +1,78 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import * as api from '@/api/tplus'
|
||||
import AppPagination from '@/components/table/AppPagination.vue'
|
||||
import { usePagedList } from '@/composables/usePagedList'
|
||||
import { handledError } from '@/utils/error'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import AuditResult from './AuditResult.vue'
|
||||
import { entities, scopeLabel, statuses } from './labels'
|
||||
|
||||
const number = defineModel<string | null>({ required: true })
|
||||
const detail = ref<api.TPlusDetail | null>(null)
|
||||
const loading = ref(false)
|
||||
const failed = ref(false)
|
||||
let sequence = 0
|
||||
const { records, total, pageNum, pageSize, loading: errorsLoading, search, onPageChange, onSizeChange } =
|
||||
usePagedList<api.TPlusError, api.ErrorQuery>((query) => api.listErrors(number.value!, query), () => ({}))
|
||||
|
||||
async function load(): Promise<void> {
|
||||
const current = ++sequence
|
||||
const key = number.value
|
||||
if (!key) return
|
||||
detail.value = null
|
||||
loading.value = true
|
||||
failed.value = false
|
||||
try {
|
||||
const result = await api.getBatch(key)
|
||||
if (current !== sequence) return
|
||||
detail.value = result
|
||||
if (result.failed_record_count) await search()
|
||||
} catch (error) {
|
||||
handledError(error)
|
||||
if (current === sequence) failed.value = true
|
||||
} finally { if (current === sequence) loading.value = false }
|
||||
}
|
||||
watch(number, (value) => { if (value) void load(); else sequence++ })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dialog :model-value="!!number" title="T+ 任务详情" width="min(880px, 96vw)" @update:model-value="number = null">
|
||||
<div v-loading="loading" class="detail-body">
|
||||
<el-alert v-if="failed" type="error" title="详情加载失败,请重试" :closable="false"><el-button link @click="load">重新加载</el-button></el-alert>
|
||||
<template v-if="detail">
|
||||
<p class="cell-code batch-number">{{ detail.batch_no }}</p>
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="状态">{{ statuses[detail.status] }}</el-descriptions-item>
|
||||
<el-descriptions-item label="范围">{{ scopeLabel(detail) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">{{ formatDateTime(detail.create_time) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="执行时间">{{ formatDateTime(detail.started_at) }} → {{ formatDateTime(detail.finished_at) }}</el-descriptions-item>
|
||||
<el-descriptions-item v-if="detail.parent_batch_no" label="原批次">{{ detail.parent_batch_no }}</el-descriptions-item>
|
||||
<el-descriptions-item label="数量">源记录 {{ detail.source_record_count.toLocaleString() }},新增 {{ detail.staged_record_count.toLocaleString() }},重复跳过 {{ detail.skipped_record_count.toLocaleString() }},异常 {{ detail.failed_record_count.toLocaleString() }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<p v-if="detail.status === 'QUEUED'" role="status">任务等待读取服务处理,可稍后刷新查看。</p>
|
||||
<p v-if="detail.error_summary" role="alert">{{ detail.error_summary }}</p>
|
||||
<AuditResult v-if="detail.audit_result" :report="detail.audit_result" />
|
||||
<section v-if="detail.failed_record_count" class="errors-section" aria-label="异常明细">
|
||||
<h3>异常明细</h3>
|
||||
<el-table v-loading="errorsLoading" :data="records" empty-text="暂无异常记录">
|
||||
<el-table-column label="范围" width="100"><template #default="{ row }">{{ entities.find((item) => item.value === row.entity_type)?.label ?? '任务' }}</template></el-table-column>
|
||||
<el-table-column prop="source_id" label="源 ID" width="90" />
|
||||
<el-table-column prop="error_message" label="异常说明" min-width="240" />
|
||||
<el-table-column prop="retry_count" label="已重试" width="80" />
|
||||
</el-table>
|
||||
<div class="pager-scroll"><AppPagination :total="total" :page-num="pageNum" :page-size="pageSize" @page-change="onPageChange" @size-change="onSizeChange" /></div>
|
||||
</section>
|
||||
</template>
|
||||
</div>
|
||||
<template #footer><el-button @click="number = null">关闭</el-button><el-button :loading="loading" @click="load">刷新详情</el-button></template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.detail-body { min-height: 120px; }
|
||||
.batch-number { overflow-wrap: anywhere; }
|
||||
.errors-section { margin-top: 24px; }
|
||||
h3 { font-size: 14px; }
|
||||
.pager-scroll { overflow-x: auto; }
|
||||
</style>
|
||||
59
frontend/src/views/system/tplus/CreateTaskDialog.vue
Normal file
59
frontend/src/views/system/tplus/CreateTaskDialog.vue
Normal file
@ -0,0 +1,59 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import * as api from '@/api/tplus'
|
||||
import { handledError } from '@/utils/error'
|
||||
import { entities } from './labels'
|
||||
|
||||
const visible = defineModel<boolean>({ required: true })
|
||||
const props = defineProps<{ configured: boolean }>()
|
||||
const emit = defineEmits<{ created: [batch: api.TPlusDetail] }>()
|
||||
const taskType = ref<api.TaskType>('READ')
|
||||
const selected = ref<api.EntityType[]>(['personnel'])
|
||||
const pending = ref(false)
|
||||
let requestKey = crypto.randomUUID()
|
||||
// 请求失败后保留键,只有意图改变或成功后才换键。
|
||||
watch([taskType, selected], () => { requestKey = crypto.randomUUID() }, { deep: true, flush: 'sync' })
|
||||
|
||||
async function submit(): Promise<void> {
|
||||
if (pending.value || !props.configured) return
|
||||
pending.value = true
|
||||
try {
|
||||
const batch = await api.createTask({ task_type: taskType.value,
|
||||
entity_types: taskType.value === 'AUDIT' ? [] : [...selected.value], request_key: requestKey })
|
||||
requestKey = crypto.randomUUID()
|
||||
visible.value = false
|
||||
ElMessage.success('任务已排队,处理完成后可刷新查看')
|
||||
emit('created', batch)
|
||||
} catch (error) { handledError(error) }
|
||||
finally { pending.value = false }
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="新建 T+ 数据任务" width="min(560px, 94vw)" :close-on-click-modal="!pending" :show-close="!pending">
|
||||
<el-form label-position="top" :disabled="pending">
|
||||
<el-form-item label="任务内容">
|
||||
<el-radio-group v-model="taskType">
|
||||
<el-radio-button value="READ">读取数据</el-radio-button>
|
||||
<el-radio-button value="AUDIT">关联检查</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="taskType === 'READ'" label="读取范围">
|
||||
<el-checkbox-group v-model="selected" class="entity-options">
|
||||
<el-checkbox v-for="item in entities" :key="item.value" :value="item.value">{{ item.label }}</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
<p class="task-note">{{ taskType === 'READ' ? '全量读取所选范围,重复快照自动跳过。只保存原始数据,不写入工资业务表。' : '核对收款方向、单据类型及订单关联,只返回字典和统计数量。' }}</p>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button :disabled="pending" @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="pending" :disabled="!configured || (taskType === 'READ' && !selected.length)" @click="submit">加入处理队列</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.entity-options { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); width: 100%; }
|
||||
.task-note { color: var(--ys-ink-2); line-height: 1.7; }
|
||||
</style>
|
||||
114
frontend/src/views/system/tplus/index.vue
Normal file
114
frontend/src/views/system/tplus/index.vue
Normal file
@ -0,0 +1,114 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import * as api from '@/api/tplus'
|
||||
import AppPagination from '@/components/table/AppPagination.vue'
|
||||
import { usePagedList } from '@/composables/usePagedList'
|
||||
import { handledError } from '@/utils/error'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import AuditResult from './AuditResult.vue'
|
||||
import BatchDetailDialog from './BatchDetailDialog.vue'
|
||||
import CreateTaskDialog from './CreateTaskDialog.vue'
|
||||
import { scopeLabel, statuses } from './labels'
|
||||
|
||||
const overview = ref<api.TPlusOverview | null>(null)
|
||||
const overviewFailed = ref(false)
|
||||
const refreshing = ref(false)
|
||||
const creating = ref(false)
|
||||
const detailNumber = ref<string | null>(null)
|
||||
const status = ref<api.TaskStatus>()
|
||||
const taskType = ref<api.TaskType>()
|
||||
const retrying = ref<string | null>(null)
|
||||
const retryKeys = new Map<string, string>()
|
||||
const { records, total, pageNum, pageSize, loading, load, search, onPageChange, onSizeChange } =
|
||||
usePagedList<api.TPlusBatch, api.BatchQuery>(api.listBatches, () => ({ status: status.value, task_type: taskType.value }))
|
||||
|
||||
async function refresh(): Promise<void> {
|
||||
if (refreshing.value) return
|
||||
refreshing.value = true
|
||||
overviewFailed.value = false
|
||||
try { overview.value = await api.getOverview() }
|
||||
catch (error) { overviewFailed.value = true; handledError(error) }
|
||||
finally { refreshing.value = false }
|
||||
await load()
|
||||
}
|
||||
async function retry(row: api.TPlusBatch): Promise<void> {
|
||||
if (retrying.value) return
|
||||
retrying.value = row.batch_no
|
||||
const key = retryKeys.get(row.batch_no) ?? crypto.randomUUID()
|
||||
retryKeys.set(row.batch_no, key)
|
||||
try {
|
||||
const batch = await api.retryTask(row.batch_no, key)
|
||||
retryKeys.delete(row.batch_no)
|
||||
ElMessage.success('重试任务已排队,原批次和异常记录已保留')
|
||||
detailNumber.value = batch.batch_no
|
||||
await refresh()
|
||||
} catch (error) { handledError(error) }
|
||||
finally { retrying.value = null }
|
||||
}
|
||||
function created(batch: api.TPlusDetail): void {
|
||||
detailNumber.value = batch.batch_no
|
||||
void refresh()
|
||||
}
|
||||
function statusType(value: api.TaskStatus): 'success' | 'warning' | 'danger' | 'info' {
|
||||
return value === 'SUCCEEDED' ? 'success' : value === 'FAILED' ? 'danger' : value === 'PARTIAL' ? 'warning' : 'info'
|
||||
}
|
||||
onMounted(refresh)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tplus-page">
|
||||
<header class="page-head">
|
||||
<div><h1 class="page-title">T+ 数据同步</h1><p class="page-desc">读取源数据、检查关联,并追踪每次处理结果。</p><div class="waterline" /></div>
|
||||
<div class="head-actions"><el-button :loading="refreshing" @click="refresh">刷新</el-button><el-button type="primary" :disabled="!overview?.is_configured || overviewFailed" @click="creating = true">新建任务</el-button></div>
|
||||
</header>
|
||||
<el-alert type="warning" title="当前仅保存原始数据,未写入工资业务表。SQL Server 2008 R2 同版本验收及业务口径确认仍待完成。" :closable="false" show-icon />
|
||||
<el-alert v-if="overviewFailed" class="config-note" type="error" title="概况加载失败,请点击刷新重试" :closable="false" />
|
||||
<p v-else class="config-note" role="status">{{ overview?.configuration_message ?? '正在加载配置状态…' }}</p>
|
||||
<div v-if="overview" class="task-counts" aria-label="任务数量概况"><span v-for="(label, key) in statuses" :key="key">{{ label }} <b class="cell-num">{{ (overview.status_counts[key] ?? 0).toLocaleString() }}</b></span><span>累计新增快照 <b>{{ overview.total_staged_records.toLocaleString() }}</b></span></div>
|
||||
<section class="batch-card" aria-label="同步批次">
|
||||
<div class="filter-bar">
|
||||
<el-select v-model="status" clearable placeholder="全部状态" aria-label="批次状态" @change="search"><el-option v-for="(label, key) in statuses" :key="key" :label="label" :value="key" /></el-select>
|
||||
<el-select v-model="taskType" clearable placeholder="全部任务" aria-label="任务类型" @change="search"><el-option label="读取数据" value="READ" /><el-option label="关联检查" value="AUDIT" /></el-select>
|
||||
</div>
|
||||
<div class="card card-body"><el-table v-loading="loading" :data="records" empty-text="暂无任务,可新建读取或关联检查" class="data-table">
|
||||
<el-table-column label="批次 / 创建时间" min-width="235"><template #default="{ row }"><el-button link type="primary" class="cell-code" @click="detailNumber = row.batch_no">{{ row.batch_no }}</el-button><p class="batch-time">{{ formatDateTime(row.create_time) }}</p></template></el-table-column>
|
||||
<el-table-column label="任务 / 范围" min-width="180"><template #default="{ row }"><b>{{ row.task_type === 'AUDIT' ? '关联检查' : '读取数据' }}</b><p class="batch-scope">{{ scopeLabel(row) }}</p></template></el-table-column>
|
||||
<el-table-column label="状态" width="110"><template #default="{ row }"><el-tag :type="statusType(row.status)">{{ statuses[row.status as api.TaskStatus] }}</el-tag></template></el-table-column>
|
||||
<el-table-column prop="source_record_count" label="源记录" width="100" align="right" />
|
||||
<el-table-column prop="staged_record_count" label="新增" width="90" align="right" />
|
||||
<el-table-column prop="skipped_record_count" label="重复跳过" width="100" align="right" />
|
||||
<el-table-column prop="failed_record_count" label="异常" width="80" align="right" />
|
||||
<el-table-column label="操作" width="140" fixed="right"><template #default="{ row }"><el-button link type="primary" @click="detailNumber = row.batch_no">查看</el-button><el-button v-if="row.can_retry" link type="primary" :disabled="!overview?.is_configured || overviewFailed || !!retrying" :loading="retrying === row.batch_no" @click="retry(row)">重试</el-button></template></el-table-column>
|
||||
</el-table>
|
||||
<div class="pager-scroll"><AppPagination :total="total" :page-num="pageNum" :page-size="pageSize" @page-change="onPageChange" @size-change="onSizeChange" /></div></div>
|
||||
</section>
|
||||
<section class="card card-body latest-audit" aria-label="最近关联检查">
|
||||
<h2>最近关联检查</h2>
|
||||
<AuditResult v-if="overview?.latest_audit" :report="overview.latest_audit" />
|
||||
<p v-else class="page-desc">暂无关联检查结果。新建“关联检查”任务后,可在此查看覆盖数量与待确认口径。</p>
|
||||
</section>
|
||||
<CreateTaskDialog v-model="creating" :configured="!!overview?.is_configured && !overviewFailed" @created="created" />
|
||||
<BatchDetailDialog v-model="detailNumber" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-head { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
|
||||
.head-actions { display: flex; gap: 8px; }
|
||||
.head-actions .el-button + .el-button { margin-left: 0; }
|
||||
.config-note { margin: 12px 0; color: var(--ys-ink-2); }
|
||||
.task-counts { display: flex; flex-wrap: wrap; gap: 12px 24px; margin: 16px 0; color: var(--ys-ink-2); }
|
||||
.task-counts b { color: var(--ys-ink); margin-left: 4px; }
|
||||
.batch-card { margin-top: 16px; }
|
||||
.filter-bar .el-select { width: 160px; }
|
||||
.batch-time, .batch-scope { margin: 4px 0 0; color: var(--ys-ink-2); font-size: 12px; }
|
||||
.latest-audit { margin-top: 20px; }
|
||||
.latest-audit h2 { margin: 0 0 12px; font-size: 16px; }
|
||||
.pager-scroll { overflow-x: auto; }
|
||||
@media (max-width: 640px) {
|
||||
.page-head { align-items: flex-start; gap: 16px; flex-direction: column; }
|
||||
.filter-bar { flex-wrap: wrap; }
|
||||
.filter-bar .el-select { width: 100%; }
|
||||
}
|
||||
</style>
|
||||
17
frontend/src/views/system/tplus/labels.ts
Normal file
17
frontend/src/views/system/tplus/labels.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import type { EntityType, TaskStatus, TPlusBatch } from '@/api/tplus'
|
||||
|
||||
export const entities: { value: EntityType; label: string }[] = [
|
||||
{ value: 'personnel', label: '人员' }, { value: 'products', label: '产品' },
|
||||
{ value: 'customers', label: '客户' }, { value: 'businesses', label: '订单' },
|
||||
{ value: 'business_items', label: '订单明细' }, { value: 'shipments', label: '销货单' },
|
||||
{ value: 'shipment_items', label: '销货明细' }, { value: 'receipts', label: '收付款单' },
|
||||
{ value: 'receipt_items', label: '收付款明细' },
|
||||
]
|
||||
export const statuses: Record<TaskStatus, string> = {
|
||||
QUEUED: '等待处理', RUNNING: '处理中', SUCCEEDED: '已完成', PARTIAL: '部分失败', FAILED: '失败',
|
||||
}
|
||||
/** 历史范围未记录时保持未知,不默认成全部。 */
|
||||
export function scopeLabel(row: TPlusBatch): string {
|
||||
if (row.task_type === 'AUDIT') return '关联与枚举检查'
|
||||
return row.entity_types?.map((key) => entities.find((item) => item.value === key)?.label ?? key).join('、') || '历史批次,范围未记录'
|
||||
}
|
||||
@ -28,7 +28,7 @@ export default defineConfig(({ mode }) => {
|
||||
// 开发期走代理而非后端 CORS,符合规范「跨域在网关统一处理」;
|
||||
// 副作用是 /api/docs(Swagger)也能从 5173 端口打开,联调期只用一个域名。
|
||||
'/api': {
|
||||
target: 'http://127.0.0.1:8000',
|
||||
target: env.DEV_API_TARGET || 'http://127.0.0.1:8000',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
|
||||
Loading…
Reference in New Issue
Block a user