实用的 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使用echarts图表自适应的几种解决方案
Dec 04 Vue.js
vue基于Echarts的拖拽数据可视化功能实现
Dec 04 Vue.js
如何在vue中使用kindeditor富文本编辑器
Dec 19 Vue.js
vue实现防抖的实例代码
Jan 11 Vue.js
Vue页面渲染中key的应用实例教程
Jan 12 Vue.js
Vue实现多页签组件
Jan 14 Vue.js
Vue常用API、高级API的相关总结
Feb 02 Vue.js
vue如何使用rem适配
Feb 06 Vue.js
Vue CLI中模式与环境变量的深入详解
May 30 Vue.js
Vue.js中v-bind指令的用法介绍
Mar 13 Vue.js
vue3 自定义图片放大器效果的示例代码
Jul 23 Vue.js
Vue router配置与使用分析讲解
Dec 24 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的内置函数,通过DES算法对数据加密和解密
2012/06/21 PHP
ajax返回值中有回车换行、空格的解决方法分享
2013/10/24 PHP
Laravel 5框架学习之Blade 简介
2015/04/08 PHP
PHP中$_SERVER使用说明
2015/07/05 PHP
PHP pthreads v3下的Volatile简介与使用方法示例
2020/02/21 PHP
JavaScript脚本语言在网页中的简单应用
2007/05/13 Javascript
HTML代码中标签的全部属性 中文注释说明
2009/03/26 Javascript
兼容IE和Firefox的javascript获取iframe文档内容的函数
2011/08/15 Javascript
Javascript 多浏览器兼容总结(实战经验)
2013/10/30 Javascript
js+jquery实现图片裁剪功能
2015/01/02 Javascript
js+html5实现canvas绘制镂空字体文本的方法
2015/06/05 Javascript
javascript弹出带文字信息的提示框效果
2016/07/19 Javascript
jQuery表格(Table)基本操作实例分析
2017/03/10 Javascript
微信小程序出现wx.navigateTo页面不跳转问题的解决方法
2017/12/26 Javascript
JavaScript基础心法 深浅拷贝(浅拷贝和深拷贝)
2018/03/05 Javascript
jQuery实现炫丽的3d旋转星空效果
2018/07/04 jQuery
vue2 设置router-view默认路径的实例
2018/09/20 Javascript
vue图片上传组件使用详解
2019/12/23 Javascript
vue学习笔记之作用域插槽实例分析
2020/02/01 Javascript
[05:34]2014DOTA2国际邀请赛中国区预选赛精彩TOPPLAY第二弹
2014/06/25 DOTA
python求pi的方法
2014/10/08 Python
Python学习笔记之解析json的方法分析
2017/04/21 Python
Python实现的计算马氏距离算法示例
2018/04/03 Python
python smtplib模块自动收发邮件功能(一)
2018/05/22 Python
对python_discover方法遍历所有执行的用例详解
2019/02/13 Python
python登录WeChat 实现自动回复实例详解
2019/05/28 Python
Django用户认证系统 组与权限解析
2019/08/02 Python
python爬虫 爬取超清壁纸代码实例
2019/08/16 Python
Python定义函数实现累计求和操作
2020/05/03 Python
Python tkinter实现简单加法计算器代码实例
2020/05/13 Python
英国家用电器折扣网站:Electrical Discount UK
2018/09/17 全球购物
写出程序把一个链表中的接点顺序倒排
2014/04/28 面试题
小学清明节活动方案
2014/03/08 职场文书
美术专业自荐信
2014/07/07 职场文书
2014年医院工作总结
2014/11/20 职场文书
SQL SERVER中的流程控制语句
2022/05/25 SQL Server