LOADING

加载过慢请开启缓存 浏览器默认开启

耀耀切克闹

vite 搭建vue3项目(二)

项目搭建 2022/10/25

1.login页面和功能就不多哔哔了

2.主体布局

dd7860edba87d1d65d1c03b81f01f4c6.png
layout->index.vue

<template>
   <el-container>
        <el-aside>
            <Menu />
        </el-aside>
        <el-container>
            <el-header>
            </el-header>
            <el-main>
            //mian开发中
                <!-- <router-view v-slot="{ Component, route }">
                    <transition appear name="fade-transform" mode="out-in">
                        <keep-alive :include="cacheRouter">
                            <component :is="Component" :key="route.path"></component>
                        </keep-alive>
                    </transition>
                </router-view> -->
            </el-main>
            <el-footer>
                <Footer />
            </el-footer>
        </el-container>
    </el-container>
</template>

<script setup lang="ts">
import Footer from "./footer/index.vue";
import Menu from "./Menu/index.vue";
</script>

<style lang="scss" scoped>
    @import "./index.scss";
</style>

3.vite中的批量自动化导入:import.meta.globEager

如果想在vite中批量导入某些文件,实现项目的模块化,vite提供的import.meta.globEager函数就很好用

比如用在路由模块化:

1、需求:不想把路由文件全部放在一个文件里面,找的时候要拖动很麻烦,就想着把每一个模块的路由按功能分成单个的文件

2、思路:在routers文件夹内新增一个modules文件夹:里面放不同功能的routers文件,然后在vue引入的路由入口处批量导入模块化的routers

3、实现:

87e7997f3c15921b398a37d928ac3a32.png

在router文件内批量引入modules内模块化的文件并处理:

import { createRouter, createWebHashHistory, RouteRecordRaw } from "vue-router";

// * 导入所有router
//const metaRouters = import.meta.globEager("./modules/*.ts");
//最新vite应该是弃用了上面的,用下面的
const metaRouters:any = import.meta.glob('./modules/*.ts', { eager: true })
// * 处理路由表
export const routerArray: RouteRecordRaw[] = [];
Object.keys(metaRouters).forEach(item => {
    Object.keys(metaRouters[item]).forEach((key: any) => {
    //    routerArray.push(...metaRouters[item][key]);
    routerArray.push(metaRouters[item][key]);
    });
});

/**
 * @description 路由配置简介
 * @param path ==> 路由路径
 * @param name ==> 路由名称
 * @param redirect ==> 路由重定向
 * @param component ==> 路由组件
 * @param meta ==> 路由元信息
 * @param meta.requireAuth ==> 是否需要权限验证
 * @param meta.keepAlive ==> 是否需要缓存该路由
 * @param meta.title ==> 路由标题
 * @param meta.key    ==> 路由key,用来匹配按钮权限
 * */
const routes: RouteRecordRaw[] = [
  ...routerArray,
];

const router = createRouter({
    history: createWebHashHistory(),
    routes,
    strict: false,
    // 切换页面,滚动到最顶部
    scrollBehavior: () => ({ left: 0, top: 0 })
});

export default router;

