备份原始代码

develoop
Your Name 1 year ago
commit 43a661633b
  1. 4
      .env.development
  2. 4
      .env.production
  3. 46
      .eslintrc.js
  4. 315
      .gitignore
  5. 3
      .npmrc
  6. 47
      .prettierrc.js
  7. 19
      .prettierrc.json
  8. 21
      LICENSE
  9. 38
      README.en.md
  10. 38
      README.md
  11. 8
      app.vue
  12. BIN
      assets/fonts/D-DIN-PRO-400-Regular.ttf
  13. BIN
      assets/fonts/D-DIN-PRO-600-SemiBold.ttf
  14. BIN
      assets/fonts/OPPOSans-M.ttf
  15. BIN
      assets/fonts/OPPOSans-R.ttf
  16. BIN
      assets/fonts/alimamashuheiti.ttf
  17. 36
      assets/fonts/font.css
  18. 218
      assets/iconfont/iconfont.css
  19. BIN
      assets/images/400.png
  20. BIN
      assets/images/404.png
  21. BIN
      assets/images/anniub.png
  22. BIN
      assets/images/coupon.png
  23. BIN
      assets/images/couponhui.png
  24. BIN
      assets/images/cuowu.png
  25. BIN
      assets/images/dayang.png
  26. BIN
      assets/images/enter.png
  27. BIN
      assets/images/gouwukong.png
  28. BIN
      assets/images/jiashi.png
  29. BIN
      assets/images/lq.png
  30. BIN
      assets/images/lqbj.png
  31. BIN
      assets/images/lqbjhui.png
  32. BIN
      assets/images/lqyhqbj.png
  33. BIN
      assets/images/miaosbj.jpg
  34. BIN
      assets/images/miaoshabj.png
  35. BIN
      assets/images/morentou.png
  36. BIN
      assets/images/msb.png
  37. BIN
      assets/images/mycoupon.png
  38. BIN
      assets/images/mycouponhui.png
  39. BIN
      assets/images/pinpaibg.png
  40. BIN
      assets/images/spb.png
  41. BIN
      assets/images/tijiaocheng.png
  42. BIN
      assets/images/tjchenggong.png
  43. BIN
      assets/images/weikaishi.png
  44. BIN
      assets/images/wudingdan.png
  45. BIN
      assets/images/wudizhi.png
  46. BIN
      assets/images/wufenlei.png
  47. BIN
      assets/images/wupinlun.png
  48. BIN
      assets/images/wushangpin.png
  49. BIN
      assets/images/wushoucang.png
  50. BIN
      assets/images/wusous.png
  51. BIN
      assets/images/wuyouhui.png
  52. BIN
      assets/images/wuzhangdan.png
  53. BIN
      assets/images/xiucheng.png
  54. BIN
      assets/images/yhq.png
  55. BIN
      assets/images/yhqwushangp.png
  56. BIN
      assets/images/yijieshu.png
  57. BIN
      assets/images/yilingquyou.png
  58. BIN
      assets/images/yiqiangwan.png
  59. BIN
      assets/images/zhizhao.png
  60. 66
      assets/scss/checkbox.scss
  61. 0
      assets/scss/element/dark.scss
  62. 26
      assets/scss/element/index.scss
  63. 430
      assets/scss/index.scss
  64. 512
      components/UserLogin.vue
  65. 311
      components/addAddress.vue
  66. 120
      components/classifyCard.vue
  67. 55
      components/confirmProduct.vue
  68. 191
      components/countDown.vue
  69. 163
      components/couponList.vue
  70. 36
      components/defaultComponents.ts
  71. 44
      components/emptyPage.vue
  72. 83
      components/headerSeach.vue
  73. 48
      components/layoutAdvertisement.vue
  74. 152
      components/layoutFooter.vue
  75. 487
      components/layoutHeader.vue
  76. 65
      components/merchantList.vue
  77. 141
      components/merchantNews.vue
  78. 23
      components/notLoggedIn.vue
  79. 70
      components/orderProduct.vue
  80. 39
      components/pageHeader.vue
  81. 64
      components/product.vue
  82. 104
      components/productSeach.vue
  83. 91
      components/recommend.vue
  84. 180
      components/rightNavigation.vue
  85. 124
      components/seachList.vue
  86. 118
      components/sortSeach.vue
  87. 60
      components/successDialog.vue
  88. 93
      components/swiperIndex.vue
  89. 148
      components/uploadFrom.vue
  90. 243
      components/userProductList.vue
  91. 489
      components/verifition/Verify.vue
  92. 273
      components/verifition/Verify/verifyPoints.vue
  93. 384
      components/verifition/Verify/verifySlider.vue
  94. 11
      components/verifition/utils/ase.js
  95. 0
      components/verifition/utils/fomat.ts
  96. 89
      components/verifition/utils/tips.ts
  97. 36
      components/verifition/utils/util.js
  98. 37
      composables/useCopy.ts
  99. 16
      composables/useDialog.ts
  100. 48
      composables/useMerchant.ts
  101. Some files were not shown because too many files have changed in this diff Show More

@ -0,0 +1,4 @@
# 请求域名
NUXT_API_URL=http://127.0.0.1:8081
# 接口默认前缀
NUXT_API_PREFIX=/api/

@ -0,0 +1,4 @@
# 请求域名
NUXT_API_URL=http://127.0.0.1:8081
# 接口默认前缀
NUXT_API_PREFIX=/api/

@ -0,0 +1,46 @@
/* eslint-env node */
module.exports = {
root: true,
extends: [
'plugin:nuxt/recommended',
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-typescript/recommended',
'@vue/eslint-config-prettier',
],
parserOptions: {
ecmaVersion: 'latest',
parser: '@typescript-eslint/parser',
sourceType: 'module',
},
plugins: ['@typescript-eslint'],
rules: {
'prettier/prettier': [
'warn',
{
semi: false,
singleQuote: true,
printWidth: 80,
proseWrap: 'preserve',
bracketSameLine: false,
endOfLine: 'auto',
tabWidth: 2,
useTabs: false,
trailingComma: 'none',
},
],
'no-useless-escape': 'off',
'vue/multi-word-component-names': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'no-undef': 'off',
'vue/prefer-import-from-vue': 'off',
'no-prototype-builtins': 'off',
'prefer-spread': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-non-null-asserted-optional-chain': 'off',
},
globals: {
module: 'readonly',
},
}

315
.gitignore vendored

@ -0,0 +1,315 @@
### Node template
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
### Vue template
# gitignore template for Vue.js projects
#
# Recommended template: Node.gitignore
# TODO: where does this rule come from?
docs/_book
# TODO: where does this rule come from?
test/
!/package-lock.json
/.idea/inspectionProfiles/profiles_settings.xml
/.idea/inspectionProfiles/Project_Default.xml
/.idea/.gitignore
/.idea/mer-pc.iml
/.idea/modules.xml
/.idea/prettier.xml
/.idea/vcs.xml
/.output/public/_nuxt/_id_.4f65f3f3.js
/.output/public/_nuxt/_id_.bb2eedb2.css
/.output/public/_nuxt/address_list.4e42d913.js
/.output/public/_nuxt/address_list.9c72fa93.css
/.output/public/_nuxt/agreement_rules.58031173.js
/.output/public/_nuxt/application_record.3aef8ea2.css
/.output/public/_nuxt/application_record.297caf13.js
/.output/public/_nuxt/asyncData.92e418ba.js
/.output/public/_nuxt/browsing_history.9e883ee3.js
/.output/public/_nuxt/checkbox.6cb61408.js
/.output/public/_nuxt/checkbox.56c0bd3a.css
/.output/public/_nuxt/collect_merchant.62c9e8e8.css
/.output/public/_nuxt/collect_merchant.143f8976.js
/.output/public/_nuxt/collect_products.59e73173.js
/.output/public/_nuxt/collect_products.962f748c.css
/.output/public/_nuxt/debounce.1a973c23.js
/.output/public/_nuxt/default.65c1aefd.css
/.output/public/_nuxt/default.68238643.js
/.output/public/_nuxt/defaultData.4ed993c7.js
/.output/public/_nuxt/defaultOrder.23384ad8.js
/.output/public/_nuxt/dialog.80e101db.js
/.output/public/_nuxt/dialog.c614b8ee.css
/.output/public/_nuxt/divider.07810808.css
/.output/public/_nuxt/divider.e4702176.js
/.output/public/_nuxt/dropdown.0b00051c.js
/.output/public/_nuxt/entry.808fc290.js
/.output/public/_nuxt/entry.c680c8a7.css
/.output/public/_nuxt/error-404.2a4421e5.js
/.output/public/_nuxt/error-404.7fc72018.css
/.output/public/_nuxt/error-500.c5df6088.css
/.output/public/_nuxt/error-500.d12ed7e7.js
/.output/public/_nuxt/error-component.80535782.js
/.output/public/_nuxt/goodsApi.3255d570.js
/.output/public/_nuxt/helper.301f26b7.js
/.output/public/_nuxt/image-viewer.1408de4f.js
/.output/public/_nuxt/image-viewer.bcc58336.css
/.output/public/_nuxt/index.35be60ef.css
/.output/public/_nuxt/index.efa0dc50.js
/.output/public/_nuxt/index.f92107a4.js
/.output/public/_nuxt/isEqual.a24691d3.js
/.output/public/_nuxt/merchant_settled.53fdfcac.css
/.output/public/_nuxt/merchant_settled.579702ed.js
/.output/public/_nuxt/merchant_street.9b2895af.js
/.output/public/_nuxt/merchantApi.da67b3ce.js
/.output/public/_nuxt/nuxt-link.24dc26bd.js
/.output/public/_nuxt/order_confirm.1ecbc8e2.css
/.output/public/_nuxt/order_confirm.d9bec777.js
/.output/public/_nuxt/order_details.9ba6bff6.css
/.output/public/_nuxt/order_details.ee22973b.js
/.output/public/_nuxt/order_payment.0ca0ace3.css
/.output/public/_nuxt/order_payment.3a6dc8c2.js
/.output/public/_nuxt/orderApi.40e90da4.js
/.output/public/_nuxt/orderProduct.vue.50f40c00.js
/.output/public/_nuxt/pageHeader.vue.dbf6b680.js
/.output/public/_nuxt/pagination.0af840e7.css
/.output/public/_nuxt/pagination.75fc347e.js
/.output/public/_nuxt/popper.6172019c.css
/.output/public/_nuxt/popper.b231ab57.js
/.output/public/_nuxt/select.6b7a2b87.css
/.output/public/_nuxt/select.c7b6ad97.js
/.output/public/_nuxt/shopping_cart.85b6814b.js
/.output/public/_nuxt/shopping_cart.c61fa8ef.css
/.output/public/_nuxt/swiper-vue.5f03663a.js
/.output/public/_nuxt/swiper-vue.f31e28c4.css
/.output/public/_nuxt/tips.0d8f84ae.js
/.output/public/_nuxt/tjchenggong.445bd1d9.png
/.output/public/_nuxt/useOrder.87a22d3f.js
/.output/public/_nuxt/user_balance.2f911ad7.js
/.output/public/_nuxt/user_balance.2406564a.css
/.output/public/_nuxt/user_coupon.86c5c4f0.css
/.output/public/_nuxt/user_coupon.f79479c7.js
/.output/public/_nuxt/user_integral.ad973a6e.js
/.output/public/_nuxt/user_integral.d45a347c.css
/.output/public/_nuxt/userApi.5cd08da7.js
/.output/public/_nuxt/userProductList.1c0ca74a.css
/.output/public/_nuxt/userProductList.361dd686.js
/.output/public/_nuxt/users.ddd09eb2.css
/.output/public/_nuxt/users.f9f7b18e.js
/.output/public/_nuxt/Verify.35438517.css
/.output/public/_nuxt/Verify.b7c4aaa1.js
/.output/public/css/animate.min.css
/.output/public/css/fullPage.css
/.output/public/fonts/iconfont.css
/.output/public/fonts/iconfont.js
/.output/public/fonts/iconfont.svg
/.output/public/fonts/iconfont.ttf
/.output/public/fonts/iconfont.woff
/.output/public/fonts/iconfont.woff2
/.output/public/images/approved.png
/.output/public/images/audit-failed.png
/.output/public/images/collection.png
/.output/public/images/creationCenter.png
/.output/public/images/crmeb.png
/.output/public/images/details.png
/.output/public/images/none-001.png
/.output/public/images/portrait.png
/.output/public/images/ranking.png
/.output/public/images/read.png
/.output/public/images/thumbsUp.png
/.output/public/images/under-review.png
/.output/public/js/web-office-sdk-v1.1.19.es.js
/.output/public/tinymce/langs/zh-Hans.js
/.output/public/tinymce/skins/content/dark/content.css
/.output/public/tinymce/skins/content/dark/content.min.css
/.output/public/tinymce/skins/content/default/content.css
/.output/public/tinymce/skins/content/default/content.min.css
/.output/public/tinymce/skins/content/document/content.css
/.output/public/tinymce/skins/content/document/content.min.css
/.output/public/tinymce/skins/content/tinymce-5/content.css
/.output/public/tinymce/skins/content/tinymce-5/content.min.css
/.output/public/tinymce/skins/content/tinymce-5-dark/content.css
/.output/public/tinymce/skins/content/tinymce-5-dark/content.min.css
/.output/public/tinymce/skins/content/writer/content.css
/.output/public/tinymce/skins/content/writer/content.min.css
/.output/public/tinymce/skins/ui/oxide/content.css
/.output/public/tinymce/skins/ui/oxide/content.inline.css
/.output/public/tinymce/skins/ui/oxide/content.inline.min.css
/.output/public/tinymce/skins/ui/oxide/content.min.css
/.output/public/tinymce/skins/ui/oxide/skin.css
/.output/public/tinymce/skins/ui/oxide/skin.min.css
/.output/public/tinymce/skins/ui/oxide/skin.shadowdom.css
/.output/public/tinymce/skins/ui/oxide/skin.shadowdom.min.css
/.output/public/tinymce/skins/ui/oxide-dark/content.css
/.output/public/tinymce/skins/ui/oxide-dark/content.inline.css
/.output/public/tinymce/skins/ui/oxide-dark/content.inline.min.css
/.output/public/tinymce/skins/ui/oxide-dark/content.min.css
/.output/public/tinymce/skins/ui/oxide-dark/skin.css
/.output/public/tinymce/skins/ui/oxide-dark/skin.min.css
/.output/public/tinymce/skins/ui/oxide-dark/skin.shadowdom.css
/.output/public/tinymce/skins/ui/oxide-dark/skin.shadowdom.min.css
/.output/public/tinymce/skins/ui/tinymce-5/content.css
/.output/public/tinymce/skins/ui/tinymce-5/content.inline.css
/.output/public/tinymce/skins/ui/tinymce-5/content.inline.min.css
/.output/public/tinymce/skins/ui/tinymce-5/content.min.css
/.output/public/tinymce/skins/ui/tinymce-5/skin.css
/.output/public/tinymce/skins/ui/tinymce-5/skin.min.css
/.output/public/tinymce/skins/ui/tinymce-5/skin.shadowdom.css
/.output/public/tinymce/skins/ui/tinymce-5/skin.shadowdom.min.css
/.output/public/tinymce/skins/ui/tinymce-5-dark/content.css
/.output/public/tinymce/skins/ui/tinymce-5-dark/content.inline.css
/.output/public/tinymce/skins/ui/tinymce-5-dark/content.inline.min.css
/.output/public/tinymce/skins/ui/tinymce-5-dark/content.min.css
/.output/public/tinymce/skins/ui/tinymce-5-dark/skin.css
/.output/public/tinymce/skins/ui/tinymce-5-dark/skin.min.css
/.output/public/tinymce/skins/ui/tinymce-5-dark/skin.shadowdom.css
/.output/public/tinymce/skins/ui/tinymce-5-dark/skin.shadowdom.min.css
/.output/public/CNAME
/.output/public/favicon.ico
/.output/server/chunks/app/client.manifest.mjs
/.output/server/chunks/app/client.manifest.mjs.map
/.output/server/chunks/app/styles.mjs
/.output/server/chunks/app/styles.mjs.map
/.output/server/chunks/handlers/renderer.mjs
/.output/server/chunks/handlers/renderer.mjs.map
/.output/server/chunks/nitro/node-server.mjs
/.output/server/chunks/nitro/node-server.mjs.map
/.output/server/chunks/rollup/_virtual_head-static.mjs
/.output/server/chunks/rollup/_virtual_head-static.mjs.map
/.output/server/chunks/error-500.mjs
/.output/server/chunks/error-500.mjs.map
/.output/server/index.mjs
/.output/server/index.mjs.map
/.output/server/package.json
/.output/nitro.json

@ -0,0 +1,3 @@
shamefully-hoist=true
strict-peer-dependencies=false
shell-emulator=true

@ -0,0 +1,47 @@
/** .prettierrc.js
* 在VSCode中安装prettier插件 打开插件配置填写`.prettierrc.js` 将本文件作为其代码格式化规范
* 在本文件中修改格式化规则不会同时触发改变ESLint代码检查所以每次修改本文件需要重启VSCodeESLint检查才能同步代码格式化
* 需要相应的代码格式化规范请自行查阅配置下面为默认项目配置
*/
module.exports = {
// 一行最多多少个字符
printWidth: 150,
// 指定每个缩进级别的空格数
tabWidth: 2,
// 使用制表符而不是空格缩进行
useTabs: true,
// 在语句末尾是否需要分号
semi: true,
// 是否使用单引号
singleQuote: true,
// 更改引用对象属性的时间 可选值"<as-needed|consistent|preserve>"
quoteProps: 'as-needed',
// 在JSX中使用单引号而不是双引号
jsxSingleQuote: false,
// 多行时尽可能打印尾随逗号。(例如,单行数组永远不会出现逗号结尾。) 可选值"<none|es5|all>",默认none
trailingComma: 'es5',
// 在对象文字中的括号之间打印空格
bracketSpacing: true,
// jsx 标签的反尖括号需要换行
jsxBracketSameLine: false,
// 在单独的箭头函数参数周围包括括号 always:(x) => x \ avoid:x => x
arrowParens: 'always',
// 这两个选项可用于格式化以给定字符偏移量(分别包括和不包括)开始和结束的代码
rangeStart: 0,
rangeEnd: Infinity,
// 指定要使用的解析器,不需要写文件开头的 @prettier
requirePragma: false,
// 不需要自动在文件开头插入 @prettier
insertPragma: false,
// 使用默认的折行标准 always\never\preserve
proseWrap: 'preserve',
// 指定HTML文件的全局空格敏感度 css\strict\ignore
htmlWhitespaceSensitivity: 'css',
// Vue文件脚本和样式标签缩进
vueIndentScriptAndStyle: false,
//在 windows 操作系统中换行符通常是回车 (CR) 加换行分隔符 (LF),也就是回车换行(CRLF),
//然而在 Linux 和 Unix 中只使用简单的换行分隔符 (LF)。
//对应的控制字符为 "\n" (LF) 和 "\r\n"(CRLF)。auto意为保持现有的行尾
// 换行符使用 lf 结尾是 可选值"<auto|lf|crlf|cr>"
endOfLine: 'auto',
};

@ -0,0 +1,19 @@
{
"printWidth": 120,
"tabWidth": 2,
"useTabs": false,
"semi": false,
"singleQuote": true,
"quoteProps": "as-needed",
"jsxSingleQuote": false,
"trailingComma": "all",
"bracketSpacing": true,
"arrowParens": "always",
"rangeStart": 0,
"requirePragma": false,
"insertPragma": false,
"proseWrap": "preserve",
"htmlWhitespaceSensitivity": "css",
"endOfLine": "lf"
}

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Element Plus
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.

