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:
create-vite@3.2.1
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

  • https://echarts.apache.org/handbook/zh/get-started

  • 安装

    $ 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 "9016814@qq.com"
  • 已有仓库

    cd his.api
    git remote add origin git@gitee.com:his7/his.vue.git
    git push -u origin "master"
  • 新建仓库

    $ git init
    $ git add .
    $ git status
    git commit -m "创建项目"
    git remote add origin git@gitee.com:his7/his.vue.git
    git push -u origin "master"

1.15、js-table2excel

  • 安装 npm install js-table2excelpnpm 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)

1.17、nprogress

  • 安装 npm i nprogress

  • 在router.js中使用

    import Vue from 'vue'
    import Router from 'vue-router'
    import NProgress from 'nprogress'
    import 'nprogress/nprogress.css' Vue.use(Router) const router = new Router({
    mode: 'history',
    routes: [
    ]
    }) router.beforeEach((to, from, next) => {
    NProgress.start()
    /// code
    })
    router.afterEach(() => {
    NProgress.done()
    })
  • 修改颜色

    #nprogress .bar {
    background: red !important; //颜色可修改
    }
  • 不显示时,nprogress的z-index,假如你的header比nprogress的高,可能看不见进度条,可以采用这个办法实施,其中数字比header高就行,或者,你改header的z-index

    #nprogress {
    .bar {
    z-index: 15031;
    }
    .spinner {
    z-index: 15031;
    }
    }
  • 其它用法

    NProgress.start() — 显示进度条
    NProgress.set(0.4) —设置百分比
    NProgress.inc() — 增加一点点
    NProgress.done() — 完成进度条
  • axios中的写法

    // axios请求拦截器
    axios.interceptors.request.use(
    config => {
    NProgress.start() // 设置加载进度条(开始..)
    return config
    },
    error => {
    return Promise.reject(error)
    }
    )
    // axios响应拦截器
    axios.interceptors.response.use(
    function(response) {
    NProgress.done() // 设置加载进度条(结束..)
    return response
    },
    function(error) {
    return Promise.reject(error)
    }
    )

二、部署

2.1、下载 Nginx

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、定义一个路由

  1. 导入组件
  2. 定义路由
    1. 类型是 Array
    2. component 必传 在 RouteRecordSingleView 中定义
      1. 第一种写法: 先定义,后引用 { name: "工作台", path: "/desktop", component: DeskTop }
      2. 第二种写法,不用定义,直接用:{ path: "/login", component: () => import("@/views/admin/LoginPage.vue") },
    3. path 必传 在 _RouteRecordBase 中定义
  3. 创建路由
  4. 路由守卫......
  5. 导出路由
// 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
  • 编程式导航
      1. 字符串模式
      2. 对象模式
      3. 命名式路由
  • 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 行:
  • 问题: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>

