shuisheng 2 éve
commit
b520caa724
67 módosított fájl, 2813 hozzáadás és 0 törlés
  1. 3 0
      .browserslistrc
  2. 10 0
      .commitlintrc.js
  3. 17 0
      .editorconfig
  4. 6 0
      .eslintignore
  5. 178 0
      .eslintrc.js
  6. 1 0
      .gitattributes
  7. 6 0
      .gitignore
  8. 4 0
      .husky/commit-msg
  9. 4 0
      .husky/pre-commit
  10. 11 0
      .lintstagedrc.js
  11. 4 0
      .npmrc
  12. 6 0
      .prettierignore
  13. 46 0
      .prettierrc.js
  14. 26 0
      .versionrc.js
  15. 4 0
      .vscode/extensions.json
  16. 41 0
      .vscode/settings.json
  17. 55 0
      README.md
  18. 27 0
      babel.config.js
  19. 76 0
      build.sh
  20. 72 0
      handleGiteeJenkins.js
  21. 102 0
      package.json
  22. 15 0
      postcss.config.js
  23. BIN
      public/Hololo.jpeg
  24. BIN
      public/favicon.ico
  25. 29 0
      public/index.html
  26. 27 0
      script/TerminalPrintPlugin.ts
  27. 423 0
      script/config/webpack.common.ts
  28. 165 0
      script/config/webpack.dev.ts
  29. 203 0
      script/config/webpack.prod.ts
  30. 39 0
      script/constant.ts
  31. 13 0
      script/utils/chalkTip.ts
  32. 5 0
      script/utils/path.ts
  33. 23 0
      src/App.vue
  34. BIN
      src/assets/font/MiSans-Normal.ttf
  35. BIN
      src/assets/img/author.jpg
  36. 8 0
      src/assets/oldcss/animate/flash-img/index.css
  37. 40 0
      src/assets/oldcss/animate/flash-img/index.scss
  38. 8 0
      src/assets/oldcss/animate/flash-txt/index.css
  39. 37 0
      src/assets/oldcss/animate/flash-txt/index.scss
  40. 11 0
      src/assets/oldcss/animate/loading-rotate/index.css
  41. 73 0
      src/assets/oldcss/animate/loading-rotate/index.scss
  42. 16 0
      src/assets/oldcss/animate/loading-size/index.css
  43. 79 0
      src/assets/oldcss/animate/loading-size/index.scss
  44. 79 0
      src/assets/oldcss/common.scss
  45. 21 0
      src/assets/oldcss/constant.scss
  46. 11 0
      src/assets/oldcss/global.scss
  47. 168 0
      src/assets/oldcss/mixin.scss
  48. 9 0
      src/assets/oldcss/utils.scss
  49. 34 0
      src/components/Baby/index.vue
  50. 32 0
      src/components/Card/index.vue
  51. 158 0
      src/constant.ts
  52. 1 0
      src/interface.ts
  53. 4 0
      src/main.scss
  54. 18 0
      src/main.ts
  55. 71 0
      src/network/websocket.ts
  56. 31 0
      src/router/index.ts
  57. 10 0
      src/shims-vue.d.ts
  58. 11 0
      src/showBilldVersion.ts
  59. 14 0
      src/store/app/index.ts
  60. 4 0
      src/store/index.ts
  61. 26 0
      src/store/user/index.ts
  62. 23 0
      src/utils/index.ts
  63. 30 0
      src/views/about/about.vue
  64. 52 0
      src/views/home/home.vue
  65. 22 0
      src/views/login/login.vue
  66. 51 0
      tsconfig.json
  67. 20 0
      windi.config.ts

+ 3 - 0
.browserslistrc

@@ -0,0 +1,3 @@
+> 1%
+last 2 version
+not dead

+ 10 - 0
.commitlintrc.js

@@ -0,0 +1,10 @@
+console.log(
+  '\x1B[0;37;44m INFO \x1B[0m',
+  '\x1B[0;;34m ' +
+    `读取了: ${__filename.slice(__dirname.length + 1)}` +
+    ' \x1B[0m'
+);
+
+module.exports = {
+  extends: ['@commitlint/config-conventional'],
+};

+ 17 - 0
.editorconfig

@@ -0,0 +1,17 @@
+# https://editorconfig.org
+# 控制编辑器代码规范,如按下回车的时候,缩进几个空格等等。
+# 最顶层的EditorConfig文件
+root = true
+
+[*]
+charset = utf-8
+indent_style = space # 缩进样式为空格,也可以设置成:tab
+indent_size = 2 # 一个缩进2个空格
+# tab_width默认为indent_size的值,通常不需要指定。
+end_of_line = lf  # 结尾符,也可以设置成:crlf
+insert_final_newline = true # 设置为true以确保文件在保存时以换行符结尾,设置为false以确保文件不以换行符号结尾。
+trim_trailing_whitespace = true # 设置为true以删除换行符之前的任何空白字符,设置为false以确保不会。
+
+[*.md]
+insert_final_newline = false
+trim_trailing_whitespace = false

+ 6 - 0
.eslintignore

@@ -0,0 +1,6 @@
+node_modules
+pnpm-lock.yaml
+dist
+components.d.ts
+.eslintcache
+.DS_Store

+ 178 - 0
.eslintrc.js

@@ -0,0 +1,178 @@
+console.log(
+  '\x1B[0;37;44m INFO \x1B[0m',
+  '\x1B[0;;34m ' +
+    `读取了: ${__filename.slice(__dirname.length + 1)}` +
+    ' \x1B[0m'
+);
+
+module.exports = {
+  root: true,
+  settings: {
+    // 'import/extensions': ['.js', '.jsx', '.ts', '.tsx'],
+  },
+  env: {
+    browser: true,
+    node: true,
+  },
+  extends: [
+    // 'airbnb-base', // airbnb的eslint规范,它会对import和require进行排序,挺好的。如果不用它的话,需要在env添加node:true
+    'eslint:recommended',
+    'plugin:import/recommended',
+    'plugin:vue/vue3-recommended',
+    '@vue/eslint-config-typescript',
+    '@vue/eslint-config-prettier',
+  ],
+  parserOptions: {
+    ecmaVersion: 2020,
+    tsconfigRootDir: __dirname, // https://typescript-eslint.io/docs/linting/typed-linting
+    project: ['./tsconfig.json'], // https://typescript-eslint.io/docs/linting/typed-linting
+  },
+  plugins: ['import'],
+  // overrides: [],
+  // rules会覆盖extends里面的规则(https://eslint.org/docs/latest/user-guide/migrating-to-6.0.0#-overrides-in-an-extended-config-file-can-now-be-overridden-by-a-parent-config-file)
+  // rules里面的规则不会对overrides里面的文件生效
+  rules: {
+    /**
+     * 0 => off
+     * 1 => warn
+     * 2 => error
+     */
+    'no-shadow': 0, // 禁止变量声明与外层作用域的变量同名
+    'class-methods-use-this': 0, // 类方法如果不使用this的话会报错
+    'no-console': 0, // 此规则不允许调用console对象的方法。
+    'spaced-comment': ['error', 'always', { exceptions: ['-', '+'] }], // 该规则强制注释中 // 或 /* 后空格的一致性
+    'no-var': 2, // 要求let或const代替var
+    camelcase: [
+      'error',
+      { properties: 'never' }, // properties默认always,即检查属性名;可以设置为never,即不检查属性名
+    ], // 强制执行驼峰命名约定
+    'no-underscore-dangle': 2, // 此规则不允许在标识符中使用悬空下划线。
+    'no-param-reassign': 2, // 禁止对 function 的参数进行重新赋值
+    'no-nested-ternary': 2, // 禁止嵌套三元
+    'no-plusplus': 2, // 禁用一元操作符 ++ 和 --
+    'no-unused-vars': 0, // 禁止出现未使用过的变量
+    'vars-on-top': 2, // 要求所有的 var 声明出现在它们所在的作用域顶部
+    'prefer-const': 2, // 要求使用 const 声明那些声明后不再被修改的变量
+    'prefer-template': 2, // 要求使用模板字符串代替字符串连接
+    'new-cap': 2, // 要求构造函数名称以大写字母开头
+    'no-restricted-syntax': [
+      // 禁用一些语法
+      'error',
+      // 'ForInStatement',
+      // 'ForOfStatement',
+      {
+        selector: 'ForInStatement',
+        /**
+         * 用 map() / every() / filter() / find() / findIndex() / reduce() / some() / ... 遍历数组,
+         * 和使用 Object.keys() / Object.values() / Object.entries() 迭代你的对象生成数组。
+         * 拥有返回值得纯函数比这个更容易解释
+         */
+        message:
+          'for in会迭代遍历原型链(__proto__),建议使用map/every/filter等遍历数组,使用Object.{keys,values,entries}等遍历对象',
+      },
+      {
+        selector: 'ForOfStatement',
+        message:
+          '建议使用map/every/filter等遍历数组,使用Object.{keys,values,entries}等遍历对象',
+      },
+    ], // https://github.com/BingKui/javascript-zh#%E8%BF%AD%E4%BB%A3%E5%99%A8%E5%92%8C%E5%8F%91%E7%94%9F%E5%99%A8
+    'no-iterator': 2, // 禁止使用__iterator__迭代器
+    'require-await': 2, // 禁止使用不带 await 表达式的 async 函数
+    'no-empty': 2, // 禁止空块语句
+    'guard-for-in': 2, // 要求for-in循环包含if语句
+    'global-require': 2, // 此规则要求所有调用require()都在模块的顶层,此规则在 ESLint v7.0.0中已弃用。请使用 中的相应规则eslint-plugin-node:https://github.com/mysticatea/eslint-plugin-node
+    'no-unused-expressions': [
+      2,
+      {
+        allowShortCircuit: true, // 允许短路
+        allowTernary: true, // 允许三元
+      },
+    ], // 禁止未使用的表达式,即let a = true && console.log(1)允许,但是true && console.log(1)不行
+    'object-shorthand': ['error', 'always'], // (默认)希望尽可能使用速记。var foo = {x:x};替换为var foo = {x};
+    'no-useless-escape': 2, // 禁止不必要的转义字符
+
+    // eslint-plugin-import插件
+    // https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/order.md
+    'import/order': [
+      'error',
+      {
+        groups: [
+          'builtin', // 如:import fs from 'fs';
+          'external', // 如:import _ from 'lodash';
+          'internal', // 如:import foo from 'src/foo';
+          'parent', // 如:import foo from '../foo';
+          'sibling', // 如:import bar from './bar';
+          // ['sibling', 'parent'],
+          // ['parent', 'sibling'],
+          'index', // 如:import main from './';
+          'object', // 如:import log = console.log;
+          'type', // 如:import type { Foo } from 'foo';
+        ],
+        pathGroups: [
+          {
+            pattern: '@/**',
+            group: 'internal',
+          },
+        ],
+        'newlines-between': 'always', // 强制或禁止导入组之间的新行
+        // 根据导入路径以字母顺序排列每个组中的顺序
+        alphabetize: {
+          order: 'asc', // 使用asc按升序排序,使用desc按降序排序(默认值:ignore)。
+          caseInsensitive: false, // 使用true忽略大小写,而false考虑大小写(默认值:false)。
+          orderImportKind: 'asc', // 使用asc以升序对各种导入类型进行排序,例如以type或typeof为前缀的导入,具有相同的导入路径。使用desc按降序排序(默认值:忽略)
+        },
+      },
+    ],
+    'import/newline-after-import': 2, // 强制在最后一个顶级导入语句或 require 调用之后有一个或多个空行
+    'import/no-extraneous-dependencies': 2, // 禁止导入未在package.json中声明的外部模块。
+    /**
+     * import/named
+     * 在import { version } from 'vuex';的时候会验证vuex有没有具名导出version,
+     * 但是在vue3的时候,import { defineComponent } from 'vue';会报错defineComponent not found in 'vue'
+     * 因此vue3项目关闭该规则
+     */
+    'import/named': 0,
+    /**
+     * a.js
+     * export const version = '1.0.0';
+     * export const bar = { name: 'bar', version };
+     * export default bar;
+     * b.js
+     * import bar from './a';
+     * console.log(bar.version); // 检测到你使用的version有具名导出,import/no-named-as-default-member就会提示`import {version} from './a'`
+     */
+    'import/no-named-as-default-member': 1, // https://github.com/import-js/eslint-plugin-import/blob/v2.26.0/docs/rules/no-named-as-default-member.md
+    'import/prefer-default-export': 0, // 当模块只有一个导出时,更喜欢使用默认导出而不是命名导出。
+    'import/extensions': 0, // 确保在导入路径中一致使用文件扩展名。在js/ts等文件里引其他文件都不能带后缀(比如.css和.jpg),因此关掉
+    'import/no-unresolved': 0, // 不能解析带别名的路径的模块,但实际上是不影响代码运行的。找不到解决办法,暂时关掉。
+    /**
+     * a.js
+     * export const bar = 'bar';
+     * export const foo = 'foo';
+     * export default foo;
+     * b.js
+     * import bar from './a'; // import/no-named-as-default规则会报错,因为import/no-named-as-default规则误以为你将具名导出的bar作为了默认导出来使用,但是实际上可能我就是想用默认导出的foo
+     * // import barr from './a'; // 改个名字import/no-named-as-default规则就不会报错了。
+     * // 不幸的是,React + Redux 是最常见的场景。但是,还有很多其他情况,HOC 会迫使开发人员关闭此规则。https://github.com/import-js/eslint-plugin-import/issues/544#issuecomment-245082471
+     */
+    'import/no-named-as-default': 0, // https://github.com/import-js/eslint-plugin-import/blob/v2.26.0/docs/rules/no-named-as-default.md
+
+    // @typescript-eslint插件
+    '@typescript-eslint/restrict-template-expressions': 2, // 强制模板文字表达式为string类型。即const a = {};console.log(`${a}`);会报错
+    '@typescript-eslint/no-unused-vars': 2,
+    '@typescript-eslint/no-floating-promises': 1, // 要求适当处理类似 Promise 的语句。即将await或者return Promise,或者对promise进行.then或者.catch
+    '@typescript-eslint/no-explicit-any': 0, // 不允许定义any类型。即let a: any;会报错
+    '@typescript-eslint/no-non-null-assertion': 0, // 禁止使用非空断言(后缀运算符!)。即const el = document.querySelector('.app');console.log(el!.tagName);会报错
+    '@typescript-eslint/ban-ts-comment': 0, // 禁止使用@ts-<directive>注释
+    '@typescript-eslint/no-unsafe-assignment': 0, // 不允许将具有类型的值分配any给变量和属性。即const a: any = {};const b = a;会报错
+    '@typescript-eslint/no-unsafe-argument': 0, // 不允许用any类型的值调用一个函数。即let a: any;Object.keys(a);会报错
+    '@typescript-eslint/no-unsafe-member-access': 0, // 不允许对类型为any的值进行成员访问。即const a: any = [];console.log(a[0]);会报错
+    '@typescript-eslint/no-unsafe-return': 0, // 不允许从一个函数中返回一个类型为any的值
+    '@typescript-eslint/no-unsafe-call': 0, // 不允许调用any类型的值
+    '@typescript-eslint/no-var-requires': 0, // 即不允许var foo = require('foo');。但是允许import foo = require('foo');
+    '@typescript-eslint/restrict-plus-operands': 0, // 要求加法的两个操作数是相同的类型并且是bigint, number, 或string。即const a = '1';console.log(a + 1);会报错
+
+    // vueeslint插件
+    'vue/multi-word-component-names': 0,
+  },
+};

