SuperPlan(6)TaoBao Winner - UI log4javascript

本文介绍如何使用Log4javascript进行JavaScript日志管理,并与Backbone框架结合使用,包括配置、日志级别、日志输出及与浏览器控制台的交互。通过配置文件实现日志级别和输出方式的灵活调整,确保开发过程中的错误信息清晰可见。
SuperPlan(6)TaoBao Winner - UI log4javascript

10. log4javascript
10.1 Introduction
From my understanding, it is a good way to take care of the javascript Log. It should be better than the console.log.
We have this kind of log Level
log.trace(message[, message2, … ] [, exception])
log.debug(message[, message2, …] [, exception])
log.info
log.error
log.fatal

Some simple examples
log.info("Hello, sillycat");

try{
throw new Error("Faking error message.");
}catch(e){
log.error("An error occurred", e);
}

var a = "hello",
var b = 3;
log.debug(a, b);

Logging an Object will directly output an JSON format
var obj = new Object();
obj.name = "hello",
obj.age = 8;
log.info(obj);

10.2 Work it out with My Backbone framework and Chrome
In the Main.js, load the path of log4javascript and log4javascript_custom.
paths: {
jquery: '//cdnjs.cloudflare.com/ajax/libs/jquery/1.8.2/jquery.min',
underscore: '//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.4.1/underscore-min',
backbone: '//cdnjs.cloudflare.com/ajax/libs/backbone.js/0.9.2/backbone-min',
text: '//cdnjs.cloudflare.com/ajax/libs/require-text/2.0.5/text',
async: 'lib/require/async',
log4javascript: '//cdnjs.cloudflare.com/ajax/libs/log4javascript/1.4.3/log4javascript',
json2: '//cdnjs.cloudflare.com/ajax/libs/json2/20121008/json2',
log4javascript_custom: 'lib/log4javascript/log4javascript_custom'

}

Using and initialize the Log4j class I have created.
require([
'Router',
'Config',
'Log4j',
'json2'
], function(Router, Config, Log4j){
new Log4j().init();
var IndexPageView = Backbone.View.extend({ el: "#content", router: null, initialize: function(){ window.logger.debug("initialize the IndexPageView and whole system.", "Good Luck."); window.logger.info("Config Start=================================="); window.logger.info(JSON.stringify(new Config())); window.logger.info("Config End===================================="); this.router = new Router(); } }); new IndexPageView();

});

The most magic configuration javascript class Log4j.js will be as follow:


