vite+vue3 2
原本要搞一下算法和Asp.NetCore,但是想到vite+vue3还有几个插件没有记录,总是感觉怪怪的,所以今天下午把eslint+prettier、postcss和svgIcon这四个插件试验性的做了个Demo,以下记录这四个的使用过程。
一、开始工作
1.eslint+prettier
1.1.eslint
1.1.1.简要说明:eslint的作用就是约束代码规范的,不论是个人项目还是多人合作,规范的代码风格都是一种保证,对可读性,避免语法错误等有很大的帮助。
1.1.2.步骤过程:
a)安装eslint:npm i eslint -D
b)配置eslint:执行npx eslint --init命令,然后按照提示完成一系列操作来创建配置文件
问:How would you like to use ESLint? (你想如何使用 ESLint?)
答:选择To check syntax, find problems, and enforce code style(检查语法、发现问题并强制执行代码风格)
问:What type of modules does your project use?(你的项目使用哪种类型的模块?)
答:选择 JavaScript modules (import/export)
问:Which framework does your project use? (你的项目使用哪种框架?)
答:选择 Vue.js
问:Does your project use TypeScript?(你的项目是否使用 TypeScript?)
答:选择No
问:Where does your code run?(你的代码在哪里运行?)
答:选择 Browser 和 Node
问:How would you like to define a style for your project?(你想怎样为你的项目定义风格?)
答:选择 Use a popular style guide(使用一种流行的风格指南)
问:Which style guide do you want to follow?(你想遵循哪一种风格指南?)
答:选择 Airbnb
问:What format do you want your config file to be in?(你想你的配置文件使用什么格式?)
答:选择JavaScript
问:Would you like to install them now with npm?(你想现在就用 NPM 安装它们吗?)
答:选择Yes
c)在VSCode使用ESlint,需要安装插件:ESLint
1.1.3.配置信息:

1 module.exports = { 2 env: { 3 browser: true, 4 es2021: true, 5 'vue/setup-compiler-macros': true 6 }, 7 extends: [ 8 'airbnb-base', 9 'plugin:vue/vue3-essential', 10 'plugin:vue/vue3-recommended', 11 'plugin:prettier/recommended' // 添加 prettier 插件 12 ], 13 parserOptions: { 14 ecmaVersion: 'latest', 15 sourceType: 'module' 16 }, 17 plugins: ['vue'], 18 rules: { 19 /* 20 * "off" 或 0 - 关闭规则 21 * "warn" 或 1 - 开启规则,使用警告级别的错误:warn (不会导致程序退出) 22 * "error" 或 2 - 开启规则,使用错误级别的错误:error (当被触发的时候,程序会退出) 23 */ 24 'vue/no-multiple-template-root': 'off', // 关闭多根节点的校验 25 'vue/multi-word-component-names': 'off', // 关闭组件命名规则 26 'vue/singleline-html-element-content-newline': 'off', 27 'vue/multiline-html-element-content-newline': 'off', 28 'vue/component-definition-name-casing': ['error', 'PascalCase'], // off=0|warn=1|error=2,'kebab-case'|'PascalCase' 29 'vue/no-v-html': 'off', 30 31 'import/no-unresolved': 'off', 32 'import/extensions': 'off', 33 'import/no-absolute-path': 'off', 34 'import/no-extraneous-dependencies': 'off', 35 36 'accessor-pairs': 2, // 定义对象的set存取器属性时,强制定义get 37 'arrow-spacing': [2, { before: true, after: true }], // =>的前/后括号 38 'block-spacing': [2, 'always'], // 块是否需要空格 39 'brace-style': [2, '1tbs', { allowSingleLine: true }], // if while function 后面的{必须与if在同一行,java风格 40 camelcase: [0, { properties: 'always' }], // 强制驼峰法命名 41 'comma-dangle': [2, 'never'], // 数组和对象键值对最后一个逗号,never:不能带末尾的逗号, always:必须带末尾的逗号,always-multiline:多行模式必须带逗号,单行模式不能带逗号 42 'comma-spacing': [2, { before: false, after: true }], // 控制逗号前后的空格 43 'comma-style': [2, 'last'], // 控制逗号在行尾出现还是在行首出现 44 'constructor-super': 2, // 强制在子类构造函数中用super()调用父类构造函数,TypeScript的编译器也会提示 45 curly: [2, 'multi-line'], // 强制所有控制语句使用一致的括号风格 46 'dot-location': [2, 'property'], // 强制object.key中.的位置,参数:property:'.'号应与属性在同一行, object:'.'号应与对象名在同一行 47 'eol-last': 2, // 文件末尾强制换行 48 eqeqeq: ['error', 'always', { null: 'ignore' }], // 使用 === 替代 == 49 'generator-star-spacing': [2, { before: true, after: true }], // 生成器函数*的前后空格 50 'handle-callback-err': [2, '^(err|error)$'], // nodejs 处理错误 51 indent: [2, 2, { SwitchCase: 1 }], // 缩进风格 52 'jsx-quotes': [2, 'prefer-single'], // JSX 属性中一致使用双引号或单引号 53 'key-spacing': [2, { beforeColon: false, afterColon: true }], // 对象字面量中冒号的前后空格 54 'keyword-spacing': [2, { before: true, after: true }], // 对象字面量中冒号的前后空格 55 'new-cap': [2, { newIsCap: true, capIsNew: false }], // 函数名首行大写必须使用new方式调用,首行小写必须用不带new方式调用 56 'new-parens': 2, // new时必须加小括号 57 'no-array-constructor': 2, // 禁止使用数组构造器 58 'no-caller': 2, // 禁止使用arguments.caller或arguments.callee 59 'no-console': 'off', // 禁用 console 60 'no-class-assign': 2, // 禁止给类赋值 61 'no-cond-assign': 2, // 禁止在条件表达式中使用赋值语句 62 'no-const-assign': 2, // 禁止修改const声明的变量 63 'no-control-regex': 0, // 禁止在正则表达式中使用控制字符 64 'no-delete-var': 2, // 不能对var声明的变量使用delete操作符 65 'no-dupe-args': 2, // 函数参数不能重复 66 'no-dupe-class-members': 2, // 不允许类中出现重复的声明 67 'no-dupe-keys': 2, // 在创建对象字面量时不允许键重复 {a:1,a:1} 68 'no-duplicate-case': 2, // switch中的case标签不能重复 69 'no-empty-character-class': 2, // 正则表达式中的[]内容不能为空 70 'no-empty-pattern': 2, // 禁止使用空解构模式 71 'no-eval': 2, // 禁止使用eval 72 'no-ex-assign': 2, // 禁止给catch语句中的异常参数赋值 73 'no-extend-native': 2, // 禁止扩展native对象 74 'no-extra-bind': 2, // 禁止不必要的函数绑定 75 'no-extra-boolean-cast': 2, // 禁止不必要的bool转换 76 'no-extra-parens': [2, 'functions'], // 禁止非必要的括号 77 'no-fallthrough': 2, // 禁止switch穿透 78 'no-floating-decimal': 2, // 禁止省略浮点数中的0 .5 3. 79 'no-func-assign': 2, // 禁止重复的函数声明 80 'no-implied-eval': 2, // 禁止使用隐式eval 81 'no-inner-declarations': [2, 'functions'], // 禁止在块语句中使用声明(变量或函数) 82 'no-invalid-regexp': 2, // 禁止无效的正则表达式 83 'no-irregular-whitespace': 2, // 不能有不规则的空格 84 'no-iterator': 2, // 禁止使用__iterator__ 属性 85 'no-label-var': 2, // label名不能与var声明的变量名相同 86 'no-labels': [2, { allowLoop: false, allowSwitch: false }], // 禁止标签声明 87 'no-lone-blocks': 2, // 禁止不必要的嵌套块 88 'no-lonely-if': 'off', // 禁止-if-语句作为唯一语句出现在-else-语句块中 89 'no-mixed-spaces-and-tabs': 2, // 禁止混用tab和空格 90 'no-multi-spaces': 2, // 不能用多余的空格 91 'no-multi-str': 2, // 字符串不能用\换行 92 'no-multiple-empty-lines': [2, { max: 1 }], // 空行最多不能超过1行 93 'no-native-reassign': 2, // 不能重写native对象 94 'no-negated-in-lhs': 2, // in 操作符的左边不能有! 95 'no-new-object': 2, // 禁止使用new Object() 96 'no-new-require': 2, // 禁止使用new require 97 'no-new-symbol': 2, // 禁止使用new symbol 98 'no-new-wrappers': 2, // 禁止使用new创建包装实例,new String new Boolean new Number 99 'no-obj-calls': 2, // 不能调用内置的全局对象,比如Math() JSON() 100 'no-octal': 2, // 禁止使用八进制数字 101 'no-octal-escape': 2, // 禁止使用八进制转义序列 102 'no-path-concat': 2, // node中不能使用__dirname或__filename做路径拼接 103 'no-proto': 2, // 禁止使用__proto__属性 104 'no-redeclare': 2, // 禁止重复声明变量 105 'no-regex-spaces': 2, // 禁止在正则表达式字面量中使用多个空格 /foo bar/ 106 'no-return-assign': [2, 'except-parens'], // return 语句中不能有赋值表达式 107 'no-self-assign': 2, // 自我分配 108 'no-self-compare': 2, // 不能比较自身 109 'no-sequences': 2, // 禁止使用逗号运算符 110 'no-shadow-restricted-names': 2, // 严格模式中规定的限制标识符不能作为声明时的变量名使用 111 'no-spaced-func': 2, // 函数调用时 函数名与()之间不能有空格 112 'no-sparse-arrays': 2, // 禁止稀疏数组, [1,,2] 113 'no-this-before-super': 2, // 在调用super()之前不能使用this或super 114 'no-throw-literal': 2, // 禁止抛出字面量错误 throw "error" 115 'no-trailing-spaces': 2, // 一行结束后面不要有空格 116 'no-undef': 2, // 不能有未定义的变量 117 'no-undef-init': 2, // 变量初始化时不能直接给它赋值为undefined 118 'no-unexpected-multiline': 2, // 避免多行表达式 119 'no-unmodified-loop-condition': 2, // 检查引用是否在循环中被修改 120 'no-unneeded-ternary': [2, { defaultAssignment: false }], // 禁止可以在有更简单的可替代的表达式时使用三元操作符 121 'no-unreachable': 2, // 不能有无法执行的代码 122 'no-unsafe-finally': 2, // 禁止对关系运算符的左操作数使用否定操作符 123 'no-unused-vars': [2, { vars: 'all', args: 'none' }], // 不能有声明后未被使用的变量或参数 124 'no-useless-call': 2, // 禁止不必要的call和apply 125 'no-useless-computed-key': 2, // 没有必要使用带文字的计算属性 126 'no-useless-constructor': 2, // 可以在不改变类的工作方式的情况下安全地移除的类构造函数 127 'no-useless-escape': 0, // 禁用不必要的转义字符 128 'no-whitespace-before-property': 2, // 禁止属性前有空白 129 'no-with': 2, // 禁用with 130 'one-var': [2, { initialized: 'never' }], // 连续声明 131 'operator-linebreak': [2, 'after', { overrides: { '?': 'before', ':': 'before' } }], // 换行时运算符在行尾还是行首 132 'padded-blocks': [2, 'never'], // 块语句内行首行尾是否要空行 133 'space-before-blocks': [2, 'always'], // 不以新行开始的块{前面要不要有空格 134 'space-before-function-paren': [ 135 2, 136 { anonymous: 'never', named: 'never', asyncArrow: 'always' } 137 ], // 函数定义时括号前面要不要有空格 138 'space-in-parens': [2, 'never'], // 小括号里面要不要有空格 139 'space-infix-ops': 2, // 中缀操作符周围要不要有空格 140 'space-unary-ops': [2, { words: true, nonwords: false }], // 一元运算符的前/后要不要加空格 141 // 注释风格不要有空格什么的 142 'spaced-comment': [ 143 2, 144 'always', 145 { markers: ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ','] } 146 ], 147 'template-curly-spacing': [2, 'never'], // 要求或禁止模板字符串中的嵌入表达式周围空格的使用 148 'use-isnan': 2, // 禁止比较时使用NaN,只能用isNaN() 149 'valid-typeof': 2, // 必须使用合法的typeof的值 150 'wrap-iife': [2, 'any'], // 立即执行函数表达式的小括号风格 151 'yield-star-spacing': [2, 'both'], // 强制在 yield* 表达式中 * 周围使用空格 152 yoda: [2, 'never'], // 禁止尤达条件 153 154 'prefer-const': 2, // 首选const 155 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, // 禁用 debugger 156 'object-curly-spacing': [0, 'always', { objectsInObjects: false }], // 大括号内是否允许不必要的空格 157 'array-bracket-spacing': [2, 'never'], // 是否允许非空数组里面有多余的空格 158 159 'no-restricted-syntax': 0, // 禁止使用特定的语法 160 'no-loop-func': 0, // 禁止循环中存在函数 161 'consistent-return': 0, // 要求 return 语句要么总是指定返回的值,要么不指定 162 'import/prefer-default-export': 0, // 希望导出default,即export default xxx 163 164 quotes: [2, 'single', { avoidEscape: true, allowTemplateLiterals: true }], // 引号类型 `` "" '' 165 semi: [2, 'never'], // 语句强制分号结尾 166 'semi-spacing': [2, { before: false, after: true }] // 分号前后空格 167 } 168 }
注意事项:
1.在vue3中,规则vue/name-property--casing已经被vue/component-definition-name-casing代替,在使用前者会出错。
2.新增规则:'no-lonely-if': 'off', // 禁止-if-语句作为唯一语句出现在-else-语句块中。
3.修改规则:'space-before-function-paren': [2,{ anonymous: 'never', named: 'never', asyncArrow: 'always' }], // 函数定义时括号前面要不要有空格。
1.1.4.使用问题:
a)在VSCode下ESlint插件不生效,如何排查问题
1.确保eslint配置文件和node_modules在根目录下
2.确定安装了vscode eslint插件
3.确认package.json中安装了 eslint和eslint-plugin等
4.vscode 按下f1
5.输入eslint
6.选择ESLint: Show Output Channel查看eslint报错信息
7.按照错误信息,去解决
b)defineProps' is not defined
1.定位问题:defineProps 属于 Vue3 的规则校验,需要在 eslint-plugin-vue官方指南中寻找对应配置。
2.解决问题:打开.eslintrc.js文件,在env配置项中添加“'vue/setup-compiler-macros': true”
c)使用eslint格式化代码
1.在package.json的scripts属性里配置 格式化 命令
1.2.prettier
1.2.1.简要说明:它是一款强大的代码格式化工具,支持JavaScript、Typescript、Css、Scss、Less、JSX、Angular、Vue、GraphQL、JSON、Markdown等,基本上前端能用到的文件格式都可以搞定。
1.2.2.步骤过程:
a)安装prettier:npm i prettier -D
b)创建 Prettier 配置文件,在./src下创建.prettierrc文件,文件内容如下:

1 { 2 "useTabs": false, 3 "tabWidth": 2, 4 "printWidth": 100, 5 "singleQuote": true, 6 "trailingComma": "none", 7 "bracketSpacing": true, 8 "semi": false, 9 "endOfLine": "auto" 10 }
c)使用其命令格式化代码文件
# 格式化所有文件 (. 表示所有文件)
npx prettier --write .
d)使用VSCode编辑器,安装Prettier插件:Prettier - Code formatter
1.3.兼容问题
1.3.1.基于个人观点,其实使用eslint基本上已经满足需求了,使用这种组合方式反而会出现很多兼容性问题要解决。看到网友都推荐eslint+prettier组合,就记录一下吧。
1.3.2.解决eslint和prettier冲突
a)安装插件:npm i eslint-plugin-prettier eslint-config-prettier -D
b)修改.eslintrc.js配置文件,添加prettier插件

1 module.exports = { 2 ... 3 extends: [ 4 'plugin:vue/essential', 5 'airbnb-base', 6 'plugin:prettier/recommended' // 添加 prettier 插件 7 ], 8 ... 9 }
c)插件功能说明:
eslint-plugin-prettier 将 Prettier 的规则设置到 ESLint 的规则中。
eslint-config-prettier 关闭 ESLint 中与 Prettier 中会发生冲突的规则。
d)特别说明:即便配置上解决规则冲突的设置,还是会出现很多兼容性问题。
2.postcss
2.1.简要说明:postcss是一种对css编译的工具,类似于babel处理js,主要功能:1.使用下一代css语法,2.自动补全浏览器前缀,3.自动把px替换为rem,4.css代码压缩等。
2.2.步骤过程:
a).安装postcss和postcss-preset-env插件:npm install postcss postcss-preset-env -D
b).配置vite.config.js文件:

1 import { defineConfig } from 'vite' 2 import vue from '@vitejs/plugin-vue' 3 import postcssPresetEnv from 'postcss-preset-env' 4 5 // https://vitejs.dev/config/ 6 export default defineConfig({ 7 base: './', 8 publicDir: 'public', // 静态资源服务的文件夹 9 logLevel: 'info', // 控制台输出的级别 info 、warn、error、silent 10 clearScreen: true, // 设为false 可以避免 vite 清屏而错过在终端中打印某些关键信息 11 css: { 12 postcss: { 13 plugins: [postcssPresetEnv] 14 } 15 } 16 })
c).修改App.vue文件,编写测试示例:

1 <template> 2 <img alt="Vue logo" src="@/assets/logo.png" /> 3 4 <div class="title">Hello Vite!</div> 5 6 <router-view /> 7 </template> 8 9 <script> 10 11 export default { 12 name: 'App' 13 } 14 </script> 15 16 <style type="text/css"> 17 .title { 18 font-size: 30px; 19 color: yellow; 20 user-select: none; 21 } 22 </style>
2.3.查看效果:启动项目,命令:npm run dev。我们重点关注样式title的user-select: none;打开浏览器,你会看到user-select:none;多出了好几处有前缀的属性项,如:-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;
2.4.注意事项:
2.4.1.上述过程中,使用了postcss-preset-env与postcss搭配,你也可以使用其他的插件,如autoprefixer等
2.4.2.由于vite自身已经集成了postcss,所以不要在根目录再创建postcss.config.js文件。如果创建了这个文件可能还会出错,因为vite是基于esmodule管理的,所有文件都会转换为module。
3.svgIcon
3.1.简要说明:svg图标是基于xml定义可缩放的矢量图形,由于其不会图形质量不会因分辨率导致失真,所以被大量应用于程序编码中。
3.2.步骤过程:
a)创建目录和文件结构:
目录:./src/icons
目录:./src/icons/svg
文件:./src/icons/index.vue
文件:./src/icons/svgBuilder.js
b)在./src/icons/svg目录里添加测试图标login-user.svg
c)安装svg插件:npm install svg-sprite-loader -D
d)编写./src/icons/index.vue文件

