
1.概述
在上一个版本的基础上根据需要把"引导线"改成了对话框形式的尾巴。
2. 使用
2.1 基本使用
vue
<template>
<div class="water-map-content">
<div id="cesiumContainer"></div>
<!-- 渲染多个弹窗实例 -->
<WaterMapPopup
v-for="popup in popups"
:key="popup.id"
:id="popup.id"
:visible="popup.visible"
:point-name="popup.pointName"
:position="popup.position"
:entity="popup.entity"
:viewer="viewer"
@close="closePopup"
@drag="handlePopupDrag"
/>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import * as Cesium from 'cesium'
import WaterMapPopup from '@/components/WaterMapPopup.vue'
// 弹窗相关状态
const popups = ref([])
const viewer = ref(null)
// 初始化Cesium地图
onMounted(() => {
viewer.value = new Cesium.Viewer('cesiumContainer', {
// 配置选项
})
// 添加点击事件监听
viewer.value.screenSpaceEventHandler.setInputAction((click) => {
const pickedObject = viewer.value.scene.pick(click.position)
if (Cesium.defined(pickedObject) && Cesium.defined(pickedObject.id)) {
const entity = pickedObject.id
if (entity.point) {
openPopup(entity, click.position)
}
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK)
})
// 打开弹窗
const openPopup = (entity, position) => {
const popupId = `popup_${Date.now()}_${Math.floor(Math.random() * 1000)}`
popups.value.push({
id: popupId,
visible: true,
pointName: entity.name,
position: { x: position.x, y: position.y },
entity: entity
})
}
// 关闭弹窗
const closePopup = (popupId) => {
const index = popups.value.findIndex(popup => popup.id === popupId)
if (index > -1) {
popups.value.splice(index, 1)
}
}
// 处理弹窗拖拽
const handlePopupDrag = (dragInfo) => {
const popup = popups.value.find(p => p.id === dragInfo.id)
if (popup) {
popup.position = dragInfo.position
}
}
</script>
2.2 使用
2.2.1 自定义弹窗内容
vue
<template>
<div class="water-map-content">
<!-- 其他代码 -->
<WaterMapPopup
v-for="popup in popups"
:key="popup.id"
:id="popup.id"
:visible="popup.visible"
:point-name="popup.pointName"
:position="popup.position"
:entity="popup.entity"
:viewer="viewer"
@close="closePopup"
@drag="handlePopupDrag"
>
<template #content>
<div class="custom-popup-content">
<h4>{{ popup.pointName }}</h4>
<p>经度: {{ popup.entity.position.getValue(viewer.clock.currentTime).toString() }}</p>
<p>纬度: {{ popup.entity.position.getValue(viewer.clock.currentTime).toString() }}</p>
<button @click="doSomething(popup)">操作</button>
</div>
</template>
</WaterMapPopup>
</div>
</template>
2.2.2 批量管理弹窗
javascript
// 关闭所有弹窗
const closeAllPopups = () => {
popups.value = []
}
// 根据实体ID关闭弹窗
const closePopupByEntityId = (entityId) => {
popups.value = popups.value.filter(popup =>
!popup.entity || !popup.entity.properties || popup.entity.properties.id.getValue() !== entityId
)
}
3. 组件属性
| 属性名 | 类型 | 必填 | 默认值 | 描述 |
|---|---|---|---|---|
| id | String | 是 | - | 弹窗唯一标识 |
| visible | Boolean | 否 | false | 弹窗是否可见 |
| pointName | String | 否 | "" | 弹窗标题 |
| position | Object | 否 | { x: 0, y: 0 } | 弹窗初始位置 |
| entity | Object | 否 | null | Cesium实体对象 |
| viewer | Object | 是 | - | Cesium Viewer实例 |
4. 组件事件
| 事件名 | 参数 | 描述 |
|---|---|---|
| close | id | 弹窗关闭时触发 |
| drag | { id, position } | 弹窗拖拽时触发 |
5. 工作原理
5.1 弹窗定位
- 初始定位:基于传入的position属性
- 地图移动时:通过监听Cesium的postRender事件,实时更新弹窗位置
- 拖拽后:保存偏移量,地图移动时保持相对位置
5.2 小尾巴
- 自动附着:根据控制点位置自动判断最佳附着边
- 拖拽功能:支持拖拽控制点调整小尾巴方向
- 响应式:随弹窗和地图位置变化而更新
5.3 多弹窗管理
- 唯一标识:每个弹窗需要唯一的id
- 状态管理:通过父组件的popups数组管理多个弹窗
- 事件通信:通过close和drag事件与父组件通信
6. 常见问题与解决方案
6.1 弹窗位置不更新
问题 :地图移动时,弹窗位置不变
解决方案:确保正确传递viewer和entity属性,组件会自动监听地图渲染事件
6.2 小尾巴显示异常
问题 :小尾巴方向不正确或显示异常
解决方案:检查entity.position是否正确设置,确保能获取到有效的坐标
6.3 拖拽后弹窗位置偏移
问题 :拖拽弹窗后,地图移动时弹窗位置偏移
解决方案:组件内部已处理此问题,会保存拖拽偏移量
6.4 多个弹窗显示问题
问题 :多个弹窗同时打开时显示异常
解决方案:确保每个弹窗都有唯一的id,并正确管理popups数组
7. 性能优化
- 合理控制弹窗数量:避免同时打开过多弹窗
- 使用虚拟滚动:如果弹窗列表较长,考虑使用虚拟滚动
- 优化地图事件监听:避免在弹窗组件中添加过多地图事件监听
8. 示例代码
8.1 完整示例
WaterMapPopup.vue
vue
<template>
<div class="popup-container" v-if="visible">
<!-- 引导线(对话框小尾巴) -->
<svg class="guide-line" :style="guideLineStyle" xmlns="http://www.w3.org/2000/svg">
<polygon :points="tailPoints" fill="rgba(255, 255, 255, 0.95)" stroke="rgba(0, 0, 0, 0.1)" stroke-width="1" />
<!-- 拖拽控制点 -->
<circle
:cx="dragHandlePoint.x"
:cy="dragHandlePoint.y"
r="6"
fill="white"
stroke="#208cd7"
stroke-width="2"
cursor="move"
@mousedown.stop="startTailDrag"
@mouseover="showDragHandle = true"
@mouseout="showDragHandle = false"
:opacity="showDragHandle || isTailDragging ? 1 : 0"
transition="opacity 0.2s ease"
/>
</svg>
<!-- 弹窗主体 -->
<div
class="water-map-popup"
:style="popupStyle"
@mousedown="startDrag"
>
<div class="popup-header">
<div class="drag-handle" @mousedown.stop="startDrag">
<span class="drag-icon">⋮⋮</span>
</div>
<h3>{{ pointName }}</h3>
<button class="close-btn" @click="closePopup">
<span>×</span>
</button>
</div>
<div class="popup-content">
<p>{{ pointName }}</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import * as Cesium from 'cesium'
// 定义组件属性
const props = defineProps({
visible: {
type: Boolean,
default: false
},
pointName: {
type: String,
default: ''
},
position: {
type: Object,
default: () => ({ x: 0, y: 0 })
},
entity: {
type: Object,
default: null
},
viewer: {
type: Object,
required: true
},
id: {
type: String,
required: true
}
})
// 定义事件
const emit = defineEmits(['close', 'drag'])
// 拖拽状态
const isDragging = ref(false)
const dragOffset = ref({ x: 0, y: 0 })
const localPosition = ref({ ...props.position })
const offsetFromPoint = ref({ x: 0, y: 0 }) // 保存弹窗相对于点位的偏移量
const hasDragged = ref(false) // 标记弹窗是否被拖拽过
// 小尾巴拖拽状态
const isTailDragging = ref(false)
const showDragHandle = ref(false)
const tailDragOffset = ref({ x: 0, y: 0 })
const tailAttachment = ref('bottom') // 小尾巴附着在弹窗的哪个边(top, bottom, left, right)
// 计算弹窗位置样式
const popupStyle = computed(() => {
return {
left: `${localPosition.value.x}px`,
top: `${localPosition.value.y}px`
}
})
// 计算引导线容器样式
const guideLineStyle = computed(() => {
// 获取点位屏幕坐标
const pointPosition = getPointScreenPosition()
if (!pointPosition) return { display: 'none' }
// 考虑弹窗的实际尺寸,确保小尾巴在各种情况下都能正确显示
const popupWidth = 200 // 弹窗宽度
const popupHeight = 80 // 弹窗高度
// 计算弹窗的四个角坐标
const popupLeft = localPosition.value.x - popupWidth / 2
const popupRight = localPosition.value.x + popupWidth / 2
const popupTop = localPosition.value.y
const popupBottom = localPosition.value.y + popupHeight
// 计算小尾巴的起始位置(弹窗底部中央)
const tailStartX = localPosition.value.x
const tailStartY = popupBottom
// 计算引导线容器的位置和大小,确保包含小尾巴的所有部分
const allX = [pointPosition.x, tailStartX, popupLeft, popupRight]
const allY = [pointPosition.y, tailStartY, popupTop, popupBottom]
const minX = Math.min(...allX) - 20
const minY = Math.min(...allY) - 20
const maxX = Math.max(...allX) + 20
const maxY = Math.max(...allY) + 20
return {
position: 'absolute',
left: `${minX}px`,
top: `${minY}px`,
width: `${maxX - minX}px`,
height: `${maxY - minY}px`,
zIndex: '1000',
pointerEvents: 'none'
}
})
// 计算拖拽控制点的位置
const dragHandlePoint = computed(() => {
const pointPosition = getPointScreenPosition()
if (!pointPosition) return { x: 0, y: 0 }
const style = guideLineStyle.value
const left = parseInt(style.left)
const top = parseInt(style.top)
// 计算相对于SVG容器的坐标
const pointX = pointPosition.x - left
const pointY = pointPosition.y - top
return { x: pointX, y: pointY }
})
// 计算对话框小尾巴的多边形点坐标
const tailPoints = computed(() => {
const pointPosition = getPointScreenPosition()
if (!pointPosition) return ''
const style = guideLineStyle.value
const left = parseInt(style.left)
const top = parseInt(style.top)
// 计算相对于SVG容器的坐标
const popupCenterX = localPosition.value.x - left
const popupTopY = localPosition.value.y - top // 弹窗顶部相对于SVG容器的位置
// 弹窗尺寸
const popupWidth = 200
const popupHeight = 80
// 计算弹窗的四个角坐标
const popupLeft = popupCenterX - popupWidth / 2
const popupRight = popupCenterX + popupWidth / 2
const popupBottomY = popupTopY + popupHeight
// 控制点位置(相对于SVG容器)
const controlPoint = dragHandlePoint.value
// 确定小尾巴附着在弹窗的哪个边
// 基于控制点的位置自动判断最佳附着边
const popupCenterY = popupTopY + popupHeight / 2
// 计算控制点相对于弹窗中心的位置
const dx = controlPoint.x - popupCenterX
const dy = controlPoint.y - popupCenterY
// 根据距离确定附着边
const absDx = Math.abs(dx)
const absDy = Math.abs(dy)
if (absDx > absDy) {
// 水平方向距离更远,附着在左右边
tailAttachment.value = dx > 0 ? 'right' : 'left'
} else {
// 垂直方向距离更远,附着在上下边
tailAttachment.value = dy > 0 ? 'bottom' : 'top'
}
// 计算附着点在弹窗边缘的位置
let attachX, attachY
switch (tailAttachment.value) {
case 'top':
// 附着在顶部
attachX = popupCenterX + dx * 0.2
attachY = popupTopY
break
case 'bottom':
// 附着在底部
attachX = popupCenterX + dx * 0.2
attachY = popupBottomY
break
case 'left':
// 附着在左侧
attachX = popupLeft
attachY = popupCenterY + dy * 0.2
break
case 'right':
// 附着在右侧
attachX = popupRight
attachY = popupCenterY + dy * 0.2
break
}
// 限制附着点在弹窗边缘范围内
if (tailAttachment.value === 'top' || tailAttachment.value === 'bottom') {
attachX = Math.max(popupLeft, Math.min(popupRight, attachX))
} else {
attachY = Math.max(popupTopY, Math.min(popupBottomY, attachY))
}
// 计算小尾巴的两个固定点(贴合弹窗边缘)
const tailWidth = 16 // 小尾巴宽度,与弹窗边框宽度匹配
let point1X, point1Y, point2X, point2Y
switch (tailAttachment.value) {
case 'top':
// 附着在顶部
point1X = attachX - tailWidth / 2
point1Y = attachY
point2X = attachX + tailWidth / 2
point2Y = attachY
break
case 'bottom':
// 附着在底部
point1X = attachX - tailWidth / 2
point1Y = attachY
point2X = attachX + tailWidth / 2
point2Y = attachY
break
case 'left':
// 附着在左侧
point1X = attachX
point1Y = attachY - tailWidth / 2
point2X = attachX
point2Y = attachY + tailWidth / 2
break
case 'right':
// 附着在右侧
point1X = attachX
point1Y = attachY - tailWidth / 2
point2X = attachX
point2Y = attachY + tailWidth / 2
break
}
// 小尾巴的第三个点(控制点)
const point3X = controlPoint.x
const point3Y = controlPoint.y
// 生成多边形点坐标
return `${point1X},${point1Y} ${point2X},${point2Y} ${point3X},${point3Y}`
})
// 获取点位的屏幕坐标
const getPointScreenPosition = () => {
if (!props.entity || !props.viewer) return null
const cartesianPosition = props.entity.position.getValue(props.viewer.clock.currentTime)
if (!cartesianPosition) return null
const canvasPosition = props.viewer.scene.cartesianToCanvasCoordinates(cartesianPosition)
if (!canvasPosition) return null
return { x: canvasPosition.x, y: canvasPosition.y }
}
// 开始拖拽
const startDrag = (event) => {
isDragging.value = true
dragOffset.value = {
x: event.clientX - localPosition.value.x,
y: event.clientY - localPosition.value.y
}
// 添加全局事件监听
document.addEventListener('mousemove', handleDrag)
document.addEventListener('mouseup', stopDrag)
}
// 处理拖拽
const handleDrag = (event) => {
if (!isDragging.value) return
localPosition.value = {
x: event.clientX - dragOffset.value.x,
y: event.clientY - dragOffset.value.y
}
// 发送拖拽事件
emit('drag', {
id: props.id,
position: localPosition.value
})
}
// 停止拖拽
const stopDrag = () => {
isDragging.value = false
hasDragged.value = true
// 计算并保存拖拽后的偏移量
if (props.entity) {
const pointPosition = getPointScreenPosition()
if (pointPosition) {
offsetFromPoint.value = {
x: localPosition.value.x - pointPosition.x,
y: localPosition.value.y - pointPosition.y
}
}
}
document.removeEventListener('mousemove', handleDrag)
document.removeEventListener('mouseup', stopDrag)
}
// 开始拖拽小尾巴控制点
const startTailDrag = (event) => {
isTailDragging.value = true
// 计算拖拽偏移量
const pointPosition = getPointScreenPosition()
if (pointPosition) {
const rect = event.currentTarget.getBoundingClientRect()
tailDragOffset.value = {
x: event.clientX - pointPosition.x,
y: event.clientY - pointPosition.y
}
}
// 添加全局事件监听
document.addEventListener('mousemove', handleTailDrag)
document.addEventListener('mouseup', stopTailDrag)
}
// 处理小尾巴拖拽
const handleTailDrag = (event) => {
if (!isTailDragging.value || !props.entity) return
// 更新点位的屏幕坐标(模拟拖拽控制点)
// 注意:这里我们不能直接修改点位的实际位置,只能修改弹窗相对于点位的偏移量
// 我们将通过调整offsetFromPoint来实现小尾巴控制点的拖拽效果
// 计算新的点位屏幕坐标
const newPointX = event.clientX - tailDragOffset.value.x
const newPointY = event.clientY - tailDragOffset.value.y
// 更新offsetFromPoint,使弹窗保持在原来的位置,而小尾巴指向新的控制点
const currentPointPosition = getPointScreenPosition()
if (currentPointPosition) {
offsetFromPoint.value = {
x: localPosition.value.x - newPointX,
y: localPosition.value.y - newPointY
}
// 标记弹窗已被拖拽,确保地图移动时使用新的偏移量
hasDragged.value = true
}
}
// 停止拖拽小尾巴控制点
const stopTailDrag = () => {
isTailDragging.value = false
document.removeEventListener('mousemove', handleTailDrag)
document.removeEventListener('mouseup', stopTailDrag)
}
// 关闭弹窗
const closePopup = () => {
emit('close', props.id)
}
// 监听props.position变化,更新localPosition
watch(
() => props.position,
(newPosition) => {
if (!isDragging.value) {
localPosition.value = { ...newPosition }
}
},
{ deep: true }
)
// 组件卸载时清理事件监听
onUnmounted(() => {
document.removeEventListener('mousemove', handleDrag)
document.removeEventListener('mouseup', stopDrag)
})
// 监听地图渲染事件,更新引导线和弹窗位置
onMounted(() => {
if (props.viewer) {
const updateGuideLineAndPosition = () => {
// 如果没有在拖拽状态,更新弹窗主体位置
if (!isDragging.value && props.entity) {
const pointPosition = getPointScreenPosition()
if (pointPosition) {
if (hasDragged.value) {
// 如果弹窗被拖拽过,使用偏移量计算新位置
localPosition.value = {
x: pointPosition.x + offsetFromPoint.value.x,
y: pointPosition.y + offsetFromPoint.value.y
}
} else {
// 否则使用点位的实时屏幕坐标
localPosition.value = { ...pointPosition }
}
}
}
// 触发引导线重新计算
localPosition.value = { ...localPosition.value }
}
props.viewer.scene.postRender.addEventListener(updateGuideLineAndPosition)
// 组件卸载时移除事件监听
onUnmounted(() => {
props.viewer.scene.postRender.removeEventListener(updateGuideLineAndPosition)
})
}
})
</script>
<style scoped>
.popup-container {
position: absolute;
pointer-events: none;
z-index: 1000;
}
.water-map-popup {
position: absolute;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
min-width: 200px;
z-index: 1001;
border: 1px solid rgba(0, 0, 0, 0.1);
pointer-events: auto;
transform: translate(-50%, 0);
top: 0;
left: 50%;
margin-top: 0;
}
.guide-line {
position: absolute;
pointer-events: none;
z-index: 1000;
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 15px;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
background: linear-gradient(135deg, #208cd7 0%, #1557b0 100%);
color: white;
border-radius: 8px 8px 0 0;
}
.drag-handle {
cursor: move;
display: flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
border-radius: 4px;
transition: all 0.2s ease;
}
.drag-handle:hover {
background: rgba(255, 255, 255, 0.2);
}
.drag-icon {
font-size: 12px;
line-height: 1;
}
.popup-header h3 {
margin: 0;
font-size: 14px;
font-weight: 600;
flex: 1;
text-align: center;
}
.close-btn {
background: transparent;
border: none;
color: white;
font-size: 18px;
cursor: pointer;
padding: 0;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.2s ease;
}
.close-btn:hover {
background: rgba(255, 255, 255, 0.2);
}
.popup-content {
padding: 15px;
font-size: 14px;
color: #333;
}
.popup-content p {
margin: 0;
}
.guide-line {
position: absolute;
pointer-events: none;
z-index: 1000;
}
</style>
集成使用
MapViewer.vue
vue
<template>
<div class="water-map-content">
<div id="cesiumContainer" class="cesium-container"></div>
<!-- 渲染多个弹窗实例 -->
<WaterMapPopup
v-for="popup in popups"
:key="popup.id"
:id="popup.id"
:visible="popup.visible"
:point-name="popup.pointName"
:position="popup.position"
:entity="popup.entity"
:viewer="viewer"
@close="closePopup"
@drag="handlePopupDrag"
/>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import * as Cesium from 'cesium'
import WaterMapPopup from "@/components/WaterMapPopup.vue";
import * as turf from '@turf/turf'
// 初始化Cesium Viewer实例
let viewer = null
// 弹窗相关状态
const popups = ref([]) // 保存多个弹窗实例
const generatedPoints = ref([])
// 在经纬度范围内随机生成点数据
const generateRandomPoints = () => {
const minLng = 82.67
const maxLng = 96.3
const minLat = 38.42
const maxLat = 46.17
// 生成10~20个随机点
const pointCount = Math.floor(Math.random() * 11) + 10
const points = []
for (let i = 0; i < pointCount; i++) {
const lng = minLng + Math.random() * (maxLng - minLng)
const lat = minLat + Math.random() * (maxLat - minLat)
points.push({
id: i + 1,
name: `点位${i + 1}`,
lng: lng,
lat: lat
})
}
return points
}
// 将点数据加载到地图上
const loadPointsToMap = (points) => {
points.forEach(point => {
// 创建点实体
const entity = viewer.entities.add({
position: Cesium.Cartesian3.fromDegrees(point.lng, point.lat),
point: {
pixelSize: 10,
color: Cesium.Color.RED,
outlineColor: Cesium.Color.WHITE,
outlineWidth: 2
},
name: point.name,
properties: {
id: point.id
}
})
generatedPoints.value.push(entity)
})
// 添加点击事件监听
viewer.screenSpaceEventHandler.setInputAction((click) => {
const pickedObject = viewer.scene.pick(click.position)
if (Cesium.defined(pickedObject) && Cesium.defined(pickedObject.id)) {
const entity = pickedObject.id
if (entity.point) {
// 打开弹窗,支持同时打开多个弹窗
openPopup(entity, click.position)
}
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK)
}
// 生成唯一弹窗ID
const generatePopupId = () => {
return `popup_${Date.now()}_${Math.floor(Math.random() * 1000)}`
}
// 打开弹窗
const openPopup = (entity, position) => {
// 检查是否已经打开该实体的弹窗
const existingPopup = popups.value.find(popup => popup.entity === entity)
if (existingPopup) {
return // 已存在,不再重复打开
}
// 创建新弹窗实例
const popupId = generatePopupId()
popups.value.push({
id: popupId,
visible: true,
pointName: entity.name,
position: { x: position.x, y: position.y },
entity: entity
})
}
// 关闭弹窗
const closePopup = (popupId) => {
const index = popups.value.findIndex(popup => popup.id === popupId)
if (index > -1) {
popups.value.splice(index, 1)
}
}
// 处理弹窗拖拽
const handlePopupDrag = (dragInfo) => {
const popup = popups.value.find(p => p.id === dragInfo.id)
if (popup) {
popup.position = dragInfo.position
}
}
onMounted(() => {
// 配置Cesium Ion访问令牌(如果需要)
// Cesium.Ion.defaultAccessToken = 'your-access-token'
viewer = new Cesium.Viewer('cesiumContainer', {
animation: false,
timeline: false,
homeButton: false,
sceneModePicker: false,
baseLayerPicker: false,
geocoder: false,
navigationHelpButton: false,
fullscreenButton: false,
infoBox: false,
selectionIndicator: false
})
//隐藏logo
viewer.cesiumWidget.creditContainer.style.display = "none";
viewer.imageryLayers.removeAll();
viewer.camera.setView({
destination: Cesium.Cartesian3.fromDegrees(85.0, 41.0, 3000000), // 中心点经纬度和高度
})
loadPointsToMap(generateRandomPoints());
})
// 组件卸载前清理资源
onBeforeUnmount(() => {
if (viewer) {
viewer.destroy()
viewer = null
}
})
</script>
<style scoped>
.water-map-content {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
position: relative;
}
.cesium-container {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
:deep(.cesium-viewer) {
width: 100%;
height: 100%;
}
:deep(.cesium-viewer-cesiumWidgetContainer) {
width: 100%;
height: 100%;
}
:deep(.cesium-widget) {
width: 100%;
height: 100%;
}
:deep(.cesium-widget-credits) {
display: none;
}
/* 液态玻璃风样式 */
.bingtuan-selector {
background: rgba(255, 255, 255, 0.25);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 16px;
box-shadow:
0 8px 32px rgba(29, 127, 198, 0.1),
inset 0 1px 0 rgba(255, 255, 255, 0.3);
padding: 8px 16px;
font-size: 16px;
font-weight: 600;
color: #1e293b;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
min-width: 200px;
}
</style>
9.2 小尾巴拖拽示例
- 将鼠标悬停在小尾巴的控制点上
- 点击并拖拽控制点调整小尾巴方向
- 释放鼠标完成调整