3 Commits b1ba59e25f ... 3525c9de61

Auteur SHA1 Message Date
  DESKTOP-6LTVLN7\Liumouren 3525c9de61 Merge branch 'MicroFunction' of http://47.103.92.60:3003/skyversation/qp_onemap_ui into onemap_zmg il y a 3 semaines
  DESKTOP-6LTVLN7\Liumouren 903299a1a3 Merge branch 'onemap_zmg' of http://47.103.92.60:3003/skyversation/qp_onemap_ui into MicroFunction il y a 3 semaines
  DESKTOP-6LTVLN7\Liumouren cb923a1f58 微功能中心UI调整,添加锚点;搜索框支持回车搜索,清空,添加暂无数据状态;演示场景页面优化:元素支持拖动编辑、删除;发送请求添加元素校验拦截。 il y a 3 semaines

+ 26 - 6
src/components/AppVue/numberScroll.vue

@@ -6,23 +6,43 @@
 import { ref, watch, onMounted } from "vue";
 
 const props = defineProps({
-  value: { type: Number, default: 0 },
+  /** 接口常为字符串;父组件 card 也按字符串传,需统一成数字再插值,避免 "0"+421028 变成字符串拼接 */
+  value: { type: [Number, String], default: 0 },
   duration: { type: Number, default: 1500 },
   easing: { type: Function, default: (t) => t * (2 - t) },
 });
 
-const displayValue = ref(0);
+const displayValue = ref("0");
 const numberEl = ref(null);
+/** 上一次动画结束时的数值,供 watch 作为起点(避免 oldVal 为 undefined / 非数字) */
+const lastNumeric = ref(0);
+
+function toNumberSafe(v) {
+  if (v == null || v === "") {
+    return 0;
+  }
+  if (typeof v === "number" && Number.isFinite(v)) {
+    return v;
+  }
+  const s = String(v).replace(/,/g, "").trim();
+  const n = Number(s);
+  return Number.isFinite(n) ? n : 0;
+}
 
 function animate(start, end) {
+  const s = toNumberSafe(start);
+  const e = toNumberSafe(end);
   const startTime = performance.now();
   const frame = (currentTime) => {
     const elapsed = currentTime - startTime;
     const progress = Math.min(elapsed / props.duration, 1);
-    displayValue.value = Math.floor(start + (end - start) * props.easing(progress));
-    displayValue.value = displayValue.value.toLocaleString();
+    const raw = s + (e - s) * props.easing(progress);
+    const n = Math.floor(raw);
+    displayValue.value = Number.isFinite(n) ? n.toLocaleString() : "0";
     if (progress < 1) {
       requestAnimationFrame(frame);
+    } else {
+      lastNumeric.value = e;
     }
   };
   requestAnimationFrame(frame);
@@ -30,8 +50,8 @@ function animate(start, end) {
 
 watch(
   () => props.value,
-  (newVal, oldVal) => {
-    animate(oldVal, newVal);
+  (newVal) => {
+    animate(lastNumeric.value, newVal);
   }
 );
 

Fichier diff supprimé car celui-ci est trop grand
+ 589 - 216
src/components/wgn/controlPanel.vue


+ 95 - 0
src/data/wgnSceneList.js

@@ -0,0 +1,95 @@
+/**
+ * 微功能中心 / 场景页「场景名称」级联数据(value 与 DMS c_scene_name 一致)
+ */
+export const WGN_SCENE_LIST = [
+  {
+    value: "1.5.1",
+    label: "拓扑计算",
+    children: [
+      {
+        value: "1.5.1.1",
+        label: "点线面拓扑关系",
+      },
+    ],
+  },
+  {
+    value: "1.5.2",
+    label: "空间量算",
+    children: [
+      { value: "1.5.2.1", label: "两点欧氏距离" },
+      { value: "1.5.2.2", label: "点到线的最短距离" },
+      { value: "1.5.2.3", label: "点到面的最短距离" },
+      { value: "1.5.2.4", label: "面到面的最短距离" },
+      { value: "1.5.2.5", label: "折线距离(累加线段长度)" },
+      { value: "1.5.2.6", label: "平面面积、不规则面面积" },
+      { value: "1.5.2.7", label: "曲面面积,考虑地形起伏" },
+      { value: "1.5.2.8", label: "单点高程查询" },
+      { value: "1.5.2.9", label: "两点高程差" },
+      { value: "1.5.2.10", label: "区域高程统计" },
+    ],
+  },
+  {
+    value: "1.5.3",
+    label: "几何运算",
+    children: [
+      { value: "1.5.3.1", label: "并集运算" },
+      { value: "1.5.3.2", label: "交集运算" },
+      { value: "1.5.3.3", label: "差集运算" },
+      { value: "1.5.3.4", label: "几何参数计算" },
+    ],
+  },
+  {
+    value: "1.5.4",
+    label: "关系分析",
+    children: [
+      { value: "1.5.4.1", label: "邻接关系分析" },
+      { value: "1.5.4.2", label: "包含关系分析" },
+      { value: "1.5.4.3", label: "相交关系分析" },
+      { value: "1.5.4.4", label: "相离关系分析" },
+    ],
+  },
+  {
+    value: "1.5.5",
+    label: "非空间数据转换",
+    children: [{ value: "1.5.5.1", label: "非空间数据转空间数据" }],
+  },
+  {
+    value: "1.5.6",
+    label: "坐标转换",
+    children: [
+      { value: "1.5.6.1", label: "单点的坐标转换接口" },
+      { value: "1.5.6.2", label: "geojson坐标转换接口" },
+    ],
+  },
+  {
+    value: "1.5.7",
+    label: "时空数据格式转换",
+    children: [
+      { value: "1.5.7.1", label: "文件格式转换" },
+      { value: "1.5.7.2", label: "转换文件下载" },
+    ],
+  },
+  {
+    value: "1.5.8",
+    label: "自定义工具",
+    children: [{ value: "1.5.8.1", label: "obj三维模型轻量化" }],
+  },
+  {
+    value: "1.2.5",
+    label: "时空算子库",
+    children: [
+      { value: "1.2.5.1", label: "墨卡托计算距离面积" },
+      { value: "1.2.5.2", label: "设置障碍的路径计算" },
+      { value: "1.2.5.3", label: "点线面体的相互状态" },
+      { value: "1.2.5.4", label: "缓冲区计算" },
+      { value: "1.2.5.5", label: "线面体分割" },
+      { value: "1.2.5.6", label: "时空差异分析" },
+    ],
+  },
+];
+
+/** 去掉 label 前导编号,如 "1.5.2空间量算" → "空间量算" */
+export function stripWgnSceneNumberPrefix(label) {
+  if (!label || typeof label !== "string") return "";
+  return label.replace(/^\d+(?:\.\d+)*\s*/, "").trim();
+}

+ 494 - 310
src/views/Wgn.vue

@@ -4,323 +4,355 @@
   </el-affix>
   <el-affix :offset="44">
     <div class="application-overview">
-      <!-- 右侧主内容区 -->
-      <div class="main-content">
-        <div style="display: flex; justify-content: space-between; padding-right: 30px">
-          <!-- 搜索栏 -->
-          <div class="search-bar">
-            <el-input placeholder="输入搜索关键词" v-model="searchStr" class="search-input">
-            </el-input>
-            <el-button type="primary" class="search-btn" @click="searchServerList">搜索</el-button>
-            <!-- <span>搜索到{{total}}条微功能服务</span> -->
-          </div>
+      <div class="wgn-body">
+        <aside class="wgn-side-nav" aria-label="微功能锚点导航">
+          <button
+            v-for="block in groupedWgnSections"
+            :key="block.cat.value"
+            type="button"
+            class="wgn-anchor"
+            :class="{ active: activeAnchorId === block.cat.value }"
+            @click="scrollToAnchor(block.cat.value)"
+          >
+            {{ block.cat.label }}
+          </button>
+        </aside>
+        <!-- 右侧主内容区 -->
+        <div class="main-content">
+          <div style="display: flex; justify-content: space-between; padding-right: 30px">
+            <!-- 搜索栏 -->
+            <div class="search-bar">
+              <el-input
+                placeholder="输入搜索关键词"
+                v-model="searchStr"
+                class="search-input"
+                clearable
+                @clear="searchServerList"
+                @keyup.enter="searchServerList"
+              >
+              </el-input>
+              <el-button type="primary" class="search-btn" @click="searchServerList"
+                >搜索</el-button
+              >
+              <!-- <span>搜索到{{total}}条微功能服务</span> -->
+            </div>
 
-          <div>
-            <el-button
-              v-if="$getUserType() != 1"
-              type="primary"
-              class="search-btn"
-              @click="createTask"
-              >大批量数据处理任务</el-button
-            >
+            <div>
+              <el-button
+                v-if="$getUserType() != 1"
+                type="primary"
+                class="search-btn"
+                @click="createTask"
+                >大批量数据处理任务</el-button
+              >
+            </div>
           </div>
-        </div>
 
-        <!-- 应用卡片网格 -->
-        <div class="wgn-content">
-          <div class="applications-grid">
-            <div class="application-card" v-for="(item, index) in dmsServerList" :key="index">
-              <div class="card-image">
-                <img :src="curUrl + item.c_picture" :alt="item.title" />
-              </div>
-              <div class="card-content">
-                <div class="wgn-header">
-                  <h3 class="wgn-name">{{ item.title }}</h3>
-                  <!-- <span class="wgn-version">{{ item.version }}</span> -->
-                </div>
-                <!-- <div class="wgn-tags">
-                <el-tag size="small" type="success">{{ item.buffName }}</el-tag>
-                <el-tag size="small" v-for="tag in item.tags" :key="tag">{{ tag }}</el-tag>
-              </div> -->
-                <el-tooltip placement="top" effect="dark">
-                  <template #content>
-                    <div style="max-width: 500px">{{ item.content }}</div>
-                  </template>
-                  <p class="wgn-description">{{ item.content }}</p>
-                </el-tooltip>
-                <div class="wgn-footer">
-                  <!-- <span class="wgn-date">{{ item.createTime }}</span> -->
-                  <el-button
-                    type="primary"
-                    size="small"
-                    class="visit-button"
-                    @click="handleOnlineDemo(item)"
-                    >在线体验</el-button
-                  >
-                  <el-button
-                    type="primary"
-                    size="small"
-                    class="visit-button"
-                    @click="handleApply(item)"
-                    >申请使用</el-button
+          <!-- 应用卡片:按一级场景分组,锚点滚动 -->
+          <div class="wgn-content" ref="wgnScrollRoot">
+            <template v-if="groupedWgnSections.length">
+              <section
+                v-for="block in groupedWgnSections"
+                :key="block.cat.value"
+                :id="sectionDomId(block.cat.value)"
+                class="wgn-anchor-section"
+              >
+                <h2 class="wgn-anchor-section-title">{{ block.cat.label }}</h2>
+                <div class="applications-grid">
+                  <div
+                    class="application-card"
+                    v-for="(item, index) in block.items"
+                    :key="(item.c_scene_name || '') + '-' + index"
                   >
+                    <div class="card-image">
+                      <img :src="curUrl + item.c_picture" :alt="item.title" />
+                    </div>
+                    <div class="card-content">
+                      <div class="wgn-header">
+                        <h3 class="wgn-name">{{ item.title }}</h3>
+                      </div>
+                      <el-tooltip placement="top" effect="dark">
+                        <template #content>
+                          <div style="max-width: 500px">{{ item.content }}</div>
+                        </template>
+                        <p class="wgn-description">{{ item.content }}</p>
+                      </el-tooltip>
+                      <div class="wgn-footer">
+                        <el-button
+                          type="primary"
+                          size="small"
+                          class="visit-button"
+                          @click="handleOnlineDemo(item)"
+                          >在线体验</el-button
+                        >
+                        <el-button
+                          type="primary"
+                          size="small"
+                          class="visit-button"
+                          @click="handleApply(item)"
+                          >申请使用</el-button
+                        >
+                      </div>
+                    </div>
+                  </div>
                 </div>
-              </div>
-            </div>
+              </section>
+            </template>
+            <el-empty v-else description="暂无数据" />
           </div>
-        </div>
 
-        <!-- 申请使用微功能服务的表单弹窗 -->
-        <el-dialog
-          title="申请使用微功能服务"
-          v-model="showFrom"
-          width="60%"
-          :close-on-click-modal="false"
-          :close-on-press-escape="false"
-          :show-close="true"
-        >
-          <el-form :model="column.from" :rules="rules" ref="formRef" label-width="120px">
-            <el-form-item label="应用名称" prop="c_application_name">
-              <el-input v-model="column.from.c_application_name" placeholder="请输入应用名称" />
-            </el-form-item>
-            <el-form-item label="接口路径" prop="c_interface_path">
-              <el-input
-                v-model="column.from.c_interface_path"
-                disabled
-                placeholder="请输入接口路径"
-              />
-            </el-form-item>
-            <el-form-item label="联系电话" prop="c_phone">
-              <el-input v-model="column.from.c_phone" placeholder="请输入联系电话" />
-            </el-form-item>
-            <el-form-item label="单位名称" prop="c_unit_name">
-              <el-input v-model="column.from.c_unit_name" placeholder="请输入单位名称" />
-            </el-form-item>
-            <el-form-item label="部门名称" prop="c_department">
-              <el-input v-model="column.from.c_department" placeholder="请输入部门名称" />
-            </el-form-item>
-            <!-- 项目负责人 -->
-            <el-form-item label="项目负责人" prop="c_business_leader">
-              <el-input v-model="column.from.c_business_leader" placeholder="请输入项目负责人" />
-            </el-form-item>
-            <!-- 申请使用微功能服务的详细信息 -->
-            <el-form-item label="申请使用微功能服务的详细信息" prop="content">
-              <el-input
-                type="textarea"
-                v-model="column.from.content"
-                placeholder="请输入申请使用微功能服务的详细信息"
-              />
-            </el-form-item>
-          </el-form>
-          <template #footer>
-            <div class="dialog-footer">
-              <el-button @click="showFrom = false">取消</el-button>
-              <el-button type="primary" @click="handleApplySubmit">确定</el-button>
-            </div>
-          </template>
-        </el-dialog>
-        <!-- 创建任务的表单弹窗 -->
-        <el-dialog
-          title="创建任务"
-          v-model="showTaskFrom"
-          width="60%"
-          :close-on-click-modal="false"
-          :close-on-press-escape="false"
-          :show-close="true"
-        >
-          <el-form
-            :model="taskColumn.from"
-            :rules="taskColumn.rules"
-            ref="taskFormRef"
-            label-width="120px"
+          <!-- 申请使用微功能服务的表单弹窗 -->
+          <el-dialog
+            title="申请使用微功能服务"
+            v-model="showFrom"
+            width="60%"
+            :close-on-click-modal="false"
+            :close-on-press-escape="false"
+            :show-close="true"
           >
-            <el-form-item label="任务名称" prop="c_name">
-              <el-input v-model="taskColumn.from.c_name" placeholder="请输入任务名称" clearable>
-                <template #prepend>{{
-                  $moment(new Date()).format("YYMMDD_HHmm") +
-                  "_" +
-                  (taskColumn.from.c_type
-                    ? $getDmsTypes("yzt_task_type", taskColumn.from.c_type) + "_"
-                    : "")
-                }}</template>
-              </el-input>
-            </el-form-item>
-            <el-form-item label="任务备注" prop="c_comment">
-              <el-input
-                type="textarea"
-                clearable
-                v-model="taskColumn.from.c_comment"
-                placeholder="请输入任务备注"
-              />
-            </el-form-item>
-            <!-- 下拉框选择任务类型 -->
-            <el-form-item label="任务类型" prop="c_type">
-              <el-select
-                v-model="taskColumn.from.c_type"
-                @change="changeTaskType"
-                placeholder="请选择任务类型"
-              >
-                <el-option
-                  v-for="(name, index) in $store.state.DmsTypesMap['yzt_task_type']"
-                  :key="index"
-                  :label="name"
-                  :value="index"
+            <el-form :model="column.from" :rules="rules" ref="formRef" label-width="120px">
+              <el-form-item label="应用名称" prop="c_application_name">
+                <el-input v-model="column.from.c_application_name" placeholder="请输入应用名称" />
+              </el-form-item>
+              <el-form-item label="接口路径" prop="c_interface_path">
+                <el-input
+                  v-model="column.from.c_interface_path"
+                  disabled
+                  placeholder="请输入接口路径"
                 />
-              </el-select>
-            </el-form-item>
-            <!-- 渲染任务描述:仅查看 -->
-            <el-form-item
-              label="任务描述"
-              prop="apiDescription"
-              v-if="taskColumn.apiFrom.apiDescription"
-            >
-              <el-input type="textarea" disabled v-model="taskColumn.apiFrom.apiDescription" />
-            </el-form-item>
-            <!-- 任务文件是必须的 -->
-            <el-form-item label="任务文件" prop="c_source_file">
-              <el-input disabled v-model="taskColumn.from.c_source_file" v-show="false" />
-              <el-upload class="avatar-uploader" :http-request="handleTCUpload" :limit="1">
-                <el-button size="small" type="primary">点击上传</el-button>
-              </el-upload>
-            </el-form-item>
-            <el-divider />
-            <!-- 其他参数数据 -->
-            <el-form-item label="其他参数数据" prop="c_source_data" v-show="false">
-              <el-input
-                type="textarea"
-                clearable
-                v-model="taskColumn.from.c_source_data"
-                placeholder="请输入其他参数数据"
-              />
-            </el-form-item>
-            <div
-              v-if="
-                taskColumn.apiFrom.apiParameterRules &&
-                includesKey([
-                  'lonKey',
-                  'latKey',
-                  'outFileType',
-                  'inPrj',
-                  'outPrj',
-                  'compressionRatio',
-                  'keyType',
-                  'geocodeColumnKeyword',
-                ])
-              "
+              </el-form-item>
+              <el-form-item label="联系电话" prop="c_phone">
+                <el-input v-model="column.from.c_phone" placeholder="请输入联系电话" />
+              </el-form-item>
+              <el-form-item label="单位名称" prop="c_unit_name">
+                <el-input v-model="column.from.c_unit_name" placeholder="请输入单位名称" />
+              </el-form-item>
+              <el-form-item label="部门名称" prop="c_department">
+                <el-input v-model="column.from.c_department" placeholder="请输入部门名称" />
+              </el-form-item>
+              <!-- 项目负责人 -->
+              <el-form-item label="项目负责人" prop="c_business_leader">
+                <el-input v-model="column.from.c_business_leader" placeholder="请输入项目负责人" />
+              </el-form-item>
+              <!-- 申请使用微功能服务的详细信息 -->
+              <el-form-item label="申请使用微功能服务的详细信息" prop="content">
+                <el-input
+                  type="textarea"
+                  v-model="column.from.content"
+                  placeholder="请输入申请使用微功能服务的详细信息"
+                />
+              </el-form-item>
+            </el-form>
+            <template #footer>
+              <div class="dialog-footer">
+                <el-button @click="showFrom = false">取消</el-button>
+                <el-button type="primary" @click="handleApplySubmit">确定</el-button>
+              </div>
+            </template>
+          </el-dialog>
+          <!-- 创建任务的表单弹窗 -->
+          <el-dialog
+            title="创建任务"
+            v-model="showTaskFrom"
+            width="60%"
+            :close-on-click-modal="false"
+            :close-on-press-escape="false"
+            :show-close="true"
+          >
+            <el-form
+              :model="taskColumn.from"
+              :rules="taskColumn.rules"
+              ref="taskFormRef"
+              label-width="120px"
             >
-              <template
-                v-for="(rulesItem, index) in taskColumn.apiFrom.apiParameterRules"
-                :key="index"
-              >
-                <el-form-item
-                  v-if="
-                    [
-                      'lonKey',
-                      'latKey',
-                      'outFileType',
-                      'inPrj',
-                      'outPrj',
-                      'compressionRatio',
-                      'keyType',
-                      'geocodeColumnKeyword',
-                    ].includes(rulesItem)
-                  "
-                  :label="
-                    rulesItem === 'keyType'
-                      ? '类型字段'
-                      : rulesItem === 'geocodeColumnKeyword'
-                        ? '数据列名'
-                        : rulesItem
-                  "
+              <el-form-item label="任务名称" prop="c_name">
+                <el-input v-model="taskColumn.from.c_name" placeholder="请输入任务名称" clearable>
+                  <template #prepend>{{
+                    $moment(new Date()).format("YYMMDD_HHmm") +
+                    "_" +
+                    (taskColumn.from.c_type
+                      ? $getDmsTypes("yzt_task_type", taskColumn.from.c_type) + "_"
+                      : "")
+                  }}</template>
+                </el-input>
+              </el-form-item>
+              <el-form-item label="任务备注" prop="c_comment">
+                <el-input
+                  type="textarea"
+                  clearable
+                  v-model="taskColumn.from.c_comment"
+                  placeholder="请输入任务备注"
+                />
+              </el-form-item>
+              <!-- 下拉框选择任务类型 -->
+              <el-form-item label="任务类型" prop="c_type">
+                <el-select
+                  v-model="taskColumn.from.c_type"
+                  @change="changeTaskType"
+                  placeholder="请选择任务类型"
                 >
