最新发布
-
微信小程序自定义Navbar自定义组件简单兼容写法 前引 在小程序项目中经常有要自定义navbar的需求,网上有很多大佬分享的教程,但有的不太兼容,有的又写的比较麻烦,这里分享一种本人在用兼容性较好而且代码比较简单的一种写法。 原理解释 微信小程序navbar头部可以拆分为几部分构成:状态栏高度、胶囊高度、胶囊上下两侧外边距高度 。如下图所示: 只要能取到各个部分的值,定位navbar和设置高度都会十分简单。 获取状态栏高度 const systemInfo = wx.getWindowInfo(); systemInfo.statusBarHeight胶囊按钮高度 用胶囊按钮的bottom - top const menuButtonInfo = wx.getMenuButtonBoundingClientRect(); menuButtonInfo.bottom - menuButtonInfo.top胶囊按钮的上下外边距 menuButtonInfo.top - systemInfo.statusBarHeight完整代码 ts代码 Component({ data: { navHeight: 0, // 胶囊高度 即 navbar高度 navWidth: 0, // 显示宽度 navMarginY: 0, // 胶囊上下外边距 statusBarHeight: 0, // 状态栏高度 }, lifetimes: { attached() { const systemInfo = wx.getWindowInfo(); const menuButtonInfo = wx.getMenuButtonBoundingClientRect(); this.setData({ statusBarHeight: systemInfo.statusBarHeight, navHeight: menuButtonInfo.bottom - menuButtonInfo.top, navMarginY: menuButtonInfo.top - systemInfo.statusBarHeight, navWidth: systemInfo.screenWidth - menuButtonInfo.right + menuButtonInfo.left - 10, }) }, }, })wxml代码 <view class="nav-box" style="padding-top: {{statusBarHeight}}px;"> <view style="height: {{navHeight}}px; margin: {{navMarginY}}px 0; " class="navbar"> <view>back</view> <view class="title">标题部分</view> </view> </view>wxss代码 .navbar { position: relative; display: flex; align-items: center; background-color: coral; color: #ffffff; padding: 0 16rpx; box-sizing: border-box; } .navbar .title { position: absolute; top: 0; left: 0; height: 100%; width: 100%; display: flex; justify-content: center; align-items: center; font-size: 32rpx; }这只是一个介绍原理的简单示例,具体代码要根据业务需求来做出修改
-
Java使用SXSSFWorkbook导出Excel工具类 列 1col2 (列2)col3 前引 在项目中经常有导出Excel的需求,为了方便就封装了一个导出Excel到前端的工具类,使用下来还比较好用 使用JDK版本:1.8 spring boot版本:2.6.15 前端请求:axios Java代码 Excel所需Maven依赖 <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>4.0.1</version> </dependency>完整代码如下: public class excelUtils { public static CellStyle headStyle(Workbook workbook) { // 创建单元格样式 CellStyle headerStyle = workbook.createCellStyle(); // 设置字体样式 Font headerFont = workbook.createFont(); headerFont.setBold(true); // 加粗 headerFont.setFontHeightInPoints((short) 12); // 字体大小 headerStyle.setFont(headerFont); // 可选:设置其他样式属性 headerStyle.setAlignment(HorizontalAlignment.CENTER); // 水平居中 headerStyle.setVerticalAlignment(VerticalAlignment.CENTER); // 垂直居中 return headerStyle; } public static CellStyle tableStyle(Workbook workbook) { CellStyle tableStyle = workbook.createCellStyle(); tableStyle.setAlignment(HorizontalAlignment.CENTER); // 水平居中 tableStyle.setVerticalAlignment(VerticalAlignment.CENTER); // 垂直居中 return tableStyle; } /** * @author Mango * * 获取对象的字段值 * @param object 对象 * @param key 字段名 * @return 字段值 */ public static String getFieldString(Object object, String key) { if(object instanceof Map){ return String.valueOf(((Map<?, ?>) object).get(key)); } Field field = null; String value = ""; try { field = object.getClass().getDeclaredField(key); field.setAccessible(true); String orgStr = String.valueOf(field.get(object)); if (orgStr != null) { value = orgStr; // 处理数组类型 if (orgStr.startsWith("[") && orgStr.endsWith("]")) { value = orgStr .replace("[", "") .replace("]", "") .replace("\"", ""); } } } catch (NoSuchFieldException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new RuntimeException(e); } return value; } /** * @author Mango * 导出Excel * @param response HttpServletResponse * @param srcList 数据源 * @param keys 数据源对应字段列表 * @param titles 表头 * @param excelName 文件名 */ public static void exportExcel(HttpServletResponse response, List<?> srcList, List<String> keys, List<String> titles, String excelName) { // 创建workbook // 使用 SXSSFWorkbook 比 XSSFWorkbook 内存占用少 SXSSFWorkbook workbook = new SXSSFWorkbook(); // 根据workbook创建sheet Sheet sheet = workbook.createSheet(excelName); //设置表头 Row titleRoe = sheet.createRow(0); // 设置表头样式 CellStyle headStyle = headStyle(workbook); // 向单元格中添加表头数据 for (int i = 0; i < titles.size(); i++) { Cell cell = titleRoe.createCell(i); cell.setCellValue(titles.get(i)); cell.setCellStyle(headStyle); } // 定义数据样式 CellStyle tableStyle = tableStyle(workbook); // 处理表格数据 for (int i = 0; i < srcList.size(); i++) { // 获取一组数据 Object object = srcList.get(i); // 生成行 Row rowData = sheet.createRow(i + 1); // 设置行高 rowData.setHeight((short) 400); // 向每一行中的单元格添加数据 // keys 为对象字段列表 for (int j = 0; j < keys.size(); j++) { String key = keys.get(j); Cell cell = rowData.createCell(j); cell.setCellStyle(tableStyle); String value = getFieldString(object, key); cell.setCellValue(value); } // 每100行就写一次磁盘,减少内存消耗 if (i % 100 == 0) { try { ((SXSSFSheet) sheet).flushRows(100); // 将缓存中的数据写入磁盘 } catch (IOException e) { throw new RuntimeException(e); } } } // 获取当前时间作为文件名 LocalDateTime localDateTime = LocalDateTime.now(); String fileName = excelName + localDateTime.format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")) + ".xlsx"; // 设置响应头 try { response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8")); } catch (UnsupportedEncodingException e) { // 发生异常不处理中文文件名 response.setHeader("Content-Disposition", "attachment;filename=" + fileName); throw new RuntimeException(e); } response.setContentType("application/vnd.ms-excel;charset=utf-8"); response.setHeader("Accept-Ranges", "bytes"); // 返回数据 返回进度条 try (ByteArrayOutputStream fileOutputStream = new ByteArrayOutputStream(); ServletOutputStream out = response.getOutputStream()) { // 这里假设 workbook 是生成的 Excel 文件对象 workbook.write(fileOutputStream); // 获取文件的总大小,并设置响应头 byte[] fileBytes = fileOutputStream.toByteArray(); response.setHeader("Content-Length", String.valueOf(fileBytes.length)); // 分块传输文件内容 int chunkSize = 256; // 每次写入数据字节 int totalSize = fileBytes.length; int currentPosition = 0; while (currentPosition < totalSize) { int remaining = totalSize - currentPosition; int chunk = Math.min(chunkSize, remaining); // 写入一块数据 out.write(fileBytes, currentPosition, chunk); out.flush(); // 刷新输出流,确保数据传送给客户端 // 更新当前下载位置 currentPosition += chunk; } out.close(); workbook.close(); // 关闭工作簿 } catch (IOException e) { // 处理异常 response.reset(); response.setContentType("application/json"); response.setCharacterEncoding("utf-8"); Map<String, String> map = new HashMap<>(); map.put("message", "下载文件失败: " + e.getMessage()); try { response.getWriter().println(JSON.toJSONString(map)); } catch (IOException ee) { ee.printStackTrace(); } e.printStackTrace(); } } }前端请求示例 请求接口 export const outExcel = async ( data: any, downloadProgress: Function, ) => { return await request({ url: "/test/outExcel", method: "post", data, responseType: "blob", onDownloadProgress: (event: any) => { downloadProgress(event); }, }); }注: 这里的代码是已经封装过的统一封装的axios responseType: “blob” 指定文件为二进制 onDownloadProgress利用回调函数返回下载进度 下载示例 const exportExcel = async () => { isProcessing.value = true; let response = null; try { response = await props.downloadMethod(props.searchForm, (event: any) => { progress.value = (event.loaded / event.total) * 100; if (progress.value > 0) { isProcessing.value = false; } }); } catch (e: any) { isProcessing.value = false; //出错时结束处理状态 isError.value = true; //下载失败 } if (response == null) return; // 没有返回数据 if (!response.status) { isError.value = true; //接口返回错误设置为下载失败 } const fileName = "教师参与情况" + formatDate(new Date(), "yy-MM-dd hh:mm:ss") + ".xlsx"; const blob = new Blob([response.data], { type: "application/vnd.ms-excel" }); // 创建一个链接来下载 Blob 对象 const downloadLink = document.createElement("a"); downloadLink.href = URL.createObjectURL(blob); downloadLink.download = decodeURI(fileName); downloadLink.style.display = "none"; document.body.appendChild(downloadLink); // 触发点击事件以下载文件 downloadLink.click(); // 清理创建的链接对象 document.body.removeChild(downloadLink); };将props.downloadMethod替换为下载接口 props.searchFormt替换为筛选数据
-
keep-alive使用记录(返回界面不刷新) 前引 keep-alive是vue自带的一个内置组件,可以实现缓存包裹在其中的动态切换组件。 {% link Vue,Built-in Components,https://cn.vuejs.org/api/built-in-components#keepalive %} 我这里是和路由结合全局使用 全局添加keep-alive 在界面切换的router-view处做修改 <router-view v-slot="{ Component }"> <keep-alive :include="cachedRoutes"> <component :is="Component" :key="$route.fullPath" /> </keep-alive> </router-view>cachedRoutes:为需要缓存界面路由name,可以根据使用情况设置,我这里写到了pinia中 export const settingStore = defineStore("setting", { state: () => { return { cachedRoutes: Array<string>(), // 缓存的路由名称列表 }; }, getters: { getCachedRoutes(): string[] { return this.cachedRoutes; }, }, actions: { addRouteToCache(routeName: string) { if (!this.cachedRoutes.includes(routeName)) { this.cachedRoutes.push(routeName); } }, removeRouteFromCache(routeName: string) { const index = this.cachedRoutes.indexOf(routeName); if (index !== -1) { this.cachedRoutes.splice(index, 1); } }, }, });设置路由 路由定义在需要缓存的界面路由mate中添加keepAlive: true { path: "/test/pageA", name: "pageA", meta: { title: "界面A", keepAlive: true }, component: () => import( "@/views/test/pageA/index.vue" as string ), }设置路由守卫,通过路由守卫向cachedRoutes 中添加需要缓存页面name const setRouteCache = (event: any) => { if (event.state) { const { name, meta } = currentRoute as any; if (meta.keepAlive) settingStore().removeRouteFromCache(name); } }; // 监听浏览器后退事件 window.addEventListener("popstate", setRouteCache); router.beforeEach(async (to: any, _from: any, next) => { // 设置浏览器标题 document.title = to.meta.title; window.history.state.action = to.name; currentRoute = _from; if (to.matched) { to.matched.forEach((record: any) => { if (record.name && record.components?.default) { const component = record.components.default; if (typeof component === "function") { // 处理异步组件 component().then((resolved: any) => { if (!resolved.default.name) { resolved.default.name = record.name; } }); } else if (!component.name) { // 处理普通组件 component.name = record.name; } } }); } if (to.meta.keepAlive) { const settingSate = settingStore(); settingSate.addRouteToCache(to.name); } next(); });其中下面这部分代码是设置页面name和路由的name绑定如果一致的可以去除 if (to.matched) { to.matched.forEach((record: any) => { if (record.name && record.components?.default) { const component = record.components.default; if (typeof component === "function") { // 处理异步组件 component().then((resolved: any) => { if (!resolved.default.name) { resolved.default.name = record.name; } }); } else if (!component.name) { // 处理普通组件 component.name = record.name; } } }); }
-
使用dpkg-deb对deb文件解压、重新打包 前引 aic8800d80相关驱动在最新的linux内核中无法安装,在网上找了很多别人重新编译的驱动都无法正常使用,后来才了解到deb文件可以解压出源码文件,在针对错误进行修改后又可以重新打包。 命令 安装 apt install dpkg 解压deb文件 dpkg-deb -x org.deb ./src/将deb文件解压到一个文件夹中 修改源码文件 为了后续重新打包最好不要改变源代码文件结构 解压出包的控制信息 dpkg-deb -e org.deb ./src/DEBIAN 重新打包 dpkg-deb -b ./src new.deb 安装 dpkg -i new.deb
-
树莓派安装AIC8800d80驱动 环境信息 系统版本:6.6.51+rpt-rpi-v8 #1 SMP PREEMPT Debian 1:6.6.51-1+rpt2 (2024-10-01) aarch64 GNU/Linux USB WIFI设备:TL-XDN7000H 设备芯片:aic8800d80 查看设备连接状态 lsusb如果在usb设备列表中看到类似 ID a69c:5721 aicsemi AIC Wlan 的输出,是将设备识别为了存储模式,需要将存储模式修改为wlan模式 切换模式(非必须) 使用命令 sudo usb_modeswitch -KQ -v a69c -p 5721将存储模式修改为wlan模式 命令模式需要在每次启动都需要输入,比较麻烦,可以使用udev来管理设备 sudo vim /etc/udev/rules.d/70-usb-modeswitch.rules粘贴以下内容 ACTION=="add", SUBSYSTEM=="usb", ATTR{idVendor}=="a69c", ATTR{idProduct}=="5721", RUN+="/usr/sbin/usb_modeswitch -v a69c -p 5721 -K"重新加载规则 sudo udevadm control --reload-rules sudo udevadm triggerimage-20241009142645898图片 安装驱动 我的TP-Link官方驱动在arm架构中安装失败,很可能是因为我的内核版本比较高,导致很多库不支持。 这里找到了绿联的AIC8800D80驱动,和我使用的TL-XDN7000H芯片一致 驱动下载地址: {% link 绿联,驱动下载,绿联WiFi6 USB无线网卡AIC8800D80驱动,https://www.lulian.cn/download/135.html %} 驱动下载链接:绿联WiFi6 USB无线网卡AIC8800D80驱动 文件中有详细安装教程,这里不再赘述 我这里直接安装了里面的aic8800d80fdrvpackage_2024_0606.deb文件 image-20241009143713835图片 成功驱动TL-XDN7000H image-20241009143856932图片
-
NextCloud更新404解决办法 前引 最近两个版本的NextCloud更新时更新器出现404导致无法更新 image-20240905151009153图片 之前的版本都正常 Snipaste_2024-09-05_09-13-15图片 解决办法:使用命令手动更新 通过手动运行更新命令,更新nextcloud版本 在nextcloud根目录中运行命令: sudo -u www-data php updater/updater.pharimage-20240905151351460图片 基本流程和在有界面中是一样 在更新过程中避免用户访问造成错误,可以启动维护模式 sudo -u www-data php occ maintenance:mode --on关闭维护模式 sudo -u www-data php occ maintenance:mode --off下载失败解决办法 由于国内网络等原因,可能会出现更新下载失败 可以在本地将新的 nextcloud 下载后上传到服务器中,替换掉data/updater-*/downloads/(nextcloud安装目录)的文件,同时修改data/updater-*/下的文件 .step 替换文件中 start 为 stop {"state":"stop","step":4}再重新运行更新命令即可
-
EC20通过gammu接收短信再转发优化 前引 在接收到短信之后会将内容较大的短信内容拆分到不同文件中保存,在之前的自动转发脚本转发短信文本内容不全 image-20240723105748080图片 解决 最近在阅读gammu官方文档的时候发现官方有个Processing message text in Python 的示例: Following script (if used as RunOnReceive handler) written in Python will concatenate all text from received message: #!/usr/bin/env python import os numparts = int(os.environ["DECODED_PARTS"]) text = "" # Are there any decoded parts? if numparts == 0: text = os.environ["SMS_1_TEXT"] # Get all text parts else: for i in range(1, numparts + 1): varname = "DECODED_%d_TEXT" % i if varname in os.environ: text = text + os.environ[varname] # Do something with the text print("Number {} have sent text: {}".format(os.environ["SMS_1_NUMBER"], text))这个python脚本能将在不同文件中的短信内容拼接,刚好能够实现我们的需求,将这个python代码转写为shell代码,和之前的短信转发代码组合后的新代码: #!/bin/bash numparts=${DECODED_PARTS} text="" if [ "$numparts" -eq 0 ]; then text=${SMS_1_TEXT} else for i in $(seq 1 $numparts); do varname="DECODED_${i}_TEXT" if [ ! -z "${!varname}" ]; then text="${text}${!varname}" fi done fi echo "${text}" |mutt -s "树莓派转发${SMS_1_NUMBER}短信" mstzf@foxmail.com就能够完美实现短信转发功能 gammu官网链接: {% link RunOnReceive,Gammu,https://docs.gammu.org/smsd/run.html#index-7 %} ec20模块使用: {% link 树莓派+EC20模块实现连接蜂窝网和短信收发,TeohZY,https://blog.teohzy.com/post/raspi-ec20/index.html %}
-
为你的Typecho博客添加一个访问统计 前引 在之前的静态Hexo 中借助的是第三方的统计平台,现在转到动态博客下,有了自己的数据库就没有必要再使用第三方的统计;于是就有了typecho访问统计插件TeohVist github地址: {% link github,TeohVisit,https://github.com/mstzf/TeohVisit.git %} 插件实现的功能非常简单,就只是记录了访问数据,和在需要的界面提供数据 安装 将代码克隆到typecho插件目录,在管理后台找到 TeohVisit 之间开启使用即可 git clone https://github.com/mstzf/TeohVisit.git使用 具体使用需要修改主题源码,或者自定义独立界面模板 插件提供了两种读取数据的方法 getStat:返回所需要的单个值,具体值为:today,yesterday,month,total getAllStats:返回全部数据 示例: if (TeohVisit_Plugin::isPluginEnabled()) { $stats = TeohVisit_Plugin::getAllStats(); $data = [ 'today_unique_visitors' => $stats['today']['unique_visitors'], 'today_views' => $stats['today']['views'], 'yesterday_unique_visitors' => $stats['yesterday']['unique_visitors'], 'yesterday_views' => $stats['yesterday']['views'], 'month_total_views' => $stats['month']['total_views'], 'total_total_views' => $stats['total']['total_views'] ]; }