注意 再使用时出现import.meta.globEager(“./modules/*.ts”);报错说什么弃用了,

去源码看标注:已弃用,使用这个什么代替

@deprecated Use import.meta.glob('*', { eager: true }) instead

4.侧边栏的开发和header里的侧边栏折叠(底部栏就不多bb)

侧边栏的开发

主要是分为两部分,一部分是logo,一部分是路由菜单

logo是图片加文字,文字根据侧边栏折叠是否展示

路由菜单

    <el-scrollbar>
            <el-menu
                :default-active="activeMenu"
                :router="true"
                :collapse="isCollapse"
                :collapse-transition="false"
                :unique-opened="true"
                background-color="#191a20"
                text-color="#bdbdc0"
                active-text-color="#fff"
            >
              //菜单项
                <SubItem :menuList="menuList" />
            </el-menu>
        </el-scrollbar>

首先需要获取菜单列表,一般是调用接口根据登录的这个用户的权限获取列表接口,暂时用得请求的json模拟后台接口数据,把菜单数据存到pinia中

onMounted(async () => {
    // 获取菜单列表
    loading.value = true;
    try {
        const res = await getMenuList();
        if (!res.data) return;
        // 把路由菜单处理成一维数组(存储到 pinia 中)
        const dynamicRouter = handleRouter(res.data);
        authStore.setAuthRouter(dynamicRouter);
        menuStore.setMenuList(res.data);
    } finally {
        loading.value = false;
    }
});

然后需要有默认激活菜单的index和菜单是否折叠 获取pinia里存着的菜单数据

//默认激活菜单的 index,当前路由对象的路径
const activeMenu = computed((): string => route.path);
//菜单是否折叠
const isCollapse = computed((): boolean => menuStore.isCollapse);
//菜单数据
const menuList = computed((): Menu.MenuOptions[] => menuStore.menuList);
// 监听窗口大小变化,折叠侧边栏
const screenWidth = ref<number>(0);
const listeningWindow = () => {
    window.onresize = () => {
        return (() => {
            screenWidth.value = document.body.clientWidth;
            if (isCollapse.value === false && screenWidth.value < 1200) menuStore.setCollapse();
            if (isCollapse.value === true && screenWidth.value > 1200) menuStore.setCollapse();
        })();
    };
};
listeningWindow();

菜单项就是遍历菜单数据展示路由菜单信息(子组件需要defineProps<{ menuList: Menu.MenuOptions[] }>();)

<template v-for="subItem in menuList" :key="subItem.path">
        <el-sub-menu v-if="subItem.children && subItem.children.length > 0" :index="subItem.path">
            <template #title>
                <el-icon>
                    <component :is="subItem.icon"></component>
                </el-icon>
                <span>{{ subItem.title }}</span>
            </template>
            <SubItem :menuList="subItem.children" />
        </el-sub-menu>
        <el-menu-item v-else :index="subItem.path">
            <el-icon>
                <component :is="subItem.icon"></component>
            </el-icon>
            <template v-if="!subItem.isLink" #title>
                <span>{{ subItem.title }}</span>
            </template>
            <template v-else #title>
                <a class="menu-href" :href="subItem.isLink" target="_blank">{{ subItem.title }}</a>
            </template>
        </el-menu-item>
    </template>
<script setup lang="ts">
defineProps<{ menuList: Menu.MenuOptions[] }>();
</script>

header里的侧边栏折叠开发

监控屏幕宽度或点击叠判断是否折叠

//菜单是否折叠
const isCollapse = computed((): boolean => menuStore.isCollapse);
//菜单数据
const menuList = computed((): Menu.MenuOptions[] => menuStore.menuList);
// 监听窗口大小变化,合并 aside
const screenWidth = ref<number>(0);
const listeningWindow = () => {
    window.onresize = () => {
        return (() => {
            screenWidth.value = document.body.clientWidth;
            if (isCollapse.value === false && screenWidth.value < 1200) menuStore.setCollapse();
            if (isCollapse.value === true && screenWidth.value > 1200) menuStore.setCollapse();
        })();
    };
};

5.vue-router 利用 $route 的 matched 属性实现面包屑效果

matched 顾名思义 就是 匹配,假如我们目前的路由是/a/aa-01,那么此时 this.$route.matched匹配到的会是一个数组,包含 ‘/‘,’/a’,’/a/aa-01’,这三个path的路由信息。然后我们可以直接利用路由信息渲染我们的面包屑导航。

布局需要使用到el-breadcrumb ,和transition-group

<template>
    <el-breadcrumb :separator-icon="ArrowRight">
        <transition-group name="breadcrumb" mode="out-in">
            <el-breadcrumb-item :to="{ path: HOME_URL }" key="/home">首页</el-breadcrumb-item>
            <el-breadcrumb-item v-for="item in matched" :key="item.path" :to="{ path: item.path }">
                {{ item.meta.title }}
            </el-breadcrumb-item>
        </transition-group>
    </el-breadcrumb>
</template>

<script setup lang="ts">
import { computed } from "vue";
import { useRoute } from "vue-router";
import { ArrowRight } from "@element-plus/icons-vue";
import { HOME_URL } from "@/config/config";
const route = useRoute();

const matched = computed(() => route.matched.filter(item =>item.meta && item.meta.title && item.meta.title !== "首页"));
</script>

TransitionGroup# 是一个内置组件,用于对 v-for 列表中的元素或组件的插入、移除和顺序改变添加动画效果。这样每次选择侧边栏的路由时,面包屑导航这边就感觉比较平滑的展示

6.后台管理系统顶部使用el-tag或el-tab实现浏览路由历史实现 (标签栏管理)

1.默认有首页,不能关闭

主要就是在tabs.ts的state的tabsMenuList写死,剩下的路由历史就是往这里面tabsMenuList添加数据,剩下的就在actions里面处理了,完成增加,移除,选择,路由历史的操作具体在下面

    state: (): TabsState => ({
        tabsMenuValue: HOME_URL,
        tabsMenuList: [{ title: "首页", path: HOME_URL, icon: "home-filled", close: false }]
    }),

2.点击侧边栏上路由菜单,判断有无存在,没有就添加同时定位到上面(也就是设置tabsMenuValue),有就定位到上面

在actions里写

// Add Tabs
        async addTabs(tabItem: TabsOptions) {
            // not add tabs black list
            if (TABS_BLACK_LIST.includes(tabItem.path)) return;
            const tabInfo: TabsOptions = {
                title: tabItem.title,
                path: tabItem.path,
                close: tabItem.close
            };
            if (this.tabsMenuList.every(item => item.path !== tabItem.path)) {
                this.tabsMenuList.push(tabInfo);
            }
            this.setTabsMenuValue(tabItem.path);
        },

3.关闭当前页,自动跳到上一个tag页面

在actions里写

    // Remove Tabs
        async removeTabs(tabPath: string) {
            let tabsMenuValue = this.tabsMenuValue;
            const tabsMenuList = this.tabsMenuList;
            if (tabsMenuValue === tabPath) {
                tabsMenuList.forEach((item, index) => {
                    if (item.path !== tabPath) return;
                    const nextTab = tabsMenuList[index + 1] || tabsMenuList[index - 1];
                    if (!nextTab) return;
                    tabsMenuValue = nextTab.path;
                    router.push(nextTab.path);
                });
            }
            this.tabsMenuValue = tabsMenuValue;
            this.tabsMenuList = tabsMenuList.filter(item => item.path !== tabPath);
        },

4.选中标签 跳转到标签对应的路由

    // Change Tabs
        async changeTabs(tabItem: TabPaneProps) {
            this.tabsMenuList.forEach(item => {
                if (item.title === tabItem.label) router.push(item.path);
            });
        },
    <div class="tabs-box">
        <div class="tabs-menu">
            <el-tabs v-model="tabsMenuValue" type="card" @tab-click="tabClick" @tab-remove="removeTab">
                <el-tab-pane
                    v-for="item in tabsMenuList"
                    :key="item.path"
                    :path="item.path"
                    :label="item.title"
                    :name="item.path"
                    :closable="item.close"
                >
                    <template #label>
                        <el-icon class="tabs-icon" v-if="item.icon">
                            <component :is="item.icon"></component>
                        </el-icon>
                        {{ item.title }}
                    </template>
                </el-tab-pane>
            </el-tabs>
            //<MoreButton />
        </div>
    </div>

页面上具体使用的el-tabs实现

总结:

在store->modules->tabs.ts

import { defineStore } from "pinia";
import { TabPaneProps } from "element-plus";
import { TabsState } from "../interface";
import { HOME_URL, TABS_BLACK_LIST } from "@/config/config";
import piniaPersistConfig from "@/config/piniaPersist";
import router from "@/router/index";

// TabsStore
export const TabsStore = defineStore({
    id: "TabsState",
    state: (): TabsState => ({
        tabsMenuValue: HOME_URL,
        tabsMenuList: [{ title: "首页", path: HOME_URL, icon: "home-filled", close: false }]
    }),
    getters: {},
    actions: {
        // Add Tabs
        async addTabs(tabItem: TabsOptions) {
            // not add tabs black list
            if (TABS_BLACK_LIST.includes(tabItem.path)) return;
            const tabInfo: TabsOptions = {
                title: tabItem.title,
                path: tabItem.path,
                close: tabItem.close
            };
            if (this.tabsMenuList.every(item => item.path !== tabItem.path)) {
                this.tabsMenuList.push(tabInfo);
            }
            this.setTabsMenuValue(tabItem.path);
        },
        // Remove Tabs
        async removeTabs(tabPath: string) {
            let tabsMenuValue = this.tabsMenuValue;
            const tabsMenuList = this.tabsMenuList;
            if (tabsMenuValue === tabPath) {
                tabsMenuList.forEach((item, index) => {
                    if (item.path !== tabPath) return;
                    const nextTab = tabsMenuList[index + 1] || tabsMenuList[index - 1];
                    if (!nextTab) return;
                    tabsMenuValue = nextTab.path;
                    router.push(nextTab.path);
                });
            }
            this.tabsMenuValue = tabsMenuValue;
            this.tabsMenuList = tabsMenuList.filter(item => item.path !== tabPath);
        },
        // Change Tabs
        async changeTabs(tabItem: TabPaneProps) {
            this.tabsMenuList.forEach(item => {
                if (item.title === tabItem.label) router.push(item.path);
            });
        },
        // Set TabsMenuValue
        async setTabsMenuValue(tabsMenuValue: string) {
            this.tabsMenuValue = tabsMenuValue;
        },
        // Set TabsMenuList
        async setTabsMenuList(tabsMenuList: TabsOptions[]) {
            this.tabsMenuList = tabsMenuList;
        },
        // Close MultipleTab
        async closeMultipleTab(tabsMenuValue?: string) {
            this.tabsMenuList = this.tabsMenuList.filter(item => {
                return item.path === tabsMenuValue || item.path === HOME_URL;
            });
        },
        // Go Home
        async goHome() {
            router.push(HOME_URL);
            this.tabsMenuValue = HOME_URL;
        }
    },
    persist: piniaPersistConfig("TabsState")
});

最终效果图:

46972a97567f3ff7c445d12b442ef7b1.png

gitHub地址:

vue3学习完成的后管模板

阅读全文

日期小组件的封装

组件 2022/10/12

一、日期小组件

公司有一个微信小程序项目用的是uni-app+uview2,uview2上的那个日期组件并不是产品想要样子,那就只好自己动手查阅资料了,看看别人的日历组件怎么搞的,再根据这些,写写样式,改改逻辑,完成产品需要的喽。不和你多bb,直接上效果图和代码,主要也是自己做个笔记,指不定哪天又用上,到时候c+v就行了,哈哈!

1.效果图

d8a086257743cc6dee6907df3bcfc63.png

2.代码

我一般是这样建文件:components=>term-picker=>term-picker.vue

<template>
    <view :class="{'pickerMask':visable}" @click="maskClick" @touchmove.stop.prevent="returnHandle">
        <view class="picker-box" :class="{'picker-show':visable}">
      <view class="selectDate">
        <view class="selectDateTxt">
          <text>选择日期</text>
        </view>
      </view>
      <view class="lineshow">
        <view class="line"></view>
      </view>
            <view class="operate-box" @touchmove.stop.prevent="returnHandle" @tap.stop="returnHandle">

                <view class="time-box">
                    <view @click="touchSelect(0)" class="time-item" :style="{color:touchIndex?'#000000':themeColor}">
                        <text>{{startText}}</text>
                        <text>{{resultDate[0]}}</text>
                    </view>
                    <text>至</text>
                    <view @click="touchSelect(1)" class="time-item" :style="{color:touchIndex?themeColor:'#000000'}">
                        <text>{{endText}}</text>
                        <text>{{resultDate[1]}}</text>
                    </view>
                </view>

            </view>
      <view class="lineshow">
        <view class="line"></view>
      </view>
            <picker-view :value="pickerValue" @change="pickerChange" class="picker-view" :indicator-style="indicatorStyle" @tap.stop="returnHandle">
        <picker-view-column style="flex: 0 0 30.5%">
                    <view class="picker-item" v-for="(item, index) in years" :key="index">{{item}}年</view>
                </picker-view-column>
                <picker-view-column style="flex: 0 0 30.5%">
                    <view class="picker-item" v-for="(item, index) in months" :key="index">{{ item }}月</view>
                </picker-view-column>
                <picker-view-column v-if="days.length > 0" style="flex: 0 0 30.5%">
                    <view class="picker-item" v-for="(item, index) in days" :key="index">{{ item }}日</view>
                </picker-view-column >
            </picker-view>
      <view class="button">
        <view  class="buttonClose" @click="pickerClose">
          <view class="buttonClosetxt">
            取消
          </view>

        </view>
        <view  class="buttonConfirm" @click="pickerConfirm">
          <view class="buttonConfirmtxt">
            确认
          </view>
        </view>
      </view>
        </view>
    </view>
</template>
<script>
export default {
  name: 'termPicker',
  props: {
    visable: {
      type: Boolean,
      default: false
    },
    defaultDate: {
      type: Array,
      default: () => []
    },
    minYear: {
      type: Number,
      default: 2020,
    },
    timeLimit: {
      type: Boolean,
      default: false
    },
    deferYear: {
      type: Number,
      default: 0,
    },
    themeColor: {
      type: String,
      default: '#10BE9D'
    },
    startText: {
      type: String,
      default: '开始时间'
    },
    endText: {
      type: String,
      default: '结束时间'
    }
  },
  data() {
    const date = new Date()
    const years = []
    const year = date.getFullYear()
    const months = []
    const month = date.getMonth() + 1
    const day = date.getDate()
    const maxYear = this.timeLimit ? year : year + this.deferYear
    for (let i = this.minYear;i <= maxYear;i++) {
      years.push(i)
    }
    for (let i = 1;i <= 12;i++) {
      months.push(i)
    }
    return {
      indicatorStyle: 'height: 100rpx;',
      touchIndex: 0,
      year,
      month,
      day,
      years,
      months,
      days: [],
      pickerValue: [],
      resultDate: []
    }
  },
  mounted() {
    this.setDate()
  },
  methods: {
    returnHandle() {},
    setDate() {
      if (this.defaultDate.length) {
        if (this.defaultDate.length > 0) {
          const date = this.defaultDate[0]
          this.resultDate = this.defaultDate
          this.setPicker(date)
        } else {
          const month = this.month < 10 ? '0' + this.month : this.month
          const day = this.day < 10 ? '0' + this.day : this.day
          const nowTime = this.year + '-' + month + '-' + day
          this.resultDate = [nowTime, nowTime]
          this.setPicker(nowTime)
        }
      }
    },
    setPicker(date) {
      if (date) {
        const splitVal = date.split('-')
        const year = this.years.indexOf(Number(splitVal[0]))
        const month = Number(splitVal[1]) - 1
        const day = Number(splitVal[2]) - 1
        this.pickerChange({
          detail: {
            value: [year, month, day]
          }
        })
      }
    },
    touchSelect(val) {
      const date = this.resultDate[val]
      this.touchIndex = val
      this.setPicker(date)
    },
    getDateTime(date) {
      const year = this.years[date[0]]
      let month = this.months[date[1]]
      let day = this.days[date[2]]
      if (month < 10) {
        month = '0' + month
      }
      if (day < 10) {
        day = '0' + day
      }
      this.resultDate[this.touchIndex] = year + '-' + month + '-' + day
    },
    pickerChange(e) {
      const currents = e.detail.value
      if (currents[1] + 1 === 2) {
        this.days = []
        if (
          ((currents[0] + this.minYear) % 4 === 0 &&
            (currents[0] + this.minYear) % 100 !== 0) ||
          (currents[0] + this.minYear) % 400 === 0
        ) {
          for (let i = 1;i < 30;i++) {
            this.days.push(i)
          }
        } else {
          for (let i = 1;i < 29;i++) {
            this.days.push(i)
          }
        }
      } else if ([4, 6, 9, 11].some((item) => currents[1] + 1 === item)) {
        this.days = []
        for (let i = 1;i < 31;i++) {
          this.days.push(i)
        }
      } else if (
        [1, 3, 5, 7, 8, 10, 12].some((item) => currents[1] + 1 === item)
      ) {
        this.days = []
        for (let i = 1;i < 32;i++) {
          this.days.push(i)
        }
      }
      this.pickerValue = currents
      this.getDateTime(currents)
    },
    maskClick() {
      this.$emit('update:visable', false)
    },
    pickerClose() {
      console.log('我是取消')
      this.maskClick()
    },
    pickerConfirm() {
      const { resultDate, timeLimit } = this
      const startTime = new Date(resultDate[0]).getTime()
      const endTime = new Date(resultDate[1]).getTime()
      const nowTime = timeLimit ? new Date().getTime() : endTime
      if (startTime <= endTime) {
        if (endTime <= nowTime) {
          this.$emit('confirm', resultDate)
          this.maskClick()
        } else {
          uni.showToast({
            title: '时间不能超过今天',
            icon: 'none'
          })
        }
      } else {
        uni.showToast({
          title: '时间范围不正确!',
          icon: 'none'
        })
      }
    }
  }
}
</script>

<style lang="scss" scoped>
.pickerMask {
  position: fixed;
  z-index: 998;
  top: 0;
  right: 0;
  left: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.6);
}

.picker-box {
  position: fixed;
  bottom: 0;
  left: 0;
  width: 100%;
  transition: all 0.3s ease;
  transform: translateY(100%);
  z-index: 998;

  .selectDate {
    width: 100%;
    height: 60rpx;
    padding: 30rpx 30rpx;
    background-color: #fff;
    display: flex;
    align-items: center;

    .selectDateTxt {
      width: 112rpx;
      height: 40rpx;
      font-size: 28rpx;
      font-family: 'PingFangSC-Medium', 'PingFang SC';
      font-weight: 500;
      color: #212034;
      line-height: 40rpx;
    }
  }

  .lineshow {
    display: flex;
    justify-content: center;
    background-color: #fff;

    .line {
      width: 690rpx;
      border: 1rpx solid #f1eff3;
    }
  }

  .operate-box {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 18rpx 30rpx;
    background-color: #fff;
    text-align: center;
    font-size: 30rpx;

    .time-box {
      width: 100%;
      display: flex;
      align-items: center;
      justify-content: space-around;

      .time-item {
        display: flex;
        flex-direction: column;
      }
    }
  }

  .button {
    width: 100%;
    background-color: #fff;
    height: 157rpx;
    display: flex;
    align-items: center;
    justify-content: center;

    .buttonClose {
      width: 330rpx;
      height: 80rpx;
      border-radius: 40rpx;
      border: 1rpx solid #5368ef;
      transform: rotateZ(360deg);
      display: flex;
      align-items: center;
      justify-content: center;

      .buttonClosetxt {
        width: 60rpx;
        height: 42rpx;
        font-size: 30rpx;
        font-family: 'PingFangSC-Medium', 'PingFang SC';
        font-weight: 400;
        color: #5368ef;
        line-height: 42rpx;
      }
    }

    .buttonConfirm {
      width: 330rpx;
      height: 80rpx;
      background: linear-gradient(159deg, #7486fc 0%, #5368ef 100%);
      border-radius: 35px;
      margin-left: 30rpx;
      display: flex;
      align-items: center;
      justify-content: center;

      .buttonConfirmtxt {
        width: 60rpx;
        height: 42rpx;
        font-size: 30rpx;
        font-family: 'PingFangSC-Medium', 'PingFang SC';
        font-weight: 400;
        color: #fff;
        line-height: 42rpx;
      }
    }
  }
}

.picker-show {
  transform: translateY(0);
}

.picker-view {
  width: 750rpx;
  height: 450rpx;
  background-color: #fff;
  padding-left: 5.5%;

  .picker-item {
    height: 42rpx;
    font-family: 'PingFangSC-Medium', 'PingFang SC';
    font-weight: 400;
    font-size: 30rpx;
    color: #20253b ;
    line-height: 42rpx;
    display: flex;
    align-items: center;
    justify-content: center;
    text-align: center;
  }
}
</style>

3.使用

在需要这个用到的组件那块导入

import termPicker from ‘@/components/term-picker/term-picker.vue’

<!-- 日期选择 -->
  <term-picker
    visable.sync="showDateSelect"
    :defaultDate="defaultDate"
    :minYear="1990"
    :timeLimit="true"
    :deferYear="0"
    themeColor="#5368ef"
    @confirm="confirm">
  </term-picker>
 

Props 参数说明

参数名 类型 默认值 说明
visable Boolean false 日期选择控件的显示/隐藏,注意使用.sync修饰符(true: 显示; false: 隐藏)
defaultDate Array [] 默认预设值(比如:['2021-06-01', '2021-07-01'])
minYear Number 1990 最小年份
timeLimit Boolean false 日期区间限制(true: 限制结束日期<=当前日期; false: 不限制)
deferYear Number 0 年份选项顺延数,仅当timeLimit为false时生效(举个栗子: deferYear = 1, 则年份选项的最大值为: 当前年份 + deferYear)
themeColor String #10BE9D 选择框操作栏部分字体颜色
startText String 开始时间 日期范围起始处文本
endText String 结束时间 日期范围结束处文本

Events 事件说明

事件名 说明 返回值
@confirm 点击确定按钮,返回当前选择的值 Array数组类型
阅读全文

从零开始搭建博客(五)——使用valine评论系统

博客搭建 2022/10/11

一、hexo框架的主题使用valine

1. 注册 Leancloud 账号

在leancloud官网注册一个账号,海外同学推荐使用leancloud国际版,国内同学可以使用华东或华北节点。

2.创建应用获取appid和appkey

注册完成后进入控制台->创建应用->创建开发版应用。创建完开发版应用如下:点击最右边小齿轮也就是设置,找到应用凭证,获取appid和appkey

img

3.配置valine

最后去自己主题下的_config.yml文件下(注意不是根目录下的,是你克隆的主题的_config.yml),配置一下valine,目前hexo官网给的许多主题都是可以用valine的,直接配置就行

以ayer主题为例,具体配置含义如下:

# 启用Valine必须先创建leancloud应用, 获取 id|key 填入即可
leancloud:
  enable: true
  app_id:# 将应用key的App ID设置在这里
  appkey: # 将应用key的App Key设置在这里
valine:
  enable: true #是否启用valine
  notify: false# 邮箱通知 , https://github.com/xCss/Valine/wiki,默认为false
  verify: false# 验证码 默认为false
  placeholder: Just go go ^_^ # 初始化评论显示,根据自己修改,这里默认,
  avatar: monsterid # 头像风格,默认为mm,可进入网址:https://valine.js.org/visitor.html查看头像设置,这里有许多头像风格,进行设置
  guest_info: nick,mail,link # 自定义评论标题
  pageSize: 10 # 分页大小,10页就自动分页
  visitor: true # 是否允许游客评论 ,进入官网查看设置:https://valine.js.org/visitor.html

二、valine评论数据管理

去LeanCloud的控制台找到数据存储=>结构化数据=>Comment,在这里你可以对评论的数据进行管理

1673512632892.png

阅读全文

vite 搭建vue3项目(一)

项目搭建 2022/10/8

一、创建项目

1.直接创建项目

使用 NPM:
$ npm create vite@latest
使用 Yarn:
$ yarn create vite
使用 PNPM:
$ pnpm create vite
然后按照提示操作即可!(选择vue,vue-ts)

2.使用模板创建项目

通过附加的命令行选项直接指定项目名称和你想要使用的模板例如,要构建一个 Vite + Vue 项目,运行:

使用 npm 6.x:
npm create vite@latest my-vue-app --template vue
使用 npm 7+, extra double-dash is needed:
npm create vite@latest my-vue-app -- --template vue
使用 yarn:
yarn create vite my-vue-app --template vue
使用 pnpm:
pnpm create vite my-vue-app --template vue

然后npm i 或者 yarn install 运行yarn dev 看看浏览器运行成了没,第一步就大功告成了

二、vite配置别名和环境变量的配置

1.配置别名

使用编辑器VScode打开刚刚搭建好的项目 进入配置文件 vite.config.ts

配置别名后的vite.config.ts:

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
const resolve = (dir: string) => path.join(__dirname, dir)

export default defineConfig({
    plugins: [vue()],
    resolve: {
        alias: {
            '@': resolve('src'),
        }
    }
})

此时 TS 可能有这个错误提示:找不到模块“path”或其相应的类型声明

解决方法:

npm install @types/node --save-dev或者yarn add @types/node --save-dev

还需要在tsconfig.json的paths配置

 "baseUrl": ".",
    "paths": {
      "@/*": [
        "src/*"
      ],
      "comps/*": [
        "src/components/*"
      ],
      "views/*": [
        "src/views/*"
      ],
      "store/*": [
        "src/store/*"
      ]
    },

2.环境变量的配置

vite 提供了两种模式:具有开发服务器的开发模式(development)和生产模式(production)

项目根目录新建:.env.development :

NODE_ENV=development

VITE_APP_WEB_URL= 'YOUR WEB URL'

项目根目录新建:.env.production :

NODE_ENV=production

VITE_APP_WEB_URL= 'YOUR WEB URL'

组件中使用:

console.log(import.meta.env.VITE_APP_WEB_URL)

配置 package.json:

打包区分开发环境和生产环境

"build:dev": "vite build --mode development",
"build:pro": "vite build --mode production",

三、配置跨域代理

在vite.config.ts中

export default defineConfig({
    plugins: [vue()],
    resolve: {
        alias: {
            '@': resolve('src'),
            comps: resolve('src/components'),
            apis: resolve('src/apis'),
            views: resolve('src/views'),
            utils: resolve('src/utils'),
            routes: resolve('src/routes'),
            styles: resolve('src/styles')
        }
    },
    server: {
     // 配置前端服务地址和端口
        //服务器主机名
        host: '',
        //端口号
        port: 3088,
        //设为 true 时若端口已被占用则会直接退出,而不是尝试下一个可用端口
        strictPort: false,
        //服务器启动时自动在浏览器中打开应用程序,当此值为字符串时,会被用作 URL 的路径名
        open: false,
        //自定义代理规则
        proxy: {
            // 选项写法
            '/api': {
                target: '',
                changeOrigin: true,
                rewrite: path => path.replace(/^\/api/, '')
            }
        }
    }
})

使用跨域代理:

​ 用代理, 首先你得有一个标识, 告诉他你这个连接要用代理. 不然的话, 可能你的 html, css, js这些静态资源都跑去代理. 所以我们一般只有接口用代理, 静态文件用本地.‘/api’: {}, 就是告诉node, 我接口只有是’/api’开头的才用代理.所以你的接口就要这么写 /api/xx/xx. 最后代理的路径就是 http://xxx.xx.com/api/xx/xx.可是不对啊, 我正确的接口路径里面没有/api啊. 所以就需要 pathRewrite,把’/api’去掉, 这样既能有正确标识, 又能在请求接口的时候去掉api.

四、添加 css 预处理器 sass

安装 :

npm install -D sass sass-loader
或者yarn add sass sass-loader

在 src/assets 下新增 style 文件夹,用于存放全局样式文件

五、约束代码风格

TypeScirpt 官方决定全面采用 ESLint 作为代码检查的工具,并创建了一个新项目 typescript-eslint,提供了 TypeScript 文件的解析器 @typescript-eslint/parser 和相关的配置选项 @typescript-eslint/eslint-plugin 等

1.Eslint支持

# eslint 安装
yarn add eslint --dev
# eslint 插件安装
yarn add eslint-plugin-vue --dev

yarn add @typescript-eslint/eslint-plugin --dev

yarn add eslint-plugin-prettier --dev

# typescript parser
yarn add @typescript-eslint/parser --dev


直接:npm i typescript eslint eslint-plugin-vue @typescript-eslint/parser @typescript-eslint/eslint-plugin -D

注意: 如果 eslint 安装报错:

可以尝试运行以下命令:

yarn config set ignore-engines true

项目下新建 .eslintrc.js配置 eslint 校验规则:

// 需要安装依赖:  npm i eslint-define-config
const { defineConfig } = require('eslint-define-config')
module.exports = defineConfig({
    root: true,
    /* 指定如何解析语法。*/
    parser: 'vue-eslint-parser',
    /* 优先级低于parse的语法解析配置 */
    parserOptions: {
        parser: '@typescript-eslint/parser',
    },
    // https://eslint.bootcss.com/docs/user-guide/configuring#specifying-globals
    globals: {
        Nullable: true,
    },
    extends: [
        // add more generic rulesets here, such as:
        // 'eslint:recommended',
        // 'plugin:vue/vue3-recommended',
        // 'plugin:vue/recommended' // Use this if you are using Vue.js 2.x.

        'plugin:vue/vue3-recommended',
        // 此条内容开启会导致 全局定义的 ts 类型报  no-undef 错误,因为
        // https://cn.eslint.org/docs/rules/
        'eslint:recommended',
        'plugin:@typescript-eslint/recommended', // typescript-eslint推荐规则,
        'prettier',
        'plugin:prettier/recommended',
    ],
    rules: {
        // 'no-undef': 'off',
        // 禁止使用 var
        'no-var': 'error',
        semi: 'off',
        // 优先使用 interface 而不是 type
        '@typescript-eslint/consistent-type-definitions': ['error', 'interface'],
        '@typescript-eslint/no-explicit-any': 'off',
        '@typescript-eslint/explicit-module-boundary-types': 'off',
        '@typescript-eslint/ban-types': 'off',
        '@typescript-eslint/no-unused-vars': 'off',
        'vue/html-indent': [
            'error',
            4,
            {
                attribute: 1,
                baseIndent: 1,
                closeBracket: 0,
                alignAttributesVertically: true,
                ignores: [],
            },
        ],
        // 关闭此规则 使用 prettier 的格式化规则, 感觉prettier 更加合理,
        // 而且一起使用会有冲突
        'vue/max-attributes-per-line': ['off'],
        // 强制使用驼峰命名
        'vue/component-name-in-template-casing': [
            'error',
            'PascalCase',
            {
                registeredComponentsOnly: false,
                ignores: [],
            },
        ],
    },
})

