实用的 vue tags 创建缓存导航的过程实现


Posted in Vue.js onDecember 03, 2020

需求

是要做一个tag,当切换页面的时候保留状态。

效果图:

实用的 vue tags 创建缓存导航的过程实现

思路

既然涉及了router跳转,那我们就去查api 发现keep-alive,巧了就用它吧。这里我们用到了include属性,该属性接受一个数组,当组件的name名称包含在inclue里的时候就会触发keep-alive。

import { Vue, Component, Watch, Mixins } from 'vue-property-decorator';

// 此处省略n行代码

// 这是个计算属性。(至于为什么这么写 这里就不介绍了。)
get cachedViews():string[] {
 return this.$store.state.tagsView.cachedViews;
}

// 此处省略n行代码

<keep-alive :include="cachedViews">
 <router-view :key="key"></router-view>
</keep-alive>

那我们接下来就处理cachedViews变量就好了。

vuex实现

import { Route } from 'vue-router'; // 检测规则

interface TagsState{
 visitedViews: Route[];
 cachedViews: string[];
}

const state = (): TagsState => ({
 visitedViews: [], // 展示的菜单
 cachedViews: [], // 缓存菜单 用来activeed
});

const mutations = {
 ADD_VISITED_VIEW: (state: TagsState, view: Route) => {
 if (state.visitedViews.some((v: any) => v.path === view.path)) { return; }
 state.visitedViews.push(
  Object.assign({}, view, {
  title: view.meta.title || 'no-name',
  }),
 );
 },
 ADD_CACHED_VIEW: (state: TagsState, view: Route) => {
 if (state.cachedViews.includes(view.meta.name)) { return; }
 if (!view.meta.noCache) {
  state.cachedViews.push(view.meta.name);
 }
 },

 DEL_VISITED_VIEW: (state: TagsState, view: Route) => {
 for (const [i, v] of state.visitedViews.entries()) {
  if (v.path === view.path) {
  state.visitedViews.splice(i, 1);
  break;
  }
 }
 },
 DEL_CACHED_VIEW: (state: TagsState, view: Route) => {
 const index = state.cachedViews.indexOf(view.meta.name);
 index > -1 && state.cachedViews.splice(index, 1);
 },

 DEL_OTHERS_VISITED_VIEWS: (state: TagsState, view: Route) => {
 state.visitedViews = state.visitedViews.filter((v: any) => {
  return v.meta.affix || v.path === view.path;
 });
 },
 DEL_OTHERS_CACHED_VIEWS: (state: TagsState, view: Route) => {
 const index = state.cachedViews.indexOf(view.meta.name);
 if (index > -1) {
  state.cachedViews = state.cachedViews.slice(index, index + 1);
 } else {
  // if index = -1, there is no cached tags
  state.cachedViews = [];
 }
 },

 DEL_ALL_VISITED_VIEWS: (state: TagsState) => {
 // keep affix tags
 const affixTags = state.visitedViews.filter((tag: any) => tag.meta.affix);
 state.visitedViews = affixTags;
 },
 DEL_ALL_CACHED_VIEWS: (state: TagsState) => {
 state.cachedViews = [];
 },

 UPDATE_VISITED_VIEW: (state: TagsState, view: Route) => {
 for (let v of state.visitedViews) {
  if (v.path === view.path) {
  v = Object.assign(v, view);
  break;
  }
 }
 },
};

