Bladeren bron

feat(frontend): 基站查询页卡片化 UI 升级

- 查询条件与结果地图改为统一卡片布局
- 筛选项与操作按钮对齐持续联系页间距和控件样式
- 基站地图空状态统一为 EmptyState,保留底图可见
cc 1 dag geleden
bovenliggende
commit
52398101a8

+ 12 - 4
ai-electron/frontend/src/trans/views/aiBaseStation/components/BaseStationQueryMap.vue

@@ -3,13 +3,21 @@
     <div ref="mapBoxRef" class="base-station-query-map__box">
       <div ref="mapRef" class="base-station-query-map__canvas"></div>
       <div v-if="errorMsg" class="base-station-query-map__error">{{ errorMsg }}</div>
-      <div v-if="!points.length" class="base-station-query-map__empty">暂无基站坐标数据</div>
+      <div v-if="!points.length" class="base-station-query-map__empty">
+        <EmptyState
+          variant="compact"
+          title="暂无基站坐标数据"
+          description="输入 LAC / CID 并搜索后,查询结果将在地图中展示"
+          description-width="min(100%, 300px)"
+        />
+      </div>
     </div>
   </div>
 </template>
 
 <script lang="ts" setup>
   import { computed, nextTick, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'vue';
+  import { EmptyState } from '@/core/components/EmptyState';
   import { useDesign } from '@/core/hooks/web/useDesign';
   import {
     getOfflineMapPmtilesPath,
@@ -527,10 +535,10 @@
       display: flex;
       align-items: center;
       justify-content: center;
-      background: rgb(248 250 252 / 68%);
-      color: #64748b;
-      font-size: 14px;
+      padding: 16px;
+      background: rgb(248 250 252 / 45%);
       pointer-events: none;
+      box-sizing: border-box;
     }
 
     .base-station-query-map__error {

+ 363 - 105
ai-electron/frontend/src/trans/views/aiBaseStation/index.vue

@@ -1,67 +1,66 @@
 <template>
   <div :class="prefixCls" class="ai-base-station-page">
-    <div class="filter-bar filter-bar--stacked">
-      <div class="filter-row filter-row--carrier">
+    <section class="filter-bar">
+      <div class="qj-filter-head">
+        <div class="qj-card-title">查询条件</div>
+        <div class="qj-filter-actions">
+          <div class="side-actions">
+            <!-- Web 化:模版与转换结果都通过浏览器下载,不再依赖 Electron 打开本机目录 -->
+            <Button :loading="templateLoading" @click="handleFetchTemplate">下载转换基站模版</Button>
+            <Button :loading="convertLoading" @click="convertFileInputRef?.click()">批量转换基站</Button>
+            <input
+              ref="convertFileInputRef"
+              class="side-actions__file-input"
+              type="file"
+              accept=".xls,.xlsx"
+              @change="handleConvertFileChange"
+            />
+          </div>
+          <Button type="primary" :loading="queryLoading" @click="handleSearch">搜索</Button>
+          <Button @click="handleReset">重置</Button>
+        </div>
+      </div>
+
+      <div class="qj-filter-grid">
         <div class="filter-item filter-item--carrier">
-          <span class="filter-item__label">MNC:</span>
+          <span class="filter-label">运营商</span>
           <RadioGroup v-model:value="formState.mnc">
             <Radio v-for="item in carrierOptions" :key="item.value" :value="item.value">{{ item.label }}</Radio>
           </RadioGroup>
         </div>
-      </div>
-
-      <div class="filter-row">
         <div class="filter-item">
-          <span class="filter-item__label">LAC:</span>
+          <span class="filter-label">LAC</span>
           <Input
             v-model:value="formState.lac"
-            class="filter-item__input"
+            class="filter-input filter-input--code"
             :maxlength="15"
-            placeholder="请输入搜索内容"
+            placeholder="请输入 LAC"
             @input="handleCodeInput('lac', $event)"
           />
         </div>
         <div class="filter-item">
-          <span class="filter-item__label">CID:</span>
+          <span class="filter-label">CID</span>
           <Input
             v-model:value="formState.ci"
-            class="filter-item__input"
+            class="filter-input filter-input--code"
             :maxlength="15"
-            placeholder="请输入搜索内容"
+            placeholder="请输入 CID"
             @input="handleCodeInput('ci', $event)"
           />
         </div>
         <div class="filter-item">
-          <span class="filter-item__label">DEC/HEX:</span>
-          <Select v-model:value="formState.radixMode" class="filter-item__select" :options="radixOptions" />
-        </div>
-        <div class="filter-actions">
-          <Button type="primary" :loading="queryLoading" @click="handleSearch">搜索</Button>
-          <Button @click="handleReset">重置</Button>
-        </div>
-        <div class="side-actions">
-          <!-- Web 化:模版与转换结果都通过浏览器下载,不再依赖 Electron 打开本机目录 -->
-          <Button :loading="templateLoading" @click="handleFetchTemplate">下载转换基站模版</Button>
-          <Button :loading="convertLoading" @click="convertFileInputRef?.click()">批量转换基站</Button>
-          <input
-            ref="convertFileInputRef"
-            class="side-actions__file-input"
-            type="file"
-            accept=".xls,.xlsx"
-            @change="handleConvertFileChange"
-          />
+          <span class="filter-label">进制</span>
+          <Select v-model:value="formState.radixMode" class="filter-select" :options="radixOptions" />
         </div>
       </div>
-    </div>
+    </section>
 
     <div class="content-panel">
-      <div class="table-card">
+      <section class="table-card">
         <div class="panel-header">
-          <div>
+          <div class="panel-heading">
             <div class="panel-title">基站结果地图</div>
-            <div class="panel-desc">
-              {{ resultSummary }}
-            </div>
+            <div class="panel-desc">{{ resultSummary }}</div>
           </div>
         </div>
         <div class="table-panel">
@@ -69,7 +68,7 @@
             <BaseStationQueryMap :points="mapPoints" />
           </div>
         </div>
-      </div>
+      </section>
     </div>
   </div>
 </template>
@@ -304,132 +303,391 @@
 </script>
 
 <style lang="less">
-  @import '../styles/trans-analysis-table-ui.less';
-
   @prefix-cls: ~'jeesite-ai-base-station';
 
+  /* ==========================================================================
+   * 基站查询:卡片语言对齐「持续联系」
+   * 背景 #f4f6fa · 卡片圆角 14 · 细边框 #e7ebf3 · 卡片间距 8px · 主色 #2a50ec
+   * ========================================================================== */
   .@{prefix-cls}.ai-base-station-page {
-    .analysis-single-panel-frame();
-
-    // 去掉 content-panel 的底色。
-    // 该底色来自 .analysis-page-frame() → .content-panel { .analysis-card-strip() },
-    // 而 @analysis-card-strip-bg = @content-bg = #f0f2f5,在白色页面上会显出一圈灰边。
-    // ★ 只覆盖 background,保留 strip 的 padding / flex / overflow,避免动到布局;
-    //   也刻意不改 mixin 本身(.analysis-page-frame 被十几个分析页共用)。
-    .content-panel {
-      background: transparent;
-    }
-
+    display: flex;
+    flex-direction: column;
+    gap: 8px;
+    width: calc(100% + 24px);
+    height: 100%;
+    min-height: 0;
+    margin: 0 -12px;
+    padding: 8px 10px !important;
+    background: #f4f6fa;
+    box-sizing: border-box;
+    overflow: hidden;
+
+    /* ---------------- 查询条件卡 ---------------- */
     .filter-bar {
+      display: flex;
+      flex: 0 0 auto;
       flex-direction: column;
+      flex-wrap: nowrap;
       align-items: stretch;
-      gap: 10px;
-      padding: 6px 8px 8px;
+      gap: 0;
+      min-height: 0;
+      width: 100%;
+      padding: 0;
+      border: 1px solid #e7ebf3;
+      border-radius: 14px;
+      background: #fff;
+      box-shadow: 0 1px 2px rgb(16 24 40 / 4%);
+      box-sizing: border-box;
+      overflow: hidden;
     }
 
-    .filter-row {
+    .qj-filter-head {
       display: flex;
+      flex: 0 0 auto;
+      flex-wrap: wrap;
       align-items: center;
-      gap: 8px;
+      gap: 8px 10px;
+      min-height: 42px;
+      padding: 0 14px;
+      border-bottom: 1px solid #eff2f7;
+      box-sizing: border-box;
+    }
+
+    .qj-card-title {
+      position: relative;
+      padding-left: 11px;
+      color: #111827;
+      font-size: 15px;
+      font-weight: 600;
+      line-height: 22px;
+      white-space: nowrap;
+
+      &::before {
+        content: '';
+        position: absolute;
+        left: 0;
+        top: 4px;
+        bottom: 4px;
+        width: 3px;
+        border-radius: 2px;
+        background: #2a50ec;
+      }
+    }
+
+    .qj-filter-actions {
+      display: flex;
+      flex: 0 0 auto;
       flex-wrap: wrap;
+      align-items: center;
+      gap: 8px;
+      margin-left: auto;
     }
 
-    .filter-row--carrier {
-      min-height: 24px;
+    .qj-filter-actions .ant-btn {
+      height: 30px;
+      padding: 0 16px;
+      border-radius: 8px;
+      font-size: 12px;
+      font-weight: 500;
+      box-shadow: none;
     }
 
-    .filter-item {
+    .qj-filter-actions .ant-btn-primary {
+      border-color: #2a50ec;
+      background: #2a50ec;
+
+      &:hover,
+      &:focus {
+        border-color: #4b6bf0;
+        background: #4b6bf0;
+      }
+
+      &:active {
+        border-color: #1e3ed0;
+        background: #1e3ed0;
+      }
+    }
+
+    .qj-filter-actions .ant-btn:not(.ant-btn-primary) {
+      border: 1px solid #e7ebf3;
+      background: #fff;
+      color: #5b6577;
+
+      &:hover,
+      &:focus {
+        border-color: #bfcdfb;
+        background: #f8faff;
+        color: #2a50ec;
+      }
+    }
+
+    .qj-filter-grid {
       display: flex;
+      flex-wrap: wrap;
       align-items: center;
-      gap: 4px;
-      min-width: 0;
+      gap: 8px 16px;
+      padding: 12px 14px 13px;
+      box-sizing: border-box;
     }
 
-    .filter-item--carrier {
-      gap: 8px;
+    .filter-item {
+      display: inline-flex;
+      flex: 0 0 auto;
+      align-items: center;
+      gap: 6px;
+      min-width: 0;
     }
 
-    .filter-item__label {
-      color: #6b7280;
+    .filter-label {
+      flex: 0 0 auto;
+      color: #5b6577;
       font-size: 12px;
-      line-height: 20px;
+      line-height: 32px;
       white-space: nowrap;
     }
 
-    .filter-item__input {
-      width: 180px;
+    .filter-input {
+      width: 160px;
+      min-width: 0;
     }
 
-    .filter-item__select {
-      width: 160px;
+    .filter-select {
+      width: 140px;
+      min-width: 0;
     }
 
-    .filter-actions,
-    .side-actions {
-      display: flex;
+    .filter-item--carrier {
+      gap: 8px;
+    }
+
+    .filter-item--carrier .ant-radio-group {
+      display: inline-flex;
       align-items: center;
-      gap: 6px;
+      gap: 14px;
+      min-height: 32px;
+    }
+
+    .filter-item--carrier .ant-radio-wrapper {
+      margin-inline-end: 0;
+      color: #5b6577;
+      font-size: 12px;
+      line-height: 32px;
     }
 
     .side-actions {
-      margin-left: auto;
+      display: inline-flex;
+      flex: 0 0 auto;
+      align-items: center;
+      gap: 8px;
+      flex-wrap: wrap;
+    }
 
-      // 原生 file 控件只用于触发选择,视觉上由「批量转换基站」按钮承担。
-      // 之前这个类没有任何样式,导致原生控件直接裸露在按钮旁边。
-      &__file-input {
-        display: none;
-      }
+    .side-actions .ant-btn {
+      height: 30px;
+      padding: 0 12px;
+      border: 1px solid #e7ebf3;
+      border-radius: 8px;
+      background: #fff;
+      color: #5b6577;
+      font-size: 12px;
+      font-weight: 500;
+      box-shadow: none;
     }
 
-    .table-card {
-      padding: 8px;
+    .side-actions .ant-btn:hover,
+    .side-actions .ant-btn:focus {
+      border-color: #bfcdfb;
+      background: #f8faff;
+      color: #2a50ec;
     }
 
-    .table-panel {
-      padding-top: 0;
+    .side-actions__file-input {
+      display: none;
     }
 
-    .map-panel__body {
+    /* ---------------- 表单控件 ---------------- */
+    .qj-filter-grid {
+      .ant-input,
+      .ant-input-affix-wrapper {
+        height: 32px;
+        padding: 0 10px;
+        border: 1px solid #e7ebf3;
+        border-radius: 8px;
+        background: #fff;
+        box-shadow: none;
+        font-size: 12px;
+        transition:
+          border-color 0.2s ease,
+          box-shadow 0.2s ease;
+      }
+
+      .ant-input-affix-wrapper > .ant-input {
+        height: 100%;
+        padding: 0;
+        border: 0;
+        border-radius: 0;
+        background: transparent;
+        box-shadow: none;
+      }
+
+      .ant-input:hover,
+      .ant-input-affix-wrapper:hover,
+      .ant-select:hover .ant-select-selector {
+        border-color: #bfcdfb;
+      }
+
+      .ant-input:focus,
+      .ant-input-affix-wrapper-focused,
+      .ant-select-focused .ant-select-selector {
+        border-color: #2a50ec !important;
+        box-shadow: 0 0 0 2px rgb(42 80 236 / 12%) !important;
+      }
+
+      .ant-select {
+        height: 32px;
+      }
+
+      .ant-select .ant-select-selector {
+        height: 32px !important;
+        padding: 0 10px;
+        border: 1px solid #e7ebf3;
+        border-radius: 8px;
+        background: #fff;
+        box-shadow: none;
+      }
+
+      .ant-select-single .ant-select-selection-item,
+      .ant-select-single .ant-select-selection-placeholder {
+        line-height: 30px;
+        font-size: 12px;
+      }
+
+      .ant-select-arrow {
+        color: #98a2b3;
+        font-size: 11px;
+      }
+    }
+
+    /* ---------------- 结果卡 ---------------- */
+    .content-panel {
       display: flex;
       flex: 1 1 auto;
+      flex-direction: column;
+      gap: 8px;
       min-height: 0;
+      padding: 0;
+      background: transparent;
+      box-sizing: border-box;
       overflow: hidden;
     }
 
-    :deep(.ant-radio-wrapper) {
-      color: #475569;
-      font-size: 14px;
+    .table-card {
+      display: flex;
+      flex: 1 1 auto;
+      flex-direction: column;
+      min-height: 0;
+      padding: 0;
+      border: 1px solid #e7ebf3;
+      border-radius: 14px;
+      background: #fff;
+      box-shadow: 0 1px 2px rgb(16 24 40 / 4%);
+      box-sizing: border-box;
+      overflow: hidden;
     }
 
-    :deep(.ant-input),
-    :deep(.ant-select-selector),
-    :deep(.ant-btn) {
-      border-radius: 8px;
+    .panel-header {
+      display: flex;
+      align-items: center;
+      gap: 10px;
+      flex: 0 0 auto;
+      width: 100%;
+      min-height: 42px;
+      padding: 6px 14px;
+      border-bottom: 1px solid #eff2f7;
+      background: #fff;
+      box-sizing: border-box;
+    }
+
+    .panel-heading {
+      display: flex;
+      flex: 1 1 auto;
+      align-items: center;
+      gap: 10px;
+      min-width: 0;
     }
 
-    @media screen and (max-width: 1280px) {
-      .side-actions {
-        margin-left: 0;
+    .panel-title {
+      position: relative;
+      flex: 0 0 auto;
+      padding-left: 11px;
+      color: #111827;
+      font-size: 15px;
+      font-weight: 600;
+      line-height: 22px;
+      white-space: nowrap;
+
+      &::before {
+        content: '';
+        position: absolute;
+        left: 0;
+        top: 4px;
+        bottom: 4px;
+        width: 3px;
+        border-radius: 2px;
+        background: #2a50ec;
       }
     }
 
+    .panel-desc {
+      flex: 1 1 auto;
+      min-width: 0;
+      overflow: hidden;
+      color: #98a2b3;
+      font-size: 11px;
+      line-height: 16px;
+      white-space: nowrap;
+      text-overflow: ellipsis;
+    }
+
+    .table-panel {
+      display: flex;
+      flex: 1 1 auto;
+      flex-direction: column;
+      min-height: 0;
+      padding: 0;
+      overflow: hidden;
+    }
+
+    .map-panel__body {
+      display: flex;
+      flex: 1 1 auto;
+      min-height: 0;
+      overflow: hidden;
+    }
+
+    .map-panel__body .base-station-query-map__box {
+      border: 0;
+      border-radius: 0;
+      background: #f4f7fb;
+    }
+
+    /* ---------------- 响应式 ---------------- */
     @media screen and (max-width: 992px) {
-      .filter-row {
-        gap: 12px;
+      .filter-item {
+        width: 100%;
       }
 
-      .filter-item,
-      .filter-item__input,
-      .filter-item__select,
-      .filter-actions,
-      .side-actions {
-        width: 100%;
+      .filter-input,
+      .filter-select,
+      .qj-filter-grid .ant-select {
+        flex: 1 1 auto;
+        width: auto;
+        min-width: 0;
       }
 
-      .filter-actions,
-      .side-actions {
-        margin-left: 0;
-        justify-content: flex-start;
+      .filter-item--carrier .ant-radio-group {
+        flex-wrap: wrap;
+        gap: 8px 14px;
       }
     }
   }