项目下新建 .eslintignore

# eslint 忽略检查 (根据项目需要自行添加)
node_modules
dist

2.prettier支持

# 安装 prettier
yarn add prettier --dev

解决 eslint 和 prettier 冲突

解决 ESLint 中的样式规范和 prettier 中样式规范的冲突,以 prettier 的样式规范为准,使 ESLint 中的样式规范自动失效

# 安装插件 eslint-config-prettier
yarn add eslint-config-prettier --dev

项目下新建 .prettier.js

配置 prettier 格式化规则:

module.exports = {
  tabWidth: 2,
  jsxSingleQuote: true,
  jsxBracketSameLine: true,
  printWidth: 100,
  singleQuote: true,
  semi: false,
  overrides: [
    {
      files: '*.json',
      options: {
        printWidth: 200,
      },
    },
  ],
  arrowParens: 'always',
}

项目下新建 .prettierignore

# 忽略格式化文件 (根据项目需要自行添加)
node_modules
dist

package.json 配置:

{
  "script": {
    "lint": "eslint src --fix --ext .ts,.tsx,.vue,.js,.jsx",
    "prettier": "prettier --write ."
  }
}

上面配置完成后,可以运行以下命令测试下代码检查个格式化效果:

# eslint 检查
yarn lint
# prettier 自动格式化
yarn prettier