vue-vite-ts 新版的更多相关文章

  1. Vite+TS带你搭建一个属于自己的Vue3组件库

    theme: nico 前言 随着前端技术的发展,业界涌现出了许多的UI组件库.例如我们熟知的ElementUI,Vant,AntDesign等等.但是作为一个前端开发者,你知道一个UI组件库是如何被 ...

  2. 快速新建并配置一个eslint+prettier+husky+commitlint+vue3+vite+ts+pnpm的项目

    前置准备 一台电脑 vscode pnpm vscode插件:ESLint v2.2.6及以上 vscode插件:Prettier - Code formatter v9.5.0及以上 vscode插 ...

  3. vite+ts+vue3+router4+Pinia+ElmPlus+axios+mock项目基本配置

    1.vite+TS+Vue3 npm create vite Project name:... yourProjectName Select a framework:>>Vue Selec ...

  4. vite + ts 快速搭建 vue3 项目 以及介绍相关特性

    博客地址:https://ainyi.com/98 Vue3.0,One Piece 接下来得抽空好好学习了 vite 尤大在 Vue 3.0 beta 直播中推荐了 vite 的工具,强调:针对Vu ...

  5. vue3.0+vite+ts项目搭建(报错处理)

    报错一 warning package.json: No license field$ vue-tsc --noEmit && vite build 解决方案,添加这两行,只添加一个是 ...

  6. vue3.0+vite+ts项目搭建--vite.config.ts配置(三)

    vite.config.ts配置 配置路径处理模块 安装ts的类型声明文件 yarn add @types/node -D 通过配置alias来定义路径的别名 resolve: { alias: { ...

  7. vue3.0+vite+ts项目搭建--基础配置(二)

    集成vue-router 使用yarn yarn add vue-router@next --save 安装完成之后在src目录下创建文件夹router/index.ts,创建完成之后需要在Vue-R ...

  8. Vue实践TS中的一些常见错误解决方案

    mixin报错 import { Component, Prop, Vue ,Mixins} from 'vue-property-decorator' import httpminix from ' ...

  9. 基于typescript编写vue的ts文件语法模板

    1 <template> 2 <div> 3 <input v-model="msg"> 4 <p>prop: {{ propMes ...

  10. vue3.0+vite+ts项目搭建-分环境打包(四)

    分环境打包配置 新建.env.dev(或者.env) VITE_NODE_ENV = 'dev' VITE_HOST = 'http://local.host.com' 执行yarn dev ,控制台 ...

随机推荐

  1. org.elasticsearch.ElasticsearchException: not all primary shards of [.geoip_databases] index are active解决办法

    解决办法 在配置elasticsearch.yml中加上 ingest.geoip.downloader.enabled: false

  2. Google Earth Engine——基于新的Landsat SR数据集去云处理

    根据GEE官方公告,明年原来的Landsat/LT05/C01/T1_SR和Landsat/LC08/C01/T1_SR数据集将停止更新,并提供了新的地表反射率数据,就是LANDSAT/LT05/C0 ...

  3. 自定义一个JdbcTemplate(增删改数据库中表记录)

    需求: 自定义一个JdbcTemplate模板,实现增删改数据库中表记录的功能 1 package demo03; 2 3 import utils.JDBC_DBCP_Utils; 4 5 impo ...

  4. N63050 第十五周运维作业

    第二十九天: 网络文件共享服务 1基于DB数据库文件实现FTP的虚拟用户 2基于MySQL数据库文件实现FTP的虚拟用户 3NFS服务的工作原理 4NFS共享服务实现详解 5实现NFS共享存储的LAM ...

  5. N63050 第十一周运维作业

    第十一周 就业和全程班小伙伴本周学习内容: 二十一.Mysql数据库二 1.MySQL的视图函数存储过程触发器和事件管理(64分钟) 2.MySQL用户和权限管理(40分钟) 3.MySQL架构和存储 ...

  6. SAP 采购订单行项目客制化字段增强

    需求: 在采购订单行项目中新增客制化字段,区分采购的项目中的物料是量产还是研发物料 开发步骤 主要使用二代增强出口:MM06E005 创建增强项目 事务码T-code:CMOD 创建项目ZEMM001 ...

  7. 多点DLT (Direct Linear Transformation) 算法

    阅读前可以先参看上一篇代数视觉博客: 四点DLT (Dierct Linear Transformation) 算法 对于大于4个点的数据点来进行 DLT 算法变换, 如果数据点的标注都十分准确,那么 ...

  8. KU060板卡设计资料原理图第636篇:基于FMC的KU060高性能 PCIe 载板

    基于FMC的KU060高性能 PCIe 载板 一.板卡概述 板卡主控芯片采用Xilinx 公司的 Kintex UltraScale系列FPGA XCKU060-2FFVA1156.板载 2 组 64 ...

  9. 【Linux】常用

    查看端口占用 yum install lsof lsof -i:8080:查看8080端口占用 lsof abc.txt:显示开启文件abc.txt的进程 lsof -c abc:显示abc进程现在打 ...

  10. gcc_to_use

    gcc 目录 gcc 概要 基本指令及功能(以gcc为例) gcc -gdb gcc -cmake 概要 GCC:GNU Compiler Collection(GUN 编译器集合),是GNU项目中符 ...