This commit is contained in:
孟帅
2023-05-14 23:55:16 +08:00
parent 1227c754d0
commit f30dbf34fa
111 changed files with 2853 additions and 1969 deletions

View File

@@ -174,4 +174,4 @@
);
</script>
<style lang="less"></style>
<style lang="less"></style>

View File

@@ -0,0 +1,64 @@
<template>
<n-form-item class="default-color">
<div class="flex view-account-other">
<div class="flex-initial">
<span>其它登录方式</span>
</div>
<div class="flex-initial mx-2">
<a @click="handleLoginWechat">
<n-icon size="24" color="rgb(24, 160, 88)">
<LogoWechat />
</n-icon>
</a>
</div>
<div class="flex-initial mx-2">
<a @click="handleLogoTiktok">
<n-icon size="24" color="rgba(25, 28, 34, 0.88)">
<LogoTiktok />
</n-icon>
</a>
</div>
<div class="flex-initial" style="margin-left: auto" v-if="userStore.loginConfig?.loginRegisterSwitch === 1">
<a @click="updateActiveModule(moduleKey)">{{ tag }}</a>
</div>
</div>
</n-form-item>
</template>
<script lang="ts" setup>
import { LogoWechat, LogoTiktok } from '@vicons/ionicons5';
import { useUserStore } from '@/store/modules/user';
import {useMessage} from "naive-ui";
const userStore = useUserStore();
interface Props {
moduleKey: string;
tag: string;
}
withDefaults(defineProps<Props>(), {
moduleKey: 'register',
tag: '注册账号',
});
const message = useMessage();
const emit = defineEmits(['updateActiveModule']);
function updateActiveModule(key: string) {
emit('updateActiveModule', key);
}
function handleLogoTiktok() {
console.log('handleLogoTiktok...');
message.info('暂未开放');
}
function handleLoginWechat() {
console.log('handleLoginWechat...');
message.info('暂未开放');
}
</script>
<style scoped></style>

View File

@@ -0,0 +1,68 @@
.view-account {
display: flex;
flex-direction: column;
height: 100vh;
overflow: auto;
&-container {
flex: 1;
padding: 32px 12px;
max-width: 384px;
min-width: 320px;
margin: 0 auto;
}
&-top {
padding: 32px 0;
text-align: center;
&-desc {
font-size: 14px;
color: #808695;
}
}
&-other {
width: 100%;
}
.default-color {
color: #515a6e;
.ant-checkbox-wrapper {
color: #515a6e;
}
}
}
@media (min-width: 768px) {
.view-account {
background-image: url('~@/assets/images/login.svg');
background-repeat: no-repeat;
background-position: 50%;
background-size: 100%;
}
.page-account-container {
padding: 32px 0 24px 0;
}
}
// ...
.justify-between {
justify-content: space-between;
}
.flex-y-center {
display: flex;
align-items: center;
}
.w-300px {
width: 300px;
}
.w-12px {
width: 12px;
}

View File

@@ -1,224 +1,96 @@
<template>
<div class="view-account">
<div class="view-account-header"></div>
<div class="view-account-container">
<div class="view-account-top">
<div class="view-account-top-logo">
<img src="~@/assets/images/account-logo.png" alt="" />
</div>
<div class="view-account-top-desc">HotGo 后台管理系统</div>
</div>
<div class="view-account-form">
<n-form
ref="formRef"
label-placement="left"
size="large"
:model="formInline"
:rules="rules"
>
<n-form-item path="username">
<n-input
@keyup.enter="handleSubmit"
v-model:value="formInline.username"
placeholder="请输入用户名"
>
<template #prefix>
<n-icon size="18" color="#808695">
<PersonOutline />
</n-icon>
</template>
</n-input>
</n-form-item>
<n-form-item path="pass">
<n-input
@keyup.enter="handleSubmit"
v-model:value="formInline.pass"
type="password"
showpassOn="click"
placeholder="请输入密码"
>
<template #prefix>
<n-icon size="18" color="#808695">
<LockClosedOutline />
</n-icon>
</template>
</n-input>
</n-form-item>
<n-form-item path="code" v-show="codeBase64 !== ''">
<n-input-group>
<n-input
:style="{ width: '100%' }"
placeholder="验证码"
@keyup.enter="handleSubmit"
v-model:value="formInline.code"
>
<template #prefix>
<n-icon size="18" color="#808695" :component="SafetyCertificateOutlined" />
</template>
<template #suffix> </template>
</n-input>
<n-loading-bar-provider
:to="loadingBarTargetRef"
container-style="position: absolute;"
>
<img
ref="loadingBarTargetRef"
style="width: 100px"
:src="codeBase64"
@click="refreshCode"
loading="lazy"
alt="点击获取"
/>
<loading-bar-trigger />
</n-loading-bar-provider>
</n-input-group>
</n-form-item>
<n-form-item class="default-color">
<div class="flex justify-between">
<div class="flex-initial">
<n-checkbox v-model:checked="autoLogin">自动登录</n-checkbox>
</div>
<div class="flex-initial order-last">
<a href="javascript:">忘记密码</a>
</div>
</div>
</n-form-item>
<n-form-item>
<n-button type="primary" @click="handleSubmit" size="large" :loading="loading" block>
登录
</n-button>
</n-form-item>
<n-form-item class="default-color">
<div class="flex view-account-other">
<div class="flex-initial">
<span>其它登录方式</span>
</div>
<div class="flex-initial mx-2">
<a href="javascript:">
<n-icon size="24" color="#2d8cf0">
<LogoWechat />
</n-icon>
</a>
</div>
<div class="flex-initial mx-2">
<a href="javascript:">
<n-icon size="24" color="#2d8cf0">
<LogoTiktok />
</n-icon>
</a>
</div>
<div class="flex-initial" style="margin-left: auto">
<a @click="handleRegister">注册账号</a>
</div>
</div>
</n-form-item>
</n-form>
</div>
<div :style="containerCSS">
<n-card :bordered="false">
<header class="justify-between">
<n-space justify="center">
<div></div>
<img src="~@/assets/images/logo.png" class="account-logo" alt="" />
<n-gradient-text type="primary" :size="26">{{ projectName }}</n-gradient-text>
<div></div>
</n-space>
</header>
<main class="pt-24px">
<div class="pt-18px">
<transition name="fade-slide" appear>
<component
:is="activeModule.component"
@updateActiveModule="handleUpdateActiveModule"
/>
</transition>
</div>
</main>
</n-card>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { ref, computed, onMounted } from 'vue';
import type { Component } from 'vue';
import LoginFrom from './login/index.vue';
import RegisterFrom from './register/index.vue';
import { useRouter } from 'vue-router';
import { useUserStore } from '@/store/modules/user';
import { useMessage, useLoadingBar } from 'naive-ui';
import { ResultEnum } from '@/enums/httpEnum';
import { PersonOutline, LockClosedOutline, LogoWechat, LogoTiktok } from '@vicons/ionicons5';
import { PageEnum } from '@/enums/pageEnum';
import { SafetyCertificateOutlined } from '@vicons/antd';
import { GetCaptcha } from '@/api/base';
import { aesEcb } from '@/utils/encrypt';
interface FormState {
username: string;
pass: string;
cid: string;
code: string;
password: string;
}
const formRef = ref();
const message = useMessage();
const loading = ref(false);
const autoLogin = ref(true);
const codeBase64 = ref('');
const loadingBar = useLoadingBar();
const loadingBarTargetRef = ref<undefined | HTMLElement>(undefined);
const LOGIN_NAME = PageEnum.BASE_LOGIN_NAME;
const formInline = ref<FormState>({
username: '',
pass: '',
cid: '',
code: '',
password: '',
});
const rules = {
username: { required: true, message: '请输入用户名', trigger: 'blur' },
pass: { required: true, message: '请输入密码', trigger: 'blur' },
code: { required: true, message: '请输入验证码', trigger: 'blur' },
};
const userStore = useUserStore();
const projectName = import.meta.env.VITE_GLOB_APP_TITLE;
interface LoginModule {
key: string;
label: string;
component: Component;
}
const router = useRouter();
const route = useRoute();
const activeModule = ref<LoginModule>({
key: 'login',
label: '账号登录',
component: LoginFrom,
});
const handleSubmit = (e) => {
e.preventDefault();
formRef.value.validate(async (errors) => {
if (!errors) {
message.loading('登录中...');
loading.value = true;
try {
const { code, message: msg } = await userStore.login({
username: formInline.value.username,
password: aesEcb.encrypt(formInline.value.pass),
cid: formInline.value.cid,
code: formInline.value.code,
});
message.destroyAll();
if (code == ResultEnum.SUCCESS) {
const toPath = decodeURIComponent((route.query?.redirect || '/') as string);
message.success('登录成功,即将进入系统');
if (route.name === LOGIN_NAME) {
await router.replace('/');
} else await router.replace(toPath);
} else {
message.info(msg || '登录失败');
await refreshCode();
}
} finally {
loading.value = false;
}
} else {
message.error('请填写完整信息,并且进行验证码校验');
}
});
};
const modules: LoginModule[] = [
{ key: 'login', label: '账号登录', component: LoginFrom },
// { key: 'register', label: '注册账号', component: RegisterFrom },
// { key: 'reset-pwd', label: '重置密码', component: ResetPwd },
// { key: 'bind-wechat', label: '绑定微信', component: BindWechat }
];
async function refreshCode() {
loadingBar.start();
const data = await GetCaptcha();
codeBase64.value = data.base64;
formInline.value.cid = data.cid;
formInline.value.code = '';
loadingBar.finish();
const containerCSS = computed(() => {
const val = document.body.clientWidth;
return val <= 720
? {}
: {
flex: `1`,
padding: `62px 12px`,
'max-width': `484px`,
'min-width': '320px',
margin: '0 auto',
};
});
function handleUpdateActiveModule(key: string) {
const findItem = modules.find((item) => item.key === key);
if (findItem) {
activeModule.value = findItem;
}
}
onMounted(() => {
setTimeout(function () {
refreshCode();
});
console.log('window.location.href',route.path);
});
//是否开放注册
if (userStore.loginConfig?.loginRegisterSwitch === 1) {
const findItem = modules.find((item) => item.key === 'register');
if (!findItem) {
modules.push({ key: 'register', label: '注册账号', component: RegisterFrom });
}
}
function handleRegister() {
message.success('即将开放,请稍后');
return;
}
const key = router.currentRoute.value.query?.scope as string;
if (key) {
handleUpdateActiveModule(key);
}
});
</script>
<style lang="less" scoped>
@@ -228,14 +100,6 @@
height: 100vh;
overflow: auto;
&-container {
flex: 1;
padding: 32px 12px;
max-width: 384px;
min-width: 320px;
margin: 0 auto;
}
&-top {
padding: 32px 0;
text-align: center;
@@ -271,4 +135,40 @@
padding: 32px 0 24px 0;
}
}
.card-tabs .n-tabs-nav--bar-type {
padding-left: 4px;
}
.pt-24px {
padding-top: 24px;
}
.pt-18px {
padding-top: 18px;
}
.text-18px {
font-size: 18px;
}
.ease-in-out {
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
.duration-300 {
transition-duration: 0.3s;
}
.transition {
transition-property: color, background-color, border-color, outline-color, text-decoration-color,
fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 0.15s;
}
.account-logo {
width: 42px;
height: 42px;
}
</style>

View File

@@ -0,0 +1,47 @@
<template>
<n-space :vertical="true">
<n-divider>演示角色登录</n-divider>
<n-space justify="center">
<n-button
v-for="item in accounts"
:key="item.username"
type="primary"
@click="login(item.username, item.password)"
>
{{ item.label }}
</n-button>
</n-space>
</n-space>
</template>
<script lang="ts" setup>
interface Emits {
(e: 'login', param: { username: string; password: string }): void;
}
const emit = defineEmits<Emits>();
const accounts = [
{
label: '超级管理员',
username: 'admin',
password: '123456',
},
{
label: '管理员',
username: 'test',
password: '123456',
},
{
label: '普通用户',
username: 'ameng',
password: '123456',
},
];
function login(username: string, password: string) {
emit('login', { username, password });
}
</script>
<style scoped></style>

View File

@@ -0,0 +1,311 @@
<template>
<n-form
ref="formRef"
label-placement="left"
size="large"
:model="mode === 'account' ? formInline : formMobile"
:rules="mode === 'account' ? rules : mobileRules"
>
<template v-if="mode === 'account'">
<n-form-item path="username">
<n-input
@keyup.enter="handleSubmit"
v-model:value="formInline.username"
placeholder="请输入用户名"
>
<template #prefix>
<n-icon size="18" color="#808695">
<PersonOutline />
</n-icon>
</template>
</n-input>
</n-form-item>
<n-form-item path="pass">
<n-input
@keyup.enter="handleSubmit"
v-model:value="formInline.pass"
type="password"
show-password-on="click"
placeholder="请输入密码"
>
<template #prefix>
<n-icon size="18" color="#808695">
<LockClosedOutline />
</n-icon>
</template>
</n-input>
</n-form-item>
<n-form-item path="code" v-show="codeBase64 !== ''">
<n-input-group>
<n-input
:style="{ width: '100%' }"
placeholder="验证码"
@keyup.enter="handleSubmit"
v-model:value="formInline.code"
>
<template #prefix>
<n-icon size="18" color="#808695" :component="SafetyCertificateOutlined" />
</template>
<template #suffix> </template>
</n-input>
<n-loading-bar-provider :to="loadingBarTargetRef" container-style="position: absolute;">
<img
ref="loadingBarTargetRef"
style="width: 100px"
:src="codeBase64"
@click="refreshCode"
loading="lazy"
alt="点击获取"
/>
<loading-bar-trigger />
</n-loading-bar-provider>
</n-input-group>
</n-form-item>
</template>
<template v-if="mode === 'mobile'">
<n-form-item path="mobile">
<n-input
@keyup.enter="handleMobileSubmit"
v-model:value="formMobile.mobile"
placeholder="请输入手机号码"
>
<template #prefix>
<n-icon size="18" color="#808695">
<MobileOutlined />
</n-icon>
</template>
</n-input>
</n-form-item>
<n-form-item path="code">
<n-input-group>
<n-input
@keyup.enter="handleMobileSubmit"
v-model:value="formMobile.code"
placeholder="请输入验证码"
>
<template #prefix>
<n-icon size="18" color="#808695" :component="SafetyCertificateOutlined" />
</template>
</n-input>
<n-button
type="primary"
ghost
@click="sendMobileCode"
:disabled="isCounting"
:loading="sendLoading"
>
{{ sendLabel }}
</n-button>
</n-input-group>
</n-form-item>
</template>
<n-space :vertical="true" :size="24">
<div class="flex-y-center justify-between">
<n-checkbox v-model:checked="autoLogin">自动登录</n-checkbox>
<n-button :text="true" @click="handleResetPassword">忘记密码</n-button>
</div>
<n-button type="primary" size="large" :block="true" :loading="loading" @click="handleLogin">
确定
</n-button>
<FormOther moduleKey="register" tag="注册账号" @updateActiveModule="updateActiveModule" />
</n-space>
<DemoAccount @login="handleDemoAccountLogin" />
</n-form>
</template>
<script lang="ts" setup>
import '../components/style.less';
import { ref, onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useUserStore } from '@/store/modules/user';
import { useMessage, useLoadingBar } from 'naive-ui';
import { ResultEnum } from '@/enums/httpEnum';
import { PersonOutline, LockClosedOutline } from '@vicons/ionicons5';
import { PageEnum } from '@/enums/pageEnum';
import { SafetyCertificateOutlined, MobileOutlined } from '@vicons/antd';
import { GetCaptcha } from '@/api/base';
import { aesEcb } from '@/utils/encrypt';
import DemoAccount from './demo-account.vue';
import FormOther from '../components/form-other.vue';
import { useSendCode } from '@/hooks/common';
import { SendSms } from '@/api/system/user';
import { validate } from '@/utils/validateUtil';
interface Props {
mode: string;
}
const props = withDefaults(defineProps<Props>(), {
mode: 'account',
});
interface FormState {
username: string;
pass: string;
cid: string;
code: string;
password: string;
}
interface FormMobileState {
mobile: string;
code: string;
}
const formRef = ref();
const message = useMessage();
const loading = ref(false);
const autoLogin = ref(true);
const codeBase64 = ref('');
const loadingBar = useLoadingBar();
const loadingBarTargetRef = ref<undefined | HTMLElement>(undefined);
const userStore = useUserStore();
const router = useRouter();
const route = useRoute();
const { sendLabel, isCounting, loading: sendLoading, activateSend } = useSendCode();
const emit = defineEmits(['updateActiveModule']);
const LOGIN_NAME = PageEnum.BASE_LOGIN_NAME;
const formInline = ref<FormState>({
username: '',
pass: '',
cid: '',
code: '',
password: '',
});
const formMobile = ref<FormMobileState>({
mobile: '',
code: '',
});
const rules = {
username: { required: true, message: '请输入用户名', trigger: 'blur' },
pass: { required: true, message: '请输入密码', trigger: 'blur' },
};
const mobileRules = {
mobile: { required: true, message: '请输入手机号码', trigger: 'blur' },
code: { required: true, message: '请输入验证码', trigger: 'blur' },
};
const handleSubmit = (e) => {
e.preventDefault();
formRef.value.validate(async (errors) => {
if (!errors) {
if (userStore.loginConfig?.loginCaptchaSwitch === 1 && formInline.value.code === '') {
message.error('请输入验证码');
return;
}
const params = {
username: formInline.value.username,
password: aesEcb.encrypt(formInline.value.pass),
cid: formInline.value.cid,
code: formInline.value.code,
};
await handleLoginResp(userStore.login(params));
} else {
message.error('请填写完整信息,并且进行验证码校验');
}
});
};
async function refreshCode() {
if (userStore.loginConfig?.loginCaptchaSwitch !== 1) {
return;
}
loadingBar.start();
const data = await GetCaptcha();
codeBase64.value = data.base64;
formInline.value.cid = data.cid;
formInline.value.code = '';
loadingBar.finish();
}
async function handleDemoAccountLogin(user: { username: string; password: string }) {
const params = {
username: user.username,
password: aesEcb.encrypt(user.password),
isLock: true,
};
await handleLoginResp(userStore.login(params));
}
const handleMobileSubmit = (e) => {
e.preventDefault();
formRef.value.validate(async (errors) => {
if (!errors) {
const params = {
mobile: formMobile.value.mobile,
code: formMobile.value.code,
};
await handleLoginResp(userStore.mobileLogin(params));
} else {
message.error('请填写完整信息,并且进行验证码校验');
}
});
};
function updateActiveModule(key: string) {
emit('updateActiveModule', key);
}
function sendMobileCode() {
validate.phone(mobileRules.mobile, formMobile.value.mobile, function (error?: Error) {
if (error === undefined) {
activateSend(SendSms({ mobile: formMobile.value.mobile, event: 'login' }));
return;
}
message.error(error.message);
});
}
function handleResetPassword() {
message.info('如果你忘记了密码,请联系管理员找回');
}
function handleLogin(e) {
if (props.mode === 'account') {
handleSubmit(e);
return;
}
handleMobileSubmit(e);
}
async function handleLoginResp(request: Promise<any>) {
message.loading('登录中...');
loading.value = true;
try {
const { code, message: msg } = await request;
message.destroyAll();
if (code == ResultEnum.SUCCESS) {
const toPath = decodeURIComponent((route.query?.redirect || '/') as string);
message.success('登录成功,即将进入系统');
if (route.name === LOGIN_NAME) {
await router.replace('/');
} else await router.replace(toPath);
} else {
message.destroyAll();
message.info(msg || '登录失败');
await refreshCode();
}
} finally {
loading.value = false;
}
}
onMounted(() => {
setTimeout(function () {
refreshCode();
});
});
</script>

View File

@@ -0,0 +1,22 @@
<template>
<n-tabs type="segment" justify-content="space-evenly">
<n-tab-pane name="account" tab="账号登录">
<Form @updateActiveModule="updateActiveModule" mode="account" />
</n-tab-pane>
<n-tab-pane name="mobile" tab="手机号登录">
<Form @updateActiveModule="updateActiveModule" mode="mobile" />
</n-tab-pane>
</n-tabs>
</template>
<script lang="ts" setup>
import Form from './form.vue';
const emit = defineEmits(['updateActiveModule']);
function updateActiveModule(key: string) {
emit('updateActiveModule', key);
}
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,53 @@
<template>
<div>
<n-checkbox v-model:checked="checked" class="text-14px">我已阅读并接受</n-checkbox>
<n-button :text="true" type="primary" @click="handleClickProtocol" class="text-13px"
>用户协议</n-button
>
<n-button :text="true" type="primary" @click="handleClickPolicy" class="text-13px"
>隐私权政策</n-button
>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
interface Props {
value?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
value: true,
});
interface Emits {
(e: 'update:value', value: boolean): void;
(e: 'click-protocol'): void;
(e: 'click-policy'): void;
}
const emit = defineEmits<Emits>();
const checked = computed({
get() {
return props.value;
},
set(newValue: boolean) {
emit('update:value', newValue);
},
});
function handleClickProtocol() {
emit('click-protocol');
}
function handleClickPolicy() {
emit('click-policy');
}
</script>
<style scoped>
.text-14px {
font-size: 14px;
}
</style>

View File

@@ -0,0 +1,278 @@
<template>
<n-form ref="formRef" label-placement="left" size="large" :model="formInline" :rules="rules">
<n-form-item path="username">
<n-input
@keyup.enter="handleSubmit"
v-model:value="formInline.username"
placeholder="请输入用户名"
>
<template #prefix>
<n-icon size="18" color="#808695">
<PersonOutline />
</n-icon>
</template>
</n-input>
</n-form-item>
<n-form-item path="pass">
<n-input
@keyup.enter="handleSubmit"
v-model:value="formInline.pass"
type="password"
placeholder="请输入密码"
show-password-on="click"
>
<template #prefix>
<n-icon size="18" color="#808695">
<LockClosedOutline />
</n-icon>
</template>
</n-input>
</n-form-item>
<n-form-item path="confirmPwd">
<n-input
@keyup.enter="handleSubmit"
v-model:value="formInline.confirmPwd"
type="password"
placeholder="再次输入密码"
show-password-on="click"
>
<template #prefix>
<n-icon size="18" color="#808695">
<LockClosedOutline />
</n-icon>
</template>
</n-input>
</n-form-item>
<n-form-item path="mobile">
<n-input
@keyup.enter="handleSubmit"
v-model:value="formInline.mobile"
placeholder="请输入手机号码"
>
<template #prefix>
<n-icon size="18" color="#808695">
<MobileOutlined />
</n-icon>
</template>
</n-input>
</n-form-item>
<n-form-item path="code">
<n-input-group>
<n-input
@keyup.enter="handleSubmit"
v-model:value="formInline.code"
placeholder="请输入验证码"
>
<template #prefix>
<n-icon size="18" color="#808695" :component="SafetyCertificateOutlined" />
</template>
</n-input>
<n-button
type="primary"
ghost
@click="sendMobileCode"
:disabled="isCounting"
:loading="sendLoading"
>
{{ sendLabel }}
</n-button>
</n-input-group>
</n-form-item>
<n-form-item path="inviteCode">
<n-input
:style="{ width: '100%' }"
placeholder="邀请码(选填)"
@keyup.enter="handleSubmit"
v-model:value="formInline.inviteCode"
:disabled="inviteCodeDisabled"
>
<template #prefix>
<n-icon size="18" color="#808695" :component="TagOutlined" />
</template>
</n-input>
</n-form-item>
<n-form-item class="default-color">
<Agreement
v-model:value="agreement"
@clickProtocol="handleClickProtocol"
@clickPolicy="handleClickPolicy"
/>
</n-form-item>
<n-form-item>
<n-button type="primary" @click="handleSubmit" size="large" :loading="loading" block>
注册
</n-button>
</n-form-item>
<FormOther moduleKey="login" tag="登录账号" @updateActiveModule="updateActiveModule" />
</n-form>
<n-modal
v-model:show="showModal"
:show-icon="false"
:mask-closable="false"
preset="dialog"
:closable="false"
:title="modalTitle"
:style="{
width: dialogWidth,
position: 'top',
bottom: '15vw',
}"
>
<div v-html="modalContent"></div>
<n-divider />
<n-space justify="center">
<n-button type="info" ghost strong @click="handleAgreement(true)">我已知晓并接受</n-button>
<n-button type="error" ghost strong @click="handleAgreement(false)">我拒绝</n-button>
</n-space>
</n-modal>
</template>
<script lang="ts" setup>
import '../components/style.less';
import { ref, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { useMessage } from 'naive-ui';
import { ResultEnum } from '@/enums/httpEnum';
import { PersonOutline, LockClosedOutline } from '@vicons/ionicons5';
import { SafetyCertificateOutlined, MobileOutlined, TagOutlined } from '@vicons/antd';
import { aesEcb } from '@/utils/encrypt';
import Agreement from './agreement.vue';
import FormOther from '../components/form-other.vue';
import { useSendCode } from '@/hooks/common';
import { validate } from '@/utils/validateUtil';
import { register, SendSms } from '@/api/system/user';
import { useUserStore } from '@/store/modules/user';
import { adaModalWidth } from '@/utils/hotgo';
interface FormState {
username: string;
pass: string;
confirmPwd: string;
mobile: string;
code: string;
inviteCode: string;
password: string;
}
const formRef = ref();
const router = useRouter();
const message = useMessage();
const userStore = useUserStore();
const loading = ref(false);
const showModal = ref(false);
const modalTitle = ref('');
const modalContent = ref('');
const { sendLabel, isCounting, loading: sendLoading, activateSend } = useSendCode();
const agreement = ref(false);
const inviteCodeDisabled = ref(false);
const dialogWidth = ref('85%');
const emit = defineEmits(['updateActiveModule']);
const formInline = ref<FormState>({
username: '',
pass: '',
confirmPwd: '',
mobile: '',
code: '',
inviteCode: '',
password: '',
});
const rules = {
username: { required: true, message: '请输入用户名', trigger: 'blur' },
pass: { required: true, message: '请输入密码', trigger: 'blur' },
mobile: { required: true, message: '请输入手机号码', trigger: 'blur' },
code: { required: true, message: '请输入验证码', trigger: 'blur' },
};
const handleSubmit = (e) => {
e.preventDefault();
formRef.value.validate(async (errors) => {
if (!errors) {
if (formInline.value.pass !== formInline.value.confirmPwd) {
message.info('两次输入的密码不一致,请检查');
return;
}
if (!agreement.value) {
message.info('请确认你已经仔细阅读并接受《用户协议》和《隐私权政策》并已勾选接受选项');
return;
}
message.loading('注册中...');
loading.value = true;
try {
const { code, message: msg } = await register({
username: formInline.value.username,
password: aesEcb.encrypt(formInline.value.pass),
mobile: formInline.value.mobile,
code: formInline.value.code,
inviteCode: formInline.value.inviteCode,
});
message.destroyAll();
if (code == ResultEnum.SUCCESS) {
message.success('注册成功,请登录!');
updateActiveModule('login');
} else {
message.info(msg || '注册失败');
}
} finally {
loading.value = false;
}
} else {
message.error('请填写完整信息,并且进行验证码校验');
}
});
};
onMounted(() => {
const inviteCode = router.currentRoute.value.query?.inviteCode as string;
if (inviteCode) {
inviteCodeDisabled.value = true;
formInline.value.inviteCode = inviteCode;
}
adaModalWidth(dialogWidth);
});
function updateActiveModule(key: string) {
emit('updateActiveModule', key);
}
function sendMobileCode() {
validate.phone(rules.mobile, formInline.value.mobile, function (error?: Error) {
if (error === undefined) {
activateSend(SendSms({ mobile: formInline.value.mobile, event: 'register' }));
return;
}
message.error(error.message);
});
}
function handleClickProtocol() {
showModal.value = true;
modalTitle.value = '用户协议';
modalContent.value = userStore.loginConfig?.loginProtocol as string;
}
function handleClickPolicy() {
showModal.value = true;
modalTitle.value = '隐私权政策';
modalContent.value = userStore.loginConfig?.loginPolicy as string;
}
function handleAgreement(agree: boolean) {
showModal.value = false;
agreement.value = agree;
}
</script>
<style lang="less" scoped></style>

View File

@@ -52,7 +52,7 @@
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
import { defineComponent } from 'vue';
import { HardwareChip, Bookmark, AppsSharp, PieChart, Analytics } from '@vicons/ionicons5';
export default defineComponent({
@@ -79,12 +79,7 @@
},
},
setup() {
// const loading = ref(true);
// setTimeout(() => {
// loading.value = false;
// }, 1000);
return {
// loading,
Bookmark,
AppsSharp,
PieChart,

View File

@@ -102,11 +102,11 @@
down: '0',
up: '0',
});
const s = ref([]);
const x = ref([]);
const s = ref<any>([]);
const x = ref<any>([]);
const sName = ref('上行宽带');
const xName = ref('下行宽带');
const months = ref([]);
const months = ref<any>([]);
const option = ref({
title: {
subtext: '单位KB',
@@ -198,14 +198,14 @@
});
const fullYearSalesChart = ref<HTMLDivElement | null>(null);
watch(props, (_newVal, _oldVal) => {
last.value = _newVal.dataModel[_newVal.dataModel.length - 1];
watch(props, (newVal, _oldVal) => {
last.value = newVal.dataModel[newVal.dataModel.length - 1];
if (months.value.length < 10) {
for (let i = 0; i < _newVal.dataModel?.length; i++) {
s.value.push(_newVal.dataModel[i].up);
x.value.push(_newVal.dataModel[i].down);
months.value.push(_newVal.dataModel[i].time);
for (let i = 0; i < newVal.dataModel?.length; i++) {
const v : any = newVal.dataModel[i]
s.value.push(v.up);
x.value.push(v.down);
months.value.push(v.time);
}
} else {
s.value.shift();

View File

@@ -19,7 +19,7 @@
},
},
setup(props) {
const data = ref([]);
const data = ref<any>([]);
const option = ref({
tooltip: {
trigger: 'item',
@@ -76,9 +76,10 @@
const orderChartWrapper = ref<HTMLDivElement | null>(null);
const init = () => {
for (let i = 0; i < props.dataModel?.length; i++) {
const v : any = props.dataModel[i]
data.value.push({
name: 'CPU分钟负载比率',
value: [props.dataModel[i]?.time, props.dataModel[i]?.ratio],
value: [v?.time, v?.ratio],
});
}
@@ -103,8 +104,8 @@
onBeforeUnmount(() => {
dispose(orderChartWrapper.value as HTMLDivElement);
});
watch(props, (_newVal, _oldVal) => {
let last = _newVal.dataModel[_newVal.dataModel.length - 1];
watch(props, (newVal, _oldVal) => {
let last : any = newVal.dataModel[newVal.dataModel.length - 1];
data.value.shift();
data.value.push({
name: 'CPU分钟负载比率',

View File

@@ -50,6 +50,20 @@
</template>
批量删除
</n-button>
<n-button
type="success"
@click="handleInviteQR(userStore.info?.inviteCode)"
class="min-left-space"
v-if="userStore.loginConfig?.loginRegisterSwitch === 1"
>
<template #icon>
<n-icon>
<QrCodeOutline />
</n-icon>
</template>
邀请注册
</n-button>
</template>
</BasicTable>
@@ -196,25 +210,44 @@
:showModal="showIntegralModal"
:formParams="formParams"
/>
<n-modal v-model:show="showQrModal" :show-icon="false" preset="dialog" title="邀请注册二维码">
<n-form class="py-4">
<div class="text-center">
<qrcode-vue :value="qrParams.qrUrl" :size="220" class="canvas" style="margin: 0 auto" />
</div>
</n-form>
<template #action>
<n-space>
<n-button @click="() => (showQrModal = false)">关闭</n-button>
</n-space>
</template>
</n-modal>
</div>
</template>
<script lang="ts" setup>
import { h, reactive, ref } from 'vue';
import { SelectOption, TreeSelectOption, useDialog, useMessage } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { ActionItem, BasicTable, TableAction } from '@/components/Table';
import { BasicForm } from '@/components/Form/index';
import { Delete, Edit, List, Status, ResetPwd } from '@/api/org/user';
import { columns } from './columns';
import { PlusOutlined, DeleteOutlined } from '@vicons/antd';
import { QrCodeOutline } from '@vicons/ionicons5';
import { sexOptions, statusOptions } from '@/enums/optionsiEnum';
import { adaModalWidth } from '@/utils/hotgo';
import { getRandomString } from '@/utils/charset';
import { cloneDeep } from 'lodash-es';
import QrcodeVue from 'qrcode.vue';
import AddBalance from './addBalance.vue';
import AddIntegral from './addIntegral.vue';
import { addNewState, addState, options, register, defaultState } from './model';
import { usePermission } from '@/hooks/web/usePermission';
import { useRouter } from 'vue-router';
import { useUserStore } from '@/store/modules/user';
import { LoginRoute } from '@/router';
interface Props {
type?: string;
@@ -233,6 +266,8 @@
};
const { hasPermission } = usePermission();
const router = useRouter();
const userStore = useUserStore();
const showIntegralModal = ref(false);
const showBalanceModal = ref(false);
const message = useMessage();
@@ -246,6 +281,11 @@
const checkedIds = ref([]);
const dialogWidth = ref('50%');
const formParams = ref<any>();
const showQrModal = ref(false);
const qrParams = ref({
name: '',
qrUrl: '',
});
const actionColumn = reactive({
width: 220,
@@ -253,6 +293,7 @@
key: 'action',
fixed: 'right',
render(record) {
const downActions = getDropDownActions(record);
return h(TableAction as any, {
style: 'button',
actions: [
@@ -289,23 +330,7 @@
auth: ['/member/delete'],
},
],
dropDownActions:
record.id === 1
? []
: [
{
label: '重置密码',
key: 0,
},
{
label: '变更余额',
key: 100,
},
{
label: '变更积分',
key: 101,
},
],
dropDownActions: downActions,
select: (key) => {
if (key === 0) {
return handleResetPwd(record);
@@ -316,11 +341,48 @@
if (key === 101) {
return handleAddIntegral(record);
}
if (key === 102) {
if (userStore.loginConfig?.loginRegisterSwitch !== 1) {
message.error('管理员暂未开启此功能');
return;
}
return handleInviteQR(record.inviteCode);
}
},
});
},
});
function getDropDownActions(record: Recordable): ActionItem[] {
if (record.id === 1) {
return [];
}
let list = [
{
label: '重置密码',
key: 0,
},
{
label: '变更余额',
key: 100,
},
{
label: '变更积分',
key: 101,
},
];
if (userStore.loginConfig?.loginRegisterSwitch === 1) {
list.push({
label: 'TA的邀请码',
key: 102,
});
}
return list;
}
function addTable() {
showModal.value = true;
formParams.value = cloneDeep(defaultState);
@@ -465,6 +527,13 @@
showIntegralModal.value = true;
formParams.value = addNewState(record as addState);
}
function handleInviteQR(code: string) {
const w = window.location;
const domain = w.protocol + '//' + w.host + w.pathname + '#';
qrParams.value.qrUrl = domain + LoginRoute.path + '?scope=register&inviteCode=' + code;
showQrModal.value = true;
}
</script>
<style lang="less" scoped></style>

View File

@@ -30,24 +30,6 @@
</template>
</n-form-item>
<n-form-item label="用户是否可注册开关" path="basicRegisterSwitch">
<n-radio-group v-model:value="formValue.basicRegisterSwitch" name="basicRegisterSwitch">
<n-space>
<n-radio :value="1">开启</n-radio>
<n-radio :value="0">关闭</n-radio>
</n-space>
</n-radio-group>
</n-form-item>
<n-form-item label="验证码开关" path="basicCaptchaSwitch">
<n-radio-group v-model:value="formValue.basicCaptchaSwitch" name="basicCaptchaSwitch">
<n-space>
<n-radio :value="1">开启</n-radio>
<n-radio :value="0">关闭</n-radio>
</n-space>
</n-radio-group>
</n-form-item>
<n-form-item label="网站开启访问" path="basicSystemOpen">
<n-switch
size="large"

View File

@@ -0,0 +1,173 @@
<template>
<div>
<n-spin :show="show" description="请稍候...">
<n-form :label-width="100" :model="formValue" :rules="rules" ref="formRef">
<n-form-item label="登录验证码开关" path="loginCaptchaSwitch">
<n-radio-group v-model:value="formValue.loginCaptchaSwitch" name="loginCaptchaSwitch">
<n-space>
<n-radio :value="1">开启</n-radio>
<n-radio :value="2">关闭</n-radio>
</n-space>
</n-radio-group>
</n-form-item>
<n-form-item label="注册开关" path="loginRegisterSwitch">
<n-radio-group v-model:value="formValue.loginRegisterSwitch" name="cashSwitch">
<n-space>
<n-radio :value="1">开启</n-radio>
<n-radio :value="2">关闭</n-radio>
</n-space>
</n-radio-group>
</n-form-item>
<n-form-item label="默认注册头像" path="loginAvatar">
<UploadImage :maxNumber="1" v-model:value="formValue.loginAvatar" />
</n-form-item>
<n-form-item label="默认注册角色" path="loginRoleId">
<n-tree-select
key-field="id"
:options="options.role"
v-model:value="formValue.loginRoleId"
:default-expand-all="true"
/>
</n-form-item>
<n-form-item label="默认注册部门" path="loginDeptId">
<n-tree-select
key-field="id"
:options="options.dept"
v-model:value="formValue.loginDeptId"
:default-expand-all="true"
/>
</n-form-item>
<n-form-item label="默认注册岗位" path="loginPostIds">
<n-select v-model:value="formValue.loginPostIds" multiple :options="options.post" />
</n-form-item>
<n-form-item label="用户协议" path="loginProtocol">
<Editor
style="height: 320px"
v-model:value="formValue.loginProtocol"
id="loginProtocol"
/>
</n-form-item>
<n-form-item label="隐私权政策" path="loginPolicy">
<Editor style="height: 320px" v-model:value="formValue.loginPolicy" id="loginPolicy" />
</n-form-item>
<div>
<n-space>
<n-button type="primary" @click="formSubmit">保存更新</n-button>
</n-space>
</div>
</n-form>
</n-spin>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { useMessage } from 'naive-ui';
import { getConfig, updateConfig } from '@/api/sys/config';
import Editor from '@/components/Editor/editor.vue';
import { getDeptOption } from '@/api/org/dept';
import { getRoleOption } from '@/api/system/role';
import { getPostOption } from '@/api/org/post';
import UploadImage from '@/components/Upload/uploadImage.vue';
const group = ref('login');
const show = ref(false);
const rules = {};
const formRef: any = ref(null);
const message = useMessage();
const formValue = ref({
loginRegisterSwitch: true,
loginCaptchaSwitch: true,
loginAvatar: '',
loginProtocol: '',
loginPolicy: '',
loginRoleId: null,
loginDeptId: null,
loginPostIds: [],
});
const options = ref<any>({
role: [],
roleTabs: [{ id: -1, name: '全部' }],
dept: [],
post: [],
});
async function loadOptions() {
const dept = await getDeptOption();
if (dept.list !== undefined) {
options.value.dept = dept.list;
}
const role = await getRoleOption();
if (role.list !== undefined) {
options.value.role = role.list;
treeDataToCompressed(role.list);
}
const post = await getPostOption();
if (post.list !== undefined && post.list.length > 0) {
for (let i = 0; i < post.list.length; i++) {
post.list[i].label = post.list[i].name;
post.list[i].value = post.list[i].id;
}
options.value.post = post.list;
}
}
function treeDataToCompressed(source) {
for (const i in source) {
options.value.roleTabs.push(source[i]);
source[i].children && source[i].children.length > 0
? treeDataToCompressed(source[i].children)
: ''; // 子级递归
}
return options.value.roleTabs;
}
function formSubmit() {
formRef.value.validate((errors) => {
if (!errors) {
updateConfig({ group: group.value, list: formValue.value })
.then((_res) => {
message.success('更新成功');
load();
})
.catch((error) => {
message.error(error.toString());
});
} else {
message.error('验证失败,请填写完整信息');
}
});
}
onMounted(async () => {
await loadOptions();
load();
});
function load() {
show.value = true;
new Promise((_resolve, _reject) => {
getConfig({ group: group.value })
.then((res) => {
show.value = false;
formValue.value = res.list;
})
.catch((error) => {
show.value = false;
message.error(error.toString());
});
});
}
</script>

View File

@@ -22,6 +22,7 @@
<RevealSetting v-if="type === 3" />
<EmailSetting v-if="type === 4" />
<SmsSetting v-if="type === 5" />
<LoginSetting v-if="type === 6" />
<CashSetting v-if="type === 7" />
<UploadSetting v-if="type === 8" />
<GeoSetting v-if="type === 9" />
@@ -44,6 +45,7 @@
import SmsSetting from './SmsSetting.vue';
import PaySetting from './PaySetting.vue';
import WechatSetting from './WechatSetting.vue';
import LoginSetting from './LoginSetting.vue';
const typeTabList = [
{
name: '基本设置',
@@ -70,11 +72,11 @@
desc: '短信验证码平台',
key: 5,
},
// {
// name: '管理员配置',
// desc: '默认设置和权限屏蔽',
// key: 6,
// },
{
name: '登录注册',
desc: '登录注册配置',
key: 6,
},
{
name: '提现配置',
desc: '管理员提现规则配置',
@@ -113,6 +115,7 @@
SmsSetting,
PaySetting,
WechatSetting,
LoginSetting,
},
setup() {
const state = reactive({