<!--
|
OA 通用:可搜索的用户单选弹层(点选即确认)
|
-->
|
<template>
|
<up-popup :show="show"
|
mode="bottom"
|
round="16"
|
:safe-area-inset-bottom="true"
|
@close="emit('update:show', false)">
|
<view class="oa-user-sheet">
|
<view class="sheet-handle" />
|
<view class="sheet-head">
|
<text class="sheet-cancel"
|
@click="emit('update:show', false)">取消</text>
|
<text class="sheet-title">{{ title }}</text>
|
<text class="sheet-spacer" />
|
</view>
|
|
<view class="sheet-search">
|
<up-search v-model="keyword"
|
placeholder="搜索姓名或工号"
|
:show-action="false"
|
shape="round"
|
bg-color="#f5f7fa" />
|
</view>
|
|
<view v-if="selfUser && showSelfQuick"
|
class="self-quick"
|
@click="pickUser(selfUser)">
|
<view class="user-avatar"
|
:style="{ backgroundColor: avatarColor(selfUser.nickName || selfUser.userName) }">
|
{{ (selfUser.nickName || selfUser.userName || "我").charAt(0) }}
|
</view>
|
<view class="user-meta">
|
<text class="user-name">选本人 · {{ userSelectLabel(selfUser) }}</text>
|
<text class="user-sub">{{ userSubLabel(selfUser) }}</text>
|
</view>
|
<up-icon name="arrow-right"
|
size="14"
|
color="#c0c4cc" />
|
</view>
|
|
<scroll-view scroll-y
|
class="user-scroll"
|
:show-scrollbar="false">
|
<view v-for="u in filteredList"
|
:key="String(u.userId ?? u.id)"
|
class="user-item"
|
:class="{ selected: isSelected(u) }"
|
@click="pickUser(u)">
|
<view class="user-avatar"
|
:style="{ backgroundColor: avatarColor(u.nickName || u.userName) }">
|
{{ (u.nickName || u.userName || "?").charAt(0) }}
|
</view>
|
<view class="user-meta">
|
<text class="user-name">{{ userSelectLabel(u) }}</text>
|
<text class="user-sub">{{ userSubLabel(u) }}</text>
|
</view>
|
<view class="user-check"
|
:class="{ checked: isSelected(u) }">
|
<up-icon v-if="isSelected(u)"
|
name="checkmark"
|
size="14"
|
color="#fff" />
|
</view>
|
</view>
|
<view v-if="!filteredList.length"
|
class="user-empty">
|
<up-empty mode="search"
|
text="暂无匹配用户" />
|
</view>
|
</scroll-view>
|
</view>
|
</up-popup>
|
</template>
|
|
<script setup>
|
import { computed, ref, watch } from "vue";
|
import useUserStore from "@/store/modules/user";
|
import {
|
filterActiveUsers,
|
userAvatarColor,
|
userSelectLabel,
|
userSubLabel,
|
} from "../_utils/userPickerUtils.js";
|
|
const props = defineProps({
|
show: { type: Boolean, default: false },
|
title: { type: String, default: "选择员工" },
|
users: { type: Array, default: () => [] },
|
modelValue: { type: [String, Number], default: "" },
|
showSelfQuick: { type: Boolean, default: true },
|
});
|
|
const emit = defineEmits(["update:show", "update:modelValue", "select"]);
|
|
const keyword = ref("");
|
const userStore = useUserStore();
|
|
const filteredList = computed(() =>
|
filterActiveUsers(props.users, keyword.value, 100)
|
);
|
|
const selfUser = computed(() => {
|
const id = userStore.id;
|
if (!id) return null;
|
const hit = props.users.find(u => String(u.userId ?? u.id) === String(id));
|
if (hit) return hit;
|
return {
|
userId: id,
|
nickName: userStore.nickName,
|
userName: userStore.name,
|
};
|
});
|
|
watch(
|
() => props.show,
|
v => {
|
if (v) keyword.value = "";
|
}
|
);
|
|
function avatarColor(name) {
|
return userAvatarColor(name);
|
}
|
|
function isSelected(u) {
|
const id = u.userId ?? u.id;
|
return id != null && String(id) === String(props.modelValue ?? "");
|
}
|
|
function pickUser(u) {
|
const id = u.userId ?? u.id;
|
emit("update:modelValue", id);
|
emit("select", u);
|
emit("update:show", false);
|
}
|
</script>
|
|
<style scoped lang="scss">
|
.oa-user-sheet {
|
background: #fff;
|
border-radius: 16px 16px 0 0;
|
max-height: 78vh;
|
display: flex;
|
flex-direction: column;
|
}
|
.sheet-handle {
|
width: 36px;
|
height: 4px;
|
background: #e4e7ed;
|
border-radius: 2px;
|
margin: 8px auto 4px;
|
}
|
.sheet-head {
|
display: flex;
|
align-items: center;
|
padding: 8px 16px 12px;
|
}
|
.sheet-cancel {
|
font-size: 15px;
|
color: #909399;
|
min-width: 48px;
|
}
|
.sheet-title {
|
flex: 1;
|
text-align: center;
|
font-size: 16px;
|
font-weight: 600;
|
color: #303133;
|
}
|
.sheet-spacer {
|
min-width: 48px;
|
}
|
.sheet-search {
|
padding: 0 16px 10px;
|
}
|
.self-quick {
|
display: flex;
|
align-items: center;
|
margin: 0 16px 8px;
|
padding: 12px;
|
background: linear-gradient(135deg, #ecf5ff 0%, #f0f9ff 100%);
|
border-radius: 12px;
|
border: 1px solid #d9ecff;
|
}
|
.user-scroll {
|
flex: 1;
|
max-height: 52vh;
|
padding: 0 8px 16px;
|
box-sizing: border-box;
|
}
|
.user-item,
|
.self-quick {
|
&:active {
|
opacity: 0.85;
|
}
|
}
|
.user-item {
|
display: flex;
|
align-items: center;
|
padding: 12px 10px;
|
border-radius: 10px;
|
margin-bottom: 4px;
|
&.selected {
|
background: #f0f7ff;
|
}
|
}
|
.user-avatar {
|
width: 40px;
|
height: 40px;
|
border-radius: 50%;
|
color: #fff;
|
font-size: 16px;
|
font-weight: 600;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
flex-shrink: 0;
|
}
|
.user-meta {
|
flex: 1;
|
margin-left: 12px;
|
min-width: 0;
|
}
|
.user-name {
|
display: block;
|
font-size: 15px;
|
color: #303133;
|
font-weight: 500;
|
overflow: hidden;
|
text-overflow: ellipsis;
|
white-space: nowrap;
|
}
|
.user-sub {
|
display: block;
|
font-size: 12px;
|
color: #909399;
|
margin-top: 2px;
|
overflow: hidden;
|
text-overflow: ellipsis;
|
white-space: nowrap;
|
}
|
.user-check {
|
width: 22px;
|
height: 22px;
|
border-radius: 50%;
|
border: 2px solid #dcdfe6;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
flex-shrink: 0;
|
&.checked {
|
background: #2979ff;
|
border-color: #2979ff;
|
}
|
}
|
.user-empty {
|
padding: 24px 0;
|
}
|
</style>
|