pujiaming пре 2 година
родитељ
комит
3c45331dcd
65 измењених фајлова са 14436 додато и 0 уклоњено
  1. 7 0
      .env.development
  2. 6 0
      .env.production
  3. 5 0
      .env.production.kupai
  4. 282 0
      .eslintrc.cjs
  5. 24 0
      .gitignore
  6. 3 0
      .vscode/extensions.json
  7. 18 0
      README.md
  8. 46 0
      components.d.ts
  9. 29 0
      index.html
  10. 9129 0
      package-lock.json
  11. 39 0
      package.json
  12. BIN
      public/logo.ico
  13. 1 0
      public/vite.svg
  14. BIN
      public/youshe.ttf
  15. BIN
      public/zihunHeavy.ttf
  16. BIN
      public/zihunRegular.ttf
  17. 14 0
      src/App.vue
  18. 187 0
      src/api/index.ts
  19. BIN
      src/assets/img/default.png
  20. BIN
      src/assets/img/defaultPoster.png
  21. BIN
      src/assets/img/default_logo.png
  22. BIN
      src/assets/img/download.png
  23. BIN
      src/assets/img/gamer.png
  24. BIN
      src/assets/img/logo.png
  25. BIN
      src/assets/img/menu.png
  26. BIN
      src/assets/img/mobbg.png
  27. 3 0
      src/assets/img/pcbg.svg
  28. BIN
      src/assets/img/search.png
  29. BIN
      src/assets/img/search_circle.png
  30. 1 0
      src/assets/vue.svg
  31. 33 0
      src/common/request.ts
  32. 38 0
      src/components/HelloWorld.vue
  33. 285 0
      src/components/layout/header.vue
  34. 31 0
      src/components/layout/index.vue
  35. 15 0
      src/components/layout/main.vue
  36. 97 0
      src/components/vantList.vue
  37. 207 0
      src/global.scss
  38. 7 0
      src/images.d.ts
  39. 44 0
      src/main.ts
  40. 125 0
      src/router/index.ts
  41. 16 0
      src/store/index.ts
  42. 58 0
      src/store/search.ts
  43. 49 0
      src/store/user.ts
  44. 140 0
      src/style.css
  45. 10 0
      src/utils/Message.ts
  46. 20 0
      src/utils/bytesFormatter.ts
  47. 20 0
      src/utils/captcha.ts
  48. 4 0
      src/utils/imgResolve.ts
  49. 400 0
      src/utils/index.ts
  50. 23 0
      src/utils/local.ts
  51. 145 0
      src/utils/oss.ts
  52. 50 0
      src/utils/version.ts
  53. 339 0
      src/views/category.vue
  54. 127 0
      src/views/ecoin.vue
  55. 439 0
      src/views/home.vue
  56. 481 0
      src/views/login.vue
  57. 272 0
      src/views/mine.vue
  58. 127 0
      src/views/ncoin.vue
  59. 159 0
      src/views/order.vue
  60. 150 0
      src/views/search.vue
  61. 563 0
      src/views/settings.vue
  62. 24 0
      src/vite-env.d.ts
  63. 35 0
      tsconfig.json
  64. 10 0
      tsconfig.node.json
  65. 99 0
      vite.config.ts

+ 7 - 0
.env.development

@@ -0,0 +1,7 @@
+VITE_ENV = development,
+VITE_PROJECT_NAME = 朱雀游戏中心
+# VITE_PROJECT_NAME = 游戏中心
+# VITE_API_HOST = http://gm.coolpad.nkfzs.com/api/web
+VITE_API_HOST = http://gm.rose.nei.nkfzs.com/api/web
+VITE_LOGO: 'QingQue'
+# VITE_LOGO: 'KuPai'

+ 6 - 0
.env.production

@@ -0,0 +1,6 @@
+VITE_ENV = 'production',
+VITE_PROJECT_NAME = 朱雀游戏中心
+VITE_API_HOST = http://gm.nkfzs.com/api/web
+# VITE_API_HOST = http://gm.rose.nei.nkfzs.com/api/web
+VITE_LOGO: 'QingQue'
+# VITE_LOGO: 'KuPai'

+ 5 - 0
.env.production.kupai

@@ -0,0 +1,5 @@
+VITE_ENV = production,
+VITE_PROJECT_NAME = 游戏中心
+VITE_API_HOST = http://gm.coolpad.nkfzs.com/api/web
+# VITE_LOGO: 'QingQue'
+VITE_LOGO: 'KuPai'

+ 282 - 0
.eslintrc.cjs

@@ -0,0 +1,282 @@
+module.exports = {
+    "root":true,
+    "env": {
+        "browser": true,
+        "es2021": true
+    },
+    "extends": [
+        "eslint:recommended",
+        "plugin:vue/vue3-essential",
+        "plugin:@typescript-eslint/recommended"
+    ],
+    "overrides": [
+    ],
+    "parser": "vue-eslint-parser",
+    "parserOptions": {
+        "ecmaVersion": "latest",
+        "parser":"@typescript-eslint/parser",
+        "sourceType": "module"
+    },
+    "ignorePatterns":[".eslintrc.cjs"],
+    "plugins": [
+        "vue",
+        "@typescript-eslint"
+    ],
+    "rules": {
+        "for-direction": "error", // for 循环迭代条件是否正确
+        "vue/component-name-in-template-casing": ["error", "kebab-case", {
+        "registeredComponentsOnly": false,
+        "ignores": []
+        }],
+        "vue/no-use-v-if-with-v-for": ["error", {
+        "allowUsingIterationVar": false
+        }],
+        "vue/require-v-for-key": "error",
+        "vue/prop-name-casing": ["error", "camelCase"],
+        // getter 函数中出现 return 语句
+        "getter-return": "error",
+        // getter 函数中出现 return 语句
+        "no-async-promise-executor": "error",
+        // getter 函数中出现 return 语句
+        "no-compare-neg-zero": "error",
+        "vue/max-attributes-per-line": [2, {
+        singleline: 10,
+        multiline: {
+            max: 1
+        }
+        }],
+        "vue/v-bind-style": ["error", "shorthand"],
+        "vue/v-on-style": ["error", "shorthand"],
+        "vue/no-v-model-argument": "off",
+        "vue/no-multiple-template-root": "off",
+        "vue/singleline-html-element-content-newline": "off",
+        "vue/multiline-html-element-content-newline": "off",
+        "vue/component-definition-name-casing": ["error", "PascalCase"],
+        "vue/no-v-html": "off",
+        // 强制 getter 和 setter 在对象中成对出现
+        "accessor-pairs": 2,
+        // 强制箭头函数的箭头前后使用一致的空格
+        "arrow-spacing": [2, {
+        before: true,
+        after: true
+        }],
+        // 禁止或强制在代码块中开括号前和闭括号后有空格
+        "block-spacing": [2, "always"],
+        // 强制在代码块中使用一致的大括号风格
+        "brace-style": [2, "1tbs", {
+        allowSingleLine: true
+        }],
+        // 强制使用骆驼拼写法命名约定
+        camelcase: [0, {
+        properties: "always"
+        }],
+        // 要求或禁止末尾逗号
+        "comma-dangle": [2, "never"],
+        // 强制在逗号前后使用一致的空格
+        "comma-spacing": [2, {
+        before: false,
+        after: true
+        }],
+        // 强制使用一致的逗号风格
+        "comma-style": [2, "last"],
+        // 要求在构造函数中有 super() 的调用
+        "constructor-super": 2,
+        // 强制所有控制语句使用一致的括号风格
+        curly: [2, "multi-line"],
+        "dot-location": [2, "property"], // 强制在点号之前和之后一致的换行
+        "eol-last": 2, // 要求或禁止文件末尾存在空行
+        eqeqeq: ["error", "always", { null: "ignore" }], // 要求使用 === 和 !==
+        "generator-star-spacing": [2, {
+        before: true,
+        after: true
+        }], // 强制 generator 函数中 * 号周围使用一致的空格
+        "handle-callback-err": [2, "^(err|error)$"], // 要求回调函数中有容错处理
+        indent: [2, 2, {
+        SwitchCase: 1
+        }], // 强制使用一致的缩进
+        "jsx-quotes": [2, "prefer-single"], // 强制在 JSX 属性中一致地使用双引号或单引号
+        "key-spacing": [2, {
+        beforeColon: false,
+        afterColon: true
+        }], // 强制在对象字面量的属性中键和值之间使用一致的间距
+        "keyword-spacing": [2, {
+        before: true,
+        after: true
+        }], // 强制在关键字前后使用一致的空格
+        "new-cap": [2, {
+        newIsCap: true,
+        capIsNew: false
+        }], // 要求构造函数首字母大写
+        "new-parens": 2, // 强制或禁止调用无参构造函数时有圆括号
+        "no-array-constructor": 2, // 禁用 Array 构造函数
+        "no-caller": 2, // 禁用 arguments.caller 或 arguments.callee
+        "no-console": "off",
+        "no-class-assign": 2, // 禁止修改类声明的变量
+        "no-cond-assign": 2, // 禁止条件表达式中出现赋值操作符
+        "no-const-assign": 2,
+        "no-control-regex": 0, // 禁止在正则表达式中使用控制字符
+        "no-delete-var": 2, // 禁止删除变量
+        "no-dupe-args": 2, // 禁止 function 定义中出现重名参数
+        "no-dupe-class-members": 2, // 禁止类成员中出现重复的名称
+        "no-dupe-keys": 2, // 禁止对象字面量中出现重复的 key
+        "no-duplicate-case": 2, // 禁止出现重复的 case 标签
+        "no-empty-character-class": 2, // 禁止在正则表达式中使用空字符集
+        "no-empty-pattern": 2, // 禁止使用空解构模式
+        "no-eval": 2, // 禁用 eval()
+        "no-ex-assign": 2, // 禁止对 catch 子句的参数重新赋值
+        "no-extend-native": 2, // 禁止扩展原生类型
+        "no-extra-bind": 2, // 禁止不必要的 .bind() 调用
+        "no-extra-boolean-cast": 2, // 禁止不必要的布尔转换
+        "no-extra-parens": [2, "functions"], // 禁止不必要的括号
+        "no-fallthrough": 2, // 禁止 case 语句落空
+        "no-floating-decimal": 2, // 禁止数字字面量中使用前导和末尾小数点
+        "no-func-assign": 2, // 禁止对 function 声明重新赋值
+        "no-implied-eval": 2, // 禁止使用类似 eval() 的方法
+        "no-inner-declarations": [2, "functions"],
+        "no-invalid-regexp": 2, // 禁止在嵌套的块中出现变量声明或 function 声明
+        "no-irregular-whitespace": 2, // 禁止不规则的空白
+        "no-iterator": 2, // 禁用 __iterator__ 属性
+        "no-label-var": 2, // 不允许标签与变量同名
+        "no-labels": [2, {
+        allowLoop: false,
+        allowSwitch: false
+        }], // 禁用标签语句
+        "no-lone-blocks": 2, // 禁用不必要的嵌套块
+        "no-mixed-spaces-and-tabs": 2, // 禁止空格和 tab 的混合缩进
+        "no-multi-spaces": 2, // 禁止使用多个空格
+        "no-multi-str": 2, // 禁止使用多行字符串
+        "no-multiple-empty-lines": [2, {
+        max: 1
+        }], // 禁止出现多行空行
+        "no-global-assign": 2, // no-global-assign
+        "no-unsafe-negation": 2, // 禁止对关系运算符的左操作数使用否定操作符
+        "no-new-object": 2, // 禁用 Object 的构造函数
+        "no-new-require": 2, // 禁止调用 require 时使用 new 操作符
+        "no-new-symbol": 2, // 禁止 Symbolnew 操作符和 new 一起使用
+        "no-new-wrappers": 2, // 禁止对 String,Number 和 Boolean 使用 new 操作符
+        "no-obj-calls": 2, // 禁止把全局对象作为函数调用
+        "no-octal": 2, // 禁用八进制字面量
+        "no-octal-escape": 2, // 禁止在字符串中使用八进制转义序列
+        "no-path-concat": 2, // 禁止对 __dirname 和 __filename 进行字符串连接
+        "no-proto": 2, // 禁用 __proto__ 属性
+        "no-redeclare": 2, // 禁止多次声明同一变量
+        "no-regex-spaces": 2, // 禁止正则表达式字面量中出现多个空格
+        "no-return-assign": [2, "except-parens"], // 禁止在 return 语句中使用赋值语句
+        "no-self-assign": 2, // 禁止自我赋值
+        "no-self-compare": 2, // 禁止自身比较
+        "no-sequences": 2, // 禁用逗号操作符
+        "no-shadow-restricted-names": 2, // 禁止将标识符定义为受限的名字
+        "func-call-spacing": 2, // 要求或禁止在函数名和开括号之间有空格。
+        "no-sparse-arrays": 2, // 禁用稀疏数组
+        "no-this-before-super": 2, // 禁止在构造函数中,在调用 super() 之前使用 this 或 super
+        "no-throw-literal": 2, // 禁止抛出异常字面量
+        "no-trailing-spaces": 2, // 禁用行尾空格
+        "no-undef": 2, // 禁用未声明的变量,除非它们在 /*global */ 注释中被提到
+        "no-undef-init": 2, // 禁止将变量初始化为 undefined
+        "no-unexpected-multiline": 2, // 禁止出现令人困惑的多行表达式
+        "no-unmodified-loop-condition": 2, // 禁用一成不变的循环条件
+        "no-unneeded-ternary": [2, {
+        defaultAssignment: false
+        }], // 禁止可以在有更简单的可替代的表达式时使用三元操作符
+        "no-unreachable": 2, // 禁止在 return、throw、continue 和 break 语句之后出现不可达代码
+        "no-unsafe-finally": 2, // 禁止在 finally 语句块中出现控制流语句
+        "no-unused-vars": [2, {
+        vars: "all",
+        args: "none"
+        }], // 禁止出现未使用过的变量
+        "no-useless-call": 2, // 禁止不必要的 .call() 和 .apply()
+        "no-useless-computed-key": 2, // 禁止在对象中使用不必要的计算属性
+        "no-useless-constructor": 2, // 禁用不必要的构造函数
+        "no-useless-escape": 0, // 禁用不必要的转义字符
+        "no-whitespace-before-property": 2, // 禁止属性前有空白
+        "no-with": 2, // 禁用 with 语句
+        "one-var": [2, {
+        initialized: "never"
+        }], // 强制函数中的变量要么一起声明要么分开声明
+        "operator-linebreak": [2, "after", {
+        overrides: {
+            "?": "before",
+            ":": "before"
+        }
+        }], // 强制操作符使用一致的换行符
+        "padded-blocks": [2, "never"], // 要求或禁止块内填充
+        quotes: [2, "single", {
+        avoidEscape: true,
+        allowTemplateLiterals: true
+        }], // 强制使用一致的反勾号、双引号或单引号
+        semi: [2, "never"],
+        "semi-spacing": [2, {
+        before: false,
+        after: true
+        }], // 强制分号之前和之后使用一致的空格
+        "space-before-blocks": [2, "always"],
+        "space-before-function-paren": [2, "never"],
+        "space-in-parens": [2, "never"],
+        "space-infix-ops": 2,
+        "space-unary-ops": [2, {
+        words: true,
+        nonwords: false
+        }], // 强制在一元操作符前后使用一致的空格
+        "spaced-comment": [2, "always", {
+        markers: ["global", "globals", "eslint", "eslint-disable", "*package", "!", ","]
+        }], // 强制在注释中 // 或 /* 使用一致的空格
+        "template-curly-spacing": [2, "never"], // 要求或禁止模板字符串中的嵌入表达式周围空格的使用
+        "use-isnan": 2, // 要求使用 isNaN() 检查 NaN
+        "valid-typeof": 2,
+        "wrap-iife": [2, "any"],
+        "yield-star-spacing": [2, "both"],
+        yoda: [2, "never"], // 要求或禁止 “Yoda” 条件
+        "prefer-const": [
+        "error",
+        {
+            destructuring: "all",
+            ignoreReadBeforeAssign: false
+        }
+        ], // 要求使用 const 声明那些声明后不再被修改的变量
+        "no-debugger": process.env.NODE_ENV === "production" ? 2 : 0,
+        "object-curly-spacing": [2, "always", {
+        objectsInObjects: false
+        }], // 强制在大括号中使用一致的空格
+        "array-bracket-spacing": [2, "never"],
+        "default-case": "off", // 要求 switch 语句中有 default 分支
+        "no-plusplus": "off", // 禁用一元操作符 ++ 和 --
+        "guard-for-in": "off",
+        "max-len": "off", // 强制一行的最大长度
+        "no-param-reassign": "off", // 禁止对 function 的参数进行重新赋值
+        "import/no-unresolved": "off", // 确保导入指向一个可以解析的文件/模块
+        "import/extensions": "off", // 确保在导入路径中统一使用文件扩展名
+        "no-bitwise": "off", // 禁用按位运算符
+        "@typescript-eslint/no-explicit-any": "off",
+        "consistent-return": "off",
+        "array-callback-return": "off",
+        "import/no-extraneous-dependencies": "off",
+        "no-restricted-syntax": "off",
+        radix: "off", // 强制在 parseInt() 使用基数参数
+        "linebreak-style": ["error", "unix"], // 强制使用一致的换行风格
+        "import/prefer-default-export": "off", // 如果模块只输入一个名字,则倾向于默认输出
+        "prefer-destructuring": "off", // 优先使用数组和对象解构
+        "import/no-cycle": "off", // 禁止一个模块导入一个有依赖路径的模块回到自己身上
+        "arrow-parens": "off", // 要求箭头函数的参数使用圆括号
+        "no-continue": "off", // 禁用 continue 语句
+        "no-prototype-builtins": "off", // 禁止直接调用 Object.prototypes 的内置属性
+        "import/named": "off", // 确保命名导入与远程文件中的命名导出相对应
+        "object-curly-newline": "off", // 强制大括号内换行符的一致性
+        "func-names": "off", // 要求或禁止使用命名的 function 表达式
+        "no-unused-expressions": "off", // 禁止出现未使用过的表达式
+        "no-underscore-dangle": "off", // 禁止标识符中有悬空下划线
+        "no-nested-ternary": "off", // 禁用嵌套的三元表达式
+        "no-await-in-loop": "off", // 禁止在循环中出现 await
+        "@typescript-eslint/no-empty-function": "off", // 不允许空函数
+        "vue/html-indent": ["error", 2], // 在<template>中强制一致缩进
+        "vue/multi-word-component-names": "off", // 组件命名必须为多词
+        "vue/html-self-closing": ["error", {
+        "html": {
+            "void": "always",
+            "normal": "always",
+            "component": "always"
+        },
+        "svg": "always",
+        "math": "always"
+        }]
+    }
+}

+ 24 - 0
.gitignore

@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?

+ 3 - 0
.vscode/extensions.json

@@ -0,0 +1,3 @@
+{
+  "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
+}

+ 18 - 0
README.md

@@ -0,0 +1,18 @@
+# Vue 3 + TypeScript + Vite
+
+This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
+
+## Recommended IDE Setup
+
+- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
+
+## Type Support For `.vue` Imports in TS
+
+TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
+
+If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
+
+1. Disable the built-in TypeScript Extension
+   1. Run `Extensions: Show Built-in Extensions` from VSCode's command palette
+   2. Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
+2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.

+ 46 - 0
components.d.ts

@@ -0,0 +1,46 @@
+/* eslint-disable */
+/* prettier-ignore */
+// @ts-nocheck
+// Generated by unplugin-vue-components
+// Read more: https://github.com/vuejs/core/pull/3399
+export {}
+
+declare module 'vue' {
+  export interface GlobalComponents {
+    Header: typeof import('./src/components/layout/header.vue')['default']
+    HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
+    Layout: typeof import('./src/components/layout/index.vue')['default']
+    Main: typeof import('./src/components/layout/main.vue')['default']
+    RouterLink: typeof import('vue-router')['RouterLink']
+    RouterView: typeof import('vue-router')['RouterView']
+    VanButton: typeof import('vant/es')['Button']
+    VanCell: typeof import('vant/es')['Cell']
+    VanCellGroup: typeof import('vant/es')['CellGroup']
+    VanCollapse: typeof import('vant/es')['Collapse']
+    VanCollapseItem: typeof import('vant/es')['CollapseItem']
+    VanDialog: typeof import('vant/es')['Dialog']
+    VanDropdownItem: typeof import('vant/es')['DropdownItem']
+    VanDropdownMenu: typeof import('vant/es')['DropdownMenu']
+    VanEmpty: typeof import('vant/es')['Empty']
+    VanField: typeof import('vant/es')['Field']
+    VanForm: typeof import('vant/es')['Form']
+    VanGrid: typeof import('vant/es')['Grid']
+    VanGridItem: typeof import('vant/es')['GridItem']
+    VanIcon: typeof import('vant/es')['Icon']
+    VanImage: typeof import('vant/es')['Image']
+    VanList: typeof import('vant/es')['List']
+    VanNavBar: typeof import('vant/es')['NavBar']
+    VanNumberKeyboard: typeof import('vant/es')['NumberKeyboard']
+    VanPasswordInput: typeof import('vant/es')['PasswordInput']
+    VanPopup: typeof import('vant/es')['Popup']
+    VanPullRefresh: typeof import('vant/es')['PullRefresh']
+    VanSearch: typeof import('vant/es')['Search']
+    VanSidebar: typeof import('vant/es')['Sidebar']
+    VanSidebarItem: typeof import('vant/es')['SidebarItem']
+    VanSwipe: typeof import('vant/es')['Swipe']
+    VanSwipeItem: typeof import('vant/es')['SwipeItem']
+    VanTab: typeof import('vant/es')['Tab']
+    VanTabs: typeof import('vant/es')['Tabs']
+    VantList: typeof import('./src/components/vantList.vue')['default']
+  }
+}

