refactor timi-web
This commit is contained in:
@@ -0,0 +1,251 @@
|
||||
<template>
|
||||
<t-form-item
|
||||
label="接收用户"
|
||||
required-mark
|
||||
:arrow="true"
|
||||
@click="openPicker"
|
||||
>
|
||||
<div class="selected-text" v-text="selectedText" />
|
||||
</t-form-item>
|
||||
<t-popup v-model="visible" placement="bottom" destroy-on-close>
|
||||
<div class="recipient-picker">
|
||||
<header class="picker-header">
|
||||
<t-button variant="text" size="small" @click="cancelPicker">取消</t-button>
|
||||
<h3>选择接收用户</h3>
|
||||
<t-button theme="primary" variant="text" size="small" @click="confirmPicker">确定</t-button>
|
||||
</header>
|
||||
<div class="search-wrapper">
|
||||
<t-input
|
||||
v-model.trim="keyword"
|
||||
class="search"
|
||||
borderless
|
||||
clearable
|
||||
placeholder="搜索姓名、昵称或手机号"
|
||||
/>
|
||||
</div>
|
||||
<div class="selection-actions">
|
||||
<span class="selection-count" v-text="`已选择 ${selectedUserIdList.length} 人`" />
|
||||
<t-button
|
||||
variant="text"
|
||||
size="small"
|
||||
:disabled="!filteredIdList.length"
|
||||
@click="toggleSelectAll"
|
||||
>
|
||||
<span v-text="allFilteredSelected ? '取消全选' : '全选当前结果'" />
|
||||
</t-button>
|
||||
</div>
|
||||
<div class="recipient-list">
|
||||
<t-checkbox-group
|
||||
v-if="filteredList.length"
|
||||
v-model="selectedUserIdList"
|
||||
class="recipient-options"
|
||||
>
|
||||
<t-checkbox
|
||||
v-for="item in filteredList"
|
||||
:key="item.userId"
|
||||
:value="item.userId"
|
||||
class="recipient-option"
|
||||
borderless
|
||||
>
|
||||
<div class="recipient-info">
|
||||
<span class="recipient-name" v-text="getRecipientName(item)" />
|
||||
<span class="recipient-phone" v-text="item.user?.phoneNo || item.userId" />
|
||||
</div>
|
||||
</t-checkbox>
|
||||
</t-checkbox-group>
|
||||
<t-empty
|
||||
v-else
|
||||
:description="loading ? '正在加载用户' : '暂无匹配用户'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</t-popup>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ManagedUser } from "@/types/ManagedUser";
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue: string[];
|
||||
users: ManagedUser[];
|
||||
selfUser?: ManagedUser;
|
||||
loading?: boolean;
|
||||
}>(), {
|
||||
loading: false
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
"update:modelValue": [value: string[]];
|
||||
}>();
|
||||
|
||||
const visible = ref(false);
|
||||
const keyword = ref("");
|
||||
const draftSelectedUserIdList = ref<string[]>([]);
|
||||
|
||||
const selectedUserIdList = computed({
|
||||
get: () => props.modelValue,
|
||||
set: value => emit("update:modelValue", value)
|
||||
});
|
||||
|
||||
const availableList = computed(() => {
|
||||
const selfUserId = props.selfUser?.userId;
|
||||
const userList = selfUserId
|
||||
? props.users.filter(item => item.userId !== selfUserId)
|
||||
: props.users;
|
||||
return props.selfUser ? [props.selfUser, ...userList] : userList;
|
||||
});
|
||||
|
||||
const filteredList = computed(() => {
|
||||
const normalizedKeyword = keyword.value.toLowerCase();
|
||||
if (!normalizedKeyword) {
|
||||
return availableList.value;
|
||||
}
|
||||
|
||||
return availableList.value.filter(item => {
|
||||
const user = item.user;
|
||||
return [item.userId, user?.name, user?.nick, user?.phoneNo]
|
||||
.some(value => Boolean(value?.toLowerCase().includes(normalizedKeyword)));
|
||||
});
|
||||
});
|
||||
|
||||
const filteredIdList = computed(() => filteredList.value
|
||||
.map(item => item.userId)
|
||||
.filter((item): item is string => Boolean(item)));
|
||||
const allFilteredSelected = computed(() => 0 < filteredIdList.value.length
|
||||
&& filteredIdList.value.every(id => selectedUserIdList.value.includes(id)));
|
||||
const selectedText = computed(() => selectedUserIdList.value.length
|
||||
? `已选择 ${selectedUserIdList.value.length} 位用户`
|
||||
: "请选择接收用户");
|
||||
|
||||
function getRecipientName(item: ManagedUser): string {
|
||||
if (item.userId === props.selfUser?.userId) {
|
||||
return `自己(${item.user?.nick || item.user?.name || "当前账号"})`;
|
||||
}
|
||||
return item.user?.nick || item.user?.name || item.userId || "未命名用户";
|
||||
}
|
||||
|
||||
function openPicker(): void {
|
||||
draftSelectedUserIdList.value = [...selectedUserIdList.value];
|
||||
keyword.value = "";
|
||||
visible.value = true;
|
||||
}
|
||||
|
||||
function cancelPicker(): void {
|
||||
selectedUserIdList.value = [...draftSelectedUserIdList.value];
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
function confirmPicker(): void {
|
||||
draftSelectedUserIdList.value = [...selectedUserIdList.value];
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
function toggleSelectAll(): void {
|
||||
const filteredIdSet = new Set(filteredIdList.value);
|
||||
if (allFilteredSelected.value) {
|
||||
selectedUserIdList.value = selectedUserIdList.value.filter(id => !filteredIdSet.has(id));
|
||||
return;
|
||||
}
|
||||
|
||||
selectedUserIdList.value = Array.from(new Set([
|
||||
...selectedUserIdList.value,
|
||||
...filteredIdList.value
|
||||
]));
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.selected-text {
|
||||
color: #52607a;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-align: right;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.recipient-picker {
|
||||
display: flex;
|
||||
height: 80vh;
|
||||
padding-top: 1rem;
|
||||
background: var(--td-bg-color-container);
|
||||
flex-direction: column;
|
||||
|
||||
.picker-header {
|
||||
padding: 0 1rem;
|
||||
display: grid;
|
||||
grid-template-columns: 4rem 1fr 4rem;
|
||||
align-items: center;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.search-wrapper {
|
||||
padding: 1rem 0 .5rem;
|
||||
box-shadow: 0 4px 4px rgba(0, 0, 0, .05);
|
||||
|
||||
.search {
|
||||
margin: 0 1rem;
|
||||
border: 1px solid var(--td-component-border);
|
||||
border-radius: .875rem;
|
||||
}
|
||||
}
|
||||
|
||||
.selection-actions {
|
||||
padding: .25rem 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.selection-count {
|
||||
color: #64748b;
|
||||
font-size: .8125rem;
|
||||
}
|
||||
}
|
||||
|
||||
.recipient-list {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
min-height: 0;
|
||||
padding: 0 1rem env(safe-area-inset-bottom, 0);
|
||||
|
||||
.recipient-options {
|
||||
display: grid;
|
||||
|
||||
.recipient-option {
|
||||
padding: .75rem 0;
|
||||
border-bottom: 1px solid var(--td-component-border);
|
||||
|
||||
.recipient-info {
|
||||
gap: .25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.recipient-name {
|
||||
font-size: .9375rem;
|
||||
}
|
||||
|
||||
.recipient-phone {
|
||||
color: #64748b;
|
||||
font-size: .75rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global(html.theme-dark) {
|
||||
.selected-text {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.selection-count,
|
||||
.recipient-phone {
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<t-form-item label="用户范围" arrow @click="openPicker">
|
||||
<div class="scope-text" v-text="selectedLabel" />
|
||||
</t-form-item>
|
||||
<t-popup v-model="visible" placement="bottom" destroy-on-close>
|
||||
<div class="scope-picker">
|
||||
<header class="picker-header">
|
||||
<t-button variant="text" size="small" @click="cancelPicker">取消</t-button>
|
||||
<h3>选择用户范围</h3>
|
||||
<t-button theme="primary" variant="text" size="small" @click="confirmPicker">确定</t-button>
|
||||
</header>
|
||||
<t-radio-group v-model="draftValue" class="scope-options">
|
||||
<t-radio
|
||||
v-for="item in options"
|
||||
:key="item.value"
|
||||
:block="false"
|
||||
borderless
|
||||
:value="item.value"
|
||||
>
|
||||
<span v-text="item.label" />
|
||||
</t-radio>
|
||||
</t-radio-group>
|
||||
</div>
|
||||
</t-popup>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
export type NotifyRecipientScopeOption = {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string;
|
||||
options: NotifyRecipientScopeOption[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
"update:modelValue": [value: string];
|
||||
}>();
|
||||
|
||||
const visible = ref(false);
|
||||
const draftValue = ref(props.modelValue);
|
||||
const selectedLabel = computed(() => props.options.find(item => item.value === props.modelValue)?.label || "请选择用户范围");
|
||||
|
||||
function openPicker(): void {
|
||||
draftValue.value = props.modelValue;
|
||||
visible.value = true;
|
||||
}
|
||||
|
||||
function cancelPicker(): void {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
function confirmPicker(): void {
|
||||
emit("update:modelValue", draftValue.value);
|
||||
visible.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.scope-text {
|
||||
color: #52607a;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-align: right;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.scope-picker {
|
||||
padding: 1rem 1rem env(safe-area-inset-bottom, 0);
|
||||
background: var(--td-bg-color-container);
|
||||
|
||||
.picker-header {
|
||||
display: grid;
|
||||
grid-template-columns: 4rem 1fr 4rem;
|
||||
align-items: center;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.scope-options {
|
||||
gap: .5rem;
|
||||
margin-top: 1rem;
|
||||
display: grid;
|
||||
}
|
||||
}
|
||||
|
||||
:global(html.theme-dark) {
|
||||
.scope-text {
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user