+ 1 - 0
.gitattributes

@@ -0,0 +1 @@
+* text=auto

+ 6 - 0
.gitignore

@@ -0,0 +1,6 @@
+node_modules
+pnpm-lock.yaml
+dist
+components.d.ts
+.eslintcache
+.DS_Store

+ 4 - 0
.husky/commit-msg

@@ -0,0 +1,4 @@
+#!/bin/sh
+. "$(dirname "$0")/_/husky.sh"
+
+npx --no-install commitlint --edit $1

+ 4 - 0
.husky/pre-commit

@@ -0,0 +1,4 @@
+#!/bin/sh
+. "$(dirname "$0")/_/husky.sh"
+
+pnpm exec lint-staged

+ 11 - 0
.lintstagedrc.js

@@ -0,0 +1,11 @@
+console.log(
+  '\x1B[0;37;44m INFO \x1B[0m',
+  '\x1B[0;;34m ' +
+    `读取了: ${__filename.slice(__dirname.length + 1)}` +
+    ' \x1B[0m'
+);
+
+module.exports = {
+  // 只对这几种格式的代码进行prettier美化
+  '*.{js,jsx,ts,tsx,vue}': ['prettier --write'],
+};

+ 4 - 0
.npmrc

@@ -0,0 +1,4 @@
+# 将所有依赖提升到最外层
+shamefully-hoist=true
+# 设置淘宝镜像
+registry=https://registry.npmmirror.com/

+ 6 - 0
.prettierignore

@@ -0,0 +1,6 @@
+node_modules
+pnpm-lock.yaml
+dist
+components.d.ts
+.eslintcache
+.DS_Store

+ 46 - 0
.prettierrc.js

@@ -0,0 +1,46 @@
+console.log(
+  '\x1B[0;37;44m INFO \x1B[0m',
+  '\x1B[0;;34m ' +
+    `读取了: ${__filename.slice(__dirname.length + 1)}` +
+    ' \x1B[0m'
+);
+
+module.exports = {
+  bracketSpacing: true, // 默认为true。即要求:{ foo: bar };可改为false,即要求{foo: bar}
+  singleQuote: true, // 默认为false。即要求:const a = "1";可改为true,即要求const a = '1'
+  semi: true, // 默认值true,即要求在所有代码语句的末尾添加分号;可改为false,即要求仅在可能导致 ASI 失败的行的开头添加分号。
+  singleAttributePerLine: true, // 默认false。即在HTML、Vue和JSX中不要每行强制执行单个属性;可改为true,即要求每行强制执行单个属性。
+
+  /**
+   * jsxBracketSameLine
+   * 注意是多行,如果是类似这种:<a>1</a>,基本不会触发换行,因此也就不会触发这个bracketSameLine
+   * 但是如果是类似这种:<a a="1 "b="2">1</a>,它有多个属性,或者说他的一个属性值很长,可能会导致换行,
+   * 如果换行了,那么就会触发bracketSameLine,将<a a="1 "b="2">最后的>单独放在一行或者最后一行的末尾
+   */
+  bracketSameLine: false, // 默认为false。即将多行HTML(HTML、JSX、Vue、Angular)元素的 > 单独放在下一行;可改为true,即将 > 放在最后一行的末尾。
+  // jsxBracketSameLine: false, // 此选项已在v2.4.0中弃用,使用bracketSameLine替换,https://prettier.io/blog/2021/09/09/2.4.0.html
+
+  /**
+   * trailingComma
+   * https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Trailing_commas
+   * 尾后逗号 (有时叫做“终止逗号”)在向 JavaScript 代码添加元素、参数、属性时十分有用。
+   * 如果你想要添加新的属性,并且上一行已经使用了尾后逗号,你可以仅仅添加新的一行,而不需要修改上一行。
+   * 这使得版本控制的代码比较(diff)更加清晰,代码编辑过程中遇到的麻烦更少。
+   */
+  trailingComma: 'es5', // 默认值在v2.0.0中none更改为es5。即在ES5中有效的尾随逗号(对象、数组等)。可选:"none":没有尾随逗号;"all":尽可能尾随逗号
+
+  /**
+   * printWidth
+   * 如果设置了printWidth值,则以设置的printWidth值为准
+   * 如果没有设置printWidth值,且.editorconfig文件有设置max_line_length值,则使用.editorconfig文件的max_line_length
+   */
+  printWidth: 80, // 默认80,printWidth不是硬性的允许行长度上限,不要试图将 printWidth 当作 ESLint 的max-len 来使用——它们不一样
+
+  /**
+   * tabWidth
+   * 如果设置了tabWidth值,则以设置的tabWidth值为准
+   * 如果没有设置tabWidth值,且.editorconfig文件有设置indent_size或者tab_width值,则使用.editorconfig文件的indent_size或者tab_width
+   */
+  tabWidth: 2, // 指定每个缩进级别的空格数。
+  // parser: 'babel', // 指定要使用的解析器。Prettier 会自动从输入文件路径推断解析器,因此您不必更改此设置。
+};

+ 26 - 0
.versionrc.js

@@ -0,0 +1,26 @@
+console.log(
+  '\x1B[0;37;44m INFO \x1B[0m',
+  '\x1B[0;;34m ' +
+    `读取了: ${__filename.slice(__dirname.length + 1)}` +
+    ' \x1B[0m'
+);
+
+module.exports = {
+  types: [
+    { type: 'feat', section: '✨ 新特性', hidden: false },
+    { type: 'fix', section: '🐛 Bug修复', hidden: false },
+    { type: 'docs', section: '📝 文档更改', hidden: false },
+    { type: 'style', section: '🎨 样式更改', hidden: false },
+    { type: 'refactor', section: '🔨 代码重构', hidden: false },
+    {
+      type: 'perf',
+      section: '⚡️ 优化性能',
+      hidden: false,
+    },
+    { type: 'test', section: '🧪 测试', hidden: false },
+    { type: 'build', section: '🚀 构建', hidden: false },
+    { type: 'ci', section: '👷 CI', hidden: false },
+    { type: 'chore', section: '🏗 其他', hidden: false },
+    { type: 'revert', section: '⏪ 回退', hidden: false },
+  ],
+};

+ 4 - 0
.vscode/extensions.json

@@ -0,0 +1,4 @@
+{
+  "recommendations": ["vue.volar"],
+  "unwantedRecommendations": ["octref.vetur"]
+}

+ 41 - 0
.vscode/settings.json

@@ -0,0 +1,41 @@
+{
+  // 指定行尾序列为\n(LF)或者\r\n(CRLF)或者auto
+  // "files.eol": "\n",
+
+  // 在保存时格式化
+  "editor.formatOnSave": true,
+
+  // 保存时进行一些操作
+  "editor.codeActionsOnSave": {
+    "source.fixAll.eslint": true, // 运行eslint
+    "source.organizeImports": true // 整理import语句(包括import的成员),以及会删掉未使用的导入,注意:会删掉declare global {import utils from 'billd-utils';}的import utils from 'billd-utils';
+    // "source.sortImports": true // 对您的导入进行排序,然而,与organizeImports不同,它不会删除任何未使用的导入,也不会对import里面的成员进行排序
+  },
+
+  // "eslint.autoFixOnSave": true, // 废弃,使用editor.codeActionsOnSave替代
+
+  // Path Autocomplete,这个插件能够支持路径补全,默认vsc默认的路径提示可能不会提示一些css或者jpg等资源,用这个插件可以完善vscode的路径提示
+  "path-autocomplete.pathMappings": {
+    "@": "${folder}/src",
+    "script": ["${folder}/src/script"]
+  },
+
+  // 别名路径跳转,这个插件可以完善vscode的跳转
+  "alias-skip.allowedsuffix": [
+    "css",
+    "less",
+    "sass",
+    "scss",
+    "jpg",
+    "jpeg",
+    "png",
+    "webp",
+    "gif",
+    "svg",
+    "js",
+    "jsx",
+    "ts",
+    "tsx",
+    "vue"
+  ]
+}

+ 55 - 0
README.md

