浏览代码

Merge remote-tracking branch 'origin/master'

heguanxun 8 月之前
父节点
当前提交
6fddd46b0c

+ 2 - 0
snowy-admin-web/src/snowy.js

@@ -1,6 +1,7 @@
 import * as antdvIcons from '@ant-design/icons-vue'
 import config from './config'
 import tool from './utils/tool'
+import util from './utils/util'
 import { hasPerm } from './utils/permission/index'
 import errorHandler from './utils/errorHandler'
 import customIcons from './assets/icons/index.js'
@@ -16,6 +17,7 @@ export default {
 		// 挂载全局对象
 		app.config.globalProperties.$CONFIG = config
 		app.config.globalProperties.$TOOL = tool
+		app.config.globalProperties.$util = util
 		app.config.globalProperties.hasPerm = hasPerm
 
 		// 注册常用组件

+ 576 - 0
snowy-admin-web/src/utils/util.js

@@ -0,0 +1,576 @@
+const util = {}
+util.isValue = (val) => {
+  if (val === null || val === undefined || (typeof val === 'string' && val.trim() === '') || (typeof val === 'object' && val?.length === 0)) {
+    return false
+  }
+  return true
+}
+
+util.structureParams = (array, attribute) => {
+  const endArray = []
+  array.forEach(v => {
+    endArray.push(v[attribute])
+  })
+  return endArray
+}
+
+util.replaceParams = (array, reArray, attribute, reAttribute) => {
+  const endArray = []
+  const endAllArray = []
+  array.forEach(v => {
+    reArray.forEach(rv => {
+      if (v === rv[attribute]) {
+        endArray.push(rv[reAttribute])
+        endAllArray.push(rv)
+      }
+    })
+  })
+  return {
+    replace: endArray,
+    all: endAllArray
+  }
+}
+
+util.copyObject = (ob) => {
+  return JSON.parse(JSON.stringify(ob))
+}
+
+util.arrayToMap = (array, key) => {
+  const map = new Map()
+  array.forEach((v) => {
+    map.set(v[key], v)
+  })
+  return map
+}
+
+/**
+ * 通过某个字段在一个多级数组中查找数据
+ * @param data 目标数组,不能包含函数
+ * @param current 目标数据
+ * @param key 查找的字段
+ * @param children 子集集合字段
+ */
+util.findInListData = (data, current, key = "id", children = 'children') => {
+
+  for(let item of data){
+
+    if(item[key] && JSON.parse(JSON.stringify(item[key])) == JSON.parse(JSON.stringify(current))) return item
+
+    if(!!item[children] && Array.isArray(item[children]) && item[children].length > 0){
+
+      const findChildData = findInListData(item[children], current, key, children)
+
+      if(findChildData) return findChildData
+
+    }
+
+  }
+
+  return null
+}
+
+util.formatGetParam = (params) => {
+  let paramUrl = ''
+  Object.keys(params).forEach((v, i) => {
+    paramUrl += i === 0 ? `${v}=${encodeURIComponent(params[v])}` : `&${v}=${encodeURIComponent(params[v])}`
+  })
+  return paramUrl
+}
+
+util.formatTableHeadFilters = (arr, text = 'dictLabel', value = 'dictValue') => {
+  return arr.map(v => {
+    v.value = v[value]
+    v.text = v[text]
+    return v
+  })
+}
+
+util.YMDHms = (date) => {
+  const _date = new Date(date)
+  const Y = `${_date.getFullYear()}`;
+  const M = `${_date.getMonth() + 1 < 10 ? `0${_date.getMonth() + 1}` : _date.getMonth() + 1}`;
+  const D = `${_date.getDate() < 10 ? `0${_date.getDate()}` : _date.getDate()}`;
+  const H = `${_date.getHours() < 10 ? `0${_date.getHours()}` : _date.getHours()}`;
+  const m = `${_date.getMinutes() < 10 ? `0${_date.getMinutes()}` : _date.getMinutes()}`;
+  const s = _date.getSeconds() < 10 ? `0${_date.getSeconds()}` : _date.getSeconds();
+  return `${Y}-${M}-${D} ${H}:${m}:${s}`;
+}
+
+util.YM = (date, format = false) => {
+  const _date = new Date(date)
+  const Y = `${_date.getFullYear()}`;
+  const M = `${_date.getMonth() + 1 < 10 ? `0${_date.getMonth() + 1}` : _date.getMonth() + 1}`;
+  return format ? `${Y}年${M}月` : `${Y}-${M}`;
+}
+util.M = (date, format = false) => {
+  const _date = new Date(date)
+  const M = `${_date.getMonth() + 1 < 10 ? `0${_date.getMonth() + 1}` : _date.getMonth() + 1}`;
+  return format ? `${M}月` : `${M}`;
+}
+
+util.YMD = (date, format = false) => {
+  const _date = new Date(date)
+  const Y = `${_date.getFullYear()}`;
+  const M = `${_date.getMonth() + 1 < 10 ? `0${_date.getMonth() + 1}` : _date.getMonth() + 1}`;
+  const D = `${_date.getDate() < 10 ? `0${_date.getDate()}` : _date.getDate()}`;
+  return format ? `${Y}年${M}月${D}日` : `${Y}-${M}-${D}`;
+}
+
+util.Hms = (date, format = false) => {
+  const _date = new Date(date)
+  const H = `${_date.getHours() < 10 ? `0${_date.getHours()}` : _date.getHours()}`;
+  const m = `${_date.getMinutes() < 10 ? `0${_date.getMinutes()}` : _date.getMinutes()}`;
+  const s = _date.getSeconds() < 10 ? `0${_date.getSeconds()}` : _date.getSeconds();
+  return format ? `${H}时${m}分${s}秒` : `${H}:${m}:${s}`;
+}
+
+//防抖
+util.debounce = function (cb, ms = 0) {
+  let timer = null
+  return function () {
+    if (timer) clearTimeout(timer)
+    timer = setTimeout(() => {
+      //  @ts-ignore
+      cb.apply(this, arguments)
+      timer = null
+    }, ms)
+  }
+}
+
+util.comTime = (time, format = false) => {
+  const sAll = time
+  const d = Math.floor(sAll / (1000 * 60 * 60 * 24))
+  const h = Math.floor((sAll - d * (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))
+  const m = Math.floor((sAll - d * (1000 * 60 * 60 * 24) - h * (1000 * 60 * 60)) / (1000 * 60))
+  const s = Math.floor((sAll - d * (1000 * 60 * 60 * 24) - h * (1000 * 60 * 60) - m * (1000 * 60)) / 1000)
+  let result = {d, h, m ,s}
+  if (format) {
+    result = ''
+    if (d > 0) {
+      result += d + '天'
+    }
+    if (d > 0 || h > 0) {
+      result += h + '时'
+    }
+    if (d > 0 || h > 0 || m > 0) {
+      result += m + '分'
+    }
+    result += s + '秒'
+  }
+  return result
+}
+
+util.comTimeByArea = (start, end, format = false, _D = '天', _H = '时', _M = '分', _S = '秒') => {
+  const sAll = new Date(end).getTime() - new Date(start).getTime()
+  const d = Math.floor(sAll / (1000 * 60 * 60 * 24))
+  const h = Math.floor((sAll - d * (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))
+  const m = Math.floor((sAll - d * (1000 * 60 * 60 * 24) - h * (1000 * 60 * 60)) / (1000 * 60))
+  const s = Math.floor((sAll - d * (1000 * 60 * 60 * 24) - h * (1000 * 60 * 60) - m * (1000 * 60)) / 1000)
+  let result = {d, h, m ,s}
+  if (format) {
+    result = ''
+    if (d > 0) {
+      result += d + _D
+    }
+    if (d > 0 || h > 0) {
+      result += h + _H
+    }
+    if (d > 0 || h > 0 || m > 0) {
+      result += m + _M
+    }
+    result += s + _S
+  }
+  return result
+}
+
+util.deepAssign = (...obj) => {
+  const result = Object.assign({}, ...obj)
+  for (let item of obj) {
+    for (let [idx, val] of Object.entries(item)) {
+      if (val instanceof Array) {
+        result[idx] = val
+      } else if (val instanceof Object) {
+        result[idx] = deepAssign(result[idx], val)
+      }
+    }
+  }
+  return result
+}
+
+util.copy = (value) => {
+  const str = document.createElement('input')
+  str.setAttribute('value', value)
+  document.body.appendChild(str)
+  str.select()
+  document.execCommand('copy')
+  document.body.removeChild(str)
+  console.log(value)
+}
+
+/**
+ *
+ * @param precision 精度  1、0.1 、0.01……
+ * @param colorArr
+ * [
+ *    [20.1, '#111111'],
+ *    [20.3, '#dddddd'],
+ *    [20.7, '#eeeeee'],
+ * ]
+ * @return colorMap
+ * new Map([
+ *    [20.1, '#111111']
+ *    ……
+ *    [20.3, '#dddddd']
+ *    ……
+ *    [20.7, '#eeeeee']
+ * ])
+ */
+util.getGradientColorArray = (precision, colorArr) => {
+  // 将hex表示方式转换为rgb表示方式(这里返回rgb数组模式)
+  const colorRgb = (sColor) => {
+    const reg = /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/;
+    let _sColor = sColor.toLowerCase();
+    if (_sColor && reg.test(_sColor)) {
+      if (_sColor.length === 4) {
+        let sColorNew = "#";
+        for (let i = 1; i < 4; i += 1) {
+          sColorNew += _sColor.slice(i, i + 1).concat(_sColor.slice(i, i + 1));
+        }
+        _sColor = sColorNew;
+      }
+      //处理六位的颜色值
+      const sColorChange = [];
+      for (let i = 1; i < 7; i += 2) {
+        // @ts-ignore
+        sColorChange.push(parseInt("0x" + _sColor.slice(i, i + 2)));
+      }
+      return sColorChange;
+    } else {
+      return _sColor;
+    }
+  };
+  // 将rgb表示方式转换为hex表示方式
+  const colorHex = (rgb) => {
+    const _this = rgb;
+    const reg = /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/;
+    if (/^(rgb|RGB)/.test(_this)) {
+      const aColor = _this.replace(/(?:(|)|rgb|RGB)*/g, "").split(",");
+      let strHex = "#";
+      for (let i = 0; i < aColor.length; i++) {
+        let hex = Number(aColor[i]).toString(16);
+        hex = Number(hex) < 10 ? 0 + '' + hex : hex;// 保证每个rgb的值为2位
+        if (hex === "0") {
+          hex += hex;
+        }
+        strHex += hex;
+      }
+      if (strHex.length !== 7) {
+        strHex = _this;
+      }
+      return strHex;
+    } else if (reg.test(_this)) {
+      const aNum = _this.replace(/#/, "").split("");
+      if (aNum.length === 6) {
+        return _this;
+      } else if (aNum.length === 3) {
+        let numHex = "#";
+        for (let i = 0; i < aNum.length; i += 1) {
+          numHex += (aNum[i] + aNum[i]);
+        }
+        return numHex;
+      }
+    } else {
+      return _this;
+    }
+  }
+  const rgb2hex = (sRGB) => {
+    const reg = /^(RGB|rgb)\((\d+),\s*(\d+),\s*(\d+)\)$/
+    if (!reg.test(sRGB)) return sRGB
+    const rgbArr = sRGB.match(/\d+/g)
+    const resultRgbArr = rgbArr.map((v) => {
+      if (+v > 16) return (+v).toString(16)
+      return '0' + (+v).toString(16)
+    })
+    return '#' + resultRgbArr.join('')
+  }
+  const gradientColor = (startColor, endColor, step) => {
+    const startRGB = colorRgb(startColor);//转换为rgb数组模式
+    const startR = startRGB[0];
+    const startG = startRGB[1];
+    const startB = startRGB[2];
+    const endRGB = colorRgb(endColor);
+    const endR = endRGB[0];
+    const endG = endRGB[1];
+    const endB = endRGB[2];
+    const sR = (endR - startR) / step;//总差值
+    const sG = (endG - startG) / step;
+    const sB = (endB - startB) / step;
+    const colorArr = [];
+    for (let i = 0; i <= step; i++) {
+      //计算每一步的hex值
+      const hex = colorHex('rgb(' + parseInt((sR * i + startR)) + ',' + parseInt((sG * i + startG)) + ',' + parseInt((sB * i + startB)) + ')');
+      // @ts-ignore
+      colorArr.push(rgb2hex(hex));
+    }
+    return colorArr;
+  }
+  const colorMap = new Map()
+  colorArr.forEach((v, i) => {
+    if (i < colorArr.length - 1) {
+      const _arr = gradientColor(v[1], colorArr[i + 1][1], (Number(colorArr[i + 1][0]) - Number(v[0])) / precision)
+      _arr.forEach((cV, cI) => {
+        colorMap.set((Number(v[0]) + cI * precision).toFixed(String(precision).split('').filter(p => p === '0').length), cV)
+      })
+    } else {
+      colorMap.set(Number(v[0]).toFixed(String(precision).split('').filter(p => p === '0').length), v[1])
+    }
+  })
+  return colorMap
+}
+// 根据部门名称 寻找上级所有父节点名称
+//data:要遍历的数据, target:查找目标, result用于装查找结果的数组
+util.findParent = (data, target, result) => {
+  for (let item of data) {
+    if (item.deptName === target) {
+      //将查找到的目标数据加入结果数组中
+      //可根据需求unshift(item.id)或unshift(item)
+      result.unshift(item.deptName);
+      return true;
+    }
+    if (item.children && item.children.length > 0) {
+      let isFind = findParent(item.children, target, result);
+      if (isFind) {
+        result.unshift(item.deptName);
+        return true;
+      }
+    }
+  }
+  return false;
+};
+
+// 涉及到跨域问题,需要配nginx代理的url地址处理
+util.proxyNginxUrl = (url) => {
+  if (url) {
+    //  @ts-ignore
+    const apiMapper = window.cusConfig?.nginxApiMapper || new Map()
+    let newUrl = url
+    apiMapper.forEach((v, k) => {
+      if (url.includes(k)) {
+        newUrl =  v + url.substring(url.indexOf(k) + k.length, url.length)
+      }
+    })
+    return newUrl
+  }
+  return url
+};
+
+//  后端接口拦截 <  > 内的文本,正反向格式化为  /《/   /》/    res = true 为接收返回值进行格式化
+util.formatInputHtmlInterceptor = (html, res = true) => {
+  if (html) {
+    const map = new Map()
+    map.set('<', '_《_')
+    map.set('>', '_》_')
+    let nHtml = html.toString()
+    map.forEach((v, k) => {
+      if (res) {
+        nHtml = nHtml.replace(eval(`/${v}/g`), k)
+      } else {
+        nHtml = nHtml.replace(eval(`/${k}/g`), v)
+      }
+    })
+    return nHtml
+  }
+  return html
+}
+
+
+
+//生成从minNum到maxNum的随机数
+// export  const randomNum = (minNum,maxNum) => {
+//   return parseInt(String(Math.random() * (maxNum - minNum + 1) + minNum),10);
+// }
+
+util.randomNum = (min = 0, max = 0, decimal=0) => {
+  // 获取数值的小数部分
+  const getDecimalNum = (data) => {
+    return Number(data.toString().split('.')[1]);
+  }
+  let min_z = Math.trunc(min); // 最小值的整数部分
+  let max_z = Math.trunc(max); // 最大值的整数部分
+  // 判断是否存在小数部分,不存在的话为0
+  let min_x = isNaN(getDecimalNum(min)) ? 0 : getDecimalNum(min);  // 最小值的小数部分
+  let max_x = isNaN(getDecimalNum(max)) ? 0 : getDecimalNum(max);  // 最大值的小数部分
+
+  // 区分有小数和没小数的情况
+  if (min_x > 0 || max_x > 0 || decimal > 0) {
+    // 整数部分随机数
+    let z = parseInt(String(Math.random() * (max_z - min_z + 1) + min_z), 10);
+    // 小数部分随机数
+    let x = 0;
+    // 小数部分随机数最大位数
+    let max_decimal = min_x.toString().length > max_x.toString().length ? min_x.toString().length : max_x.toString().length;
+    max_decimal = decimal > max_decimal ? decimal : max_decimal;
+    // 判断随机出的整数部分,是否等于最小值或者最大值
+    if(z == min_z || z == max_z){
+      if(z == min_z){
+        // 整数部分随机数等于最小值,那么应该从最小值的小数部分开始,到小数位数的最大值随机就可以
+        x = parseInt(String(Math.random() * (Math.pow(10, max_decimal) - min_x) + min_x), 10);
+      }else{
+        // 整数部分随机数等于最大值,那么应该从0开始,到最大值小数部分
+        x = parseInt(String(Math.random() * (max_x + 1)), 10);
+      }
+    }else{
+      // 整数部分在最大最小值区间的,就从0到小数位数的最大值随机就可以
+      x = parseInt(String(Math.random() * (Math.pow(10, max_decimal))), 10);
+    }
+    return Number(`${z}.${x}`);
+  } else {
+    return parseInt(String(Math.random() * (max_z - min_z + 1) + min_z), 10);
+  }
+}
+
+/**
+ *
+ * @param val 处理的数值
+ * @param unit  单位,默认空字符
+ * @param digits  小数,默认1
+ */
+util.formatNumberUnit = (val, unit = '', digits = 1) => {
+  let num = ''
+  let str = ''
+  if (String(val).length < 5) {
+    num = val
+    str = unit
+  } else if (String(val).length < 9) {
+    num = Number((Number(val) / 10000).toFixed(digits)) / 1 + ''
+    str = '万' + unit
+  } else if (String(val).length < 13) {
+    num = Number((Number(val) / 10000 / 10000).toFixed(digits)) / 1 + ''
+    str = '亿' + unit
+  }
+  return {
+    num: num, unit: str
+  }
+}
+
+/**
+ * 将一串数字str按指定间隔n填充指定字符char
+ * @param str
+ * @param n
+ * @param char
+ */
+util.numberJoinChar = (str, n = 3, char = ',') => {
+  const _str = String(str)
+  let result = '';
+  for (let i = _str.length - 1; i >= 0; i--) {
+    result = _str[i] + result;
+    if ((_str.length - i) % n === 0 && i !== 0) {
+      result = char + result;
+    }
+  }
+  return result;
+}
+// 根据起始周几获取本周第一天日期
+util.getFirstDateOfWeek = (fDate, firstDayOfWeek) => {
+  const day = fDate.getDay();
+  const diff = (day < firstDayOfWeek ? 7 : 0) + day - firstDayOfWeek;
+  const firstDate = new Date(fDate.getFullYear(), fDate.getMonth(), fDate.getDate() - diff);
+  return firstDate;
+}
+
+// 根据起始周几获取本周最后一天日期
+util.getLastDateOfWeek = (lDate, lastDayOfWeek) => {
+  const day = lDate.getDay();
+  const diff = (day < lastDayOfWeek ? 7 : 0) + day - lastDayOfWeek;
+  const lastDate = new Date(lDate.getFullYear(), lDate.getMonth(), lDate.getDate() + (6 - diff));
+  return lastDate;
+}
+
+util.oneDayTime = 1000 * 60 * 60 * 24
+
+//  根据当天时间、起始周几、选的月,获取整月日期
+util.getMonthCalendarData = (d, fD, selectMonth = null) => {
+  const getYMDTime = (date) => {
+    const _date = new Date(date)
+    const Y = _date.getFullYear()
+    const M = _date.getMonth() + 1
+    const D = _date.getDate()
+    return new Date(`${Y}-${M}-${D}`).getTime()
+  }
+  const _date = selectMonth ? new Date(selectMonth) : new Date(d)
+  // d的月份有几天
+  const monthDaysTotal = new Date(_date.getFullYear(), _date.getMonth() + 1, 0).getDate()
+  // d的月份的第一天
+  const monthFirstDate = new Date(_date.getFullYear(), _date.getMonth(), 1)
+  // 日历第一天
+  const wholeFirstDay = getFirstDateOfWeek(monthFirstDate, fD)
+  // d的月份的最后一天
+  const monthLastDate = new Date(_date.getFullYear(), _date.getMonth(), monthDaysTotal)
+  // 日历最后一天
+  const wholeLastDay = getLastDateOfWeek(monthLastDate, fD)
+  const arr = []
+  let w = []
+  for (let i = wholeFirstDay.getTime(); i <= wholeLastDay.getTime(); i += oneDayTime) {
+    const t = new Date(i)
+    const obj = {
+      date: t,
+      dateStr: YMD(t),
+      dayStr: t.getDate(),
+      tom: getYMDTime(t) > getYMDTime(d)
+    }
+    if (getYMDTime(t) < monthFirstDate.getTime()) {
+      obj.last = true //  是补全的日历过去时间
+    } else if (getYMDTime(t) > monthLastDate.getTime()) {
+      obj.will = true //  是补全的日历未来时间
+    } else if (getYMDTime(t) === getYMDTime(d)) {
+      obj.today = true // 是传入的当前日期
+    }
+    w.push(obj)
+    if (w.length === 7) {
+      const wObj = {
+        calendar: w
+      }
+      arr.push(wObj)
+      w = []
+    }
+  }
+  return arr
+}
+
+// 根据起始周几获取一周的数组
+util.getWeekCN = (xq) => {
+  const m = new Map([
+    [0, '日'],
+    [1, '一'],
+    [2, '二'],
+    [3, '三'],
+    [4, '四'],
+    [5, '五'],
+    [6, '六'],
+  ])
+  const weekArray = [];
+  for (let i = 0; i < 7; i++) {
+    const index = (xq + i) % 7;
+    weekArray.push(m.get(index));
+  }
+  return weekArray
+}
+
+util.resetRgbaOpacity = (polyColor, opacity) => {
+  // 使用正则表达式匹配rgba颜色中的透明度部分
+  const rgbaRegex = /rgba\((\d+),\s*(\d+),\s*(\d+),\s*([\d.]+)\)/;
+  const matches = polyColor.match(rgbaRegex);
+  if (!matches) {
+    throw new Error('Invalid rgba string');
+  }
+  // 提取原来的透明度值,并将其乘以给定因子
+  const oldOpacity = parseFloat(matches[4]);
+  const newOpacity = Math.min(Math.max(oldOpacity * opacity, 0), 1); // 确保透明度值在0到1之间
+  // 重新组合rgba字符串
+  const newRgbaStr = `rgba(${matches[1]}, ${matches[2]}, ${matches[3]}, ${newOpacity})`;
+  return newRgbaStr;
+}
+
+util.randomColor = (opacity) => `rgba(${randomNum(0, 255)}, ${randomNum(0, 255)}, ${randomNum(0, 255)}, ${opacity ? opacity : randomNum(0.5, 1, 1)})`
+
+export default util

+ 226 - 6
snowy-admin-web/src/views/auth/login/single-window-login.vue

@@ -65,20 +65,98 @@
 								<div class="agree">
 									<a-checkbox v-model:checked="loginAgree"/>
 									<span style="color:#222222">已阅读并同意</span>
-									<span style="color: #096ac6;text-decoration:underline dotted; cursor:pointer;" onclick="userNameProtocolAgree()">《用户服务协议》</span>
+									<span style="color: #096ac6;text-decoration:underline dotted; cursor:pointer;" @click="agreeTipsYhfwxy = true">《用户服务协议》</span>
 									<span>和</span>
-									<span style="color: #096ac6;text-decoration:underline dotted; cursor:pointer;" onclick="userNameProtocolPrivacy()">《用户隐私政策》</span>
+									<span style="color: #096ac6;text-decoration:underline dotted; cursor:pointer;"  @click="agreeTipsYhyszc = true">《用户隐私政策》</span>
 								</div>
-								<a-button type="primary" class="w-full mt-4" :loading="loading" round size="large" @click="login"
-								>{{ $t('login.signIn') }}
+								<a-button type="primary" class="w-full mt-4" :loading="loading" round size="large" @click="login">
+									{{ $t('login.signIn') }}
 								</a-button>
+								<div class="zhuce">还没有账号?<span>立即注册</span></div>
 							</a-form>
 						</div>
 					</div>
 				</div>
 			</div>
 		</div>
-		<div class="swl-foot" v-if="config.lang === 'zh-cn'"></div>
+		<div class="swl-foot" v-if="config.lang === 'zh-cn'">
+			<div class="swl-foot-main">
+				<div class="swl-foot-main-left">
+					<div class="swl-foot-main-left-top">
+						<div class="swlfmlt-item __hover"><a class="textUnderline footer-nav-text" href="https://www.singlewindow.cn/#/aboutUs" target="_blank">关于我们</a></div>
+						<div class="swlfmlt-split">|</div>
+						<div class="swlfmlt-item __hover"><a class="textUnderline footer-nav-text" href="https://www.singlewindow.cn/#/sitemap" target="_blank">网站地图</a></div>
+						<div class="swlfmlt-split">|</div>
+						<a-popover>
+							<template #content>
+								<a class="textUnderline footer-nav-text" target="_blank" href="http://www.gov.cn/hudong/ducha/2021-01/26/content_5582430.htm">
+									<img class="nav-img" src="https://www.singlewindow.cn/img/footer/hlwjd.jpg" alt="互联网+督查">
+								</a>
+							</template>
+							<div class="swlfmlt-item __hover"><a class="textUnderline footer-nav-text" target="_blank" href="http://www.gov.cn/hudong/ducha/2021-01/26/content_5582430.htm">互联网+督查</a></div>
+						</a-popover>
+						<div class="swlfmlt-split">|</div>
+						<a-popover>
+							<template #content>
+								<img class="nav-img" src="https://www.singlewindow.cn/img/footer/zwfw.jpg" alt="政务服务投诉">
+							</template>
+							<div class="swlfmlt-item __hover"><div class="swlfmlt-item __hover">政务服务投诉</div></div>
+						</a-popover>
+					</div>
+					<div class="swl-foot-main-left-bottom">
+						<div class="swlfmlb-item">主办单位:国家口岸管理办公室</div>
+						<div class="swlfmlb-item">承办单位:中国电子口岸数据中心</div>
+						<div class="swlfmlb-item">京ICP备12005222号-2</div>
+						<div class="swlfmlb-item"><a target="_blank" href="http://www.beian.gov.cn/portal/registerSystemInfo?recordcode=11011302001653">京公网安备11011302001653号</a></div>
+					</div>
+				</div>
+				<div class="swl-foot-main-right">
+					<div class="footer-code">
+						<div class="footer-code-top">
+							<a-popover>
+								<template #content>
+									<div style="display: flex;flex-direction: column;">
+										<img class="code-img" src="https://www.singlewindow.cn/img/footer/wechat-cl.jpg" alt="">
+										<span style="font-size: 12px;color: #333;line-height: 24px; text-align: center;">关注微信公众号</span>
+									</div>
+								</template>
+								<div class="wct-wechat"/>
+							</a-popover>
+							<a-popover>
+								<template #content>
+									<div style="display: flex;flex-direction: column;">
+										<img class="code-img" src="https://www.singlewindow.cn/img/footer/weibo-cl.jpg" alt="">
+										<span style="font-size: 12px;color: #333;line-height: 24px; text-align: center;">关注微博</span>
+									</div>
+								</template>
+								<div class="wct-weibo"></div>
+							</a-popover>
+						</div>
+						<div class="footer-code-bottom">
+							<div class="fcb-phone"/>服务热线:<span>95198</span>
+						</div>
+					</div>
+				</div>
+			</div>
+		</div>
+		<a-modal
+			v-model:visible="agreeTipsYhfwxy"
+			title="用户服务协议"
+			centered
+			width="730px"
+			@ok="agreeTipsYhfwxy = false, loginAgree = true"
+		>
+			<iframe src="https://app.singlewindow.cn/userserver/user/document/nraServiceAgreement" style="width: 100%; height: 530px;"/>
+		</a-modal>
+		<a-modal
+			v-model:visible="agreeTipsYhyszc"
+			title="用户隐私政策"
+			centered
+			width="730px"
+			@ok="agreeTipsYhyszc = false, loginAgree = true"
+		>
+			<iframe src="https://app.singlewindow.cn/userserver/user/document/nraPrivacyPolicy" style="width: 100%; height: 530px;"/>
+		</a-modal>
 	</div>
 </template>
 <script setup>
@@ -101,6 +179,8 @@
 	const validCodeBase64 = ref('')
 	const loading = ref(false)
 	const loginAgree = ref(false)
+	const agreeTipsYhfwxy = ref(false)
+	const agreeTipsYhyszc = ref(false)
 
 	const ruleForm = reactive({
 		account: 'superAdmin',
@@ -345,14 +425,154 @@
 						.agree {
 							font-size: 12px;
 						}
+						.zhuce {
+							margin-top: 40px;
+							width: 100%;
+							text-align: right;
+							font-size: 13px;
+							color: #999999;
+							>span {
+								color: #096AC6;
+								font-weight: 700;
+							}
+						}
 					}
 				}
 			}
 		}
 	}
 	.swl-foot {
-		height: 160px;
+		width: 100%;
+		height: 170px;
 		background-color: rgba(230, 230, 230, 1);
+		display: flex;
+		.swl-foot-main {
+			width: 1200px;
+			margin: 0 auto;
+			display: flex;
+			justify-content: space-between;
+			.swl-foot-main-left {
+				.swl-foot-main-left-top {
+					display: flex;
+					gap: 15px;
+					margin-top: 30px;
+					line-height: 40px;
+					.swlfmlt-item {
+						display: flex;
+						align-items: center;
+						line-height: 40px;
+						font-weight: 400;
+						font-style: normal;
+						font-size: 16px;
+						color: #0C3D65;
+						>a {
+							color: #0C3D65;
+							text-decoration: none;
+							&:hover {
+								text-decoration: underline;
+							}
+						}
+					}
+					.swlfmlt-split {
+						color: #bbbbbb;
+					}
+				}
+				.swl-foot-main-left-bottom {
+					margin-top: 10px;
+					display: grid;
+					grid-template-columns: repeat(2, 220px);
+					column-gap: 8px;
+					.swlfmlb-item {
+						display: block;
+						line-height: 26px;
+						color: #666;
+						font-size: 14px;
+						>a {
+							color: #666;
+							text-decoration: none;
+							&:hover {
+								text-decoration: underline;
+							}
+						}
+					}
+				}
+			}
+			.swl-foot-main-right {
+				.footer-code {
+					margin-top: 42px;
+					.footer-code-top {
+						display: flex;
+						justify-content: flex-end;
+						gap: 18px;
+						.wct-wechat {
+							width: 40px;
+							height: 40px;
+							background: #9eaebb;
+							line-height: 40px;
+							border-radius: 50%;
+							cursor: pointer;
+							position: relative;
+							display: flex;
+							align-items: center;
+							justify-content: center;
+							&:after {
+								position: absolute;
+								content: '';
+								background-image: url('https://www.singlewindow.cn/img/footer/bg.png');
+								width: 30px;
+								height: 26px;
+								background-position: -49px -83px;
+								transform: scale(.8);
+							}
+						}
+						.wct-weibo {
+							width: 40px;
+							height: 40px;
+							background: #9eaebb;
+							line-height: 40px;
+							border-radius: 50%;
+							cursor: pointer;
+							position: relative;
+							display: flex;
+							align-items: center;
+							justify-content: center;
+							&:after {
+								position: absolute;
+								content: '';
+								background-image: url('https://www.singlewindow.cn/img/footer/bg.png');
+								width: 32px;
+								height: 25px;
+								background-position: -3px -83px;
+								transform: scale(.8);
+							}
+						}
+					}
+					.footer-code-bottom {
+						display: flex;
+						align-items: center;
+						margin-top: 10px;
+						font-weight: 700;
+						font-size: 20px;
+						color: #0C3D65;
+						vertical-align: top;
+						height: 34px;
+						line-height: 34px;
+						.fcb-phone {
+							background-image: url('https://www.singlewindow.cn/img/footer/bg.png');
+							width: 34px;
+							height: 34px;
+							background-position: -1px -364px;
+							margin-right: 12px;
+							margin-top: -2px;
+							transform: scale(.8);
+						}
+						>span {
+							font-size: 24px;
+						}
+					}
+				}
+			}
+		}
 	}
 }
 </style>