-                  <el-input
-                    v-if="['lonKey', 'latKey'].includes(rulesItem)"
-                    v-model="taskColumn.from.c_source_data[rulesItem]"
-                    placeholder="当文件为xlsx时必填"
+                  <el-option
+                    v-for="(name, index) in $store.state.DmsTypesMap['yzt_task_type']"
+                    :key="index"
+                    :label="name"
+                    :value="index"
                   />
-                  <el-select
+                </el-select>
+              </el-form-item>
+              <!-- 渲染任务描述:仅查看 -->
+              <el-form-item
+                label="任务描述"
+                prop="apiDescription"
+                v-if="taskColumn.apiFrom.apiDescription"
+              >
+                <el-input type="textarea" disabled v-model="taskColumn.apiFrom.apiDescription" />
+              </el-form-item>
+              <!-- 任务文件是必须的 -->
+              <el-form-item label="任务文件" prop="c_source_file">
+                <el-input disabled v-model="taskColumn.from.c_source_file" v-show="false" />
+                <el-upload class="avatar-uploader" :http-request="handleTCUpload" :limit="1">
+                  <el-button size="small" type="primary">点击上传</el-button>
+                </el-upload>
+              </el-form-item>
+              <el-divider />
+              <!-- 其他参数数据 -->
+              <el-form-item label="其他参数数据" prop="c_source_data" v-show="false">
+                <el-input
+                  type="textarea"
+                  clearable
+                  v-model="taskColumn.from.c_source_data"
+                  placeholder="请输入其他参数数据"
+                />
+              </el-form-item>
+              <div
+                v-if="
+                  taskColumn.apiFrom.apiParameterRules &&
+                  includesKey([
+                    'lonKey',
+                    'latKey',
+                    'outFileType',
+                    'inPrj',
+                    'outPrj',
+                    'compressionRatio',
+                    'keyType',
+                    'geocodeColumnKeyword',
+                  ])
+                "
+              >
+                <template
+                  v-for="(rulesItem, index) in taskColumn.apiFrom.apiParameterRules"
+                  :key="index"
+                >
+                  <el-form-item
                     v-if="
