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

@@ -2,10 +2,8 @@
VITE_PORT=8001
# spa-title
VITE_GLOB_APP_TITLE=HG后台管理系统
VITE_GLOB_APP_TITLE=HotGo管理系统
# spa shortname
VITE_GLOB_APP_SHORT_NAME=HG
# 生产环境 开启mock
VITE_GLOB_PROD_MOCK=false

View File

@@ -3,9 +3,6 @@
# 网站根目录
VITE_PUBLIC_PATH=/
# 是否开启mock
VITE_USE_MOCK=false
# 网站前缀
VITE_BASE_URL=/

View File

@@ -3,9 +3,6 @@
# 网站根目录
VITE_PUBLIC_PATH=/admin
# 是否开启mock
VITE_USE_MOCK=false
# 网站前缀
VITE_BASE_URL=/

View File

@@ -4,13 +4,11 @@ import { NaiveUiResolver } from 'unplugin-vue-components/resolvers';
import topLevelAwait from 'vite-plugin-top-level-await';
import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx';
import { configHtmlPlugin } from './html';
import { configMockPlugin } from './mock';
import { configCompressPlugin } from './compress';
export function createVitePlugins(viteEnv: ViteEnv, isBuild: boolean, prodMock) {
const { VITE_USE_MOCK, VITE_BUILD_COMPRESS, VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE } = viteEnv;
export function createVitePlugins(viteEnv: ViteEnv, isBuild: boolean) {
const { VITE_BUILD_COMPRESS, VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE } = viteEnv;
const vitePlugins: (Plugin | Plugin[])[] = [
// have to
@@ -36,9 +34,6 @@ export function createVitePlugins(viteEnv: ViteEnv, isBuild: boolean, prodMock)
// vite-plugin-html
vitePlugins.push(configHtmlPlugin(viteEnv, isBuild));
// vite-plugin-mock
VITE_USE_MOCK && vitePlugins.push(configMockPlugin(isBuild, prodMock));
if (isBuild) {
// rollup-plugin-gzip
vitePlugins.push(

View File

@@ -1,19 +0,0 @@
/**
* Mock plugin for development and production.
* https://github.com/anncwb/vite-plugin-mock
*/
import { viteMockServe } from 'vite-plugin-mock';
export function configMockPlugin(isBuild: boolean, prodMock: boolean) {
return viteMockServe({
ignore: /^\_/,
mockPath: 'mock',
localEnabled: !isBuild,
prodEnabled: isBuild && prodMock,
injectCode: `
import { setupProdMockServer } from '../mock/_createProductionServer';
setupProdMockServer();
`,
});
}

View File

@@ -1,18 +0,0 @@
import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer';
const modules = import.meta.globEager('./**/*.ts');
const mockModules: any[] = [];
Object.keys(modules).forEach((key) => {
if (key.includes('/_')) {
return;
}
mockModules.push(...modules[key].default);
});
/**
* Used in a production environment. Need to manually import all modules
*/
export function setupProdMockServer() {
createProdMockServer(mockModules);
}

View File

@@ -1,73 +0,0 @@
import Mock from 'mockjs';
export function resultSuccess(data, { message = 'ok' } = {}) {
return Mock.mock({
code: 0,
data,
message,
type: 'success',
});
}
export function resultPageSuccess<T = any>(
page: number,
pageSize: number,
list: T[],
{ message = 'ok' } = {}
) {
const pageData = pagination(page, pageSize, list);
return {
...resultSuccess({
page,
pageSize,
pageCount: list.length,
list: pageData,
}),
message,
};
}
export function resultError(message = 'Request failed', { code = -1, result = null } = {}) {
return {
code,
result,
message,
type: 'error',
};
}
export function pagination<T = any>(pageNo: number, pageSize: number, array: T[]): T[] {
const offset = (pageNo - 1) * Number(pageSize);
const ret =
offset + Number(pageSize) >= array.length
? array.slice(offset, array.length)
: array.slice(offset, offset + Number(pageSize));
return ret;
}
/**
* @param {Number} times 回调函数需要执行的次数
* @param {Function} callback 回调函数
*/
export function doCustomTimes(times: number, callback: any) {
let i = -1;
while (++i < times) {
callback(i);
}
}
export interface requestParams {
method: string;
body: any;
headers?: { token?: string };
query: any;
}
/**
* @description 本函数用于从request数据中获取token请根据项目的实际情况修改
*
*/
export function getRequestToken({ headers }: requestParams): string | undefined {
return headers?.token;
}

View File

@@ -1,44 +0,0 @@
import { Random } from 'mockjs';
import { resultSuccess } from '../_util';
const consoleInfo = {
//访问量
visits: {
dayVisits: Random.float(10000, 99999, 2, 2),
rise: Random.float(10, 99),
decline: Random.float(10, 99),
amount: Random.float(99999, 999999, 3, 5),
},
//销售额
saleroom: {
weekSaleroom: Random.float(10000, 99999, 2, 2),
amount: Random.float(99999, 999999, 2, 2),
degree: Random.float(10, 99),
},
//订单量
orderLarge: {
weekLarge: Random.float(10000, 99999, 2, 2),
rise: Random.float(10, 99),
decline: Random.float(10, 99),
amount: Random.float(99999, 999999, 2, 2),
},
//成交额度
volume: {
weekLarge: Random.float(10000, 99999, 2, 2),
rise: Random.float(10, 99),
decline: Random.float(10, 99),
amount: Random.float(99999, 999999, 2, 2),
},
};
export default [
//主控台 卡片数据
{
url: '/admin/dashboard/console',
timeout: 1000,
method: 'get',
response: () => {
return resultSuccess(consoleInfo);
},
},
];

View File

@@ -1,89 +0,0 @@
import { resultSuccess } from '../_util';
const menuList = () => {
const result: any[] = [
{
label: 'Dashboard',
key: 'dashboard',
type: 1,
subtitle: 'dashboard',
openType: 1,
auth: 'dashboard',
path: '/dashboard',
children: [
{
label: '主控台',
key: 'console',
type: 1,
subtitle: 'console',
openType: 1,
auth: 'console',
path: '/dashboard/console',
},
{
label: '工作台',
key: 'workplace',
type: 1,
subtitle: 'workplace',
openType: 1,
auth: 'workplace',
path: '/dashboard/workplace',
},
],
},
{
label: '表单管理',
key: 'form',
type: 1,
subtitle: 'form',
openType: 1,
auth: 'form',
path: '/form',
children: [
{
label: '基础表单',
key: 'basic-form',
type: 1,
subtitle: 'basic-form',
openType: 1,
auth: 'basic-form',
path: '/form/basic-form',
},
{
label: '分步表单',
key: 'step-form',
type: 1,
subtitle: 'step-form',
openType: 1,
auth: 'step-form',
path: '/form/step-form',
},
{
label: '表单详情',
key: 'detail',
type: 1,
subtitle: 'detail',
openType: 1,
auth: 'detail',
path: '/form/detail',
},
],
},
];
return result;
};
export default [
{
url: '/admin/menu/list',
timeout: 1000,
method: 'get',
response: () => {
const list = menuList();
return resultSuccess({
list,
});
},
},
];

View File

@@ -1,46 +0,0 @@
import { doCustomTimes, resultSuccess } from '../_util';
function getMenuKeys() {
const keys = ['dashboard', 'console', 'workplace', 'basic-form', 'step-form', 'detail'];
const newKeys = [];
doCustomTimes(parseInt(Math.random() * 6), () => {
const key = keys[Math.floor(Math.random() * keys.length)];
// @ts-ignore
newKeys.push(key);
});
return Array.from(new Set(newKeys));
}
const roleList = (pageSize) => {
const result: any[] = [];
doCustomTimes(pageSize, () => {
result.push({
id: '@integer(10,100)',
name: '@cname()',
explain: '@cname()',
isDefault: '@boolean()',
menu_keys: getMenuKeys(),
create_date: `@date('yyyy-MM-dd hh:mm:ss')`,
'status|1': ['normal', 'enable', 'disable'],
});
});
return result;
};
export default [
{
url: '/admin/role/list',
timeout: 1000,
method: 'get',
response: ({ query }) => {
const { page = 1, pageSize = 10 } = query;
const list = roleList(Number(pageSize));
return resultSuccess({
page: Number(page),
pageSize: Number(pageSize),
pageCount: 60,
list,
});
},
},
];

View File

@@ -1,40 +0,0 @@
import { Random } from 'mockjs';
import { doCustomTimes, resultSuccess } from '../_util';
const tableList = (pageSize) => {
const result: any[] = [];
doCustomTimes(pageSize, () => {
result.push({
id: '@integer(10,999999)',
beginTime: '@datetime',
endTime: '@datetime',
address: '@city()',
name: '@cname()',
avatar: Random.image('400x400', Random.color(), Random.color(), Random.first()),
date: `@date('yyyy-MM-dd')`,
time: `@time('HH:mm')`,
'no|100000-10000000': 100000,
'status|1': [true, false],
});
});
return result;
};
export default [
//表格数据列表
{
url: '/admin/table/list',
timeout: 1000,
method: 'get',
response: ({ query }) => {
const { page = 1, pageSize = 10 } = query;
const list = tableList(Number(pageSize));
return resultSuccess({
page: Number(page),
pageSize: Number(pageSize),
pageCount: 60,
list,
});
},
},
];

View File

@@ -1,53 +0,0 @@
import { resultSuccess } from '../_util';
import { ApiEnum } from '@/enums/apiEnum';
const menusList = [
{
path: '/dashboard',
name: 'Dashboard',
component: 'LAYOUT',
redirect: '/dashboard/console',
meta: {
icon: 'DashboardOutlined',
title: 'Dashboard',
},
children: [
{
path: 'console',
name: 'dashboard_console',
component: '/dashboard/console/console',
meta: {
title: '主控台',
},
},
{
path: 'monitor',
name: 'dashboard_monitor',
component: '/dashboard/monitor/monitor',
meta: {
title: '监控页',
},
},
{
path: 'workplace',
name: 'dashboard_workplace',
component: '/dashboard/workplace/workplace',
meta: {
hidden: true,
title: '工作台',
},
},
],
},
];
export default [
{
url: ApiEnum.RoleDynamic,
timeout: 1000,
method: 'get',
response: () => {
return resultSuccess(menusList);
},
},
];

View File

@@ -1,60 +0,0 @@
import Mock from 'mockjs';
import { ApiEnum } from '@/enums/apiEnum';
import { resultSuccess } from '../_util';
const Random = Mock.Random;
const token = Random.string('upper', 32, 32);
const adminInfo = {
userId: '1',
username: 'admin',
realName: 'Admin',
avatar: Random.image(),
desc: 'manager',
password: Random.string('upper', 4, 16),
token,
permissions: [
{
label: '主控台',
value: 'dashboard_console',
},
{
label: '监控页',
value: 'dashboard_monitor',
},
{
label: '工作台',
value: 'dashboard_workplace',
},
{
label: '基础列表',
value: 'basic_list',
},
{
label: '基础列表删除',
value: 'basic_list_delete',
},
],
};
export default [
{
url: ApiEnum.SiteLogin,
timeout: 1000,
method: 'post',
response: () => {
return resultSuccess({ token });
},
},
{
url: ApiEnum.MemberInfo, //ApiEnum.Prefix +
timeout: 1000,
method: 'get',
response: () => {
// const token = getRequestToken(request);
// if (!token) return resultError('Invalid token');
return resultSuccess(adminInfo);
},
},
];

View File

@@ -1,6 +1,6 @@
{
"name": "hotgo",
"version": "2.6.10",
"version": "2.7.3",
"author": {
"name": "MengShuai",
"email": "133814250@qq.com",
@@ -103,7 +103,6 @@
"vite": "^2.9.8",
"vite-plugin-compression": "^0.3.6",
"vite-plugin-html": "^2.1.2",
"vite-plugin-mock": "^2.9.6",
"vite-plugin-require-transform": "^1.0.5",
"vite-plugin-style-import": "^1.4.1",
"vite-plugin-top-level-await": "^1.2.2",

View File

@@ -77,6 +77,14 @@ export function SendBindSms() {
});
}
export function SendSms(params) {
return http.request({
url: '/sms/send',
method: 'post',
params,
});
}
export function updateMemberCash(params) {
return http.request({
url: '/member/updateCash',
@@ -85,13 +93,55 @@ export function updateMemberCash(params) {
});
}
/**
* @description: 用户登录配置
*/
export function getLoginConfig() {
return http.request<BasicResponseModel>({
url: ApiEnum.SiteLoginConfig,
method: 'get',
});
}
/**
* @description: 用户注册
*/
export function register(params) {
return http.request<BasicResponseModel>(
{
url: ApiEnum.SiteRegister,
method: 'POST',
params,
},
{
isTransformResponse: false,
}
);
}
/**
* @description: 用户登录
*/
export function login(params) {
return http.request<BasicResponseModel>(
{
url: ApiEnum.SiteLogin,
url: ApiEnum.SiteAccountLogin,
method: 'POST',
params,
},
{
isTransformResponse: false,
}
);
}
/**
* @description: 手机号登录
*/
export function mobileLogin(params) {
return http.request<BasicResponseModel>(
{
url: ApiEnum.SiteMobileLogin,
method: 'POST',
params,
},

View File

@@ -1,10 +0,0 @@
import { http } from '@/utils/http/axios';
//获取table
export function getTableList(params) {
return http.request({
url: '/table/list',
method: 'get',
params,
});
}

View File

@@ -3,7 +3,10 @@ export enum ApiEnum {
Prefix = '/api',
// 基础
SiteLogin = '/site/login', // 登录
SiteRegister = '/site/register', // 账号注册
SiteAccountLogin = '/site/accountLogin', // 账号登录
SiteMobileLogin = '/site/mobileLogin', // 手机号登录
SiteLoginConfig = '/site/loginConfig', // 登录配置
SiteLogout = '/site/logout', // 注销
SiteConfig = '/site/config', // 配置信息

View File

@@ -1,5 +1,4 @@
import type { GlobConfig } from '/#/config';
import { warn } from '@/utils/log';
import { getAppEnvConfig } from '@/utils/env';
@@ -10,7 +9,6 @@ export const useGlobSetting = (): Readonly<GlobConfig> => {
VITE_GLOB_APP_SHORT_NAME,
VITE_GLOB_API_URL_PREFIX,
VITE_GLOB_UPLOAD_URL,
VITE_GLOB_PROD_MOCK,
VITE_GLOB_IMG_URL,
} = getAppEnvConfig();
@@ -27,7 +25,6 @@ export const useGlobSetting = (): Readonly<GlobConfig> => {
shortName: VITE_GLOB_APP_SHORT_NAME,
urlPrefix: VITE_GLOB_API_URL_PREFIX,
uploadUrl: VITE_GLOB_UPLOAD_URL,
prodMock: VITE_GLOB_PROD_MOCK,
imgUrl: VITE_GLOB_IMG_URL,
};
return glob as Readonly<GlobConfig>;

View File

@@ -1,7 +1,7 @@
<template>
<div class="logo">
<img src="~@/assets/images/logo.png" alt="" :class="{ 'mr-2': !collapsed }" />
<h2 v-show="!collapsed" class="title">HG后台管理系统</h2>
<h2 v-show="!collapsed" class="title">{{ projectName }}</h2>
</div>
</template>
@@ -13,6 +13,12 @@
type: Boolean,
},
},
setup() {
const projectName = import.meta.env.VITE_GLOB_APP_TITLE;
return {
projectName,
};
},
};
</script>

View File

@@ -18,6 +18,7 @@ export function createRouterGuards(router: Router) {
router.beforeEach(async (to, from, next) => {
const Loading = window['$loading'] || null;
Loading && Loading.start();
if (from.path === LOGIN_PATH && to.name === 'errorPage') {
next(PageEnum.BASE_HOME);
return;
@@ -25,6 +26,7 @@ export function createRouterGuards(router: Router) {
// Whitelist can be directly entered
if (whitePathList.includes(to.path as PageEnum)) {
await userStore.LoadLoginConfig();
next();
return;
}
@@ -37,6 +39,7 @@ export function createRouterGuards(router: Router) {
next();
return;
}
// redirect login page
const redirectData: { path: string; replace: boolean; query?: Recordable<string> } = {
path: LOGIN_PATH,
@@ -75,6 +78,7 @@ export function createRouterGuards(router: Router) {
return;
}
await userStore.LoadLoginConfig();
await userStore.GetConfig();
const routes = await asyncRouteStore.generateRoutes(userInfo);

View File

@@ -1,9 +1,22 @@
import { defineStore } from 'pinia';
import { createStorage, storage } from '@/utils/Storage';
import { store } from '@/store';
import { ACCESS_TOKEN, CURRENT_CONFIG, CURRENT_USER, IS_LOCKSCREEN } from '@/store/mutation-types';
import {
ACCESS_TOKEN,
CURRENT_CONFIG,
CURRENT_LOGIN_CONFIG,
CURRENT_USER,
IS_LOCKSCREEN,
} from '@/store/mutation-types';
import { ResultEnum } from '@/enums/httpEnum';
import { getConfig, getUserInfo, login, logout } from '@/api/system/user';
import {
getConfig,
getLoginConfig,
getUserInfo,
login,
logout,
mobileLogin,
} from '@/api/system/user';
const Storage = createStorage({ storage: localStorage });
export interface UserInfoState {
@@ -34,6 +47,7 @@ export interface UserInfoState {
lastLoginAt: string;
lastLoginIp: string;
openId: string;
inviteCode: string;
}
export interface ConfigState {
@@ -42,6 +56,13 @@ export interface ConfigState {
wsAddr: string;
}
export interface LoginConfigState {
loginRegisterSwitch: number;
loginCaptchaSwitch: number;
loginProtocol: string;
loginPolicy: string;
}
export interface IUserState {
token: string;
username: string;
@@ -50,6 +71,7 @@ export interface IUserState {
permissions: any[];
info: UserInfoState | null;
config: ConfigState | null;
loginConfig: LoginConfigState | null;
}
export const useUserStore = defineStore({
@@ -62,6 +84,7 @@ export const useUserStore = defineStore({
permissions: [],
info: Storage.get(CURRENT_USER, null),
config: Storage.get(CURRENT_CONFIG, null),
loginConfig: Storage.get(CURRENT_LOGIN_CONFIG, null),
}),
getters: {
getToken(): string {
@@ -85,6 +108,9 @@ export const useUserStore = defineStore({
getConfig(): ConfigState | null {
return this.config;
},
getLoginConfig(): LoginConfigState | null {
return this.loginConfig;
},
},
actions: {
setToken(token: string) {
@@ -108,10 +134,20 @@ export const useUserStore = defineStore({
setConfig(config: ConfigState | null) {
this.config = config;
},
// 登录
setLoginConfig(config: LoginConfigState | null) {
this.loginConfig = config;
},
// 账号登录
async login(userInfo) {
return await this.handleLogin(login(userInfo));
},
// 手机号登录
async mobileLogin(userInfo) {
return await this.handleLogin(mobileLogin(userInfo));
},
async handleLogin(request: Promise<any>) {
try {
const response = await login(userInfo);
const response = await request;
const { data, code } = response;
if (code === ResultEnum.SUCCESS) {
const ex = 30 * 24 * 60 * 60 * 1000;
@@ -150,7 +186,7 @@ export const useUserStore = defineStore({
});
});
},
// 获取用户配置
// 获取基础配置
GetConfig() {
const that = this;
return new Promise((resolve, reject) => {
@@ -166,6 +202,22 @@ export const useUserStore = defineStore({
});
});
},
// 获取登录配置
LoadLoginConfig: function () {
const that = this;
return new Promise((resolve, reject) => {
getLoginConfig()
.then((res) => {
const result = res as unknown as LoginConfigState;
that.setLoginConfig(result);
storage.set(CURRENT_LOGIN_CONFIG, result);
resolve(res);
})
.catch((error) => {
reject(error);
});
});
},
// 登出
async logout() {
try {

View File

@@ -1,5 +1,6 @@
export const ACCESS_TOKEN = 'ACCESS-TOKEN'; // 用户token
export const CURRENT_USER = 'CURRENT-USER'; // 当前用户信息
export const CURRENT_CONFIG = 'CURRENT-CONFIG'; // 当前用户信息
export const CURRENT_CONFIG = 'CURRENT-CONFIG'; // 当前基础配置
export const CURRENT_LOGIN_CONFIG = 'CURRENT-LOGIN-CONFIG'; // 当前登录配置
export const IS_LOCKSCREEN = 'IS-LOCKSCREEN'; // 是否锁屏
export const TABS_ROUTES = 'TABS-ROUTES'; // 标签页

View File

@@ -28,7 +28,6 @@ export function getAppEnvConfig() {
VITE_GLOB_APP_SHORT_NAME,
VITE_GLOB_API_URL_PREFIX,
VITE_GLOB_UPLOAD_URL,
VITE_GLOB_PROD_MOCK,
VITE_GLOB_IMG_URL,
} = ENV;
@@ -44,7 +43,6 @@ export function getAppEnvConfig() {
VITE_GLOB_APP_SHORT_NAME,
VITE_GLOB_API_URL_PREFIX,
VITE_GLOB_UPLOAD_URL,
VITE_GLOB_PROD_MOCK,
VITE_GLOB_IMG_URL,
};
}

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({

View File

@@ -52,7 +52,6 @@ export interface GlobConfig {
shortName: string;
urlPrefix?: string;
uploadUrl?: string;
prodMock: boolean;
imgUrl?: string;
}
@@ -69,6 +68,4 @@ export interface GlobEnvConfig {
VITE_GLOB_UPLOAD_URL?: string;
//图片前缀地址
VITE_GLOB_IMG_URL?: string;
//生产环境开启mock
VITE_GLOB_PROD_MOCK: boolean;
}

View File

@@ -59,12 +59,10 @@ declare global {
declare interface ViteEnv {
VITE_PORT: number;
VITE_USE_MOCK: boolean;
VITE_PUBLIC_PATH: string;
VITE_GLOB_APP_TITLE: string;
VITE_GLOB_APP_SHORT_NAME: string;
VITE_DROP_CONSOLE: boolean;
VITE_GLOB_PROD_MOCK: boolean;
VITE_GLOB_IMG_URL: string;
VITE_PROXY: [string, string][];
VITE_BUILD_COMPRESS: 'gzip' | 'brotli' | 'none';

View File

@@ -22,9 +22,8 @@ export default ({ command, mode }: ConfigEnv): UserConfig => {
const root = process.cwd();
const env = loadEnv(mode, root);
const viteEnv = wrapperEnv(env);
const { VITE_PUBLIC_PATH, VITE_DROP_CONSOLE, VITE_PORT, VITE_GLOB_PROD_MOCK, VITE_PROXY } =
const { VITE_PUBLIC_PATH, VITE_DROP_CONSOLE, VITE_PORT, VITE_PROXY } =
viteEnv;
const prodMock = VITE_GLOB_PROD_MOCK;
const isBuild = command === 'build';
return {
base: VITE_PUBLIC_PATH,
@@ -42,7 +41,7 @@ export default ({ command, mode }: ConfigEnv): UserConfig => {
],
dedupe: ['vue'],
},
plugins: createVitePlugins(viteEnv, isBuild, prodMock),
plugins: createVitePlugins(viteEnv, isBuild),
define: {
__APP_INFO__: JSON.stringify(__APP_INFO__),
},