align(去重汇总): 页顶数据权限分组汇总卡(超管全部分组·N个/组员chips组长·组员/未加入分组)、导出改流式下载+等待遮罩(正在生成导出文件/已等待N秒)、分组空值显示未分组(对齐 admin.js renderDedupeGroupSummary/showDedupeTotalDataExportWait)
This commit is contained in:
@@ -1,8 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { formatDateTime } from '@/utils/datetime'
|
import { formatDateTime } from '@/utils/datetime'
|
||||||
/** 数据去重总数据(registry):对齐 admin panel-dedupe-total-data —— 扩充筛选(用户名/日期/分组)、行编辑/删除、新增/删除导入(轮询)、导出。 */
|
/** 数据去重总数据(registry):对齐 admin panel-dedupe-total-data —— 顶部数据权限分组汇总卡、扩充筛选(用户名/日期/分组)、
|
||||||
import { onMounted, reactive, ref } from 'vue'
|
* 行编辑/删除、新增/删除导入(轮询)、导出(等待遮罩+已等待秒数)。 */
|
||||||
|
import { computed, onMounted, reactive, ref } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { useAdminSessionStore } from '@/stores/admin-session'
|
||||||
import {
|
import {
|
||||||
fetchDedupeTotalList,
|
fetchDedupeTotalList,
|
||||||
updateDedupeTotal,
|
updateDedupeTotal,
|
||||||
@@ -10,7 +12,7 @@ import {
|
|||||||
fetchDedupeTotalGroups,
|
fetchDedupeTotalGroups,
|
||||||
type DedupeGroupOption,
|
type DedupeGroupOption,
|
||||||
} from './dedupe-total-api.ts'
|
} from './dedupe-total-api.ts'
|
||||||
import type { DedupeTotalItem } from './dedupe-total-model.ts'
|
import { dedupeGroupSummaryOf, type DedupeTotalItem } from './dedupe-total-model.ts'
|
||||||
import { ASIN_COUNTRY_CODES, asinCountryLabel } from './asin-country.ts'
|
import { ASIN_COUNTRY_CODES, asinCountryLabel } from './asin-country.ts'
|
||||||
import { createDedupeTotalFilterState, toDedupeListParams } from './dedupe-total-filter.ts'
|
import { createDedupeTotalFilterState, toDedupeListParams } from './dedupe-total-filter.ts'
|
||||||
import {
|
import {
|
||||||
@@ -23,6 +25,7 @@ import { importOutcomeText, importTaskFinished, isAllowedImportFile } from './de
|
|||||||
import type { DedupeImportProgress } from './dedupe-import-model.ts'
|
import type { DedupeImportProgress } from './dedupe-import-model.ts'
|
||||||
import { toExportUrl } from './dedupe-total-export.ts'
|
import { toExportUrl } from './dedupe-total-export.ts'
|
||||||
|
|
||||||
|
const session = useAdminSessionStore()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const rows = ref<DedupeTotalItem[]>([])
|
const rows = ref<DedupeTotalItem[]>([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
@@ -31,6 +34,11 @@ const pageSize = 15
|
|||||||
const filter = reactive(createDedupeTotalFilterState())
|
const filter = reactive(createDedupeTotalFilterState())
|
||||||
const groups = ref<DedupeGroupOption[]>([])
|
const groups = ref<DedupeGroupOption[]>([])
|
||||||
|
|
||||||
|
/** 页顶数据权限分组汇总(对齐 admin.js renderDedupeGroupSummary)。 */
|
||||||
|
const groupSummary = computed(() =>
|
||||||
|
dedupeGroupSummaryOf(groups.value, session.user?.id ?? null, session.user?.role || ''),
|
||||||
|
)
|
||||||
|
|
||||||
const editVisible = ref(false)
|
const editVisible = ref(false)
|
||||||
const editSaving = ref(false)
|
const editSaving = ref(false)
|
||||||
const editTarget = ref<DedupeTotalItem | null>(null)
|
const editTarget = ref<DedupeTotalItem | null>(null)
|
||||||
@@ -46,6 +54,26 @@ const importRunning = ref(false)
|
|||||||
const importProgress = ref('')
|
const importProgress = ref('')
|
||||||
|
|
||||||
const exporting = ref(false)
|
const exporting = ref(false)
|
||||||
|
/** 导出等待遮罩与秒表(对齐 admin.js showDedupeTotalDataExportWait)。 */
|
||||||
|
const exportWaitVisible = ref(false)
|
||||||
|
const exportWaitSeconds = ref(0)
|
||||||
|
let exportWaitTimer: ReturnType<typeof setInterval> | null = null
|
||||||
|
|
||||||
|
function startExportWait(): void {
|
||||||
|
exportWaitSeconds.value = 0
|
||||||
|
exportWaitVisible.value = true
|
||||||
|
exportWaitTimer = setInterval(() => {
|
||||||
|
exportWaitSeconds.value += 1
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopExportWait(): void {
|
||||||
|
if (exportWaitTimer) {
|
||||||
|
clearInterval(exportWaitTimer)
|
||||||
|
exportWaitTimer = null
|
||||||
|
}
|
||||||
|
exportWaitVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
async function load(): Promise<void> {
|
async function load(): Promise<void> {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
@@ -213,22 +241,42 @@ async function submitImport(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function doExport(): void {
|
async function doExport(): Promise<void> {
|
||||||
const { startDate, endDate } = filter
|
const { startDate, endDate } = filter
|
||||||
if (startDate && endDate && startDate > endDate) {
|
if (startDate && endDate && startDate > endDate) {
|
||||||
ElMessage.warning('开始日期不能晚于结束日期')
|
ElMessage.warning('开始日期不能晚于结束日期')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (exporting.value) return
|
||||||
exporting.value = true
|
exporting.value = true
|
||||||
window.setTimeout(() => {
|
startExportWait()
|
||||||
|
try {
|
||||||
|
// 对齐 admin.js:fetch 流式下载,带筛选参数;失败响应(JSON)解析错误提示。
|
||||||
|
const response = await window.fetch(toExportUrl(filter), { credentials: 'include' })
|
||||||
|
const contentType = response.headers.get('content-type') || ''
|
||||||
|
if (!response.ok || contentType.includes('application/json')) {
|
||||||
|
const body = (await response.json().catch(() => ({}))) as { error?: string; message?: string; msg?: string }
|
||||||
|
throw new Error(body.error || body.message || body.msg || '导出失败')
|
||||||
|
}
|
||||||
|
const blob = await response.blob()
|
||||||
|
const disposition = response.headers.get('content-disposition') || ''
|
||||||
|
const matched = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(disposition)
|
||||||
|
const filename = matched ? decodeURIComponent(matched[1]) : 'dedupe-total-data.xlsx'
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
const anchor = document.createElement('a')
|
const anchor = document.createElement('a')
|
||||||
anchor.href = toExportUrl(filter)
|
anchor.href = url
|
||||||
|
anchor.download = filename
|
||||||
document.body.appendChild(anchor)
|
document.body.appendChild(anchor)
|
||||||
anchor.click()
|
anchor.click()
|
||||||
anchor.remove()
|
anchor.remove()
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
ElMessage.success('导出完成')
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error instanceof Error ? error.message : '导出失败,请重试')
|
||||||
|
} finally {
|
||||||
|
stopExportWait()
|
||||||
exporting.value = false
|
exporting.value = false
|
||||||
ElMessage.success('导出文件已开始下载')
|
}
|
||||||
}, 120)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -253,6 +301,24 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<el-card shadow="never" class="group-summary-card">
|
||||||
|
<div class="group-summary">
|
||||||
|
<span class="group-summary-title">数据权限分组</span>
|
||||||
|
<div class="group-summary-body">
|
||||||
|
<template v-if="groupSummary.kind === 'all'">
|
||||||
|
<span class="summary-chip">全部分组 · {{ groupSummary.count }} 个</span>
|
||||||
|
</template>
|
||||||
|
<span v-else-if="groupSummary.kind === 'none'" class="dim">未加入分组</span>
|
||||||
|
<template v-else>
|
||||||
|
<span v-for="chip in groupSummary.chips" :key="chip.name" class="summary-chip" :title="chip.name">
|
||||||
|
{{ chip.name }} · {{ chip.relation }}
|
||||||
|
</span>
|
||||||
|
<span v-if="groupSummary.extra > 0" class="dim">另 {{ groupSummary.extra }} 个</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
<el-card shadow="never">
|
<el-card shadow="never">
|
||||||
<div class="filter-grid">
|
<div class="filter-grid">
|
||||||
<div class="f-item">
|
<div class="f-item">
|
||||||
@@ -299,7 +365,7 @@ onMounted(() => {
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="username" label="用户名" width="140" />
|
<el-table-column prop="username" label="用户名" width="140" />
|
||||||
<el-table-column label="分组" width="150">
|
<el-table-column label="分组" width="150">
|
||||||
<template #default="{ row }">{{ (row as DedupeTotalItem).groupName || '—' }}</template>
|
<template #default="{ row }">{{ (row as DedupeTotalItem).groupName || '未分组' }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="createdAt" label="创建时间" width="170">
|
<el-table-column prop="createdAt" label="创建时间" width="170">
|
||||||
<template #default="{ row }">{{ formatDateTime(row.createdAt) }}</template>
|
<template #default="{ row }">{{ formatDateTime(row.createdAt) }}</template>
|
||||||
@@ -361,12 +427,36 @@ onMounted(() => {
|
|||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<div v-if="exportWaitVisible" class="export-wait-mask" role="dialog" aria-modal="true" aria-labelledby="dedupe-export-wait-title">
|
||||||
|
<div class="export-wait">
|
||||||
|
<span class="wait-spinner" aria-hidden="true"></span>
|
||||||
|
<div>
|
||||||
|
<div class="export-wait-title" id="dedupe-export-wait-title">正在生成导出文件</div>
|
||||||
|
<div class="export-wait-detail">数据量较大,请耐心等待并保持页面打开。已等待 {{ exportWaitSeconds }} 秒</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page-heading { display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; }
|
.page-heading { display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; }
|
||||||
.heading-actions { display: flex; gap: 8px; flex: none; }
|
.heading-actions { display: flex; gap: 8px; flex: none; }
|
||||||
|
.group-summary-card { margin-bottom: 12px; }
|
||||||
|
.group-summary { display: flex; align-items: flex-start; gap: 14px; }
|
||||||
|
.group-summary-title { flex: none; color: var(--el-text-color-secondary); font-size: 12.5px; line-height: 24px; }
|
||||||
|
.group-summary-body { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
|
||||||
|
.summary-chip {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--el-color-primary-light-9);
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
font-size: 12.5px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
.dim { color: var(--el-text-color-secondary); font-size: 12.5px; }
|
||||||
.filter-grid { display: flex; flex-wrap: wrap; gap: 12px 18px; }
|
.filter-grid { display: flex; flex-wrap: wrap; gap: 12px 18px; }
|
||||||
.f-item { display: flex; flex-direction: column; gap: 6px; width: 200px; }
|
.f-item { display: flex; flex-direction: column; gap: 6px; width: 200px; }
|
||||||
.f-item label { color: var(--el-text-color-secondary); font-size: 12px; }
|
.f-item label { color: var(--el-text-color-secondary); font-size: 12px; }
|
||||||
@@ -374,4 +464,37 @@ onMounted(() => {
|
|||||||
.table-footer { display: flex; justify-content: space-between; align-items: center; padding-top: 14px; }
|
.table-footer { display: flex; justify-content: space-between; align-items: center; padding-top: 14px; }
|
||||||
.table-footer span { color: var(--el-text-color-secondary); font-size: 12.5px; }
|
.table-footer span { color: var(--el-text-color-secondary); font-size: 12.5px; }
|
||||||
.import-tip { margin: 6px 0 0; color: var(--el-text-color-secondary); font-size: 12px; line-height: 1.6; }
|
.import-tip { margin: 6px 0 0; color: var(--el-text-color-secondary); font-size: 12px; line-height: 1.6; }
|
||||||
|
.export-wait-mask {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 3000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(24, 29, 43, 0.45);
|
||||||
|
backdrop-filter: blur(3px);
|
||||||
|
}
|
||||||
|
.export-wait {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
background: var(--el-bg-color);
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 22px 28px;
|
||||||
|
box-shadow: 0 12px 40px -12px rgba(39, 67, 94, 0.35);
|
||||||
|
}
|
||||||
|
.wait-spinner {
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
flex: none;
|
||||||
|
border: 3px solid var(--el-color-primary-light-7);
|
||||||
|
border-top-color: var(--el-color-primary);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: dedupe-spin 0.9s linear infinite;
|
||||||
|
}
|
||||||
|
@keyframes dedupe-spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
.export-wait-title { font-size: 15px; font-weight: 600; }
|
||||||
|
.export-wait-detail { margin-top: 4px; color: var(--el-text-color-secondary); font-size: 12.5px; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { http } from '@/api/http'
|
import { http } from '@/api/http'
|
||||||
import { unwrap } from '@/api/envelope'
|
import { unwrap } from '@/api/envelope'
|
||||||
import { normalizeAsinPageParams, toAsinPageQuery, type AsinListParams } from './asin-filter'
|
import { normalizeAsinPageParams, toAsinPageQuery, type AsinListParams } from './asin-filter'
|
||||||
import { parseDedupeTotalPage, type DedupeTotalPageResult } from './dedupe-total-model'
|
import { parseDedupeTotalPage, parseDedupeGroupOptionList, type DedupeGroupOption, type DedupeTotalPageResult } from './dedupe-total-model'
|
||||||
|
|
||||||
export const DEDUPE_TOTAL_ENDPOINT = '/api/admin/dedupe-total-data'
|
export const DEDUPE_TOTAL_ENDPOINT = '/api/admin/dedupe-total-data'
|
||||||
/** 数据权限分组选项源(与店铺/去重共用同一分组集)。 */
|
/** 数据权限分组选项源(与店铺/去重共用同一分组集)。 */
|
||||||
@@ -14,10 +14,7 @@ export async function fetchDedupeTotalList(params: Partial<AsinListParams> = {})
|
|||||||
return parseDedupeTotalPage(data)
|
return parseDedupeTotalPage(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DedupeGroupOption {
|
export { type DedupeGroupOption }
|
||||||
id: number
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 编辑单行去重总数据(ASIN 值 + 分组):PUT /dedupe-total-data/{id}。 */
|
/** 编辑单行去重总数据(ASIN 值 + 分组):PUT /dedupe-total-data/{id}。 */
|
||||||
export async function updateDedupeTotal(id: number, dataValue: string, groupId: number): Promise<void> {
|
export async function updateDedupeTotal(id: number, dataValue: string, groupId: number): Promise<void> {
|
||||||
@@ -34,30 +31,8 @@ export async function deleteDedupeTotal(id: number): Promise<void> {
|
|||||||
unwrap<unknown>(data)
|
unwrap<unknown>(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 加载数据权限分组选项:GET /shop-manage-groups → {id,name}[]。 */
|
/** 加载数据权限分组选项:GET /shop-manages/groups → {id,name,leaderUserId}[]。 */
|
||||||
export async function fetchDedupeTotalGroups(): Promise<DedupeGroupOption[]> {
|
export async function fetchDedupeTotalGroups(): Promise<DedupeGroupOption[]> {
|
||||||
const { data } = await http.get<unknown>(DEDUPE_GROUPS_ENDPOINT)
|
const { data } = await http.get<unknown>(DEDUPE_GROUPS_ENDPOINT)
|
||||||
return parseDedupeGroupOptionList(data)
|
return parseDedupeGroupOptionList(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 归一化分组选项负载(信封 items 或裸数组),缺 id 行丢弃。 */
|
|
||||||
export function parseDedupeGroupOptionList(payload: unknown): DedupeGroupOption[] {
|
|
||||||
const core = unwrap<unknown>(payload)
|
|
||||||
const rawList = Array.isArray(core)
|
|
||||||
? core
|
|
||||||
: core && typeof core === 'object'
|
|
||||||
? (core as Record<string, unknown>).items
|
|
||||||
: []
|
|
||||||
if (!Array.isArray(rawList)) return []
|
|
||||||
const options: DedupeGroupOption[] = []
|
|
||||||
for (const raw of rawList) {
|
|
||||||
if (!raw || typeof raw !== 'object') continue
|
|
||||||
const record = raw as Record<string, unknown>
|
|
||||||
const id = typeof record.id === 'number' ? Math.floor(record.id) : Number(record.group_id)
|
|
||||||
if (!Number.isFinite(id) || id <= 0) continue
|
|
||||||
const nameRaw = record.group_name ?? record.name
|
|
||||||
const name = typeof nameRaw === 'string' ? nameRaw.trim() : ''
|
|
||||||
options.push({ id, name: name || `分组${id}` })
|
|
||||||
}
|
|
||||||
return options
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -74,3 +74,60 @@ export function parseDedupeTotalPage(payload: unknown): DedupeTotalPageResult {
|
|||||||
export function emptyDedupePageParams(): Partial<AsinListParams> {
|
export function emptyDedupePageParams(): Partial<AsinListParams> {
|
||||||
return { page: 1, pageSize: ASIN_PAGE_DEFAULT_SIZE }
|
return { page: 1, pageSize: ASIN_PAGE_DEFAULT_SIZE }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 数据权限分组选项(GET /api/admin/shop-manages/groups items)。 */
|
||||||
|
export interface DedupeGroupOption {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
leaderUserId?: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 归一化分组选项负载(信封 items 或裸数组),缺 id 行丢弃;解析组长 id 供汇总卡判定组长/组员。 */
|
||||||
|
export function parseDedupeGroupOptionList(payload: unknown): DedupeGroupOption[] {
|
||||||
|
const core = unwrap<unknown>(payload)
|
||||||
|
const rawList = Array.isArray(core)
|
||||||
|
? core
|
||||||
|
: core && typeof core === 'object'
|
||||||
|
? (core as Record<string, unknown>).items
|
||||||
|
: []
|
||||||
|
if (!Array.isArray(rawList)) return []
|
||||||
|
const options: DedupeGroupOption[] = []
|
||||||
|
for (const raw of rawList) {
|
||||||
|
if (!raw || typeof raw !== 'object') continue
|
||||||
|
const record = raw as Record<string, unknown>
|
||||||
|
const id = typeof record.id === 'number' ? Math.floor(record.id) : Number(record.group_id)
|
||||||
|
if (!Number.isFinite(id) || id <= 0) continue
|
||||||
|
const nameRaw = record.group_name ?? record.name
|
||||||
|
const name = typeof nameRaw === 'string' ? nameRaw.trim() : ''
|
||||||
|
const option: DedupeGroupOption = { id, name: name || `分组${id}` }
|
||||||
|
const leaderRaw = record.leader_user_id ?? record.leaderUserId
|
||||||
|
const leaderUserId = typeof leaderRaw === 'number' ? Math.floor(leaderRaw) : Number(leaderRaw)
|
||||||
|
if (Number.isFinite(leaderUserId) && leaderUserId > 0) option.leaderUserId = leaderUserId
|
||||||
|
options.push(option)
|
||||||
|
}
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 页顶数据权限分组汇总(对齐 admin.js renderDedupeGroupSummary)。 */
|
||||||
|
export type DedupeGroupSummary =
|
||||||
|
| { kind: 'all'; count: number }
|
||||||
|
| { kind: 'none' }
|
||||||
|
| { kind: 'chips'; chips: Array<{ name: string; relation: string }>; extra: number }
|
||||||
|
|
||||||
|
export function dedupeGroupSummaryOf(
|
||||||
|
groups: DedupeGroupOption[],
|
||||||
|
currentUserId: number | null,
|
||||||
|
operatorRole: string,
|
||||||
|
): DedupeGroupSummary {
|
||||||
|
if ((operatorRole || '').toLowerCase() === 'super_admin') {
|
||||||
|
return { kind: 'all', count: groups.length }
|
||||||
|
}
|
||||||
|
if (!groups.length) return { kind: 'none' }
|
||||||
|
const visible = groups.slice(0, 4)
|
||||||
|
const currentKey = currentUserId != null ? String(currentUserId) : ''
|
||||||
|
const chips = visible.map((group) => ({
|
||||||
|
name: group.name,
|
||||||
|
relation: String(group.leaderUserId ?? '') === currentKey ? '组长' : '组员',
|
||||||
|
}))
|
||||||
|
return { kind: 'chips', chips, extra: Math.max(groups.length - visible.length, 0) }
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import test from 'node:test'
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import { readSource } from './helpers.ts'
|
||||||
|
import {
|
||||||
|
dedupeGroupSummaryOf,
|
||||||
|
parseDedupeGroupOptionList,
|
||||||
|
type DedupeGroupOption,
|
||||||
|
} from '../src/pages/asin/dedupe-total-model.ts'
|
||||||
|
|
||||||
|
/** 对齐 admin.js:4355-4380 renderDedupeGroupSummary / 3362-3365 导出等待 / 3306 未分组。 */
|
||||||
|
|
||||||
|
function group(id: number, name: string, leaderUserId: number | null = null): DedupeGroupOption {
|
||||||
|
return { id, name, leaderUserId }
|
||||||
|
}
|
||||||
|
|
||||||
|
test('align_dedupe_summary_super_admin_shows_all_count', () => {
|
||||||
|
const summary = dedupeGroupSummaryOf([group(1, 'A组'), group(2, 'B组')], 99, 'super_admin')
|
||||||
|
assert.deepEqual(summary, { kind: 'all', count: 2 })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('align_dedupe_summary_non_super_empty_shows_not_joined', () => {
|
||||||
|
const summary = dedupeGroupSummaryOf([], 7, 'admin')
|
||||||
|
assert.deepEqual(summary, { kind: 'none' })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('align_dedupe_summary_chips_with_relation', () => {
|
||||||
|
const summary = dedupeGroupSummaryOf([group(1, 'A组', 7), group(2, 'B组', 9)], 7, 'admin')
|
||||||
|
assert.deepEqual(summary, {
|
||||||
|
kind: 'chips',
|
||||||
|
chips: [
|
||||||
|
{ name: 'A组', relation: '组长' },
|
||||||
|
{ name: 'B组', relation: '组员' },
|
||||||
|
],
|
||||||
|
extra: 0,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('align_dedupe_summary_caps_at_four_with_extra', () => {
|
||||||
|
const groups = [1, 2, 3, 4, 5, 6].map((id) => group(id, `G${id}`, 9))
|
||||||
|
const summary = dedupeGroupSummaryOf(groups, 7, 'admin')
|
||||||
|
assert.equal(summary.kind, 'chips')
|
||||||
|
if (summary.kind === 'chips') {
|
||||||
|
assert.equal(summary.chips.length, 4)
|
||||||
|
assert.equal(summary.extra, 2)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('align_dedupe_group_option_parses_leader', () => {
|
||||||
|
const options = parseDedupeGroupOptionList({
|
||||||
|
success: true,
|
||||||
|
data: { items: [{ id: 3, group_name: 'C组', leader_user_id: 12 }] },
|
||||||
|
})
|
||||||
|
assert.deepEqual(options, [{ id: 3, name: 'C组', leaderUserId: 12 }])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('align_dedupe_page_summary_and_export_wait_wiring', () => {
|
||||||
|
const page = readSource('src/pages/asin/DedupeRegistryPage.vue')
|
||||||
|
assert.match(page, /数据权限分组/, '页顶数据权限分组汇总卡')
|
||||||
|
assert.match(page, /dedupeGroupSummaryOf/, '汇总走纯模型')
|
||||||
|
assert.match(page, /正在生成导出文件/, '导出等待遮罩文案对齐')
|
||||||
|
assert.match(page, /已等待/, '导出等待含等待秒数')
|
||||||
|
assert.match(page, /未分组/, '分组空值显示未分组')
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user