You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

726 lines
20 KiB

<template>
<div class="context-menu" v-if="visible" :style="positionStyle">
<div class="menu-section">
<div class="menu-item" @click="handleDelete">
<span class="menu-icon">🗑</span>
<span>删除</span>
</div>
</div>
<!-- 线段特有选项 -->
<div class="menu-section" v-if="entityData.type === 'line' && !entityData.routeId">
<div class="menu-title">线段属性</div>
<div class="menu-item" @click="toggleColorPicker('color')">
<span class="menu-icon">🎨</span>
<span>颜色</span>
<span class="menu-preview" :style="{backgroundColor: entityData.color}"></span>
</div>
<!-- 颜色选择器 -->
<div class="color-picker-container" v-if="showColorPickerFor === 'color'">
<div class="color-grid">
<div
v-for="color in presetColors"
:key="color"
class="color-item"
:style="{backgroundColor: color}"
@click="selectColor('color', color)"
:class="{ active: entityData.color === color }"
></div>
</div>
</div>
<div class="menu-item" @click="toggleWidthPicker">
<span class="menu-icon">📏</span>
<span>线宽</span>
<span class="menu-value">{{ entityData.width }}px</span>
</div>
<!-- 线宽选择器 -->
<div class="width-picker-container" v-if="showWidthPicker">
<div class="width-grid">
<div
v-for="width in presetWidths"
:key="width"
class="width-item"
@click="selectWidth(width)"
:class="{ active: entityData.width === width }"
>
{{ width }}px
</div>
</div>
</div>
<div class="menu-item" @click="toggleBearingTypeMenu">
<span class="menu-icon">🧭</span>
<span>方位角类型</span>
<span class="menu-value">{{ entityData.bearingType === 'magnetic' ? '磁方位' : '真方位' }}</span>
</div>
<!-- 方位角类型选择菜单 -->
<div class="sub-menu" v-if="showBearingTypeMenu">
<div class="sub-menu-item" @click="selectBearingType('true')" :class="{ active: entityData.bearingType === 'true' }">
<span>真方位</span>
</div>
<div class="sub-menu-item" @click="selectBearingType('magnetic')" :class="{ active: entityData.bearingType === 'magnetic' }">
<span>磁方位</span>
</div>
</div>
</div>
<!-- 点特有选项 -->
<div class="menu-section" v-if="entityData.type === 'point'">
<div class="menu-title">点属性</div>
<div class="menu-item" @click="toggleColorPicker('color')">
<span class="menu-icon">🎨</span>
<span>颜色</span>
<span class="menu-preview" :style="{backgroundColor: entityData.color}"></span>
</div>
<!-- 颜色选择器 -->
<div class="color-picker-container" v-if="showColorPickerFor === 'color'">
<div class="color-grid">
<div
v-for="color in presetColors"
:key="color"
class="color-item"
:style="{backgroundColor: color}"
@click="selectColor('color', color)"
:class="{ active: entityData.color === color }"
></div>
</div>
</div>
<div class="menu-item" @click="toggleSizePicker">
<span class="menu-icon">🔵</span>
<span>大小</span>
<span class="menu-value">{{ entityData.size }}px</span>
</div>
<!-- 大小选择器 -->
<div class="size-picker-container" v-if="showSizePicker">
<div class="size-grid">
<div
v-for="size in presetSizes"
:key="size"
class="size-item"
@click="selectSize(size)"
:class="{ active: entityData.size === size }"
>
{{ size }}px
</div>
</div>
</div>
</div>
<!-- 多边形特有选项 -->
<div class="menu-section" v-if="entityData.type === 'polygon' || entityData.type === 'rectangle' || entityData.type === 'circle' || entityData.type === 'sector'">
<div class="menu-title">填充属性</div>
<div class="menu-item" @click="toggleColorPicker('color')">
<span class="menu-icon">🎨</span>
<span>填充色</span>
<span class="menu-preview" :style="{backgroundColor: entityData.color}"></span>
</div>
<!-- 颜色选择器 -->
<div class="color-picker-container" v-if="showColorPickerFor === 'color'">
<div class="color-grid">
<div
v-for="color in presetColors"
:key="color"
class="color-item"
:style="{backgroundColor: color}"
@click="selectColor('color', color)"
:class="{ active: entityData.color === color }"
></div>
</div>
</div>
<div class="menu-item" @click="toggleOpacityPicker">
<span class="menu-icon">🌫️</span>
<span>透明度</span>
<span class="menu-value">{{ Math.round(entityData.opacity * 100) }}%</span>
</div>
<!-- 透明度选择器 -->
<div class="opacity-picker-container" v-if="showOpacityPicker">
<div class="opacity-grid">
<div
v-for="opacity in presetOpacities"
:key="opacity"
class="opacity-item"
@click="selectOpacity(opacity)"
:class="{ active: Math.round(entityData.opacity * 100) === Math.round(opacity * 100) }"
>
{{ Math.round(opacity * 100) }}%
</div>
</div>
</div>
<div class="menu-item" @click="toggleColorPicker('borderColor')">
<span class="menu-icon">🖌️</span>
<span>边框色</span>
<span class="menu-preview" :style="{backgroundColor: entityData.borderColor || entityData.color}"></span>
</div>
<!-- 边框颜色选择器 -->
<div class="color-picker-container" v-if="showColorPickerFor === 'borderColor'">
<div class="color-grid">
<div
v-for="color in presetColors"
:key="color"
class="color-item"
:style="{backgroundColor: color}"
@click="selectColor('borderColor', color)"
:class="{ active: (entityData.borderColor || entityData.color) === color }"
></div>
</div>
</div>
<div class="menu-item" @click="toggleWidthPicker">
<span class="menu-icon">📏</span>
<span>边框宽</span>
<span class="menu-value">{{ entityData.width }}px</span>
</div>
<!-- 边框宽度选择器 -->
<div class="width-picker-container" v-if="showWidthPicker">
<div class="width-grid">
<div
v-for="width in presetWidths"
:key="width"
class="width-item"
@click="selectWidth(width)"
:class="{ active: entityData.width === width }"
>
{{ width }}px
</div>
</div>
</div>
</div>
<!-- 箭头特有选项 -->
<div class="menu-section" v-if="entityData.type === 'arrow'">
<div class="menu-title">箭头属性</div>
<div class="menu-item" @click="toggleColorPicker('color')">
<span class="menu-icon">🎨</span>
<span>颜色</span>
<span class="menu-preview" :style="{backgroundColor: entityData.color}"></span>
</div>
<!-- 颜色选择器 -->
<div class="color-picker-container" v-if="showColorPickerFor === 'color'">
<div class="color-grid">
<div
v-for="color in presetColors"
:key="color"
class="color-item"
:style="{backgroundColor: color}"
@click="selectColor('color', color)"
:class="{ active: entityData.color === color }"
></div>
</div>
</div>
<div class="menu-item" @click="toggleWidthPicker">
<span class="menu-icon">📏</span>
<span>线宽</span>
<span class="menu-value">{{ entityData.width }}px</span>
</div>
<!-- 线宽选择器 -->
<div class="width-picker-container" v-if="showWidthPicker">
<div class="width-grid">
<div
v-for="width in presetWidths"
:key="width"
class="width-item"
@click="selectWidth(width)"
:class="{ active: entityData.width === width }"
>
{{ width }}px
</div>
</div>
</div>
</div>
<!-- 文本特有选项 -->
<div class="menu-section" v-if="entityData.type === 'text'">
<div class="menu-title">文本属性</div>
<div class="menu-item" @click="toggleColorPicker('color')">
<span class="menu-icon">🎨</span>
<span>文字颜色</span>
<span class="menu-preview" :style="{backgroundColor: entityData.color}"></span>
</div>
<!-- 颜色选择器 -->
<div class="color-picker-container" v-if="showColorPickerFor === 'color'">
<div class="color-grid">
<div
v-for="color in presetColors"
:key="color"
class="color-item"
:style="{backgroundColor: color}"
@click="selectColor('color', color)"
:class="{ active: entityData.color === color }"
></div>
</div>
</div>
<div class="menu-item" @click="showFontPicker">
<span class="menu-icon">📝</span>
<span>字体</span>
<span class="menu-value">{{ getFontName(entityData.font) }}</span>
</div>
<div class="menu-item" @click="toggleFontSizePicker">
<span class="menu-icon">🔤</span>
<span>字号</span>
<span class="menu-value">{{ getFontSize(entityData.font) }}px</span>
</div>
<!-- 字号选择器 -->
<div class="font-size-picker-container" v-if="showFontSizePicker">
<div class="font-size-grid">
<div
v-for="size in presetFontSizes"
:key="size"
class="font-size-item"
@click="selectFontSize(size)"
:class="{ active: getFontSize(entityData.font) === size.toString() }"
>
{{ size }}px
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'ContextMenu',
props: {
visible: {
type: Boolean,
default: false
},
position: {
type: Object,
default: () => ({ x: 0, y: 0 })
},
entityData: {
type: Object,
default: null
}
},
data() {
return {
showColorPickerFor: null,
showWidthPicker: false,
showSizePicker: false,
showOpacityPicker: false,
showFontSizePicker: false,
showBearingTypeMenu: false,
presetColors: [
'#FF0000', '#00FF00', '#0000FF', '#FFFF00', '#FF00FF', '#00FFFF',
'#FF6600', '#663399', '#999999', '#000000', '#FFFFFF', '#FF99CC',
'#CC99FF', '#99CCFF', '#99FF99', '#FFFF99', '#FFCC99', '#FF9999'
],
presetWidths: [1, 2, 3, 4, 5, 6, 8, 10, 12],
presetSizes: [6, 8, 10, 12, 14, 16, 18, 20, 24],
presetOpacities: [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0],
presetFontSizes: [8, 10, 12, 14, 16, 18, 20, 24, 28, 32]
}
},
computed: {
positionStyle() {
return {
left: this.position.x + 'px',
top: this.position.y + 'px'
}
}
},
methods: {
handleDelete() {
this.$emit('delete')
},
toggleColorPicker(property) {
if (this.showColorPickerFor === property) {
this.showColorPickerFor = null
} else {
// 隐藏其他选择器
this.showWidthPicker = false
this.showSizePicker = false
this.showOpacityPicker = false
this.showFontSizePicker = false
this.showColorPickerFor = property
}
},
selectColor(property, color) {
this.$emit('update-property', property, color)
this.showColorPickerFor = null
},
toggleWidthPicker() {
if (this.showWidthPicker) {
this.showWidthPicker = false
} else {
// 隐藏其他选择器
this.showColorPickerFor = null
this.showSizePicker = false
this.showOpacityPicker = false
this.showFontSizePicker = false
this.showWidthPicker = true
}
},
selectWidth(width) {
this.$emit('update-property', 'width', width)
this.showWidthPicker = false
},
toggleSizePicker() {
if (this.showSizePicker) {
this.showSizePicker = false
} else {
// 隐藏其他选择器
this.showColorPickerFor = null
this.showWidthPicker = false
this.showOpacityPicker = false
this.showFontSizePicker = false
this.showSizePicker = true
}
},
selectSize(size) {
this.$emit('update-property', 'size', size)
this.showSizePicker = false
},
toggleOpacityPicker() {
if (this.showOpacityPicker) {
this.showOpacityPicker = false
} else {
// 隐藏其他选择器
this.showColorPickerFor = null
this.showWidthPicker = false
this.showSizePicker = false
this.showFontSizePicker = false
this.showOpacityPicker = true
}
},
selectOpacity(opacity) {
this.$emit('update-property', 'opacity', opacity)
this.showOpacityPicker = false
},
toggleFontSizePicker() {
if (this.showFontSizePicker) {
this.showFontSizePicker = false
} else {
// 隐藏其他选择器
this.showColorPickerFor = null
this.showWidthPicker = false
this.showSizePicker = false
this.showOpacityPicker = false
this.showFontSizePicker = true
}
},
selectFontSize(size) {
const fontName = this.getFontName(this.entityData.font)
this.$emit('update-property', 'font', `${size}px ${fontName}`)
this.showFontSizePicker = false
},
showFontPicker() {
const fonts = ['Arial', 'Microsoft YaHei', 'SimSun', 'SimHei', 'KaiTi']
const fontName = prompt(`请选择字体:\n${fonts.map((f, i) => `${i+1}. ${f}`).join('\n')}\n\n输入序号:`)
if (fontName && !isNaN(fontName) && fonts[parseInt(fontName)-1]) {
const selectedFont = fonts[parseInt(fontName)-1]
const fontSize = this.getFontSize(this.entityData.font)
this.$emit('update-property', 'font', `${fontSize}px ${selectedFont}`)
}
},
getFontName(font) {
const match = font.match(/\d+px\s+(.+)/)
return match ? match[1] : 'Arial'
},
getFontSize(font) {
const match = font.match(/(\d+)px/)
return match ? match[1] : '14'
},
toggleBearingTypeMenu() {
// 切换方位角类型选择菜单的显示/隐藏
this.showBearingTypeMenu = !this.showBearingTypeMenu
// 隐藏其他选择器
if (this.showBearingTypeMenu) {
this.showColorPickerFor = null
this.showWidthPicker = false
this.showSizePicker = false
this.showOpacityPicker = false
this.showFontSizePicker = false
}
},
selectBearingType(bearingType) {
// 选择方位角类型
this.$emit('update-property', 'bearingType', bearingType)
this.showBearingTypeMenu = false
}
}
}
</script>
<style scoped>
.context-menu {
position: fixed;
z-index: 9999;
background: white;
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
padding: 8px 0;
min-width: 180px;
max-width: 220px;
}
.menu-section {
margin-bottom: 8px;
}
.menu-section:last-child {
margin-bottom: 0;
}
.menu-title {
padding: 4px 16px;
font-size: 12px;
color: #666;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.menu-item {
display: flex;
align-items: center;
padding: 8px 16px;
cursor: pointer;
transition: background-color 0.2s;
}
.menu-item:hover {
background-color: #f5f5f5;
}
.menu-icon {
margin-right: 8px;
font-size: 14px;
}
.menu-preview {
margin-left: auto;
width: 16px;
height: 16px;
border-radius: 2px;
border: 1px solid #ddd;
}
.menu-value {
margin-left: auto;
font-size: 12px;
color: #666;
min-width: 40px;
text-align: right;
}
.menu-item span {
flex: 1;
}
/* 颜色选择器样式 */
.color-picker-container {
padding: 8px 16px;
background-color: #f9f9f9;
border-top: 1px solid #eee;
border-bottom: 1px solid #eee;
}
.color-grid {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 6px;
}
.color-item {
width: 24px;
height: 24px;
border-radius: 4px;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
border: 1px solid #ddd;
}
.color-item:hover {
transform: scale(1.1);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.color-item.active {
transform: scale(1.2);
box-shadow: 0 0 0 2px white, 0 0 0 3px #007bff;
}
/* 线宽选择器样式 */
.width-picker-container {
padding: 8px 16px;
background-color: #f9f9f9;
border-top: 1px solid #eee;
border-bottom: 1px solid #eee;
}
.width-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 6px;
}
.width-item {
padding: 6px 8px;
background-color: white;
border: 1px solid #ddd;
border-radius: 4px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
font-size: 12px;
}
.width-item:hover {
background-color: #e3f2fd;
border-color: #2196f3;
}
.width-item.active {
background-color: #2196f3;
color: white;
border-color: #1976d2;
}
/* 大小选择器样式 */
.size-picker-container {
padding: 8px 16px;
background-color: #f9f9f9;
border-top: 1px solid #eee;
border-bottom: 1px solid #eee;
}
.size-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 6px;
}
.size-item {
padding: 6px 8px;
background-color: white;
border: 1px solid #ddd;
border-radius: 4px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
font-size: 12px;
}
.size-item:hover {
background-color: #e3f2fd;
border-color: #2196f3;
}
.size-item.active {
background-color: #2196f3;
color: white;
border-color: #1976d2;
}
/* 透明度选择器样式 */
.opacity-picker-container {
padding: 8px 16px;
background-color: #f9f9f9;
border-top: 1px solid #eee;
border-bottom: 1px solid #eee;
}
.opacity-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 4px;
}
.opacity-item {
padding: 4px 6px;
background-color: white;
border: 1px solid #ddd;
border-radius: 4px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
font-size: 10px;
}
.opacity-item:hover {
background-color: #e3f2fd;
border-color: #2196f3;
}
.opacity-item.active {
background-color: #2196f3;
color: white;
border-color: #1976d2;
}
/* 字号选择器样式 */
.font-size-picker-container {
padding: 8px 16px;
background-color: #f9f9f9;
border-top: 1px solid #eee;
border-bottom: 1px solid #eee;
}
.font-size-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 4px;
}
.font-size-item {
padding: 4px 6px;
background-color: white;
border: 1px solid #ddd;
border-radius: 4px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
font-size: 10px;
}
.font-size-item:hover {
background-color: #e3f2fd;
border-color: #2196f3;
}
.font-size-item.active {
background-color: #2196f3;
color: white;
border-color: #1976d2;
}
/* 子菜单样式 */
.sub-menu {
background-color: #f9f9f9;
border-top: 1px solid #eee;
border-bottom: 1px solid #eee;
padding: 4px 0;
}
.sub-menu-item {
padding: 6px 16px 6px 32px;
cursor: pointer;
transition: background-color 0.2s;
font-size: 12px;
}
.sub-menu-item:hover {
background-color: #e3f2fd;
}
.sub-menu-item.active {
background-color: #2196f3;
color: white;
}
</style>