const actions = {
 addView({ dispatch }: any, view: Route) {
 dispatch('addVisitedView', view);
 dispatch('addCachedView', view);
 },
 addVisitedView({ commit }: any, view: Route) {
 commit('ADD_VISITED_VIEW', view);
 },
 addCachedView({ commit }: any, view: Route) {
 commit('ADD_CACHED_VIEW', view);
 },

 delView({ dispatch, state }: any, view: Route) {
 return new Promise((resolve) => {
  dispatch('delVisitedView', view);
  dispatch('delCachedView', view);
  resolve({
  visitedViews: [...state.visitedViews],
  cachedViews: [...state.cachedViews],
  });
 });
 },
 delVisitedView({ commit, state }: any, view: Route) {
 return new Promise((resolve) => {
  commit('DEL_VISITED_VIEW', view);
  resolve([...state.visitedViews]);
 });
 },
 delCachedView({ commit, state }: any, view: Route) {
 return new Promise((resolve) => {
  commit('DEL_CACHED_VIEW', view);
  resolve([...state.cachedViews]);
 });
 },

 delOthersViews({ dispatch, state }: any, view: Route) {
 return new Promise((resolve) => {
  dispatch('delOthersVisitedViews', view);
  dispatch('delOthersCachedViews', view);
  resolve({
  visitedViews: [...state.visitedViews],
  cachedViews: [...state.cachedViews],
  });
 });
 },
 delOthersVisitedViews({ commit, state }: any, view: Route) {
 return new Promise((resolve) => {
  commit('DEL_OTHERS_VISITED_VIEWS', view);
  resolve([...state.visitedViews]);
 });
 },
 delOthersCachedViews({ commit, state }: any, view: Route) {
 return new Promise((resolve) => {
  commit('DEL_OTHERS_CACHED_VIEWS', view);
  resolve([...state.cachedViews]);
 });
 },

 delAllViews({ dispatch, state }: any, view: Route) {
 return new Promise((resolve) => {
  dispatch('delAllVisitedViews', view);
  dispatch('delAllCachedViews', view);
  resolve({
  visitedViews: [...state.visitedViews],
  cachedViews: [...state.cachedViews],
  });
 });
 },
 delAllVisitedViews({ commit, state }: any) {
 return new Promise((resolve) => {
  commit('DEL_ALL_VISITED_VIEWS');
  resolve([...state.visitedViews]);
 });
 },
 delAllCachedViews({ commit, state }: any) {
 return new Promise((resolve) => {
  commit('DEL_ALL_CACHED_VIEWS');
  resolve([...state.cachedViews]);
 });
 },

 updateVisitedView({ commit }: any, view: Route) {
 commit('UPDATE_VISITED_VIEW', view);
 },
};

export default {
 namespaced: true,
 state,
 mutations,
 actions,
};

上面代码,我们定义了一系列的对标签的操作。

组件实现

组件解构如图

实用的 vue tags 创建缓存导航的过程实现

TheTagsView.vue

<script lang="ts">
/**
 * @author leo
 * @description #15638 【test_tabs 组件】tab组件
 */
import { Component, Vue, Prop, Watch, Mixins } from 'vue-property-decorator';
import ScrollPane from './ScrollPane.vue';
import path from 'path';

@Component({
 components: {
 ScrollPane,
 },
})
export default class TheTagsView extends Vue {
 get visitedViews() {
 return this.$store.state.tagsView.visitedViews; // 点开过的视图
 }
 get routes() {
 return this.$store.state.permission.routes;
 }

 public visible: boolean = false; // 标签右键列表显示隐藏
 public top: number = 0; // transform定位
 public left: number = 0; // transform定位
 public selectedTag: any = {}; // 当前活跃的标签
 public affixTags: any[] = []; // 所有标签

 @Watch('$route')
 public watchRoute() {
 this.addTags(); // 新增当前标签
 this.moveToCurrentTag(); // 删除原活动标签
 }

 @Watch('visible')
 public watchVisible(value: any) {
 if (value) {
  document.body.addEventListener('click', this.closeMenu);
  } else {
  document.body.removeEventListener('click', this.closeMenu);
  }
 }

 public isActive(route: any) { // 是否当前活动
 return route.path === this.$route.path;
 }

 public isAffix(tag: any) { // 是否固定
 return tag.meta && tag.meta.affix;
 }

 // 过滤当前标签于路由
 public filterAffixTags(routes: any, basePath = '/') {
 let tags: any = [];
 routes.forEach((route: any) => {
  if (route.meta && route.meta.affix) {
  const tagPath = path.resolve(basePath, route.path);
  tags.push({
   fullPath: tagPath,
   path: tagPath,
   name: route.name,
   meta: { ...route.meta },
  });
  }
  if (route.children) {
  const tempTags = this.filterAffixTags(route.children, route.path);
  if (tempTags.length >= 1) {
   tags = [...tags, ...tempTags];
  }
  }
 });
 return tags;
 }

 public addTags() {
 const { name } = this.$route;
 if (name) {
  this.$store.dispatch('tagsView/addView', this.$route);
 }
 return false;
 }

