| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216 |
- <template>
- <div class="map-list-container">
- <!-- 顶部工具条 -->
- <div class="toolbar-section">
- <!-- 搜索和筛选区 -->
- <div class="toolbar-filters">
- <el-input
- v-model="searchKeyword"
- placeholder="搜索地图名称..."
- prefix-icon="el-icon-search"
- clearable
- class="search-input"
- @input="handleSearch"
- />
-
- <el-select
- v-model="statusFilter"
- placeholder="状态筛选"
- clearable
- class="status-filter"
- @change="handleFilterChange"
- >
- <el-option label="全部状态" value="all" />
- <el-option label="正常" value="ok" />
- <el-option label="不可用" value="down" />
- <el-option label="扫描中" value="scanning" />
- </el-select>
-
- <el-select
- v-model="sortField"
- placeholder="排序方式"
- class="sort-select"
- @change="handleSortChange"
- >
- <el-option label="最近修改" value="updated" />
- <el-option label="名称" value="name" />
- <el-option label="状态" value="status" />
- </el-select>
- </div>
- <!-- 操作按钮区 -->
- <div class="toolbar-actions">
- <el-button
- type="default"
- icon="el-icon-upload2"
- size="small"
- @click="handleImport"
- >
- 导入地图
- </el-button>
-
- <el-button
- type="primary"
- icon="el-icon-plus"
- size="small"
- @click="handleCreate"
- >
- 新建地图
- </el-button>
-
- <el-button
- class="xt-btn"
- :disabled="selectedMaps.length !== 1"
- icon="el-icon-discover"
- size="small"
- @click="openSelfExplore"
- >
- 自主探索
- </el-button>
-
- <el-button
- class="xt-btn"
- :disabled="selectedMaps.length !== 1"
- icon="el-icon-connection"
- size="small"
- @click="openRemoteExplore"
- >
- 遥控探索
- </el-button>
- <!-- 视图切换 -->
- <el-button-group class="view-toggle">
- <el-button
- :type="viewMode === 'card' ? 'primary' : 'default'"
- icon="el-icon-s-grid"
- size="small"
- @click="setViewMode('card')"
- title="卡片视图"
- />
- <el-button
- :type="viewMode === 'table' ? 'primary' : 'default'"
- icon="el-icon-menu"
- size="small"
- @click="setViewMode('table')"
- title="表格视图"
- />
- </el-button-group>
- </div>
- </div>
- <!-- 主要内容区域 -->
- <div class="main-content">
- <!-- 卡片视图 -->
- <template v-if="viewMode === 'card'">
- <!-- 加载骨架屏 -->
- <div v-if="loading" class="card-grid" :class="{ compact: isCompactMode }">
- <div v-for="n in 12" :key="n" class="skeleton-card">
- <div class="skeleton-thumb"></div>
- <div class="skeleton-content">
- <div class="skeleton-title"></div>
- <div class="skeleton-remark"></div>
- <div class="skeleton-meta"></div>
- </div>
- </div>
- </div>
- <!-- 空态 -->
- <div v-else-if="mapList.length === 0" class="empty-state">
- <div class="empty-illustration">
- <i class="el-icon-map-location"></i>
- </div>
- <h3 class="empty-title">暂无地图</h3>
- <p class="empty-description">
- {{ hasSearchOrFilter ? '没有找到符合条件的地图' : '您还没有创建任何地图' }}
- </p>
- <div v-if="!hasSearchOrFilter" class="empty-actions">
- <el-button type="primary" icon="el-icon-plus" @click="handleCreate">
- 新建地图
- </el-button>
- <el-button type="default" icon="el-icon-upload2" @click="handleImport">
- 导入地图
- </el-button>
- </div>
- </div>
- <!-- 地图卡片列表 -->
- <div v-else class="card-grid" :class="{ compact: isCompactMode }">
- <XtMapCard
- v-for="item in displayedList"
- :key="item.id || item.mapId || item.map_id"
- :item="item"
- :selectable="selectedMaps.length > 0"
- :selected="selectedMaps.includes(item.id)"
- :map-file-base="MAP_FILE_BASE"
- @clickNav="handleNavigation"
- @clickEdit="handleEdit"
- @clickMore="handleCardAction"
- @selectChange="handleSelectChange"
- />
- </div>
- </template>
- <!-- 表格视图 -->
- <template v-else>
- <div ref="tableWrap" class="table-wrapper">
- <el-table
- ref="mapTable"
- v-loading="loading"
- :data="displayedList"
- @selection-change="handleTableSelectionChange"
- class="map-table"
- :fit="true"
- style="width: 100%"
- :border="false"
- :height="useFixedHeight ? tableHeight : null"
- :header-cell-style="() => ({ padding: '10px 12px', whiteSpace: 'nowrap' })"
- :cell-style="() => ({ padding: '10px 12px' })"
- empty-text="暂无地图数据"
- >
- <el-table-column type="selection" width="56" align="center" :reserve-selection="true"></el-table-column>
-
- <el-table-column
- label="序号"
- type="index"
- :index="indexMethod"
- width="64"
- align="center"
- header-align="center"
- class-name="col-index"
- label-class-name="col-index-header">
- </el-table-column>
-
- <el-table-column prop="mapName" label="地图名称" min-width="260" show-overflow-tooltip>
- <template slot-scope="scope">
- <div class="table-name">
- <!-- {{ scope.row.name }} -->
- {{ scope.row.map }}
- </div>
- </template>
- </el-table-column>
-
- <el-table-column label="缩略图" width="120" align="center">
- <template slot-scope="scope">
- <div class="table-thumb">
- <img v-if="scope.row.thumbUrl" :src="scope.row.thumbUrl" alt="缩略图" />
- <i v-else class="el-icon-map-location"></i>
- </div>
- </template>
- </el-table-column>
-
- <el-table-column label="状态" width="120" align="center">
- <template slot-scope="scope">
- <el-tag
- :type="getStatusConfig(scope.row.state).type"
- size="mini"
- effect="plain"
- >
- <i :class="getStatusConfig(scope.row.state).icon"></i>
- {{ getStatusConfig(scope.row.state).text }}
- </el-tag>
- </template>
- </el-table-column>
-
- <el-table-column prop="remark" label="备注" min-width="360" show-overflow-tooltip />
-
- <el-table-column prop="updatedAt" label="更新时间" width="180" align="center">
- <template slot-scope="{ row }">
- {{ formatDateTimeCompat(pickUpdatedAt(row)) }}
- </template>
- </el-table-column>
-
- <el-table-column label="操作" width="220" header-align="center" align="right">
- <template slot-scope="{ row }">
- <div class="op-cell">
- <router-link :to="buildNavTo(this, row)" class="op-link">
- <i class="el-icon-position"></i><span class="op-text">导航</span>
- </router-link>
- <router-link :to="buildEditTo(this, row)" class="op-link">
- <i class="el-icon-edit"></i><span class="op-text">编辑</span>
- </router-link>
- <el-dropdown @command="cmd=>onMore(row,cmd)">
- <span class="el-dropdown-link">
- <i class="el-icon-more"></i><span class="op-text">更多</span>
- </span>
- <el-dropdown-menu slot="dropdown" class="xt-more">
- <el-dropdown-item command="rename"><i class="el-icon-edit"></i> 重命名</el-dropdown-item>
- <el-dropdown-item command="download"><i class="el-icon-download"></i> 下载地图</el-dropdown-item>
- <el-dropdown-item command="build"><i class="el-icon-cpu"></i> 构建地图</el-dropdown-item>
- <el-dropdown-item divided command="delete" class="is-danger"><i class="el-icon-delete"></i> 删除地图</el-dropdown-item>
- <el-dropdown-item divided command="calibrate"><i class="el-icon-position"></i> 坐标系标定</el-dropdown-item>
- </el-dropdown-menu>
- </el-dropdown>
- </div>
- </template>
- </el-table-column>
- </el-table>
- </div>
- </template>
- </div>
- <!-- 分页 -->
- <div v-if="!loading && mapList.length > 0" class="pagination-section">
- <!-- 密度切换 -->
- <div v-if="total > 60 && viewMode === 'card'" class="density-toggle">
- <el-button-group size="mini">
- <el-button
- :type="isCompactMode ? 'default' : 'primary'"
- @click="isCompactMode = false"
- >
- 常规
- </el-button>
- <el-button
- :type="isCompactMode ? 'primary' : 'default'"
- @click="isCompactMode = true"
- >
- 紧凑
- </el-button>
- </el-button-group>
- </div>
- <el-pagination
- :current-page="currentPage"
- :page-sizes="[12, 24, 48, 96]"
- :page-size="pageSize"
- :total="totalCount"
- layout="total, sizes, prev, pager, next, jumper"
- @size-change="handleSizeChange"
- @current-change="handlePageChange"
- />
- </div>
- <!-- 自主探索配置对话框 -->
- <el-dialog :visible.sync="autoExploreOpen" width="560px" append-to-body :close-on-click-modal="false" class="xt-explore-dialog">
- <span slot="title" class="xt-title">
- 自主探索
- <small class="xt-subtitle">为所选地图配置探索规则 · {{ selectedMapName }}</small>
- </span>
- <el-form ref="form" :model="exploreParams" :rules="rules" label-width="120px" class="xt-form">
- <el-form-item label="最大探索时间" prop="maxTime">
- <div class="xt-field">
- <el-input-number
- v-model="exploreParams.maxTime"
- :min="1"
- :step="1"
- controls-position="right"
- placeholder="请输入时间"
- />
- <span class="xt-unit">分钟</span>
- <span class="xt-hint">建议 1-10 分钟</span>
- </div>
- </el-form-item>
- <el-form-item label="最远探索距离" prop="maxDistance">
- <div class="xt-field">
- <el-input-number
- v-model="exploreParams.maxDistance"
- :min="1"
- :step="1"
- controls-position="right"
- placeholder="请输入距离"
- />
- <span class="xt-unit">米</span>
- <span class="xt-hint">建议不超过 500 米</span>
- </div>
- </el-form-item>
- <el-form-item label="最大探索范围" prop="maxRange">
- <div class="xt-field">
- <el-input-number
- v-model="exploreParams.maxRange"
- :min="1"
- :step="1"
- controls-position="right"
- placeholder="请输入范围"
- />
- <span class="xt-unit">平方米</span>
- <span class="xt-hint">建议 100-5000 平方米</span>
- </div>
- </el-form-item>
- </el-form>
- <span slot="footer" class="dialog-footer xt-footer">
- <el-button plain @click="autoExploreCancel">取 消</el-button>
- <el-button type="primary" @click="submitAutoExplore">开 始</el-button>
- </span>
- </el-dialog>
- <!-- 遥控探索确认对话框 -->
- <el-dialog :visible.sync="remoteExploreOpen" width="480px" append-to-body :close-on-click-modal="false" class="xt-confirm-dialog">
- <span slot="title" class="confirm-title">
- <i class="el-icon-warning" style="color: #E6A23C; margin-right: 8px;"></i>
- 提示
- </span>
- <div class="confirm-content">
- <p>将对编号为「{{ selectedMapCode }}-{{ selectedMapName }}」的地图执行遥控探索,是否继续?</p>
- </div>
- <span slot="footer" class="dialog-footer">
- <el-button @click="remoteExploreCancel">取 消</el-button>
- <el-button type="primary" @click="submitRemoteExplore">确 定</el-button>
- </span>
- </el-dialog>
- <!-- 下载地图dia -->
- <el-dialog :title="title" :visible.sync="downloadOpen" width="520px" append-to-body :close-on-click-modal="false">
- <div style="margin-bottom: 20px;">
- <span style="font-weight: bold;">请选择下载的地图组件:</span>
- </div>
- <el-checkbox-group v-model="downLoadTypes" style="margin-bottom: 10px;">
- <el-checkbox label="tree" :checked="true">八叉树</el-checkbox>
- <el-checkbox label="vector" :checked="true">矢量</el-checkbox>
- <el-checkbox label="las" :checked="true">las数量</el-checkbox>
- <el-checkbox label="tile" :checked="true">瓦片地图</el-checkbox>
- <el-checkbox label="task" :checked="true">任务数据</el-checkbox>
- </el-checkbox-group>
- <div slot="footer" class="dialog-footer">
- <el-button type="primary" @click="submitDownload">开 始</el-button>
- <el-button @click="downloadOpen = false">取 消</el-button>
- </div>
- </el-dialog>
- <!-- 构建地图dia -->
- <el-dialog :title="title" :visible.sync="constructOpen" width="520px" append-to-body :close-on-click-modal="false">
- <span style="margin-right: 10px;">构建模式:</span>
- <el-radio-group v-model="constructModle" size="mini">
- <el-radio-button label="auto">自动构建</el-radio-button>
- <el-radio-button label="hand">手动构建</el-radio-button>
- </el-radio-group>
- <div v-if="constructModle == 'hand'">
- <p style="margin-top: 20px;">请选择地图及数据:</p>
- <el-checkbox-group v-model="constructTypes" style="margin-bottom: 10px;">
- <el-checkbox label="tree" :checked="true">八叉树</el-checkbox>
- <el-checkbox label="las" :checked="true">las数量</el-checkbox>
- <el-checkbox label="tile" :checked="true">瓦片地图</el-checkbox>
- <el-checkbox label="potree" :checked="true">potree</el-checkbox>
- </el-checkbox-group>
- </div>
- <div slot="footer" class="dialog-footer">
- <el-button type="primary" @click="submitConstruct">开 始</el-button>
- <el-button @click="constructOpen = false">取 消</el-button>
- </div>
- </el-dialog>
- <div>
- <button @click="publishMsg">发布消息</button>
- <button @click="addTopic">动态订阅 topic3</button>
- <button @click="removeTopic">动态取消 topic2</button>
- </div>
- <MqttComp ref="mqtt" :topics="topics" @message-received="onMessage" />
- </div>
- </template>
- <script>
- import MqttComp from "@/components/Mqtt/mqttComp.vue";
- // 导入地图卡片组件
- import XtMapCard from '@/components/XtMapCard'
- // 导入日期格式化工具
- import { formatDateTimeCompat, pickUpdatedAt } from '@/utils/datefmt'
- // 导入路由辅助函数
- import { buildNavTo, buildEditTo, buildCalibrateTo, buildConstructTo, pickId } from '@/utils/route-helpers'
- // 安全获取数组函数
- function pickArray(...arrs){
- for(const a of arrs){
- if(Array.isArray(a) && a.length>=0) return a
- }
- return []
- }
- // 尝试导入真实 API,失败则使用 Mock
- let mapApi
- try {
- // 尝试导入真实 API(如果存在)
- mapApi = require('@/api/map/index.js')
- } catch (error) {
- // Fallback 到 Mock API
- mapApi = require('@/api/mock/maps')
- }
- // 地图文件服务基础URL(可根据实际情况调整)
- const MAP_FILE_BASE = 'http://101.35.49.102:9000' // 示例地址,实际使用时请替换
- export default {
- name: "MapList",
-
- components: {
- XtMapCard,
- MqttComp,
- },
-
- data() {
- return {
- // topics: ["/robot4inspection/a477be75f66fe3cb/task/target/action/goto/reply","/robot4inspection/a477be75f66fe3cb/task/target/event/arrive"], // 初始订阅的 topic
- topics:[],
- // 常量
- MAP_FILE_BASE,
-
- // 视图模式
- viewMode: 'card', // 'card' | 'table'
-
- // 加载状态
- loading: false,
-
- // 数据
- mapList: [],
- total: 0,
- currentPage: 1,
- pageSize: 12,
- // Mock 数据
- /* mockMapList: [
- {
- id: 1,
- code: 'SH001',
- name: '上海办公楼一层',
- mapName: '上海办公楼一层',
- showImg: 'https://placehold.co/320x180/4f46e5/white?text=Office+Floor+1',
- thumbUrl: 'https://placehold.co/320x180/4f46e5/white?text=Office+Floor+1',
- status: 'ok',
- use: true,
- top: true,
- remark: '主要办公区域地图,包含接待区、办公区、会议室',
- updatedAt: '2024-01-15T14:22:00Z'
- },
- {
- id: 2,
- code: 'FC001',
- name: '工厂车间A区',
- mapName: '工厂车间A区',
- showImg: 'https://placehold.co/320x180/dc2626/white?text=Factory+A',
- thumbUrl: 'https://placehold.co/320x180/dc2626/white?text=Factory+A',
- status: 'down',
- use: false,
- top: false,
- remark: '生产车间A区域,设备较多,需要定期更新',
- updatedAt: '2024-01-12T09:12:00Z'
- },
- {
- id: 3,
- code: 'WH001',
- name: '仓库主区域',
- mapName: '仓库主区域',
- showImg: 'https://placehold.co/320x180/f59e0b/white?text=Warehouse',
- thumbUrl: 'https://placehold.co/320x180/f59e0b/white?text=Warehouse',
- status: 'scanning',
- use: false,
- top: true,
- remark: '仓库主要存储区域,正在进行地图更新扫描',
- updatedAt: '2024-01-14T16:45:00Z'
- },
- {
- id: 4,
- code: 'RD002',
- name: '研发中心二楼',
- mapName: '研发中心二楼',
- showImg: 'https://placehold.co/320x180/059669/white?text=R%26D+Floor+2',
- thumbUrl: 'https://placehold.co/320x180/059669/white?text=R%26D+Floor+2',
- status: 'ok',
- use: false,
- top: false,
- remark: '研发中心实验室区域,包含多个实验室和会议室',
- updatedAt: '2024-01-11T11:30:00Z'
- },
- {
- id: 5,
- code: 'PK001',
- name: '停车场地下一层',
- mapName: '停车场地下一层',
- showImg: 'https://placehold.co/320x180/7c3aed/white?text=Parking+B1',
- thumbUrl: 'https://placehold.co/320x180/7c3aed/white?text=Parking+B1',
- status: 'ok',
- use: false,
- top: false,
- remark: '地下停车场区域图,包含车位信息和通道标识',
- updatedAt: '2024-01-10T08:15:00Z'
- },
- {
- id: 6,
- code: 'CF001',
- name: '食堂餐厅区域',
- mapName: '食堂餐厅区域',
- showImg: 'https://placehold.co/320x180/ea580c/white?text=Cafeteria',
- thumbUrl: 'https://placehold.co/320x180/ea580c/white?text=Cafeteria',
- status: 'scanning',
- use: false,
- top: false,
- remark: '员工食堂及餐厅区域,包含用餐区和厨房区域',
- updatedAt: '2024-01-09T12:40:00Z'
- },
- {
- id: 7,
- code: 'OD001',
- name: '户外园区',
- mapName: '户外园区',
- showImg: 'https://placehold.co/320x180/16a34a/white?text=Outdoor+Park',
- thumbUrl: 'https://placehold.co/320x180/16a34a/white?text=Outdoor+Park',
- status: 'ok',
- use: false,
- top: false,
- remark: '公司户外园区,包含步行道、休息区和绿化带',
- updatedAt: '2024-01-08T15:20:00Z'
- },
- {
- id: 8,
- code: 'MC001',
- name: '会议中心',
- mapName: '会议中心',
- showImg: 'https://placehold.co/320x180/0891b2/white?text=Conference',
- thumbUrl: 'https://placehold.co/320x180/0891b2/white?text=Conference',
- status: 'down',
- use: false,
- top: false,
- remark: '多功能会议中心,包含大型会议厅和小型讨论室',
- updatedAt: '2024-01-07T10:05:00Z'
- }
- ], */
-
- // 搜索和筛选
- searchKeyword: '',
- statusFilter: 'all',
- sortField: 'updated',
- sortOrder: 'desc',
- searchDebounceTimer: null,
-
- // 选择状态
- selectedMaps: [],
-
- // UI状态
- isCompactMode: false,
- useFixedHeight: false,
- tableHeight: 520, // 表格高度
-
-
- // 弹窗状态(保留原有对话框)
- title: "",
- autoExploreOpen: false, // 自主探索弹框
- remoteExploreOpen: false, // 遥控探索确认弹框
- downloadOpen: false,
- constructOpen: false,
- constructModle: 'hand',
- exploreParams: {
- maxTime: null,
- maxDistance: null,
- maxRange: null
- },
- downLoadTypes: [],
- constructTypes: [],
- rules: {
- maxTime: [
- { required: true, message: "最长时间不能为空", trigger: "blur" }
- ],
- maxDistance: [
- { required: true, message: "最远距离不能为空", trigger: "blur" }
- ],
- maxRange: [
- { required: true, message: "最大范围不能为空", trigger: "blur" }
- ],
- }
- };
- },
- computed: {
- // 原始数据:兼容不同变量名
- rawList(){
- // 按出现频率降序尝试
- return pickArray(this.mapList)
- },
-
- // 供表格/卡片使用的数据(先不做筛选,只做分页裁切;如果你已有筛选,请把你现有的筛选结果放到最前面)
- displayedList(){
- // 如果已有筛选后的数据,优先使用
- /* if (this.searchKeyword.trim() || this.statusFilter !== 'all') {
- // 使用现有的筛选逻辑
- let filteredData = [...this.mockMapList]
-
- // 搜索过滤
- if (this.searchKeyword.trim()) {
- const keyword = this.searchKeyword.toLowerCase()
- filteredData = filteredData.filter(item =>
- item.name.toLowerCase().includes(keyword) ||
- item.remark.toLowerCase().includes(keyword)
- )
- }
-
- // 状态筛选
- if (this.statusFilter !== 'all') {
- filteredData = filteredData.filter(item => item.status === this.statusFilter)
- }
-
- // 排序
- filteredData.sort((a, b) => {
- let aVal, bVal
-
- switch (this.sortField) {
- case 'name':
- aVal = a.name.toLowerCase()
- bVal = b.name.toLowerCase()
- break
- case 'status':
- // 状态优先级:ok > scanning > down
- const statusOrder = { ok: 2, scanning: 1, down: 0 }
- aVal = statusOrder[a.status] || 0
- bVal = statusOrder[b.status] || 0
- break
- case 'updated':
- default:
- aVal = new Date(a.updatedAt).getTime()
- bVal = new Date(b.updatedAt).getTime()
- break
- }
- if (this.sortOrder === 'asc') {
- return aVal > bVal ? 1 : aVal < bVal ? -1 : 0
- } else {
- return aVal < bVal ? 1 : aVal > bVal ? -1 : 0
- }
- })
-
- // 分页
- const start = (this.currentPage - 1) * this.pageSize
- const end = start + this.pageSize
- return filteredData.slice(start, end)
- }
- */
- // 否则使用通用分页逻辑
- const src = this.rawList || []
- console.log('src', src);
-
- const page = this.pagination?.page || this.pageNum || this.currentPage || 1
- const size = this.pagination?.pageSize || this.pageSize || 10
- const start = (page-1)*size
- return src.slice(start, start+size)
- },
-
- totalCount(){
- /* if (this.searchKeyword.trim() || this.statusFilter !== 'all') {
- // 使用现有的筛选逻辑计算总数
- let filteredData = [...this.mockMapList]
-
- // 搜索过滤
- if (this.searchKeyword.trim()) {
- const keyword = this.searchKeyword.toLowerCase()
- filteredData = filteredData.filter(item =>
- item.name.toLowerCase().includes(keyword) ||
- item.remark.toLowerCase().includes(keyword)
- )
- }
-
- // 状态筛选
- if (this.statusFilter !== 'all') {
- filteredData = filteredData.filter(item => item.status === this.statusFilter)
- }
-
- return filteredData.length
- } */
-
- const src = this.rawList || []
- return src.length
- },
- // 是否有搜索或筛选条件
- hasSearchOrFilter() {
- return this.searchKeyword.trim() || this.statusFilter !== 'all'
- },
- // 获取选中的地图名称
- selectedMapName() {
- if (this.selectedMaps.length === 1) {
- const selectedMap = this.displayedList.find(map => map.id === this.selectedMaps[0]) ||
- this.rawList.find(map => map.id === this.selectedMaps[0])
- return selectedMap ? selectedMap.name : ''
- }
- return ''
- },
- // 获取选中的地图编号
- selectedMapCode() {
- if (this.selectedMaps.length === 1) {
- const selectedMap = this.displayedList.find(map => map.id === this.selectedMaps[0]) ||
- this.rawList.find(map => map.id === this.selectedMaps[0])
- return selectedMap ? (selectedMap.code || selectedMap.id) : ''
- }
- return ''
- }
- },
- created() {
- this.loadViewMode()
- this.calculateTableHeight()
- },
-
- watch: {
- displayedList() {
- this.$nextTick(() => this.$refs.mapTable && this.$refs.mapTable.doLayout())
- }
- },
- mounted() {
- this.getList()
- this.calcTableHeight()
- window.addEventListener('resize', this.onResize)
- // 添加键盘监听
- this.addKeyboardListeners()
-
- // 表格渲染后重排一次,防止列宽缓存
- this.$nextTick(() => {
- this.$refs.mapTable && this.$refs.mapTable.doLayout()
- })
- },
- beforeDestroy() {
- window.removeEventListener('resize', this.onResize)
- // 移除键盘监听
- this.removeKeyboardListeners()
- },
- methods: {
- onMessage({ topic, message }) {
- console.log("收到消息:", topic, message);
- },
- publishMsg() {
- this.$refs.mqtt.publish("/robot4inspection/508b02dc5bcdca22/task/target/action/goto", {
- "timestamp" : 12345678,
- "args": [{"roadmap": "demo","nid": [10]}]
- });
- },
- addTopic() {
- if (!this.topics.includes("topic3")) this.topics.push("topic3");
- },
- removeTopic() {
- this.topics = this.topics.filter((t) => t !== "/robot4inspection/508b02dc5bcdca22/localization/pose");
- },
- // 获取地图列表
- async getList() {
- this.loading = true
-
- // 模拟加载延迟
- // await new Promise(resolve => setTimeout(resolve, 300))
-
- try {
- // TODO: 后续接入真实接口时,取消注释以下代码
-
- // const params = {
- // page: this.currentPage,
- // pageSize: this.pageSize,
- // keyword: this.searchKeyword,
- // status: this.statusFilter,
- // sort: this.sortField,
- // order: this.sortOrder
- // }
- const response = await mapApi.getMapList()
- console.log('Fetched map list:', response);
-
- if (response) {
- // this.mapList = this.processMapList(response)
- const { maps = [], states = [], total = 0 } = response || {}
-
- // 合并成对象数组 [{ map: 'demo', state: 'available' }]
- this.mapList = maps.map((map, index) => ({
- map,
- state: states[index] || null
- }))
- const test = await mapApi.getMapThumbnail(this.mapList[0].map)
- console.log('Processed map list:', test);
- const url = URL.createObjectURL(test)
-
- this.$set(this.mapList[0], 'thumbUrl', url)
-
-
- }
-
-
- // 使用 Mock 数据 - 确保 mapList 被设置
- // this.mapList = [...this.mockMapList]
-
- let filteredData = [...this.mapList]
-
- // 搜索过滤
- if (this.searchKeyword.trim()) {
- const keyword = this.searchKeyword.toLowerCase()
- filteredData = filteredData.filter(item =>
- item.name.toLowerCase().includes(keyword) ||
- item.remark.toLowerCase().includes(keyword)
- )
- }
-
- // 状态筛选
- if (this.statusFilter !== 'all') {
- filteredData = filteredData.filter(item => item.status === this.statusFilter)
- }
-
- // 排序
- filteredData.sort((a, b) => {
- let aVal, bVal
-
- switch (this.sortField) {
- case 'name':
- aVal = a.name.toLowerCase()
- bVal = b.name.toLowerCase()
- break
- case 'status':
- // 状态优先级:ok > scanning > down
- const statusOrder = { ok: 2, scanning: 1, down: 0 }
- aVal = statusOrder[a.status] || 0
- bVal = statusOrder[b.status] || 0
- break
- case 'updated':
- default:
- aVal = new Date(a.updatedAt).getTime()
- bVal = new Date(b.updatedAt).getTime()
- break
- }
- if (this.sortOrder === 'asc') {
- return aVal > bVal ? 1 : aVal < bVal ? -1 : 0
- } else {
- return aVal < bVal ? 1 : aVal > bVal ? -1 : 0
- }
- })
-
- // 分页
- this.total = filteredData.length
- const start = (this.currentPage - 1) * this.pageSize
- const end = start + this.pageSize
- const pagedData = filteredData.slice(start, end)
-
- // 处理后的数据仍然赋值给 mapList,但原始数据已保存
- this.mapList = this.processMapList(pagedData)
-
- } catch (error) {
- this.$message.error('获取地图列表失败: ' + error.message)
- this.mapList = []
- this.total = 0
- } finally {
- this.loading = false
- this.relayout()
- }
- },
- // 处理地图列表数据
- processMapList(list) {
- return list.map(item => ({
- ...item
- }))
- },
- // 表格相关方法
- calcTableHeight() {
- this.$nextTick(() => {
- if (!this.$refs.tableWrap) return
- const top = this.$refs.tableWrap.getBoundingClientRect().top
- this.tableHeight = window.innerHeight - top - 120 // 预留分页/外边距
- // 当行数 * 行高 + 表头高度 > 可用高度时,才启用固定高度(出现纵向滚动)
- const rowH = 48, headerH = 48
- const need = (this.mapList.length * rowH + headerH) > this.tableHeight
- this.useFixedHeight = need
- this.$refs.mapTable && this.$refs.mapTable.doLayout()
- })
- },
- onResize() {
- this.calcTableHeight()
- },
- relayout() {
- this.$nextTick(() => this.$refs.mapTable && this.$refs.mapTable.doLayout())
- },
- // 搜索处理(防抖)
- handleSearch() {
- clearTimeout(this.searchDebounceTimer)
- this.searchDebounceTimer = setTimeout(() => {
- this.currentPage = 1
- this.getList()
- }, 300)
- },
- // 筛选变化处理
- handleFilterChange() {
- this.currentPage = 1
- this.getList()
- },
- // 排序变化处理
- handleSortChange() {
- this.currentPage = 1
- this.getList()
- },
- // 页码变化
- handlePageChange(page) {
- this.currentPage = page
- this.getList()
- // 滚动到顶部
- this.$nextTick(() => {
- window.scrollTo({ top: 0, behavior: 'smooth' })
- })
- },
- // 页大小变化
- handleSizeChange(size) {
- this.pageSize = size
- this.currentPage = 1
- this.getList()
- },
- // 视图模式切换
- setViewMode(mode) {
- this.viewMode = mode
- this.saveViewMode()
- if (mode === 'table') {
- this.$nextTick(() => {
- this.calcTableHeight()
- this.relayout()
- })
- }
- },
- // 卡片选择变化
- handleSelectChange(id, checked) {
- if (checked) {
- if (!this.selectedMaps.includes(id)) {
- this.selectedMaps.push(id)
- }
- } else {
- const index = this.selectedMaps.indexOf(id)
- if (index > -1) {
- this.selectedMaps.splice(index, 1)
- }
- }
- },
- // 表格选择变化
- handleTableSelectionChange(selection) {
- this.selectedMaps = selection.map(item => item.id)
- },
- // 清除选择
- clearSelection() {
- this.selectedMaps = []
- },
- // 导航
- handleNavigation(id) {
- this.$router.push(`/map/navigation?id=${id}`)
- },
- // 编辑
- handleEdit(id) {
- this.$router.push(`/map/edit?id=${id}`)
- },
- // 表格操作方法
- goNav(row) {
- this.handleNavigation(row.id)
- },
- goEdit(row) {
- this.handleEdit(row.id)
- },
- async onMore(row, cmd) {
- const id = pickId(row);
- switch(cmd) {
- case 'rename':
- // 优先:已有重命名弹框/方法
- if (typeof this.openRenameDialog === 'function') return this.openRenameDialog(row);
- if (this.renameDialog) { this.renameDialog.visible = true; this.renameDialog.row = row; return; }
- // 兜底:Element prompt
- this.$prompt('请输入新的地图名称', '重命名', {
- inputValue: row.mapName || row.name || '',
- inputPattern: /\S+/,
- inputErrorMessage: '名称不能为空'
- }).then(({ value }) => {
- // 若有旧方法:this.renameMap(id, value)
- if (typeof this.renameMap === 'function') return this.renameMap(id, value);
- row.mapName = value;
- row.name = value;
- this.$message.success('已重命名'); // mock 刷新
- }).catch(()=>{});
- break;
- case 'download':
- // 打开下载对话框
- this.title = `下载地图 - ${row.name || row.mapName || ''}`;
- this.currentRow = row;
- this.downloadOpen = true;
- break;
- case 'build':
- // 优先:旧路由/方法
- if (typeof this.openBuildDialog === 'function') return this.openBuildDialog(row);
- if (typeof this.constructOpen === 'boolean') {
- this.constructOpen = true;
- this.title = `构建地图 - ${row.name || row.mapName || ''}`;
- this.currentRow = row;
- return;
- }
- if (typeof this.toBuildPage === 'function') return this.toBuildPage(row);
- return this.$router.push(buildConstructTo(row));
- case 'delete':
- // 优先:旧删除方法
- if (typeof this.handleDelete === 'function') {
- return this.$confirm('确认删除该地图?此操作不可恢复', '删除地图', {type: 'warning'})
- .then(() => this.handleDelete([id]))
- .catch(() => {});
- }
- if (typeof this.deleteMap === 'function') {
- return this.$confirm('确认删除该地图?此操作不可恢复', '删除地图', {type: 'warning'})
- .then(() => this.deleteMap(id))
- .catch(() => {});
- }
- this.$confirm('确认删除该地图?此操作不可恢复', '删除地图', {type: 'warning'})
- .then(() => {
- this.$emit && this.$emit('remove', row);
- this.$message.success('已删除(mock)');
- })
- .catch(() => {});
- break;
- case 'calibrate':
- // 优先:旧路由/方法
- if (typeof this.openCalibration === 'function') return this.openCalibration(row);
- return this.$router.push(buildCalibrateTo(row));
-
- default:
- // 兼容原有的操作
- return this.handleCardAction(row.id, cmd);
- }
- },
- // 序号计算方法
- indexMethod(i) {
- const p = this.pagination?.page || this.pageNum || this.currentPage || 1
- const ps = this.pagination?.pageSize || this.pageSize || 10
- return (p - 1) * ps + i + 1
- },
- // 卡片更多操作
- async handleCardAction(id, action) {
- switch (action) {
- case 'publish':
- await this.handlePublish([id])
- break
- case 'copy':
- await this.handleCopy(id)
- break
- case 'download':
- await this.handleDownload(id)
- break
- case 'delete':
- await this.handleDelete([id])
- break
- }
- },
- // 创建地图
- async handleCreate() {
- try {
- const { value } = await this.$prompt('请输入地图名称', '新建地图', {
- confirmButtonText: '确定',
- cancelButtonText: '取消',
- inputPattern: /^[\w\u4e00-\u9fa5_-]+$/,
- inputErrorMessage: '地图名格式不正确'
- })
- const response = await mapApi.createMap({ name: value })
- if (response.code === 200) {
- this.$message.success('地图创建成功')
- this.getList()
- }
- } catch (error) {
- if (error !== 'cancel') {
- this.$message.error('创建失败: ' + error.message)
- }
- }
- },
- // 导入地图
- handleImport() {
- this.$message.info('导入功能开发中...')
- },
- // 发布地图
- async handlePublish(ids) {
- try {
- await this.$confirm(`确定要发布选中的 ${ids.length} 个地图吗?`, '发布确认', {
- type: 'warning'
- })
- const response = await mapApi.publishMaps(ids)
- if (response.code === 200) {
- this.$message.success(response.message)
- this.getList()
- this.clearSelection()
- }
- } catch (error) {
- if (error !== 'cancel') {
- this.$message.error('发布失败: ' + error.message)
- }
- }
- },
- // 复制地图
- async handleCopy(id) {
- try {
- const { value } = await this.$prompt('请输入新地图名称', '复制地图', {
- confirmButtonText: '确定',
- cancelButtonText: '取消'
- })
- const response = await mapApi.copyMap(id, value)
- if (response.code === 200) {
- this.$message.success('地图复制成功')
- this.getList()
- }
- } catch (error) {
- if (error !== 'cancel') {
- this.$message.error('复制失败: ' + error.message)
- }
- }
- },
- // 下载地图
- async handleDownload(id) {
- try {
- const response = await mapApi.downloadMap(id, ['tree', 'vector', 'tile'])
- if (response.code === 200) {
- // 创建临时下载链接
- const link = document.createElement('a')
- link.href = response.data.downloadUrl
- link.download = `map_${id}.zip`
- link.click()
- this.$message.success('下载已开始')
- }
- } catch (error) {
- this.$message.error('下载失败: ' + error.message)
- }
- },
- // 删除地图
- async handleDelete(ids) {
- try {
- await this.$confirm(`确定要删除选中的 ${ids.length} 个地图吗?删除后无法恢复。`, '删除确认', {
- type: 'warning',
- confirmButtonText: '确定删除',
- cancelButtonText: '取消'
- })
- const response = await mapApi.deleteMap(ids)
- if (response.code === 200) {
- this.$message.success(response.message)
- this.getList()
- this.clearSelection()
- }
- } catch (error) {
- if (error !== 'cancel') {
- this.$message.error('删除失败: ' + error.message)
- }
- }
- },
- // 批量操作
- async handleBatchPublish() {
- await this.handlePublish(this.selectedMaps)
- },
- async handleBatchDelete() {
- await this.handleDelete(this.selectedMaps)
- },
- // 获取状态配置
- getStatusConfig(status) {
- const configs = {
- available: { type: 'success', icon: 'el-icon-check', text: '正常' },
- unavailable: { type: 'danger', icon: 'el-icon-close', text: '不可用' },
- building : { type: 'warning', icon: 'el-icon-loading', text: '正在建图' },
- recording : { type: 'warning', icon: 'el-icon-loading', text: '正在录制' }
- }
- return configs[status] || configs.ok
- },
- // 格式化时间
- formatDateTimeCompat,
- pickUpdatedAt,
- // 路由辅助函数
- buildNavTo(vm, row){
- return buildNavTo(row);
- },
- buildEditTo(vm, row){
- return buildEditTo(row);
- },
- pickId,
- // 本地存储相关
- loadViewMode() {
- try {
- const stored = localStorage.getItem('xt-map-view-mode')
- if (stored && ['card', 'table'].includes(stored)) {
- this.viewMode = stored
- }
- } catch (error) {
- console.warn('Failed to load view mode:', error)
- }
- },
- saveViewMode() {
- try {
- localStorage.setItem('xt-map-view-mode', this.viewMode)
- } catch (error) {
- console.warn('Failed to save view mode:', error)
- }
- },
- // 打开自主探索弹框
- openSelfExplore() {
- if (this.selectedMaps.length !== 1) {
- this.$message.warning('请选择一个地图进行自主探索')
- return
- }
- this.autoExploreOpen = true
- },
- // 打开遥控探索确认弹框
- openRemoteExplore() {
- if (this.selectedMaps.length !== 1) {
- this.$message.warning('请选择一个地图进行遥控探索')
- return
- }
- this.remoteExploreOpen = true
- },
- // 键盘监听方法
- addKeyboardListeners() {
- this.handleKeyboard = (event) => {
- if (this.autoExploreOpen) {
- if (event.key === 'Enter') {
- event.preventDefault()
- this.submitAutoExplore()
- } else if (event.key === 'Escape') {
- event.preventDefault()
- this.autoExploreCancel()
- }
- } else if (this.remoteExploreOpen) {
- if (event.key === 'Enter') {
- event.preventDefault()
- this.submitRemoteExplore()
- } else if (event.key === 'Escape') {
- event.preventDefault()
- this.remoteExploreCancel()
- }
- }
- }
- document.addEventListener('keydown', this.handleKeyboard)
- },
- removeKeyboardListeners() {
- if (this.handleKeyboard) {
- document.removeEventListener('keydown', this.handleKeyboard)
- }
- },
- // 自主探索相关方法
- autoExploreCancel() {
- this.autoExploreOpen = false
- },
- submitAutoExplore() {
- this.$refs["form"].validate(valid => {
- if (valid) {
- this.$message.success('已开始自主探索...')
- this.autoExploreOpen = false
- }
- })
- },
- // 遥控探索相关方法
- remoteExploreCancel() {
- this.remoteExploreOpen = false
- },
- submitRemoteExplore() {
- this.$message.success('已开始遥控探索...')
- this.remoteExploreOpen = false
- },
- submitDownload() {
- console.log('Download types:', this.downLoadTypes)
- this.downloadOpen = false
- this.$message.success('下载已开始')
- },
- submitConstruct() {
- console.log('Construct mode:', this.constructModle)
- this.constructOpen = false
- this.$message.success('构建已开始')
- }
- }
- };
- </script>
- <style lang="scss" scoped>
- .map-list-container {
- min-height: 100vh;
- background: var(--color-bg-secondary);
- padding: var(--spacing-6);
- .toolbar-section {
- background: var(--color-bg-card);
- border-radius: 12px;
- padding: var(--spacing-5);
- margin-bottom: var(--spacing-6);
- box-shadow: 0 6px 18px rgba(2, 6, 23, 0.05);
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: var(--spacing-4);
- .toolbar-filters {
- display: flex;
- align-items: center;
- gap: var(--spacing-3);
- .search-input {
- width: 260px;
- }
- .status-filter,
- .sort-select {
- width: 140px;
- }
- .el-input,
- .el-select {
- ::v-deep .el-input__inner {
- border-radius: var(--radius-lg);
- transition: all var(--duration-200) var(--ease-out);
- &:focus {
- border-color: var(--color-primary);
- box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.15);
- }
- }
- }
- }
- .toolbar-actions {
- display: flex;
- align-items: center;
- gap: var(--spacing-3);
- .el-button {
- border-radius: var(--radius-base);
- font-weight: var(--font-weight-medium);
- }
- .view-toggle {
- margin-left: var(--spacing-2);
- .el-button {
- padding: 8px 12px;
- border-radius: var(--radius-base);
- &:first-child {
- border-top-right-radius: 0;
- border-bottom-right-radius: 0;
- }
- &:last-child {
- border-top-left-radius: 0;
- border-bottom-left-radius: 0;
- }
- }
- }
- }
- }
- .main-content {
- margin-bottom: var(--spacing-6);
- // 卡片网格
- .card-grid {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
- gap: var(--spacing-6);
- &.compact {
- gap: var(--spacing-4);
- grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
- }
- @media (min-width: 1440px) {
- grid-template-columns: repeat(3, 1fr);
- &.compact {
- grid-template-columns: repeat(4, 1fr);
- }
- }
- }
- // 骨架屏
- .skeleton-card {
- background: var(--color-bg-card);
- border-radius: 12px;
- overflow: hidden;
- box-shadow: 0 6px 18px rgba(2, 6, 23, 0.05);
- .skeleton-thumb {
- height: 160px;
- background: linear-gradient(90deg, var(--color-bg-secondary) 25%, var(--color-bg-tertiary) 50%, var(--color-bg-secondary) 75%);
- background-size: 200% 100%;
- animation: skeleton-loading 1.5s infinite;
- }
- .skeleton-content {
- padding: 16px;
- .skeleton-title,
- .skeleton-remark,
- .skeleton-meta {
- background: linear-gradient(90deg, var(--color-bg-secondary) 25%, var(--color-bg-tertiary) 50%, var(--color-bg-secondary) 75%);
- background-size: 200% 100%;
- animation: skeleton-loading 1.5s infinite;
- border-radius: var(--radius-base);
- }
- .skeleton-title {
- height: 20px;
- margin-bottom: var(--spacing-3);
- width: 80%;
- }
- .skeleton-remark {
- height: 14px;
- margin-bottom: var(--spacing-2);
- width: 100%;
- }
- .skeleton-meta {
- height: 12px;
- width: 60%;
- }
- }
- }
- // 表格容器
- .table-wrapper {
- background: var(--color-bg-card);
- border-radius: 12px;
- overflow: hidden;
- box-shadow: 0 6px 18px rgba(2, 6, 23, 0.05);
- }
- // 表格
- .map-table {
- ::v-deep .el-table__header {
- background: var(--color-bg-secondary);
- }
- ::v-deep .el-table__body {
- .el-table__row {
- &:hover {
- background: var(--color-bg-secondary);
- }
- }
- }
- ::v-deep .el-table__header th,
- ::v-deep .el-table__body td {
- padding: 10px 12px;
- }
- ::v-deep .el-table-column--selection .cell {
- display: flex;
- justify-content: center;
- align-items: center;
- padding: 10px 8px;
- }
- /* 表头"序号"不换行、居中 */
- ::v-deep .col-index-header .cell {
- white-space: nowrap;
- text-align: center;
- padding: 10px 8px;
- }
- /* 序号数字等宽、居中、颜色稍弱一点 */
- ::v-deep .col-index .cell {
- display: flex;
- justify-content: center;
- align-items: center;
- padding: 10px 8px;
- color: var(--color-text-secondary, #6b7280);
- font-variant-numeric: tabular-nums; /* 等宽数字 */
- }
- ::v-deep .cell {
- line-height: 20px;
- white-space: nowrap;
- }
- /* 避免某些主题对固定列的默认阴影/间距影响布局(现在已不固定,这里只是兜底) */
- ::v-deep .el-table__fixed-right {
- right: 0;
- box-shadow: none;
- }
- .table-name {
- display: flex;
- align-items: center;
- gap: var(--spacing-2);
- }
- .table-thumb {
- width: 40px;
- height: 24px;
- border-radius: var(--radius-base);
- overflow: hidden;
- background: var(--color-bg-secondary);
- display: flex;
- align-items: center;
- justify-content: center;
- margin: 0 auto;
- img {
- width: 100%;
- height: 100%;
- object-fit: cover;
- }
- i {
- color: var(--color-text-quaternary);
- font-size: var(--font-size-sm);
- }
- }
- .op-cell {
- display: flex;
- justify-content: flex-end;
- align-items: center;
- gap: 8px;
- }
- .op-link {
- display: inline-flex;
- align-items: center;
- color: var(--color-primary, #1d4ed8);
- text-decoration: none;
- font-size: var(--font-size-sm);
- transition: color 0.2s ease;
- &:hover {
- color: var(--color-primary-light);
- text-decoration: none;
- }
- }
- .el-dropdown-link {
- color: var(--color-primary);
- cursor: pointer;
- display: inline-flex;
- align-items: center;
- font-size: var(--font-size-sm);
- &:hover {
- color: var(--color-primary-light);
- }
- }
- .op-text {
- margin-left: 4px;
- }
-
- .xt-more .is-danger {
- color: #e11d48;
- }
- .xt-more .is-danger i {
- color: #e11d48;
- }
- }
- // 空态
- .empty-state {
- text-align: center;
- padding: var(--spacing-12) var(--spacing-6);
- background: var(--color-bg-card);
- border-radius: 12px;
- box-shadow: 0 6px 18px rgba(2, 6, 23, 0.05);
- .empty-illustration {
- margin-bottom: var(--spacing-6);
- i {
- font-size: 80px;
- color: var(--color-text-quaternary);
- opacity: 0.6;
- }
- }
- .empty-title {
- color: var(--color-text-primary);
- font-size: var(--font-size-xl);
- font-weight: var(--font-weight-semibold);
- margin: 0 0 var(--spacing-3) 0;
- }
- .empty-description {
- color: var(--color-text-secondary);
- font-size: var(--font-size-base);
- margin: 0 0 var(--spacing-6) 0;
- line-height: var(--line-height-relaxed);
- }
- .empty-actions {
- display: flex;
- justify-content: center;
- gap: var(--spacing-3);
- .el-button {
- border-radius: var(--radius-base);
- font-weight: var(--font-weight-medium);
- }
- }
- }
- }
- .pagination-section {
- background: var(--color-bg-card);
- border-radius: 12px;
- padding: var(--spacing-4) var(--spacing-5);
- box-shadow: 0 6px 18px rgba(2, 6, 23, 0.05);
- display: flex;
- align-items: center;
- justify-content: space-between;
- .density-toggle {
- .el-button-group {
- .el-button {
- padding: 4px 12px;
- font-size: var(--font-size-xs);
- border-radius: var(--radius-base);
- &:first-child {
- border-top-right-radius: 0;
- border-bottom-right-radius: 0;
- }
- &:last-child {
- border-top-left-radius: 0;
- border-bottom-left-radius: 0;
- }
- }
- }
- }
- .el-pagination {
- ::v-deep .el-pager {
- .number {
- border-radius: var(--radius-base);
- margin: 0 2px;
- }
- }
- ::v-deep .btn-prev,
- ::v-deep .btn-next {
- border-radius: var(--radius-base);
- }
- }
- }
- // 探索按钮样式
- .xt-btn {
- border-radius: var(--radius-base);
- font-weight: var(--font-weight-medium);
- transition: all var(--duration-200) var(--ease-out);
- &:disabled {
- opacity: 0.6;
- cursor: not-allowed;
- }
- &:hover:not(:disabled) {
- transform: translateY(-1px);
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
- }
- }
- // 探索弹框样式
- .xt-explore-dialog ::v-deep .el-dialog {
- border-radius: 12px;
- }
- .xt-explore-dialog ::v-deep .el-dialog__body {
- padding: 18px 20px 6px;
- }
- /* 字体与间距 */
- .xt-title {
- display: flex;
- flex-direction: column;
- font-weight: 600;
- font-size: 16px;
- }
- .xt-subtitle {
- font-size: 12px;
- color: var(--text-secondary, #6b7280);
- font-weight: 400;
- margin-top: 4px;
- }
- .xt-form {
- background: white;
- border-radius: 12px;
- padding: 24px;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
- .el-form-item {
- margin-bottom: 24px;
- &:last-child {
- margin-bottom: 0;
- }
- .el-form-item__label {
- color: #333;
- font-size: 14px;
- font-weight: 500;
- line-height: 1.4;
- }
- }
- }
- .xt-field {
- display: flex;
- align-items: center;
- gap: 12px;
- }
- .xt-unit {
- color: #666;
- font-size: 14px;
- font-weight: 500;
- white-space: nowrap;
- }
- .xt-hint {
- color: #888;
- font-size: 12px;
- margin-left: auto;
- white-space: nowrap;
- }
- .xt-footer {
- display: flex;
- justify-content: flex-end;
- gap: 12px;
- padding: 16px 0;
- .el-button {
- padding: 10px 24px;
- border-radius: 8px;
- font-weight: 500;
- transition: all 0.2s ease;
- border: none;
- &.el-button--default {
- background: #f1f5f9;
- color: #64748b;
-
- &:hover {
- background: #e2e8f0;
- color: #475569;
- transform: translateY(-1px);
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
- }
- }
- &.el-button--primary {
- background: linear-gradient(135deg, #10b981, #059669);
- color: white;
-
- &:hover {
- background: linear-gradient(135deg, #059669, #047857);
- transform: translateY(-1px);
- box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
- }
- }
- }
- }
- /* 输入框外层容器样式 */
- .xt-explore-dialog ::v-deep .el-input-number {
- width: 160px;
- border-radius: 24px;
- overflow: hidden;
- border: 1.5px solid #d1d5db;
- transition: all 0.2s ease;
- background: white;
- &:hover {
- border-color: #9ca3af;
- }
- &:focus-within,
- &.is-focus {
- border-color: #08C28E;
- box-shadow: 0 0 0 2px rgba(8, 194, 142, 0.2);
- }
- .el-input__inner {
- height: 40px;
- line-height: 40px;
- border: none;
- border-radius: 0;
- box-shadow: none;
- padding-right: 40px;
- font-size: 14px;
- background: transparent;
- &:focus {
- outline: none;
- border: none;
- box-shadow: none;
- }
- &::placeholder {
- color: #9ca3af;
- font-size: 13px;
- }
- }
- }
- /* 右侧加减按钮 */
- .xt-explore-dialog ::v-deep .el-input-number.is-controls-right .el-input-number__increase,
- .xt-explore-dialog ::v-deep .el-input-number.is-controls-right .el-input-number__decrease {
- right: 0;
- border: none;
- border-left: 1.5px solid #d1d5db;
- width: 35px;
- height: 20px;
- border-radius: 0;
- background: #f9fafb;
- color: #6b7280;
- transition: all 0.2s ease;
- &:hover {
- background: #f3f4f6;
- color: #374151;
- }
- }
- // 遥控探索确认弹框样式
- .xt-confirm-dialog {
- ::v-deep .el-dialog {
- border-radius: 12px;
- }
- .confirm-title {
- display: flex;
- align-items: center;
- font-size: 16px;
- font-weight: 600;
- color: #333;
- }
- .confirm-content {
- padding: 20px 0;
-
- p {
- margin: 0;
- font-size: 14px;
- line-height: 1.6;
- color: #666;
- }
- }
- ::v-deep .el-dialog__footer {
- padding: 16px 24px 24px;
-
- .el-button {
- padding: 8px 20px;
- border-radius: 6px;
- font-weight: 500;
-
- &.el-button--primary {
- background: linear-gradient(135deg, #10b981, #059669);
- border-color: #10b981;
-
- &:hover {
- background: linear-gradient(135deg, #059669, #047857);
- border-color: #059669;
- }
- }
- }
- }
- }
- // 保留的对话框样式
- ::v-deep .el-dialog__body {
- padding: var(--spacing-5);
- }
- }
- // 骨架屏动画
- @keyframes skeleton-loading {
- 0% {
- background-position: -200% 0;
- }
- 100% {
- background-position: 200% 0;
- }
- }
- // 暗色主题适配
- html.dark {
- .map-list-container {
- .toolbar-section {
- background: var(--color-bg-tertiary);
- box-shadow: 0 6px 18px rgba(0, 0, 0, 0.15);
- }
- .skeleton-card {
- background: var(--color-bg-tertiary);
- box-shadow: 0 6px 18px rgba(0, 0, 0, 0.15);
- }
- .table-wrapper {
- background: var(--color-bg-tertiary);
- box-shadow: 0 6px 18px rgba(0, 0, 0, 0.15);
- }
- .map-table {
- ::v-deep .el-table__header {
- background: var(--color-bg-quaternary);
- }
- ::v-deep .el-table__body {
- .el-table__row {
- &:hover {
- background: var(--color-bg-quaternary);
- }
- }
- }
- }
- .empty-state,
- .pagination-section {
- background: var(--color-bg-tertiary);
- box-shadow: 0 6px 18px rgba(0, 0, 0, 0.15);
- }
- // 暗色主题下的探索弹框样式
- .xt-explore-dialog {
- ::v-deep .el-dialog {
- background: #1e293b;
- box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
- }
- ::v-deep .el-dialog__header {
- background: linear-gradient(135deg, #334155, #475569);
- border-bottom: 1px solid #475569;
- }
- ::v-deep .el-dialog__body,
- ::v-deep .el-dialog__footer {
- background: #1e293b;
- }
- ::v-deep .el-dialog__footer {
- border-top: 1px solid #475569;
- }
- .xt-title {
- color: #f1f5f9;
- }
- .xt-subtitle {
- color: #94a3b8;
- }
- .xt-form {
- background: #0f172a;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
- .el-form-item__label {
- color: #e2e8f0 !important;
- }
- }
- .xt-unit {
- color: #cbd5e1;
- }
- .xt-hint {
- color: #94a3b8;
- }
- ::v-deep .el-input-number {
- background: #0f172a;
- border-color: #475569;
- &:hover {
- border-color: #64748b;
- }
- &:focus-within,
- &.is-focus {
- border-color: #08C28E;
- box-shadow: 0 0 0 2px rgba(8, 194, 142, 0.3);
- }
- .el-input__inner {
- background: transparent;
- color: #e2e8f0;
- &::placeholder {
- color: #64748b;
- }
- }
- }
- ::v-deep .el-input-number__increase,
- ::v-deep .el-input-number__decrease {
- background: #334155;
- border-color: #475569;
- color: #94a3b8;
- &:hover {
- background: #475569;
- color: #e2e8f0;
- }
- }
- .xt-footer {
- .el-button {
- &.el-button--default {
- background: #334155;
- color: #94a3b8;
-
- &:hover {
- background: #475569;
- color: #e2e8f0;
- }
- }
- &.el-button--primary {
- background: linear-gradient(135deg, #10b981, #059669);
- color: white;
-
- &:hover {
- background: linear-gradient(135deg, #059669, #047857);
- }
- }
- }
- }
- }
- // 暗色主题下的确认弹框样式
- .xt-confirm-dialog {
- ::v-deep .el-dialog {
- background: #1e293b;
- }
- .confirm-title {
- color: #f1f5f9;
- }
- .confirm-content p {
- color: #cbd5e1;
- }
- ::v-deep .el-dialog__footer {
- background: #1e293b;
-
- .el-button {
- &:not(.el-button--primary) {
- background: #334155;
- color: #94a3b8;
- border-color: #475569;
-
- &:hover {
- background: #475569;
- color: #e2e8f0;
- }
- }
- }
- }
- }
- }
- }
- // 响应式设计
- @media (max-width: 1439px) {
- .map-list-container {
- .main-content {
- .card-grid {
- grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
- &.compact {
- grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
- }
- }
- }
- }
- }
- @media (max-width: 768px) {
- .map-list-container {
- padding: var(--spacing-4);
- .toolbar-section {
- flex-direction: column;
- align-items: stretch;
- gap: var(--spacing-4);
- .toolbar-filters {
- flex-direction: column;
- align-items: stretch;
- .search-input,
- .status-filter,
- .sort-select {
- width: 100%;
- }
- }
- .toolbar-actions {
- justify-content: space-between;
- flex-wrap: wrap;
- .view-toggle {
- margin-left: 0;
- order: -1;
- }
- }
- }
- .main-content {
- .card-grid {
- grid-template-columns: 1fr;
- gap: var(--spacing-4);
- &.compact {
- gap: var(--spacing-3);
- }
- }
- }
- .pagination-section {
- flex-direction: column;
- gap: var(--spacing-3);
- .density-toggle {
- order: 1;
- }
- }
- }
- }
- </style>
|