# oh-admin
**Repository Path**: dingshaohua-com/oh-admin
## Basic Information
- **Project Name**: oh-admin
- **Description**: 中后台admin
- **Primary Language**: JavaScript
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 1
- **Forks**: 0
- **Created**: 2022-12-27
- **Last Updated**: 2024-12-11
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
---
sidebar_position: 1
---
# 创建一个项目
## 初始化项目
使用脚手架工具初始化项目,vue2 以及配套的 vue cli 均已不再维护。
vue 官网都推荐使用自家新品 [vite](https://cn.vitejs.dev/)。
```shell
npm create vite
```
根据提示,选择合适的配置项,即可创建一个项目,看一下 vue 项目的主流选项:vue3+ts。
## 使用 less
因为 vite 并没有提供默认的内置支持,所以我们需要自己安装
```shell
npm install -D less less-loader
```
要求项目中,书写样式必须 scope,防止样式污染
```html
```
## 使用短链接
当路径较为复杂的时候,导入起来就比较麻烦,可以通过一些配置来设置短路径
开启功能支持:在 vite 配置文件中增加如下配置
```js
// vite.config.ts
export default defineConfig({
// ...
resolve: {
alias: {
"@": "/src",
},
},
});
```
这样仅仅是功能可以使用了,
但若是 ts 项目,还需要在 ts 配置文件中做一些配置以使得支持 ts(如 ts 报错 智能路径提示)。
```js
// tsconfig.json
{
"compilerOptions": {
/* Sort path */
"baseUrl": ".",
"paths": {"@/*": ["src/*"]}
}
}
```

:::tip 提示
在新的 vite 脚手架创建的项目中,我发现项目中 ts 相关配置被分别提取到了 `tsconfig.app.json`和 `tsconfig.node.json`中,这可能是为了方便区分吧。建议以上代码放到`tsconfig.app.json`
:::
## 添加路由
先安装路由插件 `npm install vue-router`,再在项目根路径下创建一个路由文件
```js
// src/router/index.ts
import { createRouter, createWebHistory } from "vue-router";
export default createRouter({
history: createWebHistory(),
routes: [
{
path: "/", // 使用动态导入来实现懒加载
component: () => import("@/pages/home.vue"),
},
],
});
```
在程序入口文件中引入
```js
// src/main.ts
...
import router from '@/router'
...
app.use(router);
...
```
最后,在根组件中引入路由组件标签
```html
```
## 封装 API
在支持 openApi 的情况下,优先推荐 API 自动生成工具 [Pont](https://github.com/alibaba/pont)(或者类似工具)。
在没有的情况下,可以自己封装,目录结构为
```
|--src
|--api
|--init.ts
|--index.ts
|--modules
|--student.ts
|--teacher.ts
```
其中,将 init.ts 引入项目的入口文件 main.ts 即可。
简单描述下个文件的内容
```js
// init.ts
import axios from "axios";
import store from "@/store";
import { Message } from "element-ui";
const getFileNameFromUrl = (url) => {
const match = url.match(/([^/]+)\.([^/]+)?$/); // 使用正则表达式匹配文件名(不包括扩展名)
if (match && match[1]) {
let fileName = match[1];
// 转换为小写,并用正则表达式替换每个分隔符后的字符为大写(除非它是字符串的第一个字符)
fileName = fileName
.toLowerCase() // 先转换为小写
.replace(/[-_\s]+(.)?/g, (match, p1) => (p1 ? p1.toUpperCase() : ""))
.replace(/^./, (str) => str.toLowerCase()); // 转换为小驼峰
return fileName;
}
return null; // 如果没有匹配到文件名,则返回null
};
// ---===全局默认axios配置===---
const whitePath = ["/login", "/sms-send"]; // 白名单
axios.defaults.baseURL = process.env.VUE_APP_PROXY;
axios.defaults.timeout = 10000;
axios.interceptors.request.use(
(config) => {
const { token } = store.state.app.loginInfo?.token || {};
if (token) {
config.headers["Authorization"] = token;
} else {
const requstUrl = config.url.replace("/api", "");
if (!whitePath.includes(requstUrl)) {
Message({
message: "登录失效,请重新登录!",
type: "error",
});
location.href = "#/login";
}
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
axios.interceptors.response.use(
(response) => {
const res = response.data;
if (res.code !== 200) {
if (res.code == 100004) {
Message({
message: res.msg || "登录失效,请重新登录!",
type: "error",
});
location.href = "#/login";
return Promise.reject(new Error(res.msg || "登录失效,请重新登录!"));
} else {
Message({
message: res.msg || "接口错误,请重试!",
type: "error",
});
return Promise.reject(new Error(res.msg || "接口错误,请重试!"));
}
}
return res.data;
},
(error) => {
Message({
message: error.message,
type: "error",
duration: 5 * 1000,
});
return Promise.reject(error);
}
);
// ---===将api注入全局,只需将api定义放在modules中即可===---
// 参数1:其目录路径相对于此配置文件的位置;参数2:是否搜索其子目录;参数3:匹配基础组件文件名的正则表达式
const requireComponent = require.context("./modules", false, /[\w-]+\.js$/);
// 使用 `requireComponent.keys()` 获取匹配到的文件名数组
const api = {};
requireComponent.keys().forEach(async (filePath) => {
const fileName = getFileNameFromUrl(filePath);
api[fileName] = requireComponent(filePath);
});
window.api = api;
```
```js
// index.js
export * as student from "./modules/student";
export * as teacher from "./modules/teacher";
```
```js
// module/teacher.js
import axios from "axios";
export const queryTeachers = (params) => {
return axios.get("/teacher/list", { params });
};
export const queryTeacher = (params) => {
return axios.get("/teacher", { params });
};
export const inserTeacher = (params) => {
return axios.post("/teacher", { params });
};
export const deleteTeacher = (params) => {
return axios.delete("/teacher", { params });
};
```
## 引入 Ui 库
虽然 antd 的 star 远比 element 多,但是在 vue 版本上,element 却比 antdv 多。
况且 antdv 是有社区维护的,而非蚂蚁团队官方出品。
综上选择 element ui(即便它被阿里收购了)!
现先下载`npm install element-plus`,后在入口文件中引入
```js
// main.ts
...
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
...
app.use(ElementPlus)
```
最后你就可以在组件内愉快的使用 element ui 啦
```html
按钮
```
## 引入状态管理
抛弃过时的 vuex,拥抱更好用的 pinia!
:::tip 简单易用
Pinia 的 Api 设计非常接近 Vuex5 的 提案,管理数据简单,提供数据和修改数据的逻辑即可,不像之前的 Vuex 需要记忆太多。
:::
先安装 `npm install pinia`,之后创建一个 pinia 实例 (根 store) 并将其传递给应用:
```js
// main.ts
...
import { createPinia } from 'pinia'
...
app.use(createPinia())
```
再创建一个自己的 store
```js
import { defineStore } from "pinia";
// 你可以任意命名 `defineStore()` 的返回值,但最好使用 store 的名字,同时以 `use` 开头且以 `Store` 结尾。
// (比如 `useUserStore`,`useCartStore`,`useProductStore`)
export const useAlertsStore = defineStore("app", {
// 第一个参数是你的应用中 Store 的唯一 ID。
state: () => {
// 状态
return {
count: 0,
};
},
actions: {
// 修改状态的action
increment() {
this.count++;
},
},
});
```
最后,在组件中使用即可
```html
{{ store.count }}
```

另外,它还允许[在组件之外使用](https://pinia.vuejs.org/zh/core-concepts/outside-component-usage.html),允许[使用三方插件扩展](https://pinia.vuejs.org/zh/core-concepts/plugins.html)自身能力 如[持久化存储插件](https://prazdevs.github.io/pinia-plugin-persistedstate/zh/)等等。
## 定义组件名称
`
```
打开[vue devtool](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd),也能看到效果

## 自动导入
在开发 vue 项目的过程中,像 ref、react 等常用的 api 总是频繁导入,有点麻烦。
发现 github 上有一个不错的开源工具 unplugin-auto-import,可以借助它 让所需自动导入。
安装 `npm install -D unplugin-auto-import` 完成后,在 vite 配置文件中添加即可。
```js
// vite.config.js
import AutoImport from 'unplugin-auto-import/vite'
export default defineConfig({
plugins: [
...
AutoImport({ imports: ['vue', 'vue-router'] }),
],
...
})
```
之后,你变可以这么用了
```html
{{ str }}
```
**ts 支持**
但是如果您的项目是 ts,那么还需要配置一下,否则 ts 可能会提示没有显示导入的错误。
我们可以在 ts 的配置文件中导入相关的类型声明文件 `auto-imports.d.ts` (此文件在配置好插件后会启动项目 会自动生成)。
确保正确配置完成后 重启下 VScode。

```js
// tsconfig.app.json
"include": [
...
"auto-import.d.ts"
]
```
**eslisnt 支持**
如果你还用了 eslint,也需要载额外为期配置一下,否则它也会提示了没有显示导入的错误。

在 vite 使用 auto import 插件的时候,开启 eslint 的支持 `AutoImport({eslintrc: { enabled: true }})`,之后你再次运行项目,会发现项目中自动生成了 `.eslintrc-auto-import.json`!
我们需要将其引入到 eslint 配置文件中,因为我们使用的是新版 v9 的 eslint
```js
// eslint.config.js
...
import require from './node-helper/require.js'
import absoluteFilePath from "./node-helper/absolute-file-path.js";
// 获得 auto-imports 生成的eslint配置 并导入此
const autoImportsPath = absoluteFilePath('.eslintrc-auto-import.json')
const autoImports = require(autoImportsPath)
export default [
...
{languageOptions: autoImports },
];
```
这样你再执行 `npx eslint` 就可以顺利的通过检测啦!
`require.js` 和 `absolute-file-path.js` 是我提取出去的两个帮助性文件,这里我贴出来
```js
// require.js
import { createRequire } from "module";
const require = createRequire(import.meta.url);
export default require;
```
```js
// absolute-file-path.js
import path from "path";
import process from "node:process";
export default (...arg) => {
const rootPath = process.cwd();
const filePath = path.join(rootPath, ...arg);
return filePath;
};
```
## 集成 Prettier
Prettier 前端代码格式化工具。
确保代码的缩进、括号、引号、换行等样式一致。
我们安装 `npm install -D prettier` ,然后在根目录创建配置文件。
```js
// prettier.config.js
export default {
tabWidth: 2, // 缩进2个空格
useTabs: false, // 缩进单位是否使用tab替代空格
semi: true, // 句尾添加分号
singleQuote: true, // 使用单引号代替双引号
};
```
至此,项目就已经支持 Prettier 了,
可以使用检查命令 `npx prettier src` 或者 自动修复错误命令 `npx prettier --write src`。
若想要编辑器也支持项目中 prettier 配置,可到商店安装 prettier 插件,然后右键格式化代码的时候就可以看到 `使用prettier格式化代码`的选项。
## 集成 Eslint
ESLint 是一个代码检查工具(默认只检查 js,不支持 ts 或 css),用来检查你的代码是否符合指定的规范。
安装 eslint `npm install -D eslint` ,选择合适的配置项, 即可自动生成 `eslint.config.js`
```shell
npm create @eslint/config
```
至此,项目就已经支持 eslint 了,
可以使用检查命令 `npx eslint` 或者 自动修复错误命令 `npx eslint --fix`。
若想要编辑器也支持项目中 eslint 配置,可到商店安装 eslint 插件,后重启编辑器就可以看到效果

:::tip 注意
上诉演示提示为项目中 eslint 禁用 var 关键字,需要在 eslint 配置文件中增加此规则
```js
// eslint.config.js
...
export default [
...
{rules:{'no-var': 'error'}}
];
```
:::
最后,贴出一个常用的自定义的规则
```js
// eslint.config.js
export default [
// ...
{
rules: {
"no-var": "error", // 禁止使用var
// ---vue-eslint参考:https://eslint.vuejs.org/rules---
"vue/multi-word-component-names": "off",
"vue/attribute-hyphenation": ["error", "always"], // vue模板属性中划线
"vue/component-name-in-template-casing": ["error", "kebab-case"], // vue模板使用组件名规范
"vue/html-self-closing": [
"error",
{
html: {
void: "always",
normal: "always",
component: "always",
},
},
], // 强制自闭合标签
// ---tslint 规则集参考:https://typescript-eslint.io/rules---
"@typescript-eslint/no-explicit-any": "warn", // 允许使用any类型,但是警告(默认即使警告,可以不用声明)
"@typescript-eslint/no-var-requires": "warn", // 允许使用require,但是警告(默认不允许)
"@typescript-eslint/no-empty-function": "off", // 允许空方法,因为可能是做单例限制构造or被注解修饰的空方法(默认为error)
"@typescript-eslint/ban-ts-comment": "off", // 允许@ts- 指令的使用,如@ts-nocheck(默认不允许使用)
"@typescript-eslint/no-non-null-assertion": "off", // 允许 非空断言操作符(默认为不允许)
// '@typescript-eslint/explicit-module-boundary-types': 'off' // 函数必须定义参数类型和返回类型,默认即是关闭校验
"no-unused-vars": "warn",
"@typescript-eslint/no-unused-vars": "warn",
"@typescript-eslint/no-unused-expressions": "off",
},
},
];
```
## Eslint 与 Prettier
一般情况下 我们不会单独运行 prettier,而是将 prettier 集成到 eslint 中,作为一项 rule 进行提示与修复。
我们通过两个包来做到这个能力
- eslint-config-prettier:一个 ESLint 配置规则的包,它将禁用与 Prettier 冲突的 ESLint 规则。
- eslint-plugin-prettier:一个 ESLint 插件,它将 Prettier 作为规则在 ESLint 内部运行。
```
npm install -D eslint-config-prettier eslint-plugin-prettier
```
安装完成后,在 eslint 配置文件中,使用 eslint-plugin-prettier 插件即可(此插件会自动调用 eslint-config-prettier)
```js
// eslint.config.js
...
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
export default [
...
eslintPluginPrettierRecommended,
];
```
最后就可以看到效果了

:::tip 注意
上诉提示为项目中 prettier 要求使用单引号,需要在 prettier 配置文件中增加此规则
```js
// prettier.config.js
export default {
...
singleQuote: true, // 使用单引号代替双引号
};
```
:::
**最后**
在你的项目 pakage.json 中新增一个脚本
```
{
...
"scripts": {
...
"eslint": "npx eslint src --fix"
}
}
```
这样,以后你每次提交代码前,执行一下这个命令,你犯的错误 它都帮你解决了。
## 集成 Husky
这是一个让开发人员头痛的插件,它会通过一些手段禁止不符合要求的 commit 被提交。
遇到被限制了提交了,先不要骂娘,出于团队规范的考虑,你应该先根据提示解决自己的问题!
Husky 在提交或推送时,自动检查提交信息、检查代码 和 运行测试。
首先安装它 `npm install -D husky` ,之后使用初始化命令 `npx husky init`,这将会 生成 .husky/pre-commit 脚本,并更新 package.json 中的 prepare 脚本。
最后,你可以在.husky/pre-commit 中编写 shell 脚本(如果你更喜欢 js 脚本,[可以看这里](https://typicode.github.io/husky/zh/how-to.html#%E9%9D%9E-shell-%E8%84%9A%E6%9C%AC%E9%92%A9%E5%AD%90)),脚本将会在你执行 git commit 命令的时候被触发。
比如你可以如下这么写,浙江会在你提交之前检查代码,如果报错 则不予通过
```js
// .husky/pre-commit
npx eslint src
```
## 锁定 Node 版本
为了确保团队协作项目的稳定性和一致性,我们需要采取一些措施来保证项目中的 Node 版本 和 包管理工具一致。
在项目的 package.json 文件中,可以使用 engines 字段来指定所需的 Node 版本 和 包管理器。在该字段中,我们可以定义一个范围或者具体的版本号来限制 Node 的版本。
```js
// package.json
{
...
"engines": {
"yarn": ">= 1.0.0",
"node": "=18.0.0"
},
}
```
这样当项目成员运行 npm install 时,npm 会自动检查 Node 版本是否满足要求,并给出警告信息。
npm 下 engines 只是建议,默认不开启严格版本校验,只会给出提示,需要手动开启严格模式。 而 yarn 则默认开启严格模式。
```yml
# .npmrc
engine-strict = true
```
不过实测,并不好用(如 无论怎么配置 yarn 都不支持 node 版本的检测)。
**优选方案**
除此这个方式,我们还可以利用 preinstall 在安装依赖之前做一些限制,比如使用插件 [use-yarn](https://github.com/AndersDJohnson/use-yarn)、[please-use-yarn](https://github.com/justjavac/please-use-yarn)、[only-allow](https://github.com/pnpm/only-allow)。
它们的使用方式都一样 比如,在 package.json 文件的 scripts 中添加 preinstall:
```js
// package.json
{
"scripts": {
"preinstall": "npx please-use-yarn"
}
}
```
其原理简单,`npx please-use-yarn` 会执行 please-use-yarn/bin 的脚本文件做检测,知道此原理就好办了。
**优化方案**
```js
// package.json
{
"scripts": {
"preinstall": "node ./node-helper/preinstall.js"
}
}
```
```js
// ./node-helper/preinstall.js
import chalk from "chalk";
import semver from "semver";
import process from "node:process";
const version = "22.0.0";
const pkgManager = "yarn";
const pkgManagerExecpath = process.env.npm_execpath || "";
const allowPkgManager = pkgManagerExecpath.indexOf(pkgManager) > -1;
if (!allowPkgManager) {
console.log(chalk.underline.bold.red("包管理器不符合要求"));
console.log(chalk.red("要求为:" + pkgManager));
process.exit(1);
}
if (!semver.satisfies(process.version, version)) {
console.log(chalk.underline.bold.red("Node版本不符合项目要求"));
console.log(chalk.red("要求版本:v" + version));
console.log(chalk.red("您的版本:" + process.version));
console.log(chalk.magentaBright("推荐使用n、nvm等管理node"));
process.exit(1);
}
```
**最终方案**
但是考虑到在 preinstall 阶段的时候,你可能无法使用 chalk、 semver 三方包,随意我们改为如下
```js
// version-compare.js
// 版本比较函数
export default (v1, v2, operator) => {
// 将版本号转换为数组,按.分割
v1 = v1.split(".");
v2 = v2.split(".");
const maxLen = Math.max(v1.length, v2.length);
// 补充短的版本号数组,使其长度等于最长的版本号
for (let i = 0; i < maxLen; i++) {
if (!v1[i]) {
v1[i] = "0";
}
if (!v2[i]) {
v2[i] = "0";
}
}
// 转换成数字数组进行比较
for (let i = 0; i < maxLen; i++) {
const num1 = parseInt(v1[i], 10);
const num2 = parseInt(v2[i], 10);
if (num1 > num2) {
return operator === ">" || operator === ">=" ? true : false;
} else if (num1 < num2) {
return operator === "<" || operator === "<=" ? true : false;
}
}
return true; // 版本号相等
};
// 使用示例
// console.log(versionCompare('1.2.3', '1.2.4', '<')); // true
// console.log(versionCompare('1.2.3', '1.2.4', '>')); // false
// console.log(versionCompare('1.2.3', '1.2.3', '=')); // true
// console.log(versionCompare('1.2.3', '1.2.4', '>=')); // false
// console.log(versionCompare('1.2.4', '1.2.3', '<=')); // true
```
```js
// preinstall.js
import process from "node:process";
import versionCompare from "./version-compare.js";
const currentNodeVersion = process.version.replace("v", "");
const version = "20.16.0";
const pkgManager = "yarn";
const pkgManagerExecpath = process.env.npm_execpath || "";
console.log(pkgManagerExecpath);
const allowPkgManager = pkgManagerExecpath.indexOf(pkgManager) > -1;
if (!allowPkgManager) {
console.error(`\x1B[1;31m${"*".repeat(40)}\x1B[0;0m`);
console.error(
`\x1B[1;31m* 包管理器不符合要求,要求为 ${pkgManager}\x1B[0;0m`
);
console.error(`\x1B[1;31m${"*".repeat(40)}\x1B[0;0m`);
console.error(``);
process.exit(1);
}
const allowNodeVersion = versionCompare(currentNodeVersion, version, ">=");
if (!allowNodeVersion) {
console.error(`\x1B[1;31m${"*".repeat(50)}\x1B[0;0m`);
console.error(
`\x1B[1;31m* Node不符合项目要求v${version}, 您的为 ${process.version}\x1B[0;0m`
);
console.error(`\x1B[1;31m${"*".repeat(50)}\x1B[0;0m`);
console.error(``);
process.exit(1);
}
```
## ts不识别vue文件
增加如下代码即可
```js
// vite-env.d.ts
declare module '*.vue' {
import type { DefineComponent } from 'vue';
const component: DefineComponent<_, _, any>;
export default component;
}
```
## 表单插件
一般中后台都会大量用到表单,如果自己纯手撸,效率很低。
这里推荐将 vue 开源表单生成插件(目前start最多,且较为完善) FormCreate

## 表格插件
中后台,表格多用于展示数据,
比如用户列表、角色权限列表等等,也是必不可少,
这里推荐 [vxetable](https://vxetable.cn),推荐原因:star多、简单易用!
## mock数据
MSW 全称 Mock Service Worker 是一个用于浏览器或 Node.js 的 API 模拟库。借助它,您可以拦截传出的请求、观察它们,并使用模拟响应来响应它们。
MSW 的与众不同之处在于,它极力主张使用独立的 API 模拟层,为您的网络行为创建单一事实来源,并将其集成到您使用的任何工具中。
这带来了更具弹性的设置,并与其他库功能相结合,创造了真正无缝的 API 模拟体验。
[MSW](https://mswjs.io)是目前相当优秀的方案,但是这需要一定的难度,这里有简单和快熟开始的的[使用文档](https://doc.dingshaohua.com/tool/msw/start)
## 包管理器
推荐项目包管理工具位 pnpm,即便不是 您的选择也应该按照优先级为 pnpm > yarn > npm
**npm、yarn、pnpm 比较**
都是 js 包管理工具,用于管理和下载 js 依赖项。它们的主要区别在于以下几个方面:
⚡️ 性能:yarn 和 pnpm 相对于 npm 来说更快。yarn 使用并行下载和缓存机制来提高性能,而 pnpm 则使用硬链接和符号链接来减少磁盘空间的使用。
⬇️ 安装依赖项的方式:npm 和 yarn 都会将依赖项安装在本地 node_modules 中,而 pnpm 会将依赖项安装在全局缓存中,并使用符号链接将其连接到项目中。