lanjianrong 5 years ago
parent
commit
7c6c62217f
8 changed files with 755 additions and 4 deletions
  1. 0 1
      .gitignore
  2. 0 3
      .gitmodules
  3. 11 0
      src/basic/consts.js
  4. 425 0
      src/basic/css/common.scss
  5. 64 0
      src/basic/css/variable.scss
  6. 12 0
      src/basic/utils.js
  7. 114 0
      src/basic/utils/common.js
  8. 129 0
      src/basic/utils/request.js

+ 0 - 1
.gitignore

@@ -38,4 +38,3 @@ screenshot
 .eslintcache
 
 build
-src/basic

+ 0 - 3
.gitmodules

@@ -1,3 +0,0 @@
-[submodule "src/basic"]
-	path = src/basic
-	url = http://192.168.1.41:3000/lanjianrong/basic.git

+ 11 - 0
src/basic/consts.js

@@ -0,0 +1,11 @@
+export default {
+  // 基础设置
+  VERSION: '1.0.0',
+  NAME: '', // 应用名称
+  TOKEN_KEY: 'Authorization', // token的键值
+  TOKEN_API: '/api/login/token', // 获取token令牌
+  LOGIN_PATH: '/login', //
+  TOKEN_INVALID_CODE: [ 2, 3 ], // 接口返回码如果是2, 3 则表明token过期或无效 需要自动刷新token
+  TOKEN_WHITE_LIST: [ '/api/login' ], // 不需要设置token的白名单
+  RET_CODE: { SUCCESS: 0, FAIL: 1, TOKEN_UNDEFINED: 19, TOKEN_EXPIRED: 2 } // 返回RET状态码解析
+}

+ 425 - 0
src/basic/css/common.scss

@@ -0,0 +1,425 @@
+@import './variable.scss';
+
+/** 背景颜色 */
+.zh-bg-success {
+  background-color: $zh-green;
+}
+
+.zh-bg-gray {
+  background-color: $zh-light-gray;
+}
+
+.zh-bg-blue {
+  background-color: $zh-blue;
+}
+
+.zh-bg-red {
+  background-color: $zh-red;
+}
+
+.zh-bg-yellow {
+  background-color: $zh-yellow;
+}
+
+.zh-bg-green {
+  background-color: $zh-green;
+}
+
+.zh-bg-white {
+  background-color: $zh-white;
+}
+
+/** 字体颜色 */
+.zh-gray {
+  color: $zh-gray;
+}
+
+.zh-red {
+  color: $zh-red;
+}
+
+.zh-blue {
+  color: $zh-blue;
+}
+
+.zh-green {
+  color: $zh-green;
+}
+
+.zh-yellow {
+  color: $zh-yellow;
+}
+
+.zh-muted {
+  color: $zh-muted;
+}
+
+/** 文本水平对齐方向 */
+.zh-text-center {
+  text-align: center;
+}
+
+.zh-text-left {
+  text-align: left;
+}
+
+.zh-text-right {
+  text-align: right;
+}
+
+
+.zh-width-100P {
+  width: 100%;
+}
+
+.zh-height-100P {
+  height: 100%;
+}
+
+/** 字体粗细 */
+@for $i from 1 through 9 {
+  .zh-fw-#{$i * 100} {
+    font-weight: $i * 100;
+  }
+}
+
+/* 预设常用样式 */
+@for $i from 1 through 50 {
+  /* font-size */
+  .zh-fz-#{$i} {
+    font-size: 1px * $i;
+  }
+
+  /* padding */
+  .zh-pd-#{$i} {
+    padding: 1px * $i;
+  }
+  .zh-pd-top-#{$i} {
+    padding-top: 1px * $i;
+  }
+  .zh-pd-right-#{$i} {
+    padding-right: 1px * $i;
+  }
+  .zh-pd-bottom-#{$i} {
+    padding-bottom: 1px * $i;
+  }
+  .zh-pd-left-#{$i} {
+    padding-left: 1px * $i;
+  }
+  .zh-pd-tb-#{$i} {
+    padding-top: 1px * $i;
+    padding-bottom: 1px * $i;
+  }
+  .zh-pd-lr-#{$i} {
+    padding-right: 1px * $i;
+    padding-left: 1px * $i;
+  }
+
+  /* margin */
+  .zh-mg-#{$i} {
+    margin: 1px * $i;
+  }
+  .zh-mg-top-#{$i} {
+    margin-top: 1px * $i;
+  }
+  .zh-mg-right-#{$i} {
+    margin-right: 1px * $i;
+  }
+  .zh-mg-bottom-#{$i} {
+    margin-bottom: 1px * $i;
+  }
+  .zh-mg-left-#{$i} {
+    margin-left: 1px * $i;
+  }
+  .zh-mg-tb-#{$i} {
+    margin-top: 1px * $i;
+    margin-bottom: 1px * $i;
+  }
+  .zh-mg-lr-#{$i} {
+    margin-right: 1px * $i;
+    margin-left: 1px * $i;
+  }
+  // width & height
+  .zh-width-#{$i} {
+    width: 1px * $i;
+  }
+  .zh-height-#{$i} {
+    height: 1px * $i;
+  }
+
+  // radius
+  .zh-radius-#{$i} {
+    border-radius: 1px * $i;
+  }
+
+  // line-height
+  .zh-lh-#{$i} {
+    line-height: 1px * $i;
+  }
+}
+
+/** flex布局 */
+.zh-flex {
+  display: flex;
+}
+
+.zh-flex-wrap {
+  @extend .zh-flex;
+
+  flex-wrap: wrap;
+}
+
+.zh-flex-row {
+  @extend .zh-flex;
+
+  flex-direction: row;
+}
+
+.zh-flex-column {
+  @extend .zh-flex;
+
+  flex-direction: column;
+}
+
+.zh-flex-sub {
+  flex: 1;
+}
+
+.zh-flex-twice {
+  flex: 2;
+}
+
+.zh-flex-treble {
+  flex: 3;
+}
+
+.zh-flex-quadruple {
+  flex: 4;
+}
+
+.zh-flex-quintuple {
+  flex: 5;
+}
+.zh-align-start {
+  @extend .zh-flex;
+
+  align-items: flex-start;
+}
+
+.zh-align-end {
+  @extend .zh-flex;
+
+  align-items: flex-end;
+}
+
+.zh-align-center {
+  @extend .zh-flex;
+
+  align-items: center;
+}
+
+.zh-align-stretch {
+  @extend .zh-flex;
+
+  align-items: stretch;
+}
+
+.zh-align-baseline {
+  @extend .zh-flex;
+
+  align-items: baseline;
+}
+
+.zh-self-start {
+  @extend .zh-flex;
+
+  align-self: flex-start;
+}
+
+.zh-self-center {
+  @extend .zh-flex;
+
+  align-self: flex-center;
+}
+
+.zh-self-end {
+  @extend .zh-flex;
+
+  align-self: flex-end;
+}
+
+.zh-self-stretch {
+  @extend .zh-flex;
+
+  align-self: stretch;
+}
+
+.zh-justify-start {
+  @extend .zh-flex;
+
+  justify-content: flex-start;
+}
+
+.zh-justify-end {
+  @extend .zh-flex;
+
+  justify-content: flex-end;
+}
+
+.zh-justify-center {
+  @extend .zh-flex;
+
+  justify-content: center;
+}
+
+.zh-justify-between {
+  @extend .zh-flex;
+
+  justify-content: space-between;
+}
+
+.zh-justify-around {
+  @extend .zh-flex;
+
+  justify-content: space-around;
+}
+
+.zh-flex-row-center {
+  @extend .zh-flex-row;
+  @extend .zh-justify-center;
+  @extend .zh-align-center;
+}
+
+.zh-flex-column-center {
+  @extend .zh-flex-column;
+  @extend .zh-justify-center;
+  @extend .zh-align-center;
+}
+
+
+// grid布局
+.zh-grid {
+  display: flex;
+  flex-wrap: wrap;
+}
+
+@mixin multi-border($n) {
+  &.zh-grid-border {
+    & > div {
+      @include zh-border;
+      &:not(:nth-last-child(-n + #{$n})) {
+        &::after {
+          border-bottom: 1px solid $zh-line-color;
+        }
+      }
+      &:not(:nth-child(#{$n}n)) {
+        &::after {
+          border-right: 1px solid $zh-line-color;
+        }
+      }
+    }
+  }
+}
+
+.zh-grid.zh-col-2 {
+  & > div {
+    width: 50%;
+  }
+
+  @include multi-border(2);
+}
+
+.zh-grid.zh-col-3 {
+  & > div {
+    width: calc(100% / 3);
+  }
+
+  @include multi-border(3);
+}
+
+@mixin space-calc($i, $n) {
+  & > div {
+    width: calc((100% - #{($n - 1) * $i}px) / #{$n});
+    &:not(:nth-last-child(-n + #{$n})) {
+      margin-bottom: #{$i}px;
+    }
+    &:not(:nth-child(#{$n}n)) {
+      margin-right: #{$i}px;
+    }
+  }
+}
+
+// 处理列之间需要有空隙情况
+@for $i from 1 through 50 {
+  .zh-grid.zh-col-1.zh-col-space-#{$i} {
+    & > div {
+      margin-bottom: #{$i}px;
+    }
+  }
+  .zh-grid.zh-col-2.zh-col-space-#{$i} {
+    @include space-calc($i, 2);
+  }
+  .zh-grid.zh-col-3.zh-col-space-#{$i} {
+    @include space-calc($i, 3);
+  }
+  .zh-grid.zh-col-4.zh-col-space-#{$i} {
+    @include space-calc($i, 4);
+  }
+  .zh-grid.zh-col-5.zh-col-space-#{$i} {
+    @include space-calc($i, 5);
+  }
+  .zh-grid.zh-col-6.zh-col-space-#{$i} {
+    @include space-calc($i, 6);
+  }
+}
+
+/** 省略号 */
+@for $i from 1 through 2 {
+  .zh-ellipsis-#{$i} {
+    display: -webkit-box;
+    -webkit-box-orient: vertical;
+    -webkit-line-clamp: #{$i};
+    overflow: hidden;
+  }
+}
+
+/** 鼠标手 */
+.zh-pointer {
+  cursor: pointer;
+}
+
+/** 线条 */
+@for $i from 1 through 5 {
+  .zh-border-top-#{$i} {
+    border-top: 1px * $i solid #f3f3f3;
+  }
+}
+
+/** 布局 */
+.wrap-container {
+  @extend .zh-width-100P;
+  @extend .zh-height-100P;
+
+  position: absolute;
+  top: 0;
+  left: 0;
+  overflow: hidden;
+  background: white;
+}
+
+/** 滚动条自定义 */
+::-webkit-scrollbar{
+  width: 8px;
+  height: 8px;
+}
+::-webkit-scrollbar-track{
+  background: rgb(239, 239, 239);
+  border-radius: 2px;
+}
+::-webkit-scrollbar-thumb{
+  background: #bfbfbf;
+  border-radius: 10px;
+}
+::-webkit-scrollbar-thumb:hover{
+  background: #636363;
+}

