Quellcode durchsuchen

初始化项目

lex vor 1 Jahr
Ursprung
Commit
c83235b23d
42 geänderte Dateien mit 1849 neuen und 744 gelöschten Zeilen
  1. 3 0
      .eslintrc.js
  2. 16 0
      README.md
  3. 45 12
      index.html
  4. 93 637
      package-lock.json
  5. 6 3
      package.json
  6. BIN
      public/favicon.ico
  7. BIN
      src/common/images/icon-search.png
  8. 4 0
      src/component-ui/global.less
  9. 0 79
      src/components/HelloWorld.vue
  10. BIN
      src/components/m-empty/images/404.png
  11. BIN
      src/components/m-empty/images/empty.png
  12. BIN
      src/components/m-empty/images/network.png
  13. 19 0
      src/components/m-empty/index.module.less
  14. 62 0
      src/components/m-empty/index.tsx
  15. 0 0
      src/components/m-full-refresh/datas/data.json
  16. 21 0
      src/components/m-full-refresh/index.module.less
  17. 76 0
      src/components/m-full-refresh/index.tsx
  18. BIN
      src/components/m-full-refresh/loading.gif
  19. 0 0
      src/components/m-header/index.module.less
  20. 121 0
      src/components/m-header/index.tsx
  21. 64 0
      src/components/m-search/index.module.less
  22. 85 0
      src/components/m-search/index.tsx
  23. 13 0
      src/components/m-sticky/index.module.less
  24. 117 0
      src/components/m-sticky/index.tsx
  25. 0 0
      src/components/m-uploader/index.module.less
  26. 24 0
      src/components/m-uploader/index.tsx
  27. 16 0
      src/helpers/deep-clone.ts
  28. 116 0
      src/helpers/native-message.ts
  29. 111 0
      src/helpers/request.ts
  30. 406 0
      src/helpers/toolsValidate.ts
  31. 52 0
      src/helpers/utils.ts
  32. 24 0
      src/main.ts
  33. 37 9
      src/router/index.ts
  34. 18 0
      src/router/router-root.ts
  35. 34 0
      src/router/routes-common.ts
  36. 49 0
      src/state.ts
  37. 16 3
      src/styles/index.less
  38. 0 0
      src/views/404/index.module.less
  39. 33 0
      src/views/404/index.tsx
  40. 8 1
      src/views/home/index.tsx
  41. 5 0
      src/views/test/index.module.less
  42. 155 0
      src/views/test/index.tsx

+ 3 - 0
.eslintrc.js

@@ -12,5 +12,8 @@ module.exports = {
   parserOptions: {
     ecmaVersion: 2020,
     sourceType: 'module'
+  },
+  rules: {
+    '@typescript-eslint/no-explicit-any': ['off']
   }
 };

+ 16 - 0
README.md

@@ -32,6 +32,22 @@ npm lint
 npm generate
 ```
 
+### postMessage API 方法
+
+```
+goBack
+back
+getNavHeight
+getToken
+chooseFile
+login
+joinChatGroup
+openWebView
+callPhone
+shareTripartite
+savePicture
+```
+
 ### Customize configuration
 
 See [Configuration Reference](https://vitejs.dev/config/).

+ 45 - 12
index.html

@@ -1,14 +1,47 @@
 <!DOCTYPE html>
 <html lang="en">
-  <head>
-    <meta charset="UTF-8" />
-    <link rel="icon" href="/favicon.ico" />
-    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, viewport-fit=cover">
-    <title>Vite App</title>
-    <script src="/flexible.js" charset="UTF-8"></script>
-  </head>
-  <body>
-    <div id="app"></div>
-    <script type="module" src="/src/main.ts"></script>
-  </body>
-</html>
+
+<head>
+  <meta charset="UTF-8" />
+  <link rel="icon" href="/favicon.ico" />
+  <meta name="viewport"
+    content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, viewport-fit=cover">
+  <meta http-equiv="Cache-control" content="no-cache">
+  <meta http-equiv="Cache" content="no-cache">
+  <meta name="apple-mobile-web-app-capable" content="yes" />
+  <!-- 设置苹果工具栏颜色 -->
+  <meta name="apple-mobile-web-app-status-bar-style" content="black" />
+  <!-- 忽略页面中的数字识别为电话,忽略email识别 -->
+  <meta name="format-detection" content="telphone=no, email=no" />
+  <!-- 启用360浏览器的极速模式(webkit) -->
+  <meta name="renderer" content="webkit" />
+  <!-- 避免IE使用兼容模式 -->
+  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+  <meta name="HandheldFriendly" content="true" />
+  <!-- uc强制竖屏 -->
+  <meta name="screen-orientation" content="portrait" />
+  <!-- QQ强制竖屏 -->
+  <meta name="x5-orientation" content="portrait" />
+  <!-- UC强制全屏 -->
+  <meta name="full-screen" content="yes" />
+  <!-- QQ强制全屏 -->
+  <meta name="x5-fullscreen" content="true" />
+  <!-- UC应用模式 -->
+  <meta name="browsermode" content="application" />
+  <!-- QQ应用模式 -->
+  <meta name="x5-page-mode" content="app" />
+  <!-- 设置在apple上以应用模式启动时,是否全屏 -->
+  <meta name="apple-touch-fullscreen" content="yes" />
+  <!-- windows phone 点击无高光 -->
+  <meta name="msapplication-tap-highlight" content="no" />
+  <meta name="referrer" content="no-referrer" />
+  <title>学校端</title>
+  <script src="/flexible.js" charset="UTF-8"></script>
+</head>
+
+<body>
+  <div id="app"></div>
+  <script type="module" src="/src/main.ts"></script>
+</body>
+
+</html>

Datei-Diff unterdrückt, da er zu groß ist
+ 93 - 637
package-lock.json


+ 6 - 3
package.json

@@ -22,10 +22,15 @@
     "prepare": "husky install"
   },
   "dependencies": {
+    "@vant/use": "^1.5.1",
+    "clean-deep": "^3.4.0",
     "dayjs": "^1.11.7",
+    "numeral": "^2.0.6",
+    "umi-request": "^1.4.0",
     "vant": "^4.1.2",
     "vue": "^3.2.47",
-    "vue-router": "^4.1.6"
+    "vue-router": "^4.1.6",
+    "vue3-lottie": "^2.7.0"
   },
   "devDependencies": {
     "@babel/core": "^7.21.4",
@@ -41,8 +46,6 @@
     "@vue/eslint-config-typescript": "^11.0.2",
     "autoprefixer": "^10.4.14",
     "babel-plugin-dynamic-import-node": "^2.3.3",
-    "commitizen": "^4.3.0",
-    "cz-conventional-changelog": "^3.3.0",
     "eslint": "^8.38.0",
     "eslint-plugin-prettier": "^4.2.1",
     "eslint-plugin-vue": "^9.10.0",

BIN
public/favicon.ico


BIN
src/common/images/icon-search.png


+ 4 - 0
src/component-ui/global.less

@@ -122,4 +122,8 @@
 
   // 折叠面板
   --van-collapse-item-content-text-color: #999;
+
+  // 头部高度
+  --van-nav-bar-height: 44px;
+  --van-nav-bar-arrow-size: 22px;
 }

+ 0 - 79
src/components/HelloWorld.vue

@@ -1,79 +0,0 @@
-<template>
-  <h1>{{ msg }}</h1>
-
-  <p>
-    Recommended IDE setup:
-    <a href="https://code.visualstudio.com/" target="_blank">VSCode</a>
-    +
-    <a
-      href="https://marketplace.visualstudio.com/items?itemName=octref.vetur"
-      target="_blank"
-    >
-      Vetur
-    </a>
-    or
-    <a href="https://github.com/johnsoncodehk/volar" target="_blank">Volar</a>
-    (if using
-    <code>&lt;script setup&gt;</code>
-    )
-  </p>
-
-  <p>
-    See
-    <code>README.md</code>
-    for more information.
-  </p>
-
-  <p>
-    <a href="https://vitejs.dev/guide/features.html" target="_blank">
-      Vite Docs
-    </a>
-    |
-    <a href="https://v3.vuejs.org/" target="_blank">Vue 3 Docs</a>
-  </p>
-
-  <button type="button" @click="count++">count is: {{ count }}</button>
-  <p>
-    Edit
-    <code>components/HelloWorld.vue</code>
-    to test hot module replacement.
-  </p>
-  <van-button type="primary" round @click="count++">
-    count is: {{ count }}
-  </van-button>
-</template>
-
-<script lang="ts">
-import { ref, defineComponent } from 'vue';
-export default defineComponent({
-  name: 'HelloWorld',
-  props: {
-    msg: {
-      type: String,
-      required: true
-    }
-  },
-  setup: () => {
-    const count = ref(0);
-    return { count };
-  }
-});
-</script>
-
-<style scoped>
-a {
-  color: #42b983;
-}
-
-label {
-  margin: 0 0.5em;
-  font-weight: bold;
-}
-
-code {
-  background-color: #eee;
-  padding: 2px 4px;
-  border-radius: 4px;
-  color: #304455;
-}
-</style>

BIN
src/components/m-empty/images/404.png


BIN
src/components/m-empty/images/empty.png


BIN
src/components/m-empty/images/network.png


+ 19 - 0
src/components/m-empty/index.module.less

@@ -0,0 +1,19 @@
+.mEmpty {
+  --van-empty-description-color: var(--k-gray-4);
+  --van-empty-description-font-size: 16px;
+  --van-empty-description-margin-top: 13px;
+  :global {
+    .van-empty__image {
+      width: 198px;
+      height: 124px;
+    }
+  }
+
+  .button {
+    background: transparent;
+    min-width: 76px;
+    font-size: 13px;
+    padding: 0 24px;
+    height: 26px;
+  }
+}

+ 62 - 0
src/components/m-empty/index.tsx

@@ -0,0 +1,62 @@
+import { PropType, defineComponent, onMounted, reactive } from 'vue';
+import styles from './index.module.less';
+import { Button, Empty } from 'vant';
+import iconEmpty from './images/empty.png';
+import iconNetwork from './images/network.png';
+import icon404 from './images/404.png';
+
+export default defineComponent({
+  name: 'm-empty',
+  props: {
+    description: {
+      type: String as PropType<string | undefined>,
+      default: null
+    },
+    image: {
+      type: String as PropType<'empty' | 'network' | '404'>,
+      default: 'empty'
+    },
+    showButton: {
+      type: Boolean,
+      default: false
+    },
+    buttonText: {
+      type: String,
+      default: '返回'
+    }
+  },
+  emits: ['click'],
+  setup(props, { emit }) {
+    const forms = reactive({
+      image: iconEmpty
+    });
+
+    onMounted(() => {
+      if (props.image === 'network') {
+        forms.image = iconNetwork;
+      } else if (props.image === '404') {
+        forms.image = icon404;
+      }
+    });
+    return () => (
+      <Empty
+        style={{
+          paddingTop: 0
+        }}
+        class={styles.mEmpty}
+        image={forms.image}
+        description={props.description}>
+        {props.showButton && (
+          <Button
+            type="primary"
+            plain
+            round
+            class={styles.button}
+            onClick={() => emit('click')}>
+            {props.buttonText}
+          </Button>
+        )}
+      </Empty>
+    );
+  }
+});

Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
src/components/m-full-refresh/datas/data.json


+ 21 - 0
src/components/m-full-refresh/index.module.less

@@ -0,0 +1,21 @@
+.animateWrap {
+  width: 55px !important;
+  height: 55px !important;
+}
+
+.loading {
+  height: 55px !important;
+  img {
+    height: 30px;
+    width: 120px;
+    margin-top: 20px;
+  }
+}
+
+.pullRefresh {
+  :global {
+    .van-pull-refresh__track {
+      min-height: inherit;
+    }
+  }
+}

+ 76 - 0
src/components/m-full-refresh/index.tsx

@@ -0,0 +1,76 @@
+import { Image, PullRefresh } from 'vant';
+import { defineComponent, reactive, watch } from 'vue';
+import styles from './index.module.less';
+import { Vue3Lottie } from 'vue3-lottie';
+import AstronautJSON from './datas/data.json';
+import 'vue3-lottie/dist/style.css';
+import loading from './loading.gif';
+export default defineComponent({
+  name: 's-full-refresh',
+  props: {
+    title: String,
+    modelValue: {
+      type: Boolean,
+      default: false
+    }
+  },
+  emits: ['refresh', 'update:modelValue'],
+  setup(props, { emit, slots }) {
+    const state = reactive({
+      fullState: false
+    });
+    watch(
+      () => props.modelValue,
+      (val: boolean) => {
+        state.fullState = val;
+      }
+    );
+    watch(
+      () => state.fullState,
+      (val: boolean) => {
+        emit('update:modelValue', val);
+      }
+    );
+    return () => (
+      <PullRefresh
+        v-model:modelValue={state.fullState}
+        onRefresh={() => emit('refresh')}
+        loadingText=" "
+        class={styles.pullRefresh}>
+        {{
+          loading: () => (
+            <div>
+              {
+                <Image src={loading} class={styles.loading} />
+                // <Vue3Lottie
+                //   class={styles.animateWrap}
+                //   animationData={AstronautJSON}></Vue3Lottie>
+              }
+            </div>
+          ),
+          pulling: () => (
+            <div>
+              {
+                <Image src={loading} class={styles.loading} />
+                // <Vue3Lottie
+                //   class={styles.animateWrap}
+                //   animationData={AstronautJSON}></Vue3Lottie>
+              }
+            </div>
+          ),
+          loosing: () => (
+            <div>
+              {
+                <Image src={loading} class={styles.loading} />
+                // <Vue3Lottie
+                //   class={styles.animateWrap}
+                //   animationData={AstronautJSON}></Vue3Lottie>
+              }
+            </div>
+          ),
+          default: () => <> {slots.default && slots.default()}</>
+        }}
+      </PullRefresh>
+    );
+  }
+});

BIN
src/components/m-full-refresh/loading.gif


+ 0 - 0
src/components/m-header/index.module.less


+ 121 - 0
src/components/m-header/index.tsx

@@ -0,0 +1,121 @@
+import { postMessage } from '@/helpers/native-message';
+import { browser } from '@/helpers/utils';
+import { NavBar } from 'vant';
+import { defineComponent, onMounted, reactive, watch } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
+import styles from './index.module.less';
+
+export default defineComponent({
+  name: 'm-header',
+  props: {
+    title: {
+      type: String,
+      default: ''
+    },
+    isBack: {
+      type: Boolean,
+      default: true
+    },
+    border: {
+      type: Boolean,
+      default: false
+    },
+    isFixed: {
+      type: Boolean,
+      default: true
+    },
+    styleName: {
+      type: Object,
+      default: () => ({})
+    },
+    background: {
+      type: String,
+      default: 'white'
+    },
+    color: {
+      type: String,
+      default: '#323233'
+    },
+    rightText: {
+      type: String,
+      default: ''
+    }
+  },
+  emits: ['rightClick'],
+  setup(props, { emit, slots }) {
+    const route = useRoute();
+    const router = useRouter();
+    const forms = reactive({
+      title: '',
+      navBarHeight: 0 // 顶部高度
+    });
+
+    const onClickLeft = () => {
+      if (browser().isApp) {
+        postMessage({
+          api: 'goBack'
+        });
+      } else {
+        router.back();
+      }
+    };
+    const onClickRight = () => {
+      emit('rightClick');
+    };
+
+    onMounted(() => {
+      forms.title = props.title || (route.meta.title as string);
+    });
+
+    watch(
+      () => props.title,
+      () => {
+        forms.title = props.title || (route.meta.title as string);
+      }
+    );
+    return () => (
+      <>
+        {slots.content ? (
+          <div
+            style={{
+              paddingTop: `${forms.navBarHeight}px`,
+              background: props.background
+            }}
+            class={styles.headerSection}>
+            {slots.content(forms.navBarHeight)}
+          </div>
+        ) : (
+          <>
+            <div
+              style={{
+                minHeight: `calc(var(--van-nav-bar-height) + ${forms.navBarHeight}px)`
+              }}
+              class={styles.headerSection}>
+              <NavBar
+                title={forms.title}
+                class={[styles.colHeader]}
+                style={{
+                  background: props.background,
+                  color: props.color,
+                  paddingTop: `${forms.navBarHeight}px`
+                }}
+                left-arrow={props.isBack}
+                rightText={props.rightText}
+                fixed={props.isFixed}
+                zIndex={2000}
+                border={props.border}
+                onClickLeft={onClickLeft}
+                onClickRight={onClickRight}
+                v-slots={{
+                  right: () =>
+                    (slots.right && slots.right()) || props.rightText,
+                  title: () => (slots.title && slots.title()) || forms.title
+                }}></NavBar>
+            </div>
+            {slots.default ? slots.default() : null}
+          </>
+        )}
+      </>
+    );
+  }
+});

+ 64 - 0
src/components/m-search/index.module.less

@@ -0,0 +1,64 @@
+.m-search {
+  --van-cell-background-color: transparent;
+  input::placeholder {
+    color: var(--k-gray-4);
+  }
+  :global {
+    .van-field__control {
+      -webkit-user-select: text !important;
+      user-select: text !important;
+      font-size: 13px;
+    }
+    .van-search__field {
+      background: transparent !important;
+    }
+    .van-field__right-icon {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      padding-right: 4px;
+    }
+  }
+  &.default {
+    :global {
+      .van-search__content {
+        background: #f6f6f6 !important;
+      }
+    }
+  }
+
+  &.white {
+    :global {
+      .van-search__content {
+        background: #fff !important;
+      }
+    }
+  }
+
+  &.transparent {
+    :global {
+      .van-search__content {
+        background: rgba(255, 255, 255, 0.16);
+        input::placeholder {
+          color: #fff;
+        }
+        input {
+          color: #fff;
+        }
+        .van-field__clear {
+          color: #fff;
+        }
+      }
+    }
+  }
+
+  .searchBtn {
+    width: 56px;
+    height: 27px;
+    padding: 0;
+    font-size: 14px;
+    font-weight: 500;
+    --van-button-mini-height: 28px;
+    --van-font-size-xs: 14px;
+  }
+}

+ 85 - 0
src/components/m-search/index.tsx

@@ -0,0 +1,85 @@
+import { Button, Icon, Search } from 'vant';
+import { PropType, defineComponent, reactive, watch } from 'vue';
+import styles from './index.module.less';
+import iconSearch from '@/common/images/icon-search.png';
+
+type inputBackground = 'default' | 'white' | 'transparent';
+export default defineComponent({
+  name: 'm-search',
+  props: {
+    modelValue: {
+      type: String,
+      default: ''
+    },
+    shape: {
+      type: String as PropType<'round' | 'square'>,
+      default: 'round'
+    },
+    disabled: {
+      type: Boolean,
+      default: false
+    },
+    autofocus: {
+      // ios系统不支持
+      type: Boolean,
+      default: false
+    },
+    placeholder: {
+      type: String,
+      default: '请输入搜索关键词'
+    },
+    background: {
+      type: String,
+      default: '#fff'
+    },
+    inputBackground: {
+      type: String as PropType<inputBackground>,
+      default: 'default'
+    }
+  },
+  emits: ['search'],
+  setup(props, { slots, emit }) {
+    const forms = reactive({
+      search: props.modelValue || ''
+    });
+
+    watch(
+      () => props.modelValue,
+      () => {
+        forms.search = props.modelValue;
+      }
+    );
+    return () => (
+      <Search
+        class={[styles['m-search'], styles[props.inputBackground]]}
+        shape={props.shape}
+        background={props.background}
+        placeholder={props.placeholder}
+        disabled={props.disabled}
+        autofocus={props.autofocus}
+        v-model={forms.search}
+        clearTrigger="always"
+        onClear={() => {
+          console.log('clear');
+          forms.search = '';
+          emit('search', forms.search);
+        }}
+        onSearch={() => emit('search', forms.search)}>
+        {{
+          left: () => slots.left && slots.left(),
+          'left-icon': () => <Icon name={iconSearch} class={styles.leftIcon} />,
+          'right-icon': () => (
+            <Button
+              class={styles.searchBtn}
+              round
+              type="primary"
+              size="mini"
+              onClick={() => emit('search', forms.search)}>
+              搜索
+            </Button>
+          )
+        }}
+      </Search>
+    );
+  }
+});

+ 13 - 0
src/components/m-sticky/index.module.less

@@ -0,0 +1,13 @@
+.sticky {
+  position: sticky;
+  top: 0;
+  z-index: 99;
+}
+
+.white {
+  background-color: #fff;
+  > div {
+    padding-top: 15px;
+    box-shadow: 0px 0px 10px 0px rgba(216, 216, 216, 0.5);
+  }
+}

+ 117 - 0
src/components/m-sticky/index.tsx

@@ -0,0 +1,117 @@
+import { Sticky } from 'vant';
+import {
+  PropType,
+  defineComponent,
+  nextTick,
+  onMounted,
+  reactive,
+  ref,
+  watch
+} from 'vue';
+import styles from './index.module.less';
+import { useRect } from '@vant/use';
+
+export default defineComponent({
+  name: 'm-sticky',
+  props: {
+    position: {
+      type: String as PropType<'top' | 'bottom'>,
+      default: 'top'
+    },
+    mode: {
+      type: String as PropType<'fixed' | 'sticky'>,
+      default: 'fixed'
+    },
+    offsetTop: {
+      type: String,
+      default: '0px'
+    },
+    offsetBottom: {
+      default: '0px'
+    },
+    // 变量名
+    varName: {
+      type: String,
+      default: '--header-height'
+    }
+  },
+  setup(props, { slots, emit }) {
+    const forms = reactive({
+      divStyle: {} as any,
+      heightV: 0,
+      sectionStyle: {
+        width: '100%',
+        height: 'auto',
+        left: '0'
+      }
+    });
+
+    const __initHeight = (height: any) => {
+      forms.sectionStyle.height = `${height}px`;
+      forms.heightV = height;
+
+      // 设置名称
+      document.documentElement.style.setProperty(props.varName, `${height}px`);
+      emit('barHeight', height);
+    };
+
+    const divRef = ref();
+    const div2Ref = ref();
+    onMounted(() => {
+      if (props.position === 'top') {
+        forms.divStyle.top = props.offsetTop || '0px';
+      } else {
+        forms.divStyle.bottom = props.offsetBottom || '0px';
+      }
+
+      nextTick(() => {
+        // 为了处理刚开始头部高度为0的情况
+        const { height } = useRect(divRef.value);
+        __initHeight(height);
+
+        setTimeout(() => {
+          const { height } = useRect(divRef.value);
+          // 判断获取的高度是否一致,如果一致则不做处理
+          if (height === forms.heightV) return;
+          __initHeight(height);
+        }, 200);
+
+        // 为了处理头部第一次获取高度不对的问题
+        setTimeout(() => {
+          const { height } = useRect(div2Ref.value);
+          if (height !== forms.heightV && props.position === 'top') {
+            __initHeight(height);
+          }
+        }, 1000);
+      });
+    });
+
+    watch(
+      () => props.offsetTop,
+      () => {
+        forms.divStyle.top = props.offsetTop;
+      }
+    );
+    watch(
+      () => props.offsetBottom,
+      () => {
+        forms.divStyle.bottom = props.offsetBottom;
+      }
+    );
+    return () => (
+      <div
+        style={[forms.sectionStyle]}
+        class={props.mode === 'sticky' && styles.sticky}>
+        <div
+          ref={divRef}
+          class={[
+            'van-sticky',
+            props.mode === 'fixed' ? 'van-sticky--fixed' : ''
+          ]}
+          style={[forms.divStyle, forms.sectionStyle]}>
+          <div ref={div2Ref}>{slots.default && slots.default()}</div>
+        </div>
+      </div>
+    );
+  }
+});

+ 0 - 0
src/components/m-uploader/index.module.less


+ 24 - 0
src/components/m-uploader/index.tsx

@@ -0,0 +1,24 @@
+import { defineComponent } from 'vue';
+import styles from './index.module.less';
+import { Uploader } from 'vant';
+
+export default defineComponent({
+  name: 'm-uploader',
+  props: {
+    maxCount: {
+      type: Number,
+      default: 1
+    },
+    maxSize: {
+      type: Number,
+      default: 2048
+    }
+  },
+  setup(props) {
+    return () => (
+      <>
+        <Uploader maxCount={props.maxCount} />
+      </>
+    );
+  }
+});

+ 16 - 0
src/helpers/deep-clone.ts

@@ -0,0 +1,16 @@
+const deepClone = (obj: any) => {
+  if (obj === null) return null;
+  const clone = Object.assign({}, obj);
+  Object.keys(clone).forEach(
+    key =>
+      (clone[key] =
+        typeof obj[key] === 'object' ? deepClone(obj[key]) : obj[key])
+  );
+  if (Array.isArray(obj)) {
+    clone.length = obj.length;
+    return Array.from(clone);
+  }
+  return clone;
+};
+
+export default deepClone;

+ 116 - 0
src/helpers/native-message.ts

@@ -0,0 +1,116 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import { browser, getRandomKey } from '@/helpers/utils';
+
+export interface IPostMessage {
+  api: string;
+  content?: any;
+}
+
+/**
+ * 劫持postMessage
+ */
+
+const originalPostMessage = window.postMessage;
+
+window.postMessage = (message: IPostMessage) => {
+  // console.log('通过劫持', message)
+  originalPostMessage(message, '*');
+};
+
+/**
+ *
+ * 目前已支持API
+ *
+ * openWebView
+ *
+ */
+
+type CallBack = (evt?: IPostMessage) => void;
+
+// eslint-disable-next-line @typescript-eslint/no-empty-function
+const loop = () => {};
+
+const calls: { [key: string]: CallBack | CallBack[] } = {};
+
+const browserInfo = browser();
+
+if (browserInfo.isApp) {
+  window.addEventListener('message', evt => {
+    try {
+      console.log('app交互接受:', evt.data);
+      const data = evt.data
+        ? typeof evt.data === 'object'
+          ? evt.data
+          : JSON.parse(evt.data)
+        : {};
+      const uuid = data.content?.uuid || data.uuid;
+      console.log(uuid, data.content, 'uuid');
+      try {
+        if (data.content) {
+          data.content = JSON.parse(data.content);
+        }
+      } catch (error) {
+        //
+      }
+      if (data?.content?.uuid) {
+        // console.log('data', data)
+      }
+      if (!uuid) {
+        const keys = Object.keys(calls).filter(
+          key => key.indexOf(data.api) === 0
+        );
+        // console.log(keys, 'keys')
+        // console.log(data, 'data')
+        for (const key of keys) {
+          const callback = calls[key] || loop;
+          typeof callback === 'function' && callback(data);
+        }
+        return;
+      }
+      const callId = data.content?.uuid || data.uuid || data.api + data.uuid;
+      const callback = calls[callId] || loop;
+      // console.log(data, 'data')
+      typeof callback === 'function' && callback(data);
+    } catch (error) {
+      console.error('通信消息解析错误', error);
+    }
+  });
+}
+
+const instance: any =
+  (window as any).DAYA || (window as any).webkit?.messageHandlers?.DAYA;
+
+export const postMessage = (data: IPostMessage, callback?: CallBack) => {
+  if (browserInfo.isApp) {
+    const uuid = getRandomKey();
+    calls[uuid] = callback || loop;
+    data.content = data.content ? { ...data.content, uuid } : { uuid };
+    console.log('app交互发送:', data);
+    instance.postMessage(JSON.stringify(data));
+  }
+};
+
+export const listenerMessage = (api: string, callback: CallBack) => {
+  if (browserInfo.isApp) {
+    const uuid = api + getRandomKey();
+    calls[uuid] = callback || loop;
+  }
+};
+
+export const removeListenerMessage = (api: string, callback: CallBack) => {
+  if (browserInfo.isApp) {
+    const uuid = api;
+    if (Array.isArray(calls[uuid])) {
+      const indexOf = (calls[uuid] as CallBack[]).indexOf(callback);
+      (calls[uuid] as CallBack[]).splice(indexOf, 1);
+    }
+  }
+};
+
+export const promisefiyPostMessage = (
+  data: IPostMessage
+): Promise<IPostMessage | undefined> => {
+  return new Promise(resolve => {
+    postMessage(data, res => resolve(res));
+  });
+};

+ 111 - 0
src/helpers/request.ts

@@ -0,0 +1,111 @@
+import { extend } from 'umi-request';
+import cleanDeep from 'clean-deep';
+import { browser } from '@/helpers/utils';
+import { setLogout, setLoginError } from '@/state';
+import { postMessage } from './native-message';
+import { showLoadingToast, showToast, closeToast } from 'vant';
+
+export interface SearchInitParams {
+  rows?: string | number;
+  page?: string | number;
+}
+
+const request = extend({
+  // requestType: 'form',
+  hideLoading: true, // 默认都不显示加载
+  timeout: 20000,
+  timeoutMessage: '请求超时'
+});
+
+// 是否是初始化接口
+let initRequest = false;
+let toast: ReturnType<typeof setTimeout>;
+
+request.interceptors.request.use(
+  (url, options: any) => {
+    if (!options.hideLoading) {
+      clearTimeout(toast);
+      showLoadingToast({
+        message: '加载中...',
+        forbidClick: true,
+        duration: 0
+      });
+    }
+
+    initRequest = options.initRequest || false;
+    const Authorization = sessionStorage.getItem('Authorization') || '';
+    const authHeaders: any = {};
+    if (
+      Authorization &&
+      ![
+        '/api-oauth/userlogin',
+        // `${state.platformApi}/user/getUserInfo`,
+        '/api-oauth/open/sendSms'
+      ].includes(url)
+    ) {
+      authHeaders.Authorization = Authorization;
+    }
+
+    return {
+      url,
+      options: {
+        ...options,
+        params: cleanDeep(options.params),
+        data: cleanDeep(options.data),
+        headers: {
+          ...options.headers,
+          ...authHeaders
+        }
+      }
+    };
+  },
+  { global: false }
+);
+
+request.interceptors.response.use(
+  async res => {
+    toast = setTimeout(() => {
+      closeToast();
+    }, 100);
+
+    if (res.status > 299 || res.status < 200) {
+      clearTimeout(toast);
+      const msg = '服务器错误,状态码' + res.status;
+      showToast(msg);
+      throw new Error(msg);
+    }
+    const data = await res.clone().json();
+    // 999 为特殊code码
+    if (data.code !== 200 && data.errCode !== 0 && data.code !== 999) {
+      let msg = data.msg || data.message || '处理失败,请重试';
+      if (initRequest) {
+        if (data.code === 403 || data.code === 5000) {
+          setLogout();
+        } else {
+          setLoginError();
+        }
+      }
+      console.log(data.code, '5104');
+      if (!(data.code === 403 || data.code === 5000)) {
+        clearTimeout(toast);
+        showToast(msg);
+      }
+      const browserInfo = browser();
+      if (data.code === 5000) {
+        msg += '5000';
+        if (browserInfo.isApp) {
+          postMessage({
+            api: 'login'
+          });
+        } else {
+          setLogout();
+        }
+      }
+      throw new Error(msg);
+    }
+    return res;
+  },
+  { global: false }
+);
+
+export default request;

+ 406 - 0
src/helpers/toolsValidate.ts

@@ -0,0 +1,406 @@
+/* eslint-disable no-useless-escape */
+/**
+ * 验证百分比(不可以小数)
+ * @param val 当前值字符串
+ * @returns 返回处理后的字符串
+ */
+export function verifyNumberPercentage(val: string): string {
+  // 匹配空格
+  let v = val.replace(/(^\s*)|(\s*$)/g, '');
+  // 只能是数字和小数点,不能是其他输入
+  v = v.replace(/[^\d]/g, '');
+  // 不能以0开始
+  v = v.replace(/^0/g, '');
+  // 数字超过100,赋值成最大值100
+  v = v.replace(/^[1-9]\d\d{1,3}$/, '100');
+  // 返回结果
+  return v;
+}
+
+/**
+ * 验证百分比(可以小数)
+ * @param val 当前值字符串
+ * @returns 返回处理后的字符串
+ */
+export function verifyNumberPercentageFloat(val: string): string {
+  let v = verifyNumberIntegerAndFloat(val);
+  // 数字超过100,赋值成最大值100
+  v = v.replace(/^[1-9]\d\d{1,3}$/, '100');
+  // 超过100之后不给再输入值
+  v = v.replace(/^100\.$/, '100');
+  // 返回结果
+  return v;
+}
+
+/**
+ * 小数或整数(不可以负数)
+ * @param val 当前值字符串
+ * @returns 返回处理后的字符串
+ */
+export function verifyNumberIntegerAndFloat(val: string) {
+  // 匹配空格
+  let v = val.replace(/(^\s*)|(\s*$)/g, '');
+  // 只能是数字和小数点,不能是其他输入
+  v = v.replace(/[^\d.]/g, '');
+  // 以0开始只能输入一个
+  v = v.replace(/^0{2}$/g, '0');
+  // 保证第一位只能是数字,不能是点
+  v = v.replace(/^\./g, '');
+  // 小数只能出现1位
+  v = v.replace('.', '$#$').replace(/\./g, '').replace('$#$', '.');
+  // 小数点后面保留2位
+  v = v.replace(/^(\-)*(\d+)\.(\d\d).*$/, '$1$2.$3');
+  // 返回结果
+  return v;
+}
+
+/**
+ * 正整数验证
+ * @param val 当前值字符串
+ * @returns 返回处理后的字符串
+ */
+export function verifiyNumberInteger(val: string) {
+  // 匹配空格
+  let v = val.replace(/(^\s*)|(\s*$)/g, '');
+  // 去掉 '.' , 防止贴贴的时候出现问题 如 0.1.12.12
+  v = v.replace(/[\.]*/g, '');
+  // 去掉以 0 开始后面的数, 防止贴贴的时候出现问题 如 00121323
+  v = v.replace(/(^0[\d]*)$/g, '0');
+  // 首位是0,只能出现一次
+  v = v.replace(/^0\d$/g, '0');
+  // 只匹配数字
+  v = v.replace(/[^\d]/g, '');
+  // 返回结果
+  return v;
+}
+
+/**
+ * 检测正整数
+ * @param val 当前值字符串
+ * @returns 返回boolean
+ */
+export function checkNumberInteger(val: string) {
+  const rep = /[\.]/;
+  return !!rep.test(val);
+}
+
+/**
+ * 去掉中文及空格
+ * @param val 当前值字符串
+ * @returns 返回处理后的字符串
+ */
+export function verifyCnAndSpace(val: string) {
+  // 匹配中文与空格
+  let v = val.replace(/[\u4e00-\u9fa5\s]+/g, '');
+  // 匹配空格
+  v = v.replace(/(^\s*)|(\s*$)/g, '');
+  // 返回结果
+  return v;
+}
+
+/**
+ * 去掉英文及空格
+ * @param val 当前值字符串
+ * @returns 返回处理后的字符串
+ */
+export function verifyEnAndSpace(val: string) {
+  // 匹配英文与空格
+  let v = val.replace(/[a-zA-Z]+/g, '');
+  // 匹配空格
+  v = v.replace(/(^\s*)|(\s*$)/g, '');
+  // 返回结果
+  return v;
+}
+
+/**
+ * 禁止输入空格
+ * @param val 当前值字符串
+ * @returns 返回处理后的字符串
+ */
+export function verifyAndSpace(val: string) {
+  // 匹配空格
+  const v = val.replace(/(^\s*)|(\s*$)/g, '');
+  // 返回结果
+  return v;
+}
+
+/**
+ * 金额用 `,` 区分开
+ * @param val 当前值字符串
+ * @returns 返回处理后的字符串
+ */
+export function verifyNumberComma(val: string) {
+  // 调用小数或整数(不可以负数)方法
+  let v: any = verifyNumberIntegerAndFloat(val);
+  // 字符串转成数组
+  v = v.toString().split('.');
+  // \B 匹配非单词边界,两边都是单词字符或者两边都是非单词字符
+  v[0] = v[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
+  // 数组转字符串
+  v = v.join('.');
+  // 返回结果
+  return v;
+}
+
+/**
+ * 匹配文字变色(搜索时)
+ * @param val 当前值字符串
+ * @param text 要处理的字符串值
+ * @param color 搜索到时字体高亮颜色
+ * @returns 返回处理后的字符串
+ */
+export function verifyTextColor(val: string, text = '', color = 'red') {
+  // 返回内容,添加颜色
+  const v = text.replace(
+    new RegExp(val, 'gi'),
+    `<span style='color: ${color}'>${val}</span>`
+  );
+  // 返回结果
+  return v;
+}
+
+/**
+ * 数字转中文大写
+ * @param val 当前值字符串
+ * @param unit 默认:仟佰拾亿仟佰拾万仟佰拾元角分
+ * @returns 返回处理后的字符串
+ */
+export function verifyNumberCnUppercase(
+  val: any,
+  unit = '仟佰拾亿仟佰拾万仟佰拾元角分',
+  v = ''
+) {
+  // 当前内容字符串添加 2个0,为什么??
+  val += '00';
+  // 返回某个指定的字符串值在字符串中首次出现的位置,没有出现,则该方法返回 -1
+  const lookup = val.indexOf('.');
+  // substring:不包含结束下标内容,substr:包含结束下标内容
+  if (lookup >= 0) val = val.substring(0, lookup) + val.substr(lookup + 1, 2);
+  // 根据内容 val 的长度,截取返回对应大写
+  unit = unit.substr(unit.length - val.length);
+  // 循环截取拼接大写
+  for (let i = 0; i < val.length; i++) {
+    v += '零壹贰叁肆伍陆柒捌玖'.substr(val.substr(i, 1), 1) + unit.substr(i, 1);
+  }
+  // 正则处理
+  v = v
+    .replace(/零角零分$/, '整')
+    .replace(/零[仟佰拾]/g, '零')
+    .replace(/零{2,}/g, '零')
+    .replace(/零([亿|万])/g, '$1')
+    .replace(/零+元/, '元')
+    .replace(/亿零{0,3}万/, '亿')
+    .replace(/^元/, '零元');
+  // 返回结果
+  return v;
+}
+
+/**
+ * 手机号码
+ * @param val 当前值字符串
+ * @returns 返回 true: 手机号码正确
+ */
+export function verifyPhone(val: string) {
+  // false: 手机号码不正确
+  if (
+    !/^((12[0-9])|(13[0-9])|(14[5|7])|(15([0-3]|[5-9]))|(18[0,5-9]))\d{8}$/.test(
+      val
+    )
+  )
+    return false;
+  // true: 手机号码正确
+  else return true;
+}
+
+/**
+ * 国内电话号码
+ * @param val 当前值字符串
+ * @returns 返回 true: 国内电话号码正确
+ */
+export function verifyTelPhone(val: string) {
+  // false: 国内电话号码不正确
+  if (!/\d{3}-\d{8}|\d{4}-\d{7}/.test(val)) return false;
+  // true: 国内电话号码正确
+  else return true;
+}
+
+/**
+ * 登录账号 (字母开头,允许5-16字节,允许字母数字下划线)
+ * @param val 当前值字符串
+ * @returns 返回 true: 登录账号正确
+ */
+export function verifyAccount(val: string) {
+  // false: 登录账号不正确
+  if (!/^[a-zA-Z][a-zA-Z0-9_]{4,15}$/.test(val)) return false;
+  // true: 登录账号正确
+  else return true;
+}
+
+/**
+ * 密码 (以字母开头,长度在6~16之间,只能包含字母、数字和下划线)
+ * @param val 当前值字符串
+ * @returns 返回 true: 密码正确
+ */
+export function verifyPassword(val: string) {
+  // false: 密码不正确
+  if (!/^[a-zA-Z]\w{5,15}$/.test(val)) return false;
+  // true: 密码正确
+  else return true;
+}
+
+/**
+ * 强密码 (字母+数字+特殊字符,长度在6-16之间)
+ * @param val 当前值字符串
+ * @returns 返回 true: 强密码正确
+ */
+export function verifyPasswordPowerful(val: string) {
+  // false: 强密码不正确
+  if (
+    !/^(?![a-zA-z]+$)(?!\d+$)(?![!@#$%^&\.*]+$)(?![a-zA-z\d]+$)(?![a-zA-z!@#$%^&\.*]+$)(?![\d!@#$%^&\.*]+$)[a-zA-Z\d!@#$%^&\.*]{6,16}$/.test(
+      val
+    )
+  )
+    return false;
+  // true: 强密码正确
+  else return true;
+}
+
+/**
+ * 密码强度
+ * @param val 当前值字符串
+ * @description 弱:纯数字,纯字母,纯特殊字符
+ * @description 中:字母+数字,字母+特殊字符,数字+特殊字符
+ * @description 强:字母+数字+特殊字符
+ * @returns 返回处理后的字符串:弱、中、强
+ */
+export function verifyPasswordStrength(val: string) {
+  let v = '';
+  // 弱:纯数字,纯字母,纯特殊字符
+  if (/^(?:\d+|[a-zA-Z]+|[!@#$%^&\.*]+){6,16}$/.test(val)) v = '弱';
+  // 中:字母+数字,字母+特殊字符,数字+特殊字符
+  if (
+    /^(?![a-zA-z]+$)(?!\d+$)(?![!@#$%^&\.*]+$)[a-zA-Z\d!@#$%^&\.*]{6,16}$/.test(
+      val
+    )
+  )
+    v = '中';
+  // 强:字母+数字+特殊字符
+  if (
+    /^(?![a-zA-z]+$)(?!\d+$)(?![!@#$%^&\.*]+$)(?![a-zA-z\d]+$)(?![a-zA-z!@#$%^&\.*]+$)(?![\d!@#$%^&\.*]+$)[a-zA-Z\d!@#$%^&\.*]{6,16}$/.test(
+      val
+    )
+  )
+    v = '强';
+  // 返回结果
+  return v;
+}
+
+/**
+ * IP地址
+ * @param val 当前值字符串
+ * @returns 返回 true: IP地址正确
+ */
+export function verifyIPAddress(val: string) {
+  // false: IP地址不正确
+  if (
+    !/^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/.test(
+      val
+    )
+  )
+    return false;
+  // true: IP地址正确
+  else return true;
+}
+
+/**
+ * 邮箱
+ * @param val 当前值字符串
+ * @returns 返回 true: 邮箱正确
+ */
+export function verifyEmail(val: string) {
+  // false: 邮箱不正确
+  const reg =
+    /^(([^<>()\[\]\\.,;:\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,}))$/;
+  if (!reg.test(val)) return false;
+  // true: 邮箱正确
+  else return true;
+}
+
+/**
+ * 身份证
+ * @param val 当前值字符串
+ * @returns 返回 true: 身份证正确
+ */
+export function verifyIdCard(val: string) {
+  // false: 身份证不正确
+  if (
+    !/^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/.test(
+      val
+    )
+  )
+    return false;
+  // true: 身份证正确
+  else return true;
+}
+
+/**
+ * 姓名
+ * @param val 当前值字符串
+ * @returns 返回 true: 姓名正确
+ */
+export function verifyFullName(val: string) {
+  // false: 姓名不正确
+  if (!/^[\u4e00-\u9fa5]{1,6}(·[\u4e00-\u9fa5]{1,6}){0,2}$/.test(val))
+    return false;
+  // true: 姓名正确
+  else return true;
+}
+
+/**
+ * 邮政编码
+ * @param val 当前值字符串
+ * @returns 返回 true: 邮政编码正确
+ */
+export function verifyPostalCode(val: string) {
+  // false: 邮政编码不正确
+  if (!/^[1-9][0-9]{5}$/.test(val)) return false;
+  // true: 邮政编码正确
+  else return true;
+}
+
+/**
+ * url 处理
+ * @param val 当前值字符串
+ * @returns 返回 true: url 正确
+ */
+export function verifyUrl(val: string) {
+  // false: url不正确
+  // !/^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})).?)(?::\d{2,5})?(?:[/?#]\S*)?$/i.test(
+  //   val
+  // )
+  if (
+    !/^(http|ftp|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&amp;:/~\+#]*[\w\-\@?^=%&amp;/~\+#])?$/.test(
+      val
+    )
+  )
+    return false;
+  // true: url正确
+  else return true;
+}
+
+/**
+ * 车牌号
+ * @param val 当前值字符串
+ * @returns 返回 true:车牌号正确
+ */
+export function verifyCarNum(val: string) {
+  // false: 车牌号不正确
+  if (
+    !/^(([京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-Z](([0-9]{5}[DF])|([DF]([A-HJ-NP-Z0-9])[0-9]{4})))|([京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-Z][A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9挂学警港澳使领]))$/.test(
+      val
+    )
+  )
+    return false;
+  // true:车牌号正确
+  else return true;
+}

+ 52 - 0
src/helpers/utils.ts

@@ -0,0 +1,52 @@
+export const browser = () => {
+  const u = navigator.userAgent;
+  return {
+    trident: u.indexOf('Trident') > -1, //IE内核
+    presto: u.indexOf('Presto') > -1, //opera内核
+    webKit: u.indexOf('AppleWebKit') > -1, //苹果、谷歌内核
+    gecko: u.indexOf('Gecko') > -1 && u.indexOf('KHTML') == -1, //火狐内核
+    mobile: !!u.match(/AppleWebKit.*Mobile.*/), //是否为移动终端
+    ios: !!u.match(/Mac OS X/), //ios终端
+    // ios: !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/), //ios终端
+    android: u.indexOf('DAYAAPPA') > -1 || u.indexOf('Adr') > -1, //android终端
+    iPhone: u.indexOf('DAYAAPPI') > -1, //是否为iPhone或者QQHD浏览器
+    isApp:
+      u.indexOf('DAYAAPPI') > -1 ||
+      u.indexOf('DAYAAPPA') > -1 ||
+      u.indexOf('Adr') > -1,
+    iPad: u.indexOf('iPad') > -1, //是否iPad
+    webApp: u.indexOf('Safari') == -1, //是否web应该程序,没有头部与底部
+    weixin: u.indexOf('MicroMessenger') > -1, //是否微信 (2015-01-22新增)
+    alipay: u.indexOf('AlipayClient') > -1, //是否支付宝
+    huawei: !!u.match(/huawei/i) || !!u.match(/honor/i),
+    xiaomi: !!u.match(/mi\s/i) || !!u.match(/redmi/i) || !!u.match(/mix/i)
+  };
+};
+
+export const getRandomKey = () => {
+  const key = '' + new Date().getTime() + Math.floor(Math.random() * 1000000);
+  return key;
+};
+
+/**
+ * 删除token
+ */
+export const removeAuth = () => {
+  sessionStorage.removeItem('Authorization');
+};
+
+/**
+ * 设置token
+ * @param token
+ * @returns {void}
+ */
+export const setAuth = (token: any) => {
+  sessionStorage.setItem('Authorization', token);
+};
+
+/**
+ * 获取token
+ */
+export const getAuth = () => {
+  sessionStorage.getItem('Authorization');
+};

+ 24 - 0
src/main.ts

@@ -3,8 +3,32 @@ import App from './App.tsx';
 import router from './router/index';
 import dayjs from 'dayjs';
 import 'dayjs/locale/zh-cn';
+import 'vant/lib/index.css';
 import './component-ui/index.less';
 import './styles/index.less';
+import { promisefiyPostMessage, postMessage } from './helpers/native-message';
+import { state } from './state';
+import { setAuth } from './helpers/utils';
+
+// 获取token
+promisefiyPostMessage({ api: 'getToken' }).then((res: any) => {
+  console.log(res, 'res');
+  const content = res.content;
+  if (content?.accessToken) {
+    setAuth(content.tokenType + ' ' + content.accessToken);
+  }
+});
+
+// 导航栏高度
+postMessage({ api: 'getNavHeight' }, (res: any) => {
+  const { content } = res as any;
+  const dpi = content.dpi || 2;
+  if (content.navHeight) {
+    const navHeight = content.navHeight / dpi;
+    console.log(navHeight, 'navHeight');
+    state.navBarHeight = navHeight;
+  }
+});
 
 const app = createApp(App);
 

+ 37 - 9
src/router/index.ts

@@ -1,17 +1,45 @@
+import { browser } from '@/helpers/utils';
+import { showDialog } from 'vant';
 import { createRouter, createWebHashHistory, Router } from 'vue-router';
+import { postMessage } from '@/helpers/native-message';
+import routes from './routes-common';
 
 const router: Router = createRouter({
   history: createWebHashHistory(),
-  routes: [
-    {
-      path: '/home',
-      name: 'home',
-      component: () => import('@/views/home/index'),
-      meta: {
-        title: '首页'
-      }
+  routes,
+  scrollBehavior(to) {
+    if (to.hash) {
+      return {
+        el: to.hash,
+        behavior: 'smooth'
+      };
     }
-  ]
+  }
+});
+
+let isOpen = false;
+router.onError(error => {
+  if (error instanceof Error) {
+    const isChunkLoadFailed = error.name.indexOf('chunk');
+    const targetPath = router.currentRoute.value.fullPath;
+    console.log(error);
+    if (isChunkLoadFailed && !isOpen) {
+      isOpen = true;
+      showDialog({
+        title: '更新提示',
+        message: 'APP有更新请点击确定刷新页面?',
+        confirmButtonColor: 'var(--van-primary)'
+      }).then(() => {
+        // on close
+        if (browser().isApp) {
+          postMessage({ api: 'back' });
+        } else {
+          location.hash = targetPath;
+          window.location.reload();
+        }
+      });
+    }
+  }
 });
 
 export default router;

+ 18 - 0
src/router/router-root.ts

@@ -0,0 +1,18 @@
+// 不需要登录的路由
+export default [
+  {
+    path: '/test',
+    name: 'test',
+    component: () => import('@/views/test/index'),
+    meta: {
+      title: '测试'
+    }
+  },
+  {
+    path: '/:pathMatch(.*)*',
+    component: () => import('@/views/404'),
+    meta: {
+      title: '404'
+    }
+  }
+];

+ 34 - 0
src/router/routes-common.ts

@@ -0,0 +1,34 @@
+// import Auth from '@/views/layout/auth';
+import rootRouter from './router-root';
+
+type metaType = {
+  isRegister: boolean;
+};
+
+export default [
+  // {
+  //   path: '/',
+  //   component: Auth,
+  //   children: [
+  //     ...router,
+  //     {
+  //       path: '/login',
+  //       name: 'login',
+  //       component: () => import('@/views/layout/login'),
+  //       meta: {
+  //         isRegister: false
+  //       } as metaType
+  //     }
+  //   ]
+  // },
+  {
+    path: '/home',
+    name: 'home',
+    component: () => import('@/views/home/index'),
+    meta: {
+      title: '首页'
+    }
+  },
+
+  ...rootRouter
+];

+ 49 - 0
src/state.ts

@@ -0,0 +1,49 @@
+import { reactive } from 'vue';
+import { browser } from './helpers/utils';
+import { postMessage } from './helpers/native-message';
+
+type status = 'init' | 'login' | 'logout' | 'error';
+
+export const state = reactive({
+  user: {
+    status: 'init' as status,
+    data: {} as any
+  },
+  navBarHeight: 0 // 状态栏高度
+});
+
+export const setLoginInit = () => {
+  state.user.status = 'init';
+  state.user.data = null;
+};
+
+export const setLogin = (data: any) => {
+  state.user.status = 'login';
+  state.user.data = data;
+};
+
+export const setLogout = () => {
+  state.user.status = 'logout';
+  state.user.data = null;
+};
+
+export const setLoginError = () => {
+  state.user.status = 'error';
+  state.user.data = null;
+};
+
+// 用于处理跳转地址,如果是在app内,则打开一个新的webview, 否则跳转连接
+export const openDefaultWebView = (url?: string, callBack?: any) => {
+  if (browser().isApp) {
+    postMessage({
+      api: 'openWebView',
+      content: {
+        url,
+        orientation: 1,
+        isHideTitle: false
+      }
+    });
+  } else {
+    callBack && callBack();
+  }
+};

+ 16 - 3
src/styles/index.less

@@ -1,3 +1,18 @@
+:root:root {
+  --k-primary: #01c1b5; // 主题色
+  --k-font-primary: #00b2a7; // 字体色
+  --van-pull-refresh-head-height: 55px;
+}
+
+// 默认输入框光标颜色
+input {
+  caret-color: var(--k-font-primary);
+}
+
+.van-skeleton {
+  padding: 0;
+}
+
 * {
   padding: 0;
   margin: 0;
@@ -6,7 +21,6 @@
 }
 
 #app {
-  font-family: Avenir, Helvetica, Arial, sans-serif;
   -webkit-font-smoothing: antialiased;
   -moz-osx-font-smoothing: grayscale;
   color: #333;
@@ -14,7 +28,6 @@
 }
 
 body {
-  background-color: #f6f6f6;
+  background-color: #f8f9fc;
   user-select: none;
-  margin-top: 0 !important;
 }

+ 0 - 0
src/views/404/index.module.less


+ 33 - 0
src/views/404/index.tsx

@@ -0,0 +1,33 @@
+import MEmpty from '@/components/m-empty';
+import MHeader from '@/components/m-header';
+import { browser } from '@/helpers/utils';
+import { defineComponent } from 'vue';
+import { useRouter } from 'vue-router';
+
+export default defineComponent({
+  name: 'page-404',
+  setup() {
+    const router = useRouter();
+    return () => (
+      <>
+        <MHeader />
+        <MEmpty
+          style={{
+            'min-height': 'calc(100vh - var(--van-nav-bar-height))',
+            paddingTop: '0 !important'
+          }}
+          showButton
+          description="页面找不到了~"
+          image="404"
+          onClick={() => {
+            if (browser().isApp) {
+              postMessage({ api: 'back' });
+            } else {
+              router.back();
+            }
+          }}
+        />
+      </>
+    );
+  }
+});

+ 8 - 1
src/views/home/index.tsx

@@ -1,8 +1,15 @@
+import { Button } from 'vant';
 import { defineComponent } from 'vue';
 
 export default defineComponent({
   name: 'home-page',
   setup() {
-    return () => <div style={{ fontSize: '18px' }}>home</div>;
+    return () => (
+      <div style={{ fontSize: '18px' }}>
+        <Button type="primary" block>
+          主要按钮
+        </Button>
+      </div>
+    );
   }
 });

+ 5 - 0
src/views/test/index.module.less

@@ -0,0 +1,5 @@
+:global {
+  .van-skeleton {
+    padding: 0;
+  }
+}

+ 155 - 0
src/views/test/index.tsx

@@ -0,0 +1,155 @@
+import MEmpty from '@/components/m-empty';
+import MFullRefresh from '@/components/m-full-refresh';
+import MHeader from '@/components/m-header';
+import MSearch from '@/components/m-search';
+import MSticky from '@/components/m-sticky';
+import MUploader from '@/components/m-uploader';
+import {
+  Button,
+  Cell,
+  CellGroup,
+  showLoadingToast,
+  Image,
+  Skeleton,
+  SkeletonAvatar,
+  SkeletonParagraph,
+  Search,
+  Field
+} from 'vant';
+import { defineComponent, reactive, ref } from 'vue';
+
+export default defineComponent({
+  name: 'test-page',
+  setup() {
+    const refreshing = ref(false);
+    const loading = ref(true);
+    const forms = reactive({
+      search: ''
+    });
+    const getData = () => {
+      refreshing.value = true;
+      setTimeout(() => {
+        refreshing.value = false;
+      }, 2000);
+    };
+
+    const onClick = () => {
+      showLoadingToast({
+        message: '',
+        forbidClick: true
+      });
+    };
+
+    setTimeout(() => {
+      loading.value = false;
+    }, 1000);
+    return () => (
+      <>
+        <MSticky>
+          <MHeader />
+          <MSearch v-model:modelValue={forms.search} />
+        </MSticky>
+
+        <Field style={{ marginTop: '12px' }} label="上传图片">
+          {{ input: () => <MUploader /> }}
+        </Field>
+        {/* <MFullRefresh
+          v-model:modelValue={refreshing.value}
+          onRefresh={getData}
+          style={{
+            minHeight: `calc(100vh - var(--header-height))`
+          }}>
+          <Skeleton row={3} loading={loading.value} style="flex-wrap: wrap">
+            {{
+              template: () =>
+                [1, 2, 3, 4, 5, 1, 2, 3, 4, 5].map((item: any) => (
+                  <CellGroup
+                    inset
+                    style={{
+                      marginTop: '12px',
+                      width: '100%'
+                    }}>
+                    <Cell center>
+                      {{
+                        icon: () => (
+                          <SkeletonAvatar
+                            style={{
+                              width: '50px',
+                              height: '50px',
+                              borderRadius: '50%',
+                              overflow: 'hidden',
+                              marginRight: '10px'
+                            }}
+                          />
+                        ),
+                        title: () => (
+                          <div
+                            style={{
+                              display: 'flex',
+                              justifyContent: 'space-between'
+                            }}>
+                            <SkeletonParagraph rowWidth={'40%'} />
+                            <SkeletonParagraph
+                              style={{
+                                marginTop: 0
+                              }}
+                              rowWidth={'20%'}
+                            />
+                          </div>
+                        ),
+                        label: () => <SkeletonParagraph />
+                      }}
+                    </Cell>
+                  </CellGroup>
+                )),
+              default: () => (
+                // <div
+                //   style={{
+                //     overflow: 'hidden'
+                //   }}>
+                //   {[1, 2, 3, 4, 5].map(() => (
+                //     <CellGroup
+                //       inset
+                //       style={{
+                //         marginTop: '12px'
+                //       }}>
+                //       <Cell
+                //         title="标题"
+                //         value="15:43:44"
+                //         label="乐团名称乐团名"
+                //         center>
+                //         {{
+                //           icon: () => (
+                //             <Image
+                //               style={{
+                //                 width: '50px',
+                //                 height: '50px',
+                //                 borderRadius: '50%',
+                //                 overflow: 'hidden',
+                //                 marginRight: '10px'
+                //               }}
+                //             />
+                //           )
+                //         }}
+                //       </Cell>
+                //     </CellGroup>
+                //   ))}
+                // </div>
+                <MEmpty
+                  style={{
+                    minHeight: `calc(100vh - var(--header-height))`
+                  }}
+                  description="暂无数据"
+                />
+              )
+            }}
+          </Skeleton>
+        </MFullRefresh> */}
+
+        {/* <Button type="primary" onClick={onClick}>
+          提交
+        </Button> */}
+      </>
+    );
+  }
+});

Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.