zhb 1 yıl önce
işleme
43fb06bad2
96 değiştirilmiş dosya ile 7636 ekleme ve 0 silme
  1. 3 0
      .browserslistrc
  2. 13 0
      .editorconfig
  3. 3 0
      .env.development
  4. 1 0
      .env.production
  5. 3 0
      .env.staging
  6. 1 0
      .env.test
  7. 349 0
      .eslintrc-auto-import.json
  8. 8 0
      .gitattributes
  9. 32 0
      .gitignore
  10. 3 0
      .npmrc
  11. 0 0
      .prettierignore
  12. 19 0
      .prettierrc
  13. 4 0
      .stylelintignore
  14. 11 0
      .stylelintrc.json
  15. 5 0
      .vscode/settings.json
  16. 21 0
      LICENSE
  17. 281 0
      README.md
  18. 3 0
      delete-ts.sh
  19. 32 0
      eslint.config.ts
  20. 13 0
      index.html
  21. 93 0
      package.json
  22. BIN
      public/favicon.ico
  23. 5 0
      simple-git-hooks.sh
  24. 94 0
      src/App.vue
  25. 11 0
      src/api/index.ts
  26. 278 0
      src/api/ucard.ts
  27. 31 0
      src/api/upload.ts
  28. 39 0
      src/api/user.ts
  29. BIN
      src/assets/logo.png
  30. 249 0
      src/assets/scss/global/global.scss
  31. 138 0
      src/assets/scss/global/reset.scss
  32. 48 0
      src/assets/scss/global/vant.scss
  33. 50 0
      src/assets/scss/pages/about.scss
  34. 98 0
      src/assets/scss/pages/home.scss
  35. 4 0
      src/assets/scss/style.scss
  36. 0 0
      src/assets/svgs/mp3.svg
  37. 686 0
      src/auto-imports.d.ts
  38. 43 0
      src/components.d.ts
  39. 82 0
      src/components/CurrencySelect.vue
  40. 103 0
      src/components/CustomTabbar.vue
  41. 47 0
      src/components/PageHeader.vue
  42. 17 0
      src/components/empty-components.vue
  43. 25 0
      src/components/jsx-components.tsx
  44. 222 0
      src/components/u-loadmore.vue
  45. 7 0
      src/composables/config.ts
  46. 19 0
      src/composables/crypt.ts
  47. 222 0
      src/composables/fetch.ts
  48. 43 0
      src/composables/filters.ts
  49. 304 0
      src/composables/index.ts
  50. 44 0
      src/composables/message.ts
  51. 17 0
      src/composables/provide.ts
  52. 48 0
      src/config/index.ts
  53. 33 0
      src/design.config.ts
  54. 117 0
      src/locales/cn.ts
  55. 50 0
      src/locales/de.ts
  56. 50 0
      src/locales/en.ts
  57. 63 0
      src/locales/zh.ts
  58. 27 0
      src/main.ts
  59. 23 0
      src/plugin/global.ts
  60. 21 0
      src/plugin/i18n.ts
  61. 19 0
      src/plugin/vant.ts
  62. 26 0
      src/router/index.ts
  63. 82 0
      src/shims-global.d.ts
  64. 6 0
      src/shims-types.d.ts
  65. 5 0
      src/shims-unocss.d.ts
  66. 6 0
      src/shims-vue.d.ts
  67. 2 0
      src/shims.d.ts
  68. 9 0
      src/stores/pinia.types.ts
  69. 28 0
      src/stores/use-global-store.ts
  70. 46 0
      src/stores/use-user-store.ts
  71. 90 0
      src/test.ts
  72. 72 0
      src/types.ts
  73. 26 0
      src/utils/message.ts
  74. 161 0
      src/views/about.vue
  75. 295 0
      src/views/card-recharge.vue
  76. 94 0
      src/views/card-transaction-detail.vue
  77. 533 0
      src/views/cards.vue
  78. 105 0
      src/views/change-pay-password.vue
  79. 86 0
      src/views/finance.vue
  80. 174 0
      src/views/find-password.vue
  81. 104 0
      src/views/forget-pay-password.vue
  82. 145 0
      src/views/freeze-card.vue
  83. 190 0
      src/views/home.vue
  84. 53 0
      src/views/language.vue
  85. 294 0
      src/views/login.vue
  86. 105 0
      src/views/mine.vue
  87. 80 0
      src/views/pay-password.vue
  88. 233 0
      src/views/reset-password.vue
  89. 35 0
      tsconfig.json
  90. 1 0
      uno.css
  91. 7 0
      unocss.config.ts
  92. 52 0
      vite.config.build.ts
  93. 64 0
      vite.config.components.ts
  94. 56 0
      vite.config.css.ts
  95. 37 0
      vite.config.macros.ts
  96. 59 0
      vite.config.ts

+ 3 - 0
.browserslistrc

@@ -0,0 +1,3 @@
+> 1%
+last 2 versions
+not dead

+ 13 - 0
.editorconfig

@@ -0,0 +1,13 @@
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 4
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[{*.json,*.yml,*.yaml,*.cjson}]
+indent_style = space
+indent_size = 2

+ 3 - 0
.env.development

@@ -0,0 +1,3 @@
+VITE_APP_ENV=development
+VITE_APP_API_DOMAIN=/api/
+VITE_APP_API=/api/

+ 1 - 0
.env.production

@@ -0,0 +1 @@
+VITE_APP_ENV=production

+ 3 - 0
.env.staging

@@ -0,0 +1,3 @@
+VITE_APP_ENV=pre-release
+VITE_APP_API_DOMAIN=/api/
+VITE_APP_API=/api/

+ 1 - 0
.env.test

@@ -0,0 +1 @@
+VITE_APP_ENV=test

+ 349 - 0
.eslintrc-auto-import.json

@@ -0,0 +1,349 @@
+{
+  "globals": {
+    "$api": true,
+    "CLIENT": true,
+    "Component": true,
+    "ComponentPublicInstance": true,
+    "ComputedRef": true,
+    "DirectiveBinding": true,
+    "EffectScope": true,
+    "ExtractDefaultPropTypes": true,
+    "ExtractPropTypes": true,
+    "ExtractPublicPropTypes": true,
+    "GlobalState": true,
+    "InjectionKey": true,
+    "MaybeRef": true,
+    "MaybeRefOrGetter": true,
+    "PropType": true,
+    "Ref": true,
+    "Slot": true,
+    "Slots": true,
+    "UTC2Date": true,
+    "UserInfo": true,
+    "VNode": true,
+    "WritableComputedRef": true,
+    "asyncComputed": true,
+    "autoResetRef": true,
+    "closeToast": true,
+    "computed": true,
+    "computedAsync": true,
+    "computedEager": true,
+    "computedInject": true,
+    "computedWithControl": true,
+    "config": true,
+    "controlledComputed": true,
+    "controlledRef": true,
+    "createApp": true,
+    "createEventHook": true,
+    "createGlobalState": true,
+    "createInjectionState": true,
+    "createReactiveFn": true,
+    "createRef": true,
+    "createReusableTemplate": true,
+    "createRouter": true,
+    "createSharedComposable": true,
+    "createTemplatePromise": true,
+    "createUnrefFn": true,
+    "createWebHashHistory": true,
+    "crypt": true,
+    "customRef": true,
+    "dataHasErrorKey": true,
+    "dataIsReadyKey": true,
+    "debouncedRef": true,
+    "debouncedWatch": true,
+    "deepClone": true,
+    "deepMerge": true,
+    "defineAsyncComponent": true,
+    "defineComponent": true,
+    "defineStore": true,
+    "eagerComputed": true,
+    "effectScope": true,
+    "extendRef": true,
+    "getCurrentInstance": true,
+    "getCurrentScope": true,
+    "h": true,
+    "ignorableWatch": true,
+    "inject": true,
+    "injectLocal": true,
+    "isDefined": true,
+    "isProxy": true,
+    "isReactive": true,
+    "isReadonly": true,
+    "isRef": true,
+    "jsxComponents": true,
+    "lang": true,
+    "loginMsgBox": true,
+    "makeDestructurable": true,
+    "markRaw": true,
+    "nextTick": true,
+    "onActivated": true,
+    "onBeforeMount": true,
+    "onBeforeRouteLeave": true,
+    "onBeforeRouteUpdate": true,
+    "onBeforeUnmount": true,
+    "onBeforeUpdate": true,
+    "onClickOutside": true,
+    "onDeactivated": true,
+    "onElementRemoval": true,
+    "onErrorCaptured": true,
+    "onKeyStroke": true,
+    "onLoginKey": true,
+    "onLongPress": true,
+    "onMounted": true,
+    "onRenderTracked": true,
+    "onRenderTriggered": true,
+    "onScopeDispose": true,
+    "onServerPrefetch": true,
+    "onStartTyping": true,
+    "onUnmounted": true,
+    "onUpdated": true,
+    "onWatcherCleanup": true,
+    "pausableWatch": true,
+    "provide": true,
+    "provideLocal": true,
+    "reactify": true,
+    "reactifyObject": true,
+    "reactive": true,
+    "reactiveComputed": true,
+    "reactiveOmit": true,
+    "reactivePick": true,
+    "readonly": true,
+    "ref": true,
+    "refAutoReset": true,
+    "refDebounced": true,
+    "refDefault": true,
+    "refThrottled": true,
+    "refWithControl": true,
+    "resolveComponent": true,
+    "resolveRef": true,
+    "resolveUnref": true,
+    "routerKey": true,
+    "shallowReactive": true,
+    "shallowReadonly": true,
+    "shallowRef": true,
+    "showConfirmDialog": true,
+    "showDialog": true,
+    "showDialogKey": true,
+    "showFailToast": true,
+    "showImagePreview": true,
+    "showLoadingToast": true,
+    "showMsg": true,
+    "showSuccessToast": true,
+    "showToast": true,
+    "storeToRefs": true,
+    "syncRef": true,
+    "syncRefs": true,
+    "templateRef": true,
+    "throttledRef": true,
+    "throttledWatch": true,
+    "toRaw": true,
+    "toReactive": true,
+    "toRef": true,
+    "toRefs": true,
+    "toValue": true,
+    "toggleDialogKey": true,
+    "triggerRef": true,
+    "tryOnBeforeMount": true,
+    "tryOnBeforeUnmount": true,
+    "tryOnMounted": true,
+    "tryOnScopeDispose": true,
+    "tryOnUnmounted": true,
+    "unref": true,
+    "unrefElement": true,
+    "until": true,
+    "updateParentKey": true,
+    "useActiveElement": true,
+    "useAnimate": true,
+    "useArrayDifference": true,
+    "useArrayEvery": true,
+    "useArrayFilter": true,
+    "useArrayFind": true,
+    "useArrayFindIndex": true,
+    "useArrayFindLast": true,
+    "useArrayIncludes": true,
+    "useArrayJoin": true,
+    "useArrayMap": true,
+    "useArrayReduce": true,
+    "useArraySome": true,
+    "useArrayUnique": true,
+    "useAsyncQueue": true,
+    "useAsyncState": true,
+    "useAttrs": true,
+    "useBase64": true,
+    "useBattery": true,
+    "useBluetooth": true,
+    "useBreakpoints": true,
+    "useBroadcastChannel": true,
+    "useBrowserLocation": true,
+    "useCached": true,
+    "useClipboard": true,
+    "useClipboardItems": true,
+    "useCloned": true,
+    "useColorMode": true,
+    "useConfirmDialog": true,
+    "useCountdown": true,
+    "useCounter": true,
+    "useCssModule": true,
+    "useCssVar": true,
+    "useCssVars": true,
+    "useCurrentElement": true,
+    "useCycleList": true,
+    "useDark": true,
+    "useDateFormat": true,
+    "useDebounce": true,
+    "useDebounceFn": true,
+    "useDebouncedRefHistory": true,
+    "useDeviceMotion": true,
+    "useDeviceOrientation": true,
+    "useDevicePixelRatio": true,
+    "useDevicesList": true,
+    "useDisplayMedia": true,
+    "useDocumentVisibility": true,
+    "useDraggable": true,
+    "useDropZone": true,
+    "useElementBounding": true,
+    "useElementByPoint": true,
+    "useElementHover": true,
+    "useElementSize": true,
+    "useElementVisibility": true,
+    "useEventBus": true,
+    "useEventListener": true,
+    "useEventSource": true,
+    "useEyeDropper": true,
+    "useFavicon": true,
+    "useFetch": true,
+    "useFileDialog": true,
+    "useFileSystemAccess": true,
+    "useFilters": true,
+    "useFocus": true,
+    "useFocusWithin": true,
+    "useFps": true,
+    "useFullscreen": true,
+    "useGamepad": true,
+    "useGeolocation": true,
+    "useGlobal": true,
+    "useGlobalStore": true,
+    "useHead": true,
+    "useId": true,
+    "useIdle": true,
+    "useImage": true,
+    "useInfiniteScroll": true,
+    "useIntersectionObserver": true,
+    "useInterval": true,
+    "useIntervalFn": true,
+    "useKeyModifier": true,
+    "useLastChanged": true,
+    "useLink": true,
+    "useLists": true,
+    "useLocalStorage": true,
+    "useLockFn": true,
+    "useMagicKeys": true,
+    "useManualRefHistory": true,
+    "useMediaControls": true,
+    "useMediaQuery": true,
+    "useMemoize": true,
+    "useMemory": true,
+    "useModel": true,
+    "useMounted": true,
+    "useMouse": true,
+    "useMouseInElement": true,
+    "useMousePressed": true,
+    "useMutationObserver": true,
+    "useNavigatorLanguage": true,
+    "useNetwork": true,
+    "useNow": true,
+    "useObjectUrl": true,
+    "useOffsetPagination": true,
+    "useOnline": true,
+    "usePageLeave": true,
+    "useParallax": true,
+    "useParentElement": true,
+    "usePerformanceObserver": true,
+    "usePermission": true,
+    "usePointer": true,
+    "usePointerLock": true,
+    "usePointerSwipe": true,
+    "usePreferredColorScheme": true,
+    "usePreferredContrast": true,
+    "usePreferredDark": true,
+    "usePreferredLanguages": true,
+    "usePreferredReducedMotion": true,
+    "usePreferredReducedTransparency": true,
+    "usePrevious": true,
+    "useRafFn": true,
+    "useRefHistory": true,
+    "useResizeObserver": true,
+    "useRoute": true,
+    "useRouter": true,
+    "useSSRWidth": true,
+    "useSaveScroll": true,
+    "useScreenOrientation": true,
+    "useScreenSafeArea": true,
+    "useScriptTag": true,
+    "useScroll": true,
+    "useScrollLock": true,
+    "useSessionStorage": true,
+    "useShare": true,
+    "useSlots": true,
+    "useSorted": true,
+    "useSpeechRecognition": true,
+    "useSpeechSynthesis": true,
+    "useStepper": true,
+    "useStorage": true,
+    "useStorageAsync": true,
+    "useStyleTag": true,
+    "useSupported": true,
+    "useSwipe": true,
+    "useTabLists": true,
+    "useTemplateRef": true,
+    "useTemplateRefsList": true,
+    "useTextDirection": true,
+    "useTextSelection": true,
+    "useTextareaAutosize": true,
+    "useThrottle": true,
+    "useThrottleFn": true,
+    "useThrottledRefHistory": true,
+    "useTimeAgo": true,
+    "useTimeout": true,
+    "useTimeoutFn": true,
+    "useTimeoutPoll": true,
+    "useTimestamp": true,
+    "useTitle": true,
+    "useToNumber": true,
+    "useToString": true,
+    "useToggle": true,
+    "useTransition": true,
+    "useUrlSearchParams": true,
+    "useUserMedia": true,
+    "useUserStore": true,
+    "useVModel": true,
+    "useVModels": true,
+    "useVibrate": true,
+    "useVirtualList": true,
+    "useWakeLock": true,
+    "useWebNotification": true,
+    "useWebSocket": true,
+    "useWebWorker": true,
+    "useWebWorkerFn": true,
+    "useWindowFocus": true,
+    "useWindowScroll": true,
+    "useWindowSize": true,
+    "userToken": true,
+    "watch": true,
+    "watchArray": true,
+    "watchAtMost": true,
+    "watchDebounced": true,
+    "watchDeep": true,
+    "watchEffect": true,
+    "watchIgnorable": true,
+    "watchImmediate": true,
+    "watchOnce": true,
+    "watchPausable": true,
+    "watchPostEffect": true,
+    "watchSyncEffect": true,
+    "watchThrottled": true,
+    "watchTriggerable": true,
+    "watchWithFilter": true,
+    "whenever": true
+  }
+}

+ 8 - 0
.gitattributes

@@ -0,0 +1,8 @@
+*        text=auto
+*.js     text eol=lf
+*.jsx    text eol=lf
+*.json   text eol=lf
+*.vue    text eol=lf
+*.css    text eol=lf
+*.scss   text eol=lf
+*.less   text eol=lf

+ 32 - 0
.gitignore

@@ -0,0 +1,32 @@
+.DS_Store
+.idea/
+node_modules
+/dist
+
+# local env files
+.env.local
+.env.*.local
+
+# Log files
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Editor directories and files
+.idea
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+*-port.txt
+
+package-lock.json
+admin.lock
+yarn-error.log
+server/config/secret.js
+server/config/mpapp.js
+server/config/shihua.js
+svg-backup/
+docs/
+pnpm-lock.yaml

+ 3 - 0
.npmrc

@@ -0,0 +1,3 @@
+shamefully-hoist = true
+scripts-prepend-node-path = true
+registry=https://registry.npmmirror.com/

+ 0 - 0
.prettierignore


+ 19 - 0
.prettierrc

@@ -0,0 +1,19 @@
+{
+    "printWidth": 150,
+    "semi": false,
+    "tabWidth": 4,
+    "singleQuote": true,
+    "trailingComma": "all",
+    "arrowParens": "always",
+    "proseWrap": "preserve",
+    "htmlWhitespaceSensitivity": "css",
+    "endOfLine": "lf",
+    "overrides": [
+        {
+            "files": "*.json",
+            "options": {
+                "tabWidth": 2
+            }
+        }
+    ]
+}

+ 4 - 0
.stylelintignore

@@ -0,0 +1,4 @@
+node_modules
+github-markdown.css
+*.min.css
+dist

+ 11 - 0
.stylelintrc.json

@@ -0,0 +1,11 @@
+{
+  "extends": ["@lincy/stylelint-config"],
+  "rules": {
+    "at-rule-no-unknown": [
+      true,
+      {
+        "ignoreAtRules": ["tailwind", "apply", "variants", "responsive", "screen", "use"]
+      }
+    ]
+  }
+}

+ 5 - 0
.vscode/settings.json

@@ -0,0 +1,5 @@
+{
+    "i18n-ally.localesPaths": [
+        "src/locales"
+    ]
+}

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 LinCenYing
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 281 - 0
README.md

@@ -0,0 +1,281 @@
+# Vue3 项目模板
+
+vue3 H5端脚手架, 包含技术栈(Vue3 + Vant + Pinia + Vite + TS + Unocss)
+
+## Variations
+
+-   [vite-nuxt3](https://github.com/lincenying/vite-nuxt3) - Nuxt3 + Vite 入门模板
+-   [vite-uniapp-vue3](https://github.com/lincenying/vite-uniapp-vue3) - Uniapp3 + Vite 入门模板
+-   [vite-react-mobx-ssr](https://github.com/lincenying/vite-react-mobx-ssr) - React + Mobx + Vite + SSR 入门模板
+-   [vite-react-mobx](https://github.com/lincenying/vite-react-mobx) - React + Mobx + Vite 入门模板
+-   [vite-react-redux](https://github.com/lincenying/vite-react-redux) - React + Redux + Vite 入门模板
+-   [vite-vue3-h5-ssr](https://github.com/lincenying/vite-vue3-h5-ssr) - Vue3 + Vant + Vite + SSR 入门模板
+-   [vite-vue3-h5](https://github.com/lincenying/vite-vue3-h5) - Vue3 + Vant + Vite 入门模板
+-   [vite-vue3-admin](https://github.com/lincenying/vite-vue3-admin) - Vue3 + ElementPlus + Vite 管理后台入
+
+## 使用
+
+```bash
+npx degit lincenying/vite-vue3-h5 my-h5-app
+cd my-h5-app
+pnpm i # 如果你没有安装 pnpm,请运行:npm install -g pnpm
+```
+
+### 开发环境
+
+```bash
+pnpm serve
+```
+
+### 生产环境
+
+```bash
+# 测试环境
+pnpm build:test
+# 预发布环境
+pnpm build:staging
+# 生产环境
+pnpm build
+```
+
+### 生产环境预览
+
+```bash
+pnpm start
+```
+
+### Lint 和修复文件
+
+```bash
+pnpm lint # eslint检测不修复
+pnpm lint:fix # eslint检测并修复
+pnpm lint:ts # ts 类型检测
+pnpm lint:css # css 检测并修复
+
+```
+
+## 环境变量
+
+预留4套环境变量, 具体参数可查看根目录的 `.env.xxx`, 其中 `development` 为开发环境, `test, staging, production` 依次为 `测试环境, 预发布环境, 正式环境`
+根据自己需要, 启动/编译不同的环境
+
+## Rem 适配
+
+设计稿相关参数配置见: `src/design.config.ts`, 按UI给的设计稿, 修改即可
+
+设计稿宽度: `designWidth`
+设计稿高度: `designHeight`
+字号: `fontSize`
+
+字号大小, 尽量配合Ui库, 比如默认使用的vant UI组件库就是设计稿宽度为375, rootfontsize为37.5
+如果你的设计稿是750的, 方法有2
+1: 设计稿宽度设置为750, 然后字号设置成75, 然后css代码的宽高按设计稿中实际的书写, 然后在postcss插件, 针对性判断vant的字号改成37.5( vite.config.css.ts 里已做了适配vant组件库)
+2: 设计稿宽度设置为375, 然后字号设置成37.5, 然后css代码的宽高按设计稿中实际尺寸/2书写, 也可以将设计稿尺寸调整到375后, 按375的实际尺寸书写
+
+一般项目中有3类代码的单位需要转换, 分别是自己写的css代码, 组件库或者其他第三方插件带的css代码, 使用unocss写的原子化css
+具体插件配置详见: `vite.config.css.ts`
+
+## 自动引入UI库组件/项目组件/函数等
+
+项目已经配置了`unplugin-auto-import`和`unplugin-vue-components`
+前者能自动引入vue, vue-router, vueuse等提供的方法, 而无需一遍遍的`import`
+后者能自动引入UI组件, 及项目被定义的组件, 也不用一遍遍的`import`
+详细配置见: `vite.config.components.ts`
+相关文档见:
+https://github.com/antfu/unplugin-auto-import#readme
+https://github.com/antfu/unplugin-vue-components#readme
+
+## Pinia 状态管理
+
+vue 官方出品的, 比vuex更好用的状态管理
+使用方法:
+在pinia文件夹下,新建一个ts文件, 如: `use-global-store.ts`
+里面代码如下:
+
+```ts
+import type { GlobalState } from './pinia.types'
+
+const useStore = defineStore('globalStore', () => {
+    const state: GlobalState = reactive({
+        globalLoading: true,
+        routerLoading: false,
+    })
+
+    const setGlobalLoading = (payload: boolean) => {
+        state.globalLoading = payload
+    }
+    const setRouterLoading = (payload: boolean) => {
+        state.routerLoading = payload
+    }
+
+    return {
+        ...toRefs(state),
+        setGlobalLoading,
+        setRouterLoading,
+    }
+})
+
+export default useStore
+```
+
+那么在需要用到该状态管理的地方, 只需要
+
+```ts
+const userStore = useGlobalStore()
+userStore.setGlobalLoading(true)
+```
+
+即可, 因为配置了`unplugin-auto-import`, 所以根本无需要`import`, 你只需要直接把文件名改成驼峰的方式, 直接当函数使用即可
+注意: 直接用文件名当函数名, 只有代码是用`export default`导出时可用, 如果是用`export const xxx`, `export function xxx {}` 这样导出的, 那么直接使用xxx作为方法名即可
+具体可以看`src/auto-imports.d.ts`为你生成了那些方法, 这里的方法都可以直接使用, 而无需`import`
+
+## 路由
+
+放在`views`文件夹下的`vue`文件, 都会自动加入路由中, 子文件夹里的则不会, 根据你自己的使用情况, 可以修改`src/router/index.ts`以适配
+是使用`hash`还是`history`模式, 也可以在上面的文件中修改
+
+## Api封装
+
+`src/api/index.ts`封装了`get, post, put, delete`4中常用的方法, 分别对应4种method, 而`$api`为全局方法, 可以在任何`.vue`页面, 直接使用`$api.get/post/put/delete`
+接口默认判断code=200为正常返回, 如果后端接口不是用code作为判断, 那么需要在`src/api/index.ts`做对应修改
+如:
+
+```ts
+let detail: NullAble<Article> = null
+async function getDetail() {
+    const { code, data } = await $api.get<Article>('article/detail', {})
+    if (code === 200) {
+        detail = data
+    }
+}
+
+getDetail()
+```
+
+## 列表封装: useLists
+
+在`src/composables/index.ts`中封装了`useLists`方法, 让你所有上拉加载, 下拉刷新几行代码就搞定, 如:
+
+```ts
+const apiConfig = {
+    api: {
+        method: 'get',
+        url: 'article/lists',
+        config: { per_page: 20, tab: '' },
+    },
+}
+const { api, page, config, dataList, getList, onRefresh } = useLists<Article>(apiConfig)
+```
+
+如上面代码, 只需要将接口相关参数传入接口, 返回的参数中, `api`为传入的接口相关参数(响应式的Ref数据), `page`为当前页数(响应式的Ref数据),
+`config`为加载状态(响应式的Ref数据), `dataList`为数据列表(响应式的Ref数据), `getList`为请求列表的方法, `onRefresh`为刷新数据的方法
+
+模板代码为:
+
+```html
+<template>
+    <van-pull-refresh v-model="config.isLoading" @refresh="onRefresh">
+        <van-list
+            :loading="config.loading"
+            :immediate-check="false"
+            :finished="config.finished"
+            :error="config.error"
+            loading-text="努力加载中"
+            finished-text="我也是有底线的"
+            error-text="请求失败,点击重新加载"
+            @load="getList"
+        >
+            <van-cell v-for="(item, index) in dataList" :key="`${index}_${item.c_id}`" :title="item.c_title" is-link :to="`/home/detail?id=${item.c_id}`" />
+        </van-list>
+    </van-pull-refresh>
+</template>
+```
+
+如果你需要做搜索, 或者切换分类什么的, 也很简单, 假设我现在需要修改tab的值,然后重新请求接口, 那么只需要:
+
+```ts
+function changeTab(tab: string) {
+    api.value.config.tab = tab
+    page.value = 1
+    getList()
+}
+```
+
+先更新api里的相关参数/充值page数为1, 然后重新请求接口即可
+
+上面`config`返回的参数如下:
+
+```json
+{
+  // 下拉刷新 ==>
+  "isLoading": false,
+  "isRefresh": false,
+  // <==下拉刷新
+  // 滚动加载 ==>
+  "loadStatus": "loadmore", // 'loadmore' | 'nomore' | 'loading'
+  "isLock": false, // 请求过程中锁定, 防止重复请求
+  "loading": false, // 加载状态 (直接判断loadStatus==='loading'也可以)
+  "error": false, // 接口是否报错
+  "finished": false // 列表是否已加载完成, 即是否到最后一页 (直接判断loadStatus==='nomore'也可以)
+  // <==滚动加载
+}
+```
+
+接口默认返回数据接口为:
+
+```json
+{
+  "code": 200,
+  "data": {
+    "total": 992,
+    "per_page": 20,
+    "current_page": 1,
+    "last_page": 50,
+    "data": []
+  }
+}
+```
+
+如果你用的接口返回数据结构不一样, 需要在`src/composables/index.ts`稍做修改
+
+## 开发环境配置proxy跨域
+
+```
+{
+    server: {
+        port: 7771,
+        proxy: {
+            '/api': {
+                target: 'https://php.mmxiaowu.com',
+                changeOrigin: true,
+                rewrite: (path: string) => path.replace(/^\/api/, '/api'),
+            },
+        },
+    },
+}
+```
+
+详见: `vite.config.build.ts`
+
+## Mock
+
+在`mock`文件夹, 创建ts文件, 按mock规则添加接口即可, 详情见: `mock/module-index.ts`
+相关文档见:
+https://github.com/anncwb/vite-plugin-mock/tree/master/#readme
+
+## Unocss
+
+unocss是一个及时/按需/原子化的css引擎, 项目中也做了相关配置, 可直接使用
+配置见:
+https://github.com/lincenying/unocss-base-config/blob/main/src/uno.h5.config.ts
+官方文档见:
+https://unocss.dev/
+
+## eslint/stylelint/prettierrc/vue-tsc
+
+根目录下的`eslint.config.ts`、`stylelint.config.js`、`.prettier`内置了 lint 规则,帮助你规范地开发代码,有助于提高团队的代码质量和协作性,可以根据团队的规则进行修改
+注意: `prettier`只在编辑器层面, 在`eslint`中并没有添加`prettier`插件
+
+## License
+
+[MIT]

+ 3 - 0
delete-ts.sh

@@ -0,0 +1,3 @@
+rm -rf src/auto-imports.d.ts
+rm -rf src/components.d.ts
+rm -rf .eslintrc-auto-import.json

+ 32 - 0
eslint.config.ts

@@ -0,0 +1,32 @@
+import { readFile } from 'node:fs/promises'
+import lincy from '@lincy/eslint-config'
+
+const autoImport = JSON.parse(
+    (await readFile(new URL('./.eslintrc-auto-import.json', import.meta.url))).toString(),
+)
+
+const config = await lincy(
+    {
+        unocss: true,
+        formatters: {
+            css: false,
+            graphql: true,
+            html: true,
+            markdown: true,
+        },
+        toml: false,
+        ignores: [
+            '**/assets',
+            '**/static',
+        ],
+    },
+    {
+        languageOptions: {
+            globals: {
+                ...autoImport.globals,
+            },
+        },
+    },
+)
+
+export default config

+ 13 - 0
index.html

@@ -0,0 +1,13 @@
+<!doctype html>
+<html>
+    <head>
+        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+        <link rel="icon" href="/favicon.ico" />
+        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+        <title>CWG Markets</title>
+    </head>
+    <body>
+        <div id="app"></div>
+        <script type="module" src="/src/main.ts"></script>
+    </body>
+</html>

+ 93 - 0
package.json

@@ -0,0 +1,93 @@
+{
+  "name": "vite-vue3-h5",
+  "type": "module",
+  "version": "3.0.0",
+  "private": true,
+  "packageManager": "pnpm@10.11.0",
+  "author": "lincenying <lincenying@qq.com>",
+  "scripts": {
+    "prepare": "npx simple-git-hooks",
+    "del": "sh delete-ts.sh",
+    "serve": "cross-env DEBUG=vite:transform vite",
+    "build:test": "vite build --mode test",
+    "build:staging": "vite build --mode staging",
+    "build": "vite build",
+    "start": "vite preview",
+    "lint": "eslint .",
+    "lint:fix": "eslint . --fix",
+    "lint:ts": "vue-tsc --noEmit",
+    "lint:css": "stylelint '**/*.{css,scss}' --fix"
+  },
+  "dependencies": {
+    "@lincy/utils": "^0.6.2",
+    "@unhead/vue": "^2.0.8",
+    "@vueuse/core": "^13.1.0",
+    "axios": "^1.9.0",
+    "crypto-js": "^4.0.0",
+    "default-passive-events": "^2.0.0",
+    "lodash": "^4.17.21",
+    "md5": "^2.3.0",
+    "pinia": "^3.0.2",
+    "qs": "^6.14.0",
+    "store2": "^2.14.4",
+    "unhead": "^2.0.8",
+    "vant": "^4.9.19",
+    "vue": "^3.5.13",
+    "vue-cropper": "^1.1.4",
+    "vue-i18n": "^12.0.0-alpha.2",
+    "vue-router": "^4.5.1"
+  },
+  "devDependencies": {
+    "@iconify-json/mdi": "^1.2.3",
+    "@lincy/eslint-config": "^5.5.2",
+    "@lincy/stylelint-config": "^2.0.1",
+    "@lincy/unocss-base-config": "^2.2.1",
+    "@types/crypto-js": "^4.2.2",
+    "@types/lodash": "^4.17.16",
+    "@types/node": "^22.15.17",
+    "@types/qs": "^6.9.18",
+    "@unocss/eslint-plugin": "^66.1.1",
+    "@vitejs/plugin-vue": "^5.2.4",
+    "@vitejs/plugin-vue-jsx": "^4.1.2",
+    "@vue-macros/volar": "^3.0.0-beta.12",
+    "cross-env": "^7.0.3",
+    "eslint": "^9.26.0",
+    "lint-staged": "^16.0.0",
+    "mockjs": "^1.1.0",
+    "postcss": "^8.5.3",
+    "postcss-px-to-viewport-8-plugin": "^1.2.5",
+    "sass": "^1.88.0",
+    "simple-git-hooks": "^2.13.0",
+    "typescript": "^5.8.3",
+    "unocss": "^66.1.3",
+    "unplugin-auto-import": "^19.2.0",
+    "unplugin-icons": "^22.1.0",
+    "unplugin-vue-components": "^28.5.0",
+    "unplugin-vue-macros": "^2.14.5",
+    "url-search-params-polyfill": "^8.2.5",
+    "vite": "^6.3.5",
+    "vite-plugin-inspect": "^11.0.1",
+    "vite-plugin-mock": "^3.0.2",
+    "vite-plugin-progress": "^0.0.7",
+    "vue-tsc": "^2.2.10"
+  },
+  "pnpm": {
+    "peerDependencyRules": {
+      "ignoreMissing": [
+        "rollup",
+        "postcss",
+        "esbuild",
+        "prettier"
+      ]
+    }
+  },
+  "simple-git-hooks": {
+    "pre-commit": "npx lint-staged"
+  },
+  "lint-staged": {
+    "*.{[jt]s?(x),vue,md}": [
+      "eslint --fix"
+    ],
+    "*.{css,scss}": "stylelint --fix --allow-empty-input"
+  }
+}

BIN
public/favicon.ico


+ 5 - 0
simple-git-hooks.sh

@@ -0,0 +1,5 @@
+#!/bin/sh
+
+git config core.hooksPath .git/hooks/
+rm -rf .git/hooks
+npx simple-git-hooks

+ 94 - 0
src/App.vue

@@ -0,0 +1,94 @@
+<template>
+    <div class="wrap">
+        <PageHeader :title="headerTitle" :show-back="!showBack" />
+        <router-view v-slot="{ Component }" class="app body" :class="{ 'is-tab': routeIsTab }">
+            <transition
+                v-if="!globalLoading"
+                :name="transitionName"
+                @before-enter="handleBeforeEnter"
+                @after-enter="handleAfterEnter"
+            >
+                <keep-alive :include="cacheComponents">
+                    <component :is="Component" />
+                </keep-alive>
+            </transition>
+        </router-view>
+        <CustomTabbar v-if="routeIsTab" />
+        <div v-if="globalLoading" class="global-loading">
+            <van-loading type="spinner" size="32px" />
+        </div>
+        <div v-if="routerLoading" class="router-loading">
+            <van-loading type="spinner" size="32px" color="#1989fa" />
+        </div>
+    </div>
+</template>
+
+<script setup lang="ts">
+import CustomTabbar from './components/CustomTabbar.vue'
+import { computed } from 'vue'
+import PageHeader from './components/PageHeader.vue'
+import { useI18n } from 'vue-i18n'
+defineOptions({
+    name: 'AppRoot',
+})
+const { t } = useI18n()
+const { route, globalStore } = useGlobal()
+const { globalLoading, routerLoading } = storeToRefs(globalStore)
+setTimeout(() => {
+    globalStore.setGlobalLoading(false)
+}, 200)
+const cacheComponents = $ref('HomeRouter,ListsRouter,AboutRouter')
+let transitionName = $ref('fade')
+let metaIndex = $ref<number>(route.meta.index as number)
+
+watch(
+    () => route.fullPath,
+    () => {
+        const newMetaIndex = route.meta.index as number
+        if (!metaIndex || newMetaIndex === metaIndex) {
+            transitionName = 'fade'
+        } else if (newMetaIndex > metaIndex) {
+            transitionName = 'slide-left'
+        } else {
+            transitionName = 'slide-right'
+        }
+        metaIndex = newMetaIndex
+    },
+)
+const routeIsTab = computed(() => {
+    return ['/', '/cards', '/finance', '/mine'].includes(route.path)
+})
+const showBack = computed(() => {
+    return ['/', '/cards', '/finance', '/mine', '/login'].includes(route.path)
+})
+function handleBeforeEnter() {
+    globalStore.$patch({ isPageSwitching: true })
+}
+function handleAfterEnter() {
+    globalStore.$patch({ isPageSwitching: false })
+}
+function slashToDash(str: string) {
+    return str.replace(/\//g, '-')
+}
+const headerTitle = computed(() => {
+    const path = route.path
+    let key = path === '/' ? 'wallet' : path.slice(1)
+    key = slashToDash(key)
+    return t(`${key}.title`) || ''
+})
+</script>
+
+<style scoped lang="scss">
+.wrap {
+    min-height: 100vh;
+    background: var(--main-bg);
+    color: var(--main-yellow);
+    font-family: 'PingFang SC', 'Helvetica Neue', Arial, sans-serif;
+}
+.page {
+    padding: 0 20px 80px 20px;
+    box-sizing: border-box;
+    min-height: 100vh;
+    background: var(--main-bg);
+}
+</style>

+ 11 - 0
src/api/index.ts

@@ -0,0 +1,11 @@
+import { userApi } from './user'
+import { ucardApi } from './ucard'
+import { uploadApi } from './upload'
+
+export const $Api = {
+  user: userApi,
+  ucard: ucardApi,
+  upload: uploadApi,
+}
+
+export default $Api

+ 278 - 0
src/api/ucard.ts

@@ -0,0 +1,278 @@
+import { $api } from '@/composables/fetch'
+
+// 基础响应接口
+export interface BaseResponse<T = any> {
+    code: number;
+    data: T;
+    msg: string;
+}
+
+// 分页请求参数接口
+export interface PageParams {
+    page: {
+        current: number;
+        row: number;
+    }
+    [key: string]: any;
+}
+
+// 分页响应接口
+export interface PageResponse<T> {
+    data: T[];
+    code: number;
+    msg: string;
+    page: {
+        current: number;
+        pageTotal: number;
+        row: number;
+        rowTotal: number;
+    }
+}
+
+// 卡片类型相关接口
+export interface CardType {
+    id: string;
+    name: string;
+    [key: string]: any;
+}
+
+export interface CardTypeParams extends PageParams {
+    name?: string;
+    status?: number;
+}
+
+// 商户相关接口
+export interface MerchantUser {
+    id: string;
+    username: string;
+    [key: string]: any;
+}
+
+export interface MerchantAccount {
+    balance: number;
+    [key: string]: any;
+}
+
+export interface MerchantParams extends PageParams {
+    username?: string;
+    status?: number;
+}
+
+// KYC相关接口
+export interface KycInfo {
+    id: string;
+    status: number;
+    [key: string]: any;
+}
+
+export interface KycParams {
+    merchantId: string;
+    [key: string]: any;
+}
+
+// 卡片相关接口
+export interface CardInfo {
+    id: string;
+    cardNo: string;
+    [key: string]: any;
+}
+
+export interface CardApplyParams {
+    cardTypeId: string;
+    [key: string]: any;
+}
+
+// 交易相关接口
+export interface TransactionInfo {
+    id: string;
+    amount: number;
+    [key: string]: any;
+}
+
+export interface TransactionParams extends PageParams {
+    cardId?: string;
+    type?: number;
+}
+
+// 转账相关接口
+export interface TransferInfo {
+    id: string;
+    amount: number;
+    [key: string]: any;
+}
+
+export interface TransferParams {
+    cardId: string;
+    uniqueId: string;
+    cardNo: string;
+    amount: number;
+    [key: string]: any;
+}
+
+// 汇率相关接口
+export interface RateInfo {
+    fromCurrency: string;
+    toCurrency: string;
+    rate: number;
+}
+
+// 国家城市相关接口
+export interface CountryCityInfo {
+    code: string;
+    name: string;
+    cities?: Array<{
+        code: string;
+        name: string;
+    }>;
+}
+
+export const ucardApi = {
+    //获取卡片类型列表
+    cardTypesList(params: CardTypeParams = { page: { current: 1, row: 10 } }): Promise<BaseResponse<PageResponse<CardType>>> {
+        return $api.post("/ucard/card/types/page", params);
+    },
+    // 更新卡片类型列表
+    updateCardTypes(params: Partial<CardType> = {}): Promise<BaseResponse> {
+        return $api.post("/ucard/card/types", params);
+    },
+    // 商户用户分页列表
+    merchantList(params: MerchantParams = { page: { current: 1, row: 10 } }): Promise<BaseResponse<PageResponse<MerchantUser>>> {
+        return $api.post("/ucard/merchant/user/page", params);
+    },
+    // 查询商户账户信息
+    merchantAccount(params: { merchantId: string }): Promise<BaseResponse<MerchantAccount>> {
+        return $api.post("/ucard/merchant/account", params);
+    },
+    // 商户注册选择用户列表
+    merchantSearch(params: { keyword: string }): Promise<BaseResponse<MerchantUser[]>> {
+        return $api.post("/custom/search/ucard", params);
+    },
+    // 商户用户注册
+    merchantRegister(params: Partial<MerchantUser>): Promise<BaseResponse> {
+        return $api.post("/ucard/merchant/user/register", params);
+    },
+    // 更新商户用户信息
+    merchantUpdate(params: Partial<MerchantUser>): Promise<BaseResponse> {
+        return $api.post("/ucard/merchant/user/update", params);
+    },
+    // 上传KYC附件
+    kycUpload(params: FormData): Promise<BaseResponse<{ url: string }>> {
+        return $api.post("/ucard/merchant/kyc/upload", params);
+    },
+    // 提交KYC认证
+    kycSubmit(params: KycParams): Promise<BaseResponse> {
+        return $api.post("/ucard/merchant/kyc/submit", params);
+    },
+    // 查询KYC认证状态
+    kycStatus(params: { merchantId: string }): Promise<BaseResponse<KycInfo>> {
+        return $api.post("/ucard/merchant/kyc/status", params);
+    },
+    // 获取卡片申请列表
+    applyList(params: PageParams): Promise<BaseResponse<PageResponse<PageParams>>> {
+        return $api.post("/ucard/card/apply/page", params);
+    },
+    // 获取卡片列表
+    cardList(params: PageParams): Promise<BaseResponse<PageResponse<PageParams>>> {
+        return $api.post("/ucard/card/page", params);
+    },
+    // 充值记录分页查询
+    rechargeList(params: TransactionParams): Promise<BaseResponse<PageResponse<TransactionInfo>>> {
+        return $api.post("/ucard/card/recharge/page", params);
+    },
+    // 查询交易记录分页列表
+    transactionsList(params: TransactionParams): Promise<BaseResponse<PageResponse<TransactionInfo>>> {
+        return $api.post("/ucard/card/transac/page", params);
+    },
+    // 用户订单分页查询
+    transferList(params: TransactionParams): Promise<BaseResponse<PageResponse<TransferInfo>>> {
+        return $api.post("/ucard/transfer/page", params);
+    },
+    // 文件上传
+    ucardUpload(params: FormData): Promise<BaseResponse<{ url: string }>> {
+        return $api.post("/ucard/upload/file", params);
+    },
+    // 申请开卡
+    ucardApply(params: CardApplyParams): Promise<BaseResponse> {
+        return $api.post("/ucard/card/apply", params);
+    },
+    // kyc列表
+    kycList(params: PageParams): Promise<BaseResponse<PageResponse<KycInfo>>> {
+        return $api.post("/ucard/merchant/kyc/page", params);
+    },
+    // 查询开卡进度
+    ucardApplyProgress(params: { applyId: string }): Promise<BaseResponse<{ status: number; message: string }>> {
+        return $api.post("/ucard/card/apply/progress", params);
+    },
+    // 银行卡激活
+    ucardActivate(params: { cardId: string; password: string }): Promise<BaseResponse> {
+        return $api.post("/ucard/card/activate", params);
+    },
+    // 查询充值预估到账金额
+    ucardRechargeEstimate(params: { amount: number; currency: string }): Promise<BaseResponse<{ estimatedAmount: number }>> {
+        return $api.post("/ucard/card/recharge/estimate", params);
+    },
+    // 银行卡充值
+    ucardRecharge(params: { cardId: string; amount: number; currency: string }): Promise<BaseResponse> {
+        return $api.post("/ucard/card/recharge", params);
+    },
+    // 查询充值订单
+    ucardRechargeOrder(params: { orderId: string }): Promise<BaseResponse<TransactionInfo>> {
+        return $api.post("/ucard/card/recharge/order", params);
+    },
+    // 查询卡片余额
+    ucardBalance(params: { cardNo: string; uniqueId: string }): Promise<BaseResponse<{ balance: number; currency: string }>> {
+        return $api.post("/ucard/card/balance", params);
+    },
+    // 找回密码
+    ucardResetPassword(params: { cardId: string; newPassword: string }): Promise<BaseResponse> {
+        return $api.post("/ucard/card/password/reset", params);
+    },
+    // 冻结卡片
+    ucardFreeze(params: { cardId: string }): Promise<BaseResponse> {
+        return $api.post("/ucard/card/freeze", params);
+    },
+    // 解冻卡片
+    ucardUnfreeze(params: { cardId: string }): Promise<BaseResponse> {
+        return $api.post("/ucard/card/unfreeze", params);
+    },
+    // 查询速汇银行及相关配置
+    ucardBanks(params: { country: string }): Promise<BaseResponse<Array<{ bankCode: string; bankName: string }>>> {
+        return $api.post("/ucard/transfer/banks", params);
+    },
+    // 查询法币汇率
+    ucardRate(params: { fromCurrency: string; toCurrency: string }): Promise<BaseResponse<RateInfo>> {
+        return $api.post("/ucard/transfer/rate", params);
+    },
+    // 代付校验
+    ucardValidate(params: TransferParams): Promise<BaseResponse<{ valid: boolean; message: string }>> {
+        return $api.post("/ucard/transfer/validate", params);
+    },
+    // 代付付款人校验
+    ucardValidatePayer(params: { payerId: string }): Promise<BaseResponse<{ valid: boolean; message: string }>> {
+        return $api.post("/ucard/transfer/validate/payer", params);
+    },
+    // 代付收款人校验
+    ucardValidatePayee(params: { payeeId: string }): Promise<BaseResponse<{ valid: boolean; message: string }>> {
+        return $api.post("/ucard/transfer/validate/payee", params);
+    },
+    // 代付
+    ucardTransfer(params: TransferParams): Promise<BaseResponse<{ orderId: string }>> {
+        return $api.post("/ucard/transfer", params);
+    },
+    // 提交调单信息或文件
+    ucardDispute(params: { orderId: string; reason: string; files?: FormData }): Promise<BaseResponse> {
+        return $api.post("/ucard/transfer/dispute", params);
+    },
+    // 查询订单结果
+    ucardResult(params: { orderId: string }): Promise<BaseResponse<{ status: number; message: string }>> {
+        return $api.post("/ucard/transfer/order/result", params);
+    },
+    // 国家城市
+    ucardCountryCity(params: { country?: string }): Promise<BaseResponse<CountryCityInfo[]>> {
+        return $api.post("/ucard/card/country", params);
+    },
+    // 手机区号获取
+    countryGet(params: { keyword?: string }): Promise<BaseResponse<Array<{ code: string; name: string }>>> {
+        return $api.post("/country/get", params);
+    },
+}

+ 31 - 0
src/api/upload.ts

@@ -0,0 +1,31 @@
+
+
+import { $api } from '@/composables/fetch'
+import type { UserInfo } from '@/stores/use-user-store'
+
+export interface LoginParams {
+  loginName: string
+  password: string
+  emailCode: string
+}
+export interface TokenInfo {
+  data: string
+}
+
+export interface RegisterParams extends LoginParams {
+  email: string
+  phone: string
+}
+
+export interface ResetPasswordParams {
+  email: string
+  code: string
+  newPassword: string
+}
+
+export const uploadApi = {
+  // 登录
+  uploadFile: (file: File) => {
+    return $api.uploadFile<string>('/ucard/upload/file', file)
+  }
+}

+ 39 - 0
src/api/user.ts

@@ -0,0 +1,39 @@
+import { $api } from '@/composables/fetch'
+import type { UserInfo } from '@/stores/use-user-store'
+
+export interface LoginParams {
+  loginName: string
+  password: string
+  emailCode: string
+}
+export interface TokenInfo {
+  data: string
+}
+
+export interface RegisterParams extends LoginParams {
+  email: string
+  phone: string
+}
+
+export interface ResetPasswordParams {
+  email: string
+  code: string
+  newPassword: string
+}
+
+export const userApi = {
+  // 登录
+  login: (params: LoginParams) => {
+    return $api.post<string>('/user/login', params)
+  },
+
+  // 重置密码
+  resetPassword: (params: ResetPasswordParams) => {
+    return $api.post('/user/reset-password', params)
+  },
+
+  // 获取用户信息
+  getUserInfo: () => {
+    return $api.post<UserInfo>('/user/info')
+  },
+}

BIN
src/assets/logo.png


+ 249 - 0
src/assets/scss/global/global.scss

@@ -0,0 +1,249 @@
+@use 'sass:math';
+
+:root {
+    // 设计稿宽度
+    --design-width: #{$vmDesignWidth}px;
+    --design-multiple: #{$vmDesignMultiple};
+
+    // 兼容最大宽度
+    --max-window: #{$vmMaxWindow};
+
+    // 兼容最小宽度
+    --min-window: #{$vmMinWindow};
+    --main-bg: #181a1b;
+    --card-bg: #222;
+    --action-bg: #232323;
+    --main-yellow: #d6ff00;
+    --main-yellow-dark: #b6c800;
+    --white: #fff;
+    --gray: #aaa;
+    --border: #333;
+    --black: #000;
+    --font-size-12: 12px;
+    --font-size-13: 13px;
+    --font-size-14: 14px;
+    --font-size-15: 15px;
+    --font-size-16: 16px;
+    --font-size-18: 18px;
+    --font-size-20: 20px;
+    --font-size-22: 22px;
+    --font-size-24: 24px;
+    --font-size-26: 26px;
+    --font-size-28: 28px;
+    --font-size-32: 32px;
+    --font-size-36: 36px;
+    --font-size-40: 40px;
+}
+
+html {
+    --body-width: #{$vmDesignWidth}px;
+    --tabbar-height: 50px;
+
+    font-size: math.div($vmFontSize, $vmDesignWidth) * 100vw;
+
+    // 同时,通过Media Queries 限制根元素最大最小值
+
+    @media screen and (max-width: $vmMinWindow) {
+        font-size: math.div($vmFontSize, $vmDesignWidth) * $vmMinWindow;
+    }
+    @media screen and (min-width: $vmMaxWindow) {
+        --body-width: $vmMaxWindow;
+
+        font-size: math.div($vmFontSize, $vmDesignWidth) * $vmMaxWindow;
+    }
+}
+
+// body 也增加最大最小宽度限制,避免默认100%宽度的 block 元素跟随 body 而过大过小
+body {
+    max-width: var(--max-window);
+    min-width: var(--min-window);
+    min-height: 100vh;
+    margin: 0 auto !important;
+    font-size: 12px;
+    line-height: 1;
+    color: #000;
+    background: #f4f4f4;
+}
+
+.fixed-center {
+    left: calc((100% - var(--body-width)) / 2) !important;
+    width: 100%;
+    max-width: var(--max-window);
+    min-width: var(--min-window);
+}
+
+.body {
+    position: absolute;
+    width: 100%;
+
+    // height: 100vh;
+    // padding-bottom: var(--tabbar-height);
+    height: 100vh;
+    max-width: var(--max-window);
+    min-width: var(--min-window);
+    overflow-y: scroll;
+    background: #f8f8f8;
+    box-sizing: border-box;
+    transition: all 0.4s;
+    -webkit-overflow-scrolling: touch;
+
+    &.is-tab {
+        height: calc(100vh - var(--tabbar-height));
+    }
+}
+
+.wrap {
+    position: relative;
+    width: 100%;
+    height: 100vh;
+    overflow: hidden;
+}
+
+.global-loading {
+    position: fixed;
+    top: 0;
+    left: 0;
+    z-index: 999;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    width: 100vw;
+    height: 100vh;
+    background: rgba(255, 255, 255, 0.1);
+}
+
+.router-loading {
+    position: fixed;
+    top: 0;
+    left: 0;
+    z-index: 1000;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    width: 100vw;
+    height: 100vh;
+    background: rgba(255, 255, 255, 0.1);
+}
+
+.load-more {
+    margin: 10px 0;
+}
+
+/* 通用样式 */
+
+.slide-left-enter,
+.slide-right-leave-active {
+    opacity: 0;
+    transform: translate(100%, 0);
+}
+
+.slide-left-leave-active,
+.slide-right-enter {
+    opacity: 0;
+    transform: translate(-100%, 0);
+}
+
+/* .fade-leave-active below version 2.1.8 */
+.fade-enter-active,
+.fade-leave-active {
+    transition: opacity 0.5s;
+}
+
+.fade-enter,
+.fade-leave-to {
+    opacity: 0;
+}
+
+/* start--文本行数限制--start */
+.u-line-1 {
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+}
+
+.u-line-2 {
+    -webkit-line-clamp: 2;
+}
+
+.u-line-3 {
+    -webkit-line-clamp: 3;
+}
+
+.u-line-4 {
+    -webkit-line-clamp: 4;
+}
+
+.u-line-5 {
+    -webkit-line-clamp: 5;
+}
+
+.u-line-2,
+.u-line-3,
+.u-line-4,
+.u-line-5 {
+    display: box; // 弹性伸缩盒
+    overflow: hidden;
+    text-overflow: ellipsis;
+    word-break: break-all;
+    -webkit-box-orient: vertical; // 设置伸缩盒子元素排列方式
+}
+
+/* end--文本行数限制--end */
+
+/* start--Retina 屏幕下的 1px 边框--start */
+.u-border,
+.u-border-bottom,
+.u-border-left,
+.u-border-right,
+.u-border-top,
+.u-border-top-bottom {
+    position: relative;
+}
+
+.u-border-bottom::after,
+.u-border-left::after,
+.u-border-right::after,
+.u-border-top-bottom::after,
+.u-border-top::after,
+.u-border::after {
+    position: absolute;
+    top: 0;
+    left: 0;
+    z-index: 2;
+
+    // 多加0.1%,能解决有时候边框缺失的问题
+    width: 199.8%;
+    height: 199.7%;
+    pointer-events: none;
+    border: 0 solid #e4e7ed;
+    content: ' ';
+    transform: scale(0.5, 0.5);
+    box-sizing: border-box;
+    transform-origin: 0 0;
+}
+
+.u-border-top::after {
+    border-top-width: 1PX;
+}
+
+.u-border-left::after {
+    border-left-width: 1PX;
+}
+
+.u-border-right::after {
+    border-right-width: 1PX;
+}
+
+.u-border-bottom::after {
+    border-bottom-width: 1PX;
+}
+
+.u-border-top-bottom::after {
+    border-width: 1PX 0;
+}
+
+.u-border::after {
+    border-width: 1PX;
+}
+
+/* end--Retina 屏幕下的 1px 边框--end */

+ 138 - 0
src/assets/scss/global/reset.scss

@@ -0,0 +1,138 @@
+* {
+    box-sizing: border-box;
+}
+
+/* 清除内外边距 */
+body,
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+hr,
+p,
+blockquote,
+dl,
+dt,
+dd,
+ul,
+ol,
+li,
+pre,
+fieldset,
+button,
+input,
+textarea,
+th,
+td { /* table elements 表格元素 */
+    padding: 0;
+    margin: 0;
+}
+
+/* 设置默认字体 */
+body,
+button,
+input,
+select,
+textarea { /* for ie */
+    font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', SimSun, sans-serif;
+    font-size: 12px;
+    outline: none;
+}
+
+h1 { font-size: 18px; /* 18px / 12px = 1.5 */ }
+h2 { font-size: 16px; }
+h3 { font-size: 14px; }
+
+h4,
+h5,
+h6 { font-size: 100%; }
+
+address,
+cite,
+dfn,
+em,
+var { font-style: normal; } /* 将斜体扶正 */
+code,
+kbd,
+pre,
+samp,
+tt { font-family: 'Courier New', Courier, monospace; } /* 统一等宽字体 */
+small { font-size: 12px; } /* 小于 12px 的中文很难阅读,让 small 正常化 */
+
+/* 重置列表元素 */
+ul,
+ol { list-style: none; }
+
+/* 重置文本格式元素 */
+a {
+    text-decoration: none;
+    -webkit-tap-highlight-color: rgba(255, 255, 255, 0);
+}
+a:hover { text-decoration: none; }
+
+abbr[title],
+acronym[title] { /* 注:1.ie6 不支持 abbr; 2.这里用了属性选择符,ie6 下无效果 */
+    border-bottom: 1px dotted;
+    cursor: help;
+}
+
+q::before,
+q::after { content: ''; }
+
+/* 重置表单元素 */
+legend { color: #000; } /* for ie6 */
+fieldset,
+img { border: none; } /* img 搭车:让链接里的 img 无边框 */
+
+/* 注:optgroup 无法扶正 */
+button,
+input,
+select,
+textarea {
+    font-size: 100%; /* 使得表单元素在 ie 下能继承字体大小 */
+}
+
+button {
+    cursor: pointer;
+    background: none;
+    border: none;
+    -webkit-tap-highlight-color: rgba(255, 255, 255, 0);
+}
+
+/* 重置表格元素 */
+table {
+    border-collapse: collapse;
+    border-spacing: 0;
+}
+
+/* 重置 hr */
+hr {
+    height: 1px;
+    border: none;
+}
+
+/* 让非ie浏览器默认也显示垂直滚动条,防止因滚动条引起的闪烁 */
+html { overflow: hidden; }
+
+/* 其他常用样式 */
+.hidden {
+    display: none !important;
+}
+
+.disabled {
+    pointer-events: none;
+    cursor: not-allowed;
+    opacity: 0.65;
+    filter: alpha(opacity=65);
+    box-shadow: none;
+}
+
+::-webkit-scrollbar {
+    display: none;
+    width: 0 !important;
+    height: 0 !important;
+    appearance: none;
+    background: transparent;
+}

+ 48 - 0
src/assets/scss/global/vant.scss

@@ -0,0 +1,48 @@
+.van-button:active::before {
+    opacity: 0;
+}
+
+// 防止 414 屏幕弹窗时出现内容下移
+.van-dialog__header {
+    padding-top: 20px;
+
+    // line-height: 2.5px;
+}
+
+.wrap {
+    .van-tabbar {
+        position: absolute !important;
+        border-top: 1px solid #f8f8f8;
+        box-sizing: border-box;
+        z-index: 999 !important;
+        overflow: hidden;
+    }
+
+    .body {
+        .van-nav-bar {
+            border-bottom: 1px solid #f8f8f8;
+            box-sizing: border-box;
+            z-index: 999 !important;
+        }
+
+        .van-skeleton__row,
+        .van-skeleton__title {
+            background-color: #ccc;
+        }
+
+        .van-button + .van-button {
+            margin-left: 5px;
+        }
+    }
+
+    .van-tabs {
+
+        @media screen and (width >= 540px) {
+            .van-sticky--fixed {
+                left: 50%;
+                width: 540PX;
+                transform: translateX(-50%);
+            }
+        }
+    }
+}

+ 50 - 0
src/assets/scss/pages/about.scss

@@ -0,0 +1,50 @@
+.mt-10 {
+    margin-top: 10px;
+}
+
+.about-wrap {
+    .van-panel__content {
+        padding: 6.5px;
+        font-size: 12px;
+    }
+
+    .van-panel__footer {
+        text-align: right;
+    }
+}
+
+.about-detail-wrap {
+    padding-top: 60px;
+}
+
+.avatar-wrap {
+    .croppa-container {
+        display: block;
+        width: 200Px;
+        height: 200Px;
+        margin: 10px auto;
+        background-color: lightblue;
+
+        // border: 2px solid grey;
+        border-radius: 4px;
+        box-sizing: content-box;
+
+        canvas {
+            border-radius: 0.08rem;
+        }
+    }
+
+    .croppa-container:hover {
+        background-color: #8ac9ef;
+        opacity: 1;
+    }
+
+    .preview {
+        margin-top: 15px;
+        text-align: center;
+
+        img {
+            max-width: 100px;
+        }
+    }
+}

+ 98 - 0
src/assets/scss/pages/home.scss

@@ -0,0 +1,98 @@
+.home-wrap {
+    .van-pull-refresh {
+        min-height: calc(100vh - var(--tabbar-height));
+
+        .van-pull-refresh__track {
+            min-height: calc(100vh - var(--tabbar-height));
+        }
+    }
+
+    .scroller {
+        height: 100vh;
+    }
+}
+
+.home-normal-wrap {
+    .lists {
+        padding: 0 15px;
+        background: #fff;
+
+        .item {
+            padding: 10px 0;
+
+            a {
+                line-height: 1.5;
+                color: #323232;
+            }
+        }
+    }
+}
+
+.home-detail-wrap {
+    padding-top: 46px;
+    background: #fff;
+
+    .van-nav-bar__text:active {
+        background: #fff;
+    }
+
+    .article-content {
+        padding: 10px;
+
+        .title {
+            font-size: 16px;
+            line-height: 20px;
+            text-align: center;
+        }
+
+        .date-time {
+            margin-top: 5px;
+            color: #666;
+            text-align: center;
+        }
+
+        .content {
+            padding-top: 10px;
+            font-size: 14px;
+            color: #333;
+
+            p {
+                line-height: 1.5;
+
+                & + p {
+                    margin-top: 5px;
+                }
+
+                img {
+                    max-width: 100%;
+                }
+            }
+
+            pre {
+                width: 100%;
+                padding: 10px 0;
+                overflow: scroll hidden;
+                -webkit-overflow-scrolling: touch;
+            }
+        }
+    }
+
+    .replies {
+        overflow: hidden;
+        background: #f4f4f4;
+
+        .van-panel {
+            margin-top: 10px;
+        }
+
+        .van-panel__content {
+            padding: 10px;
+        }
+
+        .markdown-text {
+            img {
+                max-width: 100%;
+            }
+        }
+    }
+}

+ 4 - 0
src/assets/scss/style.scss

@@ -0,0 +1,4 @@
+@use 'global/reset';
+@use 'global/vant';
+@use './pages/home';
+@use './pages/about';

Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
src/assets/svgs/mp3.svg


+ 686 - 0
src/auto-imports.d.ts

@@ -0,0 +1,686 @@
+/* eslint-disable */
+/* prettier-ignore */
+// @ts-nocheck
+// noinspection JSUnusedGlobalSymbols
+// Generated by unplugin-auto-import
+// biome-ignore lint: disable
+export {}
+declare global {
+  const $Api: typeof import('./api/index')['$Api']
+  const $api: typeof import('./composables/fetch')['$api']
+  const CLIENT: typeof import('./composables/config')['CLIENT']
+  const EffectScope: typeof import('vue')['EffectScope']
+  const UTC2Date: typeof import('@lincy/utils')['UTC2Date']
+  const UseTabLists: typeof import('./composables/index')['UseTabLists']
+  const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
+  const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
+  const closeToast: typeof import('vant')['closeToast']
+  const computed: typeof import('vue')['computed']
+  const computedAsync: typeof import('@vueuse/core')['computedAsync']
+  const computedEager: typeof import('@vueuse/core')['computedEager']
+  const computedInject: typeof import('@vueuse/core')['computedInject']
+  const computedWithControl: typeof import('@vueuse/core')['computedWithControl']
+  const config: typeof import('./composables/config')['default']
+  const controlledComputed: typeof import('@vueuse/core')['controlledComputed']
+  const controlledRef: typeof import('@vueuse/core')['controlledRef']
+  const createApp: typeof import('vue')['createApp']
+  const createEventHook: typeof import('@vueuse/core')['createEventHook']
+  const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
+  const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
+  const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
+  const createRef: typeof import('@vueuse/core')['createRef']
+  const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate']
+  const createRouter: typeof import('vue-router')['createRouter']
+  const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
+  const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise']
+  const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
+  const createWebHashHistory: typeof import('vue-router')['createWebHashHistory']
+  const crypt: typeof import('./composables/crypt')['default']
+  const customRef: typeof import('vue')['customRef']
+  const dataHasErrorKey: typeof import('./composables/provide')['dataHasErrorKey']
+  const dataIsReadyKey: typeof import('./composables/provide')['dataIsReadyKey']
+  const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
+  const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']
+  const deepClone: typeof import('@lincy/utils')['deepClone']
+  const deepMerge: typeof import('@lincy/utils')['deepMerge']
+  const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
+  const defineComponent: typeof import('vue')['defineComponent']
+  const defineStore: typeof import('pinia')['defineStore']
+  const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
+  const effectScope: typeof import('vue')['effectScope']
+  const extendRef: typeof import('@vueuse/core')['extendRef']
+  const getCurrentInstance: typeof import('vue')['getCurrentInstance']
+  const getCurrentScope: typeof import('vue')['getCurrentScope']
+  const h: typeof import('vue')['h']
+  const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
+  const inject: typeof import('vue')['inject']
+  const injectLocal: typeof import('@vueuse/core')['injectLocal']
+  const isDefined: typeof import('@vueuse/core')['isDefined']
+  const isProxy: typeof import('vue')['isProxy']
+  const isReactive: typeof import('vue')['isReactive']
+  const isReadonly: typeof import('vue')['isReadonly']
+  const isRef: typeof import('vue')['isRef']
+  const jsxComponents: typeof import('./components/jsx-components')['default']
+  const lang: typeof import('./composables/config')['lang']
+  const loginMsgBox: typeof import('./composables/message')['loginMsgBox']
+  const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
+  const markRaw: typeof import('vue')['markRaw']
+  const nextTick: typeof import('vue')['nextTick']
+  const onActivated: typeof import('vue')['onActivated']
+  const onBeforeMount: typeof import('vue')['onBeforeMount']
+  const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
+  const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
+  const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
+  const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
+  const onClickOutside: typeof import('@vueuse/core')['onClickOutside']
+  const onDeactivated: typeof import('vue')['onDeactivated']
+  const onElementRemoval: typeof import('@vueuse/core')['onElementRemoval']
+  const onErrorCaptured: typeof import('vue')['onErrorCaptured']
+  const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke']
+  const onLoginKey: typeof import('./composables/provide')['onLoginKey']
+  const onLongPress: typeof import('@vueuse/core')['onLongPress']
+  const onMounted: typeof import('vue')['onMounted']
+  const onRenderTracked: typeof import('vue')['onRenderTracked']
+  const onRenderTriggered: typeof import('vue')['onRenderTriggered']
+  const onScopeDispose: typeof import('vue')['onScopeDispose']
+  const onServerPrefetch: typeof import('vue')['onServerPrefetch']
+  const onStartTyping: typeof import('@vueuse/core')['onStartTyping']
+  const onUnmounted: typeof import('vue')['onUnmounted']
+  const onUpdated: typeof import('vue')['onUpdated']
+  const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
+  const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
+  const provide: typeof import('vue')['provide']
+  const provideLocal: typeof import('@vueuse/core')['provideLocal']
+  const reactify: typeof import('@vueuse/core')['reactify']
+  const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
+  const reactive: typeof import('vue')['reactive']
+  const reactiveComputed: typeof import('@vueuse/core')['reactiveComputed']
+  const reactiveOmit: typeof import('@vueuse/core')['reactiveOmit']
+  const reactivePick: typeof import('@vueuse/core')['reactivePick']
+  const readonly: typeof import('vue')['readonly']
+  const ref: typeof import('vue')['ref']
+  const refAutoReset: typeof import('@vueuse/core')['refAutoReset']
+  const refDebounced: typeof import('@vueuse/core')['refDebounced']
+  const refDefault: typeof import('@vueuse/core')['refDefault']
+  const refThrottled: typeof import('@vueuse/core')['refThrottled']
+  const refWithControl: typeof import('@vueuse/core')['refWithControl']
+  const resolveComponent: typeof import('vue')['resolveComponent']
+  const resolveRef: typeof import('@vueuse/core')['resolveRef']
+  const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
+  const routerKey: typeof import('./composables/provide')['routerKey']
+  const shallowReactive: typeof import('vue')['shallowReactive']
+  const shallowReadonly: typeof import('vue')['shallowReadonly']
+  const shallowRef: typeof import('vue')['shallowRef']
+  const showConfirmDialog: typeof import('vant')['showConfirmDialog']
+  const showDialog: typeof import('vant')['showDialog']
+  const showDialogKey: typeof import('./composables/provide')['showDialogKey']
+  const showFailToast: typeof import('vant')['showFailToast']
+  const showImagePreview: typeof import('vant')['showImagePreview']
+  const showLoadingToast: typeof import('vant')['showLoadingToast']
+  const showMsg: typeof import('./composables/message')['showMsg']
+  const showSuccessToast: typeof import('vant')['showSuccessToast']
+  const showToast: typeof import('vant')['showToast']
+  const storeToRefs: typeof import('pinia')['storeToRefs']
+  const syncRef: typeof import('@vueuse/core')['syncRef']
+  const syncRefs: typeof import('@vueuse/core')['syncRefs']
+  const templateRef: typeof import('@vueuse/core')['templateRef']
+  const throttledRef: typeof import('@vueuse/core')['throttledRef']
+  const throttledWatch: typeof import('@vueuse/core')['throttledWatch']
+  const toRaw: typeof import('vue')['toRaw']
+  const toReactive: typeof import('@vueuse/core')['toReactive']
+  const toRef: typeof import('vue')['toRef']
+  const toRefs: typeof import('vue')['toRefs']
+  const toValue: typeof import('vue')['toValue']
+  const toggleDialogKey: typeof import('./composables/provide')['toggleDialogKey']
+  const triggerRef: typeof import('vue')['triggerRef']
+  const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount']
+  const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
+  const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted']
+  const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose']
+  const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted']
+  const unref: typeof import('vue')['unref']
+  const unrefElement: typeof import('@vueuse/core')['unrefElement']
+  const until: typeof import('@vueuse/core')['until']
+  const updateParentKey: typeof import('./composables/provide')['updateParentKey']
+  const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
+  const useAnimate: typeof import('@vueuse/core')['useAnimate']
+  const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference']
+  const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery']
+  const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter']
+  const useArrayFind: typeof import('@vueuse/core')['useArrayFind']
+  const useArrayFindIndex: typeof import('@vueuse/core')['useArrayFindIndex']
+  const useArrayFindLast: typeof import('@vueuse/core')['useArrayFindLast']
+  const useArrayIncludes: typeof import('@vueuse/core')['useArrayIncludes']
+  const useArrayJoin: typeof import('@vueuse/core')['useArrayJoin']
+  const useArrayMap: typeof import('@vueuse/core')['useArrayMap']
+  const useArrayReduce: typeof import('@vueuse/core')['useArrayReduce']
+  const useArraySome: typeof import('@vueuse/core')['useArraySome']
+  const useArrayUnique: typeof import('@vueuse/core')['useArrayUnique']
+  const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
+  const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
+  const useAttrs: typeof import('vue')['useAttrs']
+  const useBase64: typeof import('@vueuse/core')['useBase64']
+  const useBattery: typeof import('@vueuse/core')['useBattery']
+  const useBluetooth: typeof import('@vueuse/core')['useBluetooth']
+  const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints']
+  const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel']
+  const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
+  const useCached: typeof import('@vueuse/core')['useCached']
+  const useClipboard: typeof import('@vueuse/core')['useClipboard']
+  const useClipboardItems: typeof import('@vueuse/core')['useClipboardItems']
+  const useCloned: typeof import('@vueuse/core')['useCloned']
+  const useColorMode: typeof import('@vueuse/core')['useColorMode']
+  const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
+  const useCountdown: typeof import('@vueuse/core')['useCountdown']
+  const useCounter: typeof import('@vueuse/core')['useCounter']
+  const useCssModule: typeof import('vue')['useCssModule']
+  const useCssVar: typeof import('@vueuse/core')['useCssVar']
+  const useCssVars: typeof import('vue')['useCssVars']
+  const useCurrentElement: typeof import('@vueuse/core')['useCurrentElement']
+  const useCycleList: typeof import('@vueuse/core')['useCycleList']
+  const useDark: typeof import('@vueuse/core')['useDark']
+  const useDateFormat: typeof import('@vueuse/core')['useDateFormat']
+  const useDebounce: typeof import('@vueuse/core')['useDebounce']
+  const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn']
+  const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory']
+  const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion']
+  const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation']
+  const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio']
+  const useDevicesList: typeof import('@vueuse/core')['useDevicesList']
+  const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia']
+  const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility']
+  const useDraggable: typeof import('@vueuse/core')['useDraggable']
+  const useDropZone: typeof import('@vueuse/core')['useDropZone']
+  const useElementBounding: typeof import('@vueuse/core')['useElementBounding']
+  const useElementByPoint: typeof import('@vueuse/core')['useElementByPoint']
+  const useElementHover: typeof import('@vueuse/core')['useElementHover']
+  const useElementSize: typeof import('@vueuse/core')['useElementSize']
+  const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility']
+  const useEventBus: typeof import('@vueuse/core')['useEventBus']
+  const useEventListener: typeof import('@vueuse/core')['useEventListener']
+  const useEventSource: typeof import('@vueuse/core')['useEventSource']
+  const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper']
+  const useFavicon: typeof import('@vueuse/core')['useFavicon']
+  const useFetch: typeof import('@vueuse/core')['useFetch']
+  const useFileDialog: typeof import('@vueuse/core')['useFileDialog']
+  const useFileSystemAccess: typeof import('@vueuse/core')['useFileSystemAccess']
+  const useFilters: typeof import('./composables/filters')['useFilters']
+  const useFocus: typeof import('@vueuse/core')['useFocus']
+  const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin']
+  const useFps: typeof import('@vueuse/core')['useFps']
+  const useFullscreen: typeof import('@vueuse/core')['useFullscreen']
+  const useGamepad: typeof import('@vueuse/core')['useGamepad']
+  const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
+  const useGlobal: typeof import('./composables/index')['useGlobal']
+  const useGlobalStore: typeof import('./stores/use-global-store')['default']
+  const useHead: typeof import('@unhead/vue')['useHead']
+  const useId: typeof import('vue')['useId']
+  const useIdle: typeof import('@vueuse/core')['useIdle']
+  const useImage: typeof import('@vueuse/core')['useImage']
+  const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
+  const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver']
+  const useInterval: typeof import('@vueuse/core')['useInterval']
+  const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn']
+  const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier']
+  const useLastChanged: typeof import('@vueuse/core')['useLastChanged']
+  const useLink: typeof import('vue-router')['useLink']
+  const useLists: typeof import('./composables/index')['useLists']
+  const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage']
+  const useLockFn: typeof import('./composables/index')['useLockFn']
+  const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys']
+  const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory']
+  const useMediaControls: typeof import('@vueuse/core')['useMediaControls']
+  const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery']
+  const useMemoize: typeof import('@vueuse/core')['useMemoize']
+  const useMemory: typeof import('@vueuse/core')['useMemory']
+  const useModel: typeof import('vue')['useModel']
+  const useMounted: typeof import('@vueuse/core')['useMounted']
+  const useMouse: typeof import('@vueuse/core')['useMouse']
+  const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']
+  const useMousePressed: typeof import('@vueuse/core')['useMousePressed']
+  const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver']
+  const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage']
+  const useNetwork: typeof import('@vueuse/core')['useNetwork']
+  const useNow: typeof import('@vueuse/core')['useNow']
+  const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl']
+  const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination']
+  const useOnline: typeof import('@vueuse/core')['useOnline']
+  const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
+  const useParallax: typeof import('@vueuse/core')['useParallax']
+  const useParentElement: typeof import('@vueuse/core')['useParentElement']
+  const usePerformanceObserver: typeof import('@vueuse/core')['usePerformanceObserver']
+  const usePermission: typeof import('@vueuse/core')['usePermission']
+  const usePointer: typeof import('@vueuse/core')['usePointer']
+  const usePointerLock: typeof import('@vueuse/core')['usePointerLock']
+  const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
+  const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
+  const usePreferredContrast: typeof import('@vueuse/core')['usePreferredContrast']
+  const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
+  const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
+  const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
+  const usePreferredReducedTransparency: typeof import('@vueuse/core')['usePreferredReducedTransparency']
+  const usePrevious: typeof import('@vueuse/core')['usePrevious']
+  const useRafFn: typeof import('@vueuse/core')['useRafFn']
+  const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
+  const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
+  const useRoute: typeof import('vue-router')['useRoute']
+  const useRouter: typeof import('vue-router')['useRouter']
+  const useSSRWidth: typeof import('@vueuse/core')['useSSRWidth']
+  const useSaveScroll: typeof import('./composables/index')['useSaveScroll']
+  const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
+  const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
+  const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
+  const useScroll: typeof import('@vueuse/core')['useScroll']
+  const useScrollLock: typeof import('@vueuse/core')['useScrollLock']
+  const useSeoMeta: typeof import('@vueuse/head')['useSeoMeta']
+  const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
+  const useShare: typeof import('@vueuse/core')['useShare']
+  const useSlots: typeof import('vue')['useSlots']
+  const useSorted: typeof import('@vueuse/core')['useSorted']
+  const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
+  const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis']
+  const useStepper: typeof import('@vueuse/core')['useStepper']
+  const useStorage: typeof import('@vueuse/core')['useStorage']
+  const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
+  const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
+  const useSupported: typeof import('@vueuse/core')['useSupported']
+  const useSwipe: typeof import('@vueuse/core')['useSwipe']
+  const useTabLists: typeof import('./composables/index')['useTabLists']
+  const useTemplateRef: typeof import('vue')['useTemplateRef']
+  const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
+  const useTextDirection: typeof import('@vueuse/core')['useTextDirection']
+  const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
+  const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize']
+  const useThrottle: typeof import('@vueuse/core')['useThrottle']
+  const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn']
+  const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory']
+  const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo']
+  const useTimeout: typeof import('@vueuse/core')['useTimeout']
+  const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn']
+  const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
+  const useTimestamp: typeof import('@vueuse/core')['useTimestamp']
+  const useTitle: typeof import('@vueuse/core')['useTitle']
+  const useToNumber: typeof import('@vueuse/core')['useToNumber']
+  const useToString: typeof import('@vueuse/core')['useToString']
+  const useToggle: typeof import('@vueuse/core')['useToggle']
+  const useTransition: typeof import('@vueuse/core')['useTransition']
+  const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
+  const useUserMedia: typeof import('@vueuse/core')['useUserMedia']
+  const useUserStore: typeof import('./stores/use-user-store')['default']
+  const useVModel: typeof import('@vueuse/core')['useVModel']
+  const useVModels: typeof import('@vueuse/core')['useVModels']
+  const useVibrate: typeof import('@vueuse/core')['useVibrate']
+  const useVirtualList: typeof import('@vueuse/core')['useVirtualList']
+  const useWakeLock: typeof import('@vueuse/core')['useWakeLock']
+  const useWebNotification: typeof import('@vueuse/core')['useWebNotification']
+  const useWebSocket: typeof import('@vueuse/core')['useWebSocket']
+  const useWebWorker: typeof import('@vueuse/core')['useWebWorker']
+  const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn']
+  const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus']
+  const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll']
+  const useWindowSize: typeof import('@vueuse/core')['useWindowSize']
+  const userToken: typeof import('./composables/config')['userToken']
+  const watch: typeof import('vue')['watch']
+  const watchArray: typeof import('@vueuse/core')['watchArray']
+  const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
+  const watchDebounced: typeof import('@vueuse/core')['watchDebounced']
+  const watchDeep: typeof import('@vueuse/core')['watchDeep']
+  const watchEffect: typeof import('vue')['watchEffect']
+  const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable']
+  const watchImmediate: typeof import('@vueuse/core')['watchImmediate']
+  const watchOnce: typeof import('@vueuse/core')['watchOnce']
+  const watchPausable: typeof import('@vueuse/core')['watchPausable']
+  const watchPostEffect: typeof import('vue')['watchPostEffect']
+  const watchSyncEffect: typeof import('vue')['watchSyncEffect']
+  const watchThrottled: typeof import('@vueuse/core')['watchThrottled']
+  const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable']
+  const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']
+  const whenever: typeof import('@vueuse/core')['whenever']
+}
+// for type re-export
+declare global {
+  // @ts-ignore
+  export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
+  import('vue')
+  // @ts-ignore
+  export type { GlobalState } from './stores/pinia.types'
+  import('./stores/pinia.types')
+  // @ts-ignore
+  export type { UserInfo } from './stores/use-user-store'
+  import('./stores/use-user-store')
+}
+
+// for vue template auto import
+import { UnwrapRef } from 'vue'
+declare module 'vue' {
+  interface GlobalComponents {}
+  interface ComponentCustomProperties {
+    readonly $api: UnwrapRef<typeof import('./composables/fetch')['$api']>
+    readonly CLIENT: UnwrapRef<typeof import('./composables/config')['CLIENT']>
+    readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
+    readonly UTC2Date: UnwrapRef<typeof import('@lincy/utils')['UTC2Date']>
+    readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
+    readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
+    readonly closeToast: UnwrapRef<typeof import('vant')['closeToast']>
+    readonly computed: UnwrapRef<typeof import('vue')['computed']>
+    readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']>
+    readonly computedEager: UnwrapRef<typeof import('@vueuse/core')['computedEager']>
+    readonly computedInject: UnwrapRef<typeof import('@vueuse/core')['computedInject']>
+    readonly computedWithControl: UnwrapRef<typeof import('@vueuse/core')['computedWithControl']>
+    readonly config: UnwrapRef<typeof import('./composables/config')['default']>
+    readonly controlledComputed: UnwrapRef<typeof import('@vueuse/core')['controlledComputed']>
+    readonly controlledRef: UnwrapRef<typeof import('@vueuse/core')['controlledRef']>
+    readonly createApp: UnwrapRef<typeof import('vue')['createApp']>
+    readonly createEventHook: UnwrapRef<typeof import('@vueuse/core')['createEventHook']>
+    readonly createGlobalState: UnwrapRef<typeof import('@vueuse/core')['createGlobalState']>
+    readonly createInjectionState: UnwrapRef<typeof import('@vueuse/core')['createInjectionState']>
+    readonly createReactiveFn: UnwrapRef<typeof import('@vueuse/core')['createReactiveFn']>
+    readonly createRef: UnwrapRef<typeof import('@vueuse/core')['createRef']>
+    readonly createReusableTemplate: UnwrapRef<typeof import('@vueuse/core')['createReusableTemplate']>
+    readonly createRouter: UnwrapRef<typeof import('vue-router')['createRouter']>
+    readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']>
+    readonly createTemplatePromise: UnwrapRef<typeof import('@vueuse/core')['createTemplatePromise']>
+    readonly createUnrefFn: UnwrapRef<typeof import('@vueuse/core')['createUnrefFn']>
+    readonly createWebHashHistory: UnwrapRef<typeof import('vue-router')['createWebHashHistory']>
+    readonly crypt: UnwrapRef<typeof import('./composables/crypt')['default']>
+    readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
+    readonly dataHasErrorKey: UnwrapRef<typeof import('./composables/provide')['dataHasErrorKey']>
+    readonly dataIsReadyKey: UnwrapRef<typeof import('./composables/provide')['dataIsReadyKey']>
+    readonly debouncedRef: UnwrapRef<typeof import('@vueuse/core')['debouncedRef']>
+    readonly debouncedWatch: UnwrapRef<typeof import('@vueuse/core')['debouncedWatch']>
+    readonly deepClone: UnwrapRef<typeof import('@lincy/utils')['deepClone']>
+    readonly deepMerge: UnwrapRef<typeof import('@lincy/utils')['deepMerge']>
+    readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
+    readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
+    readonly defineStore: UnwrapRef<typeof import('pinia')['defineStore']>
+    readonly eagerComputed: UnwrapRef<typeof import('@vueuse/core')['eagerComputed']>
+    readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
+    readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']>
+    readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
+    readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
+    readonly h: UnwrapRef<typeof import('vue')['h']>
+    readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
+    readonly inject: UnwrapRef<typeof import('vue')['inject']>
+    readonly injectLocal: UnwrapRef<typeof import('@vueuse/core')['injectLocal']>
+    readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']>
+    readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
+    readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
+    readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
+    readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
+    readonly jsxComponents: UnwrapRef<typeof import('./components/jsx-components')['default']>
+    readonly lang: UnwrapRef<typeof import('./composables/config')['lang']>
+    readonly loginMsgBox: UnwrapRef<typeof import('./composables/message')['loginMsgBox']>
+    readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
+    readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
+    readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
+    readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
+    readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
+    readonly onBeforeRouteLeave: UnwrapRef<typeof import('vue-router')['onBeforeRouteLeave']>
+    readonly onBeforeRouteUpdate: UnwrapRef<typeof import('vue-router')['onBeforeRouteUpdate']>
+    readonly onBeforeUnmount: UnwrapRef<typeof import('vue')['onBeforeUnmount']>
+    readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
+    readonly onClickOutside: UnwrapRef<typeof import('@vueuse/core')['onClickOutside']>
+    readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
+    readonly onElementRemoval: UnwrapRef<typeof import('@vueuse/core')['onElementRemoval']>
+    readonly onErrorCaptured: UnwrapRef<typeof import('vue')['onErrorCaptured']>
+    readonly onKeyStroke: UnwrapRef<typeof import('@vueuse/core')['onKeyStroke']>
+    readonly onLoginKey: UnwrapRef<typeof import('./composables/provide')['onLoginKey']>
+    readonly onLongPress: UnwrapRef<typeof import('@vueuse/core')['onLongPress']>
+    readonly onMounted: UnwrapRef<typeof import('vue')['onMounted']>
+    readonly onRenderTracked: UnwrapRef<typeof import('vue')['onRenderTracked']>
+    readonly onRenderTriggered: UnwrapRef<typeof import('vue')['onRenderTriggered']>
+    readonly onScopeDispose: UnwrapRef<typeof import('vue')['onScopeDispose']>
+    readonly onServerPrefetch: UnwrapRef<typeof import('vue')['onServerPrefetch']>
+    readonly onStartTyping: UnwrapRef<typeof import('@vueuse/core')['onStartTyping']>
+    readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
+    readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
+    readonly onWatcherCleanup: UnwrapRef<typeof import('vue')['onWatcherCleanup']>
+    readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
+    readonly provide: UnwrapRef<typeof import('vue')['provide']>
+    readonly provideLocal: UnwrapRef<typeof import('@vueuse/core')['provideLocal']>
+    readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
+    readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']>
+    readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
+    readonly reactiveComputed: UnwrapRef<typeof import('@vueuse/core')['reactiveComputed']>
+    readonly reactiveOmit: UnwrapRef<typeof import('@vueuse/core')['reactiveOmit']>
+    readonly reactivePick: UnwrapRef<typeof import('@vueuse/core')['reactivePick']>
+    readonly readonly: UnwrapRef<typeof import('vue')['readonly']>
+    readonly ref: UnwrapRef<typeof import('vue')['ref']>
+    readonly refAutoReset: UnwrapRef<typeof import('@vueuse/core')['refAutoReset']>
+    readonly refDebounced: UnwrapRef<typeof import('@vueuse/core')['refDebounced']>
+    readonly refDefault: UnwrapRef<typeof import('@vueuse/core')['refDefault']>
+    readonly refThrottled: UnwrapRef<typeof import('@vueuse/core')['refThrottled']>
+    readonly refWithControl: UnwrapRef<typeof import('@vueuse/core')['refWithControl']>
+    readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
+    readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
+    readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
+    readonly routerKey: UnwrapRef<typeof import('./composables/provide')['routerKey']>
+    readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
+    readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
+    readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
+    readonly showConfirmDialog: UnwrapRef<typeof import('vant')['showConfirmDialog']>
+    readonly showDialog: UnwrapRef<typeof import('vant')['showDialog']>
+    readonly showDialogKey: UnwrapRef<typeof import('./composables/provide')['showDialogKey']>
+    readonly showFailToast: UnwrapRef<typeof import('vant')['showFailToast']>
+    readonly showImagePreview: UnwrapRef<typeof import('vant')['showImagePreview']>
+    readonly showLoadingToast: UnwrapRef<typeof import('vant')['showLoadingToast']>
+    readonly showMsg: UnwrapRef<typeof import('./composables/message')['showMsg']>
+    readonly showSuccessToast: UnwrapRef<typeof import('vant')['showSuccessToast']>
+    readonly showToast: UnwrapRef<typeof import('vant')['showToast']>
+    readonly storeToRefs: UnwrapRef<typeof import('pinia')['storeToRefs']>
+    readonly syncRef: UnwrapRef<typeof import('@vueuse/core')['syncRef']>
+    readonly syncRefs: UnwrapRef<typeof import('@vueuse/core')['syncRefs']>
+    readonly templateRef: UnwrapRef<typeof import('@vueuse/core')['templateRef']>
+    readonly throttledRef: UnwrapRef<typeof import('@vueuse/core')['throttledRef']>
+    readonly throttledWatch: UnwrapRef<typeof import('@vueuse/core')['throttledWatch']>
+    readonly toRaw: UnwrapRef<typeof import('vue')['toRaw']>
+    readonly toReactive: UnwrapRef<typeof import('@vueuse/core')['toReactive']>
+    readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
+    readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
+    readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
+    readonly toggleDialogKey: UnwrapRef<typeof import('./composables/provide')['toggleDialogKey']>
+    readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
+    readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']>
+    readonly tryOnBeforeUnmount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeUnmount']>
+    readonly tryOnMounted: UnwrapRef<typeof import('@vueuse/core')['tryOnMounted']>
+    readonly tryOnScopeDispose: UnwrapRef<typeof import('@vueuse/core')['tryOnScopeDispose']>
+    readonly tryOnUnmounted: UnwrapRef<typeof import('@vueuse/core')['tryOnUnmounted']>
+    readonly unref: UnwrapRef<typeof import('vue')['unref']>
+    readonly unrefElement: UnwrapRef<typeof import('@vueuse/core')['unrefElement']>
+    readonly until: UnwrapRef<typeof import('@vueuse/core')['until']>
+    readonly updateParentKey: UnwrapRef<typeof import('./composables/provide')['updateParentKey']>
+    readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']>
+    readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']>
+    readonly useArrayDifference: UnwrapRef<typeof import('@vueuse/core')['useArrayDifference']>
+    readonly useArrayEvery: UnwrapRef<typeof import('@vueuse/core')['useArrayEvery']>
+    readonly useArrayFilter: UnwrapRef<typeof import('@vueuse/core')['useArrayFilter']>
+    readonly useArrayFind: UnwrapRef<typeof import('@vueuse/core')['useArrayFind']>
+    readonly useArrayFindIndex: UnwrapRef<typeof import('@vueuse/core')['useArrayFindIndex']>
+    readonly useArrayFindLast: UnwrapRef<typeof import('@vueuse/core')['useArrayFindLast']>
+    readonly useArrayIncludes: UnwrapRef<typeof import('@vueuse/core')['useArrayIncludes']>
+    readonly useArrayJoin: UnwrapRef<typeof import('@vueuse/core')['useArrayJoin']>
+    readonly useArrayMap: UnwrapRef<typeof import('@vueuse/core')['useArrayMap']>
+    readonly useArrayReduce: UnwrapRef<typeof import('@vueuse/core')['useArrayReduce']>
+    readonly useArraySome: UnwrapRef<typeof import('@vueuse/core')['useArraySome']>
+    readonly useArrayUnique: UnwrapRef<typeof import('@vueuse/core')['useArrayUnique']>
+    readonly useAsyncQueue: UnwrapRef<typeof import('@vueuse/core')['useAsyncQueue']>
+    readonly useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']>
+    readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
+    readonly useBase64: UnwrapRef<typeof import('@vueuse/core')['useBase64']>
+    readonly useBattery: UnwrapRef<typeof import('@vueuse/core')['useBattery']>
+    readonly useBluetooth: UnwrapRef<typeof import('@vueuse/core')['useBluetooth']>
+    readonly useBreakpoints: UnwrapRef<typeof import('@vueuse/core')['useBreakpoints']>
+    readonly useBroadcastChannel: UnwrapRef<typeof import('@vueuse/core')['useBroadcastChannel']>
+    readonly useBrowserLocation: UnwrapRef<typeof import('@vueuse/core')['useBrowserLocation']>
+    readonly useCached: UnwrapRef<typeof import('@vueuse/core')['useCached']>
+    readonly useClipboard: UnwrapRef<typeof import('@vueuse/core')['useClipboard']>
+    readonly useClipboardItems: UnwrapRef<typeof import('@vueuse/core')['useClipboardItems']>
+    readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']>
+    readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']>
+    readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']>
+    readonly useCountdown: UnwrapRef<typeof import('@vueuse/core')['useCountdown']>
+    readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']>
+    readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
+    readonly useCssVar: UnwrapRef<typeof import('@vueuse/core')['useCssVar']>
+    readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
+    readonly useCurrentElement: UnwrapRef<typeof import('@vueuse/core')['useCurrentElement']>
+    readonly useCycleList: UnwrapRef<typeof import('@vueuse/core')['useCycleList']>
+    readonly useDark: UnwrapRef<typeof import('@vueuse/core')['useDark']>
+    readonly useDateFormat: UnwrapRef<typeof import('@vueuse/core')['useDateFormat']>
+    readonly useDebounce: UnwrapRef<typeof import('@vueuse/core')['useDebounce']>
+    readonly useDebounceFn: UnwrapRef<typeof import('@vueuse/core')['useDebounceFn']>
+    readonly useDebouncedRefHistory: UnwrapRef<typeof import('@vueuse/core')['useDebouncedRefHistory']>
+    readonly useDeviceMotion: UnwrapRef<typeof import('@vueuse/core')['useDeviceMotion']>
+    readonly useDeviceOrientation: UnwrapRef<typeof import('@vueuse/core')['useDeviceOrientation']>
+    readonly useDevicePixelRatio: UnwrapRef<typeof import('@vueuse/core')['useDevicePixelRatio']>
+    readonly useDevicesList: UnwrapRef<typeof import('@vueuse/core')['useDevicesList']>
+    readonly useDisplayMedia: UnwrapRef<typeof import('@vueuse/core')['useDisplayMedia']>
+    readonly useDocumentVisibility: UnwrapRef<typeof import('@vueuse/core')['useDocumentVisibility']>
+    readonly useDraggable: UnwrapRef<typeof import('@vueuse/core')['useDraggable']>
+    readonly useDropZone: UnwrapRef<typeof import('@vueuse/core')['useDropZone']>
+    readonly useElementBounding: UnwrapRef<typeof import('@vueuse/core')['useElementBounding']>
+    readonly useElementByPoint: UnwrapRef<typeof import('@vueuse/core')['useElementByPoint']>
+    readonly useElementHover: UnwrapRef<typeof import('@vueuse/core')['useElementHover']>
+    readonly useElementSize: UnwrapRef<typeof import('@vueuse/core')['useElementSize']>
+    readonly useElementVisibility: UnwrapRef<typeof import('@vueuse/core')['useElementVisibility']>
+    readonly useEventBus: UnwrapRef<typeof import('@vueuse/core')['useEventBus']>
+    readonly useEventListener: UnwrapRef<typeof import('@vueuse/core')['useEventListener']>
+    readonly useEventSource: UnwrapRef<typeof import('@vueuse/core')['useEventSource']>
+    readonly useEyeDropper: UnwrapRef<typeof import('@vueuse/core')['useEyeDropper']>
+    readonly useFavicon: UnwrapRef<typeof import('@vueuse/core')['useFavicon']>
+    readonly useFetch: UnwrapRef<typeof import('@vueuse/core')['useFetch']>
+    readonly useFileDialog: UnwrapRef<typeof import('@vueuse/core')['useFileDialog']>
+    readonly useFileSystemAccess: UnwrapRef<typeof import('@vueuse/core')['useFileSystemAccess']>
+    readonly useFilters: UnwrapRef<typeof import('./composables/filters')['useFilters']>
+    readonly useFocus: UnwrapRef<typeof import('@vueuse/core')['useFocus']>
+    readonly useFocusWithin: UnwrapRef<typeof import('@vueuse/core')['useFocusWithin']>
+    readonly useFps: UnwrapRef<typeof import('@vueuse/core')['useFps']>
+    readonly useFullscreen: UnwrapRef<typeof import('@vueuse/core')['useFullscreen']>
+    readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']>
+    readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']>
+    readonly useGlobal: UnwrapRef<typeof import('./composables/index')['useGlobal']>
+    readonly useGlobalStore: UnwrapRef<typeof import('./stores/use-global-store')['default']>
+    readonly useHead: UnwrapRef<typeof import('@unhead/vue')['useHead']>
+    readonly useId: UnwrapRef<typeof import('vue')['useId']>
+    readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']>
+    readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']>
+    readonly useInfiniteScroll: UnwrapRef<typeof import('@vueuse/core')['useInfiniteScroll']>
+    readonly useIntersectionObserver: UnwrapRef<typeof import('@vueuse/core')['useIntersectionObserver']>
+    readonly useInterval: UnwrapRef<typeof import('@vueuse/core')['useInterval']>
+    readonly useIntervalFn: UnwrapRef<typeof import('@vueuse/core')['useIntervalFn']>
+    readonly useKeyModifier: UnwrapRef<typeof import('@vueuse/core')['useKeyModifier']>
+    readonly useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']>
+    readonly useLink: UnwrapRef<typeof import('vue-router')['useLink']>
+    readonly useLists: UnwrapRef<typeof import('./composables/index')['useLists']>
+    readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']>
+    readonly useLockFn: UnwrapRef<typeof import('./composables/index')['useLockFn']>
+    readonly useMagicKeys: UnwrapRef<typeof import('@vueuse/core')['useMagicKeys']>
+    readonly useManualRefHistory: UnwrapRef<typeof import('@vueuse/core')['useManualRefHistory']>
+    readonly useMediaControls: UnwrapRef<typeof import('@vueuse/core')['useMediaControls']>
+    readonly useMediaQuery: UnwrapRef<typeof import('@vueuse/core')['useMediaQuery']>
+    readonly useMemoize: UnwrapRef<typeof import('@vueuse/core')['useMemoize']>
+    readonly useMemory: UnwrapRef<typeof import('@vueuse/core')['useMemory']>
+    readonly useModel: UnwrapRef<typeof import('vue')['useModel']>
+    readonly useMounted: UnwrapRef<typeof import('@vueuse/core')['useMounted']>
+    readonly useMouse: UnwrapRef<typeof import('@vueuse/core')['useMouse']>
+    readonly useMouseInElement: UnwrapRef<typeof import('@vueuse/core')['useMouseInElement']>
+    readonly useMousePressed: UnwrapRef<typeof import('@vueuse/core')['useMousePressed']>
+    readonly useMutationObserver: UnwrapRef<typeof import('@vueuse/core')['useMutationObserver']>
+    readonly useNavigatorLanguage: UnwrapRef<typeof import('@vueuse/core')['useNavigatorLanguage']>
+    readonly useNetwork: UnwrapRef<typeof import('@vueuse/core')['useNetwork']>
+    readonly useNow: UnwrapRef<typeof import('@vueuse/core')['useNow']>
+    readonly useObjectUrl: UnwrapRef<typeof import('@vueuse/core')['useObjectUrl']>
+    readonly useOffsetPagination: UnwrapRef<typeof import('@vueuse/core')['useOffsetPagination']>
+    readonly useOnline: UnwrapRef<typeof import('@vueuse/core')['useOnline']>
+    readonly usePageLeave: UnwrapRef<typeof import('@vueuse/core')['usePageLeave']>
+    readonly useParallax: UnwrapRef<typeof import('@vueuse/core')['useParallax']>
+    readonly useParentElement: UnwrapRef<typeof import('@vueuse/core')['useParentElement']>
+    readonly usePerformanceObserver: UnwrapRef<typeof import('@vueuse/core')['usePerformanceObserver']>
+    readonly usePermission: UnwrapRef<typeof import('@vueuse/core')['usePermission']>
+    readonly usePointer: UnwrapRef<typeof import('@vueuse/core')['usePointer']>
+    readonly usePointerLock: UnwrapRef<typeof import('@vueuse/core')['usePointerLock']>
+    readonly usePointerSwipe: UnwrapRef<typeof import('@vueuse/core')['usePointerSwipe']>
+    readonly usePreferredColorScheme: UnwrapRef<typeof import('@vueuse/core')['usePreferredColorScheme']>
+    readonly usePreferredContrast: UnwrapRef<typeof import('@vueuse/core')['usePreferredContrast']>
+    readonly usePreferredDark: UnwrapRef<typeof import('@vueuse/core')['usePreferredDark']>
+    readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']>
+    readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
+    readonly usePreferredReducedTransparency: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedTransparency']>
+    readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']>
+    readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
+    readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
+    readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']>
+    readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']>
+    readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']>
+    readonly useSSRWidth: UnwrapRef<typeof import('@vueuse/core')['useSSRWidth']>
+    readonly useSaveScroll: UnwrapRef<typeof import('./composables/index')['useSaveScroll']>
+    readonly useScreenOrientation: UnwrapRef<typeof import('@vueuse/core')['useScreenOrientation']>
+    readonly useScreenSafeArea: UnwrapRef<typeof import('@vueuse/core')['useScreenSafeArea']>
+    readonly useScriptTag: UnwrapRef<typeof import('@vueuse/core')['useScriptTag']>
+    readonly useScroll: UnwrapRef<typeof import('@vueuse/core')['useScroll']>
+    readonly useScrollLock: UnwrapRef<typeof import('@vueuse/core')['useScrollLock']>
+    readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']>
+    readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
+    readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
+    readonly useSorted: UnwrapRef<typeof import('@vueuse/core')['useSorted']>
+    readonly useSpeechRecognition: UnwrapRef<typeof import('@vueuse/core')['useSpeechRecognition']>
+    readonly useSpeechSynthesis: UnwrapRef<typeof import('@vueuse/core')['useSpeechSynthesis']>
+    readonly useStepper: UnwrapRef<typeof import('@vueuse/core')['useStepper']>
+    readonly useStorage: UnwrapRef<typeof import('@vueuse/core')['useStorage']>
+    readonly useStorageAsync: UnwrapRef<typeof import('@vueuse/core')['useStorageAsync']>
+    readonly useStyleTag: UnwrapRef<typeof import('@vueuse/core')['useStyleTag']>
+    readonly useSupported: UnwrapRef<typeof import('@vueuse/core')['useSupported']>
+    readonly useSwipe: UnwrapRef<typeof import('@vueuse/core')['useSwipe']>
+    readonly useTabLists: UnwrapRef<typeof import('./composables/index')['useTabLists']>
+    readonly useTemplateRef: UnwrapRef<typeof import('vue')['useTemplateRef']>
+    readonly useTemplateRefsList: UnwrapRef<typeof import('@vueuse/core')['useTemplateRefsList']>
+    readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']>
+    readonly useTextSelection: UnwrapRef<typeof import('@vueuse/core')['useTextSelection']>
+    readonly useTextareaAutosize: UnwrapRef<typeof import('@vueuse/core')['useTextareaAutosize']>
+    readonly useThrottle: UnwrapRef<typeof import('@vueuse/core')['useThrottle']>
+    readonly useThrottleFn: UnwrapRef<typeof import('@vueuse/core')['useThrottleFn']>
+    readonly useThrottledRefHistory: UnwrapRef<typeof import('@vueuse/core')['useThrottledRefHistory']>
+    readonly useTimeAgo: UnwrapRef<typeof import('@vueuse/core')['useTimeAgo']>
+    readonly useTimeout: UnwrapRef<typeof import('@vueuse/core')['useTimeout']>
+    readonly useTimeoutFn: UnwrapRef<typeof import('@vueuse/core')['useTimeoutFn']>
+    readonly useTimeoutPoll: UnwrapRef<typeof import('@vueuse/core')['useTimeoutPoll']>
+    readonly useTimestamp: UnwrapRef<typeof import('@vueuse/core')['useTimestamp']>
+    readonly useTitle: UnwrapRef<typeof import('@vueuse/core')['useTitle']>
+    readonly useToNumber: UnwrapRef<typeof import('@vueuse/core')['useToNumber']>
+    readonly useToString: UnwrapRef<typeof import('@vueuse/core')['useToString']>
+    readonly useToggle: UnwrapRef<typeof import('@vueuse/core')['useToggle']>
+    readonly useTransition: UnwrapRef<typeof import('@vueuse/core')['useTransition']>
+    readonly useUrlSearchParams: UnwrapRef<typeof import('@vueuse/core')['useUrlSearchParams']>
+    readonly useUserMedia: UnwrapRef<typeof import('@vueuse/core')['useUserMedia']>
+    readonly useUserStore: UnwrapRef<typeof import('./stores/use-user-store')['default']>
+    readonly useVModel: UnwrapRef<typeof import('@vueuse/core')['useVModel']>
+    readonly useVModels: UnwrapRef<typeof import('@vueuse/core')['useVModels']>
+    readonly useVibrate: UnwrapRef<typeof import('@vueuse/core')['useVibrate']>
+    readonly useVirtualList: UnwrapRef<typeof import('@vueuse/core')['useVirtualList']>
+    readonly useWakeLock: UnwrapRef<typeof import('@vueuse/core')['useWakeLock']>
+    readonly useWebNotification: UnwrapRef<typeof import('@vueuse/core')['useWebNotification']>
+    readonly useWebSocket: UnwrapRef<typeof import('@vueuse/core')['useWebSocket']>
+    readonly useWebWorker: UnwrapRef<typeof import('@vueuse/core')['useWebWorker']>
+    readonly useWebWorkerFn: UnwrapRef<typeof import('@vueuse/core')['useWebWorkerFn']>
+    readonly useWindowFocus: UnwrapRef<typeof import('@vueuse/core')['useWindowFocus']>
+    readonly useWindowScroll: UnwrapRef<typeof import('@vueuse/core')['useWindowScroll']>
+    readonly useWindowSize: UnwrapRef<typeof import('@vueuse/core')['useWindowSize']>
+    readonly userToken: UnwrapRef<typeof import('./composables/config')['userToken']>
+    readonly watch: UnwrapRef<typeof import('vue')['watch']>
+    readonly watchArray: UnwrapRef<typeof import('@vueuse/core')['watchArray']>
+    readonly watchAtMost: UnwrapRef<typeof import('@vueuse/core')['watchAtMost']>
+    readonly watchDebounced: UnwrapRef<typeof import('@vueuse/core')['watchDebounced']>
+    readonly watchDeep: UnwrapRef<typeof import('@vueuse/core')['watchDeep']>
+    readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
+    readonly watchIgnorable: UnwrapRef<typeof import('@vueuse/core')['watchIgnorable']>
+    readonly watchImmediate: UnwrapRef<typeof import('@vueuse/core')['watchImmediate']>
+    readonly watchOnce: UnwrapRef<typeof import('@vueuse/core')['watchOnce']>
+    readonly watchPausable: UnwrapRef<typeof import('@vueuse/core')['watchPausable']>
+    readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>
+    readonly watchSyncEffect: UnwrapRef<typeof import('vue')['watchSyncEffect']>
+    readonly watchThrottled: UnwrapRef<typeof import('@vueuse/core')['watchThrottled']>
+    readonly watchTriggerable: UnwrapRef<typeof import('@vueuse/core')['watchTriggerable']>
+    readonly watchWithFilter: UnwrapRef<typeof import('@vueuse/core')['watchWithFilter']>
+    readonly whenever: UnwrapRef<typeof import('@vueuse/core')['whenever']>
+  }
+}

+ 43 - 0
src/components.d.ts

@@ -0,0 +1,43 @@
+/* eslint-disable */
+// @ts-nocheck
+// Generated by unplugin-vue-components
+// Read more: https://github.com/vuejs/core/pull/3399
+// biome-ignore lint: disable
+export {}
+
+/* prettier-ignore */
+declare module 'vue' {
+  export interface GlobalComponents {
+    CurrencySelect: typeof import('./components/CurrencySelect.vue')['default']
+    CustomTabbar: typeof import('./components/CustomTabbar.vue')['default']
+    EmptyComponents: typeof import('./components/empty-components.vue')['default']
+    ImgList: typeof import('./components/img-list.vue')['default']
+    JsxComponents: typeof import('./components/jsx-components.tsx')['default']
+    PageHeader: typeof import('./components/PageHeader.vue')['default']
+    RouterLink: typeof import('vue-router')['RouterLink']
+    RouterView: typeof import('vue-router')['RouterView']
+    ULoadmore: typeof import('./components/u-loadmore.vue')['default']
+    VanButton: typeof import('vant/es')['Button']
+    VanCell: typeof import('vant/es')['Cell']
+    VanCellGroup: typeof import('vant/es')['CellGroup']
+    VanCheckbox: typeof import('vant/es')['Checkbox']
+    VanDatePicker: typeof import('vant/es')['DatePicker']
+    VanDialog: typeof import('vant/es')['Dialog']
+    VanField: typeof import('vant/es')['Field']
+    VanGrid: typeof import('vant/es')['Grid']
+    VanGridItem: typeof import('vant/es')['GridItem']
+    VanIcon: typeof import('vant/es')['Icon']
+    VanImage: typeof import('vant/es')['Image']
+    VanImagePreview: typeof import('vant/es')['ImagePreview']
+    VanList: typeof import('vant/es')['List']
+    VanLoading: typeof import('vant/es')['Loading']
+    VanNavBar: typeof import('vant/es')['NavBar']
+    VanPopup: typeof import('vant/es')['Popup']
+    VanPullRefresh: typeof import('vant/es')['PullRefresh']
+    VanSkeleton: typeof import('vant/es')['Skeleton']
+    VanTab: typeof import('vant/es')['Tab']
+    VanTabbar: typeof import('vant/es')['Tabbar']
+    VanTabbarItem: typeof import('vant/es')['TabbarItem']
+    VanTabs: typeof import('vant/es')['Tabs']
+  }
+}

+ 82 - 0
src/components/CurrencySelect.vue

@@ -0,0 +1,82 @@
+<template>
+  <van-popup v-model:show="show" round position="bottom" :style="{ background: 'transparent', boxShadow: 'none' }">
+    <div class="currency-mask">
+      <div class="currency-select">
+        <div
+          v-for="item in options"
+          :key="item.value"
+          class="currency-item"
+          @click="select(item)"
+        >
+          {{ item.label }}
+        </div>
+        <div class="cancel-btn" @click="close">取消</div>
+      </div>
+    </div>
+  </van-popup>
+</template>
+
+<script setup lang="ts">
+import { ref, watch } from 'vue'
+
+const props = defineProps<{
+  modelValue: boolean
+  options: Array<{ label: string; value: string }>
+}>()
+const emit = defineEmits(['update:modelValue', 'select'])
+
+const show = ref(props.modelValue)
+watch(() => props.modelValue, val => (show.value = val))
+watch(show, val => emit('update:modelValue', val))
+
+function select(item: { label: string; value: string }) {
+  emit('select', item)
+  show.value = false
+}
+function close() {
+  show.value = false
+}
+</script>
+
+<style scoped>
+.currency-mask {
+  position: fixed;
+  inset: 0;
+  background: rgba(0, 0, 0, 0.45);
+  display: flex;
+  align-items: flex-end;
+  justify-content: center;
+  z-index: 1;
+}
+.currency-select {
+  width: 100vw;
+  padding: 0 0 22px 0;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  z-index: 2;
+}
+.currency-item {
+  width: 90vw;
+  padding: 20px 0;
+  text-align: center;
+  font-size: 22px;
+  background: #eaff00;
+  color: #111;
+  margin-bottom: 16px;
+  border-radius: 20px;
+  font-weight: 500;
+  box-shadow: 0 2px 8px rgba(0,0,0,0.08);
+}
+.cancel-btn {
+  width: 90vw;
+  padding: 20px 0;
+  text-align: center;
+  color: #111;
+  background: #eaff00;
+  border-radius: 20px;
+  font-weight: bold;
+  font-size: 22px;
+  box-shadow: 0 2px 8px rgba(0,0,0,0.08);
+}
+</style>

+ 103 - 0
src/components/CustomTabbar.vue

@@ -0,0 +1,103 @@
+<template>
+  <nav class="custom-tabbar">
+    <div
+      v-for="item in tabs"
+      :key="item.path"
+      :class="['tabbar-item', { active: route.path === item.path }]"
+      @click="onTabClick(item.path)"
+    >
+      <span class="tabbar-icon">
+        <van-icon :name="route.path === item.path ? item.iconActive : item.icon" />
+      </span>
+      <span class="tabbar-label">{{ item.label }}</span>
+    </div>
+  </nav>
+</template>
+
+<script setup lang="ts">
+import { useI18n } from 'vue-i18n'
+import { useRouter, useRoute } from 'vue-router'
+import { Icon as VanIcon } from 'vant'
+
+const { t } = useI18n()
+const router = useRouter()
+const route = useRoute()
+
+const tabs = [
+  {
+    label: t('tabs.wallet'),
+    path: '/',
+    icon: 'balance-o',
+    iconActive: 'balance-o',
+  },
+  {
+    label: t('tabs.cards'),
+    path: '/cards',
+    icon: 'credit-pay',
+    iconActive: 'credit-pay',
+  },
+//   {
+//     label: t('tabs.finance'),
+//     path: '/finance',
+//     icon: 'gold-coin-o',
+//     iconActive: 'gold-coin',
+//   },
+  {
+    label: t('tabs.mine'),
+    path: '/mine',
+    icon: 'user-o',
+    iconActive: 'user',
+  },
+]
+
+function onTabClick(path: string) {
+  if (route.path !== path) {
+    router.replace(path)
+  }
+}
+</script>
+
+<style scoped>
+.custom-tabbar {
+  position: fixed;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  height: 60px;
+  background: #181a1b;
+  border-radius: 18px 18px 0 0;
+  display: flex;
+  justify-content: space-around;
+  align-items: center;
+  box-shadow: 0 -2px 16px rgba(0,0,0,0.12);
+  z-index: 100;
+  margin: 0 8px 8px 8px;
+}
+.tabbar-item {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  color: var(--white);
+  font-size: var(--font-size-12);
+  border-radius: 12px;
+  padding: 8px 0;
+  cursor: pointer;
+}
+.tabbar-item.active {
+  color: var(--main-yellow);
+}
+.tabbar-icon {
+  margin-bottom: 4px;
+  font-size: var(--font-size-24);
+}
+.tabbar-icon-active {
+  color: var(--main-yellow);
+}
+.tabbar-label {
+  font-weight: 500;
+  line-height: 1.2;
+  letter-spacing: 1px;
+}
+</style>

+ 47 - 0
src/components/PageHeader.vue

@@ -0,0 +1,47 @@
+<template>
+  <div class="page-header">
+    <div class="header">
+      <van-icon v-if="showBack" name="arrow-left" class="back-icon" @click="handleBack" />
+      <span>{{ title }}</span>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { useRouter } from 'vue-router'
+import { Icon as VanIcon } from 'vant'
+
+const router = useRouter()
+
+defineProps<{
+  title: string
+  showBack?: boolean
+}>()
+
+const handleBack = () => {
+  router.back()
+}
+</script>
+
+<style scoped lang="scss">
+.page-header {
+  .header {
+    position: relative;
+    text-align: center;
+    font-size: var(--font-size-18);
+    color: var(--white);
+    font-weight: bold;
+    margin: 0 0 20px 0;
+    padding-top: 20px;
+
+    .back-icon {
+      position: absolute;
+      left: 16px;
+      top: 50%;
+      transform: translateY(-50%, -50%);
+      font-size: var(--font-size-20);
+      cursor: pointer;
+    }
+  }
+}
+</style>

+ 17 - 0
src/components/empty-components.vue

@@ -0,0 +1,17 @@
+<template>
+    <div>{{ title }}</div>
+</template>
+
+<script lang="ts" setup>
+import { withDefaults } from 'unplugin-vue-macros/macros' assert { type: 'macro' }
+
+defineOptions({
+    name: 'EmptyComponents',
+})
+
+const { title } = withDefaults(definePropsRefs<{
+    title?: string
+}>(), {
+    title: 'title',
+})
+</script>

+ 25 - 0
src/components/jsx-components.tsx

@@ -0,0 +1,25 @@
+import { withModifiers } from 'vue'
+
+const App = defineComponent({
+    name: 'JsxComponents',
+    setup() {
+        let count = $ref(0)
+
+        const inc = () => {
+            count += 1
+        }
+
+        return () => (
+            <div p-20px>
+                <van-button onClick={withModifiers(inc, ['self'])}>
+                    &lt;&lt;&lt;&lt;
+                    {count}
+                    {' '}
+                    &lt;&lt;&lt;&lt;
+                </van-button>
+            </div>
+        )
+    },
+})
+
+export default App

+ 222 - 0
src/components/u-loadmore.vue

@@ -0,0 +1,222 @@
+<template>
+    <div
+        class="u-load-more-wrap"
+        :style="{
+            backgroundColor: bgColor,
+            marginBottom: `${marginBottom}px`,
+            marginTop: `${marginTop}px`,
+            height,
+        }"
+    >
+        <!-- 加载中和没有更多的状态才显示两边的横线 -->
+        <div :class="status === 'loadmore' || status === 'nomore' ? 'u-more' : ''" class="u-load-more-inner">
+            <div class="u-loadmore-icon-wrap">
+                <van-loading v-show="status === 'loading' && icon" :color="iconColor" :type="iconType" size="18px" />
+            </div>
+            <!-- 如果没有更多的状态下,显示内容为dot(粗点),加载特定样式 -->
+            <div
+                class="u-line-1"
+                :style="[loadTextStyle]"
+                :class="[status === 'nomore' && isDot === true ? 'u-dot-text' : 'u-more-text']"
+                @click="loadMore"
+            >
+                {{ showText }}
+            </div>
+        </div>
+    </div>
+</template>
+
+<script>
+/**
+ * loadmore 加载更多
+ * @description 此组件一般用于标识页面底部加载数据时的状态。
+ * @see https://www.uviewui.com/components/loadMore.html
+ * @property {string} status 组件状态(默认loadmore)
+ * @property {string} bg-color 组件背景颜色,在页面是非白色时会用到(默认#ffffff)
+ * @property {boolean} icon 加载中时是否显示图标(默认true)
+ * @property {string} icon-type 加载中时的图标类型(默认circle)
+ * @property {string} icon-color icon-type为circle时有效,加载中的动画图标的颜色(默认#b7b7b7)
+ * @property {boolean} is-dot status为nomore时,内容显示为一个"●"(默认false)
+ * @property {string} color 字体颜色(默认#606266)
+ * @property {string|number} margin-top 到上一个相邻元素的距离
+ * @property {string|number} margin-bottom 到下一个相邻元素的距离
+ * @property {object} load-text 自定义显示的文字,见上方说明示例
+ * @event {Function} loadmore status为loadmore时,点击组件会发出此事件
+ * @example <u-loadmore :status="status" icon-type="iconType" load-text="loadText" />
+ */
+export default {
+    name: 'ULoadmore',
+    props: {
+        // 组件背景色
+        bgColor: {
+            type: String,
+            default: 'transparent',
+        },
+        // 是否显示加载中的图标
+        icon: {
+            type: Boolean,
+            default: true,
+        },
+        // 字体大小
+        fontSize: {
+            type: String,
+            default: '14',
+        },
+        // 字体颜色
+        color: {
+            type: String,
+            default: '#606266',
+        },
+        // 组件状态,loadmore-加载前的状态,loading-加载中的状态,nomore-没有更多的状态
+        status: {
+            type: String,
+            default: 'loadmore',
+        },
+        // 加载中状态的图标,spinner-花朵状图标,circular-圆圈状图标
+        iconType: {
+            type: String,
+            default: 'circular',
+        },
+        // 显示的文字
+        loadText: {
+            type: Object,
+            default() {
+                return {
+                    loadmore: '加载更多',
+                    loading: '正在加载...',
+                    nomore: '没有更多了',
+                }
+            },
+        },
+        // 在“没有更多”状态下,是否显示粗点
+        isDot: {
+            type: Boolean,
+            default: false,
+        },
+        // 加载中显示圆圈动画时,动画的颜色
+        iconColor: {
+            type: String,
+            default: '#b7b7b7',
+        },
+        // 上边距
+        marginTop: {
+            type: [String, Number],
+            default: 0,
+        },
+        // 下边距
+        marginBottom: {
+            type: [String, Number],
+            default: 0,
+        },
+        // 高度,单位rpx
+        height: {
+            type: [String, Number],
+            default: 'auto',
+        },
+    },
+    emits: ['loadmore'],
+    data() {
+        return {
+            // 粗点
+            dotText: '●',
+        }
+    },
+    computed: {
+        // 加载的文字显示的样式
+        loadTextStyle() {
+            return {
+                color: this.color,
+                fontSize: `${this.fontSize}px`,
+                position: 'relative',
+                zIndex: 1,
+                backgroundColor: this.bgColor,
+                // 如果是加载中状态,动画和文字需要距离近一点
+            }
+        },
+        // 加载中圆圈动画的样式
+        cricleStyle() {
+            return {
+                borderColor: `#e5e5e5 #e5e5e5 #e5e5e5 ${this.circleColor}`,
+            }
+        },
+        // 加载中花朵动画形式
+        // 动画由base64图片生成,暂不支持修改
+        flowerStyle() {
+            return {}
+        },
+        // 显示的提示文字
+        showText() {
+            let text = ''
+            if (this.status === 'loadmore') {
+                text = this.loadText.loadmore
+            }
+            else if (this.status === 'loading') {
+                text = this.loadText.loading
+            }
+            else if (this.status === 'nomore' && this.isDot) {
+                text = this.dotText
+            }
+            else {
+                text = this.loadText.nomore
+            }
+            return text
+        },
+    },
+    methods: {
+        loadMore() {
+            // 只有在“加载更多”的状态下才发送点击事件,内容不满一屏时无法触发底部上拉事件,所以需要点击来触发
+            if (this.status === 'loadmore') {
+                this.$emit('loadmore')
+            }
+        },
+    },
+}
+</script>
+
+<style scoped lang="scss">
+@mixin vue-flex($direction: row) {
+    display: flex;
+    flex-direction: $direction;
+}
+
+/* #ifdef MP */
+// 在mp.scss中,赋予了u-line为flex: 1,这里需要一个明确的长度,所以重置掉它
+// 在组件内部,把组件名(u-line)当做选择器,在微信开发工具会提示不合法,但不影响使用
+u-line {
+    flex: none;
+}
+/* #endif */
+
+.u-load-more-wrap {
+    @include vue-flex;
+    justify-content: center;
+    align-items: center;
+}
+
+.u-load-more-inner {
+    @include vue-flex;
+    justify-content: center;
+    align-items: center;
+    padding: 0 12px;
+}
+
+.u-more {
+    position: relative;
+    @include vue-flex;
+    justify-content: center;
+}
+
+.u-dot-text {
+    font-size: 28px;
+}
+
+.u-loadmore-icon-wrap {
+    margin-right: 8px;
+}
+
+.u-loadmore-icon {
+    @include vue-flex;
+    align-items: center;
+    justify-content: center;
+}
+</style>

+ 7 - 0
src/composables/config.ts

@@ -0,0 +1,7 @@
+const config = {}
+
+export default config
+
+export const userToken = useStorage('user-token', '')
+export const lang = useStorage('lang', '')
+export const CLIENT = useStorage('CLIENT', '')

+ 19 - 0
src/composables/crypt.ts

@@ -0,0 +1,19 @@
+import CryptoJS from 'crypto-js'
+class CryptToJS {
+    private crypt: typeof CryptoJS;
+    private secret: string;
+    constructor() {
+        this.crypt = CryptoJS;
+        this.secret = 'Believe in yourself.';
+    }
+    public encrypt(text: string): string {
+        return this.crypt.AES.encrypt(text, this.secret).toString();
+    }
+    public decrypt(text: string): string {
+        if (text == null || text.length === 0) {
+            return "";
+        }
+        return this.crypt.AES.decrypt(text, this.secret).toString(CryptoJS.enc.Utf8);
+    }
+}
+export default new CryptToJS();

+ 222 - 0
src/composables/fetch.ts

@@ -0,0 +1,222 @@
+import type { AxiosHeaders, AxiosRequestConfig, AxiosResponse } from 'axios'
+import axios from 'axios'
+import { showToast } from 'vant'
+import { userToken, lang, CLIENT } from './config'
+import config from '@/config'
+const { Host85 } = config
+
+window.axios = axios
+
+interface ResponseData<T = any> {
+    code: number
+    data: T
+    msg: string
+}
+
+interface Objable {
+    [key: string]: any
+}
+
+interface ApiType {
+    post: <T = any>(url: string, data?: Objable, header?: Objable, checkCode?: boolean) => Promise<ResponseData<T>>
+    get: <T = any>(url: string, data?: Objable, header?: Objable, checkCode?: boolean) => Promise<ResponseData<T>>
+    put: <T = any>(url: string, data?: Objable, header?: Objable, checkCode?: boolean) => Promise<ResponseData<T>>
+    delete: <T = any>(url: string, data?: Objable, header?: Objable, checkCode?: boolean) => Promise<ResponseData<T>>
+    downFile: (url: string, method?: string, data?: Objable) => Promise<AxiosResponse>
+    uploadFile: <T = any>(url: string, file: File, data?: Objable, header?: Objable, checkCode?: boolean) => Promise<ResponseData<T>>
+    RESTful: <T = any>(url: string, method?: string, data?: Objable, header?: Objable, checkCode?: boolean) => Promise<ResponseData<T>>
+    $RESTful: <T = any>(url: string, method?: string, data?: Objable, header?: Objable) => Promise<ResponseData<T>>
+}
+
+const headers = {
+    'X-Requested-With': 'XMLHttpRequest',
+    'Content-Type': 'application/json',
+}
+
+const baseConfig = {
+    headers,
+    timeout: 5000,
+    withCredentials: false,
+}
+
+if (import.meta.env.VITE_APP_ENV === 'production') {
+    baseConfig.timeout = 300000
+}
+
+axios.interceptors.request.use(
+    config => config,
+    error => Promise.resolve(error.response || error),
+)
+
+axios.interceptors.response.use(
+    response => response,
+    (error) => {
+        const response = {} as AxiosResponse
+        response.config = error.config
+        response.data = null
+        response.headers = error.config.headers
+        response.status = error.code
+        response.statusText = error.msg
+        response.request = error.request
+        return Promise.resolve(response)
+    },
+)
+
+function checkStatus(response: AxiosResponse): ResponseData<any> {
+    if (response.status === 200 || response.status === 304) {
+        return response.data
+    }
+    if (response.status === 401) {
+        return {
+            code: 401,
+            data: response.statusText || response.toString(),
+            msg: '您还没有登录, 或者登录超时!',
+        }
+    }
+    return {
+        code: -404,
+        data: response.statusText || response.toString(),
+        msg: `接口返回数据错误, 错误代码: ${response.status || '未知'}`,
+    }
+}
+
+function checkCodeFn(data: ResponseData<any>) {
+    const code = [0, 200, 1000]
+    if (data.code === 401 || data.code === 600) {
+        userToken.value = ''
+        const pathname = encodeURIComponent(window.location.pathname)
+        if (!window.$$lock) {
+            window.$$lock = true
+            loginMsgBox('当前登录状态已失效, 请重新登录', pathname)
+        }
+    }
+    else if (!code.includes(Number(data.code))) {
+        showToast(data.msg || '请求失败')
+    }
+    else {
+        data.code = 200
+    }
+    return data
+}
+
+const api: ApiType = {
+    post<T = any>(url: string, data?: Objable, header?: Objable, checkCode = true) {
+        return this.RESTful<T>(url, 'post', data, header, checkCode)
+    },
+    get<T = any>(url: string, data?: Objable, header?: Objable, checkCode = true) {
+        return this.RESTful<T>(url, 'get', data, header, checkCode)
+    },
+    put<T = any>(url: string, data?: Objable, header?: Objable, checkCode = true) {
+        return this.RESTful<T>(url, 'put', data, header, checkCode)
+    },
+    delete<T = any>(url: string, data?: Objable, header?: Objable, checkCode = true) {
+        return this.RESTful<T>(url, 'delete', data, header, checkCode)
+    },
+    async downFile(url: string, method = 'get', data?: Objable) {
+        const config: AxiosRequestConfig = {
+            ...baseConfig,
+            responseType: 'arraybuffer',
+            method,
+            url: Host85 + url,
+        }
+        if (method === 'get') {
+            config.params = data
+        }
+        else {
+            config.data = data
+        }
+
+        if (url.includes('NoTimeout')) {
+            config.timeout = 9999999
+        }
+        const response = await axios(config)
+        return response
+    },
+    async uploadFile<T = any>(url: string, file: File, data?: Objable, header?: Objable, checkCode = true) {
+        const formData = new FormData()
+        formData.append('file', file)
+
+        if (data) {
+            Object.keys(data).forEach(key => {
+                formData.append(key, data[key])
+            })
+        }
+
+        const config: AxiosRequestConfig = {
+            ...baseConfig,
+            headers: {
+                ...baseConfig.headers,
+                'Content-Type': 'multipart/form-data',
+                ...header,
+            },
+            method: 'post',
+            url: Host85 + url,
+            data: formData,
+        }
+
+        if (userToken.value) {
+            (config.headers as AxiosHeaders)['Access-Token'] = `${userToken.value}`
+        }
+        if (lang.value) {
+            (config.headers as AxiosHeaders)['Language'] = `${lang.value}`
+        }
+        if (CLIENT.value) {
+            (config.headers as AxiosHeaders)['CLIENT'] = `${CLIENT.value}`
+        }
+
+        if (url.includes('NoTimeout')) {
+            config.timeout = 9999999
+        }
+
+        const response = await axios(config)
+        const result = checkStatus(response)
+        if (checkCode) {
+            return checkCodeFn(result)
+        }
+        return result
+    },
+    async RESTful<T = any>(url: string, method = 'get', data?: Objable, header?: Objable, checkCode = true) {
+        const xhr = await this.$RESTful<T>(url, method, data, header)
+        if (checkCode) {
+            return checkCodeFn(xhr)
+        }
+        return xhr
+    },
+    async $RESTful<T = any>(url: string, method = 'get', data?: Objable, header?: Objable) {
+        url = Host85 + url
+        const config: AxiosRequestConfig = {
+            ...baseConfig,
+            headers: {
+                ...baseConfig.headers,
+                ...header,
+            },
+            method,
+            url,
+        }
+        if (userToken.value) {
+            (config.headers as AxiosHeaders)['Access-Token'] = `${userToken.value}`
+        }
+        if (lang.value) {
+            (config.headers as AxiosHeaders)['Language'] = `${lang.value}`
+        }
+        if (CLIENT.value) {
+            (config.headers as AxiosHeaders)['CLIENT'] = `${CLIENT.value}`
+        }
+
+        if (method === 'get') {
+            config.params = data
+        }
+        else {
+            config.data = data
+        }
+
+        if (url.includes('NoTimeout')) {
+            config.timeout = 9999999
+        }
+        const response = await axios(config)
+        return checkStatus(response)
+    },
+}
+
+window.$$api = api
+export const $api = api

+ 43 - 0
src/composables/filters.ts

@@ -0,0 +1,43 @@
+function formatTime(value: any, format: string) {
+    format = format || 'yyyy-mm-dd'
+    return UTC2Date(value, format)
+}
+
+function dateTime(value: string) {
+    if (!value || typeof value !== 'string') {
+        return ''
+    }
+    const arr = value.split(':')
+    return `${arr[0]}:${arr[1]}`
+}
+
+function arrToStr(value: string | any[]) {
+    try {
+        if (typeof value === 'string') {
+            value = JSON.parse(value)
+        }
+        if (Array.isArray(value)) {
+            return value.join(', ')
+        }
+
+        return value
+    }
+    catch (error: unknown) {
+        const err = error as Error
+        console.log(err.message)
+        return ''
+    }
+}
+
+function tofixed(value: string | number) {
+    return Number(value).toFixed(2)
+}
+
+export function useFilters() {
+    return {
+        formatTime,
+        dateTime,
+        arrToStr,
+        tofixed,
+    }
+}

+ 304 - 0
src/composables/index.ts

@@ -0,0 +1,304 @@
+import type { TopicList, UserListConfig, UserListsInit, UseTabList, UseTabListsInit } from '@/types'
+import ls from 'store2'
+
+export function useGlobal() {
+    const ins = getCurrentInstance()!
+
+    const ctx = ins.appContext.config.globalProperties
+    const options = ins.type
+    const route = useRoute()
+    const router = useRouter()
+    const globalStore = useGlobalStore()
+
+    return {
+        ctx,
+        options,
+        route,
+        router,
+        globalStore,
+    }
+}
+
+/**
+ * 竞态锁
+ * @param fn 回调函数
+ * @param autoUnlock 是否自动解锁
+ * @description
+ * ```
+ * autoUnlock === true 不管 fn 返回什么, 都自动解锁
+ * autoUnlock === false 不管 fn 返回什么, 都不自动解锁
+ * autoUnlock === 'auto' 当 fn 返回 false 时, 不自动解锁, 返回其他值时, 自动解锁
+ * ```
+ * @example
+ * ```
+ * const Fn = useLockFn(async (key) => {
+ *  console.log(key)
+ * }
+ *
+ * <div v-on:click="Fn(123)"></div>
+ * ```
+ */
+export function useLockFn(fn: AnyFn, autoUnlock: boolean | 'auto' = 'auto') {
+    const lock = ref(false)
+    return async (...args: any[]) => {
+        if (lock.value) {
+            return
+        }
+        lock.value = true
+        try {
+            const $return: any = await fn(...args)
+            if (autoUnlock === true || (autoUnlock === 'auto' && $return !== false)) {
+                lock.value = false
+            }
+        }
+        catch (e) {
+            lock.value = false
+            throw e
+        }
+    }
+}
+
+export function useSaveScroll() {
+    const ins = getCurrentInstance()
+    const route = useRoute()
+    let name: string | undefined = ''
+    if (ins) {
+        name = ins.type.name
+    }
+
+    onActivated(() => {
+        if (name) {
+            const body = document.querySelector(`.${name}`)
+            if (body) {
+                const scrollTop = ls.get(route.fullPath) || 0
+                body.scrollTo(0, scrollTop)
+                ls.remove(route.fullPath)
+            }
+        }
+    })
+
+    onBeforeRouteLeave((_to, from, next) => {
+        const body = document.querySelector('.body')
+        if (body) {
+            ls.set(from.fullPath, body.scrollTop || 0)
+        }
+
+        next()
+    })
+}
+
+/**
+ * 单列表封装
+ * @param init { api: 接口封装 }
+ */
+export function useLists<T>(init: UserListsInit) {
+    const globalStore = useGlobalStore()
+
+    const body = $ref<HTMLElement>()!
+    const res: UserListConfig<T> = reactive({
+        ...init,
+        timer: null,
+        isLoaded: false,
+        // 列表数据 ==>
+        page: 1,
+        dataList: [],
+        // <==列表数据
+        config: {
+            // 下拉刷新 ==>
+            isLoading: false,
+            isRefresh: false,
+            // <==下拉刷新
+            // 滚动加载 ==>
+            loadStatus: 'loadmore',
+            isLock: false,
+            loading: false,
+            error: false,
+            finished: false,
+            // <==滚动加载
+        },
+    })
+
+    /**
+     * 请求列表接口
+     */
+    const getList = async () => {
+        if (res.config.isLock) {
+            return
+        }
+        res.config.isLock = true
+        // 异步更新数据
+        res.timer = setTimeout(() => {
+            globalStore.$patch({ routerLoading: true })
+        }, 500)
+        // 第一页时不显示loading
+        if (res.page > 1) {
+            res.config.loading = true
+        }
+        const { data, code } = await $api[init.api.method]<ResDataLists<T>>(init.api.url, { ...init.api.config, page: res.page })
+        // 500毫秒内已经加载完成数据, 则清除定时器, 不再显示路由loading
+        if (res.timer) {
+            clearTimeout(res.timer)
+        }
+
+        globalStore.$patch({ routerLoading: false })
+        res.isLoaded = true
+
+        if (code === 200) {
+            // 如果是下拉刷新 或者是第1页, 则只保留当前数据
+            if (res.config.isRefresh || res.page === 1) {
+                res.dataList = [...data.list]
+                res.config.isRefresh = false
+            }
+            else {
+                res.dataList = res.dataList.concat(data.list)
+            }
+            await nextTick()
+            // 加载状态结束
+            res.config.loading = false
+            // 数据全部加载完成
+            if (!data.hasNext) {
+                res.config.finished = true
+                res.config.loadStatus = 'nomore'
+            }
+            else {
+                res.config.loadStatus = 'loadmore'
+                res.page += 1
+            }
+            res.config.isLock = false
+        }
+        else {
+            res.config.error = true
+        }
+    }
+
+    /**
+     * 刷新接口
+     */
+    const onRefresh = async () => {
+        res.config.isRefresh = true
+        res.page = 1
+        await getList()
+        res.config.isLoading = false
+    }
+
+    /**
+     * 触底回调
+     */
+    const reachBottom = () => {
+        if (res.config.loadStatus === 'nomore' || res.config.loadStatus === 'loading') {
+            return
+        }
+        res.config.loadStatus = 'loading'
+        getList()
+    }
+    const lazyLoading = () => {
+        // 滚动到底部,再加载的处理事件
+        const scrollTop = body.scrollTop
+        const clientHeight = body.clientHeight
+        const scrollHeight = body.scrollHeight
+        if (scrollTop + clientHeight >= scrollHeight - 300) {
+            reachBottom()
+        }
+    }
+
+    return {
+        ...toRefs(res),
+        body,
+        getList,
+        onRefresh,
+        reachBottom,
+        lazyLoading,
+    }
+}
+
+/**
+ * Tab接口列表
+ * @param init { api: 接口封装 }
+ */
+export function useTabLists<T>(init: UseTabListsInit) {
+    const { options, globalStore } = useGlobal()
+
+    const body = $ref<HTMLElement>()!
+    const res: UseTabList<T> = reactive({
+        ...init,
+        timer: null,
+        // 列表数据 ==>
+        list: Array.from({ length: 5 }, () => '').map(() => ({
+            page: 1,
+            items: [],
+            refreshing: false,
+            loading: false,
+            error: false,
+            finished: false,
+        })),
+        // <==列表数据
+    })
+
+    const activeIndex = ref(0)
+
+    const getList = async (index: number) => {
+        const list: TopicList<T> = JSON.parse(JSON.stringify(res.list[index]))
+        if (list.page === 1) {
+            const body = document.querySelector(`.${options.name}`)
+            if (body)
+                body.scrollTo(0, 0)
+        }
+        // 500毫秒数据还没请求完成, 显示路由loading
+        res.timer = setTimeout(() => {
+            globalStore.$patch({ routerLoading: true })
+        }, 500)
+        // 第一页直接用路由loading
+        if (list.page === 1) {
+            list.loading = false
+        }
+
+        // 异步更新数据
+        const { method, url, config } = res.api[index]
+        const { code, data } = await $api[method as Methods]<ResDataLists<T>>(url, { ...config, page: list.page })
+        // 500毫秒内已经加载完成数据, 则清除定时器, 不再显示路由loading
+        if (res.timer) {
+            clearTimeout(res.timer)
+        }
+        globalStore.$patch({ routerLoading: false })
+        if (code === 200) {
+            // 如果是下拉刷新, 则只保留当前数据
+            if (list.refreshing) {
+                list.items = [...data.list]
+                list.refreshing = false
+            }
+            else {
+                list.items = list.items.concat(data.list)
+            }
+            await nextTick()
+            // 加载状态结束
+            list.loading = false
+            // 数据全部加载完成
+            if (!data.hasNext) {
+                list.finished = true
+            }
+            else {
+                list.page += 1
+            }
+        }
+        else {
+            list.error = true
+        }
+        res.list.splice(index, 1, list)
+    }
+
+    const onRefresh = async (index: number) => {
+        res.list[index].refreshing = true
+        res.list[index].page = 1
+        await getList(index)
+        res.list[index].refreshing = false
+        showMsg('刷新成功')
+    }
+
+    return {
+        res,
+        body,
+        getList,
+        onRefresh,
+        activeIndex,
+    }
+}

+ 44 - 0
src/composables/message.ts

@@ -0,0 +1,44 @@
+import { showFailToast, showSuccessToast, showToast } from 'vant'
+
+type MessageType = 'success' | 'info' | 'error'
+type ConfigType = string | { content: string, type: MessageType }
+
+const types = {
+    info: showToast,
+    success: showSuccessToast,
+    error: showFailToast,
+}
+
+/**
+ * 显式提示信息
+ * @example
+ * ```
+ * showMsg('content')
+ * showMsg({ content: 'content'; type: 'success' | 'warning' | 'info' | 'error' })
+ * ```
+ */
+export function showMsg(config: ConfigType) {
+    let content, type: MessageType
+    if (!config) {
+        content = '接口返回数据错误'
+        type = 'error'
+    }
+    else if (typeof config === 'string') {
+        content = config
+        type = 'error'
+    }
+    else {
+        content = config.content
+        type = config.type
+    }
+    types[type](content)
+}
+export function loginMsgBox(content: string, pathname: string) {
+    showDialog({
+        message: content,
+    }).then(() => {
+        window.$$lock = false
+        window.location.href = `/#/login`
+    }).catch(() => {
+    })
+}

+ 17 - 0
src/composables/provide.ts

@@ -0,0 +1,17 @@
+import type { AnyFn } from '@vueuse/core'
+
+/** 登录 */
+export const onLoginKey = Symbol('onLoginKey') as InjectionKey<AnyFn>
+/** 更新路由组件数据 */
+export const updateParentKey = Symbol('updateParentKey') as InjectionKey<AnyFn>
+
+/** 路由组件name */
+export const routerKey = Symbol('routerKey') as InjectionKey<Ref<string>>
+
+/** 路由组件接口是否报错 */
+export const dataHasErrorKey = Symbol('dataHasErrorKey') as InjectionKey<ComputedRef<boolean>>
+/** 路由组件接口是否请求完成 */
+export const dataIsReadyKey = Symbol('dataIsReadyKey') as InjectionKey<ComputedRef<number>>
+
+export const showDialogKey = Symbol('showDialogKey') as InjectionKey<ComputedRef<boolean>>
+export const toggleDialogKey = Symbol('toggleDialogKey') as InjectionKey<AnyFn>

+ 48 - 0
src/config/index.ts

@@ -0,0 +1,48 @@
+let ht = window.location.protocol;
+let ho = window.location.host.split('.')[1];
+const c = import.meta.env.VITE_APP_ENV
+let Host00, Host85
+switch (c) {
+  // 测试环境
+  case 'test':
+    Host00=ht + "//testsecure." + ho + ".com"
+    Host85=ht + "//testad." + ho + ".com"
+    break;
+    // 生产环境
+  case 'production':
+    Host00=ht + "//secure." + ho + ".com"
+    Host85=ht + "//ad." + ho + ".com"
+    break;
+
+  default:
+    // 开发环境
+    Host00="https://testsecure.6cd7e0f0b52.com"
+    Host85="https://testad.6cd7e0f0b52.com"
+    // Host00="http://103.214.175.29:8000"
+    // Host85="http://103.214.175.29:8500"
+    break;
+}
+const config = {
+  Host00,
+  Host85,
+  Host80: ht + "//secure." + ho + ".com",
+  Code: {
+    StatusOK: 200,
+    StatusFail: 400,
+    StatusSessionExpire: 600,
+    StatusSNotFound: 404,
+  },
+  Pattern: {
+    Email: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
+    Phone: /^1[3-9]\d{9}$/,
+    Password: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,16}$/,
+    Tel: /^[0][1-9]{2,3}-[0-9]{5,10}$/,
+    Num: /\d/,
+    NonNegInt: /^[0-9]+?$/, //非负整数
+    PosInt: /^[1-9]\d*$/, //正整数
+    nonnegative: /^[0-9]+([.]{1}[0-9]{1,2})?$/, //非负数(最多两位小数)
+    englishName: /^[^\u4e00-\u9fa5]+$/,
+  }
+}
+
+export default config

+ 33 - 0
src/design.config.ts

@@ -0,0 +1,33 @@
+/** 设计稿宽度 */
+export const designWidth = 375
+/** 设计稿高度 */
+export const designHeight = 667
+export const designMultiple = designWidth / 750
+/** 兼容最小宽度 */
+export const minWidth = 320
+/** 兼容最小宽度 */
+export const minWindow = `${minWidth}Px`
+/** 兼容最大宽度 */
+export const maxWidth = 540
+/** 兼容最大宽度 */
+export const maxWindow = `${maxWidth}Px`
+/**
+ * ```字号大小, 尽量配合Ui库, 比如vant就是设计稿宽度为375, rootfontsize为37.5
+ * 如果你的设计稿是750的, 方法有2
+ * 1: 将上面的设计稿宽度设置为750, 然后字号设置成75, 然后css代码的宽高按设计稿中实际的书写, 然后在postcss插件, 针对性判断vant的字号改成37.5(已自动适配)
+ * 2: 将上面的设计稿宽度设置为375, 然后字号设置成37.5, 然后css代码的宽高按设计稿中实际/2书写, 也可以将设计稿尺寸调整到375后, 按375的实际尺寸书写```
+ */
+export const fontSize = 37.5
+
+export function charsetRemoval() {
+    return {
+        postcssPlugin: 'internal:charset-removal',
+        AtRule: {
+            charset: (atRule: any) => {
+                if (atRule.name === 'charset') {
+                    atRule.remove()
+                }
+            },
+        },
+    }
+}

+ 117 - 0
src/locales/cn.ts

@@ -0,0 +1,117 @@
+export default {
+  common: {
+    confirm: '确认',
+    cancel: '取消',
+    loading: '加载中...',
+    success: '成功',
+    fail: '失败'
+  },
+  tabs: {
+    wallet: '钱包',
+    cards: '卡片',
+    finance: '金融',
+    mine: '我的'
+  },
+  wallet: {
+    title: '我的钱包',
+    balance: '余额',
+    transactions: '交易记录',
+  },
+  cards: {
+    title: '我的卡片',
+    balance: '余额',
+    transactions: '交易记录',
+  },
+  finance: {
+    title: '金融',
+  },
+  mine: {
+    title: '我的',
+  },
+  language: {
+    title: '语言设置',
+    selectLang: '选择语言',
+    cn: '中文简体',
+    zh: '中文繁体',
+    en: 'English',
+    de: 'German',
+    es: 'Spanish',
+    ar: 'Arabic',
+    id: 'Indonesian',
+    ms: 'Malay',
+    th: 'Thai',
+    vi: 'Vietnamese',
+    i1: '语言设置',
+    i2: '设置支付密码',
+    i3: '安全设置',
+    i4: '购卡记录',
+    i5: '关于PayouCard',
+  },
+  login: {
+    title: '登录',
+    username: '用户名',
+    password: '密码',
+    rememberPassword: '记住密码',
+    forgotPassword: '忘记密码?',
+  },
+  "card-transaction-detail": {
+    title: '交易详情',
+    currency: '币种',
+    amount: '交易金额',
+    type: '交易类型',
+    consume: '消费',
+    desc: '交易描述',
+    status: '订单状态',
+    time: '时间',
+  },
+  "pay-password": {
+    title: '支付密码',
+    change: '修改支付密码',
+    forget: '忘记支付密码',
+  },
+  "change-pay-password": {
+    title: '修改支付密码',
+    desc: '向您手机157***1673发送验证码',
+    input: '请输入验证码',
+    getCode: '获取验证码',
+    nextStep: '下一步',
+  },
+  "forget-pay-password": {
+    title: '忘记支付密码',
+    desc: '向您手机157***1673发送验证码',
+    input: '请输入验证码',
+    getCode: '获取验证码',
+    nextStep: '下一步',
+  },
+  "find-password": {
+    title: '找回密码',
+    desc: '请上传签名照片',
+    input: '请输入验证码',
+    getCode: '获取验证码',
+    nextStep: '下一步',
+  },
+  "freeze-card": {
+    title: "冻结卡片",
+    desc: "请上传签名照片",
+    tip: "支持PNG、JPG格式。",
+    nextStep: "确定",
+    formatError: "仅支持PNG、JPG格式",
+    needUpload: "请上传签名照片",
+    submitted: "已提交"
+  },
+  "card-recharge": {
+    title: '卡片充值',
+    currency: '币种',
+    cardNo: '卡号',
+    cardNoPlaceholder: '请输入卡号',
+    amount: '金额',
+    amountPlaceholder: '请输入金额,最低金额10',
+    walletBalance: '钱包余额',
+    all: '全部',
+    recharge: '充值',
+    fee: '手续费',
+    records: '交易记录',
+    walletPay: '钱包支付',
+    rechargeSuccess: '充值成功',
+  },
+}

+ 50 - 0
src/locales/de.ts

@@ -0,0 +1,50 @@
+export default {
+    common: {
+      confirm: '确认',
+      cancel: '取消',
+      loading: '加载中...',
+      success: '成功',
+      fail: '失败'
+    },
+    tabs: {
+      wallet: '钱包',
+      cards: '卡片',
+      finance: '金融',
+      mine: '我的'
+    },
+    wallet: {
+      title: '我的钱包',
+      balance: '余额',
+      transactions: '交易记录',
+    },
+    cards: {
+      title: '我的卡片',
+      balance: '余额',
+      transactions: '交易记录',
+    },
+    finance: {
+      title: '金融',
+    },
+    mine: {
+      title: '我的',
+    },
+    language: {
+      title: '语言设置',
+      selectLang: '选择语言',
+      cn: '中文简体',
+      zh: '中文繁体',
+      en: 'English',
+      de: 'German',
+      es: 'Spanish',
+      ar: 'Arabic',
+      id: 'Indonesian',
+      ms: 'Malay',
+      th: 'Thai',
+      vi: 'Vietnamese',
+      i1: '语言设置',
+      i2: '设置支付密码',
+      i3: '安全设置',
+      i4: '购卡记录',
+      i5: '关于PayouCard',
+    },
+  }

+ 50 - 0
src/locales/en.ts

@@ -0,0 +1,50 @@
+export default {
+    common: {
+      confirm: '确认',
+      cancel: '取消',
+      loading: '加载中...',
+      success: '成功',
+      fail: '失败'
+    },
+    tabs: {
+      wallet: '钱包',
+      cards: '卡片',
+      finance: '金融',
+      mine: '我的'
+    },
+    wallet: {
+      title: '我的钱包',
+      balance: '余额',
+      transactions: '交易记录',
+    },
+    cards: {
+      title: '我的卡片',
+      balance: '余额',
+      transactions: '交易记录',
+    },
+    finance: {
+      title: '金融',
+    },
+    mine: {
+      title: '我的',
+    },
+    language: {
+      title: '语言设置',
+      selectLang: '选择语言',
+      cn: '中文简体',
+      zh: '中文繁体',
+      en: 'English',
+      de: 'German',
+      es: 'Spanish',
+      ar: 'Arabic',
+      id: 'Indonesian',
+      ms: 'Malay',
+      th: 'Thai',
+      vi: 'Vietnamese',
+      i1: '语言设置',
+      i2: '设置支付密码',
+      i3: '安全设置',
+      i4: '购卡记录',
+      i5: '关于PayouCard',
+    },
+  }

+ 63 - 0
src/locales/zh.ts

@@ -0,0 +1,63 @@
+export default {
+    common: {
+      confirm: '确认',
+      cancel: '取消',
+      loading: '加载中...',
+      success: '成功',
+      fail: '失败'
+    },
+    tabs: {
+      wallet: '钱包',
+      cards: '卡片',
+      finance: '金融',
+      mine: '我的'
+    },
+    wallet: {
+      title: '我的钱包',
+      balance: '余额',
+      transactions: '交易记录',
+    },
+    cards: {
+      title: '我的卡片',
+      balance: '余额',
+      transactions: '交易记录',
+    },
+    finance: {
+      title: '金融',
+    },
+    mine: {
+      title: '我的',
+    },
+    language: {
+      title: '语言设置',
+      selectLang: '选择语言',
+      cn: '中文简体',
+      zh: '中文繁体',
+      en: 'English',
+      de: 'German',
+      es: 'Spanish',
+      ar: 'Arabic',
+      id: 'Indonesian',
+      ms: 'Malay',
+      th: 'Thai',
+      vi: 'Vietnamese',
+      i1: '语言设置',
+      i2: '设置支付密码',
+      i3: '安全设置',
+      i4: '购卡记录',
+      i5: '关于PayouCard',
+    },
+    "card-recharge": {
+      currency: '币种',
+      account: '账户',
+      amount: '金额',
+      amountPlaceholder: '请输入金额,最低金额10',
+      walletBalance: '钱包余额',
+      all: '全部',
+      recharge: '充值',
+      fee: '手续费',
+      records: '交易记录',
+      walletPay: '钱包支付',
+      rechargeSuccess: '充值成功',
+    },
+  }

+ 27 - 0
src/main.ts

@@ -0,0 +1,27 @@
+// import 'default-passive-events'
+
+import { createHead } from '@unhead/vue/client'
+import { createPinia } from 'pinia'
+import { createApp } from 'vue'
+
+import App from './App.vue'
+import global from './plugin/global'
+import vant from './plugin/vant'
+import router from './router'
+import {i18n} from './plugin/i18n'
+
+import 'uno.css'
+import 'vue-cropper/dist/index.css'
+import 'vant/es/toast/style'
+import 'vant/es/dialog/style'
+import 'vant/es/notify/style'
+import 'vant/es/image-preview/style'
+
+import './assets/scss/global/global.scss'
+import './assets/scss/style.scss'
+
+const app = createApp(App)
+const store = createPinia()
+const head = createHead()
+
+app.use(store).use(router).use(head).use(vant).use(global).use(i18n).mount('#app')

+ 23 - 0
src/plugin/global.ts

@@ -0,0 +1,23 @@
+import type { App } from 'vue'
+import { transformStr, UTC2Date } from '@lincy/utils'
+
+function install(app: App) {
+    app.mixin({
+        mounted() {
+            const blackComponents = ['router-link', 'keep-alive', 'transition-group', 'KeepAlive', 'BaseTransition', 'RouterView']
+            const componentName = this.$options.name
+            if (componentName && !componentName.includes('van-') && !blackComponents.includes(componentName)) {
+                console.log(`%c[${UTC2Date('', 'yyyy-mm-dd hh:ii:ss.SSS')}] ${componentName} Mounted`, 'color: green')
+                window[`$$${transformStr(componentName)}` as any] = this
+            }
+        },
+        methods: {
+            handleGoUrl(url: string) {
+                window.location.href = url
+            },
+        },
+    })
+}
+export default {
+    install,
+}

+ 21 - 0
src/plugin/i18n.ts

@@ -0,0 +1,21 @@
+import { createI18n } from 'vue-i18n'
+import cn from '../locales/cn'
+import zh from '../locales/zh'
+import en from '../locales/en'
+import de from '../locales/de'
+
+const i18n = createI18n({
+  legacy: false, // 使用 Composition API 模式
+  locale: localStorage.getItem('language') || 'cn', // 默认语言
+  fallbackLocale: 'en', // 备用语言
+  messages: {
+    'zh': zh,
+    'en': en,
+    'de': de,
+    'cn': cn
+  }
+})
+const localesList = ['cn','en','zh', 'de']
+// const localesList = ['en','cn','zh', 'de','es','ar','id','ms','th','vi','ko','pt','fa','tr']
+
+export {i18n,localesList}

+ 19 - 0
src/plugin/vant.ts

@@ -0,0 +1,19 @@
+import type { App } from 'vue'
+import { closeToast, showConfirmDialog, showDialog, showFailToast, showLoadingToast, showSuccessToast, showToast } from 'vant'
+
+function install(app: App) {
+    app.config.globalProperties.$dialog = {
+        default: showDialog,
+        confirm: showConfirmDialog,
+    }
+    app.config.globalProperties.$toast = {
+        default: showToast,
+        loading: showLoadingToast,
+        success: showSuccessToast,
+        fail: showFailToast,
+        close: closeToast,
+    }
+}
+export default {
+    install,
+}

+ 26 - 0
src/router/index.ts

@@ -0,0 +1,26 @@
+import type { RouteRecordRaw } from 'vue-router'
+
+const pages = import.meta.glob('../views/*.vue')
+
+let routes: Array<RouteRecordRaw> = []
+Object.keys(pages).forEach((path: string) => {
+    const math = path.match(/\.\/views(.*)\.vue$/)
+    if (math) {
+        const name = math[1].toLowerCase()
+        routes.push({
+            name: name.replace('/', ''),
+            path: name === '/home' ? '/' : name.replace(/-/g, '/'),
+            component: pages[path], // () => import('./views/*.vue')
+        })
+    }
+    return {}
+})
+
+routes = routes.concat([{ path: '/:pathMatch(.*)', redirect: '/' }])
+
+const router = createRouter({
+    history: createWebHashHistory(),
+    routes,
+})
+
+export default router

+ 82 - 0
src/shims-global.d.ts

@@ -0,0 +1,82 @@
+/**
+ * Null 或者 Undefined 或者 T
+ */
+declare type Nullable<T> = T | null | undefined
+/**
+ * 非 Null 类型
+ */
+declare type NonNullable<T> = T extends null | undefined ? never : T
+/**
+ * 数组<T> 或者 T
+ */
+declare type Arrayable<T> = T | T[]
+/**
+ * 键为字符串, 值为 Any 的对象
+ */
+declare type Objable<T = any> = Record<string, T>
+/**
+ * Function
+ */
+declare type Fn<T = void> = () => T
+/**
+ * 任意函数
+ */
+declare type AnyFn<T = any> = (...args: any[]) => T
+/**
+ * Promise, or maybe not
+ */
+declare type Awaitable<T> = T | PromiseLike<T>
+
+declare interface ResDataLists<T> {
+    hasNext: number | boolean
+    hasPrev: number | boolean
+    total: number
+    list: T[]
+}
+
+/**
+ * 接口返回模板
+ * ```
+ * {
+    data: T
+    code: number
+    message: string
+    info?: string
+ * }
+ * ```
+ */
+declare interface ResponseData<T> {
+    data: T
+    code: number
+    msg: string
+    [propName: string]: any
+}
+
+type Methods = 'get' | 'post' | 'delete' | 'put'
+
+declare interface ApiType {
+    get: <T>(url: string, data?: Objable, header?: Objable, checkCode?: boolean) => Promise<ResponseData<T>>
+    get: <T, U = Obj>(url: string, data?: Objable, header?: Objable, checkCode?: boolean) => Promise<ResponseData<T> & U>
+    post: <T>(url: string, data?: Objable, header?: Objable, checkCode?: boolean) => Promise<ResponseData<T>>
+    post: <T, U = Obj>(url: string, data?: Objable, header?: Objable, checkCode?: boolean) => Promise<ResponseData<T> & U>
+    put: <T>(url: string, data?: Objable, header?: Objable, checkCode?: boolean) => Promise<ResponseData<T>>
+    put: <T, U = Obj>(url: string, data?: Objable, header?: Objable, checkCode?: boolean) => Promise<ResponseData<T> & U>
+    delete: <T>(url: string, data?: Objable, header?: Objable, checkCode?: boolean) => Promise<ResponseData<T>>
+    delete: <T, U = Obj>(url: string, data?: Objable, header?: Objable, checkCode?: boolean) => Promise<ResponseData<T> & U>
+    downFile: (url: string, method: Methods, data?: Objable) => Promise<any>
+    RESTful: <T>(url: string, method: Methods, data?: Objable, header?: Objable, checkCode?: boolean) => Promise<ResponseData<T>>
+    $RESTful: <T>(url: string, method: Methods, data?: Objable, header?: Objable) => Promise<ResponseData<T>>
+}
+
+declare interface Window {
+    _designWidth: number
+    _designHeight: number
+    _designMultiple: number
+    _minWidth: number
+    _minWindow: string
+    _fontSize: number
+    $$lock?: boolean
+    $$api: ApiType
+    $$time: NodeJS.Timeout
+    axios: import('axios').AxiosStatic
+}

+ 6 - 0
src/shims-types.d.ts

@@ -0,0 +1,6 @@
+/// <reference types="@types/node" />
+/// <reference types="@lincy/utils" />
+/// <reference types="vite/client" />
+/// <reference types="unplugin-vue-macros/macros-global" />
+
+export {}

+ 5 - 0
src/shims-unocss.d.ts

@@ -0,0 +1,5 @@
+import type { AttributifyAttributes } from '@unocss/preset-attributify'
+
+declare module '@vue/runtime-dom' {
+    interface HTMLAttributes extends AttributifyAttributes {}
+}

+ 6 - 0
src/shims-vue.d.ts

@@ -0,0 +1,6 @@
+declare module '*.vue' {
+    import type { defineComponent } from 'vue'
+
+    const Component: ReturnType<typeof defineComponent>
+    export default Component
+}

+ 2 - 0
src/shims.d.ts

@@ -0,0 +1,2 @@
+declare module 'locutus/php/strings/trim'
+declare module 'vue-cropper'

+ 9 - 0
src/stores/pinia.types.ts

@@ -0,0 +1,9 @@
+export interface GlobalState {
+    globalLoading: boolean
+    routerLoading: boolean
+    ISLocal: boolean
+    ISDEV: boolean
+    ISPRE: boolean
+    ISPROD: boolean
+    isPageSwitching: boolean
+}

+ 28 - 0
src/stores/use-global-store.ts

@@ -0,0 +1,28 @@
+import type { GlobalState } from './pinia.types'
+
+const useStore = defineStore('globalStore', () => {
+    const state: GlobalState = reactive({
+        globalLoading: true,
+        routerLoading: false,
+        ISLocal: import.meta.env.VITE_APP_ENV === 'development',
+        ISDEV: import.meta.env.VITE_APP_ENV === 'development',
+        ISPRE: import.meta.env.VITE_APP_ENV === 'pre-release',
+        ISPROD: import.meta.env.VITE_APP_ENV === 'production',
+        isPageSwitching: false,
+    })
+
+    const setGlobalLoading = (payload: boolean) => {
+        state.globalLoading = payload
+    }
+    const setRouterLoading = (payload: boolean) => {
+        state.routerLoading = payload
+    }
+
+    return {
+        ...toRefs(state),
+        setGlobalLoading,
+        setRouterLoading,
+    }
+})
+
+export default useStore // 导出

+ 46 - 0
src/stores/use-user-store.ts

@@ -0,0 +1,46 @@
+import { defineStore } from 'pinia'
+import { ref } from 'vue'
+import ls from 'store2'
+import crypt from '../composables/crypt'
+
+export interface UserInfo {
+  id?: string
+  name: string
+  email: string
+  phone: string
+}
+const STORAGE_KEY = 'user'
+const useUserStore = defineStore('userStore', () => {
+  const userInfo = ref<UserInfo | null>(null)
+  const isLoggedIn = ref(false)
+  const initUserInfo = () => {
+    const encryptedInfo = ls.get(STORAGE_KEY)
+    if (encryptedInfo) {
+      const decryptedInfo = crypt.decrypt(encryptedInfo)
+      if (decryptedInfo) {
+        userInfo.value = JSON.parse(decryptedInfo)
+        isLoggedIn.value = true
+      }
+    }
+  }
+  const saveUserInfo = (info: UserInfo) => {
+    userInfo.value = info
+    isLoggedIn.value = true
+    const encryptedInfo = crypt.encrypt(JSON.stringify(info))
+    ls.set(STORAGE_KEY, encryptedInfo)
+  }
+  const clearUserInfo = () => {
+    userInfo.value = null
+    isLoggedIn.value = false
+    ls.remove(STORAGE_KEY)
+  }
+  initUserInfo()
+  return {
+    userInfo,
+    isLoggedIn,
+    saveUserInfo,
+    clearUserInfo,
+  }
+})
+
+export default useUserStore

+ 90 - 0
src/test.ts

@@ -0,0 +1,90 @@
+interface SearchFunc {
+    (source: string, subString: string): boolean
+}
+
+const mySearch: SearchFunc = (source, subString) => {
+    const result = source.search(subString)
+    return result > -1
+}
+
+mySearch('123', '123')
+
+const suits = ['hearts', 'spades', 'clubs', 'diamonds']
+
+function pickCard(x: { suit: string, card: number }[]): number
+function pickCard(x: number): { suit: string, card: number }
+function pickCard(x: any) {
+    // Check to see if we're working with an object/array
+    // if so, they gave us the deck and we'll pick the card
+    if (typeof x == 'object') {
+        const pickedCard = Math.floor(Math.random() * x.length)
+        return pickedCard
+    }
+    // Otherwise just let them pick the card
+    else if (typeof x == 'number') {
+        const pickedSuit = Math.floor(x / 13)
+        return { suit: suits[pickedSuit], card: x % 13 }
+    }
+    return []
+}
+
+const myDeck = [
+    { suit: 'diamonds', card: 2 },
+    { suit: 'spades', card: 10 },
+    { suit: 'hearts', card: 4 },
+]
+const _pickedCard1 = myDeck[pickCard(myDeck)]
+const _pickedCard2 = pickCard(13)
+
+// ==================
+
+function identity<T>(arg: T): string {
+    return `${arg}!`
+}
+const _output = identity<number>(123)
+
+function loggingIdentity<T>(arg: T[]): T[] {
+    console.log(arg.length) // Array has a .length, so no more error
+    return arg
+}
+
+const _output2 = loggingIdentity<number>([123])
+
+interface GenericIdentityFn {
+    <T>(arg: T): Promise<T>
+}
+
+const myIdentity: GenericIdentityFn = (arg) => {
+    return Promise.resolve(arg)
+}
+
+myIdentity<number>(123)
+
+// ==========
+
+interface Lengthwise {
+    length: number
+}
+
+function loggingIdentity2<T extends Lengthwise>(arg: T): T {
+    console.log(arg.length) // Now we know it has a .length property, so no more error
+    return arg
+}
+function loggingIdentity3(arg: Lengthwise): Lengthwise {
+    console.log(arg.length) // Now we know it has a .length property, so no more error
+    return arg
+}
+
+loggingIdentity2('0')
+loggingIdentity3('0')
+
+// ==============
+
+function getProperty<T, K extends keyof T>(obj: T, key: K) {
+    return obj[key]
+}
+
+const x = { a: 1, b: 2, c: 3, d: 4 }
+
+getProperty(x, 'a') // okay
+getProperty(x, 'b') // error: Argument of type 'm' isn't assignable to 'a' | 'b' | 'c' | 'd'.

+ 72 - 0
src/types.ts

@@ -0,0 +1,72 @@
+export interface Article {
+    c_id: string
+    c_title: string
+    c_content: string
+    c_posttime?: string
+}
+
+export interface _UserListConfig<T = any> {
+    /** 定时器 */
+    timer: Nullable<NodeJS.Timeout>
+    /** 是否加载完成 */
+    isLoaded: boolean
+    // 列表数据 ==>
+    /** 当前页数 */
+    page: number
+    /** 列表数据 */
+    dataList: T[]
+    // <==列表数据
+    config: {
+        // 下拉刷新 ==>
+        /** 下拉刷新-加载状态 */
+        isLoading: boolean
+        /** 下拉刷新-是否刷新 */
+        isRefresh: boolean
+        // <==下拉刷新
+        // 滚动加载 ==>
+        /** 分页加载状态 */
+        loadStatus: 'loadmore' | 'nomore' | 'loading'
+        /** 竟态锁 */
+        isLock: boolean
+        /** 接口请求中 */
+        loading: boolean
+        /** 接口是否报错 */
+        error: boolean
+        /** 接口请求完成 */
+        finished: boolean
+    }
+}
+
+export interface UserListsInitApi {
+    method: Methods
+    url: string
+    config: Record<string, any>
+}
+
+export interface UserListsInit {
+    api: UserListsInitApi
+}
+
+export type UserListConfig<T = any> = _UserListConfig<T> & UserListsInit
+
+export interface TopicList<T = any> {
+    page: number
+    items: T[]
+    refreshing: boolean
+    loading: boolean
+    error: boolean
+    finished: boolean
+}
+
+export interface UseTabList<T> {
+    api: UserListsInitApi[]
+    timer: any
+    list: (TopicList<T>)[]
+    tabs: string[]
+    [propName: string]: any
+}
+
+export interface UseTabListsInit {
+    api: UserListsInitApi[]
+    tabs: string[]
+}

+ 26 - 0
src/utils/message.ts

@@ -0,0 +1,26 @@
+import type { NotifyType } from 'vant'
+import { showNotify } from 'vant'
+
+interface ConfigType {
+    content: string
+    type: NotifyType
+}
+
+export default {
+    showMsg(config: ConfigType | string) {
+        let content, type: NotifyType
+        if (!config) {
+            content = '接口返回数据错误'
+            type = 'danger'
+        }
+        else if (typeof config === 'string') {
+            content = config
+            type = 'danger'
+        }
+        else {
+            content = config.content
+            type = config.type
+        }
+        showNotify({ type, message: content })
+    },
+}

+ 161 - 0
src/views/about.vue

@@ -0,0 +1,161 @@
+<template>
+    <!-- eslint-disable vue/valid-v-model -->
+    <div class="about-wrap" :class="$options.name">
+        <div class="route-wrap p-10px">
+            <div class="">
+                <van-cell-group>
+                    <van-cell icon="points" title="我的积分" is-link to="/about/detail" />
+                    <van-cell icon="gold-coin-o" title="我的优惠券" is-link to="/about/detail" />
+                    <van-cell icon="gift-o" title="我的头像" is-link to="/about/avatar" />
+                </van-cell-group>
+            </div>
+            <div class="mt-10px">
+                <van-cell-group>
+                    <van-field value="输入框已禁用" label="用户名" left-icon="contact" />
+                    <van-field value="输入框已禁用" label="密码" left-icon="browsing-history-o" />
+                </van-cell-group>
+            </div>
+            <div class="mt-10px">
+                <van-button hairline plain type="primary" size="small">细边框按钮</van-button>
+                <van-button loading type="primary" size="small" />
+                <van-button loading type="primary" size="small" loading-type="spinner" />
+                <van-button loading type="danger" size="small" loading-text="加载中..." />
+            </div>
+            <div class="mt-10px">
+                <van-image width="5rem" height="5rem" fit="cover" src="https://img.yzcdn.cn/vant/cat.jpeg" />
+            </div>
+            <div class="mt-10px">
+                <van-button type="primary" size="small" @click="showPopup"> 展示弹出层 </van-button>
+                {{ res.dateText }}
+            </div>
+            <div class="mt-10px">
+                <van-button type="primary" size="small" @click="handleToast">加载提示</van-button>
+            </div>
+            <div class="mt-10px">
+                <van-button type="primary" size="small" @click="previewShow = true">图片预览</van-button>
+                <van-image-preview v-model:show="previewShow" :images="images" />
+            </div>
+            <div class="mt-10px">
+                <van-button type="primary" size="small" @click="dialogShow = true">组件调用Dialog</van-button>
+                <van-button type="primary" size="small" @click="handleDialog">全局调用Dialog</van-button>
+            </div>
+            <!-- slot 插槽示例 -->
+            <img-list :img-arr="images">
+                <template #default="props">
+                    <!-- {{ props }} -->
+                    <van-image width="60" height="60" :src="props.item" @click="handleClickImg(props)" />
+                </template>
+            </img-list>
+            <!-- <div class="mt-10px">
+                <van-count-down :time="100000000" format="DD 天 HH 时 mm 分 ss 秒" />
+                <van-count-down :time="100000000" format="HH 时 mm 分 ss 秒" />
+            </div> -->
+            <van-popup v-model:show="res.show" position="bottom" class="fixed-center" :style="{ height: '40%' }">
+                <van-date-picker v-model="res.currentDate" type="date" @confirm="dateChange" @cancel="res.show = false" />
+            </van-popup>
+            <van-dialog v-model:show="dialogShow" :before-close="dialogBeforeClose" title="标题" show-cancel-button>
+                <img src="https://img.yzcdn.cn/vant/apple-3.jpg" style="max-width: 100%">
+            </van-dialog>
+        </div>
+    </div>
+</template>
+
+<script setup lang="ts">
+defineOptions({
+    name: 'AboutRouter',
+})
+
+useHead({
+    title: 'About',
+})
+
+// pinia 状态管理 ===>
+// const mainStore = useMainStore()
+// const { counter, name } = storeToRefs(mainStore)
+// const tmpCount = computed(() => mainStore.counter)
+// 监听状态变化
+// mainStore.$subscribe((mutation, state) => {
+//     console.log('mutation :>> ', mutation)
+//     console.log('state :>> ', JSON.stringify(state))
+// })
+// pinia 状态管理 <===
+
+// 父子组件通讯 ===>
+// const prop = defineProps({
+//     imgArr: Array
+// })
+
+// const { imgArr } = toRefs(prop)
+// 父子组件通讯 <===
+
+// 全局组件通信 ===>
+// const dataIsReady = inject('dataIsReady')
+// 全局组件通信 <===
+
+useSaveScroll()
+
+const previewShow = ref(false)
+const dialogShow = ref(false)
+
+const res = reactive({
+    show: false,
+    currentDate: [],
+    dateText: '',
+})
+
+function showPopup() {
+    res.show = true
+}
+function dateChange(val: { selectedValues: any[] }) {
+    res.dateText = val.selectedValues.join('-')
+    res.show = false
+}
+
+function handleToast() {
+    const toast1 = showLoadingToast({
+        duration: 0,
+        mask: true,
+        message: '加载中...',
+    })
+    setTimeout(() => {
+        toast1.close()
+    }, 3000)
+}
+function dialogBeforeClose(action: 'confirm' | 'cancel'): any {
+    return new Promise((resolve) => {
+        if (action === 'confirm') {
+            setTimeout(() => {
+                resolve(true)
+            }, 1000)
+        }
+        else {
+            resolve(true)
+        }
+    })
+}
+function handleDialog() {
+    showConfirmDialog({
+        title: '提示',
+        message: '代码是写出来给人看的,附带能在机器上运行',
+    })
+        .then(() => {
+            showToast('click confirm')
+        })
+        .catch(() => {
+            showToast('click cancel')
+        })
+}
+
+const images = ref([
+    'https://img.yzcdn.cn/public_files/2017/09/05/3bd347e44233a868c99cf0fe560232be.jpg',
+    'https://img.yzcdn.cn/public_files/2017/09/05/c0dab461920687911536621b345a0bc9.jpg',
+    'https://img.yzcdn.cn/public_files/2017/09/05/4e3ea0898b1c2c416eec8c11c5360833.jpg',
+    'https://img.yzcdn.cn/public_files/2017/09/05/fd08f07665ed67d50e11b32a21ce0682.jpg',
+])
+function handleClickImg(props: { index: number }) {
+    showImagePreview({
+        images: images.value,
+        startPosition: props.index,
+    })
+}
+</script>

+ 295 - 0
src/views/card-recharge.vue

@@ -0,0 +1,295 @@
+<template>
+    <div class="page card-recharge-page">
+        <div class="recharge-form">
+            <div class="form-group">
+                <label class="form-label">{{ t('card-recharge.currency') }}</label>
+                <div class="form-select" @click="showCurrency = true" style="cursor: pointer">
+                    {{ currency }}
+                </div>
+            </div>
+            <div class="form-group">
+                <label class="form-label">{{ t('card-recharge.cardNo') }}</label>
+                <input v-model="cardNo" disabled type="number" class="form-input" :placeholder="t('card-recharge.cardNoPlaceholder')" min="10" />
+            </div>
+            <div class="form-group">
+                <label class="form-label">{{ t('card-recharge.amount') }}</label>
+                <input v-model="amount" type="number" class="form-input" :placeholder="t('card-recharge.amountPlaceholder')" min="10" />
+            </div>
+            <div class="balance-info">
+                <span>{{ t('card-recharge.walletBalance') }}:</span>
+                <span class="balance-value">{{ balance }} {{ currency }}</span>
+                <span class="all-btn" @click="useAll">{{ t('card-recharge.all') }}</span>
+            </div>
+            <button class="confirm-btn" :disabled="!canRecharge" @click="handleRecharge">{{ t('card-recharge.recharge') }}</button>
+            <div class="fee-info">{{ t('card-recharge.fee') }}:{{ receivedAmount }} + {{ exchangeRate }}%</div>
+        </div>
+        <div class="transactions">
+            <div class="trans-title">{{ t('card-recharge.records') }}</div>
+            <div v-for="t in transactions" :key="t.time + t.amount" class="transaction">
+                <div class="trans-left">
+                    <div class="trans-type">{{ t.type }}</div>
+                </div>
+                <div class="trans-right">
+                    <div class="trans-amount">{{ t.amount }} {{ t.currency }}</div>
+                    <div class="trans-date">{{ t.time }}</div>
+                </div>
+            </div>
+        </div>
+        <CurrencySelect v-model="showCurrency" :options="currencyOptions" @select="onCurrencySelect" />
+    </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted } from 'vue'
+import { useI18n } from 'vue-i18n'
+import CurrencySelect from '@/components/CurrencySelect.vue'
+import { useRoute } from 'vue-router'
+import { ucardApi } from '@/api/ucard'
+import { showToast } from 'vant'
+interface Transaction {
+    type: string
+    amount: number
+    time: string
+}
+const route = useRoute()
+const { uniqueId, cardNo } = route.query
+const { t } = useI18n()
+const currency = ref('')
+const exchangeRate = ref(0)
+const receivedAmount = ref(0)
+const showCurrency = ref(false)
+const currencyOptions = ref<{ label: string; value: string }[]>([])
+const amount = ref('')
+const balance = ref(0)
+const transactions = ref<Transaction[]>([])
+// 获取充值记录
+async function getRechargeList() {
+    try {
+        const res = await ucardApi.rechargeList({
+            cardNo: cardNo as string,
+            // currency: currency.value,
+            page: { current: 1, row: 10 },
+        })
+        if (res.code === 200 && res.data) {
+            transactions.value = res.data
+        }
+    } catch (error) {
+        showToast(error)
+    }
+}
+// 获取卡片余额
+async function getCardBalance() {
+    try {
+        const res = await ucardApi.ucardBalance({
+            cardNo: cardNo as string,
+            uniqueId: uniqueId as string,
+        })
+        if (res.code === 200 && res.data) {
+            currencyOptions.value = res.data.map((item: { currency: string }) => ({
+                label: item.currency,
+                value: item.currency,
+                ...item,
+            }))
+            currency.value = res.data[0].currency
+            balance.value = res.data[0].amount
+            getRechargeList()
+        }
+    } catch (error) {
+        console.error('获取卡片余额失败:', error)
+    }
+}
+onMounted(() => {
+    if (cardNo) {
+        getCardBalance()
+    }
+})
+const canRecharge = computed(() => Number(amount.value) >= 10 && Number(amount.value) <= balance.value)
+function useAll() {
+    amount.value = balance.value.toString()
+}
+function onCurrencySelect(item: { label: string; value: string; amount: number }) {
+    currency.value = item.value
+    balance.value = item.amount
+    getRechargeList()
+}
+// 充值
+async function handleRecharge() {
+    estimateRecharge()
+    try {
+        const params = {
+            cardNo: cardNo as string,
+            uniqueId: uniqueId as string,
+            amount: Number(amount.value),
+            currency: 'USDT',
+        }
+        const response = await ucardApi.ucardRecharge(params)
+        if (response.code === 200) {
+            showToast('充值请求已提交成功')
+            const orderId = response.data.orderId
+            return orderId
+        } else {
+            showToast(response.msg)
+        }
+    } catch (error) {
+        showToast(error)
+    }
+}
+// 预估充值
+async function estimateRecharge() {
+    try {
+        const params = {
+            amount: Number(amount.value),
+            cardTypeId: 3,
+            currency: 'USDT',
+        }
+        const response = await ucardApi.ucardRechargeEstimate(params)
+        if (response.code === 200) {
+            exchangeRate.value = response.data.exchangeRate
+            receivedAmount.value = (response.data.receivedAmount * exchangeRate.value) / 100
+        } else {
+            showToast(response.msg)
+        }
+    } catch (error) {
+        showToast(error)
+    }
+}
+</script>
+
+<style scoped lang="scss">
+.card-recharge-page {
+    min-height: 100vh;
+    background: linear-gradient(135deg, #232323 0%, #2a2a2a 100%);
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    padding-top: 32px;
+}
+.recharge-form {
+    width: 100%;
+    max-width: 340px;
+    margin: 0 auto 24px auto;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    background: rgba(24, 24, 24, 0.92);
+    border-radius: 20px;
+    box-shadow: 0 4px 32px 0 rgba(214, 255, 0, 0.08);
+    padding: 32px 20px 24px 20px;
+}
+.form-group {
+    width: 100%;
+    margin-bottom: 18px;
+}
+.form-label {
+    color: #fff;
+    font-size: 15px;
+    margin-bottom: 8px;
+    display: block;
+}
+.form-select,
+.form-input {
+    width: 100%;
+    padding: 10px 12px;
+    border-radius: 10px;
+    border: 1px solid #d6ff00;
+    background: #181818;
+    color: #fff;
+    font-size: 16px;
+    margin-top: 4px;
+}
+.form-input::-webkit-input-placeholder {
+    color: #bdbdbd;
+}
+.balance-info {
+    width: 100%;
+    color: #bdbdbd;
+    font-size: 14px;
+    margin-bottom: 10px;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+}
+.balance-value {
+    color: #d6ff00;
+    font-weight: bold;
+    margin: 0 8px;
+}
+.all-btn {
+    color: #d6ff00;
+    cursor: pointer;
+    font-size: 14px;
+    margin-left: 8px;
+}
+.confirm-btn {
+    width: 100%;
+    height: 44px;
+    background: linear-gradient(90deg, #d6ff00 0%, #eaff7b 100%);
+    color: #232323;
+    border: none;
+    border-radius: 22px;
+    font-size: 18px;
+    font-weight: bold;
+    margin-top: 18px;
+    cursor: pointer;
+    box-shadow: 0 2px 12px 0 rgba(214, 255, 0, 0.1);
+    letter-spacing: 2px;
+    transition: background 0.2s, box-shadow 0.2s;
+}
+.confirm-btn:disabled {
+    opacity: 0.5;
+    cursor: not-allowed;
+}
+.fee-info {
+    color: #bdbdbd;
+    font-size: 13px;
+    margin-top: 10px;
+    text-align: center;
+}
+.transactions {
+    background: var(--action-bg, #181818);
+    border-radius: 16px;
+    margin-bottom: 16px;
+    padding: 16px 12px;
+    width: 100%;
+    max-width: 340px;
+}
+.trans-title {
+    font-size: 16px;
+    margin-bottom: 10px;
+    color: var(--main-yellow, #d6ff00);
+}
+.transaction {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 8px 0;
+    border-bottom: 1px solid var(--border, #333);
+    font-size: 16px;
+}
+.transaction:last-child {
+    border-bottom: none;
+}
+.trans-left {
+    width: 120px;
+}
+.trans-right {
+    width: 160px;
+    div {
+        text-align: left;
+    }
+}
+.trans-type {
+    color: #fff;
+    font-size: 15px;
+    line-height: 2;
+}
+.trans-amount {
+    font-size: 16px;
+    color: var(--main-yellow, #d6ff00);
+    line-height: 2;
+}
+.trans-date {
+    color: #bdbdbd;
+    font-size: 13px;
+}
+</style>

+ 94 - 0
src/views/card-transaction-detail.vue

@@ -0,0 +1,94 @@
+<template>
+    <div class="page card-transaction-detail">
+        <div class="success-icon-wrap">
+            <svg class="success-icon" viewBox="0 0 64 64" width="80" height="80">
+                <circle cx="32" cy="32" r="30" fill="#1ed47f" opacity="0.15" />
+                <circle cx="32" cy="32" r="24" fill="#1ed47f" />
+                <polyline points="22,34 30,42 44,26" fill="none" stroke="#fff" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" />
+            </svg>
+        </div>
+        <div class="detail-card">
+            <div class="detail-row">
+                <span class="label">{{ t('card-transaction-detail.currency') }}</span>
+                <span class="value">{{ currency }}</span>
+            </div>
+            <div class="detail-row">
+                <span class="label">{{ t('card-transaction-detail.amount') }}</span>
+                <span class="value">{{ amount }}</span>
+            </div>
+            <div class="detail-row">
+                <span class="label">{{ t('card-transaction-detail.type') }}</span>
+                <span class="value">{{ type }}</span>
+            </div>
+            <div class="detail-row">
+                <span class="label">{{ t('card-transaction-detail.desc') }}</span>
+                <span class="value">{{ remark }}</span>
+            </div>
+            <div class="detail-row">
+                <span class="label">{{ t('card-transaction-detail.status') }}</span>
+                <span class="value success">{{ tradeStatusStr }}</span>
+            </div>
+            <div class="detail-row">
+                <span class="label">{{ t('card-transaction-detail.time') }}</span>
+                <span class="value">{{ date }}</span>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script setup lang="ts">
+import { useI18n } from 'vue-i18n'
+import { useRoute } from 'vue-router'
+const { t } = useI18n()
+const route = useRoute()
+const { id, cardNo, tradeStatusStr, amount, currency, type, remark, date } = route.query
+</script>
+
+<style scoped lang="scss">
+.card-transaction-detail {
+    min-height: 100vh;
+    background: linear-gradient(135deg, #232323 0%, #2a2a2a 100%);
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    padding-top: 48px;
+}
+.success-icon-wrap {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    margin-bottom: 32px;
+}
+.success-icon {
+    display: block;
+}
+.detail-card {
+    background: var(--action-bg, #181818);
+    border-radius: 18px;
+    box-shadow: 0 4px 24px rgba(30, 212, 127, 0.08);
+    padding: 28px 22px;
+    width: 90vw;
+    max-width: 400px;
+}
+.detail-row {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 12px 0;
+    border-bottom: 1px solid rgba(255, 255, 255, 0.06);
+    font-size: 16px;
+}
+.detail-row:last-child {
+    border-bottom: none;
+}
+.label {
+    color: #bdbdbd;
+}
+.value {
+    color: #fff;
+    font-weight: 500;
+}
+.value.success {
+    color: #1ed47f;
+}
+</style>

+ 533 - 0
src/views/cards.vue

@@ -0,0 +1,533 @@
+<template>
+    <div class="page">
+        <div class="card-swiper">
+            <div class="swiper-container" @touchstart="handleTouchStart" @touchmove="handleTouchMove" @touchend="handleTouchEnd">
+                <div
+                    class="card-wrapper"
+                    v-for="(card, index) in cardList"
+                    :key="card.id"
+                    :style="{ transform: `translateX(${(index - currentIndex) * 100 + offsetX}%)` }"
+                >
+                    <div class="card-info" @click.stop="(e: MouseEvent) => toggleCardNo(card.id, e)" :class="{ flipping: isFlipping[card.id] }">
+                        <div class="card-front">
+                            <div class="owner">
+                                <i class="i-mdi-account-circle-outline" />
+                                {{ card.firstName }}
+                                {{ card.lastName }}
+                            </div>
+                            <div class="number">
+                                <i class="i-mdi-credit-card" />
+                                {{ card.cardNo.replace(/(\d{4})\d+(\d{4})/, '$1 **** **** $2') }}
+                            </div>
+                            <div class="valid">
+                                <i class="i-mdi-calendar-clock" />
+                                VALID THRU {{ card.expire }}
+                            </div>
+                        </div>
+                        <div class="card-back">
+                            <div class="owner">
+                                <i class="i-mdi-account-circle-outline" />
+                                {{ card.firstName }}
+                                {{ card.lastName }}
+                            </div>
+                            <div class="number">
+                                <i class="i-mdi-credit-card" />
+                                {{ card.cardNo }}
+                            </div>
+                            <div class="valid">
+                                <i class="i-mdi-calendar-clock" />
+                                VALID THRU {{ card.expire }}
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+        <div class="actions">
+            <button class="action-btn" @click="goToCardRecharge">
+                <i class="i-mdi-credit-card-plus" />
+                <span>卡片充值</span>
+            </button>
+            <button class="action-btn" @click="goToFindPassword">
+                <i class="i-mdi-lock-reset" />
+                <span>找回密码</span>
+            </button>
+            <button class="action-btn" @click="goToFreezeCard">
+                <i class="i-mdi-credit-card-off" />
+                <span>冻结卡片</span>
+            </button>
+        </div>
+        <div class="balance-wrap">
+            <div class="currency">
+                <img src="https://upload.wikimedia.org/wikipedia/commons/a/a4/Flag_of_the_United_States.svg" class="flag" />
+                <span>USD</span>
+            </div>
+            <div class="balance">{{ balance.amount }}</div>
+        </div>
+        <div class="transactions">
+            <div class="trans-title">交易记录</div>
+            <div v-for="t in transactions" :key="t.date + t.amount + t.description" class="transaction" @click="goToTransactionDetail(t)">
+                <div class="trans-left">
+                    <div class="trans-type">{{ t.tradeTypeStr }}</div>
+                    <div class="trans-desc">{{ t.remark }}</div>
+                </div>
+                <div class="trans-right">
+                    <div class="trans-amount">{{ t.amount }} {{ t.currencyTxn }}</div>
+                    <div class="trans-date">{{ t.businessDate }}</div>
+                </div>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script setup lang="ts">
+const balance = {
+    currency: 'USD',
+    amount: 340.05,
+}
+const transactions = ref<TransactionInfo[]>([])
+import { ref, watch } from 'vue'
+import { ucardApi } from '@/api/ucard'
+import { useRouter } from 'vue-router'
+import type { CardInfo, TransactionInfo } from '@/api/ucard'
+const router = useRouter()
+const cardList = ref<CardInfo[]>([])
+const showCardNo = ref<{ [key: string]: boolean }>({})
+const isFlipping = ref<{ [key: string]: boolean }>({})
+const currentIndex = ref(5)
+const startX = ref(0)
+const offsetX = ref(0)
+const isDragging = ref(false)
+// 获取卡片列表
+const getCardList = async () => {
+    const res = await ucardApi.cardList({
+        page: {
+            current: 1,
+            row: 10,
+        },
+    })
+    cardList.value = res.data
+    cardList.value.forEach((card) => {
+        showCardNo.value[card.id] = false
+        isFlipping.value[card.id] = false
+    })
+    if (cardList.value.length > 0) {
+        getTransactions(cardList.value[currentIndex.value].cardNo)
+    }
+}
+// 获取交易记录
+const getTransactions = async (cardNo: string) => {
+    try {
+        const res = await ucardApi.transactionsList({
+            cardNo,
+            page: {
+                current: 1,
+                row: 10,
+            },
+        })
+        transactions.value = res.data || []
+    } catch (error) {
+        showToast(error)
+        transactions.value = []
+    }
+}
+watch(currentIndex, (newIndex) => {
+    if (cardList.value[newIndex]) {
+        getTransactions(cardList.value[newIndex].cardNo)
+    }
+})
+const toggleCardNo = (cardId: string, event: MouseEvent) => {
+    event.stopPropagation()
+    isFlipping.value[cardId] = !isFlipping.value[cardId]
+    showCardNo.value[cardId] = !showCardNo.value[cardId]
+}
+getCardList()
+const handleTouchStart = (e: TouchEvent) => {
+    startX.value = e.touches[0].clientX
+    isDragging.value = true
+}
+const handleTouchMove = (e: TouchEvent) => {
+    if (!isDragging.value) return
+    const currentX = e.touches[0].clientX
+    const diff = currentX - startX.value
+    const containerWidth = document.querySelector('.swiper-container')?.clientWidth || 0
+    if (currentIndex.value === 0 && diff > 0) {
+        offsetX.value = (diff / containerWidth) * 50
+    } else if (currentIndex.value === cardList.value.length - 1 && diff < 0) {
+        offsetX.value = (diff / containerWidth) * 50
+    } else {
+        offsetX.value = (diff / containerWidth) * 100
+    }
+}
+const handleTouchEnd = () => {
+    isDragging.value = false
+    if (Math.abs(offsetX.value) > 0.3) {
+        if (offsetX.value > 0 && currentIndex.value > 0) {
+            currentIndex.value--
+        } else if (offsetX.value < 0 && currentIndex.value < cardList.value.length - 1) {
+            currentIndex.value++
+        }
+        cardList.value.forEach((card) => {
+            showCardNo.value[card.id] = false
+        })
+    }
+    offsetX.value = 0
+}
+function goToCardRecharge() {
+    const currentCard = cardList.value[currentIndex.value]
+    router.push({
+        path: '/card/recharge',
+        query: {
+            cardId: currentCard.id,
+            uniqueId: currentCard.uniqueId,
+            cardNo: currentCard.cardNo,
+        },
+    })
+}
+function goToFindPassword() {
+    const currentCard = cardList.value[currentIndex.value]
+    router.push({
+        path: '/find/password',
+        query: {
+            cardId: currentCard.id,
+            uniqueId: currentCard.uniqueId,
+            cardNo: currentCard.cardNo,
+        },
+    })
+}
+function goToFreezeCard() {
+    const currentCard = cardList.value[currentIndex.value]
+    router.push({
+        path: '/freeze/card',
+        query: {
+            cardId: currentCard.id,
+            uniqueId: currentCard.uniqueId,
+            cardNo: currentCard.cardNo,
+        },
+    })
+}
+function goToTransactionDetail(transaction: TransactionInfo) {
+    router.push({
+        path: '/card/transaction/detail',
+        query: {
+            id: transaction.id,
+            cardNo: cardList.value[currentIndex.value].cardNo,
+            amount: transaction.amount,
+            currency: transaction.currencyTxn,
+            type: transaction.tradeTypeStr,
+            remark: transaction.remark,
+            date: transaction.businessDate,
+            tradeStatusStr: transaction.tradeStatusStr,
+        },
+    })
+}
+</script>
+
+<style scoped lang="scss">
+.card-wrapper {
+    position: absolute;
+    width: 100%;
+    height: 100%;
+    transition: transform 0.3s ease;
+    will-change: transform;
+}
+
+.card-info {
+    background: linear-gradient(135deg, #1a1a1a 0%, #2a2a2a 100%);
+    border-radius: 16px;
+    padding: 24px 20px 16px 20px;
+    color: var(--main-yellow);
+    box-shadow: 0 4px 20px rgba(214, 255, 0, 0.1);
+    border: 1px solid rgba(214, 255, 0, 0.2);
+    position: relative;
+    width: 100%;
+    height: 100%;
+    transform-style: preserve-3d;
+    transition: transform 0.6s ease;
+
+    &.flipping {
+        transform: rotateY(180deg);
+    }
+
+    &::before {
+        content: '';
+        position: absolute;
+        top: 0;
+        left: 0;
+        right: 0;
+        bottom: 0;
+        background: linear-gradient(135deg, rgba(214, 255, 0, 0.05) 0%, transparent 100%);
+        pointer-events: none;
+    }
+
+    &::after {
+        content: '';
+        position: absolute;
+        top: -50%;
+        left: -50%;
+        width: 200%;
+        height: 200%;
+        background: radial-gradient(circle at center, rgba(214, 255, 0, 0.1) 0%, transparent 50%);
+        opacity: 0.5;
+        pointer-events: none;
+    }
+}
+
+.card-front,
+.card-back {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    padding: 24px 20px 16px 20px;
+    backface-visibility: hidden;
+    -webkit-backface-visibility: hidden;
+}
+
+.card-back {
+    transform: rotateY(180deg);
+}
+
+.owner {
+    font-size: var(--font-size-14);
+    line-height: 2;
+    margin-bottom: 8px;
+    text-shadow: 0 0 10px rgba(214, 255, 0, 0.3);
+    display: flex;
+    align-items: center;
+    gap: 8px;
+
+    i {
+        font-size: var(--font-size-18);
+        color: var(--main-yellow);
+    }
+}
+
+.number {
+    font-size: var(--font-size-18);
+    line-height: 3;
+    letter-spacing: 2px;
+    margin-bottom: 8px;
+    text-shadow: 0 0 15px rgba(214, 255, 0, 0.4);
+    display: flex;
+    align-items: center;
+    gap: 8px;
+
+    i {
+        font-size: var(--font-size-18);
+        color: var(--main-yellow);
+    }
+}
+
+.valid {
+    font-size: var(--font-size-12);
+    color: var(--main-yellow-dark);
+    text-shadow: 0 0 8px rgba(214, 255, 0, 0.2);
+    display: flex;
+    align-items: center;
+    gap: 8px;
+
+    i {
+        font-size: var(--font-size-16);
+        color: var(--main-yellow-dark);
+    }
+}
+
+.actions {
+    display: flex;
+    justify-content: space-between;
+    margin: 20px 0 16px 0;
+}
+
+.action-btn {
+    background: var(--action-bg);
+    color: var(--white);
+    border: none;
+    border-radius: 12px;
+    padding: 10px 18px;
+    font-size: var(--font-size-14);
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+    cursor: pointer;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    gap: 4px;
+    transition: all 0.3s ease;
+
+    i {
+        font-size: var(--font-size-22);
+        color: var(--main-yellow);
+        margin-bottom: 4px;
+    }
+
+    &:hover {
+        transform: translateY(-2px);
+        box-shadow: 0 4px 12px rgba(214, 255, 0, 0.2);
+    }
+
+    &:active {
+        transform: translateY(0);
+    }
+}
+
+.balance-wrap {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    margin-bottom: 20px;
+    font-size: var(--font-size-14);
+}
+
+.currency {
+    display: flex;
+    align-items: center;
+    font-size: var(--font-size-14);
+    margin-right: 12px;
+    color: var(--white);
+}
+
+.flag {
+    width: 24px;
+    height: 24px;
+    border-radius: 50%;
+    margin-right: 6px;
+}
+
+.balance {
+    font-size: var(--font-size-16);
+    font-weight: bold;
+    color: var(--white);
+}
+
+.transactions {
+    background: var(--action-bg);
+    border-radius: 16px;
+    margin-bottom: 16px;
+    padding: 16px 12px;
+}
+
+.trans-title {
+    font-size: var(--font-size-16);
+    margin-bottom: 10px;
+    color: var(--main-yellow);
+}
+
+.transaction {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 8px 0;
+    border-bottom: 1px solid var(--border);
+    font-size: var(--font-size-16);
+}
+
+.transaction:last-child {
+    border-bottom: none;
+}
+
+.trans-left {
+    width: 200px;
+}
+
+.trans-right {
+    width: 100px;
+
+    div {
+        text-align: left;
+    }
+}
+
+.trans-type {
+    color: var(--white);
+    font-size: var(--font-size-16);
+    line-height: 2;
+}
+
+.trans-desc {
+    font-size: var(--font-size-13);
+    color: var(--white);
+}
+
+.trans-amount {
+    font-size: var(--font-size-16);
+    color: var(--main-yellow);
+    line-height: 2;
+}
+
+.trans-date {
+    color: var(--gray);
+    font-size: var(--font-size-13);
+}
+
+.card-swiper {
+    position: relative;
+    overflow: hidden;
+    margin-bottom: 20px;
+}
+
+.swiper-container {
+    position: relative;
+    width: 100%;
+    height: 150px;
+    touch-action: pan-y pinch-zoom;
+    user-select: none;
+}
+
+.card-info {
+    position: absolute;
+    width: 100%;
+    height: 100%;
+    transition: transform 0.3s ease;
+    will-change: transform;
+}
+
+.swiper-controls {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    margin-top: 16px;
+    gap: 16px;
+}
+
+.swiper-btn {
+    background: var(--action-bg);
+    border: none;
+    border-radius: 50%;
+    width: 32px;
+    height: 32px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    cursor: pointer;
+    color: var(--main-yellow);
+
+    &:disabled {
+        opacity: 0.5;
+        cursor: not-allowed;
+    }
+
+    i {
+        font-size: var(--font-size-20);
+    }
+}
+
+.swiper-dots {
+    display: flex;
+    gap: 8px;
+}
+
+.dot {
+    width: 8px;
+    height: 8px;
+    border-radius: 50%;
+    background: var(--action-bg);
+    cursor: pointer;
+    transition: all 0.3s ease;
+
+    &.active {
+        background: var(--main-yellow);
+        transform: scale(1.2);
+    }
+}
+</style>

+ 105 - 0
src/views/change-pay-password.vue

@@ -0,0 +1,105 @@
+<template>
+    <div class="page forget-pay-password-page">
+        <div class="form-box">
+            <div class="desc">{{ t('change-pay-password.desc') }}</div>
+            <div class="input-row">
+                <input class="input" v-model="code" :placeholder="t('change-pay-password.input')" />
+                <button class="code-btn" :disabled="codeBtnDisabled" @click="getCode">{{ t('change-pay-password.getCode') }}</button>
+            </div>
+            <button class="next-btn" @click="nextStep">{{ t('change-pay-password.nextStep') }}</button>
+        </div>
+    </div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import { useI18n } from 'vue-i18n'
+const { t } = useI18n()
+const code = ref('')
+const codeBtnDisabled = ref(false)
+function getCode() {
+    codeBtnDisabled.value = true
+    setTimeout(() => {
+        codeBtnDisabled.value = false
+    }, 60000)
+}
+function nextStep() {
+    // 下一步逻辑
+}
+</script>
+
+<style scoped lang="scss">
+.forget-pay-password-page {
+    min-height: 100vh;
+    background: linear-gradient(135deg, #232323 0%, #2a2a2a 100%);
+    padding: 0;
+    .header {
+        display: flex;
+        align-items: center;
+        padding: 24px 0 18px 12px;
+        .back-icon {
+            font-size: 22px;
+            color: var(--main-yellow, #d6ff00);
+            margin-right: 10px;
+            cursor: pointer;
+        }
+        .title {
+            font-size: 18px;
+            color: #fff;
+            font-weight: bold;
+        }
+    }
+    .form-box {
+        margin: 60px 18px 0 18px;
+        display: flex;
+        flex-direction: column;
+        gap: 32px;
+        .desc {
+            color: #fff;
+            font-size: 15px;
+            margin-bottom: 18px;
+        }
+        .input-row {
+            display: flex;
+            gap: 12px;
+            .input {
+                flex: 1;
+                height: 44px;
+                border-radius: 8px;
+                border: none;
+                padding: 0 12px;
+                font-size: 16px;
+                background: #181818;
+                color: #fff;
+            }
+            .code-btn {
+                background: var(--main-yellow, #d6ff00);
+                color: #232323;
+                border: none;
+                border-radius: 8px;
+                padding: 0 18px;
+                font-size: 15px;
+                height: 44px;
+                cursor: pointer;
+                font-weight: bold;
+            }
+            .code-btn:disabled {
+                opacity: 0.5;
+                cursor: not-allowed;
+            }
+        }
+        .next-btn {
+            width: 100%;
+            height: 48px;
+            background: var(--main-yellow, #d6ff00);
+            color: #232323;
+            border: none;
+            border-radius: 24px;
+            font-size: 18px;
+            font-weight: bold;
+            margin-top: 24px;
+            cursor: pointer;
+        }
+    }
+}
+</style>

+ 86 - 0
src/views/finance.vue

@@ -0,0 +1,86 @@
+<template>
+  <div class="page">
+    <div class="balance-wrap">
+      <div class="currency">
+        <img src="https://upload.wikimedia.org/wikipedia/commons/a/a4/Flag_of_the_United_States.svg" class="flag" />
+        <span>USD</span>
+      </div>
+      <div class="balance">340.05</div>
+    </div>
+    <div class="transactions">
+      <div class="trans-title">交易记录</div>
+      <div class="transaction">
+        <div class="trans-type">消费</div>
+        <div class="trans-desc">FEE RETAIL</div>
+        <div class="trans-amount">0.15 USD</div>
+        <div class="trans-date">2025-06-01</div>
+      </div>
+      <div class="transaction">
+        <div class="trans-type">消费</div>
+        <div class="trans-desc">CURSOR, AI POWERED</div>
+        <div class="trans-amount">20 USD</div>
+        <div class="trans-date">2025-06-01</div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+</script>
+
+<style scoped lang="scss">
+.balance-wrap {
+  display: flex;
+  align-items: center;
+  justify-content: flex-start;
+  margin: 0 16px 20px 16px;
+  font-size: var(--font-size-18);
+}
+.currency {
+  display: flex;
+  align-items: center;
+  margin-right: 12px;
+}
+.flag {
+  width: 24px;
+  height: 24px;
+  border-radius: 50%;
+  margin-right: 6px;
+}
+.balance {
+  font-size: var(--font-size-22);
+  font-weight: bold;
+  color: var(--main-yellow);
+}
+.transactions {
+  background: var(--action-bg);
+  border-radius: 16px;
+  margin: 0 16px 16px 16px;
+  padding: 16px 12px;
+}
+.trans-title {
+  font-size: var(--font-size-16);
+  margin-bottom: 10px;
+  color: var(--main-yellow);
+}
+.transaction {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 8px 0;
+  border-bottom: 1px solid var(--border);
+  font-size: var(--font-size-15);
+}
+.transaction:last-child {
+  border-bottom: none;
+}
+.trans-type {
+  color: var(--main-yellow);
+  width: 40px;
+}
+.trans-desc {
+  flex: 1;
+  color: var(--white);
+  margin-left: 10px;
+}
+</style>

+ 174 - 0
src/views/find-password.vue

@@ -0,0 +1,174 @@
+<template>
+    <div class="page find-password-page">
+        <div class="find-form">
+            <div class="upload-label">{{ t('find-password.desc') }}</div>
+            <div class="upload-box" @click="triggerFileInput">
+                <input ref="fileInput" type="file" accept="image/png, image/jpeg" style="display: none" @change="handleFileChange" />
+                <div v-if="!previewUrl" class="upload-placeholder">
+                    <span class="plus">+</span>
+                </div>
+                <img v-else :src="previewUrl" class="preview-img" />
+            </div>
+            <div class="tip">{{ t('find-password.tip') }}</div>
+            <button class="confirm-btn" :disabled="!file" @click="handleConfirm">{{ t('find-password.nextStep') }}</button>
+        </div>
+    </div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import { showToast } from 'vant'
+import { useI18n } from 'vue-i18n'
+import { $Api } from '@/api'
+import { useRoute } from 'vue-router'
+const { t } = useI18n()
+const route = useRoute()
+const { uniqueId, cardNo } = route.query as { uniqueId: string; cardNo: string }
+const file = ref<File | null>(null)
+const previewUrl = ref('')
+const signaturePhoto = ref('')
+const fileInput = ref<HTMLInputElement | null>(null)
+function triggerFileInput() {
+    fileInput.value?.click()
+}
+function handleFileChange(e: Event) {
+    const target = e.target as HTMLInputElement
+    if (target.files && target.files[0]) {
+        const f = target.files[0]
+        if (!['image/png', 'image/jpeg'].includes(f.type)) {
+            showToast('仅支持PNG、JPG格式')
+            return
+        }
+        file.value = f
+        previewUrl.value = URL.createObjectURL(f)
+    }
+}
+const isUploading = ref(false)
+async function handleConfirm() {
+    if (!file.value) {
+        showToast('请上传签名照片')
+        return
+    }
+    try {
+        isUploading.value = true
+        showLoadingToast({
+            message: '上传中...',
+            forbidClick: true,
+        })
+        const result = await $Api.upload.uploadFile(file.value)
+        closeToast()
+        showToast('上传成功')
+        signaturePhoto.value = result.data
+        try {
+            await $Api.ucard.ucardResetPassword({
+                signaturePhoto: result.data,
+                uniqueId,
+                cardNo,
+            })
+            showToast('重置密码成功')
+        } catch (error) {
+            showToast('重置密码失败,请重试')
+        }
+    } catch (error) {
+        closeToast()
+        showToast('上传失败,请重试')
+    } finally {
+        isUploading.value = false
+    }
+}
+</script>
+
+<style scoped lang="scss">
+.find-password-page {
+    min-height: 100vh;
+    background: linear-gradient(135deg, #232323 0%, #2a2a2a 100%);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
+.find-form {
+    width: 100%;
+    max-width: 340px;
+    margin: 0 auto;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    background: rgba(24, 24, 24, 0.92);
+    border-radius: 20px;
+    box-shadow: 0 4px 32px 0 rgba(214, 255, 0, 0.08);
+    padding: 36px 20px 32px 20px;
+}
+.upload-label {
+    color: #fff;
+    font-size: 18px;
+    font-weight: 500;
+    margin-bottom: 22px;
+    letter-spacing: 1px;
+}
+.upload-box {
+    width: 140px;
+    height: 140px;
+    background: #181818;
+    border-radius: 16px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    margin-bottom: 12px;
+    cursor: pointer;
+    border: 2px dashed var(--main-yellow, #d6ff00);
+    transition: border-color 0.2s;
+    overflow: hidden;
+    box-shadow: 0 2px 12px 0 rgba(214, 255, 0, 0.04);
+    &:hover {
+        border-color: #fff200;
+    }
+}
+.upload-placeholder {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    width: 100%;
+    height: 100%;
+}
+.plus {
+    font-size: 48px;
+    color: var(--main-yellow, #d6ff00);
+    margin-bottom: 0;
+    font-weight: bold;
+}
+.tip {
+    color: #bdbdbd;
+    font-size: 13px;
+    margin-bottom: 18px;
+    margin-top: 2px;
+    text-align: center;
+}
+.preview-img {
+    width: 100%;
+    height: 100%;
+    object-fit: cover;
+    border-radius: 16px;
+    border: 1.5px solid #d6ff00;
+    box-shadow: 0 2px 8px 0 rgba(214, 255, 0, 0.08);
+}
+.confirm-btn {
+    width: 100%;
+    height: 48px;
+    background: linear-gradient(90deg, #d6ff00 0%, #eaff7b 100%);
+    color: #232323;
+    border: none;
+    border-radius: 24px;
+    font-size: 20px;
+    font-weight: bold;
+    margin-top: 30px;
+    cursor: pointer;
+    box-shadow: 0 2px 12px 0 rgba(214, 255, 0, 0.1);
+    letter-spacing: 2px;
+    transition: background 0.2s, box-shadow 0.2s;
+}
+.confirm-btn:disabled {
+    opacity: 0.5;
+    cursor: not-allowed;
+}
+</style>

+ 104 - 0
src/views/forget-pay-password.vue

@@ -0,0 +1,104 @@
+<template>
+  <div class="page forget-pay-password-page">
+    <div class="form-box">
+      <div class="desc">{{t('forget-pay-password.desc')}}</div>
+      <div class="input-row">
+        <input class="input" v-model="code" :placeholder="t('forget-pay-password.input')" />
+        <button class="code-btn" :disabled="codeBtnDisabled" @click="getCode">{{t('forget-pay-password.getCode')}}</button>
+      </div>
+      <button class="next-btn" @click="nextStep">{{t('forget-pay-password.nextStep')}}</button>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import { useI18n } from 'vue-i18n'
+const { t } = useI18n()
+const code = ref('')
+const codeBtnDisabled = ref(false)
+function getCode() {
+  codeBtnDisabled.value = true
+  setTimeout(() => {
+    codeBtnDisabled.value = false
+  }, 60000)
+}
+function nextStep() {
+}
+</script>
+
+<style scoped lang="scss">
+.forget-pay-password-page {
+  min-height: 100vh;
+  background: linear-gradient(135deg, #232323 0%, #2a2a2a 100%);
+  padding: 0;
+  .header {
+    display: flex;
+    align-items: center;
+    padding: 24px 0 18px 12px;
+    .back-icon {
+      font-size: 22px;
+      color: var(--main-yellow, #d6ff00);
+      margin-right: 10px;
+      cursor: pointer;
+    }
+    .title {
+      font-size: 18px;
+      color: #fff;
+      font-weight: bold;
+    }
+  }
+  .form-box {
+    margin: 60px 18px 0 18px;
+    display: flex;
+    flex-direction: column;
+    gap: 32px;
+    .desc {
+      color: #fff;
+      font-size: 15px;
+      margin-bottom: 18px;
+    }
+    .input-row {
+      display: flex;
+      gap: 12px;
+      .input {
+        flex: 1;
+        height: 44px;
+        border-radius: 8px;
+        border: none;
+        padding: 0 12px;
+        font-size: 16px;
+        background: #181818;
+        color: #fff;
+      }
+      .code-btn {
+        background: var(--main-yellow, #d6ff00);
+        color: #232323;
+        border: none;
+        border-radius: 8px;
+        padding: 0 18px;
+        font-size: 15px;
+        height: 44px;
+        cursor: pointer;
+        font-weight: bold;
+      }
+      .code-btn:disabled {
+        opacity: 0.5;
+        cursor: not-allowed;
+      }
+    }
+    .next-btn {
+      width: 100%;
+      height: 48px;
+      background: var(--main-yellow, #d6ff00);
+      color: #232323;
+      border: none;
+      border-radius: 24px;
+      font-size: 18px;
+      font-weight: bold;
+      margin-top: 24px;
+      cursor: pointer;
+    }
+  }
+}
+</style>

+ 145 - 0
src/views/freeze-card.vue

@@ -0,0 +1,145 @@
+<template>
+    <div class="page freeze-card-page">
+        <div class="freeze-form">
+            <div class="upload-label">{{ t('freeze-card.desc') }}</div>
+            <div class="upload-box" @click="triggerFileInput">
+                <input ref="fileInput" type="file" accept="image/png, image/jpeg" style="display: none" @change="handleFileChange" />
+                <div v-if="!previewUrl" class="upload-placeholder">
+                    <span class="plus">+</span>
+                </div>
+                <img v-else :src="previewUrl" class="preview-img" />
+            </div>
+            <div class="tip">{{ t('freeze-card.tip') }}</div>
+            <button class="confirm-btn" :disabled="!file" @click="handleConfirm">{{ t('freeze-card.nextStep') }}</button>
+        </div>
+    </div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import { showToast } from 'vant'
+import { useI18n } from 'vue-i18n'
+const { t } = useI18n()
+const file = ref<File | null>(null)
+const previewUrl = ref('')
+const fileInput = ref<HTMLInputElement | null>(null)
+function triggerFileInput() {
+    fileInput.value?.click()
+}
+function handleFileChange(e: Event) {
+    const target = e.target as HTMLInputElement
+    if (target.files && target.files[0]) {
+        const f = target.files[0]
+        if (!['image/png', 'image/jpeg'].includes(f.type)) {
+            showToast(t('freeze-card.formatError'))
+            return
+        }
+        file.value = f
+        previewUrl.value = URL.createObjectURL(f)
+    }
+}
+
+function handleConfirm() {
+    if (!file.value) {
+        showToast(t('freeze-card.needUpload'))
+        return
+    }
+    // 这里可以添加上传逻辑
+    showToast(t('freeze-card.submitted'))
+}
+</script>
+
+<style scoped lang="scss">
+.freeze-card-page {
+    min-height: 100vh;
+    background: linear-gradient(135deg, #232323 0%, #2a2a2a 100%);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
+.freeze-form {
+    width: 100%;
+    max-width: 340px;
+    margin: 0 auto;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    background: rgba(24, 24, 24, 0.92);
+    border-radius: 20px;
+    box-shadow: 0 4px 32px 0 rgba(214, 255, 0, 0.08);
+    padding: 36px 20px 32px 20px;
+}
+.upload-label {
+    color: #fff;
+    font-size: 18px;
+    font-weight: 500;
+    margin-bottom: 22px;
+    letter-spacing: 1px;
+}
+.upload-box {
+    width: 140px;
+    height: 140px;
+    background: #181818;
+    border-radius: 16px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    margin-bottom: 12px;
+    cursor: pointer;
+    border: 2px dashed var(--main-yellow, #d6ff00);
+    transition: border-color 0.2s;
+    overflow: hidden;
+    box-shadow: 0 2px 12px 0 rgba(214, 255, 0, 0.04);
+    &:hover {
+        border-color: #fff200;
+    }
+}
+.upload-placeholder {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    width: 100%;
+    height: 100%;
+}
+.plus {
+    font-size: 48px;
+    color: var(--main-yellow, #d6ff00);
+    margin-bottom: 0;
+    font-weight: bold;
+}
+.tip {
+    color: #bdbdbd;
+    font-size: 13px;
+    margin-bottom: 18px;
+    margin-top: 2px;
+    text-align: center;
+}
+.preview-img {
+    width: 100%;
+    height: 100%;
+    object-fit: cover;
+    border-radius: 16px;
+    border: 1.5px solid #d6ff00;
+    box-shadow: 0 2px 8px 0 rgba(214, 255, 0, 0.08);
+}
+.confirm-btn {
+    width: 100%;
+    height: 48px;
+    background: linear-gradient(90deg, #d6ff00 0%, #eaff7b 100%);
+    color: #232323;
+    border: none;
+    border-radius: 24px;
+    font-size: 20px;
+    font-weight: bold;
+    margin-top: 30px;
+    cursor: pointer;
+    box-shadow: 0 2px 12px 0 rgba(214, 255, 0, 0.1);
+    letter-spacing: 2px;
+    transition: background 0.2s, box-shadow 0.2s;
+}
+.confirm-btn:disabled {
+    opacity: 0.5;
+    cursor: not-allowed;
+}
+</style>

+ 190 - 0
src/views/home.vue

@@ -0,0 +1,190 @@
+<template>
+    <div class="page">
+        <div class="wallet-banner">
+            <div class="banner-content">
+                <div class="banner-title">Join the PayouCard Global Crypto Card Agent Program</div>
+                <div class="banner-desc">Unlock High Earnings, Become a Top Crypto Agent!</div>
+            </div>
+            <div class="banner-cards">
+                <div class="card-black"></div>
+                <div class="card-yellow"></div>
+            </div>
+        </div>
+        <div class="wallet-balance-card">
+            <div class="balance-row">
+                <img src="https://upload.wikimedia.org/wikipedia/commons/b/b7/Flag_of_Europe.svg" class="flag" />
+                <span class="currency">EUR</span>
+                <span class="amount">0</span>
+                <span class="unit">EUR</span>
+                <button class="quick-btn">全球速汇</button>
+            </div>
+        </div>
+        <div class="wallet-balance-card">
+            <div class="balance-row">
+                <img src="https://cryptologos.cc/logos/tether-usdt-logo.png" class="flag" />
+                <span class="currency">USDT</span>
+                <span class="amount">0.0088</span>
+                <span class="unit">USDT</span>
+            </div>
+            <div class="wallet-actions">
+                <button class="action-btn">提现</button>
+                <button class="action-btn">充值</button>
+                <button class="action-btn">购买</button>
+                <button class="action-btn">卖出</button>
+            </div>
+        </div>
+        <div class="currency-list">
+            <div class="currency-title">币种</div>
+            <div class="currency-item">
+                <img src="https://upload.wikimedia.org/wikipedia/commons/b/b7/Flag_of_Europe.svg" class="flag" />
+                <span>EUR</span>
+                <span class="currency-amount">0</span>
+            </div>
+            <div class="currency-item">
+                <img src="https://cryptologos.cc/logos/tether-usdt-logo.png" class="flag" />
+                <span>USDT</span>
+                <span class="currency-amount">0.0088</span>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script setup lang="ts"></script>
+
+<style scoped lang="scss">
+.wallet-banner {
+    background: linear-gradient(135deg, #232323 0%, #2a2a2a 100%);
+    border-radius: 16px;
+    margin-bottom: 20px;
+    padding: 18px 20px 18px 20px;
+    color: #fff;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    .banner-content {
+        flex: 1;
+        .banner-title {
+            font-size: 16px;
+            font-weight: bold;
+            color: #d6ff00;
+            margin-bottom: 6px;
+        }
+        .banner-desc {
+            font-size: 13px;
+            color: #fff;
+            opacity: 0.8;
+        }
+    }
+    .banner-cards {
+        display: flex;
+        align-items: center;
+        gap: 8px;
+        margin-left: 12px;
+        .card-black,
+        .card-yellow {
+            width: 48px;
+            height: 68px;
+            border-radius: 8px;
+            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+        }
+        .card-black {
+            background: #111;
+        }
+        .card-yellow {
+            background: #d6ff00;
+        }
+    }
+}
+.wallet-balance-card {
+    background: var(--action-bg);
+    border-radius: 16px;
+    margin-bottom: 16px;
+    padding: 16px 12px;
+    .balance-row {
+        display: flex;
+        align-items: center;
+        gap: 8px;
+        .flag {
+            width: 24px;
+            height: 24px;
+            border-radius: 50%;
+        }
+        .currency {
+            font-size: 16px;
+            color: var(--main-yellow);
+            margin-right: 4px;
+        }
+        .amount {
+            font-size: 18px;
+            font-weight: bold;
+            color: var(--white);
+            margin: 0 4px;
+        }
+        .unit {
+            font-size: 14px;
+            color: var(--main-yellow);
+            margin-right: 8px;
+        }
+        .quick-btn {
+            background: #d6ff00;
+            color: #232323;
+            border: none;
+            border-radius: 8px;
+            padding: 4px 12px;
+            font-size: 13px;
+            margin-left: auto;
+            cursor: pointer;
+        }
+    }
+    .wallet-actions {
+        display: flex;
+        justify-content: space-between;
+        margin-top: 10px;
+        .action-btn {
+            background: var(--main-bg);
+            color: var(--main-yellow);
+            border: 1px solid var(--main-yellow);
+            border-radius: 8px;
+            padding: 6px 14px;
+            font-size: 14px;
+            cursor: pointer;
+            transition: all 0.2s;
+            &:hover {
+                background: #d6ff00;
+                color: #232323;
+            }
+        }
+    }
+}
+.currency-list {
+    margin-bottom: 16px;
+    .currency-title {
+        font-size: 15px;
+        color: var(--main-yellow);
+        margin-bottom: 8px;
+    }
+    .currency-item {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        background: var(--action-bg);
+        border-radius: 10px;
+        padding: 10px 12px;
+        margin-bottom: 8px;
+        .flag {
+            width: 22px;
+            height: 22px;
+            border-radius: 50%;
+            margin-right: 8px;
+        }
+        span {
+            font-size: 15px;
+            color: var(--white);
+        }
+        .currency-amount {
+            color: var(--main-yellow);
+            font-weight: bold;
+        }
+    }
+}
+</style>

+ 53 - 0
src/views/language.vue

@@ -0,0 +1,53 @@
+<template>
+    <div class="page">
+        <div class="title">{{ t('language.selectLang') }}</div>
+        <div v-for="item in localesList" :key="item" class="lang-item" :class="{ active: currentLang === item }" @click="changeLang(item)">
+            {{ t(`language.${item}`) }}
+            <span v-if="currentLang === item" class="check">✔</span>
+        </div>
+    </div>
+</template>
+
+<script setup lang="ts">
+import { useI18n } from 'vue-i18n'
+import { ref } from 'vue'
+import { localesList } from '@/plugin/i18n'
+import { lang } from '@/composables/config'
+const { locale, t } = useI18n()
+const currentLang = ref(lang.value || locale.value)
+function changeLang(langValue: string) {
+    locale.value = langValue
+    lang.value = langValue
+    currentLang.value = langValue
+}
+</script>
+
+<style scoped lang="scss">
+.title {
+    font-size: var(--font-size-22);
+    font-weight: bold;
+    margin-bottom: 24px;
+}
+.lang-item {
+    background: var(--main-bg);
+    border-radius: 14px;
+    padding: 18px 18px;
+    font-size: var(--font-size-18);
+    margin-bottom: 16px;
+    color: var(--main-yellow);
+    display: flex;
+    align-items: center;
+    cursor: pointer;
+    transition: background 0.2s;
+}
+.lang-item.active {
+    background: var(--main-yellow);
+    color: var(--main-bg);
+    font-weight: bold;
+}
+.check {
+    margin-left: auto;
+    color: var(--main-bg);
+    font-size: var(--font-size-18);
+}
+</style>

+ 294 - 0
src/views/login.vue

@@ -0,0 +1,294 @@
+<template>
+    <div class="page">
+        <div class="login-container">
+            <div class="login-header">
+                <h2>欢迎登录</h2>
+                <p>请使用您的账号密码登录</p>
+            </div>
+
+            <div class="login-form">
+                <div class="form-item">
+                    <van-field v-model="loginName" placeholder="请输入账号" :rules="[{ required: true, message: '请输入账号' }]">
+                        <template #left-icon>
+                            <van-icon name="user-o" />
+                        </template>
+                    </van-field>
+                </div>
+
+                <div class="form-item">
+                    <van-field v-model="password" type="password" placeholder="请输入密码" :rules="[{ required: true, message: '请输入密码' }]">
+                        <template #left-icon>
+                            <van-icon name="lock" />
+                        </template>
+                    </van-field>
+                </div>
+
+                <div class="form-options">
+                    <van-checkbox v-model="rememberPassword" shape="square" icon-size="16px"> 记住密码 </van-checkbox>
+                    <span class="forgot-password" @click="handleForgotPassword">忘记密码?</span>
+                </div>
+
+                <div class="login-button">
+                    <van-button type="primary" block :loading="loading" @click="handleLogin"> 登录 </van-button>
+                </div>
+
+                <!-- <div class="register-link">还没有账号?<span @click="handleRegister">立即注册</span></div>
+                <div class="third-party-login">
+                    <div class="divider">
+                        <span>其他登录方式</span>
+                    </div>
+                    <div class="third-party-buttons">
+                        <van-button icon="wechat" circle @click="handleWechatLogin" />
+                        <van-button icon="google" circle @click="handleGoogleLogin" />
+                    </div>
+                </div> -->
+            </div>
+        </div>
+    </div>
+</template>
+
+<script setup lang="ts">
+defineOptions({ name: 'LoginRouter' })
+import { ref, onMounted } from 'vue'
+import { showToast, showLoadingToast } from 'vant'
+import { useRouter } from 'vue-router'
+import { userApi } from '@/api/user'
+import useUserStore from '@/stores/use-user-store'
+import ls from 'store2'
+import { userToken } from '@/composables/config'
+const router = useRouter()
+const userStore = useUserStore()
+const loginName = ref('admin1')
+const password = ref('Cwg123456')
+const rememberPassword = ref(false)
+const loading = ref(false)
+onMounted(() => {
+    const savedCredentials = ls.get('savedCredentials')
+    if (savedCredentials) {
+        loginName.value = savedCredentials.loginName
+        password.value = savedCredentials.password
+        rememberPassword.value = true
+    }
+})
+
+const handleLogin = async () => {
+    if (!loginName.value || !password.value) {
+        showToast('请输入账号和密码')
+        return
+    }
+    loading.value = true
+    const loadingToast = showLoadingToast({
+        message: '登录中...',
+        forbidClick: true,
+    })
+    try {
+        const res = await userApi.login({
+            loginName: loginName.value,
+            password: password.value,
+            emailCode: '12',
+        })
+        userToken.value = res.data
+        await new Promise((resolve) => setTimeout(resolve, 100))
+        if (rememberPassword.value) {
+            ls.set('savedCredentials', {
+                loginName: loginName.value,
+                password: password.value,
+            })
+        } else {
+            ls.remove('savedCredentials')
+        }
+        showToast('登录成功')
+        getUserInfo()
+    } catch (error: any) {
+        showToast(error.message || '登录失败')
+    } finally {
+        loading.value = false
+        loadingToast.close()
+    }
+}
+const getUserInfo = async () => {
+    if (!userToken.value) {
+        showToast('请先登录')
+        return
+    }
+    try {
+        const res = await userApi.getUserInfo()
+        userStore.saveUserInfo(res.data)
+        router.push('/')
+    } catch (error: any) {
+        showToast(error.message || '登录失败')
+    } finally {
+        loading.value = false
+    }
+}
+
+const handleForgotPassword = () => {
+    router.push('/reset/password')
+}
+</script>
+
+<style scoped lang="scss">
+::v-deep {
+    .login-container {
+        padding: 40px 0;
+    }
+
+    .login-header {
+        text-align: center;
+        margin-bottom: 40px;
+
+        h2 {
+            font-size: var(--font-size-24);
+            color: var(--main-yellow);
+            margin-bottom: 8px;
+        }
+
+        p {
+            font-size: var(--font-size-14);
+            color: var(--gray);
+        }
+    }
+
+    .login-form {
+        .form-item {
+            margin-bottom: 20px;
+
+            .van-field {
+                background: var(--action-bg);
+                border-radius: 12px;
+                padding: 12px 16px;
+
+                .van-field__left-icon {
+                    margin-right: 12px;
+                    color: var(--main-yellow);
+                }
+
+                .van-field__control {
+                    color: var(--white);
+
+                    &::placeholder {
+                        color: var(--gray);
+                    }
+                }
+            }
+        }
+    }
+
+    .form-options {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        margin-bottom: 30px;
+
+        .van-checkbox {
+            .van-checkbox__label {
+                color: var(--gray);
+                font-size: var(--font-size-14);
+            }
+
+            .van-checkbox__icon {
+                border-color: var(--main-yellow);
+
+                &--checked {
+                    background-color: var(--main-yellow);
+                    border-color: var(--main-yellow);
+                }
+            }
+        }
+
+        .forgot-password {
+            color: var(--main-yellow);
+            font-size: var(--font-size-14);
+        }
+        .van-checkbox__label {
+            color: var(--main-yellow);
+            font-size: var(--font-size-14);
+        }
+    }
+
+    .login-button {
+        margin-bottom: 20px;
+
+        .van-button {
+            height: 44px;
+            border-radius: 12px;
+            background: var(--main-yellow);
+            border: none;
+            .van-button__text {
+                color: var(--black);
+                font-size: var(--font-size-16);
+                font-weight: bold;
+            }
+
+            &--loading {
+                opacity: 0.8;
+            }
+        }
+    }
+
+    .register-link {
+        text-align: center;
+        font-size: var(--font-size-14);
+        color: var(--gray);
+        margin-bottom: 40px;
+
+        span {
+            color: var(--main-yellow);
+            margin-left: 4px;
+        }
+    }
+
+    .third-party-login {
+        .divider {
+            position: relative;
+            text-align: center;
+            margin: 20px 0;
+
+            &::before,
+            &::after {
+                content: '';
+                position: absolute;
+                top: 50%;
+                width: 30%;
+                height: 1px;
+                background: var(--border);
+            }
+
+            &::before {
+                left: 0;
+            }
+
+            &::after {
+                right: 0;
+            }
+
+            span {
+                display: inline-block;
+                padding: 0 10px;
+                color: var(--gray);
+                font-size: var(--font-size-14);
+                background: var(--main-bg);
+            }
+        }
+
+        .third-party-buttons {
+            display: flex;
+            justify-content: center;
+            gap: 20px;
+
+            .van-button {
+                width: 44px;
+                height: 44px;
+                border-radius: 50%;
+                background: var(--action-bg);
+                border: 1px solid var(--border);
+                color: var(--white);
+
+                .van-icon {
+                    font-size: var(--font-size-20);
+                }
+            }
+        }
+    }
+}
+</style>

+ 105 - 0
src/views/mine.vue

@@ -0,0 +1,105 @@
+<template>
+    <div class="page">
+        <div class="user-card">
+            <div class="avatar"></div>
+            <div class="user-info">
+                <div class="hello">{{ userInfo?.name }}</div>
+                <div class="phone">{{ userInfo?.email }}</div>
+            </div>
+        </div>
+        <div class="group">
+            <div class="group-item" @click="$router.push('/language')">
+                <van-icon name="setting-o" />
+                <span>{{ t('language.i1') }}</span>
+            </div>
+            <div class="group-item">
+                <van-icon name="notes-o" />
+                <span>{{ t('language.i4') }}</span>
+            </div>
+        </div>
+        <div class="group">
+            <div class="group-item" @click="$router.push('/pay/password')">
+                <van-icon name="lock" />
+                <span>{{ t('language.i2') }}</span>
+            </div>
+            <div class="group-item">
+                <van-icon name="shield-o" />
+                <span>{{ t('language.i3') }}</span>
+            </div>
+        </div>
+        <div class="group">
+            <div class="group-item">
+                <van-icon name="info-o" />
+                <span>{{ t('language.i5') }}</span>
+                <span class="version">v2.0.21</span>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script setup lang="ts">
+import { useI18n } from 'vue-i18n'
+const { t } = useI18n()
+import useUserStore from '@/stores/use-user-store'
+const userStore = useUserStore()
+const userInfo = userStore.userInfo
+</script>
+
+<style scoped>
+.user-card {
+    background: var(--main-yellow);
+    margin-top: 24px;
+    border-radius: 16px;
+    display: flex;
+    align-items: center;
+    padding: 24px 20px;
+    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
+}
+.avatar {
+    width: 64px;
+    height: 64px;
+    border-radius: 50%;
+    background: #d8d8d8 url('https://img.icons8.com/color/96/000000/user-male-circle--v2.png') center/cover no-repeat;
+    margin-right: 18px;
+    border: 2px solid #fff;
+}
+.user-info .hello {
+    color: #333;
+    font-size: var(--font-size-16);
+    margin-bottom: 6px;
+}
+.user-info .phone {
+    color: #333;
+    font-size: var(--font-size-18);
+    font-weight: bold;
+}
+.group {
+    background: var(--action-bg);
+    border-radius: 16px;
+    margin-top: 12px;
+    overflow: hidden;
+    border: 1px solid rgba(214, 255, 0, 0.2);
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+}
+.group-item {
+    display: flex;
+    align-items: center;
+    padding: 18px 18px;
+    color: var(--white);
+    font-size: var(--font-size-15);
+    border-bottom: 1px solid var(--action-bg);
+    position: relative;
+}
+.group-item:last-child {
+    border-bottom: none;
+}
+.van-icon{
+    color: var(--main-yellow);
+    margin-right: 12px;
+}
+.group-item .version {
+    margin-left: auto;
+    color: var(--main-yellow);
+    font-size: 15px;
+}
+</style>

+ 80 - 0
src/views/pay-password.vue

@@ -0,0 +1,80 @@
+<template>
+    <div class="page pay-password-page">
+      <div class="option-list">
+        <div class="option-item" @click="goTo('change')">
+          <span>{{ t('pay-password.change') }}</span>
+          <div class="arrow-btn"><van-icon name="arrow" /></div>
+        </div>
+        <div class="option-item" @click="goTo('forget')">
+          <span>{{ t('pay-password.forget') }}</span>
+          <div class="arrow-btn"><van-icon name="arrow" /></div>
+        </div>
+      </div>
+    </div>
+  </template>
+
+  <script setup lang="ts">
+  import { useI18n } from 'vue-i18n'
+  import { useRouter } from 'vue-router'
+  const { t } = useI18n()
+  const router = useRouter()
+
+  function goTo(type: string) {
+    if (type === 'change') {
+      router.push('/change/pay/password')
+    } else if (type === 'forget') {
+      router.push('/forget/pay/password')
+    }
+  }
+  </script>
+
+  <style scoped lang="scss">
+  .pay-password-page {
+    min-height: 100vh;
+    background: linear-gradient(135deg, #232323 0%, #2a2a2a 100%);
+    padding: 0;
+  }
+  .header {
+    display: flex;
+    align-items: center;
+    padding: 24px 0 18px 12px;
+    .back-icon {
+      font-size: 22px;
+      color: var(--main-yellow, #d6ff00);
+      margin-right: 10px;
+      cursor: pointer;
+    }
+    .title {
+      font-size: 18px;
+      color: #fff;
+      font-weight: bold;
+    }
+  }
+  .option-list {
+    margin-top: 32px;
+    display: flex;
+    flex-direction: column;
+    gap: 18px;
+    padding: 0 18px;
+  }
+  .option-item {
+    background: var(--action-bg, #181818);
+    border-radius: 12px;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 18px 18px;
+    color: #fff;
+    font-size: 16px;
+    box-shadow: 0 2px 8px rgba(214, 255, 0, 0.04);
+  }
+  .arrow-btn {
+    width: 32px;
+    height: 32px;
+    background: rgba(255,255,255,0.06);
+    border-radius: 8px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+  </style>

+ 233 - 0
src/views/reset-password.vue

@@ -0,0 +1,233 @@
+<template>
+    <div class="page">
+        <div class="reset-container">
+            <div class="reset-header">
+                <h2>重置密码</h2>
+                <p>请输入您的邮箱和验证码</p>
+            </div>
+
+            <div class="reset-form">
+                <div class="form-item">
+                    <van-field
+                        v-model="form.email"
+                        placeholder="请输入邮箱"
+                        :rules="[
+                            { required: true, message: '请输入邮箱' },
+                            { pattern: config.Pattern.Email, message: '请输入正确的邮箱格式' },
+                        ]"
+                    >
+                        <template #left-icon>
+                            <van-icon name="envelop-o" />
+                        </template>
+                    </van-field>
+                </div>
+
+                <div class="form-item">
+                    <van-field v-model="form.code" placeholder="请输入验证码" :rules="[{ required: true, message: '请输入验证码' }]">
+                        <template #left-icon>
+                            <van-icon name="shield-o" />
+                        </template>
+                        <template #button>
+                            <van-button size="small" type="primary" :disabled="!!countdown" @click="handleSendCode">
+                                {{ countdown ? `${countdown}s后重试` : '获取验证码' }}
+                            </van-button>
+                        </template>
+                    </van-field>
+                </div>
+
+                <div class="form-item">
+                    <van-field
+                        v-model="form.newPassword"
+                        type="password"
+                        placeholder="请输入新密码"
+                        :rules="[
+                            { required: true, message: '请输入新密码' },
+                            { pattern: config.Pattern.Password, message: '密码必须包含大小写字母和数字,长度8-16位' },
+                        ]"
+                    >
+                        <template #left-icon>
+                            <van-icon name="lock" />
+                        </template>
+                    </van-field>
+                </div>
+
+                <div class="reset-button">
+                    <van-button type="primary" block :loading="loading" @click="handleReset"> 重置密码 </van-button>
+                </div>
+
+                <div class="login-link">记起密码了?<span @click="handleLogin">返回登录</span></div>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script setup lang="ts">
+defineOptions({
+    name: 'ResetPasswordRouter',
+})
+
+import { ref } from 'vue'
+import { showToast, showLoadingToast } from 'vant'
+import { useRouter } from 'vue-router'
+import { userApi } from '@/api/user'
+import config from '@/config'
+
+const router = useRouter()
+const loading = ref(false)
+const countdown = ref(0)
+
+const form = ref({
+    email: '',
+    code: '',
+    newPassword: '',
+})
+
+const startCountdown = () => {
+    countdown.value = 60
+    const timer = setInterval(() => {
+        countdown.value--
+        if (countdown.value <= 0) {
+            clearInterval(timer)
+        }
+    }, 1000)
+}
+
+const handleSendCode = async () => {
+    if (!form.value.email) {
+        showToast('请输入邮箱')
+        return
+    }
+
+    if (!config.Pattern.Email.test(form.value.email)) {
+        showToast('请输入正确的邮箱格式')
+        return
+    }
+
+    try {
+        await userApi.sendVerifyCode(form.value.email)
+        showToast('验证码已发送')
+        startCountdown()
+    } catch (error: any) {
+        showToast(error.message || '发送失败')
+    }
+}
+
+const handleReset = async () => {
+    if (!form.value.email || !form.value.code || !form.value.newPassword) {
+        showToast('请填写完整信息')
+        return
+    }
+
+    loading.value = true
+    const loadingToast = showLoadingToast({
+        message: '重置中...',
+        forbidClick: true,
+    })
+    try {
+        await userApi.resetPassword(form.value)
+        showToast('密码重置成功')
+        router.push('/login')
+    } catch (error: any) {
+        showToast(error.message || '重置失败')
+    } finally {
+        loading.value = false
+        loadingToast.close()
+    }
+}
+
+const handleLogin = () => {
+    router.push('/login')
+}
+</script>
+
+<style scoped lang="scss">
+.reset-container {
+    padding: 40px 20px;
+}
+
+.reset-header {
+    text-align: center;
+    margin-bottom: 40px;
+
+    h2 {
+        font-size: var(--font-size-24);
+        color: var(--main-yellow);
+        margin-bottom: 8px;
+    }
+
+    p {
+        font-size: var(--font-size-14);
+        color: var(--gray);
+    }
+}
+
+.reset-form {
+    .form-item {
+        margin-bottom: 20px;
+
+        .van-field {
+            background: var(--action-bg);
+            border-radius: 12px;
+            padding: 12px 16px;
+
+            :deep(.van-field__left-icon) {
+                margin-right: 12px;
+                color: var(--main-yellow);
+            }
+
+            :deep(.van-field__control) {
+                color: var(--white);
+
+                &::placeholder {
+                    color: var(--gray);
+                }
+            }
+
+            .van-field__button {
+                .van-button {
+                    height: 32px;
+                    padding: 0 12px;
+                    background: var(--main-yellow);
+                    border: none;
+                    color: var(--black);
+                    font-size: var(--font-size-14);
+                    border-radius: 8px;
+
+                    :deep(&--disabled) {
+                        opacity: 0.5;
+                    }
+                }
+            }
+        }
+    }
+}
+
+.reset-button {
+    margin-bottom: 20px;
+
+    .van-button {
+        height: 44px;
+        border-radius: 12px;
+        background: var(--main-yellow);
+        border: none;
+        color: var(--black);
+        font-size: var(--font-size-16);
+        font-weight: bold;
+
+        :deep(&--loading) {
+            opacity: 0.8;
+        }
+    }
+}
+
+.login-link {
+    text-align: center;
+    font-size: var(--font-size-14);
+    color: var(--gray);
+
+    span {
+        color: var(--main-yellow);
+        margin-left: 4px;
+    }
+}
+</style>

+ 35 - 0
tsconfig.json

@@ -0,0 +1,35 @@
+{
+  "compilerOptions": {
+    "target": "ESNext",
+    "jsx": "preserve",
+    "jsxImportSource": "vue",
+    "lib": ["DOM", "ESNext"],
+    "baseUrl": ".",
+    "module": "ESNext",
+    "moduleResolution": "node",
+    "paths": {
+      "~/*": ["src/*"],
+      "@/*": ["src/*"]
+    },
+    "resolveJsonModule": true,
+    "types": [],
+    "allowJs": true,
+    "strict": true,
+    "strictNullChecks": true,
+    "noUnusedLocals": true,
+    "importHelpers": true,
+    "outDir": "dist",
+    "esModuleInterop": true,
+    "forceConsistentCasingInFileNames": true,
+    "skipLibCheck": true
+  },
+  "exclude": ["node_modules", "dist/**/*", "public/**/*"],
+
+  "vueCompilerOptions": {
+    "target": 3, // or 2.7 for Vue 2
+    "plugins": [
+      "@vue-macros/volar/define-props-refs",
+      "@vue-macros/volar/define-props"
+    ]
+  }
+}

Dosya farkı çok büyük olduğundan ihmal edildi
+ 1 - 0
uno.css


+ 7 - 0
unocss.config.ts

@@ -0,0 +1,7 @@
+import { h5Config } from '@lincy/unocss-base-config'
+import { fontSize } from './src/design.config'
+
+export default h5Config({
+    baseFontSize: fontSize,
+    unti: 'rem',
+})

+ 52 - 0
vite.config.build.ts

@@ -0,0 +1,52 @@
+import type { BuildOptions, ServerOptions } from 'vite'
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
+
+const config: { server: ServerOptions, build: BuildOptions } = {
+    build: {
+        target: 'es2018',
+        cssTarget: 'chrome79',
+        minify: true,
+        assetsInlineLimit: 4096,
+        chunkSizeWarningLimit: 1000,
+        outDir: 'dist',
+        rollupOptions: {
+            input: {
+                main: path.resolve(__dirname, 'index.html'),
+            },
+            output: {
+                manualChunks(id: string) {
+                    // 处理css分块
+                    if (id.includes('node_modules')) {
+                        return 'vendor'
+                    }
+                    if (id.includes('__uno.css')) {
+                        return 'unocss'
+                    }
+                },
+            },
+            external: /\.\/static.*/,
+        },
+    },
+    server: {
+        port: 7771,
+        proxy: {
+            '/api': {
+                target: 'https://php.mmxiaowu.com',
+                changeOrigin: true,
+                rewrite: (path: string) => path.replace(/^\/api/, '/api'),
+            },
+        },
+        /**
+         * 预热常用文件
+         * @see https://cn.vitejs.dev/guide/performance#warm-up-frequently-used-files
+         */
+        warmup: {
+            clientFiles: ['./src/main.ts', './src/views/**/*.vue'],
+        },
+    },
+}
+
+export default config

+ 64 - 0
vite.config.components.ts

@@ -0,0 +1,64 @@
+import AutoImport from 'unplugin-auto-import/vite'
+import Icons from 'unplugin-icons/vite'
+import { VantResolver } from 'unplugin-vue-components/resolvers'
+import Components from 'unplugin-vue-components/vite'
+
+export default () => ([
+    /**
+     * 按需自动导入API
+     * @see https://github.com/antfu/unplugin-auto-import#readme
+     */
+    AutoImport({
+        eslintrc: {
+            enabled: true,
+        },
+        include: [
+            /\.[tj]sx?$/, // .ts, .tsx, .js, .jsx
+            /\.vue$/,
+            /\.vue\?vue/, // .vue
+            /\.md$/, // .md
+        ],
+        imports: [
+            'vue',
+            'vue-router',
+            '@vueuse/core',
+            {
+                'vant': ['closeToast', 'showConfirmDialog', 'showDialog', 'showFailToast', 'showLoadingToast', 'showSuccessToast', 'showToast', 'showImagePreview'],
+                'pinia': ['defineStore', 'storeToRefs'],
+                'vue-router': ['createRouter', 'createWebHashHistory'],
+                '@unhead/vue': ['useHead'],
+                '@lincy/utils': ['deepClone', 'deepMerge', 'UTC2Date'],
+            },
+        ],
+        dts: 'src/auto-imports.d.ts',
+        dirs: ['src/components', 'src/**/components', 'src/composables', 'src/stores'],
+
+        resolvers: [VantResolver()],
+        defaultExportByFilename: false,
+        vueTemplate: true,
+    }),
+    /**
+     * 按需自动导入Vue组件
+     * @see https://github.com/antfu/unplugin-vue-components#readme
+     */
+    Components({
+        dirs: ['src/components', 'src/**/components'],
+        include: [
+            /\.[tj]sx?$/, // .ts, .tsx, .js, .jsx
+            /\.vue$/,
+            /\.vue\?vue/, // .vue
+            /\.md$/, // .md
+        ],
+        extensions: ['vue', 'tsx', 'jsx'],
+        resolvers: [VantResolver()],
+        dts: 'src/components.d.ts',
+    }),
+    /**
+     * 按需访问数千个图标作为组件
+     * @see https://github.com/antfu/unplugin-icons#readme
+     * @example <i-mdi-account-box style="font-size: 2em; color: red"/>
+     */
+    Icons({
+        autoInstall: true,
+    }),
+])

+ 56 - 0
vite.config.css.ts

@@ -0,0 +1,56 @@
+import type { CSSOptions } from 'vite'
+import viewport from 'postcss-px-to-viewport-8-plugin'
+import { charsetRemoval, designHeight, designMultiple, designWidth, fontSize, maxWidth, maxWindow, minWidth, minWindow } from './src/design.config'
+
+const config: CSSOptions = {
+    preprocessorOptions: {
+        scss: {
+            additionalData: `
+                $vmDesignWidth: ${designWidth};
+                $vmDesignHeight: ${designHeight};
+                $vmDesignMultiple: ${designMultiple};
+                $vmMinWidth: ${minWidth};
+                $vmMinWindow: ${minWindow};
+                $vmMaxWidth: ${maxWidth};
+                $vmMaxWindow: ${maxWindow};
+                $vmFontSize: ${fontSize};
+            `,
+            api: 'modern-compiler',
+            // 忽略scss global-builtin, import 提示3.0将删除的警告
+            silenceDeprecations: ['global-builtin', 'import'],
+        },
+    },
+    postcss: {
+        plugins: [
+            /**
+             * 将 px 单位转换为视图单位(vw, vh, vmin, vmax)的 PostCSS 插件
+             * @see https://github.com/lkxian888/postcss-px-to-viewport-8-plugin#readme
+             */
+            viewport({
+                unitToConvert: 'px', // 要转化的单位
+                viewportWidth: (file: string) => {
+                    // 字号 * 100
+                    let viewportWidth = fontSize * 100
+                    if (file.includes('vant')) {
+                        viewportWidth = fontSize * (375 / designWidth * 100)
+                    }
+
+                    return viewportWidth
+                }, // UI设计稿的宽度
+                unitPrecision: 6, // 转换后的精度,即小数点位数
+                propList: ['*'], // 指定可以转换的css属性,*代表全部css属性
+                viewportUnit: 'rem', // 指定需要转换成的视窗单位,默认vw
+                fontViewportUnit: 'rem', // 指定字体需要转换成的视窗单位,默认vw
+                selectorBlackList: ['svg-text'], // 指定不转换为视窗单位的类名
+                minPixelValue: 1, // 默认值1,小于或等于1px则不进行转换
+                mediaQuery: true, // 是否在媒体查询的css代码中也进行转换,默认false
+                replace: true, // 是否转换后直接更换属性值
+                // exclude: [/node_modules/], // 设置忽略文件,用正则做目录名匹配
+                landscape: false, // 是否处理横屏情况
+            }),
+            charsetRemoval(),
+        ],
+    },
+}
+
+export default config

+ 37 - 0
vite.config.macros.ts

@@ -0,0 +1,37 @@
+import type { PluginOption } from 'vite'
+import vuePlugin from '@vitejs/plugin-vue'
+
+import vueJsx from '@vitejs/plugin-vue-jsx'
+import DefinePropsRefs from '@vue-macros/define-props-refs/vite'
+import DefineProps from '@vue-macros/define-props/vite'
+
+import ReactivityTransform from '@vue-macros/reactivity-transform/vite'
+
+export default (): PluginOption[] => ([
+    vuePlugin({
+        template: {
+            compilerOptions: {
+                isCustomElement: (tag: string) => ['def'].includes(tag),
+            },
+        },
+    }),
+    vueJsx(),
+    /**
+     * Reactivity Transform
+     * @description 响应性语法糖
+     * @see https://vue-macros.sxzz.moe/zh-CN/features/reactivity-transform.html
+     */
+    ReactivityTransform(),
+    /**
+     * defineProps
+     * @description 使用 $defineProps 可以正确地解构 props 的类型
+     * @see https://vue-macros.sxzz.moe/zh-CN/macros/define-props.html
+     */
+    DefineProps(),
+    /**
+     * definePropsRefs
+     * @description 从 defineProps 中将返回 refs 而不是 reactive 对象,可以在不丢失响应式的情况下解构 props
+     * @see https://vue-macros.sxzz.moe/zh-CN/macros/define-props-refs.html
+     */
+    DefinePropsRefs(),
+])

+ 59 - 0
vite.config.ts

@@ -0,0 +1,59 @@
+import type { ConfigEnv } from 'vite'
+import path from 'node:path'
+import process from 'node:process'
+
+import { fileURLToPath } from 'node:url'
+import UnoCSS from 'unocss/vite'
+import { defineConfig, loadEnv } from 'vite'
+import Inspect from 'vite-plugin-inspect'
+import { viteMockServe } from 'vite-plugin-mock'
+import Progress from 'vite-plugin-progress'
+
+import Build from './vite.config.build'
+import Components from './vite.config.components'
+import Css from './vite.config.css'
+import Macros from './vite.config.macros'
+
+// https://vitejs.dev/config/
+export default defineConfig(({ mode, command }: ConfigEnv) => {
+    process.env = { ...process.env, ...loadEnv(mode, process.cwd()) }
+    const __dirname = path.dirname(fileURLToPath(import.meta.url))
+    console.log(`当前编译环境: ${process.env.VITE_APP_ENV}`)
+
+    return {
+        base: './',
+        server: Build.server,
+        build: Build.build,
+        css: Css,
+        resolve: {
+            alias: {
+                '~': path.join(__dirname, './src'),
+                '@': path.join(__dirname, './src'),
+            },
+        },
+        plugins: [
+            ...Macros(),
+            ...Components(),
+            UnoCSS(),
+            /**
+             * 本地和生产模拟服务
+             * @see https://github.com/vbenjs/vite-plugin-mock/blob/main/README.zh_CN.md
+             */
+            viteMockServe({
+                mockPath: 'mock',
+                enable: command === 'serve' || process.env.VITE_APP_ENV === 'test',
+                logger: true,
+            }),
+            /**
+             * 检查Vite插件的中间状态
+             * @see https://github.com/antfu/vite-plugin-inspect#readme
+             */
+            Inspect(),
+            /**
+             * 打包时展示进度条的插件
+             * @see https://github.com/jeddygong/vite-plugin-progress/blob/main/README.zh-CN.md
+             */
+            Progress(),
+        ],
+    }
+})

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor