168 lines
9.3 KiB
Vue
168 lines
9.3 KiB
Vue
<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>
|