官方文档

http://doc.ruoyi.vip/ruoyi-vue/

前端部署到tomat服务器,刷新报404

添加一个 WEB-INF 文件夹,文件夹里面新建文件 web.xml,内容如下

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3. xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
  4. http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
  5. version="3.1" metadata-complete="true">
  6. <display-name>Router for Tomcat</display-name>
  7. <error-page>
  8. <error-code>404</error-code>
  9. <location>/index.html</location>
  10. </error-page>
  11. </web-app>

tomcat配置虚拟路径

修改server.xmlHost节点下添加

  1. <Context docBase="" path="/" reloadable="true" source=""/>

修改浏览器窗口标题

ruoyi-vue - 图1

修改位置

ruoyi-vue - 图2

ruoyi-vue - 图3

element-ui

标签 描述
el-form form 表单
el-form-item form 表单项
el-table 表格
el-table-column 表格列
el-dialog 对话框
el-button 按钮

树形结构问题

ruoyi-vue - 图4

原因:列是 居中的 ,去掉即可

ruoyi-vue - 图5

新增菜单图标

如果你是从 iconfont (opens new window)下载的图标,记得使用如 Sketch 等工具规范一下图标的大小问题,不然可能会造成项目中的图标大小尺寸不统一的问题。 本项目中使用的图标都是 128*128 大小规格的。

将下载的图标 移动到 src/assets/icons/svg 这个目录

ruoyi-vue - 图6

ruoyi-vue - 图7

treeselect 禁用分支节点

treeselect官网

ruoyi-vue - 图8

只能点击最子节点

ruoyi-vue - 图9

点击空白处禁止关闭弹窗

之前的效果:

ruoyi-vue - 图10

main.jsVue.use(Element)上面添加以下内容

  1. Element.Dialog.props.closeOnClickModal.default = false

禁用后的效果:

ruoyi-vue - 图11

show-overflow-tooltip

当内容长度超过了指定长度,会隐藏,然后鼠标悬浮在内容上面,会悬浮显示全部内容

el-select 配置 宽度

ruoyi-vue - 图12

<el-select> 加上 :style="{width: '100%'}"

  1. <el-row>
  2. <el-col :span="24">
  3. <el-form-item label="角色">
  4. <el-select v-model="form.roleIds" multiple :style="{width: '100%'}" placeholder="请选择" >
  5. <el-option
  6. v-for="item in roleOptions"
  7. :key="item.roleId"
  8. :label="item.roleName"
  9. :value="item.roleId"
  10. ></el-option>
  11. </el-select>
  12. </el-form-item>
  13. </el-col>
  14. </el-row>

Drawer 滑动

  1. .el-drawer.rtl {
  2. overflow: scroll
  3. }

列显示图片

  1. <el-form-item label="照片">
  2. <span v-for="fit in previewImgFormat(props.row)" :key="fit">
  3. <el-image
  4. style="width: 100px; height: 100px ;margin: 5px"
  5. :src="fit"
  6. :preview-src-list="previewImgFormat(props.row)"
  7. ></el-image>
  8. </span>
  9. </el-form-item>
  10. export default {
  11. methods: {
  12. previewImgFormat(row, column){
  13. var imgUrl = row.imgUrls;
  14. return imgUrl.split(",");
  15. },
  16. }
  17. }

This may cause an update error

是v-for循环里,key值可能重复了,所以会报这个错

表单增加图片

vue代码如下

  1. <el-row>
  2. <el-col :span="12">
  3. <el-form-item label="封面图片 :" label-width="90px" prop="imgUrl" >
  4. <image-upload-local-vue :parentImage="form.imgUrl" @oneImage="oneImageFun">
  5. </image-upload-local-vue>
  6. </el-form-item>
  7. </el-col>
  8. </el-row>

javascript 代码如下

  1. // 引入 imageUploadLocalVue 组件
  2. import imageUploadLocalVue from "@/components/upload/image-upload-local.vue";
  3. export default {
  4. name: "Branch",
  5. components: {
  6. imageUploadLocalVue
  7. },
  8. methods: {
  9. /** 单图上传 */
  10. oneImageFun(val) {
  11. this.form.imgUrl = val;
  12. },
  13. }
  14. }

全局配置EL-table属性

main.js 添加以下内容

  1. // 带有斑马纹
  2. Element.Table.props.stripe = {
  3. default:true,
  4. type:Boolean
  5. }
  6. // 带有边框
  7. Element.Table.props.border = {
  8. default:true,
  9. type:Boolean
  10. }
  11. Vue.use(Element, {
  12. size: Cookies.get('size') || 'medium' // set element-ui default size
  13. })

最后效果:

ruoyi-vue - 图13

ruoyi-vue - 图14

表格 行合并

ruoyi-vue - 图15

  1. <el-table v-loading="loading" :data="quesPartItemList" :span-method="objectSpanMethod" stripe border>
  2. <el-table-column type="selection" width="55" align="center" fixed="left" />
  3. <el-table-column label="检查部位" align="center" prop="partTypeId" :formatter="partTypeFormat" show-overflow-tooltip/>
  4. </el-table>
  5. <script>
  6. export default {
  7. name: "QuesPartItem",
  8. methods: {
  9. /** 查询问卷检查项列表 */
  10. getList() {
  11. this.loading = true;
  12. listQuesPartItem(this.queryParams).then(response => {
  13. this.quesPartItemList = response.rows;
  14. this.total = response.total;
  15. this.loading = false;
  16. this.getListDataForRowAndColumn(this.quesPartItemList);
  17. });
  18. },
  19. /**
  20. * 处理表格数据
  21. */
  22. getListDataForRowAndColumn(data){
  23. let self = this;
  24. self.rowAndColumn = [];
  25. self.rowRoomColumn = [];
  26. for (var i = 0; i < data.length; i++) {
  27. if (i === 0) {
  28. // 如果是第一条记录(即索引是0的时候),向数组中加入1
  29. self.rowAndColumn.push(1);
  30. self.pos = 0;
  31. self.rowRoomColumn.push(1);
  32. self.posT = 0;
  33. } else {
  34. if (data[i].partTypeId === data[i - 1].partTypeId) {
  35. // 如果storeName相等就累加,并且push 0
  36. self.rowAndColumn[self.pos] += 1
  37. self.rowAndColumn.push(0)
  38. if (data[i].partTypeId === data[i - 1].partTypeId) {
  39. // 如果roomName相等就累加,并且push 0
  40. self.rowRoomColumn[self.posT] += 1
  41. self.rowRoomColumn.push(0)
  42. }else{
  43. self.rowRoomColumn.push(1)
  44. self.posT = i
  45. }
  46. } else {
  47. // 不相等push 1
  48. self.rowAndColumn.push(1)
  49. self.pos = i;
  50. self.rowRoomColumn.push(1)
  51. self.posT = i
  52. }
  53. }
  54. }
  55. console.log(this.rowAndColumn);
  56. console.log(this.rowRoomColumn);
  57. },
  58. objectSpanMethod({ row, column, rowIndex, columnIndex }) {
  59. let self = this;
  60. if (columnIndex === 0) {
  61. if (self.rowAndColumn[rowIndex]) {
  62. let rowNum = self.rowAndColumn[rowIndex];
  63. return {
  64. rowspan: rowNum,
  65. colspan: rowNum > 0 ? 1 : 0
  66. }
  67. }
  68. return {
  69. rowspan: 0,
  70. colspan: 0
  71. }
  72. }
  73. if (columnIndex === 1) {
  74. if (self.rowRoomColumn[rowIndex]) {
  75. let roomNum = self.rowRoomColumn[rowIndex];
  76. return {
  77. rowspan: roomNum,
  78. colspan: roomNum > 0 ? 1 : 0
  79. }
  80. }
  81. return {
  82. rowspan: 0,
  83. colspan: 0
  84. }
  85. }
  86. },
  87. }
  88. }
  89. </script>

disabled 禁用

ruoyi-vue - 图16

内容字体不明显

ruoyi-vue - 图17

index.scss

  1. .el-input.is-disabled .el-input__inner {
  2. color: #333!important;
  3. cursor:default; // 鼠标悬浮光标样式
  4. }
  5. .el-range-editor.is-disabled input {
  6. color: #333!important;
  7. cursor:default;
  8. }
  9. .el-textarea.is-disabled .el-textarea__inner {
  10. color: #333!important;
  11. cursor:default;
  12. }

css cursor(鼠标悬浮禁用)

url 需使用的自定义光标的 URL。注释:请在此列表的末端始终定义一种普通的光标,以防没有由 URL 定义的可用光标。
default 默认光标(通常是一个箭头)
auto 默认。浏览器设置的光标。
crosshair 光标呈现为十字线。
pointer 光标呈现为指示链接的指针(一只手)
move 此光标指示某对象可被移动。
e-resize 此光标指示矩形框的边缘可被向右(东)移动。
ne-resize 此光标指示矩形框的边缘可被向上及向右移动(北/东)。
nw-resize 此光标指示矩形框的边缘可被向上及向左移动(北/西)。
n-resize 此光标指示矩形框的边缘可被向上(北)移动。
se-resize 此光标指示矩形框的边缘可被向下及向右移动(南/东)。
sw-resize 此光标指示矩形框的边缘可被向下及向左移动(南/西)。
s-resize 此光标指示矩形框的边缘可被向下移动(南)。
w-resize 此光标指示矩形框的边缘可被向左移动(西)。
text 此光标指示文本。
wait 此光标指示程序正忙(通常是一只表或沙漏)。
help 此光标指示可用的帮助(通常是一个问号或一个气球)。

单次定时任务推送

如何使用?

  1. SysJob sysJob = new SysJob();
  2. // 是否并发执行(0允许 1禁止)
  3. sysJob.setConcurrent("1");
  4. Integer userId = taskPushRecord.getUserId();
  5. userId = userId == null ? 0 : userId;
  6. sysJob.setInvokeTarget("dingNoticeTask.sendDingNotice(" + pushRecordId
  7. + "," + 1 + ")");
  8. sysJob.setStartAt(DateUtil.parseTimeToday(releaseRemindTime));
  9. sysJob.setJobGroup("releasePushRecord");
  10. sysJob.setJobId(Long.valueOf(pushRecordId));
  11. sysJob.setJobName("发布任务提醒");
  12. // 任务状态(0正常 1暂停)
  13. sysJob.setStatus("0");
  14. OnceScheduleUtils.createScheduleJob(scheduler, sysJob);

OnceScheduleUtils.java 代码如下

  1. package com.hn.common.utils.job;
  2. import cn.hutool.json.JSONUtil;
  3. import com.hn.common.constant.ScheduleConstants;
  4. import com.hn.common.exception.job.TaskException;
  5. import com.hn.project.monitor.domain.SysJob;
  6. import lombok.extern.slf4j.Slf4j;
  7. import org.quartz.*;
  8. /**
  9. * 定时单次任务工具类
  10. */
  11. @Slf4j
  12. public class OnceScheduleUtils {
  13. /**
  14. * 得到quartz任务类
  15. *
  16. * @param sysJob 执行计划
  17. * @return 具体执行任务类
  18. */
  19. private static Class<? extends Job> getQuartzJobClass(SysJob sysJob) {
  20. boolean isConcurrent = "0".equals(sysJob.getConcurrent());
  21. return isConcurrent ? QuartzJobExecution.class : QuartzDisallowConcurrentExecution.class;
  22. }
  23. /**
  24. * 构建任务触发对象
  25. */
  26. public static TriggerKey getTriggerKey(Long jobId, String jobGroup) {
  27. return TriggerKey.triggerKey(ScheduleConstants.TASK_CLASS_NAME + jobId, jobGroup);
  28. }
  29. /**
  30. * 构建任务键对象
  31. */
  32. public static JobKey getJobKey(Long jobId, String jobGroup) {
  33. return JobKey.jobKey(ScheduleConstants.TASK_CLASS_NAME + jobId, jobGroup);
  34. }
  35. /**
  36. * 创建定时任务
  37. */
  38. public static void createScheduleJob(Scheduler scheduler, SysJob job) throws SchedulerException, TaskException {
  39. log.info("############ 创建定时任务:{}", JSONUtil.toJsonStr(job));
  40. Class<? extends Job> jobClass = getQuartzJobClass(job);
  41. // 构建job信息
  42. Long jobId = job.getJobId();
  43. String jobGroup = job.getJobGroup();
  44. JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(getJobKey(jobId, jobGroup)).build();
  45. // 只需要执行一次
  46. SimpleScheduleBuilder simpleScheduleBuilder = SimpleScheduleBuilder.simpleSchedule();
  47. // 按新的cronExpression表达式构建一个新的trigger
  48. SimpleTrigger trigger = TriggerBuilder.newTrigger()
  49. .withIdentity(getTriggerKey(jobId, jobGroup))
  50. .startAt(job.getStartAt())
  51. .withSchedule(simpleScheduleBuilder).build();
  52. // 放入参数,运行时的方法可以获取
  53. jobDetail.getJobDataMap().put(ScheduleConstants.TASK_PROPERTIES, job);
  54. // 判断是否存在
  55. if (scheduler.checkExists(getJobKey(jobId, jobGroup))) {
  56. log.info("############ 移除定时任务:{}", JSONUtil.toJsonStr(job));
  57. // 防止创建时存在数据问题 先移除,然后在执行创建操作
  58. scheduler.deleteJob(getJobKey(jobId, jobGroup));
  59. }
  60. scheduler.scheduleJob(jobDetail, trigger);
  61. // 暂停任务
  62. if (job.getStatus().equals(ScheduleConstants.Status.PAUSE.getValue())) {
  63. scheduler.pauseJob(ScheduleUtils.getJobKey(jobId, jobGroup));
  64. }
  65. }
  66. }

生成的任务都在 qrtz_triggers 数据表

ruoyi-vue - 图18

注意: 如果时间点在之前,会立刻推送 比如当前时间 16:00 ,你设置的是当天 8:00 推送 ,那会立刻就推送

抽屉式+标签页

ruoyi-vue - 图19

  1. <el-drawer :title="detail.title" size="50%" :visible.sync="detail.open" :direction="detail.direction">
  2. <el-tabs tab-position="top" >
  3. <el-tab-pane label="巡检问卷" name="second">巡检问卷</el-tab-pane>
  4. <el-tab-pane label="任务记录" name="third">任务记录</el-tab-pane>
  5. </el-tabs>
  6. </el-drawer>
  1. <style>
  2. .el-tabs__header {
  3. margin-left: auto;
  4. margin-right: auto;
  5. width: 95%;
  6. }
  7. </style>

子组件不能修改父组件的值

子组件代码:

  1. <template>
  2. <div class="top-right-btn">
  3. <el-row>
  4. <el-tooltip class="item" effect="dark" :content="showSearch ? '隐藏搜索' : '显示搜索'" placement="top">
  5. <el-button size="mini" circle icon="el-icon-search" @click="toggleSearch()"/>
  6. </el-tooltip>
  7. <el-tooltip class="item" effect="dark" v-if="showGarbage" :content="showGarbageIcon? '垃圾箱' : '返回'"
  8. placement="top">
  9. <el-button size="medium" circle :icon="showGarbageIcon? 'el-icon-delete' : 'el-icon-back'" @click="garbage()"/>
  10. </el-tooltip>
  11. </el-row>
  12. </div>
  13. </template>
  14. <script>
  15. export default {
  16. name: "RightToolbar",
  17. data() {
  18. return {};
  19. },
  20. props: {
  21. showSearch: {
  22. type: Boolean,
  23. default: true,
  24. },
  25. showGarbage: {
  26. type: Boolean,
  27. default: true,
  28. },
  29. showGarbageIcon: {
  30. type: Boolean,
  31. default: true,
  32. },
  33. },
  34. methods: {
  35. //搜索
  36. toggleSearch() {
  37. this.$emit("update:showSearch", !this.showSearch);
  38. },
  39. garbage() {
  40. var showGarbageIcon = this.showGarbageIcon;
  41. // this.showGarbageIcon = !showGarbageIcon; //这行去掉 不能直接改变props中参数的值
  42. this.$emit("queryTable"); //通知父组件改变。
  43. },
  44. },
  45. };
  46. </script>

父组件代码:

  1. <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>

ruoyi-vue - 图20

通过props传递给子组件的show,不能在子组件内部修改props中的show值。