define([
'jquery',
'Config',
'log4javascript',
'log4javascript_custom'
], function($, Config) {
function Log4j(){
} Log4j.prototype.init = function(){ var config = new Config(); if(config.getLogEnable() == 1){ log4javascript.setEnabled(true); }else{ log4javascript.setEnabled(false); }
var logger = log4javascript.getLogger(); if(config.getLogLevel() == 'debug'){ logger.setLevel(log4javascript.Level.DEBUG); }elseif(config.getLogLevel() == 'all'){ logger.setLevel(log4javascript.Level.ALL); }elseif(config.getLogLevel() == 'info'){ logger.setLevel(log4javascript.Level.INFO); }elseif(config.getLogLevel() == 'error'){ logger.setLevel(log4javascript.Level.ERROR); }
var consoleAppender = new log4javascript.BrowserConsoleAppender(); //%d{HH:mm:ss,SSS} %l{s:l} %-5p - %m{1}%n //%d{HH:mm:ss} %-5p - %m%n var simpleLayout = new log4javascript.PatternLayout("%d{HH:mm:ss,SSS} %l{s:l} %-5p - %m %n "); consoleAppender.setLayout(simpleLayout); logger.addAppender(consoleAppender); window.logger = logger; }; return Log4j;

});

The Config.js is an idea that I want to place all the javascript configuration.


define([
'jquery'
], function($) {
function Config(){
this.logLevel = 'all'; //info, debug, all this.logEnable = 1; } Config.prototype.setLogLevel = function(logLevel){ this.logLevel = logLevel; }; Config.prototype.getLogLevel = function(){ return this.logLevel; };
Config.prototype.setLogEnable = function(logEnable){ this.logEnable = logEnalbe; };
Config.prototype.getLogEnable = function(){ return this.logEnable; }; return Config;

});

11. BackBone
come soon...

Tips:
1. Always show the line number in log4javascript
log4javascript.js:155

Solution:
Copy some implementation from log4javascript.js and create a file log4javascript_custom.js.

Change the method
define([
'log4javascript'
],function() {

log4javascript.PatternLayout.prototype.format = function(loggingEvent) {
…snip..
case"l": //Location var isChrome = navigator.userAgent.indexOf("Chrome") !== -1; if(isChrome){ //do someting else

var stack = new Error().stack;

…snip…


The detail is in the project easygooglemap/web/js/lib/log4javascript/log4javascript_custom.js.


After that, we will have the debug information like this
13:43:01,112 Router.js:20 DEBUG - init the router, and make backbone start.


References:
log4javascript
http://blog.youkuaiyun.com/realcsq/article/details/6695532
http://chenjc-it.iteye.com/blog/1574910
http://log4javascript.org/index.html

log4javascript line number
http://stackoverflow.com/questions/11308239/console-log-wrapper-that-keeps-line-numbers-and-supports-most-methods
https://gist.github.com/leoroos/3441763

BackBone
http://dailyjs.com/2012/11/29/backbone-tutorial-1/

http://www.youtube.com/watch?v=HsEw2i4wQMM
https://github.com/thomasdavis/backbonetutorials/tree/gh-pages/videos/beginner

http://backbonejs.org/#introduction

http://coenraets.org/blog/2012/02/sample-app-with-backbone-js-and-twitter-bootstrap/

http://stackoverflow.com/questions/9914952/backbone-js-complex-model-fetch

BackBone Router
https://github.com/JesterXL/Backbone-Router-Example/tree/master/code/src
package com.ruoyi.system.mapper; import java.util.List; import com.ruoyi.system.domain.HonorCertificate; /** * 荣誉证书Mapper接口 * * @author ruoyi * @date 2025-09-16 */ public interface HonorCertificateMapper { /** * 查询荣誉证书 * * @param id 荣誉证书主键 * @return 荣誉证书 */ public HonorCertificate selectHonorCertificateById(Long id); /** * 查询荣誉证书列表 * * @param honorCertificate 荣誉证书 * @return 荣誉证书集合 */ public List<HonorCertificate> selectHonorCertificateList(HonorCertificate honorCertificate); /** * 新增荣誉证书 * * @param honorCertificate 荣誉证书 * @return 结果 */ public int insertHonorCertificate(HonorCertificate honorCertificate); /** * 修改荣誉证书 * * @param honorCertificate 荣誉证书 * @return 结果 */ public int updateHonorCertificate(HonorCertificate honorCertificate); /** * 删除荣誉证书 * * @param id 荣誉证书主键 * @return 结果 */ public int deleteHonorCertificateById(Long id); /** * 批量删除荣誉证书 * * @param ids 需要删除的数据主键集合 * @return 结果 */ public int deleteHonorCertificateByIds(String[] ids); } package com.ruoyi.system.domain; import java.util.Date; import com.fasterxml.jackson.annotation.JsonFormat; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; import com.ruoyi.common.annotation.Excel; import com.ruoyi.common.core.domain.BaseEntity; /** * 荣誉证书对象 honor_certificate * * @author ruoyi * @date 2025-09-16 */ public class HonorCertificate extends BaseEntity { private static final long serialVersionUID = 1L; /** 主键ID */ private Long id; /** 类别 */ @Excel(name = "类别") private String category; /** 荣誉 */ @Excel(name = "荣誉") private String honor; /** 获得时间 */ @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8") @Excel(name = "获得时间", width = 30, dateFormat = "yyyy-MM-dd") private Date obtainTime; /** 授予单位 */ @Excel(name = "授予单位") private String awardingUnit; /** 获奖人 */ @Excel(name = "获奖人") private String winner; /** 附件(存储文件路径) */ @Excel(name = "附件", readConverterExp = "存储文件路径") private String attachment; public void setId(Long id) { this.id = id; } public Long getId() { return id; } public void setCategory(String category) { this.category = category; } public String getCategory() { return category; } public void setHonor(String honor) { this.honor = honor; } public String getHonor() { return honor; } public void setObtainTime(Date obtainTime) { this.obtainTime = obtainTime; } public Date getObtainTime() { return obtainTime; } public void setAwardingUnit(String awardingUnit) { this.awardingUnit = awardingUnit; } public String getAwardingUnit() { return awardingUnit; } public void setWinner(String winner) { this.winner = winner; } public String getWinner() { return winner; } public void setAttachment(String attachment) { this.attachment = attachment; } public String getAttachment() { return attachment; } @Override public String toString() { return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) .append("id", getId()) .append("category", getCategory()) .append("honor", getHonor()) .append("obtainTime", getObtainTime()) .append("awardingUnit", getAwardingUnit()) .append("winner", getWinner()) .append("attachment", getAttachment()) .toString(); } } package com.ruoyi.system.service; import java.util.List; import com.ruoyi.system.domain.HonorCertificate; /** * 荣誉证书Service接口 * * @author ruoyi * @date 2025-09-16 */ public interface IHonorCertificateService { /** * 查询荣誉证书 * * @param id 荣誉证书主键 * @return 荣誉证书 */ public HonorCertificate selectHonorCertificateById(Long id); /** * 查询荣誉证书列表 * * @param honorCertificate 荣誉证书 * @return 荣誉证书集合 */ public List<HonorCertificate> selectHonorCertificateList(HonorCertificate honorCertificate); /** * 新增荣誉证书 * * @param honorCertificate 荣誉证书 * @return 结果 */ public int insertHonorCertificate(HonorCertificate honorCertificate); /** * 修改荣誉证书 * * @param honorCertificate 荣誉证书 * @return 结果 */ public int updateHonorCertificate(HonorCertificate honorCertificate); /** * 批量删除荣誉证书 * * @param ids 需要删除的荣誉证书主键集合 * @return 结果 */ public int deleteHonorCertificateByIds(String ids); /** * 删除荣誉证书信息 * * @param id 荣誉证书主键 * @return 结果 */ public int deleteHonorCertificateById(Long id); } package com.ruoyi.system.service.impl; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.ruoyi.system.mapper.HonorCertificateMapper; import com.ruoyi.system.domain.HonorCertificate; import com.ruoyi.system.service.IHonorCertificateService; import com.ruoyi.common.core.text.Convert; /** * 荣誉证书Service业务层处理 * * @author ruoyi * @date 2025-09-16 */ @Service public class HonorCertificateServiceImpl implements IHonorCertificateService { @Autowired private HonorCertificateMapper honorCertificateMapper; /** * 查询荣誉证书 * * @param id 荣誉证书主键 * @return 荣誉证书 */ @Override public HonorCertificate selectHonorCertificateById(Long id) { return honorCertificateMapper.selectHonorCertificateById(id); } /** * 查询荣誉证书列表 * * @param honorCertificate 荣誉证书 * @return 荣誉证书 */ @Override public List<HonorCertificate> selectHonorCertificateList(HonorCertificate honorCertificate) { return honorCertificateMapper.selectHonorCertificateList(honorCertificate); } /** * 新增荣誉证书 * * @param honorCertificate 荣誉证书 * @return 结果 */ @Override public int insertHonorCertificate(HonorCertificate honorCertificate) { return honorCertificateMapper.insertHonorCertificate(honorCertificate); } /** * 修改荣誉证书 * * @param honorCertificate 荣誉证书 * @return 结果 */ @Override public int updateHonorCertificate(HonorCertificate honorCertificate) { return honorCertificateMapper.updateHonorCertificate(honorCertificate); } /** * 批量删除荣誉证书 * * @param ids 需要删除的荣誉证书主键 * @return 结果 */ @Override public int deleteHonorCertificateByIds(String ids) { return honorCertificateMapper.deleteHonorCertificateByIds(Convert.toStrArray(ids)); } /** * 删除荣誉证书信息 * * @param id 荣誉证书主键 * @return 结果 */ @Override public int deleteHonorCertificateById(Long id) { return honorCertificateMapper.deleteHonorCertificateById(id); } } package com.ruoyi.web.controller.system; import java.io.IOException; import java.util.Arrays; import java.util.List; import java.util.ArrayList; import com.ruoyi.common.config.RuoYiConfig; import com.ruoyi.common.utils.file.FileUploadUtils; import com.ruoyi.common.utils.file.FileUtils; import org.apache.shiro.authz.annotation.RequiresPermissions; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; import org.springframework.web.bind.annotation.*; import com.ruoyi.common.annotation.Log; import com.ruoyi.common.enums.BusinessType; import com.ruoyi.system.domain.HonorCertificate; import com.ruoyi.system.service.IHonorCertificateService; import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.utils.poi.ExcelUtil; import com.ruoyi.common.core.page.TableDataInfo; import org.springframework.web.multipart.MultipartFile; import javax.servlet.http.HttpServletResponse; /** * 荣誉证书Controller * * @author ruoyi * @date 2025-09-16 */ @Controller @RequestMapping("/system/certificate") public class HonorCertificateController extends BaseController { private String prefix = "system/certificate"; // 支持的文件类型 private static final List<String> ALLOWED_EXTENSIONS = new ArrayList<>(Arrays.asList( ".pdf", ".jpg", ".jpeg", ".png", ".gif" )); @Autowired private IHonorCertificateService honorCertificateService; @RequiresPermissions("system:certificate:view") @GetMapping() public String certificate() { return prefix + "/certificate"; } /** * 查询荣誉证书列表 */ @RequiresPermissions("system:certificate:list") @PostMapping("/list") @ResponseBody public TableDataInfo list(HonorCertificate honorCertificate) { startPage(); List<HonorCertificate> list = honorCertificateService.selectHonorCertificateList(honorCertificate); return getDataTable(list); } /** * 导出荣誉证书列表 */ @RequiresPermissions("system:certificate:export") @Log(title = "荣誉证书", businessType = BusinessType.EXPORT) @PostMapping("/export") @ResponseBody public AjaxResult export(HonorCertificate honorCertificate) { List<HonorCertificate> list = honorCertificateService.selectHonorCertificateList(honorCertificate); ExcelUtil<HonorCertificate> util = new ExcelUtil<HonorCertificate>(HonorCertificate.class); return util.exportExcel(list, "荣誉证书数据"); } /** * 新增荣誉证书 */ @RequiresPermissions("system:certificate:add") @GetMapping("/add") public String add() { return prefix + "/add"; } /** * 新增保存荣誉证书 */ @RequiresPermissions("system:certificate:add") @Log(title = "荣誉证书", businessType = BusinessType.INSERT) @PostMapping("/add") @ResponseBody public AjaxResult addSave(HonorCertificate honorCertificate) { return toAjax(honorCertificateService.insertHonorCertificate(honorCertificate)); } /** * 修改荣誉证书 */ @RequiresPermissions("system:certificate:edit") @GetMapping("/edit/{id}") public String edit(@PathVariable("id") Long id, ModelMap mmap) { HonorCertificate honorCertificate = honorCertificateService.selectHonorCertificateById(id); mmap.put("honorCertificate", honorCertificate); return prefix + "/edit"; } /** * 上传文件(支持PDF和图片) */ // @RequiresPermissions("system:certificate:add") // @PostMapping("/uploadFile") // @ResponseBody // public AjaxResult uploadFile(MultipartFile file) { // try { // if (file.isEmpty()) { // return error("上传文件不能为空"); // } // // // 检查文件类型 // String fileName = file.getOriginalFilename(); // String fileExt = fileName.substring(fileName.lastIndexOf(".")).toLowerCase(); // // if (!ALLOWED_EXTENSIONS.contains(fileExt)) { // return error("请上传以下格式的文件:pdf、jpg、jpeg、png、gif"); // } // // // 保存文件到指定目录 // String filePath = FileUploadUtils.upload(RuoYiConfig.getUploadPath(), file); // // // 返回文件路径和原文件名 // return AjaxResult.success() // .put("filePath", filePath) // .put("fileName", fileName); // } catch (Exception e) { // return error("上传失败:" + e.getMessage()); // } // } // // /** // * 下载文件 // */ // @RequiresPermissions("system:certificate:view") // @GetMapping("/downloadFile") // public void downloadFile(@RequestParam("path") String path, // @RequestParam("fileName") String fileName, // HttpServletResponse response) throws IOException { // try { // // 拼接完整文件路径 // String filePath = RuoYiConfig.getUploadPath() + path; // // // 设置响应头,指定文件名 // response.setHeader("Content-Disposition", "attachment;filename=" + // java.net.URLEncoder.encode(fileName, "UTF-8")); // // // 调用工具类实现文件下载 // FileUtils.downloadFile(filePath, response); // } catch (Exception e) { // e.printStackTrace(); // response.getWriter().write("文件下载失败:" + e.getMessage()); // } // } /** * 修改保存荣誉证书 */ @RequiresPermissions("system:certificate:edit") @Log(title = "荣誉证书", businessType = BusinessType.UPDATE) @PostMapping("/edit") @ResponseBody public AjaxResult editSave(HonorCertificate honorCertificate) { return toAjax(honorCertificateService.updateHonorCertificate(honorCertificate)); } /** * 删除荣誉证书 */ @RequiresPermissions("system:certificate:remove") @Log(title = "荣誉证书", businessType = BusinessType.DELETE) @PostMapping( "/remove") @ResponseBody public AjaxResult remove(String ids) { return toAjax(honorCertificateService.deleteHonorCertificateByIds(ids)); } } <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.ruoyi.system.mapper.HonorCertificateMapper"> <resultMap type="HonorCertificate" id="HonorCertificateResult"> <result property="id" column="id" /> <result property="category" column="category" /> <result property="honor" column="honor" /> <result property="obtainTime" column="obtain_time" /> <result property="awardingUnit" column="awarding_unit" /> <result property="winner" column="winner" /> <result property="attachment" column="attachment" /> </resultMap> <sql id="selectHonorCertificateVo"> select id, category, honor, obtain_time, awarding_unit, winner, attachment from honor_certificate </sql> <select id="selectHonorCertificateList" parameterType="HonorCertificate" resultMap="HonorCertificateResult"> <include refid="selectHonorCertificateVo"/> <where> <if test="category != null and category != ''"> and category = #{category}</if> <if test="honor != null and honor != ''"> and honor = #{honor}</if> <if test="obtainTime != null "> and obtain_time = #{obtainTime}</if> <if test="awardingUnit != null and awardingUnit != ''"> and awarding_unit = #{awardingUnit}</if> <if test="winner != null and winner != ''"> and winner = #{winner}</if> <if test="attachment != null and attachment != ''"> and attachment = #{attachment}</if> </where> </select> <select id="selectHonorCertificateById" parameterType="Long" resultMap="HonorCertificateResult"> <include refid="selectHonorCertificateVo"/> where id = #{id} </select> <insert id="insertHonorCertificate" parameterType="HonorCertificate" useGeneratedKeys="true" keyProperty="id"> insert into honor_certificate <trim prefix="(" suffix=")" suffixOverrides=","> <if test="category != null">category,</if> <if test="honor != null">honor,</if> <if test="obtainTime != null">obtain_time,</if> <if test="awardingUnit != null">awarding_unit,</if> <if test="winner != null">winner,</if> <if test="attachment != null">attachment,</if> </trim> <trim prefix="values (" suffix=")" suffixOverrides=","> <if test="category != null">#{category},</if> <if test="honor != null">#{honor},</if> <if test="obtainTime != null">#{obtainTime},</if> <if test="awardingUnit != null">#{awardingUnit},</if> <if test="winner != null">#{winner},</if> <if test="attachment != null">#{attachment},</if> </trim> </insert> <update id="updateHonorCertificate" parameterType="HonorCertificate"> update honor_certificate <trim prefix="SET" suffixOverrides=","> <if test="category != null">category = #{category},</if> <if test="honor != null">honor = #{honor},</if> <if test="obtainTime != null">obtain_time = #{obtainTime},</if> <if test="awardingUnit != null">awarding_unit = #{awardingUnit},</if> <if test="winner != null">winner = #{winner},</if> <if test="attachment != null">attachment = #{attachment},</if> </trim> where id = #{id} </update> <delete id="deleteHonorCertificateById" parameterType="Long"> delete from honor_certificate where id = #{id} </delete> <delete id="deleteHonorCertificateByIds" parameterType="String"> delete from honor_certificate where id in <foreach item="id" collection="array" open="(" separator="," close=")"> #{id} </foreach> </delete> </mapper><!DOCTYPE html> <html lang="zh" xmlns:th="http://www.thymeleaf.org" > <head> <th:block th:include="include :: header('新增荣誉证书')" /> <th:block th:include="include :: datetimepicker-css" /> </head> <body class="white-bg"> <div class="wrapper wrapper-content animated fadeInRight ibox-content"> <form class="form-horizontal m" id="form-certificate-add"> <div class="col-xs-12"> <div class="form-group"> <label class="col-sm-3 control-label">类别:</label> <div class="col-sm-8"> <input name="category" class="form-control" type="text"> </div> </div> </div> <div class="col-xs-12"> <div class="form-group"> <label class="col-sm-3 control-label">荣誉:</label> <div class="col-sm-8"> <input name="honor" class="form-control" type="text"> </div> </div> </div> <div class="col-xs-12"> <div class="form-group"> <label class="col-sm-3 control-label">获得时间:</label> <div class="col-sm-8"> <div class="input-group date"> <input name="obtainTime" class="form-control" placeholder="yyyy-MM-dd" type="text"> <span class="input-group-addon"><i class="fa fa-calendar"></i></span> </div> </div> </div> </div> <div class="col-xs-12"> <div class="form-group"> <label class="col-sm-3 control-label">授予单位:</label> <div class="col-sm-8"> <input name="awardingUnit" class="form-control" type="text"> </div> </div> </div> <div class="col-xs-12"> <div class="form-group"> <label class="col-sm-3 control-label">获奖人:</label> <div class="col-sm-8"> <input name="winner" class="form-control" type="text"> </div> </div> </div> <div class="col-xs-12"> <div class="form-group"> <label class="col-sm-3 control-label">附件:</label> <div class="col-sm-8"> <input type="file" name="file" id="file" accept=".pdf,.jpg,.jpeg,.png,.gif" /> <p class="help-block">支持上传PDF、JPG、PNG格式文件</p> <!-- 用于显示已上传的文件信息 --> <div id="fileInfo" style="margin-top:10px;"></div> <!-- <input name="attachment" th:field="*{attachment}" class="form-control" type="text">--> </div> </div> </div> </form> </div> <th:block th:include="include :: footer" /> <th:block th:include="include :: datetimepicker-js" /> <script th:inline="javascript"> var prefix = ctx + "system/certificate" $("#form-certificate-add").validate({ focusCleanup: true }); function submitHandler() { if ($.validate.form()) { $.operate.save(prefix + "/add", $('#form-certificate-add').serialize()); } } $("input[name='obtainTime']").datetimepicker({ format: "yyyy-mm-dd", minView: "month", autoclose: true }); </script> </body> </html><!DOCTYPE html> <html lang="zh" xmlns:th="http://www.thymeleaf.org" > <head> <th:block th:include="include :: header('修改荣誉证书')" /> <th:block th:include="include :: datetimepicker-css" /> </head> <body class="white-bg"> <div class="wrapper wrapper-content animated fadeInRight ibox-content"> <form class="form-horizontal m" id="form-certificate-edit" th:object="${honorCertificate}"> <input name="id" th:field="*{id}" type="hidden"> <div class="col-xs-12"> <div class="form-group"> <label class="col-sm-3 control-label">类别:</label> <div class="col-sm-8"> <input name="category" th:field="*{category}" class="form-control" type="text"> </div> </div> </div> <div class="col-xs-12"> <div class="form-group"> <label class="col-sm-3 control-label">荣誉:</label> <div class="col-sm-8"> <input name="honor" th:field="*{honor}" class="form-control" type="text"> </div> </div> </div> <div class="col-xs-12"> <div class="form-group"> <label class="col-sm-3 control-label">获得时间:</label> <div class="col-sm-8"> <div class="input-group date"> <input name="obtainTime" th:value="${#dates.format(honorCertificate.obtainTime, 'yyyy-MM-dd')}" class="form-control" placeholder="yyyy-MM-dd" type="text"> <span class="input-group-addon"><i class="fa fa-calendar"></i></span> </div> </div> </div> </div> <div class="col-xs-12"> <div class="form-group"> <label class="col-sm-3 control-label">授予单位:</label> <div class="col-sm-8"> <input name="awardingUnit" th:field="*{awardingUnit}" class="form-control" type="text"> </div> </div> </div> <div class="col-xs-12"> <div class="form-group"> <label class="col-sm-3 control-label">获奖人:</label> <div class="col-sm-8"> <input name="winner" th:field="*{winner}" class="form-control" type="text"> </div> </div> </div> <div class="col-xs-12"> <div class="form-group"> <label class="col-sm-3 control-label">附件:</label> <div class="col-sm-8"> <input type="file" name="file" id="file" accept=".pdf,.jpg,.jpeg,.png,.gif" /> <p class="help-block">支持上传PDF、JPG、PNG格式文件</p> <!-- 用于显示已上传的文件信息 --> <div id="fileInfo" style="margin-top:10px;"></div> <!-- <input name="attachment" th:field="*{attachment}" class="form-control" type="text">--> </div> </div> </div> </form> </div> <th:block th:include="include :: footer" /> <th:block th:include="include :: datetimepicker-js" /> <script th:inline="javascript"> var prefix = ctx + "system/certificate"; $("#form-certificate-edit").validate({ focusCleanup: true }); function submitHandler() { if ($.validate.form()) { $.operate.save(prefix + "/edit", $('#form-certificate-edit').serialize()); } } $("input[name='obtainTime']").datetimepicker({ format: "yyyy-mm-dd", minView: "month", autoclose: true }); </script> <script th:inline="javascript"> $(function () { // 监听文件选择,异步上传 $("#attachmentFile").change(function () { var file = this.files[0]; if (!file) return; var formData = new FormData(); formData.append("file", file); $.ajax({ url: ctx + "system/certificate/upload", type: "post", data: formData, processData: false, contentType: false, dataType: "json", success: function (result) { if (result.code === 200) { // 显示上传成功信息,并存储路径到隐藏域 $("#fileInfo").html("已上传:" + file.name); $("#attachment").val(result.filePath); } else { $.modal.alertError(result.msg); } }, error: function () { $.modal.alertError("附件上传失败"); } }); }); // 表单提交(若依默认生成,需确保包含 attachment 字段) $("#form-certificate-add").validate({ submitHandler: function (form) { // 若依默认提交逻辑,会将 hidden 的 attachment 字段一并提交 $.operate.save(form, ctx + "system/certificate/add"); } }); }); </script> </body> </html><!DOCTYPE html> <html lang="zh" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro"> <head> <th:block th:include="include :: header('荣誉证书列表')" /> </head> <body class="gray-bg"> <div class="container-div"> <div class="row"> <div class="col-sm-12 search-collapse"> <form id="formId"> <div class="select-list"> <ul> <li> <label>类别:</label> <input type="text" name="category"/> </li> <li> <label>荣誉:</label> <input type="text" name="honor"/> </li> <li> <label>获得时间:</label> <input type="text" class="time-input" placeholder="请选择获得时间" name="obtainTime"/> </li> <li> <label>授予单位:</label> <input type="text" name="awardingUnit"/> </li> <li> <label>获奖人:</label> <input type="text" name="winner"/> </li> <li> <label>附件:</label> <input type="text" name="attachment"/> </li> <li> <a class="btn btn-primary btn-rounded btn-sm" onclick="$.table.search()"><i class="fa fa-search"></i> 搜索</a> <a class="btn btn-warning btn-rounded btn-sm" onclick="$.form.reset()"><i class="fa fa-refresh"></i> 重置</a> </li> </ul> </div> </form> </div> <div class="btn-group-sm" id="toolbar" role="group"> <a class="btn btn-success" onclick="$.operate.add()" shiro:hasPermission="system:certificate:add"> <i class="fa fa-plus"></i> 添加 </a> <a class="btn btn-primary single disabled" onclick="$.operate.edit()" shiro:hasPermission="system:certificate:edit"> <i class="fa fa-edit"></i> 修改 </a> <a class="btn btn-danger multiple disabled" onclick="$.operate.removeAll()" shiro:hasPermission="system:certificate:remove"> <i class="fa fa-remove"></i> 删除 </a> <a class="btn btn-warning" onclick="$.table.exportExcel()" shiro:hasPermission="system:certificate:export"> <i class="fa fa-download"></i> 导出 </a> </div> <div class="col-sm-12 select-table table-striped"> <table id="bootstrap-table"></table> </div> </div> </div> <th:block th:include="include :: footer" /> <script th:inline="javascript"> var editFlag = [[${@permission.hasPermi('system:certificate:edit')}]]; var removeFlag = [[${@permission.hasPermi('system:certificate:remove')}]]; var prefix = ctx + "system/certificate"; $(function() { var options = { url: prefix + "/list", createUrl: prefix + "/add", updateUrl: prefix + "/edit/{id}", removeUrl: prefix + "/remove", exportUrl: prefix + "/export", modalName: "荣誉证书", columns: [{ checkbox: true }, { field: 'id', title: '主键ID', visible: false }, { field: 'category', title: '类别' }, { field: 'honor', title: '荣誉' }, { field: 'obtainTime', title: '获得时间' }, { field: 'awardingUnit', title: '授予单位' }, { field: 'winner', title: '获奖人' }, { field: 'attachment', title: '附件' }, { title: '操作', align: 'center', formatter: function(value, row, index) { var actions = []; actions.push('<a class="btn btn-success btn-xs ' + editFlag + '" href="javascript:void(0)" onclick="$.operate.edit(\'' + row.id + '\')"><i class="fa fa-edit"></i>编辑</a> '); actions.push('<a class="btn btn-danger btn-xs ' + removeFlag + '" href="javascript:void(0)" onclick="$.operate.remove(\'' + row.id + '\')"><i class="fa fa-remove"></i>删除</a>'); return actions.join(''); } }] }; $.table.init(options); }); </script> </body> </html>上述是公司荣誉模块的代码,这是一个若依前后端不分离的项目,请根据上述代码,完成附件的上传下载功能,附件包括PDF或图片,若没有上传附件则显示无附件,请告诉我在哪里添加代码,而且保证若依项目能够运行
09-18
<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"/> <title>🤖 AI大乱斗 3.0 - 智能战场革命</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { background: #0a0f1a; color: #cdd; font-family: 'Segoe UI', sans-serif; text-align: center; padding: 20px; } h1 { font-size: 2.8em; margin: 15px 0; color: #aaccff; text-shadow: 0 0 20px rgba(100, 180, 255, 0.6); } .intro { color: #9ab; max-width: 900px; margin: 0 auto 20px; font-size: 1.1em; } /* 配置面板 */ .config-panel { width: 90%; max-width: 1000px; margin: 20px auto; padding: 15px; background: rgba(30, 40, 60, 0.6); border: 1px solid #335; border-radius: 10px; text-align: left; display: grid; grid-template-columns: repeat(auto-fit, minmax(480px, 1fr)); gap: 15px; } .char-config { padding: 15px; background: rgba(20, 30, 50, 0.5); border-radius: 8px; border: 1px solid #446; } .char-config h3 { color: #bbd; margin-bottom: 10px; } .slider-group { margin: 8px 0; display: flex; align-items: center; } .slider-group label { width: 60px; text-align: right; margin-right: 10px; font-size: 0.95em; } input[type="range"] { flex: 1; } span.value { width: 40px; text-align: center; font-weight: bold; color: #8ac; } .points-left { font-size: 0.95em; color: #faa; margin: 5px 0; font-weight: bold; } button.start { background: linear-gradient(to bottom, #4a6fa5, #3a5a80); color: white; border: none; padding: 14px 24px; font-size: 1.1em; font-weight: bold; cursor: pointer; border-radius: 8px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); } button.reset { background: linear-gradient(to bottom, #8a4f4f, #7a3f3f); color: white; border: none; padding: 12px 20px; font-size: 1em; margin: 10px 5px; cursor: pointer; border-radius: 6px; } /* 决斗场 */ #arena { width: 90vw; height: 60vh; max-width: 1000px; margin: 20px auto; border: 2px solid #335577; position: relative; background: radial-gradient(circle at center, #121828 0%, #0a0f1a 80%); box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.8), 0 0 20px rgba(60, 100, 180, 0.2); overflow: hidden; border-radius: 12px; } .character { position: absolute; width: 56px; height: 56px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 1.1em; color: white; text-shadow: 1px 1px 2px black; user-select: none; transition: transform 0.1s ease; box-shadow: 0 0 10px currentColor; z-index: 2; } .hp-bar-outer { position: absolute; top: -18px; left: 0; width: 56px; height: 6px; background: rgba(0, 0, 0, 0.6); border-radius: 3px; overflow: hidden; } .hp-bar-inner { height: 100%; width: 100%; background: linear-gradient(90deg, #50c878, #41a05a); transition: width 0.3s ease; } .status-tag { position: absolute; bottom: -18px; left: 0; width: 56px; font-size: 0.7em; text-align: center; white-space: nowrap; color: #ddd; text-shadow: 1px 1px 1px black; font-weight: bold; } .buff-point { position: absolute; width: 30px; height: 30px; border: 2px solid; border-radius: 50%; animation: pulse 1.5s infinite alternate; z-index: 1; box-shadow: 0 0 10px currentColor; } .buff-health { background: #00ff88; border-color: #00ff88; } .buff-attack { background: #ff4444; border-color: #ff4444; } .buff-speed { background: #44aaff; border-color: #44aaff; } @keyframes pulse { 0% { transform: scale(1); opacity: 0.8; } 100% { transform: scale(1.2); opacity: 1; } } .explosion { position: absolute; width: 80px; height: 80px; background: radial-gradient(circle, #ffd700, #ff4500, transparent); border-radius: 50%; animation: explode 0.6s ease-out forwards; pointer-events: none; z-index: 10; } @keyframes explode { 0% { transform: scale(0); opacity: 1; } 100% { transform: scale(1.5); opacity: 0; } } #log { width: 90%; max-width: 1000px; max-height: 140px; margin: 15px auto; padding: 12px; background: rgba(20, 30, 40, 0.6); border: 1px solid #335; color: #ccc; font-family: 'Courier New', monospace; font-size: 0.95em; overflow-y: auto; text-align: left; white-space: pre-line; border-radius: 8px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); } #stats { width: 90%; max-width: 1000px; margin: 15px auto; padding: 12px; background: rgba(20, 30, 40, 0.6); border: 1px solid #335; color: #ccc; font-size: 0.95em; border-radius: 8px; text-align: left; } .controls { margin: 20px auto; max-width: 800px; display: flex; justify-content: center; gap: 12px; } </style> </head> <body> <h1>🤖 AI大乱斗 3.0 - 智能战场革命</h1> <p class="intro">无障·全自动·多策略·防卡死·可视化。AI会思考、会逃跑、会抢包、会走位!</p> <!-- 配置系统 --> <div class="config-panel"> <script> const chars = [ { name: "董", color: "#e57373", hp: 50, atk: 30, spd: 20 }, { name: "邱", color: "#64b5f6", hp: 50, atk: 30, spd: 20 }, { name: "郭", color: "#ffb74d", hp: 50, atk: 30, spd: 20 }, { name: "陆", color: "#ba68c8", hp: 50, atk: 30, spd: 20 } ]; function createConfig() { const panel = document.querySelector('.config-panel'); panel.innerHTML = ''; chars.forEach(char => { const div = document.createElement('div'); div.className = 'char-config'; div.innerHTML = ` <h3><span style="color:${char.color}">${char.name}</span> - 分配100技能点</h3> <div class="points-left" id="points-${char.name}">剩余: 0</div> <div class="slider-group"> <label>血量</label> <input type="range" min="30" max="100" value="${char.hp}" step="1" oninput="updateStat('${char.name}', 'hp', this.value)"> <span id="hp-${char.name}" class="value">${char.hp}</span> </div> <div class="slider-group"> <label>攻击</label> <input type="range" min="10" max="60" value="${char.atk}" step="1" oninput="updateStat('${char.name}', 'atk', this.value)"> <span id="atk-${char.name}" class="value">${char.atk}</span> </div> <div class="slider-group"> <label>速度</label> <input type="range" min="5" max="40" value="${char.spd}" step="1" oninput="updateStat('${char.name}', 'spd', this.value)"> <span id="spd-${char.name}" class="value">${char.spd}</span> </div> `; panel.appendChild(div); }); updateAllPoints(); } function updateStat(name, stat, value) { const c = chars.find(x => x.name === name); const oldTotal = c.hp + c.atk + c.spd; c[stat] = parseInt(value); const newTotal = c.hp + c.atk + c.spd; if (newTotal > 100) { c[stat] -= (newTotal - 100); document.querySelector(`[oninput*="'${name}', '${stat}'"]`).value = c[stat]; } document.getElementById(`${stat}-${name}`).textContent = c[stat]; updateAllPoints(); } function updateAllPoints() { chars.forEach(c => { const total = c.hp + c.atk + c.spd; const el = document.getElementById(`points-${c.name}`); el.textContent = `剩余: ${100 - total}`; el.style.color = total === 100 ? '#8f8' : total < 100 ? '#faa' : '#f88'; }); } window.onload = createConfig; </script> </div> <div class="controls"> <button class="start" onclick="startGame()">▶️ 开启智能战场</button> <button class="reset" onclick="location.reload()">🔄 重置配置</button> </div> <!-- 决斗场 --> <div id="arena"></div> <div id="log">【系统】请为每位AI分配100技能点,然后点击开始。</div> <div id="stats">🏆 胜率统计:<br>董: 0胜|邱: 0胜|郭: 0胜|陆: 0胜</div> <!-- 主逻辑 --> <script> let arena = document.getElementById("arena"); let log = document.getElementById("log"); let statsEl = document.getElementById("stats"); let gameRunning = false; let characters = {}; let buffPoints = []; let winCount = { 董: 0, 邱: 0, 郭: 0, 陆: 0 }; // 属性成长(非线性) function calcHp(raw) { return 50 + Math.floor(50 * (raw / 100) ** 0.8) * 2; } function calcAtk(raw) { return 10 + Math.floor(50 * (raw / 100) ** 0.7); } function calcSpd(raw) { return 1.0 + parseFloat(((raw / 100) ** 0.6 * 3.0).toFixed(2)); } function resetArena() { cancelAnimationFrame(window.gameLoopId); arena.innerHTML = ""; characters = {}; buffPoints = []; gameRunning = false; } function logMessage(msg) { log.innerHTML += msg + "\n"; log.scrollTop = log.scrollHeight; } function distance(a, b) { return Math.hypot(a.x - b.x, a.y - b.y); } function spawnBuffPoint(type = 'health') { const x = 50 + Math.random() * (arena.clientWidth - 100); const y = 50 + Math.random() * (arena.clientHeight - 100); const el = document.createElement("div"); el.className = `buff-point buff-${type}`; el.dataset.type = type; el.style.left = `${x - 15}px`; el.style.top = `${y - 15}px`; arena.appendChild(el); const point = { x, y, el, type }; buffPoints.push(point); setTimeout(() => { if (el.parentElement) el.remove(); buffPoints = buffPoints.filter(p => p !== point); }, 8000); } function findNearestBuff(character) { return buffPoints.reduce((closest, p) => { const d1 = closest ? distance(character, closest) : Infinity; const d2 = distance(character, p); return d2 < d1 ? p : closest; }, null); } function createCharacter(config) { let x = 100 + Math.random() * (arena.clientWidth - 200); let y = 100 + Math.random() * (arena.clientHeight - 200); const finalHp = calcHp(config.hp); const finalAtk = calcAtk(config.atk); const finalSpd = calcSpd(config.spd); const el = document.createElement("div"); el.className = "character"; el.innerHTML = ` <div>${config.name}</div> <div class="hp-bar-outer"><div class="hp-bar-inner" id="hp-ui-${config.name}"></div></div> <div class="status-tag" id="tag-${config.name}">待命</div> `; el.style.backgroundColor = config.color; el.id = "char-" + config.name; el.style.transform = `translate(${x}px, ${y}px)`; arena.appendChild(el); characters[config.name] = { element: el, data: config, x, y, hp: finalHp, maxHp: finalHp, atk: finalAtk, spd: finalSpd, moveSpeed: finalSpd, hate: {}, lastAttack: 0, strategy: 'idle', target: null, lastPos: { x, y }, stuckTimer: 0, aiBrain: new AIBrain(config.name) }; logMessage(`⚙️ 【${config.name}】生成: 血${finalHp} 攻${finalAtk} 速${finalSpd.toFixed(2)} (${config.hp}/${config.atk}/${config.spd})`); updateHealth(config.name); } function updateHealth(name) { const c = characters[name]; const bar = document.getElementById(`hp-ui-${name}`); if (bar && c) { const ratio = c.hp / c.maxHp; bar.style.width = `${Math.max(ratio * 100, 0)}%`; bar.style.backgroundColor = ratio > 0.6 ? '#50c878' : ratio > 0.3 ? '#ffbb33' : '#ff4444'; } } function updateStrategyTag(name, strategy) { const tag = document.getElementById(`tag-${name}`); if (tag) { tag.textContent = strategy; tag.style.color = { 'attack': '#f88', 'flee': '#8f8', 'heal': '#0f0', 'speed': '#8af', 'chase': '#fa0' }[strategy] || '#ccc'; } } function addExplosion(x, y) { const explosion = document.createElement("div"); explosion.className = "explosion"; explosion.style.left = `${x - 40}px`; explosion.style.top = `${y - 40}px`; arena.appendChild(explosion); setTimeout(() => explosion.remove(), 600); } function applyBuff(character, buff) { if (buff.type === 'health') { const heal = character.maxHp * 0.4; character.hp = Math.min(character.maxHp, character.hp + heal); logMessage(`💚 【${character.data.name}】拾取【医疗包】,恢复 ${heal.toFixed(0)} HP!`); } else if (buff.type === 'attack') { const boost = 6; character.atk += boost; logMessage(`⚡ 【${character.data.name}】拾取【攻击包】,攻击力+${boost}!`); } else if (buff.type === 'speed') { const boost = 0.8; character.moveSpeed += boost; logMessage(`🚀 【${character.data.name}】拾取【速度包】,移动速度+${boost.toFixed(1)}!`); setTimeout(() => { character.moveSpeed = character.spd; }, 5000); } updateHealth(character.data.name); buff.el.remove(); buffPoints = buffPoints.filter(b => b !== buff); } function attack(attackerName, defenderName) { const now = Date.now(); const attacker = characters[attackerName]; const defender = characters[defenderName]; if (!attacker || !defender || defender.hp <= 0 || now - attacker.lastAttack < 1000) return; attacker.lastAttack = now; let dmg = attacker.atk; if (attacker.data.name === "董" && Math.random() < 0.3) { dmg *= 2; logMessage(`🔥 【雷霆一击】${attacker.data.name} 暴击!造成 ${dmg} 伤害!!`); } else { logMessage(`💥 ${attacker.data.name} 攻击 ${defender.data.name},造成 ${dmg} 伤害`); } if (defender.data.name === "郭" && Math.random() < 0.2) { dmg *= 0.5; logMessage(`🛡️ 【钢铁意志】${defender.data.name} 减伤成功!`); } defender.hp -= dmg; playSound(defender.hp <= 0 ? 'explode' : 'hit'); updateHealth(defenderName); defender.element.style.transform += " scale(1.1)"; setTimeout(() => { defender.element.style.transform = `translate(${defender.x}px, ${defender.y}px)`; }, 150); if (defender.hp <= 0) { addExplosion(defender.x + 28, defender.y + 28); logMessage(`💀 ${defender.data.name} 被击败了!`); } } // ======== 🧠 AI大脑模块 ========== class AIBrain { constructor(name) { this.name = name; } decide(self, allChars, buffs) { const alive = allChars.filter(c => c.hp > 0 && c.data.name !== self.data.name); if (alive.length === 0) return { action: 'idle', target: null }; const lowHp = self.hp < self.maxHp * 0.4; const midHp = self.hp < self.maxHp * 0.7; const nearestBuff = findNearestBuff(self); // 逃跑逻辑 if (lowHp && nearestBuff?.type === 'health' && distance(self, nearestBuff) < 200) { return { action: 'heal', target: nearestBuff }; } if (lowHp) { return { action: 'flee', target: null }; } // 抢包逻辑 if (midHp && nearestBuff?.type === 'health') { return { action: 'heal', target: nearestBuff }; } if (self.atk < 30 && nearestBuff?.type === 'attack') { return { action: 'attack', target: nearestBuff }; } if (nearestBuff?.type === 'speed') { return { action: 'speed', target: nearestBuff }; } // 战斗逻辑 const target = alive.reduce((a, b) => distance(self, a) < distance(self, b) ? a : b); return { action: 'chase', target }; } } function startGame() { const totalPoints = chars.map(c => c.hp + c.atk + c.spd); if (totalPoints.some(p => p !== 100) && !confirm("有人技能点不等于100,是否继续?")) return; resetArena(); chars.forEach(createCharacter); gameRunning = true; log.innerHTML = "【系统】🎮 智能战场启动!\n"; ['health', 'attack', 'speed'].forEach(t => spawnBuffPoint(t)); setInterval(() => { if (!gameRunning) return; const types = ['health', 'attack', 'speed']; spawnBuffPoint(types[Math.floor(Math.random() * 3)]); }, 8000); function getAlive() { return Object.values(characters).filter(c => c.hp > 0); } function gameLoop() { if (!gameRunning) return; const alive = getAlive(); if (alive.length <= 1) { const winner = alive[0]?.data.name; if (winner) winCount[winner]++; statsEl.innerHTML = `🏆 胜率统计:<br>` + `董: ${winCount['董']}胜|` + `邱: ${winCount['邱']}胜|` + `郭: ${winCount['郭']}胜|` + `陆: ${winCount['陆']}胜`; endGame(alive.length ? `🏆 胜者:【${winner}】` : "🔚 全军覆没"); setTimeout(() => { if (confirm("本轮结束!是否开启下一轮?")) { startGame(); } }, 500); return; } for (const name in characters) { const self = characters[name]; if (self.hp <= 0) continue; // AI 决策 const decision = self.aiBrain.decide(self, Object.values(characters), buffPoints); updateStrategyTag(name, decision.action); let moveX = 0, moveY = 0; const baseSpeed = self.moveSpeed * 1.2; if (decision.action === 'heal' || decision.action === 'attack' || decision.action === 'speed') { const goal = decision.target; const dx = goal.x - self.x; const dy = goal.y - self.y; const dist = distance(self, goal); if (dist > 40) { moveX = (dx / dist) * baseSpeed; moveY = (dy / dist) * baseSpeed; } else { if (goal.el) applyBuff(self, goal); } } else if (decision.action === 'flee') { const enemies = alive.filter(c => c.data.name !== self.data.name); const threat = enemies.reduce((a, b) => distance(self, a) < distance(self, b) ? a : b); const dx = self.x - threat.x; const dy = self.y - threat.y; const dist = distance(self, threat); moveX = (dx / dist) * baseSpeed; moveY = (dy / dist) * baseSpeed; } else if (decision.action === 'chase' && decision.target) { const target = decision.target; const dx = target.x - self.x; const dy = target.y - self.y; const dist = distance(self, target); if (dist > 50) { moveX = (dx / dist) * baseSpeed; } else if (dist > 30) { // 走A:保持距离 moveX = (dx / dist) * (baseSpeed * 0.5); moveY = (dy / dist) * (baseSpeed * 0.5); if (Date.now() - self.lastAttack > 1000) attack(name, target.data.name); } else { // 小幅后退绕行 const angle = Math.atan2(dy, dx); moveX = Math.cos(angle + Math.PI/2) * baseSpeed; moveY = Math.sin(angle + Math.PI/2) * baseSpeed; if (Date.now() - self.lastAttack > 1000) attack(name, target.data.name); } } // 排斥力 for (const other of alive) { if (other.data.name === name) continue; const d = distance(self, other); if (d < 60) { const push = (60 - d) * 0.6; const dirX = (self.x - other.x) / d; const dirY = (self.y - other.y) / d; moveX += dirX * push; moveY += dirY * push; } } self.x += moveX; self.y += moveY; self.x = Math.max(28, Math.min(arena.clientWidth - 28, self.x)); self.y = Math.max(28, Math.min(arena.clientHeight - 28, self.y)); self.element.style.transform = `translate(${self.x}px, ${self.y}px)`; } window.gameLoopId = requestAnimationFrame(gameLoop); } setTimeout(() => requestAnimationFrame(gameLoop), 1000); } function endGame(msg) { logMessage(msg); gameRunning = false; } function playSound(type) { try { const ctx = new (window.AudioContext || window.webkitAudioContext)(); const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.connect(gain); gain.connect(ctx.destination); if (type === 'hit') { osc.frequency.setValueAtTime(180, ctx.currentTime); osc.type = 'square'; gain.gain.setValueAtTime(0.15, ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.3); osc.start(); osc.stop(ctx.currentTime + 0.3); } else if (type === 'explode') { osc.frequency.setValueAtTime(120, ctx.currentTime); osc.type = 'sawtooth'; gain.gain.setValueAtTime(0.3, ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.6); osc.start(); osc.stop(ctx.currentTime + 0.6); } } catch (e) {} } </script> </body> </html> ,这个代码有bug,开始后他们会自动站成1列
11-18
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值