@@ -0,0 +1,55 @@
+# 简介
+
+> 主要实现了 vuecli5 的大部分功能
+
+- [x] 基于 vue3 + webpack5
+- [x] 路由管理:vue-router4.x
+- [x] 状态管理:pinia2.x
+- [x] css 处理:scss + windicss(可选)
+- [x] 代码规范:eslint + prettier
+- [x] 项目规范:husky + commitizen + commitlint + lintstaged
+
+- [x] 支持热更新、typescript、路由懒加载
+
+> 一些相关的配置(如 eslint、windicss、outputStaticUrl 等)暴露在 script/constant.ts 了
+
+# 安装依赖
+
+更新 billd 依赖:
+
+```bash
+pnpm i billd-utils@latest billd-scss@latest billd-html-webpack-plugin@latest billd-deploy@latest
+```
+
+```bash
+pnpm install
+```
+
+# 项目运行
+
+```bash
+pnpm run start
+```
+
+script/constant.ts 里的 outputStaticUrl 如果是'/'的话,默认就运行在 [http://localhost:8000/](http://localhost:8000/),如果 8000 端口被占用了,会自动递增+1
+script/constant.ts 里的 outputStaticUrl 如果是'/aaa/'的话,默认就运行在 [http://localhost:8000/aaa/](http://localhost:8000/aaa/),如果 8000 端口被占用了,会自动递增+1
+
+> 项目启动完成后,终端会打印调试地址,不必担心调试地址是什么~
+
+# 项目打包
+
+```bash
+pnpm run build
+```
+
+# git 提交
+
+```bash
+pnpm run cz
+```
+
+# 内置第三方包
+
+- [billd-utils](https://github.com/galaxy-s10/billd-utils)
+- [billd-scss](https://github.com/galaxy-s10/billd-scss),已在 sass-loader 里配置了 additionalData: `@use 'billd-scss/src/index.scss' as *;`
+- [billd-html-webpack-plugin](https://github.com/galaxy-s10/billd-html-webpack-plugin),已在 webpack 配置里使用了该插件

+ 27 - 0
babel.config.js

@@ -0,0 +1,27 @@
+console.log(
+  '\x1B[0;37;44m INFO \x1B[0m',
+  '\x1B[0;;34m ' +
+    `读取了: ${__filename.slice(__dirname.length + 1)}` +
+    ' \x1B[0m'
+);
+
+module.exports = {
+  presets: [
+    [
+      '@babel/preset-env',
+      {
+        /**
+         * useBuiltIns:
+         * false: 默认值就是false,不用任何的polyfill相关的代码
+         * usage: 代码中需要哪些polyfill, 就引用相关的api
+         * entry: 手动在入口文件中导入 core-js/regenerator-runtime, 根据目标浏览器引入所有对应的polyfill
+         */
+        useBuiltIns: 'usage',
+        corejs: 3,
+        modules: 'auto', // modules设置成commonjs后,路由懒加载就没了。
+        // modules: 'commonjs', // https://github.com/vuejs/vue-cli/blob/HEAD/packages/@vue/babel-preset-app/index.js#L226
+      },
+    ],
+  ],
+  plugins: ['@babel/plugin-syntax-dynamic-import'],
+};

+ 76 - 0
build.sh

@@ -0,0 +1,76 @@
+#!/usr/bin/env bash
+###
+# Author: shuisheng
+# Date: 2023-03-19 12:17:20
+# Description:
+# Email: 2274751790@qq.com
+# FilePath: /webrtc-live/build.sh
+# Github: https://github.com/galaxy-s10
+# LastEditors: shuisheng
+# LastEditTime: 2023-03-19 12:22:57
+###
+# 生成头部文件快捷键:ctrl+cmd+i
+
+# 静态部署的项目,一般流程是在jenkins里面执行build.sh进行构建,
+# 构建完成后会连接ssh,执行/node/sh/frontend.sh,frontend.sh会将构建的完成资源复制到/node/xxx。
+# 复制完成后,frontend.sh会执行清除buff/cache操作
+
+# node项目,一般流程是在jenkins里面执行build.sh进行构建,
+# 构建完成后会连接ssh,执行/node/sh/node.sh,node.sh会将构建的完成资源复制到/node/xxx,并且执行/node/xxx/pm2.sh。
+# 最后,node.sh会执行清除buff/cache操作
+
+# 注意:JOBNAME=$1,这个等号左右不能有空格!
+JOBNAME=$1      #约定$1为任务名
+ENV=$2          #约定$2为环境
+WORKSPACE=$3    #约定$3为Jenkins工作区
+PORT=$4         #约定$4为端口号
+TAG=$5          #约定$5为git标签
+PUBLICDIR=/node #约定公共目录为/node
+
+echo 删除node_modules:
+rm -rf node_modules
+
+echo 查看node版本:
+node -v
+
+echo 查看npm版本:
+npm -v
+
+echo 设置npm淘宝镜像:
+npm config set registry https://registry.npm.taobao.org/
+
+echo 查看当前npm镜像:
+npm get registry
+
+if ! type pnpm >/dev/null 2>&1; then
+  echo 'pnpm未安装,先全局安装pnpm'
+  npm i pnpm -g
+else
+  echo 'pnpm已安装'
+fi
+
+echo 查看pnpm版本:
+pnpm -v
+
+echo 设置pnpm淘宝镜像:
+pnpm config set registry https://registry.npm.taobao.org/
+pnpm config set @billd:registry http://registry.hsslive.cn/
+
+echo 查看当前pnpm镜像:
+pnpm config get registry
+pnpm config get @billd:registry
+
+echo 开始安装依赖:
+pnpm install
+
+if [ $ENV = 'beta' ]; then
+  echo 开始构建测试环境:
+elif [ $ENV = 'preview' ]; then
+  echo 开始构建预发布环境:
+elif [ $ENV = 'prod' ]; then
+  echo 开始构建正式环境:
+else
+  echo 开始构建$ENV环境:
+fi
+
+npx cross-env VUE_APP_RELEASE_PROJECT_NAME=$JOBNAME VUE_APP_RELEASE_PROJECT_ENV=$ENV webpack --config ./script/config/webpack.common.ts --env production

+ 72 - 0
handleGiteeJenkins.js

@@ -0,0 +1,72 @@
+// WARN 该文件只是方便我将当前项目复制一份到我电脑的另一个位置(gitee私有仓库的位置),其他人不需要管这个文件~
+const fs = require('fs');
+const path = require('path');
+const { execSync } = require('child_process');
+
+const allFile = [];
+const ignore = ['.DS_Store', '.git', 'node_modules', 'dist'];
+const localDir = path.resolve(__dirname);
+const giteeDir = path.resolve(__dirname, '../../jenkins/billd-live');
+
+const dir = fs.readdirSync(localDir).filter((item) => {
+  if (ignore.includes(item)) {
+    return false;
+  }
+  return true;
+});
+
+function findFile(inputDir) {
+  for (let i = 0; i < inputDir.length; i += 1) {
+    const file = inputDir[i];
+    const filePath = `${localDir}/${file}`;
+    const stat = fs.statSync(filePath);
+    const isDir = stat.isDirectory();
+    if (!isDir) {
+      allFile.push(filePath);
+    } else {
+      findFile(fs.readdirSync(filePath).map((key) => `${file}/${key}`));
+    }
+  }
+}
+
+function putFile() {
+  for (let i = 0; i < allFile.length; i += 1) {
+    const file = allFile[i];
+    const arr = [];
+    const githubFile = file.replace(localDir, '');
+    const githubFileArr = githubFile.split('/').filter((item) => item !== '');
+    githubFileArr.forEach((item) => {
+      if (arr.length) {
+        arr.push(path.resolve(arr[arr.length - 1], item));
+      } else {
+        arr.push(path.resolve(giteeDir, item));
+      }
+    });
+    arr.forEach((item, index) => {
+      // 数组的最后一个一定是文件,因此不需要判断它是不是目录
+      if (index !== arr.length - 1) {
+        const flag = fs.existsSync(item);
+        // eslint-disable-next-line
+        !flag && fs.mkdirSync(item);
+      }
+    });
+    fs.copyFileSync(
+      file,
+      path.join(giteeDir, './', file.replace(localDir, ''))
+    );
+  }
+}
+
+if (path.resolve(__dirname) === giteeDir) {
+  // eslint-disable-next-line
+  console.log('当前在gitee文件目录,直接退出!');
+} else {
+  findFile(dir);
+  putFile();
+  execSync(`pnpm i`, { cwd: giteeDir });
+  execSync(`git add .`, { cwd: giteeDir });
+  execSync(`git commit -m 'feat: ${new Date().toLocaleString()}'`, {
+    cwd: giteeDir,
+  });
+  execSync(`git push`, { cwd: giteeDir });
+}

+ 102 - 0
package.json

@@ -0,0 +1,102 @@
+{
+  "name": "billd-live",
+  "version": "0.0.0",
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/galaxy-s10/billd-live.git"
+  },
+  "author": "shuisheng <2274751790@qq.com>",
+  "scripts": {
+    "build": "webpack --config ./script/config/webpack.common.ts --env production",
+    "commit": "cz",
+    "copy": "node ./handleGiteeJenkins.js",
+    "lint": "eslint --config ./.eslintrc.js . --ext .js,.jsx,.ts,.tsx,.vue --cache",
+    "lint:fix": "eslint --config ./.eslintrc.js . --ext .js,.jsx,.ts,.tsx,.vue --cache --fix",
+    "prepare": "husky install",
+    "prettier": "prettier --write .",
+    "push": "cz && standard-version",
+    "release": "standard-version",
+    "start": "cross-env webpack serve --config ./script/config/webpack.common.ts --env development",
+    "start:beta": "cross-env VUE_APP_RELEASE_PROJECT_ENV=beta VUE_APP_RELEASE_PROJECT_NAME=billd-live webpack serve --config ./script/config/webpack.common.ts --env development",
+    "start:preview": "cross-env VUE_APP_RELEASE_PROJECT_ENV=preview VUE_APP_RELEASE_PROJECT_NAME=billd-live webpack serve --config ./script/config/webpack.common.ts --env development",
+    "start:prod": "cross-env VUE_APP_RELEASE_PROJECT_ENV=prod VUE_APP_RELEASE_PROJECT_NAME=billd-live webpack serve --config ./script/config/webpack.common.ts --env production",
+    "start:stats": "cross-env WEBPACK_ANALYZER_SWITCH=true webpack serve --config ./script/config/webpack.common.ts --env development",
+    "typecheck": "tsc -p ./tsconfig.json --noEmit"
+  },
+  "config": {
+    "commitizen": {
+      "path": "./node_modules/cz-conventional-changelog"
+    }
+  },
+  "dependencies": {
+    "axios": "^1.2.1",
+    "billd-deploy": "^1.0.13",
+    "billd-html-webpack-plugin": "^1.0.0",
+    "billd-scss": "^0.0.6",
+    "billd-utils": "^0.0.9",
+    "pinia": "^2.0.11",
+    "socket.io-client": "^4.6.1",
+    "vue": "^3.2.31",
+    "vue-demi": "^0.13.11",
+    "vue-router": "^4.0.13"
+  },
+  "devDependencies": {
+    "@babel/core": "^7.14.0",
+    "@babel/preset-env": "^7.14.2",
+    "@commitlint/cli": "^16.0.1",
+    "@commitlint/config-conventional": "^16.0.0",
+    "@rushstack/eslint-patch": "^1.1.0",
+    "@soda/friendly-errors-webpack-plugin": "^1.8.1",
+    "@types/node": "^18.11.9",
+    "@typescript-eslint/parser": "^5.8.1",
+    "@vue/compiler-sfc": "^3.2.31",
+    "@vue/eslint-config-prettier": "^7.0.0",
+    "@vue/eslint-config-typescript": "^10.0.0",
+    "@vue/preload-webpack-plugin": "^2.0.0",
+    "babel-loader": "^8.2.2",
+    "chalk": "^4",
+    "commitizen": "^4.2.4",
+    "compression-webpack-plugin": "^9.2.0",
+    "copy-webpack-plugin": "^8.1.0",
+    "core-js": "^3.17.2",
+    "cross-env": "^7.0.3",
+    "css-loader": "^6.7.1",
+    "css-minimizer-webpack-plugin": "^3.0.0",
+    "cz-conventional-changelog": "^3.3.0",
+    "eslint": "^8.13.0",
+    "eslint-plugin-import": "^2.27.5",
+    "eslint-plugin-vue": "^8.5.0",
+    "eslint-webpack-plugin": "^3.1.1",
+    "file-loader": "^6.2.0",
+    "fork-ts-checker-webpack-plugin": "^7.2.6",
+    "html-webpack-plugin": "^5.5.0",
+    "html-webpack-tags-plugin": "^3.0.1",
+    "husky": "^7.0.0",
+    "lint-staged": "^12.1.4",
+    "mini-css-extract-plugin": "^2.6.0",
+    "node-emoji": "^1.11.0",
+    "portfinder": "^1.0.28",
+    "postcss": "^8.4.8",
+    "postcss-loader": "^6.2.1",
+    "postcss-preset-env": "^7.4.2",
+    "prettier": "^2.5.1",
+    "sass": "^1.45.2",
+    "sass-loader": "^12.4.0",
+    "standard-version": "^9.3.2",
+    "terser": "^5.7.0",
+    "terser-webpack-plugin": "^5.3.6",
+    "thread-loader": "^3.0.4",
+    "ts-loader": "^9.2.7",
+    "ts-node": "^10.9.1",
+    "typescript": "^4.6.2",
+    "vue-loader": "^17.0.0",
+    "vue-style-loader": "^4.1.3",
+    "webpack": "^5.68.0",
+    "webpack-bundle-analyzer": "^4.4.1",
+    "webpack-cli": "^4.9.2",
+    "webpack-dev-server": "^4.7.2",
+    "webpack-merge": "^5.7.3",
+    "webpackbar": "^5.0.2",
+    "windicss-webpack-plugin": "^1.7.7"
+  }
+}

+ 15 - 0
postcss.config.js

@@ -0,0 +1,15 @@
+console.log(
+  '\x1B[0;37;44m INFO \x1B[0m',
+  '\x1B[0;;34m ' +
+    `读取了: ${__filename.slice(__dirname.length + 1)}` +
+    ' \x1B[0m'
+);
+
+// 把.browserslistrc的last 2 version改成last 20 version就可以看到明显效果
+module.exports = {
+  plugins: [
+    // 'autoprefixer',  // postcss-preset-env包含了autoprefixer的功能
+    'postcss-preset-env', // 简写,具体看各个插件的官网提供几种写法
+    // require('postcss-preset-env'),
+  ],
+};

BIN
public/Hololo.jpeg


BIN
public/favicon.ico


+ 29 - 0
public/index.html

@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html lang="">
+  <head>
+    <meta charset="utf-8" />
+    <meta
+      http-equiv="X-UA-Compatible"
+      content="IE=edge"
+    />
+    <meta
+      name="viewport"
+      content="width=device-width,initial-scale=1.0"
+    />
+    <link
+      rel="icon"
+      href="<%= BASE_URL %>favicon.ico"
+    />
+    <title><%= htmlWebpackPlugin.options.title %></title>
+  </head>
+  <body>
+    <noscript>
+      <strong>
+        We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work
+        properly without JavaScript enabled. Please enable it to continue.
+      </strong>
+    </noscript>
+    <div id="app"></div>
+    <div id="message"></div>
+  </body>
+</html>

+ 27 - 0
script/TerminalPrintPlugin.ts

@@ -0,0 +1,27 @@
+import chalk from 'chalk';
+import { Compiler } from 'webpack';
+import WebpackDevServer from 'webpack-dev-server';
+
+const localIPv4 = WebpackDevServer.internalIPSync('v4');
+
+class TerminalPrintPlugin {
+  constructor() {}
+
+  apply(compiler: Compiler) {
+    compiler.hooks.done.tapAsync('TerminalPrintPlugin', (stats, callback) => {
+      const publicPath = stats.compilation.outputOptions.publicPath as string;
+      const port = stats.compilation.options.devServer!.port as number;
+      console.log('  App running at:');
+      console.log(
+        `- Local:   ${chalk.cyan(`http://localhost:${port}${publicPath}`)}`
+      );
+      console.log(
+        `- Network: ${chalk.cyan(`http://${localIPv4!}:${port}${publicPath}`)}`
+      );
+      console.log();
+      callback();
+    });
+  }
+}
+
+export default TerminalPrintPlugin;

+ 423 - 0
script/config/webpack.common.ts

@@ -0,0 +1,423 @@
+import FriendlyErrorsWebpackPlugin from '@soda/friendly-errors-webpack-plugin';
+import BilldHtmlWebpackPlugin from 'billd-html-webpack-plugin';
+import CopyWebpackPlugin from 'copy-webpack-plugin';
+import ESLintPlugin from 'eslint-webpack-plugin';
+import HtmlWebpackPlugin from 'html-webpack-plugin';
+import MiniCssExtractPlugin from 'mini-css-extract-plugin';
+import { VueLoaderPlugin } from 'vue-loader';
+import { Configuration, DefinePlugin } from 'webpack';
+import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
+import { merge } from 'webpack-merge';
+import WindiCSSWebpackPlugin from 'windicss-webpack-plugin';
+
+import {
+  analyzerEnable,
+  eslintEnable,
+  htmlWebpackPluginTitle,
+  outputDir,
+  outputStaticUrl,
+  windicssEnable,
+} from '../constant';
+import { chalkINFO, chalkWARN } from '../utils/chalkTip';
+import { resolveApp } from '../utils/path';
+
+import devConfig from './webpack.dev';
+import prodConfig from './webpack.prod';
+
+console.log(chalkINFO(`读取: ${__filename.slice(__dirname.length + 1)}`));
+
+const sassRules = (isProduction: boolean, module?: boolean) => {
+  return [
+    isProduction
+      ? {
+          loader: MiniCssExtractPlugin.loader,
+          options: {
+            publicPath: outputStaticUrl(isProduction),
+          },
+        }
+      : {
+          loader: 'vue-style-loader',
+          options: {
+            sourceMap: false,
+          },
+        },
+    {
+      loader: 'css-loader', // 默认会自动找postcss.config.js
+      options: {
+        importLoaders: 2, // https://www.npmjs.com/package/css-loader#importloaders
+        sourceMap: false,
+        modules: module
+          ? {
+              localIdentName: '[name]_[local]_[hash:base64:5]',
+            }
+          : undefined,
+      },
+    },
+    {
+      loader: 'postcss-loader', // 默认会自动找postcss.config.js
+      options: {
+        sourceMap: false,
+      },
+    },
+    {
+      loader: 'sass-loader',
+      options: {
+        sourceMap: false,
+        // 根据sass-loader9.x以后使用additionalData,9.x以前使用prependData
+        additionalData: `@use 'billd-scss/src/index.scss' as *;`,
+      },
+    },
+  ].filter(Boolean);
+};
+
+const cssRules = (isProduction: boolean, module?: boolean) => {
+  return [
+    isProduction
+      ? {
+          loader: MiniCssExtractPlugin.loader,
+          options: {
+            publicPath: outputStaticUrl(isProduction),
+          },
+        }
+      : {
+          loader: 'vue-style-loader',
+          options: {
+            sourceMap: false,
+          },
+        },
+    {
+      loader: 'css-loader', // 默认会自动找postcss.config.js
+      options: {
+        importLoaders: 1, // https://www.npmjs.com/package/css-loader#importloaders
+        sourceMap: false,
+        modules: module
+          ? {
+              localIdentName: '[name]_[local]_[hash:base64:5]',
+            }
+          : undefined,
+      },
+    },
+    {
+      loader: 'postcss-loader', // 默认会自动找postcss.config.js
+      options: {
+        sourceMap: false,
+      },
+    },
+  ].filter(Boolean);
+};
+
+const commonConfig = (isProduction) => {
+  const result: Configuration = {
+    entry: {
+      main: {
+        import: './src/main.ts',
+      },
+    },
+    output: {
+      clean: true, // 在生成文件之前清空 output 目录。替代clean-webpack-plugin
+      filename: 'js/[name]-[contenthash:6]-bundle.js', // 入口文件打包生成后的文件的文件名
+      /**
+       * 入口文件中,符合条件的代码,被抽离出来后生成的文件的文件名
+       * 如:动态(即异步)导入,默认不管大小,是一定会被单独抽离出来的。
+       * 如果一个模块既被同步引了,又被异步引入了,不管顺序(即不管是先同步引入再异步引入,还是先异步引入在同步引入),
+       * 这个模块会打包进bundle.js,而不会单独抽离出来。
+       */
+      chunkFilename: 'js/[name]-[contenthash:6]-bundle-chunk.js',
+      path: resolveApp(`./${outputDir}`),
+      assetModuleFilename: 'assets/[name]-[contenthash:6].[ext]', // 静态资源生成目录(不管什么资源默认都统一生成到这里,除非单独设置了generator)
+      /**
+       * webpack-dev-server 也会默认从 publicPath 为基准,使用它来决定在哪个目录下启用服务,来访问 webpack 输出的文件。
+       * 所以不管开发模式还是生产模式,output.publicPath都会生效,
+       * output的publicPath建议(或者绝大部分情况下必须)与devServer的publicPath一致。
+       * 如果不设置publicPath,它默认就约等于output.publicPath:"",到时候不管开发还是生产模式,最终引入到
+       * index.html的所有资源都会拼上这个路径,如果不设置output.publicPath,会有问题:
+       * 比如vue的history模式下,如果不设置output.publicPath,如果路由全都是/foo,/bar,/baz这样的一级路由没有问题,
+       * 因为引入的资源都是js/bundle.js,css/bundle.css等等,浏览器输入:http://localhost:8080/foo,回车访问,
+       * 引入的资源就是http://localhost:8080/js/bundle.js,http://localhost:8080/css/bundle.css,这些资源都
+       * 是在http://localhost:8080/根目录下的没问题,但是如果有这些路由:/logManage/logList,/logManage/logList/editLog,
+       * 等等超过一级的路由,就会有问题,因为没有设置output.publicPath,所以它默认就是"",此时浏览器输入:
+       * http://localhost:8080/logManage/logList回车访问,引入的资源就是http://localhost:8080/logManage/logList/js/bundle.js,
+       * 而很明显,我们的http://localhost:8080/logManage/logList/js目录下没有bundle.js这个资源(至少默认情况下是没有,除非设置了其他属性)
+       * 找不到这个资源就会报错,这种情况的路由是很常见的,所以建议默认必须手动设置output.publicPath:"/",这样的话,
+       * 访问http://localhost:8080/logManage/logList,引入的资源就是:http://localhost:8080/js/bundle.js,就不会报错。
+       * 此外,output.publicPath还可设置cdn地址。
+       */
+      publicPath: outputStaticUrl(isProduction),
+    },
+    resolve: {
+      // 解析路径
+      extensions: ['.js', '.jsx', '.ts', '.tsx', '.vue', '.mjs'], // 解析扩展名,加上.mjs是因为vant,https://github.com/youzan/vant/issues/10738
+      alias: {
+        '@': resolveApp('./src'), // 设置路径别名
+        script: resolveApp('./script'), // 设置路径别名
+        vue$: 'vue/dist/vue.runtime.esm-bundler.js', // 设置vue的路径别名
+      },
+      fallback: {
+        /**
+         * webpack5移除了nodejs的polyfill,更专注于web了?
+         * 其实webpack5之前的版本能用nodejs的polyfill,也是
+         * 和nodejs正统的api不一样,比如path模块,nodejs的path,
+         * __dirname是读取到的系统级的文件绝对路径的(即/user/xxx)
+         * 但在webpack里面使用__dirname,读取到的是webpack配置的绝对路径/
+         * 可能有用的polyfill就是crypto这些通用的模块,类似path和fs这些模
+         * 块其实都是他们的polyfill都是跑在浏览器的,只是有这些api原本的一些功能,
+         * 还是没有nodejs的能力,所以webpack5干脆就移除了这些polyfill,你可以通过
+         * 安装他们的polyfill来实现原本webpack4之前的功能,但是即使安装他们的polyfill
+         * 也只是实现api的功能,没有他们原本在node的能力
+         */
+        // path: require.resolve('path-browserify'),
+        // path: false,
+        // fs: false,
+        // child_process: false,
+      },
+    },
+    resolveLoader: {
+      // 用于解析webpack的loader
+      modules: ['node_modules'],
+    },
+    module: {
+      noParse: /^(vue|vue-router)$/,
+      // loader执行顺序:从下往上,从右往左
+      rules: [
+        {
+          test: /\.vue$/,
+          use: [
+            {
+              loader: 'vue-loader',
+            },
+          ],
+        },
+        {
+          test: /\.jsx?$/,
+          exclude: /node_modules/,
+          use: [
+            // 'thread-loader',
+            {
+              loader: 'babel-loader',
+              options: {
+                cacheDirectory: true,
+                cacheCompression: false, // https://github.com/facebook/create-react-app/issues/6846
+              },
+            },
+          ],
+        },
+        {
+          test: /\.ts$/,
+          exclude: /node_modules/,
+          use: [
+            {
+              loader: 'babel-loader',
+              options: {
+                cacheDirectory: true,
+                cacheCompression: false, // https://github.com/facebook/create-react-app/issues/6846
+              },
+            },
+            {
+              loader: 'ts-loader',
+              options: {
+                appendTsSuffixTo: ['\\.vue$'],
+                // If you want to speed up compilation significantly you can set this flag. https://www.npmjs.com/package/ts-loader#transpileonly
+                transpileOnly: true,
+                happyPackMode: false,
+              },
+            },
+          ],
+        },
+        {
+          test: /\.tsx$/,
+          exclude: /node_modules/,
+          use: [
+            {
+              loader: 'babel-loader',
+              options: {
+                cacheDirectory: true,
+                cacheCompression: false, // https://github.com/facebook/create-react-app/issues/6846
+              },
+            },
+            {
+              loader: 'ts-loader',
+              options: {
+                appendTsxSuffixTo: ['\\.vue$'],
+                // If you want to speed up compilation significantly you can set this flag. https://www.npmjs.com/package/ts-loader#transpileonly
+                transpileOnly: true,
+                happyPackMode: false,
+              },
+            },
+          ],
+        },
+        {
+          test: /\.css$/,
+          oneOf: [
+            {
+              resourceQuery: /module/,
+              use: cssRules(isProduction, true),
+            },
+            {
+              resourceQuery: /\?vue/,
+              use: cssRules(isProduction),
+            },
+            {
+              test: /\.module\.\w+$/,
+              use: cssRules(isProduction, true),
+            },
+            {
+              use: cssRules(isProduction),
+            },
+          ],
+          sideEffects: true, // 告诉webpack是有副作用的,不对css进行删除
+        },
+        {
+          test: /\.(sass|scss)$/,
+          oneOf: [
+            {
+              resourceQuery: /module/,
+              use: sassRules(isProduction, true),
+            },
+            {
+              resourceQuery: /\?vue/,
+              use: sassRules(isProduction),
+            },
+            {
+              test: /\.module\.\w+$/,
+              use: sassRules(isProduction, true),
+            },
+            {
+              use: sassRules(isProduction),
+            },
+          ],
+          sideEffects: true,
+        },
+        {
+          test: /\.(jpg|jpeg|png|gif|svg|webp)$/,
+          type: 'asset',
+          generator: {
+            filename: 'img/[name]-[contenthash:6][ext]',
+          },
+          parser: {
+            dataUrlCondition: {
+              maxSize: 4 * 1024, // 如果一个模块源码大小小于 maxSize,那么模块会被作为一个 Base64 编码的字符串注入到包中, 否则模块文件会被生成到输出的目标目录中
+            },
+          },
+        },
+        {
+          test: /\.(eot|ttf|woff2?)$/,
+          type: 'asset/resource',
+          generator: {
+            filename: 'font/[name]-[contenthash:6][ext]',
+          },
+        },
+      ],
+    },
+    plugins: [
+      // 友好的显示错误信息在终端
+      new FriendlyErrorsWebpackPlugin(),
+      // 解析vue
+      new VueLoaderPlugin(),
+      // windicss
+      windicssEnable && new WindiCSSWebpackPlugin(),
+      // 该插件将为您生成一个HTML5文件,其中包含使用脚本标签的所有Webpack捆绑包
+      new HtmlWebpackPlugin({
+        filename: 'index.html',
+        title: htmlWebpackPluginTitle,
+        template: resolveApp('./public/index.html'),
+        hash: true,
+        minify: isProduction
+          ? {
+              collapseWhitespace: true, // 折叠空白
+              keepClosingSlash: true, // 在单标签上保留末尾斜杠
+              removeComments: true, // 移除注释
+              removeRedundantAttributes: true, // 移除多余的属性(如:input的type默认就是text,如果写了type="text",就移除它,因为不写它默认也是type="text")
+              removeScriptTypeAttributes: true, // 删除script标签中type="text/javascript"
+              removeStyleLinkTypeAttributes: true, // 删除style和link标签中type="text/css"
+              useShortDoctype: true, // 使用html5的<!doctype html>替换掉之前的html老版本声明方式<!doctype>
+              // 上面的都是production模式下默认值。
+              removeEmptyAttributes: true, // 移除一些空属性,如空的id,classs,style等等,但不是空的就全删,比如<img alt />中的alt不会删。http://perfectionkills.com/experimenting-with-html-minifier/#remove_empty_or_blank_attributes
+              minifyCSS: true, // 使用clean-css插件删除 CSS 中一些无用的空格、注释等。
+              minifyJS: true, // 使用Terser插件优化
+            }
+          : false,
+        chunks: ['main'], // 要仅包含某些块,您可以限制正在使用的块
+      }),
+      // 注入项目信息
+      new BilldHtmlWebpackPlugin({
+        env: 'webpack5',
+      }),
+      // 将已存在的单个文件或整个目录复制到构建目录。
+      new CopyWebpackPlugin({
+        patterns: [
+          {
+            from: 'public', // 复制public目录的文件
+            // to: 'assets', //复制到output.path下的assets,不写默认就是output.path根目录
+            globOptions: {
+              ignore: [
+                // 复制到output.path时,如果output.paht已经存在重复的文件了,会报错:
+                // ERROR in Conflict: Multiple assets emit different content to the same filename md.html
+                '**/index.html', // 忽略from目录下的index.html,它是入口文件
+              ],
+            },
+          },
+        ],
+      }),
+      // 定义全局变量
+      new DefinePlugin({
+        BASE_URL: `${JSON.stringify(outputStaticUrl(isProduction))}`, // public下的index.html里面的favicon.ico的路径
+        'process.env': {
+          NODE_ENV: JSON.stringify(isProduction ? 'production' : 'development'),
+          PUBLIC_PATH: JSON.stringify(outputStaticUrl(isProduction)),
+          VUE_APP_RELEASE_PROJECT_NAME: JSON.stringify(
+            process.env.VUE_APP_RELEASE_PROJECT_NAME
+          ),
+          VUE_APP_RELEASE_PROJECT_ENV: JSON.stringify(
+            process.env.VUE_APP_RELEASE_PROJECT_ENV
+          ),
+        },
+        __VUE_OPTIONS_API__: 'true',
+        __VUE_PROD_DEVTOOLS__: 'false',
+      }),
+      // bundle分析
+      analyzerEnable &&
+        new BundleAnalyzerPlugin({
+          analyzerMode: 'server',
+          generateStatsFile: true,
+          statsOptions: { source: false },
+        }), // configuration.plugins should be one of these object { apply, … } | function
+      // eslint
+      eslintEnable &&
+        new ESLintPlugin({
+          extensions: ['js', 'jsx', 'ts', 'tsx', 'vue'],
+          emitError: false, // 发现的错误将始终发出,禁用设置为false.
+          emitWarning: false, // 找到的警告将始终发出,禁用设置为false.
+          failOnError: false, // 如果有任何错误,将导致模块构建失败,禁用设置为false
+          failOnWarning: false, // 如果有任何警告,将导致模块构建失败,禁用设置为false
+          cache: true,
+          cacheLocation: resolveApp('./node_modules/.cache/.eslintcache'),
+        }),
+    ].filter(Boolean),
+  };
+  return result;
+};
+
+export default (env) => {
+  return new Promise((resolve) => {
+    const isProduction = env.production;
+    process.env.NODE_ENV = isProduction ? 'production' : 'development';
+    const configPromise = Promise.resolve(
+      isProduction ? prodConfig : devConfig
+    );
+    configPromise.then(
+      (config: any) => {
+        // 根据当前环境,合并配置文件
+        const mergeConfig = merge(commonConfig(isProduction), config);
+        console.log(
+          chalkWARN(
+            `根据当前环境,合并配置文件,当前是: ${process.env.NODE_ENV!}环境`
+          )
+        );
+        resolve(mergeConfig);
+      },
+      (err) => {
+        console.log(err);
+      }
+    );
+  });
+};

+ 165 - 0
script/config/webpack.dev.ts

@@ -0,0 +1,165 @@
+import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
+import portfinder from 'portfinder';
+import { Configuration } from 'webpack';
+import WebpackBar from 'webpackbar';
+
+import TerminalPrintPlugin from '../TerminalPrintPlugin';
+import { webpackBarEnable, outputStaticUrl } from '../constant';
+import { chalkINFO } from '../utils/chalkTip';
+import { resolveApp } from '../utils/path';
+
+console.log(chalkINFO(`读取: ${__filename.slice(__dirname.length + 1)}`));
+
+export default new Promise((resolve) => {
+  // 默认端口8000,如果被占用了,会自动递增+1
+  const defaultPort = 8000;
+  portfinder
+    .getPortPromise({
+      port: defaultPort,
+      stopPort: 9000,
+    })
+    .then((port) => {
+      const devConfig: Configuration = {
+        target: 'web',
+        // https://github.com/webpack/webpack/blob/main/lib/config/defaults.js
+        mode: 'development',
+        stats: 'none',
+        cache: {
+          type: 'filesystem',
+          buildDependencies: {
+            // https://webpack.js.org/configuration/cache/#cacheallowcollectingmemory
+            // 建议cache.buildDependencies.config: [__filename]在您的 webpack 配置中设置以获取最新配置和所有依赖项。
+            config: [__filename],
+          },
+        },
+        // https://webpack.docschina.org/configuration/devtool/
+        devtool: 'eval-cheap-module-source-map',
+        // devtool: 'eval', // eval,具有最高性能的开发构建的推荐选择。
+        // 这个infrastructureLogging设置参考了vuecli5,如果不设置,webpack-dev-server会打印一些信息
+        infrastructureLogging: {
+          level: 'none',
+        },
+        devServer: {
+          client: {
+            logging: 'none', // https://webpack.js.org/configuration/dev-server/#devserverclient
+          },
+          hot: true, // 启用 webpack 的热模块替换功能
+          // hot: 'only', // 要在构建失败的情况下启用热模块替换而不刷新页面作为后备,请使用hot: 'only'。但在vue项目的话,使用only会导致ts文件没有热更,得使用true
+          compress: true, // 为所有服务启用gzip 压缩
+          port, // 开发服务器端口,默认8080
+          open: false, // 告诉 dev-server 在服务器启动后打开浏览器。
+          historyApiFallback: {
+            rewrites: [
+              /**
+               * 如果publicPath设置了/abc,就不能直接设置historyApiFallback: true,这样会重定向到vue3-blog-admin根目录下的index.html
+               * publicPath设置了/abc,就重定向到/abc,这样就可以了
+               */
+              {
+                from: new RegExp(outputStaticUrl(false)),
+                to: outputStaticUrl(false),
+              },
+            ],
+          },
+          /**
+           * devServer.static提供静态文件服务器,默认是 'public' 文件夹。static: false禁用
+           * 即访问localhost:8080/a.js,其实访问的是public目录的a.js
+           */
+          // WARN 因为CopyWebpackPlugin插件会复制public的文件,所以static: false后再访问localhost:8080/a.js,其实还是能访问到public目录的a.js
+          static: {
+            watch: true, // 告诉 dev-server 监听文件。默认启用,文件更改将触发整个页面重新加载。可以通过将 watch 设置为 false 禁用。
+            publicPath: outputStaticUrl(false), // 让它和输入的静态目录对应
+            directory: resolveApp('./public/'),
+          },
+          proxy: {
+            '/api': {
+              target: 'http://localhost:3300',
+              secure: false, // 默认情况下(secure: true),不接受在HTTPS上运行的带有无效证书的后端服务器。设置secure: false后,后端服务器的HTTPS有无效证书也可运行
+              /**
+               * changeOrigin,是否修改请求地址的源
+               * 默认changeOrigin: false,即发请求即使用devServer的localhost:port发起的,如果后端服务器有校验源,就会有问题
+               * 设置changeOrigin: true,就会修改发起请求的源,将原本的localhost:port修改为target,这样就可以通过后端服务器对源的校验
+               */
+              changeOrigin: true,
+              pathRewrite: {
+                // '^/api': '', // 效果:/api/link/list ==> http://localhost:3300/link/list
+                '^/api': '/admin/', // 效果:/api/link/list ==> http://localhost:3300/admin/link/list
+              },
+            },
+            '/prodapi': {
+              target: 'http://42.193.157.44:3200',
+              secure: false,
+              changeOrigin: true,
+              pathRewrite: {
+                '^/prodapi': '/admin/',
+              },
+            },
+            '/betaapi': {
+              target: 'http://42.193.157.44:3300',
+              secure: false,
+              changeOrigin: true,
+              pathRewrite: {
+                '^/betaapi': '/admin/',
+              },
+            },
+          },
+        },
+        // @ts-ignore
+        plugins: [
+          // 构建进度条
+          webpackBarEnable && new WebpackBar(),
+          // 终端打印调试地址
+          new TerminalPrintPlugin(),
+          new ForkTsCheckerWebpackPlugin({
+            // https://github.com/TypeStrong/fork-ts-checker-webpack-plugin
+            typescript: {
+              extensions: {
+                vue: {
+                  enabled: true,
+                  compiler: resolveApp(
+                    './node_modules/vue/compiler-sfc/index.js'
+                  ),
+                },
+              },
+              diagnosticOptions: {
+                semantic: true,
+                syntactic: false,
+              },
+            },
+            /**
+             * devServer如果设置为false,则不会向 Webpack Dev Server 报告错误。
+             * 但是控制台还是会打印错误。
+             */
+            devServer: false, // 7.x版本:https://github.com/TypeStrong/fork-ts-checker-webpack-plugin/issues/723
+            // logger: {
+            //   devServer: false, // fork-ts-checker-webpack-plugin6.x版本
+            // },
+            /**
+             * async 为 false,同步的将错误信息反馈给 webpack,如果报错了,webpack 就会编译失败
+             * async 默认为 true,异步的将错误信息反馈给 webpack,如果报错了,不影响 webpack 的编译
+             */
+            async: true,
+          }),
+        ].filter(Boolean),
+        optimization: {
+          /**
+           * 官网解释:告知 webpack 去辨识 package.json 中的 副作用 标记或规则,
+           * 以跳过那些当导出不被使用且被标记不包含副作用的模块。'flag' 值在非生产环境默认使用。
+           * 个人理解:flag,即如果package.json有标识就会用它的标识,
+           * 但不意味着你的项目的package.json就得设置sideEffects,你的项目不设置,它也会对你
+           * 项目里面用到的node_modules里面的包的package.json做检查。
+           * 设置true的话,还会分析源代码的副作用?但测试结果貌似不会,可能我理解有问题,已经
+           * 提了issue:https://github.com/webpack/webpack/issues/16314
+           * 设置false的话,即不会检查package.json的sideEffects字段,把所有模块都当成有副作
+           * 用的(即使某个包的package.json设置sideEffects为false),因为sideEffects并不
+           * 是npm的package.json合法字段,只是写给webpack识别用的而已
+           */
+          // sideEffects: true,
+          sideEffects: 'flag',
+        },
+      };
+      resolve(devConfig);
+    })
+    .catch((error) => {
+      console.log(error);
+    });
+});

+ 203 - 0
script/config/webpack.prod.ts

@@ -0,0 +1,203 @@
+import PreloadPlugin from '@vue/preload-webpack-plugin';
+// import { version as axiosVersion } from 'axios/package.json';
+import CompressionPlugin from 'compression-webpack-plugin';
+import CssMinimizerPlugin from 'css-minimizer-webpack-plugin';
+import HtmlWebpackTagsPlugin from 'html-webpack-tags-plugin';
+import MiniCssExtractPlugin from 'mini-css-extract-plugin';
+// import { version as piniaVersion } from 'pinia/package.json';
+import TerserPlugin from 'terser-webpack-plugin';
+// import { version as vueDemiVersion } from 'vue-demi/package.json';
+// import { version as vueRouterVersion } from 'vue-router/package.json';
+// import { version as vueVersion } from 'vue/package.json';
+import { Configuration } from 'webpack';
+import WebpackBar from 'webpackbar';
+
+import { gzipEnable } from '../constant';
+import { chalkINFO } from '../utils/chalkTip';
+
+console.log(chalkINFO(`读取: ${__filename.slice(__dirname.length + 1)}`));
+
+const prodConfig: Configuration = {
+  mode: 'production',
+  devtool: false,
+  // externals: {
+  //   vue: 'Vue',
+  //   'vue-router': 'VueRouter',
+  //   pinia: 'Pinia',
+  //   axios: 'axios',
+  // },
+  optimization: {
+    /**
+     * splitChunks属性,如果设置了mode: 'production',会有默认行为,具体看官网
+     * 但即使没有设置mode: 'production',也没有手动添加splitChunks属性,默认还是会添加splitChunks的部分行为,
+     * 比如:splitChunks.chunks:'async'等等,即会将异步代码抽离!
+     */
+    splitChunks: {
+      // 对入口文件进行代码分离
+      // chunks: 'all',  //async,initial,all
+      // minSize: 20 * 1024, //生成 chunk 的最小体积。默认:20000(19.5kb)
+      /**
+       * maxSize:尝试将大于maxSize的chunk分割成较小的部分chunks。
+       * 官网写的默认值是0,但是,实际测试:如果在chunks:async的时候,确实这个属性会生效,会将异步代码配合minSize进行抽离;
+       * 但是如果在chunks:initial或all的时候,如果不手动添加maxSize属性,就不会将同步代码配合minSize进行抽离!
+       * 因此,如果希望maxSize可以对同步和异步代码都进行分离,就手动设置maxSize:0,或者手动设置maxSize为自己需要设置的值,
+       * 但一定不能不写这个maxSize!最起码也得写一个maxSize:0,虽然这样写会报警告,或者直接写maxSize的值和minSize值一样!
+       */
+      // maxSize: 0,   //不写maxSize默认就是0,这里手动设置0
+      // maxSize: 30 * 1024,
+      // minRemainingSize: 0, //???
+      // minChunks: 1, //模块被不同entry引用的次数大于等于才能分割。
+      // maxAsyncRequests: 30, //按需加载时的最大并行请求数。默认:30
+      // maxInitialRequests: 30, //按需加载时的最大并行请求数。默认:30
+      /**
+       * enforceSizeThreshold:强制执行拆分的体积阈值和其他限制(minRemainingSize,maxAsyncRequests,maxInitialRequests)将被忽略。
+       * 即拆分的包大小范围允许在这个阈值范围,即设置minSize:20 * 1024,enforceSizeThreshold: 10 * 1024,
+       * 允许拆分的包在10kb-30kb之间!
+       */
+      // enforceSizeThreshold: 1 * 1024,  //默认:50000byte
+      /**
+       * 不建议全局设置filename,因为如果缓存组没有手动设置filename,默认缓存组会继承全局
+       * 的filename,这样在某些情况会显得很奇葩,比如:全局设置了chunks:'async',filename:'[id]-asyncChunks.js',
+       * 而缓存组设置了一个chunks:'initial',且没有设置它的filename,那么最终打包会先匹配缓存组,然后匹配
+       * 到同步代码就抽离,然后设置filename,由于这个缓存组没有设置它的filename,因此会继承全局的filename,
+       * 因此就会把同步代码抽离后叫[id]-asyncChunks.js,虽然还是一样把代码抽离出来了,但是
+       * 抽离出来的文件和文件名"货不对板",做不到见名知意,这样就很别扭了。因此如果设置设置了全局的filename,那
+       * 么最好就是每一个缓存组都设置自己的filename,这样就可以和全局的进行区分了
+       */
+      // filename: "[id]-splitChunks.js", //默认[name]-bundle.js
+      /**
+       * 缓存组可以继承和/或覆盖来自 splitChunks.* 的任何选项
+       * 即如果匹配到缓存缓存组里的某一个,如vendor,vendor里的设置会对splitChunks的设置进行继承或覆盖
+       * 即vendor里没有设置chunks,vendor就会继承splitChunks的chunks,vendor设置了filename,会覆盖splitChunks的filename
+       */
+      cacheGroups: {
+        // cacheGroups里的优先级默认比外面的高
+        // defaultVendors:false,  //禁用默认webpack默认设置的defaultVendors缓存组
+        // default:false, //禁用默认webpack默认设置的default缓存组
+        defaultVendors: {
+          // 重写默认的defaultVendors
+          chunks: 'initial',
+          // minSize: 50 * 1024,
+          maxSize: 100 * 1024,
+          test: /[\\/]node_modules[\\/]/,
+          // filename: 'js/[name]-defaultVendors.js',
+          filename: 'js/[name]-[contenthash:6]-defaultVendors.js',
+          priority: -10,
+        },
+        default: {
+          // 重写默认的default
+          chunks: 'all',
+          maxSize: 100 * 1024,
+          filename: 'js/[name]-[contenthash:6]-default.js',
+          minChunks: 2, // 至少被minChunks个入口文件引入了minChunks次。
+          priority: -20,
+        },
+      },
+    },
+    usedExports: true, // production模式或者不设置usedExports,它默认就是true。usedExports的目的是标注出来哪些函数是没有被使用 unused,会结合Terser进行处理
+    sideEffects: true, // webpack.dev.ts有注释
+    minimize: true, // 是否开启Terser,默认就是true,设置false后,不会压缩和转化
+    minimizer: [
+      new TerserPlugin({
+        parallel: true, // 使用多进程并发运行以提高构建速度
+        extractComments: false, // 默认true,会将/^\**!|@preserve|@license|@cc_on/i的注释提取到单独的文件中
+        // Terser 压缩配置
+        terserOptions: {
+          parse: {
+            // 注意:terserOptions.parse被标记了deprecated。
+          },
+          compress: {
+            // defaults:true,默认true,传递false禁用大多数默认启用的compress转换
+            arguments: true, // 默认false,尽可能将参数[index]替换为函数参数名
+            dead_code: true, // 默认true,删除无法访问的代码(比如return后面的语句)
+            toplevel: false, // 默认false,在顶级作用域中删除未引用的函数("funcs")和/或变量("vars"), 设置true表示同时删除未引用的函数和变量
+            keep_classnames: false, // 默认false,传递true以防止terser丢弃类名
+            keep_fnames: false, // 默认false,传递true以防止terser丢弃函数名
+            drop_console: false, // 默认false,设置true会删掉丢掉对console.*函数的调用
+            // pure_funcs: ['console.log'], // 告诉terser,console.log没有副作用,terser会将它删除
+          },
+          /**
+           * mangle,默认值true,会将keep_classnames,keep_fnames,toplevel等等mangle options的所有选项设为true。
+           * 传递false以跳过篡改名称,或者传递一个对象来指定篡改选项
+           */
+          mangle: true,
+          toplevel: false, // 注意:terserOptions.toplevel被标记了deprecated。默认false,如果希望启用顶级变量和函数名修改,并删除未使用的变量和函数,则设置为true。
+          keep_classnames: true, // 默认undefined,传递true以防止丢弃或混淆类名。
+          keep_fnames: false, // 默认false,传递true以防止函数名被丢弃或混淆。
+          // TODO 外层的keep_classnames和keep_fnames和compress的有啥区别or优先级?
+        },
+      }),
+      new CssMinimizerPlugin({
+        parallel: true, // 使用多进程并发执行,提升构建速度。
+      }), // css压缩,去除无用的空格等等
+    ],
+    // runtimeChunk: {
+    //   name: 'runtime'
+    // }
+  },
+  plugins: [
+    // 构建进度条
+    new WebpackBar(),
+    // http压缩
+    gzipEnable &&
+      new CompressionPlugin({
+        test: /\.(css|js)$/i,
+        threshold: 10 * 1024, // 大于10k的文件才进行压缩
+        minRatio: 0.8, // 只有压缩比这个比率更好的资产才会被处理(minRatio =压缩大小/原始大小),即压缩如果达不到0.8就不会进行压缩
+        algorithm: 'gzip', // 压缩算法
+      }),
+    // 注入script或link
+    new HtmlWebpackTagsPlugin({
+      append: false,
+      publicPath: '', // 默认会拼上output.publicPath,因为我们引入的是cdn的地址,因此不需要拼上output.publicPath,直接publicPath:'',这样就约等于拼上空字符串''
+      links: [],
+      // scripts: [
+      //   `https://unpkg.com/vue@${vueVersion}/dist/vue.global.prod.js`,
+      //   `https://unpkg.com/vue-router@${vueRouterVersion}/dist/vue-router.global.prod.js`,
+      //   `https://unpkg.com/axios@${axiosVersion}/dist/axios.min.js`,
+      //   `https://unpkg.com/vue-demi@${vueDemiVersion}/lib/index.iife.js`,
+      //   `https://unpkg.com/pinia@${piniaVersion}/dist/pinia.iife.prod.js`,
+      // ],
+    }),
+    // 将 CSS 提取到单独的文件中
+    new MiniCssExtractPlugin({
+      /**
+       * Options similar to the same options in webpackOptions.output
+       * all options are optional
+       */
+      filename: 'css/[name]-[contenthash:6].css',
+      chunkFilename: 'css/[id]-[contenthash:6].css',
+      ignoreOrder: false, // Enable to remove warnings about conflicting order
+    }),
+    // Css TreeShaking
+    // new PurgeCssPlugin({
+    //   /**
+    //    * 实际测试有一些bug,比如html里面有ul这个元素,css里面的.ul{},#ul{},ul{}都会打包进去???
+    //    * 在js文件里如果有给元素添加类,但是注释了,如:// divEle.className='test123',但是这个.test123一样会打包进去,得手动删除这行注释代码才行!
+    //    * 而且貌似不能对.vue文件进行treeShaking,而且构建出来的css文件会空白,没有任何内容,所以暂时不用这个插件了
+    //    */
+    //   paths: glob.sync(`${path.resolve(__dirname, '../src')}/**/*`, {
+    //     nodir: true,
+    //   }),
+    //   safelist: function () {
+    //     return {
+    //       standard: ['body', 'html'],
+    //     };
+    //   },
+    // }),
+    // 预加载
+    new PreloadPlugin({
+      rel: 'preload',
+      include: 'initial',
+      fileBlacklist: [/\.map$/, /hot-update\.js$/],
+    }),
+    // 预获取
+    new PreloadPlugin({
+      rel: 'prefetch',
+      include: 'asyncChunks',
+    }),
+    // new webpack.optimize.ModuleConcatenationPlugin(), //作用域提升。!!!在使用cdn时,axios有问题,先不用!
+  ].filter(Boolean),
+};
+
+export default prodConfig;

+ 39 - 0
script/constant.ts

@@ -0,0 +1,39 @@
+export const APP_NAME = process.env.VUE_APP_RELEASE_PROJECT_NAME;
+export const APP_ENV = process.env.VUE_APP_RELEASE_PROJECT_ENV;
+export const PUBLIC_PATH = process.env.PUBLIC_PATH;
+export const NODE_ENV = process.env.NODE_ENV;
+
+export const outputDir = 'dist'; // 输出目录名称
+export const eslintEnable = false; // 是否开启eslint(开发环境会读取它),会影响热更新速度,这里只是关闭了webpack的eslint插件,但可以依靠编辑器的eslint提示。
+export const webpackBarEnable = false; // 是否开启WebpackBar(开发环境会读取它),只要是插件就会影响构建速度,开发环境关掉它吧
+export const analyzerEnable = false; // 是否开启Webpack包分析
+export const gzipEnable = false; // 是否开启http压缩
+export const windicssEnable = false; // 是否开启windicss
+export const htmlWebpackPluginTitle = 'billd-live'; // htmlWebpackPlugin的标题
+
+export const outputStaticUrl = (isProduction: boolean) => {
+  // console.table({ isProduction, APP_NAME, APP_ENV, NODE_ENV, PUBLIC_PATH });
+  if (APP_ENV === undefined && APP_NAME === undefined) {
+    return '/';
+  }
+  if (isProduction) {
+    // 如果是jenkins里面构建,会执行build.sh,一定会有APP_NAME,APP_ENV可能是:'null'|'beta'|'preview'|'prod'
+    if (APP_ENV === 'null') {
+      return `/${APP_NAME!}/`;
+    } else {
+      return `/${APP_NAME!}/${APP_ENV!}/`;
+    }
+  } else {
+    if (APP_NAME === undefined) {
+      // 如果没设置项目名称,则判断是否设置了项目环境,如果设置了,则返回/项目环境/,否则返回'/'
+      return APP_ENV ? `/${APP_ENV}/` : '/';
+    }
+    if (APP_ENV === undefined) {
+      // 如果没设置项目环境,则判断是否设置了项目名称,如果设置了,则返回/项目名称/,否则返回'/'
+      return APP_NAME ? `/${APP_NAME}/` : '/';
+    }
+
+    // 返回:/项目名称/项目环境/
+    return `/${APP_NAME}/${APP_ENV}/`;
+  }
+};

+ 13 - 0
script/utils/chalkTip.ts

@@ -0,0 +1,13 @@
+import nodeChalk from 'chalk';
+import nodeEmoji from 'node-emoji';
+
+export const emoji = nodeEmoji;
+export const chalk = nodeChalk;
+export const chalkINFO = (v) =>
+  `${chalk.bgBlueBright.black(' INFO ')} ${chalk.blueBright(v)}`;
+export const chalkSUCCESS = (v) =>
+  `${chalk.bgGreenBright.black(' SUCCESS ')} ${chalk.greenBright(v)}`;
+export const chalkERROR = (v) =>
+  `${chalk.bgRedBright.black(' ERROR ')} ${chalk.redBright(v)}`;
+export const chalkWARN = (v) =>
+  `${chalk.bgHex('#FFA500').black(' WARN ')} ${chalk.hex('#FFA500')(v)}`;

+ 5 - 0
script/utils/path.ts

@@ -0,0 +1,5 @@
+import path from 'path';
+
+const appDir = process.cwd();
+
+export const resolveApp = (relativePath) => path.resolve(appDir, relativePath);

+ 23 - 0
src/App.vue

@@ -0,0 +1,23 @@
+<template>
+  <div>
+    <div class="btn">btn</div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { onMounted } from 'vue';
+
+import { WebSocketClass, wsConnectStatus } from '@/network/websocket';
+
+let _instance;
+
+onMounted(() => {
+  const instance = new WebSocketClass({ url: 'ws://localhost:3300' });
+  console.log(instance, 11);
+  instance.wsInstance?.on(wsConnectStatus.connect, () => {
+    console.log('连接websocket成功!');
+  });
+});
+</script>
+
+<style lang="scss" scoped></style>

BIN
src/assets/font/MiSans-Normal.ttf


BIN
src/assets/img/author.jpg


+ 8 - 0
src/assets/oldcss/animate/flash-img/index.css

@@ -0,0 +1,8 @@
+@keyframes flashImgMove {
+  0% {
+    transform: translate(-50%, -50%) rotate(45deg);
+  }
+  100% {
+    transform: translate(50%, 80%) rotate(45deg);
+  }
+}

+ 40 - 0
src/assets/oldcss/animate/flash-img/index.scss

@@ -0,0 +1,40 @@
+// 请尽量不要全局引入这个文件,该文件有一个@keyframes是一定并且只会被编译一次的
+// 虽然@keyframes会被编译,但是如果有css模块化的话,其实也不会受影响
+
+@keyframes flashImgMove {
+  0% {
+    transform: translate(-50%, -50%) rotate(45deg);
+  }
+  100% {
+    transform: translate(50%, 80%) rotate(45deg);
+  }
+}
+
+%flashImgMixin {
+  position: relative;
+  display: inline-flex;
+  overflow: hidden;
+  &::after {
+    position: absolute;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    left: 0;
+    margin: auto;
+    width: 20%;
+    background: white;
+    content: '';
+    opacity: 0.6;
+    filter: blur(6px);
+    transform: translate(0%, -25%) rotate(45deg);
+  }
+}
+
+// 图片闪光
+@mixin flashImg($duration: 1.5s, $height: 200%) {
+  @extend %flashImgMixin;
+  &::after {
+    height: $height;
+    animation: flashImgMove $duration infinite ease-out;
+  }
+}

+ 8 - 0
src/assets/oldcss/animate/flash-txt/index.css

@@ -0,0 +1,8 @@
+@keyframes flashTxtMove {
+  0% {
+    left: 0;
+  }
+  100% {
+    left: 120%;
+  }
+}

+ 37 - 0
src/assets/oldcss/animate/flash-txt/index.scss

@@ -0,0 +1,37 @@
+// 请尽量不要全局引入这个文件,该文件有一个@keyframes是一定并且只会被编译一次的
+// 虽然@keyframes会被编译,但是如果有css模块化的话,其实也不会受影响
+
+@keyframes flashTxtMove {
+  0% {
+    left: 0;
+  }
+  100% {
+    left: 120%;
+  }
+}
+
+%flashTxtMixin {
+  position: relative;
+  display: inline-flex;
+  overflow: hidden;
+  z &::after {
+    position: absolute;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    left: 0;
+    width: 10%;
+    height: 200%;
+    background: white;
+    content: '';
+    opacity: 0.6;
+    filter: blur(4px);
+    transform: translate(-50%, -25%) rotate(45deg);
+    animation: flashTxtMove 2s infinite ease;
+  }
+}
+
+// 文字闪光
+@mixin flashTxt {
+  @extend %flashTxtMixin;
+}

+ 11 - 0
src/assets/oldcss/animate/loading-rotate/index.css

@@ -0,0 +1,11 @@
+@keyframes rotate {
+  0% {
+    transform: rotate(0);
+  }
+  50% {
+    transform: rotate(180deg);
+  }
+  100% {
+    transform: rotate(360deg);
+  }
+}

+ 73 - 0
src/assets/oldcss/animate/loading-rotate/index.scss

@@ -0,0 +1,73 @@
+// 请尽量不要全局引入这个文件,该文件有一个@keyframes是一定并且只会被编译一次的
+// 虽然@keyframes会被编译,但是如果有css模块化的话,其实也不会受影响
+
+@keyframes rotate {
+  0% {
+    transform: rotate(0);
+  }
+  50% {
+    transform: rotate(180deg);
+  }
+  100% {
+    transform: rotate(360deg);
+  }
+}
+
+%loadingRotateChangeMixin {
+  position: relative;
+  overflow: hidden;
+  margin: 0 auto;
+  border-radius: 50%;
+
+  &::before {
+    position: absolute;
+    bottom: 50%;
+    left: 50%;
+    z-index: 1;
+    content: '';
+    transform-origin: left bottom;
+    animation: rotate 1.5s infinite linear;
+  }
+  &::after {
+    position: absolute;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    left: 0;
+    z-index: 2;
+    margin: auto;
+    border-radius: 50%;
+    background-color: white;
+    content: '';
+  }
+}
+
+// 加载中,旋转
+@mixin loadingRotateChange($px: 30px, $color: gray) {
+  width: $px;
+  height: $px;
+  background-color: rgba($color, 0.5);
+
+  @extend %loadingRotateChangeMixin;
+
+  &::before {
+    width: $px + 10px;
+    height: $px + 10px;
+    background-color: $color;
+  }
+  &::after {
+    width: $px - 10px;
+    height: $px - 10px;
+    content: '';
+  }
+}
+
+// .aaa {
+//   @include loadingRotateChange();
+// }
+// .bbb {
+//   @include loadingRotateChange();
+// }
+// .ccc {
+//   @include loadingRotateChange();
+// }

+ 16 - 0
src/assets/oldcss/animate/loading-size/index.css

@@ -0,0 +1,16 @@
+@keyframes big {
+  0%, 100% {
+    transform: scale(1);
+  }
+  50% {
+    transform: scale(0);
+  }
+}
+@keyframes small {
+  0%, 100% {
+    transform: scale(0);
+  }
+  50% {
+    transform: scale(1);
+  }
+}

+ 79 - 0
src/assets/oldcss/animate/loading-size/index.scss

@@ -0,0 +1,79 @@
+// 请尽量不要全局引入这个文件,该文件有两个@keyframes是一定并且只会被编译一次的
+// 虽然@keyframes会被编译,但是如果有css模块化的话,其实也不会受影响
+
+@keyframes big {
+  0%,
+  100% {
+    transform: scale(1);
+  }
+  50% {
+    transform: scale(0);
+  }
+}
+
+@keyframes small {
+  0%,
+  100% {
+    transform: scale(0);
+  }
+  50% {
+    transform: scale(1);
+  }
+}
+
+%loadingSizeChangeMixin {
+  position: relative;
+  overflow: hidden;
+  margin: 0 auto;
+  border-radius: 50%;
+  opacity: 0.6;
+
+  &::before {
+    position: absolute;
+    z-index: 1;
+    border-radius: 50%;
+    content: '';
+    animation: small 2s infinite ease;
+  }
+  &::after {
+    position: absolute;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    left: 0;
+    margin: auto;
+    border-radius: 50%;
+    content: '';
+    animation: big 2s infinite ease;
+  }
+}
+
+// 加载中,大小变化
+@mixin loadingSizeChange($px: 30px, $color: gray) {
+  width: $px;
+  height: $px;
+
+  @extend %loadingSizeChangeMixin;
+
+  &::before {
+    width: $px;
+    height: $px;
+    background-color: $color;
+    opacity: 0.5;
+  }
+  &::after {
+    width: $px;
+    height: $px;
+    background-color: $color;
+  }
+}
+
+// .aaa {
+//   @include loadingSizeChange();
+// }
+// .bbb {
+//   @include loadingSizeChange();
+// }
+// .ccc {
+//   @include loadingSizeChange();
+// }

+ 79 - 0
src/assets/oldcss/common.scss

@@ -0,0 +1,79 @@
+// flex的水平垂直居中
+%flexCenter {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+// 解决图片缩小模糊问题
+%imgBlur {
+  -ms-interpolation-mode: nearest-neighbor;
+
+  image-rendering: -moz-crisp-edges;
+  image-rendering: -o-crisp-edges;
+  image-rendering: -webkit-optimize-contrast;
+  image-rendering: crisp-edges;
+}
+
+// 单行省略号
+%singleEllipsis {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+// 多行省略号在mixin里面
+
+// ltr
+%ltr {
+  direction: ltr;
+}
+
+// rtl
+%rtl {
+  direction: rtl;
+}
+
+// 置灰
+%grayscale {
+  filter: grayscale(100%);
+}
+
+// 隐藏滚动条:https://developer.mozilla.org/zh-CN/docs/Web/CSS/::-webkit-scrollbar
+%hideScrollbar {
+  // 整个滚动条
+  &::-webkit-scrollbar {
+    width: 0px;
+    height: 0px;
+    border-radius: 0px;
+    background: rgba(0, 0, 0, 0);
+  }
+
+  // 滚动条轨道
+  &::-webkit-scrollbar-track {
+    border-radius: 0px;
+    background: rgba(0, 0, 0, 0);
+    box-shadow: rgba(0, 0, 0, 0);
+  }
+
+  // 滚动条上的滚动滑块
+  &::-webkit-scrollbar-thumb {
+    border-radius: 0px;
+    background: transparent;
+  }
+}
+
+// 长文本折行
+%long-text {
+  white-space: pre-line;
+  word-wrap: break-word;
+}
+
+// float清除浮动,在浮动的元素的父元素添加这个class:https://developer.mozilla.org/zh-CN/docs/Web/CSS/clear
+%clearfix {
+  &:after {
+    display: block;
+    clear: both;
+    content: '';
+  }
+}

+ 21 - 0
src/assets/oldcss/constant.scss

@@ -0,0 +1,21 @@
+// constant.scss放的是一些sass变量
+// 用法:.header{color: $theme-color1;}
+// 编译结果:.header{color: #0984e3;}
+
+// :root 这个 CSS 伪类匹配文档树的根元素。对于 HTML 来说,:root 表示 <html> 元素,除了优先级更高之外,与 html 选择器相同。
+:root {
+  --dark-primary-color: 'red'; //CSS 自定义属性(变量)
+  --light-primary-color: blue; //CSS 自定义属性(变量)
+}
+
+$theme-color1: #0984e3;
+$theme-color2: #c92929;
+$theme-color3: #1bb61b;
+$theme-dark-btn-color: var(
+  --dark-primary-color,
+  'skyblue'
+); //使用var()函数获取变量值。有--dark-primary-color就用--dark-primary-color,否则用skyblue
+$theme-light-btn-color: var(
+  --light-primary-color,
+  'green'
+); //使用var()函数获取变量值。有--light-primary-color就用--light-primary-color,否则用green

+ 11 - 0
src/assets/oldcss/global.scss

@@ -0,0 +1,11 @@
+// sass-loader的additionalData会引用该global.scss文件
+@import './constant.scss';
+@import './mixin.scss';
+@import './common.scss';
+@import './utils.scss';
+@import './animate/flash-txt';
+@import './animate/flash-img';
+
+// 这段注释会被删除
+/* 这段注释不会被删除,但是压缩模式下还是会被删除 */
+/*! 这段注释在压缩模式下也不会被删除,常用于声明版权 */

+ 168 - 0
src/assets/oldcss/mixin.scss

@@ -0,0 +1,168 @@
+%fullMixin {
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+}
+
+%colorTextMixin {
+  position: relative;
+  overflow: hidden;
+  // 因为colorTextMixin会先编译,它生成的css在colorText的前面,
+  // 所以colorTextMixin的background-clip会被后面的colorText的background覆盖,
+  // 因此colorTextMixin的background-clip得使用!important
+  -webkit-background-clip: text !important;
+  background-clip: text !important;
+  color: transparent;
+}
+
+%multiEllipsisMixin {
+  display: -webkit-box;
+  overflow: hidden;
+  -webkit-box-orient: vertical;
+  text-overflow: -o-ellipsis-lastline;
+  text-overflow: ellipsis;
+  word-break: break-all;
+}
+
+%arrowMixin {
+  position: relative;
+  display: inline-block;
+  &::after {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: inherit;
+    height: inherit;
+    border-width: 0 0 1px 1px;
+    border-style: solid;
+    border-color: inherit;
+    background-color: inherit;
+    content: '';
+    transition: all 0.3s ease;
+  }
+}
+
+// 充满屏幕
+@mixin full($position: absolute) {
+  position: $position;
+
+  @extend %fullMixin;
+}
+
+// 多行省略号
+@mixin multiEllipsis($row: 2) {
+  @extend %multiEllipsisMixin;
+  -webkit-line-clamp: ($row);
+  line-clamp: ($row);
+}
+
+// 箭头
+@mixin arrow($position, $size: 10px) {
+  width: $size;
+  height: $size;
+
+  @extend %arrowMixin;
+  &::after {
+    @if $position == 'left' {
+      transform: rotate(45deg);
+    }
+    @if $position == 'right' {
+      transform: rotate(-135deg);
+    }
+    @if $position == 'top' {
+      transform: rotate(135deg);
+    }
+    @if $position == 'bottom' {
+      transform: rotate(-45deg);
+    }
+  }
+}
+
+// 颜色文字,用法:@include colorText('#8a2387, #f27121', 'to left');
+@mixin colorText($color: '#8a2387, #e94057, #f27121', $position: 'to left') {
+  background: -webkit-linear-gradient(
+    #{$position},
+    #{$color}
+  ); /* Chrome 10-25, Safari 5.1-6 */
+  background: linear-gradient(
+    #{$position},
+    #{$color}
+  ); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
+
+  @extend %colorTextMixin;
+}
+
+// 设置背景,用法:@include colorText('#8a2387, #f27121', 'to left');
+@mixin setBackground(
+  $url,
+  $x: center,
+  $y: center,
+  $repeat: no-repeat,
+  $size: contain
+) {
+  background-image: #{url($url)};
+  background-position: $x $y;
+  background-size: $size;
+  background-repeat: $repeat;
+}
+
+// 设置定位位置(默认absolute定位)
+@mixin setPosition(
+  $position: absolute,
+  $top: initial,
+  $right: initial,
+  $bottom: initial,
+  $left: initial
+) {
+  position: $position;
+  top: $top;
+  right: $right;
+  bottom: $bottom;
+  left: $left;
+}
+
+// 彩色阴影
+@mixin colorShadow($background: skyblue) {
+  position: relative;
+  width: 200px;
+  height: 200px;
+  img {
+    position: absolute;
+    top: 0;
+    left: 0;
+    z-index: 2;
+    width: 100%;
+    height: 100%;
+    border-radius: 100%;
+
+    object-fit: cover;
+  }
+  &::after {
+    position: absolute;
+    top: 10%;
+    left: 0;
+    z-index: 1;
+    width: 100%;
+    height: 100%;
+    border-radius: 100%;
+    background: $background;
+    background-position: center;
+    background-size: cover;
+    background-repeat: no-repeat;
+    content: '';
+    filter: blur(10px) brightness(80%) opacity(0.8);
+    transform: scale(0.95);
+  }
+}
+
+// 黑色阴影
+@mixin shadow($color: black) {
+  // box-shadow: 水平偏移 垂直偏移 模糊半径 阴影颜色;
+  box-shadow: 0 10px 10px rgba(0, 0, 0, 0.1), 0 10px 10px #{$color};
+}
+
+// 高斯模糊/毛玻璃效果
+@mixin blur($px: 5px) {
+  filter: blur($px);
+}

+ 9 - 0
src/assets/oldcss/utils.scss

@@ -0,0 +1,9 @@
+@use 'sass:math';
+
+// 用法:.header { width: px2vw(360) }
+// 编译结果:.header { width: 96vw; }
+
+@function px2vw($px) {
+  // 根据屏幕像素为375px进行适配
+  @return math.div($px, 375) * 100vw;
+}

+ 34 - 0
src/components/Baby/index.vue

@@ -0,0 +1,34 @@
+<template>
+  <div class="baby-wrap">
+    <div>Baby组件</div>
+    <div>pinia的counter: {{ counter }}</div>
+
+    <button @click="handlecounter">设置counter</button>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, toRef } from 'vue';
+
+import { useAppStore } from '@/store/app';
+
+export default defineComponent({
+  components: {},
+  setup() {
+    const appStore = useAppStore();
+    const counter = toRef(appStore, 'counter');
+    const handlecounter = () => {
+      appStore.setCounter((counter.value += 1));
+    };
+
+    return { counter, handlecounter };
+  },
+});
+</script>
+
+<style lang="scss" scoped>
+.baby-wrap {
+  padding: 20px;
+  background-color: orchid;
+}
+</style>

+ 32 - 0
src/components/Card/index.vue

@@ -0,0 +1,32 @@
+<template>
+  <div class="card-wrap">
+    <div>Card组件</div>
+    <div>pinia的counter: {{ counter }}</div>
+    <BabyCpt></BabyCpt>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, toRef } from 'vue';
+
+import BabyCpt from '@/components/Baby/index.vue';
+import { useAppStore } from '@/store/app';
+
+export default defineComponent({
+  name: 'Card',
+  components: { BabyCpt },
+  setup() {
+    const appStore = useAppStore();
+    const counter = toRef(appStore, 'counter');
+
+    return { counter };
+  },
+});
+</script>
+
+<style lang="scss" scoped>
+.card-wrap {
+  padding: 20px;
+  background-color: pink;
+}
+</style>

+ 158 - 0
src/constant.ts

@@ -0,0 +1,158 @@
+import path from 'path';
+
+export enum PROJECT_ENV_ENUM {
+  development = 'development',
+  prod = 'prod',
+  beta = 'beta',
+}
+
+export const PROJECT_NAME = process.env.NODE_APP_RELEASE_PROJECT_NAME as string;
+export const PROJECT_ENV = process.env
+  .NODE_APP_RELEASE_PROJECT_ENV as PROJECT_ENV_ENUM;
+export const PROJECT_PORT = process.env.NODE_APP_RELEASE_PROJECT_PORT as string;
+export const PROJECT_NODE_ENV = process.env.NODE_ENV as string;
+
+export const STATIC_DIR = path.join(__dirname, './public/'); // 静态文件目录
+export const UPLOAD_DIR = path.join(__dirname, './upload/'); // 上传文件接口接收到的文件存放的目录
+export const SECRET_FILE = path.join(
+  __dirname,
+  PROJECT_NODE_ENV === 'development'
+    ? './config/secret.ts'
+    : './config/secret.js'
+); // 秘钥文件
+export const SECRETTEMP_FILE = path.join(
+  __dirname,
+  PROJECT_NODE_ENV === 'development'
+    ? './config/secretTemp.ts'
+    : './config/secretTemp.js'
+); // 秘钥文件模板
+export const QQ_MAIL_CONFIG = {
+  from: '2274751790@qq.com', // sender address
+  to: '2274751790@qq.com', // list of receivers
+};
+
+export const ERROR_HTTP_CODE = {
+  serverError: 10000, // 服务器错误
+  banIp: 1000,
+  adminDisableUser: 1001,
+  notFound: 1002, // 返回了404的http状态码
+  errStatusCode: 1003, // 返回了即不是200也不是404的http状态码
+  shutdown: 1004, // 停机维护
+};
+
+export const ALLOW_HTTP_CODE = {
+  ok: 200, // 成功
+  apiCache: 304, // 接口缓存
+  paramsError: 400, // 参数错误
+  unauthorized: 401, // 未授权
+  forbidden: 403, // 权限不足
+  notFound: 404, // 未找到
+  serverError: 500, // 服务器错误
+};
+
+export const HTTP_ERROE_MSG = {
+  paramsError: '参数错误!',
+  unauthorized: '未授权!',
+  forbidden: '权限不足!',
+  notFound: '未找到!',
+  serverError: '服务器错误!',
+};
+
+export const HTTP_SUCCESS_MSG = {
+  GET: '获取成功!',
+  POST: '新增成功!',
+  PUT: '修改成功!',
+  DELETE: '删除成功!',
+};
+
+export const BLACKLIST_TYPE = {
+  banIp: 1, // 频繁操作
+  adminDisableUser: 2, // 被管理员禁用
+};
+
+export const COMMON_ERR_MSG = {
+  banIp: '此ip已被禁用,请联系管理员处理!',
+  jwtExpired: '登录信息过期!',
+  invalidToken: '非法token!',
+  adminDisableUser: '你的账号已被管理员禁用,请联系管理员处理!',
+  shutdown: '停机维护中~',
+};
+
+// 没有用到这个DisableEnum枚举,eslint会报错
+// export enum DisableEnum {
+//   'banIp' = 1,
+//   'adminDisableUser' = 2,
+// }
+
+// 发送邮件结果类型
+export const VERIFY_EMAIL_RESULT_CODE = {
+  ok: '发送成功!',
+  more: '一天只能发5次验证码!',
+  later: '一分钟内只能发1次验证码,请稍后再试!',
+  err: '验证码错误或已过期!',
+  system: '发送邮件错误!',
+};
+
+// redis前缀
+export const REDIS_PREFIX = {
+  emailLogin: `${PROJECT_NAME}-${PROJECT_ENV}-emailLogin`, // 登录不区分前后台
+  emailRegister: `${PROJECT_NAME}-${PROJECT_ENV}-emailRegister`, // 注册不区分前后台
+  userBindEmail: `${PROJECT_NAME}-${PROJECT_ENV}-userBindEmail`, // 用户绑定邮箱
+  userCancelBindEmail: `${PROJECT_NAME}-${PROJECT_ENV}-userCancelBindEmail`, // 用户取消绑定邮箱
+  fileProgress: `${PROJECT_NAME}-${PROJECT_ENV}-fileProgress`, // 文件上传进度
+  chunkFileProgress: `${PROJECT_NAME}-${PROJECT_ENV}-chunkFileProgress`, // 分片文件上传进度
+  chooseSongList: `${PROJECT_NAME}-${PROJECT_ENV}-chooseSongList`, // 点歌列表
+  historyHightOnlineNum: `${PROJECT_NAME}-${PROJECT_ENV}-historyHightOnlineNum`, // 历史最高同时在线数
+  currDayHightOnlineNum: `${PROJECT_NAME}-${PROJECT_ENV}-currDayHightOnlineNum`, // 当前最高同时在线数
+  onlineUser: `${PROJECT_NAME}-${PROJECT_ENV}-onlineUser`, // 当前在线用户
+  onlineVisitor: `${PROJECT_NAME}-${PROJECT_ENV}-onlineVisitor`, // 当前在线游客
+  onlineList: `${PROJECT_NAME}-${PROJECT_ENV}-onlineList`, // 当前在线游客+用户
+  live: `${PROJECT_NAME}-${PROJECT_ENV}-live`, // 在线游客+用户
+};
+
+// 平台类型
+export const THIRD_PLATFORM = {
+  website: 1, // 站内(user表里面的用户就是这个类型,但是不记录在third_user表里)
+  qq_www: 2, // qq前台(现在不存在这个类型了)
+  qq_admin: 3, // qq后台
+  github: 4, // github
+  email: 5, // 邮箱
+};
+
+// 监控任务
+export const MONIT_JOB = {
+  MEMORY: 'monitMemoryJob', // 监控内存任务
+  PROCESS: 'monitProcessJob', // 监控node进程任务
+  BACKUPSDB: 'monitBackupsDbJob', // 监控备份数据库任务
+  QINIUCDN: 'monitQiniuCDNJob', // 监控七牛云cdn任务
+  DELETELOG: 'monitDeleteLog', // 监控删除日志
+};
+
+// 监控类型
+export const MONIT_TYPE = {
+  MEMORY_LOG: 1, // 服务器内存日志
+  MEMORY_THRESHOLD: 2, // 服务器内存达到阈值
+  QINIU_CDN: 3, // 监控七牛云
+  VUE3_BLOG_SERVER_NODE_PROCESS: 4, // 监控node进程
+  RESTART_PM2: 5, // 重启pm2
+  CLEAR_CACHE: 6, // 清除buff/cache
+  BACKUPS_DB_OK: 7, // 备份数据库成功
+  BACKUPS_DB_ERR: 8, // 备份数据库失败
+};
+
+// 七牛云文件上传进度类型
+export enum QINIU_UPLOAD_PROGRESS_TYPE {
+  fileProgress = 1,
+  chunkFileProgress = 2,
+}
+export const QINIU_PROGRESS_LOG_V1 = path.join(UPLOAD_DIR, 'progressv1.log'); // 上传文件接口接收到的文件存放的目录
+export const QINIU_PROGRESS_LOG_V2 = path.join(UPLOAD_DIR, 'progressv2.log'); // 上传文件接口接收到的文件存放的目录
+export const QINIU_CDN_DOMAIN = 'resource.hsslive.cn';
+export const QINIU_CDN_URL = 'https://resource.hsslive.cn/';
+export const QINIU_BUCKET = 'hssblog'; // 七牛云bucket
+export enum QINIU_PREFIX {
+  'image/' = 'image/',
+  'backupsDatabase/' = 'backupsDatabase/',
+  'media/' = 'media/',
+  'nuxt-blog-client/' = 'nuxt-blog-client/',
+}

+ 1 - 0
src/interface.ts

@@ -0,0 +1 @@
+// 这里放项目里面的类型

+ 4 - 0
src/main.scss

@@ -0,0 +1,4 @@
+body {
+  padding: 0;
+  margin: 0;
+}

+ 18 - 0
src/main.ts

@@ -0,0 +1,18 @@
+import './main.scss';
+import './showBilldVersion';
+
+import { createApp } from 'vue';
+
+import router from '@/router/index';
+import store from '@/store/index';
+
+import App from './App.vue';
+
+// import 'windi.css'; // windicss-webpack-plugin会解析windi.css这个MODULE_ID
+
+const app = createApp(App);
+
+app.use(store);
+app.use(router);
+
+app.mount('#app');

+ 71 - 0
src/network/websocket.ts

@@ -0,0 +1,71 @@
+import { Socket, io } from 'socket.io-client';
+
+// websocket连接状态
+export const wsConnectStatus = {
+  /** 已连接 */
+  connection: 'connection',
+  /** 连接中 */
+  connecting: 'connecting',
+  /** 已连接 */
+  connected: 'connected',
+  /** 断开连接中 */
+  disconnecting: 'disconnecting',
+  /** 已断开连接 */
+  disconnect: 'disconnect',
+  /** 重新连接 */
+  reconnect: 'reconnect',
+  /** 客户端的已连接 */
+  connect: 'connect',
+};
+
+export class WebSocketClass {
+  wsInstance: Socket | null = null;
+  wsUrl = 'ws://localhost:3300';
+
+  constructor({ url }) {
+    if (!window.WebSocket) {
+      console.error('当前环境不支持WebSocket!');
+      alert('当前环境不支持WebSocket!');
+      return;
+    }
+    this.wsUrl = url;
+    this.wsInstance = io(url, { transports: ['websocket'] });
+    // 连接websocket成功
+    this.wsInstance.on(wsConnectStatus.connect, (socket: Socket) => {
+      console.log('连接websocket成功', socket);
+    });
+    // 用户加入房间
+    this.wsInstance.on('join', (data) => {
+      console.log('用户加入房间', data);
+    });
+    // 用户离开房间
+    this.wsInstance.on('leave', (data) => {
+      console.log('用户离开房间', data);
+    });
+    // 监听连接断开
+    this.wsInstance.on('disconnect', () => {
+      console.log('监听连接断开');
+    });
+    // 用户发送 offer
+    this.wsInstance.on('offer', (data) => {
+      console.log('用户发送 offer', data);
+    });
+    // 用户发送 answer
+    this.wsInstance.on('answer', (data) => {
+      console.log('用户发送 answer', data);
+    });
+    // 用户发送消息
+    this.wsInstance.on('message', (data) => {
+      console.log('用户发送消息', data);
+    });
+  }
+
+  // 发送websocket消息
+  send = () => {};
+
+  // 手动关闭websocket连接
+  close = () => {};
+
+  // 连接websocket
+  connect = () => {};
+}

+ 31 - 0
src/router/index.ts

@@ -0,0 +1,31 @@
+import { outputStaticUrl } from 'script/constant';
+import { createRouter, createWebHistory } from 'vue-router';
+
+import type { RouteRecordRaw } from 'vue-router';
+
+// 默认路由
+export const defaultRoutes: RouteRecordRaw[] = [
+  {
+    // name: '',
+    path: '/',
+    component: () => import('@/views/home/home.vue'),
+  },
+  {
+    name: 'login',
+    path: '/login',
+    component: () => import('@/views/login/login.vue'),
+  },
+  {
+    name: 'about',
+    path: '/about',
+    component: () => import('@/views/about/about.vue'),
+  },
+];
+const router = createRouter({
+  routes: defaultRoutes,
+  history: createWebHistory(
+    outputStaticUrl(process.env.NODE_ENV === 'production')
+  ),
+});
+
+export default router;

+ 10 - 0
src/shims-vue.d.ts

@@ -0,0 +1,10 @@
+declare module '*.vue' {
+  /* eslint-disable */
+  import type { DefineComponent } from 'vue';
+  const component: DefineComponent<{}, {}, any>;
+  export default component;
+}
+
+declare global {
+  // eslint-disable-next-line
+}

+ 11 - 0
src/showBilldVersion.ts

@@ -0,0 +1,11 @@
+import BilldDeploy from 'billd-deploy/package.json';
+import BilldHtmlWebpackPlugin from 'billd-html-webpack-plugin/package.json';
+import BilldScss from 'billd-scss/package.json';
+import BilldUtils from 'billd-utils/package.json';
+
+console.table({
+  'billd-utils version': BilldUtils.version,
+  'billd-scss version': BilldScss.version,
+  'billd-deploy version': BilldDeploy.version,
+  'billd-html-webpack-plugin version': BilldHtmlWebpackPlugin.version,
+});

+ 14 - 0
src/store/app/index.ts

@@ -0,0 +1,14 @@
+import { defineStore } from 'pinia';
+
+export const useAppStore = defineStore('app', {
+  state: () => {
+    return {
+      counter: 1,
+    };
+  },
+  actions: {
+    setCounter(res) {
+      this.counter = res;
+    },
+  },
+});

+ 4 - 0
src/store/index.ts

@@ -0,0 +1,4 @@
+import { createPinia } from 'pinia';
+
+const store = createPinia();
+export default store;

+ 26 - 0
src/store/user/index.ts

@@ -0,0 +1,26 @@
+import { mockAjax } from 'billd-utils';
+import { defineStore } from 'pinia';
+
+type RootState = {
+  detail: any;
+};
+
+export const useUserStore = defineStore('user', {
+  state: () => {
+    return {
+      detail: { id: -1 },
+    } as RootState;
+  },
+  actions: {
+    async setDetail(payload: number) {
+      console.log('setDetail的payload', payload);
+      try {
+        const data = await mockAjax({ flag: payload === 1 });
+        console.log(data);
+        this.detail = data;
+      } catch (error) {
+        console.log(error);
+      }
+    },
+  },
+});

+ 23 - 0
src/utils/index.ts

@@ -0,0 +1,23 @@
+/** 模拟ajax请求 */
+export const mockAjax = ({ flag = false, delay = 500 }) => {
+  return new Promise<{ code: Number; data: { id: number }; msg: string }>(
+    (resolve, rejected) => {
+      setTimeout(() => {
+        if (flag) {
+          resolve({
+            code: 200,
+            data: {
+              id: 1,
+            },
+            msg: '请求成功',
+          });
+        } else {
+          rejected({
+            code: 400,
+            msg: '请求失败',
+          });
+        }
+      }, delay);
+    }
+  );
+};

+ 30 - 0
src/views/about/about.vue

@@ -0,0 +1,30 @@
+<template>
+  <div class="about-wrap">
+    <h1>about页面</h1>
+    <p class="myfont">MIUI 13 采用全新系统字体 MiSans</p>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+
+export default defineComponent({
+  components: {},
+  setup() {},
+});
+</script>
+
+<style lang="scss" scoped>
+@font-face {
+  font-family: 'MiSans';
+  src: url('@/assets/font/MiSans-Normal.ttf');
+}
+
+.about-wrap {
+  padding: 20px;
+  background-color: bisque;
+  .myfont {
+    font-family: 'MiSans';
+  }
+}
+</style>

+ 52 - 0
src/views/home/home.vue

@@ -0,0 +1,52 @@
+<template>
+  <div class="home-wrap">
+    <h1>home页面</h1>
+    <div>pinia的user: {{ userDetail }}</div>
+    <div>pinia的counter: {{ counter }}</div>
+    <button @click="handlecounter">设置counter</button>
+    <button @click="handleInfo(1)">模拟异步请求成功</button>
+    <button @click="handleInfo(2)">模拟异步请求失败</button>
+    <CardCpt></CardCpt>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, toRef, ref } from 'vue';
+
+import CardCpt from '@/components/Card/index.vue';
+import { useAppStore } from '@/store/app';
+import { useUserStore } from '@/store/user';
+
+export default defineComponent({
+  components: { CardCpt },
+  setup() {
+    const userStore = useUserStore();
+    const appStore = useAppStore();
+    const userInfo = ref(userStore);
+    const userDetail = toRef(userStore, 'detail');
+    const counter = toRef(appStore, 'counter');
+    const handlecounter = () => {
+      appStore.setCounter((counter.value += 1));
+    };
+    const handleInfo = (num) => {
+      userStore.setDetail(num).then(
+        (res) => {
+          console.log(res);
+        },
+        (err) => {
+          console.log(err);
+        }
+      );
+    };
+
+    return { userInfo, userDetail, counter, handlecounter, handleInfo };
+  },
+});
+</script>
+
+<style lang="scss" scoped>
+.home-wrap {
+  padding: 20px;
+  background-color: skyblue;
+}
+</style>

+ 22 - 0
src/views/login/login.vue

@@ -0,0 +1,22 @@
+<template>
+  <div class="login-wrap">
+    <h1>login页面</h1>
+    <p>欢迎登录!</p>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+
+export default defineComponent({
+  components: {},
+  setup() {},
+});
+</script>
+
+<style lang="scss" scoped>
+.login-wrap {
+  padding: 20px;
+  background-color: aqua;
+}
+</style>

+ 51 - 0
tsconfig.json

@@ -0,0 +1,51 @@
+{
+  "compilerOptions": {
+    "target": "esnext",
+    "module": "esnext",
+    "strict": true,
+    "noImplicitAny": false,
+    "lib": ["dom", "esnext"],
+    "jsx": "preserve",
+    "moduleResolution": "node",
+    "esModuleInterop": true, // ES 模块互操作,import React from 'react';react是module.exports导出的,因此需要设置该属性
+    "forceConsistentCasingInFileNames": true, // 在文件名中强制使用一致的大小写
+    "skipLibCheck": true, // 跳过d.ts声明文件的类型检查。
+    "resolveJsonModule": true, //解析json模块
+    "allowJs": true,
+    "baseUrl": "./",
+    /**
+     * 当 TypeScript 编译文件时,它在输出目录中保持与输入目录中相同的目录结构。
+     * 如果你设置了allowJs:true,就会导致输入目录的js文件,编译后又输出到了同样的目录结构,
+     * 就会报错:无法写入文件xxx,因为它会覆盖输入文件。
+     * 因此手动设置输出目录,让输出目录不和原本的目录一致即可
+     */
+    "outDir": "./dist",
+    "paths": {
+      "@/*": ["src/*"],
+      "script/*": ["script/*"]
+    }
+    // "paths": {
+    //   "@/*": ["./src/*"] // 这样写的话,@/不会提示路径,得使用baseUrl:'./'+paths:{"@/*": ["src/*"]}这样才的话@/才会提示路径
+    // }
+  },
+  // 仅仅匹配这些文件,除了src以外的文件都不会被匹配
+  "include": [
+    "src/**/*.ts",
+    "src/**/*.tsx",
+    "src/**/*.vue",
+    "script/**/*.ts",
+    "./.eslintrc.js",
+    "./babel.config.js",
+    "./postcss.config.js",
+    "windi.config.ts"
+  ],
+  // https://github.com/microsoft/TypeScript/wiki/Performance
+  "exclude": ["**/node_modules"],
+  // ts-node的时候会读取这里的配置
+  "ts-node": {
+    "compilerOptions": {
+      "module": "commonjs" // 指定生成什么模块代码。
+    },
+    "transpileOnly": true // 只编译,报警告或者错误一样运行
+  }
+}

+ 20 - 0
windi.config.ts

@@ -0,0 +1,20 @@
+// eslint-disable-next-line
+import { defineConfig } from 'windicss/helpers';
+
+console.log(
+  '\x1B[0;37;44m INFO \x1B[0m',
+  '\x1B[0;;34m ' +
+    `读取了: ${__filename.slice(__dirname.length + 1)}` +
+    ' \x1B[0m'
+);
+
+export default defineConfig({
+  darkMode: 'class', // or 'media'
+
+  extract: {
+    // A common use case is scanning files from the root directory
+    include: ['**/*.{vue,html,jsx,tsx}'],
+    // if you are excluding files, make sure you always include node_modules and .git
+    exclude: ['node_modules', '.git', 'dist'],
+  },
+});