feat: 钉钉、企业微信webbook、企业微信应用api,问题:交互
This commit is contained in:
170
apps/web-antd/src/components/form/components/user-tag-select.vue
Normal file
170
apps/web-antd/src/components/form/components/user-tag-select.vue
Normal file
@@ -0,0 +1,170 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 通用 Tag 选择器(带头像)
|
||||
*
|
||||
* - 基于 antd Select,multiple 模式默认开启,单选传 multiple=false
|
||||
* - 已选项以 Tag 形式展示头像+名字:
|
||||
* - 有 avatar 用图片头像
|
||||
* - 无 avatar 用文本头像(取 label 首字符)
|
||||
* - 下拉选项同样展示头像+名字
|
||||
* - 选项数据:options: { value, label, avatar? }[]
|
||||
*/
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { Avatar, Select, Tag } from 'ant-design-vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'UserTagSelect',
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
/** 单个选项结构 */
|
||||
export interface UserTagOption {
|
||||
value: string | number;
|
||||
label: string;
|
||||
avatar?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
/** 已选值数组(多选)或单值(单选时仍用数组包装) */
|
||||
modelValue?: (string | number)[];
|
||||
/** 选项列表 */
|
||||
options: UserTagOption[];
|
||||
/** 是否多选,默认 true */
|
||||
multiple?: boolean;
|
||||
/** 最大选择数,超出截断 */
|
||||
maxCount?: number;
|
||||
placeholder?: string;
|
||||
allowClear?: boolean;
|
||||
showSearch?: boolean;
|
||||
disabled?: boolean;
|
||||
}>(),
|
||||
{
|
||||
modelValue: () => [],
|
||||
multiple: true,
|
||||
maxCount: 99,
|
||||
placeholder: '请选择',
|
||||
allowClear: true,
|
||||
showSearch: true,
|
||||
disabled: false,
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [v: (string | number)[]];
|
||||
}>();
|
||||
|
||||
// 双向绑定:useVModel 自动处理 modelValue 的读写
|
||||
const innerValue = useVModel(props, 'modelValue', emit, {
|
||||
defaultValue: [] as (string | number)[],
|
||||
passive: true,
|
||||
});
|
||||
|
||||
/** 用于 Select 的 mode,单选时不传 mode */
|
||||
const selectMode = computed(() => (props.multiple ? 'multiple' : undefined));
|
||||
|
||||
/** 按 value 索引选项,方便 tagRender/option 插槽快速查头像与名字 */
|
||||
const optionMap = computed(() => {
|
||||
const map = new Map<string | number, UserTagOption>();
|
||||
for (const opt of props.options) {
|
||||
map.set(opt.value, opt);
|
||||
}
|
||||
return map;
|
||||
});
|
||||
|
||||
/** 取某 value 对应的展示名 */
|
||||
function labelOf(value: string | number) {
|
||||
return optionMap.value.get(value)?.label || String(value);
|
||||
}
|
||||
|
||||
/** 取某 value 对应的头像 */
|
||||
function avatarOf(value: string | number) {
|
||||
return optionMap.value.get(value)?.avatar;
|
||||
}
|
||||
|
||||
/** 取首字符(文本头像) */
|
||||
function firstChar(label?: string) {
|
||||
return (label || '?').trim().charAt(0) || '?';
|
||||
}
|
||||
|
||||
/** 选项过滤:按 label 包含输入关键字 */
|
||||
function filterOption(input: string, option: any) {
|
||||
const v = String(option?.label || '').toLowerCase();
|
||||
return v.includes((input || '').toLowerCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* change 处理:
|
||||
* - 单选:把单值包成数组
|
||||
* - 多选:超 maxCount 截断
|
||||
*/
|
||||
function onChange(v: any) {
|
||||
if (!props.multiple) {
|
||||
innerValue.value = v == null ? [] : [v];
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(v) && v.length > props.maxCount) {
|
||||
v = v.slice(0, props.maxCount);
|
||||
}
|
||||
innerValue.value = v;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Select
|
||||
:value="innerValue as any"
|
||||
:mode="selectMode"
|
||||
:options="options"
|
||||
:placeholder="placeholder"
|
||||
:allow-clear="allowClear"
|
||||
:show-search="showSearch"
|
||||
:filter-option="filterOption"
|
||||
:disabled="disabled"
|
||||
:max-tag-count="10"
|
||||
option-filter-prop="label"
|
||||
style="width: 100%"
|
||||
@change="onChange"
|
||||
>
|
||||
<!-- 多选已选项展示头像+名字 -->
|
||||
<template v-if="multiple" #tagRender="{ value, closable, onClose }">
|
||||
<Tag :closable="closable" @close="onClose" class="user-tag-chip">
|
||||
<Avatar :size="16" :src="avatarOf(value)">
|
||||
{{ firstChar(labelOf(value)) }}
|
||||
</Avatar>
|
||||
<span class="user-tag-name">{{ labelOf(value) }}</span>
|
||||
</Tag>
|
||||
</template>
|
||||
|
||||
<!-- 下拉选项展示头像+名字 -->
|
||||
<template #option="{ label, avatar }">
|
||||
<div class="user-tag-option">
|
||||
<Avatar :size="20" :src="avatar">{{ firstChar(label) }}</Avatar>
|
||||
<span>{{ label }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</Select>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.user-tag-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
|
||||
.user-tag-name {
|
||||
max-width: 80px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.user-tag-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user