|
@@ -38,7 +38,13 @@
|
|
|
<div v-for="product in currentProductList" :key="product.id" class="product-card"
|
|
<div v-for="product in currentProductList" :key="product.id" class="product-card"
|
|
|
:class="{ active: selectedProduct === product.id }" @click="handleProductSelect(product.id)">
|
|
:class="{ active: selectedProduct === product.id }" @click="handleProductSelect(product.id)">
|
|
|
<div class="product-image">
|
|
<div class="product-image">
|
|
|
- <img :src="getProductImage(product.id)" :alt="product.name">
|
|
|
|
|
|
|
+ <img v-if="product.productImage" :src="product.productImage" :alt="product.name"
|
|
|
|
|
+ @error="handleImageError">
|
|
|
|
|
+ <div v-else class="product-image-placeholder">
|
|
|
|
|
+ <el-icon>
|
|
|
|
|
+ <Goods />
|
|
|
|
|
+ </el-icon>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
<div class="product-content">
|
|
<div class="product-content">
|
|
|
<div class="product-name">{{ product.name }}</div>
|
|
<div class="product-name">{{ product.name }}</div>
|
|
@@ -50,17 +56,32 @@
|
|
|
</el-icon>
|
|
</el-icon>
|
|
|
<span>{{ product.cardTypeText }}</span>
|
|
<span>{{ product.cardTypeText }}</span>
|
|
|
</span>
|
|
</span>
|
|
|
- <span class="seats-info">
|
|
|
|
|
|
|
+ <!-- 日期卡(cardType >= 3)显示席位信息 -->
|
|
|
|
|
+ <span v-if="product.cardType >= 3" class="seats-info">
|
|
|
<el-icon>
|
|
<el-icon>
|
|
|
<UserFilled />
|
|
<UserFilled />
|
|
|
</el-icon>
|
|
</el-icon>
|
|
|
<span v-if="operationMode === 'assign'">
|
|
<span v-if="operationMode === 'assign'">
|
|
|
- 可用: {{ product.seats ?? 0 }}/{{ product.totalSeats ?? 0 }}
|
|
|
|
|
|
|
+ 可用: {{ product.seats ?? 0 }}
|
|
|
</span>
|
|
</span>
|
|
|
<span v-else>
|
|
<span v-else>
|
|
|
- 已分配: {{ product.assignedSeats || 0 }}/{{ product.totalSeats ?? 0 }}
|
|
|
|
|
|
|
+ 已分配: {{ product.assignedSeats || 0 }}
|
|
|
</span>
|
|
</span>
|
|
|
</span>
|
|
</span>
|
|
|
|
|
+ <!-- 次卡(cardType === 1)显示剩余次数 -->
|
|
|
|
|
+ <span v-else-if="product.cardType === 1 && product.remainingTimes !== undefined" class="times-info">
|
|
|
|
|
+ <el-icon>
|
|
|
|
|
+ <Ticket />
|
|
|
|
|
+ </el-icon>
|
|
|
|
|
+ <span>剩余: {{ product.remainingTimes }} 次</span>
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <!-- 积分卡(cardType === 2)显示剩余积分 -->
|
|
|
|
|
+ <span v-else-if="product.cardType === 2 && product.remainingPoints !== undefined" class="points-info">
|
|
|
|
|
+ <el-icon>
|
|
|
|
|
+ <Ticket />
|
|
|
|
|
+ </el-icon>
|
|
|
|
|
+ <span>剩余: {{ product.remainingPoints }} 积分</span>
|
|
|
|
|
+ </span>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
<div v-if="selectedProduct === product.id" class="product-check">
|
|
<div v-if="selectedProduct === product.id" class="product-check">
|
|
@@ -97,20 +118,33 @@
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
<div class="member-list-wrapper">
|
|
<div class="member-list-wrapper">
|
|
|
- <div v-if="selectedProduct && selectedMembers.length > 0" class="selected-badge">
|
|
|
|
|
- <el-icon>
|
|
|
|
|
- <Check />
|
|
|
|
|
- </el-icon>
|
|
|
|
|
- <span>已选择 <strong>{{ selectedMembers.length }}</strong> 位成员</span>
|
|
|
|
|
|
|
+ <div v-if="selectedProduct" class="info-badges">
|
|
|
|
|
+ <div v-if="selectedMembers.length > 0" class="selected-badge">
|
|
|
|
|
+ <el-icon>
|
|
|
|
|
+ <Check />
|
|
|
|
|
+ </el-icon>
|
|
|
|
|
+ <span>已选择 <strong>{{ selectedMembers.length }}</strong> 位成员</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <!-- 只有日期卡才显示可用席位信息 -->
|
|
|
|
|
+ <div v-if="operationMode === 'assign' && isDateCard && currentAvailableSeats > 0" class="seats-badge">
|
|
|
|
|
+ <el-icon>
|
|
|
|
|
+ <UserFilled />
|
|
|
|
|
+ </el-icon>
|
|
|
|
|
+ <span>可用席位: <strong>{{ currentAvailableSeats }}</strong> 个</span>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
<div class="member-list-scroll">
|
|
<div class="member-list-scroll">
|
|
|
<template v-if="selectedProduct">
|
|
<template v-if="selectedProduct">
|
|
|
- <el-checkbox-group v-model="selectedMembers" @change="handleMemberChange">
|
|
|
|
|
- <div v-for="member in currentMemberList" :key="member.id" class="member-card"
|
|
|
|
|
- :class="{ selected: selectedMembers.includes(member.id) }">
|
|
|
|
|
- <el-checkbox :label="member.id" class="member-checkbox">
|
|
|
|
|
|
|
+ <el-checkbox-group v-model="selectedMembers">
|
|
|
|
|
+ <div v-for="member in currentMemberList" :key="member.id" class="member-card" :class="{
|
|
|
|
|
+ selected: selectedMembers.includes(member.id),
|
|
|
|
|
+ disabled: isMemberDisabled(member.id)
|
|
|
|
|
+ }">
|
|
|
|
|
+ <el-checkbox :label="member.id" :disabled="isMemberDisabled(member.id)" class="member-checkbox">
|
|
|
<div class="member-avatar">
|
|
<div class="member-avatar">
|
|
|
- <el-icon>
|
|
|
|
|
|
|
+ <img v-if="member.avatar" :src="member.avatar" :alt="member.name"
|
|
|
|
|
+ @error="handleMemberAvatarError">
|
|
|
|
|
+ <el-icon v-else>
|
|
|
<UserFilled />
|
|
<UserFilled />
|
|
|
</el-icon>
|
|
</el-icon>
|
|
|
</div>
|
|
</div>
|
|
@@ -123,6 +157,9 @@
|
|
|
<Check />
|
|
<Check />
|
|
|
</el-icon>
|
|
</el-icon>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <div v-if="isMemberDisabled(member.id)" class="member-disabled-badge">
|
|
|
|
|
+ {{ isDateCard ? '席位已满' : '不可选择' }}
|
|
|
|
|
+ </div>
|
|
|
</el-checkbox>
|
|
</el-checkbox>
|
|
|
</div>
|
|
</div>
|
|
|
</el-checkbox-group>
|
|
</el-checkbox-group>
|
|
@@ -203,24 +240,47 @@ const currentProductList = computed(() =>
|
|
|
// 当前显示的人员列表
|
|
// 当前显示的人员列表
|
|
|
const currentMemberList = computed(() => {
|
|
const currentMemberList = computed(() => {
|
|
|
if (!selectedProduct.value) return []
|
|
if (!selectedProduct.value) return []
|
|
|
- if (operationMode.value === 'assign') {
|
|
|
|
|
- // 分配模式:显示未分配的成员列表
|
|
|
|
|
- return props.memberList
|
|
|
|
|
- }
|
|
|
|
|
- // 取消分配模式:显示该套餐已分配的成员列表
|
|
|
|
|
|
|
+ // 分配模式和取消分配模式都从 productMembersMap 中获取对应的成员列表
|
|
|
return productMembersMap.value[selectedProduct.value] || []
|
|
return productMembersMap.value[selectedProduct.value] || []
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
-const canSubmit = computed(() => {
|
|
|
|
|
|
|
+// 当前选中的产品信息
|
|
|
|
|
+const currentSelectedProduct = computed(() => {
|
|
|
|
|
+ if (!selectedProduct.value) return null
|
|
|
if (operationMode.value === 'assign') {
|
|
if (operationMode.value === 'assign') {
|
|
|
- // 分配模式:需要选择人员且选择产品
|
|
|
|
|
- return selectedMembers.value.length > 0 && !!selectedProduct.value
|
|
|
|
|
|
|
+ return availableProducts.value.find(p => p.id === selectedProduct.value)
|
|
|
} else {
|
|
} else {
|
|
|
- // 取消分配模式:需要选择产品且选择人员
|
|
|
|
|
- return !!selectedProduct.value && selectedMembers.value.length > 0
|
|
|
|
|
|
|
+ return assignedProducts.value.find(p => p.id === selectedProduct.value)
|
|
|
}
|
|
}
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
|
|
+// 判断当前选中的产品是否是日期卡(cardType >= 3)
|
|
|
|
|
+const isDateCard = computed(() => {
|
|
|
|
|
+ return currentSelectedProduct.value?.cardType >= 3
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+// 当前选中产品的可用席位数(仅日期卡有效)
|
|
|
|
|
+const currentAvailableSeats = computed(() => {
|
|
|
|
|
+ if (!selectedProduct.value || operationMode.value !== 'assign' || !isDateCard.value) return 0
|
|
|
|
|
+ return currentSelectedProduct.value?.availableSeats || 0
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+// 是否禁用某个成员的选择(只有日期卡才进行席位限制)
|
|
|
|
|
+const isMemberDisabled = (memberId) => {
|
|
|
|
|
+ if (operationMode.value !== 'assign') return false
|
|
|
|
|
+ // 次卡/积分卡不限制,不禁用
|
|
|
|
|
+ if (!isDateCard.value) return false
|
|
|
|
|
+ // 如果该成员已被选中,则不禁用(允许取消选择)
|
|
|
|
|
+ if (selectedMembers.value.includes(memberId)) return false
|
|
|
|
|
+ // 日期卡:如果已选人数达到可用席位数,则禁用未选中的成员
|
|
|
|
|
+ return selectedMembers.value.length >= currentAvailableSeats.value
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const canSubmit = computed(() => {
|
|
|
|
|
+ return selectedMembers.value.length > 0 && !!selectedProduct.value
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
const message = proxy?.$message
|
|
const message = proxy?.$message
|
|
|
|
|
|
|
|
watch(
|
|
watch(
|
|
@@ -244,52 +304,80 @@ watch(
|
|
|
{ immediate: true, deep: true }
|
|
{ immediate: true, deep: true }
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
|
|
+// 映射产品数据
|
|
|
|
|
+const mapProductData = (pkg) => ({
|
|
|
|
|
+ id: pkg.seats_id,
|
|
|
|
|
+ name: pkg.product_name,
|
|
|
|
|
+ description: `${pkg.spec_name} - ${pkg.card_type_text}`,
|
|
|
|
|
+ seats: pkg.available_seats,
|
|
|
|
|
+ productImage: pkg.product_image || pkg.image || '',
|
|
|
|
|
+ packagePriceId: pkg.package_price_id,
|
|
|
|
|
+ productId: pkg.product_id,
|
|
|
|
|
+ productType: pkg.product_type,
|
|
|
|
|
+ productLink: pkg.product_link,
|
|
|
|
|
+ cardType: pkg.card_type,
|
|
|
|
|
+ cardTypeText: pkg.card_type_text,
|
|
|
|
|
+ totalSeats: pkg.total_seats,
|
|
|
|
|
+ assignedSeats: pkg.assigned_seats,
|
|
|
|
|
+ availableSeats: pkg.available_seats,
|
|
|
|
|
+ totalTimes: pkg.total_times,
|
|
|
|
|
+ remainingTimes: pkg.remaining_times,
|
|
|
|
|
+ totalPoints: pkg.total_points,
|
|
|
|
|
+ remainingPoints: pkg.remaining_points,
|
|
|
|
|
+ endTime: pkg.end_time,
|
|
|
|
|
+ allocatedCount: pkg.allocated_count,
|
|
|
|
|
+ unallocatedCount: pkg.unallocated_count,
|
|
|
|
|
+ totalMembers: pkg.total_members
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+// 映射成员数据
|
|
|
|
|
+const mapMemberData = (member, seatsId, packagePriceId) => ({
|
|
|
|
|
+ id: member.user_id,
|
|
|
|
|
+ name: member.member_name,
|
|
|
|
|
+ email: member.email,
|
|
|
|
|
+ avatar: member.avatar || '',
|
|
|
|
|
+ mobile: member.mobile || '',
|
|
|
|
|
+ role: member.role,
|
|
|
|
|
+ ...(member.allocation_id && { allocation_id: member.allocation_id }),
|
|
|
|
|
+ seats_id: seatsId,
|
|
|
|
|
+ package_price_id: packagePriceId
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
const initDialog = async () => {
|
|
const initDialog = async () => {
|
|
|
- selectedMembers.value = props.selectedMemberIds.length > 0 ? [...props.selectedMemberIds] : []
|
|
|
|
|
if (operationMode.value === 'assign') {
|
|
if (operationMode.value === 'assign') {
|
|
|
await loadAvailableProducts()
|
|
await loadAvailableProducts()
|
|
|
} else {
|
|
} else {
|
|
|
await loadAssignedProducts()
|
|
await loadAssignedProducts()
|
|
|
}
|
|
}
|
|
|
|
|
+ if (props.selectedMemberIds.length > 0) {
|
|
|
|
|
+ selectedMembers.value = [...props.selectedMemberIds]
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const loadAvailableProducts = async () => {
|
|
const loadAvailableProducts = async () => {
|
|
|
try {
|
|
try {
|
|
|
const res = await allocationList()
|
|
const res = await allocationList()
|
|
|
- if (res.data && res.data.packages) {
|
|
|
|
|
- // 筛选出有可用席位的套餐(用于分配模式)
|
|
|
|
|
|
|
+ if (res.data?.packages) {
|
|
|
availableProducts.value = res.data.packages
|
|
availableProducts.value = res.data.packages
|
|
|
- .filter(pkg => pkg.available_seats > 0)
|
|
|
|
|
- .map(pkg => ({
|
|
|
|
|
- id: pkg.seats_id,
|
|
|
|
|
- name: pkg.product_name,
|
|
|
|
|
- description: `${pkg.spec_name} - ${pkg.card_type_text}`,
|
|
|
|
|
- seats: pkg.available_seats,
|
|
|
|
|
- packagePriceId: pkg.package_price_id,
|
|
|
|
|
- productId: pkg.product_id,
|
|
|
|
|
- cardType: pkg.card_type,
|
|
|
|
|
- cardTypeText: pkg.card_type_text,
|
|
|
|
|
- totalSeats: pkg.total_seats,
|
|
|
|
|
- assignedSeats: pkg.assigned_seats
|
|
|
|
|
- }))
|
|
|
|
|
-
|
|
|
|
|
- // 存储未分配成员列表
|
|
|
|
|
- if (res.data.members && res.data.members.length > 0) {
|
|
|
|
|
- // 将未分配成员转换为组件需要的格式
|
|
|
|
|
- const unassignedMembers = res.data.members.map(member => ({
|
|
|
|
|
- id: member.member_id,
|
|
|
|
|
- name: member.member_name,
|
|
|
|
|
- email: member.email,
|
|
|
|
|
- avatar: member.avatar
|
|
|
|
|
- }))
|
|
|
|
|
- // 更新 props.memberList(这里需要通过 emit 通知父组件更新)
|
|
|
|
|
- emit('update:memberList', unassignedMembers)
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ .filter(pkg => {
|
|
|
|
|
+ if (pkg.card_type >= 3) {
|
|
|
|
|
+ return pkg.available_seats > 0
|
|
|
|
|
+ }
|
|
|
|
|
+ return pkg.unallocated_members?.length > 0
|
|
|
|
|
+ })
|
|
|
|
|
+ .map(mapProductData)
|
|
|
|
|
+
|
|
|
|
|
+ res.data.packages.forEach(pkg => {
|
|
|
|
|
+ if (pkg.unallocated_members?.length > 0) {
|
|
|
|
|
+ productMembersMap.value[pkg.seats_id] = pkg.unallocated_members.map(member =>
|
|
|
|
|
+ mapMemberData(member, pkg.seats_id, pkg.package_price_id)
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
|
|
|
autoSelectFirstProduct()
|
|
autoSelectFirstProduct()
|
|
|
}
|
|
}
|
|
|
- } catch (error) {
|
|
|
|
|
- console.error('加载可分配套餐失败:', error)
|
|
|
|
|
|
|
+ } catch {
|
|
|
message?.error('加载可分配套餐失败')
|
|
message?.error('加载可分配套餐失败')
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -297,50 +385,31 @@ const loadAvailableProducts = async () => {
|
|
|
const loadAssignedProducts = async () => {
|
|
const loadAssignedProducts = async () => {
|
|
|
try {
|
|
try {
|
|
|
const res = await allocationList()
|
|
const res = await allocationList()
|
|
|
- if (res.data && res.data.packages) {
|
|
|
|
|
- // 筛选出已有分配成员的套餐(用于取消分配模式)
|
|
|
|
|
|
|
+ if (res.data?.packages) {
|
|
|
assignedProducts.value = res.data.packages
|
|
assignedProducts.value = res.data.packages
|
|
|
- .filter(pkg => pkg.allocated_members && pkg.allocated_members.length > 0)
|
|
|
|
|
- .map(pkg => ({
|
|
|
|
|
- id: pkg.seats_id,
|
|
|
|
|
- name: pkg.product_name,
|
|
|
|
|
- description: `${pkg.spec_name} - ${pkg.card_type_text}`,
|
|
|
|
|
- assignedCount: pkg.assigned_seats,
|
|
|
|
|
- packagePriceId: pkg.package_price_id,
|
|
|
|
|
- productId: pkg.product_id,
|
|
|
|
|
- cardType: pkg.card_type,
|
|
|
|
|
- cardTypeText: pkg.card_type_text,
|
|
|
|
|
- totalSeats: pkg.total_seats,
|
|
|
|
|
- assignedSeats: pkg.assigned_seats
|
|
|
|
|
- }))
|
|
|
|
|
-
|
|
|
|
|
- // 存储每个套餐的已分配成员列表
|
|
|
|
|
|
|
+ .filter(pkg => pkg.allocated_members?.length > 0)
|
|
|
|
|
+ .map(mapProductData)
|
|
|
|
|
+
|
|
|
res.data.packages.forEach(pkg => {
|
|
res.data.packages.forEach(pkg => {
|
|
|
- if (pkg.allocated_members && pkg.allocated_members.length > 0) {
|
|
|
|
|
- productMembersMap.value[pkg.seats_id] = pkg.allocated_members.map(member => ({
|
|
|
|
|
- id: member.member_id,
|
|
|
|
|
- name: member.member_name,
|
|
|
|
|
- email: member.email,
|
|
|
|
|
- avatar: member.avatar
|
|
|
|
|
- }))
|
|
|
|
|
|
|
+ if (pkg.allocated_members?.length > 0) {
|
|
|
|
|
+ productMembersMap.value[pkg.seats_id] = pkg.allocated_members.map(member =>
|
|
|
|
|
+ mapMemberData(member, pkg.seats_id, pkg.package_price_id)
|
|
|
|
|
+ )
|
|
|
}
|
|
}
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
autoSelectFirstProduct()
|
|
autoSelectFirstProduct()
|
|
|
}
|
|
}
|
|
|
- } catch (error) {
|
|
|
|
|
- console.error('加载已分配套餐失败:', error)
|
|
|
|
|
|
|
+ } catch {
|
|
|
message?.error('加载已分配套餐失败')
|
|
message?.error('加载已分配套餐失败')
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const autoSelectFirstProduct = () => {
|
|
const autoSelectFirstProduct = () => {
|
|
|
- if (currentProductList.value.length > 0) {
|
|
|
|
|
- selectedProduct.value = currentProductList.value[0].id
|
|
|
|
|
- } else {
|
|
|
|
|
- selectedProduct.value = null
|
|
|
|
|
|
|
+ selectedProduct.value = currentProductList.value[0]?.id || null
|
|
|
|
|
+ if (props.selectedMemberIds.length === 0) {
|
|
|
|
|
+ selectedMembers.value = []
|
|
|
}
|
|
}
|
|
|
- selectedMembers.value = []
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 选择产品时,更新人员列表
|
|
// 选择产品时,更新人员列表
|
|
@@ -353,6 +422,10 @@ const handleModeChange = async () => {
|
|
|
// 切换模式时重置状态并重新拉取对应产品列表
|
|
// 切换模式时重置状态并重新拉取对应产品列表
|
|
|
selectedProduct.value = null
|
|
selectedProduct.value = null
|
|
|
selectedMembers.value = []
|
|
selectedMembers.value = []
|
|
|
|
|
+ // 清空产品列表和成员映射,避免数据混乱
|
|
|
|
|
+ availableProducts.value = []
|
|
|
|
|
+ assignedProducts.value = []
|
|
|
|
|
+ productMembersMap.value = {}
|
|
|
if (operationMode.value === 'assign') {
|
|
if (operationMode.value === 'assign') {
|
|
|
await loadAvailableProducts()
|
|
await loadAvailableProducts()
|
|
|
} else {
|
|
} else {
|
|
@@ -360,19 +433,48 @@ const handleModeChange = async () => {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-const handleMemberChange = () => {
|
|
|
|
|
- // 人员选择变化时的处理
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
const handleSelectAll = () => {
|
|
const handleSelectAll = () => {
|
|
|
- selectedMembers.value = currentMemberList.value.map(m => m.id)
|
|
|
|
|
|
|
+ if (operationMode.value === 'assign') {
|
|
|
|
|
+ // 分配模式
|
|
|
|
|
+ if (isDateCard.value) {
|
|
|
|
|
+ // 日期卡:最多只能选择可用席位数量的人员
|
|
|
|
|
+ const maxSelectable = Math.min(currentMemberList.value.length, currentAvailableSeats.value)
|
|
|
|
|
+ selectedMembers.value = currentMemberList.value.slice(0, maxSelectable).map(m => m.id)
|
|
|
|
|
+ if (currentMemberList.value.length > currentAvailableSeats.value) {
|
|
|
|
|
+ message?.warning(`可用席位仅剩 ${currentAvailableSeats.value} 个,已为您选择前 ${maxSelectable} 位成员`)
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 次卡/积分卡:不限制,可以全选
|
|
|
|
|
+ selectedMembers.value = currentMemberList.value.map(m => m.id)
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 取消分配模式:可以全选
|
|
|
|
|
+ selectedMembers.value = currentMemberList.value.map(m => m.id)
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const handleSelectNone = () => {
|
|
const handleSelectNone = () => {
|
|
|
selectedMembers.value = []
|
|
selectedMembers.value = []
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-const getProductImage = productId => `https://picsum.photos/200/200?random=${productId}`
|
|
|
|
|
|
|
+// 处理产品图片加载错误
|
|
|
|
|
+const handleImageError = (event) => {
|
|
|
|
|
+ event.target.style.display = 'none'
|
|
|
|
|
+ const placeholder = event.target.nextElementSibling
|
|
|
|
|
+ if (placeholder && placeholder.classList.contains('product-image-placeholder')) {
|
|
|
|
|
+ placeholder.style.display = 'flex'
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 处理成员头像加载错误
|
|
|
|
|
+const handleMemberAvatarError = (event) => {
|
|
|
|
|
+ event.target.style.display = 'none'
|
|
|
|
|
+ // 显示默认图标
|
|
|
|
|
+ const icon = event.target.nextElementSibling
|
|
|
|
|
+ if (icon) {
|
|
|
|
|
+ icon.style.display = 'flex'
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
const handleSubmit = async () => {
|
|
const handleSubmit = async () => {
|
|
|
if (!selectedProduct.value) {
|
|
if (!selectedProduct.value) {
|
|
@@ -387,17 +489,77 @@ const handleSubmit = async () => {
|
|
|
|
|
|
|
|
submitting.value = true
|
|
submitting.value = true
|
|
|
try {
|
|
try {
|
|
|
- const requestData = {
|
|
|
|
|
- seats_id: selectedProduct.value,
|
|
|
|
|
- assigned_to_user_id: selectedMembers.value
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
if (operationMode.value === 'assign') {
|
|
if (operationMode.value === 'assign') {
|
|
|
- await assign(requestData)
|
|
|
|
|
- message?.success(`成功为 ${selectedMembers.value.length} 位人员分配产品席位`)
|
|
|
|
|
|
|
+ // 分配模式:后端只支持单个用户分配,需要循环调用
|
|
|
|
|
+ let successCount = 0
|
|
|
|
|
+ let failCount = 0
|
|
|
|
|
+
|
|
|
|
|
+ for (const userId of selectedMembers.value) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await assign({
|
|
|
|
|
+ seats_id: selectedProduct.value,
|
|
|
|
|
+ assigned_to_user_id: userId
|
|
|
|
|
+ })
|
|
|
|
|
+ successCount++
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ failCount++
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 只显示一次提示,根据情况选择最合适的提示
|
|
|
|
|
+ if (successCount > 0 && failCount > 0) {
|
|
|
|
|
+
|
|
|
|
|
+ } else if (successCount > 0) {
|
|
|
|
|
+
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 全部失败
|
|
|
|
|
+ message?.error('分配席位失败,请重试')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
} else {
|
|
} else {
|
|
|
- await cancel(requestData)
|
|
|
|
|
- message?.success(`成功取消 ${selectedMembers.value.length} 位人员的产品席位`)
|
|
|
|
|
|
|
+ // 取消分配模式:需要 allocation_id
|
|
|
|
|
+ // 后端已返回 allocation_id 字段,直接使用
|
|
|
|
|
+ const currentMembers = currentMemberList.value
|
|
|
|
|
+ let successCount = 0
|
|
|
|
|
+ let failCount = 0
|
|
|
|
|
+ let missingAllocationIdCount = 0
|
|
|
|
|
+
|
|
|
|
|
+ for (const userId of selectedMembers.value) {
|
|
|
|
|
+ const member = currentMembers.find(m => m.id === userId)
|
|
|
|
|
+ if (!member) {
|
|
|
|
|
+ failCount++
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const allocationId = member.allocation_id
|
|
|
|
|
+ if (allocationId) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await cancel({ allocation_id: allocationId })
|
|
|
|
|
+ successCount++
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ failCount++
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ missingAllocationIdCount++
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const totalFailCount = failCount + missingAllocationIdCount
|
|
|
|
|
+
|
|
|
|
|
+ if (successCount > 0 && totalFailCount > 0) {
|
|
|
|
|
+ const failDetails = []
|
|
|
|
|
+ if (failCount > 0) failDetails.push(`${failCount} 位操作失败`)
|
|
|
|
|
+ if (missingAllocationIdCount > 0) failDetails.push(`${missingAllocationIdCount} 位缺少分配记录ID`)
|
|
|
|
|
+ // message?.success(`成功取消 ${successCount} 位人员的产品席位,${failDetails.join(',')}`)
|
|
|
|
|
+ } else if (successCount > 0) {
|
|
|
|
|
+ // message?.success(`成功取消 ${successCount} 位人员的产品席位`)
|
|
|
|
|
+ } else if (totalFailCount > 0) {
|
|
|
|
|
+ const failDetails = []
|
|
|
|
|
+ if (failCount > 0) failDetails.push(`${failCount} 位操作失败`)
|
|
|
|
|
+ if (missingAllocationIdCount > 0) failDetails.push(`${missingAllocationIdCount} 位缺少分配记录ID`)
|
|
|
|
|
+ message?.error(`取消分配失败:${failDetails.join(',')}。请刷新页面后重试。`)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
emit('success', {
|
|
emit('success', {
|
|
@@ -406,9 +568,6 @@ const handleSubmit = async () => {
|
|
|
memberIds: selectedMembers.value
|
|
memberIds: selectedMembers.value
|
|
|
})
|
|
})
|
|
|
emit('update:visible', false)
|
|
emit('update:visible', false)
|
|
|
- } catch (error) {
|
|
|
|
|
- console.error('操作失败:', error)
|
|
|
|
|
- message?.error(operationMode.value === 'assign' ? '分配席位失败' : '取消分配失败')
|
|
|
|
|
} finally {
|
|
} finally {
|
|
|
submitting.value = false
|
|
submitting.value = false
|
|
|
}
|
|
}
|
|
@@ -427,6 +586,7 @@ const resetForm = () => {
|
|
|
operationMode.value = 'assign'
|
|
operationMode.value = 'assign'
|
|
|
selectedMembers.value = []
|
|
selectedMembers.value = []
|
|
|
selectedProduct.value = null
|
|
selectedProduct.value = null
|
|
|
|
|
+ availableProducts.value = []
|
|
|
assignedProducts.value = []
|
|
assignedProducts.value = []
|
|
|
productMembersMap.value = {}
|
|
productMembersMap.value = {}
|
|
|
submitting.value = false
|
|
submitting.value = false
|
|
@@ -571,17 +731,21 @@ const resetForm = () => {
|
|
|
// 人员选择区域
|
|
// 人员选择区域
|
|
|
.member-section {
|
|
.member-section {
|
|
|
.member-list-wrapper {
|
|
.member-list-wrapper {
|
|
|
- .selected-badge {
|
|
|
|
|
|
|
+ .info-badges {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ margin-bottom: 16px;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .selected-badge,
|
|
|
|
|
+ .seats-badge {
|
|
|
display: inline-flex;
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
gap: 6px;
|
|
gap: 6px;
|
|
|
padding: 8px 16px;
|
|
padding: 8px 16px;
|
|
|
- background: linear-gradient(135deg, #ecf5ff 0%, #e1f3ff 100%);
|
|
|
|
|
- border: 1px solid #b3d8ff;
|
|
|
|
|
border-radius: 20px;
|
|
border-radius: 20px;
|
|
|
- margin-bottom: 16px;
|
|
|
|
|
font-size: 14px;
|
|
font-size: 14px;
|
|
|
- color: #409eff;
|
|
|
|
|
|
|
|
|
|
i {
|
|
i {
|
|
|
font-size: 16px;
|
|
font-size: 16px;
|
|
@@ -593,6 +757,18 @@ const resetForm = () => {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ .selected-badge {
|
|
|
|
|
+ background: linear-gradient(135deg, #ecf5ff 0%, #e1f3ff 100%);
|
|
|
|
|
+ border: 1px solid #b3d8ff;
|
|
|
|
|
+ color: #409eff;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .seats-badge {
|
|
|
|
|
+ background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
|
|
|
|
|
+ border: 1px solid #bae6fd;
|
|
|
|
|
+ color: #0284c7;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
.member-list-scroll {
|
|
.member-list-scroll {
|
|
|
max-height: 320px;
|
|
max-height: 320px;
|
|
|
overflow-y: auto;
|
|
overflow-y: auto;
|
|
@@ -656,6 +832,22 @@ const resetForm = () => {
|
|
|
transform: translateY(-1px);
|
|
transform: translateY(-1px);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ &.disabled {
|
|
|
|
|
+ opacity: 0.5;
|
|
|
|
|
+ cursor: not-allowed;
|
|
|
|
|
+ background: #f5f7fa;
|
|
|
|
|
+
|
|
|
|
|
+ &:hover {
|
|
|
|
|
+ border-color: #e4e7ed;
|
|
|
|
|
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
|
|
|
|
+ transform: none;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .member-checkbox {
|
|
|
|
|
+ cursor: not-allowed;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
::v-deep .member-checkbox {
|
|
::v-deep .member-checkbox {
|
|
|
width: 100%;
|
|
width: 100%;
|
|
|
margin: 0;
|
|
margin: 0;
|
|
@@ -688,6 +880,14 @@ const resetForm = () => {
|
|
|
flex-shrink: 0;
|
|
flex-shrink: 0;
|
|
|
box-shadow: 0 2px 6px rgba(64, 158, 255, 0.25);
|
|
box-shadow: 0 2px 6px rgba(64, 158, 255, 0.25);
|
|
|
transition: transform 0.2s ease;
|
|
transition: transform 0.2s ease;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+
|
|
|
|
|
+ img {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ object-fit: cover;
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
i {
|
|
i {
|
|
|
font-size: 22px;
|
|
font-size: 22px;
|
|
@@ -744,6 +944,17 @@ const resetForm = () => {
|
|
|
color: #fff;
|
|
color: #fff;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ .member-disabled-badge {
|
|
|
|
|
+ padding: 4px 10px;
|
|
|
|
|
+ background: #f56c6c;
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.empty-member {
|
|
.empty-member {
|
|
@@ -844,6 +1055,20 @@ const resetForm = () => {
|
|
|
display: block;
|
|
display: block;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ .product-image-placeholder {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ background: linear-gradient(135deg, #f5f7fa 0%, #e4e7ed 100%);
|
|
|
|
|
+ color: #c0c4cc;
|
|
|
|
|
+
|
|
|
|
|
+ i {
|
|
|
|
|
+ font-size: 48px;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
&::after {
|
|
&::after {
|
|
|
content: '';
|
|
content: '';
|
|
|
position: absolute;
|
|
position: absolute;
|
|
@@ -852,6 +1077,7 @@ const resetForm = () => {
|
|
|
right: 0;
|
|
right: 0;
|
|
|
bottom: 0;
|
|
bottom: 0;
|
|
|
background: linear-gradient(to bottom, transparent 0%, rgba(0, 0, 0, 0.02) 100%);
|
|
background: linear-gradient(to bottom, transparent 0%, rgba(0, 0, 0, 0.02) 100%);
|
|
|
|
|
+ pointer-events: none;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -901,17 +1127,30 @@ const resetForm = () => {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- .seats-info {
|
|
|
|
|
|
|
+ .seats-info,
|
|
|
|
|
+ .times-info,
|
|
|
|
|
+ .points-info {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
gap: 4px;
|
|
gap: 4px;
|
|
|
- color: #409eff;
|
|
|
|
|
font-weight: 500;
|
|
font-weight: 500;
|
|
|
|
|
|
|
|
i {
|
|
i {
|
|
|
font-size: 14px;
|
|
font-size: 14px;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ .seats-info {
|
|
|
|
|
+ color: #409eff;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .times-info {
|
|
|
|
|
+ color: #67c23a;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .points-info {
|
|
|
|
|
+ color: #e6a23c;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|