+ 64 - 0
src/basic/css/variable.scss

@@ -0,0 +1,64 @@
+$zh-font-size: 12px !default;
+$zh-green: #28a745 !default;
+$zh-gray: #757575 !default;
+$zh-muted: #6c757d !default;
+$zh-light-gray: #bbbbbb !default;
+$zh-blue: #007bff !default;
+$zh-red: #dc3545 !default;
+$zh-yellow: #ffc170 !default;
+$zh-white: #ffffff !default;
+$zh-line-color: rgba(0,0,0,.125) !default;
+$zh-danger:#fd3995 !default;
+$zh-warning:#ffc241 !default;
+$zh-success:#1dc9b7 !default;
+
+
+// 实现0.5px的效果
+@mixin zh-border {
+  position: relative;
+  &::after {
+    position: absolute;
+    top: 0;
+    left: 0;
+    z-index: 2;
+    box-sizing: border-box;
+    width: 100%;
+    height: 100%;
+    border-radius: inherit;
+    content: ' ';
+    pointer-events: none;
+  }
+}
+
+.auto-scroll-y {
+  height: 100%;
+  .ant-spin-nested-loading,
+  .ant-spin-container,
+  .ant-table,
+  .ant-table-scroll,
+  .ant-table-body-inner,
+  .ant-table-fixed-left,
+  .ant-table-content        {
+    height: 100%;
+  }
+  .ant-table-content {
+    position: relative;
+  }
+  .ant-table-body, .ant-table-body-inner {
+    overflow-y: auto !important;
+  }
+  .ant-table-header {
+    overflow-y: hidden !important;
+  }
+  .ant-table-body {
+    height: 580px;
+    overflow-y: auto;
+  }
+  .ant-table-body-outer {
+    height: 580px;
+    overflow-y: auto;
+  }
+  .ant-table-body-inner {
+    overflow-x: hidden;
+  }
+}