+ 29 - 0
index.html

@@ -0,0 +1,29 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="icon" type="image/svg+xml" href="/logo.ico" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
+    <meta name="apple-mobile-web-app-capable" content="yes" />
+    <meta name="apple-mobile-web-app-status-bar-style” content=black" />
+    <meta name="apple-touch-fullscreen" content="yes">
+    <meta name="full-screen" content="true" />
+    <meta name="x5-fullscreen" content="true" />
+    <meta name="360-fullscreen" content="true" />
+    <meta name="google" value="notranslate">
+    <meta name="renderer" content="webkit|ie-stand">
+    <meta name="force-rendering" content="webkit"/>
+    <meta name="layoutmode" content="fitscreen">
+    <meta name="HandheldFriendly" content="true">
+    <meta name="Cache-Control" content="no-siteapp">
+    <meta name="format-detection" content="telephone=no, email=no" />
+    <meta http-equiv="Cache-Control" content="no-siteapp" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+    <title></title>
+  </head>
+  <body ontouchstart="">
+    <div id="app"></div>
+    <script type="module" src="/src/main.ts"></script>
+    <script src="http://cdn.bootcss.com/blueimp-md5/1.1.0/js/md5.js"></script>
+  </body>
+</html>

Разлика између датотеке није приказан због своје велике величине
+ 9129 - 0
package-lock.json


+ 39 - 0
package.json

@@ -0,0 +1,39 @@
+{
+  "name": "new-website",
+  "private": true,
+  "version": "0.0.0",
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "vue-tsc && vite build",
+    "preview": "vite preview"
+  },
+  "dependencies": {
+    "@element-plus/icons-vue": "^2.1.0",
+    "@vant/touch-emulator": "^1.4.0",
+    "autoprefixer": "^10.4.15",
+    "axios": "^1.4.0",
+    "element-plus": "^2.3.9",
+    "pinia": "^2.1.6",
+    "pinia-plugin-persistedstate": "^3.2.0",
+    "vant": "^4.6.5",
+    "vue": "^3.3.4",
+    "vue-router": "^4.2.4"
+  },
+  "devDependencies": {
+    "@types/node": "^20.5.0",
+    "@typescript-eslint/eslint-plugin": "^6.4.0",
+    "@typescript-eslint/parser": "^6.4.0",
+    "@vitejs/plugin-legacy": "^4.1.1",
+    "@vitejs/plugin-vue": "^4.2.3",
+    "eslint": "^8.47.0",
+    "eslint-plugin-vue": "^9.17.0",
+    "postcss-px-to-viewport-8-plugin": "^1.2.2",
+    "sass": "^1.65.1",
+    "typescript": "^5.0.2",
+    "unplugin-vue-components": "^0.25.1",
+    "vite": "^4.4.5",
+    "vue-eslint-parser": "^9.3.1",
+    "vue-tsc": "^1.8.5"
+  }
+}


Разлика између датотеке није приказан због своје велике величине
+ 1 - 0
public/vite.svg


BIN
public/youshe.ttf


BIN
public/zihunHeavy.ttf


BIN
public/zihunRegular.ttf


+ 14 - 0
src/App.vue

@@ -0,0 +1,14 @@
+<script setup lang="ts">
+</script>
+
+<template>
+  <router-view v-slot="{ Component, route }">
+    <transition :name="route.meta.transition || 'fade'">
+      <component :is="Component" />
+    </transition>
+  </router-view>
+</template>
+
+<style scoped>
+
+</style>

+ 187 - 0
src/api/index.ts

@@ -0,0 +1,187 @@
+// 引入工具函数
+import request from '@/common/request'
+
+// 热门游戏(有参数)
+export const getGameHot = (params:any) => {
+  return request({
+    url: '/game/hot',
+    method: 'get',
+    params
+  })
+}
+export const getIndexGameHot = () => {
+  return request({
+    url: '/game/hot',
+    method: 'get'
+  })
+}
+// 游戏推荐(有参数)
+export const getGameRecommand = (params:any) => {
+  return request({
+    url: '/game/recommand',
+    method: 'get',
+    params
+  })
+}
+export const getIndexGameRecommand = () => {
+  return request({
+    url: '/game/recommand',
+    method: 'get'
+  })
+}
+// 游戏列表
+export const getGameList = (params:any) => {
+  return request({
+    url: '/game/list',
+    method: 'get',
+    params
+  })
+}
+export const getGameLis = () => {
+  return request({
+    url: '/game/list',
+    method: 'get'
+  })
+}
+// 用户登录
+export const getUserLogin = (data:any) => {
+  return request({
+    url: '/user/login',
+    method: 'post',
+    data
+  })
+}
+// 我的游戏
+export const getMineGame = (params:any) => {
+  return request({
+    url: '/game/mine',
+    method: 'get',
+    params
+  })
+}
+// 游戏标签
+export const getGameTag = () => {
+  return request({
+    url: '/game/tag',
+    method: 'get'
+  })
+}
+// 游戏类型
+export const getGameType = () => {
+  return request({
+    url: '/game/type',
+    method: 'get'
+  })
+}
+// 验证码
+export const getCaptchaHttp = <T>(data:T) => {
+  return request({
+    url: '/user/captcha',
+    method: 'post',
+    data
+  })
+}
+// 验证码登录
+export const captchaLogin = <T>(data:T) => {
+  return request({
+    url: 'user/captcha_login',
+    method: 'post',
+    data
+  })
+}
+// 用户信息
+export const userProfile = <T>(params:T) => {
+  return request({
+    url: 'user/profile',
+    method: 'get',
+    params
+  })
+}
+// 更改密码
+export const resetPassword = <T>(data:T) => {
+  return request({
+    url: '/user/reset_password',
+    method: 'post',
+    data
+  })
+}
+// 更改密码
+export const setPassword = <T>(data:T) => {
+  return request({
+    url: '/user/set_password',
+    method: 'post',
+    data
+  })
+}
+// 用户名更改
+export const userAccount = <T>(data:T) => {
+  return request({
+    url: '/user/account',
+    method: 'post',
+    data
+  })
+}
+// 用户名更改
+export const getGiftHttp = <T>(params:T) => {
+  return request({
+    url: '/game/gift',
+    method: 'get',
+    params
+  })
+}
+export const receiveGiftHttp = <T>(data:T) => {
+  return request({
+    url: '/game/tera',
+    method: 'post',
+    data
+  })
+}
+export const userOrder = <T>(params:T) => {
+  return request({
+    url: '/user/order',
+    method: 'get',
+    params
+  })
+}
+export const userEcoin = <T>(params:T) => {
+  return request({
+    url: '/user/ecoin',
+    method: 'get',
+    params
+  })
+}
+export const userNcoin = <T>(params:T) => {
+  return request({
+    url: '/user/ncoin',
+    method: 'get',
+    params
+  })
+}
+export const unbindCaptcha = <T>(params?:T) => {
+  return request({
+    url: '/user/unbind_captcha',
+    method: 'get',
+    params
+  })
+}
+export const clearMobile = <T>(data:T) => {
+  return request({
+    url: '/user/clear_mobile',
+    method: 'post',
+    data
+  })
+}
+export const bindCaptcha = <T>(data:T) => {
+  return request({
+    url: '/user/bind_captcha',
+    method: 'post',
+    data
+  })
+}
+export const bindMobile = <T>(data:T) => {
+  return request({
+    url: '/user/bind_mobile',
+    method: 'post',
+    data
+  })
+}

BIN
src/assets/img/default.png


BIN
src/assets/img/defaultPoster.png


BIN
src/assets/img/default_logo.png


BIN
src/assets/img/download.png


BIN
src/assets/img/gamer.png


BIN
src/assets/img/logo.png


BIN
src/assets/img/menu.png


BIN
src/assets/img/mobbg.png


Разлика између датотеке није приказан због своје велике величине
+ 3 - 0
src/assets/img/pcbg.svg


BIN
src/assets/img/search.png


BIN
src/assets/img/search_circle.png


+ 1 - 0
src/assets/vue.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

+ 33 - 0
src/common/request.ts

@@ -0,0 +1,33 @@
+import router from '@/router/index'
+import axios from 'axios'
+
+const api = import.meta.env.VITE_API_HOST
+
+const instance = axios.create({
+  baseURL: api
+})
+
+// 请求拦截器
+instance.interceptors.request.use((config:any) => {
+  config.headers['Authorization'] = sessionStorage.getItem('token')
+  return config
+}, err => {
+  return Promise.reject(err)
+})
+
+// 响应拦截器
+instance.interceptors.response.use(response => {
+  return response
+}, err => {
+  if (err.response.status === 401) {
+    sessionStorage.removeItem('token')
+    // Message.error('登录状态已失效!请重新登录!')
+    setTimeout(() => {
+      router.push({ path: '/login' })
+    }, 1000)
+  }
+  return Promise.reject(err.response)
+})
+
+// 导出工具函数
+export default instance

+ 38 - 0
src/components/HelloWorld.vue

@@ -0,0 +1,38 @@
+<script setup lang="ts">
+import { ref } from 'vue'
+
+defineProps<{ msg: string }>()
+
+const count = ref(0)
+</script>
+
+<template>
+  <h1>{{ msg }}</h1>
+
+  <div class="card">
+    <button type="button" @click="count++">count is {{ count }}</button>
+    <p>
+      Edit
+      <code>components/HelloWorld.vue</code> to test HMR
+    </p>
+  </div>
+
+  <p>
+    Check out
+    <a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
+      >create-vue</a
+    >, the official Vue + Vite starter
+  </p>
+  <p>
+    Install
+    <a href="https://github.com/vuejs/language-tools" target="_blank">Volar</a>
+    in your IDE for a better DX
+  </p>
+  <p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
+</template>
+
+<style scoped>
+.read-the-docs {
+  color: #888;
+}
+</style>

+ 285 - 0
src/components/layout/header.vue

@@ -0,0 +1,285 @@
+<template>
+  <header id="header">
+    <div class="wrap">
+      <div class="left-part">
+        <img v-if="user.isQingQue" class="logo" src="http://gm.nkfzs.com/favicon.ico" alt="游戏中心"/>
+        <img v-else class="logo" :src="getAssetsFile('default_logo.png')" alt="游戏中心"/>
+        {{ user.projectName }}
+      </div>
+      <div class="menu">
+        <el-menu
+          :default-active="route.path"
+          class="el-menu-wrapper"
+          :ellipsis="false"
+          router
+          mode="horizontal"
+        >
+          <el-menu-item index="/home">首页</el-menu-item>
+          <el-menu-item index="/cate">分类</el-menu-item>
+          <el-menu-item index="/my_game">我的游戏</el-menu-item>
+        </el-menu>
+      </div>
+      <div class="search-wrap">
+        <el-autocomplete
+          v-model="search.params.keywords"
+          :fetch-suggestions="searchChange"
+          placeholder="请输入游戏名称"
+          :prefix-icon="Search"
+          clearable
+          @select="handleSelect"
+          @keyup.enter="handleKeyEnter"
+          maxlength="30"
+        />
+      </div>
+      <div class="mob-search" v-if="route.path === '/home' && platform.mobile">
+        <van-search shape='round' @click="router.push({ path: '/search' })" />
+      </div>
+      <div  class="mini-menu" v-if="platform.mobile && route.path !== '/home' ">
+        <img v-if="route.path !== '/search'" :src="getAssetsFile('search_circle.png')" alt="" @click="router.push({ path: '/search' })" />
+        <i class="verticle" />
+        <img :src="getAssetsFile('menu.png')" alt="" @click="show=true"/>
+      </div>
+      <div class="user">
+        <span v-if="!hasToken" class="el-dropdown-link">
+          <img :src="getAssetsFile('gamer.png')" @click="router.push('/login')" />
+        </span>
+        <el-dropdown v-else trigger="click" >
+          <span class="el-dropdown-link">
+            <img :src="getAssetsFile('gamer.png')" />
+          </span>
+          <template #dropdown>
+            <el-dropdown-menu>
+              <el-dropdown-item class="clearfix" @click="toSetting">
+                账号设置
+              </el-dropdown-item>
+              <el-dropdown-item class="clearfix" @click="logOut">
+                退出登录
+              </el-dropdown-item>
+            </el-dropdown-menu>
+          </template>
+        </el-dropdown>
+      </div>
+    </div>
+  </header>
+  <div class="header-hidden"/>
+  <van-popup
+    v-model:show="show"
+    position="top"
+    :style="{ width: '100%', height: 'auto' }"
+  >
+    <van-cell
+      class="menu_title"
+      v-for="(item, index) in user.menuPath"
+      :key="index"
+      :title="item.text"
+      @click="toPath(item.url)"/>
+    <van-cell title="账号设置" @click="toSetting"/>
+    <van-cell v-if="hasToken" title="退出登录" @click="logOut"/>
+  </van-popup>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, onMounted, watch, inject, computed } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
+import { useStore } from '@/store'
+import Message from '@/utils/Message'
+import local from '@/utils/local'
+import { getGameList } from '@/api/index'
+import { Search } from '@element-plus/icons-vue'
+import { getAssetsFile } from '@/utils/imgResolve'
+
+const img:any = inject('img')
+const router = useRouter()
+const route = useRoute()
+const { user } = useStore()
+const { search } = useStore()
+// showSuccessToast('成功文案')
+// showFailToast('失败文案')
+const show = ref(false)
+const game_name = ref(local.get('searchText') || '')
+// 绑定el-autocomplete组件
+const hasToken = ref(sessionStorage.getItem('token'))
+
+const currentIndex = ref(local.get('headerPath') || '') // 取保存的title
+console.log('currentIndex', currentIndex.value)
+
+if (route.query.account) {
+  currentIndex.value = '我的'
+  console.log('1234567', currentIndex.value)
+}
+
+// 顶部导航栏
+onMounted(() => {
+  const token = sessionStorage.getItem('token')
+  if (token) {
+    user.getUserProfile()
+  }
+})
+
+// 退出登录
+const logOut = () => {
+  // console.log(22);
+  localStorage.removeItem('token')
+  router.push({ path: 'p_login' })
+  hasToken.value = ''
+}
+const toSetting = () => {
+  router.push('/settings')
+}
+interface LinkItem {
+  id: string
+  value: string
+}
+const links: any = ref<LinkItem[]>([])
+
+const searchChange = async(str: string, cb: (arg: any) => void) => {
+  currentIndex.value = ''
+  local.remove('headerPath')
+  if (str && str.length > 0) {
+    game_name.value = str
+    try {
+      var params = reactive({
+        type: 0,
+        tag_id: 0,
+        page: 1,
+        pagesize: 10,
+        keywords: str
+      })
+      var { data } = await getGameList(params)
+      links.value = data.data.lists
+      links.value = links.value.map((item:any, index:any) => {
+        return {
+          id: `${index}`,
+          value: `${item.screen_name}`
+        }
+      })
+      links.value = links.value.filter((item:any) => {
+        return item.value.indexOf(str) > -1
+      })
+      cb(links.value)
+    } catch (error: any) {
+      // links.value.push({
+      //   id: '-1',
+      //   value: '暂无匹配数据'
+      // })
+      // cb(links.value)
+      console.log(error)
+      Message.error(error.data.msg)
+    }
+  } else {
+    cb([])
+  }
+}
+
+const handleSelect = (item: LinkItem) => {
+  if (item) {
+    const { value } = item
+    if (route.path !== 'search') {
+      router.push({ path: '/search' })
+    }
+    search.onSearch()
+    local.set('searchText', game_name.value)
+  }
+}
+
+// 手动回车跳转搜索页
+const handleKeyEnter = () => {
+  if (route.path !== 'search') {
+    router.push({ path: '/search' })
+  }
+  search.onSearch()
+  local.set('searchText', game_name.value)
+}
+
+watch(currentIndex, () => { }, { deep: true, immediate: true })
+const platform = computed(() => {
+  var u = navigator.userAgent
+  return {
+    mobile: !!u.match(/AppleWebKit.*Mobile.*/), // 是否为移动终端
+    ios: !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/), // ios终端
+    android: u.indexOf('Android') > -1 || u.indexOf('Adr') > -1, // android终端
+    iPhone: u.indexOf('iPhone') > -1, // 是否为iPhone或者QQHD浏览器
+    iPad: u.indexOf('iPad') > -1, // 是否iPad
+    webApp: u.indexOf('Safari') === -1, // 是否web应该程序,没有头部与底部
+    weixin: u.indexOf('MicroMessenger') > -1 // 是否微信
+  }
+})
+const toPath = (url: any) => {
+  router.push({ path: url })
+  show.value = false
+}
+</script>
+
+<style lang="scss" scoped>
+#header{
+    width: 100%;
+    max-width: 100vw;
+    background-color: #fff;
+    position: fixed;
+    z-index: 1000;
+    .wrap{
+        margin: 0 auto;
+        height: 100%;
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        box-sizing: border-box;
+        .left-part{
+            display: flex;
+            align-items: center;
+            font-family: 'zihunchaoji';
+            white-space: nowrap;
+        }
+        .menu{
+            padding: 0 5rem 0 1rem;
+            .el-menu--horizontal.el-menu{
+                border: 0;
+                >.el-menu-item.is-active{
+                    border: 0;
+                }
+            }
+            .el-menu-item{
+              border: 0;
+            }
+        }
+        .search-wrap{
+          width: 8.5rem;
+          :deep(.el-input__wrapper){
+              border-radius: 1rem;
+          }
+        }
+        .user{
+          cursor: pointer;
+          .el-dropdown-link{
+            overflow: hidden;
+            width: 2rem;
+            height: 2rem;
+            border-radius: 50%;
+            >img{
+              width: 2rem;
+              height: 2rem;
+              display: block;
+            }
+          }
+
+        }
+        .mini-menu{
+          display: flex;
+          align-items: center;
+          >img{
+            width: 25px;
+            height: 25px;
+          }
+          .verticle{
+            width: 1px;
+            height: 30px;
+            background-color: #ddd;
+            margin: 0 10px;
+          }
+        }
+    }
+}
+.header-hidden{
+  height: 3rem;
+}
+</style>

+ 31 - 0
src/components/layout/index.vue

@@ -0,0 +1,31 @@
+<template>
+  <div>
+    <header-vue/>
+    <main-vue/>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, onMounted, computed, inject } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
+import { useStore } from '@/store'
+import headerVue from './header.vue'
+import mainVue from './main.vue'
+import { showSuccessToast, showFailToast } from 'vant'
+import Message from '@/utils/Message'
+
+const img:any = inject('img')
+const router = useRouter()
+const route = useRoute()
+const { user } = useStore()
+// showSuccessToast('成功文案')
+// showFailToast('失败文案')
+onMounted(() => {
+
+})
+
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 15 - 0
src/components/layout/main.vue

@@ -0,0 +1,15 @@
+<template>
+  <div id="main">
+    <div class="main-wrap">
+      <router-view/>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 97 - 0
src/components/vantList.vue

@@ -0,0 +1,97 @@
+<template>
+  <div v-if="list.length||loading">
+    <van-pull-refresh v-model="refreshing" success-text="刷新成功" @refresh="onRefresh">
+      <van-list
+        v-model:loading="loading"
+        v-model:error="error"
+        :finished="finished"
+        finished-text="没有更多了"
+        :immediate-check="false"
+        error-text="请求失败,点击重新加载"
+        @load="onLoad"
+      >
+        <slot v-for="item in list" :key="item" name="listItem" :row="item" />
+      </van-list>
+    </van-pull-refresh>
+  </div>
+  <van-empty v-else image="search" description="没有数据" />
+</template>
+
+<script setup lang="ts">
+import { Notify } from 'vant'
+import { ref } from 'vue'
+const list = ref<any[]>([])
+const refreshing = ref(false)
+const loading = ref(false)
+const error = ref(false)
+const finished = ref(false)
+const total = ref(0)
+// eslint-disable-next-line no-undef
+const props = defineProps<{
+    param:any,
+    pagination:any,
+    request:(params:any)=>any
+}>()
+// eslint-disable-next-line no-undef
+const emit = defineEmits(['resetPage', 'pagePlus', 'noData'])
+const paginationCopy = ref()
+const paramCopy = ref()
+// paginationCopy.value = Object.assign({}, (props.pagination ? props.pagination : { page: 1, per_page: 15 }))
+
+const onLoad = async(flag?:any, pa?:any) => {
+  loading.value = true
+  // 是否从第1页加载
+  if (flag) {
+    emit('resetPage')
+    list.value = []
+    finished.value = false
+  }
+  paginationCopy.value = props.pagination ? props.pagination : { page: 1, per_page: 15 }
+  // console.log(props.param)
+
+  paramCopy.value = pa || { ...(props.param ? props.param : {}) }
+  await props.request({ ...paramCopy.value, ...paginationCopy.value }).then(res => {
+    // console.log('列表res', res.data)
+    if (!res.data.total) {
+      finished.value = true
+    }
+    total.value = res.data.total || 0
+    list.value = list.value.concat(res.data.data ? res.data.data : [])
+    // list.value = [...list.value, ...(res.data.data ? res.data.data : [])]
+    console.log('list', list.value)
+    if (!list.value.length) emit('noData')
+  }).catch(err => {
+    console.log(err)
+    Notify({ type: 'danger', message: err.data })
+    finished.value = true
+    error.value = true
+  })
+  loading.value = false
+  const pageAttr = Object.keys(paginationCopy.value)
+  if (paginationCopy.value[pageAttr[0]] * paginationCopy.value[pageAttr[1]] > total.value) {
+    finished.value = true
+  } else {
+    emit('pagePlus')
+  }
+}
+
+const onRefresh = async() => {
+  // 清空列表数据
+  finished.value = false
+
+  // 重新加载数据
+  // 将 loading 设置为 true,表示处于加载状态
+  loading.value = true
+  await onLoad(1)
+  refreshing.value = false
+}
+// eslint-disable-next-line no-undef
+defineExpose({
+  onLoad,
+  list
+})
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 207 - 0
src/global.scss

@@ -0,0 +1,207 @@
+@media screen and (max-width: 1920px) {
+    #header{
+        height: 3rem;
+        .wrap{
+            width: 55rem;
+            .mini-menu{
+                display: none!important;
+            }
+        }
+    }
+    #main{
+        background-color: #f7f7f7;
+        .main-wrap{
+            width: 55rem;
+            margin: 0 auto;
+            margin-top: .5rem;
+            overflow: hidden;
+            display: flex;
+            justify-content: space-between;
+            box-sizing: border-box;
+            .cate-list{
+                
+                .hot-item{
+                    .game_tag{
+                        margin-right: 0.5rem;
+                        font-size: .6rem;
+                    }
+                }
+            }
+        }
+    }
+   
+}
+@media screen and (max-width: 1680px) {
+    #header{
+        font-size: 1rem;
+    }
+    
+}
+@media screen and (max-width: 1440px) {
+    #header{
+        font-size: 1rem;
+    }
+    
+}
+@media screen and (max-width: 1366px) {
+    #header{
+        font-size: 1rem;
+    }
+    
+    
+}
+@media screen and (max-width: 1250px){
+    #header{
+        font-size: 1rem;
+    }
+    
+}
+
+@media screen and (max-width:1024px) {
+    #header{
+        font-size: 1rem;
+    }
+}
+@media screen and (max-width:750px) {
+    #header{
+        height: 60px;
+        background-color: #eaedfc;
+        .wrap{
+            padding:0 10px;
+            width: 100%;
+            .left-part{
+                font-size: 22px;
+            }
+            .menu{
+                display: none;
+            }
+            .search-wrap{
+                // width: 170px!important;
+                // :deep(.el-input__wrapper){
+                //     border-radius: 20px;
+                // }
+                display: none;
+            }
+            .mini-menu{
+                display: block!important;
+                padding-right: 15px;
+            }
+            .user{
+                display: none;
+            }
+            
+        }
+    }
+    .header-hidden{
+        height: 60px!important;
+    }
+    #main{
+        .main-wrap{
+            width: 100%;
+            margin: 0;
+            padding: 0;
+            display: block;
+            .left-list, .right-action{
+                display: none;
+            }
+            .mob-page{
+                display: block!important;
+            }
+            .cate-list{
+                .cate-tag, .divide{
+                    display: none;
+                }
+                .hot-item{
+                    height: 120px;
+                    .game-logo{
+                        display: none;
+                    }
+                    flex-direction: row-reverse;
+                    justify-content: flex-end;
+                    --van-button-small-padding:15px;
+                    .game-poster{
+                        width: 133px;
+                        height: 75px;
+                        border-radius: var(--logo-radius);
+                        margin-right: 5px;
+                    }
+                    margin-bottom: 0;
+                    .down-btn{
+                        display: block!important;
+                    }
+                    .detail{
+                        width: 150px;
+                        
+                    }
+                    .game_tag{
+                        margin-right: 2px;
+                        font-size: 10px;
+                    }
+                }
+            }
+            .bg{
+                display: none;
+            }
+            .mob-login{
+                display: flex!important;
+            }
+            .rest-list{
+                display: none;
+            }
+            .mob-list{
+                display: block!important;
+                min-height: calc(100vh - 60px)
+            }
+            .settings{
+                display: none;
+            }
+            .mob-setting{
+                display: block!important;
+            }
+            .mine{
+                .pcbt, .game-poster{
+                    display: none;
+                }
+                .right{
+                    display: flex!important;
+                }
+            }
+            .search_cont{
+                .search_key{
+                    display: block!important;
+                }
+                .pcbt, .game-poster{
+                    display: none;
+                }
+                .right{
+                    display: flex!important;
+                }
+                .hot-item{
+                    height: 96px;
+                    width: 100%;
+                    box-sizing: border-box;
+                    .game-logo{
+                        margin-right: 10px;
+                    }
+                    .detail{
+                        width: 170px;
+                    }
+                }
+            }
+            
+        }
+    }
+}
+@media screen and (max-width:350px) {
+    #header{
+        height: 60px;
+        box-sizing: border-box;
+        overflow: hidden;
+        .wrap{
+            width: 100%;
+        }
+    }
+    .header-hidden{
+        height: 60px!important;
+    }
+}

+ 7 - 0
src/images.d.ts

@@ -0,0 +1,7 @@
+declare module '*.svg'
+declare module '*.png'
+declare module '*.jpg'
+declare module '*.jpeg'
+declare module '*.gif'
+declare module '*.bmp'
+declare module '*.tiff'

+ 44 - 0
src/main.ts

@@ -0,0 +1,44 @@
+import { createApp } from 'vue'
+import './style.css'
+import './global.scss'
+import App from './App.vue'
+import { getAssetsFile } from '@/utils/imgResolve'
+import router from './router'
+import store from './store/index'
+import ElementPlus from 'element-plus'
+import 'element-plus/dist/index.css'
+import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
+import 'vant/lib/index.css'
+import '@vant/touch-emulator'
+import * as ElementPlusIconsVue from '@element-plus/icons-vue'
+
+const setHtmlFontSize = () => {
+  //   store.commit('screenChange');
+  const htmlDom = document.getElementsByTagName('html')[0]
+  let htmlWidth = document.documentElement.clientWidth || document.body.clientWidth
+  console.log(htmlWidth)
+  if (htmlWidth >= 750) {
+    htmlWidth = 750
+  }
+  if (htmlWidth <= 450) {
+    htmlWidth = 450 // 12px
+  }
+  htmlDom.style.fontSize = `${htmlWidth / 37.5}px`
+}
+
+window.onresize = setHtmlFontSize
+setHtmlFontSize()
+
+const app = createApp(App)
+
+app.provide('img', getAssetsFile)
+app.use(store)
+app.use(router)
+for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
+  app.component(key, component)
+}
+app.use(ElementPlus, {
+  locale: zhCn,
+  size: 'large', zIndex: 3000
+})
+app.mount('#app')

+ 125 - 0
src/router/index.ts

@@ -0,0 +1,125 @@
+import { createRouter, RouteRecordRaw, createWebHashHistory } from 'vue-router'
+import { getVersionFR } from '@/utils/version'
+import layout from '@/components/layout/index.vue'
+declare module 'vue-router' {
+  interface RouteMeta {
+    title?: string,
+    transition?:any
+  }
+}
+const routes: Array<RouteRecordRaw> = [
+  {
+    // 路由初始指向
+    path: '/',
+    name: 'redirect',
+    component: layout,
+    meta: {
+      transition: 'van-fade'
+    },
+    redirect: 'home',
+    children: [
+      {
+        path: 'home',
+        name: 'home',
+        component: () => import('@/views/home.vue'),
+        meta: {
+          transition: 'van-fade'
+        }
+      },
+      {
+        path: 'cate',
+        name: 'cate',
+        component: () => import('@/views/category.vue'),
+        meta: {
+          transition: 'van-fade'
+        }
+      },
+      {
+        path: 'search',
+        name: 'search',
+        component: () => import('@/views/search.vue'),
+        meta: {
+          transition: 'van-fade'
+        }
+      },
+      {
+        path: 'settings',
+        name: 'settings',
+        component: () => import('@/views/settings.vue'),
+        meta: {
+          transition: 'van-fade'
+        }
+      },
+      {
+        path: 'my_game',
+        name: 'my_game',
+        component: () => import('@/views/mine.vue'),
+        meta: {
+          transition: 'van-fade'
+        }
+      },
+      {
+        path: 'order',
+        name: 'order',
+        component: () => import('@/views/order.vue'),
+        meta: {
+          transition: 'van-fade'
+        }
+      },
+      {
+        path: 'login',
+        name: 'login',
+        component: () => import('@/views/login.vue'),
+        meta: {
+          transition: 'van-fade'
+        }
+      },
+      {
+        path: 'coin_type1',
+        name: 'coin_type1',
+        component: () => import('@/views/ncoin.vue'),
+        meta: {
+          transition: 'van-fade'
+        }
+      },
+      {
+        path: 'coin_type2',
+        name: 'coin_type2',
+        component: () => import('@/views/ecoin.vue'),
+        meta: {
+          transition: 'van-fade'
+        }
+      }
+    ]
+  }
+]
+const router = createRouter({
+  // createWebHashHistory hash 路由
+  // createWebHistory history 路由
+  // createMemoryHistory 带缓存 history 路由
+  history: createWebHashHistory(),
+  routes,
+  scrollBehavior() {
+    // 始终滚动到顶部
+    return { top: 0 }
+  }
+})
+
+router.beforeEach((to, from, next) => {
+  // ...
+  // 返回 false 以取消导航
+  // return false
+
+  // const token = sessionStorage.getItem('token')
+  // if (!token && to.path !== '/login') {
+  //   next('/login')
+  // } else {
+  //   next()
+  // }
+  next()
+})
+router.afterEach(() => {
+  getVersionFR()
+})
+
+export default router

+ 16 - 0
src/store/index.ts

@@ -0,0 +1,16 @@
+import { createPinia } from 'pinia'
+import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
+import { user } from './user'
+import { search } from './search'
+
+const store = createPinia()
+store.use(piniaPluginPersistedstate)
+
+export default store
+
+export const useStore = () => {
+  return {
+    user: user(),
+    search: search()
+  }
+}

+ 58 - 0
src/store/search.ts

@@ -0,0 +1,58 @@
+import { defineStore } from 'pinia'
+import { getGameList } from '@/api/index'
+import Message from '@/utils/Message'
+
+interface Params {
+    keywords:string
+    pagesize:number
+    page:number
+    tag_id:number
+    type:number
+  }
+export const search = defineStore('search', {
+  // 推荐使用 完整类型推断的箭头函数
+  // persist: true, // 持久化储存
+  state: () => {
+    return {
+      // 所有这些属性都将自动推断其类型
+      params: {
+        keywords: '',
+        pagesize: 10,
+        page: 1,
+        tag_id: 0,
+        type: 0
+      } as Params,
+      prefix: '',
+      total: 0,
+      searchList: [] as any[]
+    }
+  },
+  getters: {
+
+  },
+  actions: {
+    loadMore() {
+      this.params.page += 1
+      this.getData()
+    },
+    async onSearch() {
+      this.params.page = 1
+      this.searchList = []
+      await this.getData()
+    },
+    async getData() {
+      // from 2 手机登录   1密码登录
+      await getGameList(this.params).then((res) => {
+        if (res.data.code === 200 && res.data.data) {
+          console.log('res', res)
+          this.prefix = res.data.data.prefix
+          this.total = res.data.data.total
+          this.searchList = this.searchList.concat(res.data.data.lists || [])
+        }
+      }).catch((error) => {
+        console.log(error)
+        Message.error(error.data.msg)
+      })
+    }
+  }
+})

+ 49 - 0
src/store/user.ts

@@ -0,0 +1,49 @@
+import { defineStore } from 'pinia'
+import { userProfile } from '@/api/index'
+
+export const user = defineStore('user', {
+  // 推荐使用 完整类型推断的箭头函数
+  // persist: true, // 持久化储存
+  state: () => {
+    return {
+      // 所有这些属性都将自动推断其类型
+      isQingQue: import.meta.env.VITE_LOGO === 'QingQue',
+      projectName: import.meta.env.VITE_PROJECT_NAME,
+      keywords: '',
+      profile: {
+        ecoinName: '',
+        ecoin: 0
+      } as any,
+      menuPath: [
+        { text: '首页', value: 1, url: '/home' },
+        { text: '分类', value: 2, url: '/cate' },
+        { text: '我的游戏', value: 3, url: '/my_game' }
+      ]
+    }
+  },
+  getters: {
+
+  },
+  actions: {
+    setProfile(info:any) {
+      this.profile = info
+    },
+    async getUserProfile() {
+      // from 2 手机登录   1密码登录
+      await userProfile({ from: parseInt(sessionStorage.getItem('from') as string) }).then((res) => {
+        console.log(res)
+        this.setProfile(res.data.data)
+        this.menuPath = [
+          { text: '首页', value: 1, url: '/home' },
+          { text: '分类', value: 2, url: '/cate' },
+          { text: '订单', value: 3, url: '/order' },
+          { text: this.profile.ecoinName + ':' + (this.profile.ecoin / 100).toFixed(2), value: 5, url: '/coin_type2' },
+          { text: this.profile.ncoinName + ':' + (this.profile.ncoin / 100).toFixed(2), value: 6, url: '/coin_type1' },
+          { text: '我的游戏', value: 4, url: '/my_game' }
+        ]
+      }).catch((error) => {
+        console.log(error)
+      })
+    }
+  }
+})

+ 140 - 0
src/style.css

@@ -0,0 +1,140 @@
+
+/* reset */
+html, body, div, span, applet, object, iframe,
+h1, h2, h3, h4, h5, h6, p, blockquote, pre,
+a, abbr, acronym, address, big, cite, code,
+del, dfn, em, img, ins, kbd, q, s, samp,
+small, strike, strong, sub, sup, tt, var,
+b, u, i, center,
+dl, dt, dd, ol, ul, li,
+fieldset, form, label, legend,
+table, caption, tbody, tfoot, thead, tr, th, td,
+article, aside, canvas, details, embed, 
+figure, figcaption, footer, header, hgroup, 
+menu, nav, output, ruby, section, summary,
+time, mark, audio, video {
+	margin: 0;
+	padding: 0;
+	border: 0;
+	font-size: 100%;
+	font: inherit;
+	vertical-align: baseline;
+}
+/* HTML5 display-role reset for older browsers */
+article, aside, details, figcaption, figure, 
+footer, header, hgroup, menu, nav, section {
+	display: block;
+}
+body {
+	line-height: 1;
+}
+ol, ul {
+	list-style: none;
+}
+blockquote, q {
+	quotes: none;
+}
+blockquote:before, blockquote:after,
+q:before, q:after {
+	content: '';
+	content: none;
+}
+table {
+	border-collapse: collapse;
+	border-spacing: 0;
+}
+/*谷歌*/
+input::-webkit-outer-spin-button,
+input::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+}
+/*火狐*/
+input[type="number"] {
+ -moz-appearance: textfield;
+}
+*{
+  user-select: none;
+}
+/* ------------------------------------------------------------------------- */
+@font-face {
+  font-family: 'zihunchaoji';
+  src: url('/zihunRegular.ttf') format('truetype');
+}
+@font-face {
+  font-family: 'youshe';
+  src: url('/youshe.ttf') format('truetype');
+}
+:root {
+  font-family:  Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
+  line-height: 1.5;
+  font-weight: 400;
+
+  color-scheme: light dark;
+  /* color: rgba(255, 255, 255, 0.87); */
+  background-color: #f7f7f7;
+
+  font-synthesis: none;
+  text-rendering: optimizeLegibility;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+  -webkit-text-size-adjust: 100%;
+  --logo-radius:5px;
+}
+:root:root{
+  --van-grid-item-content-background:transparent;
+  /* --van-button-small-padding:15px; */
+}
+
+
+h1 {
+  font-size: 3.2em;
+  line-height: 1.1;
+}
+
+/* button {
+  border-radius: 8px;
+  border: 1px solid transparent;
+  padding: 0.6em 1.2em;
+  font-size: 1em;
+  font-weight: 500;
+  font-family: inherit;
+  background-color: #1a1a1a;
+  cursor: pointer;
+  transition: border-color 0.25s;
+}
+button:hover {
+  border-color: #646cff;
+}
+button:focus,
+button:focus-visible {
+  outline: 4px auto -webkit-focus-ring-color;
+} */
+
+
+#app {
+  /* max-width: 1280px; */
+  /* margin: 0 auto; */
+  /* padding: 2rem; */
+  /* text-align: center; */
+  margin: 0;
+  padding: 0;
+}
+
+@media (prefers-color-scheme: light) {
+  :root {
+    color: #213547;
+    background-color: #ffffff;
+  }
+  a:hover {
+    color: #747bff;
+  }
+  button {
+    background-color: #f9f9f9;
+  }
+}
+
+.ellip{
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}

+ 10 - 0
src/utils/Message.ts

@@ -0,0 +1,10 @@
+import { ElMessage } from 'element-plus'
+
+export default ({
+  success(msgInfo: any) {
+    return ElMessage.success({ message: msgInfo, ...{ duration: 1800, grouping: true, center: true }})
+  },
+  error(msgInfo: any) {
+    return ElMessage.error({ message: msgInfo, ...{ duration: 1800, grouping: true, center: true }})
+  }
+})

+ 20 - 0
src/utils/bytesFormatter.ts

@@ -0,0 +1,20 @@
+export const bytesChange = (limit:number) => {
+  let size = ''
+  if (limit < 1 * 1024) { // 小于1KB,则转化成B
+    size = limit.toFixed(2) + 'B'
+  } else if (limit < 1 * 1024 * 1024) { // 小于1MB,则转化成KB
+    size = (limit / 1024).toFixed(2) + 'KB'
+  } else if (limit < 1 * 1024 * 1024 * 1024) { // 小于1GB,则转化成MB
+    size = (limit / (1024 * 1024)).toFixed(2) + 'MB'
+  } else { // 其他转化成GB
+    size = (limit / (1024 * 1024 * 1024)).toFixed(2) + 'GB'
+  }
+
+  const sizeStr = size + '' // 转成字符串
+  const index = sizeStr.indexOf('.') // 获取小数点处的索引
+  const dou = sizeStr.substr(index + 1, 2) // 获取小数点后两位的值
+  if (dou == '00') { // 判断后两位是否为00,如果是则删除00
+    return sizeStr.substring(0, index) + sizeStr.substr(index + 3, 2)
+  }
+  return size
+}

+ 20 - 0
src/utils/captcha.ts

@@ -0,0 +1,20 @@
+const checkRestTime = (sessionItem:string, validTime:number = 60) => {
+  let LASTTIME:number, restTime:number
+
+  const hasRecord = sessionStorage.getItem(sessionItem)
+  const nowTime = Math.round(new Date().getTime() / 1000)
+  if (hasRecord) {
+    LASTTIME = Math.round(Number(hasRecord))
+  } else {
+    return null
+  }
+
+  if (LASTTIME && nowTime - LASTTIME < validTime) {
+    restTime = validTime - (nowTime - LASTTIME)
+    return restTime
+  } else if (LASTTIME && nowTime - LASTTIME >= 60) {
+    sessionStorage.removeItem(sessionItem)
+    return null
+  }
+}
+export default checkRestTime

+ 4 - 0
src/utils/imgResolve.ts

@@ -0,0 +1,4 @@
+// 获取static文件夹静态资源
+export function getAssetsFile(url: string) {
+  return new URL(`/src/assets/img/${url}`, import.meta.url).href
+}

+ 400 - 0
src/utils/index.ts

@@ -0,0 +1,400 @@
+/**
+ * Created by PanJiaChen on 16/11/18.
+ */
+
+/**
+ * Parse the time to string
+ * @param {(Object|string|number)} time
+ * @param {string} cFormat
+ * @returns {string | null}
+ */
+export function parseTime(time:any, cFormat:any) {
+  if (arguments.length === 0 || !time) {
+    return null
+  }
+  const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}'
+  let date
+  if (typeof time === 'object') {
+    date = time
+  } else {
+    if ((typeof time === 'string')) {
+      if ((/^[0-9]+$/.test(time))) {
+        // support "1548221490638"
+        time = parseInt(time)
+      } else {
+        // support safari
+        // https://stackoverflow.com/questions/4310953/invalid-date-in-safari
+        time = time.replace(new RegExp(/-/gm), '/')
+      }
+    }
+
+    if ((typeof time === 'number') && (time.toString().length === 10)) {
+      time = time * 1000
+    }
+    date = new Date(time)
+  }
+  const formatObj:any = {
+    y: date.getFullYear(),
+    m: date.getMonth() + 1,
+    d: date.getDate(),
+    h: date.getHours(),
+    i: date.getMinutes(),
+    s: date.getSeconds(),
+    a: date.getDay()
+  }
+  const time_str = format.replace(/{([ymdhisa])+}/g, (result:any, key:any) => {
+    const value = formatObj[key]
+    // Note: getDay() returns 0 on Sunday
+    if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value ] }
+    return value.toString().padStart(2, '0')
+  })
+  return time_str
+}
+
+/**
+ * @param {number} time
+ * @param {string} option
+ * @returns {string}
+ */
+export function formatTime(time:any, option:any) {
+  if (('' + time).length === 10) {
+    time = parseInt(time) * 1000
+  } else {
+    time = +time
+  }
+  const d = new Date(time)
+  const now = Date.now()
+
+  const diff = (now - d.getTime()) / 1000
+
+  if (diff < 30) {
+    return '刚刚'
+  } else if (diff < 3600) {
+    // less 1 hour
+    return Math.ceil(diff / 60) + '分钟前'
+  } else if (diff < 3600 * 24) {
+    return Math.ceil(diff / 3600) + '小时前'
+  } else if (diff < 3600 * 24 * 2) {
+    return '1天前'
+  }
+  if (option) {
+    return parseTime(time, option)
+  } else {
+    return (
+      d.getMonth() +
+      1 +
+      '月' +
+      d.getDate() +
+      '日' +
+      d.getHours() +
+      '时' +
+      d.getMinutes() +
+      '分'
+    )
+  }
+}
+
+/**
+ * @param {string} url
+ * @returns {Object}
+ */
+export function getQueryObject(url:string) {
+  url = url == null ? window.location.href : url
+  const search = url.substring(url.lastIndexOf('?') + 1)
+  const obj:any = {}
+  const reg = /([^?&=]+)=([^?&=]*)/g
+  search.replace(reg, (rs, $1, $2) => {
+    const name = decodeURIComponent($1)
+    let val = decodeURIComponent($2)
+    val = String(val)
+    obj[name as keyof typeof obj] = val
+    return rs
+  })
+  return obj
+}
+
+/**
+ * @param {string} input value
+ * @returns {number} output value
+ */
+export function byteLength(str:any) {
+  // returns the byte length of an utf8 string
+  let s = str.length
+  for (let i = str.length - 1; i >= 0; i--) {
+    const code = str.charCodeAt(i)
+    if (code > 0x7f && code <= 0x7ff) s++
+    else if (code > 0x7ff && code <= 0xffff) s += 2
+    if (code >= 0xDC00 && code <= 0xDFFF) i--
+  }
+  return s
+}
+
+/**
+ * @param {Array} actual
+ * @returns {Array}
+ */
+export function cleanArray(actual:any) {
+  const newArray = []
+  for (let i = 0; i < actual.length; i++) {
+    if (actual[i]) {
+      newArray.push(actual[i])
+    }
+  }
+  return newArray
+}
+
+/**
+ * @param {Object} json
+ * @returns {Array}
+ */
+export function param(json:any) {
+  if (!json) return ''
+  return cleanArray(
+    Object.keys(json).map(key => {
+      if (json[key] === undefined) return ''
+      return encodeURIComponent(key) + '=' + encodeURIComponent(json[key])
+    })
+  ).join('&')
+}
+
+/**
+ * @param {string} url
+ * @returns {Object}
+ */
+export function param2Obj(url:any) {
+  const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ')
+  if (!search) {
+    return {}
+  }
+  const obj:any = {}
+  const searchArr = search.split('&')
+  searchArr.forEach(v => {
+    const index = v.indexOf('=')
+    if (index !== -1) {
+      const name = v.substring(0, index)
+      const val = v.substring(index + 1, v.length)
+      obj[name] = val
+    }
+  })
+  return obj
+}
+
+/**
+ * @param {string} val
+ * @returns {string}
+ */
+export function html2Text(val:any) {
+  const div = document.createElement('div')
+  div.innerHTML = val
+  return div.textContent || div.innerText
+}
+
+/**
+ * Merges two objects, giving the last one precedence
+ * @param {Object} target
+ * @param {(Object|Array)} source
+ * @returns {Object}
+ */
+export function objectMerge(target:any, source:any) {
+  if (typeof target !== 'object') {
+    target = {}
+  }
+  if (Array.isArray(source)) {
+    return source.slice()
+  }
+  Object.keys(source).forEach(property => {
+    const sourceProperty = source[property]
+    if (typeof sourceProperty === 'object') {
+      target[property] = objectMerge(target[property], sourceProperty)
+    } else {
+      target[property] = sourceProperty
+    }
+  })
+  return target
+}
+
+/**
+ * @param {HTMLElement} element
+ * @param {string} className
+ */
+export function toggleClass(element:any, className:any) {
+  if (!element || !className) {
+    return
+  }
+  let classString = element.className
+  const nameIndex = classString.indexOf(className)
+  if (nameIndex === -1) {
+    classString += '' + className
+  } else {
+    classString =
+      classString.substr(0, nameIndex) +
+      classString.substr(nameIndex + className.length)
+  }
+  element.className = classString
+}
+
+/**
+ * @param {string} type
+ * @returns {Date}
+ */
+export function getTime(type:any) {
+  if (type === 'start') {
+    return new Date().getTime() - 3600 * 1000 * 24 * 90
+  } else {
+    return new Date(new Date().toDateString())
+  }
+}
+
+/**
+ * @param {Function} func
+ * @param {number} wait
+ * @param {boolean} immediate
+ * @return {*}
+ */
+// export function debounce(func:any, wait:any, immediate:any) {
+//   let timeout:any, args:any, context:any, timestamp:any, result:any
+
+//   const later = function() {
+//     // 据上一次触发时间间隔
+//     const last = +new Date() - timestamp
+
+//     // 上次被包装函数被调用时间间隔 last 小于设定时间间隔 wait
+//     if (last < wait && last > 0) {
+//       timeout = setTimeout(later, wait - last)
+//     } else {
+//       timeout = null
+//       // 如果设定为immediate===true,因为开始边界已经调用过了此处无需调用
+//       if (!immediate) {
+//         result = func.apply(context, args)
+//         if (!timeout) context = args = null
+//       }
+//     }
+//   }
+
+//   return function(...args:any) {
+//     /* eslint-disable */
+//     context = this
+//     timestamp = +new Date()
+//     const callNow = immediate && !timeout
+//     // 如果延时不存在,重新设定延时
+//     if (!timeout) timeout = setTimeout(later, wait)
+//     if (callNow) {
+//       result = func.apply(context, args)
+//       context = args = null
+//     }
+
+//     return result
+//   }
+// }
+
+/**
+ * This is just a simple version of deep copy
+ * Has a lot of edge cases bug
+ * If you want to use a perfect deep copy, use lodash's _.cloneDeep
+ * @param {Object} source
+ * @returns {Object}
+ */
+// export function deepClone(source:any) {
+//   if (!source && typeof source !== 'object') {
+//     throw new Error('error arguments', 'deepClone')
+//   }
+//   const targetObj:any = source.constructor === Array ? [] : {}
+//   Object.keys(source).forEach(keys => {
+//     if (source[keys] && typeof source[keys] === 'object') {
+//       targetObj[keys] = deepClone(source[keys])
+//     } else {
+//       targetObj[keys] = source[keys]
+//     }
+//   })
+//   return targetObj
+// }
+
+/**
+ * @param {Array} arr
+ * @returns {Array}
+ */
+export function uniqueArr(arr:any) {
+  return Array.from(new Set(arr))
+}
+
+/**
+ * @returns {string}
+ */
+export function createUniqueString() {
+  const timestamp = +new Date() + ''
+  const randomNum = parseInt(((1 + Math.random()) * 65536).toString()) + ''
+  return (+(randomNum + timestamp)).toString(32)
+}
+
+/**
+ * Check if an element has a class
+ * @param {HTMLElement} elm
+ * @param {string} cls
+ * @returns {boolean}
+ */
+export function hasClass(ele:any, cls:any) {
+  return !!ele.className.match(new RegExp('(\\s|^)' + cls + '(\\s|$)'))
+}
+
+/**
+ * Add class to element
+ * @param {HTMLElement} elm
+ * @param {string} cls
+ */
+export function addClass(ele:any, cls:any) {
+  if (!hasClass(ele, cls)) ele.className += ' ' + cls
+}
+
+/**
+ * Remove class from element
+ * @param {HTMLElement} elm
+ * @param {string} cls
+ */
+export function removeClass(ele:any, cls:any) {
+  if (hasClass(ele, cls)) {
+    const reg = new RegExp('(\\s|^)' + cls + '(\\s|$)')
+    ele.className = ele.className.replace(reg, ' ')
+  }
+}
+
+/**
+ * 时间日期格式化
+ * @param dateObj
+ * @param format
+ * @returns {*}
+ */
+export const dateFormat = function(dateObj:any, format:any) {
+  const date = {
+    'M+': dateObj.getMonth() + 1,
+    'd+': dateObj.getDate(),
+    'h+': dateObj.getHours(),
+    'm+': dateObj.getMinutes(),
+    's+': dateObj.getSeconds(),
+    'q+': Math.floor((dateObj.getMonth() + 3) / 3),
+    'S+': dateObj.getMilliseconds()
+  }
+  if (/(y+)/i.test(format)) {
+    format = format.replace(RegExp.$1, (dateObj.getFullYear() + '').substr(4 - RegExp.$1.length))
+  }
+  for (const k in date) {
+    if (new RegExp('(' + k + ')').test(format)) {
+      format = format.replace(RegExp.$1, RegExp.$1.length === 1
+        ? date[k as keyof typeof date] : ('00' + date[k as keyof typeof date]).substr(('' + date[k as keyof typeof date]).length))
+    }
+  }
+  return format
+}
+
+export function dateFormat2(date:Date) {
+  const year = date.getFullYear()
+  let month:number|string = date.getMonth() + 1
+  let day:number|string = date.getDate()
+
+  if (month < 10) {
+    month = '0' + month
+  }
+  if (day < 10) {
+    day = '0' + day
+  }
+  return (year + '-' + month + '-' + day)
+}

+ 23 - 0
src/utils/local.ts

@@ -0,0 +1,23 @@
+/*
+    window.localStorage 本地存储
+*/
+
+export default {
+  // 存
+  set(key:string, val:any) {
+    sessionStorage.setItem(key, JSON.stringify(val))
+  },
+  // 取
+  get(key:string) {
+    const json:any = sessionStorage.getItem(key)
+    return JSON.parse(json)
+  },
+  // 删除
+  remove(key:string) {
+    sessionStorage.removeItem(key)
+  },
+  // 清空
+  clear() {
+    sessionStorage.clear()
+  }
+}

+ 145 - 0
src/utils/oss.ts

@@ -0,0 +1,145 @@
+'use strict'
+import { dateFormat } from '@/utils/index'
+import { getOssSts } from '@/api/api'
+import { showFailToast } from 'vant'
+import OSS from 'ali-oss'
+
+export default {
+
+  /**
+     * 创建随机字符串
+     * @param num
+     * @returns {string}
+     */
+  randomString(num:number) {
+    const chars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']
+    let res = ''
+    for (let i = 0; i < num; i++) {
+      const id = Math.ceil(Math.random() * 35)
+      res += chars[id]
+    }
+    return res
+  },
+
+  /**
+     * 创建oss客户端对象
+     * @returns {*}
+     */
+  createOssClient() {
+    return getOssSts().then(
+      success => {
+        const data = success.data.data
+        console.log('success.data.data', data)
+
+        return new Promise((resolve, reject:any) => {
+          const client = new OSS({
+            region: 'oss-cn-hangzhou',
+            accessKeyId: data.credentials.AccessKeyId,
+            accessKeySecret: data.credentials.AccessKeySecret,
+            stsToken: data.credentials.SecurityToken,
+            bucket: data.config.BucketName,
+            secure: true
+          })
+          resolve(client)
+        })
+      },
+      error => {
+        showFailToast(error)
+        // this.$message.error(error)
+      }
+    )
+  },
+  /**
+     * 文件上传
+     * @param option
+     */
+  ossUploadFile(option:any) {
+    const file = option.file
+    /* eslint-disable*/
+    const self = this
+    /* eslint-enable*/
+    return new Promise((resolve, reject) => {
+      const date = dateFormat(new Date(), 'yyyyMMdd') // 当前时间
+      const dateTime = dateFormat(new Date(), 'yyyyMMddhhmmss') // 当前时间
+      const randomStr = self.randomString(4)//  4位随机字符串
+      const extensionName = file.name.substr(file.name.indexOf('.')) // 文件扩展名
+      const fileName = 'backend/' + date + '/' + dateTime + '_' + randomStr + extensionName // 文件名字(相对于根目录的路径 + 文件名)
+      // 执行上传
+      self.createOssClient().then((client:any) => {
+        // 异步上传,返回数据
+        // 上传处理
+        // 分片上传文件
+        client.multipartUpload(fileName, file, {
+          progress: function(p:any) {
+            const e:any = {}
+            e.percent = Math.floor(p * 100)
+            console.log('Progress: ' + p)
+            option.onProgress(e)
+          }
+        }).then((val:any) => {
+          console.log(val)
+          if (val.res.statusCode === 200) {
+            option.onSuccess(val)
+            resolve({
+              fileName: file.name,
+              fileUrl: 'https://' + val.bucket + '.oss-cn-hangzhou.aliyuncs.com/' + fileName
+            })
+            return val
+          } else {
+            option.onError('上传失败')
+          }
+        }, (err:any) => {
+          option.onError('上传失败')
+          reject(err)
+        })
+      })
+    })
+  },
+  /**
+     * base64上传
+     * @param option
+     */
+  ossUploadBase(file:any) {
+    // const file = option.file
+    /* eslint-disable*/
+    const self = this
+    /* eslint-enable*/
+    return new Promise((resolve, reject) => {
+      const date = dateFormat(new Date(), 'yyyyMMdd') // 当前时间
+      const dateTime = dateFormat(new Date(), 'yyyyMMddhhmmss') // 当前时间
+      const randomStr = self.randomString(4)//  4位随机字符串
+      const extensionName = file.name.substr(file.name.indexOf('.')) // 文件扩展名
+      const fileName = 'backend/' + date + '/' + dateTime + '_' + randomStr + extensionName // 文件名字(相对于根目录的路径 + 文件名)
+      // 执行上传
+      self.createOssClient().then((client:any) => {
+        // 异步上传,返回数据
+        // 上传处理
+        // 分片上传文件
+        client.multipartUpload(fileName, file, {
+          progress: function(p:any) {
+            const e:any = {}
+            e.percent = Math.floor(p * 100)
+            console.log('Progress: ' + p)
+            // option.onProgress(e)
+          }
+        }).then((val:any) => {
+          console.log(val)
+          if (val.res.statusCode === 200) {
+            // option.onSuccess(val)
+            resolve({
+              fileName: file.name,
+              fileUrl: 'https://' + val.bucket + '.oss-cn-hangzhou.aliyuncs.com/' + fileName
+            })
+            return val
+          } else {
+            // option.onError('上传失败')
+          }
+        }, (err:any) => {
+          // option.onError('上传失败')
+          reject(err)
+        })
+      })
+    })
+  }
+
+}

+ 50 - 0
src/utils/version.ts

@@ -0,0 +1,50 @@
+declare global { // 设置全局属性
+  interface Window { // window对象属性
+    pendingETAG: boolean; // 加入对象
+  }
+}
+export function getVersion() {
+  // main.js 和 拦截器中加入
+  if (('pendingETAG' in window && window.pendingETAG) || import.meta.env.VITE_ENV === 'development') return
+  const htmlFileUrl = `${location.origin}${location.pathname}`
+  window.pendingETAG = true
+  setTimeout(() => {
+    window.pendingETAG = false
+  }, 20000)
+  fetch(htmlFileUrl, {
+    method: 'HEAD',
+    cache: 'no-cache'
+  }).then(res => {
+    const version:string | undefined = res.headers.get('etag') as string || undefined
+    if (!version) throw new Error('can not get version')
+    const previousVersion = localStorage.getItem('version')
+    if (!previousVersion) return localStorage.setItem('version', version)
+    if (version !== previousVersion) {
+      const isConfirm = confirm('检测到存在可用更新,是否更新?')
+      if (isConfirm) {
+        localStorage.setItem('version', version)
+        location.reload()
+      }
+    }
+  })
+}
+export function getVersionFR() {
+  // 或者 路由守卫afterEach 中加入
+  console.log(import.meta.env.VITE_ENV === 'development')
+
+  if (import.meta.env.VITE_ENV === 'development') return
+  const htmlFileUrl = `${location.origin}${location.pathname}`
+  fetch(htmlFileUrl, {
+    method: 'HEAD',
+    cache: 'no-cache'
+  }).then(res => {
+    const version:string | undefined = res.headers.get('etag') as string || undefined
+    if (!version) throw new Error('can not get version')
+    const previousVersion = localStorage.getItem('version')
+    if (!previousVersion) return localStorage.setItem('version', version)
+    if (version !== previousVersion) {
+      localStorage.setItem('version', version)
+      location.reload()
+    }
+  })
+}

+ 339 - 0
src/views/category.vue

@@ -0,0 +1,339 @@
+<template>
+  <div class="cate-list">
+    <div class="cate-tag">
+      <div class="tag-kinds">类型:</div>
+      <div v-for="item in typeArr"
+           :key="item.id"
+           class="tags"
+           :class="{ active: typeIndex === item.id }"
+           @click="selectType('type', item.id)">
+        {{ item.name }}
+      </div>
+    </div>
+    <div class="cate-tag">
+      <div class="tag-kinds">标签:</div>
+      <div v-for="item in tagArr"
+           :key="item.id"
+           class="tags"
+           :class="{ active: tag_idIndex === item.id }"
+           @click="selectType('tag_id', item.id)">
+        {{ item.name }}
+      </div>
+    </div>
+    <div class="drop-tag">
+      <van-dropdown-menu ref="menuRef">
+        <van-dropdown-item title="类型" >
+          <van-cell
+            v-for="item in typeArr"
+            :key="item.id"
+            :title="item.name"
+            :class="{ active: typeIndex === item.id }"
+            @click="selectType('type', item.id)"/>
+        </van-dropdown-item>
+        <van-dropdown-item title="标签" ref="tag">
+          <van-cell
+            v-for="item in tagArr"
+            :key="item.id"
+            :title="item.name"
+            :class="{ active: tag_idIndex === item.id }"
+            @click="selectType('tag_id', item.id)"/>
+        </van-dropdown-item>
+      </van-dropdown-menu>
+    </div>
+    <el-divider class="divide" />
+    <ul>
+      <li class="hot-item" v-for="(item,index) in gameLis" :key="index">
+        <img class="game-logo" :src="prefix + item.logopic"  :onerror="logoErrorFun" alt=""/>
+        <van-button type="primary" class="down-btn" :disabled="item.download_url === ''" size="small" round color="#14b9c7"  @click="downGame(item.download_url, item.download_url === '')">下载</van-button>
+        <div class="detail">
+          <div class="top">
+            <span class="game_title ellip">{{ item.screen_name }}</span>
+            <el-tag size="small" type="success">{{ (item.game_score / 10) > 5 ? '5.0' : (item.game_score /
+              10).toFixed(1)
+            }}</el-tag>
+          </div>
+          <div class="desc ellip">{{ item.sketch || '暂无简介' }}</div>
+          <el-tag
+            size="small"
+            class="game_tag"
+            type="warning"
+            round
+            v-for="(tag, tIndex) in item.tags"
+            :key="tIndex"
+          >
+            {{ tagArr.findIndex((i)=>i.id === tag)>=0 ? tagArr.find((i:any)=>i.id === tag).name :''  }}
+          </el-tag>
+        </div>
+        <img  :src="prefix + item.logopic" class="game-poster" :onerror="posterErrorFun" alt=""/>
+        <div class="action" @click="downGame(item.download_url, item.download_url === '')" :style="item.download_url === ''? 'cursor:not-allowed;':'cursor: pointer;'">
+          <img :src="img('download.png')" alt=""/>
+          <div class="size">{{ bytesChange(item.size) }}</div>
+        </div>
+      </li>
+    </ul>
+    <div class="more-action">
+      <el-button v-if="params.page*params.pagesize < total" type="info" @click="loadMore">更多</el-button>
+      <el-divider v-else>没有更多了</el-divider>
+    </div>
+  </div>
+
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, onMounted, inject } from 'vue'
+import { getGameType, getGameTag, getGameList } from '@/api/index'
+import Message from '@/utils/Message'
+import { bytesChange } from '@/utils/bytesFormatter'
+import type { DropdownMenuInstance } from 'vant'
+
+const img:any = inject('img')
+const typeIndex = ref<number>(0)
+const tag_idIndex = ref<number>(0)
+const gameLis: any = ref([])
+const typeArr: any = ref([])
+const tagArr: any = ref([])
+const prefix = ref<string>('')
+const isLoading = ref<boolean>(true)
+const total = ref<number>(0)
+interface Params {
+    type: number,
+    tag_id: number,
+    page: number,
+    pagesize: number
+}
+const params = reactive<Params>({
+  type: 0,
+  tag_id: 0,
+  page: 1,
+  pagesize: 15
+})
+
+onMounted(async() => {
+  await getGameType().then(res => {
+    // console.log('类型', res);
+    typeArr.value = res.data.data
+    typeArr.value.unshift({ id: 0, name: '全部' })
+  })
+  await getGameTag().then(res => {
+    // console.log('标签', res);
+    tagArr.value = res.data.data
+    tagArr.value.unshift({ id: 0, name: '全部' })
+  })
+  getGameLists(params)
+})
+
+// 初始化列表数据
+const getGameLists = async(params:any) => {
+  // console.log('参数', params);
+
+  await getGameList(params).then(res => {
+    // console.log('数据', res);
+    if (res.data.code === 200 && res.data.data) {
+      gameLis.value = gameLis.value.concat(res.data.data.lists)
+      total.value = res.data.data.total
+      prefix.value = res.data.data.prefix
+      isLoading.value = false
+    }
+  }).catch(err => {
+    Message.error(err.data.msg)
+  })
+}
+const menuRef = ref<DropdownMenuInstance>()
+// 点击类型、标签、排序
+const selectType = (type: any, index: any) => {
+  gameLis.value = []
+  params.page = 1
+
+  if (type === 'type') {
+    typeIndex.value = index
+    params.type = index
+  }
+  if (type === 'tag_id') {
+    tag_idIndex.value = index
+    params.tag_id = index
+  }
+  if (menuRef.value) {
+    menuRef.value.close()
+  }
+  // 如果都为 全部  则发起请求 页面显示15条数据
+  if (params.type === 0 && params.tag_id === 0) {
+    params.pagesize = 15
+    getGameLists(params)
+  } else {
+    allData()
+  }
+}
+// 全部数据
+const allData = async() => {
+  params.pagesize = total.value
+  // console.log('params', params);
+  await getGameList(params).then(resp => {
+    // console.log('resp===>', resp);
+    if (resp.data.code === 200 && resp.data.data) {
+      gameLis.value = resp.data.data.lists.filter(item => {
+        // 如果标签为全部  类型不为全部 返回匹配的类型数据
+        if (params.tag_id === 0) {
+          return item.type === params.type
+        } else if (params.type === 0) {
+          // 如果类型为全部  标签不为全部 返回匹配的标签数据
+          return item.tags.includes(params.tag_id)
+        } else return item.type === params.type && item.tags.includes(params.tag_id) // 否则返回匹配的标签和类型数据
+      })
+    }
+  }).catch(err => {
+    Message.error(err.data.msg)
+  })
+}
+const downGame = (url: string, disabled:boolean) => {
+  if (disabled) return
+  // 下载游戏
+  window.open(url)
+  // window.location.href = url;
+}
+// 加载更多
+const loadMore = () => {
+  params.page += 1
+  getGameLists(params)
+}
+const logoErrorFun = (event:any) => {
+  var imgDom = event.srcElement
+  imgDom.src = img('default.png')
+  imgDom.onerror = null
+}
+const posterErrorFun = (event:any) => {
+  var imgDom = event.srcElement
+  imgDom.src = img('defaultPoster.png')
+  imgDom.onerror = null
+}
+</script>
+
+<style scoped lang="scss">
+.cate-list{
+  background-color: #fff;
+  padding: 1.1rem 1rem;
+  box-sizing: border-box;
+  width: 100%;
+  >div{
+    font-size: .8rem;
+  }
+  .tag-kinds{
+    margin-top: 1rem;
+  }
+  .cate-tag{
+    display: flex;
+    .tags{
+      margin: 1rem;
+      cursor: pointer;
+    }
+  }
+  .hot-item{
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    position: relative;
+    margin-bottom: .5rem;
+    border-bottom: .5px solid #ddd;
+    height: 10rem;
+    .down-btn{
+      display: none;
+    }
+    .game-logo{
+      width: 6rem;
+      height: 6rem;
+      border-radius: .2rem;
+      background-color: #f7f7f7;
+    }
+    .game-poster{
+      width: 12.1rem;
+      max-height: 7rem;
+    }
+    .detail{
+      width: 30rem;
+      .top{
+        display: flex;
+        align-items: center;
+        .game_title{
+          margin-right: .3rem;
+          font-size: 1rem;
+        }
+      }
+      .desc{
+        font-size: .8rem;
+        margin: .5rem 0;
+      }
+    }
+    &:hover .action{
+        transform: translateX(-5rem);
+    }
+    .action{
+      position: absolute;
+      right: -6rem;
+      // top: 50%;
+      width: 5rem;
+      height: 7rem;
+      background-color: rgba($color: #e6a23c, $alpha: .5);
+      // transform: translateY(-50%);
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      flex-direction: column;
+      transition: all 200ms ease;
+      img{
+        width: 2.5rem;
+      }
+      .size{
+        font-size: .5rem;
+        margin-top: .5rem;
+        color: #fff;
+      }
+
+    }
+  }
+  .more-action{
+    text-align: center;
+  }
+}
+// .header_list {
+//     .top_type {
+//         margin: 15px 0 10px;
+//         background-color: #fff;
+//         padding: 20px 50px 20px 20px;
+//         box-sizing: border-box;
+
+//         .left_title {
+//             width: 50px;
+//             font-size: 14px;
+//             color: #666;
+//         }
+
+//         .right_type {
+
+//             // background-color: #ed8c0f;
+//             .cont {
+//                 margin-right: 20px;
+//                 padding: 3px 5px;
+//                 cursor: pointer;
+
+//                 &:hover {
+//                     color: #ed8c0f;
+//                 }
+//             }
+//         }
+
+//         .type {
+//             margin-bottom: 20px;
+//         }
+//     }
+
+//     .el-button {
+//         margin: 5px 20px 15px 0;
+//         padding: 0 20px;
+//         border: 0;
+//         color: #999;
+//     }
+// }
+
+.active {
+    color: #ed8c0f
+}
+</style>

+ 127 - 0
src/views/ecoin.vue

@@ -0,0 +1,127 @@
+<template>
+  <div class="rest-list">
+    <div class="list-wrapper">
+      <el-table :data="gameHot" style="width: 100%">
+        <el-table-column prop="game_name" label="游戏名称"  />
+        <el-table-column prop="total_ecoin" label="余额">
+          <template #default="scope">
+            <div>
+              {{ (scope.row.total_ecoin/100).toFixed(2)}}
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column prop="created_at" label="创建时间" width="180" />
+        <el-table-column prop="modified_at" label="修改时间" width="180" />
+      </el-table>
+      <div class="pagination-block">
+        <el-pagination
+          v-model:current-page="pageConfig.page"
+          hide-on-single-page
+          v-model:page-size="pageConfig.pagesize"
+          layout="prev, pager, next, jumper,total"
+          :total="pageConfig.total"
+          @current-change="handleCurrentChange"
+        />
+      </div>
+    </div>
+  </div>
+  <div class="mob-list">
+    <vant-list ref="vanList" :param="{}" :pagination="pageConfig" :request="userEcoin" @resetPage="resetPage" @pagePlus="addPage">
+      <template #listItem="listItem">
+        <div class="item" v-for="(item, index) in listItem.row.lists" :key="index">
+          <van-cell-group inset >
+            <van-cell title="游戏名称" :value="item.game_name" />
+            <van-cell title="余额" :value="(item.total_ecoin/100).toFixed(2)" />
+            <van-cell title="创建时间" :value="item.created_at" />
+            <van-cell title="修改时间" :value="item.modified_at" />
+          </van-cell-group>
+        </div>
+      </template>
+    </vant-list>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, onMounted, computed, inject } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
+import { useStore } from '@/store'
+import { showSuccessToast, showFailToast } from 'vant'
+import Message from '@/utils/Message'
+import { userEcoin } from '@/api/index'
+import vantList from '@/components/vantList.vue'
+import type { ListInstance } from 'vant'
+const img:any = inject('img')
+const router = useRouter()
+const route = useRoute()
+const { user } = useStore()
+// showSuccessToast('成功文案')
+// showFailToast('失败文案')
+const gameHot: any = ref([])
+const isLoading = ref<boolean>(true)
+const vanList = ref<any>()
+const pageConfig = reactive({
+  page: 1,
+  pagesize: 10,
+  total: 0
+})
+const handleCurrentChange = (val: number) => {
+  console.log(`current page: ${val}`)
+  pageConfig.page = val
+  getTableData()
+}
+const getTableData = async() => {
+  const params = {
+    page: pageConfig.page,
+    pagesize: pageConfig.pagesize
+  }
+  await userEcoin(params).then(res => {
+    console.log('热门游戏', res.data)
+    if (res.data.code === 200 && res.data.data) {
+      gameHot.value = res.data.data.lists || []
+      isLoading.value = false
+      pageConfig.total = res.data.data.total || 0
+    }
+  }).catch(err => {
+    console.log(err)
+
+    Message.error(err.data.msg)
+  })
+}
+const resetPage = () => {
+  pageConfig.page = 1
+}
+const addPage = () => {
+  pageConfig.page += 1
+}
+onMounted(async() => {
+  getTableData()
+  if (vanList.value) {
+    vanList.value.onLoad()
+  }
+})
+
+</script>
+
+<style lang="scss" scoped>
+.rest-list{
+  width: 100%;
+  padding: 3rem;
+  box-sizing: border-box;
+  background-color: #fff;
+  min-height: 40rem;
+  .pagination-block{
+    padding-top: 1rem;
+    display: flex;
+    flex-direction: row-reverse;
+  }
+}
+.mob-list{
+  display: none;
+  width: 100%;
+  padding: 10px 0px;
+  box-sizing: border-box;
+  .item{
+    margin-bottom: 10px;
+  }
+}
+</style>

+ 439 - 0
src/views/home.vue

@@ -0,0 +1,439 @@
+<template>
+  <div class="left-list">
+    <div class="list-part">
+      <span style="margin-bottom: 1rem;display: block;">今日热游</span>
+      <ul>
+        <li class="hot-item" v-for="(item,index) in gameHot" :key="index">
+          <img :src="prefix + item.screenshot" class="game-poster" :onerror="posterErrorFun" alt=""/>
+          <div class="detail">
+            <div class="top">
+              <span class="game_title ellip">{{ item.screen_name }}</span>
+              <el-tag  type="success">{{ (item.game_score / 10) > 5 ? '5.0' : (item.game_score /
+                10).toFixed(1)
+              }}</el-tag>
+            </div>
+            <div class="desc ellip">{{ item.sketch || '暂无简介' }}</div>
+
+          </div>
+          <div class="action" @click="downGame(item.download_url, item.download_url === '')" :style="item.download_url === ''? 'cursor:not-allowed;':'cursor: pointer;'">
+            <img :src="img('download.png')" alt=""/>
+            <div class="size">{{ bytesChange(item.size) }}</div>
+          </div>
+        </li>
+      </ul>
+    </div>
+    <div class="list-part">
+      <span style="margin-bottom: 1rem;display: block;">推荐游戏</span>
+      <ul>
+        <li class="hot-item" v-for="(item,index) in gameRecommand" :key="index">
+          <img :src="prefix + item.screenshot" class="game-poster" :onerror="posterErrorFun" alt=""/>
+          <div class="detail">
+            <div class="top">
+              <span class="game_title ellip">{{ item.screen_name }}</span>
+              <el-tag  type="success">{{ (item.game_score / 10) > 5 ? '5.0' : (item.game_score /
+                10).toFixed(1)
+              }}</el-tag>
+            </div>
+            <div class="desc ellip">{{ item.sketch || '暂无简介' }}</div>
+
+          </div>
+          <div class="action" @click="downGame(item.download_url, item.download_url === '')" :style="item.download_url === ''? 'cursor:not-allowed;':'cursor: pointer;'">
+            <img :src="img('download.png')" alt=""/>
+            <div class="size">{{ bytesChange(item.size) }}</div>
+          </div>
+        </li>
+      </ul>
+    </div>
+  </div>
+  <div class="right-action">
+    <div v-if="user.isQingQue" class="action-item service">
+      <el-icon><service /></el-icon>
+      <div>客服qq:2885393309</div>
+    </div>
+    <div class="action-item">
+      <div class="service">
+        <el-icon><user-filled /></el-icon>
+        <div>个人中心</div>
+      </div>
+      <el-divider />
+      <div v-if="hasToken" class="money">
+        <el-card shadow="hover" @click="router.push('/coin_type2')">
+          <div class="service">
+            <el-icon><wallet /></el-icon>
+            <div>{{ user.profile.ecoinName + '余额:' + (user.profile.ecoin / 100).toFixed(2) }}</div>
+          </div>
+        </el-card>
+        <el-card shadow="hover" @click="router.push('/coin_type1')">
+          <div class="service">
+            <el-icon><money /></el-icon>
+            <div>{{ user.profile.ncoinName + '余额:' + (user.profile.ncoin / 100).toFixed(2) }}</div>
+          </div>
+        </el-card>
+        <el-card shadow="hover" @click="router.push('/order')">
+          <div class="service">
+            <el-icon><tickets /></el-icon>
+            <div>我的订单</div>
+          </div>
+        </el-card>
+        <el-card shadow="hover" @click="router.push('/my_game')">
+          <div class="service">
+            <el-icon><present /></el-icon>
+            <div>我的游戏</div>
+          </div>
+        </el-card>
+        <el-card shadow="hover" @click="router.push('/settings')">
+          <div class="service">
+            <el-icon><setting /></el-icon>
+            <div>账号设置</div>
+          </div>
+        </el-card>
+      </div>
+      <div v-else class="money" @click="router.push('/login')">
+        <el-card shadow="hover">请登录</el-card>
+      </div>
+    </div>
+    <div class="action-item">
+      <div class="service">
+        <el-icon><collection-tag /></el-icon>
+        <div>热门标签</div>
+      </div>
+      <el-divider />
+      <el-button v-for="(item,index) in tagArr" round :key="index" type="primary" size="small" style="margin-bottom: .3rem;">{{ item.name }}</el-button>
+    </div>
+    <div class="action-item">
+      <div class="service">
+        <el-icon><trophy-base /></el-icon>
+        <div>排行榜</div>
+      </div>
+      <el-divider />
+      <ul>
+        <li class="rank-item" v-for="(item,index) in gameRecommand" :key="index">
+          <el-tag size="small" round>{{ index+1 }}</el-tag>
+          <img :src="prefix + item.logopic" class="game-logo" :onerror="logoErrorFun" alt=""/>
+          <div class="detail">
+            <div class="top">
+              <span class="game_title ellip">{{ item.screen_name }}</span>
+              <el-tag size="small" type="success">{{ (item.game_score / 10) > 5 ? '5.0' : (item.game_score /
+                10).toFixed(1)
+              }}</el-tag>
+            </div>
+            <div class="desc ellip">{{ item.sketch || '暂无简介' }}</div>
+          </div>
+        </li>
+      </ul>
+    </div>
+  </div>
+  <div class="mob-page">
+    <van-swipe class="my-swipe" indicator-color="white" :autoplay="5000">
+      <van-swipe-item v-for="item in listSwiper" :key="item.game_id" class="swiper-item">
+        <img class="swiper-item-img" :src="prefix + item.screenshot" :onerror="posterErrorFun" />
+        <div class="game-card ">
+          <div class="ellip">{{ item.screen_name }}</div>
+          <van-button type="primary" size="small" color="#14b9c7" round  @click="downGame(item.download_url, item.download_url === '')">下载</van-button>
+        </div>
+      </van-swipe-item>
+    </van-swipe>
+    <div class="menu">
+      <van-grid column-num="4">
+        <van-grid-item icon="filter-o" text="分类" to="/cate" />
+        <van-grid-item icon="aim" text="我的游戏" to="/my_game" />
+        <van-grid-item icon="completed" text="订单中心" to="/order" />
+        <van-grid-item icon="setting-o" text="设置" to="/settings" />
+      </van-grid>
+    </div>
+    <div class="games">
+      <h3 class="list-title">今日热游</h3>
+      <ul>
+        <li class="rank-item" v-for="(item,index) in gameHot" :key="index">
+          <img :src="prefix + item.logopic" class="game-logo" :onerror="logoErrorFun" alt=""/>
+          <div class="detail">
+            <div class="top">
+              <span class="game_title ellip">{{ item.screen_name }}</span>
+              <el-tag size="small" type="success">{{ (item.game_score / 10) > 5 ? '5.0' : (item.game_score /
+                10).toFixed(1)
+              }}</el-tag>
+            </div>
+            <div class="desc ellip">{{ item.sketch || '暂无简介' }}</div>
+          </div>
+          <van-button type="primary" :disabled="item.download_url === ''" size="small" round color="#14b9c7"  @click="downGame(item.download_url, item.download_url === '')">下载</van-button>
+        </li>
+      </ul>
+    </div>
+    <div class="games">
+      <h3 class="list-title">推荐游戏</h3>
+      <ul>
+        <li class="rank-item" v-for="(item,index) in gameRecommand" :key="index">
+          <img :src="prefix + item.logopic" class="game-logo" :onerror="logoErrorFun" alt=""/>
+          <div class="detail">
+            <div class="top">
+              <span class="game_title ellip">{{ item.screen_name }}</span>
+              <el-tag size="small" type="success">{{ (item.game_score / 10) > 5 ? '5.0' : (item.game_score /
+                10).toFixed(1)
+              }}</el-tag>
+            </div>
+            <div class="desc ellip">{{ item.sketch || '暂无简介' }}</div>
+          </div>
+          <van-button type="primary" :disabled="item.download_url === ''" size="small" round color="#14b9c7"  @click="downGame(item.download_url, item.download_url === '')">下载</van-button>
+        </li>
+      </ul>
+    </div>
+  </div>
+</template>
+<script setup lang="ts">
+import { ref, reactive, onMounted, computed, inject } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
+import { useStore } from '@/store'
+import { showSuccessToast, showFailToast } from 'vant'
+import Message from '@/utils/Message'
+import { getIndexGameHot, getIndexGameRecommand, getGameTag } from '@/api/index'
+import { bytesChange } from '@/utils/bytesFormatter'
+
+const img:any = inject('img')
+const router = useRouter()
+const route = useRoute()
+const { user } = useStore()
+// showSuccessToast('成功文案')
+// showFailToast('失败文案')
+const gameHot: any = ref([])
+const gameRecommand: any = ref([])
+const prefix = ref<string>('')
+const isLoading = ref<boolean>(true)
+const tagArr = ref<any[]>([])
+const listSwiper: any = ref([])
+const hasToken = ref(sessionStorage.getItem('token'))
+
+onMounted(async() => {
+  getIndexGameHot().then(res => {
+    console.log('热门游戏', res.data)
+    if (res.data.code === 200 && res.data.data) {
+      gameHot.value = [...res.data.data.lists]
+      prefix.value = res.data.data.prefix
+      isLoading.value = false
+      listSwiper.value = res.data.data.lists.splice(0, 5)
+    }
+  }).catch(err => {
+    showFailToast(err.data.msg)
+  })
+
+  getIndexGameRecommand().then(res => {
+    console.log('推荐游戏', res.data.data)
+    if (res.data.code === 200 && res.data.data) {
+      gameRecommand.value = res.data.data.lists
+      prefix.value = res.data.data.prefix
+      isLoading.value = false
+    }
+  }).catch(err => {
+    showFailToast(err.data.msg)
+  })
+  getGameTag().then(res => {
+    // console.log('标签', res);
+    tagArr.value = res.data.data
+  })
+})
+const logoErrorFun = (event:any) => {
+  var imgDom = event.srcElement
+  imgDom.src = img('default.png')
+  imgDom.onerror = null
+}
+const posterErrorFun = (event:any) => {
+  var imgDom = event.srcElement
+  imgDom.src = img('defaultPoster.png')
+  imgDom.onerror = null
+}
+const downGame = (url: string, disabled:boolean) => {
+  if (disabled) return
+  // 下载游戏
+  window.open(url)
+  // window.location.href = url;
+}
+</script>
+
+<style lang="scss" scoped>
+#main{
+  .main-wrap{
+    .left-list{
+      width: 36rem;
+      box-sizing: border-box;
+      overflow: hidden;
+      .list-part{
+        margin-bottom: 1rem;
+        background-color: #fff;
+        padding: 1.1rem 1rem;
+        box-sizing: border-box;
+      }
+      .hot-item{
+        height: 6.5rem;
+        display: flex;
+        align-items: center;
+        margin-bottom: 1rem;
+        // overflow: hidden;
+        width: 100%;
+        position: relative;
+        .game-poster{
+          height: 100%;
+          width: 11rem;
+          flex-shrink: 0;
+        }
+        .top{
+          display: flex;
+          align-items: center;
+        }
+        .detail{
+          margin-left: 2rem;
+          width: 15rem;
+          .game_title{
+            display: inline-block;
+            margin-right: .5rem;
+          }
+          .desc{
+            font-size: 0.7rem;
+            color: #666;
+            line-height: 1.3rem;
+            margin: .5rem 0;
+          }
+        }
+        &:hover .action{
+            transform: translateX(-5rem);
+        }
+        .action{
+          position: absolute;
+          right: -6rem;
+          top: 0;
+          width: 5rem;
+          height: 6.5rem;
+          background-color: rgba($color: #99ccff, $alpha: .5);
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          flex-direction: column;
+          transition: all 200ms ease;
+          img{
+            width: 2.5rem;
+          }
+          .size{
+            font-size: .5rem;
+            margin-top: .5rem;
+            color: #856c6c;
+          }
+
+        }
+      }
+    }
+    .right-action{
+      width: 18rem;
+      box-sizing: border-box;
+      .action-item{
+        background-color: #fff;
+        padding: 1rem;
+        box-sizing: border-box;
+        margin-bottom: 1rem;
+      }
+      .service{
+        display: flex;
+        align-items: center;
+        >div{
+          padding-left: .5rem;
+          font-size: .8rem;
+        }
+      }
+      .money{
+        font-size: .8rem;
+      }
+      .rank-item{
+        display: flex;
+        align-items: center;
+        height: 3rem;
+        margin-bottom: .5rem;
+        width: 100%;
+        justify-content: space-between;
+        .game-logo{
+          width: 3rem;
+          height: 3rem;
+          flex-shrink: 0;
+          margin: 0 .5rem;
+        }
+        .top{
+          margin-bottom: .2rem;
+
+        }
+        .game_title{
+          font-size: .8rem;
+        }
+        .desc{
+          width: 10rem;
+          font-size: .6rem;
+        }
+      }
+    }
+    .mob-page{
+      display: none;
+      padding:10px 15px;
+      box-sizing: border-box;
+      background: radial-gradient(circle at 0 -10%,#dcf4f7,rgba(220,244,247,0) 50%),radial-gradient(circle at 100% 10%,#e8ebfc,rgba(232,235,252,0) 50%);
+      // background-image: linear-gradient(#dcf4f7, #EBEEFD, #F5F9FF);
+      min-height: calc(100vh - 60px);
+      .my-swipe{
+        height: 140px;
+        overflow: hidden;
+        border-radius: 5%;
+        .swiper-item{
+          position: relative;
+          .game-card{
+            position: absolute;
+            right: 5px;
+            bottom: 5px;
+            width: 46px;
+            height: 46px;
+          }
+        }
+        .swiper-item-img{
+          height: 140px;
+          width: 100%;
+        }
+      }
+      .menu{
+        margin-top: 10px;
+      }
+      .games{
+        padding: 15px 5px;
+        box-sizing: border-box;
+        background-color: #EBEEFD;
+        border-radius: var(--logo-radius);
+        margin-top: 10px;
+        --van-button-small-padding:15px;
+        .list-title{
+          font-size: 14px;
+          margin-bottom: 10px;
+        }
+        .rank-item{
+          display: flex;
+          align-items: center;
+          margin-bottom: 5px;
+          justify-content: space-between;
+          img{
+            width: 18.13vw;
+            height: 18.13vw;
+            border-radius: var(--logo-radius);
+            background-color: #fff;
+          }
+          .detail{
+            width: 50vw;
+            .top{
+              display: flex;
+              align-items: center;
+              font-size: 14px;
+              margin: 5px 0;
+              .game_title{
+                margin-right: 5px;
+              }
+            }
+            .desc{
+              color: rgba(0,0,0,0.4);
+            }
+          }
+        }
+      }
+    }
+  }
+}
+</style>

+ 481 - 0
src/views/login.vue

@@ -0,0 +1,481 @@
+<template>
+  <div class="bg">
+    <div class="login-box">
+      <el-form
+        ref="ruleFormRef"
+        :rules="rules"
+        class="login_from df fdc aic jcc"
+        label-position="left"
+        :model="loginForm"
+        style="max-width: 400px"
+        @submit.prevent="submitForm(ruleFormRef)">
+        <el-tabs v-model="login_typ" style="width: 50%;" @tab-change="resetForm(ruleFormRef)">
+          <el-tab-pane label="密码登录" name="pwd" />
+          <el-tab-pane label="验证码登录" name="sms" />
+        </el-tabs>
+        <template v-if="login_typ === 'pwd'">
+          <el-form-item label="" prop="account">
+            <el-input v-model="loginForm.account" type="text" placeholder="请输入账号" clearable :prefix-icon="User" />
+          </el-form-item>
+          <el-form-item label="" prop="password">
+            <el-input
+              v-model="loginForm.password"
+              type="password"
+              show-password
+              placeholder="请输入密码"
+              autocomplete="off"
+              :prefix-icon="Lock" />
+            <el-input
+              v-model="loginForm.password"
+              type="hidden"
+              autocomplete="off"
+              id="md5_password"
+              show-password
+              placeholder="请输入密码"
+              :prefix-icon="Lock" />
+          </el-form-item>
+        </template>
+        <template v-else-if="login_typ === 'sms'">
+          <el-form-item label="" prop="account">
+            <el-input v-model="loginForm.account"  type="text" placeholder="请输入手机号" clearable :prefix-icon="Cellphone" />
+          </el-form-item>
+          <el-form-item label="" prop="smsCaptcha">
+            <el-input v-model="loginForm.smsCaptcha" type="number" placeholder="请输入验证码" :prefix-icon="Lock">
+              <template #append>
+                <div>
+                  <el-button @click="getCaptcha" color="#ed8c0f" style="width: 100px;" :disabled ="!can_send">{{smsMessage}}</el-button>
+                </div>
+              </template>
+            </el-input>
+          </el-form-item>
+        </template>
+        <!-- <el-form-item style="margin-top:-10px;margin-bottom:-5px;">
+                    <el-checkbox v-model="checked" style="color:#a0a0a0;margin-top:-10px;">记住我</el-checkbox>
+                </el-form-item> -->
+        <div class="btn df aic jcsb">
+          <el-button
+            round
+            class="submit_btn"
+            block
+            type="primary"
+            size="large"
+            native-type="submit">登&emsp;录</el-button>
+        </div>
+      </el-form>
+    </div>
+  </div>
+  <div class="mob-login">
+    <div class="login-box" :style="'transform: translateX('+transWidth+')'">
+      <div class="item">
+        <div class="text">欢迎登录</div>
+        <img v-if="user.isQingQue" class="logo" :src="img('logo.png')" alt=""/>
+        <div class="tel-login">
+          <van-form @submit="submitFormM">
+            <van-cell-group inset>
+              <van-field
+                style="font-size: 16px;"
+                v-model="loginForm.account"
+                autocomplete="off"
+                left-icon="user-circle-o"
+                placeholder="请输入用户名"
+                :rules="[{ required: true, message: '请填写用户名' }]" />
+              <van-field
+                style="font-size: 16px;"
+                v-model="loginForm.password"
+                autocomplete="off"
+                left-icon="shield-o"
+                type="password"
+                placeholder="请输入密码"
+                :rules="[{ required: true, message: '请填写密码' }]" />
+            </van-cell-group>
+            <div style="margin: 16px;margin-top: 30px;">
+              <van-button round block color="linear-gradient(to right,#1989FA, #00B0FF)" native-type="submit">
+                登录
+              </van-button>
+            </div>
+          </van-form>
+        </div>
+        <div class="trans" @click="login_typ = 'sms', transWidth = '0vw'">
+          <van-icon name="exchange" />
+          验证码登录
+        </div>
+      </div>
+      <div class="item">
+        <div class="text">欢迎登录</div>
+        <img v-if="user.isQingQue" class="logo" :src="img('logo.png')" alt=""/>
+        <div class="tel-login">
+          <van-form >
+            <van-cell-group inset>
+              <van-field
+                type="tel"
+                style="font-size: 16px;"
+                v-model="loginForm.account"
+                autocomplete="off"
+                left-icon="phone-o"
+                placeholder="请输入手机号"
+                :rules="[{ required: true, message: '请填写手机号' }]" />
+            </van-cell-group>
+            <div style="margin: 16px;margin-top: 30px;">
+              <van-button round block color="linear-gradient(to right,#1989FA, #00B0FF)" :disabled="!can_send || loginForm.account.length!==11" @click="getCaptcha">
+                {{ smsMessage }}
+              </van-button>
+            </div>
+          </van-form>
+        </div>
+        <div class="trans" @click="login_typ = 'pwd', transWidth = '100vw'">
+          <van-icon name="exchange"  />
+          密码登录
+        </div>
+      </div>
+      <div class="item">
+        <div class="text">输入验证码
+          <div class="notice">验证码已发送至{{loginForm.account}}</div>
+        </div>
+        <div class="tel-login">
+          <van-form @submit="submitFormM">
+            <van-password-input
+              :value="loginForm.smsCaptcha"
+              :mask="false"
+              info="请输入6位手机验证码"
+              :error-info="errorInfo"
+              length="6"
+              :gutter="10"
+              :focused="showKeyboard"
+              @focus="showKeyboard = true"
+            />
+            <div style="margin: 16px;margin-top: 30px;">
+              <van-button round block color="linear-gradient(to right,#1989FA, #00B0FF)" :disabled="loginForm.smsCaptcha === undefined || loginForm.smsCaptcha.length!==6" native-type="submit">
+                登录
+              </van-button>
+            </div>
+          </van-form>
+        </div>
+        <div class="trans"  @click="transWidth = '0vw'">
+          <van-icon name="arrow-left"  />
+          返回
+        </div>
+      </div>
+
+    </div>
+  </div>
+  <van-number-keyboard
+    v-model="loginForm.smsCaptcha"
+    :show="showKeyboard"
+    @blur="showKeyboard = false"
+  />
+</template>
+
+<script setup lang="ts">
+import { reactive, ref, onMounted, inject } from 'vue'
+import { Lock, User, Cellphone } from '@element-plus/icons-vue'
+import type { FormInstance, FormRules } from 'element-plus'
+import { getUserLogin, captchaLogin, getCaptchaHttp } from '@/api/index'
+import { showSuccessToast, showFailToast } from 'vant'
+// import '@/utils/md5'
+import Message from '@/utils/Message'
+import checkRestTime from '@/utils/captcha'
+import { useRouter } from 'vue-router'
+import { useStore } from '@/store/index'
+import local from '@/utils/local'
+
+const transWidth = ref('0vw')
+const { user } = useStore()
+const router = useRouter()
+const VALID = 60
+const login_typ = ref<'pwd'| 'sms'>('sms')
+const smsMessage = ref<'获取验证码' | number>('获取验证码')
+const errorInfo = ref<string>('')
+const ruleFormRef = ref<FormInstance>()
+const img:any = inject('img')
+const can_send = ref<boolean>(true)
+interface login {
+    account: string,
+    password: string,
+    timestamp: number | string,
+    smsCaptcha:string | undefined
+}
+const loginForm = reactive<login>({
+  account: '',
+  password: '',
+  timestamp: parseInt(`${Date.now() / 1000}`),
+  smsCaptcha: undefined
+})
+const showKeyboard = ref(false)
+const rules = reactive<FormRules>({
+  account: [
+    { required: true, message: '请输入账号', trigger: 'blur' },
+    { min: 3, max: 255, message: '请正确输入账号', trigger: 'blur' }
+  ],
+  password: [
+    { required: true, message: '请输入密码', trigger: 'blur' },
+    { min: 6, max: 20, message: '请输入6-20位密码', trigger: 'blur,change' }
+  ],
+  smsCaptcha: [
+    { required: true, message: '请输入验证码', trigger: 'blur' }
+  ]
+})
+
+const submitForm = async(formEl: FormInstance | undefined) => {
+  if (!formEl) return
+  await formEl.validate(async(valid, fields) => {
+    if (valid) {
+      if (login_typ.value === 'pwd') {
+        // 登录密码加密
+        let md5Pwd: any = document.querySelector('#md5_password')
+        md5Pwd = md5(md5(md5(loginForm.password)) + loginForm.timestamp)
+
+        const params = {
+          account: loginForm.account,
+          password: md5Pwd,
+          timestamp: loginForm.timestamp
+        }
+        getUserLogin(params).then(async(res) => {
+          // console.log('res---->', res);
+          if (res.data.code === 200) {
+            Message.success('登录成功')
+            // 保存用户信息 和 token
+            sessionStorage.setItem('token', res.data.data.token)
+            sessionStorage.setItem('account', loginForm.account)
+            sessionStorage.setItem('from', '1')
+            await user.getUserProfile()
+            // 跳转我的游戏页面
+            setTimeout(() => {
+              router.push({ path: '/my_game', query: { account: loginForm.account }})
+              local.set('headerPath', '我的')
+            }, 1000)
+          }
+        }).catch(err => {
+          // console.log('err===>', err);
+          Message.error(err.data.msg)
+        })
+      } else if (login_typ.value === 'sms') {
+        const params = {
+          mobile: loginForm.account,
+          captcha: loginForm.smsCaptcha
+        }
+        console.log(params)
+
+        await captchaLogin(params).then(async(res) => {
+          console.log(res)
+          if (res.data.code === 200) {
+            Message.success('登录成功')
+            sessionStorage.setItem('token', res.data.data.token)
+            sessionStorage.setItem('account', loginForm.account)
+            sessionStorage.setItem('from', '2')
+            await user.getUserProfile()
+            setTimeout(() => {
+              router.push({ path: '/my_game', query: { account: loginForm.account }})
+              local.set('headerPath', '我的')
+            }, 1000)
+          }
+        }).catch((error) => {
+          console.log(error)
+          Message.error(error.data.msg)
+        })
+      }
+    } else {
+      console.log('登录失败', fields)
+    }
+  })
+}
+const submitFormM = async() => {
+  console.log(login_typ.value)
+
+  if (login_typ.value === 'pwd') {
+    // 登录密码加密
+    let md5Pwd: any = document.querySelector('#md5_password')
+    md5Pwd = md5(md5(md5(loginForm.password)) + loginForm.timestamp)
+
+    const params = {
+      account: loginForm.account,
+      password: md5Pwd,
+      timestamp: loginForm.timestamp
+    }
+    await getUserLogin(params).then(async(res) => {
+      // console.log('res---->', res);
+      if (res.data.code === 200) {
+        Message.success('登录成功')
+        // 保存用户信息 和 token
+        sessionStorage.setItem('token', res.data.data.token)
+        sessionStorage.setItem('account', loginForm.account)
+        sessionStorage.setItem('from', '1')
+        await user.getUserProfile()
+        // 跳转我的游戏页面
+        setTimeout(() => {
+          router.push({ path: '/my_game', query: { account: loginForm.account }})
+          local.set('headerPath', '我的')
+        }, 1000)
+      }
+    }).catch(err => {
+      // console.log('err===>', err);
+      errorInfo.value = err.data.msg
+      showFailToast(err.data.msg)
+    })
+  } else if (login_typ.value === 'sms') {
+    const params = {
+      mobile: loginForm.account,
+      captcha: loginForm.smsCaptcha
+    }
+    console.log(params)
+
+    await captchaLogin(params).then(async(res) => {
+      console.log(res)
+      if (res.data.code === 200) {
+        showSuccessToast('登录成功')
+        localStorage.setItem('token', res.data.data.token)
+        localStorage.setItem('account', loginForm.account)
+        localStorage.setItem('from', '2')
+        await user.getUserProfile()
+        setTimeout(() => {
+          router.push({ path: '/my_game', query: { account: loginForm.account }})
+          local.set('headerPath', '我的')
+        }, 1000)
+      }
+    }).catch((error) => {
+      errorInfo.value = error.data.msg
+      showFailToast(error.data.msg)
+    })
+  }
+}
+const resetForm = (formEl: FormInstance | undefined) => {
+  if (!formEl) return
+  formEl.resetFields()
+}
+
+const getCaptcha = async() => {
+  if (loginForm.account.length !== 11) {
+    return Message.error('请输入有效手机号!')
+  }
+  await getCaptchaHttp({ mobile: loginForm.account }).then((res) => {
+    console.log(res)
+    can_send.value = false
+    smsMessage.value = VALID
+    settimes()
+    sessionStorage.setItem('LOGIN_CAPTCHA', Math.round(new Date().getTime() / 1000).toString())
+    Message.success('验证码已发送,请注意查收')
+    transWidth.value = '-100vw'
+  }).catch((error) => {
+    console.log(error)
+    Message.error(error.data.msg)
+  })
+}
+const settimes = () => {
+  var interval
+  const setTimeFn = () => {
+    (smsMessage.value as number)--
+    if (smsMessage.value as number < 0 || can_send.value === true) {
+      clearInterval(interval)
+      can_send.value = true
+      smsMessage.value = '获取验证码'
+    }
+  }
+  interval = setInterval(function() {
+    setTimeFn()
+  }, 1000)
+}
+onMounted(() => {
+  const res = checkRestTime('LOGIN_CAPTCHA', VALID)
+  if (res) {
+    smsMessage.value = res
+    can_send.value = false
+    settimes()
+  }
+})
+
+</script>
+
+<style lang="scss" scoped>
+.bg{
+  width: 100%;
+  height: 29rem;
+  background: #EBEEFD url('../assets/img/pcbg.svg') no-repeat left;
+  background-position: 4rem;
+  background-size: cover;
+  border-radius: .8rem;
+  overflow: hidden;
+  margin-top: 3rem;
+  position: relative;
+  .login-box{
+    position: absolute;
+    width: 22rem;
+    height: 17.5rem;
+    left: 3rem;
+    top: 50%;
+    transform: translateY(-45%);
+    background-color: rgba($color: #fff, $alpha: 0.5);
+    border-radius: .5rem;
+    box-sizing: border-box;
+    padding: 2rem 1rem 0 1rem;
+    .submit_btn{
+      width: 100%;
+    }
+  }
+}
+.mob-login{
+  display: none;
+  width: 100%;
+  height: 100%;
+  min-height: calc(100vh - 60px);
+  background: #EBEEFD url('../assets/img/mobbg.png') no-repeat center;
+  background-size: cover;
+  // position: relative;
+  // align-items: center;
+  padding-top: 70px;
+  justify-content: center;
+  box-sizing: border-box;
+  .login-box{
+    width: 300vw;
+    height: 400px;
+    border-radius: 15px;
+    padding: 20px 10px;
+    box-sizing: border-box;
+    // padding-left: 7.5vw;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    transition:  all 300ms ease;
+    .item{
+      width: 100vw;
+      // margin-right: 7.5vw;
+      padding: 0 7.5vw;
+    }
+    .logo{
+      width: 150px;
+      display: block;
+      margin: 30px auto;
+    }
+    .text{
+      font-size: 25px;
+      margin-bottom: 40px;
+      border-bottom: 5px solid #99ccff;
+      border-radius: 10px;
+      padding-bottom: 5px;
+      width: fit-content;
+      font-family: 'youshe';
+      font-weight: 400;
+      position: relative;
+      box-sizing: border-box;
+      .notice{
+        position: absolute;
+        width: 80vw;
+        height: 20px;
+        bottom: -30px;
+        left: 0;
+        font-family:Arial, Helvetica, sans-serif;
+        font-size: 12px;
+        color: rgba($color: #000000, $alpha: .8);
+      }
+    }
+    .trans{
+      margin-top: 30px;
+    }
+    .tel-login{
+
+    }
+    .pwd-login{
+      transform: translateX(-100vw);
+    }
+
+  }
+}
+</style>

+ 272 - 0
src/views/mine.vue

@@ -0,0 +1,272 @@
+<template>
+  <div class="mine">
+    <ul v-if="token">
+      <li class="hot-item" v-for="(item,index) in mineGame" :key="index">
+        <img :src="prefix + item.logopic" class="game-logo" :onerror="logoErrorFun" alt=""/>
+        <div class="detail">
+          <div class="top">
+            <span class="game_title ellip">{{ item.screen_name }}</span>
+            <el-tag  type="success">{{ (item.game_score / 10) > 5 ? '5.0' : (item.game_score /
+              10).toFixed(1)
+            }}</el-tag>
+          </div>
+          <div class="desc ellip">{{ item.sketch || '暂无简介' }}</div>
+          <el-button type="info" :disabled="item.download_url === '' ? true : false" @click="downGame(item.download_url)"  class="pcbt" :icon="Download">
+            {{ bytesChange(item.size) }}
+          </el-button>
+          <el-button type="info" @click="showGifts(item)" class="pcbt">查看礼包</el-button>
+        </div>
+        <div class="right">
+          <van-button size="small" round color="#ed8c0f" @click="showGifts2(item)">查看礼包</van-button>
+          <van-button size="small" :disabled="item.download_url === '' ? true : false"  round color="#ed8c0f" @click="downGame(item.download_url)">下载</van-button>
+        </div>
+        <img :src="prefix + item.screenshot" class="game-poster" :onerror="posterErrorFun" alt=""/>
+      </li>
+    </ul>
+    <div class="more-action">
+      <el-button v-if="params.page*params.pagesize < total" type="info" @click="loadMore">更多</el-button>
+      <el-divider v-else>没有更多了</el-divider>
+    </div>
+  </div>
+
+  <div v-if="!token" :style="{ height: viewWidth + 'px' }" class="df fdc aic jcc">
+    <el-empty description="您还未登录!登录后可查询数据!">
+      <el-button style="color:#fff" color="#ed8c0f" @click="goLogin">去登录 &gt;&gt;</el-button>
+    </el-empty>
+  </div>
+
+  <el-dialog  v-model="dialogVisible" title="游戏礼包" width="80%">
+    <el-collapse v-if="giftData.length" v-model="activeName" accordion>
+      <div v-for="(item, index) in giftData" :key="index" style="display: flex;justify-content: space-between; align-items: center;">
+        <el-collapse-item :title="item.gift_name" :name="item.id" style="width: 70%;">
+          <el-form label-width="120px">
+            <el-form-item label="礼包内容:">{{ item.gift_content }}</el-form-item>
+            <el-form-item label="使用方法:">{{ item.usage }}</el-form-item>
+            <el-form-item label="有效期:">{{ item.start_at+'-'+item.end_at }}</el-form-item>
+          </el-form>
+        </el-collapse-item>
+        <div style="width: 20%;">
+          <el-button :disabled="item.user_get_at !== ''" color="#ed8c0f" style="color: #fff;" @click="receiveGift(item.id)">领取礼包</el-button>
+        </div>
+      </div>
+
+    </el-collapse>
+    <el-empty v-else description="暂无礼包" />
+
+  </el-dialog>
+  <van-dialog v-model:show="dialogVisible2" title="查看礼包" confirm-button-text="关闭">
+    <van-collapse v-if="giftData.length" v-model="activeNames">
+      <van-collapse-item v-for="(item, index) in giftData" :key="index" :title="item.gift_name" :name="item.id">
+        <template #value>
+          <van-button size="small" :disabled="item.user_get_at !== ''" round color="#ed8c0f" @click="receiveGift(item.id)">领取礼包</van-button>
+        </template>
+        <van-cell-group>
+          <van-cell value-class="value-length" center size="large" title="礼包内容:" :value="item.gift_content">
+            <template #value>
+              <div style="text-align: left;">
+                {{ item.gift_content }}
+              </div>
+            </template>
+          </van-cell>
+          <van-cell value-class="value-length" center size="large" title="使用方法:" :value="item.usage">
+            <template #value>
+              <div style="text-align: left;">
+                {{ item.usage }}
+              </div>
+            </template>
+          </van-cell>
+          <van-cell value-class="value-length" center size="large" title="有效期:" :value="item.start_at+'-'+item.end_at">
+            <template #value>
+              <div style="text-align: left;">
+                {{ item.start_at+'-'+item.end_at }}
+              </div>
+            </template>
+          </van-cell>
+        </van-cell-group>
+      </van-collapse-item>
+
+    </van-collapse>
+    <van-empty v-else description="暂无礼包" />
+  </van-dialog>
+</template>
+
+<script setup lang="ts">
+import { onMounted, ref, reactive, inject } from 'vue'
+import { getMineGame, getGiftHttp, receiveGiftHttp } from '@/api/index'
+import { useStore } from '@/store/index'
+import { useRouter, useRoute } from 'vue-router'
+import Message from '@/utils/Message'
+import local from '@/utils/local'
+import { bytesChange } from '@/utils/bytesFormatter'
+import { Download } from '@element-plus/icons-vue'
+
+const { user } = useStore()
+const router = useRouter()
+const img:any = inject('img')
+const route = useRoute()
+// const header = useStore('header')
+const dialogVisible = ref<boolean>(false)
+const dialogVisible2 = ref<boolean>(false)
+const mineGame: any = ref([])
+const prefix = ref<string>('')
+const total = ref<number>(0)
+const isLoading = ref<boolean>(true)
+const viewWidth = ref<number>(document.documentElement.clientHeight - 80)
+const userAccount = ref<string | null>(sessionStorage.getItem('account'))
+const token = ref<string | null>(sessionStorage.getItem('token'))
+const activeName = ref('1')
+interface Params {
+    page: number,
+    pagesize: number,
+    account: string | null
+}
+const params = reactive<Params>({
+  page: 1,
+  pagesize: 10,
+  account: userAccount.value
+})
+onMounted(() => {
+  // 进入页面刷新一次
+  if (token.value) {
+    if (route.query.account || userAccount.value) {
+      getMyGame(params)
+    }
+  }
+  user.getUserProfile()
+})
+
+const getMyGame = async(params:Params) => {
+  // console.log('params', params);
+  await getMineGame(params).then(res => {
+    // console.log('res========>', res);
+    if (res.data.code === 200 && res.data.data) {
+      mineGame.value = mineGame.value.concat(res.data.data.lists)
+      prefix.value = res.data.data.prefix
+      total.value = res.data.data.total
+      isLoading.value = false
+    }
+  }).catch(err => {
+    Message.error(err.data.msg)
+  })
+}
+
+// 加载更多
+const loadMore = () => {
+  params.page += 1
+  getMyGame(params)
+}
+
+const goLogin = () => {
+  router.push({ path: 'p_login' })
+  local.remove('headerPath')
+}
+const giftData = ref<any[]>([])
+const showGifts = async(row:any) => {
+//    console.log(row);
+  await getGiftHttp({ game_id: row.game_id }).then((res) => {
+    // console.log(res.data.data)
+    giftData.value = res.data.data || []
+    dialogVisible.value = true
+  }).catch((error) => {
+    console.log(error)
+  })
+}
+const showGifts2 = async(row:any) => {
+//    console.log(row);
+  await getGiftHttp({ game_id: row.game_id }).then((res) => {
+    // console.log(res.data.data)
+    giftData.value = res.data.data || []
+    dialogVisible2.value = true
+  }).catch((error) => {
+    console.log(error)
+  })
+}
+const receiveGift = async(id:number) => {
+  await receiveGiftHttp({ id }).then((res) => {
+    console.log(res)
+    Message.error(res.data.msg)
+    dialogVisible.value = false
+  }).catch((error) => {
+    console.log(error)
+    Message.error(error.data.msg)
+  })
+}
+const posterErrorFun = (event:any) => {
+  var imgDom = event.srcElement
+  imgDom.src = img('defaultPoster.png')
+  imgDom.onerror = null
+}
+const logoErrorFun = (event:any) => {
+  var imgDom = event.srcElement
+  imgDom.src = img('default.png')
+  imgDom.onerror = null
+}
+const downGame = (url: string) => {
+  // 下载游戏
+  window.open(url)
+  // window.location.href = url;
+}
+const activeNames = ref(['1'])
+
+</script>
+
+<style scoped lang="scss">
+.mine{
+  width: 100%;
+  background-color: #fff;
+  padding: 1rem;
+  box-sizing: border-box;
+  .hot-item{
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    height: 8rem;
+    overflow: hidden;
+    margin-bottom: .7rem;
+    border-bottom: .01rem solid #ddd;
+    box-sizing: border-box;
+    padding: .8rem 0;
+    .game-logo{
+      width: 6.5rem;
+      height: 6.5rem;
+      border-radius: .5rem;
+      overflow: hidden;
+      flex-shrink: 0;
+      margin-right: 2rem;
+    }
+    .detail{
+      flex: 1;
+      .top{
+        display: flex;
+        align-items: center;
+        .game_title{
+          display: inline-block;
+          margin-right: 0.5rem;
+        }
+      }
+      .desc{
+          font-size: 0.7rem;
+          color: #666;
+          line-height: 1.3rem;
+          margin: 0.5rem 0;
+        }
+    }
+    .right{
+      height: 100%;
+      // display: flex;
+      display: none;
+      align-items: center;
+      justify-content: space-around;
+      flex-direction: column;
+    }
+    .game-poster{
+      width: 11rem;
+      height: 6.5rem;
+    }
+  }
+}
+.more-action {
+    margin: 1rem auto;
+    text-align: center;
+}
+</style>

+ 127 - 0
src/views/ncoin.vue

@@ -0,0 +1,127 @@
+<template>
+  <div class="rest-list">
+    <div class="list-wrapper">
+      <el-table :data="gameHot" style="width: 100%">
+        <el-table-column prop="game_name" label="游戏名称"  />
+        <el-table-column prop="total_coin" label="余额">
+          <template #default="scope">
+            <div>
+              {{ (scope.row.total_coin/100).toFixed(2)}}
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column prop="created_at" label="创建时间" width="180" />
+        <el-table-column prop="modified_at" label="修改时间" width="180" />
+      </el-table>
+      <div class="pagination-block">
+        <el-pagination
+          v-model:current-page="pageConfig.page"
+          hide-on-single-page
+          v-model:page-size="pageConfig.pagesize"
+          layout="prev, pager, next, jumper,total"
+          :total="pageConfig.total"
+          @current-change="handleCurrentChange"
+        />
+      </div>
+    </div>
+  </div>
+  <div class="mob-list">
+    <vant-list ref="vanList" :param="{}" :pagination="pageConfig" :request="userNcoin" @resetPage="resetPage" @pagePlus="addPage">
+      <template #listItem="listItem">
+        <div class="item" v-for="(item, index) in listItem.row.lists" :key="index">
+          <van-cell-group inset >
+            <van-cell title="游戏名称" :value="item.game_name" />
+            <van-cell title="余额" :value="(item.total_coin/100).toFixed(2)" />
+            <van-cell title="创建时间" :value="item.created_at" />
+            <van-cell title="修改时间" :value="item.modified_at" />
+          </van-cell-group>
+        </div>
+      </template>
+    </vant-list>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, onMounted, computed, inject } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
+import { useStore } from '@/store'
+import { showSuccessToast, showFailToast } from 'vant'
+import Message from '@/utils/Message'
+import { userNcoin } from '@/api/index'
+import vantList from '@/components/vantList.vue'
+import type { ListInstance } from 'vant'
+const img:any = inject('img')
+const router = useRouter()
+const route = useRoute()
+const { user } = useStore()
+// showSuccessToast('成功文案')
+// showFailToast('失败文案')
+const gameHot: any = ref([])
+const isLoading = ref<boolean>(true)
+const vanList = ref<any>()
+const pageConfig = reactive({
+  page: 1,
+  pagesize: 10,
+  total: 0
+})
+const handleCurrentChange = (val: number) => {
+  console.log(`current page: ${val}`)
+  pageConfig.page = val
+  getTableData()
+}
+const getTableData = async() => {
+  const params = {
+    page: pageConfig.page,
+    pagesize: pageConfig.pagesize
+  }
+  await userNcoin(params).then(res => {
+    console.log('热门游戏', res.data)
+    if (res.data.code === 200 && res.data.data) {
+      gameHot.value = res.data.data.lists || []
+      isLoading.value = false
+      pageConfig.total = res.data.data.total || 0
+    }
+  }).catch(err => {
+    console.log(err)
+
+    Message.error(err.data.msg)
+  })
+}
+const resetPage = () => {
+  pageConfig.page = 1
+}
+const addPage = () => {
+  pageConfig.page += 1
+}
+onMounted(async() => {
+  getTableData()
+  if (vanList.value) {
+    vanList.value.onLoad()
+  }
+})
+
+</script>
+
+<style lang="scss" scoped>
+.rest-list{
+  width: 100%;
+  padding: 3rem;
+  box-sizing: border-box;
+  background-color: #fff;
+  min-height: 40rem;
+  .pagination-block{
+    padding-top: 1rem;
+    display: flex;
+    flex-direction: row-reverse;
+  }
+}
+.mob-list{
+  display: none;
+  width: 100%;
+  padding: 10px 0px;
+  box-sizing: border-box;
+  .item{
+    margin-bottom: 10px;
+  }
+}
+</style>

+ 159 - 0
src/views/order.vue

@@ -0,0 +1,159 @@
+<template>
+  <div class="rest-list">
+    <div class="list-wrapper">
+      <el-table v-loading="isLoading" :data="gameHot" style="width: 100%" :stripe="true">
+        <el-table-column prop="order_number" label="订单号" width="180"/>
+        <el-table-column prop="appname" label="游戏名称" width="120" />
+        <el-table-column prop="game_product_name" label="道具名称" width="120" />
+        <el-table-column prop="order_amount" label="道具数量" width="120" />
+        <el-table-column prop="order_submit_time" label="创建时间" width="180">
+          <template #default="scope">
+            <div>
+              {{ parseTime(scope.row.order_submit_time, '') }}
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column prop="order_asynch_time" label="付款时间" width="180">
+          <template #default="scope">
+            <div>
+              {{ scope.row.order_asynch_time? parseTime(scope.row.order_asynch_time, ''):'-' }}
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column prop="channel_name" label="支付方式" width="120" />
+        <el-table-column prop="order_submit_time" label="订单状态" width="120" >
+          <template #default="scope">
+            <div>
+              {{ status[scope.row.order_status] }}
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column prop="target_role_id" label="游戏订单号" width="190"/>
+        <el-table-column prop="out_trade_no" label="支付平台订单号" width="260" />
+      </el-table>
+      <div class="pagination-block">
+        <el-pagination
+          v-model:current-page="pageConfig.page"
+          hide-on-single-page
+          v-model:page-size="pageConfig.pagesize"
+          layout="prev, pager, next, jumper,total"
+          :total="pageConfig.total"
+          @current-change="handleCurrentChange"
+        />
+      </div>
+    </div>
+  </div>
+  <div class="mob-list">
+    <vant-list ref="vanList" :param="{}" :pagination="pageConfig" :request="userOrder" @resetPage="resetPage" @pagePlus="addPage">
+      <template #listItem="listItem">
+        <div class="item" v-for="(item, index) in listItem.row.lists" :key="index">
+          <van-cell-group inset >
+            <van-cell title="游戏名称" :value="item.appname" />
+            <van-cell title="订单号" :value="item.order_number" />
+            <van-cell title="游戏订单号" :value="item.target_role_id" />
+            <van-cell title="支付平台订单号" :value="item.out_trade_no" />
+            <van-cell title="道具名称" :value="item.game_product_name" />
+            <van-cell title="道具数量" :value="item.order_amount" />
+            <van-cell title="支付方式" :value="item.channel_name" />
+            <van-cell title="订单状态" :value="status[item.order_status]" />
+            <van-cell title="创建时间" :value="parseTime(item.order_submit_time, '')" />
+            <van-cell title="付款时间" :value="parseTime(item.order_asynch_time, '')" />
+          </van-cell-group>
+        </div>
+      </template>
+    </vant-list>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, onMounted, computed, inject } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
+import { useStore } from '@/store'
+import { showSuccessToast, showFailToast } from 'vant'
+import Message from '@/utils/Message'
+import { userOrder } from '@/api/index'
+import vantList from '@/components/vantList.vue'
+import { parseTime } from '@/utils/index'
+
+const img:any = inject('img')
+const router = useRouter()
+const route = useRoute()
+const { user } = useStore()
+// showSuccessToast('成功文案')
+// showFailToast('失败文案')
+const gameHot: any = ref([])
+const isLoading = ref<boolean>(false)
+const vanList = ref<any>()
+const token = ref<string | null>(localStorage.getItem('token'))
+// const user = useStore('user')
+
+const pageConfig = reactive({
+  page: 1,
+  pagesize: 10,
+  total: 0
+})
+const handleCurrentChange = (val: number) => {
+  console.log(`current page: ${val}`)
+  pageConfig.page = val
+  getTableData()
+}
+const status = ref<any[]>([])
+const getTableData = async() => {
+  const params = {
+    page: pageConfig.page,
+    pagesize: pageConfig.pagesize
+  }
+  isLoading.value = true
+  await userOrder(params).then(res => {
+    console.log('热门游戏', res.data)
+    if (res.data.code === 200 && res.data.data) {
+      gameHot.value = res.data.data.lists || []
+      isLoading.value = false
+      pageConfig.total = res.data.data.total.total || 0
+      status.value = res.data.data.status
+    }
+  }).catch(err => {
+    console.log(err)
+
+    Message.error(err.data.msg)
+  })
+}
+
+const resetPage = () => {
+  pageConfig.page = 1
+}
+const addPage = () => {
+  pageConfig.page += 1
+}
+onMounted(async() => {
+  getTableData()
+  if (vanList.value) {
+    vanList.value.onLoad()
+  }
+})
+
+</script>
+
+<style lang="scss" scoped>
+.rest-list{
+  width: 100%;
+  padding: 3rem;
+  box-sizing: border-box;
+  background-color: #fff;
+  min-height: 40rem;
+  .pagination-block{
+    padding-top: 1rem;
+    display: flex;
+    flex-direction: row-reverse;
+  }
+}
+.mob-list{
+  display: none;
+  width: 100%;
+  padding: 10px 0px;
+  box-sizing: border-box;
+  .item{
+    margin-bottom: 10px;
+  }
+}
+</style>

+ 150 - 0
src/views/search.vue

@@ -0,0 +1,150 @@
+<template>
+  <div class="search_cont">
+    <div class="search_key">
+      <van-search
+        v-model="search.params.keywords"
+        placeholder="请输入游戏关键词"
+        shape="round"
+        show-action
+        clearable
+        @search="search.onSearch">
+        <template #action>
+          <div @click="search.onSearch">搜索</div>
+        </template>
+      </van-search>
+    </div>
+    <ul v-if="search.searchList.length > 0">
+      <li class="hot-item" v-for="(item,index) in search.searchList" :key="index">
+        <img :src="search.prefix + item.logopic" class="game-logo" :onerror="logoErrorFun" alt=""/>
+        <div class="detail">
+          <div class="top">
+            <span class="game_title ellip">{{ item.screen_name }}</span>
+            <el-tag size="small" type="success">{{ (item.game_score / 10) > 5 ? '5.0' : (item.game_score /
+              10).toFixed(1)
+            }}</el-tag>
+          </div>
+          <div class="desc ellip">{{ item.sketch || '暂无简介' }}</div>
+          <el-button type="info" :disabled="item.download_url === '' ? true : false" @click="downGame(item.download_url)"  class="pcbt" :icon="Download">
+            {{ bytesChange(item.size) }}
+          </el-button>
+        </div>
+        <div class="right">
+          <van-button size="small" type="success" :disabled="item.download_url === '' ? true : false"  round  @click="downGame(item.download_url)">下载</van-button>
+        </div>
+        <img :src="search.prefix + item.screenshot" class="game-poster" :onerror="posterErrorFun" alt=""/>
+      </li>
+      <div class="more-action">
+        <el-button v-if="search.params.page*search.params.pagesize < search.total" type="info" @click="search.loadMore">更多</el-button>
+        <el-divider v-else>没有更多了</el-divider>
+      </div>
+    </ul>
+    <van-empty v-else description="No Data" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, onMounted, computed, inject } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
+import { useStore } from '@/store'
+import { showSuccessToast, showFailToast } from 'vant'
+import Message from '@/utils/Message'
+import { getGameList } from '@/api/index'
+import { bytesChange } from '@/utils/bytesFormatter'
+import { Download } from '@element-plus/icons-vue'
+
+const img:any = inject('img')
+const router = useRouter()
+const route = useRoute()
+const { user } = useStore()
+const { search } = useStore()
+// showSuccessToast('成功文案')
+// showFailToast('失败文案')
+
+const posterErrorFun = (event:any) => {
+  var imgDom = event.srcElement
+  imgDom.src = img('defaultPoster.png')
+  imgDom.onerror = null
+}
+const logoErrorFun = (event:any) => {
+  var imgDom = event.srcElement
+  imgDom.src = img('default.png')
+  imgDom.onerror = null
+}
+const downGame = (url: string) => {
+  // 下载游戏
+  window.open(url)
+  // window.location.href = url;
+}
+onMounted(() => {
+
+})
+
+</script>
+
+<style lang="scss" scoped>
+.search_cont{
+  width: 100%;
+  padding: 0 20px;
+  box-sizing: border-box;
+  background-color: #fff;
+  min-height: 40rem;
+  .search_key{
+    display: none;
+  }
+  .hot-item{
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    height: 8rem;
+    overflow: hidden;
+    margin-bottom: .7rem;
+    border-bottom: .01rem solid #ddd;
+    box-sizing: border-box;
+    padding: .8rem 0;
+    width: 100%;
+    .game-logo{
+      width: 6.5rem;
+      height: 6.5rem;
+      border-radius: .5rem;
+      overflow: hidden;
+      flex-shrink: 0;
+      margin-right: 2rem;
+    }
+    .detail{
+      // flex: 1;
+      width: 25rem;
+      .top{
+        display: flex;
+        align-items: center;
+        .game_title{
+          display: inline-block;
+          margin-right: 0.5rem;
+        }
+      }
+      .desc{
+          font-size: 0.7rem;
+          color: #666;
+          line-height: 1.3rem;
+          margin: 0.5rem 0;
+        }
+    }
+    .right{
+      height: 100%;
+      // display: flex;
+      display: none;
+      align-items: center;
+      justify-content: space-around;
+      flex-direction: column;
+    }
+    .game-poster{
+      width: 11rem;
+      height: 6.5rem;
+      display: block;
+    }
+  }
+  .more-action {
+    margin: 1rem auto;
+    text-align: center;
+  }
+}
+</style>

+ 563 - 0
src/views/settings.vue

@@ -0,0 +1,563 @@
+<template>
+  <div class="settings">
+    <el-tabs tab-position="left">
+      <el-tab-pane label="密码设置">
+        <div class="form-wrapper">
+          <el-form
+            ref="ruleFormRef"
+            :model="ruleForm"
+            :rules="rules"
+            label-width="140px"
+            class="demo-ruleForm"
+            status-icon
+          >
+            <el-form-item label="旧密码:" v-if="from === 1" prop="old_password">
+              <el-input type="password" v-model="ruleForm.old_password" placeholder="请输入旧密码" />
+            </el-form-item>
+            <el-form-item label="新密码:" prop="new_password">
+              <el-input type="password" v-model="ruleForm.new_password" placeholder="请输入新密码" @input="blurValidate(ruleFormRef)" />
+            </el-form-item>
+            <el-form-item label="再次输入新密码:" prop="new_password2">
+              <el-input type="password" v-model="ruleForm.new_password2" placeholder="请再次输入新密码" @input="blurValidate(ruleFormRef)" />
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" color="#ed8c0f" style="color: #fff;" size="large" @click="submitForm(ruleFormRef)">
+                提交
+              </el-button>
+            </el-form-item>
+          </el-form>
+        </div>
+      </el-tab-pane>
+      <el-tab-pane label="用户名设置">
+        <div class="form-wrapper">
+          <el-form
+            ref="ruleFormRef2"
+            :model="ruleForm2"
+            :rules="rules2"
+            label-width="140px"
+            class="demo-ruleForm"
+            status-icon
+          >
+            <el-form-item label="用户名:" prop="account">
+              <el-input type="text" :disabled="!!user.profile.user_name" v-model="ruleForm2.account" placeholder="请输入用户名" />
+            </el-form-item>
+            <el-form-item v-if="!user.profile.user_name">
+              <el-button type="primary" color="#ed8c0f" style="color: #fff;" size="large" @click="submitForm2(ruleFormRef2)">
+                提交
+              </el-button>
+            </el-form-item>
+          </el-form>
+        </div>
+      </el-tab-pane>
+      <el-tab-pane label="手机号设置">
+        <div class="form-wrapper">
+          <el-form
+            ref="ruleFormRef3"
+            :model="ruleForm3"
+            :rules="rules3"
+            label-width="140px"
+            class="demo-ruleForm"
+            status-icon
+          >
+            <el-form-item label="手机号:" prop="mobile">
+              <el-input type="text" :disabled="!!user.profile.mobile" v-model="ruleForm3.mobile" placeholder="请输入手机号" />
+            </el-form-item>
+            <el-form-item label="验证码:" prop="captcha">
+              <el-input type="text" :disabled="ruleForm3.mobile.length !== 11" v-model="ruleForm3.captcha" placeholder="请输入验证码" >
+                <template #append>
+                  <el-button v-if="!!user.profile.mobile" @click="getCaptcha" style="width: 100px;" :disabled ="!can_send">{{smsMessage}}</el-button>
+                  <el-button v-else @click="getCaptcha2" style="width: 100px;" :disabled ="!can_send">{{smsMessage}}</el-button>
+                </template>
+              </el-input>
+            </el-form-item>
+            <el-form-item >
+              <el-button v-if="!!user.profile.mobile" type="primary" color="#ed8c0f" style="color: #fff;" size="large" @click="submitForm3(ruleFormRef3)">
+                解绑
+              </el-button>
+              <el-button v-else type="primary" color="#ed8c0f" style="color: #fff;" size="large" @click="submitForm4(ruleFormRef3)">
+                绑定
+              </el-button>
+            </el-form-item>
+          </el-form>
+        </div>
+      </el-tab-pane>
+    </el-tabs>
+
+  </div>
+  <div class="mob-setting">
+    <div class="info-wrap">
+      <div class="row-wrap">
+        <div class="img-outer">
+          <img class="avatar" :src="img('gamer.png')" alt=""/>
+        </div>
+        <div class="info">
+          <div class="account" v-if="user.profile.user_name" >{{ user.profile.user_name }}</div>
+          <div class="account" v-else>未设置</div>
+          <div class="mobile" v-if="user.profile.mobile">{{user.profile.mobile}}</div>
+          <div class="mobile" v-else>未绑定</div>
+        </div>
+      </div>
+      <div class="menu">
+        <van-tabs v-model:active="active" animated color="#BD5B8F" background="transparent" title-inactive-color="#2F4858" title-active-color="">
+          <van-tab title="密码设置" name="a"/>
+          <van-tab title="用户名设置" name="b"/>
+          <van-tab title="手机号设置" name="c"/>
+        </van-tabs>
+      </div>
+    </div>
+    <div v-if="active === 'a'" class="form-wrapper">
+      <van-form @submit="onSubmit">
+        <van-field
+          v-if="from === 1"
+          v-model="ruleForm.old_password"
+          name="旧密码"
+          label="旧密码"
+          type="password"
+          placeholder="请填写旧密码"
+          :rules="[{ required: true, message: '请填写旧密码' },{ validator: checkAgeMob}]"
+        />
+        <van-field
+          v-model="ruleForm.new_password"
+          type="password"
+          name="新密码"
+          label="新密码"
+          placeholder="请填写新密码"
+          :rules="[{ required: true, message: '请填写新密码' },{ validator: checkAgeMob}]"
+        />
+        <van-field
+          v-model="ruleForm.new_password2"
+          type="password"
+          name="新密码"
+          label="新密码"
+          placeholder="请再次填写新密码"
+          :rules="[{ required: true, message: '请再次填写新密码' },{ validator: checkAgeMob}]"
+        />
+        <div style="margin: 16px;display: flex;flex-direction: row-reverse;">
+          <van-button style="width: 100%;padding: 10px;" round block type="primary" color="#ed8c0f" native-type="submit">
+            提交
+          </van-button>
+        </div>
+      </van-form>
+    </div>
+    <div v-else-if="active === 'b'" class="form-wrapper">
+      <van-form @submit="onSubmit2">
+        <van-field
+          v-model="account"
+          type="text"
+          name="用户名"
+          :readonly="!!user.profile.user_name"
+          label="用户名"
+          placeholder="请填写用户名"
+          :rules="[{ required: true, message: '请填写用户名',trigger: 'onBlur' },{ pattern: pattern, message: '请输入以字母为开头,长度为6-20位的用户名'}]"
+        />
+        <div v-if="!user.profile.user_name" style="margin: 16px;display: flex;flex-direction: row-reverse;">
+          <van-button round block style="width: 100%;padding: 10px;" type="primary" color="#ed8c0f" native-type="submit">
+            提交
+          </van-button>
+        </div>
+      </van-form>
+    </div>
+    <div v-else-if="active === 'c'" class="form-wrapper">
+      <van-form>
+        <van-field
+          v-model="ruleForm3.mobile"
+          type="text"
+          name="手机号"
+          :readonly="!!user.profile.mobile"
+          label="手机号"
+          placeholder="请填写手机号"
+          :rules="[{ required: true, message: '请填写手机号',trigger: 'onBlur' },{ pattern: pattern2, message: '请输入正确手机号'}]"
+        />
+        <van-field
+          v-model="ruleForm3.captcha"
+          type="number"
+          name="验证码"
+          label="验证码"
+          placeholder="请输入验证码"
+          :rules="[{ required: true, message: '请填写验证码' }]">
+          <template #button>
+            <van-button v-if="!!user.profile.mobile" color="#ed8c0f" size="small" type="primary" style="min-width: 20vw;" :disabled="!can_send" @click="getCaptcha">{{ smsMessage }}</van-button>
+            <van-button v-else size="small" color="#ed8c0f" type="primary" style="min-width: 20vw;" :disabled="!can_send" @click="getCaptcha2">{{ smsMessage }}</van-button>
+          </template>
+        </van-field>
+        <div style="margin: 16px;">
+          <van-button v-if="!!user.profile.mobile" style="width: 100%;padding: 10px;" round block type="primary" color="#ed8c0f" @click="onSubmit3">
+            解绑
+          </van-button>
+          <van-button v-else round block type="primary" style="width: 100%;padding: 10px;" color="#ed8c0f" @click="onSubmit4">
+            绑定
+          </van-button>
+        </div>
+      </van-form>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, onMounted, inject } from 'vue'
+import type { FormInstance, FormRules } from 'element-plus'
+import { resetPassword, setPassword, userAccount, unbindCaptcha, clearMobile, bindCaptcha, bindMobile } from '@/api/index'
+import Message from '@/utils/Message'
+import { useStore } from '@/store/index'
+import { showSuccessToast, showFailToast } from 'vant'
+
+const { user } = useStore()
+const img:any = inject('img')
+const active = ref('a')
+const ruleFormRef = ref<FormInstance>()
+const ruleFormRef2 = ref<FormInstance>()
+const ruleFormRef3 = ref<FormInstance>()
+interface RuleForm {
+  old_password: string
+  new_password: string
+  new_password2: string
+  account?:string
+}
+const ruleForm = reactive<RuleForm>({
+  old_password: '',
+  new_password: '',
+  new_password2: ''
+})
+const ruleForm2 = reactive({
+  account: ''
+})
+const ruleForm3 = reactive({
+  mobile: '',
+  captcha: ''
+})
+const blurValidate = (formEl: FormInstance | undefined) => {
+  if (!formEl) return
+  if (!ruleForm.new_password || !ruleForm.new_password2) return
+  formEl.validateField(['new_password', 'new_password2']).then((res) => {
+    console.log(res)
+  }).catch((error) => {
+    console.log(error)
+  })
+}
+const checkAge = (rule: any, value: any, callback: any) => {
+  if (!ruleForm.new_password2.length || !ruleForm.new_password.length) {
+    return callback()
+  }
+  if (ruleForm.new_password2 !== ruleForm.new_password) {
+    callback(new Error('两次输入的新密码不一致'))
+  } else {
+    callback()
+  }
+}
+const rules2 = reactive<FormRules>({
+  account: [
+    { required: true, message: '请输入用户名', trigger: 'blur' },
+    { pattern: '^[a-zA-Z][a-zA-Z0-9]{5,19}$', message: '请输入以字母为开头,长度为6-20位的用户名', trigger: 'blur' }
+  ]
+})
+const rules3 = reactive<FormRules>({
+  mobile: [
+    { required: true, message: '请输入手机号', trigger: 'blur' },
+    { pattern: /^(?:(?:\+|00)86)?1[3-9]\d{9}$/, message: '请输入正确手机号', trigger: 'blur' }
+  ],
+  captcha: [
+    { required: true, message: '请输入验证码', trigger: 'blur' }
+  ]
+})
+const rules = reactive<FormRules>({
+  old_password: [
+    { required: true, message: '请输入旧密码', trigger: 'blur' },
+    { min: 6, max: 20, message: '请输入6-20位密码', trigger: 'blur' }
+  ],
+  new_password: [
+    { required: true, message: '请输入新密码', trigger: 'blur' },
+    { validator: checkAge },
+    { min: 6, max: 20, message: '请输入6-20位密码', trigger: 'blur' }
+  ],
+  new_password2: [
+    { required: true, message: '请再次输入新密码', trigger: 'blur' },
+    { validator: checkAge },
+    { min: 6, max: 20, message: '请输入6-20位密码', trigger: 'blur' }
+  ]
+})
+
+const submitForm = async(formEl: FormInstance | undefined) => {
+  if (!formEl) return
+  await formEl.validate(async(valid, fields) => {
+    if (valid) {
+      console.log('submit!')
+      if (from.value === 1) {
+        // 账号密码登录
+        await resetPassword({ old_pass: ruleForm.old_password, new_pass: ruleForm.new_password }).then((res) => {
+          console.log(res)
+          Message.success(res.data.msg)
+        }).catch((error) => {
+          console.log(error)
+          Message.error(error.data.msg)
+        })
+      } else if (from.value === 2) {
+        await setPassword({ new_pass: ruleForm.new_password }).then((res) => {
+          console.log(res)
+          Message.success(res.data.msg)
+        }).catch((error) => {
+          console.log(error)
+          Message.error(error.data.msg)
+        })
+      }
+    } else {
+      console.log('error submit!', fields)
+    }
+  })
+}
+const submitForm2 = async(formEl: FormInstance | undefined) => {
+  if (!formEl) return
+  await formEl.validate(async(valid, fields) => {
+    if (valid) {
+      console.log('submit!')
+      await userAccount({ account: ruleForm2.account.toLowerCase() }).then((res) => {
+        console.log(res)
+        Message.success(res.data.msg)
+        user.getUserProfile()
+      }).catch((error) => {
+        console.log(error)
+        Message.error(error.data.msg)
+      })
+    } else {
+      console.log('error submit!', fields)
+    }
+  })
+}
+const submitForm3 = async(formEl: FormInstance | undefined) => {
+  if (!formEl) return
+  await formEl.validate(async(valid, fields) => {
+    if (valid) {
+      console.log('submit!')
+      await clearMobile({ captcha: ruleForm3.captcha }).then(async(res) => {
+        console.log(res)
+        Message.success(res.data.msg)
+        await user.getUserProfile()
+        ruleForm3.mobile = user.profile.mobile
+        ruleForm3.captcha = ''
+        clearInterval(interval)
+        smsMessage.value = '获取验证码'
+        can_send.value = true
+      }).catch((error) => {
+        console.log(error)
+        Message.error(error.data.msg)
+      })
+    } else {
+      console.log('error submit!', fields)
+    }
+  })
+}
+const submitForm4 = async(formEl: FormInstance | undefined) => {
+  if (!formEl) return
+  await formEl.validate(async(valid, fields) => {
+    if (valid) {
+      console.log('submit!')
+      await bindMobile({ mobile: ruleForm3.mobile, captcha: ruleForm3.captcha }).then(async(res) => {
+        console.log(res)
+        Message.success(res.data.msg)
+        await user.getUserProfile()
+        ruleForm3.mobile = user.profile.mobile
+        ruleForm3.captcha = ''
+        clearInterval(interval)
+        smsMessage.value = '获取验证码'
+        can_send.value = true
+      }).catch((error) => {
+        console.log(error)
+        Message.error(error.data.msg)
+      })
+    } else {
+      console.log('error submit!', fields)
+    }
+  })
+}
+const can_send = ref<boolean>(true)
+const smsMessage = ref<'获取验证码' | number>('获取验证码')
+const VALID = 60
+
+const from = ref<1 | 2>(1)
+const getCaptcha = async() => {
+  await unbindCaptcha().then((res) => {
+    console.log(res)
+    can_send.value = false
+    smsMessage.value = VALID
+    settimes()
+    Message.success('验证码已发送,请注意查收')
+  }).catch((error) => {
+    console.log(error)
+    Message.error(error.data.msg)
+  })
+}
+const getCaptcha2 = async() => {
+  await bindCaptcha({ mobile: ruleForm3.mobile }).then((res) => {
+    console.log(res)
+    can_send.value = false
+    smsMessage.value = VALID
+    settimes()
+    Message.success('验证码已发送,请注意查收')
+  }).catch((error) => {
+    console.log(error)
+    Message.error(error.data.msg)
+  })
+}
+let interval:any
+const settimes = () => {
+  const setTimeFn = () => {
+    (smsMessage.value as number)--
+    if (smsMessage.value as number < 0 || can_send.value === true) {
+      clearInterval(interval)
+      can_send.value = true
+      smsMessage.value = '获取验证码'
+    }
+  }
+  interval = setInterval(function() {
+    setTimeFn()
+  }, 1000)
+}
+
+// mob
+const checkAgeMob = (value:any, rule:any) => {
+  console.log(value, rule)
+  if (value.length < 6 || value.length > 20) {
+    return '请输入6-20位密码'
+  }
+  return ''
+}
+const onSubmit = async() => {
+  if (ruleForm.new_password2 !== ruleForm.new_password) {
+    return showFailToast('两次输入的密码不一致!')
+  }
+  if (from.value === 1) {
+    // 账号密码登录
+    await resetPassword({ old_pass: ruleForm.old_password, new_pass: ruleForm.new_password }).then((res) => {
+      console.log(res)
+      showSuccessToast(res.data.msg)
+    }).catch((error) => {
+      console.log(error)
+      showFailToast(error.data.msg)
+    })
+  } else if (from.value === 2) {
+    await setPassword({ new_pass: ruleForm.new_password }).then((res) => {
+      console.log(res)
+      showSuccessToast(res.data.msg)
+    }).catch((error) => {
+      console.log(error)
+      showFailToast(error.data.msg)
+    })
+  }
+}
+const onSubmit2 = async() => {
+  await userAccount({ account: account.value.toLowerCase() }).then((res) => {
+    console.log(res)
+    showSuccessToast(res.data.msg)
+    user.getUserProfile()
+  }).catch((error) => {
+    console.log(error)
+    showFailToast(error.data.msg)
+  })
+}
+const onSubmit3 = async() => {
+  await clearMobile({ captcha: ruleForm3.captcha }).then(async(res) => {
+    console.log(res)
+    showSuccessToast(res.data.msg)
+    await user.getUserProfile()
+    ruleForm3.mobile = user.profile.mobile
+    ruleForm3.captcha = ''
+    clearInterval(interval)
+    smsMessage.value = '获取验证码'
+    can_send.value = true
+  }).catch((error) => {
+    console.log(error)
+    showFailToast(error.data.msg)
+  })
+}
+const onSubmit4 = async() => {
+  await bindMobile({ mobile: ruleForm3.mobile, captcha: ruleForm3.captcha }).then(async(res) => {
+    console.log(res)
+    showSuccessToast(res.data.msg)
+    await user.getUserProfile()
+    ruleForm3.mobile = user.profile.mobile
+    ruleForm3.captcha = ''
+    clearInterval(interval)
+    smsMessage.value = '获取验证码'
+    can_send.value = true
+  }).catch((error) => {
+    console.log(error)
+    showFailToast(error.data.msg)
+  })
+}
+const pattern = ref<RegExp>(/^[a-zA-Z][a-zA-Z0-9]{5,19}$/)
+const pattern2 = ref<RegExp>(/^(?:(?:\+|00)86)?1[3-9]\d{9}$/)
+const account = ref<string>('')
+onMounted(async() => {
+  from.value = parseInt(localStorage.getItem('from') as string)as 1 | 2
+  await user.getUserProfile()
+  ruleForm2.account = user.profile?.user_name
+  ruleForm3.mobile = user.profile?.mobile
+  account.value = user.profile?.user_name
+})
+
+</script>
+
+<style lang="scss" scoped>
+.settings{
+  width: 100%;
+  background-color: #fff;
+  min-height: 40rem;
+  padding: 3rem;
+  box-sizing: border-box;
+  .form-wrapper{
+    width: 70%;
+    padding-top: 1rem;
+  }
+}
+.mob-setting{
+  display: none;
+  width: 100vw;
+  min-height: calc(100vh - 60px);
+  .info-wrap{
+    width: 100%;
+    height: 50vw;
+    background-image: linear-gradient(#FFE8D2, #C4FCF0);
+    overflow: hidden;
+    position: relative;
+    .row-wrap{
+      display: flex;
+      align-items: center;
+      margin-top: 8vw;
+      padding-left: 5vw;
+    }
+    .img-outer{
+      // margin: 10vw 0 0 5vw;
+      background-color: #fff;
+      border-radius: 50%;
+      width: 24vw;
+      height: 24vw;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      overflow: hidden;
+      box-shadow: 1px 3px 4px rgba($color: #000000, $alpha: .5);
+    }
+    .avatar{
+      width: 20vw;
+      height: 20vw;
+      border-radius: 50%;
+    }
+    .info{
+      margin-left: 2vw;
+      .account{
+        font-size: 14px;
+        margin-bottom: 2vw;
+      }
+      .mobile{
+        font-size: 13px;
+      }
+    }
+  }
+  .menu{
+    width: 100vw;
+    height: 44px;
+    position: absolute;
+    bottom: 1px;
+    left: 0;
+  }
+}
+</style>

+ 24 - 0
src/vite-env.d.ts

@@ -0,0 +1,24 @@
+// / <reference types="vite/client" />
+// declare module '*.vue' {
+//     import type { DefineComponent } from 'vue'
+//     const component: DefineComponent<{}, {}, any>
+//     export default component
+// }
+interface ImportMetaEnv {
+
+    readonly VITE_LOGO: 'QingQue'|'KuPai'
+
+    readonly VITE_API_HOST: string
+
+    readonly VITE_PROJECT_NAME: string
+
+    readonly VITE_ENV: any
+
+}
+
+interface ImportMeta {
+    readonly env: ImportMetaEnv
+}
+
+declare module 'element-plus/dist/locale/zh-cn.mjs'
+// declare module 'vant'

+ 35 - 0
tsconfig.json

@@ -0,0 +1,35 @@
+{
+  "compilerOptions": {
+    "target": "ES2020",
+    "useDefineForClassFields": true,
+    "module": "ESNext",
+    "lib": ["ES2020", "DOM", "DOM.Iterable"],
+    "skipLibCheck": true,
+
+    /* Bundler mode */
+    "moduleResolution": "bundler",
+    "allowImportingTsExtensions": true,
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "noEmit": true,
+    "jsx": "preserve",
+
+    /* Linting */
+    "strict": true,
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "noFallthroughCasesInSwitch": true,
+    "baseUrl": "./",
+    "paths": {
+      "@/*": ["src/*"]
+    }
+  },
+  "include": [
+    "src/**/*.ts",
+    "src/**/*.d.ts",
+    "src/**/*.tsx",
+    "src/**/*.vue",
+    "images.d.ts"
+  ],
+  "references": [{ "path": "./tsconfig.node.json" }]
+}

+ 10 - 0
tsconfig.node.json

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

+ 99 - 0
vite.config.ts

@@ -0,0 +1,99 @@
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+import legacy from '@vitejs/plugin-legacy'
+import { resolve } from 'path'
+import postcsspxtoviewport from 'postcss-px-to-viewport-8-plugin'
+import autoprefixer from 'autoprefixer'
+import Components from 'unplugin-vue-components/vite'
+import { VantResolver } from 'unplugin-vue-components/resolvers'
+
+function pathResolve(dir) {
+  // eslint-disable-next-line no-undef
+  return resolve(__dirname, '.', dir)
+}
+// https://vitejs.dev/config/
+export default defineConfig({
+  plugins: [
+    vue(),
+    Components({
+      resolvers: [VantResolver()]
+    }),
+    legacy({
+      targets: ['defaults', 'not IE 11', 'chrome >= 50', 'UCAndroid >= 12'],
+      additionalLegacyPolyfills: ['regenerator-runtime/runtime'],
+      renderLegacyChunks: true,
+      polyfills: [
+        'es.symbol',
+        'es.array.filter',
+        'es.promise',
+        'es.promise.finally',
+        'es/map',
+        'es/set',
+        'es.array.for-each',
+        'es.object.define-properties',
+        'es.object.define-property',
+        'es.object.get-own-property-descriptor',
+        'es.object.get-own-property-descriptors',
+        'es.object.keys',
+        'es.object.to-string',
+        'web.dom-collections.for-each',
+        'esnext.global-this',
+        'esnext.string.match-all'
+      ]
+    })
+
+  ],
+  server: {
+    host: '0.0.0.0',
+    cors: true,
+    open: true,
+    port: 5066,
+    proxy: {
+      // 跨域前缀写法
+      '/api': {
+        target: 'http://192.168.99.223:3000',
+        changeOrigin: true,
+        rewrite: (path) => path.replace(/^\/api/, '')
+      }
+    }
+  },
+  resolve: {
+    alias: {
+      '@': pathResolve('src') // 路径别名
+    },
+    extensions: ['.js', '.json', '.ts', '.mjs'] // 使用路径别名时想要省略的后缀名,可以自己 增减
+  },
+  css: {
+    postcss: {
+      plugins: [
+        autoprefixer({
+          overrideBrowserslist: [
+            'Android 4.1',
+            'iOS 7.1',
+            'Chrome > 31',
+            'ff > 31',
+            'ie >= 8'
+            // 'last 2 versions', // 所有主流浏览器最近2个版本
+          ],
+          grid: true
+        }),
+        postcsspxtoviewport({
+          unitToConvert: 'px', // 需要转换的单位,默认为"px"
+          viewportWidth: 375, // 设计稿的视口宽度
+          exclude: [/node_modules/], // 解决vant375,设计稿750问题。忽略某些文件夹下的文件或特定文件
+          unitPrecision: 5, // 单位转换后保留的精度
+          propList: ['*'], // 能转化为vw的属性列表
+          viewportUnit: 'vw', // 希望使用的视口单位
+          fontViewportUnit: 'vw', // 字体使用的视口单位
+          selectorBlackList: [], // 需要忽略的CSS选择器,不会转为视口单位,使用原有的px等单位。
+          minPixelValue: 1, // 设置最小的转换数值,如果为1的话,只有大于1的值会被转换
+          mediaQuery: false, // 媒体查询里的单位是否需要转换单位
+          replace: true, //  是否直接更换属性值,而不添加备用属性
+          landscape: false, // 是否添加根据 landscapeWidth 生成的媒体查询条件 @media (orientation: landscape)
+          landscapeUnit: 'vw', // 横屏时使用的单位
+          landscapeWidth: 1125 // 横屏时使用的视口宽度
+        })
+      ]
+    }
+  }
+})