@ -0,0 +1,38 @@
# mer_pc
#### Description
多商户PC商城
```
## 开发
```bash
# node版本
node版本建议使用v16.18以上
# 进入项目目录
cd ##
# 安装依赖
npm install
# 建议不要直接使用 cnpm 安装依赖,会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题
npm install --registry=https://registry.npm.taobao.org
# 启动服务
npm run dev
```
浏览器访问 http://localhost:9527
## 发布
```bash
# 构建测试环境
npm run build
# 代码格式检查并自动修复
npm run prettier
```

@ -0,0 +1,38 @@
# mer_pc
#### 介绍
多商户PC商城
```
## 开发
```bash
# node版本
node版本建议使用v16.18以上
# 进入项目目录
cd ##
# 安装依赖
npm install
# 建议不要直接使用 cnpm 安装依赖,会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题
npm install --registry=https://registry.npm.taobao.org
# 启动服务
npm run dev
```
浏览器访问 http://localhost:9527
## 发布
```bash
# 构建测试环境
npm run build
# 代码格式检查并自动修复
npm run prettier
```

@ -0,0 +1,8 @@
<template>
<!-- 路由出口 -->
<NuxtLayout>
<NuxtPage></NuxtPage>
</NuxtLayout>
</template>
<script setup>
</script>

Binary file not shown.

Binary file not shown.

Binary file not shown.

@ -0,0 +1,36 @@
@font-face {
font-family: 'oppoSans-R';
src: url('./OPPOSans-R.ttf') format('truetype');
}
@font-face {
font-family: 'oppoSans-M';
src: url('./OPPOSans-M.ttf') format('truetype');
}
@font-face {
font-family: 'dinProSemiBold';
src: url('./D-DIN-PRO-600-SemiBold.ttf');
}
@font-face {
font-family: 'dinProRegular';
src: url('./D-DIN-PRO-400-Regular.ttf');
}
@font-face {
font-family: 'alimama';
src: url('alimamashuheiti.ttf') format('truetype');
}
.dinProSemiBold{
font-family: 'dinProSemiBold';
}
.dinProRegular{
font-family: 'dinProRegular';
}
.alimama{
font-family: 'alimama';
}
.oppoSans-M{
font-family: 'oppoSans-M';
font-weight: 500;
}
.oppoSans-R{
font-family: 'oppoSans-R';
}

@ -0,0 +1,218 @@
@font-face {
font-family: "iconfont"; /* Project id 4247807 */
src: url('//at.alicdn.com/t/c/font_4247807_9t2habcu5gg.woff2?t=1701833017633') format('woff2'),
url('//at.alicdn.com/t/c/font_4247807_9t2habcu5gg.woff?t=1701833017633') format('woff'),
url('//at.alicdn.com/t/c/font_4247807_9t2habcu5gg.ttf?t=1701833017633') format('truetype');
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-lianxikefu:before {
content: "\e6f6";
}
.icon-yishoucang:before {
content: "\e6f4";
}
.icon-zhiding:before {
content: "\e6f5";
}
.icon-shenqingjilu:before {
content: "\e6f3";
}
.icon-shijian:before {
content: "\e6f2";
}
.icon-di:before {
content: "\e6db";
}
.icon-gao:before {
content: "\e6f1";
}
.icon-weitongguo1:before {
content: "\e6f0";
}
.icon-gaodi:before {
content: "\e6ee";
}
.icon-baozhang:before {
content: "\e6ef";
}
.icon-shenhezhong:before {
content: "\e6df";
}
.icon-shangchuan:before {
content: "\e6e0";
}
.icon-sousuo:before {
content: "\e6e1";
}
.icon-shanchu:before {
content: "\e6cd";
}
.icon-shoujishangcheng:before {
content: "\e6e2";
}
.icon-shangchengshouye:before {
content: "\e6ce";
}
.icon-kuaidi:before {
content: "\e6e3";
}
.icon-shangpinfenlei:before {
content: "\e6e4";
}
.icon-guanzhudianpu:before {
content: "\e6cc";
}
.icon-liulan:before {
content: "\e6e5";
}
.icon-jia:before {
content: "\e6e6";
}
.icon-jian:before {
content: "\e6e7";
}
.icon-guanzhu:before {
content: "\e6e8";
}
.icon-gouwuche:before {
content: "\e6e9";
}
.icon-dingdan:before {
content: "\e6ea";
}
.icon-baobeishoucang:before {
content: "\e6eb";
}
.icon-gengduo:before {
content: "\e6ec";
}
.icon-bianji:before {
content: "\e6ed";
}
.icon-shoucang:before {
content: "\e6d2";
}
.icon-zuji:before {
content: "\e6d3";
}
.icon-youhuiquan:before {
content: "\e6d1";
}
.icon-xiala:before {
content: "\e6d0";
}
.icon-zuo1:before {
content: "\e6d4";
}
.icon-yitongguo:before {
content: "\e6d5";
}
.icon-zhifubaozhifu:before {
content: "\e6d7";
}
.icon-xiaoxi:before {
content: "\e6d8";
}
.icon-yuezhifu:before {
content: "\e6d9";
}
.icon-you1:before {
content: "\e6da";
}
.icon-wode:before {
content: "\e6dc";
}
.icon-xingji:before {
content: "\e6dd";
}
.icon-weixinzhifu:before {
content: "\e6de";
}
.icon-xuanzhong:before {
content: "\e6ca";
}
.icon-weixuanzhong:before {
content: "\e6cb";
}
.icon-zuo:before {
content: "\e6c9";
}
.icon-you:before {
content: "\e6c8";
}
.icon-yanzhengma:before {
content: "\e6c5";
}
.icon-shoujihao:before {
content: "\e6c6";
}
.icon-mima:before {
content: "\e6c7";
}
.icon-danchuangguanbi:before {
content: "\e6c1";
}
.icon-erweima:before {
content: "\e6c3";
}
.icon-zhanghaodenglu:before {
content: "\e6c4";
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 668 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 882 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 440 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 747 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 838 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 705 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 578 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 410 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 796 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 563 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 407 B

@ -0,0 +1,66 @@
/* 单选框和多选框 */
.checkbox-wrapper {
position: relative;
input {
display: none;
}
.icon {
position: relative;
left: 0px;
top: 7px;
display: inline-block;
width: 13px;
height: 13px;
border: 1px solid #cccccc;
border-radius: 50%;
-webkit-transform: translate(0, -50%);
-moz-transform: translate(0, -50%);
-o-transform: translate(0, -50%);
-ms-transform: translate(0, -50%);
transform: translate(0, -50%);
}
input:checked + .icon {
background-color: #e93323;
border-color: #e93323;
background-image: url("../images/enter.png");
-webkit-background-size: 10px 8px;
-moz-background-size: 10px 8px;
background-size: 10px 8px;
background-repeat: no-repeat;
background-position: center center;
}
}
.Checkbox {
position: absolute;
visibility: hidden;
outline: none;
background: #fff;
}
.Checkbox+label {
position:absolute;
width: 16px;
height: 16px;
border: 1px solid #9B9B9B;
border-radius: 50%;
background-color:#fff;
left: 11px;
top: 50%;
margin-top: -8px;
}
.Checkbox:checked+label:after {
content: "";
position: absolute;
left: 3px;
top:3px;
width: 6px;
height: 3px;
border: 2px solid #9B9B9B;
border-top-color: transparent;
border-right-color: transparent;
transform: rotate(-45deg);
-ms-transform: rotate(-45deg);
-moz-transform: rotate(-45deg);
-webkit-transform: rotate(-45deg);
}

@ -0,0 +1,26 @@
$-colors: (
'primary': (
'base': #E93323,
),
'success': (
'base': #67c23a,
),
'warning': (
'base': #e6a23c,
),
'danger': (
'base': #f56c6c,
),
'error': (
'base': #E93323,
),
'info': (
'base': #909399,
),
);
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
$colors: $-colors
);
@use './dark.scss';

@ -0,0 +1,430 @@
* {
margin: 0;
padding: 0;
font-family: 'oppoSans-M';
}
html {
background: #f5f5f5;
}
.page-enter-active,
.page-leave-active {
transition: all 0.4s;
}
.page-enter-from,
.page-leave-to {
opacity: 0;
filter: blur(1rem);
}
.el-dropdown-link{
border: none;
outline: none;
}
//标签
.labelClass{
font-size: 12px;
line-height: 1;
padding: 2px 4px;
border-radius: 2px;
}
.verticalClass{
writing-mode: vertical-rl;
text-orientation: upright;
white-space: nowrap;
letter-spacing: 0.2em;
display: flex;
align-items: center;
}
.acea-row {
display: -webkit-box;
display: -moz-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-lines: multiple;
-moz-box-lines: multiple;
-o-box-lines: multiple;
-webkit-flex-wrap: wrap;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
}
.min_wrapper_1200 {
width: 1200px;
margin: 0 auto;
height: 100%;
}
.acea-row.row-middle {
-webkit-box-align: center;
-moz-box-align: center;
-o-box-align: center;
-ms-flex-align: center;
-webkit-align-items: center;
align-items: center;
}
.acea-row.row-top {
-webkit-box-align: start;
-moz-box-align: start;
-o-box-align: start;
-ms-flex-align: start;
-webkit-align-items: flex-start;
align-items: flex-start;
}
.acea-row.row-bottom {
-webkit-box-align: end;
-moz-box-align: end;
-o-box-align: end;
-ms-flex-align: end;
-webkit-align-items: flex-end;
align-items: flex-end;
}
.acea-row.row-center {
-webkit-box-pack: center;
-moz-box-pack: center;
-o-box-pack: center;
-ms-flex-pack: center;
-webkit-justify-content: center;
justify-content: center;
}
.acea-row.row-right {
-webkit-box-pack: end;
-moz-box-pack: end;
-o-box-pack: end;
-ms-flex-pack: end;
-webkit-justify-content: flex-end;
justify-content: flex-end;
}
.acea-row.row-left {
-webkit-box-pack: start;
-moz-box-pack: start;
-o-box-pack: start;
-ms-flex-pack: start;
-webkit-justify-content: flex-start;
justify-content: flex-start;
}
.acea-row.row-between {
-webkit-box-pack: justify;
-moz-box-pack: justify;
-o-box-pack: justify;
-ms-flex-pack: justify;
-webkit-justify-content: space-between;
justify-content: space-between;
}
.acea-row.row-around {
justify-content: space-around;
-webkit-justify-content: space-around;
}
.acea-row.row-column-around {
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
justify-content: space-around;
-webkit-justify-content: space-around;
}
.acea-row.row-column {
-webkit-box-orient: vertical;
-moz-box-orient: vertical;
-o-box-orient: vertical;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
}
.acea-row.row-column-between {
-webkit-box-orient: vertical;
-moz-box-orient: vertical;
-o-box-orient: vertical;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-pack: justify;
-moz-box-pack: justify;
-o-box-pack: justify;
-ms-flex-pack: justify;
-webkit-justify-content: space-between;
justify-content: space-between;
}
.acea-row.row-center-wrapper {
-webkit-box-align: center;
-moz-box-align: center;
-o-box-align: center;
-ms-flex-align: center;
-webkit-align-items: center;
align-items: center;
-webkit-box-pack: center;
-moz-box-pack: center;
-o-box-pack: center;
-ms-flex-pack: center;
-webkit-justify-content: center;
justify-content: center;
}
.acea-row.row-between-wrapper {
-webkit-box-align: center;
-moz-box-align: center;
-o-box-align: center;
-ms-flex-align: center;
-webkit-align-items: center;
align-items: center;
-webkit-box-pack: justify;
-moz-box-pack: justify;
-o-box-pack: justify;
-ms-flex-pack: justify;
-webkit-justify-content: space-between;
justify-content: space-between;
}
.wrapper_1200 {
width: 1200px;
margin: 0 auto;
}
.router-link-active {
text-decoration: none; //去除默认样式
color: orange; //高亮的颜色
}
a {
text-decoration: none;
color: grey;
}
div {
box-sizing: border-box;
}
.pad020 {
padding: 0 20px;
}
::-webkit-input-placeholder {
color: #cccccc !important;
}
::-moz-placeholder {
color: #cccccc !important;
}
:-ms-input-placeholder {
color: #cccccc !important;
}
.font14 {
font-size: 14px;
color: #666666;
}
.font12 {
font-size: 12px;
}
.fonts14 {
font-size: 14px;
}
.font20 {
font-size: 20px;
}
.fonts16 {
font-size: 16px;
}
.fontsweight {
font-weight: 600 !important;
}
.fontColor333 {
color: #333333;
}
.fontColor6 {
color: #666666;
}
.font18 {
font-size: 18px;
color: #333333;
font-weight: 500 !important;
}
.mbtom24 {
margin-bottom: 24px;
}
.mbtom30 {
margin-bottom: 30px;
}
.cursors {
cursor: pointer;
}
.mbtom20 {
margin-bottom: 20px;
}
.pad30 {
padding: 30px;
}
.bg-color {
background: #e93323;
}
.font-color {
color: #FF4E8D !important;
}
.borRadius {
border-radius: 16px 16px 16px 16px !important;
}
.line1 {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.line2 {
word-break: break-all;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
/* height: 84rpx; */
}
//分页样式
.page-item {
margin: 0 auto;
}
.textarea {
outline: none;
resize: none;
border-radius: 8px 8px 8px 8px;
opacity: 1;
border: 1px solid #cccccc;
padding: 15px;
font-size: 14px;
box-sizing: border-box;
}
.borderSolRed {
border: 1px solid #E93323;
}
.borderSol {
border: 1px solid #cccccc;
}
.borderSol-eee {
border: 1px solid #EEEEEE;
}
.borderSolE9 {
border: 1px solid #E93323 !important;
}
.borderBotDas {
border-bottom: 1px dashed #eeeeee;
}
.borderBotSol {
border-bottom: 1px solid #eeeeee;
}
.page-item{
--el-color-primary:#E93323;
}
.image-slot{
display: flex;
justify-content: center;
align-items: center;
font-size: 14px;
background: var(--el-fill-color-light);
color: var(--el-text-color-placeholder);
vertical-align: middle;
}
//搜索框
.seachInput{
:deep(.el-input__wrapper) {
border: none !important;
box-shadow: none !important;
}
:deep(.el-input) {
--el-fill-color-blank: #f7f7f7 !important;
--el-border-radius-base: 20px;
}
}
//btn样式 面性
.handleBtn{
background: #E93323 !important;
color: #fff;
text-align: center;
border-radius: 25px;
font-size: 12px;
}
//btn样式 线性
.handleBtnBorder{
text-align: center;
border-radius: 22px;
font-size: 12px;
border: 1px solid #CCCCCC;
}
.el-dialog{
border-radius: 16px !important;
}
//视频
video, .wscnph{
width: 100%;
height: 100%;
}
.el-rate__text{
color: #e93323 !important;
margin-left: 5px;
}
// 模态框头部标题样式
.el-dialog__header{
padding: 30px 0 36px 0 !important;
}
.el-dialog__body{
padding: 0 calc(var(--el-dialog-padding-primary) + 5px) 30px !important;
}
.el-upload-list{
.el-icon--close-tip{
display: none !important;
}
}
//资讯信息选中菜单背景图去掉颜色
.el-dropdown__popper{
background: #fff !important;
--el-dropdown-menuItem-hover-fill: #fff !important;
}
.line-heightOne{
line-height: 1;
}
// 去掉input小箭头
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
}
input[type="number"]{
-moz-appearance: textfield;
}
//富文本样式
.htmlClass{
img{
max-width: 100%;
}
h1,h2,h3,h4,h5,h6,img, p{
display: block;
margin-block-start: 1em;
margin-block-end: 1em;
margin-inline-start: 0px;
margin-inline-end: 0px;
}
*{
line-height: 30px;
font-family: 'oppoSans-R';
}
}
.el-popover{
min-width: auto !important;
}

@ -0,0 +1,512 @@
<!--登录弹窗组件-->
<template>
<el-dialog
class="user-login-dialog"
v-model="dialogVisible"
:align-center="true"
:width="420"
:append-to-body="true"
:show-close="false"
>
<div class="login-count">
<!-- 登录弹窗 -->
<div class="wrapper-count">
<span class="closeBtn iconfont icon-danchuangguanbi" @click="closeLogin"></span>
<div class="wrapper">
<div class="title">
<span class="item_title" @click="current = 1" :class="current === 1 ? 'font18' : ''">登录/注册</span>
<span class="item_title" @click="current = 2" :class="current === 2 ? 'font18' : ''">密码登录</span>
</div>
<div class="iconfont icon-erweima2" @click="current = 3"></div>
<!--手机号-->
<div class="item phone acea-row row-middle">
<div class="number mr-14px"><span class="iconfont icon-shoujihao"></span></div>
<input type="text" placeholder="请输入手机号" v-model="formData.phone" class="w-90% text-14px text-#333" />
</div>
<!--验证码-->
<div
v-show="current === 1 || current === 4 || current === 5"
class="item phone verificat acea-row row-between-wrapper"
>
<div class="acea-row row-middle">
<div class="number mr-14px"><span class="iconfont icon-yanzhengma"></span></div>
<input
type="text"
autocomplete="new-password"
placeholder="请输入验证码"
v-model="formData.captcha"
class="text-14px text-#333"
/>
</div>
<button
class="code font-color cursors"
:disabled="disabled"
:class="disabled === true ? 'on' : ''"
@click="handleSendcode()"
>
{{ text }}
</button>
</div>
<!--密码登录-->
<div v-show="current === 2">
<div class="item phone verificat acea-row">
<div class="number mr-14px"><span class="iconfont icon-mima"></span></div>
<input
type="password"
class="text-14px text-#333 w-90%"
placeholder="请输入密码"
v-model="formData.password"
/>
</div>
</div>
<!--底部按钮-->
<div class="checkbox-wrapper item_protocol acea-row row-middle" style="margin-top: 25px;">
<label class="well-check" style="line-height: 2">
<input type="checkbox" name="" value="" :checked="isAgreement" @click="isAgreement = !isAgreement" />
<i class="iconfont mr-7px icon" style="top: 11px"></i>
<span>十天内免登录公共场合慎选</span>
</label>
</div>
<div class="checkbox-wrapper item_protocol acea-row row-middle">
<label class="well-check" style="line-height: 2">
<input type="checkbox" name="" value="" :checked="isAgreement" @click="isAgreement = !isAgreement" />
<i class="iconfont mr-7px icon" style="top: 11px"></i>
<span>我已阅读并同意</span>
</label>
<nuxt-link
:to="{ path: `/users/agreement_rules`, query: { type: 'userinfo', name: '用户协议' } }"
target="_blank"
class="show_protocol"
>用户协议
</nuxt-link>
<nuxt-link
:to="{ path: `/users/agreement_rules`, query: { type: 'userprivacyinfo', name: '隐私协议' } }"
target="_blank"
class="show_protocol"
>隐私协议
</nuxt-link>
</div>
<div class="signIn bg-color" @click="handleLogin">登录</div>
<div class="forgot-password">
<div class="left-btn">忘记密码</div>
<div class="left-btn">注册</div>
</div>
</div>
<div class="wxLogin wrapper" v-if="current === 3">
<div class="inner">扫码登录</div>
<div class="iconfont icon-zhanghaodenglu1" @click="current = 1"></div>
<div class="wxCode">
<span class="iconfont icon-erweimabianjiao"></span>
<span class="iconfont icon-erweimabianjiao"></span>
<span class="iconfont icon-erweimabianjiao"></span>
<span class="iconfont icon-erweimabianjiao"></span>
<img v-if="qrCode" :src="qrCode" />
</div>
<div class="tip">请使用微信扫一扫登录</div>
</div>
</div>
</div>
<VerifitionVerify ref="verifyRef" :phone="formData.phone" @success="handlerOnVerSuccess"></VerifitionVerify>
</el-dialog>
</template>
<script setup lang="ts">
import { PhoneReg } from '~/utils/validate'
import { Debounce } from '~/utils/util'
import { useUserStore } from '~/stores/user'
import { loginMobile, loginPassword, registerVerify, userPhoneCodeApi } from '~/server/userApi'
import feedback from '~/utils/feedback'
import useOrder from '~/composables/useOrder'
import {ref, reactive} from 'vue'
const userStore = useUserStore()
const emit = defineEmits(['onLoginSucceeded'])
const handleEmit = () => {
emit('onLoginSucceeded')
}
//
const isAgreement = ref(false)
//
const formData = reactive({
captcha: '',
phone: '',
spreadPid: 0,
password: '',
})
//
const qrCode = ref('')
//
const { disabled, text, handleCodeSend } = useSmsCode()
const isSendCode = ref(false)
//
const checkPhone = (rule: any, value: any, callback: any) => {
if (!value) {
return callback(new Error('请输入手机号码'))
}
setTimeout(() => {
if (PhoneReg.test(value)) {
callback()
} else {
callback(new Error('请输入有效的电话号码'))
}
}, 150)
}
//
const inputPhone = (e: any) => {
isSendCode.value = PhoneReg.test(e)
}
//
const { bool: dialogVisible, DialogOpen, DialogClose } = useDialog()
const open = () => {
DialogOpen()
}
defineExpose({ open })
const colse = () => {
DialogClose()
formData.phone = ''
formData.captcha = ''
}
//
const closeLogin = () => {
DialogClose()
}
//
const verifyRef = shallowRef()
const handleSendcode = () => {
if (!formData.phone) return feedback.msgWarning('请填写手机号')
verifyRef.value.show()
}
//
const handlerOnVerSuccess = async (e: any) => {
await registerVerify({ phone: formData.phone })
feedback.msgSuccess('发送成功')
handleCodeSend()
}
const agreementConfirm = async () => {
if (isAgreement.value) {
return
}
await feedback.confirm('确认已阅读并同意《服务协议》和《隐私政策》')
isAgreement.value = true
}
//
// 1 2
const current = ref<number>(1)
const { onGetCartCount } = useOrder()
const handleLogin = Debounce(async () => {
if (!formData.phone) return feedback.msgError('请填写手机号码')
if (!/^1(3|4|5|7|8|9|6)\d{9}$/i.test(formData.phone)) return feedback.msgError('请输入正确的手机号码')
if (!isAgreement.value) return feedback.msgError('请勾选用户隐私协议')
//
if (current.value === 1) {
await getLoginMobile()
} else {
//
if (!formData.password) return feedback.msgError('请填写密码')
await getloginPassword()
}
await colse()
await handleEmit() //
await onGetCartCount()
},500)
//
const getloginPassword = async () => {
const params = {
phone: formData.phone,
password: formData.password,
spreadPid: formData.spreadPid,
}
const data = await loginPassword(params)
userStore.login(data.token)
userStore.setUserInfo(data)
}
///
const getLoginMobile = async () => {
const params = {
phone: formData.phone,
captcha: formData.captcha,
spreadPid: formData.spreadPid,
}
const data = await loginMobile(params)
userStore.login(data.token)
userStore.setUserInfo(data)
}
</script>
<style scoped lang="scss">
@import '@/assets/scss/checkbox.scss';
input::-webkit-input-placeholder {
/* 修改placeholder颜色 */
color: #cccccc;
}
.login-count {
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
position: fixed;
top: 0;
left: 0;
z-index: 100;
}
.wrapper-count {
position: relative;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 430px;
height: 428px;
background: #ffffff;
border-radius: 16px 16px 16px 16px;
opacity: 1;
.closeBtn {
color: #fff;
position: absolute;
top: -33px;
right: -33px;
text-align: center;
font-size: 24px;
cursor: pointer;
}
}
.wrapper {
position: relative;
background-color: #fff;
text-align: center;
padding: 60px 0 0 0;
margin: 0 auto;
border-radius: 16px 16px 16px 16px;
.font_red {
color: #e93323;
}
.title {
font-size: 18px;
font-weight: 400;
color: #999999;
position: relative;
.item_title {
cursor: pointer;
&:first-child {
margin-right: 70px;
}
}
.iconfont {
position: absolute;
top: -71px;
right: 0;
font-size: 60px;
cursor: pointer;
}
}
.item {
width: 370px;
height: 50px;
border: 1px solid #cccccc;
border-radius: 8px 8px 8px 8px;
margin: 0 auto;
padding: 0 14px;
.code {
height: 96%;
border: 0;
background-color: #fff;
font-size: 14px;
img {
width: 100%;
height: 100%;
}
&.on {
color: #ccc !important;
}
}
&.phone {
margin-top: 30px;
.number {
height: 100%;
line-height: 49px;
color: #cccccc;
}
}
&.pwd {
margin-top: 20px;
}
&.verificat {
margin-top: 20px;
}
input {
/*height: 96%;*/
border: 0;
outline: none;
font-size: 14px;
}
}
.signIn {
width: 370px;
height: 50px;
border-radius: 8px 8px 8px 8px;
text-align: center;
line-height: 50px;
margin: 15px auto 0 auto;
color: #fff;
cursor: pointer;
background: linear-gradient(to right, #FFE9AF, #FF4E8D);
}
.forgot-password{
display: flex;
justify-content: space-between;
padding: 20px 30px;
color: #8A8E99;
font-size: 12px;
}
.fastLogin {
margin-top: 14px;
cursor: pointer;
color: #cccccc;
}
.title + .iconfont {
position: absolute;
top: 0;
right: 0;
font-size: 46px;
color: #282828;
}
&.wxLogin {
position: relative;
padding-top: 98px;
.inner {
position: absolute;
top: 34px;
left: 30px;
font-size: 20px;
color: #282828;
}
.icon-zhanghaodenglu1 {
position: absolute;
top: 0;
right: 0;
font-size: 46px;
color: #282828;
}
}
.wxCode {
position: relative;
width: 213px;
height: 213px;
padding: 10px;
margin: 0 auto;
img {
display: block;
width: 100%;
}
.iconfont {
font-size: 22px;
color: #cbcbcb;
}
.iconfont:nth-child(1) {
position: absolute;
top: 0;
left: 0;
}
.iconfont:nth-child(2) {
position: absolute;
top: 0;
right: 0;
transform: rotate(90deg);
}
.iconfont:nth-child(3) {
position: absolute;
right: 0;
bottom: 0;
transform: rotate(180deg);
}
.iconfont:nth-child(4) {
position: absolute;
bottom: 0;
left: 0;
transform: rotate(270deg);
}
}
.tip {
margin-top: 20px;
font-size: 16px;
color: #666;
}
.item_protocol {
margin: 0 auto 0;
text-align: left;
padding-left: 30px;
font-size: 12px;
.icon {
width: 14px;
height: 14px;
}
.show_protocol {
color: #43A7E5;
cursor: pointer;
}
.forget_password {
float: right;
color: #999999;
cursor: pointer;
.icon-wangjimima {
display: inline-block;
width: 12px;
height: 12px;
line-height: 12px;
margin-right: 5px;
position: relative;
top: 1px;
text-align: center;
border: 1px solid #999999;
border-radius: 100%;
}
}
}
}
</style>

@ -0,0 +1,311 @@
<!--地址弹窗页面-->
<script lang="ts" setup>
import { getCityListApi, addressAddApi, agreementInfoApi, addressEditApi } from '~/server/userApi'
import feedback from '~/utils/feedback'
import { ref, reactive, watch, toRefs } from 'vue'
import { AddressInfo } from '~/types/user'
import { addressDefault } from '~/pages/users/defaultUser'
const props = defineProps({
//
addressInfo: {
type: Object,
default: null,
},
//
selAddressData: {
type: Array,
default: [],
},
//
isShowDialog: {
type: Boolean,
default: false,
},
})
const { addressInfo, selAddressData, isShowDialog } = toRefs(props)
const dialogVisibleAddress = computed({
get: () => isShowDialog?.value,
set: (value) => {},
})
//
const isShowSelect = ref(false)
//
const loading = ref(false)
//
const loadingAddress = ref(false)
//
const selectedAddress = ref<any[]>([])
//
const selectedIndex = ref<number>(0)
//
const cityData = reactive({
parentId: 0,
step: 1,
list: [],
})
//
watch(
() => selAddressData,
(value) => {
selectedAddress.value = value?.value
},
{
immediate: true,
},
)
//
const formData = reactive<AddressInfo>(addressInfo ? Object.assign(addressInfo.value) : addressDefault())
const { DialogOpen, DialogClose } = useDialog()
//
defineExpose({ DialogOpen })
//
const handleChangeAddress = async () => {
isShowSelect.value = !isShowSelect.value
await getCityList(1, 1)
}
//
const handleChangeProvince = async (item: any, index: number) => {
cityData.list = []
isShowSelect.value = !isShowSelect.value
if (index == selectedIndex.value) {
return
}
await getCityList(item.parentId, item.regionType)
selectedIndex.value = index
}
//
const CACHE_ADDRESS = reactive<any>({})
const getCityList = async (parentId: number, regionType: any) => {
if (CACHE_ADDRESS[parentId]) {
cityData.list = CACHE_ADDRESS[parentId]
return
}
isShowSelect.value = true
loadingAddress.value = true
await getCityListApi({ parentId: parentId, regionType: regionType })
.then(async (result: any) => {
CACHE_ADDRESS[parentId] = result
cityData.list = result
loadingAddress.value = false
})
.catch(async (err: any) => {
loadingAddress.value = false
})
}
//
const handleChangeCity = async (item: object) => {
if (selectedIndex.value > -1) {
selectedAddress.value.splice(selectedIndex.value + 1, 999)
selectedAddress.value[selectedIndex.value] = item
selectedIndex.value = -1
} else if (item.regionType === 1) {
selectedAddress.value = [item]
} else {
selectedAddress.value.push(item)
}
if (item.isChild) {
await getCityList(item.regionId, item.regionType + 1)
} else {
isShowSelect.value = false
}
}
//
const getData = () => {
formData.province = getAddressVal(0).regionName
formData.provinceId = getAddressVal(0).regionId
formData.city = getAddressVal(1).regionName
formData.cityId = getAddressVal(1).regionId
formData.district = getAddressVal(2).regionName
formData.districtId = getAddressVal(2).regionId
formData.street = getAddressVal(3).regionName
}
const getAddressVal = (num: number) => {
return selectedAddress.value[num]
}
const validate = () => {
if (!formData.realName) {
return feedback.msgWarning('请填写姓名')
}
if (!formData.phone || !/^1(3|4|5|7|8|9|6)\d{9}$/i.test(formData.phone)) {
return feedback.msgWarning('请填写正确的手机号码')
}
if (!selectedAddress.value.length) {
return feedback.msgWarning('请选择省市区')
}
if (!formData.detail) {
return feedback.msgWarning('请填写详细地址')
}
}
//
const handleSubmit = async () => {
if (!formData.realName) {
return feedback.msgWarning('请填写姓名')
}
if (!formData.phone || !/^1(3|4|5|7|8|9|6)\d{9}$/i.test(formData.phone)) {
return feedback.msgWarning('请填写正确的手机号码')
}
if (!selectedAddress.value.length) {
return feedback.msgWarning('请选择省市区')
}
if (!formData.detail) {
return feedback.msgWarning('请填写详细地址')
}
await getData()
loading.value = true
await handlerSave()
}
const handlerSave=async ()=>{
let addressId = 0
try {
loading.value = true
if (formData.id > 0) {
addressId = await addressEditApi(formData)
} else {
addressId = await addressAddApi(formData)
}
feedback.msgSuccess('提交成功')
loading.value = true
dialogVisibleAddress.value = false
await handleEmit(addressId)
} catch (err) {
loading.value = false
}
}
const handleEmit = (addressId: number) => {
emit('handleSubmitAddress', addressId)
}
const emit = defineEmits(['handleSubmitAddress', 'handleSubmitClose'])
//
const handleClose = async () => {
emit('handleSubmitClose')
isShowSelect.value = false
loading.value = false
}
</script>
<template>
<el-dialog
class="user-login-dialog"
v-model="dialogVisibleAddress"
:align-center="true"
width="690px"
:append-to-body="true"
:show-close="false"
:title="formData.id > 0 ? '编辑收货地址' : '添加收货地址'"
center
:close-on-click-modal="false"
>
<div class="form-box">
<div class="input-item mbtom20" style="width: 48%; display: inline-block">
<el-input class="iptHeight" v-model="formData.realName" maxlength="25" placeholder="姓名"></el-input>
</div>
<div class="input-item mbtom20" style="width: 48%; display: inline-block; margin-left: 3%">
<el-input class="iptHeight" v-model="formData.phone" placeholder="手机号"></el-input>
</div>
<div class="input-item text-wrapper mbtom20">
<div @click="handleChangeAddress" v-if="!selectedAddress.length">请选择省///街道</div>
<div v-if="selectedAddress.length" style="color: #333">
<span v-for="(item, index) in selectedAddress" class="mr-7px" @click="handleChangeAddress" :key="index">
{{ item.regionName }}
</span>
</div>
<div class="select-wrapper" v-if="isShowSelect">
<div class="borderBotSol" v-loading="loadingAddress">
<div class="acea-row borderBotSol">
<div
class="title-box acea-row"
@click="handleChangeProvince(item, index)"
v-for="(item, index) in selectedAddress"
:key="index"
>
{{ item.regionName ? item.regionName : '请选择' }}
<span v-if="index !== selectedAddress.length - 1" class="ml-2px mr-2px">/</span>
</div>
</div>
<div class="label-txt">
<span
v-for="(item, index) in cityData.list"
:key="index"
@click.stop="handleChangeCity(item)"
:class="{ on: cityData.parentId == item.regionId }"
>{{ item.regionName }}</span
>
</div>
</div>
</div>
</div>
<div class="input-item mbtom20">
<el-input type="textarea" rows="3" v-model="formData.detail" placeholder="详细地址"></el-input>
</div>
<div class="input-item">
<el-checkbox v-model="formData.isDefault">设为默认</el-checkbox>
</div>
</div>
<template #footer class="dialog-footer">
<span class="dialog-footer">
<el-button size="large" @click="handleClose" round> </el-button>
<el-button size="large" color="#E93323" @click="handleSubmit" round :loading="loading"> </el-button>
</span>
</template>
</el-dialog>
</template>
<style lang="scss" scoped>
.iptHeight {
height: 50px;
}
.text-wrapper {
position: relative;
height: 40px;
line-height: 40px;
border: 1px solid #dcdfe6;
padding: 0 15px;
box-sizing: border-box;
border-radius: 4px;
color: #cfcfcf;
.select-wrapper {
z-index: 10;
position: absolute;
left: 0;
top: 45px;
width: 100%;
padding: 0 15px;
background: #fff;
border: 1px solid #e93323;
border-radius: 4px;
line-height: 2;
.title-box {
height: 40px;
line-height: 40px;
color: #e93323;
font-size: 14px;
}
.label-txt {
margin: 8px 0 18px;
color: #666666;
font-size: 14px;
span {
margin-right: 10px;
cursor: pointer;
&.on {
color: #e93323;
}
}
}
}
}
</style>

@ -0,0 +1,120 @@
<!--商品分类-->
<script setup lang="ts">
import { ref, toRefs } from 'vue'
import { categoryTreeApi } from '~/server/goodsApi'
import { linkNavigateTo } from '~/utils/util'
const route = useRoute()
//
const homeRoutePath = route.path
/**
* 平台端商品分类
*/
const categoryCurrent = ref<any[]>([])
const current = ref<number| null>(null)
const { data: productClassify } = await useAsyncData(async () => categoryTreeApi())
/**
* 鼠标移入
* @param idx
*/
const seen = ref<boolean>(false)
const handlerEnter = (idx: number) => {
seen.value = true
current.value = idx
categoryCurrent.value = productClassify.value[idx].childList
}
/**
* 鼠标移出
*/
const emit = defineEmits(['handleSubmitLeave'])
const handlerLeave = () => {
seen.value = false
current.value = null
emit('handleSubmitLeave')
}
const handleEnterKey = (id: number) => {
linkNavigateTo(`/product/product_list`, { productCid: id })
}
</script>
<template>
<div>
<div
class="acea-row absolute z-11 classifyCard overflow-hidden"
:class="homeRoutePath !== '/' ? 'boxShadow' : ''"
@mouseleave="handlerLeave"
>
<el-scrollbar height="376px" class="w-200px px-20px py-17px bg-#FFFFFF">
<div class="classify-left bg-#FFFFFF">
<div v-for="(item, index) in productClassify" :key="item.id" class="flex classifyClassA cursors">
<el-image :src="item.icon" class="w-40px h-40px b-rd-50%"></el-image>
<div class="ml-10px">
<div
@mouseenter="handlerEnter(index)"
class="fonts14 line1 mb-8px w-109px"
:class="index === current ? 'font-color' : 'fontColor333'"
>
{{ item.name }}
</div>
<div class="acea-row">
<div
v-for="(items, idx) in item.childList.length === 2 ? item.childList : item.childList.slice(0, 2)"
:key="index"
class="text-12px text-#999 classifyClassB line1"
>
{{ items.name }}
</div>
</div>
</div>
</div>
</div>
</el-scrollbar>
<div v-if="seen" class="classify-rgiht bg-#FAFAFA w-1000px px-40px py-20px">
<el-scrollbar height="360px">
<div v-for="(item, index) in categoryCurrent" :key="index" class="mb18px acea-row">
<div class="fonts14 fontColor333 mr30px font-500 oppoSans-M">{{ item.name }}</div>
<div class="acea-row w-85%">
<span
@click="handleEnterKey(items.id)"
v-for="(items, idx) in item.childList"
:key="idx"
class="fonts14 fontColor333 mr20px mb-10px cursors oppoSans-R childName"
>{{ items.name }}</span
>
</div>
</div>
</el-scrollbar>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.childName:hover{
color: #e93323;
}
.classifyCard {
background: #fafafa;
border-radius: 0px 0px 16px 16px;
}
.boxShadow {
box-shadow: 0px 1px 6px 0px rgba(0, 0, 0, 0.1);
}
.classifyClassA {
margin-bottom: 24px;
&:last-child {
margin-bottom: 0;
}
}
.classifyClassB {
max-width: 49px;
margin-right: 10px;
&:last-child {
margin-right: 0;
}
}
</style>

@ -0,0 +1,55 @@
<!--下单页中的商品信息-->
<script setup lang="ts">
import { Mul } from '~/utils/util'
import useOrder from '~/composables/useOrder'
import { productTypeFilter } from '~/utils/filter'
const { handlerNuxtLink } = useOrder()
const props = defineProps({
//
list: {
type: Object,
default: null,
},
//:0-1-,2-
productType: {
type: Number,
default: 0,
},
//
// seckillStatus: {
// type: Number,
// default: 0,
// }
// //
// datatime: {
// type: Number,
// default: 0,
// }
})
const { list } = toRefs(props)
</script>
<template>
<div
@click="handlerNuxtLink(list.productId, productTypeFilter(productType))"
class="flex-between-center mbtom30 cursors"
>
<div class="acea-row justify-between">
<el-image :src="list.image" class="backImg w100px h100px b-rd-12px" lazy></el-image>
<div class="ml-20px acea-row flex-col justify-center">
<div class="text-14px fontColor333 font-500 line1 mb14px w-580px oppoSans-M">{{ list.productName }}</div>
<div class="borRadius text-14px fontColor333 line1 oppoSans-R">规格{{ list.sku }}</div>
</div>
</div>
<div class="flex-x-center flex-y-center">
<div v-if="list.price || list.productPrice" class="w-160px text-14px oppoSans-R fontColor333">{{ list.price || list.productPrice }}</div>
<div v-if="list.payNum" class="w-100px text-right text-14px oppoSans-R fontColor333">×{{ list.payNum }}</div>
<div v-if="list.price && list.payNum" class="w-160px text-right font-color text-12px">
<span class="oppoSans-M"></span
><span class="dinProSemiBold text-18px fw-600">{{ Mul(list.price, list.payNum) }}</span>
</div>
</div>
</div>
</template>
<style scoped lang="scss"></style>

@ -0,0 +1,191 @@
<!--倒计时展示 fontSize-->
<template>
<view class="time" :style="justifyLeft">
<text class="text" style="width: auto;">{{ tipText }}</text>
<text class="styleAll p6 is_day" v-if="isDay === true && day>0"
:style="{color:bgColor.bgColor, fontSize:bgColor.fontSize}">{{ day }}{{bgColor.isDay?'天':''}}</text>
<text class="timeTxt" v-if="dayText"
:style="{width:bgColor.timeTxtwidth,color:bgColor.bgColor}"> {{ dayText }} </text>
<text class="styleAll" :class='isCol?"timeCol":""'
:style="{background:bgColor.bgColor,color:bgColor.Color,width:bgColor.width, fontSize:bgColor.fontSize}">{{ hour }}</text>
<text class="timeTxt" v-if="hourText" :class='isCol?"whit":""'
:style="{width:bgColor.timeTxtwidth,color:bgColor.bgColor}"> {{ hourText }} </text>
<text class="styleAll" :class='isCol?"timeCol":""'
:style="{background:bgColor.bgColor,color:bgColor.Color,width:bgColor.width, fontSize:bgColor.fontSize}">{{ minute }}</text>
<text class="timeTxt" v-if="minuteText" :class='isCol?"whit":""'
:style="{width:bgColor.timeTxtwidth,color:bgColor.bgColor}"> {{ minuteText }}</text>
<text class="styleAll" :class='isCol?"timeCol":""'
:style="{background:bgColor.bgColor,color:bgColor.Color,width:bgColor.width,fontSize:bgColor.fontSize}">{{ second }}</text>
<text class="timeTxt" v-if="secondText"> {{ secondText }} </text>
</view>
</template>
<script>
export default {
name: "countDown",
props: {
justifyLeft: {
type: String,
default: ""
},
//
tipText: {
type: String,
default: "倒计时"
},
dayText: {
type: String,
default: "天"
},
hourText: {
type: String,
default: "时"
},
minuteText: {
type: String,
default: "分"
},
secondText: {
type: String,
default: "秒"
},
datatime: {
type: Number,
default: 0
},
isDay: {
type: Boolean,
default: true
},
isCol: {
type: Boolean,
default: false
},
bgColor: {
type: Object,
default: null
},
status: {
type: Number,
default: -1
},
//
dataItem: {
type: Object,
default: null
}
},
data() {
return {
day: "00",
hour: "00",
minute: "00",
second: "00",
intv: undefined
};
},
watch: {
status(nal) {
if (nal !== -1) {
this.intv = undefined
this.show_time();
}
},
datatime(val){
this.show_time();
}
},
mounted() {
this.show_time();
},
methods: {
show_time() {
let that = this;
this.intv = setInterval(function() {
that.runTime()
}, 1000)
},
runTime() {
let that = this;
//
let intDiff = that.datatime - Date.parse(new Date()) / 1000; //
let day = 0,
hour = 0,
minute = 0,
second = 0;
if (intDiff > 0) {
//
if (that.isDay === true) {
day = Math.floor(intDiff / (60 * 60 * 24));
} else {
day = 0;
}
hour = Math.floor(intDiff / (60 * 60)) - day * 24;
minute = Math.floor(intDiff / 60) - day * 24 * 60 - hour * 60;
second =
Math.floor(intDiff) -
day * 24 * 60 * 60 -
hour * 60 * 60 -
minute * 60;
if (hour <= 9) hour = "0" + hour;
if (minute <= 9) minute = "0" + minute;
if (second <= 9) second = "0" + second;
that.day = day;
that.hour = hour;
that.minute = minute;
that.second = second;
} else {
that.day = "00";
that.hour = "00";
that.minute = "00";
that.second = "00";
that.$emit('stopTime', false, this.dataItem)
clearInterval(that.intv)
}
},
},
beforeDestroy() {
clearInterval(this.intv)
}
};
</script>
<style scoped lang="scss">
.p6 {
padding: 0 8px;
}
.styleAll {
font-size: 22px;
}
.timeTxt {
text-align: center;
display: inline-block;
font-weight: 600;
}
.whit {
color: #fff !important;
}
.time {
display: flex;
justify-content: center;
align-items: center;
}
.red {
color: #fc4141;
margin: 0 2px;
}
.text{
font-size: 10px;
color: #2D2D2D;
line-height: 18px;
}
.timeCol {
color: #E93323;
}
</style>

@ -0,0 +1,163 @@
<!--商户主页优惠券列表我的优惠券-->
<script setup lang="ts">
import { couponTypeFilter } from '~/utils/filter'
import { toRefs } from 'vue'
import { couponReceiveApi } from '~/server/merchantApi'
import { getCouponTime } from '~/utils/util'
import feedback from '~/utils/feedback'
const props = defineProps({
//
list: {
type: Array,
default: [],
},
//
receivedText:{
type: String,
default: '',
},
//使
isShowJump: {
type: Boolean,
default: false,
}
})
const { list, isShowJump, receivedText } = toRefs(props)
const loading = ref<boolean>(false)
//
const handleReceiveCoupon = async (item: any) => {
if (item.isUse) return
loading.value = true
try {
await couponReceiveApi(item.id)
feedback.msgSuccess('领取成功')
item.isUse = !item.isUse
loading.value = false
} catch (e) {
loading.value = false
}
}
//使
import { linkNavigateTo } from '~/utils/util'
const handleToUse = async (item: any) => {
if(!isShowJump.value && item.validStr === 'unusable') return
await linkNavigateTo('/activity/coupon_goods_list', { type: 2,couponId: item.id, isUserReceive: true,money:item.money,minPrice:item.minPrice })
}
</script>
<template>
<!-- validStr 优惠券有效状态usable-可用unusable-已用overdue-过期notStart-未开始 || !item.validStr ? 'usable' : ''-->
<div class="acea-row" v-loading="loading">
<div
v-for="item in list"
:key="item.id"
class="coupon-item mb-20px w-306px h-130px flex-between-center pos-relative"
:class="item.validStr ? (item.validStr === 'usable'||item.validStr === 'notStart' ? 'usable' : 'unusable'):'usable'"
>
<div class="w-254px h-130px pl-14px pr-5px">
<div class="flex">
<div class="mt-27px">
<div
class="text-18px flex-y-center mb-6px"
:class="item.validStr ? (item.validStr === 'usable' ? 'font-color' : 'text-#999') : 'font-color'"
>
<span class="mt-13px oppoSans-M"></span><span class="text-34px dinProSemiBold">{{ item.money }}</span>
</div>
<div
class="text-12px left-10px text-center"
>
<span :class="item.validStr ? (item.validStr === 'usable'||item.validStr === 'notStart' ? 'font-color bg-#FFE1DE' : 'text-#999 bg-#EDEDED') : 'font-color bg-#FFE1DE'" class="line-heightOne px-7px py-2px b-rd-8px inline-block">{{ couponTypeFilter(item.category) }}</span>
</div>
</div>
<div class="ml-14px mt-24px">
<div
class="text-14px mb-14px font-500 h-34px lh-18px line2"
:class="item.validStr ? (item.validStr === 'usable' ? 'fontColor333' : 'text-#999') : 'fontColor333'"
>
{{ item.name }}
</div>
<div
class="text-14px"
:class="item.validStr ? (item.validStr === 'usable' ? 'text-#666' : 'text-#999') : 'text-#666'"
>
{{ item.minPrice }} 可用
</div>
</div>
</div>
<!--领券中心时间显示-->
<div v-if="item.validStr">
<div
v-if="item.startTime && item.endTime && (item.validStr === 'usable' || item.validStr ==='notStart')"
class="pos-absolute bottom-15px left-14px text-12px text-#666"
>
{{ getCouponTime(item.startTime, item.endTime) }}
</div>
<div v-if="item.validStr === 'unusable'" class="pos-absolute bottom-15px left-14px text-12px text-#666">
该优惠券已使用
</div>
<div v-if="item.validStr === 'overdue'" class="pos-absolute bottom-15px left-14px text-12px text-#666">
该优惠券已失效无法使用
</div>
</div>
<!--商户主页领券时间显示-->
<div v-else>
<div v-show="item.isFixedTime" class="pos-absolute bottom-15px left-14px text-12px text-#666">
{{ item.useStartTimeStr }} ~ {{ item.useEndTimeStr }} 可用
</div>
<div
v-show="!item.isFixedTime && !item.validStr"
class="pos-absolute bottom-15px left-14px text-12px text-#666"
>
{{ '领取后' + item.day + '天内可用' }}
</div>
</div>
</div>
<div v-if="item.validStr" class="w-50px text-#fff text-16px verticalClass" :class="item.validStr === 'unusable'?'':'cursors'" @click="handleToUse(item)">
{{
item.validStr === 'usable' || item.validStr === 'notStart'
? receivedText
: item.validStr === 'unusable'
? '已使用'
: '已失效'
}}
</div>
<div v-else class="w-50px text-#fff text-16px verticalClass" :class="!item.isUse?'cursors':''" @click="handleReceiveCoupon(item)">
{{ item.isUse ? '已领取' : '立即领取' }}
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.manage {
display: flex;
align-items: center; /* 在交叉轴上居中对齐 */
writing-mode: vertical-rl;
text-orientation: upright;
white-space: nowrap;
}
.coupon {
&-item {
margin-right: 20px;
&:nth-child(3n) {
margin-right: 0;
}
}
.item {
position: relative;
cursor: pointer;
&.on {
color: #e93323;
}
}
.usable {
background-image: url('@/assets/images/mycoupon.png');
}
.unusable {
background-image: url('@/assets/images/mycouponhui.png');
}
}
</style>

@ -0,0 +1,36 @@
/**
*
*/
export const headerMenuListDefault = () => {
return [
{
name: '商品分类',
id: 'classify',
},
{
name: '限时秒杀',
id: 'seckill',
pc_url: '/activity/seckill_list',
},
{
name: '品牌好店',
id: 'merchant',
pc_url: '/merchant/merchant_street',
},
{
name: '领券中心',
id: 'coupon',
pc_url: '/activity/coupon_list',
},
{
name: '订单中心',
id: '1',
pc_url: '/users/order_list',
},
{
name: '资讯信息',
id: 'information',
pc_url: '/activity/information_list',
},
]
}

@ -0,0 +1,44 @@
<!--数据空状态页面-->
<script setup lang="ts">
import {toRefs, reactive} from "vue";
//
const { getWindowHeight } = useScrollHeight()
const ScrollHeight = ref<number>(getWindowHeight() - 176-200)
const props = defineProps({
// resolve
title: {
type: String,
default: '',
},
mTop: {
type: String,
default: '0',
}
})
const { title, mTop } = toRefs(props)
</script>
<template>
<div class="empty-box h-100% m-auto" :style="{ height: ScrollHeight + 'px' }">
<slot name="emptyImage"></slot>
<div class="txt">{{title}}</div>
</div>
</template>
<style scoped lang="scss">
.empty-box{
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
image{
width: 274px;
height: 147px;
}
.txt{
font-size: 13px;
color: #999;
}
}
</style>

@ -0,0 +1,83 @@
<script setup lang="ts">
import {auto} from "@popperjs/core";
</script>
<template>
<div>
<!--黑条-->
<div class="header bg-#F5F5F5" :class="showSeach ? 'headerfixed' : ''">
<div class="headerCon acea-row row-between-wrapper">
<div class="flex-y-center text-12px">
<div class="mr-30px flex-y-center">
<nuxt-link :to="{ path: '/' }" class="flex-y-center"
><span class="iconfont icon-shangchengshouye inline-block"></span>商城首页</nuxt-link
>
</div>
<el-popover
v-if="pcHomeCon && pcHomeCon.goPhoneQrCodeType"
:width="auto"
:minWidth="auto"
popper-style="box-shadow: rgb(14 18 22 / 35%) 0px 10px 38px -10px, rgb(14 18 22 / 20%) 0px 10px 20px -15px; padding: 20px;"
>
<template #reference>
<div class="flex-y-center mobileMall">
<span class="iconfont icon-shoujishangcheng inline-block"></span>手机商城
</div>
</template>
<template #default>
<div class="acea-row">
<div v-if="pcHomeCon.goPhoneQrCodeType.includes('1')">
<div :class="pcHomeCon.goPhoneQrCodeType.includes('2')?'mr-26px':''">
<div class="borderSol-eee w-80px h-80px flex-center b-rd-4px mb-10px">
<el-image :src="wechatQrcode" class="w-72px h-72px"></el-image>
</div>
<div class="font-400 text-12px text-#333 w-80px text-center">小程序商城</div>
</div>
</div>
<div v-if="pcHomeCon.goPhoneQrCodeType.includes('2')">
<div class="borderSol-eee w-80px h-80px flex-center b-rd-4px mb-10px">
<qrcode-vue :value="indexDomain" :size="72" level="H" />
</div>
<div class="font-400 text-12px text-#333 w-80px text-center">H5商城</div>
</div>
</div>
</template>
</el-popover>
</div>
<div class="user acea-row row-middle">
<div v-if="!userStore.isLogin" class="item" @click="handlerLogin">登录/注册</div>
<div v-else class="acea-row row-middle" @click.stop="handlerNuxtLink('/users/user_info', 11)">
<span class="line1 font-color" style="max-width: 135px">Hi{{ userStore.userInfo.nikeName }}</span>
<p class="ml-10px item" @click.stop="handleHeaderLogout">退出</p>
</div>
<div class="item" @click="handlerNuxtLink('/users/order_list', 1)">我的订单</div>
<div class="item" @click="handlerNuxtLink('/users/collect_products', 3)">我的收藏</div>
<el-dropdown class="user">
<span class="el-dropdown-link text-12px item">
商户入驻
<el-icon class="el-icon--right">
<arrow-down />
</el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item class="text-12px" @click="handlerNuxtLink('/merchant/merchant_settled', 0)"
>商户入驻</el-dropdown-item
>
<el-dropdown-item class="text-12px" @click="handlerNuxtLink('/merchant/application_record', 0)"
>申请记录</el-dropdown-item
>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
</style>

@ -0,0 +1,48 @@
<script setup lang="ts">
import { toRefs, watch, ref } from 'vue'
import {useAppStore} from "~/stores/app";
const useStore = useAppStore()
const props = defineProps({
//
pcHomeCon: {
type: Object,
default: null,
}
})
const { pcHomeCon } = toRefs(props)
//广
const isShowAdvertisement = ref<boolean>(true)
</script>
<template>
<div>
<div class="relative wrapper_1200">
<span @click="isShowAdvertisement=false" v-if="isShowAdvertisement" class="iconfont icon-weitongguo1 pos-absolute z-10 text-#9EA9AD cursors right-0px top-20px" style="font-size: 22px"></span>
</div>
<div class="advertisement" :class="isShowAdvertisement ? 'on' : ''">
<a v-if="pcHomeCon.advertisement.linkUrl" :href="pcHomeCon.advertisement.linkUrl" class="w-100% h-auto" target="_blank">
<img :src="pcHomeCon.advertisement.imageUrl" class="w-100% h-auto">
</a>
<img v-else :src="pcHomeCon.advertisement.imageUrl" class="w-100% h-auto">
</div>
</div>
</template>
<style scoped lang="scss">
.advertisement{
width: 100%;
min-width: 1200px;
position: fixed;
line-height: 0;
transition: all 0.3s cubic-bezier(0.25, 0.5, 0.5, 0.9);
transform: translate3d(0, -100%, 0);
top: 0;
left: 0;
&.on {
transform: translate3d(0, 0, 0);
position: static;
}
}
</style>

@ -0,0 +1,152 @@
<script setup lang="ts">
//
import {ref} from "vue";
import {useAppStore} from "~/stores/app";
const props = defineProps({
//
pcHomeCon: {
type: Object,
default: null,
},
})
const { pcHomeCon } = toRefs(props)
const { getFooterIsIntersecting } = useAppStore()
const targetElement = ref(null);
onMounted(() => {
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
//
getFooterIsIntersecting(true)
} else {
getFooterIsIntersecting(false)
//
}
});
});
if (targetElement.value) {
observer.observe(targetElement.value);
}
});
</script>
<!--页面底部-->
<template>
<div v-if="pcHomeCon" class="layoutFooter mt-20px" ref="targetElement">
<div
v-if="pcHomeCon.philosophyList && pcHomeCon.philosophyList.length"
class="w-100% h-90px bg-#FFFFFF"
style="border-top: 1px solid #f5f5f5;"
>
<div class="wrapper_1200 h-90px flex-between-center">
<div v-for="item in pcHomeCon.philosophyList" :key="item.id">
<div class="flex-y-center">
<img :src="item.imageUrl" class="w-40px h-40px b-rd-50% mr-14px" />
<div class="text-16px text-#333 font-500">{{ item.name }}</div>
</div>
</div>
</div>
</div>
<!-- 底部黑色模块 -->
<div class="py-40px bg-#333333">
<div class="wrapper_1200 flex justify-between">
<!-- 左边 -->
<div class="color-#FFFFFF l-box">
<div v-for="(val, index) in pcHomeCon.quickEntryList" :key="index">
<div class="l-title" @click="openUrl(val.linkUrl)"><span class="font-500 oppoSans-M">{{ val.name }}</span></div>
<div class="l-tips" v-for="(tip, index) in val.linkList" :key="index">
<a v-if="tip.linkUrl" :href="tip.linkUrl" target="_blank" class="font-400 oppoSans-R"> {{ tip.name }}</a>
<span v-else class="font-400 oppoSans-R">{{ tip.name }}</span>
</div>
</div>
</div>
<!-- 右边 -->
<div class="flex">
<div class="code-r" v-for="(code, index) in pcHomeCon.qrCodeList">
<div class="code">
<img :src="code.imageUrl" alt="" />
</div>
<span class="mt-12px">{{ code.name }}</span>
</div>
</div>
</div>
<!-- 分割线 -->
<div class="wrapper_1200 divisionLine"></div>
<!-- 企业信息 -->
<div v-if="pcHomeCon.friendlyLinkList.length" class="wrapper_1200 flex-x-center text-s mb-40px">
<div class="w-953px flex flex-justify-center flex-wrap lh-19px">
<div class="mr-15px text-#999" v-for="item in pcHomeCon.friendlyLinkList">
<a style="color: #999999" v-if="item.linkUrl" :href="item.linkUrl" target="_blank"> {{ item.name }}</a>
<span v-else>{{ item.name }}</span>
</div>
</div>
</div>
<div class="min_wrapper_1200 flex-col flex-items-center flex-x-center text-s">
<div>
<span v-if="pcHomeCon.phone" class="mr-20px">联系电话:{{ pcHomeCon.phone }}</span> <span v-if="pcHomeCon.address">地址{{ pcHomeCon.address }}</span>
</div>
<div class="mt-17px">{{ pcHomeCon.authInfo }}<a style="color: #999999" href="https://beian.mps.gov.cn/#/query/webSearch" target="_blank"> {{ pcHomeCon.filingNum }}</a></div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.l-box {
display: grid;
grid-template-columns: repeat(6,minmax(0,1fr));
grid-template-rows: none;
grid-gap: 40px;
font-weight: 400;
font-family: PingFang SC-Regular, PingFang SC;
.l-title {
font-size: 16px;
margin-bottom: 26px;
}
.l-tips {
cursor: pointer;
margin-bottom: 14px;
color: #cccccc !important;
font-size: 14px;
a{
color: #cccccc !important;
}
}
}
.code-r {
color: #ffffff;
display: flex;
flex-direction: column;
align-items: center;
font-weight: 400;
font-size: 14px;
font-family: PingFang SC-Regular, PingFang SC;
.code {
width: 100px;
height: 100px;
background-color: #fff;
img {
width: 100%;
height: 100%;
}
}
}
.code-r:nth-child(1) {
margin-right: 40px;
}
.divisionLine {
margin-top: 10px;
border: 0;
border-top: 2px dashed rgba(255, 255, 255, 0.15);
margin-bottom: 40px;
}
.text-s {
font-size: 12px;
font-family: PingFang SC-Regular, PingFang SC;
font-weight: 400;
color: #999999;
}
</style>

@ -0,0 +1,487 @@
<!--页面头部-->
<template>
<div class="relative w-100%">
<!--黑条-->
<div class="header bg-#F5F5F5" :class="isShowTop && useApp.isHomePage ? 'headerfixed' : ''">
<div class="headerCon acea-row row-between-wrapper">
<div class="flex-y-center text-12px">
<div class="mr-30px flex-y-center">
<nuxt-link :to="{ path: '/' }" class="flex-y-center"
><span class="iconfont icon-shangchengshouye inline-block"></span>商城首页</nuxt-link
>
</div>
<el-popover
v-if="pcHomeCon && pcHomeCon.goPhoneQrCodeType"
:width="auto"
:minWidth="auto"
popper-style="box-shadow: rgb(14 18 22 / 35%) 0px 10px 38px -10px, rgb(14 18 22 / 20%) 0px 10px 20px -15px; padding: 20px;"
>
<template #reference>
<div class="flex-y-center mobileMall">
<span class="iconfont icon-shoujishangcheng inline-block"></span>手机商城
</div>
</template>
<template #default>
<div class="acea-row">
<div v-if="pcHomeCon.goPhoneQrCodeType.includes('1')">
<div :class="pcHomeCon.goPhoneQrCodeType.includes('2')?'mr-26px':''">
<div class="borderSol-eee w-80px h-80px flex-center b-rd-4px mb-10px">
<el-image :src="wechatQrcode" class="w-72px h-72px"></el-image>
</div>
<div class="font-400 text-12px text-#333 w-80px text-center">小程序商城</div>
</div>
</div>
<div v-if="pcHomeCon.goPhoneQrCodeType.includes('2')">
<div class="borderSol-eee w-80px h-80px flex-center b-rd-4px mb-10px">
<qrcode-vue :value="indexDomain" :size="72" level="H" />
</div>
<div class="font-400 text-12px text-#333 w-80px text-center">H5商城</div>
</div>
</div>
</template>
</el-popover>
</div>
<div class="user acea-row row-middle">
<div v-if="!userStore.isLogin" class="item" @click="handlerLogin">登录/注册</div>
<div v-else class="acea-row row-middle" @click.stop="handlerNuxtLink('/users/user_info', 11)">
<span class="line1 font-color" style="max-width: 135px">Hi{{ userStore.userInfo.nikeName }}</span>
<p class="ml-10px item" @click.stop="handleHeaderLogout">退出</p>
</div>
<div class="item" @click="handlerNuxtLink('/users/order_list', 1)">我的订单</div>
<div class="item" @click="handlerNuxtLink('/users/collect_products', 3)">我的收藏</div>
<el-dropdown class="user">
<span class="el-dropdown-link text-12px item">
商户入驻
<el-icon class="el-icon--right">
<arrow-down />
</el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item class="text-12px" @click="handlerNuxtLink('/merchant/merchant_settled', 0)"
>商户入驻</el-dropdown-item
>
<el-dropdown-item class="text-12px" @click="handlerNuxtLink('/merchant/application_record', 0)"
>申请记录</el-dropdown-item
>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</div>
<!--菜单/搜索框/菜单-->
<div class="w-100% bg-white">
<!--搜索框-->
<div :class="isShowTop && useApp.isHomePage ? 'menufixed' : ''" class="w-100% bg-white">
<div class="min_wrapper_1200">
<div class="flex flex-justify-between pt-30px">
<!--logo-->
<nuxt-link :to="{ path: '/' }"
><el-image lazy :src="pcHomeCon?.leftTopLogo" class="w100px h36px"></el-image
></nuxt-link>
<!--搜索框商品详情页商户主页-->
<div v-if="useApp.routePath === '/merchant/merchant_home' || useApp.routePath.includes('product/detail')" class="acea-row">
<div class="w-560px ml-5px">
<el-input
v-model="searchVal"
placeholder="搜索全站/本店"
class="input-with-select"
@keyup.enter="handleEnterKey"
>
<template #append>
<div @click="handleEnterKey" class="cursors w-80px h-40px bg-color b-rd-20px text-center lh-40px">
<span class="text-#fff text-14px font-400">搜全站</span>
</div>
</template>
</el-input>
</div>
<div @click="handleEnterKeyHome" class="cursors w-80px h-40px bg-#333333 b-rd-20px text-center lh-40px ml-10px">
<span class="text-#fff text-14px font-400">搜本店</span>
</div>
</div>
<!--搜索框-->
<div v-else class="w-560px ml-5px">
<el-input
v-model="searchVal"
placeholder="搜索商品/店铺"
class="input-with-select"
@keyup.enter="handleEnterKey"
>
<template #prepend>
<el-select v-model="selectVal" placeholder="Select" style="width: 115px">
<el-option label="商品" value="1" />
<el-option label="店铺" value="2" />
</el-select>
</template>
<template #append>
<div @click="handleEnterKey" class="cursors w-80px h-40px bg-color b-rd-20px text-center lh-40px">
<span class="iconfont icon-sousuo text-#fff font-400"></span>
</div>
</template>
</el-input>
</div>
<!--购物车-->
<div
@click="handlerNuxtLink('/order/shopping_cart', 0)"
class="h-40px borderSol b-rd-20px font-color text-14px flex-center mb-26px cursors w-140px mr-5px"
>
<span class="iconfont icon-gouwuche mr-10px"></span>购物车({{ useApp.carNumber }})
</div>
</div>
</div>
</div>
<!--菜单分类-->
<div v-if="useApp.isShowHeaderMenu" class="w-100% bg-white header-menu">
<div class="min_wrapper_1200">
<div @mouseenter="handlerMenuEnter" @mouseleave="handleSubmitLeave">
<!--菜单分类-->
<div class="menu flex-y-center">
<div
v-for="(item, index) in menuList"
:key="item.id"
@mouseenter="handlerMenuEnter(index)"
@click="handleGoPage(item, index)"
:class="index === 0 ? 'classification' : current===item.id ? 'font-color':''"
class="cursor-pointer menu-item fontColor333 text-16px mr-40px font-500 oppoSans-M"
>
<span v-show="index === 0" class="iconfont icon-shangpinfenlei mr-8px"></span>{{ item.name }}
</div>
</div>
<!--分类显示 -->
<div v-if="showMenu" class="absolute w-1200px yop-179px z-11" :class="showSeach ? 'on' : ''">
<div class="classify z-8">
<!--分类组件-->
<classify-card @handleSubmitLeave="handleSubmitLeave"></classify-card>
</div>
</div>
</div>
</div>
</div>
</div>
<!--登录弹窗-->
<user-login ref="userLoginRef" @on-login-succeeded="onLoginSucceeded"></user-login>
</div>
</template>
<script setup lang="ts">
import { headerMenuListDefault } from '~/components/defaultComponents'
import { ArrowDown } from '@element-plus/icons-vue'
import {b64toBlob, Debounce, linkNavigateTo} from '~/utils/util'
import { loginLogout } from '~/server/userApi'
import feedback from '~/utils/feedback'
defineOptions({ name: 'layoutHeader' })
import { useUserStore } from '@/stores/user'
import {onMounted, ref, watch} from 'vue'
import { searcKeywordApi } from '~/server/goodsApi'
import { useAppStore } from '~/stores/app'
import QrcodeVue from 'qrcode.vue'
import { indexDomainApi, wechatQrcodeApi } from '~/server/homeApi'
import {auto} from "@popperjs/core";
const userStore = useUserStore()
const useApp = useAppStore()
const route = useRoute()
/**
* 监听页面滚动
*/
onMounted(() => {
window.addEventListener('scroll', handleScroll)
})
const isShowTop = ref<boolean>(false)
const handleScroll = () => {
const t = document.documentElement.scrollTop || document.body.scrollTop
if (t > 100) {
isShowTop.value = true
} else {
isShowTop.value = false
}
}
//
const props = defineProps({
pcHomeCon: {
type: Object,
default: null,
},
})
const { pcHomeCon } = toRefs(props)
//
const { data: indexDomain } = useAsyncData(() => indexDomainApi())
//
const wechatQrcode = ref<string>('')
const getWechatQrcode = async () => {
let data = await wechatQrcodeApi({
scene: 'id=0',
path: 'pages/index/index',
env_version: 'trial',
})
let blob = b64toBlob(data.code)
wechatQrcode.value = URL.createObjectURL(blob)
}
getWechatQrcode();
//
const searchVal = ref<string>('') //
const selectVal = ref<string>('1') //
searchVal.value = route.query.searchValue ? route.query.searchValue.toString() : searchVal.value.toString()
onMounted(() => {
selectVal.value = '1'
});
//
const handleEnterKeyHome = async()=>{
linkNavigateTo(`/merchant/merchant_home`, { merId: useApp.pcMerId, searchValue: searchVal.value, currentVal: 1 })
}
//
const handleEnterKey = async () => {
//useApp.routePath === '/merchant/merchant_home' || useApp.routePath.includes('product/detail')
if(useApp.routePath === '/merchant/merchant_home'|| useApp.routePath.includes('product/detail')){
await linkNavigateTo(`/product/product_list`, { searchValue: searchVal.value })
}else{
if (selectVal.value === '1') {
await linkNavigateTo(`/product/product_list`, { searchValue: searchVal.value })
} else {
await linkNavigateTo(`/merchant/merchant_list`, { searchValue: searchVal.value })
}
}
showSeach.value = !showSeach.value
showMenu.value = false
}
//
const menuList = ref<ItemObject[]>(headerMenuListDefault())
//
const { $aos } = useNuxtApp()
onMounted(() => {
$aos().init({
easing: 'ease-out-back',
duration: 1000,
})
})
//
const onLoginSucceeded = () => {
// getCartCount()
}
/**
* 热门搜索词
*/
const showSeach = ref<boolean>(false)
const { data: searcKeywordList } = await useAsyncData(async () => searcKeywordApi())
//
const nuxtApp = useNuxtApp()
nuxtApp.provide('onClearSearchVal', () => {
searchVal.value = ''
})
/**
* 菜单移入
*/
const checkedIndex = ref<number>(10) //
const showMenu = ref<boolean>(false) //
const handlerMenuEnter = (idx: number | undefined) => {
checkedIndex.value = idx ? idx : 10
if (idx === 0 && !useApp.isHomePage) {
showMenu.value = true
} else {
showMenu.value = false
}
}
//
const handleSubmitLeave = () => {
showMenu.value = false
}
//
const current = ref<string>(route.query.type?<string>route.query.type:'')
watch(
() => <string>route.query.type,
(newValue) => {
current.value = newValue
},
)
const handleGoPage = async (obj: any, idx: number) => {
current.value = obj.id
if (idx === 0) return
await linkNavigateTo(obj.pc_url, { type: obj.id })
}
/**
* 登录
*/
const userLoginRef = shallowRef()
const handlerLogin = async () => {
if (userStore.isLogin) {
// await linkNavigateTo('/forum/create')
} else {
//data.backUrl = '/forum/create'
userLoginRef.value.open()
}
}
/**
* 地址跳转
*/
const handlerNuxtLink = async (url: string, type: number) => {
if (userStore.token) {
await linkNavigateTo(url, { type: type })
} else {
userLoginRef.value.open()
}
}
//退
//0
const { getCarNumber } = useAppStore()
const handleHeaderLogout = Debounce(async () => {
await feedback.confirm('确定退出吗?')
await loginLogout()
await getCarNumber(0)
await userStore.logout()
},500)
</script>
<style scoped lang="scss">
:deep(.el-dropdown-menu__item){
font-size: 12px !important;
}
:deep(.el-dropdown__popper.el-popper), :deep(.el-popper){
font-size: 12px !important;
}
.header-menu {
border-bottom: 2px solid #e93323;
}
.mobileMall{
&:hover {
color: #e93323;
}
}
.classification {
width: 200px;
height: 40px;
background: #e93323;
border-radius: 12px 12px 0px 0px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
}
:deep(.el-input-group) {
height: 40px;
background: #ffffff;
border-radius: 20px 20px 20px 20px;
border: 1px solid #e93323;
overflow: hidden;
}
:deep(.el-input__wrapper) {
padding-left: 20px !important;
}
:deep(.el-input__wrapper),
:deep(.el-input-group__prepend),
:deep(.el-input-group__prepend:hover),
:deep(.el-input-group__append) {
padding: 0;
box-shadow: none !important;
--el-fill-color-light: rgba(255, 255, 255, 0) !important;
}
:deep(.el-input-group__append) {
padding: 0;
}
:deep(.el-input) {
--el-input-hover-border-color: none !important;
--el-input-focus-border-color: none !important;
}
:deep(.el-select) {
margin: 0 !important;
width: 74px !important;
--el-select-input-focus-border-color: none !important;
}
:deep(.el-input-group__prepend:hover) {
border: none;
}
.headerfixed {
top: 0;
z-index: 99;
position: fixed;
}
.menufixed {
top: 40px;
z-index: 99;
position: fixed;
}
.seachfixed {
position: fixed;
top: 124px;
z-index: 99;
}
.classify {
border-radius: 0px 0px 26px 26px;
overflow: hidden;
box-shadow: 0px 1px 6px 0px rgba(0, 0, 0, 0.1);
}
.search {
&-ipt {
outline: none;
border: 0;
background: none;
}
&-history {
border-radius: 0px 0px 16px 16px;
}
}
.header {
flex: 1;
width: 100%;
height: 40px;
font-size: 12px;
color: #666666;
cursor: pointer;
.headerCon {
height: 100%;
position: relative;
max-width: 1200px;
margin: 0 auto;
a {
color: #666666;
&:hover {
color: #e93323;
}
}
.iconfont {
margin-right: 5px;
}
.user {
.item {
margin-right: 8px;
position: relative;
padding-left: 8px;
color: #666666;
&:hover {
color: #e93323;
}
}
}
}
}
</style>

@ -0,0 +1,65 @@
<!--商户列表关注商品店铺街-->
<script setup lang="ts">
import { toRefs } from 'vue'
const props = defineProps({
//
list: {
type: Array,
default: [],
},
//
isShowFollow: {
type: Boolean,
default: false,
},
})
const { list, isShowFollow } = toRefs(props)
const { onUnfollowMerchant } = useMerchant()
const emit = defineEmits(['handleSubmitSuccess'])
const handleCancel = (merId: number) => {
onUnfollowMerchant(merId).then(() => {
emit('handleSubmitSuccess')
})
}
const colors = ['#E93323', '#E93323', '#EEEEEE']
//
import { linkNavigateTo } from '~/utils/util'
const handleGoPage = (id) => {
linkNavigateTo(`/merchant/merchant_home`, { merId: id })
}
const isNthChild = (index: number) => {
return index % 5 === 0
}
</script>
<template>
<div v-for="(item, index) in list" :key="index" class="mb-15px mer-item cursors">
<div
class="w-228px acea-row flex-col b-rd-16px items-center overflow-hidden pb-20px borderSol-eee bg-#FFFFFF"
@click="handleGoPage(item.id || item.merId)"
>
<el-image class="w-228px h-228px" :src="item.pcGoodStoreCoverImage" lazy></el-image>
<el-image class="w-80px h-80px b-rd-50% mb-18px borderSol-eee" :src="item.pcLogo" style="margin-top: -36px;"></el-image>
<div class="flex-center mb-8px w-212px line1">
<div v-if="item.isSelf" class="text-12px bg-color text-#fff b-rd-2px py-2px px-4px mr-6px lh-12px">自营</div>
<div v-else class="h-16px"></div>
<div class="fontColor333 text-16px line1">{{ item.merName || item.name }}</div>
</div>
<div v-if="item.starLevel"><el-rate v-model="item.starLevel" disabled/></div>
<div
v-if="isShowFollow"
@click.stop="handleCancel(item.merId)"
class="mt-10px w-86px h-30px lh-29px text-12px font-color text-center b-rd-16px cursors borderSolE9"
>
取消关注
</div>
</div>
</div>
</template>
<style scoped lang="scss">
</style>

@ -0,0 +1,141 @@
<!--商户头像 资质 星级等-->
<script setup lang="ts">
import { toRefs } from 'vue'
const props = defineProps({
//
merchantInfo: {
type: Object,
default: null,
},
//id
merId: {
type: Number,
default: 0,
},
//使,productDetail:使
fromType: {
type: String,
default: '',
},
})
const { merchantInfo,merId,fromType } = toRefs(props)
const useMerchants = useMerchant()
//
const emit = defineEmits(['handleSubmitSuccess'])
const handleFollow = () =>{
if(merchantInfo.value.isCollect){
//
useMerchants.onUnfollowMerchant(merId?.value).then(() => {
emit('handleSubmitSuccess')
})
}else{
//
useMerchants.onfollowMerchant(merId?.value).then(() => {
emit('handleSubmitSuccess')
})
}
}
//
import { linkNavigateTo } from '~/utils/util'
import useOrder from "~/composables/useOrder";
const handleGoPage = () => {
linkNavigateTo(`/merchant/merchant_home`, { merId: merId.value })
}
//
const { bool: dialogVisible, DialogOpen, DialogClose } = useDialog()
//
const { chatConfig } = useOrder()
const handleChat = async () => {
let chatConfigData = {
serviceLink:merchantInfo?.value.serviceLink,
servicePhone:merchantInfo?.value.servicePhone,
serviceType:merchantInfo?.value.serviceType
}
await chatConfig(chatConfigData)
}
</script>
<template>
<div>
<div class="w-210px b-rd-12px bg-#FFFFFF overflow-hidden flex-col px-20px py-20px merchantNews">
<el-image @click="handleGoPage" class="w-60px h-60px b-rd-50%" :class="fromType ==='productDetail'?'cursors':''" :src="merchantInfo.avatar" lazy style="margin: 0 auto"></el-image>
<div @click="handleGoPage" class="flex-center w-170px mt-16px mb-20px" :class="fromType ==='productDetail'?'cursors':''">
<div v-if="merchantInfo.isSelf" class="text-12px bg-color text-#fff b-rd-2px py-2px mr-6px px-4px lh-12px min-w-33px">自营</div>
<div class="fontColor333 text-14px">{{ merchantInfo.name }}</div>
</div>
<div v-if="fromType !=='productDetail'" @click="DialogOpen()" class="text-12px text-#999 mb-15px cursors">资质证照:<img src="@/assets/images/zhizhao.png"></div>
<div class="text-12px text-#999 flex-y-center">店铺星级<el-rate size="small" v-model="merchantInfo.starLevel" disabled /></div>
<el-divider border-style="dashed" />
<div class="acea-row">
<div v-if="fromType ==='productDetail'" class="cursors w-79px h-30px lh-29px borderSol text-12px text-#333 text-center b-rd-15px mr-10px" @click="handleGoPage">进店逛逛</div>
<div v-else @click="handleChat" class="cursors w-79px h-30px lh-29px borderSol text-12px text-#333 text-center b-rd-15px mr-10px">联系客服</div>
<div @click="handleFollow" :class="merchantInfo.isCollect?'borderSol text-#333':'borderSolE9 font-color'" class="w-79px h-30px lh-29px text-12px text-center b-rd-15px cursors">{{merchantInfo.isCollect?'已关注':'关注店铺'}}</div>
</div>
</div>
<el-dialog
class="merchantNewsDialog"
v-model="dialogVisible"
:append-to-body="true"
:show-close="false"
center
align-center
width="532px"
title="资质证照"
>
<span class="closeBtn iconfont icon-danchuangguanbi" @click="DialogClose()"></span>
<div class="">
<el-carousel :interval="5000" height="340px" arrow="always" class="w-100% m-auto">
<el-carousel-item v-for="item in JSON.parse(merchantInfo.qualificationPicture)" :key="item" class="w-340px h-340px el-image">
<el-image :src="item" class="w-340px h-340px el-image b-rd-12px" fit="fill" />
</el-carousel-item>
</el-carousel>
</div>
</el-dialog>
</div>
</template>
<style>
.merchantNews{
:deep(.el-dialog__header){
padding: 30px 0 !important;
}
}
</style>
<style scoped lang="scss">
//
:deep(.el-rate){
height:auto !important;
--el-rate-icon-margin: 0 !important;
--el-rate-fill-color:#E93323 !important;
--el-rate-icon-size: 12px !important;
}
.merchantNewsDialog{
.el-carousel__item{
width: 340px !important;
left: 71px !important;
}
}
.closeBtn {
color: #fff;
position: absolute;
top: -33px;
right: -33px;
text-align: center;
font-size: 24px;
cursor: pointer;
}
.merchantNews{
:deep(.el-divider--horizontal){
margin: 15px 0 15px 0 !important;
}
}
</style>

@ -0,0 +1,23 @@
<!--没登录页面展示-->
<script setup lang="ts">
//layouts
const nuxtApp = useNuxtApp()
//
const { getWindowHeight } = useScrollHeight()
const ScrollHeight = ref<number>(getWindowHeight() - 215)
</script>
<template>
<!--未登录页面-->
<div class="error-page w-1000px h-100% flex ml-20px" :style="{ height: ScrollHeight + 'px' }">
<div class="m-auto">
<img src="@/assets/images/cuowu.png">
<div class="text-#333 text-24px mt-35px text-center mb-26px">请登录查看</div>
<div class="handleBtnBorder w-100px h-40px text-14px lh-40px m-auto cursors" @click="nuxtApp.$onHandlerLogin()">登录</div>
</div>
</div>
</template>
<style scoped lang="scss">
</style>