六、安装路由

# 安装路由
yarn add vue-router@4

在 src 文件下新增 router 文件夹 => index.ts 文件,内容如下:

import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'

const routes: RouteRecordRaw[] = [
  {
    path: '/',
    name: 'Login',
    component: () => import('@/pages/login/Login.vue'), // 注意这里要带上 文件后缀.vue
  },
]

const router = createRouter({
  history: createWebHistory(),
  routes,
})

export default router

修改入口文件 mian.ts :

import { createApp } from 'vue'
import App from './App.vue'
import router from './router/index'

const app = createApp(App)
app.use(router)
app.mount('#app')

七、axios统一请求封装

# 安装 axios
yarn add axios
# 安装 nprogress 用于请求 loading
# 也可以根据项目需求自定义其它 loading
yarn add nprogress
# 类型声明,或者添加一个包含 `declare module 'nprogress'
yarn add @types/nprogress --dev

新增 service 文件夹,service 下新增 http.ts 文件以及 moudles 文件夹(存放各模块接口)和interface(公共ts)文件夹:

http.ts : 用于axios封装**

//http.ts
import axios, { AxiosRequestConfig } from 'axios'
import NProgress from 'nprogress'

// 设置请求头和请求路径
axios.defaults.baseURL = '/api'
axios.defaults.timeout = 10000
axios.defaults.headers.post['Content-Type'] = 'application/json;charset=UTF-8'
axios.interceptors.request.use(
  (config): AxiosRequestConfig<any> => {
    const token = window.sessionStorage.getItem('token')
    if (token) {
      //@ts-ignore
      config.headers.token = token
    }
    return config
  },
  (error) => {
    return error
  }
)
// 响应拦截
axios.interceptors.response.use((res) => {
  if (res.data.code === 111) {
    sessionStorage.setItem('token', '')
    // token过期操作
  }
  return res
})

interface ResType<T> {
  code: number
  data?: T
  msg: string
  err?: string
}
interface Http {
  get<T>(url: string, params?: unknown): Promise<ResType<T>>
  post<T>(url: string, params?: unknown): Promise<ResType<T>>
  upload<T>(url: string, params: unknown): Promise<ResType<T>>
  download(url: string): void
}

const http: Http = {
  get(url, params) {
    return new Promise((resolve, reject) => {
      NProgress.start()
      axios
        .get(url, { params })
        .then((res) => {
          NProgress.done()
          resolve(res.data)
        })
        .catch((err) => {
          NProgress.done()
          reject(err.data)
        })
    })
  },
  post(url, params) {
    return new Promise((resolve, reject) => {
      NProgress.start()
      axios
        .post(url, JSON.stringify(params))
        .then((res) => {
          NProgress.done()
          resolve(res.data)
        })
        .catch((err) => {
          NProgress.done()
          reject(err.data)
        })
    })
  },
  upload(url, file) {
    return new Promise((resolve, reject) => {
      NProgress.start()
      axios
        .post(url, file, {
          headers: { 'Content-Type': 'multipart/form-data' },
        })
        .then((res) => {
          NProgress.done()
          resolve(res.data)
        })
        .catch((err) => {
          NProgress.done()
          reject(err.data)
        })
    })
  },
  download(url) {
    const iframe = document.createElement('iframe')
    iframe.style.display = 'none'
    iframe.src = url
    iframe.onload = function () {
      document.body.removeChild(iframe)
    }
    document.body.appendChild(iframe)
  },
}
export default http

例如:moudles 下新增login文件夹,用于存放登录模块的请求接口,login 文件夹下分别新增 login.ts types.ts :

import http from '@/service/http'
import * as T from './types'

const loginApi: T.ILoginApi = {
    login(params){
        return http.post('/login', params)
    }

}
export default loginApi

types.ts:

export interface ILoginParams {
    userName: string
    passWord: string | number
}
export interface ILoginApi {
    login: (params: ILoginParams)=> Promise<any>
}

八、状态管理 pinia

# 安装
yarn add pinia@next

在 src 文件夹下新增 store 文件夹,接在在 store 中新增 main.ts

main.ts 中增加

# 引入
import { createPinia } from "pinia"
# 创建根存储库并将其传递给应用程序
app.use(createPinia())

九、添加element-plus

#安装 element-plus 
yarn add element-plus

1.element-plus按需引入

需要用到两个插件unplugin-vue-components、unplugin-auto-import这两个插件。

npm i unplugin-vue-components unplugin-auto-import -D

另外这里要注意的是,由于使用了 unplugin-vue-components unplugin-auto-import 这两个插件,按需加载其实是不需要 import 组件,但如果使用Api创建组件,例如elmesage,elnotification这些,可以看到不 import 的话会提示错误,如果 import 又会导致样式的丢失,需要下载一个插件

yarn add unplugin-element-plus -D
#或者 
npm i unplugin-element-plus -D 

配置vite.config.js

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import path from "path";
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import ElementPlus from 'unplugin-element-plus/vite'

const resolve = (dir: string) => path.join(__dirname, dir);

export default defineConfig({
  plugins: [
    vue(),
    AutoImport({
      resolvers: [ElementPlusResolver()],
    }),
    Components({
      resolvers: [ElementPlusResolver()],
    }),
    ElementPlus()
  ],
  //配置别名
  resolve: {
    alias: {
      "@": resolve("src"),
      comps: resolve("src/components"),
      service: resolve("src/service"),
      views: resolve("src/views"),
      route: resolve("src/route"),
    },
  },
 // css:{
    //preprocessorOptions:{
      //scss:{
      //  additionalData:'@import "@/assets/style/main.scss";'
     // }
    //}
 // },
  //配置跨域代理
  server: {
    // 配置前端服务地址和端口
    //服务器主机名
    host: "127.0.0.1",
    //端口号
    port: 3088,
    //设为 true 时若端口已被占用则会直接退出,而不是尝试下一个可用端口
    strictPort: false,
    //服务器启动时自动在浏览器中打开应用程序,当此值为字符串时,会被用作 URL 的路径名
    open: true,
    //自定义代理规则
    proxy: {
      "/api": {
        target: "http://localhost:3000", //要代理的本地api地址,也可以换成线上测试地址
        changeOrigin: true, //跨域
        rewrite: (path) => path.replace(/^\/api/, ""),
      },
    },
  },
});

2.添加element-plus图标

# NPM
$ npm install @element-plus/icons-vue
# Yarn
$ yarn add @element-plus/icons-vue

然后在main.ts中全局注册并使用

