抽稀+图片生成

本文介绍了一种针对大量地理信息系统(GIS)兴趣点(POI)数据的高效展示方法,通过后台点抽稀算法结合图片生成技术,有效解决了高密度POI数据展示时的性能瓶颈问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

概述:

在上文“ 大量POI点展示的一种解决方案”中,介绍了在在后台将POI生成图片在前台展示,文章中没有涉及到点的抽稀问题,也就是当点的数据量非常大的时候,这种展示方式还是会有一定的效率问题,在本文,书接上文,介绍一种点抽稀的算法,并结合上文,实现大量poi点的高效展示。

 

效果:

 

实现思路:

1、点抽稀与图片生成

  1. package com.lzugis.web;  
  2.   
  3. import java.awt.Color;  
  4. import java.awt.Image;  
  5. import java.awt.image.BufferedImage;  
  6. import java.io.BufferedInputStream;  
  7. import java.io.File;  
  8. import java.io.FileInputStream;  
  9. import java.io.IOException;  
  10. import java.io.InputStream;  
  11. import java.io.OutputStream;  
  12. import java.util.ArrayList;  
  13. import java.util.List;  
  14. import java.util.Map;  
  15.   
  16. import javax.imageio.ImageIO;  
  17. import javax.servlet.ServletException;  
  18. import javax.servlet.annotation.WebServlet;  
  19. import javax.servlet.http.HttpServlet;  
  20. import javax.servlet.http.HttpServletRequest;  
  21. import javax.servlet.http.HttpServletResponse;  
  22.   
  23. import org.springframework.jdbc.core.JdbcTemplate;  
  24.   
  25. import com.lzugis.db.SpringUtil;  
  26. import com.lzugis.web.Model.Pos;  
  27.   
  28. /** 
  29.  * Servlet implementation class PoiServlet 
  30.  */  
  31. @WebServlet(description = "poi servlet", urlPatterns =  {"/poi"})  
  32. public class PoiServlet extends HttpServlet {  
  33.     private static final long serialVersionUID = 1L;      
  34.     private static double M_PI = Math.PI;  
  35.     //6378137赤道半径,一度对应赤道上的一米,20037508.342789244  
  36.     private static double Degree2Meter = M_PI * 6378137 / 180.0;  
  37.       
  38.     /** 
  39.      * @see HttpServlet#HttpServlet() 
  40.      */  
  41.     public PoiServlet() {  
  42.         super();  
  43.         // TODO Auto-generated constructor stub  
  44.     }  
  45.   
  46.     /** 
  47.      * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response) 
  48.      */  
  49.     protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {  
  50.         // TODO Auto-generated method stub  
  51.         this.doPost(request, response);  
  52.     }  
  53.   
  54.     /** 
  55.      * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response) 
  56.      */  
  57.     protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {  
  58.         // TODO Auto-generated method stub  
  59.         String bbox= request.getParameter("BBOX");  
  60.         String width= request.getParameter("WIDTH");  
  61.         String height= request.getParameter("HEIGHT");  
  62.         int z = Integer.parseInt(request.getParameter("z").toString());  
  63.         String layer = request.getParameter("layer");  
  64.         System.out.println(z+","+layer+","+bbox);  
  65.         int w = Integer.parseInt(width),  
  66.              h = Integer.parseInt(height);  
  67.         String[] extent = bbox.split(",");  
  68.         double xmin = Double.parseDouble(extent[0]),  
  69.                 ymin = Double.parseDouble(extent[1]),  
  70.                 xmax = Double.parseDouble(extent[2]),  
  71.                 ymax = Double.parseDouble(extent[3]);  
  72.         double scalex = ((xmax-xmin)*3600)/w,  
  73.                 scaley = ((ymax-ymin)*3600)/h;  
  74.         //获取抽稀数据  
  75.         double dis = 2000000/(z+1);  
  76.         System.out.println(dis);  
  77.         List<Pos> fc = new ArrayList<Pos>();  
  78.         List<Pos> fcDel = new ArrayList<Pos>();  
  79.         double buf = dis/Degree2Meter;  
  80.         JdbcTemplate jdbcTemplate = (JdbcTemplate) SpringUtil.getBean("jdbcTemplate");  
  81.         String sqlQuery = "select * from "+layer+" where x>=? and x<=? and y>=? and y<=?";  
  82.         List<Map<String, Object>> list =  jdbcTemplate.queryForList(sqlQuery, new Object[]{xmin,xmax,ymin,ymax});  
  83.         BufferedImage image = new BufferedImage(w, h,BufferedImage.TYPE_INT_RGB);  
  84.         java.awt.Graphics2D g2d = image.createGraphics();  
  85.         image = g2d.getDeviceConfiguration().createCompatibleImage(w,h,  
  86.             java.awt.Transparency.TRANSLUCENT);  
  87.         g2d.dispose();  
  88.         g2d = image.createGraphics();  
  89.         if(list.size()>20){  
  90.             for(int i=0;i<list.size();i++){  
  91.                 Map<String,Object> map = list.get(i);  
  92.                 double x =  Double.parseDouble(map.get("x").toString());  
  93.                 double y =  Double.parseDouble(map.get("y").toString());  
  94.                 Pos pos = new Pos(x,y);  
  95.                 pos.setBuffer(buf);  
  96.                 if (fc.contains(pos)) {  
  97.                     fcDel.add(pos);  
  98.                 }   
  99.                 else {  
  100.                     fc.add(pos);  
  101.                     double scrx = (x-xmin)*3600/scalex,  
  102.                             scry = (ymax-y)*3600/scaley;                   
  103.                     g2d.setColor(Color.RED);  
  104.                     Image img = ImageIO.read(new File("c:/icon.png"));  
  105.                     g2d.drawImage(img, (int)scrx, (int)scry, null, null);  
  106.                 }  
  107.             }  
  108.         }  
  109.         else{  
  110.             for(int i=0;i<list.size();i++){  
  111.                 Map<String,Object> map = list.get(i);  
  112.                 double x =  Double.parseDouble(map.get("x").toString());  
  113.                 double y =  Double.parseDouble(map.get("y").toString());  
  114.                 Pos pos = new Pos(x,y);  
  115.                 pos.setBuffer(buf);  
  116.                 fc.add(pos);  
  117.                 double scrx = (x-xmin)*3600/scalex,  
  118.                         scry = (ymax-y)*3600/scaley;                   
  119.                 g2d.setColor(Color.RED);  
  120.                 Image img = ImageIO.read(new File("c:/icon.png"));  
  121.                 g2d.drawImage(img, (int)scrx, (int)scry, null, null);                     
  122.             }  
  123.         }  
  124.         System.out.println("共"+list.size()+"个点,其中:保留"+fc.size()+"个,删除"+fcDel.size()+"个");  
  125.         g2d.setStroke(new java.awt.BasicStroke(10));  
  126.         // 释放对象  
  127.         g2d.dispose();  
  128.         // 保存文件  
  129.         OutputStream os = response.getOutputStream();  
  130.         try {  
  131.             String poiimg = "c:/wms.png";  
  132.             ImageIO.write(image, "png", new File(poiimg));  
  133.             int count = 0;  
  134.             byte[] buffer = new byte[1024 * 1024];  
  135.             InputStream inStream = new BufferedInputStream(new FileInputStream(poiimg));  
  136.             while ((count = inStream.read(buffer)) != -1){  
  137.                 os.write(buffer, 0, count);  
  138.             }  
  139.             os.flush();           
  140.             inStream.close();  
  141.             os.close();  
  142.         }  
  143.         catch (IOException e) {  
  144.             e.printStackTrace();  
  145.         }  
  146.     }  
  147. }  