+ 12 - 0
src/basic/utils.js

@@ -0,0 +1,12 @@
+export const importAll = context => {
+  const map = {}
+  // eslint-disable-next-line no-restricted-syntax
+  for (const key of context.keys()) {
+    const keyArr = key.split('/')
+    keyArr.shift() // 移除 ./
+    map[keyArr.join('.').replace(/\.js$/g, '')] = context(key)
+  }
+  return map
+}
+
+export default importAll(require.context('./utils', true, /\.js$/))

+ 114 - 0
src/basic/utils/common.js

@@ -0,0 +1,114 @@
+import { message } from "antd"
+import { history  } from "umi"
+import consts from '../consts'
+
+const USER_INFO = 'USER_INFO' // 用户个人信息
+const USER_ACCOUNT = 'USER_ACCOUNT' // 用户账号
+const TOKEN_ID = 'TOKEN_ID' // TOKEN令牌ID
+
+// 本地存储封装
+const storage = {
+  get(key) {
+    const val = localStorage.getItem(key)
+    if (val) {
+      return JSON.parse(val)
+    }
+    return null
+  },
+  set(key, value) {
+    localStorage.setItem(key, JSON.stringify(value))
+  },
+  del(key) {
+    localStorage.removeItem(key)
+  },
+  clear() {
+    localStorage.clear()
+  }
+}
+
+/**
+ * 保存用户信息到本地存储中
+ * @param {*} user 用户信息
+ */
+export const saveUserInfo = user => {
+  storage.set(USER_INFO, user)
+}
+
+/**
+ * 获取用户信息
+ */
+export const getUserInfo = () => {
+  const user = storage.get(USER_INFO)
+  return user
+}
+
+/**
+ * 移除用户信息
+ */
+export const removeUserInfo = () => {
+  return storage.del(USER_INFO)
+}
+
+/**
+ * 保存用户账号信息
+ * @param {*} account 账号
+ */
+export const saveUserAccount = account => {
+  storage.set(USER_ACCOUNT, account)
+}
+
+/**
+ * 获取用户账号信息
+ */
+export const getUserAccount = () => {
+  return storage.get(USER_ACCOUNT)
+}
+
+/**
+ * 移除用户账号信息
+ */
+export const removeUserAccount = () => {
+  return storage.del(USER_ACCOUNT)
+}
+/**
+ * 保存令牌ID到本地存储中
+ * @param {*} id token令牌id
+ */
+export const saveTokenId = id => {
+  return storage.set(TOKEN_ID, id)
+}
+
+/**
+ * 移除令牌ID
+ */
+export const removeTokenId = () => {
+  return storage.del(TOKEN_ID)
+}
+
+/**
+ * 从本地存储中获取令牌ID
+ */
+export const getTokenid = () => {
+  return storage.get(TOKEN_ID) || ''
+}
+
+/**
+ * 重定向到登录页
+ */
+export const redirectToLogin = url => {
+  // 重定向登录页面
+  history.replace(url || consts.LOGIN_PATH)
+}
+
+
+/**
+ * 用户注销
+ * @param {String} url 注销后重定向登录的地址
+ */
+export const logout = () => {
+  // message.success('退出登录')
+  removeTokenId()
+  removeUserInfo()
+  removeUserAccount()
+  // redirectToLogin(url || consts.LOGIN_PATH)
+}