 public moveToCurrentTag() {
 const tags: any = this.$refs.tag;
 this.$nextTick(() => {
  if (tags) {
  for (const tag of tags) {
   if (tag.to.path === this.$route.path) {
   (this.$refs.scrollPane as any).moveToTarget(tag);
   // when query is different then update
   if (tag.to.fullPath !== this.$route.fullPath) {
    this.$store.dispatch('tagsView/updateVisitedView', this.$route);
   }
   break;
   }
  }
  }
 });
 }
 public refreshSelectedTag(view: any) {
 this.$store.dispatch('tagsView/delCachedView', view).then(() => {
  const { fullPath } = view;
  this.$nextTick(() => {
  this.$router.replace({
   path: '/redirect' + fullPath,
  });
  });
  });
 }

 public closeSelectedTag(view: any) {
 this.$store.dispatch('tagsView/delView', view).then(({ visitedViews }) => {
  if (this.isActive(view)) {
  this.toLastView(visitedViews, view);
  }
 });
 }

 public closeOthersTags() {
 this.$router.push(this.selectedTag);
 this.$store.dispatch('tagsView/delOthersViews', this.selectedTag).then(() => {
  this.moveToCurrentTag();
 });
 }

 public closeAllTags(view: any) {
 this.$store.dispatch('tagsView/delAllViews').then(({ visitedViews }) => {
  if (this.affixTags.some((tag) => tag.path === view.path)) {
  return;
  }
  this.toLastView(visitedViews, view);
 });
 }

 public toLastView(visitedViews: any , view: any) {
 const latestView = visitedViews.slice(-1)[0];
 if (latestView) {
  this.$router.push(latestView.fullPath);
 } else {
  // now the default is to redirect to the home page if there is no tags-view,
  // you can adjust it according to your needs.
  if (view.name === 'Dashboard') {
  // to reload home page
  this.$router.replace({ path: '/redirect' + view.fullPath });
  } else {
  this.$router.push('/');
  }
 }
 }
 public openMenu(tag: any , e: any) {
 const menuMinWidth = 105;
 const offsetLeft = this.$el.getBoundingClientRect().left; // container margin left
 const offsetWidth = this.$el.offsetWidth; // container width
 const maxLeft = offsetWidth - menuMinWidth; // left boundary
 const left = e.clientX - offsetLeft + 15 + 160; // 15: margin right

 if (left > maxLeft) {
  this.left = maxLeft;
 } else {
  this.left = left;
 }

 this.top = e.clientY;
 this.visible = true;
 this.selectedTag = tag;
 }

 public closeMenu() {
 this.visible = false;
 }

 public mounted() {
 this.initTags();
 this.addTags(); // 添加当前页面tag
 }
}
</script>

<template>
 <div id="tags-view-container" class="tags-view-container">
 <scroll-pane ref="scrollPane" class="tags-view-wrapper">
  <router-link
  v-for="(tag, index) in visitedViews"
  ref="tag"
  :key="tag.path"
  :class="isActive(tag)?'active':''"
  :to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
  tag="span"
  class="tags-view-item"
  @click.middle.native="!isAffix(tag)?closeSelectedTag(tag):''"
  @contextmenu.prevent.native="openMenu(tag,$event)"
  >
  {{ tag.title }}
  <!-- <span v-if="!isAffix(tag)" class="el-icon-close" @click.prevent.stop="closeSelectedTag(tag)" /> -->
  <span class="tab-border" v-if="index!==visitedViews.length-1"></span>
  </router-link>
 </scroll-pane>
 <ul v-show="visible" :style="{left:left+'px',top:top+'px'}" class="contextmenu">
  <li @click="refreshSelectedTag(selectedTag)">刷新</li>
  <li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">关闭当前标签</li>
  <li @click="closeOthersTags">关闭其他标签</li>
  <li @click="closeAllTags(selectedTag)">关闭所有</li>
 </ul>
 </div>
</template>

<style lang="less" scoped>
.tags-view-container {
 height: 46px;
 width: 100%;
 background: #fff;
 .tags-view-wrapper {
 position: relative;
 .tags-view-item {
  display: inline-block;
  position: relative;
  cursor: pointer;
  height: 46px;
  line-height: 46px;
  // border: 1px solid #d8dce5;
  color: #495060;
  background: #fff;
  padding: 0 4px;
  font-size: 14px;
  .tab-border {
  display: inline-block;
  height: 10px;
  width: 1px;
  background: #f1f1f1;
  margin-left: 4px;
  }
  &:hover {
  border-bottom: 2px solid #666;
  }
  &.active {
  // background-color: #1F1A16;
  border-bottom: 2px solid #1F1A16;
  color: #333;
  // border-color: #1F1A16;
  // &::before {
  // content: '';
  // background: #fff;
  // display: inline-block;
  // width: 8px;
  // height: 8px;
  // border-radius: 50%;
  // position: relative;
  // margin-right: 2px;
  // }
  }
 }
 }
 .contextmenu {
 margin: 0;
 background: #fff;
 z-index: 3000;
 position: absolute;
 list-style-type: none;
 padding: 5px 0;
 border-radius: 4px;
 font-size: 12px;
 font-weight: 400;
 color: #333;
 box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3);
 li {
  margin: 0;
  padding: 7px 16px;
  cursor: pointer;
  &:hover {
  background: #eee;
  }
 }
 }
}
</style>

<style lang="less">
//reset element css of el-icon-close
.tags-view-wrapper {
 .tags-view-item {
 .el-icon-close {
  width: 16px;
  height: 16px;
  vertical-align: 3px;
  border-radius: 50%;
  text-align: center;
  transition: all .3s cubic-bezier(.645, .045, .355, 1);
  transform-origin: 100% 50%;
  &:before {
  transform: scale(.6);
  display: inline-block;
  vertical-align: -3px;
  }
  &:hover {
  background-color: #b4bccc;
  color: #fff;
  }
 }
 }
 .el-scrollbar__bar{
 pointer-events: none;
 opacity: 0;
 }
}
</style>

ScrollPane.vue

<script lang="ts">
/**
 * @author leo
 * @description #15638 【test_tabs 组件】tab组件
 */
import { Component, Vue, Prop, Watch, Mixins } from 'vue-property-decorator';

const tagAndTagSpacing = 4; // tagAndTagSpacing

@Component({
 components: {
 ScrollPane,
 },
})
export default class ScrollPane extends Vue {
 get scrollWrapper() {
 return (this.$refs.scrollContainer as any).$refs.wrap;
 }

 public left: number = 0;

 public handleScroll(e: any) {
 const eventDelta = e.wheelDelta || -e.deltaY * 40;
 const $scrollWrapper = this.scrollWrapper;
 $scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4;
 }

 public moveToTarget(currentTag: any) {
 const $container = (this.$refs.scrollContainer as any).$el;
 const $containerWidth = $container.offsetWidth;
 const $scrollWrapper = this.scrollWrapper;
 const tagList: any = this.$parent.$refs.tag;

 let firstTag = null;
 let lastTag = null;

 // find first tag and last tag
 if (tagList.length > 0) {
  firstTag = tagList[0];
  lastTag = tagList[tagList.length - 1];
 }

 if (firstTag === currentTag) {
  $scrollWrapper.scrollLeft = 0;
 } else if (lastTag === currentTag) {
  $scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth;
 } else {
  // find preTag and nextTag
  const currentIndex = tagList.findIndex((item: any) => item === currentTag);
  const prevTag = tagList[currentIndex - 1];
  const nextTag = tagList[currentIndex + 1];

  // the tag's offsetLeft after of nextTag
  const afterNextTagOffsetLeft = nextTag.$el.offsetLeft + nextTag.$el.offsetWidth + tagAndTagSpacing;

  // the tag's offsetLeft before of prevTag
  const beforePrevTagOffsetLeft = prevTag.$el.offsetLeft - tagAndTagSpacing;

  if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) {
  $scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth;
  } else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
  $scrollWrapper.scrollLeft = beforePrevTagOffsetLeft;
  }
 }
 }
}
</script>

<template>
 <el-scrollbar ref="scrollContainer" :vertical="false" class="scroll-container" @wheel.native.prevent="handleScroll">
 <slot />
 </el-scrollbar>
</template>

<style lang="less" scoped>
.scroll-container {
 white-space: nowrap;
 position: relative;
 overflow: hidden;
 width: 100%;
 .el-scrollbar__bar {
 bottom: 0px;
 }
 .el-scrollbar__wrap {
 height: 49px;
 }
}
</style>

index.ts

import TheTagsView from './TheTagsView.vue';
export default TheTagsView;

这样我们的组件就写完啦,有哪里有问题的小伙伴可以留言哦。

组件调用

因为是全局的,所以放在全局下直接调用就好了

总结

这样我们一个简单的能实现alive 页面的tag功能就实现了。大家赶紧尝试一下吧~

兄台,请留步。这里有几点要注意一下哦~

问题1: 开发环境缓存住了,线上环境不好用了

我们是根据组件name值是否是include里包含的来判断的。但是你会发现生产的的时候 class后面的名在线上被打包后变了。 什么?!这岂不是缓存不住了???是的。 所以解决办法如图。一般人我不告诉他0.o

实用的 vue tags 创建缓存导航的过程实现

问题2: tags的显示名字我在哪定义呢

tags显示的名字我怎么定义呢,好问题。小兄弟肯定没有仔细读代码

ADD_VISITED_VIEW: (state: TagsState, view: Route) => {
 if (state.visitedViews.some((v: any) => v.path === view.path)) { return; }
 state.visitedViews.push(
  Object.assign({}, view, {
  title: view.meta.title || 'no-name', /// 我在这里!!!!!
  }),
 );
 },

由上图我们可知,我是在路由的配置里mate标签里的tile里配置的。至于你,随你哦~

{
 	path: 'index', // 入口
 name: 'common-home-index-index',
 component: () => import(/* webpackChunkName: "auth" */ '@/views/home/index.vue'),
 meta: {
  title: '首页', // 看见了么,我就是你要显示的名字
  name: 'CommonHome', // 记住,我要跟你的上面name页面组件名字一样
 },
}

问题3:我有的页面,跳路由后想刷新了怎么办

那我们页面缓存住了,我怎么让页面刷新呢,比如我新增页面,新增完了需要关闭当前页面跳回列表页面的,我们的思路就是,关闭标签,url参数添加refresh参数

this.$store
   .dispatch('tagsView/delView', this.$route)
   .then(({ visitedViews }) => {
    EventBus.$emit('gotoOwnerDeliveryOrderIndex', {
    refresh: true,
    });
   });

然后在activated钩子里判断下是否有这个参数,

this.$route.query.refresh && this.fetchData();

记得处理完结果后吧refresh删了,不然每次进来都刷新了,我们是在拉去数据的混合里删的

if ( this.$route.query.refresh ) {
 this.$route.query.refresh = '';
}

问题4:有没有彩蛋啊

有的,请看图。 实用的 vue tags 创建缓存导航的过程实现 我的哥乖乖,怎么实现的呢。这个留给你们研究吧。上面代码已经实现了。只不过你需要在加一个页面,跟路由。其实就是跳转到一个新空页面路由。重新跳回来一下~

redirect/index.vue

<script lang="ts">
import { Component, Vue, Prop, Watch, Mixins } from 'vue-property-decorator';

@Component
export default class Redirect extends Vue {
 public created() {
 const { params, query } = this.$route;
 const { path } = params;
 // debugger;
 this.$router.replace({ path: '/' + path, query });
 }
}
</script>
<template>
</template>
/**
 * 刷新跳转路由
 */
export const redirectRouter: any = {
 path: '/redirect',
 name: 'redirect',
 component: RouterView,
 children: [
 {
  path: '/redirect/:path*',
  component: () => import(/* webpackChunkName: "redirect" */ '@/views/redirect/index.vue'),
  meta: {
  title: 'title',
  },
 },
 ],
};

参考

https://github.com/PanJiaChen/vue-element-admin

到此这篇关于实用的 vue tags 创建缓存导航的过程的文章就介绍到这了,更多相关实用的 vue tags 创建缓存导航的过程内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Vue.js 相关文章推荐
Vue +WebSocket + WaveSurferJS 实现H5聊天对话交互的实例
Nov 18 Vue.js
如何使用 vue-cli 创建模板项目
Nov 19 Vue.js
在Vue中使用CSS3实现内容无缝滚动的示例代码
Nov 27 Vue.js
Vue router传递参数并解决刷新页面参数丢失问题
Dec 02 Vue.js
vue 导航守卫和axios拦截器有哪些区别
Dec 19 Vue.js
vue 动态生成拓扑图的示例
Jan 03 Vue.js
手写Vue源码之数据劫持示例详解
Jan 04 Vue.js
vue组件是如何解析及渲染的?
Jan 13 Vue.js
vue+elementui 实现新增和修改共用一个弹框的完整代码
Jun 08 Vue.js
vue使用echarts实现折线图
Mar 21 Vue.js
vue项目打包后路由错误的解决方法
Apr 13 Vue.js
关于vue-router-link选择样式设置
Apr 30 Vue.js
如何实现vue的tree组件
Dec 03 #Vue.js
Vue实现图书管理小案例
Dec 03 #Vue.js
Vue router安装及使用方法解析
Dec 02 #Vue.js
vue3.0中setup使用(两种用法)
Dec 02 #Vue.js
vue3.0+vue-router+element-plus初实践
Dec 02 #Vue.js
Vue router传递参数并解决刷新页面参数丢失问题
Dec 02 #Vue.js
详解Vue3 Teleport 的实践及原理
Dec 02 #Vue.js
You might like
php 获得汉字拼音首字母的函数
2009/08/01 PHP
php准确获取文件MIME类型的方法
2015/06/17 PHP
Session 失效的原因汇总及解决丢失办法
2015/09/30 PHP
PHP7多线程搭建教程
2017/04/21 PHP
php如何比较两个浮点数是否相等详解
2019/02/12 PHP
tp5框架前台无限极导航菜单类实现方法分析
2020/03/29 PHP
jquery 检测元素是否存在的实例代码
2013/11/19 Javascript
利用js实现在浏览器状态栏显示访问者在本页停留的时间
2013/12/29 Javascript
javascript运行机制之this详细介绍
2014/02/07 Javascript
AMD异步模块定义介绍和Require.js中使用jQuery及jQuery插件的方法
2014/06/06 Javascript
Node.js中使用Buffer编码、解码二进制数据详解
2014/08/16 Javascript
使用js画图之饼图
2015/01/12 Javascript
js 获取元素在页面上的偏移量的方法汇总
2015/04/13 Javascript
使用console进行性能测试
2015/04/27 Javascript
论JavaScript模块化编程
2016/03/07 Javascript
JS+CSS实现鼠标经过弹出一个DIV框完整实例(带缓冲动画渐变效果)
2016/03/25 Javascript
javascript创建对象的几种模式介绍
2016/05/06 Javascript
js实时获取窗口大小变化的实例代码
2016/11/18 Javascript
pm2 部署 node的三种方法示例
2017/10/20 Javascript
ES6中javascript实现函数绑定及类的事件绑定功能详解
2017/11/08 Javascript
node实现登录图片验证码的示例代码
2018/04/20 Javascript
js中的闭包实例展示
2018/11/01 Javascript
关于ligerui子页面关闭后,父页面刷新,重新加载的方法
2019/09/27 Javascript
mpvue网易云短信接口实现小程序短信登录的示例代码
2020/04/03 Javascript
详解Python3 中hasattr()、getattr()、setattr()、delattr()函数及示例代码数
2018/04/18 Python
小白入门篇使用Python搭建点击率预估模型
2018/10/12 Python
Python3爬虫学习之将爬取的信息保存到本地的方法详解
2018/12/12 Python
python实现诗歌游戏(类继承)
2019/02/26 Python
Django表单提交后实现获取相同name的不同value值
2020/05/14 Python
python 制作本地应用搜索工具
2021/02/27 Python
加拿大最大的相机店:Henry’s
2017/05/17 全球购物
揭牌仪式策划方案
2014/05/28 职场文书
社区党的群众路线教育实践活动领导班子对照检查材料
2014/09/25 职场文书
教师查摆问题自查报告
2014/10/11 职场文书
导游词之宁夏贺兰山岩画
2019/11/08 职场文书
MySQL单表千万级数据处理的思路分享
2021/06/05 MySQL