feat: react/mobile

This commit is contained in:
NICE CODE BY DEV 2022-05-26 17:18:23 +08:00
parent 9d6340c83e
commit 452b6bb803
18 changed files with 315 additions and 419 deletions

View File

@ -1,4 +1,5 @@
import { defineConfig } from 'umi';
import px2rem from 'postcss-px2rem';
export default defineConfig({
favicon: '#',
@ -7,53 +8,27 @@ export default defineConfig({
immer: true,
hmr: false,
},
publicPath: '/',
webpack5: {},
mfsu: {},
dynamicImport: {
loading: '@/components/PageLoading/index',
},
extraPostCSSPlugins: [px2rem({ remUnit: 66.7, exclude: /node_modules/i })],
routes: [
{
path: '/window',
component: '@/layouts/WindowLayout',
routes: [
{
path: 'demo',
component: '@/pages/index',
name: '一级菜单',
title: '一级菜单',
icon: 'EntranceOutlined',
}
]
},
{
path: '/',
component: '@/layouts/BasicLayout',
// wrappers: ['@/wrappers/SecurityLayout'],
component: '@/layouts/BlankLayout',
wrappers: ['@/wrappers/SecurityWrapper'],
routes: [
{ exact: true, path: '/', redirect: '/a' },
// { exact: true, path: '/', redirect: '/home' },
{
path: 'a',
path: '/',
component: '@/pages/index',
name: '一级菜单',
title: '一级菜单',
icon: 'EntranceOutlined',
},
{
path: 'b',
name: '一级菜单',
title: '一级菜单',
icon: 'EntranceOutlined',
routes: [
{ exact: true, path: '/b', redirect: '/b/c' },
{
path: 'c',
component: '@/pages/index',
name: '二级菜单',
title: '二级菜单',
},
],
},
],
},
],
@ -98,10 +73,20 @@ export default defineConfig({
},
locale: {
default: 'zh-CN',
antd: true,
antd: false,
},
ignoreMomentLocale: true,
targets: {
ie: 10,
}
},
extraBabelPlugins: [
[
'import',
{
libraryName: 'antd-mobile',
libraryDirectory: 'es/components',
style: false,
},
],
],
});

View File

@ -40,29 +40,37 @@
"dependencies": {
"@ant-design/icons": "^4.7.0",
"@ant-design/pro-layout": "^6.15.4",
"antd": "^4.20.6",
"@nicecode/tools": "^0.2.12",
"antd-mobile": "^5.12.6",
"antd-mobile-icons": "^0.2.2",
"axios": "^0.19.2",
"classnames": "^2.2.6",
"js-cookie": "^2.2.1",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"umi": "^3.5.15"
"umi": "^3.5.15",
"weixin-js-sdk": "^1.6.0"
},
"devDependencies": {
"@nicecode/commit-lint": "^0.1.2",
"@types/classnames": "^2.2.10",
"@types/react": "^18.0.9",
"@types/react-dom": "^18.0.5",
"@typescript-eslint/eslint-plugin": "^5.26.0",
"@typescript-eslint/parser": "^5.26.0",
"@umijs/preset-react": "1.x",
"@umijs/test": "^3.0.16",
"babel-plugin-import": "^1.13.5",
"babel-plugin-transform-remove-console": "^6.9.4",
"commitlint": "^17.0.1",
"cross-env": "^7.0.2",
"eruda": "^2.4.1",
"eslint": "^7.16.0",
"eslint-config-alloy": "^4.5.1",
"eslint-plugin-react": "^7.30.0",
"husky": "^8.0.1",
"lint-staged": "^10.0.7",
"postcss-px2rem": "^0.3.0",
"prettier": "^1.19.1",
"typescript": "^4.1.3",
"yorkie": "^2.0.0"

View File

@ -1,2 +1,4 @@
import '@/utils/flexible';
import 'antd-mobile/es/global';
import '@/styles/index.less';
import '@/styles/reset.less';

View File

@ -1,44 +0,0 @@
import React from 'react';
import classnames from 'classnames';
import BreadcrumbItem from './BreadcrumbItem';
import './index.less';
interface BreadcrumbProps {
routes: any[];
}
const Breadcrumb: React.FC<BreadcrumbProps> = ({ routes }) => {
const validRoutes = routes.filter(item => !!item);
return (
<div className="g-basic-layout-header-breadcrumb">
<BreadcrumbItem.Root
className={classnames({
'g-basic-layout-header-breadcrumb-item-active': !validRoutes.length,
})}
/>
{validRoutes.map(
(item: any, index) =>
item && (
<span key={item.key}>
<span className="g-basic-layout-header-breadcrumb-divider">
/
</span>
<BreadcrumbItem
path={item.path}
className={classnames({
'g-basic-layout-header-breadcrumb-item-active':
index === validRoutes.length - 1,
})}
disabled={index === validRoutes.length - 1}
>
{item.name}
</BreadcrumbItem>
</span>
),
)}
</div>
);
};
export default Breadcrumb;

View File

@ -1,48 +0,0 @@
import React from 'react';
import { history } from 'umi';
import { Button } from 'antd';
import { ButtonProps } from 'antd/lib/button';
import { HomeOutlined } from '@ant-design/icons';
import classnames from 'classnames';
import './index.less';
interface BreadcrumbItemProps extends ButtonProps {
path: string;
}
const BreadcrumbItem = ({
path,
children,
className,
...rest
}: BreadcrumbItemProps) => {
return (
<Button
className={classnames('g-basic-layout-header-breadcrumb-item', className)}
type="text"
onClick={() => history.push(path)}
{...rest}
>
{children}
</Button>
);
};
const RootItem: React.FC<ButtonProps> = ({ className, ...rest }) => (
<Button
className={classnames(
'g-basic-layout-header-breadcrumb-item',
'g-basic-layout-header-breadcrumb-item-root',
className,
)}
type="text"
onClick={() => history.push('/')}
{...rest}
>
<HomeOutlined />
</Button>
);
BreadcrumbItem.Root = RootItem;
export default BreadcrumbItem;

View File

@ -1,84 +0,0 @@
@import '../../styles/var.less';
.g-basic-layout {
&-header {
display: flex;
&-breadcrumb {
.g-basic-layout-header-breadcrumb-item {
color: @M4;
padding: 0;
&:active {
color: @M4;
}
&:hover {
color: @S3;
background-color: @M7;
}
&:focus {
background-color: @M7;
}
&-active {
color: @M2;
&[disabled],
&[disabled]:hover,
&[disabled]:focus,
&[disabled]:active {
color: @M2;
cursor: default;
}
}
}
.g-basic-layout-header-breadcrumb-divider {
color: @M4;
margin: 0 @Sp-3;
}
}
}
.ant-pro-sider-logo img {
width: 32px;
height: 32px;
}
.ant-pro-global-header {
background-color: @M7;
box-shadow: none;
}
.ant-pro-basicLayout-content {
margin: @Sp-5 @Sp-8;
}
.ant-page-header-heading {
&-left {
margin: 0;
}
&-title {
font-size: @Fs-4;
line-height: @Lh-4;
}
}
.ant-layout {
background-color: @M7;
}
.ant-layout-sider,
.ant-menu.ant-menu-dark,
.ant-menu-dark .ant-menu-sub,
.ant-menu.ant-menu-dark .ant-menu-sub {
background-color: @S1;
}
.ant-layout-content {
min-height: calc(100vh - 72px);
}
}

View File

@ -1,82 +0,0 @@
import React from 'react';
import { history, Link } from 'umi';
import { EnterOutlined } from '@ant-design/icons';
import ProLayout, { MenuDataItem } from '@ant-design/pro-layout';
import { HeaderViewProps } from '@ant-design/pro-layout/lib/Header';
// import User from '@/components/User';
import Breadcrumb from './Breadcrumb';
import './index.less';
interface IBasicLayout {
children: React.ReactNode
}
const BasicLayout: React.FC<IBasicLayout> = ({ children, ...rest }: any) => {
const iconMap = {
EnterOutlined: <EnterOutlined />,
};
// 带子菜单的一级导航
const renderSubMenuItem = (itemProps: MenuDataItem): React.ReactNode => {
return (
<>
{itemProps.icon && iconMap[itemProps.icon as string]}
<span>{itemProps.name}</span>
</>
);
};
// 不带子菜单的导航
const renderMenuItem = (itemProps: MenuDataItem): React.ReactNode => {
return itemProps.isUrl || !itemProps.path ? (
<>
{itemProps.icon && iconMap[itemProps.icon as string]}
<span>{itemProps.name}</span>
</>
) : (
<Link to={itemProps.path}>
{itemProps.icon && iconMap[itemProps.icon as string]}
<span>{itemProps.name}</span>
</Link>
);
};
// 面包屑
const renderHeaderContent: (
props: HeaderViewProps,
) => React.ReactNode = props => {
// 匹配到到路由和面包屑信息
const { matchMenuKeys, breadcrumb } = props as any;
const matchRoutes = matchMenuKeys.map((item: any) => breadcrumb[item]);
return (
<div className="g-basic-layout-header">
<Breadcrumb routes={matchRoutes} />
</div>
);
};
// 用户信息
// const renderUserAvatar = () => <User />;
return (
<ProLayout
className="g-basic-layout"
logo="http://jzx-h5.oss-cn-hangzhou.aliyuncs.com/static/pill.png?x-oss-process=img/q/80"
title="nicecode"
siderWidth={180}
fixedHeader
fixSiderbar
onMenuHeaderClick={() => history.push('/')}
subMenuItemRender={renderSubMenuItem}
menuItemRender={renderMenuItem}
headerContentRender={renderHeaderContent}
// rightContentRender={renderUserAvatar}
{...rest}
>
{children}
</ProLayout>
);
};
export default BasicLayout;

View File

@ -1,33 +0,0 @@
import { history } from 'umi';
import { Image, Divider } from 'antd';
import User from '@/components/User';
import './index.less';
function Header() {
return (
<div className="g-window-layout-header">
<div
className="g-window-layout-header-logo"
onClick={() => history.push('/')}
>
<Image
src={require('../../assets/images/logo.png')}
alt="logo"
preview={false}
width={36}
height={24}
/>
<Divider
type="vertical"
className="g-window-layout-header-logo-divider"
/>
<span className="g-window-layout-header-logo-text">react-template</span>
</div>
<div className="g-window-layout-header-actions">
<User />
</div>
</div>
);
}
export default Header;

View File

@ -1,65 +0,0 @@
@import '../../styles/var.less';
@headerHeader: 64px;
.g-window-layout {
display: flex;
flex-direction: column;
min-width: 1200px;
min-height: 100vh;
background-color: @M7;
&-header {
display: flex;
justify-content: space-between;
align-items: center;
height: @headerHeader;
width: 1200px;
margin: 0 auto;
&-wrapper {
background-color: #000000;
}
&-logo {
display: flex;
justify-content: center;
align-items: center;
color: #ffffff;
cursor: pointer;
&-divider.ant-divider {
border-color: #ffffff;
height: 12px;
margin: 0 @Sp-5;
}
&-text {
font-size: @Fs-3;
}
}
&-actions {
color: #fff;
}
}
&-content {
flex: 1;
width: 1200px;
min-height: 600px;
margin: 0 auto;
padding: @Sp-8;
background-color: #ffffff;
box-shadow: @Sh-2;
border-radius: @Ra-2;
&-wrapper {
display: flex;
flex-direction: column;
flex: 1;
min-height: 500px;
padding: @Sp-8 0;
}
}
}

View File

@ -1,22 +0,0 @@
import React from 'react';
import Header from './Header';
import './index.less';
interface IWindowLayout {
children: React.ReactNode
}
const WindowLayout: React.FC<IWindowLayout> = ({ children }) => {
return (
<div className="g-window-layout">
<div className="g-window-layout-header-wrapper">
<Header />
</div>
<div className="g-window-layout-content-wrapper">
<div className="g-window-layout-content">{children}</div>
</div>
</div>
);
};
export default WindowLayout;

15
src/pages/document.ejs Normal file
View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="full-screen" content="yes">
<meta name="x5-fullscreen" content="true">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no">
<title>加载中...</title>
</head>
<body>
<div id="root" style="position:relative;"></div>
</body>
</html>

View File

@ -1,5 +1,6 @@
.title {
margin: 100px auto;
font-size: 16px;
text-align: center;
// background: rgb(121, 242, 157);
}

27
src/services/common.ts Normal file
View File

@ -0,0 +1,27 @@
import request from '@/utils/request';
/**
*
* @returns
*/
export async function getWxAuthorities(data: { url: string; }): Promise<any> {
return request.post('/dianhun/traffic/accident/weixin/param/v1', data);
}
/**
*
* @returns
*/
export async function getUserInfo(data: any): Promise<any> {
return request.post(
process.env.BASE_API + '/dianhun/traffic/accident/weixin/code/v1',
data,
);
}
export async function jumpToWxUrl(params: any): Promise<any> {
return request.post(
process.env.BASE_API + '/dianhun/traffic/accident/weixin/redirectUrl/v1',
params,
);
}

53
src/utils/flexible.ts Normal file
View File

@ -0,0 +1,53 @@
(function flexible(window, document) {
let docEl = document.documentElement;
let dpr = window.devicePixelRatio || 1;
// adjust body font size
function setBodyFontSize() {
if (document.body) {
document.body.style.fontSize = 12 * dpr + 'px';
} else {
document.addEventListener('DOMContentLoaded', setBodyFontSize);
}
}
setBodyFontSize();
// set 1rem = viewWidth / 10
function setRemUnit() {
let width = document.documentElement.clientWidth;
let height = document.documentElement.clientHeight;
let rem = 0
if (width > height) {
// 横屏
// @ts-ignore
rem = height * 1.78;
} else {
// 竖屏
rem = width * 1.78;
}
docEl.style.fontSize = rem / 10 + 'px';
}
setRemUnit();
// reset rem unit on page resize
window.addEventListener('resize', setRemUnit);
window.addEventListener('pageshow', (e) => {
if (e.persisted) {
setRemUnit();
}
});
// detect 0.5px supports
if (dpr >= 2) {
let fakeBody = document.createElement('body');
let testElement = document.createElement('div');
testElement.style.border = '.5px solid transparent';
fakeBody.appendChild(testElement);
docEl.appendChild(fakeBody);
if (testElement.offsetHeight === 1) {
docEl.classList.add('hairlines');
}
docEl.removeChild(fakeBody);
}
})(window, document);

View File

@ -0,0 +1,55 @@
import { Toast } from 'antd-mobile';
export const resize = () => {
let width = document.documentElement.clientWidth;
let height = document.documentElement.clientHeight;
console.log(width, height);
let root = document.querySelector('#root') as any;
root.style.position = 'relative';
// root.style.overflow = 'hidden';
root.style.top = '50%';
root.style.left = '50%';
root.style['transform-origin'] = '50% 50%';
if (width > height) {
// 横屏
// 判断比例是否正确
root.style.width = height * 1.78 + 'px';
root.style.height = height + 'px';
root.style.transform = 'translate(-50%, -50%)';
// 不旋转
} else {
// 竖屏
root.style.width = width * 1.78 + 'px';
root.style.height = width + 'px';
root.style.transform = 'translate(-50%, -50%) rotate(90deg)';
}
};
/**
*
* @param value
*/
export const copy = function (value: any) {
return new Promise((resolve) => {
let copyTextArea = null;
try {
copyTextArea = document.createElement('textarea');
copyTextArea.style.height = '0px';
copyTextArea.style.opacity = '0';
copyTextArea.style.width = '0px';
document.body.appendChild(copyTextArea);
copyTextArea.value = value;
copyTextArea.select();
document.execCommand('copy');
Toast.show({
content: '链接复制成功!',
});
resolve(value);
} finally {
if (copyTextArea?.parentNode) {
copyTextArea.parentNode.removeChild(copyTextArea);
}
}
});
};

View File

@ -1,5 +1,5 @@
import axios from 'axios';
import { message } from 'antd';
import { Toast } from 'antd-mobile';
import CodeMsg from '@/assets/data/code';
import { BaseResponse } from '@/interfaces/base';
@ -11,7 +11,9 @@ export const DEFAULT_TIP_MESSAGE = '请求失败,请刷新重试';
*/
export function handleError(data: BaseResponse): void {
const msg = CodeMsg[data.code] || data.msg || DEFAULT_TIP_MESSAGE;
message.error(msg);
Toast.show({
content: msg,
})
}
// create an axios instance
@ -33,9 +35,13 @@ service.interceptors.request.use(
error => {
// Do something with request error
if (error.status === '504') {
message.error('网关超时,请重试!');
Toast.show({
content: '网关超时,请重试!',
})
} else {
message.error(`网络异常[-${error.status}]`);
Toast.show({
content: `网络异常[-${error.status}]`,
})
console.log(error); // for debug
}
Promise.reject(error);

120
src/utils/wxShare.ts Normal file
View File

@ -0,0 +1,120 @@
import { getWxAuthorities } from '@/services/common';
import { checkDevice } from '@nicecode/tools';
import wx from 'weixin-js-sdk';
// 要用到微信API
function getJSSDK(
shareUrl: string,
shareMsg: {
title: string;
desc: string;
link: string;
imgUrl: string;
},
) {
if (checkDevice.isWeChat()) {
getWxAuthorities({ url: shareUrl }).then(
(res: {
data: {
appId: string;
timestamp: number;
echostr: string;
signature: string;
};
}) => {
wx.config({
// debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来若要查看传入的参数可以在pc端打开参数信息会通过log打出仅在pc端时才会打印。
appId: res.data.appId, // 必填,公众号的唯一标识
timestamp: res.data.timestamp, // 必填,生成签名的时间戳
nonceStr: res.data.echostr, // 必填,生成签名的随机串
signature: res.data.signature, // 必填,签名
jsApiList: ['updateAppMessageShareData', 'updateTimelineShareData'],
});
wx.ready(() => {
// 分享给微信朋友
wx.updateAppMessageShareData({
title: shareMsg.title,
desc: shareMsg.desc,
link: shareMsg.link,
imgUrl: shareMsg.imgUrl,
success: function success(res: any) {
console.log(res, '分享成功');
},
cancel: function cancel() {
// console.log('已取消');
},
fail: function fail() {
// alert(JSON.stringify(res));
},
});
// 2.2 监听“分享到朋友圈”按钮点击、自定义分享内容及分享结果接口
wx.updateTimelineShareData({
title: shareMsg.title,
link: shareMsg.link,
imgUrl: shareMsg.imgUrl,
success: function success(_res: any) {
// alert('已分享');
},
cancel: function cancel(_res: any) {
// alert('已取消');
},
fail: function fail() {
// alert(JSON.stringify(res));
},
});
// 2.3 监听“分享到QQ”按钮点击、自定义分享内容及分享结果接口
// wx.onMenuShareQQ({
// title: shareMsg.title,
// desc: shareMsg.desc,
// link: shareMsg.linkurl,
// imgUrl: shareMsg.img,
// trigger: function trigger(res) {
// //alert('用户点击分享到QQ');
// },
// complete: function complete(res) {
// alert(JSON.stringify(res));
// },
// success: function success(res) {
// //alert('已分享');
// },
// cancel: function cancel(res) {
// //alert('已取消');
// },
// fail: function fail(res) {
// //alert(JSON.stringify(res));
// }
// });
// 2.4 监听“分享到微博”按钮点击、自定义分享内容及分享结果接口
// wx.onMenuShareWeibo({
// title: shareMsg.title,
// desc: shareMsg.desc,
// link: shareMsg.linkurl,
// imgUrl: shareMsg.img,
// trigger: function trigger(res) {
// //alert('用户点击分享到微博');
// },
// complete: function complete(res) {
// // alert(JSON.stringify(res));
// },
// success: function success(res) {
// //alert('已分享');
// },
// cancel: function cancel(res) {
// // alert('已取消');
// },
// fail: function fail(res) {
// // alert(JSON.stringify(res));
// }
// });
});
wx.error(() => {
// alert("微信验证失败");
});
},
);
}
}
export default {
// 获取JSSDK
getJSSDK,
};

4
typings.d.ts vendored
View File

@ -3,4 +3,6 @@ declare module '*.less';
declare module '*.png';
declare module '*.jpeg';
declare module '*.jpg';
declare module 'immer'
declare module 'eruda'
declare module 'weixin-js-sdk'
declare module 'postcss-px2rem'