-                      ['outFileType', 'inPrj', 'outPrj', 'compressionRatio'].includes(rulesItem)
+                      [
+                        'lonKey',
+                        'latKey',
+                        'outFileType',
+                        'inPrj',
+                        'outPrj',
+                        'compressionRatio',
+                        'keyType',
+                        'geocodeColumnKeyword',
+                      ].includes(rulesItem)
+                    "
+                    :label="
+                      rulesItem === 'keyType'
+                        ? '类型字段'
+                        : rulesItem === 'geocodeColumnKeyword'
+                          ? '数据列名'
+                          : rulesItem
                     "
-                    v-model="taskColumn.from.c_source_data[rulesItem]"
-                    :placeholder="'请选择' + rulesItem"
                   >
-                    <el-option
-                      v-for="item in taskColumn.selectOptions[rulesItem]"
-                      :key="'wgn-task-selectOptions-form-' + item.label"
-                      :label="item.label"
-                      :value="item.value"
+                    <el-input
+                      v-if="['lonKey', 'latKey'].includes(rulesItem)"
+                      v-model="taskColumn.from.c_source_data[rulesItem]"
+                      placeholder="当文件为xlsx时必填"
                     />
-                  </el-select>
-                  <el-select
-                    v-if="rulesItem === 'keyType'"
-                    v-model="taskColumn.from.c_source_data.keyType"
-                    placeholder="非空转空时选择地物类型"
-                    :clearable="!isGeocodingTaskKeyTypeReadonly()"
-                    filterable
-                    :disabled="isGeocodingTaskKeyTypeReadonly()"
-                  >
-                    <el-option
-                      v-for="item in taskColumn.selectOptions.keyType"
-                      :key="'wgn-task-keyType-' + item.value"
-                      :label="item.label"
-                      :value="item.value"
+                    <el-select
+                      v-if="
+                        ['outFileType', 'inPrj', 'outPrj', 'compressionRatio'].includes(rulesItem)
+                      "
+                      v-model="taskColumn.from.c_source_data[rulesItem]"
+                      :placeholder="'请选择' + rulesItem"
+                    >
+                      <el-option
+                        v-for="item in taskColumn.selectOptions[rulesItem]"
+                        :key="'wgn-task-selectOptions-form-' + item.label"
+                        :label="item.label"
+                        :value="item.value"
+                      />
+                    </el-select>
+                    <el-select
+                      v-if="rulesItem === 'keyType'"
+                      v-model="taskColumn.from.c_source_data.keyType"
+                      placeholder="非空转空时选择地物类型"
+                      :clearable="!isGeocodingTaskKeyTypeReadonly()"
+                      filterable
+                      :disabled="isGeocodingTaskKeyTypeReadonly()"
+                    >
+                      <el-option
+                        v-for="item in taskColumn.selectOptions.keyType"
+                        :key="'wgn-task-keyType-' + item.value"
+                        :label="item.label"
+                        :value="item.value"
+                      />
+                    </el-select>
+                    <el-input
+                      v-if="rulesItem === 'geocodeColumnKeyword' && isTaskGeocodeZipForColumnInput"
+                      v-model="taskColumn.from.c_source_data.geocodeColumnKeyword"
+                      placeholder="zip 内含多个表格时请手动输入列名(与各表表头一致)"
+                      clearable
                     />
