提交 ec9478f7 编写于 作者: V vben

refactor: refactor login page

上级 e3851dc5
{ {
"recommendations": [ "recommendations": [
"johnsoncodehk.volar",
"octref.vetur", "octref.vetur",
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"stylelint.vscode-stylelint", "stylelint.vscode-stylelint",
"DavidAnson.vscode-markdownlint",
"esbenp.prettier-vscode", "esbenp.prettier-vscode",
"mrmlnc.vscode-less", "mrmlnc.vscode-less",
"antfu.i18n-ally", "antfu.i18n-ally",
"cpylua.language-postcss",
"Orta.vscode-jest",
"antfu.iconify", "antfu.iconify",
"mikestead.dotenv", "mikestead.dotenv",
"bradlc.vscode-tailwindcss", "bradlc.vscode-tailwindcss",
"heybourn.headwind", "heybourn.headwind"
"znck.vue-language-features"
] ]
} }
# Review comments generated by i18n-ally. Please commit this file.
reviews:
sys.login.autoLogin:
description: '1'
## Wip ## Wip
### ✨ Refactor
- 登录页重构,新增注册页面/重置密码页面/手机登录/二维码登录
### ✨ Features ### ✨ Features
- 新增 `settingButtonPosition`配置项,用于配置`设置`按钮位置 - 新增 `settingButtonPosition`配置项,用于配置`设置`按钮位置
- `modal`可以通过双击头部切换全屏 - `modal`可以通过双击头部切换全屏
- 新增`CountDownInput`组件
### ⚡ Performance Improvements ### ⚡ Performance Improvements
......
...@@ -94,7 +94,7 @@ export function generateModifyVars() { ...@@ -94,7 +94,7 @@ export function generateModifyVars() {
'disabled-color': 'rgba(0, 0, 0, 0.25)', // Failure color 'disabled-color': 'rgba(0, 0, 0, 0.25)', // Failure color
'heading-color': 'rgba(0, 0, 0, 0.85)', // Title color 'heading-color': 'rgba(0, 0, 0, 0.85)', // Title color
'text-color': 'rgba(0, 0, 0, 0.85)', // Main text color 'text-color': 'rgba(0, 0, 0, 0.85)', // Main text color
'text-color-secondary ': 'rgba(0, 0, 0, 0.45)', // Subtext color 'text-color-secondary': 'rgba(0, 0, 0, 0.45)', // Subtext color
'font-size-base': '14px', // Main font size 'font-size-base': '14px', // Main font size
'box-shadow-base': '0 2px 8px rgba(0, 0, 0, 0.15)', // Floating shadow 'box-shadow-base': '0 2px 8px rgba(0, 0, 0, 0.15)', // Floating shadow
'border-color-base': '#d9d9d9', // Border color, 'border-color-base': '#d9d9d9', // Border color,
......
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="6395" height="1079" viewBox="0 0 6395 1079">
<defs>
<clipPath id="clip-path">
<rect id="Rectangle_73" data-name="Rectangle 73" width="6395" height="1079" transform="translate(-5391)" fill="#fff"/>
</clipPath>
<linearGradient id="linear-gradient" x1="0.747" y1="0.222" x2="0.973" y2="0.807" gradientUnits="objectBoundingBox">
<stop offset="0" stop-color="#2b51b4"/>
<stop offset="1" stop-color="#1c3faa"/>
</linearGradient>
</defs>
<g id="Mask_Group_1" data-name="Mask Group 1" transform="translate(5391)" clip-path="url(#clip-path)">
<g id="Group_118" data-name="Group 118" transform="translate(-419.333 -1.126)">
<path id="Path_142" data-name="Path 142" d="M6271.734-6.176s-222.478,187.809-55.349,583.254c44.957,106.375,81.514,205.964,84.521,277,8.164,192.764-156.046,268.564-156.046,268.564l-653.53-26.8L5475.065-21.625Z" transform="translate(-4876.383 0)" fill="#f1f5f8"/>
<path id="Union_6" data-name="Union 6" d="M-2631.1,1081.8v-1.6H-8230.9V.022H-2631.1V0H-1871.4s-187.845,197.448-91.626,488.844c49.167,148.9,96.309,256.289,104.683,362.118,7.979,100.852-57.98,201.711-168.644,254.286-65.858,31.29-144.552,42.382-223.028,42.383C-2441.2,1147.632-2631.1,1081.8-2631.1,1081.8Z" transform="translate(3259.524 0.803)" fill="url(#linear-gradient)"/>
</g>
</g>
</svg>
<svg id="a622e68e-7a65-46e9-94a9-d455de519afc" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="971.44" height="502" viewBox="0 0 971.44 502"><defs><linearGradient id="341b0e5e-a21f-44db-b85f-76180f33f0d3" x1="599.5" y1="668.05" x2="599.5" y2="199" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="gray" stop-opacity="0.25"/><stop offset="0.54" stop-color="gray" stop-opacity="0.12"/><stop offset="1" stop-color="gray" stop-opacity="0.1"/></linearGradient><linearGradient id="9c19d1ba-0c1d-4cca-8c15-e6f3831a5e67" x1="485.72" y1="258.88" x2="485.72" y2="71.12" xlink:href="#341b0e5e-a21f-44db-b85f-76180f33f0d3"/><linearGradient id="fe76f7c7-2126-4e48-920d-21143a22d340" x1="132" y1="515" x2="303" y2="515" xlink:href="#341b0e5e-a21f-44db-b85f-76180f33f0d3"/><linearGradient id="2cf89a04-5a05-413b-983a-d2bc296cbb5e" x1="933" y1="568.28" x2="1031" y2="568.28" xlink:href="#341b0e5e-a21f-44db-b85f-76180f33f0d3"/></defs><title>responsive</title><g opacity="0.7"><path d="M852.69,199H346.31A16.37,16.37,0,0,0,330,215.42V563.94a16.37,16.37,0,0,0,16.31,16.42H520.47v60.16h-7.94a8.3,8.3,0,0,0-8.27,8.33v12.07h16.21v7.14H678.53v-7.14h16.21V648.85a8.3,8.3,0,0,0-8.27-8.33H679V640h-.51V580.36H852.69A16.37,16.37,0,0,0,869,563.94V215.42A16.37,16.37,0,0,0,852.69,199Z" transform="translate(-114.28 -199)" fill="url(#341b0e5e-a21f-44db-b85f-76180f33f0d3)"/></g><rect x="407.72" y="371" width="156" height="92" fill="#bdbdbd"/><g opacity="0.1"><path d="M525.07,579H675.24c1.81-7.87,3.26-13,3.26-13h-157S523.11,571.11,525.07,579Z" transform="translate(-114.28 -199)"/></g><path d="M235.82,3h499.8a16.1,16.1,0,0,1,16.1,16.1V327a0,0,0,0,1,0,0h-532a0,0,0,0,1,0,0V19.1A16.1,16.1,0,0,1,235.82,3Z" fill="#535461"/><path d="M849.9,576H350.1A16.1,16.1,0,0,1,334,559.9V526H866v33.9A16.1,16.1,0,0,1,849.9,576Z" transform="translate(-114.28 -199)" fill="#bdbdbd"/><circle cx="485.72" cy="352" r="9" fill="#535461"/><path d="M399.89,436H571.55a8.17,8.17,0,0,1,8.17,8.17V456a0,0,0,0,1,0,0h-188a0,0,0,0,1,0,0V444.17A8.17,8.17,0,0,1,399.89,436Z" fill="#bdbdbd"/><g opacity="0.5"><rect x="320.72" y="71.12" width="330" height="187.76" rx="4.5" ry="4.5" fill="url(#9c19d1ba-0c1d-4cca-8c15-e6f3831a5e67)"/></g><rect x="324.95" y="72.5" width="321.54" height="183.96" rx="4.5" ry="4.5" fill="#fff"/><g opacity="0.5"><rect x="414.52" y="98.91" width="35.44" height="31.9" rx="4.5" ry="4.5" fill="#0960bd"/></g><rect x="460.59" y="98.91" width="95.69" height="3.54" rx="1.77" ry="1.77" fill="#e0e0e0"/><rect x="460.59" y="109.55" width="79.54" height="3.54" rx="1.77" ry="1.77" fill="#e0e0e0"/><g opacity="0.5"><rect x="414.52" y="148.53" width="35.44" height="31.9" rx="4.5" ry="4.5" fill="#0960bd"/></g><rect x="460.59" y="148.53" width="95.69" height="3.54" rx="1.77" ry="1.77" fill="#e0e0e0"/><rect x="460.59" y="159.16" width="95.69" height="3.54" rx="1.77" ry="1.77" fill="#e0e0e0"/><g opacity="0.5"><rect x="414.52" y="198.15" width="35.44" height="31.9" rx="4.5" ry="4.5" fill="#0960bd"/></g><rect x="460.59" y="198.15" width="95.69" height="3.54" rx="1.77" ry="1.77" fill="#e0e0e0"/><rect x="460.59" y="208.78" width="96.33" height="3.54" rx="1.59" ry="1.59" fill="#e0e0e0"/><line x1="485.72" y1="42" x2="485.72" y2="20" stroke="#0960bd" stroke-miterlimit="10" stroke-width="2"/><line x1="485.72" y1="79" x2="485.72" y2="50.13" stroke="#0960bd" stroke-miterlimit="10" stroke-width="2"/><circle cx="485.72" cy="79" r="4" fill="#0960bd"/><circle cx="485.72" cy="46" r="4" fill="none" stroke="#fff" stroke-miterlimit="10"/><line x1="485.72" y1="42" x2="485.72" y2="20" stroke="#0960bd" stroke-miterlimit="10" stroke-width="2"/><line x1="485.72" y1="79" x2="485.72" y2="50.13" stroke="#0960bd" stroke-miterlimit="10" stroke-width="2"/><circle cx="485.72" cy="79" r="4" fill="#0960bd"/><circle cx="485.72" cy="46" r="4" fill="none" stroke="#fff" stroke-miterlimit="10"/><line x1="485.72" y1="279" x2="485.72" y2="310" stroke="#0960bd" stroke-miterlimit="10" stroke-width="2"/><line x1="485.72" y1="251" x2="485.72" y2="279.87" stroke="#0960bd" stroke-miterlimit="10" stroke-width="2"/><circle cx="485.72" cy="251" r="4" fill="#0960bd"/><line x1="305.72" y1="168.5" x2="274.22" y2="168.5" stroke="#0960bd" stroke-miterlimit="10" stroke-width="2"/><line x1="333.22" y1="168.5" x2="304.35" y2="168.5" stroke="#0960bd" stroke-miterlimit="10" stroke-width="2"/><circle cx="333.22" cy="168.5" r="4" fill="#0960bd"/><g opacity="0.1"><rect x="408.22" y="435.5" width="156" height="3"/></g><g opacity="0.7"><path d="M293.48,566.06H221.08l1-8.14c20.46-18.37,33.69-67.31,33.69-67.31a6.78,6.78,0,0,0-.87.18c-12,2.42-20.54,7.35-26.51,13.28l2.54-21.66c37.8-8.14,52.79-58.14,52.79-58.14-24.12,5.35-39.16,13.63-48.5,21.49l3.72-31.82c25.56,8.77,52-37.82,52-37.82l-1-.21.5-.32-.76.27c-28.25-6.09-43.35,10.06-48.25,16.77l.37-3.12q-1.12,3-2.18,5.88h0l0,.08q-3,8.13-5.49,16.06l0,0h0q-2.17,6.77-4.06,13.4l0-.06s-1.17-28.46-31.18-35.95c0,0,3.15,62.07,26.93,51.91h0c-2.2,9-4,17.66-5.56,26.07h0q-1.49,8.21-2.6,16l-.14.16.14-.12-.06.41v0h0q-1,7.07-1.7,13.78c.46-8.62-1.11-33.52-30.45-56.92,0,0-39,68.54,27.5,82,.15.13.3.26.44.38l-.1-.31.6.13.27-3.52a369.39,369.39,0,0,0,.23,44.1h0c.07,1,.14,2,.21,2.95H141.37c-27.94,57.79,15.52,89.46,15.52,89.46h120C323.49,596.66,293.48,566.06,293.48,566.06Zm-78-65.68h0v0Z" transform="translate(-114.28 -199)" fill="url(#fe76f7c7-2126-4e48-920d-21143a22d340)"/></g><path d="M217,588s-19-83,23-190" transform="translate(-114.28 -199)" fill="none" stroke="#535461" stroke-miterlimit="10" stroke-width="3" opacity="0.6"/><path d="M143,563H290s29,37-16,92H158S116,617,143,563Z" transform="translate(-114.28 -199)" fill="#0960bd"/><path d="M237.89,403.5s14.61-26,49.61-18c0,0-28.93,49.26-55,33.13Z" transform="translate(-114.28 -199)" fill="#4db6ac"/><path d="M228.63,431.09S227.5,404.5,198.5,397.5c0,0,3,58,26,48.5Z" transform="translate(-114.28 -199)" fill="#4db6ac"/><path d="M219.15,470.36s5.35-27.86,61.35-39.86c0,0-17.86,57.62-63.93,55.31Z" transform="translate(-114.28 -199)" fill="#4db6ac"/><path d="M214.61,501.63s5.89-29.13-29.11-56.13c0,0-38,64.67,27.48,76.83Z" transform="translate(-114.28 -199)" fill="#4db6ac"/><path d="M213.56,541.67S209.5,500.5,253.5,492.5c0,0-16.07,57.49-40,67.74Z" transform="translate(-114.28 -199)" fill="#4db6ac"/><path d="M233,419s38-29,54-34" transform="translate(-114.28 -199)" fill="none" stroke="#535461" stroke-miterlimit="10" opacity="0.3"/><path d="M216.5,485.5s46-49,64-55" transform="translate(-114.28 -199)" fill="none" stroke="#535461" stroke-miterlimit="10" opacity="0.3"/><path d="M198.5,397.5s28,38,26,48" transform="translate(-114.28 -199)" fill="none" stroke="#535461" stroke-miterlimit="10" opacity="0.3"/><path d="M185.5,445.5s15,68,27,77" transform="translate(-114.28 -199)" fill="none" stroke="#535461" stroke-miterlimit="10" opacity="0.3"/><path d="M213.5,560.5s24-66,40-68" transform="translate(-114.28 -199)" fill="none" stroke="#535461" stroke-miterlimit="10" opacity="0.3"/><g opacity="0.1"><path d="M290,563H143c-.33.67-.65,1.34-1,2H285s28.29,36.11-14.4,90H274C319,600,290,563,290,563Z" transform="translate(-114.28 -199)"/></g><rect y="455.6" width="971.44" height="32.93" fill="#e0e0e0"/><rect x="41.16" y="488.53" width="889.11" height="13.47" fill="#e0e0e0"/><rect x="41.16" y="488.53" width="889.11" height="4.49" opacity="0.1"/><line x1="690.22" y1="168.5" x2="696.22" y2="168.5" stroke="#0960bd" stroke-miterlimit="10" stroke-width="2"/><line x1="637.22" y1="168.5" x2="682.1" y2="168.5" stroke="#0960bd" stroke-miterlimit="10" stroke-width="2"/><circle cx="637.22" cy="168.5" r="4" fill="#0960bd"/><circle cx="686.22" cy="168.5" r="4" fill="none" stroke="#fff" stroke-miterlimit="10"/><g opacity="0.7"><path d="M1027,643.88l.1-.15q.31-.48.61-1l.11-.19q.29-.49.55-1l.09-.17c.2-.39.39-.78.56-1.19h0a23.79,23.79,0,0,0,.94-2.51l.1-.33c.09-.31.18-.62.26-.93l.1-.44q.1-.42.18-.85c0-.16.06-.32.09-.48s.09-.56.13-.85,0-.33.06-.49.06-.61.08-.92c0-.14,0-.29,0-.43,0-.45,0-.91,0-1.36V548h-13.85V507.52h-17V548H988.39V489.86h-17V548H965V481.55h-17V548H933V630.6c0,13.48,11.21,24.4,25,24.4H1006a25.19,25.19,0,0,0,20.24-10.06l0,0Q1026.61,644.41,1027,643.88Z" transform="translate(-114.28 -199)" fill="url(#2cf89a04-5a05-413b-983a-d2bc296cbb5e)"/></g><rect x="835.72" y="321" width="16" height="100" fill="#535461"/><rect x="835.72" y="288" width="16" height="33" fill="#3ad29f"/><rect x="857.72" y="329" width="16" height="100" fill="#535461"/><rect x="857.72" y="296" width="16" height="33" fill="#4d8af0"/><rect x="884.72" y="346" width="16" height="100" fill="#535461"/><rect x="884.72" y="313" width="16" height="33" fill="#f55f44"/><path d="M821.72,352h92a0,0,0,0,1,0,0v79.5a23.5,23.5,0,0,1-23.5,23.5h-45a23.5,23.5,0,0,1-23.5-23.5V352A0,0,0,0,1,821.72,352Z" fill="#0960bd"/><g opacity="0.1"><path d="M936,551v4h88v79.5a23.39,23.39,0,0,1-5,14.49,23.45,23.45,0,0,0,9-18.49V551Z" transform="translate(-114.28 -199)"/></g></svg>
\ No newline at end of file
...@@ -10,8 +10,13 @@ ...@@ -10,8 +10,13 @@
> >
<img src="../../../assets/images/logo.png" /> <img src="../../../assets/images/logo.png" />
<div <div
class="ml-2 truncate xs:opacity-0 md:opacity-100" class="ml-2 truncate md:opacity-100"
:class="`${prefixCls}__title`" :class="[
`${prefixCls}__title`,
{
'xs:opacity-0': !alwaysShowTitle,
},
]"
v-show="showTitle" v-show="showTitle"
> >
{{ title }} {{ title }}
...@@ -38,6 +43,7 @@ ...@@ -38,6 +43,7 @@
theme: propTypes.oneOf(['light', 'dark']), theme: propTypes.oneOf(['light', 'dark']),
// Whether to show title // Whether to show title
showTitle: propTypes.bool.def(true), showTitle: propTypes.bool.def(true),
alwaysShowTitle: propTypes.bool.def(false),
}, },
setup() { setup() {
const { prefixCls } = useDesign('app-logo'); const { prefixCls } = useDesign('app-logo');
......
import CountButton from './src/CountButton.vue';
import CountdownInput from './src/CountdownInput.vue';
export { CountdownInput, CountButton };
<template>
<Button v-bind="$attrs" :disabled="isStart" @click="handleStart" :loading="loading">
{{
!isStart
? t('component.countdown.normalText')
: t('component.countdown.sendText', [currentCount])
}}
</Button>
</template>
<script lang="ts">
import { defineComponent, ref, PropType } from 'vue';
import { Button } from 'ant-design-vue';
import { useCountdown } from './useCountdown';
import { isFunction } from '/@/utils/is';
import { useI18n } from '/@/hooks/web/useI18n';
export default defineComponent({
name: 'CountButton',
components: { Button },
props: {
count: {
type: Number,
default: 60,
},
beforeStartFunc: {
type: Function as PropType<() => boolean>,
default: null,
},
},
setup(props) {
const loading = ref(false);
const { currentCount, isStart, start } = useCountdown(props.count);
const { t } = useI18n();
/**
* @description: Judge whether there is an external function before execution, and decide whether to start after execution
*/
async function handleStart() {
const { beforeStartFunc } = props;
if (beforeStartFunc && isFunction(beforeStartFunc)) {
loading.value = true;
try {
const canStart = await beforeStartFunc();
canStart && start();
} finally {
loading.value = false;
}
} else {
start();
}
}
return { handleStart, isStart, currentCount, loading, t };
},
});
</script>
<template>
<div :class="prefixCls">
<AInput v-bind="$attrs" :size="size" v-model:value="state">
<template #addonAfter>
<CountButton :size="size" :count="count" :beforeStartFunc="sendCodeApi" />
</template>
</AInput>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import { Input } from 'ant-design-vue';
import CountButton from './CountButton.vue';
import { propTypes } from '/@/utils/propTypes';
import { useDesign } from '/@/hooks/web/useDesign';
import { useRuleFormItem } from '/@/hooks/component/useFormItem';
export default defineComponent({
name: 'CountDownInput',
components: { [Input.name]: Input, CountButton },
props: {
value: propTypes.string,
size: propTypes.oneOf(['default', 'large', 'small']),
count: propTypes.number.def(60),
sendCodeApi: {
type: Function as PropType<() => boolean>,
default: null,
},
},
setup(props) {
const { prefixCls } = useDesign('countdown-input');
const [state] = useRuleFormItem(props);
return { prefixCls, state };
},
});
</script>
<style lang="less">
@prefix-cls: ~'@{namespace}-countdown-input';
.@{prefix-cls} {
.ant-input-group-addon {
padding-right: 0;
background-color: transparent;
border: none;
button {
font-size: 14px;
}
}
}
</style>
import { ref, unref } from 'vue';
import { tryOnUnmounted } from '/@/utils/helper/vueHelper';
export function useCountdown(count: number) {
const currentCount = ref(count);
const isStart = ref(false);
let timerId: ReturnType<typeof setInterval> | null;
function clear() {
timerId && window.clearInterval(timerId);
}
function stop() {
isStart.value = false;
timerId = null;
clear();
}
function start() {
if (unref(isStart) || !!timerId) {
return;
}
isStart.value = true;
timerId = setInterval(() => {
if (unref(currentCount) === 1) {
stop();
currentCount.value = count;
} else {
currentCount.value -= 1;
}
}, 1000);
}
function reset() {
currentCount.value = count;
stop();
}
function restart() {
reset();
start();
}
tryOnUnmounted(() => {
reset();
});
return { start, reset, restart, clear, stop, currentCount, isStart };
}
<template> <template>
<div :class="prefixCls"> <div :class="prefixCls" class="relative">
<InputPassword <InputPassword
v-if="showInput" v-if="showInput"
v-bind="$attrs" v-bind="$attrs"
...@@ -24,15 +24,14 @@ ...@@ -24,15 +24,14 @@
import { Input } from 'ant-design-vue'; import { Input } from 'ant-design-vue';
import zxcvbn from '@zxcvbn-ts/core'; import zxcvbn from '@zxcvbn-ts/core';
import { propTypes } from '/@/utils/propTypes';
import { useDesign } from '/@/hooks/web/useDesign'; import { useDesign } from '/@/hooks/web/useDesign';
import { propTypes } from '/@/utils/propTypes';
export default defineComponent({ export default defineComponent({
name: 'StrengthMeter', name: 'StrengthMeter',
components: { InputPassword: Input.Password }, components: { InputPassword: Input.Password },
props: { props: {
value: propTypes.string, value: propTypes.string,
showInput: propTypes.bool.def(true), showInput: propTypes.bool.def(true),
disabled: propTypes.bool, disabled: propTypes.bool,
}, },
...@@ -43,9 +42,9 @@ ...@@ -43,9 +42,9 @@
const getPasswordStrength = computed(() => { const getPasswordStrength = computed(() => {
const { disabled } = props; const { disabled } = props;
if (disabled) return null; if (disabled) return -1;
const innerValue = unref(innerValueRef); const innerValue = unref(innerValueRef);
const score = innerValue ? zxcvbn(unref(innerValueRef)).score : null; const score = innerValue ? zxcvbn(unref(innerValueRef)).score : -1;
emit('score-change', score); emit('score-change', score);
return score; return score;
}); });
...@@ -57,6 +56,7 @@ ...@@ -57,6 +56,7 @@
watchEffect(() => { watchEffect(() => {
innerValueRef.value = props.value || ''; innerValueRef.value = props.value || '';
}); });
watch( watch(
() => unref(innerValueRef), () => unref(innerValueRef),
(val) => { (val) => {
...@@ -77,14 +77,12 @@ ...@@ -77,14 +77,12 @@
@prefix-cls: ~'@{namespace}-strength-meter'; @prefix-cls: ~'@{namespace}-strength-meter';
.@{prefix-cls} { .@{prefix-cls} {
position: relative;
&-bar { &-bar {
position: relative; position: relative;
height: 4px; height: 6px;
margin: 10px auto 6px; margin: 10px auto 6px;
background: @disabled-color; background: @disabled-color;
border-radius: 3px; border-radius: 6px;
&::before, &::before,
&::after { &::after {
......
...@@ -13,10 +13,8 @@ ...@@ -13,10 +13,8 @@
} }
} }
body { span.anticon:not(.app-iconify) {
.anticon:not(.app-iconify) { vertical-align: 0.125em;
vertical-align: 0.1em;
}
} }
.ant-back-top { .ant-back-top {
......
...@@ -2,37 +2,17 @@ ...@@ -2,37 +2,17 @@
// ==============屏幕断点============ // ==============屏幕断点============
// ================================= // =================================
// Extra small screen / phone
@screen-xs: 480px;
@screen-xs-min: @screen-xs;
// Small screen / tablet // Small screen / tablet
@screen-sm: 576px; @screen-sm: 640px;
@screen-sm-min: @screen-sm;
// Medium screen / desktop // Medium screen / desktop
@screen-md: 768px; @screen-md: 768px;
@screen-md-min: @screen-md;
// Large screen / wide desktop // Large screen / wide desktop
@screen-lg: 992px; @screen-lg: 1024px;
@screen-lg-min: @screen-lg;
// Extra large screen / full hd // Extra large screen / full hd
@screen-xl: 1200px; @screen-xl: 1280px;
@screen-xl-min: @screen-xl;
// Extra extra large screen / large desktop // Extra extra large screen / large desktop
@screen-xxl: 1600px; @screen-2xl: 1536px;
@screen-xxl-min: @screen-xxl;
@screen-xxxl: 1900px;
@screen-xxxl-min: @screen-xxxl;
// provide a maximum
@screen-xs-max: (@screen-sm-min - 1px);
@screen-sm-max: (@screen-md-min - 1px);
@screen-md-max: (@screen-lg-min - 1px);
@screen-lg-max: (@screen-xl-min - 1px);
@screen-xl-max: (@screen-xxl-min - 1px);
@screen-xxl-max: (@screen-xxxl-min - 1px);
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
<span :class="[prefixCls, `${prefixCls}--${theme}`]"> <span :class="[prefixCls, `${prefixCls}--${theme}`]">
<img :class="`${prefixCls}__header`" :src="headerImg" /> <img :class="`${prefixCls}__header`" :src="headerImg" />
<span :class="`${prefixCls}__info`"> <span :class="`${prefixCls}__info`">
<span :class="`${prefixCls}__name anticon`">{{ getUserInfo.realName }}</span> <span :class="`${prefixCls}__name`" class="truncate">{{ getUserInfo.realName }}</span>
</span> </span>
</span> </span>
......
export default {
normalText: 'Get SMS code',
sendText: 'Reacquire in {0}s',
};
export default { export default {
loginButton: 'Login', backSignIn: 'Back sign in',
autoLogin: 'AutoLogin', mobileSignInFormTitle: 'Mobile sign in',
forgetPassword: 'Forget Password', qrSignInFormTitle: 'Qr code sign in',
signInFormTitle: 'Sign in',
signUpFormTitle: 'Sign up',
forgetFormTitle: 'Reset password',
signInTitle: 'Backstage management system',
signInDesc: 'Enter your personal details and get started!',
policy: 'I agree to the xxx Privacy Policy',
scanSign: `scanning the code to complete the login`,
loginButton: 'Sign in',
registerButton: 'Sign up',
rememberMe: 'Remember me',
forgetPassword: 'Forget Password?',
otherSignIn: 'Sign in with',
// notify // notify
loginSuccessTitle: 'Login successful', loginSuccessTitle: 'Login successful',
loginSuccessDesc: 'Welcome back', loginSuccessDesc: 'Welcome back',
// placeholder // placeholder
accountPlaceholder: 'Please input Username', accountPlaceholder: 'Please input username',
passwordPlaceholder: 'Please input Password', passwordPlaceholder: 'Please input password',
smsPlaceholder: 'Please input sms code',
mobilePlaceholder: 'Please input mobile',
policyPlaceholder: 'Register after checking',
diffPwd: 'The two passwords are inconsistent',
userName: 'Username',
password: 'Password',
confirmPassword: 'Confirm Password',
email: 'Email',
smsCode: 'SMS code',
mobile: 'Mobile',
}; };
export default {
normalText: '获取验证码',
sendText: '{0}秒后重新获取',
};
export default { export default {
backSignIn: '返回',
signInFormTitle: '登录',
mobileSignInFormTitle: '手机登录',
qrSignInFormTitle: '二维码登录',
signUpFormTitle: '注册',
forgetFormTitle: '重置密码',
signInTitle: '开箱即用的中后台管理系统',
signInDesc: '输入您的个人详细信息开始使用!',
policy: '我同意xxx隐私政策',
scanSign: `扫码后点击"确认",即可完成登录`,
loginButton: '登录', loginButton: '登录',
autoLogin: '自动登录', registerButton: '注册',
forgetPassword: '忘记密码', rememberMe: '记住我',
forgetPassword: '忘记密码?',
otherSignIn: '其他登录方式',
// notify // notify
loginSuccessTitle: '登录成功', loginSuccessTitle: '登录成功',
...@@ -10,4 +24,15 @@ export default { ...@@ -10,4 +24,15 @@ export default {
// placeholder // placeholder
accountPlaceholder: '请输入账号', accountPlaceholder: '请输入账号',
passwordPlaceholder: '请输入密码', passwordPlaceholder: '请输入密码',
smsPlaceholder: '请输入验证码',
mobilePlaceholder: '请输入手机号码',
policyPlaceholder: '勾选后才能注册',
diffPwd: '两次输入密码不一致',
userName: '账号',
password: '密码',
confirmPassword: '确认密码',
email: '邮箱',
smsCode: '短信验证码',
mobile: '手机号码',
}; };
<template> <template>
<div :class="prefixCls"> <div
<div :class="`${prefixCls}__unlock`" @click="handleShowForm(false)" v-show="showDate"> :class="prefixCls"
class="fixed inset-0 flex h-screen w-screen bg-black items-center justify-center"
>
<div
:class="`${prefixCls}__unlock`"
class="absolute top-0 left-1/2 flex pt-5 h-16 items-center justify-center sm:text-md xl:text-xl text-white flex-col cursor-pointer transform translate-x-1/2"
@click="handleShowForm(false)"
v-show="showDate"
>
<LockOutlined /> <LockOutlined />
<span>{{ t('sys.lock.unlock') }}</span> <span>{{ t('sys.lock.unlock') }}</span>
</div> </div>
<div :class="`${prefixCls}__date`"> <div class="flex w-screen h-screen justify-center items-center">
<div :class="`${prefixCls}__hour`"> <div :class="`${prefixCls}__hour`" class="relative mr-5 md:mr-20 w-2/5 h-2/5 md:h-4/5">
{{ hour }} <span>{{ hour }}</span>
<span class="meridiem" v-show="showDate">{{ meridiem }}</span> <span class="meridiem absolute left-5 top-5 text-md xl:text-xl" v-show="showDate">
{{ meridiem }}
</span>
</div> </div>
<div :class="`${prefixCls}__minute`"> <div :class="`${prefixCls}__minute w-2/5 h-2/5 md:h-4/5 `">
{{ minute }} <span> {{ minute }}</span>
</div> </div>
</div> </div>
<transition name="fade-slide"> <transition name="fade-slide">
<div :class="`${prefixCls}-entry`" v-show="!showDate"> <div :class="`${prefixCls}-entry`" v-show="!showDate">
<div :class="`${prefixCls}-entry-content`"> <div :class="`${prefixCls}-entry-content`">
<div :class="`${prefixCls}-entry__header`"> <div :class="`${prefixCls}-entry__header enter-x`">
<img :src="headerImg" :class="`${prefixCls}-entry__header-img`" /> <img :src="headerImg" :class="`${prefixCls}-entry__header-img`" />
<p :class="`${prefixCls}-entry__header-name`"> <p :class="`${prefixCls}-entry__header-name`">
{{ realName }} {{ realName }}
</p> </p>
</div> </div>
<InputPassword :placeholder="t('sys.lock.placeholder')" v-model:value="password" /> <InputPassword
<span :class="`${prefixCls}-entry__err-msg`" v-if="errMsgRef"> :placeholder="t('sys.lock.placeholder')"
class="enter-x"
v-model:value="password"
/>
<span :class="`${prefixCls}-entry__err-msg enter-x`" v-if="errMsgRef">
{{ t('sys.lock.alert') }} {{ t('sys.lock.alert') }}
</span> </span>
<div :class="`${prefixCls}-entry__footer`"> <div :class="`${prefixCls}-entry__footer enter-x`">
<a-button <a-button
type="link" type="link"
size="small" size="small"
class="mt-2 mr-2" class="mt-2 mr-2 enter-x"
:disabled="loadingRef" :disabled="loadingRef"
@click="handleShowForm(true)" @click="handleShowForm(true)"
> >
...@@ -40,7 +54,7 @@ ...@@ -40,7 +54,7 @@
<a-button <a-button
type="link" type="link"
size="small" size="small"
class="mt-2 mr-2" class="mt-2 mr-2 enter-x"
:disabled="loadingRef" :disabled="loadingRef"
@click="goLogin" @click="goLogin"
> >
...@@ -54,11 +68,11 @@ ...@@ -54,11 +68,11 @@
</div> </div>
</transition> </transition>
<div :class="`${prefixCls}__footer-date`"> <div class="absolute bottom-5 w-full text-gray-300 xl:text-xl 2xl:text-3xl text-center enter-y">
<div class="time" v-show="!showDate"> <div class="text-5xl mb-4 enter-x" v-show="!showDate">
{{ hour }}:{{ minute }} <span class="meridiem">{{ meridiem }}</span> {{ hour }}:{{ minute }} <span class="text-3xl">{{ meridiem }}</span>
</div> </div>
<div class="date"> {{ year }}/{{ month }}/{{ day }} {{ week }} </div> <div class="text-2xl"> {{ year }}/{{ month }}/{{ day }} {{ week }} </div>
</div> </div>
</div> </div>
</template> </template>
...@@ -144,125 +158,54 @@ ...@@ -144,125 +158,54 @@
@prefix-cls: ~'@{namespace}-lock-page'; @prefix-cls: ~'@{namespace}-lock-page';
.@{prefix-cls} { .@{prefix-cls} {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: @lock-page-z-index; z-index: @lock-page-z-index;
display: flex;
width: 100vw;
height: 100vh;
// background: rgba(23, 27, 41);
background: #000;
align-items: center;
justify-content: center;
&__unlock { &__unlock {
position: absolute;
top: 0;
left: 50%;
display: flex;
height: 50px;
padding-top: 20px;
font-size: 18px;
color: #fff;
cursor: pointer;
transform: translate(-50%, 0); transform: translate(-50%, 0);
flex-direction: column;
align-items: center;
justify-content: space-between;
transition: all 0.3s;
}
&__date {
display: flex;
width: 100vw;
height: 100vh;
align-items: center;
justify-content: center;
}
&__hour {
position: relative;
margin-right: 80px;
.meridiem {
position: absolute;
top: 20px;
left: 20px;
font-size: 26px;
}
@media (max-width: @screen-xs) {
margin-right: 20px;
}
} }
&__hour, &__hour,
&__minute { &__minute {
display: flex; display: flex;
width: 40%;
height: 74%;
font-weight: 700; font-weight: 700;
color: #bababa; color: #bababa;
background: #141313; background: #141313;
border-radius: 30px; border-radius: 30px;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@media (min-width: @screen-xxxl-min) {
font-size: 46em;
}
@media (min-width: @screen-xl-max) and (max-width: @screen-xxl-max) {
font-size: 38em;
}
@media (min-width: @screen-lg-max) and (max-width: @screen-xl-max) { @media screen and (max-width: @screen-md) {
font-size: 30em; span:not(.meridiem) {
} font-size: 160px;
@media (min-width: @screen-md-max) and (max-width: @screen-lg-max) { }
font-size: 23em;
} }
@media (min-width: @screen-sm-max) and (max-width: @screen-md-max) {
height: 50%;
font-size: 12em;
border-radius: 10px;
.meridiem { @media screen and (min-width: @screen-md) {
font-size: 20px; span:not(.meridiem) {
font-size: 160px;
} }
} }
@media (min-width: @screen-xs-max) and (max-width: @screen-sm-max) {
font-size: 13em;
}
@media (max-width: @screen-xs) {
height: 30%;
font-size: 5em;
border-radius: 10px;
.meridiem { @media screen and (max-width: @screen-sm) {
font-size: 14px; span:not(.meridiem) {
font-size: 90px;
} }
} }
} @media screen and (min-width: @screen-lg) {
span:not(.meridiem) {
&__footer-date { font-size: 220px;
position: absolute;
bottom: 20px;
width: 100%;
font-family: helvetica;
color: #bababa;
text-align: center;
.time {
font-size: 50px;
.meridiem {
font-size: 32px;
} }
} }
.date { @media screen and (min-width: @screen-xl) {
font-size: 26px; span:not(.meridiem) {
font-size: 260px;
}
}
@media screen and (min-width: @screen-2xl) {
span:not(.meridiem) {
font-size: 320px;
}
} }
} }
......
<template>
<Form class="p-4" :model="formData" :rules="getFormRules" ref="formRef">
<FormItem name="account" class="enter-x">
<Input size="large" v-model:value="formData.account" :placeholder="t('sys.login.userName')" />
</FormItem>
<FormItem name="mobile" class="enter-x">
<Input size="large" v-model:value="formData.mobile" :placeholder="t('sys.login.mobile')" />
</FormItem>
<FormItem name="sms" class="enter-x">
<CountdownInput
size="large"
v-model:value="formData.sms"
:placeholder="t('sys.login.smsCode')"
/>
</FormItem>
<FormItem class="enter-x">
<Button
type="primary"
size="large"
block
@click="handleReset"
:loading="loading"
class="enter-x"
>
{{ t('common.resetText') }}
</Button>
<Button size="large" block class="mt-4 enter-x" @click="handleBackLogin">
{{ t('sys.login.backSignIn') }}
</Button>
</FormItem>
</Form>
</template>
<script lang="ts">
import { defineComponent, reactive, ref } from 'vue';
import { Form, Input, Button } from 'ant-design-vue';
import { CountdownInput } from '/@/components/CountDown';
import { useI18n } from '/@/hooks/web/useI18n';
import { LoginStateEnum, useLoginState, useFormRules, useFormValid } from './useLogin';
export default defineComponent({
name: 'ForgetPasswordForm',
components: {
Button,
Form,
FormItem: Form.Item,
Input,
CountdownInput,
},
setup() {
const { t } = useI18n();
const { setLoginState } = useLoginState();
const { getFormRules } = useFormRules();
const formRef = ref<any>(null);
const loading = ref(false);
const formData = reactive({
account: '',
mobile: '',
sms: '',
});
const { validForm } = useFormValid(formRef);
async function handleReset() {
const data = await validForm();
if (!data) return;
console.log(data);
}
function handleBackLogin() {
setLoginState(LoginStateEnum.LOGIN);
}
return {
t,
formRef,
formData,
getFormRules,
handleReset,
loading,
handleBackLogin,
};
},
});
</script>
<template> <template>
<div class="login"> <div :class="prefixCls" class="relative w-full h-full px-4">
<div class="opacity-0 login-mask lg:opacity-100"></div> <AppLocalePicker
<div class="justify-center login-form-wrap lg:justify-end"> class="absolute top-4 right-4 enter-x text-white xl:text-gray-600"
<div class="mx-6 login-form"> :showText="false"
<AppLocalePicker v-if="showLocale" class="login-form__locale" /> />
<div class="px-2 py-10 login-form__content">
<header> <span class="-enter-x xl:hidden">
<img :src="logo" class="mr-4" /> <AppLogo :alwaysShowTitle="true" />
<h1>{{ title }}</h1> </span>
</header>
<div class="container relative h-full py-2 mx-auto sm:px-10">
<a-form class="login-form__main" :model="formData" :rules="formRules" ref="formRef"> <div class="flex h-full">
<a-form-item name="account"> <div class="hidden xl:flex xl:flex-col xl:w-6/12 min-h-full mr-4 pl-4">
<a-input size="large" v-model:value="formData.account" placeholder="username: vben" /> <AppLogo class="-enter-x" />
</a-form-item> <div class="my-auto">
<a-form-item name="password"> <img
<a-input-password :alt="title"
size="large" src="../../../assets/svg/login-box-bg.svg"
visibilityToggle class="w-1/2 -mt-16 -enter-x"
v-model:value="formData.password" />
placeholder="password: 123456" <div class="mt-10 font-medium text-white -enter-x">
/> <span class="mt-4 text-3xl inline-block"> {{ t('sys.login.signInTitle') }}</span>
</a-form-item> </div>
<div class="mt-5 text-md text-white font-normal dark:text-gray-500 -enter-x">
<a-row> {{ t('sys.login.signInDesc') }}
<a-col :span="12"> </div>
<a-form-item> </div>
<!-- No logic, you need to deal with it yourself --> </div>
<a-checkbox v-model:checked="autoLogin" size="small">{{ <div class="h-full xl:h-auto flex py-5 xl:py-0 xl:my-0 w-full xl:w-6/12">
t('sys.login.autoLogin') <div
}}</a-checkbox> class="my-auto mx-auto xl:ml-20 bg-white xl:bg-transparent px-5 py-8 sm:px-8 xl:p-0 rounded-md shadow-md xl:shadow-none w-full sm:w-3/4 lg:w-2/4 xl:w-auto enter-x relative"
</a-form-item> >
</a-col> <h2 class="font-bold text-2xl xl:text-3xl enter-x text-center xl:text-left mb-6">
<a-col :span="12"> {{ getFormTitle }}
<a-form-item :style="{ 'text-align': 'right' }"> </h2>
<!-- No logic, you need to deal with it yourself --> <LoginForm v-show="getShowLogin" />
<a-button type="link" size="small"> <ForgetPasswordForm v-if="getShowResetPassword" />
{{ t('sys.login.forgetPassword') }} <RegisterForm v-if="getShowRegister" />
</a-button> <MobileForm v-if="getShowMobile" />
</a-form-item> <QrCodeForm v-if="getShowQrCode" />
</a-col> </div>
</a-row>
<a-form-item>
<a-button
type="primary"
size="large"
class="rounded-sm"
:block="true"
@click="login"
:loading="formState.loading"
>
{{ t('sys.login.loginButton') }}
</a-button>
</a-form-item>
</a-form>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, reactive, ref, unref, toRaw } from 'vue'; import { defineComponent, computed } from 'vue';
import { Checkbox, Form, Input, Row, Col } from 'ant-design-vue';
import { Button } from '/@/components/Button'; import { AppLogo } from '/@/components/Application';
import { AppLocalePicker } from '/@/components/Application'; import { AppLocalePicker } from '/@/components/Application';
import LoginForm from './LoginForm.vue';
import ForgetPasswordForm from './ForgetPasswordForm.vue';
import RegisterForm from './RegisterForm.vue';
import MobileForm from './MobileForm.vue';
import QrCodeForm from './QrCodeForm.vue';
import { userStore } from '/@/store/modules/user';
import { useMessage } from '/@/hooks/web/useMessage';
import { useGlobSetting, useProjectSetting } from '/@/hooks/setting'; import { useGlobSetting, useProjectSetting } from '/@/hooks/setting';
import logo from '/@/assets/images/logo.png';
import { useI18n } from '/@/hooks/web/useI18n'; import { useI18n } from '/@/hooks/web/useI18n';
import { useDesign } from '/@/hooks/web/useDesign';
import { useShowLoginForm, useFormTitle } from './useLogin';
export default defineComponent({ export default defineComponent({
name: 'Login',
components: { components: {
[Checkbox.name]: Checkbox, AppLogo,
[Form.name]: Form, LoginForm,
[Form.Item.name]: Form.Item, ForgetPasswordForm,
[Input.name]: Input, RegisterForm,
[Input.Password.name]: Input.Password, MobileForm,
AButton: Button, QrCodeForm,
AppLocalePicker, AppLocalePicker,
[Row.name]: Row,
[Col.name]: Col,
}, },
setup() { setup() {
const formRef = ref<any>(null);
const autoLoginRef = ref(false);
const globSetting = useGlobSetting(); const globSetting = useGlobSetting();
const { getFormTitle } = useFormTitle();
const { prefixCls } = useDesign('login');
const { locale } = useProjectSetting(); const { locale } = useProjectSetting();
const { notification } = useMessage();
const { t } = useI18n(); const { t } = useI18n();
const formData = reactive({
account: 'vben',
password: '123456',
});
const formState = reactive({
loading: false,
});
const formRules = reactive({
account: [{ required: true, message: t('sys.login.accountPlaceholder'), trigger: 'blur' }],
password: [
{ required: true, message: t('sys.login.passwordPlaceholder'), trigger: 'blur' },
],
});
async function handleLogin() {
const form = unref(formRef);
if (!form) return;
formState.loading = true;
try {
const data = await form.validate();
const userInfo = await userStore.login(
toRaw({
password: data.password,
username: data.account,
})
);
if (userInfo) {
notification.success({
message: t('sys.login.loginSuccessTitle'),
description: `${t('sys.login.loginSuccessDesc')}: ${userInfo.realName}`,
duration: 3,
});
}
} catch (error) {
} finally {
formState.loading = false;
}
}
return { return {
formRef,
formData,
formState,
formRules,
login: handleLogin,
autoLogin: autoLoginRef,
title: globSetting && globSetting.title,
logo,
t, t,
showLocale: locale.show, prefixCls,
title: computed(() => globSetting?.title ?? ''),
showLocale: computed(() => locale.show),
getFormTitle,
...useShowLoginForm(),
}; };
}, },
}); });
</script> </script>
<style lang="less"> <style lang="less">
.login-form__locale { @prefix-cls: ~'@{namespace}-login';
position: absolute; @logo-prefix-cls: ~'@{namespace}-app-logo';
top: 14px; @countdown-prefix-cls: ~'@{namespace}-countdown-input';
right: 14px;
z-index: 1;
}
.login { .@{prefix-cls} {
position: relative; @media (max-width: @screen-xl) {
height: 100vh; background: linear-gradient(180deg, #1c3faa, #1c3faa);
background: url(../../../assets/images/login/login-bg.png) no-repeat; }
background-size: 100% 100%;
&-mask { &::before {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%; height: 100%;
background: url(../../../assets/images/login/login-in.png) no-repeat; margin-left: -48%;
background-position: 30% 30%; background-image: url(/@/assets/svg/login-bg.svg);
background-size: 80% 80%; background-position: 100%;
background-repeat: no-repeat;
background-size: auto 100%;
content: '';
@media (max-width: @screen-xl) {
display: none;
}
} }
&-form { .@{logo-prefix-cls} {
position: relative; position: absolute;
bottom: 60px; top: 12px;
width: 400px; height: 30px;
background: @white;
border: 10px solid rgba(255, 255, 255, 0.5); &__title {
border-width: 8px; font-size: 16px;
border-radius: 4px; color: #fff;
background-clip: padding-box;
&__main {
margin: 30px auto 0 auto !important;
} }
&-wrap { img {
position: absolute; width: 32px;
top: 0;
right: 0;
display: flex;
width: 100%;
height: 100%;
align-items: center;
} }
}
.container {
.@{logo-prefix-cls} {
display: flex;
width: 60%;
height: 80px;
&__title {
font-size: 24px;
color: #fff;
}
&__content { img {
position: relative; width: 48px;
width: 100%;
height: 100%;
padding: 60px 0 40px 0;
border: 1px solid #999;
border-radius: 2px;
header {
display: flex;
justify-content: center;
align-items: center;
img {
display: inline-block;
width: 48px;
}
h1 {
margin-bottom: 0;
font-size: 24px;
text-align: center;
}
} }
}
}
&-sign-in-way {
.anticon {
font-size: 22px;
color: #888;
cursor: pointer;
form { &:hover {
width: 80%; color: @primary-color;
} }
} }
} }
input:not([type='checkbox']) {
min-width: 360px;
@media (max-width: @screen-sm) {
min-width: 240px;
}
}
.@{countdown-prefix-cls} input {
min-width: unset;
}
.ant-divider-inner-text {
font-size: 12px;
color: @text-color-secondary;
}
} }
</style> </style>
<template>
<Form class="p-4" :model="formData" :rules="getFormRules" ref="formRef">
<FormItem name="account" class="enter-x">
<Input size="large" v-model:value="formData.account" :placeholder="t('sys.login.userName')" />
</FormItem>
<FormItem name="password" class="enter-x">
<InputPassword
size="large"
visibilityToggle
v-model:value="formData.password"
:placeholder="t('sys.login.password')"
/>
</FormItem>
<ARow class="enter-x">
<ACol :span="12">
<FormItem>
<!-- No logic, you need to deal with it yourself -->
<Checkbox v-model:checked="rememberMe" size="small">
{{ t('sys.login.rememberMe') }}
</Checkbox>
</FormItem>
</ACol>
<ACol :span="12">
<FormItem :style="{ 'text-align': 'right' }">
<!-- No logic, you need to deal with it yourself -->
<Button type="link" size="small" @click="setLoginState(LoginStateEnum.RESET_PASSWORD)">
{{ t('sys.login.forgetPassword') }}
</Button>
</FormItem>
</ACol>
</ARow>
<FormItem class="enter-x">
<Button
type="primary"
size="large"
block
@click="handleLogin"
:loading="loading"
class="enter-x"
>
{{ t('sys.login.loginButton') }}
</Button>
<!-- <Button size="large" class="mt-4 enter-x" block @click="handleRegister">
{{ t('sys.login.registerButton') }}
</Button> -->
</FormItem>
<ARow class="enter-x">
<ACol :span="7">
<Button block @click="setLoginState(LoginStateEnum.MOBILE)">
{{ t('sys.login.mobileSignInFormTitle') }}
</Button>
</ACol>
<ACol :span="8" :offset="1">
<Button block @click="setLoginState(LoginStateEnum.QR_CODE)">
{{ t('sys.login.qrSignInFormTitle') }}
</Button>
</ACol>
<ACol :span="7" :offset="1">
<Button block @click="setLoginState(LoginStateEnum.REGISTER)">
{{ t('sys.login.registerButton') }}
</Button>
</ACol>
</ARow>
<Divider>{{ t('sys.login.otherSignIn') }}</Divider>
<div class="flex justify-evenly enter-x" :class="`${prefixCls}-sign-in-way`">
<GithubFilled />
<WechatFilled />
<AlipayCircleFilled />
<GoogleCircleFilled />
<TwitterCircleFilled />
</div>
</Form>
</template>
<script lang="ts">
import { defineComponent, reactive, ref, toRaw } from 'vue';
import { Checkbox, Form, Input, Row, Col, Button, Divider } from 'ant-design-vue';
import {
GithubFilled,
WechatFilled,
AlipayCircleFilled,
GoogleCircleFilled,
TwitterCircleFilled,
} from '@ant-design/icons-vue';
import { useI18n } from '/@/hooks/web/useI18n';
import { useMessage } from '/@/hooks/web/useMessage';
import { userStore } from '/@/store/modules/user';
import { LoginStateEnum, useLoginState, useFormRules, useFormValid } from './useLogin';
import { useDesign } from '/@/hooks/web/useDesign';
export default defineComponent({
name: 'LoginForm',
components: {
Checkbox,
Button,
Form,
FormItem: Form.Item,
Input,
Divider,
InputPassword: Input.Password,
[Col.name]: Col,
[Row.name]: Row,
GithubFilled,
WechatFilled,
AlipayCircleFilled,
GoogleCircleFilled,
TwitterCircleFilled,
},
setup() {
const { t } = useI18n();
const { notification } = useMessage();
const { prefixCls } = useDesign('login');
const { setLoginState } = useLoginState();
const { getFormRules } = useFormRules();
const formRef = ref<any>(null);
const loading = ref(false);
const rememberMe = ref(false);
const formData = reactive({
account: 'vben',
password: '123456',
});
const { validForm } = useFormValid(formRef);
async function handleLogin() {
const data = await validForm();
if (!data) return;
try {
loading.value = true;
const userInfo = await userStore.login(
toRaw({
password: data.password,
username: data.account,
})
);
if (userInfo) {
notification.success({
message: t('sys.login.loginSuccessTitle'),
description: `${t('sys.login.loginSuccessDesc')}: ${userInfo.realName}`,
duration: 3,
});
}
} finally {
loading.value = false;
}
}
return {
t,
prefixCls,
formRef,
formData,
getFormRules,
rememberMe,
handleLogin,
loading,
setLoginState,
LoginStateEnum,
};
},
});
</script>
<template>
<Form class="p-4" :model="formData" :rules="getFormRules" ref="formRef">
<FormItem name="mobile" class="enter-x">
<Input size="large" v-model:value="formData.mobile" :placeholder="t('sys.login.mobile')" />
</FormItem>
<FormItem name="sms" class="enter-x">
<CountdownInput
size="large"
v-model:value="formData.sms"
:placeholder="t('sys.login.smsCode')"
/>
</FormItem>
<FormItem class="enter-x">
<Button
type="primary"
size="large"
block
@click="handleLogin"
:loading="loading"
class="enter-x"
>
{{ t('sys.login.loginButton') }}
</Button>
<Button size="large" block class="mt-4 enter-x" @click="handleBackLogin">
{{ t('sys.login.backSignIn') }}
</Button>
</FormItem>
</Form>
</template>
<script lang="ts">
import { defineComponent, reactive, ref } from 'vue';
import { Form, Input, Button } from 'ant-design-vue';
import { CountdownInput } from '/@/components/CountDown';
import { useI18n } from '/@/hooks/web/useI18n';
import { LoginStateEnum, useLoginState, useFormRules, useFormValid } from './useLogin';
export default defineComponent({
name: 'MobileForm',
components: {
Button,
Form,
FormItem: Form.Item,
Input,
CountdownInput,
},
setup() {
const { t } = useI18n();
const { setLoginState } = useLoginState();
const { getFormRules } = useFormRules();
const formRef = ref<any>(null);
const loading = ref(false);
const formData = reactive({
mobile: '',
sms: '',
});
const { validForm } = useFormValid(formRef);
async function handleLogin() {
const data = await validForm();
if (!data) return;
console.log(data);
}
function handleBackLogin() {
setLoginState(LoginStateEnum.LOGIN);
}
return {
t,
formRef,
formData,
getFormRules,
handleLogin,
loading,
handleBackLogin,
};
},
});
</script>
<template>
<div class="enter-x min-w-64 min-h-64">
<QrCode :value="qrCodeUrl" class="enter-x flex justify-center xl:justify-start" :width="280" />
<Divider>{{ t('sys.login.scanSign') }}</Divider>
<Button size="large" block class="mt-4 enter-x" @click="handleBackLogin">
{{ t('sys.login.backSignIn') }}
</Button>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { Button, Divider } from 'ant-design-vue';
import { useI18n } from '/@/hooks/web/useI18n';
import { LoginStateEnum, useLoginState } from './useLogin';
import { QrCode } from '/@/components/Qrcode/index';
const qrCodeUrl = 'https://vvbin.cn/next/login';
export default defineComponent({
name: 'QrCodeForm',
components: {
Button,
QrCode,
Divider,
},
setup() {
const { t } = useI18n();
const { setLoginState } = useLoginState();
function handleBackLogin() {
setLoginState(LoginStateEnum.LOGIN);
}
return {
t,
handleBackLogin,
qrCodeUrl,
};
},
});
</script>
<template>
<Form class="p-4" :model="formData" :rules="getFormRules" ref="formRef">
<FormItem name="account" class="enter-x">
<Input size="large" v-model:value="formData.account" :placeholder="t('sys.login.userName')" />
</FormItem>
<FormItem name="mobile" class="enter-x">
<Input size="large" v-model:value="formData.mobile" :placeholder="t('sys.login.mobile')" />
</FormItem>
<FormItem name="sms" class="enter-x">
<CountdownInput
size="large"
v-model:value="formData.sms"
:placeholder="t('sys.login.smsCode')"
/>
</FormItem>
<FormItem name="password" class="enter-x">
<StrengthMeter
size="large"
v-model:value="formData.password"
:placeholder="t('sys.login.password')"
/>
</FormItem>
<FormItem name="confirmPassword" class="enter-x">
<InputPassword
size="large"
visibilityToggle
v-model:value="formData.confirmPassword"
:placeholder="t('sys.login.confirmPassword')"
/>
</FormItem>
<FormItem class="enter-x" name="policy">
<!-- No logic, you need to deal with it yourself -->
<Checkbox v-model:checked="formData.policy" size="small">
{{ t('sys.login.policy') }}
</Checkbox>
</FormItem>
<Button
type="primary"
size="large"
block
@click="handleReset"
:loading="loading"
class="enter-x"
>
{{ t('sys.login.registerButton') }}
</Button>
<Button size="large" block class="mt-4 enter-x" @click="handleBackLogin">
{{ t('sys.login.backSignIn') }}
</Button>
</Form>
</template>
<script lang="ts">
import { defineComponent, reactive, ref } from 'vue';
import { Form, Input, Button, Divider, Checkbox } from 'ant-design-vue';
import { StrengthMeter } from '/@/components/StrengthMeter';
import { CountdownInput } from '/@/components/CountDown';
import { useI18n } from '/@/hooks/web/useI18n';
import { LoginStateEnum, useLoginState, useFormRules, useFormValid } from './useLogin';
export default defineComponent({
name: 'RegisterPasswordForm',
components: {
Button,
Form,
FormItem: Form.Item,
Input,
Divider,
InputPassword: Input.Password,
Checkbox,
StrengthMeter,
CountdownInput,
},
setup() {
const { t } = useI18n();
const { setLoginState } = useLoginState();
const formRef = ref<any>(null);
const loading = ref(false);
const formData = reactive({
account: '',
password: '',
confirmPassword: '',
mobile: '',
sms: '',
policy: false,
});
const { getFormRules } = useFormRules(formData);
const { validForm } = useFormValid(formRef);
async function handleReset() {
const data = await validForm();
if (!data) return;
console.log(data);
}
function handleBackLogin() {
setLoginState(LoginStateEnum.LOGIN);
}
return {
t,
formRef,
formData,
getFormRules,
handleReset,
loading,
handleBackLogin,
};
},
});
</script>
import { RuleObject } from 'ant-design-vue/lib/form/interface';
import { ref, computed, unref, Ref } from 'vue';
import { useI18n } from '/@/hooks/web/useI18n';
export enum LoginStateEnum {
LOGIN,
REGISTER,
RESET_PASSWORD,
MOBILE,
QR_CODE,
}
const currentState = ref(LoginStateEnum.LOGIN);
export function useFormTitle() {
const { t } = useI18n();
const getFormTitle = computed(() => {
const titleObj = {
[LoginStateEnum.RESET_PASSWORD]: t('sys.login.forgetFormTitle'),
[LoginStateEnum.LOGIN]: t('sys.login.signInFormTitle'),
[LoginStateEnum.REGISTER]: t('sys.login.signUpFormTitle'),
[LoginStateEnum.MOBILE]: t('sys.login.mobileSignInFormTitle'),
[LoginStateEnum.QR_CODE]: t('sys.login.qrSignInFormTitle'),
};
return titleObj[unref(currentState)];
});
return { getFormTitle };
}
export function useLoginState() {
function setLoginState(state: LoginStateEnum) {
currentState.value = state;
}
const getLoginState = computed(() => currentState.value);
return { setLoginState, getLoginState };
}
export function useShowLoginForm() {
const getShowLogin = computed(() => unref(currentState) === LoginStateEnum.LOGIN);
const getShowResetPassword = computed(
() => unref(currentState) === LoginStateEnum.RESET_PASSWORD
);
const getShowRegister = computed(() => unref(currentState) === LoginStateEnum.REGISTER);
const getShowMobile = computed(() => unref(currentState) === LoginStateEnum.MOBILE);
const getShowQrCode = computed(() => unref(currentState) === LoginStateEnum.QR_CODE);
return { getShowLogin, getShowResetPassword, getShowRegister, getShowMobile, getShowQrCode };
}
export function useFormValid<T extends Object = any>(formRef: Ref<any>) {
async function validForm() {
const form = unref(formRef);
if (!form) return;
const data = await form.validate();
return data as T;
}
return { validForm };
}
export function useFormRules(formData?: Recordable) {
const { t } = useI18n();
const getAccountFormRule = computed(() => createRule(t('sys.login.accountPlaceholder')));
const getPasswordFormRule = computed(() => createRule(t('sys.login.passwordPlaceholder')));
const getSmsFormRule = computed(() => createRule(t('sys.login.smsPlaceholder')));
const getMobileFormRule = computed(() => createRule(t('sys.login.mobilePlaceholder')));
const validatePolicy = async (_: RuleObject, value: boolean) => {
return !value ? Promise.reject(t('sys.login.policyPlaceholder')) : Promise.resolve();
};
const validateConfirmPassword = (password: string) => {
return async (_: RuleObject, value: string) => {
if (!value) {
return Promise.reject(t('sys.login.passwordPlaceholder'));
}
if (value !== password) {
return Promise.reject(t('sys.login.diffPwd'));
}
return Promise.resolve();
};
};
const getFormRules = computed(() => {
const accountFormRule = unref(getAccountFormRule);
const passwordFormRule = unref(getPasswordFormRule);
const smsFormRule = unref(getSmsFormRule);
const mobileFormRule = unref(getMobileFormRule);
const mobileRule = {
sms: smsFormRule,
mobile: mobileFormRule,
};
switch (unref(currentState)) {
case LoginStateEnum.REGISTER:
return {
account: accountFormRule,
password: passwordFormRule,
confirmPassword: [
{ validator: validateConfirmPassword(formData?.password), trigger: 'change' },
],
policy: [{ validator: validatePolicy, trigger: 'change' }],
...mobileRule,
};
case LoginStateEnum.RESET_PASSWORD:
return {
account: accountFormRule,
...mobileRule,
};
case LoginStateEnum.MOBILE:
return mobileRule;
default:
return {
account: accountFormRule,
password: passwordFormRule,
};
}
});
return { getFormRules };
}
function createRule(message: string) {
return [
{
required: true,
message,
trigger: 'change',
},
];
}
/* eslint-disable @typescript-eslint/no-var-requires */
const colors = require('windicss/colors');
const defaultTheme = require('windicss/defaultTheme');
module.exports = {
darkMode: 'class',
plugins: [
require('windicss/plugin/forms'),
require('windicss/plugin/typography'),
require('windicss/plugin/line-clamp'),
require('windicss/plugin/aspect-ratio'),
],
theme: {
extend: {
colors,
fontFamily: {
sans: ['Righteous', ...defaultTheme.fontFamily.sans],
},
},
},
};
import lineClamp from 'windicss/plugin/line-clamp';
import colors from 'windicss/colors';
import { defineConfig } from 'vite-plugin-windicss';
export default defineConfig({
darkMode: 'class',
plugins: [lineClamp, createEnterPlugin()],
theme: {
extend: {
colors,
},
// screen: {
// sm: '576px',
// md: '768px',
// lg: '992px',
// xl: '1200px',
// '2xl': '1600px',
// },
},
});
/**
* Used for animation when the element is displayed
* @param maxOutput The larger the maxOutput output, the larger the generated css volume
*/
function createEnterPlugin(maxOutput = 10) {
const createCss = (index: number, d = 'x') => {
const upd = d.toUpperCase();
return {
[`*> .enter-${d}:nth-child(${index})`]: {
transform: `translate${upd}(50px)`,
},
[`*> .-enter-${d}:nth-child(${index})`]: {
transform: `translate${upd}(-50px)`,
},
[`* > .enter-${d}:nth-child(${index}),* > .-enter-${d}:nth-child(${index})`]: {
'z-index': `${10 - index}`,
opacity: '0',
animation: `enter-${d}-animation 0.4s ease-in-out 0.3s`,
'animation-fill-mode': 'forwards',
'animation-delay': `${(index * 1) / 10}s`,
},
};
};
const handler = ({ addBase }) => {
for (let index = 1; index < maxOutput; index++) {
addBase({
...createCss(index, 'x'),
...createCss(index, 'y'),
});
}
addBase({
[`@keyframes enter-x-animation`]: {
to: {
opacity: '1',
transform: 'translateX(0)',
},
},
[`@keyframes enter-y-animation`]: {
to: {
opacity: '1',
transform: 'translateY(0)',
},
},
});
};
return { handler };
}
...@@ -1936,14 +1936,16 @@ ...@@ -1936,14 +1936,16 @@
dependencies: dependencies:
vue-demi latest vue-demi latest
"@windicss/plugin-utils@0.3.12": "@windicss/plugin-utils@0.4.3":
version "0.3.12" version "0.4.3"
resolved "https://registry.npmjs.org/@windicss/plugin-utils/-/plugin-utils-0.3.12.tgz#69b55be1ffb45753e6f01aa236f5ecd8df7a92ee" resolved "https://registry.npmjs.org/@windicss/plugin-utils/-/plugin-utils-0.4.3.tgz#84e85fd3cd6eaf54ca72cae276f9cf0610f45e56"
integrity sha512-XA+xeyu5KM322dIp+EEHeXnAPuK+KxuWyoGvJnxXi9U50nIp0QraqXAH7xl9ghIkVHvVrb8pmm8vHpzFvsqF2A== integrity sha512-ilddLED+sZQIA9rOwE5eYwdBEBWKREvAVkkiAOOTNf7oDcP/a1cxT3f/nE4tgfhz+MC/FKcy7NkfrqfXRdEQaQ==
dependencies: dependencies:
esbuild "^0.8.49"
esbuild-register "^2.0.0"
fast-glob "^3.2.5" fast-glob "^3.2.5"
micromatch "^4.0.2" micromatch "^4.0.2"
windicss "^2.1.12" windicss "^2.1.15"
"@zxcvbn-ts/core@^0.2.0": "@zxcvbn-ts/core@^0.2.0":
version "0.2.0" version "0.2.0"
...@@ -3870,6 +3872,11 @@ esbuild@^0.8.48: ...@@ -3870,6 +3872,11 @@ esbuild@^0.8.48:
resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.8.48.tgz#a57e4dde84ec56da1c6ecaefee97e9da6c5b00b5" resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.8.48.tgz#a57e4dde84ec56da1c6ecaefee97e9da6c5b00b5"
integrity sha512-lrH8lA8wWQ6Lpe1z6C7ZZaFSmRsUlcQAqe16nf7ITySQ7MV4+vI7qAqQlT/u+c3+9AL3VXmT4MXTxV2e63pO4A== integrity sha512-lrH8lA8wWQ6Lpe1z6C7ZZaFSmRsUlcQAqe16nf7ITySQ7MV4+vI7qAqQlT/u+c3+9AL3VXmT4MXTxV2e63pO4A==
esbuild@^0.8.49:
version "0.8.49"
resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.8.49.tgz#3d33f71b3966611f822cf4c838115f3fbd16def2"
integrity sha512-itiFVYv5UZz4NooO7/Y0bRGVDGz/M/cxKbl6zyNI5pnKaz1mZjvZXAFhhDVz6rGCmcdTKj5oag6rh8DaaSSmfQ==
escalade@^3.1.1: escalade@^3.1.1:
version "3.1.1" version "3.1.1"
resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
...@@ -8454,10 +8461,10 @@ stylelint-order@^4.1.0: ...@@ -8454,10 +8461,10 @@ stylelint-order@^4.1.0:
postcss "^7.0.31" postcss "^7.0.31"
postcss-sorting "^5.0.1" postcss-sorting "^5.0.1"
stylelint@^13.10.0: stylelint@^13.11.0:
version "13.10.0" version "13.11.0"
resolved "https://registry.npmjs.org/stylelint/-/stylelint-13.10.0.tgz#67b0c6f378c3fa61aa569a55d38feb8570b0b587" resolved "https://registry.npmjs.org/stylelint/-/stylelint-13.11.0.tgz#591981fbdd68c9d3d3e6147a0cd6a07539fc216d"
integrity sha512-eDuLrL0wzPKbl5/TbNGZcbw0lTIGbDEr5W6lCODvb1gAg0ncbgCRt7oU0C2VFDvbrcY0A3MFZOwltwTRmc0XCw== integrity sha512-DhrKSWDWGZkCiQMtU+VroXM6LWJVC8hSK24nrUngTSQvXGK75yZUq4yNpynqrxD3a/fzKMED09V+XxO4z4lTbw==
dependencies: dependencies:
"@stylelint/postcss-css-in-js" "^0.37.2" "@stylelint/postcss-css-in-js" "^0.37.2"
"@stylelint/postcss-markdown" "^0.36.2" "@stylelint/postcss-markdown" "^0.36.2"
...@@ -9262,17 +9269,17 @@ vite-plugin-purge-icons@^0.7.0: ...@@ -9262,17 +9269,17 @@ vite-plugin-purge-icons@^0.7.0:
"@purge-icons/generated" "^0.7.0" "@purge-icons/generated" "^0.7.0"
rollup-plugin-purge-icons "^0.7.0" rollup-plugin-purge-icons "^0.7.0"
vite-plugin-pwa@^0.5.1: vite-plugin-pwa@^0.5.2:
version "0.5.1" version "0.5.2"
resolved "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.5.1.tgz#7f94b8c4092ba0bba0a1bceb690e7420b18071e7" resolved "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.5.2.tgz#48131ebadc0c98c34a543dbf1bb1c86aeef532e0"
integrity sha512-hf8BgyH0XLNEJUoMsk7ywMoE+OoQelK/+4RQoftQomZhlKXgsTWrfshFGOV7sKUbLsxMh0cVoh1DmAulQmRzKQ== integrity sha512-4SHKxYhd5sCF/ebbgxGYlN91UHlylzh7C32a5+Y2c2vbrWzw5x62ZxsYzolQzBosdOim4Ez+e/dX4hmP3BCmow==
dependencies: dependencies:
debug "^4.3.2" debug "^4.3.2"
fast-glob "^3.2.5" fast-glob "^3.2.5"
pretty-bytes "^5.5.0" pretty-bytes "^5.5.0"
workbox-build "^6.1.0" workbox-build "^6.1.0"
vite-plugin-style-import@^0.7.2: vite-plugin-style-import@^0.7.3:
version "0.7.3" version "0.7.3"
resolved "https://registry.npmjs.org/vite-plugin-style-import/-/vite-plugin-style-import-0.7.3.tgz#4a9fb08bf5f2fc4796391c9be9a587ecb5c97e9e" resolved "https://registry.npmjs.org/vite-plugin-style-import/-/vite-plugin-style-import-0.7.3.tgz#4a9fb08bf5f2fc4796391c9be9a587ecb5c97e9e"
integrity sha512-oKM6vOl7iWaB5U1HcR5oM1oPRhT1n5yJt7h4h9jwpMPCD5ckHPcSjjSU7ZlOJkoS/IWEnDkQqoZi162bOs1rTQ== integrity sha512-oKM6vOl7iWaB5U1HcR5oM1oPRhT1n5yJt7h4h9jwpMPCD5ckHPcSjjSU7ZlOJkoS/IWEnDkQqoZi162bOs1rTQ==
...@@ -9295,13 +9302,13 @@ vite-plugin-theme@^0.4.3: ...@@ -9295,13 +9302,13 @@ vite-plugin-theme@^0.4.3:
es-module-lexer "^0.3.26" es-module-lexer "^0.3.26"
tinycolor2 "^1.4.2" tinycolor2 "^1.4.2"
vite-plugin-windicss@0.3.12: vite-plugin-windicss@0.4.3:
version "0.3.12" version "0.4.3"
resolved "https://registry.npmjs.org/vite-plugin-windicss/-/vite-plugin-windicss-0.3.12.tgz#5503b4ee738268a37c857c0cf55cea41f28fa3e6" resolved "https://registry.npmjs.org/vite-plugin-windicss/-/vite-plugin-windicss-0.4.3.tgz#f86e5a3b78882caa3cdd50cba2b08770e2d627c8"
integrity sha512-NuzIjSrqBQKvpbLJoU9qi8PIWBBXCqBmuLg9Dl/cFl4MB/vAHIOB6sZYJatCBFTU39Kw4UU0GhAjDBSNqzTn0w== integrity sha512-Lnv6OhcYzcJvecTs4LIpMSfo54rSewkHrW85IVwy8hacR0krY319jXr5nwiDpSTp6HM3QJhoJ4zxHF+t5Q+Nwg==
dependencies: dependencies:
"@windicss/plugin-utils" "0.3.12" "@windicss/plugin-utils" "0.4.3"
windicss "^2.1.12" windicss "^2.1.15"
vite@2.0.1: vite@2.0.1:
version "2.0.1" version "2.0.1"
...@@ -9356,10 +9363,10 @@ vue-i18n@9.0.0-rc.2: ...@@ -9356,10 +9363,10 @@ vue-i18n@9.0.0-rc.2:
"@intlify/shared" "9.0.0-rc.2" "@intlify/shared" "9.0.0-rc.2"
"@vue/devtools-api" "^6.0.0-beta.3" "@vue/devtools-api" "^6.0.0-beta.3"
vue-router@^4.0.3: vue-router@^4.0.4:
version "4.0.3" version "4.0.4"
resolved "https://registry.npmjs.org/vue-router/-/vue-router-4.0.3.tgz#8b26050c88b2dec7e27a88835f71046b365823ec" resolved "https://registry.npmjs.org/vue-router/-/vue-router-4.0.4.tgz#ad9b4b7bbdad622407b4ff189b1646f48c1e9053"
integrity sha512-AD1OjtVPyQHTSpoRsEGfPpxRQwhAhxcacOYO3zJ3KNkYP/r09mileSp6kdMQKhZWP2cFsPR3E2M3PZguSN5/ww== integrity sha512-uN6PDEaYdU9aRO7mU+Dkr1uaY49hV3fucEDG/Vre/Qj8ct3RoJS16vcPrvKVzn69zDDjBV5b9Xw7fZA9r6b/Iw==
vue-types@^3.0.0: vue-types@^3.0.0:
version "3.0.1" version "3.0.1"
...@@ -9434,10 +9441,10 @@ which@^2.0.1: ...@@ -9434,10 +9441,10 @@ which@^2.0.1:
dependencies: dependencies:
isexe "^2.0.0" isexe "^2.0.0"
windicss@^2.1.12: windicss@^2.1.15:
version "2.1.12" version "2.1.15"
resolved "https://registry.npmjs.org/windicss/-/windicss-2.1.12.tgz#840b963f03af7a3e31b989d2b51de52dcd57a37a" resolved "https://registry.npmjs.org/windicss/-/windicss-2.1.15.tgz#0a5bf1a56711ab53de8093a3c855764d93ffac00"
integrity sha512-VC057iG65zlvdqUI+1ynzOuKikalvYg6XqPGbG17HEAfwQ0sg1dACTk2plEp1QAEQNtKU3BnLnueWa4oKlltEQ== integrity sha512-gBihXNJPzv/kBaelOlXvbrmWsWuv98OPSf/yUYjc8EnRGCOxDOQIRin4FYPTWCmZi91PZThh7nMjzQZiBV+MYg==
wmf@~1.0.1: wmf@~1.0.1:
version "1.0.2" version "1.0.2"
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册