\u003C/div>\n \u003Cdiv>\u003Cimg src=\"../assets//bilibili/cloud.webp\" />\u003C/div>\n \u003Cdiv>\u003Cimg src=\"../assets//bilibili/banner1.webp\" />\u003C/div>\n \u003Cdiv>\u003Cimg src=\"../assets//bilibili/banner3.webp\" />\u003C/div>\n \u003Cdiv>\u003Cimg src=\"../assets//bilibili/banner4.webp\" />\u003C/div>\n \u003Cdiv>\u003Cimg src=\"../assets//bilibili/banner5.webp\" />\u003C/div>\n \u003Cdiv>\u003Cimg src=\"../assets//bilibili/banner7.webp\" />\u003C/div>\n \u003Cdiv>\u003Cimg src=\"../assets//bilibili/banner8.webp\" />\u003C/div>\n \u003Cdiv>\u003Cimg class=\"car\" src=\"../assets//bilibili/car.webp\" />\u003C/div>\n \u003Cdiv>\u003Cimg class=\"person\" src=\"../assets//bilibili/characterSmall.webp\" />\u003C/div>\n \u003Cdiv>\u003Cimg src=\"../assets//bilibili/characterBig.webp\" />\u003C/div>\n \u003Cdiv>\u003Cimg src=\"../assets//bilibili/fence.webp\" />\u003C/div>\n \u003Cdiv>\u003Cimg src=\"../assets//bilibili/leftBottomGrass.webp\" />\u003C/div>\n \u003Cdiv>\u003Cimg src=\"../assets//bilibili/leftTopGrass.webp\" />\u003C/div>\n \u003Cdiv>\u003Cimg src=\"../assets//bilibili/rabbit.webp\" />\u003C/div>\n \u003Cdiv>\u003Cimg src=\"../assets//bilibili/banner2.webp\" />\u003C/div>\n \u003Cdiv>\u003Cimg src=\"../assets//bilibili/banner6.webp\" />\u003C/div>\n \u003Cdiv>\n \u003Cvideo loop autoplay muted playsinline>\n \u003Csource src=\"../assets/bilibili/video.webm\" type=\"video/webm\" />\n 您的浏览器不支持 video 标签。\n \u003C/video>\n \u003C/div>\n \u003Cdiv>\n \u003Ch1 class=\"title leading-tight font-bold mb-6\">{{ title }}\u003C/h1>\n \u003C/div>\n \u003C/header>\n\u003C/template>\n\n\u003Cscript lang=\"ts\" setup>\nimport { ref, onMounted, onBeforeUnmount } from 'vue';\n\ndefineProps({\n title: {\n type: String,\n default: '',\n },\n});\n\nconst headerRef = ref\u003CHTMLElement | null>(null);\nconst startX = ref(0);\n\nconst handleMouseEnter = (e: MouseEvent) => {\n startX.value = e.clientX; // 计算开始位移\n\n const images = document.querySelectorAll\u003CHTMLElement>('header > div > img');\n images.forEach(image => {\n // 移除在鼠标离开后添加的恢复图片原始位置的过渡\n image.classList.remove('smooth-transition');\n });\n};\n\nconst handleMouseMove = (e: MouseEvent) => {\n const images = document.querySelectorAll\u003CHTMLElement>('header > div > img');\n\n const percentage = (e.clientX - startX.value) / window.outerWidth; // 计算位移百分比\n let xOffset = percentage,\n yOffset = percentage;\n\n images.forEach(image => {\n xOffset *= 1.3;\n yOffset *= 1.1;\n\n // 设置元素的位移\n image.style.setProperty('--xOffset', `${xOffset}px`);\n image.style.setProperty('--yOffset', `${yOffset}px`);\n image.style.setProperty('--personXoffset', `${-250 + xOffset}px`);\n image.style.setProperty('--personYoffset', `${-30 + yOffset}px`);\n image.style.setProperty('--carXoffset', `${-100 + xOffset}px`);\n image.style.setProperty('--carYoffset', `${20 + yOffset}px`);\n });\n};\n\nconst handleMouseLeave = () => {\n const images = document.querySelectorAll\u003CHTMLElement>('header > div > img');\n images.forEach(image => {\n image.classList.add('smooth-transition');\n });\n images.forEach(image => {\n image.style.setProperty('--xOffset', `${0}px`);\n image.style.setProperty('--yOffset', `${0}px`);\n image.style.setProperty('--personXoffset', `${-250}px`);\n image.style.setProperty('--personYoffset', `${-30}px`);\n image.style.setProperty('--carXoffset', `${-100}px`);\n image.style.setProperty('--carYoffset', `${20}px`);\n });\n};\n\nonMounted(() => {\n if (headerRef.value) {\n headerRef.value.addEventListener('mouseenter', handleMouseEnter);\n headerRef.value.addEventListener('mousemove', handleMouseMove);\n headerRef.value.addEventListener('mouseleave', handleMouseLeave);\n }\n});\n\nonBeforeUnmount(() => {\n if (headerRef.value) {\n headerRef.value.removeEventListener('mouseenter', handleMouseEnter);\n headerRef.value.removeEventListener('mousemove', handleMouseMove);\n headerRef.value.removeEventListener('mouseleave', handleMouseLeave);\n }\n});\n\u003C/script>\n\n\u003Cstyle lang=\"scss\" scoped>\nheader {\n height: 155px;\n position: relative;\n overflow: hidden;\n\n &::after {\n content: '';\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 40%; // 遮罩高度可调整\n background: linear-gradient(to bottom, rgba(0, 0, 0, 0.5), transparent);\n pointer-events: none; // 确保不影响鼠标事件\n z-index: 10; // 确保遮罩在最上层\n }\n}\n\n.title {\n color: #fff;\n letter-spacing: 3px;\n}\n\nheader > div {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n display: flex;\n justify-content: center;\n align-items: center;\n\n --xOffset: 0px;\n --yOffset: 0px;\n --personXoffset: -250px;\n --personYoffset: -30px;\n --carXoffset: -100px;\n --carYoffset: 20px;\n}\n\nheader > div > img,\nheader > div > video {\n display: block;\n object-fit: cover;\n height: 100%;\n width: 100%;\n transform: translate(var(--xOffset), var(--yOffset)) scale(1.1);\n}\n\n.smooth-transition {\n transition: transform 0.3s ease-out !important;\n}\n\n.person {\n width: 75px;\n height: 60px;\n transform: translate(var(--personXoffset), var(--personYoffset)) !important;\n}\n\n.car {\n transform: translate(var(--carXoffset), var(--carYoffset)) !important;\n}\n\u003C/style>\n```\n",{"id":72,"createdAt":73,"updatedAt":73,"creator":4,"updater":4,"title":74,"description":75,"cover":76,"content":77,"top":19,"order":9,"status":20,"type":21,"contentType":22,"originalUrl":17},40,"2025-07-09T15:30:15.700Z","吐槽大会-Nuxt3","最近使用Nuxt3比较多,吐槽一下","https://nest-admin-1308002460.cos.ap-chengdu.myqcloud.com/1752164286209-Snipaste_2025-07-11_00-17-46.webp","\u003Cp>最近本人沉迷于 Nuxt3 博客的开发,有感而发。\u003C/p>\n\u003Cp>1、框架更新迭代太快,开发时会提示哪些框架版本过低,升级到指定版本后,很多时候控制台会报错,直接 500,我已经不敢升了。就拿 @Nuxtkit 来说,每次都给我提示升级到最新版本,升级后只要一修改 nuxt.config.ts 直接在控制台报错,卡死,太抽象了。\u003C/p>\n\u003Cp>2、nuxt3 文档很简略,好多 api 的参数、用途、示例没有写清楚,期待后续完善。\u003C/p>\n\u003Cp>3、本地运行没有问题,部署到线上首页直接显示打包时候的旧数据,还不知道怎么第一次加载页面的时候使用最新的数据,很无语,文档和AI都问透了,不是很理解。\u003C/p>\n\u003Cp> --- 呜呜呜,是我的问题,不小心把首页打包成静态页面了,我的错。\u003C/p>\n\u003Cp>4、sitemap 困扰了我很久,nuxt sitemap 的官方文档让我很难理解,也不知道为什么不收录,打算换其他的了。\u003C/p>\n\u003Cp>5、@nuxt/image 特别坑,本地需要搞一些魔法才能正常显示,部署到线上又不能显示。本地的话我的电脑是 Mac M1 Pro,看 github 的 issues 很多都有这个情况,有解决方式,不过很抽象;线上部署也有人遇到过问题,各种离谱的解决方案,太搞心态了,气得我我直接使用 原生的 img 了。\u003C/p>\n\u003Cp> \u003C/p>\n\u003Cp>最后,希望 Nuxt 能越来越好,越来越完善,我还等着学呢(吐槽是吐槽,该用还得用)。\u003C/p>",{"id":79,"createdAt":80,"updatedAt":81,"creator":4,"updater":4,"title":82,"description":83,"cover":84,"content":85,"top":19,"order":9,"status":20,"type":21,"contentType":29,"originalUrl":17},44,"2025-07-29T15:21:30.258Z","2025-08-04T01:51:57.092Z","quill 封装","Quill 2.0.3 版本发布了,作为开源且功能强大的文本编辑器,Quill的使用文档很少,不过还是有很多开源的 vue-quill 库,为了组件的稳定与体验新版本,我也是来尝试一下。","https://nest-admin-1308002460.cos.ap-chengdu.myqcloud.com/1753802488232-Snipaste_2025-07-29_23-21-04.webp","## 封装\n```vue\n\u003Cscript setup lang=\"ts\">\nimport Quill from 'quill'\nimport type { Delta, EmitterSource, QuillOptions, Range } from 'quill'\nimport { onBeforeUnmount, onMounted, ref, watch } from 'vue'\nimport { Mention, MentionBlot } from 'quill-mention' // 插件\nimport 'quill/dist/quill.snow.css'\nimport '@/assets/styles/quill-mention/quill.mention.min.css'\nimport undo from '@/assets/icons/quill-undo.svg?raw'\nimport redo from '@/assets/icons/quill-redo.svg?raw'\nimport attachment from '@/assets/icons/quill-attachment.svg?raw'\nimport useUsersStore from '@/store/cache/users'\nimport type { ApiError, User } from '@/models'\nimport { uploadFile as apiUploadFile } from '@/api/modules'\n\nconst props = defineProps\u003C{\n // HTML model value, supports v-model\n modelValue?: string | null\n // Quill initialization options\n options?: QuillOptions\n semantic?: boolean\n}>()\nconst emit = defineEmits\u003C{\n (e: 'update:modelValue', value: string): void\n (e: 'textChange', { delta, oldContent, source }: { delta: Delta; oldContent: Delta; source: EmitterSource }): void\n (e: 'selectionChange', { range, oldRange, source }: { range: Range; oldRange: Range; source: EmitterSource }): void\n (e: 'editorChange', eventName: 'textChange' | 'selectionChange'): void\n (e: 'blur', quill: Quill): void\n (e: 'focus', quill: Quill): void\n (e: 'ready', quill: Quill): void\n}>()\n\nlet quillInstance: Quill | null = null\nconst quillEditor = ref\u003CHTMLElement>()\nconst model = ref\u003Cstring | null>()\nconst userList = ref\u003CUser[]>([])\nconst fileUploadLoading = ref(false)\n\nconst defaultOptions: QuillOptions = {\n theme: 'snow',\n placeholder: '',\n readOnly: false,\n modules: {\n toolbar: {\n container: [\n ['undo', 'redo'],\n ['bold', 'italic', 'underline'],\n [{ background: [] }],\n [{ align: [] }],\n [{ list: 'ordered' }, { list: 'bullet' }],\n ['link', 'attachment'],\n ],\n handlers: {\n image: fileHandler('image'), // 正确放置 handlers\n attachment: fileHandler(),\n undo: () => {\n quillInstance?.history.undo()\n updateHistoryStatus()\n },\n redo: () => {\n quillInstance?.history.redo()\n updateHistoryStatus()\n },\n },\n },\n mention: {\n blotName: 'styled-mention',\n allowedChars: /^[\\u4E00-\\u9FA5\\w]*$/, // 允许中文/英文/数字/下划线\n mentionDenotationChars: ['@'], // 触发字符\n source(searchTerm: any, renderList: any, mentionChar: any) {\n // 这里可以换成后端接口\n const users = userList.value.map((u) => {\n return {\n id: u.id,\n value: u.fullName,\n avatar: u.avatar,\n email: u.email,\n }\n })\n const matches = users.filter(u => u.value.toLowerCase().includes(searchTerm.toLowerCase()))\n renderList(matches, searchTerm)\n },\n renderItem(item: any) {\n const container = document.createElement('div')\n container.className = 'mention-list-item'\n\n const avatarWrapper = document.createElement('div')\n avatarWrapper.className = 'mention-avatar'\n\n if (item.avatar) {\n const avatarImg = document.createElement('img')\n avatarImg.src = item.avatar\n avatarImg.alt = item.value\n avatarImg.className = 'mention-avatar-img'\n avatarWrapper.appendChild(avatarImg)\n }\n else {\n avatarWrapper.textContent = item.value.charAt(0).toUpperCase()\n }\n\n const text = document.createElement('div')\n text.className = 'mention-text'\n\n const name = document.createElement('div')\n name.className = 'mention-name'\n name.innerText = item.value\n\n const email = document.createElement('div')\n email.className = 'mention-email'\n email.innerText = item.email\n\n text.appendChild(name)\n text.appendChild(email)\n\n container.appendChild(avatarWrapper)\n container.appendChild(text)\n\n return container\n },\n onSelect(item: any, insertItem: any) {\n insertItem({ ...item, value: `${item.value} ` })\n },\n },\n },\n}\n\n// Convert modelValue HTML to Delta and replace editor content\nconst pasteHTML = (quill: Quill) => {\n model.value = props.modelValue\n const oldContent = quill.getContents()\n const delta = quill.clipboard.convert({ html: props.modelValue ?? '' })\n quill.setContents(delta)\n emit('textChange', { delta, oldContent, source: 'api' })\n}\n\nfunction registerMention() {\n class StyledMentionBlot extends MentionBlot {\n static render(data: any) {\n const element = document.createElement('span')\n element.innerText = data.value\n element.setAttribute('mention-id', data.id)\n return element\n }\n }\n StyledMentionBlot.blotName = 'styled-mention'\n if (!Quill.imports['formats/styled-mention']) {\n Quill.register('formats/styled-mention', StyledMentionBlot, true)\n }\n\n if (!Quill.imports['modules/mention']) {\n Quill.register('modules/mention', Mention, true)\n }\n}\n\nfunction customIcons() {\n const icons = Quill.import('ui/icons') as any\n icons.undo = undo\n icons.redo = redo\n icons.attachment = attachment\n}\n\n// Editor initialization, returns Quill instance\nconst initialize = async () => {\n registerMention()\n customIcons()\n userList.value = await useUsersStore().asyncGetList()\n Object.assign(defaultOptions, props.options)\n const quill = new Quill(quillEditor.value as HTMLElement, defaultOptions)\n\n // Set editor initial model\n if (props.modelValue) {\n pasteHTML(quill)\n }\n\n // Handle editor selection change, emit blur and focus\n quill.on('selection-change', (range: any, oldRange: any, source: any) => {\n if (!range) {\n emit('blur', quill)\n }\n else {\n emit('focus', quill)\n }\n updateHistoryStatus()\n emit('selectionChange', { range, oldRange, source })\n })\n\n // Handle editor text change\n quill.on('text-change', (delta: any, oldContent: any, source: any) => {\n model.value = props.semantic ? quill.getSemanticHTML() : quill.root.innerHTML\n updateHistoryStatus()\n emit('textChange', { delta, oldContent, source })\n })\n\n // Handle editor change\n quill.on('editor-change', (eventName: 'textChange' | 'selectionChange') => {\n emit('editorChange', eventName)\n })\n\n emit('ready', quill)\n\n quillInstance = quill\n updateHistoryStatus()\n\n return quill\n}\n\nfunction updateHistoryStatus() {\n if (!quillInstance) {\n return\n }\n const undoBtn = document.querySelector('.ql-undo') as HTMLButtonElement\n const redoBtn = document.querySelector('.ql-redo') as HTMLButtonElement\n if (!undoBtn || !redoBtn) {\n return\n }\n undoBtn.disabled = quillInstance.history.stack.undo.length === 0\n redoBtn.disabled = quillInstance.history.stack.redo.length === 0\n}\n\n// Watch modelValue and paste HTML if has changes\nwatch(\n () => props.modelValue,\n (newValue) => {\n if (!quillInstance) {\n return\n }\n if (newValue && newValue !== model.value) {\n pasteHTML(quillInstance)\n // Update HTML model depending on type\n model.value = props.semantic ? quillInstance.getSemanticHTML() : quillInstance.root.innerHTML\n }\n else if (!newValue) {\n quillInstance.setContents([])\n }\n },\n)\n\n// Watch model and update modelValue if has changes\nwatch(model, (newValue, oldValue) => {\n if (!quillInstance) {\n return\n }\n if (newValue && newValue !== oldValue) {\n emit('update:modelValue', newValue)\n }\n else if (!newValue) {\n quillInstance.setContents([])\n }\n})\n\nfunction getQuillInstance() {\n return quillInstance\n}\n\nfunction fileHandler(type?: string) {\n return () => {\n const input = document.createElement('input')\n input.setAttribute('type', 'file')\n if (type) {\n input.setAttribute('accept', `${type}/*`)\n }\n input.click()\n\n input.onchange = async () => {\n const file = input.files?.[0]\n if (!file) {\n return\n }\n\n fileUploadLoading.value = true\n const formData = new FormData()\n formData.append('file', file)\n apiUploadFile(formData)\n .then((res) => {\n if (quillInstance) {\n const index = quillInstance.getSelection()?.index || 0\n\n switch (type) {\n case 'image':\n quillInstance.insertEmbed(index, 'image', res)\n break\n default: {\n const linkHtml = `\u003Ca href=\"${res}\" target=\"_blank\">🔗${file.name}\u003C/a>`\n quillInstance.clipboard.dangerouslyPasteHTML(index, linkHtml)\n break\n }\n }\n }\n })\n .catch((error: ApiError) => {\n ElMessage.error(error.message)\n })\n .finally(() => {\n fileUploadLoading.value = false\n })\n }\n }\n}\n\nonMounted(() => {\n initialize()\n})\n\nonBeforeUnmount(() => {\n quillInstance = null\n})\n\n// Expose init function\ndefineExpose\u003C{\n getQuillInstance: () => Quill | null\n}>({\n getQuillInstance,\n })\n\u003C/script>\n\n\u003Ctemplate>\n \u003Cdiv class=\"quill-container\">\n \u003Cdiv ref=\"quillEditor\" />\n \u003C/div>\n\u003C/template>\n\n\u003Cstyle lang=\"scss\" scoped>\n.quill-container {\n width: 100%;\n height: 100%;\n min-height: 200px;\n display: flex;\n flex-direction: column;\n line-height: normal;\n\n :deep(.ql-toolbar) {\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n }\n\n :deep(.ql-container) {\n font-size: 16px;\n border-bottom-left-radius: 4px;\n border-bottom-right-radius: 4px;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans',\n 'Helvetica Neue', sans-serif;\n }\n\n :deep(.ql-blank::before) {\n font-style: normal;\n }\n\n :deep(.ql-undo[disabled]),\n :deep(.ql-redo[disabled]) {\n opacity: 0.5;\n cursor: not-allowed;\n\n .ql-stroke {\n stroke: #444 !important;\n }\n }\n\n :deep(.ql-mention-list) {\n padding: 4px;\n }\n\n :deep(.ql-mention-list-item) {\n padding: 0;\n line-height: normal;\n border-radius: 3px;\n }\n\n // mention style\n :deep(.mention-list-item) {\n display: flex;\n padding: 4px 8px;\n border-radius: 3px;\n align-items: center;\n\n .mention-avatar {\n width: 45px;\n height: 45px;\n border-radius: 50%;\n background-color: var(--el-text-color-disabled);\n color: var(--el-color-white);\n display: flex;\n align-items: center;\n justify-content: center;\n margin-right: var(--g-app-margin);\n overflow: hidden;\n flex-shrink: 0;\n }\n\n .mention-avatar-img {\n width: 100%;\n height: 100%;\n object-fit: cover;\n }\n\n .mention-text {\n flex: 1;\n display: flex;\n justify-content: flex-start;\n align-items: flex-start;\n flex-direction: column;\n }\n\n .mention-name {\n display: inline-block;\n font-size: 14px;\n font-weight: 400;\n line-height: 24px;\n text-transform: none;\n }\n .mention-email {\n color: var(--el-text-color-secondary);\n font-size: var(--el-font-size-extra-small);\n width: 160px;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n }\n}\n\u003C/style>\n```\n\n## 使用\n\n```vue\n\u003CQuillEditor ref=\"editorRef\" v-model:model-value=\"content\" :options=\"options\" />\n```\n",{"id":87,"createdAt":88,"updatedAt":89,"creator":4,"updater":4,"title":90,"description":91,"cover":17,"content":92,"top":19,"order":9,"status":20,"type":21,"contentType":29,"originalUrl":17},48,"2025-08-26T13:20:15.796Z","2025-09-01T01:55:00.710Z","flex布局玄学-内容影响布局","flex布局玄学,那些年我们遇到的奇奇怪怪的bug","## flex 布局 固定宽度内容被挤压变形\n当我们在使用 flex 布局实现两栏布局时,我们给一个容器的宽度设置固定宽度 比如 `width: 20px;`,另外一个容器给 `flex: 1`,这样就能实现一侧固定宽度,另外一侧自适应宽度的一个布局,一切看起来是正常的,但是某些情况下,可能会遇见自适应宽度容器内子元素宽度过大,把自适应宽度容器撑开的情况,比如内部有不限制宽高的图片、code块等。会导致另外一侧固定宽度被挤压变小,这时我们可以给一个属性 `flex-shrink: 0;` 来让元素不会因为挤压而产生缩放。\n\n## flex 布局 内容撑大父容器\n前面那种情况是很常见的,如果解决了固定容器被挤压变形的问题,你可能会发现一个新的问题,就是固定宽度容器宽度是不受挤压发生变形了,可是原来的自适应容器宽度甚至比父容器还大,这就是内容把自适应容器宽度撑大了,解决方式就是添加 `min-width: 0; `,原理是:`min-width: 0` 允许它缩小到内容以下,避免被内容撑开。\n\n这两个问题是我在开发博客的过程中遇到的,困扰我很久。第二个问题解决起来真的很玄学?\n\n",{"id":94,"createdAt":95,"updatedAt":95,"creator":4,"updater":9,"title":96,"description":17,"cover":17,"content":97,"top":19,"order":9,"status":20,"type":21,"contentType":29,"originalUrl":17},51,"2025-09-30T03:27:41.989Z","博客代码重构记录","### Nuxt3 -> Nuxt4\n基本上就是把依赖升级了一遍,Nuxt4比Nuxt3稳定一些,非常Nice\n\n### 样式\n将通用/可复用的样式抽离到了全局的样式文件里,大概有:\n_variables:样式变量\ntheme:主题样式变量\nlayout:布局\nform:表单\nbutton:按钮\nhighlight:代码高亮\ntransition:过渡动画\nblog:博客的通用样式\nblog-post:博客文章通用样式\n\n### 类型\n一些模块的类型放在 types 下\n一些特殊的联系放在 types/form、types/query 下\nform:表单参数\nquery:搜索参数\n\n### 静态资源\n将 styles、icons、images 放在 assets 下\n\n### 组件\n重构了所有的组件,基本满足组件设计原则\n\n主要调整的有 Comment、Editor 等\n\n#### Comment\n将 Comment 拆分成了 CommentCard、Input、CommentText、ReplyCard 等组件\n每一个组件负责单一的职责,接口数据处理全在 comment/index 组件内统一处理",5,{"id":100,"createdAt":101,"updatedAt":101,"createdBy":9,"updatedBy":9,"creator":9,"updater":9,"name":102,"articles":103,"articleCount":112,"count":113},12,"2025-07-21T03:49:59.988Z","组件设计原则",[104,110,111],{"id":105,"createdAt":106,"updatedAt":106,"creator":4,"updater":4,"title":107,"description":108,"cover":17,"content":109,"top":19,"order":9,"status":20,"type":21,"contentType":29,"originalUrl":17},41,"2025-07-21T03:50:00.032Z","表格如何二次封装","最近工作里接触到了表格组件的封装,表格组件的功能其实不多,但是怎么封装一个好用、可扩展、内容简洁的表格组件,真是一个值得思考的问题。为此,我还阅读了 wiki 百科的 SOLID (面向对象设计),受益颇深。","# 公共组件封装原则\n\n1、单一职责原则:一个组件应该只负责一个功能\n2、可复用性原则:组件应该是独立的通用的,可以在多种上下文使用\n3、可配置性:组件设计时应该具有足够的配置选项和参数,用户可以根据参数控制组件行为\n - props: 属性\n - slots:插槽\n - 事件:通知外部组件,比如点击、改变、成功、失败\n - hooks:处理一些通用逻辑,然后注入到组件中,增强组件\n - Provide/Inject:共享数据\n - 全局配置:组件库级别的处理,比如颜色、字体、图标、语言等\n\n4、可测试性\n - 使用测试框架来测试组件,比如jest、vitest、jasmine、mocha等\n - 模拟数据和事件,模拟数据来测试组件的行为,事件来测试组件的交互\n\n5、单向数据流:数据从外部进入组件内部,组件内部不处理数据,数据变化触发事件通知父组件处理\n\n# 表格组件封装历程\n\n此次表格组件的核心是:实现单一职责原则\n其他的封装原则之前都是严格遵守的,只是这个表格内部实现了对用户自定义列的获取、展示、控制等,这不符合单一职责原则,表格应该只负责表格列的控制和内容展示\n\n## 收集表格逻辑\n由于我对表格的封装、功能不熟悉,所以需要熟悉一下。\n\n1、表格的列可以配置显示/隐藏,列的顺序可以拖拽\n2、表格的列可以配置默认列(默认展示的列)、固定列(所有表格的列),相当于在初始化时,可以展示你想展示的那几列(默认列)\n3、表格的列通过api获取 格式:['name', 'age', 'sex'],意义在于保存到后端后,刷新页面可以获取到这些列,通过返回的列的key从而展示这些列\n4、表格列(columns)需要自行组装,不同表格的列的title、width、minWdth、sorter都是变化的,需要在不同组件内编写函数来组装\n5、表格数据排序\n6、一些表格除了固定列以外,还有用户自定义的列,也就是在用户设置里可以对(可以定义表格列的表格)进行自定义列添加,后续可以在这条数据的编辑页面修改自定义列的值,表格就负责显示自定义列和值\n\n## 之前封装的表格流程\ntips:之前的表格组件存在很多问题,相关内容都在后面括号里有解释\n\n1、封装了一个抽象类来获取表格列和用户自定义列(这是一种很好的设计模式 -- 模板方法模式,但是也不是很适用,因为这两个方法是有通用逻辑的,抽象方法里没有逻辑,所以每个页面都得重新写)\n2、用户在具体页面继承抽象类,并且实现抽象类中的抽象方法,获取表格列和用户自定义列\n3、在获取表格列和用户自定义列时,会组装列和用户自定义列(这是在组件外部组装的)\n4、最后会把这些配置传给表格组件\n5、表格组件内部会根据用户自定义的列配置,获取到列的值然后组装显示(用户自定义列就和表格耦合了,表格不应该处理自定义列的值这部分内容)\n6、表格的列变化后,表格内部封装了一个重新组装列的方法对列进行重新组装(表格内部不应该组装列)\n\n## 我的思路\n由于刚开始我不了解组件表格的逻辑,所以我只是想着把原来的逻辑通过 hooks + 组件的方式来封装,先保留原有表格组件的功能,然后再抽离公共逻辑\n\n1、我将 获取表格列和用户自定义列 的方法写到了 hooks 里,并且整合了这过程中必要的逻辑,用户只需要提供 api 就可以获取到列了,列的组装函数也是通过参数提供的\n2、我把逻辑全放到 hooks 里了,然后把需要用到的列的信息、自定义列的信息、自定义列的数据、列的操作等都返回出去了,表格只需要接受这些参数就可以了\n3、项目里有25个页面使用到了表格,由于那两个抽象方法的原因,好多多余的逻辑在那两个方法里实现了,我在观察完这些表格后,对这两个方法进行了重构,把冗余的逻辑都删除了,但是也添加了很多当时感觉是必要的逻辑,导致方法过于庞大(好多都是列排序的逻辑,实际上是多余的,排序完全可以根据显示列的顺序来对表格传入的 columns 进行排序,排序逻辑应该在表格组件内部,或者是组装列时通过 computed 来实时排序,而不是每一次有变化我都要调一次排序函数,这里我就不得不吐槽一下原作者了,由于我也没认真思考,被带偏了)\n\n## 我的问题\n1、首先我的思路是在原有的基础上对整个组件进行实现,优化了用户可配置功能,增加了类型提示,减小了开发者使用难度,但是我并没有用重构的思路去解决这个问题,也就是我并没有解决当前组件表格和自定义列耦合的问题\n2、我的思路被原组件的实现方式限制了,这个组件相当于还是原来的组件,就是代码换了一种写法\n\n## 优化思路\nleader 也是耐心地让我好好想想这个重构任务到底要做什么,我应该如何去分解这个任务\n叫我先不用管自定义列,先实现表格的基础功能\n1、实现表格列在组件外部的组装\n2、实现表格列显示隐藏控制\n3、实现表格列的排序功能\n\n后续就是对自定义列的处理\n(都是在组件外部处理的)\n1、我写了一个自定义列的 hooks,通过 api 获取自定义列,然后组装列(涉及到后端api的,都应该传入 api 来调用(可配置、可复用)或者是暴露事件到组件外部处理(方便扩展))\n2、我编写了一个用于渲染自定义列的组件,和一个 hooks 处理用户自定义列数据\n3、我将列组合搭配一起后传给表格组件\n4、通过插槽来将自定义列组件放到自定义列的位置,并且把自定义列的数据传给自定义列组件,自定义列数据更新可以通过表格数据变化事件来触发更新\n5、列的显示/隐藏、排序变化后重新生成列我不用考虑,因为全部的列是一开始就组装好了的,表格内部只是展示可以展示的那几列,排序我也可以根据后端返回的列进行排序,所以我完全去掉了重新组装列的逻辑(这个逻辑之前组件设计不好导致的)\n\nmore:列的组装其实挺繁琐的,因为宽度、展示列的信息等内容需要自定义,所以这个组装函数和表格的名称这些都有挺多耦合的,也是通过不同的页面传入不同的函数来解耦的。\n\n到现在我就实现了 去掉用户自定义列和固定列的耦合,表格内部也是只处理了列的展示,完全不用担心列的组装,希望这类表格的封装能给你一定的思路启发(我陆陆续续干了差不多两个星期,哭死)\n# 我的解释\n可能你看完想不明白我为什么要花这么多时间去做这件事情,你先别急,听我慢慢道来:最大原因是这个组件不符合软件设计原则,虽然能用,但是如果自定义列有新的逻辑进来,开发者就得扩展这部分逻辑,到后面组件慢慢就变成了 shi 山,如果自定义列有新的渲染方式,那么组件内还得扩展自定义列那个组件,表格内部就不应该有其他与表格无关的组件,一个简单的表格控制功能,变成了充满了自定义列内容的天下。如果这个组件只是保留了基础的列显示/隐藏控制、列渲染、提供插槽功能,那么后续无论什么需求进来,表格都不用动,动的是 hooks,插槽内容,或者是具体页面内的逻辑(可能就一个页面特殊需要扩展,其他的都不用)。\n\n# 相关链接\n[面向对象设计](https://zh.wikipedia.org/wiki/SOLID_(%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E8%AE%BE%E8%AE%A1))",{"id":79,"createdAt":80,"updatedAt":81,"creator":4,"updater":4,"title":82,"description":83,"cover":84,"content":85,"top":19,"order":9,"status":20,"type":21,"contentType":29,"originalUrl":17},{"id":94,"createdAt":95,"updatedAt":95,"creator":4,"updater":9,"title":96,"description":17,"cover":17,"content":97,"top":19,"order":9,"status":20,"type":21,"contentType":29,"originalUrl":17},4,3,{"id":115,"createdAt":116,"updatedAt":116,"createdBy":9,"updatedBy":9,"creator":9,"updater":9,"name":117,"articles":118,"articleCount":113,"count":113},15,"2025-08-20T10:31:37.065Z","前端",[119,125,126],{"id":120,"createdAt":121,"updatedAt":121,"creator":4,"updater":9,"title":122,"description":123,"cover":17,"content":124,"top":19,"order":9,"status":20,"type":21,"contentType":29,"originalUrl":17},47,"2025-08-20T10:31:37.083Z","Axios如何取消请求","本文介绍了 Axios 如何取消请求的两种方式。","## 序言\n\n相信很多小伙伴都没有想过发出去的请求如何取消,一方面是普通的后台管理系统类型取消请求的需求不多,另外一方面是普通的后台管理系统涉及不到高并发的请求,以及连续的批量请求。\n\n我在第一年的工作里,确实没有接触过取消请求的需求,对于这方面的知识也就没有积累,导致面试官问到如何取消请求时,我回答不上来,然后就是被一顿输出,最后不了了之。\n\n## 正文\n那么 axios 如何取消请求呢?本文介绍以下两种方式:\n1、使用 axios 的静态方法\n```js\nconst CancelToken = axios.cancelToken\nconst source = CancelToken.source()\n\naxios.get('/xxxx', {\n cancelToken: source.token\n})\n\nsource.cancel('cancel request by user')\n```\n2、实例化 CancelToken\n```js\nlet cancel = null\naxios.get('/xxxxx', {\n cancelToken: new CancelToken(function executor(c){\n cancel = c\n })\n})\n\ncancel()\n```\n\n## 封装一个好用的 hook 在项目里使用\n\n```ts\nimport { onUnmounted, ref } from 'vue'\nimport type { CancelToken, CancelTokenSource } from 'axios'\nimport axios from 'axios'\n\n/**\n * 封装一个可取消的 API 请求 hook\n * @param apiFn - 需要调用的 API 方法 (接收 cancelToken)\n */\nexport function useCancelableApi\u003CT>(\n apiFn: (cancelToken: CancelToken) => Promise\u003CT>,\n) {\n const lastSource = ref\u003CCancelTokenSource | null>(null)\n const loading = ref(false)\n const error = ref\u003Cunknow>()\n\n function abort() {\n if (lastSource.value) {\n lastSource.value.cancel('Request aborted')\n lastSource.value = null\n }\n }\n\n async function fetchData(): Promise\u003CT> {\n abort() // 先取消上一次\n lastSource.value = axios.CancelToken.source()\n loading.value = true\n error.value = undefined\n\n try {\n const res = await apiFn(lastSource.value.token)\n return res\n }\n catch (err: unknown) {\n error.value = err \n throw err\n }\n finally {\n loading.value = false\n }\n }\n\n onUnmounted(() => {\n abort()\n })\n\n return {\n fetchData,\n abort,\n loading,\n error,\n }\n}\n```\n\n使用方式\n```ts\nconst getData = async (cancelToken: CancelToken) => {\n return await apiGetData(query, cancelToken)\n}\n\nconst { fetchData, abort } = useCancelableApi(getDealsData)\n\n// 使用 fetchData 获取数据 使用 abort 可以取消请求\n```",{"id":87,"createdAt":88,"updatedAt":89,"creator":4,"updater":4,"title":90,"description":91,"cover":17,"content":92,"top":19,"order":9,"status":20,"type":21,"contentType":29,"originalUrl":17},{"id":127,"createdAt":128,"updatedAt":128,"creator":4,"updater":9,"title":129,"description":130,"cover":17,"content":131,"top":19,"order":9,"status":20,"type":21,"contentType":29,"originalUrl":17},50,"2025-09-09T05:14:05.875Z","多分支代码提交规范","代码提交、合并也是一门技术","> 此规范适用情况\n\n1、项目基于 gitlab 工作流开发\n- gitlab 有一套完整的多分支开发流程\n\n2、项目存在多分支协同开发\n- 有 dev、staging、prod 等分支\n- 有具体每个 issue 的 merge-request 分支(参考 gitlab 工作流)\n\n3、项目不关注每个提交的具体提交记录 只关注整体的代码\n- 当 merge-request 有多条提交记录时,需要合并提交记录为一条 -> 尽量减少主分支的提交记录\n- 项目希望提交树尽量干净,不需要 merge 信息\n\ntip: 如果是小项目,开发者在 dev 分支开发就不会涉及到此规范,直接拉别人的代码再解决冲突提交就好了\n\n# 创建 merge-request 分支\n在 gitlab 上,当管理者 assign 给我们 issue 时,我们可以在 issue 详情页面 create merge request,我们可以选择 merge request 的源分支,当 merge request 通过代码审查后,会自动合并到主分支\n\n# squash 合并提交记录\n可以使用 squash 操作合并提交记录\n有很多工具有可视化操作,git使用命令也可以完成,不过使用工具会更方便 比如:GitHub Desktop\n\n# cherry-pick 代码\n如果在开发过程中,有一个代码是其他分支的,没有提交到主分支,你刚好需要使用,这时候可以使用 cherry-pick 命令,把代码拉过来,这时候你的代码里就会有这条记录\n\n具体命令: \n```bash\ngit checkout target branch // 先切换到其他分支\n\ngit log // 查看 提交记录\n\ngit cherry-pick [git hash] // [git hash] 就是需要 pick 过来的 提交记录的 hash 值,是一串很长的字符串\n```\n\n# rebase 代码变基\n如果在开发过程中,有一个新合并到主分支的代码,或者是你在其他分支有新的提交,而当前你工作的分支需要那些代码,你可以基于 主分支/其他新分支进行 rebase(变基),相当于代码变成了目标分支的代码\n\ntips: rebase 代码后可能需要解决冲突,解决后一定要强推到远端,提交记录才会是正常的,不然可能会重复\n\n具体命令: \n```bash\ngit rebase target branch // rebase\n\ngit push target branch --force // 强推\n```\n因为 rebase 是在你工作的分支上进行的,所以可以强推,只要处理好冲突就行\n\n# 为什么需要使用 rebase 而不是 merge\n\n我们可以通过以下两张图来看出区别\n\nmerge:\n\n\n\nrebase:\n\n\n\n\n可以看到 merge 后的代码在主分支上会比 rebase 多一个 merge 的提交信息,这个信息其实我们是不关注的,多分支协同开发的情况下,这种合并情况会很多,为了避免出现这么多的merge提交信息,我们通常采用 rebase 合并代码",{"id":133,"createdAt":134,"updatedAt":134,"createdBy":9,"updatedBy":59,"creator":9,"updater":4,"name":135,"articles":136,"articleCount":150,"count":150},7,"2025-05-21T07:48:29.177Z","小张的备忘录",[137,143],{"id":138,"createdAt":139,"updatedAt":139,"creator":4,"updater":9,"title":140,"description":141,"cover":17,"content":142,"top":19,"order":9,"status":20,"type":21,"contentType":22,"originalUrl":17},29,"2025-05-21T07:48:29.192Z","拿小本本记录欠的人情","要随时记得别人对自己做出的承诺","\u003Col>\n\u003Cli>宋老板欠我一份 298 按摩,以图为证\n\u003Cp>\u003Cimg src=\"https://nest-admin-1308002460.cos.ap-chengdu.myqcloud.com/1747813576968-image.png\">\u003C/p>\n\u003Cp> \u003C/p>\n\u003C/li>\n\u003C/ol>",{"id":144,"createdAt":145,"updatedAt":146,"creator":4,"updater":4,"title":147,"description":17,"cover":148,"content":149,"top":37,"order":150,"status":20,"type":21,"contentType":29,"originalUrl":17},49,"2025-09-05T10:20:17.587Z","2025-09-15T16:44:39.232Z","留言板","https://nest-admin-1308002460.cos.ap-chengdu.myqcloud.com/1757067612595-image.webp","> **请在下方留下您的评论,邮件系统会主动推送消息,我会尽快回复您**\n> 邮件默认为 QQ 邮件,留言后请持续关注您的QQ邮箱\n\n\n",2,{"id":152,"createdAt":153,"updatedAt":153,"createdBy":9,"updatedBy":9,"creator":9,"updater":9,"name":154,"articles":155,"articleCount":113,"count":150},13,"2025-07-22T13:50:49.520Z","软件设计原则",[156,162],{"id":157,"createdAt":158,"updatedAt":158,"creator":4,"updater":4,"title":159,"description":160,"cover":17,"content":161,"top":19,"order":9,"status":20,"type":21,"contentType":29,"originalUrl":17},42,"2025-07-22T13:50:49.527Z","SOLID (面向对象设计)","我们编写的代码是否符合规范,不仅需要从项目自身的代码规范上来看,也要从是否符合软件设计原则上来看,这也是为什么很多时候功能实现了,code review 不通过的原因,学好基本的软件设计原则,有助于我们写出健壮的代码。","# SOLID 面向对象设计\nSOLID(单一功能、开闭原则、里氏替换、接口隔离、依赖反转)\n\n# S \n单一功能原则(Single responsibility principle),规定每个类都应该有一个单一的功能,并且该功能由这个类完全封装起来,不能和其他平行的类有依赖。\n[参见 维基百科 - 单一功能原则](https://zh.wikipedia.org/wiki/%E5%8D%95%E4%B8%80%E5%8A%9F%E8%83%BD%E5%8E%9F%E5%88%99)\n\n# O\n开闭原则(The Open/Closed principle, OCP),软件中的对象(类、模块、函数等),对于扩展是开放的,对于修改是封闭的。\n[参见 维基百科 - 开闭原则](https://zh.wikipedia.org/wiki/%E5%BC%80%E9%97%AD%E5%8E%9F%E5%88%99)\n\n# L\n里氏替换原则(Liskov substitution principle),派生类(子类)对象可以在程序中代替其基类(超类)对象。\n[参见 维基百科 - 里氏替换原则](https://zh.wikipedia.org/wiki/%E9%87%8C%E6%B0%8F%E6%9B%BF%E6%8D%A2%E5%8E%9F%E5%88%99)\n\n# I\n接口隔离原则(interface-segregation principles, ISP),\n指明客户(client)不应被迫使用对其而言无用的方法或功能。[1]接口隔离原则(ISP)拆分非常庞大臃肿的接口成为更小的和更具体的接口,这样客户将会只需要知道他们感兴趣的方法。这种缩小的接口也被称为角色接口(role interfaces)。[2]接口隔离原则(ISP)的目的是系统解开耦合,从而容易重构,更改和重新部署\n[参见 维基百科 - 接口隔离原则](https://zh.wikipedia.org/wiki/%E9%87%8C%E6%B0%8F%E6%9B%BF%E6%8D%A2%E5%8E%9F%E5%88%99)\n\n# D\n依赖反转原则(Dependency Inversion Principle,DIP),是指一种特定的解耦(传统的依赖关系建立在高层次上,而具体的策略设置则应用在低层次的模块上)形式,使得高层次的模块不依赖于低层次的模块的实现细节,依赖关系被颠倒(反转),从而使得低层次模块依赖于高层次模块的需求抽象。\n\n高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口。\n抽象接口不应该依赖于具体实现。而具体实现则应该依赖于抽象接口。",{"id":163,"createdAt":164,"updatedAt":164,"creator":4,"updater":4,"title":165,"description":166,"cover":17,"content":167,"top":19,"order":9,"status":20,"type":21,"contentType":29,"originalUrl":17},43,"2025-07-24T14:01:53.576Z","依赖注入、控制反转","面向对象编程,必须了解依赖注入、控制反转的思想","依赖注入(DI,Dependency Inject)与控制反转(IoC,Invertion of Contorl)在许多框架中都存在,如spring、.net、nest等。很多小伙伴都听说过,却不是很了解。\n\n## 解释\n控制反转(IoC),是一种设计原则(设计思想)。用于对象与对象之间的解耦。在传统编程中,对象通常会自行创建和管理其依赖对象,导致组件之间的耦合度高。实现依赖注入的方式就是通过控制反转,将创建对象和依赖关系过程交给外部容器。\n\n```javascript\nclass A\n{\n constructor(B){\n // 因为A掌控B的创建,因此A控制了B\n this.b = new B();\n }\n} \n```\n\n依赖注入(Dependency Inject,DI),也叫注入,是一种设计模式(具体实现)。将对象的创建和依赖关系管理由外部容器负责,而不是在具体使用到对象的地方管理。\n```javascript\nclass A\n{\n // B由外部注入,称之为依赖注入\n constructor(B)\n {\n // B由外部创建,脱离了A的控制,称之为控制反转\n this.b = B;\n }\n} \n```\n\n## 为什么需要依赖注入和控制反转\n\n### 解耦\n\n\n\n相信大家都听说过一个法则:“高内聚,低耦合”,第一张图,B 依赖了 A,而 C 通过依赖 B 依赖了 A,稍微多一点这种关系,就会很复杂,难以维护。如果 我们把 B 和 C 所依赖的功能向 A 内聚,那么 B 和 C 就实现了解耦。\n\n还有一种方式是做抽象的接口,只要遵循一定的规则,你就可以把具体的实现使用到这个接口上,这也是解耦。比如内存卡,使用内存卡的设备只是一个接口,而内存卡就是这个接口的具体实现,所以你就可以把不同的内存卡(遵循标准生产的)使用到这个设备上。\n\n\n## IoC 容器\n\n\n\n在项目里,通常会遇到两个关于对象的问题,一个是对象数量多,如何统一管理对象的创建和销毁,依赖一个问题是对象间的依赖错综复杂,如何管理他们的依赖关系?\n\n针对上述问题,解决方案就是 IoC 容器,IoC 容器是一个对象管理器,它统一管理对象的创建过程、生命周期、依赖关系,以及提供自动注入,根据配置创建对象的功能。\n\n开源IoC容器Autofac:\n```c#\nusing Autofac;\n\nnamespace AutofacDemo\n{\n class A\n { }\n\n class B\n {\n A _a;\n // 只需要声明需要注入的对象,由容器自动完成依赖对象的创建与注入\n public B(A a)\n {\n _a = a;\n }\n }\n\n internal class Program\n {\n static void Main(string[] args)\n {\n // 将类型注册至容器中\n ContainerBuilder builder = new ContainerBuilder();\n builder.RegisterType\u003CA>();\n // 设置对象的生命周期(单例模式)\n builder.RegisterType\u003CB>().SingleInstance();\n\n // 构造IoC容器\n IContainer container = builder.Build();\n // 从容器中获取对象\n B b = container.Resolve\u003CB>();\n }\n }\n} \n```\n",{"id":169,"createdAt":170,"updatedAt":170,"createdBy":9,"updatedBy":9,"creator":9,"updater":9,"name":171,"articles":172,"articleCount":150,"count":37},10,"2025-05-29T09:38:11.804Z","面试",[173],{"id":174,"createdAt":175,"updatedAt":176,"creator":4,"updater":4,"title":177,"description":178,"cover":17,"content":179,"top":19,"order":9,"status":20,"type":21,"contentType":22,"originalUrl":17},35,"2025-05-30T06:43:29.912Z","2025-07-24T15:38:34.234Z","个人介绍-英文版","DeepSeek 帮我写的个人介绍,希望有用吧","\u003Cp> Hello, I'm a Front-End Developer, currently 25 years old. I graduated from Chengdu University of Information Technology, where I laid a solid foundation in computer science and software development.\u003C/p>\n\u003Cp> I have accumulated three years of practical experience in front-end development, during which I have been primarily focused on Vue.js as my core technology stack. I've had the opportunity to work on a large-scale project management platform, which has given me an in-depth understanding of the entire development and deployment process. From initial requirements gathering to final deployment and maintenance, I have done well in every stage.\u003C/p>\n\u003Cp> One of my strengths is my ability to complete tasks efficiently within the given time frame.I am also adept at leveraging AI tools to enhance productivity. Whether it's automating repetitive tasks or generating code snippets, I can effectively utilize AI to streamline my workflow and improve overall efficiency.\u003C/p>\n\u003Cp> Moreover, I have a strong learning ability. I am always eager to explore new technologies and methodologies in the ever-evolving field of front-end development. This allows me to stay up-to-date with industry trends and continuously improve my skills.\u003C/p>\n\u003Cp> I am passionate about creating user-friendly and visually appealing interfaces. I believe that a well-designed front-end can significantly enhance the user experience and contribute to the success of any project.\u003C/p>\n\u003Cp> Thank you for taking the time to get to know me. I look forward to bringing my skills and enthusiasm to new opportunities and challenges.\u003C/p>",{"id":181,"createdAt":182,"updatedAt":182,"createdBy":9,"updatedBy":9,"creator":9,"updater":9,"name":183,"articles":184,"articleCount":37,"count":37},11,"2025-07-09T15:30:15.692Z","Nuxt3",[185],{"id":72,"createdAt":73,"updatedAt":73,"creator":4,"updater":4,"title":74,"description":75,"cover":76,"content":77,"top":19,"order":9,"status":20,"type":21,"contentType":22,"originalUrl":17},{"id":187,"createdAt":188,"updatedAt":188,"createdBy":9,"updatedBy":9,"creator":9,"updater":9,"name":189,"articles":190,"articleCount":37,"count":37},14,"2025-08-18T02:35:50.305Z","旅行",[191],{"id":52,"createdAt":53,"updatedAt":54,"creator":4,"updater":4,"title":55,"description":56,"cover":17,"content":57,"top":19,"order":9,"status":20,"type":21,"contentType":22,"originalUrl":17},{"id":193,"createdAt":194,"updatedAt":194,"createdBy":9,"updatedBy":9,"creator":9,"updater":9,"name":195,"articles":196,"articleCount":37,"count":37},16,"2025-08-20T10:31:37.073Z","axios",[197],{"id":120,"createdAt":121,"updatedAt":121,"creator":4,"updater":9,"title":122,"description":123,"cover":17,"content":124,"top":19,"order":9,"status":20,"type":21,"contentType":29,"originalUrl":17},{"id":199,"createdAt":200,"updatedAt":200,"createdBy":9,"updatedBy":9,"creator":9,"updater":9,"name":201,"articles":202,"articleCount":37,"count":19},9,"2025-05-29T09:38:11.794Z","React",[],{"id":59,"createdAt":204,"updatedAt":204,"username":4,"from":205,"uniqueId":206,"status":37,"profile":207},"2025-05-15T14:01:32.594Z","github",87762995,{"id":59,"createdAt":208,"updatedAt":209,"nickName":210,"gender":37,"email":211,"phone":9,"avatar":212,"signature":213,"address":214,"birthDate":215,"introduction":216},"2025-05-15T14:01:32.577Z","2025-09-02T01:43:07.000Z","XiaoZhang","2715158815@qq.com","https://avatars.githubusercontent.com/u/87762995?v=4","Dream of becoming a fisherman.","中国 成都","1999-11-01T00:00:00.000Z","A Junior Front-End Developer. Vue.js enthusiast.",["Reactive",218],{"$scolor-mode":219,"$ssite-config":223},{"preference":220,"value":220,"unknown":221,"forced":222},"system",true,false,{"_priority":224,"env":227,"name":228,"url":229},{"name":225,"env":226,"url":225},-3,-15,"production","小张的个人博客","https://blog.mrzym.top",["Set"],["ShallowReactive",232],{"allTags":-1,"username":-1,"bloggerInfo":-1},"/archive/React",{"user":235,"blogArticle":237},{"user":9,"mentionList":236},[],{"currentArticle":9,"refresh":222}]