@@ -0,0 +1,558 @@
< template >
< el-dialog v-model = "dialogVisible" :title="`SKU管理 - ${assetName}`" width="1000px" :close-on-click-modal="false" @close="onClose" >
< ! - - 搜索区域 - - >
< div class = "sku-search mb15" >
< el-button type = "primary" @click ="onOpenAddSku" >
< el -icon > < ele-Plus / > < / el-icon >
添加SKU
< / el-button >
< el-input v-model = "queryParams.keyword" placeholder="请输入SKU名称" clearable style="width: 180px; margin-left: 10px" / >
< el-select v-model = "queryParams.status" placeholder="状态" clearable style="width: 100px; margin-left: 10px" >
< el -option label = "激活" :value = "1" / >
< el-option label = "未激活" :value = "0" / >
< / el-select >
< el-button type = "primary" style = "margin-left: 10px" @click ="getSkuList" >
< el -icon > < ele-Search / > < / el-icon >
搜索
< / el-button >
< el-button @click ="onResetQuery" > 重置 < / el -button >
< / div >
<!-- SKU 列表 -- >
< el-table :data = "tableData" v-loading = "loading" border style="width: 100%" >
< el -table -column prop = "skuName" label = "SKU名称" min -width = " 120 " show -overflow -tooltip / >
< el-table-column prop = "specValues" label = "规格属性" min -width = " 150 " >
< template # default = "scope" >
< span v-if = "scope.row.specValues && Object.keys(scope.row.specValues).length > 0" >
< el -tag v-for = "(value, key) in scope.row.specValues" :key="key" size="small" style="margin-right: 4px" >
{{ key }} : {{ value }}
< / el -tag >
< / span >
< span v-else > - < / span >
< / template >
< / el -table -column >
< el-table-column prop = "price" label = "价格" width = "100" align = "center" >
< template # default = "scope" >
¥ { { ( scope . row . price / 100 ) . toFixed ( 2 ) } }
< / template >
< / el-table-column >
< el-table-column prop = "stock" label = "库存数量" width = "100" align = "center" >
< template # default = "scope" >
{ { scope . row . unlimitedStock ? '无限' : scope . row . stock } }
< / template >
< / el-table-column >
< el-table-column prop = "status" label = "状态" width = "80" align = "center" >
< template # default = "scope" >
< el-tag : type = "scope.row.status === 1 ? 'success' : 'info'" >
{ { scope . row . status === 1 ? '激活' : '未激活' } }
< / el-tag >
< / template >
< / el-table-column >
< el-table-column prop = "sort" label = "排序" width = "80" align = "center" / >
< el-table-column prop = "createdAt" label = "创建时间" width = "160" align = "center" / >
< el-table-column prop = "updatedAt" label = "修改时间" width = "160" align = "center" / >
< el-table-column label = "操作" width = "120" align = "center" >
< template # default = "scope" >
< el-button size = "small" text type = "primary" @click ="onEditSku(scope.row)" > 编辑 < / el -button >
< el-button size = "small" text type = "danger" @click ="onDeleteSku(scope.row)" > 删除 < / el -button >
< / template >
< / el-table-column >
< / el-table >
<!-- 分页 -- >
< div class = "mt15" style = "text-align: right" >
< el-pagination
v -model :current-page = "queryParams.page"
v -model :page-size = "queryParams.pageSize"
: page -sizes = " [ 10 , 20 , 50 , 100 ] "
:total = "total"
layout = "total, sizes, prev, pager, next, jumper"
@ size -change = " onSizeChange "
@ current -change = " onCurrentChange "
/ >
< / div >
<!-- 添加 / 编辑 SKU 弹窗 -- >
< el-dialog v-model = "skuFormVisible" :title="isEditSku ? '编辑SKU' : '添加SKU'" width="500px" :close-on-click-modal="false" append -to -body >
< el-form ref = "skuFormRef" :model = "skuForm" :rules = "skuRules" label -width = " 80px " v-loading = "editLoading" >
< el -form -item label = "资产名称" >
< el-input v-model = "assetName" disabled / >
< / el-form-item >
< el-form-item label = "SKU名称" prop = "skuName" >
< el-input v-model = "skuForm.skuName" placeholder="请输入SKU名称" / >
< / el-form-item >
< el-form-item label = "规格属性" v-if = "assetSpecAttrs.length > 0" >
< div class = "spec-values-container" >
< div v-for = "attr in assetSpecAttrs" :key="attr.name" class="spec-item" >
< span class = "spec-label" > { { attr . name } } : < / span >
< el-select v-if = "attr.options && attr.options.length > 0" v-model="specValuesMap[attr.name]" placeholder="请选择" style="width: 120px" filterable allow -create clearable >
< el-option v-for = "opt in attr.options" :key="opt" :label="opt" :value="opt" / >
< / el-select >
< el-input v-else v-model = "specValuesMap[attr.name]" placeholder="请输入" style="width: 120px" / >
< / div >
< / div >
< / el-form-item >
< el-form-item label = "价格" prop = "price" >
< el-input-number v-model = "skuForm.price" :min="0" :precision="2" :step="0.01" controls-position="right" / >
< span style = "margin-left: 8px" > 元 < / span >
< / el-form-item >
< el-form-item label = "库存数量" prop = "stock" >
< el-input-number v-model = "skuForm.stock" :min="0" :disabled="skuForm.unlimitedStock" controls-position="right" / >
< el-checkbox v-model = "skuForm.unlimitedStock" style="margin-left: 10px" > 无限库存 < / el -checkbox >
< / el-form-item >
< el-form-item label = "状态" >
< el-radio-group v-model = "skuForm.status" >
< el -radio :value = "1" > 激活 < / el-radio >
< el-radio :value = "0" > 未激活 < / el-radio >
< / el-radio-group >
< / el-form-item >
< el-form-item label = "排序" >
< el-input-number v-model = "skuForm.sort" :min="0" controls-position="right" / >
< span style = "margin-left: 8px; color: #909399; font-size: 12px" > 数值越小越靠前 < / span >
< / el-form-item >
< el-form-item label = "图片" prop = "imageUrl" required >
< el-upload
class = "sku-image-uploader"
:show-file-list = "false"
:http-request = "handleSkuImageUpload"
accept = "image/*"
>
< img v-if = "skuImagePreview" :src="skuImagePreview" class="sku-image" / >
< el-icon v-else class = "sku-image-uploader-icon" > < ele-Plus / > < / el-icon >
< / el-upload >
< el-button v-if = "skuImagePreview" type="danger" text size="small" style="margin-left: 10px" @click="removeSkuImage" > 删除 < / el -button >
< / el-form-item >
<!-- < el-form-item label = "描述" >
< el-input v-model = "skuForm.description" type="textarea" :rows="3" placeholder="请输入SKU描述" / >
< / el-form-item > -- >
< / el-form >
< template # footer >
< el-button @click ="skuFormVisible = false" > 取消 < / el -button >
< el-button type = "primary" :loading = "submitLoading" @click ="onSubmitSku" > 确认 < / el -button >
< / template >
< / el-dialog >
< / el-dialog >
< / template >
< script setup lang = "ts" >
import { ref , reactive } from 'vue' ;
import { ElMessage , type FormInstance , type FormRules } from 'element-plus' ;
import { ElMessageBox } from 'element-plus' ;
import { listAssetSkus , createAssetSku , updateAssetSku , deleteAssetSku , getAssetSku , getAsset , uploadAssetImage } from '/@/api/assets/asset' ;
import type { UploadRequestOptions , UploadUserFile } from 'element-plus' ;
interface SpecValueItem {
key : string ;
value : string ;
}
interface AssetSpecAttr {
name : string ;
options ? : string [ ] ;
}
const dialogVisible = ref ( false ) ;
const loading = ref ( false ) ;
const submitLoading = ref ( false ) ;
const editLoading = ref ( false ) ;
const skuFormVisible = ref ( false ) ;
const isEditSku = ref ( false ) ;
const editSkuId = ref ( '' ) ;
const assetId = ref ( '' ) ;
const assetName = ref ( '' ) ;
const assetSpecAttrs = ref < AssetSpecAttr [ ] > ( [ ] ) ;
const fileAddressPrefix = ref ( '' ) ;
const skuImagePreview = ref ( '' ) ;
const tableData = ref < any [ ] > ( [ ] ) ;
const total = ref ( 0 ) ;
const queryParams = reactive ( {
keyword : '' ,
status : undefined as number | undefined ,
page : 1 ,
pageSize : 10 ,
} ) ;
const skuFormRef = ref < FormInstance > ( ) ;
const skuForm = reactive ( {
skuName : '' ,
price : 0 ,
stock : 0 ,
unlimitedStock : false ,
status : 1 ,
sort : 0 ,
imageUrl : '' ,
description : '' ,
} ) ;
const specValuesList = ref < SpecValueItem [ ] > ( [ { key : '' , value : '' } ] ) ;
const specValuesMap = reactive < Record < string , string > > ( { } ) ;
const skuRules : FormRules = {
skuName : [ { required : true , message : '请输入SKU名称' , trigger : 'blur' } ] ,
price : [ { required : true , message : '请输入价格' , trigger : 'blur' } ] ,
stock : [ { required : true , message : '请输入库存数量' , trigger : 'blur' } ] ,
imageUrl : [ { required : true , message : '请上传SKU图片' , trigger : 'change' } ] ,
} ;
// 打开弹窗
const openDialog = ( row : { id : string ; name : string } ) => {
assetId . value = row . id ;
assetName . value = row . name ;
dialogVisible . value = true ;
resetQuery ( ) ;
getSkuList ( ) ;
fetchAssetSpecAttrs ( ) ;
} ;
// 获取资产规格属性(只获取多选类型)
const fetchAssetSpecAttrs = ( ) => {
getAsset ( assetId . value )
. then ( ( res : any ) => {
const data = res . data || { } ;
// 设置图片前缀
fileAddressPrefix . value = data . fileAddressPrefix || data . imgAddressPrefix || '' ;
// 从资产详情中获取规格属性列表
if ( data . specAttrs && Array . isArray ( data . specAttrs ) ) {
assetSpecAttrs . value = data . specAttrs ;
} else if ( data . metadata && Array . isArray ( data . metadata ) ) {
// 只获取 multi_select 类型的属性
assetSpecAttrs . value = data . metadata
. filter ( ( item : any ) => item . type === 'multi_select' )
. map ( ( item : any ) => ( {
name : item . name ,
options : item . options ? . map ( ( opt : any ) => opt . label || opt . value ) || [ ] ,
} ) ) ;
} else {
assetSpecAttrs . value = [ ] ;
}
} )
. catch ( ( ) => {
assetSpecAttrs . value = [ ] ;
} ) ;
} ;
// 获取 SKU 列表
const getSkuList = ( ) => {
loading . value = true ;
listAssetSkus ( {
assetId : assetId . value ,
... queryParams ,
} )
. then ( ( res : any ) => {
tableData . value = res . data ? . list ? ? [ ] ;
total . value = res . data ? . total ? ? 0 ;
} )
. catch ( ( ) => {
tableData . value = [ ] ;
total . value = 0 ;
} )
. finally ( ( ) => {
loading . value = false ;
} ) ;
} ;
// 重置查询
const resetQuery = ( ) => {
queryParams . keyword = '' ;
queryParams . status = undefined ;
queryParams . page = 1 ;
} ;
const onResetQuery = ( ) => {
resetQuery ( ) ;
getSkuList ( ) ;
} ;
// 分页
const onSizeChange = ( size : number ) => {
queryParams . pageSize = size ;
queryParams . page = 1 ;
getSkuList ( ) ;
} ;
const onCurrentChange = ( page : number ) => {
queryParams . page = page ;
getSkuList ( ) ;
} ;
// 打开添加 SKU 弹窗
const onOpenAddSku = ( ) => {
isEditSku . value = false ;
resetSkuForm ( ) ;
skuFormVisible . value = true ;
} ;
// 编辑 SKU
const onEditSku = async ( row : any ) => {
isEditSku . value = true ;
editSkuId . value = row . id || '' ;
resetSkuForm ( ) ;
skuFormVisible . value = true ;
editLoading . value = true ;
try {
const res : any = await getAssetSku ( row . id ) ;
const data = res . data || { } ;
skuForm . skuName = data . skuName || '' ;
skuForm . price = ( data . price || 0 ) / 100 ;
skuForm . stock = data . stock || 0 ;
skuForm . unlimitedStock = data . unlimitedStock || false ;
skuForm . status = data . status ? ? 1 ;
skuForm . sort = data . sort || 0 ;
skuForm . imageUrl = data . imageUrl || '' ;
skuForm . description = data . description || '' ;
// 图片预览回显
if ( data . imageUrl ) {
skuImagePreview . value = formatImageUrl ( data . imageUrl ) ;
}
// 处理规格属性
if ( data . specValues && Object . keys ( data . specValues ) . length > 0 ) {
specValuesList . value = Object . entries ( data . specValues ) . map ( ( [ key , value ] ) => ( {
key ,
value : String ( value ) ,
} ) ) ;
// 回显到 specValuesMap
Object . entries ( data . specValues ) . forEach ( ( [ key , value ] ) => {
specValuesMap [ key ] = String ( value ) ;
} ) ;
} else {
specValuesList . value = [ { key : '' , value : '' } ] ;
}
} catch ( error ) {
skuFormVisible . value = false ;
} finally {
editLoading . value = false ;
}
} ;
// 删除 SKU
const onDeleteSku = ( row : any ) => {
ElMessageBox . confirm ( ` 确定要删除SKU " ${ row . skuName } " 吗? ` , '提示' , {
confirmButtonText : '确认' ,
cancelButtonText : '取消' ,
type : 'warning' ,
} )
. then ( ( ) => {
deleteAssetSku ( row . id ) . then ( ( ) => {
ElMessage . success ( '删除成功' ) ;
getSkuList ( ) ;
} ) ;
} )
. catch ( ( ) => { } ) ;
} ;
// 重置 SKU 表单
const resetSkuForm = ( ) => {
skuForm . skuName = '' ;
skuForm . price = 0 ;
skuForm . stock = 0 ;
skuForm . unlimitedStock = false ;
skuForm . status = 1 ;
skuForm . sort = 0 ;
skuForm . imageUrl = '' ;
skuForm . description = '' ;
specValuesList . value = [ { key : '' , value : '' } ] ;
// 清空 specValuesMap
Object . keys ( specValuesMap ) . forEach ( ( key ) => delete specValuesMap [ key ] ) ;
skuImagePreview . value = '' ;
skuFormRef . value ? . clearValidate ( ) ;
} ;
// 格式化图片 URL
const formatImageUrl = ( url ? : string ) => {
if ( ! url ) return '' ;
if ( /^https?:\/\//i . test ( url ) ) return url ;
if ( /^blob:/i . test ( url ) ) return url ;
return ` ${ fileAddressPrefix . value || '' } ${ url } ` ;
} ;
// 上传图片
const uploadImage = async ( file : File ) : Promise < string > => {
const res : any = await uploadAssetImage ( file ) ;
if ( res . fileAddressPrefix ) {
fileAddressPrefix . value = res . fileAddressPrefix ;
} else if ( res . data && typeof res . data === 'object' && res . data . fileAddressPrefix ) {
fileAddressPrefix . value = res . data . fileAddressPrefix ;
}
if ( res . fileURL ) return res . fileURL ;
if ( res . data && typeof res . data === 'object' ) {
if ( res . data . fileURL ) return res . data . fileURL ;
if ( res . data . url ) return res . data . url ;
}
if ( typeof res . data === 'string' ) return res . data ;
return '' ;
} ;
// 处理 SKU 图片上传
const handleSkuImageUpload = async ( options : UploadRequestOptions ) => {
try {
const url = await uploadImage ( options . file ) ;
if ( url ) {
skuForm . imageUrl = url ;
skuImagePreview . value = formatImageUrl ( url ) ;
skuFormRef . value ? . validateField ( 'imageUrl' ) ;
}
} catch ( error ) {
ElMessage . error ( '图片上传失败' ) ;
}
} ;
// 删除 SKU 图片
const removeSkuImage = ( ) => {
skuForm . imageUrl = '' ;
skuImagePreview . value = '' ;
skuFormRef . value ? . validateField ( 'imageUrl' ) ;
} ;
// 获取属性的可选值
const getAttrOptions = ( key : string ) : string [ ] => {
if ( ! key ) return [ ] ;
const attr = assetSpecAttrs . value . find ( ( a ) => a . name === key ) ;
return attr ? . options || [ ] ;
} ;
// 属性名称变更时清空属性值
const onSpecKeyChange = ( index : number ) => {
specValuesList . value [ index ] . value = '' ;
} ;
// 添加规格属性
const addSpecValue = ( ) => {
specValuesList . value . push ( { key : '' , value : '' } ) ;
} ;
// 删除规格属性
const removeSpecValue = ( index : number ) => {
specValuesList . value . splice ( index , 1 ) ;
} ;
// 提交 SKU
const onSubmitSku = async ( ) => {
const form = skuFormRef . value ;
if ( ! form ) return ;
await form . validate ( async ( valid ) => {
if ( ! valid ) return ;
submitLoading . value = true ;
// 构建规格属性对象(优先使用 specValuesMap)
const specValues : Record < string , string > = { } ;
// 从 specValuesMap 获取(直接展示的属性)
Object . entries ( specValuesMap ) . forEach ( ( [ key , value ] ) => {
if ( key && value ) {
specValues [ key ] = value ;
}
} ) ;
// 兼容旧的 specValuesList
specValuesList . value . forEach ( ( item ) => {
if ( item . key . trim ( ) && ! specValues [ item . key . trim ( ) ] ) {
specValues [ item . key . trim ( ) ] = item . value . trim ( ) ;
}
} ) ;
const data = {
assetId : assetId . value ,
assetName : assetName . value ,
skuName : skuForm . skuName ,
imageUrl : skuForm . imageUrl || undefined ,
specValues : Object . keys ( specValues ) . length > 0 ? specValues : undefined ,
price : Math . round ( skuForm . price * 100 ) ,
unlimitedStock : skuForm . unlimitedStock ,
stock : skuForm . stock ,
sort : skuForm . sort ,
status : skuForm . status ,
description : skuForm . description ,
} ;
try {
if ( isEditSku . value ) {
await updateAssetSku ( { ... data , id : editSkuId . value } ) ;
} else {
await createAssetSku ( data ) ;
}
ElMessage . success ( isEditSku . value ? '编辑成功' : '添加成功' ) ;
skuFormVisible . value = false ;
getSkuList ( ) ;
} catch ( error ) {
// 错误已由拦截器处理
} finally {
submitLoading . value = false ;
}
} ) ;
} ;
// 关闭弹窗
const onClose = ( ) => {
tableData . value = [ ] ;
total . value = 0 ;
resetQuery ( ) ;
} ;
defineExpose ( {
openDialog ,
} ) ;
< / script >
< style scoped lang = "scss" >
. sku - search {
display : flex ;
align - items : center ;
flex - wrap : wrap ;
}
. spec - values - container {
. spec - item {
display : flex ;
align - items : center ;
margin - bottom : 8 px ;
& : last - child {
margin - bottom : 0 ;
}
}
. spec - label {
min - width : 60 px ;
color : # 606266 ;
font - size : 14 px ;
}
}
. sku - image - uploader {
: deep ( . el - upload ) {
border : 1 px dashed var ( -- el - border - color ) ;
border - radius : 6 px ;
cursor : pointer ;
position : relative ;
overflow : hidden ;
transition : var ( -- el - transition - duration - fast ) ;
width : 100 px ;
height : 100 px ;
display : flex ;
align - items : center ;
justify - content : center ;
& : hover {
border - color : var ( -- el - color - primary ) ;
}
}
}
. sku - image - uploader - icon {
font - size : 28 px ;
color : # 8 c939d ;
}
. sku - image {
width : 100 px ;
height : 100 px ;
object - fit : cover ;
}
< / style >