Browse Source

智能选址客户端使用sse接受

liutao 1 month ago
parent
commit
843a38a645

+ 0 - 1
jiaxing/landsite_agent_jx/xuanzhi_query.py

@@ -7,7 +7,6 @@ from database import Database
 router = APIRouter()
 database = Database()
 
-
 @router.get("/xzgyyd-query")
 async def klyzy_query(id: str = Query(..., description="ID列表, 逗号分隔")) -> List[Any]:
     """

+ 250 - 162
web_ui/src/views/industrial-land/AiIndustriaLandDetail.vue

@@ -126,13 +126,21 @@
                   <span class="txt">你可以试着这样问我:</span>
                 </p>
                 <ul>
-                  <li v-for="(item, index) in kws" :key="index" @click="hotQuestionReset(item['kw'], (index+1).toString())">
+                  <li
+                    v-for="(item, index) in kws"
+                    :key="index"
+                    @click="
+                      hotQuestionReset(item['kw'], (index + 1).toString())
+                    "
+                  >
                     <div class="left_box">
                       <span class="desc">
-                        <span class="txt">{{ item['kw'] }}</span>
+                        <span class="txt">{{ item["kw"] }}</span>
                       </span>
-                      </div>
-                    <span class="icon right_icon"><Icon type="ios-arrow-forward" /></span>
+                    </div>
+                    <span class="icon right_icon">
+                      <Icon type="ios-arrow-forward" />
+                    </span>
                   </li>
                 </ul>
               </div>
@@ -304,8 +312,17 @@
                 >
               </Select>
             </div>
-            <AIBtn v-if="!btnSendDisabled" @send="zwsend" :disabled="btnSendDisabled" />
-            <div v-else class="stop_send_btn" title="停止" @click="onStopSendHandle"></div>
+            <AIBtn
+              v-if="!btnSendDisabled"
+              @send="zwsend"
+              :disabled="btnSendDisabled"
+            />
+            <div
+              v-else
+              class="stop_send_btn"
+              title="停止"
+              @click="onStopSendHandle"
+            ></div>
           </div>
         </vue-perfect-scrollbar>
         <znxz-detail
@@ -385,7 +402,7 @@ import landMethods from "@/api/land";
 import AIBtn from "@/components/AIBtn.vue";
 import ALoading from "@/components/ALoading.vue";
 
-const _timers = []
+const _timers = [];
 export default {
   components: {
     industrialTop,
@@ -403,17 +420,17 @@ export default {
     return {
       kws: [
         {
-          kw: '请在萧山区推荐3宗30-100亩左右的工业用地',
+          kw: "请在萧山区推荐3宗30-100亩左右的工业用地",
         },
         {
-          kw: '请在萧山区找出面积最大的商业用地',
+          kw: "请在萧山区找出面积最大的商业用地",
         },
         {
-          kw: '请在萧山机场附近推荐3宗50亩内的工业用地',
-        }
+          kw: "请在萧山机场附近推荐3宗50亩内的工业用地",
+        },
       ],
       geoLayer: null,
-      analysisGeoLayer:null,
+      analysisGeoLayer: null,
       nearMarkerList: [],
       radarScanStyle: {
         width: "100%",
@@ -521,7 +538,6 @@ export default {
       this.initMap();
 
       // this.testStubXgdk();
-
     });
   },
   created() {
@@ -533,12 +549,12 @@ export default {
   },
 
   methods: {
-    onBackHandle () {
+    onBackHandle() {
       this.showResult = this.showQuery = this.loading = false;
-      this.hzData = []
-      this.listType = ''
-      this.inputText = ''
-      this.hintText = '思考中...'
+      this.hzData = [];
+      this.listType = "";
+      this.inputText = "";
+      this.hintText = "思考中...";
     },
     dSourceChange() {
       if (this.dSource === "3") {
@@ -587,10 +603,10 @@ export default {
       }
       this.analysisGeoLayer = L.geoJSON(geojsonList, {
         style: function (feature) {
-          return { color: "yellow",fillOpacity:0.6 ,weight:2};
+          return { color: "yellow", fillOpacity: 0.6, weight: 2 };
         },
       });
-      if(!geojsonList || !geojsonList.length){
+      if (!geojsonList || !geojsonList.length) {
         return;
       }
       this.analysisGeoLayer.addTo(this.map);
@@ -629,7 +645,7 @@ export default {
 
       if (this.ctrlAbout) {
         this.ctrlAbout.abort();
-        this.ctrlAbout = null
+        this.ctrlAbout = null;
       }
       this.loading = false;
       if (this.interval) {
@@ -638,11 +654,11 @@ export default {
 
       this.inputText = "";
     },
-    onStopSendHandle () {
-      console.log(this.ctrlAbout)
+    onStopSendHandle() {
+      console.log(this.ctrlAbout);
       if (this.ctrlAbout) {
         this.ctrlAbout.abort();
-        this.ctrlAbout = null
+        this.ctrlAbout = null;
         this.btnSendDisabled = false;
       }
     },
@@ -742,8 +758,8 @@ export default {
       }
     },
     resetAnswer() {
-        this.input = this.inputText;
-        this.questionModal = true;
+      this.input = this.inputText;
+      this.questionModal = true;
     },
 
     hideModal() {
@@ -1056,10 +1072,10 @@ export default {
     getDetailByIdList(idList, func) {
       // 参数判空处理
       if (!Array.isArray(idList) || idList.length === 0) {
-        if (typeof func === 'function') func([]);
+        if (typeof func === "function") func([]);
         return;
       }
-      if (typeof func !== 'function') {
+      if (typeof func !== "function") {
         // 如果回调不是函数,直接返回
         return;
       }
@@ -1076,12 +1092,12 @@ export default {
         }
         const result = res.map((ele) => {
           let viewObj = {};
-          if (type === 'ggdk') {
+          if (type === "ggdk") {
             viewObj = {
               id: ele.id,
               name: ele.dkmc,
               tdyt: ele.tdyt,
-              dkmj: ele.dkmj ? ele.dkmj.toFixed(2) : '',
+              dkmj: ele.dkmj ? ele.dkmj.toFixed(2) : "",
               center_wkt: ele.center_wkt,
               geom: ele.geom,
               wzxx: "",
@@ -1098,12 +1114,15 @@ export default {
                 // wkt 解析失败
               }
             }
-          } else if (type === 'kg') {
+          } else if (type === "kg") {
             viewObj = {
               id: ele.id,
               name: ele.ydxz,
-              tdyt: ele.ydxz && ele.yddm ? `${ele.ydxz}(${ele.yddm})` : ele.ydxz || '',
-              dkmj: ele.pfmarea ? (ele.pfmarea * 0.0015).toFixed(2) : '',
+              tdyt:
+                ele.ydxz && ele.yddm
+                  ? `${ele.ydxz}(${ele.yddm})`
+                  : ele.ydxz || "",
+              dkmj: ele.pfmarea ? (ele.pfmarea * 0.0015).toFixed(2) : "",
               center_wkt: ele.center_wkt,
               geom: ele.geom,
               wzxx: "",
@@ -1133,7 +1152,7 @@ export default {
           func([]);
           return;
         }
-        this.klyzyQuery(ids, (res) => processResult(res, 'ggdk'));
+        this.klyzyQuery(ids, (res) => processResult(res, "ggdk"));
       } else if (isKg) {
         // 控制性详细规划
         const ids = idList.map((ele) => ele && ele.id).filter(Boolean);
@@ -1141,7 +1160,7 @@ export default {
           func([]);
           return;
         }
-        this.kgQuery(ids, (res) => processResult(res, 'kg'));
+        this.kgQuery(ids, (res) => processResult(res, "kg"));
       } else {
         // 其他类型暂不处理
         func([]);
@@ -1747,7 +1766,7 @@ export default {
       let that = this;
       this.showResult = false;
       this.zhuiwen = false;
-
+      this.hzData=[];
       // Clean up existing connections and resources
       if (this.eventSource) {
         this.eventSource.close();
@@ -1771,7 +1790,10 @@ export default {
       var question = this.wt.trim();
 
       // Add data source context if needed
-      if (question.indexOf("控制性详细规划") == -1 && question.indexOf("公告地块") == -1) {
+      if (
+        question.indexOf("控制性详细规划") == -1 &&
+        question.indexOf("公告地块") == -1
+      ) {
         if (this.dSource == "1") {
           question += ",数据表是控制性详细规划";
         } else if (this.dSource == "2") {
@@ -1800,28 +1822,127 @@ export default {
 
       // 每次请求都新建 AbortController,避免 signal 复用导致无法再次请求
       this.ctrlAbout = new AbortController();
-
+      let sqlGenIndex = -1;
+      let sqlGenSummary = "";
       try {
-        const response = await fetch(window.ApplicationConfig.landAnalysisUrl, {
-          method: 'POST',
+        await fetchEventSource(window.ApplicationConfig.landAnalysisUrl, {
+          method: "POST",
           headers: {
-            'Content-Type': 'application/json',
+            "Content-Type": "application/json",
           },
           body: JSON.stringify({
-            description: question
+            description: question,
           }),
-          signal: this.ctrlAbout.signal
+          signal: this.ctrlAbout.signal,
+          onopen(response) {
+            if (response.ok && response.status === 200) {
+              console.log("SSE connection established");
+            } else {
+              throw new Error(
+                `Failed to open SSE connection: ${response.status} ${response.statusText}`
+              );
+            }
+          },
+          onmessage(event) {
+            try {
+              console.log(event.data);
+              const data = JSON.parse(event.data);
+              let contentToAdd = "";
+              switch (data.type) {
+                case "sql_generation":
+                  contentToAdd =
+                    typeof data.content === "string"
+                      ? data.content
+                      : JSON.stringify(data.content);
+                  if (sqlGenIndex === -1) {
+                    sqlGenSummary = contentToAdd;
+                    that.hzData.push({
+                      type: "sql_generation",
+                      summary: sqlGenSummary,
+                    });
+                    sqlGenIndex = that.hzData.length-1;
+                  } else {
+                    sqlGenSummary += contentToAdd;
+                    that.hzData[sqlGenIndex].summary = sqlGenSummary;
+                  }
+                  
+                  break;
+                case "result":
+                  if (data.data && data.data) {
+                    try {
+                      that.getDetailByIdList(
+                        data.data,
+                        (detailData) => {
+                          if (
+                            Array.isArray(detailData) &&
+                            detailData.length > 0
+                          ) {
+                            that.clearMark();
+                            that.addGdLayer(detailData);
+                            that.xgdk = detailData;
+                            that.showXgdkCenterPoint();
+                            that.mContentVisabled = true;
+                            that.xgdk.forEach((item) => {
+                              item["show"] = true;
+                            });
+                          } else {
+                            that.clearMark();
+                            that.xgdk = [];
+                            that.mContentVisabled = false;
+                          }
+                        }
+                      );
+                    } catch (e) {
+                      console.error("Error processing result data:", e);
+                    }
+                  }
+                  break;
+                case "end":
+                  contentToAdd =
+                    typeof data.content === "string"
+                      ? data.content
+                      : JSON.stringify(data.content);
+                  that.hzData.push({ type: "end", summary: contentToAdd });
+                  that.loading = false;
+                  that.showResult = true;
+                  that.radarShow = false;
+                  that.activeIndex = that.steps.length - 1;
+                  that.btnSendDisabled = false;
+                  break;
+              }
+
+              // 使用 requestAnimationFrame 来优化滚动性能
+              let timer = setTimeout(() => {
+                that.scrollToBottom();
+                clearTimeout(timer);
+                timer = null;
+              }, 200);
+            } catch (e) {
+              console.error("Error processing SSE message:", e);
+            }
+          },
+          onerror(err) {
+            if (err.name === "AbortError") {
+              // 请求被取消,静默处理
+              return;
+            }
+            console.error("SSE connection error:", err);
+            this.checkTimeOut();
+          },
+          onclose() {
+            console.log("SSE connection closed");
+          },
         });
-        await this.handleStreamResponse(response);
+
         const timer = setTimeout(async () => {
-          _timers.push(timer)
-        }, _timers.length * 15)
+          _timers.push(timer);
+        }, _timers.length * 15);
       } catch (err) {
-        if (err.name === 'AbortError') {
+        if (err.name === "AbortError") {
           // 请求被取消,静默处理
           return;
         }
-        console.error('Request error:', err);
+        console.error("Request error:", err);
         this.checkTimeOut();
       } finally {
         this.loading = false;
@@ -1831,100 +1952,8 @@ export default {
         }
       }
     }, 500),
-    async handleStreamResponse(response) {
-      const reader = response.body.getReader();
-      const decoder = new TextDecoder();
-
-      this.hzData = [];
-      let sqlGenSummary = '';
-      let sqlGenIndex = -1;
-
-      while (true) {
-        const { value, done } = await reader.read();
-        if (done) break;
-
-        const chunk = decoder.decode(value);
-        const lines = chunk.split('\n').filter(line => line.trim());
-        for (const line of lines) {
-          try {
-            await this.sleep(30)
-            const data = JSON.parse(line);
-            let contentToAdd = '';
-            switch (data.type) {
-              case 'sql_generation':
-                contentToAdd = typeof data.content === 'string' ? data.content : JSON.stringify(data.content);
-                if (sqlGenIndex === -1) {
-                  sqlGenSummary = contentToAdd;
-                  this.hzData.push({ type: 'sql_generation', summary: sqlGenSummary });
-                  sqlGenIndex = this.hzData.length - 1;
-                } else {
-                  sqlGenSummary += contentToAdd;
-                  this.hzData[sqlGenIndex].summary = sqlGenSummary;
-                }
-                break;
-              case 'result':
-                if (data.data && data.data.sql) {
-                  contentToAdd = typeof data.data.sql === 'string' ? data.data.sql : JSON.stringify(data.data.sql);
-                  this.hzData.push({ type: 'result', summary: contentToAdd });
-                }
-                // 保持原有地块处理逻辑
-                if (data.data && data.data.exec_result) {
-                  try {
-                    this.getDetailByIdList(data.data.exec_result, (detailData) => {
-                      if (Array.isArray(detailData) && detailData.length > 0) {
-                        this.clearMark();
-                        this.addGdLayer(detailData);
-                        this.xgdk = detailData;
-                        this.showXgdkCenterPoint();
-                        this.mContentVisabled = true;
-                        this.xgdk.forEach((item) => {
-                          item["show"] = true;
-                        });
-                      } else {
-                        // 无有效地块数据时,清空相关内容
-                        this.clearMark();
-                        this.xgdk = [];
-                        this.mContentVisabled = false;
-                      }
-                    });
-                  } catch (e) {
-                    console.error('Error processing result data:', e);
-                  }
-                }
-                break;
-              case 'end':
-                contentToAdd = typeof data.content === 'string' ? data.content : JSON.stringify(data.content);
-                this.hzData.push({ type: 'end', summary: contentToAdd });
-                this.loading = false;
-                this.showResult = true;
-                this.radarShow = false;
-                this.activeIndex = this.steps.length - 1;
-                this.btnSendDisabled = false;
-                break;
-            }
-            let timer = setTimeout(() => {
-              this.scrollToBottom()
-              clearTimeout(timer)
-              timer = null
-            }, 200)
-          } catch (e) {
-            console.error('Error parsing stream data:', e);
-          }
-        }
-      }
-    },
-
-    sleep (time) {
-      return new Promise((resolve, reject) => {
-        let timer = setTimeout(() => {
-          resolve(true)
-          clearTimeout(timer)
-          timer = null;
-        }, time)
-      })
-    },
-    scrollToBottom () {
-      const container = this.$refs['container']
+    scrollToBottom() {
+      const container = this.$refs["container"];
       container.scrollTop = container.scrollHeight;
     },
     /**
@@ -1992,11 +2021,12 @@ export default {
   background: #ffffff;
   position: relative;
   padding: 17px 0px 0px 19px;
-  box-shadow: 0px 6px 12px 1px rgba(11,53,103,0.08);
+  box-shadow: 0px 6px 12px 1px rgba(11, 53, 103, 0.08);
   border-radius: 16px 16px 16px 16px;
   position: relative;
   padding-bottom: 50px;
-  border: 1px solid #E5E6EA;
+  border: 1px solid #e5e6ea;
+
   textarea {
     border: none;
     resize: none;
@@ -2004,6 +2034,7 @@ export default {
     font-weight: normal;
     font-size: 18px;
     height: 100%;
+
     &:focus-visible {
       outline: none;
     }
@@ -2183,6 +2214,7 @@ export default {
           height: calc(100% - 62px);
           max-height: 300px;
           overflow-y: auto;
+
           .item {
             list-style: none;
             display: flex;
@@ -2232,7 +2264,6 @@ export default {
         }
       }
 
-      
       .topdiv {
         position: absolute;
         width: 486px;
@@ -2243,16 +2274,18 @@ export default {
         background-color: #ffffff;
         display: flex;
         flex-direction: column;
-        box-shadow: 0px 4px 15px 1px rgba(0,54,116,0.15);
+        box-shadow: 0px 4px 15px 1px rgba(0, 54, 116, 0.15);
         background-image: url(~@/assets/image/box_bg.png);
         background-repeat: no-repeat;
         background-size: 100% 100%;
         padding: 30px 20px;
+
         > .box {
           flex: 1;
           display: flex;
           align-items: flex-start;
           position: relative;
+
           > div {
             &.summary-icon {
               width: 88px;
@@ -2263,65 +2296,94 @@ export default {
               position: absolute;
               left: -12px;
               top: -8px;
+
               > img {
                 width: 100%;
                 height: 100%;
               }
             }
+
             &.content_box {
               border-radius: 16px;
               border-top-left-radius: 50px;
-              background: linear-gradient( 228deg, rgba(77,151,252,0.3) 0%, rgba(102,160,255,0.3) 37%, rgba(225,134,252,0.57) 69%, #B072F9 100%);
+              background: linear-gradient(
+                228deg,
+                rgba(77, 151, 252, 0.3) 0%,
+                rgba(102, 160, 255, 0.3) 37%,
+                rgba(225, 134, 252, 0.57) 69%,
+                #b072f9 100%
+              );
+
               > div {
                 &.one_box {
                   padding: 10px 20px;
                   padding-left: 90px;
-                  >.title {
+
+                  > .title {
                     font-size: 18px;
                     font-weight: bold;
                     color: transparent;
-                    background: linear-gradient( 270deg, #2534F4 0%, #1160F5 47%, #0075E8 100%);
+                    background: linear-gradient(
+                      270deg,
+                      #2534f4 0%,
+                      #1160f5 47%,
+                      #0075e8 100%
+                    );
                     -webkit-background-clip: text;
                   }
+
                   > div {
                     &:first-child {
                       font-size: 18px;
                       font-weight: bold;
                       color: transparent;
-                      background: linear-gradient( 270deg, #2534F4 0%, #1160F5 47%, #0075E8 100%);
+                      background: linear-gradient(
+                        270deg,
+                        #2534f4 0%,
+                        #1160f5 47%,
+                        #0075e8 100%
+                      );
                       -webkit-background-clip: text;
                     }
+
                     &.desc {
-                      color: #0C254A;
+                      color: #0c254a;
                       font-size: 15px;
                       line-height: 19px;
                     }
                   }
                 }
+
                 &.two_box {
                   background: #f1f9fe;
-                  box-shadow: 0px 4px 10px 1px rgba(0,54,116,0.15);
+                  box-shadow: 0px 4px 10px 1px rgba(0, 54, 116, 0.15);
                   border-radius: 16px;
                   padding: 20px;
+
                   > .title {
                     display: flex;
                     align-items: center;
                     padding-bottom: 10px;
-                    >.logo {
+
+                    > .logo {
                       width: 24px;
                       height: 24px;
-                      background: url("../../assets/image/title_wh.png") no-repeat;
+                      background: url("../../assets/image/title_wh.png")
+                        no-repeat;
                       background-size: 100% 100%;
                       margin-right: 5px;
                     }
-                    >.txt {
-                      color: #326EF4;
+
+                    > .txt {
+                      color: #326ef4;
                       font-weight: bold;
                       font-size: 17px;
                     }
                   }
+
                   > ul {
                     list-style: none;
+
                     > li {
                       padding: 15px;
                       margin-bottom: 10px;
@@ -2332,19 +2394,24 @@ export default {
                       justify-content: space-between;
                       margin-bottom: 15px;
                       cursor: pointer;
-                      >.left_box {
+
+                      > .left_box {
                         display: flex;
                         flex: 1;
+
                         > span {
                           &.desc {
                             flex: 1;
+
                             > span {
                               display: block;
+
                               &.title {
                                 font-size: 18px;
                                 color: #183258;
                                 font-weight: bold;
                               }
+
                               &.txt {
                                 font-size: 16px;
                                 color: #183258;
@@ -2353,6 +2420,7 @@ export default {
                           }
                         }
                       }
+
                       > span {
                         &.right_icon {
                           margin-left: 20px;
@@ -2360,7 +2428,7 @@ export default {
                           height: 28px;
                           border-radius: 50%;
                           background: #e0eeff;
-                          color: #507FFF;
+                          color: #507fff;
                           font-size: 18px;
                           font-weight: bold;
                           text-align: center;
@@ -2398,13 +2466,15 @@ export default {
           background-size: 100% 100%;
           opacity: 0.4;
         }
+
         .left-panel-content {
           width: 100%;
           height: calc(100% - 150px);
           overflow-x: hidden;
           overflow-y: auto;
           position: relative;
-          >.back_icon {
+
+          > .back_icon {
             position: absolute;
             top: 6px;
             right: 0px;
@@ -2416,7 +2486,6 @@ export default {
           }
         }
 
-
         .desc {
           font-family: Microsoft YaHei;
           font-weight: 400;
@@ -2446,6 +2515,7 @@ export default {
           margin-bottom: 36px;
           padding-right: 50px;
         }
+
         .jsz {
           margin: 0 0px 17px 0px;
           display: flex;
@@ -2663,18 +2733,22 @@ export default {
           display: flex;
           align-content: center;
           justify-content: space-between;
+
           > span {
             &:first-child {
               color: #212121;
             }
+
             &:last-child {
               cursor: pointer;
               display: flex;
               align-content: center;
               justify-content: space-between;
+
               .arrowIcon {
                 margin-right: 2px;
               }
+
               .text {
                 font-size: 14px;
                 color: #777c88;
@@ -2902,6 +2976,7 @@ export default {
   bottom: 12px;
   left: 15px;
 }
+
 .dkitemchecked {
   background: rgba(224, 234, 250, 0.4);
 }
@@ -2911,11 +2986,13 @@ export default {
   padding: 10px;
   border-bottom: 1px solid #e4e8ed;
   cursor: pointer;
+
   > .dk_title {
     display: flex;
     align-items: center;
     justify-content: space-between;
     margin-bottom: 8px;
+
     > div {
       &:first-child {
         color: #2553d5;
@@ -2926,6 +3003,7 @@ export default {
         overflow: hidden;
         margin-right: 10px;
       }
+
       &:last-child {
         color: #777c88;
         font-size: 20px;
@@ -2933,19 +3011,24 @@ export default {
       }
     }
   }
+
   > .content {
     flex: 1;
+
     p {
       color: #212121;
       margin-top: 5px;
     }
+
     > p {
       font-size: 16px;
       font-weight: bold;
     }
+
     > div {
       > p {
         font-size: 15px;
+
         > span:first-child {
           font-weight: bold;
         }
@@ -3025,19 +3108,23 @@ export default {
   background: url("~@/assets/image/staticImage/icon-blue.png");
   background-size: cover;
 }
+
 .arrowIcon {
   display: inline-block;
   width: 20px;
   height: 20px;
 }
+
 .arrow_up {
   background: url("~@/assets/image/arrow2.png") no-repeat;
   background-size: 100% 100%;
 }
+
 .arrow_down {
   background: url("~@/assets/image/arrow1.png") no-repeat;
   background-size: 100% 100%;
 }
+
 .stop_send_btn {
   width: 40px;
   height: 40px;
@@ -3048,13 +3135,14 @@ export default {
   display: flex;
   align-items: center;
   justify-content: center;
-  background: linear-gradient(124deg, #505DFF 0%, #418CFF 100%);
+  background: linear-gradient(124deg, #505dff 0%, #418cff 100%);
   border-radius: 10px 10px 10px 10px;
   cursor: pointer;
   opacity: 0.8;
 }
+
 .stop_send_btn::before {
-  display: block;  
+  display: block;
   content: "";
   width: 20px;
   height: 20px;

+ 61 - 28
web_ui/src/views/industrial-land/ZnxzDetail.vue

@@ -45,11 +45,11 @@
                   <div class="label c">人口总量:</div>
                   <div class="count">{{ polulation.人口总量 }}人</div>
                 </div>
-                <!-- <div class="diver10"></div>
+                <div class="diver10"></div>
                 <div class="item">
                   <div class="label c">人口密度:</div>
                   <div class="count">{{ polulation.人口密度 }}人/公顷</div>
-                </div> -->
+                </div>
               </div>
               <div class="line60"></div>
               <div class="title">年龄分布</div>
@@ -1646,27 +1646,9 @@ export default {
     },
     async populationSearch(wktStr) {
       this.queryVillageName(wktStr)
-        .then(villageName => {
+        .then(({ villageName, areaInHectares }) => {
           if (!villageName) {
-            throw new Error('No village name found');
-          }
-          return axios.get(window.ApplicationConfig.populationQueryUrl, {
-            params: {
-              name: villageName
-            }
-          });
-        })
-        .then(response => {
-          if (response.data && response.data.length > 0) {
-            this.polulation = response.data[0];
-            
-            // Update chart data
-            this.yearData[0].value = this.polulation.中青年人口;
-            this.yearData[1].value = this.polulation.老年人口;
-            this.sexData[0].value = this.polulation.男性人口;
-            this.sexData[1].value = this.polulation.女性人口;
-          } else {
-            // Reset to default values if no data
+            // If no village name, set population to default and return
             this.polulation = {
               人口总量: 0,
               人口密度: 0
@@ -1675,10 +1657,48 @@ export default {
             this.yearData[1].value = 0;
             this.sexData[0].value = 0;
             this.sexData[1].value = 0;
+            console.warn('No village name found for population search.');
+            return; // Exit the then block
           }
+          return axios.get(window.ApplicationConfig.populationQueryUrl, {
+            params: {
+              name: villageName
+            }
+          })
+          .then(response => {
+            if (response.data && response.data.length > 0) {
+              const populationData = response.data[0];
+              this.polulation = populationData;
+
+              // Calculate population density using total population and area in hectares
+              if (areaInHectares > 0) {
+                 this.polulation.人口密度 = (populationData.人口总量 / parseFloat(areaInHectares)).toFixed(2);
+              } else {
+                 this.polulation.人口密度 = 0;
+              }
+
+
+              // Update chart data
+              this.yearData[0].value = this.polulation.中青年人口;
+              this.yearData[1].value = this.polulation.老年人口;
+              this.sexData[0].value = this.polulation.男性人口;
+              this.sexData[1].value = this.polulation.女性人口;
+            } else {
+              // Reset to default values if no population data found for the village
+              this.polulation = {
+                人口总量: 0,
+                人口密度: 0
+              };
+              this.yearData[0].value = 0;
+              this.yearData[1].value = 0;
+              this.sexData[0].value = 0;
+              this.sexData[1].value = 0;
+              console.warn(`No population data found for village: ${villageName}`);
+            }
+          });
         })
         .catch(error => {
-          console.error('Error in population search:', error);
+          console.error('Error in population search after getting village name and area:', error);
           // Reset to default values on error
           this.polulation = {
             人口总量: 0,
@@ -1704,9 +1724,10 @@ export default {
           formdata.append("geometry", esrijson_str);
           formdata.append("inSR", "4490");
           formdata.append("spatialRel", "esriSpatialRelIntersects");
-          formdata.append("outFields", "name");
+          //outTdields增加shape字段,可以获取图形
+          formdata.append("outFields", "name,shape");
           formdata.append("returnGeometry", "false");
-          formdata.append("outSR", "4490");
+          formdata.append("outSR", "4528"); // Ensure output SR is 4528 for planar area calculation
           formdata.append("f", "pjson");
 
           axios.post(window.ApplicationConfig.xiangcunMapUrl, formdata, {
@@ -1716,13 +1737,25 @@ export default {
           })
             .then(response => {
               if (response.data && response.data.features && response.data.features.length > 0) {
-                resolve(response.data.features[0].attributes.name);
+                const feature = response.data.features[0];
+                const villageName = feature.attributes.name;
+                // Convert ArcGIS JSON geometry (in 4528) to GeoJSON (in 4326 by default for arcgisToGeoJSON)
+                const geojsonGeometry = arcgisToGeoJSON(feature.geometry);
+                // Calculate planar area in square meters using turf (assuming input coordinates are in meters)
+                let areaInSquareMeters = 0;
+                if (geojsonGeometry && geojsonGeometry.type === 'Polygon' || geojsonGeometry.type === 'MultiPolygon') {
+                    areaInSquareMeters = turf.area(geojsonGeometry);
+                }
+                // Convert area to hectares
+                const areaInHectares = areaInSquareMeters > 0 ? (areaInSquareMeters / 10000).toFixed(2) : 0;
+
+                resolve({ villageName, areaInHectares });
               } else {
-                resolve(null);
+                resolve({ villageName: null, areaInHectares: 0 }); // Return 0 hectares if no feature
               }
             })
             .catch(error => {
-              console.error('Error querying village name:', error);
+              console.error('Error querying village name and area:', error);
               reject(error);
             });
         } catch (error) {

+ 2 - 2
web_ui/static/config.js

@@ -1,6 +1,6 @@
 (function (window) {
-  const landsiteai_base_url = '/landsiteai'
-  // const landsiteai_base_url = 'http://localhost:8521'
+  // const landsiteai_base_url = '/landsiteai'
+  const landsiteai_base_url = 'http://localhost:8521'
   window.ApplicationConfig = {
     serverURL: "/server",
     // ===================== 订阅与推送服务 (8511) =====================