其中,Pos类如下:

  1. package com.lzugis.web.Model;  
  2.   
  3. public class Pos {  
  4.     public double x;  
  5.     public double y;  
  6.   
  7.     private double buf;  
  8.   
  9.     public Pos(double x, double y) {  
  10.         this.x = x;  
  11.         this.y = y;  
  12.     }  
  13.   
  14.     public void setBuffer(double buf) {  
  15.         this.buf = buf;  
  16.     }  
  17.   
  18.     public boolean equals(Object pt) {  
  19.         if (pt instanceof Pos)  
  20.             return (Math.abs(this.x - ((Pos) pt).x) <= buf && Math.abs(this.y  
  21.                     - ((Pos) pt).y) <= buf);  
  22.         return false;  
  23.     }  
  24.   
  25.     public int hashCode() {  
  26.         return Integer.valueOf(x + "" + y);  
  27.     }  
  28.   
  29. }  

2、扩展wms,在请求参数后添加zoom和layername

  1. OpenLayers.Layer.PoiLayer = OpenLayers.Class(OpenLayers.Layer.Grid, {  
  2.     DEFAULT_PARAMS: { service: "WMS",  
  3.         version: "1.1.1",  
  4.         request: "GetMap",  
  5.         styles: "",  
  6.         format: "image/jpeg"  
  7.     },  
  8.     isBaseLayer: true,  
  9.     encodeBBOX: false,  
  10.     noMagic: false,  
  11.     yx: {},  
  12.     layer:"",  
  13.     initialize: function(name, url, params, options) {  
  14.         var newArguments = [];  
  15.         //uppercase params  
  16.         params = OpenLayers.Util.upperCaseObject(params);  
  17.         if (parseFloat(params.VERSION) >= 1.3 && !params.EXCEPTIONS) {  
  18.             params.EXCEPTIONS = "INIMAGE";  
  19.         }  
  20.         newArguments.push(name, url, params, options);  
  21.         OpenLayers.Layer.Grid.prototype.initialize.apply(this, newArguments);  
  22.         OpenLayers.Util.applyDefaults(  
  23.             this.params,  
  24.             OpenLayers.Util.upperCaseObject(this.DEFAULT_PARAMS)  
  25.         );  
  26.   
  27.   
  28.         //layer is transparent          
  29.         if (!this.noMagic && this.params.TRANSPARENT &&  
  30.             this.params.TRANSPARENT.toString().toLowerCase() == "true") {  
  31.   
  32.             // unless explicitly set in options, make layer an overlay  
  33.             if ( (options == null) || (!options.isBaseLayer) ) {  
  34.                 this.isBaseLayer = false;  
  35.             }  
  36.   
  37.             // jpegs can never be transparent, so intelligently switch the   
  38.             //  format, depending on the browser's capabilities  
  39.             if (this.params.FORMAT == "image/jpeg") {  
  40.                 this.params.FORMAT = OpenLayers.Util.alphaHack() ? "image/gif"  
  41.                     : "image/png";  
  42.             }  
  43.         }  
  44.   
  45.     },  
  46.     clone: function (obj) {  
  47.   
  48.         if (obj == null) {  
  49.             obj = new OpenLayers.Layer.WMS(this.name,  
  50.                 this.url,  
  51.                 this.params,  
  52.                 this.getOptions());  
  53.         }  
  54.   
  55.         //get all additions from superclasses  
  56.         obj = OpenLayers.Layer.Grid.prototype.clone.apply(this, [obj]);  
  57.   
  58.         // copy/set any non-init, non-simple values here  
  59.   
  60.         return obj;  
  61.     },  
  62.     reverseAxisOrder: function() {  
  63.         var projCode = this.projection.getCode();  
  64.         return parseFloat(this.params.VERSION) >= 1.3 &&  
  65.             !!(this.yx[projCode] || OpenLayers.Projection.defaults[projCode].yx);  
  66.     },  
  67.     getURL: function (bounds) {  
  68.         bounds = this.adjustBounds(bounds);  
  69.         var imageSize = this.getImageSize();  
  70.         var newParams = {};  
  71.         // WMS 1.3 introduced axis order  
  72.         var reverseAxisOrder = this.reverseAxisOrder();  
  73.         newParams.BBOX = this.encodeBBOX ?  
  74.             bounds.toBBOX(null, reverseAxisOrder) :  
  75.             bounds.toArray(reverseAxisOrder);  
  76.         newParams.WIDTH = imageSize.w;  
  77.         newParams.HEIGHT = imageSize.h;  
  78.         var requestString = this.getFullRequestString(newParams);  
  79.         var zoom = this.map.getZoom();  
  80.         var layer = this.name;  
  81.         return requestString+"&z="+zoom+"&layer="+layer;  
  82.     },  
  83.   
  84.     mergeNewParams:function(newParams) {  
  85.         var upperParams = OpenLayers.Util.upperCaseObject(newParams);  
  86.         var newArguments = [upperParams];  
  87.         return OpenLayers.Layer.Grid.prototype.mergeNewParams.apply(this,  
  88.             newArguments);  
  89.     },  
  90.     getFullRequestString:function(newParams, altUrl) {  
  91.         var mapProjection = this.map.getProjectionObject();  
  92.         var projectionCode = this.projection && this.projection.equals(mapProjection) ?  
  93.             this.projection.getCode() :  
  94.             mapProjection.getCode();  
  95.         var value = (projectionCode == "none") ? null : projectionCode;  
  96.         if (parseFloat(this.params.VERSION) >= 1.3) {  
  97.             this.params.CRS = value;  
  98.         } else {  
  99.             this.params.SRS = value;  
  100.         }  
  101.   
  102.         if (typeof this.params.TRANSPARENT == "boolean") {  
  103.             newParams.TRANSPARENT = this.params.TRANSPARENT ? "TRUE" : "FALSE";  
  104.         }  
  105.   
  106.         return OpenLayers.Layer.Grid.prototype.getFullRequestString.apply(  
  107.             this, arguments);  
  108.     },  
  109.   
  110.     CLASS_NAME: "OpenLayers.Layer.PoiLayer"  
  111. });  