@ -0,0 +1,70 @@
<!--商品信息订单列表评论列表中展示-->
<script setup lang="ts">
import { toRefs } from 'vue'
import {orderProductTypeFilter, productTypeFilter} from '~/utils/filter'
import useOrder from '~/composables/useOrder'
const { handlerNuxtLink } = useOrder()
const props = defineProps({
//
list: {
type: Object,
default: null,
},
refundStatus: {
type: Number,
default: null,
},
})
const { list,refundStatus } = toRefs(props)
//
const handlerNuxtLinks = async (list: any) => {
if(refundStatus.value && refundStatus.value===-1) {
await handlerNuxtLink(list.productId, orderProductTypeFilter(list.productType))
}
}
//
const emit = defineEmits(['onHandlerToRecord'])
const handlerToRecord = async (orderNo:string)=>{
emit('onHandlerToRecord',orderNo)
}
</script>
<template>
<div class="flex-between-center orderProduct cursors">
<div
class="acea-row justify-between"
@click="handlerNuxtLinks(list)"
>
<el-image :src="list.image" class="backImg w100px h100px b-rd-12px" lazy></el-image>
<div class="ml-20px acea-row flex-col justify-center">
<div class="text-14px fontColor333 line1 mb14px w-635px font-500 oppoSans-M">{{ list.productName }}</div>
<div class="borRadius text-14px fontColor333 line1 mb-15px font-400 oppoSans-R">规格{{ list.sku }}</div>
<div class="acea-row">
<div v-if="list.price && list.payNum" class="text-14px fontColor333 oppoSans-R mr-20px">
{{ list.price }} ×{{ list.payNum }}
</div>
<div @click.stop="handlerToRecord(list.orderNo)" v-if="(list.applyRefundNum+list.refundNum) && refundStatus === -1" class="text-14px font-color oppoSans-R mr-20px flex-y-center">
{{ list.applyRefundNum+list.refundNum }}件商品已申请售后 <span class="iconfont icon-gengduo"></span>
</div>
<div v-if="list.applyRefundNum && refundStatus > -1" class="text-14px fontColor333 oppoSans-R mr-20px">
申请数量{{ list.applyRefundNum }}
</div>
<div v-if="list.afterSalesType" class="text-14px font-color oppoSans-R mr-20px">
{{ list.afterSalesType === 1 ? '仅退款' : '退货退款' }}
</div>
<div v-if="list.refundStatus === 3" class="text-14px font-color">
已退款{{ list.refundPrice }}
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.orderProduct {
margin-bottom: 20px;
}
</style>

@ -0,0 +1,39 @@
<!--页面中头部展示标题使用-->
<script setup lang="ts">
import { toRefs } from 'vue'
import { linkNavigateTo } from '~/utils/util'
const props = defineProps({
//
title: {
type: String,
default: '',
},
//
menuTitle:{
type: String,
default: '',
},
navigateToTitle: {
type: Object,
default: null,
}
})
const { title } = toRefs(props)
const router = useRouter();
const goBack =()=>{
router.back(); // 退
}
</script>
<template>
<div class="mt-20px mb-20px">
<span class="fontColor333 fonts14 cursors" @click="linkNavigateTo('/')">首页 </span>
<span v-if="navigateToTitle" class="fontColor333 fonts14 cursors" @click="linkNavigateTo(navigateToTitle.linkUrl, {type: 0})"> > {{navigateToTitle.title}}</span>
<span v-if="menuTitle" class="fontColor333 fonts14 cursors" @click="goBack"> > {{menuTitle}}</span>
<span class="text-#666 fonts14"> > {{title}}</span>
</div>
</template>
<style scoped lang="scss"></style>

@ -0,0 +1,64 @@
<!--商品列表推荐商品首页商品展示-->
<script setup lang="ts">
import {linkNavigateTo} from "~/utils/util";
import { toRefs } from 'vue'
const props = defineProps({
//
item: {
type: Object,
default: null,
},
//
sizeData: {
type: Object,
default: {
width: '228px',
height: '228px',
},
}
})
const { item, sizeData } = toRefs(props)
/**
* 地址跳转
*/
const handlerNuxtLink = async (id: number)=>{
await linkNavigateTo(`/product/detail/${id}`,{type: 'normal'})
}
</script>
<template>
<div class="productItem cursors">
<div data-aos-anchor-placement="top-bottom" @click="handlerNuxtLink(item.id)">
<div class="borRadius bg-#FFFFFF" :style="{width:sizeData.width}">
<el-image class="backImg" :src="item.image" :style="{width:sizeData.width,height:sizeData.height}"></el-image>
<div class="bg-white px15px py-14px content h-132px flex flex-justify-between" style="margin-top: -5px;flex-direction: column;">
<div class="line2 text-14px fontColor333 mb15px products-name h-36px lh-19px acea-row">
<div v-if="item.productTags && item.productTags.locationLeftTitle.length" class="inline-block text-12px bg-color text-#fff b-rd-2px py-2px mr-6px px-4px line-heightOne">{{item.productTags.locationLeftTitle[0].tagName}}</div>{{item.name}}</div>
<div v-if="item.productTags && item.productTags.locationUnderTitle.length" class="acea-row mb14px">
<div v-for="items in item.productTags.locationUnderTitle.length>3?item.productTags.locationUnderTitle.slice(0,3):item.productTags.locationUnderTitle" :key="items.id" class="text-12px line-heightOne font-color base-border b-color-#E93323 b-rd-2px px4px py2px mr8px productTags">{{items.tagName}}</div>
</div>
<div class="flex-between-center">
<div class="text-12px font-color"><span class="oppoSans-M"></span><span class="dinProSemiBold text-20px lh-19px font-600">{{item.price}}</span></div>
<div class="text-12px text-#999">已售{{item.sales}}{{item.unitName}}</div>
</div>
<div v-if="item.merName" class="mt-15px">
<span class="iconfont icon-guanzhudianpu mr-5px font-color"></span>
<span class="text-12px text-#666">{{item.merName}}</span>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.backImg{
border-top-left-radius: 16px;
border-top-right-radius: 16px;
}
.content{
border-bottom-left-radius: 16px;
border-bottom-right-radius: 16px;
}
</style>

@ -0,0 +1,104 @@
<!--商品列表可用优惠券商品列表商品搜索列表使用-->
<script setup lang="ts">
import { linkNavigateTo } from '~/utils/util'
import { toRefs } from 'vue'
import { Mul } from '~/utils/util'
const props = defineProps({
//
list: {
type: Array,
default: [],
},
//
sizeData: {
type: Object,
default: {
width: '228px',
height: '228px',
},
},
})
const { list, sizeData } = toRefs(props)
/**
* 地址跳转
*/
const handlerNuxtLink = async (id: number) => {
await linkNavigateTo(`/product/detail/${id}`, { type: 'normal' })
}
//
const handleGoPage = (id) => {
linkNavigateTo(`/merchant/merchant_home`, { merId: id })
}
</script>
<template>
<div class="acea-row mt-20px">
<div v-for="item in list" :key="item.id" class="mb15px item cursors">
<div data-aos="fade-up" data-aos-anchor-placement="top-bottom" @click="handlerNuxtLink(item.id)">
<div class="w228px borRadius overflow-hidden bg-#FFFFFF" :style="{ width: sizeData.width }">
<el-image
class="backImg w228px h228px"
:src="item.image"
:style="{ width: sizeData.width, height: sizeData.height }"
></el-image>
<div
class="bg-white pt-15px pb-13px px-15px flex flex-justify-between"
style="flex-direction: column; margin-top: -5px"
>
<div class="text-12px font-color mb-8px font-500">
<span class="oppoSans-M"></span
><span class="text-20px lh-20px font-600 dinProSemiBold">{{ item.price }}</span>
</div>
<div class="line2 text-14px fontColor333 mb-12px h-38px lh-19px acea-row">
<div
v-if="item.productTags && item.productTags.locationLeftTitle.length"
class="inline-block text-12px bg-color text-#fff b-rd-2px py-2px mr-6px px-4px lh-12px"
>
{{ item.productTags.locationLeftTitle[0].tagName }}
</div>{{ item.name }}
</div>
<div class="flex mb-10px h-16px">
<div class="text-12px text-#999 mr-10px">
<text v-if="Math.floor(item.replyNum) > 0">{{ item.replyNum }}条评论</text>
<text v-if="item.replyNum === 0 || !item.replyNum">暂无评论</text>
<text v-if="item.positiveRatio && Number(item.positiveRatio) > 0" class="ml-2px"
>好评{{ Mul(item.positiveRatio, 100) }}%</text
>
</div>
<div class="text-12px text-#999">已售{{ item.sales }}{{ item.unitName }}</div>
</div>
<div v-if="item.productTags" class="h-20px mb-10px">
<div v-if="item.productTags && item.productTags.locationUnderTitle.length" class="acea-row">
<div
v-for="items in item.productTags.locationUnderTitle.length > 3
? item.productTags.locationUnderTitle.slice(0, 3)
: item.productTags.locationUnderTitle"
:key="items.id"
class="text-12px lh-12px font-color base-border b-color-#E93323 b-rd-2px px3px py2px mr8px"
>
{{ items.tagName }}
</div>
</div>
</div>
<div class="cursors acea-row items-center" @click.stop="handleGoPage(item.merId)">
<span class="iconfont icon-guanzhudianpu mr-5px text-12px font-color"></span>
<span class="text-12px text-#666 line1 w-175px">{{ item.merName }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.item {
margin-right: 15px;
&:nth-child(5n) {
margin-right: 0;
}
}
</style>

@ -0,0 +1,91 @@
<!--推荐商品列表组件-->
<script setup lang="ts">
import { productRecommendListApi } from '~/server/goodsApi'
import {PageQuery} from "~/types/global";
const example = useScrollHeight()
/**
* 监听页面滚动
*/
onMounted(() => {
window.addEventListener('scroll', handleScroll)
})
/**
* 推荐商品列表
*/
const where = reactive<PageQuery>({
page: 1,
limit: 10,
cid: '0',
})
const tableData = reactive<PageResult>({
list: [],
total: 0
})
const pullRefreshss =ref<boolean>(false)
const loading = ref<boolean>(false)
const getList = async () =>{
loading.value = true
await productRecommendListApi(where)
.then(async (data:PageResult) => {
tableData.total = data.total;
let proList = data.list;
tableData.list = tableData.list.concat(proList);
loading.value = false
})
.catch(async (err: any) => {
loading.value = false
})
}
getList()
/**
* 滚动加载
*/
const handleScroll = () => {
let scollY = example.getScrollTop() + example.getWindowHeight() - example.getScrollHeight()+636
// false
if (scollY >= -50) {
if (where.page >= Math.ceil(tableData.total / where.limit)) {
pullRefreshss.value = true;
return false;
}
if (!pullRefreshss.value) {
return false;
}
pullRefreshss.value = false;
if (tableData.list.length < tableData.total) {
//
where.page++;
getList()
}
} else {
// true
pullRefreshss.value = true;
}
}
</script>
<template>
<div class="wrapper_1200" v-if="tableData.list.length">
<div class="w-100% text-28px fontColor333 fw-700 text-center mb-30px alimama">猜你喜欢</div>
<div class="pb-10px item grid-column-5 gap-15px">
<div v-for="item in tableData.list" :key="item.id" class="productItem cursors">
<product :item="item"></product>
</div>
</div>
<div
class="loadingicon acea-row row-center-wrapper"
v-if="loading"
>
<span class="loading iconfont icon-jiazai">加载中</span
>
</div>
</div>
</template>
<style scoped lang="scss">
</style>

@ -0,0 +1,180 @@
<script setup lang="ts">
import { useAppStore } from '~/stores/app'
const useStore = useAppStore()
import { ref, toRefs } from 'vue'
import useOrder from '~/composables/useOrder'
import { indexCustomerServiceApi } from '~/server/homeApi'
const { handleIntoPage } = useOrder()
const props = defineProps({
//
isShowCoupon: {
type: Boolean,
default: false,
},
})
const { isShowCoupon } = toRefs(props)
/**
* 监听页面滚动
*/
onMounted(() => {
window.addEventListener('scroll', handleScroll)
})
const isShowTop = ref<boolean>(false)
const handleScroll = () => {
const t = document.documentElement.scrollTop || document.body.scrollTop
if (t > 100) {
isShowTop.value = true
} else {
isShowTop.value = false
}
}
//
const handleTop = () => {
;(function n() {
var t = document.documentElement.scrollTop || document.body.scrollTop
if (t > 0) {
window.requestAnimationFrame(n)
window.scrollTo(0, t - t / 5)
}
})()
}
//
const { data: indexCustomerService } = useAsyncData(() => indexCustomerServiceApi())
const { chatConfig } = useOrder()
const handleChat = async () => {
let chatConfigData = {
serviceLink: indexCustomerService.value?.consumerH5Url,
servicePhone: indexCustomerService.value?.consumerHotline,
serviceType: indexCustomerService.value?.consumerType,
}
await chatConfig(chatConfigData)
}
</script>
<template>
<div
class="flex-col-center w-40px bg-#222222 h-100% fixed top-0px text-#fff rightNavigation z-2010"
:class="isShowCoupon ? 'right-278px' : 'right-0px'"
>
<div
@click="handleIntoPage('/users/user_info', { type: '11' })"
class="floatClass flex-col-center w-40px h-40px cursors mb-3px"
>
<span class="iconfont icon-wode"></span>
<div class="absolute floatTitle">个人信息</div>
</div>
<div
@click="handleIntoPage('/order/shopping_cart', { type: '0' })"
class="w-40px h-136px bg-color cursors flex-col-center mb-3px"
>
<span class="iconfont icon-gouwuche mb-10px"></span>
<span class="text-14px text-#fff mb-10px verticalClass">购物车</span>
<span class="w-16px h-16px b-rd-50% text-center bg-#FFFFFF flex-between-center text-12px font-color block">{{
useStore.carNumber
}}</span>
</div>
<div
@click="handleIntoPage('/users/order_list', { type: '1' })"
class="floatClass flex-col-center w-40px h-40px cursors mb-3px relative"
>
<span class="iconfont icon-dingdan"></span>
<div class="absolute floatTitle">我的订单</div>
</div>
<div
@click="handleIntoPage('/users/user_coupon', { type: '6' })"
class="floatClass flex-col-center w-40px h-40px cursors mb-3px"
>
<span class="iconfont icon-youhuiquan"></span>
<div class="absolute floatTitle w-110px text-center">优惠券</div>
</div>
<div
@click="handleIntoPage('/users/collect_products', { type: '3' })"
class="floatClass flex-col-center w-40px h-40px cursors mb-3px"
>
<span class="iconfont icon-shoucang"></span>
<div class="absolute floatTitle">我的收藏</div>
</div>
<div
@click="handleIntoPage('/users/browsing_history', { type: '8' })"
class="floatClass flex-col-center w-40px h-40px cursors mb-3px"
>
<span class="iconfont icon-zuji"></span>
<div class="absolute floatTitle">我的足迹</div>
</div>
<div @click="handleChat" class="floatClass absolute flex-col-center w-40px h-40px cursors bottom-75px mb-3px">
<span class="iconfont icon-lianxikefu"></span>
<div class="absolute floatTitle">联系客服</div>
</div>
<div
v-show="isShowTop"
@click="handleTop"
class="floatClass flex-col-center w-40px h-40px cursors mb-3px absolute bottom-30px isShowTop"
>
<span class="iconfont icon-zhiding"></span>
</div>
</div>
</template>
<style scoped lang="scss">
.iconfont {
font-size: 18px;
}
.isShowTop {
transition: top 10s ease-out;
}
.floatClass:hover .floatTitle {
display: block !important;
animation: slideRight 0.5s forwards; /* 调用名为slideRight的动画,持续2秒 */
}
@keyframes slideRight {
from {
left: -150px; /* 元素初始位置在左侧屏幕外 */
}
to {
left: -122px; /* 元素最终位置靠左边缘 */
}
}
.floatClass:hover {
width: 100%;
background: #e93323;
}
.floatTitle {
height: 40px;
background: #222222;
padding: 0 27px;
font-size: 14px;
color: #fff;
line-height: 40px;
left: -122px;
border-radius: 3px;
display: none;
}
.floatTitle::before {
content: '';
position: absolute;
top: 50%;
right: -16px;
border-width: 8px;
border-style: solid;
border-color: transparent transparent transparent #222222; /* 右边三角形的颜色 */
transform: translateY(-50%);
}
.right-278px,
.right-0px {
transition: all 1s;
}
.main_Box {
//height: 78vh;
}
.rightNavigation {
.title {
display: none;
}
}
</style>

@ -0,0 +1,124 @@
<!--搜索条件筛选可多选商品搜索列表商户搜索列表中使用-->
<script setup lang="ts">
import { toRefs, ref, onMounted, shallowRef } from 'vue'
const props = defineProps({
//
list: {
type: Array,
default: [],
},
})
const { list } = toRefs(props)
const emit = defineEmits(['handleSubmitChecked'])
const selectedList = ref({})
//
onMounted(() => {
list.value.forEach((item:any,index:number)=>{
if(document.getElementById(`myElement${index}`)){
let myElement = document.getElementById(`myElement${index}`);
if(myElement.offsetHeight>38){
item.isShowBtnMore=true
}else{
item.isShowBtnMore=false
}
}
})
});
/**
* 点击菜单
* @param list 当前选中的大类
* @param item 当前选中菜单的对象
*/
const handleMenu = (list: any, item: any) => {
list.list.forEach((val, i) => {
if (val.id == item.id) {
val.isChoose = !val.isChoose
}else{
if(!list.multiple) val.isChoose =false
}
})
//
if(list.multiple){
if (item.isChoose) {
list.checked.push(item.id)
} else {
let index = list.checked.findIndex((itemn) => itemn === item.id)
list.checked.splice(index,1)
}
selectedList.value[list.type] = list.checked
}else{
//
if (item.isChoose) {
selectedList.value[list.type] = item.id
}else{
selectedList.value[list.type] = ''
}
}
emit('handleSubmitChecked', selectedList.value)
}
</script>
<template>
<div class="b-rd-16px bg-#FFFFFF px-30px" v-if="list.length>0">
<template v-for="(item, index) in list" :key="index">
<div v-if="item.list && item.list.length" class="sort acea-row">
<div class="name mr-20px">{{ item.name }}</div>
<div class="listCon acea-row row-between text-#333 text-14px cursors w-1050px" :class="item.showMore?'on':''">
<div :ref="'myElement'+index" :id="'myElement'+index" class="list acea-row w-960px h-auto">
<div
class="item"
:class="(items.isChoose &&item.multiple) || (selectedList[item.type]===items.id && !item.multiple) ? 'font-color' : ''"
v-for="(items, idx) in item.list"
:key="idx"
@click="handleMenu(item, items)"
>
{{ items.name || items.tagName }}
</div>
</div>
<div v-if="!item.showMore && item.isShowBtnMore" class="cursors fontColor6 text-14px text-center" @click="item.showMore = !item.showMore">
收起 <span class="iconfont icon-gao"></span>
</div>
<div v-if="item.showMore && item.isShowBtnMore" class="fontColor6 text-14px text-center cursors" @click="item.showMore = !item.showMore">
展开 <span class="iconfont icon-di"></span>
</div>
</div>
</div>
</template>
</div>
</template>
<style scoped lang="scss">
.on {
height: 37px;
overflow: hidden;
}
.sort {
border-bottom: 1px dashed #eeeeee;
padding-bottom: 11px;
padding-top: 30px;
&:last-child {
border: none;
}
.name {
width: 70px;
font-size: 14px;
color: #666666;
}
.item {
margin-right: 30px;
margin-bottom: 19px;
.icon {
font-size: 15px;
margin-left: 5px;
}
.iconfont {
font-size: 15px;
color: #e2e2e2;
margin-left: 5px;
}
}
}
</style>

@ -0,0 +1,118 @@
<!--销量价格搜索搜索条件商品列表商户主页搜索展示-->
<script setup lang="ts">
import { toRefs, ref } from 'vue'
import {Search} from "@element-plus/icons-vue";
const props = defineProps({
//
where: {
type: Object,
default: null,
},
})
const { where } = toRefs(props)
const stock = ref<number>(0) //
const price = ref<number>(0) //
//
const emit = defineEmits(['handleSubmitSelect'])
const iSdefaults = ref<number>(1);
const handleSeachList = async (e: number) => {
where.value.page = 1;
iSdefaults.value = e;
switch (e) {
case 1:
price.value = 0
stock.value = 0
break
case 2:
if (price.value == 0) price.value = 1
else if (price.value == 1) price.value = 2
else if (price.value == 2) price.value = 0
stock.value = 0
break
case 3:
if (stock.value == 0) stock.value = 1
else if (stock.value == 1) stock.value = 2
else if (stock.value == 2) stock.value = 0
price.value = 0
break
case 4:
price.value = 0
stock.value = 0
break
}
await setWhere()
emit('handleSubmitSelect', where.value)
}
//where
const setWhere = () => {
if (price.value == 0) where.value.priceOrder = ''
else if (price.value == 1) where.value.priceOrder = 'asc'
else if (price.value == 2) where.value.priceOrder = 'desc'
if (stock.value == 0) where.value.salesOrder = ''
else if (stock.value == 1) where.value.salesOrder = 'asc'
else if (stock.value == 2) where.value.salesOrder = 'desc'
}
</script>
<template>
<div class="acea-row sortSeach items-center mb-20px w-100% h-50px bg-#FFFFFF b-rd-12px pl-10px">
<div class="items" :class="iSdefaults === 1 ? 'font-color' : ''" @click="handleSeachList(1)">综合排序</div>
<div class="items flex-center" :class="stock !== 0 ? 'font-color' : ''" @click="handleSeachList(3)">
销量
<div>
<span class="iconfont icon-gao text-12px lh-7px" :class="stock==1?'font-color':'text-#999'"></span>
<span class="iconfont icon-di text-12px lh-7px" :class="stock==2?'font-color':'text-#999'"></span>
</div>
</div>
<div class="items flex-center" :class="price !== 0 ? 'font-color' : ''" @click="handleSeachList(2)">
<span>价格</span>
<div>
<span class="iconfont icon-gao text-12px lh-7px" :class="price==1?'font-color':'text-#999'"></span>
<span class="iconfont icon-di text-12px lh-7px" :class="price==2?'font-color':'text-#999'"></span>
</div>
</div>
<div class="item price-range flex-center">
<div class="price-count mr-10px">
<el-input style="width: 120px" min="0" type="number" v-model.number="where.minPrice"> <template #prefix></template></el-input>
-
<el-input style="width: 120px" min="0" type="number" v-model.number="where.maxPrice">
<template #prefix></template>
</el-input>
</div>
<el-button plain @click="handleSeachList">确定</el-button>
</div>
</div>
</template>
<style scoped lang="scss">
.sort {
background: #fff;
.items {
margin-right: 30px;
cursor: pointer;
font-size: 14px;
color: #333333;
&:hover {
color: #e93323;
}
.icon {
font-size: 15px;
margin-left: 5px;
color: #e2e2e2;
}
.iconfont {
font-size: 12px;
margin-left: 5px;
display: block;
}
}
.name {
color: #969696;
margin-right: 20px;
}
}
</style>

@ -0,0 +1,60 @@
<!--成功提示弹窗-->
<script setup lang="ts">
import { ref, reactive, toRefs } from 'vue'
//
const dialogVisible = ref(false)
const props = defineProps({
//
successInfo: {
type: Object,
default: null,
},
})
const { successInfo } = toRefs(props)
const open = () => {
dialogVisible.value = true
}
defineExpose({ open })
//
const emit = defineEmits(['handleSubmitSuccess'])
const handleGoBack =()=>{
emit('handleSubmitSuccess')
}
</script>
<template>
<el-dialog
class="success-dialog borRadius"
v-model="dialogVisible"
center
width="600px"
:append-to-body="true"
:show-close="false"
:close-on-click-modal="false"
>
<div class="mb-30px flex justify-center">
<img src="~/assets/images/tijiaocheng.png" alt="" class="w-140px h-140px" />
</div>
<div class="text-24px fontColor333 mb-20px text-center">{{ successInfo.title }}</div>
<div class="text-16px text-#666 mb-50px text-center">{{ successInfo.tips }}</div>
<div class="flex justify-center">
<nuxt-link :to="{ path: successInfo.url, query: { type: successInfo.type } }">
<div @click="handleGoBack" class="w-150px h-50px text-center borderSol b-rd-25px text-16px text-#333 lh-48px mr-20px">返回列表</div>
</nuxt-link>
<nuxt-link :to="{ path: '/' }">
<div class="w-150px h-50px text-center b-rd-25px text-16px text-#fff bg-color lh-48px">去首页</div>
</nuxt-link>
</div>
</el-dialog>
</template>
<style lang="scss">
.success-dialog {
:deep(.el-dialog__body) {
padding: 25px 0 55px 0 !important;
}
}
</style>

@ -0,0 +1,93 @@
<!--swiper轮播-->
<script setup lang="ts">
import { reactive, toRefs } from 'vue'
//
//import {Pagination, Navigation} from 'swiper/modules'
import { Swiper, SwiperSlide } from 'swiper/vue'
import type SwiperClass from 'swiper'
import 'swiper/css'
import 'swiper/css/pagination'
import 'swiper/css/navigation'
import { ItemObject } from '~/types/global'
import { merchantRecommendApi } from '~/server/merchantApi'
import { Keyboard, Navigation, Pagination } from 'swiper'
const props = defineProps({
// swiperImage
swiperSlideList: {
type: Array,
default: [],
},
swiperData: {
type: Object,
default: null,
},
})
const { swiperSlideList, swiperData } = toRefs(props)
let swiperRef: SwiperClass | null = null
const setSwiperRef = (swiper: SwiperClass) => {
swiperRef = swiper
}
//
const slideTo = (index: number) => {
swiperRef?.slideTo(index - 1, 0)
}
defineExpose({ slideTo })
</script>
<template>
<div class="swiper" :style="swiperData.boxPadding">
<swiper
:space-between="swiperData.spaceBetween"
:slides-per-view="swiperData.slidesPerView"
:modules="swiperData.modules"
@swiper="setSwiperRef"
:navigation="{
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
}"
:pagination="{ clickable: true, el: '.swiper-pagination'}"
class="swiper h-100% proList acea-row"
>
<swiper-slide v-for="(item, index) in swiperSlideList" :key="index" class="h-100%" :class="swiperData.slideWidth">
<slot
:value="{
swiperItem: item,
swiperIndex: index,
}"
></slot>
</swiper-slide>
</swiper>
<div class="swiper-button-prev iconfont icon-zuo1 text-center" :style="swiperData.navigationBgColor+';'+ swiperData.navigationColor" slot="button-prev"></div>
<div class="swiper-button-next iconfont icon-you1 text-center" :style="swiperData.navigationBgColor+';'+ swiperData.navigationColor" slot="button-prev"></div>
<div class="swiper-pagination"></div>
</div>
</template>
<style scoped lang="scss">
.swiper-button-prev:after,
.swiper-button-next:after {
display: none !important;
}
.swiper-button-prev {
text-align: center;
width: 30px;
height: 42px;
//background: rgba(0, 0, 0, 0.3);
border-radius: 0px 23px 23px 0px;
// color: #fff;
margin-left: -10px;
}
.swiper-button-next {
text-align: center;
width: 30px;
height: 40px;
// background: rgba(0, 0, 0, 0.3);
border-radius: 23px 0 0 23px;
// color: #fff;
margin-right: -9px;
}
</style>

