Compare commits

..

2 Commits

Author SHA1 Message Date
guofei 6e0d9cca5d feat: 环境区分 2024-06-05 19:24:38 +08:00
guofei 758f426b93 feat: super admin over 2024-06-05 16:39:56 +08:00
22 changed files with 1736 additions and 106 deletions

5
.umirc.dev.ts 100644
View File

@ -0,0 +1,5 @@
export default {
define: {
API_URL: 'http://127.0.0.1:3008',
},
};

5
.umirc.prod.ts 100644
View File

@ -0,0 +1,5 @@
export default {
define: {
API_URL: 'https://adseed-api.soyootech.com',
},
};

View File

@ -9,7 +9,22 @@ export default defineConfig({
layout: false,
dva: {},
valtio: {},
// history: { type: 'hash' },
plugins: [require.resolve('@umijs/plugins/dist/unocss')],
unocss: {
watch: ['src/**/*.tsx'],
},
extraPostCSSPlugins: [
require('tailwindcss')({
config: './tailwind.config.ts',
}),
],
routes: [
{
path: '/login',
component: 'Login',
layout: false,
},
{
path: '/',
id: 0,
@ -18,12 +33,5 @@ export default defineConfig({
routes: [],
},
],
proxy: {
'/api': {
target: 'http://127.0.0.1:3008',
changeOrigin: true,
pathRewrite: { '^/api': '' },
},
},
npmClient: 'pnpm',
});

1
devops/config.js 100644
View File

@ -0,0 +1 @@
exports.ossSecret = 'ck84eTxx4aSTjornlYrCy8RkurCHfc';

35
devops/deploy.js 100644
View File

@ -0,0 +1,35 @@
/* eslint-disable */
var aliOss = require('ali-oss');
var fs = require('fs');
var path = require('path');
var accessKeySecret = require('./config').ossSecret;
const adseedOssManager = new aliOss({
region: 'oss-cn-shanghai',
accessKeyId: 'LTAI5tEday8PJNaMTz5mp8g4',
accessKeySecret,
bucket: 'adseed-admin-ux',
});
const distDir = path.resolve(__dirname, '../dist');
const distFiles = traverseFiles(distDir, []);
console.log('start deploying');
Promise.all(distFiles.map((fileName) => adseedOssManager.put(fileName.slice(distDir.length), fileName)))
.then(() => console.log('deployment succeed'))
.catch((e) => console.log('deployment failed:', e));
function traverseFiles(dir, distFiles) {
const dirents = fs.readdirSync(dir, { withFileTypes: true });
for (const dirent of dirents) {
const res = `${dir}/${dirent.name}`;
const file = fs.statSync(res);
if (file.isDirectory()) {
traverseFiles(res, distFiles);
} else {
distFiles.push(res);
}
}
return distFiles;
}

View File

@ -2,8 +2,8 @@
"private": true,
"author": "guofei <guofeichu@gmail.com>",
"scripts": {
"build": "max build",
"dev": "max dev",
"build": "cross-env NODE_ENV=prod max build && node ./devops/deploy.js",
"dev": "cross-env UMI_ENV=dev max dev",
"format": "prettier --cache --write .",
"postinstall": "max setup",
"prepare": "husky",
@ -14,15 +14,23 @@
"@ant-design/icons": "^5.0.1",
"@ant-design/pro-components": "^2.7.9",
"@umijs/max": "^4.2.8",
"@umijs/plugins": "^4.2.9",
"@unocss/cli": "^0.60.4",
"antd": "^5.18.0",
"antd-style": "^3.6.2",
"axios": "^1.7.2",
"qs": "^6.12.1"
"lodash-es": "^4.17.21",
"qs": "^6.12.1",
"tailwindcss": "^3.4.3",
"unocss": "^0.60.4"
},
"devDependencies": {
"@types/lodash": "^4.17.4",
"@types/qs": "^6.9.15",
"@types/react": "^18.0.33",
"@types/react-dom": "^18.0.11",
"ali-oss": "^6.20.0",
"cross-env": "^7.0.3",
"husky": "^9",
"lint-staged": "^13.2.0",
"prettier": "^2.8.7",

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,5 @@
import { getSupderInfoApi } from '@/services/system/supderLogin';
import { history } from '@umijs/max';
import { routes as appRouters, loopMenuItem, type MenuItem } from './utils/router';
export function patchClientRoutes({ routes }: any) {
@ -10,19 +12,17 @@ export function render(oldRender: any) {
oldRender();
}
// 全局初始化数据配置,用于 Layout 用户信息和权限初始化
// 更多信息见文档https://umijs.org/docs/api/runtime-config#getinitialstate
export async function getInitialState(): Promise<{ name: string }> {
export async function getInitialState(): Promise<LoginAPI.LoginUserInfo | null> {
const { data: userInfo }: { data: LoginAPI.LoginUserInfo } = await getSupderInfoApi();
if (!userInfo) {
history.replace('/login');
return null;
}
if (userInfo && history.location.pathname === '/login') {
history.push('/');
}
return {
name: 'xx',
...userInfo,
};
}
// export const layout = () => {
// return {
// logo: 'https://img.alicdn.com/tfs/TB1YHEpwUT1gK0jSZFhXXaAtVXa-28-27.svg',
// menu: {
// locale: false,
// },
// };
// };

View File

@ -0,0 +1,91 @@
import { updatePasswordAPI, userLogoutAPI } from '@/services/system/supderLogin';
import { history, useModel } from '@umijs/max';
import { Alert, Form, Input, Modal, Spin, message } from 'antd';
import { useEffect, useState } from 'react';
export default () => {
const { initialState } = useModel('@@initialState');
const [loading, setLoading] = useState<boolean>(false);
const [formRef] = Form.useForm();
const [updatePwd, setUpdatePwd] = useState<boolean>(false);
useEffect(() => {
const isInitPwd = initialState?.isFirstLogin;
console.log(initialState?.isFirstLogin);
if (isInitPwd) {
setUpdatePwd(true);
}
}, []);
const handleUpdatePwd = async () => {
const formValues = await formRef.validateFields();
setLoading(true);
const params: LoginAPI.UpdatePassWordType = { oldPwd: '123456', newPwd: formValues.password };
const result = await updatePasswordAPI(params);
setLoading(false);
if (result.code === 200) {
message.success('密码修改成功,请重新登录');
setTimeout(() => {
userLogoutAPI();
history.replace('/login');
}, 2000);
}
};
return (
<Modal
title="提示"
open={updatePwd}
cancelButtonProps={{ style: { display: 'none' } }}
okText="确定修改"
closeIcon={null}
onOk={handleUpdatePwd}
>
<Spin spinning={loading}>
<Alert message="系统监测到您的密码为初始密码,请修改。" type="warning" style={{ marginBottom: '10px' }} />
<Form form={formRef}>
<Form.Item
name="password"
label="请输入新密码"
rules={[
{
required: true,
message: '请输入要修改的密码!',
},
{
pattern: /^[A-Za-z0-9]{6,15}$/,
message: '请输入6~15位数的密码其中包含大小写字母、数字',
},
]}
hasFeedback
>
<Input.Password />
</Form.Item>
<Form.Item
name="confirm"
label="请确认新密码"
dependencies={['password']}
hasFeedback
rules={[
{
required: true,
message: '请确定修改的密码!',
},
({ getFieldValue }) => ({
validator(_, value) {
if (!value || getFieldValue('password') === value) {
return Promise.resolve();
}
return Promise.reject(new Error('俩次密码不一样!'));
},
}),
]}
>
<Input.Password />
</Form.Item>
</Form>
</Spin>
</Modal>
);
};

View File

@ -1,4 +1,5 @@
import type { ConnectProps, Reducer } from '@umijs/max';
import { userLoginAPI } from '@/services/system/supderLogin';
import type { ConnectProps, Effect, Reducer } from '@umijs/max';
type UserModelState = {
loading: boolean;
@ -11,9 +12,9 @@ type UserModelType = {
namespace: 'user';
state: UserModelState;
effects: {
// login: Effect;
// setLogin: Effect;
// setKey: Effect;
login: Effect;
setLogin: Effect;
setKey: Effect;
};
reducers: {
save: Reducer<UserModelState>;
@ -31,14 +32,24 @@ const UserModel: UserModelType = {
loginLoading: false,
},
effects: {
// 登录
// *login({ payload }, { call, put }) {},
// *setLogin({ payload }, { put }) {
// yield put({ type: 'save', payload: { loading: payload } });
// },
// *setKey({ payload }, { put }) {
// yield put({ type: 'save', payload: { ...payload } });
// },
*login({ payload }, { call, put }) {
try {
yield put({ type: 'save', payload: { loginLoading: true } });
const result: LoginAPI.LoginResponse = yield call(userLoginAPI, payload);
localStorage.setItem('Authorization', result.data.token);
yield put({ type: 'save', payload: { loginLoading: true, isLogin: true } });
} catch {
yield put({ type: 'save', payload: { loginLoading: false, isLogin: false } });
} finally {
yield put({ type: 'save', payload: { loginLoading: false } });
}
},
*setLogin({ payload }, { put }) {
yield put({ type: 'save', payload: { loading: payload } });
},
*setKey({ payload }, { put }) {
yield put({ type: 'save', payload: { ...payload } });
},
},
reducers: {
save(state, action) {

View File

@ -1,4 +1,5 @@
import Guide from '@/components/Guide';
import UpdatePwd from '@/layouts/UpdatePwd';
import { trim } from '@/utils/format';
import { PageContainer } from '@ant-design/pro-components';
import { useModel } from '@umijs/max';
@ -8,6 +9,7 @@ const HomePage: React.FC = () => {
const { name } = useModel('global');
return (
<PageContainer ghost>
<UpdatePwd />
<div className={styles.container}>
<Guide name={trim(name)} />
</div>

View File

@ -0,0 +1,49 @@
.container {
display: flex;
flex-direction: column;
height: 100vh;
overflow: auto;
background: #f0f2f5;
}
.lang {
width: 100%;
height: 40px;
line-height: 44px;
text-align: right;
:global(.ant-dropdown-trigger) {
margin-right: 24px;
}
}
.content {
flex: 1;
padding: 32px 0;
}
@media (min-width: 768px) {
.container {
background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg');
background-repeat: no-repeat;
background-position: center 110px;
background-size: 100%;
}
.content {
padding: 32px 0 24px;
}
}
.icon {
margin-left: 8px;
color: rgba(0, 0, 0, 20%);
font-size: 24px;
vertical-align: middle;
cursor: pointer;
transition: color 0.3s;
&:hover {
color: #1890ff;
}
}

View File

@ -0,0 +1,91 @@
import type { UserConnectedProps } from '@/models/user';
import { LockOutlined, UserOutlined } from '@ant-design/icons';
import { LoginForm, ProFormInstance, ProFormText } from '@ant-design/pro-components';
import { connect } from '@umijs/max';
import { Button } from 'antd';
import { FC, Fragment, useEffect, useRef } from 'react';
import styles from './index.less';
const Login: FC<UserConnectedProps> = (props) => {
const { user, dispatch } = props;
const { loginLoading, isLogin } = user;
const formRef = useRef<ProFormInstance>();
const handleLoginSubmit = async (values: LoginAPI.LoginParams) => {
dispatch?.({
type: 'user/login',
payload: values,
});
};
useEffect(() => {
console.log(isLogin);
if (isLogin) {
window.location.href = '/home';
}
}, [isLogin]);
return (
<>
<div className={styles.container}>
<div className={styles.content}>
<LoginForm
formRef={formRef}
title="Adseed 管理系统"
subTitle=" "
initialValues={{
autoLogin: true,
}}
submitter={{
render: () => {
return (
<Button type="primary" htmlType="submit" className="w-full" loading={loginLoading}>
</Button>
);
},
}}
onFinish={async (values) => {
await handleLoginSubmit(values);
}}
>
<Fragment>
<ProFormText
name="userName"
fieldProps={{
size: 'large',
prefix: <UserOutlined />,
}}
placeholder="账号"
rules={[
{
required: true,
message: '请输入用户名!',
},
]}
/>
<ProFormText.Password
name="password"
fieldProps={{
size: 'large',
prefix: <LockOutlined />,
}}
placeholder="密码"
rules={[
{
required: true,
message: '请输入密码!',
},
]}
/>
</Fragment>
</LoginForm>
</div>
</div>
</>
);
};
const UserConnect = ({ user }: { user: UserConnectedProps['user'] }) => ({ user });
export default connect(UserConnect)(Login);

View File

@ -1,4 +1,5 @@
import { addSupderAdminAPI, editSupderAdminAPI } from '@/services/system/supderAdmin';
import { Reg } from '@/utils/reg';
import { ActionType, ProFormRadio, ProFormText } from '@ant-design/pro-components';
import { App, Form, Modal } from 'antd';
import { useEffect, useState } from 'react';
@ -65,10 +66,45 @@ const AddSupderAdmin = (props: PropTypes) => {
required
label="管理员账户"
placeholder="请输入管理员账户"
rules={[{ required: true, message: '请输入管理员账户' }]}
rules={[
{ required: true, message: '请输入管理员账户' },
{ pattern: Reg.SuperAdminAccount, message: '验证失败4~15位的非中文账户' },
]}
/>
<ProFormText
initialValue={''}
width="md"
name="email"
label="邮箱"
placeholder="请输入邮箱"
rules={[
{
validator: (rule, value) => {
if (value && !Reg.Email.test(value)) {
return Promise.reject(new Error('邮箱格式不正确'));
}
return Promise.resolve();
},
},
]}
/>
<ProFormText
initialValue={''}
width="md"
name="phone"
label="手机号"
placeholder="请输入手机号"
rules={[
{
validator: (rule, value) => {
if (value && !Reg.Phone.test(value)) {
return Promise.reject(new Error('手机号格式不正确'));
}
return Promise.resolve();
},
},
]}
/>
<ProFormText initialValue={''} width="md" name="email" label="邮箱" placeholder="请输入邮箱" />
<ProFormText initialValue={''} width="md" name="phone" label="手机号" placeholder="请输入手机号" />
<ProFormRadio.Group
width="md"
required

View File

@ -42,6 +42,20 @@ const Page = () => {
return record.role;
},
},
{
title: '状态',
dataIndex: 'status',
align: 'center',
hideInSearch: true,
renderText: (_, record: SuperAdmin.SuperAdminItem) => {
if (record.status === 1) {
return '正常';
} else if (record.status === 2) {
return '禁用';
}
return record.status;
},
},
{
title: '创建时间',
dataIndex: 'createTime',
@ -141,6 +155,7 @@ const Page = () => {
<AddSupderAdmin
visible={createModalVisible}
onCancel={() => {
setEditRow(undefined);
setCreateModalVisible(false);
}}
tableRef={tableRef}

View File

@ -0,0 +1,18 @@
import request from '@/utils/request';
//登录
export const userLoginAPI = (data: LoginAPI.LoginParams): Promise<LoginAPI.LoginResponse> => {
return request.post('/system/account/login', data);
};
export const getSupderInfoApi = (): Promise<API.ResponstBody<LoginAPI.LoginUserInfo>> => {
return request.post('/system/account/userInfo');
};
export const updatePasswordAPI = (data: LoginAPI.UpdatePassWordType): Promise<API.ResponstBody> => {
return request.post('/system/account/updatePwd', data);
};
export const userLogoutAPI = (): Promise<API.ResponstBody> => {
return request.post('/system/account/logout');
};

View File

@ -0,0 +1,29 @@
declare namespace LoginAPI {
type LoginParams = {
userName?: string;
password?: string;
};
type LoginUserInfo = {
id?: string;
userName?: string;
email?: string;
phone?: string;
status?: number;
createTime?: string;
updateTime?: string;
isFirstLogin?: boolean;
role?: number;
};
type UpdatePassWordType = {
oldPwd: string;
newPwd: string;
};
type LoginRespone = {
token: string;
};
type LoginResponse = API.ResponstBody<LoginRespone>;
}

7
src/utils/reg.ts 100644
View File

@ -0,0 +1,7 @@
export const Reg = {
Email:
// eslint-disable-next-line no-useless-escape
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
Phone: /^1\d{10}$/,
SuperAdminAccount: /^[A-Za-z][-_!@#$%^&*.a-zA-Z0-9]{4,15}$/,
};

View File

@ -1,7 +1,7 @@
import { notification } from 'antd';
import { message as antMessage, notification } from 'antd';
import axios, { AxiosRequestHeaders } from 'axios';
const instance = axios.create({ baseURL: 'http://127.0.0.1:3008/backend/' });
const instance = axios.create({ baseURL: API_URL + '/backend/' });
instance.interceptors.request.use(
(config) => {
@ -17,9 +17,10 @@ instance.interceptors.request.use(
instance.interceptors.response.use(
(response) => {
const { data } = response;
const { code } = data;
const { code, message } = data;
if (code === 401 || code === 403) {
if (code === 401) {
antMessage.error(message);
localStorage.removeItem('Authorization');
}

20
tailwind.config.ts 100644
View File

@ -0,0 +1,20 @@
module.exports = {
mode: 'jit',
purge: ['./public/**/*.html', './src/**/*.{js,jsx,ts,tsx}'],
darkMode: false, // or 'media' or 'class'
theme: {
backgroundColor: (theme) => ({
...theme('colors'),
dark70: 'rgba(0,0,0,.7)',
}),
extend: {
colors: {
primary: '#1677ff',
},
},
},
variants: {
extend: {},
},
plugins: [],
};

11
typings.d.ts vendored
View File

@ -2,4 +2,15 @@ import '@umijs/max/typings';
declare global {
declare type EditRow<T> = T | undefined;
const API_URL: string;
}
declare module '*.less';
declare module '*.svg';
declare module '*.png';
declare module '*.jpg';
declare module '*.jpeg';
declare module '*.gif';
declare module '*.bmp';
declare module '*.tiff';

10
unocss.config.ts 100644
View File

@ -0,0 +1,10 @@
import { defineConfig, presetAttributify, presetUno } from 'unocss';
export function createConfig({ strict = true, dev = true } = {}) {
return defineConfig({
envMode: dev ? 'dev' : 'build',
presets: [presetAttributify({ strict }), presetUno()],
});
}
export default createConfig();