import { mitts } from "./tool"; /* * @Author: lzx * @Date: 2022-05-25 15:42:37 * @LastEditors: lzx * @LastEditTime: 2022-08-18 15:01:38 * @Description: Fuck Bug * @FilePath: \talk_pc\src\utils\socket.ts */ let socketUrl: any = ""; // socket地址 let websocket: any = null; // websocket 实例 let heartTime: any = null; // 心跳定时器实例 let socketHeart: number = 0; // 心跳次数 let HeartTimeOut: number = 3000; // 心跳超时时间 let socketError: number = 0; // 错误次数 // 初始化socket const initWebSocket = (url: any) => { socketUrl = url; // 初始化 websocket websocket = new WebSocket(url); websocketonopen(); websocketonmessage(); sendSocketHeart() }; // socket 连接成功 const websocketonopen = () => { websocket.onopen = function (e: any) { console.log("连接 websocket 成功", e); resetHeart(); }; }; // socket 连接失败 const websocketonerror = () => { websocket.onerror = function (e: any) { console.log("连接 websocket 失败", e); }; }; // socket 断开链接 const websocketclose = () => { websocket.onclose = function (e: any) { console.log("断开连接", e); }; }; // socket 接收数据 const websocketonmessage = () => { websocket.onmessage = function (e: any) { let msg = JSON.parse(e.data); if (msg.type === 'heartbeat') { resetHeart() console.log("心跳"); } // console.log("收到socket消息", JSON.parse(e.data)); test(msg) // 测试数据 }; }; // socket 发送数据 const sendMsg = (data: any) => { websocket.send(data); }; // socket 错误 const websocketError = () => { websocket.onerror = function (e: any) { console.log("socket 错误", e); }; }; // socket 重置心跳 const resetHeart = () => { socketHeart = 0; socketError = 0; clearInterval(heartTime); sendSocketHeart(); }; // socket心跳发送 const sendSocketHeart = () => { heartTime = setInterval(() => { if (socketHeart <= 2) { console.log("心跳发送：", socketHeart); websocket.send( JSON.stringify({ content: "", requestId: "aa9872be-d5b9-478e-aba4-50527cd3ef32", type: "heartbeat" }) ); socketHeart = socketHeart + 1; } else { reconnect() } }, HeartTimeOut); }; // socket重连 const reconnect = () => { if (socketError <= 2) { clearInterval(heartTime); initWebSocket(socketUrl); socketError = socketError + 1; console.log("socket重连", socketError); } else { console.log("重试次数已用完的逻辑", socketError); clearInterval(heartTime); } }; // 测试收到消息传递 const test = (msg: any) => { switch (msg.type) { case 'heartbeat': //加入会议 mitts.emit('heartbeat', msg) break; } } export { initWebSocket, websocketonmessage, sendMsg, websocketonopen, websocketonerror, websocketclose, websocketError, resetHeart, sendSocketHeart, reconnect, };
W3C在2020年12月份出台了Presentation API规范，这个api接口允许用户自由控制投放在其他屏幕上，并和投放窗口通信，查看W3C规范。演示地址：投屏演示，详细demo（Vue3/HTML）见文章末尾{alert type="error"} 特别注意：需要注意必须为https环境{/alert} 1、创建一个PresentationRequest对象，该对象将包含我们要在辅助附加显示器上显示的地址。const presentationRequest = new PresentationRequest('receiver.html'); 2、做控制及监听事件/* * @Author: lzx * @Date: 2022-07-08 15:34:16 * @LastEditors: lzx * @LastEditTime: 2022-07-08 17:14:31 * @Description: Fuck Bug * @FilePath: \test\js\index.js */ // 创建个投屏对象 const presentationRequest = new PresentationRequest(['https://blog.fxnws.com']); // 投屏网址 // const presentationRequest = new PresentationRequest(['test.html']);//投屏页面 // Make this presentation the default one when using the "Cast" browser menu. navigator.presentation.defaultRequest = presentationRequest; let presentationConnection; //监视连接是否可用 presentationRequest.addEventListener('connectionavailable', function (event) { presentationConnection = event.connection; presentationConnection.addEventListener('close', function () { console.log('> Connection closed.'); }); presentationConnection.addEventListener('terminate', function () { console.log('> Connection terminated.'); }); presentationConnection.addEventListener('message', function (event) { console.log('> ' + event.data); }); }); //监视可用的显示器 presentationRequest.getAvailability() .then(availability => { console.log('Available presentation displays: ' + availability.value); availability.addEventListener('change', function () { console.log('> Available presentation displays: ' + availability.value); }); }) .catch(error => { console.log('Presentation availability not supported, ' + error.name + ': ' + error.message); }); // 开启投屏 function startProjectionScreen () { presentationRequest.start() .then(connection => { console.log('Connected to ' + connection.url + ', id: ' + connection.id); }) .catch(error => { console.log(error); }); } //发送消息 function sendMsg () { const message = '发送消息测试'; const lang = document.body.lang || 'en-US'; console.log('Sending "' + message + '"...'); presentationConnection.send(JSON.stringify({ message, lang })); } //关闭连接，关闭连接后无法再控制弹出窗口 function closeLink () { console.log('Closing connection...'); presentationConnection.close(); } //关闭连接后，换可以重新再次连接，这里需要指定id // presentationRequest.reconnect(presentationId); //关闭弹窗，结束连接调用 function endLink () { console.log('Terminating connection...'); presentationConnection.terminate(); } 3、连接端页面消息的展示（搬运的官方）/* * @Author: lzx * @Date: 2022-07-08 15:34:16 * @LastEditors: lzx * @LastEditTime: 2022-07-08 15:34:30 * @Description: Fuck Bug * @FilePath: \test\js\test.js */ let connectionIdx = 0; let messageIdx = 0; //如果有连接建立，则调用该函数 function addConnection (connection) { connection.connectionId = ++connectionIdx; addMessage('New connection #' + connectionIdx); connection.addEventListener('message', function (event) { messageIdx++; const data = JSON.parse(event.data); const logString = 'Message ' + messageIdx + ' from connection #' + connection.connectionId + ': ' + data.message; addMessage(logString, data.lang); maybeSetFruit(data.message); connection.send('Received message ' + messageIdx); }); connection.addEventListener('close', function (event) { addMessage('Connection #' + connection.connectionId + ' closed, reason = ' + event.reason + ', message = ' + event.message); }); }; const fruitEmoji = { 'grapes': '\u{1F347}', 'watermelon': '\u{1F349}', 'melon': '\u{1F348}', 'tangerine': '\u{1F34A}', 'lemon': '\u{1F34B}', 'banana': '\u{1F34C}', 'pineapple': '\u{1F34D}', 'green apple': '\u{1F35F}', 'apple': '\u{1F34E}', 'pear': '\u{1F350}', 'peach': '\u{1F351}', 'cherries': '\u{1F352}', 'strawberry': '\u{1F353}' }; function addMessage (content, language) { const listItem = document.createElement("li"); if (language) { listItem.lang = language; } listItem.textContent = content; document.querySelector("#message-list").appendChild(listItem); }; function maybeSetFruit (message) { const fruit = message.toLowerCase(); if (fruit in fruitEmoji) { document.querySelector('#main').textContent = fruitEmoji[fruit]; } }; //文档载入后，监听连接 document.addEventListener('DOMContentLoaded', function () { if (navigator.presentation.receiver) { navigator.presentation.receiver.connectionList.then(list => { list.connections.map(connection => addConnection(connection)); list.addEventListener('connectionavailable', function (event) { addConnection(event.connection); }); }); } }); 4、接收端页面构 Demo Receiver Hello World! 5、vue3页面 开启投屏 发送消息 结束调用 6、详细demo附件下载{hide}{cloud title="PresentationAPI的Demo(Vue3/HTML)" type="default" url="https://blog.fxnws.com/usr/uploads/2023/10/1902289599.zip" password=""/}{/hide}2024年11月04日更新，看到评论区有部分朋友，遇到问题，我更新了相关Git完整Demo地址见下文1、vue3+ts+vite 报错 PresentationRequest 未定义 答：应该是ts未定义的报错，我当时做这个的时候，这个功能刚推出ts支持应该不完善，我也不想去定义相关的ts就直接使用// @ts-nocheck（这个指令用于告诉TypeScript编译器不在当前文件中执行类型检查）2、发现用ip访问不行，用127.0.0.1可以 答：直接用IP不行是因为没有https支持吧，我记得这个协议是需要https的支持的，具体要看下报错信息了，127.0.0.1是本机地址所以没事{alert type="warning"} 码云 {/alert}{alert type="warning"} Github {/alert}
使用vue-clipboard3实现复制到剪切板，首先引入：npm install --save vue-clipboard3（详见）使用教程 点我复制
这两天遇到了个软件，想反编一下，整理各种不太会，最后想着会不会是electron开发的，就突发奇想把软件.exe改成zip打开看看，果不其然看到了electron图标，看了下是用electron-quick-start写的（electron-quick-start）目录结构如下：打开resources目录可以看到app.asar（这个文件就是项目源码的归档文件，asar是一种归档格式）既然找到了app.asar，那就把它解压出来看看吧，具体操作如下： 1、npm install -g asar全局安装asar插件 2、使用asar命令解压asar extract app.asar（源文件路径） ./（解压路径） 3、解压后的样式，可以看到解压出来了个app文件夹，里面就是代码了，可以对着代码为所欲为了，逆向出来的package.json文件里是没有devDependencies的，而electron是开发环境依赖，所以要自己手动添加npm add electron --save-dev，然后加个启动方式script："start" : " electron . "需要执行npm install就执行下，然后执行npm start跑起来即可。（也可以创建一个空项目，将上面从app.asar中逆向处理的源代码拷贝过去） 4、改写下代码就可以重新打包了，打包前需要先为项目添加一些打包选项，编辑项目根目录下的package.json文件，为其添加build选项："build": { "appId": "程序包名", // 程序包名 "copyright": "版权相关信息", // 版权相关信息 "productName": "项目名字", // 项目名字 "dmg": { "background": "背景图片的路径", // 背景图片的路径 "window": { // 启动后窗口的大小 "width": 500, "height": 300 } }, "win": { // 打包后的文件会有个图标，这个图标的路径，不指定的话默认的不好看 "icon": "图标" } // 注意：图标、appId什么的要对应好，然后在项目的根路径下执行打包命令 5、如何重新打包app.asar，执行asar pack ./（路径） app.asar（路径&名称）虽然能重新打包个app.asar文件，但是无法文件夹打包成为exe文件（我暂时未找到好方法）结语：如果能直接文件夹打包exe的话，直接替换app.asar文件就很nice了
1、下载screenfull插件npm install screenfull@5 --save，注意Vue3中使用需要下载低版本的，否则会报错误2、引入，安装教程引入会发现，node_modules/screenfull下找不到index.js文件会报错如上，具体使用如下{mtitle title="具体使用"/} 测试全屏 import ScreenFull from 'screenfull/dist/screenfull' test() { if (!ScreenFull.isEnabled) { alert('不支持全屏') return false; } ScreenFull.toggle(); } 3、webApi提供的全屏API详见（MDN全屏API）
/* * @Author: lzx * @Date: 2022-05-06 13:33:43 * @LastEditors: lzx * @LastEditTime: 2022-05-06 13:57:06 * @Description: Fuck Bug * @FilePath: \reconsitution_talk_vant\utils\tool.js */ /*函数节流*/ const throttle = (fn, interval) => { let enterTime = 0; // 触发的时间 let gapTime = interval || 500; // 间隔时间，如果interval不传，则默认300ms return function () { var context = this; var backTime = new Date(); // 第一次函数return即触发的时间 if (backTime - enterTime > gapTime) { fn.call(context, arguments); enterTime = backTime; // 赋值给第一次触发的时间，这样就保存了第二次触发的时间 } }; } /*函数防抖*/ const debounce = (fn, interval) => { let timer; let gapTime = interval || 500; // 间隔时间，如果interval不传，则默认500ms return function () { clearTimeout(timer); var context = this; var args = arguments; // 保存此处的arguments，因为setTimeout是全局的，arguments不是防抖函数需要的。 timer = setTimeout(function () { fn.call(context, args); }, gapTime); }; } export default { throttle, debounce }; // 说明 // 页面引入：import tool from '../../utils/tool' // 调用示例： // 防抖 // test1: tool.debounce(function () { // console.log('防抖') // }, 5000) // 节流 // test2: tool.throttle(function () { // console.log('节流') // }, 5000)
1、语法糖与非语法糖代码写法对比{mtitle title="普通写法非语法糖"/} {mtitle title="setup语法糖写法"/}/** * setup语法糖写法 */ 2、如何在setup语法糖中使用props和配置TS使用（首先需要在 {mtitle title="setup语法糖使用props"/}/** *setup语法糖使用props */ 3、如何在setup语法糖中使用emit和配置TS使用（首先需要在 {mtitle title="子组件调用在setup语法糖下"/}/** * 子组件调用在setup语法糖下 */ 3、使用defineExpose（使用 ⽗组件 <child ref="child" />
// 下载引入 npm i pdfh5 -S 或者 npm install pdfh5 需要用到的页面引入 import Pdfh5 from "pdfh5" import"pdfh5/css/pdfh5.css" 模板 创建实例对象 setup (props, { emit }) { // 获取dom let pdfBox = ref(null) // 传参 const state = reactive({ pdfh5: null }) // 挂载 onMounted(() => { if (router.currentRoute.value.query) { state.pdfh5 = new Pdfh5(pdfBox.value, { // 此处使用ref获取避免不刷新 pdfurl: 'http://192.168.4.67:17000/sign/' + router.currentRoute.value.query.url }); //监听完成事件 state.pdfh5.on("complete", function (status, msg, time) { console.log("状态：" + status + "，信息：" + msg + "，耗时：" + time + "毫秒，总页数：" + this.totalNum) }) } }) onUnmounted(() => { state.pdfh5 = null }) }
通过前端edge库去实现（officeNet源码见附件）1、方案系统环境： 需要安装office，试过office2019 和 2016，其他版本尚未测试； 需要安装vs2010的office运行时（office安装完毕之后应该已经包含运行时环境，但实际情况不能保证每一台安装都没问题）（最好下载安装个微软运行库） 运行时微软官方下载地址（或者直接下载微软运行库合集）： 微软运行库 微软运行库 需要.netframework4.5及其以上（因为edge支持的.net的类库版本要求不得低于4.5，否则类库语法编译不通过，类库代码编写必须按照edge的格式要求定义返回值方法名入参格式等）2、方案前端环境： 1.package.json dependencies 追加 "electron-edge-js": "^14.16.1" （和electron 13.6.0版本匹配） 2.vue.config.js 外部拓展 externals 追加 "electron-edge-js"，打包的时候否则遗漏报错 3.officeNet.dll 提供的中间类库，放到前端项目根目录下调用 const edge = require('electron-edge-js')3、officeNet功能：{mtitle title="执行word转换pdf：前端代码调用示例（backgrund.js中测试）"/}var sdkPath = path.resolve("./officeNet.dll") // 三个参数名称和参数的值写死 const translate = edgefunc({ assemblyFile: sdkPath, typeName:"officeNet.OfficeTrans", translate: "Invoke" }) // 这两个参数名称写死，一个是输入文件绝对地址，一个是输出文件绝对地址，类库取这两个字段 var input = { inputFilePath: "E:\office-dll\0222\1.docx", outputFilePath: "E:\office-dll\0222\1.pdf" } translate(input, function(err,result) { if (err) throw err; console.log("traslate doc -> pdf: " + result); // 成功返回1 失败返回2 });{mtitle title="判断当前系统是否安装office"/}// 三个参数名称和参数的值写死 const officeVersionCheck =edge.func({ assemblyFile: sdkPath, typeName:"officeNet.OfficeCheck", translate: "Invoke" }) // 入参为{}对象（预留） officeVersionCheck({}, function(err,result) { if (err) throw err; console.log("office word version check：" + result); // 返回1 环境正常，返回2，office版本低于2010，返回3，系统无office });该方法通过注册表去获取word信息，不能绝对确认office在系统中安装，无法检测office安装是否存在问题；简单举例：注册表异常 或者 注册表残留 都会导致返回的值不准确（该方法只能作为一个参考）备注： 1.不能保证所有版本的word文件都能转换为pdf，可能存在转换失败的情况，代码注意做好不能转换的处理 2.仅限于windows环境使用 3.虽然edge内可以写c#代码，c#中可以写node代码，禁止这样写，代码互侵太严重 4.edge官方地址https://github.com/tjanczuk/edge#how-to-marshal-data-between-c-and-nodejs 5.自行熟悉edge的语法和代码约束和方法定义要求，edge性能不高（node和c#两个运行环境的胶水层），但是应用于当前pc场景本地转换文件功能足够{hide}officeNet.zip{/hide}
{mtitle title="使用教程"/}配合使用 Vue3接口封装（配合请求封装）{mtitle title="新建一个request.js文件"/}import axios from 'axios' import {getUserInfo} from '@/utils/auth' import errorCode from '@/utils/errorCode' let msgBox: any = null; // 创建axios实例 const service = axios.create({ baseURL: process.env.VUE_APP_API, // 配置的地址 timeout: 30000, // 超时 headers: { 'content-type': 'application/json;charset=utf-8' } }); // request拦截器 // @ts-ignore service.interceptors.request.use(config => { if (getUserInfo() && getUserInfo().token) { // @ts-ignore config.headers.token = getUserInfo().token.toString() // 自定义token } // get请求映射params参数 if (config.method === 'get' && config.params) { let url = config.url + '?'; for (const propName of Object.keys(config.params)) { // @ts-ignore const value = config.params[propName]; let part = encodeURIComponent(propName) + "="; if (value !== null && typeof (value) !== "undefined") { if (typeof value === 'object') { for (const key of Object.keys(value)) { let params = propName + '[' + key + ']'; let subPart = encodeURIComponent(params) + "="; url += subPart + encodeURIComponent(value[key]) + "&"; } } else { url += part + encodeURIComponent(value) + "&"; } } } url = url.slice(0, -1); config.params = {}; config.url = url; } return config }, error => { return Promise.reject(error) }) // 响应拦截器 service.interceptors.response.use(res => { // 未设置状态码则默认成功状态 const code = res.data.code || 200; // 获取错误信息 // @ts-ignore const msg = errorCode[code] || res.data.message || errorCode['default']; if (code === 403) { if (msgBox) return false; // msgBox = ElMessage.error("登录状态已过期，请重新登录！") setTimeout(() => { location.href = '/'; }, 1500) } else if (code === 400) { // ElMessage.error(msg); return res.data; } else if (code !== 200) { // ElMessage.error(msg); return res.data; } else { return res.data } }, error => { // ElMessage.error("服务器连接错误，请联系管理员！") // ElMessage.error(errorCode['default']); return null } ) export default service {mtitle title="新建一个errorCode.js文件"/}// 用于定义一些错误码封装 export default { '401': '认证失败，无法访问系统资源', '403': '当前操作没有权限', '404': '访问资源不存在', 'default': '服务器繁忙，请稍后再试！' } {mtitle title="新建一个request.js文件"/}另一个写的request.js封装/* * @Author: lzx * @Date: 2022-01-11 09:08:05 * @LastEditors: lzx * @LastEditTime: 2022-04-06 15:56:55 * @Description: Fuck Bug * @FilePath: \cs_talk_admin\src\utils\request.js */ import axios from 'axios' import router from '@/router/index' import { message } from '@utils/resetMessage.js' import { ElMessageBox } from 'element-plus' // 数据请求字符 // axios.defaults.baseURL = 'http://192.168.10.254:20000/talk/' // axios.defaults.baseURL = window.g.baseURL axios.defaults.baseURL = process.env.VUE_APP_API // 设置请求超时时长 axios.defaults.timeout = 5000 // 表示跨域请求时是否需要使用凭证 axios.defaults.withCredentials = false // axios.defaults.headers.common['token'] = AUTH_TOKEN axios.defaults.headers.post['Content-Type'] = 'application/json;charset=UTF-8' // 允许跨域 axios.defaults.headers.post['Access-Control-Allow-Origin-Type'] = '*' // 请求拦截器 axios.interceptors.request.use( config => { if (config.push === '/user') { } else { if (sessionStorage.getItem('token')) { // 在请求头加入token，名字要和后端接收请求头的token名字一样 config.headers.token = sessionStorage.getItem('token') } } return config }, error => { message.error('接口错误，请联系管理员！') return Promise.reject(error) } ) // 响应拦截器 axios.interceptors.response.use( response => { // 后端返回的状态码 // if (response.data.code === 403 || response.data.code === 110) { // // 清空缓存 // sessionStorage.clear() // message.error('登录已过期，请重新登录！') // console.log(router) // console.log(router.options.history) // console.log(router.options.history.state.current) // // 避免重复跳转,导致控制台红色 // if (router.options.history.state.current !== '/login') { // router.replace('/login') // } // } switch (response.data.code) { case 403 || 110: message.error('登录已过期，请重新登录！') // 清空缓存 sessionStorage.clear() if (router.options.history.state.current !== '/login') { router.replace('/login') } break case 500: message.error(response.data.message) break } return response }, error => { // message.error('服务器繁忙，请联系管理员！') // 清空缓存 // sessionStorage.clear() // // 避免重复跳转,导致控制台红色 // if (router.options.history.state.current !== '/login') { // router.replace('/login') // } ElMessageBox.confirm( '服务器繁忙，请联系管理员！', '错误提示', { confirmButtonText: '退出登录', cancelButtonText: '取消', type: 'warning' } ) .then(() => { message.success('退出登录！') // 清空缓存 sessionStorage.clear() // 避免重复跳转,导致控制台红色 if (router.options.history.state.current !== '/login') { router.replace('/login') } }).catch() return Promise.reject(error) } ) export default axios