index.vue 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728
  1. <template>
  2. <div class="element-config-panel" :class="{ 'is-collapsed': collapsed, 'is-visible': visible }">
  3. <!-- 折叠/展开提示条 -->
  4. <div class="panel-toggle-bar" @click="togglePanel">
  5. <div class="toggle-content">
  6. <i :class="collapsed ? 'el-icon-arrow-up' : 'el-icon-arrow-down'"></i>
  7. <span v-if="selectedElement.id">
  8. {{ getElementTypeLabel(selectedElement.typeEle) }} - {{ selectedElement.id }}
  9. <span v-if="hasUnsavedChanges" class="unsaved-indicator">*</span>
  10. </span>
  11. <span v-else>选择元素以编辑参数</span>
  12. </div>
  13. <div v-if="!collapsed && selectedElement.id" class="toggle-actions">
  14. <el-button size="mini" @click.stop="resetForm">重置</el-button>
  15. <el-button size="mini" @click.stop="cancelEdit">取消</el-button>
  16. <el-button type="primary" size="mini" @click.stop="saveElement">保存</el-button>
  17. </div>
  18. </div>
  19. <!-- 参数配置表单 -->
  20. <div v-if="!collapsed && selectedElement.id" class="panel-content">
  21. <el-form
  22. ref="elementForm"
  23. :model="formData"
  24. :rules="formRules"
  25. label-width="120px"
  26. size="small"
  27. class="element-form"
  28. @input="handleFormChange"
  29. >
  30. <!-- 点类型参数 -->
  31. <template v-if="selectedElement.typeEle === 'Point'">
  32. <div class="form-section">
  33. <div class="section-title">基础参数</div>
  34. <el-form-item label="元素ID" prop="id">
  35. <el-input v-model="formData.id" disabled />
  36. </el-form-item>
  37. <el-form-item label="名称" prop="name">
  38. <el-input v-model="formData.name" placeholder="请输入名称" />
  39. </el-form-item>
  40. <el-form-item label="X坐标(m)" prop="x">
  41. <el-input-number
  42. v-model="formData.x"
  43. :precision="3"
  44. :step="0.1"
  45. controls-position="right"
  46. style="width: 100%;"
  47. />
  48. </el-form-item>
  49. <el-form-item label="Y坐标(m)" prop="y">
  50. <el-input-number
  51. v-model="formData.y"
  52. :precision="3"
  53. :step="0.1"
  54. controls-position="right"
  55. style="width: 100%;"
  56. />
  57. </el-form-item>
  58. <el-form-item label="Z坐标(m)" prop="z">
  59. <el-input-number
  60. v-model="formData.z"
  61. :precision="3"
  62. :step="0.1"
  63. controls-position="right"
  64. style="width: 100%;"
  65. />
  66. </el-form-item>
  67. </div>
  68. <div class="form-section">
  69. <div class="section-title">高级参数</div>
  70. <el-form-item label="航偏角使能" prop="isyawfix">
  71. <el-switch v-model="formData.isyawfix" />
  72. </el-form-item>
  73. <el-form-item label="航偏角(rad)" prop="yaw">
  74. <el-input-number
  75. v-model="formData.yaw"
  76. :precision="3"
  77. :step="0.1"
  78. controls-position="right"
  79. style="width: 100%;"
  80. />
  81. </el-form-item>
  82. </div>
  83. </template>
  84. <!-- 线/曲线类型参数 -->
  85. <template v-if="selectedElement.typeEle === 'LineString'">
  86. <div class="form-section">
  87. <div class="section-title">基础参数</div>
  88. <el-form-item label="元素ID" prop="id">
  89. <el-input v-model="formData.id" disabled />
  90. </el-form-item>
  91. <el-form-item label="名称" prop="name">
  92. <el-input v-model="formData.name" placeholder="请输入名称" />
  93. </el-form-item>
  94. <el-form-item label="起点ID" prop="startid">
  95. <el-input v-model="formData.startid" disabled />
  96. </el-form-item>
  97. <el-form-item label="终点ID" prop="endid">
  98. <el-input v-model="formData.endid" disabled />
  99. </el-form-item>
  100. </div>
  101. <div class="form-section">
  102. <div class="section-title">运动方向</div>
  103. <el-form-item label="起点→终点">
  104. <div class="direction-checkboxes">
  105. <el-checkbox v-model="formData.directList[0]">前行</el-checkbox>
  106. <el-checkbox v-model="formData.directList[1]">倒行</el-checkbox>
  107. </div>
  108. </el-form-item>
  109. <el-form-item label="终点→起点">
  110. <div class="direction-checkboxes">
  111. <el-checkbox v-model="formData.directList[2]">前行</el-checkbox>
  112. <el-checkbox v-model="formData.directList[3]">倒行</el-checkbox>
  113. </div>
  114. </el-form-item>
  115. </div>
  116. <div class="form-section">
  117. <div class="section-title">速度参数</div>
  118. <el-form-item label="最大限速(m/s)" prop="maxspeed">
  119. <el-input-number
  120. v-model="formData.maxspeed"
  121. :min="0"
  122. :precision="2"
  123. :step="0.1"
  124. controls-position="right"
  125. style="width: 100%;"
  126. />
  127. </el-form-item>
  128. <el-form-item label="最小限速(m/s)" prop="minspeed">
  129. <el-input-number
  130. v-model="formData.minspeed"
  131. :min="0"
  132. :precision="2"
  133. :step="0.1"
  134. controls-position="right"
  135. style="width: 100%;"
  136. />
  137. </el-form-item>
  138. </div>
  139. <div class="form-section">
  140. <div class="section-title">车道参数</div>
  141. <el-form-item label="车道宽度(m)" prop="lanewidth">
  142. <el-input-number
  143. v-model="formData.lanewidth"
  144. :min="0"
  145. :precision="2"
  146. :step="0.1"
  147. controls-position="right"
  148. style="width: 100%;"
  149. />
  150. </el-form-item>
  151. <el-form-item label="左车道数" prop="leftlanenum">
  152. <el-input-number
  153. v-model="formData.leftlanenum"
  154. :min="0"
  155. :step="1"
  156. controls-position="right"
  157. style="width: 100%;"
  158. />
  159. </el-form-item>
  160. <el-form-item label="右车道数" prop="rightlanenum">
  161. <el-input-number
  162. v-model="formData.rightlanenum"
  163. :min="0"
  164. :step="1"
  165. controls-position="right"
  166. style="width: 100%;"
  167. />
  168. </el-form-item>
  169. </div>
  170. <div class="form-section">
  171. <div class="section-title">高级参数</div>
  172. <el-form-item label="避障方式" prop="obstype">
  173. <el-select v-model="formData.obstype" style="width: 100%;">
  174. <el-option label="停车等待" :value="0" />
  175. <el-option label="车道绕障" :value="1" />
  176. <el-option label="路网绕障" :value="2" />
  177. </el-select>
  178. </el-form-item>
  179. <el-form-item label="正向前移(m)" prop="s2eforward">
  180. <el-input-number
  181. v-model="formData.s2eforward"
  182. :precision="2"
  183. :step="0.1"
  184. controls-position="right"
  185. style="width: 100%;"
  186. />
  187. </el-form-item>
  188. <el-form-item label="逆向前移(m)" prop="e2sforward">
  189. <el-input-number
  190. v-model="formData.e2sforward"
  191. :precision="2"
  192. :step="0.1"
  193. controls-position="right"
  194. style="width: 100%;"
  195. />
  196. </el-form-item>
  197. </div>
  198. </template>
  199. <!-- 面类型参数 -->
  200. <template v-if="selectedElement.typeEle === 'Polygon'">
  201. <div class="form-section">
  202. <div class="section-title">基础参数</div>
  203. <el-form-item label="元素ID" prop="id">
  204. <el-input v-model="formData.id" disabled />
  205. </el-form-item>
  206. <el-form-item label="名称" prop="name">
  207. <el-input v-model="formData.name" placeholder="请输入名称" />
  208. </el-form-item>
  209. <el-form-item label="区域类型" prop="type">
  210. <el-select v-model="formData.type" style="width: 100%;">
  211. <el-option label="隔离区域" :value="0" />
  212. <el-option label="装饰区域" :value="1" />
  213. <el-option label="禁行区域" :value="2" />
  214. <el-option label="会车管制区" :value="3" />
  215. <el-option label="道闸管控区" :value="4" />
  216. <el-option label="GPS定位区" :value="11" />
  217. <el-option label="动态禁行区" :value="22" />
  218. </el-select>
  219. </el-form-item>
  220. <el-form-item label="颜色" prop="color">
  221. <el-color-picker v-model="formData.color" />
  222. </el-form-item>
  223. <el-form-item label="透明度" prop="transparent">
  224. <el-slider
  225. v-model="formData.transparent"
  226. :min="0"
  227. :max="255"
  228. :step="5"
  229. style="width: 100%;"
  230. />
  231. </el-form-item>
  232. </div>
  233. </template>
  234. </el-form>
  235. </div>
  236. </div>
  237. </template>
  238. <script>
  239. export default {
  240. name: 'XtElementConfigPanel',
  241. props: {
  242. // 选中的元素
  243. selectedElement: {
  244. type: Object,
  245. default: () => ({})
  246. },
  247. // 面板是否可见
  248. visible: {
  249. type: Boolean,
  250. default: false
  251. },
  252. // 初始折叠状态
  253. initialCollapsed: {
  254. type: Boolean,
  255. default: true
  256. }
  257. },
  258. data() {
  259. return {
  260. collapsed: this.initialCollapsed,
  261. formData: {},
  262. originalFormData: {},
  263. hasUnsavedChanges: false,
  264. formRules: {
  265. name: [
  266. { max: 50, message: '名称长度不能超过50个字符', trigger: 'blur' }
  267. ],
  268. x: [
  269. { type: 'number', message: 'X坐标必须是数字', trigger: 'blur' }
  270. ],
  271. y: [
  272. { type: 'number', message: 'Y坐标必须是数字', trigger: 'blur' }
  273. ],
  274. z: [
  275. { type: 'number', message: 'Z坐标必须是数字', trigger: 'blur' }
  276. ],
  277. maxspeed: [
  278. { type: 'number', min: 0, message: '最大限速必须大于等于0', trigger: 'blur' }
  279. ],
  280. minspeed: [
  281. { type: 'number', min: 0, message: '最小限速必须大于等于0', trigger: 'blur' }
  282. ],
  283. lanewidth: [
  284. { type: 'number', min: 0, message: '车道宽度必须大于0', trigger: 'blur' }
  285. ]
  286. }
  287. }
  288. },
  289. computed: {
  290. // 计算面板是否应该显示
  291. shouldShow() {
  292. return this.visible && this.selectedElement && this.selectedElement.id
  293. }
  294. },
  295. watch: {
  296. selectedElement: {
  297. handler(newElement) {
  298. if (newElement && newElement.id) {
  299. this.initFormData(newElement)
  300. this.collapsed = false
  301. } else {
  302. this.collapsed = true
  303. this.formData = {}
  304. this.hasUnsavedChanges = false
  305. }
  306. },
  307. deep: true,
  308. immediate: true
  309. },
  310. visible(newVal) {
  311. if (!newVal) {
  312. this.collapsed = true
  313. }
  314. }
  315. },
  316. mounted() {
  317. // 监听 ESC 键
  318. document.addEventListener('keydown', this.handleKeydown)
  319. },
  320. beforeDestroy() {
  321. document.removeEventListener('keydown', this.handleKeydown)
  322. },
  323. methods: {
  324. // 初始化表单数据
  325. initFormData(element) {
  326. this.formData = { ...element }
  327. // 处理点类型的坐标
  328. if (element.typeEle === 'Point' && element.position) {
  329. this.formData.x = element.position[0] || 0
  330. this.formData.y = element.position[1] || 0
  331. this.formData.z = element.position[2] || 0
  332. }
  333. // 处理线类型的方向参数
  334. if (element.typeEle === 'LineString' && !element.directList) {
  335. // 如果没有directList,根据direct值生成
  336. const value = (element.direct || 100) - 100
  337. const binary = value.toString(2).padStart(4, '0')
  338. this.formData.directList = [
  339. binary[0] === '1',
  340. binary[1] === '1',
  341. binary[2] === '1',
  342. binary[3] === '1'
  343. ]
  344. }
  345. // 保存原始数据用于重置
  346. this.originalFormData = JSON.parse(JSON.stringify(this.formData))
  347. this.hasUnsavedChanges = false
  348. },
  349. // 切换面板展开/折叠
  350. togglePanel() {
  351. if (this.selectedElement.id) {
  352. this.collapsed = !this.collapsed
  353. this.$emit('panel-toggle', !this.collapsed)
  354. }
  355. },
  356. // 处理表单变化
  357. handleFormChange() {
  358. this.hasUnsavedChanges = true
  359. },
  360. // 保存元素
  361. saveElement() {
  362. this.$refs.elementForm.validate((valid) => {
  363. if (valid) {
  364. // 处理特殊字段
  365. const saveData = { ...this.formData }
  366. // 点类型:合并坐标
  367. if (this.selectedElement.typeEle === 'Point') {
  368. saveData.position = [
  369. this.formData.x || 0,
  370. this.formData.y || 0,
  371. this.formData.z || 0
  372. ]
  373. }
  374. // 线类型:处理方向参数
  375. if (this.selectedElement.typeEle === 'LineString' && this.formData.directList) {
  376. const binaryString = this.formData.directList.map(bit => (bit ? '1' : '0')).join('')
  377. const value = parseInt(binaryString, 2)
  378. saveData.direct = value + 100
  379. }
  380. this.$emit('save', saveData)
  381. this.hasUnsavedChanges = false
  382. this.originalFormData = JSON.parse(JSON.stringify(this.formData))
  383. }
  384. })
  385. },
  386. // 取消编辑
  387. cancelEdit() {
  388. if (this.hasUnsavedChanges) {
  389. this.$confirm('有未保存的更改,确定要取消吗?', '确认', {
  390. confirmButtonText: '确定',
  391. cancelButtonText: '继续编辑',
  392. type: 'warning'
  393. }).then(() => {
  394. this.resetForm()
  395. this.$emit('cancel')
  396. })
  397. } else {
  398. this.$emit('cancel')
  399. }
  400. },
  401. // 重置表单
  402. resetForm() {
  403. this.formData = JSON.parse(JSON.stringify(this.originalFormData))
  404. this.hasUnsavedChanges = false
  405. this.$refs.elementForm && this.$refs.elementForm.clearValidate()
  406. },
  407. // 获取元素类型标签
  408. getElementTypeLabel(type) {
  409. const typeMap = {
  410. 'Point': '点',
  411. 'LineString': '线',
  412. 'Polygon': '面'
  413. }
  414. return typeMap[type] || type
  415. },
  416. // 处理键盘事件
  417. handleKeydown(event) {
  418. if (!this.visible || this.collapsed) return
  419. if (event.key === 'Escape') {
  420. event.preventDefault()
  421. this.cancelEdit()
  422. } else if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) {
  423. event.preventDefault()
  424. this.saveElement()
  425. }
  426. },
  427. // 检查是否有未保存的更改
  428. checkUnsavedChanges() {
  429. return this.hasUnsavedChanges
  430. }
  431. }
  432. }
  433. </script>
  434. <style lang="scss" scoped>
  435. .element-config-panel {
  436. position: fixed;
  437. bottom: 0;
  438. left: 0;
  439. right: 0;
  440. z-index: 1000;
  441. background: #fff;
  442. box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.1);
  443. border-top: 1px solid #e4e7ed;
  444. transform: translateY(100%);
  445. transition: all 0.3s ease;
  446. &.is-visible {
  447. transform: translateY(0);
  448. }
  449. &.is-collapsed {
  450. .panel-content {
  451. height: 0;
  452. overflow: hidden;
  453. }
  454. }
  455. .panel-toggle-bar {
  456. display: flex;
  457. align-items: center;
  458. justify-content: space-between;
  459. padding: 12px 24px;
  460. background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
  461. border-bottom: 1px solid #e2e8f0;
  462. cursor: pointer;
  463. user-select: none;
  464. min-height: 48px;
  465. &:hover {
  466. background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%);
  467. }
  468. .toggle-content {
  469. display: flex;
  470. align-items: center;
  471. gap: 8px;
  472. font-size: 14px;
  473. font-weight: 500;
  474. color: #475569;
  475. i {
  476. font-size: 16px;
  477. transition: transform 0.2s ease;
  478. }
  479. .unsaved-indicator {
  480. color: #f56c6c;
  481. font-weight: bold;
  482. margin-left: 4px;
  483. }
  484. }
  485. .toggle-actions {
  486. display: flex;
  487. gap: 8px;
  488. .el-button {
  489. padding: 6px 12px;
  490. font-size: 12px;
  491. }
  492. }
  493. }
  494. .panel-content {
  495. max-height: 60vh;
  496. overflow-y: auto;
  497. padding: 24px;
  498. background: #fff;
  499. // 自定义滚动条
  500. &::-webkit-scrollbar {
  501. width: 6px;
  502. }
  503. &::-webkit-scrollbar-track {
  504. background: #f1f1f1;
  505. border-radius: 3px;
  506. }
  507. &::-webkit-scrollbar-thumb {
  508. background: #c1c1c1;
  509. border-radius: 3px;
  510. &:hover {
  511. background: #a8a8a8;
  512. }
  513. }
  514. }
  515. .element-form {
  516. max-width: 1200px;
  517. margin: 0 auto;
  518. .form-section {
  519. margin-bottom: 32px;
  520. padding: 20px;
  521. background: #f8fafc;
  522. border-radius: 8px;
  523. border: 1px solid #e2e8f0;
  524. &:last-child {
  525. margin-bottom: 0;
  526. }
  527. .section-title {
  528. font-size: 16px;
  529. font-weight: 600;
  530. color: #334155;
  531. margin-bottom: 16px;
  532. padding-bottom: 8px;
  533. border-bottom: 2px solid #3b82f6;
  534. position: relative;
  535. &::before {
  536. content: '';
  537. position: absolute;
  538. left: 0;
  539. bottom: -2px;
  540. width: 40px;
  541. height: 2px;
  542. background: #3b82f6;
  543. }
  544. }
  545. }
  546. .direction-checkboxes {
  547. display: flex;
  548. gap: 16px;
  549. .el-checkbox {
  550. margin-right: 0;
  551. }
  552. }
  553. // 表单项样式优化
  554. :deep(.el-form-item) {
  555. margin-bottom: 20px;
  556. .el-form-item__label {
  557. font-weight: 500;
  558. color: #374151;
  559. line-height: 1.6;
  560. }
  561. .el-form-item__content {
  562. line-height: 1.6;
  563. }
  564. }
  565. // 输入框样式
  566. :deep(.el-input) {
  567. .el-input__inner {
  568. border-radius: 6px;
  569. border: 1px solid #d1d5db;
  570. transition: all 0.2s ease;
  571. &:focus {
  572. border-color: #3b82f6;
  573. box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
  574. }
  575. }
  576. }
  577. // 数字输入框样式
  578. :deep(.el-input-number) {
  579. width: 100%;
  580. .el-input__inner {
  581. border-radius: 6px;
  582. border: 1px solid #d1d5db;
  583. text-align: left;
  584. &:focus {
  585. border-color: #3b82f6;
  586. box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
  587. }
  588. }
  589. }
  590. // 选择器样式
  591. :deep(.el-select) {
  592. .el-input__inner {
  593. border-radius: 6px;
  594. border: 1px solid #d1d5db;
  595. &:focus {
  596. border-color: #3b82f6;
  597. box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
  598. }
  599. }
  600. }
  601. // 开关样式
  602. :deep(.el-switch) {
  603. .el-switch__core {
  604. border-radius: 12px;
  605. }
  606. }
  607. // 滑块样式
  608. :deep(.el-slider) {
  609. .el-slider__runway {
  610. background: #e2e8f0;
  611. border-radius: 3px;
  612. }
  613. .el-slider__bar {
  614. background: #3b82f6;
  615. border-radius: 3px;
  616. }
  617. .el-slider__button {
  618. border: 2px solid #3b82f6;
  619. background: #fff;
  620. }
  621. }
  622. // 复选框样式
  623. :deep(.el-checkbox) {
  624. .el-checkbox__input.is-checked .el-checkbox__inner {
  625. background-color: #3b82f6;
  626. border-color: #3b82f6;
  627. }
  628. .el-checkbox__inner {
  629. border-radius: 4px;
  630. }
  631. }
  632. }
  633. }
  634. // 响应式适配
  635. @media (max-width: 768px) {
  636. .element-config-panel {
  637. .panel-toggle-bar {
  638. padding: 8px 16px;
  639. flex-direction: column;
  640. gap: 8px;
  641. .toggle-actions {
  642. width: 100%;
  643. justify-content: flex-end;
  644. }
  645. }
  646. .panel-content {
  647. padding: 16px;
  648. }
  649. .element-form {
  650. .form-section {
  651. padding: 16px;
  652. margin-bottom: 24px;
  653. }
  654. .direction-checkboxes {
  655. flex-direction: column;
  656. gap: 8px;
  657. }
  658. }
  659. }
  660. }
  661. </style>