-                  </el-select>
-                  <el-input
-                    v-if="rulesItem === 'geocodeColumnKeyword' && isTaskGeocodeZipForColumnInput"
-                    v-model="taskColumn.from.c_source_data.geocodeColumnKeyword"
-                    placeholder="zip 内含多个表格时请手动输入列名(与各表表头一致)"
-                    clearable
+                    <el-select
+                      v-if="rulesItem === 'geocodeColumnKeyword' && !isTaskGeocodeZipForColumnInput"
+                      v-model="taskColumn.from.c_source_data.geocodeColumnKeyword"
+                      placeholder="请先上传 CSV / XLSX,将自动读取表头"
+                      clearable
+                      filterable
+                      :disabled="
+                        !taskColumn.selectOptions.geocodeColumnKeyword ||
+                        !taskColumn.selectOptions.geocodeColumnKeyword.length
+                      "
+                    >
+                      <el-option
+                        v-for="item in taskColumn.selectOptions.geocodeColumnKeyword"
+                        :key="'wgn-task-geocol-' + item.value"
+                        :label="item.label"
+                        :value="item.value"
+                      />
+                    </el-select>
+                  </el-form-item>
+                </template>
+              </div>
+              <!-- 任务状态默认为初始化 -->
+              <el-form-item label="任务状态" prop="c_state" v-show="false">
+                <el-select disabled v-model="taskColumn.from.c_state" placeholder="请选择任务状态">
+                  <el-option
+                    v-for="(name, index) in $store.state.DmsTypesMap['task_status']"
+                    :key="'wgn-task-form-' + index"
+                    :label="name"
+                    :value="index"
                   />
