feat(tplus): 新增 T+ 同步管理页面与菜单入口

This commit is contained in:
liuwenxin 2026-10-28 11:42:51 +08:00
parent 4fe5916c2c
commit ea989ce9ad
12 changed files with 410 additions and 2 deletions

2
frontend/.env.example Normal file
View File

@ -0,0 +1,2 @@
# Vite 开发代理目标,仅开发服务器读取;前端请求仍使用 /api 相对地址。
DEV_API_TARGET=http://127.0.0.1:8000

View File

@ -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
View 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 } })
}

View File

@ -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,

View File

@ -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',

View File

@ -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 = [

View 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>

View 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>

View 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>

View 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>

View 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('、') || '历史批次,范围未记录'
}

View File

@ -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,
},
},