+ 129 - 0
src/basic/utils/request.js

@@ -0,0 +1,129 @@
+/**
+ * request 网络请求工具
+ * 更详细的 api 文档: https://github.com/umijs/umi-request
+ */
+import { extend } from 'umi-request'
+import { message, notification } from 'antd'
+import { getTokenid, getUserAccount, redirectToLogin, saveTokenId } from './common'
+import consts from '@/consts'
+
+const RETRY_TOKEN_TIME = 3 // 重新获取token次数
+let currentTokenRetry = RETRY_TOKEN_TIME // 当前token刷新次数
+const codeMessage = {
+  200: '服务器成功返回请求的数据。',
+  201: '新建或修改数据成功。',
+  202: '一个请求已经进入后台排队(异步任务)。',
+  204: '删除数据成功。',
+  400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
+  401: '用户没有权限(令牌、用户名、密码错误)。',
+  403: '用户得到授权,但是访问是被禁止的。',
+  404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
+  406: '请求的格式不可得。',
+  410: '请求的资源被永久删除,且不会再得到的。',
+  422: '当创建一个对象时,发生一个验证错误。',
+  500: '服务器发生错误,请检查服务器。',
+  502: '网关错误。',
+  503: '服务不可用,服务器暂时过载或维护。',
+  504: '网关超时。'
+}
+/**
+ * 异常处理程序
+ */
+
+const errorHandler = error => {
+  const { response } = error
+
+  if (response && response.status) {
+    const errorText = codeMessage[response.status] || response.statusText
+    const { status, url } = response
+    notification.error({
+      message: `请求错误 ${status}: ${url}`,
+      description: errorText
+    })
+  } else if (!response) {
+    notification.error({
+      description: '您的网络发生异常,无法连接服务器',
+      message: '网络异常'
+    })
+  }
+
+  return response
+}
+
+
+/**
+ * 配置request请求时的默认参数
+ */
+
+const request = extend({
+  errorHandler,
+  // 默认错误处理
+  credentials: 'include' // 默认请求是否带上cookie
+})
+
+
+/**
+ * 获取token令牌
+ */
+async function apiGetToken() {
+  const data = await request.get(consts.TOKEN_API, {})
+  return data
+}
+
+// 请求拦截器
+request.interceptors.request.use((url, options) => {
+  const token = getTokenid()
+  if (token) {
+    // 在白名单里的请求放过
+    if (consts.TOKEN_WHITE_LIST.includes(url)) {
+      return { url, options }
+    }
+    // eslint-disable-next-line no-param-reassign
+    options.headers[consts.TOKEN_KEY] = `bearer ${token}`
+    return { url, options }
+  }
+  return { url, options }
+})
+const refreshTokenIfNeed = async (response, config) => {
+  const res = await response.clone().json()
+  if (
+    res &&
+    res.code !== consts.RET_CODE.SUCCESS &&
+    !consts.TOKEN_INVALID_CODE.includes(res.code)
+  ) {
+    message.error((res && res.msg) || '请求错误')
+    return response
+  }
+
+  if (
+    res &&
+    res.code &&
+    consts.TOKEN_INVALID_CODE.includes(res.code)
+  ) {
+    const account = getUserAccount()
+    if (!account || !currentTokenRetry) {
+      redirectToLogin()
+      return response
+    }
+    currentTokenRetry -= 1
+    // token过期或者没有传token
+    const data = await apiGetToken(account)
+    if (data && data.data && data.data.code === consts.RET_CODE.SUCCESS) {
+      currentTokenRetry = RETRY_TOKEN_TIME // 重置token重试次数
+      // 刷新token成功,保存token
+      const token = data.data && data.data.token
+      saveTokenId(token)
+      // 重新发起请求
+      const newResponse = await request(response.url, config)
+      return newResponse
+    }
+  }
+  return response
+}
+
+// 响应拦截器
+request.interceptors.response.use((response, options) => {
+  return refreshTokenIfNeed(response, options)
+})
+
+export default request