update notify style
This commit is contained in:
@@ -1,136 +0,0 @@
|
|||||||
<template>
|
|
||||||
<t-popup v-model="visible" placement="bottom">
|
|
||||||
<div class="bottom-popup" :style="{height: props.height}">
|
|
||||||
<div v-if="hasHeader" class="header">
|
|
||||||
<div class="button left">
|
|
||||||
<slot name="leftButton">
|
|
||||||
<t-button v-if="leftButtonText" variant="text" size="small" @click="emitLeftButtonClick">
|
|
||||||
<span v-text="leftButtonText" />
|
|
||||||
</t-button>
|
|
||||||
</slot>
|
|
||||||
</div>
|
|
||||||
<div class="title">
|
|
||||||
<slot name="title">
|
|
||||||
<span v-text="title" />
|
|
||||||
</slot>
|
|
||||||
</div>
|
|
||||||
<div class="button right">
|
|
||||||
<slot name="rightButton">
|
|
||||||
<t-button v-if="rightButtonText" theme="primary" variant="text" size="small" @click="emitRightButtonClick">
|
|
||||||
<span v-text="rightButtonText" />
|
|
||||||
</t-button>
|
|
||||||
</slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="body">
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</t-popup>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
defineOptions({
|
|
||||||
name: "BottomPopup"
|
|
||||||
});
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
modelValue?: boolean;
|
|
||||||
leftButtonText?: string;
|
|
||||||
rightButtonText?: string;
|
|
||||||
height?: string | number;
|
|
||||||
title?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
|
||||||
modelValue: false,
|
|
||||||
leftButtonText: "取消",
|
|
||||||
rightButtonText: "确定",
|
|
||||||
height: "auto",
|
|
||||||
title: ""
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
"update:modelValue": [value: boolean];
|
|
||||||
leftButtonClick: [];
|
|
||||||
rightButtonClick: [];
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const slots = useSlots();
|
|
||||||
const visible = computed({
|
|
||||||
get: () => props.modelValue,
|
|
||||||
set: value => emit("update:modelValue", value)
|
|
||||||
});
|
|
||||||
const hasHeader = computed(() => Boolean(
|
|
||||||
props.title
|
|
||||||
|| props.leftButtonText
|
|
||||||
|| props.rightButtonText
|
|
||||||
|| slots.title
|
|
||||||
|| slots.leftButton
|
|
||||||
|| slots.rightButton
|
|
||||||
));
|
|
||||||
|
|
||||||
function emitLeftButtonClick(): void {
|
|
||||||
emit("leftButtonClick");
|
|
||||||
}
|
|
||||||
|
|
||||||
function emitRightButtonClick(): void {
|
|
||||||
emit("rightButtonClick");
|
|
||||||
}
|
|
||||||
|
|
||||||
defineSlots<{
|
|
||||||
default?: () => unknown;
|
|
||||||
title?: () => unknown;
|
|
||||||
leftButton?: () => unknown;
|
|
||||||
rightButton?: () => unknown;
|
|
||||||
}>();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="less">
|
|
||||||
.bottom-popup {
|
|
||||||
display: flex;
|
|
||||||
overflow: hidden;
|
|
||||||
background: var(--td-bg-color-container, #FFF);
|
|
||||||
padding-top: 1rem;
|
|
||||||
border-radius: 1.25rem 1.25rem 0 0;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
.header {
|
|
||||||
flex: none;
|
|
||||||
padding: 0 1rem;
|
|
||||||
display: grid;
|
|
||||||
align-items: center;
|
|
||||||
grid-template-columns: 4rem 1fr 4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button {
|
|
||||||
min-width: 0;
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
&.right {
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
margin: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
text-align: center;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.body {
|
|
||||||
flex: 1;
|
|
||||||
padding: .75rem 0 env(safe-area-inset-bottom, 0);
|
|
||||||
overflow: auto;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(html.theme-dark) {
|
|
||||||
.bottom-popup {
|
|
||||||
background: #0f172a;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -155,6 +155,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { BottomPopup } from "timi-tdesign-mobile";
|
||||||
import { Message } from "@/utils/message";
|
import { Message } from "@/utils/message";
|
||||||
import { PassTimeLabel, Text } from "timi-web";
|
import { PassTimeLabel, Text } from "timi-web";
|
||||||
import { useUserStore, type Attachment } from "timi-web-api";
|
import { useUserStore, type Attachment } from "timi-web-api";
|
||||||
|
|||||||
@@ -173,6 +173,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { BottomPopup } from "timi-tdesign-mobile";
|
||||||
import { type Attachment, AttachmentAPI, useUserStore } from "timi-web-api";
|
import { type Attachment, AttachmentAPI, useUserStore } from "timi-web-api";
|
||||||
import { Message } from "@/utils/message";
|
import { Message } from "@/utils/message";
|
||||||
import CustomerAPI from "@/api/CustomerAPI";
|
import CustomerAPI from "@/api/CustomerAPI";
|
||||||
|
|||||||
@@ -9,7 +9,11 @@
|
|||||||
<pass-time-label class="time" :timestamp="detail.sendAt" underline />
|
<pass-time-label class="time" :timestamp="detail.sendAt" underline />
|
||||||
</header>
|
</header>
|
||||||
<section class="body">
|
<section class="body">
|
||||||
<notify-view :detail="detail" empty-text="暂无通知内容" />
|
<notify-view
|
||||||
|
:detail="detail"
|
||||||
|
empty-text="暂无通知内容"
|
||||||
|
:resolve-attachment-url="resolveAttachmentUrl"
|
||||||
|
/>
|
||||||
</section>
|
</section>
|
||||||
</article>
|
</article>
|
||||||
<t-empty
|
<t-empty
|
||||||
@@ -26,6 +30,7 @@ import { NotifyAPI, type NotifyDetail } from "timi-web-api";
|
|||||||
import { DialogPlugin } from "tdesign-mobile-vue";
|
import { DialogPlugin } from "tdesign-mobile-vue";
|
||||||
import { useNavBarStore } from "@/store/navBarStore";
|
import { useNavBarStore } from "@/store/navBarStore";
|
||||||
import { getErrorMessage } from "@/utils/error";
|
import { getErrorMessage } from "@/utils/error";
|
||||||
|
import { getAttachmentReadURL } from "@/utils/attachment";
|
||||||
import { corePermissionCodes } from "@/utils/permission";
|
import { corePermissionCodes } from "@/utils/permission";
|
||||||
import { Message } from "@/utils/message.ts";
|
import { Message } from "@/utils/message.ts";
|
||||||
|
|
||||||
@@ -148,6 +153,10 @@ function getSenderName(item: NotifyDetail): string {
|
|||||||
return item.fromUser?.nick?.trim() || item.fromUser?.name?.trim() || "系统通知";
|
return item.fromUser?.nick?.trim() || item.fromUser?.name?.trim() || "系统通知";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveAttachmentUrl(attachmentId: string): string {
|
||||||
|
return getAttachmentReadURL(attachmentId);
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- 生命周期 ----------
|
// ---------- 生命周期 ----------
|
||||||
|
|
||||||
watch(() => route.params.id, async () => {
|
watch(() => route.params.id, async () => {
|
||||||
|
|||||||
@@ -10,17 +10,12 @@
|
|||||||
placeholder="请输入通知标题"
|
placeholder="请输入通知标题"
|
||||||
/>
|
/>
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<notify-recipient-scope-picker
|
<t-form-item
|
||||||
v-if="isGlobalScope"
|
label="接收用户"
|
||||||
v-model="selectedStoreId"
|
:description="selectedRecipientDescription"
|
||||||
:options="storeScopeOptions"
|
required-mark
|
||||||
@update:model-value="handleStoreScopeChange"
|
arrow
|
||||||
/>
|
@click="openRecipientPicker"
|
||||||
<notify-recipient-picker
|
|
||||||
v-model="selectedUserIdList"
|
|
||||||
:users="recipientList"
|
|
||||||
:self-user="selfRecipient"
|
|
||||||
:loading="isRecipientLoading"
|
|
||||||
/>
|
/>
|
||||||
<t-form-item label="发送时间" arrow @click="openSchedulePicker">
|
<t-form-item label="发送时间" arrow @click="openSchedulePicker">
|
||||||
<div class="schedule-value">
|
<div class="schedule-value">
|
||||||
@@ -39,10 +34,20 @@
|
|||||||
class="editor"
|
class="editor"
|
||||||
v-model="draftDetail"
|
v-model="draftDetail"
|
||||||
placeholder="请输入通知内容"
|
placeholder="请输入通知内容"
|
||||||
|
:upload-image="uploadImage"
|
||||||
|
:upload-video="uploadVideo"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</t-form>
|
</t-form>
|
||||||
</div>
|
</div>
|
||||||
|
<notify-recipient-picker
|
||||||
|
v-model="selectedUserIdList"
|
||||||
|
v-model:visible="recipientPickerVisible"
|
||||||
|
:users="recipientList"
|
||||||
|
:stores="storeScopeOptions"
|
||||||
|
:self-user="selfRecipient"
|
||||||
|
:loading="isRecipientLoading"
|
||||||
|
/>
|
||||||
<t-popup v-model="schedulePickerVisible" placement="bottom">
|
<t-popup v-model="schedulePickerVisible" placement="bottom">
|
||||||
<t-date-time-picker
|
<t-date-time-picker
|
||||||
v-model="scheduleDraftText"
|
v-model="scheduleDraftText"
|
||||||
@@ -59,18 +64,15 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { NotifyEditor, type NotifyEditorExpose } from "timi-tdesign-mobile";
|
import { NotifyEditor, type NotifyEditorExpose } from "timi-tdesign-mobile";
|
||||||
import { Time } from "timi-web";
|
import { Time } from "timi-web";
|
||||||
import { type Attachment as ApiAttachment, type NotifyContentType, type NotifyDetail, useUserStore } from "timi-web-api";
|
import { AttachmentAPI, type Attachment as ApiAttachment, type NotifyContentType, type NotifyDetail, useUserStore } from "timi-web-api";
|
||||||
import { Message } from "@/utils/message";
|
import { Message } from "@/utils/message";
|
||||||
import { getErrorMessage } from "@/utils/error";
|
import { getErrorMessage } from "@/utils/error";
|
||||||
import GaoNotifyAPI from "@/api/GaoNotifyAPI";
|
import GaoNotifyAPI from "@/api/GaoNotifyAPI";
|
||||||
import type { ManagedUser } from "@/types/ManagedUser";
|
import type { ManagedUser } from "@/types/ManagedUser";
|
||||||
|
import NotifyRecipientPicker, { type NotifyRecipientScopeOption } from "@/pages/notify/component/NotifyRecipientPicker.vue";
|
||||||
import { corePermissionCodes, coreRoleCodes, gaoRoleCodes, permissionCodes } from "@/utils/permission";
|
import { corePermissionCodes, coreRoleCodes, gaoRoleCodes, permissionCodes } from "@/utils/permission";
|
||||||
import { useNavBarStore } from "@/store/navBarStore";
|
import { useNavBarStore } from "@/store/navBarStore";
|
||||||
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
||||||
import NotifyRecipientScopePicker, { type NotifyRecipientScopeOption } from "@/pages/notify/component/NotifyRecipientScopePicker.vue";
|
|
||||||
import NotifyRecipientPicker from "@/pages/notify/component/NotifyRecipientPicker.vue";
|
|
||||||
|
|
||||||
const allStoreValue = "__ALL_STORES__";
|
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "NotifyManagementCreate"
|
name: "NotifyManagementCreate"
|
||||||
@@ -95,12 +97,33 @@ const draftDetail = ref<NotifyDetail>({
|
|||||||
});
|
});
|
||||||
const selectedUserIdList = ref<string[]>([]);
|
const selectedUserIdList = ref<string[]>([]);
|
||||||
const isSaving = ref(false);
|
const isSaving = ref(false);
|
||||||
const selectedStoreId = ref(allStoreValue);
|
|
||||||
|
async function uploadMedia(file: File): Promise<{ id: string; url: string; name: string }> {
|
||||||
|
const [tempFile] = await AttachmentAPI.upload(file);
|
||||||
|
if (!tempFile?.id) {
|
||||||
|
throw new Error("媒体上传失败");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: tempFile.id,
|
||||||
|
url: AttachmentAPI.getTempReadURL(tempFile.id),
|
||||||
|
name: file.name
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadImage(file: File) {
|
||||||
|
return await uploadMedia(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadVideo(file: File) {
|
||||||
|
return await uploadMedia(file);
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- 接收用户 ----------
|
// ---------- 接收用户 ----------
|
||||||
|
|
||||||
const isRecipientLoading = ref(false);
|
const isRecipientLoading = ref(false);
|
||||||
const recipientList = ref<ManagedUser[]>([]);
|
const recipientList = ref<ManagedUser[]>([]);
|
||||||
|
const recipientPickerVisible = ref(false);
|
||||||
|
const recipientStoreId = ref("");
|
||||||
const loginUserId = computed(() => userStore.loginUser.user?.id || userStore.loginUser.id || "");
|
const loginUserId = computed(() => userStore.loginUser.user?.id || userStore.loginUser.id || "");
|
||||||
const selfRecipient = computed<ManagedUser | undefined>(() => {
|
const selfRecipient = computed<ManagedUser | undefined>(() => {
|
||||||
if (!loginUserId.value) {
|
if (!loginUserId.value) {
|
||||||
@@ -114,20 +137,41 @@ const selfRecipient = computed<ManagedUser | undefined>(() => {
|
|||||||
id: user.id,
|
id: user.id,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
nick: user.nick,
|
nick: user.nick,
|
||||||
phoneNo: user.phoneNo
|
phoneNo: user.phoneNo,
|
||||||
|
attachmentList: user.attachmentList as unknown as ApiAttachment[]
|
||||||
} : undefined
|
} : undefined
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const storeScopeOptions = computed<NotifyRecipientScopeOption[]>(() => [
|
const storeScopeOptions = computed<NotifyRecipientScopeOption[]>(() => {
|
||||||
...(isGlobalScope.value ? [{ label: "所有门店", value: allStoreValue }] : []),
|
const storeList = currentStoreStore.storeList.length
|
||||||
...currentStoreStore.storeList
|
? currentStoreStore.storeList
|
||||||
|
: currentStoreStore.currentStore
|
||||||
|
? [currentStoreStore.currentStore]
|
||||||
|
: [];
|
||||||
|
return storeList
|
||||||
.map(item => ({
|
.map(item => ({
|
||||||
label: item.name || item.code || "未命名门店",
|
label: item.name || item.code || "未命名门店",
|
||||||
value: item.id || ""
|
value: item.id || ""
|
||||||
}))
|
}))
|
||||||
.filter(item => item.value)
|
.filter(item => item.value);
|
||||||
]);
|
});
|
||||||
|
const availableRecipientList = computed(() => {
|
||||||
|
const selfUserId = selfRecipient.value?.userId;
|
||||||
|
const userList = selfUserId
|
||||||
|
? recipientList.value.filter(item => item.userId !== selfUserId)
|
||||||
|
: recipientList.value;
|
||||||
|
return selfRecipient.value ? [selfRecipient.value, ...userList] : userList;
|
||||||
|
});
|
||||||
|
const selectedRecipientDescription = computed(() => {
|
||||||
|
if (!selectedUserIdList.value.length) {
|
||||||
|
return "请选择接收用户";
|
||||||
|
}
|
||||||
|
const recipientMap = new Map(availableRecipientList.value.map(item => [item.userId, item]));
|
||||||
|
return selectedUserIdList.value
|
||||||
|
.map(userId => getRecipientName(recipientMap.get(userId) || { userId }))
|
||||||
|
.join("、");
|
||||||
|
});
|
||||||
|
|
||||||
// ---------- 定时发送 ----------
|
// ---------- 定时发送 ----------
|
||||||
|
|
||||||
@@ -236,6 +280,19 @@ function clearSchedule(): void {
|
|||||||
scheduleDraftText.value = "";
|
scheduleDraftText.value = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 接收用户选择 ----------
|
||||||
|
|
||||||
|
function openRecipientPicker(): void {
|
||||||
|
recipientPickerVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRecipientName(item: ManagedUser): string {
|
||||||
|
if (item.userId === selfRecipient.value?.userId) {
|
||||||
|
return `自己(${item.user?.nick || item.user?.name || "当前账号"})`;
|
||||||
|
}
|
||||||
|
return item.user?.nick || item.user?.name || item.userId || "未命名用户";
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- 接收用户 ----------
|
// ---------- 接收用户 ----------
|
||||||
|
|
||||||
async function loadRecipients(): Promise<void> {
|
async function loadRecipients(): Promise<void> {
|
||||||
@@ -249,9 +306,7 @@ async function loadRecipients(): Promise<void> {
|
|||||||
const list: ManagedUser[] = [];
|
const list: ManagedUser[] = [];
|
||||||
let pageIndex = 0;
|
let pageIndex = 0;
|
||||||
let total = 0;
|
let total = 0;
|
||||||
const storeId = selectedStoreId.value && selectedStoreId.value !== allStoreValue
|
const storeId = recipientStoreId.value || undefined;
|
||||||
? selectedStoreId.value
|
|
||||||
: undefined;
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const result = await GaoNotifyAPI.listRecipients(
|
const result = await GaoNotifyAPI.listRecipients(
|
||||||
{ index: pageIndex, size: pageSize },
|
{ index: pageIndex, size: pageSize },
|
||||||
@@ -280,11 +335,6 @@ async function loadRecipients(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleStoreScopeChange(): Promise<void> {
|
|
||||||
selectedUserIdList.value = [];
|
|
||||||
await loadRecipients();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- 生命周期 ----------
|
// ---------- 生命周期 ----------
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
@@ -296,7 +346,7 @@ onMounted(async () => {
|
|||||||
await currentStoreStore.loadStoreList();
|
await currentStoreStore.loadStoreList();
|
||||||
} else {
|
} else {
|
||||||
const loginStore = await currentStoreStore.loadLoginStore();
|
const loginStore = await currentStoreStore.loadLoginStore();
|
||||||
selectedStoreId.value = loginStore?.id || currentStoreStore.currentStoreId || "";
|
recipientStoreId.value = loginStore?.id || currentStoreStore.currentStoreId || "";
|
||||||
}
|
}
|
||||||
navBarStore.setRightText("发送", "var(--td-brand-color)", saveNotification);
|
navBarStore.setRightText("发送", "var(--td-brand-color)", saveNotification);
|
||||||
await loadRecipients();
|
await loadRecipients();
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
v-if="mainDetail"
|
v-if="mainDetail"
|
||||||
:detail="mainDetail"
|
:detail="mainDetail"
|
||||||
empty-text="暂无通知内容"
|
empty-text="暂无通知内容"
|
||||||
|
:resolve-attachment-url="resolveAttachmentUrl"
|
||||||
/>
|
/>
|
||||||
<div v-else class="empty-content">暂无通知内容</div>
|
<div v-else class="empty-content">暂无通知内容</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -75,6 +76,7 @@ import { NotifyAPI, type Notify, type NotifyDetail, useUserStore } from "timi-we
|
|||||||
import { DialogPlugin } from "tdesign-mobile-vue";
|
import { DialogPlugin } from "tdesign-mobile-vue";
|
||||||
import { Message } from "@/utils/message";
|
import { Message } from "@/utils/message";
|
||||||
import { getErrorMessage } from "@/utils/error";
|
import { getErrorMessage } from "@/utils/error";
|
||||||
|
import { getAttachmentReadURL } from "@/utils/attachment";
|
||||||
import { useNavBarStore } from "@/store/navBarStore";
|
import { useNavBarStore } from "@/store/navBarStore";
|
||||||
import { corePermissionCodes } from "@/utils/permission";
|
import { corePermissionCodes } from "@/utils/permission";
|
||||||
|
|
||||||
@@ -145,6 +147,10 @@ const getSummaryTheme = computed<NotifyTagTheme>(() => {
|
|||||||
return "success";
|
return "success";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function resolveAttachmentUrl(attachmentId: string): string {
|
||||||
|
return getAttachmentReadURL(attachmentId);
|
||||||
|
}
|
||||||
|
|
||||||
async function loadDetail(): Promise<void> {
|
async function loadDetail(): Promise<void> {
|
||||||
const requestToken = loadToken + 1;
|
const requestToken = loadToken + 1;
|
||||||
loadToken = requestToken;
|
loadToken = requestToken;
|
||||||
|
|||||||
@@ -1,73 +1,153 @@
|
|||||||
<template>
|
<template>
|
||||||
<t-form-item
|
<bottom-popup
|
||||||
label="接收用户"
|
v-model="popupVisible"
|
||||||
required-mark
|
title="选择接收用户"
|
||||||
:arrow="true"
|
right-button-text="完成"
|
||||||
@click="openPicker"
|
height="80vh"
|
||||||
|
@left-button-click="handleLeftButton"
|
||||||
|
@right-button-click="confirmSelection"
|
||||||
>
|
>
|
||||||
<div class="selected-text" v-text="selectedText" />
|
<template #leftButton>
|
||||||
</t-form-item>
|
<t-button variant="text" size="small" @click="handleLeftButton">
|
||||||
<t-popup v-model="visible" placement="bottom" destroy-on-close>
|
<span v-text="pickerStoreId ? '返回' : '取消'" />
|
||||||
<div class="recipient-picker">
|
</t-button>
|
||||||
<header class="picker-header">
|
</template>
|
||||||
<t-button variant="text" size="small" @click="cancelPicker">取消</t-button>
|
<div class="page-transition">
|
||||||
<h3>选择接收用户</h3>
|
<transition :name="pageTransitionName">
|
||||||
<t-button theme="primary" variant="text" size="small" @click="confirmPicker">确定</t-button>
|
<div v-if="!pickerStoreId" key="store-list" class="recipient-picker store">
|
||||||
</header>
|
<div class="summary">
|
||||||
|
<span v-text="`已选择 ${draftSelectedUserIdList.length} 位用户`" />
|
||||||
|
<span v-text="loading ? '正在加载' : '勾选门店可选择全部用户'" />
|
||||||
|
</div>
|
||||||
|
<t-cell-group v-if="stores.length || selfUser" class="list">
|
||||||
|
<t-cell
|
||||||
|
v-if="selfUser"
|
||||||
|
class="item"
|
||||||
|
title="当前账号"
|
||||||
|
description="可接收自己发送的通知"
|
||||||
|
@click="toggleSelfRecipient(!isSelfRecipientSelected())"
|
||||||
|
>
|
||||||
|
<template #left-icon>
|
||||||
|
<div class="leading">
|
||||||
|
<div @click.stop>
|
||||||
|
<t-checkbox
|
||||||
|
class="checkbox"
|
||||||
|
:model-value="draftSelectedUserIdList.includes(selfUser.userId || '')"
|
||||||
|
borderless
|
||||||
|
@change="toggleSelfRecipient"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<t-image
|
||||||
|
:src="buildUserAvatar(selfUser)"
|
||||||
|
:alt="getRecipientName(selfUser)"
|
||||||
|
fit="cover"
|
||||||
|
class="avatar"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</t-cell>
|
||||||
|
<t-cell
|
||||||
|
class="item"
|
||||||
|
v-for="item in stores"
|
||||||
|
:key="item.value"
|
||||||
|
:title="item.label"
|
||||||
|
:description="getStoreSelectionDescription(item.value)"
|
||||||
|
arrow
|
||||||
|
@click="openStore(item.value)"
|
||||||
|
>
|
||||||
|
<template #left-icon>
|
||||||
|
<div @click.stop>
|
||||||
|
<t-checkbox
|
||||||
|
class="checkbox"
|
||||||
|
icon="rectangle"
|
||||||
|
:model-value="isStoreSelected(item.value) || isStoreIndeterminate(item.value)"
|
||||||
|
:indeterminate="isStoreIndeterminate(item.value)"
|
||||||
|
borderless
|
||||||
|
@change="value => toggleStoreSelection(item.value, value === true)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</t-cell>
|
||||||
|
</t-cell-group>
|
||||||
|
<t-empty
|
||||||
|
v-else
|
||||||
|
:description="loading ? '正在加载门店用户' : '暂无可选择门店'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-else key="user-list" class="recipient-picker user">
|
||||||
|
<div class="header">
|
||||||
|
<span v-text="selectedStore?.label || '选择用户'" />
|
||||||
|
<span v-text="getStoreSelectionDescription(pickerStoreId)" />
|
||||||
|
</div>
|
||||||
<div class="search-wrapper">
|
<div class="search-wrapper">
|
||||||
<t-input
|
<t-input
|
||||||
v-model.trim="keyword"
|
v-model.trim="keyword"
|
||||||
class="search"
|
class="search"
|
||||||
borderless
|
borderless
|
||||||
|
size="small"
|
||||||
clearable
|
clearable
|
||||||
placeholder="搜索姓名、昵称或手机号"
|
placeholder="搜索编码、姓名(拼音)、或手机号"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="selection-actions">
|
<t-cell-group v-if="filteredUserList.length">
|
||||||
<span class="selection-count" v-text="`已选择 ${selectedUserIdList.length} 人`" />
|
<t-cell
|
||||||
<t-button
|
v-for="item in filteredUserList"
|
||||||
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"
|
:key="item.userId"
|
||||||
:value="item.userId"
|
class="item recipient-option"
|
||||||
class="recipient-option"
|
:title="getRecipientName(item)"
|
||||||
borderless
|
:description="item.user?.phoneNo || item.userId"
|
||||||
|
@click="toggleUserSelection(item.userId)"
|
||||||
>
|
>
|
||||||
<div class="recipient-info">
|
<template #left-icon>
|
||||||
<span class="recipient-name" v-text="getRecipientName(item)" />
|
<div class="leading">
|
||||||
<span class="recipient-phone" v-text="item.user?.phoneNo || item.userId" />
|
<div @click.stop>
|
||||||
|
<t-checkbox
|
||||||
|
class="checkbox"
|
||||||
|
:model-value="isUserSelected(item.userId)"
|
||||||
|
borderless
|
||||||
|
@change="value => toggleUserSelection(item.userId, value === true)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</t-checkbox>
|
<t-image
|
||||||
</t-checkbox-group>
|
:src="buildUserAvatar(item)"
|
||||||
|
:alt="getRecipientName(item)"
|
||||||
|
fit="cover"
|
||||||
|
class="avatar"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</t-cell>
|
||||||
|
</t-cell-group>
|
||||||
<t-empty
|
<t-empty
|
||||||
v-else
|
v-else
|
||||||
:description="loading ? '正在加载用户' : '暂无匹配用户'"
|
:description="loading ? '正在加载用户' : '暂无匹配用户'"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
</t-popup>
|
</bottom-popup>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { BottomPopup } from "timi-tdesign-mobile";
|
||||||
|
import { UserAPI } from "timi-web-api";
|
||||||
import type { ManagedUser } from "@/types/ManagedUser";
|
import type { ManagedUser } from "@/types/ManagedUser";
|
||||||
|
import { buildFallbackAvatar } from "@/utils/avatar";
|
||||||
|
|
||||||
|
export type NotifyRecipientScopeOption = {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "NotifyRecipientPicker"
|
||||||
|
});
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
modelValue: string[];
|
modelValue: string[];
|
||||||
|
visible: boolean;
|
||||||
users: ManagedUser[];
|
users: ManagedUser[];
|
||||||
|
stores: NotifyRecipientScopeOption[];
|
||||||
selfUser?: ManagedUser;
|
selfUser?: ManagedUser;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
}>(), {
|
}>(), {
|
||||||
@@ -76,46 +156,42 @@ const props = withDefaults(defineProps<{
|
|||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
"update:modelValue": [value: string[]];
|
"update:modelValue": [value: string[]];
|
||||||
|
"update:visible": [value: boolean];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const visible = ref(false);
|
const popupVisible = computed({
|
||||||
const keyword = ref("");
|
get: () => props.visible,
|
||||||
|
set: value => emit("update:visible", value)
|
||||||
|
});
|
||||||
const draftSelectedUserIdList = ref<string[]>([]);
|
const draftSelectedUserIdList = ref<string[]>([]);
|
||||||
|
const pickerStoreId = ref("");
|
||||||
|
const keyword = ref("");
|
||||||
|
const pageDirection = ref<"forward" | "backward">("forward");
|
||||||
|
|
||||||
const selectedUserIdList = computed({
|
const pageTransitionName = computed(() => `page-slide-${pageDirection.value}`);
|
||||||
get: () => props.modelValue,
|
const selectedStore = computed(() => props.stores.find(item => item.value === pickerStoreId.value));
|
||||||
set: value => emit("update:modelValue", value)
|
const filteredUserList = computed(() => {
|
||||||
});
|
|
||||||
|
|
||||||
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();
|
const normalizedKeyword = keyword.value.toLowerCase();
|
||||||
if (!normalizedKeyword) {
|
return props.users
|
||||||
return availableList.value;
|
.filter(item => item.storeId === pickerStoreId.value)
|
||||||
}
|
.filter(item => {
|
||||||
|
|
||||||
return availableList.value.filter(item => {
|
|
||||||
const user = item.user;
|
const user = item.user;
|
||||||
return [item.userId, user?.name, user?.nick, user?.phoneNo]
|
return !normalizedKeyword || [item.userId, user?.name, user?.nick, user?.phoneNo]
|
||||||
.some(value => Boolean(value?.toLowerCase().includes(normalizedKeyword)));
|
.some(value => Boolean(value?.toLowerCase().includes(normalizedKeyword)));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
const storeUserMap = computed(() => {
|
||||||
const filteredIdList = computed(() => filteredList.value
|
const map = new Map<string, ManagedUser[]>();
|
||||||
.map(item => item.userId)
|
for (const item of props.users) {
|
||||||
.filter((item): item is string => Boolean(item)));
|
if (!item.storeId || !item.userId) {
|
||||||
const allFilteredSelected = computed(() => 0 < filteredIdList.value.length
|
continue;
|
||||||
&& filteredIdList.value.every(id => selectedUserIdList.value.includes(id)));
|
}
|
||||||
const selectedText = computed(() => selectedUserIdList.value.length
|
const userList = map.get(item.storeId) || [];
|
||||||
? `已选择 ${selectedUserIdList.value.length} 位用户`
|
userList.push(item);
|
||||||
: "请选择接收用户");
|
map.set(item.storeId, userList);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
});
|
||||||
|
|
||||||
function getRecipientName(item: ManagedUser): string {
|
function getRecipientName(item: ManagedUser): string {
|
||||||
if (item.userId === props.selfUser?.userId) {
|
if (item.userId === props.selfUser?.userId) {
|
||||||
@@ -124,127 +200,253 @@ function getRecipientName(item: ManagedUser): string {
|
|||||||
return item.user?.nick || item.user?.name || item.userId || "未命名用户";
|
return item.user?.nick || item.user?.name || item.userId || "未命名用户";
|
||||||
}
|
}
|
||||||
|
|
||||||
function openPicker(): void {
|
function buildUserAvatar(item: ManagedUser): string {
|
||||||
draftSelectedUserIdList.value = [...selectedUserIdList.value];
|
const name = item.user?.nick || item.user?.name || item.userId || "用户";
|
||||||
keyword.value = "";
|
return UserAPI.getAvatarURL(item.user) || buildFallbackAvatar(name, "#0f766e");
|
||||||
visible.value = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelPicker(): void {
|
function isUserSelected(userId?: string): boolean {
|
||||||
selectedUserIdList.value = [...draftSelectedUserIdList.value];
|
return Boolean(userId && draftSelectedUserIdList.value.includes(userId));
|
||||||
visible.value = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function confirmPicker(): void {
|
function toggleUserSelection(userId?: string, checked?: boolean): void {
|
||||||
draftSelectedUserIdList.value = [...selectedUserIdList.value];
|
if (!userId) {
|
||||||
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;
|
return;
|
||||||
}
|
}
|
||||||
|
const selectedUserIdSet = new Set(draftSelectedUserIdList.value);
|
||||||
selectedUserIdList.value = Array.from(new Set([
|
if (checked === undefined ? !selectedUserIdSet.has(userId) : checked) {
|
||||||
...selectedUserIdList.value,
|
selectedUserIdSet.add(userId);
|
||||||
...filteredIdList.value
|
} else {
|
||||||
]));
|
selectedUserIdSet.delete(userId);
|
||||||
|
}
|
||||||
|
draftSelectedUserIdList.value = Array.from(selectedUserIdSet);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isSelfRecipientSelected(): boolean {
|
||||||
|
const userId = props.selfUser?.userId;
|
||||||
|
return Boolean(userId && draftSelectedUserIdList.value.includes(userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSelfRecipient(value: boolean): void {
|
||||||
|
const userId = props.selfUser?.userId;
|
||||||
|
if (!userId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const selectedUserIdSet = new Set(draftSelectedUserIdList.value);
|
||||||
|
if (value) {
|
||||||
|
selectedUserIdSet.add(userId);
|
||||||
|
} else {
|
||||||
|
selectedUserIdSet.delete(userId);
|
||||||
|
}
|
||||||
|
draftSelectedUserIdList.value = Array.from(selectedUserIdSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isStoreSelected(storeId: string): boolean {
|
||||||
|
const userList = storeUserMap.value.get(storeId) || [];
|
||||||
|
const selectedUserIdSet = new Set(draftSelectedUserIdList.value);
|
||||||
|
return 0 < userList.length
|
||||||
|
&& userList.every(item => Boolean(item.userId && selectedUserIdSet.has(item.userId)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function isStoreIndeterminate(storeId: string): boolean {
|
||||||
|
const userList = storeUserMap.value.get(storeId) || [];
|
||||||
|
const selectedUserIdSet = new Set(draftSelectedUserIdList.value);
|
||||||
|
const selectedCount = userList.filter(item => Boolean(item.userId && selectedUserIdSet.has(item.userId))).length;
|
||||||
|
return 0 < selectedCount && selectedCount < userList.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStoreSelectionDescription(storeId: string): string {
|
||||||
|
const userList = storeUserMap.value.get(storeId) || [];
|
||||||
|
const selectedUserIdSet = new Set(draftSelectedUserIdList.value);
|
||||||
|
const selectedCount = userList.filter(item => Boolean(item.userId && selectedUserIdSet.has(item.userId))).length;
|
||||||
|
return userList.length ? `已选择 ${selectedCount}/${userList.length} 位用户` : "暂无可选择用户";
|
||||||
|
}
|
||||||
|
|
||||||
|
function openStore(storeId: string): void {
|
||||||
|
pageDirection.value = "forward";
|
||||||
|
pickerStoreId.value = storeId;
|
||||||
|
keyword.value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleStoreSelection(storeId: string, checked: boolean): void {
|
||||||
|
const selectedUserIdSet = new Set(draftSelectedUserIdList.value);
|
||||||
|
for (const item of storeUserMap.value.get(storeId) || []) {
|
||||||
|
if (!item.userId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (checked) {
|
||||||
|
selectedUserIdSet.add(item.userId);
|
||||||
|
} else {
|
||||||
|
selectedUserIdSet.delete(item.userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
draftSelectedUserIdList.value = Array.from(selectedUserIdSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLeftButton(): void {
|
||||||
|
if (pickerStoreId.value) {
|
||||||
|
pageDirection.value = "backward";
|
||||||
|
pickerStoreId.value = "";
|
||||||
|
keyword.value = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
popupVisible.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmSelection(): void {
|
||||||
|
emit("update:modelValue", Array.from(new Set(draftSelectedUserIdList.value)));
|
||||||
|
pickerStoreId.value = "";
|
||||||
|
keyword.value = "";
|
||||||
|
popupVisible.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.visible, (value) => {
|
||||||
|
if (!value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
draftSelectedUserIdList.value = [...props.modelValue];
|
||||||
|
pageDirection.value = "forward";
|
||||||
|
pickerStoreId.value = "";
|
||||||
|
keyword.value = "";
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
.selected-text {
|
|
||||||
color: #52607a;
|
|
||||||
width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
text-align: right;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recipient-picker {
|
.recipient-picker {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 80vh;
|
height: 100%;
|
||||||
padding-top: 1rem;
|
background: white;
|
||||||
background: var(--td-bg-color-container);
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
.picker-header {
|
.summary {
|
||||||
padding: 0 1rem;
|
padding: .5rem 1rem;
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: 4rem 1fr 4rem;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
h3 {
|
span {
|
||||||
margin: 0;
|
color: #64748b;
|
||||||
text-align: center;
|
font-size: .8125rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
padding: .25rem 1rem;
|
||||||
|
|
||||||
|
:deep(.t-cell__left) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox {
|
||||||
|
padding: .5rem;
|
||||||
|
|
||||||
|
:deep(.t-checkbox__icon--left) {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.leading {
|
||||||
|
gap: .5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 2.75rem;
|
||||||
|
height: 2.75rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.avatar .t-image__img) {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.store {
|
||||||
|
|
||||||
|
.list {
|
||||||
|
flex: 1;
|
||||||
|
overflow: auto;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.user {
|
||||||
|
|
||||||
|
.header {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: .8125rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.search-wrapper {
|
.search-wrapper {
|
||||||
padding: 1rem 0 .5rem;
|
padding: 1rem 0 .5rem;
|
||||||
box-shadow: 0 4px 4px rgba(0, 0, 0, .05);
|
box-shadow: 0 4px 4px rgba(0, 0, 0, .05);
|
||||||
|
|
||||||
.search {
|
.search {
|
||||||
|
--td-input-vertical-padding: .5rem 1rem;
|
||||||
|
|
||||||
margin: 0 1rem;
|
margin: 0 1rem;
|
||||||
border: 1px solid var(--td-component-border);
|
border: 1px solid var(--td-component-border);
|
||||||
border-radius: .875rem;
|
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,
|
.page-transition {
|
||||||
.recipient-phone {
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-slide-forward-enter-active,
|
||||||
|
.page-slide-forward-leave-active,
|
||||||
|
.page-slide-backward-enter-active,
|
||||||
|
.page-slide-backward-leave-active {
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
transition: transform 320ms var(--tui-bezier), opacity 320ms var(--tui-bezier);
|
||||||
|
backface-visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-slide-forward-enter-active {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-slide-forward-enter-from {
|
||||||
|
transform: translate3d(100%, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-slide-forward-leave-to {
|
||||||
|
transform: translate3d(-25%, 0, 0);
|
||||||
|
opacity: .92;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-slide-backward-enter-from {
|
||||||
|
transform: translate3d(-25%, 0, 0);
|
||||||
|
opacity: .92;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-slide-backward-leave-active {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-slide-backward-leave-to {
|
||||||
|
transform: translate3d(100%, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html.theme-dark) {
|
||||||
|
.summary span,
|
||||||
|
.header,
|
||||||
|
.user .item :deep(.t-cell__description) {
|
||||||
color: #94a3b8;
|
color: #94a3b8;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,98 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -99,6 +99,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { BottomPopup } from "timi-tdesign-mobile";
|
||||||
import { Message } from "@/utils/message";
|
import { Message } from "@/utils/message";
|
||||||
import { useUserStore } from "timi-web-api";
|
import { useUserStore } from "timi-web-api";
|
||||||
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
||||||
|
|||||||
@@ -35,7 +35,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<bottom-popup
|
<bottom-popup
|
||||||
v-model="tierEditorVisible"
|
v-model="tierEditorVisible"
|
||||||
placement="bottom"
|
|
||||||
:title="tierEditorTitle"
|
:title="tierEditorTitle"
|
||||||
@left-button-click="closeTierEditor"
|
@left-button-click="closeTierEditor"
|
||||||
@right-button-click="confirmTierEditor"
|
@right-button-click="confirmTierEditor"
|
||||||
@@ -91,6 +90,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { BottomPopup } from "timi-tdesign-mobile";
|
||||||
import type { EventValueUnit } from "@/types/EventItem";
|
import type { EventValueUnit } from "@/types/EventItem";
|
||||||
import type { PointAction, PointRuleTierForm } from "@/types/Point";
|
import type { PointAction, PointRuleTierForm } from "@/types/Point";
|
||||||
import { formatTierBoundaryValue, getTierMinText, getTierMinValue, isLastTier, resolveNonNegativeInteger, resolveTierBoundaryValue } from "@/utils/pointRule";
|
import { formatTierBoundaryValue, getTierMinText, getTierMinValue, isLastTier, resolveNonNegativeInteger, resolveTierBoundaryValue } from "@/utils/pointRule";
|
||||||
|
|||||||
Reference in New Issue
Block a user