|
|
@@ -1,34 +1,21 @@
|
|
|
<template>
|
|
|
- <el-dialog
|
|
|
- title="产品分配席位"
|
|
|
- :model-value="props.visible"
|
|
|
- @update:model-value="(val) => emit('update:visible', val)"
|
|
|
- width="900px"
|
|
|
- :close-on-click-modal="false"
|
|
|
- :close-on-press-escape="true"
|
|
|
- class="product-seat-dialog"
|
|
|
- @close="handleClose"
|
|
|
- >
|
|
|
+ <el-dialog title="产品分配席位" :model-value="props.visible" @update:model-value="(val) => emit('update:visible', val)"
|
|
|
+ width="900px" :close-on-click-modal="false" :close-on-press-escape="true" class="product-seat-dialog"
|
|
|
+ @close="handleClose">
|
|
|
|
|
|
<div class="dialog-content">
|
|
|
<!-- 操作模式选择 - 使用标签页样式 -->
|
|
|
<div class="mode-section">
|
|
|
<div class="mode-tabs">
|
|
|
- <div
|
|
|
- class="mode-tab"
|
|
|
- :class="{ active: operationMode === 'assign' }"
|
|
|
- @click="operationMode = 'assign'; handleModeChange()"
|
|
|
- >
|
|
|
+ <div class="mode-tab" :class="{ active: operationMode == 'assign' }"
|
|
|
+ @click="operationMode = 'assign'; handleModeChange()">
|
|
|
<el-icon>
|
|
|
<Plus />
|
|
|
</el-icon>
|
|
|
<span>分配席位</span>
|
|
|
</div>
|
|
|
- <div
|
|
|
- class="mode-tab"
|
|
|
- :class="{ active: operationMode === 'cancel' }"
|
|
|
- @click="operationMode = 'cancel'; handleModeChange()"
|
|
|
- >
|
|
|
+ <div class="mode-tab" :class="{ active: operationMode == 'cancel' }"
|
|
|
+ @click="operationMode = 'cancel'; handleModeChange()">
|
|
|
<el-icon>
|
|
|
<Minus />
|
|
|
</el-icon>
|
|
|
@@ -48,13 +35,8 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="product-grid">
|
|
|
- <div
|
|
|
- v-for="product in currentProductList"
|
|
|
- :key="product.id"
|
|
|
- class="product-card"
|
|
|
- :class="{ active: selectedProduct === product.id }"
|
|
|
- @click="handleProductSelect(product.id)"
|
|
|
- >
|
|
|
+ <div v-for="product in currentProductList" :key="product.id" class="product-card"
|
|
|
+ :class="{ active: selectedProduct === product.id }" @click="handleProductSelect(product.id)">
|
|
|
<div class="product-image">
|
|
|
<img :src="getProductImage(product.id)" :alt="product.name">
|
|
|
</div>
|
|
|
@@ -62,12 +44,21 @@
|
|
|
<div class="product-name">{{ product.name }}</div>
|
|
|
<div v-if="product.description" class="product-desc">{{ product.description }}</div>
|
|
|
<div class="product-meta">
|
|
|
+ <span class="card-type-badge">
|
|
|
+ <el-icon>
|
|
|
+ <Ticket />
|
|
|
+ </el-icon>
|
|
|
+ <span>{{ product.cardTypeText }}</span>
|
|
|
+ </span>
|
|
|
<span class="seats-info">
|
|
|
<el-icon>
|
|
|
<UserFilled />
|
|
|
</el-icon>
|
|
|
- <span>
|
|
|
- {{ operationMode === 'assign' ? `可用席位: ${product.seats ?? 0}` : `已分配 ${product.assignedCount || 0} 人` }}
|
|
|
+ <span v-if="operationMode === 'assign'">
|
|
|
+ 可用: {{ product.seats ?? 0 }}/{{ product.totalSeats ?? 0 }}
|
|
|
+ </span>
|
|
|
+ <span v-else>
|
|
|
+ 已分配: {{ product.assignedSeats || 0 }}/{{ product.totalSeats ?? 0 }}
|
|
|
</span>
|
|
|
</span>
|
|
|
</div>
|
|
|
@@ -115,12 +106,8 @@
|
|
|
<div class="member-list-scroll">
|
|
|
<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) }"
|
|
|
- >
|
|
|
+ <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">
|
|
|
<div class="member-avatar">
|
|
|
<el-icon>
|
|
|
@@ -179,7 +166,8 @@
|
|
|
|
|
|
<script setup>
|
|
|
import { computed, getCurrentInstance, ref, watch } from 'vue'
|
|
|
-import { Check, Close, Goods, Minus, Plus, Remove, User, UserFilled } from '@element-plus/icons-vue'
|
|
|
+import { Check, Close, Goods, Minus, Plus, Remove, Ticket, User, UserFilled } from '@element-plus/icons-vue'
|
|
|
+import { allocationList, assign, cancel } from '/@/api/frontend/me'
|
|
|
|
|
|
defineOptions({ name: 'ProductSeatDialog' })
|
|
|
|
|
|
@@ -198,7 +186,7 @@ const props = defineProps({
|
|
|
}
|
|
|
})
|
|
|
|
|
|
-const emit = defineEmits(['update:visible', 'success', 'close'])
|
|
|
+const emit = defineEmits(['update:visible', 'success', 'close', 'update:memberList'])
|
|
|
const { proxy } = getCurrentInstance()
|
|
|
|
|
|
const operationMode = ref('assign')
|
|
|
@@ -216,8 +204,10 @@ const currentProductList = computed(() =>
|
|
|
const currentMemberList = computed(() => {
|
|
|
if (!selectedProduct.value) return []
|
|
|
if (operationMode.value === 'assign') {
|
|
|
+ // 分配模式:显示未分配的成员列表
|
|
|
return props.memberList
|
|
|
}
|
|
|
+ // 取消分配模式:显示该套餐已分配的成员列表
|
|
|
return productMembersMap.value[selectedProduct.value] || []
|
|
|
})
|
|
|
|
|
|
@@ -265,61 +255,82 @@ const initDialog = async () => {
|
|
|
|
|
|
const loadAvailableProducts = async () => {
|
|
|
try {
|
|
|
- availableProducts.value = [
|
|
|
- {
|
|
|
- id: 1,
|
|
|
- name: '聚德神笔套餐',
|
|
|
- description: '聚德 AI 写作/办公套件',
|
|
|
- seats: 10
|
|
|
- },
|
|
|
- {
|
|
|
- id: 2,
|
|
|
- name: '棱镜套餐',
|
|
|
- description: '图像/视频增强助手',
|
|
|
- seats: 5
|
|
|
+ const res = await allocationList()
|
|
|
+ if (res.data && 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)
|
|
|
}
|
|
|
- ]
|
|
|
- autoSelectFirstProduct()
|
|
|
+
|
|
|
+ autoSelectFirstProduct()
|
|
|
+ }
|
|
|
} catch (error) {
|
|
|
- console.error('加载产品列表失败:', error)
|
|
|
- message?.error('加载产品列表失败')
|
|
|
- availableProducts.value = []
|
|
|
- selectedProduct.value = null
|
|
|
+ console.error('加载可分配套餐失败:', error)
|
|
|
+ message?.error('加载可分配套餐失败')
|
|
|
}
|
|
|
}
|
|
|
|
|
|
const loadAssignedProducts = async () => {
|
|
|
try {
|
|
|
- // 加载所有已分配的产品列表
|
|
|
- assignedProducts.value = [
|
|
|
- {
|
|
|
- id: 1,
|
|
|
- name: '聚德神笔套餐',
|
|
|
- description: '聚德 AI 写作/办公套件',
|
|
|
- assignedCount: 2
|
|
|
- }
|
|
|
- ]
|
|
|
-
|
|
|
- // 加载每个产品下的人员列表
|
|
|
- // 这里应该调用API获取每个产品下的人员
|
|
|
- // 暂时使用模拟数据
|
|
|
- assignedProducts.value.forEach(product => {
|
|
|
- // 模拟数据:每个产品下的人员
|
|
|
- productMembersMap.value[product.id] = props.memberList.filter((member, index) => {
|
|
|
- // 模拟:产品1包含前2个成员
|
|
|
- if (product.id === 1) {
|
|
|
- return index < 2
|
|
|
+ const res = await allocationList()
|
|
|
+ if (res.data && 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
|
|
|
+ }))
|
|
|
+
|
|
|
+ // 存储每个套餐的已分配成员列表
|
|
|
+ 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
|
|
|
+ }))
|
|
|
}
|
|
|
- return false
|
|
|
})
|
|
|
- })
|
|
|
- autoSelectFirstProduct()
|
|
|
+
|
|
|
+ autoSelectFirstProduct()
|
|
|
+ }
|
|
|
} catch (error) {
|
|
|
- console.error('加载已分配产品失败:', error)
|
|
|
- message?.error('加载已分配产品失败')
|
|
|
- assignedProducts.value = []
|
|
|
- productMembersMap.value = {}
|
|
|
- selectedProduct.value = null
|
|
|
+ console.error('加载已分配套餐失败:', error)
|
|
|
+ message?.error('加载已分配套餐失败')
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -363,41 +374,35 @@ const handleSelectNone = () => {
|
|
|
|
|
|
const getProductImage = productId => `https://picsum.photos/200/200?random=${productId}`
|
|
|
|
|
|
-const handleCancelProduct = product => {
|
|
|
- selectedProduct.value = product.id
|
|
|
- handleSubmit()
|
|
|
-}
|
|
|
-
|
|
|
const handleSubmit = async () => {
|
|
|
if (!selectedProduct.value) {
|
|
|
- message?.warning(operationMode.value === 'assign' ? '没有可分配的套餐' : '请先选择要取消的套餐')
|
|
|
+ message?.warning(operationMode.value == 'assign' ? '没有可分配的套餐' : '请先选择要取消的套餐')
|
|
|
return
|
|
|
}
|
|
|
|
|
|
- if (selectedMembers.value.length === 0) {
|
|
|
+ if (selectedMembers.value.length == 0) {
|
|
|
message?.warning('请至少选择一个人员')
|
|
|
return
|
|
|
}
|
|
|
|
|
|
- if (operationMode.value !== 'assign' && selectedMembers.value.length === 0) {
|
|
|
- message?.warning('请选择要取消分配的人员')
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
submitting.value = true
|
|
|
try {
|
|
|
- const productId =
|
|
|
- selectedProduct.value || (assignedProducts.value.length > 0 ? assignedProducts.value[0].id : null)
|
|
|
+ const requestData = {
|
|
|
+ seats_id: selectedProduct.value,
|
|
|
+ assigned_to_user_id: selectedMembers.value
|
|
|
+ }
|
|
|
|
|
|
if (operationMode.value === 'assign') {
|
|
|
+ await assign(requestData)
|
|
|
message?.success(`成功为 ${selectedMembers.value.length} 位人员分配产品席位`)
|
|
|
} else {
|
|
|
+ await cancel(requestData)
|
|
|
message?.success(`成功取消 ${selectedMembers.value.length} 位人员的产品席位`)
|
|
|
}
|
|
|
|
|
|
emit('success', {
|
|
|
mode: operationMode.value,
|
|
|
- productId,
|
|
|
+ seatsId: selectedProduct.value,
|
|
|
memberIds: selectedMembers.value
|
|
|
})
|
|
|
emit('update:visible', false)
|
|
|
@@ -875,8 +880,26 @@ const resetForm = () => {
|
|
|
.product-meta {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
- gap: 8px;
|
|
|
+ gap: 12px;
|
|
|
font-size: 13px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+
|
|
|
+ .card-type-badge {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 4px;
|
|
|
+ padding: 4px 10px;
|
|
|
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
|
|
+ color: #fff;
|
|
|
+ border-radius: 12px;
|
|
|
+ font-weight: 500;
|
|
|
+ font-size: 12px;
|
|
|
+ box-shadow: 0 2px 6px rgba(245, 87, 108, 0.25);
|
|
|
+
|
|
|
+ i {
|
|
|
+ font-size: 13px;
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
.seats-info {
|
|
|
display: flex;
|
|
|
@@ -988,4 +1011,3 @@ const resetForm = () => {
|
|
|
}
|
|
|
}
|
|
|
</style>
|
|
|
-
|