| New file |
| | |
| | | <template> |
| | | <view class="staff-selector-component"> |
| | | <view class="form-item"> |
| | | <view class="form-label" :class="{ required: required }">{{ label }}</view> |
| | | <view class="staff-list"> |
| | | <view class="staff-item" v-for="(staff, index) in selectedStaff" :key="staff.userId"> |
| | | <view class="staff-info"> |
| | | <text class="staff-name">{{ staff.nickName }}</text> |
| | | <view class="staff-roles"> |
| | | <text class="staff-role" v-for="(roleType, idx) in staff.types" :key="idx"> |
| | | {{ getUserTypeName(roleType) }} |
| | | </text> |
| | | </view> |
| | | </view> |
| | | <uni-icons |
| | | v-if="canRemove(index)" |
| | | type="closeempty" |
| | | size="20" |
| | | color="#ff4d4f" |
| | | @click="removeStaff(index)" |
| | | ></uni-icons> |
| | | <uni-icons |
| | | v-else |
| | | type="checkmarkempty" |
| | | size="20" |
| | | color="#007AFF" |
| | | ></uni-icons> |
| | | </view> |
| | | <view class="add-staff" @click="showStaffSelector"> |
| | | <uni-icons type="plusempty" size="20" color="#007AFF"></uni-icons> |
| | | <text>添加人员</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 人员选择弹窗 --> |
| | | <uni-popup ref="staffPopup" type="bottom" :safe-area="true"> |
| | | <view class="staff-selector-popup"> |
| | | <view class="popup-header"> |
| | | <view class="popup-title">选择执行人员</view> |
| | | <view class="popup-close" @click="closeStaffSelector"> |
| | | <uni-icons type="closeempty" size="24" color="#333"></uni-icons> |
| | | </view> |
| | | </view> |
| | | |
| | | <view class="search-box"> |
| | | <uni-icons type="search" size="18" color="#999"></uni-icons> |
| | | <input |
| | | class="search-input" |
| | | placeholder="搜索姓名、手机号" |
| | | v-model="staffSearchKeyword" |
| | | @input="onStaffSearch" |
| | | /> |
| | | </view> |
| | | |
| | | <view class="staff-filter"> |
| | | <view |
| | | class="filter-item" |
| | | :class="{ active: staffFilterType === 'all' }" |
| | | @click="filterStaff('all')" |
| | | >全部</view> |
| | | <view |
| | | class="filter-item" |
| | | :class="{ active: staffFilterType === 'driver' }" |
| | | @click="filterStaff('driver')" |
| | | >司机</view> |
| | | <view |
| | | class="filter-item" |
| | | :class="{ active: staffFilterType === 'doctor' }" |
| | | @click="filterStaff('doctor')" |
| | | >医生</view> |
| | | <view |
| | | class="filter-item" |
| | | :class="{ active: staffFilterType === 'nurse' }" |
| | | @click="filterStaff('nurse')" |
| | | >护士</view> |
| | | </view> |
| | | |
| | | <scroll-view class="staff-list-popup" scroll-y="true"> |
| | | <view |
| | | class="staff-item-popup" |
| | | v-for="staff in filteredStaffList" |
| | | :key="staff.userId" |
| | | @click="toggleStaffSelection(staff)" |
| | | > |
| | | <view class="staff-info"> |
| | | <view class="staff-name-row"> |
| | | <text class="staff-name">{{ staff.nickName }}</text> |
| | | <text class="staff-phone">{{ staff.phonenumber }}</text> |
| | | </view> |
| | | <view class="staff-detail-row"> |
| | | <text class="staff-dept">{{ staff.deptName }}</text> |
| | | <view class="staff-types"> |
| | | <text |
| | | class="type-tag" |
| | | :class="'type-' + type" |
| | | v-for="(type, idx) in staff.types" |
| | | :key="idx" |
| | | > |
| | | {{ getUserTypeName(type) }} |
| | | </text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <uni-icons |
| | | v-if="isStaffSelected(staff.userId)" |
| | | type="checkmarkempty" |
| | | size="24" |
| | | color="#007AFF" |
| | | ></uni-icons> |
| | | <view v-else class="checkbox-empty"></view> |
| | | </view> |
| | | |
| | | <view class="no-data" v-if="filteredStaffList.length === 0"> |
| | | <uni-icons type="info" size="40" color="#ccc"></uni-icons> |
| | | <text>暂无人员数据</text> |
| | | </view> |
| | | </scroll-view> |
| | | |
| | | <view class="popup-footer"> |
| | | <button class="cancel-btn" @click="closeStaffSelector">取消</button> |
| | | <button class="confirm-btn" @click="confirmStaffSelection">确定(已选{{ selectedStaff.length }})</button> |
| | | </view> |
| | | </view> |
| | | </uni-popup> |
| | | </view> |
| | | </template> |
| | | |
| | | <script> |
| | | import { mapState } from 'vuex' |
| | | import uniPopup from '@/uni_modules/uni-popup/components/uni-popup/uni-popup.vue' |
| | | import { listBranchUsers } from "@/api/system/user" |
| | | |
| | | export default { |
| | | name: 'StaffSelector', |
| | | components: { |
| | | uniPopup |
| | | }, |
| | | props: { |
| | | // 已选择的人员列表 |
| | | value: { |
| | | type: Array, |
| | | default: () => [] |
| | | }, |
| | | // 标签文本 |
| | | label: { |
| | | type: String, |
| | | default: '执行任务人员' |
| | | }, |
| | | // 是否必填 |
| | | required: { |
| | | type: Boolean, |
| | | default: false |
| | | }, |
| | | // 是否自动添加当前用户 |
| | | autoAddCurrentUser: { |
| | | type: Boolean, |
| | | default: true |
| | | }, |
| | | // 当前用户是否可移除 |
| | | currentUserRemovable: { |
| | | type: Boolean, |
| | | default: false |
| | | } |
| | | }, |
| | | data() { |
| | | return { |
| | | selectedStaff: [], |
| | | allStaffList: [], |
| | | filteredStaffList: [], |
| | | staffSearchKeyword: '', |
| | | staffFilterType: 'all' |
| | | } |
| | | }, |
| | | computed: { |
| | | ...mapState({ |
| | | currentUser: state => ({ |
| | | userId: state.user.userId, |
| | | nickName: state.user.nickName || '张三', |
| | | phonenumber: state.user.phonenumber || '', |
| | | deptId: state.user.deptId || 100, |
| | | posts: state.user.posts || [], |
| | | roles: state.user.roles || [], |
| | | dept: state.user.dept || null |
| | | }) |
| | | }) |
| | | }, |
| | | watch: { |
| | | value: { |
| | | handler(newVal) { |
| | | if (newVal && Array.isArray(newVal)) { |
| | | this.selectedStaff = [...newVal] |
| | | } |
| | | }, |
| | | immediate: true, |
| | | deep: true |
| | | } |
| | | }, |
| | | mounted() { |
| | | this.loadStaffList() |
| | | |
| | | // 如果需要自动添加当前用户且选中人员为空 |
| | | if (this.autoAddCurrentUser && this.selectedStaff.length === 0) { |
| | | this.initWithCurrentUser() |
| | | } |
| | | }, |
| | | methods: { |
| | | // 初始化选中的人员(默认包含当前用户) |
| | | initWithCurrentUser() { |
| | | const currentUserStaff = { |
| | | userId: this.currentUser.userId, |
| | | nickName: this.currentUser.nickName, |
| | | phonenumber: this.currentUser.phonenumber, |
| | | deptId: this.currentUser.deptId, |
| | | posts: this.currentUser.posts || [], |
| | | roles: this.currentUser.roles || [], |
| | | dept: this.currentUser.dept || null |
| | | } |
| | | |
| | | // 获取当前用户的所有角色类型(可能有多个) |
| | | currentUserStaff.types = this.getUserTypes(currentUserStaff) |
| | | currentUserStaff.type = currentUserStaff.types[0] || 'driver' // 主要类型 |
| | | |
| | | this.selectedStaff = [currentUserStaff] |
| | | this.emitChange() |
| | | }, |
| | | |
| | | // 加载人员列表 |
| | | loadStaffList() { |
| | | listBranchUsers().then(response => { |
| | | const userList = response.data || [] |
| | | |
| | | this.allStaffList = userList.map(user => ({ |
| | | userId: user.userId, |
| | | nickName: user.nickName, |
| | | phonenumber: user.phonenumber, |
| | | deptName: user.dept?.deptName || '', |
| | | postName: user.posts && user.posts.length > 0 ? user.posts[0].postName : '', |
| | | roleName: user.roles && user.roles.length > 0 ? user.roles[0].roleName : '', |
| | | posts: user.posts || [], |
| | | roles: user.roles || [], |
| | | dept: user.dept || null, |
| | | // 支持多种类型 |
| | | types: this.getUserTypes(user), |
| | | type: this.getUserTypes(user)[0] || 'driver' // 主要类型(用于向后兼容) |
| | | })) |
| | | |
| | | this.filterStaffList() |
| | | }).catch(error => { |
| | | console.error('加载人员列表失败:', error) |
| | | this.$modal.showToast('加载人员列表失败') |
| | | }) |
| | | }, |
| | | |
| | | // 根据用户的岗位或角色判断所有类型(支持多种身份) |
| | | getUserTypes(user) { |
| | | const types = [] |
| | | const postNames = user.posts ? user.posts.map(p => p.postName).join('') : '' |
| | | const roleNames = user.roles ? user.roles.map(r => r.roleName).join('') : '' |
| | | const deptName = user.dept?.deptName || '' |
| | | |
| | | // 判断是否为司机 |
| | | if (postNames.includes('司机') || roleNames.includes('司机') || |
| | | deptName.includes('车队') || deptName.includes('司机')) { |
| | | types.push('driver') |
| | | } |
| | | |
| | | // 判断是否为医生 |
| | | if (postNames.includes('医生') || roleNames.includes('医生') || |
| | | deptName.includes('医生') || deptName.includes('医护')) { |
| | | types.push('doctor') |
| | | } |
| | | |
| | | // 判断是否为护士 |
| | | if (postNames.includes('护士') || roleNames.includes('护士') || deptName.includes('护士')) { |
| | | types.push('nurse') |
| | | } |
| | | |
| | | // 如果没有匹配到任何类型,默认为司机 |
| | | if (types.length === 0) { |
| | | types.push('driver') |
| | | } |
| | | |
| | | return types |
| | | }, |
| | | |
| | | // 获取类型名称 |
| | | getUserTypeName(staffType) { |
| | | const typeMap = { |
| | | 'driver': '司机', |
| | | 'doctor': '医生', |
| | | 'nurse': '护士' |
| | | } |
| | | return typeMap[staffType] || staffType || '司机' |
| | | }, |
| | | |
| | | // 显示人员选择弹窗 |
| | | showStaffSelector() { |
| | | this.$refs.staffPopup.open() |
| | | this.filterStaffList() |
| | | }, |
| | | |
| | | // 关闭人员选择弹窗 |
| | | closeStaffSelector() { |
| | | this.$refs.staffPopup.close() |
| | | this.staffSearchKeyword = '' |
| | | this.staffFilterType = 'all' |
| | | }, |
| | | |
| | | // 人员搜索 |
| | | onStaffSearch(e) { |
| | | this.staffSearchKeyword = e.detail.value |
| | | this.filterStaffList() |
| | | }, |
| | | |
| | | // 筛选人员类型 |
| | | filterStaff(type) { |
| | | this.staffFilterType = type |
| | | this.filterStaffList() |
| | | }, |
| | | |
| | | // 过滤人员列表 |
| | | filterStaffList() { |
| | | let list = [...this.allStaffList] |
| | | |
| | | // 按类型过滤(支持多类型) |
| | | if (this.staffFilterType !== 'all') { |
| | | list = list.filter(staff => staff.types.includes(this.staffFilterType)) |
| | | } |
| | | |
| | | // 按关键词搜索 |
| | | if (this.staffSearchKeyword && this.staffSearchKeyword.trim() !== '') { |
| | | const keyword = this.staffSearchKeyword.trim().toLowerCase() |
| | | list = list.filter(staff => { |
| | | return staff.nickName.toLowerCase().includes(keyword) || |
| | | (staff.phonenumber && staff.phonenumber.includes(keyword)) |
| | | }) |
| | | } |
| | | |
| | | this.filteredStaffList = list |
| | | }, |
| | | |
| | | // 切换人员选中状态 |
| | | toggleStaffSelection(staff) { |
| | | const index = this.selectedStaff.findIndex(s => s.userId === staff.userId) |
| | | |
| | | if (index > -1) { |
| | | // 如果是第一个且不可移除(当前用户),不允许移除 |
| | | if (!this.canRemove(index)) { |
| | | this.$modal.showToast('当前用户不能移除') |
| | | return |
| | | } |
| | | // 已选中,移除 |
| | | this.selectedStaff.splice(index, 1) |
| | | } else { |
| | | // 未选中,添加 |
| | | this.selectedStaff.push(staff) |
| | | } |
| | | }, |
| | | |
| | | // 判断人员是否已选中 |
| | | isStaffSelected(userId) { |
| | | return this.selectedStaff.some(staff => staff.userId === userId) |
| | | }, |
| | | |
| | | // 判断是否可以移除 |
| | | canRemove(index) { |
| | | // 如果是第一个且当前用户不可移除 |
| | | if (index === 0 && !this.currentUserRemovable) { |
| | | return false |
| | | } |
| | | return true |
| | | }, |
| | | |
| | | // 确认人员选择 |
| | | confirmStaffSelection() { |
| | | if (this.selectedStaff.length === 0) { |
| | | this.$modal.showToast('请至少选择一名人员') |
| | | return |
| | | } |
| | | this.emitChange() |
| | | this.closeStaffSelector() |
| | | }, |
| | | |
| | | // 移除人员 |
| | | removeStaff(index) { |
| | | if (!this.canRemove(index)) { |
| | | this.$modal.showToast('当前用户不能移除') |
| | | return |
| | | } |
| | | this.selectedStaff.splice(index, 1) |
| | | this.emitChange() |
| | | }, |
| | | |
| | | // 触发change事件 |
| | | emitChange() { |
| | | this.$emit('input', this.selectedStaff) |
| | | this.$emit('change', this.selectedStaff) |
| | | } |
| | | } |
| | | } |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | .staff-selector-component { |
| | | .form-item { |
| | | margin-bottom: 40rpx; |
| | | |
| | | .form-label { |
| | | font-size: 28rpx; |
| | | margin-bottom: 15rpx; |
| | | color: #333; |
| | | |
| | | &.required::before { |
| | | content: '*'; |
| | | color: #ff4d4f; |
| | | margin-right: 4rpx; |
| | | font-weight: bold; |
| | | } |
| | | } |
| | | |
| | | .staff-list { |
| | | .staff-item { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | padding: 20rpx; |
| | | background-color: #f9f9f9; |
| | | border-radius: 10rpx; |
| | | margin-bottom: 20rpx; |
| | | |
| | | .staff-info { |
| | | flex: 1; |
| | | |
| | | .staff-name { |
| | | font-size: 28rpx; |
| | | color: #333; |
| | | margin-right: 10rpx; |
| | | display: block; |
| | | margin-bottom: 8rpx; |
| | | } |
| | | |
| | | .staff-roles { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 8rpx; |
| | | |
| | | .staff-role { |
| | | font-size: 22rpx; |
| | | color: white; |
| | | background-color: #007AFF; |
| | | padding: 4rpx 12rpx; |
| | | border-radius: 6rpx; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | .add-staff { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | padding: 20rpx; |
| | | border: 1rpx dashed #007AFF; |
| | | border-radius: 10rpx; |
| | | color: #007AFF; |
| | | |
| | | text { |
| | | margin-left: 10rpx; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 人员选择弹窗样式 |
| | | .staff-selector-popup { |
| | | background-color: white; |
| | | border-radius: 20rpx 20rpx 0 0; |
| | | max-height: 80vh; |
| | | display: flex; |
| | | flex-direction: column; |
| | | |
| | | .popup-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | padding: 30rpx; |
| | | border-bottom: 1rpx solid #f0f0f0; |
| | | flex-shrink: 0; |
| | | |
| | | .popup-title { |
| | | font-size: 32rpx; |
| | | font-weight: bold; |
| | | color: #333; |
| | | } |
| | | |
| | | .popup-close { |
| | | padding: 10rpx; |
| | | } |
| | | } |
| | | |
| | | .search-box { |
| | | display: flex; |
| | | align-items: center; |
| | | margin: 20rpx 30rpx; |
| | | padding: 15rpx 20rpx; |
| | | background-color: #f5f5f5; |
| | | border-radius: 10rpx; |
| | | flex-shrink: 0; |
| | | |
| | | .search-input { |
| | | flex: 1; |
| | | margin-left: 10rpx; |
| | | font-size: 28rpx; |
| | | } |
| | | } |
| | | |
| | | .staff-filter { |
| | | display: flex; |
| | | padding: 0 30rpx 20rpx; |
| | | gap: 15rpx; |
| | | flex-shrink: 0; |
| | | |
| | | .filter-item { |
| | | flex: 1; |
| | | text-align: center; |
| | | padding: 15rpx 0; |
| | | background-color: #f5f5f5; |
| | | border-radius: 10rpx; |
| | | font-size: 26rpx; |
| | | color: #666; |
| | | |
| | | &.active { |
| | | background-color: #007AFF; |
| | | color: white; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .staff-list-popup { |
| | | flex: 1; |
| | | overflow-y: auto; |
| | | padding: 0 30rpx; |
| | | |
| | | .staff-item-popup { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | padding: 25rpx 20rpx; |
| | | border-bottom: 1rpx solid #f0f0f0; |
| | | |
| | | &:active { |
| | | background-color: #f5f5f5; |
| | | } |
| | | |
| | | .staff-info { |
| | | flex: 1; |
| | | |
| | | .staff-name-row { |
| | | display: flex; |
| | | align-items: center; |
| | | margin-bottom: 10rpx; |
| | | |
| | | .staff-name { |
| | | font-size: 30rpx; |
| | | font-weight: bold; |
| | | color: #333; |
| | | margin-right: 20rpx; |
| | | } |
| | | |
| | | .staff-phone { |
| | | font-size: 24rpx; |
| | | color: #999; |
| | | } |
| | | } |
| | | |
| | | .staff-detail-row { |
| | | display: flex; |
| | | align-items: center; |
| | | flex-wrap: wrap; |
| | | |
| | | .staff-dept { |
| | | font-size: 24rpx; |
| | | color: #666; |
| | | margin-right: 15rpx; |
| | | } |
| | | |
| | | .staff-types { |
| | | display: flex; |
| | | gap: 8rpx; |
| | | |
| | | .type-tag { |
| | | font-size: 20rpx; |
| | | color: white; |
| | | padding: 4rpx 10rpx; |
| | | border-radius: 6rpx; |
| | | |
| | | &.type-driver { |
| | | background-color: #007AFF; |
| | | } |
| | | |
| | | &.type-doctor { |
| | | background-color: #34C759; |
| | | } |
| | | |
| | | &.type-nurse { |
| | | background-color: #AF52DE; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | .checkbox-empty { |
| | | width: 40rpx; |
| | | height: 40rpx; |
| | | border: 2rpx solid #ddd; |
| | | border-radius: 50%; |
| | | } |
| | | } |
| | | |
| | | .no-data { |
| | | text-align: center; |
| | | padding: 100rpx 0; |
| | | color: #999; |
| | | |
| | | text { |
| | | display: block; |
| | | margin-top: 20rpx; |
| | | font-size: 28rpx; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .popup-footer { |
| | | display: flex; |
| | | padding: 20rpx 30rpx; |
| | | border-top: 1rpx solid #f0f0f0; |
| | | gap: 20rpx; |
| | | flex-shrink: 0; |
| | | |
| | | button { |
| | | flex: 1; |
| | | height: 80rpx; |
| | | border-radius: 10rpx; |
| | | font-size: 30rpx; |
| | | } |
| | | |
| | | .cancel-btn { |
| | | background-color: #f5f5f5; |
| | | color: #666; |
| | | } |
| | | |
| | | .confirm-btn { |
| | | background-color: #007AFF; |
| | | color: white; |
| | | } |
| | | } |
| | | } |
| | | </style> |