@@ -0,0 +1,167 @@
< 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 : '待同步' , SYNCING : '同步中' , SUCCESS : '同步成功' , FAILED : '同步失败' ,
}
const statusType : Record < MojiaSkuSyncStatus , ' success ' | ' warning ' | ' danger ' | ' info ' > = {
PENDING : 'warning' , SYNCING : 'info' , 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 : 220 px ; }
. density - actions { display : flex ; gap : var ( -- admin - space - 2 ) ; }
. sync - image { width : 52 px ; height : 40 px ; border - radius : 4 px ; 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 , 1 fr ) ) ; gap : 0 16 px ; }
. sync - edit - grid : deep ( . el - input - number ) { width : 100 % ; }
@ media ( max - width : 900 px ) { . sync - filters . el - input { width : 180 px ; } }
< / style >