add event option
This commit is contained in:
@@ -1,18 +1,13 @@
|
||||
<template>
|
||||
<t-popup v-model="popupVisible" placement="bottom">
|
||||
<div class="customer-selector bg-white">
|
||||
<div class="title-bar">
|
||||
<t-button variant="text" size="small" @click="cancelSelection">取消</t-button>
|
||||
<h3 class="title">选择客户</h3>
|
||||
<t-button
|
||||
v-if="isMultiple"
|
||||
theme="primary"
|
||||
variant="text"
|
||||
size="small"
|
||||
@click="confirmSelection"
|
||||
>确定</t-button>
|
||||
<span v-else class="title-placeholder" />
|
||||
</div>
|
||||
<bottom-popup
|
||||
v-model="popupVisible"
|
||||
title="选择客户"
|
||||
height="80vh"
|
||||
:right-button-text="isMultiple ? '确定' : ''"
|
||||
@left-button-click="cancelSelection"
|
||||
@right-button-click="confirmSelection"
|
||||
>
|
||||
<div class="customer-selector">
|
||||
<div class="search-wrapper">
|
||||
<t-input
|
||||
v-model.trim="keyword"
|
||||
@@ -32,11 +27,12 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</t-popup>
|
||||
</bottom-popup>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Scroller } from "timi-web";
|
||||
import { BottomPopup } from "timi-tdesign-mobile";
|
||||
import CustomerAPI from "@/api/CustomerAPI";
|
||||
import CustomerList from "@/components/CustomerList.vue";
|
||||
import type { Customer } from "@/types/Customer";
|
||||
@@ -223,27 +219,9 @@ onUnmounted(() => {
|
||||
<style scoped lang="less">
|
||||
.customer-selector {
|
||||
display: flex;
|
||||
height: 80vh;
|
||||
padding-top: 1rem;
|
||||
border-radius: 1.25rem 1.25rem 0 0;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
|
||||
.title-bar {
|
||||
padding: 0 1rem;
|
||||
display: grid;
|
||||
grid-template-columns: 4rem 1fr 4rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.title-placeholder {
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.search-wrapper {
|
||||
z-index: 1;
|
||||
padding: 1rem 0 .5rem 0;
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
<template>
|
||||
<div v-if="selectedEventList.length" class="event-option-selector" :class="{ readonly: props.readonly }">
|
||||
<section v-for="event in selectedEventList" :key="event.id" class="groups">
|
||||
<div v-if="1 < selectedEventList.length" v-text="event.name || '未命名事件'" />
|
||||
<div v-for="group in event.optionGroupList || []" :key="group.id" class="group">
|
||||
<div class="title">
|
||||
<span v-text="group.name" />
|
||||
<span class="gray tips" v-text="getGroupHint(group)" />
|
||||
</div>
|
||||
<div
|
||||
class="options"
|
||||
:role="group.selectionMode === 'SINGLE' ? 'radiogroup' : 'group'"
|
||||
:aria-label="group.name || '选项组'"
|
||||
>
|
||||
<button
|
||||
v-for="option in group.optionList"
|
||||
:key="option.id"
|
||||
type="button"
|
||||
class="option bg-white"
|
||||
:class="{
|
||||
active: isOptionSelected(event, group, option),
|
||||
readonly
|
||||
}"
|
||||
:role="group.selectionMode === 'SINGLE' ? 'radio' : 'checkbox'"
|
||||
:aria-checked="isOptionSelected(event, group, option)"
|
||||
:aria-disabled="props.readonly"
|
||||
:disabled="props.disabled"
|
||||
:tabindex="props.readonly ? -1 : undefined"
|
||||
@click="toggleOption(event, group, option)"
|
||||
>
|
||||
<span v-text="option.label" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { EventItem, EventOptionGroup, EventOptionSelectionMap } from "@/types/EventItem";
|
||||
import { normalizeEventId, normalizeOptionId } from "@/utils/eventOption";
|
||||
|
||||
defineOptions({
|
||||
name: "EventOptionSelector"
|
||||
});
|
||||
|
||||
interface Props {
|
||||
eventList: EventItem[];
|
||||
selectedEventIdList: string[];
|
||||
disabled?: boolean;
|
||||
readonly?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
disabled: false,
|
||||
readonly: false
|
||||
});
|
||||
|
||||
const selectionMap = defineModel<EventOptionSelectionMap>({required: true});
|
||||
const selectedEventList = computed(() => props.selectedEventIdList
|
||||
.map(eventId => props.eventList.find(item => normalizeEventId(item) === eventId))
|
||||
.filter((item): item is EventItem => !!item && hasOptionGroup(item)));
|
||||
|
||||
function hasOptionGroup(event: EventItem): boolean {
|
||||
return !!event.optionGroupList?.some(group => group.optionList?.length);
|
||||
}
|
||||
|
||||
function getSelectedIdList(event: EventItem, group: EventOptionGroup): string[] {
|
||||
return selectionMap.value[normalizeEventId(event)]?.[normalizeOptionId(group)] || [];
|
||||
}
|
||||
|
||||
function isOptionSelected(event: EventItem, group: EventOptionGroup, option: EventOptionGroup["optionList"][number]): boolean {
|
||||
const optionId = normalizeOptionId(option);
|
||||
return !!optionId && getSelectedIdList(event, group).includes(optionId);
|
||||
}
|
||||
|
||||
function toggleOption(event: EventItem, group: EventOptionGroup, option: EventOptionGroup["optionList"][number]): void {
|
||||
const optionId = normalizeOptionId(option);
|
||||
if (!optionId || props.disabled || props.readonly) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedIdList = getSelectedIdList(event, group);
|
||||
if (group.selectionMode === "SINGLE") {
|
||||
if (selectedIdList.includes(optionId) && !group.required) {
|
||||
updateSelection(event, group, []);
|
||||
return;
|
||||
}
|
||||
updateSelection(event, group, optionId);
|
||||
return;
|
||||
}
|
||||
|
||||
updateSelection(
|
||||
event,
|
||||
group,
|
||||
selectedIdList.includes(optionId)
|
||||
? selectedIdList.filter(selectedId => selectedId !== optionId)
|
||||
: [...selectedIdList, optionId]
|
||||
);
|
||||
}
|
||||
|
||||
function updateSelection(event: EventItem, group: EventOptionGroup, value: string | string[] | number): void {
|
||||
const eventId = normalizeEventId(event);
|
||||
const groupId = normalizeOptionId(group);
|
||||
if (!eventId || !groupId || props.disabled || props.readonly) {
|
||||
return;
|
||||
}
|
||||
const selectedIdList = Array.isArray(value) ? value.map(String) : value ? [String(value)] : [];
|
||||
const eventSelectionMap = selectionMap.value[eventId] || {};
|
||||
selectionMap.value[eventId] = {
|
||||
...eventSelectionMap,
|
||||
[groupId]: selectedIdList
|
||||
};
|
||||
}
|
||||
|
||||
function getGroupHint(group: EventOptionGroup): string {
|
||||
const modeText = group.selectionMode === "MULTIPLE" ? "可多选" : "单选";
|
||||
return group.required ? `${modeText},必选` : `${modeText},非必选`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.event-option-selector {
|
||||
gap: .75rem;
|
||||
display: flex;
|
||||
border-radius: 1rem;
|
||||
flex-direction: column;
|
||||
|
||||
.groups {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.group {
|
||||
gap: .75rem;
|
||||
display: flex;
|
||||
padding: .75rem .75rem .75rem 0;
|
||||
position: relative;
|
||||
border-radius: .75rem;
|
||||
flex-direction: column;
|
||||
|
||||
&:not(:last-child):after {
|
||||
content: "";
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
position: absolute;
|
||||
background: var(--td-border-level-1-color);
|
||||
}
|
||||
|
||||
.title {
|
||||
gap: .5rem;
|
||||
color: var(--td-gray-color-9);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.tips {
|
||||
font-size: .8125rem;
|
||||
}
|
||||
}
|
||||
|
||||
.options {
|
||||
gap: 1rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
min-width: 0;
|
||||
|
||||
.option {
|
||||
font: inherit;
|
||||
color: #AAA;
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
border: none;
|
||||
outline: 1px solid #BBB;
|
||||
padding: .25rem .875rem;
|
||||
min-width: 0;
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
transition: border-color .2s ease, color .2s ease;
|
||||
appearance: none;
|
||||
line-height: 1.5;
|
||||
border-radius: .5rem;
|
||||
|
||||
&.active {
|
||||
color: var(--td-brand-color);
|
||||
outline: 2px solid var(--td-brand-color);
|
||||
}
|
||||
|
||||
&.readonly {
|
||||
color: var(--td-text-color-secondary);
|
||||
border-color: var(--td-text-color-secondary);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: .6;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global(html.theme-dark) {
|
||||
.event-option-selector {
|
||||
.group {
|
||||
background: rgba(15, 23, 42, .72);
|
||||
}
|
||||
|
||||
.hint {
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user