@ -0,0 +1,148 @@
<!--上传组件-->
<script setup lang="ts">
import { ref, reactive, toRefs,watch } from 'vue'
import { uploadImageApi } from '~/server/userApi'
import { Plus } from '@element-plus/icons-vue'
const props = defineProps({
//
isShow: {
type: Boolean,
default: true,
},
//
maxlength: {
type: Number,
default: 5,
},
//
limitNum: {
type: Number,
default: 5,
},
pictureList: {
type: Array,
default: [],
},
})
const { isShow, limitNum, maxlength, pictureList } = toRefs(props)
//
const picList = ref<ItemObject[]>(pictureList?.value as ItemObject[])
watch(
() => pictureList.value,
(newValue) => {
picList.value = newValue
},
)
const hideUploadEdit = ref<boolean>(false)
const myHeaders = reactive<any>({
'Content-Type': 'multipart/form-data',
})
/**
* 上传前
* @param file
*/
const beforeUpload = (file: { name: string; size: number }) => {
const fileTypeName = file.name.substr(file.name.lastIndexOf('.') + 1)
const isImage = ['jpeg', 'gif', 'bmp', 'png', 'jpg'].includes(fileTypeName)
const isLtSize = file.size / 1024 / 1024 < 10
if (!isImage) {
feedback.msgError('仅支持 ' + ['jpeg', 'gif', 'bmp', 'png', 'jpg'].join(',') + ' 格式')
return false
}
if (!isLtSize) {
//feedback.msgError(' ' + data.uploadSize + ' MB!')
return false
}
return true
}
/**
* 自定义图片上传
* @param options
*/
const emit = defineEmits(['handleComplete','handleRemoveComplete'])
const pictureData = ref<ItemObject[]>(pictureList?.value as ItemObject[]) //
const handleUploadForm = async (options: { file: string | Blob }) => {
let formData = new FormData()
formData.append('multipart', options.file)
const res = await uploadImageApi('merchant', '7', formData)
pictureData.value.push({
name: res.fileName,
url: res.url,
})
emit('handleComplete', res)
// emit('handleComplete', res)
}
const beforeRemove =(file, fileList)=> {
let index = fileList.indexOf(file)
emit('handleRemoveComplete', index)
}
/**
* 删除图片
* @param file
* @param fileList
*/
const handleRemove = async (file, fileList) => {
let index = fileList.indexOf(file)
}
const handleExceed = async () => {
feedback.msgWarning(`最多可上传${limitNum.value}`)
}
const handleEditChange = async (file, fileList) => {
// picList.value = JSON.parse((JSON.stringify(fileList)))
//hideUploadEdit.value = fileList.length >= limitNum.value
}
//
import type { UploadProps } from 'element-plus'
import {ItemObject} from "~/types/global";
import {merchantFormDefault} from "~/pages/merchant/defaultMerchant";
const dialogImageUrl = ref('')
const dialogVisible = ref(false)
const handlePictureCardPreview: UploadProps['onPreview'] = (uploadFile) => {
dialogImageUrl.value = uploadFile.url!
dialogVisible.value = true
}
</script>
<template>
<div class="el-form">
<div class="img-box-wrapper">
<el-upload
v-model:file-list="picList"
list-type="picture-card"
:limit="limitNum"
:action="'#'"
:headers="myHeaders"
:on-remove="handleRemove"
:before-remove="beforeRemove"
:http-request="handleUploadForm"
:on-exceed="handleExceed"
:show-file-list="isShow"
:before-upload="beforeUpload"
:on-change="handleEditChange"
:class="{ hide: hideUploadEdit }"
:disabled="hideUploadEdit"
:on-preview="handlePictureCardPreview"
>
<!-- <div v-if="picList" class="w-100px h-100px">-->
<!-- <img v-for="(item, index) in picList" :src="item" alt="" class="w-100px h-100px" />-->
<!-- </div>-->
<el-icon><Plus /></el-icon>
</el-upload>
</div>
<el-dialog v-model="dialogVisible">
<img w-full :src="dialogImageUrl" alt="Preview Image" />
</el-dialog>
</div>
</template>

@ -0,0 +1,243 @@
<!--商品列表收藏商品我的足迹使用-->
<template>
<div class="productList">
<div v-if="type === 'collect_products'" class="acea-row row-middle mbtom20">
<el-button size="large" round plain @click="isBatch = !isBatch">{{ isBatch ? '批量操作' : '取消' }}</el-button>
<div v-show="!isBatch" class="allSelect">
<div class="checkbox-wrapper">
<label class="well-check acea-row row-middle">
<input type="checkbox" name="" value="" v-model="isAllSelect" />
<i class="icon cursors allIcon" style="top: 11px"></i>
<span class="checkAll fontColor333 fonts14 cursors">全选</span>
</label>
</div>
</div>
<div v-show="!isBatch" class="fontColor333 fonts14 cursors" @click.stop="handleAllCancel">取消收藏</div>
</div>
<div v-loading="loading">
<div v-if="type === 'collect_products'" class="products">
<div v-for="(item, index) in list" :key="index">
<div class="item">
<div v-show="!isBatch" class="smegma b-rd-16px">
<div class="checkbox-wrapper one-checkbox">
<label class="well-check">
<input type="checkbox" name="" value="" v-model="item.checked" />
<span class="icon top-20px cursors oneIcon"></span>
</label>
</div>
</div>
<div v-show="isBatch" class="btn font12 cursors" @click.stop="handleCancel(item.productId)">取消收藏</div>
<el-image class="backImg img" :src="item.image" lazy @click="handlerNuxtLink(item)"></el-image>
<div class="cent" @click="handlerNuxtLink(item)">
<div class="name text-14px text-#333 line2">{{ item.name }}</div>
<div class="price font12 fontColor333">
<span class="oppoSans-M"></span><span class="font20 dinProSemiBold">{{ item.price }}</span>
</div>
</div>
</div>
</div>
</div>
<div v-else>
<div v-for="(item, index) in list" :key="item.id" class="mb-30px">
<div class="text-14px fontColor333 mb-18px text font-500 ml-10px">{{ item.date }}</div>
<div class="products">
<div v-for="(itm, i) in item.list" :key="i" class="item" @click="handlerNuxtLink(itm)">
<el-image class="backImg img" :src="itm.image" lazy :style="{opacity:itm.isShow?'1':'.5'}"></el-image>
<div class="cent">
<div class="name text-14px line2" :class="itm.isShow?'fontColor333':'text-#CCCCCC'">{{ itm.name }}</div>
<div v-if="itm.isShow" class="price font12 fontColor333">
<span class="oppoSans-M"></span><span class="font20 dinProSemiBold">{{ itm.price }}</span>
</div>
<div v-else class="fontColor333 text-14px oppoSans-R">商品已下架</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import {addressDelApi, getCollectCancelProApi} from '~/server/userApi'
import { ElButton, Image } from 'element-plus'
import feedback from '@/utils/feedback'
import { linkNavigateTo } from '~/utils/util'
import { ref, toRefs } from 'vue'
const props = defineProps({
//
list: {
type: Array,
default: [],
},
//,browsing_historycollect_products
type: {
type: String,
default: '',
},
})
const { list } = toRefs(props)
const loading = ref(false)
/**
* 地址跳转
*/
const handlerNuxtLink = async (item: any) => {
if(!item.isShow) return
linkNavigateTo(`/product/detail/${item.productId}`, { type: 'normal' })
}
//isAllSelect
const isAllSelect = computed({
get() {
let flag = list.value.map((item: any) => {
if (!item.checked) {
return false
} else {
return true
}
})
return !flag.includes(false)
},
set(newVal) {
if (newVal) {
list.value.map((value: any, _index, _array) => {
value.checked = true
})
} else {
list.value.map((value: any, _index, _array) => {
value.checked = false
})
}
},
})
//
const isBatch = ref(true)
//
const handleAllCancel = async () => {
let idArr = [] as any
Object.keys(list.value).forEach((item: any) => {
if (list.value[item].checked) idArr.push(list.value[item].productId)
})
if (idArr.length === 0) return feedback.msgWarning('请至少选择一个商品')
let ids = idArr.join(',')
await onCancel(ids)
}
//
const handleCancel = async (id: string) => {
await onCancel(id)
}
//
const onCancel = async (id: string) => {
await feedback.confirm('确认取消收藏吗?')
try {
await getCollectCancelProApi({ ids: id })
feedback.msgSuccess('取消成功')
loading.value = false
emit('handleComplete')
}catch (e) {
loading.value = false
}
}
const emit = defineEmits(['handleComplete'])
</script>
<style scoped lang="scss">
@import '@/assets/scss/checkbox.scss';
:deep(.el-button){
width: 90px !important;
height: 32px !important;
--el-button-text-color: #333333 !important;
font-size: 14px !important;
}
.allIcon{
width: 18px !important;
height: 18px !important;
}
.oneIcon{
width: 20px !important;
height: 20px !important;
}
.one-checkbox input:checked + .icon{
background-size: 18px 13px !important;
}
.top-20px {
top: 28px !important;
left: 10px !important;
background-color: #fff;
}
.smegma {
background-color: rgba(0, 0, 0, 0.2);
transition: opacity 0.5s ease-in-out;
width: 228px;
height: 100%;
position: absolute;
z-index: 999;
}
.allSelect {
margin-left: 20px;
margin-right: 20px;
position: relative;
.checkAll {
margin-left: 10px;
}
}
.products {
position: relative;
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-row-gap: 16px;
grid-column-gap: 16px;
grid-template-rows: auto;
.item {
width: 228px;
border-radius: 16px;
border: 1px solid #eeeeee;
position: relative;
cursor: pointer;
// opacity: 0;
.btn {
cursor: pointer;
width: 70px;
height: 26px;
background: #000000;
border-radius: 4px 4px 4px 4px;
opacity: 0.5;
position: absolute;
z-index: 1;
text-align: center;
line-height: 26px;
color: #fff;
display: none;
left: 10px;
top: 10px;
}
}
.item:hover .btn {
display: block;
}
.backImg {
width: 227px;
height: 227px;
display: inline-block !important;
border-radius: 16px 16px 0 0;
}
.cent {
margin-top: -4px;
height: 112px;
padding: 15px 20px 20px 20px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.price {
span {
font-weight: 600;
}
}
}
</style>

File diff suppressed because one or more lines are too long

@ -0,0 +1,273 @@
<template>
<div style="position: relative">
<div class="verify-img-out">
<div
class="verify-img-panel"
:style="{
width: setSize.imgWidth,
height: setSize.imgHeight,
'background-size': setSize.imgWidth + ' ' + setSize.imgHeight,
'margin-bottom': vSpace + 'px',
}"
>
<div v-show="showRefresh" class="verify-refresh" style="z-index: 3" @click="refresh">
<i class="iconfont icon-refresh" />
</div>
<img
ref="canvas"
:src="pointBackImgBase ? 'data:image/png;base64,' + pointBackImgBase : defaultImg"
alt=""
style="width: 100%; height: 100%; display: block"
@click="bindingClick ? canvasClick($event) : undefined"
/>
<div
v-for="(tempPoint, index) in tempPoints"
:key="index"
class="point-area"
:style="{
'background-color': '#1abd6c',
color: '#fff',
'z-index': 9999,
width: '20px',
height: '20px',
'text-align': 'center',
'line-height': '20px',
'border-radius': '50%',
position: 'absolute',
top: parseInt(tempPoint.y - 10) + 'px',
left: parseInt(tempPoint.x - 10) + 'px',
}"
>
{{ index + 1 }}
</div>
</div>
</div>
<!-- 'height': this.barSize.height, -->
<div
class="verify-bar-area"
:style="{
width: setSize.imgWidth,
color: this.barAreaColor,
'border-color': this.barAreaBorderColor,
'line-height': this.barSize.height,
}"
>
<span class="verify-msg">{{ text }}</span>
</div>
</div>
</template>
<script type="text/babel">
/**
* VerifyPoints
* @description 点选
* */
import { resetSize, _code_chars, _code_color1, _code_color2 } from './../utils/util';
import { aesEncrypt } from './../utils/ase';
// import { reqGet, reqCheck } from './../api/index';
export default {
name: 'VerifyPoints',
props: {
// popfixed
mode: {
type: String,
default: 'fixed',
},
captchaType: {
type: String,
},
//
vSpace: {
type: Number,
default: 5,
},
imgSize: {
type: Object,
default() {
return {
width: '310px',
height: '155px',
};
},
},
barSize: {
type: Object,
default() {
return {
width: '310px',
height: '40px',
};
},
},
defaultImg: {
type: String,
default: '',
},
},
data() {
return {
secretKey: '', // ase
checkNum: 3, //
fontPos: [], //
checkPosArr: [], //
num: 1, //
pointBackImgBase: '', //
poinTextList: [], //
backToken: '', // token
setSize: {
imgHeight: 0,
imgWidth: 0,
barHeight: 0,
barWidth: 0,
},
tempPoints: [],
text: '',
barAreaColor: undefined,
barAreaBorderColor: undefined,
showRefresh: true,
bindingClick: true,
};
},
computed: {
resetSize() {
return resetSize;
},
},
watch: {
// type
type: {
immediate: true,
handler() {
this.init();
},
},
},
mounted() {
//
this.$el.onselectstart = function () {
return false;
};
},
methods: {
init() {
//
this.fontPos.splice(0, this.fontPos.length);
this.checkPosArr.splice(0, this.checkPosArr.length);
this.num = 1;
this.getPictrue();
this.$nextTick(() => {
this.setSize = this.resetSize(this); //
this.$parent.$emit('ready', this);
});
},
canvasClick(e) {
this.checkPosArr.push(this.getMousePos(this.$refs.canvas, e));
if (this.num == this.checkNum) {
this.num = this.createPoint(this.getMousePos(this.$refs.canvas, e));
//
this.checkPosArr = this.pointTransfrom(this.checkPosArr, this.setSize);
//
setTimeout(() => {
// var flag = this.comparePos(this.fontPos, this.checkPosArr);
//
var captchaVerification = this.secretKey
? aesEncrypt(this.backToken + '---' + JSON.stringify(this.checkPosArr), this.secretKey)
: this.backToken + '---' + JSON.stringify(this.checkPosArr);
const data = {
captchaType: this.captchaType,
pointJson: this.secretKey
? aesEncrypt(JSON.stringify(this.checkPosArr), this.secretKey)
: JSON.stringify(this.checkPosArr),
token: this.backToken,
};
reqCheck(data).then((res) => {
if (res.repCode == '0000') {
this.barAreaColor = '#4cae4c';
this.barAreaBorderColor = '#5cb85c';
this.text = '验证成功';
this.bindingClick = false;
if (this.mode == 'pop') {
setTimeout(() => {
this.$parent.clickShow = false;
this.refresh();
}, 1500);
}
this.$parent.$emit('success', { captchaVerification });
} else {
this.$parent.$emit('error', this);
this.barAreaColor = '#d9534f';
this.barAreaBorderColor = '#d9534f';
this.text = '验证失败';
setTimeout(() => {
this.refresh();
}, 700);
}
});
}, 400);
}
if (this.num < this.checkNum) {
this.num = this.createPoint(this.getMousePos(this.$refs.canvas, e));
}
},
//
getMousePos: function (obj, e) {
var x = e.offsetX;
var y = e.offsetY;
return { x, y };
},
//
createPoint: function (pos) {
this.tempPoints.push(Object.assign({}, pos));
return ++this.num;
},
refresh: function () {
this.tempPoints.splice(0, this.tempPoints.length);
this.barAreaColor = '#000';
this.barAreaBorderColor = '#ddd';
this.bindingClick = true;
this.fontPos.splice(0, this.fontPos.length);
this.checkPosArr.splice(0, this.checkPosArr.length);
this.num = 1;
this.getPictrue();
this.text = '验证失败';
this.showRefresh = true;
},
//
getPictrue() {
const data = {
captchaType: this.captchaType,
clientUid: localStorage.getItem('point'),
ts: Date.now(), //
};
reqGet(data).then((res) => {
if (res.repCode == '0000') {
this.pointBackImgBase = res.repData.originalImageBase64;
this.backToken = res.repData.token;
this.secretKey = res.repData.secretKey;
this.poinTextList = res.repData.wordList;
this.text = '请依次点击【' + this.poinTextList.join(',') + '】';
} else {
this.text = res.repMsg;
}
//
if (res.repCode == '6201') {
this.pointBackImgBase = null;
}
});
},
//
pointTransfrom(pointArr, imgSize) {
var newPointArr = pointArr.map((p) => {
const x = Math.round((310 * p.x) / parseInt(imgSize.imgWidth));
const y = Math.round((155 * p.y) / parseInt(imgSize.imgHeight));
return { x, y };
});
return newPointArr;
},
},
};
</script>

@ -0,0 +1,384 @@
<template>
<div style="position: relative;">
<div v-if="type === '2'" class="verify-img-out" :style="{ height: parseInt(setSize.imgHeight) + vSpace + 'px' }">
<div v-loading="loading" class="verify-img-panel" :style="{ width: setSize.imgWidth, height: setSize.imgHeight }">
<img
:src="backImgBase ? 'data:image/png;base64,' + backImgBase : defaultImg"
alt=""
style="width: 100%; height: 100%; display: block;"
/>
<div v-show="showRefresh" class="verify-refresh" @click="refresh"><i class="iconfont icon-refresh" /></div>
<transition name="tips">
<span v-if="tipWords" class="verify-tips" :class="passFlag ? 'suc-bg' : 'err-bg'">{{ tipWords }}</span>
</transition>
</div>
</div>
<!-- 公共部分 -->
<div
class="verify-bar-area"
:style="{ width: setSize.imgWidth, height: barSize.height, 'line-height': barSize.height }"
>
<span class="verify-msg" v-text="text" />
<div
class="verify-left-bar"
:style="{
width: leftBarWidth !== undefined ? leftBarWidth : barSize.height,
height: barSize.height,
'border-color': leftBarBorderColor,
transaction: transitionWidth,
}"
>
<span class="verify-msg" v-text="finishText" />
<div
class="verify-move-block"
:style="{
width: barSize.height,
height: barSize.height,
'background-color': moveBlockBackgroundColor,
left: moveBlockLeft,
transition: transitionLeft,
}"
@touchstart="start"
@mousedown="start"
>
<i :class="['verify-icon iconfont', iconClass]" :style="{ color: iconColor }" />
<div
v-if="type === '2'"
class="verify-sub-block"
:style="{
width: Math.floor((parseInt(setSize.imgWidth) * 47) / 310) + 'px',
height: setSize.imgHeight,
top: '-' + (parseInt(setSize.imgHeight) + vSpace) + 'px',
'background-size': setSize.imgWidth + ' ' + setSize.imgHeight,
}"
>
<img
:src="'data:image/png;base64,' + blockBackImgBase"
alt=""
style="width: 100%; height: 100%; display: block;"
/>
</div>
</div>
</div>
</div>
</div>
</template>
<script type="text/babel">
/**
* VerifySlide
* @description 滑块
* */
import { aesEncrypt } from './../utils/ase'
import { resetSize } from './../utils/util'
import { knowUserCaptchaApi, knowUserSmsCaptchaApi } from '~/server/userApi'
// "captchaType":"blockPuzzle",
export default {
name: 'VerifySlide',
props: {
captchaType: {
type: String,
},
type: {
type: String,
default: '1',
},
phone: {
type: String,
default: '',
},
// popfixed
mode: {
type: String,
default: 'fixed',
},
vSpace: {
type: Number,
default: 5,
},
explain: {
type: String,
default: '向右滑动完成验证',
},
imgSize: {
type: Object,
default() {
return {
width: '310px',
height: '155px',
}
},
},
blockSize: {
type: Object,
default() {
return {
width: '50px',
height: '50px',
}
},
},
barSize: {
type: Object,
default() {
return {
width: '310px',
height: '40px',
}
},
},
defaultImg: {
type: String,
default: '',
},
},
data() {
return {
secretKey: '', //
passFlag: '', //
backImgBase: '', //
blockBackImgBase: '', //
backToken: '', // token
startMoveTime: '', //
endMovetime: '', //
tipsBackColor: '', //
tipWords: '',
text: '',
finishText: '',
setSize: {
imgHeight: 0,
imgWidth: 0,
barHeight: 0,
barWidth: 0,
},
top: 0,
left: 0,
moveBlockLeft: undefined,
leftBarWidth: undefined,
//
moveBlockBackgroundColor: undefined,
leftBarBorderColor: '#ddd',
iconColor: undefined,
iconClass: 'icon-right',
status: false, //
isEnd: false, //
showRefresh: true,
transitionLeft: '',
transitionWidth: '',
loading: false //
}
},
computed: {
barArea() {
return this.$el.querySelector('.verify-bar-area')
},
resetSize() {
return resetSize
},
},
watch: {
// type
type: {
immediate: true,
handler() {
this.init()
},
},
},
mounted() {
//
this.$el.onselectstart = function () {
return false
}
},
methods: {
init() {
this.text = this.explain
this.getPictrue()
this.$nextTick(() => {
const setSize = this.resetSize(this) //
this.setSize.imgWidth = setSize.imgWidth
this.setSize.imgHeight = setSize.imgHeight
this.setSize.barHeight = setSize.barHeight
this.setSize.barWidth = setSize.barWidth
this.$parent.$emit('ready', this)
})
var _this = this
window.removeEventListener('touchmove', function (e) {
_this.move(e)
})
window.removeEventListener('mousemove', function (e) {
_this.move(e)
})
//
window.removeEventListener('touchend', function () {
_this.end()
})
window.removeEventListener('mouseup', function () {
_this.end()
})
window.addEventListener('touchmove', function (e) {
_this.move(e)
})
window.addEventListener('mousemove', function (e) {
_this.move(e)
})
//
window.addEventListener('touchend', function () {
_this.end()
})
window.addEventListener('mouseup', function () {
_this.end()
})
},
//
start: function (e) {
e = e || window.event
if (!e.touches) {
// PC
var x = e.clientX
} else {
//
var x = e.touches[0].pageX
}
this.startLeft = Math.floor(x - this.barArea.getBoundingClientRect().left)
this.startMoveTime = +new Date() //
if (this.isEnd == false) {
this.text = ''
this.moveBlockBackgroundColor = '#337ab7'
this.leftBarBorderColor = '#337AB7'
this.iconColor = '#fff'
e.stopPropagation()
this.status = true
}
},
//
move: function (e) {
e = e || window.event
if (this.status && this.isEnd == false) {
if (!e.touches) {
// PC
var x = e.clientX
} else {
//
var x = e.touches[0].pageX
}
var bar_area_left = this.barArea.getBoundingClientRect().left
var move_block_left = x - bar_area_left // left
if (move_block_left >= this.barArea.offsetWidth - parseInt(parseInt(this.blockSize.width) / 2) - 2) {
move_block_left = this.barArea.offsetWidth - parseInt(parseInt(this.blockSize.width) / 2) - 2
}
if (move_block_left <= 0) {
move_block_left = parseInt(parseInt(this.blockSize.width) / 2)
}
// left
this.moveBlockLeft = move_block_left - this.startLeft + 'px'
this.leftBarWidth = move_block_left - this.startLeft + 'px'
}
},
//
end() {
this.endMovetime = +new Date()
var _this = this
//
if (this.status && this.isEnd == false) {
var moveLeftDistance = parseInt((this.moveBlockLeft || '').replace('px', ''))
moveLeftDistance = (moveLeftDistance * 310) / parseInt(this.setSize.imgWidth)
const params = {
captchaType: this.captchaType,
pointJson: this.secretKey
? aesEncrypt(JSON.stringify({ x: moveLeftDistance, y: 5.0 }), this.secretKey)
: JSON.stringify({ x: moveLeftDistance, y: 5.0 }),
token: this.backToken,
}
knowUserSmsCaptchaApi(params).then((res) => {
if (res.repCode == '0000') {
this.moveBlockBackgroundColor = '#5cb85c';
this.leftBarBorderColor = '#5cb85c';
this.iconColor = '#fff';
this.iconClass = 'icon-check';
this.showRefresh = false;
this.isEnd = true;
if (this.mode == 'pop') {
setTimeout(() => {
this.$parent.clickShow = false;
this.refresh();
}, 1500);
}
this.passFlag = true;
this.tipWords = `${((this.endMovetime - this.startMoveTime) / 1000).toFixed(2)}s验证成功`;
var captchaVerification = this.secretKey
? aesEncrypt(this.backToken + '---' + JSON.stringify({ x: moveLeftDistance, y: 5.0 }), this.secretKey)
: this.backToken + '---' + JSON.stringify({ x: moveLeftDistance, y: 5.0 });
setTimeout(() => {
this.tipWords = '';
this.$emit('success', { captchaVerification });
}, 1000);
} else {
this.moveBlockBackgroundColor = '#d9534f';
this.leftBarBorderColor = '#d9534f';
this.iconColor = '#fff';
this.iconClass = 'icon-close';
this.passFlag = false;
setTimeout(function () {
_this.refresh();
}, 1000);
this.$emit('error', this);
this.tipWords = '验证失败';
setTimeout(() => {
this.tipWords = '';
}, 1000);
}
})
}
},
refresh: function () {
this.showRefresh = true
this.finishText = ''
this.transitionLeft = 'left .3s'
this.moveBlockLeft = undefined
this.leftBarWidth = undefined
this.transitionWidth = 'width .3s'
this.leftBarBorderColor = '#ddd'
this.moveBlockBackgroundColor = '#fff'
this.iconColor = '#000'
this.iconClass = 'icon-right'
this.isEnd = false
this.getPictrue()
setTimeout(() => {
this.status = false
this.transitionWidth = ''
this.transitionLeft = ''
this.text = this.explain
}, 300)
},
//
async getPictrue() {
const params = {
captchaType: this.captchaType,
clientUid: localStorage.getItem('slider'),
ts: Date.now(), //
}
this.loading = true
const res = await knowUserCaptchaApi(params)
if (res.repCode == '0000') {
this.backImgBase = res.repData.originalImageBase64;
this.blockBackImgBase = res.repData.jigsawImageBase64
this.backToken = res.repData.token;
this.secretKey = res.repData.secretKey;
} else {
this.text = res.repMsg;
}
this.loading = false
},
},
}
</script>

@ -0,0 +1,11 @@
import CryptoJS from 'crypto-js'
/**
* @word 要加密的内容
* @keyWord String 服务器随机返回的关键字
* */
export function aesEncrypt(word, keyWord = 'XwKsGlMcdPMEhR1B') {
var key = CryptoJS.enc.Utf8.parse(keyWord)
var srcs = CryptoJS.enc.Utf8.parse(word)
var encrypted = CryptoJS.AES.encrypt(srcs, key, { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 })
return encrypted.toString()
}

@ -0,0 +1,89 @@
import { ElMessage, ElMessageBox, ElNotification, ElLoading, type ElMessageBoxOptions } from 'element-plus'
import type { LoadingInstance } from 'element-plus/es/components/loading/src/loading'
export class Tips {
private loadingInstance: LoadingInstance | null = null
static instance: Tips | null = null
static getInstance() {
return this.instance ?? (this.instance = new Tips())
}
// 消息提示
msg(msg: string) {
ElMessage.info(msg)
}
// 错误消息
msgError(msg: string) {
ElMessage.error(msg)
}
// 成功消息
msgSuccess(msg: string) {
ElMessage.success(msg)
}
// 警告消息
msgWarning(msg: string) {
ElMessage.warning(msg)
}
// 弹出提示
alert(msg: string) {
ElMessageBox.alert(msg, '系统提示')
}
// 错误提示
alertError(msg: string) {
ElMessageBox.alert(msg, '系统提示', { type: 'error' })
}
// 成功提示
alertSuccess(msg: string) {
ElMessageBox.alert(msg, '系统提示', { type: 'success' })
}
// 警告提示
alertWarning(msg: string) {
ElMessageBox.alert(msg, '系统提示', { type: 'warning' })
}
// 通知提示
notify(msg: string) {
ElNotification.info(msg)
}
// 错误通知
notifyError(msg: string) {
ElNotification.error(msg)
}
// 成功通知
notifySuccess(msg: string) {
ElNotification.success(msg)
}
// 警告通知
notifyWarning(msg: string) {
ElNotification.warning(msg)
}
// 确认窗体
confirm(msg: string) {
return ElMessageBox.confirm(msg, '温馨提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
}
// 提交内容
prompt(content: string, title: string, options?: ElMessageBoxOptions) {
return ElMessageBox.prompt(content, title, {
confirmButtonText: '确定',
cancelButtonText: '取消',
...options,
})
}
// 打开全局loading
loading(msg: string) {
this.loadingInstance = ElLoading.service({
lock: true,
text: msg,
})
}
// 关闭全局loading
closeLoading() {
this.loadingInstance?.close()
}
}
const tips = Tips.getInstance()
export default tips

@ -0,0 +1,36 @@
export function resetSize(vm) {
var img_width, img_height, bar_width, bar_height // 图片的宽度、高度,移动条的宽度、高度
var parentWidth = vm.$el.parentNode.offsetWidth || window.offsetWidth
var parentHeight = vm.$el.parentNode.offsetHeight || window.offsetHeight
if (vm.imgSize.width.indexOf('%') != -1) {
img_width = parseInt(this.imgSize.width) / 100 * parentWidth + 'px'
} else {
img_width = this.imgSize.width
}
if (vm.imgSize.height.indexOf('%') != -1) {
img_height = parseInt(this.imgSize.height) / 100 * parentHeight + 'px'
} else {
img_height = this.imgSize.height
}
if (vm.barSize.width.indexOf('%') != -1) {
bar_width = parseInt(this.barSize.width) / 100 * parentWidth + 'px'
} else {
bar_width = this.barSize.width
}
if (vm.barSize.height.indexOf('%') != -1) {
bar_height = parseInt(this.barSize.height) / 100 * parentHeight + 'px'
} else {
bar_height = this.barSize.height
}
return { imgWidth: img_width, imgHeight: img_height, barWidth: bar_width, barHeight: bar_height }
}
export const _code_chars = [1, 2, 3, 4, 5, 6, 7, 8, 9, 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']
export const _code_color1 = ['#fffff0', '#f0ffff', '#f0fff0', '#fff0f0']
export const _code_color2 = ['#FF0033', '#006699', '#993366', '#FF9900', '#66CC66', '#FF33CC']

@ -0,0 +1,37 @@
import feedback from "~/utils/feedback";
export function useCopy() {
const handleCopy = (text:string) =>{
if (navigator.clipboard && window.isSecureContext) {
const textToCopy = text
navigator.clipboard
.writeText(textToCopy)
.then(() => {
feedback.msgSuccess('复制成功')
})
.catch((error) => {
console.error('复制文本时出错:', error)
})
} else {
// 创建text area
const textArea = document.createElement('textarea')
textArea.value = text
// 使text area不在viewport,同时设置不可见
document.body.appendChild(textArea)
textArea.focus()
textArea.select()
return new Promise((resolve, reject) => {
// 执行复制命令并移除文本框
document.execCommand('copy') ? resolve() : reject(new Error('出错了'))
textArea.remove()
}).then(() => {
feedback.msgSuccess('复制成功')
}, () => {
feedback.msgError('复制失败')
})
}
}
return{
handleCopy
}
}

@ -0,0 +1,16 @@
export default function useDialog(initValue: boolean = false) {
const bool: globalThis.Ref<boolean> = ref(initValue)
const DialogOpen = () => {
bool.value = true
}
const DialogClose = () => {
bool.value = false
}
return {
bool,
DialogOpen,
DialogClose,
// /handleDialogOpen,
}
}

@ -0,0 +1,48 @@
import {couponReceiveApi, getMerCollectAddApi, getMerCollectCancelApi} from '~/server/merchantApi'
import {ref} from "vue";
export default function useMerchant() {
//店铺关注
const onfollowMerchant = (merId: number) => {
return new Promise((resolve, reject) => {
getMerCollectAddApi(merId).then(() => {
feedback.msgSuccess('关注店铺成功')
//linkNavigateTo(`/users/order_list`, { type: 1 })
return resolve()
})
})
}
//店铺取消关注
const onUnfollowMerchant = (merId: number) => {
return new Promise((resolve, reject) => {
feedback.confirm('确认取消关注吗?').then(async () => {
await getMerCollectCancelApi(merId).then(() => {
//linkNavigateTo(`/users/order_list`, { type: 1 })
return resolve()
})
})
})
}
//领取优惠券
const loading = ref<boolean>(false)
const onReceiveCoupon = (couponId: number) => {
loading.value = true
return new Promise(async(resolve, reject) => {
try {
await couponReceiveApi(couponId)
feedback.msgSuccess('领取成功')
return resolve()
loading.value = false
} catch (e) {
loading.value = false
}
})
}
return {
onfollowMerchant,
onUnfollowMerchant,
onReceiveCoupon
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save