index.vue 61 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216
  1. <template>
  2. <div class="map-list-container">
  3. <!-- 顶部工具条 -->
  4. <div class="toolbar-section">
  5. <!-- 搜索和筛选区 -->
  6. <div class="toolbar-filters">
  7. <el-input
  8. v-model="searchKeyword"
  9. placeholder="搜索地图名称..."
  10. prefix-icon="el-icon-search"
  11. clearable
  12. class="search-input"
  13. @input="handleSearch"
  14. />
  15. <el-select
  16. v-model="statusFilter"
  17. placeholder="状态筛选"
  18. clearable
  19. class="status-filter"
  20. @change="handleFilterChange"
  21. >
  22. <el-option label="全部状态" value="all" />
  23. <el-option label="正常" value="ok" />
  24. <el-option label="不可用" value="down" />
  25. <el-option label="扫描中" value="scanning" />
  26. </el-select>
  27. <el-select
  28. v-model="sortField"
  29. placeholder="排序方式"
  30. class="sort-select"
  31. @change="handleSortChange"
  32. >
  33. <el-option label="最近修改" value="updated" />
  34. <el-option label="名称" value="name" />
  35. <el-option label="状态" value="status" />
  36. </el-select>
  37. </div>
  38. <!-- 操作按钮区 -->
  39. <div class="toolbar-actions">
  40. <el-button
  41. type="default"
  42. icon="el-icon-upload2"
  43. size="small"
  44. @click="handleImport"
  45. >
  46. 导入地图
  47. </el-button>
  48. <el-button
  49. type="primary"
  50. icon="el-icon-plus"
  51. size="small"
  52. @click="handleCreate"
  53. >
  54. 新建地图
  55. </el-button>
  56. <el-button
  57. class="xt-btn"
  58. :disabled="selectedMaps.length !== 1"
  59. icon="el-icon-discover"
  60. size="small"
  61. @click="openSelfExplore"
  62. >
  63. 自主探索
  64. </el-button>
  65. <el-button
  66. class="xt-btn"
  67. :disabled="selectedMaps.length !== 1"
  68. icon="el-icon-connection"
  69. size="small"
  70. @click="openRemoteExplore"
  71. >
  72. 遥控探索
  73. </el-button>
  74. <!-- 视图切换 -->
  75. <el-button-group class="view-toggle">
  76. <el-button
  77. :type="viewMode === 'card' ? 'primary' : 'default'"
  78. icon="el-icon-s-grid"
  79. size="small"
  80. @click="setViewMode('card')"
  81. title="卡片视图"
  82. />
  83. <el-button
  84. :type="viewMode === 'table' ? 'primary' : 'default'"
  85. icon="el-icon-menu"
  86. size="small"
  87. @click="setViewMode('table')"
  88. title="表格视图"
  89. />
  90. </el-button-group>
  91. </div>
  92. </div>
  93. <!-- 主要内容区域 -->
  94. <div class="main-content">
  95. <!-- 卡片视图 -->
  96. <template v-if="viewMode === 'card'">
  97. <!-- 加载骨架屏 -->
  98. <div v-if="loading" class="card-grid" :class="{ compact: isCompactMode }">
  99. <div v-for="n in 12" :key="n" class="skeleton-card">
  100. <div class="skeleton-thumb"></div>
  101. <div class="skeleton-content">
  102. <div class="skeleton-title"></div>
  103. <div class="skeleton-remark"></div>
  104. <div class="skeleton-meta"></div>
  105. </div>
  106. </div>
  107. </div>
  108. <!-- 空态 -->
  109. <div v-else-if="mapList.length === 0" class="empty-state">
  110. <div class="empty-illustration">
  111. <i class="el-icon-map-location"></i>
  112. </div>
  113. <h3 class="empty-title">暂无地图</h3>
  114. <p class="empty-description">
  115. {{ hasSearchOrFilter ? '没有找到符合条件的地图' : '您还没有创建任何地图' }}
  116. </p>
  117. <div v-if="!hasSearchOrFilter" class="empty-actions">
  118. <el-button type="primary" icon="el-icon-plus" @click="handleCreate">
  119. 新建地图
  120. </el-button>
  121. <el-button type="default" icon="el-icon-upload2" @click="handleImport">
  122. 导入地图
  123. </el-button>
  124. </div>
  125. </div>
  126. <!-- 地图卡片列表 -->
  127. <div v-else class="card-grid" :class="{ compact: isCompactMode }">
  128. <XtMapCard
  129. v-for="item in displayedList"
  130. :key="item.id || item.mapId || item.map_id"
  131. :item="item"
  132. :selectable="selectedMaps.length > 0"
  133. :selected="selectedMaps.includes(item.id)"
  134. :map-file-base="MAP_FILE_BASE"
  135. @clickNav="handleNavigation"
  136. @clickEdit="handleEdit"
  137. @clickMore="handleCardAction"
  138. @selectChange="handleSelectChange"
  139. />
  140. </div>
  141. </template>
  142. <!-- 表格视图 -->
  143. <template v-else>
  144. <div ref="tableWrap" class="table-wrapper">
  145. <el-table
  146. ref="mapTable"
  147. v-loading="loading"
  148. :data="displayedList"
  149. @selection-change="handleTableSelectionChange"
  150. class="map-table"
  151. :fit="true"
  152. style="width: 100%"
  153. :border="false"
  154. :height="useFixedHeight ? tableHeight : null"
  155. :header-cell-style="() => ({ padding: '10px 12px', whiteSpace: 'nowrap' })"
  156. :cell-style="() => ({ padding: '10px 12px' })"
  157. empty-text="暂无地图数据"
  158. >
  159. <el-table-column type="selection" width="56" align="center" :reserve-selection="true"></el-table-column>
  160. <el-table-column
  161. label="序号"
  162. type="index"
  163. :index="indexMethod"
  164. width="64"
  165. align="center"
  166. header-align="center"
  167. class-name="col-index"
  168. label-class-name="col-index-header">
  169. </el-table-column>
  170. <el-table-column prop="mapName" label="地图名称" min-width="260" show-overflow-tooltip>
  171. <template slot-scope="scope">
  172. <div class="table-name">
  173. <!-- {{ scope.row.name }} -->
  174. {{ scope.row.map }}
  175. </div>
  176. </template>
  177. </el-table-column>
  178. <el-table-column label="缩略图" width="120" align="center">
  179. <template slot-scope="scope">
  180. <div class="table-thumb">
  181. <img v-if="scope.row.thumbUrl" :src="scope.row.thumbUrl" alt="缩略图" />
  182. <i v-else class="el-icon-map-location"></i>
  183. </div>
  184. </template>
  185. </el-table-column>
  186. <el-table-column label="状态" width="120" align="center">
  187. <template slot-scope="scope">
  188. <el-tag
  189. :type="getStatusConfig(scope.row.state).type"
  190. size="mini"
  191. effect="plain"
  192. >
  193. <i :class="getStatusConfig(scope.row.state).icon"></i>
  194. {{ getStatusConfig(scope.row.state).text }}
  195. </el-tag>
  196. </template>
  197. </el-table-column>
  198. <el-table-column prop="remark" label="备注" min-width="360" show-overflow-tooltip />
  199. <el-table-column prop="updatedAt" label="更新时间" width="180" align="center">
  200. <template slot-scope="{ row }">
  201. {{ formatDateTimeCompat(pickUpdatedAt(row)) }}
  202. </template>
  203. </el-table-column>
  204. <el-table-column label="操作" width="220" header-align="center" align="right">
  205. <template slot-scope="{ row }">
  206. <div class="op-cell">
  207. <router-link :to="buildNavTo(this, row)" class="op-link">
  208. <i class="el-icon-position"></i><span class="op-text">导航</span>
  209. </router-link>
  210. <router-link :to="buildEditTo(this, row)" class="op-link">
  211. <i class="el-icon-edit"></i><span class="op-text">编辑</span>
  212. </router-link>
  213. <el-dropdown @command="cmd=>onMore(row,cmd)">
  214. <span class="el-dropdown-link">
  215. <i class="el-icon-more"></i><span class="op-text">更多</span>
  216. </span>
  217. <el-dropdown-menu slot="dropdown" class="xt-more">
  218. <el-dropdown-item command="rename"><i class="el-icon-edit"></i> 重命名</el-dropdown-item>
  219. <el-dropdown-item command="download"><i class="el-icon-download"></i> 下载地图</el-dropdown-item>
  220. <el-dropdown-item command="build"><i class="el-icon-cpu"></i> 构建地图</el-dropdown-item>
  221. <el-dropdown-item divided command="delete" class="is-danger"><i class="el-icon-delete"></i> 删除地图</el-dropdown-item>
  222. <el-dropdown-item divided command="calibrate"><i class="el-icon-position"></i> 坐标系标定</el-dropdown-item>
  223. </el-dropdown-menu>
  224. </el-dropdown>
  225. </div>
  226. </template>
  227. </el-table-column>
  228. </el-table>
  229. </div>
  230. </template>
  231. </div>
  232. <!-- 分页 -->
  233. <div v-if="!loading && mapList.length > 0" class="pagination-section">
  234. <!-- 密度切换 -->
  235. <div v-if="total > 60 && viewMode === 'card'" class="density-toggle">
  236. <el-button-group size="mini">
  237. <el-button
  238. :type="isCompactMode ? 'default' : 'primary'"
  239. @click="isCompactMode = false"
  240. >
  241. 常规
  242. </el-button>
  243. <el-button
  244. :type="isCompactMode ? 'primary' : 'default'"
  245. @click="isCompactMode = true"
  246. >
  247. 紧凑
  248. </el-button>
  249. </el-button-group>
  250. </div>
  251. <el-pagination
  252. :current-page="currentPage"
  253. :page-sizes="[12, 24, 48, 96]"
  254. :page-size="pageSize"
  255. :total="totalCount"
  256. layout="total, sizes, prev, pager, next, jumper"
  257. @size-change="handleSizeChange"
  258. @current-change="handlePageChange"
  259. />
  260. </div>
  261. <!-- 自主探索配置对话框 -->
  262. <el-dialog :visible.sync="autoExploreOpen" width="560px" append-to-body :close-on-click-modal="false" class="xt-explore-dialog">
  263. <span slot="title" class="xt-title">
  264. 自主探索
  265. <small class="xt-subtitle">为所选地图配置探索规则 · {{ selectedMapName }}</small>
  266. </span>
  267. <el-form ref="form" :model="exploreParams" :rules="rules" label-width="120px" class="xt-form">
  268. <el-form-item label="最大探索时间" prop="maxTime">
  269. <div class="xt-field">
  270. <el-input-number
  271. v-model="exploreParams.maxTime"
  272. :min="1"
  273. :step="1"
  274. controls-position="right"
  275. placeholder="请输入时间"
  276. />
  277. <span class="xt-unit">分钟</span>
  278. <span class="xt-hint">建议 1-10 分钟</span>
  279. </div>
  280. </el-form-item>
  281. <el-form-item label="最远探索距离" prop="maxDistance">
  282. <div class="xt-field">
  283. <el-input-number
  284. v-model="exploreParams.maxDistance"
  285. :min="1"
  286. :step="1"
  287. controls-position="right"
  288. placeholder="请输入距离"
  289. />
  290. <span class="xt-unit">米</span>
  291. <span class="xt-hint">建议不超过 500 米</span>
  292. </div>
  293. </el-form-item>
  294. <el-form-item label="最大探索范围" prop="maxRange">
  295. <div class="xt-field">
  296. <el-input-number
  297. v-model="exploreParams.maxRange"
  298. :min="1"
  299. :step="1"
  300. controls-position="right"
  301. placeholder="请输入范围"
  302. />
  303. <span class="xt-unit">平方米</span>
  304. <span class="xt-hint">建议 100-5000 平方米</span>
  305. </div>
  306. </el-form-item>
  307. </el-form>
  308. <span slot="footer" class="dialog-footer xt-footer">
  309. <el-button plain @click="autoExploreCancel">取 消</el-button>
  310. <el-button type="primary" @click="submitAutoExplore">开 始</el-button>
  311. </span>
  312. </el-dialog>
  313. <!-- 遥控探索确认对话框 -->
  314. <el-dialog :visible.sync="remoteExploreOpen" width="480px" append-to-body :close-on-click-modal="false" class="xt-confirm-dialog">
  315. <span slot="title" class="confirm-title">
  316. <i class="el-icon-warning" style="color: #E6A23C; margin-right: 8px;"></i>
  317. 提示
  318. </span>
  319. <div class="confirm-content">
  320. <p>将对编号为「{{ selectedMapCode }}-{{ selectedMapName }}」的地图执行遥控探索,是否继续?</p>
  321. </div>
  322. <span slot="footer" class="dialog-footer">
  323. <el-button @click="remoteExploreCancel">取 消</el-button>
  324. <el-button type="primary" @click="submitRemoteExplore">确 定</el-button>
  325. </span>
  326. </el-dialog>
  327. <!-- 下载地图dia -->
  328. <el-dialog :title="title" :visible.sync="downloadOpen" width="520px" append-to-body :close-on-click-modal="false">
  329. <div style="margin-bottom: 20px;">
  330. <span style="font-weight: bold;">请选择下载的地图组件:</span>
  331. </div>
  332. <el-checkbox-group v-model="downLoadTypes" style="margin-bottom: 10px;">
  333. <el-checkbox label="tree" :checked="true">八叉树</el-checkbox>
  334. <el-checkbox label="vector" :checked="true">矢量</el-checkbox>
  335. <el-checkbox label="las" :checked="true">las数量</el-checkbox>
  336. <el-checkbox label="tile" :checked="true">瓦片地图</el-checkbox>
  337. <el-checkbox label="task" :checked="true">任务数据</el-checkbox>
  338. </el-checkbox-group>
  339. <div slot="footer" class="dialog-footer">
  340. <el-button type="primary" @click="submitDownload">开 始</el-button>
  341. <el-button @click="downloadOpen = false">取 消</el-button>
  342. </div>
  343. </el-dialog>
  344. <!-- 构建地图dia -->
  345. <el-dialog :title="title" :visible.sync="constructOpen" width="520px" append-to-body :close-on-click-modal="false">
  346. <span style="margin-right: 10px;">构建模式:</span>
  347. <el-radio-group v-model="constructModle" size="mini">
  348. <el-radio-button label="auto">自动构建</el-radio-button>
  349. <el-radio-button label="hand">手动构建</el-radio-button>
  350. </el-radio-group>
  351. <div v-if="constructModle == 'hand'">
  352. <p style="margin-top: 20px;">请选择地图及数据:</p>
  353. <el-checkbox-group v-model="constructTypes" style="margin-bottom: 10px;">
  354. <el-checkbox label="tree" :checked="true">八叉树</el-checkbox>
  355. <el-checkbox label="las" :checked="true">las数量</el-checkbox>
  356. <el-checkbox label="tile" :checked="true">瓦片地图</el-checkbox>
  357. <el-checkbox label="potree" :checked="true">potree</el-checkbox>
  358. </el-checkbox-group>
  359. </div>
  360. <div slot="footer" class="dialog-footer">
  361. <el-button type="primary" @click="submitConstruct">开 始</el-button>
  362. <el-button @click="constructOpen = false">取 消</el-button>
  363. </div>
  364. </el-dialog>
  365. <div>
  366. <button @click="publishMsg">发布消息</button>
  367. <button @click="addTopic">动态订阅 topic3</button>
  368. <button @click="removeTopic">动态取消 topic2</button>
  369. </div>
  370. <MqttComp ref="mqtt" :topics="topics" @message-received="onMessage" />
  371. </div>
  372. </template>
  373. <script>
  374. import MqttComp from "@/components/Mqtt/mqttComp.vue";
  375. // 导入地图卡片组件
  376. import XtMapCard from '@/components/XtMapCard'
  377. // 导入日期格式化工具
  378. import { formatDateTimeCompat, pickUpdatedAt } from '@/utils/datefmt'
  379. // 导入路由辅助函数
  380. import { buildNavTo, buildEditTo, buildCalibrateTo, buildConstructTo, pickId } from '@/utils/route-helpers'
  381. // 安全获取数组函数
  382. function pickArray(...arrs){
  383. for(const a of arrs){
  384. if(Array.isArray(a) && a.length>=0) return a
  385. }
  386. return []
  387. }
  388. // 尝试导入真实 API,失败则使用 Mock
  389. let mapApi
  390. try {
  391. // 尝试导入真实 API(如果存在)
  392. mapApi = require('@/api/map/index.js')
  393. } catch (error) {
  394. // Fallback 到 Mock API
  395. mapApi = require('@/api/mock/maps')
  396. }
  397. // 地图文件服务基础URL(可根据实际情况调整)
  398. const MAP_FILE_BASE = 'http://101.35.49.102:9000' // 示例地址,实际使用时请替换
  399. export default {
  400. name: "MapList",
  401. components: {
  402. XtMapCard,
  403. MqttComp,
  404. },
  405. data() {
  406. return {
  407. // topics: ["/robot4inspection/a477be75f66fe3cb/task/target/action/goto/reply","/robot4inspection/a477be75f66fe3cb/task/target/event/arrive"], // 初始订阅的 topic
  408. topics:[],
  409. // 常量
  410. MAP_FILE_BASE,
  411. // 视图模式
  412. viewMode: 'card', // 'card' | 'table'
  413. // 加载状态
  414. loading: false,
  415. // 数据
  416. mapList: [],
  417. total: 0,
  418. currentPage: 1,
  419. pageSize: 12,
  420. // Mock 数据
  421. /* mockMapList: [
  422. {
  423. id: 1,
  424. code: 'SH001',
  425. name: '上海办公楼一层',
  426. mapName: '上海办公楼一层',
  427. showImg: 'https://placehold.co/320x180/4f46e5/white?text=Office+Floor+1',
  428. thumbUrl: 'https://placehold.co/320x180/4f46e5/white?text=Office+Floor+1',
  429. status: 'ok',
  430. use: true,
  431. top: true,
  432. remark: '主要办公区域地图,包含接待区、办公区、会议室',
  433. updatedAt: '2024-01-15T14:22:00Z'
  434. },
  435. {
  436. id: 2,
  437. code: 'FC001',
  438. name: '工厂车间A区',
  439. mapName: '工厂车间A区',
  440. showImg: 'https://placehold.co/320x180/dc2626/white?text=Factory+A',
  441. thumbUrl: 'https://placehold.co/320x180/dc2626/white?text=Factory+A',
  442. status: 'down',
  443. use: false,
  444. top: false,
  445. remark: '生产车间A区域,设备较多,需要定期更新',
  446. updatedAt: '2024-01-12T09:12:00Z'
  447. },
  448. {
  449. id: 3,
  450. code: 'WH001',
  451. name: '仓库主区域',
  452. mapName: '仓库主区域',
  453. showImg: 'https://placehold.co/320x180/f59e0b/white?text=Warehouse',
  454. thumbUrl: 'https://placehold.co/320x180/f59e0b/white?text=Warehouse',
  455. status: 'scanning',
  456. use: false,
  457. top: true,
  458. remark: '仓库主要存储区域,正在进行地图更新扫描',
  459. updatedAt: '2024-01-14T16:45:00Z'
  460. },
  461. {
  462. id: 4,
  463. code: 'RD002',
  464. name: '研发中心二楼',
  465. mapName: '研发中心二楼',
  466. showImg: 'https://placehold.co/320x180/059669/white?text=R%26D+Floor+2',
  467. thumbUrl: 'https://placehold.co/320x180/059669/white?text=R%26D+Floor+2',
  468. status: 'ok',
  469. use: false,
  470. top: false,
  471. remark: '研发中心实验室区域,包含多个实验室和会议室',
  472. updatedAt: '2024-01-11T11:30:00Z'
  473. },
  474. {
  475. id: 5,
  476. code: 'PK001',
  477. name: '停车场地下一层',
  478. mapName: '停车场地下一层',
  479. showImg: 'https://placehold.co/320x180/7c3aed/white?text=Parking+B1',
  480. thumbUrl: 'https://placehold.co/320x180/7c3aed/white?text=Parking+B1',
  481. status: 'ok',
  482. use: false,
  483. top: false,
  484. remark: '地下停车场区域图,包含车位信息和通道标识',
  485. updatedAt: '2024-01-10T08:15:00Z'
  486. },
  487. {
  488. id: 6,
  489. code: 'CF001',
  490. name: '食堂餐厅区域',
  491. mapName: '食堂餐厅区域',
  492. showImg: 'https://placehold.co/320x180/ea580c/white?text=Cafeteria',
  493. thumbUrl: 'https://placehold.co/320x180/ea580c/white?text=Cafeteria',
  494. status: 'scanning',
  495. use: false,
  496. top: false,
  497. remark: '员工食堂及餐厅区域,包含用餐区和厨房区域',
  498. updatedAt: '2024-01-09T12:40:00Z'
  499. },
  500. {
  501. id: 7,
  502. code: 'OD001',
  503. name: '户外园区',
  504. mapName: '户外园区',
  505. showImg: 'https://placehold.co/320x180/16a34a/white?text=Outdoor+Park',
  506. thumbUrl: 'https://placehold.co/320x180/16a34a/white?text=Outdoor+Park',
  507. status: 'ok',
  508. use: false,
  509. top: false,
  510. remark: '公司户外园区,包含步行道、休息区和绿化带',
  511. updatedAt: '2024-01-08T15:20:00Z'
  512. },
  513. {
  514. id: 8,
  515. code: 'MC001',
  516. name: '会议中心',
  517. mapName: '会议中心',
  518. showImg: 'https://placehold.co/320x180/0891b2/white?text=Conference',
  519. thumbUrl: 'https://placehold.co/320x180/0891b2/white?text=Conference',
  520. status: 'down',
  521. use: false,
  522. top: false,
  523. remark: '多功能会议中心,包含大型会议厅和小型讨论室',
  524. updatedAt: '2024-01-07T10:05:00Z'
  525. }
  526. ], */
  527. // 搜索和筛选
  528. searchKeyword: '',
  529. statusFilter: 'all',
  530. sortField: 'updated',
  531. sortOrder: 'desc',
  532. searchDebounceTimer: null,
  533. // 选择状态
  534. selectedMaps: [],
  535. // UI状态
  536. isCompactMode: false,
  537. useFixedHeight: false,
  538. tableHeight: 520, // 表格高度
  539. // 弹窗状态(保留原有对话框)
  540. title: "",
  541. autoExploreOpen: false, // 自主探索弹框
  542. remoteExploreOpen: false, // 遥控探索确认弹框
  543. downloadOpen: false,
  544. constructOpen: false,
  545. constructModle: 'hand',
  546. exploreParams: {
  547. maxTime: null,
  548. maxDistance: null,
  549. maxRange: null
  550. },
  551. downLoadTypes: [],
  552. constructTypes: [],
  553. rules: {
  554. maxTime: [
  555. { required: true, message: "最长时间不能为空", trigger: "blur" }
  556. ],
  557. maxDistance: [
  558. { required: true, message: "最远距离不能为空", trigger: "blur" }
  559. ],
  560. maxRange: [
  561. { required: true, message: "最大范围不能为空", trigger: "blur" }
  562. ],
  563. }
  564. };
  565. },
  566. computed: {
  567. // 原始数据:兼容不同变量名
  568. rawList(){
  569. // 按出现频率降序尝试
  570. return pickArray(this.mapList)
  571. },
  572. // 供表格/卡片使用的数据(先不做筛选,只做分页裁切;如果你已有筛选,请把你现有的筛选结果放到最前面)
  573. displayedList(){
  574. // 如果已有筛选后的数据,优先使用
  575. /* if (this.searchKeyword.trim() || this.statusFilter !== 'all') {
  576. // 使用现有的筛选逻辑
  577. let filteredData = [...this.mockMapList]
  578. // 搜索过滤
  579. if (this.searchKeyword.trim()) {
  580. const keyword = this.searchKeyword.toLowerCase()
  581. filteredData = filteredData.filter(item =>
  582. item.name.toLowerCase().includes(keyword) ||
  583. item.remark.toLowerCase().includes(keyword)
  584. )
  585. }
  586. // 状态筛选
  587. if (this.statusFilter !== 'all') {
  588. filteredData = filteredData.filter(item => item.status === this.statusFilter)
  589. }
  590. // 排序
  591. filteredData.sort((a, b) => {
  592. let aVal, bVal
  593. switch (this.sortField) {
  594. case 'name':
  595. aVal = a.name.toLowerCase()
  596. bVal = b.name.toLowerCase()
  597. break
  598. case 'status':
  599. // 状态优先级:ok > scanning > down
  600. const statusOrder = { ok: 2, scanning: 1, down: 0 }
  601. aVal = statusOrder[a.status] || 0
  602. bVal = statusOrder[b.status] || 0
  603. break
  604. case 'updated':
  605. default:
  606. aVal = new Date(a.updatedAt).getTime()
  607. bVal = new Date(b.updatedAt).getTime()
  608. break
  609. }
  610. if (this.sortOrder === 'asc') {
  611. return aVal > bVal ? 1 : aVal < bVal ? -1 : 0
  612. } else {
  613. return aVal < bVal ? 1 : aVal > bVal ? -1 : 0
  614. }
  615. })
  616. // 分页
  617. const start = (this.currentPage - 1) * this.pageSize
  618. const end = start + this.pageSize
  619. return filteredData.slice(start, end)
  620. }
  621. */
  622. // 否则使用通用分页逻辑
  623. const src = this.rawList || []
  624. console.log('src', src);
  625. const page = this.pagination?.page || this.pageNum || this.currentPage || 1
  626. const size = this.pagination?.pageSize || this.pageSize || 10
  627. const start = (page-1)*size
  628. return src.slice(start, start+size)
  629. },
  630. totalCount(){
  631. /* if (this.searchKeyword.trim() || this.statusFilter !== 'all') {
  632. // 使用现有的筛选逻辑计算总数
  633. let filteredData = [...this.mockMapList]
  634. // 搜索过滤
  635. if (this.searchKeyword.trim()) {
  636. const keyword = this.searchKeyword.toLowerCase()
  637. filteredData = filteredData.filter(item =>
  638. item.name.toLowerCase().includes(keyword) ||
  639. item.remark.toLowerCase().includes(keyword)
  640. )
  641. }
  642. // 状态筛选
  643. if (this.statusFilter !== 'all') {
  644. filteredData = filteredData.filter(item => item.status === this.statusFilter)
  645. }
  646. return filteredData.length
  647. } */
  648. const src = this.rawList || []
  649. return src.length
  650. },
  651. // 是否有搜索或筛选条件
  652. hasSearchOrFilter() {
  653. return this.searchKeyword.trim() || this.statusFilter !== 'all'
  654. },
  655. // 获取选中的地图名称
  656. selectedMapName() {
  657. if (this.selectedMaps.length === 1) {
  658. const selectedMap = this.displayedList.find(map => map.id === this.selectedMaps[0]) ||
  659. this.rawList.find(map => map.id === this.selectedMaps[0])
  660. return selectedMap ? selectedMap.name : ''
  661. }
  662. return ''
  663. },
  664. // 获取选中的地图编号
  665. selectedMapCode() {
  666. if (this.selectedMaps.length === 1) {
  667. const selectedMap = this.displayedList.find(map => map.id === this.selectedMaps[0]) ||
  668. this.rawList.find(map => map.id === this.selectedMaps[0])
  669. return selectedMap ? (selectedMap.code || selectedMap.id) : ''
  670. }
  671. return ''
  672. }
  673. },
  674. created() {
  675. this.loadViewMode()
  676. this.calculateTableHeight()
  677. },
  678. watch: {
  679. displayedList() {
  680. this.$nextTick(() => this.$refs.mapTable && this.$refs.mapTable.doLayout())
  681. }
  682. },
  683. mounted() {
  684. this.getList()
  685. this.calcTableHeight()
  686. window.addEventListener('resize', this.onResize)
  687. // 添加键盘监听
  688. this.addKeyboardListeners()
  689. // 表格渲染后重排一次,防止列宽缓存
  690. this.$nextTick(() => {
  691. this.$refs.mapTable && this.$refs.mapTable.doLayout()
  692. })
  693. },
  694. beforeDestroy() {
  695. window.removeEventListener('resize', this.onResize)
  696. // 移除键盘监听
  697. this.removeKeyboardListeners()
  698. },
  699. methods: {
  700. onMessage({ topic, message }) {
  701. console.log("收到消息:", topic, message);
  702. },
  703. publishMsg() {
  704. this.$refs.mqtt.publish("/robot4inspection/508b02dc5bcdca22/task/target/action/goto", {
  705. "timestamp" : 12345678,
  706. "args": [{"roadmap": "demo","nid": [10]}]
  707. });
  708. },
  709. addTopic() {
  710. if (!this.topics.includes("topic3")) this.topics.push("topic3");
  711. },
  712. removeTopic() {
  713. this.topics = this.topics.filter((t) => t !== "/robot4inspection/508b02dc5bcdca22/localization/pose");
  714. },
  715. // 获取地图列表
  716. async getList() {
  717. this.loading = true
  718. // 模拟加载延迟
  719. // await new Promise(resolve => setTimeout(resolve, 300))
  720. try {
  721. // TODO: 后续接入真实接口时,取消注释以下代码
  722. // const params = {
  723. // page: this.currentPage,
  724. // pageSize: this.pageSize,
  725. // keyword: this.searchKeyword,
  726. // status: this.statusFilter,
  727. // sort: this.sortField,
  728. // order: this.sortOrder
  729. // }
  730. const response = await mapApi.getMapList()
  731. console.log('Fetched map list:', response);
  732. if (response) {
  733. // this.mapList = this.processMapList(response)
  734. const { maps = [], states = [], total = 0 } = response || {}
  735. // 合并成对象数组 [{ map: 'demo', state: 'available' }]
  736. this.mapList = maps.map((map, index) => ({
  737. map,
  738. state: states[index] || null
  739. }))
  740. const test = await mapApi.getMapThumbnail(this.mapList[0].map)
  741. console.log('Processed map list:', test);
  742. const url = URL.createObjectURL(test)
  743. this.$set(this.mapList[0], 'thumbUrl', url)
  744. }
  745. // 使用 Mock 数据 - 确保 mapList 被设置
  746. // this.mapList = [...this.mockMapList]
  747. let filteredData = [...this.mapList]
  748. // 搜索过滤
  749. if (this.searchKeyword.trim()) {
  750. const keyword = this.searchKeyword.toLowerCase()
  751. filteredData = filteredData.filter(item =>
  752. item.name.toLowerCase().includes(keyword) ||
  753. item.remark.toLowerCase().includes(keyword)
  754. )
  755. }
  756. // 状态筛选
  757. if (this.statusFilter !== 'all') {
  758. filteredData = filteredData.filter(item => item.status === this.statusFilter)
  759. }
  760. // 排序
  761. filteredData.sort((a, b) => {
  762. let aVal, bVal
  763. switch (this.sortField) {
  764. case 'name':
  765. aVal = a.name.toLowerCase()
  766. bVal = b.name.toLowerCase()
  767. break
  768. case 'status':
  769. // 状态优先级:ok > scanning > down
  770. const statusOrder = { ok: 2, scanning: 1, down: 0 }
  771. aVal = statusOrder[a.status] || 0
  772. bVal = statusOrder[b.status] || 0
  773. break
  774. case 'updated':
  775. default:
  776. aVal = new Date(a.updatedAt).getTime()
  777. bVal = new Date(b.updatedAt).getTime()
  778. break
  779. }
  780. if (this.sortOrder === 'asc') {
  781. return aVal > bVal ? 1 : aVal < bVal ? -1 : 0
  782. } else {
  783. return aVal < bVal ? 1 : aVal > bVal ? -1 : 0
  784. }
  785. })
  786. // 分页
  787. this.total = filteredData.length
  788. const start = (this.currentPage - 1) * this.pageSize
  789. const end = start + this.pageSize
  790. const pagedData = filteredData.slice(start, end)
  791. // 处理后的数据仍然赋值给 mapList,但原始数据已保存
  792. this.mapList = this.processMapList(pagedData)
  793. } catch (error) {
  794. this.$message.error('获取地图列表失败: ' + error.message)
  795. this.mapList = []
  796. this.total = 0
  797. } finally {
  798. this.loading = false
  799. this.relayout()
  800. }
  801. },
  802. // 处理地图列表数据
  803. processMapList(list) {
  804. return list.map(item => ({
  805. ...item
  806. }))
  807. },
  808. // 表格相关方法
  809. calcTableHeight() {
  810. this.$nextTick(() => {
  811. if (!this.$refs.tableWrap) return
  812. const top = this.$refs.tableWrap.getBoundingClientRect().top
  813. this.tableHeight = window.innerHeight - top - 120 // 预留分页/外边距
  814. // 当行数 * 行高 + 表头高度 > 可用高度时,才启用固定高度(出现纵向滚动)
  815. const rowH = 48, headerH = 48
  816. const need = (this.mapList.length * rowH + headerH) > this.tableHeight
  817. this.useFixedHeight = need
  818. this.$refs.mapTable && this.$refs.mapTable.doLayout()
  819. })
  820. },
  821. onResize() {
  822. this.calcTableHeight()
  823. },
  824. relayout() {
  825. this.$nextTick(() => this.$refs.mapTable && this.$refs.mapTable.doLayout())
  826. },
  827. // 搜索处理(防抖)
  828. handleSearch() {
  829. clearTimeout(this.searchDebounceTimer)
  830. this.searchDebounceTimer = setTimeout(() => {
  831. this.currentPage = 1
  832. this.getList()
  833. }, 300)
  834. },
  835. // 筛选变化处理
  836. handleFilterChange() {
  837. this.currentPage = 1
  838. this.getList()
  839. },
  840. // 排序变化处理
  841. handleSortChange() {
  842. this.currentPage = 1
  843. this.getList()
  844. },
  845. // 页码变化
  846. handlePageChange(page) {
  847. this.currentPage = page
  848. this.getList()
  849. // 滚动到顶部
  850. this.$nextTick(() => {
  851. window.scrollTo({ top: 0, behavior: 'smooth' })
  852. })
  853. },
  854. // 页大小变化
  855. handleSizeChange(size) {
  856. this.pageSize = size
  857. this.currentPage = 1
  858. this.getList()
  859. },
  860. // 视图模式切换
  861. setViewMode(mode) {
  862. this.viewMode = mode
  863. this.saveViewMode()
  864. if (mode === 'table') {
  865. this.$nextTick(() => {
  866. this.calcTableHeight()
  867. this.relayout()
  868. })
  869. }
  870. },
  871. // 卡片选择变化
  872. handleSelectChange(id, checked) {
  873. if (checked) {
  874. if (!this.selectedMaps.includes(id)) {
  875. this.selectedMaps.push(id)
  876. }
  877. } else {
  878. const index = this.selectedMaps.indexOf(id)
  879. if (index > -1) {
  880. this.selectedMaps.splice(index, 1)
  881. }
  882. }
  883. },
  884. // 表格选择变化
  885. handleTableSelectionChange(selection) {
  886. this.selectedMaps = selection.map(item => item.id)
  887. },
  888. // 清除选择
  889. clearSelection() {
  890. this.selectedMaps = []
  891. },
  892. // 导航
  893. handleNavigation(id) {
  894. this.$router.push(`/map/navigation?id=${id}`)
  895. },
  896. // 编辑
  897. handleEdit(id) {
  898. this.$router.push(`/map/edit?id=${id}`)
  899. },
  900. // 表格操作方法
  901. goNav(row) {
  902. this.handleNavigation(row.id)
  903. },
  904. goEdit(row) {
  905. this.handleEdit(row.id)
  906. },
  907. async onMore(row, cmd) {
  908. const id = pickId(row);
  909. switch(cmd) {
  910. case 'rename':
  911. // 优先:已有重命名弹框/方法
  912. if (typeof this.openRenameDialog === 'function') return this.openRenameDialog(row);
  913. if (this.renameDialog) { this.renameDialog.visible = true; this.renameDialog.row = row; return; }
  914. // 兜底:Element prompt
  915. this.$prompt('请输入新的地图名称', '重命名', {
  916. inputValue: row.mapName || row.name || '',
  917. inputPattern: /\S+/,
  918. inputErrorMessage: '名称不能为空'
  919. }).then(({ value }) => {
  920. // 若有旧方法:this.renameMap(id, value)
  921. if (typeof this.renameMap === 'function') return this.renameMap(id, value);
  922. row.mapName = value;
  923. row.name = value;
  924. this.$message.success('已重命名'); // mock 刷新
  925. }).catch(()=>{});
  926. break;
  927. case 'download':
  928. // 打开下载对话框
  929. this.title = `下载地图 - ${row.name || row.mapName || ''}`;
  930. this.currentRow = row;
  931. this.downloadOpen = true;
  932. break;
  933. case 'build':
  934. // 优先:旧路由/方法
  935. if (typeof this.openBuildDialog === 'function') return this.openBuildDialog(row);
  936. if (typeof this.constructOpen === 'boolean') {
  937. this.constructOpen = true;
  938. this.title = `构建地图 - ${row.name || row.mapName || ''}`;
  939. this.currentRow = row;
  940. return;
  941. }
  942. if (typeof this.toBuildPage === 'function') return this.toBuildPage(row);
  943. return this.$router.push(buildConstructTo(row));
  944. case 'delete':
  945. // 优先:旧删除方法
  946. if (typeof this.handleDelete === 'function') {
  947. return this.$confirm('确认删除该地图?此操作不可恢复', '删除地图', {type: 'warning'})
  948. .then(() => this.handleDelete([id]))
  949. .catch(() => {});
  950. }
  951. if (typeof this.deleteMap === 'function') {
  952. return this.$confirm('确认删除该地图?此操作不可恢复', '删除地图', {type: 'warning'})
  953. .then(() => this.deleteMap(id))
  954. .catch(() => {});
  955. }
  956. this.$confirm('确认删除该地图?此操作不可恢复', '删除地图', {type: 'warning'})
  957. .then(() => {
  958. this.$emit && this.$emit('remove', row);
  959. this.$message.success('已删除(mock)');
  960. })
  961. .catch(() => {});
  962. break;
  963. case 'calibrate':
  964. // 优先:旧路由/方法
  965. if (typeof this.openCalibration === 'function') return this.openCalibration(row);
  966. return this.$router.push(buildCalibrateTo(row));
  967. default:
  968. // 兼容原有的操作
  969. return this.handleCardAction(row.id, cmd);
  970. }
  971. },
  972. // 序号计算方法
  973. indexMethod(i) {
  974. const p = this.pagination?.page || this.pageNum || this.currentPage || 1
  975. const ps = this.pagination?.pageSize || this.pageSize || 10
  976. return (p - 1) * ps + i + 1
  977. },
  978. // 卡片更多操作
  979. async handleCardAction(id, action) {
  980. switch (action) {
  981. case 'publish':
  982. await this.handlePublish([id])
  983. break
  984. case 'copy':
  985. await this.handleCopy(id)
  986. break
  987. case 'download':
  988. await this.handleDownload(id)
  989. break
  990. case 'delete':
  991. await this.handleDelete([id])
  992. break
  993. }
  994. },
  995. // 创建地图
  996. async handleCreate() {
  997. try {
  998. const { value } = await this.$prompt('请输入地图名称', '新建地图', {
  999. confirmButtonText: '确定',
  1000. cancelButtonText: '取消',
  1001. inputPattern: /^[\w\u4e00-\u9fa5_-]+$/,
  1002. inputErrorMessage: '地图名格式不正确'
  1003. })
  1004. const response = await mapApi.createMap({ name: value })
  1005. if (response.code === 200) {
  1006. this.$message.success('地图创建成功')
  1007. this.getList()
  1008. }
  1009. } catch (error) {
  1010. if (error !== 'cancel') {
  1011. this.$message.error('创建失败: ' + error.message)
  1012. }
  1013. }
  1014. },
  1015. // 导入地图
  1016. handleImport() {
  1017. this.$message.info('导入功能开发中...')
  1018. },
  1019. // 发布地图
  1020. async handlePublish(ids) {
  1021. try {
  1022. await this.$confirm(`确定要发布选中的 ${ids.length} 个地图吗?`, '发布确认', {
  1023. type: 'warning'
  1024. })
  1025. const response = await mapApi.publishMaps(ids)
  1026. if (response.code === 200) {
  1027. this.$message.success(response.message)
  1028. this.getList()
  1029. this.clearSelection()
  1030. }
  1031. } catch (error) {
  1032. if (error !== 'cancel') {
  1033. this.$message.error('发布失败: ' + error.message)
  1034. }
  1035. }
  1036. },
  1037. // 复制地图
  1038. async handleCopy(id) {
  1039. try {
  1040. const { value } = await this.$prompt('请输入新地图名称', '复制地图', {
  1041. confirmButtonText: '确定',
  1042. cancelButtonText: '取消'
  1043. })
  1044. const response = await mapApi.copyMap(id, value)
  1045. if (response.code === 200) {
  1046. this.$message.success('地图复制成功')
  1047. this.getList()
  1048. }
  1049. } catch (error) {
  1050. if (error !== 'cancel') {
  1051. this.$message.error('复制失败: ' + error.message)
  1052. }
  1053. }
  1054. },
  1055. // 下载地图
  1056. async handleDownload(id) {
  1057. try {
  1058. const response = await mapApi.downloadMap(id, ['tree', 'vector', 'tile'])
  1059. if (response.code === 200) {
  1060. // 创建临时下载链接
  1061. const link = document.createElement('a')
  1062. link.href = response.data.downloadUrl
  1063. link.download = `map_${id}.zip`
  1064. link.click()
  1065. this.$message.success('下载已开始')
  1066. }
  1067. } catch (error) {
  1068. this.$message.error('下载失败: ' + error.message)
  1069. }
  1070. },
  1071. // 删除地图
  1072. async handleDelete(ids) {
  1073. try {
  1074. await this.$confirm(`确定要删除选中的 ${ids.length} 个地图吗?删除后无法恢复。`, '删除确认', {
  1075. type: 'warning',
  1076. confirmButtonText: '确定删除',
  1077. cancelButtonText: '取消'
  1078. })
  1079. const response = await mapApi.deleteMap(ids)
  1080. if (response.code === 200) {
  1081. this.$message.success(response.message)
  1082. this.getList()
  1083. this.clearSelection()
  1084. }
  1085. } catch (error) {
  1086. if (error !== 'cancel') {
  1087. this.$message.error('删除失败: ' + error.message)
  1088. }
  1089. }
  1090. },
  1091. // 批量操作
  1092. async handleBatchPublish() {
  1093. await this.handlePublish(this.selectedMaps)
  1094. },
  1095. async handleBatchDelete() {
  1096. await this.handleDelete(this.selectedMaps)
  1097. },
  1098. // 获取状态配置
  1099. getStatusConfig(status) {
  1100. const configs = {
  1101. available: { type: 'success', icon: 'el-icon-check', text: '正常' },
  1102. unavailable: { type: 'danger', icon: 'el-icon-close', text: '不可用' },
  1103. building : { type: 'warning', icon: 'el-icon-loading', text: '正在建图' },
  1104. recording : { type: 'warning', icon: 'el-icon-loading', text: '正在录制' }
  1105. }
  1106. return configs[status] || configs.ok
  1107. },
  1108. // 格式化时间
  1109. formatDateTimeCompat,
  1110. pickUpdatedAt,
  1111. // 路由辅助函数
  1112. buildNavTo(vm, row){
  1113. return buildNavTo(row);
  1114. },
  1115. buildEditTo(vm, row){
  1116. return buildEditTo(row);
  1117. },
  1118. pickId,
  1119. // 本地存储相关
  1120. loadViewMode() {
  1121. try {
  1122. const stored = localStorage.getItem('xt-map-view-mode')
  1123. if (stored && ['card', 'table'].includes(stored)) {
  1124. this.viewMode = stored
  1125. }
  1126. } catch (error) {
  1127. console.warn('Failed to load view mode:', error)
  1128. }
  1129. },
  1130. saveViewMode() {
  1131. try {
  1132. localStorage.setItem('xt-map-view-mode', this.viewMode)
  1133. } catch (error) {
  1134. console.warn('Failed to save view mode:', error)
  1135. }
  1136. },
  1137. // 打开自主探索弹框
  1138. openSelfExplore() {
  1139. if (this.selectedMaps.length !== 1) {
  1140. this.$message.warning('请选择一个地图进行自主探索')
  1141. return
  1142. }
  1143. this.autoExploreOpen = true
  1144. },
  1145. // 打开遥控探索确认弹框
  1146. openRemoteExplore() {
  1147. if (this.selectedMaps.length !== 1) {
  1148. this.$message.warning('请选择一个地图进行遥控探索')
  1149. return
  1150. }
  1151. this.remoteExploreOpen = true
  1152. },
  1153. // 键盘监听方法
  1154. addKeyboardListeners() {
  1155. this.handleKeyboard = (event) => {
  1156. if (this.autoExploreOpen) {
  1157. if (event.key === 'Enter') {
  1158. event.preventDefault()
  1159. this.submitAutoExplore()
  1160. } else if (event.key === 'Escape') {
  1161. event.preventDefault()
  1162. this.autoExploreCancel()
  1163. }
  1164. } else if (this.remoteExploreOpen) {
  1165. if (event.key === 'Enter') {
  1166. event.preventDefault()
  1167. this.submitRemoteExplore()
  1168. } else if (event.key === 'Escape') {
  1169. event.preventDefault()
  1170. this.remoteExploreCancel()
  1171. }
  1172. }
  1173. }
  1174. document.addEventListener('keydown', this.handleKeyboard)
  1175. },
  1176. removeKeyboardListeners() {
  1177. if (this.handleKeyboard) {
  1178. document.removeEventListener('keydown', this.handleKeyboard)
  1179. }
  1180. },
  1181. // 自主探索相关方法
  1182. autoExploreCancel() {
  1183. this.autoExploreOpen = false
  1184. },
  1185. submitAutoExplore() {
  1186. this.$refs["form"].validate(valid => {
  1187. if (valid) {
  1188. this.$message.success('已开始自主探索...')
  1189. this.autoExploreOpen = false
  1190. }
  1191. })
  1192. },
  1193. // 遥控探索相关方法
  1194. remoteExploreCancel() {
  1195. this.remoteExploreOpen = false
  1196. },
  1197. submitRemoteExplore() {
  1198. this.$message.success('已开始遥控探索...')
  1199. this.remoteExploreOpen = false
  1200. },
  1201. submitDownload() {
  1202. console.log('Download types:', this.downLoadTypes)
  1203. this.downloadOpen = false
  1204. this.$message.success('下载已开始')
  1205. },
  1206. submitConstruct() {
  1207. console.log('Construct mode:', this.constructModle)
  1208. this.constructOpen = false
  1209. this.$message.success('构建已开始')
  1210. }
  1211. }
  1212. };
  1213. </script>
  1214. <style lang="scss" scoped>
  1215. .map-list-container {
  1216. min-height: 100vh;
  1217. background: var(--color-bg-secondary);
  1218. padding: var(--spacing-6);
  1219. .toolbar-section {
  1220. background: var(--color-bg-card);
  1221. border-radius: 12px;
  1222. padding: var(--spacing-5);
  1223. margin-bottom: var(--spacing-6);
  1224. box-shadow: 0 6px 18px rgba(2, 6, 23, 0.05);
  1225. display: flex;
  1226. align-items: center;
  1227. justify-content: space-between;
  1228. gap: var(--spacing-4);
  1229. .toolbar-filters {
  1230. display: flex;
  1231. align-items: center;
  1232. gap: var(--spacing-3);
  1233. .search-input {
  1234. width: 260px;
  1235. }
  1236. .status-filter,
  1237. .sort-select {
  1238. width: 140px;
  1239. }
  1240. .el-input,
  1241. .el-select {
  1242. ::v-deep .el-input__inner {
  1243. border-radius: var(--radius-lg);
  1244. transition: all var(--duration-200) var(--ease-out);
  1245. &:focus {
  1246. border-color: var(--color-primary);
  1247. box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.15);
  1248. }
  1249. }
  1250. }
  1251. }
  1252. .toolbar-actions {
  1253. display: flex;
  1254. align-items: center;
  1255. gap: var(--spacing-3);
  1256. .el-button {
  1257. border-radius: var(--radius-base);
  1258. font-weight: var(--font-weight-medium);
  1259. }
  1260. .view-toggle {
  1261. margin-left: var(--spacing-2);
  1262. .el-button {
  1263. padding: 8px 12px;
  1264. border-radius: var(--radius-base);
  1265. &:first-child {
  1266. border-top-right-radius: 0;
  1267. border-bottom-right-radius: 0;
  1268. }
  1269. &:last-child {
  1270. border-top-left-radius: 0;
  1271. border-bottom-left-radius: 0;
  1272. }
  1273. }
  1274. }
  1275. }
  1276. }
  1277. .main-content {
  1278. margin-bottom: var(--spacing-6);
  1279. // 卡片网格
  1280. .card-grid {
  1281. display: grid;
  1282. grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
  1283. gap: var(--spacing-6);
  1284. &.compact {
  1285. gap: var(--spacing-4);
  1286. grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  1287. }
  1288. @media (min-width: 1440px) {
  1289. grid-template-columns: repeat(3, 1fr);
  1290. &.compact {
  1291. grid-template-columns: repeat(4, 1fr);
  1292. }
  1293. }
  1294. }
  1295. // 骨架屏
  1296. .skeleton-card {
  1297. background: var(--color-bg-card);
  1298. border-radius: 12px;
  1299. overflow: hidden;
  1300. box-shadow: 0 6px 18px rgba(2, 6, 23, 0.05);
  1301. .skeleton-thumb {
  1302. height: 160px;
  1303. background: linear-gradient(90deg, var(--color-bg-secondary) 25%, var(--color-bg-tertiary) 50%, var(--color-bg-secondary) 75%);
  1304. background-size: 200% 100%;
  1305. animation: skeleton-loading 1.5s infinite;
  1306. }
  1307. .skeleton-content {
  1308. padding: 16px;
  1309. .skeleton-title,
  1310. .skeleton-remark,
  1311. .skeleton-meta {
  1312. background: linear-gradient(90deg, var(--color-bg-secondary) 25%, var(--color-bg-tertiary) 50%, var(--color-bg-secondary) 75%);
  1313. background-size: 200% 100%;
  1314. animation: skeleton-loading 1.5s infinite;
  1315. border-radius: var(--radius-base);
  1316. }
  1317. .skeleton-title {
  1318. height: 20px;
  1319. margin-bottom: var(--spacing-3);
  1320. width: 80%;
  1321. }
  1322. .skeleton-remark {
  1323. height: 14px;
  1324. margin-bottom: var(--spacing-2);
  1325. width: 100%;
  1326. }
  1327. .skeleton-meta {
  1328. height: 12px;
  1329. width: 60%;
  1330. }
  1331. }
  1332. }
  1333. // 表格容器
  1334. .table-wrapper {
  1335. background: var(--color-bg-card);
  1336. border-radius: 12px;
  1337. overflow: hidden;
  1338. box-shadow: 0 6px 18px rgba(2, 6, 23, 0.05);
  1339. }
  1340. // 表格
  1341. .map-table {
  1342. ::v-deep .el-table__header {
  1343. background: var(--color-bg-secondary);
  1344. }
  1345. ::v-deep .el-table__body {
  1346. .el-table__row {
  1347. &:hover {
  1348. background: var(--color-bg-secondary);
  1349. }
  1350. }
  1351. }
  1352. ::v-deep .el-table__header th,
  1353. ::v-deep .el-table__body td {
  1354. padding: 10px 12px;
  1355. }
  1356. ::v-deep .el-table-column--selection .cell {
  1357. display: flex;
  1358. justify-content: center;
  1359. align-items: center;
  1360. padding: 10px 8px;
  1361. }
  1362. /* 表头"序号"不换行、居中 */
  1363. ::v-deep .col-index-header .cell {
  1364. white-space: nowrap;
  1365. text-align: center;
  1366. padding: 10px 8px;
  1367. }
  1368. /* 序号数字等宽、居中、颜色稍弱一点 */
  1369. ::v-deep .col-index .cell {
  1370. display: flex;
  1371. justify-content: center;
  1372. align-items: center;
  1373. padding: 10px 8px;
  1374. color: var(--color-text-secondary, #6b7280);
  1375. font-variant-numeric: tabular-nums; /* 等宽数字 */
  1376. }
  1377. ::v-deep .cell {
  1378. line-height: 20px;
  1379. white-space: nowrap;
  1380. }
  1381. /* 避免某些主题对固定列的默认阴影/间距影响布局(现在已不固定,这里只是兜底) */
  1382. ::v-deep .el-table__fixed-right {
  1383. right: 0;
  1384. box-shadow: none;
  1385. }
  1386. .table-name {
  1387. display: flex;
  1388. align-items: center;
  1389. gap: var(--spacing-2);
  1390. }
  1391. .table-thumb {
  1392. width: 40px;
  1393. height: 24px;
  1394. border-radius: var(--radius-base);
  1395. overflow: hidden;
  1396. background: var(--color-bg-secondary);
  1397. display: flex;
  1398. align-items: center;
  1399. justify-content: center;
  1400. margin: 0 auto;
  1401. img {
  1402. width: 100%;
  1403. height: 100%;
  1404. object-fit: cover;
  1405. }
  1406. i {
  1407. color: var(--color-text-quaternary);
  1408. font-size: var(--font-size-sm);
  1409. }
  1410. }
  1411. .op-cell {
  1412. display: flex;
  1413. justify-content: flex-end;
  1414. align-items: center;
  1415. gap: 8px;
  1416. }
  1417. .op-link {
  1418. display: inline-flex;
  1419. align-items: center;
  1420. color: var(--color-primary, #1d4ed8);
  1421. text-decoration: none;
  1422. font-size: var(--font-size-sm);
  1423. transition: color 0.2s ease;
  1424. &:hover {
  1425. color: var(--color-primary-light);
  1426. text-decoration: none;
  1427. }
  1428. }
  1429. .el-dropdown-link {
  1430. color: var(--color-primary);
  1431. cursor: pointer;
  1432. display: inline-flex;
  1433. align-items: center;
  1434. font-size: var(--font-size-sm);
  1435. &:hover {
  1436. color: var(--color-primary-light);
  1437. }
  1438. }
  1439. .op-text {
  1440. margin-left: 4px;
  1441. }
  1442. .xt-more .is-danger {
  1443. color: #e11d48;
  1444. }
  1445. .xt-more .is-danger i {
  1446. color: #e11d48;
  1447. }
  1448. }
  1449. // 空态
  1450. .empty-state {
  1451. text-align: center;
  1452. padding: var(--spacing-12) var(--spacing-6);
  1453. background: var(--color-bg-card);
  1454. border-radius: 12px;
  1455. box-shadow: 0 6px 18px rgba(2, 6, 23, 0.05);
  1456. .empty-illustration {
  1457. margin-bottom: var(--spacing-6);
  1458. i {
  1459. font-size: 80px;
  1460. color: var(--color-text-quaternary);
  1461. opacity: 0.6;
  1462. }
  1463. }
  1464. .empty-title {
  1465. color: var(--color-text-primary);
  1466. font-size: var(--font-size-xl);
  1467. font-weight: var(--font-weight-semibold);
  1468. margin: 0 0 var(--spacing-3) 0;
  1469. }
  1470. .empty-description {
  1471. color: var(--color-text-secondary);
  1472. font-size: var(--font-size-base);
  1473. margin: 0 0 var(--spacing-6) 0;
  1474. line-height: var(--line-height-relaxed);
  1475. }
  1476. .empty-actions {
  1477. display: flex;
  1478. justify-content: center;
  1479. gap: var(--spacing-3);
  1480. .el-button {
  1481. border-radius: var(--radius-base);
  1482. font-weight: var(--font-weight-medium);
  1483. }
  1484. }
  1485. }
  1486. }
  1487. .pagination-section {
  1488. background: var(--color-bg-card);
  1489. border-radius: 12px;
  1490. padding: var(--spacing-4) var(--spacing-5);
  1491. box-shadow: 0 6px 18px rgba(2, 6, 23, 0.05);
  1492. display: flex;
  1493. align-items: center;
  1494. justify-content: space-between;
  1495. .density-toggle {
  1496. .el-button-group {
  1497. .el-button {
  1498. padding: 4px 12px;
  1499. font-size: var(--font-size-xs);
  1500. border-radius: var(--radius-base);
  1501. &:first-child {
  1502. border-top-right-radius: 0;
  1503. border-bottom-right-radius: 0;
  1504. }
  1505. &:last-child {
  1506. border-top-left-radius: 0;
  1507. border-bottom-left-radius: 0;
  1508. }
  1509. }
  1510. }
  1511. }
  1512. .el-pagination {
  1513. ::v-deep .el-pager {
  1514. .number {
  1515. border-radius: var(--radius-base);
  1516. margin: 0 2px;
  1517. }
  1518. }
  1519. ::v-deep .btn-prev,
  1520. ::v-deep .btn-next {
  1521. border-radius: var(--radius-base);
  1522. }
  1523. }
  1524. }
  1525. // 探索按钮样式
  1526. .xt-btn {
  1527. border-radius: var(--radius-base);
  1528. font-weight: var(--font-weight-medium);
  1529. transition: all var(--duration-200) var(--ease-out);
  1530. &:disabled {
  1531. opacity: 0.6;
  1532. cursor: not-allowed;
  1533. }
  1534. &:hover:not(:disabled) {
  1535. transform: translateY(-1px);
  1536. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  1537. }
  1538. }
  1539. // 探索弹框样式
  1540. .xt-explore-dialog ::v-deep .el-dialog {
  1541. border-radius: 12px;
  1542. }
  1543. .xt-explore-dialog ::v-deep .el-dialog__body {
  1544. padding: 18px 20px 6px;
  1545. }
  1546. /* 字体与间距 */
  1547. .xt-title {
  1548. display: flex;
  1549. flex-direction: column;
  1550. font-weight: 600;
  1551. font-size: 16px;
  1552. }
  1553. .xt-subtitle {
  1554. font-size: 12px;
  1555. color: var(--text-secondary, #6b7280);
  1556. font-weight: 400;
  1557. margin-top: 4px;
  1558. }
  1559. .xt-form {
  1560. background: white;
  1561. border-radius: 12px;
  1562. padding: 24px;
  1563. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
  1564. .el-form-item {
  1565. margin-bottom: 24px;
  1566. &:last-child {
  1567. margin-bottom: 0;
  1568. }
  1569. .el-form-item__label {
  1570. color: #333;
  1571. font-size: 14px;
  1572. font-weight: 500;
  1573. line-height: 1.4;
  1574. }
  1575. }
  1576. }
  1577. .xt-field {
  1578. display: flex;
  1579. align-items: center;
  1580. gap: 12px;
  1581. }
  1582. .xt-unit {
  1583. color: #666;
  1584. font-size: 14px;
  1585. font-weight: 500;
  1586. white-space: nowrap;
  1587. }
  1588. .xt-hint {
  1589. color: #888;
  1590. font-size: 12px;
  1591. margin-left: auto;
  1592. white-space: nowrap;
  1593. }
  1594. .xt-footer {
  1595. display: flex;
  1596. justify-content: flex-end;
  1597. gap: 12px;
  1598. padding: 16px 0;
  1599. .el-button {
  1600. padding: 10px 24px;
  1601. border-radius: 8px;
  1602. font-weight: 500;
  1603. transition: all 0.2s ease;
  1604. border: none;
  1605. &.el-button--default {
  1606. background: #f1f5f9;
  1607. color: #64748b;
  1608. &:hover {
  1609. background: #e2e8f0;
  1610. color: #475569;
  1611. transform: translateY(-1px);
  1612. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  1613. }
  1614. }
  1615. &.el-button--primary {
  1616. background: linear-gradient(135deg, #10b981, #059669);
  1617. color: white;
  1618. &:hover {
  1619. background: linear-gradient(135deg, #059669, #047857);
  1620. transform: translateY(-1px);
  1621. box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
  1622. }
  1623. }
  1624. }
  1625. }
  1626. /* 输入框外层容器样式 */
  1627. .xt-explore-dialog ::v-deep .el-input-number {
  1628. width: 160px;
  1629. border-radius: 24px;
  1630. overflow: hidden;
  1631. border: 1.5px solid #d1d5db;
  1632. transition: all 0.2s ease;
  1633. background: white;
  1634. &:hover {
  1635. border-color: #9ca3af;
  1636. }
  1637. &:focus-within,
  1638. &.is-focus {
  1639. border-color: #08C28E;
  1640. box-shadow: 0 0 0 2px rgba(8, 194, 142, 0.2);
  1641. }
  1642. .el-input__inner {
  1643. height: 40px;
  1644. line-height: 40px;
  1645. border: none;
  1646. border-radius: 0;
  1647. box-shadow: none;
  1648. padding-right: 40px;
  1649. font-size: 14px;
  1650. background: transparent;
  1651. &:focus {
  1652. outline: none;
  1653. border: none;
  1654. box-shadow: none;
  1655. }
  1656. &::placeholder {
  1657. color: #9ca3af;
  1658. font-size: 13px;
  1659. }
  1660. }
  1661. }
  1662. /* 右侧加减按钮 */
  1663. .xt-explore-dialog ::v-deep .el-input-number.is-controls-right .el-input-number__increase,
  1664. .xt-explore-dialog ::v-deep .el-input-number.is-controls-right .el-input-number__decrease {
  1665. right: 0;
  1666. border: none;
  1667. border-left: 1.5px solid #d1d5db;
  1668. width: 35px;
  1669. height: 20px;
  1670. border-radius: 0;
  1671. background: #f9fafb;
  1672. color: #6b7280;
  1673. transition: all 0.2s ease;
  1674. &:hover {
  1675. background: #f3f4f6;
  1676. color: #374151;
  1677. }
  1678. }
  1679. // 遥控探索确认弹框样式
  1680. .xt-confirm-dialog {
  1681. ::v-deep .el-dialog {
  1682. border-radius: 12px;
  1683. }
  1684. .confirm-title {
  1685. display: flex;
  1686. align-items: center;
  1687. font-size: 16px;
  1688. font-weight: 600;
  1689. color: #333;
  1690. }
  1691. .confirm-content {
  1692. padding: 20px 0;
  1693. p {
  1694. margin: 0;
  1695. font-size: 14px;
  1696. line-height: 1.6;
  1697. color: #666;
  1698. }
  1699. }
  1700. ::v-deep .el-dialog__footer {
  1701. padding: 16px 24px 24px;
  1702. .el-button {
  1703. padding: 8px 20px;
  1704. border-radius: 6px;
  1705. font-weight: 500;
  1706. &.el-button--primary {
  1707. background: linear-gradient(135deg, #10b981, #059669);
  1708. border-color: #10b981;
  1709. &:hover {
  1710. background: linear-gradient(135deg, #059669, #047857);
  1711. border-color: #059669;
  1712. }
  1713. }
  1714. }
  1715. }
  1716. }
  1717. // 保留的对话框样式
  1718. ::v-deep .el-dialog__body {
  1719. padding: var(--spacing-5);
  1720. }
  1721. }
  1722. // 骨架屏动画
  1723. @keyframes skeleton-loading {
  1724. 0% {
  1725. background-position: -200% 0;
  1726. }
  1727. 100% {
  1728. background-position: 200% 0;
  1729. }
  1730. }
  1731. // 暗色主题适配
  1732. html.dark {
  1733. .map-list-container {
  1734. .toolbar-section {
  1735. background: var(--color-bg-tertiary);
  1736. box-shadow: 0 6px 18px rgba(0, 0, 0, 0.15);
  1737. }
  1738. .skeleton-card {
  1739. background: var(--color-bg-tertiary);
  1740. box-shadow: 0 6px 18px rgba(0, 0, 0, 0.15);
  1741. }
  1742. .table-wrapper {
  1743. background: var(--color-bg-tertiary);
  1744. box-shadow: 0 6px 18px rgba(0, 0, 0, 0.15);
  1745. }
  1746. .map-table {
  1747. ::v-deep .el-table__header {
  1748. background: var(--color-bg-quaternary);
  1749. }
  1750. ::v-deep .el-table__body {
  1751. .el-table__row {
  1752. &:hover {
  1753. background: var(--color-bg-quaternary);
  1754. }
  1755. }
  1756. }
  1757. }
  1758. .empty-state,
  1759. .pagination-section {
  1760. background: var(--color-bg-tertiary);
  1761. box-shadow: 0 6px 18px rgba(0, 0, 0, 0.15);
  1762. }
  1763. // 暗色主题下的探索弹框样式
  1764. .xt-explore-dialog {
  1765. ::v-deep .el-dialog {
  1766. background: #1e293b;
  1767. box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
  1768. }
  1769. ::v-deep .el-dialog__header {
  1770. background: linear-gradient(135deg, #334155, #475569);
  1771. border-bottom: 1px solid #475569;
  1772. }
  1773. ::v-deep .el-dialog__body,
  1774. ::v-deep .el-dialog__footer {
  1775. background: #1e293b;
  1776. }
  1777. ::v-deep .el-dialog__footer {
  1778. border-top: 1px solid #475569;
  1779. }
  1780. .xt-title {
  1781. color: #f1f5f9;
  1782. }
  1783. .xt-subtitle {
  1784. color: #94a3b8;
  1785. }
  1786. .xt-form {
  1787. background: #0f172a;
  1788. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
  1789. .el-form-item__label {
  1790. color: #e2e8f0 !important;
  1791. }
  1792. }
  1793. .xt-unit {
  1794. color: #cbd5e1;
  1795. }
  1796. .xt-hint {
  1797. color: #94a3b8;
  1798. }
  1799. ::v-deep .el-input-number {
  1800. background: #0f172a;
  1801. border-color: #475569;
  1802. &:hover {
  1803. border-color: #64748b;
  1804. }
  1805. &:focus-within,
  1806. &.is-focus {
  1807. border-color: #08C28E;
  1808. box-shadow: 0 0 0 2px rgba(8, 194, 142, 0.3);
  1809. }
  1810. .el-input__inner {
  1811. background: transparent;
  1812. color: #e2e8f0;
  1813. &::placeholder {
  1814. color: #64748b;
  1815. }
  1816. }
  1817. }
  1818. ::v-deep .el-input-number__increase,
  1819. ::v-deep .el-input-number__decrease {
  1820. background: #334155;
  1821. border-color: #475569;
  1822. color: #94a3b8;
  1823. &:hover {
  1824. background: #475569;
  1825. color: #e2e8f0;
  1826. }
  1827. }
  1828. .xt-footer {
  1829. .el-button {
  1830. &.el-button--default {
  1831. background: #334155;
  1832. color: #94a3b8;
  1833. &:hover {
  1834. background: #475569;
  1835. color: #e2e8f0;
  1836. }
  1837. }
  1838. &.el-button--primary {
  1839. background: linear-gradient(135deg, #10b981, #059669);
  1840. color: white;
  1841. &:hover {
  1842. background: linear-gradient(135deg, #059669, #047857);
  1843. }
  1844. }
  1845. }
  1846. }
  1847. }
  1848. // 暗色主题下的确认弹框样式
  1849. .xt-confirm-dialog {
  1850. ::v-deep .el-dialog {
  1851. background: #1e293b;
  1852. }
  1853. .confirm-title {
  1854. color: #f1f5f9;
  1855. }
  1856. .confirm-content p {
  1857. color: #cbd5e1;
  1858. }
  1859. ::v-deep .el-dialog__footer {
  1860. background: #1e293b;
  1861. .el-button {
  1862. &:not(.el-button--primary) {
  1863. background: #334155;
  1864. color: #94a3b8;
  1865. border-color: #475569;
  1866. &:hover {
  1867. background: #475569;
  1868. color: #e2e8f0;
  1869. }
  1870. }
  1871. }
  1872. }
  1873. }
  1874. }
  1875. }
  1876. // 响应式设计
  1877. @media (max-width: 1439px) {
  1878. .map-list-container {
  1879. .main-content {
  1880. .card-grid {
  1881. grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  1882. &.compact {
  1883. grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  1884. }
  1885. }
  1886. }
  1887. }
  1888. }
  1889. @media (max-width: 768px) {
  1890. .map-list-container {
  1891. padding: var(--spacing-4);
  1892. .toolbar-section {
  1893. flex-direction: column;
  1894. align-items: stretch;
  1895. gap: var(--spacing-4);
  1896. .toolbar-filters {
  1897. flex-direction: column;
  1898. align-items: stretch;
  1899. .search-input,
  1900. .status-filter,
  1901. .sort-select {
  1902. width: 100%;
  1903. }
  1904. }
  1905. .toolbar-actions {
  1906. justify-content: space-between;
  1907. flex-wrap: wrap;
  1908. .view-toggle {
  1909. margin-left: 0;
  1910. order: -1;
  1911. }
  1912. }
  1913. }
  1914. .main-content {
  1915. .card-grid {
  1916. grid-template-columns: 1fr;
  1917. gap: var(--spacing-4);
  1918. &.compact {
  1919. gap: var(--spacing-3);
  1920. }
  1921. }
  1922. }
  1923. .pagination-section {
  1924. flex-direction: column;
  1925. gap: var(--spacing-3);
  1926. .density-toggle {
  1927. order: 1;
  1928. }
  1929. }
  1930. }
  1931. }
  1932. </style>