4 Commits

17 changed files with 251 additions and 82 deletions

View File

@ -2,6 +2,16 @@
🎉🎉🔥 `vue-next-admin` 基于 vue3.x 、Typescript、vite、Element plus 等适配手机、平板、pc 的后台开源免费模板库vue2.x 请切换 vue-prev-admin 分支)
## 1.2.2
`2021.12.21`
- 🌟 更新 依赖更新最新版本
- 🎯 优化 iframes 滚动条问题
- 🎯 优化 部署后每次都要强制刷新清浏览器缓存问题
- 🎉 新增 工具类百分比验证演示
- 🐞 修复 [tag-view 标签右键会超出浏览器 #I4KN78](https://gitee.com/lyt-top/vue-next-admin/issues/I4KN78)
## 1.2.1
`2021.12.12`

View File

@ -68,19 +68,10 @@ cnpm run dev
cnpm run build
```
#### 🍉 git 命令
- 在本地新建一个分支:`git branch newBranch`
- 切换到你的新分支:`git checkout newBranch`
- 将新分支发布在 github、gitee 上:`git push origin newBranch`
- 在本地删除一个分支:`git branch -d newBranch`
- 在 github 远程端删除一个分支:`git push origin :newBranch (分支名前的冒号代表删除)`
- 注意删除远程分支后,如果有对应的本地分支,本地分支并不会同步删除!
#### 💯 学习交流加 QQ 群
- 若加群了没同意(一般不会超过一天),那就是群满了,请换一个群试试
- 查看开发文档<a href="https://lyt-top.gitee.io/vue-next-admin-preview/#/login" target="_blank">vue-next-admin</a> 开发文档正在编写中...
- 查看开发文档<a href="https://lyt-top.gitee.io/vue-next-admin-doc-preview" target="_blank">vue-next-admin-doc</a>
- 群号码:
1 群:<a target="_blank" href="https://qm.qq.com/cgi-bin/qm/qr?k=RdUY97Vx0T0vZ_1OOu-X1yFNkWgDwbjC&jump_from=webapi">665452019</a>
2 群:<a target="_blank" href="https://qm.qq.com/cgi-bin/qm/qr?k=zVfy3gNy7pNWVK3kMduDzwU369PZg2fw&jump_from=webapi">766356862</a>
@ -92,6 +83,11 @@ cnpm run build
<img src="https://gitee.com/lyt-top/vue-next-admin-images/raw/master/user/qq2.png" width="220" height="220" alt="vue-next-admin 讨论群" title="vue-next-admin 讨论群2"/>
</a>
#### 💒 集成后端
- <a target="_blank" href="https://github.com/PandaGoAdmin/PandaX">@熊猫 PandaGoAdmin</a>
- <a target="_blank" href="https://www.gnet.top/public">@甜蜜蜜 GoPro 平台</a>
#### ❤️ 鸣谢列表
- <a href="https://github.com/vuejs/vue" target="_blank">vue</a>

View File

@ -1,7 +1,7 @@
<!DOCTYPE html>
<html lang="en">
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta

View File

@ -1,6 +1,6 @@
{
"name": "vue-next-admin",
"version": "1.2.0",
"version": "1.2.2",
"description": "vue3 vite next admin template",
"author": "lyt_20201208",
"license": "MIT",
@ -38,21 +38,21 @@
"devDependencies": {
"@types/axios": "^0.14.0",
"@types/clipboard": "^2.0.1",
"@types/node": "^16.11.12",
"@types/node": "^17.0.2",
"@types/nprogress": "^0.2.0",
"@types/sortablejs": "^1.10.7",
"@typescript-eslint/eslint-plugin": "^5.6.0",
"@typescript-eslint/parser": "^5.6.0",
"@vitejs/plugin-vue": "^2.0.0",
"@typescript-eslint/eslint-plugin": "^5.8.0",
"@typescript-eslint/parser": "^5.8.0",
"@vitejs/plugin-vue": "^2.0.1",
"@vue/compiler-sfc": "^3.2.26",
"dotenv": "^10.0.0",
"eslint": "^8.4.1",
"eslint": "^8.5.0",
"eslint-plugin-vue": "^8.2.0",
"prettier": "^2.5.1",
"sass": "^1.45.0",
"sass": "^1.45.1",
"sass-loader": "^12.4.0",
"typescript": "^4.5.3",
"vite": "^2.7.1",
"typescript": "^4.5.4",
"vite": "^2.7.4",
"vue-eslint-parser": "^8.0.1"
},
"browserslist": [

View File

@ -20,7 +20,7 @@
<div class="layout-lock-screen-date-box-info">{{ time.mdq }}</div>
</div>
<div class="layout-lock-screen-date-top">
<i class="el-icon-top"></i>
<SvgIcon name="elementTop" />
<div class="layout-lock-screen-date-top-text">上滑解锁</div>
</div>
</div>
@ -39,15 +39,19 @@
@keyup.enter.native.stop="onLockScreenSubmit()"
>
<template #append>
<el-button icon="el-icon-right" @click="onLockScreenSubmit"></el-button>
<el-button @click="onLockScreenSubmit">
<el-icon class="el-input__icon">
<elementRight />
</el-icon>
</el-button>
</template>
</el-input>
</div>
</div>
<div class="layout-lock-screen-login-icon">
<i class="el-icon-microphone"></i>
<i class="el-icon-alarm-clock"></i>
<i class="el-icon-switch-button"></i>
<SvgIcon name="elementMicrophone" />
<SvgIcon name="elementAlarmClock" />
<SvgIcon name="elementSwitchButton" />
</div>
</div>
</transition>

View File

@ -201,10 +201,10 @@
<el-switch v-model="getThemeConfig.isCacheTagsView" @change="setLocalThemeConfig"></el-switch>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex mt15">
<div class="layout-breadcrumb-seting-bar-flex mt15" :style="{ opacity: isMobile ? 0.5 : 1 }">
<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.fourIsSortableTagsView') }}</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-switch v-model="getThemeConfig.isSortableTagsView" @change="onSortableTagsViewChange"></el-switch>
<el-switch v-model="getThemeConfig.isSortableTagsView" :disabled="isMobile ? true : false" @change="onSortableTagsViewChange"></el-switch>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex mt15">
@ -381,19 +381,23 @@
</template>
<script lang="ts">
import { nextTick, onUnmounted, onMounted, getCurrentInstance, defineComponent, computed } from 'vue';
import { nextTick, onUnmounted, onMounted, getCurrentInstance, defineComponent, computed, reactive, toRefs } from 'vue';
import { useStore } from '/@/store/index';
import { getLightColor } from '/@/utils/theme';
import { verifyAndSpace } from '/@/utils/toolsValidate';
import { Local } from '/@/utils/storage';
import Watermark from '/@/utils/wartermark';
import commonFunction from '/@/utils/commonFunction';
import other from '/@/utils/other';
export default defineComponent({
name: 'layoutBreadcrumbSeting',
setup() {
const { proxy } = getCurrentInstance() as any;
const store = useStore();
const { copyText } = commonFunction();
const state = reactive({
isMobile: false,
});
// 获取布局配置信息
const getThemeConfig = computed(() => {
return store.state.themeConfig.themeConfig;
@ -613,6 +617,7 @@ export default defineComponent({
getThemeConfig.value.isDrawer = false;
initLayoutChangeFun();
onMenuBarHighlightChange();
state.isMobile = other.isMobile();
});
setTimeout(() => {
// 修复防止退出登录再进入界面时,需要刷新样式才生效的问题,初始化布局样式等(登录的时候触发,目前方案)
@ -661,6 +666,7 @@ export default defineComponent({
onShareTagsViewChange,
onCopyConfigClick,
onResetConfigClick,
...toRefs(state),
};
},
});

View File

@ -24,13 +24,13 @@
</li>
</template>
</ul>
<div class="el-popper__arrow" style="left: 10px"></div>
<div class="el-popper__arrow" :style="{ left: `${arrowLeft}px` }"></div>
</div>
</transition>
</template>
<script lang="ts">
import { computed, defineComponent, reactive, toRefs, onMounted, onUnmounted } from 'vue';
import { computed, defineComponent, reactive, toRefs, onMounted, onUnmounted, watch } from 'vue';
export default defineComponent({
name: 'layoutTagsViewContextmenu',
props: {
@ -54,10 +54,19 @@ export default defineComponent({
},
],
item: {},
arrowLeft: 10,
});
// 父级传过来的坐标 x,y 值
const dropdowns = computed(() => {
return props.dropdown;
// 117 为 `Dropdown 下拉菜单` 的宽度
if (props.dropdown.x + 117 > document.documentElement.clientWidth) {
return {
x: document.documentElement.clientWidth - 117 - 5,
y: props.dropdown.y,
};
} else {
return props.dropdown;
}
});
// 当前项菜单点击
const onCurrentContextmenuClick = (contextMenuClickId: number) => {
@ -84,6 +93,17 @@ export default defineComponent({
onUnmounted(() => {
document.body.removeEventListener('click', closeContextmenu);
});
// 监听下拉菜单位置
watch(
() => props.dropdown,
({ x }) => {
if (x + 117 > document.documentElement.clientWidth) state.arrowLeft = 117 - (document.documentElement.clientWidth - x);
else state.arrowLeft = 10;
},
{
deep: true,
}
);
return {
dropdowns,
openContextmenu,
@ -107,5 +127,10 @@ export default defineComponent({
font-size: 12px !important;
}
}
.el-dropdown-menu-arrow {
&::before {
content: '';
}
}
}
</style>

View File

@ -49,6 +49,7 @@ import { ElMessage } from 'element-plus';
import { useStore } from '/@/store/index';
import { Session } from '/@/utils/storage';
import { isObjectValueEqual } from '/@/utils/arrayOperation';
import other from '/@/utils/other';
import Contextmenu from '/@/layout/navBars/tagsView/contextmenu.vue';
export default {
name: 'layoutTagsView',
@ -397,10 +398,10 @@ export default {
});
};
// 设置 tagsView 可以进行拖拽
const initSortable = () => {
const initSortable = async () => {
const el = document.querySelector('.layout-navbars-tagsview-ul') as HTMLElement;
if (!el) return false;
state.sortable && state.sortable.destroy();
state.sortable.el && state.sortable.destroy();
state.sortable = Sortable.create(el, {
animation: 300,
dataIdAttr: 'data-url',
@ -417,11 +418,9 @@ export default {
});
};
// 拖动问题https://gitee.com/lyt-top/vue-next-admin/issues/I3ZRRI
const onSortableResize = () => {
const clientWidth = document.body.clientWidth;
if (clientWidth < 1000) getThemeConfig.value.isSortableTagsView = false;
else getThemeConfig.value.isSortableTagsView = true;
initSortable();
const onSortableResize = async () => {
await initSortable();
if (other.isMobile()) state.sortable.el && state.sortable.destroy();
};
// 页面加载前
onBeforeMount(() => {

View File

@ -34,10 +34,10 @@ export default defineComponent({
let { isTagsview } = store.state.themeConfig.themeConfig;
let { isTagsViewCurrenFull } = store.state.tagsViewRoutes;
if (isTagsViewCurrenFull) {
return `0px`;
return `1px`;
} else {
if (isTagsview) return `83px`;
else return `49px`;
if (isTagsview) return `85px`;
else return `51px`;
}
});
// 页面加载时

View File

@ -89,6 +89,21 @@ export function deepClone(obj: any) {
return newObj;
}
/**
* 判断是否是移动端
*/
export function isMobile() {
if (
navigator.userAgent.match(
/('phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone')/i
)
) {
return true;
} else {
return false;
}
}
/**
* 统一批量导出
* @method elSvg 导出全局注册 element plus svg 图标
@ -96,6 +111,7 @@ export function deepClone(obj: any) {
* @method lazyImg 图片懒加载
* @method globalComponentSize element plus 全局组件大小
* @method deepClone 对象深克隆
* @method isMobile 判断是否是移动端
*/
const other = {
elSvg: (app: App) => {
@ -113,6 +129,9 @@ const other = {
deepClone: (obj: any) => {
deepClone(obj);
},
isMobile: () => {
return isMobile();
},
};
// 统一批量导出

View File

@ -4,6 +4,39 @@
* 新增多行注释信息,鼠标放到方法名即可查看
*/
/**
* 验证百分比(不可以小数)
* @param val 当前值字符串
* @returns 返回处理后的字符串
*/
export function verifyNumberPercentage(val: string): string {
// 匹配空格
let v = val.replace(/(^\s*)|(\s*$)/g, '');
// 只能是数字和小数点,不能是其他输入
v = v.replace(/[^\d]/g, '');
// 不能以0开始
v = v.replace(/^0/g, '');
// 数字超过100赋值成最大值100
v = v.replace(/^[1-9]\d\d{1,3}$/, '100');
// 返回结果
return v;
}
/**
* 验证百分比(可以小数)
* @param val 当前值字符串
* @returns 返回处理后的字符串
*/
export function verifyNumberPercentageFloat(val: string): string {
let v = verifyNumberIntegerAndFloat(val);
// 数字超过100赋值成最大值100
v = v.replace(/^[1-9]\d\d{1,3}$/, '100');
// 超过100之后不给再输入值
v = v.replace(/^100\.$/, '100');
// 返回结果
return v;
}
/**
* 小数或整数(不可以负数)
* @param val 当前值字符串

View File

@ -1,13 +1,13 @@
<template>
<el-form class="login-content-form">
<el-form-item>
<el-form-item class="login-animation-one">
<el-input type="text" :placeholder="$t('message.account.accountPlaceholder1')" v-model="ruleForm.userName" clearable autocomplete="off">
<template #prefix>
<el-icon class="el-input__icon"><elementUser /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item>
<el-form-item class="login-animation-two">
<el-input
:type="isShowPassword ? 'text' : 'password'"
:placeholder="$t('message.account.accountPlaceholder2')"
@ -27,7 +27,7 @@
</template>
</el-input>
</el-form-item>
<el-form-item>
<el-form-item class="login-animation-three">
<el-row :gutter="15">
<el-col :span="16">
<el-input
@ -50,11 +50,15 @@
</el-col>
</el-row>
</el-form-item>
<el-form-item>
<el-form-item class="login-animation-four">
<el-button type="primary" class="login-content-submit" round @click="onSignIn" :loading="loading.signIn">
<span>{{ $t('message.account.accountBtnText') }}</span>
</el-button>
</el-form-item>
<el-form-item class="login-animation-five">
<el-button type="text" size="small">{{ $t('message.link.one3') }}</el-button>
<el-button type="text" size="small">{{ $t('message.link.two4') }}</el-button>
</el-form-item>
</el-form>
</template>
@ -179,6 +183,32 @@ export default defineComponent({
<style scoped lang="scss">
.login-content-form {
margin-top: 20px;
.login-animation-one,
.login-animation-two,
.login-animation-three,
.login-animation-four,
.login-animation-five {
opacity: 0;
animation-name: error-num;
animation-duration: 0.5s;
animation-fill-mode: forwards;
}
.login-animation-one {
animation-delay: 0.1s;
}
.login-animation-two {
animation-delay: 0.2s;
}
.login-animation-three {
animation-delay: 0.3s;
}
.login-animation-four {
animation-delay: 0.4s;
margin-bottom: 5px;
}
.login-animation-five {
animation-delay: 0.5s;
}
.login-content-password {
display: inline-block;
width: 25px;

View File

@ -1,13 +1,13 @@
<template>
<el-form class="login-content-form">
<el-form-item>
<el-form-item class="login-animation-one">
<el-input type="text" :placeholder="$t('message.mobile.placeholder1')" v-model="ruleForm.userName" clearable autocomplete="off">
<template #prefix>
<i class="iconfont icon-dianhua el-input__icon"></i>
</template>
</el-input>
</el-form-item>
<el-form-item>
<el-form-item class="login-animation-two">
<el-row :gutter="15">
<el-col :span="16">
<el-input type="text" maxlength="4" :placeholder="$t('message.mobile.placeholder2')" v-model="ruleForm.code" clearable autocomplete="off">
@ -21,11 +21,15 @@
</el-col>
</el-row>
</el-form-item>
<el-form-item>
<el-form-item class="login-animation-three">
<el-button type="primary" class="login-content-submit" round>
<span>{{ $t('message.mobile.btnText') }}</span>
</el-button>
</el-form-item>
<el-form-item class="login-animation-four">
<el-button type="text" size="small">{{ $t('message.link.one3') }}</el-button>
<el-button type="text" size="small">{{ $t('message.link.two4') }}</el-button>
</el-form-item>
</el-form>
</template>
@ -50,6 +54,29 @@ export default defineComponent({
<style scoped lang="scss">
.login-content-form {
margin-top: 20px;
.login-animation-one,
.login-animation-two,
.login-animation-three,
.login-animation-four,
.login-animation-five {
opacity: 0;
animation-name: error-num;
animation-duration: 0.5s;
animation-fill-mode: forwards;
}
.login-animation-one {
animation-delay: 0.1s;
}
.login-animation-two {
animation-delay: 0.2s;
}
.login-animation-three {
animation-delay: 0.3s;
margin-bottom: 5px;
}
.login-animation-four {
animation-delay: 0.4s;
}
.login-content-code {
width: 100%;
padding: 0;

View File

@ -1,6 +1,6 @@
<template>
<div class="login-scan-container">
<div class="login-scan-qrcode" ref="qrcodeRef"></div>
<div ref="qrcodeRef"></div>
</div>
</template>
@ -36,11 +36,9 @@ export default defineComponent({
<style scoped lang="scss">
.login-scan-container {
.login-scan-qrcode {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -40%);
}
padding: 20px;
display: flex;
justify-content: center;
animation: logoAnimation 0.3s ease;
}
</style>

View File

@ -3,28 +3,20 @@
<div class="login-logo">
<span>{{ getThemeConfig.globalViceTitle }}</span>
</div>
<div class="login-content" :class="{ 'login-content-mobile': tabsActiveName === 'mobile' }">
<div class="login-content">
<div class="login-content-main">
<h4 class="login-content-title">{{ getThemeConfig.globalTitle }}后台模板</h4>
<div v-if="!isScan">
<el-tabs v-model="tabsActiveName" @tab-click="onTabsClick">
<el-tab-pane :label="$t('message.label.one1')" name="account" :disabled="tabsActiveName === 'account'">
<transition name="el-zoom-in-center">
<Account v-show="isTabPaneShow" />
</transition>
<el-tabs v-model="tabsActiveName">
<el-tab-pane :label="$t('message.label.one1')" name="account">
<Account />
</el-tab-pane>
<el-tab-pane :label="$t('message.label.two2')" name="mobile" :disabled="tabsActiveName === 'mobile'">
<transition name="el-zoom-in-center">
<Mobile v-show="!isTabPaneShow" />
</transition>
<el-tab-pane :label="$t('message.label.two2')" name="mobile">
<Mobile />
</el-tab-pane>
</el-tabs>
<div class="mt10">
<el-button type="text" size="small">{{ $t('message.link.one3') }}</el-button>
<el-button type="text" size="small">{{ $t('message.link.two4') }}</el-button>
</div>
</div>
<Scan v-else />
<Scan v-if="isScan" />
<div class="login-content-main-sacn" @click="isScan = !isScan">
<i class="iconfont" :class="isScan ? 'icon-diannao1' : 'icon-barcode-qr'"></i>
<div class="login-content-main-sacn-delta"></div>
@ -58,12 +50,7 @@ export default {
const getThemeConfig = computed(() => {
return store.state.themeConfig.themeConfig;
});
// 切换密码、手机登录
const onTabsClick = () => {
state.isTabPaneShow = !state.isTabPaneShow;
};
return {
onTabsClick,
getThemeConfig,
...toRefs(state),
};
@ -100,8 +87,7 @@ export default {
background-color: rgba(255, 255, 255, 0.99);
border: 5px solid var(--color-primary-light-8);
border-radius: 4px;
transition: height 0.2s linear;
height: 480px;
transition: all 0.3s ease;
overflow: hidden;
z-index: 1;
.login-content-main {
@ -155,9 +141,6 @@ export default {
}
}
}
.login-content-mobile {
height: 418px;
}
.login-copyright {
position: absolute;
left: 50%;

View File

@ -1,7 +1,23 @@
<template>
<el-card shadow="hover" header="正则验证(一些项目中常用的正则)">
<el-form :model="ruleForm" :rules="rules" class="tools-warp-form" size="small" label-position="top">
<el-form-item label="小数或整数:" prop="a1">
<el-form-item label="验证百分比(不可以小数):" prop="a22">
<div class="tools-warp-form-msg">验证可以输入大于0小于100的数字</div>
<div>
<el-input v-model="ruleForm.a22" @input="onVerifyNumberPercentage($event)" placeholder="请输入数字进行测试">
<template #append> % </template>
</el-input>
</div>
</el-form-item>
<el-form-item label="验证百分比(可以小数):" prop="a23" class="mt20">
<div class="tools-warp-form-msg">验证可以输入大于0小于100的数字</div>
<div>
<el-input v-model="ruleForm.a23" @input="onVerifyNumberPercentageFloat($event)" placeholder="请输入数字进行测试">
<template #append> % </template>
</el-input>
</div>
</el-form-item>
<el-form-item label="小数或整数:" prop="a1" class="mt20">
<div class="tools-warp-form-msg">
验证可以输入小数或整数0 开始 . 只能出现一次保留小数点后保留2位小数(负数时模拟拼接负号给后台)
</div>
@ -174,6 +190,8 @@
<script lang="ts">
import { reactive, toRefs } from 'vue';
import {
verifyNumberPercentage,
verifyNumberPercentageFloat,
verifyNumberIntegerAndFloat,
verifiyNumberInteger,
verifyCnAndSpace,
@ -241,6 +259,8 @@ export default {
a19: '',
a20: '',
a21: '',
a22: '',
a23: '',
},
rules: {
a1: [
@ -318,8 +338,18 @@ export default {
trigger: 'change',
},
],
a22: [{ required: true, message: '请输入数字进行测试', trigger: 'change' }],
a23: [{ required: true, message: '请输入数字进行测试', trigger: 'change' }],
},
});
// 验证百分比(不可以小数)
const onVerifyNumberPercentage = (val: string) => {
state.ruleForm.a22 = verifyNumberPercentage(val);
};
// 验证百分比(可以小数)
const onVerifyNumberPercentageFloat = (val: string) => {
state.ruleForm.a23 = verifyNumberPercentageFloat(val);
};
// 小数或整数
const onVerifyNumberIntegerAndFloat = (val: string) => {
state.ruleForm.a1 = verifyNumberIntegerAndFloat(val);
@ -419,6 +449,8 @@ export default {
state.carNum = verifyCarNum(state.ruleForm.a21);
};
return {
onVerifyNumberPercentage,
onVerifyNumberPercentageFloat,
onVerifyNumberIntegerAndFloat,
onVerifiyNumberInteger,
onVerifyCnAndSpace,

View File

@ -40,6 +40,13 @@ const viteConfig: UserConfig = {
minify: 'esbuild',
sourcemap: false,
chunkSizeWarningLimit: 1500,
rollupOptions: {
output: {
entryFileNames: `assets/[name]-${new Date().getTime()}.js`,
chunkFileNames: `assets/[name]-${new Date().getTime()}.js`,
assetFileNames: `assets/[name]-${new Date().getTime()}.[ext]`,
},
},
},
define: {
__VUE_I18N_LEGACY_API__: JSON.stringify(false),