mirror of
https://github.com/bufanyun/hotgo.git
synced 2025-11-14 13:13:51 +08:00
This commit is contained in:
4
web/.env
4
web/.env
@@ -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
|
||||
|
||||
@@ -3,9 +3,6 @@
|
||||
# 网站根目录
|
||||
VITE_PUBLIC_PATH=/
|
||||
|
||||
# 是否开启mock
|
||||
VITE_USE_MOCK=false
|
||||
|
||||
# 网站前缀
|
||||
VITE_BASE_URL=/
|
||||
|
||||
|
||||
@@ -3,9 +3,6 @@
|
||||
# 网站根目录
|
||||
VITE_PUBLIC_PATH=/admin
|
||||
|
||||
# 是否开启mock
|
||||
VITE_USE_MOCK=false
|
||||
|
||||
# 网站前缀
|
||||
VITE_BASE_URL=/
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
`,
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -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,
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -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,
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -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,
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -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);
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -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);
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { http } from '@/utils/http/axios';
|
||||
|
||||
//获取table
|
||||
export function getTableList(params) {
|
||||
return http.request({
|
||||
url: '/table/list',
|
||||
method: 'get',
|
||||
params,
|
||||
});
|
||||
}
|
||||
@@ -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', // 配置信息
|
||||
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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'; // 标签页
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -174,4 +174,4 @@
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="less"></style>
|
||||
<style lang="less"></style>
|
||||
|
||||
64
web/src/views/login/components/form-other.vue
Normal file
64
web/src/views/login/components/form-other.vue
Normal 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>
|
||||
68
web/src/views/login/components/style.less
Normal file
68
web/src/views/login/components/style.less
Normal 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;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
47
web/src/views/login/login/demo-account.vue
Normal file
47
web/src/views/login/login/demo-account.vue
Normal 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>
|
||||
311
web/src/views/login/login/form.vue
Normal file
311
web/src/views/login/login/form.vue
Normal 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>
|
||||
22
web/src/views/login/login/index.vue
Normal file
22
web/src/views/login/login/index.vue
Normal 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>
|
||||
53
web/src/views/login/register/agreement.vue
Normal file
53
web/src/views/login/register/agreement.vue
Normal 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>
|
||||
278
web/src/views/login/register/index.vue
Normal file
278
web/src/views/login/register/index.vue
Normal 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>
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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分钟负载比率',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
173
web/src/views/system/config/LoginSetting.vue
Normal file
173
web/src/views/system/config/LoginSetting.vue
Normal 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>
|
||||
@@ -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({
|
||||
|
||||
3
web/types/config.d.ts
vendored
3
web/types/config.d.ts
vendored
@@ -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;
|
||||
}
|
||||
|
||||
2
web/types/global.d.ts
vendored
2
web/types/global.d.ts
vendored
@@ -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';
|
||||
|
||||
@@ -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__),
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user