修改下拉框电脑端

main
kangwenjing 2026-04-03 14:35:30 +08:00
parent eda76e84c0
commit 0d4a3eea8b
8 changed files with 189 additions and 65 deletions

View File

@ -4,7 +4,15 @@
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="4c558d98-824e-4a48-ba48-bd2e6172f9f4" name="更改" comment="修改定位信息 0323" />
<list default="true" id="4c558d98-824e-4a48-ba48-bd2e6172f9f4" name="更改" comment="修改定位信息 0323">
<change beforePath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/controller/DashboardController.java" beforeDir="false" afterPath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/controller/DashboardController.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/controller/ExpansionController.java" beforeDir="false" afterPath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/controller/ExpansionController.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/controller/OpportunityController.java" beforeDir="false" afterPath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/controller/OpportunityController.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/controller/OpportunityIntegrationController.java" beforeDir="false" afterPath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/controller/OpportunityIntegrationController.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/controller/WorkController.java" beforeDir="false" afterPath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/controller/WorkController.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/src/components/AdaptiveSelect.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/src/components/AdaptiveSelect.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/frontend/src/pages/Opportunities.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/frontend/src/pages/Opportunities.tsx" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
@ -84,6 +92,9 @@
<workItem from="1774576185724" duration="1651000" />
<workItem from="1774763863609" duration="16084000" />
<workItem from="1775116718799" duration="3143000" />
<workItem from="1775182869187" duration="639000" />
<workItem from="1775195780460" duration="622000" />
<workItem from="1775197297892" duration="43000" />
</task>
<task id="LOCAL-00001" summary="修改定位信息 0323">
<option name="closed" value="true" />

View File

@ -3,6 +3,7 @@ package com.unis.crm.controller;
import com.unis.crm.common.ApiResponse;
import com.unis.crm.dto.dashboard.DashboardHomeDTO;
import com.unis.crm.service.DashboardService;
import com.unisbase.common.annotation.Log;
import jakarta.validation.constraints.Min;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
@ -30,6 +31,7 @@ public class DashboardController {
}
@PostMapping("/todos/{todoId}/complete")
@Log(type = "工作台", value = "完成待办")
public ApiResponse<Void> completeTodo(
@RequestHeader("X-User-Id") @Min(1) Long userId,
@PathVariable("todoId") @Min(1) Long todoId) {

View File

@ -12,6 +12,7 @@ import com.unis.crm.dto.expansion.ExpansionOverviewDTO;
import com.unis.crm.dto.expansion.UpdateChannelExpansionRequest;
import com.unis.crm.dto.expansion.UpdateSalesExpansionRequest;
import com.unis.crm.service.ExpansionService;
import com.unisbase.common.annotation.Log;
import jakarta.validation.Valid;
import java.util.List;
import org.springframework.web.bind.annotation.GetMapping;
@ -75,6 +76,7 @@ public class ExpansionController {
}
@PostMapping("/sales")
@Log(type = "拓展管理", value = "新增售前拓展")
public ApiResponse<Long> createSales(
@RequestHeader("X-User-Id") Long userId,
@Valid @RequestBody CreateSalesExpansionRequest request) {
@ -82,6 +84,7 @@ public class ExpansionController {
}
@PostMapping("/channel")
@Log(type = "拓展管理", value = "新增渠道拓展")
public ApiResponse<Long> createChannel(
@RequestHeader("X-User-Id") Long userId,
@Valid @RequestBody CreateChannelExpansionRequest request) {
@ -89,6 +92,7 @@ public class ExpansionController {
}
@PutMapping("/sales/{id}")
@Log(type = "拓展管理", value = "编辑售前拓展")
public ApiResponse<Void> updateSales(
@RequestHeader("X-User-Id") Long userId,
@PathVariable("id") Long id,
@ -98,6 +102,7 @@ public class ExpansionController {
}
@PutMapping("/channel/{id}")
@Log(type = "拓展管理", value = "编辑渠道拓展")
public ApiResponse<Void> updateChannel(
@RequestHeader("X-User-Id") Long userId,
@PathVariable("id") Long id,
@ -107,6 +112,7 @@ public class ExpansionController {
}
@PostMapping("/{bizType}/{bizId}/followups")
@Log(type = "拓展管理", value = "新增拓展跟进")
public ApiResponse<Long> createFollowUp(
@RequestHeader("X-User-Id") Long userId,
@PathVariable("bizType") String bizType,

View File

@ -9,6 +9,7 @@ import com.unis.crm.dto.opportunity.OpportunityMetaDTO;
import com.unis.crm.dto.opportunity.OpportunityOverviewDTO;
import com.unis.crm.dto.opportunity.PushOpportunityToOmsRequest;
import com.unis.crm.service.OpportunityService;
import com.unisbase.common.annotation.Log;
import jakarta.validation.Valid;
import java.util.List;
import org.springframework.web.bind.annotation.GetMapping;
@ -50,6 +51,7 @@ public class OpportunityController {
}
@PostMapping
@Log(type = "商机管理", value = "新增商机")
public ApiResponse<Long> createOpportunity(
@RequestHeader("X-User-Id") Long userId,
@Valid @RequestBody CreateOpportunityRequest request) {
@ -57,6 +59,7 @@ public class OpportunityController {
}
@PutMapping("/{opportunityId}")
@Log(type = "商机管理", value = "编辑商机")
public ApiResponse<Long> updateOpportunity(
@RequestHeader("X-User-Id") Long userId,
@PathVariable("opportunityId") Long opportunityId,
@ -65,6 +68,7 @@ public class OpportunityController {
}
@PostMapping("/{opportunityId}/push-oms")
@Log(type = "商机管理", value = "推送商机到OMS")
public ApiResponse<Long> pushToOms(
@RequestHeader("X-User-Id") Long userId,
@PathVariable("opportunityId") Long opportunityId,
@ -73,6 +77,7 @@ public class OpportunityController {
}
@PostMapping("/{opportunityId}/followups")
@Log(type = "商机管理", value = "新增商机跟进")
public ApiResponse<Long> createFollowUp(
@RequestHeader("X-User-Id") Long userId,
@PathVariable("opportunityId") Long opportunityId,

View File

@ -5,6 +5,7 @@ import com.unis.crm.common.UnauthorizedException;
import com.unis.crm.config.InternalAuthProperties;
import com.unis.crm.dto.opportunity.UpdateOpportunityIntegrationRequest;
import com.unis.crm.service.OpportunityService;
import com.unisbase.common.annotation.Log;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import java.util.Objects;
@ -30,6 +31,7 @@ public class OpportunityIntegrationController {
}
@PutMapping("/update")
@Log(type = "商机集成", value = "同步更新商机")
public ApiResponse<Long> updateOpportunity(
HttpServletRequest httpServletRequest,
@Valid @RequestBody UpdateOpportunityIntegrationRequest request) {

View File

@ -8,6 +8,7 @@ import com.unis.crm.dto.work.CreateWorkDailyReportRequest;
import com.unis.crm.dto.work.WorkHistoryPageDTO;
import com.unis.crm.dto.work.WorkOverviewDTO;
import com.unis.crm.service.WorkService;
import com.unisbase.common.annotation.Log;
import jakarta.validation.Valid;
import java.math.BigDecimal;
import org.springframework.core.io.Resource;
@ -66,6 +67,7 @@ public class WorkController {
}
@PostMapping("/checkins")
@Log(type = "工作管理", value = "提交打卡")
public ApiResponse<Long> saveCheckIn(
@RequestHeader("X-User-Id") Long userId,
@Valid @RequestBody CreateWorkCheckInRequest request) {
@ -73,6 +75,7 @@ public class WorkController {
}
@PostMapping(path = "/checkin-photos", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@Log(type = "工作管理", value = "上传打卡照片")
public ApiResponse<String> uploadCheckInPhoto(
@RequestHeader("X-User-Id") Long userId,
@RequestPart("file") MultipartFile file) {
@ -89,6 +92,7 @@ public class WorkController {
}
@PostMapping("/daily-reports")
@Log(type = "工作管理", value = "提交日报")
public ApiResponse<Long> saveDailyReport(
@RequestHeader("X-User-Id") Long userId,
@Valid @RequestBody CreateWorkDailyReportRequest request) {

View File

@ -10,6 +10,14 @@ export type AdaptiveSelectOption = {
disabled?: boolean;
};
function getOptionLabel(option: AdaptiveSelectOption) {
const normalizedLabel = typeof option.label === "string" ? option.label.trim() : "";
if (normalizedLabel) {
return normalizedLabel;
}
return option.value;
}
type AdaptiveSelectBaseProps = {
options: AdaptiveSelectOption[];
placeholder?: string;
@ -80,20 +88,25 @@ export function AdaptiveSelect({
const containerRef = useRef<HTMLDivElement | null>(null);
const desktopDropdownRef = useRef<HTMLDivElement | null>(null);
const isMobile = useIsMobileViewport();
const [desktopDropdownStyle, setDesktopDropdownStyle] = useState<{ top: number; left: number; width: number } | null>(null);
const [desktopDropdownPlacement, setDesktopDropdownPlacement] = useState<"top" | "bottom">("bottom");
const [desktopDropdownMaxHeight, setDesktopDropdownMaxHeight] = useState(288);
const [desktopDropdownStyle, setDesktopDropdownStyle] = useState<{ left: number; width: number; top: number } | null>(null);
const selectedValues = multiple
? Array.isArray(value) ? value : []
: typeof value === "string" && value ? [value] : [];
const selectedLabel = selectedValues.length > 0
? selectedValues
.map((selectedValue) => options.find((option) => option.value === selectedValue)?.label)
.map((selectedValue) => {
const matchedOption = options.find((option) => option.value === selectedValue);
return matchedOption ? getOptionLabel(matchedOption) : selectedValue;
})
.filter((label): label is string => Boolean(label))
.join("、")
: placeholder;
const normalizedSearchKeyword = searchKeyword.trim().toLowerCase();
const visibleOptions = searchable && normalizedSearchKeyword
? options.filter((option) => {
const label = option.label.toLowerCase();
const label = getOptionLabel(option).toLowerCase();
const optionValue = option.value.toLowerCase();
return label.includes(normalizedSearchKeyword) || optionValue.includes(normalizedSearchKeyword);
})
@ -105,12 +118,24 @@ export function AdaptiveSelect({
}
const rect = containerRef.current.getBoundingClientRect();
const viewportHeight = window.innerHeight;
const safePadding = 24;
const panelChromeHeight = (searchable ? 72 : 16) + 24;
const availableBelow = Math.max(180, viewportHeight - rect.bottom - safePadding - panelChromeHeight);
const availableAbove = Math.max(180, rect.top - safePadding - panelChromeHeight);
const shouldOpenUpward = availableBelow < 280 && availableAbove > availableBelow;
const popupContentHeight = Math.min(360, shouldOpenUpward ? availableAbove : availableBelow);
setDesktopDropdownPlacement(shouldOpenUpward ? "top" : "bottom");
setDesktopDropdownMaxHeight(popupContentHeight);
setDesktopDropdownStyle({
top: rect.bottom + 8,
left: rect.left,
width: rect.width,
top: shouldOpenUpward
? Math.max(safePadding, rect.top - 8 - (popupContentHeight + panelChromeHeight))
: rect.bottom + 8,
});
}, [isMobile, open]);
}, [isMobile, multiple, open, searchable]);
useEffect(() => {
if (!open || isMobile) {
@ -192,9 +217,10 @@ export function AdaptiveSelect({
const isSelected = multiple
? selectedValues.includes(option.value)
: option.value === value;
const optionLabel = getOptionLabel(option);
return (
<button
key={`${option.value}-${option.label}`}
key={`${option.value}-${optionLabel}`}
type="button"
disabled={option.disabled}
onClick={() => handleSelect(option.value)}
@ -206,7 +232,7 @@ export function AdaptiveSelect({
option.disabled ? "cursor-not-allowed opacity-50" : "",
)}
>
<span className="break-anywhere">{option.label}</span>
<span className="break-anywhere">{optionLabel}</span>
{isSelected ? <Check className="h-4 w-4 shrink-0" /> : null}
</button>
);
@ -233,21 +259,18 @@ export function AdaptiveSelect({
<ChevronDown className={cn("h-4 w-4 shrink-0 text-slate-400 transition-transform", open ? "rotate-180" : "")} />
</button>
<AnimatePresence>
{open && !isMobile && desktopDropdownStyle && typeof document !== "undefined"
? createPortal(
<motion.div
{open && !isMobile && desktopDropdownStyle && typeof document !== "undefined"
? createPortal(
<div className="pointer-events-none fixed inset-0 z-[220]">
<div
ref={desktopDropdownRef}
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 8 }}
style={{
position: "fixed",
top: desktopDropdownStyle.top,
position: "absolute",
left: desktopDropdownStyle.left,
width: desktopDropdownStyle.width,
top: desktopDropdownStyle.top,
}}
className="z-[160] rounded-2xl border border-slate-200 bg-white p-2 shadow-2xl dark:border-slate-800 dark:bg-slate-900"
className="pointer-events-auto rounded-2xl border border-slate-200 bg-white p-2 shadow-2xl dark:border-slate-800 dark:bg-slate-900"
>
{searchable ? (
<div className="mb-2">
@ -260,18 +283,18 @@ export function AdaptiveSelect({
/>
</div>
) : null}
<div className="max-h-72 space-y-1 overflow-y-auto pr-1">
<div style={{ maxHeight: `${desktopDropdownMaxHeight}px` }} className="space-y-1 overflow-y-auto pr-1">
{visibleOptions.length > 0 ? visibleOptions.map(renderOption) : (
<div className="rounded-xl px-3 py-6 text-center text-sm text-slate-500 dark:text-slate-400">
</div>
)}
</div>
</motion.div>,
document.body,
)
: null}
</AnimatePresence>
</div>
</div>,
document.body,
)
: null}
<AnimatePresence>
{open && isMobile ? (

View File

@ -1,6 +1,7 @@
import { useEffect, useRef, useState, type ReactNode } from "react";
import { Search, Plus, Download, ChevronDown, Check, Building, Calendar, DollarSign, Activity, X, Clock, FileText, User, Tag, AlertTriangle, ListFilter } from "lucide-react";
import { motion, AnimatePresence } from "motion/react";
import { createPortal } from "react-dom";
import { createOpportunity, getExpansionOverview, getOpportunityMeta, getOpportunityOmsPreSalesOptions, getOpportunityOverview, getStoredCurrentUserId, pushOpportunityToOms, updateOpportunity, type ChannelExpansionItem, type CreateOpportunityPayload, type OmsPreSalesOption, type OpportunityDictOption, type OpportunityFollowUp, type OpportunityItem, type PushOpportunityToOmsPayload, type SalesExpansionItem } from "@/lib/auth";
import { AdaptiveSelect } from "@/components/AdaptiveSelect";
import { useIsWecomBrowser } from "@/hooks/useIsWecomBrowser";
@ -452,6 +453,14 @@ type SearchableOption = {
keywords?: string[];
};
function getSearchableOptionLabel(option: SearchableOption) {
const normalizedLabel = typeof option.label === "string" ? option.label.trim() : "";
if (normalizedLabel) {
return normalizedLabel;
}
return String(option.value ?? "");
}
function dedupeSearchableOptions(options: SearchableOption[]) {
const seenValues = new Set<SearchableOption["value"]>();
return options.filter((option) => {
@ -514,7 +523,9 @@ function SearchableSelect({
const [desktopDropdownPlacement, setDesktopDropdownPlacement] = useState<"top" | "bottom">("bottom");
const [desktopDropdownMaxHeight, setDesktopDropdownMaxHeight] = useState(256);
const containerRef = useRef<HTMLDivElement | null>(null);
const desktopDropdownRef = useRef<HTMLDivElement | null>(null);
const isMobile = useIsMobileViewport();
const [desktopDropdownStyle, setDesktopDropdownStyle] = useState<{ top: number; left: number; width: number } | null>(null);
const normalizedOptions = dedupeSearchableOptions(options);
const selectedOption = normalizedOptions.find((item) => item.value === value);
const normalizedQuery = query.trim().toLowerCase();
@ -523,7 +534,7 @@ function SearchableSelect({
return true;
}
const haystacks = [item.label, ...(item.keywords ?? [])]
const haystacks = [getSearchableOptionLabel(item), ...(item.keywords ?? [])]
.filter(Boolean)
.map((entry) => entry.toLowerCase());
@ -532,6 +543,7 @@ function SearchableSelect({
useEffect(() => {
if (!open || isMobile) {
setDesktopDropdownStyle(null);
return;
}
@ -550,12 +562,18 @@ function SearchableSelect({
setDesktopDropdownPlacement(shouldOpenUpward ? "top" : "bottom");
setDesktopDropdownMaxHeight(Math.min(320, shouldOpenUpward ? availableAbove : availableBelow));
setDesktopDropdownStyle({
top: shouldOpenUpward ? Math.max(safePadding, rect.top - 8) : rect.bottom + 8,
left: rect.left,
width: rect.width,
});
};
updateDesktopDropdownLayout();
const handlePointerDown = (event: MouseEvent) => {
if (!containerRef.current?.contains(event.target as Node)) {
const targetNode = event.target as Node;
if (!containerRef.current?.contains(targetNode) && !desktopDropdownRef.current?.contains(targetNode)) {
setOpen(false);
setQuery("");
}
@ -629,7 +647,7 @@ function SearchableSelect({
: "text-slate-700 hover:bg-slate-50 dark:text-slate-200 dark:hover:bg-slate-800"
}`}
>
<span>{item.label}</span>
<span>{getSearchableOptionLabel(item)}</span>
{item.value === value ? <Check className="h-4 w-4 shrink-0" /> : null}
</button>
))
@ -659,31 +677,37 @@ function SearchableSelect({
)}
>
<span className={selectedOption ? "text-slate-900 dark:text-white" : "text-slate-400 dark:text-slate-500"}>
{selectedOption?.label || placeholder}
{selectedOption ? getSearchableOptionLabel(selectedOption) : placeholder}
</span>
<ChevronDown className={`h-4 w-4 shrink-0 text-slate-400 transition-transform ${open ? "rotate-180" : ""}`} />
</button>
<AnimatePresence>
{open && !isMobile ? (
<motion.div
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 8 }}
className={cn(
"absolute z-20 w-full rounded-2xl border border-slate-200 bg-white p-3 shadow-xl dark:border-slate-800 dark:bg-slate-900",
desktopDropdownPlacement === "top" ? "bottom-full mb-2" : "top-full mt-2",
)}
>
<div
style={{ maxHeight: `${desktopDropdownMaxHeight}px` }}
className="overflow-y-auto overscroll-contain pr-1"
>
{renderSearchBody()}
</div>
</motion.div>
) : null}
</AnimatePresence>
{open && !isMobile && desktopDropdownStyle && typeof document !== "undefined"
? createPortal(
<div className="pointer-events-none fixed inset-0 z-[220]">
<div
ref={desktopDropdownRef}
style={{
position: "absolute",
top: desktopDropdownPlacement === "top"
? Math.max(24, desktopDropdownStyle.top - Math.min(desktopDropdownMaxHeight, 320))
: desktopDropdownStyle.top,
left: desktopDropdownStyle.left,
width: desktopDropdownStyle.width,
}}
className="pointer-events-auto rounded-2xl border border-slate-200 bg-white p-3 shadow-2xl dark:border-slate-800 dark:bg-slate-900"
>
<div
style={{ maxHeight: `${desktopDropdownMaxHeight}px` }}
className="overflow-y-auto overscroll-contain pr-1"
>
{renderSearchBody()}
</div>
</div>
</div>,
document.body,
)
: null}
<AnimatePresence>
{open && isMobile ? (
@ -748,23 +772,61 @@ function CompetitorMultiSelect({
}) {
const [open, setOpen] = useState(false);
const containerRef = useRef<HTMLDivElement | null>(null);
const desktopDropdownRef = useRef<HTMLDivElement | null>(null);
const isMobile = useIsMobileViewport();
const [desktopDropdownPlacement, setDesktopDropdownPlacement] = useState<"top" | "bottom">("bottom");
const [desktopDropdownMaxHeight, setDesktopDropdownMaxHeight] = useState(320);
const [desktopDropdownStyle, setDesktopDropdownStyle] = useState<{ left: number; width: number; top: number } | null>(null);
const summary = value.length > 0 ? value.join("、") : placeholder;
useEffect(() => {
if (!open || isMobile) {
setDesktopDropdownStyle(null);
return;
}
const updateDesktopDropdownPosition = () => {
const rect = containerRef.current?.getBoundingClientRect();
if (!rect) {
return;
}
const viewportHeight = window.innerHeight;
const safePadding = 24;
const panelChromeHeight = 132;
const availableBelow = Math.max(180, viewportHeight - rect.bottom - safePadding - panelChromeHeight);
const availableAbove = Math.max(180, rect.top - safePadding - panelChromeHeight);
const shouldOpenUpward = availableBelow < 300 && availableAbove > availableBelow;
const popupContentHeight = Math.min(360, shouldOpenUpward ? availableAbove : availableBelow);
setDesktopDropdownPlacement(shouldOpenUpward ? "top" : "bottom");
setDesktopDropdownMaxHeight(popupContentHeight);
setDesktopDropdownStyle({
left: rect.left,
width: rect.width,
top: shouldOpenUpward
? Math.max(safePadding, rect.top - 8 - (popupContentHeight + panelChromeHeight))
: rect.bottom + 8,
});
};
updateDesktopDropdownPosition();
const handlePointerDown = (event: MouseEvent) => {
if (!containerRef.current?.contains(event.target as Node)) {
const targetNode = event.target as Node;
if (!containerRef.current?.contains(targetNode) && !desktopDropdownRef.current?.contains(targetNode)) {
setOpen(false);
}
};
window.addEventListener("resize", updateDesktopDropdownPosition);
window.addEventListener("scroll", updateDesktopDropdownPosition, true);
document.addEventListener("mousedown", handlePointerDown);
return () => {
window.removeEventListener("resize", updateDesktopDropdownPosition);
window.removeEventListener("scroll", updateDesktopDropdownPosition, true);
document.removeEventListener("mousedown", handlePointerDown);
};
}, [isMobile, open]);
@ -852,22 +914,31 @@ function CompetitorMultiSelect({
<ChevronDown className={cn("h-4 w-4 shrink-0 text-slate-400 transition-transform", open && "rotate-180")} />
</button>
<AnimatePresence>
{open && !isMobile ? (
<motion.div
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 8 }}
className="absolute z-20 mt-2 w-full rounded-2xl border border-slate-200 bg-white p-3 shadow-xl dark:border-slate-800 dark:bg-slate-900"
>
<div className="mb-3">
<p className="text-sm font-semibold text-slate-900 dark:text-white"></p>
<p className="crm-field-note mt-1"></p>
</div>
{renderOptions()}
</motion.div>
) : null}
</AnimatePresence>
{open && !isMobile && desktopDropdownStyle && typeof document !== "undefined"
? createPortal(
<div className="pointer-events-none fixed inset-0 z-[220]">
<div
ref={desktopDropdownRef}
style={{
position: "absolute",
left: desktopDropdownStyle.left,
width: desktopDropdownStyle.width,
top: desktopDropdownStyle.top,
}}
className="pointer-events-auto rounded-2xl border border-slate-200 bg-white p-3 shadow-2xl dark:border-slate-800 dark:bg-slate-900"
>
<div className="mb-3 shrink-0">
<p className="text-sm font-semibold text-slate-900 dark:text-white"></p>
<p className="crm-field-note mt-1"></p>
</div>
<div style={{ maxHeight: `${desktopDropdownMaxHeight}px` }} className="overflow-y-auto pr-1">
{renderOptions()}
</div>
</div>
</div>,
document.body,
)
: null}
<AnimatePresence>
{open && isMobile ? (