Files
admin-web/src/modules/overseas-warehouse/pages/MojiaSkuSyncView.vue
T

168 lines
9.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import '@/styles/adminDensity.css'
import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Edit3, RefreshCw, RotateCw } from '@lucide/vue'
import AdminPage from '@/shared/components/AdminPage.vue'
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
import AppDialog from '@/shared/components/AppDialog.vue'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { getApiErrorMessage } from '@/shared/api/http'
import {
listMojiaSkuSync, retryMojiaSkuSync, updateMojiaSkuSync,
type MojiaSkuSyncRecord, type MojiaSkuSyncStatus,
} from '../api/mojiaSkuSync'
const permission = {
list: 'platform:mojia-sku-sync:list',
edit: 'platform:mojia-sku-sync:edit',
retry: 'platform:mojia-sku-sync:retry',
} as const
const access = useAccessPreviewStore()
const canEdit = computed(() => access.can(permission.edit))
const canRetry = computed(() => access.can(permission.retry))
const rows = ref<MojiaSkuSyncRecord[]>([])
const total = ref(0)
const loading = ref(false)
const error = ref('')
const actionId = ref('')
const page = ref(1)
const size = ref(20)
const form = reactive({ skuCode: '', syncStatus: '' as '' | MojiaSkuSyncStatus })
const editOpen = ref(false)
const editing = ref<MojiaSkuSyncRecord | null>(null)
const editForm = reactive({ skuCode: '', lengthCm: 0, widthCm: 0, heightCm: 0, weightKg: 0, revision: 0 })
const columns: DataTableColumn[] = [
{ key: 'actions', label: '操作', width: 190, fixed: 'left', slotName: 'actions' },
{ key: 'skuCode', prop: 'skuCode', label: 'SKU', minWidth: 180, showOverflowTooltip: true },
{ key: 'image', label: '测量图片', width: 110, slotName: 'image' },
{ key: 'dimensions', label: '长 × 宽 × 高(cm)', width: 170, slotName: 'dimensions' },
{ key: 'weightKg', prop: 'weightKg', label: '重量(kg)', width: 110 },
{ key: 'syncStatus', label: '同步状态', width: 120, slotName: 'syncStatus' },
{ key: 'match', label: '领星 SKU', minWidth: 150, slotName: 'match' },
{ key: 'measuredAt', label: '测量时间', width: 170, slotName: 'measuredAt' },
{ key: 'failure', label: '失败原因', minWidth: 220, slotName: 'failure' },
]
const statusLabel: Record<MojiaSkuSyncStatus, string> = {
PENDING: '待同步', SUCCESS: '同步成功', FAILED: '同步失败',
}
const statusType: Record<MojiaSkuSyncStatus, 'success' | 'warning' | 'danger' | 'info'> = {
PENDING: 'warning', SUCCESS: 'success', FAILED: 'danger',
}
async function load() {
loading.value = true
error.value = ''
try {
const result = await listMojiaSkuSync({ page: page.value, size: size.value, skuCode: form.skuCode || undefined, syncStatus: form.syncStatus || undefined })
rows.value = result.records
total.value = result.total
} catch (cause) {
error.value = getApiErrorMessage(cause, 'SKU同步记录加载失败')
} finally { loading.value = false }
}
function search() { page.value = 1; void load() }
function reset() { form.skuCode = ''; form.syncStatus = ''; search() }
function formatTime(value?: string | null) { return value ? value.replace('T', ' ').slice(0, 19) : '—' }
function asRecord(row: unknown) { return row as MojiaSkuSyncRecord }
function syncLabel(row: unknown) { return statusLabel[asRecord(row).syncStatus] }
function syncTagType(row: unknown) { return statusType[asRecord(row).syncStatus] }
function openEdit(row: MojiaSkuSyncRecord) {
editing.value = row
Object.assign(editForm, { skuCode: row.skuCode, lengthCm: row.lengthCm, widthCm: row.widthCm, heightCm: row.heightCm, weightKg: row.weightKg, revision: row.revision })
editOpen.value = true
}
async function saveEdit() {
const row = editing.value
if (!row || !editForm.skuCode.trim() || [editForm.lengthCm, editForm.widthCm, editForm.heightCm, editForm.weightKg].some((value) => value <= 0)) {
ElMessage.warning('请填写有效的 SKU、尺寸和重量')
return
}
actionId.value = row.id
try {
await updateMojiaSkuSync(row.id, { ...editForm, skuCode: editForm.skuCode.trim() })
editOpen.value = false
ElMessage.success('已保存人工修正版,记录将重新进入同步队列')
await load()
} catch (cause) { ElMessage.error(getApiErrorMessage(cause, '保存修正版失败')) }
finally { actionId.value = '' }
}
async function retry(row: MojiaSkuSyncRecord) {
try {
await ElMessageBox.confirm(`确认重新推送 SKU「${row.skuCode}」的最新测量值?`, '重新推送领星', { type: 'warning', confirmButtonText: '重新推送', cancelButtonText: '取消', customClass: 'admin-density-confirm' })
} catch { return }
actionId.value = row.id
try { await retryMojiaSkuSync(row.id, row.revision); ElMessage.success('已进入待同步队列'); await load() }
catch (cause) { ElMessage.error(getApiErrorMessage(cause, '重新推送失败')) }
finally { actionId.value = '' }
}
onMounted(() => { void load() })
</script>
<template>
<AdminPage class="admin-density mojia-sku-sync-page">
<ListPageLayout aria-label="一件代发 SKU 同步">
<section class="surface-panel table-panel" :aria-busy="loading">
<el-alert v-if="error" type="error" :title="error" show-icon :closable="false" />
<DataTable :data="rows" :columns="columns" :loading="loading" row-key="id" border stripe empty-text="暂无墨家 SKU 测量记录">
<template #toolbar-start>
<div class="sync-filters">
<el-input v-model.trim="form.skuCode" clearable placeholder="搜索 SKU" @keyup.enter="search" />
<el-select v-model="form.syncStatus" clearable placeholder="同步状态" style="width: 140px">
<el-option v-for="(label, value) in statusLabel" :key="value" :label="label" :value="value" />
</el-select>
<el-button type="primary" @click="search">查询</el-button>
<el-button @click="reset">重置</el-button>
</div>
</template>
<template #toolbar-actions><el-button :loading="loading" @click="load"><RefreshCw :size="16" />刷新</el-button></template>
<template #actions="{ row }">
<div class="density-actions">
<el-button v-if="canEdit" class="density-action" @click="openEdit(asRecord(row))"><Edit3 :size="15" />编辑</el-button>
<el-button v-if="canRetry && row.syncStatus === 'FAILED'" class="density-action" data-tone="primary" :loading="actionId === row.id" @click="retry(asRecord(row))"><RotateCw :size="15" />重新推送</el-button>
</div>
</template>
<template #image="{ row }"><el-image v-if="row.imageUrl" :src="row.imageUrl" fit="cover" class="sync-image" preview-teleported /><span v-else>—</span></template>
<template #dimensions="{ row }">{{ row.lengthCm }} × {{ row.widthCm }} × {{ row.heightCm }}</template>
<template #syncStatus="{ row }"><el-tag size="small" :type="syncTagType(row)">{{ syncLabel(row) }}</el-tag></template>
<template #match="{ row }">{{ row.lingxingSkuId || '未匹配' }}</template>
<template #measuredAt="{ row }">{{ formatTime(row.measuredAt) }}</template>
<template #failure="{ row }">{{ row.syncFailureReason || '—' }}</template>
</DataTable>
<div class="sync-pagination"><el-pagination v-model:current-page="page" v-model:page-size="size" :total="total" background layout="total, sizes, prev, pager, next" @current-change="load" @size-change="search" /></div>
</section>
</ListPageLayout>
<AppDialog v-model="editOpen" class="admin-density-dialog" title="编辑 SKU 测量修正版" width="min(620px, calc(100vw - 28px))" confirm-text="保存并重新同步" :confirm-loading="Boolean(actionId)" @confirm="saveEdit">
<el-form label-position="top" class="sync-edit-form">
<el-form-item label="SKU"><el-input v-model.trim="editForm.skuCode" /></el-form-item>
<div class="sync-edit-grid">
<el-form-item label="长(cm)"><el-input-number v-model="editForm.lengthCm" :min="0.001" :controls="false" /></el-form-item>
<el-form-item label="宽(cm)"><el-input-number v-model="editForm.widthCm" :min="0.001" :controls="false" /></el-form-item>
<el-form-item label="高(cm)"><el-input-number v-model="editForm.heightCm" :min="0.001" :controls="false" /></el-form-item>
<el-form-item label="重量(kg)"><el-input-number v-model="editForm.weightKg" :min="0.001" :controls="false" /></el-form-item>
</div>
<el-alert type="info" :closable="false" title="原始墨家测量记录不可变,本次保存会生成新的人工修正版并进入同步队列。" />
</el-form>
</AppDialog>
</AdminPage>
</template>
<style scoped>
.sync-filters { display: flex; flex-wrap: wrap; align-items: center; gap: var(--admin-space-2); }
.sync-filters .el-input { width: 220px; }
.density-actions { display: flex; gap: var(--admin-space-2); }
.sync-image { width: 52px; height: 40px; border-radius: 4px; background: var(--cc-surface-muted); }
.sync-pagination { display: flex; justify-content: flex-end; padding: var(--admin-space-4); }
.sync-edit-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 16px; }
.sync-edit-grid :deep(.el-input-number) { width: 100%; }
@media (max-width: 900px) { .sync-filters .el-input { width: 180px; } }
</style>