import * as ElementPlusIconsVue from '@element-plus/icons-vue'
Object.keys(ElementPlusIconsVue).forEach(key => {
    app.component(key, ElementPlusIconsVue[key as keyof typeof ElementPlusIconsVue]);
});

十、svg图标插件使用

1.安装svg图标插件

#安装插件vue-svg-icon
npm install vue-svg-icon --save-dev

2. 注册全局组件svgIcon

在main.ts中全局注册并使用

import { createApp } from 'vue'
import App from './App.vue'
import router from './router/index'
import { createPinia } from "pinia"
import SvgIcon from './components/SvgIcon/index.vue'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import "element-plus/dist/index.css"
//样式
import './styles/index.scss'

const app = createApp(App)
// 注册element Icons组件
Object.keys(ElementPlusIconsVue).forEach(key => {
    app.component(key, ElementPlusIconsVue[key as keyof typeof ElementPlusIconsVue]);
});
app.use(router).use(createPinia()).component('svg-icon', SvgIcon).mount('#app')

3.下载存放svg图标

在src下新建assets=>icons=>svg文件夹,用来存放svg图标,所有下载的SVG图标放入其中

image-20221008180639420

4. 使用svg图标

以bug.svg图标为例,修改scale的值调整图标的大小。

<svgIcon name="del" :scale="1" />

十一、untils文件夹

在根目录下新建一个untils文件夹,这个文件夹下的内容主要是导出常用的一些公共方法等等:

image-20221008171843967

untils=>util.ts

//常用的工具方法
import { isArray } from "@/utils/is";

/**
 * @description 获取localStorage
 * @param {String} key Storage名称
 * @return string
 */
 export function localGet(key: string) {
    const value = window.localStorage.getItem(key);
    try {
        return JSON.parse(window.localStorage.getItem(key) as string);
    } catch (error) {
        return value;
    }
}

/**
 * @description 存储localStorage
 * @param {String} key Storage名称
 * @param {Any} value Storage值
 * @return void
 */
export function localSet(key: string, value: any) {
    window.localStorage.setItem(key, JSON.stringify(value));
}

/**
 * @description 清除localStorage
 * @param {String} key Storage名称
 * @return void
 */
export function localRemove(key: string) {
    window.localStorage.removeItem(key);
}

/**
 * @description 清除所有localStorage
 * @return void
 */
export function localClear() {
    window.localStorage.clear();
}

/**
 * @description 对象数组深克隆
 * @param {Object} obj 源对象
 * @return object
 */
export function deepCopy<T>(obj: any): T {
    let newObj: any;
    try {
        newObj = obj.push ? [] : {};
    } catch (error) {
        newObj = {};
    }
    for (let attr in obj) {
        if (typeof obj[attr] === "object") {
            newObj[attr] = deepCopy(obj[attr]);
        } else {
            newObj[attr] = obj[attr];
        }
    }
    return newObj;
}

/**
 * @description 判断数据类型
 * @param {Any} val 需要判断类型的数据
 * @return string
 */
export function isType(val: any) {
    if (val === null) return "null";
    if (typeof val !== "object") return typeof val;
    else return Object.prototype.toString.call(val).slice(8, -1).toLocaleLowerCase();
}

/**
 * @description 生成随机数
 * @param {Number} min 最小值
 * @param {Number} max 最大值
 * @return number
 */
export function randomNum(min: number, max: number): number {
    let num = Math.floor(Math.random() * (min - max) + max);
    return num;
}
/**
 * @description 递归查询当前路由所对应的路由
 * @param {Array} menuList 菜单列表
 * @param {String} path 当前地址
 * @return array
 */
export function getTabPane<T, U>(menuList: any[], path: U): T {
    let result: any;
    for (let item of menuList || []) {
        if (item.path === path) result = item;
        const res = getTabPane(item.children, path);
        if (res) result = res;
    }
    return result;
}

/**
 * @description 使用递归处理路由菜单,生成一维数组
 * @param {Array} menuList 所有菜单列表
 * @param {Array} newArr 菜单的一维数组
 * @return array
 */
export function handleRouter(routerList: Menu.MenuOptions[], newArr: string[] = []) {
    routerList.forEach((item: Menu.MenuOptions) => {
        typeof item === "object" && item.path && newArr.push(item.path);
        item.children && item.children.length && handleRouter(item.children, newArr);
    });
    return newArr;
}

/**
 * @description 扁平化数组对象
 * @param {Array} arr 数组对象
 * @return array
 */
export function getFlatArr(arr: any) {
    return arr.reduce((pre: any, current: any) => {
        let flatArr = [...pre, current];
        if (current.children) flatArr = [...flatArr, ...getFlatArr(current.children)];
        return flatArr;
    }, []);
}

/**
 * @description 格式化表格单元格默认值
 * @param {Number} row 行
 * @param {Number} col 列
 * @param {String} callValue 当前单元格值
 * @return string
 * */
export function defaultFormat(row: number, col: number, callValue: any) {
    // 如果当前值为数组,使用 / 拼接(根据需求自定义)
    if (isArray(callValue)) return callValue.length ? callValue.join(" / ") : "--";
    return callValue ?? "--";
}

/**
 * @description 处理无数据情况
 * @param {String} callValue 需要处理的值
 * @return string
 * */
export function formatValue(callValue: any) {
    // 如果当前值为数组,使用 / 拼接(根据需求自定义)
    if (isArray(callValue)) return callValue.length ? callValue.join(" / ") : "--";
    return callValue ?? "--";
}

/**
 * @description 根据枚举列表查询当需要的数据(如果指定了 label 和 value 的 key值,会自动识别格式化)
 * @param {String} callValue 当前单元格值
 * @param {Array} enumData 枚举列表
 * @param {String} type 过滤类型(目前只有 tag)
 * @return string
 * */
export function filterEnum(callValue: any, enumData: any, searchProps?: { [key: string]: any }, type?: string): string {
    const value = searchProps?.value ?? "value";
    const label = searchProps?.label ?? "label";
    let filterData: any = {};
    if (Array.isArray(enumData)) filterData = enumData.find((item: any) => item[value] === callValue);
    if (type == "tag") return filterData?.tagType ? filterData.tagType : "";
    return filterData ? filterData[label] : "--";
}

is文件夹下的index.ts 常用的判断方式,is=>index.ts

const toString = Object.prototype.toString;

/**
 * @description: 判断值是否未某个类型
 */
export function is(val: unknown, type: string) {
    return toString.call(val) === `[object ${type}]`;
}

/**
 * @description:  是否为函数
 */
export function isFunction<T = Function>(val: unknown): val is T {
    return is(val, "Function");
}

/**
 * @description: 是否已定义
 */
export const isDef = <T = unknown>(val?: T): val is T => {
    return typeof val !== "undefined";
};

export const isUnDef = <T = unknown>(val?: T): val is T => {
    return !isDef(val);
};
/**
 * @description: 是否为对象
 */
export const isObject = (val: any): val is Record<any, any> => {
    return val !== null && is(val, "Object");
};

/**
 * @description:  是否为时间
 */
export function isDate(val: unknown): val is Date {
    return is(val, "Date");
}

/**
 * @description:  是否为数值
 */
export function isNumber(val: unknown): val is number {
    return is(val, "Number");
}

/**
 * @description:  是否为AsyncFunction
 */
export function isAsyncFunction<T = any>(val: unknown): val is Promise<T> {
    return is(val, "AsyncFunction");
}

/**
 * @description:  是否为promise
 */
export function isPromise<T = any>(val: unknown): val is Promise<T> {
    return is(val, "Promise") && isObject(val) && isFunction(val.then) && isFunction(val.catch);
}

/**
 * @description:  是否为字符串
 */
export function isString(val: unknown): val is string {
    return is(val, "String");
}

/**
 * @description:  是否为boolean类型
 */
export function isBoolean(val: unknown): val is boolean {
    return is(val, "Boolean");
}

/**
 * @description:  是否为数组
 */
export function isArray(val: any): val is Array<any> {
    return val && Array.isArray(val);
}

/**
 * @description: 是否客户端
 */
export const isClient = () => {
    return typeof window !== "undefined";
};

/**
 * @description: 是否为浏览器
 */
export const isWindow = (val: any): val is Window => {
    return typeof window !== "undefined" && is(val, "Window");
};

export const isElement = (val: unknown): val is Element => {
    return isObject(val) && !!val.tagName;
};

export const isServer = typeof window === "undefined";

// 是否为图片节点
export function isImageDom(o: Element) {
    return o && ["IMAGE", "IMG"].includes(o.tagName);
}