-                  <el-select
-                    v-if="rulesItem === 'geocodeColumnKeyword' && !isTaskGeocodeZipForColumnInput"
-                    v-model="taskColumn.from.c_source_data.geocodeColumnKeyword"
-                    placeholder="请先上传 CSV / XLSX,将自动读取表头"
-                    clearable
-                    filterable
-                    :disabled="
-                      !taskColumn.selectOptions.geocodeColumnKeyword ||
-                      !taskColumn.selectOptions.geocodeColumnKeyword.length
-                    "
-                  >
-                    <el-option
-                      v-for="item in taskColumn.selectOptions.geocodeColumnKeyword"
-                      :key="'wgn-task-geocol-' + item.value"
-                      :label="item.label"
-                      :value="item.value"
-                    />
-                  </el-select>
-                </el-form-item>
-              </template>
-            </div>
-            <!-- 任务状态默认为初始化 -->
-            <el-form-item label="任务状态" prop="c_state" v-show="false">
-              <el-select disabled v-model="taskColumn.from.c_state" placeholder="请选择任务状态">
-                <el-option
-                  v-for="(name, index) in $store.state.DmsTypesMap['task_status']"
-                  :key="'wgn-task-form-' + index"
-                  :label="name"
-                  :value="index"
-                />
-              </el-select>
-            </el-form-item>
-            <!-- 任务负责人默认为当前登录用户 -->
-            <el-form-item label="用户id" prop="c_user_id" v-show="false">
-              <el-input disabled v-model="taskColumn.from.c_user_id" />
-            </el-form-item>
-            <!-- 任务负责人默认为当前登录用户 -->
-            <el-form-item label="用户名" prop="c_user_name" v-show="false">
-              <el-input disabled v-model="taskColumn.from.c_user_name" />
-            </el-form-item>
-          </el-form>
-          <template #footer>
-            <div class="dialog-footer">
-              <el-button @click="showTaskFrom = false">取消</el-button>
-              <el-button type="primary" @click="handleTaskSubmit">确定</el-button>
-            </div>
-          </template>
-        </el-dialog>
+                </el-select>
+              </el-form-item>
+              <!-- 任务负责人默认为当前登录用户 -->
+              <el-form-item label="用户id" prop="c_user_id" v-show="false">
+                <el-input disabled v-model="taskColumn.from.c_user_id" />
+              </el-form-item>
+              <!-- 任务负责人默认为当前登录用户 -->
+              <el-form-item label="用户名" prop="c_user_name" v-show="false">
+                <el-input disabled v-model="taskColumn.from.c_user_name" />
+              </el-form-item>
+            </el-form>
+            <template #footer>
+              <div class="dialog-footer">
+                <el-button @click="showTaskFrom = false">取消</el-button>
+                <el-button type="primary" @click="handleTaskSubmit">确定</el-button>
+              </div>
+            </template>
+          </el-dialog>
+        </div>
       </div>
     </div>
   </el-affix>
@@ -331,6 +363,7 @@ import { ElNotification } from "element-plus";
 import moment from "moment";
 import * as XLSX from "xlsx";
 import api from "@/api/content";
+import { WGN_SCENE_LIST } from "@/data/wgnSceneList";
 export default {
   name: "ApplicationOverview",
   data() {
@@ -341,6 +374,9 @@ export default {
       searchStr: "",
       // 微功能服务列表
       dmsServerList: [],
+      sceneMenuTree: WGN_SCENE_LIST,
+      activeAnchorId: null,
+      anchorScrollLock: false,
       // 是否显示申请使用微功能服务的表单弹窗
       showFrom: false,
       // 是否显示创建任务的表单弹窗
@@ -532,6 +568,17 @@ export default {
     };
   },
   computed: {
+    /** 按一级场景分组,仅保留当前搜索结果下仍有卡片的分类 */
+    groupedWgnSections() {
+      const list = this.dmsServerList || [];
+      return this.sceneMenuTree
+        .map((cat) => {
+          const codes = new Set((cat.children || []).map((ch) => ch.value));
+          const items = list.filter((i) => codes.has(i.c_scene_name));
+          return { cat, items };
+        })
+        .filter((g) => g.items.length > 0);
+    },
     isTaskGeocodeZipForColumnInput() {
       const n =
         (this.taskUploadRawFile && this.taskUploadRawFile.name) ||
@@ -540,6 +587,22 @@ export default {
       return String(n).toLowerCase().endsWith(".zip");
     },
   },
+  watch: {
+    groupedWgnSections: {
+      handler(blocks) {
+        this.$nextTick(() => {
+          if (blocks.length) {
+            if (!blocks.some((b) => b.cat.value === this.activeAnchorId)) {
+              this.activeAnchorId = blocks[0].cat.value;
+            }
+          } else {
+            this.activeAnchorId = null;
+          }
+          this.bindAnchorSpy();
+        });
+      },
+    },
+  },
   mounted() {
     // 任务负责人默认为当前登录用户
     this.taskColumn.from.c_user_id = this.$store.state.userInfo.id;
@@ -547,7 +610,61 @@ export default {
     this.taskColumn.from.c_user_name = this.$store.state.userInfo.username;
     this.searchServerList();
   },
+  beforeUnmount() {
+    this.unbindAnchorSpy();
+  },
   methods: {
+    sectionDomId(catValue) {
+      return "wgn-section-" + String(catValue).replace(/\./g, "-");
+    },
+    scrollToAnchor(catValue) {
+      const root = this.$refs.wgnScrollRoot;
+      const el = document.getElementById(this.sectionDomId(catValue));
+      if (!root || !el) return;
+      this.anchorScrollLock = true;
+      this.activeAnchorId = catValue;
+      const rootRect = root.getBoundingClientRect();
+      const elRect = el.getBoundingClientRect();
+      const nextTop = elRect.top - rootRect.top + root.scrollTop - 8;
+      root.scrollTo({ top: Math.max(0, nextTop), behavior: "smooth" });
+      window.setTimeout(() => {
+        this.anchorScrollLock = false;
+        this.updateActiveAnchorFromScroll();
+      }, 480);
+    },
+    unbindAnchorSpy() {
+      const root = this.$refs.wgnScrollRoot;
+      if (root && this._wgnOnScroll) {
+        root.removeEventListener("scroll", this._wgnOnScroll);
+      }
+      this._wgnOnScroll = null;
+    },
+    bindAnchorSpy() {
+      this.unbindAnchorSpy();
+      const root = this.$refs.wgnScrollRoot;
+      if (!root) return;
+      this._wgnOnScroll = () => this.updateActiveAnchorFromScroll();
+      root.addEventListener("scroll", this._wgnOnScroll, { passive: true });
+      this.updateActiveAnchorFromScroll();
+    },
+    updateActiveAnchorFromScroll() {
+      if (this.anchorScrollLock) return;
+      const root = this.$refs.wgnScrollRoot;
+      const blocks = this.groupedWgnSections;
+      if (!root || !blocks.length) return;
+      const rootRect = root.getBoundingClientRect();
+      const threshold = 28;
+      let current = blocks[0].cat.value;
+      for (const { cat } of blocks) {
+        const el = document.getElementById(this.sectionDomId(cat.value));
+        if (!el) continue;
+        const top = el.getBoundingClientRect().top - rootRect.top;
+        if (top <= threshold) {
+          current = cat.value;
+        }
+      }
+      this.activeAnchorId = current;
+    },
     isGeocodingTaskKeyTypeReadonly() {
       const label = this.$getDmsTypes("yzt_task_type", this.taskColumn.from.c_type);
       return label === "文件地名地址查询";
@@ -644,11 +761,7 @@ export default {
               // 接口路径
               this.taskColumn.apiFrom.apiUrl = apiInfo.c_url;
               const pr = this.taskColumn.apiFrom.apiParameterRules;
-              if (
-                pr &&
-                pr.includes("geocodeColumnKeyword") &&
-                this.taskUploadRawFile
-              ) {
+              if (pr && pr.includes("geocodeColumnKeyword") && this.taskUploadRawFile) {
                 this.refreshTaskGeocodeColumns(this.taskUploadRawFile);
               }
               this.syncKeyTypeForGeocodingTask();
@@ -783,7 +896,7 @@ export default {
         routerPath = {
           // path: item.c_url,
           path: "sksj",
-        }
+        };
         // this.$router.push(routerPath);
         window.open(routerPath.path, "_blank");
       } else {
@@ -824,22 +937,33 @@ export default {
           .then((res) => {
             if (res.code === 200) {
               // 过滤掉自定义工具微功能服务1.5.8.1
-              this.dmsServerList = res.content.data.filter(
-                (item) => item.c_scene_name != "1.5.8.1"
-              );
+              // this.dmsServerList = res.content.data.filter(
+              //   (item) => item.c_scene_name != "1.5.8.1"
+              // );
               // 转换时间格式
-              this.dmsServerList = this.dmsServerList.map((item) => ({
+              this.dmsServerList = res.content.data.map((item) => ({
                 ...item,
                 createTime: moment(item.create_time).format("YYYY-MM-DD HH:mm:ss"),
               }));
               this.total = this.dmsServerList.length;
+              if (this.total === 0) {
+                this.$message({
+                  message: "暂无数据",
+                  type: "info",
+                });
+              }
               // ElNotification.success({
               //   title: "成功",
               //   message: "搜索到" + this.dmsServerList.length + "条微功能服务",
               //   offset: 80,
               // });
             } else {
+              this.dmsServerList = [];
               this.total = 0;
+              this.$message({
+                message: "暂无数据",
+                type: "info",
+              });
               // ElNotification.error({
               //   title: "失败",
               //   message: "搜索到0条微功能服务",
@@ -969,10 +1093,70 @@ export default {
   color: #ffffff;
 }
 
+.wgn-body {
+  display: flex;
+  align-items: flex-start;
+  width: 100%;
+  box-sizing: border-box;
+  min-height: calc(100vh - 44px);
+}
+
+.wgn-side-nav {
+  flex: 0 0 200px;
+  padding: 12px 12px 24px 24px;
+  background: transparent;
+  border: none;
+  position: sticky;
+  top: 52px;
+  align-self: flex-start;
+  max-height: calc(100vh - 56px);
+  overflow-y: auto;
+}
+
+.wgn-anchor {
+  display: block;
+  width: 100%;
+  padding: 10px 4px 10px 0;
+  margin: 0 0 2px;
+  border: none;
+  background: transparent;
+  color: rgba(255, 255, 255, 0.85);
+  font-size: 16px;
+  text-align: left;
+  cursor: pointer;
+  border-radius: 4px;
+}
+
+.wgn-anchor:hover {
+  color: #ffffff;
+}
+
+.wgn-anchor.active {
+  color: #69b1ff;
+  font-weight: 600;
+}
+
+.wgn-anchor-section {
+  scroll-margin-top: 12px;
+}
+
+.wgn-anchor-section + .wgn-anchor-section {
+  margin-top: 36px;
+}
+
+.wgn-anchor-section-title {
+  margin: 0 0 18px;
+  font-size: 20px;
+  font-weight: 600;
+  color: #ffffff;
+  letter-spacing: 0.02em;
+}
+
 /* 右侧主内容区 */
 .main-content {
   flex: 1;
-  padding: 20px 100px;
+  min-width: 0;
+  padding: 20px 40px 20px 8px;
   overflow-y: auto;
 }
 

+ 15 - 9
src/views/WgnLast.vue

@@ -53,6 +53,8 @@
             clearable
             size="large"
             @change="searchServerList()"
+            @clear="searchServerList()"
+            @keyup.enter="searchServerList()"
           >
             <template #append>
               <el-button icon="Search" @click="searchServerList()" />
@@ -60,7 +62,7 @@
           </el-input>
         </div>
       </el-affix>
-      <div class="server_list_box_table" v-if="dmsServerList">
+      <div class="server_list_box_table" v-if="dmsServerList && dmsServerList.length">
         <div
           v-for="item in dmsServerList"
           :key="item.c_scene_name"
@@ -94,6 +96,7 @@
           </div>
         </div>
       </div>
+      <el-empty v-else description="暂无数据" />
       <!-- 申请使用微功能服务的表单弹窗 -->
       <el-dialog
         title="申请使用微功能服务"
@@ -794,15 +797,18 @@ export default {
           .then((res) => {
             if (res.code === 200) {
               this.dmsServerList = res.content.data;
-              ElNotification.success({
-                title: "成功",
-                message: "搜索到" + this.dmsServerList.length + "条微功能服务",
-                offset: 80,
-              });
+              if (!this.dmsServerList.length) {
+                ElNotification.info({
+                  title: "提示",
+                  message: "暂无数据",
+                  offset: 80,
+                });
+              }
             } else {
-              ElNotification.error({
-                title: "失败",
-                message: "搜索到0条微功能服务",
+              this.dmsServerList = [];
+              ElNotification.info({
+                title: "提示",
+                message: "暂无数据",
                 offset: 80,
               });
             }

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff