Merge branch 'feature/task1.0.0_xg' into 'feature/task1.0.0'

Feature/task1.0.0 xg

See merge request yanxuan-frontend/shop-pc!41
merge-requests/42/head
肖广 2 years ago
commit 5128c50ddd

@ -0,0 +1,16 @@
{
"env": {
"test": {
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "current"
}
}
]
]
}
}
}

@ -0,0 +1,13 @@
# editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false

94
.gitignore vendored

@ -0,0 +1,94 @@
# Created by .ignore support plugin (hsz.mobi)
### Node template
# Logs
/logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.history/
# 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
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://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/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# Nuxt generate
dist
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless
# IDE / Editor
.idea
# Service worker
sw.*
# macOS
.DS_Store
# Vim swap files
*.swp
env.js

@ -0,0 +1,2 @@
registry=https://registry.npm.taobao.org/
sass_binary_site="https://npm.taobao.org/mirrors/node-sass"

@ -0,0 +1,12 @@
FROM node:12.13.1
WORKDIR /workload
COPY nuxt.config.js /workload/nuxt.config.js
COPY package.json /workload/package.json
COPY .nuxt /workload/.nuxt
RUN npm config set registry https://registry.npm.taobao.org \
&& npm install
EXPOSE 3000
CMD npm run start

@ -1,92 +1,127 @@
<!--
* @Author: ch
* @Date: 2022-05-05 15:39:29
* @LastEditors: ch
* @LastEditTime: 2022-05-07 11:39:38
* @Description: file content
-->
# shop-pc
马士兵严选pc
## 运行&打包
- 运行直接执行 npm run dev 打包执行npm run build 即可,会根据分支读取不同环境变量配置;
## Getting started
To make it easy for you to get started with GitLab, here's a list of recommended next steps.
Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
## Add your files
- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files
- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command:
## 环境变量配置
- 环境变量配置文件env.config;
- 配置与分支对应关系msb_prod -> prod msb_beta -> beta msb_test -> test msb-其他 -> dev
- 输出的环境变量文件plugins/config/env.js
- 修改环境变量配置后需要执行 “ node env.config " 输出的环境变量才会更新
``` js
// 直接引入输出的配置文件即可
import ENV from '@/plugins/config/env.js';
// 直接访问你在配置文件中定义的属性
console.log(ENV.baseUrl);
```
cd existing_repo
git remote add origin http://internel-git.mashibing.cn/yanxuan-frontend/shop-pc.git
git branch -M main
git push -uf origin main
```
## Integrate with your tools
- [ ] [Set up project integrations](http://internel-git.mashibing.cn/yanxuan-frontend/shop-pc/-/settings/integrations)
## Collaborate with your team
- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/)
- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
- [ ] [Automatically merge when pipeline succeeds](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html)
## Test and Deploy
Use the built-in continuous integration in GitLab.
- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html)
- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing(SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
***
# Editing this README
When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thank you to [makeareadme.com](https://www.makeareadme.com/) for this template.
## Suggestions for a good README
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
## Name
Choose a self-explaining name for your project.
## Description
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
## Badges
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
## Visuals
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
## Installation
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
## Usage
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
## Support
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
## Roadmap
If you have ideas for releases in the future, it is a good idea to list them in the README.
## Contributing
State if you are open to contributions and what your requirements are for accepting them.
## 公共方法utils
- 公共方法统一放置utils文件夹内可以按分类建方法文件 如验证类verify.js 请求类request.js
- 所有公共方法采用大驼峰命名法
- 所有的方法都从index.js输出引入时统一引入index不允许直接引入方法文件
- 所有方法文件如果导出的是多个方法,不允许在定义方法时导出,必须在文件底部一一导出,并附上方法简单的注释
``` js
// 正确
import {Req, IsPhone} from '@/common/utils';
// 错误
import {Req} from '@/common/utils/request';
import {IsPhone} from '@/common/utils/utils';
// 正确
const IsPhone = (str) => {....}
const IsEmail = (str) => {....}
export {
// 判断手机号
IsPhone,
// 判断邮箱
IsEmail
}
```
## 组件
- 根目录的components 只放置真正的组件某个页面的业务模块应该在pages的相应目录下新建module目录放置
- 所有的自定义组件文件名以大驼峰命名且在templet中使用也用大驼峰形式使用(包括页面内的模块组件)
## storage的使用
- 不要在页面内直接使用loaclStorage全都放置到vuex中做一次管理
## 请求
- 项目中有两个axios请求实例一个事nuxt自带的不需要Token请使用这个一个是额外封装的需要Token请使用这个
- 需要Token的请求不要写在asyncData中
- 所有请求方法命名以Api+请求类型+具体方) 法命名
- 所有请求使用ToAsyncAwait 包裹
- 不允许使用try catch 和 then 处理返回结果
``` js
// 使用示例
// xxapi.js
import {ToAsyncAwait, ReqestTk} from '@/common/utils'
const ApiGetUserInfo = (parapms) => ToAsyncAwait(ReqestTk.get('xxxxUrl',{params}));
exprot {
// 获取用户信息
ApiGetUserInfo
}
// user.vue
improt {ApiGetUserInfo} from '@/common/api/xxapi.js';
const getUserInfo = async () =>{
const {error, result} = await ApiGetUserInfo();
if(error){
alert(error);
return false;
}
app.userInfo = result;
}
For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
```
## css
- 采用BEM命名法
### 兼容CSS
``` css
/* 以下兼容方式的样式请使用util.css中的adj方法 */
.my-class{
transform: translate3d(-50%, 0, 0);
-webkit-transform: translate3d(-50%, 0, 0);
-moz-transform: translate3d(-50%, 0, 0);
-o-transform: translate3d(-50%, 0, 0);
-ms-transform: translate3d(-50%, 0, 0);
}
/* 使用以下方法 */
@import "~/assets/scss/util.scss";
.my-class{
@include adj(transform, translate3d(-50%, 0, 0));
}
You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
```
## Authors and acknowledgment
Show your appreciation to those who have contributed to the project.
## 登录相关
```javascript
// 访问token
this.$store.state.token
// 设置token
this.$store.commit('setToken')
// 退出登录
this.$store.commit('setLoginOut')
// 获取登录的用户信息
this.$store.state.userInfo
// 登录拦截
// 示例:点击购买课程前需要判断当前用户是否登录,未登录则弹出登录弹窗
function onPurchaseCourse() {
if (!this.$isLoginValidate()) {
return;
}
// 此处省略其他业务代码...
}
## License
For open source projects, say how it is licensed.
```
## Project status
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 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: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 928 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 763 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 752 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 812 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 904 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 689 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 671 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 653 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 728 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 369 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 928 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 375 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 540 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 553 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 351 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 567 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 542 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 815 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 611 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 706 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 710 B

@ -0,0 +1,7 @@
/* 改变主题色变量 */
$--color-primary: #FF512B;
/* 改变 icon 字体路径变量,必需 */
$--font-path: '~element-ui/lib/theme-chalk/fonts';
@import "~element-ui/packages/theme-chalk/src/index";

@ -0,0 +1,118 @@
/* ========================================================================
Component: Flex
========================================================================== */
.flex { display: flex; }
.flex-inline { display: inline-flex; }
/*
* Remove pseudo elements created by micro clearfix as precaution
*/
.flex::before,
.flex::after,
.flex-inline::before,
.flex-inline::after { display: none; }
/* Alignment
========================================================================== */
/*
* Align items along the main axis of the current line of the flex container
* Row: Horizontal
*/
// Default
.flex-left { justify-content: flex-start; }
.flex-center { justify-content: center; }
.flex-right { justify-content: flex-end; }
.flex-between { justify-content: space-between; }
.flex-around { justify-content: space-around; }
/*
* Align items in the cross axis of the current line of the flex container
* Row: Vertical
*/
// Default
.flex-stretch { align-items: stretch; }
.flex-top { align-items: flex-start; }
.flex-middle { align-items: center; }
.flex-bottom { align-items: flex-end; }
.flex-baseline { align-items: baseline; }
/* Direction
========================================================================== */
// Default
.flex-row { flex-direction: row; }
.flex-row-reverse { flex-direction: row-reverse; }
.flex-column { flex-direction: column; }
.flex-column-reverse { flex-direction: column-reverse; }
/* Wrap
========================================================================== */
// Default
.flex-nowrap { flex-wrap: nowrap; }
.flex-wrap { flex-wrap: wrap; }
.flex-wrap-reverse { flex-wrap: wrap-reverse; }
/*
* Aligns items within the flex container when there is extra space in the cross-axis
* Only works if there is more than one line of flex items
*/
// Default
.flex-wrap-stretch { align-content: stretch; }
.flex-wrap-top { align-content: flex-start; }
.flex-wrap-middle { align-content: center; }
.flex-wrap-bottom { align-content: flex-end; }
.flex-wrap-between { align-content: space-between; }
.flex-wrap-around { align-content: space-around; }
/* Item ordering
========================================================================== */
/*
* Default is 0
*/
.flex-first { order: -1;}
.flex-last { order: 99;}
/* Item dimensions
========================================================================== */
/*
* Initial: 0 1 auto
* Content dimensions, but shrinks
*/
/*
* No Flex: 0 0 auto
* Content dimensions
*/
.flex-none { flex: none; }
/*
* Relative Flex: 1 1 auto
* Space is allocated considering content
*/
.flex-auto { flex: auto; }
/*
* Absolute Flex: 1 1 0%
* Space is allocated solely based on flex
*/
.flex-1 { flex: 1; }

@ -0,0 +1,53 @@
@import './flex.scss';
@import './util.scss';
* {
-webkit-box-sizing: border-box; box-sizing: border-box;
-webkit-touch-callout:none; /*系统默认菜单被禁用*/
-webkit-user-select:none; /*webkit浏览器*/
-khtml-user-select:none; /*早期浏览器*/
-moz-user-select:none;/*火狐*/
-ms-user-select:none; /*IE10*/
user-select:none;
&:after, &:before {
@extend *;
}
}
body, dl, dd, h1, h2, h3, h4, h5, p, figure, form, ul, ol { margin: 0; font-size: 14px;}
ul, ol, input, button { padding: 0;}
ul, ol, li { list-style: none;list-style-type: none}
h1, h2, h3, h4, h5 { font-size: 100%;}
input { border: 0; margin: 0;}
img { width: 100%;}
a, button, input, optgroup, select, textarea, img { outline:none; -webkit-tap-highlight-color: rgba(0,0,0,0); }
a, img { -webkit-touch-callout: none; border: 0;}
html, body { position: absolute; min-height: 100%; height: 100%; width: 100%;}
input, textarea, img, button { vertical-align: middle; outline: none;}
body {
font-family: PingFang SC, Microsoft YaHei, Helvetica, STHeiTi, sans-serif !important;
color: #333; -webkit-font-smoothing: antialiased; font-smoothing: antialiased;
overflow-anchor: none}
table {border-collapse: collapse; border-spacing: 0;
color: #333;}
a { text-decoration:none;
color: #333;
&:hover { text-decoration:none; color: #FF512B;}
}
/* 自定义滚动条样式 */
.scrollbar-self {
&::-webkit-scrollbar {
width: 4px;
background-color: none;
}
&::-webkit-scrollbar-track {
background-color: none;
}
&::-webkit-scrollbar-thumb {
background: #dddddd;
border-radius: 10px;
}
}

@ -0,0 +1,115 @@
$baseFontSize: 100 !default;
/**/
@mixin adj ($styleName, $value...) {
#{$styleName}: $value;
-webkit-#{$styleName}: $value;
-moz-#{$styleName}: $value;
-o-#{$styleName}: $value;
-ms-#{$styleName}: $value;
}
// @mixin transition($value...) {
// -webkit-transiton: $value;
// -moz-transtion: $value;
// -ms-transtion: $value;
// transition: $value;
// }
// @mixin transition-delay($value...) {
// transition-delay: $value;
// -moz-transition-delay: $value; /* Firefox 4 */
// -webkit-transition-delay: $value; /* Safari 和 Chrome */
// -o-transition-delay: $value; /* Opera */
// }
// @mixin transform($value...) {
// transform: $value;
// -webkit-transform: $value;
// -moz-transform: $value;
// -o-transform: $value;
// -ms-transform: $value;
// }
// @mixin animation($value...) {
// -webkit-animation: $value;
// -moz-animation: $value;
// -ms-animation: $value;
// animation: $value;
// }
@mixin keyframes($animationName) {
@-webkit-keyframes #{$animationName} {
@content;
}
@-moz-keyframes #{$animationName} {
@content;
}
@-o-keyframes #{$animationName} {
@content;
}
@keyframes #{$animationName} {
@content;
}
}
// @mixin filter($value...) {
// -webkit-filter: $value;
// -moz-filter: $value;
// -ms-filter: $value;
// filter: $value;
// }
@mixin linear-gradient($value...) {
background: -webkit-linear-gradient($value); /* Safari 5.1 - 6.0 */
background: -o-linear-gradient($value); /* Opera 11.1 - 12.0 */
background: -moz-linear-gradient($value); /* Firefox 3.6 - 15 */
background: linear-gradient(t$value); /* 标准的语法 */
}
// @mixin boxShow($value...) {
// -webkit-box-shadow: $value;
// -moz-box-shadow: $value;
// -ms-box-shadow: $value;
// box-shadow: $value;
// }
@function torem($value) {
@return ($value / $baseFontSize * 1rem);
}
%overflow-scrolling {
-webkit-overflow-scrolling: touch;
overflow-scrolling: touch;
overflow: auto;
}
%els {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
/*内容居中布局*/
@mixin layout-box {
width: 1200px;
margin: 0 auto;
}
/*单行溢出*/
@mixin ellipsis {
overflow:hidden;
text-overflow:ellipsis;
white-space:nowrap
}
/*多行溢出*/
@mixin ellipses($line) {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: $line;
-webkit-box-orient: vertical;
}
/*可点击文字hover*/
.hover-text:hover {
color: #FF875B;
}

@ -0,0 +1,423 @@
<template>
<div>
<div class="address flex flex-middle" v-if="!isOperation && !isAccount">
<article class="address__msg">
<div class="address__msg-line flex flex-middle">
<img src="@/assets/img/goods/point.png" alt="收货地址" />
<span class="address__msg-line--txt">默认地址</span>
<UiButton type="yellow_panel">修改</UiButton>
</div>
<div class="address__msg-line flex flex-middle">
<span class="address__msg-line--type">收货地址:</span>
<span class="address__msg-line--txt">{{
curAddressListItem.province +
curAddressListItem.city +
curAddressListItem.area +
curAddressListItem.detailAddress
}}</span>
</div>
<div class="address__msg-line flex flex-middle">
<span class="address__msg-line--type"> :</span>
<span class="address__msg-line--txt">{{
curAddressListItem.name
}}</span>
</div>
<div class="address__msg-line flex flex-middle">
<span class="address__msg-line--type">手机号码:</span>
<span class="address__msg-line--txt">{{
curAddressListItem.phone
}}</span>
</div>
</article>
<div class="hr"></div>
<div class="address__operation flex flex-column flex-middle">
<UiButton type="grey" class="flex flex-middle" @click="onSwtich">
<img
class="icon"
src="@/assets/img/goods/switch.png"
alt="切换地址"
/>
切换地址
</UiButton>
<UiButton
type="grey"
@click="isOperation = true"
class="flex flex-middle"
>
<img class="icon" src="@/assets/img/goods/add.png" alt="新建地址" />
新建地址
</UiButton>
</div>
</div>
<div
class="address"
:class="isAccount ? 'account' : 'flex flex-middle'"
v-else
>
<article class="address__msg">
<div class="address__msg-line flex flex-middle">
<span class="address__msg-line--type">所在区域</span>
<no-ssr
><v-distpicker
:province="selectData.province.value"
:city="selectData.city.value"
:area="selectData.area.value"
@selected="onSelected"
></v-distpicker
></no-ssr>
</div>
<div class="address__msg-line flex flex-middle">
<span class="address__msg-line--type">详细地址</span>
<el-input
v-model="detailAddress"
type="textarea"
:rows="3"
class="max-ipt"
></el-input>
</div>
<div class="address__msg-line flex flex-middle">
<span class="address__msg-line--type">收货人</span>
<el-input v-model="addressUser" class="min-ipt"></el-input>
<span class="address__msg-line--type">手机号码</span>
<el-input
v-model="addressPhone"
oninput="value=value.replace(/[^\d]/g,'')"
class="min-ipt"
></el-input>
</div>
</article>
<div class="hr" v-if="!isAccount"></div>
<div
class="address__operation flex flex-middle"
:class="!isAccount ? 'flex-column' : ''"
>
<el-checkbox v-model="defaultAddress"></el-checkbox>
<div>
<UiButton
v-if="addressList && addressList.length && !isAccount"
type="grey"
@click="isOperation = false"
>取消</UiButton
>
<UiButton type="yellow_panel" @click="onAddAddress"
>保存地址</UiButton
>
</div>
</div>
</div>
<el-dialog
title="选择地址"
center
:visible.sync="dialogVisible"
width="380px"
>
<div class="address__list">
<section
@click="onAddressItem(item)"
class="address__list--item"
v-for="item in addressList"
:key="item.id"
>
<div>收货人{{ item.name }}</div>
<div>手机号码{{ item.phone }}</div>
<div>收货地址{{ item.detailAddress }}</div>
</section>
</div>
<span slot="footer" class="dialog-footer flex flex-between">
<UiButton type="grey" @click="onCancel"></UiButton>
<UiButton type="yellow_panel">确认</UiButton>
</span>
</el-dialog>
</div>
</template>
<script>
import UiButton from "@/components/UiButton.vue";
import { IsPhone } from "@/plugins/utils/index.js";
import {
ApiGetAddress,
ApiPostAddress,
ApiPutAddress,
} from "@/plugins/api/base";
export default {
components: { UiButton },
props: {
addressData: {
type: Object,
default: () => {},
},
isAccount: {
type: Boolean,
default: false,
},
isEdit: {
type: Boolean,
default: false,
},
},
model: {
prop: "addressData",
event: "emitAddress",
},
data() {
return {
isOperation: false,
dialogVisible: false,
detailAddress: "",
addressUser: "",
addressPhone: "",
defaultAddress: true,
selectData: {
area: {
value: "",
},
city: {
value: "",
},
province: {
value: "",
},
},
addressList: [{}],
curAddressListItem: 0,
editIndex: 0,
};
},
created() {
this.getAddressData();
},
methods: {
async getAddressData() {
let vm = this;
let res = await ApiGetAddress();
if (res.result && res.result.length) {
vm.isOperation = false;
vm.addressList = res.result;
let curIndex = vm.addressList.findIndex((item) => item.isDefault);
vm.curAddressListItem =
curIndex > -1 ? vm.addressList[curIndex] : vm.addressList[0];
vm.$emit("getList", vm.addressList);
vm.$emit("emitAddress", vm.curAddressListItem);
} else {
vm.isOperation = true;
}
console.log("获取收货地址", res);
},
setAddressData(i) {
let vm = this;
vm.editIndex = i;
let editItem = vm.addressList[i];
console.log(editItem);
vm.$set(vm.selectData.area, "value", editItem.area);
vm.$set(vm.selectData.city, "value", editItem.city);
vm.$set(vm.selectData.province, "value", editItem.province);
vm.$set(vm, "detailAddress", editItem.detailAddress);
vm.$set(vm, "defaultAddress", editItem.isDefault);
vm.$set(vm, "addressUser", editItem.name);
vm.$set(vm, "addressPhone", editItem.phone);
},
onSwtich() {
this.dialogVisible = true;
},
onCancel() {
this.dialogVisible = false;
},
onSelected(data) {
console.log(data);
this.selectData = data;
},
async onAddAddress() {
let vm = this;
if (!vm.selectData?.area?.code) {
vm.$message.error("请选择所在区域~");
return;
}
if (!vm.detailAddress) {
vm.$message.error("请输入详细地址~");
return;
}
if (!vm.addressUser) {
vm.$message.error("请输入收货人~");
return;
}
if (!IsPhone(vm.addressPhone)) {
vm.$message.error("请输入收货人手机号码~");
return;
}
let params = {
area: vm.selectData.area.value,
areaCode: vm.selectData.area.code,
city: vm.selectData.city.value,
cityCode: vm.selectData.city.code,
detailAddress: vm.detailAddress,
isDefault: vm.defaultAddress,
name: vm.addressUser,
phone: vm.addressPhone,
province: vm.selectData.province.value,
provinceCode: vm.selectData.province.code,
};
let res;
if (vm.isEdit) {
let editParams = Object.assign(vm.addressList[vm.editIndex], params);
res = await ApiPutAddress(editParams);
} else {
res = await ApiPostAddress(params);
}
if (!res.error) {
vm.$message.success("添加成功!");
vm.getAddressData();
}
console.log(res);
},
onAddressItem(item) {
let vm = this;
vm.curAddressListItem = item;
vm.$emit("emitAddress", item);
vm.dialogVisible = false;
},
},
};
</script>
<style lang="scss" scoped>
.address {
@include layout-box;
min-height: 189px;
border: 1px solid #dddddd;
padding: 28px 34px 8px;
&__msg {
width: 776px;
&-line {
margin-bottom: 20px;
img {
width: 20px;
height: 24px;
margin-right: 14px;
}
&--type {
width: 70px;
font-size: 14px;
font-family: Source Han Sans CN-Regular, Source Han Sans CN;
font-weight: 400;
color: #999999;
}
&--txt {
font-size: 14px;
font-family: Source Han Sans CN-Regular, Source Han Sans CN;
font-weight: 400;
color: #333333;
}
/deep/.ui-button__yellow_panel {
width: 62px;
height: 30px;
color: #ff875b;
background: rgba(255, 135, 91, 0.1);
border-radius: 4px 4px 4px 4px;
margin-left: 54px;
}
.max-ipt {
width: 450px;
}
.min-ipt {
width: 164px;
margin-right: 50px;
/deep/.el-input__inner {
width: 164px;
height: 28px;
background: #ffffff;
border-radius: 2px;
border: 1px solid rgba(0, 0, 0, 0.15);
}
}
}
}
.hr {
width: 2px;
height: 107px;
background: #eeeeee;
}
&__operation {
flex: 1;
/deep/.ui-button__grey {
width: 114px;
font-size: 14px;
font-family: Source Han Sans CN-Regular, Source Han Sans CN;
font-weight: 400;
color: #666666;
&:nth-child(1) {
margin-bottom: 30px;
}
}
.icon {
width: 18px;
height: 18px;
margin-right: 10px;
}
}
}
.address__list {
height: 265px;
overflow-y: auto;
//
overflow-y: auto;
scrollbar-width: none;
&::-webkit-scrollbar {
width: 4px;
}
/*定义滚动条轨道 内阴影+圆角*/
&::-webkit-scrollbar-track {
border-radius: 10px;
background-color: #fff;
}
/*定义滑块 内阴影+圆角*/
&::-webkit-scrollbar-thumb {
border-radius: 10px;
background-color: #eeeeee;
}
&--item {
width: 320px;
height: 120px;
border: 1px solid #dddddd;
margin-bottom: 24px;
}
}
.dialog-footer {
/deep/.ui-button {
width: 150px;
height: 36px;
background: #ffffff;
border-radius: 2px 2px 2px 2px;
border: 1px solid #eeeeee;
font-size: 14px;
font-family: Microsoft YaHei-Regular, Microsoft YaHei;
font-weight: 400;
color: #000;
}
/deep/.ui-button__yellow_panel {
background: #ff875b;
color: #fff;
}
}
/deep/.distpicker-address-wrapper {
select {
width: 164px;
height: 28px;
background: #ffffff;
border-radius: 2px 2px 2px 2px;
border: 1px solid rgba(0, 0, 0, 0.15);
font-size: 14px;
font-family: Microsoft YaHei-Regular, Microsoft YaHei;
font-weight: 400;
color: #333333;
padding: 0;
}
}
.account {
width: 1000px;
}
</style>

@ -0,0 +1,123 @@
<!--
* @Author: ch
* @Date: 2022-05-09 20:20:02
* @LastEditors: ch
* @LastEditTime: 2022-05-12 11:18:03
* @Description: file content
-->
<template>
<el-dialog :visible.sync="myVisible" title="取消订单原因" width="380px">
<ul class="cancel">
<li v-for="item in cancelData" :key="item.value" @click="selected = item.value"
:class="selected == item.value && 'cancel__active'">
{{item.label}}
</li>
</ul>
<UiButton type="yellow_gradual" class="btn" :radius="true" :disabled="!selected"
@click="cancelOrder">确认取消</UiButton>
</el-dialog>
</template>
<script>
import {ApiPutCancelOrder,ApiPutOrderReceive} from '@/plugins/api/order'
import UiButton from '@/components/UiButton.vue';
export default {
components: { UiButton },
props : {
visible : {
type : Boolean,
default : false
},
orderId : {
type : Number
}
},
data(){
return {
selected : '',
cancelData : [
{value : 1, label : '我不想买了'},
{value : 2, label : '地址信息填写错误'},
{value : 3, label : '商品降价'},
{value : 4, label : '商品无货'},
{value : 5, label : '其他'}
]
}
},
computed:{
myVisible : {
get(){
return this.visible;
},
set(val){
this.$emit('update:visible', val)
}
}
},
methods:{
async cancelOrder(){
const ooderId = this.orderId;
const {error, result} = await ApiPutCancelOrder({
cancelReasonType : this.selected,
orderId : ooderId
});
if(error){
this.$message.error(error.message);
return false;
}
this.myVisible = false;
this.$emit('cancel')
//
}
}
}
</script>
<style lang="scss" scoped>
.cancel{
width: 320px;
margin: 0 auto;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
margin-top: 10px;
li{
width: 150px;
height: 36px;
line-height: 36px;
text-align: center;
border: 1px solid #ddd;
cursor: pointer;
margin: 15px 0;
&.cancel__active{
border-color: #FF875B;
color: #FF875B;
}
}
}
.btn{
width: 320px;
height: 36px;
margin-top: 25px;
}
/deep/{
.el-dialog{
border-radius: 4px;
}
.el-dialog__header{
padding: 30px 0 15px;
text-align: center;
}
.el-dialog__title{
font-size: 20px;
font-weight: bold;
}
.el-dialog__body{
padding-top: 0;
padding-bottom: 50px;
}
.el-dialog__headerbtn{
top: 32px;
right: 30px;
}
}
</style>

@ -0,0 +1,265 @@
<template>
<div class="bs-login">
<el-dialog
:visible.sync="dialogTableVisible"
:show-close="false"
:close-on-click-modal="false"
:close-on-press-escape="false"
width="20%"
>
<div class="bs-login-wrap">
<img
class="bs-login-wrap__logo"
src="~/assets/img/login/icon-logo.png"
/>
<div class="bs-login-wrap__content">
<el-form :model="form" :rules="rules" ref="ruleForm">
<el-form-item prop="phone">
<el-input
v-model="form.phone"
placeholder="请输入手机号"
></el-input>
</el-form-item>
<el-form-item prop="verificationCode">
<el-input
class="input-code"
v-model="form.verificationCode"
placeholder="请输入密码"
>
<el-button slot="suffix" type="text" @click="onSendCode">
{{ codeValue }}
</el-button>
</el-input>
</el-form-item>
<el-button class="login-wrap-content__login-btn" @click="onLogin"
>登录</el-button
>
</el-form>
<div class="login-wrap-content__agreement flex felx-start">
<div
class="wrap-content-agreement-icons"
@click="onAgreementSelect"
>
<img
v-if="isAcceptAgreement"
class="icon-choose"
src="~/assets/img/login/icon-accept.png"
/>
<span v-else class="icon-unchoose"></span>
</div>
<span class="wrap-content-agreement__text flex-1">
同意用户协议隐私协议首次 登陆将自动注册
</span>
</div>
</div>
<div class="bs-login-wrap__btn--close" @click="onClose">
<img src="~/assets/img/login/icon-close.png" />
</div>
</div>
</el-dialog>
</div>
</template>
<script>
import { mapState } from "vuex";
import { Message } from "element-ui";
import { ApiGetCode, ApiPostLogin } from "@/plugins/api/account";
import { IsPhone } from "/plugins/utils";
const COUNT_DOWN_TIME = 60; //
export default {
name: "BsLogin",
props: {
visible: {
type: Boolean,
default: false,
},
},
data() {
const validatorPhone = (_, value, callback) => {
if (!value) {
return callback(new Error("手机号不能为空"));
}
if (!IsPhone(value)) {
return callback(new Error("请输入正确的手机号"));
}
return callback();
};
return {
form: {
phone: "",
verificationCode: "",
},
isAcceptAgreement: false, //
countDown: 0, //
rules: {
phone: [{ validator: validatorPhone, trigger: "blur" }],
verificationCode: [
{ required: true, message: "请输入验证码", trigger: "change" },
],
},
};
},
computed: {
...mapState(["token"]),
dialogTableVisible: {
get() {
return this.visible;
},
set(val) {
this.$emit("update:visible", val);
},
},
codeValue() {
return this.countDown ? `${this.countDown}s重新获取` : "获取验证码";
},
},
methods: {
onAgreementSelect() {
this.isAcceptAgreement = !this.isAcceptAgreement;
},
async onSendCode() {
if (this.countDown > 0 || !IsPhone(this.form.phone)) {
return;
}
this.countDown = COUNT_DOWN_TIME;
let time;
const { result, error } = await ApiGetCode({
phone: this.form.phone,
});
if (result) {
time = setInterval(() => {
if (this.countDown === 0) {
clearInterval(time);
return;
}
this.countDown -= 1;
}, 1e3);
return;
}
this.countDown = 0;
clearInterval(time);
Message.error(error.message || "验证码发送失败,请检查手机号是否正确");
},
onLogin() {
this.$refs.ruleForm.validate(async (valid) => {
if (valid) {
if (!this.isAcceptAgreement) {
Message.error("请勾选同意《用户协议》和《隐私协议》");
return;
}
const { result, error } = await ApiPostLogin({
...this.form,
clientId: 1,
systemId: 3,
});
if (error !== null) {
Message.error(
error.message || "登录失败,请检查手机号或验证码是否正确"
);
return;
}
if (result) {
this.dialogTableVisible = false;
this.$store.commit("setToken", result.token);
this.$store.dispatch("getUserInfo");
}
}
});
},
onClose() {
this.dialogTableVisible = false;
},
},
};
</script>
<style lang="scss" scoped>
.bs-login {
/deep/.el-dialog {
border-radius: 4px;
.el-dialog__header {
display: none;
}
.el-dialog__body {
padding: 38px 30px;
}
.bs-login-wrap {
.bs-login-wrap__logo {
width: 198px;
height: 32px;
margin-bottom: 44px;
}
.bs-login-wrap__content {
.el-form {
.el-form-item {
margin-bottom: 24px;
.input-code {
.el-input__inner {
padding-right: 100px;
}
}
.el-input {
.el-input__suffix {
padding-right: 16px;
.el-button {
color: #ff512b;
}
}
.el-input__inner {
border-style: none;
background: #f8f8f8;
border-radius: 4px 4px 4px 4px;
}
}
}
.login-wrap-content__login-btn {
width: 100%;
height: 42px;
border-style: none;
color: #ffffff;
font-size: 16px;
margin-top: 14px;
background: linear-gradient(270deg, #ffa25a 0%, #ff7f39 100%);
border-radius: 4px 4px 4px 4px;
}
}
.login-wrap-content__agreement {
padding: 0 33px;
color: #999999;
font-size: 12px;
margin-top: 25px;
.wrap-content-agreement-icons {
cursor: pointer;
.icon-unchoose {
display: block;
width: 16px;
height: 16px;
border: 1px solid #cccccc;
border-radius: 50%;
}
.icon-choose {
width: 16px;
height: 16px;
}
}
.wrap-content-agreement__text {
display: block;
text-align: center;
margin-left: 8px;
}
}
}
.bs-login-wrap__btn--close {
position: absolute;
left: 50%;
transform: translate(-50%, 0);
bottom: -60px;
cursor: pointer;
img {
width: 30px;
height: 30px;
}
}
}
}
}
</style>

@ -0,0 +1,173 @@
<!--
* @Author: ch
* @Date: 2022-05-08 00:39:50
* @LastEditors: ch
* @LastEditTime: 2022-05-12 11:44:04
* @Description: file content
-->
<template>
<el-dialog title="打开微信扫描付款" width="380px" class="box" center
:visible.sync="myVisible" @open="open" @close="close">
<div class="pay">
<span class="pay--timer">{{timerTxt}}</span>
<UiMoney class="money" sufSize="14px" preSize="14px" size="20px"
float suffix prefix :money="orderInfo.payAmount"/>
<div class="pay--code">
<img :src="imgUrl" v-if="imgUrl"/>
<!-- <p v-if="!timer"></p> -->
</div>
<p class="pay--tips">如支付后没有自动跳转请点击 <span class="pay--finish" @click="finish"></span></p>
</div>
</el-dialog>
</template>
<script>
import {ApiPostPayCdoeImg} from '@/plugins/api/wx'
import {ApiGetOrderDetail} from '@/plugins/api/order'
import UiMoney from './UiMoney.vue'
export default {
components: { UiMoney },
props : {
visible : {
type : Boolean,
default : false
},
orderId : {
type : Number | String,
default : ''
}
},
data(){
return {
orderInfo: {},
imgUrl : '',
timerTxt : '',
startSecondNum : 0,
timerStop : null,
}
},
computed:{
myVisible : {
get(){
return this.visible;
},
set(val){
this.$emit('update:visible', val)
}
}
},
methods : {
open(){
this.getOrderInfo();
this.getCodeImg();
},
/**
* 获取订单最新信息
*/
async getOrderInfo(){
const {error, result} = await ApiGetOrderDetail(this.orderId);
if(error){
this.$message.warning(error.message);
return false;
}
this.orderInfo = result;
if(this.orderInfo.orderStatus === 1){
//
if(this.timerStop){
clearInterval(this.timerStop);
}
this.calcTimerStartSecondNum();
this.timer();
}
},
async getCodeImg(){
const {error, result} = await ApiPostPayCdoeImg({orderId : this.orderId});
if(error){
return false;
}
this.imgUrl = result.dataInfo.codeImgData;
},
/**
* 计算倒计时开始秒数
*/
calcTimerStartSecondNum(){
let expireTime = (new Date(this.orderInfo.expireTime.replace(/-/g,'/'))).getTime(),
curTime = (new Date(this.orderInfo.serverTime.replace(/-/g,'/'))).getTime(),
second = Math.floor((expireTime - curTime) / 1000);
this.startSecondNum = second > 0 ? second : 0;
this.timerStop = setInterval(()=> {
this.timer()
},1000)
},
/**
* 待付款的倒计时
*/
timer(){
if(this.startSecondNum == 0){
this.close();
return;
}
this.startSecondNum--;
let minute = parseInt(this.startSecondNum / 60);
let second = parseInt(this.startSecondNum % 60);
this.timerTxt = `剩余${minute > 0 ? `${minute}` : ''} ${second}`;
},
close(){
clearInterval(this.timerStop);
this.timerStop = null;
this.$emit('cancel');
},
finish(){
this.myVisible = false;
this.$emit('finish')
}
}
}
</script>
<style lang="scss" scoped>
.pay{
text-align: center;
&--code{
width: 160px;
height: 160px;
margin: 15px auto 20px;
}
&--timer{
color: #999;
display: block;
margin-bottom: 15px;
}
&--tips, &--timer{
font-size: 14px;
}
&--finish{
color: #FF512B;
cursor: pointer;
}
}
.money{
color: #FF512B;
font-weight: bold;
}
/deep/{
.el-dialog{
border-radius: 4px;
}
.el-dialog__header{
padding: 30px 0 15px;
}
.el-dialog__title{
font-size: 20px;
font-weight: bold;
}
.el-dialog__body{
padding-top: 0;
padding-bottom: 50px;
}
.el-dialog__headerbtn{
top: 32px;
right: 30px;
}
}
</style>

@ -0,0 +1,57 @@
<!--
* @Author: ch
* @Date: 2022-05-08 14:41:42
* @LastEditors: ch
* @LastEditTime: 2022-05-12 14:37:00
* @Description: file content
-->
<template>
<div class="ui-goods-info" @click="$router.push(`/goods/detail/${goods.productId}`)">
<div class="ui-goods-info--img">
<img :src="goods.productImageUrl || goods.productMainPicture"/>
</div>
<p>
<b>{{goods.productName}}</b>
<span>{{goods.skuDescribe}}</span>
</p>
</div>
</template>
<script>
export default {
props : {
goods : {
type : Object,
default : () => ({})
}
}
}
</script>
<style lang="scss" scoped>
.ui-goods-info{
display: flex;
cursor: pointer;
&--img{
width: 100px;
height: 100px;
border: 1px solid #eee;
}
p{
width: 270px;
margin: 7px 0 0 18px;
text-align: left;
b{
display: block;
line-height: 22px;
margin-bottom: 10px;
overflow: hidden;
text-overflow:ellipsis;
display:-webkit-box;
-webkit-box-orient:vertical;
-webkit-line-clamp:2;
}
span{
color: #999;
}
}
}
</style>

@ -0,0 +1,104 @@
<!--
* @Author: ch
* @Date: 2022-05-09 11:31:29
* @LastEditors: ch
* @LastEditTime: 2022-05-09 15:24:30
* @Description: 按钮每个类型有设置默认高 宽度默认随内容变化有特殊大小需要自定义class控制
props
type 固定按钮类型 红色面板 red_panel 红色描边red_line 黄色面板yellow_panel 黄色线条yellow_line 黄色渐变yellow_gradual 灰色grey
radius 是否圆角 true false
disabled 是否禁用 true false
-->
<template>
<button :class="`ui-button ${myClass}`" @click.stop="click">
<slot></slot>
</button>
</template>
<script>
export default {
props : {
type : {
type : String,
default : 'red_panel'
},
radius: {
type : String | Boolean,
default : false
},
disabled : {
type : Boolean,
default : false
}
},
computed : {
myClass(){
let classStr = this.type ? ` ui-button__${this.type}` : '';
classStr += this.radius ? ` ui-button__radius` : '';
classStr += this.disabled ? ' ui-button__disabled' : '';
return classStr;
}
},
methods : {
click(...args){
if(this.disabled){
return false;
}
this.$emit('click', args);
}
}
}
</script>
<style lang="scss" scoped>
.ui-button{
cursor: pointer;
&__red_panel{
background: #FF512B;
color: #fff;
height: 46px;
font-size: 18px;
padding: 0 30px;
border: none;
font-weight: bold;
}
&__red_line{
border:1px solid #FF512B;
height: 44px;
font-size: 18px;
padding: 0 30px;
color: #FF512B;
}
&__yellow_line{
border:1px solid #FF512B;
height: 28px;
padding: 0 10px;
color: #FF512B;
}
&__yellow_panel{
background:#FF512B;
height: 30px;
padding: 0 10px;
border: none;
color: #fff;
}
&__yellow_gradual{
background: linear-gradient(90deg, #FFA25A 0%, #FF7F39 100%);
height: 30px;
border: none;
color: #fff;
padding: 0 10px;
}
&__grey{
background:#f5f5f5;
border: 1px solid #ccc;
height: 28px;
padding: 0 10px;
}
&__radius{
border-radius: 4px;
}
&__disabled{
opacity: .8;
cursor: not-allowed;
}
}
</style>

@ -0,0 +1,81 @@
<template>
<div class="chosen">
<div class="chosen-title flex flex-between flex-middle">
<h3 class="chosen-title--txt">为你精选</h3>
<div class="chosen-title--btn flex" @click="getRecommendedGoodsList()">
<img src="@/assets/img/goods/each.png" alt="切换推荐" />
<span>换一组</span>
</div>
</div>
<div class="chosen-list">
<UiGoodsItem
:item="item"
v-for="item in recommendedData"
:key="item.id"
></UiGoodsItem>
</div>
</div>
</template>
<script>
import { ApiGetRecommendedGoodsList } from "@/plugins/api/goods";
import UiGoodsItem from "@/components/UiGoodsItem.vue";
export default {
components: { UiGoodsItem },
data() {
return {
recommendedData: [],
};
},
created() {
this.getRecommendedGoodsList();
},
methods: {
async getRecommendedGoodsList() {
let vm = this;
let res = await ApiGetRecommendedGoodsList();
vm.recommendedData = res.result;
},
},
};
</script>
<style lang="scss" scoped>
.chosen {
width: 100%;
padding: 30px 0 40px;
background: #f8f8f8;
&-title {
@include layout-box;
&--txt {
font-size: 24px;
font-family: Microsoft YaHei-Bold, Microsoft YaHei;
font-weight: bold;
color: #333333;
}
&--btn {
width: 140px;
cursor: pointer;
img {
width: 27px;
height: 27px;
margin-right: 5px;
}
span {
font-size: 18px;
font-family: Microsoft YaHei-Regular, Microsoft YaHei;
font-weight: 400;
color: #999999;
}
}
}
&-list {
@include layout-box;
padding-top: 40px;
display: grid;
grid-template-columns: repeat(auto-fill, 232px);
justify-content: space-between;
grid-row-gap: 10px;
}
}
</style>

@ -0,0 +1,83 @@
<template>
<el-dialog
width="16%"
center
:visible.sync="dialogVisible"
:show-close="false"
class="bs-order-ensure"
>
<div class="dialog-content flex flex-middle">
<img src="~/assets/img/common/icon-warning.png" />
<span>{{ title }}</span>
</div>
<div class="dialog-footer flex flex-between">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button class="dialog-footer__btn--ensure" @click="onConfirm"
>确认</el-button
>
</div>
</el-dialog>
</template>
<script>
import UiButton from "./UiButton.vue";
export default {
components: { UiButton },
props: {
visible: {
type: Boolean,
default: false,
},
title: {
type: String,
default: "",
},
},
computed: {
dialogVisible: {
get() {
return this.visible;
},
set(val) {
this.$emit("update:visible", val);
},
},
},
methods: {
onConfirm() {
this.$emit("confirm");
this.dialogVisible = false;
},
},
};
</script>
<style lang="scss" scoped>
/deep/.el-dialog {
.el-dialog__header {
display: none;
}
.el-dialog__body {
padding: 41px 60px;
.dialog-content {
color: rgba(0, 0, 0, 0.8);
font-size: 16px;
padding: 0 2px 42px 2px;
img {
width: 24px;
height: 24px;
margin-right: 17px;
}
}
.dialog-footer {
.el-button {
width: 90px;
height: 30px;
font-size: 14px;
}
.dialog-footer__btn--ensure {
background: #ff875b;
color: #ffffff;
}
}
}
}
</style>

@ -0,0 +1,27 @@
<!--
* @Author: ch
* @Date: 2022-05-09 20:00:57
* @LastEditors: ch
* @LastEditTime: 2022-05-09 20:04:59
* @Description: file content
-->
<template>
<el-dialog :visible="visible" @close="close">
<slot></slot>
</el-dialog>
</template>
<script>
export default {
props: {
visible : {
type : Boolean,
default : false
}
},
methods:{
close(...args){
this.$emit('close', args)
}
}
}
</script>

@ -0,0 +1,37 @@
<!--
* @Author: ch
* @Date: 2022-05-12 10:30:07
* @LastEditors: ch
* @LastEditTime: 2022-05-12 11:01:57
* @Description: file content
-->
<template>
<div class="ui-empty">
<img class="ui-empty--icon" :src="icon"/>
<p class="ui-empty--desc">{{desc}}</p>
<slot></slot>
</div>
</template>
<script>
export default {
props : {
title : String,
desc : String,
icon : String
},
}
</script>
<style lang="scss" scoped>
.ui-empty{
margin-top: 30px;
text-align: center;
padding: 80px 0;
&--icon{
width: 228px;
}
&--desc{
margin: 20px 0;
color: #999;
}
}
</style>

@ -0,0 +1,93 @@
<template>
<div class="goods-item" @click="onItem">
<img class="goods-item__img" :src="item.mainPicture" alt="商品图片" />
<div class="goods-item__title">
<span class="goods-item__title-label" v-if="isLabel(item.labelList)">
{{ getLabel(item.labelList) }}
</span>
{{ item.name }}
</div>
<div class="goods-item__price">
<UiMoney :money="item.startingPrice"></UiMoney>
</div>
</div>
</template>
<script>
import UiMoney from "@/components/UiMoney.vue";
export default {
name: "UiGoodsItem",
componetns: { UiMoney },
props: {
item: {
type: Object,
default: () => {},
},
},
data() {
return {};
},
methods: {
isLabel(arr) {
return arr.some((item) => item.code);
},
getLabel(arr) {
let str = "";
for (let i = 0; i < arr.length; i++) {
if (arr[i].code != "miaosha") {
str = arr[i].text;
break;
}
}
return str;
},
onItem() {
this.$router.push({
path: "/goods/detail/" + this.item.id,
});
},
},
};
</script>
<style lang="scss" scoped>
.goods-item {
width: 232px;
height: 340px;
cursor: pointer;
background: #ffffff;
&__img {
width: 232px;
height: 232px;
}
&__title {
width: 200px;
height: 45px;
line-height: 22px;
margin: 17px auto 10px;
font-size: 14px;
font-family: Microsoft YaHei-Regular, Microsoft YaHei;
font-weight: 400;
color: #333333;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
&-label {
display: inline-block;
padding: 2px 8px;
background: rgba(255, 135, 91, 0.1);
font-size: 12px;
font-family: Microsoft YaHei-Regular, Microsoft YaHei;
font-weight: 400;
color: #ff875b;
text-align: center;
margin-right: 8px;
}
}
&__price {
width: 200px;
margin: 0 auto;
}
}
</style>

@ -0,0 +1,41 @@
<!--
* @Author: ch
* @Date: 2022-05-07 22:40:55
* @LastEditors: ch
* @LastEditTime: 2022-05-10 13:48:39
* @Description: file content
-->
<template>
<div class="ui-line-box">
<div class="ui-line-box--head">
<slot name="head">
<b class="ui-line-box--title">{{title}}</b>
</slot>
</div>
<slot name="body"></slot>
</div>
</template>
<script>
export default {
props : {
title : {
type : String,
default : ''
}
}
}
</script>
<style lang="scss" scoped>
.ui-line-box{
border:1px solid #ddd;
&--head{
height: 42px;
line-height: 42px;
padding: 0 30px;
background: #f8f8f8;
}
&--title{
font-weight: normal;
}
}
</style>

@ -0,0 +1,24 @@
<!--
* @Author: ch
* @Date: 2022-05-12 16:52:52
* @LastEditors: ch
* @LastEditTime: 2022-05-12 17:02:52
* @Description: file content
-->
<template>
<div class="loading" >正在查询数据...</div>
</template>
<script>
export default {
}
</script>
<style lang="scss" scoped>
.loading{
height: 300px;
line-height: 100px;
text-align: center;
color: #666;
}
</style>

@ -0,0 +1,109 @@
<!--
* @Author: ch
* @Date: 2022-05-04 17:44:29
* @LastEditors: ch
* @LastEditTime: 2022-05-10 13:55:12
* @Description: 金额显示UI组件金额和小数点显示大小不同以及需不需要小数点
props
money 金额 数组或字符串
float 是否需要展示小数点
prefix 是否需要把前缀单独提取到一个标签
suffix 是否需要把小数点后缀单独提取到一个标签
preSize 前缀字体大小 Strin 记得带单位 浏览器支持的单位都可以 默认12px
sufSize 小数点字体大小 Strin 记得带单位 浏览器支持的单位都可以 默认12px
size 整数数字体大小 Strin 记得带单位 浏览器支持的单位都可以 默认14px
-->
<template>
<div class="ui-money">
<span v-if="prefix" :style="preStyle" class="ui-money--prefix"></span>
<span :style="style" class="ui-money--price">{{moneyStr}}</span>
<span v-if="float && suffix" :style="sufStyle" class="ui-money--suffix">.{{moneyArr[1]}}</span>
</div>
</template>
<script>
export default {
props : {
money : {
type : Number | String,
default : '0'
},
float : {
type : Boolean,
default : false
},
prefix : {
type : Boolean,
default : false
},
suffix : {
type : Boolean,
default : false
},
sufSize : {
type : String,
default : '12px'
},
preSize : {
type : String,
default : '12px'
},
size : {
type : String,
default : '14px'
}
},
computed : {
sufStyle(){
return {
fontSize : this.sufSize
}
},
preStyle(){
return {
fontSize : this.preSize
}
},
style(){
return {
fontSize : this.size
}
},
moneyStr (){
let priceStr = '',
intNum = this.moneyArr[0],
floatNum = this.moneyArr[1];
//
if(!this.prefix){
priceStr = `${intNum}`;
}else{
priceStr = intNum;
}
//
if(!this.suffix){
priceStr += floatNum ? `.${floatNum}` : '';
}
return priceStr;
},
moneyArr (){
let moneyArr = (this.money || '0').toString().split('.');
// 0 00
if(this.float){
if(!moneyArr[1]){
moneyArr[1] = '00';
}else if(moneyArr[1].length < 1){
moneyArr[1] = `${moneyArr[1]}0`;
}
}
return moneyArr;
}
}
}
</script>
<style lang="scss" scoped>
.ui-money{
font-size: 0;
}
</style>

@ -0,0 +1,77 @@
<!--
* @Description使用请参考https://element.eleme.cn/#/zh-CN/component/pagination
-->
<template>
<div class="bs-pagination flex flex-right">
<el-pagination
background
v-bind="$attrs"
:current-page="currentPage"
:page-size="pageSize"
:layout="layout"
:total="total"
@size-change="$emit('size-change', $event)"
@current-change="$emit('current-change', $event)"
>
</el-pagination>
<slot></slot>
</div>
</template>
<script>
export default {
name: "BsPagination",
props: {
total: {
type: Number,
default: 0,
},
pageSize: {
type: Number,
default: 0,
},
layout: {
type: String,
default: "prev, pager, next, jumper",
},
currentPage: {
type: Number,
default: 0,
},
},
};
</script>
<style lang="scss" scoped>
.bs-pagination {
@include layout-box;
margin-top: 60px;
/deep/.el-pagination {
.btn-prev,
.btn-next {
width: 32px;
height: 32px;
background: #ffffff;
border-radius: 2px;
border: 1px solid rgba(0, 0, 0, 0.2);
}
.el-pagination__jump {
color: #333333;
}
.el-pager {
.number {
width: 32px;
height: 32px;
line-height: 32px;
font-size: 14px;
background: #ffffff;
border: 1px solid rgba(0, 0, 0, 0.2);
border-radius: 2px;
}
.active {
color: #ffffff;
background: #ff512b;
border: none;
}
}
}
}
</style>

@ -0,0 +1,10 @@
<!--
* @Author: ch
* @Date: 2022-05-07 22:57:24
* @LastEditors: ch
* @LastEditTime: 2022-05-07 22:57:39
* @Description: file content
-->
<template>
<el-radio/>
</template>

@ -0,0 +1,34 @@
/**
* 全局常量请避免使用魔法数字
*/
// 用户凭证名
const TOKEN_KEY = "msbPcToken";
// 订单状态
const ORDER_STATUS = {
WAIT_PAY: 1, // 待付款
WAIT_SEND: 3, //待发货
WAIT_RECEIVE: 4, // 待收货
};
// 性别
const SEX_TYPE = {
MALE: 1, // 男
FEMALE: 2, // 女
UNKNOW: 3, // 未知
};
// 热门分类类级
const CATEGROY_LEVEL = {
ONE: 1,
TWO: 2,
};
// 秒杀活动状态
const SECKILL_STATUS = {
NOT_START: 1, // 未开始
GOING: 2, // 进行中
};
export { TOKEN_KEY, ORDER_STATUS, SEX_TYPE, CATEGROY_LEVEL, SECKILL_STATUS };

@ -0,0 +1,54 @@
kind: Deployment
apiVersion: apps/v1
metadata:
labels:
app: $IMAGES
name: $IMAGES
namespace: yanxuan
spec:
progressDeadlineSeconds: 600
replicas: 1
selector:
matchLabels:
app: $IMAGES
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 25%
maxSurge: 25%
template:
metadata:
labels:
app: $IMAGES
spec:
imagePullSecrets:
- name: aliyun-docker-hub
containers:
- image: '$REGISTRY/$DOCKERHUB_NAMESPACE/$IMAGES:$BUILD_NUMBER'
name: app
ports:
- containerPort: $JAR_PORD
protocol: TCP
resources:
limits:
cpu: '0.5'
memory: 500Mi
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
dnsPolicy: ClusterFirst
restartPolicy: Always
terminationGracePeriodSeconds: 30
---
kind: Service
apiVersion: v1
metadata:
name: $IMAGES
namespace: yanxuan
spec:
ports:
- port: 3000
protocol: TCP
targetPort: 3000
selector:
app: $IMAGES
type: ClusterIP

@ -0,0 +1,40 @@
/*
* @Author: ch
* @Date: 2022-05-05 14:40:00
* @LastEditors: ch
* @LastEditTime: 2022-05-07 10:51:53
* @Description: 根据git分支生成对应环境的环境变量
* 开发时如果环境变量换了可以不用重启服务直接运行node env.config.js即可
*/
const fs = require('fs');
const path = require('path');
const getRepoInfo = require('git-repo-info');
const envConfig = {
dev : {
base_url: 'https://k8s-horse-gateway.mashibing.cn'
},
test : {
base_url: 'https://k8s-horse-gateway.mashibing.cn'
},
beta : {
base_url: 'https://you-gateway.mashibing.com'
},
prod : {
base_url: 'https://you-gateway.mashibing.com'
}
}
const branch = getRepoInfo().branch; // 调用获取git信息
let curEnvConfig = null;
const argv = global.process.argv;
for(key in envConfig){
if(argv.includes(`--ENV:${key}`)){
curEnvConfig = envConfig[key];
break;
}
}
if(!curEnvConfig){
curEnvConfig = envConfig.dev;
}
fs.writeFileSync(`${path.resolve(__dirname, './plugins/config')}/env.js`,
`const ENV = ${JSON.stringify(curEnvConfig)}; export default ENV;`);

@ -0,0 +1,22 @@
module.exports = {
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/$1',
'^~/(.*)$': '<rootDir>/$1',
'^vue$': 'vue/dist/vue.common.js'
},
moduleFileExtensions: [
'js',
'vue',
'json'
],
transform: {
'^.+\\.js$': 'babel-jest',
'.*\\.(vue)$': 'vue-jest'
},
collectCoverage: true,
collectCoverageFrom: [
'<rootDir>/components/**/*.vue',
'<rootDir>/pages/**/*.vue'
],
testEnvironment: 'jsdom'
}

@ -0,0 +1,67 @@
<!--
* @Author: ch
* @Date: 2022-05-04 17:56:39
* @LastEditors: ch
* @LastEditTime: 2022-05-08 15:53:49
* @Description: file content
-->
<template>
<div class="layout">
<BsLogin :visible.sync="loginVisible" />
<Header :is-sticky="isSticky" />
<Nuxt />
<Footer />
</div>
</template>
<script>
import BsLogin from "@/components/BsLogin.vue";
import Header from "./module/header/index.vue";
import Footer from "./module/footer/index.vue";
export default {
name: "Layout",
components: { Header, Footer, BsLogin },
data() {
return {
isSticky: false,
ticking: false,
};
},
computed: {
loginVisible: {
get() {
return this.$store.state.loginVisible;
},
set(val) {
this.$store.commit("setLoginVisible", val);
},
},
},
mounted() {
//
window.addEventListener("scroll", this.scrollEventMethod);
},
destroyed() {
window.removeEventListener("scroll", this.scrollEventMethod);
},
methods: {
scrollEventMethod(e) {
const that = this;
//
if (!that.ticking) {
window.requestAnimationFrame(function () {
that.ticking = false;
that.isSticky = window.scrollY > 300;
});
that.ticking = true;
}
},
},
};
</script>
<style lang="scss" scoped>
.layout-footer {
height: 189px;
background: #ddd;
}
</style>

@ -0,0 +1,84 @@
<template>
<div class="layout-footer">
<div class="layout-footer-wrap">
<div class="layout-footer-wrap__promise flex flex-middle flex-between">
<div
v-for="(item, index) in promiseList"
:key="index"
class="wrap-promise-item"
>
<img :src="item.icon" />
<span>{{ item.label }}</span>
</div>
</div>
<div class="layout-footer-wrap__line"></div>
<div class="layout-footer-wrap__address">
© 2020 马士兵北京教育科技有限公司
地址北京市海淀区北三环中路44号4号楼1层114 京ICP备17012835号-1
</div>
</div>
</div>
</template>
<script>
export default {
name: "LayoutFooter",
data() {
return {
promiseList: [
{
label: "马士兵严选",
icon: require("~/assets/img/layout/footer-1.png"),
},
{
label: "马士兵严选",
icon: require("~/assets/img/layout/footer-2.png"),
},
{
label: "马士兵严选",
icon: require("~/assets/img/layout/footer-3.png"),
},
{
label: "马士兵严选",
icon: require("~/assets/img/layout/footer-4.png"),
},
],
};
},
};
</script>
<style lang="scss" scoped>
.layout-footer {
height: 189px;
background: #dddddd;
.layout-footer-wrap {
.layout-footer-wrap__promise {
width: 1060px;
margin: 0 auto;
padding: 25px 0;
.wrap-promise-item {
font-weight: bold;
color: #999999;
font-size: 18px;
margin-left: 20px;
img {
width: 52px;
height: 52px;
}
}
}
.layout-footer-wrap__line {
width: 100%;
height: 1px;
background: #cccccc;
}
.layout-footer-wrap__address {
width: 500px;
margin: 0 auto;
padding: 20px 0;
font-size: 12px;
color: #797979;
text-align: center;
}
}
}
</style>

@ -0,0 +1,263 @@
<template>
<!-- 购物车 -->
<el-popover
popper-class="header-cart-popover"
trigger="hover"
placement="bottom"
width="330"
>
<div
slot="reference"
class="header-cart-popover__refrence flex flex-middle"
@click="onJumpCart"
>
<img src="~/assets/img/layout/icon-shop.png" />
<span>购物车</span>
<div v-if="cartProducts.length > 0" class="wrap-right-cart__tip">
{{ cartProducts.length }}
</div>
</div>
<div class="header-cart-products scrollbar-self">
<div
v-for="item in products"
:key="item.id"
@click="onJumpGoodsDetail"
class="header-cart-products__wrap flex flex-middle flex-between"
>
<div class="cart-products-wrap__left flex felx-middle">
<div class="products-wrap-left__cover">
<img :src="item.productMainPicture" />
</div>
<div class="products-wrap-left__info">
<p class="wrap-left-info__title">{{ item.productName }}</p>
<div class="wrap-left-info__detail flex">
<span class="left-info-detail__skuname">{{
item.productSku.name
}}</span>
<span class="left-info-detail__count">{{ item.number }}</span>
</div>
</div>
</div>
<UiMoney
class="cart-products-wrap__right"
:float="true"
:money="item.product.startingPrice"
/>
</div>
<!-- 失效商品 -->
<template v-if="failureProducts.length > 0">
<div class="header-cart-products__bar">以下商品已失效</div>
<div
v-for="item in failureProducts"
:key="item.id"
@click="onJumpGoodsDetail"
class="header-cart-products__wrap flex flex-middle flex-between"
>
<div class="cart-products-wrap__left flex felx-middle">
<div class="products-wrap-left__cover">
<img :src="item.productMainPicture" />
</div>
<div class="products-wrap-left__info">
<p
class="wrap-left-info__title header-cart-products--failure-color"
>
{{ item.productName }}
</p>
<div class="wrap-left-info__detail flex">
<span class="left-info-detail__skuname">{{
item.productSku && item.productSku.name
}}</span>
<span class="left-info-detail__count">{{ item.number }}</span>
</div>
</div>
</div>
<UiMoney
class="header-cart-products--failure-color"
:float="true"
:money="item.product.startingPrice"
/>
</div>
</template>
</div>
<div class="header-cart-bottom flex flex-middle flex-between">
<p>{{ cartProducts.length }}件商品</p>
<UiButton type="red_panel" :radius="true" @click="onJumCartPage"
>去购物车</UiButton
>
</div>
</el-popover>
</template>
<script>
import { mapState } from "vuex";
import UiButton from "@/components/UiButton.vue";
export default {
name: "HeaderCart",
components: { UiButton },
data() {
return {
products: [],
failureProducts: [], //
};
},
computed: {
...mapState(["cartProducts"]),
},
watch: {
cartProducts: {
immediate: true,
deep: true,
handler(val) {
this.products = [];
this.failureProducts = [];
val.forEach((item) => {
if (item.product.isEnable) {
if (item.productSku && item.productSku.stock > 0) {
//
this.products.push(item);
return;
}
this.failureProducts.push(item);
return;
}
this.failureProducts.push(item);
});
},
},
},
created() {
this.$store.dispatch("getCartProducts");
},
methods: {
onJumpCart() {
if (!this.$isLoginValidate()) {
return;
}
this.$router.push("/cart");
},
onJumpGoodsDetail(id) {
this.$router.push(`/goods/detail/${id}`);
},
onJumCartPage() {
this.$router.push("/cart");
},
},
};
</script>
<style lang="scss">
.header-cart-popover {
padding: 16px 20px;
}
</style>
<style lang="scss" scoped>
.header-cart-popover__refrence {
padding: 0 18px;
height: 42px;
line-height: 42px;
color: #999999;
border-radius: 8px 8px 8px 8px;
border: 1px solid #eeeeee;
cursor: pointer;
.wrap-right-cart__tip {
min-width: 14px;
height: 14px;
padding: 0 3px;
font-size: 10px;
line-height: 14px;
text-align: center;
background: #ff512b;
border-radius: 50%;
color: #ffffff;
}
img {
width: 16px;
height: 16px;
margin: 0 4px 0 10px;
}
}
.header-cart-popover {
padding: 20px 16px;
.header-cart-products {
padding: 0 10px 50px 0;
max-height: 360px;
overflow: auto;
&--failure-color {
color: #999999 !important;
}
.header-cart-products__wrap {
margin-bottom: 20px;
cursor: pointer;
.cart-products-wrap__left {
.products-wrap-left__cover {
width: 54px;
height: 54px;
padding: 3px;
border: 1px solid #eeeeee;
border-radius: 4px;
margin-right: 11px;
img {
width: 100%;
height: 100%;
}
}
.products-wrap-left__info {
font-size: 12px;
color: #999999;
.wrap-left-info__title {
display: block;
width: 120px;
@include ellipsis;
font-size: 14px;
color: #333333;
margin-bottom: 10px;
}
.wrap-left-info__detail {
.left-info-detail__skuname {
display: block;
width: 70px;
@include ellipsis;
}
.left-info-detail__count {
&::before {
content: "X";
font-size: 8px;
}
}
}
}
}
.cart-products-wrap__right {
color: #ff512b;
}
}
.header-cart-products__bar {
font-size: 14px;
width: 298px;
height: 40px;
line-height: 40px;
padding: 0 11px;
background: #f8f8f8;
color: #999999;
margin-bottom: 20px;
}
}
.header-cart-bottom {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
height: 50px;
background: #eeeeee;
padding: 0 16px;
font-size: 14px;
color: #666666;
/deep/.ui-button {
width: 84px;
height: 30px;
font-size: 14px;
font-weight: normal;
padding: 0;
}
}
}
</style>

@ -0,0 +1,219 @@
<template>
<div class="header-category">
<!-- 热门分类 -->
<div
v-show="showCategroyTab"
class="header-box-tab__category"
@mouseenter="handleCategoryChange(true)"
@mouseleave="handleCategoryChange(false)"
>
<div class="tab-category__label flex flex-center flex-middle">
<img src="~/assets/img/layout/icon-category.png" />
<span>热门分类</span>
</div>
<div
v-show="isCategroyOpen || categroyVisible"
class="tab-category__menu flex"
@mouseenter="handleCategoryTwoChange(true)"
@mouseleave="handleCategoryTwoChange(false)"
>
<!-- 左侧一级分类 -->
<div class="tab-category-menu__left">
<div
v-for="item in categroyData"
:key="item.id"
@mouseenter="handleCategoryHover(item.id)"
@click="onCategoryClick(item.id, CATEGROY_LEVEL.ONE)"
class="menu-left__item flex flex-middle"
:class="{
'menu-left__item--light': item.id === currentCategroyId,
}"
>
<img />
<span>{{ item.name }}</span>
</div>
</div>
<!-- 右侧二级分类 -->
<div
v-show="categroyTwoVisible"
class="tab-category-menu__right flex-1"
>
<div
v-for="item in categroyData"
:key="item.id"
@mouseenter="handleCategoryHover(item.id)"
class="category-menu-right__wrap"
:class="{
'category-menu-right__wrap--light': item.id === currentCategroyId,
}"
>
<span
v-for="itemList in item.list"
:key="itemList.id"
class="menu-right-wrap__item"
@click="onCategoryClick(itemList.id, CATEGROY_LEVEL.TWO)"
>{{ itemList.name }}</span
>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import {
ApiGetCategoryOneList,
ApiGetCategoryTwoAndGoods,
} from "@/plugins/api/goods";
const CATEGROY_HIDE_PAGES = [/\/account/]; // tab
export default {
name: "HeaderCategory",
data() {
return {
categroyTwoVisible: false, //
categroyVisible: false, //
currentCategroyId: 0, // id
categroyData: [],
};
},
computed: {
showCategroyTab() {
return !CATEGROY_HIDE_PAGES.some((reg) => {
return reg.test(this.$route.path);
});
},
//
isCategroyOpen() {
return this.$route.path === "/";
},
},
created() {
this.getCategroyData();
},
methods: {
//
async getCategroyData() {
const { result } = await ApiGetCategoryOneList();
if (result && result.length > 0) {
this.categroyData = await Promise.all(
result.map(async (item) => {
const { result: resultGoods } = await ApiGetCategoryTwoAndGoods({
categoryId: item.id,
});
if (resultGoods && resultGoods.length > 0) {
return {
...item,
list: resultGoods,
};
}
})
);
}
},
//
handleCategoryHover(id) {
this.currentCategroyId = id;
},
//
onCategoryClick(id, levelType) {
this.categroyVisible = false;
this.categroyTwoVisible = false;
this.$router.push({
path: "/goods/list",
query: {
id,
levelType,
},
});
},
//
handleCategoryChange(val) {
this.categroyVisible = val;
if (!val) {
this.currentCategroyId = 0;
}
},
//
handleCategoryTwoChange(val) {
this.categroyTwoVisible = val;
},
},
};
</script>
<style lang="scss" scoped>
.header-category {
height: 100%;
.header-box-tab__category {
position: relative;
height: 100%;
.tab-category__label {
width: 190px;
height: 100%;
background: linear-gradient(270deg, #ffa25a 0%, #ff7f39 100%);
border-radius: 4px 4px 0px 0px;
font-weight: bold;
color: #ffffff;
cursor: pointer;
img {
width: 20px;
height: 20px;
margin-right: 16px;
}
}
.tab-category__menu {
position: absolute;
top: 38px;
left: 0;
font-size: 14px;
color: #333333;
.tab-category-menu__left {
width: 190px;
padding: 15px 0;
background: #ffffff;
.menu-left__item {
height: 50px;
cursor: pointer;
padding: 0 24px 0 41px;
&:hover,
&--light {
color: #ff875b;
}
img {
width: 20px;
height: 20px;
margin-right: 16px;
}
}
}
.tab-category-menu__right {
padding: 15px 26px;
box-shadow: 7px 0px 10px 1px rgba(0, 0, 0, 0.1);
border: 1px solid #eeeeee;
background: #ffffff;
.category-menu-right__wrap {
height: 50px;
line-height: 50px;
padding: 0 16px;
font-size: 12px;
color: #999999;
white-space: nowrap;
&:hover,
&--light {
background: #f8f8f8;
}
.menu-right-wrap__item {
color: #999999;
margin-right: 20px;
cursor: pointer;
&:hover {
color: #ff875b;
}
}
}
}
}
}
}
</style>

@ -0,0 +1,275 @@
<template>
<div class="info-bar-header">
<div
class="info-bar-header-wrap layout-center flex flex-between flex-middle"
>
<div class="header-wrap__logo">马士兵严选欢迎你</div>
<div class="header-wrap__content flex flex-middle">
<div class="header-wrap-content__login">
<!-- 已登录 -->
<el-dropdown
v-if="token"
@visible-change="menuVisible = $event"
@command="handleCommandClick"
>
<div
class="wrap-content-login__info flex flex-middle flex-center"
:class="{ 'wrap-content-login__info--hover': menuVisible }"
>
<span>你好{{ userInfo.nickname }}</span>
<img class="content-login-info__logo" :src="menuIcon" />
</div>
<el-dropdown-menu slot="dropdown" class="header-info-bar__dropdown">
<div class="menu-item__wrap flex flex-middle">
<img class="menu-item-wrap__avatar" :src="userInfo.avatar" />
<span>{{ userInfo.nickname }}</span>
</div>
<div class="menu-item__line"></div>
<el-dropdown-item
class="flex flex-between flex-middle"
v-for="item in menuList"
:key="item.value"
:command="item.value"
>
<span> {{ item.label }}</span>
<img src="~/assets/img/layout/icon-arrow.png" />
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<!-- 未登录 -->
<div v-else class="wrap-content-login__text flex">
<span>请先</span>
<span class="content-login-text--light" @click="onLoginClick">
登录/注册
</span>
</div>
</div>
<template>
<div v-if="!menuVisible" class="header-wrap-content--line"></div>
<div
class="header-wrap-content__common flex flex-middle"
@click="onJumpPersonalPage('message')"
>
<img src="~/assets/img/layout/icon-message.png" />
<span class="hover-text">消息</span>
<div v-if="messageCount > 0" class="wrap-content-message__tip">
{{ messageCount }}
</div>
</div>
</template>
<div class="header-wrap-content--line"></div>
<div
class="header-wrap-content__common flex flex-middle"
@click="onJumpPersonalPage('order/list')"
>
<img src="~/assets/img/layout/icon-order.png" />
<span class="hover-text">我的订单</span>
</div>
<div class="header-wrap-content--line"></div>
<!-- 下载app -->
<el-popover
popper-class="header-info-bar__popover"
placement="bottom"
width="230"
trigger="hover"
>
<div
slot="reference"
class="header-wrap-content__common flex flex-middle"
>
<img src="~/assets/img/layout/icon-phone.png" />
<span class="hover-text">下载app</span>
</div>
<div class="info-bar-popover__qrcode">
<img src="~/assets/img/common/app-qrcode.png" />
<span>扫一扫上方二维码 下载APP</span>
</div>
</el-popover>
</div>
</div>
</div>
</template>
<script>
import { mapState } from "vuex";
const MENU_VALUE = {
PERSONAL: 1,
ADDRESS: 2,
LOGON_OUT: 3,
};
export default {
name: "HeaderInfoBar",
data() {
return {
menuVisible: false,
menuList: [
{
label: "个人中心",
value: MENU_VALUE.PERSONAL,
},
{
label: "收货地址",
value: MENU_VALUE.ADDRESS,
},
{
label: "退出登录",
value: MENU_VALUE.LOGON_OUT,
},
],
messageCount: 0, //
};
},
computed: {
...mapState(["userInfo", "token"]),
menuIcon() {
return this.menuVisible
? require("~/assets/img/layout/icon-up-light.png")
: require("~/assets/img/layout/icon-up.png");
},
},
methods: {
onLoginClick() {
this.$isLoginValidate();
},
handleCommandClick(event) {
switch (event) {
case MENU_VALUE.PERSONAL:
this.$router.push("/account/home");
break;
case MENU_VALUE.ADDRESS:
this.$router.push("/account/address");
break;
case MENU_VALUE.LOGON_OUT:
this.$store.dispatch("logout");
}
},
//
onJumpPersonalPage(path) {
if (!this.$isLoginValidate()) {
return;
}
this.$router.push(`/account/${path}`);
},
},
};
</script>
<style lang="scss">
.header-info-bar__dropdown,
.header-info-bar__popover {
.popper__arrow {
display: none;
}
}
</style>
<style lang="scss" scoped>
.header-info-bar__dropdown {
width: 200px;
margin-top: 0 !important;
padding-bottom: 0;
.el-dropdown-menu__item:hover {
background: #f8f9fb;
color: #666666;
}
.el-dropdown-menu__item {
height: 42px;
font-size: 14px;
img {
width: 8px;
height: 14px;
}
}
.menu-item__wrap {
padding: 16px 24px;
font-size: 14px;
.menu-item-wrap__avatar {
width: 50px;
height: 50px;
margin-right: 13px;
border-radius: 50%;
object-fit: cover;
}
}
.menu-item__line {
width: 100%;
height: 1px;
background: #eeeeee;
border-radius: 0px 0px 0px 0px;
}
}
.header-info-bar__popover {
.info-bar-popover__qrcode {
font-size: 14px;
color: #666666;
text-align: center;
img {
width: 186px;
height: 186px;
margin-bottom: 15px;
}
}
}
.info-bar-header {
height: 30px;
color: #999999;
background: #f1f1f1;
border-radius: 0px 0px 0px 0px;
.info-bar-header-wrap {
height: 100%;
@include layout-box;
.header-wrap__content {
.header-wrap-content__login {
.wrap-content-login__text {
padding: 0 18px;
.content-login-text--light {
margin-left: 6px;
color: #ff875b;
cursor: pointer;
}
}
.wrap-content-login__info--hover {
background: #ffffff !important;
color: #ff875b;
}
.wrap-content-login__info {
position: relative;
padding: 0 18px;
height: 30px;
cursor: pointer;
.content-login-info__logo {
width: 8px;
height: 4px;
margin-left: 4px;
}
}
}
.wrap-content-message__tip {
min-width: 14px;
height: 14px;
padding: 0 3px;
line-height: 14px;
font-size: 10px;
color: #ffffff;
text-align: center;
background: #ff512b;
border-radius: 50%;
margin-left: 4px;
}
.header-wrap-content--line {
width: 1px;
height: 10px;
background: #d8d8d8;
}
.header-wrap-content__common {
cursor: pointer;
padding: 0 18px;
img {
width: 14px;
height: 14px;
margin-right: 4px;
}
}
}
}
}
</style>

@ -0,0 +1,334 @@
<template>
<div class="layout-header">
<!-- 滚动吸顶头部 -->
<div v-show="isSticky">
<div
class="sticky-bar-header"
:class="{ 'sticky-bar-header--hide-shadow': hideBarShadow }"
>
<div class="sticky-bar-header__wrap flex flex-middle flex-between">
<div class="flex flex-middle">
<img
class="bar-header-wrap__logo"
src="~/assets/img/layout/logo-sticky.png"
/>
<el-menu
:default-active="tabPath"
mode="horizontal"
@select="onTabSelect"
>
<el-menu-item
v-for="item in tabList"
:key="item.value"
:index="item.value"
>{{ item.label }}</el-menu-item
>
</el-menu>
</div>
<div class="bar-header-wrap__icons flex flex-middle">
<img
src="~/assets/img/layout/icon-search-sticky.png"
@click="$router.push('/goods/list')"
/>
<div class="header-wrap-icons__shop" @click="$router.push('/cart')">
<img src="~/assets/img/layout/icon-shop-sticky.png" />
<span v-if="cartCount > 0" class="">{{ cartCount }}</span>
</div>
<div
v-if="token"
class="header-wrap-icons__login"
@click="$router.push('/account/home')"
>
<img :src="userInfo.avatar" />
</div>
<div
v-else
class="header-wrap-icons__unlogin"
@click="onLoginClick"
>
登录
</div>
</div>
</div>
</div>
</div>
<template>
<HeaderInfoBar />
<div class="default-bar-header">
<div class="bar-header-box">
<div class="bar-header-box__wrap flex flex-between flex-middle">
<img
class="header-box-wrap__logo"
src="~/assets/img/layout/logo.png"
/>
<div class="header-box-wrap__right flex flex-middle">
<div class="box-wrap-right__search flex">
<div class="search-input">
<el-input
v-model="searchContent"
clearable
placeholder="请输入商品名称"
></el-input>
</div>
<div
class="search-icon flex flex-center flex-middle"
@click="onSearch"
>
<img src="~/assets/img/layout/icon-search.png" />
</div>
</div>
<!-- 购物车 -->
<HeaderCart />
</div>
</div>
<div class="bar-header-box__tab flex flex-middle">
<HeaderCategory />
<div
v-for="item in tabList"
:key="item.value"
class="header-box-tab__common flex flex-center flex-middle"
:class="{
'header-box-tab__common--light':
item.value === $nuxt.$route.fullPath,
}"
@click="onTabSelect(item.value)"
>
{{ item.label }}
</div>
</div>
</div>
</div>
<div v-if="!hideBarLine" class="layout-header-line"></div>
</template>
</div>
</template>
<script>
import { mapState } from "vuex";
import { CATEGROY_LEVEL } from "@/constants";
import HeaderInfoBar from "./HeaderInfoBar.vue";
import HeaderCategory from "./HeaderCategory.vue";
import HeaderCart from "./HeaderCart.vue";
export default {
name: "DefaultHeader",
components: { HeaderInfoBar, HeaderCategory, HeaderCart },
props: {
//
isSticky: {
type: Boolean,
default: false,
},
},
data() {
return {
CATEGROY_LEVEL,
searchContent: "",
tabPath: "/",
cartCount: 0, //
cartProductList: [], //
};
},
computed: {
...mapState(["userInfo", "token", "seckillTabVisible"]),
tabList() {
const defaultList = [
{ label: "首页", value: "/" },
{
label: "开发书籍",
value: `/goods/list?id=6&levelType=${CATEGROY_LEVEL.ONE}`,
},
];
if (this.seckillTabVisible) {
return [...defaultList, { label: "限时秒杀", value: "/seckill" }];
}
return defaultList;
},
// tab
hideBarShadow() {
return ["/seckill"].includes(this.$route.path);
},
//
hideBarLine() {
return ["/", "/seckill"].includes(this.$route.path);
},
},
watch: {
"$route.path"(val) {
if (val !== "/goods/list") {
this.searchContent = "";
}
},
},
methods: {
onLoginClick() {
this.$isLoginValidate();
},
onTabSelect(value) {
this.tabPath = value;
this.searchContent = "";
this.$router.push({ path: value });
},
onSearch() {
this.$router.push({
path: "/goods/list",
query: {
keyword: this.searchContent,
},
});
},
},
};
</script>
<style lang="scss" scoped>
.layout-header-popover {
&__cart-content {
}
}
.sticky-bar-header {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 50px;
z-index: 10;
background: #ffffff;
box-shadow: 0px 4px 10px 1px rgba(0, 0, 0, 0.1);
&--hide-shadow {
box-shadow: none;
/deep/.el-menu {
.is-active {
border-bottom: none !important;
}
}
}
.sticky-bar-header__wrap {
@include layout-box;
height: 100%;
.bar-header-wrap__logo {
width: 164px;
height: 28px;
margin-right: 50px;
}
.bar-header-wrap__icons {
cursor: pointer;
img {
width: 24px;
height: 24px;
}
.header-wrap-icons__shop {
position: relative;
margin-left: 14px;
span {
position: absolute;
right: -6px;
top: -4px;
display: block;
height: 14px;
padding: 0 2px;
line-height: 14px;
text-align: center;
background: #ff512b;
font-size: 10px;
color: #ffffff;
border-radius: 50%;
}
}
.header-wrap-icons__login {
margin-left: 47px;
img {
width: 34px;
height: 34px;
border-radius: 50%;
object-fit: cover;
}
}
.header-wrap-icons__unlogin {
font-size: 16px;
color: #909399;
margin-left: 30px;
}
}
/deep/ .el-menu {
height: 50px;
color: #666666;
.is-active {
color: #ff7f39;
border-bottom: 3px solid #ff823c;
}
.el-menu-item:hover {
color: #ff7f39;
}
.el-menu-item {
height: 100%;
line-height: 50px;
font-size: 16px;
margin: 0 30px;
padding: 0 5px;
}
}
}
}
.default-bar-header {
padding-top: 32px;
position: relative;
z-index: 3;
.bar-header-box {
@include layout-box;
background: #ffffff;
.bar-header-box__wrap {
height: 42px;
font-size: 14px;
margin-bottom: 38px;
padding-right: 50px;
.header-box-wrap__logo {
width: 244px;
height: 100%;
}
.header-box-wrap__right {
.box-wrap-right__search {
margin-right: 23px;
.search-input {
width: 551px;
z-index: 1;
/deep/.el-input__inner:focus {
border-color: #ff512b;
}
}
.search-icon {
width: 77px;
margin-left: -2px;
background: linear-gradient(270deg, #ffa25a 0%, #ff7f39 100%);
border-radius: 0px 8px 8px 0px;
z-index: 2;
cursor: pointer;
img {
width: 26px;
height: 26px;
}
}
}
}
}
.bar-header-box__tab {
height: 38px;
.header-box-tab__common--light {
color: #ff7f39 !important;
}
.header-box-tab__common {
width: 160px;
height: 100%;
font-size: 16px;
color: #666666;
cursor: pointer;
}
}
}
}
.layout-header-line {
width: 100%;
height: 2px;
background: #ff875b;
}
</style>

@ -0,0 +1,93 @@
/*
* @Author: ch
* @Date: 2022-05-03 22:14:16
* @LastEditors: ch
* @LastEditTime: 2022-05-10 14:18:39
* @Description: file content
*/
export default {
// Global page headers: https://go.nuxtjs.dev/config-head
head: {
title: 'shop-pc',
htmlAttrs: {
lang: 'zh'
},
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'description', name: 'description', content: '' },
{ name: 'format-detection', content: 'telephone=no' }
],
link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
]
},
router: {
extendRoutes(routes, resolve) {
routes.push({
name: 'custom',
path: '/',
component: resolve(__dirname, 'pages/index/index.vue')
})
}
},
// Global CSS: https://go.nuxtjs.dev/config-css
css: [
'@assets/scss/global.scss',
'element-ui/lib/theme-chalk/index.css'
],
// Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins
plugins: [
'@/plugins/element-ui',
'@/plugins/axios',
'@plugins/axiosTk.js',
'@plugins/vue-inject.js',
'@/plugins/v-distpicker',
],
// Auto import components: https://go.nuxtjs.dev/config-components
components: true,
// Modules for dev and build (recommended): https://go.nuxtjs.dev/config-modules
buildModules: [
],
build: {
vendor: ['v-distpicker']
},
styleResources: {
scss: '@/assets/scss/global.scss'
},
// Modules: https://go.nuxtjs.dev/config-modules
modules: [
'@nuxtjs/axios',
'cookie-universal-nuxt',
'@nuxtjs/style-resources'
],
// Build Configuration: https://go.nuxtjs.dev/config-build
build: {
transpile: [/^element-ui/],
},
axios: {
// 表示开启代理
proxy: true,
},
proxy: {
'/mall/': {
// target: 'http://114.55.64.39:3004', // 目标接口域名
target: 'https://you-gateway.mashibing.com/', // 目标接口域名
pathRewrite: {
changeOrigin: true, // 表示是否跨域
},
},
},
server: {
port: 3000, // default: 3000
host: '0.0.0.0' // default: localhost,
},
}

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

Loading…
Cancel
Save