export function isNull(val: unknown): val is null {
    return val === null;
}

export function isNullAndUnDef(val: unknown): val is null | undefined {
    return isUnDef(val) && isNull(val);
}

export function isNullOrUnDef(val: unknown): val is null | undefined {
    return isUnDef(val) || isNull(val);
}

十二、typings->global.d.ts放全局变量,命名空间

// * Menu
declare namespace Menu {
    interface MenuOptions {
        path: string;
        title: string;
        icon?: string;
        isLink?: string;
        close?: boolean;
        children?: MenuOptions[];
    }
}

declare type TabsOptions = Menu.MenuOptions & {};

// * Vite
declare type Recordable<T = any> = Record<string, T>;

declare interface ViteEnv {
    VITE_API_URL: string;
    VITE_PORT: number;
    VITE_OPEN: boolean;
    VITE_GLOB_APP_TITLE: string;
    VITE_DROP_CONSOLE: boolean;
    VITE_PROXY_URL: string;
    VITE_BUILD_GZIP: boolean;
    VITE_REPORT: boolean;
}

十三、vue3.0使用tsx语法

1.下载

yarn add @vitejs/plugin-vue-jsx -D

2.引入

在vite.config.ts中

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx';

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue(),vueJsx()]
})

3.tsconfig.json 配置文件

    "jsx": "preserve",
    "jsxFactory": "h",
    "jsxFragmentFactory": "Fragment",

参考文章:

Vite2 + Vue3 + TypeScript + Pinia 搭建一套企业级的开发脚手架

Vue3 + Ts + ElementPlus + Vite2 从零搭建后台管理系统

禾几元老哥的github源码

阅读全文

git基础

git 2022/9/30

一、git基础

重新下载git后

1.先配钥匙(钥匙的作用是把你电脑上面的git和github或gitab连接)

$ ssh-keygen -t rsa -C “your_email@youremail.com“ //注意啊孩子,双引号里面是你的邮箱(如果在公司你应该有自己的企业邮箱)。填你常用的就行。还有enter代表执行。

添加公钥到 Github(gitee等) 中

登录 github 账号,选中并打开 setting,选择 SSH and GPG keys,选择 New SSH key,在 Title 中填入题目,在 Key 中填入id_rsa.pub 文件中的公钥。

可用如下命令验证上述配置是否成功:

ssh -T [git@github.com](mailto:git@github.com) 

2.配置user.name user.email

# 配置用户名 
git config --global user.name "xxx"                       
# 配置邮件
git config --global user.email "[xxx@xxx.com](mailto:xxx@xxx.com)"              

3.建本地仓库

新建一个文件夹,用vscode打开该文件夹,git init,该命令执行完后会在当前目录生成一个 .git 目录

4.添加远程地址(将本地仓库与远程仓库关联起来)

git remote add origin [git@github.com](mailto:git@github.com)/你的github用户名/仓库名.git

5.新建一个txt文本,里面写点东西。我的命名为read

6.add和commit

 git add read.txt $ git commit -m "这是你commit的原因,写了方便你我他"

7.push推送到你的github

git push -u origin master

另外注意事项:

生成秘钥

在windows下查看[c盘->用户->用户名->.ssh]下是否有id_rsa、id_rsa.pub文件

,如果没有需要手动生成

打开git bash,在控制台中输入以下命令: $ ssh-keygen -t rsa -C “自己邮箱”。


创建分支流程

1.先切换到主分支,然后创建本地新分支

git branch  <BranchName>(master)

2.显示所有本地分支(创建完看看本地有没有该分支)

git branch

3.切换分支(既然都创建了,肯定要用这个分支,切换到该分支)

git checkout <BranchName>

4.建立远程分支

git push --set-upstream origin + 刚刚创建的分支名 

删除分支流程

1.切换分支(先切换到其他分支上)

git checkout  <BranchName2>

2.删除本地分支( 需要切换到别的分支上去删除) 命令行:

git branch -d  <BranchName>

3.删除远程分支(没建立远程分支就不要没必要) 命令行:

git push origin --delete  <BranchName>

Git 把master的内容更新到自己分支上

1.如果分支(将此分支暂命名为feature)有修改,则先提交修改的内容,如果没有,直接跳到下一步

git commit -m “这是你commit的原因,写了方便你我他”

2.切换到master分支下

git checkout master

3.将远程的master代码pull拉取到本地

git pull

4.切换到自己分支下

git checkout  <BranchName>

5.合并master到自己分支

git merge master

如果在合并的过程中,例如提示Test.java文件出现冲突了,手动修改一下这个文件,修改成自己想要内容;如果没有出现冲突,vs code上下载git history插件 ,可以对比冲突,保留自己想要的代码,没有就直接跳到7步
冲突解决
上方的Accept Current Change等四种选择按钮,根据情况选择保留Current Change或者Incoming Change;颜色提示很明显,<<<<<< HEAD以下绿色部分是current change,同理蓝色部分是incoming.
both是全部保留

6.添加修改的文件,将新添加的文件提交上去

git add Test.java

git commit -m “这是你commit的原因,写了方便你我他”

7.将本地分支的代码push到远程仓库对应的分支上

git push origin  

测试通过后可以上线了将自己分支代码合并到master

1.需要先在,Git 把master的内容更新到自己分支上(在上面)

2.切换到master主分支 git checkout master

3.git pull一下

4.合并到master

git merge (自己分支名字)

5.在vscode 源代码管理工具或者sourecetree上对比一下自己提交的代码

5.git push 上传推送代码

git stash的使用

应用场景:某一天你正在 feature 分支开发新需求,突然产品经理跑过来说线上有bug,必须马上修复。而此时你的功能开发到一半,于是你急忙想切到 master 分支,然后你就会看到以下报错:

因为当前有文件更改了,需要提交commit保持工作区干净才能切分支

使用git stash代码就被存起来了

当你修复完线上问题,切回 feature 分支,想恢复代码也只需要:git stash apply

相关的命令
# 保存当前未commit的代码
git stash

# 保存当前未commit的代码并添加备注
git stash save "备注的内容"

# 列出stash的所有记录
git stash list

# 删除stash的所有记录
git stash clear

# 应用最近一次的stash
git stash apply

# 应用最近一次的stash,随后删除该记录
git stash pop

# 删除最近的一次stash
git stash drop

git reset –soft的使用

应用场景1:有时候手滑不小心把不该提交的内容 commit 了,这时想改回来,只能再 commit 一次,又多一条“黑历史”。

# 恢复最近一次 commit
git reset --soft HEAD^

reset –soft 相当于后悔药,给你重新改过的机会。对于上面的场景,就可以再次修改重新提交,保持干净的 commit 记录

二、电脑同时配置github与公司内部使用的gitlab

下载git

生成对应的gitlab和github的公秘钥 ssh-keygen -t rsa -C “你的gitlab邮箱” -f ~/.ssh/id_rsa_gitlab ssh-keygen -t rsa -C “注册 github 账户的邮箱”

这样你会在.ssh文件夹(.ssh在哪具体百度去),找到四个文件

这四个文件分别对应gitlab和github的公私钥,分别将gitlab和github的公钥配置到github和你公司的gitlab的ssh上

在.ssh目录下创建一个config文件,写入以下内容

Host github.com
HostName github.com
User zhangyao
IdentityFile ~/.ssh/github_rsa 

Host hcgit.hengchang6.com
HostName gitlab.com
User zhangyao
IdentityFile ~/.ssh/id_rsa

测试连接是否成功

ssh -T git@gitlab
ssh -T git@github.com
# Hi XXX! You've successfully authenticated, but GitHub does not provide shell access.
# 出现上边这句,表示链接成功

参考文章:电脑同时配置github与公司内部使用的gitlab

本地已有项目如何上传到github上

不和你多bb了,自己看:本地已有项目如何上传到github上_前端学狗的博客-CSDN博客

git 冲突相关

#预防发生冲突的方法:在每次push前git pull一下,保证本地代码都是最新的。

#冲突解决方法:

第一种冲突:在Git push的时候提示冲突了。

这种解决方法可以使用 git stash

保存本地代码,然后拉取最新的远程分支代码

git fetch --all   //只是下载远程内容,不做任何合并  
git reset --hard origin/master    //把HEAD指向刚下载的最新版本
再使用git stash apply应用最近存储,重新push即可。
阅读全文

前端路由随笔

前端随笔 2022/9/29

一、什么是路由?

