[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$f2QqAh4Sq0zUNd4rpg-G3JRq5w8nNh1VQfyuZZUNGUh0":3,"donations-sidebar":10,"posts-{\"page\":1,\"pageSize\":10,\"categoryId\":\"e0f3b8d8-cfe7-41fb-802b-a79699d95968\"}":11,"sidebar-data":162},{"id":4,"name":5,"slug":6,"description":5,"sortOrder":7,"createdAt":8,"updatedAt":9},"e0f3b8d8-cfe7-41fb-802b-a79699d95968","JavaScript插件","javascript插件",7,"2022-06-01T14:08:31.000Z","2023-02-08T02:49:14.000Z",[],{"list":12,"total":159,"page":160,"pageSize":161},[13,34,46,58,71,86,99,115,129,144],{"id":14,"title":15,"slug":15,"content":16,"excerpt":17,"coverImage":18,"status":19,"isPinned":20,"pinnedAt":21,"viewCount":22,"publishedAt":23,"createdAt":23,"updatedAt":24,"category":25,"author":26,"tags":29},"832f843c-1039-4688-9c02-9ca87a25665a","localforage超大存储","## 安装\n```shell\npnpm add -S localforage\n```\n\n\n## 方法封装\n```js\nlocalforage.config({\n    name: \\\"mj-opreation-vue\\\", \u002F\u002F 项目名，即数据表名字\n});\n\u002F\u002F 查\nlocalforage.getItem(key);\n\n\u002F\u002F 增 | 改\nlocalforage.setItem(key, value);\n\n\u002F\u002F 删\nlocalforage.removeItem(key);\n\n```\n","localForage 是一个快速简单的 JavaScript 存储库。localForage 通过使用简单的localStorage类似 API 的异步存储（IndexedDB 或 WebSQL）来改善 Web 应用程序的离线体验。","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002F92c81574d19a39b31c2092b25290f993.png","PUBLISHED",false,null,512,"2023-06-02T03:48:21.000Z","2026-06-26T09:54:47.420Z",{"id":4,"name":5,"slug":6},{"id":27,"name":28,"avatar":21},"f9d0f2de-c700-4f90-b535-afd3dbe78128","Admin",[30],{"id":31,"name":32,"slug":33},"994cc226-578b-4a72-a57e-a47a63d2793e","JavaScript生态","javascript生态",{"id":35,"title":36,"slug":36,"content":37,"excerpt":36,"coverImage":38,"status":19,"isPinned":20,"pinnedAt":21,"viewCount":39,"publishedAt":40,"createdAt":40,"updatedAt":41,"category":42,"author":43,"tags":44},"c536eeaa-4ba3-439c-a68f-e2de06eaf994","小程序web-view教程","## 官方教程\n```language\nhttps:\u002F\u002Fdevelopers.weixin.qq.com\u002Fminiprogram\u002Fdev\u002Fcomponent\u002Fweb-view.html\n```\n\n\n## web-view页面通过wx.miniProgram.navigateTo跳转到小程序页面\n> CDN导入\n```language\n\u003Cscript src=\\\"https:\u002F\u002Fres.wx.qq.com\u002Fopen\u002Fjs\u002Fjweixin-1.3.2.js#wechat_redirect\\\">\u003C\u002Fscript>\n```\n\n> npm安装\n```language\nnpm i -S weixin-js-sdk\n```\n\n```js\nimport wx from 'weixin-js-sdk';\n```\n\n\n\n## 在需要做跳转的事件中：\n```js\n\u002F\u002F关闭浏览器（微信内置浏览器私有接口）\nWeixinJSBridge.call('closeWindow');\n\u002F\u002F返回小程序页面\nwx.miniProgram.navigateTo({\n      url: '\u002Fpages\u002Fad\u002Findex'\n})\n```\n\n\n\n","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002F32d9090e7ffb54c7e29011f04ababd8c.png",575,"2023-02-23T10:04:52.000Z","2026-05-24T20:08:40.000Z",{"id":4,"name":5,"slug":6},{"id":27,"name":28,"avatar":21},[45],{"id":31,"name":32,"slug":33},{"id":47,"title":48,"slug":48,"content":49,"excerpt":48,"coverImage":50,"status":19,"isPinned":20,"pinnedAt":21,"viewCount":51,"publishedAt":52,"createdAt":52,"updatedAt":53,"category":54,"author":55,"tags":56},"a948ba5c-cf86-4327-a1d7-128ba4fd13ac","js-cookie插件教程","## 简介\n> js-cookie是一个简单的，轻量级的处理cookies的js API。 通过npm安装后, 首先要在main.js中引入，否则报错。 在其他的单页面vue文件中使用js-cookie时, 每次使用都要再次引入， 否则报错。不能全局引入或者挂载到Vue的实例上。\n\n## 安装\n```shell\nnpm i js-cookie -S\n```\n### 在main.js入口文件中引入\n```js\nimport Cookies from 'js-cookie'\n```\n### 在test.vuevue单文件中使用\n> 首先引入import Cookies from 'js-cookie'。 然后在methods中定义的方法中去根据js-cookie的使用语法进行操作\n\n##  最基础用法\n```js\n\u002F\u002F 设置cookie\nCookies.set('name', 'andy凌云');\n\u002F\u002F 获取cookie   获取不到返回undefined\nCookies.get('name');\n\u002F\u002F 直接获取解析后的全部cookie对象\nCookies.getJSON(); \n\u002F\u002F 删除cookie\nCookies.remove('name');\n```\n\n## 进阶用法\n```js\n\u002F\u002F 1. 设置cookie \n    \nCookies.set(\\\"name\\\", { foo: \\\"bar\\\" });   \u002F\u002F json格式\n\u002F\u002F Create an expiring cookie, valid to the path of the current page:\nCookies.set(\\\"name\\\", \\\"andy凌云\\\", { expires: 7, path: \\\"\\\" }); \u002F\u002F 7天过期\n\n\u002F\u002F 2. 读取cookie\nCookies.get(\\\"name\\\"); \u002F\u002F读取名字为name的cookie, \nCookies.get(); \u002F\u002F 读取所有的cookie,结果是个对象。\nCookies.getJSON(); \u002F\u002F 读取所有cookie, 结果是个解析后的对象， 推荐使用\n\n\u002F\u002F 3. 删除cookie\nCookies.remove('name') \u002F\u002F 最基本的方式是根据所存的键值删除对应cookie\n\u002F\u002F 删除全部cookie \n\u002F* 在网上搜了半天， 没搜到删除全部cookie的API方法。 笨的办法通过Cookies.get()去获取\n全部cookie组成的对象,然后遍历对象，根据各个键的值去依次删除 *\u002F\nlet allCookies = Cookies.get();\nObject.keys(allCookies).forEach((item)=>{\n  Cookies.remove(item);\n})\n\u002F\u002F 删除完成后， 再取Cookies.get()得到是一个空对象\n\n\u002F\u002F4. 特殊使用(在cookie中读取对象或数组)\n\n\u002F\u002F 跟一般使用不同的是，从Cookie中取出的时候，要从字符串转换成json格式：\nconst user = {\n  name: \\\"lia\\\",\n  age: 18\n};\nconst nameList = [\n  'andy凌云',\n  '天道酬勤',\n  '地道酬善'\n]\nCookies.set('user', user)\nCookies.set('nameList', nameList)\n\u002F\u002F 4.1  使用基础的JSON.parse方法去将get后的字符串解析为对象或数组\nJSON.parse(Cookies.get(\\\"user\\\"));\nJSON.parse(Cookies.get(\\\"nameList\\\"));\n\u002F\u002F 4.2 使用js-cookie自带的getJSON方法， 直接就解析成了能直接进行操作的数组或对象\nCookies.getJSON('user');\nCookies.getJSON('nameList');\n\u002F\u002F 如果是想直接操作所有的cookie对象，建议不要使用Cookies.get(),因为它得到的值是字符串，还需要使用JSON.parse()去解析， 最好使用Cookies.getJSON()。\n\n```\n\n## set方法支持的属性\n| 属性 | 说明 |\n| - | - |\n| expires | 定义有效期。如果传入Number，那么单位为天，你也可以传入一个Date对象，表示有效期至Date指定时间。默认情况下cookie有效期截止至用户退出浏览器 |\n| path | string，表示此cookie对哪个地址可见。默认为'\u002F' |\n| domain | string，表示此cookie对哪个域名可见, 设置后cookie会对所有子域名可见。默认为对创建此cookie的域名和子域名可见 |\n| secure | true或false，表示cookie传输是否仅支持https。默认为不要求协议必须为https |\n\n## 封装公共方法来操作token\n```js\nimport Cookies from 'js-cookie'\n\nconst TokenKey = 'Admin-Token'\n\nexport function getToken() {\n  return Cookies.get(TokenKey)\n}\n\nexport function setToken(token) {\n  return Cookies.set(TokenKey, token)\n}\n\nexport function removeToken() {\n  return Cookies.remove(TokenKey)\n}\n```\n","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002F25d7d5663c87996a87b6334fae798458.png",780,"2023-01-02T02:44:29.000Z","2026-05-25T07:18:33.000Z",{"id":4,"name":5,"slug":6},{"id":27,"name":28,"avatar":21},[57],{"id":31,"name":32,"slug":33},{"id":59,"title":60,"slug":61,"content":62,"excerpt":60,"coverImage":63,"status":19,"isPinned":20,"pinnedAt":21,"viewCount":64,"publishedAt":65,"createdAt":65,"updatedAt":66,"category":67,"author":68,"tags":69},"e6b34f13-ba61-4db4-960f-d96ead94bcf4","Echarts组件简单封装","echarts组件简单封装","## 官网示例参考\n```http\nhttps:\u002F\u002Fecharts.apache.org\u002Fexamples\u002Fzh\u002Findex.html\n```\n\n```js\n           communityOption: {\n                xAxis: {\n                    type: \\\"category\\\",\n                    data: [\n                        \\\"文章总量\\\",\n                        \\\"文章本月新增\\\",\n                        \\\"文章本周新增\\\",\n                        \\\"\\\",\n                        \\\"观点总量\\\",\n                        \\\"观点本月新增\\\",\n                        \\\"观点本周新增\\\",\n                        \\\"\\\",\n                        \\\"回答总量\\\",\n                        \\\"回答本月新增\\\",\n                        \\\"回答本周新增\\\",\n                        \\\"\\\",\n                        \\\"话题总量\\\",\n                        \\\"话题本月新增\\\",\n                        \\\"话题本周新增\\\",\n                        \\\"\\\",\n                        \\\"问题总量\\\",\n                        \\\"问题本月新增\\\",\n                        \\\"问题本周新增\\\"\n                    ],\n                    \u002F\u002F文本放不下，倾斜显示\n                    axisLabel: {\n                        interval: 0,\n                        rotate: -20\n                    }\n                },\n                yAxis: {\n                    type: \\\"value\\\"\n                },\n                series: [\n                    {\n                        data: [],\n                        \u002F\u002F柱图宽度\n                        barWidth: 10,\n                        type: \\\"bar\\\"\n                    }\n                ]\n            }\n```\n\n\n\n\n## 代码展示\n```vue\n\u003Ctemplate>\n    \u003Cdiv class=\\\"echarts\\\">\n        \u003Cdiv :id=\\\"id\\\" :style=\\\"[size]\\\">\u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript>\nimport * as echarts from \\\"echarts\\\";\nexport default {\n    name: \\\"pieChart\\\",\n\n    props: {\n        \u002F\u002Fecharts配置文件\n        option: {\n            type: Object,\n            default: null\n        },\n        \u002F\u002Fdiv的id，同一页面使用多个图库，id要不一样\n        id: {\n            type: String,\n            default: null\n        },\n\n        \u002F\u002F宽度\n        width: {\n            type: String,\n            default: \\\"400px\\\"\n        },\n        \u002F\u002F高度\n        height: {\n            type: String,\n            default: \\\"400px\\\"\n        }\n    },\n\n    data() {\n        return {\n            size: {\n                width: this.width,\n                height: this.height\n            }\n        };\n    },\n    mounted() {},\n\n    \u002F\u002F监听数据是否改变，改变就重新渲染\n    watch: {\n        option: {\n            handler(newValue, oldValue) {\n                this.myEcharts();\n            },\n            immediate: true,\n            deep: true\n        }\n    },\n\n    methods: {\n        myEcharts() {\n            this.$nextTick(() => {\n                const chartDom = document.getElementById(this.id);\n                const myChart = echarts.init(chartDom);\n                this.option && myChart.setOption(this.option);\n            });\n        }\n    }\n};\n\u003C\u002Fscript>\n\n\u003Cstyle>\u003C\u002Fstyle>\n\n```\n\n\n## vue3 版本(echart改成按需加载，性能更加好)\n```vue\n\u003Ctemplate>\n  \u003Cdiv class=\\\"echarts\\\">\n    \u003Cdiv :id=\\\"id\\\" :style=\\\"[size]\\\">\u003C\u002Fdiv>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript lang=\\\"ts\\\" setup>\n  import * as echarts from 'echarts\u002Fcore';\n  import {\n    PieChart,\n    \u002F\u002F 系列类型的定义后缀都为 SeriesOption\n    BarSeriesOption,\n    LineChart,\n    LineSeriesOption,\n  } from 'echarts\u002Fcharts';\n  import {\n    TitleComponent,\n    \u002F\u002F 组件类型的定义后缀都为 ComponentOption\n    TitleComponentOption,\n    TooltipComponent,\n    TooltipComponentOption,\n    GridComponent,\n    GridComponentOption,\n    \u002F\u002F 数据集组件\n    DatasetComponent,\n    DatasetComponentOption,\n    \u002F\u002F 内置数据转换器组件 (filter, sort)\n    TransformComponent,\n  } from 'echarts\u002Fcomponents';\n  import { LabelLayout, UniversalTransition } from 'echarts\u002Ffeatures';\n  import { CanvasRenderer } from 'echarts\u002Frenderers';\n\n  \u002F\u002F 通过 ComposeOption 来组合出一个只有必须组件和图表的 Option 类型\n  type ECOption = echarts.ComposeOption\u003C\n    | BarSeriesOption\n    | LineSeriesOption\n    | TitleComponentOption\n    | TooltipComponentOption\n    | GridComponentOption\n    | DatasetComponentOption\n  >;\n\n  \u002F\u002F 注册必须的组件\n  echarts.use([\n    TitleComponent,\n    TooltipComponent,\n    GridComponent,\n    DatasetComponent,\n    TransformComponent,\n    PieChart,\n    LineChart,\n    LabelLayout,\n    UniversalTransition,\n    CanvasRenderer,\n  ]);\n\n  const props = defineProps({\n    \u002F\u002Fecharts配置文件\n    option: {\n      type: Object,\n      default: null,\n    },\n    \u002F\u002Fdiv的id，同一页面使用多个图库，id要不一样\n    id: {\n      type: String,\n      default: null,\n    },\n\n    \u002F\u002F宽度\n    width: {\n      type: String,\n      default: '400px',\n    },\n    \u002F\u002F高度\n    height: {\n      type: String,\n      default: '400px',\n    },\n  });\n\n  const size = reactive({\n    width: props.width,\n    height: props.height,\n  });\n\n  const myEcharts = () => {\n    nextTick(() => {\n      const chartDom = document.getElementById(props.id);\n\n      if (chartDom) {\n        const myChart = echarts.init(chartDom);\n        props.option && myChart.setOption(props.option);\n\n        window.addEventListener('resize', () => {\n          myChart.resize();\n        });\n      }\n    });\n  };\n\n  watch(props.option, (newValue, oldValue) => {\n    myEcharts();\n  });\n\u003C\u002Fscript>\n\n\u003Cstyle>\u003C\u002Fstyle>\n\n```\n\n","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002Fa6d93c75bc8066f903f6dfcf9d2b6093.jpg",613,"2022-11-23T09:26:05.000Z","2026-05-25T02:09:14.000Z",{"id":4,"name":5,"slug":6},{"id":27,"name":28,"avatar":21},[70],{"id":31,"name":32,"slug":33},{"id":72,"title":73,"slug":73,"content":74,"excerpt":75,"coverImage":76,"status":19,"isPinned":20,"pinnedAt":21,"viewCount":77,"publishedAt":78,"createdAt":78,"updatedAt":79,"category":80,"author":81,"tags":82},"c49720d4-b2b0-4466-ad15-533d7a8884a3","gsap数字滚动动画","## 安装\n```shell\npnpm add -S gsap\n```\n\n\n### 封装子组件\n## vue3版本\n\n```vue\n\u003Ctemplate>\n    {{ d.num.toFixed(0) }}\n\u003C\u002Ftemplate>\n\u003Cscript setup lang=\\\"ts\\\">\nimport gsap from 'gsap';\n\nconst props = defineProps({\n    value: {\n        type: Number,\n        default: 0\n    }\n});\n\nconst d = reactive({\n    num: 0\n});\n\nfunction AnimateToValue() {\n    gsap.to(d, {\n        duration: 0.5,\n        num: props.value\n    });\n}\n\nAnimateToValue();\n\nwatch(\n    () => props.value,\n    () => AnimateToValue()\n);\n\u003C\u002Fscript>\n\n```\n\n\n## vue2版本\n\n```vue\n\u003Ctemplate>\n    \u003Cdiv>\n        {{ num.toFixed(0) }}\n    \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\u003Cscript>\nimport gsap from \\\"gsap\\\";\n\nexport default {\n    name: \\\"CountTo\\\",\n\n    props: {\n        value: {\n            type: Number,\n            default: 0\n        }\n    },\n\n    data() {\n        return {\n            num: 0\n        };\n    },\n\n    mounted() {\n        this.AnimateToValue();\n    },\n    methods: {\n        AnimateToValue() {\n            gsap.to(this.$data, {\n                duration: 1,\n                num: this.value\n            });\n        }\n    },\n    \u002F\u002F监听数据是否改变，改变就重新渲染\n    watch: {\n        value: {\n            handler(newValue, oldValue) {\n                this.AnimateToValue();\n            },\n            immediate: true,\n            deep: true\n        }\n    }\n};\n\u003C\u002Fscript>\n\n```\n\n\n\n### 使用\n```js\n  \u003CCountTo :value=\\\"item\\\">\u003C\u002FCountTo>\n\nimport CountTo from '@\u002Fcomponents\u002FCountTo.vue';\n```\n\n","GSAP 是一个强大的 JavaScript 工具集，可将开发人员变成动画超级英雄。","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002F26b499d9f656321d51a04850e38220bc.png",1144,"2022-09-11T13:08:12.000Z","2026-05-24T20:20:06.000Z",{"id":4,"name":5,"slug":6},{"id":27,"name":28,"avatar":21},[83],{"id":84,"name":85,"slug":85},"4c6be544-8a00-4445-92a3-e3dcbaf6142e","动画",{"id":87,"title":88,"slug":89,"content":90,"excerpt":88,"coverImage":91,"status":19,"isPinned":20,"pinnedAt":21,"viewCount":92,"publishedAt":93,"createdAt":93,"updatedAt":94,"category":95,"author":96,"tags":97},"58bca27d-4c18-41d1-87d1-27d8d03681ba","常见IP定位地区方法","常见ip定位地区方法","## 高德地图API\n```url\nhttps:\u002F\u002Flbs.amap.com\u002Fapi\u002Fwebservice\u002Fguide\u002Fapi\u002Fipconfig\n```\n\n## 百度地图API\n```url\nhttps:\u002F\u002Flbsyun.baidu.com\u002Ffaq\u002Fapi?title=webapi\u002Fip-api-base\n```\n\n## 免费API\n```url\nhttps:\u002F\u002Fwww.free-api.com\u002Fdoc\u002F271\n```\n\n## 免费API（比较方便）\n```url\nhttps:\u002F\u002Fpv.sohu.com\u002Fcityjson?ie=utf-8\n```\n\n## 高级玩家-IP离线库\n\n```url\nhttps:\u002F\u002Fgitee.com\u002Flionsoul\u002Fip2region\n```\n","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002Fcf238b17b191be75bf74afb7165df581.png",693,"2022-08-15T14:56:44.000Z","2026-05-25T05:27:03.000Z",{"id":4,"name":5,"slug":6},{"id":27,"name":28,"avatar":21},[98],{"id":31,"name":32,"slug":33},{"id":100,"title":101,"slug":102,"content":103,"excerpt":104,"coverImage":105,"status":19,"isPinned":20,"pinnedAt":21,"viewCount":106,"publishedAt":107,"createdAt":107,"updatedAt":108,"category":109,"author":110,"tags":111},"5a60b315-d098-4e9b-b800-3b0abc886ed5","Compressor.js前端图片压缩","compressor-js前端图片压缩","\n## 引用方法\n\n\n### Vue3\n\n```shell\nnpm i -S compressorjs\n```\n\n```shell\nimport Compressor from 'compressorjs';\n```\n\n## 使用方法\n\n```js\nimport axios from 'axios';\nimport Compressor from 'compressorjs';\n\ndocument.getElementById('file').addEventListener('change', (e) => {\n  const file = e.target.files[0];\n\n  if (!file) {\n    return;\n  }\n\n  new Compressor(file, {\n    quality: 0.6,\n    success(result) {\n      const formData = new FormData();\n      formData.append('file', result, result.name);\n      axios.post('\u002Fpath\u002Fto\u002Fupload', formData).then(() => {\n        console.log('Upload success');\n      });\n    },\n    error(err) {\n      console.log(err.message);\n    },\n  });\n  \n});\n\n```\n\n\n## 常见问题\n","JavaScript 图像压缩器","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002Fe3ee16fcc524cdacfb6633eaa6f90d83.png",1011,"2022-08-01T02:40:13.000Z","2026-05-25T03:04:30.000Z",{"id":4,"name":5,"slug":6},{"id":27,"name":28,"avatar":21},[112],{"id":113,"name":114,"slug":114},"9321a12e-ea72-49a9-a32d-5566149f812f","图片压缩",{"id":116,"title":117,"slug":117,"content":118,"excerpt":117,"coverImage":119,"status":19,"isPinned":20,"pinnedAt":21,"viewCount":120,"publishedAt":121,"createdAt":121,"updatedAt":122,"category":123,"author":124,"tags":125},"66d5dab1-895a-4fca-b6f0-f5d535737146","sortablejs实现列表拖拽排序","\n## 引用方法\n\n\n### Vue3\n\n```shell\nnpm i -S sortablejs\n```\n\n```shell\nimport Sortable from \\\"sortablejs\\\";\n```\n\n## 使用方法\n\n```js\n\u003Ctemplate>\n  \u003Cel-card class=\\\"box-card\\\">\n    \u003Ctemplate #header>\n      \u003Cdiv class=\\\"flex justify-between\\\">\n        \u003Cspan>友链列表\u003C\u002Fspan>\n        \u003Cel-button type=\\\"primary\\\" @click=\\\"addEditFriendLinks\\\">添加友链\u003C\u002Fel-button>\n      \u003C\u002Fdiv>\n    \u003C\u002Ftemplate>\n\n    \u003Cdiv class=\\\"table1\\\">\n      \u003Cel-table\n        ref=\\\"dragTable\\\"\n        :data=\\\"tableData\\\"\n        style=\\\"width: 100%\\\"\n        border\n        show-overflow-tooltip\n        stripe\n        class=\\\"t1\\\"\n        row-key=\\\"id\\\"\n        :row-class-name=\\\"tableRowClassName\\\"\n      >\n        \u003Cel-table-column prop=\\\"orderId\\\" label=\\\"排序\\\" align=\\\"center\\\" width=\\\"100\\\" class=\\\"sorting\\\">\n          \u003Ctemplate #default=\\\"scope\\\">\n            \u003Cel-button class=\\\"move\\\" type=\\\"text\\\" size=\\\"small\\\">{{ scope.row.orderId }}\u003C\u002Fel-button>\n          \u003C\u002Ftemplate>\n        \u003C\u002Fel-table-column>\n        \u003Cel-table-column prop=\\\"avatar\\\" label=\\\"友链Logo\\\" align=\\\"center\\\" width=\\\"100\\\">\n          \u003Ctemplate #default=\\\"scope\\\">\n            \u003Ca :href=\\\"scope.row.url\\\" target=\\\"_blank\\\">\n              \u003Cimg :src=\\\"scope.row.avatar\\\" class=\\\"avatar\\\" \u002F>\n            \u003C\u002Fa>\n          \u003C\u002Ftemplate>\n        \u003C\u002Fel-table-column>\n        \u003Cel-table-column prop=\\\"name\\\" label=\\\"友链名称\\\" align=\\\"center\\\" width=\\\"150\\\" \u002F>\n        \u003Cel-table-column\n          prop=\\\"status\\\"\n          :formatter=\\\"formatterStatus\\\"\n          label=\\\"状态\\\"\n          width=\\\"120\\\"\n          align=\\\"center\\\"\n        \u002F>\n        \u003Cel-table-column prop=\\\"desc\\\" :formatter=\\\"formatterDesc\\\" label=\\\"简介描述\\\" align=\\\"center\\\" \u002F>\n        \u003Cel-table-column prop=\\\"url\\\" label=\\\"友链地址\\\" align=\\\"center\\\">\n          \u003Ctemplate #default=\\\"scope\\\">\n            \u003Ca :href=\\\"scope.row.url\\\" target=\\\"_blank\\\">{{ scope.row.url }}\u003C\u002Fa>\n          \u003C\u002Ftemplate>\n        \u003C\u002Fel-table-column>\n        \u003Cel-table-column prop=\\\"createdAt\\\" label=\\\"创建时间\\\" align=\\\"center\\\" width=\\\"120\\\" \u002F>\n        \u003Cel-table-column prop=\\\"updatedAt\\\" label=\\\"更新时间\\\" align=\\\"center\\\" width=\\\"120\\\" \u002F>\n        \u003Cel-table-column prop=\\\"address\\\" label=\\\"操作\\\" align=\\\"center\\\" width=\\\"220\\\">\n          \u003Ctemplate #default=\\\"scope\\\">\n            \u003Cel-button @click=\\\"handleEdit(scope.row)\\\">编辑\u003C\u002Fel-button>\n            \u003Cel-popconfirm\n              title=\\\"确定删除这个友链么？\\\"\n              confirm-button-text=\\\"确定\\\"\n              cancel-button-text=\\\"取消\\\"\n              icon=\\\"el-icon-info\\\"\n              icon-color=\\\"red\\\"\n              @confirm=\\\"deleteType(scope.row.id)\\\"\n            >\n              \u003Ctemplate #reference>\n                \u003Cel-button type=\\\"danger\\\">删除\u003C\u002Fel-button>\n              \u003C\u002Ftemplate>\n            \u003C\u002Fel-popconfirm>\n          \u003C\u002Ftemplate>\n        \u003C\u002Fel-table-column>\n      \u003C\u002Fel-table>\n    \u003C\u002Fdiv>\n    \u003C!-- dialog -->\n    \u003Cel-dialog v-model=\\\"visible\\\" :title=\\\"titleMsg\\\" width=\\\"400px\\\">\n      \u003Cel-form ref=\\\"formRef\\\" :model=\\\"formData\\\" :rules=\\\"rules\\\">\n        \u003Cel-form-item label=\\\"友链名称\\\" prop=\\\"name\\\" label-width=\\\"90px\\\">\n          \u003Cel-input v-model=\\\"formData.name\\\" placeholder=\\\"请填写友链名称\\\" \u002F>\n        \u003C\u002Fel-form-item>\n        \u003Cel-form-item label=\\\"友链logo\\\" prop=\\\"avatar\\\" label-width=\\\"90px\\\">\n          \u003Cel-input v-model=\\\"formData.avatar\\\" placeholder=\\\"请填写友链logo图片地址\\\" \u002F>\n        \u003C\u002Fel-form-item>\n        \u003Cel-form-item label=\\\"友链地址\\\" prop=\\\"url\\\" label-width=\\\"90px\\\">\n          \u003Cel-input v-model=\\\"formData.url\\\" placeholder=\\\"请填写友链地址链接\\\" \u002F>\n        \u003C\u002Fel-form-item>\n        \u003Cel-form-item label=\\\"友链简述\\\" prop=\\\"desc\\\" label-width=\\\"90px\\\">\n          \u003Cel-input\n            v-model=\\\"formData.desc\\\"\n            :rows=\\\"3\\\"\n            type=\\\"textarea\\\"\n            placeholder=\\\"请填写友链简述说明\\\"\n          \u002F>\n        \u003C\u002Fel-form-item>\n        \u003Cel-form-item label=\\\"友链状态\\\" prop=\\\"status\\\" label-width=\\\"90px\\\">\n          \u003Cel-select v-model=\\\"formData.status\\\" placeholder=\\\"请选择友链状态\\\">\n            \u003Cel-option\n              v-for=\\\"item in statusMap\\\"\n              :key=\\\"item.value\\\"\n              :label=\\\"item.label\\\"\n              :value=\\\"item.value\\\"\n            \u002F>\n          \u003C\u002Fel-select>\n        \u003C\u002Fel-form-item>\n        \u003Cel-form-item label=\\\"排序定义\\\" label-width=\\\"90px\\\" prop=\\\"orderId\\\">\n          \u003Cel-input v-model=\\\"formData.orderId\\\" placeholder=\\\"填写排序数字[不填写系统自动赋予]\\\" \u002F>\n        \u003C\u002Fel-form-item>\n      \u003C\u002Fel-form>\n      \u003Ctemplate #footer>\n        \u003Cspan class=\\\"dialog-footer\\\">\n          \u003Cel-button @click=\\\"visible = false\\\">取 消\u003C\u002Fel-button>\n          \u003Cel-button type=\\\"primary\\\" @click=\\\"submit\\\">确 定\u003C\u002Fel-button>\n        \u003C\u002Fspan>\n      \u003C\u002Ftemplate>\n    \u003C\u002Fel-dialog>\n\n    \u003C!-- 分页 -->\n    \u003Cdiv class=\\\"flex justify-center mt-6\\\">\n      \u003Cel-pagination\n        :current-page=\\\"params.page\\\"\n        :page-size=\\\"params.pageSize\\\"\n        :page-sizes=\\\"[10, 20, 30, 10000]\\\"\n        :disabled=\\\"disabled\\\"\n        :background=\\\"background\\\"\n        layout=\\\"sizes, prev, pager, next, jumper\\\"\n        :total=\\\"total\\\"\n        @size-change=\\\"handleSizeChange\\\"\n        @current-change=\\\"changePage\\\"\n      \u002F>\n    \u003C\u002Fdiv>\n  \u003C\u002Fel-card>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\\\"ts\\\">\n  import {\n    getFriendLinks,\n    createFriendLinks,\n    updateFriendLinks,\n    delFriendLinks,\n  } from '@\u002Fapi\u002Ffriend-links';\n  import { ElMessage } from 'element-plus';\n  import { formatTime } from '@\u002Futils\u002Ftools';\n  import { statusMap } from '.\u002Fconstant';\n\n  import Sortable from 'sortablejs';\n  const background = ref(false);\n  const disabled = ref(false);\n\n  const handleSizeChange = (val: number) => {\n    params.value.pageSize = val;\n    queryFriendLinks();\n  };\n  const formRef: any = ref(null);\n  const data: any = reactive({\n    tableData: [],\n    total: null,\n    params: {\n      page: 1,\n      pageSize: 10,\n    },\n    activeId: null,\n    formData: {\n      name: null,\n      status: 1,\n      orderId: null,\n      desc: null,\n      url: null,\n      avatar: null,\n    },\n    visible: false,\n    type: 1,\n    rules: {\n      name: [{ required: true, message: '请填写友链名称', trigger: 'blur' }],\n      desc: [{ required: true, message: '请填写友链描述', trigger: 'blur' }],\n      url: [{ required: true, message: '请填写友链地址', trigger: 'blur' }],\n      avatar: [{ required: true, message: '请填写友链logo地址', trigger: 'blur' }],\n      status: [{ required: true, message: '请选择友链状态', trigger: 'blur' }],\n    },\n  });\n\n  const { tableData, total, params, formData, visible, rules } = toRefs(data);\n\n  \u002F* 查询友链列表 *\u002F\n  async function queryFriendLinks() {\n    const res: any = await getFriendLinks(data.params);\n    formatTime(res.rows);\n    data.tableData = res.rows;\n    data.total = res.count;\n  }\n\n  queryFriendLinks();\n\n  \u002F* 添加或者修改友链 *\u002F\n  function addEditFriendLinks() {\n    data.type = 1;\n    data.visible = true;\n  }\n\n  const submit = async () => {\n    formRef.value.validate(async (valid) => {\n      if (valid) {\n        let param = JSON.parse(JSON.stringify(data.formData));\n        param.id && delete param.id;\n        data.type === 2 && (param.id = data.activeId);\n\n        if (data.type === 1) {\n          await createFriendLinks(param);\n        } else {\n          await updateFriendLinks(param);\n        }\n\n        ElMessage({ message: '操作成功', type: 'success' });\n        data.visible = false;\n        formRef.value.resetFields();\n        data.formData = resetForm();\n        queryFriendLinks();\n      }\n    });\n  };\n\n  function changePage(val) {\n    data.params.page = val;\n    queryFriendLinks();\n  }\n\n  function handleEdit(row) {\n    const { name, status, orderId, desc, url, avatar, id } = row;\n    data.activeId = id;\n    data.type = 2;\n    Object.assign(data.formData, { name, status, orderId, desc, url, avatar });\n    data.visible = true;\n  }\n\n  async function deleteType(id) {\n    await delFriendLinks({ id });\n    ElMessage({ message: '删除成功', type: 'success' });\n    queryFriendLinks();\n  }\n\n  const titleMsg = computed(() => {\n    return data.type === 1 ? '添加友链' : '修改友链';\n  });\n\n  const resetForm = () => {\n    return { name: null, status: 1, orderId: null };\n  };\n\n  function formatterStatus(row) {\n    return row.status == 1 ? '激活' : '禁止';\n  }\n\n  function formatterDesc(row) {\n    return row.desc.length > 16 ? row.desc.substr(0, 16) + '...' : row.desc;\n  }\n\n  \u002F\u002F 创建sortable实例\n  function initSortable(className) {\n    \u002F\u002F 获取表格row的父节点\n    const table = document.querySelector('.' + className + ' .el-table__body-wrapper tbody');\n    \u002F\u002F 创建拖拽实例\n    let dragTable = Sortable.create(table, {\n      \u002F\u002F动画\n      animation: 150,\n      \u002F\u002F 拖拽不可用? false 启用（刚刚渲染表格的时候起作用，后面不起作用）\n      disabled: false,\n      \u002F\u002F指定拖拽目标，点击此目标才可拖拽元素(此例中设置操作按钮拖拽)\n      handle: '.move',\n      \u002F\u002F指定不可拖动的类名（el-table中可通过row-class-name设置行的class）\n      filter: '.disabled',\n      \u002F\u002F设置拖拽样式类名\n      dragClass: 'dragClass',\n      \u002F\u002F设置拖拽停靠样式类名\n      ghostClass: 'ghostClass',\n      \u002F\u002F设置选中样式类名\n      chosenClass: 'chosenClass',\n      \u002F\u002F 开始拖动事件\n      onStart: () => {\n        console.log('开始拖动');\n      },\n      \u002F\u002F 结束拖动事件\n      onEnd: async ({ newIndex, oldIndex }) => {\n        console.log('结束拖动', `拖动前索引${oldIndex}---拖动后索引${newIndex}`);\n        const currRow = tableData.value.splice(oldIndex, 1)[0];\n        tableData.value.splice(newIndex, 0, currRow);\n        console.log('结束拖动', tableData.value);\n\n        let newTableData: any = [];\n        \u002F\u002F更新排序\n        tableData.value.map(async (item, index) => {\n          item.orderId = index + 1 + (params.value.page - 1) * params.value.pageSize;\n          newTableData.push({\n            id: item.id,\n            orderId: item.orderId,\n          });\n\n          console.log(item.orderId);\n        });\n\n        await updateFriendLinks(newTableData);\n      },\n    });\n\n    console.log('dragTable', dragTable);\n  }\n  \u002F\u002F 设置表格row的class\n  function tableRowClassName({ row }) {\n    if (row.disabled) {\n      return 'disabled';\n    }\n    return '';\n  }\n  onMounted(() => {\n    initSortable('t1');\n  });\n\u003C\u002Fscript>\n\n\u003Cstyle lang=\\\"scss\\\" scoped>\n  .el-select {\n    width: 100%;\n  }\n  .avatar {\n    width: 50px;\n    height: 50px;\n    border-radius: 50%;\n    margin: 0 auto;\n  }\n\u003C\u002Fstyle>\n\n```\n\n\n## 常见问题\n\n","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002Fa6f24db2029a30c3d9446a1b2761938a.png",1645,"2022-07-28T12:39:00.000Z","2026-05-25T03:37:12.000Z",{"id":4,"name":5,"slug":6},{"id":27,"name":28,"avatar":21},[126],{"id":127,"name":128,"slug":128},"1831cd06-0d6b-48f7-94fa-324782fe23cb","拖拽",{"id":130,"title":131,"slug":132,"content":133,"excerpt":131,"coverImage":134,"status":19,"isPinned":20,"pinnedAt":21,"viewCount":135,"publishedAt":136,"createdAt":136,"updatedAt":137,"category":138,"author":139,"tags":140},"6d1d24b5-b8cf-4f27-b55f-64ffe553501a","Clipboard复制插件","clipboard复制插件","\n## 引用方法\n\n### 原生\n\n```js\n\u003Cscript src=\\\"https:\u002F\u002Fcdn.bootcdn.net\u002Fajax\u002Flibs\u002Fclipboard.js\u002F2.0.6\u002Fclipboard.min.js\\\">\u003C\u002Fscript>\n```\n\n### vue\n\n```shell\nnpm i -S clipboard \n```\n\n```shell\nimport ClipboardJS from \\\"clipboard\\\";\n```\n\n## 使用方法\n\n- 给目标元素添加一个 data-clipboard-target 属性\n- data-clipboard-action=\\\"cut\\\" 剪切内容，但是 cut 操作只在`\u003Cinput>` 或者 `\u003Ctextarea>` 元素上生效\n\n```html\n\u003Cdiv id = \\\"foo\\\"> 我被data - clipboard - target复制了 \u003C\u002Fdiv>\n\u003Ca\n  class=\\\"btn\\\"\n  href=\\\"javascript:\\\"\n  data-clipboard-target=\\\"#foo\\\"\n  rel=\\\"noopener noreferrer\\\"\n>\n  从\\\"foo\\\"复制文本\n\u003C\u002Fa>\n```\n\n### 通过属性复制文本\n\n> 仅需要给目标元素设置一个 data-clipboard-text 属性即可,从属性复制文本\n\n```html\n\u003Cbutton class = \\\"btn\\\" data-clipboard-text = \\\"我复制这个属性\\\">\u003C\u002Fbutton>\n```\n\n```js\n   \u002F\u002F初始化 clipboard 对象\n    var clipboard = new ClipboardJS('.btn');\n\n    \u002F\u002F成功\n    clipboard.on('success', (e) => {\n        console.info('Action:', e.action);\n        console.info('Text:', e.text);\n        console.info('Trigger:', e.trigger);\n        \u002F\u002F清除文本选中状态\n        e.clearSelection();\n    });\n\n    \u002F\u002F失败\n    clipboard.on('error', (e) => {\n        console.error('Action:', e.action);\n        console.error('Trigger:', e.trigger);\n    });\n```\n\n## 高级用法\n\n- 例如 如果想动态的设置一个目标元素`target`，则需要返回一个节点,即 动态设置点击复制的目标元素\n\n```js\nnew ClipboardJS('.btn', {\n    target: function(trigger) {\n        return trigger.nextElementSibling  ||  document.getElementById('name');\n    }\n});\n```\n\n- 如果想动态设置内容文本`text`，则返回一个字符串`String`\n\n```js\nnew ClipboardJS('.btn', {\n    text: function(trigger) {\n        return trigger.getAttribute('aria-label') || 'default text ';\n    }\n});\n```\n\n- vue的用法 \n\n```js\n  data() {\n   \u002F\u002F复制实例\n   clipboard: \\\"\\\"\n  },\n  mounted() {\n        \u002F\u002F初始化 clipboard 对象\n        this.clipboard = new ClipboardJS(\\\".btn\\\");\n  },\n  methods: {\n        copyurl() {\n            \u002F\u002F成功\n            this.clipboard.on(\\\"success\\\", e => {\n                this.$message({\n                    message: \\\"链接复制成功\\\",\n                    type: \\\"success\\\"\n                });\n\n                \u002F\u002F清除文本选中状态\n                e.clearSelection();\n                this.clipboard.destroy();\n                this.clipboard = new ClipboardJS(\\\".btn\\\");\n            });\n\n            \u002F\u002F失败\n            this.clipboard.on(\\\"error\\\", e => {\n                this.$message.error(\\\"链接复制失败\\\");\n            });\n        },\n  },\n destroyed() {\n        \u002F\u002F 销毁Clipboard实例，避免在其它页面或组件中实例化Clipboard后造成再次监听，产生重复回调\n        this.clipboard.destroy();\n }\n```\n\n## 常见问题\n\n### css3要是设置了user-select:none,复制和剪贴都不会生效\n\n- 可以清除这个样式\n- 直接用变量赋值\n\n### ios默认非点击标签没有点击效果\n\n- 把div 换成 button 就行了","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002F1658022842365.png",893,"2022-07-17T01:54:16.000Z","2026-05-24T20:25:33.000Z",{"id":4,"name":5,"slug":6},{"id":27,"name":28,"avatar":21},[141],{"id":142,"name":143,"slug":143},"19ac8998-7e0a-459b-9702-bb1adca70e8c","文本复制",{"id":145,"title":146,"slug":147,"content":148,"excerpt":146,"coverImage":149,"status":19,"isPinned":20,"pinnedAt":21,"viewCount":150,"publishedAt":151,"createdAt":151,"updatedAt":152,"category":153,"author":154,"tags":155},"f906171d-40c7-4674-af08-2a1fd7c0ab0b","Dayjs日期处理类库","dayjs日期处理类库","\n\n\n## 官方文档\n\n```http\nhttps:\u002F\u002Fdayjs.fenxianglu.cn\u002Fcategory\u002F#node-js\n```\n\n\n\n## 安装命令\n\n```shell\nnpm i dayjs -S\n```\n\n\n\n## 全局导入\n\n```js\nimport dayjs from \\\"dayjs\\\";\nVue.prototype.dayjs = dayjs;\n```\n\n\n\n## 所有可用解析标记的列表\n\n| **标识** | 示例        | **描述**           |\n| -------- | ----------- | ------------------ |\n| YY       | 18          | 年，两位数         |\n| YYYY     | 2018        | 年，四位数         |\n| M        | 1-12        | 月，从1开始        |\n| MM       | 01-12       | 月，两位数字       |\n| MMM      | Jan-Dec     | 月，英文缩写       |\n| D        | 1-31        | 日                 |\n| DD       | 01-31       | 日，两位数         |\n| H        | 0-23        | 24小时             |\n| HH       | 00-23       | 24小时，两位数     |\n| h        | 1-12        | 12小时             |\n| hh       | 01-12       | 12小时，两位数     |\n| m        | 0-59        | 分钟               |\n| mm       | 00-59       | 分钟，两位数       |\n| s        | 0-59        | 秒                 |\n| ss       | 00-59       | 秒，两位数         |\n| S        | 0-9         | 毫秒（百），一位数 |\n| SS       | 00-99       | 毫秒（十），两位数 |\n| SSS      | 000-999     | 毫秒，三位数       |\n| Z        | -05:00      | UTC偏移            |\n| ZZ       | -0500       | UTC偏移，两位数    |\n| A        | AM \u002F PM     | 上\u002F下午，大写      |\n| a        | am \u002F pm     | 上\u002F下午，小写      |\n| Do       | 1st... 31st | 月份的日期与序号   |\n\n\n\n## 常用\n\n>  时间戳-毫秒\n\n```js\ndayjs(1639638979*1000).format(\\\"YYYY-MM-DD HH:mm:ss\\\")    \u002F\u002F   2021-12-16 15:16:19\n```\n\n>  时间戳-秒\n\n```js\ndayjs.unix(1639638979).format(\\\"MM\u002FDD\u002FYYYY HH:mm:ss\\\")    \u002F\u002F   12\u002F16\u002F2021 15:16:19\n```\n\n> 时间文字格式   []表示转义，将字符放在方括号中，即可原样返回而不被格式化替换 (例如， `[MM]`)。\n\n```js\ndayjs.unix(1639638979).format(\\\"YYYY[年]MM[月]DD[日] HH[时]mm[分]ss[秒]\\\")   \u002F\u002F   2021年12月16日 15时16分19秒\n```\n\n\n\n\n\n## 基于公司项目二次封装\n\n```js\n\u002F*\n * @Author: 小白龙\n * @Date: 2022-01-26 16:28:58\n * @LastEditors: 小白龙\n * @LastEditTime: 2022-01-27 13:18:54\n * @Description: 基于公司常用项目时间格式化二次封装，默认时间戳为秒\n * @param {Number} time  时间戳\n * @param {String} format  时间格式\n *\u002F\n\nimport dayjs from \\\"dayjs\\\";\nexport default function formateDate(\n  time = Date.parse(new Date()) \u002F 1000,\n  format\n) {\n  const dayPrefix = dayjs.unix(time);\n  \u002F\u002F 判断是否自定义格式\n  if (typeof format === \\\"string\\\") return dayPrefix.format(format);\n\n  const appObject = {\n    hsing: \\\"MM月DD日 HH:mm\\\",\n    singnow: \\\"HH:mm DD[\u002F]MM\\\",\n    gosing: \\\"HH:mm MM[\u002F]DD\\\",\n    singstar: \\\"HH:mm DD[\u002F]MM\\\",\n  };\n\n  return dayPrefix.format(appObject[window._app || \\\"singnow\\\"]);\n}\n```\n\n","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002F1658022624982.png",738,"2022-07-17T01:50:50.000Z","2026-05-24T20:26:01.000Z",{"id":4,"name":5,"slug":6},{"id":27,"name":28,"avatar":21},[156],{"id":157,"name":158,"slug":158},"5ff33473-71a4-4e02-8c82-f9ea369a768f","时间",16,1,10,{"categories":163,"tags":243,"postCount":414,"tagCount":415,"hotPosts":416},[164,171,177,184,190,197,204,208,209,213,217,222,228,233,237],{"id":165,"name":166,"slug":167,"description":168,"sortOrder":169,"createdAt":170,"updatedAt":170,"postCount":160},"e8d0bd45-d10c-46d3-8afb-0c072df7f8a7","技术","tech","技术文章",0,"2026-06-27T04:18:37.371Z",{"id":172,"name":173,"slug":174,"description":173,"sortOrder":160,"createdAt":175,"updatedAt":9,"postCount":176},"15ac46ad-edf7-4435-9a64-ff78117d58c5","Vue3 生态","vue3-生态","2022-05-21T08:05:39.000Z",6,{"id":178,"name":179,"slug":180,"description":179,"sortOrder":181,"createdAt":182,"updatedAt":9,"postCount":183},"11d4d397-685c-4180-a7b3-9b0e3a1e411e","Css","css",2,"2022-05-23T07:19:37.000Z",9,{"id":185,"name":186,"slug":186,"description":186,"sortOrder":187,"createdAt":188,"updatedAt":9,"postCount":189},"d10456a5-e649-4741-a38f-f07f266ce5f2","开发环境",3,"2022-05-24T01:52:41.000Z",13,{"id":191,"name":192,"slug":193,"description":192,"sortOrder":194,"createdAt":195,"updatedAt":9,"postCount":196},"5ed5cc62-43ea-49a2-b0b2-38bc7aae52a0","Vue3","vue3",4,"2022-05-24T01:55:05.000Z",8,{"id":198,"name":199,"slug":200,"description":199,"sortOrder":201,"createdAt":202,"updatedAt":9,"postCount":203},"da130ba9-d4f4-49f3-aa0f-149078097ef0","JavaScript","javascript",5,"2022-05-24T02:22:57.000Z",18,{"id":205,"name":206,"slug":206,"description":206,"sortOrder":176,"createdAt":207,"updatedAt":9,"postCount":160},"d8cbe380-54b3-4a61-a12d-5438c2918574","限时优惠","2022-05-25T07:18:03.000Z",{"id":4,"name":5,"slug":6,"description":5,"sortOrder":7,"createdAt":8,"updatedAt":9,"postCount":159},{"id":210,"name":211,"slug":211,"description":211,"sortOrder":196,"createdAt":212,"updatedAt":9,"postCount":176},"4ea3d8af-9cc3-49bb-a9cd-34dbcdc3bd85","构建工具","2022-06-02T07:28:13.000Z",{"id":214,"name":215,"slug":215,"description":215,"sortOrder":183,"createdAt":216,"updatedAt":9,"postCount":196},"9ed9827c-9cbb-42da-80e4-d04c7fdba886","开发工具","2022-06-21T03:35:05.000Z",{"id":218,"name":219,"slug":220,"description":219,"sortOrder":161,"createdAt":221,"updatedAt":9,"postCount":201},"6b9179c3-17b2-43ff-a431-a03d6eb32d89","Vue2 生态","vue2-生态","2022-07-16T13:14:29.000Z",{"id":223,"name":224,"slug":225,"description":224,"sortOrder":226,"createdAt":227,"updatedAt":9,"postCount":201},"73a5f62c-3c47-45b9-9ae2-f29953ae8dc0","Node","node",11,"2022-07-16T13:15:39.000Z",{"id":229,"name":230,"slug":230,"description":230,"sortOrder":231,"createdAt":232,"updatedAt":9,"postCount":181},"2b696c16-48ef-403b-a88b-6e57cfc79596","开发问题",12,"2022-07-16T14:06:54.000Z",{"id":234,"name":235,"slug":235,"description":235,"sortOrder":189,"createdAt":236,"updatedAt":9,"postCount":160},"c0f0561e-a47a-4ecd-8caa-cc1df2315d57","算法","2022-07-16T14:22:34.000Z",{"id":238,"name":239,"slug":240,"description":239,"sortOrder":241,"createdAt":242,"updatedAt":9,"postCount":187},"a629c1f7-29f1-439e-be3c-29670b17ba20","Vue2","vue2",15,"2022-07-16T14:41:51.000Z",[244,250,255,258,263,268,269,273,278,283,288,293,296,301,305,309,314,319,324,329,333,337,341,343,347,352,357,360,363,366,369,370,372,375,377,379,382,384,387,389,391,394,397,399,402,404,408,411],{"id":245,"name":246,"slug":247,"createdAt":248,"updatedAt":249},"076bd8b9-293e-45cb-9dc3-e162007ca474","Axios","axios","2022-06-05T07:41:56.000Z","2025-12-30T07:26:21.000Z",{"id":251,"name":252,"slug":253,"createdAt":254,"updatedAt":9},"2aa7f6d0-1fac-4ed1-b9bb-f3afc813f42c","Axure","axure","2022-06-21T03:35:15.000Z",{"id":256,"name":179,"slug":180,"createdAt":257,"updatedAt":9},"b084ddd8-09be-4e57-98f0-cf4e376aecd7","2022-05-21T09:59:55.000Z",{"id":259,"name":260,"slug":261,"createdAt":262,"updatedAt":9},"78a62bff-ff77-4878-8c25-3e6aae18c668","Docker","docker","2022-07-16T14:34:37.000Z",{"id":264,"name":265,"slug":266,"createdAt":267,"updatedAt":9},"2de16806-ef3f-4e54-a259-d1e1e182468c","Git","git","2022-07-16T14:25:15.000Z",{"id":31,"name":32,"slug":33,"createdAt":257,"updatedAt":9},{"id":270,"name":271,"slug":272,"createdAt":257,"updatedAt":9},"5086e93c-23b9-43d3-9643-cc87f0e9ee94","JenKins","jenkins",{"id":274,"name":275,"slug":276,"createdAt":277,"updatedAt":9},"b73007a8-bb5c-42a8-9fd9-163033a5b45d","Linux","linux","2022-07-16T14:40:17.000Z",{"id":279,"name":280,"slug":281,"createdAt":282,"updatedAt":9},"0b658b92-dd6b-4db3-a398-9f6d69950a02","Markdown","markdown","2022-07-16T14:39:25.000Z",{"id":284,"name":285,"slug":286,"createdAt":287,"updatedAt":9},"ab034d3a-6e5b-4db5-a2dc-faf4ccbb63f5","Nest","nest","2022-07-16T13:15:49.000Z",{"id":289,"name":290,"slug":291,"createdAt":292,"updatedAt":9},"52c41978-da06-4962-9636-45bbaeedda80","Nginx","nginx","2022-05-21T09:59:56.000Z",{"id":294,"name":295,"slug":295,"createdAt":292,"updatedAt":9},"0f1cc678-40e4-44b1-b2cf-a6fd8a1c867a","npm",{"id":297,"name":298,"slug":299,"createdAt":300,"updatedAt":9},"a4370d78-70e1-4073-a8f6-3dc5d81fd8fd","Nuxt","nuxt","2022-06-01T13:07:07.000Z",{"id":302,"name":303,"slug":304,"createdAt":257,"updatedAt":9},"d232e01f-048e-4151-8a0a-fff9561f946f","Pinia","pinia",{"id":306,"name":307,"slug":308,"createdAt":292,"updatedAt":9},"14e9ab02-b0bb-4c85-8604-fe6f1f0f33cd","Pnpm","pnpm",{"id":310,"name":311,"slug":312,"createdAt":313,"updatedAt":313},"399d1d38-cc0d-43ce-8baf-c769447a2ebd","React生态","react生态","2023-02-21T02:03:09.000Z",{"id":315,"name":316,"slug":317,"createdAt":318,"updatedAt":9},"c95bbe84-bdd0-410a-86a9-e87958c55f4f","Redis","redis","2022-10-05T05:14:14.000Z",{"id":320,"name":321,"slug":322,"createdAt":323,"updatedAt":9},"6d05f9df-e116-450f-af57-85ed710c4870","Swiper","swiper","2022-06-01T14:08:46.000Z",{"id":325,"name":326,"slug":327,"createdAt":328,"updatedAt":9},"66f3aeb0-84ef-45f6-a43a-944eefc9895a","Vite","vite","2022-06-02T07:28:24.000Z",{"id":330,"name":331,"slug":332,"createdAt":257,"updatedAt":9},"bf5b94d3-090b-4098-a03c-4bc69781fb2d","Vue","vue",{"id":334,"name":335,"slug":336,"createdAt":292,"updatedAt":9},"2f7fb1be-b9c5-4606-b54f-e9f66f2653b2","Vue-Router","vue-router",{"id":338,"name":339,"slug":340,"createdAt":257,"updatedAt":9},"2fef3b91-1c1c-4ae8-b2c1-0e04b4f9b3a2","Vue2生态","vue2生态",{"id":342,"name":192,"slug":193,"createdAt":257,"updatedAt":9},"62b94c93-724f-488d-8fc5-0449971d9204",{"id":344,"name":345,"slug":346,"createdAt":257,"updatedAt":9},"20bff9cd-7848-4c16-8775-42cf12b44b30","Vue3生态","vue3生态",{"id":348,"name":349,"slug":350,"createdAt":351,"updatedAt":9},"c807b2c6-cb12-4409-a1f1-6bea9f330a6b","Vuex","vuex","2022-07-16T13:14:59.000Z",{"id":353,"name":354,"slug":355,"createdAt":356,"updatedAt":9},"5782dff5-2ea2-4427-9696-d4363a7fd5bc","Webpack","webpack","2022-07-16T14:33:41.000Z",{"id":358,"name":359,"slug":359,"createdAt":257,"updatedAt":9},"d0aa41f4-68f8-48d4-a4ed-3a503ea90451","下载",{"id":361,"name":362,"slug":362,"createdAt":257,"updatedAt":9},"a046060c-39ef-474a-8c85-2546aca0e2e5","代码片段",{"id":364,"name":365,"slug":365,"createdAt":257,"updatedAt":9},"fee73435-b2be-4b55-85b1-d133ea96aea4","伪元素",{"id":367,"name":368,"slug":368,"createdAt":257,"updatedAt":9},"436bd369-8c57-4869-8827-e88e50e5e0ab","伪类",{"id":84,"name":85,"slug":85,"createdAt":257,"updatedAt":9},{"id":113,"name":114,"slug":114,"createdAt":371,"updatedAt":9},"2022-08-02T00:37:47.000Z",{"id":373,"name":374,"slug":374,"createdAt":292,"updatedAt":9},"512b16fb-576a-4397-a7c5-dd20e6a8f9ca","布局",{"id":376,"name":215,"slug":215,"createdAt":257,"updatedAt":9},"f32faa96-f2ec-45c6-9a17-2c76062edcb0",{"id":378,"name":186,"slug":186,"createdAt":257,"updatedAt":9},"3c46ed3f-6d6b-4f91-bcb3-af5112860bf5",{"id":380,"name":381,"slug":381,"createdAt":257,"updatedAt":9},"dbfc086a-73a6-4560-814d-593acb61cf98","性能优化",{"id":127,"name":128,"slug":128,"createdAt":383,"updatedAt":9},"2022-07-28T12:39:13.000Z",{"id":385,"name":386,"slug":386,"createdAt":292,"updatedAt":9},"9a74300d-06f7-46d0-80d9-8fe67ec0539b","数组",{"id":142,"name":143,"slug":143,"createdAt":388,"updatedAt":9},"2022-07-17T01:54:45.000Z",{"id":157,"name":158,"slug":158,"createdAt":390,"updatedAt":9},"2022-07-17T01:51:12.000Z",{"id":392,"name":393,"slug":393,"createdAt":257,"updatedAt":9},"aa47ca4d-d3f6-4cac-b495-2c67c9592c36","最新优惠",{"id":395,"name":396,"slug":396,"createdAt":292,"updatedAt":9},"f6766d54-54fc-405e-932d-b7d550559125","服务器",{"id":398,"name":211,"slug":211,"createdAt":257,"updatedAt":9},"d856559a-03ff-40b4-980d-3f272b998c3c",{"id":400,"name":401,"slug":401,"createdAt":257,"updatedAt":9},"692d5d68-b188-4e5c-aca8-65d0229399a1","渐变",{"id":403,"name":235,"slug":235,"createdAt":257,"updatedAt":9},"38e1fd6b-d7c6-4d62-bf70-7bacc175bea9",{"id":405,"name":406,"slug":406,"createdAt":407,"updatedAt":9},"be7b10bc-49eb-4a03-bea7-ceb915d500fe","规范","2022-07-16T14:41:06.000Z",{"id":409,"name":410,"slug":410,"createdAt":292,"updatedAt":9},"b42e2916-ad62-4b8a-a863-cd8c19a829de","面试",{"id":412,"name":413,"slug":413,"createdAt":292,"updatedAt":9},"7069add9-b636-44f1-9cd4-ea3a6d2b85d3","面试题",104,48,[417,429,441,454,467],{"id":418,"title":419,"slug":419,"content":420,"excerpt":419,"coverImage":421,"status":19,"isPinned":20,"pinnedAt":21,"viewCount":422,"publishedAt":423,"createdAt":423,"updatedAt":424,"category":425,"author":426,"tags":427},"a1bd1f49-6f6d-4fea-9789-b5636e19a6b3","uni-app瀑布流","## 实现思路\n> 获取父组件的列表数组，watch监听数组长度变化，截取后面新的数据，创建两个左右数组，比较左右dom的长度，哪个短，就push一条数据进去，源数组删除一条数据。利用img的load（加载成功）和error方法（加载失败），触发数组的push，实现瀑布流\n\n## 代码实现\n\n```language\n\u003Ctemplate>\n    \u003Cview class=\\\"waterfall\\\">\n        \u003Cview class=\\\"waterfall_left\\\">\n            \u003Cview class=\\\"waterfall_list\\\" v-for=\\\"(item, index) in leftList\\\" :key=\\\"index\\\">\n                \u003CSearch\n                    :name=\\\"item.name\\\"\n                    :image=\\\"item.src\\\"\n                    :label=\\\"item.label\\\"\n                    :item=\\\"item\\\"\n                    @considerPush=\\\"considerPush\\\"\n                >\n                \u003C\u002FSearch>\n            \u003C\u002Fview>\n        \u003C\u002Fview>\n        \u003Cview class=\\\"waterfall_right\\\">\n            \u003Cview class=\\\"waterfall_list\\\" v-for=\\\"(item, index) in rightList\\\" :key=\\\"index\\\">\n                \u003CSearch\n                    :name=\\\"item.name\\\"\n                    :image=\\\"item.src\\\"\n                    :label=\\\"item.label\\\"\n                    :item=\\\"item\\\"\n                    @considerPush=\\\"considerPush\\\"\n                >\n                \u003C\u002FSearch>\n            \u003C\u002Fview>\n        \u003C\u002Fview>\n    \u003C\u002Fview>\n\u003C\u002Ftemplate>\n\n\u003Cscript>\nimport Search from '@\u002Fcomponents\u002Fsearch\u002Fsearch.vue';\nexport default {\n    components: { Search },\n    props: {\n        list: {\n            type: Array,\n            default: () => []\n        }\n    },\n    data() {\n        return {\n            \u002F\u002F 左侧列表\n            leftList: [],\n            \u002F\u002F 右侧列表\n            rightList: [],\n            \u002F\u002F 组件数据备份\n            newList: [],\n            \u002F\u002F默认请求数,主要为了正常排序\n            interceptNumber: 10\n        };\n    },\n    created() {\n        this.touchOff(); \u002F\u002F 触发排列\n    },\n    mounted() {},\n    watch: {\n        list(newValue, oldValue) {\n            this.interceptNumber = newValue.length - oldValue.length;\n            this.touchOff();\n        }\n    },\n    computed: {},\n    methods: {\n        \u002F\u002F 触发重新排列\n        touchOff() {\n            this.newList = [...this.list.slice(-this.interceptNumber)];\n            if (this.newList.length !== 0) {\n                this.leftList.push(this.newList.shift()); \u002F\u002F触发排列\n            }\n        },\n        \u002F\u002F 计算排列\n        considerPush() {\n            this.$nextTick(() => {\n                if (this.newList.length == 0) return; \u002F\u002F没有数据了\n                let leftH = 0;\n                let rightH = 0; \u002F\u002F左右高度\n                let query = uni.createSelectorQuery().in(this);\n                query.selectAll('.waterfall_left').boundingClientRect();\n                query.selectAll('.waterfall_right').boundingClientRect();\n                query.exec(res => {\n                    leftH = res[0].length != 0 ? res[0][0].height : 0; \u002F\u002F防止查询不到做个处理\n                    rightH = res[1].length != 0 ? res[1][0].height : 0;\n                    if (leftH == rightH || leftH \u003C rightH) {\n                        \u002F\u002F 相等 || 左边小\n                        this.leftList.push(this.newList.shift());\n                    } else {\n                        \u002F\u002F 右边小\n                        this.rightList.push(this.newList.shift());\n                    }\n\n                    \u002F\u002F console.log('左右高度：', leftH, rightH, leftH == rightH || leftH \u003C rightH);\n                });\n            });\n        }\n    }\n};\n\u003C\u002Fscript>\n\n\u003Cstyle scoped lang=\\\"scss\\\">\n.waterfall {\n    display: flex;\n    align-items: flex-start;\n    justify-content: flex-start;\n    .waterfall_left {\n        flex: 1;\n    }\n\n    .waterfall_right {\n        flex: 1;\n    }\n}\n\u003C\u002Fstyle>\n\n```\n","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002F7d4fa8d1d2775177b882a4656e3a5ed5.png",1817,"2022-11-22T07:54:41.000Z","2026-06-26T10:25:05.932Z",{"id":198,"name":199,"slug":200},{"id":27,"name":28,"avatar":21},[428],{"id":31,"name":32,"slug":33},{"id":430,"title":431,"slug":431,"content":432,"excerpt":431,"coverImage":433,"status":19,"isPinned":20,"pinnedAt":21,"viewCount":434,"publishedAt":435,"createdAt":435,"updatedAt":436,"category":437,"author":438,"tags":439},"b2c46bf6-d971-4cce-b21b-052dbea8e8a2","v-html使用img点击实现放大效果","## 代码实现\n```js\n\u002F**\n * JS获取html代码中所有的图片地址\n * @param htmlstr\n * @returns arr 数组\n *\u002F\n\nexport function getimgsrc(htmlstr) {\n    let reg = \u002F\u003Cimg.+?src=('|\\\")?([^'\\\"]+)('|\\\")?(?:\\s+|>)\u002Fg;\n    let arr = [];\n    let tem = 0;\n    \u002F\u002Feslint-disable-next-line\n    while ((tem = reg.exec(htmlstr))) {\n        arr.push(tem[2]); \u002F\u002F eslint-disable-line\n    }\n\n    return arr;\n}\n\n```\n\n\n\n```vue\n\u003Ctemplate>\n    \u003Cdiv class=\\\"image-expansion\\\" :class=\\\"classArr\\\">\n        \u003Cdiv @click.stop=\\\"hanldeImage($event)\\\" v-html=\\\"formatHtmlData\\\">\u003C\u002Fdiv>\n\n        \u003Cel-image-viewer\n            v-if=\\\"imgPreviewUrl\\\"\n            :initial-index=\\\"subscript\\\"\n            :src=\\\"imgPreviewUrl\\\"\n            :on-close=\\\"closeViewer\\\"\n            :url-list=\\\"imgList\\\"\n        >\u003C\u002Fel-image-viewer>\n    \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript>\nimport { getimgsrc } from '..\u002F..\u002Futils\u002Fgetimgsrc';\nimport ElImageViewer from 'element-ui\u002Fpackages\u002Fimage\u002Fsrc\u002Fimage-viewer';\nexport default {\n    components: {\n        ElImageViewer\n    },\n    props: {\n        htmlData: {\n            type: String,\n            default: () => {\n                return '';\n            }\n        },\n        classArr: {\n            type: Array,\n            default: () => {\n                return ['min'];\n            }\n        },\n\n        isArticle: {\n            type: Boolean,\n            default: () => {\n                return false;\n            }\n        }\n    },\n    data() {\n        return {\n            imgList: [],\n            formatHtmlData: '',\n            imgPreviewUrl: '',\n            subscript: 0\n        };\n    },\n    computed: {},\n\n    watch: {\n        \u002F\u002F监听数据，防止数据不更新\n        htmlData: {\n            handler(newName, oldName) {\n                \u002F\u002F判断是否为文章\n                if (this.isArticle) {\n                    newName ? (this.formatHtmlData = newName.replace(\u002F(\u003C([^>]+)>)\u002Fgi, '').replace(\u002F[\\\r\\\n]\u002Fg, '')) : '';\n                } else {\n                    \u002F\u002F剔除strong和p标签\n                    newName ? (this.formatHtmlData = newName.replace(\u002F(\u003C\\\u002F?strong.*?>)|(\u003C\\\u002F?p.*?>)\u002Fg, '')) : '';\n\n                    \u002F\u002F获取html全部图片，push成图片数组\n                    this.imgList = Object.values(getimgsrc(this.formatHtmlData));\n                    \u002F\u002F获取图片下标\n                    let subscript = this.imgList.indexOf(this.imgPreviewUrl);\n                    this.subscript = subscript > -1 ? subscript : 0;\n                }\n            },\n\n            immediate: true\n        }\n    },\n\n    mounted() {},\n\n    methods: {\n        \u002F\u002F监听点击事件\n        hanldeImage(event) {\n            if (event.target.nodeName === 'IMG' || event.target.nodeName === 'img') {\n                \u002F\u002F获取点击的图片url,decodeURIComponent转码一下，防禁url转码\n                this.imgPreviewUrl = decodeURIComponent(event.target.currentSrc);\n\n                \u002F\u002F获取图片下标\n                let subscript = this.imgList.indexOf(this.imgPreviewUrl);\n                this.subscript = subscript > -1 ? subscript : 0;\n\n                \u002F\u002F禁止遮罩层后面的内容滚动\n                document.documentElement.style.overflowY = 'hidden';\n            } else {\n                this.$emit('goDetail');\n            }\n        },\n\n        \u002F\u002F关闭弹框\n        closeViewer() {\n            this.imgPreviewUrl = '';\n            \u002F\u002F恢复遮罩层后面的内容滚动\n            document.documentElement.style.overflowY = 'auto';\n        }\n    }\n};\n\u003C\u002Fscript>\n\n\u003Cstyle lang=\\\"scss\\\" scoped>\n.image-expansion {\n}\n\n.min {\n    \u002Fdeep\u002F img {\n        cursor: pointer;\n        height: 28px;\n        padding: 0 10px 3px;\n    }\n}\n\n.max {\n    \u002Fdeep\u002F img {\n        cursor: pointer;\n    }\n}\n\n.class1 {\n    \u002Fdeep\u002F div {\n        font-size: 15px;\n        font-family: Microsoft YaHei;\n        font-weight: 400;\n        color: #888888;\n        line-height: 30px;\n    }\n}\n\u003C\u002Fstyle>\n\n\n```\n","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002Fc77c3fb113d1ab2f67e7afba1ca33b95.png",1571,"2023-01-10T07:22:29.000Z","2026-06-26T10:25:07.650Z",{"id":218,"name":219,"slug":220},{"id":27,"name":28,"avatar":21},[440],{"id":113,"name":114,"slug":114},{"id":442,"title":443,"slug":444,"content":445,"excerpt":443,"coverImage":446,"status":19,"isPinned":20,"pinnedAt":21,"viewCount":447,"publishedAt":448,"createdAt":448,"updatedAt":449,"category":450,"author":451,"tags":452},"9f33b49b-0785-4404-9a4e-d85937e7fc92","在 vue3 中优雅的使用 jsx\u002Ftsx","在-vue3-中优雅的使用-jsx-tsx","## 安装插件（@vitejs\u002Fplugin-vue-jsx）\n```shell\npnpm add @vitejs\u002Fplugin-vue-jsx -D\n```\n\n## 配置vite.config.ts\n```ts\nimport vueJsx from \\\"@vitejs\u002Fplugin-vue-jsx\\\";\n\nexport default defineConfig({\n  plugins: [\n    vueJsx(),\n  ]\n})\n```\n\n## 插值\n```language\n\u002F\u002F vue3模板语法\n\u003Cspan>{{ a + b }}\u003C\u002Fspan>\n\n\u002F\u002F jsx\u002Ftsx\n\u003Cspan>{ a + b }\u003C\u002Fspan>\n```\n\n\n## class与style 绑定\n\n```ts\n\u002F\u002F 模板字符串\n\u003Cdiv className={`header ${ isBg ? 'headerBg' : '' }`}>header\u003C\u002Fdiv>\n\u002F\u002F数组\n\u003Cdiv class={ [ 'header', isBg && 'headerBg' ] } >header\u003C\u002Fdiv>\n```\n\n```ts\nconst color = 'red'\nconst element = \u003Csapn style={{ color, fontSize: '16px' }}>style\u003C\u002Fsapn>\n```\n\n## 条件渲染\n\n```ts\n   setup() {\n       const isShow = false\n       const element = () => {\n           if (isShow) {\n               return \u003Cspan>我是if\u003C\u002Fspan>\n           } else {\n               return \u003Cspan>我是else\u003C\u002Fspan>\n           }\n       }\n       return () => (\n           \u003Cdiv>\n               \u003Cspan v-show={isShow}>我是v-show\u003C\u002Fspan>\n               {\n                   element()\n               }\n               {\n                   isShow ? \u003Cp>我是三目1\u003C\u002Fp> : \u003Cp>我是三目2\u003C\u002Fp>\n               }\n           \u003Cdiv>\n       )\n   }\n```\n\n## 列表渲染\n```language\nsetup() {\n   const listData = [\n       {name: 'Tom', age: 18},\n       {name: 'Jim', age: 20},\n       {name: 'Lucy', age: 16}\n   ]\n   return () => (\n       \u003Cdiv>\n           \u003Cdiv class={'box'}>\n               \u003Cspan>姓名\u003C\u002Fspan>\n               \u003Cspan>年龄\u003C\u002Fspan>\n           \u003C\u002Fdiv>\n           {\n               prop.listData.map(item => {\n                   return \u003Cdiv class={'box'}>\n                       \u003Cspan>{item.name}\u003C\u002Fspan>\n                       \u003Cspan>{item.age}\u003C\u002Fspan>\n                   \u003C\u002Fdiv>\n               })\n           }\n       \u003C\u002Fdiv>\n   )\n}\n\n```\n\n## 事件处理\n\n```language\nsetup() {\n    const clickBox = val => {\n        console.log(val)\n    }\n    return () => (\n        \u003Cdiv class={'box1'} onClick={() => clickBox('box1')}>\n            \u003Cspan>我是box1\u003C\u002Fspan>\n            \u003Cdiv class={'box2'} onClick={() => clickBox('box2')}>\n                \u003Cspan>我是box2\u003C\u002Fspan>\n                \u003Cdiv class={'box3'} onClick={withModifiers(() => clickBox('box3'), ['stop'])}>我是box3\u003C\u002Fdiv>\n            \u003C\u002Fdiv>\n        \u003C\u002Fdiv>\n    )\n}\n```\n\n## v-model\n\n```ts\n\u002F\u002F 正常写法\n\u003Cinput v-model=\\\"value\\\" \u002F> \u002F\u002F vue\n\u003Cinput v-model={value} \u002F> \u002F\u002F jsx\n\n\u002F\u002F 指定绑定值写法\n\u003Cinput v-model:modelValue=\\\"value\\\" \u002F> \u002F\u002F vue\n\u003Cinput v-model={[value,'modelValue']} \u002F> \u002F\u002F jsx\n\n\u002F\u002F 修饰符写法\n\u003Cinput v-model:modelValue.trim=\\\"value\\\" \u002F> \u002F\u002F vue\n\u003Cinput v-model={[value,'modelValue',['trim']]} \u002F> \u002F\u002F jsx\n```\n\n## slot插槽\n\n### 定义插槽\n```ts\nimport { renderSlot } from \\\"vue\\\"\nexport default defineComponent({\n    \u002F\u002F 从ctx中解构出来 slots\n    setup(props, { slots }) {\n        return () => (\n            \u003Cdiv>\n                { renderSlot(slots, 'default') }\n                { slots.title?.() }\n            \u003C\u002Fdiv>\n        )\n    }\n})\n\n```\n\n### 使用插槽\n\n```ts\nimport Vslot from '.\u002FslotTem'\nexport default defineComponent({\n    setup() {\n        return () => (\n            \u003Cdiv class={'box'}>\n                \u003CVslot v-slots={{\n                    title: () => {\n                        return \u003Cp>我是title插槽\u003C\u002Fp>\n                    },\n                    default: () => {\n                        return \u003Cp>我是default插槽\u003C\u002Fp>\n                    }\n                }} \u002F>\n            \u003C\u002Fdiv>\n        )\n    }\n})\n\n```\n\n","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002F3e4cb0d3df611ee3f57b8ed503e1015e.png",1391,"2023-04-19T09:23:27.000Z","2026-06-26T09:54:47.369Z",{"id":191,"name":192,"slug":193},{"id":27,"name":28,"avatar":21},[453],{"id":342,"name":192,"slug":193},{"id":455,"title":456,"slug":457,"content":458,"excerpt":456,"coverImage":459,"status":19,"isPinned":20,"pinnedAt":21,"viewCount":460,"publishedAt":461,"createdAt":461,"updatedAt":462,"category":463,"author":464,"tags":465},"1dcdd755-0e46-45a3-8998-9a213fd3fcd5","Vue3 导入导出Excel","vue3-导入导出excel","## 安装\n```shell\npnpm add -S XLSX\n```\n\n\n## 方法封装\n```js\nimport * as XLSX from 'xlsx';\n\n\u002F\u002F参数说明\n\u002F\u002Fconfiguration: {\n\u002F\u002F  data: [], \u002F\u002F 导出的数据\n\u002F\u002F  head: {}, \u002F\u002F 导出的数据对应的表头\n\u002F\u002F  name: '', \u002F\u002F 导出的文件名\n\u002F\u002F  label: '', \u002F\u002F 导出的表单名\n\u002F\u002F  widthArr: [], \u002F\u002F 导出的表单列宽\n\u002F\u002F}\n\n\u002F\u002F 导出excel\nexport const ExportXlsx = (configuration) => {\n  const { data, head, name, label, widthArr } = configuration;\n\n  const list = data.map((item) => {\n    const obj = {};\n    for (const k in item) {\n      if (head[k]) {\n        obj[head[k]] = item[k];\n      }\n    }\n    return obj;\n  });\n\n  \u002F\u002F 创建工作表\n  const xLSXData = XLSX.utils.json_to_sheet(list);\n  \u002F\u002F 创建工作簿\n  const wb = XLSX.utils.book_new();\n  \u002F\u002F 将工作表放入工作簿中\n  XLSX.utils.book_append_sheet(wb, xLSXData, label);\n  xLSXData['!cols'] = [];\n  \u002F\u002F 设置列宽\n  widthArr.forEach((item) => {\n    xLSXData['!cols'].push({ wpx: item });\n  });\n\n  \u002F\u002F 生成文件并下载\n  XLSX.writeFile(wb, `${name}.xlsx`);\n};\n\n\u002F\u002F 导入excel\nexport const ImportXlsx = (e) => {\n  const file = e.target.files[0];\n  const reader = new FileReader();\n  reader.readAsArrayBuffer(file);\n  reader.onload = (e) => {\n    const data = e.target.result;\n    const workbook = XLSX.read(data, { type: 'binary', cellDates: true });\n    const wsname = workbook.SheetNames[0];\n    const outdata = XLSX.utils.sheet_to_json(workbook.Sheets[wsname]);\n    console.log(outdata);\n  };\n};\n\n```\n","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002F80231a326d3846b895a7d46c99e738ce.png",1334,"2023-03-14T06:18:59.000Z","2026-06-26T09:54:47.798Z",{"id":198,"name":199,"slug":200},{"id":27,"name":28,"avatar":21},[466],{"id":31,"name":32,"slug":33},{"id":468,"title":469,"slug":470,"content":471,"excerpt":469,"coverImage":472,"status":19,"isPinned":20,"pinnedAt":21,"viewCount":473,"publishedAt":474,"createdAt":474,"updatedAt":475,"category":476,"author":480,"tags":481},"80920598-a452-4357-bf53-842b200560e8","React18入门到精通教程","react18入门到精通教程","\n## JSX实现列表渲染\n> 使用`map()`方法遍历数组，必须添加`key`属性提高性能\n```jsx\nconst songs = [\n  { id: 1, name: \\\"helo1\\\" },\n  { id: 2, name: \\\"helo2\\\" },\n  { id: 3, name: \\\"helo3\\\" },\n];\n\nfunction App() {\n  return (\n    \u003Cdiv>\n      \u003Cul>\n        {songs.map((song) => (\n          \u002F\u002F 关键：添加唯一key标识符（避免使用索引）\n          \u003Cli key={song.id}>  \n            {song.id}-{song.name}\n          \u003C\u002Fli>\n        ))}\n      \u003C\u002Ful>\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n**最佳实践：**\n1. 使用`\u003Cul>`包裹列表项\n2. `key`应使用稳定唯一标识（如ID），避免数组索引\n3. 空列表处理：`{songs.length > 0 && ...}` 或 `{songs.map(...) || \u003CEmptyView\u002F>}`\n\n## JSX实现条件渲染\n### 简单逻辑\n```jsx\n\u002F\u002F 三元表达式\n{isLoggedIn ? \u003CDashboard \u002F> : \u003CLoginForm \u002F>}\n\n\u002F\u002F 逻辑短路\n{hasNotification && \u003CNotificationBadge count={5} \u002F>}\n\n\u002F\u002F 空值处理\n{userProfile?.avatar || \u003CDefaultAvatar \u002F>}\n```\n\n### 复杂逻辑\n```jsx\nconst renderContent = (type) => {\n  switch(type) {\n    case 'success': \n      return \u003CSuccessAlert \u002F>;\n    case 'error':\n      return \u003CErrorAlert \u002F>;\n    default:\n      return \u003CInfoAlert \u002F>;\n  }\n}\n\nfunction App() {\n  return (\n    \u003Cdiv className=\\\"container\\\">\n      {renderContent(status)}\n      \n      {\u002F* 另一种模式：立即执行函数 *\u002F}\n      {(() => {\n        if (isLoading) return \u003CSpinner \u002F>;\n        if (isEmpty) return \u003CEmptyState \u002F>;\n        return \u003CDataTable \u002F>;\n      })()}\n    \u003C\u002Fdiv>\n  )\n}\n```\n\n## JSX样式处理\n### 行内样式\n```jsx\n\u002F\u002F 直接对象\n\u003Cdiv style={{ \n  color: 'white', \n  backgroundColor: 'teal',\n  padding: '1rem'\n}}>\n\n\u002F\u002F 样式对象复用\nconst alertStyle = {\n  padding: '15px',\n  borderRadius: '4px',\n  margin: '10px 0'\n};\n\nfunction Alert({ type }) {\n  return (\n    \u003Cdiv style={{\n      ...alertStyle,  \u002F\u002F 扩展运算符合并样式\n      background: type === 'error' ? '#f8d7da' : '#d4edda'\n    }}>\n      {message}\n    \u003C\u002Fdiv>\n  )\n}\n```\n\n### 类名控制（推荐）\n```css\n\u002F* styles.module.css *\u002F\n.card {\n  border: 1px solid #ddd;\n  border-radius: 8px;\n  padding: 20px;\n  box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n}\n\n.highlight {\n  background-color: #ffffe0;\n}\n```\n\n```jsx\nimport styles from '.\u002Fstyles.module.css';\n\nfunction ProductCard({ featured }) {\n  return (\n    \u003Cdiv className={`${styles.card} ${featured ? styles.highlight : ''}`}>\n      {\u002F* 内容 *\u002F}\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n## React 18新特性\n### 并发模式（Concurrent Mode）\n```jsx\nimport { startTransition } from 'react';\n\n\u002F\u002F 非紧急状态更新\nfunction handleSearch(query) {\n  startTransition(() => {\n    setSearchQuery(query); \u002F\u002F 可中断的渲染\n  });\n}\n```\n\n### 自动批处理（Automatic Batching）\n```jsx\n\u002F\u002F React 17及之前：两次渲染\n\u002F\u002F React 18：自动批处理，一次渲染\nfunction handleClick() {\n  setCount(c => c + 1);\n  setFlag(f => !f);\n}\n```\n\n## Redux状态管理（现代写法）\n### 安装依赖\n```bash\nnpm install @reduxjs\u002Ftoolkit react-redux\n```\n\n### 创建Store\n```js\n\u002F\u002F store.js\nimport { configureStore, createSlice } from '@reduxjs\u002Ftoolkit';\n\nconst counterSlice = createSlice({\n  name: 'counter',\n  initialState: { value: 0 },\n  reducers: {\n    increment: state => { state.value += 1 },\n    decrement: state => { state.value -= 1 },\n    incrementByAmount: (state, action) => {\n      state.value += action.payload\n    }\n  }\n});\n\n\u002F\u002F 异步操作示例\nexport const fetchUserData = () => async (dispatch) => {\n  const response = await fetch('\u002Fapi\u002Fuser');\n  dispatch(setUser(await response.json()));\n};\n\nexport const store = configureStore({\n  reducer: {\n    counter: counterSlice.reducer,\n    \u002F\u002F 其他reducer...\n  }\n});\n\nexport const { increment, decrement } = counterSlice.actions;\n```\n\n### 组件集成\n```jsx\n\u002F\u002F index.js\nimport { Provider } from 'react-redux';\nimport { store } from '.\u002Fstore';\n\nroot.render(\n  \u003CProvider store={store}>\n    \u003CApp \u002F>\n  \u003C\u002FProvider>\n);\n\n\u002F\u002F Counter.js\nimport { useSelector, useDispatch } from 'react-redux';\nimport { increment, decrement } from '.\u002Fstore';\n\nfunction Counter() {\n  const count = useSelector(state => state.counter.value);\n  const dispatch = useDispatch();\n\n  return (\n    \u003Cdiv>\n      \u003Cbutton onClick={() => dispatch(decrement())}>-\u003C\u002Fbutton>\n      \u003Cspan>{count}\u003C\u002Fspan>\n      \u003Cbutton onClick={() => dispatch(increment())}>+\u003C\u002Fbutton>\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n## 最佳实践总结\n1. **组件设计**：遵循单一职责原则，拆分智能组件（容器组件）和展示组件\n2. **状态管理**：\n   - 局部状态用`useState`\u002F`useReducer`\n   - 全局共享状态用Redux\n   - 避免过度使用状态提升\n3. **性能优化**：\n   - 使用`React.memo`记忆组件\n   - 使用`useCallback`\u002F`useMemo`避免不必要的重渲染\n   - 虚拟化长列表（react-window）\n4. **Hooks规范**：\n   - 避免在循环\u002F条件中使用Hook\n   - 自定义Hook以`use`前缀命名\n5. **TypeScript集成**：\n   ```tsx\n   interface UserCardProps {\n     name: string;\n     age: number;\n     onSelect: (id: string) => void;\n   }\n   \n   const UserCard: React.FC\u003CUserCardProps> = ({ name, age }) => (\n     \u003Cdiv>{name} ({age})\u003C\u002Fdiv>\n   )\n   ```\n\n下面为您完善教程，增加React路由和生命周期相关内容：\n\n## React路由管理（React Router v6）\n\n### 安装与基础配置\n```bash\nnpm install react-router-dom@6\n```\n\n### 路由基础结构\n```jsx\n\u002F\u002F index.js\nimport { BrowserRouter } from 'react-router-dom';\n\nconst root = ReactDOM.createRoot(document.getElementById('root'));\nroot.render(\n  \u003CBrowserRouter>\n    \u003CApp \u002F>\n  \u003C\u002FBrowserRouter>\n);\n\n\u002F\u002F App.js\nimport { Routes, Route, Link } from 'react-router-dom';\n\nfunction App() {\n  return (\n    \u003Cdiv>\n      \u003Cnav>\n        \u003CLink to=\\\"\u002F\\\">首页\u003C\u002FLink>\n        \u003CLink to=\\\"\u002Fabout\\\">关于\u003C\u002FLink>\n        \u003CLink to=\\\"\u002Fusers\\\">用户列表\u003C\u002FLink>\n      \u003C\u002Fnav>\n      \n      \u003CRoutes>\n        \u003CRoute path=\\\"\u002F\\\" element={\u003CHome \u002F>} \u002F>\n        \u003CRoute path=\\\"\u002Fabout\\\" element={\u003CAbout \u002F>} \u002F>\n        \u003CRoute path=\\\"\u002Fusers\\\" element={\u003CUserList \u002F>} \u002F>\n        \u003CRoute path=\\\"\u002Fusers\u002F:id\\\" element={\u003CUserDetail \u002F>} \u002F>\n        \u003CRoute path=\\\"*\\\" element={\u003CNotFound \u002F>} \u002F>\n      \u003C\u002FRoutes>\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n### 嵌套路由\n```jsx\n\u002F\u002F Dashboard.js\nimport { Outlet } from 'react-router-dom';\n\nfunction Dashboard() {\n  return (\n    \u003Cdiv>\n      \u003Ch2>仪表盘\u003C\u002Fh2>\n      \u003COutlet \u002F> {\u002F* 子路由渲染位置 *\u002F}\n    \u003C\u002Fdiv>\n  );\n}\n\n\u002F\u002F 路由配置\n\u003CRoutes>\n  \u003CRoute path=\\\"\u002Fdashboard\\\" element={\u003CDashboard \u002F>}>\n    \u003CRoute index element={\u003CDashboardHome \u002F>} \u002F>\n    \u003CRoute path=\\\"settings\\\" element={\u003CDashboardSettings \u002F>} \u002F>\n    \u003CRoute path=\\\"analytics\\\" element={\u003CDashboardAnalytics \u002F>} \u002F>\n  \u003C\u002FRoute>\n\u003C\u002FRoutes>\n```\n\n### 编程式导航\n```jsx\nimport { useNavigate, useParams, useLocation } from 'react-router-dom';\n\nfunction UserCard({ user }) {\n  const navigate = useNavigate();\n  \n  return (\n    \u003Cdiv onClick={() => navigate(`\u002Fusers\u002F${user.id}`)}>\n      {user.name}\n    \u003C\u002Fdiv>\n  );\n}\n\nfunction UserDetail() {\n  const { id } = useParams(); \u002F\u002F 获取URL参数\n  const location = useLocation(); \u002F\u002F 获取位置对象\n  \n  return (\n    \u003Cdiv>\n      \u003Ch2>用户ID: {id}\u003C\u002Fh2>\n      \u003Cp>当前路径: {location.pathname}\u003C\u002Fp>\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n### 路由守卫（认证保护）\n```jsx\nimport { Navigate } from 'react-router-dom';\n\nfunction ProtectedRoute({ children }) {\n  const { isAuthenticated } = useAuth();\n  \n  if (!isAuthenticated) {\n    return \u003CNavigate to=\\\"\u002Flogin\\\" replace \u002F>;\n  }\n  \n  return children;\n}\n\n\u002F\u002F 使用\n\u003CRoute \n  path=\\\"\u002Fdashboard\\\" \n  element={\n    \u003CProtectedRoute>\n      \u003CDashboard \u002F>\n    \u003C\u002FProtectedRoute>\n  } \n\u002F>\n```\n\n## React生命周期\n\n### 类组件生命周期方法\n\n```jsx\nclass LifecycleDemo extends React.Component {\n  \u002F\u002F 1. 初始化阶段\n  constructor(props) {\n    super(props);\n    this.state = { count: 0 };\n    console.log('Constructor');\n  }\n\n  \u002F\u002F 2. 挂载阶段\n  componentDidMount() {\n    console.log('Component did mount');\n    \u002F\u002F 适合进行API调用、事件订阅\n    this.timer = setInterval(() => {\n      this.setState(prev => ({ count: prev.count + 1 }));\n    }, 1000);\n  }\n\n  \u002F\u002F 3. 更新阶段\n  shouldComponentUpdate(nextProps, nextState) {\n    console.log('Should component update?');\n    return nextState.count !== this.state.count;\n  }\n\n  componentDidUpdate(prevProps, prevState) {\n    console.log('Component did update');\n  }\n\n  \u002F\u002F 4. 卸载阶段\n  componentWillUnmount() {\n    console.log('Component will unmount');\n    \u002F\u002F 清理操作\n    clearInterval(this.timer);\n  }\n\n  render() {\n    console.log('Render');\n    return \u003Cdiv>Count: {this.state.count}\u003C\u002Fdiv>;\n  }\n}\n```\n\n### 函数组件生命周期（Hooks实现）\n\n```jsx\nimport { useState, useEffect } from 'react';\n\nfunction FunctionLifecycle() {\n  const [count, setCount] = useState(0);\n  const [data, setData] = useState(null);\n\n  \u002F\u002F 相当于componentDidMount + componentDidUpdate\n  useEffect(() => {\n    console.log('每次渲染后执行');\n  });\n\n  \u002F\u002F 相当于componentDidMount\n  useEffect(() => {\n    console.log('组件挂载后执行');\n    \n    \u002F\u002F 数据获取\n    fetch('\u002Fapi\u002Fdata')\n      .then(res => res.json())\n      .then(setData);\n    \n    \u002F\u002F 相当于componentWillUnmount\n    return () => {\n      console.log('组件卸载前执行');\n    };\n  }, []); \u002F\u002F 空依赖数组\n\n  \u002F\u002F 依赖变化时执行\n  useEffect(() => {\n    console.log('count变化时执行:', count);\n    \n    document.title = `Count: ${count}`;\n    \n    return () => {\n      console.log('清理count效果');\n    };\n  }, [count]); \u002F\u002F count依赖\n\n  return (\n    \u003Cdiv>\n      \u003Cp>Count: {count}\u003C\u002Fp>\n      \u003Cbutton onClick={() => setCount(c => c + 1)}>增加\u003C\u002Fbutton>\n      {data && \u003Cpre>{JSON.stringify(data, null, 2)}\u003C\u002Fpre>}\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n### 生命周期阶段对比\n\n| 阶段 | 类组件方法 | 函数组件Hook |\n|------|------------|--------------|\n| **挂载** | constructor | useState初始化 |\n|       | render      | 函数体执行    |\n|       | componentDidMount | useEffect(() => {}, []) |\n| **更新** | shouldComponentUpdate | React.memo, useMemo |\n|       | render      | 函数体执行    |\n|       | componentDidUpdate | useEffect(() => {}) |\n| **卸载** | componentWillUnmount | useEffect返回函数 |\n| **错误处理** | componentDidCatch | 暂无直接等效，需错误边界组件 |\n\n### 现代React开发建议\n\n1. **优先使用函数组件+Hooks**：\n   - 90%的场景可替代类组件\n   - 更简洁的代码结构\n   - 更好的逻辑复用\n\n2. **关键生命周期替代**：\n   - `componentDidMount` → `useEffect(() => {}, [])`\n   - `componentDidUpdate` → `useEffect(() => {})` 或带依赖的 `useEffect`\n   - `componentWillUnmount` → `useEffect(() => { return cleanup }, [])`\n   - `shouldComponentUpdate` → `React.memo` 或 `useMemo`\n\n3. **数据获取最佳实践**：\n```jsx\nuseEffect(() => {\n  let isMounted = true;\n  \n  const fetchData = async () => {\n    try {\n      const result = await api.getData();\n      if (isMounted) setData(result);\n    } catch (error) {\n      if (isMounted) setError(error);\n    }\n  };\n  \n  fetchData();\n  \n  return () => {\n    isMounted = false; \u002F\u002F 避免组件卸载后设置状态\n  };\n}, []);\n```\n\n## 路由与生命周期整合示例\n\n```jsx\nfunction UserProfile() {\n  const { id } = useParams();\n  const [user, setUser] = useState(null);\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    let isActive = true;\n    \n    const fetchUser = async () => {\n      try {\n        setLoading(true);\n        const data = await fetchUserById(id);\n        if (isActive) {\n          setUser(data);\n          setLoading(false);\n        }\n      } catch (error) {\n        if (isActive) {\n          setError(error.message);\n          setLoading(false);\n        }\n      }\n    };\n    \n    fetchUser();\n    \n    return () => {\n      isActive = false; \u002F\u002F 清理效果\n    };\n  }, [id]); \u002F\u002F id变化时重新获取\n\n  if (loading) return \u003CSpinner \u002F>;\n  \n  return (\n    \u003Cdiv>\n      \u003Ch2>{user.name}\u003C\u002Fh2>\n      \u003Cp>Email: {user.email}\u003C\u002Fp>\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n## 完整项目结构建议\n\n```\nsrc\u002F\n├── components\u002F      # 通用UI组件\n├── pages\u002F           # 页面组件\n├── layouts\u002F         # 布局组件\n├── hooks\u002F           # 自定义Hooks\n├── store\u002F           # Redux状态\n│   ├── slices\u002F\n│   └── store.js\n├── services\u002F        # API服务\n├── routers\u002F         # 路由配置\n├── utils\u002F           # 工具函数\n├── assets\u002F          # 静态资源\n└── App.js           # 主应用组件\n```\n\n这些新增内容涵盖了React路由的现代用法（v6版本）以及React生命周期的详细解释，包括类组件和函数组件的实现方式对比。同时还提供了路由与生命周期整合的实际示例，帮助开发者理解如何在真实项目中应用这些概念。","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002F46e205aa1bd33d1bb7201019fc2fdf43.png",1147,"2023-02-21T02:01:52.000Z","2026-06-26T10:25:15.418Z",{"id":477,"name":478,"slug":479},"f1701085-b8c1-413a-8750-58e7a0a33832","React","react",{"id":27,"name":28,"avatar":21},[482],{"id":310,"name":311,"slug":312}]