|
|
@@ -1,7 +1,8 @@
|
|
|
<template>
|
|
|
<el-dialog
|
|
|
title="产品分配席位"
|
|
|
- v-model:visible="dialogVisible"
|
|
|
+ :model-value="props.visible"
|
|
|
+ @update:model-value="(val) => emit('update:visible', val)"
|
|
|
width="900px"
|
|
|
:close-on-click-modal="false"
|
|
|
:close-on-press-escape="true"
|
|
|
@@ -36,84 +37,23 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
- <!-- 选择人员 - 卡片式布局 -->
|
|
|
- <div class="member-section">
|
|
|
- <div class="section-header">
|
|
|
- <div class="section-title">
|
|
|
- <el-icon>
|
|
|
- <UserFilled />
|
|
|
- </el-icon>
|
|
|
- <span>选择人员</span>
|
|
|
- </div>
|
|
|
- <div class="section-actions">
|
|
|
- <el-button type="text" size="small" :icon="Check" @click="handleSelectAll">
|
|
|
- 全选
|
|
|
- </el-button>
|
|
|
- <el-button type="text" size="small" :icon="Close" @click="handleSelectNone">
|
|
|
- 清空
|
|
|
- </el-button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div class="member-list-wrapper">
|
|
|
- <div v-if="selectedMembers.length > 0" class="selected-badge">
|
|
|
- <el-icon>
|
|
|
- <Check />
|
|
|
- </el-icon>
|
|
|
- <span>已选择 <strong>{{ selectedMembers.length }}</strong> 位成员</span>
|
|
|
- </div>
|
|
|
- <div class="member-list">
|
|
|
- <el-checkbox-group v-model="selectedMembers" @change="handleMemberChange">
|
|
|
- <div
|
|
|
- v-for="member in memberList"
|
|
|
- :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>
|
|
|
- <UserFilled />
|
|
|
- </el-icon>
|
|
|
- </div>
|
|
|
- <div class="member-details">
|
|
|
- <div class="member-name">{{ member.name }}</div>
|
|
|
- <div class="member-email">{{ member.email }}</div>
|
|
|
- </div>
|
|
|
- <div v-if="selectedMembers.includes(member.id)" class="member-check-icon">
|
|
|
- <el-icon>
|
|
|
- <Check />
|
|
|
- </el-icon>
|
|
|
- </div>
|
|
|
- </el-checkbox>
|
|
|
- </div>
|
|
|
- </el-checkbox-group>
|
|
|
- <div v-if="memberList.length === 0" class="empty-member">
|
|
|
- <el-icon>
|
|
|
- <User />
|
|
|
- </el-icon>
|
|
|
- <p>暂无人员数据</p>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 选择产品 - 卡片式展示 -->
|
|
|
- <div v-if="operationMode === 'assign'" class="product-section">
|
|
|
+ <!-- 选择套餐(分配/取消统一放在最前,进入即展示) -->
|
|
|
+ <div class="product-section">
|
|
|
<div class="section-header">
|
|
|
<div class="section-title">
|
|
|
<el-icon>
|
|
|
<Goods />
|
|
|
</el-icon>
|
|
|
- <span>选择套餐产品</span>
|
|
|
+ <span>{{ operationMode === 'assign' ? '选择可分配套餐' : '选择要取消的套餐' }}</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="product-grid">
|
|
|
<div
|
|
|
- v-for="product in availableProducts"
|
|
|
+ v-for="product in currentProductList"
|
|
|
:key="product.id"
|
|
|
class="product-card"
|
|
|
:class="{ active: selectedProduct === product.id }"
|
|
|
- @click="selectedProduct = product.id"
|
|
|
+ @click="handleProductSelect(product.id)"
|
|
|
>
|
|
|
<div class="product-image">
|
|
|
<img :src="getProductImage(product.id)" :alt="product.name">
|
|
|
@@ -121,12 +61,14 @@
|
|
|
<div class="product-content">
|
|
|
<div class="product-name">{{ product.name }}</div>
|
|
|
<div v-if="product.description" class="product-desc">{{ product.description }}</div>
|
|
|
- <div v-if="product.seats !== undefined" class="product-meta">
|
|
|
+ <div class="product-meta">
|
|
|
<span class="seats-info">
|
|
|
<el-icon>
|
|
|
- <User />
|
|
|
+ <UserFilled />
|
|
|
</el-icon>
|
|
|
- <span>可用席位: {{ product.seats }}</span>
|
|
|
+ <span>
|
|
|
+ {{ operationMode === 'assign' ? `可用席位: ${product.seats ?? 0}` : `已分配 ${product.assignedCount || 0} 人` }}
|
|
|
+ </span>
|
|
|
</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -136,81 +78,95 @@
|
|
|
</el-icon>
|
|
|
</div>
|
|
|
</div>
|
|
|
- <div v-if="availableProducts.length === 0" class="empty-product">
|
|
|
+ <div v-if="currentProductList.length === 0" class="empty-product">
|
|
|
<el-icon>
|
|
|
<Goods />
|
|
|
</el-icon>
|
|
|
- <p>暂无可用产品</p>
|
|
|
+ <p>{{ operationMode === 'assign' ? '没有可以分配的套餐' : '暂无可取消的套餐' }}</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
- <!-- 已分配产品列表(取消分配模式) -->
|
|
|
- <div v-if="operationMode === 'cancel'" class="assigned-section">
|
|
|
+ <!-- 选择人员 - 卡片式布局 -->
|
|
|
+ <div class="member-section">
|
|
|
<div class="section-header">
|
|
|
<div class="section-title">
|
|
|
<el-icon>
|
|
|
- <List />
|
|
|
+ <UserFilled />
|
|
|
</el-icon>
|
|
|
- <span>已分配产品</span>
|
|
|
+ <span>{{ operationMode === 'assign' ? '选择人员' : '选择要取消分配的人员' }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="section-actions">
|
|
|
+ <el-button type="text" size="small" :icon="Check" @click="handleSelectAll" :disabled="!selectedProduct">
|
|
|
+ 全选
|
|
|
+ </el-button>
|
|
|
+ <el-button type="text" size="small" :icon="Close" @click="handleSelectNone">
|
|
|
+ 清空
|
|
|
+ </el-button>
|
|
|
</div>
|
|
|
</div>
|
|
|
- <div v-if="assignedProducts.length > 0" class="assigned-product-list">
|
|
|
- <div
|
|
|
- v-for="product in assignedProducts"
|
|
|
- :key="product.id"
|
|
|
- class="assigned-product-card"
|
|
|
- :class="{ active: selectedProduct === product.id }"
|
|
|
- @click="selectedProduct = product.id"
|
|
|
- >
|
|
|
- <div class="product-icon assigned">
|
|
|
- <el-icon>
|
|
|
- <Goods />
|
|
|
- </el-icon>
|
|
|
- </div>
|
|
|
- <div class="product-content">
|
|
|
- <div class="product-name">{{ product.name }}</div>
|
|
|
- <div v-if="product.description" class="product-desc">{{ product.description }}</div>
|
|
|
- <div class="product-meta">
|
|
|
- <span class="assigned-count">
|
|
|
- <el-icon>
|
|
|
- <UserFilled />
|
|
|
- </el-icon>
|
|
|
- <span>已分配 {{ product.assignedCount || selectedMembers.length }} 人</span>
|
|
|
- </span>
|
|
|
+ <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>
|
|
|
+ <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) }"
|
|
|
+ >
|
|
|
+ <el-checkbox :label="member.id" class="member-checkbox">
|
|
|
+ <div class="member-avatar">
|
|
|
+ <el-icon>
|
|
|
+ <UserFilled />
|
|
|
+ </el-icon>
|
|
|
+ </div>
|
|
|
+ <div class="member-details">
|
|
|
+ <div class="member-name">{{ member.name }}</div>
|
|
|
+ <div class="member-email">{{ member.email }}</div>
|
|
|
+ </div>
|
|
|
+ <div v-if="selectedMembers.includes(member.id)" class="member-check-icon">
|
|
|
+ <el-icon>
|
|
|
+ <Check />
|
|
|
+ </el-icon>
|
|
|
+ </div>
|
|
|
+ </el-checkbox>
|
|
|
+ </div>
|
|
|
+ </el-checkbox-group>
|
|
|
+ <div v-if="currentMemberList.length === 0" class="empty-member">
|
|
|
+ <el-icon>
|
|
|
+ <User />
|
|
|
+ </el-icon>
|
|
|
+ <p>{{ operationMode === 'assign' ? '暂无可分配的人员' : '该套餐暂无可取消的人员' }}</p>
|
|
|
</div>
|
|
|
- </div>
|
|
|
- <div v-if="selectedProduct === product.id" class="product-check">
|
|
|
+ </template>
|
|
|
+ <div v-else class="empty-member">
|
|
|
<el-icon>
|
|
|
- <Check />
|
|
|
+ <User />
|
|
|
</el-icon>
|
|
|
+ <p>请选择套餐后查看可{{ operationMode === 'assign' ? '分配' : '取消' }}的人员</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
- <div v-else-if="selectedMembers.length > 0" class="empty-assigned">
|
|
|
- <el-icon>
|
|
|
- <InfoFilled />
|
|
|
- </el-icon>
|
|
|
- <p>选中人员暂无已分配的产品</p>
|
|
|
- </div>
|
|
|
- <div v-else class="empty-assigned">
|
|
|
- <el-icon>
|
|
|
- <InfoFilled />
|
|
|
- </el-icon>
|
|
|
- <p>请先选择要操作的人员</p>
|
|
|
- </div>
|
|
|
</div>
|
|
|
+
|
|
|
</div>
|
|
|
|
|
|
<template #footer>
|
|
|
<span class="dialog-footer">
|
|
|
- <el-button size="medium" @click="handleCancel">
|
|
|
+ <el-button size="small" @click="handleCancel">
|
|
|
<el-icon>
|
|
|
<Close />
|
|
|
</el-icon>
|
|
|
<span>取消</span>
|
|
|
</el-button>
|
|
|
- <el-button type="primary" size="medium" :loading="submitting" :disabled="!canSubmit" @click="handleSubmit">
|
|
|
+ <el-button type="primary" size="small" :loading="submitting" :disabled="!canSubmit" @click="handleSubmit">
|
|
|
<el-icon v-if="!submitting">
|
|
|
<component :is="operationMode === 'assign' ? Check : Remove" />
|
|
|
</el-icon>
|
|
|
@@ -223,7 +179,7 @@
|
|
|
|
|
|
<script setup>
|
|
|
import { computed, getCurrentInstance, ref, watch } from 'vue'
|
|
|
-import { Check, Close, Goods, InfoFilled, List, Minus, Plus, Remove, User, UserFilled } from '@element-plus/icons-vue'
|
|
|
+import { Check, Close, Goods, Minus, Plus, Remove, User, UserFilled } from '@element-plus/icons-vue'
|
|
|
|
|
|
defineOptions({ name: 'ProductSeatDialog' })
|
|
|
|
|
|
@@ -251,18 +207,28 @@ const selectedProduct = ref(null)
|
|
|
const submitting = ref(false)
|
|
|
const availableProducts = ref([])
|
|
|
const assignedProducts = ref([])
|
|
|
+const productMembersMap = ref({}) // 存储每个产品下的人员列表
|
|
|
+const currentProductList = computed(() =>
|
|
|
+ operationMode.value === 'assign' ? availableProducts.value : assignedProducts.value
|
|
|
+)
|
|
|
|
|
|
-const dialogVisible = computed({
|
|
|
- get: () => props.visible,
|
|
|
- set: val => emit('update:visible', val)
|
|
|
+// 当前显示的人员列表
|
|
|
+const currentMemberList = computed(() => {
|
|
|
+ if (!selectedProduct.value) return []
|
|
|
+ if (operationMode.value === 'assign') {
|
|
|
+ return props.memberList
|
|
|
+ }
|
|
|
+ return productMembersMap.value[selectedProduct.value] || []
|
|
|
})
|
|
|
|
|
|
const canSubmit = computed(() => {
|
|
|
- if (selectedMembers.value.length === 0) return false
|
|
|
if (operationMode.value === 'assign') {
|
|
|
- return !!selectedProduct.value
|
|
|
+ // 分配模式:需要选择人员且选择产品
|
|
|
+ return selectedMembers.value.length > 0 && !!selectedProduct.value
|
|
|
+ } else {
|
|
|
+ // 取消分配模式:需要选择产品且选择人员
|
|
|
+ return !!selectedProduct.value && selectedMembers.value.length > 0
|
|
|
}
|
|
|
- return assignedProducts.value.length > 0
|
|
|
})
|
|
|
|
|
|
const message = proxy?.$message
|
|
|
@@ -288,31 +254,11 @@ watch(
|
|
|
{ immediate: true, deep: true }
|
|
|
)
|
|
|
|
|
|
-watch(operationMode, newVal => {
|
|
|
- if (newVal === 'cancel' && selectedMembers.value.length > 0) {
|
|
|
- loadAssignedProducts()
|
|
|
- } else {
|
|
|
- assignedProducts.value = []
|
|
|
- }
|
|
|
- selectedProduct.value = null
|
|
|
-})
|
|
|
-
|
|
|
-watch(
|
|
|
- selectedMembers,
|
|
|
- newVal => {
|
|
|
- if (newVal.length > 0 && operationMode.value === 'cancel') {
|
|
|
- loadAssignedProducts()
|
|
|
- } else if (newVal.length === 0) {
|
|
|
- assignedProducts.value = []
|
|
|
- }
|
|
|
- },
|
|
|
- { deep: true }
|
|
|
-)
|
|
|
-
|
|
|
const initDialog = async () => {
|
|
|
selectedMembers.value = props.selectedMemberIds.length > 0 ? [...props.selectedMemberIds] : []
|
|
|
- await loadAvailableProducts()
|
|
|
- if (operationMode.value === 'cancel' && selectedMembers.value.length > 0) {
|
|
|
+ if (operationMode.value === 'assign') {
|
|
|
+ await loadAvailableProducts()
|
|
|
+ } else {
|
|
|
await loadAssignedProducts()
|
|
|
}
|
|
|
}
|
|
|
@@ -333,60 +279,82 @@ const loadAvailableProducts = async () => {
|
|
|
seats: 5
|
|
|
}
|
|
|
]
|
|
|
+ autoSelectFirstProduct()
|
|
|
} catch (error) {
|
|
|
console.error('加载产品列表失败:', error)
|
|
|
message?.error('加载产品列表失败')
|
|
|
availableProducts.value = []
|
|
|
+ selectedProduct.value = null
|
|
|
}
|
|
|
}
|
|
|
|
|
|
const loadAssignedProducts = async () => {
|
|
|
- if (selectedMembers.value.length === 0) {
|
|
|
- assignedProducts.value = []
|
|
|
- selectedProduct.value = null
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
try {
|
|
|
+ // 加载所有已分配的产品列表
|
|
|
assignedProducts.value = [
|
|
|
{
|
|
|
id: 1,
|
|
|
name: '聚德神笔套餐',
|
|
|
description: '聚德 AI 写作/办公套件',
|
|
|
- assignedCount: selectedMembers.value.length
|
|
|
+ assignedCount: 2
|
|
|
}
|
|
|
]
|
|
|
|
|
|
- if (assignedProducts.value.length === 1 && !selectedProduct.value) {
|
|
|
- selectedProduct.value = assignedProducts.value[0].id
|
|
|
- }
|
|
|
+ // 加载每个产品下的人员列表
|
|
|
+ // 这里应该调用API获取每个产品下的人员
|
|
|
+ // 暂时使用模拟数据
|
|
|
+ assignedProducts.value.forEach(product => {
|
|
|
+ // 模拟数据:每个产品下的人员
|
|
|
+ productMembersMap.value[product.id] = props.memberList.filter((member, index) => {
|
|
|
+ // 模拟:产品1包含前2个成员
|
|
|
+ if (product.id === 1) {
|
|
|
+ return index < 2
|
|
|
+ }
|
|
|
+ return false
|
|
|
+ })
|
|
|
+ })
|
|
|
+ autoSelectFirstProduct()
|
|
|
} catch (error) {
|
|
|
console.error('加载已分配产品失败:', error)
|
|
|
message?.error('加载已分配产品失败')
|
|
|
assignedProducts.value = []
|
|
|
+ productMembersMap.value = {}
|
|
|
+ selectedProduct.value = null
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const autoSelectFirstProduct = () => {
|
|
|
+ if (currentProductList.value.length > 0) {
|
|
|
+ selectedProduct.value = currentProductList.value[0].id
|
|
|
+ } else {
|
|
|
selectedProduct.value = null
|
|
|
}
|
|
|
+ selectedMembers.value = []
|
|
|
}
|
|
|
|
|
|
-const handleModeChange = () => {
|
|
|
+// 选择产品时,更新人员列表
|
|
|
+const handleProductSelect = productId => {
|
|
|
+ selectedProduct.value = productId
|
|
|
+ selectedMembers.value = []
|
|
|
+}
|
|
|
+
|
|
|
+const handleModeChange = async () => {
|
|
|
+ // 切换模式时重置状态并重新拉取对应产品列表
|
|
|
selectedProduct.value = null
|
|
|
- if (operationMode.value === 'cancel' && selectedMembers.value.length > 0) {
|
|
|
- loadAssignedProducts()
|
|
|
+ selectedMembers.value = []
|
|
|
+ if (operationMode.value === 'assign') {
|
|
|
+ await loadAvailableProducts()
|
|
|
} else {
|
|
|
- assignedProducts.value = []
|
|
|
+ await loadAssignedProducts()
|
|
|
}
|
|
|
}
|
|
|
|
|
|
const handleMemberChange = () => {
|
|
|
- if (operationMode.value === 'cancel' && selectedMembers.value.length > 0) {
|
|
|
- loadAssignedProducts()
|
|
|
- } else if (selectedMembers.value.length === 0) {
|
|
|
- assignedProducts.value = []
|
|
|
- }
|
|
|
+ // 人员选择变化时的处理
|
|
|
}
|
|
|
|
|
|
const handleSelectAll = () => {
|
|
|
- selectedMembers.value = props.memberList.map(m => m.id)
|
|
|
+ selectedMembers.value = currentMemberList.value.map(m => m.id)
|
|
|
}
|
|
|
|
|
|
const handleSelectNone = () => {
|
|
|
@@ -401,18 +369,18 @@ const handleCancelProduct = product => {
|
|
|
}
|
|
|
|
|
|
const handleSubmit = async () => {
|
|
|
+ if (!selectedProduct.value) {
|
|
|
+ message?.warning(operationMode.value === 'assign' ? '没有可分配的套餐' : '请先选择要取消的套餐')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
if (selectedMembers.value.length === 0) {
|
|
|
message?.warning('请至少选择一个人员')
|
|
|
return
|
|
|
}
|
|
|
|
|
|
- if (operationMode.value === 'assign') {
|
|
|
- if (!selectedProduct.value) {
|
|
|
- message?.warning('请选择要分配的套餐产品')
|
|
|
- return
|
|
|
- }
|
|
|
- } else if (!selectedProduct.value && assignedProducts.value.length === 0) {
|
|
|
- message?.warning('没有可取消分配的产品')
|
|
|
+ if (operationMode.value !== 'assign' && selectedMembers.value.length === 0) {
|
|
|
+ message?.warning('请选择要取消分配的人员')
|
|
|
return
|
|
|
}
|
|
|
|
|
|
@@ -432,7 +400,7 @@ const handleSubmit = async () => {
|
|
|
productId,
|
|
|
memberIds: selectedMembers.value
|
|
|
})
|
|
|
- dialogVisible.value = false
|
|
|
+ emit('update:visible', false)
|
|
|
} catch (error) {
|
|
|
console.error('操作失败:', error)
|
|
|
message?.error(operationMode.value === 'assign' ? '分配席位失败' : '取消分配失败')
|
|
|
@@ -442,7 +410,7 @@ const handleSubmit = async () => {
|
|
|
}
|
|
|
|
|
|
const handleCancel = () => {
|
|
|
- dialogVisible.value = false
|
|
|
+ emit('update:visible', false)
|
|
|
}
|
|
|
|
|
|
const handleClose = () => {
|
|
|
@@ -455,6 +423,7 @@ const resetForm = () => {
|
|
|
selectedMembers.value = []
|
|
|
selectedProduct.value = null
|
|
|
assignedProducts.value = []
|
|
|
+ productMembersMap.value = {}
|
|
|
submitting.value = false
|
|
|
}
|
|
|
</script>
|
|
|
@@ -619,59 +588,73 @@ const resetForm = () => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- .member-list {
|
|
|
- max-height: 300px;
|
|
|
+ .member-list-scroll {
|
|
|
+ max-height: 320px;
|
|
|
overflow-y: auto;
|
|
|
- padding: 4px;
|
|
|
+ overflow-x: hidden;
|
|
|
+ padding: 8px;
|
|
|
border-radius: 12px;
|
|
|
+ background: #fafbfc;
|
|
|
+ border: 1px solid #f0f2f5;
|
|
|
|
|
|
+ // 自定义滚动条样式
|
|
|
&::-webkit-scrollbar {
|
|
|
- width: 6px;
|
|
|
+ width: 8px;
|
|
|
}
|
|
|
|
|
|
&::-webkit-scrollbar-track {
|
|
|
background: #f5f7fa;
|
|
|
- border-radius: 3px;
|
|
|
+ border-radius: 4px;
|
|
|
+ margin: 4px 0;
|
|
|
}
|
|
|
|
|
|
&::-webkit-scrollbar-thumb {
|
|
|
- background: #dcdfe6;
|
|
|
- border-radius: 3px;
|
|
|
+ background: linear-gradient(180deg, #c0c4cc 0%, #909399 100%);
|
|
|
+ border-radius: 4px;
|
|
|
+ transition: background 0.3s ease;
|
|
|
|
|
|
&:hover {
|
|
|
- background: #c0c4cc;
|
|
|
+ background: linear-gradient(180deg, #909399 0%, #606266 100%);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ // Firefox 滚动条样式
|
|
|
+ scrollbar-width: thin;
|
|
|
+ scrollbar-color: #c0c4cc #f5f7fa;
|
|
|
+
|
|
|
+ // 复选框组样式
|
|
|
+ ::v-deep .el-checkbox-group {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
.member-card {
|
|
|
position: relative;
|
|
|
- margin-bottom: 12px;
|
|
|
- border-radius: 12px;
|
|
|
- border: 2px solid #f0f2f5;
|
|
|
+ border-radius: 10px;
|
|
|
+ border: 1.5px solid #e4e7ed;
|
|
|
background: #fff;
|
|
|
- transition: all 0.3s ease;
|
|
|
+ transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
overflow: hidden;
|
|
|
-
|
|
|
- &:last-child {
|
|
|
- margin-bottom: 0;
|
|
|
- }
|
|
|
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
|
|
|
|
|
&:hover {
|
|
|
- border-color: #b3d8ff;
|
|
|
- box-shadow: 0 4px 12px rgba(64, 158, 255, 0.1);
|
|
|
- transform: translateY(-2px);
|
|
|
+ border-color: #409eff;
|
|
|
+ box-shadow: 0 4px 12px rgba(64, 158, 255, 0.15);
|
|
|
+ transform: translateY(-1px);
|
|
|
}
|
|
|
|
|
|
&.selected {
|
|
|
border-color: #409eff;
|
|
|
- background: linear-gradient(135deg, #ecf5ff 0%, #e1f3ff 100%);
|
|
|
- box-shadow: 0 4px 16px rgba(64, 158, 255, 0.2);
|
|
|
+ background: linear-gradient(135deg, #ecf5ff 0%, #d9ecff 100%);
|
|
|
+ box-shadow: 0 4px 16px rgba(64, 158, 255, 0.25);
|
|
|
+ transform: translateY(-1px);
|
|
|
}
|
|
|
|
|
|
::v-deep .member-checkbox {
|
|
|
width: 100%;
|
|
|
margin: 0;
|
|
|
- padding: 16px;
|
|
|
+ padding: 30px 14px;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
cursor: pointer;
|
|
|
@@ -685,59 +668,74 @@ const resetForm = () => {
|
|
|
padding: 0;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
+ gap: 12px;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.member-avatar {
|
|
|
- width: 48px;
|
|
|
- height: 48px;
|
|
|
- border-radius: 12px;
|
|
|
+ width: 44px;
|
|
|
+ height: 44px;
|
|
|
+ border-radius: 10px;
|
|
|
background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%);
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
- margin-right: 16px;
|
|
|
flex-shrink: 0;
|
|
|
- box-shadow: 0 2px 8px rgba(64, 158, 255, 0.3);
|
|
|
+ box-shadow: 0 2px 6px rgba(64, 158, 255, 0.25);
|
|
|
+ transition: transform 0.2s ease;
|
|
|
|
|
|
i {
|
|
|
- font-size: 24px;
|
|
|
+ font-size: 22px;
|
|
|
color: #fff;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ .member-card:hover .member-avatar {
|
|
|
+ transform: scale(1.05);
|
|
|
+ }
|
|
|
+
|
|
|
+ .member-card.selected .member-avatar {
|
|
|
+ box-shadow: 0 3px 10px rgba(64, 158, 255, 0.4);
|
|
|
+ }
|
|
|
+
|
|
|
.member-details {
|
|
|
flex: 1;
|
|
|
min-width: 0;
|
|
|
+ overflow: hidden;
|
|
|
|
|
|
.member-name {
|
|
|
font-size: 15px;
|
|
|
font-weight: 600;
|
|
|
color: #303133;
|
|
|
margin-bottom: 4px;
|
|
|
+ white-space: nowrap;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
}
|
|
|
|
|
|
.member-email {
|
|
|
font-size: 13px;
|
|
|
color: #909399;
|
|
|
- word-break: break-all;
|
|
|
+ white-space: nowrap;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.member-check-icon {
|
|
|
- width: 32px;
|
|
|
- height: 32px;
|
|
|
+ width: 28px;
|
|
|
+ height: 28px;
|
|
|
border-radius: 50%;
|
|
|
background: linear-gradient(135deg, #67c23a 0%, #85ce61 100%);
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
- margin-left: 12px;
|
|
|
flex-shrink: 0;
|
|
|
- animation: scaleIn 0.3s ease;
|
|
|
+ box-shadow: 0 2px 6px rgba(103, 194, 58, 0.3);
|
|
|
+ animation: scaleIn 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
|
|
|
i {
|
|
|
- font-size: 18px;
|
|
|
+ font-size: 16px;
|
|
|
color: #fff;
|
|
|
}
|
|
|
}
|
|
|
@@ -748,17 +746,22 @@ const resetForm = () => {
|
|
|
flex-direction: column;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
- padding: 60px 20px;
|
|
|
+ padding: 50px 20px;
|
|
|
color: #c0c4cc;
|
|
|
+ background: #fafbfc;
|
|
|
+ border-radius: 8px;
|
|
|
+ border: 1px dashed #e4e7ed;
|
|
|
|
|
|
i {
|
|
|
font-size: 48px;
|
|
|
margin-bottom: 12px;
|
|
|
+ opacity: 0.6;
|
|
|
}
|
|
|
|
|
|
p {
|
|
|
margin: 0;
|
|
|
font-size: 14px;
|
|
|
+ color: #909399;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -932,129 +935,6 @@ const resetForm = () => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // 已分配产品区域
|
|
|
- .assigned-section {
|
|
|
- .assigned-product-list {
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- gap: 12px;
|
|
|
-
|
|
|
- .assigned-product-card {
|
|
|
- position: relative;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- padding: 16px 20px;
|
|
|
- border-radius: 12px;
|
|
|
- border: 2px solid #f0f2f5;
|
|
|
- background: #fff;
|
|
|
- cursor: pointer;
|
|
|
- transition: all 0.3s ease;
|
|
|
-
|
|
|
- &:hover {
|
|
|
- border-color: #f56c6c;
|
|
|
- box-shadow: 0 4px 12px rgba(245, 108, 108, 0.1);
|
|
|
- transform: translateX(4px);
|
|
|
- }
|
|
|
-
|
|
|
- &.active {
|
|
|
- border-color: #f56c6c;
|
|
|
- background: linear-gradient(135deg, #fef0f0 0%, #fde2e2 100%);
|
|
|
- box-shadow: 0 4px 16px rgba(245, 108, 108, 0.2);
|
|
|
- }
|
|
|
-
|
|
|
- .product-icon {
|
|
|
- width: 48px;
|
|
|
- height: 48px;
|
|
|
- border-radius: 10px;
|
|
|
- background: linear-gradient(135deg, #f56c6c 0%, #f78989 100%);
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- margin-right: 16px;
|
|
|
- flex-shrink: 0;
|
|
|
- box-shadow: 0 2px 8px rgba(245, 108, 108, 0.3);
|
|
|
-
|
|
|
- i {
|
|
|
- font-size: 24px;
|
|
|
- color: #fff;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- .product-content {
|
|
|
- flex: 1;
|
|
|
- min-width: 0;
|
|
|
-
|
|
|
- .product-name {
|
|
|
- font-size: 15px;
|
|
|
- font-weight: 600;
|
|
|
- color: #303133;
|
|
|
- margin-bottom: 4px;
|
|
|
- }
|
|
|
-
|
|
|
- .product-desc {
|
|
|
- font-size: 13px;
|
|
|
- color: #909399;
|
|
|
- margin-bottom: 8px;
|
|
|
- }
|
|
|
-
|
|
|
- .product-meta {
|
|
|
- .assigned-count {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 4px;
|
|
|
- font-size: 13px;
|
|
|
- color: #f56c6c;
|
|
|
- font-weight: 500;
|
|
|
-
|
|
|
- i {
|
|
|
- font-size: 14px;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- .product-check {
|
|
|
- width: 28px;
|
|
|
- height: 28px;
|
|
|
- border-radius: 50%;
|
|
|
- background: linear-gradient(135deg, #67c23a 0%, #85ce61 100%);
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- flex-shrink: 0;
|
|
|
- box-shadow: 0 2px 8px rgba(103, 194, 58, 0.3);
|
|
|
- animation: scaleIn 0.3s ease;
|
|
|
-
|
|
|
- i {
|
|
|
- font-size: 16px;
|
|
|
- color: #fff;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- .empty-assigned {
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- padding: 60px 20px;
|
|
|
- color: #c0c4cc;
|
|
|
- background: #fafafa;
|
|
|
- border-radius: 12px;
|
|
|
- border: 2px dashed #e4e7ed;
|
|
|
-
|
|
|
- i {
|
|
|
- font-size: 48px;
|
|
|
- margin-bottom: 12px;
|
|
|
- }
|
|
|
-
|
|
|
- p {
|
|
|
- margin: 0;
|
|
|
- font-size: 14px;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
}
|
|
|
|
|
|
@keyframes scaleIn {
|
|
|
@@ -1108,3 +988,4 @@ const resetForm = () => {
|
|
|
}
|
|
|
}
|
|
|
</style>
|
|
|
+
|