路由的概念起源于服务端,在以前前后端不分离的时候,由后端来控制路由,当接收到客户端发来的 HTTP 请求,就会根据所请求的相应 URL,来找到相应的映射函数,然后执行该函数,并将函数的返回值发送给客户端。对于最简单的静态资源服务器,可以认为,所有 URL 的映射函数就是一个文件读取操作。对于动态资源,映射函数可能是一个数据库读取操作,也可能是进行一些数据的处理等等。然后根据这些读取的数据,在服务器端就使用相应的模板来对页面进行渲染后,再返回渲染完毕的页面。它的好处与缺点非常明显,如下:

  • 好处:安全性好,SEO 好;

  • 缺点:加大服务器的压力,不利于用户体验,代码冗合不好维护;

也正是由于后端路由还存在着自己的不足,前端路由才有了自己的发展空间。对于前端路由来说,路由的映射函数通常是进行一些 DOM 的显示和隐藏操作。这样,当访问不同的路径的时候,会显示不同的页面组件。前端路由主要有以下两种实现方案:

  • Hash
  • History

    二、前端路由Hash模式

一个URL是由很多部分组成,包括协议、域名、路径、query、hash等,早期的前端路由的实现就是基于location.hash来实现的。其实现原理也很简单,location.hash的值就是URL中#后面的内容。例如:https://www.WDNMD.com#666,它的location.hash='#666'

Hash模式的特性:

  1. URL中hash值只是客户端的一种状态,也就是说当向服务器端发出请求时,hash部分不会被发送。

  2. hash值的改变,都会在浏览器的访问历史中增加一个记录。因此我们能通过浏览器的回退、前进按钮控制hash的切换。

  3. 我们可以使用hashchange事件来监听hash的变化。(a标签和对loaction.hash进行赋值等等)

    通过上面的了解,我们大概知道这些

    • hash模式所有的工作都是在前端完成的,不需要后端服务的配合

    • hash模式的实现方式就是通过监听URL中hash部分的变化,从而做出对应的渲染逻辑

    • hash模式下,URL中会带有#,看起来不太美观

      因此Hash模式还是有些小小的不足,History API也就出现了

三、前端路由History API模式

history路由模式的实现,是要归功于HTML5提供的一个history全局对象,可以将它理解为其中包含了关于我们访问网页(历史会话)的一些信息。window.history 属性指向 History 对象,它表示当前窗口的浏览历史。当发生改变时,只会改变页面的路径,不会刷新页面。 History 对象保存了当前窗口访问过的所有页面网址。通过 history.length 可以得出当前窗口一共访问过几个网址。 由于安全原因,浏览器不允许脚本读取这些地址,但是允许在地址之间导航。 浏览器工具栏的“前进”和“后退”按钮,其实就是对 History 对象进行操作。

同时它还暴露了一些有用的方法,比如:

  • window.history.go 可以跳转到浏览器会话历史中的指定的某一个记录页
  • window.history.forward 指向浏览器会话历史中的下一页,跟浏览器的前进按钮相同
  • window.history.back 返回浏览器会话历史中的上一页,跟浏览器的回退按钮功能相同
  • window.history.pushState 可以将给定的数据压入到浏览器会话历史栈中
  • window.history.replaceState 将当前的会话页面的url替换成指定的数据

而history路由的实现,主要就是依靠于pushState与replaceState实现的

参考文章:

什么是路由?

vue路由模式及 history 模式下服务端配置

阅读全文

从零开始搭建博客(四)——关于我页面搭建

博客搭建 2022/9/28

关于我页面搭建

相关链接

都是开始整关于自己页面了,肯定需要把与自己相关的展示出来,例如你的github,gitee,掘金,csdn等等。当然,这里建议把Hexo和你使用的相关主题的链接也展示一下,毕竟,使用了别人开源的东西,多少要表示一下,哈哈。下面我的具体操作:

在根目录下的source新建about文件夹,about=>index.md,写入如下图的内容,其中type: ‘friends’ 和layout: ‘friends’是ayer主题原作者封装好的友情链接布局及操作,当然在此之前需要在themes=>ayer=>_config.yml的侧边栏菜单配置menu下配置 关于我: /about

链接相关配置也是在themes=>ayer=>_config.yml下配置,找到friends_link,图片地址和github链接填自己的

# 友情链接
friends_link:
  DarknessZY:
    url: https://github.com/DarknessZY
    img: /images/github.png
  耀耀切克闹灬:
    url: https://juejin.cn/user/1781681116679854/columns
    img: /images/juejin.png
  Hexo官网:
    url: https://hexo.io
    img: /images/hexo.png
  Ayer主题: #网站名称
    #网站地址
    url: https://github.com/Shen-Yu/hexo-theme-ayer
    #网站图片(可忽略不写)
    img: /images/ayer.png

然后执行hexo命令,页面上就显示出来了:

1673512329931.png

个人介绍

这块不能想之前那样直接写markdown就行了,需要自己写页面了,例如我的就是:找到ayer主题下的原作者写的友情链接的封装,themes=>ayer=>layout=>_partial=>friends.ejs,当然我的页面随便画画的,自己看着舒服就行了,最下面两个img标签,放的是收款码,有钱的捧个钱场,具体代码和效果如下:

<section class="outer">
    <% if(theme.friends_link && theme.friends_link.length != 0){ %>
    <h1 class="page-type-title">关于俺!</h1>
     <!-- 打赏 -->
    <div>
        <p style="font-size: 25px;font-weight: bolder;color: #dee1e6;">秋梨膏!请我杯喝茶怎么样!</p> 
    </div>
    <div id="reword-out">
        <div id="reward-btn">
          <%= __('post.reward') %>
        </div>
    </div>
    <!-- 介绍 -->
    <div>
        <div>
            <p style="font-size: 25px;font-weight: bolder;color: #dee1e6;"> 一位正在北漂的菜鸡程序员</p> 
        </div>
        <div>
            <div>
                <span style="color: red;"> 姓名:张耀 &nbsp;&nbsp;&nbsp;</span>
            </div> 
            <hr/>
            <div>
                <span style="color: blue;"> QQ:2498652509&nbsp;&nbsp;&nbsp;</span>有志同道合的小伙伴们可以加我qq,验证消息填'博客'+'你的姓名',我就会通过!
            </div>
            <hr/>
            <div>
                <span style="color:greenyellow;">现居地:北京朝阳&nbsp;&nbsp;&nbsp;</span>  
            </div>
            <hr/>
            <div>
                <span style="color:orange;">家乡:湖北汉川&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>小县城一个,装满了故事,我有故事,你有酒吗?
            </div>
            <hr/>
        </div>  
    </div>
    <!-- 链接 -->
    <div>
        <p style="font-size: 25px;font-weight: bolder;color: #dee1e6;"> 点击进去看看吧!</p> 
    </div>
    <ul id="friends_link">
        <% for (var i in theme.friends_link){ %>
        <li class="friends_li">
            <a href="<%- url_for(theme.friends_link[i].url) %>" target="_blank" class="search-title"
                <% if (theme.friends_link[i].description){ %>title="<%= theme.friends_link[i].description %>" <% } %>>
                <% if (theme.friends_link[i].img){ %>
                <img src="<%- url_for(theme.friends_link[i].img) %>">
                <% } else { %>
                <i class="ri-user-fill"></i>
                <% } %>
                <%= i %>
            </a>
        </li>
        <% } %>
    </ul>
    <%}%>
</section>

具体页面如下:

1673512358047.png

阅读全文

前端兼容性问题

一、CSS样式兼容

1.border移动端兼容

box-shadow, border等不明原因被遮挡的问题;小于1px的边框在部分机型显示不全 或 完全渲染不出来的问题,这个是在工作中UI走查,查出来的,刚开始我还不知道为什么,还是经验太浅了,哈哈!

方案一 transform rotateZ(360deg)

此方法也能解决 box-shadow, border等不明原因被遮挡的问题

border: 0.5px solid #000;
border-radius: 0.5px;

// 解决ios边框显示一半的问题
transform: rotateZ(360deg);

方案二 和UI沟通,尽量避免使用0.5px的边,线(推荐)

0.5px的边线在移动端渲染还有一些未知的问题,建议尽量不要使用0.5px的边线

// iphone XS 把0.5px的dashed 虚线  渲染成了 border-bottom 0.5px dotted #C4C4C9
border-bottom: 0.5px dashed #C4C4C9;
阅读全文
1 ... 8 9
avatar
耀耀切克闹

欢迎来到耀耀切克闹的博客

不是因为是主角才成功,而是成功了才成为主角

想要的都拥有,得不到的都释怀