Vue 后台管理系统
一、系统创建
1.1、环境检测
$ node -v
v18.10.0
$ npm -v
9.1.2
## 若没有该命令 需要用 npm install -g pnpm 安装
$ pnpm -v
7.13.6
$ vue -V
@vue/cli 4.5.19
1.2、创建项目
## 利用vite创建项目
$ npm create vite his.vue
Need to install the following packages:
[email protected]
Ok to proceed? (y) y
√ Select a framework: » Vue
√ Select a variant: » TypeScript
Scaffolding project in D:\vscode\his.vue...
Done. Now run:
cd his.vue
npm install
npm run dev
## 进入目录
$ cd his.vue
## 安装依赖
$ pnpm install
## 运行
$ pnpm dev
## 用vs code 打开
$ code .
1.3、初始化一些配置信息
1.3.1、构建成功后自动打开浏览器
-
vite.config.ts
export default defineConfig({ plugins: [vue()], // 自动打开 server: { open: true, }, });
1.3.2、配置端口
-
package.json
"scripts": { "dev": "vite --port 9527", "build": "vue-tsc && vite build", "preview": "vite preview" },
1.3.3、配置@
-
path 模块是 node.js 的内置模块,而 node.js 默认不支持 ts 文件的,所以需要安装 @type/node 依赖包
$ pnpm install @types/node
-
vite.config.ts
import { defineConfig } from "vite"; import vue from "@vitejs/plugin-vue"; import { resolve } from "path"; // https://vitejs.dev/config/ export default defineConfig({ plugins: [vue()], // 自动打开 server: { open: true, }, resolve: { alias: { "@": resolve(__dirname, "./src"), }, }, });
-
修改 ts.config.json 文件
{ "compilerOptions": { "target": "ESNext", "useDefineForClassFields": true, "module": "ESNext", "moduleResolution": "Node", "strict": true, "jsx": "preserve", "resolveJsonModule": true, "isolatedModules": true, "esModuleInterop": true, "lib": ["ESNext", "DOM"], "skipLibCheck": true, "noEmit": true, "baseUrl": "./", "paths": { "@/": ["src/*"] } }, "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], "references": [{ "path": "./tsconfig.node.json" }] }
-
使用
<script setup lang="ts"> import HelloWorld from '@/components/HelloWorld.vue' </script> <template> <HelloWorld msg="Vite + Vue" /> </template>
1.3.4、修改 css
- .\src\style.css 重命名为.\src\style.scss
- main.ts 修改引入的名称
1.4、安装 sass
$ pnpm install sass
Progress: resolved 0, reused 1, downloaded 0, added 0
Progress: resolved 6, reused 6, downloaded 0, added 0
Progress: resolved 28, reused 27, downloaded 0, added 0
Progress: resolved 54, reused 42, downloaded 0, added 0
Progress: resolved 75, reused 51, downloaded 1, added 0
Progress: resolved 80, reused 51, downloaded 4, added 0
Progress: resolved 83, reused 51, downloaded 7, added 0
Packages: +17 -1
+++++++++++++++++-
Progress: resolved 89, reused 51, downloaded 15, added 15
dependencies:
+ sass 1.56.1
Done in 8.2s
Progress: resolved 89, reused 51, downloaded 16, added 17, done
1.5、Element Plus
- 安装
$ pnpm install element-plus
Progress: resolved 0, reused 1, downloaded 0, added 0
Progress: resolved 7, reused 7, downloaded 0, added 0
Progress: resolved 8, reused 7, downloaded 0, added 0
Progress: resolved 78, reused 62, downloaded 2, added 0
Progress: resolved 104, reused 67, downloaded 3, added 0
Progress: resolved 107, reused 67, downloaded 7, added 0
Progress: resolved 109, reused 67, downloaded 15, added 0
Packages: +21
+++++++++++++++++++++
Progress: resolved 110, reused 67, downloaded 16, added 0
Progress: resolved 110, reused 67, downloaded 18, added 17
Progress: resolved 110, reused 67, downloaded 20, added 19
Progress: resolved 110, reused 67, downloaded 20, added 20
Progress: resolved 110, reused 67, downloaded 21, added 20
Progress: resolved 110, reused 67, downloaded 21, added 21
.../node_modules/vue-demi postinstall$ node ./scripts/postinstall.js
.../node_modules/vue-demi postinstall: Done
dependencies:
+ element-plus 2.2.23
Done in 18.4s
Progress: resolved 110, reused 67, downloaded 21, added 21, done
-
自动导入(推荐)
$ pnpm install -D unplugin-vue-components unplugin-auto-import Progress: resolved 0, reused 1, downloaded 0, added 0 Progress: resolved 8, reused 8, downloaded 0, added 0 Progress: resolved 70, reused 68, downloaded 1, added 0 Progress: resolved 82, reused 74, downloaded 4, added 0 Progress: resolved 128, reused 88, downloaded 16, added 0 Progress: resolved 138, reused 88, downloaded 28, added 0 Packages: +32 ++++++++++++++++++++++++++++++++ Progress: resolved 142, reused 88, downloaded 31, added 31 devDependencies: + unplugin-auto-import 0.11.5 + unplugin-vue-components 0.22.11 Done in 7.4s Progress: resolved 142, reused 88, downloaded 32, added 32, done
-
修改 vite.config.ts
import { defineConfig } from "vite"; import vue from "@vitejs/plugin-vue"; import { resolve } from "path"; import AutoImport from "unplugin-auto-import/vite"; import Components from "unplugin-vue-components/vite"; import { ElementPlusResolver } from "unplugin-vue-components/resolvers"; // https://vitejs.dev/config/ export default defineConfig({ plugins: [ vue(), // Element Plus自动导入 AutoImport({ resolvers: [ElementPlusResolver()], }), Components({ resolvers: [ElementPlusResolver()], }), ], // 自动打开 server: { open: true, }, resolve: { alias: { "@": resolve(__dirname, "./src"), }, }, });
-
main.ts 引入 css
import "element-plus/dist/index.css";
1.6、安装 route
-
安装
$ pnpm install vue-router@next Progress: resolved 0, reused 1, downloaded 0, added 0 Progress: resolved 10, reused 10, downloaded 0, added 0 Progress: resolved 11, reused 10, downloaded 0, added 0 Progress: resolved 134, reused 109, downloaded 0, added 0 Packages: +2 ++ Progress: resolved 144, reused 120, downloaded 2, added 2, done dependencies: + vue-router 4.0.13 (4.1.6 is available) Done in 6.3s
-
src 目录创建 views 文件夹,新建文件 Login.vue
-
src 目录下新建文件夹 router,文件夹新建 路由文件 index.ts 写入页面和路由映射关系
import { createRouter, createWebHistory } from "vue-router"; import Login from "@/views/Login.vue"; const router = createRouter({ history: createWebHistory(), routes: [{ path: "/login", component: Login }], }); export default router;
-
在 main.ts 中引入路由
import { createApp } from "vue"; import "./style.scss"; import App from "./App.vue"; import "element-plus/dist/index.css"; import router from "./router"; createApp(App).use(router).mount("#app");
-
在 App.vue 里面加上路由标签
<template> <router-view></router-view> </template>
1.7、安装 pinia
-
pinia 是一款新的 vue3 的状态管理库,完整的 typescript 支持。
-
起名规则:userXxxStore ,Xxx 为 id 值
-
下载
$ pnpm i pinia@next Packages: +1 + dependencies: + pinia 2.0.0-rc.10 (2.0.26 is available) Progress: resolved 145, reused 122, downloaded 1, added 1, done Done in 4.9s
-
设置为全局对象,在 main.js 中引用
import { createPinia } from "pinia"; // 创建pinia实例 const pinia = createPinia(); createApp(App).use(router).use(pinia).mount("#app");
-
新建文件夹 store,新建文件 index.ts
// store/index.ts import { defineStore } from "pinia"; // 使用defineStore定义store,第一个参数必须是全局唯一的id,可以使用Symbol // id:必须,在所有store中唯一 export const useGlobalStore = defineStore("global", { // 定义状态 返回的对象函数 state: () => ({ count: 10, title: "医院信息系统", }), // 计算属性 computed Getter 是计算属性,也可叫只读属性,因此不可能将任何参数传递给它们 // 都带一个可选参数 state ,建议都带上,不建议用this getters: { getUserById: (state) => { return (userId) => state.users.find((user) => user.id === userId); }, // 返回值会类型推导 count10(state) { return state.count + 10; }, // 若getters使用了this必须手动指定返回值类型,否则类型推导不出来 count11(): number { return this.count + 10; }, }, // 相当于组件中的方法,不建议用箭头函数,因为无法用this actions: { setCount(n: number) { this.count = n; }, // 也可以用批量更新 // this.$patch({...}) // this.$patch(state=>{...}) }, });
import { useOtherStore } from "./other-store"; // 访问其他store的getters export const useMainStore = defineStore("main", { state: () => ({ // ... }), getters: { otherGetter(state) { const otherStore = useOtherStore(); return state.localData + otherStore.data; }, }, });
-
使用
<script setup lang="ts"> import { reactive, ref, computed } from 'vue' // 在setup中使用GlobalStore创建store实例 import { useGlobalStore } from '@/stores'; // 获取父组件传值 defineProps<{ msg: string }>() let store = useGlobalStore(); // 如果直接取state的值必须使用computed才能实现数据的响应式 // 如果直接取 store.state.a 则不会监听到数据的变化,或者使用getter,就可以不使用computed (这边和vuex是一样的) let count = computed(() => store.count); let title = store.$state.title; const clickAdd = () => { store.setCount(store.count + 1); console.log(store.count); }; </script> <template> {{ msg }}<br /> {{ title }} <div>Login:{{ count }}</div> <el-button type="primary" @click="clickAdd">{{ count }}++</el-button> </template> <style lang="scss" scoped> </style>
-
解构
import { storeToRefs } from "pinia"; import { GlobalStore } from "@/stores"; const store = GlobalStore(); // 响应式的代理 解构成响应式 const { UserName, NickName, Token } = storeToRefs(store); const login = () => { // 一次性修改多个数据,建议使用$patch // store.SettingNickName(user.Name) // store.SettingUserName(user.UserName) // store.SettingToken(token) // 一次性修改多个数据,建议使用$patch,内部做了性能优化 // store.$patch({ // NickName: user.Name, // UserName: user.UserName, // Token: token, // Arr: [...store.Arr, 4] // }) // 这种是推荐写法,复杂情况用这种,简单数据修改用上边的写法 store.$patch((state) => { (state.NickName = user.Name), (state.UserName = user.UserName), (state.Token = token), state.Arr.push(6); }); };
-
一个 ts 中可以写多个 store,但是 id 必须唯一
export const useTestStore = defineStore({ id: "test", /** * 属性 * @returns */ state: () => ({ count: 0, }), /** * 计算属性 */ getters: {}, /** * 方法 */ actions: {}, });
-
另一种写法
export const useTestStore = defineStore("test", { /** * 属性 * @returns */ state: () => ({ count: 0, }), /** * 计算属性 */ getters: {}, /** * 方法 */ actions: {}, });
-
对比:
- vuex 两个代码的对比我们可以看出使用 pinia 更加的简洁,轻便。
- pinia 取消了原有的 mutations,合并成了 actions,且我们在取值的时候可以直接点到那个值,而不需要在.state,方法也是如此。
1.8、echarts
-
安装
$ pnpm install echarts Progress: resolved 0, reused 1, downloaded 0, added 0 Progress: resolved 11, reused 11, downloaded 0, added 0 Progress: resolved 117, reused 114, downloaded 0, added 0 Packages: +3 +++ Progress: resolved 147, reused 122, downloaded 2, added 1 Progress: resolved 147, reused 122, downloaded 2, added 2 dependencies: + echarts 5.4.0 Done in 5.5s Progress: resolved 147, reused 122, downloaded 3, added 3, done
1.9、axios
-
安装
$ pnpm install axios Progress: resolved 0, reused 1, downloaded 0, added 0 Progress: resolved 13, reused 13, downloaded 0, added 0 Progress: resolved 14, reused 13, downloaded 0, added 0 Progress: resolved 116, reused 113, downloaded 1, added 0 Progress: resolved 156, reused 126, downloaded 4, added 0 Packages: +9 +++++++++ Progress: resolved 157, reused 126, downloaded 8, added 8 dependencies: + axios 1.2.0 Done in 6.2s Progress: resolved 157, reused 126, downloaded 9, added 9, done
-
src 目录下新建 api 文件夹,新建 index.ts 文件
import axios from "axios"; //需要拦截器的地方使用instance对象, 有自定义返回逻辑的地方沿用axios,在组件内部处理返回结果即可 import instance from "@/api/filter"; const http = "/api"; //获取token,,因为instance开启了withCredentials 所以这里要用axios,因为instance有连接器功效,必须携带token export const getToken = (name: string, password: string) => { return axios.get( http + "/Login/GetToken?name=" + name + "&password=" + password ); }; //获取列表 export const getMenuDataNew = async (parms: {}) => { instance.defaults.headers.common["Authorization"] = "Bearer " + localStorage["token"]; return instance.post(http + "/Menu/GetMenus", parms); };
-
在需要使用的组件里导入 http 中的方法即可
import { getToken } from '../../http/index' //请求后端数据,获取token,并将token放入localStorage const token = await getToken(form.userName, form.passWord) as any as string const user: UserInfo = JSON.parse(new Tool().FormatToken(token)) localStorage["token"] = token localStorage["nickname"] = user.NickName store.commit("SettingNickName",user.NickName) store.commit("SettingToken",token) router.push({ path: '/desktop' });
-
拦截器:
- 对 api 的返回结果解析,返回统一的格式。
- 对于错误信息,在拦截器中弹窗提示,业务层页面只需关注页面,无需过度关注交互
- 新建 filter.ts 文件
//导入axios import axios from "axios"; import { ElMessage } from "element-plus"; // 处理 类型“AxiosResponse<any, any>”上不存在属性“errorinfo”。ts(2339) 脑壳疼!关键一步。 declare module "axios" { interface AxiosResponse<T = any> { errorinfo: null; // 这里追加你的参数 } export function create(config?: AxiosRequestConfig): AxiosInstance; } //创建一个axios实例 const instance = axios.create({ headers: { "content-type": "application/json", }, // true:在跨域请求时,会携带用户凭证 // false(默认):在跨域请求时,不会携带用户凭证;返回的 response 里也会忽略 cookie // withCredentials: true, timeout: 5000, //5秒 }); //http 拦截器 instance.interceptors.response.use( (response) => { //拦截请求,统一相应 if (response.data.isSuccess) { return response.data.result; } else { ElMessage.error(response.data.msg); return response.data.result; } }, //error也可以处理 (error) => { if (error.response) { switch (error.response.status) { case 401: ElMessage.warning("资源没有访问权限!"); break; case 404: ElMessage.warning("接口不存在,请检查接口地址是否正确!"); break; case 500: ElMessage.warning("内部服务器错误,请联系系统管理员!"); break; default: return Promise.reject(error.response.data); // 返回接口返回的错误信息 } } else { ElMessage.error("遇到跨域错误,请设置代理或者修改后端允许跨域访问!"); } } ); export default instance;
1.10、icons-vue
-
网址:https://element-plus.gitee.io/zh-CN/component/icon.html#基础用法
-
安装
$ pnpm install @element-plus/icons-vue Progress: resolved 0, reused 1, downloaded 0, added 0 Progress: resolved 14, reused 14, downloaded 0, added 0 Progress: resolved 15, reused 14, downloaded 0, added 0 Progress: resolved 122, reused 122, downloaded 0, added 0 Progress: resolved 127, reused 127, downloaded 0, added 0 Progress: resolved 128, reused 127, downloaded 0, added 0 Already up to date dependencies: + @element-plus/icons-vue 2.0.10 Done in 9.2s Progress: resolved 157, reused 135, downloaded 0, added 0, done
-
使用
<script setup lang="ts"> import { CoffeeCup} from '@element-plus/icons-vue' </script> <template> <div class="left"> <coffee-cup /> </div> </template> <style lang="scss" scoped> .left { width: 32px; height: 32px; } </style>
1.11、前端解决跨域问题
-
和后端解决是两种方案,而不是必须前后端都解决跨域问题;
-
代码 vite.config.ts
// vite.config.ts ,和plugins平级添加 server:{ port:3000, open:true, proxy:{ '/api':{ target:'http://localhost:5294/api', changeOrigin:true, rewrite:(path) => path.replace(/^\/api/,'') } }, }
1.12、 json-viewer(弃用)
-
安装
# 需要依赖clipboard,先安装clipboard $ pnpm install clipboard Progress: resolved 0, reused 1, downloaded 0, added 0 Progress: resolved 40, reused 39, downloaded 0, added 0 Packages: +5 +++++ Progress: resolved 161, reused 134, downloaded 4, added 0 dependencies: + clipboard 2.0.11 Done in 2.8s Progress: resolved 161, reused 134, downloaded 5, added 5, done Administrator@wanghx MINGW64 /d/vsdemo/vs2022/HisApi/vue-vite-ts-admin # 再安装vue3-json-viewer $ pnpm i vue3-json-viewer Progress: resolved 0, reused 1, downloaded 0, added 0 Progress: resolved 30, reused 29, downloaded 0, added 0 Packages: +1 + Progress: resolved 162, reused 139, downloaded 1, added 1, done dependencies: + vue3-json-viewer 2.2.2
-
main.ts 引入
import JsonViewer from "vue3-json-viewer"; import "vue3-json-viewer/dist/index.css"; createApp(App).use(router).use(pinia).use(JsonViewer).mount("#app");
-
新建文件.\src\declaration.d.ts,防止报错
declare module "vue3-json-viewer" { const vis: any; export default vis; }
-
新建组件
/** * @description JsonView组件,格式化JSON用 * @author wanghx * @date 2022-11-26 12:40:39 */ <template> <div class="container"> <json-viewer :value="json" :copyable="{ copyText: '复制代码', copiedText: '复制成功' }" :expand-depth=5 boxed expanded="true" sort :theme="theme" /> </div> </template> <script setup lang='ts'> import { reactive, ref } from 'vue' let theme = ref("dark"); // light,dark // 获取父组件传值 defineProps<{ json: string }>() </script> <style lang='scss' scoped> .container { margin: auto; padding-top: 5px; width: 70%; } </style>
-
使用
<template> <JsonComp :json="jsonData" /> <HelloWorld msg="Login"></HelloWorld> </template> <script lang="ts" setup> import JsonComp from "@/components/JsonComp.vue"; const form = { jsonData: '[{"name":"黑子","sex":"男","Age":25,"abc":null,"hobby":["篮球","跑步","看电影","王者荣耀"],"normal":true},{"name":"张三","sex":"男","Age":25,"hobby":["上天","入地"],"normal":false},{"name":"黑子","sex":"男","Age":25,"abc":null,"hobby":["篮球","跑步","看电影","王者荣耀"],"normal":true},{"name":"张三","sex":"男","Age":25,"hobby":["上天","入地"],"normal":false}]', }; const jsonData = JSON.parse(form.jsonData); </script> <style lang="scss" scoped></style>
1.13、Json:vue3-ace-editor
-
下载
$ pnpm i vue3-ace-editor dependencies: + vue3-ace-editor 2.2.2 $ pnpm install file-loader Packages: +3 +++ dependencies: + file-loader 6.2.0 $ pnpm install ace-builds dependencies: + ace-builds 1.13.1
-
代码
/** * @description vue3-ace-editor * @author wanghx * @date 2022-11-29 17:05:41 * pnpm i vue3-ace-editor * pnpm install file-loader * pnpm install ace-builds */ <template> <div class="common-layout"> <el-container> <el-header> <el-select v-model="aceConfig.theme" class="m-2" placeholder="Select" size="large" > <el-option v-for="item in aceConfig.arr" :key="item" :label="item" :value="item" /> </el-select> <el-button style="width: 120px;" type="success" round size="large" class="jsonFormat" @click="jsonFormat" > 格式化Json </el-button> <el-button style="width: 120px;" type="primary" round size="large" @click="jsonNoFormat" >压缩 </el-button> </el-header> <el-main> <v-ace-editor v-model:value="dataForm.textareashow" @init="jsonFormat" lang="json" :theme="aceConfig.theme" :options="aceConfig.options" style="height:300px" :readonly="aceConfig.readOnly" class="ace-editor" /> </el-main> </el-container> </div> </template> <script setup lang="ts"> import { reactive, ref } from "vue"; import { VAceEditor } from "vue3-ace-editor"; import "ace-builds/webpack-resolver"; import "ace-builds/src-noconflict/mode-json"; import "ace-builds/src-noconflict/theme-ambiance"; import "ace-builds/src-noconflict/theme-chaos"; import "ace-builds/src-noconflict/theme-chrome"; import "ace-builds/src-noconflict/theme-cloud9_day"; import "ace-builds/src-noconflict/theme-cloud9_night"; import "ace-builds/src-noconflict/theme-cloud9_night_low_color"; import "ace-builds/src-noconflict/theme-clouds"; import "ace-builds/src-noconflict/theme-clouds_midnight"; import "ace-builds/src-noconflict/theme-cobalt"; import "ace-builds/src-noconflict/theme-crimson_editor"; import "ace-builds/src-noconflict/theme-dawn"; import "ace-builds/src-noconflict/theme-dracula"; import "ace-builds/src-noconflict/theme-dreamweaver"; import "ace-builds/src-noconflict/theme-eclipse"; import "ace-builds/src-noconflict/theme-github"; import "ace-builds/src-noconflict/theme-gob"; import "ace-builds/src-noconflict/theme-gruvbox"; import "ace-builds/src-noconflict/theme-gruvbox_dark_hard"; import "ace-builds/src-noconflict/theme-gruvbox_light_hard"; import "ace-builds/src-noconflict/theme-idle_fingers"; import "ace-builds/src-noconflict/theme-iplastic"; import "ace-builds/src-noconflict/theme-katzenmilch"; import "ace-builds/src-noconflict/theme-kr_theme"; import "ace-builds/src-noconflict/theme-kuroir"; import "ace-builds/src-noconflict/theme-merbivore"; import "ace-builds/src-noconflict/theme-merbivore_soft"; import "ace-builds/src-noconflict/theme-mono_industrial"; import "ace-builds/src-noconflict/theme-monokai"; import "ace-builds/src-noconflict/theme-one_dark"; import "ace-builds/src-noconflict/theme-pastel_on_dark"; import "ace-builds/src-noconflict/theme-solarized_dark"; import "ace-builds/src-noconflict/theme-solarized_light"; import "ace-builds/src-noconflict/theme-sqlserver"; import "ace-builds/src-noconflict/theme-terminal"; import "ace-builds/src-noconflict/theme-textmate"; import "ace-builds/src-noconflict/theme-tomorrow"; import "ace-builds/src-noconflict/theme-tomorrow_night"; import "ace-builds/src-noconflict/theme-tomorrow_night_blue"; import "ace-builds/src-noconflict/theme-tomorrow_night_bright"; import "ace-builds/src-noconflict/theme-tomorrow_night_eighties"; import "ace-builds/src-noconflict/theme-twilight"; import "ace-builds/src-noconflict/theme-vibrant_ink"; import "ace-builds/src-noconflict/theme-xcode"; import "ace-builds/src-noconflict/ext-language_tools"; //ace编辑器配置 const aceConfig = reactive({ lang: "json", // 解析json theme: "vibrant_ink", // 主题 arr: [ /*所有主题*/ "ambiance", "chaos", "chrome", "cloud9_day", "cloud9_night", "cloud9_night_low_color", "clouds", "clouds_midnight", "cobalt", "crimson_editor", "dawn", "dracula", "dreamweaver", "eclipse", "github", "gob", "gruvbox", "gruvbox_dark_hard", "gruvbox_light_hard", "idle_fingers", "iplastic", "katzenmilch", "kr_theme", "kuroir", "merbivore", "merbivore_soft", "mono_industrial", "monokai", "one_dark", "pastel_on_dark", "solarized_dark", "solarized_light", "sqlserver", "terminal", "textmate", "tomorrow", "tomorrow_night", "tomorrow_night_blue", "tomorrow_night_bright", "tomorrow_night_eighties", "twilight", "vibrant_ink", "xcode", ], readOnly: false, //是否只读 options: { enableBasicAutocompletion: true, enableSnippets: true, enableLiveAutocompletion: true, tabSize: 2, showPrintMargin: false, fontSize: 16, }, }); //form const dataForm = reactive({ textareashow: '{"infno": "2001","msgid": "H22240100110202211280916066003","mdtrtarea_admvs": "222401","insuplc_admdvs": "222401","recer_sys_code": "123","dev_no": "","dev_safe_info": "","cainfo": "","signtype": "","infver": "V1.0","opter_type": "1","opter": "YBYYJYL","opter_name": "周树人","inf_time": "2022-11-28 09:16:06","fixmedins_code": "H22240100110","fixmedins_name": "延边大学附属医院(延边医院)","sign_no": "","input": {"data": {"psn_no": "22000020001200084268","insutype": "310","fixmedins_code": "H22240100110","med_type": "21","begntime": "2022-11-28 09:16:06","endtime": "","dise_codg": "","dise_name": "","oprn_oprt_code": "","oprn_oprt_name": "","matn_type": "","birctrl_type": ""}}} ', }); const jsonError = (e: any) => { console.log(`JSON字符串错误:${e.message}`); }; // JSON格式化 const jsonFormat = () => { try { dataForm.textareashow = JSON.stringify( JSON.parse(dataForm.textareashow), null, 2 ); } catch (e) { jsonError(e); } }; // JSON压缩 const jsonNoFormat = () => { try { dataForm.textareashow = JSON.stringify( JSON.parse(dataForm.textareashow) ); } catch (e) { jsonError(e); } }; </script> <style lang="scss" scoped> .common-layout { width: 100%; height: 100%; } .jsonFormat { margin-left: 10px; margin-right: 10px; } </style>
1.14、git
- git全局设置
git config --global user.name "wanghx"
git config --global user.email "[email protected]"
-
已有仓库
cd his.api git remote add origin [email protected]:his7/his.vue.git git push -u origin "master"
-
新建仓库
$ git init $ git add . $ git status git commit -m "创建项目" git remote add origin [email protected]:his7/his.vue.git git push -u origin "master"
1.15、js-table2excel
-
安装
npm install js-table2excel
和pnpm install sortablejs
$ pnpm install js-table2excel dependencies: + js-table2excel 1.0.3 WARN Issues with peer dependencies found . └─┬ file-loader 6.2.0 └── ✕ missing peer webpack@"^4.0.0 || ^5.0.0" Peer dependencies that should be installed: webpack@"^4.0.0 || ^5.0.0" Done in 2.8s $ pnpm install sortablejs Progress: resolved 0, reused 1, downloaded 0, added 0 Progress: resolved 49, reused 49, downloaded 0, added 0 Packages: +1 + Progress: resolved 182, reused 160, downloaded 0, added 1, done dependencies: + sortablejs 1.15.0 WARN Issues with peer dependencies found . └─┬ file-loader 6.2.0 └── ✕ missing peer webpack@"^4.0.0 || ^5.0.0" Peer dependencies that should be installed: webpack@"^4.0.0 || ^5.0.0" Done in 2.7s
-
使用
import table2excel from 'js-table2excel' //js 部分 var column = []; this.$refs['myTable'].$children.forEach(element => { if(element.label && element.label!='操作') { let temp = { title: element.label, key: element.prop, //key值对应表单数据字段名称 type: 'text', } if(temp.title=='照片') { temp.type = 'image'; temp.key = 'photo'; temp.width= 75, temp.height= 100 } column.push(temp) } }); var datas = this.multipleSelection; //表单数据 //文件名称 const excelName = '学生信息_'+ new Date().toLocaleString() //生成Excel表格,自动下载 table2excel(column, datas, excelName)
二、部署
2.1、下载 Nginx
- 地址:http://nginx.org/en/download.html
- 下载:nginx/Windows-1.22.1
2.2、安装
-
解压 Nginx 压缩包
-
尽量在 cmd 窗口启动,不要直接双击 nginx.exe,这样会导致修改配置后重启、停止 nginx 无效,需要手动关闭任务管理器内的所有 nginx 进程,再启动才可以,就很麻烦。
-
查看任务进程是否存在
$ tasklist /fi "imagename eq nginx.exe" 映像名称 PID 会话名 会话# 内存使用 ========================= ======== ================ =========== ============ nginx.exe 820 Console 5 18,484 K nginx.exe 22132 Console 5 18,784 K
-
打开浏览器,输入 localhost,看到 Welcome to nginx! 即表示安装成功
-
若失败,查看 80 端口是否被占用,有 0.0.0.0:80 用任务管理器看后面 pid22132 被哪个进程占用了
netstat -ano | findstr "80"
TCP 0.0.0.0:80 0.0.0.0:0 LISTENING 22132
TCP 0.0.0.0:49666 0.0.0.0:0 LISTENING 1800
TCP 127.0.0.1:80 127.0.0.1:57124 ESTABLISHED 22132
TCP 127.0.0.1:80 127.0.0.1:57125 ESTABLISHED 22132
TCP 127.0.0.1:80 127.0.0.1:57127 ESTABLISHED 22132 -
nginx 的配置文件是 conf 目录下的 nginx.conf,默认配置的 nginx 监听的端口为 80,如果本地电脑的 80 端口有被占用,如果本地 80 端口已经被使用则修改成其他端口。
-
常用命令
1、启动: $ start nginx 2、停止: $ nginx.exe -s stop 或 C:\nginx>nginx.exe -s quit 注:stop是快速停止nginx,可能并不保存相关信息;quit是完整有序的停止nginx,并保存相关信息。 执行 nginx.exe -s stop或者quit命令是不是不能删除进程?查看进程开了一堆nignx.exe 还有80端口在Listening,并且浏览器F5刷新还能访问页面,可能nginx.exe版本或系统的原因,用 taskkill /f /im nginx.exe > null 杀死nginx进程 3、重新载入Nginx: $ nginx.exe -s reload 当配置信息修改,需要重新载入这些配置时使用此命令。 4、重新打开日志文件: $ nginx.exe -s reopen 5、查看Nginx版本: $ nginx -v nginx version: nginx/1.22.1
-
配置文件 nginx.conf
#user nobody; #==工作进程数,一般设置为cpu核心数 worker_processes 1; #error_log logs/error.log; #error_log logs/error.log notice; #error_log logs/error.log info; #pid logs/nginx.pid; events { #==最大连接数,一般设置为cpu*2048 worker_connections 1024; } http { include mime.types; default_type application/octet-stream; #log_format main '$remote_addr - $remote_user [$time_local] "$request" ' # '$status $body_bytes_sent "$http_referer" ' # '"$http_user_agent" "$http_x_forwarded_for"'; #access_log logs/access.log main; sendfile on; #tcp_nopush on; #keepalive_timeout 0; #==客户端链接超时时间 keepalive_timeout 65; #gzip on; #当配置多个server节点时,默认server names的缓存区大小就不够了,需要手动设置大一点 server_names_hash_bucket_size 512; #server表示虚拟主机可以理解为一个站点,可以配置多个server节点搭建多个站点 #每一个请求进来确定使用哪个server由server_name确定 server { #站点监听端口 listen 80; #站点访问域名 server_name localhost; #编码格式,避免url参数乱码 charset utf-8; #access_log logs/host.access.log main; #location用来匹配同一域名下多个URI的访问规则 #比如动态资源如何跳转,静态资源如何跳转等 #location后面跟着的/代表匹配规则 location / { #站点根目录,可以是相对路径,也可以使绝对路径 root html; #默认主页 index index.html index.htm; #转发后端站点地址,一般用于做软负载,轮询后端服务器 #proxy_pass http://10.11.12.237:8080; #拒绝请求,返回403,一般用于某些目录禁止访问 #deny all; #允许请求 #allow all; add_header 'Access-Control-Allow-Origin' '*'; add_header 'Access-Control-Allow-Credentials' 'true'; add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type'; #重新定义或者添加发往后端服务器的请求头 #给请求头中添加客户请求主机名 proxy_set_header Host $host; #给请求头中添加客户端IP proxy_set_header X-Real-IP $remote_addr; #将$remote_addr变量值添加在客户端“X-Forwarded-For”请求头的后面,并以逗号分隔。 如果客户端请求未携带“X-Forwarded-For”请求头,$proxy_add_x_forwarded_for变量值将与$remote_addr变量相同 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; #给请求头中添加客户端的Cookie proxy_set_header Cookie $http_cookie; #将使用代理服务器的主域名和端口号来替换。如果端口是80,可以不加。 proxy_redirect off; #浏览器对 Cookie 有很多限制,如果 Cookie 的 Domain 部分与当前页面的 Domain 不匹配就无法写入。 #所以如果请求 A 域名,服务器 proxy_pass 到 B 域名,然后 B 服务器输出 Domian=B 的 Cookie, #前端的页面依然停留在 A 域名上,于是浏览器就无法将 Cookie 写入。 #不仅是域名,浏览器对 Path 也有限制。我们经常会 proxy_pass 到目标服务器的某个 Path 下, #不把这个 Path 暴露给浏览器。这时候如果目标服务器的 Cookie 写死了 Path 也会出现 Cookie 无法写入的问题。 #设置“Set-Cookie”响应头中的domain属性的替换文本,其值可以为一个字符串、正则表达式的模式或一个引用的变量 #转发后端服务器如果需要Cookie则需要将cookie domain也进行转换,否则前端域名与后端域名不一致cookie就会无法存取 #配置规则:proxy_cookie_domain serverDomain(后端服务器域) nginxDomain(nginx服务器域) proxy_cookie_domain localhost .testcaigou800.com; #取消当前配置级别的所有proxy_cookie_domain指令 #proxy_cookie_domain off; #与后端服务器建立连接的超时时间。一般不可能大于75秒; proxy_connect_timeout 30; } #error_page 404 /404.html; # redirect server error pages to the static page /50x.html # error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } } #当需要对同一端口监听多个域名时,使用如下配置,端口相同域名不同,server_name也可以使用正则进行配置 #但要注意server过多需要手动扩大server_names_hash_bucket_size缓存区大小 server { listen 80; server_name www.tjhis.cn; charset utf-8; location / { proxy_pass http://localhost:10001; } } #server { # listen 80; # server_name aaa.abc.com; # charset utf-8; # location / { # proxy_pass http://localhost:20002; # } #} }
-
将要发布的文件拷贝到目录 html 中或者指定路径,可以是相对路径,也可以使绝对路径
2.3、问题
2.3.1 无法访问子路由
-
nginx 增加配置即可
try_files $uri $uri/ /index.html;
-
完整的位置
location / { root D:\vscode\his.vue\dist; index index.html index.htm; try_files $uri $uri/ /index.html; }
3.4、显示中文
-
比如分页组件不显示中文的问题
-
修改app.vue
<template> <el-config-provider :locale="locale"> <router-view></router-view> </el-config-provider> </template> <script lang="ts"> import { ElConfigProvider } from 'element-plus' // 引入中文包 import zhCn from 'element-plus/lib/locale/lang/zh-cn' export default { components: { [ElConfigProvider.name]: ElConfigProvider }, setup() { const lang = () => {// eslint-disable-line no-unused-vars(如果运行时报错,就加上前面这行注释) location.reload() } let locale = zhCn return { locale } } } </script>
三、vue
3.1、组件传值
- 父传子:defineProps
- 子传父:defineEmits
- 子暴露:defineExpose
3.1.1、父传给子
-
父
<HelloWorld msg="我是父组件" /> <!-- 复杂类型 --> <script setup lang="ts"> import { reactive } from "vue"; import HelloWorld from "./components/HelloWorld.vue"; let obj = reactive<number[]>([]); obj = [1, 4, 6]; </script> <template> <h1>父组件</h1> <hr /> <HelloWorld :msg="obj" title="测试" /> </template>
-
子
<script setup lang="ts"> defineProps<{ msg: string }>(); </script> <!-- 复杂类型 --> <script setup lang="ts"> // 都是必填项 const props = defineProps<{ msg: number[]; title: string }>(); // 如何使用 console.log(props.msg[0]); </script> <!-- 默认值 --> <script setup lang="ts"> const props = withDefaults( defineProps<{ msg: number[]; title: string }>(), { title: "123" } ); </script>
-
问题
- 父给子传值:单项绑定属性的方法
- defineProps 里面的都是必填,父组件在调用是不填写,会报错。
- 如何变成非必填项,给个默认值即可,使用 withDefaults,没有给默认值的仍然为必填项。
3.1.2、子传给父
-
子
<script setup lang="ts"> import { ref } from "vue"; // const emit = defineEmits(['on-click', 'on-change']) const emit = defineEmits<{ (e: "on-click", name: any): void; // 注意这里的类型不要定义成number,否则无法和父组件同时改变值 (e: "on-change", name: string): void; }>(); const btn = () => { emit("on-click", count); emit("on-change", "张三"); }; let count = ref(0); </script> <template> <button @click="btn">给父组件传值</button> <h1>子组件:</h1> <button @click="count++">count : {{ count }}</button> </template>
<script setup lang="ts">
import { ref } from "vue";
// const emit = defineEmits(['on-click', 'on-change'])
let count = ref(0);
const btn = () => {
emit("on-click", count);
emit("on-change", "张三");
};
</script>
<template>
<button @click="btn">给父组件传值</button>
<h1>子组件:</h1>
<button @click="count++">count : {{ count }}</button>
</template>
-
父
<script setup lang="ts"> import { reactive, ref } from "vue"; import HelloWorld from "./components/HelloWorld.vue"; let a = ref(10); let b = ref(""); const getName = (val: number) => { a.value = val; }; const getName2 = (val: string) => { b.value = val; }; </script> <template> <h1>父组件 {{ a }}</h1> {{ b }} <hr /> <HelloWorld @on-change="getName2" @on-click="getName" /> </template>
-
点击 count++按钮,count 会实现父子同时改变
-
问题
3.1.3、子组件暴露属性和方法给父组件
-
子
<script setup lang="ts"> const click = () => { console.log('北京'); } let title = "中国" defineExpose({ title, click }) </script>
-
父
<HelloWorld ref="childVal" /> <script setup lang="ts"> import { reactive, ref, onMounted } from 'vue'; import HelloWorld from './components/HelloWorld.vue'; // const childVal = ref<InstanceType<typeof HelloWorld>>() const childVal = ref() onMounted(() => { console.log(childVal.value?.title); childVal.value?.click() })
3.2、Route
3.2.1、定义一个路由
- 导入组件
- 定义路由
- 类型是 Array
- component 必传 在 RouteRecordSingleView 中定义
- 第一种写法: 先定义,后引用
{ name: "工作台", path: "/desktop", component: DeskTop }
- 第二种写法,不用定义,直接用:
{ path: "/login", component: () => import("@/views/admin/LoginPage.vue") },
- path 必传 在 _RouteRecordBase 中定义
- 创建路由
- 路由守卫......
- 导出路由
// 1. 导入组件
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
import LoginPage from "@/views/admin/LoginPage.vue";
import Main from "@/views/admin/Main.vue";
import DeskTop from "@/views/admin/DeskTop.vue";
import PersonCenter from "@/views/admin/PersonCenter.vue";
import Json from "@/views/others/JsonView.vue";
import ProductListComVue from "@/components/shoppingCart/ProductListCom.vue";
// 2. 定义路由
const routes: Array<RouteRecordRaw> = [
{
path: "/",
name: "主页",
component: Main,
children: [
{ name: "工作台", path: "/desktop", component: DeskTop },
{ name: "个人信息", path: "/person", component: PersonCenter },
{ name: "Json解析", path: "/json", component: Json },
{ name: "购物车", path: "/product", component: ProductListComVue },
],
},
{ path: "/login", component: () => import("@/views/admin/LoginPage.vue") },
];
// 3. 创建路由
const router = createRouter({
history: createWebHistory(),
routes,
});
// 4.导出路由
export default router;
3.2.2、导航
- createWebHashHistory(),
- createWebHistory(),
- router-link
- 编程式导航
- 字符串模式
- 对象模式
- 命名式路由
- a标签
- 直接通过a href也可以跳转但是会刷新页面
- replace 可以进行页面跳转,但history中其不会重复保存记录,就是无法前进和后退
<!-- 使用 router-link 组件进行导航 -->
<!-- 通过传递 `to` 来指定链接 -->
<!--`<router-link>` 将呈现一个带有正确 `href` 属性的 `<a>` 标签-->
<router-link tag="div" to="/">跳转a</router-link>
<router-link tag="div" to="/register">跳转b</router-link>
<!-- name必须和定义路由的名字一致 -->
<router-link :to="{name:'Login'}">Login</router-link>
<router-link :to="{name:'Reg'}">Reg</router-link>
<script>
import { useRouter } from 'vue-router'
const router = useRouter()
// 1. 字符串模式
const toPage = () => {
router.push('/reg')}
// 2. 对象模式
const toPage = () => {
router.push({
path: '/reg'
})}
// 3. 命名式路由
const toPage = () => {
router.push({
name: 'Reg'
})
</script>
// replace用法
<router-link replace to="/reg">Reg</router-link>
<script>
const toPage = (url: string) => {
router.replace(url)
</script>
3.2.3. 路由传参
-
区别
- query 传参配置的是 path,而 params 传参配置的是name,在 params中配置 path 无效
- query 在路由配置不需要设置参数,而 params 必须设置
- query 传递的参数会显示在地址栏中
- params传参刷新会无效,但是 query 会保存传递过来的值,刷新不变 ;
-
query的传参方式
- 发送方
const showClick = (product: IProduct) => { router.push({ path: '/productDetail', query: { ...product } }); }
-
接收方
<el-form :model="form" label-width="120px"> <el-form-item label="商品ID"> <el-input v-model="form.id" disabled /> </el-form-item> <el-form-item label="代码"> <el-input v-model="form.itemCode" disabled /> </el-form-item> <el-form-item label="名称"> <el-input v-model="form.itemName" disabled /> </el-form-item> <el-form-item> <el-button type="primary" @click="router.back()">返回</el-button> </el-form-item> </el-form> <script setup lang='ts'> import { reactive, ref, onMounted } from 'vue' import { useRoute, useRouter } from "vue-router"; const route = useRoute() const router = useRouter() const form = route.query </script>
-
编程式导航:params的传参方式,刷新后参数会丢失
const toDetail = (item: Item) => { router.push({ name: 'Reg', params: item }) }
-
动态路由参数
-
路径参数 用冒号
:
表示。当一个路由被匹配时,它的 params 的值将在每个组件//动态路由参数 path:"/reg/:id", name:"Reg", component:()=> import('../components/reg.vue') // 传参 const toDetail = (item: Item) => { router.push({ name: 'Reg', params: { id: item.id } }) }
-
3.3.4、路由守卫
to: Route, 即将要进入的目标 路由对象;
from: Route,当前导航正要离开的路由;
next(): 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。
next(false): 中断当前的导航。如果浏览器的 URL 改变了 (可能是用户手动或者浏览器后退按钮),那么 URL 地址会重置到 from 路由对应的地址。
next('/') 或者 next({ path: '/' }): 跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航。
router.beforeEach((to, form) => {
if (localStorage["nickname"] != undefined) {
const user: UserInfo = JSON.parse(
new Tool().FormatToken(localStorage["token"])
);
const expDate = toolObj.FormatDate(user.exp);
const currDate = toolObj.GetDate();
if (to.path == "/login") {
if (expDate >= currDate) {
return { path: "/desktop" };
} else {
toolObj.ClearLocalStorage();
}
} else {
if (expDate < currDate) {
toolObj.ClearLocalStorage();
return { path: "/login" };
}
}
} else {
//避免无限重定向,因此要做个判断
if (to.path !== "/login") {
return { path: "/login" };
}
}
});
3.3.5、利用守卫做loading效果
-
定义页面
/** * @description 页面加载的loading效果 * @author wanghx * @date 2022-12-02 19:15:34 */ <template> <div class="wraps"> <div ref="bar" class="bar"></div> </div> </template> <script setup lang='ts'> import { ref } from 'vue' let speed = ref<number>(1) let bar = ref<HTMLElement>() let timer = ref<number>(0) const startLoading = () => { let dom = bar.value as HTMLElement; speed.value = 1 timer.value = window.requestAnimationFrame(function fn() { if (speed.value < 90) { speed.value += 1; dom.style.width = speed.value + '%' timer.value = window.requestAnimationFrame(fn) } else { speed.value = 1; window.cancelAnimationFrame(timer.value) } }) } const endLoading = () => { let dom = bar.value as HTMLElement; setTimeout(() => { window.requestAnimationFrame(() => { speed.value = 100; dom.style.width = speed.value + '%' }) }, 500) } defineExpose({ startLoading, endLoading }) </script> <style scoped lang="scss"> .wraps { position: fixed; top: 0; width: 100%; height: 2px; .bar { height: inherit; width: 0; background: #F56C6C; } } </style>
-
在路由的ts中,挂在页面,并在路由守卫中加载
import { createVNode, render } from "vue"; import LoadingBar from "@/components/others/loadingBar.vue"; const Vnode = createVNode(LoadingBar); render(Vnode, document.body); // 前置路由守卫 router.beforeEach((to, form) => { // 页面加载效果 开始 Vnode.component?.exposed?.startLoading(); } // 后置路由守卫 router.afterEach((to, from) => { // 页面加载loading效果结束 Vnode.component?.exposed?.endLoading(); });
3.4.6、路由元数据
通过路由记录的
meta
属性可以定义路由的元信息。使用路由元信息可以在路由中附加自定义的数据,例如:
- 权限校验标识。
- 路由组件的过渡名称。
- 路由组件持久化缓存 (keep-alive) 的相关配置。
- 标题名称
{
name: "登录",
path: "/login",
component: () => import("@/views/admin/LoginPage.vue"),
meta: {
title: "登录",
},
},
declare module 'vue-router' {
interface RouteMeta {
title?: string
}
}
3.4.7、动态路由
-
登录时加载路由
-
动态路由必须是相对路径.
-
因为刷新时路由会丢失,会在路由守卫beforeEach中重新加载,所以components的路径一定要用../打头,且LoginPage,必须在views的根目录,index.ts必须在router的根目录.
-
直接把‘@/’配置到url中引入,会报错,没法识别地址
-
比如在LoginPage中加载路由,那么components的路径必须是相对于LoginPage的路径
-
页面刷新时,路由重新初始化,动态添加的路由此时已不存在,只有一些固定路由(比如登录页面)还在,所以出现了404的情况
-
所以:在vue项目中采用动态添加路由的方式,第一次进入页面会正常显示,但是点击刷新页面后会导致页面空白
-
vue3中去掉了addRoutes只能使用addRoute添加路由
{ key: 唯一值id, title: 名称, icon: 图标, children: [{ parentKey: 父id, key: 唯一值id, title:名称, name:路由名称 path: 文件路径, type: 类型, MENU菜单 BUTTON 具体按钮 hiddle:作为菜单是否隐藏 true false }] }
-
网上的处理办法
// 路由守卫 let registerRouteFresh = true router.beforeEach(async (to, from, next) => { let res = await api.parentMenu() let arr = [] res.data.data.filter((value, index) => { let child = [] if (value.children && value.children.length) { value.children.filter((val, i) => { child.push({ name: val.routeName, path: val.path, component: () => import(`@/${val.component}`) }) }) } arr.push({ name: value.routeName, redirect: value.redirect, path: value.path, component: () => import(`@/${value.component}`), children: child }) }) // 如果首次或者刷新界面,next(...to, replace: true)会循环遍历路由,如果to找不到对应的路由那么他会再执行一次beforeEach((to, from, next))直到找到对应的路由,我们的问题在于页面刷新以后异步获取数据,直接执行next()感觉路由添加了但是在next()之后执行的,所以我们没法导航到相应的界面。这里使用变量registerRouteFresh变量做记录,直到找到相应的路由以后,把值设置为false然后走else执行next(),整个流程就走完了,路由也就添加完了。 if (registerRouteFresh) { arr.forEach((value, index) => { router.addRoute(value) }) next({...to, replace: true}) registerRouteFresh = false } else { next() }
-
我的办法,当刷新时,路由会重置,那么就把代码写在路由中store/index.ts,使用defineAsyncComponent,Vue 3.x 新增一个辅助函数defineAsyncComponent,用来显示声明异步组件
//创建路由 const router = createRouter({ history: createWebHistory(), routes, }); //当前存在用户信息且有效,才会读取动态路由 if (localStorage["nickname"] != undefined) { const user: UserInfo = JSON.parse( new Tool().FormatToken(localStorage["token"]) ); const expDate = toolObj.FormatDate(user.exp); const currDate = toolObj.GetDate(); if (expDate >= currDate) { //读取webapi,加载用户路由信息 const list: MenuModel[] = (await getRouters()) as any as MenuModel[]; if (list.length > 0) { router.addRoute({ path: "/", name: "admin", //这儿不能使用@ component: () => import("@/views/admin/Main.vue"), }); list.forEach((v: any) => { router.addRoute("admin", { path: v.path, name: v.name, //这儿不能使用@ component: () => defineAsyncComponent( () => import(/* @vite-ignore */ v.filePath + ".vue") ), }); }); } } }
-
总结:动态路由,问题太多,直接配置吧
四、TypeScript
4.1、定义接口
interface IProduct {
id: number;
itemCode: string;
itemName: string;
itemSpec: string;
units: string;
price: number;
inventory: number;
}
4.2、定义类型
- 合并 IProduct 类,但是不需要
inventory属性
type Cart = {
quantity: number;
} & Omit<IProduct, "inventory">;
4.3、展开运算符
- 18 行:{ ...product, quantity: 1 }
- 问题:TCartProduct 会多一个属性 inventory
state: () => {
return {
ProductList: [] as IProduct[],
CartProducts: [] as TCartProduct[],
};
},
actions: {
addProductToCart(product: IProduct) {
// 1、是否有库存
if (product.inventory < 1) {
ElMessage.error(
`商品:${product.itemName}库存为${product.inventory},已经不足`
);
return;
}
// 2、购物车是否已存在该商品
const cartItem = this.CartProducts.find((item) => item.id === product.id);
// 3、存在则数量加 1
if (cartItem) {
cartItem.quantity++;
product.inventory--;
}
// 4、不存在则新增,且数量为 1
else {
this.CartProducts.push({ ...product, quantity: 1 });
product.inventory--;
}
},
}
五、Element Plus
5.1、table 中行内按钮获取行数据
- 10 行:通过<template #default="scope"> 获取
<el-table :data="productStore.ProductList" border stripe style="width: 100%">
<el-table-column prop="itemName" label="名称" width="300" fixed sortable />
<el-table-column prop="id" label="Id" width="120" align="center" />
<el-table-column prop="itemCode" label="编号" width="200" />
<el-table-column prop="itemSpec" label="规格" width="300" />
<el-table-column prop="units" label="单位" width="120" />
<el-table-column prop="price" label="单价" width="120" align="right" />
<el-table-column prop="inventory" label="库存" width="120" align="right" />
<el-table-column fixed="right" label="操作" width="140" align="center">
<template #default="scope">
<el-button
type="primary"
size="small"
icon="el-icon-delete"
style="width: 100px;"
@click.stop="addClick(scope.row)"
>
<el-icon size="16" color="#ffffff"> <ShoppingCart /> </el-icon
><span style="padding-left: 6px;">加入购物车</span>
</el-button>
</template>
</el-table-column>
</el-table>
5.2、tab的计算列
- html代码
<h3>【{{ useGlobalStore().NickName }}】的购物车</h3>
<el-table :data="cartStore.CartProducts" border stripe style="width: 100%" show-summary sum-text="总计"
:summary-method="getSummaries">
<el-table-column prop="itemName" label="名称" width="300" fixed />
<el-table-column prop="itemSpec" label="规格" width="300" />
<el-table-column prop="units" label="单位" width="160" />
<el-table-column prop="price" label="单价" width="160" align="right" />
<el-table-column fixed="right" prop="quantity" label="数量" width="160" align="right" />
<el-table-column fixed="right" prop="costs" label="小计" width="160" align="right" />
<el-table-column fixed="right" label="操作" width="140">
<template #default="scope">
<el-button type="danger" size="small" style="width: 100px;" @click.stop="delClick(scope.row)">
<el-icon size="16" color="#ffffff">
<Delete />
</el-icon><span style="padding-left: 6px;">删除</span>
</el-button>
</template>
</el-table-column>
</el-table>
- 计算函数
/**
* tab的计算列
*/
const getSummaries = (param: SummaryMethodProps): string[] => {
// 解构出来所有的字段和数据
const { columns, data } = param
// 计算列字段
const sums: string[] = []
columns.forEach((column, index) => {
if (index === 0) {
sums[index] = '总计'
return
}
// 单价不合计
if (column.property === 'price') {
return
}
// 获取所有的值
const values = data.map(item => Number(item[column.property]))
if (!values.every(value => Number.isNaN(value))) {
var temp = ` ${values.reduce((prev, curr) => {
const value = Number(curr)
if (!Number.isNaN(value)) {
return prev + curr
} else {
return prev
}
}, 0)}`;
sums[index] = (Number(temp)).toFixed(2);
} else {
sums[index] = ''
}
})
return sums
}
5.3、动态Icon
-
全局引入
// 在main.ts注册Icon组件 import * as Icons from "@element-plus/icons-vue"; const app = createApp(App); // 创建Icon组件 for (const [key, component] of Object.entries(Icons)) { app.component(key, component); }
-
创建通用组件
/** * @description icon图标 * @author wanghx * @date 2022-12-03 14:31:36 */ <template> <i class="el-icon" :style="setIconSvgStyle"> <component :is="getIconName" /> </i> </template> <script setup lang='ts'> import { computed } from 'vue' // 定义父组件传过来的值 const props = defineProps({ // svg 图标组件名字 name: { type: String, }, // svg 大小 size: { type: Number, default: () => 1.2, }, // svg 颜色 color: { type: String, }, }); // 获取 icon 图标名称 const getIconName = computed(() => { return props?.name; }); // 设置图标样式 const setIconSvgStyle = computed(() => { return `font-size: ${props.size}em;color: ${props.color};margin-right:0.5em`; }); </script> <style lang='scss' scoped> </style>
-
使用方法
<script> import IconCom from '@/components/others/IconCom.vue'; </script> <template> <IconCom name="box" :size=1 color="red" /> </template>
-
按钮的图标
<el-button class="log-btn-commit" type="primary" :icon="Select" @click="submitForm(ruleFormRef)">登录 </el-button>
5.4、消息提示框
-
消息提示
ElMessage({ message: '删除成功!', type: 'success', })
-
弹出式消息提示
showMessage(msg: string) { ElMessageBox.alert(msg, "提示信息", { confirmButtonText: "OK", icon: msg.includes("成功") ? markRaw(SuccessFilled) : msg.includes("失败") ? markRaw(CircleCloseFilled) : markRaw(QuestionFilled), type: msg.includes("成功") ? "success" : msg.includes("失败") ? "error" : "warning", draggable: true, // 是否支持html dangerouslyUseHTMLString: true, }); },
-
弹出式带确认框的提示
ElMessageBox.confirm(`你确定要删除id为【${ids}】的${arr.length}条数据吗?`, '提示信息', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning', }).then(async () => { // 成功之后的回调函数 const res = await batchDelMenu(ids) as any as boolean if (res) { globalStore.showMessage("批量删除成功") LoadTableData() } }).catch(() => { // 取消按钮的回调 console.log("====="); })
5.5 、Tree V2 虚拟化树形控件
-
用法
<el-tree-v2 :data="treedata" :props="{ value: 'id', label: 'name', children: 'children' }" show-checkbox ref="tree" :height="500" />
-
操作
// 获取选中的节点 通过ref指向 const nodes: [] = tree.value.getCheckedNodes() // 将某些节点设置为选中 watch(() => props.roleId, async (newId, oldIdo) => { if (newId != undefined) { defaultCheckedKeys = await GetRoleMenuById(newId) as unknown as number[]; tree.value.setCheckedKeys(defaultCheckedKeys) } else { } })
六、pinia
6.1、计算属性的用法
getters: {
// 获取所有商品的总价格
totalPrice(state) {
return state.CartProducts.reduce((total, item) => {
return total + item.costs;
}, 0);
},
},
七、其他技巧
7.1、元素两边分
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
7.2、自动适应
<el-row :gutter="10">
<el-col :span="24" :offset="0" :xs="24" :sm="12" :md="12" :lg="6" :xl="6">
</el-col>
</el-row>
标签:reused,vue,const,ts,downloaded,added,import,vite
From: https://www.cnblogs.com/his365/p/16977016.html