基于 React16.x、Ant Design4.x,react-admin
├── config // 项目构建配置 ├── public // 不参与构建的静态文件 ├── scripts // 构建脚本 ├── src │ ├── assets // 全项目通用图片文件等 │ ├── commons // 全项目通用js,业务相关 │ ├── components // 全项目通用组件,业务相关 │ ├── config // 项目构建补充配置 │ ├── layouts // 页面框架布局组件+ │ ├── mock // 模拟数据 │ ├── models // 模块封装,基于redux,提供各组件共享数据、共享逻辑 │ ├── pages // 主项目页面目录 │ ├── ├── Project // 子项目顶级目录 │ ├── ├── ├── nft // NFT--PC版本项目目录 │ ├── ├── ├── ├── assets // NFT--PC版本公共图片文件 │ ├── ├── ├── ├── components // NFT--PC版本公共组件库 │ ├── ├── ├── ├── pages // NFT--PC版本页面目录 │ ├── ├── ├── mobile // 移动端项目目录 │ ├── ├── ├── ├── nft-mobile // NFT--微信版本项目目录 │ ├── ├── ├── ├── ├── assets // NFT--微信版本公共图片文件 │ ├── ├── ├── ├── ├── components // NFT--微信版本公共组件库 │ ├── ├── ├── ├── ├── pages // NFT--微信版本页面目录 │ ├── router // 路由 │ ├── ant.less // 主体配置 │ ├── App.js // 根组件 │ ├── index.css // 全局样式 慎用 │ ├── index.dark.css // 全局样式 慎用 │ ├── index.js // 项目入口 │ ├── menus.js // 菜单配置 │ ├── setupProxy.js // 后端联调代理配置 │ └── theme.less // 主题变量 ├── package.json ├── README.md └── yarn.lock
$ yarn
依赖安装完成后,src/setupProxy.js 为项目本地代理文件,请及时更改您需要代理的接口地址!
!!!微信版本只可以在微信打开运行,并直接在 router/AuthRoute.jsx 中填入自己申请的 appid,目前为''
此为两个项目依托同一底层框架,PC 及微信端口分离,如需切换请在 router/app.router.js 中更换路由,router/AppRoute.jsx 中切换引入的 AuthRoute(分别为微信版本:AuthRoute.jsx,PC 版本:AuthRoutePC.jsx) 并在 src/commons/PRE_ROUTER.js 中进行相关配置,以及 package.json 中更改 homepage 为相应的项目名称。
两套 PRE_ROUTER.js 中配置分别如下:
微信版本: 路由前缀----/front_nft_mobile 请求地址前缀----/api/nft 登录页面----/nft_mobile_home
PC 版本: 路由前缀----/front_nft_pc 请求地址前缀----/api/nft 登录页面----/nft_home
$ yarn start #指定端口 $ PORT=8080 yarn start # HTTPS方式启动 $ HTTPS=true yarn start
$ yarn build // 构建输入到指定目录 $ BUILD_PATH=../dist yarn build
// 开发启动 $ BASE_NAME=/synext-admin yarn start // 开发访问 'http://localhost:XXXX/synext-admin/' //生产环境 同上 $ BASE_NAME=/synext-admin yarn start // 访问 'http://xxx.com/synext-admin'
//在/src/menus.js文件中配置菜单数据,前端硬编码或异步加载菜单数据。 // 菜单支持头部、左侧、头部+左侧三种布局方式,默认左侧菜单。如需放开设置,请到'src/layouts/index.jsx'放开注释 //菜单字段说明。 字段 必须 说明 key 是 //需要唯一 parentKey 否 //用于关联父级 path 是 //菜单对应的路由地址 text 是 //菜单标题 icon 否 //菜单图标配置 url 否 //菜单对应会打开url对应的iframe页面,如果配置了url,path将无效 target 否 //配合url使用,菜单将为a标签 <a href={url} target={target}>{text}</a> order 否 //菜单排序,数值越大越靠前显示 type 否 //如果菜单数据中携带功能权限配置,type==='1' 为菜单,type==='2'为功能 code 否 //功能码,如果是type==='2',会用到此字段
//配置组件 import React, {Component} from 'react'; import config from 'src/commons/config-hoc'; @config({ title: '页面title', ajax: true, ... }) export default class SomePage extend Component { componentDidMount() { this.props.ajax .get(...) .then(...) } ... }
参数 | 类型 | 默认值 | 说明 |
---|---|---|---|
noFrame | boolean | false | 标记当前页面为不需要导航框架的页面,比如登录页,通过脚本抓取实现 |
noAuth | boolean | false | 标记当前页面为不需要登录即可访问的页面,通过脚本抓取实现 |
keepAlive | boolean | - | 标记当前页面内容在页面切换之后是否保持 |
title | boolean 或 string 或 ReactNode 或 object 或 function(props) | true | true:当前页面显示通过菜单结构自动生成的 title;false:当前页面不显示 title;string:自定义 title;object:{text,icon} text 为显示的名称,icon 为图标;function(props): 返回值作为 title |
breadcrumbs | boolean 或 array 或 function(props) | true | true:当前页面显示通过菜单结构自动生成的面包屑;false:当前页面不显示面包屑;object:[{icon, text, ...}];function(props): 返回值作为面包屑 |
appendBreadcrumbs | array 或 function(props) | [] | 在当前面包屑基础上添加;function(props): 返回值作为新添加的面包屑 |
pageHead | boolean | - | 页面头部是否显示 |
side | boolean | - | 页面左侧是否显示 |
sideCollapsed | boolean | - | 左侧是否收起 |
ajax | boolean | true | 是否添加 ajax 高阶组件,内部可以通过 this.props.ajax 使用 ajax API,组件卸载时,会自动打断未完成的请求 |
router | boolean | false | 是否添加 withRouter 装饰器,组件内部可以使用 this.props.history 等 API |
query | boolean | false | 是否添加地址查询字符串转换高阶组件,内部可以通过 this.props.query 访问查询字符串 |
connect | boolean 或 function(state) | false | 是否与 redux 进行连接,true:只注入了 this.props.action 相关方法;false:不与 redux 进行连接;(state) => ({title: state.page.title}):将函数返回的数据注入 this.props |
event | boolean | false | 是否添加 event 高阶组件,可以使用 this.props.addEventListener 添加 dom 事件,并在组件卸载时会自动清理;通过 this.props.removeEventListener 移出 dom 事件 |
pubSub | boolean | false | 是否添加发布订阅高阶组件,可以使用 this.props.subscribe(topic, (msg, data) => {...})订阅事件,并在组件卸载时,会自动取消订阅; 通过 this.props.publish(topic, data)发布事件 |
modal | string 或 object | false | 当前组件是否是 modal。string: 弹框标题;object:弹框配置 |
注:
noFrame
、noAuth
、keepAlive
只有配置了path
才有效!title
、breadcrumbs
、appendBreadcrumbs
、pageHead
、side
、sideCollapsed
最好在路由对应的页面组件中使用//页面保持 //页面渲染一次之后会保持状态,再次跳转到此页面不会重新创建或重新渲染 开启方式: 1. /src/models/system.js initState.keepAlive 属性修改默认值 2. config装饰器 keepAlive属性
config
装饰器为组件注入了两个事件 onComponentWillShow
、onComponentWillHide
,如果页面使用了 Keep Alive 功能,切换显示/隐藏时会触发
@config({ ... }) export default class SomePage extends React.Component { constructor(...props) { super(...props); this.props.onComponentWillShow(() => { // do some thing }); this.props.onComponentWillHide(() => { // do some thing }); } ... }
系统提供了页面的跟节点 PageContent,有如下特性:
是否显示 footer,默认 true
<PageContent footer={false}>...</PageContent>
显示 loading,有两种方式。
model 方式
this.props.action.page.showLoading(); this.props.action.page.hideLoading();
props 方式
const { loading } = this.state; <PageContent loading={loading}>...</PageContent>;
添加、修改等场景,往往会用到弹框,antd Modal 组件使用不当会产生脏数据问题(两次弹框渲染数据互相干扰)
系统提供了基于 modal 封装的高阶组件,每次弹框关闭,都会销毁弹框内容,避免互相干扰
modal 高阶组件集成到了 config 中,也可以单独引用:import { ModalContent } from 'src/commons/ra-lib';
import React from "react"; import config from "src/commons/config-hoc"; import { ModalContent } from "src/commons/ra-lib"; export default config({ modal: { title: "弹框标题", }, })((props) => { const { onOk, onCancel } = props; return ( <ModalContent onOk={onOk} onCancel={onCancel}> 弹框内容 </ModalContent> ); });
modal 所有参数说明如下:
弹框内容通过 ModalContent 包裹,具体参数如下:
参数 | 类型 | 默认值 | 说明 |
---|---|---|---|
surplusSpace | boolean | false | 是否使用屏幕垂直方向剩余空间 |
otherHeight | number | - | 除了主体内容之外的其他高度,用于计算主体高度; |
loading | boolean | false | 加载中 |
loadingTip | - | - | 加载提示文案 |
footer | - | - | 底部 |
okText | string | - | 确定按钮文案 |
onOk | function | - | 确定按钮事件 |
cancelText | string | - | 取消按钮文案 |
onCancel | function | - | 取消按钮事件 |
resetText | string | - | 重置按钮文案 |
onReset | function | - | 重置按钮事件 |
style | object | - | 最外层容器样式 |
bodyStyle | object | - | 内容容器样式 |
系统路由使用 react-router,通过 route-loader 将路由内容填充到/src/pages/page-routes.js 文件,支持两种写法:
export const PAGE_ROUTE = "/path";
@config({ path: '/path', }) export default class SomePage extends React.Component { ... }
二级页面如果要保持父级菜单的选中状态,以父级 path 开始并以/_/
作为分隔符即可:parent/path/_/child/path
// parent page @config({ path: '/parent/path' }) export default class Parent extends React.Component { ... } // child page @config({ path: '/parent/path/_/child/path' }) export default class Parent extends React.Component { ... }
系统的 ajax 请求基于 axios 封装。 基于 restful 规范,提供了 5 个方法:
//第一种 config装饰器ajax属性(推荐) import React, {Component} from 'react'; import config from 'src/commons/config-hoc'; @config({ ajax: true, ... }) export default class SomePage extend Component { componentDidMount() { this.props.ajax .get(...) .then(...) } ... } //第二种 ajax装饰器 import React, {Component} from 'react'; import {ajaxHoc} from 'src/commpons/ajax'; @ajaxHoc() export default class SomePage extend Component { componentDidMount() { this.props.ajax .get(...) .then(...) } ... } //第三种 直接引入ajax对象 import React, {Component} from 'react'; import {sxAjax} from 'src/commpons/ajax'; export default class SomePage extend Component { componentDidMount() { sxAjax.post(...).then(...); // 组件卸载或者其他什么情况,需要打算ajax请求,可以用如下方式 const ajax = sxAjax.get(...); ajax.then(...).finally(...); ajax.cancel(); } ... }
注:config、ajaxHoc 方式做了封装,页面被卸载之后会自动打断未完成的请求
所有的 ajax 方法参数统一,都能够接受三个参数: 参数|说明 ---|--- url|请求地址 params|请求传递给后端的参数 options|请求配置,即 axios 的配置。
参数 | 说明 |
---|---|
axios 配置 | 可以接受 axios 参数 |
successTip | 扩展的参数,成功提示 |
errorTip | 扩展的参数,失败提示 |
noEmpty | 扩展的参数,过滤掉 ''、null、undefined 的参数,不提交给后端 |
originResponse | 扩展参数,.then 中可以拿到完整的 response,而不只是 response.data |
注:全局默认参数可以在 src/commons/ajax.js 中进行配置,默认 baseURL='/api'、timeout=1000 * 60。
this.props.ajax.del("/user/1", null, { successTip: "删除成功!", errorTip: "删除失败!", noEmpty: true, });
系统扩展了 promise,提供了 finally 方法,用于无论成功还是失败,都要进行的处理。一般用于关闭 loading
this.setState({loading: true}); this.props.ajax .get('/url') .then(...) .finally(() => this.setState({loading: false}));
前后端并行开发,为了方便后端快速开发,不需要等待后端接口,系统提供了 mock 功能。基于mockjs
在/src/mock 目录下进行 mock 数据编写,比如:
import { getUsersByPageSize } from "./mockdata/user"; export default { "post /mock/login": (config) => { const { userName, password } = JSON.parse(config.data); return new Promise((resolve, reject) => { if (userName !== "test" || password !== "111") { setTimeout(() => { reject({ code: 1001, message: "用户名或密码错误", }); }, 1000); } else { setTimeout(() => { resolve([ 200, { id: "1234567890abcde", name: "MOCK 用户", loginName: "MOCK 登录名", }, ]); }, 1000); } }); }, "post /mock/logout": {}, "get /mock/user-center": (config) => { const { pageSize, pageNum } = config.params; return new Promise((resolve) => { setTimeout(() => { resolve([ 200, { pageNum, pageSize, total: 888, list: getUsersByPageSize(pageSize), }, ]); }, 1000); }); }, "get re:/mock/user-center/.+": { id: 1, name: "熊大", age: 22, job: "前端", }, "post /mock/user-center": true, "put /mock/user-center": true, "delete re:/mock/user-center/.+": "id", };
为了方便 mock 接口编写,系统提供了简化脚本(/src/mock/simplify.js),上面的例子就是简化写法
对象的 key 由 method url delay,各部分组成,以空格隔开
字段 | 说明 |
---|---|
method | 请求方法 get post 等 |
url | 请求的 url |
delay | 模拟延迟,毫秒 默认 1000 |
系统封装的 ajax 可以通过以下两种方式,自动区分是 mock 数据,还是真实后端数据,无需其他配置
mock 请求:
this.props.ajax.get('/mock/users').then(...);
如果后端真实接口准备好之后,去掉 url 中的/mock 即可
注:mock 功能只有开发模式下开启了,生产模式不会开启 mock 功能,如果其他环境要开启 mock 使用 MOCK=true 参数,比如 MOCK=true yarn build
系统使用less进行样式的编写。 为了避免多人合作样式冲突,系统对 src 下的 less 文件启用了 Css Module,css 文件没有使用 Css Module。
style.less
.root { width: 100%; height: 100%; }
Some.jsx
import "/path/to/style.less"; export default class Some extends React.Component { render() { return <div styleName="root"></div>; } }
注:基础组件不使用 Css Module,不利于样式覆盖;
使用 less,通过样式覆盖来实现。
/src/theme.less
通过less-loader的modifyVars
覆盖 less 中的变量;注:目前每次修改了 theme.less 需要重新 yarn start 才能生效
为了满足不同系统的需求,提供了四种导航布局:
src/models/index.js
指定布局方式;有些页面可能不需要显示导航,可以通过如下任意一种方式进行设置:
@config({ noFrame: true, })
/path/to?noFrame=true
页面头部标签,有如下特性:
Keep Page Alive
);system model(redux)中提供了如下操作 tab 页的方法:
API | 说明 |
---|---|
setCurrentTabTitle(title) | 设置当前激活的 tab 标题 title: stirng 或 {text, icon} |
refreshTab(targetPath) | 刷新 targetPath 指定的 tab 页内容(重新渲染) |
refreshAllTab() | 刷新所有 tab 页内容(重新渲染) |
closeCurrentTab() | 关闭当前 tab 页 |
closeTab(targetPath) | 关闭 targetPath 对应的 tab 页 |
closeOtherTabs(targetPath) | 关闭除了 targetPath 对应的 tab 页之外的所有 tab 页 |
closeAllTabs() | 关闭所有 tab 页,系统将跳转首页 |
closeLeftTabs(targetPath) | 关闭 targetPath 对应的 tab 页左侧所有 tab 页 |
closeRightTabs(targetPath) | 关闭 targetPath 对应的 tab 页右侧所有的 tab 页 |
使用方式:
import config from 'src/commons/config-hoc'; @config({ connect: true, }) export default class SomeComponent extends React.Component { componentDidMount() { this.props.action.system.closeTab('/some/path'); } ... }
注:
this.props.history.push('/some/path')
,就会选中或者新打开一个 tab 页(/path
与 /path?name=Tom
属于不同 url 地址,会对应两个 tab 页);基于redux进行封装,不改变 redux 源码,可以结合使用 redux 社区中其他解决方案。
注:一般情况下,用不到 redux~
models/page.js
中的写法;所有的 model 直接在 models 或 pages 下定义:
model 模块名规则:
/path/to/models/user-center.js --> userCenter; /path/to/models/user.js --> user; /path/to/pages/users/model.js --> users; /path/to/pages/users/job.model.js --> job; /path/to/pages/users/user-center.model.js --> userCenter; /path/to/pages/users/user.center.model.js --> userCenter;
提供了多种种方式,装饰器方式、函数调用、hooks、js 文件直接使用;
推荐使用装饰器方式
import {connect} from 'path/to/models'; @connect(state => { return { ... } }) class Demo extends Component{ ... }
import {connectComponent} from 'path/to/models'; class Demo extends Component { ... } function mapStateToProps(state) { return { ... }; } export default connectComponent({LayoutComponent: Demo, mapStateToProps});
import { useSelector } from "react-redux"; import { useAction } from "src/models"; export default () => { const action = useAction(); const show = useSelector((state) => state.side.show); console.log(show); useEffect(() => { action.side.hide(); }, []); return <div />; };
对 useSelector 的说明:
useSelector(select) 默认对 select 函数的返回值进行引用比较 ===,并且仅在返回值改变时触发重渲染。
即:如果 select 函数返回一个临时对象,会多次 re-render
最好不要这样使用: const someData = useSelector(state => { // 每次都返回一个新对象,导致re-render return {name: state.name, age: state.age}; }) 最好多次调用useSelector,单独返回数据,或者返回非引用类型数据 const name = useSelector(state => state.firstName + state.lastName); const age = useSelector(state => state.age);
没有特殊需求,一般不会在普通 js 文件中使用
import { action, store } from "src/models"; // 获取数据 const state = store.getState(); // 修改数据 action.side.hide();
action reducer 二合一,省去了 actionType,简化写法;
注意:
一个函数,即可作为 action 方法,也作为 reduce 使用
// page.model.js export default { initialState: { title: void 0, name: void 0, user: {}, toggle: true, }, setTitle: (title) => ({ title }), setName: (name, state, action) => { const { name: prevName } = state; if (name !== prevName) return { name: "Different Name" }; }, setUser: ({ name, age } = {}) => ({ user: { name, age } }), setToggle: (arg, state) => ({ toggle: !state.toggle }), }; // 使用 this.props.action.page.setTitle("my title");
通过配置的方式,可以让 redux 中的数据自动与 localStorage 同步
export default { initialState: { title: '', show: true, user: {}, users: [], job: {}, total: 0, loading: false, ... }, // initialState会全部同步到localStorage中 // syncStorage: true, // 配置部分存数据储到localStorage中 syncStorage: { titel: true, user: { // 支持对象指定字段,任意层次 name: true, address: { city: true, }, }, job: true, users: [{name: true, age: true}], // 支持数组 }, }
如果 action 有额外的数据处理,并且一个 action 只对应一个 reducer,这种写法不需要指定 actionType,可以有效简化代码;
export default { initialState: { title: '', ... }, arDemo: { // 如果是函数返回值将作为action.payload 传递给reducer,如果非函数,直接将payload的值,作为action.payload; payload(options) {...}, // 如果是函数返回值将作为action.meta 传递给reducer,如果非函数,直接将meta的值,作为action.meta; meta(options) {...}, reducer(state, action) { returtn {...newState}; // 可以直接返回要修改的数据,内部封装会与原state合并`{...state, ...newState}`; }, }, };
export default { initialState: { title: '', ... }, fetchUser: { // 异步action payload 返回promise payload: ({params, options}) => axios.get('/mock/users', params, options), // 异步action 默认使用通用异步meta配置 commonAsyncMeta,对successTip errorTip onResolve onReject onComplete 进行了合理的默认值处理,需要action以对象形式传参调用 // meta: commonAsyncMeta, // meta: { // successTip: '查询成功!欧耶~', // errorTip: '自定义errorTip!马丹~', // }, // meta: () => { // return {...}; // }, // 基于promise 异步reducer写法; reducer: { pending: (state, action) => ({loading: true}), resolve(state, {payload = {}}) { const {total = 0, list = []} = payload; return { users: list, total, } }, complete: (state, action) => ({loading: false}), } }, };
调用方式:
this.props.action.user.fetchUser({ params, options, successTip, errorTip, onResolve, onReject, onComplete, });
参数约定为一个对象,各个属性说明如下:
参数 | 说明 |
---|---|
params | 请求参数 |
options | 请求配置 |
successTip | 成功提示信息 |
errorTip | 错误提示信息 |
onResolve | 成功回调 |
onReject | 失败回调 |
onComplete | 完成回调,无论成功、失败都会调用 |
支持这种比较传统的写法,一般也不会太用到
import {createAction} from 'redux-actions'; export const types = { GET_MENU_STATUS: 'MENU:GET_MENU_STATUS', // 防止各个模块冲突,最好模块名开头 }; export default { initialState: { title: '', ... }, // 单独action定义,需要使用actionType与reducer进行关联 actions: { getMenuStatus: createAction(types.GET_MENU_STATUS), }, // 单独reducer定义,使用了actionType,不仅可以处理当前model中的action // 也可以处理其他任意action(只要actionType能对应) reducers: { [types.GET_MENU_STATUS](state) { ... return { ... }; } }, }
系统菜单、具体功能点都可以进行权限控制。
菜单由后端提供(一般系统都是后端提供),后台通过登录用户返回用户的菜单权限;页面只显示获取到的菜单;
系统提供了一个基础的菜单、权限管理页面,需要后端配合存储数据。
可以通过src/components/permission
组件对功能的权限进行控制
import React, { Component } from "react"; import Permission from "src/components/permission"; export default class SomePage extends Component { render() { return ( <div> <Permission code="USER_ADD"> <Button>添加用户</Button> </Permission> </div> ); } }
注:权限的 code 前端使用时会硬编码,注意语义化、唯一性。
一般系统都会提供角色管理功能,系统中提供了一个基础的角色管理功能,稍作修改即可使用。
开发时,要与后端进行接口对接,可以通过代理与后端进行连接,开发代理配置在src/setupProxy.js
中编写
const proxy = require("http-proxy-middleware"); const prefix = process.env.AJAX_PREFIX || "/api"; module.exports = function (app) { app.use( proxy(prefix, { target: "http://localhost:3000/", pathRewrite: { ["^" + prefix]: "", // 如果后端接口无前缀,可以通过这种方式去掉 }, changeOrigin: true, secure: false, // 是否验证证书 ws: true, // 启用websocket }) ); };
注:更多代理配置请参考http-proxy-middleware
前端默认 ajax 前缀 /api 可以通过 AJAX_PREFIX 参数进行修改。
这里只是参考文件,根据自己的项目需求自行配置
. ├── /usr/local/nginx/html │ ├── static │ ├── index.html │ └── favicon.ico
# 后端服务地址 upstream api_service { server xxx.xxx.xxx.xxx:xxxx; keepalive 2000; } server { listen 80; server_name www.xxxx.com xxxx.com; # 域名地址 root /usr/local/nginx/html; # 前端静态文件目录 location / { index index.html; try_files $uri $uri/ /index.html; #react-router 防止页面刷新出现404 } # 静态文件缓存,启用Cache-Control: max-age、Expires location ~ ^/static/(css|js|media)/ { expires 10y; access_log off; add_header Cache-Control "public"; } # 代理ajax请求 前端ajax请求以 /api 开头 location ^~/api { rewrite ^/api/(.*)$ /$1 break; # 如果后端接口不是统一以api开头,去掉api前缀 proxy_pass http://api_service/; proxy_set_header Host $http_host; proxy_set_header Connection close; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Server $host; } }
多个项目挂载到同一个域名下,可以通过子目录方式区分
比如,如下地址各对应一个项目
前端项目构建时,添加 BASE_NAME PUBLIC_URL 参数
BASE_NAME=/project1 PUBLIC_URL=/project1 yarn build
. ├── /home/ubuntu/synext-admin │ ├── build // 主项目 静态文件目录 │ │ ├── static │ │ ├── index.html │ │ └── favicon.ico │ ├── project1 // 子项目静态目录 名称与 location /project1 location ~ ^/project1/static/.* 配置对应 │ │ ├── static │ │ ├── index.html │ │ └── favicon.ico
upstream api_service { server xxx.xxx.xxx.xxx:xxxx; keepalive 2000; } upstream api_service_project1 { server xxx.xxx.xxx.xxx:xxxx; keepalive 2000; } server { listen 80; server_name www.xxxx.com xxxx.com; # 域名地址 # Allow file uploads client_max_body_size 100M; # 主项目配置,访问地址 http://www.xxxx.com location / { root /home/ubuntu/synext-admin/build; index index.html; try_files $uri $uri/ /index.html; } # 静态文件缓存,启用Cache-Control: max-age、Expires location ~ ^/static/.* { root /home/ubuntu/synext-admin/build; expires 20y; access_log off; add_header Cache-Control "public"; } # 代理ajax请求 前端ajax请求以/api开头 location ^~/api { rewrite ^/api/(.*)$ /$1 break; # 如果后端接口不是统一以api开头,去掉api前缀 proxy_pass http://api_service/; proxy_set_header Host $http_host; proxy_set_header Connection close; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Server $host; } # 子项目配置 访问地址 http://www.xxxx.com/project1 location /project1 { root /home/ubuntu/synext-admin; index index.html; try_files $uri $uri/ /project1/index.html; } # 静态文件缓存,启用Cache-Control: max-age、Expires location ~ ^/project1/static/.* { root /home/ubuntu/synext-admin; expires 10y; access_log off; add_header Cache-Control "public"; } # 代理ajax请求 前端ajax请求以 /project1_api 开头 location ^~/project1_api { rewrite ^/api/(.*)$ /$1 break; # 如果后端接口不是统一以api开头,去掉api前缀 proxy_pass http://api_service_project1/; proxy_set_header Host $http_host; proxy_set_header Connection close; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Server $host; } }
通过给元素添加相应的 class,控制打印内容:
.just-print
只在打印时显示.no-print
在打印时不显示如果前端项目,不是 git 根目录,在提交的时候,会报错 Not a git repository
修改 package.json,lint-staged 如下即可
"lint-staged": { "gitDir": "../", "linters": { "**/*.{js,jsx}": "lint-staged:js", "**/*.less": "stylelint --syntax less" } },
const name = res?.data?.user?.name || "匿名";
'input', 'hidden', 'number', 'textarea', 'password', 'mobile', 'email', 'select', 'select-tree', 'checkbox', 'checkbox-group', 'radio', 'radio-button', 'radio-group', 'switch', 'date', 'time', 'date-time', 'date-range', 'cascader', 'transfer', 'icon-picker'
打开MySQL-Front 选菜单帮助->登记 复制注册码: gNBpPFgyOw9Rwt/ozsnjgM7tJNo2 bhaaAThangemMkaz2tQhq3/f7dZ7 Vj29WeGHjuupj/AhYqymjAuokhYi X1T/fG+q1yR22PdcEP39dxU3ovEo lLLzwIZlQr9oJYwUf5eG4x5e1bMP nfIaIl8reszQPHFNbrxBjCVr
参考: MySQL错误1055 MySQL Err1055错误的解决方法 错误原因:在MySQL5.7之后,sql_mode中默认存在ONLY_FULL_GROUP_BY,SQL语句未通过ONLY_FULL_GROUP_BY语义检查所以报错。 解决办法: 在MySQL/my.ini文件中添加如下语句:sql_mode='NO_ENGINE_SUBSTITUTION' 错误出现背景,在更新集成环境P
在B站上看韩顺平老师关于数据结构与算法的视频时,在对环形队列进行实现的代码中,有一个公式 (rear + maxSize - front) % maxSize。虽然我认为可以定义一个成员变量,在添加和删除是分别对其进行++ 或 --,但是为了锻炼思维,所以研究了一下这个公式。在对算法的理解中,我主要是带入实例进行思考,所以下面将以实例进行讲解。 首先是关于 % maxSize 的的理解。假设数组的
部分用户出现无法连接的原因是:MySQL8.0版本的加密方式与之前版本的加密方式不太一样。 解决方式:更改加密方式 (1)以管理员身份运行控制台程序,连接mysql: mysql -u root -p (2)输入 ALTER USER 'root'@'数据库地址' IDENTIFIED WITH mysql_native_password BY '数
配置mysql-front时总是报错#1109unkonw table’session_variables’ in information_schema 有没有大佬知道时为什么,快给我整疯了
数源三街 NFT 是一款稳定、安全、合规、高效的数字藏品系统,适用于企业 / 品牌开展数字藏品平台开发、NFT 平台开发;主要包括数字藏品铸造、发行、空投、盲盒、合并、交易、分享、预售等相关功能开发。标准化产品,已对接 BSN、Polygon、以太坊等。快速交付。后端 Java 语言,稳定可靠。 系统架构 前端: Uniapp 开发者编写一套代码,可发布到 iOS、Android、Web(响应式)
克隆代码 git clone https://github.com/shengjian-tech/opennft.git 注册百度超级链开放网络 注册百度超级链开放网络账户https://xuper.baidu.com/ 完成账户初始化, 下载个人私钥和address 合约安装 此步骤可省略,使用已安装完成的opennft合约即可 找到blockchainplatform-nft-web模块下的
1.1.2.开源平台 Android是一个开放源码的平台。它的整个架构,从底层的Linux模块、原生库,到高层的开发框架乃至完整的应用程序,都是完全开放的。 而且,Android选择了对商业行为比较友好的许可证(Apache/MIT),从而允许他人在自由的扩展之余,更可以合法地用作其它用途。为保证整个架构的开放,Android甚至重写了一些三方库,并按照新的许可证重新发布。 因此作为开发者,你有权
Abstract: Python is an easy to learn, powerful programming language. It has efficient high-level data structures and a simple but effective approach to object-oriented programming. Python's elegant sy
nft-qos 是 RosyWrt 团队为 OpenWrt 和 LuCI 项目研发的流量限速应用,基于全新的 nftables 防火墙以及 OpenWrt master 分支, 目的在于为项目提供一个细粒度(具体到客户端)的限速解决方案。 主要功能特点如下: 1. 静态限速 - 支持 IPv4/IPv6 - 支持对单个客户端(IP)限速 ; - 支持全局限速(整个局域网);
opennft-client 需要谷歌浏览器或 Chromium 内核 前期准备 注册百度账号以及获取私钥 充值百度开放网络 Tip:用户地址下需要有百度开放网络余额才能使用转移资产,查询余额等功能。建议在百度开放网络充值0.1元。充值链接:https://xuper.baidu.com/n/console#/finance/wallet/recharge 插件使用 使用帮助二维码 插件安装 插件