错误信息:

  1. [Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "showGarbageIcon"

正确传值方法:

  1. this.$emit("queryTable");

参考:Vue子组件与父组件之间传值

父组件修改子组件data值

父组件代码如下

  1. <!--详情-->
  2. <details-from ref="orderDetail" :orderId="orderId"/>
  3. // 引入组件
  4. import detailsFrom from './orderDetail'
  5. components: {
  6. detailsFrom
  7. },
  8. data(){
  9. orderStatusOptions:[],
  10. orderPayTypeOptions:[]
  11. }
  12. method(){
  13. updateChildData(){
  14. // ref='orderDetail'
  15. this.$refs.orderDetail.orderStatusOptions = this.orderStatusOptions
  16. this.$refs.orderDetail.orderPayTypeOptions = this.orderPayTypeOptions
  17. }
  18. }

子组件代码如下

  1. data() {
  2. return {
  3. orderStatusOptions:[],
  4. orderPayTypeOptions:[]
  5. }
  6. },

实现表单里面的 lable 标签对齐

ruoyi-vue - 图21

  1. <!-- lable-width 值调大一点 -->
  2. <el-form ref="form" :model="form" :rules="rules" label-width="120px">
  3. </el-form>

watch 用法

  1. watch: {
  2. // 监听这个字段变化
  3. showGarbageIcon: function (val) {
  4. console.log(val)
  5. this.queryParams.delFlag = val ? 0 : 2;
  6. this.showDeleteIcon = val ? 'el-icon-delete':'el-icon-circle-check';
  7. this.getList();
  8. }
  9. },

增加回收站

  1. <right-toolbar :showSearch.sync="showSearch" :showGarbage="garbage.show"
  2. :showGarbageIcon="garbage.iconShow"
  3. @queryGarbage="listGarbage"
  4. @queryTable="getList"></right-toolbar>

api.js

  1. // 删除问卷检查项
  2. export function delQuesPartItem(id, delFlag) {
  3. let url = '/rqinspect/quesPartItem/' + id;
  4. if (delFlag != null) {
  5. url = url + "?delFlag=" + delFlag;
  6. }
  7. return request({
  8. url: url,
  9. method: 'delete'
  10. })
  11. }

vue

  1. queryParams: {
  2. pageNum: 1,
  3. pageSize: 10,
  4. delFlag: 0,
  5. }

查询垃圾箱的js

  1. listGarbage(val){
  2. console.log(val)
  3. this.garbage.iconShow = val;
  4. this.garbage.btnIcon = val ? 'el-icon-delete':'el-icon-circle-check';
  5. this.garbage.btnText = val ? '删除':'启用';
  6. this.queryParams.delFlag = val ? 0 : 2;
  7. this.getList();
  8. },

删除js

  1. /** 删除按钮操作 */
  2. handleDelete(row) {
  3. const ids = row.id || this.ids;
  4. var delFlag = this.garbage.iconShow ? 2 : 0;
  5. var text = this.garbage.btnText;
  6. this.$confirm('是否确认'+text+'网点任务编号为"' + ids + '"的数据项?', "警告", {
  7. confirmButtonText: "确定",
  8. cancelButtonText: "取消",
  9. type: "warning"
  10. }).then(function () {
  11. return delTask(ids,delFlag);
  12. }).then(() => {
  13. this.getList();
  14. this.msgSuccess(text+"成功");
  15. }).catch(function () {
  16. });
  17. },

Controller.java

  1. @PreAuthorize("@ss.hasPermi('rqinspect:quesPartItem:remove')")
  2. @Log(title = "问卷检查项", businessType = BusinessType.DELETE)
  3. @DeleteMapping("/{ids}/{delFlag}")
  4. public AjaxResult remove(@PathVariable Integer[] ids,@PathVariable("delFlag") Integer delFlag){
  5. return toAjax(quesPartItemService.deleteQuesPartItemByIds(ids));
  6. }

mapper.java

  1. public int deleteQuesByIds(@Param("ids") Integer[] ids, @Param("delFlag") Integer delFlag);

mapper.xml

  1. <delete id="deleteQuesByIds" parameterType="String">
  2. update sys_ques set del_flag = #{delFlag} where id in
  3. <foreach item="id" collection="ids" open="(" separator="," close=")">
  4. #{id}
  5. </foreach>
  6. </delete>

执行重置方法报错

  • 报错信息

    TypeError: Cannot read property ‘indexOf’ of undefined at VueComponent.resetField (element-ui.common.js?5c96:23528)

  • 正常的

ruoyi-vue - 图22

错误的,path 等于 undefined

ruoyi-vue - 图23

element-ui.commmon.js

  1. resetField: function resetField() {
  2. var _this2 = this;
  3. this.validateState = '';
  4. this.validateMessage = '';
  5. var model = this.form.model;
  6. var value = this.fieldValue; // 报undefined
  7. var path = this.prop; // 报undefined
  8. if (path.indexOf(':') !== -1) {
  9. path = path.replace(/:/, '.');
  10. }
  11. var prop = Object(util_["getPropByPath"])(model, path, true);
  12. this.validateDisabled = true;
  13. if (Array.isArray(value)) {
  14. prop.o[prop.k] = [].concat(this.initialValue);
  15. } else {
  16. prop.o[prop.k] = this.initialValue;
  17. }
  18. // reset validateDisabled after onFieldChange triggered
  19. this.$nextTick(function () {
  20. _this2.validateDisabled = false;
  21. });
  22. this.broadcast('ElTimeSelect', 'fieldReset', this.initialValue);
  23. },
  • 原因如下,在使用 validateresetFields 方法的情况下,prop 属性是必填的

ruoyi-vue - 图24

ruoyi-vue - 图25

  • 解决方法如下
  1. <!--加一个 prop='属性名' -->
  2. <el-form-item label="角色" prop="roleIds">

ruoyi-vue - 图26

el-select 远程搜索

ruoyi-vue - 图27

el-select 一次性加载数据如果很多,就会很慢,我们这边改进一下。

改成远程搜索

  1. <el-form-item label="用户" prop="userIds">
  2. <el-select filterable remote :remote-method="remoteMethod" v-model="form.userIds" multiple :loading="selectloading" :style="{width: '100%'}" placeholder="请选择用户">
  3. <el-option
  4. v-for="item in userOptions"
  5. :key="item.userId"
  6. :label="item.userName"
  7. :value="item.userId"
  8. ></el-option>
  9. </el-select>
  10. </el-form-item>
  1. export default {
  2. name: "Group",
  3. data() {
  4. return {
  5. selectloading: true,
  6. userOptions: []
  7. }
  8. },
  9. methods:{
  10. /** 修改按钮操作 */
  11. handleUpdate(row) {
  12. this.reset();
  13. const id = row.id;
  14. getGroup(id).then(response => {
  15. this.form = response.data;
  16. this.form.userIds = response.userIds;
  17. // 根据用户id 获取获取用户信息
  18. listAllUser({'userIds':response.userIds.join(',')}).then(response=>{
  19. this.userOptions = response.data;
  20. });
  21. this.open = true;
  22. this.title = "修改分组";
  23. });
  24. },
  25. // 远程搜索
  26. remoteMethod(query){
  27. if (query !== '') {
  28. this.selectloading = true;
  29. var _this = this;
  30. setTimeout(() => {
  31. this.selectloading = false;
  32. listAllUser({'userName':query}).then(response=>{
  33. _this.userOptions = response.data;
  34. });
  35. }, 200);
  36. } else {
  37. this.options = [];
  38. }
  39. }
  40. }
  41. }

固定表格最右一栏

ruoyi-vue - 图28

参数 说明 类型 可选值
fixed 列是否固定在左侧或者右侧,true 表示固定在左侧 string, boolean true, left, right

el-table-column 加上 fixed="right"

  1. <el-table-column
  2. label="操作"
  3. align="center"
  4. width="160"
  5. class-name="small-padding fixed-width"
  6. fixed="right"
  7. >
  8. <template slot-scope="scope">
  9. <el-button
  10. size="mini"
  11. type="text"
  12. icon="el-icon-edit"
  13. @click="handleUpdate(scope.row)"
  14. v-hasPermi="['system:user:edit']"
  15. >修改
  16. </el-button>
  17. </template>
  18. </el-table-column>

去掉 url 中的

nginx 配置的时候注意一下

router/index.js

ruoyi-vue - 图29

改成

  1. export default new Router({
  2. // mode: 'history', // 去掉url中的#
  3. scrollBehavior: () => ({ y: 0 }),
  4. routes: constantRoutes
  5. })

hash和history两种模式的区别 https://www.jianshu.com/p/bfffb4b8c9fa

应用部署在子路径

vue.config.js

  1. // 例如 https://www.ruoyi.vip/。如果应用被部署在一个子路径上,你就需要用这个选项指定这个子路径。例如,如果你的应用被部署在 https://www.ruoyi.vip/admin/,则设置 baseUrl 为 /admin/。
  2. publicPath: process.env.NODE_ENV === "production" ? "/" : "/",

springboot配置文件上传大小

错误信息如下:

  1. Maximum upload size exceeded; nested exception is java.lang.IllegalStateException: org.apache.tomcat.util.http.fileupload.FileUploadBase$FileSizeLimitExceededException: The field avatarfile exceeds its maximum permitted size of 10485760 bytes.

配置文件增加以下内容:

  1. # springboot 2.x
  2. spring:
  3. servlet:
  4. multipart:
  5. # 文件最大
  6. max-file-size: 20MB
  7. # 设置总上传数据总大小
  8. max-request-size: 20MB

ruoyi-vue - 图30

el-upload 上传替换上一个文件

应用场景: 导入excel文件的时候用到

  1. # 在 el-upload 加上监听文件修改的方法
  2. :on-change="handleFileChange"
  3. # 限制文件上传个数
  4. :limit="2"

监听代码如下

  1. handleFileChange(file, fileList) {
  2. // 当多余一个的时候替换文件
  3. if (fileList.length > 1) {
  4. fileList.splice(0, 1);
  5. }
  6. },

ruoyi-vue - 图31

echarts 图表引入

ruoyi-vue - 图32

ruoyi-vue - 图33

代码如下,更多请参考 index.vue

  1. <el-col :span="8">
  2. <div class="card_centre">
  3. <div class="card_title">
  4. <div class="item1">收入统计</div>
  5. </div>
  6. <!-- <div class="total_data">
  7. <span>{{indexData.user.total}}</span>
  8. </div>-->
  9. <pie-chart :chart-data="pieChartData"/>
  10. </div>
  11. </el-col>
  12. </el-row>
  13. # 引入
  14. import PieChart from './dashboard/PieChart'

实现第三方登录

创建好token之后在Authorization携带token访问接口即可。不需要用户名和密码。会根据token查询到对应用户的权限

  1. OapiUserGetuserinfoResponse userInfoByCode = DingUserUtil.getUserInfoByCode(code);
  2. String userid = userInfoByCode.getUserid();
  3. AssertUtils.notNull(userid,"登录失败,用户不存在");
  4. SysUser sysUser = sysUserService.selectUserByDingId(userid);
  5. if(sysUser == null){
  6. throw new BaseException(userid,"登录失败,用户不存在");
  7. }
  8. AsyncManager.me().execute(AsyncFactory.recordLogininfor(sysUser.getUserName(), Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
  9. LoginUser loginUser = new LoginUser(sysUser,permissionService.getMenuPermission(sysUser));
  10. // 生成token
  11. return tokenService.createToken(loginUser);

处理多图上传问题,闪屏

ruoyi-vue - 图34

去掉watch,绑定唯一key值

  1. <el-form-item label="相册" prop="photoAlbum">
  2. <upload-image-list :key="form.userId" v-model="form.photoAlbum"/>
  3. </el-form-item>

UploadImageList 代码如下

  1. <template>
  2. <div class="component-upload-image">
  3. <el-upload
  4. :action="uploadImgUrl"
  5. :before-upload="handleBeforeUpload"
  6. :data="data"
  7. :file-list="fullUrls"
  8. :http-request="handleRequest"
  9. :limit="limit"
  10. :on-error="handleUploadError"
  11. :on-preview="handlePictureCardPreview"
  12. :on-remove="handleRemove"
  13. :on-success="handleUploadSuccess"
  14. class="uploadComponent"
  15. list-type="picture-card"
  16. name="file"
  17. style="display: inline-block; vertical-align: top"
  18. >
  19. <i class="el-icon-plus avatar-uploader-icon"></i>
  20. <el-dialog :visible.sync="dialogVisible" append-to-body width="500px">
  21. <img :src="dialogImageUrl" width="100%">
  22. </el-dialog>
  23. </el-upload>
  24. </div>
  25. </template>
  26. <script>
  27. import { getMinioToken } from '@/api/oss'
  28. import { createUploadPath } from '@/utils/index'
  29. import axios from 'axios'
  30. export default {
  31. components: {},
  32. data() {
  33. return {
  34. dialogImageUrl: '',
  35. dialogVisible: false,
  36. //process.env.VUE_APP_BASE_API + "/common/upload",
  37. uploadImgUrl: '', // 上传的图片服务器地址
  38. downloadImgUrl: 'http://8.136.97.240:9000/gongchanjuan/', // 上传的图片服务器地址
  39. data: {
  40. key: '', //图片名字处理,
  41. },
  42. method: 'put',
  43. fullUrls: []
  44. }
  45. },
  46. props: {
  47. limit: {
  48. type: Number,
  49. default: 3
  50. },
  51. value: {
  52. type: String,
  53. default: ''
  54. }
  55. },
  56. // watch: {
  57. // value: function(val) {
  58. // console.log(val,"val");
  59. // console.log(this.value,"this.value");
  60. // this.split(val)
  61. // }
  62. // },
  63. created() {
  64. this.split(this.value)
  65. },
  66. methods: {
  67. handleRequest(params) {
  68. axios({
  69. url: this.uploadImgUrl,
  70. method: this.method,
  71. headers: {
  72. 'Content-Type': params.file.type
  73. },
  74. data: params.file
  75. }).then(() => {
  76. this.handleUploadSuccess()
  77. }).catch(() => {
  78. this.handleUploadError()
  79. })
  80. },
  81. getQiniuToken() {
  82. // getToken(this.form).then(response => {
  83. // if (response.code === 200) {
  84. // this.data.token = response.data;
  85. // } else {
  86. // this.msgError(response.msg);
  87. // }
  88. // });
  89. },
  90. async getMinioToken(fileName) {
  91. await getMinioToken(fileName).then(response => {
  92. if (response.code === 200) {
  93. this.uploadImgUrl = response.data
  94. console.log(this.uploadImgUrl)
  95. } else {
  96. this.msgError(response.msg)
  97. }
  98. })
  99. },
  100. split(str) {
  101. console.log("### split")
  102. this.fullUrls = []
  103. if (str == null) return
  104. var urlArr = str.split(',')
  105. if (this.limit > 0) {
  106. urlArr.slice(0, this.limit)
  107. }
  108. for (let index in urlArr) {
  109. var url = urlArr[index]
  110. if (url != null && url !== '') {
  111. this.fullUrls.push({ url: this.downloadImgUrl + url, fileName: url })
  112. }
  113. }
  114. },
  115. merge(arr) {
  116. if (arr == null && arr.length == 0) return ''
  117. let str = ''
  118. for (let i in arr) {
  119. var fileName = arr[i].fileName
  120. str += fileName + ','
  121. }
  122. if (str.length > 0) {
  123. str = str.substr(0, str.length - 1)
  124. }
  125. return str
  126. },
  127. handleUploadSuccess() {
  128. this.fullUrls.push({ url: this.downloadImgUrl + this.data.key, fileName: this.data.key })
  129. console.log(this.fullUrls)
  130. var str = this.merge(this.fullUrls)
  131. console.log(str)
  132. this.$emit('input', str)
  133. this.loading.close()
  134. },
  135. async handleBeforeUpload(file) {
  136. console.log(file)
  137. var index1 = file.name.lastIndexOf('.')
  138. var index2 = file.name.length
  139. var suffix = file.name.substring(index1 + 1, index2)
  140. this.data.key = `${createUploadPath()}.${suffix}`
  141. console.log(this.data.key)
  142. await this.getMinioToken(this.data.key)
  143. this.loading = this.$loading({
  144. lock: true,
  145. text: '上传中',
  146. background: 'rgba(0, 0, 0, 0.7)'
  147. })
  148. },
  149. handleRemove(val) {
  150. var uid = val.uid
  151. var index = this.fullUrls.findIndex(x => x.uid === uid)
  152. this.fullUrls.splice(index, 1)
  153. var str = this.merge(this.fullUrls)
  154. this.$emit('input', str)
  155. },
  156. handleUploadError() {
  157. this.$message({
  158. type: 'error',
  159. message: '上传失败'
  160. })
  161. this.loading.close()
  162. },
  163. handlePictureCardPreview(file) {
  164. this.dialogImageUrl = file.url
  165. this.dialogVisible = true
  166. }
  167. }
  168. }
  169. </script>
  170. <style lang="scss" scoped>
  171. .avatar {
  172. width: 100%;
  173. height: 100%;
  174. }
  175. //在样式把这个is-ready给隐藏掉就好了,就是这么Easy
  176. /deep/.el-upload-list__item.is-ready {
  177. display: none;
  178. }
  179. </style>

@EqualsAndHashCode()注解详解

原文中提到的大致有以下几点:

  1. 此注解会生成equals(Object other) 和 hashCode()方法。
  2. 它默认使用非静态,非瞬态的属性
  3. 可通过参数exclude排除一些属性
  4. 可通过参数of指定仅使用哪些属性
  5. 它默认仅使用该类中定义的属性且不调用父类的方法
  6. 可通过callSuper=true解决上一点问题。让其生成的方法中调用父类的方法。

另:@Data相当于@Getter @Setter @RequiredArgsConstructor @ToString @EqualsAndHashCode这5个注解的合集。

通过官方文档,可以得知,当使用@Data注解时,则有了@EqualsAndHashCode注解,那么就会在此类中存在equals(Object other) 和 hashCode()方法,且不会使用父类的属性,这就导致了可能的问题。

比如,有多个类有相同的部分属性,把它们定义到父类中,恰好id(数据库主键)也在父类中,那么就会存在部分对象在比较时,它们并不相等,却因为lombok自动生成的equals(Object other) 和 hashCode()方法判定为相等,从而导致出错。

@EqualsAndHashCode(callSuper = true),那就是用自己的属性和从父类继承的属性 来生成hashcode,所以结果为false;

修复此问题的方法很简单:

  1. 使用@Getter @Setter @ToString代替@Data并且自定义equals(Object other) 和 hashCode()方法,比如有些类只需要判断主键id是否相等即足矣。
  2. 或者使用在使用@Data时同时加上@EqualsAndHashCode(callSuper=true)注解。

来源 https://blog.csdn.net/c851204293/article/details/96989512

上传图片报 403 ,拦截权限问题

拦截权限问题

  1. // 使用 permitAll() 方法所有人都能访问,包括带上 token 访问
  2. .antMatchers("/admins/**").permitAll()
  3. // 使用 anonymous() 所有人都能访问,但是带上 token 访问后会报错
  4. .antMatchers("/admins/**").anonymous()

分页注意

分页必须要在查询列表的上一行代码

  1. startPage();
  2. // 查询网点列表
  3. List<Integer> branchIds = userService.listBranchIds();
  4. // 查询网点下的任务列表
  5. List<Task> tasks = taskService.listTaskIds();

这时候就会出现 userService.listBranchIds() 分页了

真正需要分页的 taskService.listTaskIds() 没有分页

正确的写法如下:

  1. // 查询网点列表
  2. List<Integer> branchIds = userService.listBranchIds();
  3. startPage();
  4. // 查询网点下的任务列表
  5. List<Task> tasks = taskService.listTaskIds();

nginx 配置静态资源目录,反向代理服务

  1. server
  2. {
  3. listen 80;
  4. server_name xxx.com;
  5. index index.php index.html index.htm default.php default.htm default.html;
  6. root /www/wwwroot/xxx.com/;
  7. location /
  8. {
  9. // 如果访问不到静态资源,就访问反向代理服务
  10. if ( !-e $request_filename)
  11. {
  12. proxy_pass http://127.0.0.1:8130;
  13. }
  14. }
  15. }

列表数据图片预览拼接全路径

ruoyi-vue - 图35

ruoyi-vue - 图36

引用代码如下:

  1. <el-table-column align="center" label="品牌LOGO" prop="logo" width="100">
  2. <template slot-scope="scope">
  3. <image-view imageStyle="width:20px;height:20px" v-model="scope.row.logo" />
  4. </template>
  5. </el-table-column>

imageView 代码如下

  1. <template>
  2. <div class="component-image-view">
  3. <span v-if="fullUrls.length===0" style="color:#c0c4cc;">
  4. 暂无
  5. </span>
  6. <el-image v-for="(item,index) in fullUrls"
  7. :style="imageStyle"
  8. :src="item"
  9. :preview-src-list="fullUrls" slot="reference">
  10. </el-image>
  11. </div>
  12. </template>
  13. <script>
  14. export default {
  15. data() {
  16. return {
  17. source:{
  18. //本地
  19. local:{
  20. downloadImgUrl: process.env.VUE_APP_BASE_API + "/upload",
  21. },
  22. //七牛
  23. qiniu:{
  24. downloadImgUrl: "http://qnimage.fangfujie.cn/",
  25. },
  26. //minio
  27. minio:{
  28. downloadImgUrl: "http://8.136.97.240:9000/gongchanjuan/",
  29. },
  30. // 阿里云
  31. ali:{
  32. downloadImgUrl: "http://youlishop.oss-cn-beijing.aliyuncs.com/",
  33. },
  34. },
  35. fullUrls:[],
  36. };
  37. },
  38. props: {
  39. value: {
  40. type: String,
  41. default: "",
  42. },
  43. imageStyle: {
  44. type: String,
  45. default: "width:100px;height:100px;margin-right:4px",
  46. },
  47. limit:{
  48. type:Number,
  49. default:-1,
  50. },
  51. to:{
  52. type: String,
  53. // 默认访问图片根地址
  54. default: "ali",
  55. },
  56. },
  57. watch: {
  58. value:function(val){
  59. this.value = val;
  60. this.init();
  61. },
  62. imageStyle:function (val) {
  63. this.style = val;
  64. this.init();
  65. },
  66. limit:function (val) {
  67. this.limit = val;
  68. this.init();
  69. },
  70. to:function (val) {
  71. this.to = val;
  72. this.init();
  73. }
  74. },
  75. created() {
  76. this.init();
  77. },
  78. methods: {
  79. init(){
  80. this.fullUrls = [];
  81. if (this.value==null)return
  82. var urlArr = this.value.split(",");
  83. if (this.limit>0){
  84. urlArr = urlArr.slice(0,this.limit)
  85. }
  86. for (let index in urlArr){
  87. var url = urlArr[index];
  88. if (url!=null && url!==''){
  89. this.fullUrls.push(this.source[this.to].downloadImgUrl+url);
  90. }
  91. }
  92. }
  93. },
  94. };
  95. </script>
  96. <style scoped lang="scss">
  97. </style>

手动关闭当前标签页

  1. this.$store.dispatch("tagsView/delView", this.$route);

修改表头背景颜色

ruoyi-vue - 图37

修改后

ruoyi-vue - 图38

  1. .el-table__header-wrapper, .el-table__fixed-header-wrapper {
  2. th {
  3. word-break: break-word;
  4. //background-color: #f8f8f9;
  5. background-color: #ffffff;
  6. color: #333333;
  7. //height: 20px;
  8. line-height: 23px;
  9. font-weight: 500;
  10. font-size: 12px;
  11. }
  12. }

路径地址: manage-ui/src/assets/styles/ruoyi.scss

ruoyi-vue - 图39

修改侧边栏主题

ruoyi-vue - 图40

manage-ui/src/settings.js

ruoyi-vue - 图41

ruoyi-vue - 图42

manage-ui/src/layout/components/Sidebar/Logo.vue

ruoyi-vue - 图43

修改左上角标题颜色

manage-ui/src/layout/components/Sidebar/Logo.vue

  1. & .sidebar-title {
  2. display: inline-block;
  3. margin: 0;
  4. // color 变成黑色
  5. color: #131313;
  6. font-weight: 600;
  7. line-height: 50px;
  8. font-size: 14px;
  9. font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
  10. vertical-align: middle;
  11. }

注释以下代码:

manage-ui/src/layout/components/Navbar.vue

  1. /*box-shadow: 0 1px 4px rgba(0,21,41,.08);*/

manage-ui/src/layout/components/TagsView/index.vue

  1. /*box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04);*/

manage-ui/src/assets/styles/sidebar.scss

  1. //-webkit-box-shadow: 2px 0 6px rgba(0,21,41,.35);
  2. //box-shadow: 2px 0 6px rgba(0,21,41,.35);

最後效果

ruoyi-vue - 图44

table展开后搜索问题

ruoyi-vue - 图45

  1. /** 查询订单列表 */
  2. getList() {
  3. this.loading = true;
  4. listOrder(this.queryParams).then(response => {
  5. if (response) {
  6. response.rows.map(item => {
  7. item.subOrderList = [];
  8. });
  9. this.orderList = response.rows;
  10. this.total = response.total;
  11. this.loading = false;
  12. // 搜索后置位空
  13. this.expands = [];
  14. }
  15. });
  16. },

ruoyi-vue - 图46

富文本上传图片

manage-ui/src/components/Editor/index.vue

  1. <!-- 图片上传组件辅助 -->
  2. <el-upload
  3. class="avatar-uploader quill-img"
  4. :action="host"
  5. :data="reqData"
  6. name="file"
  7. :show-file-list="false"
  8. :on-success="quillImgSuccess"
  9. :on-error="uploadError"
  10. :before-upload="quillImgBefore"
  11. accept='.jpg,.jpeg,.png,.gif,.jfif'
  12. ></el-upload>
  1. import { getToken } from "@/api/system/upload.js";
  1. data:{
  2. host:'http://youlishop.oss-cn-beijing.aliyuncs.com/',
  3. reqData: {
  4. },
  5. dir:"fuwenben",
  6. }
  1. methods: {
  2. // 获取阿里云数据
  3. async getAliyunToken(query) {
  4. const { data } = await getToken(query)
  5. this.reqData.OSSAccessKeyId = data.OSSAccessKeyId;
  6. this.reqData.policy = data.policy;
  7. this.reqData.signature = data.signature;
  8. this.reqData.success_action_status = 200;
  9. },
  10. // 富文本图片上传前
  11. async quillImgBefore(file) {
  12. let fileType = file.type;
  13. if(fileType === 'image/jpeg' || fileType === 'image/png' || fileType === 'image/jpg'){
  14. // 获取上传token
  15. var index1 = file.name.lastIndexOf(".");
  16. var index2 = file.name.length;
  17. var suffix = file.name.substring(index1 + 1, index2);
  18. var timestamp = Date.parse(new Date());
  19. this.reqData.key = this.dir+'/'+timestamp+"."+suffix;
  20. let query = {dir:this.dir};
  21. await this.getAliyunToken(query)
  22. console.log(this.reqData.key);
  23. return true;
  24. }else {
  25. this.$message.error('请插入图片类型文件(jpg/jpeg/png)');
  26. return false;
  27. }
  28. },
  29. quillImgSuccess(res, file) {
  30. // res为图片服务器返回的数据
  31. // 获取富文本组件实例
  32. let quill = this.$refs.quillEditor.quill;
  33. // 如果上传成功
  34. // 获取光标所在位置
  35. let length = quill.getSelection().index;
  36. // 插入图片
  37. quill.insertEmbed(length, "image", this.host + this.reqData.key);
  38. // 调整光标到最后
  39. quill.setSelection(length + 1);
  40. },

引入cards,卡片式统计

ruoyi-vue - 图47

页面引入

  1. <cards-data :cardLists="cardLists"></cards-data>
  2. import cardsData from '@/components/cards/index'
  3. components: {
  4. detailsFrom,
  5. orderSend,
  6. cardsData
  7. },
  8. data(){
  9. cardLists: [
  10. { name: '订单数量', count: '' },
  11. { name: '订单金额', count: '' },
  12. { name: '微信支付金额', count: '' },
  13. { name: '余额支付金额', count: '' },
  14. ],
  15. }

行拖拽

https://zhuanlan.zhihu.com/p/267865182

  1. npm install vuedraggable

图片按顺序显示

https://www.cnblogs.com/mianbaodaxia/p/11421092.html

前端接口请求超时配置

utils->request.js

image.png

image.png

设置el-table行高

  1. <el-table v-show="total > 0" v-loading="loading" :data="dataList"
  2. :row-style="{height:'50px'}"
  3. style="font-size: 15px" @selection-change="handleSelectionChange">

全局配置el-table字体大小

  1. .el-table{
  2. font-size: 15px
  3. }

动态配置表格

image.png

  1. <el-table-column v-for="column in tableColumnList" :label="column.name" align="center"
  2. >
  3. <!-- <el-table-column min-width="45%" label="数量(袋)" align="center" :prop="'wmsNum' + column.id">-->
  4. <!-- </el-table-column>-->
  5. <!-- <el-table-column min-width='55%' label="重量(千克)" align="center" :prop="'wmsWeight' + column.id">-->
  6. <!-- </el-table-column>-->
  7. <template slot-scope="scope">
  8. <div style="text-align: left;">{{ scope.row['wmsNum' + column.id] }}</div>
  9. <div style="text-align: left;">{{ scope.row['wmsWeight' + column.id] }}</div>
  10. </template>
  11. </el-table-column>

处理图片富文本中图片宽度最大100%

image.png

  1. <div v-html="formatContent(form.content)"></div>
  1. // 实现rich-text富文本中图片宽度最大100%
  2. formatContent(html){
  3. if(!html){
  4. return;
  5. }
  6. // 去掉img标签里的style、width、height属性
  7. let content_data= html.replace(/<img[^>]*>/gi,function(match,capture){
  8. match = match.replace(/style="[^"]+"/gi, '').replace(/style='[^']+'/gi, '');
  9. match = match.replace(/width="[^"]+"/gi, '').replace(/width='[^']+'/gi, '');
  10. match = match.replace(/height="[^"]+"/gi, '').replace(/height='[^']+'/gi, '');
  11. return match;
  12. }); // 修改所有style里的width属性为max-width:100%
  13. content_data = content_data.replace(/style="[^"]+"/gi,function(match,capture){
  14. match = match.replace(/width:[^;]+;/gi, 'max-width:100%;').replace(/width:[^;]+;/gi, 'max-width:100%;');
  15. return match;
  16. }); // 去掉<br/>标签
  17. content_data = content_data.replace(/<br[^>]*\/>/gi, ''); // img标签添加style属性:max-width:100%;height:auto
  18. content_data = content_data.replace(/\<img/gi, '<img style="max-width:100%;height:auto;display:block;margin:0px auto;"');
  19. return content_data;
  20. }