@@ -1,405 +0,0 @@
< template >
< div v-can = "permissionCodes.customerPackage.create" class="customer-package-purchase" >
< t -form class = "form" label -align = " left " label -width = " 6rem " v-entrance-transition >
< t -form -item v-if = "isGlobalManager" label="所属门店" arrow @click="openStorePicker" >
< span class = "picker-text" v-text = "selectedStoreName" / >
< / t-form-item >
< t-form-item label = "客户" required -mark arrow @click ="customerSelectorVisible = true" >
< span class = "picker-text" v-text = "selectedCustomer?.name || selectedCustomer?.code || '请选择客户'" / >
< / t-form-item >
< customer-profile-card v-if = "selectedCustomer" class="customer-profile" :customer="selectedCustomer" / >
< t-form-item label = "购买事件" required -mark arrow @click ="openEventPicker" >
< span class = "picker-text" v-text = "selectedEvent?.name || '请选择 PACKAGE_PURCHASE 事件'" / >
< / t-form-item >
< div v-if = "selectedConfig" class="config-summary" >
< strong v-text = "selectedConfig.name || '未命名套餐'" / >
< span v-text = "`标准价:${formatPackageAmount(selectedConfig.standardAmount)}| ${getPackageConfigStatusText(selectedConfig.status)}`" / >
< / div >
< div v-if = "!eventList.length" class="option-tip" >
< div v-if = "!eventList.length" > 当前门店没有可用的 PACKAGE_PURCHASE 购买事件 , 请先新增并启用购买事件 。 < / div >
< / div >
< div v-if = "selectedEvent && !selectedConfig" class="option-tip" > 当前购买事件未返回绑定套餐 , 无法购买 , 请联系管理员检查事件配置 。 < / div >
< div v-if = "selectedConfig && !isSelectedConfigSaleable" class="option-tip" > 事件绑定的套餐当前不可购买 , 请检查套餐状态和有效期 。 < / div >
< t -form -item label = "成交价(元)" required -mark >
< t-input v -model .trim = " saleAmountText " type = "number" align = "right" borderless :readonly = "!canPriceOverride" placeholder = "请输入成交价" / >
< / t-form-item >
< t-form-item label = "购买时间" arrow @click ="openPurchaseTime" >
< span class = "picker-text" v-text = "purchasedAtText || '使用当前时间'" / >
< / t-form-item >
< t-form-item label = "备注" >
< t-textarea v -model .trim = " remark " borderless autosize maxlength = "500" placeholder = "购买备注" / >
< / t-form-item >
< / t-form >
< section class = "items" v-entrance-transition = "70" >
< div class = "section-title" > < strong > 购买权益明细 < / strong > < span v-if = "!canPriceOverride" > 当前账号无定制权限 , 明细不可修改 < / span > < / div >
< div v-for = "(item, index) in purchaseItems" :key="item.clientKey" class="item-card" >
< strong v-text = "`权益 ${index + 1}`" / >
< t-input v -model .trim = " item.productName " borderless readonly placeholder = "产品名称" / >
< t-input v -model .trim = " item.unit " borderless readonly placeholder = "单位" / >
< t-input v -model .trim = " item.totalQuantity " type = "number" borderless :readonly = "!canPriceOverride" placeholder = "总权益数量" / >
< span v-if = "item.remark" class="item-remark" v-text="`备注:${item.remark}`" / >
< / div >
< div v-if = "!purchaseItems.length" class="empty" v-text="'选择购买事件后加载绑定套餐权益明细'" / >
< / section >
< package-photo-field v-model = "photoItems" :required="selectedEvent?.requirePhoto === true" title="购买照片" / >
< customer-selector v-model = "selectedCustomerId" v-model:visible="customerSelectorVisible" :store-id="formStoreId" @change="handleCustomerChange" / >
< t-popup v-model = "storePickerVisible" placement="bottom" >
< t -picker v-model = "storeDraft" :columns="storePickerColumns" title="选择门店" @cancel="storePickerVisible = false" @confirm="confirmStore" / >
< / t-popup >
< t-popup v-model = "eventPickerVisible" placement="bottom" >
< t -picker v-model = "eventDraft" :columns="eventPickerColumns" title="选择购买事件" @cancel="eventPickerVisible = false" @confirm="confirmEvent" / >
< / t-popup >
< t-popup v-model = "purchasePickerVisible" placement="bottom" >
< t -date -time -picker v-model = "purchaseDraft" format="YYYY-MM-DD HH:mm" :mode="['date', 'minute']" title="购买时间" @cancel="purchasePickerVisible = false" @confirm="confirmPurchaseTime" / >
< / t-popup >
< / div >
< / template >
< script setup lang = "ts" >
import { Time } from "timi-web" ;
import { useUserStore } from "timi-web-api" ;
import CustomerAPI from "@/api/CustomerAPI" ;
import EventAPI from "@/api/EventAPI" ;
import PackageAPI from "@/api/PackageAPI" ;
import CustomerProfileCard from "@/components/CustomerProfileCard.vue" ;
import CustomerSelector from "@/components/CustomerSelector.vue" ;
import PackagePhotoField from "@/components/PackagePhotoField.vue" ;
import { useCurrentStoreStore } from "@/store/currentStoreStore" ;
import { useNavBarStore } from "@/store/navBarStore" ;
import type { Customer } from "@/types/Customer" ;
import type { EventItem } from "@/types/EventItem" ;
import type { GaoPackageConfig , PackageAttachment , PackagePurchaseItem } from "@/types/Package" ;
import type { PhotoItem } from "@/types/Photo" ;
import { createPackageRequestNo , formatPackageAmount , formatPackageAmountInput , getPackageConfigStatusText , normalizePackageQuantity , parsePackageAmount } from "@/utils/package" ;
import { getErrorMessage } from "@/utils/error" ;
import { Message } from "@/utils/message" ;
import { gaoRoleCodes , permissionCodes } from "@/utils/permission" ;
type PickerValue = string | number ;
type PurchaseFormItem = PackagePurchaseItem & { clientKey : string } ;
defineOptions ( { name : "CustomerPackagePurchase" } ) ;
const router = useRouter ( ) ;
const route = useRoute ( ) ;
const userStore = useUserStore ( ) ;
const navBarStore = useNavBarStore ( ) ;
const currentStoreStore = useCurrentStoreStore ( ) ;
const isGlobalManager = computed ( ( ) => userStore . hasRoles ( gaoRoleCodes . globalManager ) ) ;
const canPriceOverride = computed ( ( ) => userStore . hasPermissions ( permissionCodes . customerPackage . priceOverride ) ) ;
const formStoreId = ref ( "" ) ;
const selectedCustomerId = ref ( "" ) ;
const selectedCustomer = ref < Customer > ( ) ;
const selectedEventId = ref ( "" ) ;
const eventList = ref < EventItem [ ] > ( [ ] ) ;
const purchaseItems = ref < PurchaseFormItem [ ] > ( [ ] ) ;
const selectedEvent = computed ( ( ) => eventList . value . find ( item => item . id === selectedEventId . value ) ) ;
const selectedConfig = computed < GaoPackageConfig | undefined > ( ( ) => selectedEvent . value ? . packageConfig ) ;
const isSelectedConfigSaleable = computed ( ( ) => {
const config = selectedConfig . value ;
if ( ! config || config . status !== "ACTIVE" ) {
return false ;
}
const now = Date . now ( ) ;
const beginAt = config . beginAt ;
const endAt = config . endAt ;
return ( beginAt === undefined || beginAt === null || beginAt <= now )
&& ( endAt === undefined || endAt === null || now <= endAt ) ;
} ) ;
const saleAmountText = ref ( "" ) ;
const purchasedAtText = ref ( "" ) ;
const purchaseDraft = ref ( "" ) ;
const remark = ref ( "" ) ;
const photoItems = ref < PhotoItem [ ] > ( [ ] ) ;
const isSaving = ref ( false ) ;
const customerSelectorVisible = ref ( false ) ;
const storePickerVisible = ref ( false ) ;
const eventPickerVisible = ref ( false ) ;
const purchasePickerVisible = ref ( false ) ;
const storeDraft = ref < PickerValue [ ] > ( [ ] ) ;
const eventDraft = ref < PickerValue [ ] > ( [ ] ) ;
const storePickerColumns = computed ( ( ) => [ [ ... currentStoreStore . storeList ] . map ( item => ( { label : item . name || item . code || item . id || "未知门店" , value : item . id || "" } ) ) ] ) ;
const eventPickerColumns = computed ( ( ) => [ [ ... eventList . value ] . map ( item => ( { label : item . name || "未命名事件" , value : item . id || "" } ) ) ] ) ;
const selectedStoreName = computed ( ( ) => currentStoreStore . storeList . find ( item => item . id === formStoreId . value ) ? . name || formStoreId . value || "请选择门店" ) ;
async function loadOptions ( ) : Promise < void > {
eventList . value = formStoreId . value
? ( await EventAPI . listValid ( formStoreId . value ) ) . filter ( item => item . bizType === "PACKAGE_PURCHASE" && ! ! item . packageConfigId )
: [ ] ;
if ( ! eventList . value . some ( item => item . id === selectedEventId . value ) ) {
selectedEventId . value = "" ;
resetPurchaseConfig ( ) ;
}
}
async function handleCustomerChange ( id : string ) : Promise < void > {
selectedCustomerId . value = id ;
selectedCustomer . value = id ? await CustomerAPI . detail ( id ) : undefined ;
}
function openStorePicker ( ) : void {
storeDraft . value = [ formStoreId . value ] ;
storePickerVisible . value = true ;
}
function openEventPicker ( ) : void {
if ( ! eventList . value . length ) {
Message . warning ( "当前门店没有 PACKAGE_PURCHASE 类型购买事件,请先启用购买事件" ) ;
return ;
}
eventDraft . value = [ selectedEventId . value ] ;
eventPickerVisible . value = true ;
}
async function confirmStore ( value : Array < string | number > ) : Promise < void > {
const storeId = String ( value [ 0 ] || "" ) ;
if ( storeId && storeId !== formStoreId . value ) {
formStoreId . value = storeId ;
selectedCustomerId . value = "" ;
selectedCustomer . value = undefined ;
await loadOptions ( ) ;
}
storePickerVisible . value = false ;
}
function resetPurchaseConfig ( ) : void {
saleAmountText . value = "" ;
purchaseItems . value = [ ] ;
}
function applySelectedEvent ( ) : void {
const config = selectedConfig . value ;
if ( ! config ) {
resetPurchaseConfig ( ) ;
return ;
}
saleAmountText . value = formatPackageAmountInput ( config . standardAmount ) ;
purchaseItems . value = ( config . itemList || [ ] ) . map ( ( item , index ) => ( {
clientKey : window . crypto . randomUUID ( ) ,
productName : item . productName ,
unit : item . unit ,
totalQuantity : normalizePackageQuantity ( item . defaultQuantity ) || "" ,
sort : item . sort ? ? index ,
remark : item . remark ? ? null
} ) ) ;
}
function confirmEvent ( value : Array < string | number > ) : void {
selectedEventId . value = String ( value [ 0 ] || "" ) ;
applySelectedEvent ( ) ;
eventPickerVisible . value = false ;
}
function openPurchaseTime ( ) : void {
purchaseDraft . value = purchasedAtText . value || Time . formatDateTime ( new Date ( ) ) ;
purchasePickerVisible . value = true ;
}
function confirmPurchaseTime ( value : string | number ) : void {
purchasedAtText . value = String ( value ) ;
purchasePickerVisible . value = false ;
}
function resolveTimestamp ( value : string ) : number | undefined {
if ( ! value ) {
return undefined ;
}
const timestamp = new Date ( value . replace ( " " , "T" ) ) . getTime ( ) ;
return Number . isFinite ( timestamp ) ? timestamp : undefined ;
}
function validateForm ( ) : boolean {
if ( ! selectedCustomerId . value ) {
Message . warning ( "请选择客户" ) ;
return false ;
}
if ( ! selectedEventId . value || ! selectedEvent . value ) {
Message . warning ( "请选择 PACKAGE_PURCHASE 类型购买事件" ) ;
return false ;
}
if ( ! selectedConfig . value ) {
Message . warning ( "当前购买事件未绑定有效套餐配置" ) ;
return false ;
}
if ( ! isSelectedConfigSaleable . value ) {
Message . warning ( "事件绑定的套餐当前不可购买,请检查套餐状态和有效期" ) ;
return false ;
}
if ( parsePackageAmount ( saleAmountText . value ) === undefined ) {
Message . warning ( "成交价必须是合法的元金额,最多两位小数" ) ;
return false ;
}
if ( ! purchaseItems . value . length ) {
Message . warning ( "购买权益至少需要一项" ) ;
return false ;
}
for ( const item of purchaseItems . value ) {
if ( ! item . productName . trim ( ) || ! item . unit . trim ( ) || ! normalizePackageQuantity ( item . totalQuantity ) || normalizePackageQuantity ( item . totalQuantity ) === "0" ) {
Message . warning ( "产品名称、单位和正数量不能为空" ) ;
return false ;
}
}
if ( selectedEvent . value . requirePhoto && ! photoItems . value . some ( item => item . tempFileId ) ) {
Message . warning ( "当前购买事件要求上传照片" ) ;
return false ;
}
return true ;
}
function buildAttachments ( ) : PackageAttachment [ ] {
return photoItems . value . flatMap ( item => item . tempFileId ? [ { tempFileId : item . tempFileId , attachType : "PHOTO" , name : item . attachment ? . name } ] : [ ] ) ;
}
async function save ( ) : Promise < void > {
if ( isSaving . value || ! validateForm ( ) ) {
return ;
}
isSaving . value = true ;
try {
const result = await PackageAPI . purchase ( {
requestNo : createPackageRequestNo ( ) ,
eventId : selectedEventId . value ,
customerId : selectedCustomerId . value ,
saleAmount : parsePackageAmount ( saleAmountText . value ) ! ,
purchasedAt : resolveTimestamp ( purchasedAtText . value ) ,
itemList : purchaseItems . value . map ( ( item , index ) => ( {
productName : item . productName . trim ( ) ,
unit : item . unit . trim ( ) ,
totalQuantity : normalizePackageQuantity ( item . totalQuantity ) ! ,
sort : item . sort ? ? index ,
remark : item . remark ? ? null
} ) ) ,
attachmentList : buildAttachments ( ) ,
remark : remark . value . trim ( ) || null
} ) ;
Message . success ( "套餐购买已提交" ) ;
if ( result . id ) {
await router . replace ( ` /management/customer-packages/ ${ result . id } ` ) ;
return ;
}
await router . replace ( "/management/customer-packages" ) ;
} catch ( error ) {
Message . error ( getErrorMessage ( error , "套餐购买失败,请刷新后重试" ) ) ;
} finally {
isSaving . value = false ;
}
}
async function loadData ( ) : Promise < void > {
if ( isGlobalManager . value ) {
await currentStoreStore . loadStoreList ( ) ;
} else {
await currentStoreStore . loadLoginStore ( ) ;
}
formStoreId . value = currentStoreStore . currentStoreId ;
if ( ! formStoreId . value ) {
const store = await currentStoreStore . loadLoginStore ( ) ;
formStoreId . value = store ? . id || "" ;
}
await loadOptions ( ) ;
const queryEventId = String ( route . query . eventId || "" ) ;
if ( queryEventId && eventList . value . some ( item => item . id === queryEventId ) ) {
selectedEventId . value = queryEventId ;
applySelectedEvent ( ) ;
}
const queryCustomerId = String ( route . query . customerId || "" ) ;
if ( queryCustomerId ) {
await handleCustomerChange ( queryCustomerId ) ;
}
}
onMounted ( async ( ) => {
if ( userStore . hasPermissions ( permissionCodes . customerPackage . create ) ) {
navBarStore . setRightText ( "提交购买" , "var(--td-brand-color)" , save ) ;
}
try {
await loadData ( ) ;
} catch ( error ) {
Message . error ( getErrorMessage ( error , "购买页面数据加载失败" ) ) ;
}
} ) ;
onUnmounted ( ( ) => navBarStore . clearRight ( ) ) ;
< / script >
< style scoped lang = "less" >
. customer - package - purchase {
gap : 1 rem ;
padding : calc ( var ( -- app - nav - height ) + 1 rem ) 0 2 rem ;
display : flex ;
flex - direction : column ;
. form {
border - radius : .75 rem ;
}
. picker - text {
width : 100 % ;
text - align : right ;
}
. customer - profile {
margin : 0 1 rem ;
}
. items ,
. package - photo - field {
padding : 0 1 rem ;
}
. section - title {
color : var ( -- td - text - color - secondary ) ;
margin - bottom : .5 rem ;
font - size : .8125 rem ;
}
. section - title span {
margin - left : .5 rem ;
}
. item - card {
gap : .5 rem ;
padding : .75 rem ;
margin - bottom : .75 rem ;
display : grid ;
background : var ( -- td - bg - color - container ) ;
box - shadow : var ( -- td - shadow - 1 ) ;
border - radius : .75 rem ;
}
. config - summary {
gap : .25 rem ;
padding : .75 rem 1 rem ;
display : flex ;
color : var ( -- td - text - color - secondary ) ;
font - size : .8125 rem ;
flex - direction : column ;
background : var ( -- td - bg - color - secondarycontainer ) ;
strong {
color : var ( -- td - text - color - primary ) ;
font - size : .9375 rem ;
}
}
. item - remark {
color : var ( -- td - text - color - secondary ) ;
font - size : .8125 rem ;
}
. empty {
color : var ( -- td - text - color - secondary ) ;
padding : 1 rem ;
text - align : center ;
}
. option - tip {
padding : .75 rem 1 rem ;
color : var ( -- td - text - color - secondary ) ;
font - size : .8125 rem ;
line - height : 1.5 ;
background : var ( -- td - bg - color - secondarycontainer ) ;
}
}
< / style >