+ 2 - 2
snowy-plugin/snowy-plugin-gen/src/main/java/vip/xiaonuo/gen/modular/basic/service/impl/GenBasicServiceImpl.java

@@ -668,8 +668,8 @@ public class GenBasicServiceImpl extends ServiceImpl<GenBasicMapper, GenBasic> i
         bindingJsonObject.set("editButtonId", IdWorker.getIdStr());
         // 删除按钮ID
         bindingJsonObject.set("deleteButtonId", IdWorker.getIdStr());
-        // 批量删除按钮ID
-        bindingJsonObject.set("batchDeleteButtonId", IdWorker.getIdStr());
+        // 批量导出按钮ID
+        bindingJsonObject.set("batchExportButtonId", IdWorker.getIdStr());
         // 作者
         bindingJsonObject.set("authorName", genBasic.getAuthorName());
         // 生成时间

+ 6 - 0
snowy-plugin/snowy-plugin-gen/src/main/resources/frontend/Api.js.btl

@@ -21,6 +21,12 @@ export default {
 	${classNameFirstLower}Delete(data) {
 		return request('delete', data)
 	},
+	// 导出${functionName}
+    ${classNameFirstLower}Export(data) {
+        return request('export', data, 'get', {
+            responseType: 'blob'
+        })
+    },
 	// 获取${functionName}详情
 	${classNameFirstLower}Detail(data) {
 		return request('detail', data, 'get')

+ 39 - 10
snowy-plugin/snowy-plugin-gen/src/main/resources/frontend/index.vue.btl

@@ -64,7 +64,7 @@
 				<% } %>
 				<% } %>
 				<a-col :span="6">