1 <template> 2 <div 3 v-if="isExternal" 4 :style="styleExternalIcon" 5 class="svg-external-icon svg-icon" 6 v-on="$attrs" 7 /> 8 <svg v-else :class="svgClass" aria-hidden="true" v-on="$attrs"> 9 <use :xlink:href="iconName" /> 10 </svg> 11 </template> 12 <script> 13 /** 14 * 名称:SvgIcon 15 * @param iconClass String required 16 * @param className String 17 * 依赖:src/icons/svgBuilder.js 需要在vite中配置 18 * 使用方式: 19 * 在 template 中使用 <svg-icon icon-class="login-user"/> 20 */ 21 export default { 22 name: 'SvgIcon', 23 props: { 24 iconClass: { 25 type: String, 26 required: true 27 }, 28 className: { 29 type: String, 30 default: '' 31 } 32 }, 33 computed: { 34 isExternal() { 35 return /^(https?:|mailto:|tel:)/.test(this.iconClass) 36 }, 37 iconName() { 38 return `#icon-${this.iconClass}` 39 }, 40 svgClass() { 41 if (this.className) { 42 return `svg-icon ${this.className}` 43 } 44 return 'svg-icon' 45 }, 46 styleExternalIcon() { 47 return { 48 mask: `url(${this.iconClass}) no-repeat 50% 50%`, 49 '-webkit-mask': `url(${this.iconClass}) no-repeat 50% 50%` 50 } 51 } 52 } 53 } 54 </script> 55 56 <style scoped> 57 .svg-icon { 58 width: 1em; 59 height: 1em; 60 vertical-align: -0.15em; 61 fill: currentColor; 62 overflow: hidden; 63 } 64 65 .svg-external-icon { 66 background-color: currentColor; 67 mask-size: cover !important; 68 display: inline-block; 69 } 70 </style>
e)引入./src/icons/index.vue文件,在main.js文件中

1 import * as Vue from 'vue' 2 3 import SvgIcon from './icons/index.vue' 4 5 import App from './App.vue' 6 7 const app = Vue.createApp(App) 8 app.component('svg-icon', SvgIcon) 9 app.mount('#app')
f)编写./src/icons/svgBuilder.js文件,需要安装fs模块:npm install fs

1 import { readFileSync, readdirSync } from 'fs' 2 3 let idPerfix = '' 4 const svgTitle = /<svg([^>+].*?)>/ 5 const clearHeightWidth = /(width|height)="([^>+].*?)"/g 6 const hasViewBox = /(viewBox="[^>+].*?")/g 7 const clearReturn = /(\r)|(\n)/g 8 9 function svgFind(dir) { 10 const svgRes = [] 11 const dirents = readdirSync(dir, { 12 withFileTypes: true 13 }) 14 for (const dirent of dirents) { 15 if (dirent.isDirectory()) { 16 svgRes.push(...svgFind(`${dir + dirent.name}/`)) 17 } else { 18 const svg = readFileSync(dir + dirent.name) 19 .toString() 20 .replace(clearReturn, '') 21 .replace(svgTitle, ($1, $2) => { 22 let width = 0 23 let height = 0 24 let content = $2.replace(clearHeightWidth, (s1, s2, s3) => { 25 if (s2 === 'width') { 26 width = s3 27 } else if (s2 === 'height') { 28 height = s3 29 } 30 return '' 31 }) 32 if (!hasViewBox.test($2)) { 33 content += `viewBox="0 0 ${width} ${height}"` 34 } 35 return `<symbol id="${idPerfix}-${dirent.name.replace('.svg', '')}" ${content}>` 36 }) 37 .replace('</svg>', '</symbol>') 38 svgRes.push(svg) 39 } 40 } 41 return svgRes 42 } 43 44 export const svgBuilder = (path, perfix = 'icon') => { 45 if (path === '') return 46 idPerfix = perfix 47 const res = svgFind(path) 48 49 return { 50 name: 'svg-transform', 51 transformIndexHtml(html) { 52 return html.replace( 53 '<body>', 54 ` 55 <body> 56 <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="position: absolute; width: 0; height: 0"> 57 ${res.join('')} 58 </svg> 59 ` 60 ) 61 } 62 } 63 }
g)配置vite.config.js文件,使svgBuilder.js文件功能启用

1 import { defineConfig } from 'vite' 2 import vue from '@vitejs/plugin-vue' 3 import { svgBuilder } from './src/icons/svgBuilder' 4 5 // https://vitejs.dev/config/ 6 export default defineConfig({ 7 base: './', 8 plugins: [ 9 vue(), 10 svgBuilder('./src/icons/svg/') // 这里已经将src/icons/svg/下的svg全部导入 11 ] 12 })
h)修改App.vue文件,编写测试示例

1 <template> 2 <img alt="Vue logo" src="@/assets/logo.png" /> 3 <div> 4 <svg-icon icon-class="login-user" /> 5 </div> 6 <router-view /> 7 </template> 8 9 <script> 10 export default { 11 name: 'App' 12 } 13 </script>
3.3.查看效果:启动项目,命令:npm run dev。
二、总结
通过《vite+vue3》和《vite+vue3 2》两篇文章的记录,基本上把web开发中使用的基础模块和常用插件都记录了。
三、参考信息
1.https://blog.csdn.net/z591102/article/details/106787171/
2.https://juejin.cn/post/6991233820410249252
3.https://juejin.cn/post/6973288527802925092
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix