tangning vor 1 Woche
Ursprung
Commit
fb06132e4c
86 geänderte Dateien mit 9497 neuen und 5163 gelöschten Zeilen
  1. 15 0
      .eslintrc.cjs
  2. 16 11
      .gitignore
  3. 8 0
      .prettierrc.json
  4. 8 0
      auto-imports.d.ts
  5. 0 5
      babel.config.js
  6. 29 0
      components.d.ts
  7. 1 0
      env.d.ts
  8. 14 0
      index.html
  9. 0 19
      jsconfig.json
  10. 3026 0
      package-lock.json
  11. 37 35
      package.json
  12. BIN
      public/favicon.ico
  13. 0 17
      public/index.html
  14. 39 17
      src/App.vue
  15. 77 0
      src/api/api.ts
  16. 89 0
      src/assets/base.css
  17. 1 0
      src/assets/config.less
  18. BIN
      src/assets/images/active.png
  19. BIN
      src/assets/images/alipay56.png
  20. BIN
      src/assets/images/baicon.png
  21. BIN
      src/assets/images/icon/error.png
  22. BIN
      src/assets/images/icon/success.png
  23. BIN
      src/assets/images/icon/warn.png
  24. BIN
      src/assets/images/logoCn.png
  25. BIN
      src/assets/images/logoEn.png
  26. BIN
      src/assets/images/paypal.png
  27. BIN
      src/assets/images/scan-tip-en.png
  28. BIN
      src/assets/images/scan-tip.png
  29. BIN
      src/assets/images/stripe.png
  30. 1 0
      src/assets/images/vip_true.svg
  31. BIN
      src/assets/images/wechat56.png
  32. BIN
      src/assets/images/whietlogo.png
  33. BIN
      src/assets/images/whietlogoEn.png
  34. BIN
      src/assets/logo.png
  35. 1 0
      src/assets/logo.svg
  36. 25 0
      src/assets/main.css
  37. 29 47
      src/components/HelloWorld.vue
  38. 86 0
      src/components/TheWelcome.vue
  39. 193 0
      src/components/Toast/Confirm.vue
  40. 45 0
      src/components/Toast/Toast.vue
  41. 58 0
      src/components/Toast/index.ts
  42. 86 0
      src/components/WelcomeItem.vue
  43. 7 0
      src/components/icons/IconCommunity.vue
  44. 7 0
      src/components/icons/IconDocumentation.vue
  45. 7 0
      src/components/icons/IconEcosystem.vue
  46. 7 0
      src/components/icons/IconSupport.vue
  47. 19 0
      src/components/icons/IconTooling.vue
  48. 8 0
      src/components/mobile/footer.vue
  49. 33 0
      src/components/mobile/header.vue
  50. 24 0
      src/components/mobile/index.vue
  51. 81 0
      src/components/pc/footer.vue
  52. 28 0
      src/components/pc/header.vue
  53. 45 0
      src/components/pc/index.vue
  54. 161 0
      src/i18n/en.js
  55. 31 0
      src/i18n/index.js
  56. 157 0
      src/i18n/ja.js
  57. 159 0
      src/i18n/kr.js
  58. 159 0
      src/i18n/zh.js
  59. 0 4
      src/main.js
  60. 19 0
      src/main.ts
  61. 69 0
      src/router/index.ts
  62. 50 0
      src/router/mbRoute.ts
  63. 50 0
      src/router/pcRoute.ts
  64. 11 0
      src/stores/counter.ts
  65. 27 0
      src/stores/user.ts
  66. 121 0
      src/utils/api.js
  67. 41 0
      src/utils/file/base64Conver.ts
  68. 137 0
      src/utils/file/download.ts
  69. 150 0
      src/utils/index.ts
  70. 99 0
      src/utils/is.ts
  71. 31 0
      src/utils/request.js
  72. 47 0
      src/utils/status.ts
  73. 4 0
      src/utils/vconsole.js
  74. 364 0
      src/views/mobile/index.vue
  75. 27 0
      src/views/mobile/information/index.vue
  76. 27 0
      src/views/pc/appProduct/index.vue
  77. 27 0
      src/views/pc/device/index.vue
  78. 450 0
      src/views/pc/index.vue
  79. 27 0
      src/views/pc/information/index.vue
  80. 27 0
      src/views/pc/order/index.vue
  81. 27 0
      src/views/pc/scene/index.vue
  82. 36 0
      tsconfig.json
  83. 10 0
      tsconfig.node.json
  84. 59 0
      vite.config.ts
  85. 0 4
      vue.config.js
  86. 2743 5004
      yarn.lock

+ 15 - 0
.eslintrc.cjs

@@ -0,0 +1,15 @@
+/* eslint-env node */
+require('@rushstack/eslint-patch/modern-module-resolution')
+
+module.exports = {
+  root: true,
+  'extends': [
+    'plugin:vue/vue3-essential',
+    'eslint:recommended',
+    '@vue/eslint-config-typescript',
+    '@vue/eslint-config-prettier/skip-formatting'
+  ],
+  parserOptions: {
+    ecmaVersion: 'latest'
+  }
+}

+ 16 - 11
.gitignore

@@ -1,21 +1,26 @@
-.DS_Store
-node_modules
-/dist
-
-
-# local env files
-.env.local
-.env.*.local
-
-# Log files
+# Logs
+logs
+*.log
 npm-debug.log*
 yarn-debug.log*
 yarn-error.log*
 pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+.DS_Store
+dist
+dist-ssr
+coverage
+*.local
+
+/cypress/videos/
+/cypress/screenshots/
 
 # Editor directories and files
+.vscode/*
+!.vscode/extensions.json
 .idea
-.vscode
 *.suo
 *.ntvs*
 *.njsproj

+ 8 - 0
.prettierrc.json

@@ -0,0 +1,8 @@
+{
+  "$schema": "https://json.schemastore.org/prettierrc",
+  "semi": false,
+  "tabWidth": 2,
+  "singleQuote": true,
+  "printWidth": 100,
+  "trailingComma": "none"
+}

+ 8 - 0
auto-imports.d.ts

@@ -0,0 +1,8 @@
+/* eslint-disable */
+/* prettier-ignore */
+// @ts-nocheck
+// Generated by unplugin-auto-import
+export {}
+declare global {
+
+}

+ 0 - 5
babel.config.js

@@ -1,5 +0,0 @@
-module.exports = {
-  presets: [
-    '@vue/cli-plugin-babel/preset'
-  ]
-}

+ 29 - 0
components.d.ts

@@ -0,0 +1,29 @@
+/* eslint-disable */
+/* prettier-ignore */
+// @ts-nocheck
+// Generated by unplugin-vue-components
+// Read more: https://github.com/vuejs/core/pull/3399
+import '@vue/runtime-core'
+
+export {}
+
+declare module '@vue/runtime-core' {
+  export interface GlobalComponents {
+    Confirm: typeof import('./src/components/Toast/Confirm.vue')['default']
+    Footer: typeof import('./src/components/mobile/footer.vue')['default']
+    Header: typeof import('./src/components/mobile/header.vue')['default']
+    HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
+    IconCommunity: typeof import('./src/components/icons/IconCommunity.vue')['default']
+    IconDocumentation: typeof import('./src/components/icons/IconDocumentation.vue')['default']
+    IconEcosystem: typeof import('./src/components/icons/IconEcosystem.vue')['default']
+    IconSupport: typeof import('./src/components/icons/IconSupport.vue')['default']
+    IconTooling: typeof import('./src/components/icons/IconTooling.vue')['default']
+    Mobile: typeof import('./src/components/mobile/index.vue')['default']
+    Pc: typeof import('./src/components/pc/index.vue')['default']
+    RouterLink: typeof import('vue-router')['RouterLink']
+    RouterView: typeof import('vue-router')['RouterView']
+    TheWelcome: typeof import('./src/components/TheWelcome.vue')['default']
+    Toast: typeof import('./src/components/Toast/Toast.vue')['default']
+    WelcomeItem: typeof import('./src/components/WelcomeItem.vue')['default']
+  }
+}

+ 1 - 0
env.d.ts

@@ -0,0 +1 @@
+/// <reference types="vite/client" />

+ 14 - 0
index.html

@@ -0,0 +1,14 @@
+<!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">
+    <title>四维看看</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.ts"></script>
+  </body>
+</html>

+ 0 - 19
jsconfig.json

@@ -1,19 +0,0 @@
-{
-  "compilerOptions": {
-    "target": "es5",
-    "module": "esnext",
-    "baseUrl": "./",
-    "moduleResolution": "node",
-    "paths": {
-      "@/*": [
-        "src/*"
-      ]
-    },
-    "lib": [
-      "esnext",
-      "dom",
-      "dom.iterable",
-      "scripthost"
-    ]
-  }
-}

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


+ 37 - 35
package.json

@@ -1,43 +1,45 @@
 {
-  "name": "personalhubs",
-  "version": "0.1.0",
+  "name": "paypcmobile",
+  "version": "0.0.0",
   "private": true,
   "scripts": {
-    "serve": "vue-cli-service serve",
-    "build": "vue-cli-service build",
-    "lint": "vue-cli-service lint"
+    "dev": "vite",
+    "build": "vite build",
+    "preview": "vite preview",
+    "build-only": "vite build",
+    "type-check": "vue-tsc --noEmit",
+    "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
+    "format": "prettier --write src/"
   },
   "dependencies": {
-    "core-js": "^3.8.3",
-    "vue": "^3.2.13"
+    "@vitejs/plugin-legacy": "^4.0.3",
+    "@vueuse/core": "^9.13.0",
+    "axios": "^1.3.5",
+    "element-plus": "^2.3.3",
+    "less": "^4.1.3",
+    "pinia": "^2.0.32",
+    "qrcode.vue": "^3.3.4",
+    "vconsole": "^3.15.0",
+    "vue": "^3.2.47",
+    "vue-i18n": "^9.2.2",
+    "vue-router": "^4.1.6"
   },
   "devDependencies": {
-    "@babel/core": "^7.12.16",
-    "@babel/eslint-parser": "^7.12.16",
-    "@vue/cli-plugin-babel": "~5.0.0",
-    "@vue/cli-plugin-eslint": "~5.0.0",
-    "@vue/cli-service": "~5.0.0",
-    "eslint": "^7.32.0",
-    "eslint-plugin-vue": "^8.0.3"
-  },
-  "eslintConfig": {
-    "root": true,
-    "env": {
-      "node": true
-    },
-    "extends": [
-      "plugin:vue/vue3-essential",
-      "eslint:recommended"
-    ],
-    "parserOptions": {
-      "parser": "@babel/eslint-parser"
-    },
-    "rules": {}
-  },
-  "browserslist": [
-    "> 1%",
-    "last 2 versions",
-    "not dead",
-    "not ie 11"
-  ]
+    "@rushstack/eslint-patch": "^1.2.0",
+    "@types/node": "^18.14.2",
+    "@vitejs/plugin-vue": "^4.0.0",
+    "@vue/eslint-config-prettier": "^7.1.0",
+    "@vue/eslint-config-typescript": "^11.0.2",
+    "@vue/tsconfig": "^0.1.3",
+    "eslint": "^8.34.0",
+    "eslint-plugin-vue": "^9.9.0",
+    "npm-run-all": "^4.1.5",
+    "prettier": "^2.8.4",
+    "terser": "^5.17.5",
+    "typescript": "~4.8.4",
+    "unplugin-auto-import": "^0.15.3",
+    "unplugin-vue-components": "^0.24.1",
+    "vite": "^4.1.4",
+    "vue-tsc": "^1.2.0"
+  }
 }

BIN
public/favicon.ico


+ 0 - 17
public/index.html

@@ -1,17 +0,0 @@
-<!DOCTYPE html>
-<html lang="">
-  <head>
-    <meta charset="utf-8">
-    <meta http-equiv="X-UA-Compatible" content="IE=edge">
-    <meta name="viewport" content="width=device-width,initial-scale=1.0">
-    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
-    <title><%= htmlWebpackPlugin.options.title %></title>
-  </head>
-  <body>
-    <noscript>
-      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
-    </noscript>
-    <div id="app"></div>
-    <!-- built files will be auto injected -->
-  </body>
-</html>

+ 39 - 17
src/App.vue

@@ -1,26 +1,48 @@
+<script setup lang="ts">
+import { RouterView } from 'vue-router'
+</script>
+
 <template>
-  <img alt="Vue logo" src="./assets/logo.png">
-  <HelloWorld msg="Welcome to Your Vue.js App"/>
+  <div class="app">
+    <RouterView />
+  </div>
 </template>
 
-<script>
-import HelloWorld from './components/HelloWorld.vue'
+<style scoped>
+header {
+  line-height: 1.5;
+  max-height: 100vh;
+}
 
-export default {
-  name: 'App',
-  components: {
-    HelloWorld
-  }
+.logo {
+  display: block;
+  margin: 0 auto 2rem;
 }
-</script>
 
-<style>
-#app {
-  font-family: Avenir, Helvetica, Arial, sans-serif;
-  -webkit-font-smoothing: antialiased;
-  -moz-osx-font-smoothing: grayscale;
+nav {
+  width: 100%;
+  font-size: 12px;
   text-align: center;
-  color: #2c3e50;
-  margin-top: 60px;
+  margin-top: 2rem;
+}
+
+nav a.router-link-exact-active {
+  color: var(--color-text);
+}
+
+nav a.router-link-exact-active:hover {
+  background-color: transparent;
+}
+
+nav a {
+  display: inline-block;
+  padding: 0 1rem;
+  border-left: 1px solid var(--color-border);
 }
+
+nav a:first-of-type {
+  border: 0;
+}
+
+
 </style>

+ 77 - 0
src/api/api.ts

@@ -0,0 +1,77 @@
+import { request } from "@/utils/request";
+interface logonParam {
+    code:string,
+    orderSn:string,
+}
+export const getOrder = (orderSn) => {
+  return request({
+    url: "/service/pay/order/info",
+    method: "get",
+    data:{
+        orderSn
+    },
+    config: {
+      timeout: 10000,
+      headers: {
+        "Content-Type": "application/json;charset=UTF-8",
+      },
+    },
+  });
+};
+
+export const openPay = (data) => {
+    return request({
+      url: "/service/pay/openPay",
+      method: "post",
+      data,
+      config: {
+        timeout: 10000,
+        headers: {
+            "Content-Type": "application/json;charset=UTF-8",
+        },
+      },
+    });
+  };
+
+  export const wxLogin = (data:logonParam) => {
+    return request({
+      url: "/service/pay/wxLogin",
+      method: "get",
+      data,
+      config: {
+        timeout: 10000,
+        headers: {
+            "Content-Type": "application/json;charset=UTF-8",
+        },
+      },
+    });
+  };
+  
+export const getOrderInfo = (orderSn:string) => {
+    return request({
+      url: `/service/pay/order/info/${orderSn}`,
+      method: "get",
+      config: {
+        timeout: 10000,
+        loading: true,//隐藏进度条
+        headers: {
+            "Content-Type": "application/json;charset=UTF-8",
+        },
+      },
+    });
+  };
+
+export const queryOrderStatus = (data) => {
+    return request({
+      url: `/ucenter/user/order/queryOrderStatus`,
+      method: "post",
+      data,
+      config: {
+        timeout: 10000,
+        loading: true,//隐藏进度条
+        headers: {
+            "Content-Type": "application/json;charset=UTF-8",
+        },
+      },
+    });
+  };

+ 89 - 0
src/assets/base.css

@@ -0,0 +1,89 @@
+/* color palette from <https://github.com/vuejs/theme> */
+:root {
+  --vt-c-white: #ffffff;
+  --vt-c-white-soft: #f8f8f8;
+  --vt-c-white-mute: #f2f2f2;
+
+  --vt-c-black: #181818;
+  --vt-c-black-soft: #222222;
+  --vt-c-black-mute: #282828;
+
+  --vt-c-indigo: #2c3e50;
+
+  --vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
+  --vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
+  --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
+  --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
+
+  --vt-c-text-light-1: var(--vt-c-indigo);
+  --vt-c-text-light-2: rgba(60, 60, 60, 0.66);
+  --vt-c-text-dark-1: var(--vt-c-white);
+  --vt-c-text-dark-2: #2c3e50;/* rgba(235, 235, 235, 0.64); */
+}
+
+/* semantic color variables for this project */
+:root {
+  --color-background: var(--vt-c-white);
+  --color-background-soft: var(--vt-c-white-soft);
+  --color-background-mute: var(--vt-c-white-mute);
+
+  --color-border: var(--vt-c-divider-light-2);
+  --color-border-hover: var(--vt-c-divider-light-1);
+
+  --color-heading: var(--vt-c-text-light-1);
+  --color-text: var(--vt-c-text-light-1);
+
+  --section-gap: 160px;
+}
+
+@media (prefers-color-scheme: dark) {
+  :root {
+    --color-background: var(--vt-c-black);
+    --color-background-soft: var(--vt-c-black-soft);
+    --color-background-mute: var(--vt-c-black-mute);
+
+    --color-border: var(--vt-c-divider-dark-2);
+    --color-border-hover: var(--vt-c-divider-dark-1);
+
+    --color-heading: var(--vt-c-text-dark-1);
+    --color-text: var(--vt-c-text-dark-2);
+  }
+}
+
+*,
+*::before,
+*::after {
+  /* box-sizing: border-box; */
+  margin: 0;
+  position: relative;
+  font-weight: normal;
+}
+
+body {
+  min-height: 100vh;
+  margin: 0;
+  color: var(--color-text);
+  background: var(--color-background);
+  transition: color 0.5s, background-color 0.5s;
+  line-height: 1.6;
+  font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
+    Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
+  font-size: 15px;
+  text-rendering: optimizeLegibility;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+::-webkit-scrollbar-thumb {
+  height: 50px;
+  background-color: #999;
+  -webkit-border-radius: 4px;
+  outline: 2px solid #fff;
+  outline-offset: -2px;
+  border: 2px solid #fff;
+}
+
+::-webkit-scrollbar-thumb:hover {
+  height: 50px;
+  background-color: #9f9f9f;
+  -webkit-border-radius: 4px;
+}

+ 1 - 0
src/assets/config.less

@@ -0,0 +1 @@
+@primary-color: #29b2ff;

BIN
src/assets/images/active.png


BIN
src/assets/images/alipay56.png


BIN
src/assets/images/baicon.png


BIN
src/assets/images/icon/error.png


BIN
src/assets/images/icon/success.png


BIN
src/assets/images/icon/warn.png


BIN
src/assets/images/logoCn.png


BIN
src/assets/images/logoEn.png


BIN
src/assets/images/paypal.png


BIN
src/assets/images/scan-tip-en.png


BIN
src/assets/images/scan-tip.png


BIN
src/assets/images/stripe.png


Datei-Diff unterdrückt, da er zu groß ist
+ 1 - 0
src/assets/images/vip_true.svg


BIN
src/assets/images/wechat56.png


BIN
src/assets/images/whietlogo.png


BIN
src/assets/images/whietlogoEn.png


BIN
src/assets/logo.png


+ 1 - 0
src/assets/logo.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

+ 25 - 0
src/assets/main.css

@@ -0,0 +1,25 @@
+@import './base.css';
+
+#app {
+  /* margin: 0 auto;
+  font-weight: normal; */
+}
+
+a,
+.green {
+  text-decoration: none;
+  color: hsla(160, 100%, 37%, 1);
+  transition: 0.4s;
+}
+
+@media (hover: hover) {
+  a:hover {
+    background-color: hsla(160, 100%, 37%, 0.2);
+  }
+}
+
+.contentPage{
+  max-width: 1366px;
+  margin: 0 auto;
+  min-width: 1200px;
+}

+ 29 - 47
src/components/HelloWorld.vue

@@ -1,58 +1,40 @@
+<script setup lang="ts">
+defineProps<{
+  msg: string
+}>()
+</script>
+
 <template>
-  <div class="hello">
-    <h1>{{ msg }}</h1>
-    <p>
-      For a guide and recipes on how to configure / customize this project,<br>
-      check out the
-      <a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
-    </p>
-    <h3>Installed CLI Plugins</h3>
-    <ul>
-      <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
-      <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
-    </ul>
-    <h3>Essential Links</h3>
-    <ul>
-      <li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
-      <li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
-      <li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
-      <li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
-      <li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
-    </ul>
-    <h3>Ecosystem</h3>
-    <ul>
-      <li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
-      <li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
-      <li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
-      <li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
-      <li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
-    </ul>
+  <div class="greetings">
+    <h1 class="green">{{ msg }}</h1>
+    <h3>
+      You’ve successfully created a project with
+      <a href="https://vitejs.dev/" target="_blank" rel="noopener">Vite</a> +
+      <a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>. What's next?
+    </h3>
   </div>
 </template>
 
-<script>
-export default {
-  name: 'HelloWorld',
-  props: {
-    msg: String
-  }
+<style scoped>
+h1 {
+  font-weight: 500;
+  font-size: 2.6rem;
+  top: -10px;
 }
-</script>
 
-<!-- Add "scoped" attribute to limit CSS to this component only -->
-<style scoped>
 h3 {
-  margin: 40px 0 0;
-}
-ul {
-  list-style-type: none;
-  padding: 0;
+  font-size: 1.2rem;
 }
-li {
-  display: inline-block;
-  margin: 0 10px;
+
+.greetings h1,
+.greetings h3 {
+  text-align: center;
 }
-a {
-  color: #42b983;
+
+@media (min-width: 1024px) {
+  .greetings h1,
+  .greetings h3 {
+    text-align: left;
+  }
 }
 </style>

+ 86 - 0
src/components/TheWelcome.vue

@@ -0,0 +1,86 @@
+<script setup lang="ts">
+import WelcomeItem from './WelcomeItem.vue'
+import DocumentationIcon from './icons/IconDocumentation.vue'
+import ToolingIcon from './icons/IconTooling.vue'
+import EcosystemIcon from './icons/IconEcosystem.vue'
+import CommunityIcon from './icons/IconCommunity.vue'
+import SupportIcon from './icons/IconSupport.vue'
+</script>
+
+<template>
+  <WelcomeItem>
+    <template #icon>
+      <DocumentationIcon />
+    </template>
+    <template #heading>Documentation</template>
+
+    Vue’s
+    <a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
+    provides you with all information you need to get started.
+  </WelcomeItem>
+
+  <WelcomeItem>
+    <template #icon>
+      <ToolingIcon />
+    </template>
+    <template #heading>Tooling</template>
+
+    This project is served and bundled with
+    <a href="https://vitejs.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
+    recommended IDE setup is
+    <a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a> +
+    <a href="https://github.com/johnsoncodehk/volar" target="_blank" rel="noopener">Volar</a>. If
+    you need to test your components and web pages, check out
+    <a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a> and
+    <a href="https://on.cypress.io/component" target="_blank">Cypress Component Testing</a>.
+
+    <br />
+
+    More instructions are available in <code>README.md</code>.
+  </WelcomeItem>
+
+  <WelcomeItem>
+    <template #icon>
+      <EcosystemIcon />
+    </template>
+    <template #heading>Ecosystem</template>
+
+    Get official tools and libraries for your project:
+    <a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
+    <a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
+    <a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
+    <a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
+    you need more resources, we suggest paying
+    <a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
+    a visit.
+  </WelcomeItem>
+
+  <WelcomeItem>
+    <template #icon>
+      <CommunityIcon />
+    </template>
+    <template #heading>Community</template>
+
+    Got stuck? Ask your question on
+    <a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>, our official
+    Discord server, or
+    <a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
+      >StackOverflow</a
+    >. You should also subscribe to
+    <a href="https://news.vuejs.org" target="_blank" rel="noopener">our mailing list</a> and follow
+    the official
+    <a href="https://twitter.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
+    twitter account for latest news in the Vue world.
+  </WelcomeItem>
+
+  <WelcomeItem>
+    <template #icon>
+      <SupportIcon />
+    </template>
+    <template #heading>Support Vue</template>
+
+    As an independent project, Vue relies on community backing for its sustainability. You can help
+    us by
+    <a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
+  </WelcomeItem>
+</template>

+ 193 - 0
src/components/Toast/Confirm.vue

@@ -0,0 +1,193 @@
+
+<template>
+  <div class="xtx-confirm">
+    <div class="wrapper" ref="target">
+      <div class="header" v-if="options.title">
+        <h3>{{ options.title }}</h3>
+        <a href="JavaScript:;" class="iconfont icon-close-new" @click="cancelCallback"></a>
+      </div>
+      <div class="body">
+        <div class="typeImg">
+          <img
+            style="height: 48px; width: 48px"
+            v-if="options.type == 'success'"
+            src="@/assets/images/icon/success.png"
+            alt=""
+          />
+          <img
+            style="height: 48px; width: 48px"
+            v-else-if="options.type == 'warn'"
+            src="@/assets/images/icon/warn.png"
+            alt=""
+          />
+          <img
+            style="height: 48px; width: 48px"
+            v-else
+            src="@/assets/images/icon/error.png"
+            alt=""
+          />
+        </div>
+        <span>{{ options.text }}</span>
+      </div>
+      <div class="footer">
+        <!-- <button @click="cancelCallback" size="mini" type="gray">取消</button> -->
+        <button @click="confirmCallback" size="mini" type="primary">{{confirm}}</button>
+      </div>
+    </div>
+  </div>
+</template>
+  
+
+  <script lang="ts">
+// 注意:当前组件不是在 #app 下进行渲染,无法使用 #app 下的环境(全局组件,全局指令,原型属性函数)
+import i18n from '@/i18n'
+import { ref } from 'vue'
+// import { onClickOutside } from '@vueuse/core'
+export default {
+  name: 'showConfirm',
+  props: {
+    type: {
+      type: String,
+      default: 'success'
+    },
+    options: {
+      type: Object,
+      default: () => {
+        return {
+          title: '',
+          text: '',
+          type: 'success',
+          callback: () => {}
+        }
+      }
+    },
+    // 关闭方法
+    close: {
+      type: Function,
+      default: () => {}
+    },
+    // 取消按钮
+    // cancelCallback: {
+    //   type: Function,
+    //   default: () => {}
+    // }
+  },
+  setup(props) {
+    // 点击 target 目标元素外部相当于点击了取消
+    const target = ref(null)
+    let current = (localStorage && localStorage.getItem('language'))
+    if (!current) {
+      current = window.navigator.language || window.navigator.userLanguage || null
+        if (current && !/^zh/.test(current)) {
+            console.log('自动获取浏览器语言:' + current)
+            current = 'en'
+        }else{
+          current = 'zh'
+        }
+    }
+    // onClickOutside(target, () => {
+    //   props.cancelCallback()
+    // })
+    function cancelCallback() {
+      props.close()
+    }
+    function confirmCallback() {
+      props.options.callback()
+      props.close()
+    }
+    return { options:props.options,target,confirmCallback,cancelCallback,confirm:i18n.global.t('confirm.text') }
+  }
+}
+</script>
+
+<style scoped lang="less">
+.xtx-confirm {
+  position: fixed;
+  left: 0;
+  top: 0;
+  width: 100%;
+  height: 100%;
+  z-index: 8888;
+  background: rgba(0, 0, 0, 0.5);
+  .wrapper {
+    width: calc(100% - 40px);
+    max-width: 400px;
+    border-radius: 10px 10px 10px 10px;
+    background: #fff;
+    border-radius: 4px;
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    .header,
+    .footer {
+      height: 50px;
+      line-height: 50px;
+      padding: 0 20px;
+      text-align: center;
+    }
+    .body {
+      padding: 30px 40px;
+      font-size: 16px;
+      font-family: PingFang SC-Regular, PingFang SC;
+      font-weight: 400;
+      color: #202020;
+      line-height: 19px;
+      text-align: center;
+      .icon-warning {
+        color: red;
+        margin-right: 3px;
+        font-size: 16px;
+      }
+      .typeImg {
+        margin-bottom: 20px;
+      }
+    }
+    .footer {
+      //   text-align: right;
+      border-top: 1px solid #ebebeb;
+      padding: 20px;
+      .xtx-button {
+        margin-left: 20px;
+      }
+      button {
+        height: 40px;
+        background: #29b2ff;
+        border-radius: 4px 4px 4px 4px;
+        opacity: 1;
+        font-size: 14px;
+        font-family: PingFang SC-Regular, PingFang SC;
+        font-weight: 400;
+        color: #ffffff;
+        border: none;
+        padding: 10px 50px;
+        text-align: center;
+        margin: 0 15px;
+      }
+    }
+    .header {
+      position: relative;
+      h3 {
+        font-weight: normal;
+        font-size: 18px;
+      }
+      a {
+        position: absolute;
+        right: 15px;
+        top: 15px;
+        font-size: 20px;
+        width: 20px;
+        height: 20px;
+        line-height: 20px;
+        text-align: center;
+        color: #999;
+        &:hover {
+          color: #666;
+        }
+      }
+    }
+  }
+}
+</style>
+  
+  

+ 45 - 0
src/components/Toast/Toast.vue

@@ -0,0 +1,45 @@
+<template>
+  <div class="toast-wrap">
+    <div class="toast">
+      {{ title }}
+    </div>
+  </div>
+</template>
+  <script lang="ts">
+export default {
+  name: 'Toast',
+  props: {
+    title: {
+      type: String,
+      default: '我是toast'
+    }
+  },
+  setup() {
+    return {}
+  }
+}
+</script>
+  <style lang="less" scoped>
+.toast-wrap {
+  z-index: 1000;
+  width: 100%;
+  height: 100%;
+  position: fixed;
+  top: 0;
+  left: 0;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  background: rgba(0,0,0,0.5);
+}
+
+.toast {
+  max-width: 6rem;
+  color: #ffffff;
+  font-size: 14px;
+  text-align: center;
+  border-radius: 5px;
+  padding: 10px;
+  background: rgba(0, 0, 0, 0.8);
+}
+</style>

+ 58 - 0
src/components/Toast/index.ts

@@ -0,0 +1,58 @@
+import { createVNode, render } from 'vue';
+import Toast from '@/components/Toast/Toast.vue';
+import Confirm from '@/components/Toast/Confirm.vue'
+// 准备一个DOM容器
+const div = document.createElement('div');
+div.setAttribute('class', 'toast-wrapper');
+document.body.appendChild(div);
+const divConfirm = document.createElement('div');
+divConfirm.setAttribute('class', 'toast-wrapper-confirm');
+document.body.appendChild(divConfirm);
+let time: any = null;
+
+// 初始数据
+type title = string;
+interface ToastType {
+  title: title;
+  duration?: number;
+}
+interface showConfirmType {
+    title?:string;
+    text?:string;
+    type?:string;
+    callback?:Function;
+}
+type union = title | ToastType;
+let timer: any = null;
+export function show(options: union) {
+  let title, duration;
+  if (typeof options === 'string') {
+    title = options || '我是默认文案';
+    duration = 2000;
+  } else {
+    title = (options as ToastType).title || '我是默认文案';
+    duration = (options as ToastType).duration || 2000;
+  }
+  // 创建虚拟dom  (组件对象, props)
+  const vnode = createVNode(Toast, { title });
+  
+  // 把虚拟dom渲染到div
+  render(vnode, div);
+  clearTimeout(timer);
+  timer = setTimeout(() => {
+    render(null, div);
+  }, duration);
+}
+export function showConfirm(options:showConfirmType) {
+    // 创建虚拟dom  (组件对象, props)
+    function close() {
+        render(null, divConfirm);
+    }
+    const vnode = createVNode(Confirm, {options, close,});
+    // 把虚拟dom渲染到div
+    render(vnode, divConfirm);
+    // clearTimeout(time);
+    // time = setTimeout(() => {
+    //   render(null, divConfirm);
+    // }, duration);
+}

+ 86 - 0
src/components/WelcomeItem.vue

@@ -0,0 +1,86 @@
+<template>
+  <div class="item">
+    <i>
+      <slot name="icon"></slot>
+    </i>
+    <div class="details">
+      <h3>
+        <slot name="heading"></slot>
+      </h3>
+      <slot></slot>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.item {
+  margin-top: 2rem;
+  display: flex;
+}
+
+.details {
+  flex: 1;
+  margin-left: 1rem;
+}
+
+i {
+  display: flex;
+  place-items: center;
+  place-content: center;
+  width: 32px;
+  height: 32px;
+
+  color: var(--color-text);
+}
+
+h3 {
+  font-size: 1.2rem;
+  font-weight: 500;
+  margin-bottom: 0.4rem;
+  color: var(--color-heading);
+}
+
+@media (min-width: 1024px) {
+  .item {
+    margin-top: 0;
+    padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
+  }
+
+  i {
+    top: calc(50% - 25px);
+    left: -26px;
+    position: absolute;
+    border: 1px solid var(--color-border);
+    background: var(--color-background);
+    border-radius: 8px;
+    width: 50px;
+    height: 50px;
+  }
+
+  .item:before {
+    content: ' ';
+    border-left: 1px solid var(--color-border);
+    position: absolute;
+    left: 0;
+    bottom: calc(50% + 25px);
+    height: calc(50% - 25px);
+  }
+
+  .item:after {
+    content: ' ';
+    border-left: 1px solid var(--color-border);
+    position: absolute;
+    left: 0;
+    top: calc(50% + 25px);
+    height: calc(50% - 25px);
+  }
+
+  .item:first-of-type:before {
+    display: none;
+  }
+
+  .item:last-of-type:after {
+    display: none;
+  }
+}
+</style>

Datei-Diff unterdrückt, da er zu groß ist
+ 7 - 0
src/components/icons/IconCommunity.vue


Datei-Diff unterdrückt, da er zu groß ist
+ 7 - 0
src/components/icons/IconDocumentation.vue


Datei-Diff unterdrückt, da er zu groß ist
+ 7 - 0
src/components/icons/IconEcosystem.vue


+ 7 - 0
src/components/icons/IconSupport.vue

@@ -0,0 +1,7 @@
+<template>
+  <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
+    <path
+      d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
+    />
+  </svg>
+</template>

+ 19 - 0
src/components/icons/IconTooling.vue

@@ -0,0 +1,19 @@
+<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
+<template>
+  <svg
+    xmlns="http://www.w3.org/2000/svg"
+    xmlns:xlink="http://www.w3.org/1999/xlink"
+    aria-hidden="true"
+    role="img"
+    class="iconify iconify--mdi"
+    width="24"
+    height="24"
+    preserveAspectRatio="xMidYMid meet"
+    viewBox="0 0 24 24"
+  >
+    <path
+      d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
+      fill="currentColor"
+    ></path>
+  </svg>
+</template>

+ 8 - 0
src/components/mobile/footer.vue

@@ -0,0 +1,8 @@
+<script setup lang="ts">
+</script>
+
+<template>
+  <div class="footer">
+    我的底部
+  </div>
+</template>

+ 33 - 0
src/components/mobile/header.vue

@@ -0,0 +1,33 @@
+<script setup lang="ts">
+import { ref } from 'vue'
+import { useI18n } from 'vue-i18n'
+import logoCn from '@/assets/images/whietlogo.png'
+import logoEn from '@/assets/images/whietlogoEn.png'
+
+const {locale } = useI18n();
+console.log('locale',locale)
+</script>
+
+<template>
+  <div class="header">
+    <div class="logo">
+      <img :src="locale === 'zh' ? logoCn : logoEn" alt="logo">
+    </div>
+  </div>
+</template>
+<style lang="less" scoped>
+.header{
+  height: 50px;
+  background: #202020;
+  border-radius: 0px 0px 0px 0px;
+  opacity: 1;
+  .logo{
+    text-align: center;
+    padding: 8px 0;
+    img{
+      // height: 20px;
+      width: 80px;
+    }
+  }
+}
+</style>

+ 24 - 0
src/components/mobile/index.vue

@@ -0,0 +1,24 @@
+<script setup lang="ts">
+import Header from "./header.vue";
+// import Footer from "./footer.vue";
+</script>
+
+<template>
+    <div class="container">
+      <Header />
+      <router-view />
+    </div>
+</template>
+
+<style scoped>
+.container{
+  height: 100vh;
+  overflow-y: auto;
+  overflow-x: hidden;
+  *{
+    box-sizing: content-box;
+  }
+}
+</style>
+
+

+ 81 - 0
src/components/pc/footer.vue

@@ -0,0 +1,81 @@
+<script setup lang="ts">
+</script>
+
+<template>
+  <div class="footer">
+    <div class="contentPage">
+      <div class="relevant clear">
+        <p class="relevant-1">Copyright © 2022 4DAGE Co., Ltd. All rights reserved.</p>
+        <span>
+          <p class="relevant-2">
+          <a target="_blank" href="https://beian.miit.gov.cn/" class="a_class">
+            <img src="@/assets/images/baicon.png" alt="" />
+            粤ICP备14078495号
+          </a>
+        </p>
+        <p class="relevant-2">
+          <a
+            target="_blank"
+            href="http://www.beian.gov.cn/portal/registerSystemInfo?recordcode=44049102496647"
+            class="a_class"
+          >
+            <img src="@/assets/images/baicon.png" alt="" />
+            粤公网安备 44049102496647号
+          </a>
+        </p>
+        </span>
+      </div>
+    </div>
+  </div>
+</template>
+<style lang="less" scoped>
+.footer {
+  width: 100%;
+  height: 83px;
+  background: #202020;
+  border-radius: 0px 0px 0px 0px;
+  opacity: 1;
+  .contentPage{
+    height: 100%;
+  }
+  .relevant {
+    // border-top: 1px solid #909090;
+    color: #909090;
+    height: 100%;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    &-1 {
+      float: left;
+      background-color: transparent;
+    }
+    &-2 {
+      float: right;
+      margin-left: 20px;
+    }
+    div {
+      margin-bottom: 8px;
+
+      a {
+        color: @primary-color;
+        font-size: 14px;
+        text-decoration: underline;
+        display: inline-block;
+        line-height: 25px;
+
+        &:nth-child(2) {
+          margin: 0 42px;
+        }
+      }
+    }
+    a {
+      color: #909090;
+    }
+    p {
+      font-size: 14px;
+      color: #909090;
+      margin-bottom: 8px;
+    }
+  }
+}
+</style>

+ 28 - 0
src/components/pc/header.vue

@@ -0,0 +1,28 @@
+<script setup lang="ts">
+import { useI18n } from 'vue-i18n'
+import logoCn from '@/assets/images/logoCn.png'
+import logoEn from '@/assets/images/logoEn.png'
+//得到i18n的locale
+const { locale } = useI18n();
+</script>
+
+<template>
+  <div class="header">
+    <div class="logo contentPage">
+      <img :src="locale === 'zh' ? logoCn : logoEn" alt="logo">
+    </div>
+  </div>
+</template>
+<style lang="less" scoped>
+.header{
+  height: 80px;
+  background: #FFFFFF;
+  box-shadow: 0px 3px 6px 0px rgba(0,0,0,0.1);
+  border-radius: 0px 0px 0px 0px;
+  opacity: 1;
+  .logo{
+    height: 100%;
+    padding: 17px 0;
+  }
+}
+</style>

+ 45 - 0
src/components/pc/index.vue

@@ -0,0 +1,45 @@
+<script setup lang="ts">
+import Header from "./header.vue";
+import Footer from "./footer.vue";
+</script>
+
+<template>
+    <div class="container">
+      <Header />
+      <div class="main_container">
+        <router-view class="contentPage" />
+      </div>
+      <Footer />
+    </div>
+</template>
+
+<style scoped>
+h1 {
+  font-weight: 500;
+  font-size: 2.6rem;
+  top: -10px;
+}
+
+h3 {
+  font-size: 1.2rem;
+}
+
+.greetings h1,
+.greetings h3 {
+  text-align: center;
+}
+.container{
+  min-height: 100vh;
+}
+.main_container{
+  min-height: calc(100vh - 163px);
+  background-color: #F7F7F7;
+}
+@media (min-width: 1024px) {
+  .greetings h1,
+  .greetings h3 {
+    text-align: left;
+  }
+}
+</style>
+

Datei-Diff unterdrückt, da er zu groß ist
+ 161 - 0
src/i18n/en.js


+ 31 - 0
src/i18n/index.js

@@ -0,0 +1,31 @@
+import { createI18n } from 'vue-i18n';
+import ZH from './zh.js';
+import EN from './en.js';
+import JA from './ja.js';
+import KR from './kr.js';
+const messages = {
+  zh: { ...ZH  },
+  en: { ...EN  },
+  ja: { ...JA  },
+  kr: { ...KR  },
+};
+let current = (localStorage && localStorage.getItem('language'))
+console.log('current', current )
+if (!current) {
+  current = window.navigator.language || window.navigator.userLanguage || null
+    if (current && !/^zh/.test(current)) {
+        console.log('自动获取浏览器语言:' + current)
+        current = 'en'
+    }else{
+      current = 'zh'
+    }
+}
+document.title = current !== 'zh' ? '4DAGE' : '四维时代'
+
+const i18n = createI18n({
+  legacy: false,
+  globalInjection: true,
+  locale: current,
+  messages
+});
+export default i18n;

Datei-Diff unterdrückt, da er zu groß ist
+ 157 - 0
src/i18n/ja.js


Datei-Diff unterdrückt, da er zu groß ist
+ 159 - 0
src/i18n/kr.js


+ 159 - 0
src/i18n/zh.js

@@ -0,0 +1,159 @@
+export default {
+    confirm: {
+        text:'确认'
+    },
+    payInfo: {
+        payErr: '支付异常',
+        payfail: '支付失败',
+        paySuccess: '支付成功',
+        payTitle: '支付中心',
+        userName: '当前账号',
+        membership: '购买会员',
+        zyhy:'会员权益',
+        gjhy: '高级会员',
+        zyhys: '专业会员',
+        expiration:'有效期{num}年',
+        expirationmon:'有效期{num}个月',
+        payCenter: "支付中心",
+        wx:'微信支付',
+        zfb:'支付宝支付',
+        paypal:'paypal',
+        payPrice:'应付金额',
+        priceUnit:'¥',
+        payOrder:'付款',
+        autoRenew:'订阅',
+        autoPayTips: '会员权益订阅成功,订单支付处理时间可能需要1~2个小时,请耐心等待。 (可在{paytype}查阅实际支付情况)',
+    },
+    mall: {
+        buyGoods: '购买商品',
+        "postage": "运费:顺丰包邮",
+        "baseVolume": "包含基础云容量",
+        "knowMore": "了解详情",
+        "standard": "标准套餐",
+        "addCou": "增值套餐(包含三脚架)",
+        "count": "数量",
+        "couTip": "是否需要添加推荐的配件(标准套餐不含以下配件)",
+        "jiaojia": "四维看看三脚架套装",
+        "usb": "四维深时高速固态 U 盘",
+        "battery": "四维深时专用电池",
+        "jiaojiaDetail": "三脚架 + 滑轮 + 云台",
+        "cart": "加入购物车",
+        "fahuo": "预计3日内发货",
+        "product": "产品说明",
+        "techDetail": "技术参数",
+        "widthDetail": "尺寸参数",
+        "purchaseTpis": "购买请联系:overseas@4dage.com",
+        "cartTitle": "购物车",
+        "xzsh": "新增收货地址",
+        "cartTitleTip": "本订单顺丰包邮",
+        "goods": "商品",
+        "price": "价格",
+        "mini": "小计",
+        "continueBuy": "继续选购",
+        "nowJieSuan": "立即结算",
+        "nowBuy": "立即购买",
+        "countNum": "共计{num}件商品",
+        "addBuy": "加入购物车",
+        "confirmOrder": "确认订单",
+        "submitOrder": "下单",
+        "receiveAddress": "收货地址",
+        "invoiceIntro": "发票信息",
+        "normalInvoice": "电子普通发票",
+        "zengzhiInvoice": "专用发票",
+        "people": "个人",
+        "enterprise": "企业",
+        "wenxinTip": "温馨提示",
+        "wenxinTipContent": "专票随商品一并寄出,若因特殊情况会于10天内顺丰寄送给您(如遇节假日稍有延迟)。请注意查收;增值税专用发票收到后请妥善保存,如退货请一同寄回,如退货专票未能寄回,则需扣除相应的税点。",
+        "goodsInfo": "商品信息",
+        "kankanPro": "四维看看 八目相机",
+        "zhijiaName": "四维看看三脚架套装",
+        "goodsTotalPrice": "商品总价",
+        "otherPrice": "税费及其它费用",
+        "yunfei": "运费",
+        "total": "合计",
+        "blackColor": "静谧黑",
+        "black": "黑色",
+        "silvery": "银色",
+        "voice": "容量套餐(10G)",
+        "zhijiaColor": "标准色",
+        "payOrder": "付款",
+        "addAddressTip": "请添加地址",
+        "payCenter": "支付中心",
+        "nowAccount": "当前帐号",
+        "vip": "购买会员",
+        'goqy': '购买配件',
+        "cooperationOrder": '添加协作',
+        "cooperationOrderName": "添加协作者{count}个",
+        "downdesc": '场景下载',
+        "payType": "支付方式",
+        "aliPay": "支付宝",
+        "wechatPay": "微信支付",
+        "payNum": "应付金额",
+        "wechat": "微信",
+        "payTip": "使用{type}app扫码完成支付",
+        "noGoodsConfirm": "请选择需要购买的商品",
+        "yunVoice": "关于云容量",
+        "editAddress": "编辑收货地址",
+        "receiver": "收货人",
+        "phone": "手机号码",
+        "area": "所在地区",
+        "addressDetail": "详细地址",
+        "nashui": "纳税人识别号",
+        "paySuccess": "恭喜您,支付成功!",
+        "paySuccessTip": "支付成功",
+        "payFail": "支付失败!请重新支付",
+        "fahuoTip": "我们将尽快安排为您发货!",
+        "reWriteToCameraTip": "……{count}秒后跳转至我的相机页面",
+        "reWriteToOrderTip": "……{count}秒后跳转至我的订单页面",
+        "member": {
+            "support": "支持",
+            "notSupport": "不支持",
+            "startUse": "开始使用",
+            "downloadPrice": "99元/次",
+            "memberTitle": "会员权益",
+            "privilege": "会员权益",
+            "buySuccessTip": "支付成功!请前往账户信息页授权相机使用",
+            "privilegeIntro": "适用于品牌宣传要求较高,灵活定制品牌信息,拍摄场景较多,小程序部署的专业用户或企业。",
+            "privilegeCapacity": "无限容量",
+            "privilegeCal": "弹性服务器免排队",
+            "mapping": '空间贴图',
+            "mappingbeautify": '场景美化调节',
+            "3dModel": '三维模型',
+            "privilegeDownload": "免费下载10次",
+            "privilegePrice": "5980",
+            "normal": "普通权益",
+            "normalIntro": "具备基础的场景编辑功能及有限云容量,适用于个人爱好者,或无强定制信息需求的团队。",
+            "normalCapacity": "10GB",
+            "normalCal": "根据实际情况排队",
+            "normalPrice": "免费",
+            "capacity": "相机云容量",
+            "customLogo": "自定义LOGO",
+            "customHot": "自定义热点样式",
+            "daikan": "带看",
+            "sceneCal": "场景计算",
+            "sceneDownload": "场景下载",
+            "sceneCoyp": "场景复制",
+            "quanjing": "四维全景制作",
+            "price": "年费",
+            "aboutMember": "关于会员权益",
+            "question1": "会员权益如何生效?",
+            "answer1": "答:用户点击购买会员权益,并付款成功后即开始生效。生效日期即权益的开始日期,用户可将会员权益对相机进行授权使用,即使不进行授权也视为生效。",
+            "question2": "如何授权会员权益?",
+            "answer2": "答:成功购买会员权益后,请您进入我的账号 - 账号信息页面,在页面下方的会员权益栏点击授权按钮,并输入相机S/N码完成授权。会员权益不可授权非本账号相机,您可在我的账号 - 我的相机页面先绑定相机后再进行授权。",
+            "question3": "会员权益是否可以更换授权相机?",
+            "answer3": "答:授权相机不影响权益的生效和结束。在会员权益生效期间,可更换授权给任意相机。但是要注意,如A相机为授权相机,且拍摄了多于10G的场景,当会员权益到期或被解除授权后,超出10G部分的场景将被封存。续费或相机重新被授权后,场景才能解封。",
+            "question4": "权益相机的场景还可以协作给他人吗?",
+            "answer4": "答:可以,而且权益相机拍摄的场景,协作给协作者后,协作者也可以同样享受针对此场景的会员权益功能。",
+            "question5": "场景更新LOGO后,权益到期会恢复默认吗?",
+            "answer5": "答:不会,用户在会员权益生效期间自定义的logo,在权益到期后,不会恢复为默认的四维看看logo。",
+            "question6": "拥有多台相机,可以购买多个会员权益吗?",
+            "answer6": "答:可以,每个会员权益,默认有效期为一年,一年后到期。用户可购买多个,分别授权给不同的相机。",
+            "question7": "什么是空间贴图?",
+            "answer7": "答:支持在漫游场景中任意位置张贴图片。功能位置:我的账号 / 场景管理 / 编辑 / 空间装饰。",
+            "question8": "什么是三维模型?",
+            "answer8": "答:支持上传您自己的模型,仅支持obj格式模型。模型文件需包含obj模型、贴图、mtl文件。功能位置:我的账号 / 场景管理 / 编辑 / 空间装饰。",
+            "question9": "什么是场景美化调节?",
+            "answer9": "答:支持对全景图的亮度、饱和度、色温、对比度等进行调节。功能位置:我的账号 / 场景管理 / 编辑 / 场景美化 / 调节。"
+        }
+    }
+};

+ 0 - 4
src/main.js

@@ -1,4 +0,0 @@
-import { createApp } from 'vue'
-import App from './App.vue'
-
-createApp(App).mount('#app')

+ 19 - 0
src/main.ts

@@ -0,0 +1,19 @@
+import { createApp } from 'vue'
+import { createPinia } from 'pinia'
+
+import App from './App.vue'
+import router from './router'
+import i18n from './i18n/index.js'
+
+import 'element-plus/dist/index.css'
+import './assets/main.css'
+// import './utils/vconsole.js'
+const app = createApp(App)
+
+app.use(createPinia())
+app.use(router)
+app.use(i18n)
+
+app.config.globalProperties.$cdn = 'https://4dscene.4dage.com/new4dkk/';
+
+app.mount('#app')

+ 69 - 0
src/router/index.ts

@@ -0,0 +1,69 @@
+import { createRouter, createWebHistory } from 'vue-router'
+const pc = () => import('../components/pc/index.vue')
+const mobile = () => import('../components/mobile/index.vue')
+import pcHome from '../views/pc/index.vue'
+import mobileHome from '../views/mobile/index.vue'
+const routesP = [{
+  path: '/',
+  name: 'Pc',
+  redirect: '/',
+  component: pc,
+  children: [{
+    path: '/',
+    name: 'index',
+    component: pcHome
+  }]
+}]
+
+const routesM = [{
+  path: '/',
+  name: 'Mobile',
+  redirect: '/',
+  component: mobile,
+          children: [
+            {
+              name: 'information',
+              path: '/information',
+              component: resolve => require(['@/page/manage/temp/information'], resolve),
+              meta: { hideFooterFind: false, requireAuth: true }
+            },
+            {
+              name: 'scene',
+              path: '/scene',
+              component: resolve => require(['@/page/manage/temp/scene'], resolve),
+              meta: { hideFooterFind: false, requireAuth: true }
+            },
+            {
+              name: 'order',
+              path: '/order',
+              component: resolve => require(['@/page/manage/temp/order'], resolve),
+              meta: { hideFooterFind: false, requireAuth: true }
+
+            },
+            {
+              name: 'device',
+              path: '/device',
+              component: resolve => require(['@/page/manage/temp/device'], resolve),
+              meta: { hideFooterFind: false, requireAuth: true }
+            },
+            {
+              name: 'appProduct',
+              path: '/appProduct',
+              component: resolve => require(['@/page/manage/temp/appProduct'], resolve),
+              meta: { hideFooterFind: false, requireAuth: true }
+            }
+          ]
+}]
+
+var routes = [];
+if ((navigator.userAgent.match(/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i))) {
+routes = routesM
+} else {
+  routes = routesP
+}
+const router = createRouter({
+  history: createWebHistory(import.meta.env.BASE_URL),
+  routes,
+})
+
+export default router

+ 50 - 0
src/router/mbRoute.ts

@@ -0,0 +1,50 @@
+const mobile = () => import('../components/mobile/index.vue')
+const mobileHome = () => import('../views/mobile/index.vue')
+const scene = () => import('../views/mobile/scene/index.vue')
+const order = () => import('../views/mobile/order/index.vue')
+const device = () => import('../views/mobile/device/index.vue')
+const appProduct = () => import('../views/mobile/appProduct/index.vue')
+const information = () => import('../views/mobile/information/index.vue')
+const routesP = [{
+  path: '/',
+  name: 'Mobile',
+  redirect: '/information',
+  component: mobile,
+  children: [{
+    path: '/',
+    name: 'information',
+    component: information
+  },{
+    name: 'information',
+    path: '/information',
+    component: information,
+    meta: { hideFooterFind: false, requireAuth: true }
+  },
+  {
+    name: 'scene',
+    path: '/scene',
+    component: scene,
+    meta: { hideFooterFind: false, requireAuth: true }
+  },
+  {
+    name: 'order',
+    path: '/order',
+    component: order,
+    meta: { hideFooterFind: false, requireAuth: true }
+
+  },
+  {
+    name: 'device',
+    path: '/device',
+    component: device,
+    meta: { hideFooterFind: false, requireAuth: true }
+  },
+  {
+    name: 'appProduct',
+    path: '/appProduct',
+    component: appProduct,
+    meta: { hideFooterFind: false, requireAuth: true }
+  }]
+
+}]
+export default routesP

+ 50 - 0
src/router/pcRoute.ts

@@ -0,0 +1,50 @@
+const pc = () => import('../components/pc/index.vue')
+const pcHome = () => import('../views/pc/index.vue')
+const information = () => import('../views/pc/information/index.vue')
+const scene = () => import('../views/pc/scene/index.vue')
+const order = () => import('../views/pc/order/index.vue')
+const device = () => import('../views/pc/device/index.vue')
+const appProduct = () => import('../views/pc/appProduct/index.vue')
+const routesP = [{
+  path: '/',
+  name: 'Pc',
+  redirect: '/',
+  component: pc,
+  children: [{
+    path: '/',
+    name: 'index',
+    component: pcHome
+  }, {
+    name: 'information',
+    path: '/information',
+    component: information,
+    meta: { hideFooterFind: false, requireAuth: true }
+  },
+  {
+    name: 'scene',
+    path: '/scene',
+    component: scene,
+    meta: { hideFooterFind: false, requireAuth: true }
+  },
+  {
+    name: 'order',
+    path: '/order',
+    component: order,
+    meta: { hideFooterFind: false, requireAuth: true }
+
+  },
+  {
+    name: 'device',
+    path: '/device',
+    component: device,
+    meta: { hideFooterFind: false, requireAuth: true }
+  },
+  {
+    name: 'appProduct',
+    path: '/appProduct',
+    component: appProduct,
+    meta: { hideFooterFind: false, requireAuth: true }
+  }]
+
+}]
+export default routesP

+ 11 - 0
src/stores/counter.ts

@@ -0,0 +1,11 @@
+import { ref, computed } from 'vue'
+import { defineStore } from 'pinia'
+
+export const useCounterStore = defineStore('counter', () => {
+  const count = ref(0)
+  const doubleCount = computed(() => count.value * 2)
+  function increment() {
+    count.value++
+  }
+  return { count, doubleCount, increment }
+})

+ 27 - 0
src/stores/user.ts

@@ -0,0 +1,27 @@
+import { defineStore } from 'pinia';
+// defineStore 方法有两个参数,第一个参数是模块化名字(也就相当于身份证一样,不能重复)
+
+// 第二个参数是选项,对象里面有三个属性,相比于vuex 少了一个 mutations.
+export const useUserStore = defineStore('user', {
+  state(){  // 存放的就是模块的变量
+    return {
+      token: localStorage.getItem('token'),
+      openId: localStorage.getItem('openId'),
+      isEur: window.location.hostname.includes('eur'),
+    }
+  },
+  getters:{ // 相当于vue里面的计算属性,可以缓存数据
+    getToken(state){
+      return state.token || localStorage.getItem('token')
+    },
+    getOpenId(state){
+      return state.openId || localStorage.getItem('openId')
+    }
+  },
+  actions:{ // 可以通过actions 方法,改变 state 里面的值。
+    setUserOpenId(value:string){
+      this.openId = value
+      localStorage.setItem('openId', value,)
+    }
+  }
+})

+ 121 - 0
src/utils/api.js

@@ -0,0 +1,121 @@
+import axios from "axios";
+import { ElLoading, ElMessage } from "element-plus";
+let current = (localStorage && localStorage.getItem('language')) || 'zh'
+let token = (localStorage && localStorage.getItem('token')) || 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxNTkxNTgxNjA0MSIsImxvZ2luVHlwZSI6InVzZXIiLCJ1c2VyTmFtZSI6IjE1OTE1ODE2MDQxIiwiaWF0IjoxNjgxODAzNzY1LCJqdGkiOiJhMDU4M2EwZS01M2EzLTQ1YTUtOTI1ZS1kZDgzYzU5Y2Y5MGMifQ.bdu5jqbSxSlo9LH4w_uPEuP67DUJk6w5Yqnu633OtQI'
+// request是一个axios实例,每一个实例你都可以单独定制它的baseURL,超时时间,请求头和一些其他配置项。
+const baseUrl = import.meta.env.VITE_BASE_URL; //接口统一域名
+const instance = axios.create({
+    baseURL: baseUrl,
+    timeout: 60 * 1000, //设置超时
+    headers: {
+        "Content-Type": "application/json;charset=UTF-8;",
+        "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
+        ".AspNetCore.Culture": "c=zh-Hans|uic=zh-Hans",
+        token:token,
+        current,
+        lang:current,
+    },
+});
+
+let loading;
+//正在请求的数量
+let requestCount = 0;
+//显示loading
+const showLoading = () => {
+    if (requestCount === 0) {
+        loading = ElLoading.service({
+            fullscreen: true,
+            text: "Loading  ",
+            background: "rgba(0, 0, 0, 0.7)",
+        });
+    }
+    requestCount++;
+};
+//隐藏loading
+const hideLoading = () => {
+    requestCount--;
+    if (requestCount == 0) {
+        loading.close();
+    }
+};
+
+//请求拦截器
+instance.interceptors.request.use(
+    (config) => {
+        console.log('config',config.loading,'if',config.loading != true)
+        if(!config.loading){
+            showLoading();
+        }
+        // 每次发送请求之前判断是否存在token,如果存在,则统一在http请求的header都加上token,不用每次请求都手动添加了
+        //若请求方式为post,则将data参数转为JSON字符串
+        if (config.method === "POST") {
+            config.data = JSON.stringify(config.data);
+        }
+        return config;
+    },
+    (error) =>
+        // 对请求错误做些什么
+        Promise.reject(error)
+);
+
+//响应拦截器
+instance.interceptors.response.use(
+    (response) => {
+        hideLoading();
+        if (response.data.code !== 200) {
+            ElMessage.error(response.data.message);
+            return  Promise.reject(response.data)
+            // router.push("/");
+        }
+        else return response.data?.data;
+    },
+    (error) => {
+        hideLoading();
+        //响应错误
+        let message = "";
+        if (error.response && error.response.status) {
+            const status = error.response.status;
+            console.log('Error',error.response)
+            switch (status) {
+                case 400:
+                    message = "请求错误";
+                    break;
+                case 401:
+                    message = "请求错误";
+                    break;
+                case 404:
+                    message = "请求地址出错";
+                    break;
+                case 408:
+                    message = "请求超时";
+                    break;
+                case 500:
+                    message = "服务器内部错误!";
+                    break;
+                case 501:
+                    message = "服务未实现!";
+                    break;
+                case 502:
+                    message = "网关错误!";
+                    break;
+                case 503:
+                    message = "服务不可用!";
+                    break;
+                case 504:
+                    message = "网关超时!";
+                    break;
+                case 505:
+                    message = "HTTP版本不受支持";
+                    break;
+                default:
+                    message = "请求失败";
+            }
+
+            ElMessage.error(message);
+            return Promise.reject(error);
+        }
+        return Promise.reject(error);
+    }
+);
+
+export default instance;

+ 41 - 0
src/utils/file/base64Conver.ts

@@ -0,0 +1,41 @@
+/**
+ * @description: base64 to blob
+ */
+export function dataURLtoBlob(base64Buf: string): Blob {
+  const arr = base64Buf.split(',');
+  const typeItem = arr[0];
+  const mime = typeItem.match(/:(.*?);/)![1];
+  const bstr = atob(arr[1]);
+  let n = bstr.length;
+  const u8arr = new Uint8Array(n);
+  while (n--) {
+    u8arr[n] = bstr.charCodeAt(n);
+  }
+  return new Blob([u8arr], { type: mime });
+}
+
+/**
+ * img url to base64
+ * @param url
+ */
+export function urlToBase64(url: string, mineType?: string): Promise<string> {
+  return new Promise((resolve, reject) => {
+    let canvas = document.createElement('CANVAS') as Nullable<HTMLCanvasElement>;
+    const ctx = canvas!.getContext('2d');
+
+    const img = new Image();
+    img.crossOrigin = '';
+    img.onload = function () {
+      if (!canvas || !ctx) {
+        return reject();
+      }
+      canvas.height = img.height;
+      canvas.width = img.width;
+      ctx.drawImage(img, 0, 0);
+      const dataURL = canvas.toDataURL(mineType || 'image/png');
+      canvas = null;
+      resolve(dataURL);
+    };
+    img.src = url;
+  });
+}

+ 137 - 0
src/utils/file/download.ts

@@ -0,0 +1,137 @@
+import { openWindow } from '..';
+import { dataURLtoBlob, urlToBase64 } from './base64Conver';
+// import * as XLSX from 'xlsx' // Vue3 版本
+// import fs from 'file-saver'
+// /**
+//  * Download online pictures
+//  * @param data
+//  * @param filename
+//  */
+//  export function exportElsxFile(json, fields, filename = '表格.xlsx') {
+//   json.forEach(item => {
+//       for (let i in item) {
+//           if (fields.hasOwnProperty(i)) {
+//               item[fields[i]] = item[i];
+//           }
+//           delete item[i]; //删除原先的对象属性
+//       }
+//   })
+
+//   let sheetName = filename //excel的文件名称
+//   let wb = XLSX.utils.book_new()  //工作簿对象包含一SheetNames数组,以及一个表对象映射表名称到表对象。XLSX.utils.book_new实用函数创建一个新的工作簿对象。
+//   let ws = XLSX.utils.json_to_sheet(json, { header: Object.values(fields) }) //将JS对象数组转换为工作表。
+//   wb.SheetNames.push(sheetName)
+//   wb.Sheets[sheetName] = ws
+//   const defaultCellStyle = { font: { name: "Verdana", sz: 13, color: "FF00FF88" }, fill: { fgColor: { rgb: "FFFFAA00" } } };//设置表格的样式
+//   let wopts = { bookType: 'xlsx', bookSST: false, type: 'binary', cellStyles: true, defaultCellStyle: defaultCellStyle, showGridLines: false }  //写入的样式
+//   let wbout = XLSX.write(wb, wopts)
+//   let blob = new Blob([s2ab(wbout)], { type: 'application/octet-stream' })
+//   fs.saveAs(blob, filename + '.xlsx')
+// }
+// const s2ab = s => {
+//   if (typeof ArrayBuffer !== 'undefined') {
+//       var buf = new ArrayBuffer(s.length)
+//       var view = new Uint8Array(buf)
+//       for (var i = 0; i != s.length; ++i) view[i] = s.charCodeAt(i) & 0xff
+//       return buf
+//   } else {
+//       var buf = new Array(s.length);
+//       for (var i = 0; i != s.length; ++i) buf[i] = s.charCodeAt(i) & 0xFF;
+//       return buf;
+//   }
+// }
+
+/**
+ * Download online pictures
+ * @param url
+ * @param filename
+ * @param mime
+ * @param bom
+ */
+export function downloadByOnlineUrl(url: string, filename: string, mime?: string, bom?: BlobPart) {
+  urlToBase64(url).then((base64) => {
+    downloadByBase64(base64, filename, mime, bom);
+  });
+}
+
+/**
+ * Download pictures based on base64
+ * @param buf
+ * @param filename
+ * @param mime
+ * @param bom
+ */
+export function downloadByBase64(buf: string, filename: string, mime?: string, bom?: BlobPart) {
+  const base64Buf = dataURLtoBlob(buf);
+  downloadByData(base64Buf, filename, mime, bom);
+}
+
+/**
+ * Download according to the background interface file stream
+ * @param {*} data
+ * @param {*} filename
+ * @param {*} mime
+ * @param {*} bom
+ */
+export function downloadByData(data: BlobPart, filename: string, mime?: string, bom?: BlobPart) {
+  const blobData = typeof bom !== 'undefined' ? [bom, data] : [data];
+  const blob = new Blob(blobData, { type: mime || 'application/octet-stream' });
+
+  const blobURL = window.URL.createObjectURL(blob);
+  const tempLink = document.createElement('a');
+  tempLink.style.display = 'none';
+  tempLink.href = blobURL;
+  tempLink.setAttribute('download', filename);
+  if (typeof tempLink.download === 'undefined') {
+    tempLink.setAttribute('target', '_blank');
+  }
+  document.body.appendChild(tempLink);
+  tempLink.click();
+  document.body.removeChild(tempLink);
+  window.URL.revokeObjectURL(blobURL);
+}
+
+/**
+ * Download file according to file address
+ * @param {*} sUrl
+ */
+declare type TargetContext = '_self' | '_blank';
+export function downloadByUrl({
+  url,
+  target = '_blank',
+  fileName,
+}: {
+  url: string;
+  target?: TargetContext;
+  fileName?: string;
+}): boolean {
+  const isChrome = window.navigator.userAgent.toLowerCase().indexOf('chrome') > -1;
+  const isSafari = window.navigator.userAgent.toLowerCase().indexOf('safari') > -1;
+
+  if (/(iP)/g.test(window.navigator.userAgent)) {
+    console.error('Your browser does not support download!');
+    return false;
+  }
+  if (isChrome || isSafari) {
+    const link = document.createElement('a');
+    link.href = url;
+    link.target = target;
+
+    if (link.download !== undefined) {
+      link.download = fileName || url.substring(url.lastIndexOf('/') + 1, url.length);
+    }
+
+    if (document.createEvent) {
+      const e = document.createEvent('MouseEvents');
+      e.initEvent('click', true, true);
+      link.dispatchEvent(e);
+      return true;
+    }
+  }
+  if (url.indexOf('?') === -1) {
+    url += '?download';
+  }
+
+  openWindow(url, { target });
+  return true;
+}

+ 150 - 0
src/utils/index.ts

@@ -0,0 +1,150 @@
+import type { RouteLocationNormalized, RouteRecordNormalized } from 'vue-router';
+import type { App, Plugin } from 'vue';
+import { useUserStore } from '@/stores/user'
+import { useI18n } from 'vue-i18n'
+
+import { unref } from 'vue';
+import { isObject } from '@/utils/is';
+import type { Component } from 'vue';
+
+export const noop = () => { };
+declare type TargetContext = '_self' | '_blank';
+declare type Recordable<T = any> = Record<string, T>;
+/**
+ * @description:  Set ui mount node
+ */
+export function getPopupContainer(node?: HTMLElement): HTMLElement {
+  return (node?.parentNode as HTMLElement) ?? document.body;
+}
+
+/**
+ * Add the object as a parameter to the URL
+ * @param baseUrl url
+ * @param obj
+ * @returns {string}
+ * eg:
+ *  let obj = {a: '3', b: '4'}
+ *  setObjToUrlParams('www.baidu.com', obj)
+ *  ==>www.baidu.com?a=3&b=4
+ */
+export function setObjToUrlParams(baseUrl: string, obj: any): string {
+  let parameters = '';
+  for (const key in obj) {
+    parameters += key + '=' + encodeURIComponent(obj[key]) + '&';
+  }
+  parameters = parameters.replace(/&$/, '');
+  return /\?$/.test(baseUrl) ? baseUrl + parameters : baseUrl.replace(/\/?$/, '?') + parameters;
+}
+
+export function deepMerge<T = any>(src: any = {}, target: any = {}): T {
+  let key: string;
+  for (key in target) {
+    src[key] = isObject(src[key]) ? deepMerge(src[key], target[key]) : (src[key] = target[key]);
+  }
+  return src;
+}
+
+export function openWindow(
+  url: string,
+  opt?: { target?: TargetContext | string; noopener?: boolean; noreferrer?: boolean },
+) {
+  const { target = '__blank', noopener = true, noreferrer = true } = opt || {};
+  const feature: string[] = [];
+
+  noopener && feature.push('noopener=yes');
+  noreferrer && feature.push('noreferrer=yes');
+
+  window.open(url, target, feature.join(','));
+}
+
+// dynamic use hook props
+export function getDynamicProps<T, U>(props: T): Partial<U> {
+  const ret: Recordable = {};
+
+  Object.keys(props).map((key) => {
+    ret[key] = unref((props as Recordable)[key]);
+  });
+
+  return ret as Partial<U>;
+}
+
+export function getRawRoute(route: RouteLocationNormalized): RouteLocationNormalized {
+  if (!route) return route;
+  const { matched, ...opt } = route;
+  return {
+    ...opt,
+    matched: (matched
+      ? matched.map((item) => ({
+        meta: item.meta,
+        name: item.name,
+        path: item.path,
+      }))
+      : undefined) as RouteRecordNormalized[],
+  };
+}
+
+export const withInstall = <T>(component: T, alias?: string) => {
+  const comp = component as any;
+  comp.install = (app: App) => {
+    app.component(comp.name || comp.displayName, component);
+    if (alias) {
+      app.config.globalProperties[alias] = component;
+    }
+  };
+  return component as T & Plugin;
+};
+//请求微信接口,用来获取code
+export async function getWeChatCode(appid:string) {
+  const local = encodeURIComponent(window.location.href) //一定要用encodeURIComponent方法获取当前页面地址作为回调地址
+  //通过微信官方接口获取code之后,会重新刷新设置的回调地址【redirect_uri】
+  window.location.href =
+    'https://open.weixin.qq.com/connect/oauth2/authorize?appid=' +
+    appid +
+    '&redirect_uri=' +
+    local +
+    '&response_type=code&scope=snsapi_userinfo&state=#wechat_redirect&connect_redirect=1'
+}
+
+export function GetRequest(value: string) {
+  var url = decodeURI(window.location.search); //?id="123456"&name="www";
+  var object = {};
+  if (url.indexOf("?") != -1)//url中存在问号,也就说有参数。  
+  {
+    var str = url.substr(1);  //得到?后面的字符串
+    var strs = str.split("&");  //将得到的参数分隔成数组[id="123456",name="www"];
+    for (var i = 0; i < strs.length; i++) {
+      object[strs[i].split("=")[0]] = strs[i].split("=")[1];//得到{id:'123456',name:'www'}
+    }
+  }
+  console.log('GetRequest', object)
+  return object[value];
+}
+
+//支付备注
+export const getRemark = (list, orderType, t) => {
+  
+// const { locale, t } = useI18n()
+const { isEur } = useUserStore()
+  let str = []
+  list.map(item => {
+    let newStr = ''
+    if (orderType == 'cooperationOrder') {
+      newStr = t('mall.cooperationOrderName',{count: item.count })
+    } else if (orderType == 'downOrder') {
+      newStr = `${item.name} (${item.sceneNum})`
+    } else if (orderType != 'incrementOrder') {
+      newStr = `${locale.value == 'zh' ? item.name : item.nameEn}`
+    } else {
+      newStr = `${item.type == 1 ? t('payInfo.gjhy') : isEur ? t('payInfo.zyhys') : t('payInfo.zyhy')}`
+    }
+    if(orderType != 'cooperationOrder' && orderType != 'downOrder'){
+      newStr += `x ${item.count}` 
+    }
+    
+    if(orderType == 'incrementOrder'){
+      newStr += `,${item.type?t(`payInfo.expirationmon`, { num: item.monthQy || 1 }):t(`payInfo.expiration`, { num: 1 })}` 
+    }
+    str.push(newStr)
+  })
+  return str.join('、')
+}

+ 99 - 0
src/utils/is.ts

@@ -0,0 +1,99 @@
+const toString = Object.prototype.toString;
+
+export function is(val: unknown, type: string) {
+  return toString.call(val) === `[object ${type}]`;
+}
+
+export function isDef<T = unknown>(val?: T): val is T {
+  return typeof val !== 'undefined';
+}
+
+export function isUnDef<T = unknown>(val?: T): val is T {
+  return !isDef(val);
+}
+
+export function isObject(val: any): val is Record<any, any> {
+  return val !== null && is(val, 'Object');
+}
+
+export function isEmpty<T = unknown>(val: T): val is T {
+  if (isArray(val) || isString(val)) {
+    return val.length === 0;
+  }
+
+  if (val instanceof Map || val instanceof Set) {
+    return val.size === 0;
+  }
+
+  if (isObject(val)) {
+    return Object.keys(val).length === 0;
+  }
+
+  return false;
+}
+
+export function isDate(val: unknown): val is Date {
+  return is(val, 'Date');
+}
+
+export function isNull(val: unknown): val is null {
+  return val === null;
+}
+
+export function isNullAndUnDef(val: unknown): val is null | undefined {
+  return isUnDef(val) && isNull(val);
+}
+
+export function isNullOrUnDef(val: unknown): val is null | undefined {
+  return isUnDef(val) || isNull(val);
+}
+
+export function isNumber(val: unknown): val is number {
+  return is(val, 'Number');
+}
+
+export function isPromise<T = any>(val: unknown): val is Promise<T> {
+  return is(val, 'Promise') && isObject(val) && isFunction(val.then) && isFunction(val.catch);
+}
+
+export function isString(val: unknown): val is string {
+  return is(val, 'String');
+}
+
+export function isFunction(val: unknown): val is Function {
+  return typeof val === 'function';
+}
+
+export function isBoolean(val: unknown): val is boolean {
+  return is(val, 'Boolean');
+}
+
+export function isRegExp(val: unknown): val is RegExp {
+  return is(val, 'RegExp');
+}
+
+export function isArray(val: any): val is Array<any> {
+  return val && Array.isArray(val);
+}
+
+export function isWindow(val: any): val is Window {
+  return typeof window !== 'undefined' && is(val, 'Window');
+}
+
+export function isElement(val: unknown): val is Element {
+  return isObject(val) && !!val.tagName;
+}
+
+export function isMap(val: unknown): val is Map<any, any> {
+  return is(val, 'Map');
+}
+
+export const isServer = typeof window === 'undefined';
+
+export const isClient = !isServer;
+
+export function isUrl(path: string): boolean {
+  const reg =
+    /(((^https?:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$/;
+  return reg.test(path);
+}

+ 31 - 0
src/utils/request.js

@@ -0,0 +1,31 @@
+import instance from "./api.js";
+/**
+ * @param {String} method  请求的方法:get、post、delete、put
+ * @param {String} url     请求的url:
+ * @param {Object} data    请求的参数
+ * @param {Object} config  请求的配置
+ * @returns {Promise}     返回一个promise对象,其实就相当于axios请求数据的返回值
+ */
+
+export const request = ({ method, url, data, config }) => {
+  //把大写转换成小写
+  method = method.toLowerCase();
+  if (method == "post") {
+    return instance.post(url, data, { ...config });
+  } else if (method == "get") {
+    return instance.get(url, {
+      params: data,
+      ...config,
+    });
+  } else if (method == "delete") {
+    return instance.delete(url, {
+      params: data,
+      ...config,
+    });
+  } else if (method == "put") {
+    return instance.put(url, data, { ...config });
+  } else {
+    console.error("未知的method" + method);
+    return false;
+  }
+};

+ 47 - 0
src/utils/status.ts

@@ -0,0 +1,47 @@
+export const showMessage = (status:number|string) : string => {
+    let message:string = "";
+    switch (status) {
+        case 400:
+            message = "请求错误(400)";
+            break;
+        case 401:
+            message = "未授权,请重新登录(401)";
+            break;
+        case 403:
+            message = "拒绝访问(403)";
+            break;
+        case 404:
+            message = "请求出错(404)";
+            break;
+        case 408:
+            message = "请求超时(408)";
+            break;
+        case 500:
+            message = "服务器错误(500)";
+            break;
+        case 501:
+            message = "服务未实现(501)";
+            break;
+        case 502:
+            message = "网络错误(502)";
+            break;
+        case 503:
+            message = "服务不可用(503)";
+            break;
+        case 504:
+            message = "网络超时(504)";
+            break;
+        case 505:
+            message = "HTTP版本不受支持(505)";
+            break;
+        default:
+            message = `连接出错(${status})!`;
+    }
+    return `${message},请检查网络或联系管理员!`;
+};
+export const paysidtype = {
+    wechatPay: 2,
+    alipay: 3,
+    paypal: 5,
+    stripe: 6
+};

+ 4 - 0
src/utils/vconsole.js

@@ -0,0 +1,4 @@
+import Vconsole from 'vconsole'
+if ((navigator.userAgent.match(/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i))) {
+new Vconsole()
+}

+ 364 - 0
src/views/mobile/index.vue

@@ -0,0 +1,364 @@
+<script setup lang="ts">
+import { showConfirm } from '@/components/Toast'
+import { ref, computed, onMounted } from 'vue'
+import { useUserStore } from '@/stores/user'
+import { openPay, getOrderInfo, wxLogin } from '@/api/api'
+import { useRoute } from 'vue-router'
+import { GetRequest, getWeChatCode, getRemark } from '@/utils/index'
+import { useI18n } from 'vue-i18n'
+const route = useRoute()
+const orderDetal = ref({
+  payStatus:0
+})
+const orderSn = ref(route.query.id || GetRequest('id'))
+const { locale: language, t } = useI18n()
+const userStore = useUserStore();
+const isEur = userStore.isEur
+const payType = ref(isEur?'5':'0')
+const openId = computed(() => userStore.getOpenId);
+const is_zfb = ref(false)
+const is_weixn = ref(false)
+var browser = navigator.userAgent.toLowerCase();
+  if(isEur){
+    payType.value = '5'
+  }else if(browser.match(/Alipay/i)=="alipay"){
+    console.log("支付宝app的浏览器");
+    is_zfb.value = true
+    payType.value = '4'
+  }else if(browser.match(/MicroMessenger/i)=="micromessenger"){
+    console.log("微信app的浏览器");
+    is_weixn.value = true
+  }
+// const is_weixn = computed(() => {
+//   var wx = window.navigator.userAgent.toLowerCase()
+//   if (wx.match(/MicroMessenger/i) == 'micromessenger') {
+//     return true // 是微信端
+//   } else {
+//     return false
+//   }
+// })
+// let PAYSID = {
+//   wechatPay: is_weixn.value ? 1 : 0,
+//   alipay: 4,
+//   paypal: 5
+// }
+//判断是否微信
+onMounted(() => {
+  getDetial()
+})
+async function handelPay() {
+  let apiData = {
+    orderSn: orderSn.value,
+    payType: is_weixn.value ? '1' : payType.value,
+    openId:openId.value,
+    productName: getRemark(orderDetal.value.goodsInfo, orderDetal.value.orderType, t),
+  }
+  const res = await openPay(apiData)
+
+  if (is_weixn.value) {
+    //微信内支付
+    onBridgeReady(res)
+  } else if (res.form || res.h5Url || res.redirect) {
+    console.log('payType', res.form || res.h5Url)
+    window.location.href = res.form || res.h5Url || res.redirect
+  } else {
+    showConfirm({
+      text: t('payInfo.userName'),
+      type: 'err',
+      callback: (val) => {
+      }
+    })
+  }
+}
+// 调微信支付
+function onBridgeReady(obj) {
+  var that = this
+  WeixinJSBridge.invoke(
+    'getBrandWCPayRequest',
+    {
+      appId: obj.appid,
+      //公众号名称,由商户传入
+      timeStamp: obj.timeStamp + '',
+      //时间戳,自1970年以来的秒数
+      nonceStr: obj.nonceStr,
+      //随机串
+      package: `prepay_id=${obj.prepayId}`,
+      signType: obj.signType,
+      //微信签名方式:
+      paySign: obj.paySign
+    },
+    function (res) {
+      if (res.err_msg == 'get_brand_wcpay_request:ok') {
+        orderDetal.value.payStatus = 1
+        // 使用以上方式判断前端返回,微信团队郑重提示:
+        //res.err_msg将在用户支付成功后返回ok,但并不保证它绝对可靠。
+        handlePayresult()
+      } else {
+        orderDetal.value.payStatus = 2
+        handlePayresult()
+      }
+    }
+  )
+}
+function handleType(params: string) {
+  payType.value = params
+}
+function getDetial() {
+  getOrderInfo(orderSn.value).then((res) => {
+    orderDetal.value = res
+    if(res.payStatus != 0){
+      handlePayresult()
+      return
+    }else{
+      setTimeout(()=>{
+        renewStatus()
+      },2000)
+    }
+    if(is_weixn.value){
+      handleWxlogin(res.wxAppId)
+    }
+  })
+}
+
+function renewStatus() {
+  getOrderInfo(orderSn.value).then((res) => {
+    orderDetal.value = res
+    if(res.payStatus != 0){
+      handlePayresult()
+      return
+    }else{
+      setTimeout(()=>{
+        renewStatus()
+      },2000)
+    }
+  })
+}
+async function handlePayresult(){
+  let item = orderDetal.value
+  if(!item.orderType){
+    orderDetal.value = await getOrderInfo(orderSn.value)
+    item = orderDetal.value
+  }
+  showConfirm({
+        text: item.autoPay? t('payInfo.autoPayTips', {paytype: item.payType == 5 ? 'PayPal' : 'Stripe'}) : item.payStatus === 2 ? t('payInfo.payfail') : t('mall.paySuccessTip'),
+        type: item.payStatus == 2 ? 'err' : 'success',
+        callback: () => {
+        if(item.orderType == 'incrementOrder'){
+          location.replace(`/mobile.html#/information`)
+        }else{
+          location.replace(`/mobile.html#/payresult/${item.payStatus == 2?'fail':'success'}`)
+        }
+        }
+      })
+}
+async function handleWxlogin(wxAppId: string) {
+  // userStore.setUserOpenId('o3S0L1Hyd3O0vYI2Kr1lFDEtEO2k')
+  const code = GetRequest('code')
+  if (!openId.value && !code) {
+    //微信登录
+    getWeChatCode(wxAppId)
+  } else if (code) {
+    //存在code 换取openid
+    const res = await wxLogin({ code, orderSn: orderSn.value })
+    if (res.openid) {
+      userStore.setUserOpenId(res.openid)
+    }
+  }
+}
+async function handleOpenPay() {
+  openPay({
+    orderSn: GetRequest('orderSn'),
+    payType: payType.value,
+    openId:openId.value,
+  }).then((res) => {
+    let url = res.qrCodeUrl
+  })
+}
+</script>
+<template>
+  <div class="mobilePage">
+    <div class="pageTitle">{{$t('payInfo.payCenter')}}</div>
+    <div class="contentInfo">
+      <div class="info">
+        <div class="cell">
+          <span>{{$t('payInfo.userName')}}{{ getOpenId }}</span>
+          <span>{{orderDetal.userName}}</span>
+        </div>
+        <div class="cell" v-if="orderDetal.orderType == 'incrementOrder'">
+          <span>{{$t('payInfo.membership')}}</span>
+          <span v-if="orderDetal.goodsInfo && orderDetal.goodsInfo[0]">
+            {{ orderDetal.goodsInfo[0].type==1?$t('payInfo.gjhy'):isEur?$t('payInfo.zyhys'):$t('payInfo.zyhy') }}
+            <span>× {{orderDetal.goodsInfo[0].count}}</span>
+            <!-- <span v-else>1</span> -->
+            <span v-if="orderDetal.orderType == 'incrementOrder'">,{{ orderDetal.goodsInfo[0].type==1?$t(`payInfo.expirationmon`,{num:orderDetal.goodsInfo[0].monthQy || 1}):$t(`payInfo.expiration`,{num:1}) }}</span>
+            </span>
+        </div>
+        <div class="item" v-if="orderDetal.orderType == 'partOrder'">
+          <div class="cell" style="border:none">
+            <span style="max-width:85px">{{ $t(`mall.goqy`) }}</span>
+            <span></span>
+          </div>
+          <div class="cell" v-for="(item,index) in orderDetal.goodsInfo" :key="index" style="border:none">
+          <span style="max-width:230px;line-height: 16px;">{{ language === 'zh' ?item.name:item.nameEn }}</span>
+          <span>
+            × {{item.count}}
+          </span>
+        </div>
+        </div>
+        <div class="item" v-else-if="orderDetal.orderType == 'cooperationOrder'">
+          <div class="cell" style="border:none">
+            <span style="max-width:85px">{{ $t(`mall.cooperationOrder`) }}</span>
+            <span></span>
+          </div>
+          <div class="cell" v-for="(item,index) in orderDetal.goodsInfo" :key="index" style="border:none">
+          <span style="max-width:230px;line-height: 16px;">{{ $t('mall.cooperationOrderName',{count: item.count }) }}</span>
+        </div>
+        </div>
+        <div class="cell" v-else>
+          <span style="min-width:85px">{{ $t(`mall.${orderDetal.orderType == 'downOrder'?'downdesc':'goqy'}`) }}</span>
+          <span>
+            <span v-for="(item,index) in orderDetal.goodsInfo" :key="index">
+              <span v-if="index != 0">、</span>
+                  <span style="margin-right:5px" v-if="orderDetal.orderType != 'incrementOrder'">
+                    {{ orderDetal.orderType == 'downOrder'?`${item.name} (${item.sceneNum})`:language === 'zh' ?item.name:item.nameEn }}
+                  </span>
+                  <span style="margin-right:5px" v-else>
+                    {{ item.type==1?$t('payInfo.gjhy'):isEur?$t('payInfo.zyhys'):$t('payInfo.zyhy') }}
+                  </span>
+                   <span v-if="orderDetal.orderType != 'downOrder'">× {{item.count}}</span>
+                
+            </span>
+          </span>
+        </div>
+      </div>
+      <div class="payType">
+        <div class="cell" @click="handleType('0')" v-if="!is_zfb && !isEur">
+          <span>
+            <img class="payTypeImg" src="@/assets/images/wechat56.png" alt="" />
+            {{$t('payInfo.wx')}}
+          </span>
+          <div class="select" :class="{ active: payType == '0' }"></div>
+        </div>
+        <div class="cell" @click="handleType('4')" v-if="!is_weixn && !isEur">
+          <span>
+            <img class="payTypeImg" src="@/assets/images/alipay56.png" alt="" />
+            {{$t('payInfo.zfb')}}
+          </span>
+          <div class="select" :class="{ active: payType == '4' }"></div>
+        </div>
+        <div class="cell" @click="handleType('5')" v-if="isEur">
+          <span>
+            <img style="height:26px;width:100px" class="payTypeImg" src="@/assets/images/paypal.png" alt="" />
+            <!-- {{$t('payInfo.paypal')}} -->
+          </span>
+          <div class="select" :class="{ active: payType == '5' }"></div>
+        </div>
+        <!-- <div class="cell" @click="handleType('6')" v-if="isEur">
+          <span>
+            <img style="height:26px;width:62px" class="payTypeImg" src="@/assets/images/stripe.png" alt="" />
+          </span>
+          <div class="select" :class="{ active: payType == '6' }"></div>
+        </div> -->
+      </div>
+    </div>
+    <div class="bottomInfo">
+      <div class="price">
+        {{$t('payInfo.payPrice')}}<span style="margin-left:10px">{{isEur?'$':$t('payInfo.priceUnit')}}{{ orderDetal?.orderMoney || 0.0 }}</span>
+      </div>
+      <div class="payBut" @click="handelPay">{{ orderDetal.autoPay == 1?$t('payInfo.autoRenew'):$t('payInfo.payOrder') }}</div>
+    </div>
+  </div>
+</template>
+
+<style lang="less" scoped>
+.mobilePage {
+  background: #f7f7f7;
+  min-height: calc(100vh - 50px);
+  max-width: 100vw;
+  display: block;
+  color:#202020;
+  .pageTitle {
+    width: calc(100% - 40px);
+    padding: 23px 20px;
+    background: #ffffff;
+    border-radius: 0px 0px 0px 0px;
+    opacity: 1;
+    border: 1px solid #e6e6e6;
+    margin-bottom: 10px;
+  }
+  .contentInfo {
+    .cell {
+      display: flex;
+      justify-content: space-between;
+      background-color: #fff;
+      padding: 15px 20px;
+      border-bottom: 1px solid #ebedf0;
+      align-items: center;
+      &:last-child {
+        border-bottom: none;
+      }
+      .select {
+        border-radius: 50%;
+        height: 20px;
+        width: 20px;
+        border: 1px solid #d5d8de;
+      }
+      .active {
+        background: url('@/assets/images/active.png');
+        background-position: center center;
+        background-size: 22px;
+        height: 22px;
+        width: 22px;
+        border: none;
+      }
+      .payTypeImg {
+        height: 18px;
+        width: 20px;
+        margin-right: 8px;
+        position: relative;
+        top: 3px;
+      }
+    }
+  }
+  .bottomInfo {
+    height: 56px;
+    width: 100%;
+    border-radius: 0px 0px 0px 0px;
+    display: flex;
+    align-items: center;
+    background-color: #fff;
+    justify-content: space-between;
+    position: fixed;
+    bottom: 0px;
+    .price {
+      padding: 5px 20px;
+      span {
+        font-size: 16px;
+        font-family: PingFang SC-Regular, PingFang SC;
+        font-weight: 400;
+        color: #ff0000;
+        line-height: 19px;
+        // letter-spacing: 4px;
+      }
+    }
+    .payBut {
+      background: @primary-color;
+      border-radius: 0px 0px 0px 0px;
+      opacity: 1;
+      width: 120px;
+      line-height: 56px;
+      height: 100%;
+      color: #fff;
+      text-align: center;
+    }
+  }
+  .info {
+    margin-bottom: 10px;
+    font-size: 16px;
+    font-family: PingFang SC-Regular, PingFang SC;
+    font-weight: 400;
+    color: #202020;
+    line-height: 19px;
+  }
+}
+</style>

+ 27 - 0
src/views/mobile/information/index.vue

@@ -0,0 +1,27 @@
+<script setup lang="ts">
+import { showConfirm } from '@/components/Toast'
+import { ref, computed, onMounted } from 'vue'
+import { useUserStore } from '@/stores/user'
+import { openPay, getOrderInfo, wxLogin } from '@/api/api'
+import { useRoute } from 'vue-router'
+import { GetRequest, getWeChatCode, getRemark } from '@/utils/index'
+import { useI18n } from 'vue-i18n'
+const route = useRoute()
+const { locale: language, t } = useI18n()
+const userStore = useUserStore();
+const isEur = userStore.isEur
+</script>
+<template>
+  <div class="mobilePage">mobilePage
+  </div>
+</template>
+
+<style lang="less" scoped>
+.mobilePage {
+  background: #f7f7f7;
+  min-height: calc(100vh - 50px);
+  max-width: 100vw;
+  display: block;
+  color:#202020;
+}
+</style>

+ 27 - 0
src/views/pc/appProduct/index.vue

@@ -0,0 +1,27 @@
+<script setup lang="ts">
+import { showConfirm } from '@/components/Toast'
+import { ref, computed, onMounted } from 'vue'
+import { useUserStore } from '@/stores/user'
+import { openPay, getOrderInfo, wxLogin } from '@/api/api'
+import { useRoute } from 'vue-router'
+import { GetRequest, getWeChatCode, getRemark } from '@/utils/index'
+import { useI18n } from 'vue-i18n'
+const route = useRoute()
+const { locale: language, t } = useI18n()
+const userStore = useUserStore();
+const isEur = userStore.isEur
+</script>
+<template>
+  <div class="mobilePage">mobilePage
+  </div>
+</template>
+
+<style lang="less" scoped>
+.mobilePage {
+  background: #f7f7f7;
+  min-height: calc(100vh - 50px);
+  max-width: 100vw;
+  display: block;
+  color:#202020;
+}
+</style>

+ 27 - 0
src/views/pc/device/index.vue

@@ -0,0 +1,27 @@
+<script setup lang="ts">
+import { showConfirm } from '@/components/Toast'
+import { ref, computed, onMounted } from 'vue'
+import { useUserStore } from '@/stores/user'
+import { openPay, getOrderInfo, wxLogin } from '@/api/api'
+import { useRoute } from 'vue-router'
+import { GetRequest, getWeChatCode, getRemark } from '@/utils/index'
+import { useI18n } from 'vue-i18n'
+const route = useRoute()
+const { locale: language, t } = useI18n()
+const userStore = useUserStore();
+const isEur = userStore.isEur
+</script>
+<template>
+  <div class="mobilePage">mobilePage
+  </div>
+</template>
+
+<style lang="less" scoped>
+.mobilePage {
+  background: #f7f7f7;
+  min-height: calc(100vh - 50px);
+  max-width: 100vw;
+  display: block;
+  color:#202020;
+}
+</style>

+ 450 - 0
src/views/pc/index.vue

@@ -0,0 +1,450 @@
+<script setup lang="ts">
+import { mapState } from 'pinia'
+import { useI18n } from 'vue-i18n'
+import { ref, watch, onMounted, nextTick } from 'vue'
+import logoCn from '@/assets/images/logoCn.png'
+import logoEn from '@/assets/images/logoEn.png'
+import { useUserStore } from '@/stores/user'
+import QrcodeVue from 'qrcode.vue'
+import { show, showConfirm } from '@/components/Toast'
+import { openPay, queryOrderStatus, getOrderInfo } from '@/api/api'
+import { useRouter, useRoute } from 'vue-router'
+import { GetRequest, getRemark } from '@/utils/index'
+import { paysidtype } from '@/utils/status'
+import { getCurrentInstance } from 'vue'
+
+//得到i18n的locale token, info,
+const { locale: language, t } = useI18n()
+let t1 = null
+const route = useRoute()
+const { isEur, info } = useUserStore()
+const orderSn = ref(route.query.id || GetRequest('orderSn'))
+let { $cdn } = getCurrentInstance()?.proxy
+
+const selectedPayType = ref(isEur ? 'paypal' : 'alipay')
+watch(selectedPayType, () => {
+  getCode()
+})
+const qrCodeUrl = ref('')
+const orderDetal = ref({})
+const response = ref({
+  price: 0.01,
+  src: ''
+})
+onMounted(() => {
+  // getDetial()
+})
+
+function inverRequest() {
+  clearInterval(t1)
+  t1 = null
+  t1 = setInterval(() => {
+    handleQueryOrderStatus()
+  }, 5000)
+}
+async function handleQueryOrderStatus() {
+  if (t1) {
+    let response = await getOrderInfo(orderSn.value)
+    orderDetal.value = response
+    if (response.payStatus === 0) {
+      //未支付
+    } else {
+      //支付失败
+      t1 = null
+      t1 && clearInterval(t1)
+      showConfirm({
+        text: response.autoPay
+          ? t('payInfo.autoPayTips', {paytype: response.payType == 5 ? 'PayPal' : 'Stripe'})
+          : response.payStatus === 2
+          ? t('payInfo.payfail')
+          : t('mall.paySuccessTip'),
+        type: response.payStatus === 2 ? 'err' : 'success',
+        callback: () => {
+          if (response.payStatus === 2) {
+            getCode()
+          } else {
+            window.close()
+          }
+        }
+      })
+    }
+  }
+}
+function getDetial() {
+  getOrderInfo(orderSn.value).then((res) => {
+    orderDetal.value = res
+    if (isEur) {
+      selectedPayType.value = orderDetal.value.payType == 6 ? 'stripe' : 'paypal'
+    }
+    getCode()
+  })
+}
+getDetial()
+function handlePay() {
+  if (isEur) {
+    //打开一个不被拦截的新窗口
+    // var win = window.open();
+    // win.document.body.innerHTML="loading......";
+    openPay({
+      orderSn: orderSn.value,
+      payType: paysidtype[selectedPayType.value],
+      productName: getRemark(orderDetal.value.goodsInfo, orderDetal.value.orderType, t),
+      openId: ''
+    }).then((res) => {
+      qrCodeUrl.value = res.qrCodeUrl || res.redirect
+      window.location.href = qrCodeUrl.value
+    })
+  } else {
+    window.open(qrCodeUrl.value)
+  }
+}
+async function getCode() {
+  inverRequest()
+  if (orderDetal.value.payType == 5 || orderDetal.value.payType == 6) return
+  qrCodeUrl.value = ''
+  openPay({
+    orderSn: orderSn.value,
+    payType: paysidtype[selectedPayType.value],
+    productName: getRemark(orderDetal.value.goodsInfo, orderDetal.value.orderType, t),
+    openId: ''
+  }).then((res) => {
+    qrCodeUrl.value = res.qrCodeUrl || res.redirect
+  })
+}
+</script>
+<template>
+  <div class="pcPage">
+    <div class="mall-pay">
+      <div class="container">
+        <div class="pay-header">
+          <!-- <div class="logo">
+            <img :src="language === 'zh' ? logoCn : logoEn" alt="" />
+          </div> -->
+          <p>{{ $t('mall.payCenter') }}</p>
+        </div>
+        <div class="pay-content">
+          <div class="account">
+            <span class="label">{{ $t('mall.nowAccount') }}:</span
+            ><span>{{ orderDetal.userName }}</span>
+          </div>
+          <div class="account">
+            <span class="label">
+              {{
+                $t(
+                  `mall.${
+                    orderDetal.orderType == 'incrementOrder'
+                      ? 'vip'
+                      : orderDetal.orderType == 'cooperationOrder'
+                      ? 'cooperationOrder'
+                      : orderDetal.orderType == 'downOrder'
+                      ? 'downdesc'
+                      : 'goqy'
+                  }`
+                )
+              }}:
+              <span
+                v-for="(item, index) in orderDetal.goodsInfo"
+                :key="index"
+                style="margin-right: 10px"
+              >
+                <span v-if="item">
+                  <span v-if="index != 0">、</span>
+
+                  <span style="margin-right: 5px" v-if="orderDetal.orderType == 'cooperationOrder'">
+                    {{ $t('mall.cooperationOrderName', { count: item.count }) }}
+                  </span>
+                  <span
+                    style="margin-right: 5px"
+                    v-else-if="orderDetal.orderType != 'incrementOrder'"
+                  >
+                    {{
+                      orderDetal.orderType == 'downOrder'
+                        ? `${item.name} (${item.sceneNum})`
+                        : language === 'zh'
+                        ? item.name
+                        : item.nameEn
+                    }}
+                  </span>
+                  <span style="margin-right: 5px" v-else>
+                    {{
+                      item.type == 1
+                        ? $t('payInfo.gjhy')
+                        : isEur
+                        ? $t('payInfo.zyhys')
+                        : $t('payInfo.zyhy')
+                    }}
+                  </span>
+                  <span
+                    v-if="
+                      orderDetal.orderType != 'downOrder' &&
+                      orderDetal.orderType != 'cooperationOrder'
+                    "
+                    >× {{ item.count }}</span
+                  >
+                </span>
+                <!-- <span v-else>1</span> -->
+                <span v-if="orderDetal.orderType == 'incrementOrder'"
+                  >,{{
+                    item.type == 1
+                      ? $t(`payInfo.expirationmon`, { num: item.monthQy || 1 })
+                      : $t(`payInfo.expiration`, { num: 1 })
+                  }}</span
+                >
+              </span></span
+            >
+          </div>
+          <div class="pay-types">
+            <p class="label">{{ $t('mall.payType') }}:</p>
+            <div
+              v-if="!isEur"
+              class="pay-type ali-pay"
+              @click="selectedPayType = 'alipay'"
+              :class="{ 'is-active': selectedPayType === 'alipay' }"
+            >
+              <img :src="`${$cdn}images/ali-pay.png`" class="t-icon" alt />
+              <h-icon type="vip_true" class="select-icon"></h-icon>
+              <img src="@/assets/images/vip_true.svg" class="t-click" alt />
+              {{ $t('mall.aliPay') }}
+            </div>
+            <div
+              v-if="!isEur"
+              class="pay-type wechat-pay"
+              @click="selectedPayType = 'wechatPay'"
+              :class="{ 'is-active': selectedPayType === 'wechatPay' }"
+            >
+              <img :src="`${$cdn}images/wechat-pay.png`" class="t-icon" alt />
+              <h-icon type="vip_true" class="select-icon"></h-icon>
+              <img src="@/assets/images/vip_true.svg" class="t-click" alt />
+              {{ $t('mall.wechatPay') }}
+            </div>
+            <div
+              v-if="isEur"
+              class="pay-type paypal-pay"
+              @click="selectedPayType = 'paypal'"
+              :class="{ 'is-active': selectedPayType === 'paypal' }"
+            >
+              <img
+                style="height: 26px; width: 100px"
+                src="@/assets/images/paypal.png"
+                class="t-icon"
+                alt
+              />
+              <h-icon type="vip_true" class="select-icon"></h-icon>
+              <img src="@/assets/images/vip_true.svg" class="t-click" alt />
+            </div>
+
+            <!-- <div
+              v-if="isEur"
+              class="pay-type paypal-pay"
+              @click="selectedPayType = 'stripe'"
+              :class="{ 'is-active': selectedPayType === 'stripe' }"
+            >
+              <img
+                style="height: 26px; width: 62px"
+                src="@/assets/images/stripe.png"
+                class="t-icon"
+                alt
+              />
+              <h-icon type="vip_true" class="select-icon"></h-icon>
+              <img src="@/assets/images/vip_true.svg" class="t-click" alt />
+            </div> -->
+          </div>
+          <div class="pay-info">
+            <p class="label">{{ $t('mall.payNum') }}:</p>
+            <p class="price">{{ isEur ? '$' : '¥' }}{{ orderDetal.orderMoney }}</p>
+            <div class="pay-qrcode" style="min-height: 220px" v-if="!isEur">
+              <QrcodeVue
+                v-if="qrCodeUrl"
+                :value="qrCodeUrl"
+                class="enter-x flex justify-center xl:justify-start"
+                :size="185"
+              />
+              <p>
+                {{
+                  $t('mall.payTip', {
+                    type: selectedPayType === 'alipay' ? $t('mall.aliPay') : $t('mall.wechat')
+                  })
+                }}
+              </p>
+            </div>
+            <div v-else class="payment" @click="handlePay">
+              {{ orderDetal.autoPay == 1 ? $t('payInfo.autoRenew') : $t('payInfo.payOrder') }}
+            </div>
+            <div v-show="!isEur" :class="`pay-scan-tip ${language === 'zh' ? '' : 'isEn'}`"></div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+
+<style lang="less" scoped>
+.mall-pay {
+  padding: 61px 0 61px;
+  background: #f7f7f7;
+  .container {
+    background: #fff;
+  }
+  .pay-header {
+    border-bottom: 1px solid #e4e4e4;
+    padding: 26px 0 21px 92px;
+    font-size: 24px;
+    line-height: 32px;
+    .logo,
+    p {
+      display: inline-block;
+      vertical-align: middle;
+    }
+    .logo {
+      margin-right: 52px;
+      position: relative;
+      &::after {
+        content: '';
+        display: block;
+        width: 1px;
+        height: 34px;
+        background: #c8c8c8;
+        position: absolute;
+        right: -30px;
+        top: 11px;
+      }
+    }
+  }
+  .pay-content {
+    padding: 0 92px 80px;
+  }
+  .account {
+    // height: 56px;
+    line-height: 56px;
+    font-weight: bold;
+    border-bottom: 1px solid #e4e4e4;
+  }
+  .label {
+    font-weight: bold;
+    margin-right: 13px;
+  }
+  .pay-types {
+    padding: 50px 0;
+    border-bottom: 1px solid #e4e4e4;
+    & > p,
+    & > div {
+      display: inline-block;
+    }
+    .pay-type {
+      width: 190px;
+      height: 40px;
+      line-height: 40px;
+      margin-right: 30px;
+      border: 1px solid #909090;
+      border-radius: 2px;
+      padding-left: 60px;
+      cursor: pointer;
+      position: relative;
+      &.is-active {
+        border-color: @primary-color;
+        &::after {
+          content: '';
+          display: block;
+          position: absolute;
+          right: 0;
+          bottom: 0;
+          border-bottom: 20px solid @primary-color;
+          border-left: 22px solid transparent;
+        }
+        .t-click {
+          display: block;
+          z-index: 1;
+        }
+      }
+      .select-icon {
+        color: #fff;
+        position: absolute;
+        bottom: 0;
+        right: 0;
+        z-index: 1;
+        line-height: 1;
+        font-size: 12px;
+      }
+      .t-icon {
+        width: 30px;
+        height: 30px;
+        padding: 6px;
+        left: 0;
+        position: absolute;
+      }
+      .t-click {
+        position: absolute;
+        right: 0;
+        bottom: 0;
+        display: none;
+        display: none;
+      }
+    }
+    .pay-icon {
+      position: absolute;
+      left: 4px;
+      top: 0;
+      height: 40px;
+      width: 34px;
+      display: inline-block;
+      background: url(~@/assets/images/refactor/mall/ali-icon.png) no-repeat center center;
+      background-size: 34px 34px;
+      &.wechat-icon {
+        background: url(~@/assets/images/refactor/mall/wechat-icon.png) no-repeat center center;
+        background-size: 28px 25px;
+      }
+    }
+  }
+  .pay-info {
+    padding-top: 24px;
+    position: relative;
+    & > p {
+      display: inline-block;
+      vertical-align: middle;
+    }
+    .price {
+      color: #ff0000;
+      font-size: 40px;
+      line-height: 50px;
+    }
+  }
+  .pay-qrcode {
+    padding: 60px 0 0 0px;
+    text-align: center;
+    img {
+      width: 190px;
+      height: 190px;
+      margin-bottom: 7px;
+      display: block;
+    }
+  }
+  .payment {
+    width: 216px;
+    height: 55px;
+    line-height: 55px;
+    background: #29b2ff;
+    margin: 60px auto 0 auto;
+    border-radius: 4px 4px 4px 4px;
+    opacity: 1;
+    text-align: center;
+    font-size: 16px;
+    font-family: PingFang SC-Regular, PingFang SC;
+    font-weight: 400;
+    color: #ffffff;
+    cursor: pointer;
+  }
+  .pay-scan-tip {
+    width: 186px;
+    height: 165.5px;
+    background: url(@/assets/images/scan-tip.png) no-repeat center center;
+    background-size: contain;
+    position: absolute;
+    top: 113px;
+    left: calc(50% + 40px + 92px);
+    &.isEn {
+      background-image: url(@/assets/images/scan-tip-en.png);
+    }
+  }
+}
+</style>

+ 27 - 0
src/views/pc/information/index.vue

@@ -0,0 +1,27 @@
+<script setup lang="ts">
+import { showConfirm } from '@/components/Toast'
+import { ref, computed, onMounted } from 'vue'
+import { useUserStore } from '@/stores/user'
+import { openPay, getOrderInfo, wxLogin } from '@/api/api'
+import { useRoute } from 'vue-router'
+import { GetRequest, getWeChatCode, getRemark } from '@/utils/index'
+import { useI18n } from 'vue-i18n'
+const route = useRoute()
+const { locale: language, t } = useI18n()
+const userStore = useUserStore();
+const isEur = userStore.isEur
+</script>
+<template>
+  <div class="mobilePage">mobilePage
+  </div>
+</template>
+
+<style lang="less" scoped>
+.mobilePage {
+  background: #f7f7f7;
+  min-height: calc(100vh - 50px);
+  max-width: 100vw;
+  display: block;
+  color:#202020;
+}
+</style>

+ 27 - 0
src/views/pc/order/index.vue

@@ -0,0 +1,27 @@
+<script setup lang="ts">
+import { showConfirm } from '@/components/Toast'
+import { ref, computed, onMounted } from 'vue'
+import { useUserStore } from '@/stores/user'
+import { openPay, getOrderInfo, wxLogin } from '@/api/api'
+import { useRoute } from 'vue-router'
+import { GetRequest, getWeChatCode, getRemark } from '@/utils/index'
+import { useI18n } from 'vue-i18n'
+const route = useRoute()
+const { locale: language, t } = useI18n()
+const userStore = useUserStore();
+const isEur = userStore.isEur
+</script>
+<template>
+  <div class="mobilePage">mobilePage
+  </div>
+</template>
+
+<style lang="less" scoped>
+.mobilePage {
+  background: #f7f7f7;
+  min-height: calc(100vh - 50px);
+  max-width: 100vw;
+  display: block;
+  color:#202020;
+}
+</style>

+ 27 - 0
src/views/pc/scene/index.vue

@@ -0,0 +1,27 @@
+<script setup lang="ts">
+import { showConfirm } from '@/components/Toast'
+import { ref, computed, onMounted } from 'vue'
+import { useUserStore } from '@/stores/user'
+import { openPay, getOrderInfo, wxLogin } from '@/api/api'
+import { useRoute } from 'vue-router'
+import { GetRequest, getWeChatCode, getRemark } from '@/utils/index'
+import { useI18n } from 'vue-i18n'
+const route = useRoute()
+const { locale: language, t } = useI18n()
+const userStore = useUserStore();
+const isEur = userStore.isEur
+</script>
+<template>
+  <div class="mobilePage">mobilePage
+  </div>
+</template>
+
+<style lang="less" scoped>
+.mobilePage {
+  background: #f7f7f7;
+  min-height: calc(100vh - 50px);
+  max-width: 100vw;
+  display: block;
+  color:#202020;
+}
+</style>

+ 36 - 0
tsconfig.json

@@ -0,0 +1,36 @@
+{
+  "compilerOptions": {
+    "target": "ESNext",
+    "useDefineForClassFields": true,
+    "module": "ESNext",
+    "moduleResolution": "Node",
+    "strict": true,
+    "jsx": "preserve",
+    "sourceMap": true,
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "esModuleInterop": true,
+    "lib": [
+      "ESNext",
+      "DOM"
+    ],
+    "skipLibCheck": true,
+    "baseUrl": "./",
+    "paths": {
+    "@/*":["src/*"]
+  }
+  },
+  "include": [
+    "src/**/*.ts",
+    "src/**/*.d.ts",
+    "src/**/*.tsx",
+    "src/**/*.vue",
+    "src/store/userInfo.js",
+    "src/theme/theme.ts"
+  ],
+  "references": [
+    {
+      "path": "./tsconfig.node.json"
+    }
+  ]
+}

+ 10 - 0
tsconfig.node.json

@@ -0,0 +1,10 @@
+{
+  "compilerOptions": {
+    "composite": true,
+    "module": "ESNext",
+    "moduleResolution": "Node",
+    "allowSyntheticDefaultImports": true
+  },
+  "include": ["vite.config.ts"],
+}
+

+ 59 - 0
vite.config.ts

@@ -0,0 +1,59 @@
+import { defineConfig } from 'vite'
+import AutoImport from 'unplugin-auto-import/vite'
+import Components from 'unplugin-vue-components/vite'
+import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
+import legacy from "@vitejs/plugin-legacy";
+import vue from '@vitejs/plugin-vue'
+import path from "path";
+
+// https://vitejs.dev/config/
+export default defineConfig({
+  plugins: [
+    vue(),    
+    // require('unplugin-element-plus/webpack')(),
+    // ...
+    AutoImport({
+      resolvers: [ElementPlusResolver()],
+    }),
+    Components({
+      resolvers: [ElementPlusResolver()],
+    }),
+    legacy({
+      polyfills: ["es.promise.finally", "es/map", "es/set"],
+      targets: ["chrome<60","and_uc 9"],
+      modernPolyfills: ["es.promise.finally"]
+    }),
+  ],
+  base: '/',
+  resolve: {
+    alias: {
+      "@": path.resolve(__dirname, "./src/"),
+      // "@": path.resolve(__dirname, "src"),
+    }
+  },
+  css: {
+    preprocessorOptions: {
+      less: {
+        modifyVars: {
+          hack: `true; @import (reference) "${path.resolve(__dirname, 'src/assets/config.less')}";`,
+        },
+        javascriptEnabled: true,
+      }
+    }
+  },
+  server: {
+    proxy: {
+      '/service': {
+        target: 'https://testeur.4dkankan.com/',
+        changeOrigin: true,
+        // rewrite: (path) => path.replace(/^\/api/, '')
+      },
+      '/ucenter': {
+        target: 'https://testeur.4dkankan.com/',
+        changeOrigin: true,
+        // rewrite: (path) => path.replace(/^\/api/, '')
+      }
+    }
+ },
+
+})

+ 0 - 4
vue.config.js

@@ -1,4 +0,0 @@
-const { defineConfig } = require('@vue/cli-service')
-module.exports = defineConfig({
-  transpileDependencies: true
-})

Datei-Diff unterdrückt, da er zu groß ist
+ 2743 - 5004
yarn.lock