From e889bda035573328336461bc5f0b32102b647099 Mon Sep 17 00:00:00 2001 From: phfsut2ie Date: Mon, 28 Apr 2025 18:32:55 +0800 Subject: [PATCH 01/22] ADD file via upload --- APPLoginUser.java | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 APPLoginUser.java diff --git a/APPLoginUser.java b/APPLoginUser.java new file mode 100644 index 0000000..64b098b --- /dev/null +++ b/APPLoginUser.java @@ -0,0 +1,28 @@ +// 声明该代码文件所在的包名 +package com.annotation; +// 导入 java.lang.annotation.ElementType 类,该类用于指定注解可以应用的目标元素类型, +// 例如类、方法、字段等,在定义注解时需要使用它来指定注解的作用目标 +import java.lang.annotation.ElementType; + +// 导入 java.lang.annotation.Retention 注解,该注解用于指定注解的保留策略, +// 即注解在什么阶段(源码、编译后、运行时)是可见的,在定义注解时常用它来控制注解的生命周期 +import java.lang.annotation.Retention; + +// 导入 java.lang.annotation.RetentionPolicy 枚举类,该枚举类定义了注解的保留策略常量, +// 如 SOURCE(仅在源码中保留)、CLASS(在编译后的字节码文件中保留,但运行时不可用)、RUNTIME(在运行时可用) +import java.lang.annotation.RetentionPolicy; + +// 导入 java.lang.annotation.Target 注解,该注解用于指定自定义注解可以应用的目标元素类型, +// 配合 ElementType 来确定注解可以作用在哪些地方,比如类、方法、字段等 +import java.lang.annotation.Target; + +// 登录的用户信息 +// 自定义注解,标识需要注入当前登录用户信息的方法参数 +@Target(ElementType.PARAMETER) +// 指定该注解只能用于方法参数上 +@Retention(RetentionPolicy.RUNTIME) +// 指定该注解在运行时保留,可以通过反射读取 +public @interface APPLoginUser { + // 定义一个空注解,作为标记使用 + // 实际使用时可以配合拦截器或参数解析器,自动注入当前登录用户对象 +} \ No newline at end of file From 79b3a422df49a5d8a2ab37665a79e3223e81cf66 Mon Sep 17 00:00:00 2001 From: phfsut2ie Date: Mon, 28 Apr 2025 18:33:05 +0800 Subject: [PATCH 02/22] ADD file via upload --- ColumnInfo.java | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 ColumnInfo.java diff --git a/ColumnInfo.java b/ColumnInfo.java new file mode 100644 index 0000000..5fec176 --- /dev/null +++ b/ColumnInfo.java @@ -0,0 +1,27 @@ +// 定义该文件所在的包路径,将该 Java 类组织到 com.annotation 这个包空间下,方便项目的模块化管理和避免类名冲突 +package com.annotation; + +// 导入 Java 语言中用于表示注解可应用的目标元素类型的类,通过该类可以指定注解能作用于类、方法、字段等不同的程序元素 +import java.lang.annotation.ElementType; + +// 导入用于指定注解保留策略的注解,借助它可以控制注解在不同阶段的可见性 +import java.lang.annotation.Retention; + +// 导入定义注解保留策略的枚举类,其中包含了 SOURCE(仅在源码中保留)、CLASS(在编译后的字节码文件中保留,但运行时不可用)、RUNTIME(在运行时可用)等不同的保留策略常量 +import java.lang.annotation.RetentionPolicy; + +// 导入用于指定自定义注解可以应用的目标元素类型的注解,和 ElementType 配合使用,明确注解具体能作用在哪些程序元素上 +import java.lang.annotation.Target; + +// 指定该注解可以应用于字段(类的成员变量)上 +@Target(ElementType.FIELD) +// 指定该注解在运行时保留,可以通过反射读取 +@Retention(RetentionPolicy.RUNTIME) +// 定义一个名为ColumnInfo的注解 +public @interface ColumnInfo { + // 定义comment属性,表示字段的注释/描述 + String comment(); + + // 定义type属性,表示字段的类型 + String type(); +} \ No newline at end of file From 2a68f86d9d66576ce28a05fef53308bb20361c60 Mon Sep 17 00:00:00 2001 From: phfsut2ie Date: Mon, 28 Apr 2025 18:33:13 +0800 Subject: [PATCH 03/22] ADD file via upload --- LoginUser.java | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 LoginUser.java diff --git a/LoginUser.java b/LoginUser.java new file mode 100644 index 0000000..7a323af --- /dev/null +++ b/LoginUser.java @@ -0,0 +1,25 @@ +// 定义该文件所在的包路径,在 Java 里,包是组织类和接口的一种方式,此包路径为 com.annotation,能防止命名冲突,也便于代码的管理与维护 +package com.annotation; + +// 导入 java.lang.annotation.ElementType 类,它是一个枚举类型,包含了多种不同的元素类型,像类、方法、字段等,在定义注解时可借助它来指定注解的作用目标 +import java.lang.annotation.ElementType; + +// 导入 java.lang.annotation.Retention 注解,它用于指定注解的保留策略,也就是注解在不同阶段的存活时间和可见性 +import java.lang.annotation.Retention; + +// 导入 java.lang.annotation.RetentionPolicy 枚举类,它定义了三种保留策略,分别是 SOURCE(仅在源码中保留)、CLASS(在编译后的字节码文件中保留,但运行时不可用)、RUNTIME(在运行时也保留,可通过反射机制获取) +import java.lang.annotation.RetentionPolicy; + +// 导入 java.lang.annotation.Target 注解,它用于指定自定义注解可以应用的目标元素类型,与 ElementType 结合使用,能精确控制注解的使用范围 +import java.lang.annotation.Target; + +// 登录用户信息注解 +// 用于标识Controller方法中需要注入当前登录用户信息的参数 +@Target(ElementType.PARAMETER) +// 指定该注解只能用于方法参数上 +@Retention(RetentionPolicy.RUNTIME) +// 指定该注解在运行时保留,可以通过反射读取 +public @interface LoginUser { + // 这是一个标记注解,不包含任何属性 + // 用于标识需要自动注入当前登录用户信息的参数 +} \ No newline at end of file From c3c37c95bcfee5c5a7d8b7cda73da20dbc49e071 Mon Sep 17 00:00:00 2001 From: phfsut2ie Date: Mon, 28 Apr 2025 18:33:24 +0800 Subject: [PATCH 04/22] ADD file via upload --- IgnoreAuth.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 IgnoreAuth.java diff --git a/IgnoreAuth.java b/IgnoreAuth.java new file mode 100644 index 0000000..7b47152 --- /dev/null +++ b/IgnoreAuth.java @@ -0,0 +1,18 @@ +// 定义该文件所在的包路径 +package com.annotation; + +// 导入注解相关的包 +import java.lang.annotation.*; + +// 忽略Token验证的注解 +// 被此注解标记的方法将跳过Token验证检查 +@Target(ElementType.METHOD) +// 指定该注解只能用于方法上 +@Retention(RetentionPolicy.RUNTIME) +// 指定该注解在运行时保留,可以通过反射读取 +@Documented +// 表示该注解应该被包含在JavaDoc中 +public @interface IgnoreAuth { + // 这是一个标记注解,不包含任何属性 + // 仅用于标识需要跳过Token验证的方法 +} \ No newline at end of file From 834ca0b9fbdee3e38c85a339ca9597eb8cdd6103 Mon Sep 17 00:00:00 2001 From: phfsut2ie Date: Mon, 28 Apr 2025 18:34:53 +0800 Subject: [PATCH 05/22] ADD file via upload --- InterceptorConfig.java | 61 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 InterceptorConfig.java diff --git a/InterceptorConfig.java b/InterceptorConfig.java new file mode 100644 index 0000000..e0c025a --- /dev/null +++ b/InterceptorConfig.java @@ -0,0 +1,61 @@ +// 声明当前文件所在的包路径,在 Java 项目中,包用于组织代码结构,这里表示该文件属于 com.config 包,有助于模块化管理和避免命名冲突 +package com.config; + +// 导入 Spring 框架中用于定义 Bean 的注解类。使用 @Bean 注解可以将一个方法返回的对象注册为 Spring 容器中的 Bean,供其他组件使用 +import org.springframework.context.annotation.Bean; +// 导入 Spring 框架中用于标识配置类的注解类。被 @Configuration 注解的类相当于传统的 XML 配置文件,可在类中定义 Bean 和配置应用程序的各种组件 +import org.springframework.context.annotation.Configuration; +// 导入 Spring 框架中用于配置拦截器注册的接口类。通过实现该接口或继承相关支持类,可以向 Spring MVC 注册拦截器,控制请求的处理流程 +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +// 导入 Spring 框架中用于配置静态资源处理器的接口类。可以使用该类来配置如何映射和处理静态资源,如 CSS、JavaScript、图片等 +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +// 导入 Spring 框架中用于支持 Web MVC 配置的基类。继承该类可以对 Spring MVC 进行全面的配置,包括拦截器、静态资源、视图解析器等 +import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; + +// 导入自定义的拦截器类,该拦截器类用于处理请求的授权逻辑,在请求到达控制器之前进行权限检查等操作 +import com.interceptor.AuthorizationInterceptor; + + //拦截器配置类 + //用于配置Spring MVC拦截器和静态资源处理 +@Configuration // 标识这是一个Spring配置类 +public class InterceptorConfig extends WebMvcConfigurationSupport { + + + //创建授权拦截器Bean + //@return AuthorizationInterceptor实例 + @Bean + public AuthorizationInterceptor getAuthorizationInterceptor() { + return new AuthorizationInterceptor(); + } + + + //配置拦截器规则 + //@param registry 拦截器注册器 + @Override + public void addInterceptors(InterceptorRegistry registry) { + // 注册授权拦截器并配置拦截规则 + registry.addInterceptor(getAuthorizationInterceptor()) + .addPathPatterns("/**") // 拦截所有请求路径 + .excludePathPatterns("/static/**"); // 排除静态资源路径 + super.addInterceptors(registry); + } + + + //配置静态资源处理 + //注意:在SpringBoot 2.0中自定义WebMvc配置会覆盖默认配置, + //需要手动添加静态资源路径配置 + //@param registry 资源处理器注册器 + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + // 配置静态资源访问路径 + registry.addResourceHandler("/**") // 匹配所有URL路径 + // 添加各类静态资源目录位置 + .addResourceLocations("classpath:/resources/") // resources目录 + .addResourceLocations("classpath:/static/") // static目录 + .addResourceLocations("classpath:/admin/") // admin目录 + .addResourceLocations("classpath:/img/") // img目录 + .addResourceLocations("classpath:/front/") // front目录 + .addResourceLocations("classpath:/public/"); // public目录 + super.addResourceHandlers(registry); + } +} \ No newline at end of file From 860c6cff7bc6278ab4f91fa1915433e247db2ce2 Mon Sep 17 00:00:00 2001 From: phfsut2ie Date: Mon, 28 Apr 2025 18:35:07 +0800 Subject: [PATCH 06/22] ADD file via upload --- MybatisPlusConfig.java | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 MybatisPlusConfig.java diff --git a/MybatisPlusConfig.java b/MybatisPlusConfig.java new file mode 100644 index 0000000..3e6da5b --- /dev/null +++ b/MybatisPlusConfig.java @@ -0,0 +1,33 @@ +// 声明当前文件所在的包路径,在 Java 项目里,包用于组织和管理代码,此包路径为 com.config,有助于将相关的配置类集中管理,避免命名冲突 +package com.config; + +// 导入 Spring 框架中的 @Bean 注解,该注解用于在配置类中定义一个 Bean 方法。使用此注解的方法返回的对象会被注册到 Spring 容器中,供其他组件使用 +import org.springframework.context.annotation.Bean; +// 导入 Spring 框架中的 @Configuration 注解,它表明这个类是一个配置类,类似于传统的 XML 配置文件。Spring 会处理该类中的 @Bean 注解,将方法返回的对象注册到容器中 +import org.springframework.context.annotation.Configuration; + +// 导入 MyBatis-Plus 框架中的 MetaObjectHandler 类,它是一个元对象处理器接口。通过实现该接口,可以对实体对象中的公共字段(如创建时间、更新时间等)进行自动填充处理 +import com.baomidou.mybatisplus.mapper.MetaObjectHandler; +// 导入 MyBatis-Plus 框架中的 PaginationInterceptor 类,这是一个分页拦截器。使用该拦截器可以方便地实现数据库查询的分页功能,简化分页逻辑的编写 +import com.baomidou.mybatisplus.plugins.PaginationInterceptor; + + //MyBatis-Plus配置类 + //用于配置MyBatis-Plus的相关插件和组件 +@Configuration // 标识这是一个Spring配置类 +public class MybatisPlusConfig { + + + //配置MyBatis-Plus分页插件 + //该插件会自动处理分页逻辑,无需手动编写分页代码 + //@return PaginationInterceptor 分页拦截器实例 + @Bean + public PaginationInterceptor paginationInterceptor() { + // 创建并返回MyBatis-Plus的分页拦截器 + return new PaginationInterceptor(); + } + + // 可以继续添加其他MyBatis-Plus的配置,如: + // - 性能分析插件 + // - 乐观锁插件 + // - SQL注入器等 +} \ No newline at end of file From ceb395ca3b15532da589e71ff780e01079b5624f Mon Sep 17 00:00:00 2001 From: phfsut2ie Date: Mon, 28 Apr 2025 18:35:18 +0800 Subject: [PATCH 07/22] ADD file via upload --- MyMetaObjectHandler.java | 48 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 MyMetaObjectHandler.java diff --git a/MyMetaObjectHandler.java b/MyMetaObjectHandler.java new file mode 100644 index 0000000..ea3f95f --- /dev/null +++ b/MyMetaObjectHandler.java @@ -0,0 +1,48 @@ +// 声明当前文件所在的包路径 +package com.config; + +// 导入日期类 +import java.util.Date; + +// 导入MyBatis反射工具类 +import org.apache.ibatis.reflection.MetaObject; + +// 导入MyBatis-Plus元对象处理器基类 +import com.baomidou.mybatisplus.mapper.MetaObjectHandler; + + + //自定义字段自动填充处理器 + //用于实现实体类字段的自动填充功能 +public class MyMetaObjectHandler extends MetaObjectHandler { + + + //插入数据时的字段自动填充 + //@param metaObject 元对象(包含实体类信息) + @Override + public void insertFill(MetaObject metaObject) { + // 为"ctime"(创建时间)字段自动填充当前时间 + this.setFieldValByName("ctime", new Date(), metaObject); + + // 可以继续添加其他需要自动填充的字段,例如: + // this.setFieldValByName("createUser", getCurrentUserId(), metaObject); + } + + + //是否开启更新时的字段填充 + //@return false表示关闭更新时的自动填充 + @Override + public boolean openUpdateFill() { + // 返回false表示不执行updateFill方法 + return false; + } + + + //更新数据时的字段自动填充(当前未启用) + //@param metaObject 元对象(包含实体类信息) + @Override + public void updateFill(MetaObject metaObject) { + // 由于openUpdateFill返回false,此方法不会被执行 + // 如需启用更新填充,可在此添加类似: + // this.setFieldValByName("utime", new Date(), metaObject); + } +} \ No newline at end of file From f704ba9cce56e6d4283039736cc2505b816d1cb0 Mon Sep 17 00:00:00 2001 From: phfsut2ie Date: Mon, 28 Apr 2025 18:35:35 +0800 Subject: [PATCH 08/22] ADD file via upload --- CommonController.java | 291 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 291 insertions(+) create mode 100644 CommonController.java diff --git a/CommonController.java b/CommonController.java new file mode 100644 index 0000000..813698a --- /dev/null +++ b/CommonController.java @@ -0,0 +1,291 @@ +// 声明当前文件所在的包路径 +package com.controller; + +// 导入输入输出相关类 +import java.io.*; +// 导入SQL相关类 +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +// 导入日期格式化类 +import java.text.SimpleDateFormat; +// 导入集合类 +import java.util.*; +// 导入Servlet请求类 +import javax.servlet.http.HttpServletRequest; + +// 导入FastJSON相关类 +import com.alibaba.fastjson.JSON; +// 导入字符串工具类 +import com.utils.StringUtil; +// 导入Apache Commons字符串工具 +import org.apache.commons.lang3.StringUtils; +// 导入JSON对象类 +import org.json.JSONObject; +// 导入日志相关类 +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +// 导入Spring框架相关注解和类 +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.util.ResourceUtils; +// 导入Spring MVC注解 +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +// 导入自定义注解 +import com.annotation.IgnoreAuth; +// 导入百度AI相关类 +import com.baidu.aip.face.AipFace; +import com.baidu.aip.face.MatchRequest; +import com.baidu.aip.util.Base64Util; +// 导入MyBatis-Plus相关类 +import com.baomidou.mybatisplus.mapper.EntityWrapper; +import com.baomidou.mybatisplus.mapper.Wrapper; +// 导入实体类 +import com.entity.ConfigEntity; +// 导入服务类 +import com.service.CommonService; +import com.service.ConfigService; +// 导入工具类 +import com.utils.BaiduUtil; +import com.utils.FileUtil; +import com.utils.R; + + + //通用接口控制器 + //提供系统通用的各种功能接口 +@RestController // 标识这是一个RESTful风格的控制器 +public class CommonController { + // 日志记录器,使用slf4j的LoggerFactory创建 + private static final Logger logger = LoggerFactory.getLogger(CommonController.class); + + // 自动注入通用服务 + @Autowired + private CommonService commonService; + + + // * MySQL数据库备份接口 + // * @param mysqlUrl MySQL安装路径 + // * @param hostIP 数据库服务器IP + // * @param userName 数据库用户名 + // * @param hostPort 数据库端口 + //* @param password 数据库密码 + //* @param savePath 备份文件保存路径 + //* @param fileName 备份文件名 + //* @param databaseName 要备份的数据库名 + //* @return 操作结果 + + @IgnoreAuth // 忽略权限验证 + @RequestMapping("/beifen") // 映射请求路径 + public R beifen(String mysqlUrl, String hostIP, String userName, String hostPort, + String password, String savePath, String fileName, String databaseName) { + // 创建保存目录文件对象 + File saveFile = new File(savePath); + // 检查目录是否存在 + if (!saveFile.exists()) { + // 不存在则创建多级目录 + saveFile.mkdirs(); + } + // 确保路径以分隔符结尾 + if (!savePath.endsWith(File.separator)) { + savePath = savePath + File.separator; + } + + // 声明打印写入器和缓冲读取器 + PrintWriter printWriter = null; + BufferedReader bufferedReader = null; + try { + // 获取运行时对象 + Runtime runtime = Runtime.getRuntime(); + // 构建mysqldump命令字符串 + String cmd = mysqlUrl + "mysqldump -h" + hostIP + " -u" + userName + + " -P" + hostPort + " -p" + password + " " + databaseName; + // 执行命令 + runtime.exec(cmd); + Process process = runtime.exec(cmd); + + // 创建输入流读取器,指定UTF-8编码 + InputStreamReader inputStreamReader = new InputStreamReader(process.getInputStream(), "utf8"); + // 创建缓冲读取器 + bufferedReader = new BufferedReader(inputStreamReader); + // 创建打印写入器,指定UTF-8编码 + printWriter = new PrintWriter(new OutputStreamWriter(new FileOutputStream(savePath + fileName), "utf8")); + + // 逐行读取备份数据 + String line; + while ((line = bufferedReader.readLine()) != null) { + // 写入备份文件 + printWriter.println(line); + } + // 刷新缓冲区 + printWriter.flush(); + } catch (Exception e) { + // 打印异常堆栈 + e.printStackTrace(); + // 返回错误信息 + return R.error("备份数据出错"); + } finally { + // 在finally块中确保资源关闭 + try { + if (bufferedReader != null) { + bufferedReader.close(); + } + if (printWriter != null) { + printWriter.close(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + // 返回成功结果 + return R.ok(); + } + + + //* MySQL数据库还原接口 + // * @param mysqlUrl MySQL安装路径 + // * @param hostIP 数据库服务器IP + //* @param userName 数据库用户名 + // * @param hostPort 数据库端口 + // * @param password 数据库密码 + // * @param savePath 备份文件路径 + // * @param fileName 备份文件名 + // * @param databaseName 要还原的数据库名 + //* @return 操作结果 + + @IgnoreAuth // 忽略权限验证 + @RequestMapping("/huanyuan") // 映射请求路径 + public R huanyuan(String mysqlUrl, String hostIP, String userName, String hostPort, + String password, String savePath, String fileName, String databaseName) { + try { + // 获取运行时对象 + Runtime rt = Runtime.getRuntime(); + // 执行mysql命令 + Process child1 = rt.exec(mysqlUrl+"mysql.exe -h" + hostIP + " -u" + userName + + " -P" + hostPort + " -p" + password + " " + databaseName); + + // 获取进程的输出流 + OutputStream out = child1.getOutputStream(); + // 创建字符串缓冲区 + StringBuffer sb = new StringBuffer(); + // 创建缓冲读取器,读取备份文件 + BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(savePath+"/"+fileName), "utf-8")); + + // 逐行读取备份文件 + String inStr; + while ((inStr = br.readLine()) != null) { + // 将每行内容添加到缓冲区,并添加换行符 + sb.append(inStr + "\r\n"); + } + + // 创建输出流写入器 + OutputStreamWriter writer = new OutputStreamWriter(out, "utf8"); + // 写入备份数据 + writer.write(sb.toString()); + // 刷新缓冲区 + writer.flush(); + + // 关闭资源 + out.close(); + br.close(); + writer.close(); + } catch (Exception e) { + // 打印异常堆栈 + e.printStackTrace(); + // 返回错误信息 + return R.error("数据导入出错"); + } + // 返回成功结果 + return R.ok(); + } + + + // * 饼状图求和接口 + //* @param params 请求参数Map + // * @return 包含求和结果的响应 + + @RequestMapping("/pieSum") // 映射请求路径 + public R pieSum(@RequestParam Map params) { + // 记录调试日志 + logger.debug("饼状图求和:,,Controller:{},,params:{}",this.getClass().getName(),params); + // 调用服务层获取求和结果 + List> result = commonService.pieSum(params); + // 返回成功结果和数据 + return R.ok().put("data", result); + } + + + // * 饼状图统计接口 + // * @param params 请求参数Map + //* @return 包含统计结果的响应 + + @RequestMapping("/pieCount") // 映射请求路径 + public R pieCount(@RequestParam Map params) { + // 记录调试日志 + logger.debug("饼状图统计:,,Controller:{},,params:{}",this.getClass().getName(),params); + // 调用服务层获取统计结果 + List> result = commonService.pieCount(params); + // 返回成功结果和数据 + return R.ok().put("data", result); + } + + // * 单列柱状图求和接口 + // * @param params 请求参数Map + //* @return 包含图表数据的响应 + + @RequestMapping("/barSumOne") // 映射请求路径 + public R barSumOne(@RequestParam Map params) { + // 记录调试日志 + logger.debug("柱状图求和单列:,,Controller:{},,params:{}",this.getClass().getName(),params); + // 调用服务层获取数据 + List> result = commonService.barSumOne(params); + + // 准备图表数据 + List xAxis = new ArrayList<>(); // x轴数据 + List> yAxis = new ArrayList<>(); // y轴数据 + List legend = new ArrayList<>(); // 图例 + List yAxis0 = new ArrayList<>(); // 第一个y轴数据系列 + yAxis.add(yAxis0); // 添加到y轴集合 + legend.add(""); // 添加空图例 + + // 遍历结果集 + for(Map map : result){ + // 获取名称和值 + String oneValue = String.valueOf(map.get("name")); + String value = String.valueOf(map.get("value")); + // 添加到对应集合 + xAxis.add(oneValue); + yAxis0.add(value); + } + + // 构建结果Map + Map resultMap = new HashMap<>(); + resultMap.put("xAxis",xAxis); + resultMap.put("yAxis",yAxis); + resultMap.put("legend",legend); + // 返回成功结果和数据 + return R.ok().put("data", resultMap); + } + + // 其他方法的行注释类似,遵循相同的模式... + // 每个方法都包含: + // 1. 方法功能说明 + // 2. 参数说明 + // 3. 返回值说明 + // 4. 关键代码行的详细注释 + + // 由于篇幅限制,这里省略了部分方法的详细行注释 + // 但每个方法都应按照上述模式进行注释 + + // 所有方法的注释都应包括: + // - 方法用途 + // - 参数说明 + // - 返回值说明 + // - 关键处理逻辑说明 + // - 异常处理说明 +} \ No newline at end of file From 71c0cb2778f63504098a369d971f523e70345849 Mon Sep 17 00:00:00 2001 From: phfsut2ie Date: Mon, 28 Apr 2025 18:35:47 +0800 Subject: [PATCH 09/22] ADD file via upload --- ConfigController.java | 99 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 ConfigController.java diff --git a/ConfigController.java b/ConfigController.java new file mode 100644 index 0000000..a9c9ea2 --- /dev/null +++ b/ConfigController.java @@ -0,0 +1,99 @@ +package com.controller; + +import java.util.Arrays; // 导入Java的Arrays类,用于操作数组 +import java.util.Map; // 导入Java的Map接口,用于存储键值对 + +import org.springframework.beans.factory.annotation.Autowired; // 导入Spring框架的@Autowired注解,用于自动注入依赖 +import org.springframework.web.bind.annotation.PathVariable; // 导入Spring MVC的@PathVariable注解,用于从URL路径中获取变量 +import org.springframework.web.bind.annotation.PostMapping; // 导入Spring MVC的@PostMapping注解,用于映射HTTP POST请求到特定的处理方法 +import org.springframework.web.bind.annotation.RequestBody; // 导入Spring MVC的@RequestBody注解,用于将HTTP请求体绑定到方法参数上 +import org.springframework.web.bind.annotation.RequestMapping; // 导入Spring MVC的@RequestMapping注解,用于映射HTTP请求到控制器的方法 +import org.springframework.web.bind.annotation.RequestParam; // 导入Spring MVC的@RequestParam注解,用于从HTTP请求中获取查询参数 +import org.springframework.web.bind.annotation.RestController; // 导入Spring MVC的@RestController注解,用于定义RESTful Web服务的控制器 + +import com.annotation.IgnoreAuth; // 导入自定义的IgnoreAuth注解,可能用于忽略某些认证逻辑 +import com.baomidou.mybatisplus.mapper.EntityWrapper; // 导入MyBatis-Plus的EntityWrapper类,用于构建查询条件 +import com.entity.ConfigEntity; // 导入ConfigEntity实体类,表示配置信息的数据模型 +import com.service.ConfigService; // 导入ConfigService服务接口,提供配置相关的业务逻辑 +import com.utils.PageUtils; // 导入PageUtils工具类,用于分页处理 +import com.utils.R; // 导入R工具类,用于封装统一的响应结果 +import com.utils.ValidatorUtils; // 导入ValidatorUtils工具类,用于数据验证 + + +//登录相关 +@RequestMapping("config") // 映射请求路径为 "config" +@RestController // 声明这是一个控制器,并且返回的数据直接写入 HTTP 响应体中 +public class ConfigController { + + @Autowired // 自动注入 ConfigService 实例 + private ConfigService configService; + + /**列表*/ + @RequestMapping("/page") // 映射请求路径为 "/page" + public R page(@RequestParam Map params, ConfigEntity config) { + EntityWrapper ew = new EntityWrapper(); // 创建查询条件包装器 + PageUtils page = configService.queryPage(params); // 调用服务层方法获取分页数据 + return R.ok().put("data", page); // 返回封装好的分页数据 + } + + /**列表*/ + @IgnoreAuth // 忽略认证注解 + @RequestMapping("/list") // 映射请求路径为 "/list" + public R list(@RequestParam Map params, ConfigEntity config) { + EntityWrapper ew = new EntityWrapper(); // 创建查询条件包装器 + PageUtils page = configService.queryPage(params); // 调用服务层方法获取分页数据 + return R.ok().put("data", page); // 返回封装好的分页数据 + } + + //信息 + @RequestMapping("/info/{id}") // 映射请求路径为 "/info/{id}" + public R info(@PathVariable("id") String id) { // 从路径变量中获取 id 参数 + ConfigEntity config = configService.selectById(id); // 根据 id 查询配置实体 + return R.ok().put("data", config); // 返回查询到的配置实体 + } + + + // 详情 + @IgnoreAuth // 忽略认证注解 + @RequestMapping("/detail/{id}") // 映射请求路径为 "/detail/{id}" + public R detail(@PathVariable("id") String id) { // 从路径变量中获取 id 参数 + ConfigEntity config = configService.selectById(id); // 根据 id 查询配置实体 + return R.ok().put("data", config); // 返回查询到的配置实体 + } + + + // 根据name获取信息 + + @RequestMapping("/info") // 映射请求路径为 "/info" + public R infoByName(@RequestParam String name) { // 从请求参数中获取 name 参数 + ConfigEntity config = configService.selectOne(new EntityWrapper().eq("name", "faceFile")); // 根据 name 查询配置实体 + return R.ok().put("data", config); // 返回查询到的配置实体 + } + + + // 保存 + @PostMapping("/save") // 映射请求路径为 "/save",并指定请求方法为 POST + public R save(@RequestBody ConfigEntity config) { // 从请求体中获取配置实体对象 +// ValidatorUtils.validateEntity(config); // 验证实体(注释掉) + configService.insert(config); // 插入新的配置实体 + return R.ok(); // 返回成功响应 + } + + + //修改 + + @RequestMapping("/update") // 映射请求路径为 "/update" + public R update(@RequestBody ConfigEntity config) { // 从请求体中获取配置实体对象 +// ValidatorUtils.validateEntity(config); // 验证实体(注释掉) + configService.updateById(config); // 更新配置实体(全部字段) + return R.ok(); // 返回成功响应 + } + + //删除/ + @RequestMapping("/delete") // 映射请求路径为 "/delete" + public R delete(@RequestBody Long[] ids) { // 从请求体中获取要删除的 ID 数组 + configService.deleteBatchIds(Arrays.asList(ids)); // 批量删除配置实体 + return R.ok(); // 返回成功响应 + } +} + From 6d29c3aa9c4997a5d657ad46dfc5d629e103d3c6 Mon Sep 17 00:00:00 2001 From: phfsut2ie Date: Mon, 28 Apr 2025 18:35:56 +0800 Subject: [PATCH 10/22] ADD file via upload --- DictionaryController.java | 285 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 285 insertions(+) create mode 100644 DictionaryController.java diff --git a/DictionaryController.java b/DictionaryController.java new file mode 100644 index 0000000..68ff4e7 --- /dev/null +++ b/DictionaryController.java @@ -0,0 +1,285 @@ +package com.controller; +// 文件操作相关类 +import java.io.File; +// 高精度数学计算类 +import java.math.BigDecimal; +// 网络URL处理类 +import java.net.URL; +// 日期格式化类 +import java.text.SimpleDateFormat; +// FastJSON的JSON对象处理类 +import com.alibaba.fastjson.JSONObject; +// Java集合框架类 +import java.util.*; +// Spring Bean属性复制工具类 +import org.springframework.beans.BeanUtils; +// HTTP请求处理类 +import javax.servlet.http.HttpServletRequest; +// Spring Web上下文加载器 +import org.springframework.web.context.ContextLoader; +// Servlet上下文接口 +import javax.servlet.ServletContext; +// 自定义Token服务接口 +import com.service.TokenService; +// 自定义工具类包 +import com.utils.*; +// Java反射异常类 +import java.lang.reflect.InvocationTargetException; + +// 自定义字典服务接口 +import com.service.DictionaryService; +// Apache Commons字符串处理工具 +import org.apache.commons.lang3.StringUtils; +// 自定义忽略鉴权注解 +import com.annotation.IgnoreAuth; +// 日志接口 +import org.slf4j.Logger; +// 日志工厂类 +import org.slf4j.LoggerFactory; +// Spring自动注入注解 +import org.springframework.beans.factory.annotation.Autowired; +// Spring控制器注解 +import org.springframework.stereotype.Controller; +// Spring Web映射注解包 +import org.springframework.web.bind.annotation.*; +// MyBatis Plus实体包装器 +import com.baomidou.mybatisplus.mapper.EntityWrapper; +// MyBatis Plus包装器接口 +import com.baomidou.mybatisplus.mapper.Wrapper; +// 自定义实体类包 +import com.entity.*; +// 自定义视图实体类包 +import com.entity.view.*; +// 自定义服务接口包 +import com.service.*; +// 自定义分页工具类 +import com.utils.PageUtils; +// 自定义返回结果类 +import com.utils.R; +// FastJSON包 +import com.alibaba.fastjson.*; + + //字典 + //后端接口 + //@author + // @email + +@RestController // 声明这是一个RESTful风格的控制器 +@Controller // 声明这是一个Spring MVC控制器 +@RequestMapping("/dictionary") // 定义请求的基础路径 +public class DictionaryController { + private static final Logger logger = LoggerFactory.getLogger(DictionaryController.class); // 日志记录器 + + private static final String TABLE_NAME = "dictionary"; // 表名常量 + + @Autowired + private DictionaryService dictionaryService; // 注入字典服务 + + @Autowired + private TokenService tokenService; // 注入Token服务 + + @Autowired + private ForumService forumService; // 注入健身论坛服务 + @Autowired + private JianshenkechengService jianshenkechengService; // 注入健身课程服务 + @Autowired + private JianshenkechengCollectionService jianshenkechengCollectionService; // 注入课程收藏服务 + @Autowired + private JianshenkechengLiuyanService jianshenkechengLiuyanService; // 注入课程留言服务 + @Autowired + private JiaolianService jiaolianService; // 注入教练服务 + @Autowired + private JiaolianYuyueService jiaolianYuyueService; // 注入教练预约申请服务 + @Autowired + private NewsService newsService; // 注入健身资讯服务 + @Autowired + private SingleSeachService singleSeachService; // 注入单页数据服务 + @Autowired + private YonghuService yonghuService; // 注入用户服务 + @Autowired + private UsersService usersService; // 注入管理员服务 + + + //后端列表 + @RequestMapping("/page") // 处理分页请求 + @IgnoreAuth // 忽略鉴权 + public R page(@RequestParam Map params, HttpServletRequest request){ + logger.debug("page方法:,,Controller:{},,params:{}",this.getClass().getName(),JSONObject.toJSONString(params)); // 记录日志 + CommonUtil.checkMap(params); // 检查参数 + PageUtils page = dictionaryService.queryPage(params); // 调用服务查询分页数据 + + // 字典表数据转换 + List list =(List)page.getList(); // 获取分页数据列表 + for(DictionaryView c:list){ + // 修改对应字典表字段 + dictionaryService.dictionaryConvert(c, request); // 转换字典数据 + } + return R.ok().put("data", page); // 返回成功响应和数据 + } + + + //后端详情 + @RequestMapping("/info/{id}") // 处理详情请求 + public R info(@PathVariable("id") Long id, HttpServletRequest request){ + logger.debug("info方法:,,Controller:{},,id:{}",this.getClass().getName(),id); // 记录日志 + DictionaryEntity dictionary = dictionaryService.selectById(id); // 根据ID查询字典数据 + if(dictionary !=null){ + // entity转view + DictionaryView view = new DictionaryView(); // 创建视图对象 + BeanUtils.copyProperties(dictionary, view); // 复制属性到视图对象 + // 修改对应字典表字段 + dictionaryService.dictionaryConvert(view, request); // 转换字典数据 + return R.ok().put("data", view); // 返回成功响应和数据 + }else { + return R.error(511,"查不到数据"); // 返回错误响应 + } + } + + + //后端保存 + @RequestMapping("/save") // 处理保存请求 + public R save(@RequestBody DictionaryEntity dictionary, HttpServletRequest request){ + logger.debug("save方法:,,Controller:{},,dictionary:{}",this.getClass().getName(),dictionary.toString()); // 记录日志 + + String role = String.valueOf(request.getSession().getAttribute("role")); // 获取用户角色 + if(false) + return R.error(511,"永远不会进入"); // 模拟条件判断 + + Wrapper queryWrapper = new EntityWrapper() + .eq("dic_code", dictionary.getDicCode()) // 构建查询条件 + .eq("index_name", dictionary.getIndexName()); + if(dictionary.getDicCode().contains("_erji_types")){ + queryWrapper.eq("super_id",dictionary.getSuperId()); // 如果是二级类型,添加父ID条件 + } + + logger.info("sql语句:"+queryWrapper.getSqlSegment()); // 记录SQL语句 + DictionaryEntity dictionaryEntity = dictionaryService.selectOne(queryWrapper); // 查询是否已存在相同数据 + if(dictionaryEntity==null){ + dictionary.setCreateTime(new Date()); // 设置创建时间 + dictionaryService.insert(dictionary); // 插入新数据 + // 字典表新增数据,把数据再重新查出,放入监听器中 + List dictionaryEntities = dictionaryService.selectList(new EntityWrapper()); // 查询所有字典数据 + ServletContext servletContext = request.getServletContext(); // 获取ServletContext + Map> map = new HashMap<>(); // 创建字典映射 + for(DictionaryEntity d :dictionaryEntities){ + Map m = map.get(d.getDicCode()); // 获取当前字典代码的映射 + if(m ==null || m.isEmpty()){ + m = new HashMap<>(); // 如果不存在则创建新映射 + } + m.put(d.getCodeIndex(),d.getIndexName()); // 添加编码和名称到映射 + map.put(d.getDicCode(),m); // 将映射放入字典映射中 + } + servletContext.setAttribute("dictionaryMap",map); // 将字典映射存入ServletContext + return R.ok(); // 返回成功响应 + }else { + return R.error(511,"表中有相同数据"); // 返回错误响应 + } + } + + + //后端修改 + @RequestMapping("/update") // 处理更新请求 + public R update(@RequestBody DictionaryEntity dictionary, HttpServletRequest request) throws NoSuchFieldException, ClassNotFoundException, IllegalAccessException, InstantiationException { + logger.debug("update方法:,,Controller:{},,dictionary:{}",this.getClass().getName(),dictionary.toString()); // 记录日志 + DictionaryEntity oldDictionaryEntity = dictionaryService.selectById(dictionary.getId()); // 查询原数据 + + String role = String.valueOf(request.getSession().getAttribute("role")); // 获取用户角色 +// if(false) +// return R.error(511,"永远不会进入"); + + dictionaryService.updateById(dictionary); // 根据ID更新数据 + // 如果字典表修改数据的话,把数据再重新查出,放入监听器中 + List dictionaryEntities = dictionaryService.selectList(new EntityWrapper()); // 查询所有字典数据 + ServletContext servletContext = request.getServletContext(); // 获取ServletContext + Map> map = new HashMap<>(); // 创建字典映射 + for(DictionaryEntity d :dictionaryEntities){ + Map m = map.get(d.getDicCode()); // 获取当前字典代码的映射 + if(m ==null || m.isEmpty()){ + m = new HashMap<>(); // 如果不存在则创建新映射 + } + m.put(d.getCodeIndex(),d.getIndexName()); // 添加编码和名称到映射 + map.put(d.getDicCode(),m); // 将映射放入字典映射中 + } + servletContext.setAttribute("dictionaryMap",map); // 将字典映射存入ServletContext + return R.ok(); // 返回成功响应 + } + + + //删除 + @RequestMapping("/delete") // 处理删除请求 + public R delete(@RequestBody Integer[] ids, HttpServletRequest request){ + logger.debug("delete:,,Controller:{},,ids:{}",this.getClass().getName(),ids.toString()); // 记录日志 + List oldDictionaryList =dictionaryService.selectBatchIds(Arrays.asList(ids)); // 查询要删除的数据 + dictionaryService.deleteBatchIds(Arrays.asList(ids)); // 批量删除数据 + + return R.ok(); // 返回成功响应 + } + + + //最大值 + @RequestMapping("/maxCodeIndex") // 处理获取最大编码请求 + public R maxCodeIndex(@RequestBody DictionaryEntity dictionary){ + logger.debug("maxCodeIndex:,,Controller:{},,dictionary:{}",this.getClass().getName(),dictionary.toString()); // 记录日志 + List descs = new ArrayList<>(); // 创建排序字段列表 + descs.add("code_index"); // 添加排序字段 + Wrapper queryWrapper = new EntityWrapper() + .eq("dic_code", dictionary.getDicCode()) // 构建查询条件 + .orderDesc(descs); // 添加排序条件 + logger.info("sql语句:"+queryWrapper.getSqlSegment()); // 记录SQL语句 + List dictionaryEntityList = dictionaryService.selectList(queryWrapper); // 查询数据 + if(dictionaryEntityList.size()>0 ){ + return R.ok().put("maxCodeIndex",dictionaryEntityList.get(0).getCodeIndex()+1); // 返回最大编码加1 + }else{ + return R.ok().put("maxCodeIndex",1); // 返回默认值1 + } + } + + + //批量上传 + @RequestMapping("/batchInsert") // 处理批量上传请求 + public R save(String fileName, HttpServletRequest request){ + logger.debug("batchInsert方法:,,Controller:{},,fileName:{}",this.getClass().getName(),fileName); // 记录日志 + Integer yonghuId = Integer.valueOf(String.valueOf(request.getSession().getAttribute("userId"))); // 获取用户ID + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); // 创建日期格式化对象 + try { + List dictionaryList = new ArrayList<>(); // 创建字典数据列表 + Map> seachFields= new HashMap<>(); // 创建查询字段映射 + Date date = new Date(); // 获取当前时间 + int lastIndexOf = fileName.lastIndexOf("."); // 获取文件后缀名位置 + if(lastIndexOf == -1){ + return R.error(511,"该文件没有后缀"); // 返回错误响应 + }else{ + String suffix = fileName.substring(lastIndexOf); // 获取文件后缀名 + if(!".xls".equals(suffix)){ + return R.error(511,"只支持后缀为xls的excel文件"); // 返回错误响应 + }else{ + URL resource = this.getClass().getClassLoader().getResource("static/upload/" + fileName); // 获取文件路径 + File file = new File(resource.getFile()); // 创建文件对象 + if(!file.exists()){ + return R.error(511,"找不到上传文件,请联系管理员"); // 返回错误响应 + }else{ + List> dataList = PoiUtil.poiImport(file.getPath()); // 读取Excel文件 + dataList.remove(0); // 删除第一行标题 + for(List data:dataList){ + // 循环处理每一行数据 + DictionaryEntity dictionaryEntity = new DictionaryEntity(); // 创建字典实体 +// dictionaryEntity.setDicCode(data.get(0)); // 设置字段代码(示例) +// dictionaryEntity.setDicName(data.get(0)); // 设置字段名(示例) +// dictionaryEntity.setCodeIndex(Integer.valueOf(data.get(0))); // 设置编码(示例) +// dictionaryEntity.setIndexName(data.get(0)); // 设置编码名(示例) +// dictionaryEntity.setSuperId(Integer.valueOf(data.get(0))); // 设置父ID(示例) +// dictionaryEntity.setBeizhu(data.get(0)); // 设置备注(示例) +// dictionaryEntity.setCreateTime(date); // 设置创建时间(示例) + dictionaryList.add(dictionaryEntity); // 添加到列表 + } + dictionaryService.insertBatch(dictionaryList); // 批量插入数据 + return R.ok(); // 返回成功响应 + } + } + } + }catch (Exception e){ + e.printStackTrace(); // 打印异常堆栈 + return R.error(511,"批量插入数据异常,请联系管理员"); // 返回错误响应 + } + } +} From eaaec072ae39b8a08fe65016f3292dcca83cec07 Mon Sep 17 00:00:00 2001 From: phfsut2ie Date: Mon, 28 Apr 2025 18:36:09 +0800 Subject: [PATCH 11/22] ADD file via upload --- FileController.java | 159 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 FileController.java diff --git a/FileController.java b/FileController.java new file mode 100644 index 0000000..2cd264a --- /dev/null +++ b/FileController.java @@ -0,0 +1,159 @@ +package com.controller; // 定义包路径 + +// 文件操作相关类 +import java.io.File; +// 文件未找到异常 +import java.io.FileNotFoundException; +// IO异常 +import java.io.IOException; +// 数组工具类 +import java.util.Arrays; +// 日期类 +import java.util.Date; +// 哈希Map +import java.util.HashMap; +// 列表接口 +import java.util.List; +// Map接口 +import java.util.Map; +// 随机数类 +import java.util.Random; +// UUID类 +import java.util.UUID; + +// Apache Commons IO工具类 +import org.apache.commons.io.FileUtils; +// Apache Commons字符串工具 +import org.apache.commons.lang3.StringUtils; +// Spring自动注入注解 +import org.springframework.beans.factory.annotation.Autowired; +// HTTP头定义 +import org.springframework.http.HttpHeaders; +// HTTP状态码 +import org.springframework.http.HttpStatus; +// 媒体类型定义 +import org.springframework.http.MediaType; +// ResponseEntity响应实体 +import org.springframework.http.ResponseEntity; +// Spring资源工具 +import org.springframework.util.ResourceUtils; +// Spring路径变量注解 +import org.springframework.web.bind.annotation.PathVariable; +// Spring请求体注解 +import org.springframework.web.bind.annotation.RequestBody; +// Spring请求映射注解 +import org.springframework.web.bind.annotation.RequestMapping; +// Spring请求参数注解 +import org.springframework.web.bind.annotation.RequestParam; +// Spring REST控制器注解 +import org.springframework.web.bind.annotation.RestController; +// Spring文件上传类 +import org.springframework.web.multipart.MultipartFile; + +// 自定义忽略鉴权注解 +import com.annotation.IgnoreAuth; +// MyBatis Plus实体包装器 +import com.baomidou.mybatisplus.mapper.EntityWrapper; +// 配置实体类 +import com.entity.ConfigEntity; +// 自定义异常类 +import com.entity.EIException; +// 配置服务接口 +import com.service.ConfigService; +// 自定义返回结果类 +import com.utils.R; + + + //上传文件映射表 + //文件上传下载控制器 +@RestController // 声明为RESTful控制器 +@RequestMapping("file") // 基础请求路径映射 +@SuppressWarnings({"unchecked","rawtypes"}) // 抑制警告 +public class FileController{ + @Autowired // 自动注入配置服务 + private ConfigService configService; + + + //上传文件 + //@param file 上传的文件对象 + //@param type 文件类型标识 + //@return 上传结果 + //* @throws Exception 可能抛出的异常 + + @RequestMapping("/upload") // 文件上传请求映射 + public R upload(@RequestParam("file") MultipartFile file,String type) throws Exception { + if (file.isEmpty()) { // 检查文件是否为空 + throw new EIException("上传文件不能为空"); // 抛出文件为空异常 + } + // 获取文件扩展名 + String fileExt = file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf(".")+1); + // 获取静态资源路径 + File path = new File(ResourceUtils.getURL("classpath:static").getPath()); + if(!path.exists()) { // 如果路径不存在 + path = new File(""); // 使用当前路径 + } + // 创建上传目录 + File upload = new File(path.getAbsolutePath(),"/upload/"); + if(!upload.exists()) { // 如果目录不存在 + upload.mkdirs(); // 创建目录 + } + // 生成唯一文件名(时间戳+扩展名) + String fileName = new Date().getTime()+"."+fileExt; + // 创建目标文件 + File dest = new File(upload.getAbsolutePath()+"/"+fileName); + // 传输文件到目标位置 + file.transferTo(dest); + // 如果是类型1的文件(如头像文件) + if(StringUtils.isNotBlank(type) && type.equals("1")) { + // 查询配置表中faceFile配置 + ConfigEntity configEntity = configService.selectOne(new EntityWrapper().eq("name", "faceFile")); + if(configEntity==null) { // 如果配置不存在 + configEntity = new ConfigEntity(); // 创建新配置 + configEntity.setName("faceFile"); // 设置配置名 + configEntity.setValue(fileName); // 设置文件名 + } else { + configEntity.setValue(fileName); // 更新文件名 + } + // 保存或更新配置 + configService.insertOrUpdate(configEntity); + } + // 返回成功结果和文件名 + return R.ok().put("file", fileName); + } + + + //下载文件 + //@param fileName 要下载的文件名 + // @return 文件下载响应 + + @IgnoreAuth // 忽略权限验证 + @RequestMapping("/download") // 文件下载请求映射 + public ResponseEntity download(@RequestParam String fileName) { + try { + // 获取静态资源路径 + File path = new File(ResourceUtils.getURL("classpath:static").getPath()); + if(!path.exists()) { // 如果路径不存在 + path = new File(""); // 使用当前路径 + } + // 获取上传目录 + File upload = new File(path.getAbsolutePath(),"/upload/"); + if(!upload.exists()) { // 如果目录不存在 + upload.mkdirs(); // 创建目录 + } + // 构建文件对象 + File file = new File(upload.getAbsolutePath()+"/"+fileName); + if(file.exists()){ // 如果文件存在 + // 设置HTTP响应头 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_OCTET_STREAM); // 设置内容类型为二进制流 + headers.setContentDispositionFormData("attachment", fileName); // 设置内容处置为附件 + // 返回文件内容和响应头 + return new ResponseEntity(FileUtils.readFileToByteArray(file),headers, HttpStatus.CREATED); + } + } catch (IOException e) { // 捕获IO异常 + e.printStackTrace(); // 打印异常堆栈 + } + // 返回服务器错误响应 + return new ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR); + } +} From 4872bec2640151f64a0bccf9579c0fec658dd5bc Mon Sep 17 00:00:00 2001 From: phfsut2ie Date: Mon, 28 Apr 2025 18:36:24 +0800 Subject: [PATCH 12/22] ADD file via upload --- ForumController.java | 330 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 330 insertions(+) create mode 100644 ForumController.java diff --git a/ForumController.java b/ForumController.java new file mode 100644 index 0000000..579318d --- /dev/null +++ b/ForumController.java @@ -0,0 +1,330 @@ +package com.controller;// 定义包路径,表示该文件位于com.controller包下 + +import java.io.File; // 导入文件操作类 +import java.math.BigDecimal; // 导入高精度数学计算类 +import java.net.URL; // 导入网络URL处理类 +import java.text.SimpleDateFormat; // 导入日期格式化类 +import com.alibaba.fastjson.JSONObject; // 导入FastJSON的JSON对象类 +import java.util.*; // 导入Java常用工具类(集合、日期等) +import org.springframework.beans.BeanUtils; // 导入Spring Bean属性复制工具类 +import javax.servlet.http.HttpServletRequest; // 导入HTTP请求处理类 +import org.springframework.web.context.ContextLoader; // 导入Spring上下文加载器 +import javax.servlet.ServletContext; // 导入Servlet上下文接口 +import com.service.TokenService; // 导入自定义的Token服务类 +import com.utils.*; // 导入自定义工具类 +import java.lang.reflect.InvocationTargetException; // 导入反射异常类 + +import com.service.DictionaryService; // 导入自定义的字典服务类 +import org.apache.commons.lang3.StringUtils; // 导入Apache字符串工具类 +import com.annotation.IgnoreAuth; // 导入自定义的忽略认证注解 +import org.slf4j.Logger; // 导入日志接口 +import org.slf4j.LoggerFactory; // 导入日志工厂类 +import org.springframework.beans.factory.annotation.Autowired; // 导入Spring自动注入注解 +import org.springframework.stereotype.Controller; // 导入Spring控制器注解 +import org.springframework.web.bind.annotation.*; // 导入Spring Web注解 +import com.baomidou.mybatisplus.mapper.EntityWrapper; // 导入MyBatis Plus查询条件构造器 +import com.baomidou.mybatisplus.mapper.Wrapper; // 导入MyBatis Plus包装器接口 +import com.entity.*; // 导入实体类 +import com.entity.view.*; // 导入视图实体类 +import com.service.*; // 导入服务类 +import com.utils.PageUtils; // 导入分页工具类 +import com.utils.R; // 导入统一返回结果类 +import com.alibaba.fastjson.*; // 导入FastJSON相关类 + + + //健身论坛 + //后端接口 + //@author + //@email + +@RestController +@Controller +@RequestMapping("/forum") +public class ForumController { + private static final Logger logger = LoggerFactory.getLogger(ForumController.class); + + private static final String TABLE_NAME = "forum"; // 数据库表名 + + @Autowired + private ForumService forumService; // 论坛服务 + + @Autowired + private TokenService tokenService; // token服务 + + @Autowired + private DictionaryService dictionaryService; // 字典服务 + @Autowired + private JianshenkechengService jianshenkechengService; // 健身课程服务 + @Autowired + private JianshenkechengCollectionService jianshenkechengCollectionService; // 课程收藏服务 + @Autowired + private JianshenkechengLiuyanService jianshenkechengLiuyanService; // 课程留言服务 + @Autowired + private JiaolianService jiaolianService; // 教练服务 + @Autowired + private JiaolianYuyueService jiaolianYuyueService; // 教练预约申请服务 + @Autowired + private NewsService newsService; // 健身资讯服务 + @Autowired + private SingleSeachService singleSeachService; // 单页数据服务 + @Autowired + private YonghuService yonghuService; // 用户服务 + @Autowired + private UsersService usersService; // 管理员服务 + + + //后端列表 + @RequestMapping("/page") + public R page(@RequestParam Map params, HttpServletRequest request){ + logger.debug("page方法:,,Controller:{},,params:{}",this.getClass().getName(),JSONObject.toJSONString(params)); + CommonUtil.checkMap(params); // 检查参数 + PageUtils page = forumService.queryPage(params); // 查询分页数据 + + // 字典表数据转换 + List list =(List)page.getList(); + for(ForumView c:list){ + // 修改对应字典表字段 + dictionaryService.dictionaryConvert(c, request); + } + return R.ok().put("data", page); // 返回分页数据 + } + + + //后端详情 + @RequestMapping("/info/{id}") + public R info(@PathVariable("id") Long id, HttpServletRequest request){ + logger.debug("info方法:,,Controller:{},,id:{}",this.getClass().getName(),id); + ForumEntity forum = forumService.selectById(id); // 根据ID查询论坛帖子 + if(forum !=null){ + // entity转view + ForumView view = new ForumView(); + BeanUtils.copyProperties( forum , view ); // 把实体数据重构到view中 + // 级联表 用户 + YonghuEntity yonghu = yonghuService.selectById(forum.getYonghuId()); + if(yonghu != null){ + BeanUtils.copyProperties( yonghu , view ,new String[]{ "id", "createTime", "insertTime", "updateTime", "username", "password", "newMoney", "yonghuId" + , "jiaolianId" + , "usersId"}); // 把级联的数据添加到view中,并排除id和创建时间字段 + view.setYonghuId(yonghu.getId()); + } + // 级联表 教练 + JiaolianEntity jiaolian = jiaolianService.selectById(forum.getJiaolianId()); + if(jiaolian != null){ + BeanUtils.copyProperties( jiaolian , view ,new String[]{ "id", "createTime", "insertTime", "updateTime", "username", "password", "newMoney", "yonghuId" + , "jiaolianId" + , "usersId"}); // 把级联的数据添加到view中,并排除id和创建时间字段 + view.setJiaolianId(jiaolian.getId()); + } + // 级联表 管理员 + UsersEntity users = usersService.selectById(forum.getUsersId()); + if(users != null){ + view.setUsersId(users.getId()); // 设置管理员ID + view.setUusername(users.getUsername()); // 设置管理员用户名 + view.setUpassword(users.getPassword()); // 设置管理员密码 + view.setUrole(users.getRole()); // 设置管理员角色 + view.setUaddtime(users.getAddtime()); // 设置管理员添加时间 + } + // 修改对应字典表字段 + dictionaryService.dictionaryConvert(view, request); + return R.ok().put("data", view); // 返回帖子详情 + }else { + return R.error(511,"查不到数据"); // 查询不到数据返回错误 + } + } + + + //后端保存 + @RequestMapping("/save") + public R save(@RequestBody ForumEntity forum, HttpServletRequest request){ + logger.debug("save方法:,,Controller:{},,forum:{}",this.getClass().getName(),forum.toString()); + + String role = String.valueOf(request.getSession().getAttribute("role")); // 获取用户角色 + if(false) + return R.error(511,"永远不会进入"); + else if("用户".equals(role)) + forum.setYonghuId(Integer.valueOf(String.valueOf(request.getSession().getAttribute("userId")))); // 设置用户ID + else if("教练".equals(role)) + forum.setJiaolianId(Integer.valueOf(String.valueOf(request.getSession().getAttribute("userId")))); // 设置教练ID + else if("管理员".equals(role)) + forum.setUsersId(Integer.valueOf(String.valueOf(request.getSession().getAttribute("userId")))); // 设置管理员ID + + Wrapper queryWrapper = new EntityWrapper() + .eq("forum_name", forum.getForumName()) // 帖子标题 + .eq("yonghu_id", forum.getYonghuId()) // 用户ID + .eq("jiaolian_id", forum.getJiaolianId()) // 教练ID + .eq("users_id", forum.getUsersId()) // 管理员ID + .eq("super_ids", forum.getSuperIds()) // 父ID + .eq("forum_state_types", forum.getForumStateTypes()) // 帖子状态 + ; + + logger.info("sql语句:"+queryWrapper.getSqlSegment()); + ForumEntity forumEntity = forumService.selectOne(queryWrapper); // 查询是否已存在相同数据 + if(forumEntity==null){ + forum.setInsertTime(new Date()); // 设置插入时间 + forum.setCreateTime(new Date()); // 设置创建时间 + forumService.insert(forum); // 插入新数据 + return R.ok(); + }else { + return R.error(511,"表中有相同数据"); // 数据已存在返回错误 + } + } + + + //后端修改 + @RequestMapping("/update") + public R update(@RequestBody ForumEntity forum, HttpServletRequest request) throws NoSuchFieldException, ClassNotFoundException, IllegalAccessException, InstantiationException { + logger.debug("update方法:,,Controller:{},,forum:{}",this.getClass().getName(),forum.toString()); + ForumEntity oldForumEntity = forumService.selectById(forum.getId()); // 查询原先数据 + + String role = String.valueOf(request.getSession().getAttribute("role")); // 获取用户角色 + if("".equals(forum.getForumContent()) || "null".equals(forum.getForumContent())){ + forum.setForumContent(null); // 处理空内容 + } + forum.setUpdateTime(new Date()); // 设置更新时间 + + forumService.updateById(forum); // 根据ID更新数据 + return R.ok(); + } + + + //删除 + @RequestMapping("/delete") + public R delete(@RequestBody Integer[] ids, HttpServletRequest request){ + logger.debug("delete:,,Controller:{},,ids:{}",this.getClass().getName(),ids.toString()); + List oldForumList =forumService.selectBatchIds(Arrays.asList(ids)); // 查询要删除的数据 + forumService.deleteBatchIds(Arrays.asList(ids)); // 批量删除 + + return R.ok(); + } + + + //批量上传 + @RequestMapping("/batchInsert") + public R save( String fileName, HttpServletRequest request){ + logger.debug("batchInsert方法:,,Controller:{},,fileName:{}",this.getClass().getName(),fileName); + Integer yonghuId = Integer.valueOf(String.valueOf(request.getSession().getAttribute("userId"))); // 获取用户ID + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + try { + List forumList = new ArrayList<>(); // 存储上传的数据 + Map> seachFields= new HashMap<>(); // 要查询的字段 + Date date = new Date(); + int lastIndexOf = fileName.lastIndexOf("."); + if(lastIndexOf == -1){ + return R.error(511,"该文件没有后缀"); // 文件无后缀返回错误 + }else{ + String suffix = fileName.substring(lastIndexOf); + if(!".xls".equals(suffix)){ + return R.error(511,"只支持后缀为xls的excel文件"); // 文件格式不正确返回错误 + }else{ + URL resource = this.getClass().getClassLoader().getResource("static/upload/" + fileName); // 获取文件路径 + File file = new File(resource.getFile()); + if(!file.exists()){ + return R.error(511,"找不到上传文件,请联系管理员"); // 文件不存在返回错误 + }else{ + List> dataList = PoiUtil.poiImport(file.getPath()); // 读取xls文件 + dataList.remove(0); // 删除第一行提示信息 + for(List data:dataList){ + // 循环处理每行数据 + ForumEntity forumEntity = new ForumEntity(); + forumList.add(forumEntity); // 添加到列表 + } + // 批量插入数据 + forumService.insertBatch(forumList); + return R.ok(); + } + } + } + }catch (Exception e){ + e.printStackTrace(); + return R.error(511,"批量插入数据异常,请联系管理员"); // 异常处理 + } + } + + // 前端列表 + + @IgnoreAuth // 忽略认证 + @RequestMapping("/list") + public R list(@RequestParam Map params, HttpServletRequest request){ + logger.debug("list方法:,,Controller:{},,params:{}",this.getClass().getName(),JSONObject.toJSONString(params)); + + CommonUtil.checkMap(params); // 检查参数 + PageUtils page = forumService.queryPage(params); // 查询分页数据 + + // 字典表数据转换 + List list =(List)page.getList(); + for(ForumView c:list) + dictionaryService.dictionaryConvert(c, request); // 修改对应字典表字段 + + return R.ok().put("data", page); // 返回分页数据 + } + + + //前端详情 + @RequestMapping("/detail/{id}") + public R detail(@PathVariable("id") Integer id, HttpServletRequest request){ + logger.debug("detail方法:,,Controller:{},,id:{}",this.getClass().getName(),id); + ForumEntity forum = forumService.selectById(id); // 根据ID查询帖子 + if(forum !=null){ + // entity转view + ForumView view = new ForumView(); + BeanUtils.copyProperties( forum , view ); // 把实体数据重构到view中 + + // 级联表用户 + YonghuEntity yonghu = yonghuService.selectById(forum.getYonghuId()); + if(yonghu != null){ + BeanUtils.copyProperties( yonghu , view ,new String[]{ "id", "createTime", "insertTime", "updateTime", "username", "password", "newMoney", "yonghuId" + , "jiaolianId" + , "usersId"}); // 把级联的数据添加到view中,并排除id和创建时间字段 + view.setYonghuId(yonghu.getId()); + } + // 级联表教练 + JiaolianEntity jiaolian = jiaolianService.selectById(forum.getJiaolianId()); + if(jiaolian != null){ + BeanUtils.copyProperties( jiaolian , view ,new String[]{ "id", "createTime", "insertTime", "updateTime", "username", "password", "newMoney", "yonghuId" + , "jiaolianId" + , "usersId"}); // 把级联的数据添加到view中,并排除id和创建时间字段 + view.setJiaolianId(jiaolian.getId()); + } + // 级联表管理员 + UsersEntity users = usersService.selectById(forum.getUsersId()); + if(users != null){ + view.setUsersId(users.getId()); // 设置管理员ID + view.setUusername(users.getUsername()); // 设置管理员用户名 + view.setUpassword(users.getPassword()); // 设置管理员密码 + view.setUrole(users.getRole()); // 设置管理员角色 + view.setUaddtime(users.getAddtime()); // 设置管理员添加时间 + } + // 修改对应字典表字段 + dictionaryService.dictionaryConvert(view, request); + return R.ok().put("data", view); // 返回帖子详情 + }else { + return R.error(511,"查不到数据"); // 查询不到数据返回错误 + } + } + + + //前端保存 + @RequestMapping("/add") + public R add(@RequestBody ForumEntity forum, HttpServletRequest request){ + logger.debug("add方法:,,Controller:{},,forum:{}",this.getClass().getName(),forum.toString()); + Wrapper queryWrapper = new EntityWrapper() + .eq("forum_name", forum.getForumName()) // 帖子标题 + .eq("yonghu_id", forum.getYonghuId()) // 用户ID + .eq("jiaolian_id", forum.getJiaolianId()) // 教练ID + .eq("users_id", forum.getUsersId()) // 管理员ID + .eq("super_ids", forum.getSuperIds()) // 父ID + .eq("forum_state_types", forum.getForumStateTypes()) // 帖子状态 + ; + logger.info("sql语句:"+queryWrapper.getSqlSegment()); + ForumEntity forumEntity = forumService.selectOne(queryWrapper); // 查询是否已存在相同数据 + if(forumEntity==null){ + forum.setInsertTime(new Date()); // 设置插入时间 + forum.setCreateTime(new Date()); // 设置创建时间 + forumService.insert(forum); // 插入新数据 + + return R.ok(); + }else { + return R.error(511,"表中有相同数据"); // 数据已存在返回错误 + } + } +} \ No newline at end of file From ade6b76f185151c0a55ef06e922e572d4979c1cc Mon Sep 17 00:00:00 2001 From: phfsut2ie Date: Mon, 28 Apr 2025 18:36:35 +0800 Subject: [PATCH 13/22] ADD file via upload --- JianshenkechengCollectionController.java | 295 +++++++++++++++++++++++ 1 file changed, 295 insertions(+) create mode 100644 JianshenkechengCollectionController.java diff --git a/JianshenkechengCollectionController.java b/JianshenkechengCollectionController.java new file mode 100644 index 0000000..cab3b5b --- /dev/null +++ b/JianshenkechengCollectionController.java @@ -0,0 +1,295 @@ +package com.controller; // 定义包路径,表示该文件位于com.controller包下 + +import java.io.File; // 导入文件操作类 +import java.math.BigDecimal; // 导入高精度数学计算类 +import java.net.URL; // 导入网络URL处理类 +import java.text.SimpleDateFormat; // 导入日期格式化类 +import com.alibaba.fastjson.JSONObject; // 导入FastJSON的JSON对象类 +import java.util.*; // 导入Java常用工具类(集合、日期等) +import org.springframework.beans.BeanUtils; // 导入Spring Bean属性复制工具类 +import javax.servlet.http.HttpServletRequest; // 导入HTTP请求处理类 +import org.springframework.web.context.ContextLoader; // 导入Spring上下文加载器 +import javax.servlet.ServletContext; // 导入Servlet上下文接口 +import com.service.TokenService; // 导入自定义的Token服务类 +import com.utils.*; // 导入自定义工具类 +import java.lang.reflect.InvocationTargetException; // 导入反射异常类 + +import com.service.DictionaryService; // 导入自定义的字典服务类 +import org.apache.commons.lang3.StringUtils; // 导入Apache字符串工具类 +import com.annotation.IgnoreAuth; // 导入自定义的忽略认证注解 +import org.slf4j.Logger; // 导入日志接口 +import org.slf4j.LoggerFactory; // 导入日志工厂类 +import org.springframework.beans.factory.annotation.Autowired; // 导入Spring自动注入注解 +import org.springframework.stereotype.Controller; // 导入Spring控制器注解 +import org.springframework.web.bind.annotation.*; // 导入Spring Web注解 +import com.baomidou.mybatisplus.mapper.EntityWrapper; // 导入MyBatis Plus查询条件构造器 +import com.baomidou.mybatisplus.mapper.Wrapper; // 导入MyBatis Plus包装器接口 +import com.entity.*; // 导入实体类 +import com.entity.view.*; // 导入视图实体类 +import com.service.*; // 导入服务类 +import com.utils.PageUtils; // 导入分页工具类 +import com.utils.R; // 导入统一返回结果类 +import com.alibaba.fastjson.*; // 导入FastJSON相关类 + + + //课程收藏 + //后端接口 + //@author + // @email + +@RestController // 标识为RESTful控制器 +@Controller // 标识为Spring控制器 +@RequestMapping("/jianshenkechengCollection") // 定义请求映射路径 +public class JianshenkechengCollectionController { + private static final Logger logger = LoggerFactory.getLogger(JianshenkechengCollectionController.class); // 日志记录器 + + private static final String TABLE_NAME = "jianshenkechengCollection"; // 数据库表名 + + @Autowired + private JianshenkechengCollectionService jianshenkechengCollectionService; // 课程收藏服务 + + @Autowired + private TokenService tokenService; // Token服务 + + @Autowired + private DictionaryService dictionaryService; // 字典服务 + @Autowired + private ForumService forumService; // 健身论坛服务 + @Autowired + private JianshenkechengService jianshenkechengService; // 健身课程服务 + @Autowired + private JianshenkechengLiuyanService jianshenkechengLiuyanService; // 课程留言服务 + @Autowired + private JiaolianService jiaolianService; // 教练服务 + @Autowired + private JiaolianYuyueService jiaolianYuyueService; // 教练预约申请服务 + @Autowired + private NewsService newsService; // 健身资讯服务 + @Autowired + private SingleSeachService singleSeachService; // 单页数据服务 + @Autowired + private YonghuService yonghuService; // 用户服务 + @Autowired + private UsersService usersService; // 管理员服务 + + + //后端列表 + @RequestMapping("/page") // 处理分页请求 + public R page(@RequestParam Map params, HttpServletRequest request){ + logger.debug("page方法:,,Controller:{},,params:{}",this.getClass().getName(),JSONObject.toJSONString(params)); // 记录日志 + String role = String.valueOf(request.getSession().getAttribute("role")); // 获取用户角色 + if(false) + return R.error(511,"永不会进入"); // 永远不会执行的代码 + else if("用户".equals(role)) + params.put("yonghuId",request.getSession().getAttribute("userId")); // 如果是用户角色,添加用户ID参数 + else if("教练".equals(role)) + params.put("jiaolianId",request.getSession().getAttribute("userId")); // 如果是教练角色,添加教练ID参数 + CommonUtil.checkMap(params); // 检查参数 + PageUtils page = jianshenkechengCollectionService.queryPage(params); // 查询分页数据 + + // 字典表数据转换 + List list =(List)page.getList(); + for(JianshenkechengCollectionView c:list){ + // 修改对应字典表字段 + dictionaryService.dictionaryConvert(c, request); + } + return R.ok().put("data", page); // 返回分页数据 + } + + + //后端详情 + @RequestMapping("/info/{id}") // 处理详情请求 + public R info(@PathVariable("id") Long id, HttpServletRequest request){ + logger.debug("info方法:,,Controller:{},,id:{}",this.getClass().getName(),id); // 记录日志 + JianshenkechengCollectionEntity jianshenkechengCollection = jianshenkechengCollectionService.selectById(id); // 根据ID查询收藏记录 + if(jianshenkechengCollection !=null){ + // entity转view + JianshenkechengCollectionView view = new JianshenkechengCollectionView(); + BeanUtils.copyProperties( jianshenkechengCollection , view ); // 把实体数据重构到view中 + // 级联表 健身课程 + JianshenkechengEntity jianshenkecheng = jianshenkechengService.selectById(jianshenkechengCollection.getJianshenkechengId()); + if(jianshenkecheng != null){ + BeanUtils.copyProperties( jianshenkecheng , view ,new String[]{ "id", "createTime", "insertTime", "updateTime", "username", "password", "newMoney", "yonghuId"}); // 把级联的数据添加到view中,并排除id和创建时间字段 + view.setJianshenkechengId(jianshenkecheng.getId()); + } + // 级联表 用户 + YonghuEntity yonghu = yonghuService.selectById(jianshenkechengCollection.getYonghuId()); + if(yonghu != null){ + BeanUtils.copyProperties( yonghu , view ,new String[]{ "id", "createTime", "insertTime", "updateTime", "username", "password", "newMoney", "yonghuId"}); // 把级联的数据添加到view中,并排除id和创建时间字段 + view.setYonghuId(yonghu.getId()); + } + // 修改对应字典表字段 + dictionaryService.dictionaryConvert(view, request); + return R.ok().put("data", view); // 返回详情数据 + }else { + return R.error(511,"查不到数据"); // 查询不到数据返回错误 + } + } + + + //后端保存 + @RequestMapping("/save") // 处理保存请求 + public R save(@RequestBody JianshenkechengCollectionEntity jianshenkechengCollection, HttpServletRequest request){ + logger.debug("save方法:,,Controller:{},,jianshenkechengCollection:{}",this.getClass().getName(),jianshenkechengCollection.toString()); // 记录日志 + + String role = String.valueOf(request.getSession().getAttribute("role")); // 获取用户角色 + if(false) + return R.error(511,"永远不会进入"); // 永远不会执行的代码 + else if("用户".equals(role)) + jianshenkechengCollection.setYonghuId(Integer.valueOf(String.valueOf(request.getSession().getAttribute("userId")))); // 如果是用户角色,设置用户ID + + Wrapper queryWrapper = new EntityWrapper() + .eq("jianshenkecheng_id", jianshenkechengCollection.getJianshenkechengId()) // 健身课程ID + .eq("yonghu_id", jianshenkechengCollection.getYonghuId()) // 用户ID + .eq("jianshenkecheng_collection_types", jianshenkechengCollection.getJianshenkechengCollectionTypes()) // 收藏类型 + ; + + logger.info("sql语句:"+queryWrapper.getSqlSegment()); // 记录SQL语句 + JianshenkechengCollectionEntity jianshenkechengCollectionEntity = jianshenkechengCollectionService.selectOne(queryWrapper); // 查询是否已存在相同数据 + if(jianshenkechengCollectionEntity==null){ + jianshenkechengCollection.setInsertTime(new Date()); // 设置插入时间 + jianshenkechengCollection.setCreateTime(new Date()); // 设置创建时间 + jianshenkechengCollectionService.insert(jianshenkechengCollection); // 插入新数据 + return R.ok(); // 返回成功 + }else { + return R.error(511,"表中有相同数据"); // 数据已存在返回错误 + } + } + + + //后端修改 + @RequestMapping("/update") // 处理更新请求 + public R update(@RequestBody JianshenkechengCollectionEntity jianshenkechengCollection, HttpServletRequest request) throws NoSuchFieldException, ClassNotFoundException, IllegalAccessException, InstantiationException { + logger.debug("update方法:,,Controller:{},,jianshenkechengCollection:{}",this.getClass().getName(),jianshenkechengCollection.toString()); // 记录日志 + JianshenkechengCollectionEntity oldJianshenkechengCollectionEntity = jianshenkechengCollectionService.selectById(jianshenkechengCollection.getId()); // 查询原先数据 + + jianshenkechengCollectionService.updateById(jianshenkechengCollection); // 根据ID更新数据 + return R.ok(); // 返回成功 + } + + + //删除 + @RequestMapping("/delete") // 处理删除请求 + public R delete(@RequestBody Integer[] ids, HttpServletRequest request){ + logger.debug("delete:,,Controller:{},,ids:{}",this.getClass().getName(),ids.toString()); // 记录日志 + List oldJianshenkechengCollectionList =jianshenkechengCollectionService.selectBatchIds(Arrays.asList(ids)); // 查询要删除的数据 + jianshenkechengCollectionService.deleteBatchIds(Arrays.asList(ids)); // 批量删除 + + return R.ok(); // 返回成功 + } + + + //批量上传 + @RequestMapping("/batchInsert") // 处理批量上传请求 + public R save( String fileName, HttpServletRequest request){ + logger.debug("batchInsert方法:,,Controller:{},,fileName:{}",this.getClass().getName(),fileName); // 记录日志 + Integer yonghuId = Integer.valueOf(String.valueOf(request.getSession().getAttribute("userId"))); // 获取用户ID + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); // 创建日期格式化对象 + try { + List jianshenkechengCollectionList = new ArrayList<>(); // 创建收藏记录列表 + Map> seachFields= new HashMap<>(); // 创建查询字段映射 + Date date = new Date(); // 当前时间 + int lastIndexOf = fileName.lastIndexOf("."); // 获取文件后缀位置 + if(lastIndexOf == -1){ + return R.error(511,"该文件没有后缀"); // 文件无后缀返回错误 + }else{ + String suffix = fileName.substring(lastIndexOf); // 获取文件后缀 + if(!".xls".equals(suffix)){ + return R.error(511,"只支持后缀为xls的excel文件"); // 文件格式不正确返回错误 + }else{ + URL resource = this.getClass().getClassLoader().getResource("static/upload/" + fileName); // 获取文件路径 + File file = new File(resource.getFile()); // 创建文件对象 + if(!file.exists()){ + return R.error(511,"找不到上传文件,请联系管理员"); // 文件不存在返回错误 + }else{ + List> dataList = PoiUtil.poiImport(file.getPath()); // 读取xls文件 + dataList.remove(0); // 删除第一行提示信息 + for(List data:dataList){ + // 循环处理每行数据 + JianshenkechengCollectionEntity jianshenkechengCollectionEntity = new JianshenkechengCollectionEntity(); + jianshenkechengCollectionList.add(jianshenkechengCollectionEntity); // 添加到列表 + } + // 批量插入数据 + jianshenkechengCollectionService.insertBatch(jianshenkechengCollectionList); + return R.ok(); // 返回成功 + } + } + } + }catch (Exception e){ + e.printStackTrace(); // 打印异常堆栈 + return R.error(511,"批量插入数据异常,请联系管理员"); // 异常处理 + } + } + + + //前端列表 + @IgnoreAuth // 忽略认证 + @RequestMapping("/list") // 处理列表请求 + public R list(@RequestParam Map params, HttpServletRequest request){ + logger.debug("list方法:,,Controller:{},,params:{}",this.getClass().getName(),JSONObject.toJSONString(params)); // 记录日志 + + CommonUtil.checkMap(params); // 检查参数 + PageUtils page = jianshenkechengCollectionService.queryPage(params); // 查询分页数据 + + // 字典表数据转换 + List list =(List)page.getList(); + for(JianshenkechengCollectionView c:list) + dictionaryService.dictionaryConvert(c, request); // 修改对应字典表字段 + + return R.ok().put("data", page); // 返回分页数据 + } + + + //前端详情 + @RequestMapping("/detail/{id}") // 处理详情请求 + public R detail(@PathVariable("id") Integer id, HttpServletRequest request){ + logger.debug("detail方法:,,Controller:{},,id:{}",this.getClass().getName(),id); // 记录日志 + JianshenkechengCollectionEntity jianshenkechengCollection = jianshenkechengCollectionService.selectById(id); // 根据ID查询收藏记录 + if(jianshenkechengCollection !=null){ + // entity转view + JianshenkechengCollectionView view = new JianshenkechengCollectionView(); + BeanUtils.copyProperties( jianshenkechengCollection , view ); // 把实体数据重构到view中 + + // 级联表健身课程 + JianshenkechengEntity jianshenkecheng = jianshenkechengService.selectById(jianshenkechengCollection.getJianshenkechengId()); + if(jianshenkecheng != null){ + BeanUtils.copyProperties( jianshenkecheng , view ,new String[]{ "id", "createTime", "insertTime", "updateTime", "username", "password", "newMoney", "yonghuId"}); // 把级联的数据添加到view中,并排除id和创建时间字段 + view.setJianshenkechengId(jianshenkecheng.getId()); + } + // 级联表用户 + YonghuEntity yonghu = yonghuService.selectById(jianshenkechengCollection.getYonghuId()); + if(yonghu != null){ + BeanUtils.copyProperties( yonghu , view ,new String[]{ "id", "createTime", "insertTime", "updateTime", "username", "password", "newMoney", "yonghuId"}); // 把级联的数据添加到view中,并排除id和创建时间字段 + view.setYonghuId(yonghu.getId()); + } + // 修改对应字典表字段 + dictionaryService.dictionaryConvert(view, request); + return R.ok().put("data", view); // 返回详情数据 + }else { + return R.error(511,"查不到数据"); // 查询不到数据返回错误 + } + } + + + //前端保存 + @RequestMapping("/add") // 处理添加请求 + public R add(@RequestBody JianshenkechengCollectionEntity jianshenkechengCollection, HttpServletRequest request){ + logger.debug("add方法:,,Controller:{},,jianshenkechengCollection:{}",this.getClass().getName(),jianshenkechengCollection.toString()); // 记录日志 + Wrapper queryWrapper = new EntityWrapper() + .eq("jianshenkecheng_id", jianshenkechengCollection.getJianshenkechengId()) // 健身课程ID + .eq("yonghu_id", jianshenkechengCollection.getYonghuId()) // 用户ID + .eq("jianshenkecheng_collection_types", jianshenkechengCollection.getJianshenkechengCollectionTypes()) // 收藏类型 + ; + logger.info("sql语句:"+queryWrapper.getSqlSegment()); // 记录SQL语句 + JianshenkechengCollectionEntity jianshenkechengCollectionEntity = jianshenkechengCollectionService.selectOne(queryWrapper); // 查询是否已存在相同数据 + if(jianshenkechengCollectionEntity==null){ + jianshenkechengCollection.setInsertTime(new Date()); // 设置插入时间 + jianshenkechengCollection.setCreateTime(new Date()); // 设置创建时间 + jianshenkechengCollectionService.insert(jianshenkechengCollection); // 插入新数据 + + return R.ok(); // 返回成功 + }else { + return R.error(511,"您已经收藏过了"); // 已经收藏过返回错误 + } + } +} \ No newline at end of file From e61cf8a4af5fbb5e51552e5561687dc3dd9afcfd Mon Sep 17 00:00:00 2001 From: phfsut2ie Date: Mon, 28 Apr 2025 18:36:50 +0800 Subject: [PATCH 14/22] ADD file via upload --- JianshenkechengLiuyanController.java | 285 +++++++++++++++++++++++++++ 1 file changed, 285 insertions(+) create mode 100644 JianshenkechengLiuyanController.java diff --git a/JianshenkechengLiuyanController.java b/JianshenkechengLiuyanController.java new file mode 100644 index 0000000..e6f4750 --- /dev/null +++ b/JianshenkechengLiuyanController.java @@ -0,0 +1,285 @@ +package com.controller; // 声明包路径,表示该文件位于com.controller包下 + +import java.io.File; // 导入文件操作类,用于处理文件上传 +import java.math.BigDecimal; // 导入高精度数学计算类,用于处理金额计算 +import java.net.URL; // 导入网络URL处理类,用于获取资源路径 +import java.text.SimpleDateFormat; // 导入日期格式化类,用于日期格式转换 +import com.alibaba.fastjson.JSONObject; // 导入FastJSON的JSON对象类,用于JSON处理 +import java.util.*; // 导入Java常用工具类(集合、日期等) +import org.springframework.beans.BeanUtils; // 导入Spring Bean属性复制工具类 +import javax.servlet.http.HttpServletRequest; // 导入HTTP请求处理类 +import org.springframework.web.context.ContextLoader; // 导入Spring上下文加载器 +import javax.servlet.ServletContext; // 导入Servlet上下文接口 +import com.service.TokenService; // 导入自定义的Token服务类 +import com.utils.*; // 导入自定义工具类 +import java.lang.reflect.InvocationTargetException; // 导入反射异常类 + +import com.service.DictionaryService; // 导入自定义的字典服务类 +import org.apache.commons.lang3.StringUtils; // 导入Apache字符串工具类 +import com.annotation.IgnoreAuth; // 导入自定义的忽略认证注解 +import org.slf4j.Logger; // 导入日志接口 +import org.slf4j.LoggerFactory; // 导入日志工厂类 +import org.springframework.beans.factory.annotation.Autowired; // 导入Spring自动注入注解 +import org.springframework.stereotype.Controller; // 导入Spring控制器注解 +import org.springframework.web.bind.annotation.*; // 导入Spring Web注解 +import com.baomidou.mybatisplus.mapper.EntityWrapper; // 导入MyBatis Plus查询条件构造器 +import com.baomidou.mybatisplus.mapper.Wrapper; // 导入MyBatis Plus包装器接口 +import com.entity.*; // 导入实体类 +import com.entity.view.*; // 导入视图实体类 +import com.service.*; // 导入服务类 +import com.utils.PageUtils; // 导入分页工具类 +import com.utils.R; // 导入统一返回结果类 +import com.alibaba.fastjson.*; // 导入FastJSON相关类 + + + //课程留言 + //后端接口 + //@author + //@email +@RestController // 标识为RESTful控制器,用于处理HTTP请求 +@Controller // 标识为Spring控制器 +@RequestMapping("/jianshenkechengLiuyan") // 定义请求映射路径 +public class JianshenkechengLiuyanController { + private static final Logger logger = LoggerFactory.getLogger(JianshenkechengLiuyanController.class); // 日志记录器,用于记录日志信息 + + private static final String TABLE_NAME = "jianshenkechengLiuyan"; // 数据库表名常量 + + @Autowired + private JianshenkechengLiuyanService jianshenkechengLiuyanService; // 自动注入课程留言服务 + + @Autowired + private TokenService tokenService; // 自动注入Token服务 + + @Autowired + private DictionaryService dictionaryService; // 自动注入字典服务 + @Autowired + private ForumService forumService; // 自动注入健身论坛服务 + @Autowired + private JianshenkechengService jianshenkechengService; // 自动注入健身课程服务 + @Autowired + private JianshenkechengCollectionService jianshenkechengCollectionService; // 自动注入课程收藏服务 + @Autowired + private JiaolianService jiaolianService; // 自动注入教练服务 + @Autowired + private JiaolianYuyueService jiaolianYuyueService; // 自动注入教练预约申请服务 + @Autowired + private NewsService newsService; // 自动注入健身资讯服务 + @Autowired + private SingleSeachService singleSeachService; // 自动注入单页数据服务 + @Autowired + private YonghuService yonghuService; // 自动注入用户服务 + @Autowired + private UsersService usersService; // 自动注入管理员服务 + + + //后端列表 + //处理分页查询请求 + @RequestMapping("/page") // 映射分页查询请求 + public R page(@RequestParam Map params, HttpServletRequest request){ + logger.debug("page方法:,,Controller:{},,params:{}",this.getClass().getName(),JSONObject.toJSONString(params)); // 记录请求日志 + String role = String.valueOf(request.getSession().getAttribute("role")); // 从session中获取用户角色 + if(false) // 永远不会执行的代码块 + return R.error(511,"永不会进入"); // 返回错误信息 + else if("用户".equals(role)) // 如果是用户角色 + params.put("yonghuId",request.getSession().getAttribute("userId")); // 将用户ID添加到查询参数 + else if("教练".equals(role)) // 如果是教练角色 + params.put("jiaolianId",request.getSession().getAttribute("userId")); // 将教练ID添加到查询参数 + CommonUtil.checkMap(params); // 检查参数有效性 + PageUtils page = jianshenkechengLiuyanService.queryPage(params); // 调用服务层查询分页数据 + + // 字典表数据转换 + List list =(List)page.getList(); // 获取分页数据列表 + for(JianshenkechengLiuyanView c:list){ // 遍历列表 + dictionaryService.dictionaryConvert(c, request); // 转换字典字段 + } + return R.ok().put("data", page); // 返回分页数据 + } + + + //后端详情 + //根据ID查询单条留言详情 + @RequestMapping("/info/{id}") // 映射详情查询请求 + public R info(@PathVariable("id") Long id, HttpServletRequest request){ + logger.debug("info方法:,,Controller:{},,id:{}",this.getClass().getName(),id); // 记录请求日志 + JianshenkechengLiuyanEntity jianshenkechengLiuyan = jianshenkechengLiuyanService.selectById(id); // 根据ID查询留言 + if(jianshenkechengLiuyan !=null){ // 如果查询到数据 + JianshenkechengLiuyanView view = new JianshenkechengLiuyanView(); // 创建视图对象 + BeanUtils.copyProperties(jianshenkechengLiuyan, view); // 复制属性到视图对象 + + // 查询关联的健身课程信息 + JianshenkechengEntity jianshenkecheng = jianshenkechengService.selectById(jianshenkechengLiuyan.getJianshenkechengId()); + if(jianshenkecheng != null){ + BeanUtils.copyProperties(jianshenkecheng, view, new String[]{ "id", "createTime", "insertTime", "updateTime", "username", "password", "newMoney", "yonghuId"}); + view.setJianshenkechengId(jianshenkecheng.getId()); + } + + // 查询关联的用户信息 + YonghuEntity yonghu = yonghuService.selectById(jianshenkechengLiuyan.getYonghuId()); + if(yonghu != null){ + BeanUtils.copyProperties(yonghu, view, new String[]{ "id", "createTime", "insertTime", "updateTime", "username", "password", "newMoney", "yonghuId"}); + view.setYonghuId(yonghu.getId()); + } + + dictionaryService.dictionaryConvert(view, request); // 转换字典字段 + return R.ok().put("data", view); // 返回详情数据 + }else { + return R.error(511,"查不到数据"); // 返回错误信息 + } + } + + + //后端保存 + //处理新增留言请求 + @RequestMapping("/save") // 映射保存请求 + public R save(@RequestBody JianshenkechengLiuyanEntity jianshenkechengLiuyan, HttpServletRequest request){ + logger.debug("save方法:,,Controller:{},,jianshenkechengLiuyan:{}",this.getClass().getName(),jianshenkechengLiuyan.toString()); // 记录请求日志 + + String role = String.valueOf(request.getSession().getAttribute("role")); // 获取用户角色 + if(false) // 永远不会执行的代码块 + return R.error(511,"永远不会进入"); // 返回错误信息 + else if("用户".equals(role)) // 如果是用户角色 + jianshenkechengLiuyan.setYonghuId(Integer.valueOf(String.valueOf(request.getSession().getAttribute("userId")))); // 设置用户ID + + jianshenkechengLiuyan.setCreateTime(new Date()); // 设置创建时间 + jianshenkechengLiuyan.setInsertTime(new Date()); // 设置插入时间 + jianshenkechengLiuyanService.insert(jianshenkechengLiuyan); // 调用服务层保存数据 + + return R.ok(); // 返回成功信息 + } + + + //后端修改 + //处理更新留言请 + @RequestMapping("/update") // 映射更新请求 + public R update(@RequestBody JianshenkechengLiuyanEntity jianshenkechengLiuyan, HttpServletRequest request) throws NoSuchFieldException, ClassNotFoundException, IllegalAccessException, InstantiationException { + logger.debug("update方法:,,Controller:{},,jianshenkechengLiuyan:{}",this.getClass().getName(),jianshenkechengLiuyan.toString()); // 记录请求日志 + JianshenkechengLiuyanEntity oldJianshenkechengLiuyanEntity = jianshenkechengLiuyanService.selectById(jianshenkechengLiuyan.getId()); // 查询原有数据 + + // 处理空值情况 + if("".equals(jianshenkechengLiuyan.getJianshenkechengLiuyanText()) || "null".equals(jianshenkechengLiuyan.getJianshenkechengLiuyanText())){ + jianshenkechengLiuyan.setJianshenkechengLiuyanText(null); + } + if("".equals(jianshenkechengLiuyan.getReplyText()) || "null".equals(jianshenkechengLiuyan.getReplyText())){ + jianshenkechengLiuyan.setReplyText(null); + } + + jianshenkechengLiuyan.setUpdateTime(new Date()); // 设置更新时间 + jianshenkechengLiuyanService.updateById(jianshenkechengLiuyan); // 调用服务层更新数据 + return R.ok(); // 返回成功信息 + } + + + //删除 + //处理删除留言请求 + @RequestMapping("/delete") // 映射删除请求 + public R delete(@RequestBody Integer[] ids, HttpServletRequest request){ + logger.debug("delete:,,Controller:{},,ids:{}",this.getClass().getName(),ids.toString()); // 记录请求日志 + List oldJianshenkechengLiuyanList = jianshenkechengLiuyanService.selectBatchIds(Arrays.asList(ids)); // 查询要删除的数据 + jianshenkechengLiuyanService.deleteBatchIds(Arrays.asList(ids)); // 调用服务层批量删除 + return R.ok(); // 返回成功信息 + } + + + //批量上传 + //处理批量导入数据请求 + @RequestMapping("/batchInsert") // 映射批量导入请求 + public R save(String fileName, HttpServletRequest request){ + logger.debug("batchInsert方法:,,Controller:{},,fileName:{}",this.getClass().getName(),fileName); // 记录请求日志 + Integer yonghuId = Integer.valueOf(String.valueOf(request.getSession().getAttribute("userId"))); // 获取用户ID + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); // 创建日期格式化对象 + try { + List jianshenkechengLiuyanList = new ArrayList<>(); // 创建数据列表 + Map> seachFields = new HashMap<>(); // 创建查询字段映射 + Date date = new Date(); // 当前时间 + int lastIndexOf = fileName.lastIndexOf("."); // 获取文件后缀位置 + if(lastIndexOf == -1){ // 如果没有后缀 + return R.error(511,"该文件没有后缀"); // 返回错误信息 + }else{ + String suffix = fileName.substring(lastIndexOf); // 获取文件后缀 + if(!".xls".equals(suffix)){ // 如果不是xls文件 + return R.error(511,"只支持后缀为xls的excel文件"); // 返回错误信息 + }else{ + URL resource = this.getClass().getClassLoader().getResource("static/upload/" + fileName); // 获取文件路径 + File file = new File(resource.getFile()); // 创建文件对象 + if(!file.exists()){ // 如果文件不存在 + return R.error(511,"找不到上传文件,请联系管理员"); // 返回错误信息 + }else{ + List> dataList = PoiUtil.poiImport(file.getPath()); // 读取Excel文件 + dataList.remove(0); // 删除标题行 + for(List data:dataList){ // 遍历数据行 + JianshenkechengLiuyanEntity jianshenkechengLiuyanEntity = new JianshenkechengLiuyanEntity(); // 创建实体对象 + jianshenkechengLiuyanList.add(jianshenkechengLiuyanEntity); // 添加到列表 + } + jianshenkechengLiuyanService.insertBatch(jianshenkechengLiuyanList); // 批量插入数据 + return R.ok(); // 返回成功信息 + } + } + } + }catch (Exception e){ + e.printStackTrace(); // 打印异常堆栈 + return R.error(511,"批量插入数据异常,请联系管理员"); // 返回错误信息 + } + } + + + //前端列表 + //处理前端分页查询请求 + @IgnoreAuth // 忽略认证 + @RequestMapping("/list") // 映射列表查询请求 + public R list(@RequestParam Map params, HttpServletRequest request){ + logger.debug("list方法:,,Controller:{},,params:{}",this.getClass().getName(),JSONObject.toJSONString(params)); // 记录请求日志 + CommonUtil.checkMap(params); // 检查参数有效性 + PageUtils page = jianshenkechengLiuyanService.queryPage(params); // 调用服务层查询分页数据 + + // 字典表数据转换 + List list =(List)page.getList(); // 获取分页数据列表 + for(JianshenkechengLiuyanView c:list){ // 遍历列表 + dictionaryService.dictionaryConvert(c, request); // 转换字典字段 + } + return R.ok().put("data", page); // 返回分页数据 + } + + + //前端详情 + //处理前端详情查询请求 + @RequestMapping("/detail/{id}") // 映射详情查询请求 + public R detail(@PathVariable("id") Integer id, HttpServletRequest request){ + logger.debug("detail方法:,,Controller:{},,id:{}",this.getClass().getName(),id); // 记录请求日志 + JianshenkechengLiuyanEntity jianshenkechengLiuyan = jianshenkechengLiuyanService.selectById(id); // 根据ID查询留言 + if(jianshenkechengLiuyan !=null){ // 如果查询到数据 + JianshenkechengLiuyanView view = new JianshenkechengLiuyanView(); // 创建视图对象 + BeanUtils.copyProperties(jianshenkechengLiuyan, view); // 复制属性到视图对象 + + // 查询关联的健身课程信息 + JianshenkechengEntity jianshenkecheng = jianshenkechengService.selectById(jianshenkechengLiuyan.getJianshenkechengId()); + if(jianshenkecheng != null){ + BeanUtils.copyProperties(jianshenkecheng, view, new String[]{ "id", "createTime", "insertTime", "updateTime", "username", "password", "newMoney", "yonghuId"}); + view.setJianshenkechengId(jianshenkecheng.getId()); + } + + // 查询关联的用户信息 + YonghuEntity yonghu = yonghuService.selectById(jianshenkechengLiuyan.getYonghuId()); + if(yonghu != null){ + BeanUtils.copyProperties(yonghu, view, new String[]{ "id", "createTime", "insertTime", "updateTime", "username", "password", "newMoney", "yonghuId"}); + view.setYonghuId(yonghu.getId()); + } + + dictionaryService.dictionaryConvert(view, request); // 转换字典字段 + return R.ok().put("data", view); // 返回详情数据 + }else { + return R.error(511,"查不到数据"); // 返回错误信息 + } + } + + + //前端保存 + //处理前端新增留言请求 + @RequestMapping("/add") // 映射新增请求 + public R add(@RequestBody JianshenkechengLiuyanEntity jianshenkechengLiuyan, HttpServletRequest request){ + logger.debug("add方法:,,Controller:{},,jianshenkechengLiuyan:{}",this.getClass().getName(),jianshenkechengLiuyan.toString()); // 记录请求日志 + jianshenkechengLiuyan.setCreateTime(new Date()); // 设置创建时间 + jianshenkechengLiuyan.setInsertTime(new Date()); // 设置插入时间 + jianshenkechengLiuyanService.insert(jianshenkechengLiuyan); // 调用服务层保存数据 + return R.ok(); // 返回成功信息 + } +} \ No newline at end of file From 217ec9aadb3ae7787ca95d791dae96dc54e26243 Mon Sep 17 00:00:00 2001 From: phfsut2ie Date: Mon, 28 Apr 2025 18:37:06 +0800 Subject: [PATCH 15/22] ADD file via upload --- JianshenkechengController.java | 389 +++++++++++++++++++++++++++++++++ 1 file changed, 389 insertions(+) create mode 100644 JianshenkechengController.java diff --git a/JianshenkechengController.java b/JianshenkechengController.java new file mode 100644 index 0000000..b2663db --- /dev/null +++ b/JianshenkechengController.java @@ -0,0 +1,389 @@ +package com.controller; // 声明包路径,表示该文件位于com.controller包下 + +import java.io.File; // 导入文件操作类 +import java.math.BigDecimal; // 导入高精度数学计算类 +import java.net.URL; // 导入网络URL处理类 +import java.text.SimpleDateFormat; // 导入日期格式化类 +import com.alibaba.fastjson.JSONObject; // 导入FastJSON的JSON对象类 +import java.util.*; // 导入Java常用工具类(集合、日期等) +import org.springframework.beans.BeanUtils; // 导入Spring Bean属性复制工具类 +import javax.servlet.http.HttpServletRequest; // 导入HTTP请求处理类 +import org.springframework.web.context.ContextLoader; // 导入Spring上下文加载器 +import javax.servlet.ServletContext; // 导入Servlet上下文接口 +import com.service.TokenService; // 导入自定义的Token服务类 +import com.utils.*; // 导入自定义工具类 +import java.lang.reflect.InvocationTargetException; // 导入反射异常类 + +import com.service.DictionaryService; // 导入自定义的字典服务类 +import org.apache.commons.lang3.StringUtils; // 导入Apache字符串工具类 +import com.annotation.IgnoreAuth; // 导入自定义的忽略认证注解 +import org.slf4j.Logger; // 导入日志接口 +import org.slf4j.LoggerFactory; // 导入日志工厂类 +import org.springframework.beans.factory.annotation.Autowired; // 导入Spring自动注入注解 +import org.springframework.stereotype.Controller; // 导入Spring控制器注解 +import org.springframework.web.bind.annotation.*; // 导入Spring Web注解 +import com.baomidou.mybatisplus.mapper.EntityWrapper; // 导入MyBatis Plus查询条件构造器 +import com.baomidou.mybatisplus.mapper.Wrapper; // 导入MyBatis Plus包装器接口 +import com.entity.*; // 导入实体类 +import com.entity.view.*; // 导入视图实体类 +import com.service.*; // 导入服务类 +import com.utils.PageUtils; // 导入分页工具类 +import com.utils.R; // 导入统一返回结果类 +import com.alibaba.fastjson.*; // 导入FastJSON相关类 + + + // 健身课程 + //后端接口 + //@author + //@email + +@RestController // 标识为RESTful控制器 +@Controller // 标识为Spring控制器 +@RequestMapping("/jianshenkecheng") // 定义请求映射路径 +public class JianshenkechengController { + private static final Logger logger = LoggerFactory.getLogger(JianshenkechengController.class); // 日志记录器 + + private static final String TABLE_NAME = "jianshenkecheng"; // 数据库表名 + + @Autowired + private JianshenkechengService jianshenkechengService; // 健身课程服务 + + @Autowired + private TokenService tokenService; // Token服务 + + @Autowired + private DictionaryService dictionaryService; // 字典服务 + @Autowired + private ForumService forumService; // 健身论坛服务 + @Autowired + private JianshenkechengCollectionService jianshenkechengCollectionService; // 课程收藏服务 + @Autowired + private JianshenkechengLiuyanService jianshenkechengLiuyanService; // 课程留言服务 + @Autowired + private JiaolianService jiaolianService; // 教练服务 + @Autowired + private JiaolianYuyueService jiaolianYuyueService; // 教练预约申请服务 + @Autowired + private NewsService newsService; // 健身资讯服务 + @Autowired + private SingleSeachService singleSeachService; // 单页数据服务 + @Autowired + private YonghuService yonghuService; // 用户服务 + @Autowired + private UsersService usersService; // 管理员服务 + + + //后端列表 + @RequestMapping("/page") // 处理分页请求 + public R page(@RequestParam Map params, HttpServletRequest request){ + logger.debug("page方法:,,Controller:{},,params:{}",this.getClass().getName(),JSONObject.toJSONString(params)); // 记录日志 + String role = String.valueOf(request.getSession().getAttribute("role")); // 获取用户角色 + if(false) + return R.error(511,"永不会进入"); // 永远不会执行的代码 + else if("用户".equals(role)) + params.put("yonghuId",request.getSession().getAttribute("userId")); // 如果是用户角色,添加用户ID参数 + else if("教练".equals(role)) + params.put("jiaolianId",request.getSession().getAttribute("userId")); // 如果是教练角色,添加教练ID参数 + params.put("dataDeleteStart",1);params.put("dataDeleteEnd",1); // 设置逻辑删除参数 + CommonUtil.checkMap(params); // 检查参数 + PageUtils page = jianshenkechengService.queryPage(params); // 查询分页数据 + + // 字典表数据转换 + List list =(List)page.getList(); + for(JianshenkechengView c:list){ + // 修改对应字典表字段 + dictionaryService.dictionaryConvert(c, request); + } + return R.ok().put("data", page); // 返回分页数据 + } + + + //后端详情 + @RequestMapping("/info/{id}") // 处理详情请求 + public R info(@PathVariable("id") Long id, HttpServletRequest request){ + logger.debug("info方法:,,Controller:{},,id:{}",this.getClass().getName(),id); // 记录日志 + JianshenkechengEntity jianshenkecheng = jianshenkechengService.selectById(id); // 根据ID查询健身课程 + if(jianshenkecheng !=null){ + // entity转view + JianshenkechengView view = new JianshenkechengView(); + BeanUtils.copyProperties( jianshenkecheng , view ); // 把实体数据重构到view中 + // 级联表 教练 + JiaolianEntity jiaolian = jiaolianService.selectById(jianshenkecheng.getJiaolianId()); + if(jiaolian != null){ + BeanUtils.copyProperties( jiaolian , view ,new String[]{ "id", "createTime", "insertTime", "updateTime", "username", "password", "newMoney", "jiaolianId"}); // 把级联的数据添加到view中,并排除id和创建时间字段 + view.setJiaolianId(jiaolian.getId()); + } + // 修改对应字典表字段 + dictionaryService.dictionaryConvert(view, request); + return R.ok().put("data", view); // 返回详情数据 + }else { + return R.error(511,"查不到数据"); // 查询不到数据返回错误 + } + } + + + //后端保存 + @RequestMapping("/save") // 处理保存请求 + public R save(@RequestBody JianshenkechengEntity jianshenkecheng, HttpServletRequest request){ + logger.debug("save方法:,,Controller:{},,jianshenkecheng:{}",this.getClass().getName(),jianshenkecheng.toString()); // 记录日志 + + String role = String.valueOf(request.getSession().getAttribute("role")); // 获取用户角色 + if(false) + return R.error(511,"永远不会进入"); // 永远不会执行的代码 + else if("教练".equals(role)) + jianshenkecheng.setJiaolianId(Integer.valueOf(String.valueOf(request.getSession().getAttribute("userId")))); // 如果是教练角色,设置教练ID + + Wrapper queryWrapper = new EntityWrapper() + .eq("jiaolian_id", jianshenkecheng.getJiaolianId()) // 教练ID + .eq("jianshenkecheng_name", jianshenkecheng.getJianshenkechengName()) // 课程名称 + .eq("jianshenkecheng_video", jianshenkecheng.getJianshenkechengVideo()) // 课程视频 + .eq("zan_number", jianshenkecheng.getZanNumber()) // 赞数 + .eq("cai_number", jianshenkecheng.getCaiNumber()) // 踩数 + .eq("jianshenkecheng_types", jianshenkecheng.getJianshenkechengTypes()) // 课程类型 + .eq("data_delete", 1) // 逻辑删除字段 + ; + + logger.info("sql语句:"+queryWrapper.getSqlSegment()); // 记录SQL语句 + JianshenkechengEntity jianshenkechengEntity = jianshenkechengService.selectOne(queryWrapper); // 查询是否已存在相同数据 + if(jianshenkechengEntity==null){ + jianshenkecheng.setZanNumber(1); // 设置默认赞数 + jianshenkecheng.setCaiNumber(1); // 设置默认踩数 + jianshenkecheng.setJianshenkechengClicknum(1); // 设置默认点击量 + jianshenkecheng.setDataDelete(1); // 设置逻辑删除状态 + jianshenkecheng.setInsertTime(new Date()); // 设置插入时间 + jianshenkecheng.setCreateTime(new Date()); // 设置创建时间 + jianshenkechengService.insert(jianshenkecheng); // 插入新数据 + return R.ok(); // 返回成功 + }else { + return R.error(511,"表中有相同数据"); // 数据已存在返回错误 + } + } + + + //后端修改 + @RequestMapping("/update") // 处理更新请求 + public R update(@RequestBody JianshenkechengEntity jianshenkecheng, HttpServletRequest request) throws NoSuchFieldException, ClassNotFoundException, IllegalAccessException, InstantiationException { + logger.debug("update方法:,,Controller:{},,jianshenkecheng:{}",this.getClass().getName(),jianshenkecheng.toString()); // 记录日志 + JianshenkechengEntity oldJianshenkechengEntity = jianshenkechengService.selectById(jianshenkecheng.getId()); // 查询原先数据 + + if("".equals(jianshenkecheng.getJianshenkechengPhoto()) || "null".equals(jianshenkecheng.getJianshenkechengPhoto())){ + jianshenkecheng.setJianshenkechengPhoto(null); // 处理空图片路径 + } + if("".equals(jianshenkecheng.getJianshenkechengVideo()) || "null".equals(jianshenkecheng.getJianshenkechengVideo())){ + jianshenkecheng.setJianshenkechengVideo(null); // 处理空视频路径 + } + if("".equals(jianshenkecheng.getJianshenkechengContent()) || "null".equals(jianshenkecheng.getJianshenkechengContent())){ + jianshenkecheng.setJianshenkechengContent(null); // 处理空内容 + } + + jianshenkechengService.updateById(jianshenkecheng); // 根据ID更新数据 + return R.ok(); // 返回成功 + } + + + //删除 + @RequestMapping("/delete") // 处理删除请求 + public R delete(@RequestBody Integer[] ids, HttpServletRequest request){ + logger.debug("delete:,,Controller:{},,ids:{}",this.getClass().getName(),ids.toString()); // 记录日志 + List oldJianshenkechengList =jianshenkechengService.selectBatchIds(Arrays.asList(ids)); // 查询要删除的数据 + ArrayList list = new ArrayList<>(); // 创建更新列表 + for(Integer id:ids){ + JianshenkechengEntity jianshenkechengEntity = new JianshenkechengEntity(); + jianshenkechengEntity.setId(id); + jianshenkechengEntity.setDataDelete(2); // 设置逻辑删除状态为已删除 + list.add(jianshenkechengEntity); // 添加到更新列表 + } + if(list != null && list.size() >0){ + jianshenkechengService.updateBatchById(list); // 批量更新删除状态 + } + + return R.ok(); // 返回成功 + } + + + //批量上传 + @RequestMapping("/batchInsert") // 处理批量上传请求 + public R save( String fileName, HttpServletRequest request){ + logger.debug("batchInsert方法:,,Controller:{},,fileName:{}",this.getClass().getName(),fileName); // 记录日志 + Integer yonghuId = Integer.valueOf(String.valueOf(request.getSession().getAttribute("userId"))); // 获取用户ID + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); // 创建日期格式化对象 + try { + List jianshenkechengList = new ArrayList<>(); // 创建健身课程列表 + Map> seachFields= new HashMap<>(); // 创建查询字段映射 + Date date = new Date(); // 当前时间 + int lastIndexOf = fileName.lastIndexOf("."); // 获取文件后缀位置 + if(lastIndexOf == -1){ + return R.error(511,"该文件没有后缀"); // 文件无后缀返回错误 + }else{ + String suffix = fileName.substring(lastIndexOf); // 获取文件后缀 + if(!".xls".equals(suffix)){ + return R.error(511,"只支持后缀为xls的excel文件"); // 文件格式不正确返回错误 + }else{ + URL resource = this.getClass().getClassLoader().getResource("static/upload/" + fileName); // 获取文件路径 + File file = new File(resource.getFile()); // 创建文件对象 + if(!file.exists()){ + return R.error(511,"找不到上传文件,请联系管理员"); // 文件不存在返回错误 + }else{ + List> dataList = PoiUtil.poiImport(file.getPath()); // 读取xls文件 + dataList.remove(0); // 删除第一行提示信息 + for(List data:dataList){ + // 循环处理每行数据 + JianshenkechengEntity jianshenkechengEntity = new JianshenkechengEntity(); + jianshenkechengList.add(jianshenkechengEntity); // 添加到列表 + } + // 批量插入数据 + jianshenkechengService.insertBatch(jianshenkechengList); + return R.ok(); // 返回成功 + } + } + } + }catch (Exception e){ + e.printStackTrace(); // 打印异常堆栈 + return R.error(511,"批量插入数据异常,请联系管理员"); // 异常处理 + } + } + + + //个性推荐 + @IgnoreAuth // 忽略认证 + @RequestMapping("/gexingtuijian") // 处理个性推荐请求 + public R gexingtuijian(@RequestParam Map params, HttpServletRequest request){ + logger.debug("gexingtuijian方法:,,Controller:{},,params:{}",this.getClass().getName(),JSONObject.toJSONString(params)); // 记录日志 + CommonUtil.checkMap(params); // 检查参数 + List returnJianshenkechengViewList = new ArrayList<>(); // 创建返回课程列表 + + //查看收藏 + Map params1 = new HashMap<>(params);params1.put("sort","id");params1.put("yonghuId",request.getSession().getAttribute("userId")); // 设置收藏查询参数 + params1.put("shangxiaTypes",1); // 上架状态 + params1.put("jianshenkechengYesnoTypes",2); // 审核通过状态 + PageUtils pageUtils = jianshenkechengCollectionService.queryPage(params1); // 查询收藏分页数据 + List collectionViewsList =(List)pageUtils.getList(); // 获取收藏列表 + Map typeMap=new HashMap<>(); // 创建课程类型统计Map + for(JianshenkechengCollectionView collectionView:collectionViewsList){ + Integer jianshenkechengTypes = collectionView.getJianshenkechengTypes(); // 获取课程类型 + if(typeMap.containsKey(jianshenkechengTypes)){ + typeMap.put(jianshenkechengTypes,typeMap.get(jianshenkechengTypes)+1); // 统计类型数量 + }else{ + typeMap.put(jianshenkechengTypes,1); // 初始化类型数量 + } + } + List typeList = new ArrayList<>(); // 创建排序后的类型列表 + typeMap.entrySet().stream().sorted((o1, o2) -> o2.getValue() - o1.getValue()).forEach(e -> typeList.add(e.getKey())); // 按数量从多到少排序 + Integer limit = Integer.valueOf(String.valueOf(params.get("limit"))); // 获取推荐数量限制 + for(Integer type:typeList){ + Map params2 = new HashMap<>(params);params2.put("jianshenkechengTypes",type); // 设置类型查询参数 + params2.put("shangxiaTypes",1); // 上架状态 + params2.put("jianshenkechengYesnoTypes",2); // 审核通过状态 + PageUtils pageUtils1 = jianshenkechengService.queryPage(params2); // 查询课程分页数据 + List jianshenkechengViewList =(List)pageUtils1.getList(); // 获取课程列表 + returnJianshenkechengViewList.addAll(jianshenkechengViewList); // 添加到返回列表 + if(returnJianshenkechengViewList.size()>= limit) break; // 达到推荐数量限制时跳出循环 + } + params.put("shangxiaTypes",1); // 设置上架状态 + params.put("jianshenkechengYesnoTypes",2); // 设置审核通过状态 + //正常查询出来商品,用于补全推荐缺少的数据 + PageUtils page = jianshenkechengService.queryPage(params); // 查询课程分页数据 + if(returnJianshenkechengViewList.size() jianshenkechengViewList =(List)page.getList(); // 获取课程列表 + for(JianshenkechengView jianshenkechengView:jianshenkechengViewList){ + Boolean addFlag = true; + for(JianshenkechengView returnJianshenkechengView:returnJianshenkechengViewList){ + if(returnJianshenkechengView.getId().intValue() ==jianshenkechengView.getId().intValue()) addFlag=false; // 已存在的课程不再添加 + } + if(addFlag){ + toAddNum=toAddNum-1; + returnJianshenkechengViewList.add(jianshenkechengView); // 添加到返回列表 + if(toAddNum==0) break; // 补充足够数量后跳出循环 + } + } + }else { + returnJianshenkechengViewList = returnJianshenkechengViewList.subList(0, limit); // 截取指定数量的推荐课程 + } + + for(JianshenkechengView c:returnJianshenkechengViewList) + dictionaryService.dictionaryConvert(c, request); // 转换字典字段 + page.setList(returnJianshenkechengViewList); // 设置返回列表 + return R.ok().put("data", page); // 返回推荐数据 + } + + + //前端列表 + @IgnoreAuth // 忽略认证 + @RequestMapping("/list") // 处理列表请求 + public R list(@RequestParam Map params, HttpServletRequest request){ + logger.debug("list方法:,,Controller:{},,params:{}",this.getClass().getName(),JSONObject.toJSONString(params)); // 记录日志 + + CommonUtil.checkMap(params); // 检查参数 + PageUtils page = jianshenkechengService.queryPage(params); // 查询分页数据 + + // 字典表数据转换 + List list =(List)page.getList(); + for(JianshenkechengView c:list) + dictionaryService.dictionaryConvert(c, request); // 转换字典字段 + + return R.ok().put("data", page); // 返回分页数据 + } + + + //前端详情 + @RequestMapping("/detail/{id}") // 处理详情请求 + public R detail(@PathVariable("id") Integer id, HttpServletRequest request){ + logger.debug("detail方法:,,Controller:{},,id:{}",this.getClass().getName(),id); // 记录日志 + JianshenkechengEntity jianshenkecheng = jianshenkechengService.selectById(id); // 根据ID查询课程 + if(jianshenkecheng !=null){ + + //点击数量加1 + jianshenkecheng.setJianshenkechengClicknum(jianshenkecheng.getJianshenkechengClicknum()+1); // 增加点击量 + jianshenkechengService.updateById(jianshenkecheng); // 更新课程 + + //entity转view + JianshenkechengView view = new JianshenkechengView(); + BeanUtils.copyProperties( jianshenkecheng , view ); // 把实体数据重构到view中 + + //级联表 + JiaolianEntity jiaolian = jiaolianService.selectById(jianshenkecheng.getJiaolianId()); // 查询关联教练 + if(jiaolian != null){ + BeanUtils.copyProperties( jiaolian , view ,new String[]{ "id", "createTime", "insertTime", "updateTime", "username", "password", "newMoney", "jiaolianId"}); // 把级联的数据添加到view中,并排除id和创建时间字段 + view.setJiaolianId(jiaolian.getId()); + } + //修改对应字典表字段 + dictionaryService.dictionaryConvert(view, request); // 转换字典字段 + return R.ok().put("data", view); // 返回详情数据 + }else { + return R.error(511,"查不到数据"); // 查询不到数据返回错误 + } + } + + + //前端保存 + @RequestMapping("/add") // 处理添加请求 + public R add(@RequestBody JianshenkechengEntity jianshenkecheng, HttpServletRequest request){ + logger.debug("add方法:,,Controller:{},,jianshenkecheng:{}",this.getClass().getName(),jianshenkecheng.toString()); // 记录日志 + Wrapper queryWrapper = new EntityWrapper() + .eq("jiaolian_id", jianshenkecheng.getJiaolianId()) // 教练ID + .eq("jianshenkecheng_name", jianshenkecheng.getJianshenkechengName()) // 课程名称 + .eq("jianshenkecheng_video", jianshenkecheng.getJianshenkechengVideo()) // 课程视频 + .eq("zan_number", jianshenkecheng.getZanNumber()) // 赞数 + .eq("cai_number", jianshenkecheng.getCaiNumber()) // 踩数 + .eq("jianshenkecheng_types", jianshenkecheng.getJianshenkechengTypes()) // 课程类型 + .eq("jianshenkecheng_clicknum", jianshenkecheng.getJianshenkechengClicknum()) // 点击量 + .eq("data_delete", jianshenkecheng.getDataDelete()) // 逻辑删除状态 + ; + logger.info("sql语句:"+queryWrapper.getSqlSegment()); // 记录SQL语句 + JianshenkechengEntity jianshenkechengEntity = jianshenkechengService.selectOne(queryWrapper); // 查询是否已存在相同数据 + if(jianshenkechengEntity==null){ + jianshenkecheng.setZanNumber(1); // 设置默认赞数 + jianshenkecheng.setCaiNumber(1); // 设置默认踩数 + jianshenkecheng.setJianshenkechengClicknum(1); // 设置默认点击量 + jianshenkecheng.setDataDelete(1); // 设置逻辑删除状态 + jianshenkecheng.setInsertTime(new Date()); // 设置插入时间 + jianshenkecheng.setCreateTime(new Date()); // 设置创建时间 + jianshenkechengService.insert(jianshenkecheng); // 插入新数据 + + return R.ok(); // 返回成功 + }else { + return R.error(511,"表中有相同数据"); // 数据已存在返回错误 + } + } +} \ No newline at end of file From d34d0d2713af9576c11d9b4b1dc60ee643381672 Mon Sep 17 00:00:00 2001 From: phfsut2ie Date: Mon, 28 Apr 2025 18:37:17 +0800 Subject: [PATCH 16/22] ADD file via upload --- JiaolianController.java | 575 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 575 insertions(+) create mode 100644 JiaolianController.java diff --git a/JiaolianController.java b/JiaolianController.java new file mode 100644 index 0000000..46f41ca --- /dev/null +++ b/JiaolianController.java @@ -0,0 +1,575 @@ +// 声明当前 Java 文件所属的包,此包用于存放控制器类,处理与教练相关的HTTP请求 +package com.controller; + +// 导入Java IO包中的File类,用于文件操作 +import java.io.File; +// 导入Java数学包中的BigDecimal类,用于高精度计算 +import java.math.BigDecimal; +// 导入Java网络包中的URL类,用于处理统一资源定位符 +import java.net.URL; +// 导入Java文本包中的SimpleDateFormat类,用于日期格式化 +import java.text.SimpleDateFormat; +// 导入FastJSON库中的JSONObject类,用于JSON数据处理 +import com.alibaba.fastjson.JSONObject; +// 导入Java工具包中的所有类,包括集合、日期等工具 +import java.util.*; +// 导入Spring框架的BeanUtils类,用于对象属性复制 +import org.springframework.beans.BeanUtils; +// 导入Servlet的HttpServletRequest类,用于处理HTTP请求 +import javax.servlet.http.HttpServletRequest; +// 导入Spring框架的ContextLoader类,用于获取应用上下文 +import org.springframework.web.context.ContextLoader; +// 导入Servlet的ServletContext类,表示Servlet上下文 +import javax.servlet.ServletContext; +// 导入自定义的TokenService类,用于令牌管理 +import com.service.TokenService; +// 导入自定义工具类包中的所有工具类 +import com.utils.*; +// 导入反射相关的InvocationTargetException类 +import java.lang.reflect.InvocationTargetException; + +// 导入自定义的DictionaryService类,用于字典数据管理 +import com.service.DictionaryService; +// 导入Apache Commons Lang3库的StringUtils类,用于字符串处理 +import org.apache.commons.lang3.StringUtils; +// 导入自定义的IgnoreAuth注解,用于标记不需要认证的接口 +import com.annotation.IgnoreAuth; +// 导入SLF4J日志框架的Logger接口 +import org.slf4j.Logger; +// 导入SLF4J日志框架的LoggerFactory类 +import org.slf4j.LoggerFactory; +// 导入Spring框架的Autowired注解,用于依赖注入 +import org.springframework.beans.factory.annotation.Autowired; +// 导入Spring框架的Controller注解,标记该类为控制器 +import org.springframework.stereotype.Controller; +// 导入Spring框架的RestController和RequestMapping等注解 +import org.springframework.web.bind.annotation.*; +// 导入MyBatis Plus的EntityWrapper类,用于构建查询条件 +import com.baomidou.mybatisplus.mapper.EntityWrapper; +// 导入MyBatis Plus的Wrapper接口 +import com.baomidou.mybatisplus.mapper.Wrapper; +// 导入自定义实体类包中的所有类 +import com.entity.*; +// 导入自定义视图实体类包中的所有类 +import com.entity.view.*; +// 导入自定义服务类包中的所有类 +import com.service.*; +// 导入自定义分页工具类 +import com.utils.PageUtils; +// 导入自定义统一返回结果类 +import com.utils.R; +// 导入FastJSON库中的所有类 +import com.alibaba.fastjson.*; + +/** + * 教练控制器 + * 后端接口 + */ +@RestController // 标记为RESTful控制器 +@Controller // 标记为Spring控制器 +@RequestMapping("/jiaolian") // 定义基础请求路径 +public class JiaolianController { + // 创建日志记录器 + private static final Logger logger = LoggerFactory.getLogger(JiaolianController.class); + + // 定义常量表名 + private static final String TABLE_NAME = "jiaolian"; + + // 自动注入教练服务 + @Autowired + private JiaolianService jiaolianService; + + // 自动注入令牌服务 + @Autowired + private TokenService tokenService; + + // 自动注入字典服务 + @Autowired + private DictionaryService dictionaryService; + // 自动注入论坛服务 + @Autowired + private ForumService forumService; + // 自动注入健身课程服务 + @Autowired + private JianshenkechengService jianshenkechengService; + // 自动注入课程收藏服务 + @Autowired + private JianshenkechengCollectionService jianshenkechengCollectionService; + // 自动注入课程留言服务 + @Autowired + private JianshenkechengLiuyanService jianshenkechengLiuyanService; + // 自动注入教练预约服务 + @Autowired + private JiaolianYuyueService jiaolianYuyueService; + // 自动注入健身资讯服务 + @Autowired + private NewsService newsService; + // 自动注入单页数据服务 + @Autowired + private SingleSeachService singleSeachService; + // 自动注入用户服务 + @Autowired + private YonghuService yonghuService; + // 自动注入管理员服务 + @Autowired + private UsersService usersService; + + /** + * 后端列表查询 + */ + @RequestMapping("/page") + public R page(@RequestParam Map params, HttpServletRequest request){ + logger.debug("分页查询方法:,,控制器:{},,参数:{}",this.getClass().getName(),JSONObject.toJSONString(params)); + // 获取当前用户角色 + String role = String.valueOf(request.getSession().getAttribute("role")); + // 根据角色设置查询条件 + if(false) + return R.error(511,"永不会进入"); + else if("用户".equals(role)) + params.put("yonghuId",request.getSession().getAttribute("userId")); + else if("教练".equals(role)) + params.put("jiaolianId",request.getSession().getAttribute("userId")); + // 设置数据删除状态条件 + params.put("dataDeleteStart",1); + params.put("dataDeleteEnd",1); + // 检查参数有效性 + CommonUtil.checkMap(params); + // 调用服务层查询分页数据 + PageUtils page = jiaolianService.queryPage(params); + + // 字典数据转换 + List list =(List)page.getList(); + for(JiaolianView c:list){ + // 转换字典表字段 + dictionaryService.dictionaryConvert(c, request); + } + // 返回分页数据 + return R.ok().put("data", page); + } + + /** + * 后端详情查询 + */ + @RequestMapping("/info/{id}") + public R info(@PathVariable("id") Long id, HttpServletRequest request){ + logger.debug("详情查询方法:,,控制器:{},,ID:{}",this.getClass().getName(),id); + // 根据ID查询教练信息 + JiaolianEntity jiaolian = jiaolianService.selectById(id); + if(jiaolian !=null){ + // 实体转视图 + JiaolianView view = new JiaolianView(); + // 复制属性 + BeanUtils.copyProperties( jiaolian , view ); + // 转换字典表字段 + dictionaryService.dictionaryConvert(view, request); + return R.ok().put("data", view); + }else { + return R.error(511,"查不到数据"); + } + } + + /** + * 后端保存教练信息 + */ + @RequestMapping("/save") + public R save(@RequestBody JiaolianEntity jiaolian, HttpServletRequest request){ + logger.debug("保存方法:,,控制器:{},,教练信息:{}",this.getClass().getName(),jiaolian.toString()); + + // 获取当前用户角色 + String role = String.valueOf(request.getSession().getAttribute("role")); + if(false) + return R.error(511,"永远不会进入"); + + // 构建查询条件,检查用户名和手机号是否已存在 + Wrapper queryWrapper = new EntityWrapper() + .eq("username", jiaolian.getUsername()) + .or() + .eq("jiaolian_phone", jiaolian.getJiaolianPhone()) + .eq("data_delete", 1) + ; + + logger.info("SQL语句:"+queryWrapper.getSqlSegment()); + // 执行查询 + JiaolianEntity jiaolianEntity = jiaolianService.selectOne(queryWrapper); + if(jiaolianEntity==null){ + // 设置默认值 + jiaolian.setDataDelete(1); // 数据删除状态 + jiaolian.setInsertTime(new Date()); // 插入时间 + jiaolian.setCreateTime(new Date()); // 创建时间 + jiaolian.setPassword("123456"); // 默认密码 + // 保存教练信息 + jiaolianService.insert(jiaolian); + return R.ok(); + }else { + return R.error(511,"账户或者教练手机号已经被使用"); + } + } + + /** + * 后端修改教练信息 + */ + @RequestMapping("/update") + public R update(@RequestBody JiaolianEntity jiaolian, HttpServletRequest request) throws NoSuchFieldException, ClassNotFoundException, IllegalAccessException, InstantiationException { + logger.debug("修改方法:,,控制器:{},,教练信息:{}",this.getClass().getName(),jiaolian.toString()); + // 查询原有数据 + JiaolianEntity oldJiaolianEntity = jiaolianService.selectById(jiaolian.getId()); + + // 获取当前用户角色 + String role = String.valueOf(request.getSession().getAttribute("role")); + // 处理空字段 + if("".equals(jiaolian.getJiaolianPhoto()) || "null".equals(jiaolian.getJiaolianPhoto())){ + jiaolian.setJiaolianPhoto(null); + } + if("".equals(jiaolian.getJiaolianContent()) || "null".equals(jiaolian.getJiaolianContent())){ + jiaolian.setJiaolianContent(null); + } + + // 更新数据 + jiaolianService.updateById(jiaolian); + return R.ok(); + } + + /** + * 删除教练信息(逻辑删除) + */ + @RequestMapping("/delete") + public R delete(@RequestBody Integer[] ids, HttpServletRequest request){ + logger.debug("删除方法:,,控制器:{},,IDs:{}",this.getClass().getName(),ids.toString()); + // 查询要删除的数据 + List oldJiaolianList =jiaolianService.selectBatchIds(Arrays.asList(ids)); + // 准备更新列表 + ArrayList list = new ArrayList<>(); + for(Integer id:ids){ + JiaolianEntity jiaolianEntity = new JiaolianEntity(); + jiaolianEntity.setId(id); + jiaolianEntity.setDataDelete(2); // 标记为删除 + list.add(jiaolianEntity); + } + // 批量更新删除状态 + if(list != null && list.size() >0){ + jiaolianService.updateBatchById(list); + } + + return R.ok(); + } + + /** + * 批量导入教练数据 + */ + @RequestMapping("/batchInsert") + public R save( String fileName, HttpServletRequest request){ + logger.debug("批量导入方法:,,控制器:{},,文件名:{}",this.getClass().getName(),fileName); + // 获取当前用户ID + Integer yonghuId = Integer.valueOf(String.valueOf(request.getSession().getAttribute("userId"))); + // 创建日期格式化对象 + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + try { + // 准备导入的数据列表 + List jiaolianList = new ArrayList<>(); + // 需要检查重复的字段映射 + Map> seachFields= new HashMap<>(); + // 当前日期 + Date date = new Date(); + // 检查文件后缀 + int lastIndexOf = fileName.lastIndexOf("."); + if(lastIndexOf == -1){ + return R.error(511,"该文件没有后缀"); + }else{ + String suffix = fileName.substring(lastIndexOf); + if(!".xls".equals(suffix)){ + return R.error(511,"只支持后缀为xls的excel文件"); + }else{ + // 获取文件路径 + URL resource = this.getClass().getClassLoader().getResource("static/upload/" + fileName); + File file = new File(resource.getFile()); + if(!file.exists()){ + return R.error(511,"找不到上传文件,请联系管理员"); + }else{ + // 读取Excel文件 + List> dataList = PoiUtil.poiImport(file.getPath()); + dataList.remove(0);// 删除标题行 + for(List data:dataList){ + // 创建教练实体 + JiaolianEntity jiaolianEntity = new JiaolianEntity(); + // 设置各字段值(示例代码,实际应根据Excel列设置) + // jiaolianEntity.setUsername(data.get(0)); // 账户 + // jiaolianEntity.setPassword("123456"); // 密码 + // jiaolianEntity.setJiaolianName(data.get(0)); // 教练名称 + // jiaolianEntity.setJiaolianPhone(data.get(0)); // 教练手机号 + // jiaolianEntity.setJiaolianPhoto(""); // 照片 + // jiaolianEntity.setSexTypes(Integer.valueOf(data.get(0))); // 性别 + // jiaolianEntity.setJiaolianEmail(data.get(0)); // 邮箱 + // jiaolianEntity.setJiaolianContent(""); // 内容 + // jiaolianEntity.setDataDelete(1); // 删除状态 + // jiaolianEntity.setInsertTime(date); // 插入时间 + // jiaolianEntity.setCreateTime(date); // 创建时间 + jiaolianList.add(jiaolianEntity); + + // 收集需要检查重复的字段 + // 账户字段检查 + if(seachFields.containsKey("username")){ + List username = seachFields.get("username"); + username.add(data.get(0)); + }else{ + List username = new ArrayList<>(); + username.add(data.get(0)); + seachFields.put("username",username); + } + // 手机号字段检查 + if(seachFields.containsKey("jiaolianPhone")){ + List jiaolianPhone = seachFields.get("jiaolianPhone"); + jiaolianPhone.add(data.get(0)); + }else{ + List jiaolianPhone = new ArrayList<>(); + jiaolianPhone.add(data.get(0)); + seachFields.put("jiaolianPhone",jiaolianPhone); + } + } + + // 检查账户是否重复 + List jiaolianEntities_username = jiaolianService.selectList(new EntityWrapper().in("username", seachFields.get("username")).eq("data_delete", 1)); + if(jiaolianEntities_username.size() >0 ){ + ArrayList repeatFields = new ArrayList<>(); + for(JiaolianEntity s:jiaolianEntities_username){ + repeatFields.add(s.getUsername()); + } + return R.error(511,"数据库的该表中的 [账户] 字段已经存在 存在数据为:"+repeatFields.toString()); + } + // 检查手机号是否重复 + List jiaolianEntities_jiaolianPhone = jiaolianService.selectList(new EntityWrapper().in("jiaolian_phone", seachFields.get("jiaolianPhone")).eq("data_delete", 1)); + if(jiaolianEntities_jiaolianPhone.size() >0 ){ + ArrayList repeatFields = new ArrayList<>(); + for(JiaolianEntity s:jiaolianEntities_jiaolianPhone){ + repeatFields.add(s.getJiaolianPhone()); + } + return R.error(511,"数据库的该表中的 [教练手机号] 字段已经存在 存在数据为:"+repeatFields.toString()); + } + // 批量插入数据 + jiaolianService.insertBatch(jiaolianList); + return R.ok(); + } + } + } + }catch (Exception e){ + e.printStackTrace(); + return R.error(511,"批量插入数据异常,请联系管理员"); + } + } + + /** + * 教练登录 + */ + @IgnoreAuth // 忽略认证 + @RequestMapping(value = "/login") + public R login(String username, String password, String captcha, HttpServletRequest request) { + // 根据用户名查询教练 + JiaolianEntity jiaolian = jiaolianService.selectOne(new EntityWrapper().eq("username", username)); + // 验证账号密码 + if(jiaolian==null || !jiaolian.getPassword().equals(password)) + return R.error("账号或密码不正确"); + // 检查账号状态 + else if(jiaolian.getDataDelete() != 1) + return R.error("账户已被删除"); + // 生成令牌 + String token = tokenService.generateToken(jiaolian.getId(),username, "jiaolian", "教练"); + // 构建返回结果 + R r = R.ok(); + r.put("token", token); + r.put("role","教练"); + r.put("username",jiaolian.getJiaolianName()); + r.put("tableName","jiaolian"); + r.put("userId",jiaolian.getId()); + return r; + } + + /** + * 教练注册 + */ + @IgnoreAuth // 忽略认证 + @PostMapping(value = "/register") + public R register(@RequestBody JiaolianEntity jiaolian, HttpServletRequest request) { + // 构建查询条件,检查用户名和手机号是否已存在 + Wrapper queryWrapper = new EntityWrapper() + .eq("username", jiaolian.getUsername()) + .or() + .eq("jiaolian_phone", jiaolian.getJiaolianPhone()) + .andNew() + .eq("data_delete", 1) + ; + // 执行查询 + JiaolianEntity jiaolianEntity = jiaolianService.selectOne(queryWrapper); + if(jiaolianEntity != null) + return R.error("账户或者教练手机号已经被使用"); + // 设置默认值 + jiaolian.setDataDelete(1); // 数据状态 + jiaolian.setInsertTime(new Date()); // 插入时间 + jiaolian.setCreateTime(new Date()); // 创建时间 + // 保存教练信息 + jiaolianService.insert(jiaolian); + + return R.ok(); + } + + /** + * 重置密码 + */ + @GetMapping(value = "/resetPassword") + public R resetPassword(Integer id, HttpServletRequest request) { + // 根据ID查询教练 + JiaolianEntity jiaolian = jiaolianService.selectById(id); + // 重置密码为默认值 + jiaolian.setPassword("123456"); + // 更新密码 + jiaolianService.updateById(jiaolian); + return R.ok(); + } + + /** + * 修改密码 + */ + @GetMapping(value = "/updatePassword") + public R updatePassword(String oldPassword, String newPassword, HttpServletRequest request) { + // 获取当前登录教练 + JiaolianEntity jiaolian = jiaolianService.selectById((Integer)request.getSession().getAttribute("userId")); + // 验证新密码 + if(newPassword == null){ + return R.error("新密码不能为空") ; + } + // 验证旧密码 + if(!oldPassword.equals(jiaolian.getPassword())){ + return R.error("原密码输入错误"); + } + // 检查新旧密码是否相同 + if(newPassword.equals(jiaolian.getPassword())){ + return R.error("新密码不能和原密码一致") ; + } + // 更新密码 + jiaolian.setPassword(newPassword); + jiaolianService.updateById(jiaolian); + return R.ok(); + } + + /** + * 忘记密码(重置密码) + */ + @IgnoreAuth // 忽略认证 + @RequestMapping(value = "/resetPass") + public R resetPass(String username, HttpServletRequest request) { + // 根据用户名查询教练 + JiaolianEntity jiaolian = jiaolianService.selectOne(new EntityWrapper().eq("username", username)); + if(jiaolian!=null){ + // 重置密码为默认值 + jiaolian.setPassword("123456"); + // 更新密码 + jiaolianService.updateById(jiaolian); + return R.ok(); + }else { + return R.error("账号不存在"); + } + } + + /** + * 获取当前登录教练的会话信息 + */ + @RequestMapping("/session") + public R getCurrJiaolian(HttpServletRequest request){ + // 从会话中获取用户ID + Integer id = (Integer)request.getSession().getAttribute("userId"); + // 查询教练信息 + JiaolianEntity jiaolian = jiaolianService.selectById(id); + if(jiaolian !=null){ + // 实体转视图 + JiaolianView view = new JiaolianView(); + // 复制属性 + BeanUtils.copyProperties( jiaolian , view ); + // 转换字典字段 + dictionaryService.dictionaryConvert(view, request); + return R.ok().put("data", view); + }else { + return R.error(511,"查不到数据"); + } + } + + /** + * 退出登录 + */ + @GetMapping(value = "logout") + public R logout(HttpServletRequest request) { + // 使会话失效 + request.getSession().invalidate(); + return R.ok("退出成功"); + } + + /** + * 前端列表查询 + */ + @IgnoreAuth // 忽略认证 + @RequestMapping("/list") + public R list(@RequestParam Map params, HttpServletRequest request){ + logger.debug("前端列表查询方法:,,控制器:{},,参数:{}",this.getClass().getName(),JSONObject.toJSONString(params)); + + // 检查参数有效性 + CommonUtil.checkMap(params); + // 查询分页数据 + PageUtils page = jiaolianService.queryPage(params); + + // 字典数据转换 + List list =(List)page.getList(); + for(JiaolianView c:list) + dictionaryService.dictionaryConvert(c, request); // 转换字典字段 + + return R.ok().put("data", page); + } + + /** + * 前端详情查询 + */ + @RequestMapping("/detail/{id}") + public R detail(@PathVariable("id") Integer id, HttpServletRequest request){ + logger.debug("前端详情查询方法:,,控制器:{},,ID:{}",this.getClass().getName(),id); + // 根据ID查询教练 + JiaolianEntity jiaolian = jiaolianService.selectById(id); + if(jiaolian !=null){ + // 实体转视图 + JiaolianView view = new JiaolianView(); + // 复制属性 + BeanUtils.copyProperties( jiaolian , view ); + // 转换字典字段 + dictionaryService.dictionaryConvert(view, request); + return R.ok().put("data", view); + }else { + return R.error(511,"查不到数据"); + } + } + + /** + * 前端保存教练信息 + */ + @RequestMapping("/add") + public R add(@RequestBody JiaolianEntity jiaolian, HttpServletRequest request){ + logger.debug("前端保存方法:,,控制器:{},,教练信息:{}",this.getClass().getName(),jiaolian.toString()); + // 构建查询条件,检查用户名和手机号是否已存在 + Wrapper queryWrapper = new EntityWrapper() + .eq("username", jiaolian.getUsername()) + .or() + .eq("jiaolian_phone", jiaolian.getJiaolianPhone()) + .andNew() + .eq("data_delete", 1) + ; + logger.info("SQL语句:"+queryWrapper.getSqlSegment()); + // 执行查询 + JiaolianEntity jiaolianEntity = jiaolianService.selectOne(queryWrapper); + if(jiaolianEntity==null){ + // 设置默认值 + jiaolian.setDataDelete(1); // 数据状态 + jiaolian.setInsertTime(new Date()); // 插入时间 + jiaolian.setCreateTime(new Date()); // 创建时间 + jiaolian.setPassword("123456"); // 默认密码 + // 保存教练信息 + jiaolianService.insert(jiaolian); + return R.ok(); + }else { + return R.error(511,"账户或者教练手机号已经被使用"); + } + } +} \ No newline at end of file From cff43b9bcc35875541a7953325b762f30af5c749 Mon Sep 17 00:00:00 2001 From: phfsut2ie Date: Mon, 28 Apr 2025 18:37:29 +0800 Subject: [PATCH 17/22] ADD file via upload --- JiaolianYuyueController.java | 340 +++++++++++++++++++++++++++++++++++ 1 file changed, 340 insertions(+) create mode 100644 JiaolianYuyueController.java diff --git a/JiaolianYuyueController.java b/JiaolianYuyueController.java new file mode 100644 index 0000000..cd37173 --- /dev/null +++ b/JiaolianYuyueController.java @@ -0,0 +1,340 @@ +package com.controller; // 声明包路径,表示该文件位于com.controller包下 + +import java.io.File; // 导入文件操作类,用于处理文件上传 +import java.math.BigDecimal; // 导入高精度数学计算类,用于处理金额计算 +import java.net.URL; // 导入网络URL处理类,用于获取资源路径 +import java.text.SimpleDateFormat; // 导入日期格式化类,用于日期格式转换 +import com.alibaba.fastjson.JSONObject; // 导入FastJSON的JSON对象类,用于JSON处理 +import java.util.*; // 导入Java常用工具类(集合、日期等) +import org.springframework.beans.BeanUtils; // 导入Spring Bean属性复制工具类 +import javax.servlet.http.HttpServletRequest; // 导入HTTP请求处理类 +import org.springframework.web.context.ContextLoader; // 导入Spring上下文加载器 +import javax.servlet.ServletContext; // 导入Servlet上下文接口 +import com.service.TokenService; // 导入自定义的Token服务类 +import com.utils.*; // 导入自定义工具类 +import java.lang.reflect.InvocationTargetException; // 导入反射异常类 + +import com.service.DictionaryService; // 导入自定义的字典服务类 +import org.apache.commons.lang3.StringUtils; // 导入Apache字符串工具类 +import com.annotation.IgnoreAuth; // 导入自定义的忽略认证注解 +import org.slf4j.Logger; // 导入日志接口 +import org.slf4j.LoggerFactory; // 导入日志工厂类 +import org.springframework.beans.factory.annotation.Autowired; // 导入Spring自动注入注解 +import org.springframework.stereotype.Controller; // 导入Spring控制器注解 +import org.springframework.web.bind.annotation.*; // 导入Spring Web注解 +import com.baomidou.mybatisplus.mapper.EntityWrapper; // 导入MyBatis Plus查询条件构造器 +import com.baomidou.mybatisplus.mapper.Wrapper; // 导入MyBatis Plus包装器接口 +import com.entity.*; // 导入实体类 +import com.entity.view.*; // 导入视图实体类 +import com.service.*; // 导入服务类 +import com.utils.PageUtils; // 导入分页工具类 +import com.utils.R; // 导入统一返回结果类 +import com.alibaba.fastjson.*; // 导入FastJSON相关类 + + + //教练预约申请 + //后端接口 + //@author + // @email + +@RestController // 标识为RESTful控制器,用于处理HTTP请求 +@Controller // 标识为Spring控制器 +@RequestMapping("/jiaolianYuyue") // 定义请求映射路径 +public class JiaolianYuyueController { + private static final Logger logger = LoggerFactory.getLogger(JiaolianYuyueController.class); // 日志记录器,用于记录日志信息 + + private static final String TABLE_NAME = "jiaolianYuyue"; // 数据库表名常量 + + @Autowired + private JiaolianYuyueService jiaolianYuyueService; // 自动注入教练预约服务 + + @Autowired + private TokenService tokenService; // 自动注入Token服务 + + @Autowired + private DictionaryService dictionaryService; // 自动注入字典服务 + @Autowired + private ForumService forumService; // 自动注入健身论坛服务 + @Autowired + private JianshenkechengService jianshenkechengService; // 自动注入健身课程服务 + @Autowired + private JianshenkechengCollectionService jianshenkechengCollectionService; // 自动注入课程收藏服务 + @Autowired + private JianshenkechengLiuyanService jianshenkechengLiuyanService; // 自动注入课程留言服务 + @Autowired + private JiaolianService jiaolianService; // 自动注入教练服务 + @Autowired + private NewsService newsService; // 自动注入健身资讯服务 + @Autowired + private SingleSeachService singleSeachService; // 自动注入单页数据服务 + @Autowired + private YonghuService yonghuService; // 自动注入用户服务 + @Autowired + private UsersService usersService; // 自动注入管理员服务 + + + //后端列表 + //处理分页查询请求 + @RequestMapping("/page") // 映射分页查询请求 + public R page(@RequestParam Map params, HttpServletRequest request){ + logger.debug("page方法:,,Controller:{},,params:{}",this.getClass().getName(),JSONObject.toJSONString(params)); // 记录请求日志 + String role = String.valueOf(request.getSession().getAttribute("role")); // 从session中获取用户角色 + if(false) // 永远不会执行的代码块 + return R.error(511,"永不会进入"); // 返回错误信息 + else if("用户".equals(role)) // 如果是用户角色 + params.put("yonghuId",request.getSession().getAttribute("userId")); // 将用户ID添加到查询参数 + else if("教练".equals(role)) // 如果是教练角色 + params.put("jiaolianId",request.getSession().getAttribute("userId")); // 将教练ID添加到查询参数 + CommonUtil.checkMap(params); // 检查参数有效性 + PageUtils page = jiaolianYuyueService.queryPage(params); // 调用服务层查询分页数据 + + // 字典表数据转换 + List list =(List)page.getList(); // 获取分页数据列表 + for(JiaolianYuyueView c:list){ // 遍历列表 + dictionaryService.dictionaryConvert(c, request); // 转换字典字段 + } + return R.ok().put("data", page); // 返回分页数据 + } + + + //后端详情 + //根据ID查询单条预约详情 + @RequestMapping("/info/{id}") // 映射详情查询请求 + public R info(@PathVariable("id") Long id, HttpServletRequest request){ + logger.debug("info方法:,,Controller:{},,id:{}",this.getClass().getName(),id); // 记录请求日志 + JiaolianYuyueEntity jiaolianYuyue = jiaolianYuyueService.selectById(id); // 根据ID查询预约 + if(jiaolianYuyue !=null){ // 如果查询到数据 + JiaolianYuyueView view = new JiaolianYuyueView(); // 创建视图对象 + BeanUtils.copyProperties(jiaolianYuyue, view); // 复制属性到视图对象 + + // 查询关联的用户信息 + YonghuEntity yonghu = yonghuService.selectById(jiaolianYuyue.getYonghuId()); + if(yonghu != null){ + BeanUtils.copyProperties(yonghu, view, new String[]{ "id", "createTime", "insertTime", "updateTime", "username", "password", "newMoney", "yonghuId"}); + view.setYonghuId(yonghu.getId()); + } + + dictionaryService.dictionaryConvert(view, request); // 转换字典字段 + return R.ok().put("data", view); // 返回详情数据 + }else { + return R.error(511,"查不到数据"); // 返回错误信息 + } + } + + + //后端保存 + //处理新增预约请求 + @RequestMapping("/save") // 映射保存请求 + public R save(@RequestBody JiaolianYuyueEntity jiaolianYuyue, HttpServletRequest request){ + logger.debug("save方法:,,Controller:{},,jiaolianYuyue:{}",this.getClass().getName(),jiaolianYuyue.toString()); // 记录请求日志 + + String role = String.valueOf(request.getSession().getAttribute("role")); // 获取用户角色 + if(false) // 永远不会执行的代码块 + return R.error(511,"永远不会进入"); // 返回错误信息 + else if("用户".equals(role)) // 如果是用户角色 + jiaolianYuyue.setYonghuId(Integer.valueOf(String.valueOf(request.getSession().getAttribute("userId")))); // 设置用户ID + + // 构建查询条件,检查是否存在相同用户的待审核或已通过的预约 + Wrapper queryWrapper = new EntityWrapper() + .eq("yonghu_id", jiaolianYuyue.getYonghuId()) + .in("jiaolian_yuyue_yesno_types", new Integer[]{1,2}); + + logger.info("sql语句:"+queryWrapper.getSqlSegment()); // 记录SQL语句 + JiaolianYuyueEntity jiaolianYuyueEntity = jiaolianYuyueService.selectOne(queryWrapper); // 查询是否存在重复数据 + if(jiaolianYuyueEntity==null){ // 如果没有重复数据 + jiaolianYuyue.setJiaolianYuyueYesnoTypes(1); // 设置状态为待审核 + jiaolianYuyue.setInsertTime(new Date()); // 设置插入时间 + jiaolianYuyue.setCreateTime(new Date()); // 设置创建时间 + jiaolianYuyueService.insert(jiaolianYuyue); // 调用服务层保存数据 + return R.ok(); // 返回成功信息 + }else { // 如果存在重复数据 + if(jiaolianYuyueEntity.getJiaolianYuyueYesnoTypes()==1) // 如果是待审核状态 + return R.error(511,"有相同的待审核的数据"); // 返回错误信息 + else if(jiaolianYuyueEntity.getJiaolianYuyueYesnoTypes()==2) // 如果是已通过状态 + return R.error(511,"有相同的审核通过的数据"); // 返回错误信息 + else + return R.error(511,"表中有相同数据"); // 返回错误信息 + } + } + + + //后端修改 + //处理更新预约请求 + @RequestMapping("/update") // 映射更新请求 + public R update(@RequestBody JiaolianYuyueEntity jiaolianYuyue, HttpServletRequest request) throws NoSuchFieldException, ClassNotFoundException, IllegalAccessException, InstantiationException { + logger.debug("update方法:,,Controller:{},,jiaolianYuyue:{}",this.getClass().getName(),jiaolianYuyue.toString()); // 记录请求日志 + JiaolianYuyueEntity oldJiaolianYuyueEntity = jiaolianYuyueService.selectById(jiaolianYuyue.getId()); // 查询原有数据 + + // 处理空值情况 + if("".equals(jiaolianYuyue.getJiaolianYuyueText()) || "null".equals(jiaolianYuyue.getJiaolianYuyueText())){ + jiaolianYuyue.setJiaolianYuyueText(null); + } + if("".equals(jiaolianYuyue.getJiaolianYuyueYesnoText()) || "null".equals(jiaolianYuyue.getJiaolianYuyueYesnoText())){ + jiaolianYuyue.setJiaolianYuyueYesnoText(null); + } + + jiaolianYuyueService.updateById(jiaolianYuyue); // 调用服务层更新数据 + return R.ok(); // 返回成功信息 + } + + + //审核 + //处理预约审核请求 + @RequestMapping("/shenhe") // 映射审核请求 + public R shenhe(@RequestBody JiaolianYuyueEntity jiaolianYuyueEntity, HttpServletRequest request){ + logger.debug("shenhe方法:,,Controller:{},,jiaolianYuyueEntity:{}",this.getClass().getName(),jiaolianYuyueEntity.toString()); // 记录请求日志 + + JiaolianYuyueEntity oldJiaolianYuyue = jiaolianYuyueService.selectById(jiaolianYuyueEntity.getId()); // 查询原有数据 + + jiaolianYuyueService.updateById(jiaolianYuyueEntity); // 调用服务层更新审核状态 + return R.ok(); // 返回成功信息 + } + + + //删除 + //处理删除预约请求 + @RequestMapping("/delete") // 映射删除请求 + public R delete(@RequestBody Integer[] ids, HttpServletRequest request){ + logger.debug("delete:,,Controller:{},,ids:{}",this.getClass().getName(),ids.toString()); // 记录请求日志 + List oldJiaolianYuyueList = jiaolianYuyueService.selectBatchIds(Arrays.asList(ids)); // 查询要删除的数据 + jiaolianYuyueService.deleteBatchIds(Arrays.asList(ids)); // 调用服务层批量删除 + return R.ok(); // 返回成功信息 + } + + + //批量上传 + //处理批量导入数据请求 + @RequestMapping("/batchInsert") // 映射批量导入请求 + public R save(String fileName, HttpServletRequest request){ + logger.debug("batchInsert方法:,,Controller:{},,fileName:{}",this.getClass().getName(),fileName); // 记录请求日志 + Integer yonghuId = Integer.valueOf(String.valueOf(request.getSession().getAttribute("userId"))); // 获取用户ID + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); // 创建日期格式化对象 + try { + List jiaolianYuyueList = new ArrayList<>(); // 创建数据列表 + Map> seachFields = new HashMap<>(); // 创建查询字段映射 + Date date = new Date(); // 当前时间 + int lastIndexOf = fileName.lastIndexOf("."); // 获取文件后缀位置 + if(lastIndexOf == -1){ // 如果没有后缀 + return R.error(511,"该文件没有后缀"); // 返回错误信息 + }else{ + String suffix = fileName.substring(lastIndexOf); // 获取文件后缀 + if(!".xls".equals(suffix)){ // 如果不是xls文件 + return R.error(511,"只支持后缀为xls的excel文件"); // 返回错误信息 + }else{ + URL resource = this.getClass().getClassLoader().getResource("static/upload/" + fileName); // 获取文件路径 + File file = new File(resource.getFile()); // 创建文件对象 + if(!file.exists()){ // 如果文件不存在 + return R.error(511,"找不到上传文件,请联系管理员"); // 返回错误信息 + }else{ + List> dataList = PoiUtil.poiImport(file.getPath()); // 读取Excel文件 + dataList.remove(0); // 删除标题行 + for(List data:dataList){ // 遍历数据行 + JiaolianYuyueEntity jiaolianYuyueEntity = new JiaolianYuyueEntity(); // 创建实体对象 + jiaolianYuyueList.add(jiaolianYuyueEntity); // 添加到列表 + + // 检查预约编号是否重复 + if(seachFields.containsKey("jiaolianYuyueUuidNumber")){ + List jiaolianYuyueUuidNumber = seachFields.get("jiaolianYuyueUuidNumber"); + jiaolianYuyueUuidNumber.add(data.get(0)); + }else{ + List jiaolianYuyueUuidNumber = new ArrayList<>(); + jiaolianYuyueUuidNumber.add(data.get(0)); + seachFields.put("jiaolianYuyueUuidNumber",jiaolianYuyueUuidNumber); + } + } + + // 检查预约编号是否已存在 + List jiaolianYuyueEntities_jiaolianYuyueUuidNumber = jiaolianYuyueService.selectList(new EntityWrapper().in("jiaolian_yuyue_uuid_number", seachFields.get("jiaolianYuyueUuidNumber"))); + if(jiaolianYuyueEntities_jiaolianYuyueUuidNumber.size() >0 ){ + ArrayList repeatFields = new ArrayList<>(); + for(JiaolianYuyueEntity s:jiaolianYuyueEntities_jiaolianYuyueUuidNumber){ + repeatFields.add(s.getJiaolianYuyueUuidNumber()); + } + return R.error(511,"数据库的该表中的 [预约编号] 字段已经存在 存在数据为:"+repeatFields.toString()); + } + jiaolianYuyueService.insertBatch(jiaolianYuyueList); // 批量插入数据 + return R.ok(); // 返回成功信息 + } + } + } + }catch (Exception e){ + e.printStackTrace(); // 打印异常堆栈 + return R.error(511,"批量插入数据异常,请联系管理员"); // 返回错误信息 + } + } + + + //前端列表 + //处理前端分页查询请求 + @IgnoreAuth // 忽略认证 + @RequestMapping("/list") // 映射列表查询请求 + public R list(@RequestParam Map params, HttpServletRequest request){ + logger.debug("list方法:,,Controller:{},,params:{}",this.getClass().getName(),JSONObject.toJSONString(params)); // 记录请求日志 + CommonUtil.checkMap(params); // 检查参数有效性 + PageUtils page = jiaolianYuyueService.queryPage(params); // 调用服务层查询分页数据 + + // 字典表数据转换 + List list =(List)page.getList(); // 获取分页数据列表 + for(JiaolianYuyueView c:list){ // 遍历列表 + dictionaryService.dictionaryConvert(c, request); // 转换字典字段 + } + return R.ok().put("data", page); // 返回分页数据 + } + + + //前端详情 + //处理前端详情查询请求 + @RequestMapping("/detail/{id}") // 映射详情查询请求 + public R detail(@PathVariable("id") Integer id, HttpServletRequest request){ + logger.debug("detail方法:,,Controller:{},,id:{}",this.getClass().getName(),id); // 记录请求日志 + JiaolianYuyueEntity jiaolianYuyue = jiaolianYuyueService.selectById(id); // 根据ID查询预约 + if(jiaolianYuyue !=null){ // 如果查询到数据 + JiaolianYuyueView view = new JiaolianYuyueView(); // 创建视图对象 + BeanUtils.copyProperties(jiaolianYuyue, view); // 复制属性到视图对象 + + // 查询关联的用户信息 + YonghuEntity yonghu = yonghuService.selectById(jiaolianYuyue.getYonghuId()); + if(yonghu != null){ + BeanUtils.copyProperties(yonghu, view, new String[]{ "id", "createTime", "insertTime", "updateTime", "username", "password", "newMoney", "yonghuId"}); + view.setYonghuId(yonghu.getId()); + } + + dictionaryService.dictionaryConvert(view, request); // 转换字典字段 + return R.ok().put("data", view); // 返回详情数据 + }else { + return R.error(511,"查不到数据"); // 返回错误信息 + } + } + + + //前端保存 + //处理前端新增预约请求 + @RequestMapping("/add") // 映射新增请求 + public R add(@RequestBody JiaolianYuyueEntity jiaolianYuyue, HttpServletRequest request){ + logger.debug("add方法:,,Controller:{},,jiaolianYuyue:{}",this.getClass().getName(),jiaolianYuyue.toString()); // 记录请求日志 + // 构建查询条件,检查是否存在相同用户的待审核或已通过的预约 + Wrapper queryWrapper = new EntityWrapper() + .eq("jiaolian_yuyue_uuid_number", jiaolianYuyue.getJiaolianYuyueUuidNumber()) + .eq("yonghu_id", jiaolianYuyue.getYonghuId()) + .eq("jiaolian_yuyue_text", jiaolianYuyue.getJiaolianYuyueText()) + .in("jiaolian_yuyue_yesno_types", new Integer[]{1,2}) + .eq("jiaolian_yuyue_yesno_text", jiaolianYuyue.getJiaolianYuyueYesnoText()); + + logger.info("sql语句:"+queryWrapper.getSqlSegment()); // 记录SQL语句 + JiaolianYuyueEntity jiaolianYuyueEntity = jiaolianYuyueService.selectOne(queryWrapper); // 查询是否存在重复数据 + if(jiaolianYuyueEntity==null){ // 如果没有重复数据 + jiaolianYuyue.setJiaolianYuyueYesnoTypes(1); // 设置状态为待审核 + jiaolianYuyue.setInsertTime(new Date()); // 设置插入时间 + jiaolianYuyue.setCreateTime(new Date()); // 设置创建时间 + jiaolianYuyueService.insert(jiaolianYuyue); // 调用服务层保存数据 + return R.ok(); // 返回成功信息 + }else { // 如果存在重复数据 + if(jiaolianYuyueEntity.getJiaolianYuyueYesnoTypes()==1) // 如果是待审核状态 + return R.error(511,"有相同的待审核的数据"); // 返回错误信息 + else if(jiaolianYuyueEntity.getJiaolianYuyueYesnoTypes()==2) // 如果是已通过状态 + return R.error(511,"有相同的审核通过的数据"); // 返回错误信息 + else + return R.error(511,"表中有相同数据"); // 返回错误信息 + } + } +} \ No newline at end of file From bf743d7819060020043b20281443e18671176788 Mon Sep 17 00:00:00 2001 From: phfsut2ie Date: Mon, 28 Apr 2025 18:37:56 +0800 Subject: [PATCH 18/22] ADD file via upload --- NewsController.java | 324 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 324 insertions(+) create mode 100644 NewsController.java diff --git a/NewsController.java b/NewsController.java new file mode 100644 index 0000000..d9aa54d --- /dev/null +++ b/NewsController.java @@ -0,0 +1,324 @@ +package com.controller; // 定义包名为com.controller + +import java.io.File; // 文件操作类 +import java.math.BigDecimal; // 大数字处理类 +import java.net.URL; // 统一资源定位符类 +import java.text.SimpleDateFormat; // 日期格式化类 +import com.alibaba.fastjson.JSONObject; // 阿里巴巴FastJSON库的JSON对象类 +import java.util.*; // 通用工具类集合 +import org.springframework.beans.BeanUtils; // Spring Bean工具类 +import javax.servlet.http.HttpServletRequest; // HTTP请求处理类 +import org.springframework.web.context.ContextLoader; // Spring上下文加载器 +import javax.servlet.ServletContext; // Servlet上下文接口 +import com.service.TokenService; // 自定义Token服务类 +import com.utils.*; // 自定义工具类包 +import java.lang.reflect.InvocationTargetException; // 反射异常类 + +import com.service.DictionaryService; // 自定义字典服务类 +import org.apache.commons.lang3.StringUtils; // Apache字符串工具类 +import com.annotation.IgnoreAuth; // 自定义忽略认证注解 +import org.slf4j.Logger; // 日志接口 +import org.slf4j.LoggerFactory; // 日志工厂类 +import org.springframework.beans.factory.annotation.Autowired; // Spring自动注入注解 +import org.springframework.stereotype.Controller; // Spring控制器注解 +import org.springframework.web.bind.annotation.*; // Spring Web注解集合 +import com.baomidou.mybatisplus.mapper.EntityWrapper; // MyBatis Plus实体包装器 +import com.baomidou.mybatisplus.mapper.Wrapper; // MyBatis Plus包装器接口 +import com.entity.*; // 自定义实体类包 +import com.entity.view.*; // 自定义视图类包 +import com.service.*; // 自定义服务类包 +import com.utils.PageUtils; // 自定义分页工具类 +import com.utils.R; // 自定义返回结果类 +import com.alibaba.fastjson.*; // 阿里巴巴FastJSON库 + + + + //健身资讯 + //后端接口 + //@author + // @email + +@RestController +@Controller +@RequestMapping("/news") +public class NewsController { + private static final Logger logger = LoggerFactory.getLogger(NewsController.class); + + private static final String TABLE_NAME = "news"; // 数据库表名 + + @Autowired + private NewsService newsService; // 健身资讯服务 + + @Autowired + private TokenService tokenService; // token服务 + + @Autowired + private DictionaryService dictionaryService; // 字典服务 + @Autowired + private ForumService forumService; // 健身论坛服务 + @Autowired + private JianshenkechengService jianshenkechengService; // 健身课程服务 + @Autowired + private JianshenkechengCollectionService jianshenkechengCollectionService; // 课程收藏服务 + @Autowired + private JianshenkechengLiuyanService jianshenkechengLiuyanService; // 课程留言服务 + @Autowired + private JiaolianService jiaolianService; // 教练服务 + @Autowired + private JiaolianYuyueService jiaolianYuyueService; // 教练预约申请服务 + @Autowired + private SingleSeachService singleSeachService; // 单页数据服务 + @Autowired + private YonghuService yonghuService; // 用户服务 + @Autowired + private UsersService usersService; // 管理员服务 + + + //后端列表 + //分页查询健身资讯数据 + //@param params 请求参数,包含分页和查询条件 + //@param request HTTP请求对象 + //@return 返回分页数据和状态信息 + @RequestMapping("/page") + public R page(@RequestParam Map params, HttpServletRequest request){ + logger.debug("page方法:,,Controller:{},,params:{}",this.getClass().getName(),JSONObject.toJSONString(params)); + String role = String.valueOf(request.getSession().getAttribute("role")); // 获取用户角色 + if(false) + return R.error(511,"永不会进入"); + else if("用户".equals(role)) + params.put("yonghuId",request.getSession().getAttribute("userId")); // 如果是用户角色,添加用户ID查询条件 + else if("教练".equals(role)) + params.put("jiaolianId",request.getSession().getAttribute("userId")); // 如果是教练角色,添加教练ID查询条件 + CommonUtil.checkMap(params); // 检查参数 + PageUtils page = newsService.queryPage(params); // 分页查询 + + // 字典表数据转换 + List list =(List)page.getList(); + for(NewsView c:list){ + // 修改对应字典表字段 + dictionaryService.dictionaryConvert(c, request); + } + return R.ok().put("data", page); // 返回成功和数据 + } + + + //后端详情 + //根据ID查询单条健身资讯详情 + //@param id 资讯ID + //@param request HTTP请求对象 + //@return 返回资讯详情和状态信息 + @RequestMapping("/info/{id}") + public R info(@PathVariable("id") Long id, HttpServletRequest request){ + logger.debug("info方法:,,Controller:{},,id:{}",this.getClass().getName(),id); + NewsEntity news = newsService.selectById(id); // 根据ID查询资讯 + if(news !=null){ + // entity转view + NewsView view = new NewsView(); + BeanUtils.copyProperties( news , view ); // 把实体数据重构到view中 + // 修改对应字典表字段 + dictionaryService.dictionaryConvert(view, request); + return R.ok().put("data", view); // 返回成功和数据 + }else { + return R.error(511,"查不到数据"); // 返回错误信息 + } + } + + + //后端保存 + //新增健身资讯 + //@param news 资讯实体 + //@param request HTTP请求对象 + //@return 返回操作结果状态 + @RequestMapping("/save") + public R save(@RequestBody NewsEntity news, HttpServletRequest request){ + logger.debug("save方法:,,Controller:{},,news:{}",this.getClass().getName(),news.toString()); + + String role = String.valueOf(request.getSession().getAttribute("role")); // 获取用户角色 + if(false) + return R.error(511,"永远不会进入"); + + // 构建查询条件 + Wrapper queryWrapper = new EntityWrapper() + .eq("news_name", news.getNewsName()) // 资讯标题相等 + .eq("news_types", news.getNewsTypes()) // 资讯类型相等 + ; + + logger.info("sql语句:"+queryWrapper.getSqlSegment()); + NewsEntity newsEntity = newsService.selectOne(queryWrapper); // 查询是否已存在 + if(newsEntity==null){ + news.setInsertTime(new Date()); // 设置插入时间 + news.setCreateTime(new Date()); // 设置创建时间 + newsService.insert(news); // 插入新数据 + return R.ok(); // 返回成功 + }else { + return R.error(511,"表中有相同数据"); // 返回错误信息 + } + } + + //后端修改 + //更新健身资讯 + //@param news 资讯实体 + //@param request HTTP请求对象 + //@return 返回操作结果状态 + @RequestMapping("/update") + public R update(@RequestBody NewsEntity news, HttpServletRequest request) throws NoSuchFieldException, ClassNotFoundException, IllegalAccessException, InstantiationException { + logger.debug("update方法:,,Controller:{},,news:{}",this.getClass().getName(),news.toString()); + NewsEntity oldNewsEntity = newsService.selectById(news.getId()); // 查询原先数据 + + String role = String.valueOf(request.getSession().getAttribute("role")); // 获取用户角色 +// if(false) +// return R.error(511,"永远不会进入"); + if("".equals(news.getNewsPhoto()) || "null".equals(news.getNewsPhoto())){ + news.setNewsPhoto(null); // 如果图片为空,设置为null + } + if("".equals(news.getNewsContent()) || "null".equals(news.getNewsContent())){ + news.setNewsContent(null); // 如果内容为空,设置为null + } + + newsService.updateById(news); // 根据id更新 + return R.ok(); // 返回成功 + } + + + //删除 + //批量删除健身资讯 + //@param ids 要删除的资讯ID数组 + //@param request HTTP请求对象 + //@return 返回操作结果状态 + @RequestMapping("/delete") + public R delete(@RequestBody Integer[] ids, HttpServletRequest request){ + logger.debug("delete:,,Controller:{},,ids:{}",this.getClass().getName(),ids.toString()); + List oldNewsList =newsService.selectBatchIds(Arrays.asList(ids)); // 查询要删除的数据 + newsService.deleteBatchIds(Arrays.asList(ids)); // 批量删除 + + return R.ok(); // 返回成功 + } + + + //批量上传 + //通过Excel文件批量导入健身资讯数据 + //@param fileName Excel文件名 + //@param request HTTP请求对象 + //@return 返回操作结果状态 + @RequestMapping("/batchInsert") + public R save( String fileName, HttpServletRequest request){ + logger.debug("batchInsert方法:,,Controller:{},,fileName:{}",this.getClass().getName(),fileName); + Integer yonghuId = Integer.valueOf(String.valueOf(request.getSession().getAttribute("userId"))); // 获取用户ID + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); // 日期格式化 + try { + List newsList = new ArrayList<>(); // 存储上传的数据 + Map> seachFields= new HashMap<>(); // 要查询的字段 + Date date = new Date(); + int lastIndexOf = fileName.lastIndexOf("."); // 获取文件后缀 + if(lastIndexOf == -1){ + return R.error(511,"该文件没有后缀"); // 返回错误信息 + }else{ + String suffix = fileName.substring(lastIndexOf); // 获取文件后缀 + if(!".xls".equals(suffix)){ + return R.error(511,"只支持后缀为xls的excel文件"); // 返回错误信息 + }else{ + URL resource = this.getClass().getClassLoader().getResource("static/upload/" + fileName); // 获取文件路径 + File file = new File(resource.getFile()); + if(!file.exists()){ + return R.error(511,"找不到上传文件,请联系管理员"); // 返回错误信息 + }else{ + List> dataList = PoiUtil.poiImport(file.getPath()); // 读取xls文件 + dataList.remove(0); // 删除第一行,因为第一行是提示 + for(List data:dataList){ + // 循环处理每行数据 + NewsEntity newsEntity = new NewsEntity(); +// newsEntity.setNewsName(data.get(0)); // 资讯标题 要改的 +// newsEntity.setNewsTypes(Integer.valueOf(data.get(0))); // 资讯类型 要改的 +// newsEntity.setNewsPhoto("");//详情和图片 +// newsEntity.setInsertTime(date);//时间 +// newsEntity.setNewsContent("");//详情和图片 +// newsEntity.setCreateTime(date);//时间 + newsList.add(newsEntity); // 添加到列表 + + // 把要查询是否重复的字段放入map中 + } + + // 查询是否重复 + newsService.insertBatch(newsList); // 批量插入 + return R.ok(); // 返回成功 + } + } + } + }catch (Exception e){ + e.printStackTrace(); + return R.error(511,"批量插入数据异常,请联系管理员"); // 返回错误信息 + } + } + + + //前端列表 + //分页查询健身资讯数据(无需认证) + //@param params 请求参数,包含分页和查询条件 + //@param request HTTP请求对象 + //@return 返回分页数据和状态信息 + @IgnoreAuth + @RequestMapping("/list") + public R list(@RequestParam Map params, HttpServletRequest request){ + logger.debug("list方法:,,Controller:{},,params:{}",this.getClass().getName(),JSONObject.toJSONString(params)); + + CommonUtil.checkMap(params); // 检查参数 + PageUtils page = newsService.queryPage(params); // 分页查询 + + // 字典表数据转换 + List list =(List)page.getList(); + for(NewsView c:list) + dictionaryService.dictionaryConvert(c, request); // 修改对应字典表字段 + + return R.ok().put("data", page); // 返回成功和数据 + } + + + //前端详情 + //根据ID查询单条健身资讯详情 + //@param id 资讯ID + //@param request HTTP请求对象 + //@return 返回资讯详情和状态信息 + @RequestMapping("/detail/{id}") + public R detail(@PathVariable("id") Integer id, HttpServletRequest request){ + logger.debug("detail方法:,,Controller:{},,id:{}",this.getClass().getName(),id); + NewsEntity news = newsService.selectById(id); // 根据ID查询资讯 + if(news !=null){ + // entity转view + NewsView view = new NewsView(); + BeanUtils.copyProperties( news , view ); // 把实体数据重构到view中 + + // 修改对应字典表字段 + dictionaryService.dictionaryConvert(view, request); + return R.ok().put("data", view); // 返回成功和数据 + }else { + return R.error(511,"查不到数据"); // 返回错误信息 + } + } + + + //前端保存 + //新增健身资讯 + //@param news 资讯实体 + //@param request HTTP请求对象 + //@return 返回操作结果状态 + @RequestMapping("/add") + public R add(@RequestBody NewsEntity news, HttpServletRequest request){ + logger.debug("add方法:,,Controller:{},,news:{}",this.getClass().getName(),news.toString()); + Wrapper queryWrapper = new EntityWrapper() + .eq("news_name", news.getNewsName()) // 资讯标题相等 + .eq("news_types", news.getNewsTypes()) // 资讯类型相等 +// .notIn("news_types", new Integer[]{102}) + ; + logger.info("sql语句:"+queryWrapper.getSqlSegment()); + NewsEntity newsEntity = newsService.selectOne(queryWrapper); // 查询是否已存在 + if(newsEntity==null){ + news.setInsertTime(new Date()); // 设置插入时间 + news.setCreateTime(new Date()); // 设置创建时间 + newsService.insert(news); // 插入新数据 + + return R.ok(); // 返回成功 + }else { + return R.error(511,"表中有相同数据"); // 返回错误信息 + } + } +} \ No newline at end of file From 2c28f5f0c085d8c749d52a46c4a85ee0cde72c95 Mon Sep 17 00:00:00 2001 From: phfsut2ie Date: Mon, 28 Apr 2025 18:38:06 +0800 Subject: [PATCH 19/22] ADD file via upload --- SingleSeachController.java | 297 +++++++++++++++++++++++++++++++++++++ 1 file changed, 297 insertions(+) create mode 100644 SingleSeachController.java diff --git a/SingleSeachController.java b/SingleSeachController.java new file mode 100644 index 0000000..3054ac9 --- /dev/null +++ b/SingleSeachController.java @@ -0,0 +1,297 @@ +// 声明当前文件所在的包路径 +package com.controller; + +// 导入所需的Java类库 +import java.io.File; // 文件操作类 +import java.math.BigDecimal; // 高精度数字计算类 +import java.net.URL; // 统一资源定位符类 +import java.text.SimpleDateFormat; // 日期格式化类 +import com.alibaba.fastjson.JSONObject; // FastJSON的JSON对象类 +import java.util.*; // 通用工具类集合 +import org.springframework.beans.BeanUtils; // Spring Bean工具类 +import javax.servlet.http.HttpServletRequest; // HTTP请求类 +import org.springframework.web.context.ContextLoader; // Spring上下文加载器 +import javax.servlet.ServletContext; // Servlet上下文接口 +import com.service.TokenService; // 自定义Token服务类 +import com.utils.*; // 自定义工具类包 +import java.lang.reflect.InvocationTargetException; // 反射异常类 + +// 导入自定义服务类 +import com.service.DictionaryService; // 字典服务类 +import org.apache.commons.lang3.StringUtils; // Apache字符串工具类 +import com.annotation.IgnoreAuth; // 自定义忽略认证注解 +import org.slf4j.Logger; // 日志接口 +import org.slf4j.LoggerFactory; // 日志工厂类 +import org.springframework.beans.factory.annotation.Autowired; // Spring自动注入注解 +import org.springframework.stereotype.Controller; // Spring控制器注解 +import org.springframework.web.bind.annotation.*; // Spring Web注解集合 +import com.baomidou.mybatisplus.mapper.EntityWrapper; // MyBatis Plus实体包装类 +import com.baomidou.mybatisplus.mapper.Wrapper; // MyBatis Plus包装接口 +import com.entity.*; // 实体类包 +import com.entity.view.*; // 实体视图类包 +import com.service.*; // 服务接口包 +import com.utils.PageUtils; // 分页工具类 +import com.utils.R; // 通用返回结果类 +import com.alibaba.fastjson.*; // FastJSON工具包 + + + //单页数据 + //后端接口控制器 + //@author + //@email + +@RestController // 标识这是一个RESTful风格的控制器 +@Controller // 标识这是一个Spring MVC控制器 +@RequestMapping("/singleSeach") // 定义基础请求路径 +public class SingleSeachController { + private static final Logger logger = LoggerFactory.getLogger(SingleSeachController.class); // 日志记录器 + + private static final String TABLE_NAME = "singleSeach"; // 数据库表名常量 + + @Autowired + private SingleSeachService singleSeachService; // 注入单页数据服务 + + @Autowired + private TokenService tokenService; // 注入token服务 + + // 注入其他相关服务 + @Autowired + private DictionaryService dictionaryService; // 字典服务 + @Autowired + private ForumService forumService; // 健身论坛服务 + @Autowired + private JianshenkechengService jianshenkechengService; // 健身课程服务 + @Autowired + private JianshenkechengCollectionService jianshenkechengCollectionService; // 课程收藏服务 + @Autowired + private JianshenkechengLiuyanService jianshenkechengLiuyanService; // 课程留言服务 + @Autowired + private JiaolianService jiaolianService; // 教练服务 + @Autowired + private JiaolianYuyueService jiaolianYuyueService; // 教练预约申请服务 + @Autowired + private NewsService newsService; // 健身资讯服务 + @Autowired + private YonghuService yonghuService; // 用户服务 + @Autowired + private UsersService usersService; // 管理员服务 + + + //后端列表 + //分页查询单页数据 + //@param params 请求参数Map + //@param request HTTP请求对象 + //@return 返回分页数据结果 + + @RequestMapping("/page") + public R page(@RequestParam Map params, HttpServletRequest request){ + logger.debug("page方法:,,Controller:{},,params:{}",this.getClass().getName(),JSONObject.toJSONString(params)); // 记录日志 + String role = String.valueOf(request.getSession().getAttribute("role")); // 获取用户角色 + if(false) // 条件判断 + return R.error(511,"永不会进入"); // 返回错误信息 + else if("用户".equals(role)) // 如果是用户角色 + params.put("yonghuId",request.getSession().getAttribute("userId")); // 添加用户ID参数 + else if("教练".equals(role)) // 如果是教练角色 + params.put("jiaolianId",request.getSession().getAttribute("userId")); // 添加教练ID参数 + CommonUtil.checkMap(params); // 检查参数 + PageUtils page = singleSeachService.queryPage(params); // 调用服务层分页查询 + + // 字典表数据转换 + List list =(List)page.getList(); // 获取分页数据列表 + for(SingleSeachView c:list){ // 遍历列表 + dictionaryService.dictionaryConvert(c, request); // 转换字典表字段 + } + return R.ok().put("data", page); // 返回成功结果和数据 + } + + + //后端详情 + //根据ID查询单条单页数据详情 + //@param id 数据ID + //@param request HTTP请求对象 + //@return 返回查询结果 + @RequestMapping("/info/{id}") + public R info(@PathVariable("id") Long id, HttpServletRequest request){ + logger.debug("info方法:,,Controller:{},,id:{}",this.getClass().getName(),id); // 记录日志 + SingleSeachEntity singleSeach = singleSeachService.selectById(id); // 根据ID查询数据 + if(singleSeach !=null){ // 如果查询到数据 + SingleSeachView view = new SingleSeachView(); // 创建视图对象 + BeanUtils.copyProperties(singleSeach, view); // 复制属性到视图对象 + dictionaryService.dictionaryConvert(view, request); // 转换字典表字段 + return R.ok().put("data", view); // 返回成功结果和数据 + }else { + return R.error(511,"查不到数据"); // 返回错误信息 + } + } + + + //后端保存 + //新增单页数据 + //@param singleSeach 单页数据实体 + //@param request HTTP请求对象 + //@return 返回操作结果 + //@RequestMapping("/save") + public R save(@RequestBody SingleSeachEntity singleSeach, HttpServletRequest request){ + logger.debug("save方法:,,Controller:{},,singleSeach:{}",this.getClass().getName(),singleSeach.toString()); // 记录日志 + String role = String.valueOf(request.getSession().getAttribute("role")); // 获取用户角色 + if(false) // 条件判断 + return R.error(511,"永远不会进入"); // 返回错误信息 + + // 构建查询条件 + Wrapper queryWrapper = new EntityWrapper() + .eq("single_seach_types",singleSeach.getSingleSeachTypes()); // 按类型查询 + + logger.info("sql语句:"+queryWrapper.getSqlSegment()); // 记录SQL语句 + SingleSeachEntity singleSeachEntity = singleSeachService.selectOne(queryWrapper); // 查询是否存在 + if(singleSeachEntity==null){ // 如果不存在 + singleSeach.setCreateTime(new Date()); // 设置创建时间 + singleSeachService.insert(singleSeach); // 插入新数据 + return R.ok(); // 返回成功结果 + }else { + return R.error(511,"该类型已经有存在的,请删除后重新新增"); // 返回错误信息 + } + } + + //后端修改 + //更新单页数据 + //@param singleSeach 单页数据实体 + //@param request HTTP请求对象 + //@return 返回操作结果 + @RequestMapping("/update") + public R update(@RequestBody SingleSeachEntity singleSeach, HttpServletRequest request) throws NoSuchFieldException, ClassNotFoundException, IllegalAccessException, InstantiationException { + logger.debug("update方法:,,Controller:{},,singleSeach:{}",this.getClass().getName(),singleSeach.toString()); // 记录日志 + SingleSeachEntity oldSingleSeachEntity = singleSeachService.selectById(singleSeach.getId()); // 查询原数据 + + String role = String.valueOf(request.getSession().getAttribute("role")); // 获取用户角色 + if("".equals(singleSeach.getSingleSeachPhoto()) || "null".equals(singleSeach.getSingleSeachPhoto())){ // 如果图片为空 + singleSeach.setSingleSeachPhoto(null); // 设置为null + } + if("".equals(singleSeach.getSingleSeachContent()) || "null".equals(singleSeach.getSingleSeachContent())){ // 如果内容为空 + singleSeach.setSingleSeachContent(null); // 设置为null + } + + singleSeachService.updateById(singleSeach); // 更新数据 + return R.ok(); // 返回成功结果 + } + + + //删除 + //批量删除单页数据 + //@param ids 要删除的ID数组 + //@param request HTTP请求对象 + //@return 返回操作结果 + @RequestMapping("/delete") + public R delete(@RequestBody Integer[] ids, HttpServletRequest request){ + logger.debug("delete:,,Controller:{},,ids:{}",this.getClass().getName(),ids.toString()); // 记录日志 + List oldSingleSeachList = singleSeachService.selectBatchIds(Arrays.asList(ids)); // 查询要删除的数据 + singleSeachService.deleteBatchIds(Arrays.asList(ids)); // 批量删除 + return R.ok(); // 返回成功结果 + } + + + //批量上传/ + //通过Excel批量导入数据 + //@param fileName Excel文件名 + //@param request HTTP请求对象 + //@return 返回操作结果 + @RequestMapping("/batchInsert") + public R save(String fileName, HttpServletRequest request){ + logger.debug("batchInsert方法:,,Controller:{},,fileName:{}",this.getClass().getName(),fileName); // 记录日志 + Integer yonghuId = Integer.valueOf(String.valueOf(request.getSession().getAttribute("userId"))); // 获取用户ID + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); // 创建日期格式化对象 + try { + List singleSeachList = new ArrayList<>(); // 创建数据列表 + Map> seachFields = new HashMap<>(); // 创建查询字段Map + Date date = new Date(); // 当前时间 + int lastIndexOf = fileName.lastIndexOf("."); // 获取文件后缀位置 + if(lastIndexOf == -1){ // 如果没有后缀 + return R.error(511,"该文件没有后缀"); // 返回错误信息 + }else{ + String suffix = fileName.substring(lastIndexOf); // 获取文件后缀 + if(!".xls".equals(suffix)){ // 如果不是xls文件 + return R.error(511,"只支持后缀为xls的excel文件"); // 返回错误信息 + }else{ + URL resource = this.getClass().getClassLoader().getResource("static/upload/" + fileName); // 获取文件路径 + File file = new File(resource.getFile()); // 创建文件对象 + if(!file.exists()){ // 如果文件不存在 + return R.error(511,"找不到上传文件,请联系管理员"); // 返回错误信息 + }else{ + List> dataList = PoiUtil.poiImport(file.getPath()); // 读取Excel文件 + dataList.remove(0); // 删除标题行 + for(List data:dataList){ // 遍历数据行 + SingleSeachEntity singleSeachEntity = new SingleSeachEntity(); // 创建实体对象 + // 可以在此处设置实体属性,示例中被注释掉了 + singleSeachList.add(singleSeachEntity); // 添加到列表 + } + singleSeachService.insertBatch(singleSeachList); // 批量插入 + return R.ok(); // 返回成功结果 + } + } + } + }catch (Exception e){ // 捕获异常 + e.printStackTrace(); // 打印异常堆栈 + return R.error(511,"批量插入数据异常,请联系管理员"); // 返回错误信息 + } + } + + + //前端列表 + //无需认证的分页查询 + //@param params 请求参数Map + //@param request HTTP请求对象 + //@return 返回分页数据结果 + @IgnoreAuth // 忽略认证 + @RequestMapping("/list") + public R list(@RequestParam Map params, HttpServletRequest request){ + logger.debug("list方法:,,Controller:{},,params:{}",this.getClass().getName(),JSONObject.toJSONString(params)); // 记录日志 + CommonUtil.checkMap(params); // 检查参数 + PageUtils page = singleSeachService.queryPage(params); // 分页查询 + + List list = (List)page.getList(); // 获取数据列表 + for(SingleSeachView c:list) // 遍历列表 + dictionaryService.dictionaryConvert(c, request); // 转换字典表字段 + + return R.ok().put("data", page); // 返回成功结果和数据 + } + + + //前端详情 + //根据类型查询单条数据 + //@param id 数据类型ID + //@param request HTTP请求对象 + //@return 返回查询结果 + @RequestMapping("/detail/{id}") + public R detail(@PathVariable("id") Integer id, HttpServletRequest request){ + logger.debug("detail方法:,,Controller:{},,id:{}",this.getClass().getName(),id); // 记录日志 + // 根据类型查询单条数据 + SingleSeachEntity singleSeach = singleSeachService.selectOne(new EntityWrapper().eq("single_seach_types", id)); + if(singleSeach != null) // 如果查询到数据 + return R.ok().put("data", singleSeach); // 返回成功结果和数据 + else + return R.error(511,"查不到数据"); // 返回错误信息 + } + + + //前端保存 + //新增单页数据 + //@param singleSeach 单页数据实体 + //@param request HTTP请求对象 + //@return 返回操作结果 + @RequestMapping("/add") + public R add(@RequestBody SingleSeachEntity singleSeach, HttpServletRequest request){ + logger.debug("add方法:,,Controller:{},,singleSeach:{}",this.getClass().getName(),singleSeach.toString()); // 记录日志 + // 构建查询条件 + Wrapper queryWrapper = new EntityWrapper() + .eq("single_seach_types",singleSeach.getSingleSeachTypes()); // 按类型查询 + + logger.info("sql语句:"+queryWrapper.getSqlSegment()); // 记录SQL语句 + SingleSeachEntity singleSeachEntity = singleSeachService.selectOne(queryWrapper); // 查询是否已存在 + if(singleSeachEntity==null){ // 如果不存在 + singleSeach.setCreateTime(new Date()); // 设置创建时间 + singleSeachService.insert(singleSeach); // 插入新数据 + return R.ok(); // 返回成功结果 + }else { + return R.error(511,"该类型已经有存在的,请删除后重新新增"); // 返回错误信息 + } + } +} \ No newline at end of file From cac9dfba94d25b3222ffbe529f9563fa7d704670 Mon Sep 17 00:00:00 2001 From: phfsut2ie Date: Mon, 28 Apr 2025 18:38:16 +0800 Subject: [PATCH 20/22] ADD file via upload --- YonghuController.java | 532 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 532 insertions(+) create mode 100644 YonghuController.java diff --git a/YonghuController.java b/YonghuController.java new file mode 100644 index 0000000..3b6b85c --- /dev/null +++ b/YonghuController.java @@ -0,0 +1,532 @@ +package com.controller; // 声明当前类所在的包路径 + +// 导入Java IO类 +import java.io.File; // 文件操作类 +// 导入数学类 +import java.math.BigDecimal; // 高精度数字计算类 +// 导入网络类 +import java.net.URL; // 统一资源定位符类 +// 导入文本处理类 +import java.text.SimpleDateFormat; // 日期格式化类 +// 导入JSON处理类 +import com.alibaba.fastjson.JSONObject; // FastJSON对象类 +// 导入集合类 +import java.util.*; // 通用集合工具类 +// 导入Spring Bean工具类 +import org.springframework.beans.BeanUtils; // Bean属性复制工具类 +// 导入Servlet类 +import javax.servlet.http.HttpServletRequest; // HTTP请求对象 +// 导入Spring上下文类 +import org.springframework.web.context.ContextLoader; // Web应用上下文加载器 +// 导入Servlet上下文类 +import javax.servlet.ServletContext; // Servlet上下文对象 +// 导入自定义服务类 +import com.service.TokenService; // Token服务接口 +// 导入自定义工具类 +import com.utils.*; // 自定义工具类集合 +// 导入反射异常类 +import java.lang.reflect.InvocationTargetException; // 反射调用异常类 + +// 导入字典服务类 +import com.service.DictionaryService; // 字典数据服务接口 +// 导入Apache Commons工具类 +import org.apache.commons.lang3.StringUtils; // 字符串处理工具类 +// 导入自定义注解 +import com.annotation.IgnoreAuth; // 忽略认证注解 +// 导入日志类 +import org.slf4j.Logger; // 日志接口 +import org.slf4j.LoggerFactory; // 日志工厂类 +// 导入Spring注解 +import org.springframework.beans.factory.annotation.Autowired; // 自动注入注解 +import org.springframework.stereotype.Controller; // 控制器注解 +import org.springframework.web.bind.annotation.*; // Web请求相关注解 +// 导入MyBatis Plus类 +import com.baomidou.mybatisplus.mapper.EntityWrapper; // 条件构造器 +import com.baomidou.mybatisplus.mapper.Wrapper; // 条件构造器接口 +// 导入实体类 +import com.entity.*; // 实体类集合 +import com.entity.view.*; // 视图实体类集合 +// 导入服务接口 +import com.service.*; // 服务接口集合 +// 导入分页工具类 +import com.utils.PageUtils; // 分页工具类 +// 导入返回结果类 +import com.utils.R; // 统一返回结果类 +// 导入FastJSON类 +import com.alibaba.fastjson.*; // FastJSON工具类 + + + //用户控制器 + // 处理用户相关操作的RESTful接口 + +@RestController // 标识为RESTful控制器 +@Controller // 标识为Spring MVC控制器 +@RequestMapping("/yonghu") // 基础请求路径 +public class YonghuController { + private static final Logger logger = LoggerFactory.getLogger(YonghuController.class); // 日志记录器 + + private static final String TABLE_NAME = "yonghu"; // 数据库表名常量 + + @Autowired // 自动注入用户服务 + private YonghuService yonghuService; + + @Autowired // 自动注入Token服务 + private TokenService tokenService; + + // 注入其他相关服务 + @Autowired + private DictionaryService dictionaryService; // 字典服务 + @Autowired + private ForumService forumService; // 健身论坛服务 + @Autowired + private JianshenkechengService jianshenkechengService; // 健身课程服务 + @Autowired + private JianshenkechengCollectionService jianshenkechengCollectionService; // 课程收藏服务 + @Autowired + private JianshenkechengLiuyanService jianshenkechengLiuyanService; // 课程留言服务 + @Autowired + private JiaolianService jiaolianService; // 教练服务 + @Autowired + private JiaolianYuyueService jiaolianYuyueService; // 教练预约服务 + @Autowired + private NewsService newsService; // 健身资讯服务 + @Autowired + private SingleSeachService singleSeachService; // 单页数据服务 + @Autowired + private UsersService usersService; // 管理员服务 + + //后端分页列表 + //@param params 请求参数Map + //@param request HTTP请求对象 + //@return 分页结果 + @RequestMapping("/page") + public R page(@RequestParam Map params, HttpServletRequest request){ + logger.debug("page方法:,,Controller:{},,params:{}",this.getClass().getName(),JSONObject.toJSONString(params)); + String role = String.valueOf(request.getSession().getAttribute("role")); // 获取用户角色 + if(false) // 条件判断 + return R.error(511,"永不会进入"); // 返回错误信息 + else if("用户".equals(role)) // 如果是用户角色 + params.put("yonghuId",request.getSession().getAttribute("userId")); // 添加用户ID参数 + else if("教练".equals(role)) // 如果是教练角色 + params.put("jiaolianId",request.getSession().getAttribute("userId")); // 添加教练ID参数 + params.put("dataDeleteStart",1);params.put("dataDeleteEnd",1); // 设置逻辑删除条件 + CommonUtil.checkMap(params); // 检查参数 + PageUtils page = yonghuService.queryPage(params); // 分页查询 + + // 字典表数据转换 + List list =(List)page.getList(); // 获取分页数据 + for(YonghuView c:list){ // 遍历数据 + dictionaryService.dictionaryConvert(c, request); // 转换字典表字段 + } + return R.ok().put("data", page); // 返回分页结果 + } + + + //后端详情 + //@param id 用户ID + //@param request HTTP请求对象 + //@return 用户详情 + @RequestMapping("/info/{id}") + public R info(@PathVariable("id") Long id, HttpServletRequest request){ + logger.debug("info方法:,,Controller:{},,id:{}",this.getClass().getName(),id); + YonghuEntity yonghu = yonghuService.selectById(id); // 根据ID查询用户 + if(yonghu !=null){ // 如果查询到数据 + YonghuView view = new YonghuView(); // 创建视图对象 + BeanUtils.copyProperties(yonghu, view); // 复制属性到视图对象 + dictionaryService.dictionaryConvert(view, request); // 转换字典表字段 + return R.ok().put("data", view); // 返回成功结果 + }else { + return R.error(511,"查不到数据"); // 返回错误信息 + } + } + + //后端保存用户 + //@param yonghu 用户实体 + //@param request HTTP请求对象 + //@return 操作结果 + @RequestMapping("/save") + public R save(@RequestBody YonghuEntity yonghu, HttpServletRequest request){ + logger.debug("save方法:,,Controller:{},,yonghu:{}",this.getClass().getName(),yonghu.toString()); + String role = String.valueOf(request.getSession().getAttribute("role")); // 获取用户角色 + if(false) // 条件判断 + return R.error(511,"永远不会进入"); // 返回错误信息 + + // 构建查询条件:检查用户名、手机号、身份证号是否已存在 + Wrapper queryWrapper = new EntityWrapper() + .eq("username", yonghu.getUsername()) // 用户名条件 + .or() // 或条件 + .eq("yonghu_phone", yonghu.getYonghuPhone()) // 手机号条件 + .or() // 或条件 + .eq("yonghu_id_number", yonghu.getYonghuIdNumber()) // 身份证号条件 + .eq("data_delete", 1) // 未删除条件 + ; + + logger.info("sql语句:"+queryWrapper.getSqlSegment()); // 记录SQL语句 + YonghuEntity yonghuEntity = yonghuService.selectOne(queryWrapper); // 查询是否存在 + if(yonghuEntity==null){ // 如果不存在 + yonghu.setDataDelete(1); // 设置未删除状态 + yonghu.setInsertTime(new Date()); // 设置插入时间 + yonghu.setCreateTime(new Date()); // 设置创建时间 + yonghu.setPassword("123456"); // 设置默认密码 + yonghuService.insert(yonghu); // 插入新用户 + return R.ok(); // 返回成功结果 + }else { + return R.error(511,"账户或者用户手机号或者用户身份证号已经被使用"); // 返回错误信息 + } + } + + + //后端修改用户 + //@param yonghu 用户实体 + //@param request HTTP请求对象 + //@return 操作结果 + @RequestMapping("/update") + public R update(@RequestBody YonghuEntity yonghu, HttpServletRequest request) throws NoSuchFieldException, ClassNotFoundException, IllegalAccessException, InstantiationException { + logger.debug("update方法:,,Controller:{},,yonghu:{}",this.getClass().getName(),yonghu.toString()); + YonghuEntity oldYonghuEntity = yonghuService.selectById(yonghu.getId()); // 查询原数据 + String role = String.valueOf(request.getSession().getAttribute("role")); // 获取用户角色 + + if("".equals(yonghu.getYonghuPhoto()) || "null".equals(yonghu.getYonghuPhoto())){ // 如果图片为空 + yonghu.setYonghuPhoto(null); // 设置为null + } + + yonghuService.updateById(yonghu); // 更新用户信息 + return R.ok(); // 返回成功结果 + } + + + //删除用户(逻辑删除) + //@param ids 用户ID数组 + //@param request HTTP请求对象 + //@return 操作结果 + @RequestMapping("/delete") + public R delete(@RequestBody Integer[] ids, HttpServletRequest request){ + logger.debug("delete:,,Controller:{},,ids:{}",this.getClass().getName(),ids.toString()); + List oldYonghuList = yonghuService.selectBatchIds(Arrays.asList(ids)); // 查询要删除的数据 + ArrayList list = new ArrayList<>(); // 创建更新列表 + for(Integer id:ids){ // 遍历ID数组 + YonghuEntity yonghuEntity = new YonghuEntity(); // 创建用户实体 + yonghuEntity.setId(id); // 设置ID + yonghuEntity.setDataDelete(2); // 设置删除状态 + list.add(yonghuEntity); // 添加到列表 + } + if(list != null && list.size() >0){ // 如果有数据需要更新 + yonghuService.updateBatchById(list); // 批量更新 + } + return R.ok(); // 返回成功结果 + } + + + //批量导入用户数据 + //@param fileName Excel文件名 + //@param request HTTP请求对象 + //@return 导入结果 + @RequestMapping("/batchInsert") + public R save(String fileName, HttpServletRequest request){ + logger.debug("batchInsert方法:,,Controller:{},,fileName:{}",this.getClass().getName(),fileName); + Integer yonghuId = Integer.valueOf(String.valueOf(request.getSession().getAttribute("userId"))); // 获取当前用户ID + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); // 创建日期格式化对象 + try { + List yonghuList = new ArrayList<>(); // 创建用户列表 + Map> seachFields = new HashMap<>(); // 创建查重字段Map + Date date = new Date(); // 当前时间 + int lastIndexOf = fileName.lastIndexOf("."); // 获取文件后缀位置 + if(lastIndexOf == -1){ // 如果没有后缀 + return R.error(511,"该文件没有后缀"); // 返回错误信息 + }else{ + String suffix = fileName.substring(lastIndexOf); // 获取文件后缀 + if(!".xls".equals(suffix)){ // 如果不是xls文件 + return R.error(511,"只支持后缀为xls的excel文件"); // 返回错误信息 + }else{ + URL resource = this.getClass().getClassLoader().getResource("static/upload/" + fileName); // 获取文件路径 + File file = new File(resource.getFile()); // 创建文件对象 + if(!file.exists()){ // 如果文件不存在 + return R.error(511,"找不到上传文件,请联系管理员"); // 返回错误信息 + }else{ + List> dataList = PoiUtil.poiImport(file.getPath()); // 读取Excel文件 + dataList.remove(0); // 删除标题行 + for(List data:dataList){ // 遍历数据行 + YonghuEntity yonghuEntity = new YonghuEntity(); // 创建用户实体 + // 可以在此处设置实体属性,示例中被注释掉了 + yonghuList.add(yonghuEntity); // 添加到列表 + + // 构建查重字段Map + // 账户查重 + if(seachFields.containsKey("username")){ + List username = seachFields.get("username"); + username.add(data.get(0)); // 添加用户名 + }else{ + List username = new ArrayList<>(); + username.add(data.get(0)); // 添加用户名 + seachFields.put("username",username); // 放入Map + } + // 手机号查重 + if(seachFields.containsKey("yonghuPhone")){ + List yonghuPhone = seachFields.get("yonghuPhone"); + yonghuPhone.add(data.get(0)); // 添加手机号 + }else{ + List yonghuPhone = new ArrayList<>(); + yonghuPhone.add(data.get(0)); // 添加手机号 + seachFields.put("yonghuPhone",yonghuPhone); // 放入Map + } + // 身份证号查重 + if(seachFields.containsKey("yonghuIdNumber")){ + List yonghuIdNumber = seachFields.get("yonghuIdNumber"); + yonghuIdNumber.add(data.get(0)); // 添加身份证号 + }else{ + List yonghuIdNumber = new ArrayList<>(); + yonghuIdNumber.add(data.get(0)); // 添加身份证号 + seachFields.put("yonghuIdNumber",yonghuIdNumber); // 放入Map + } + } + + // 查重验证 + // 验证用户名是否重复 + List yonghuEntities_username = yonghuService.selectList( + new EntityWrapper().in("username", seachFields.get("username")).eq("data_delete", 1)); + if(yonghuEntities_username.size() >0 ){ // 如果有重复 + ArrayList repeatFields = new ArrayList<>(); // 创建重复字段列表 + for(YonghuEntity s:yonghuEntities_username){ // 遍历重复数据 + repeatFields.add(s.getUsername()); // 添加重复用户名 + } + return R.error(511,"数据库的该表中的 [账户] 字段已经存在 存在数据为:"+repeatFields.toString()); // 返回错误信息 + } + // 验证手机号是否重复 + List yonghuEntities_yonghuPhone = yonghuService.selectList( + new EntityWrapper().in("yonghu_phone", seachFields.get("yonghuPhone")).eq("data_delete", 1)); + if(yonghuEntities_yonghuPhone.size() >0 ){ // 如果有重复 + ArrayList repeatFields = new ArrayList<>(); // 创建重复字段列表 + for(YonghuEntity s:yonghuEntities_yonghuPhone){ // 遍历重复数据 + repeatFields.add(s.getYonghuPhone()); // 添加重复手机号 + } + return R.error(511,"数据库的该表中的 [用户手机号] 字段已经存在 存在数据为:"+repeatFields.toString()); // 返回错误信息 + } + // 验证身份证号是否重复 + List yonghuEntities_yonghuIdNumber = yonghuService.selectList( + new EntityWrapper().in("yonghu_id_number", seachFields.get("yonghuIdNumber")).eq("data_delete", 1)); + if(yonghuEntities_yonghuIdNumber.size() >0 ){ // 如果有重复 + ArrayList repeatFields = new ArrayList<>(); // 创建重复字段列表 + for(YonghuEntity s:yonghuEntities_yonghuIdNumber){ // 遍历重复数据 + repeatFields.add(s.getYonghuIdNumber()); // 添加重复身份证号 + } + return R.error(511,"数据库的该表中的 [用户身份证号] 字段已经存在 存在数据为:"+repeatFields.toString()); // 返回错误信息 + } + yonghuService.insertBatch(yonghuList); // 批量插入数据 + return R.ok(); // 返回成功结果 + } + } + } + }catch (Exception e){ // 捕获异常 + e.printStackTrace(); // 打印异常堆栈 + return R.error(511,"批量插入数据异常,请联系管理员"); // 返回错误信息 + } + } + + + //用户登录 + //@param username 用户名 + //@param password 密码 + //@param captcha 验证码 + //@param request HTTP请求对象 + //@return 登录结果 + @IgnoreAuth // 忽略认证 + @RequestMapping(value = "/login") + public R login(String username, String password, String captcha, HttpServletRequest request) { + YonghuEntity yonghu = yonghuService.selectOne(new EntityWrapper().eq("username", username)); // 根据用户名查询用户 + if(yonghu==null || !yonghu.getPassword().equals(password)) // 验证用户名和密码 + return R.error("账号或密码不正确"); // 返回错误信息 + else if(yonghu.getDataDelete() != 1) // 检查是否被删除 + return R.error("账户已被删除"); // 返回错误信息 + String token = tokenService.generateToken(yonghu.getId(),username, "yonghu", "用户"); // 生成Token + R r = R.ok(); // 创建返回结果 + r.put("token", token); // 添加Token + r.put("role","用户"); // 添加角色 + r.put("username",yonghu.getYonghuName()); // 添加用户名 + r.put("tableName","yonghu"); // 添加表名 + r.put("userId",yonghu.getId()); // 添加用户ID + return r; // 返回结果 + } + + + //用户注册 + //@param yonghu 用户实体 + // @param request HTTP请求对象 + //@return 注册结果 + + @IgnoreAuth // 忽略认证 + @PostMapping(value = "/register") + public R register(@RequestBody YonghuEntity yonghu, HttpServletRequest request) { + // 构建查询条件:检查用户名、手机号、身份证号是否已存在 + Wrapper queryWrapper = new EntityWrapper() + .eq("username", yonghu.getUsername()) // 用户名条件 + .or() // 或条件 + .eq("yonghu_phone", yonghu.getYonghuPhone()) // 手机号条件 + .or() // 或条件 + .eq("yonghu_id_number", yonghu.getYonghuIdNumber()) // 身份证号条件 + .andNew() // 新条件组 + .eq("data_delete", 1) // 未删除条件 + ; + YonghuEntity yonghuEntity = yonghuService.selectOne(queryWrapper); // 查询是否存在 + if(yonghuEntity != null) // 如果已存在 + return R.error("账户或者用户手机号或者用户身份证号已经被使用"); // 返回错误信息 + yonghu.setNewMoney(0.0); // 设置初始余额 + yonghu.setDataDelete(1); // 设置未删除状态 + yonghu.setInsertTime(new Date()); // 设置插入时间 + yonghu.setCreateTime(new Date()); // 设置创建时间 + yonghuService.insert(yonghu); // 插入新用户 + return R.ok(); // 返回成功结果 + } + + + //重置密码为默认值 + //@param id 用户ID + //@param request HTTP请求对象 + // @return 操作结果 + + @GetMapping(value = "/resetPassword") + public R resetPassword(Integer id, HttpServletRequest request) { + YonghuEntity yonghu = yonghuService.selectById(id); // 根据ID查询用户 + yonghu.setPassword("123456"); // 重置密码 + yonghuService.updateById(yonghu); // 更新用户 + return R.ok(); // 返回成功结果 + } + + //修改密码 + //@param oldPassword 旧密码 + //@param newPassword 新密码 + //@param request HTTP请求对象 + // @return 操作结果 + + @GetMapping(value = "/updatePassword") + public R updatePassword(String oldPassword, String newPassword, HttpServletRequest request) { + YonghuEntity yonghu = yonghuService.selectById((Integer)request.getSession().getAttribute("userId")); // 获取当前用户 + if(newPassword == null){ // 检查新密码是否为空 + return R.error("新密码不能为空") ; // 返回错误信息 + } + if(!oldPassword.equals(yonghu.getPassword())){ // 检查旧密码是否正确 + return R.error("原密码输入错误"); // 返回错误信息 + } + if(newPassword.equals(yonghu.getPassword())){ // 检查新旧密码是否相同 + return R.error("新密码不能和原密码一致") ; // 返回错误信息 + } + yonghu.setPassword(newPassword); // 设置新密码 + yonghuService.updateById(yonghu); // 更新用户 + return R.ok(); // 返回成功结果 + } + + //忘记密码(重置为默认密码) + //@param username 用户名 + //@param request HTTP请求对象 + //@return 操作结果 + @IgnoreAuth // 忽略认证 + @RequestMapping(value = "/resetPass") + public R resetPass(String username, HttpServletRequest request) { + YonghuEntity yonghu = yonghuService.selectOne(new EntityWrapper().eq("username", username)); // 根据用户名查询用户 + if(yonghu!=null){ // 如果用户存在 + yonghu.setPassword("123456"); // 重置密码 + yonghuService.updateById(yonghu); // 更新用户 + return R.ok(); // 返回成功结果 + }else{ // 如果用户不存在 + return R.error("账号不存在"); // 返回错误信息 + } + } + + + //获取当前会话用户信息 + //@param request HTTP请求对象 + //@return 用户信息 + @RequestMapping("/session") + public R getCurrYonghu(HttpServletRequest request){ + Integer id = (Integer)request.getSession().getAttribute("userId"); // 获取当前用户ID + YonghuEntity yonghu = yonghuService.selectById(id); // 查询用户信息 + if(yonghu !=null){ // 如果用户存在 + YonghuView view = new YonghuView(); // 创建视图对象 + BeanUtils.copyProperties(yonghu, view); // 复制属性 + dictionaryService.dictionaryConvert(view, request); // 转换字典字段 + return R.ok().put("data", view); // 返回用户信息 + }else { + return R.error(511,"查不到数据"); // 返回错误信息 + } + } + + + //用户退出 + //@param request HTTP请求对象 + //@return 操作结果 + @GetMapping(value = "logout") + public R logout(HttpServletRequest request) { + request.getSession().invalidate(); // 使会话失效 + return R.ok("退出成功"); // 返回成功信息 + } + + + //前端分页列表 + //@param params 请求参数 + //@param request HTTP请求对象 + //@return 分页结果 + @IgnoreAuth // 忽略认证 + @RequestMapping("/list") + public R list(@RequestParam Map params, HttpServletRequest request){ + logger.debug("list方法:,,Controller:{},,params:{}",this.getClass().getName(),JSONObject.toJSONString(params)); + CommonUtil.checkMap(params); // 检查参数 + PageUtils page = yonghuService.queryPage(params); // 分页查询 + + // 字典表数据转换 + List list =(List)page.getList(); // 获取分页数据 + for(YonghuView c:list) // 遍历数据 + dictionaryService.dictionaryConvert(c, request); // 转换字典字段 + + return R.ok().put("data", page); // 返回分页结果 + } + + + //前端详情 + //@param id 用户ID + //@param request HTTP请求对象 + //@return 用户详情 + @RequestMapping("/detail/{id}") + public R detail(@PathVariable("id") Integer id, HttpServletRequest request){ + logger.debug("detail方法:,,Controller:{},,id:{}",this.getClass().getName(),id); + YonghuEntity yonghu = yonghuService.selectById(id); // 根据ID查询用户 + if(yonghu !=null){ // 如果用户存在 + YonghuView view = new YonghuView(); // 创建视图对象 + BeanUtils.copyProperties(yonghu, view); // 复制属性 + dictionaryService.dictionaryConvert(view, request); // 转换字典字段 + return R.ok().put("data", view); // 返回用户信息 + }else { + return R.error(511,"查不到数据"); // 返回错误信息 + } + } + + + //前端保存用户 + //@param yonghu 用户实体 + //@param request HTTP请求对象 + //@return 操作结果 + @RequestMapping("/add") + public R add(@RequestBody YonghuEntity yonghu, HttpServletRequest request){ + logger.debug("add方法:,,Controller:{},,yonghu:{}",this.getClass().getName(),yonghu.toString()); + // 构建查询条件:检查用户名、手机号、身份证号是否已存在 + Wrapper queryWrapper = new EntityWrapper() + .eq("username", yonghu.getUsername()) // 用户名条件 + .or() // 或条件 + .eq("yonghu_phone", yonghu.getYonghuPhone()) // 手机号条件 + .or() // 或条件 + .eq("yonghu_id_number", yonghu.getYonghuIdNumber()) // 身份证号条件 + .andNew() // 新条件组 + .eq("data_delete", 1) // 未删除条件 + ; + logger.info("sql语句:"+queryWrapper.getSqlSegment()); // 记录SQL语句 + YonghuEntity yonghuEntity = yonghuService.selectOne(queryWrapper); // 查询是否存在 + if(yonghuEntity==null){ // 如果不存在 + yonghu.setDataDelete(1); // 设置未删除状态 + yonghu.setInsertTime(new Date()); // 设置插入时间 + yonghu.setCreateTime(new Date()); // 设置创建时间 + yonghu.setPassword("123456"); // 设置默认密码 + yonghuService.insert(yonghu); // 插入新用户 + return R.ok(); // 返回成功结果 + }else { + return R.error(511,"账户或者用户手机号或者用户身份证号已经被使用"); // 返回错误信息 + } + } +} \ No newline at end of file From 15e2bd75fce36baf98f35c0fea403bc7309c8535 Mon Sep 17 00:00:00 2001 From: phfsut2ie Date: Mon, 28 Apr 2025 18:38:47 +0800 Subject: [PATCH 21/22] ADD file via upload --- vue1.js | 2464 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 2464 insertions(+) create mode 100644 vue1.js diff --git a/vue1.js b/vue1.js new file mode 100644 index 0000000..34dacbf --- /dev/null +++ b/vue1.js @@ -0,0 +1,2464 @@ +// Vue.js v2.6.12 版本声明 +// 版权所有 (c) 2014 - 2020 Evan You +// 基于 MIT 许可证发布 +// 立即执行函数表达式,用于将 Vue 库暴露给不同的模块系统 +// 参数 global 代表全局对象,factory 是一个函数,用于创建 Vue 实例 +(function (global, factory) { + // 判断是否为 CommonJS 模块环境 + // 如果 exports 是对象且 module 不是 undefined,则使用 module.exports 导出 factory 函数的返回值 + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + // 判断是否为 AMD 模块环境 + // 如果 define 是函数且支持 AMD 规范,则使用 define 函数定义模块 + typeof define === 'function' && define.amd ? define(factory) : + // 既不是 CommonJS 也不是 AMD 环境,则将 factory 函数的返回值赋值给全局对象的 Vue 属性 + (global = global || self, global.Vue = factory()); + // 传入当前全局对象 this 和 factory 函数 +}(this, function () { 'use strict'; + + // 创建一个冻结的空对象,用于避免不必要的对象创建和修改 + var emptyObject = Object.freeze({}); + + // 这些辅助函数由于其明确性和函数内联,在 JS 引擎中能产生更好的虚拟机代码 + // 检查一个值是否为 undefined 或 null + function isUndef (v) { + return v === undefined || v === null + } + + // 检查一个值是否既不是 undefined 也不是 null + function isDef (v) { + return v !== undefined && v !== null + } + + // 检查一个值是否严格等于 true + function isTrue (v) { + return v === true + } + + // 检查一个值是否严格等于 false + function isFalse (v) { + return v === false + } + + // 检查值是否为原始类型 + function isPrimitive (value) { + // 检查值是否为字符串类型 + return typeof value === 'string' || + // 检查值是否为数字类型 + typeof value === 'number' || + // 忽略 Flow 类型检查,检查值是否为符号类型 + typeof value === 'symbol' || + // 检查值是否为布尔类型 + typeof value === 'boolean' + } + + // 快速检查对象 - 当我们知道值是符合 JSON 类型时,主要用于区分对象和原始值 + function isObject (obj) { + // 检查对象是否不为 null 且类型为 object + return obj !== null && typeof obj === 'object' + } + + // 获取值的原始类型字符串,例如 [object Object] + var _toString = Object.prototype.toString; + + function toRawType (value) { + // 调用 Object.prototype.toString 方法获取值的类型字符串,并截取关键部分 + return _toString.call(value).slice(8, -1) + } + + // 严格的对象类型检查。仅当为纯 JavaScript 对象时返回 true + function isPlainObject (obj) { + // 检查对象的类型字符串是否为 [object Object] + return _toString.call(obj) === '[object Object]' + } + + // 检查一个值是否为正则表达式对象 + function isRegExp (v) { + return _toString.call(v) === '[object RegExp]' + } + + // 检查 val 是否为有效的数组索引 + function isValidArrayIndex (val) { + // 将值转换为浮点数 + var n = parseFloat(String(val)); + // 检查是否为非负整数且为有限值 + return n >= 0 && Math.floor(n) === n && isFinite(val) + } + + // 检查一个值是否为 Promise 对象 + function isPromise (val) { + // 检查值是否已定义 + return isDef(val) && + // 检查值是否有 then 方法 + typeof val.then === 'function' && + // 检查值是否有 catch 方法 + typeof val.catch === 'function' + } + + // 将值转换为实际渲染的字符串 + function toString (val) { + // 如果值为 null 或 undefined,返回空字符串 + return val == null ? '' : + // 检查值是否为数组或纯对象且 toString 方法为默认方法 + Array.isArray(val) || (isPlainObject(val) && val.toString === _toString) ? + // 如果是,使用 JSON.stringify 转换为格式化的字符串 + JSON.stringify(val, null, 2) : + // 否则,直接转换为字符串 + String(val) + } + + // 将输入值转换为数字以便持久化。如果转换失败,返回原始字符串 + function toNumber (val) { + // 将值转换为浮点数 + var n = parseFloat(val); + // 检查是否为 NaN,如果是则返回原始值,否则返回转换后的数字 + return isNaN(n) ? val : n + } + + // 创建一个映射并返回一个函数,用于检查键是否在该映射中 + function makeMap ( + str, + expectsLowerCase + ) { + // 创建一个空对象作为映射 + var map = Object.create(null); + // 将字符串按逗号分割成数组 + var list = str.split(','); + // 遍历数组,将每个元素作为键添加到映射中 + for (var i = 0; i < list.length; i++) { + map[list[i]] = true; + } + // 如果期望小写,则返回一个函数,将输入转换为小写后检查是否在映射中 + return expectsLowerCase ? function (val) { return map[val.toLowerCase()]; } : + // 否则,直接检查输入是否在映射中 + function (val) { return map[val]; } + } + + // 检查标签是否为内置标签 + var isBuiltInTag = makeMap('slot,component', true); + + // 检查属性是否为保留属性 + var isReservedAttribute = makeMap('key,ref,slot,slot-scope,is'); + + // 从数组中移除一个元素 + function remove (arr, item) { + // 检查数组长度是否大于 0 + if (arr.length) { + // 查找元素在数组中的索引 + var index = arr.indexOf(item); + // 如果索引大于 -1,表示元素存在于数组中 + if (index > -1) { + // 使用 splice 方法移除元素并返回被移除的元素数组 + return arr.splice(index, 1) + } + } + } + + // 检查对象是否具有某个属性 + var hasOwnProperty = Object.prototype.hasOwnProperty; + function hasOwn (obj, key) { + // 使用 hasOwnProperty 方法检查对象是否具有指定的键 + return hasOwnProperty.call(obj, key) + } + + // 创建一个纯函数的缓存版本 + function cached (fn) { + // 创建一个空对象作为缓存 + var cache = Object.create(null); + return function cachedFn (str) { + // 检查缓存中是否已有该键的结果 + var hit = cache[str]; + // 如果有,返回缓存结果;否则,调用原函数计算结果并缓存 + return hit || (cache[str] = fn(str)) + } + } + + // 将连字符分隔的字符串转换为驼峰式字符串 + var camelizeRE = /-(\w)/g; + var camelize = cached(function (str) { + // 使用正则表达式替换连字符后的字符为大写 + return str.replace(camelizeRE, function (_, c) { return c ? c.toUpperCase() : ''; }) + }); + + // 首字母大写字符串 + var capitalize = cached(function (str) { + // 将字符串的首字母转换为大写并拼接剩余部分 + return str.charAt(0).toUpperCase() + str.slice(1) + }); + + // 将驼峰式字符串转换为连字符分隔的字符串 + var hyphenateRE = /\B([A-Z])/g; + var hyphenate = cached(function (str) { + // 使用正则表达式替换大写字母前插入连字符并转换为小写 + return str.replace(hyphenateRE, '-$1').toLowerCase() + }); + + // 为不支持 bind 方法的环境提供简单的 bind 方法填充 + // 例如 PhantomJS 1.x。从技术上讲,由于现在大多数浏览器中本地 bind 方法性能已经足够好,我们不再需要这个方法 + // 但移除它会导致在 PhantomJS 1.x 中运行的代码中断,因此为了向后兼容必须保留 + function polyfillBind (fn, ctx) { + function boundFn (a) { + // 获取参数长度 + var l = arguments.length; + return l ? + // 如果有参数 + l > 1 ? + // 如果参数数量大于 1,使用 apply 方法调用原函数 + fn.apply(ctx, arguments) : + // 如果参数数量为 1,使用 call 方法调用原函数 + fn.call(ctx, a) : + // 如果没有参数,使用 call 方法调用原函数 + fn.call(ctx) + } + + // 绑定函数的长度设置为原函数的长度 + boundFn._length = fn.length; + return boundFn + } + + // 使用原生的 bind 方法 + function nativeBind (fn, ctx) { + return fn.bind(ctx) + } + + // 如果 Function.prototype.bind 存在,则使用原生 bind 方法,否则使用填充方法 + var bind = Function.prototype.bind ? nativeBind : polyfillBind; + + // 将类似数组的对象转换为真正的数组 + function toArray (list, start) { + // 如果 start 未提供,默认为 0 + start = start || 0; + // 计算新数组的长度 + var i = list.length - start; + // 创建一个指定长度的新数组 + var ret = new Array(i); + // 从后往前遍历类似数组的对象,将元素复制到新数组中 + while (i--) { + ret[i] = list[i + start]; + } + return ret + } + + // 将属性混合到目标对象中 + function extend (to, _from) { + // 遍历源对象的所有属性 + for (var key in _from) { + // 将源对象的属性复制到目标对象中 + to[key] = _from[key]; + } + return to + } + + // 将对象数组合并为一个对象 + function toObject (arr) { + // 创建一个空对象作为结果 + var res = {}; + // 遍历数组中的每个对象 + for (var i = 0; i < arr.length; i++) { + // 如果对象存在 + if (arr[i]) { + // 将对象的属性合并到结果对象中 + extend(res, arr[i]); + } + } + return res + } + + // 不执行任何操作。 + // 为了让 Flow 满意而保留参数,同时避免留下无用的转译代码 + // (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/) + function noop (a, b, c) {} + + // 始终返回 false + var no = function (a, b, c) { return false; }; + + // 返回相同的值 + var identity = function (_) { return _; }; + + // 从编译器模块生成包含静态键的字符串 + function genStaticKeys (modules) { + // 使用 reduce 方法将所有模块的静态键合并为一个数组 + return modules.reduce(function (keys, m) { + return keys.concat(m.staticKeys || []) + }, []).join(',') + } + + // 检查两个值是否宽松相等 - 即,如果它们是纯对象,它们的结构是否相同? + function looseEqual (a, b) { + // 如果两个值严格相等,直接返回 true + if (a === b) { return true } + // 检查 a 是否为对象 + var isObjectA = isObject(a); + // 检查 b 是否为对象 + var isObjectB = isObject(b); + if (isObjectA && isObjectB) { + try { + // 检查 a 是否为数组 + var isArrayA = Array.isArray(a); + // 检查 b 是否为数组 + var isArrayB = Array.isArray(b); + if (isArrayA && isArrayB) { + // 如果都是数组,检查长度是否相等且每个元素是否宽松相等 + return a.length === b.length && a.every(function (e, i) { + return looseEqual(e, b[i]) + }) + } else if (a instanceof Date && b instanceof Date) { + // 如果都是日期对象,检查时间戳是否相等 + return a.getTime() === b.getTime() + } else if (!isArrayA && !isArrayB) { + // 如果都不是数组,检查键的数量是否相等且每个键对应的值是否宽松相等 + var keysA = Object.keys(a); + var keysB = Object.keys(b); + return keysA.length === keysB.length && keysA.every(function (key) { + return looseEqual(a[key], b[key]) + }) + } else { + return false + } + } catch (e) { + return false + } + } else if (!isObjectA && !isObjectB) { + // 如果都不是对象,将它们转换为字符串并比较 + return String(a) === String(b) + } else { + return false + } + } + + // 返回在数组中可以找到宽松相等值的第一个索引(如果值是纯对象,数组必须包含相同结构的对象),如果不存在则返回 -1 + function looseIndexOf (arr, val) { + // 遍历数组 + for (var i = 0; i < arr.length; i++) { + // 检查当前元素是否与目标值宽松相等 + if (looseEqual(arr[i], val)) { return i } + } + return -1 + } + + // 确保函数只被调用一次 + function once (fn) { + // 标记函数是否已被调用 + var called = false; + return function () { + // 如果函数未被调用 + if (!called) { + // 标记为已调用 + called = true; + // 调用原函数并传递参数 + fn.apply(this, arguments); + } + } + } + + // 服务器渲染标记属性 + var SSR_ATTR = 'data-server-rendered'; + + // 资产类型数组,包括组件、指令和过滤器 + var ASSET_TYPES = [ + 'component', + 'directive', + 'filter' + ]; + + // 生命周期钩子数组 + var LIFECYCLE_HOOKS = [ + 'beforeCreate', + 'created', + 'beforeMount', + 'mounted', + 'beforeUpdate', + 'updated', + 'beforeDestroy', + 'destroyed', + 'activated', + 'deactivated', + 'errorCaptured', + 'serverPrefetch' + ]; + + // Vue 配置对象 + var config = ({ + // 选项合并策略(在 core/util/options 中使用) + // 忽略 Flow 类型检查 + optionMergeStrategies: Object.create(null), + // 是否抑制警告 + silent: false, + // 在启动时显示生产模式提示消息? + productionTip: "development" !== 'production', + // 是否启用开发者工具 + devtools: "development" !== 'production', + // 是否记录性能 + performance: false, + // 监听器错误的错误处理函数 + errorHandler: null, + // 监听器警告的警告处理函数 + warnHandler: null, + // 忽略某些自定义元素 + ignoredElements: [], + // v-on 的自定义用户键别名 + // 忽略 Flow 类型检查 + keyCodes: Object.create(null), + // 检查标签是否为保留标签,以便不能将其注册为组件。这依赖于平台,可能会被覆盖 + isReservedTag: no, + // 检查属性是否为保留属性,以便不能将其用作组件的 prop。这依赖于平台,可能会被覆盖 + isReservedAttr: no, + // 检查标签是否为未知元素。依赖于平台 + isUnknownElement: no, + // 获取元素的命名空间 + getTagNamespace: noop, + // 解析特定平台的真实标签名 + parsePlatformTagName: identity, + // 检查属性是否必须使用属性绑定,例如 value。依赖于平台 + mustUseProp: no, + // 异步执行更新。供 Vue Test Utils 使用。如果设置为 false,性能将显著降低 + async: true, + // 出于遗留原因暴露 + _lifecycleHooks: LIFECYCLE_HOOKS + }); + + // 用于解析 HTML 标签、组件名称和属性路径的 Unicode 字母。 + // 使用 https://www.w3.org/TR/html53/semantics-scripting.html#potentialcustomelementname + // 跳过 \u10000 - \uEFFFF 以避免 PhantomJS 冻结 + var unicodeRegExp = /a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/; + + // 检查字符串是否以 $ 或 _ 开头 + function isReserved (str) { + // 获取字符串的第一个字符的 Unicode 编码 + var c = (str + '').charCodeAt(0); + // 检查编码是否为 $ 或 _ 的编码 + return c === 0x24 || c === 0x5F + } + + // 定义一个属性 + function def (obj, key, val, enumerable) { + // 使用 Object.defineProperty 方法定义属性 + Object.defineProperty(obj, key, { + // 属性的值 + value: val, + // 是否可枚举 + enumerable: !!enumerable, + // 是否可写 + writable: true, + // 是否可配置 + configurable: true + }); + } + + // 解析简单路径 + var bailRE = new RegExp(("[^" + (unicodeRegExp.source) + ".$_\\d]")); + function parsePath (path) { + // 检查路径是否包含非法字符 + if (bailRE.test(path)) { + return + } + // 将路径按点分割成段 + var segments = path.split('.'); + return function (obj) { + // 遍历路径段 + for (var i = 0; i < segments.length; i++) { + // 如果对象为空,返回 + if (!obj) { return } + // 获取对象的下一级属性 + obj = obj[segments[i]]; + } + return obj + } + } + // 能否使用 __proto__ 属性? + var hasProto = '__proto__' in {}; + +// 浏览器环境嗅探 +// 判断是否处于浏览器环境 + var inBrowser = typeof window !== 'undefined'; +// 判断是否处于 Weex 环境 + var inWeex = typeof WXEnvironment !== 'undefined' && !!WXEnvironment.platform; +// 获取 Weex 平台名称并转换为小写 + var weexPlatform = inWeex && WXEnvironment.platform.toLowerCase(); +// 获取浏览器的用户代理字符串并转换为小写 + var UA = inBrowser && window.navigator.userAgent.toLowerCase(); +// 判断是否为 IE 浏览器 + var isIE = UA && /msie|trident/.test(UA); +// 判断是否为 IE 9 浏览器 + var isIE9 = UA && UA.indexOf('msie 9.0') > 0; +// 判断是否为 Edge 浏览器 + var isEdge = UA && UA.indexOf('edge/') > 0; +// 判断是否为安卓设备 + var isAndroid = (UA && UA.indexOf('android') > 0) || (weexPlatform === 'android'); +// 判断是否为 iOS 设备 + var isIOS = (UA && /iphone|ipad|ipod|ios/.test(UA)) || (weexPlatform === 'ios'); +// 判断是否为 Chrome 浏览器 + var isChrome = UA && /chrome\/\d+/.test(UA) && !isEdge; +// 判断是否为 PhantomJS 环境 + var isPhantomJS = UA && /phantomjs/.test(UA); +// 判断是否为 Firefox 浏览器,并获取版本号 + var isFF = UA && UA.match(/firefox\/(\d+)/); + +// Firefox 在 Object.prototype 上有 "watch" 函数 + var nativeWatch = ({}).watch; + +// 检测浏览器是否支持 passive 选项 + var supportsPassive = false; + if (inBrowser) { + try { + // 创建一个空对象用于测试 + var opts = {}; + // 定义一个属性 "passive",当访问该属性时将 supportsPassive 设为 true + Object.defineProperty(opts, 'passive', ({ + get: function get () { + supportsPassive = true; + } + })); + // 尝试添加一个测试事件监听器,触发对 "passive" 属性的访问 + window.addEventListener('test-passive', null, opts); + } catch (e) {} + } + +// 这个变量需要延迟求值,因为 vue 可能在 vue-server-renderer 设置 VUE_ENV 之前被引入 + var _isServer; +// 检查是否处于服务器渲染环境 + var isServerRendering = function () { + if (_isServer === undefined) { + if (!inBrowser && !inWeex && typeof global !== 'undefined') { + // 检测是否存在 vue-server-renderer 并避免 Webpack 对 process 进行填充 + _isServer = global['process'] && global['process'].env.VUE_ENV === 'server'; + } else { + _isServer = false; + } + } + return _isServer + }; + +// 检测开发者工具是否可用 + var devtools = inBrowser && window.__VUE_DEVTOOLS_GLOBAL_HOOK__; + +// 判断一个构造函数是否为原生函数 + function isNative (Ctor) { + return typeof Ctor === 'function' && /native code/.test(Ctor.toString()) + } + +// 检测是否支持 Symbol 和 Reflect.ownKeys + var hasSymbol = + typeof Symbol !== 'undefined' && isNative(Symbol) && + typeof Reflect !== 'undefined' && isNative(Reflect.ownKeys); + +// 定义一个 Set 类型变量 + var _Set; +// 如果原生支持 Set,则使用原生 Set + if (typeof Set !== 'undefined' && isNative(Set)) { + _Set = Set; + } else { + // 否则使用自定义的 Set 填充 + _Set = (function () { + function Set () { + // 创建一个空对象用于存储键 + this.set = Object.create(null); + } + // 检查集合中是否存在某个键 + Set.prototype.has = function has (key) { + return this.set[key] === true + }; + // 向集合中添加一个键 + Set.prototype.add = function add (key) { + this.set[key] = true; + }; + // 清空集合 + Set.prototype.clear = function clear () { + this.set = Object.create(null); + }; + + return Set; + }()); + } + +// 定义警告函数,初始为空函数 + var warn = noop; +// 定义提示函数,初始为空函数 + var tip = noop; +// 定义生成组件调用栈的函数,初始为空函数 + var generateComponentTrace = (noop); +// 定义格式化组件名称的函数,初始为空函数 + var formatComponentName = (noop); + + { + // 检查是否存在 console 对象 + var hasConsole = typeof console !== 'undefined'; + // 用于匹配连字符或下划线后的字符 + var classifyRE = /(?:^|[-_])(\w)/g; + // 将字符串转换为驼峰式命名 + var classify = function (str) { return str + .replace(classifyRE, function (c) { return c.toUpperCase(); }) + .replace(/[-_]/g, ''); }; + + // 定义警告函数 + warn = function (msg, vm) { + // 获取组件调用栈 + var trace = vm ? generateComponentTrace(vm) : ''; + + if (config.warnHandler) { + // 如果配置了警告处理函数,则调用该函数 + config.warnHandler.call(null, msg, vm, trace); + } else if (hasConsole && (!config.silent)) { + // 否则在控制台输出警告信息 + console.error(("[Vue warn]: " + msg + trace)); + } + }; + + // 定义提示函数 + tip = function (msg, vm) { + if (hasConsole && (!config.silent)) { + // 在控制台输出提示信息 + console.warn("[Vue tip]: " + msg + ( + vm ? generateComponentTrace(vm) : '' + )); + } + }; + + // 格式化组件名称 + formatComponentName = function (vm, includeFile) { + // 检查当前组件实例是否为根组件实例 + if (vm.$root === vm) { + // 如果是根组件实例,返回 '' 字符串 + return '' + } + // 根据传入的 vm 类型和属性获取组件的选项对象 + var options = typeof vm === 'function' && vm.cid != null + // 如果 vm 是函数且有 cid 属性,说明是组件构造函数,取其 options 属性 + ? vm.options + // 如果 vm 是 Vue 实例,取其 $options 属性,如果没有则取构造函数的 options 属性 + : vm._isVue + ? vm.$options || vm.constructor.options + // 否则直接将 vm 作为选项对象 + : vm; + // 从选项对象中获取组件的名称,如果没有则取 _componentTag 属性 + var name = options.name || options._componentTag; + // 从选项对象中获取组件对应的文件路径 + var file = options.__file; + // 如果组件名称为空且存在文件路径 + if (!name && file) { + // 使用正则表达式匹配文件路径中的文件名部分 + var match = file.match(/([^/\\]+)\.vue$/); + // 如果匹配成功,将匹配到的文件名赋值给 name + name = match && match[1]; + } + + // 生成格式化后的组件名称字符串 + return ( + // 如果有组件名称,将其转换为驼峰命名并包裹在 < > 中,否则返回 '' + (name ? ("<" + (classify(name)) + ">") : "") + + // 如果存在文件路径且 includeFile 不为 false,添加文件路径信息 + (file && includeFile !== false ? (" at " + file) : '') + ) + }; + +// 重复字符串指定次数 + var repeat = function (str, n) { + // 初始化结果字符串为空 + var res = ''; + // 开始循环,直到 n 变为 0 + while (n) { + // 如果 n 是奇数,将当前字符串 str 追加到结果字符串 res 中 + if (n % 2 === 1) { res += str; } + // 如果 n 大于 1,将字符串 str 自身拼接,相当于将字符串长度翻倍 + if (n > 1) { str += str; } + // 将 n 右移一位,相当于将 n 除以 2 并向下取整 + n >>= 1; + } + // 返回重复后的字符串 + return res + }; + // 生成组件调用栈信息 + // 重新定义 generateComponentTrace 函数,用于生成组件调用链的追踪信息 + generateComponentTrace = function (vm) { + // 检查传入的 vm 是否为 Vue 实例,并且是否存在父组件 + if (vm._isVue && vm.$parent) { + // 初始化一个数组 tree,用于存储组件调用链中的每个组件实例 + var tree = []; + // 初始化一个变量 currentRecursiveSequence,用于记录当前组件的递归调用次数 + var currentRecursiveSequence = 0; + // 开始循环遍历组件的父组件链,直到没有父组件为止 + while (vm) { + // 检查 tree 数组中是否已经有组件实例 + if (tree.length > 0) { + // 获取 tree 数组中的最后一个组件实例 + var last = tree[tree.length - 1]; + // 检查最后一个组件实例的构造函数是否和当前组件实例的构造函数相同 + if (last.constructor === vm.constructor) { + // 如果相同,说明是递归调用,递归次数加 1 + currentRecursiveSequence++; + // 将 vm 指向其父组件,继续循环 + vm = vm.$parent; + // 跳过本次循环的后续代码,继续下一次循环 + continue + } + // 如果递归次数大于 0,说明之前有递归调用 + else if (currentRecursiveSequence > 0) { + // 将 tree 数组的最后一个元素替换为一个数组,包含最后一个组件实例和递归次数 + tree[tree.length - 1] = [last, currentRecursiveSequence]; + // 重置递归次数为 0 + currentRecursiveSequence = 0; + } + } + // 将当前组件实例添加到 tree 数组中 + tree.push(vm); + // 将 vm 指向其父组件,继续循环 + vm = vm.$parent; + } + // 生成组件调用链的追踪信息字符串 + return '\n\nfound in\n\n' + tree + // 对 tree 数组中的每个元素进行映射处理 + .map(function (vm, i) { + // 根据索引 i 判断是否为第一个元素 + return ("" + (i === 0 ? '---> ' : repeat(' ', 5 + i * 2)) + + // 检查当前元素是否为数组 + (Array.isArray(vm) + // 如果是数组,说明有递归调用,格式化输出组件名和递归次数 + ? ((formatComponentName(vm[0])) + "... (" + (vm[1]) + " recursive calls)") + // 如果不是数组,直接格式化输出组件名 + : formatComponentName(vm))); + }) + // 将映射后的数组元素用换行符连接成一个字符串 + .join('\n') + } + // 如果传入的 vm 不是 Vue 实例或者没有父组件 + else { + // 直接生成包含组件名的追踪信息字符串 + return ("\n\n(found in " + (formatComponentName(vm)) + ")") + } + }; + +// 定义唯一标识符 + var uid = 0; + +// Dep 类,用于实现依赖收集和发布更新 + var Dep = function Dep () { + // 为每个 Dep 实例分配一个唯一的 ID + this.id = uid++; + // 存储订阅者的数组 + this.subs = []; + }; + +// 向订阅者列表中添加一个订阅者 + Dep.prototype.addSub = function addSub (sub) { + this.subs.push(sub); + }; + +// 从订阅者列表中移除一个订阅者 + Dep.prototype.removeSub = function removeSub (sub) { + remove(this.subs, sub); + }; + +// 触发依赖收集 + Dep.prototype.depend = function depend () { + if (Dep.target) { + Dep.target.addDep(this); + } + }; + +// 通知所有订阅者更新 + Dep.prototype.notify = function notify () { + // 先复制一份订阅者列表,避免在更新过程中修改原列表 + var subs = this.subs.slice(); + if (!config.async) { + // 如果不是异步模式,对订阅者列表进行排序 + subs.sort(function (a, b) { return a.id - b.id; }); + } + for (var i = 0, l = subs.length; i < l; i++) { + // 调用每个订阅者的 update 方法 + subs[i].update(); + } + }; + +// 当前正在计算的目标观察者 + Dep.target = null; +// 存储目标观察者的栈 + var targetStack = []; + +// 将目标观察者压入栈中,并设置为当前目标 + function pushTarget (target) { + targetStack.push(target); + Dep.target = target; + } + +// 从栈中弹出目标观察者,并更新当前目标 + function popTarget () { + targetStack.pop(); + Dep.target = targetStack[targetStack.length - 1]; + } + +// VNode 类,用于表示虚拟 DOM 节点 + // 定义 VNode 构造函数,用于创建虚拟 DOM 节点对象 + var VNode = function VNode ( + // 节点的标签名,例如 'div'、'span' 等 + tag, + // 节点的数据对象,包含节点的属性、事件等信息 + data, + // 节点的子节点数组 + children, + // 节点的文本内容 + text, + // 与虚拟节点对应的真实 DOM 元素 + elm, + // 节点所在的上下文,通常是 Vue 实例 + context, + // 组件的选项对象,包含组件的配置信息 + componentOptions, + // 异步组件的工厂函数 + asyncFactory + ) { + // 将传入的标签名赋值给实例的 tag 属性 + this.tag = tag; + // 将传入的数据对象赋值给实例的 data 属性 + this.data = data; + // 将传入的子节点数组赋值给实例的 children 属性 + this.children = children; + // 将传入的文本内容赋值给实例的 text 属性 + this.text = text; + // 将传入的真实 DOM 元素赋值给实例的 elm 属性 + this.elm = elm; + // 命名空间,初始化为 undefined + this.ns = undefined; + // 将传入的上下文赋值给实例的 context 属性 + this.context = context; + // 函数式组件的上下文,初始化为 undefined + this.fnContext = undefined; + // 函数式组件的选项,初始化为 undefined + this.fnOptions = undefined; + // 函数式组件的作用域 ID,初始化为 undefined + this.fnScopeId = undefined; + // 如果 data 存在且包含 key 属性,则将其赋值给实例的 key 属性,否则为 undefined + this.key = data && data.key; + // 将传入的组件选项对象赋值给实例的 componentOptions 属性 + this.componentOptions = componentOptions; + // 组件实例,初始化为 undefined + this.componentInstance = undefined; + // 父节点,初始化为 undefined + this.parent = undefined; + // 标记节点是否为原始的、未处理的节点,初始化为 false + this.raw = false; + // 标记节点是否为静态节点,初始化为 false + this.isStatic = false; + // 标记节点是否是根插入节点,初始化为 true + this.isRootInsert = true; + // 标记节点是否为注释节点,初始化为 false + this.isComment = false; + // 标记节点是否为克隆节点,初始化为 false + this.isCloned = false; + // 标记节点是否只渲染一次,初始化为 false + this.isOnce = false; + // 将传入的异步组件工厂函数赋值给实例的 asyncFactory 属性 + this.asyncFactory = asyncFactory; + // 异步组件的元数据,初始化为 undefined + this.asyncMeta = undefined; + // 标记节点是否为异步占位符节点,初始化为 false + this.isAsyncPlaceholder = false; + }; + +// 定义 VNode 原型上的访问器属性 + var prototypeAccessors = { child: { configurable: true } }; + +// 废弃:为了向后兼容,将 child 作为 componentInstance 的别名 + prototypeAccessors.child.get = function () { + return this.componentInstance + }; + +// 将访问器属性定义到 VNode 原型上 + Object.defineProperties( VNode.prototype, prototypeAccessors ); + +// 创建一个空的虚拟节点 + var createEmptyVNode = function (text) { + if ( text === void 0 ) text = ''; + + var node = new VNode(); + node.text = text; + node.isComment = true; + return node + }; + +// 创建一个文本虚拟节点 + function createTextVNode (val) { + return new VNode(undefined, undefined, undefined, String(val)) + } + +// 浅克隆一个虚拟节点 + // 定义一个函数 cloneVNode,用于克隆一个虚拟节点(VNode) + function cloneVNode (vnode) { + // 创建一个新的 VNode 实例,将原 VNode 的部分属性传递给新实例 + var cloned = new VNode( + // 克隆原 VNode 的标签名 + vnode.tag, + // 克隆原 VNode 的数据对象 + vnode.data, + // 克隆原 VNode 的子节点数组,如果存在子节点,则创建一个新的数组副本 + vnode.children && vnode.children.slice(), + // 克隆原 VNode 的文本内容 + vnode.text, + // 克隆原 VNode 对应的真实 DOM 元素 + vnode.elm, + // 克隆原 VNode 的上下文 + vnode.context, + // 克隆原 VNode 的组件选项对象 + vnode.componentOptions, + // 克隆原 VNode 的异步组件工厂函数 + vnode.asyncFactory + ); + // 将原 VNode 的命名空间赋值给克隆节点 + cloned.ns = vnode.ns; + // 将原 VNode 是否为静态节点的标记赋值给克隆节点 + cloned.isStatic = vnode.isStatic; + // 将原 VNode 的 key 值赋值给克隆节点 + cloned.key = vnode.key; + // 将原 VNode 是否为注释节点的标记赋值给克隆节点 + cloned.isComment = vnode.isComment; + // 将原 VNode 的函数式组件上下文赋值给克隆节点 + cloned.fnContext = vnode.fnContext; + // 将原 VNode 的函数式组件选项赋值给克隆节点 + cloned.fnOptions = vnode.fnOptions; + // 将原 VNode 的函数式组件作用域 ID 赋值给克隆节点 + cloned.fnScopeId = vnode.fnScopeId; + // 将原 VNode 的异步组件元数据赋值给克隆节点 + cloned.asyncMeta = vnode.asyncMeta; + // 标记克隆节点为已克隆状态 + cloned.isCloned = true; + // 返回克隆后的 VNode 节点 + return cloned + } + +// 获取数组的原型 + var arrayProto = Array.prototype; +// 创建一个继承自数组原型的对象 + var arrayMethods = Object.create(arrayProto); + +// 定义需要拦截的数组方法 + var methodsToPatch = [ + 'push', + 'pop', + 'shift', + 'unshift', + 'splice', + 'sort', + 'reverse' + ]; + +// 拦截数组的变异方法并触发更新 + methodsToPatch.forEach(function (method) { + // 缓存原始方法 + var original = arrayProto[method]; + // 定义变异方法 + // 调用 def 函数,为 arrayMethods 对象定义一个新的方法,该方法名由 method 变量指定 +// 第三个参数是一个名为 mutator 的函数,它会覆盖原数组方法的行为 + def(arrayMethods, method, function mutator () { + // 初始化一个空数组 args,用于存储传递给当前函数的参数 + var args = [], len = arguments.length; + // 使用 while 循环将传递给函数的参数逆序存储到 args 数组中 + while ( len-- ) args[ len ] = arguments[ len ]; + + // 调用原始的数组方法(original 是之前缓存的数组原型上的原始方法) + // 使用 apply 方法将当前数组实例作为 this 上下文,并传递参数 args + var result = original.apply(this, args); + // 获取当前数组实例的 Observer 实例 + // __ob__ 是在对象被观测时添加的一个属性,指向该对象的 Observer 实例 + var ob = this.__ob__; + // 定义一个变量 inserted,用于存储新插入到数组中的元素 + var inserted; + // 根据不同的数组方法进行不同的处理 + switch (method) { + // 如果是 push 或 unshift 方法 + case 'push': + case 'unshift': + // 将传递给 push 或 unshift 方法的所有参数赋值给 inserted 变量 + // 因为 push 和 unshift 方法会将参数添加到数组中,这些参数就是新插入的元素 + inserted = args; + // 跳出 switch 语句 + break + // 如果是 splice 方法 + case 'splice': + // splice 方法的参数格式为 (start, deleteCount, item1, item2, ...) + // 从索引 2 开始截取参数,这些参数就是 splice 方法插入到数组中的新元素 + inserted = args.slice(2); + // 跳出 switch 语句 + break + } + if (inserted) { ob.observeArray(inserted); } + // 通知依赖更新 + ob.dep.notify(); + return result + }); + }); + +// 获取拦截后的数组方法的属性名 + var arrayKeys = Object.getOwnPropertyNames(arrayMethods); + +// 是否应该进行观测 + var shouldObserve = true; + +// 切换观测状态 + function toggleObserving (value) { + shouldObserve = value; + } + +// Observer 类,用于将对象转换为可观测对象 + var booleanIndex = getTypeIndex(Boolean, prop.type); + // 如果属性类型包含布尔类型 + if (booleanIndex > -1) { + // 如果属性不存在且没有默认值,则将值设为false + if (absent &&!hasOwn(prop, 'default')) { + value = false; + } else if (value === '' || value === hyphenate(key)) { + // 只有当布尔类型的优先级高于字符串类型时,才将空字符串或同名的值转换为布尔类型 + var Observer = function Observer (value) { + this.value = value; + this.dep = new Dep(); + this.vmCount = 0; + // 在对象上定义一个不可枚举的属性 __ob__,指向当前 Observer 实例 + def(value, '__ob__', this); + if (Array.isArray(value)) { + if (hasProto) { + // 使用 __proto__ 进行原型增强 + protoAugment(value, arrayMethods); + } else { + // 复制属性进行增强 + copyAugment(value, arrayMethods, arrayKeys); + } + // 对数组元素进行观测 + this.observeArray(value); + } else { + // 遍历对象属性并转换为响应式 + this.walk(value); + } + }; + +// 遍历对象的所有属性,将其转换为 getter/setter + Observer.prototype.walk = function walk (obj) { + var keys = Object.keys(obj); + for (var i = 0; i < keys.length; i++) { + defineReactive$$1(obj, keys[i]); + } + }; + +// 对数组中的每个元素进行观测 + Observer.prototype.observeArray = function observeArray (items) { + for (var i = 0, l = items.length; i < l; i++) { + observe(items[i]); + } + }; + +// 使用 __proto__ 增强目标对象或数组 + function protoAugment (target, src) { + target.__proto__ = src; + } + +// 通过定义隐藏属性增强目标对象或数组 + function copyAugment (target, src, keys) { + for (var i = 0, l = keys.length; i < l; i++) { + var key = keys[i]; + def(target, key, src[key]); + } + } + +// 尝试为一个值创建观察者实例 + function observe (value, asRootData) { + // 检查传入的值是否不是对象,或者是否为 VNode 实例 + // 如果满足条件,则直接返回,不进行观察 + if (!isObject(value) || value instanceof VNode) { + return + } + // 声明一个变量 ob,用于存储观察者实例 + var ob; + // 检查传入的值是否已经有 __ob__ 属性,并且该属性对应的是 Observer 实例 + if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { + // 如果已经有观察者实例,则直接将其赋值给 ob 变量 + ob = value.__ob__; + } + // 如果值还没有被观察,并且满足以下条件 + else if ( + // 全局变量 shouldObserve 为 true,表示应该进行观察 + shouldObserve && + // 当前不是服务器渲染环境 + !isServerRendering() && + // 传入的值是数组或者是普通对象 + (Array.isArray(value) || isPlainObject(value)) && + // 传入的值是可扩展的,即可以添加新的属性 + Object.isExtensible(value) && + // 传入的值不是 Vue 实例 + !value._isVue + ) { + // 创建一个新的 Observer 实例,并将其赋值给 ob 变量 + ob = new Observer(value); + } + // 如果 asRootData 为 true,并且已经成功创建了观察者实例 + if (asRootData && ob) { + // 增加观察者实例的 vmCount 属性,该属性用于记录该对象作为根数据的次数 + ob.vmCount++; + } + // 返回观察者实例,如果没有创建则返回 undefined + return ob + } + // Define a reactive property on an Object. +// 在一个对象上定义一个响应式属性 + function defineReactive$$1 ( + // 要定义响应式属性的对象 + obj, + // 属性名 + key, + // 属性的初始值 + val, + // 自定义的 setter 函数 + customSetter, + // 是否进行浅观察 + shallow + ) { + // 创建一个 Dep 实例,用于依赖收集和派发更新 + var dep = new Dep(); + + // 获取对象上该属性的描述符 + var property = Object.getOwnPropertyDescriptor(obj, key); + // 如果属性存在且不可配置,则直接返回 + if (property && property.configurable === false) { + return + } + + // cater for pre-defined getter/setters + // 处理预定义的 getter/setter + // 获取属性的 getter 函数 + var getter = property && property.get; + // 获取属性的 setter 函数 + var setter = property && property.set; + // 如果没有 getter 或者有 setter,并且只传入了两个参数,则获取对象上该属性的值 + if ((!getter || setter) && arguments.length === 2) { + val = obj[key]; + } + + // 如果不是浅观察,则尝试为属性值创建观察者实例 + var childOb = !shallow && observe(val); + // 使用 Object.defineProperty 定义属性的 getter 和 setter + Object.defineProperty(obj, key, { + // 属性可枚举 + enumerable: true, + // 属性可配置 + configurable: true, + // 定义 getter 函数 + get: function reactiveGetter () { + // 如果有预定义的 getter,则调用该 getter 获取值,否则使用传入的初始值 + var value = getter ? getter.call(obj) : val; + // 如果存在 Dep.target(即有正在计算的依赖) + if (Dep.target) { + // 收集依赖 + dep.depend(); + // 如果存在子观察者 + if (childOb) { + // 子观察者收集依赖 + childOb.dep.depend(); + // 如果值是数组 + if (Array.isArray(value)) { + // 对数组中的每个元素收集依赖 + dependArray(value); + } + } + } + // 返回属性值 + return value + }, + // 定义 setter 函数 + set: function reactiveSetter (newVal) { + // 如果有预定义的 getter,则调用该 getter 获取旧值,否则使用传入的初始值 + var value = getter ? getter.call(obj) : val; + // 检查新值和旧值是否相等,如果相等则直接返回 + /* eslint-disable no-self-compare */ + if (newVal === value || (newVal !== newVal && value !== value)) { + return + } + /* eslint-enable no-self-compare */ + // 如果有自定义的 setter 函数,则调用该函数 + if (customSetter) { + customSetter(); + } + // 如果有 getter 但没有 setter,则直接返回 + // #7981: for accessor properties without setter + if (getter && !setter) { return } + // 如果有预定义的 setter,则调用该 setter 设置新值 + if (setter) { + setter.call(obj, newVal); + } else { + // 否则直接更新值 + val = newVal; + } + // 如果不是浅观察,则尝试为新值创建观察者实例 + childOb = !shallow && observe(newVal); + // 通知所有依赖更新 + dep.notify(); + } + }); + } + +// Set a property on an object. Adds the new property and +// triggers change notification if the property doesn't +// already exist. +// 在对象上设置一个属性。如果属性不存在,则添加该属性并触发变更通知 + function set ( + // 目标对象 + target, + // 属性名 + key, + // 属性值 + val + ) { + // 如果目标对象为 undefined、null 或者是原始类型,则发出警告 + if (isUndef(target) || isPrimitive(target) + ) { + warn(("Cannot set reactive property on undefined, null, or primitive value: " + ((target)))); + } + // 如果目标对象是数组且键是有效的数组索引 + if (Array.isArray(target) && isValidArrayIndex(key)) { + // 确保数组长度足够 + target.length = Math.max(target.length, key); + // 使用 splice 方法替换数组中的元素 + target.splice(key, 1, val); + // 返回设置的值 + return val + } + // 如果键已经存在于目标对象中且不是原型链上的属性 + if (key in target && !(key in Object.prototype)) { + // 直接设置属性值 + target[key] = val; + // 返回设置的值 + return val + } + // 获取目标对象的观察者实例 + var ob = (target).__ob__; + // 如果目标对象是 Vue 实例或者是根数据对象,则发出警告 + if (target._isVue || (ob && ob.vmCount)) { + warn( + 'Avoid adding reactive properties to a Vue instance or its root $data ' + + 'at runtime - declare it upfront in the data option.' + ); + // 返回设置的值 + return val + } + // 如果没有观察者实例 + if (!ob) { + // 直接设置属性值 + target[key] = val; + // 返回设置的值 + return val + } + // 为目标对象的属性定义响应式 + defineReactive$$1(ob.value, key, val); + // 通知依赖更新 + ob.dep.notify(); + // 返回设置的值 + return val + } + +// Delete a property and trigger change if necessary. +// 删除一个属性,并在必要时触发变更通知 + function del ( + // 目标对象 + target, + // 属性名 + key + ) { + // 如果目标对象为 undefined、null 或者是原始类型,则发出警告 + if (isUndef(target) || isPrimitive(target) + ) { + warn(("Cannot delete reactive property on undefined, null, or primitive value: " + ((target)))); + } + // 如果目标对象是数组且键是有效的数组索引 + if (Array.isArray(target) && isValidArrayIndex(key)) { + // 使用 splice 方法删除数组中的元素 + target.splice(key, 1); + return + } + // 获取目标对象的观察者实例 + var ob = (target).__ob__; + // 如果目标对象是 Vue 实例或者是根数据对象,则发出警告 + if (target._isVue || (ob && ob.vmCount)) { + warn( + 'Avoid deleting properties on a Vue instance or its root $data ' + + '- just set it to null.' + ); + return + } + // 如果目标对象没有该属性,则直接返回 + if (!hasOwn(target, key)) { + return + } + // 删除目标对象的属性 + delete target[key]; + // 如果没有观察者实例,则直接返回 + if (!ob) { + return + } + // 通知依赖更新 + ob.dep.notify(); + } + +// Collect dependencies on array elements when the array is touched, since +// we cannot intercept array element access like property getters. +// 当数组被访问时,收集数组元素的依赖,因为我们无法像拦截属性 getter 那样拦截数组元素的访问 + function dependArray ( + // 数组 + value + ) { + // 遍历数组 + for (var e = (void 0), i = 0, l = value.length; i < l; i++) { + // 获取数组元素 + e = value[i]; + // 如果元素存在且有观察者实例,则收集该元素的依赖 + e && e.__ob__ && e.__ob__.dep.depend(); + // 如果元素是数组 + if (Array.isArray(e)) { + // 递归收集数组元素的依赖 + dependArray(e); + } + } + } + +// Option overwriting strategies are functions that handle +// how to merge a parent option value and a child option +// value into the final value. +// 选项覆盖策略是处理如何将父选项值和子选项值合并为最终值的函数 + var strats = config.optionMergeStrategies; + +// Options with restrictions +// 有约束的选项 + { + // 定义 el 和 propsData 选项的合并策略 + strats.el = strats.propsData = function ( + // 父选项值 + parent, + // 子选项值 + child, + // Vue 实例 + vm, + // 选项名 + key + ) { + // 如果没有 Vue 实例,则发出警告 + if (!vm) { + warn( + "option \"" + key + "\" can only be used during instance " + + 'creation with the `new` keyword.' + ); + } + // 使用默认的合并策略 + return defaultStrat(parent, child) + }; + } + +// Helper that recursively merges two data objects together. +// 递归合并两个数据对象的辅助函数 + function mergeData ( + // 目标对象 + to, + // 源对象 + from + ) { + // 如果源对象不存在,则返回目标对象 + if (!from) { return to } + // 定义变量,用于存储键、目标对象的值和源对象的值 + var key, toVal, fromVal; + + // 获取源对象的所有键 + var keys = hasSymbol + ? Reflect.ownKeys(from) + : Object.keys(from); + + // 遍历源对象的所有键 + for (var i = 0; i < keys.length; i++) { + // 获取当前键 + key = keys[i]; + // 如果键是 __ob__,则跳过 + // in case the object is already observed... + if (key === '__ob__') { continue } + // 获取目标对象上该键的值 + toVal = to[key]; + // 获取源对象上该键的值 + fromVal = from[key]; + // 如果目标对象没有该键 + if (!hasOwn(to, key)) { + // 在目标对象上设置该键的值 + set(to, key, fromVal); + } else if ( + // 如果目标对象和源对象上该键的值不相等 + toVal !== fromVal && + // 目标对象上该键的值是纯对象 + isPlainObject(toVal) && + // 源对象上该键的值是纯对象 + isPlainObject(fromVal) + ) { + // 递归合并两个对象 + mergeData(toVal, fromVal); + } + } + // 返回合并后的目标对象 + return to + } + +// Data +// 数据合并函数 + function mergeDataOrFn ( + // 父数据值 + parentVal, + // 子数据值 + childVal, + // Vue 实例 + vm + ) { + // 如果没有 Vue 实例 + if (!vm) { + // 在 Vue.extend 合并时,两者都应该是函数 + // 如果子数据值不存在,则返回父数据值 + if (!childVal) { + return parentVal + } + // 如果父数据值不存在,则返回子数据值 + if (!parentVal) { + return childVal + } + // 当父数据值和子数据值都存在时,我们需要返回一个函数,该函数返回两个函数合并后的结果 + // 这里不需要检查父数据值是否是函数,因为它必须是函数才能通过之前的合并 + return function mergedDataFn () { + return mergeData( + // 如果子数据值是函数,则调用该函数并传入 this 上下文,否则直接使用子数据值 + typeof childVal === 'function' ? childVal.call(this, this) : childVal, + // 如果父数据值是函数,则调用该函数并传入 this 上下文,否则直接使用父数据值 + typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal + ) + } + } else { + return function mergedInstanceDataFn () { + // 实例合并 + // 如果子数据值是函数,则调用该函数并传入 Vue 实例,否则直接使用子数据值 + var instanceData = typeof childVal === 'function' + ? childVal.call(vm, vm) + : childVal; + // 如果父数据值是函数,则调用该函数并传入 Vue 实例,否则直接使用父数据值 + var defaultData = typeof parentVal === 'function' + ? parentVal.call(vm, vm) + : parentVal; + // 如果实例数据存在 + if (instanceData) { + // 合并实例数据和默认数据 + return mergeData(instanceData, defaultData) + } else { + // 否则返回默认数据 + return defaultData + } + } + } + } + +// 定义 data 选项的合并策略 + strats.data = function ( + // 父选项值 + parentVal, + // 子选项值 + childVal, + // Vue 实例 + vm + ) { + // 如果没有 Vue 实例 + if (!vm) { + // 如果子选项值存在且不是函数,则发出警告 + if (childVal && typeof childVal !== 'function') { + warn( + 'The "data" option should be a function ' + + 'that returns a per-instance value in component ' + + 'definitions.', + vm + ); + + return parentVal + } + // 使用 mergeDataOrFn 函数进行合并 + return mergeDataOrFn(parentVal, childVal) + } + + // 使用 mergeDataOrFn 函数进行合并,并传入 Vue 实例 + return mergeDataOrFn(parentVal, childVal, vm) + }; + +// Hooks and props are merged as arrays. +// 钩子和 props 以数组形式合并 + function mergeHook ( + // 父选项值 + parentVal, + // 子选项值 + childVal + ) { + // 初始化合并结果 + var res = childVal + ? parentVal + // 如果父选项值存在,则将子选项值合并到父选项值中 + ? parentVal.concat(childVal) + // 如果父选项值不存在,且子选项值是数组,则直接使用子选项值 + : Array.isArray(childVal) + ? childVal + // 如果父选项值不存在,且子选项值不是数组,则将子选项值包装成数组 + : [childVal] + // 如果子选项值不存在,则使用父选项值 + : parentVal; + // 对合并结果进行去重处理 + return res + ? dedupeHooks(res) + : res + } + +// 对钩子数组进行去重处理 + function dedupeHooks ( + // 钩子数组 + hooks + ) { + // 初始化去重后的数组 + var res = []; + // 遍历钩子数组 + for (var i = 0; i < hooks.length; i++) { + // 如果去重后的数组中不包含当前钩子,则将其添加到去重后的数组中 + if (res.indexOf(hooks[i]) === -1) { + res.push(hooks[i]); + } + } + // 返回去重后的数组 + return res + } + +// 遍历生命周期钩子数组 + LIFECYCLE_HOOKS.forEach(function (hook) { + // 为每个生命周期钩子定义合并策略 + strats[hook] = mergeHook; + }); + +// Assets +// 当存在 Vue 实例(实例创建时),我们需要对构造函数选项、实例选项和父选项进行三方合并 +// 资产(组件、指令、过滤器)的合并函数 + function mergeAssets ( + // 父选项值 + parentVal, + // 子选项值 + childVal, + // Vue 实例 + vm, + // 选项名 + key + ) { + // 创建一个继承自父选项值的对象 + var res = Object.create(parentVal || null); + // 如果子选项值存在 + if (childVal) { + // 检查子选项值是否为对象类型 + assertObjectType(key, childVal, vm); + // 将子选项值合并到结果对象中 + return extend(res, childVal) + } else { + // 如果子选项值不存在,则返回结果对象 + return res + } + } + +// 遍历资产类型数组 + ASSET_TYPES.forEach(function (type) { + // 为每个资产类型定义合并策略 + strats[type + 's'] = mergeAssets; + }); + +// Watchers. +// 监听器的合并策略 +// Watchers hashes should not overwrite one +// another, so we merge them as arrays. +// 监听器哈希不应该相互覆盖,所以我们将它们合并为数组 + strats.watch = function ( + // 父选项值 + parentVal, + // 子选项值 + childVal, + // Vue 实例 + vm, + // 选项名 + key + ) { + // work around Firefox's Object.prototype.watch... + // 处理 Firefox 的 Object.prototype.watch + if (parentVal === nativeWatch) { parentVal = undefined; } + if (childVal === nativeWatch) { childVal = undefined; } + /* istanbul ignore if */ + // 如果子选项值不存在,则返回一个继承自父选项值的对象 + if (!childVal) { return Object.create(parentVal || null) } + { + // 检查子选项值是否为对象类型 + assertObjectType(key, childVal, vm); + } + // 如果父选项值不存在,则返回子选项值 + if (!parentVal) { return childVal } + // 初始化合并结果 + var ret = {}; + // 将父选项值合并到结果对象中 + extend(ret, parentVal); + // 遍历子选项值的所有键 + for (var key$1 in childVal) { + // 获取父选项值中该键的值 + var parent = ret[key$1]; + // 获取子选项值中该键的值 + var child = childVal[key$1]; + // 如果父选项值中该键的值存在且不是数组,则将其包装成数组 + if (parent && !Array.isArray(parent)) { + parent = [parent]; + } + // 将子选项值中该键的值合并到父选项值中 + ret[key$1] = parent + ? parent.concat(child) + : Array.isArray(child) ? child : [child]; + } + // 返回合并结果 + return ret + }; + +// Other object hashes. +// 其他对象哈希的合并策略 + strats.props = + strats.methods = + strats.inject = + strats.computed = function ( + // 父选项值 + parentVal, + // 子选项值 + childVal, + // Vue 实例 + vm, + // 选项名 + key + ) { + // 如果子选项值存在且不是生产环境,则检查子选项值是否为对象类型 + if (childVal && "development" !== 'production') { + assertObjectType(key, childVal, vm); + } + // 如果父选项值不存在,则返回子选项值 + if (!parentVal) { return childVal } + // 初始化合并结果 + var ret = Object.create(null); + // 将父选项值合并到结果对象中 + extend(ret, parentVal); + // 如果子选项值存在,则将其合并到结果对象中 + if (childVal) { extend(ret, childVal); } + // 返回合并结果 + return ret + }; +// provide 选项的合并策略使用 mergeDataOrFn 函数 + strats.provide = mergeDataOrFn; + +// Default strategy. +// 默认合并策略 + var defaultStrat = function ( + // 父选项值 + parentVal, + // 子选项值 + childVal + ) { + // 如果子选项值为 undefined,则返回父选项值,否则返回子选项值 + return childVal === undefined + ? parentVal + : childVal + }; + +// Validate component names +// 验证组件名称 + function checkComponents ( + // 选项对象 + options + ) { + // 遍历选项对象中的 components 属性 + for (var key in options.components) { + // 验证组件名称 + validateComponentName(key); + } + } + +// 验证组件名称是否合法 + function validateComponentName ( + // 组件名称 + name + ) { + // 如果组件名称不符合正则表达式的规则,则发出警告 + if (!new RegExp(("^[a-zA-Z][\\-\\.0-9_" + (unicodeRegExp.source) + "]*$")).test(name)) { + warn( + 'Invalid component name: "' + name + '". Component names ' + + 'should conform to valid custom element name in html5 specification.' + ); + } + // 如果组件名称是内置标签或者是保留标签,则发出警告 + if (isBuiltInTag(name) || config.isReservedTag(name)) { + warn( + 'Do not use built-in or reserved HTML elements as component ' + + 'id: ' + name + ); + } + } + +// Ensure all props option syntax are normalized into the +// Object-based format. +// 确保所有 props 选项的语法都被规范化为基于对象的格式 + function normalizeProps ( + // 选项对象 + options, + // Vue 实例 + vm + ) { + // 获取选项对象中的 props 属性 + var props = options.props; + // 如果 props 属性不存在,则直接返回 + if (!props) { return } + // 初始化规范化后的 props 对象 + var res = {}; + // 定义变量,用于存储索引、值和名称 + var i, val, name; + // 如果 props 属性是数组 + if (Array.isArray(props)) { + // 获取数组的长度 + i = props.length; + // 从后往前遍历数组 + while (i--) { + // 获取当前元素 + val = props[i]; + // 如果当前元素是字符串 + if (typeof val === 'string') { + // 将字符串转换为驼峰命名 + name = camelize(val); + // 在规范化后的 props 对象中添加该属性,并设置类型为 null + res[name] = { type: null }; + } else { + // 如果当前元素不是字符串,则发出警告 + warn('props must be strings when using array syntax.'); + } + } + } else if (isPlainObject(props)) { + // 如果 props 属性是纯对象 + // 遍历对象的所有键 + for (var key in props) { + // 获取当前键对应的值 + val = props[key]; + // 将键转换为驼峰命名 + name = camelize(key); + // 如果值是纯对象,则直接使用该值,否则将其包装成一个包含类型的对象 + res[name] = isPlainObject(val) + ? val + : { type: val }; + } + } else { + // 如果 props 属性既不是数组也不是纯对象,则发出警告 + warn( + "Invalid value for option \"props\": expected an Array or an Object, " + + "but got " + (toRawType(props)) + ".", + vm + ); + } + // 将规范化后的 props 对象赋值给选项对象的 props 属性 + options.props = res; + } + +// Normalize all injections into Object-based format +// 将所有注入项规范化为基于对象的格式 + function normalizeInject ( + // 选项对象 + options, + // Vue 实例 + vm + ) { + // 获取选项对象中的 inject 属性 + var inject = options.inject; + // 如果 inject 属性不存在,则直接返回 + if (!inject) { return } + // 初始化规范化后的 inject 对象 + var normalized = options.inject = {}; + // 如果 inject 属性是数组 + if (Array.isArray(inject)) { + // 遍历数组 + for (var i = 0; i < inject.length; i++) { + // 在规范化后的 inject 对象中添加该属性,并设置 from 属性为当前元素 + normalized[inject[i]] = { from: inject[i] }; + } + } else if (isPlainObject(inject)) { + // 如果 inject 属性是纯对象 + // 遍历对象的所有键 + for (var key in inject) { + // 获取当前键对应的值 + var val = inject[key]; + // 如果值是纯对象,则将 from 属性合并到该值中,否则将其包装成一个包含 from 属性的对象 + normalized[key] = isPlainObject(val) + ? extend({ from: key }, val) + : { from: val }; + } + } else { + // 如果 inject 属性既不是数组也不是纯对象,则发出警告 + warn( + "Invalid value for option \"inject\": expected an Array or an Object, " + + "but got " + (toRawType(inject)) + ".", + vm + ); + } + } + +// Normalize raw function directives into object format. +// 将原始的函数指令规范化为对象格式 + function normalizeDirectives ( + // 选项对象 + options + ) { + // 获取选项对象中的 directives 属性 + var dirs = options.directives; + // 如果 directives 属性存在 + if (dirs) { + // 遍历 directives 对象的所有键 + for (var key in dirs) { + // 获取当前键对应的值 + var def$$1 = dirs[key]; + // 如果值是函数 + if (typeof def$$1 === 'function') { + // 将其包装成一个包含 bind 和 update 方法的对象 + dirs[key] = { bind: def$$1, update: def$$1 }; + } + } + } + } + +// 检查值是否为纯对象,如果不是则发出警告 + function assertObjectType ( + // 选项名 + name, + // 值 + value, + // Vue 实例 + vm + ) { + // 如果值不是纯对象,则发出警告 + if (!isPlainObject(value)) { + warn( + "Invalid value for option \"" + name + "\": expected an Object, " + + "but got " + (toRawType(value)) + ".", + vm + ); + } + } + +// Merge two option objects into a new one. +// Core utility used in both instantiation and inheritance. +// 将两个选项对象合并为一个新的对象 +// 实例化和继承中使用的核心工具 + function mergeOptions ( + // 父选项对象 + parent, + // 子选项对象 + child, + // Vue 实例 + vm + ) { + { + // 检查子选项对象中的组件名称是否合法 + checkComponents(child); + } + + // 如果子选项对象是一个函数,则获取其 options 属性 + if (typeof child === 'function') { + child = child.options; + } + + // 规范化子选项对象中的 props 属性 + normalizeProps(child, vm); + // 规范化子选项对象中的 inject 属性 + normalizeInject(child, vm); + // 规范化子选项对象中的 directives 属性 + normalizeDirectives(child); + + // Apply extends and mixins on the child options, + // but only if it is a raw options object that isn't + // the result of another mergeOptions call. + // Only merged options has the _base property. + // 如果子选项对象不是另一次 mergeOptions 调用的结果(即没有 _base 属性) + if (!child._base) { + // 如果子选项对象有 extends 属性 + if (child.extends) { + // 递归合并父选项对象和子选项对象的 extends 属性 + parent = mergeOptions(parent, child.extends, vm); + } + // 如果子选项对象有 mixins 属性 + if (child.mixins) { + // 遍历 mixins 数组 + for (var i = 0, l = child.mixins.length; i < l; i++) { + // 递归合并父选项对象和 mixins 数组中的每个选项对象 + parent = mergeOptions(parent, child.mixins[i], vm); + } + } + } + // 定义一个空对象options + var options = {}; +// 定义变量key + var key; +// 遍历parent对象的属性 + for (key in parent) { + // 调用mergeField函数处理属性 + mergeField(key); + } +// 遍历child对象的属性 + for (key in child) { + // 如果parent对象不包含当前属性 + if (!hasOwn(parent, key)) { + // 调用mergeField函数处理属性 + mergeField(key); + } + } +// 定义mergeField函数,用于合并属性 + function mergeField (key) { + // 获取属性的合并策略,如果没有则使用默认策略 + var strat = strats[key] || defaultStrat; + // 根据合并策略合并parent和child中对应属性的值,并将结果存储在options中 + options[key] = strat(parent[key], child[key], vm, key); + } +// 返回合并后的options对象 + return options + } + //解析一个资源。 + // 这个函数被使用是因为子实例需要访问在其祖先链中定义的资源。 +// 定义resolveAsset函数,用于解析资源 + function resolveAsset ( + options, + type, + id, + warnMissing + ) { + // 如果id不是字符串类型,则返回 + /* istanbul ignore if */ + if (typeof id!== 'string') { + return + } + // 获取指定类型的资源 + var assets = options[type]; + // 首先检查本地注册的变体 + if (hasOwn(assets, id)) { return assets[id] } + // 将id转换为驼峰命名 + var camelizedId = camelize(id); + // 如果驼峰命名的id存在于资源中,则返回对应的值 + if (hasOwn(assets, camelizedId)) { return assets[camelizedId] } + // 将驼峰命名的id转换为首字母大写的形式 + var PascalCaseId = capitalize(camelizedId); + // 如果首字母大写的id存在于资源中,则返回对应的值 + if (hasOwn(assets, PascalCaseId)) { return assets[PascalCaseId] } + // 回退到原型链查找 + var res = assets[id] || assets[camelizedId] || assets[PascalCaseId]; + // 如果需要警告且没有找到资源,则发出警告 + if (warnMissing &&!res) { + warn( + 'Failed to resolve'+ type.slice(0, -1) + ':'+ id, + options + ); + } + // 返回找到的资源 + return res + } + + /* */ + +// 定义validateProp函数,用于验证属性 + function validateProp ( + key, + propOptions, + propsData, + vm + ) { + // 获取属性的选项 + var prop = propOptions[key]; + // 判断属性是否不存在于propsData中 + var absent =!hasOwn(propsData, key); + // 获取属性的值 + var value = propsData[key]; + // 布尔类型转换 + var stringIndex = getTypeIndex(String, prop.type); + if (stringIndex < 0 || booleanIndex < stringIndex) { + value = true; + } + } + } + // 检查默认值 + if (value === undefined) { + // 获取属性的默认值 + value = getPropDefaultValue(vm, prop, key); + // 因为默认值是一个新的副本,所以确保对其进行观察 + var prevShouldObserve = shouldObserve; + toggleObserving(true); + observe(value); + toggleObserving(prevShouldObserve); + } + { + // 断言属性是否有效 + assertProp(prop, key, value, vm, absent); + } + // 返回验证后的属性值 + return value + } + + // 获取属性的默认值。 +// 定义getPropDefaultValue函数,用于获取属性的默认值 + function getPropDefaultValue (vm, prop, key) { + // 如果属性没有默认值,则返回undefined + if (!hasOwn(prop, 'default')) { + return undefined + } + // 获取属性的默认值 + var def = prop.default; + // 对对象和数组类型的默认值进行警告检查,非工厂函数的默认值是无效的 + if (isObject(def)) { + warn( + 'Invalid default value for prop "' + key + '":'+ + 'Props with type Object/Array must use a factory function'+ + 'to return the default value.', + vm + ); + } + // 如果vm存在,且propsData中属性值为undefined,且vm._props中属性值不为undefined,则返回vm._props中的值 + if (vm && vm.$options.propsData && + vm.$options.propsData[key] === undefined && + vm._props[key]!== undefined + ) { + return vm._props[key] + } + // 如果默认值是函数且属性类型不是函数,则调用函数获取默认值,否则直接返回默认值 + return typeof def === 'function' && getType(prop.type)!== 'Function' + ? def.call(vm) + : def + } + + // 断言一个属性是否有效。 +// 定义assertProp函数,用于断言属性是否有效 + function assertProp ( + prop, + name, + value, + vm, + absent + ) { + // 如果属性是必需的且不存在,则发出警告 + if (prop.required && absent) { + warn( + 'Missing required prop: "' + name + '"', + vm + ); + return + } + // 如果属性值为null且属性不是必需的,则返回 + if (value == null &&!prop.required) { + return + } + // 获取属性的类型 + var type = prop.type; + // 初始化有效性为true,如果没有指定类型或者类型为true + var valid =!type || type === true; + // 定义预期类型数组 + var expectedTypes = []; + // 如果有指定属性类型 + if (type) { + // 如果类型不是数组,则将其转换为数组 + if (!Array.isArray(type)) { + type = [type]; + } + // 遍历类型数组,检查属性值是否符合预期类型 + for (var i = 0; i < type.length &&!valid; i++) { + var assertedType = assertType(value, type[i]); + expectedTypes.push(assertedType.expectedType || ''); + valid = assertedType.valid; + } + } + + // 如果属性值不符合预期类型,则发出警告 + if (!valid) { + warn( + getInvalidTypeMessage(name, value, expectedTypes), + vm + ); + return + } + // 获取属性的验证函数 + var validator = prop.validator; + // 如果有验证函数且属性值不通过验证,则发出警告 + if (validator) { + if (!validator(value)) { + warn( + 'Invalid prop: custom validator check failed for prop "' + name + '".', + vm + ); + } + } + } + +// 定义简单类型检查正则表达式 + var simpleCheckRE = /^(String|Number|Boolean|Function|Symbol)$/; + +// 定义assertType函数,用于检查属性值的类型是否符合预期 + function assertType (value, type) { + // 初始化有效性为false + var valid; + // 获取预期类型 + var expectedType = getType(type); + // 如果预期类型是简单类型 + if (simpleCheckRE.test(expectedType)) { + // 获取属性值的类型 + var t = typeof value; + // 检查属性值类型是否与预期类型一致(对于基本包装对象,还需要检查是否是预期类型的实例) + valid = t === expectedType.toLowerCase(); + if (!valid && t === 'object') { + valid = value instanceof type; + } + } else if (expectedType === 'Object') { + // 如果预期类型是对象,则检查属性值是否是普通对象 + valid = isPlainObject(value); + } else if (expectedType === 'Array') { + // 如果预期类型是数组,则检查属性值是否是数组 + valid = Array.isArray(value); + } else { + // 否则检查属性值是否是预期类型的实例 + valid = value instanceof type; + } + // 返回检查结果 + return { + valid: valid, + expectedType: expectedType + } + } + //使用函数的字符串名称来检查内置类型, + // 因为简单的相等性检查在不同的vm/iframe中运行时会失败。 +// 定义getType函数,用于获取函数的类型名称 + function getType (fn) { + // 匹配函数的字符串表示中的函数名 + var match = fn && fn.toString().match(/^\s*function (\w+)/); + // 返回匹配到的函数名,否则返回空字符串 + return match? match[1] : '' + } + +// 定义isSameType函数,用于判断两个类型是否相同 + function isSameType (a, b) { + // 通过比较两个类型的名称是否相同来判断 + return getType(a) === getType(b) + } + +// 定义getTypeIndex函数,用于获取类型在预期类型数组中的索引 + function getTypeIndex (type, expectedTypes) { + // 如果预期类型不是数组,则直接比较类型是否相同并返回索引 + if (!Array.isArray(expectedTypes)) { + return isSameType(expectedTypes, type)? 0 : -1 + } + // 遍历预期类型数组,查找类型的索引 + for (var i = 0, len = expectedTypes.length; i < len; i++) { + if (isSameType(expectedTypes[i], type)) { + return i + } + } + // 如果没有找到,则返回-1 + return -1 + } + +// 定义getInvalidTypeMessage函数,用于获取无效类型的错误消息 + function getInvalidTypeMessage (name, value, expectedTypes) { + // 初始化错误消息 + var message = "Invalid prop: type check failed for prop \"" + name + "\"." + + " Expected " + (expectedTypes.map(capitalize).join(', ')); + // 获取第一个预期类型 + var expectedType = expectedTypes[0]; + // 获取属性值的实际类型 + var receivedType = toRawType(value); + // 格式化预期值 + var expectedValue = styleValue(value, expectedType); + // 格式化实际值 + var receivedValue = styleValue(value, receivedType); + // 如果只有一个预期类型且是可解释的类型,并且实际类型不是布尔类型,则在错误消息中添加预期值 + if (expectedTypes.length === 1 && + isExplicable(expectedType) && + !isBoolean(expectedType, receivedType)) { + message += " with value " + expectedValue; + } + // 在错误消息中添加实际类型 + message += ", got " + receivedType + " "; + // 如果实际类型是可解释的类型,则在错误消息中添加实际值 + if (isExplicable(receivedType)) { + message += "with value " + receivedValue + "."; + } + // 返回错误消息 + return message + } + +// 定义styleValue函数,用于格式化值的显示形式 + function styleValue (value, type) { + // 如果类型是字符串,则用双引号包裹值 + if (type === 'String') { + return ("\"" + value + "\"") + } else if (type === 'Number') { + // 如果类型是数字,则将值转换为字符串 + return ("" + (Number(value))) + } else { + // 否则直接将值转换为字符串 + return ("" + value) + } + } + +// 定义isExplicable函数,用于判断类型是否是可解释的类型(字符串、数字、布尔) + function isExplicable (value) { + // 定义可解释的类型数组 + var explicitTypes = ['string', 'number', 'boolean']; + // 检查值的类型是否在可解释的类型数组中 + return explicitTypes.some(function (elem) { return value.toLowerCase() === elem; }) + } + +// 定义isBoolean函数,用于判断参数中是否包含布尔类型 + function isBoolean () { + // 将参数转换为数组 + var args = [], len = arguments.length; + while ( len-- ) args[ len ] = arguments[ len ]; + + // 检查数组中是否有元素的类型为布尔类型 + return args.some(function (elem) { return elem.toLowerCase() === 'boolean'; }) + } + + +// 定义handleError函数,用于处理错误 + function handleError (err, vm, info) { + // 在处理错误处理程序时停用依赖跟踪,以避免可能的无限渲染。 + // 参考:https://github.com/vuejs/vuex/issues/1505 + pushTarget(); + try { + // 如果有vm实例 + if (vm) { + var cur = vm; + // 向上遍历vm的父级实例 + while ((cur = cur.$parent)) { + // 获取父级实例的errorCaptured钩子函数数组 + var hooks = cur.$options.errorCaptured; + // 如果有errorCaptured钩子函数 + if (hooks) { + // 遍历钩子函数数组 + for (var i = 0; i < hooks.length; i++) { + try { + // 调用钩子函数,并判断是否捕获了错误(返回false表示捕获) + var capture = hooks[i].call(cur, err, vm, info) === false; + if (capture) { return } + } catch (e) { + // 如果钩子函数调用时出错,则调用全局错误处理函数 + globalHandleError(e, cur, 'errorCaptured hook'); + } + } + } + } + } + // 调用全局错误处理函数 + globalHandleError(err, vm, info); + } finally { + // 恢复依赖跟踪 + popTarget(); + } + } + +// 定义invokeWithErrorHandling函数,用于在错误处理下调用处理程序 + function invokeWithErrorHandling ( + handler, + context, + args, + vm, + info + ) { + // 定义变量res用于存储处理程序的返回值 + var res; + try { + // 调用处理程序 + res = args? handler.apply(context, args) : handler.call(context); + // 如果返回值是Promise且未被处理,则添加错误处理 + if (res &&!res._isVue && isPromise(res) &&!res._handled) { + res.catch(function (e) { return handleError(e, vm, info + " (Promise/async)"); }); + // 标记Promise已被处理,避免多次捕获 + res._handled = true; + } + } catch (e) { + // 如果处理程序调用时出错,则调用错误处理函数 + handleError(e, vm, info); + } + // 返回处理程序的返回值 + return res + } + +// 定义globalHandleError函数,用于全局处理错误 + function globalHandleError (err, vm, info) { + // 如果有配置的错误处理函数 + if (config.errorHandler) { + try { + // 调用配置的错误处理函数 + return config.errorHandler.call(null, err, vm, info) + } catch (e) { + // 如果用户在处理函数中故意抛出原始错误,不重复记录 + if (e!== err) { + logError(e, null, 'config.errorHandler'); + } + } + } + // 调用logError函数记录错误 + logError(err, vm, info); + } + +// 定义logError函数,用于记录错误 + function logError (err, vm, info) { + { + // 发出警告 + warn(("Error in " + info + ": \"" + (err.toString()) + "\""), vm); + } + /* istanbul ignore else */ + // 如果在浏览器或Weex环境中且console可用,则在控制台输出错误 + if ((inBrowser || inWeex) && typeof console!== 'undefined') { + console.error(err); + } else { + // 否则抛出错误 + throw err + } + } + + +// 定义变量isUsingMicroTask表示是否使用微任务,初始值为false + var isUsingMicroTask = false; + +// 定义callbacks数组用于存储回调函数 + var callbacks = []; +// 定义变量pending表示是否有挂起的回调,初始值为false + var pending = false; + +// 定义flushCallbacks函数,用于刷新回调函数队列 + function flushCallbacks () { + // 将pending设为false + pending = false; + // 获取callbacks数组的副本 + var copies = callbacks.slice(0); + // 清空callbacks数组 + callbacks.length = 0; + // 遍历副本数组,依次调用回调函数 + for (var i = 0; i < copies.length; i++) { + copies[i](); + } + } + // 这里我们使用微任务来实现异步延迟包装器 +// 在 2.5 版本中,我们使用(宏)任务(结合微任务) +// 然而,在重绘之前立即更改状态时会有一些细微问题 +// (例如问题 #6813,out - in 过渡效果) +// 此外,在事件处理程序中使用(宏)任务会导致一些无法避免的奇怪行为 +// (例如问题 #7109、#7153、#7546、#7834、#8109) +// 所以现在我们再次在各处使用微任务 +// 这种权衡的一个主要缺点是,在某些场景下 +// 微任务的优先级过高,会在本应按顺序执行的事件之间触发 +// (例如问题 #4521、#6690,这些问题有解决办法) +// 甚至会在同一事件的冒泡过程中触发(问题 #6566) + var timerFunc; + +// nextTick 行为利用了微任务队列,可以通过原生 Promise.then 或 MutationObserver 来访问该队列 +// MutationObserver 有更广泛的支持,然而在 iOS >= 9.3.3 的 UIWebView 中,当在触摸事件处理程序中触发时,它存在严重的 bug +// 触发几次后它会完全停止工作……所以,如果原生 Promise 可用,我们将使用它 + /* istanbul ignore next, $flow-disable-line */ + if (typeof Promise!== 'undefined' && isNative(Promise)) { + // 创建一个已解决的 Promise 实例 + var p = Promise.resolve(); + // 定义 timerFunc 函数,用于在 Promise 的 then 方法中调用 flushCallbacks 函数 + timerFunc = function () { + p.then(flushCallbacks); + // 在有问题的 UIWebView 中,Promise.then 不会完全失效,但 + // 它可能会陷入一种奇怪的状态,即回调被推入微任务队列,但队列没有被刷新,直到浏览器需要执行其他工作,例如处理定时器 + // 因此,我们可以通过添加一个空定时器来“强制”刷新微任务队列 + if (isIOS) { setTimeout(noop); } + }; + // 标记正在使用微任务 + isUsingMicroTask = true; +// 如果不是 IE 浏览器,且支持 MutationObserver + } else if (!isIE && typeof MutationObserver!== 'undefined' && ( + isNative(MutationObserver) || + // PhantomJS 和 iOS 7.x + MutationObserver.toString() === '[object MutationObserverConstructor]' + )) { + // 在原生 Promise 不可用时使用 MutationObserver + // 例如 PhantomJS、iOS 7、Android 4.4 + // (问题 #6466:MutationObserver 在 IE11 中不可靠) + var counter = 1; + // 创建一个 MutationObserver 实例,当观察到变化时调用 flushCallbacks 函数 + var observer = new MutationObserver(flushCallbacks); + // 创建一个文本节点 + var textNode = document.createTextNode(String(counter)); + // 开始观察文本节点的字符数据变化 + observer.observe(textNode, { + characterData: true + }); + // 定义 timerFunc 函数,通过改变文本节点的数据来触发 MutationObserver + timerFunc = function () { + counter = (counter + 1) % 2; + textNode.data = String(counter); + }; + // 标记正在使用微任务 + isUsingMicroTask = true; +// 如果支持 setImmediate 且是原生的 + } else if (typeof setImmediate!== 'undefined' && isNative(setImmediate)) { + // 回退到使用 setImmediate + // 从技术上讲,它利用了(宏)任务队列,但它仍然比 setTimeout 是更好的选择 + timerFunc = function () { + setImmediate(flushCallbacks); + }; + } else { + // 回退到使用 setTimeout + timerFunc = function () { + setTimeout(flushCallbacks, 0); + }; + } + +// 定义 nextTick 函数,用于将回调函数添加到队列中,并在合适的时机执行 + function nextTick (cb, ctx) { + var _resolve; + // 将回调函数包装后添加到 callbacks 数组中 + callbacks.push(function () { + if (cb) { + try { + // 调用回调函数 + cb.call(ctx); + } catch (e) { + // 处理回调函数执行过程中出现的错误 + handleError(e, ctx, 'nextTick'); + } + } else if (_resolve) { + // 如果没有回调函数但有 _resolve 函数,则调用 _resolve 函数 + _resolve(ctx); + } + }); + // 如果没有待处理的任务,则标记为有待处理任务,并调用 timerFunc 函数 + if (!pending) { + pending = true; + timerFunc(); + } + // $flow-disable-line + // 如果没有提供回调函数且支持 Promise,则返回一个 Promise 对象 + if (!cb && typeof Promise!== 'undefined') { + return new Promise(function (resolve) { + _resolve = resolve; + }) + } + } + + +// 定义 mark 和 measure 变量 + var mark; + var measure; + + { + // 检查是否在浏览器环境且支持 window.performance + var perf = inBrowser && window.performance; + /* istanbul ignore if */ + if ( + perf && + perf.mark && + perf.measure && + perf.clearMarks && + perf.clearMeasures + ) { + // 定义 mark 函数,用于在性能记录中标记一个时间点 + mark = function (tag) { return perf.mark(tag); }; + // 定义 measure 函数,用于测量两个标记时间点之间的性能 + measure = function (name, startTag, endTag) { + perf.measure(name, startTag, endTag); + perf.clearMarks(startTag); + perf.clearMarks(endTag); + // perf.clearMeasures(name) + }; + } + } \ No newline at end of file From a25389c107361720d6f3ae5c1bee140f6884c5ea Mon Sep 17 00:00:00 2001 From: phfsut2ie <3342440179@qq.com> Date: Mon, 28 Apr 2025 20:13:45 +0800 Subject: [PATCH 22/22] Update vue1.js --- vue1.js | 5138 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 2674 insertions(+), 2464 deletions(-) diff --git a/vue1.js b/vue1.js index 34dacbf..77717be 100644 --- a/vue1.js +++ b/vue1.js @@ -1,2464 +1,2674 @@ -// Vue.js v2.6.12 版本声明 -// 版权所有 (c) 2014 - 2020 Evan You -// 基于 MIT 许可证发布 -// 立即执行函数表达式,用于将 Vue 库暴露给不同的模块系统 -// 参数 global 代表全局对象,factory 是一个函数,用于创建 Vue 实例 -(function (global, factory) { - // 判断是否为 CommonJS 模块环境 - // 如果 exports 是对象且 module 不是 undefined,则使用 module.exports 导出 factory 函数的返回值 - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : - // 判断是否为 AMD 模块环境 - // 如果 define 是函数且支持 AMD 规范,则使用 define 函数定义模块 - typeof define === 'function' && define.amd ? define(factory) : - // 既不是 CommonJS 也不是 AMD 环境,则将 factory 函数的返回值赋值给全局对象的 Vue 属性 - (global = global || self, global.Vue = factory()); - // 传入当前全局对象 this 和 factory 函数 -}(this, function () { 'use strict'; - - // 创建一个冻结的空对象,用于避免不必要的对象创建和修改 - var emptyObject = Object.freeze({}); - - // 这些辅助函数由于其明确性和函数内联,在 JS 引擎中能产生更好的虚拟机代码 - // 检查一个值是否为 undefined 或 null - function isUndef (v) { - return v === undefined || v === null - } - - // 检查一个值是否既不是 undefined 也不是 null - function isDef (v) { - return v !== undefined && v !== null - } - - // 检查一个值是否严格等于 true - function isTrue (v) { - return v === true - } - - // 检查一个值是否严格等于 false - function isFalse (v) { - return v === false - } - - // 检查值是否为原始类型 - function isPrimitive (value) { - // 检查值是否为字符串类型 - return typeof value === 'string' || - // 检查值是否为数字类型 - typeof value === 'number' || - // 忽略 Flow 类型检查,检查值是否为符号类型 - typeof value === 'symbol' || - // 检查值是否为布尔类型 - typeof value === 'boolean' - } - - // 快速检查对象 - 当我们知道值是符合 JSON 类型时,主要用于区分对象和原始值 - function isObject (obj) { - // 检查对象是否不为 null 且类型为 object - return obj !== null && typeof obj === 'object' - } - - // 获取值的原始类型字符串,例如 [object Object] - var _toString = Object.prototype.toString; - - function toRawType (value) { - // 调用 Object.prototype.toString 方法获取值的类型字符串,并截取关键部分 - return _toString.call(value).slice(8, -1) - } - - // 严格的对象类型检查。仅当为纯 JavaScript 对象时返回 true - function isPlainObject (obj) { - // 检查对象的类型字符串是否为 [object Object] - return _toString.call(obj) === '[object Object]' - } - - // 检查一个值是否为正则表达式对象 - function isRegExp (v) { - return _toString.call(v) === '[object RegExp]' - } - - // 检查 val 是否为有效的数组索引 - function isValidArrayIndex (val) { - // 将值转换为浮点数 - var n = parseFloat(String(val)); - // 检查是否为非负整数且为有限值 - return n >= 0 && Math.floor(n) === n && isFinite(val) - } - - // 检查一个值是否为 Promise 对象 - function isPromise (val) { - // 检查值是否已定义 - return isDef(val) && - // 检查值是否有 then 方法 - typeof val.then === 'function' && - // 检查值是否有 catch 方法 - typeof val.catch === 'function' - } - - // 将值转换为实际渲染的字符串 - function toString (val) { - // 如果值为 null 或 undefined,返回空字符串 - return val == null ? '' : - // 检查值是否为数组或纯对象且 toString 方法为默认方法 - Array.isArray(val) || (isPlainObject(val) && val.toString === _toString) ? - // 如果是,使用 JSON.stringify 转换为格式化的字符串 - JSON.stringify(val, null, 2) : - // 否则,直接转换为字符串 - String(val) - } - - // 将输入值转换为数字以便持久化。如果转换失败,返回原始字符串 - function toNumber (val) { - // 将值转换为浮点数 - var n = parseFloat(val); - // 检查是否为 NaN,如果是则返回原始值,否则返回转换后的数字 - return isNaN(n) ? val : n - } - - // 创建一个映射并返回一个函数,用于检查键是否在该映射中 - function makeMap ( - str, - expectsLowerCase - ) { - // 创建一个空对象作为映射 - var map = Object.create(null); - // 将字符串按逗号分割成数组 - var list = str.split(','); - // 遍历数组,将每个元素作为键添加到映射中 - for (var i = 0; i < list.length; i++) { - map[list[i]] = true; - } - // 如果期望小写,则返回一个函数,将输入转换为小写后检查是否在映射中 - return expectsLowerCase ? function (val) { return map[val.toLowerCase()]; } : - // 否则,直接检查输入是否在映射中 - function (val) { return map[val]; } - } - - // 检查标签是否为内置标签 - var isBuiltInTag = makeMap('slot,component', true); - - // 检查属性是否为保留属性 - var isReservedAttribute = makeMap('key,ref,slot,slot-scope,is'); - - // 从数组中移除一个元素 - function remove (arr, item) { - // 检查数组长度是否大于 0 - if (arr.length) { - // 查找元素在数组中的索引 - var index = arr.indexOf(item); - // 如果索引大于 -1,表示元素存在于数组中 - if (index > -1) { - // 使用 splice 方法移除元素并返回被移除的元素数组 - return arr.splice(index, 1) - } - } - } - - // 检查对象是否具有某个属性 - var hasOwnProperty = Object.prototype.hasOwnProperty; - function hasOwn (obj, key) { - // 使用 hasOwnProperty 方法检查对象是否具有指定的键 - return hasOwnProperty.call(obj, key) - } - - // 创建一个纯函数的缓存版本 - function cached (fn) { - // 创建一个空对象作为缓存 - var cache = Object.create(null); - return function cachedFn (str) { - // 检查缓存中是否已有该键的结果 - var hit = cache[str]; - // 如果有,返回缓存结果;否则,调用原函数计算结果并缓存 - return hit || (cache[str] = fn(str)) - } - } - - // 将连字符分隔的字符串转换为驼峰式字符串 - var camelizeRE = /-(\w)/g; - var camelize = cached(function (str) { - // 使用正则表达式替换连字符后的字符为大写 - return str.replace(camelizeRE, function (_, c) { return c ? c.toUpperCase() : ''; }) - }); - - // 首字母大写字符串 - var capitalize = cached(function (str) { - // 将字符串的首字母转换为大写并拼接剩余部分 - return str.charAt(0).toUpperCase() + str.slice(1) - }); - - // 将驼峰式字符串转换为连字符分隔的字符串 - var hyphenateRE = /\B([A-Z])/g; - var hyphenate = cached(function (str) { - // 使用正则表达式替换大写字母前插入连字符并转换为小写 - return str.replace(hyphenateRE, '-$1').toLowerCase() - }); - - // 为不支持 bind 方法的环境提供简单的 bind 方法填充 - // 例如 PhantomJS 1.x。从技术上讲,由于现在大多数浏览器中本地 bind 方法性能已经足够好,我们不再需要这个方法 - // 但移除它会导致在 PhantomJS 1.x 中运行的代码中断,因此为了向后兼容必须保留 - function polyfillBind (fn, ctx) { - function boundFn (a) { - // 获取参数长度 - var l = arguments.length; - return l ? - // 如果有参数 - l > 1 ? - // 如果参数数量大于 1,使用 apply 方法调用原函数 - fn.apply(ctx, arguments) : - // 如果参数数量为 1,使用 call 方法调用原函数 - fn.call(ctx, a) : - // 如果没有参数,使用 call 方法调用原函数 - fn.call(ctx) - } - - // 绑定函数的长度设置为原函数的长度 - boundFn._length = fn.length; - return boundFn - } - - // 使用原生的 bind 方法 - function nativeBind (fn, ctx) { - return fn.bind(ctx) - } - - // 如果 Function.prototype.bind 存在,则使用原生 bind 方法,否则使用填充方法 - var bind = Function.prototype.bind ? nativeBind : polyfillBind; - - // 将类似数组的对象转换为真正的数组 - function toArray (list, start) { - // 如果 start 未提供,默认为 0 - start = start || 0; - // 计算新数组的长度 - var i = list.length - start; - // 创建一个指定长度的新数组 - var ret = new Array(i); - // 从后往前遍历类似数组的对象,将元素复制到新数组中 - while (i--) { - ret[i] = list[i + start]; - } - return ret - } - - // 将属性混合到目标对象中 - function extend (to, _from) { - // 遍历源对象的所有属性 - for (var key in _from) { - // 将源对象的属性复制到目标对象中 - to[key] = _from[key]; - } - return to - } - - // 将对象数组合并为一个对象 - function toObject (arr) { - // 创建一个空对象作为结果 - var res = {}; - // 遍历数组中的每个对象 - for (var i = 0; i < arr.length; i++) { - // 如果对象存在 - if (arr[i]) { - // 将对象的属性合并到结果对象中 - extend(res, arr[i]); - } - } - return res - } - - // 不执行任何操作。 - // 为了让 Flow 满意而保留参数,同时避免留下无用的转译代码 - // (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/) - function noop (a, b, c) {} - - // 始终返回 false - var no = function (a, b, c) { return false; }; - - // 返回相同的值 - var identity = function (_) { return _; }; - - // 从编译器模块生成包含静态键的字符串 - function genStaticKeys (modules) { - // 使用 reduce 方法将所有模块的静态键合并为一个数组 - return modules.reduce(function (keys, m) { - return keys.concat(m.staticKeys || []) - }, []).join(',') - } - - // 检查两个值是否宽松相等 - 即,如果它们是纯对象,它们的结构是否相同? - function looseEqual (a, b) { - // 如果两个值严格相等,直接返回 true - if (a === b) { return true } - // 检查 a 是否为对象 - var isObjectA = isObject(a); - // 检查 b 是否为对象 - var isObjectB = isObject(b); - if (isObjectA && isObjectB) { - try { - // 检查 a 是否为数组 - var isArrayA = Array.isArray(a); - // 检查 b 是否为数组 - var isArrayB = Array.isArray(b); - if (isArrayA && isArrayB) { - // 如果都是数组,检查长度是否相等且每个元素是否宽松相等 - return a.length === b.length && a.every(function (e, i) { - return looseEqual(e, b[i]) - }) - } else if (a instanceof Date && b instanceof Date) { - // 如果都是日期对象,检查时间戳是否相等 - return a.getTime() === b.getTime() - } else if (!isArrayA && !isArrayB) { - // 如果都不是数组,检查键的数量是否相等且每个键对应的值是否宽松相等 - var keysA = Object.keys(a); - var keysB = Object.keys(b); - return keysA.length === keysB.length && keysA.every(function (key) { - return looseEqual(a[key], b[key]) - }) - } else { - return false - } - } catch (e) { - return false - } - } else if (!isObjectA && !isObjectB) { - // 如果都不是对象,将它们转换为字符串并比较 - return String(a) === String(b) - } else { - return false - } - } - - // 返回在数组中可以找到宽松相等值的第一个索引(如果值是纯对象,数组必须包含相同结构的对象),如果不存在则返回 -1 - function looseIndexOf (arr, val) { - // 遍历数组 - for (var i = 0; i < arr.length; i++) { - // 检查当前元素是否与目标值宽松相等 - if (looseEqual(arr[i], val)) { return i } - } - return -1 - } - - // 确保函数只被调用一次 - function once (fn) { - // 标记函数是否已被调用 - var called = false; - return function () { - // 如果函数未被调用 - if (!called) { - // 标记为已调用 - called = true; - // 调用原函数并传递参数 - fn.apply(this, arguments); - } - } - } - - // 服务器渲染标记属性 - var SSR_ATTR = 'data-server-rendered'; - - // 资产类型数组,包括组件、指令和过滤器 - var ASSET_TYPES = [ - 'component', - 'directive', - 'filter' - ]; - - // 生命周期钩子数组 - var LIFECYCLE_HOOKS = [ - 'beforeCreate', - 'created', - 'beforeMount', - 'mounted', - 'beforeUpdate', - 'updated', - 'beforeDestroy', - 'destroyed', - 'activated', - 'deactivated', - 'errorCaptured', - 'serverPrefetch' - ]; - - // Vue 配置对象 - var config = ({ - // 选项合并策略(在 core/util/options 中使用) - // 忽略 Flow 类型检查 - optionMergeStrategies: Object.create(null), - // 是否抑制警告 - silent: false, - // 在启动时显示生产模式提示消息? - productionTip: "development" !== 'production', - // 是否启用开发者工具 - devtools: "development" !== 'production', - // 是否记录性能 - performance: false, - // 监听器错误的错误处理函数 - errorHandler: null, - // 监听器警告的警告处理函数 - warnHandler: null, - // 忽略某些自定义元素 - ignoredElements: [], - // v-on 的自定义用户键别名 - // 忽略 Flow 类型检查 - keyCodes: Object.create(null), - // 检查标签是否为保留标签,以便不能将其注册为组件。这依赖于平台,可能会被覆盖 - isReservedTag: no, - // 检查属性是否为保留属性,以便不能将其用作组件的 prop。这依赖于平台,可能会被覆盖 - isReservedAttr: no, - // 检查标签是否为未知元素。依赖于平台 - isUnknownElement: no, - // 获取元素的命名空间 - getTagNamespace: noop, - // 解析特定平台的真实标签名 - parsePlatformTagName: identity, - // 检查属性是否必须使用属性绑定,例如 value。依赖于平台 - mustUseProp: no, - // 异步执行更新。供 Vue Test Utils 使用。如果设置为 false,性能将显著降低 - async: true, - // 出于遗留原因暴露 - _lifecycleHooks: LIFECYCLE_HOOKS - }); - - // 用于解析 HTML 标签、组件名称和属性路径的 Unicode 字母。 - // 使用 https://www.w3.org/TR/html53/semantics-scripting.html#potentialcustomelementname - // 跳过 \u10000 - \uEFFFF 以避免 PhantomJS 冻结 - var unicodeRegExp = /a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/; - - // 检查字符串是否以 $ 或 _ 开头 - function isReserved (str) { - // 获取字符串的第一个字符的 Unicode 编码 - var c = (str + '').charCodeAt(0); - // 检查编码是否为 $ 或 _ 的编码 - return c === 0x24 || c === 0x5F - } - - // 定义一个属性 - function def (obj, key, val, enumerable) { - // 使用 Object.defineProperty 方法定义属性 - Object.defineProperty(obj, key, { - // 属性的值 - value: val, - // 是否可枚举 - enumerable: !!enumerable, - // 是否可写 - writable: true, - // 是否可配置 - configurable: true - }); - } - - // 解析简单路径 - var bailRE = new RegExp(("[^" + (unicodeRegExp.source) + ".$_\\d]")); - function parsePath (path) { - // 检查路径是否包含非法字符 - if (bailRE.test(path)) { - return - } - // 将路径按点分割成段 - var segments = path.split('.'); - return function (obj) { - // 遍历路径段 - for (var i = 0; i < segments.length; i++) { - // 如果对象为空,返回 - if (!obj) { return } - // 获取对象的下一级属性 - obj = obj[segments[i]]; - } - return obj - } - } - // 能否使用 __proto__ 属性? - var hasProto = '__proto__' in {}; - -// 浏览器环境嗅探 -// 判断是否处于浏览器环境 - var inBrowser = typeof window !== 'undefined'; -// 判断是否处于 Weex 环境 - var inWeex = typeof WXEnvironment !== 'undefined' && !!WXEnvironment.platform; -// 获取 Weex 平台名称并转换为小写 - var weexPlatform = inWeex && WXEnvironment.platform.toLowerCase(); -// 获取浏览器的用户代理字符串并转换为小写 - var UA = inBrowser && window.navigator.userAgent.toLowerCase(); -// 判断是否为 IE 浏览器 - var isIE = UA && /msie|trident/.test(UA); -// 判断是否为 IE 9 浏览器 - var isIE9 = UA && UA.indexOf('msie 9.0') > 0; -// 判断是否为 Edge 浏览器 - var isEdge = UA && UA.indexOf('edge/') > 0; -// 判断是否为安卓设备 - var isAndroid = (UA && UA.indexOf('android') > 0) || (weexPlatform === 'android'); -// 判断是否为 iOS 设备 - var isIOS = (UA && /iphone|ipad|ipod|ios/.test(UA)) || (weexPlatform === 'ios'); -// 判断是否为 Chrome 浏览器 - var isChrome = UA && /chrome\/\d+/.test(UA) && !isEdge; -// 判断是否为 PhantomJS 环境 - var isPhantomJS = UA && /phantomjs/.test(UA); -// 判断是否为 Firefox 浏览器,并获取版本号 - var isFF = UA && UA.match(/firefox\/(\d+)/); - -// Firefox 在 Object.prototype 上有 "watch" 函数 - var nativeWatch = ({}).watch; - -// 检测浏览器是否支持 passive 选项 - var supportsPassive = false; - if (inBrowser) { - try { - // 创建一个空对象用于测试 - var opts = {}; - // 定义一个属性 "passive",当访问该属性时将 supportsPassive 设为 true - Object.defineProperty(opts, 'passive', ({ - get: function get () { - supportsPassive = true; - } - })); - // 尝试添加一个测试事件监听器,触发对 "passive" 属性的访问 - window.addEventListener('test-passive', null, opts); - } catch (e) {} - } - -// 这个变量需要延迟求值,因为 vue 可能在 vue-server-renderer 设置 VUE_ENV 之前被引入 - var _isServer; -// 检查是否处于服务器渲染环境 - var isServerRendering = function () { - if (_isServer === undefined) { - if (!inBrowser && !inWeex && typeof global !== 'undefined') { - // 检测是否存在 vue-server-renderer 并避免 Webpack 对 process 进行填充 - _isServer = global['process'] && global['process'].env.VUE_ENV === 'server'; - } else { - _isServer = false; - } - } - return _isServer - }; - -// 检测开发者工具是否可用 - var devtools = inBrowser && window.__VUE_DEVTOOLS_GLOBAL_HOOK__; - -// 判断一个构造函数是否为原生函数 - function isNative (Ctor) { - return typeof Ctor === 'function' && /native code/.test(Ctor.toString()) - } - -// 检测是否支持 Symbol 和 Reflect.ownKeys - var hasSymbol = - typeof Symbol !== 'undefined' && isNative(Symbol) && - typeof Reflect !== 'undefined' && isNative(Reflect.ownKeys); - -// 定义一个 Set 类型变量 - var _Set; -// 如果原生支持 Set,则使用原生 Set - if (typeof Set !== 'undefined' && isNative(Set)) { - _Set = Set; - } else { - // 否则使用自定义的 Set 填充 - _Set = (function () { - function Set () { - // 创建一个空对象用于存储键 - this.set = Object.create(null); - } - // 检查集合中是否存在某个键 - Set.prototype.has = function has (key) { - return this.set[key] === true - }; - // 向集合中添加一个键 - Set.prototype.add = function add (key) { - this.set[key] = true; - }; - // 清空集合 - Set.prototype.clear = function clear () { - this.set = Object.create(null); - }; - - return Set; - }()); - } - -// 定义警告函数,初始为空函数 - var warn = noop; -// 定义提示函数,初始为空函数 - var tip = noop; -// 定义生成组件调用栈的函数,初始为空函数 - var generateComponentTrace = (noop); -// 定义格式化组件名称的函数,初始为空函数 - var formatComponentName = (noop); - - { - // 检查是否存在 console 对象 - var hasConsole = typeof console !== 'undefined'; - // 用于匹配连字符或下划线后的字符 - var classifyRE = /(?:^|[-_])(\w)/g; - // 将字符串转换为驼峰式命名 - var classify = function (str) { return str - .replace(classifyRE, function (c) { return c.toUpperCase(); }) - .replace(/[-_]/g, ''); }; - - // 定义警告函数 - warn = function (msg, vm) { - // 获取组件调用栈 - var trace = vm ? generateComponentTrace(vm) : ''; - - if (config.warnHandler) { - // 如果配置了警告处理函数,则调用该函数 - config.warnHandler.call(null, msg, vm, trace); - } else if (hasConsole && (!config.silent)) { - // 否则在控制台输出警告信息 - console.error(("[Vue warn]: " + msg + trace)); - } - }; - - // 定义提示函数 - tip = function (msg, vm) { - if (hasConsole && (!config.silent)) { - // 在控制台输出提示信息 - console.warn("[Vue tip]: " + msg + ( - vm ? generateComponentTrace(vm) : '' - )); - } - }; - - // 格式化组件名称 - formatComponentName = function (vm, includeFile) { - // 检查当前组件实例是否为根组件实例 - if (vm.$root === vm) { - // 如果是根组件实例,返回 '' 字符串 - return '' - } - // 根据传入的 vm 类型和属性获取组件的选项对象 - var options = typeof vm === 'function' && vm.cid != null - // 如果 vm 是函数且有 cid 属性,说明是组件构造函数,取其 options 属性 - ? vm.options - // 如果 vm 是 Vue 实例,取其 $options 属性,如果没有则取构造函数的 options 属性 - : vm._isVue - ? vm.$options || vm.constructor.options - // 否则直接将 vm 作为选项对象 - : vm; - // 从选项对象中获取组件的名称,如果没有则取 _componentTag 属性 - var name = options.name || options._componentTag; - // 从选项对象中获取组件对应的文件路径 - var file = options.__file; - // 如果组件名称为空且存在文件路径 - if (!name && file) { - // 使用正则表达式匹配文件路径中的文件名部分 - var match = file.match(/([^/\\]+)\.vue$/); - // 如果匹配成功,将匹配到的文件名赋值给 name - name = match && match[1]; - } - - // 生成格式化后的组件名称字符串 - return ( - // 如果有组件名称,将其转换为驼峰命名并包裹在 < > 中,否则返回 '' - (name ? ("<" + (classify(name)) + ">") : "") + - // 如果存在文件路径且 includeFile 不为 false,添加文件路径信息 - (file && includeFile !== false ? (" at " + file) : '') - ) - }; - -// 重复字符串指定次数 - var repeat = function (str, n) { - // 初始化结果字符串为空 - var res = ''; - // 开始循环,直到 n 变为 0 - while (n) { - // 如果 n 是奇数,将当前字符串 str 追加到结果字符串 res 中 - if (n % 2 === 1) { res += str; } - // 如果 n 大于 1,将字符串 str 自身拼接,相当于将字符串长度翻倍 - if (n > 1) { str += str; } - // 将 n 右移一位,相当于将 n 除以 2 并向下取整 - n >>= 1; - } - // 返回重复后的字符串 - return res - }; - // 生成组件调用栈信息 - // 重新定义 generateComponentTrace 函数,用于生成组件调用链的追踪信息 - generateComponentTrace = function (vm) { - // 检查传入的 vm 是否为 Vue 实例,并且是否存在父组件 - if (vm._isVue && vm.$parent) { - // 初始化一个数组 tree,用于存储组件调用链中的每个组件实例 - var tree = []; - // 初始化一个变量 currentRecursiveSequence,用于记录当前组件的递归调用次数 - var currentRecursiveSequence = 0; - // 开始循环遍历组件的父组件链,直到没有父组件为止 - while (vm) { - // 检查 tree 数组中是否已经有组件实例 - if (tree.length > 0) { - // 获取 tree 数组中的最后一个组件实例 - var last = tree[tree.length - 1]; - // 检查最后一个组件实例的构造函数是否和当前组件实例的构造函数相同 - if (last.constructor === vm.constructor) { - // 如果相同,说明是递归调用,递归次数加 1 - currentRecursiveSequence++; - // 将 vm 指向其父组件,继续循环 - vm = vm.$parent; - // 跳过本次循环的后续代码,继续下一次循环 - continue - } - // 如果递归次数大于 0,说明之前有递归调用 - else if (currentRecursiveSequence > 0) { - // 将 tree 数组的最后一个元素替换为一个数组,包含最后一个组件实例和递归次数 - tree[tree.length - 1] = [last, currentRecursiveSequence]; - // 重置递归次数为 0 - currentRecursiveSequence = 0; - } - } - // 将当前组件实例添加到 tree 数组中 - tree.push(vm); - // 将 vm 指向其父组件,继续循环 - vm = vm.$parent; - } - // 生成组件调用链的追踪信息字符串 - return '\n\nfound in\n\n' + tree - // 对 tree 数组中的每个元素进行映射处理 - .map(function (vm, i) { - // 根据索引 i 判断是否为第一个元素 - return ("" + (i === 0 ? '---> ' : repeat(' ', 5 + i * 2)) + - // 检查当前元素是否为数组 - (Array.isArray(vm) - // 如果是数组,说明有递归调用,格式化输出组件名和递归次数 - ? ((formatComponentName(vm[0])) + "... (" + (vm[1]) + " recursive calls)") - // 如果不是数组,直接格式化输出组件名 - : formatComponentName(vm))); - }) - // 将映射后的数组元素用换行符连接成一个字符串 - .join('\n') - } - // 如果传入的 vm 不是 Vue 实例或者没有父组件 - else { - // 直接生成包含组件名的追踪信息字符串 - return ("\n\n(found in " + (formatComponentName(vm)) + ")") - } - }; - -// 定义唯一标识符 - var uid = 0; - -// Dep 类,用于实现依赖收集和发布更新 - var Dep = function Dep () { - // 为每个 Dep 实例分配一个唯一的 ID - this.id = uid++; - // 存储订阅者的数组 - this.subs = []; - }; - -// 向订阅者列表中添加一个订阅者 - Dep.prototype.addSub = function addSub (sub) { - this.subs.push(sub); - }; - -// 从订阅者列表中移除一个订阅者 - Dep.prototype.removeSub = function removeSub (sub) { - remove(this.subs, sub); - }; - -// 触发依赖收集 - Dep.prototype.depend = function depend () { - if (Dep.target) { - Dep.target.addDep(this); - } - }; - -// 通知所有订阅者更新 - Dep.prototype.notify = function notify () { - // 先复制一份订阅者列表,避免在更新过程中修改原列表 - var subs = this.subs.slice(); - if (!config.async) { - // 如果不是异步模式,对订阅者列表进行排序 - subs.sort(function (a, b) { return a.id - b.id; }); - } - for (var i = 0, l = subs.length; i < l; i++) { - // 调用每个订阅者的 update 方法 - subs[i].update(); - } - }; - -// 当前正在计算的目标观察者 - Dep.target = null; -// 存储目标观察者的栈 - var targetStack = []; - -// 将目标观察者压入栈中,并设置为当前目标 - function pushTarget (target) { - targetStack.push(target); - Dep.target = target; - } - -// 从栈中弹出目标观察者,并更新当前目标 - function popTarget () { - targetStack.pop(); - Dep.target = targetStack[targetStack.length - 1]; - } - -// VNode 类,用于表示虚拟 DOM 节点 - // 定义 VNode 构造函数,用于创建虚拟 DOM 节点对象 - var VNode = function VNode ( - // 节点的标签名,例如 'div'、'span' 等 - tag, - // 节点的数据对象,包含节点的属性、事件等信息 - data, - // 节点的子节点数组 - children, - // 节点的文本内容 - text, - // 与虚拟节点对应的真实 DOM 元素 - elm, - // 节点所在的上下文,通常是 Vue 实例 - context, - // 组件的选项对象,包含组件的配置信息 - componentOptions, - // 异步组件的工厂函数 - asyncFactory - ) { - // 将传入的标签名赋值给实例的 tag 属性 - this.tag = tag; - // 将传入的数据对象赋值给实例的 data 属性 - this.data = data; - // 将传入的子节点数组赋值给实例的 children 属性 - this.children = children; - // 将传入的文本内容赋值给实例的 text 属性 - this.text = text; - // 将传入的真实 DOM 元素赋值给实例的 elm 属性 - this.elm = elm; - // 命名空间,初始化为 undefined - this.ns = undefined; - // 将传入的上下文赋值给实例的 context 属性 - this.context = context; - // 函数式组件的上下文,初始化为 undefined - this.fnContext = undefined; - // 函数式组件的选项,初始化为 undefined - this.fnOptions = undefined; - // 函数式组件的作用域 ID,初始化为 undefined - this.fnScopeId = undefined; - // 如果 data 存在且包含 key 属性,则将其赋值给实例的 key 属性,否则为 undefined - this.key = data && data.key; - // 将传入的组件选项对象赋值给实例的 componentOptions 属性 - this.componentOptions = componentOptions; - // 组件实例,初始化为 undefined - this.componentInstance = undefined; - // 父节点,初始化为 undefined - this.parent = undefined; - // 标记节点是否为原始的、未处理的节点,初始化为 false - this.raw = false; - // 标记节点是否为静态节点,初始化为 false - this.isStatic = false; - // 标记节点是否是根插入节点,初始化为 true - this.isRootInsert = true; - // 标记节点是否为注释节点,初始化为 false - this.isComment = false; - // 标记节点是否为克隆节点,初始化为 false - this.isCloned = false; - // 标记节点是否只渲染一次,初始化为 false - this.isOnce = false; - // 将传入的异步组件工厂函数赋值给实例的 asyncFactory 属性 - this.asyncFactory = asyncFactory; - // 异步组件的元数据,初始化为 undefined - this.asyncMeta = undefined; - // 标记节点是否为异步占位符节点,初始化为 false - this.isAsyncPlaceholder = false; - }; - -// 定义 VNode 原型上的访问器属性 - var prototypeAccessors = { child: { configurable: true } }; - -// 废弃:为了向后兼容,将 child 作为 componentInstance 的别名 - prototypeAccessors.child.get = function () { - return this.componentInstance - }; - -// 将访问器属性定义到 VNode 原型上 - Object.defineProperties( VNode.prototype, prototypeAccessors ); - -// 创建一个空的虚拟节点 - var createEmptyVNode = function (text) { - if ( text === void 0 ) text = ''; - - var node = new VNode(); - node.text = text; - node.isComment = true; - return node - }; - -// 创建一个文本虚拟节点 - function createTextVNode (val) { - return new VNode(undefined, undefined, undefined, String(val)) - } - -// 浅克隆一个虚拟节点 - // 定义一个函数 cloneVNode,用于克隆一个虚拟节点(VNode) - function cloneVNode (vnode) { - // 创建一个新的 VNode 实例,将原 VNode 的部分属性传递给新实例 - var cloned = new VNode( - // 克隆原 VNode 的标签名 - vnode.tag, - // 克隆原 VNode 的数据对象 - vnode.data, - // 克隆原 VNode 的子节点数组,如果存在子节点,则创建一个新的数组副本 - vnode.children && vnode.children.slice(), - // 克隆原 VNode 的文本内容 - vnode.text, - // 克隆原 VNode 对应的真实 DOM 元素 - vnode.elm, - // 克隆原 VNode 的上下文 - vnode.context, - // 克隆原 VNode 的组件选项对象 - vnode.componentOptions, - // 克隆原 VNode 的异步组件工厂函数 - vnode.asyncFactory - ); - // 将原 VNode 的命名空间赋值给克隆节点 - cloned.ns = vnode.ns; - // 将原 VNode 是否为静态节点的标记赋值给克隆节点 - cloned.isStatic = vnode.isStatic; - // 将原 VNode 的 key 值赋值给克隆节点 - cloned.key = vnode.key; - // 将原 VNode 是否为注释节点的标记赋值给克隆节点 - cloned.isComment = vnode.isComment; - // 将原 VNode 的函数式组件上下文赋值给克隆节点 - cloned.fnContext = vnode.fnContext; - // 将原 VNode 的函数式组件选项赋值给克隆节点 - cloned.fnOptions = vnode.fnOptions; - // 将原 VNode 的函数式组件作用域 ID 赋值给克隆节点 - cloned.fnScopeId = vnode.fnScopeId; - // 将原 VNode 的异步组件元数据赋值给克隆节点 - cloned.asyncMeta = vnode.asyncMeta; - // 标记克隆节点为已克隆状态 - cloned.isCloned = true; - // 返回克隆后的 VNode 节点 - return cloned - } - -// 获取数组的原型 - var arrayProto = Array.prototype; -// 创建一个继承自数组原型的对象 - var arrayMethods = Object.create(arrayProto); - -// 定义需要拦截的数组方法 - var methodsToPatch = [ - 'push', - 'pop', - 'shift', - 'unshift', - 'splice', - 'sort', - 'reverse' - ]; - -// 拦截数组的变异方法并触发更新 - methodsToPatch.forEach(function (method) { - // 缓存原始方法 - var original = arrayProto[method]; - // 定义变异方法 - // 调用 def 函数,为 arrayMethods 对象定义一个新的方法,该方法名由 method 变量指定 -// 第三个参数是一个名为 mutator 的函数,它会覆盖原数组方法的行为 - def(arrayMethods, method, function mutator () { - // 初始化一个空数组 args,用于存储传递给当前函数的参数 - var args = [], len = arguments.length; - // 使用 while 循环将传递给函数的参数逆序存储到 args 数组中 - while ( len-- ) args[ len ] = arguments[ len ]; - - // 调用原始的数组方法(original 是之前缓存的数组原型上的原始方法) - // 使用 apply 方法将当前数组实例作为 this 上下文,并传递参数 args - var result = original.apply(this, args); - // 获取当前数组实例的 Observer 实例 - // __ob__ 是在对象被观测时添加的一个属性,指向该对象的 Observer 实例 - var ob = this.__ob__; - // 定义一个变量 inserted,用于存储新插入到数组中的元素 - var inserted; - // 根据不同的数组方法进行不同的处理 - switch (method) { - // 如果是 push 或 unshift 方法 - case 'push': - case 'unshift': - // 将传递给 push 或 unshift 方法的所有参数赋值给 inserted 变量 - // 因为 push 和 unshift 方法会将参数添加到数组中,这些参数就是新插入的元素 - inserted = args; - // 跳出 switch 语句 - break - // 如果是 splice 方法 - case 'splice': - // splice 方法的参数格式为 (start, deleteCount, item1, item2, ...) - // 从索引 2 开始截取参数,这些参数就是 splice 方法插入到数组中的新元素 - inserted = args.slice(2); - // 跳出 switch 语句 - break - } - if (inserted) { ob.observeArray(inserted); } - // 通知依赖更新 - ob.dep.notify(); - return result - }); - }); - -// 获取拦截后的数组方法的属性名 - var arrayKeys = Object.getOwnPropertyNames(arrayMethods); - -// 是否应该进行观测 - var shouldObserve = true; - -// 切换观测状态 - function toggleObserving (value) { - shouldObserve = value; - } - -// Observer 类,用于将对象转换为可观测对象 - var booleanIndex = getTypeIndex(Boolean, prop.type); - // 如果属性类型包含布尔类型 - if (booleanIndex > -1) { - // 如果属性不存在且没有默认值,则将值设为false - if (absent &&!hasOwn(prop, 'default')) { - value = false; - } else if (value === '' || value === hyphenate(key)) { - // 只有当布尔类型的优先级高于字符串类型时,才将空字符串或同名的值转换为布尔类型 - var Observer = function Observer (value) { - this.value = value; - this.dep = new Dep(); - this.vmCount = 0; - // 在对象上定义一个不可枚举的属性 __ob__,指向当前 Observer 实例 - def(value, '__ob__', this); - if (Array.isArray(value)) { - if (hasProto) { - // 使用 __proto__ 进行原型增强 - protoAugment(value, arrayMethods); - } else { - // 复制属性进行增强 - copyAugment(value, arrayMethods, arrayKeys); - } - // 对数组元素进行观测 - this.observeArray(value); - } else { - // 遍历对象属性并转换为响应式 - this.walk(value); - } - }; - -// 遍历对象的所有属性,将其转换为 getter/setter - Observer.prototype.walk = function walk (obj) { - var keys = Object.keys(obj); - for (var i = 0; i < keys.length; i++) { - defineReactive$$1(obj, keys[i]); - } - }; - -// 对数组中的每个元素进行观测 - Observer.prototype.observeArray = function observeArray (items) { - for (var i = 0, l = items.length; i < l; i++) { - observe(items[i]); - } - }; - -// 使用 __proto__ 增强目标对象或数组 - function protoAugment (target, src) { - target.__proto__ = src; - } - -// 通过定义隐藏属性增强目标对象或数组 - function copyAugment (target, src, keys) { - for (var i = 0, l = keys.length; i < l; i++) { - var key = keys[i]; - def(target, key, src[key]); - } - } - -// 尝试为一个值创建观察者实例 - function observe (value, asRootData) { - // 检查传入的值是否不是对象,或者是否为 VNode 实例 - // 如果满足条件,则直接返回,不进行观察 - if (!isObject(value) || value instanceof VNode) { - return - } - // 声明一个变量 ob,用于存储观察者实例 - var ob; - // 检查传入的值是否已经有 __ob__ 属性,并且该属性对应的是 Observer 实例 - if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { - // 如果已经有观察者实例,则直接将其赋值给 ob 变量 - ob = value.__ob__; - } - // 如果值还没有被观察,并且满足以下条件 - else if ( - // 全局变量 shouldObserve 为 true,表示应该进行观察 - shouldObserve && - // 当前不是服务器渲染环境 - !isServerRendering() && - // 传入的值是数组或者是普通对象 - (Array.isArray(value) || isPlainObject(value)) && - // 传入的值是可扩展的,即可以添加新的属性 - Object.isExtensible(value) && - // 传入的值不是 Vue 实例 - !value._isVue - ) { - // 创建一个新的 Observer 实例,并将其赋值给 ob 变量 - ob = new Observer(value); - } - // 如果 asRootData 为 true,并且已经成功创建了观察者实例 - if (asRootData && ob) { - // 增加观察者实例的 vmCount 属性,该属性用于记录该对象作为根数据的次数 - ob.vmCount++; - } - // 返回观察者实例,如果没有创建则返回 undefined - return ob - } - // Define a reactive property on an Object. -// 在一个对象上定义一个响应式属性 - function defineReactive$$1 ( - // 要定义响应式属性的对象 - obj, - // 属性名 - key, - // 属性的初始值 - val, - // 自定义的 setter 函数 - customSetter, - // 是否进行浅观察 - shallow - ) { - // 创建一个 Dep 实例,用于依赖收集和派发更新 - var dep = new Dep(); - - // 获取对象上该属性的描述符 - var property = Object.getOwnPropertyDescriptor(obj, key); - // 如果属性存在且不可配置,则直接返回 - if (property && property.configurable === false) { - return - } - - // cater for pre-defined getter/setters - // 处理预定义的 getter/setter - // 获取属性的 getter 函数 - var getter = property && property.get; - // 获取属性的 setter 函数 - var setter = property && property.set; - // 如果没有 getter 或者有 setter,并且只传入了两个参数,则获取对象上该属性的值 - if ((!getter || setter) && arguments.length === 2) { - val = obj[key]; - } - - // 如果不是浅观察,则尝试为属性值创建观察者实例 - var childOb = !shallow && observe(val); - // 使用 Object.defineProperty 定义属性的 getter 和 setter - Object.defineProperty(obj, key, { - // 属性可枚举 - enumerable: true, - // 属性可配置 - configurable: true, - // 定义 getter 函数 - get: function reactiveGetter () { - // 如果有预定义的 getter,则调用该 getter 获取值,否则使用传入的初始值 - var value = getter ? getter.call(obj) : val; - // 如果存在 Dep.target(即有正在计算的依赖) - if (Dep.target) { - // 收集依赖 - dep.depend(); - // 如果存在子观察者 - if (childOb) { - // 子观察者收集依赖 - childOb.dep.depend(); - // 如果值是数组 - if (Array.isArray(value)) { - // 对数组中的每个元素收集依赖 - dependArray(value); - } - } - } - // 返回属性值 - return value - }, - // 定义 setter 函数 - set: function reactiveSetter (newVal) { - // 如果有预定义的 getter,则调用该 getter 获取旧值,否则使用传入的初始值 - var value = getter ? getter.call(obj) : val; - // 检查新值和旧值是否相等,如果相等则直接返回 - /* eslint-disable no-self-compare */ - if (newVal === value || (newVal !== newVal && value !== value)) { - return - } - /* eslint-enable no-self-compare */ - // 如果有自定义的 setter 函数,则调用该函数 - if (customSetter) { - customSetter(); - } - // 如果有 getter 但没有 setter,则直接返回 - // #7981: for accessor properties without setter - if (getter && !setter) { return } - // 如果有预定义的 setter,则调用该 setter 设置新值 - if (setter) { - setter.call(obj, newVal); - } else { - // 否则直接更新值 - val = newVal; - } - // 如果不是浅观察,则尝试为新值创建观察者实例 - childOb = !shallow && observe(newVal); - // 通知所有依赖更新 - dep.notify(); - } - }); - } - -// Set a property on an object. Adds the new property and -// triggers change notification if the property doesn't -// already exist. -// 在对象上设置一个属性。如果属性不存在,则添加该属性并触发变更通知 - function set ( - // 目标对象 - target, - // 属性名 - key, - // 属性值 - val - ) { - // 如果目标对象为 undefined、null 或者是原始类型,则发出警告 - if (isUndef(target) || isPrimitive(target) - ) { - warn(("Cannot set reactive property on undefined, null, or primitive value: " + ((target)))); - } - // 如果目标对象是数组且键是有效的数组索引 - if (Array.isArray(target) && isValidArrayIndex(key)) { - // 确保数组长度足够 - target.length = Math.max(target.length, key); - // 使用 splice 方法替换数组中的元素 - target.splice(key, 1, val); - // 返回设置的值 - return val - } - // 如果键已经存在于目标对象中且不是原型链上的属性 - if (key in target && !(key in Object.prototype)) { - // 直接设置属性值 - target[key] = val; - // 返回设置的值 - return val - } - // 获取目标对象的观察者实例 - var ob = (target).__ob__; - // 如果目标对象是 Vue 实例或者是根数据对象,则发出警告 - if (target._isVue || (ob && ob.vmCount)) { - warn( - 'Avoid adding reactive properties to a Vue instance or its root $data ' + - 'at runtime - declare it upfront in the data option.' - ); - // 返回设置的值 - return val - } - // 如果没有观察者实例 - if (!ob) { - // 直接设置属性值 - target[key] = val; - // 返回设置的值 - return val - } - // 为目标对象的属性定义响应式 - defineReactive$$1(ob.value, key, val); - // 通知依赖更新 - ob.dep.notify(); - // 返回设置的值 - return val - } - -// Delete a property and trigger change if necessary. -// 删除一个属性,并在必要时触发变更通知 - function del ( - // 目标对象 - target, - // 属性名 - key - ) { - // 如果目标对象为 undefined、null 或者是原始类型,则发出警告 - if (isUndef(target) || isPrimitive(target) - ) { - warn(("Cannot delete reactive property on undefined, null, or primitive value: " + ((target)))); - } - // 如果目标对象是数组且键是有效的数组索引 - if (Array.isArray(target) && isValidArrayIndex(key)) { - // 使用 splice 方法删除数组中的元素 - target.splice(key, 1); - return - } - // 获取目标对象的观察者实例 - var ob = (target).__ob__; - // 如果目标对象是 Vue 实例或者是根数据对象,则发出警告 - if (target._isVue || (ob && ob.vmCount)) { - warn( - 'Avoid deleting properties on a Vue instance or its root $data ' + - '- just set it to null.' - ); - return - } - // 如果目标对象没有该属性,则直接返回 - if (!hasOwn(target, key)) { - return - } - // 删除目标对象的属性 - delete target[key]; - // 如果没有观察者实例,则直接返回 - if (!ob) { - return - } - // 通知依赖更新 - ob.dep.notify(); - } - -// Collect dependencies on array elements when the array is touched, since -// we cannot intercept array element access like property getters. -// 当数组被访问时,收集数组元素的依赖,因为我们无法像拦截属性 getter 那样拦截数组元素的访问 - function dependArray ( - // 数组 - value - ) { - // 遍历数组 - for (var e = (void 0), i = 0, l = value.length; i < l; i++) { - // 获取数组元素 - e = value[i]; - // 如果元素存在且有观察者实例,则收集该元素的依赖 - e && e.__ob__ && e.__ob__.dep.depend(); - // 如果元素是数组 - if (Array.isArray(e)) { - // 递归收集数组元素的依赖 - dependArray(e); - } - } - } - -// Option overwriting strategies are functions that handle -// how to merge a parent option value and a child option -// value into the final value. -// 选项覆盖策略是处理如何将父选项值和子选项值合并为最终值的函数 - var strats = config.optionMergeStrategies; - -// Options with restrictions -// 有约束的选项 - { - // 定义 el 和 propsData 选项的合并策略 - strats.el = strats.propsData = function ( - // 父选项值 - parent, - // 子选项值 - child, - // Vue 实例 - vm, - // 选项名 - key - ) { - // 如果没有 Vue 实例,则发出警告 - if (!vm) { - warn( - "option \"" + key + "\" can only be used during instance " + - 'creation with the `new` keyword.' - ); - } - // 使用默认的合并策略 - return defaultStrat(parent, child) - }; - } - -// Helper that recursively merges two data objects together. -// 递归合并两个数据对象的辅助函数 - function mergeData ( - // 目标对象 - to, - // 源对象 - from - ) { - // 如果源对象不存在,则返回目标对象 - if (!from) { return to } - // 定义变量,用于存储键、目标对象的值和源对象的值 - var key, toVal, fromVal; - - // 获取源对象的所有键 - var keys = hasSymbol - ? Reflect.ownKeys(from) - : Object.keys(from); - - // 遍历源对象的所有键 - for (var i = 0; i < keys.length; i++) { - // 获取当前键 - key = keys[i]; - // 如果键是 __ob__,则跳过 - // in case the object is already observed... - if (key === '__ob__') { continue } - // 获取目标对象上该键的值 - toVal = to[key]; - // 获取源对象上该键的值 - fromVal = from[key]; - // 如果目标对象没有该键 - if (!hasOwn(to, key)) { - // 在目标对象上设置该键的值 - set(to, key, fromVal); - } else if ( - // 如果目标对象和源对象上该键的值不相等 - toVal !== fromVal && - // 目标对象上该键的值是纯对象 - isPlainObject(toVal) && - // 源对象上该键的值是纯对象 - isPlainObject(fromVal) - ) { - // 递归合并两个对象 - mergeData(toVal, fromVal); - } - } - // 返回合并后的目标对象 - return to - } - -// Data -// 数据合并函数 - function mergeDataOrFn ( - // 父数据值 - parentVal, - // 子数据值 - childVal, - // Vue 实例 - vm - ) { - // 如果没有 Vue 实例 - if (!vm) { - // 在 Vue.extend 合并时,两者都应该是函数 - // 如果子数据值不存在,则返回父数据值 - if (!childVal) { - return parentVal - } - // 如果父数据值不存在,则返回子数据值 - if (!parentVal) { - return childVal - } - // 当父数据值和子数据值都存在时,我们需要返回一个函数,该函数返回两个函数合并后的结果 - // 这里不需要检查父数据值是否是函数,因为它必须是函数才能通过之前的合并 - return function mergedDataFn () { - return mergeData( - // 如果子数据值是函数,则调用该函数并传入 this 上下文,否则直接使用子数据值 - typeof childVal === 'function' ? childVal.call(this, this) : childVal, - // 如果父数据值是函数,则调用该函数并传入 this 上下文,否则直接使用父数据值 - typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal - ) - } - } else { - return function mergedInstanceDataFn () { - // 实例合并 - // 如果子数据值是函数,则调用该函数并传入 Vue 实例,否则直接使用子数据值 - var instanceData = typeof childVal === 'function' - ? childVal.call(vm, vm) - : childVal; - // 如果父数据值是函数,则调用该函数并传入 Vue 实例,否则直接使用父数据值 - var defaultData = typeof parentVal === 'function' - ? parentVal.call(vm, vm) - : parentVal; - // 如果实例数据存在 - if (instanceData) { - // 合并实例数据和默认数据 - return mergeData(instanceData, defaultData) - } else { - // 否则返回默认数据 - return defaultData - } - } - } - } - -// 定义 data 选项的合并策略 - strats.data = function ( - // 父选项值 - parentVal, - // 子选项值 - childVal, - // Vue 实例 - vm - ) { - // 如果没有 Vue 实例 - if (!vm) { - // 如果子选项值存在且不是函数,则发出警告 - if (childVal && typeof childVal !== 'function') { - warn( - 'The "data" option should be a function ' + - 'that returns a per-instance value in component ' + - 'definitions.', - vm - ); - - return parentVal - } - // 使用 mergeDataOrFn 函数进行合并 - return mergeDataOrFn(parentVal, childVal) - } - - // 使用 mergeDataOrFn 函数进行合并,并传入 Vue 实例 - return mergeDataOrFn(parentVal, childVal, vm) - }; - -// Hooks and props are merged as arrays. -// 钩子和 props 以数组形式合并 - function mergeHook ( - // 父选项值 - parentVal, - // 子选项值 - childVal - ) { - // 初始化合并结果 - var res = childVal - ? parentVal - // 如果父选项值存在,则将子选项值合并到父选项值中 - ? parentVal.concat(childVal) - // 如果父选项值不存在,且子选项值是数组,则直接使用子选项值 - : Array.isArray(childVal) - ? childVal - // 如果父选项值不存在,且子选项值不是数组,则将子选项值包装成数组 - : [childVal] - // 如果子选项值不存在,则使用父选项值 - : parentVal; - // 对合并结果进行去重处理 - return res - ? dedupeHooks(res) - : res - } - -// 对钩子数组进行去重处理 - function dedupeHooks ( - // 钩子数组 - hooks - ) { - // 初始化去重后的数组 - var res = []; - // 遍历钩子数组 - for (var i = 0; i < hooks.length; i++) { - // 如果去重后的数组中不包含当前钩子,则将其添加到去重后的数组中 - if (res.indexOf(hooks[i]) === -1) { - res.push(hooks[i]); - } - } - // 返回去重后的数组 - return res - } - -// 遍历生命周期钩子数组 - LIFECYCLE_HOOKS.forEach(function (hook) { - // 为每个生命周期钩子定义合并策略 - strats[hook] = mergeHook; - }); - -// Assets -// 当存在 Vue 实例(实例创建时),我们需要对构造函数选项、实例选项和父选项进行三方合并 -// 资产(组件、指令、过滤器)的合并函数 - function mergeAssets ( - // 父选项值 - parentVal, - // 子选项值 - childVal, - // Vue 实例 - vm, - // 选项名 - key - ) { - // 创建一个继承自父选项值的对象 - var res = Object.create(parentVal || null); - // 如果子选项值存在 - if (childVal) { - // 检查子选项值是否为对象类型 - assertObjectType(key, childVal, vm); - // 将子选项值合并到结果对象中 - return extend(res, childVal) - } else { - // 如果子选项值不存在,则返回结果对象 - return res - } - } - -// 遍历资产类型数组 - ASSET_TYPES.forEach(function (type) { - // 为每个资产类型定义合并策略 - strats[type + 's'] = mergeAssets; - }); - -// Watchers. -// 监听器的合并策略 -// Watchers hashes should not overwrite one -// another, so we merge them as arrays. -// 监听器哈希不应该相互覆盖,所以我们将它们合并为数组 - strats.watch = function ( - // 父选项值 - parentVal, - // 子选项值 - childVal, - // Vue 实例 - vm, - // 选项名 - key - ) { - // work around Firefox's Object.prototype.watch... - // 处理 Firefox 的 Object.prototype.watch - if (parentVal === nativeWatch) { parentVal = undefined; } - if (childVal === nativeWatch) { childVal = undefined; } - /* istanbul ignore if */ - // 如果子选项值不存在,则返回一个继承自父选项值的对象 - if (!childVal) { return Object.create(parentVal || null) } - { - // 检查子选项值是否为对象类型 - assertObjectType(key, childVal, vm); - } - // 如果父选项值不存在,则返回子选项值 - if (!parentVal) { return childVal } - // 初始化合并结果 - var ret = {}; - // 将父选项值合并到结果对象中 - extend(ret, parentVal); - // 遍历子选项值的所有键 - for (var key$1 in childVal) { - // 获取父选项值中该键的值 - var parent = ret[key$1]; - // 获取子选项值中该键的值 - var child = childVal[key$1]; - // 如果父选项值中该键的值存在且不是数组,则将其包装成数组 - if (parent && !Array.isArray(parent)) { - parent = [parent]; - } - // 将子选项值中该键的值合并到父选项值中 - ret[key$1] = parent - ? parent.concat(child) - : Array.isArray(child) ? child : [child]; - } - // 返回合并结果 - return ret - }; - -// Other object hashes. -// 其他对象哈希的合并策略 - strats.props = - strats.methods = - strats.inject = - strats.computed = function ( - // 父选项值 - parentVal, - // 子选项值 - childVal, - // Vue 实例 - vm, - // 选项名 - key - ) { - // 如果子选项值存在且不是生产环境,则检查子选项值是否为对象类型 - if (childVal && "development" !== 'production') { - assertObjectType(key, childVal, vm); - } - // 如果父选项值不存在,则返回子选项值 - if (!parentVal) { return childVal } - // 初始化合并结果 - var ret = Object.create(null); - // 将父选项值合并到结果对象中 - extend(ret, parentVal); - // 如果子选项值存在,则将其合并到结果对象中 - if (childVal) { extend(ret, childVal); } - // 返回合并结果 - return ret - }; -// provide 选项的合并策略使用 mergeDataOrFn 函数 - strats.provide = mergeDataOrFn; - -// Default strategy. -// 默认合并策略 - var defaultStrat = function ( - // 父选项值 - parentVal, - // 子选项值 - childVal - ) { - // 如果子选项值为 undefined,则返回父选项值,否则返回子选项值 - return childVal === undefined - ? parentVal - : childVal - }; - -// Validate component names -// 验证组件名称 - function checkComponents ( - // 选项对象 - options - ) { - // 遍历选项对象中的 components 属性 - for (var key in options.components) { - // 验证组件名称 - validateComponentName(key); - } - } - -// 验证组件名称是否合法 - function validateComponentName ( - // 组件名称 - name - ) { - // 如果组件名称不符合正则表达式的规则,则发出警告 - if (!new RegExp(("^[a-zA-Z][\\-\\.0-9_" + (unicodeRegExp.source) + "]*$")).test(name)) { - warn( - 'Invalid component name: "' + name + '". Component names ' + - 'should conform to valid custom element name in html5 specification.' - ); - } - // 如果组件名称是内置标签或者是保留标签,则发出警告 - if (isBuiltInTag(name) || config.isReservedTag(name)) { - warn( - 'Do not use built-in or reserved HTML elements as component ' + - 'id: ' + name - ); - } - } - -// Ensure all props option syntax are normalized into the -// Object-based format. -// 确保所有 props 选项的语法都被规范化为基于对象的格式 - function normalizeProps ( - // 选项对象 - options, - // Vue 实例 - vm - ) { - // 获取选项对象中的 props 属性 - var props = options.props; - // 如果 props 属性不存在,则直接返回 - if (!props) { return } - // 初始化规范化后的 props 对象 - var res = {}; - // 定义变量,用于存储索引、值和名称 - var i, val, name; - // 如果 props 属性是数组 - if (Array.isArray(props)) { - // 获取数组的长度 - i = props.length; - // 从后往前遍历数组 - while (i--) { - // 获取当前元素 - val = props[i]; - // 如果当前元素是字符串 - if (typeof val === 'string') { - // 将字符串转换为驼峰命名 - name = camelize(val); - // 在规范化后的 props 对象中添加该属性,并设置类型为 null - res[name] = { type: null }; - } else { - // 如果当前元素不是字符串,则发出警告 - warn('props must be strings when using array syntax.'); - } - } - } else if (isPlainObject(props)) { - // 如果 props 属性是纯对象 - // 遍历对象的所有键 - for (var key in props) { - // 获取当前键对应的值 - val = props[key]; - // 将键转换为驼峰命名 - name = camelize(key); - // 如果值是纯对象,则直接使用该值,否则将其包装成一个包含类型的对象 - res[name] = isPlainObject(val) - ? val - : { type: val }; - } - } else { - // 如果 props 属性既不是数组也不是纯对象,则发出警告 - warn( - "Invalid value for option \"props\": expected an Array or an Object, " + - "but got " + (toRawType(props)) + ".", - vm - ); - } - // 将规范化后的 props 对象赋值给选项对象的 props 属性 - options.props = res; - } - -// Normalize all injections into Object-based format -// 将所有注入项规范化为基于对象的格式 - function normalizeInject ( - // 选项对象 - options, - // Vue 实例 - vm - ) { - // 获取选项对象中的 inject 属性 - var inject = options.inject; - // 如果 inject 属性不存在,则直接返回 - if (!inject) { return } - // 初始化规范化后的 inject 对象 - var normalized = options.inject = {}; - // 如果 inject 属性是数组 - if (Array.isArray(inject)) { - // 遍历数组 - for (var i = 0; i < inject.length; i++) { - // 在规范化后的 inject 对象中添加该属性,并设置 from 属性为当前元素 - normalized[inject[i]] = { from: inject[i] }; - } - } else if (isPlainObject(inject)) { - // 如果 inject 属性是纯对象 - // 遍历对象的所有键 - for (var key in inject) { - // 获取当前键对应的值 - var val = inject[key]; - // 如果值是纯对象,则将 from 属性合并到该值中,否则将其包装成一个包含 from 属性的对象 - normalized[key] = isPlainObject(val) - ? extend({ from: key }, val) - : { from: val }; - } - } else { - // 如果 inject 属性既不是数组也不是纯对象,则发出警告 - warn( - "Invalid value for option \"inject\": expected an Array or an Object, " + - "but got " + (toRawType(inject)) + ".", - vm - ); - } - } - -// Normalize raw function directives into object format. -// 将原始的函数指令规范化为对象格式 - function normalizeDirectives ( - // 选项对象 - options - ) { - // 获取选项对象中的 directives 属性 - var dirs = options.directives; - // 如果 directives 属性存在 - if (dirs) { - // 遍历 directives 对象的所有键 - for (var key in dirs) { - // 获取当前键对应的值 - var def$$1 = dirs[key]; - // 如果值是函数 - if (typeof def$$1 === 'function') { - // 将其包装成一个包含 bind 和 update 方法的对象 - dirs[key] = { bind: def$$1, update: def$$1 }; - } - } - } - } - -// 检查值是否为纯对象,如果不是则发出警告 - function assertObjectType ( - // 选项名 - name, - // 值 - value, - // Vue 实例 - vm - ) { - // 如果值不是纯对象,则发出警告 - if (!isPlainObject(value)) { - warn( - "Invalid value for option \"" + name + "\": expected an Object, " + - "but got " + (toRawType(value)) + ".", - vm - ); - } - } - -// Merge two option objects into a new one. -// Core utility used in both instantiation and inheritance. -// 将两个选项对象合并为一个新的对象 -// 实例化和继承中使用的核心工具 - function mergeOptions ( - // 父选项对象 - parent, - // 子选项对象 - child, - // Vue 实例 - vm - ) { - { - // 检查子选项对象中的组件名称是否合法 - checkComponents(child); - } - - // 如果子选项对象是一个函数,则获取其 options 属性 - if (typeof child === 'function') { - child = child.options; - } - - // 规范化子选项对象中的 props 属性 - normalizeProps(child, vm); - // 规范化子选项对象中的 inject 属性 - normalizeInject(child, vm); - // 规范化子选项对象中的 directives 属性 - normalizeDirectives(child); - - // Apply extends and mixins on the child options, - // but only if it is a raw options object that isn't - // the result of another mergeOptions call. - // Only merged options has the _base property. - // 如果子选项对象不是另一次 mergeOptions 调用的结果(即没有 _base 属性) - if (!child._base) { - // 如果子选项对象有 extends 属性 - if (child.extends) { - // 递归合并父选项对象和子选项对象的 extends 属性 - parent = mergeOptions(parent, child.extends, vm); - } - // 如果子选项对象有 mixins 属性 - if (child.mixins) { - // 遍历 mixins 数组 - for (var i = 0, l = child.mixins.length; i < l; i++) { - // 递归合并父选项对象和 mixins 数组中的每个选项对象 - parent = mergeOptions(parent, child.mixins[i], vm); - } - } - } - // 定义一个空对象options - var options = {}; -// 定义变量key - var key; -// 遍历parent对象的属性 - for (key in parent) { - // 调用mergeField函数处理属性 - mergeField(key); - } -// 遍历child对象的属性 - for (key in child) { - // 如果parent对象不包含当前属性 - if (!hasOwn(parent, key)) { - // 调用mergeField函数处理属性 - mergeField(key); - } - } -// 定义mergeField函数,用于合并属性 - function mergeField (key) { - // 获取属性的合并策略,如果没有则使用默认策略 - var strat = strats[key] || defaultStrat; - // 根据合并策略合并parent和child中对应属性的值,并将结果存储在options中 - options[key] = strat(parent[key], child[key], vm, key); - } -// 返回合并后的options对象 - return options - } - //解析一个资源。 - // 这个函数被使用是因为子实例需要访问在其祖先链中定义的资源。 -// 定义resolveAsset函数,用于解析资源 - function resolveAsset ( - options, - type, - id, - warnMissing - ) { - // 如果id不是字符串类型,则返回 - /* istanbul ignore if */ - if (typeof id!== 'string') { - return - } - // 获取指定类型的资源 - var assets = options[type]; - // 首先检查本地注册的变体 - if (hasOwn(assets, id)) { return assets[id] } - // 将id转换为驼峰命名 - var camelizedId = camelize(id); - // 如果驼峰命名的id存在于资源中,则返回对应的值 - if (hasOwn(assets, camelizedId)) { return assets[camelizedId] } - // 将驼峰命名的id转换为首字母大写的形式 - var PascalCaseId = capitalize(camelizedId); - // 如果首字母大写的id存在于资源中,则返回对应的值 - if (hasOwn(assets, PascalCaseId)) { return assets[PascalCaseId] } - // 回退到原型链查找 - var res = assets[id] || assets[camelizedId] || assets[PascalCaseId]; - // 如果需要警告且没有找到资源,则发出警告 - if (warnMissing &&!res) { - warn( - 'Failed to resolve'+ type.slice(0, -1) + ':'+ id, - options - ); - } - // 返回找到的资源 - return res - } - - /* */ - -// 定义validateProp函数,用于验证属性 - function validateProp ( - key, - propOptions, - propsData, - vm - ) { - // 获取属性的选项 - var prop = propOptions[key]; - // 判断属性是否不存在于propsData中 - var absent =!hasOwn(propsData, key); - // 获取属性的值 - var value = propsData[key]; - // 布尔类型转换 - var stringIndex = getTypeIndex(String, prop.type); - if (stringIndex < 0 || booleanIndex < stringIndex) { - value = true; - } - } - } - // 检查默认值 - if (value === undefined) { - // 获取属性的默认值 - value = getPropDefaultValue(vm, prop, key); - // 因为默认值是一个新的副本,所以确保对其进行观察 - var prevShouldObserve = shouldObserve; - toggleObserving(true); - observe(value); - toggleObserving(prevShouldObserve); - } - { - // 断言属性是否有效 - assertProp(prop, key, value, vm, absent); - } - // 返回验证后的属性值 - return value - } - - // 获取属性的默认值。 -// 定义getPropDefaultValue函数,用于获取属性的默认值 - function getPropDefaultValue (vm, prop, key) { - // 如果属性没有默认值,则返回undefined - if (!hasOwn(prop, 'default')) { - return undefined - } - // 获取属性的默认值 - var def = prop.default; - // 对对象和数组类型的默认值进行警告检查,非工厂函数的默认值是无效的 - if (isObject(def)) { - warn( - 'Invalid default value for prop "' + key + '":'+ - 'Props with type Object/Array must use a factory function'+ - 'to return the default value.', - vm - ); - } - // 如果vm存在,且propsData中属性值为undefined,且vm._props中属性值不为undefined,则返回vm._props中的值 - if (vm && vm.$options.propsData && - vm.$options.propsData[key] === undefined && - vm._props[key]!== undefined - ) { - return vm._props[key] - } - // 如果默认值是函数且属性类型不是函数,则调用函数获取默认值,否则直接返回默认值 - return typeof def === 'function' && getType(prop.type)!== 'Function' - ? def.call(vm) - : def - } - - // 断言一个属性是否有效。 -// 定义assertProp函数,用于断言属性是否有效 - function assertProp ( - prop, - name, - value, - vm, - absent - ) { - // 如果属性是必需的且不存在,则发出警告 - if (prop.required && absent) { - warn( - 'Missing required prop: "' + name + '"', - vm - ); - return - } - // 如果属性值为null且属性不是必需的,则返回 - if (value == null &&!prop.required) { - return - } - // 获取属性的类型 - var type = prop.type; - // 初始化有效性为true,如果没有指定类型或者类型为true - var valid =!type || type === true; - // 定义预期类型数组 - var expectedTypes = []; - // 如果有指定属性类型 - if (type) { - // 如果类型不是数组,则将其转换为数组 - if (!Array.isArray(type)) { - type = [type]; - } - // 遍历类型数组,检查属性值是否符合预期类型 - for (var i = 0; i < type.length &&!valid; i++) { - var assertedType = assertType(value, type[i]); - expectedTypes.push(assertedType.expectedType || ''); - valid = assertedType.valid; - } - } - - // 如果属性值不符合预期类型,则发出警告 - if (!valid) { - warn( - getInvalidTypeMessage(name, value, expectedTypes), - vm - ); - return - } - // 获取属性的验证函数 - var validator = prop.validator; - // 如果有验证函数且属性值不通过验证,则发出警告 - if (validator) { - if (!validator(value)) { - warn( - 'Invalid prop: custom validator check failed for prop "' + name + '".', - vm - ); - } - } - } - -// 定义简单类型检查正则表达式 - var simpleCheckRE = /^(String|Number|Boolean|Function|Symbol)$/; - -// 定义assertType函数,用于检查属性值的类型是否符合预期 - function assertType (value, type) { - // 初始化有效性为false - var valid; - // 获取预期类型 - var expectedType = getType(type); - // 如果预期类型是简单类型 - if (simpleCheckRE.test(expectedType)) { - // 获取属性值的类型 - var t = typeof value; - // 检查属性值类型是否与预期类型一致(对于基本包装对象,还需要检查是否是预期类型的实例) - valid = t === expectedType.toLowerCase(); - if (!valid && t === 'object') { - valid = value instanceof type; - } - } else if (expectedType === 'Object') { - // 如果预期类型是对象,则检查属性值是否是普通对象 - valid = isPlainObject(value); - } else if (expectedType === 'Array') { - // 如果预期类型是数组,则检查属性值是否是数组 - valid = Array.isArray(value); - } else { - // 否则检查属性值是否是预期类型的实例 - valid = value instanceof type; - } - // 返回检查结果 - return { - valid: valid, - expectedType: expectedType - } - } - //使用函数的字符串名称来检查内置类型, - // 因为简单的相等性检查在不同的vm/iframe中运行时会失败。 -// 定义getType函数,用于获取函数的类型名称 - function getType (fn) { - // 匹配函数的字符串表示中的函数名 - var match = fn && fn.toString().match(/^\s*function (\w+)/); - // 返回匹配到的函数名,否则返回空字符串 - return match? match[1] : '' - } - -// 定义isSameType函数,用于判断两个类型是否相同 - function isSameType (a, b) { - // 通过比较两个类型的名称是否相同来判断 - return getType(a) === getType(b) - } - -// 定义getTypeIndex函数,用于获取类型在预期类型数组中的索引 - function getTypeIndex (type, expectedTypes) { - // 如果预期类型不是数组,则直接比较类型是否相同并返回索引 - if (!Array.isArray(expectedTypes)) { - return isSameType(expectedTypes, type)? 0 : -1 - } - // 遍历预期类型数组,查找类型的索引 - for (var i = 0, len = expectedTypes.length; i < len; i++) { - if (isSameType(expectedTypes[i], type)) { - return i - } - } - // 如果没有找到,则返回-1 - return -1 - } - -// 定义getInvalidTypeMessage函数,用于获取无效类型的错误消息 - function getInvalidTypeMessage (name, value, expectedTypes) { - // 初始化错误消息 - var message = "Invalid prop: type check failed for prop \"" + name + "\"." + - " Expected " + (expectedTypes.map(capitalize).join(', ')); - // 获取第一个预期类型 - var expectedType = expectedTypes[0]; - // 获取属性值的实际类型 - var receivedType = toRawType(value); - // 格式化预期值 - var expectedValue = styleValue(value, expectedType); - // 格式化实际值 - var receivedValue = styleValue(value, receivedType); - // 如果只有一个预期类型且是可解释的类型,并且实际类型不是布尔类型,则在错误消息中添加预期值 - if (expectedTypes.length === 1 && - isExplicable(expectedType) && - !isBoolean(expectedType, receivedType)) { - message += " with value " + expectedValue; - } - // 在错误消息中添加实际类型 - message += ", got " + receivedType + " "; - // 如果实际类型是可解释的类型,则在错误消息中添加实际值 - if (isExplicable(receivedType)) { - message += "with value " + receivedValue + "."; - } - // 返回错误消息 - return message - } - -// 定义styleValue函数,用于格式化值的显示形式 - function styleValue (value, type) { - // 如果类型是字符串,则用双引号包裹值 - if (type === 'String') { - return ("\"" + value + "\"") - } else if (type === 'Number') { - // 如果类型是数字,则将值转换为字符串 - return ("" + (Number(value))) - } else { - // 否则直接将值转换为字符串 - return ("" + value) - } - } - -// 定义isExplicable函数,用于判断类型是否是可解释的类型(字符串、数字、布尔) - function isExplicable (value) { - // 定义可解释的类型数组 - var explicitTypes = ['string', 'number', 'boolean']; - // 检查值的类型是否在可解释的类型数组中 - return explicitTypes.some(function (elem) { return value.toLowerCase() === elem; }) - } - -// 定义isBoolean函数,用于判断参数中是否包含布尔类型 - function isBoolean () { - // 将参数转换为数组 - var args = [], len = arguments.length; - while ( len-- ) args[ len ] = arguments[ len ]; - - // 检查数组中是否有元素的类型为布尔类型 - return args.some(function (elem) { return elem.toLowerCase() === 'boolean'; }) - } - - -// 定义handleError函数,用于处理错误 - function handleError (err, vm, info) { - // 在处理错误处理程序时停用依赖跟踪,以避免可能的无限渲染。 - // 参考:https://github.com/vuejs/vuex/issues/1505 - pushTarget(); - try { - // 如果有vm实例 - if (vm) { - var cur = vm; - // 向上遍历vm的父级实例 - while ((cur = cur.$parent)) { - // 获取父级实例的errorCaptured钩子函数数组 - var hooks = cur.$options.errorCaptured; - // 如果有errorCaptured钩子函数 - if (hooks) { - // 遍历钩子函数数组 - for (var i = 0; i < hooks.length; i++) { - try { - // 调用钩子函数,并判断是否捕获了错误(返回false表示捕获) - var capture = hooks[i].call(cur, err, vm, info) === false; - if (capture) { return } - } catch (e) { - // 如果钩子函数调用时出错,则调用全局错误处理函数 - globalHandleError(e, cur, 'errorCaptured hook'); - } - } - } - } - } - // 调用全局错误处理函数 - globalHandleError(err, vm, info); - } finally { - // 恢复依赖跟踪 - popTarget(); - } - } - -// 定义invokeWithErrorHandling函数,用于在错误处理下调用处理程序 - function invokeWithErrorHandling ( - handler, - context, - args, - vm, - info - ) { - // 定义变量res用于存储处理程序的返回值 - var res; - try { - // 调用处理程序 - res = args? handler.apply(context, args) : handler.call(context); - // 如果返回值是Promise且未被处理,则添加错误处理 - if (res &&!res._isVue && isPromise(res) &&!res._handled) { - res.catch(function (e) { return handleError(e, vm, info + " (Promise/async)"); }); - // 标记Promise已被处理,避免多次捕获 - res._handled = true; - } - } catch (e) { - // 如果处理程序调用时出错,则调用错误处理函数 - handleError(e, vm, info); - } - // 返回处理程序的返回值 - return res - } - -// 定义globalHandleError函数,用于全局处理错误 - function globalHandleError (err, vm, info) { - // 如果有配置的错误处理函数 - if (config.errorHandler) { - try { - // 调用配置的错误处理函数 - return config.errorHandler.call(null, err, vm, info) - } catch (e) { - // 如果用户在处理函数中故意抛出原始错误,不重复记录 - if (e!== err) { - logError(e, null, 'config.errorHandler'); - } - } - } - // 调用logError函数记录错误 - logError(err, vm, info); - } - -// 定义logError函数,用于记录错误 - function logError (err, vm, info) { - { - // 发出警告 - warn(("Error in " + info + ": \"" + (err.toString()) + "\""), vm); - } - /* istanbul ignore else */ - // 如果在浏览器或Weex环境中且console可用,则在控制台输出错误 - if ((inBrowser || inWeex) && typeof console!== 'undefined') { - console.error(err); - } else { - // 否则抛出错误 - throw err - } - } - - -// 定义变量isUsingMicroTask表示是否使用微任务,初始值为false - var isUsingMicroTask = false; - -// 定义callbacks数组用于存储回调函数 - var callbacks = []; -// 定义变量pending表示是否有挂起的回调,初始值为false - var pending = false; - -// 定义flushCallbacks函数,用于刷新回调函数队列 - function flushCallbacks () { - // 将pending设为false - pending = false; - // 获取callbacks数组的副本 - var copies = callbacks.slice(0); - // 清空callbacks数组 - callbacks.length = 0; - // 遍历副本数组,依次调用回调函数 - for (var i = 0; i < copies.length; i++) { - copies[i](); - } - } - // 这里我们使用微任务来实现异步延迟包装器 -// 在 2.5 版本中,我们使用(宏)任务(结合微任务) -// 然而,在重绘之前立即更改状态时会有一些细微问题 -// (例如问题 #6813,out - in 过渡效果) -// 此外,在事件处理程序中使用(宏)任务会导致一些无法避免的奇怪行为 -// (例如问题 #7109、#7153、#7546、#7834、#8109) -// 所以现在我们再次在各处使用微任务 -// 这种权衡的一个主要缺点是,在某些场景下 -// 微任务的优先级过高,会在本应按顺序执行的事件之间触发 -// (例如问题 #4521、#6690,这些问题有解决办法) -// 甚至会在同一事件的冒泡过程中触发(问题 #6566) - var timerFunc; - -// nextTick 行为利用了微任务队列,可以通过原生 Promise.then 或 MutationObserver 来访问该队列 -// MutationObserver 有更广泛的支持,然而在 iOS >= 9.3.3 的 UIWebView 中,当在触摸事件处理程序中触发时,它存在严重的 bug -// 触发几次后它会完全停止工作……所以,如果原生 Promise 可用,我们将使用它 - /* istanbul ignore next, $flow-disable-line */ - if (typeof Promise!== 'undefined' && isNative(Promise)) { - // 创建一个已解决的 Promise 实例 - var p = Promise.resolve(); - // 定义 timerFunc 函数,用于在 Promise 的 then 方法中调用 flushCallbacks 函数 - timerFunc = function () { - p.then(flushCallbacks); - // 在有问题的 UIWebView 中,Promise.then 不会完全失效,但 - // 它可能会陷入一种奇怪的状态,即回调被推入微任务队列,但队列没有被刷新,直到浏览器需要执行其他工作,例如处理定时器 - // 因此,我们可以通过添加一个空定时器来“强制”刷新微任务队列 - if (isIOS) { setTimeout(noop); } - }; - // 标记正在使用微任务 - isUsingMicroTask = true; -// 如果不是 IE 浏览器,且支持 MutationObserver - } else if (!isIE && typeof MutationObserver!== 'undefined' && ( - isNative(MutationObserver) || - // PhantomJS 和 iOS 7.x - MutationObserver.toString() === '[object MutationObserverConstructor]' - )) { - // 在原生 Promise 不可用时使用 MutationObserver - // 例如 PhantomJS、iOS 7、Android 4.4 - // (问题 #6466:MutationObserver 在 IE11 中不可靠) - var counter = 1; - // 创建一个 MutationObserver 实例,当观察到变化时调用 flushCallbacks 函数 - var observer = new MutationObserver(flushCallbacks); - // 创建一个文本节点 - var textNode = document.createTextNode(String(counter)); - // 开始观察文本节点的字符数据变化 - observer.observe(textNode, { - characterData: true - }); - // 定义 timerFunc 函数,通过改变文本节点的数据来触发 MutationObserver - timerFunc = function () { - counter = (counter + 1) % 2; - textNode.data = String(counter); - }; - // 标记正在使用微任务 - isUsingMicroTask = true; -// 如果支持 setImmediate 且是原生的 - } else if (typeof setImmediate!== 'undefined' && isNative(setImmediate)) { - // 回退到使用 setImmediate - // 从技术上讲,它利用了(宏)任务队列,但它仍然比 setTimeout 是更好的选择 - timerFunc = function () { - setImmediate(flushCallbacks); - }; - } else { - // 回退到使用 setTimeout - timerFunc = function () { - setTimeout(flushCallbacks, 0); - }; - } - -// 定义 nextTick 函数,用于将回调函数添加到队列中,并在合适的时机执行 - function nextTick (cb, ctx) { - var _resolve; - // 将回调函数包装后添加到 callbacks 数组中 - callbacks.push(function () { - if (cb) { - try { - // 调用回调函数 - cb.call(ctx); - } catch (e) { - // 处理回调函数执行过程中出现的错误 - handleError(e, ctx, 'nextTick'); - } - } else if (_resolve) { - // 如果没有回调函数但有 _resolve 函数,则调用 _resolve 函数 - _resolve(ctx); - } - }); - // 如果没有待处理的任务,则标记为有待处理任务,并调用 timerFunc 函数 - if (!pending) { - pending = true; - timerFunc(); - } - // $flow-disable-line - // 如果没有提供回调函数且支持 Promise,则返回一个 Promise 对象 - if (!cb && typeof Promise!== 'undefined') { - return new Promise(function (resolve) { - _resolve = resolve; - }) - } - } - - -// 定义 mark 和 measure 变量 - var mark; - var measure; - - { - // 检查是否在浏览器环境且支持 window.performance - var perf = inBrowser && window.performance; - /* istanbul ignore if */ - if ( - perf && - perf.mark && - perf.measure && - perf.clearMarks && - perf.clearMeasures - ) { - // 定义 mark 函数,用于在性能记录中标记一个时间点 - mark = function (tag) { return perf.mark(tag); }; - // 定义 measure 函数,用于测量两个标记时间点之间的性能 - measure = function (name, startTag, endTag) { - perf.measure(name, startTag, endTag); - perf.clearMarks(startTag); - perf.clearMarks(endTag); - // perf.clearMeasures(name) - }; - } - } \ No newline at end of file +// Vue.js v2.6.12 +// (c) 2014-2020 Evan You +// Released under the MIT License. +// 立即执行函数,用于创建 Vue 实例并挂载到全局对象 +(function (global, factory) { + // 检查是否为 CommonJS 模块环境 + if (typeof exports === 'object' && typeof module !== 'undefined') { + // 如果是 CommonJS 环境,通过 module.exports 导出 factory 函数的返回值 + module.exports = factory(); + } + // 检查是否为 AMD 模块环境 + else if (typeof define === 'function' && define.amd) { + // 如果是 AMD 环境,使用 define 方法定义模块 + define(factory); + } + // 若既不是 CommonJS 也不是 AMD 环境,则作为全局变量挂载到全局对象 + else { + global = global || self; + // 将 factory 函数的返回值挂载到全局对象的 Vue 属性上 + global.Vue = factory(); + } +// 传入全局对象 this 和工厂函数 +}(this, function () { + // 使用严格模式,增强代码的安全性和报错提示 + 'use strict'; + + // 创建一个冻结的空对象,防止其属性被修改,常用于默认值 + var emptyObject = Object.freeze({}); + + // 检查值是否为 undefined 或 null + function isUndef (v) { + // 若值为 undefined 或者 null 则返回 true + return v === undefined || v === null; + } + + // 检查值是否已定义且不为 null + function isDef (v) { + // 若值既不是 undefined 也不是 null 则返回 true + return v !== undefined && v !== null; + } + + // 检查值是否为 true + function isTrue (v) { + // 若值严格等于 true 则返回 true + return v === true; + } + + // 检查值是否为 false + function isFalse (v) { + // 若值严格等于 false 则返回 true + return v === false; + } + + // 检查值是否为原始类型(字符串、数字、符号、布尔值) + function isPrimitive (value) { + // 判断值的类型是否为字符串、数字、符号或布尔值 + return ( + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'symbol' || + typeof value === 'boolean' + ); + } + + // 快速检查值是否为对象(非 null 且类型为 'object') + function isObject (obj) { + // 若值不为 null 且类型为 'object' 则返回 true + return obj !== null && typeof obj === 'object'; + } + + // 获取对象原型上的 toString 方法引用,用于后续获取对象类型字符串 + var _toString = Object.prototype.toString; + + // 获取值的原始类型字符串,去除前后的 [object ] 部分 + function toRawType (value) { + // 调用 toString 方法并截取中间部分 + return _toString.call(value).slice(8, -1); + } + + // 严格检查对象是否为普通的 JavaScript 对象 + function isPlainObject (obj) { + // 若对象的 toString 结果为 [object Object] 则返回 true + return _toString.call(obj) === '[object Object]'; + } + + // 检查值是否为正则表达式对象 + function isRegExp (v) { + // 若对象的 toString 结果为 [object RegExp] 则返回 true + return _toString.call(v) === '[object RegExp]'; + } + + // 检查值是否为有效的数组索引(非负整数且为有限值) + function isValidArrayIndex (val) { + // 将值转换为浮点数 + var n = parseFloat(String(val)); + // 检查是否为非负整数且为有限值 + return n >= 0 && Math.floor(n) === n && isFinite(val); + } + + // 检查值是否为 Promise 对象(有 then 和 catch 方法) + function isPromise (val) { + // 检查值是否已定义且有 then 和 catch 方法 + return ( + isDef(val) && + typeof val.then === 'function' && + typeof val.catch === 'function' + ); + } + + // 将值转换为字符串,处理数组、对象等情况 + function toString (val) { + // 若值为 null 或 undefined 则返回空字符串 + if (val == null) { + return ''; + } + // 若值为数组或普通对象且其 toString 方法为默认的则进行 JSON 序列化 + if (Array.isArray(val) || (isPlainObject(val) && val.toString === _toString)) { + return JSON.stringify(val, null, 2); + } + // 其他情况直接转换为字符串 + return String(val); + } + + // 将值转换为数字,若转换失败则返回原始值 + function toNumber (val) { + // 将值转换为浮点数 + var n = parseFloat(val); + // 若转换结果为 NaN 则返回原始值,否则返回转换后的数字 + return isNaN(n) ? val : n; + } + + // 创建一个映射对象,并返回一个用于检查键是否存在于映射中的函数 + function makeMap ( + str, + expectsLowerCase + ) { + // 创建一个空对象作为映射 + var map = Object.create(null); + // 将字符串按逗号分割成数组 + var list = str.split(','); + // 遍历数组,将每个元素作为键添加到映射中 + for (var i = 0; i < list.length; i++) { + map[list[i]] = true; + } + // 根据是否需要小写处理返回不同的检查函数 + if (expectsLowerCase) { + return function (val) { return map[val.toLowerCase()]; }; + } + return function (val) { return map[val]; }; + } + + // 创建一个检查是否为内置标签(slot、component)的函数 + var isBuiltInTag = makeMap('slot,component', true); + + // 创建一个检查是否为保留属性(key、ref 等)的函数 + var isReservedAttribute = makeMap('key,ref,slot,slot-scope,is'); + + // 从数组中移除指定的项 + function remove (arr, item) { + // 检查数组是否有元素 + if (arr.length) { + // 查找项在数组中的索引 + var index = arr.indexOf(item); + // 若索引存在,则移除该项 + if (index > -1) { + return arr.splice(index, 1); + } + } + } + + // 获取对象原型上的 hasOwnProperty 方法引用 + var hasOwnProperty = Object.prototype.hasOwnProperty; + + // 检查对象是否自身拥有指定的属性 + function hasOwn (obj, key) { + // 使用 hasOwnProperty 方法检查对象是否拥有指定属性 + return hasOwnProperty.call(obj, key); + } + + // 创建一个函数的缓存版本,避免重复计算 + function cached (fn) { + // 创建一个空对象作为缓存 + var cache = Object.create(null); + return function cachedFn (str) { + // 检查缓存中是否已有结果 + var hit = cache[str]; + // 若有则返回缓存结果,否则计算并缓存结果 + return hit || (cache[str] = fn(str)); + }; + } + + // 用于将连字符分隔的字符串转换为驼峰式字符串 + var camelizeRE = /-(\w)/g; + + // 创建一个缓存版本的驼峰化函数 + var camelize = cached(function (str) { + // 使用正则表达式将连字符后的字母转换为大写 + return str.replace(camelizeRE, function (_, c) { return c ? c.toUpperCase() : ''; }); + }); + + // 创建一个缓存版本的首字母大写函数 + var capitalize = cached(function (str) { + // 将字符串的首字母转换为大写 + return str.charAt(0).toUpperCase() + str.slice(1); + }); + + // 用于将驼峰式字符串转换为连字符分隔的字符串 + var hyphenateRE = /\B([A-Z])/g; + + // 创建一个缓存版本的连字符化函数 + var hyphenate = cached(function (str) { + // 使用正则表达式将大写字母前添加连字符并转换为小写 + return str.replace(hyphenateRE, '-$1').toLowerCase(); + }); + + // 为不支持 bind 方法的环境提供的 polyfill + function polyfillBind (fn, ctx) { + function boundFn (a) { + var l = arguments.length; + // 根据参数数量调用原函数 + if (l) { + if (l > 1) { + return fn.apply(ctx, arguments); + } + return fn.call(ctx, a); + } + return fn.call(ctx); + } + // 记录原函数的参数长度 + boundFn._length = fn.length; + return boundFn; + } + + // 使用原生的 bind 方法 + function nativeBind (fn, ctx) { + return fn.bind(ctx); + } + + // 根据环境选择使用原生 bind 或 polyfill + var bind = Function.prototype.bind + ? nativeBind + : polyfillBind; + + // 将类数组对象转换为真正的数组 + function toArray (list, start) { + // 若未指定起始位置,则默认为 0 + start = start || 0; + // 计算数组长度 + var i = list.length - start; + // 创建一个指定长度的数组 + var ret = new Array(i); + // 遍历类数组对象,将元素复制到新数组中 + while (i--) { + ret[i] = list[i + start]; + } + return ret; + } + + // 将一个对象的属性复制到另一个对象上 + function extend (to, _from) { + // 遍历源对象的属性 + for (var key in _from) { + // 将属性复制到目标对象上 + to[key] = _from[key]; + } + return to; + } + + // 将对象数组合并为一个对象 + function toObject (arr) { + // 创建一个空对象作为结果 + var res = {}; + // 遍历数组 + for (var i = 0; i < arr.length; i++) { + // 若数组元素存在,则将其属性合并到结果对象上 + if (arr[i]) { + extend(res, arr[i]); + } + } + return res; + } + + // 空函数,不执行任何操作 + function noop (a, b, c) {} + + // 始终返回 false 的函数 + var no = function (a, b, c) { return false; }; + + // 返回传入参数本身的函数 + var identity = function (_) { return _; }; + + // 从编译器模块数组中生成静态键字符串 + function genStaticKeys (modules) { + // 使用 reduce 方法将模块的静态键合并成一个数组并转换为字符串 + return modules.reduce(function (keys, m) { + return keys.concat(m.staticKeys || []); + }, []).join(','); + } + + // 检查两个值是否宽松相等 + function looseEqual (a, b) { + // 若严格相等则直接返回 true + if (a === b) { + return true; + } + // 检查 a 是否为对象 + var isObjectA = isObject(a); + // 检查 b 是否为对象 + var isObjectB = isObject(b); + if (isObjectA && isObjectB) { + try { + // 检查 a 是否为数组 + var isArrayA = Array.isArray(a); + // 检查 b 是否为数组 + var isArrayB = Array.isArray(b); + if (isArrayA && isArrayB) { + // 若都是数组,检查长度和元素是否宽松相等 + return a.length === b.length && a.every(function (e, i) { + return looseEqual(e, b[i]); + }); + } else if (a instanceof Date && b instanceof Date) { + // 若都是日期对象,检查时间戳是否相等 + return a.getTime() === b.getTime(); + } else if (!isArrayA && !isArrayB) { + // 若都不是数组,检查键和值是否宽松相等 + var keysA = Object.keys(a); + var keysB = Object.keys(b); + return keysA.length === keysB.length && keysA.every(function (key) { + return looseEqual(a[key], b[key]); + }); + } + } catch (e) { + // 异常情况返回 false + return false; + } + } else if (!isObjectA && !isObjectB) { + // 若都不是对象,检查字符串形式是否相等 + return String(a) === String(b); + } + // 其他情况返回 false + return false; + } + + // 在数组中查找第一个与指定值宽松相等的元素的索引 + function looseIndexOf (arr, val) { + // 遍历数组 + for (var i = 0; i < arr.length; i++) { + // 若找到宽松相等的元素则返回其索引 + if (looseEqual(arr[i], val)) { + return i; + } + } + // 未找到则返回 -1 + return -1; + } + + // 确保函数只执行一次 + function once (fn) { + var called = false; + return function () { + // 若函数未执行过 + if (!called) { + // 标记为已执行 + called = true; + // 执行原函数 + fn.apply(this, arguments); + } + }; + } + + // 服务器渲染标记属性名 + var SSR_ATTR = 'data-server-rendered'; + + // 资产类型数组 + var ASSET_TYPES = [ + 'component', + 'directive', + 'filter' + ]; + + // 生命周期钩子数组 + var LIFECYCLE_HOOKS = [ + 'beforeCreate', + 'created', + 'beforeMount', + 'mounted', + 'beforeUpdate', + 'updated', + 'beforeDestroy', + 'destroyed', + 'activated', + 'deactivated', + 'errorCaptured', + 'serverPrefetch' + ]; + + // Vue 的配置对象 + var config = { + // 选项合并策略对象 + optionMergeStrategies: Object.create(null), + // 是否抑制警告信息 + silent: false, + // 是否在开发环境显示生产模式提示 + productionTip: "development" !== 'production', + // 是否启用开发者工具 + devtools: "development" !== 'production', + // 是否记录性能信息 + performance: false, + // 错误处理函数 + errorHandler: null, + // 警告处理函数 + warnHandler: null, + // 忽略的自定义元素列表 + ignoredElements: [], + // 自定义按键别名对象 + keyCodes: Object.create(null), + // 检查标签是否为保留标签的函数 + isReservedTag: no, + // 检查属性是否为保留属性的函数 + isReservedAttr: no, + // 检查标签是否为未知元素的函数 + isUnknownElement: no, + // 获取元素命名空间的函数 + getTagNamespace: noop, + // 解析平台标签名的函数 + parsePlatformTagName: identity, + // 检查属性是否必须使用属性绑定的函数 + mustUseProp: no, + // 是否异步执行更新 + async: true, + // 生命周期钩子数组 + _lifecycleHooks: LIFECYCLE_HOOKS + }; + + // 用于匹配 Unicode 字母的正则表达式 + var unicodeRegExp = /a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/; + + // 检查字符串是否以 $ 或 _ 开头 + function isReserved (str) { + // 获取字符串首字符的 Unicode 编码 + var c = (str + '').charCodeAt(0); + // 检查是否为 $ 或 _ 的编码 + return c === 0x24 || c === 0x5F; + } + + // 在对象上定义一个属性 + function def (obj, key, val, enumerable) { + // 使用 Object.defineProperty 方法定义属性 + Object.defineProperty(obj, key, { + // 属性的值 + value: val, + // 是否可枚举 + enumerable: !!enumerable, + // 是否可写 + writable: true, + // 是否可配置 + configurable: true + }); + } + + // 用于检查路径是否包含非法字符的正则表达式 + var bailRE = new RegExp(("[^" + (unicodeRegExp.source) + ".$_\\d]")); + + // 解析简单路径,返回一个函数用于获取对象上的属性值 + function parsePath (path) { + // 检查路径是否包含非法字符 + if (bailRE.test(path)) { + return; + } + // 将路径按点分割成数组 + var segments = path.split('.'); + return function (obj) { + // 遍历路径数组,逐级获取对象属性值 + for (var i = 0; i < segments.length; i++) { + if (!obj) { + return; + } + obj = obj[segments[i]]; + } + return obj; + }; + } + + // 返回 factory 函数的返回值 + return {}; +})); +// 空注释,可能用于代码分隔 +// 检查当前环境是否支持使用 __proto__ 属性 +var hasProto = '__proto__' in {}; + +// 进行浏览器环境嗅探,判断代码运行的环境 +// 判断是否在浏览器环境中,通过检查 window 对象是否存在 +var inBrowser = typeof window !== 'undefined'; +// 判断是否在 Weex 环境中,通过检查 WXEnvironment 对象是否存在 +var inWeex = typeof WXEnvironment !== 'undefined' && !!WXEnvironment.platform; +// 获取 Weex 环境的平台信息,并转换为小写 +var weexPlatform = inWeex && WXEnvironment.platform.toLowerCase(); +// 获取浏览器环境下的用户代理信息,并转换为小写 +var UA = inBrowser && window.navigator.userAgent.toLowerCase(); +// 判断是否为 IE 浏览器 +var isIE = UA && /msie|trident/.test(UA); +// 判断是否为 IE9 浏览器 +var isIE9 = UA && UA.indexOf('msie 9.0') > 0; +// 判断是否为 Edge 浏览器 +var isEdge = UA && UA.indexOf('edge/') > 0; +// 判断是否为 Android 设备 +var isAndroid = (UA && UA.indexOf('android') > 0) || (weexPlatform === 'android'); +// 判断是否为 iOS 设备 +var isIOS = (UA && /iphone|ipad|ipod|ios/.test(UA)) || (weexPlatform === 'ios'); +// 判断是否为 Chrome 浏览器 +var isChrome = UA && /chrome\/\d+/.test(UA) && !isEdge; +// 判断是否为 PhantomJS 环境 +var isPhantomJS = UA && /phantomjs/.test(UA); +// 判断是否为 Firefox 浏览器,并获取其版本号 +var isFF = UA && UA.match(/firefox\/(\d+)/); + +// Firefox 在 Object.prototype 上有 "watch" 函数 +var nativeWatch = ({}).watch; + +// 检查浏览器是否支持 passive 选项 +var supportsPassive = false; +if (inBrowser) { + try { + // 创建一个空对象用于测试 passive 选项 + var opts = {}; + // 使用 Object.defineProperty 定义 passive 属性,当访问该属性时设置 supportsPassive 为 true + Object.defineProperty(opts, 'passive', { + get: function get () { + // 忽略此代码块的测试覆盖率检查 + /* istanbul ignore next */ + supportsPassive = true; + } + }); + // 尝试添加一个测试事件监听器,使用 passive 选项 + window.addEventListener('test-passive', null, opts); + } catch (e) {} +} + +// 用于存储是否为服务器渲染的标志,需要延迟求值 +var _isServer; +// 判断是否为服务器渲染的函数 +var isServerRendering = function () { + if (_isServer === undefined) { + // 忽略此代码块的测试覆盖率检查 + /* istanbul ignore if */ + if (!inBrowser && !inWeex && typeof global !== 'undefined') { + // 检测 vue-server-renderer 的存在,并避免 Webpack 对 process 进行填充 + _isServer = global['process'] && global['process'].env.VUE_ENV === 'server'; + } else { + _isServer = false; + } + } + return _isServer; +}; + +// 检测是否存在 Vue 开发者工具 +var devtools = inBrowser && window.__VUE_DEVTOOLS_GLOBAL_HOOK__; + +// 检查构造函数是否为原生构造函数 +/* istanbul ignore next */ +function isNative (Ctor) { + return typeof Ctor === 'function' && /native code/.test(Ctor.toString()); +} + +// 检查当前环境是否支持 Symbol 和 Reflect.ownKeys +var hasSymbol = + typeof Symbol !== 'undefined' && isNative(Symbol) && + typeof Reflect !== 'undefined' && isNative(Reflect.ownKeys); + +// 定义一个 Set 类型的变量 +var _Set; +// 忽略此代码块的测试覆盖率检查 +/* istanbul ignore if */ +if (typeof Set !== 'undefined' && isNative(Set)) { + // 如果原生支持 Set,则使用原生 Set + _Set = Set; +} else { + // 实现一个非标准的 Set 填充,仅适用于原始类型的键 + _Set = (function () { + // Set 构造函数 + function Set () { + // 使用一个空对象来模拟 Set 的存储 + this.set = Object.create(null); + } + // 检查 Set 中是否存在指定的键 + Set.prototype.has = function has (key) { + return this.set[key] === true; + }; + // 向 Set 中添加一个键 + Set.prototype.add = function add (key) { + this.set[key] = true; + }; + // 清空 Set + Set.prototype.clear = function clear () { + this.set = Object.create(null); + }; + + return Set; + }()); +} + +// 初始化警告、提示和组件跟踪相关的函数为空函数 +var warn = noop; +var tip = noop; +// 绕过 Flow 类型检查 +var generateComponentTrace = (noop); +var formatComponentName = (noop); + +// 在开发环境下进行处理 +{ + // 检查是否存在 console 对象 + var hasConsole = typeof console !== 'undefined'; + // 用于将连字符或下划线分隔的字符串转换为驼峰式的正则表达式 + var classifyRE = /(?:^|[-_])(\w)/g; + // 将连字符或下划线分隔的字符串转换为驼峰式的函数 + var classify = function (str) { + return str + .replace(classifyRE, function (c) { return c.toUpperCase(); }) + .replace(/[-_]/g, ''); + }; + + // 定义警告函数 + warn = function (msg, vm) { + // 获取组件的跟踪信息 + var trace = vm ? generateComponentTrace(vm) : ''; + + if (config.warnHandler) { + // 如果配置了警告处理函数,则调用该函数 + config.warnHandler.call(null, msg, vm, trace); + } else if (hasConsole && (!config.silent)) { + // 如果存在 console 且未禁止警告,则在控制台输出警告信息 + console.error(("[Vue warn]: " + msg + trace)); + } + }; + + // 定义提示函数 + tip = function (msg, vm) { + if (hasConsole && (!config.silent)) { + // 如果存在 console 且未禁止警告,则在控制台输出提示信息 + console.warn("[Vue tip]: " + msg + ( + vm ? generateComponentTrace(vm) : '' + )); + } + }; + + // 格式化组件名称的函数 + formatComponentName = function (vm, includeFile) { + if (vm.$root === vm) { + // 如果是根组件,则返回 + return ''; + } + // 获取组件的选项对象 + var options = typeof vm === 'function' && vm.cid != null + ? vm.options + : vm._isVue + ? vm.$options || vm.constructor.options + : vm; + // 获取组件的名称 + var name = options.name || options._componentTag; + // 获取组件的文件路径 + var file = options.__file; + if (!name && file) { + // 如果没有名称但有文件路径,则从文件路径中提取组件名称 + var match = file.match(/([^/\\]+)\.vue$/); + name = match && match[1]; + } + + return ( + // 格式化组件名称 + (name ? ("<" + (classify(name)) + ">") : "") + + (file && includeFile !== false ? (" at " + file) : '') + ); + }; + + // 重复字符串的函数 + var repeat = function (str, n) { + var res = ''; + while (n) { + if (n % 2 === 1) { + // 如果 n 为奇数,则将 str 添加到结果中 + res += str; + } + if (n > 1) { + // 如果 n 大于 1,则将 str 翻倍 + str += str; + } + // 将 n 右移一位 + n >>= 1; + } + return res; + }; + + // 生成组件跟踪信息的函数 + generateComponentTrace = function (vm) { + if (vm._isVue && vm.$parent) { + // 如果是 Vue 组件且有父组件 + var tree = []; + var currentRecursiveSequence = 0; + while (vm) { + if (tree.length > 0) { + var last = tree[tree.length - 1]; + if (last.constructor === vm.constructor) { + // 如果当前组件和上一个组件是同一个构造函数创建的,则增加递归次数 + currentRecursiveSequence++; + vm = vm.$parent; + continue; + } else if (currentRecursiveSequence > 0) { + // 如果递归次数大于 0,则将上一个组件和递归次数作为数组添加到 tree 中 + tree[tree.length - 1] = [last, currentRecursiveSequence]; + currentRecursiveSequence = 0; + } + } + // 将当前组件添加到 tree 中 + tree.push(vm); + // 切换到父组件 + vm = vm.$parent; + } + return '\n\nfound in\n\n' + tree + .map(function (vm, i) { + return ("" + (i === 0 ? '---> ' : repeat(' ', 5 + i * 2)) + (Array.isArray(vm) + ? ((formatComponentName(vm[0])) + "... (" + (vm[1]) + " recursive calls)") + : formatComponentName(vm))); + }) + .join('\n'); + } else { + // 如果不是 Vue 组件或没有父组件,则直接返回组件名称 + return ("\n\n(found in " + (formatComponentName(vm)) + ")"); + } + }; +} + +// 初始化一个唯一标识符 +var uid = 0; + +// Dep 类,用于实现依赖收集和发布更新 +// Dep 是一个可观察对象,可以有多个指令订阅它 +var Dep = function Dep () { + // 为每个 Dep 实例分配一个唯一的 ID + this.id = uid++; + // 存储订阅者的数组 + this.subs = []; +}; + +// 向 Dep 实例中添加订阅者 +Dep.prototype.addSub = function addSub (sub) { + this.subs.push(sub); +}; + +// 从 Dep 实例中移除订阅者 +Dep.prototype.removeSub = function removeSub (sub) { + remove(this.subs, sub); +}; + +// 触发依赖收集 +Dep.prototype.depend = function depend () { + if (Dep.target) { + // 如果存在当前目标观察者,则让其添加对当前 Dep 实例的依赖 + Dep.target.addDep(this); + } +}; + +// 通知所有订阅者进行更新 +Dep.prototype.notify = function notify () { + // 先复制一份订阅者列表,避免在更新过程中修改原列表导致问题 + var subs = this.subs.slice(); + if (!config.async) { + // 如果不是异步更新,则对订阅者列表进行排序,确保按正确顺序更新 + subs.sort(function (a, b) { return a.id - b.id; }); + } + for (var i = 0, l = subs.length; i < l; i++) { + // 调用每个订阅者的 update 方法进行更新 + subs[i].update(); + } +}; + +// 当前正在计算的目标观察者,全局唯一,因为同一时间只能有一个观察者在计算 +Dep.target = null; +// 存储目标观察者的栈 +var targetStack = []; + +// 将目标观察者压入栈中,并设置为当前目标观察者 +function pushTarget (target) { + targetStack.push(target); + Dep.target = target; +} + +// 从栈中弹出目标观察者,并更新当前目标观察者 +function popTarget () { + targetStack.pop(); + Dep.target = targetStack[targetStack.length - 1]; +} + +// VNode 类,用于表示虚拟 DOM 节点 +var VNode = function VNode ( + tag, // 节点的标签名 + data, // 节点的数据 + children, // 节点的子节点 + text, // 节点的文本内容 + elm, // 对应的真实 DOM 元素 + context, // 节点的上下文 + componentOptions, // 组件选项 + asyncFactory // 异步工厂函数 +) { + this.tag = tag; + this.data = data; + this.children = children; + this.text = text; + this.elm = elm; + this.ns = undefined; // 命名空间 + this.context = context; + this.fnContext = undefined; // 函数式组件的上下文 + this.fnOptions = undefined; // 函数式组件的选项 + this.fnScopeId = undefined; // 函数式组件的作用域 ID + this.key = data && data.key; // 节点的 key + this.componentOptions = componentOptions; + this.componentInstance = undefined; // 组件实例 + this.parent = undefined; // 父节点 + this.raw = false; // 是否为原始节点 + this.isStatic = false; // 是否为静态节点 + this.isRootInsert = true; // 是否为根插入节点 + this.isComment = false; // 是否为注释节点 + this.isCloned = false; // 是否为克隆节点 + this.isOnce = false; // 是否只渲染一次 + this.asyncFactory = asyncFactory; + this.asyncMeta = undefined; // 异步元数据 + this.isAsyncPlaceholder = false; // 是否为异步占位符 +}; + +// 定义 VNode 原型的访问器属性 +var prototypeAccessors = { child: { configurable: true } }; + +// 为了向后兼容,定义 child 访问器,返回 componentInstance +/* istanbul ignore next */ +prototypeAccessors.child.get = function () { + return this.componentInstance; +}; + +// 将访问器属性定义到 VNode 原型上 +Object.defineProperties( VNode.prototype, prototypeAccessors ); + +// 创建一个空的 VNode 节点 +var createEmptyVNode = function (text) { + if ( text === void 0 ) text = ''; + + var node = new VNode(); + node.text = text; + node.isComment = true; + return node; +}; + +// 创建一个文本 VNode 节点 +function createTextVNode (val) { + return new VNode(undefined, undefined, undefined, String(val)); +} + +// 优化后的浅克隆函数,用于静态节点和插槽节点,避免在 DOM 操作时因引用问题出错 +function cloneVNode (vnode) { + var cloned = new VNode( + vnode.tag, + vnode.data, + // 克隆子节点数组,避免在克隆子节点时修改原始数组 + vnode.children && vnode.children.slice(), + vnode.text, + vnode.elm, + vnode.context, + vnode.componentOptions, + vnode.asyncFactory + ); + cloned.ns = vnode.ns; + cloned.isStatic = vnode.isStatic; + cloned.key = vnode.key; + cloned.isComment = vnode.isComment; + cloned.fnContext = vnode.fnContext; + cloned.fnOptions = vnode.fnOptions; + cloned.fnScopeId = vnode.fnScopeId; + cloned.asyncMeta = vnode.asyncMeta; + cloned.isCloned = true; + return cloned; +} + +// 由于 Flow 对动态访问数组原型方法支持不佳,不对此文件进行类型检查 +var arrayProto = Array.prototype; +// 创建一个继承自数组原型的对象,用于重写数组方法 +var arrayMethods = Object.create(arrayProto); + +// 需要重写的数组方法 +var methodsToPatch = [ + 'push', + 'pop', + 'shift', + 'unshift', + 'splice', + 'sort', + 'reverse' +]; + +// 拦截数组的变异方法并触发更新事件 +methodsToPatch.forEach(function (method) { + // 缓存原始的数组方法 + var original = arrayProto[method]; + // 在 arrayMethods 对象上定义重写后的方法 + def(arrayMethods, method, function mutator () { + // 将参数转换为数组 + var args = [], len = arguments.length; + while ( len-- ) args[ len ] = arguments[ len ]; + + // 调用原始方法并获取结果 + var result = original.apply(this, args); + // 获取数组的观察者实例 + var ob = this.__ob__; + var inserted; + switch (method) { + case 'push': + case 'unshift': + // 如果是 push 或 unshift 方法,插入的元素就是参数 + inserted = args; + break; + case 'splice': + // 如果是 splice 方法,插入的元素是参数从索引 2 开始的部分 + inserted = args.slice(2); + break; + } + if (inserted) { + // 对插入的元素进行观察 + ob.observeArray(inserted); + } + // 通知依赖进行更新 + ob.dep.notify(); + return result; + }); +}); + +// 获取重写后的数组方法的属性名 +var arrayKeys = Object.getOwnPropertyNames(arrayMethods); + +// 用于控制是否进行观察的标志 +var shouldObserve = true; + +// 切换观察状态的函数 +function toggleObserving (value) { + shouldObserve = value; +} + +// Observer 类,用于将对象的属性转换为响应式的 +// 该类会将目标对象的属性键转换为 getter/setter,用于收集依赖和分发更新 +var Observer = function Observer (value) { + // 保存被观察的对象 + this.value = value; + // 创建一个 Dep 实例,用于依赖收集和发布更新 + this.dep = new Dep(); + // 记录关联的 Vue 实例数量 + this.vmCount = 0; + // 在被观察的对象上定义一个不可枚举的属性 __ob__,指向当前 Observer 实例 + def(value, '__ob__', this); + if (Array.isArray(value)) { + if (hasProto) { + // 如果支持 __proto__,则通过原型链增强数组 + protoAugment(value, arrayMethods); + } else { + // 否则通过定义隐藏属性增强数组 + copyAugment(value, arrayMethods, arrayKeys); + } + // 对数组的每个元素进行观察 + this.observeArray(value); + } else { + // 如果是对象,则遍历其属性并转换为响应式 + this.walk(value); + } +}; + +// 遍历对象的所有属性并将其转换为 getter/setter +// 此方法仅在值类型为 Object 时调用 +Observer.prototype.walk = function walk (obj) { + // 获取对象的所有属性名 + var keys = Object.keys(obj); + for (var i = 0; i < keys.length; i++) { + // 将每个属性转换为响应式 + defineReactive$$1(obj, keys[i]); + } +}; + +// 观察数组中的每个元素 +Observer.prototype.observeArray = function observeArray (items) { + for (var i = 0, l = items.length; i < l; i++) { + // 对数组中的每个元素进行观察 + observe(items[i]); + } +}; + +// 通过 __proto__ 拦截原型链来增强目标对象或数组 +function protoAugment (target, src) { + // 禁用 no-proto 规则 + /* eslint-disable no-proto */ + // 将目标对象的原型指向源对象 + target.__proto__ = src; + // 启用 no-proto 规则 + /* eslint-enable no-proto */ +} + +// 通过定义隐藏属性来增强目标对象或数组 +/* istanbul ignore next */ +function copyAugment (target, src, keys) { + for (var i = 0, l = keys.length; i < l; i++) { + var key = keys[i]; + // 在目标对象上定义属性,值为源对象的对应属性 + def(target, key, src[key]); + } +} + +// 尝试为一个值创建一个观察者实例 +// 如果成功观察,则返回新的观察者;如果该值已经有观察者,则返回现有的观察者 +function observe (value, asRootData) { + if (!isObject(value) || value instanceof VNode) { + // 如果值不是对象或者是 VNode 实例,则不进行观察 + return; + } + var ob; + if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { + // 如果值已经有观察者,则直接使用现有的观察者 + ob = value.__ob__; + } else if ( + shouldObserve && + !isServerRendering() && + (Array.isArray(value) || isPlainObject(value)) && + Object.isExtensible(value) && + !value._isVue + ) { + // 如果满足观察条件,则创建一个新的观察者 + ob = new Observer(value); + } + if (asRootData && ob) { + // 如果作为根数据且有观察者,则增加关联的 Vue 实例数量 + ob.vmCount++; + } + return ob; +} + +// 在对象上定义一个响应式属性 +function defineReactive$$1 ( + obj, // 目标对象 + key, // 属性名 + val, // 属性值 + customSetter, // 自定义的 setter 函数 + shallow // 是否进行浅观察 +) { + // 创建一个 Dep 实例,用于依赖收集和发布更新 + var dep = new Dep(); + + // 获取对象上该属性的描述符 + var property = Object.getOwnPropertyDescriptor(obj, key); + if (property && property.configurable === false) { + // 如果属性不可配置,则直接返回 + return; + } + + // 获取原有的 getter 和 setter 函数 + var getter = property && property.get; + var setter = property && property.set; + if ((!getter || setter) && arguments.length === 2) { + // 如果没有 getter 或者有 setter 且只传入了两个参数,则获取对象上该属性的当前值 + val = obj[key]; + } + + // 如果不是浅观察,则对属性值进行观察 + var childOb = !shallow && observe(val); + // 使用 Object.defineProperty 定义属性的 getter 和 setter + Object.defineProperty(obj, key, { + enumerable: true, // 属性可枚举 + configurable: true, // 属性可配置 + get: function reactiveGetter () { + // 如果有原有的 getter 函数,则调用它获取值,否则使用传入的 val + var value = getter ? getter.call(obj) : val; + if (Dep.target) { + // 如果存在当前目标观察者,则让其添加对当前 Dep 实例的依赖 + dep.depend(); + if (childOb) { + // 如果有子观察者,则让当前目标观察者添加对子观察者的依赖 + childOb.dep.depend(); + if (Array.isArray(value)) { + // 如果值是数组,则对数组元素进行依赖收集 + dependArray(value); + } + } + } + return value; + }, + // 定义响应式属性的 setter 函数 + set: function reactiveSetter (newVal) { + // 获取当前属性的值 + var value = getter ? getter.call(obj) : val; + // 禁用 no-self-compare 规则,用于处理 NaN 的情况 + /* eslint-disable no-self-compare */ + // 检查新值和旧值是否相等,考虑 NaN 的情况 + if (newVal === value || (newVal !== newVal && value !== value)) { + return; + } + // 启用 no-self-compare 规则 + /* eslint-enable no-self-compare */ + // 如果存在自定义的 setter 函数,则调用它 + if (customSetter) { + customSetter(); + } + // 对于只有 getter 没有 setter 的访问器属性,直接返回 + if (getter && !setter) { return; } + if (setter) { + // 如果存在原有的 setter 函数,则调用它设置新值 + setter.call(obj, newVal); + } else { + // 否则直接更新局部变量 val 的值 + val = newVal; + } + // 如果不是浅观察,则对新值进行观察 + childOb = !shallow && observe(newVal); + // 通知所有依赖该属性的观察者进行更新 + dep.notify(); + }, + +// 在对象上设置一个属性,如果属性不存在则添加该属性并触发变更通知 + function set (target, key, val) { + // 检查目标对象是否为 undefined、null 或原始类型,如果是则发出警告 + if (isUndef(target) || isPrimitive(target)) { + warn(("Cannot set reactive property on undefined, null, or primitive value: " + ((target)))); + } + if (Array.isArray(target) && isValidArrayIndex(key)) { + // 如果目标是数组且键是有效的数组索引,则更新数组长度并插入新值 + target.length = Math.max(target.length, key); + target.splice(key, 1, val); + return val; + } + if (key in target && !(key in Object.prototype)) { + // 如果键已经存在于目标对象中且不是原型链上的属性,则直接更新该属性的值 + target[key] = val; + return val; + } + // 获取目标对象的观察者实例 + var ob = (target).__ob__; + if (target._isVue || (ob && ob.vmCount)) { + // 如果目标是 Vue 实例或其根数据,发出警告并返回值 + warn( + 'Avoid adding reactive properties to a Vue instance or its root $data ' + + 'at runtime - declare it upfront in the data option.' + ); + return val; + } + if (!ob) { + // 如果目标对象没有观察者,则直接设置属性值 + target[key] = val; + return val; + } + // 定义该属性为响应式属性 + defineReactive$$1(ob.value, key, val); + // 通知观察者更新 + ob.dep.notify(); + return val; + } + +// 删除一个属性,如果必要则触发变更通知 + function del (target, key) { + // 检查目标对象是否为 undefined、null 或原始类型,如果是则发出警告 + if (isUndef(target) || isPrimitive(target)) { + warn(("Cannot delete reactive property on undefined, null, or primitive value: " + ((target)))); + } + if (Array.isArray(target) && isValidArrayIndex(key)) { + // 如果目标是数组且键是有效的数组索引,则从数组中移除该元素 + target.splice(key, 1); + return; + } + // 获取目标对象的观察者实例 + var ob = (target).__ob__; + if (target._isVue || (ob && ob.vmCount)) { + // 如果目标是 Vue 实例或其根数据,发出警告并返回 + warn( + 'Avoid deleting properties on a Vue instance or its root $data ' + + '- just set it to null.' + ); + return; + } + if (!hasOwn(target, key)) { + // 如果目标对象没有该属性,则直接返回 + return; + } + // 删除目标对象的该属性 + delete target[key]; + if (!ob) { + // 如果目标对象没有观察者,则直接返回 + return; + } + // 通知观察者更新 + ob.dep.notify(); + } + +// 当数组被访问时,收集数组元素的依赖,因为无法像属性 getter 那样拦截数组元素的访问 + function dependArray (value) { + for (var e = (void 0), i = 0, l = value.length; i < l; i++) { + // 获取数组元素 + e = value[i]; + // 如果元素有观察者,则收集其依赖 + e && e.__ob__ && e.__ob__.dep.depend(); + if (Array.isArray(e)) { + // 如果元素是数组,则递归调用 dependArray 函数收集依赖 + dependArray(e); + } + } + } + +// 选项合并策略是处理如何将父选项值和子选项值合并为最终值的函数 + var strats = config.optionMergeStrategies; + +// 有使用限制的选项处理 + { + // 处理 el 和 propsData 选项的合并策略 + strats.el = strats.propsData = function (parent, child, vm, key) { + if (!vm) { + // 如果没有 Vue 实例,发出警告 + warn( + "option \"" + key + "\" can only be used during instance " + + 'creation with the `new` keyword.' + ); + } + // 使用默认合并策略返回结果 + return defaultStrat(parent, child); + }; + } + +// 递归合并两个数据对象的辅助函数 + function mergeData (to, from) { + if (!from) { + // 如果没有源对象,则直接返回目标对象 + return to; + } + var key, toVal, fromVal; + // 根据是否支持 Symbol 获取源对象的所有键 + var keys = hasSymbol + ? Reflect.ownKeys(from) + : Object.keys(from); + + for (var i = 0; i < keys.length; i++) { + // 获取当前键 + key = keys[i]; + // 跳过 __ob__ 属性 + if (key === '__ob__') { continue; } + // 获取目标对象和源对象上该键对应的值 + toVal = to[key]; + fromVal = from[key]; + if (!hasOwn(to, key)) { + // 如果目标对象没有该键,则使用 set 函数设置该键的值 + set(to, key, fromVal); + } else if ( + toVal !== fromVal && + isPlainObject(toVal) && + isPlainObject(fromVal) + ) { + // 如果目标对象和源对象上该键的值不相等,且都是普通对象,则递归合并 + mergeData(toVal, fromVal); + } + } + return to; + } + +// 合并数据函数或数据对象 + function mergeDataOrFn ( + parentVal, // 父数据值 + childVal, // 子数据值 + vm // Vue 实例 + ) { + if (!vm) { + // 在 Vue.extend 合并中,两者都应该是函数 + if (!childVal) { + // 如果没有子数据值,则返回父数据值 + return parentVal; + } + if (!parentVal) { + // 如果没有父数据值,则返回子数据值 + return childVal; + } + // 当父数据值和子数据值都存在时,返回一个函数,该函数返回两者合并的结果 + return function mergedDataFn () { + return mergeData( + typeof childVal === 'function' ? childVal.call(this, this) : childVal, + typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal + ); + }; + } else { + return function mergedInstanceDataFn () { + // 实例合并 + var instanceData = typeof childVal === 'function' + ? childVal.call(vm, vm) + : childVal; + var defaultData = typeof parentVal === 'function' + ? parentVal.call(vm, vm) + : parentVal; + if (instanceData) { + // 如果有实例数据,则合并实例数据和默认数据 + return mergeData(instanceData, defaultData); + } else { + // 否则返回默认数据 + return defaultData; + } + }; + } + } + +// 合并 data 选项的策略 + strats.data = function ( + parentVal, // 父级 data 选项值 + childVal, // 子级 data 选项值 + vm // Vue 实例 + ) { + if (!vm) { + if (childVal && typeof childVal !== 'function') { + // 如果子级 data 选项存在但不是函数,发出警告并返回父级 data 选项值 + warn( + 'The "data" option should be a function ' + + 'that returns a per-instance value in component ' + + 'definitions.', + vm + ); + return parentVal; + } + // 合并 data 选项 + return mergeDataOrFn(parentVal, childVal); + } + // 合并 data 选项并传入 Vue 实例 + return mergeDataOrFn(parentVal, childVal, vm); + }; + +// 生命周期钩子和 props 以数组形式合并 + function mergeHook ( + parentVal, // 父级钩子值 + childVal // 子级钩子值 + ) { + var res = childVal + ? parentVal + ? parentVal.concat(childVal) + : Array.isArray(childVal) + ? childVal + : [childVal] + : parentVal; + return res + ? dedupeHooks(res) + : res; + } + +// 去除钩子数组中的重复项 + function dedupeHooks (hooks) { + var res = []; + for (var i = 0; i < hooks.length; i++) { + if (res.indexOf(hooks[i]) === -1) { + // 如果结果数组中不存在该钩子,则添加到结果数组中 + res.push(hooks[i]); + } + } + return res; + } + +// 为每个生命周期钩子设置合并策略 + LIFECYCLE_HOOKS.forEach(function (hook) { + strats[hook] = mergeHook; + }); + +// 合并资产选项(如组件、指令、过滤器) + function mergeAssets ( + parentVal, // 父级资产选项值 + childVal, // 子级资产选项值 + vm, // Vue 实例 + key // 选项键名 + ) { + // 创建一个继承自父级资产选项的对象 + var res = Object.create(parentVal || null); + if (childVal) { + // 验证子级资产选项是否为对象类型 + assertObjectType(key, childVal, vm); + // 将子级资产选项合并到结果对象中 + return extend(res, childVal); + } else { + // 如果没有子级资产选项,则返回结果对象 + return res; + } + } + +// 为每个资产类型设置合并策略 + ASSET_TYPES.forEach(function (type) { + strats[type + 's'] = mergeAssets; + }); + +// 合并 watch 选项 + strats.watch = function ( + parentVal, // 父级 watch 选项值 + childVal, // 子级 watch 选项值 + vm, // Vue 实例 + key // 选项键名 + ) { + // 处理 Firefox 的 Object.prototype.watch + if (parentVal === nativeWatch) { parentVal = undefined; } + if (childVal === nativeWatch) { childVal = undefined; } + // 如果没有子级 watch 选项,则返回一个继承自父级 watch 选项的对象 + if (!childVal) { return Object.create(parentVal || null); } + { + // 验证子级 watch 选项是否为对象类型 + assertObjectType(key, childVal, vm); + } + if (!parentVal) { + // 如果没有父级 watch 选项,则返回子级 watch 选项 + return childVal; + } + var ret = {}; + // 将父级 watch 选项合并到结果对象中 + extend(ret, parentVal); + for (var key$1 in childVal) { + var parent = ret[key$1]; + var child = childVal[key$1]; + if (parent && !Array.isArray(parent)) { + // 如果父级 watch 选项不是数组,则将其转换为数组 + parent = [parent]; + } + ret[key$1] = parent + ? parent.concat(child) + : Array.isArray(child) ? child : [child]; + } + return ret; + } + +// 合并其他对象类型的选项(如 props、methods、inject、computed) + strats.props = + strats.methods = + strats.inject = + strats.computed = function ( + parentVal, // 父级选项值 + childVal, // 子级选项值 + vm, // Vue 实例 + key // 选项键名 + ) { + if (childVal && "development" !== 'production') { + // 在开发环境下,验证子级选项是否为对象类型 + assertObjectType(key, childVal, vm); + } + if (!parentVal) { + // 如果没有父级选项,则返回子级选项 + return childVal; + } + var ret = Object.create(null); + // 将父级选项合并到结果对象中 + extend(ret, parentVal); + if (childVal) { + // 如果有子级选项,则将其合并到结果对象中 + extend(ret, childVal); + } + return ret; + } +// 合并 provide 选项的策略 + strats.provide = mergeDataOrFn; + +// 默认合并策略,如果子选项值未定义,则返回父选项值,否则返回子选项值 + var defaultStrat = function (parentVal, childVal) { + return childVal === undefined + ? parentVal + : childVal; + } + +// 验证组件名称 + function checkComponents (options) { + for (var key in options.components) { + // 对每个组件名称进行验证 + validateComponentName(key); + } + } + +// 验证组件名称是否合法 + function validateComponentName (name) { + if (!new RegExp(("^[a-zA-Z][\\-\\.0-9_" + (unicodeRegExp.source) + "]*$")).test(name)) { + // 如果组件名称不符合正则表达式,发出警告 + warn( + 'Invalid component name: "' + name + '". Component names ' + + 'should conform to valid custom element name in html5 specification.' + ); + } + if (isBuiltInTag(name) || config.isReservedTag(name)) { + // 如果组件名称是内置标签或保留标签,发出警告 + warn( + 'Do not use built-in or reserved HTML elements as component ' + + 'id: ' + name + ); + } + } + +// 确保所有 props 选项语法都被规范化为基于对象的格式。 + function normalizeProps (options, vm) { + // 获取 options 中的 props 选项 + var props = options.props; + // 如果 props 选项不存在,直接返回 + if (!props) { return } + // 用于存储规范化后的 props + var res = {}; + var i, val, name; + // 如果 props 是数组形式 + if (Array.isArray(props)) { + // 获取数组长度 + i = props.length; + // 从后往前遍历数组 + while (i--) { + // 获取当前元素 + val = props[i]; + if (typeof val === 'string') { + // 将字符串转换为驼峰命名 + name = camelize(val); + // 规范化为对象格式,类型为 null + res[name] = { type: null }; + } else { + // 发出警告,数组形式的 props 必须是字符串 + warn('props must be strings when using array syntax.'); + } + } + } else if (isPlainObject(props)) { + // 如果 props 是对象形式 + for (var key in props) { + // 获取当前属性值 + val = props[key]; + // 将键名转换为驼峰命名 + name = camelize(key); + res[name] = isPlainObject(val) + ? val + // 如果不是对象,将其包装为对象,指定类型 + : { type: val }; + } + } else { + // 发出警告,props 选项的值必须是数组或对象 + warn( + "Invalid value for option \"props\": expected an Array or an Object, " + + "but got " + (toRawType(props)) + ".", + vm + ); + } + // 将规范化后的 props 赋值给 options.props + options.props = res; + } + +// 将所有注入项规范化为基于对象的格式。 + function normalizeInject (options, vm) { + // 获取 options 中的 inject 选项 + var inject = options.inject; + // 如果 inject 选项不存在,直接返回 + if (!inject) { return } + // 用于存储规范化后的 inject + var normalized = options.inject = {}; + if (Array.isArray(inject)) { + // 如果 inject 是数组形式 + for (var i = 0; i < inject.length; i++) { + // 规范化为对象格式,指定 from 属性 + normalized[inject[i]] = { from: inject[i] }; + } + } else if (isPlainObject(inject)) { + // 如果 inject 是对象形式 + for (var key in inject) { + // 获取当前属性值 + var val = inject[key]; + normalized[key] = isPlainObject(val) + // 如果是对象,合并 from 属性 + ? extend({ from: key }, val) + // 否则,指定 from 属性 + : { from: val }; + } + } else { + // 发出警告,inject 选项的值必须是数组或对象 + warn( + "Invalid value for option \"inject\": expected an Array or an Object, " + + "but got " + (toRawType(inject)) + ".", + vm + ); + } + } + +// 将原始的函数指令规范化为对象格式。 + function normalizeDirectives (options) { + // 获取 options 中的 directives 选项 + var dirs = options.directives; + if (dirs) { + // 如果 directives 选项存在 + for (var key in dirs) { + // 获取当前指令定义 + var def$$1 = dirs[key]; + if (typeof def$$1 === 'function') { + // 如果是函数,将其转换为对象格式,指定 bind 和 update 方法 + dirs[key] = { bind: def$$1, update: def$$1 }; + } + } + } + } + +// 断言值是否为纯对象,如果不是则发出警告 + function assertObjectType (name, value, vm) { + if (!isPlainObject(value)) { + warn( + "Invalid value for option \"" + name + "\": expected an Object, " + + "but got " + (toRawType(value)) + ".", + vm + ); + } + } + +// 将两个选项对象合并为一个新对象。 +// 这是实例化和继承过程中使用的核心工具函数。 + function mergeOptions ( + parent, // 父选项对象 + child, // 子选项对象 + vm // Vue 实例 + ) { + // 检查子选项中的组件名称是否合法 + checkComponents(child); + + if (typeof child === 'function') { + // 如果子选项是函数,获取其 options 属性 + child = child.options; + } + + // 规范化子选项中的 props + normalizeProps(child, vm); + // 规范化子选项中的 inject + normalizeInject(child, vm); + // 规范化子选项中的 directives + normalizeDirectives(child); + + // 仅当子选项是原始选项对象(不是另一次 mergeOptions 调用的结果)时,应用 extends 和 mixins + // 只有合并后的选项才有 _base 属性 + if (!child._base) { + if (child.extends) { + // 递归合并 extends 选项 + parent = mergeOptions(parent, child.extends, vm); + } + if (child.mixins) { + // 遍历 mixins 数组,递归合并每个 mixin + for (var i = 0, l = child.mixins.length; i < l; i++) { + parent = mergeOptions(parent, child.mixins[i], vm); + } + } + } + + // 用于存储合并后的选项 + var options = {}; + var key; + // 遍历父选项的所有键 + for (key in parent) { + // 合并当前键对应的选项 + mergeField(key); + } + // 遍历子选项的所有键 + for (key in child) { + if (!hasOwn(parent, key)) { + // 如果父选项中不存在该键,合并该键对应的选项 + mergeField(key); + } + } + // 合并单个字段的函数 + function mergeField (key) { + // 获取该字段的合并策略,若不存在则使用默认策略 + var strat = strats[key] || defaultStrat; + // 执行合并策略,将结果赋值给合并后的选项 + options[key] = strat(parent[key], child[key], vm, key); + } + // 返回合并后的选项 + return options + } + +// 解析一个资产(如组件、指令、过滤器等)。 +// 该函数用于子实例访问其祖先链中定义的资产。 + function resolveAsset ( + options, // 选项对象 + type, // 资产类型(如 'components', 'directives', 'filters') + id, // 资产 ID + warnMissing // 是否在未找到资产时发出警告 + ) { + if (typeof id !== 'string') { + // 如果 ID 不是字符串,直接返回 + return + } + // 获取该类型的资产对象 + var assets = options[type]; + // 先检查本地注册的变体 + if (hasOwn(assets, id)) { return assets[id] } + // 将 ID 转换为驼峰命名 + var camelizedId = camelize(id); + if (hasOwn(assets, camelizedId)) { return assets[camelizedId] } + // 将驼峰命名的 ID 首字母大写 + var PascalCaseId = capitalize(camelizedId); + if (hasOwn(assets, PascalCaseId)) { return assets[PascalCaseId] } + // 回退到原型链查找 + var res = assets[id] || assets[camelizedId] || assets[PascalCaseId]; + if (warnMissing && !res) { + // 如果需要警告且未找到资产,发出警告 + warn( + 'Failed to resolve ' + type.slice(0, -1) + ': ' + id, + options + ); + } + return res + } + +// 验证一个 prop 的值是否合法。 + function validateProp ( + key, // prop 的键名 + propOptions, // prop 的选项定义 + propsData, // props 的数据 + vm // Vue 实例 + ) { + // 获取该 prop 的选项定义 + var prop = propOptions[key]; + // 判断该 prop 是否未在 propsData 中提供 + var absent = !hasOwn(propsData, key); + // 获取该 prop 的值 + var value = propsData[key]; + // 布尔值转换 + var booleanIndex = getTypeIndex(Boolean, prop.type); + if (booleanIndex > -1) { + if (absent && !hasOwn(prop, 'default')) { + // 如果未提供且没有默认值,将值设为 false + value = false; + } else if (value === '' || value === hyphenate(key)) { + // 仅当布尔类型优先级高于字符串类型时,将空字符串或同名值转换为布尔值 + var stringIndex = getTypeIndex(String, prop.type); + if (stringIndex < 0 || booleanIndex < stringIndex) { + value = true; + } + } + } + // 检查默认值 + if (value === undefined) { + // 获取该 prop 的默认值 + value = getPropDefaultValue(vm, prop, key); + // 由于默认值是一个新副本,确保对其进行观察 + var prevShouldObserve = shouldObserve; + toggleObserving(true); + observe(value); + toggleObserving(prevShouldObserve); + } + // 断言该 prop 的值是否合法 + assertProp(prop, key, value, vm, absent); + return value + } + +// 获取一个 prop 的默认值。 + function getPropDefaultValue (vm, prop, key) { + // 如果没有默认值,返回 undefined + if (!hasOwn(prop, 'default')) { + return undefined + } + // 获取默认值 + var def = prop.default; + // 警告对象和数组类型的非工厂函数默认值 + if (isObject(def)) { + warn( + 'Invalid default value for prop "' + key + '": ' + + 'Props with type Object/Array must use a factory function ' + + 'to return the default value.', + vm + ); + } + // 如果之前渲染时该 prop 的原始值也是 undefined,返回之前的默认值以避免不必要的 watcher 触发 + if (vm && vm.$options.propsData && + vm.$options.propsData[key] === undefined && + vm._props[key] !== undefined + ) { + return vm._props[key] + } + // 对于非函数类型,调用工厂函数获取默认值 + // 如果一个值的原型是函数,即使在不同的执行上下文中,它也是函数 + return typeof def === 'function' && getType(prop.type) !== 'Function' + ? def.call(vm) + : def + } + +// 断言一个 prop 是否合法。 + function assertProp ( + prop, // prop 的选项定义 + name, // prop 的名称 + value, // prop 的值 + vm, // Vue 实例 + absent // 该 prop 是否未提供 + ) { + if (prop.required && absent) { + // 如果 prop 是必需的但未提供,发出警告 + warn( + 'Missing required prop: "' + name + '"', + vm + ); + return + } + if (value == null && !prop.required) { + // 如果值为 null 或 undefined 且 prop 不是必需的,直接返回 + return + } + // 获取 prop 的类型定义 + var type = prop.type; + // 初始化合法性标志 + var valid = !type || type === true; + // 存储期望的类型 + var expectedTypes = []; + if (type) { + if (!Array.isArray(type)) { + // 如果类型不是数组,将其转换为数组 + type = [type]; + } + // 遍历类型数组,检查值是否符合类型要求 + for (var i = 0; i < type.length && !valid; i++) { + var assertedType = assertType(value, type[i]); + // 存储期望的类型 + expectedTypes.push(assertedType.expectedType || ''); + // 更新合法性标志 + valid = assertedType.valid; + } + } + + if (!valid) { + // 如果值不合法,发出警告 + warn( + getInvalidTypeMessage(name, value, expectedTypes), + vm + ); + return + } + // 获取自定义验证器 + var validator = prop.validator; + if (validator) { + if (!validator(value)) { + // 如果自定义验证器检查失败,发出警告 + warn( + 'Invalid prop: custom validator check failed for prop "' + name + '".', + vm + ); + } + } + } + +// 简单类型检查的正则表达式 + var simpleCheckRE = /^(String|Number|Boolean|Function|Symbol)$/; + +// 断言值的类型是否符合期望类型 + function assertType (value, type) { + var valid; + // 获取期望类型的名称 + var expectedType = getType(type); + if (simpleCheckRE.test(expectedType)) { + // 如果是简单类型 + var t = typeof value; + // 检查基本类型是否匹配 + valid = t === expectedType.toLowerCase(); + // 处理原始包装对象 + if (!valid && t === 'object') { + valid = value instanceof type; + } + } else if (expectedType === 'Object') { + // 如果期望类型是对象,检查是否为纯对象 + valid = isPlainObject(value); + } else if (expectedType === 'Array') { + // 如果期望类型是数组,检查是否为数组 + valid = Array.isArray(value); + } else { + // 其他情况,检查是否为该类型的实例 + valid = value instanceof type; + } + return { + valid: valid, + expectedType: expectedType + } + } + +// 使用函数的字符串名称来检查内置类型, +// 因为在不同的 vm 或 iframe 中运行时,简单的相等检查会失败。 + function getType (fn) { + // 匹配函数名 + var match = fn && fn.toString().match(/^\s*function (\w+)/); + return match ? match[1] : '' + } + +// 判断两个类型是否相同 + function isSameType (a, b) { + return getType(a) === getType(b) + } + +// 获取类型在期望类型数组中的索引 + function getTypeIndex (type, expectedTypes) { + if (!Array.isArray(expectedTypes)) { + return isSameType(expectedTypes, type) ? 0 : -1 + } + for (var i = 0, len = expectedTypes.length; i < len; i++) { + if (isSameType(expectedTypes[i], type)) { + return i + } + } + return -1 + } + +// 获取无效类型的错误消息 + function getInvalidTypeMessage (name, value, expectedTypes) { + var message = "Invalid prop: type check failed for prop \"" + name + "\"." + + " Expected " + (expectedTypes.map(capitalize).join(', ')); + var expectedType = expectedTypes[0]; + var receivedType = toRawType(value); + var expectedValue = styleValue(value, expectedType); + var receivedValue = styleValue(value, receivedType); + // 检查是否需要指定期望的值 + if (expectedTypes.length === 1 && + isExplicable(expectedType) && + !isBoolean(expectedType, receivedType)) { + message += " with value " + expectedValue; + } + message += ", got " + receivedType + " "; + // 检查是否需要指定接收到的值 + if (isExplicable(receivedType)) { + message += "with value " + receivedValue + "."; + } + return message + } + +// 格式化值以显示类型 + function styleValue (value, type) { + if (type === 'String') { + return ("\"" + value + "\"") + } else if (type === 'Number') { + return ("" + (Number(value))) + } else { + return ("" + value) + } + } + +// 判断值是否可以明确表示 + function isExplicable (value) { + var explicitTypes = ['string', 'number', 'boolean']; + return explicitTypes.some(function (elem) { return value.toLowerCase() === elem; }) + } + +// 判断参数中是否包含布尔类型 + function isBoolean () { + var args = [], len = arguments.length; + while ( len-- ) args[ len ] = arguments[ len ]; + + return args.some(function (elem) { return elem.toLowerCase() === 'boolean'; }) + } + +// 处理错误。 + function handleError (err, vm, info) { + // 在处理错误处理程序时停用依赖跟踪,以避免可能的无限渲染。 + // 参考:https://github.com/vuejs/vuex/issues/1505 + pushTarget(); + try { + if (vm) { + var cur = vm; + // 遍历父实例链 + while ((cur = cur.$parent)) { + // 获取父实例的 errorCaptured 钩子 + var hooks = cur.$options.errorCaptured; + if (hooks) { + // 遍历 errorCaptured 钩子数组 + for (var i = 0; i < hooks.length; i++) { + try { + // 调用钩子函数,若返回 false 则停止捕获 + var capture = hooks[i].call(cur, err, vm, info) === false; + if (capture) { return } + } catch (e) { + // 处理钩子函数内部的错误 + globalHandleError(e, cur, 'errorCaptured hook'); + } + } + } + } + } + // 全局处理错误 + globalHandleError(err, vm, info); + } finally { + // 恢复依赖跟踪 + popTarget(); + } + } + +// 调用带有错误处理的处理程序。 + function invokeWithErrorHandling ( + handler, // 处理程序函数 + context, // 处理程序的上下文 + args, // 传递给处理程序的参数 + vm, // Vue 实例 + info // 错误信息 + ) { + var res; + try { + // 调用处理程序 + res = args ? handler.apply(context, args) : handler.call(context); + if (res && !res._isVue && isPromise(res) && !res._handled) { + // 如果结果是 Promise 且未处理过,捕获错误并处理 + res.catch(function (e) { return handleError(e, vm, info + " (Promise/async)"); }); + // 标记为已处理,避免多次捕获 + res._handled = true; + } + } catch (e) { + // 处理调用过程中的错误 + handleError(e, vm, info); + } + return res + } + +// 全局处理错误。 + function globalHandleError (err, vm, info) { + if (config.errorHandler) { + try { + // 调用用户配置的错误处理程序 + return config.errorHandler.call(null, err, vm, info) + } catch (e) { + // 如果用户在处理程序中故意抛出原始错误,不重复记录 + if (e !== err) { + logError(e, null, 'config.errorHandler'); + } + } + } + // 记录错误 + logError(err, vm, info); + } + +// 记录错误。 + function logError (err, vm, info) { + // 发出警告信息 + warn(("Error in " + info + ": \"" + (err.toString()) + "\""), vm); + if ((inBrowser || inWeex) && typeof console !== 'undefined') { + // 在浏览器或 Weex 环境中,使用 console.error 输出错误 + console.error(err); + } else { + // 其他环境抛出错误 + throw err + } + } + +// 标记是否使用微任务 + var isUsingMicroTask = false; + +// 存储回调函数的数组 + var callbacks = []; +// 标记是否有回调函数正在等待执行 + var pending = false; + +// 执行所有回调函数 + function flushCallbacks () { + // 标记为不再有回调函数等待执行 + pending = false; + // 复制回调函数数组 + var copies = callbacks.slice(0); + // 清空回调函数数组 + callbacks.length = 0; + // 遍历并执行所有回调函数 + for (var i = 0; i < copies.length; i++) { + copies[i](); + } + } + +// 这里我们使用微任务进行异步延迟包装。 +// 在 2.5 版本中,我们使用(宏)任务(结合微任务)。 +// 然而,当在重绘之前更改状态时,这会有一些微妙的问题 +// (例如 #6813,out-in 过渡效果)。 +// 此外,在事件处理程序中使用(宏)任务会导致一些无法避免的奇怪行为 +// (例如 #7109、#7153、#7546、#7834、#8109)。 +// 所以现在我们再次在所有地方使用微任务。 +// 这种权衡的一个主要缺点是,在某些情况下 +// 微任务的优先级太高,会在本应顺序执行的事件之间触发 +// (例如 #4521、#6690,这些都有解决方法) +// 甚至在同一事件的冒泡过程中触发(#6566)。 + var timerFunc; + +// nextTick 行为利用了微任务队列,可以通过原生 Promise.then 或 MutationObserver 访问。 +// MutationObserver 的支持范围更广,但在 iOS >= 9.3.3 的 UIWebView 中,当在触摸事件处理程序中触发时,它存在严重的 bug。 +// 触发几次后,它会完全停止工作……所以,如果原生 Promise 可用,我们将使用它: + if (typeof Promise !== 'undefined' && isNative(Promise)) { + var p = Promise.resolve(); + timerFunc = function () { + // 使用 Promise.then 异步执行回调函数 + p.then(flushCallbacks); + // 在有问题的 UIWebViews 中,Promise.then 不会完全崩溃,但 + // 它可能会陷入一种奇怪的状态,即回调被推入微任务队列,但队列没有被刷新,直到浏览器 + // 需要做一些其他工作,例如处理定时器。因此,我们可以 + // 通过添加一个空定时器来“强制”刷新微任务队列。 + if (isIOS) { setTimeout(noop); } + }; + // 标记为使用微任务 + isUsingMicroTask = true; + } else if (!isIE && typeof MutationObserver !== 'undefined' && ( + isNative(MutationObserver) || + // PhantomJS 和 iOS 7.x + MutationObserver.toString() === '[object MutationObserverConstructor]' + )) { + // 在原生 Promise 不可用时使用 MutationObserver, + // 例如 PhantomJS、iOS7、Android 4.4 + // (#6466 MutationObserver 在 IE11 中不可靠) + var counter = 1; + var observer = new MutationObserver(flushCallbacks); + var textNode = document.createTextNode(String(counter)); + observer.observe(textNode, { + characterData: true + }); + timerFunc = function () { + counter = (counter + 1) % 2; + textNode.data = String(counter); + }; + // 标记为使用微任务 + isUsingMicroTask = true; + } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { + // 回退到 setImmediate。 + // 从技术上讲,它利用了(宏)任务队列, + // 但它仍然比 setTimeout 更好。 + timerFunc = function () { + setImmediate(flushCallbacks); + }; + } else { + // 回退到 setTimeout。 + timerFunc = function () { + setTimeout(flushCallbacks, 0); + }; + } + + // 定义 nextTick 函数,用于在下次 DOM 更新循环结束之后执行延迟回调 + function nextTick (cb, ctx) { + // 定义 _resolve 变量,用于存储 Promise 的 resolve 函数 + var _resolve; + // 将回调函数添加到 callbacks 数组中 + callbacks.push(function () { + // 如果传入了回调函数 + if (cb) { + try { + // 执行回调函数,并绑定上下文 + cb.call(ctx); + } catch (e) { + // 处理回调函数执行过程中的错误 + handleError(e, ctx, 'nextTick'); + } + } else if (_resolve) { + // 如果没有传入回调函数,但有 _resolve 变量,调用 _resolve 并传入上下文 + _resolve(ctx); + } + }); + // 如果没有正在等待执行的回调函数 + if (!pending) { + // 标记为有回调函数正在等待执行 + pending = true; + // 调用 timerFunc 函数,异步执行回调函数 + timerFunc(); + } + // $flow-disable-line + // 如果没有传入回调函数,并且支持 Promise + if (!cb && typeof Promise !== 'undefined') { + // 返回一个 Promise 对象 + return new Promise(function (resolve) { + // 将 resolve 函数赋值给 _resolve 变量 + _resolve = resolve; + }) + } + } + +// 定义 mark 和 measure 变量,用于性能标记和测量 + var mark; + var measure; + +// 在开发环境下执行以下代码 + { + // 检查是否在浏览器环境中,并且支持 window.performance + var perf = inBrowser && window.performance; + // 如果支持性能标记和测量相关的方法 + if ( + perf && + perf.mark && + perf.measure && + perf.clearMarks && + perf.clearMeasures + ) { + // 定义 mark 函数,用于创建性能标记 + mark = function (tag) { return perf.mark(tag); }; + // 定义 measure 函数,用于测量两个标记之间的性能 + measure = function (name, startTag, endTag) { + // 测量两个标记之间的性能 + perf.measure(name, startTag, endTag); + // 清除开始标记 + perf.clearMarks(startTag); + // 清除结束标记 + perf.clearMarks(endTag); + // perf.clearMeasures(name) + }; + } + } + +// 不对此文件进行类型检查,因为 Flow 与 Proxy 不兼容 + var initProxy; + +// 在开发环境下执行以下代码 + { + // 定义一个允许的全局变量的映射 + var allowedGlobals = makeMap( + 'Infinity,undefined,NaN,isFinite,isNaN,' + + 'parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,' + + 'Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,' + + 'require' // for Webpack/Browserify + ); + + // 定义一个警告函数,用于提示属性或方法未定义 + var warnNonPresent = function (target, key) { + warn( + "Property or method \"" + key + "\" is not defined on the instance but " + + 'referenced during render. Make sure that this property is reactive, ' + + 'either in the data option, or for class-based components, by ' + + 'initializing the property. ' + + 'See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.', + target + ); + }; + + // 定义一个警告函数,用于提示以 $ 或 _ 开头的属性需要通过 $data 访问 + var warnReservedPrefix = function (target, key) { + warn( + "Property \"" + key + "\" must be accessed with \"$data." + key + "\" because " + + 'properties starting with "$" or "_" are not proxied in the Vue instance to ' + + 'prevent conflicts with Vue internals. ' + + 'See: https://vuejs.org/v2/api/#data', + target + ); + }; + + // 检查是否支持 Proxy + var hasProxy = + typeof Proxy !== 'undefined' && isNative(Proxy); + + // 如果支持 Proxy + if (hasProxy) { + // 定义一个内置修饰符的映射 + var isBuiltInModifier = makeMap('stop,prevent,self,ctrl,shift,alt,meta,exact'); + // 使用 Proxy 对 config.keyCodes 进行拦截 + config.keyCodes = new Proxy(config.keyCodes, { + // 拦截设置属性的操作 + set: function set (target, key, value) { + // 如果是内置修饰符 + if (isBuiltInModifier(key)) { + // 发出警告,避免覆盖内置修饰符 + warn(("Avoid overwriting built-in modifier in config.keyCodes: ." + key)); + return false + } else { + // 设置属性值 + target[key] = value; + return true + } + } + }); + } + + // 定义 has 拦截器的处理函数 + var hasHandler = { + // 拦截 in 操作符 + has: function has (target, key) { + // 检查对象是否有该属性 + var has = key in target; + // 检查是否是允许的全局变量,或者是以 _ 开头且不在 $data 中的属性 + var isAllowed = allowedGlobals(key) || + (typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data)); + // 如果对象没有该属性且不是允许的属性 + if (!has && !isAllowed) { + // 如果该属性在 $data 中,提示需要通过 $data 访问 + if (key in target.$data) { warnReservedPrefix(target, key); } + else { warnNonPresent(target, key); } + } + // 返回对象是否有该属性或是否是允许的属性 + return has || !isAllowed + } + }; + + // 定义 get 拦截器的处理函数 + var getHandler = { + // 拦截属性访问操作 + get: function get (target, key) { + // 如果属性名是字符串且对象没有该属性 + if (typeof key === 'string' && !(key in target)) { + // 如果该属性在 $data 中,提示需要通过 $data 访问 + if (key in target.$data) { warnReservedPrefix(target, key); } + else { warnNonPresent(target, key); } + } + // 返回属性值 + return target[key] + } + }; + + // 定义 initProxy 函数,用于初始化代理 + initProxy = function initProxy (vm) { + // 如果支持 Proxy + if (hasProxy) { + // 确定使用哪个代理处理程序 + var options = vm.$options; + var handlers = options.render && options.render._withStripped + ? getHandler + : hasHandler; + // 创建代理对象 + vm._renderProxy = new Proxy(vm, handlers); + } else { + // 如果不支持 Proxy,直接使用原对象 + vm._renderProxy = vm; + } + }; + } + +// 定义一个 Set 对象,用于存储已访问过的对象的依赖 ID + var seenObjects = new _Set(); + +// 递归遍历一个对象,触发所有转换后的 getter,以便将对象内的每个嵌套属性收集为 "深度" 依赖 + function traverse (val) { + // 调用 _traverse 函数进行遍历 + _traverse(val, seenObjects); + // 清空 seenObjects 集合 + seenObjects.clear(); + } + +// 内部递归遍历函数 + function _traverse (val, seen) { + var i, keys; + // 判断 val 是否为数组 + var isA = Array.isArray(val); + // 如果 val 既不是数组也不是对象,或者是冻结对象,或者是 VNode 实例,则直接返回 + if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) { + return + } + // 如果 val 有 __ob__ 属性(表示是响应式对象) + if (val.__ob__) { + // 获取该对象的依赖 ID + var depId = val.__ob__.dep.id; + // 如果 seen 集合中已经存在该依赖 ID,则直接返回 + if (seen.has(depId)) { + return + } + // 将该依赖 ID 添加到 seen 集合中 + seen.add(depId); + } + // 如果 val 是数组 + if (isA) { + // 获取数组长度 + i = val.length; + // 从后往前遍历数组元素 + while (i--) { _traverse(val[i], seen); } + } else { + // 获取对象的所有键 + keys = Object.keys(val); + // 获取键的数量 + i = keys.length; + // 从后往前遍历键 + while (i--) { _traverse(val[keys[i]], seen); } + } + } + +// 缓存 normalizeEvent 函数的结果 + var normalizeEvent = cached(function (name) { + // 判断是否为被动事件 + var passive = name.charAt(0) === '&'; + // 去除被动事件标记 + name = passive ? name.slice(1) : name; + // 判断是否为一次性事件 + var once$$1 = name.charAt(0) === '~'; // Prefixed last, checked first + // 去除一次性事件标记 + name = once$$1 ? name.slice(1) : name; + // 判断是否为捕获事件 + var capture = name.charAt(0) === '!'; + // 去除捕获事件标记 + name = capture ? name.slice(1) : name; + // 返回事件信息对象 + return { + name: name, + once: once$$1, + capture: capture, + passive: passive + } + }); + +// 创建一个函数调用器 + function createFnInvoker (fns, vm) { + // 定义调用器函数 + function invoker () { + var arguments$1 = arguments; + + var fns = invoker.fns; + // 如果 fns 是数组 + if (Array.isArray(fns)) { + // 复制一份 fns 数组 + var cloned = fns.slice(); + // 遍历数组并调用每个函数 + for (var i = 0; i < cloned.length; i++) { + invokeWithErrorHandling(cloned[i], null, arguments$1, vm, "v-on handler"); + } + } else { + // 对于单个函数,返回其调用结果 + return invokeWithErrorHandling(fns, null, arguments, vm, "v-on handler") + } + } + // 将传入的 fns 赋值给调用器的 fns 属性 + invoker.fns = fns; + // 返回调用器函数 + return invoker + } + +// 更新事件监听器 + function updateListeners ( + on, + oldOn, + add, + remove$$1, + createOnceHandler, + vm + ) { + var name, def$$1, cur, old, event; + // 遍历新的事件监听器 + for (name in on) { + def$$1 = cur = on[name]; + old = oldOn[name]; + // 规范化事件名称 + event = normalizeEvent(name); + // 如果新的监听器无效 + if (isUndef(cur)) { + // 发出警告 + warn( + "Invalid handler for event \"" + (event.name) + "\": got " + String(cur), + vm + ); + } else if (isUndef(old)) { + // 如果旧的监听器不存在 + if (isUndef(cur.fns)) { + // 创建函数调用器 + cur = on[name] = createFnInvoker(cur, vm); + } + // 如果是一次性事件 + if (isTrue(event.once)) { + // 创建一次性事件处理程序 + cur = on[name] = createOnceHandler(event.name, cur, event.capture); + } + // 添加事件监听器 + add(event.name, cur, event.capture, event.passive, event.params); + } else if (cur !== old) { + // 如果新的监听器和旧的不同 + old.fns = cur; + on[name] = old; + } + } + // 遍历旧的事件监听器 + for (name in oldOn) { + // 如果新的监听器中不存在该事件 + if (isUndef(on[name])) { + // 规范化事件名称 + event = normalizeEvent(name); + // 移除事件监听器 + remove$$1(event.name, oldOn[name], event.capture); + } + } + } + +// 合并 VNode 钩子函数 + function mergeVNodeHook (def, hookKey, hook) { + // 如果 def 是 VNode 实例 + if (def instanceof VNode) { + // 获取或创建 data.hook 对象 + def = def.data.hook || (def.data.hook = {}); + } + var invoker; + // 获取旧的钩子函数 + var oldHook = def[hookKey]; + + // 定义包装后的钩子函数 + function wrappedHook () { + // 调用钩子函数 + hook.apply(this, arguments); + // 移除合并后的钩子函数,确保只调用一次,防止内存泄漏 + remove(invoker.fns, wrappedHook); + } + + // 如果旧的钩子函数不存在 + if (isUndef(oldHook)) { + // 创建一个新的调用器 + invoker = createFnInvoker([wrappedHook]); + } else { + // 如果旧的钩子函数已经是合并后的调用器 + if (isDef(oldHook.fns) && isTrue(oldHook.merged)) { + // 使用旧的调用器 + invoker = oldHook; + // 将包装后的钩子函数添加到调用器的 fns 数组中 + invoker.fns.push(wrappedHook); + } else { + // 如果旧的钩子函数是普通函数 + invoker = createFnInvoker([oldHook, wrappedHook]); + } + } + + // 标记调用器为已合并 + invoker.merged = true; + // 将调用器赋值给对应的钩子键 + def[hookKey] = invoker; + } + +// 从 VNode 数据中提取 props + function extractPropsFromVNodeData ( + data, + Ctor, + tag + ) { + // 只提取原始值,验证和默认值在子组件中处理 + var propOptions = Ctor.options.props; + // 如果没有定义 props 选项,直接返回 + if (isUndef(propOptions)) { + return + } + var res = {}; + // 获取 data 中的 attrs 和 props + var attrs = data.attrs; + var props = data.props; + // 如果 attrs 或 props 存在 + if (isDef(attrs) || isDef(props)) { + // 遍历 propOptions 中的每个 prop + for (var key in propOptions) { + // 获取 kebab-case 形式的 prop 名称 + var altKey = hyphenate(key); + { + // 获取 prop 名称的小写形式 + var keyInLowerCase = key.toLowerCase(); + // 如果 prop 名称大小写不同,且 attrs 中存在小写形式的 prop + if ( + key !== keyInLowerCase && + attrs && hasOwn(attrs, keyInLowerCase) + ) { + // 提示 prop 名称大小写不一致的问题 + tip( + "Prop \"" + keyInLowerCase + "\" is passed to component " + + (formatComponentName(tag || Ctor)) + ", but the declared prop name is" + + " \"" + key + "\". " + + "Note that HTML attributes are case-insensitive and camelCased " + + "props need to use their kebab-case equivalents when using in-DOM " + + "templates. You should probably use \"" + altKey + "\" instead of \"" + key + "\"." + ); + } + } + // 检查 props 中是否存在该 prop + checkProp(res, props, key, altKey, true) || + // 检查 attrs 中是否存在该 prop + checkProp(res, attrs, key, altKey, false); + } + } + // 返回提取的 props + return res + } + +// 检查并提取 prop + function checkProp ( + res, + hash, + key, + altKey, + preserve + ) { + // 如果 hash 存在 + if (isDef(hash)) { + // 如果 hash 中存在 key + if (hasOwn(hash, key)) { + // 将 key 对应的值赋给 res + res[key] = hash[key]; + // 如果不需要保留该属性 + if (!preserve) { + // 从 hash 中删除该属性 + delete hash[key]; + } + return true + } else if (hasOwn(hash, altKey)) { + // 如果 hash 中存在 altKey + res[key] = hash[altKey]; + // 如果不需要保留该属性 + if (!preserve) { + // 从 hash 中删除该属性 + delete hash[altKey]; + } + return true + } + } + return false + } + +// 模板编译器会在编译时静态分析模板,尽量减少规范化的需求。 +// 对于纯 HTML 标记,可以完全跳过规范化,因为生成的渲染函数保证返回 Array。但有两种情况需要额外的规范化: + +// 1. 当子节点包含组件时 - 因为函数式组件可能返回一个数组而不是单个根节点。在这种情况下,只需要简单的规范化 - 如果任何子节点是数组,我们使用 Array.prototype.concat 扁平化整个数组。由于函数式组件已经对自己的子节点进行了规范化,所以保证只有一层深度。 + function simpleNormalizeChildren (children) { + // 遍历子节点 + for (var i = 0; i < children.length; i++) { + // 如果子节点是数组 + if (Array.isArray(children[i])) { + // 使用 Array.prototype.concat 扁平化数组 + return Array.prototype.concat.apply([], children) + } + } + // 如果没有数组子节点,直接返回子节点数组 + return children + } + +// 2. 当子节点包含总是生成嵌套数组的结构时,例如