vue-vite-ts 新版

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>
posted @ 2022-12-12 20:25  his365  阅读(328)  评论(0编辑  收藏  举报