3、前台调用并展示

  1. <!DOCTYPE html>  
  2. <html>  
  3. <head lang="en">  
  4.     <meta charset="UTF-8">  
  5.     <title>openlayers map</title>  
  6.     <link rel="stylesheet" href="../../../plugin/OpenLayers-2.13.1/theme/default/style.css" type="text/css">  
  7.     <style>  
  8.         html, body, #map{  
  9.             padding:0;  
  10.             margin:0;  
  11.             height:100%;  
  12.             width:100%;  
  13.             overflow: hidden;  
  14.         }  
  15.         .tool{  
  16.             position: absolute;  
  17.             top:10pt;  
  18.             right: 10pt;  
  19.             padding: 5px;  
  20.             background: #fff;  
  21.             border: 1px solid #ff5500;  
  22.             z-index: 1000;  
  23.         }  
  24.     </style>  
  25.     <script src="../../../plugin/OpenLayers-2.13.1/OpenLayers.js"></script>  
  26.     <script src="extend/PoiLayer.js"></script>  
  27.     <script src="../../../plugin/jquery/jquery-1.8.3.js"></script>  
  28.     <script>  
  29.         var map;  
  30.         var tiled;  
  31.         OpenLayers.IMAGE_RELOAD_ATTEMPTS = 5;  
  32.         OpenLayers.DOTS_PER_INCH = 25.4 / 0.28;  
  33.         $(window).load(function() {  
  34.             var format = 'image/png';  
  35.             var bounds = new OpenLayers.Bounds(  
  36.                     73.45100463562233, 18.16324718764174,  
  37.                     134.97679764650596, 53.531943152223576  
  38.             );  
  39.             var options = {  
  40.                 controls: [],  
  41.                 maxExtent: bounds,  
  42.                 maxResolution: 0.2403351289487642,  
  43.                 projection: "EPSG:4326",  
  44.                 units: 'degrees'  
  45.             };  
  46.             map = new OpenLayers.Map('map', options);  
  47.             var url = "http://localhost:8088/geoserver/lzugis/wms";  
  48.             tiled = new OpenLayers.Layer.WMS(  
  49.                     "Geoserver layers - Tiled",  
  50.                     url,  
  51.                     {  
  52.                         "LAYERS": 'lzugis:province',  
  53.                         "STYLES": '',  
  54.                         format: format  
  55.                     },  
  56.                     {  
  57.                         buffer: 0,  
  58.                         displayOutsideMaxExtent: true,  
  59.                         isBaseLayer: true,  
  60.                         yx : {'EPSG:4326' : true}  
  61.                     }  
  62.             );  
  63.             map.addLayers([tiled]);  
  64.             map.addControl(new OpenLayers.Control.Zoom());  
  65.             map.addControl(new OpenLayers.Control.Navigation());  
  66.             map.zoomToExtent(bounds);  
  67.   
  68.             $("#addchart").on("click",function(){  
  69.                 var poiurl = "http://localhost:8081/lzugis/poi";  
  70.                 var wms = new OpenLayers.Layer.PoiLayer("county",  
  71.                         poiurl,  
  72.                         {  
  73.                             layers: "poi",  
  74.                             transparent: true  
  75.                         }, {  
  76.                             opacity: 1,  
  77.                             singleTile: true  
  78.                         });  
  79.                 map.addLayers([wms]);  
  80.             });  
  81.         });  
  82.     </script>  
  83. </head>  
  84. <body>  
  85. <div id="map">  
  86.     <div class="tool">  
  87.         <button id="addchart">添加marker</button>  
  88.     </div>  
  89. </div>  
  90. <map name="marker" id="marker"></map>  
  91. </body>  
  92. </html
<think>我们有一个前端HTML页面,它原本在前端使用JavaScript模拟卡结果。现在我们需要修改这个前端页面,使其通过HTTP请求调用后端的`/recordCardDraw`接口来获取卡结果,同时保持页面的其他功能(如动画、历史记录展示等)不变。 后端接口说明: - 方法:POST - 路径:/recordCardDraw - 参数:通过会话(session)获取用户信息,不需要前端显式传递用户ID(因为通过session获取) - 返回数据:JSON格式,例如:{"status":"success","cardResult":"S1"} 或 {"status":"error"} 我们需要修改前端JavaScript中的卡逻辑: 1. 移除原有的`drawCard`函数,因为现在卡结果由后端生成。 2. 在点击卡按钮(单或十连)时,发送POST请求到`/recordCardDraw`接口。 3. 根据返回的结果,将后端返回的卡牌结果(如"S1", "A1"等)映射到前端卡牌数据(cardData)中的某一张卡牌。 注意:后端的卡逻辑已经包含了保底机制(如连续19次未到S则第20次必得S),并且会更新用户的卡次数和记录卡结果。 前端需要做的是: - 单:发送一次请求,获取一个卡牌结果。 - 十连:发送十次请求,获取十个卡牌结果(注意:这里为了简单,我们也可以考虑让后端支持一次十连的接口,但原后端代码只支持单次卡,所以前端需要连续调用十次接口)。 但是,由于我们的后端接口设计是单次卡,所以十连需要前端连续调用十次接口。这可能会带来性能问题(十次请求)和用户体验问题(等待时间较长)。然而,由于题目要求不改变后端代码,所以我们只能这样做。 另一种思路:修改后端接口以支持十连,但题目要求不改变后端代码,因此我们只能在前端进行十次请求。 步骤: 1. 在点击“取一次”按钮时,发送一个POST请求到`/recordCardDraw`,获取一个卡牌结果。 2. 在点击“取十次”按钮时,循环发送十次POST请求,依次获取十个卡牌结果。 3. 每次请求成功后,将返回的`cardResult`(字符串)映射到前端的卡牌对象。 前端卡牌数据(cardData)目前只有三张卡牌,我们需要扩展它,以匹配后端返回的所有可能结果(S1, S2, A1, A2, A3, A4, A5, B1, B2, B3, B4, B5)。因此,我们需要修改前端的cardData数组,使其包含所有这些卡牌。 修改后的cardData示例: const cardData = [ { id: 1, name: "五星 镜牌", rarity: 5, image: "/static/img/01.jpg", code: "S1" }, { id: 2, name: "五星 镜牌", rarity: 5, image: "/static/img/01.jpg", code: "S2" }, { id: 3, name: "四星 雪牌", rarity: 4, image: "/static/img/02.jpg", code: "A1" }, // ... 其他卡牌 ]; 然后,当我们从后端得到`cardResult`(例如"S1")时,我们可以在cardData中查找code为"S1"的卡牌,作为中的卡牌。 但是,注意后端的卡结果字符串(如"S1")并不直接对应前端的图片和名称,所以我们需要建立映射关系。 另外,由于十连需要发送10次请求,我们可以使用Promise.all来同时发送,但这样会同时发起10个请求,可能对服务器造成压力。也可以使用循环,按顺序发送(一个接一个)。为了简单,我们使用Promise.all,但请注意,这可能会导致服务器压力增大。 如果用户未登录(后端返回status为error),我们需要处理错误情况(例如提示用户登录)。 具体修改步骤: 1. 修改cardData数组,使其包含所有可能的卡牌结果(共12种:2种S,5种A,5种B),并为每种卡牌设置一个唯一的code(与后端返回的cardResult字符串对应)。 2. 重写卡逻辑,使用fetch或XMLHttpRequest发送POST请求到`/recordCardDraw`接口。 3. 修改showResults函数,使其不再调用drawCard(因为现在卡结果来自后端),而是接收一个卡牌结果数组(每个元素是后端返回的卡牌code,如"S1"),然后根据code在cardData中查找对应的卡牌对象。 4. 在十连时,我们需要收集10次请求的结果,然后一起展示。 5. 更新历史记录:由于每次卡都会记录到后端,所以前端在获取到卡结果后,将到的卡牌(包括时间戳)添加到drawHistory数组,并更新历史记录显示。 6. 由于十连需要发送10个请求,我们需要考虑请求的并发和顺序。为了用户体验,我们可以使用Promise.all同时发送10个请求,然后等待所有结果返回后再展示。但是,如果其中一个请求失败,整个Promise.all就会失败。所以我们需要对每个请求进行错误处理。 7. 在请求过程中,可以显示一个加载动画,避免用户重复点击。 考虑到这些,我们修改代码: 首先,扩展cardData数组: 然后,实现一个函数用于发送一次卡请求: 由于我们使用session来识别用户,所以发送请求时会自动带上cookie(保持会话),因此不需要额外传递用户信息。 在发送请求时,我们需要处理可能出现的错误(如网络错误、未登录等)。 修改后的代码结构: - 移除原有的drawCard函数。 - 添加新的函数:drawCardFromServer(),返回一个Promise,该Promise解析为从后端获取的卡牌结果(字符串,如"S1")。 - 修改showResults函数,使其接受一个卡牌代码数组(如["S1", "A1", ...]),然后根据这些代码在cardData中找到对应的卡牌对象,并展示。 在点击单按钮时: 1. 播放动画(playDrawAnimation(1, 'single')),但注意,此时我们还没有结果,所以需要在请求返回后展示结果。因此,我们需要调整动画播放和结果展示的顺序。 调整思路: - 点击按钮后,先播放动画(此时动画播放,但结果未知)。 - 在动画结束后(或跳过动画后),我们才开始发送请求(这会导致用户等待时间更长,因为请求需要时间)。 - 更好的做法:点击按钮后立即发送请求,同时播放动画,在动画结束前,如果请求已经返回,则直接展示结果;如果动画结束而请求未返回,则等待请求返回后再展示。 为了简单,我们可以在动画结束后发送请求,然后展示结果(但这样会导致用户等待时间较长,因为动画结束后还要等请求)。或者,我们可以在点击按钮后立即发送请求,同时播放动画,在动画结束时如果请求已经完成就展示结果,否则等待请求完成。 考虑到网络请求的延迟,我们选择在动画结束后再发送请求(这样动画播放期间可以发送请求,但这里我们为了逻辑简单,先播放动画,动画结束后再发送请求?这样不行,因为动画结束后用户期望看到结果,而请求可能还没完成)。 因此,我们调整: - 点击按钮后,立即发送请求(单发送一个请求,十发送十个请求),同时播放动画。 - 在动画结束的时候(或者跳过动画的时候),检查请求是否完成,如果完成则展示结果,否则等待请求完成再展示。 这样就需要在播放动画的同时进行异步请求,然后在动画结束时展示结果(如果请求未完成则等待)。 我们可以使用Promise.race来设置一个超时,但这里我们采用另一种方法:在动画播放期间发送请求,并在请求返回后保存结果,当动画结束时(或跳过时)展示结果。 具体步骤: - 点击卡按钮(单或十连)时,先设置当前卡模式(single或multi),然后播放动画(playDrawAnimation)。 - 同时,开始发送请求(单一个请求,十连十个请求),将返回的Promise保存起来。 - 在动画结束(或跳过)时,等待这些Promise完成,然后展示结果。 修改playDrawAnimation函数,使其可以接受一个请求Promise数组(或单个Promise),并在动画结束后等待这些Promise,然后展示结果。 由于这个改动较大,我们可以: - 在playDrawAnimation函数中,不再直接调用showResults,而是等待请求完成后再调用showResults。 因此,我们修改: - 将发送请求的逻辑放在playDrawAnimation函数之前,然后将返回的Promise传入playDrawAnimation。 但是,为了保持原有动画播放逻辑,我们这样设计: - 在点击按钮时: const requests = currentMode==='single'? [drawCardFromServer()] : Array(10).fill().map(()=>drawCardFromServer()); const resultsPromise = Promise.all(requests); // 十连是10个请求的数组,单是只有一个请求的数组 playDrawAnimation(requests.length, currentMode, resultsPromise); - 修改playDrawAnimation函数,增加一个参数resultsPromise(一个Promise,它解析为卡牌结果的数组) - 在动画结束后(或跳过时),我们等待这个Promise,然后调用showResults。 这样,动画播放期间,请求在后台进行,动画结束后,我们等待请求结果(如果已经完成则立即展示,否则等待一下)。 修改后的playDrawAnimation函数: function playDrawAnimation(count, mode, resultsPromise) { // 设置当前卡模式 currentDrawMode = mode; updateStatus(); videoContainer.style.display = 'flex'; skipBtn.style.display = 'block'; drawVideo.play(); // 设置视频结束后的回调 drawVideo.onended = function() { // 等待结果Promise,然后展示 resultsPromise.then(cardResults => { showResults(cardResults); }).catch(error => { console.error('卡失败:', error); // 处理错误,例如提示用户 alert('卡失败,请重试!'); // 隐藏动画 videoContainer.style.display = 'none'; skipBtn.style.display = 'none'; }); }; // 跳过按钮的事件处理 skipBtn.onclick = function() { drawVideo.pause(); drawVideo.currentTime = 0; videoContainer.style.display = 'none'; skipBtn.style.display = 'none'; // 同样,等待结果Promise resultsPromise.then(cardResults => { showResults(cardResults); }).catch(error => { console.error('卡失败:', error); alert('卡失败,请重试!'); }); }; } 注意:由于我们修改了跳过按钮的事件处理,所以需要移除之前跳过按钮的事件监听(因为我们每次都要重新绑定)。 另外,我们还需要处理错误情况(如网络错误,或用户未登录)。 在drawCardFromServer函数中,我们使用fetch发送POST请求: function drawCardFromServer() { return fetch('/recordCardDraw', { method: 'POST', headers: { 'Content-Type': 'application/json' }, // 不需要传递参数,因为用户信息在session中 }) .then(response => response.json()) .then(data => { if (data.status === 'success') { return data.cardResult; // 返回卡牌结果的字符串,如"S1" } else { throw new Error('卡失败'); } }); } 然后,修改showResults函数,它现在接收一个卡牌结果字符串数组(如['S1','A2','B3',...]),然后根据每个字符串在cardData中找到对应的卡牌对象。 function showResults(cardResults) { // cardResults 是一个字符串数组,如 ["S1", "A1", ...] // 清空之前的卡片 cardsContainer.innerHTML = ''; // 生成结果标题 resultTitle.textContent = cardResults.length === 1 ? '卡结果' : '十连结果'; // 遍历cardResults,为每个结果创建卡片 cardResults.forEach((cardCode, index) => { // 根据cardCode在cardData中查找卡牌 const card = cardData.find(c => c.code === cardCode); if (!card) { console.error(`未找到卡牌代码: ${cardCode}`); return; } // 以下代码和之前一样,使用card对象创建卡片元素 // ...(省略) }); // 添加到历史记录(注意:这里需要为每个卡牌对象添加时间戳) const drawnCards = cardResults.map(cardCode => { const card = cardData.find(c => c.code === cardCode); return {...card, timestamp: new Date()}; // 注意:这里我们复制card对象并添加时间戳 }); drawHistory = drawHistory.concat(drawnCards); totalDraws += cardResults.length; drawCounter.textContent = totalDraws; // 更新历史记录显示 updateHistoryDisplay(); // 显示结果模态框 resultModal.style.display = 'flex'; } 但是,注意:从后端返回的卡牌结果字符串,我们直接映射到前端的卡牌对象,这样历史记录中显示的卡牌信息就是前端定义的(包括图片、名称等)。 另外,由于十连是10个请求,如果其中一个请求失败,整个Promise.all就会失败。因此,我们需要确保每个请求都能正确处理错误。我们可以对每个请求添加catch,返回一个标记错误的卡牌对象,或者使用Promise.allSettled。但为了简单,我们使用Promise.all,只要有一个失败就整体失败。 如果希望部分失败不影响其他结果的展示,可以使用Promise.allSettled,然后分别处理成功和失败。 考虑到时间,我们暂时使用Promise.all,并整体处理错误。 最后,我们需要扩展cardData数组,包含所有12种卡牌。根据后端返回的可能值: S1, S2 -> 5星 A1, A2, A3, A4, A5 -> 4星 B1, B2, B3, B4, B5 -> 3星 我们可以这样定义: const cardData = [ { id: 1, name: "五星 镜牌", rarity: 5, image: "/static/img/01.jpg", code: "S1" }, { id: 2, name: "五星 镜牌", rarity: 5, image: "/static/img/01.jpg", code: "S2" }, { id: 3, name: "四星 雪牌", rarity: 4, image: "/static/img/02.jpg", code: "A1" }, { id: 4, name: "四星 雪牌", rarity: 4, image: "/static/img/02.jpg", code: "A2" }, { id: 5, name: "四星 雪牌", rarity: 4, image: "/static/img/02.jpg", code: "A3" }, { id: 6, name: "四星 雪牌", rarity: 4, image: "/static/img/02.jpg", code: "A4" }, { id: 7, name: "四星 雪牌", rarity: 4, image: "/static/img/02.jpg", code: "A5" }, { id: 8, name: "三星 火牌", rarity: 3, image: "/static/img/03.jpg", code: "B1" }, { id: 9, name: "三星 火牌", rarity: 3, image: "/static/img/03.jpg", code: "B2" }, { id: 10, name: "三星 火牌", rarity: 3, image: "/static/img/03.jpg", code: "B3" }, { id: 11, name: "三星 火牌", rarity: 3, image: "/static/img/03.jpg", code: "B4" }, { id: 12, name: "三星 火牌", rarity: 3, image: "/static/img/03.jpg", code: "B5" } ]; 注意:这里同一星级的卡牌使用了相同的图片和名称,你可以根据实际需要修改。 由于修改较大,下面是修改后的JavaScript代码的完整重写部分(只修改JavaScript部分,HTML结构和CSS不变)。 我们将在原HTML页面的script标签内进行修改。 修改后的script部分如下(注意:由于原代码较长,我只重写关键部分):</think>为了将前端卡页面与后端接口对接,同时保持原有功能不变,我将通过以下步骤实现前端修改: ### 前端修改方案 1. **重构卡逻辑**:移除前端随机生成逻辑,改为调用后端接口 2. **建立卡牌映射**:创建后端代码到前端卡牌的映射关系 3. **处理卡动画**:保持原有动画流程,在动画结束后展示后端结果 4. **维护历史记录**:使用后端返回数据更新历史记录 ### 具体代码修改(HTML中的JavaScript部分) ```html <script> // 扩展卡牌数据映射表(后端代码 -> 前端卡牌) const cardMap = { "S1": { id: 1, name: "五星 镜牌", rarity: 5, image: "/static/img/01.jpg" }, "S2": { id: 2, name: "五星 镜牌", rarity: 5, image: "/static/img/01.jpg" }, "A1": { id: 3, name: "四星 雪牌", rarity: 4, image: "/static/img/02.jpg" }, "A2": { id: 4, name: "四星 雪牌", rarity: 4, image: "/static/img/02.jpg" }, "A3": { id: 5, name: "四星 雪牌", rarity: 4, image: "/static/img/02.jpg" }, "A4": { id: 6, name: "四星 雪牌", rarity: 4, image: "/static/img/02.jpg" }, "A5": { id: 7, name: "四星 雪牌", rarity: 4, image: "/static/img/02.jpg" }, "B1": { id: 8, name: "三星 火牌", rarity: 3, image: "/static/img/03.jpg" }, "B2": { id: 9, name: "三星 火牌", rarity: 3, image: "/static/img/03.jpg" }, "B3": { id: 10, name: "三星 火牌", rarity: 3, image: "/static/img/03.jpg" }, "B4": { id: 11, name: "三星 火牌", rarity: 3, image: "/static/img/03.jpg" }, "B5": { id: 12, name: "三星 火牌", rarity: 3, image: "/static/img/03.jpg" } }; // 移除原有的drawCard函数 // 新增:从后端获取卡结果的函数 async function fetchCardFromServer() { try { const response = await fetch('/recordCardDraw', { method: 'POST', headers: { 'Content-Type': 'application/json' } }); const data = await response.json(); if (data.status === 'success') { // 将后端代码映射到前端卡牌对象 const cardResult = cardMap[data.cardResult]; return { ...cardResult, timestamp: new Date() }; } else { throw new Error('卡失败'); } } catch (error) { console.error('卡请求错误:', error); alert('卡失败,请检查登录状态或重试'); return null; } } // 修改播放动画函数 async function playDrawAnimation(count, mode) { currentDrawMode = mode; updateStatus(); videoContainer.style.display = 'flex'; skipBtn.style.display = 'block'; drawVideo.play(); // 在动画播放期间获取卡结果 const drawPromises = Array(count).fill().map(() => fetchCardFromServer()); const drawnCards = (await Promise.all(drawPromises)).filter(card => card !== null); // 设置视频结束后的回调 drawVideo.onended = function() { if (drawnCards.length > 0) { showResults(drawnCards); } else { videoContainer.style.display = 'none'; skipBtn.style.display = 'none'; } }; // 跳过按钮直接显示结果 skipBtn.onclick = function() { drawVideo.pause(); drawVideo.currentTime = 0; videoContainer.style.display = 'none'; skipBtn.style.display = 'none'; if (drawnCards.length > 0) { showResults(drawnCards); } }; } // 修改显示结果函数(保持原有UI逻辑) function showResults(drawnCards) { // 清空之前的卡片 cardsContainer.innerHTML = ''; // 生成结果标题 resultTitle.textContent = drawnCards.length === 1 ? '卡结果' : '十连结果'; // 生成新卡片(使用后端返回的数据) drawnCards.forEach((card, index) => { // 根据有度设置样式类 const rarityClass = card.rarity === 5 ? 'five-star' : card.rarity === 4 ? 'four-star' : 'three-star'; // 创建卡片包装容器 const cardWrapper = document.createElement('div'); cardWrapper.className = 'card-wrapper'; // 创建卡片元素 const cardElement = document.createElement('div'); cardElement.className = 'card'; // 卡片内部结构 cardElement.innerHTML = ` <div class="card-inner"> <div class="card-front"> <div class="card-image-container"> <img src="${card.image}" alt="${card.name}" class="card-image"> </div> </div> <div class="card-back"> <div class="card-back-content"></div> </div> </div> `; // 创建卡片外部信息容器 const cardInfo = document.createElement('div'); cardInfo.className = 'card-info'; cardInfo.innerHTML = ` <div class="card-name">${card.name}</div> <div class="card-rarity ${rarityClass}">${'★'.repeat(card.rarity)}</div> `; // 组装卡片 cardWrapper.appendChild(cardElement); cardWrapper.appendChild(cardInfo); cardsContainer.appendChild(cardWrapper); // 添加点击翻转功能 cardElement.addEventListener('click', function() { if (!this.classList.contains('flipped')) { this.classList.add('flipped'); cardWrapper.classList.add('flipped'); } }); }); // 添加到历史记录 drawHistory = [...drawHistory, ...drawnCards]; totalDraws += drawnCards.length; drawCounter.textContent = totalDraws; // 更新历史记录显示 updateHistoryDisplay(); // 显示结果模态框 resultModal.style.display = 'flex'; } // 修改事件监听部分(保持原有按钮逻辑) drawOneBtn.addEventListener('click', async () => { drawOneBtn.classList.add('active'); drawTenBtn.classList.remove('active'); await playDrawAnimation(1, 'single'); }); drawTenBtn.addEventListener('click', async () => { drawTenBtn.classList.add('active'); drawOneBtn.classList.remove('active'); await playDrawAnimation(10, 'multi'); }); // 保持其他原有代码不变... </script> ``` ### 关键修改说明 1. **卡牌映射系统**: - 创建 `cardMap` 对象将后端返回的代码(如 "S1")映射到前端卡牌数据 - 保持前端UI不变,包括星级、图片和名称显示 2. **异步请求处理**: - 使用 `fetch` API 调用 `/recordCardDraw` 接口 - 在动画播放期间并行发送请求,优化等待时间 - 错误处理包括网络错误和未登录情况 3. **动画与结果展示**: - 保持原有动画流程(播放视频->展示结果) - 跳过按钮功能保持不变 - 卡结果模态框展示方式不变 4. **历史记录同步**: - 使用后端返回的时间戳记录卡时间 - 历史记录表格展示方式保持不变 - 卡计数器功能保持不变 ### 注意事项 1. **会话管理**:后端通过session获取用户信息,前端请求会自动携带cookie 2. **错误处理**:当用户未登录时,后端返回{"status":"error"},前端会提示错误 3. **性能优化**: - 十连使用 `Promise.all` 并行发送请求 - 动画播放期间完成网络请求,减少用户等待时间 4. **兼容性**:使用现代JavaScript特性(async/await),支持所有主流浏览器 此修改完全遵循了不改变后端代码的要求,同时保持了前端所有原有功能不变,包括卡动画、历史记录展示和UI交互效果。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值