@@ -44,20 +44,19 @@
:customer = "selectedCustomer"
v-entrance-transition
/ >
< t-form-item v-if = "isCustomerPackageCreate" label="套餐" required -mark >
< t-input
: model -value = " config ? .name | | ' ' "
readonly
borderless
placeholder = "请选择套餐"
@click ="configSelectorVisible = true"
/ >
< / t-form-item >
< t-form-item
v-if = "isCustomerPackageCreate"
label = "套餐"
required -mark
content -align = " right "
arrow
@click ="configSelectorVisible = true"
> 选择套餐 < / t-form-item >
< div v-if = "config && !isSaleable" class="saleable-tip red" > 套餐当前不可购买 , 请检查套餐状态和有效期 < / div >
< section class = "package-purchase-form ">
< div v-if = "config" class="config -summary" >
< t -form -item v-if = "config" label-width="0 ">
< div class = "config-summary" >
< div class = "header" >
< div class = "text-bold" v-text = "config.name || '未命名套餐'" / >
< div class = "name text-bold clip-text " v-text = "config.name || '未命名套餐'" / >
< div class = "price" >
< span class = "label gray" > 标准价 : < / span >
< span class = "value red text-bold" v-text = "formatPackageAmount(config.standardAmount)" / >
@@ -65,80 +64,93 @@
< / div >
< div class = "description" v-text = "config.description" / >
< / div >
< div v-else class = "center-tips" v-text = "'当前套餐未返回,无法完成购买。'" / >
< t-form-item label = "成交价(元)" label -width = " 6rem " required -mark >
< t-input
v -model .trim = " saleAmountText "
type = "number"
align = "right"
borderless
:readonly = "!canPriceOverride"
placeholder = "请输入成交价"
/ >
< / t-form-item >
< t-form-item label -width = " 0 " >
< t-form class = "items" label -width = " 2.5rem " >
< div class = "title" >
< strong > 购买权益明细 < / strong >
< span v-if = "!canPriceOverride" > 当前账号无定制权限 , 仅可等值转换单位 < / span >
< / div >
< t -form -item
v-for = "(item, index) in packagePurchaseItemList"
:key = "`${index}-${item.skuId}-${item.skuUnitId}`"
class = "item"
>
< template # label >
< t-image
v-if = "getItemPhotoURL(item)"
:src = "getItemPhotoURL(item) "
:alt = "getItemName(item)"
fit = "cover"
class = "avatar"
/ >
< div
v-else
class = "avatar fallback"
v-text = "getItemName(item).slice(0, 1)"
/ >
< / template >
< div class = "content" >
< / t-form-item >
< t-form-item label = "成交价(元)" label -width = " 6rem " required -mark >
< t-input
v -model .trim = " saleAmountText "
type = "number"
align = "right"
borderless
:readonly = "!canPriceOverride"
placeholder = "请输入成交价"
/ >
< / t-form-item >
< t-form-item v-if = "config" label="购买权益明细" label-align="top" >
< t -form class = "items" label -width = " 2.5rem " >
< span v-if = "!canPriceOverride" > 当前账号无定制权限 , 仅可等值转换单位 < / span >
< t -form -item
v-for = "(item, index) in packagePurchaseItemList"
:key = "`${index}-${item.skuId}-${item.skuUnitId}-${item.gifted}`"
class = "item"
>
< template # label >
< t-image
v-if = "getItemPhotoURL(item)"
:src = "getItemPhotoURL(item)"
:alt = "getItemName(item)"
fit = "cover"
class = "avatar "
/ >
< div
v-else
class = "avatar fallback"
v-text = "getItemName(item).slice(0, 1)"
/ >
< / template >
< div class = "content" >
< div class = "main" >
< span class = "name" v-text = "getItemLabel(item)" / >
< t-tag v-if = "item.gifted" size="small" theme="success" content="赠" / >
< t-input
class = "input"
:model-value = "item.totalQuantity"
type = "number"
:readonly = "!canPriceOverride "
:readonly = "!canEditItem(item) "
placeholder = "总权益数量"
borderless
align = "right"
@ update : model -value = " updateItemQuantity ( index , $ event ) "
>
< template # suffix -icon >
< div class = "actions" >
< t-button
size = "extra-small"
variant = "outline"
: content = "getItemUnit(item)?.name || '选择单位'"
@click ="openItemUnitPicker(index)"
/ >
< t-button
v-if = "canPriceOverride"
class = "remove"
size = "extra-small"
theme = "danger"
variant = "text"
content = "移除"
@click ="removePurchaseItem(index)"
/ >
< / div >
< / template >
< / t-input >
< div class = "actions" >
< t-button
size = "extra-small"
variant = "outline"
: content = "getItemUnit(item)?.name || '选择单位'"
@click ="openItemUnitPicker(index)"
/ >
< t-button
v-if = "canRemoveItem(item)"
class = "remove"
size = "extra-small"
theme = "danger"
variant = "text"
content = "移除"
@click ="removePurchaseItem(index)"
/ >
< / div >
< / div >
< / t-form-item >
< div v-if = "!packagePurchaseItemList.length" class="center-tips" v-text="'当前套餐暂无权益明细'" / >
< / t-form >
< / t-form-item >
< / section >
< t-input
v-if = "item.gifted"
class = "gift-remark"
: model -value = " item.remark | | ' ' "
align = "right"
borderless
placeholder = "赠送说明(选填)"
@ update : model -value = " updateItemRemark ( index , $ event ) "
/ >
< / div >
< / t-form-item >
< product-selector
v-if = "canGiftCreate"
:model-value = "undefined"
label = ""
placeholder = "添加可售赠送产品"
@ update :model-value = "addGiftProduct"
/ >
< div v-if = "!packagePurchaseItemList.length" class="center-tips" v-text="'当前套餐暂无权益明细'" / >
< / t-form >
< / t-form-item >
< t-form-item label = "购买时间" content -align = " right " >
< div class = "time-actions" >
< t-button
@@ -254,9 +266,14 @@ import { useSettingStore } from "@/store/settingStore";
import type { Customer } from "@/types/Customer" ;
import type { PhotoItem } from "@/types/Photo" ;
import type { GaoPackageConfig , PackageAttachment , PackagePurchaseItem } from "@/types/Package" ;
import type { ProductSku , ProductSkuUnit } from "@/types/Product" ;
import type { Product , ProductSku , ProductSkuUnit } from "@/types/Product" ;
import { getErrorMessage } from "@/utils/error" ;
import { addPackageQuantity , convertPackageQuantity , formatPackageAmount , normalizePackageQuantity } from "@/utils/package" ;
import {
addPackageQuantity ,
convertPackageQuantity ,
formatPackageAmount ,
normalizePackageQuantity
} from "@/utils/package" ;
import { Message } from "@/utils/message" ;
import { permissionCodes } from "@/utils/permission" ;
import { uploadPhotoList } from "@/utils/photo" ;
@@ -280,6 +297,7 @@ const config = ref<GaoPackageConfig>();
const configList = ref < GaoPackageConfig [ ] > ( [ ] ) ;
const configSelectorVisible = ref ( false ) ;
const skuMap = reactive < Record < string , ProductSku > > ( { } ) ;
const giftProductMap = reactive < Record < string , Product > > ( { } ) ;
const unitPickerVisible = ref ( false ) ;
const unitPickerIndex = ref ( - 1 ) ;
const unitPickerDraft = ref < Array < string | number > > ( [ ] ) ;
@@ -417,6 +435,7 @@ function buildAttachments(): PackageAttachment[] {
const {
isSaleable ,
canPriceOverride ,
canGiftCreate ,
saleAmountText ,
packagePurchaseItemList ,
applyConfig ,
@@ -439,16 +458,19 @@ const unitPickerOptions = computed(() => {
function getItemLabel ( item : PackagePurchaseItem ) : string {
const configItem = config . value ? . itemList ? . find ( configItem => configItem . skuId === item . skuId ) ;
if ( ! configItem ) {
return "产品权益" ;
}
const productName = configItem . productNameSnapshot || "未命名产品" ;
const skuText = getProductSkuSnapshotText ( productName , configItem . skuNameSnapshot , configItem . specSummarySnapshot ) ;
const product = item . gifted ? giftProductMap [ item . skuId ] : undefined ;
const productName = item . gifted
? product ? . name || item . productNameSnapshot || "未命名产品"
: configItem ? . productNameSnapshot || "未命名产品" ;
const skuName = item . gifted ? item . skuNameSnapshot || getItemSku ( item ) ? . name : configItem ? . skuNameSnapshot ;
const specSummary = item . gifted ? item . specSummarySnapshot || getItemSku ( item ) ? . specSummary : configItem ? . specSummarySnapshot ;
const skuText = getProductSkuSnapshotText ( productName , skuName , specSummary ) ;
return skuText ? ` ${ productName } · ${ skuText } ` : productName ;
}
function getItemName ( item : PackagePurchaseItem ) : string {
return config . value ? . itemList ? . find ( configItem => configItem . skuId === item . skuId ) ? . productNameSnapshot || "未命名产品" ;
return item . gifted ? giftProductMap [ item . skuId ] ? . name || item . productNameSnapshot || "未命名产品"
: config . value ? . itemList ? . find ( configItem => configItem . skuId === item . skuId ) ? . productNameSnapshot || "未命名产品" ;
}
function getItemPhotoURL ( item : PackagePurchaseItem ) : string {
@@ -464,6 +486,9 @@ function getItemUnit(item: PackagePurchaseItem): ProductSkuUnit | undefined {
}
function getSelectableUnitList ( item : PackagePurchaseItem ) : ProductSkuUnit [ ] {
if ( item . gifted ) {
return ( getItemSku ( item ) ? . unitList || [ ] ) . filter ( unit => unit . allowSale !== false && ! ! unit . id ) ;
}
const maxBaseQuantityPerUnit = Math . max ( ... ( config . value ? . itemList || [ ] )
. filter ( configItem => configItem . skuId === item . skuId )
. map ( configItem => Number ( configItem . baseQuantityPerUnitSnapshot || 0 ) ) ) ;
@@ -473,7 +498,7 @@ function getSelectableUnitList(item: PackagePurchaseItem): ProductSkuUnit[] {
function openItemUnitPicker ( index : number ) : void {
const item = packagePurchaseItemList . value [ index ] ;
if ( ! item || ! getSelectableUnitList ( item ) . length ) {
if ( ! item || ! canEditItem ( item ) || ! getSelectableUnitList ( item ) . length ) {
Message . warning ( "当前 SKU 没有可用于套餐权益的销售单位" ) ;
return ;
}
@@ -488,7 +513,7 @@ function confirmItemUnitPicker(value: Array<string | number>): void {
const skuUnitId = String ( value [ 0 ] || "" ) ;
const currentUnit = item && getItemUnit ( item ) ;
const targetUnit = item && getSelectableUnitList ( item ) . find ( unit => unit . id === skuUnitId ) ;
if ( ! item || ! currentUnit || ! targetUnit || ! skuUnitId || skuUnitId === item . skuUnitId ) {
if ( ! item || ! canEditItem ( item ) || ! currentUnit || ! targetUnit || ! skuUnitId || skuUnitId === item . skuUnitId ) {
unitPickerVisible . value = false ;
return ;
}
@@ -498,7 +523,7 @@ function confirmItemUnitPicker(value: Array<string | number>): void {
return ;
}
const duplicateIndex = packagePurchaseItemList . value . findIndex ( ( candidate , candidateIndex ) => candidateIndex !== index
&& candidate . skuId === item . skuId && candidate . skuUnitId === skuUnitId ) ;
&& candidate . skuId === item . skuId && candidate . skuUnitId === skuUnitId && Boolean ( candidate . gifted ) === Boolean ( item . gifted ) ) ;
if ( duplicateIndex < 0 ) {
packagePurchaseItemList . value [ index ] = { ... item , skuUnitId , totalQuantity : convertedQuantity } ;
} else {
@@ -522,7 +547,63 @@ function confirmItemUnitPicker(value: Array<string | number>): void {
unitPickerVisible . value = false ;
}
async function addGiftProduct ( value ? : { skuId : string ; skuUnitId : string } ) : Promise < void > {
if ( ! value || ! canGiftCreate . value ) {
return ;
}
try {
const sku = skuMap [ value . skuId ] || await ProductAPI . skuDetail ( value . skuId ) ;
skuMap [ value . skuId ] = sku ;
const productId = sku . productId || "" ;
const product = productId ? giftProductMap [ value . skuId ] || await ProductAPI . detail ( productId ) : undefined ;
if ( ! product ) {
Message . warning ( "赠送产品信息加载失败,请重新选择" ) ;
return ;
}
giftProductMap [ value . skuId ] = product ;
const existingIndex = packagePurchaseItemList . value . findIndex ( item => item . gifted
&& item . skuId === value . skuId && item . skuUnitId === value . skuUnitId ) ;
if ( 0 <= existingIndex ) {
const existingItem = packagePurchaseItemList . value [ existingIndex ] ;
const totalQuantity = addPackageQuantity ( existingItem . totalQuantity , "1" ) ;
if ( ! totalQuantity ) {
Message . warning ( "赠送数量合并后不能精确保留三位小数" ) ;
return ;
}
packagePurchaseItemList . value [ existingIndex ] = { ... existingItem , totalQuantity } ;
} else {
const maxSort = Math . max ( - 1 , ... packagePurchaseItemList . value . map ( item => item . sort ? ? - 1 ) ) ;
packagePurchaseItemList . value . push ( {
skuId : value . skuId ,
skuUnitId : value . skuUnitId ,
totalQuantity : "1" ,
gifted : true ,
sort : maxSort + 1 ,
remark : null ,
productNameSnapshot : product . name ,
skuNameSnapshot : sku . name ,
specSummarySnapshot : sku . specSummary
} ) ;
}
resetRequestNo ( ) ;
} catch ( error ) {
Message . error ( getErrorMessage ( error , "赠送产品加载失败" ) ) ;
}
}
function canEditItem ( item : PackagePurchaseItem ) : boolean {
return Boolean ( item . gifted ) ? canGiftCreate . value : canPriceOverride . value ;
}
function canRemoveItem ( item : PackagePurchaseItem ) : boolean {
return canEditItem ( item ) ;
}
function updateItemQuantity ( index : number , value : string | number ) : void {
const item = packagePurchaseItemList . value [ index ] ;
if ( ! item || ! canEditItem ( item ) ) {
return ;
}
packagePurchaseItemList . value = packagePurchaseItemList . value . map ( ( item , itemIndex ) => itemIndex === index ? {
... item ,
totalQuantity : String ( value )
@@ -530,8 +611,18 @@ function updateItemQuantity(index: number, value: string | number): void {
resetRequestNo ( ) ;
}
function updateItemRemark ( index : number , value : string | number ) : void {
const item = packagePurchaseItemList . value [ index ] ;
if ( ! item ? . gifted || ! canGiftCreate . value ) {
return ;
}
packagePurchaseItemList . value [ index ] = { ... item , remark : String ( value ) || null } ;
resetRequestNo ( ) ;
}
function removePurchaseItem ( index : number ) : void {
if ( ! canPriceOverride . value ) {
const item = packagePurchaseItemList . value [ index ] ;
if ( ! item || ! canRemoveItem ( item ) ) {
return ;
}
packagePurchaseItemList . value = packagePurchaseItemList . value . filter ( ( _ , itemIndex ) => itemIndex !== index ) ;
@@ -662,84 +753,86 @@ watch(config, value => {
margin : 1 rem ;
}
. saleable - tip {
padding : 0 1 rem .5 rem ;
font - size : .8125 rem ;
line - height : 1.5 ;
}
. time - actions {
gap : .5 rem ;
. config - summary {
gap : .25 rem ;
width : 100 % ;
padding : .5 rem .75 rem ;
display : flex ;
align - items : center ;
justify - content : flex - end ;
}
background : var ( -- td - error - color - 1 ) ;
border - radius : .75 rem ;
flex - direction : column ;
. package - purchase - form {
gap : .75 rem ;
display : grid ;
margin - top : 1 rem ;
. config - summary {
gap : .25 rem ;
margin : 0 1 rem ;
padding : .5 rem .75 rem ;
. header {
display : flex ;
background : var ( -- td - error - color - 1 ) ;
border - radius : .75 rem ;
flex - direction : column ;
justify - content : space - between ;
. header {
. name {
flex : 1 ;
}
. price {
flex : none ;
display : flex ;
justify - content : space - between ;
align - items : baseline ;
. price {
display : flex ;
align - items : baseline ;
. label {
font - size : .75 rem ;
}
. label {
font - size : .75 rem ;
}
. value {
font - size : 1.75 rem ;
line - height : 1 ;
}
. value {
font - size : 1.75 rem ;
line - height : 1 ;
}
}
}
. items {
gap : .5 rem ;
margin : 0 ;
display : grid ;
. description {
width : 100 % ;
font - size : .75 rem ;
min - width : 0 ;
max - width : 100 % ;
word - break : break - word ;
white - space : normal ;
overflow - wrap : anywhere ;
}
}
. title {
font - size : .8125 rem ;
margin - bottom : .5 rem ;
. items {
gap : .5 rem ;
flex : 1 ;
margin : 0 ;
display : grid ;
. title {
font - size : .8125 rem ;
margin - bottom : .5 rem ;
}
. item {
padding : 10 px ;
box - shadow : .25 rem .25 rem 1 rem rgba ( 0 , 0 , 0 , .08 ) ;
border - radius : 1 rem ;
. avatar {
width : 2 rem ;
height : 2 rem ;
flex : none ;
border - radius : .5 rem ;
}
. item {
padding : 10 px ;
box - shadow : .25 rem .25 rem 1 rem rgba ( 0 , 0 , 0 , .08 ) ;
border - radius : 1 rem ;
& : : after {
display : none ;
}
. avatar {
width : 2 rem ;
height : 2 rem ;
flex : none ;
border - radius : .5 rem ;
}
. content {
gap : .25 rem ;
display : flex ;
flex - direction : column ;
& : : after {
display : none ;
}
. content {
. main {
gap : .25 rem ;
display : flex ;
height : 2 rem ;
align - items : center ;
justify - content : space - between ;
. name {
min - width : 0 ;
@@ -749,13 +842,19 @@ watch(config, value => {
}
. input {
min - width : 0 ;
flex : 1 ;
min - width : 0 ;
margin - top : .2 rem ;
}
. actions {
gap : .25 rem ;
display : flex ;
}
. actions {
gap : .25 rem ;
margin : .1 rem 0 0 .75 rem ;
display : flex ;
}
. gift - remark {
padding : 0 ;
}
}
}
@@ -763,6 +862,13 @@ watch(config, value => {
}
}
. time - actions {
gap : .5 rem ;
display : flex ;
align - items : center ;
justify - content : flex - end ;
}
. file - input {
display : none ;
}