-					<a-button type="primary" @click="tableRef.refresh()">查询</a-button>
+					<a-button type="primary" @click="onSearch()">查询</a-button>
 					<a-button style="margin: 0 8px" @click="reset">重置</a-button>
 					<% if(searchCount > 3) { %>
 					<a @click="toggleAdvanced" style="margin-left: 8px">
@@ -97,10 +97,14 @@
 						新增
 					</a-button>
 					<xn-batch-delete
-						v-if="hasPerm('${classNameFirstLower}BatchDelete')"
+						v-if="hasPerm('${classNameFirstLower}Delete')"
 						:selectedRowKeys="selectedRowKeys"
 						@batchDelete="deleteBatch${className}"
 					/>
+					<a-button @click="onExport" v-if="hasPerm('${classNameFirstLower}BatchExport')">
+                        <template #icon><export-outlined /></template>
+                        批量导出
+                    </a-button>
 				</a-space>
 			</template>
 			<template #bodyCell="{ column, record }">
@@ -137,7 +141,7 @@
 			</template>
 		</s-table>
 	</a-card>
-	<Form ref="formRef" @successful="tableRef.refresh()" />
+	<Form ref="formRef" @successful="onSearch()" />
 </template>
 
 <script setup name="${busName}">
@@ -159,8 +163,11 @@
 	import { cloneDeep } from 'lodash-es'
 	import Form from './form.vue'
 	import ${classNameFirstLower}Api from '@/api/${moduleName}/${classNameFirstLower}Api'
+	import downloadUtil from "@/utils/downloadUtil";
+    const { proxy } = getCurrentInstance()
 	<% if (searchCount > 0) { %>
 	const searchFormState = ref({})
+	const searchFormStateReal = ref({}) // 点击搜索后备份的查询参数
 	const searchFormRef = ref()
 	<% } %>
 	const tableRef = ref()
@@ -213,30 +220,34 @@
 	}
 	const loadData = (parameter) => {
 		<% if (searchCount > 0) { %>
-		const searchFormParam = cloneDeep(searchFormState.value)
 		<% for(var i = 0; i < configList.~size; i++) { %>
 		<% if(!configList[i].needTableId && configList[i].needPage) { %>
 		<% if (configList[i].effectType == 'datepicker') {%>
 		// ${configList[i].fieldNameCamelCase}范围查询条件重载
-		if (searchFormParam.${configList[i].fieldNameCamelCase}) {
-			searchFormParam.start${configList[i].fieldNameCamelCaseFirstUpper} = searchFormParam.${configList[i].fieldNameCamelCase}[0]
-			searchFormParam.end${configList[i].fieldNameCamelCaseFirstUpper} = searchFormParam.${configList[i].fieldNameCamelCase}[1]
-			delete searchFormParam.${configList[i].fieldNameCamelCase}
+		if (searchFormStateReal.value.${configList[i].fieldNameCamelCase}) {
+			searchFormStateReal.value.start${configList[i].fieldNameCamelCaseFirstUpper} = searchFormStateReal.value.${configList[i].fieldNameCamelCase}[0]
+			searchFormStateReal.value.end${configList[i].fieldNameCamelCaseFirstUpper} = searchFormStateReal.value.${configList[i].fieldNameCamelCase}[1]
+			delete searchFormStateReal.value.${configList[i].fieldNameCamelCase}
 		}
 		<% } %>
 		<% } %>
 		<% } %>
-		return ${classNameFirstLower}Api.${classNameFirstLower}Page(Object.assign(parameter, searchFormParam)).then((data) => {
+		return ${classNameFirstLower}Api.${classNameFirstLower}Page(Object.assign(parameter, searchFormStateReal.value)).then((data) => {
 		<% } else { %>
 		return ${classNameFirstLower}Api.${classNameFirstLower}Page(parameter).then((data) => {
 		<% } %>
 			return data
 		})
 	}
+	// 搜索同时备份参数
+    const onSearch = (parameter) => {
+        searchFormStateReal.value = cloneDeep(searchFormState.value)
+        tableRef.value.refresh(parameter)
+    }
 	// 重置
 	const reset = () => {
 		searchFormRef.value.resetFields()
-		tableRef.value.refresh(true)
+		onSearch(true)
 	}
 	// 删除
 	const delete${className} = (record) => {
@@ -259,6 +270,24 @@
 			tableRef.value.clearRefreshSelected()
 		})
 	}
+	// 批量导出
+    const onExport = () => {
+        const params = {}
+        if (selectedRowKeys.value.length > 0) {
+            params.ids = selectedRowKeys.value
+        } else {
+            Object.entries(searchFormStateReal.value).forEach(([key, value]) => {
+                console.log(key)
+                if (proxy.$util.isValue(value)) {
+                    params[key] = value
+                }
+            })
+        }
+        ${classNameFirstLower}Api.${classNameFirstLower}Export(params).then((res) => {
+            downloadUtil.resultDownload(res)
+            tableRef.value.clearSelected()
+        })
+    }
 	<% if (searchCount > 0) { %>
 	<% for(var i = 0; i < configList.~size; i++) { %>
 	<% if(!configList[i].needTableId && configList[i].needPage) { %>

+ 58 - 29
snowy-plugin/snowy-plugin-gen/src/main/resources/frontend/index_inside.vue.btl

@@ -64,7 +64,7 @@
 				<% } %>
 				<% } %>
 				<a-col :span="6">
-					<a-button type="primary" @click="tableRef.refresh()">查询</a-button>
+					<a-button type="primary" @click="onSearch()">查询</a-button>
 					<a-button style="margin: 0 8px" @click="reset">重置</a-button>
 					<% if(searchCount > 3) { %>
 					<a @click="toggleAdvanced" style="margin-left: 8px">
@@ -97,10 +97,14 @@
 						新增
 					</a-button>
 					<xn-batch-delete
-						v-if="hasPerm('${classNameFirstLower}BatchDelete')"
+						v-if="hasPerm('${classNameFirstLower}Delete')"
 						:selectedRowKeys="selectedRowKeys"
 						@batchDelete="deleteBatch${className}"
 					/>
+					<a-button @click="onExport" v-if="hasPerm('${classNameFirstLower}BatchExport')">
+                        <template #icon><export-outlined /></template>
+                        批量导出
+                    </a-button>
 				</a-space>
 			</template>
 			<template #bodyCell="{ column, record }">
@@ -137,7 +141,7 @@
 			</template>
 		</s-table>
 	</a-card>
-	<Detail v-else ref="detailRef" @onClose="indexShow = true" @successful="tableRef.refresh()" />
+	<Detail v-else ref="detailRef" @onClose="indexShow = true" @successful="onSearch()" />
 </template>
 
 <script setup name="${busName}">
@@ -159,8 +163,11 @@
 	import { cloneDeep } from 'lodash-es'
 	import Detail from './detail.vue'
 	import ${classNameFirstLower}Api from '@/api/${moduleName}/${classNameFirstLower}Api'
+	import downloadUtil from "@/utils/downloadUtil";
+    const { proxy } = getCurrentInstance()
 	<% if (searchCount > 0) { %>
 	const searchFormState = ref({})
+	const searchFormStateReal = ref({}) // 点击搜索后备份的查询参数
 	const searchFormRef = ref()
 	<% } %>
 	const tableRef = ref()
@@ -213,32 +220,36 @@
 		}
 	}
 	const loadData = (parameter) => {
-		<% if (searchCount > 0) { %>
-		const searchFormParam = cloneDeep(searchFormState.value)
-		<% for(var i = 0; i < configList.~size; i++) { %>
-		<% if(!configList[i].needTableId && configList[i].needPage) { %>
-		<% if (configList[i].effectType == 'datepicker') {%>
-		// ${configList[i].fieldNameCamelCase}范围查询条件重载
-		if (searchFormParam.${configList[i].fieldNameCamelCase}) {
-			searchFormParam.start${configList[i].fieldNameCamelCaseFirstUpper} = searchFormParam.${configList[i].fieldNameCamelCase}[0]
-			searchFormParam.end${configList[i].fieldNameCamelCaseFirstUpper} = searchFormParam.${configList[i].fieldNameCamelCase}[1]
-			delete searchFormParam.${configList[i].fieldNameCamelCase}
-		}
-		<% } %>
-		<% } %>
-		<% } %>
-		return ${classNameFirstLower}Api.${classNameFirstLower}Page(Object.assign(parameter, searchFormParam)).then((data) => {
-		<% } else { %>
-		return ${classNameFirstLower}Api.${classNameFirstLower}Page(parameter).then((data) => {
-		<% } %>
-			return data
-		})
-	}
-	// 重置
-	const reset = () => {
-		searchFormRef.value.resetFields()
-		tableRef.value.refresh(true)
-	}
+        <% if (searchCount > 0) { %>
+        <% for(var i = 0; i < configList.~size; i++) { %>
+        <% if(!configList[i].needTableId && configList[i].needPage) { %>
+        <% if (configList[i].effectType == 'datepicker') {%>
+        // ${configList[i].fieldNameCamelCase}范围查询条件重载
+        if (searchFormStateReal.value.${configList[i].fieldNameCamelCase}) {
+            searchFormStateReal.value.start${configList[i].fieldNameCamelCaseFirstUpper} = searchFormStateReal.value.${configList[i].fieldNameCamelCase}[0]
+            searchFormStateReal.value.end${configList[i].fieldNameCamelCaseFirstUpper} = searchFormStateReal.value.${configList[i].fieldNameCamelCase}[1]
+            delete searchFormStateReal.value.${configList[i].fieldNameCamelCase}
+        }
+        <% } %>
+        <% } %>
+        <% } %>
+        return ${classNameFirstLower}Api.${classNameFirstLower}Page(Object.assign(parameter, searchFormStateReal.value)).then((data) => {
+        <% } else { %>
+        return ${classNameFirstLower}Api.${classNameFirstLower}Page(parameter).then((data) => {
+        <% } %>
+            return data
+        })
+    }
+    // 搜索同时备份参数
+    const onSearch = (parameter) => {
+        searchFormStateReal.value = cloneDeep(searchFormState.value)
+        tableRef.value.refresh(parameter)
+    }
+    // 重置
+    const reset = () => {
+        searchFormRef.value.resetFields()
+        onSearch(true)
+    }
 	// 删除
 	const delete${className} = (record) => {
 		let params = [
@@ -260,6 +271,24 @@
 			tableRef.value.clearRefreshSelected()
 		})
 	}
+	// 批量导出
+    const onExport = () => {
+        const params = {}
+        if (selectedRowKeys.value.length > 0) {
+            params.ids = selectedRowKeys.value
+        } else {
+            Object.entries(searchFormStateReal.value).forEach(([key, value]) => {
+                console.log(key)
+                if (proxy.$util.isValue(value)) {
+                    params[key] = value
+                }
+            })
+        }
+        ${classNameFirstLower}Api.${classNameFirstLower}Export(params).then((res) => {
+            downloadUtil.resultDownload(res)
+            tableRef.value.clearSelected()
+        })
+    }
 	// 切换至表单
     const onDetail = (record = null) => {
     	indexShow.value = false

+ 1 - 1
snowy-plugin/snowy-plugin-gen/src/main/resources/sqlend/Mysql.sql.btl

@@ -7,7 +7,7 @@ INSERT INTO `SYS_RESOURCE` VALUES ('${menuId}', '${parentId}', '${functionName}
 
 INSERT INTO `SYS_RESOURCE` VALUES ('${addButtonId}', '${menuId}', '新增${functionName}', NULL, '${classNameFirstLower}Add', 'BUTTON', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 1, NULL, 'NOT_DELETE', NULL, NULL, NULL, NULL);
 
-INSERT INTO `SYS_RESOURCE` VALUES ('${batchDeleteButtonId}', '${menuId}', '批量删除${functionName}', NULL, '${classNameFirstLower}BatchDelete', 'BUTTON', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 2, NULL, 'NOT_DELETE', NULL, NULL, NULL, NULL);
+INSERT INTO `SYS_RESOURCE` VALUES ('${batchExportButtonId}', '${menuId}', '批量导出${functionName}', NULL, '${classNameFirstLower}BatchExport', 'BUTTON', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 2, NULL, 'NOT_DELETE', NULL, NULL, NULL, NULL);
 
 INSERT INTO `SYS_RESOURCE` VALUES ('${editButtonId}', '${menuId}', '编辑${functionName}', NULL, '${classNameFirstLower}Edit', 'BUTTON', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 3, NULL, 'NOT_DELETE', NULL, NULL, NULL, NULL);