首页
留言
友链
关于
Search
1
思源笔记docker私有化部署及使用体验分享
2,621 阅读
2
windows11 远程提示:为安全考虑,已锁定该用户帐户,原因是登录尝试或密码更改尝试过多。
1,170 阅读
3
解决 nginxProxyManager 申请证书时的SSL失败问题
753 阅读
4
Pointer-Focus:一款功能强大的教学、录屏辅助软件
707 阅读
5
使用cspell对项目做拼写规范检查
621 阅读
Web前端
CSS
JavaScript
交互
Vue
小程序
后端
运维
项目
生活
其他
转载
软件
职场
登录
Search
标签搜索
docker
DevOps
magic-boot
Linux
酷壳
frp
RabbitMQ
gitlab
Node
git
工具
MybatisPlus
clickhouse
Syncthing
规范
前端
产品
nginx
markdown
axios
朱治龙
累计撰写
149
篇文章
累计收到
9
条评论
首页
栏目
Web前端
CSS
JavaScript
交互
Vue
小程序
后端
运维
项目
生活
其他
转载
软件
职场
页面
留言
友链
关于
搜索到
149
篇与
朱治龙
的结果
2023-03-14
用*输出一个等腰三角形
缘起工作间隙,得到一个刚学编程时的题目,题目如下:用输出一个等腰三角形。提示用户输入一个整数 ,代表输出的等边三角形由n行组成。例:输入n=5。输出: * *** ***** ******* *********朋友提供的代码如下:public static void main(String[] args) { System.out.println("请输入需要的行数(不可输入0):"); Scanner scanner = new Scanner(System.in); int rows = scanner.nextInt(); int rowIndex = rows - 1; //最后一行总个数 int total = 1 + (rows - 1) * 2; // 每行*个数 int num = 1; // 打印标记 boolean flag = false; if (rows != 0) { for (int i = 0; i < rows; i++) { for (int j = 0, k = 0; j < total; j++) { // 倒数第几行就从第几个下标开始 if (j == rowIndex - i) { flag = true; } if (flag && k < num) { System.out.print("*"); // 开始打印计数 k++; } else { System.out.print(" "); // 结束打印 flag = false; } } num = num + 2; System.out.println(); } } }整段代码看下来实现方式有些复杂,感觉有些陷在循环里了,脑袋里不跟着一行一行的跑代码的话,不怎么能想出来结果输出是怎样的。于是,我从头开始梳理了下需要输出的东西,主要是要从中找寻规律:第一步:手工输出前几个结果,从结果中找规律2: * *** 3: * *** ***** 4: * *** ***** ******* 5: * *** ***** ******* ********* 6: * *** ***** ******* ********* *********** 7: * *** ***** ******* ********* *********** *************第二步:分析得出如下规律:令 行数为1开始,当前行的输出情况如下:星号数量 = 当前行数 * 2 - 1左侧空格数量 = 总行数 - 当前行数得出规律后,就能够很好的着手后面的代码工作了,以下是我提供的代码:JS版:/** * 输出三角形 * @param rows 三角形行数 */ let showTriangle = (rows) => { console.log(`输出${rows}等腰三角形:`) // 如果小于2的话不能构成等腰三角形 if (rows < 2) { console.error('必须大于1') return; } for (let row = 1; row <= rows; row++) { // 输出空格 let rowStr = '' for (let blankIndex = 0; blankIndex < rows - row; blankIndex++) { rowStr += ' ' } // 输出星号 for (let starIndex = 0; starIndex < row * 2 - 1; starIndex++) { rowStr += '*' } console.log(rowStr) } } // 测试输出: for (let i = 0; i < 15; i++) { showTriangle(i) }输出结果:Java 版由于使用了 String.repeat(), JDK 版本应 >= 11,若 JDK 低于 11 也可以用循环实现。import java.util.InputMismatchException; import java.util.Scanner; /** * @author zhuzl */ public class ShowTriangle { public static void main(String[] args) { System.out.println("请输入三角形的行数(需大于1):"); Scanner scanner = new Scanner(System.in); int rows = scanner.nextInt(); printTriangle(rows); } /** * 打印等腰三角形 * * @param rows 三角形行数 */ private static void printTriangle(int rows) { if (rows < 2) { throw new InputMismatchException("必须大于1"); } for (int row = 1; row <= rows; row++) { System.out.println(" ".repeat(rows - row) + "*".repeat(row * 2 - 1)); } } }输出结果如下:
2023年03月14日
16 阅读
0 评论
0 点赞
2022-11-04
CentOS安装LibreOffice记录
LibreOffice安装下载地址:https://zh-cn.libreoffice.org/download/libreoffice/下载最新的Windows版本,目前最新文档版本是7.3.7。下载后正常安装即可。下载LibreOffice:# 下载主安装程序 wget https://mirrors.ustc.edu.cn/tdf/libreoffice/stable/7.3.7/rpm/x86_64/LibreOffice_7.3.7_Linux_x86-64_rpm.tar.gz # 下载中文字体支持包 wget https://mirrors.ustc.edu.cn/tdf/libreoffice/stable/7.3.7/rpm/x86_64/LibreOffice_7.3.7_Linux_x86-64_rpm_langpack_zh-CN.tar.gz解压tar -zxvf LibreOffice_7.3.7_Linux_x86-64_rpm.tar.gz安装# 进入安装目录 cd LibreOffice_7.3.7_Linux_x86-64_rpm/RPMS/ # 安装 yum localinstall -y *.rpm程序路径:/opt/libreoffice7.3/program/soffice启动nohup /opt/libreoffice7.3/program/soffice --headless --accept="socket,host=127.0.0.1,port=8100;urp;" --nofirststartwizard &
2022年11月04日
71 阅读
0 评论
0 点赞
2022-11-04
CentOS 安装 OpenOffice记录
项目有需要用到OpenOffice进行word文档解析,需在服务器部署OpenOffice,以下为安装记录。下载地址:https://www.openoffice.org/download/index.html当前最新版本是4.1.13。下载后正常安装即可。下载wget http://jaist.dl.sourceforge.net/project/openofficeorg.mirror/4.1.13/binaries/zh-CN/Apache_OpenOffice_4.1.13_Linux_x86-64_install-rpm_zh-CN.tar.gz解压tar -zxvf Apache_OpenOffice_4.1.13_Linux_x86-64_install-rpm_zh-CN.tar.gz安装cd zh-CN/RPMS/ rpm -ivh *.rpm 启动服务nohup /opt/openoffice4/program/soffice -headless -accept="socket,host=127.0.0.1,port=8100;urp;" -nofirststartwizard >/dev/null 2>&1 & 若启动过程中提示如下信息:no suitable windowing system found, exiting.。可执行如下代码安装缺失的依赖。yum groupinstall "X Window System"自启动将以上启动服务的代码添加到 /etc/rc.d/rc.local最底部,或自行参考Linux添加服务的方式实现自启动。
2022年11月04日
30 阅读
0 评论
0 点赞
2022-10-14
20 个 JS 工具函数助力高效开发
前言日常开发中,面对各种不同的需求,我们经常会用到以前开发过的一些工具函数,把这些工具函数收集起来,将大大提高我们的开发效率。1、校验数据类型export const typeOf = function(obj) { return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase() } // Example typeOf('zhuzl') // string typeOf([]) // array typeOf(new Date()) // date typeOf(null) // null typeOf(true) // boolean typeOf(() => { }) // function 2、防抖export const debounce = (() => { let timer = null return (callback, wait = 800) => { timer&&clearTimeout(timer) timer = setTimeout(callback, wait) } })() // Example // 如 vue 中使用 methods: { loadList() { debounce(() => { console.log('加载数据') }, 500) } }3、节流export const throttle = (() => { let last = 0 return (callback, wait = 800) => { let now = +new Date() if (now - last > wait) { callback() last = now } } })() 4、手机号脱敏export const hideMobile = (mobile) => { return mobile.replace(/^(\d{3})\d{4}(\d{4})$/, "$1****$2") }5、开启全屏export const launchFullscreen = (element) => { if (element.requestFullscreen) { element.requestFullscreen() } else if (element.mozRequestFullScreen) { element.mozRequestFullScreen() } else if (element.msRequestFullscreen) { element.msRequestFullscreen() } else if (element.webkitRequestFullscreen) { element.webkitRequestFullScreen() } }6、关闭全屏export const exitFullscreen = () => { if (document.exitFullscreen) { document.exitFullscreen() } else if (document.msExitFullscreen) { document.msExitFullscreen() } else if (document.mozCancelFullScreen) { document.mozCancelFullScreen() } else if (document.webkitExitFullscreen) { document.webkitExitFullscreen() } }7、大小写转换/** * 大小写转换 * @param str 待转换的字符串 * @param type 1-全大写 2-全小写 3-首字母大写 * @returns */ export const turnCase = (str, type) => { switch (type) { case 1: return str.toUpperCase() case 2: return str.toLowerCase() case 3: return str[0].toUpperCase() + str.substring(1).toLowerCase() default: return str } } // Example turnCase('vue', 1) // VUE turnCase('REACT', 2) // react turnCase('vue', 3) // Vue8、解析URL参数export const getSearchParams = () => { const searchPar = new URLSearchParams(window.location.search) const paramsObj = {} for (const [key, value] of searchPar.entries()) { paramsObj[key] = value } return paramsObj } // Example // 假设目前位于 https://****com/index?id=154513&age=18; getSearchParams(); // {id: "154513", age: "18"}9、判断手机是Andoird还是IOS/** * 1: ios * 2: android * 3: 其它 */ export const getOSType=() => { let u = navigator.userAgent, app = navigator.appVersion; let isAndroid = u.indexOf('Android') > -1 || u.indexOf('Linux') > -1; let isIOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/); if (isIOS) { return 1; } if (isAndroid) { return 2; } return 3; }10、数组对象根据字段去重/** * 数组对象根据某字段去重 * @param arr 要去重的数组 * @param key 根据去重的字段名 * @returns */ export const uniqueArrayObject = (arr = [], key = 'id') => { if (arr.length === 0) return let list = [] const map = {} arr.forEach((item) => { if (!map[item[key]]) { map[item[key]] = item } }) list = Object.values(map) return list } // Example const responseList = [ { id: 1, name: '树哥' }, { id: 2, name: '黄老爷' }, { id: 3, name: '张麻子' }, { id: 1, name: '黄老爷' }, { id: 2, name: '张麻子' }, { id: 3, name: '树哥' }, { id: 1, name: '树哥' }, { id: 2, name: '黄老爷' }, { id: 3, name: '张麻子' }, ] uniqueArrayObject(responseList, 'id') // [{ id: 1, name: '树哥' },{ id: 2, name: '黄老爷' },{ id: 3, name: '张麻子' }] 11、滚动到页面顶部export const scrollToTop = () => { const height = document.documentElement.scrollTop || document.body.scrollTop; if (height > 0) { window.requestAnimationFrame(scrollToTop); window.scrollTo(0, height - height / 8); } }12、滚动到指定元素位置export const smoothScroll = element =>{ document.querySelector(element).scrollIntoView({ behavior: 'smooth' }) } // Example smoothScroll('#target') // 平滑滚动到 ID 为 target 的元素13、uuidexport const uuid = () => { const temp_url = URL.createObjectURL(new Blob()) const uuid = temp_url.toString() URL.revokeObjectURL(temp_url) //释放这个url return uuid.substring(uuid.lastIndexOf('/') + 1) } // Example uuid() // d9ffece8-0a9d-4ca2-bbab-0af7bc0f71df14、金额格式化/** * 金额格式化 * @param number 要格式化的数字 * @param decimals 保留几位小数 * @param dec_point 小数点符号 * @param thousands_sep 千分位符号 * @returns */ export const moneyFormat = (number, decimals, dec_point, thousands_sep) => { number = (number + '').replace(/[^0-9+-Ee.]/g, '') const n = !isFinite(+number) ? 0 : +number const prec = !isFinite(+decimals) ? 2 : Math.abs(decimals) const sep = typeof thousands_sep === 'undefined' ? ',' : thousands_sep const dec = typeof dec_point === 'undefined' ? '.' : dec_point let s = '' const toFixedFix = function(n, prec) { const k = Math.pow(10, prec) return '' + Math.ceil(n * k) / k } s = (prec ? toFixedFix(n, prec) : '' + Math.round(n)).split('.') const re = /(-?\d+)(\d{3})/ while (re.test(s[0])) { s[0] = s[0].replace(re, '$1' + sep + '$2') } if ((s[1] || '').length < prec) { s[1] = s[1] || '' s[1] += new Array(prec - s[1].length + 1).join('0') } return s.join(dec) } // Example moneyFormat(10000000) // 10,000,000.00 moneyFormat(10000000, 3, '.', '-') // 10-000-000.00015、存储操作class MyCache { constructor(isLocal = true) { this.storage = isLocal ? localStorage : sessionStorage } setItem(key, value) { if (typeof (value) === 'object') value = JSON.stringify(value) this.storage.setItem(key, value) } getItem(key) { try { return JSON.parse(this.storage.getItem(key)) } catch (err) { return this.storage.getItem(key) } } removeItem(key) { this.storage.removeItem(key) } clear() { this.storage.clear() } key(index) { return this.storage.key(index) } length() { return this.storage.length } } const localCache = new MyCache() const sessionCache = new MyCache(false) export { localCache, sessionCache } // Example localCache.getItem('user') sessionCache.setItem('name','树哥') sessionCache.getItem('token') localCache.clear() 16、下载文件/** * 下载文件 * @param api API接口路径 * @param params 请求参数 * @param fileName 文件名 * @param type HTTP请求方式,默认为get */ const downloadFile = (api, params, fileName, type = 'get') => { axios({ method: type, url: api, responseType: 'blob', params: params }).then((res) => { let str = res.headers['content-disposition'] if (!res || !str) { return } let suffix = '' // 截取文件名和文件类型 if (str.lastIndexOf('.')) { fileName ? '' : fileName = decodeURI(str.substring(str.indexOf('=') + 1, str.lastIndexOf('.'))) suffix = str.substring(str.lastIndexOf('.'), str.length) } // 如果支持微软的文件下载方式(ie10+浏览器) if (window.navigator.msSaveBlob) { try { const blobObject = new Blob([res.data]); window.navigator.msSaveBlob(blobObject, fileName + suffix); } catch (e) { console.log(e); } } else { // 其他浏览器 let url = window.URL.createObjectURL(res.data) let link = document.createElement('a') link.style.display = 'none' link.href = url link.setAttribute('download', fileName + suffix) document.body.appendChild(link) link.click() document.body.removeChild(link) window.URL.revokeObjectURL(link.href); } }).catch((err) => { console.log(err.message); }) } // Example downloadFile('/api/resources/download', {id}, '文件名')17、时间操作关于时间操作,没必要自己再写一大串代码了,强烈推荐使用 [Day.js](https://dayjs.gitee.io/zh-CN/) > Day.js 是一个轻量的处理时间和日期的 JavaScript 库,和 Moment.js 的 API 设计保持完全一样。 > 如果您曾经用过 Moment.js, 那么您已经知道如何使用 Day.js 。18、深拷贝此方法存在一定局限性:一些特殊情况没有处理: 例如Buffer对象、Promise、Set、Map。如果确实想要完备的深拷贝,推荐使用 lodash 中的 cloneDeep 方法。export const clone = parent => { // 判断类型 const isType = (obj, type) => { if (typeof obj !== "object") return false const typeString = Object.prototype.toString.call(obj) let flag; switch (type) { case "Array": flag = typeString === "[object Array]" break; case "Date": flag = typeString === "[object Date]" break; case "RegExp": flag = typeString === "[object RegExp]" break; default: flag = false } return flag; }; // 处理正则 const getRegExp = re => { var flags = "" if (re.global) flags += "g" if (re.ignoreCase) flags += "i" if (re.multiline) flags += "m" return flags; }; // 维护两个储存循环引用的数组 const parents = [] const children = [] const _clone = parent => { if (parent === null) return null if (typeof parent !== "object") return parent let child, proto if (isType(parent, "Array")) { // 对数组做特殊处理 child = [] } else if (isType(parent, "RegExp")) { // 对正则对象做特殊处理 child = new RegExp(parent.source, getRegExp(parent)) if (parent.lastIndex) child.lastIndex = parent.lastIndex } else if (isType(parent, "Date")) { // 对Date对象做特殊处理 child = new Date(parent.getTime()) } else { // 处理对象原型 proto = Object.getPrototypeOf(parent) // 利用Object.create切断原型链 child = Object.create(proto) } // 处理循环引用 const index = parents.indexOf(parent) if (index != -1) { // 如果父数组存在本对象,说明之前已经被引用过,直接返回此对象 return children[index] } parents.push(parent) children.push(child) for (let i in parent) { // 递归 child[i] = _clone(parent[i]) } return child; } return _clone(parent) }19、模糊搜索/** * 数组对象模糊搜索 * @param list 原数组对象 * @param keyWord 查询的关键词 * @param attribute 数组需要检索的对象属性,默认问name * @returns */ export const fuzzyQuery = (list, keyWord, attribute = 'name') => { const reg = new RegExp(keyWord) const arr = [] for (let i = 0; i < list.length; i++) { if (reg.test(list[i][attribute])) { arr.push(list[i]) } } return arr } // Example const list = [ { id: 1, name: '树哥' }, { id: 2, name: '黄老爷' }, { id: 3, name: '张麻子' }, { id: 4, name: '汤师爷' }, { id: 5, name: '胡万' }, { id: 6, name: '花姐' }, { id: 7, name: '小梅' } ] fuzzyQuery(list, '树', 'name') // [{id: 1, name: '树哥'}]20、遍历树节点export const foreachTree = (data, callback, childrenName = 'children') => { for (let i = 0; i < data.length; i++) { callback(data[i]) if (data[i][childrenName] && data[i][childrenName].length > 0) { foreachTree(data[i][childrenName], callback, childrenName) } } } // Example const treeData = [{ id: 1, label: '一级 1', children: [{ id: 4, label: '二级 1-1', children: [{ id: 9, label: '三级 1-1-1' }, { id: 10, label: '三级 1-1-2' }] }] }, { id: 2, label: '一级 2', children: [{ id: 5, label: '二级 2-1' }, { id: 6, label: '二级 2-2' }] }, { id: 3, label: '一级 3', children: [{ id: 7, label: '二级 3-1' }, { id: 8, label: '二级 3-2' }] }], // 假设我们要从树状结构数据中查找 id 为 9 的节点 let result foreachTree(data, (item) => { if (item.id === 9) { result = item } }) console.log('result', result) // {id: 9,label: "三级 1-1-1"} 关于本文作者:呛再首https://juejin.cn/post/7132714583399071758
2022年10月14日
23 阅读
0 评论
0 点赞
2022-09-20
记一次KBS项目实施经验分享
背景介绍KBS 是我入职并行后接触的第一个项目,该项目之前有同事基于 OpenKB(采用 Node + Express + Mongodb技术栈的开源知识库应用,github地址:https://github.com/mrvautin/openKB) 搭建过一个简单的知识库,但是初步实施后的效果不是很符合预期,便安排我着手知识库相关的后续工作。以前在 TRS 的时候有接触过三一项目的知识库项目,该项目基于 TRS 自主研发的企业知识门户产品 —— TRS EKP V6.5 进行实施,是整合了公司的EKP、全文检索系统、数据网关系统等产品综合实施的一套企业内部知识分享的平台,是一个将公司内部各领域专家们的经验进行梳理沉淀的知识管理工具。经过前期的了解,我们将要做的知识库跟之前实施的三一知识库项目有非常大的出入,主要是用于一些产品文档的发布,类似阿里云、腾讯云的产品文档生成工具。最开始有考虑采用纯前端的方式实现,如当前较主流的VuePress实现,这样可以很方便的实现markdown文档编写 + algolia全文检索,并能基于git很方便达到文档版本控制的目的,但在深入沟通后,发现该方式不能满足其他的需求,如支持审核发布、支持用户认证后才可浏览。所以只能考虑其他的解决方案了。经过沟通后,决定先找个开源的 CMS,做个基础效果,再论证可行性技术调研及产品选型结合以往工作经验,经过沟通,最终考虑采用CMS的方式来实现我们的知识库项目。可考虑开源 CMS 或低成本的采购商业 CMS。以下是一些当时(2021-07-05)调研的一些开源 CMS 情况:经过沟通,最终选择了基于 jspxCMS 来进行项目可行性验证实施工作,主要有如下原因:1、jspxCMS 采用 SpringBoot + FreeMarker 跟我们团队技术栈较接近2、编辑器支持Ueditor 和 Markdown 两种编辑模式,适合给运营人员进行富文本排版及程序员轻量内容排版3、可商用,但需在页面底部保留jspxCMS版权声明,购买商业版后可去除,商业版价格也就几千块钱,且商业版支持站群模式,功能试用满足运营要求的话,可购买商业版License。项目实施过程1、搭建示例效果项目初期在本地部署了 jspxCMS 应用,准备基于该CMS实现基本的内容管理及网站发布工作。有阿里云文档做参考,主要是仿站,经过两天的实施工作,在没有UI参与的情况下实现了站点的最初的界面效果:知识库首页产品首页内容详情页搜索结果页按照目前的规划,KBS 系统可以通过栏目的方式很方便的做成公司整个产品体系的知识库门户。2、系统功能改造经过需求梳理,整理了如下待办事项:1、知识库站点模板{x} 知识库首页模板 zzl{x} 产品页模板 zzl{x} 内容页模板 zzl{x} 实现检索功能 zzl{x} 用户登录、认证对接 zzl{x} 北龙超云版知识库模板(优先级低) zzl2、后台改造{x} 改造成并行蓝风格 zzl{x} 改造左侧菜单导航样式 zzl{x} 改造顶部显示逻辑 zzl{x} 将列表中的操作列调整到表格最后一列 zzl{x} 去掉左侧菜单中商业版相关菜单入口 zzl{x} 去掉系统业务界面中商业版相关的功能入口 zzl{x} 将启动方式改成 spring-boot 模式,即 java -jar [xxx].war ,不用放到 tomcat 里 hx{x} 调研是否能支持多例 hx{x} 用户认证对接 hx{x} 支持多环境配置,即有生产,stage 和本地。 使用配置中心 hx{x} 支持 JDK 17 hx{x} 购买商业版后整合商业版相关功能模块 zzl{x} 应用资源文件分离改造。将上传文件目录、模板目录、索引文件目录从war包中分离 zzl{x} 后台界面支持并行蓝风格和北龙红风格 zzl3、技术实现细节完成该项目后主要有如下可圈可点的部分技术细节用于分享1、知识库前台模板制作网站模板的工作其实主要就是在html静态文件中插入 CMS 的标签,并结合CMS的静态资源引入机制改写页面相关的静态资源文件。一般会提取一些公共底部、公共底部模板之类的。CMS 实施过程中常用的标签其实并不多,主要也就是获取栏目列表、内容列表、栏目信息、内容信息这些最基础的。经过实践,发现jspxCMS的标签还是非常灵活的,支持模板嵌套、标签嵌套等jspxCMS 使用的freemarker实现静态化,自带了非常灵活的判断、循环等表达式。经过整理,部分标签使用记录如下:获取栏目列表及栏目信息以下是本项目中获取知识库左侧栏目结构树的模板示例<ul class="menu-list-container"> [@NodeList parentId=node.id;list] [#list list as pnode] <li class="level1" data-node-id="${pnode.id}"> <a href="javascript:void(0);"> <i class="icon-triangle kbsIconfont kbs-triangle"></i> <span class="menu-item-text">${pnode.name}</span> </a> <ul> [@NodeList parentId=pnode.id;subNodes] [#list subNodes as subNode] <li class="level2"> <a href="javascript:void(0);"> <i class="icon-triangle kbsIconfont kbs-triangle"></i> <span class="menu-item-text">${subNode.name}</span> </a> <ul> [@InfoList nodeId=subNode.id;subNodeDocs] [#list subNodeDocs as info] <li class="level3"> <a href="${info.url}" target="_self"> <span class="menu-item-text">${info.title}</span> </a> </li> [/#list] [/@InfoList] </ul> </li> [/#list] [/@NodeList] [@InfoList nodeId=pnode.id;infos] [#list infos as info] <li class="level2"> <a href="${info.url}" target="_self"> <span class="menu-item-text">${info.title}</span> </a> </li> [/#list] [/@InfoList] </ul> </li> [/#list] [/@NodeList] [@InfoList nodeId=node.id;infos] [#list infos as info] <li class="level1"> <a href="${info.url}" target="_self"> <span class="menu-item-text">${info.title}</span> </a> </li> [/#list] [/@InfoList] </ul>获取内容列表及内容信息 <div class="box-product-docs"> <div class="box"> <h3 class="box-title">最新发布</h3> <div class="box-content row"> [@InfoList nodeId=node.id limit='6' isIncludeChildren='true';list] [#list list as info] <div class="col-md-4 col-sm-6"> <div class="list-body"> <a href="${info.url}"> <p class="list-title text-left">${info.title}</p> <p class="list-content">${info.description}</p> <span class="list-more">详情+</span> <span class="list-date">更新时间:${info.publishDate?string('yyyy-MM-dd HH:mm')}</span> </a> </div> </div> [/#list] [/@InfoList] </div> </div> <div class="box"> <h3 class="box-title">热门知识</h3> <div class="box-content row"> [@InfoList nodeId=node.id sort='views desc' limit='6' isIncludeChildren='true';list] [#list list as info] <div class="col-md-4 col-sm-6"> <div class="list-body"> <a href="${info.url}"> <p class="list-title text-left">${info.title}</p> <p class="list-content">${info.description}</p> <span class="list-more">详情+</span> <span class="list-date">浏览次数:${info.views}</span> </a> </div> </div> [/#list] [/@InfoList] </div> </div> </div> 模板嵌套[#include "include_header.html"/]当前位置<div class="body-position"> <i class=" kbsIconfont kbs-home"></i> [#list node.hierarchy as n]<a href="${n.url}">${n.name}</a>[#if n_has_next] > [/#if][/#list] </div>内容分页[@InfoPage nodeId=node.id pageSize='15';pagedList] <ul class="list-unstyled mt10"> [#list pagedList.content as info] <li style="padding:15px 0;border-bottom:1px dotted #ccc;"> [#if info.withImage] <div class="left" style="padding:3px 10px 3px 0;width:22%;"> <a href="${info.url}" target="_blank"><img src="${info.smallImageUrl}" width="138" height="92"/></a> </div> [/#if] <div class="left" style="[#if info.withImage]width:75%;[/#if]"> <div>[@A bean=info class='ff-yh fs18 a c-000' target="_blank"/]</div> <div class="" style="line-height:1.8;padding:2px 0;color:#818181;">${substring(info.description,100,'...')}</div> <div class="" style="padding:2px;color:#a1a1a1;">${info.publishDate?string('yyyy-MM-dd HH:mm:ss')}</div> </div> <div class="clear"></div> </li> [/#list] </ul> <div class="mt20"> [#include "inc_page.html"/] </div> [/@InfoPage] 分页嵌套模板(inc_page.html)[#escape x as (x)!?html] <div class="pager"> [#if page>1]<a href="${paging(1)}" class="page"><i class="kbsIconfont kbs-first"></i></a>[/#if] [#if page>1] <a href="${paging(page-1)}" class="page"><i class="kbsIconfont kbs-prev"></i></a> [/#if] [#assign start=page-4/][#if start<1][#assign start=1/][/#if] [#assign end=start+8/][#if end>pagedList.totalPages][#assign end=pagedList.totalPages/][/#if][#if end<1][#assign end=1/][/#if] [#if end-start<8][#assign start=end-8/][/#if][#if start<1][#assign start=1/][/#if] [#list start..end as p] <a[#if page!=p] href="${paging(p)}"[/#if] class="[#if page!=p]page[#else]page-curr[/#if]">${p}</a> [/#list] [#if page < pagedList.totalPages] <a href="${paging(page+1)}" class="page"><i class="kbsIconfont kbs-next"></i></a> <a href="${paging(pagedList.totalPages)}" class="page" ><i class="kbsIconfont kbs-last"></i></a> [/#if] </div> [/#escape] 超级方便的app自由模版app自由模板,主要是可以传参给模板获取一些指定的内容片段,这就极大的扩展了模板的功能,目前我们用得最多的场景是不同系统间通过app模板获取KBS的内容列表及内容详情等json数据,然后在异构系统中进行个性化展现。app自由模板的官网介绍如下(https://www.ujcms.com/documentation/269.html):整理了部分自由模板内容以作备忘:1、内容列表页(app_newsPage.html)[#-- 栏目内容分页列表数据,用于列表新品快报、公告更多页面分页显示数据 --]{ [@InfoPage node=Param.nodeCode pageSize=Param.pageSize page=Param.pageNum;pagedList] "total":${pagedList.totalElements}, "size":${pagedList.size}, "pages":${pagedList.totalPages}, "current":${pagedList.number + 1}, "records": [ [#list pagedList.content as info] { "id":"${info.id}", "title":"${info.title?js_string}", "publishDate":"${info.publishDate?string('yyyy-MM-dd HH:mm:ss')}", "url":"${info.url?js_string}" }[#if info_has_next],[/#if] [/#list] ] [/@InfoPage] } 2、内容详情页(app_newsDetail.html)[#-- 内容详情接口,用于点击查看内容详情 --][@Info id=Param.articleId;bean] { "id":"${bean.id}", "url":"${bean.url?js_string}", "title":"${bean.title?js_string}", "publishDate":"${bean.publishDate?string('yyyy-MM-dd HH:mm')}", "content":"${bean.text?replace("/uploads/1/","${site.domain}/uploads/1/")?js_string}", "editorType":"${bean.customs.text_editor_type}" } [/@Info] 2、应用资源文件分离改造jspxCMS 默认的部署方式是将war包部署到tomcat下,相关的应用资源文件(模板文件、后台上传的图片及音视频文件、静态化文件等)也都在应用根目录下,功能强大的jspxCMS提供了发布点功能,将上传的资源文件、静态化的html文件生成到指定的目录下,但是该方式功能有限,不能完全满足我们的需求,如索引文件目录、站点模板文件等均不可配置。我们的实际需求是需要将应用采用docker部署并使用java -jar 的方式进行启动,那样就需要将原有跟应用整合在一起的资源文件分离出来,独立部署的其他存储目录中。充分理解该需求及使用场景后,特制定了本改造方案。改造过程如下:1、application.yml 增加分离文件相关根目录配置信息经过前期梳理,考虑到应用实际情况,将分离文件拆分为两个目录:kbs.appDataPath:应用数据(临时文件/日志/缓存/索引等)的存放路径kbs.resourceRootPath:需本地持久化保存应用资源数据(模板数据/上传文件/静态化文件等)的存放路径目录支持%{Parent}、%{Self}这样的相对路径进行占位,默认为应用当前目录下的 appdata 和 KBSData 目录。亦可在启动应用时增加启动参数的方式修改路径,如--kbs.resourceRootPath=c:/workspace/KBSData/kbs: appDataPath: '%{Parent}/appdata/' resourceRootPath: '%{Parent}/KBSData/'2、启动应用时对目录进行初始化阅读源代码后,发现应用在启动时,有调用Constants.loadEnvironment()方法,我们可以利用该方法作为入口,调用我们的目录初始化方法 KbsConfig.init(),在应用启动时初始化相关目录,保障相关资源目录是绝对存在的。KbsConfig 相关核心代码如下:/** * 知识库初始化配置 * @author 朱治龙 * @Date 2021-08-27 10:18:32 */ @Component public class KbsConfig { private static String appDataPathInConfig; private static String resourceRootPathInConfig; /** * 应用数据目录 */ protected static String appDataPath = null; /** * 资源数据目录 */ protected static String resourceDataPath = null; public static void init(Environment env) { appDataPathInConfig = env.getProperty("kbs.appDataPath"); resourceRootPathInConfig = env.getProperty("kbs.resourceRootPath"); initPaths(); } /** * 初始化相关文件路径 */ private static void initPaths() { // 初始化应用数据目录 String applicationRealPath = KbsUtil.getApplicationRealPath(); String tempAppDataPath = applicationRealPath; if (ObjectUtil.isNotEmpty(appDataPathInConfig)) { tempAppDataPath = appDataPathInConfig; tempAppDataPath = KbsUtil.replacePathHolder(tempAppDataPath); } else { tempAppDataPath += "appdata/"; } appDataPath = tempAppDataPath; AppDataPath.initialAppDataPath(); // 初始化资源目录 String tempResourceDataPath = applicationRealPath; if (ObjectUtil.isNotEmpty(resourceRootPathInConfig)) { tempResourceDataPath = resourceRootPathInConfig; tempResourceDataPath = KbsUtil.replacePathHolder(tempResourceDataPath); } else { tempResourceDataPath += "KBSData"; } resourceDataPath = tempResourceDataPath; ResourceDataPath.initialResourcePath(); } /** * 获取应用数据目录 * @return */ public static String getAppDataPath() { if(ObjectUtil.isEmpty(appDataPath)) { initPaths(); } return appDataPath; } /** * 获取应用数据目录 * @return */ public static String getResourceRootPath() { if(ObjectUtil.isEmpty(resourceDataPath)) { initPaths(); } return resourceDataPath; } }3、改造模板、索引文件、上传资源资源文件的代码逻辑相关逻辑主要涉及 jspxCMS 的核心代码,均在阅读相关源码后进行改造,此处就不做代码罗列了。4、系统资源目录中的静态资源文件提供对外访问服务/** * 系统资源目录中的静态资源文件提供对外访问服务 * @author 朱治龙 * @Date 2021-08-27 14:48:36 */ @Configuration public class KbsWebMvcConfigurer implements WebMvcConfigurer { /** * 资源目录相关文件提供后台访问 */ @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/uploads/**").addResourceLocations("file:" + ResourceDataPath.getUploadsPath() + "/"); registry.addResourceHandler("/template/**").addResourceLocations("file:" + ResourceDataPath.getTemplatePath() + "/"); } /** * 默认首页跳转到index.html */ @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/").setViewName("redirect:/index.html"); } }3、后台界面支持并行蓝风格和北龙红风格在之前对 jspxCMS 改造为并行蓝风格时,有将并行蓝风格相关的样式文件提取到paratera-kbs.css中,项目在升级改造过程中后端的同事有整合Spring Cloud 及配置中心组件spring cloud config,可在配置中心中设置当前主题信息,然后应用拿到主题信息后调用不同的样式文件,即可达到同一套后台代码展现不同风格的目的。具体实现过程如下:1、配置中心增加 kbs.theme-mode 配置项值为 blsc 或 paratera,若不配置则默认为 paratera2、全局注入主题风格变量,名称为themeMode在阅读源码后,修改后台拦截器 BackInterceptor 相关的代码逻辑,全局注入themeMode变量,便于在后台jsp文件中拿到该值进行主题相关业务逻辑判断3、修改后台jsp视图文件主要修改代码为:实施成果后台效果1、并行知识库2、北龙知识库前台效果并行知识库1、站点首页2、产品页3、内容详情页4、检索结果页北龙知识库1、站点首页2、产品页3、内容详情页4、检索结果页
2022年09月20日
83 阅读
0 评论
0 点赞
2022-09-15
思源笔记docker私有化部署及使用体验分享
说明考虑到网络问题、数据隐私和安全性,很多人都无法放心将所有笔记保存在 Notion、Wolai 这类纯在线服务上,一旦服务变卦 (比如收费、倒闭等) 都会很被动。所以,如果有一款既拥有各种先进特性,还能自建服务器的开源笔记软件,那就非常完美了!而它就是思源笔记以下是思源笔记官网的介绍,着实让人心动:部署官网提供 Docker 方式,让私有化部署便捷了不少。只需要两条命令即可:# 拉取镜像 docker pull b3log/siyuan # docker 运行 docker run --name siyuan -it -d --restart=always -v /data/siyuan/workspace:/siyuan/workspace -p 6806:6806 -u $(id -u):$(id -g) b3log/siyuan --workspace=/siyuan/workspace或使用 docker-compose 方式部署:services: siyuannote: image: b3log/siyuan container_name: siyuannote privileged: true restart: always # 可修改下方的xxxxxx为自己的访问授权码 command: ['--workspace=/siyuan/workspace/', '--accessAuthCode=xxxxxx'] user: '1000:1000' ports: - 6806:6806 volumes: - ./data:/siyuan/workspace environment: # A list of time zone identifiers can be found at https://en.wikipedia.org/wiki/List_of_tz_database_time_zones - TZ=Asia/Shanghai注意: 镜像中是使用默认创建的普通用户 siyuan(uid 1000/gid 1000)来启动内核进程的,所以在宿主机创建工作空间文件夹时请注意设置该文件夹所属用户组:chown -R 1000:1000 ./data,命令行启动容器时需要带参数 -u 1000:1000。配置 nginx 对外提供服务我使用的腾讯云的服务器,将思源笔记使用docker 运行后,通常使用 nginx 代理对外提供应用服务。nginx 代理配置信息如下server { listen 80; server_name siyuannote.work.zhuzhilong.com; index index.html index.htm default.htm default.html; root /www/wwwroot/siyuannote.work.zhuzhilong.com; location / { proxy_pass http://10.0.12.15:6806; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header REMOTE-HOST $remote_addr; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "Upgrade"; add_header X-Cache $upstream_cache_status; #Set Nginx Cache add_header Cache-Control no-cache; } access_log /www/wwwlogs/siyuannote.work.zhuzhilong.com.log; error_log /www/wwwlogs/siyuannote.work.zhuzhilong.com.error.log; }配制好后,重启 nginx 服务即可使用我们nginx配置的域名(siyuannote.work.zhuzhilong.com)访问了。初体验国际化初次访问的时候,默认是英文版的,可以按Alt + P,在打开的对话框中,选择 Appearance -> Language 中选择 简体中文:使用体验Block编辑思源笔记在使用体验上,较常规markdown编辑器,一大特色就是Block编辑模式,按下 /后可快速使用内置 20+ 种类型的块元素和 10+ 种行级元素。表格排版对于一些结构化的数据,难免会使用表格进行布局,而常规的markdown 在使用表格时异常难使用,思源笔记提供右键方式操作表格,也还算灵活:粘贴 Markdown初次使用的时候,直接把 markdown 内容在编辑器中Ctrl + V 粘贴,发现直接变成代码块了(2024-06-28更新:该问题已在试用的最新版本(版本号: v3.0.17)修复)。感觉有些不习惯,这一点语雀做得最好。在快捷键中查看了下 粘贴为纯文本方式 ,可以使用 Ctrl + Shift + V。从快捷键配置界面可以看出思源笔记的快捷键还是很丰富的,而且可以自定义。粘贴剪贴板中的图片经验证可直接粘贴剪贴板中的截图图片上传到服务器中,图片上传后持久化存储在服务器思源笔记工作目录的assets目录中。没有看到可以配置图床的地方。安全配置思源笔记部署后默认是不需要密码即可访问的,我们私有化部署更多的是要控制只有我们自己能访问。可在配置界面的 关于 -> 访问授权码 中 设置授权码 (2024-06-28更新:在试用的最新版本(版本号: v3.0.17)已改为在部署时指定访问授权码,界面中不再提供配置入口):配置后将只能输入授权码解锁后才可访问:功能扩展思源笔记提供「集市」模块,可在线下载主题、模板、图标、模板、挂件,初步使用感觉还是蛮方便的。插件机制给思源笔记扩展了无限可能,还有很多功能可以在后续的使用中逐步体验。尾声Typora 开启强制收费模式后,有短暂的使用过 Mark Text,但是发现 Mark Text 在打开较大 Markdown 文件时比较卡顿。偶然在公众号中看到推荐的思源笔记,通过官网了解后,便有了试用的冲动。使用下来,感觉可以作为后续的主力 Markdown 编辑器使用。Links官网:https://b3log.org/siyuan/思源笔记Docker镜像:https://hub.docker.com/r/b3log/siyuan
2022年09月15日
2,621 阅读
0 评论
0 点赞
2022-09-06
扫码登录项目实践
背景说明随着移动端应用的普及,扫码登录在日常的登录认证过程中已经非常普及了,在提升用户便利的同时,由于减少了账号密码的输入,在一定程度上也起到了增强安全性的目的。随着控制台产品的迭代,PC端界面及功能已趋于稳定,考虑到目前移动终端更便于用户使用,经过公司内部层层审批,控制台移动端的项目顺利立项。作为充分为用户提供便利的移动端应用,扫码登录功能便自然的纳入了起基础功能。登录认证的本质扫码登录本质上也是一种登录认证方式。既然是登录认证,要做的也就两件事情!告诉系统我是谁向系统证明我是谁比如账号密码登录,账号就是告诉系统我是谁, 密码就是向系统证明我是谁;比如手机验证码登录,手机号就是告诉系统我是谁,验证码就是向系统证明我是谁;那么扫码登录是怎么做到这两件事情的呢?扫码登录过程中移动端是已登录状态,这两件事对移动端而言都是明确的,登录过程中的重点是怎么把移动端的登录状态传递给PC端。在这个过程中主要就是通过扫码的二维码了,二维码本质上就是一串具有唯一性的字符串。通过这个字符串便可以将已登录方授权未登录端进行登录了。技术方案根据项目实际情况,参与扫码登录的应用为console-ui、consent、console-biz、app:console-ui :控制台前端,需登录的应用consent :用户认证端console-biz :控制台后端服务app :移动端应用,微信端H5网页实现流程在使用超算云服务控制台应用时,由 console-ui 判断用户是否登录,未登录的情况下将自动跳转到 consent 进行登录操作,本需求将修改consent登录界面,在已有的账号登录基础上增加扫码登录机制,默认采用账号登录方式,当用户切换登录方式为扫码登录时,将调用console-biz提供的接口获取 scanId,使用 QRCode.js 生成携带scanId参数的app端网页认证URL的二维码。用户使用微信扫一扫功能,可直接打开app端的指定页面,在该页面完成用户确认授权操作,consent通过轮询二维码状态接口,当获取到用户在 app端的确认操作后,携带 scanId 参数跳转到 console-ui,在console-ui 调用接口获取用户 token 信息完成登录。整体交互流程如下所示:二维码生命周期码登录的核心是对二维码生命周期的管理,为了更好地理解扫码登录过程,将上图中二维码生命周期相关的核心流程梳理如下:二维码状态数据字典字典项字典值说明未使用1获取scanId时设置初始状态为该值已扫码(未授权)2app端扫码后更新状态为已扫码已授权(登录)3在app授权界面单击“确认授权”已使用4在app端授权界面确认授权,然后在console-ui中获取到用户token后已过期5超过5分钟redis中不存在记录则返回该值取消授权(登录失败)6在app授权界面单击“取消授权”数据存储方案scanId作为整个扫码登录过程的核心,后端需存储scanId整个生命周期的状态值及需交换的用户数据,而每个 scanId 仅有5分钟的有效期,基于 Redis 的过期策略很容易就能实现这个目的,且比关系型数据库在数据存取上有更优异的表现。故本功能模块相关的数据统一存储于 Redis。后端实现方案后端基于现有的console-biz工程添加新的接口用于前端对接,相关接口见“接口设计”章节。前端实现方案在需求中参与前端开发工作的有consent、console-ui、app三端。1、consent:负责登录界面的交互及展示,基于QRCode.js实现二维码生成,轮询获取二维码状态。2、console-ui:在获取到consent授权回调后,通过scanId调用接口获取用户token实现用户在PC端登录。3、app:扫码后直接跳转到app指定的授权页面,进入授权页面后实现。扫码操作的入口有两个:微信扫一扫、app应用首页顶部的扫一扫。使用app应用首页的扫一扫功能需要解析获取二维码URL中的scanId,然后跳转到授权页面进行授权操作。4、单击某功能时使用component组件渲染对应功能的抽屉功能进行显示接口设计后端以 Restful API 方式为前端提供接口,根据项目情况涉及的接口列表如下:获取scanId接口(PC端调用)获取scanId状态接口(PC端轮询调用)app端扫码接口app端授权接口授权成功后获取登录信息接口(PC端调用)效果截图相关功能将在控制台移动端功能上线后,所有并行用户可线上体验。一、PC端截图:1、初始状态:2、刷新时有个 gif加载动图:3、已扫码:4、已失效:5、取消授权:二、移动端截图:1、正常状态:2、已被使用:3、单击取消授权:4、单击确认授权:5、授权操作时二维码已过期:
2022年09月06日
26 阅读
0 评论
0 点赞
2022-09-01
数据库设计挑战问题
缘起聊天内容整理文字如下问题现有如下表:作业记录表T1作业ID作业日期机时记录时间Job0012022/01/0110002022/01/02Job0022022/01/0120002022/01/02作业扣费表T2作业ID作业日期机时扣费扣费时间Job0012022/01/0110001002022/01/02Job0022022/01/0120002002022/01/02在正常情况下,一切可以工作得很好,但是事实并非理想,作业调度器也会出故障,导致作业机时发生变化。例如在 2022/01/03 重新采集作业时 Job001 的机时从 1000 又变回了 500,但系统对 Job001 已完成了 1000 机时的扣费问题:如何设计数据库表在不修改已写入的历史记录的前提下,解决作业机时发生变化的问题?情境说明:机时变化是不可抗力因素,1000是错误的,后来500是正确的,但也可能最终200是正确的最终时间其实不确定,如果非要给个期限,可以限定为一年内变动都是可能发生的设计字段可以随意,但已存在的记录数据不能变化了增加新表,新字段都不受任何限制综合要求如下:前提:可以增加新表,可以在原先这两个表基础上加字段,但是不能修改或者删除这两个表已有的字段目标:以当前机时作为扣费依据,历史多扣或者少扣需要进行补扣或者补偿我的解答综上问题,我的解决思路如下:1、对最关键的作业记录表增加内容Hash字段,内容为 md5(作业ID+作业日期+机时),该字段可作为主键,由于数据在入库前用于计算Hash的内容都是已知的,作为主键字段可以最大程度的利用主键索引提升检索效率。如果这个Hash数据不存在则在下次 作业调度器 同步作业记录时自动计入 T1 表,如果已存在则直接忽略2、T2 表关联 T1 表的 DataHash字段,主要用于判断作业是否已扣费,便于后续扣费任务的执行3、增加 「异常作业退费记录表T3」,用于对之前已扣费的作业进行退费行为记录。4、若出现重复的情况(作业ID跟作业日期相同),将上一次作业(以记录时间排序)已扣费记录进行退费,并记入T3表调整后的表结构记录如下:作业记录表T1DataHash字段内容为:md5(作业ID+作业日期+机时),如:md5(Job001-2022/01/01-1000)作业ID作业日期机时记录时间DataHashJob0012022/01/0110002022/01/0215dc9093ab0febf9aff2810891ef1a64Job0022022/01/0120002022/01/02fa8f51fb2f36087a9b4c6deacf5f1dd2Job0012022/01/015002022/08/304ec0fea71753f4a7585dc4cf4dd460f2作业扣费表T2作业ID作业日期机时扣费扣费时间作业记录DataHashJob0012022/01/0110001002022/01/0215dc9093ab0febf9aff2810891ef1a64Job0022022/01/0120002002022/01/02fa8f51fb2f36087a9b4c6deacf5f1dd2Job0012022/01/01500502022/08/304ec0fea71753f4a7585dc4cf4dd460f2异常作业退费记录表T3作业ID作业日期机时退费退费时间作业记录DataHashJob0012022/01/0110001002022/08/3015dc9093ab0febf9aff2810891ef1a64其他小伙伴的解答胡大神毛老师
2022年09月01日
18 阅读
0 评论
0 点赞
2022-08-31
正则表达式-元字符
特殊单字符空白符范围量词
2022年08月31日
25 阅读
0 评论
0 点赞
2022-08-06
Keyviz – 开源按键可视化工具:实时显示键盘按键[Windows]
介绍Keyviz 是一款开源、免费的按键可视化工具,它可以实时显示用户当前按下的按键,可自定义显示按键风格、样式,非常适合录屏、演示等场合使用。其可定制性让你能制作出任意风格的按键风格:使用很简单,在设置界面的 Style 里选中一个喜欢的、独特的键盘样式以及位置,对于很多截图、录屏、直播用户,甚至可以作为个人标识使用:还可以设置显示全部按键,或者仅显示快捷键,比如 Ctrl + C,但不显示 Shift + 1、A、B、C 等按键。最重要的是宣传图做的好看,那么用户将来截图录屏也会很漂亮。链接官网:https://kutt.appinn.net/UPg2QUGithub:https://github.com/mulaRahul/keyviz
2022年08月06日
330 阅读
0 评论
0 点赞
2022-07-02
使用 Syncthing 同步文件
下载syncthingwget https://github.com/syncthing/syncthing/releases/download/v1.20.2/syncthing-linux-amd64-v1.20.2.tar.gz tar -zxvf syncthing-linux-amd64-v1.20.2.tar.gz本应用解压的路径为 /opt/syncthing-linux-amd64-v1.20.2。下面配置syncthing 自启动。配置自启动根据官方提供的说明(https://docs.syncthing.net/users/autostart.html)有两种方式.第1种为 Using Supervisord,这种还要安装 supervisor,并启动服务,稍显麻烦,这里我们直接采用系统服务的方式。1、拷贝自启动文件到服务目录cp /opt/syncthing-linux-amd64-v1.20.2/etc/linux-systemd/system/syncthing@.service /etc/systemd/system/syncthing@root.service2、修改自启动文件主要修改内容为ExecStart 的内容,修改后的内容示例如下:/opt/syncthing-linux-amd64-v1.20.2/syncthing serve --gui-address=172.17.0.11:8384 --no-restart --logflags=0 3、启用并启动服务systemctl enable syncthing@root.service systemctl start syncthing@root.service
2022年07月02日
118 阅读
0 评论
0 点赞
2022-06-15
前端导出Excel项目实践
背景介绍近期项目有个「导出作业详情」的需求,之前接触大部分导出需求均为后端获取数据生成Excel文件或将Excel文件的下载地址或文件流提供给前端进行下载。在本需求中对下载的文件有如下需求点:命名:用户名作业详单-起止日期格式:xlsx,下载内容CPU与GPU分别显示在两个工作表里技术调研在以前的项目中有=接触过前端解析 Excel 导入数据的功能,使用的是 [xlsx](https://www.npmjs.com/package/xlsx),功能超级强大,便先了解一下这个库是否能满足咱们的需求,经过稍加深入地调研,了解到这个库仅侧重在 Excel 解析,我们的需求是要能根据后端提供的数据动态生成 Excel 文件,可能这个库就不适应了。经过经一步调研,发现一款采用MIT开源授权, Star 数近 10k 的开源库:exceljs,看描述为:读取,操作并写入电子表格数据和样式到 XLSX 和 JSON 文件。这不就正是我所需要的嘛,接下来的问题就是验证工作了,根据我们的需求,主要需要验证的有如下事项:1、是否可支持生成多工作表的Excel文件2、是否支持自定义单元格样式:如背景色、边框、对齐方式、格式化3、测试Excel文件生成效率经过对相关API的深入了解及验证,发现完全能满足我们的需求。其中生成效率,一次性生成40000多条数据,不到15秒即可完成Excel生成工作,也能满足我们的实际要求:技术点梳理详细见代码及相关注释。仅用于说明相关功能点,非完整代码。// 引入 exceljs 依赖 import ExcelJS from 'exceljs' // 创建工作簿 const workbook = new ExcelJS.Workbook() // 在工作簿中添加两个工作表 const cpuSheet = workbook.addWorksheet('CPU作业详单') const gpuSheet = workbook.addWorksheet('GPU作业详单') // 添加表头 const cpuHeaderRow = cpuSheet.addRow(['作业ID', '作业名称', '超算中心', '超算账号', '队列', '运行时长', '核数', '消费核时', '消费金额', '提交时间', '开始时间', '结束时间', '提交账号', '付费账号']) // 设置表头样式 // 行高 cpuHeaderRow.height = 26.5 // 字体 cpuHeaderRow.font = { bold: true } // 设置表头单元格样式 cpuHeaderRow.eachCell((cell, rowNumber) => { // 设置边框 cell.border = { top: { style: 'thin' }, left: { style: 'thin' }, bottom: { style: 'thin' }, right: { style: 'thin' } } // 填充背景 cell.fill = { type: 'pattern', pattern: 'darkTrellis', fgColor: { argb: 'F1F1F1FF' }, bgColor: { argb: 'F1F1F1FF' } } // 对齐方式 cell.alignment = { vertical: 'middle', horizontal: cpuLeftAlignCols.includes(rowNumber) ? 'left' : 'center', wrapText: true } }) // 根据内容适当调整部分列宽度 cpuSheet.getColumn(2).width = 30 // 作业名称 cpuSheet.getColumn(10).width = 20 // 提交时间 cpuSheet.getColumn(11).width = 20 // 开始时间 cpuSheet.getColumn(12).width = 20 // 结束时间 cpuSheet.getColumn(13).width = 15 // 提交账号 cpuSheet.getColumn(14).width = 15 // 付费账号 // 生成相关数据行 for (let i = 0; i < cpuJobList.length; i++) { const job = cpuJobList[i] const rowData = [job.jobId, job.jobName, job.cluster, job.user, job.partition, ...] const dataRow = cpuSheet.addRow(rowData) // 设置数据行样式 dataRow.height = 26.5 dataRow.eachCell((cell, rowNumber) => { // 设置单元格边框 cell.border = { top: { style: 'thin' }, left: { style: 'thin' }, bottom: { style: 'thin' }, right: { style: 'thin' } } // 设置对齐方式 cell.alignment = { vertical: 'middle', horizontal: cpuLeftAlignCols.includes(rowNumber) ? 'left' : 'center', wrapText: true } // 金额格式化 if (rowNumber === 10) { cell.numFmt = '"¥"#,##.##' } }) } // 保存Excel文件 const buf = await workbook.xlsx.writeBuffer() const fileName = `${userForFileName}作业详单_${dayjs(startDate).format('YYYYMMDD')}-${dayjs(endDate).format('YYYYMMDD')}.xlsx` // 基于 file-saver 实现保存文件到本地 saveAs(new Blob([buf]), fileName)最终整合到应用中的效果扩展打印相关// 设置页面方向 workSheet.pageSetup.orientation = 'portrait' // portrait || landscape // 设置页边距 workSheet.pageSetup.margins = { left: 0.3, right: 0.3, top: 0.2, bottom: 0.2, header: 0.2, footer: 0.2 } workSheet.pageSetup.horizontalCentered = true workSheet.pageSetup.verticalCentered = false // 每页均显示的行 workSheet.pageSetup.printTitlesRow = '1:3' // 指定打印哪些列 workSheet.pageSetup.printTitlesColumn = 'A:G'解析 Excelconst ExcelJS = require('exceljs') const excelfile = './test.xlsx' var workbook = new ExcelJS.Workbook() workbook.xlsx.readFile(excelfile).then(function() { // 获取第一个worksheet var worksheet = workbook.getWorksheet(1) // 编辑worksheet worksheet.eachRow(function(row, rowNumber) { var rowSize = row.cellCount var numValues = row.actualCellCount console.log('单元格数量/实际数量:' + rowSize+'/' + numValues) row.eachCell(function(cell, colNumber) { // cell.type单元格类型:6-公式 ;2-数值;3-字符串 let cellValue = cell.value if(cell.type === 6) { cellValue = cell.result } console.log('cell',rowNumber, colNumber, cellValue, cell.numFmt) }) }) })数值格式化问题前期为满足跟前端列表展现业务逻辑保持一致,金额及数值格式化均使用 Intl.NumberFormat进行格式化,但是该类格式化后的数据是字符串,不便于导出后对数据进行排序及数值比较等操作。由于官方文档的单元格格式化说明信息较简单,Excel里对数值格式化的方式较丰富,如下图:为完整的复原格式化后的内容,可以先在Excel文件中对相关单元格设置好格式后,使用上面的 解析 Excel 章节的代码解析获取单元格的 numFmt数据。下面列举本工程中用到的数值格式化 numFmt 值:数值格式化:#,##0.00_ 货币格式化:"¥"#,##0.00;"¥"-#,##0.00
2022年06月15日
113 阅读
0 评论
0 点赞
2022-06-15
前端库推荐:JSZip
jszip是一个用于创建、读取和编辑.zip文件的JavaScript库,且API的使用也很简单链接官网:https://stuk.github.io/jszip/Github:https://hub.fastgit.xyz/Stuk/jszip示例var zip = new JSZip(); zip.file("Hello.txt", "Hello World\n"); var img = zip.folder("images"); img.file("smile.gif", imgData, {base64: true}); zip.generateAsync({type:"blob"}) .then(function(content) { // see FileSaver.js saveAs(content, "example.zip"); });兼容性OperaFirefoxSafariChromeInternet ExplorerNode.jsYesYesYesYesYesYesTested with the latest versionTested with 3.0 / 3.6 / latest versionTested with the latest versionTested with the latest versionTested with IE 6 / 7 / 8 / 9 / 10Tested with node.js 0.10 / latest version
2022年06月15日
12 阅读
0 评论
0 点赞
2022-06-06
vue-cli工程打包生成gzip相关压缩文件
一、添加依赖yarn add vue-cli-plugin-compression -D二、更新vue.config.js文件添加zlib引用const zlib = require('zlib')module.exports 中添加如下配置信息:pluginOptions: { compression:{ brotli: { filename: '[file].br[query]', algorithm: 'brotliCompress', include: /\.(js|css|html|svg|json)(\?.*)?$/i, compressionOptions: { params: { [zlib.constants.BROTLI_PARAM_QUALITY]: 11, }, }, minRatio: 0.8, }, gzip: { filename: '[file].gz[query]', algorithm: 'gzip', include: /\.(js|css|html|svg|json)(\?.*)?$/i, minRatio: 0.8, } } }效果打包后生成相应的.br和.gz结尾的文件
2022年06月06日
14 阅读
0 评论
0 点赞
2022-05-28
微软拼音输入法快速当前时间
微软拼音输入法默认可以使用 rq 快速输入 2022年5月28日 和 使用 sj 快速输入 16点20分,可是在写代码过程中我们常使用 yyyy-MM-dd HH:mm:ss 格式的当前日期数据。经过简单搜索后,可以通过添加自定义词库的方式实现,具体操作步骤见下图:最核心的是短语中输入如下数据:%yyyy%-%MM%-%dd% %HH%:%mm%:%ss%
2022年05月28日
33 阅读
0 评论
0 点赞
2022-05-19
快速下载github文件
将github.com修改为 hub.fastgit.xyz下载文件时可以在下载链接前加如下前缀:https://github.91chi.fun//如:https://github.91chi.fun//https://github.com/git-for-windows/git/releases/download/v2.36.1.windows.1/Git-2.36.1-64-bit.exe另一种方案,使用FastGithub:https://github.com/dotnetcore/FastGithub
2022年05月19日
12 阅读
0 评论
0 点赞
2022-05-13
H5与小程序直播入门-学习笔记
链接:https://www.bilibili.com/video/BV1b7411T7t1直播原理HLS协议动态列表:基本上未使用静态列表全量列表:一般用于直播RTMP协议RTMP是Real Time Messaging Protocol (实时消息传输协议)的首字母缩写。该协议基于 TCP,是一个协议族,包括RTMP基本协议及RTMPT/RTMPS/RTMPE等多种变种。RTMP是一种设计用来进行实时数据通信的网络协议,主要用来在Flash、AIR平台和支持RTMP协议的流媒体/交互服务器之间进行音视频和数据通信。flvHTTP-FLV协议1、可以在一定程度上避免防火墙的干扰(例如,有的机房只允许80端口通过).2、可以很好的兼容HTTP 302跳转,做到灵活调度.3、可以使用HTTPS做加密通道.4、很好的支持移动端(Android, IOS).video详解controls:是否显示控制条controlslist:控制条显示内容autoplay:自动播放poster:封面图loop:循环播放preload:预加载,在不同设备上行为有些不一致volume:音量,通过 js 控制muted:是否静音<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>video 详解</title> </head> <body> <video src="./media/blsc.mp4" id="video" autoplay muted loop preload="auto" controls controlslist="nodownload nofullscreen" width="800" height="600" poster="./images/poster.jpg"></video> <script> var videoEl = document.getElementById('video') // 调整音量 videoEl.volumn = 0.5 // 设置进度,单位为秒 videoEl.currentTime = 60 // 切换视频地址 videoEl.src = './media/paratera.mp4' </script> <video id="sourceDemo" controls poster="./images/poster.jpg"> <source src="./media/blsc.mp4" type="video/mp4"> <source src="./media/paratera.mp4" type="video/mp4"><!-- 第一个错误时使用第二个地址进行播放 --> </video> <script> var videoEl = document.getElementById('sourceDemo') console.log('videoEl.currentSrc', videoEl.currentSrc) </script> </body> </html>video 事件<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>video 事件</title> </head> <body> <video src="./media/blsc.mp4" id="video" autoplay muted preload="auto" controls controlslist="nodownload nofullscreen" width="800" height="600" poster="./images/poster.jpg"></video> <script> var videoEl = document.getElementById('video') // 开始加载视频 video.addEventListener('loadstart', e => { console.log('loadStart', e) }) // 时长变化,不一定发生一次 video.addEventListener('durationchange', e => { console.log('durationchange', e, video.duration) }) // 视频的元数据下载完成 video.addEventListener('loadedmetadata', e => { console.log('loadedmetadata', e) }) // 数据加载完成 video.addEventListener('loadeddata', e => { console.log('loadeddata', e) }) // 播放进度 video.addEventListener('progress', e => { console.log('progress', e) }) // 有帧可以播放 video.addEventListener('canplay', e => { console.log('canplay', e) }) // 可以流畅播放 video.addEventListener('canplaythrough', e => { console.log('canplaythrough', e) }) // 监听播放 video.addEventListener('play', e => { console.log('play', e) }) // 监听暂停 video.addEventListener('pause', e => { console.log('pause', e) }) // seeking,点进度条加载,开始查找 video.addEventListener('seeking', e => { console.log('seeking', e) }) // seeked,下载数据完成后,标记seek结束 video.addEventListener('seeked', e => { console.log('seeked', e) }) // waiting,解码过程中 video.addEventListener('waiting', e => { console.log('waiting', e) }) // playing,暂停到播放状态之间会触发playing video.addEventListener('playing', e => { console.log('playing', e) }) // timeupdate,一般用于更新播放进度条 video.addEventListener('timeupdate', e => { console.log('timeupdate', e) }) // ended:播放结束 video.addEventListener('ended', e => { console.log('ended', e) }) // error:异常捕获,浏览器自带重试机制 video.addEventListener('error', e => { console.log('error', e) }) </script> </body> </html>
2022年05月13日
40 阅读
0 评论
0 点赞
2022-05-13
Android互动直播APP开发入门-学习笔记
链接:https://www.imooc.com/learn/923慕课网上的免费视频,绝对的良心之作,视频录制于2017年左右,虽说是五六年前的课程了,但是在直播大行其道的今天,相关的技术及观点依然不过时直播现状直播流程直播流程之采集直播流程之前处理直播流程之编码直播流程之推流与优化直播流程之服务端直播流程之客户端直播流程之交互系统交互方式:聊天、礼物、连麦、点赞直播流程-工具直播SDK的对比
2022年05月13日
54 阅读
0 评论
0 点赞
2022-05-07
易读错英语单词记录
整理我个人常读错的英语单词static发音:['stætɪk]链接:https://www.iciba.com/word?w=static释义:静态的;静止的;稳定的;静力的;静电的静电;静电干扰deprecated英[ˈdeprəkeɪtɪd]美[ 'dɛprə,ketɪd]链接:http://www.iciba.com/word?w=deprecated释义:不赞成,反对( deprecate的过去式和过去分词 )encrypt英[ɪnˈkrɪpt]美[ɛnkrɪpt]链接:http://www.iciba.com/word?w=encrypt释义:v.加密,将…译成密码vt.& vi. 把…加密(或编码),将…译成密码record英[ˈrekɔːd]美[ˈrekərd]链接:http://www.iciba.com/word?w=record释义n.记录,记载; 唱片; (体育运动或活动的)纪录; 履历; 案底v.记录;记载; 录制; 标明,显示adj.创纪录的parameter英[pəˈræmɪtə(r)]美[pəˈræmɪtər]链接:http://www.iciba.com/word?w=parameter释义n.[数]参数; <物><数>参量; 限制因素; 决定因素execute英[ˈeksɪkjuːt]美[ˈeksɪkjuːt]链接:http://www.iciba.com/word?w=execute释义v.处死; 实施; 完成; 制作decimal英[ˈdesɪml]美[ˈdesɪml]链接:http://www.iciba.com/word?w=decimal释义adj.十进位的; 小数的n.小数unique英[juˈniːk]美[juˈniːk]链接:http://www.iciba.com/word?w=unique释义adj.唯一的; 独特的; 特有的input英[ˈɪnpʊt]美[ˈɪnpʊt]链接:http://www.iciba.com/word?w=input释义n.输入,投入; 输入电路; <电>输入端; 输入的数据vt.把…输入电脑; 自 输入; 输入,给料
2022年05月07日
23 阅读
0 评论
0 点赞
2022-05-05
UI设计-AI基础-学习笔记
以下仅为学习过程中的简单记录图标的定义分类与应用场景线性![线性图表标(/usr/uploads/2022/05/2214274233.png)面性线性填充扁平图标通过色块构成图标,没有渐变、投影等效果手绘图标写实图标图标绘制的标准化流程设计流程确定主题风格 --> 关联性 --> 切入点(受众、行业) --> 草稿(大致形状构想)设计过程中的注意点结构造型:一致性栅格线细节处理色彩搭配后续输出包装总结图标结构设计之布尔运算可尝试使用布尔运算方式绘制手工如下图标:阵列复制技巧先使用 Ctrl+T 自由切换,按住Alt键可结合鼠标移动中心点,对图形进行调整(如位置、大小、角度等)后,可使用 Ctrl+Alt+Shift+T 快速复制可尝试使用阵列复制的方式绘制如下图标:圆角半径JS脚本插件Corner Editor圆角插件,下载地址:Corner Editor.zip下载后,解压将jsx文件拷贝到PS安装目录下的 Presets 目录,重启应用即可通过 文件 -> 脚本 -> Corner Editor 打开插件
2022年05月05日
39 阅读
0 评论
0 点赞
1
...
3
4
5
...
8